From a427bba8c8e592ff0c1df835dbaa87add5b9bd45 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 24 Jun 2026 05:07:19 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=8E=E4=B9=A6=E7=B1=8D=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=95=99=E6=A1=88=EF=BC=88=E7=B2=98=E8=B4=B4=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=20+=20=E4=B8=8A=E4=BC=A0=20md=20=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E5=8C=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增「从书籍生成」流程:用户粘贴教材目录(多行)并上传内含各章 .md 的 ZIP(文件名=目录标题),前端用 JSZip 解压匹配,AI 按章节 体量划分课时,预览/编辑后批量生成,每篇以对应章节正文作为上下文。 服务端 - generate.ts: 新增 POST /lessons-from-book(目录条目→AI 划分课时 JSON,剥围栏/校验/丢非法行);扩展 POST / 支持可选 content 上下文, 无 content 时请求体不变(向后兼容)。 前端 - bookImport.ts: normalizeTitle / parseZip / matchToc / assembleContent。 - booksApi.ts: generateLesson 兼容签名 + divideLessonsFromBook。 - useTeachingBook.ts: 抽出 runGenerationPool,新增 generateLessonsFromBook。 - BookImportDialog.vue 多阶段对话框;UploadDropzone 加 accept/multiple; 菜单串接「从书籍生成」。 顺带修复 5 个遗留失败测试:import 功能已于 fb0b8d1 删除但测试漏删, 清理孤儿测试;批量生成对齐为单参 generateLesson(topic)。 docs/book-import-spec.md 记录规格。 Co-Authored-By: Claude Opus 4.8 --- docs/book-import-spec.md | 98 ++++++++++ server/routes/generate.test.ts | 145 +++++++++++++++ server/routes/generate.ts | 116 +++++++++++- src/components/BookImportDialog.test.ts | 90 ++++++++++ src/components/BookImportDialog.vue | 207 ++++++++++++++++++++++ src/components/GenerateMenuButton.test.ts | 9 + src/components/GenerateMenuButton.vue | 13 ++ src/components/UploadDropzone.vue | 8 +- src/components/WorkspaceToolbar.test.ts | 7 + src/components/WorkspaceToolbar.vue | 7 +- src/components/WorkspaceView.test.ts | 5 +- src/components/WorkspaceView.vue | 60 ++++++- src/composables/useTeachingBook.test.ts | 74 +++++--- src/composables/useTeachingBook.ts | 54 ++++-- src/services/bookImport.test.ts | 60 +++++++ src/services/bookImport.ts | 75 ++++++++ src/services/booksApi.test.ts | 37 ++++ src/services/booksApi.ts | 36 +++- src/style.css | 48 +++++ 19 files changed, 1101 insertions(+), 48 deletions(-) create mode 100644 docs/book-import-spec.md create mode 100644 src/components/BookImportDialog.test.ts create mode 100644 src/components/BookImportDialog.vue create mode 100644 src/services/bookImport.test.ts create mode 100644 src/services/bookImport.ts diff --git a/docs/book-import-spec.md b/docs/book-import-spec.md new file mode 100644 index 0000000..17a0c45 --- /dev/null +++ b/docs/book-import-spec.md @@ -0,0 +1,98 @@ +# 从书籍生成教案 — 功能规格(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`:目录粘贴 `