Files
openmaic/docs/handover.md
2026-08-16 14:58:47 +08:00

389 lines
16 KiB
Markdown
Raw Permalink 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.

# 麦洛学习项目交接
> 交接基线:2026-08-15
>
> 项目根:/Users/inmanw/项目/麦洛学习
>
> 代码:/Users/inmanw/项目/麦洛学习/OpenMAIC
>
> 文档:/Users/inmanw/项目/麦洛学习/docs
## 1. 一句话现状
三端能力边界、大型连续课程生成、完整 classroom 冻结发布和精确版本学习链路已经落地;
当前仍是一份 OpenMAIC Next.js 16.1.2 应用与文件仓库,learner desktop / ops / server
尚未物理拆分,发布层尚未迁到 Postgres/对象存储,learner 身份与资源 owner
也尚未完成。这两项都是公网生产上线前的硬前置。
## 2. 接手前必须知道的本机事实
| 项目 | 当前事实 |
|---|---|
| OpenMAIC 版本 | package version 0.3.2 |
| Next.js | 16.1.2 |
| 版本管理 | OpenMAIC 当前不是 Git worktree,没有可依赖的 commit/diff/rollback |
| 环境文件 | .env.local 当前存在;不要输出、分享或提交其中内容 |
| .env | 当前不存在 |
| 代码与文档位置 | 文档在 OpenMAIC 同级的 docs,不在代码目录内 |
| 存储 | 课程发布主链路仍使用 data 下的文件仓库 |
| 写入拓扑 | 只能单 Node.js 写实例 |
在建立 Git 基线前,任何后续改动都应小批次、保留触及文件清单,并先备份 data 与
.env.local。
## 3. 产品与能力矩阵
| 能力 | learner desktop | ops | server |
|---|---:|---:|---:|
| 普通文本单课件后台生成 | 是 | 同构代码仍可用 | 当前承载 job/API |
| 材料/Interactive/职教前台生成 | 是 | 同构代码仍可用 | 提供生成 API |
| 原单课件编辑与 Stage | 是 | 模块审阅 | 否 |
| 大型课程主 Agent | 否 | 是 | 当前同进程运行 |
| 框架人工确认、模块重试 | 否 | 是 | 否 |
| classroom 冻结与课程发布 | 否 | 冻结并发起 | 整批事务接收与发布 |
| courseware / manifest 读服务 | 消费 | 发布 | 主责 |
| 大课目录与精确模块学习 | 是 | 验收 | 提供数据 |
| Agent/Chat/白板/PBL/音频 | 原样保留 | 审阅时原样保留 | 供数据/模型端点 |
当前部署角色为 all、ops、server、learner。生产默认 learner,开发/测试默认 all。
大型课程运营能力由 role + ACCESS_CODE 会话保护;浏览器不再持有发布 token。
server 角色已是无头服务:所有页面以及运营/API 家族返回 404,但这不等于
其他 API 已有 learner 身份和资源级授权。当前已额外隐藏 provider 验证、
MP4 export、开发态 persistence、usage、全局 job 列表和未鉴权媒体代理;
原 Chat/Quiz/PBL/TTS 与单课件生成等待正式 learner capability,不得直接删除。
全局 job 列表的 HEAD 与 GET 同样隐藏,内部整课发布在尾斜杠形式下也仍是
POST-only。ops 远程发布成功后会用当前 publication receipt 保护 source classroom,
发布和 job/source 删除还有同进程互斥。这不包含被重生清除的历史回执,也不是
跨副本锁;多实例生产化时必须改为持久发布历史和数据库事务/分布式锁。
ACCESS_CODE 会话默认 TTL 为 7 天,cookie Max-Age 与服务端验证同步;登录只接受
同源 application/json,实际请求体最大 8 KiB,且有进程内失败限流。所有
cookie 授权的 ops 写请求还会校验同源 Origin。
反向代理部署要配置 OPS_PUBLIC_ORIGIN 作为 canonical origin;该校验不信任
X-Forwarded-Host。
## 4. 不可跨越的保护边界
本项目的原则是:不改原课堂内核,只改外围编排、权限、发布、目录和存储。
受保护范围:
- components/stage.tsx
- components/edit/PlaybackChromeRoot.tsx
- components/chat/**
- components/roundtable/**
- components/scene-renderers/**
- lib/playback/**
- lib/chat/**
- lib/action/**
- lib/whiteboard/**
- lib/pbl/**
- 原 Stage / Scene / Action DSL 和 /classroom/{id} 链路
验收不变式:
1. learner 发布模块仍走 /learn → /classroom → 原 Stage。
2. learner 外层没有第二套 AssistantPanel;原 ChatArea/Roundtable 是唯一课堂问答入口。
3. frozen bundle 带完整 Agent persona/voice、Stage/Scene 白板、四类 Scene、Action、
HTML、Quiz、PBL、媒体和 speech 音频。
4. 大课模块仍复用原单课件 agent,没有第二套内容生成器。
5. 拆分工作不得以修复无关 Chat 断言为理由修改 protected core。
## 5. 已实现主链路
### 5.1 learner desktop 单课件双路径
app/page.tsx 的当前分流:
- 普通纯文本需求 → POST /api/generate-classroom → 持久化后台 job;
- 有材料,或 Interactive/职教开关打开 →
lib/generation/foreground-session.ts 保存文件引用和 generationSession →
原 /generation-preview。
后台路径可以关闭页面后从 /tasks 查看;前台路径保留浏览器拥有的材料与原互动/职教生成。
### 5.2 大型课程
POST /api/courses
→ 主 Agent CourseFramework
→ framework_ready
→ 人工确认
→ 模块按序复用 classroom job
→ outputDigest / continuityInputRefs
→ 全部成功
主 Agent 输出 targetAudience、courseGoals、continuityContract,以及每模块
generationPrompt、输入/输出知识边界和 excludedTopics。任一模块失败会暂停后续模块;
修复后按顺序续跑。
所有大课模块固定 Interactive Mode 与 TTS。若没有生成的非空 HTML interactive Scene,
在 persistClassroom 前失败。
### 5.3 发布
validate CourseRecord snapshot
→ persisted classroom 转完整 frozen bundle
→ 每模块暂存 unpublished 精确版本
→ 再验证来源未变化
→ 本批切 published
→ schemaVersion=2 manifest
发布保留完整课堂,并拒绝缺 Agent、音频、媒体、HTML、连续性或发生 digest 漂移的模块。
每个 manifest module 固定 coursewareId + coursewareVersion + contentHash。
发布校验还使用 sourceRevisionHash 锁定完整持久化 classroom;ops 只在 server 返回
的 id/version/contentHash 逐项一致后保存带 sourceRevisionHash 的 publication receipt。
独立部署时,ops 把 metadata 与全部模块 ZIP 作为一个 multipart 请求发往
/api/internal/course-publish,server 只接受 COURSEWARE_PUBLISH_TOKEN Bearer。模块在
schema-v2 manifest exact pin 提交前不会进入公共 catalog/detail/download。未配置远程
origin 时的同源 fallback 复用同一内容幂等与可见性规则。
contentHash v2 忽略 manifest.json.exportedAt,使相同内容的远程/本地重试返回既有
manifest;无 contentHashVersion 的真正历史包仍按 v1 原始字节哈希验证。
### 5.4 学习
manifest 精确 pin
→ registry 精确版本
→ bundle 内外 id/version/hash/completeness 校验
→ 物化 Agent/音频/媒体/document
→ hash 隔离的本地 stage id
→ 原 Stage
发布文档不带 outlines,所以课堂页不会触发场景/媒体恢复生成。进入原 Stage 后,原课堂
教师/助教、动态评分和 PBL 运行时仍可按原设计调用模型。
## 6. 关键代码地图
### 角色与鉴权
| 路径 | 作用 |
|---|---|
| lib/config/deployment-role.ts | 角色解析、生产 fail closed、manage_courses |
| lib/server/ops-access.ts | ops role + ACCESS_CODE cookie route guard |
| middleware.ts | 运营页面/API 第一层边界 |
| app/api/coursewares/route.ts | ops 会话或 server Bearer 的双发布入口 |
| e2e/tests/deployment-role-boundary.spec.ts | learner 隐藏/拒绝运营能力 |
### 单课件双路径
| 路径 | 作用 |
|---|---|
| app/page.tsx | 普通后台与材料/Interactive/职教前台分流 |
| lib/generation/foreground-session.ts | 文件持久化与原 preview session 适配 |
| app/generation-preview | 原前台生成 |
| app/api/generate-classroom | 后台 job |
| lib/server/classroom-generation.ts | 原单课件服务端生成管线与 HTML 门 |
### 主 Agent 与连续性
| 路径 | 作用 |
|---|---|
| lib/course-framework/types.ts | CourseFramework、generationPrompt、digest/ref 契约 |
| lib/prompts/templates/course-framework/system.md | 主 Agent prompt |
| lib/course-framework/generate-framework.ts | 严格解析、校验与重试 |
| lib/course-framework/compat.ts | 旧 CourseRecord 读取兼容 |
| lib/course-framework/runner.ts | 人工确认后的顺序单课件复用 |
| lib/course-framework/module-digest.ts | 实际产出摘要与 semanticHash |
| lib/course-framework/publish-validation.ts | digest、连续性、HTML 发布门 |
| lib/course-framework/store.ts | 文件记录、原子更新、读时自愈 |
### 冻结与版本
| 路径 | 作用 |
|---|---|
| lib/server/classroom-courseware-publish.ts | persisted classroom → 完整 frozen bundle |
| lib/bundle/packager.ts | bundle 组装、完整性和内容 hash |
| lib/bundle/serialize.ts | Stage/Agent/Scene/Action portable manifest |
| lib/courseware-repo | append-only 课件版本与 bundle bytes |
| lib/course-manifest-repo | schema v2 课程清单历史 |
| app/api/courses/{id}/publish | 暂存、二次校验、提交、失败补偿 |
### learner
| 路径 | 作用 |
|---|---|
| app/api/learn/courses | 课程目录与精确 manifest;legacy 返回 409 |
| app/learn/course/{id} | 课程模块列表和精确 pin 导航 |
| lib/bundle/learner-load.ts | 严格身份/哈希/完整性校验与回滚 |
| lib/import/import-classroom-core.ts | portable manifest → 原课堂 document |
| app/learn/{coursewareId} | 加载并跳转 classroom |
| app/classroom/{id} | 仍挂原 Stage;learner query 只加返回导航 |
## 7. 环境与数据目录
关键角色变量:
- OPENMAIC_DEPLOYMENT_ROLE
- NEXT_PUBLIC_OPENMAIC_DEPLOYMENT_ROLE
- ACCESS_CODE
- ACCESS_CODE_SESSION_TTL_SECONDS
- OPS_PUBLIC_ORIGIN
- COURSEWARE_PUBLISH_TOKEN
- COURSE_PUBLISH_SERVER_BASE_URL
- COURSEWARE_PUBLIC_BASE_URL
严禁重新加入 NEXT_PUBLIC_COURSEWARE_PUBLISH_TOKEN。
关键目录变量:
- CLASSROOM_DATA_DIR
- CLASSROOM_JOBS_DIR
- COURSE_FRAMEWORK_DIR
- COURSEWARE_DATA_DIR
- COURSEWARE_BUNDLE_DIR
- COURSE_MANIFEST_DIR
- COURSEWARE_MAX_UPLOAD_BYTES
- COURSE_PUBLISH_MAX_UPLOAD_BYTES
- COURSE_PUBLISH_TIMEOUT_MS
模型按需配置 MODEL_ROUTES 与 provider/TTS/image/video/web-search 变量。
大课至少需要 course-framework 与原 generate-classroom/scene stages 可解析;
发布需要所有 speech 的持久化音频。
通用 NEXT_PUBLIC_PERSISTENCE、DATABASE_URL、ASSET_S3_BUCKET 不等于发布仓库已经迁移。
完整矩阵和示例见 deployment-3-tier.md。
默认数据:
| 目录 | 内容 |
|---|---|
| data/classrooms | classroom JSON、本地 media/audio 子目录 |
| data/classroom-jobs | 单课件 job |
| data/course-frameworks | ops CourseRecord |
| data/coursewares | courseware 版本历史 metadata |
| data/courseware-bundles | id/version ZIP |
| data/course-manifests | course manifest 版本历史 |
| data/usage | 计量 |
| data/tts-cache | QA/TTS 缓存能力仍在代码中 |
## 8. 单实例限制与未来存储
当前 version 分配与 read-modify-write 依赖进程内 Map 锁。临时文件 + rename/link 只能保证
文件操作层面的原子性,不能协调多个 Node.js 进程。发布失败只会由应用层把本批
记录补偿回 unpublished,不具有跨进程隔离性、原子提交或崩溃一致性。现阶段:
- ops/framework writer 单实例;
- server courseware/manifest writer 单实例;
- 生成和发布期间避免滚动重启;
- runner 中断后依赖持久记录自愈,再由运营续跑;
- 备份 metadata history 与 bundle bytes 必须成对。
公网生产上线或多实例前必须完成:
- Postgres 唯一键和事务分配 courseware/manifest version;
- 行锁或 advisory lock 保护每个 course/courseware;
- 对象存储以 id/version/hash 为 key 并 put-if-absent;
- CDN 只分发已被 schema-v2 manifest exact pin 的 immutable bundle;当前文件下载路由已用
同一门禁隐藏 promote→manifest 的中间状态,未来对象存储不能以公开 URL 绕过它;
- 持久任务队列取代内存 runner;
- ops 已可通过内部 HTTP 一次提交 metadata 与全部 bundle;独立部署必须配置
COURSE_PUBLISH_SERVER_BASE_URL,不再直接写 server 文件。
## 9. legacy manifest 重发
不要手工给旧记录填 current latest。
1. learner API 对旧记录返回 409,这是预期 fail closed。
2. 备份旧 singleton/history 文件。
3. 在 ops 打开原 CourseRecord 并运行发布校验。
4. 若缺 outputDigest、continuityInputRefs、Interactive HTML、音频或媒体,从最早失败模块
开始按顺序重生成。
5. 人工审阅后重发课程。
6. 新 manifest version 使用 schemaVersion=2 并精确 pin;旧版本保留审计但不可学习。
7. 若原 CourseRecord/classroom 已丢失,只能按新课程重新生产,不能伪造历史身份。
## 10. 验收结果与命令
### 已确认结果
| 检查 | 结果 |
|---|---|
| 首页双路径 Playwright | 3 passed |
| learner 部署边界 Playwright | 1 passed |
| 三端角色/ops access 定向单测 | 通过 |
| 主 Agent、连续性、Interactive 回归 | 通过 |
| 冻结发布相关合并回归 | 10 files / 85 tests 通过 |
| 本次文档交接定向回归 | 18 files / 108 tests 通过 |
| 本次 TypeScript 检查 | pnpm exec tsc --noEmit,exit 0 |
| 本次四文档格式检查 | Prettier,exit 0 |
| 受保护课堂广泛回归 | 130 files / 1,294 tests 中 1,291 通过,3 个 Chat 基线漂移 |
| 本次隔离 Chat 复现 | 3 files;40 passed / 3 failed |
| i18n key check | exit 1;9 个非 zh-CN/en-US locale 各缺 180 个键 |
### 定向回归
在 OpenMAIC 目录执行:
pnpm exec vitest run \
tests/config/deployment-role.test.ts \
tests/server/ops-access.test.ts \
tests/generation/foreground-session.test.ts \
tests/server/classroom-outline-mode.test.ts \
tests/server/classroom-generation-retry.test.ts \
tests/course-framework \
tests/bundle \
tests/courseware
### 静态与构建
pnpm exec tsc --noEmit
pnpm build
pnpm exec prettier ../docs/architecture-3-tier.md ../docs/deployment-3-tier.md ../docs/large-course-mode.md ../docs/handover.md --check
### 浏览器边界
pnpm exec playwright test \
e2e/tests/home-to-generation.spec.ts \
e2e/tests/deployment-role-boundary.spec.ts \
--project=chromium
### i18n 已知缺口
pnpm check:i18n-keys
该命令当前预期失败,输出为 ar-SA、es-MX、fr-FR、ja-JP、ko-KR、pt-BR、ru-RU、
vi-VN、zh-TW 各缺 180 个相对 en-US 的键。运行时有 fallback,但发布前应补齐。
## 11. 已知 3 个受保护 Chat 基线漂移
隔离命令:
pnpm exec vitest run \
tests/lib/chat/pi/director-tool-wiring.test.ts \
tests/lib/chat/pi/route-cue-user.test.ts \
tests/lib/chat/pi/prompts.test.ts
稳定失败:
1. tests/lib/chat/pi/prompts.test.ts
- keeps concept and mechanism questions teacher-led before student reactions
- 当前 prompt 明确课堂只有 teacher + user,旧断言仍期待 assistant fallback 文字。
2. tests/lib/chat/pi/route-cue-user.test.ts
- hands successful web evidence to one child and clears it before later delegations
- 旧 fixture 断言 URL 必须直接出现在 child prompt 字符串。
3. tests/lib/chat/pi/route-cue-user.test.ts
- does not leak consumed web evidence after the selected child fails
- 同属 evidence attachment 与旧 prompt 字符串断言漂移。
大型课程和冻结发布改造未修改 lib/chat/** 或 app/api/chat/pi。按用户确定的保护边界,
这 3 项只记录、隔离复现,不在本改造中修改 Chat 内核、prompt 或断言。
## 12. 未完成事项
按优先级:
1. 建立 Git 基线并备份 data/.env.local。
2. 完成 learner 用户身份、classroom/job owner、资源级授权、课程权益和成本限额。
在此之前,不得宣称匿名 Chat、生成、TTS、搜索等高成本 API 已安全。
3. 把 courseware/manifest metadata 迁到 Postgres、bundle 迁到私有对象存储,并在 CDN
层保留 manifest exact-pin 可见性门禁。
4. 按 deployment-3-tier.md 的顺序拆 server → learner desktop → ops;内部整批 HTTP adapter
已完成,可直接迁移而不是重新设计。
5. 将大课目录 localStorage 完成标记改为 runtime 进度聚合。
6. 补齐 9 个 locale 的 180 个键。
7. 在真实部署网络上补一条 ops → server → learner smoke;仓内已有两个隔离文件根、mock
fetch 的无 LLM 跨进程契约测试。
拆分期间继续坚持:移动边界、替换 adapter、保持契约;不要复制或重写原 Stage/Chat。