import json import re import html as html_lib import httpx import logging import time import base64 from config import get_settings settings = get_settings() logger = logging.getLogger(__name__) class AIService: # Shared circuit-breaker timestamp: all instances share one breaker so a # single AI outage trips the breaker globally across every router. _ai_unavailable_until: float = 0.0 def __init__(self): self.api_key = settings.AI_API_KEY self.api_base = settings.AI_API_BASE.rstrip("/") self.model = settings.AI_MODEL self.client = httpx.AsyncClient( base_url=self.api_base, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=httpx.Timeout(connect=10.0, read=120.0, write=15.0, pool=10.0), ) async def _chat(self, messages: list[dict], temperature: float = 0.7, max_tokens: int = 8192) -> str: if time.monotonic() < AIService._ai_unavailable_until: logger.warning("AI provider skipped (circuit breaker active for %.0fs more)", AIService._ai_unavailable_until - time.monotonic()) raise RuntimeError("AI_PROVIDER_UNAVAILABLE") try: response = await self.client.post( "/chat/completions", json={ "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, # glm-5.x is a reasoning model: without disabling # thinking it burns the entire token budget on # chain-of-thought and returns EMPTY content # (finish_reason=length), forcing every feature to # fall back to static templates. Disable for real output. "enable_thinking": False, "thinking": {"type": "disabled"}, }, ) response.raise_for_status() except httpx.ConnectError as exc: # Connection-level failure: server likely down. Short breaker so we retry sooner. logger.error("AI provider connect failed: %s: %s", type(exc).__name__, exc) AIService._ai_unavailable_until = time.monotonic() + 20 raise RuntimeError("AI_PROVIDER_CONNECT_ERROR") from exc except httpx.TimeoutException as exc: # Timeout (connect/read): server slow or unreachable. Short breaker. logger.error("AI provider timeout: %s: %s", type(exc).__name__, exc) AIService._ai_unavailable_until = time.monotonic() + 20 raise RuntimeError("AI_PROVIDER_TIMEOUT") from exc except httpx.HTTPStatusError as exc: # HTTP error (4xx/5xx): server is up but rejected the request. logger.error("AI provider HTTP %s: %s", exc.response.status_code, str(exc.response.text)[:200]) breaker = 30 if exc.response.status_code >= 500 else 120 AIService._ai_unavailable_until = time.monotonic() + breaker raise RuntimeError("AI_PROVIDER_HTTP_ERROR") from exc except Exception as exc: logger.error("AI provider request failed: %s: %s", type(exc).__name__, exc) AIService._ai_unavailable_until = time.monotonic() + 30 raise RuntimeError("AI_PROVIDER_UNAVAILABLE") from exc data = response.json() content = data["choices"][0]["message"]["content"] or "" # Reasoning models may return empty content if thinking consumed the token budget if not content.strip(): logger.warning("AI provider returned empty content (finish=%s, model=%s) - reasoning budget likely exhausted", data["choices"][0].get("finish_reason","?"), self.model) raise RuntimeError("AI_PROVIDER_EMPTY_RESPONSE") finish_reason = data["choices"][0].get("finish_reason", "") if finish_reason == "length": content = content + '..."}' return content async def _chat_json(self, messages: list[dict], temperature: float = 0.3, max_tokens: int = 8192) -> dict: content = await self._chat(messages, temperature, max_tokens) return self._extract_json(content) async def ocr_image(self, image_bytes: bytes, filename: str = "") -> str: """Extract text from an image using the AI model's vision capability.""" if not self.api_key or self.api_key == "change-me-in-production": raise RuntimeError("AI_PROVIDER_UNAVAILABLE") if time.monotonic() < AIService._ai_unavailable_until: raise RuntimeError("AI_PROVIDER_UNAVAILABLE") b64 = base64.b64encode(image_bytes).decode("ascii") ext = "png" lower_name = (filename or "").lower() if lower_name.endswith((".jpg", ".jpeg")): ext = "jpeg" elif lower_name.endswith(".webp"): ext = "webp" elif lower_name.endswith(".gif"): ext = "gif" messages = [ { "role": "user", "content": [ { "type": "text", "text": ( "请仔细识别这张图片中的所有文字内容,包括题目、公式、表格和标注。" "完整提取文字,保持原有结构和层次。" "如果是题目,请保留题号和选项格式。" "数学公式用可读文本表示(例如 a² + b² = c²)。" "只输出识别到的文字内容,不要添加解释或说明。" "如果图片中没有文字或无法识别,请回复:[无法识别文字内容]" ), }, { "type": "image_url", "image_url": {"url": f"data:image/{ext};base64,{b64}"}, }, ], } ] try: response = await self.client.post( "/chat/completions", json={ "model": self.model, "messages": messages, "temperature": 0.1, "max_tokens": 4096, "enable_thinking": False, "thinking": {"type": "disabled"}, }, timeout=httpx.Timeout(connect=10.0, read=90.0, write=15.0, pool=10.0), ) response.raise_for_status() except Exception as exc: logger.error("Vision OCR request failed: %s", exc) AIService._ai_unavailable_until = time.monotonic() + 60 raise RuntimeError("AI_PROVIDER_UNAVAILABLE") from exc data = response.json() return data["choices"][0]["message"]["content"].strip() def _extract_json(self, content: str) -> dict: content = content.strip() # Strip markdown code fences import re as _re content = _re.sub(r'^```\w*\n?', '', content) content = _re.sub(r'\n?```$', '', content) content = content.strip() first_brace = content.find('{') if first_brace == -1: return {"raw_content": content} last_brace = content.rfind('}') if last_brace <= first_brace: return {"raw_content": content} # Try the full span first candidate = content[first_brace:last_brace + 1] try: result = json.loads(candidate) if isinstance(result, dict): return result except json.JSONDecodeError: pass # Try auto-fixing truncated JSON by closing open structures fixed = self._try_fix_truncated_json(candidate) if fixed: return fixed # Try replacing HTML double quotes with single quotes inside the html field fixed2 = _re.sub(r'(style|onclick|onchange)=\\?"([^"]*?)\\?"', r"\1='\2'", candidate) if fixed2 != candidate: try: result = json.loads(fixed2) if isinstance(result, dict): return result except json.JSONDecodeError: pass # Find all '}' positions from end to start pos = last_brace while pos > first_brace: if content[pos] == '}': try: result = json.loads(content[first_brace:pos + 1]) if isinstance(result, dict): return result except json.JSONDecodeError: pass pos -= 1 # Last resort: try to extract html field via regex html_match = _re.search(r'"html"\s*:\s*"((?:[^"\\]|\\.)*)"', content, _re.DOTALL) if html_match: try: html_val = json.loads('"' + html_match.group(1) + '"') return {"title": "动画", "description": "", "html": html_val} except: pass # All attempts failed - save debug info try: with open("debug_json_fail.txt", "w", encoding="utf-8") as f: f.write(f"Content length: {len(content)}\n") f.write(f"First 500 chars:\n{content[:500]}\n\n") f.write(f"Last 500 chars:\n{content[-500:]}\n\n") f.write(f"Full content:\n{content}\n") except: pass return {"raw_content": content} def _try_fix_truncated_json(self, candidate: str) -> dict | None: """Try to fix truncated JSON by closing open strings/brackets.""" import re as _re # Count brackets opens = candidate.count('{') - candidate.count('}') opens_bracket = candidate.count('[') - candidate.count(']') # Check if we're inside a string in_string = False escape = False for ch in candidate: if escape: escape = False continue if ch == '\\': escape = True continue if ch == '"': in_string = not in_string fixed = candidate if in_string: fixed += '' # Close open string if inside one if in_string: fixed += '"' # Close open brackets for _ in range(max(0, opens_bracket)): fixed += ']' for _ in range(max(0, opens)): fixed += '}' try: result = json.loads(fixed) if isinstance(result, dict): # If html field exists, this is usable if 'html' in result: return result except json.JSONDecodeError: pass # Try simpler fix: just close the outermost object if in_string: # Find last complete key-value pair before truncation # Try: add closing quote, skip to end of object for suffix in ['"}', '"}]}', '"}]}', '"]}', '"}' ]: try: result = json.loads(candidate + suffix) if isinstance(result, dict): return result except json.JSONDecodeError: continue return None async def chat_teaching(self, history: list[dict], user_message: str, subject: str = "", grade: str = "") -> str: """General-purpose conversational teaching assistant (multi-turn).""" context_bits = [] if subject: context_bits.append(f"任教学科:{subject}") if grade: context_bits.append(f"教学学段/年级:{grade}") context_line = (";".join(context_bits) + "。") if context_bits else "" system_prompt = ( "你是“智教助手”,一位经验丰富、耐心细致的 K12 全学科教学顾问与备课搭档。" "你的任务是帮助一线教师解决真实的教学问题:备课思路、知识点讲解、活动设计、" "课堂提问、学情诊断、家校沟通、教法选择等。\n" "回答要求:\n" "1. 用简体中文回答,语气专业、亲切、可落地,避免空话套话。\n" "2. 结构清晰,善用标题、要点、表格和示例,必要时分步骤说明。\n" "3. 紧扣中国课程标准和一线课堂实际,给出可直接使用的内容(如完整提问、活动脚本、讲解话术)。\n" "4. 遇到模糊请求时主动追问关键信息(年级、学科、目标、时长)再给出方案。\n" "5. 对学生的易错点、认知难点要有针对性提醒;涉及数值/事实要准确。\n" f"{context_line}" ) messages: list[dict] = [{"role": "system", "content": system_prompt}] for turn in (history or []): role = turn.get("role") content = (turn.get("content") or "").strip() if role in {"user", "assistant"} and content: messages.append({"role": role, "content": content}) messages.append({"role": "user", "content": user_message}) try: return await self._chat(messages, temperature=0.6, max_tokens=8192) except RuntimeError: return self._fallback_chat(history or [], user_message, subject, grade) async def generate_mindmap(self, topic: str, subject: str = "综合", grade: str = "") -> dict: """生成知识思维导图,返回层级节点结构。""" topic = self._repair_text(topic) grade_hint = f",适用学段:{grade}" if grade else "" system_prompt = f"""你是一位资深学科教学专家,擅长梳理知识结构。根据教师给出的主题,生成一份层次清晰、内容专业的知识思维导图。 要求: - 围绕主题梳理 4 到 6 个一级分支(核心知识维度) - 每个一级分支下展开 2 到 4 个二级子节点 - 节点标题用精炼短语(4 到 14 字),不要长句 - 内容要符合学科规范与教学逻辑,覆盖该主题的核心知识脉络(学科:{subject}{grade_hint}) - 如果主题偏小众,也尽量给出合理、可教学的结构 直接输出JSON(不要markdown代码块,不要其他文字): {{"title":"主题名","nodes":[{{"title":"一级分支标题","color":"#00a870","children":[{{"title":"二级子节点标题","children":[{{"title":"可展开的细节要点"}}]}}]}}]}}""" try: result = await self._chat_json([ {"role": "system", "content": system_prompt}, {"role": "user", "content": topic}, ], temperature=0.5, max_tokens=8192) return self._normalize_mindmap_result(result, topic, subject) except RuntimeError: return self._fallback_mindmap(topic, subject) def _normalize_mindmap_result(self, result: dict, topic: str, subject: str) -> dict: topic = self._repair_text(topic) nodes = result.get("nodes") if not isinstance(nodes, list) or not nodes: return self._fallback_mindmap(topic, subject) palette = ["#00a870", "#2563eb", "#d97706", "#db2777", "#7c3aed", "#0891b2", "#4f46e5", "#ea580c"] clean: list = [] for i, branch in enumerate(nodes): if not isinstance(branch, dict) or not str(branch.get("title", "")).strip(): continue color = branch.get("color") or palette[i % len(palette)] children = self._collect_mindmap_children(branch.get("children")) if not children: continue clean.append({"title": str(branch["title"]).strip()[:30], "color": color, "children": children}) if len(clean) < 2: return self._fallback_mindmap(topic, subject) return {"title": str(result.get("title") or topic)[:80], "nodes": clean[:8]} def _collect_mindmap_children(self, raw) -> list: if not isinstance(raw, list): return [] out: list = [] for item in raw: if isinstance(item, dict) and str(item.get("title", "")).strip(): title = str(item["title"]).strip()[:30] sub = self._collect_mindmap_children(item.get("children")) node = {"title": title} if sub: node["children"] = sub out.append(node) elif isinstance(item, str) and item.strip(): out.append({"title": item.strip()[:30]}) return out[:6] def _fallback_chat(self, history: list, user_message: str, subject: str = "", grade: str = "") -> str: """AI 离线时的教学顾问兜底:按意图匹配给出可落地、结构化的教学建议。""" subject = (subject or "").strip() grade = (grade or "").strip() msg = self._repair_text(user_message).strip() ctx_bits = [] if subject: ctx_bits.append(subject) if grade: ctx_bits.append(grade) ctx = ("(" + "·".join(ctx_bits) + ")") if ctx_bits else "" offline_note = ( "\n\n— 当前为离线知识模式(AI 云端未连通),以上为基于教学经验的通用建议;" "如需针对你具体班级、教材版本的个性化方案,请在 AI 服务恢复后再次提问。" ) if any(k in msg for k in ["你好", "您好", "在吗", "谢谢", "感谢", "辛苦"]) and len(msg) <= 12: hi = "你好!我是智教助手,你的教学顾问与备课搭档。" + (f"已记录你的任教学段{ctx}。" if ctx else "") return ( hi + "\n\n我可以帮你:备课与教学设计、知识点讲解思路、课堂活动与提问设计、" "学情诊断与分层教学、家校沟通、复习备考、课堂管理、学生动机激发等。\n\n" "直接告诉我你想解决的教学问题,例如:\n" "- \"如何讲清分数乘法的算理?\"\n" "- \"设计一节《草船借箭》的导入环节\"\n" "- \"班上两极分化严重怎么办?\"\n" "- \"家长群里如何回复投诉?\"" + offline_note ) subj_resp = self._subject_advice(msg, subject, grade) if subj_resp: return subj_resp + offline_note if any(k in msg for k in ["备课", "教学设计", "教案", "教学环节", "教学目标", "怎么设计", "如何设计", "设计一节", "设计这节", "课时方案"]): return self._chat_lesson_prep(ctx) + offline_note if any(k in msg for k in ["怎么讲", "如何讲", "怎么解释", "如何解释", "讲清楚", "讲不清", "学生不懂", "听不懂", "理解不了", "突破难点", "重难点", "算理", "为什么这样", "原理"]): return self._chat_explain(ctx) + offline_note if any(k in msg for k in ["导入", "开场", "引入", "情境", "激发兴趣", "吸引"]): return self._chat_intro(ctx) + offline_note if any(k in msg for k in ["课堂活动", "互动活动", "小组活动", "合作学习", "游戏", "动手", "探究", "角色扮演", "情境表演"]): return self._chat_activity(ctx) + offline_note if any(k in msg for k in ["提问", "追问", "问题设计", "提问技巧", "课堂提问"]): return self._chat_questioning(ctx) + offline_note if any(k in msg for k in ["纪律", "管纪律", "课堂管理", "课堂常规", "调皮", "捣乱", "走神", "注意力", "不专心", "讲话", "吵闹", "坐不住"]): return self._chat_management(ctx) + offline_note if any(k in msg for k in ["学困生", "后进生", "学困", "待优生", "跟不上", "两极分化", "差距大", "分层", "差异化", "因材施教", "培优补差"]): return self._chat_differentiation(ctx) + offline_note if any(k in msg for k in ["家长", "家校", "家访", "家长会", "家长群", "沟通家长", "投诉"]): return self._chat_parents(ctx) + offline_note if any(k in msg for k in ["作业", "布置作业", "评价", "批改", "反馈", "形成性评价", "作业量", "作业设计"]): return self._chat_assessment(ctx) + offline_note if any(k in msg for k in ["不想学", "没兴趣", "没动力", "厌学", "积极性", "不想听", "敷衍", "内驱力", "学习动机"]): return self._chat_motivation(ctx) + offline_note if any(k in msg for k in ["复习", "备考", "期末", "期中", "考试", "复习课", "冲刺", "考点", "应试"]): return self._chat_review(ctx) + offline_note if any(k in msg for k in ["新教师", "新手", "刚入职", "实习", "第一次上课", "新老师", "刚上班"]): return self._chat_newteacher(ctx) + offline_note return self._chat_general(msg, ctx) + offline_note def _subject_advice(self, msg: str, subject: str, grade: str) -> str: """识别学科相关提问,返回学科特色建议;无匹配返回空串。""" is_math = any(k in msg for k in ["数学", "算理", "算式", "方程", "函数", "几何", "分数", "小数", "乘法", "除法", "加减法", "应用题"]) is_chinese = any(k in msg for k in ["语文", "课文", "识字", "写字", "阅读理解", "作文", "古诗词", "古诗", "文言文", "拼音", "草船借箭", "荷塘月色"]) is_english = any(k in msg for k in ["英语", "单词", "语法", "口语", "听力", "时态", "从句", "because"]) is_physics = any(k in msg for k in ["物理", "力学", "电学", "电路", "牛顿", "压强", "浮力", "光学"]) if is_math: return ( "【数学教学建议】\n\n" "一、算理优于算法\n" "数学核心是让学生懂\"为什么\",而非记\"怎么做\"。讲新运算先用实物/画图/数轴把算理可视化(如分数乘法用面积模型),再给法则。\n\n" "二、三讲三练结构\n" "- 讲例题(边讲边写思维过程)→ 练模仿题\n" "- 讲变式(改一个条件/数据)→ 练变式题\n" "- 讲错题(学生典型错误当反面教材)→ 练易错题\n\n" "三、让错误可见\n" "数学最怕假装会。多用板演、白板小测、手势即时暴露思维。错题不只对答案,让学生讲\"我当时怎么想的\"。\n\n" "四、应用题三部曲\n" "读题(圈关键信息)→ 画图/列表(文字转成关系)→ 列式(再算)。低年级卡第一步,高年级卡第二步。\n\n" "告诉我具体课题(如\"分数除法\"\"平行四边形面积\"),我给更细的活动设计。" ) if is_chinese: return ( "【语文教学建议】\n\n" "一、朗读是地基\n" "低中年级把朗读做扎实:教师范读→跟读→指名读→齐读,注意停顿、重音、语气。朗读到位,理解自然跟上。\n\n" "二、课文\"三问法\"\n" "- 写了什么?(整体感知)\n" "- 怎么写的?(品词析句、写法)\n" "- 为什么这样写?(情感、主旨)\n\n" "三、作文从\"有话写\"到\"写得好\"\n" "先解决没素材:多搞观察课、活动作文。再解决写不具体:训练动作分解(把\"他跑了\"拆成 5 个动作)。最后才润色文采。\n\n" "四、识字/字词\n" "字理识字(讲字源)、归类识字(同偏旁)、语境识字(放词句里记),比机械抄写有效。\n\n" "告诉我具体课文或课型,我给针对性设计。" ) if is_english: return ( "【英语教学建议】\n\n" "一、输入先行(i+1 原则)\n" "新语言点先大量听/看→理解,再要求说/写。用 TPR、图片、情境让学生先懂意思,不要一上来就机械跟读。\n\n" "二、词不离句,句不离境\n" "单词放进有意义的句子里记,句子放进情境里用。避免单词—中文翻译的孤立记忆,那样学生只会背不会用。\n\n" "三、PPP 模式\n" "Presentation(呈现)→ Practice(控制练习)→ Production(自由产出)。第三步最常被省略,却最关键——要给真实任务去用语言。\n\n" "四、让沉默期变短\n" "不爱开口的学生:先做非语言回应(指、选、画),再重复性输出(跟读),最后创造性输出。降低开口焦虑。\n\n" "告诉我具体单元/课型,我给细化方案。" ) if is_physics: return ( "【物理教学建议】\n\n" "一、实验是物理的灵魂\n" "能做实验绝不只讲实验。演示实验要让现象出乎意料(引发认知冲突),分组实验让每个人动手。器材不足用生活物品替代(矿泉水瓶测压强)。\n\n" "二、从现象到规律四步\n" "观察现象 → 提出问题 → 猜想/设计实验 → 得出规律。规律不要直接给,让学生发现。\n\n" "三、建模与数学化\n" "讲公式时一定讲清每个字母的物理意义和单位,强调公式是规律的语言,而非要背的符号。\n\n" "四、典型误区预防\n" "力与运动(惯性误解)、电流方向、浮力沉浮条件……把这些高频误区编成判断题课前测,对症讲解。" ) return "" def _chat_lesson_prep(self, ctx: str) -> str: return ( f"关于备课,建议你按\"目标—学情—活动—评价\"四步设计{ctx}:\n\n" "一、明确可检验的教学目标(最关键)\n" "用\"学生能做什么 + 什么条件下 + 达到什么程度\"来写。例:学生能在 5 分钟内独立完成 3 道两位数乘法,正确率≥80%。\n" "避免\"让学生理解…\"\"培养…能力\"这类无法检测的目标。一节课 2-3 个核心目标即可。\n\n" "二、诊断学情\n" "这节课学生已有什么基础?常见误区是什么?课前用 2 道前测题摸底,或翻看上节课作业的典型错误。\n\n" "三、设计主干活动(建议 3 环节)\n" "- 导入(3-5 分钟):旧知、生活情境或认知冲突引入。\n" "- 新知建构(15-20 分钟):不要直接给结论,设计让学生发现的活动——观察、操作、讨论、归纳。\n" "- 巩固迁移(10-15 分钟):模仿→变式→应用,难度阶梯上升。\n\n" "四、嵌入评价(防止假装听懂)\n" "每个环节末尾设一个检测点:提问、小测、板演、同伴互评,让你随时知道学生学到哪了。\n\n" "时间提醒:教师讲授总时长控制在 15 分钟内,把时间还给学生练与思。\n\n" "告诉我具体课题和年级,我给完整的环节脚本与活动设计。" ) def _chat_explain(self, ctx: str) -> str: return ( f"讲清一个难点,核心是搭台阶 + 可视化 + 证伪误区{ctx}:\n\n" "一、先找到学生的卡点\n" "学生不懂通常卡在某一个具体环节,不是整体不懂。问自己:这个知识依赖哪个旧知?哪个概念是转折点?把卡点定位到最小单元。\n\n" "二、搭台阶(最近发展区)\n" "把难点拆成 3-4 个递进小问题,每个学生能答上来,最终自己推出结论。例:讲分数除法→先复习分数意义→再问除以一个数=乘它倒数为什么成立→用面积图验证。\n\n" "三、可视化(多通道呈现)\n" "能画图就画图,能摆实物就摆实物。抽象概念配具象表征(数轴、面积模型、流程图、表格),让学生看见关系。\n\n" "四、用错误反证(认知冲突)\n" "先抛一个看起来对其实是错的判断,让多数学生踩坑,再一起分析为什么错——比直接讲正确结论印象深 3 倍。\n\n" "五、让学生复述/教别人\n" "讲完别问懂了吗(学生都会点头),而让 1-2 个学生用自己的话复述,或讲给同桌听。能讲清楚才是真懂。\n\n" "把具体知识点告诉我,我帮你拆台阶、设计冲突问题。" ) def _chat_intro(self, ctx: str) -> str: return ( f"好的导入能在 3-5 分钟抓住学生,常用 5 种方式{ctx}:\n\n" "1. 情境导入:用学生熟悉的生活场景切入(买菜算账、校园事件、新闻热点),让知识有用。\n" "2. 冲突导入:抛一个反直觉的现象或问题(如:1千克铁和1千克棉花谁重?),引发好奇。\n" "3. 悬念导入:讲半截故事/留个谜,答案藏在今天的内容里。\n" "4. 旧知导入:从上节课内容自然延伸,温故知新。\n" "5. 操作导入:动手做个小实验/小游戏,身体先参与,注意力立刻集中。\n\n" "避坑:导入别超过 5 分钟,别为热闹而热闹——导入必须直接服务于本课目标。\n\n" "告诉我课题,我帮你设计 1-2 个具体导入脚本。" ) def _chat_activity(self, ctx: str) -> str: return ( f"设计有效课堂活动,关键是任务清晰 + 人人有事做 + 及时反馈{ctx}:\n\n" "常用活动类型\n" "- 小组讨论:4 人一组,问题要有争议性/开放性,讨论前给 1 分钟独立思考,避免搭便车。\n" "- 角色扮演:语文/英语/历史特别适合,提前给角色卡和台词框架。\n" "- 操作探究:数学/科学课用学具、实验,让学生做出结论。\n" "- 游戏竞赛:抢答、卡片配对、知识闯关,适合巩固环节,注意控制节奏。\n" "- 同伴互教:会的学生教不会的,教的人反而学得更深。\n\n" "让活动不失控的 3 个原则\n" "1. 活动前讲清规则和产出(讨论结束每组派代表说 1 条),黑板上挂任务。\n" "2. 活动中教师巡视、记录、点拨,不站在讲台。\n" "3. 活动后必须汇报/展示/点评,否则学生会觉得讨论了没用。\n\n" "告诉我学科和课型,我帮你设计一个具体活动。" ) def _chat_questioning(self, ctx: str) -> str: return ( f"高质量课堂提问,记住三秒原则 + 追问 + 全员应答{ctx}:\n\n" "一、提问前的设计\n" "备课时写下 3-5 个核心问题,分三类:\n" "- 记忆型(是什么)→ 唤醒旧知\n" "- 理解型(为什么/怎么说)→ 促思考\n" "- 应用/评价型(如果…会怎样/你同意吗)→ 促迁移\n" "一节好课,后两类要占多数。\n\n" "二、提问技巧\n" "- 先问后叫人:先抛问题,停 3-5 秒(让所有人想),再随机点名。\n" "- 追问:学生答完,追问你怎么想到的?还有别的可能吗?把思维引向深处。\n" "- 不轻易否定:错误答案也是资源,追问这个想法哪里有问题,让全班分析。\n" "- 全员应答:用白板、手势、答题卡、随机抽签,让每个学生被迫回应。\n\n" "避坑:别只问对不对/是不是;别一问完立刻自己答。\n\n" "把要讲的课题告诉我,我帮你设计一组阶梯式提问。" ) def _chat_management(self, ctx: str) -> str: return ( f"课堂纪律靠规则前置 + 节奏控制 + 关系先行,而非吼叫{ctx}:\n\n" "一、预防优于纠正\n" "- 开学第一周把课堂常规立清楚(举手发言、不插嘴、安静信号),坚持执行。\n" "- 用固定的安静信号(拍手节奏、举手倒数、手势),比喊安静有效。\n\n" "二、走神/讲话:用教学手段拉回,而非批评\n" "- 突然停顿 3 秒(全班会安静看你)。\n" "- 把走神学生的名字编进例题(假设小明买了 3 个本子…),温和提醒。\n" "- 走下讲台站到他旁边继续讲,距离本身就是管理。\n\n" "三、调皮/对抗:私下处理,保护自尊\n" "- 课堂上不当众发作(易激化),课后单独谈。\n" "- 谈话用我观察到…+ 我担心…+ 我希望…句式,而非指责。\n" "- 找到行为背后的需求(求关注?听不懂?家庭问题?),对症下药。\n\n" "四、根本:让课足够有趣 + 师生关系好\n" "纪律问题 80% 来自课堂太无聊或学生不喜欢老师。把课上精彩、多关心学生,纪律自然好转。\n\n" "具体场景(哪个年级、什么行为)告诉我,我给针对性策略。" ) def _chat_differentiation(self, ctx: str) -> str: return ( f"应对两极分化/学困生,核心是分层不减负、抓两头带中间{ctx}:\n\n" "一、分层不分班(课内分层)\n" "- 目标分层:同一节课,学困生只要达成基础目标,学优生有挑战任务。\n" "- 任务分层:布置 A(基础)/B(提高)/C(拓展)三档作业,学生选做或指定。\n" "- 提问分层:简单问题给学困生(建立信心),难题给学优生。\n\n" "二、补差要抓前置基础\n" "学困生卡住往往是旧知识漏洞,不是这节课听不懂。诊断他缺哪块基础,用课前 10 分钟或课后针对性补。\n\n" "三、同伴互助(最省力)\n" "小老师制:1 个学优生帮 1 个学困生,捆绑考核(两人都进步才表扬)。教的人学得更深,被教的人压力小、敢问。\n\n" "四、保护学困生自尊\n" "- 永远不当众羞辱(这么简单都不会是毒药)。\n" "- 抓住任何小进步公开表扬,建立我能学的信念。\n" "- 错题私下反馈,不公布排名。\n\n" "五、培优要给空间\n" "学优生最怕吃不饱被打发。给自主探究任务、当小老师、参加竞赛,让能量有出口。\n\n" "具体学科和年级告诉我,我帮你设计分层任务清单。" ) def _chat_parents(self, ctx: str) -> str: return ( f"家校沟通关键是先共情 + 讲事实 + 给方案,而非告状{ctx}:\n\n" "一、沟通前心态\n" "家长不是对手,是同盟。即使投诉的家长,本质也是为孩子好。先听他说完,别急着辩解。\n\n" "二、报问题的标准话术(三明治法)\n" "1. 先说一个孩子的优点/进步(让家长放下防备)。\n" "2. 客观陈述问题(只讲事实,不带评价:最近三次作业有两次没交,而非这孩子太懒)。\n" "3. 表达关心 + 给具体方案(我担心影响他后续学习,咱们一起想想办法,我建议…您看行吗?)。\n\n" "三、家长群沟通禁忌\n" "- 不在群里点名批评个别学生(保护隐私)。\n" "- 不在群里跟家长争论(私聊处理)。\n" "- 通知类信息简洁清晰,避免长语音。\n" "- 投诉先回应已收到,X 点前给您答复,别晾着。\n\n" "四、家访/约谈\n" "- 提前预约,不突袭。\n" "- 先表扬再谈问题,结束时达成 1-2 个可执行小目标。\n" "- 记录谈话要点,下次跟进。\n\n" "具体场景(投诉什么/孩子什么问题)告诉我,我帮你写一段沟通话术。" ) def _chat_assessment(self, ctx: str) -> str: return ( f"作业与评价的核心是少而精 + 即时反馈 + 促学而非排名{ctx}:\n\n" "一、作业设计原则\n" "- 少而精:3 道有思维含量的题 > 30 道机械重复。\n" "- 分层:基础题全员必做,挑战题选做,避免学困生抄、学优生闲。\n" "- 形式多样:除书面外,加口头、实践、项目作业(如用本周学的统计调查家里一周用电量)。\n" "- 必批必改必反馈:不批的作业等于没布置;批改后要讲评典型错题。\n\n" "二、形成性评价(重过程)\n" "不只看期末考试。日常用:课堂提问记录、小测、作品集、同伴互评,让评价贯穿学习全过程。\n\n" "三、反馈的艺术\n" "- 具体:第二步符号写反了 > 粗心。\n" "- 可改进:指出下一步怎么做,而非只判对错。\n" "- 及时:当天/次日反馈,隔一周就失效。\n" "- 成长导向:比上次进步了比 85 分更有激励作用。\n\n" "具体学科和题型告诉我,我帮你设计一份分层作业。" ) def _chat_motivation(self, ctx: str) -> str: return ( f"激发学习动机,从胜任感 + 自主感 + 归属感入手{ctx}:\n\n" "一、胜任感:让他尝到成功的甜头\n" "没兴趣的学生,往往因为长期学不会而放弃。给他够得着的小目标,让他体验我会了——一次小测进步、一道题做对,立刻肯定。信心是兴趣的前提。\n\n" "二、自主感:给他选择权\n" "人对被命令的事天然抗拒。给有限选择:今天作业 A 还是 B?小组讨论你想当记录员还是汇报员?参与感提升投入度。\n\n" "三、归属感:让他感到被看见\n" "记住每个学生的名字和一件小事,课堂上让每个人都有发言机会。学生因为喜欢这个老师而喜欢这门课是真实存在的。\n\n" "四、让知识有用 + 有趣\n" "- 联系生活/热点/学生关心的事,让知识活起来。\n" "- 用游戏、竞赛、悬念,让过程有乐趣。\n" "- 慎用外在奖励:物质奖励/加分短期有效,长期会削弱内驱力。多用成长反馈、公开认可。\n\n" "五、排查外部原因\n" "突然厌学,先了解:家庭变故?同伴关系?沉迷手机?身体/心理问题?对症才能解。\n\n" "具体是哪个学生/什么表现告诉我,我给针对性方案。" ) def _chat_review(self, ctx: str) -> str: return ( f"复习备考要结构化 + 错题驱动 + 模拟实战,而非题海{ctx}:\n\n" "一、先建知识结构(脑图)\n" "复习不是把新课重讲一遍。先带学生画整章/整册知识脑图,理清脉络,让零散知识串成网。结构清晰,提取才快。\n\n" "二、错题驱动(最高效)\n" "把全学期高频错题/易错点整理成专题,针对性突破。会的题不重复刷,时间留给真问题。建议每个学生建错题本,定期重做。\n\n" "三、三轮复习节奏\n" "- 第一轮:单元过关,扫清知识盲点(重基础)。\n" "- 第二轮:专题整合,跨章节串联(重综合)。\n" "- 第三轮:模拟实战,限时训练(重应试与心态)。\n\n" "四、模拟考试要仿真\n" "限时、独立、评分标准对齐正式考试。考后不只对答案,要分析:哪类题失分?知识漏洞还是应试失误?制定针对性补救。\n\n" "五、别忘了心态\n" "考前焦虑的学生很多。适当减压,强调尽力就好,别把分数和人格绑定。\n\n" "具体学科和学段告诉我,我帮你列复习专题清单。" ) def _chat_newteacher(self, ctx: str) -> str: return ( f"新手教师上路,先稳住课堂 + 关系 + 心态三件事{ctx}:\n\n" "一、第一年别追求花哨,先求稳\n" "- 把常规立住(纪律、作业、发言规则),课堂乱什么都做不好。\n" "- 备课写详案(每环节说什么、问什么、学生可能怎么答),别只写框架。\n" "- 多听老教师的课,模仿是最好的学习。\n\n" "二、师生关系:严慈相济\n" "- 开学先立威(规则严、说到做到),再慢慢施恩(关心、幽默、认可)。顺序反了很难管。\n" "- 记住每个学生名字,课后多聊天。被看见的学生才会配合你。\n" "- 永远不当众发火/羞辱学生,一次就可能毁掉整学期关系。\n\n" "三、教学:少讲多练,别贪多\n" "- 一节课 2-3 个核心点足够,讲透练透比走马观花强。\n" "- 教师讲授控制在 15 分钟内,剩下时间给学生。\n" "- 每节课留 5 分钟检测,知道自己教得怎么样。\n\n" "四、心态:别和名师比,和自己比\n" "- 第一年上不好很正常,三年才成型,五年才出风格。\n" "- 别把所有问题归咎于自己(学生基础、家庭都是变量)。\n" "- 找一个愿意带你/听你课的师傅,成长快 3 倍。\n\n" "具体困惑(管不住班?备课没底?家长沟通?)告诉我,我针对性支招。" ) def _chat_general(self, msg: str, ctx: str) -> str: return ( f"我收到了你的问题。为了让建议更精准,先按问题—对象—目标帮你理一理{ctx}:\n\n" "一、你想解决的核心问题是什么?\n" "比如:备课没思路?某个知识点学生听不懂?课堂纪律乱?学生没兴趣?家长难沟通?复习效率低?\n\n" "二、针对的对象和情境?\n" "学科、年级、班级特点(人数、两极分化程度)、这节课/这个学生的具体情况。\n\n" "三、你期望达成的目标?\n" "想要一份完整教案?一个活动设计?一段沟通话术?一个分层作业?还是某个具体问题的解决思路?\n\n" "把这些信息补充给我,我能给出可直接用的方案。你也可以直接试试这些常见问题:\n" "- 如何讲清[某知识点]?\n" "- 设计一节[某课题]的导入/活动\n" "- 班上两极分化怎么办?\n" "- 学生上课走神怎么管理?\n" "- 如何回复家长的投诉?" ) def _fallback_mindmap(self, topic: str, subject: str) -> dict: topic = self._repair_text(topic) title = topic[:24] or "思维导图" for keys, nodes in self._mindmap_kb(): if any(k in topic for k in keys): return {"title": title, "nodes": nodes} return {"title": title, "nodes": self._mindmap_subject_template(subject)} # ---- 主题感知思维导图知识库 ---- _MM_COLORS = ["#00a870", "#2563eb", "#d97706", "#db2777", "#7c3aed", "#0891b2"] def _mm_nodes(self, *branches): out = [] for i, (title, kids) in enumerate(branches): out.append({"title": title, "color": self._MM_COLORS[i % len(self._MM_COLORS)], "children": [{"title": k} for k in kids]}) return out def _mindmap_kb(self): mm = self._mm_nodes # 历史 if False: pass kb = [ (["朝代", "历代", "王朝", "中国古代史"], mm( ("先秦文明", ["夏商周更替", "春秋五霸", "战国七雄"]), ("秦汉大一统", ["秦始皇统一", "汉武帝强盛", "丝绸之路"]), ("隋唐盛世", ["隋朝开科", "贞观之治", "开元盛世"]), ("宋元变革", ["宋代理学", "元朝行省制", "四大发明"]), )), (["丝绸之路"], mm( ("陆上丝路", ["张骞出使西域", "长安出发", "途经河西走廊"]), ("海上丝路", ["泉州广州港口", "瓷器丝绸外销", "郑和下西洋"]), ("贸易商品", ["丝绸", "瓷器", "茶叶", "香料"]), ("文化交流", ["佛教东传", "四大发明西传", "中外互通"]), )), (["工业革命"], mm( ("第一次工业革命", ["蒸汽机", "纺织业机械化", "铁路交通"]), ("第二次工业革命", ["电力", "内燃机", "化学工业"]), ("社会影响", ["城市化", "阶级变化", "殖民扩张"]), ("科技进步", ["生产力飞跃", "世界市场形成", "环境污染"]), )), (["抗日战争", "抗战"], mm( ("局部抗战", ["九一八事变", "东北沦陷", "义勇军抗争"]), ("全面抗战", ["七七事变", "正面战场", "敌后战场"]), ("重大战役", ["台儿庄", "百团大战", "平型关"]), ("伟大胜利", ["统一战线", "世界反法西斯", "日本投降"]), )), (["文艺复兴"], mm( ("兴起背景", ["意大利城邦", "资本主义萌芽", "人文主义"]), ("代表人物", ["但丁", "达芬奇", "莎士比亚"]), ("核心思想", ["以人为本", "个性解放", "反对神权"]), ("深远影响", ["思想解放", "科学革命", "宗教改革"]), )), (["辛亥革命"], mm( ("革命背景", ["清末危机", "民族资本主义", "革命思想传播"]), ("革命过程", ["武昌起义", "各省响应", "建立民国"]), ("历史意义", ["推翻帝制", "民主共和", "思想解放"]), ("局限性", ["革命不彻底", "军阀割据", "任务未完成"]), )), # 地理 (["气候", "气候类型", "气温降水"], mm( ("影响因子", ["纬度", "海陆", "地形", "洋流"]), ("主要类型", ["热带雨林", "季风气候", "温带大陆性", "地中海气候"]), ("降水规律", ["赤道多雨", "副热带少雨", "温带增多"]), ("中国气候", ["季风显著", "雨热同期", "气候复杂多样"]), )), (["地形", "地貌", "地势"], mm( ("基本地形", ["山地", "平原", "高原", "盆地", "丘陵"]), ("中国地势", ["西高东低", "阶梯分布", "大河东流"]), ("外力作用", ["流水侵蚀", "风力沉积", "冰川作用"]), ("典型地貌", ["喀斯特", "丹霞", "黄土高原", "冲积平原"]), )), (["河流", "水系", "长江", "黄河"], mm( ("长江", ["发源青藏", "流经十一省", "入东海", "黄金水道"]), ("黄河", ["发源巴颜喀拉", "泥沙含量大", "地上河", "入渤海"]), ("河流作用", ["供水灌溉", "航运发电", "塑造平原"]), ("治理保护", ["防洪堤坝", "水土保持", "生态修复"]), )), (["地球", "地球运动", "自转公转"], mm( ("自转", ["绕轴旋转", "昼夜交替", "时差"]), ("公转", ["绕日运行", "四季更替", "五带划分"]), ("黄赤交角", ["23.5度", "太阳直射点移动", "昼夜长短变化"]), ("地理意义", ["正午太阳高度", "节气", "气候形成"]), )), # 生物 (["细胞"], mm( ("细胞结构", ["细胞膜", "细胞质", "细胞核", "细胞壁(植物)"]), ("细胞器", ["线粒体", "叶绿体", "核糖体", "液泡"]), ("细胞分裂", ["有丝分裂", "减数分裂", "无丝分裂"]), ("细胞分化", ["形态变化", "功能特化", "形成组织"]), )), (["光合作用"], mm( ("反应条件", ["光照", "叶绿体", "适宜温度"]), ("原料产物", ["二氧化碳", "水", "氧气", "有机物"]), ("两个阶段", ["光反应", "暗反应", "能量转化"]), ("重要意义", ["制造有机物", "释放氧气", "碳氧平衡"]), )), (["生态系统"], mm( ("组成成分", ["非生物物质", "生产者", "消费者", "分解者"]), ("食物链网", ["捕食关系", "能量单向", "物质循环"]), ("能量流动", ["太阳能输入", "逐级递减", "10%-20%传递"]), ("生态平衡", ["自我调节", "稳定性", "人类影响"]), )), (["遗传", "基因", "DNA"], mm( ("遗传物质", ["DNA", "基因", "染色体"]), ("基本规律", ["分离定律", "自由组合", "显隐性"]), ("遗传变异", ["基因突变", "基因重组", "染色体变异"]), ("应用拓展", ["育种", "遗传咨询", "基因工程"]), )), (["人体", "消化", "呼吸", "循环"], mm( ("消化系统", ["口腔胃小肠", "消化酶", "吸收营养"]), ("呼吸系统", ["呼吸道", "肺泡", "气体交换"]), ("循环系统", ["心脏", "血管", "血液循环"]), ("神经系统", ["大脑", "神经传导", "反射调节"]), )), # 化学 (["元素周期表", "元素"], mm( ("结构规律", ["周期", "族", "原子序数", "递变规律"]), ("主族元素", ["碱金属", "卤素", "氧族", "氮族"]), ("周期变化", ["原子半径递减", "金属性减弱", "非金属性增强"]), ("应用价值", ["预测性质", "寻找新材料", "指导分类"]), )), (["化学反应"], mm( ("反应类型", ["化合", "分解", "置换", "复分解"]), ("反应条件", ["加热", "点燃", "催化", "通电"]), ("能量变化", ["放热反应", "吸热反应", "化学能转化"]), ("方程式", ["质量守恒", "配平", "符号规范"]), )), (["酸碱盐", "酸碱"], mm( ("常见酸", ["盐酸", "硫酸", "硝酸", "醋酸"]), ("常见碱", ["氢氧化钠", "氢氧化钙", "氨水"]), ("酸碱反应", ["中和反应", "生成盐和水", "pH变化"]), ("盐的性质", ["溶解性", "复分解", "焰色反应"]), )), (["原子", "分子", "物质构成"], mm( ("原子结构", ["原子核", "质子", "中子", "核外电子"]), ("分子构成", ["共价键", "分子间作用力", "分子特性"]), ("离子化合物", ["得失电子", "离子键", "晶体结构"]), ("物质分类", ["单质", "化合物", "混合物"]), )), # 物理 (["力学", "力", "牛顿"], mm( ("常见的力", ["重力", "弹力", "摩擦力", "浮力"]), ("牛顿三定律", ["惯性定律", "加速度定律", "作用反作用"]), ("压强", ["固体压强", "液体压强", "大气压强"]), ("简单机械", ["杠杆", "滑轮", "斜面", "功的原理"]), )), (["电学", "电流", "电路", "欧姆"], mm( ("基本物理量", ["电流", "电压", "电阻", "电功率"]), ("欧姆定律", ["I=U/R", "串并联电阻", "电压电流关系"]), ("电路连接", ["串联", "并联", "混联", "短路断路"]), ("电与磁", ["电流磁效应", "电磁感应", "电动机发电机"]), )), (["光学", "光", "反射折射"], mm( ("光的传播", ["直线传播", "光速", "影子小孔成像"]), ("光的反射", ["反射定律", "镜面反射", "漫反射"]), ("光的折射", ["折射定律", "全反射", "色散"]), ("透镜成像", ["凸透镜", "凹透镜", "成像规律", "应用"]), )), (["运动", "速度", "加速度"], mm( ("运动描述", ["参照物", "路程位移", "速度"]), ("匀速运动", ["速度恒定", "s=vt", "图像分析"]), ("变速运动", ["加速度", "v=v0+at", "匀变速直线"]), ("相对运动", ["相对速度", "相遇追及", "实际应用"]), )), # 数学 (["函数"], mm( ("函数概念", ["定义域", "值域", "对应关系"]), ("基本初等函数", ["一次函数", "二次函数", "反比例函数"]), ("指数对数", ["指数函数", "对数函数", "运算法则"]), ("函数性质", ["单调性", "奇偶性", "周期性"]), )), (["几何", "三角形", "圆"], mm( ("线与角", ["点线面", "相交平行", "角的关系"]), ("三角形", ["内角和", "全等相似", "勾股定理"]), ("四边形", ["平行四边形", "矩形菱形", "梯形"]), ("圆", ["圆的性质", "切线", "弧长扇形"]), )), (["方程", "一元", "二元", "不等式"], mm( ("一元一次", ["等式性质", "求解步骤", "应用题"]), ("方程组", ["二元一次", "代入消元", "加减消元"]), ("一元二次", ["因式分解", "公式法", "韦达定理"]), ("不等式", ["性质", "一元一次不等式", "不等式组"]), )), (["概率", "统计"], mm( ("数据收集", ["普查抽样", "频数频率", "统计图表"]), ("数据特征", ["平均数", "中位数众数", "方差极差"]), ("概率初步", ["事件分类", "古典概型", "频率估计", "树状图列举"]), ("应用", ["决策分析", "风险评估", "生活实例"]), )), # 语文 (["古诗", "古诗词", "唐诗宋词"], mm( ("发展脉络", ["诗经楚辞", "唐诗", "宋词", "元曲"]), ("代表诗人", ["李白杜甫", "白居易", "苏轼李清照"]), ("常见题材", ["山水田园", "边塞", "咏物言志", "送别"]), ("鉴赏方法", ["意象意境", "表现手法", "炼字", "情感主旨"]), )), (["记叙文"], mm( ("六要素", ["时间地点", "人物", "起因经过结果"]), ("结构层次", ["开头引入", "主体展开", "结尾点题"]), ("表达方式", ["叙述", "描写", "议论抒情"]), ("写作技巧", ["详略得当", "线索贯穿", "细节描写"]), )), (["议论文"], mm( ("三要素", ["论点", "论据", "论证"]), ("论点确立", ["中心论点", "分论点", "鲜明准确"]), ("论证方法", ["举例论证", "道理论证", "对比比喻"]), ("结构思路", ["提出问题", "分析问题", "解决问题"]), )), # 英语 (["时态"], mm( ("一般时态", ["一般现在", "一般过去", "一般将来"]), ("进行时", ["现在进行", "过去进行", "be+doing"]), ("完成时", ["现在完成", "过去完成", "have+done"]), ("时间状语", ["always/often", "yesterday", "already/yet"]), )), (["从句"], mm( ("名词性从句", ["主语从句", "宾语从句", "表语从句"]), ("定语从句", ["关系代词", "关系副词", "限制非限制"]), ("状语从句", ["时间原因", "条件让步", "目的结果"]), ("用法要点", ["连接词", "语序", "时态呼应"]), )), ] return kb def _mindmap_subject_template(self, subject: str) -> list: subject = (subject or "").strip() mm = self._mm_nodes templates = { "数学": mm( ("概念理解", ["定义", "性质", "判定条件"]), ("公式法则", ["基本公式", "运算法则", "推导过程"]), ("典型例题", ["基础题", "应用题", "易错题"]), ("知识应用", ["实际问题", "跨学科", "拓展提升"]), ), "物理": mm( ("物理概念", ["定义", "物理意义", "单位"]), ("规律定律", ["适用条件", "公式表达", "实验基础"]), ("实验探究", ["实验器材", "操作步骤", "数据处理"]), ("生活应用", ["现象解释", "技术应用", "综合计算"]), ), "化学": mm( ("物质组成", ["元素", "结构", "性质"]), ("变化规律", ["反应类型", "反应条件", "实验现象"]), ("化学计算", ["化学式", "方程式", "质量计算"]), ("实际应用", ["材料能源", "环境保护", "生命健康"]), ), "生物": mm( ("生命现象", ["结构基础", "生理功能", "生命活动"]), ("核心概念", ["定义", "原理", "机制"]), ("实验观察", ["方法", "现象", "结论"]), ("联系应用", ["生产实践", "健康生活", "生态保护"]), ), "历史": mm( ("时代背景", ["政治", "经济", "文化"]), ("重要事件", ["时间", "人物", "过程"]), ("深远影响", ["制度变革", "思想文化", "社会发展"]), ("历史启示", ["经验教训", "规律认识", "现实意义"]), ), "地理": mm( ("空间分布", ["位置", "范围", "界线"]), ("自然要素", ["地形气候", "水文土壤", "植被"]), ("人文要素", ["人口城市", "农业工业", "交通"]), ("人地关系", ["资源利用", "环境问题", "可持续发展"]), ), "语文": mm( ("基础知识", ["字词", "语句", "修辞"]), ("文本理解", ["内容", "结构", "主旨"]), ("表达技巧", ["描写方法", "表现手法", "语言特色"]), ("拓展运用", ["写作借鉴", "迁移阅读", "文化传承"]), ), "英语": mm( ("词汇积累", ["核心单词", "短语搭配", "词形变化"]), ("语法要点", ["句型结构", "时态语态", "从句"]), ("功能话题", ["日常交际", "话题表达", "文化背景"]), ("技能训练", ["听说", "读写", "综合运用"]), ), } if subject in templates: return templates[subject] return mm( ("核心概念", ["定义", "特征", "分类"]), ("知识结构", ["基本原理", "重要规律", "内在联系"]), ("重点方法", ["解题思路", "分析方法", "易错提示"]), ("应用拓展", ["生活实例", "学科关联", "拓展提升"]), ) async def generate_animation(self, prompt: str, anim_type: str = "general") -> dict: prompt = self._repair_text(prompt) system_prompt = f"""你是一位教学动画设计专家。根据教师描述,生成教学动画的HTML页面。 重要规则: - 所有HTML属性必须用单引号,如 style='color:red;' onclick='fn()' - 不要使用" ) if qtype == "fill_blank": return ( "" "

