Files
openmaic/OpenMAIC/task_plan.md
2026-08-16 14:58:47 +08:00

21 KiB
Raw Permalink Blame History

麦洛教育大型课程改造计划

Goal

在不改动 OpenMAIC 原互动课堂内核的前提下,建立用户端、运营端、服务端的可验证能力边界,并把现有大型课程骨架升级为“主 Agent 规划连续课程、逐模块复用原单课件生成、发布后仍进入原课堂”的生产链路。

Current Phase

Phase 7公网安全网络与身份边界

Protected Core

除修复原项目自身独立缺陷外,本项目改造不得修改:

  • 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 与单课件课堂播放链路

Phases

Phase 1基线与第一批外围改造

  • 确认产品边界与保护范围
  • 审计现有大课、课堂运行时和三端边界
  • 建立部署角色/运营权限的服务端边界
  • 移除学习端额外助教挂载,保留原课堂助教为唯一入口
  • 恢复用户端材料/互动/职教单课件生成入口
  • 补充对应自动化测试
  • Status: complete

Phase 2大型课程主 Agent 契约

  • 主 Agent 显式生成每模块 generationPrompt
  • 增加课程目标、术语、教学风格、难度和考核连续性契约
  • runner 传递语言、时长、知识边界和模块提示词
  • 保持旧课程记录兼容
  • Status: complete

Phase 3跨模块连续性与互动生成

  • 生成并持久化每模块输出摘要/知识覆盖
  • 后续模块读取前序实际产出
  • 大课模块使用原 Interactive Mode 生成能力
  • 验证实际产生可操作 HTML 互动场景
  • Status: complete

Phase 4冻结发布与版本锁定

  • 打通服务端 classroom 到 frozen bundle 的发布路径
  • 保留 Agent persona/voice、媒体、音频和必要 Stage 契约
  • 大型课程 manifest 锁定模块版本与内容哈希
  • 学习端只加载 manifest 指定版本
  • Status: complete

Phase 5三端拆分准备与验收

  • 完整能力矩阵和部署文档
  • 原课堂交互回归完成1,291/1,294 通过3 个受保护 Chat 旧基线漂移已隔离记录)
  • 大课生成、冻结发布、manifest 精确加载的无 LLM 端到端数据边界通过
  • 基于稳定边界制定 desktop/ops/server 机械拆分计划
  • Status: complete

Phase 6独立部署边界加固

  • Docker learner/ops/server 分别在构建期内联正确公开角色
  • learner 的任务与大课目录不依赖 ops API运营页面全部受保护
  • 上游模块重生成会级联失效并顺序重生下游
  • learner 不能绕过 manifest pin 读取大课源 classroom且 learner 课堂不恢复课程生成
  • 浏览器重发与 learner loader 严格校验 bundle 内部 version
  • 运营 ACCESS_CODE 会话具备 TTL、登录限流与同源写保护
  • server 部署无 UI且不暴露运营页面/API
  • 定义并实现 ops → server 的跨进程事务发布契约
  • 大课模块在 manifest exact pin 提交前不对 learner 可见
  • 冻结发布具备全源 revision 校验、路径防穿越和同版本 ZIP 完整性复核
  • server 隐藏明确非课堂必需的高风险 API并阻断 metadata SSRF 与已发布源删除绕过
  • 将数据库事务、对象存储、共享锁与资源 owner 明确列为生产上线硬前置,不在单实例文件仓库阶段伪装完成
  • Status: complete

