Files
teaching-design/docs/book-import-spec.md
yuetsh a427bba8c8 feat: 从书籍生成教案(粘贴目录 + 上传 md 压缩包)
新增「从书籍生成」流程:用户粘贴教材目录(多行)并上传内含各章
.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 <noreply@anthropic.com>
2026-06-24 05:07:19 -06:00

6.8 KiB
Raw Blame History

从书籍生成教案 — 功能规格Spec

背景与目标

当前 FakeTeachingDesignVue 3 + Hono/Bun + SQLiteDeepSeek 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=falsecontent=''(仍可生成,但只靠标题)。

接口

POST /api/generate/lessons-from-book(新增,挂 /api/generate/*,复用现有鉴权)

请求:

{ "entries": [ { "index": 0, "title": "第1章 C# 入门", "charCount": 5821 } ] }
  • 系统提示要求 AI 据目录把全书拆为单课时课题,每条对应一个或多个目录条目,难度由浅入深、覆盖全部条目,复杂条目可拆多课时;仅输出 JSON,形如 {"lessons":[{"title":"项目名——课时任务","sourceIndexes":[0]}]}
  • 复用现有 fenceMatchgenerate.ts:86-87)剥代码块围栏;JSON.parse 后校验每行 title:string + sourceIndexes:number[](索引在 entries 范围内),丢弃非法行。

响应:

{ "lessons": [ { "title": "C# 入门——搭建环境运行首个程序", "sourceIndexes": [0] } ] }

错误400 缺 entries500 无 key502 DeepSeek 失败/空。

POST /api/generate(扩展,向后兼容)

  • 读可选 content:string。有则把它作为编写依据拼进 user 消息(提炼要点、不照抄),系统提示不变,返回 {filename, markdown} 不变。
  • content 时请求体与现状完全一致。

前端

  • 菜单GenerateMenuButton.vue 加「从书籍生成」(data-testid="book-import"),经 WorkspaceToolbarWorkspaceView
  • 对话框 BookImportDialog.vue(仿 BatchGenerateDialog.vue 多阶段):
    • input:目录粘贴 <textarea> + UploadDropzoneaccept .zip,单文件)。校验:目录非空 + 已传 ZIP。点「解析」→ 前端解压匹配展示匹配概况如「20 行目录18 行匹配到正文2 行缺正文」)。
    • division-loading:调 divideLessonsFromBook(entries)提示「AI 正在划分课时…」。
    • preview可编辑课时列表改标题、删行显示每课时所辖章节标题。「开始生成」emit start:[lessons](含拼好的 content
    • running / done / error:与 batch 相同(进度条 + 停止)。
  • 解压匹配工具 src/services/bookImport.tsparseZip(file) 用 JSZip 读所有 .mdMap<归一化标题, 正文>matchToc(tocLines, map)entries[]
  • API src/services/booksApi.tsgenerateLesson 增加 content 选项(兼容旧 AbortSignal 第二参);新增 divideLessonsFromBook(entries);新增类型 BookEntryProposedLesson
  • Store useTeachingBook.ts:新增 generateLessonsFromBook(lessons:{title,content}[], options),复用 generateLessons:199-265)的 worker 池(并发 3、有序追加、可取消、进度回调每个 worker 调 generateLesson(title,{content,signal})

非目标

  • 不支持 PDF / Word / 扫描件 / OCR仅 ZIP-of-md + 粘贴目录)。
  • 不做章节↔课时映射的高级可视化编辑(预览仅改标题、删行)。
  • 不持久化上传的原始 ZIP/目录(仅用于一次生成)。

验证

  • 服务端 bun test server/lessons-from-book 从 mock JSON 解出 lessons、畸形 JSON 优雅处理、400 缺 entriesPOST /contentcontent 出现在捕获的 DeepSeek 请求体,且不带时请求体不变(向后兼容)。
  • 前端 vitest runbookImport.ts 解压+匹配(含归一化、缺正文)单测;booksApigenerateLesson('x') 仍发 {topic:'x'}、带 content 时附加;divideLessonsFromBook{entries}BookImportDialog 走 input→preview→start 流程,断言 start 载荷;useTeachingBook.generateLessonsFromBook 有序追加 + 取消。
  • 类型检查npx vue-tsc -b
  • 手动 E2E:粘贴一份目录 + 传一个 md 的 ZIP → 看匹配概况 → 确认 AI 课时 → 改标题/删行 → 开始生成 → 教案追加、内容贴合章节;负向:目录空 / 传非 ZIP / 全部不匹配 → 友好提示。