" + stem + "

" + "" + "" + "

" + "" ) return "

" + stem + "

" def _fallback_lesson_plan(self, title, subject, grade, objectives, duration): t = title or "大单元教案" subj = (subject or "").strip() prof = self._lesson_plan_profile(subj) new_dur = max(10, duration - 20) practice_dur = max(8, duration // 4) return { "title": t, "objectives": objectives or prof["objectives"](t), "key_points": prof["key_points"](t), "difficulties": prof["difficulties"](t), "phases": [ {"name": "导入", "duration": 5, "activities": [prof["intro"](t)], "teacher_actions": ["创设情境,激发兴趣。", "提出核心问题。"], "student_actions": ["观察思考。", "联系已有经验。"], "resources": prof["intro_resources"]}, {"name": "新授", "duration": new_dur, "activities": prof["new_activities"](t), "teacher_actions": prof["new_teacher"], "student_actions": prof["new_student"], "resources": prof["new_resources"]}, {"name": "练习巩固", "duration": practice_dur, "activities": prof["practice"](t), "teacher_actions": ["巡视指导,个别辅导。"], "student_actions": prof["practice_student"], "resources": prof["practice_resources"]}, {"name": "总结", "duration": 5, "activities": [prof["summary"](t)], "teacher_actions": ["梳理知识脉络,提炼方法。"], "student_actions": ["回顾反思,记录收获。"], "resources": ["板书", "思维导图"]}, ], "homework": prof["homework"](t), "reflection": f"根据课堂反馈与学生掌握情况,调整「{t}」后续教学的深度、广度与练习分层策略。", } def _lesson_plan_profile(self, subject: str) -> dict: s = subject profiles = { "数学": { "objectives": lambda t: [f"理解「{t}」的概念内涵与几何或代数意义", f"掌握「{t}」的基本公式与运算法则", "能运用所学知识分析并解决实际问题"], "key_points": lambda t: [f"「{t}」的定义与核心性质", "公式推导与规范书写"], "difficulties": lambda t: [f"「{t}」的灵活运用与变形", "数形结合思想的建立"], "intro": lambda t: f"借助生活实例或已有知识,引出「{t}」的研究必要性。", "new_activities": lambda t: [f"通过探究活动归纳「{t}」的概念与性质。", "推导公式并分析适用条件。"], "new_teacher": ["引导探究,板书推导过程。", "组织小组讨论。"], "new_student": ["动手操作或画图探究。", "小组合作归纳结论。"], "intro_resources": ["课件", "实物教具", "几何画板"], "new_resources": ["学案", "探究任务单", "动态演示"], "practice": lambda t: ["基础题巩固概念。", "变式训练提升能力。", "实际问题应用。"], "practice_student": ["独立完成,规范步骤。", "同桌互批,错题订正。"], "practice_resources": ["分层练习", "答题卡"], "summary": lambda t: f"梳理「{t}」的知识结构图,提炼解题方法与易错点。", "homework": lambda t: [f"完成「{t}」分层作业(基础+提升)。", "整理本节易错题集。"], }, "物理": { "objectives": lambda t: [f"理解「{t}」的物理意义与适用条件", "掌握相关公式并能进行简单计算", "经历实验探究过程,培养科学思维"], "key_points": lambda t: [f"「{t}」的概念建立与公式表达", "实验现象的观察与分析"], "difficulties": lambda t: [f"「{t}」物理模型的构建", "实验方案的设计与数据分析"], "intro": lambda t: f"通过演示实验或生活现象,引发对「{t}」的思考。", "new_activities": lambda t: ["分组实验探究规律。", "分析实验数据,归纳结论。"], "new_teacher": ["演示关键实验,强调安全。", "指导小组实验,引导分析。"], "new_student": ["分组实验,记录数据。", "分析数据,得出结论。"], "intro_resources": ["演示器材", "课件", "视频"], "new_resources": ["学生实验器材", "数据记录表"], "practice": lambda t: ["基础概念辨析。", "公式应用计算。", "实验数据分析题。"], "practice_student": ["独立解题,规范作图。", "小组互查实验报告。"], "practice_resources": ["练习卷", "实验报告单"], "summary": lambda t: f"构建「{t}」的知识网络,强调实验方法与物理思想。", "homework": lambda t: [f"完成「{t}」课后练习。", "撰写实验探究报告。"], }, } # 化学/生物共用理科探究模板 sci = { "objectives": lambda t: [f"理解「{t}」的基本概念与原理", "掌握核心知识并能解释相关现象", "培养实验观察与科学探究能力"], "key_points": lambda t: [f"「{t}」的核心概念与特征", "实验现象的观察与解释"], "difficulties": lambda t: [f"「{t}」微观机制的理解", "知识在真实情境中的迁移应用"], "intro": lambda t: f"通过实验现象或生活实例,引出「{t}」的探究问题。", "new_activities": lambda t: ["实验观察与操作。", "小组讨论归纳知识要点。"], "new_teacher": ["组织实验,强调规范操作。", "引导分析现象背后的原理。"], "new_student": ["动手实验,如实记录。", "讨论交流,形成结论。"], "intro_resources": ["实验器材", "课件", "标本/模型"], "new_resources": ["实验材料", "学习单"], "practice": lambda t: ["概念辨析与判断。", "现象解释与应用。"], "practice_student": ["独立完成练习。", "小组互评实验记录。"], "practice_resources": ["练习卡", "实验记录册"], "summary": lambda t: f"梳理「{t}」的知识体系,强调科学方法与探究思路。", "homework": lambda t: [f"完成「{t}」巩固练习。", "预习下一节内容并完成预习单。"], } for k in ("化学", "生物"): profiles[k] = sci profiles["语文"] = { "objectives": lambda t: [f"品味「{t}」的语言特色与表达技巧", "理解文章主旨与作者情感", "学习并运用相关的写作方法"], "key_points": lambda t: [f"「{t}」的文本内容与结构梳理", "关键语句的赏析与理解"], "difficulties": lambda t: [f"「{t}」深层意蕴与作者情感的体悟", "写作手法在表达中的迁移运用"], "intro": lambda t: f"借助背景介绍或朗读导入,唤起对「{t}」的阅读期待。", "new_activities": lambda t: ["初读感知,整体把握文意。", "精读品味,赏析关键语段。"], "new_teacher": ["范读指导,组织品析。", "点拨赏析方法。"], "new_student": ["朗读体会,圈点批注。", "小组交流赏析心得。"], "intro_resources": ["课件", "背景资料", "音频朗读"], "new_resources": ["文本", "批注学习单"], "practice": lambda t: ["仿写练习。", "片段赏析与表达。"], "practice_student": ["独立完成仿写。", "分享交流,互评互改。"], "practice_resources": ["仿写学习单", "评价量表"], "summary": lambda t: f"总结「{t}」的写法与主旨,提炼可借鉴的表达技巧。", "homework": lambda t: [f"围绕「{t}」完成读写结合小练笔。", "积累本文好词好句。"], } profiles["英语"] = { "objectives": lambda t: [f"掌握「{t}」相关核心词汇与句型", "能在真实情境中运用所学进行交流", "提升听、说、读、写综合语言能力"], "key_points": lambda t: [f"「{t}」的核心词汇与目标句型", "语言功能的得体运用"], "difficulties": lambda t: [f"「{t}」相关语法结构的正确使用", "在真实交际中的灵活表达"], "intro": lambda t: f"通过歌曲、游戏或情境对话导入「{t}」话题。", "new_activities": lambda t: ["词汇与句型学习。", "情境对话操练与角色扮演。"], "new_teacher": ["创设情境,示范句型。", "组织pair work与group work。"], "new_student": ["跟读模仿,记忆词汇。", "结对操练,大胆表达。"], "intro_resources": ["课件", "音频视频", "单词卡"], "new_resources": ["对话任务单", "角色卡片"], "practice": lambda t: ["词汇句型巩固。", "情境交际任务。"], "practice_student": ["完成听力与填空。", "小组表演对话。"], "practice_resources": ["练习单", "评价表"], "summary": lambda t: f"回顾「{t}」核心语言知识,梳理交际策略。", "homework": lambda t: [f"完成「{t}」听读作业。", "用目标句型写一段对话。"], } profiles["历史"] = { "objectives": lambda t: [f"了解「{t}」的基本史实与发展脉络", "分析历史事件的因果关系与影响", "培养历史思维与家国情怀"], "key_points": lambda t: [f"「{t}」的关键事件、人物与时间", "历史发展规律与时代特征"], "difficulties": lambda t: [f"对「{t}」历史现象的多角度分析", "史论结合,以史为鉴"], "intro": lambda t: f"通过史料、图片或视频导入「{t}」的时代背景。", "new_activities": lambda t: ["梳理时间线与重大事件。", "研读史料,分析因果关系。"], "new_teacher": ["提供史料,引导分析。", "组织讨论,启发思考。"], "new_student": ["阅读史料,做时间轴。", "小组讨论,发表见解。"], "intro_resources": ["课件", "历史地图", "史料摘录"], "new_resources": ["史料学习单", "时间轴模板"], "practice": lambda t: ["史实梳理填空。", "材料分析题。"], "practice_student": ["独立完成基础题。", "小组研讨材料题。"], "practice_resources": ["练习卷", "材料研读单"], "summary": lambda t: f"构建「{t}」的知识框架,总结历史启示。", "homework": lambda t: [f"完成「{t}」巩固练习。", "撰写一段历史小短评。"], } profiles["地理"] = { "objectives": lambda t: [f"认识「{t}」的空间分布与基本特征", "理解各地理要素的相互关系", "树立人地协调与可持续发展观念"], "key_points": lambda t: [f"「{t}」的位置、分布与主要特征", "自然与人文地理要素的相互作用"], "difficulties": lambda t: [f"「{t}」空间格局的综合分析", "读图分析与地理推理"], "intro": lambda t: f"借助地图、卫星图或景观图导入「{t}」。", "new_activities": lambda t: ["读图分析空间分布。", "小组探究地理要素关系。"], "new_teacher": ["指导读图方法。", "组织探究活动。"], "new_student": ["读图标注,提取信息。", "小组合作,归纳特征。"], "intro_resources": ["课件", "地图", "景观图片"], "new_resources": ["空白地图", "探究任务单"], "practice": lambda t: ["读图填图训练。", "综合分析题。"], "practice_student": ["独立完成读图题。", "小组研讨分析题。"], "practice_resources": ["练习图册", "分析学习单"], "summary": lambda t: f"整理「{t}」知识结构,强调人地关系与可持续发展。", "homework": lambda t: [f"完成「{t}」读图与练习。", "调查身边的地理现象。"], } if s in profiles: return profiles[s] # 通用模板 return { "objectives": lambda t: [f"理解「{t}」的核心内容与基本要求", "掌握关键知识与基本方法", "培养分析思维与合作探究能力"], "key_points": lambda t: [f"「{t}」的核心知识建构", "方法与技能的训练"], "difficulties": lambda t: [f"「{t}」的深入理解与灵活运用", "知识的迁移与综合"], "intro": lambda t: f"创设情境,引出「{t}」的学习主题。", "new_activities": lambda t: [f"围绕「{t}」进行讲解与互动探究。"], "new_teacher": ["讲解引导,组织探究。"], "new_student": ["主动思考,合作探究。"], "intro_resources": ["课件", "情境素材"], "new_resources": ["学习单", "探究材料"], "practice": lambda t: ["分层练习巩固。", "互评与订正。"], "practice_student": ["独立完成,规范作答。"], "practice_resources": ["练习卡"], "summary": lambda t: f"梳理「{t}」知识结构,提炼重点方法。", "homework": lambda t: [f"完成「{t}」巩固作业。", "预习下节内容。"], } def _fallback_essay_grade(self, essay_text, total_score): text = essay_text or "" char_count = len(text) import re as _re2 sentences = [s.strip() for s in _re2.split(r"[。!?]", text) if s.strip()] sent_count = len(sentences) has_dialog = "“" in text or "说" in text has_detail = any(w in text for w in ("有一次", "记得", "那天", "当时", "忽然", "只见")) avg_sent = char_count / max(sent_count, 1) score_ratio = 0.82 if avg_sent > 25 else 0.74 score = max(1, int(total_score * score_ratio)) annotations = [] if has_detail: for kw in ("有一次", "记得", "那天", "当时"): pos = text.find(kw) if pos >= 0: snippet = text[max(0,pos-3):pos+12] annotations.append({"text": snippet, "comment": "这里用具体事例展开,增加了文章的真实感。", "type": "good"}) break if char_count < 100: annotations.append({"text": text[:20], "comment": "文章字数偏少,建议增加具体描写。", "type": "error"}) strengths = [] weaknesses = [] suggestions = [] if has_detail: strengths.append("能用具体事例展开叙述,增加了文章的感染力。") else: weaknesses.append("缺乏具体事例和细节描写,内容较空洞。") suggestions.append("增加一到两个具体事例,展开细节描写。") if has_dialog: strengths.append("运用了对话描写,使人物形象更加鲜活。") else: suggestions.append("可以加入对话描写,让人物更鲜活。") if sent_count <= 2: weaknesses.append("句子偏少,结构较单谬。") suggestions.append("增加句子数量,丰富文章层次。") if avg_sent > 40: weaknesses.append("部分句子较长,建议适当断句。") suggestions.append("将过长的句子拆分为短句,提高可读性。") content_ratio = 0.32 if has_detail else 0.25 content_s = int(total_score * content_ratio) structure_s = int(total_score * 0.22) language_s = int(total_score * 0.18) writing_s = score - content_s - structure_s - language_s if not strengths: strengths = ["主题明确,有基本的表达。"] if not weaknesses: weaknesses = ["可以在细节上进一步丰富。"] if not suggestions: suggestions = ["继续保持认真观察和真诚表达。"] level = "优秀" if score_ratio > 0.80 else ("良好" if score_ratio > 0.72 else "中等") return { "total_score": score, "scores": { "content": {"score": content_s, "max": int(total_score * 0.3), "comment": "内容较为充实。" if has_detail else "内容基本完整,建议增加细节。"}, "structure": {"score": structure_s, "max": int(total_score * 0.25), "comment": "结构较清晰。" if sent_count > 2 else "结构较为单谬,建议增加层次。"}, "language": {"score": language_s, "max": int(total_score * 0.25), "comment": "表达较通顺。" if avg_sent <= 40 else "部分句子较长,可适当断句。"}, "writing": {"score": writing_s, "max": int(total_score * 0.2), "comment": "书写规范尚可。"}, }, "overall_comment": "作文围绕主题展开," + ("能用具体事例进行叙述," if has_detail else "但缺乏具体细节,") + "表达有一定的真实感。" + ("建议继续加强细节描写和语言表现力,让文章更有感染力。" if not has_detail else "建议在语言精炼和结构层次上进一步提升。"), "strengths": strengths, "weaknesses": weaknesses, "suggestions": suggestions, "annotations": annotations, "level": level, } def _fallback_exam(self, subject, grade, knowledge_points, difficulty, question_types, count, total_score): total = max(1, min(count or 10, 20)) each = max(1, total_score // max(total, 1)) subj = subject or "综合" kp = "、".join(knowledge_points) if knowledge_points else "基础知识" bank = self._question_bank(kp, subj) if not bank: bank = self._generic_question_bank(kp, subj) questions = [] for idx in range(total): q = dict(bank[idx % len(bank)]) q["id"] = idx + 1 q["type"] = q.get("type", (question_types or ["choice"])[0]) q["score"] = each q["difficulty"] = difficulty if isinstance(difficulty, str) else ["easy", "medium", "hard"][idx % 3] q["content"] = q.pop("question", None) or q.get("content", "") q["analysis"] = q.get("explanation", q.get("analysis", "")) if "explanation" in q: q.pop("explanation", None) if "html" in q: q.pop("html", None) questions.append(q) return {"title": f"{subj}{grade or ''}智能试卷", "questions": questions} def _courseware_category(self, prompt: str) -> str: prompt = self._repair_text(prompt) if any(k in prompt for k in ["圆柱", "圆柱的体积", "体积公式", "底面积", "高"]): return "cylinder" return "general" def _courseware_shell(self, title: str, subtitle: str, body: str, footer_left: str = "") -> str: title_html = html_lib.escape(title) subtitle_html = html_lib.escape(subtitle) footer_html = html_lib.escape(footer_left) return f"""

