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
This commit is contained in:
SpecialX
2026-07-03 10:25:21 +08:00
parent a16f09d3c3
commit 20023e13fd
75 changed files with 5131 additions and 1186 deletions

View File

@@ -1,8 +1,8 @@
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,
@@ -18,18 +18,23 @@ const SuggestedKpListSchema = z.array(SuggestedKpSchema);
/** 从 unknown 节点安全提取文本(类型守卫从 unknown 收窄) */
const extractNodeText = (node: unknown): string => {
if (!node || typeof node !== "object") return ""
// 从 unknown 收窄为 Record<string, unknown> 以进行字段检查
const record = node as Record<string, unknown>
const data = record.data
if (!data || typeof data !== "object") return ""
// 从 unknown 收窄为 Record<string, unknown> 以进行字段检查
const dataRecord = data as Record<string, unknown>
const html = typeof dataRecord.html === "string" ? dataRecord.html : ""
const sourceText = typeof dataRecord.sourceText === "string" ? dataRecord.sourceText : ""
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,
@@ -52,13 +57,10 @@ export async function suggestKnowledgePoints(
const kpList = allKps.map((kp) => ({ id: kp.id, name: kp.name })).slice(0, 100);
// 3. 调用 AI
const prompt = `你是教学设计助手。以下是教师备课内容:
---
${text}
---
请从下列知识点中推荐最相关的 3-8 个,并说明理由。返回 JSON 数组,每项含 id/name/reason。
候选知识点:${JSON.stringify(kpList)}`;
// 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 }],