Phase 7公网安全网络与身份边界

  • 建立 DNS 固定解析、逐跳重定向校验的 public safe-fetch并迁移匿名媒体代理
  • 盘点其余接收用户 URL、上游返回 URL 与 SDK/fetch 出站链路并完成风险分级
  • 封住公网 server 的 unmanaged BYOK并加固 AliDoc、MinerU、Qwen 的 provider-returned URL
  • 按 trust class 迁移 provider/生成路由,生产网络增加 metadata/private egress deny
  • 复核专用 server 的 method/path 边界和已发布源删除保护
  • 完成 learner 身份方案审计:正式账号为长期主体、桌面设备密钥做持有证明、课堂使用短时 exact-resource grant
  • 建立 provider-neutral principal/ownership 纯策略,并以 job 做不改变响应的 observe-only 竖切
  • 用户确认正式账号为永久 owner/权益主体,设备身份只做持有证明或可迁移游客
  • 用户确认登录渠道、多租户、课程可见性/离线、BYOK 与学习记录可信度策略
  • 建立 owner 不可变绑定、显式 legacy/guest 迁移和可切换 fail-closed enforcement
    • 定义 shadow/required 模式、稳定 deny 响应与账号/设备 subject 映射
    • job create 原子绑定 owner/guest普通状态 patch 禁止改归属
    • job list/detail/cancel/resume/delete 在 required 模式按资源归属 fail closed
    • legacy/guest → account 仅允许显式 compare-and-set 迁移
    • 课堂继承 job owner直接写入不能覆盖已有归属
    • 课堂 GET/POST 在 required 模式按 owner/guest/capability fail closed
    • 课堂 legacy/guest 显式 compare-and-set 迁移,归属不进入 Stage/Scene
    • 完成扩展兼容、越权、迁移和原课堂回归
  • 为 job/classroom/voice/render/draft 建立 owner 与资源级授权
    • job
    • classroom
    • voice/render/draft
  • 为 Chat、Quiz/PBL 动态评分、TTS 与单课件生成建立 entitlement、配额和并发边界
  • 多实例上线前迁移到数据库事务、对象存储、分布式锁/CAS 与共享限流
  • Status: in_progress

Decisions Made

Decision Rationale
先在当前应用封能力边界,后物理拆成三端 避免把未稳定的权限、发布和生成问题复制到三套应用
原互动课堂作为受保护内核 用户明确要求完整保留 HTML 互动、原教师/助教、Quiz、PBL、白板和语音
大课始终复用现有单课件生成与同一 Stage 播放链路 保持交互品质且降低分叉维护成本
同一课程同一时间只允许一个生成 run 保证模块顺序和提示词快照语义,避免整课流水线与单模块重试互相覆盖
不再使用 DeepSeek Harness 用户已明确要求停止使用;后续只用本地实现、验证和内置协作能力
受保护 Chat 基线失败只记录、不顺手改 用户明确要求保持原课堂交互代码Phase 4 未触及这些文件,避免把独立 Chat 语义选择混入大课改造
正式 learner 采用“账号主体 + 设备持有证明 + 课堂短时 grant”三层身份 账号负责权益/跨设备/找回,设备密钥降低令牌复制风险,精确资源 grant 在不改课堂内核的前提下约束 Chat/QA/PBL/TTS
身份提供方未确定前只做 observe-only 授权竖切 当前 resolver 明确返回 unavailable不信任请求头也不让授权判断改变既有响应待 owner 主体确认后再启用 fail-closed enforcement
公网 server 的 provider URL 必须按 trust class 分流 server-managed 公网地址、运营自托管私网地址与 learner BYOK 不能继续共用 ALLOW_LOCAL_NETWORKS;先封公开 BYOK再逐步迁移 secure transport
正式账号是永久 owner/权益主体 用户已确认;设备密钥只证明设备持有,游客身份必须可显式迁移,不能成为不可恢复的永久权益主体

Errors Encountered