{title_html}

{subtitle_html}

AI生成
{body}
""" def _fallback_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: prompt = self._repair_text(prompt) if self._courseware_category(prompt) == "cylinder": return self._fallback_cylinder_courseware(prompt, subject, grade, page_count) return self._fallback_general_courseware(prompt, subject, grade, page_count) def _normalize_courseware_result(self, result: dict, prompt: str, subject: str, grade: str, page_count: int, aspect_ratio: str) -> dict | None: if not isinstance(result, dict): return None pages = result.get("pages") if not isinstance(pages, list) or len(pages) < 5: return None normalized_pages = [] for page in pages: if not isinstance(page, dict): continue content = page.get("content") if not isinstance(content, str): continue cleaned = content.replace("100vh", "100%").replace("100vw", "100%") if not cleaned.strip().lower().startswith("{cleaned}" normalized_pages.append({ "type": page.get("type") or "content", "title": self._repair_text(str(page.get("title") or "")), "content": cleaned, "notes": self._repair_text(str(page.get("notes") or "")), }) if len(normalized_pages) < 5: return None return { "title": self._repair_text(str(result.get("title") or prompt[:24] or "课件")), "summary": self._repair_text(str(result.get("summary") or "")), "tags": [self._repair_text(str(tag)) for tag in (result.get("tags") or []) if str(tag).strip()], "pages": normalized_pages[:max(5, min(page_count, len(normalized_pages)))], "aspect_ratio": aspect_ratio, "subject": subject, "grade": grade, } def _courseware_topic(self, prompt: str) -> str: prompt = self._repair_text(prompt) text = prompt.lower() if any(k in prompt for k in ["地球公转", "四季", "春分", "夏至", "秋分", "冬至", "昼夜长短", "太阳直射"]) or any(k in text for k in ["orbit", "season"]): return "orbit" if any(k in prompt for k in ["圆柱", "体积", "底面积", "圆锥", "长方体"]): return "cylinder" return "general" def _courseware_shell_clean(self, title: str, subtitle: str, body: str, footer_left: str = "") -> str: title_html = html_lib.escape(title) subtitle_html = html_lib.escape(subtitle) footer_html = html_lib.escape(footer_left) return ( "
" "" "" "

" + title_html + "

" + subtitle_html + "

AI生成
" "
" + body + "
" "" "
" ) def _fallback_cylinder_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: prompt = self._repair_text(prompt) title = "圆柱的体积(第一课时)" pages = [ { "type": "title", "title": "封面", "content": self._courseware_shell_clean( title, f"{subject or '数学'} · {grade or '六年级'}", """
课堂导入
数学 · 形体与测量

