import "server-only"; import { z } from "zod"; import { env } from "@/env.mjs"; import { createAiChatCompletion } from "@/shared/lib/ai"; import { isRecord } from "@/shared/lib/type-guards"; import { getKnowledgePointsByTextbookId, getKnowledgePointsByChapterId, } from "@/modules/textbooks/data-access"; const SuggestedKpSchema = z.object({ id: z.string().min(1), name: z.string().min(1), reason: z.string(), }); const SuggestedKpListSchema = z.array(SuggestedKpSchema); /** 从 unknown 节点安全提取文本(类型守卫从 unknown 收窄) */ const extractNodeText = (node: unknown): string => { if (!isRecord(node)) return "" const data = node.data if (!isRecord(data)) return "" const html = typeof data.html === "string" ? data.html : "" const sourceText = typeof data.sourceText === "string" ? data.sourceText : "" return html || sourceText || "" } // P2 修复:AI prompt 提取为模块常量,便于维护和未来国际化 // 注:AI prompt 属于系统级提示词,非用户可见文本,暂不纳入 next-intl i18n 体系 const AI_SUGGEST_PROMPT_TEMPLATE = `你是教学设计助手。以下是教师备课内容: --- {text} --- 请从下列知识点中推荐最相关的 3-8 个,并说明理由。返回 JSON 数组,每项含 id/name/reason。 候选知识点:{kpList}`; export async function suggestKnowledgePoints( doc: { nodes: unknown[] }, textbookId?: string, chapterId?: string, ): Promise<{ id: string; name: string; reason: string }[]> { // 1. 提取课案纯文本 const text = doc.nodes .map((b) => extractNodeText(b)) .join("\n") .slice(0, 3000); if (!text.trim()) return []; // 2. 获取候选知识点池 if (!textbookId) return []; const allKps = chapterId ? await getKnowledgePointsByChapterId(chapterId) : await getKnowledgePointsByTextbookId(textbookId); if (allKps.length === 0) return []; const kpList = allKps.map((kp) => ({ id: kp.id, name: kp.name })).slice(0, 100); // 3. 调用 AI(使用模板构建 prompt) const prompt = AI_SUGGEST_PROMPT_TEMPLATE .replace("{text}", text) .replace("{kpList}", JSON.stringify(kpList)); const { content } = await createAiChatCompletion({ messages: [{ role: "user", content: prompt }], model: env.AI_MODEL ?? "gpt-4o-mini", temperature: 0.3, }); try { // 尝试从返回内容中提取 JSON 数组 const jsonMatch = content.match(/\[[\s\S]*\]/); if (!jsonMatch) return []; const parsed: unknown = JSON.parse(jsonMatch[0]); const validated = SuggestedKpListSchema.safeParse(parsed); if (!validated.success) return []; // 过滤掉不在候选池中的 id const validIds = new Set(kpList.map((k) => k.id)); return validated.data.filter((p) => validIds.has(p.id)); } catch { return []; } }