Error Attempt Resolution
当前工作区没有 Git 仓库,无法依赖 git diff/rollback 1 严格记录触及文件、使用小批次补丁并逐批测试
三个 DeepSeek Harness 只读任务并行调用超过预期超时且未返回结果 1 终止该组调用;后续缩小任务并改为串行,避免重复相同失败方式
首个补丁 Prettier 检查发现两个新文件格式不符合项目规则 1 使用项目固定版本 Prettier 机械格式化后重新检查
运营边界首轮验证:课程页残留 publishToken、测试环境 all 绕过发布 Token、zsh 展开方括号路径 1 删除残留 UI 变量引用;仅显式 ops/本地 development 使用会话发布test/all 保持 Bearer 合约;格式检查改用逐项引用路径
Prettier 无法为 .env.example 推断 parser 1 环境示例保持人工格式;后续 Prettier 清单排除该文件
新增部署边界 E2E 首次格式检查未通过 1 使用项目 Prettier 格式化单文件后再运行浏览器测试
Phase 2 测试中的无参数 mock 让 TypeScript 将调用参数推断为空元组 1 为 framework AI mock 显式声明 system/user 两个字符串参数
Phase 2 首轮 UI 类型检查仍把详情模块识别为持久化模块,且 compat 局部变量命中 Next.js 保留名 1 CourseDetail 显式覆盖为 CourseModuleView[];局部变量由 module 改名为 moduleObj
Phase 3 prompt adapter 测试再次把无参数 mock 推断成空元组 1 给 adapter 的 base AICall mock 显式声明 system/user/images 参数
Phase 4 发布路由首个补丁使用了已漂移的 import 上下文,无法套用 1 重新读取文件后缩小补丁并成功应用
Phase 4 learner loader 既有测试仍使用无哈希详情和旧 stage ID 1 已定位为测试 fixture 漂移,正在更新为 version+hash 身份契约
更新 Phase 4 计划日志的首个补丁匹配了旧错误文案 1 rg 读取实际上下文后重新套用
Phase 4 冻结包测试发现 audioRef 存在时 ...rest 仍泄漏 audioUrl 1 两处 speech 序列化先显式剔除 audioUrl仅在无 audioRef 时回填
受保护课堂回归 1,294 项中 3 项既有 Chat director 断言失败 1 Phase 4 相关 1,291 项通过;正在只读定位基线漂移,不跨边界修改 Chat 内核
Phase 5 Playwright 首轮把部署边界用例跑在非 learner 的 dev server 上 1 E2E webServer 显式固定 learner 角色,隔离重跑 1 项通过
受保护浏览器回归并行跑 9 项时 5 项失败 1 4 项通过3 项卡在并行 IndexedDB seed另 2 项为跳转/旧场景断言,改为单 worker 隔离诊断
中断 Playwright 后遗留的 Next dev server 进程占用 3002 但不再响应 1 终止由本次测试启动的残留进程,重新由 Playwright 启动干净 learner 服务
zsh 将 Next.js 动态路由的方括号当作 glob首次读取 classroom-media 路由失败,且假定了错误的参数名 1 find 确认实际 [classroomId] 路径,之后对动态路径整体加引号
为 registry 增加 fail-closed source 反查的首个合并补丁匹配了 Prettier 前的单行类型签名 1 重新读取已格式化上下文,拆成三个小补丁后成功
Phase 6 manifest repo 工厂首个大补丁因上下文过大且含误字无法套用 1 重读当前文件,改为边界更精确的局部替换并成功
Docker CLI 不可用,无法运行 docker compose config 1 js-yaml 静态解析 Compose 并实测 ops 角色构建;待有 Docker 环境时再做容器 smoke test
级联重生首次测试对“省略的可选键”使用了错误匹配方式 1 改为 not.toHaveProperty 后重跑 47 项全绿
ACCESS_CODE 加固首轮 TSC 发现角色缩小后的冗余分支与 WebCrypto BufferSource 类型差异 1 删除冗余分支并显式传递 ArrayBuffer,后续 TSC/构建通过
文档检查首次假定中文 README 名为 README.zh-CN.md 1 ls README* 确认实际文件为 README-zh.md 后重跑
Phase 7 查看并发 safe-fetch 文件时 zsh 对尚不存在的 *safe* glob 报 no matches found 1 改用 rg --files/显式路径检查,不再用未解析 glob
Phase 7 主线收口补丁使用了代理已更新前的 server 复核状态行 1 重读计划与进度后保留代理结果,只追加主线独立验证
复算受保护内核哈希的两个探索命令分别遇到 zsh 字符串不拆词和嵌套 awk 转义错误 1 停止依赖猜测命令;以已记录基线哈希、明确触及文件清单和后续受保护回归共同验证,本批身份文件全部位于受保护边界外
AliDoc HTTP→HTTPS 回归用例首次 Prettier check 未通过 1 使用项目固定 Prettier 格式化该测试后重验通过
Qwen 超时回归首轮在 Promise 可能拒绝后才挂断言Vitest 报 unhandled rejection 1 在推进 fake timer 前先挂载 rejection 断言,干净重跑通过
provider 扩展回归受宿主 HTTP(S)_PROXY 影响4 个既有 Response mock 被 Undici 真实代理接管 1 清除代理环境变量后重跑99 files / 773 tests 通过,确认非代码回归
主线审查时假定 extract-document 测试位于 tests/api 1 rg --files 找到实际 tests/document/extract-document-route.test.ts 后继续复核
provider 403 映射测试首轮 mock 使用 rest 参数导致 TypeScript 签名不匹配 1 改为显式参数签名,随后 TSC 与回归通过