圆柱的体积

围绕“底面积 × 高”展开探究,通过观察、切分、比较和例题,把抽象公式变成可理解、可操作的课堂体验。

层次清楚 页面可切换 支持课堂互动
""", "封面页", ), "notes": "封面", }, { "type": "content", "title": "知识回顾", "content": self._courseware_shell_clean( "知识回顾", "先把旧知识拉回来,再进入新课探究", """
旧知 1

长方体体积公式

体积和底面积、高有关。先看“底面铺了多少”,再看“竖起来有多高”。

""", "点击卡片切换旧知", ), "notes": "知识回顾", }, { "type": "interactive", "title": "引入新知", "content": self._courseware_shell_clean( "引入新知", "先看形状和高度,再让学生猜一猜体积和什么有关", """
情境导入
课堂提问

你觉得圆柱体积和什么有关?

A. 只和底面形状有关
B. 和底面积、高都有关系
C. 只和颜色有关
课堂结论:体积计算要抓住“底面积 × 高”这条主线。
""", "学生先猜想,再看图验证", ), "notes": "引入", }, { "type": "content", "title": "公式探究", "content": self._courseware_shell_clean( "公式探究", "用切、拼、比的方式,把圆柱体积想清楚", """
切分观察

把圆柱切成很多薄片

圆柱越像被分成很多很薄的小层,每一层越接近一个圆形小面。层数越多,拼出的形状就越接近长方体。

