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

40 KiB
Raw Permalink Blame History

Findings & Decisions

Requirements

  • 麦洛教育目标形态包含用户桌面端、运营端和后台数据服务。
  • 用户端保留原单课件生成 Agent并使用运营端发布的大型连续课程。
  • 大型课程制作仅开放给运营端。
  • 主 Agent 围绕课程目标拆解全局框架,并为每个子模块生成提示词;人工确认后串行调用原单课件生成 Agent。
  • 发布后的每个模块完整保留原互动课堂、HTML 代码互动、原教师/助教、多 Agent 讨论、Quiz、PBL、白板和音频。
  • 不新增或重写课堂交互内核;改造集中在编排、权限、发布、目录和服务端外围。

Research Findings

  • 2026-08-16 用户正式确认:平台账号是课程资源、购买权益和跨设备学习记录的永久主体;设备身份只用于持有证明,未登录游客数据必须支持显式迁移到账号。

  • 当前 generation job store 的 updateClassroomGenerationJob 可用任意 patch 覆盖或清空 ownerPrincipalIdcreate 又始终省略 owner因此先要把 owner 绑定从普通更新中剥离成单独的 compare-and-set/显式迁移操作。

  • 当前 observe helper 故意只返回 void,无法直接复用作 enforcement。最小安全演进应新增独立的 evaluate/enforce helper保留 shadow 路径原响应,同时由显式环境模式决定是否 fail closed不能偷偷把现有 observe 改成返回授权结果。

  • 在真实 session/device-proof provider 未接入前production resolver 必须继续返回 unavailable;可先让创建路由在解析到 authenticated principal 时原子写 owner并在 enforcement 模式下对 unavailable/anonymous/legacy-unowned 返回稳定错误,默认仍保持 shadow 兼容。

  • generation job 的 POST/GET-list 目前没有身份测试list 在非 server 角色会返回全部 job。required enforcement 竖切必须同时处理 create、list、detail、cancel、resume、delete不能只保护单资源路由而留下全局枚举。

  • 大型课程 runner 直接调用 job store 创建运营内部 jobowner 绑定参数必须保持可选,避免把 ops 编排误当 learner 账号资源。公开 API 创建才从已验证 principal 派生 owner/guest binding。

  • required 模式若先从全局 job 文件中截取 limit=20 再按 owner 过滤,会让活跃多用户场景中的本人任务被其他用户的新任务挤出结果;正确顺序必须是“归属过滤 → 时间排序 → 用户 limit”。文件仓库阶段可在 required 模式读取全集后过滤,未来数据库迁移应下推为带 owner 索引的查询。

  • 持久化 JSON 可能被旧版本或人工操作写成 owner+guest 同时存在;授权分类与迁移都必须将这种双归属视为损坏并 fail closed不能因为先检查 owner 就忽略 guest。

  • owner 字段除运行时防御外还应从普通 update patch 的 TypeScript 类型中移除;显式迁移函数是唯一允许写归属字段的代码路径。

  • Phase 7 已落 provider-neutral 授权底座:RequestPrincipal 不绑定具体登录供应商,当前生产 resolver 恒为 unavailable 且不读取任何身份 header纯 owner policy 对 legacy/unknown 给出 indeterminate对 ops/service 身份不提供隐式越权。

  • generation job 的 detail/cancel/delete/resume 已接 observe-only seamobserver 只返回 void 且隔离同步/异步异常,现有状态码与响应体不依赖授权 decisionownerPrincipalId 为可选字段,旧 JSON 仍可读取,新任务目前不会伪造 owner。

  • 独立审查未发现 observe-only 竖切阻断问题;未来接真实身份/遥测时必须增加超时或有界异步投递,并在创建资源时原子绑定不可被普通更新清空的 owner。

  • Phase 7 完成全量出站链路分级proxy-media、Azure Voices、MinerU Cloud 验证已 DNS pin专用 server 已隐藏 provider verify/probe但 image/video/TTS/voice/ASR/PDF/extract 及 LLM BYOK 生成入口仍可能把客户端 baseUrl 交给二次解析的 fetch/SDK。

  • validateUrlForSSRF() 是“先解析校验、后由调用方重新解析连接”,仍有 DNS rebinding TOCTOUALLOW_LOCAL_NETWORKS 又同时放开 operator self-hosted 与不可信客户端 URL必须拆成 server-managed public、operator-private 和 learner-BYOK 三类信任策略。

  • 当前最直接的链式 SSRF 是 provider 返回的二次 URLMinerU Cloud 的 upload URL/ZIP URL、Qwen TTS 的 audio URL会被直接请求这些 URL 即使首个 API endpoint 是公网,也必须再次执行 public allowlist/DNS pin/redirect/大小限制。

  • 自托管 model probe/PDF、企业 HTTP proxy 和 AI SDK 流式调用不能直接替换成 GET-only public helper需要支持任意 method/body/stream 的 secure transport或由可信 egress proxy 执行目的地址策略。

  • fetchPinnedPublicUrl 已扩展为有界 GET/PUT只接受静态可测量 body禁止覆盖 Host/Content-Length/Transfer-Encoding上传前先做 50 MiB 默认上限,仍以单次 DNS 解析固定 Host/SNI 和连接地址。

  • MinerU 预签名 PUT 与结果 ZIP GET、Qwen TTS 返回音频 GET、AliDoc OSS 图片 GET 均已逐资源重新 pin不把首个 API Bearer 转发给二次 URL拒绝重定向并限制请求/响应体。

  • dedicated server 现通过中央 provider access policy 禁止 unmanaged client key/baseUrl所有统一 LLM/TTS/ASR/PDF/image/video/web-search resolver 只接受运营配置。桌面/all、ops、learner BYOK 和运营配置的 Ollama/private self-hosted 保持原行为;无网络 unpdf 是明确例外。

  • 当前已有两阶段大课骨架:框架生成后停在 framework_ready,人工确认后按顺序创建并运行 classroom generation job。

  • CourseModuleSpec 目前只有标题、说明、目标、前置条件和时长,没有主 Agent 输出的模块级 generationPrompt

  • runner 仅把说明、目标、前置条件和泛化连续性提示拼成 requirement没有传递 languageDirective,也不读取前序模块实际产出。

  • 大课模块复用了服务端单课件生成管线,发布后的学习路径最终仍进入原 <Stage>

  • learner 模式在原 Stage 外额外挂载了自包含 AssistantPanel,形成与原 ChatArea 并行的第二套助教。

  • 大课后台生成没有使用前台 Interactive Mode 的专用 outline prompt因此可能生成很少或没有 HTML 互动场景。

  • 首页在材料、互动模式或职教模式开启时只 toast 后 return没有进入仍存在的 /generation-preview

  • /courses 页面和 /api/courses/** 缺少运营角色服务端鉴权;发布 Token 通过 NEXT_PUBLIC_* 暴露到浏览器。

  • 大型课程 manifest 只保存 coursewareId,没有固定模块版本和 contentHash

  • 当前目录没有 Git 仓库;已有定向测试曾达到 14 个文件、83 个测试通过i18n 检查仍有多语言缺失键。

  • middleware.ts 只在配置 ACCESS_CODE 时校验 HMAC cookie未配置时全部请求放行且页面请求即使未认证也继续进入前端。

  • 首页 startBackgroundGeneration 在存在课程材料、互动模式或职教模式时于 app/page.tsx:169 仅 toast 后返回;否则 POST /api/generate-classroom

  • /generation-preview 通过 sessionStorage['generationSession'] 恢复输入,说明首页恢复前台流程必须先写入兼容的 generation session再导航不能只做裸 router.push

  • app/classroom/[id]/page.tsx 先渲染原 <Stage>,随后仅在 learner query 下额外挂载 AssistantPanel;删除该 import/条件挂载即可恢复原课堂助教唯一性,不需触碰 Stage。

  • 首页大型课程入口目前无条件渲染;lib/config/feature-flags.ts 还通过 NEXT_PUBLIC_COURSEWARE_PUBLISH_TOKEN 决定浏览器侧发布能力,课程构建页直接读取并发送该公开变量。

  • 前台生成会话的最小必需结构由 app/generation-preview/types.ts 定义:sessionIdrequirementspdfTextcurrentStep,并可带 documentSources、图片、outline 与 preview phase。

  • 无材料时可直接写入 requirements.requirement/language/webSearch/interactiveMode/vocationalTestMode 等状态;有材料时必须先用 storeDocumentBlob 持久化每个 Blob再写入含 storageKey 的有序 documentSources

  • 现有 home-to-generationfull-happy-path E2E 仍断言普通首页提交进入 /generation-preview,但当前产品决定应调整为:普通无材料模式走后台任务,材料/互动/职教模式走前台预览。

  • isCoursewarePublishEnabled()getCoursewarePublishToken() 当前均依赖 NEXT_PUBLIC_COURSEWARE_PUBLISH_TOKEN;课程构建页把它作为 Bearer header 发回同源 API不能作为运营安全边界。

  • UserRequirements 已原生支持 webSearchinteractiveModetaskEngineMode;职教开关应映射到 taskEngineMode,而不是另造 session 字段。

  • generation-preview 的 active steps 会在 documentSources 存在且 pdfText 为空时自动执行材料解析;storeDocumentBlob(File) 返回 IndexedDB storage key正好用于首页会话构造。

  • 语言由 useI18n() 提供 localeUserRequirements 类型本身没有 language 字段,当前生成语言主要由服务端 outline 结果的 languageDirective 决定,因此首页无需为了恢复链路新增未使用的 language 字段。

  • 部署边界现采用双层校验:服务实例角色决定是否具备 manage_coursesops 生产环境还必须由现有 HMAC ACCESS_CODE cookie 认证;生产未配置角色默认 learner。

  • COURSEWARE_PUBLISH_TOKEN 已收回服务端:同源 ops 发布由角色+ACCESS_CODE 会话授权,独立 server 部署仍用 Bearer Token 接收远端 ops 发布。

  • readCourseRecord 目前直接把 JSON 断言为 CourseRecord,没有 schema migration新增框架字段必须在读取时规范化或让所有消费者都安全 fallback否则旧文件会在 runner 访问新字段时失效。

  • validateCourseFramework 目前只校验标题、语言、摘要和 415 个模块;模块索引会重排,时长会夹在 560 分钟,但主 Agent 输出没有课程目标、连续性契约、模块生成提示词或知识边界。

  • runFrameworkPhase 手工复制模块记录,只保存标题与说明;应复用 emptyModuleRecord 并保存生成提示词快照,确保之后框架再生成或模型输出变化时有可审计输入。

  • buildModuleRequirement 当前硬编码 1525 分钟,忽略 estimatedMinutes,也没有拼入 languageDirective;这是 Phase 2 可以在大课编排层修复的明确数据丢失点。

  • Phase 2 已把新模型输出和旧存量分成两条边界:新输出严格要求课程目标、连续性契约、模块提示词和知识边界;旧 JSON 只在 read/list 时确定性规范化,读取不会主动改写磁盘。

  • 运营确认页已改用共享 CourseDetail 类型,并在确认前展示目标学习者、语言、整课目标、四项连续性契约及每模块知识边界/生成提示词。

  • course runner 原先只按 run key 去重,不同 key 的整课生成与单模块重试可以并发;现在 runner 以 courseId 做互斥,所有 start/resume/regenerate 入口也会在调度前检查并立即登记运行。

  • 后台 GenerateClassroomInput 目前没有 interactiveMode,构造的 UserRequirements 也只有 requirement因此大课 runner 即使复用单课件管线,也只能进入 package 的普通 requirements-to-outlines prompt。

  • 原前台 Interactive Mode 是 app 层 INTERACTIVE_OUTLINES 模板(目标 70% widgetspackage 的 generateSceneOutlinesFromRequirements 负责统一解析/补 id。最小复用方式是在服务端仅替换传给该函数的 outline aiCall prompt继续复用其解析与后续 content/action/persist 管线。

  • classroom job 成功记录只保存 classroomId 和 scenesCountrunner 可从服务端持久化 classroom JSON 读取四类 Scene 实际内容,生成有界、确定性的 output digest再把此前成功模块的 digest 注入下一模块 requirement无需改课堂运行时。

  • Phase 3 已在模块成功后从持久化 classroom 提取四类 Scene 的实际语义证据,保存 semanticHash;后续模块记录 continuityInputRefs 并只在所有前序模块有成功实际产出时继续。

  • 全课程流水线现在遇到模块失败即暂停;失败模块修复后,同一个课程 run 会按顺序继续剩余 pending 模块,避免在知识断层上生成后续课件。

  • 后台 Interactive Mode 复用原 app prompt 模板,只替换 outline AI call仍走 package 的解析、content、actions 和原子持久化;持久化前强制至少一个 interactive Scene 具有非空 HTML。

  • output digest 会过滤 script/style、data URI 和 learner runtime并保留所有前序模块的哈希索引详细 evidence 受 12k 上限约束且优先近期模块。

  • Phase 4 漂移根因是 course manifest 只保存 coursewareIdlearner 又查询 latest正确不可变身份必须是 coursewareId + coursewareVersion + contentHash,传输 URL 只能由该身份派生。

  • 文件型 courseware 与 manifest 原先都允许同版本覆盖或覆盖 latest现已改成历史记录/append-only并在单进程发布锁中完成版本分配和保存。多实例仍需要数据库唯一键或跨进程事务作为后续部署约束。

  • 服务端生成的课堂素材位于 data/classrooms/<id>/{media,audio}Scene 中却保留 /api/classroom-media/... URL冻结发布必须先把这些 URL 解析成字节和稳定 bundle ref不能让 learner 依赖运营端源文件。

  • 原序列化同时保存 audioRefaudioUrl,而播放器优先 URL冻结包必须在已有 audioRef 时删除远端 URL才能真正离线且不漂移。

  • 大课发布要求每条 speech 有持久化音频,因此大型课程生成统一启用 TTS缺少 provider/字节时允许运营预览,但发布门禁失败并指出缺失资源。

  • Agent persona/voice 与 Stage 初始 whiteboard 原先未完整冻结;它们属于发布数据契约而非课堂运行时逻辑,已在外围序列化/导入和 server publisher 中补齐。

  • “冻结学习包”只禁止 outline/scene/media 的课程生成调用;原 ChatArea/Roundtable、动态评分和 PBL 教学运行时仍可正常调用模型。

  • 受保护核心广泛回归的 3 个失败属于既有 Chat director 契约内部漂移,隔离重跑仍稳定复现:当前 director prompt 明确写“课堂只有 teacher + user”但旧测试仍期待 assistant/student fallbackweb evidence 的源码具备一次性 take/attachment 路径,但这两条 route fixture 没有把 URL带入 child turn。Phase 4 未修改 lib/chat/**/api/chat/pi,因此按用户边界记录而不在本改造中修写 Chat 内核。

  • 现有 Playwright 已覆盖 learner 部署拒绝 ops、首页普通后台任务和互动/材料前台单课件路径但没有大型课程的真实发布→manifest→精确模块 URLPhase 5 应优先用无 LLM 的 Vitest/route 集成夹具补这条数据边界,再用少量浏览器 smoke 验证 UI 导航。

  • Playwright 原先在 development 启动服务且未声明部署角色development 按设计 fallback 为 all,导致 learner 边界用例与其环境互相矛盾。E2E 默认服务现显式固定为 learner未来 ops 浏览器测试应使用独立 project/server 配置。

  • learner 角色下 / 不保证提前打开 MAIC-Databaseclassroom-interaction.spec.ts 的旧 seed helper 直接 indexedDB.open() 后在缺表时同步抛错却没有 rejectPromise 因而悬挂到超时。该失败属于 E2E fixture 假设(注释仍写 DB v8而当前已 v17可在测试层显式初始化 Dexie/捕获事务创建错误,不需改课堂运行时。

  • 课堂文档的当前权威存储是 BrowserDocumentStore 使用的 maic-documents v1stages/scenes/outlinesQuiz、Playback 和 iframe E2E 已按该契约显式 seed。classroom-interaction.spec.ts 仍写入旧 MAIC-DatabasestageOutlines,应仅在测试夹具中迁移到前者。

  • Phase 5 已有无 LLM 的真实文件仓储集成证据:两模块冻结发布生成 schema v2 精确 pin manifest中途冻结失败会回滚已暂存模块且 learner 端不可见manifest v1 在模块独立发布 v2 后仍精确指向 v1。

  • 三端最终边界审计表明当前闭环仅在“同进程、单副本、共享本地文件库”条件下成立。Docker 未传构建期公开角色server 尚非无头 allowlistops → server 没有跨进程 manifest 事务接口;文件库+内存锁不支持多实例/Vercel。

  • learner 可见 manifest 目前暴露与源 classroom 相同的 coursewareId,而 raw /api/classroom 没有资源权限;多用户共享 server 下 job 列表/读取/取消/删除也没有 owner。这两项属于正式上线前必须建立的资源级边界。

  • 已定位的可独立修复外围漂移还包括learner /tasks/api/courses 403 与正常 job 绑在同一 Promise.all;首页无 /learn 目录入口;/publish 未纳入 ops gating上游成功模块重生成不会级联失效下游浏览器重发的 ZIP 内部 version 可能仍为 1。

  • 合并 Playwright 单 worker 回归已在干净 learner dev server 上 11/11 通过:先前的 iframe 场景标题和 generation redirect 失败不可隔离复现,与并行干扰/残留 server 一致,不应对原 Stage 代码做“修复”。

  • 源 classroom 与公开 module id 可以在外围完整解耦:新发布使用 course_{courseId}_module_{index} 稳定公开 idregistry 私有保存 sourceClassroomId,公开 summary 不返回learner/server 可用该私有关联拒绝 raw classroom 旁路,同时兼容拦截旧的 shared-id 记录。

  • 源 classroom 反查是授权路径,不能复用“公开列表跳过损坏 JSON”的宽容语义文件 repo 因此增加了严格 hasPublishedSource 反查,任一 registry 文件损坏时 raw source 请求 fail closed。

  • 浏览器 packager 无法无竞态地预知 next version可行契约是 server 在 per-courseware lock 内分配版本,仅改写被 content hash 明确排除的 bundle.json.meta.version,再保存 ZIP。learner 必须同时校验 registry、包内 version 和 hash。

  • 上游模块重生成不能仅修目标模块;当前做法是一次原子清空 N..end 的所有尝试/输出字段并将课程置为 generating随后顺序重建失败会保留后续 pending既有 resume 可续跑。

  • NEXT_PUBLIC_OPENMAIC_DEPLOYMENT_ROLE 必须在 pnpm build 之前注入;只在容器运行时设置会造成权限与 UI 不一致。Docker 现在对 learner/ops/server 分别构建,并在运行层固定相同角色。

  • learner 冻结课堂的“不再生成”应是路由外层明确策略,而不只依赖 bundle 恰好没有 outlines现在 learner mode 即使遇到异常缓存也不调 Scene/媒体生成,作者课堂恢复逻辑不变。

  • 跨进程发布的可复用边界已定位ops 可用 packagePersistedClassroom(..., version: 1) 生成完整 ZIP 模板server 的 publishCourseware 会在每个 courseware 锁内分配真实版本、重写 bundle.json version 并重验 hash/完整性,因此运输层不应复制这些门禁。

  • 当前大课发布 route 的事务边界全在 ops 进程:本地写 unpublished courseware、二次验证源快照、切 published 再写 manifest。独立部署必须把“metadata + 全部 ZIP”作为一次 server-only 批量请求,并在 server 一个 course 互斥边界内复用 publishCoursewarepublishCourse 和 status rollback。

  • 现有 /api/coursewares 只能单 ZIP multipart 上传,已有 COURSEWARE_MAX_UPLOAD_BYTES 单模块上限和 Bearer 校验;大课远程 transport 需在此之上再增加整批 Content-Length/实际 File size 上限,不能仅信任客户头。

  • 文件 repo 可以把本批版本状态补偿为 unpublishedbundle byte store 也支持删除单个版本;当前 manifest repo 是全局实例,为做真实隔离仓库测试,publishCourse 需增加可选 CourseManifestRepo 注入,生产默认仍用原全局 repo。

  • runCoursePublishExclusive 已提供每 course 单进程互斥server 批量接口可直接复用。它不是跨副本锁,因此文档仍必须保留“多实例上线前迁到数据库事务”限制。

  • 远程发布的安全重试可不新增独立 idempotency 表server 在 staging 前解析/重算全部 incoming ZIP hash再将 latest schema-v2 manifest 的课程 metadata 与 module index/id/title/description/hash 精确比较;完全一致时直接返回旧 manifest超时重发不会产生空转版本。

  • ops 详情当前只查本地 courseware repo 并比较私有 sourceClassroomId;独立 server 下必然误显示“未发布”。最小闭环是在 ops CourseRecord 保存一份精确 publication receiptmanifest version + 每模块 source classroom/digest + id/version/hash详情仅在 receipt 与当前源身份一致时显示已发布;从任意模块开始重生时清除整课 receipt。

  • ACCESS_CODE 原先的 HMAC 会话没有过期语义、登录限流或 Cookie 写请求的 Origin 检查;现在 Node/Edge 共用同一时效契约(默认 7 天),登录使用有界单进程限流,只在显式信任代理头时按客户 IP 分桶。

  • server 角色不能只是“隐藏运营入口”;它必须是无头服务。当前 middleware 已将全部非 API 页面以直接 404 隐藏,并将 courses/ops/access-code 信任域从 server API 面移除;其余课堂/生成 API 仍等待 owner 契约,不应被误宣称为已完成多用户安全化。

  • 项目已有 capBodyStream,可在 formData() 之前包裹请求 stream因此整批上传能同时防 Content-Length 谎报和 chunked 无上限缓冲;解析后仍逐模块检查 File.size。

  • internal server 接口不应用 request Host 生成 registry bundleUrl,否则会把 ops 访问的内网 hostname 固化给 learner生产必须用严格 origin 形式的 COURSEWARE_PUBLIC_BASE_URL

  • publication receipt 存在时必须 fail closed若当前 classroom/digest 与 receipt 不符,不能再 fallback 到本地 latest sourceClassroomId;只有完全没有 receipt 的 legacy/shared-FS 记录可用旧查询兼容。

  • 除模块级 invalidateCourseModulesFrom 外,整个框架重生路径会直接将 framework/modules 置空;该原子更新也必须同时清除 publication receipt避免新框架尚未产生时运营端仍持有旧发布身份。

  • 新冻结包使用 contentHashVersion: 2:哈希规范化时忽略仅表示导出时刻的 manifest.exportedAt,因此相同课堂字节在网络超时后重新打包仍能命中 server 幂等判断;无版本字段的历史包继续按原始 v1 规则验证,避免破坏已发布内容。

  • ops → server 已使用一次 multipart 传输 metadata+全部 ZIPremote/local 共用同一 commit 内核,同内容重复发布均返回原 manifest响应丢失不会制造空转版本。

  • 大课模块的 registry 记录私有保留 courseId/moduleIndex/sourceClassroomId;公开 catalog/detail/download 只在 schema-v2 manifest 包含精确 id/version/hash pin 后可见。

  • 发布回执新增全量 canonical classroom sourceRevisionHash,不再仅靠会丢弃 script/style/media/action 细节的语义摘要判断当前性HTML 脚本单独变化也会使发布状态失效。

  • classroom/courseware 文件库已在路由、facade、record store 和 byte store 多层校验 id/version 与 resolved path同版本幂等命中前会读回实际 ZIP核对完整性、内部身份、私有归属和精确字节缺失/损坏不再返回假成功。

  • OPS_PUBLIC_ORIGIN 是 cookie-authenticated 运营写请求的 canonical OriginX-Forwarded-Host 即使用于可信 IP 限流,也不再能扩充 CSRF 允许域。

  • server 端其余课堂/单课件 API 仍需要 learner identity/entitlement 契约;不能为了隐藏高成本 API 而直接禁用原 Chat、Quiz、PBL、TTS 或用户端单课件生成。

  • /api/proxy-media 不是原课堂教学运行时必需合约,且存在 DNS-rebinding 窗口server 角色现已对其直接 404同时 SSRF IP 分类补齐 RFC6598 100.64.0.0/10(含阿里云 100.100.100.200)。其他用户控制 URL 的服务端 fetch 仍应在正式身份契约阶段统一切换到 pinned-resolution safe fetch。

  • server 无头边界已额外隐藏 provider 探测/验证、MP4 export、开发态 persistence、usage 和无 owner 的全局 job 列表;单课件 POST 与原课堂运行时端点保留,等待 learner capability。

  • generation job DELETE 原先可绕过 raw classroom 读取保护删掉已发布源;现在会先做 fail-closed published-source 反查,命中时 job 与 classroom 都保留。

  • QA/TTS 进程内限流默认不信任 XFF/X-Real-IP仅在 LEARNER_RATE_LIMIT_TRUST_PROXY_HEADERS=true 且入口覆写头时启用;活跃 key 采用硬上限+共享 overflow bucket不再无界增长或每次全表 reap。

  • manifest metadata/hash 相同不足以证明一次重试仍可用;幂等返回前必须读取 exact bundle bytes并复用完整单课件 publish gate 验证内部 id/version/hash、重算 hash、complete 与 portable 资源。对象丢失或只篡改未参与 hash 的 bundle.json 都必须拒绝旧幂等命中并生成新版本。

  • 文件补偿事务不能提供数据库隔离,但大课 record 的私有 courseId/moduleIndex 可以建立最小可见性门禁catalog、detail、download 只有在任一已提交 schema-v2 manifest 精确 pin 该 id/version/hash 时才公开,因此逐模块 promotion 到 manifest commit 之间不会泄漏给 learner。

  • 同源共享文件 fallback 已复用与远程 server 相同的批量 transaction helper远近端现在对相同 metadata + 内容都幂等;显式重复点击也是 no-op当前没有 forceNewVersion 契约。

  • Phase 6 已完成可在单实例文件仓库边界内可靠验证的三端隔离、跨进程发布、冻结内容身份、源 revision 和高风险 server deny数据库事务、对象存储、共享锁/限流及 learner owner 不是同一层面的“收尾”,已独立迁入生产化阶段。

  • 专用 role=server 已通过 404 隔离 /api/proxy-media 并补齐 100.64.0.0/10,但 learner/all 公网部署仍需要连接级 DNS pin仅在校验阶段解析域名不足以抵御 fetch 时的二次解析和重定向变址。

  • 身份机制不能替代 SSRF 防护safe-fetch 可在不决定账号模型的前提下先落地。job/classroom owner、课堂运行时调用额度与单课件生成权益则会改变公开/登录产品体验,必须在身份方案确定后实施。

  • 项目已直接依赖 undici,可通过自定义 dispatcher/连接 lookup 把请求连接绑定到预先验证的 IP同时保留 URL hostname 供 Host 与 TLS SNI 使用;现有 /api/proxy-media 的逐跳 URL 校验仍使用全局 fetch尚未绑定到验证结果。

  • 当前依赖中没有 NextAuth/Better Auth/Lucia/Clerk/JWT/Jose 等正式账号或 Session 库,只有 pg 和面向运行时/文档/资产的存储包;现有 PERSISTENCE_DEV_TOKEN + x-learner-key 已在源码中明确标注为开发态、客户端可伪造,不能升级命名后当作 learner 身份。

  • 现成 Postgres document/runtime/asset stores 可以承载学习状态,但没有 users/devices/sessions/entitlements/resource ownership schema正式身份必须新增独立 auth 数据模型,并由服务端 claim 派生 learner principal不能继续信任浏览器提交的 learnerKey。

  • learner 身份的推荐终态不是账号 Session 与设备 capability 二选一:账号内部 UUID 是权益、跨设备和找回主体;桌面端用系统浏览器 OAuth 2.1 Authorization Code + PKCE并用设备密钥/DPoP 证明令牌持有;打开课堂时再签发 515 分钟、绑定 coursewareId + version + contentHash 的 runtime grant。

  • RequestPrincipal 应统一承载 user/device/ops/service 四类主体owner、learnerKey 和配额主体都只能从服务端 claim 派生。job/classroom/render/voice/draft 必须服务端写 owner跨 owner 查询统一 404随机 ID 不能代替授权。

  • 课堂 runtime grant 只约束外围 APIChat/QA/Quiz/PBL/TTS 先校验 exact classroom/courseware 资源、action scope、entitlement 与 quota再进入现有 handler不需要修改 Stage、ChatArea/Roundtable、Quiz/PBL 或播放内核。

  • 当前 /api/qa 按 coursewareId 读取 latest未来必须改为 grant 固定的 version/hash现有大课 manifest exact pin 不能在动态答疑层被 latest 查询绕过。

  • 身份实施前仍需产品确认:正式发布是否强制登录/保留邀请码游客;桌面技术形态与登录渠道;是否首版多租户;目录/manifest/bundle 是否公开或需 entitlement离线下载不可撤回的接受度BYOK 是否开放及是否经过平台;学习进度是否需要跨设备/教师/证书级可信。

  • 无需等待产品选择的安全不变量是:内部 UUID 主体、服务端派生 owner、短期 exact-resource grant、动态高成本 API fail closed、事务配额预留、旧 legacy_unowned 数据不自动认领、原课堂内核保持不变。

  • server 精确路径策略不能只看 GETNext.js 可从 GET 派生 HEAD而尾斜杠会绕过 未规范化的精确比较。当前全局 job 列表已同时隐藏 GET/HEAD内部发布在路径 规范化后仍只允许 POST。Azure voices 列表属于 provider 探测,也不再暴露于专用 server。

  • remote ops 不挂载 server courseware registry因此已发布 source 保护必须同时查当前 CourseRecord.publication 回执。回执与 registry 反查都是授权路径,损坏时 fail closed。

  • 发布最终复核与 receipt 落盘之间存在 source DELETE TOCTOU当前用同进程多 source 原子 claim 与单 source delete claim 互斥,冲突返回 409。这与其他内存锁一样不跨副本。

  • 当前 publication receipt 会在重生时清除;旧已发布 source 是否必须永久保留没有 产品契约,本轮不新增历史索引。跨进程提交后本地 receipt 写失败/响应丢失的窗口也只能 由持久 idempotency/outbox 与数据库事务最终消除。

  • courseware record/bundle 之外course manifest 文件仓库也必须在自身边界校验 id 和 safe positive version并只解析配置根目录的直接子文件不能只依赖当前 HTTP 路由的上游校验。

  • 新的 public URL fetch 边界采用“每跳单次 dns.lookup(all) → 任一非 global-unicast 即整跳拒绝 → Undici Agent lookup 只返回已验证集合 → 手动重定向重新建 Agent”的连接级契约URL hostname 保持不变,因此 Host/TLS SNI 不需要改写为 IP。

  • public media policy 明确忽略全局 ALLOW_LOCAL_NETWORKS,拒绝凭据 URL、IPv4 mapped IPv6、CGNAT/metadata、文档/benchmark/multicast/reserved 与 IPv6 transition/documentation 范围;内部自托管 provider 的例外不能外溢成匿名公网代理。

  • /api/proxy-media 已改为 8 KiB 声明+实际 JSON 流上限、最多 5 次逐跳 pinned redirect、25 MiB transport/读取上限和泛化错误。连接 dispatcher 只在响应体读取/取消后关闭,避免释放过早又不遗留 socket。

  • 远端发布的 ops 进程没有 server 端 courseware registry因此“是否为已发布 source”的删除保护不能只查本地 registry现以 current CourseRecord.publication.modules[].sourceClassroomId 作为现有远端回执 fallback并对损坏回执 fail closed。框架/模块重生成清掉 current receipt 后是否永久保留旧 source仍是历史审计产品语义不在本批擅自增加永久索引。

  • 发布从课堂快照、冻结、远端提交到回执落盘期间,与 job DELETE 共享按 source classroomId 的同步 claim这样删除无法在最后 revalidate 与 receipt 写入之间穿过。该 gate 仍是单 Node 进程语义,多实例要由数据库 CAS/锁取代。

  • 专用 server 对 ownerless 全局 job list 的隐藏必须同时覆盖 GET 与 Next 自动派生的 HEAD内部 publish 也必须在带尾斜杠时维持“仅 POST”。无客户端调用的 /api/azure-voices 属 provider 探测,已从专用 server 面隐藏。

  • course manifest 文件仓储现在也在自身边界校验 courseId、safe-positive version 与 direct-child 路径,不再只依赖上游 route/transaction这与 courseware record/ZIP 的多层路径防护一致。

  • outbound 初盘显示剩余风险不是一个入口provider probe、Azure voices、PDF/ASR/TTS、image/video adapter、文档解析、模型 SDK 和素材下载都有 URL/baseUrl fetch。必须区分客户端 URL 与运营配置的 managed URL后者可能是私网自托管服务不能被 public-only policy 静默破坏。

  • Azure voices 与 MinerU Cloud 这类明确公网、单次 GET 且拒绝重定向的路径可直接复用 pinned helperimage/video/voice/AI SDK 涉及 POST、轮询、provider fallback 或 SDK 自己的 fetch 注入,应按 adapter 设计逐批迁移,不能用一次全局替换冒险。

  • /api/provider/probe-models 原本也支持 ALLOW_LOCAL_NETWORKS 下的本地自托管模型。正式方案要么在身份保护后用 operator 精确 host allowlist + pinned connection要么保持专用 server deny不应为了 SSRF 修复把桌面自托管能力无提示删除,也不能继续用“允许任意私网”的全局开关。

  • 身份供应商未定不妨碍先建立纯策略,但必须显式区分 anonymousresolver_unavailable:当前没有 learner authenticator后者不能伪装成游客。ownership 决策只接受服务端 owner/principal/capability不从 IP、ACCESS_CODE、anon:*x-learner-key 或 body 推导。

  • observe-only authz seam 只有在接入至少一个真实资源时才有价值;首个选择 generation job可让旧记录的 owner missing 与未来 owner match/mismatch 被结构化观察,同时保持所有现有 status/body 不变。observer 不能返回 allow抛错也不得影响请求。

  • 永久权益主体仍需用户确认。推荐以正式账号内部 UUID 为永久 owner设备 capability 仅作为账号绑定持有证明或可迁移游客身份;具体手机号/邮箱/微信/SSO 供应商可以以后再选。

  • public pinned helper 已最小支持调用方提供认证头,同时拒绝覆盖 Host保持原 URL hostname 驱动 Host/SNI。Azure voices 与 MinerU Cloud probe 已迁移;它们拒绝重定向,因此订阅 key/Bearer 不会跨 host 转发。

  • 为保留原自托管能力,本批没有把 model probe、PDF self-hosted、AliDocMind SDK 强制切成 public-onlyAI SDK 与 image/video adapter 也未动。这些调用点仍需“public pinned / operator exact private allowlist / managed internal”三类明确策略后再迁移。

  • 账号 owner 传递到课堂的最小边界已确认:generateClassroom 的服务端 options 可承载 ownerjob runner 从已落盘 job 读取 owner 后传入;不应把 owner 放进客户端可控的 GenerateClassroomInput

  • 当前 PersistedClassroomData 没有归属字段,/api/classroom 的 POST/GET 也没有资源级授权;发布可见性门仅保护“已发布原始课堂”,不能代替 owner 校验。

  • 课堂归属必须保存在 Stage/Scene 之外的持久化包装中,避免改动原互动课堂 DSL 与播放内核。

  • 课堂重复 POST 是一个隐含的 owner 篡改面:对已存在课堂必须先按旧 owner 授权,并保留旧归属;只有首次持久化才从当前服务端 principal 创建 owner/guest 绑定。

  • 生成课堂必须以已落盘 job 的 owner 为权威来源,不再从长任务结束时的请求上下文推导;当前 runner 已按此传递。

2026-08-16 生成任务恢复故障发现

  • Python 普通课件 job Xjk5UcLtOx 在本次检查时仍可推进到 scene 5/10、已完成 4 个场景;说明当前后端没有持续挂死,用户点击“继续”确实启动了新的生成尝试。
  • 初一英语大型课程 S9V3Mm0AWy 的模块 1 job 在本地时间 12:09 左右开始,最后一次模型使用记录在 12:17:44当前 Next 进程启动于 12:19:07之后旧 runner 不再更新该 job12:43 左右读时被判为 Stale job: process may have restarted during generation。这能证明“进程重启/runner 失联”是本次大课失败的直接机制,但不能单凭现有日志区分崩溃和人工重启。
  • readClassroomGenerationJob 的 stale 判断只比较 updatedAt,并返回内存中的 failed 视图;它不知道当前进程是否仍持有活跃 runner也没有 heartbeat/lease因此长模型调用超过 30 分钟时可能误报,进程重启时又只能在下次读取才暴露。
  • runClassroomGenerationJobrunningJobsrunningControllers 和大课 runningRuns 都是进程内 Map磁盘只保存状态和原始输入不保存 runner 接管 token 或可恢复执行上下文。
  • generateClassroom 的 Stage、outlines 和 generatedScenes 都在内存中;正式 persistClassroom 只在所有场景、媒体/TTS 阶段结束后调用,取消或进程终止不会留下正式 classroom也没有场景级 checkpoint。
  • 普通 job 的 resume 路由与 runner 注释明确规定“从 persisted input 重新开始”;大型课程 resume 只跳过 succeeded 模块。故障发生在模块 1 时,后续模块尚未成功,用户看到的就是整门大课重跑,而不是框架被重新生成。
  • 课程 reconciliation 把 stale/failed classroom job 变成 module.status = failed 并清空 jobId;随后普通 resume 创建新 job旧 job 的任何中间产出没有引用入口。这是大课无法复用模块内 checkpoint 的第二个结构性缺口。
  • 恢复协议必须把“逻辑 job、attempt/lease、checkpoint”和“正式课堂可见性”分开checkpoint 用于恢复,正式 classroom 仍保持完成后原子发布;旧任务没有 checkpoint 时才退化为从头重跑。

Technical Decisions

Decision Rationale
第一批先做权限、入口和 learner 外围收敛 都是 P0 边界问题,可在不碰课堂内核的情况下独立验证
权限同时区分部署能力与请求用户角色 仅隐藏 UI 或仅靠环境变量不足以保护运营 API
平行 AssistantPanel 从课堂外层取消挂载 恢复原 ChatArea/Roundtable 的唯一性,无需修改课堂内核
大课连续性字段采用兼容性 fallback 已有文件型课程记录不能因新增字段失效
新框架字段在边界处规范化runner 只消费规范化结果 避免把旧数据兼容判断散落到生成管线和 UI
generationPrompt 由主 Agent 生成并在模块记录中保留快照 满足主 Agent 拆解职责,并让每次子课件生成输入可追溯
同一课程同一时间只允许一个生成 run 保证模块顺序和快照语义,避免恢复、整课流水线与单模块重试互相覆盖
新大课 manifest 只接受精确 version/hash pins 防止模块单独重发后旧大课静默切到 latest
legacy 无 pin manifest 要求运营重新发布 历史记录没有保存当时版本,不能把当前 latest 伪装为原始事实
服务端课堂先冻结成自包含 bundle再提交课程 manifest learner 不依赖运营端媒体 URL同时仍通过原 import → Stage 链路上课
大课冻结发布强制 TTS、Agent roster 与非空互动 HTML 发布后保证原有语音、教师人格和 HTML 互动能力可重放

Protected Runtime Evidence

  • components/stage.tsx 装配 PlaybackChromeRootInteractiveIframeHost
  • components/stage/scene-renderer.tsx 是 slide/quiz/interactive/pbl 的唯一分派器。
  • components/edit/PlaybackChromeRoot.tsx 装配 ActionEngine、PlaybackEngine、Canvas、Roundtable 和 ChatArea。
  • InteractiveIframeHost.tsx、iframe pool 和 widget messaging 维持 HTML iframe 保活及安全契约。
  • lib/bundle/serialize.ts 与 import builder 已保留 scene content、actions、scene whiteboards 和 multiAgent 字段。

Issues Encountered

Issue Resolution
DeepSeek Harness 首次任务未进入工具清单 重启桌面应用后已成功加载并完成只读自检
无 Git 历史,现有定制改动均为未版本化状态 使用计划文件记录范围,限制并行任务触及不同文件,并逐批验证
用户要求停止使用 DeepSeek 算力 后续不再调用 Harness主线程完成实现与本地验证

Resources

  • 产品与部署文档:../docs/
  • 大课编排:lib/course-framework/
  • 服务端单课件生成:lib/server/classroom-generation.ts
  • 课程运行时入口:app/classroom/[id]/page.tsx
  • 原课堂:components/stage.tsxcomponents/edit/PlaybackChromeRoot.tsx
  • 发布与学习包:lib/bundle/lib/course-manifest-repo/