- Add actions-ai-evaluation, actions-analytics, actions-attachments, actions-calendar, actions-comments, actions-formative, actions-questions, actions-review, actions-substitutes - Add corresponding data-access layers for each new action module - Add calendar-view, curriculum-map-view, version-diff-viewer components - Add editor-slice, selection-slice, version-slice hooks for state management - Add document-diff and scope-check lib utilities - Add default-question-service and external-questions-bridge services
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
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 [];
|
||
}
|
||
}
|