""", "切、拼、比,逐步导出公式", ), "notes": "公式探究", }, { "type": "interactive", "title": "例题讲解", "content": self._courseware_shell_clean( "例题讲解", "把公式落到计算里,按步骤说清楚", """
例题

一个圆柱的底面积是 28.26 cm²,高是 10 cm,体积是多少?

已知
S = 28.26 cm²,h = 10 cm
解题提醒
圆柱体积公式中的 S 是底面积,不是底面周长。单位也要对应写成立方单位。
写答语时要带上单位,例如 cm³、m³。
""", "按步骤理解计算过程", ), "notes": "例题", }, { "type": "exercise", "title": "巩固练习", "content": self._courseware_shell_clean( "巩固练习", "点击选项,立即查看判断结果", """
选择题

圆柱体积公式正确的是哪一个?

反馈
A 不是正确答案。圆柱体积不是把周长直接相乘。
""", "选择答案后直接反馈", ), "notes": "练习", }, { "type": "summary", "title": "总结", "content": self._courseware_shell_clean( "总结与作业", "把公式、方法和易错点一次收好", """
本课总结
1. 圆柱体积公式:V = S × h
2. 核心方法:切、拼、转化成近似长方体
3. 易错提醒:S 是底面积,不是底面周长
课后任务
1. 完成课本练习。
2. 找一个生活中的圆柱体,量一量底面和高,试着算体积。
""", "总结方法并布置作业", ), "notes": "总结", }, ] total = max(5, min(page_count or 7, len(pages))) return {"title": title, "summary": "围绕底面积与高探究圆柱体积", "tags": ["圆柱", "体积", "互动课件"], "pages": pages[:total]} def _fallback_orbit_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: prompt = self._repair_text(prompt) title = "地球公转与四季变化" pages = [ { "type": "title", "title": "封面", "content": self._courseware_shell_clean( title, f"{subject or '科学'} · {grade or '六年级'}", """
情境导入
科学 · 地球与宇宙