| Phase 6 远程发布新测试首轮 4 项中 ops receipt 闭环返回 409 | 1 | 结构化错误定位为 mock fetch 与 ops route 共用同一进程 publish mutex测试用独立 module instance 模拟两个 Node 进程后通过 | | 为跨进程测试加独立 module instance 的首个补丁因 Prettier 已收缩 import 格式而上下文不匹配 | 1 | 读取当前行后以精确单行上下文成功套用 |

| Phase 6 远程发布复验时全量 TSC 出现 classroom-storage-security.test.ts 两处 Stage fixture 缺少时间字段 | 1 | 本批首轮 TSC 已通过且未触及该文件;已向主线报告并由并发 security 任务修正 fixture本任务先继续定向检查 |

| Phase 6 幂等复验发现同一 classroom 在旧哈希契约下因不同 publishedAt 产生不同 contentHash | 1 | 新冻结包升级为兼容 v2 哈希并规范化导出时间;旧包保持 v1 验证,远程与本地同内容重试现在都返回既有 manifest |

| 将稳定 publishedAt 与测试回退合在一个补丁时,测试注释已被 Prettier 压成单行导致上下文失配 | 1 | 分别读取 production/test 精确上下文后用小补丁成功完成 | | 本地 fallback 复用统一事务后publish route 的 result 被推断为永远成功,旧错误分支触发 TS2339 | 1 | 删除已不可达的旧 union 分支;事务错误统一通过结构化异常进入现有 catch随后全量 TSC 通过 |

Acceptance Invariants

  1. 发布模块仍走 /classroom → Stage → PlaybackChromeRoot → SceneRenderer,不存在 learner 专用简化播放器。
  2. 原 ChatArea/Roundtable 是课堂唯一问答入口,能够携带课堂上下文并中断/恢复讲解。
  3. HTML iframe、四类 Scene、Action、Quiz、PBL、白板、音频和讨论 TTS 不因大课改造退化。
  4. 用户端可生成单课件;只有运营端可制作、重生成和发布大型课程。
  5. 大课模块由主 Agent 生成专属提示词,后续模块理解前序模块实际覆盖内容。
  6. 已发布大型课程锁定具体模块版本,不随模块单独重发而静默漂移。

生成任务恢复专项计划2026-08-16

触发事件

本次 Python 单课件和初一英语大型课程都暴露了同一组外围缺陷:生成 runner 只存在于 Node 进程内存中,后端进程重启后磁盘上的 running 记录没有可接管的执行者;单课件生成又只在全部场景完成后一次性持久化,因此失败或重启时没有场景级检查点。

专项目标

在不改动原 Stage/Scene/Action、播放、Chat、Quiz、PBL、白板和 iframe 内核的前提下:

  • 后端重启后把孤儿任务变成明确的“可恢复中断”,不把它伪装成正常完成,也不让活跃长调用被误判为失败。
  • 每完成一个完整场景就持久化私有检查点;恢复时从最后一个完整场景继续,而不是重跑整个课件。
  • 大型课程普通“继续”只重试失败模块及其尚未完成的下游,保留已成功模块;只有显式“从此重新生成”才清除级联下游。
  • 旧 JSON 任务、旧课程记录和没有检查点的历史任务继续可读;历史任务最多退化为从头重跑,不静默丢失或覆盖。

约束与非目标

  • 检查点只属于服务端 job/course 数据,不进入 Stage/Scene DSL也不暴露给 learner。
  • 最终 classroom 仍保持原子可见:检查点是恢复材料,不是半成品课堂;只有完整生成通过后才写入正式 classroom。
  • 第一阶段兼容当前文件仓库和单实例开发环境;多实例生产仍必须把 job/checkpoint/lease 迁移到数据库、对象存储和分布式锁/CAS。
  • 在现有 Python 任务未明确停稳前不热更新生成链路;需要中断时保留任务 JSON禁止删除以免失去故障样本。

分阶段实施

R0故障数据保护与兼容基线

  • 为现有 runningfailedcancelledsucceeded 记录建立读取兼容矩阵。
  • 明确 Xjk5UcLtOxS9V3Mm0AWy 和旧 Python 大课 YtqWlpO5zF 的迁移/重试语义;不把它们混成同一条课程。
  • 增加一次性故障样本快照测试,确保中断、恢复和旧记录读取不依赖实际模型。

