# 从书籍生成教案 — 功能规格(Spec) ## 背景与目标 当前 FakeTeachingDesign(Vue 3 + Hono/Bun + SQLite,DeepSeek `deepseek-v4-flash`)只能: - **生成一篇**:手输一个主题 → `/api/generate` 生成单篇教案。 - **批量生成**:输入主题 → `/api/generate/outline` 生成标题列表 → 逐篇生成。 缺口:无法基于**真实教材内容**生成教案。本功能让用户提供一本教材的**目录**与**正文**,由 AI 据此划分课时,用户确认后批量生成,且生成每篇时把对应章节正文作为上下文喂给 AI,产出更贴合教材的教案。 ## 输入(用户提供) 1. **目录**:多行文本,用户**粘贴**到文本框。每行一个标题,给出课程的**顺序**与**完整标题清单**。 2. **正文压缩包**:用户**上传**一个 ZIP,内含多份 `.md`,**文件名 = 目录中的标题**,文件内容 = 该标题对应的正文。 ## 核心流程 ``` 粘贴目录(多行) + 上传 ZIP │ ▼ 前端解压(JSZip) → {标题 → 正文} 映射 → 与目录每行按文件名匹配 │ 得到 entries: [{index, title, content, charCount, matched}] ▼ POST /api/generate/lessons-from-book { entries: [{index,title,charCount}] } │ AI 据目录与各条体量「再划分课时」(可合并/拆分) ▼ 返回 lessons: [{title, sourceIndexes:[...]}] │ ▼ 预览/编辑课时列表(改标题、删行) │ 前端按 sourceIndexes 拼出每课时的正文上下文 ▼ 批量生成: 每课时 POST /api/generate { topic, content } │ content = 该课时所辖章节正文(拼接并截断 ~6000 字) ▼ 逐篇 parseTeachingDesign → 追加进 book.designs(复用现有有序追加/进度/取消) ``` ## 关键决策 - **解压与匹配全在前端**:`JSZip` 已是项目依赖(`src/services/zipExporter.ts` 在用)。浏览器内 `JSZip.loadAsync` 解压,无需 PDF 解析、OCR、服务端缓存、multipart 上传——发给后端只是 JSON。 - **AI 再划分课时**:目录条目(章/节)不一定 1:1 对应课时。AI 据标题 + 各条字数(`charCount`,廉价信号,不传全文)决定合并小节、拆分大章。 - **正文随生成请求直传**:每课时生成时把拼好的正文作为 `content` 字段直接 POST,无服务端缓存、无 `parseId`。 - **纯增量扩展 `/api/generate`**:新增可选 `content`,老调用方仍只传 `{topic}`,现有测试不变。 ## 标题↔文件名 匹配规则 ZIP 内文件名可能带 `.md` 后缀、含被替换的非法字符(现有 `sanitizeFilename` 把 `[\\/:*?"<>|]` 换成 `_`)。匹配时对**两侧都归一化**后比对: 1. 去掉 `.md` 后缀; 2. `trim`,把连续空白折叠为单空格; 3. 把 `[\\/:*?"<>|]` 替换为 `_`(与服务端 `sanitizeFilename` 一致)。 归一化后字符串相等即匹配。匹配不到的目录行 `matched=false`、`content=''`(仍可生成,但只靠标题)。 ## 接口 ### `POST /api/generate/lessons-from-book`(新增,挂 `/api/generate/*`,复用现有鉴权) 请求: ```json { "entries": [ { "index": 0, "title": "第1章 C# 入门", "charCount": 5821 } ] } ``` - 系统提示要求 AI 据目录把全书拆为单课时课题,每条对应一个或多个目录条目,难度由浅入深、覆盖全部条目,复杂条目可拆多课时;**仅输出 JSON**,形如 `{"lessons":[{"title":"项目名——课时任务","sourceIndexes":[0]}]}`。 - 复用现有 `fenceMatch`(`generate.ts:86-87`)剥代码块围栏;`JSON.parse` 后校验每行 `title:string` + `sourceIndexes:number[]`(索引在 entries 范围内),丢弃非法行。 响应: ```json { "lessons": [ { "title": "C# 入门——搭建环境运行首个程序", "sourceIndexes": [0] } ] } ``` 错误:400 缺 `entries`;500 无 key;502 DeepSeek 失败/空。 ### `POST /api/generate`(扩展,向后兼容) - 读可选 `content:string`。有则把它作为编写依据拼进 user 消息(提炼要点、不照抄),系统提示不变,返回 `{filename, markdown}` 不变。 - 无 `content` 时请求体与现状完全一致。 ## 前端 - **菜单**:`GenerateMenuButton.vue` 加「从书籍生成」(`data-testid="book-import"`),经 `WorkspaceToolbar` → `WorkspaceView`。 - **对话框** `BookImportDialog.vue`(仿 `BatchGenerateDialog.vue` 多阶段): - `input`:目录粘贴 `