地球公转与四季变化

通过轨道、倾角和阳光照射方向的变化,把春夏秋冬的形成过程讲清楚、看明白、能点击。

轨道示意 四季按钮 课堂可演示
""", "封面页", ), "notes": "封面", }, { "type": "content", "title": "知识回顾", "content": self._courseware_shell_clean( "知识回顾", "先把轨道、地轴倾斜和太阳光照这三个要点记住", """
要点 1

地球在公转

地球绕太阳转一圈,需要大约一年。

要点 2

地轴是倾斜的

地轴倾斜让同一地区在不同时间受到的阳光不同。

要点 3

太阳光照变化

阳光照射角度和昼夜长短,会随着公转位置变化。

""", "三个关键概念先对齐", ), "notes": "知识回顾", }, { "type": "interactive", "title": "四季探究", "content": self._courseware_shell_clean( "四季探究", "点击春夏秋冬,观察光照和气候变化", """
春季

阳光开始变强,气温慢慢回升

春季时太阳直射位置逐渐北移,北半球白天变长,天气回暖,植物也开始生长。

""", "四季按钮可直接切换", ), "notes": "四季探究", }, { "type": "content", "title": "现象解释", "content": self._courseware_shell_clean( "现象解释", "同样是太阳,为什么不同季节感觉不一样?", """
原因 1