R1可接管的 job lease、心跳与进程恢复

  • 在 job 中增加 attemptIdrunnerIdheartbeatAtleaseExpiresAtresumeReasoncheckpointVersion
  • runner 启动时取得带 token 的 lease所有状态、进度和检查点写入都校验 token旧进程不能覆盖新 runner。
  • 用定时心跳延长 lease把当前“读取时直接把 running 映射为 failed”的逻辑改成显式 orphan recovery避免单次模型调用超过 30 分钟时误判。
  • 后端启动或首次访问时扫描过期 lease将任务置为 interrupted/resumable;不自动重新计费/运行,等用户显式继续。
  • 对没有 checkpoint 的旧任务保留兼容路径:继续时从头运行一次,并在界面明确标注“旧任务无断点”。

R2场景级 checkpoint 与模型调用边界

  • 抽出可序列化的 GenerationCheckpoint输入指纹、outline 快照、Stage 快照、已完成 Scene/Action、下一个场景索引和生成阶段。
  • 大纲生成完成后先保存 checkpoint每个场景的 content 与 actions 都完整成功后再一次性追加该场景 checkpoint。
  • 恢复时复用同一输入/大纲/Stage 快照,从 nextSceneIndex 开始;中断在场景内部时只重做当前场景,不能产生重复场景。
  • 检查点写入使用原子文件替换,并限制单 job 大小后续生产迁移到对象存储JSON 记录只保留引用和摘要。
  • 将 job 的 AbortSignal 传入所有生成调用,增加可配置的单次 LLM timeout、有限重试和明确的 MODEL_TIMEOUT 错误;心跳不能替代 timeout。
  • 媒体/TTS 阶段单独记录阶段和已完成资源;第一版至少保证场景生成可断点,媒体/TTS 失败可重试且不污染已完成场景。

R3大型课程续跑与 UI 语义

  • 失败模块保留其 job/checkpoint 引用;普通 resume 复用检查点,不因为创建新 job 就丢掉上一个尝试的恢复材料。
  • runModulePhase 继续跳过 succeeded 模块;失败模块从最近检查点继续,后续模块仍严格按顺序生成。
  • 只有显式模块级 regenerate 才执行现有的 N..end 失效级联;普通 resume 不清除前序成功模块和当前模块已有检查点。
  • UI 区分“后端中断,可从场景 N 继续”“模型调用失败,重试当前场景”“旧任务无检查点,将从头重跑”,避免都显示成笼统的失败。
  • 对 Python 单课件和大型课程模块使用同一 checkpoint 协议,但保留两层恢复语义:单课件恢复场景,大课恢复模块内场景并继续课程顺序。

R4故障演练、回归与上线门槛

  • 单测覆盖 checkpoint schema、旧记录兼容、原子写入、重复 resume、旧 runner token、过期 lease 和损坏 checkpoint fail closed。
  • 集成测试在大纲后、场景完成后、场景内部、媒体阶段、TTS 阶段和最终 persist 前模拟进程终止,再由新 runner 恢复。
  • 大课程测试验证:模块 1 失败后只重做模块 1模块 13 成功、模块 4 中断时继续保留 13并按顺序完成 4 以后模块。
  • 运行真实文件仓库的恢复 smoke test验证最终 classroom 只有一个正式版本、无重复 Scene、continuity digest 与实际输出一致。
  • 完成受保护课堂回归、全量 TypeScript、定向 ESLint/Prettier、生成任务回归和一次生产构建后再开放默认 checkpoint 路径。

专项验收不变量

  1. 杀掉生成进程并重新启动后,任务不会永久停留在 running,也不会被静默标记为成功。
  2. 在第 N 个场景完成后中断,继续任务只调用第 N+1 个场景及后续,不重复 N也不重新生成大纲。
  3. 两个 runner 不能同时写同一 attempt旧 runner 的迟到写入必须被拒绝或丢弃。
  4. 大课普通继续不重生成已成功模块;显式级联重生成仍保持现有 N..end 语义。
  5. 没有 checkpoint 的 legacy job 仍可重跑,但接口和 UI 明确说明会从头开始。
  6. 原互动课堂内核文件保持不变,最终课堂仍走原有 classroom → Stage → SceneRenderer 链路。