Files
NextEdu/src/modules/lesson-preparation/ai-suggest.ts
SpecialX 20023e13fd feat(lesson-preparation): add AI evaluation, analytics, attachments, calendar, comments, review, substitutes, formative, and version diff
- 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
2026-07-03 10:25:21 +08:00

85 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 [];
}
}