阳光直射角度不同

角度越直,单位面积接收到的热量越多;角度越斜,热量越分散。

原因 2

白昼长短不同

白天越长,太阳照射时间越久,地面吸收热量也更多。

""", "从光照与时间两个角度解释", ), "notes": "现象解释", }, { "type": "summary", "title": "总结", "content": self._courseware_shell_clean( "总结与作业", "把公转、地轴倾斜和四季变化连起来理解", """
本课总结
1. 地球围绕太阳公转,形成一年四季的周期变化。
2. 地轴倾斜,让不同季节接受的阳光不同。
3. 阳光直射角度和白昼长短,决定了冷热变化。
课后任务
1. 画一张地球绕太阳公转示意图。
2. 说一说春夏秋冬各自最明显的特点。
""", "把四季原因说完整", ), "notes": "总结", }, ] total = max(5, min(page_count or 6, len(pages))) return {"title": title, "summary": "围绕地球公转与太阳光照变化解释四季形成", "tags": ["地球公转", "四季", "科学探究"], "pages": pages[:total]} def _fallback_general_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict: prompt = self._repair_text(prompt) title = prompt[:24] or "互动课件" pages = [ { "type": "title", "title": "封面", "content": self._courseware_shell_clean( title, f"{subject or '综合'} · {grade or '课堂教学'}", """
课堂导入
知识生成 · 互动呈现

""" + html_lib.escape(title) + """

用清爽、分层、可点击的页面组织课堂内容,适合投屏展示和逐步讲解。

层次清楚 页面可切换 支持课堂互动
""", "封面页", ), "notes": "封面", }, { "type": "content", "title": "核心概念", "content": self._courseware_shell_clean( "核心概念", "先把问题拆开,再逐步组织内容", """
第 1 点

要讲什么

把主题拆成 2 到 3 个核心问题,再分别展开。

第 2 点

怎么讲

先观察,再归纳,最后用例子验证。

第 3 点

怎么练

用点击反馈、小练习和总结页收尾。

""", "搭好课堂结构", ), "notes": "核心概念", }, { "type": "interactive", "title": "互动探究", "content": self._courseware_shell_clean( "互动探究", "点击左侧选项,右侧显示不同内容", """
观察

先看现象,再找规律

围绕图片、数据或情境,先让学生说出“看见了什么”,再引导他们说“为什么会这样”。

""", "交互区可直接切换", ), "notes": "互动探究", }, { "type": "exercise", "title": "课堂练习", "content": self._courseware_shell_clean( "课堂练习", "选择一个答案,马上看反馈", """
练习题

下面哪一种说法最符合今天的主题?

反馈
A 不完整。课堂结构不能只靠一个结论支撑。
""", "选择后看反馈", ), "notes": "课堂练习", }, { "type": "summary", "title": "总结", "content": self._courseware_shell_clean( "总结与作业", "把今天的重点提炼成一页", """
本课总结
1. 先观察,再分析,再总结。
2. 把复杂内容拆成若干清楚的部分。
3. 用点击互动提高课堂参与感。
课后任务
1. 把本课内容整理成 3 条要点。
2. 试着为一个新的知识点设计 1 个互动问题。
""", "整理成可复述的结论", ), "notes": "总结", }, ] total = max(5, min(page_count or 5, len(pages))) return {"title": title, "summary": "把主题拆分成观察、分析、总结的互动课件", "tags": ["互动课件", "可点击", "课堂讲解"], "pages": pages[:total]} async def generate_courseware(self, prompt: str, subject: str = "", grade: str = "", page_count: int = 8, aspect_ratio: str = "16:9") -> dict: prompt = self._repair_text(prompt) topic = self._courseware_topic(prompt) if topic == "cylinder": return self._fallback_cylinder_courseware(prompt, subject or "数学", grade or "六年级", page_count) if topic == "orbit": return self._fallback_orbit_courseware(prompt, subject or "科学", grade or "六年级", page_count) try: result = await self._chat_json([ {"role": "system", "content": "请生成多页互动课件 JSON,包含 title、summary、tags、pages。每页 content 必须是可直接渲染的
HTML 片段,页面结构清爽,适合教室投屏。"}, {"role": "user", "content": f"学科:{subject or '未指定'}\n年级:{grade or '未指定'}\n页数要求:{page_count}\n\n教师需求:{prompt}"}, ], temperature=0.45, max_tokens=12000) normalized = self._normalize_courseware_result(result, prompt, subject, grade, page_count, aspect_ratio) if normalized: return normalized except Exception: pass return self._fallback_general_courseware(prompt, subject, grade, page_count)