diff --git a/harness-progress.txt b/harness-progress.txt index dec3dd83..33b499d5 100644 --- a/harness-progress.txt +++ b/harness-progress.txt @@ -1,458 +1,28 @@ -[2026-04-14T05:21:15Z] [SESSION-0] INIT Harness initialized for project /mnt/Data1T/mnote -[[2026-04-14T05:26:16Z] [SESSION-0] INIT Added [task-001] via harness add -[2026-04-14T05:27:30Z] [SESSION-0] WARN Previous log line has malformed prefix from add command; corrected entry appended -[2026-04-14T05:27:30Z] [SESSION-0] INIT Added [task-001] via harness add -[2026-04-14T05:34:11Z] [SESSION-1] LOCK acquired (pid=2142754) -[2026-04-14T05:34:11Z] [SESSION-1] INIT Environment health check: PASS -[2026-04-14T05:34:11Z] [SESSION-1] Starting [task-001] 请参考/mnt/Data1T/mnote/design/rust-kernel-backport-plan.md完成单仓收口 Rust 内核回迁计划,这是详细checklist/mnt/Data1T/mnote/design/rust-kernel-backport-phase-checklist.md,里面已经完成了部分内容,你需要继续完成 (base=fc26ed21) -[2026-04-14T05:40:04Z] [SESSION-1] CHECKPOINT [task-001] step=1/4 "完成 Rust 协议层对 documents.meta/content/save 的建模与映射,cargo test/check 已通过" -[2026-04-14T05:40:04Z] [SESSION-1] ERROR [task-001] [CONFIG] Missing validation.command for umbrella task; execution decomposed into task-002..task-004 -[2026-04-14T05:40:04Z] [SESSION-1] Starting [task-002] 补齐 Rust core-protocol/storage-convex-bridge 对 documents.meta/content/save 的协议与映射 (base=fc26ed21) -[2026-04-14T05:40:04Z] [SESSION-1] CHECKPOINT [task-002] step=1/1 "已补齐 documents.meta.get / documents.content.get / documents.save 的 Rust 协议对象、Convex 映射与单测" -[2026-04-14T05:40:04Z] [SESSION-1] Completed [task-002] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T05:40:04Z] [SESSION-1] STATS tasks_total=4 completed=1 failed=1 pending=2 blocked=0 attempts_total=2 checkpoints=2 -[2026-04-14T05:40:04Z] [SESSION-1] LOCK released -[2026-04-14T05:42:39Z] [SESSION-1] WARN Structured state synchronized after session-1 shell export bug; future Rust validation switched to CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness -[2026-04-14T05:42:53Z] [SESSION-2] LOCK acquired (pid=2188805) -[2026-04-14T05:42:53Z] [SESSION-2] INIT Environment health check: PASS -[2026-04-14T05:42:53Z] [SESSION-2] Starting [task-003] 为 blocks.patch 与正文保存补最小 Rust command 协议和 storage-convex-bridge 映射 (base=fc26ed21) -[2026-04-14T05:43:41Z] [SESSION-2] CHECKPOINT [task-003] step=1/2 "已把 blocks.patch 映射纳入 storage-convex-bridge,目标仍对齐当前 documents:updateContent 写链" -[2026-04-14T05:43:54Z] [SESSION-2] CHECKPOINT [task-003] step=2/2 "storage-convex-bridge 单测与 workspace cargo check 已通过" -[2026-04-14T05:43:54Z] [SESSION-2] Completed [task-003] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T05:43:54Z] [SESSION-2] STATS tasks_total=4 completed=2 failed=1 pending=1 blocked=0 attempts_total=3 checkpoints=4 -[2026-04-14T05:43:54Z] [SESSION-2] LOCK released -[2026-04-14T06:09:58Z] [SESSION-3] LOCK acquired (pid=manual-sync) -[2026-04-14T06:09:58Z] [SESSION-3] INIT Environment health check: PASS -[2026-04-14T06:09:58Z] [SESSION-3] INFO Previous early stop root cause confirmed: umbrella task lacked validation.command and prior run ended after task-003 without continuing to next eligible task -[2026-04-14T06:10:01Z] [SESSION-3] Starting [task-004] 为 sidebar.dataset.list 固定 Rust query 契约并对齐 SidebarInitialData 聚合边界 (base=fc26ed21) -[2026-04-14T06:10:45Z] [SESSION-3] CHECKPOINT [task-004] step=1/2 "已为 sidebar.dataset.list 冻结共享 query payload/result 契约,并让 route、server helper、实时 hook 复用同一映射边界" -[2026-04-14T06:11:23Z] [SESSION-3] CHECKPOINT [task-004] step=2/2 "pnpm test src/lib/sidebar-data.test.ts 与定向 eslint 已通过,Sidebar 聚合边界收口完成" -[2026-04-14T06:11:23Z] [SESSION-3] Completed [task-004] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T06:14:52Z] [SESSION-3] Starting [task-005] 为 documents.save 固定共享 payload 契约并抽出 save command adapter,收口正文保存前后端边界 (base=fc26ed21) -[2026-04-14T06:15:20Z] [SESSION-3] CHECKPOINT [task-005] step=1/2 "已新增 documents.save 共享 payload contract,补齐 revision/conflictDetectionKey 的最小边界并让 BlockNote 保存请求复用" -[2026-04-14T06:16:40Z] [SESSION-3] CHECKPOINT [task-005] step=2/2 "save-contract/bridge 定向测试与 eslint 已通过,正文保存 route 已抽出 save command adapter" -[2026-04-14T06:16:40Z] [SESSION-3] Completed [task-005] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T06:18:30Z] [SESSION-3] STATS tasks_total=6 completed=4 failed=1 pending=1 blocked=0 attempts_total=5 checkpoints=8 -[2026-04-14T06:18:30Z] [SESSION-3] LOCK released -[2026-04-14T06:42:18Z] [SESSION-4] CHECKPOINT [task-006] step=1/2 "已补 documents.content/save 的 revision 与 conflictDetectionKey 链路,并让 BlockNote 保存时携带快照采集元数据" -[2026-04-14T06:44:13Z] [SESSION-4] CHECKPOINT [task-006] step=2/2 "已通过 save-contract/bridge 定向测试,正文保存冲突会返回稳定 bridge 错误,checklist 已同步到 C3" -[2026-04-14T06:44:13Z] [SESSION-4] Completed [task-006] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T06:44:13Z] [SESSION-4] STATS tasks_total=6 completed=5 failed=1 pending=0 blocked=0 attempts_total=6 checkpoints=9 -[2026-04-14T06:44:13Z] [SESSION-4] LOCK released -[2026-04-14T06:47:35Z] [SESSION-4] INIT Added [task-007] via harness add -[2026-04-14T07:25:47Z] [SESSION-5] LOCK acquired (pid=2564404) -[2026-04-14T07:25:48Z] [SESSION-5] INIT Environment health check: PASS -[2026-04-14T07:25:49Z] [SESSION-5] Starting [task-007] 请完成该checklist /mnt/Data1T/mnote/design/rust-kernel-backport-phase-checklist.md (base=fc26ed21) -[2026-04-14T07:25:50Z] [SESSION-5] CHECKPOINT [task-007] step=1/2 "确认该 checklist 为 umbrella 任务,拆分为 task-008..task-017 的可验证子任务" -[2026-04-14T07:25:51Z] [SESSION-5] ERROR [task-007] [CONFIG] Missing validation.command for umbrella task; execution decomposed into task-008..task-017 -[2026-04-14T07:25:52Z] [SESSION-5] Starting [task-008] 统一单仓执行口径并补主仓维护边界文档,清理 ARCHITECTURE 中残留的 mnote-rust 承载叙事 (base=fc26ed21) -[2026-04-14T07:25:54Z] [SESSION-5] CHECKPOINT [task-008] step=1/1 "已新增单仓维护边界文档,并同步 ARCHITECTURE/plan/checklist 的主仓执行口径" -[2026-04-14T07:25:55Z] [SESSION-5] Completed [task-008] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T07:25:55Z] [SESSION-5] Starting [task-009] 固定 Mindmap 与 OnlyOffice 当前边界,明确历史仓对象页壳与第二套 sidebar 不进入主线 (base=fc26ed21) -[2026-04-14T07:25:56Z] [SESSION-5] CHECKPOINT [task-009] step=1/1 "已新增 Mindmap/OnlyOffice 边界说明,并把 Phase D 中可由源码证明的项补到账" -[2026-04-14T07:25:57Z] [SESSION-5] Completed [task-009] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T07:25:57Z] [SESSION-5] Starting [task-010] 冻结 Sidebar Rust query 目标文档,收口 sidebar.dataset.list 的最小 payload/result 契约 (base=fc26ed21) -[2026-04-14T07:25:58Z] [SESSION-5] CHECKPOINT [task-010] step=1/1 "已把 Sidebar Rust query 接入目标、前后端职责边界与禁止事项固定成独立文档" -[2026-04-14T07:25:59Z] [SESSION-5] Completed [task-010] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T07:25:59Z] [SESSION-5] Starting [task-011] 补主仓回迁资产与发布前源码审计文档,完成单仓启动路径、保存口径、trace 一致性与禁止事项复查 (base=fc26ed21) -[2026-04-14T07:26:00Z] [SESSION-5] CHECKPOINT [task-011] step=1/1 "已新增回迁资产清单与发布前源代码审计文档,并同步发布前 checklist 中源码可验证项" -[2026-04-14T07:26:01Z] [SESSION-5] Completed [task-011] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T07:26:01Z] [SESSION-5] Starting [task-012] 补 documents.save 成功路径 bridge 单测,确认低风险保存模式、identity 口径与回查链闭环 (base=fc26ed21) -[2026-04-14T07:26:02Z] [SESSION-5] CHECKPOINT [task-012] step=1/1 "已补 documents.save 成功路径 bridge 测试,并把 C3 的低风险保存模式、identity 口径与可回查项同步到 checklist" -[2026-04-14T07:26:03Z] [SESSION-5] Completed [task-012] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T07:26:05Z] [SESSION-5] STATS tasks_total=17 completed=10 failed=2 pending=5 blocked=0 attempts_total=12 checkpoints=15 -[2026-04-14T07:26:06Z] [SESSION-5] LOCK released -[2026-04-14T07:42:15Z] [SESSION-6] LOCK acquired (pid=2803230) -[2026-04-14T07:42:15Z] [SESSION-6] INIT Environment health check: PASS -[2026-04-14T07:42:15Z] [SESSION-6] Starting [task-013] 将至少一条 Sidebar 主查询改为真实 Rust query 供给,并验证文档树/文件树/回收站语义不变 (base=fc26ed21) -[2026-04-14T07:56:00Z] [SESSION-6] CHECKPOINT [task-013] step=1/1 "已新增单个 sidebar.datasetList 主查询,并让 server helper / 实时 hook 统一消费该数据集;sidebar-data/file-tree 定向测试、eslint 与 storage-convex-bridge 单测通过" -[2026-04-14T07:56:00Z] [SESSION-6] Completed [task-013] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T07:56:00Z] [SESSION-6] STATS tasks_total=17 completed=11 failed=2 pending=4 blocked=0 attempts_total=13 checkpoints=16 -[2026-04-14T07:56:00Z] [SESSION-6] LOCK released -[2026-04-14T08:13:25Z] [SESSION-7] LOCK acquired (pid=23844) -[2026-04-14T08:13:25Z] [SESSION-7] RECOVERY [task-014] action="resume_validation" reason="exclusive mode found stale in_progress task with no active lock; validating uncommitted implementation" -[2026-04-14T08:13:25Z] [SESSION-7] INIT Environment health check: PASS -[2026-04-14T08:13:25Z] [SESSION-7] Starting [task-014] 统一所有新命令和查询先进入 core-protocol envelope,再转给 storage-convex-bridge (base=fc26ed21) -[2026-04-14T08:14:45Z] [SESSION-7] CHECKPOINT [task-014] step=1/1 "已确认新增命令/查询统一先进入 core-protocol envelope,并完成 storage-convex-bridge 映射与 Rust 单测/检查闭环" -[2026-04-14T08:14:45Z] [SESSION-7] Completed [task-014] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T08:14:45Z] [SESSION-7] Starting [task-015] 补文档页、Sidebar、BlockNote 保存的端到端 UI 回归,并确认外观与交互未退化 (base=fc26ed21) -[2026-04-14T08:14:45Z] [SESSION-7] CHECKPOINT [task-015] step=1/3 "已修复 layout/sidebar route 中未定义 auth 的直接回归点,开始执行前端 lint/test" -[2026-04-14T08:15:16Z] [SESSION-7] CHECKPOINT [task-015] step=2/3 "pnpm lint 已通过(0 error,保留仓库既有 warnings),继续执行全量测试" -[2026-04-14T08:48:19Z] [SESSION-7] Starting [task-016] 统一 Mindmap 与 OnlyOffice 的页面标识、附件标识、页面归属和追踪字段,并补 Mindmap 交互回归 (base=fc26ed21) -[2026-04-14T08:48:19Z] [SESSION-7] CHECKPOINT [task-016] step=1/2 "已为 Mindmap API 与组件侧补统一 meta 口径,对齐 pageId/documentId、attachmentId/mindmapId、workspaceId 与 requestId/traceId" -[2026-04-14T08:50:25Z] [SESSION-7] CHECKPOINT [task-015] step=3/3 "已完成全量前端回归:pnpm lint 0 error,vitest 76/76 通过,文档页/Sidebar/正文保存链未见退化" -[2026-04-14T08:50:25Z] [SESSION-7] Completed [task-015] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T08:50:25Z] [SESSION-7] CHECKPOINT [task-016] step=2/2 "已通过定向 eslint 验证 Mindmap API 与组件侧的统一标识/追踪元信息口径,OnlyOffice 共享口径已同步到设计文档" -[2026-04-14T08:50:25Z] [SESSION-7] Completed [task-016] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T08:50:25Z] [SESSION-7] Starting [task-017] 补 OnlyOffice 打开、签名、代理、callback、forcesave 与静态资源回归,完成单仓发布前收口标准 (base=fc26ed21) -[2026-04-14T08:50:25Z] [SESSION-7] CHECKPOINT [task-017] step=1/1 "开始执行 OnlyOffice 定向 eslint 回归,验证页面、签名、代理、callback、forcesave 边界" -[2026-04-14T08:51:34Z] [SESSION-7] CHECKPOINT [task-017] step=2/2 "已完成 OnlyOffice 页面/API 定向 eslint,且根目录 onlyoffice-web-apps/onlyoffice-plugins/onlyoffice-data 关键静态资源目录仍在" -[2026-04-14T08:51:34Z] [SESSION-7] Completed [task-017] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T08:51:34Z] [SESSION-7] STATS tasks_total=17 completed=15 failed=2 pending=0 blocked=0 attempts_total=17 checkpoints=20 -[2026-04-14T08:51:34Z] [SESSION-7] LOCK released -[2026-04-14T11:18:19Z] [SESSION-8] LOCK acquired (pid=3857410) -[2026-04-14T11:18:19Z] [SESSION-8] INIT Environment health check: PASS -[2026-04-14T11:18:19Z] [SESSION-8] Starting [task-018] 把 documents.title/stats/options/save 与 sidebar.dataset.list 的当前 route/adapter 契约同步回 checklist 和计划文档,明确哪些链路已 envelope 化、哪些仍未切到真实 Rust 执行链 (base=fc26ed21) -[2026-04-14T11:20:16Z] [SESSION-8] CHECKPOINT [task-018] step=1/1 "已确认 checklist 与计划文档同步包含 documents.title/stats/options/save、sidebar.dataset.list 的 envelope/真实 Rust bridge 现状说明,无需再补正文" -[2026-04-14T11:20:16Z] [SESSION-8] Completed [task-018] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T11:23:47Z] [SESSION-8] Starting [task-019] 为文档页元信息、Sidebar、BlockNote 保存链补真实浏览器端到端回归,完成发布前 UI 验收缺口并把 checklist 对应项打勾 (base=fc26ed21) -[2026-04-14T11:53:03Z] [SESSION-8] CHECKPOINT [task-019] step=1/2 "已修复真实浏览器回归前置环境:重新注册 Convex functions,首页从 500 恢复为正常 307 跳转到文档页" -[2026-04-14T11:53:03Z] [SESSION-8] CHECKPOINT [task-019] step=2/2 "已新增并跑通 scripts/task019-document-ui-regression.js:真实浏览器验证 Sidebar/页面标题/BlockNote 正文保存与刷新保留,临时页面已自动清理" -[2026-04-14T11:56:09Z] [SESSION-8] Completed [task-019] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T12:25:20Z] [SESSION-8] Starting [task-020] 把 pages 元信息与正文保存链从当前 TypeScript adapter 再向前推进一小步:至少选一条 documents.title.update 或 documents.save,做真实 Rust bridge 执行样板或最小可运行接缝,避免 route->TS adapter 长期停留在临时态 (base=fc26ed21)\n[2026-04-14T12:43:51Z] [SESSION-8] CHECKPOINT [task-020] step=1/2 "已新增 buildDocumentBridgeMutationRequest 与统一执行器,让 documents.title/stats/options/save 先生成与 storage-convex-bridge 对齐的 functionName/payloadJson/args runtime request" -[2026-04-14T12:43:51Z] [SESSION-8] CHECKPOINT [task-020] step=2/2 "已通过 task-020 指定的 cargo test、bridge vitest 与定向 eslint,并同步 plan/checklist 说明最小真实 bridge 运行接缝" -[2026-04-14T12:43:51Z] [SESSION-8] Completed [task-020] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T12:43:51Z] [SESSION-8] Starting [task-021] 补 Mindmap 真实交互回归并同步 checklist,确认当前页面、保存、节点操作与追踪字段口径在浏览器层未退化 (base=fc26ed21) -[2026-04-14T14:01:55Z] [SESSION-8] CHECKPOINT [task-021] step=1/2 "已新增并跑通 scripts/task021-mindmap-ui-regression.js:真实浏览器验证 Mindmap 全屏页元信息、工具栏/大纲侧栏、子节点新增删除与保存刷新链" -[2026-04-14T14:01:55Z] [SESSION-8] CHECKPOINT [task-021] step=2/2 "已通过 task-021 指定的定向 eslint(0 error,保留仓库既有 warnings),并同步 checklist 中的 Mindmap 浏览器回归结论" -[2026-04-14T14:01:55Z] [SESSION-8] Completed [task-021] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T14:01:55Z] [SESSION-8] Starting [task-022] 补 OnlyOffice 真实页面打开与回调链回归,确认签名、代理、callback、forcesave 与静态资源在浏览器层可用,并把发布前 checklist 对应项收口 (base=fc26ed21) -[2026-04-14T18:08:23Z] [SESSION-9] LOCK acquired (pid=1767411) -[2026-04-14T18:08:23Z] [SESSION-9] INIT Environment health check: PASS -[2026-04-14T18:08:23Z] [SESSION-9] RECOVERY [task-022] action="reprioritize_to_upgrade_first" reason="user requested ONLYOFFICE upgrade before reconnecting Rust bridge" -[2026-04-14T18:08:23Z] [SESSION-9] WARN [task-022] strategy changed: split upgrade into [task-023] and reconnect regression into [task-024] -[2026-04-14T18:08:23Z] [SESSION-9] STATS tasks_total=24 completed=19 failed=3 pending=2 blocked=0 attempts_total=22 checkpoints=28 -[2026-04-14T18:20:18Z] [SESSION-9] Starting [task-023] 升级 ONLYOFFICE 文档服务到当前可用稳定版本,并对齐静态资源、WebSocket 代理、自定义插件与 callback/forcesave 基线 (base=fc26ed21)\n[2026-04-14T18:20:18Z] [SESSION-9] CHECKPOINT [task-023] step=1/3 "已确认升级目标为 GitHub release v9.3.1,且当前 8082 容器仍由旧仓 mnote-rust 的 compose 与插件挂载驱动。"\n[2026-04-14T18:25:24Z] [SESSION-9] CHECKPOINT [task-023] step=2/3 "已在当前主仓补齐 ONLYOFFICE 部署文件,并拉取 onlyoffice/documentserver:9.3.1 镜像,开始接管 8082 容器。"\n[2026-04-14T18:40:31Z] [SESSION-9] CHECKPOINT [task-023] step=3/3 "已用当前主仓 compose 接管 8082 并升级到 onlyoffice/documentserver:9.3.1,api.js 探测与 3001 代理均恢复正常。"\n[2026-04-14T18:40:31Z] [SESSION-9] Completed [task-023] (commit skipped by repo rule; base=fc26ed21)\n[2026-04-14T18:40:31Z] [SESSION-9] Starting [task-024] 在升级后的 ONLYOFFICE 基线上,重新连接页面打开、签名、代理、callback、forcesave、自定义插件桥与 Rust 内核接缝,并完成真实浏览器回归与 checklist 收口 (base=fc26ed21)\n[2026-04-14T18:40:31Z] [SESSION-9] CHECKPOINT [task-024] step=1/1 "已修复 /onlyoffice/plugins/* 放行与 agent-tools 非可视插件配置,升级后真实浏览器回归通过,签名/代理/callback/forcesave/插件桥闭环正常。"\n[2026-04-14T18:40:31Z] [SESSION-9] Completed [task-024] (commit skipped by repo rule; base=fc26ed21)\n[2026-04-14T18:40:31Z] [SESSION-9] STATS tasks_total=24 completed=21 failed=3 pending=0 blocked=0 attempts_total=24 checkpoints=32\n[2026-04-14T18:43:06Z] [SESSION-9] Starting [task-025] 把 OnlyOffice callback 附件写回链接到统一 bridge/Rust 协议边界:补最小 media asset writeback command、storage-convex-bridge 映射与 Next route 执行器,替换 callback 里的直连 Convex mutation (base=fc26ed21)\n[2026-04-14T18:43:06Z] [SESSION-9] CHECKPOINT [task-025] step=1/3 "已锁定 OnlyOffice callback 写回链为下一条 Rust 接缝推进目标,因为它仍直接调用 Convex mediaAssets mutation。"\n[2026-04-14T20:07:41Z] [SESSION-9] CHECKPOINT [task-025] step=2/3 "已把 OnlyOffice callback 写回链收口到 media.assets.replace_storage:补齐 core-protocol/storage-convex-bridge 映射、显式 actor bridge context 与 callback 写回执行器。" -[2026-04-14T20:07:41Z] [SESSION-9] CHECKPOINT [task-025] step=3/3 "已通过 cargo test -p core-protocol -p storage-convex-bridge、bridge/callback route 定向测试、定向 eslint 与真实 task022 浏览器回归;中途通过重新注册 Convex functions 修复了 auth:signIn 缺失导致的登录页卡死。" -[2026-04-14T20:07:41Z] [SESSION-9] Completed [task-025] (commit skipped by repo rule; base=fc26ed21) -[2026-04-14T20:07:41Z] [SESSION-9] STATS tasks_total=25 completed=22 failed=3 pending=0 blocked=0 attempts_total=25 checkpoints=34 -[2026-04-14T20:07:41Z] [SESSION-9] LOCK released -[2026-04-14T22:51:41Z] [SESSION-10] LOCK acquired (pid=3163948) -[2026-04-14T22:51:41Z] [SESSION-10] INIT Environment health check: PASS -[2026-04-14T22:52:10Z] [SESSION-10] Starting [task-026] 基于 design/rust-kernel-replacement-roadmap-v1.md 建立真实 Rust bridge/runtime 目录与最小执行样板,让至少一条读链和两条写链从 Web route 真实进入 Rust 执行器 (base=de35d423) -[2026-04-14T22:59:47Z] [SESSION-10] CHECKPOINT [task-026] step=1/3 "已确定 task-026 最小样板边界:新增 Rust bridge runtime 二进制,先接管 documents.content.get、documents.title.update、documents.save 的执行计划与参数映射,TS 仅保留 Convex transport。" -[2026-04-14T23:32:53Z] [SESSION-10] CHECKPOINT [task-026] step=2/3 "已完成 Phase 1 最小样板:新增 bridge-runtime crate 与 rust/bridge 目录说明,documents.content.get / documents.title.update / documents.save 现已先经 Rust 生成执行计划,再由 TS 仅做 Convex transport;cargo test 全 workspace、bridge.test.ts、定向 eslint 与 runtime stdin 烟测均通过。" -[2026-04-14T23:32:53Z] [SESSION-10] Completed [task-026] (commit skipped by repo rule; base=de35d423) -[2026-04-14T23:33:12Z] [SESSION-10] Starting [task-027] 把 mnote-cli 并入主仓 Rust workspace,先冻结 page/block/search/sidebar/tool 的最小命令面与 --json 输出协议 (base=de35d423) -[2026-04-14T23:49:31Z] [SESSION-10] CHECKPOINT [task-027] step=1/1 "已并入 mnote-cli:新增主仓 workspace crate,冻结 page/block/search/sidebar/tool 五类最小命令面与 --json 输出协议,cargo check/test -p mnote-cli 通过,workspace 全量 cargo test 通过,CLI page get --json 烟测输出稳定。" -[2026-04-14T23:49:31Z] [SESSION-10] Completed [task-027] (commit skipped by repo rule; base=de35d423) -[2026-04-14T23:51:48Z] [SESSION-10] Starting [task-028] 完成页面系统 Rust 化第一批:把 documents.create/move/delete/restore/duplicate/copy-tree 收口到 Rust command,Next route 仅保留 transport 包装 (base=de35d423) -[2026-04-15T00:47:59Z] [SESSION-11] LOCK acquired (pid=main-resume) -[2026-04-15T00:47:59Z] [SESSION-11] RECOVERY [task-028] action="resume_from_uncommitted_state" reason="exclusive mode found stale lock and task remained in_progress with local implementation changes present" -[2026-04-15T00:47:59Z] [SESSION-11] INIT Environment health check: PASS -[2026-04-15T01:36:12Z] [SESSION-11] CHECKPOINT [task-028] step=4/4 "已把 documents.create/move/delete/restore/duplicate/copy-tree 收口到 Rust command + page adapter;Convex functions 刷新、cargo test、bridge vitest 与定向 eslint(0 error)已通过" -[2026-04-15T01:36:12Z] [SESSION-11] Completed [task-028] (commit skipped by repo rule; base=de35d423) -[2026-04-15T01:36:12Z] [SESSION-11] STATS tasks_total=35 completed=25 failed=3 pending=7 blocked=0 attempts_total=28 checkpoints=69 -[2026-04-15T01:36:12Z] [SESSION-11] LOCK released -[2026-04-15T01:45:50Z] [SESSION-12] LOCK acquired (pid=main-session12) -[2026-04-15T01:45:50Z] [SESSION-12] INIT Environment health check: PASS -[2026-04-15T01:45:50Z] [SESSION-12] Starting [task-029] 完成块系统 Rust 化第一批:把 blocks.get/patch/move/embed 与 AI 文档写入工具统一到 Rust block ops 协议 (base=de35d423) -[2026-04-15T01:49:32Z] [SESSION-12] CHECKPOINT [task-029] step=1/3 "已确认 block route/AI 文档写入仍存在 TS 业务拼接,开始收口到统一 block ops 适配层" -[2026-04-15T01:56:41Z] [SESSION-13] LOCK acquired (pid=4119291) -[2026-04-15T01:56:41Z] [SESSION-13] RECOVERY [task-029] action="resume_from_uncommitted_state" reason="exclusive mode found stale task-029 in_progress with no active lock; continuing unfinished Rust block ops integration" -[2026-04-15T01:56:41Z] [SESSION-13] INIT Environment health check: PASS -[2026-04-15T02:17:43Z] [SESSION-12] CHECKPOINT [task-029] step=2/3 "AI 文档写入工具改为复用纯 block ops 适配层,不再依赖长期的 loadBlocks/saveBlocks 直连路径" -[2026-04-15T02:17:43Z] [SESSION-12] CHECKPOINT [task-029] step=3/3 "已补定向 vitest;eslint 仅剩仓库既有 warnings,未引入新 error" -[2026-04-15T02:17:43Z] [SESSION-12] STATS tasks_total=35 completed=25 failed=3 pending=6 blocked=0 attempts_total=28 checkpoints=70 -[2026-04-15T02:51:38Z] [SESSION-13] CHECKPOINT [task-029] step=1/3 "已补 blocks.get/patch/move/embed 的 Rust 契约与共享 block adapter,4 个 route 收口为 transport 薄层" -[2026-04-15T02:51:38Z] [SESSION-13] CHECKPOINT [task-029] step=2/3 "AI doc_insert_blocks/doc_replace_range 已切到共享块快照 adapter,不再直连旧 saveBlocks 注入" -[2026-04-15T02:51:38Z] [SESSION-13] CHECKPOINT [task-029] step=3/3 "已通过 cargo test --manifest-path rust/Cargo.toml 与 task-029 指定 eslint(仅仓库既有 warnings,无新增 error)" -[2026-04-15T02:51:38Z] [SESSION-13] Completed [task-029] (commit skipped by repo rule; base=de35d423) -[2026-04-15T02:51:38Z] [SESSION-13] STATS tasks_total=35 completed=26 failed=3 pending=6 blocked=0 attempts_total=28 checkpoints=70 -[2026-04-15T02:52:21Z] [SESSION-13] LOCK released -[2026-04-15T03:02:25Z] [SESSION-14] LOCK acquired (pid=300141) -[2026-04-15T03:02:25Z] [SESSION-14] INIT Environment health check: PASS -[2026-04-15T03:55:00Z] [SESSION-14] CHECKPOINT [task-030] step=1/3 "已让 search.documents、search.recent、sidebar.dataset.list 进入 Rust query/runtime 主链,route 不再持有核心 ranking/snippet 逻辑" -[2026-04-15T03:55:30Z] [SESSION-14] CHECKPOINT [task-030] step=2/3 "已把 index-fts 接入产品搜索主路径,新增 evaluate_search_documents 负责召回合并、高亮 snippet 与 OCR 待补队列决策" -[2026-04-15T03:56:00Z] [SESSION-14] CHECKPOINT [task-030] step=3/3 "已通过 cargo test -p index-fts -p storage-convex-bridge -p bridge-runtime 与 task-030 指定 eslint;tsc 失败仍为仓库既有 Convex/类型问题,非本次新增" -[2026-04-15T03:56:00Z] [SESSION-14] Completed [task-030] (commit skipped by repo rule; base=de35d423) -[2026-04-15T03:56:00Z] [SESSION-14] STATS tasks_total=35 completed=27 failed=3 pending=5 blocked=0 attempts_total=28 checkpoints=70 -[2026-04-15T03:56:05Z] [SESSION-14] LOCK released -[2026-04-15T05:23:51Z] [SESSION-15] LOCK acquired (pid=1000177) -[2026-04-15T05:23:51Z] [SESSION-15] INIT Environment health check: PASS -[2026-04-15T05:23:51Z] [SESSION-15] Starting [task-031] 为 Rust 内核建立统一 Tool 协议与注册表,让 CLI 与 AI 共享 doc_get/doc_find/doc_insert_blocks/doc_replace_range 等一组同源工具 (base=de35d423) -[2026-04-15T05:58:48Z] [SESSION-15] CHECKPOINT [task-031] step=1/3 "已在 Rust 侧补齐 tool registry、Tool execution mode 与 bridge-runtime Tool 计划/结果执行链" -[2026-04-15T05:58:48Z] [SESSION-15] CHECKPOINT [task-031] step=2/3 "CLI tool run 与 /api/ai-agent/run 的 doc_* 已切到统一 Rust tool protocol,不再以前端私有 docTools 作为 Convex 主路径真入口" -[2026-04-15T05:58:48Z] [SESSION-15] CHECKPOINT [task-031] step=3/3 "已补 explain-plan/result 模式并通过 cargo test、eslint、CLI/runtime 烟测;trace/request 字段沿用统一 bridge context 口径" -[2026-04-15T06:01:57Z] [SESSION-15] Completed [task-031] (commit skipped by repo rule; base=de35d423) -[2026-04-15T06:01:57Z] [SESSION-15] STATS tasks_total=35 completed=28 failed=3 pending=4 blocked=0 attempts_total=29 checkpoints=46 -[2026-04-15T06:01:57Z] [SESSION-15] LOCK released -[2026-04-15T06:26:48Z] [SESSION-16] LOCK acquired (pid=main-session16) -[2026-04-15T06:26:48Z] [SESSION-16] INIT Environment health check: PASS -[2026-04-15T06:26:48Z] [SESSION-16] Starting [task-032] 并入 adapter-mindmap,形成可被 Web/CLI/AI 共用的 Rust mindmap ops 协议与执行层 (base=de35d423) -[2026-04-15T07:07:41Z] [SESSION-17] LOCK acquired (pid=main-session17) -[2026-04-15T07:07:41Z] [SESSION-17] RECOVERY [task-032] action="resume_from_uncommitted_state" reason="exclusive mode found task-032 in_progress with no active lock; continuing unfinished Rust mindmap integration" -[2026-04-15T07:07:41Z] [SESSION-17] INIT Environment health check: PASS -[2026-04-15T07:15:09Z] [SESSION-17] CHECKPOINT [task-032] step=1/3 "已修复 Rust mindmap 协议兼容层:MindmapOp 支持 camelCase/历史 alias,bridge-runtime 与 CLI 定向 cargo test 全部通过" -[2026-04-15T07:21:09Z] [SESSION-17] CHECKPOINT [task-032] step=2/3 "已把 legacy mindmap route 收口到 Rust query/command plan,AI mindmap tools 已接入 executeRustBridgeTool,核心读写与高阶包装工具统一落到 Rust tool runtime" -[2026-04-15T07:24:31Z] [SESSION-17] CHECKPOINT [task-032] step=3/3 "已通过 workspace cargo test、task-032 指定 eslint、CLI smoke;legacy mindmap route 与 AI mindmap tools 均已切到 Rust bridge/runtime 主链" -[2026-04-15T07:24:31Z] [SESSION-17] Completed [task-032] (commit skipped by repo rule; base=de35d423) -[$TS] [SESSION-17] STATS tasks_total=35 completed=29 failed=3 pending=3 blocked=0 attempts_total=30 checkpoints=70 -[2026-04-15T07:24:31Z] [SESSION-17] LOCK released -[2026-04-15T07:29:56Z] [SESSION-18] LOCK acquired (pid=1699064) -[2026-04-15T07:29:56Z] [SESSION-18] INIT Environment health check: PASS -[2026-04-15T07:29:56Z] [SESSION-18] Starting [task-033] 并入 adapter-onlyoffice,把 sign/proxy/callback/forcesave/session 收口到 Rust 对象适配层,OnlyOffice 前端页只保留 transport 与展示职责 (base=de35d423) -[2026-04-15T08:02:28Z] [SESSION-18] CHECKPOINT [task-033] step=1/3 "已并入 adapter-onlyoffice,补齐 asset/session/sign/proxy/callback/forcesave 的 Rust 对象边界与 tool registry" -[2026-04-15T08:02:28Z] [SESSION-18] CHECKPOINT [task-033] step=2/3 "OnlyOffice sign/proxy/callback/forcesave route 已收口为共享 adapter + Rust tool 包装,前端页只保留展示与 transport 组装" -[2026-04-15T08:02:28Z] [SESSION-18] CHECKPOINT [task-033] step=3/3 "已通过 Rust workspace 测试、OnlyOffice 定向 vitest/eslint 与真实 task022 浏览器回归;并修正 mnote-env.json 与回归脚本默认端口到 3000" -[2026-04-15T08:02:28Z] [SESSION-18] Completed [task-033] (commit skipped by repo rule; base=de35d423) -[2026-04-15T08:02:28Z] [SESSION-18] STATS tasks_total=35 completed=30 failed=3 pending=2 blocked=0 attempts_total=31 checkpoints=70 -[2026-04-15T08:02:28Z] [SESSION-18] LOCK released -[2026-04-15T08:21:36Z] [SESSION-19] LOCK acquired (pid=1981918) -[2026-04-15T08:21:36Z] [SESSION-19] INIT Environment health check: PASS -[2026-04-15T08:21:36Z] [SESSION-19] Starting [task-034] 建立 Rust 内核统一观测面:补全 command/event/request/trace 的全域落账、幂等、冲突、回放和索引重建命令,作为 CLI 与 AI 稳定运行前置条件 (base=de35d423) -[2026-04-15T08:40:21Z] [SESSION-19] CHECKPOINT [task-034] step=1/3 "已统一 bridge request/trace/command 的 Rust query/tool 协议,并让 /api/bridge/request 与 /api/bridge/trace 切到 Rust runtime transport 主链" -[2026-04-15T08:40:21Z] [SESSION-19] CHECKPOINT [task-034] step=2/3 "已补 event_replay 与 index_rebuild 恢复命令,bridge-runtime 可直接输出回放/重建摘要,cargo test 与 bridge 定向 eslint 已通过" -[2026-04-15T08:40:21Z] [SESSION-19] CHECKPOINT [task-034] step=3/3 "已同步路线图、missing-targets、phase checklist 与 core 设计文档,把统一观测面写成 Rust cutover 的硬前置条件" -[2026-04-15T08:40:21Z] [SESSION-19] Completed [task-034] (commit skipped by repo rule; base=de35d423) -[2026-04-15T08:40:21Z] [SESSION-19] STATS tasks_total=35 completed=31 failed=3 pending=1 blocked=0 attempts_total=32 checkpoints=70 -[2026-04-15T08:56:43Z] [SESSION-20] LOCK acquired (pid=2168549) -[2026-04-15T08:56:43Z] [SESSION-20] INIT Environment health check: PASS -[2026-04-15T08:56:43Z] [SESSION-20] Starting [task-035] 制定并执行旧 TS 执行面退役清单:按路线图阶段标记哪些 route 仅剩 transport,哪些业务逻辑允许删除,并形成 Rust 成为唯一业务执行平面的最终 cutover 文档 (base=de35d423) -[2026-04-15T09:01:04Z] [SESSION-20] CHECKPOINT [task-035] step=1/3 "已按能力域盘点主仓 route,明确页面、块、查询、AI、Mindmap、OnlyOffice 分别属于 Rust owner、transport keep 或旧 TS 兼容面" -[2026-04-15T09:01:04Z] [SESSION-20] CHECKPOINT [task-035] step=2/3 "已补最终 cutover 文档,写清 Phase 8 四组 gate、允许删除与必须保留的 Web 壳,以及旧 TS 执行面退役顺序" -[2026-04-15T09:01:04Z] [SESSION-20] CHECKPOINT [task-035] step=3/3 "已同步 roadmap、missing-targets、phase checklist 与 release audit,并通过 task-035 验证脚本" -[2026-04-15T09:01:04Z] [SESSION-20] Completed [task-035] (commit skipped by repo rule; base=de35d423) -[2026-04-15T09:01:04Z] [SESSION-20] STATS tasks_total=35 completed=32 failed=3 pending=0 blocked=0 attempts_total=33 checkpoints=70 -[2026-04-15T10:04:37Z] [SESSION-20] INFO Pending queue rebuilt from rust-kernel-cutover-v1.md; appended [task-036]..[task-044] to cover remaining TS_COMPAT_PENDING page routes, AI builtins/tool cutover, mindmap leftovers, observability gaps, status logging, TS_LEGACY_DELETE cleanup, and final Phase 8 verification -[2026-04-15T10:50:15Z] [SESSION-21] LOCK acquired (pid=main-session21) -[2026-04-15T10:50:15Z] [SESSION-21] INIT Environment health check: PASS -[2026-04-15T10:50:15Z] [SESSION-21] Starting [task-041] 补齐统一观测面 workspace 总览、分页与按对象/状态过滤,让 bridge 查询成为 cutover 阶段的正式运营与回查入口 (base=de35d423) -[2026-04-15T10:50:15Z] [SESSION-21] CHECKPOINT [task-041] step=1/2 "已补 bridge.workspace.overview:Convex bridgeLogs 支持 workspace 总览、cursor 分页、command/event 状态过滤、对象过滤;Rust query/runtime 与 /api/bridge/workspace 已接通。" -[2026-04-15T10:50:15Z] [SESSION-21] CHECKPOINT [task-041] step=2/2 "已通过 cargo test -p bridge-runtime -p storage-convex-bridge、src/lib/documents/bridge.test.ts、bridge workspace/request/trace 定向 eslint;仅剩仓库既有 any warnings。" -[2026-04-15T10:50:15Z] [SESSION-21] Completed [task-041] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:03:14Z] [SESSION-21] Starting [task-036] 页面兼容旧面第一批 Rust 化:把 documents/create-child、documents/embed、documents/template 收口到统一 page command/query adapter,route 仅保留 transport (base=de35d423) -[2026-04-15T11:03:14Z] [SESSION-21] CHECKPOINT [task-036] step=1/2 "已把 create-child/embed/template 三条页面旧路由收口到统一 page adapter,route 仅保留 transport。" -[2026-04-15T11:03:14Z] [SESSION-21] CHECKPOINT [task-036] step=2/2 "已通过 route-adapters/bridge 定向测试、cargo test -p bridge-runtime -p storage-convex-bridge 与 task-036 指定 eslint。" -[2026-04-15T11:03:14Z] [SESSION-21] Completed [task-036] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:03:14Z] [SESSION-21] Starting [task-037] 页面兼容旧面第二批 Rust 化:把 documents/empty-trash、documents/purge 收口到 Rust lifecycle command,并切断 route 内直连 Convex 业务逻辑 (base=de35d423) -[2026-04-15T11:03:14Z] [SESSION-21] CHECKPOINT [task-037] step=1/2 "已把 empty-trash/purge 收口到统一 page adapter,并补统一失败态落账接缝。" -[2026-04-15T11:03:14Z] [SESSION-21] CHECKPOINT [task-037] step=2/2 "已通过 task-037 指定 cargo test 与 eslint;页面旧面第二批已降级为 transport route。" -[2026-04-15T11:03:14Z] [SESSION-21] Completed [task-037] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:03:14Z] [SESSION-21] Starting [task-039] 建立 AI 工具 cutover 矩阵:盘点 builtins/**、ai-agent/run 余下工具与 Rust Tool registry 的一一对应,并冻结迁移顺序与删除清单 (base=de35d423) -[2026-04-15T11:03:14Z] [SESSION-21] CHECKPOINT [task-039] step=1/2 "已新增 ai-tool-cutover-matrix,覆盖 docsServerTools/mediaServerTools/mindmapServerTools/onlyofficeServerTools/registryBuiltins/ai-agent/run 的映射关系。" -[2026-04-15T11:03:14Z] [SESSION-21] CHECKPOINT [task-039] step=2/2 "已补 search_web/image_read/slash_run 的 Rust tool registry 对照,并通过 task-039 文档校验。" -[2026-04-15T11:03:14Z] [SESSION-21] Completed [task-039] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:03:14Z] [SESSION-21] ERROR [task-038] [TASK_EXEC] delegated subagent failed before code delivery: Missing required parameter: input[204].name; task reclaimed by main session for local execution -[2026-04-15T11:06:05Z] [SESSION-21] Starting [task-038] Mindmap 剩余旧面收口:把 mindmap-trash/empty 与 mindmap-ai/* 迁到 Rust mindmap adapter/tool,明确 route 只做产品编排而不是对象规则真入口 (base=de35d423) -[2026-04-15T11:06:05Z] [SESSION-21] CHECKPOINT [task-038] step=1/2 "已确认 mindmap-trash/empty 与 mindmap-ai 主路径接入 mindmap_empty_trash、mindmap_apply_ops 等 Rust tool/runtime。" -[2026-04-15T11:06:05Z] [SESSION-21] CHECKPOINT [task-038] step=2/2 "已通过 task-038 指定 cargo test 与 eslint;仅保留仓库既有 unused/no-explicit-any warnings。" -[2026-04-15T11:06:05Z] [SESSION-21] Completed [task-038] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] Starting [task-040] 迁移 AI 非 doc_* 核心工具到 Rust Tool runtime,收缩 ai-agent/run 与 builtins/** 私有真入口,只保留 client-tool-result 这类 transport 壳 (base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-040] step=1/2 "已补 bridge-runtime 对 search_web/image_read/slash_run 的 tool plan 与 slash 结果解析,ai-agent/run 不再保留 search_web 的 TS fallback,media/slash builtins 只剩 transport helper。" -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-040] step=2/2 "已通过 cargo test -p bridge-runtime -p mnote-cli 与 task-040 指定 eslint;仅剩仓库既有 any/unused warnings。" -[2026-04-15T11:32:44Z] [SESSION-21] Completed [task-040] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] Starting [task-042] 把失败态、冲突态与补偿态统一落账到 bridge-log/runtime,覆盖页面主写链与 AI、Mindmap 写链,完成 cutover 所需状态语义闭环 (base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-042] step=1/2 "已补 bridge-log 失败/补偿状态语义、metadata command 失败落账,以及 AI/Mindmap 写链的统一失败态记录;mindmap 本地 ops 冲突不再伪装成功。" -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-042] step=2/2 "已通过 cargo test -p bridge-runtime -p storage-convex-bridge、src/lib/documents/bridge.test.ts 与 task-042 指定 eslint;仅剩仓库既有 any/unused warnings。" -[2026-04-15T11:32:44Z] [SESSION-21] Completed [task-042] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] Starting [task-043] 按 cutover 文档执行第一批 TS_LEGACY_DELETE:先停写并切调用方,再删除已被 Rust 替代的 builtins 服务端真入口与过渡 helper (base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-043] step=1/2 "已新增 rust-kernel-legacy-delete-list.md,冻结第一批 TS_LEGACY_DELETE 清单:documents/create-child/embed/empty-trash/purge/template、mindmap-trash/empty、builtins/**。" -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-043] step=2/2 "已通过 task-043 文档校验与指定 eslint;仅剩仓库既有 any/unused warnings。" -[2026-04-15T11:32:44Z] [SESSION-21] Completed [task-043] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] Starting [task-044] 完成 Phase 8 cutover 最终验证与文档回写,确认剩余 Web route 全部降级为 TS_TRANSPORT_KEEP 或 Rust owner,并形成可对外宣布的统一口径 (base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-044] step=1/2 "已回写 cutover/release-audit/phase-checklist/missing-targets,补齐 Phase 8 最终口径、TS_TRANSPORT_KEEP/TS_LEGACY_DELETE 边界与可对外宣布文案。" -[2026-04-15T11:32:44Z] [SESSION-21] CHECKPOINT [task-044] step=2/2 "已通过 task-044 文档校验脚本。" -[2026-04-15T11:32:44Z] [SESSION-21] Completed [task-044] (commit skipped by repo rule; base=de35d423) -[2026-04-15T11:32:44Z] [SESSION-21] STATS tasks_total=44 completed=41 failed=3 pending=0 blocked=0 attempts_total=42 checkpoints=88 -[2026-04-15T11:53:44Z] [SESSION-22] LOCK acquired (pid=main-session22) -[2026-04-15T11:53:44Z] [SESSION-22] INIT Environment health check: PASS -[2026-04-15T11:53:44Z] [SESSION-22] RECOVERY [task-001] action="mark_completed_from_decomposed_subtasks" reason="task-002..task-006 all completed; umbrella task retained only for audit trace" -[2026-04-15T11:53:44Z] [SESSION-22] RECOVERY [task-007] action="mark_completed_from_decomposed_subtasks" reason="task-008..task-015 all completed; umbrella task retained only for audit trace" -[2026-04-15T11:53:44Z] [SESSION-22] RECOVERY [task-022] action="mark_completed_from_replacement_tasks" reason="task-023/task-024 completed after ONLYOFFICE upgrade and reconnect flow" -[2026-04-15T11:53:44Z] [SESSION-22] STATS tasks_total=44 completed=44 failed=0 pending=0 blocked=0 attempts_total=42 checkpoints=91 -[2026-04-15T11:53:44Z] [SESSION-22] LOCK released -[2026-04-15T13:15:38Z] [SESSION-23] LOCK acquired (pid=manual-task045) -[2026-04-15T13:15:38Z] [SESSION-23] CHECKPOINT [task-045] step=1/3 "已把第一批 TS_LEGACY_DELETE 的真实边界写实:当前不可直接批量删除 route 文件,必须先清 adapter 内 TS 真执行,并去掉 mindmap-trash/empty 的伪 Rust 包装。" -[2026-04-15T13:15:38Z] [SESSION-23] Completed [task-045] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-15T13:15:38Z] [SESSION-23] STATS tasks_total=53 completed=45 failed=0 pending=8 blocked=0 attempts_total=43 checkpoints=119 -[2026-04-15T13:15:38Z] [SESSION-23] LOCK released -[2026-04-15T13:57:26Z] [SESSION-24] LOCK acquired (pid=3830030) -[2026-04-15T13:57:26Z] [SESSION-24] INIT Environment health check: PASS -[2026-04-15T13:57:26Z] [SESSION-24] Starting [task-046] 执行最终收尾 F2-A:清理页面系统残留的“Rust 包装 + TS 真执行”双路径,把 create-child/embed/empty-trash/purge/template 从 adapter 内直连 Convex mutation 推进到真正 Rust owner 或明确 transport 壳 (base=4fbab6dd) -[2026-04-15T13:57:26Z] [SESSION-24] CHECKPOINT [task-046] step=1/3 "已盘点 page-command-adapter 真实残留:create-child/template/empty-trash/purge 为假 cutover,embed 仍保留 TS 内容编排。" -[2026-04-15T13:57:26Z] [SESSION-24] CHECKPOINT [task-046] step=2/3 "已将 create-child 切到 documents.create,template/empty-trash/purge 切到统一 Rust runtime + transport,embed 改走 documents.save,不再直连 Convex mutation。" -[2026-04-15T13:57:26Z] [SESSION-24] CHECKPOINT [task-046] step=3/3 "已更新 cutover/legacy-delete/release-audit 文档,并通过 task-046 指定 eslint、route-adapters 测试、bridge-runtime/storage-convex-bridge cargo test。" -[2026-04-15T13:57:26Z] [SESSION-24] Completed [task-046] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-15T15:43:04Z] [SESSION-25] LOCK acquired (pid=manual-session25) -[2026-04-15T15:43:04Z] [SESSION-25] INIT Environment health check: PASS -[2026-04-15T15:43:04Z] [SESSION-25] Starting [task-047] 执行最终收尾 F2-B:清理块系统与 Mindmap 残留双路径,重点处理 block-command-adapter 与 mindmap-trash/empty 中“先走 Rust 再直接 Convex mutation”的假完成状态 (base=4fbab6dd) -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-047] step=1/3 "已确认现网 /api/blocks/* 主链统一走 src/lib/blocks/block-command-adapter.ts,src/lib/documents/block-command-adapter.ts 仅作为遗留兼容 helper 冻结。" -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-047] step=2/3 "已把 mindmap-trash/empty 与 mindmap 删除/恢复/清空回收站路径收口到 Rust runtime/transport,不再由 route 直接调用 Convex mutation。" -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-047] step=3/3 "已通过 task-047 指定 eslint 与 bridge-runtime/storage-convex-bridge cargo test,并同步 release audit/cutover 文档口径。" -[2026-04-15T15:43:04Z] [SESSION-25] Completed [task-047] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-050] step=1/3 "asset_to_mindmap 的实际导图写入已切到 Rust mindmap_apply_ops,outline-to-mindmap 的 Rust 结果读取接缝已修正。" -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-050] step=2/3 "已把 docsServerTools/onlyofficeServerTools/ai-agent-run 的当前边界写实:docs_* 仍是 TS 兼容查询层,asset_extract_outline 仍依赖 TS/MinerU。" -[2026-04-15T15:43:04Z] [SESSION-25] WARN [task-050] 保持 pending:docs_search/docs_read 仍缺少 Rust runtime,AI 工具链运行态证据未闭合 -[2026-04-15T15:43:04Z] [SESSION-25] Starting [task-051] 执行最终收尾 F5-A:做文档页、Sidebar 与主写链的发布前运行态复核,确认 legacy delete 与双路径收缩后页面编辑、保存、回查、刷新未退化 (base=4fbab6dd) -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-051] step=1/3 "已复跑 scripts/task019-document-ui-regression.js,确认文档页标题、正文、保存、刷新与回查链通过。" -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-051] step=2/3 "已通过 blocknote-editor、documents/save、sidebar route 定向 eslint;仅剩仓库既有 warnings。" -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-051] step=3/3 "已把文档页运行态结果同步到 release audit 与 final closure checklist,并移除对 CLI 线的无关依赖。" -[2026-04-15T15:43:04Z] [SESSION-25] Completed [task-051] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-048] step=1/3 "已直接核实 mnote-cli 当前能编译并输出稳定 JSON,但 page get / block insert 仍只返回 transport plan,而不是真执行结果。" -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-052] step=1/3 "已复跑 scripts/task021-mindmap-ui-regression.js,确认新增/删除节点、保存链、刷新回查与 requestId/traceId 元信息同步通过。" -[2026-04-15T15:43:04Z] [SESSION-25] CHECKPOINT [task-052] step=2/3 "已复跑 scripts/task022-onlyoffice-ui-regression.js,确认打开、插件桥、callback、forcesave 与 storage_id 变化通过;定向 eslint 无 error。" -[2026-04-15T15:43:04Z] [SESSION-25] WARN [task-052] 保持 pending:AI 工具链仍缺独立运行态通过证据,需等待 task-050 继续收口 -[2026-04-15T15:43:04Z] [SESSION-25] STATS tasks_total=58 completed=48 failed=0 pending=10 blocked=0 attempts_total=46 checkpoints=145 -[2026-04-15T15:43:04Z] [SESSION-25] LOCK released -[2026-04-16T02:29:29Z] [SESSION-26] LOCK acquired (pid=manual-session26) -[2026-04-16T02:29:30Z] [SESSION-26] INIT Environment health check: PASS -[2026-04-16T02:29:31Z] [SESSION-26] Starting [task-048] 执行最终收尾 F3-A:把 mnote-cli 从“计划输出/协议冻结器”推进到页面与块系统的最小真实执行 CLI,至少覆盖 page get/title/save/create/delete/move 与 block insert/patch/move/embed (base=4fbab6dd) -[2026-04-16T02:29:32Z] [SESSION-26] CHECKPOINT [task-048] step=1/3 "已修复 mnote-cli 的 execute_block_patch 块树回写形状问题,block move --execute 不再因 snapshot 损坏失败。" -[2026-04-16T02:29:33Z] [SESSION-26] CHECKPOINT [task-048] step=2/3 "已扩展 rust/scripts/task049-cli-smoke.sh,覆盖 page get/create/title/save/move/delete/restore 与 block insert/patch/move/embed,并实跑通过。" -[2026-04-16T02:29:34Z] [SESSION-26] CHECKPOINT [task-048] step=3/3 "已通过 cargo test -p mnote-cli;页面与块系统 CLI 已进入最小真实执行态。" -[2026-04-16T02:29:35Z] [SESSION-26] Completed [task-048] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T02:29:36Z] [SESSION-26] Starting [task-049] 执行最终收尾 F3-B:继续把 search/sidebar/mindmap/tool 主链推进到可真实执行 CLI,并补齐 rust/scripts、tests、fixtures 的最小 CLI 验收骨架 (base=4fbab6dd) -[2026-04-16T02:29:37Z] [SESSION-26] CHECKPOINT [task-049] step=1/3 "已把 search documents、sidebar dataset、tool run docs_search/docs_read 与 mindmap put/get/op 纳入仓库内 smoke。" -[2026-04-16T02:29:38Z] [SESSION-26] CHECKPOINT [task-049] step=2/3 "已更新 rust/tests/README.md,记录 /tmp/task049-cli-smoke-* 输出与 mindmap op 的 result-only 验收边界。" -[2026-04-16T02:29:39Z] [SESSION-26] CHECKPOINT [task-049] step=3/3 "已通过 cargo test -p bridge-runtime 与新版 task049-cli-smoke.sh;CLI 主链验收骨架闭合。" -[2026-04-16T02:29:40Z] [SESSION-26] Completed [task-049] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T02:29:41Z] [SESSION-26] Starting [task-050] 执行最终收尾 F4:继续收掉 AI 工具矩阵中的 TS_COMPAT_PENDING,优先处理 docs_search/docs_read/asset_to_mindmap,并把 remaining builtins 全部归类为 Rust owner 或 TS_TRANSPORT_KEEP (base=4fbab6dd) -[2026-04-16T02:29:42Z] [SESSION-26] CHECKPOINT [task-050] step=1/3 "已核实 Convex 主链下 ai-agent/run 通过 executeRustBridgeTool(...) 执行 docs_search/docs_read,docsServerTools 仅保留 fallback。" -[2026-04-16T02:29:43Z] [SESSION-26] CHECKPOINT [task-050] step=2/3 "已将 AI 工具矩阵回写为 docs_search/docs_read=RUST_OWNER,asset_extract_outline/asset_to_mindmap/oo_*=TS_TRANSPORT_KEEP,builtins 主链不再保留 TS_COMPAT_PENDING。" -[2026-04-16T02:29:44Z] [SESSION-26] CHECKPOINT [task-050] step=3/3 "已通过 bridge-runtime 测试、runAgent.test.ts 与 task-050 关联 eslint;AI 工具矩阵收口完成。" -[2026-04-16T02:29:45Z] [SESSION-26] Completed [task-050] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T02:29:46Z] [SESSION-26] Starting [task-052] 执行最终收尾 F5-B:做 Mindmap、OnlyOffice 与 AI 工具链的发布前运行态复核,确认对象域与工具域在最终收尾后仍可稳定工作 (base=4fbab6dd) -[2026-04-16T02:29:47Z] [SESSION-26] CHECKPOINT [task-052] step=3/3 "已通过 runAgent.test.ts 补齐 docs_search -> docs_read 的 AI runtime 证据;结合 task021/task022 浏览器回归,Mindmap/OnlyOffice/AI 工具链运行态闭合。" -[2026-04-16T02:29:48Z] [SESSION-26] Completed [task-052] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T02:29:49Z] [SESSION-26] Starting [task-053] 执行最终收尾 F6:统一最终宣布口径,确保 cutover/release-audit/final-closure-checklist/harness 任务状态与真实代码完全一致,并给出可对外使用的最终完成结论 (base=4fbab6dd) -[2026-04-16T02:29:50Z] [SESSION-26] CHECKPOINT [task-053] step=1/3 "已回写 ai-tool-cutover-matrix、release-readiness-source-audit、rust-kernel-final-closure-checklist、rust-kernel-cutover-v1 到同一最终口径。" -[2026-04-16T02:29:51Z] [SESSION-26] CHECKPOINT [task-053] step=2/3 "已把 task-048/049/050/052/053 的验证命令更新为当前真实 smoke/test,并同步完成状态。" -[2026-04-16T02:29:52Z] [SESSION-26] CHECKPOINT [task-053] step=3/3 "统一结论:Rust 已成为 mnote 的主业务执行平面,TS 仅保留 transport、外部服务编排、客户端桥与冻结兼容壳。" -[2026-04-16T02:29:53Z] [SESSION-26] Completed [task-053] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T02:29:54Z] [SESSION-26] STATS tasks_total=58 completed=53 failed=0 pending=5 blocked=0 attempts_total=51 checkpoints=158 -[2026-04-16T02:29:55Z] [SESSION-26] LOCK released -[2026-04-16T04:51:54Z] [SESSION-27] LOCK acquired (pid=manual-session27) -[2026-04-16T04:51:55Z] [SESSION-27] INIT Environment health check: PASS -[2026-04-16T04:51:56Z] [SESSION-27] Starting [task-054] 按 AI 前端精简方案 v1 执行第一阶段:启用并验证本机 Hermes API Server,把 /api/ai-agent/run 从前端 AI 编排器收口为 Hermes bridge,停止在 Next route 内继续扩展 tool registry、builtin toolset 和 create*ServerTools 编排逻辑 (base=4fbab6dd) -[2026-04-16T04:51:57Z] [SESSION-27] CHECKPOINT [task-054] step=1/3 "已启用并验证 Hermes API Server:/health、/v1/models 与 /v1/runs 最小链路均通过,run_fc6786126fe645b8af6aa65d1c8d4627 返回‘你好’。" -[2026-04-16T04:51:58Z] [SESSION-27] CHECKPOINT [task-054] step=2/3 "已把 /api/ai-agent/run 收口为 Hermes bridge,route 仅保留鉴权、上下文规范化、Hermes 转发、SSE 回流与 Codex 兼容桥。" -[2026-04-16T04:51:59Z] [SESSION-27] CHECKPOINT [task-054] step=3/3 "已冻结前端 registry/builtins/runAgent 为遗留兼容,run route 不再导入 createToolRegistry、builtinToolSets 或 create*ServerTools。" -[2026-04-16T04:52:00Z] [SESSION-27] Completed [task-054] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T04:52:01Z] [SESSION-27] Starting [task-058] 按 AI 前端精简方案 v1 补齐 Hermes 与 mnote Rust 的业务能力桥:明确并落地 Hermes 调用 mnote 文档/导图/OnlyOffice Rust 能力的最小边界,优先评估 MCP server、Hermes plugin/tool adapter 或最小业务 bridge (base=4fbab6dd) -[2026-04-16T04:52:02Z] [SESSION-27] CHECKPOINT [task-058] step=1/3 "已明确 Hermes 不复制 mnote 业务真逻辑,mnote Rust 继续作为文档、块、导图与 OnlyOffice 的真执行面。" -[2026-04-16T04:52:03Z] [SESSION-27] CHECKPOINT [task-058] step=2/3 "已在 AI 前端精简方案中固定首选接缝:优先最小业务 bridge,并记录 MCP server 与 Hermes plugin/tool adapter 的评估边界。" -[2026-04-16T04:52:04Z] [SESSION-27] CHECKPOINT [task-058] step=3/3 "已补齐 Hermes API Server -> mnote Rust 的最小业务桥边界,避免 /api/ai-agent/run 收口后出现业务能力真空。" -[2026-04-16T04:52:05Z] [SESSION-27] Completed [task-058] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T04:52:06Z] [SESSION-27] Starting [task-055] 按 AI 前端精简方案 v1 执行第二阶段:把全局 AI、页面 AI、Mindmap AI、OnlyOffice AI 收口为一个通用 AiBridgePanel,加上少量 context adapter,删除四套重复状态机 (base=4fbab6dd) -[2026-04-16T04:52:07Z] [SESSION-27] CHECKPOINT [task-055] step=1/3 "已提炼 AiBridgePanel 与 panelShared,统一面板壳、SSE 解析和 provider/model/maxSteps 偏好读写。" -[2026-04-16T04:52:08Z] [SESSION-27] CHECKPOINT [task-055] step=2/3 "已让 global/document/mindmap/onlyoffice 四类场景接入 AiBridgePanel,并保留各自的 context adapter 与内部业务流。" -[2026-04-16T04:52:09Z] [SESSION-27] CHECKPOINT [task-055] step=3/3 "已删除 Mindmap 隐藏旧壳并收缩重复 helper,四套面板不再各自维护独立的壳与基础状态读写。" -[2026-04-16T04:52:10Z] [SESSION-27] Completed [task-055] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T04:52:11Z] [SESSION-27] Starting [task-056] 按 AI 前端精简方案 v1 执行第三阶段:弱化或下线 GlobalAiAgentHost,把全局 AI 从主布局常驻入口降级为实验入口或可选入口,保留页面 AI 作为主场景 (base=4fbab6dd) -[2026-04-16T04:52:12Z] [SESSION-27] CHECKPOINT [task-056] step=1/3 "已从 app layout 移除 GlobalAiAgentHost 常驻挂载,主布局不再继续保留全局 AI 壳。" -[2026-04-16T04:52:13Z] [SESSION-27] CHECKPOINT [task-056] step=2/3 "已保留页面 AI 作为 breadcrumb 主入口,全局 AI 仅保留为实验入口组件,不再影响主场景布局。" -[2026-04-16T04:52:14Z] [SESSION-27] CHECKPOINT [task-056] step=3/3 "已同步方案文档与当前代码口径:页面 AI 为主场景,全局 AI 退为实验/开发入口。" -[2026-04-16T04:52:15Z] [SESSION-27] Completed [task-056] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T04:52:16Z] [SESSION-27] Starting [task-057] 按 AI 前端精简方案 v1 执行第四阶段:清理前端 AI 遗留 registry、builtins、重复 localStorage/session 管理与历史兼容 helper,仅保留 Hermes bridge、mnote Rust 业务执行面与浏览器专属 client capability bridge (base=4fbab6dd) -[2026-04-16T04:52:17Z] [SESSION-27] CHECKPOINT [task-057] step=1/3 "已确认 /api/ai-agent/run 主链不再依赖 registry/builtins/runAgent,旧文件全部标记为遗留兼容。" -[2026-04-16T04:52:18Z] [SESSION-27] CHECKPOINT [task-057] step=2/3 "已保留 OnlyOffice 插件桥与 /api/ai-agent/client-tool-result,前端只继续承担浏览器专属 client capability bridge。" -[2026-04-16T04:52:19Z] [SESSION-27] CHECKPOINT [task-057] step=3/3 "已通过 task-057 定向 eslint,前端 AI 收口为 Hermes bridge + mnote Rust 执行面 + 浏览器专属 client bridge。" -[2026-04-16T04:52:20Z] [SESSION-27] Completed [task-057] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T04:52:21Z] [SESSION-27] INFO Removed .harness-active after task-054..058 completion; all harness tasks are now completed -[2026-04-16T04:52:22Z] [SESSION-27] STATS tasks_total=58 completed=58 failed=0 pending=0 blocked=0 attempts_total=56 checkpoints=173 -[2026-04-16T04:52:23Z] [SESSION-27] LOCK released -[2026-04-16T07:14:55Z] [SESSION-28] LOCK acquired (pid=1066951) -[2026-04-16T07:14:55Z] [SESSION-28] INIT Environment health check: PASS -[2026-04-16T07:25:39Z] [SESSION-28] CHECKPOINT [task-059] step=1/1 "已补长期路线阶段边界、依赖顺序、并行规则与阶段输出输入。" -[2026-04-16T07:25:39Z] [SESSION-28] Completed [task-059] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T07:25:39Z] [SESSION-28] CHECKPOINT [task-060] step=1/1 "已补 Phase 0 重模块审计表、性能基线、阅读态/编辑态边界与 islands 候选清单。" -[2026-04-16T07:25:39Z] [SESSION-28] Completed [task-060] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T07:25:39Z] [SESSION-28] CHECKPOINT [task-061] step=1/1 "已补横向能力 H1 的观测、缓存预取、权限边界与开发规范最低交付物。" -[2026-04-16T07:25:39Z] [SESSION-28] Completed [task-061] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T07:36:07Z] [SESSION-28] CHECKPOINT [task-062] step=1/1 "已新增 mnote-web axum 承载层骨架,统一 request/trace/auth/workspace context,并补 SSE/WS/Hermes/compat 占位路由。" -[2026-04-16T07:36:07Z] [SESSION-28] Completed [task-062] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T07:41:38Z] [SESSION-28] CHECKPOINT [task-067] step=1/1 "已让独立导图页走 StandaloneMindmapView,并把内嵌导图收口为轻预览/轻交互卡片。" -[2026-04-16T07:41:38Z] [SESSION-28] Completed [task-067] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T08:01:56Z] [SESSION-28] CHECKPOINT [task-063] step=1/1 "已将文档页改造成 server-first 阅读入口:服务端预取 meta+content,去掉 mounted gating,阅读态默认不再依赖 BlockNote。" -[2026-04-16T08:01:56Z] [SESSION-28] Completed [task-063] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T08:01:56Z] [SESSION-28] CHECKPOINT [task-064] step=1/1 "已确认 Sidebar/页面树/文件树具备服务端首包 + 共享 Rust query 契约 + 局部 island 的最小落账证据,并通过定向测试与 eslint。" -[2026-04-16T08:01:56Z] [SESSION-28] Completed [task-064] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T08:01:56Z] [SESSION-28] CHECKPOINT [task-065] step=1/1 "已将 SearchPalette 收口为轻量 host + 按需 runtime island,修复自引用懒加载问题,主布局只保留轻接线。" -[2026-04-16T08:01:56Z] [SESSION-28] Completed [task-065] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T08:01:56Z] [SESSION-28] CHECKPOINT [task-066] step=1/1 "已将文档页/导图/OnlyOffice AI 面板收口为轻 host + runtime island,Hermes bridge 与 client action 主链保持不变。" -[2026-04-16T08:01:56Z] [SESSION-28] Completed [task-066] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T08:01:56Z] [SESSION-28] CHECKPOINT [task-068] step=1/1 "已完成文档阅读态/编辑态分离与 BlockNote 孤岛化:默认阅读、显式进入编辑、grace unmount 与外围初始化链外移。" -[2026-04-16T08:01:56Z] [SESSION-28] Completed [task-068] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T08:01:56Z] [SESSION-28] CHECKPOINT [task-069] step=1/1 "已统一回写长期 architecture/checklist 与 harness 状态,明确当前旧前端壳保留边界、实验入口边界与下一步 Phase 8 收缩口径。" -[2026-04-16T08:01:56Z] [SESSION-28] Completed [task-069] (commit skipped by repo rule; base=4fbab6dd) -[2026-04-16T08:01:56Z] [SESSION-28] STATS tasks_total=69 completed=69 failed=0 pending=0 blocked=0 attempts_total=62 checkpoints=179 -[2026-04-16T08:01:56Z] [SESSION-28] INFO Removed .harness-active after task-063..069 completion; all harness tasks are now completed -[2026-04-16T08:06:20Z] [SESSION-28] LOCK released -[2026-04-16T09:06:15Z] [SESSION-29] LOCK acquired (pid=manual-session29) -[2026-04-16T09:06:15Z] [SESSION-29] INIT Environment health check: PASS -[2026-04-16T09:06:15Z] [SESSION-29] INIT Added task-070..073 for tree-first graph kernel checklist execution -[2026-04-16T09:30:55Z] [SESSION-29] CHECKPOINT [task-070] step=1/1 "已在 tree-first-graph-kernel-v1.md 冻结 node/edge/projection/subtree 等术语,并明确事实源/投影/编辑器/外挂四层边界与并入策略。" -[2026-04-16T09:30:55Z] [SESSION-29] Completed [task-070] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T09:30:55Z] [SESSION-29] CHECKPOINT [task-071] step=1/1 "已在 core-protocol 新增 kernel.rs,落地 KernelNode/KernelEdge/KernelProjectionRequest/KernelSubtreeRef/KernelAuditStamp 等统一类型。" -[2026-04-16T09:30:55Z] [SESSION-29] Completed [task-071] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T09:30:55Z] [SESSION-29] CHECKPOINT [task-072] step=1/1 "已在 bridge-runtime 接入 kernel.node.get/kernel.subtree.get/kernel.children.list/kernel.edges.list/kernel.graph.traverse/kernel.project_view 与对应写协议,并让 sidebar dataset 可投成统一 kernel subtree/projection 结果。" -[2026-04-16T09:30:55Z] [SESSION-29] Completed [task-072] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T09:30:55Z] [SESSION-29] CHECKPOINT [task-073] step=1/1 "已在 mnote-web 新增 /api/kernel/projections/sidebar、/api/kernel/subtree、/api/kernel/edges、/api/kernel/graph,并补路由测试与 checklist 回写。" -[2026-04-16T09:30:55Z] [SESSION-29] Completed [task-073] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T09:30:55Z] [SESSION-29] STATS tasks_total=73 completed=73 failed=0 pending=0 blocked=0 attempts_total=71 checkpoints=173 -[2026-04-16T09:30:55Z] [SESSION-29] INFO Removed .harness-active after task-070..073 completion; current harness batch completed -[2026-04-16T09:30:55Z] [SESSION-29] LOCK released -[2026-04-16T11:09:41Z] [SESSION-31] LOCK acquired (pid=manual-session31) -[2026-04-16T11:09:41Z] [SESSION-31] WARN structured state ahead of progress log; harness-tasks.json shows session_count=30/last_session=2026-04-16T10:28:02Z but progress log has no SESSION-30, proceeding conservatively with SESSION-31 -[2026-04-16T11:09:41Z] [SESSION-31] INIT Environment health check: PASS -[2026-04-16T11:09:41Z] [SESSION-31] Starting [task-074] 按 Tree-First Graph 内核实施清单 v2 收尾 Kernel Phase 3:替换 mnote-web kernel route 中的 demo dataset,接入真实 workspace/storage/bridge 数据源,并让至少一条非 demo kernel 查询主链稳定运行 (base=2c643e5f) -[2026-04-16T11:52:00Z] [SESSION-31] CHECKPOINT [task-074] step=1/1 "已删除 mnote-web kernel route 的 demo_sidebar_dataset,改为真实 sidebar.dataset.list -> kernel projection/subtree 归一化主链,并通过 cargo test -p mnote-web。" -[2026-04-16T11:52:00Z] [SESSION-31] Completed [task-074] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T11:52:00Z] [SESSION-31] Starting [task-075] 执行 Tree-First Graph Kernel Phase 4:让 Sidebar、页面树、文件树直接消费 kernel projection,移除主路径上的旧拼树逻辑,并把树交互收口为局部 UI 状态 (base=2c643e5f) -[2026-04-16T11:52:00Z] [SESSION-31] CHECKPOINT [task-075] step=1/1 "已让主 Sidebar、页面树与文件树切到 kernelSidebarProjection/kernelSidebarTree,主路径不再从 documents 调 buildDocumentTree,并通过 sidebar/file-tree 定向测试与 eslint(仅剩 warnings,无 error)。" -[2026-04-16T11:52:00Z] [SESSION-31] Completed [task-075] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T11:52:00Z] [SESSION-31] STATS tasks_total=80 completed=75 failed=0 pending=5 blocked=0 attempts_total=73 checkpoints=175 -[2026-04-16T13:18:24Z] [SESSION-31] WARN continuing SESSION-31 after interim STATS to sync task-076..080 completion and final checklist status -[2026-04-16T13:18:25Z] [SESSION-31] Starting [task-076] 执行 Tree-First Graph Kernel Phase 5:建立 cron 驱动的结构知识刷新与 kernel-aware 检索主链,把 summary node、ai_note node、index node、reference edge 与 BookMindmap/章节树接入统一索引 (base=2c643e5f) -[2026-04-16T13:18:26Z] [SESSION-31] CHECKPOINT [task-076] step=1/1 "已确认 cron 驱动的 kernel_aware_refresh 过渡链与搜索 nodeId/subtreeRootId/evidence 结果形状落地,并把 Kernel Phase 5 文档回写为 PARTIAL;cargo check 与定向 eslint 通过(仅 warnings,无 error)。" -[2026-04-16T13:18:27Z] [SESSION-31] Completed [task-076] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T13:18:28Z] [SESSION-31] Starting [task-077] 执行 Tree-First Graph Kernel Phase 6:把 Mindmap 从重前端对象页收口为 kernel subtree projection/editor,独立页读取 kernel projection,内嵌形态降为轻预览或轻编辑入口 (base=2c643e5f) -[2026-04-16T13:18:29Z] [SESSION-31] CHECKPOINT [task-077] step=1/1 "已确认独立导图页服务端 initial projection、StandaloneMindmapView 与 preview-first 内嵌入口已落地,并回写 Phase 6 文档;定向 eslint 通过(仅 warnings,无 error)。" -[2026-04-16T13:18:30Z] [SESSION-31] Completed [task-077] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T13:18:31Z] [SESSION-31] Starting [task-078] 执行 Tree-First Graph Kernel Phase 7:让文档阅读页与 AI 面板直接消费 page subtree projection 和 kernel-aware 检索结果,统一 node/subtree/edge 操作协议 (base=2c643e5f) -[2026-04-16T13:18:32Z] [SESSION-31] CHECKPOINT [task-078] step=1/1 "已确认 pageSubtree 阅读链与 AI node/subtree/outline/evidence Hermes 上下文链落地,并把 Kernel Phase 7 文档维持为 PARTIAL;定向 eslint 通过(仅 warnings,无 error)。" -[2026-04-16T13:18:33Z] [SESSION-31] Completed [task-078] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T13:18:34Z] [SESSION-31] Starting [task-079] 执行 Tree-First Graph Kernel Phase 8:定义 page subtree 与 content node 的边界,让 BlockNote 退化为内容节点编辑挂件,并继续拆走 DocumentContent 上的外围 panel/drawer (base=2c643e5f) -[2026-04-16T13:18:35Z] [SESSION-31] CHECKPOINT [task-079] step=1/1 "已回写 Phase 8 为 PARTIAL,明确 pageSubtree/content node 边界、阅读态先于编辑态与 BlockNote 按需挂载现状;定向 eslint 通过(仅 warnings,无 error)。" -[2026-04-16T13:18:36Z] [SESSION-31] Completed [task-079] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T13:18:37Z] [SESSION-31] Starting [task-080] 执行 Tree-First Graph Kernel Phase 9:在 kernel projection 成为主路径后收缩旧 Next/React 页面壳与旧对象模型,完成双栈切流、兼容层清理与最终架构口径统一 (base=2c643e5f) -[2026-04-16T13:18:38Z] [SESSION-31] CHECKPOINT [task-080] step=1/1 "已统一回写 tree-first 与 rust-web long-term checklist,并同步 harness 状态,明确 Phase 9 仍 NOT_STARTED、当前不能宣称旧前端壳已下线;文档校验通过。" -[2026-04-16T13:18:39Z] [SESSION-31] Completed [task-080] (commit skipped by repo rule; base=2c643e5f) -[2026-04-16T13:18:40Z] [SESSION-31] STATS tasks_total=80 completed=80 failed=0 pending=0 blocked=0 attempts_total=78 checkpoints=180 -[2026-04-16T13:18:41Z] [SESSION-31] INFO Removed .harness-active after task-076..080 completion; all harness tasks are now completed -[2026-04-16T13:18:42Z] [SESSION-31] LOCK released - -[2026-04-16T15:00:53Z] [SESSION-32] LOCK acquired (pid=manual-session32) -[2026-04-16T15:00:54Z] [SESSION-32] INIT Environment health check: PASS -[2026-04-16T15:00:55Z] [SESSION-32] INIT Added task-081..084 for Tree-First Graph Kernel Phase 3 completion -[2026-04-16T15:00:56Z] [SESSION-32] Starting [task-081] 执行 Kernel Phase 3 P3-1/P3-2:把 mnote-web query transport 从 Sidebar 单点实现提升为通用 Convex query 执行层,并把 fixture fallback 收紧到测试/开发边界 (base=17afde7d) -[2026-04-16T15:00:56Z] [SESSION-32] CHECKPOINT [task-081] step=1/1 "已把 mnote-web transport 收口为通用 execute_convex_query_plan,并以 allow_dev_fixtures + MNOTE_WEB_QUERY_FIXTURES_JSON 将 fixture 限定到测试/开发边界;mnote-web 定向 cargo test/check 通过。" -[2026-04-16T15:00:57Z] [SESSION-32] Completed [task-081] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T15:00:58Z] [SESSION-32] Starting [task-082] 执行 Kernel Phase 3 P3-3:为 mnote-web 新增 bridge.workspace/request/trace 真实查询主链,证明 Rust Web 不再只承接 sidebar 数据集 (base=17afde7d) -[2026-04-16T15:00:59Z] [SESSION-32] CHECKPOINT [task-082] step=1/1 "已新增 /api/bridge/workspace、/api/bridge/request、/api/bridge/trace,并复用通用 query transport 读取真实 bridgeLogs 查询;mnote-web route test 通过。" -[2026-04-16T15:01:00Z] [SESSION-32] Completed [task-082] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T15:01:01Z] [SESSION-32] Starting [task-083] 执行 Kernel Phase 3 P3-4/P3-5:补齐 workspace/request/trace/error 语义,并把 Sidebar 首包与 /api/sidebar 通过 compat sidebar 接到 mnote-web,形成第一条真实切流边界 (base=17afde7d) -[2026-04-16T15:01:02Z] [SESSION-32] CHECKPOINT [task-083] step=1/1 "已统一 workspace 解析与错误码,transport 优先转发真实 Authorization;Sidebar 服务端首包与 /api/sidebar 在配置 MNOTE_WEB_BASE_URL 后默认经 /api/compat/next/sidebar 走 mnote-web。前端定向 vitest / eslint 通过(eslint 仅 baseline-browser-mapping 提示,无 error)。" -[2026-04-16T15:01:03Z] [SESSION-32] Completed [task-083] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T15:01:04Z] [SESSION-32] Starting [task-084] 执行 Kernel Phase 3 P3-6:回写 phase3 拆解/checklist 与 harness 状态,确认 Tree-First Graph Kernel Phase 3 已满足 DONE 判定 (base=17afde7d) -[2026-04-16T15:01:04Z] [SESSION-32] CHECKPOINT [task-084] step=1/1 "已回写 tree-first kernel checklist、phase3 breakdown 与 rust-web long-term checklist,统一 Phase 3 DONE / Rust Web Phase 1 仍 PARTIAL 的真实口径,并同步 harness 完成状态。" -[2026-04-16T15:01:05Z] [SESSION-32] Completed [task-084] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T15:01:06Z] [SESSION-32] STATS tasks_total=84 completed=84 failed=0 pending=0 blocked=0 attempts_total=82 checkpoints=184 -[2026-04-16T15:01:06Z] [SESSION-32] INFO Removed .harness-active after task-081..084 completion; current harness batch completed -[2026-04-16T15:01:07Z] [SESSION-32] LOCK released -[2026-04-16T16:11:17Z] [SESSION-33] LOCK acquired (pid=manual-session33) -[2026-04-16T16:11:18Z] [SESSION-33] INIT Environment health check: PASS -[2026-04-16T16:11:19Z] [SESSION-33] INIT Added task-085..090 for Tree-First Graph Kernel Phase 4 completion -[2026-04-16T16:11:20Z] [SESSION-33] Starting [task-085] 执行 Kernel Phase 4 P4-1:冻结树域 page_tree projection protocol,并补齐 Sidebar / 页面树共享字段与类型边界 (base=17afde7d) -[2026-04-16T16:11:20Z] [SESSION-33] CHECKPOINT [task-085] step=1/1 "已新增 tree-projection.ts,冻结 rowId/nodeId/parentNodeId/projectionKind/depth/position/capabilities/resourceMeta 等 page_tree protocol,并补充对应单测。" -[2026-04-16T16:11:21Z] [SESSION-33] Completed [task-085] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T16:11:22Z] [SESSION-33] Starting [task-086] 执行 Kernel Phase 4 P4-2:统一 Sidebar 主树与页面树 consumer,让 PrivateTree 与相关树列表直接消费 page_tree projection (base=17afde7d) -[2026-04-16T16:11:23Z] [SESSION-33] CHECKPOINT [task-086] step=1/1 "已让 Sidebar 顶部树列表、PrivateTree 与 convert-to-child 等页面树 consumer 统一消费 page_tree projection,可见行只保留 expanded 等 UI 状态。" -[2026-04-16T16:11:24Z] [SESSION-33] Completed [task-086] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T16:11:25Z] [SESSION-33] Starting [task-087] 执行 Kernel Phase 4 P4-3:把文件树收口为统一 projection consumer,让 buildVisibleRows 只负责 projection 到 visible rows 的映射 (base=17afde7d) -[2026-04-16T16:11:26Z] [SESSION-33] CHECKPOINT [task-087] step=1/1 "文件树已改为只消费 page_tree projection + asset 映射;buildVisibleRows 不再递归定义树结构真相,相关 rows 单测通过。" -[2026-04-16T16:11:27Z] [SESSION-33] Completed [task-087] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T16:11:28Z] [SESSION-33] Starting [task-088] 执行 Kernel Phase 4 P4-4:统一 move/embed picker 等兼容树域,结束 documents[] 到 buildDocumentTree 的双轨主路径 (base=17afde7d) -[2026-04-16T16:11:29Z] [SESSION-33] CHECKPOINT [task-088] step=1/1 "move-embed picker 空查询态已直接消费 kernelSidebarTree -> page_tree projection,不再调用 buildDocumentTree(...)。" -[2026-04-16T16:11:30Z] [SESSION-33] Completed [task-088] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T16:11:31Z] [SESSION-33] Starting [task-089] 执行 Kernel Phase 4 P4-5:收敛旧 helper 与兼容逻辑,把 buildDocumentTree 退回兼容边界并收紧 sidebar-data 主路径投影要求 (base=17afde7d) -[2026-04-16T16:11:32Z] [SESSION-33] CHECKPOINT [task-089] step=1/1 "已将 SidebarInitialData 的 kernelSidebarProjection/kernelSidebarTree 收紧为主路径必备字段,并给 buildDocumentTree 标明仅保留兼容 helper 边界。" -[2026-04-16T16:11:33Z] [SESSION-33] Completed [task-089] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T16:11:34Z] [SESSION-33] Starting [task-090] 执行 Kernel Phase 4 P4-6:回写 Phase 4 checklist/breakdown 与 harness 状态,确认树域 consumer/protocol 已满足 DONE 判定并交接下一阶段 Rust Web tree shell 重构 (base=17afde7d) -[2026-04-16T16:11:35Z] [SESSION-33] CHECKPOINT [task-090] step=1/1 "已回写 Phase 4 checklist/breakdown 与 harness 状态,统一 Phase 4 DONE 口径,并明确下一阶段才是 sidebar-pagetree-filetree-rust-web-rebuild-v1.md。" -[2026-04-16T16:11:36Z] [SESSION-33] Completed [task-090] (commit skipped by repo rule; base=17afde7d) -[2026-04-16T16:11:37Z] [SESSION-33] STATS tasks_total=90 completed=90 failed=0 pending=0 blocked=0 attempts_total=88 checkpoints=190 -[2026-04-16T16:11:37Z] [SESSION-33] INFO Removed .harness-active after task-085..090 completion; current harness batch completed -[2026-04-16T16:11:38Z] [SESSION-33] LOCK released - +[2026-04-17T07:58:16Z] [SESSION-0] INIT Archived previous harness state to /mnt/Data1T/mnote/tmp/harness-backups/20260417T075718Z-pre-longterm-reset +[2026-04-17T07:58:16Z] [SESSION-0] INIT Initialized new harness batch from /mnt/Data1T/mnote/design/tree-first-graph-convex-rust-long-term-architecture-v1.md +[2026-04-17T08:02:25Z] [SESSION-1] LOCK acquired (pid=640651) +[2026-04-17T08:02:25Z] [SESSION-1] INIT Environment health check: PASS +[2026-04-17T08:02:25Z] [SESSION-1] Starting [task-001] 执行 Stage A-1:固定首页与 /api/sidebar 主路径只走 Convex/Rust 主链,禁止默认回落到 mnote-web compat/sidebar,并把 tree shell 默认运行态切成显式开关 (base=4538f85d) +[2026-04-17T08:03:15Z] [SESSION-1] CHECKPOINT [task-001] step=1/1 "已验证 /api/sidebar 不再默认走 mnote-web compat,tree shell 默认改为显式关闭,主链边界符合长期架构文档。" +[2026-04-17T08:03:15Z] [SESSION-1] Completed [task-001] (commit skipped by repo rule; base=4538f85d) +[2026-04-17T08:03:15Z] [SESSION-1] Starting [task-002] 执行 Stage A-2:收紧 move/embed picker 的实验壳边界,tree shell 关闭时不再额外请求 viewer/tree-shell 依赖,空查询路径继续只消费稳定 projection 数据 (base=4538f85d) +[2026-04-17T08:06:06Z] [SESSION-1] CHECKPOINT [task-002] step=1/1 "已让 move/embed picker 在 tree shell 默认关闭时直接走稳定 projection fallback,不再额外请求 viewer/tree-shell 依赖。" +[2026-04-17T08:06:06Z] [SESSION-1] Completed [task-002] (commit skipped by repo rule; base=4538f85d) +[2026-04-17T08:06:06Z] [SESSION-1] Starting [task-003] 执行 Stage A-3:新增并跑通 3000 首页入口 smoke,验证在 3104 不可用时首页主链仍可进入 /auth 或已登录文档页,不因实验壳阻塞 (base=4538f85d) +[2026-04-17T08:12:27Z] [SESSION-1] CHECKPOINT [task-003] step=1/1 "已新增 task097 首页入口 smoke,并验证 3104 不可达时 3000 仍可进入 /auth;问题落在 dev 进程健康度,而非首页主链再次依赖实验壳。" +[2026-04-17T08:12:27Z] [SESSION-1] Completed [task-003] (commit skipped by repo rule; base=4538f85d) +[2026-04-17T08:12:27Z] [SESSION-1] Starting [task-004] 执行 Stage B-1:继续收缩前端树域真相泄漏,确认主路径 consumer 只消费 kernel projection,兼容 helper 不再回到运行时主链 (base=4538f85d) +[2026-04-17T08:11:55Z] [SESSION-1] CHECKPOINT [task-004] step=1/1 "已确认 sidebar / picker / host 主路径不再回退到 buildDocumentTree;树域主消费面继续停留在 kernel projection 上。" +[2026-04-17T08:11:55Z] [SESSION-1] Completed [task-004] (commit skipped by repo rule; base=4538f85d) +[2026-04-17T08:11:55Z] [SESSION-1] Starting [task-005] 执行 Stage B-2:梳理 sidebar/page/filetree 的 create/rename/move 命令入口,给出并落第一批 Rust command envelope 收口边界,避免前端继续扩散树语义 (base=4538f85d) +[2026-04-17T08:28:00Z] [SESSION-2] LOCK acquired (pid=resume) +[2026-04-17T08:28:10Z] [SESSION-2] INIT Resumed long-term harness batch after state cleanup; fixed harness-tasks.json quoting corruption and removed stale harness-tasks.json.tmp. +[2026-04-17T08:40:10Z] [SESSION-2] CHECKPOINT [task-005] step=1/2 "已完成 tree command 入口与 Rust envelope 接缝审计,并把 create/rename/move 的前端直连入口收成共享 tree command client。" +[2026-04-17T08:40:10Z] [SESSION-2] Completed [task-005] (commit skipped by repo rule; base=4538f85d) +[2026-04-17T08:42:00Z] [SESSION-2] CHECKPOINT [task-006] step=1/1 "已产出 Rust Web 正式实时树事件流方案文档,明确 Convex substrate 与 Rust semantic owner 的职责边界。" +[2026-04-17T08:42:00Z] [SESSION-2] Completed [task-006] (commit skipped by repo rule; base=4538f85d) +[2026-04-17T08:55:41Z] [SESSION-2] CHECKPOINT [task-007] step=1/2 "已定位 3000 无响应根因为 wolai-frontend/scripts/dev-server.js 的 Convex HTTP 代理未显式结束无请求体转发,导致 /convex/* 与 /auth 挂起。" +[2026-04-17T08:56:10Z] [SESSION-2] CHECKPOINT [task-007] step=2/2 "已修复 dev server 的 Convex HTTP 代理并完成回归验证:/convex/version=200, /auth=200, 首页 smoke 通过。" +[2026-04-17T08:56:30Z] [SESSION-2] Completed [task-007] (commit skipped by repo rule; base=4538f85d) +[2026-04-17T08:56:30Z] [SESSION-2] STATS tasks_total=7 completed=7 failed=0 pending=0 blocked=0 attempts_total=7 checkpoints=8 +[2026-04-17T08:56:30Z] [SESSION-2] LOCK released diff --git a/harness-tasks.json b/harness-tasks.json index 7dcfbb75..c7068316 100644 --- a/harness-tasks.json +++ b/harness-tasks.json @@ -1,6 +1,6 @@ { "version": 2, - "created": "2026-04-14T05:21:15Z", + "created": "2026-04-17T07:58:16Z", "session_config": { "concurrency_mode": "exclusive", "max_tasks_per_session": 20, @@ -9,144 +9,106 @@ "tasks": [ { "id": "task-001", - "title": "请参考/mnt/Data1T/mnote/design/rust-kernel-backport-plan.md完成单仓收口 Rust 内核回迁计划,这是详细checklist/mnt/Data1T/mnote/design/rust-kernel-backport-phase-checklist.md,里面已经完成了部分内容,你需要继续完成", + "title": "执行 Stage A-1:固定首页与 /api/sidebar 主路径只走 Convex/Rust 主链,禁止默认回落到 mnote-web compat/sidebar,并把 tree shell 默认运行态切成显式开关", "status": "completed", - "priority": "P1", + "priority": "P0", "depends_on": [], "attempts": 1, - "max_attempts": 1, - "started_at_commit": "fc26ed21", + "max_attempts": 3, + "started_at_commit": "4538f85d", "validation": { - "command": null, - "timeout_seconds": null + "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm eslint src/app/api/sidebar/route.ts src/lib/runtime-config.ts src/components/sidebar/sidebar.tsx src/components/sidebar/MnoteWebTreeShell.tsx && node - <<'NODE'\nconst fs = require('fs');\nconst route = fs.readFileSync('src/app/api/sidebar/route.ts', 'utf8');\nconst runtime = fs.readFileSync('src/lib/runtime-config.ts', 'utf8');\nconst sidebar = fs.readFileSync('src/components/sidebar/sidebar.tsx', 'utf8');\nconst shell = fs.readFileSync('src/components/sidebar/MnoteWebTreeShell.tsx', 'utf8');\nconst envJson = JSON.parse(fs.readFileSync('public/mnote-env.json', 'utf8'));\nif (/fetchSidebarDatasetFromMnoteWeb|getMnoteWebBaseUrl\\(/.test(route)) throw new Error('route.ts 仍残留 mnote-web compat 主路径');\nif (!runtime.includes('parseRuntimeBoolean(cfg.mnoteWebTreeShellEnabled) ?? false')) throw new Error('runtime-config.ts 未把 tree shell 默认值固定为 false');\nif (!sidebar.includes('mnoteWebTreeShellEnabled === true')) throw new Error('sidebar.tsx 未收紧为显式开关');\nif (!shell.includes('mnoteWebTreeShellEnabled === true')) throw new Error('MnoteWebTreeShell.tsx 未收紧为显式开关');\nif (envJson.mnoteWebTreeShellEnabled !== false) throw new Error('public/mnote-env.json 未默认关闭 tree shell');\nconsole.log('task-001-boundary-ok');\nNODE", + "timeout_seconds": 300 }, "on_failure": { "cleanup": null }, - "error_log": [ - "umbrella 任务已由 task-002..task-006 取代,相关子任务均已完成;原 failed 原因仅为缺少 validation.command。" - ], - "checkpoints": [ - { - "step": 1, - "total": 4, - "description": "完成 Rust 协议层对 documents.meta/content/save 的建模与映射,cargo test/check 已通过", - "timestamp": "2026-04-14T05:40:04Z" - }, - { - "timestamp": "2026-04-15T11:53:44Z", - "step": 2, - "total": 2, - "description": "umbrella 任务已由 task-002..task-006 取代,相关子任务均已完成;原 failed 原因仅为缺少 validation.command。" - } - ], - "completed_at": "2026-04-15T11:53:44Z" + "error_log": [], + "checkpoints": [], + "completed_at": "2026-04-17T08:03:15Z" }, { "id": "task-002", - "title": "补齐 Rust core-protocol/storage-convex-bridge 对 documents.meta/content/save 的协议与映射", + "title": "执行 Stage A-2:收紧 move/embed picker 的实验壳边界,tree shell 关闭时不再额外请求 viewer/tree-shell 依赖,空查询路径继续只消费稳定 projection 数据", "status": "completed", "priority": "P0", - "depends_on": [], + "depends_on": [ + "task-001" + ], "attempts": 1, "max_attempts": 3, - "started_at_commit": "fc26ed21", + "started_at_commit": "4538f85d", "validation": { - "command": "CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p core-protocol && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p storage-convex-bridge && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml", - "timeout_seconds": 900 + "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm eslint src/components/documents/move-embed-picker-dialog.tsx src/components/documents/move-embed-picker-host.tsx && node - <<'NODE'\nconst fs = require('fs');\nconst dialog = fs.readFileSync('src/components/documents/move-embed-picker-dialog.tsx', 'utf8');\nif (!dialog.includes('const treeShellEnabled =')) throw new Error('picker 未显式计算 treeShellEnabled');\nif (!dialog.includes('enabled: Boolean(workspaceId) && isEmptyQuery && treeShellEnabled')) throw new Error('viewerQuery 仍会在 tree shell 关闭时触发');\nconsole.log('task-002-picker-boundary-ok');\nNODE", + "timeout_seconds": 300 }, "on_failure": { "cleanup": null }, "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已补齐 documents.meta.get / documents.content.get / documents.save 的 Rust 协议对象、Convex 映射与单测", - "timestamp": "2026-04-14T05:40:04Z" - } - ], - "completed_at": "2026-04-14T05:40:04Z" + "checkpoints": [], + "completed_at": "2026-04-17T08:06:06Z" }, { "id": "task-003", - "title": "为 blocks.patch 与正文保存补最小 Rust command 协议和 storage-convex-bridge 映射", + "title": "执行 Stage A-3:新增并跑通 3000 首页入口 smoke,验证在 3104 不可用时首页主链仍可进入 /auth 或已登录文档页,不因实验壳阻塞", "status": "completed", "priority": "P0", "depends_on": [ + "task-001", "task-002" ], "attempts": 1, "max_attempts": 3, - "started_at_commit": "fc26ed21", + "started_at_commit": "4538f85d", "validation": { - "command": "CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p storage-convex-bridge && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml", - "timeout_seconds": 900 + "command": "cd /mnt/Data1T/mnote && node scripts/task097-homepage-entry-smoke.js", + "timeout_seconds": 600 }, "on_failure": { "cleanup": null }, "error_log": [], - "checkpoints": [ - { - "step": 2, - "total": 2, - "description": "已通过 CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness 下的 storage-convex-bridge 单测与 workspace cargo check", - "timestamp": "2026-04-14T05:43:54Z" - } - ], - "completed_at": "2026-04-14T05:43:54Z" + "checkpoints": [], + "completed_at": "2026-04-17T08:12:27Z" }, { "id": "task-004", - "title": "为 sidebar.dataset.list 固定 Rust query 契约并对齐 SidebarInitialData 聚合边界", + "title": "执行 Stage B-1:继续收缩前端树域真相泄漏,确认主路径 consumer 只消费 kernel projection,兼容 helper 不再回到运行时主链", "status": "completed", "priority": "P1", "depends_on": [ + "task-001", "task-002" ], "attempts": 1, "max_attempts": 3, - "started_at_commit": "fc26ed21", + "started_at_commit": "4538f85d", "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/sidebar-data.test.ts && pnpm exec eslint src/lib/sidebar-data.ts src/lib/server/sidebar-data.ts src/app/api/sidebar/route.ts src/hooks/use-convex-sidebar-data.ts", - "timeout_seconds": 1200 + "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm eslint src/components/sidebar/sidebar.tsx src/components/documents/move-embed-picker-dialog.tsx src/components/documents/move-embed-picker-host.tsx && node - <<'NODE'\nconst fs = require('fs');\nconst sidebar = fs.readFileSync('src/components/sidebar/sidebar.tsx', 'utf8');\nconst picker = fs.readFileSync('src/components/documents/move-embed-picker-dialog.tsx', 'utf8');\nconst host = fs.readFileSync('src/components/documents/move-embed-picker-host.tsx', 'utf8');\nfor (const [name, source] of [['sidebar', sidebar], ['picker', picker], ['host', host]]) {\n if (source.includes('buildDocumentTree(')) throw new Error(name + ' 仍残留 buildDocumentTree 主路径依赖');\n}\nconsole.log('task-004-tree-truth-mainpath-ok');\nNODE", + "timeout_seconds": 300 }, "on_failure": { "cleanup": null }, "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已为 sidebar.dataset.list 冻结共享 query payload/result 契约,并让 route、server helper、实时 hook 复用同一映射边界", - "timestamp": "2026-04-14T06:10:45Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 pnpm test src/lib/sidebar-data.test.ts 与定向 eslint,Sidebar 聚合边界收口完成", - "timestamp": "2026-04-14T06:11:23Z" - } - ], - "completed_at": "2026-04-14T06:18:30Z" + "checkpoints": [], + "completed_at": "2026-04-17T08:11:55Z" }, { "id": "task-005", - "title": "为 documents.save 固定共享 payload 契约并抽出 save command adapter,收口正文保存前后端边界", + "title": "执行 Stage B-2:梳理 sidebar/page/filetree 的 create/rename/move 命令入口,给出并落第一批 Rust command envelope 收口边界,避免前端继续扩散树语义", "status": "completed", "priority": "P1", "depends_on": [ - "task-003" + "task-004" ], "attempts": 1, "max_attempts": 3, - "started_at_commit": "fc26ed21", + "started_at_commit": "4538f85d", "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/documents/save-contract.test.ts src/lib/documents/bridge.test.ts && pnpm exec eslint src/lib/documents/save-contract.ts src/lib/documents/save-command-adapter.ts src/app/api/documents/save/route.ts src/components/editor/blocknote-editor.tsx src/lib/documents/bridge.test.ts", - "timeout_seconds": 1200 + "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\npath = root / 'design/tree-command-envelope-cutover-stage1-v1.md'\ntext = path.read_text(encoding='utf-8')\nassert 'sidebar.tsx' in text\nassert 'create / rename / move' in text or 'create/rename/move' in text\nprint('task-005-command-cutover-doc-ok')\nPY", + "timeout_seconds": 120 }, "on_failure": { "cleanup": null @@ -156,32 +118,33 @@ { "step": 1, "total": 2, - "description": "已新增 documents.save 共享 payload contract,补齐 revision/conflictDetectionKey 的最小边界并让 BlockNote 保存请求复用", - "timestamp": "2026-04-14T06:15:20Z" + "description": "已审计 sidebar/document-content/CustomSideMenu 与 create/title/move route,确认第一批 tree command 已接入 envelope,但入口与部分树策略仍散落在前端组件层。", + "timestamp": "2026-04-17T08:32:00Z" }, { "step": 2, "total": 2, - "description": "已抽出 save command adapter,并通过 save-contract/bridge 定向测试与 eslint 验证", - "timestamp": "2026-04-14T06:16:40Z" + "description": "已新增共享 tree command client,并产出 design/tree-command-envelope-cutover-stage1-v1.md 固定第一批 cutover 边界与顺序。", + "timestamp": "2026-04-17T08:40:10Z" } ], - "completed_at": "2026-04-14T06:18:30Z" + "completed_at": "2026-04-17T08:40:10Z" }, { "id": "task-006", - "title": "继续补齐 documents.save 的 revision 冲突检测与 BlockNote 快照采集字段,对齐 Rust SavePageContent 最小协议", + "title": "执行 Stage C-1:为 Rust Web 正式实时树事件流补方案与接缝说明,明确 Convex substrate 与 Rust semantic owner 的订阅职责划分", "status": "completed", - "priority": "P1", + "priority": "P2", "depends_on": [ + "task-003", "task-005" ], "attempts": 1, "max_attempts": 3, - "started_at_commit": "fc26ed21", + "started_at_commit": "4538f85d", "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/documents/save-contract.test.ts src/lib/documents/bridge.test.ts && pnpm exec eslint src/lib/documents/save-contract.ts src/lib/documents/save-command-adapter.ts src/app/api/documents/save/route.ts src/components/editor/blocknote-editor.tsx src/lib/documents/bridge.test.ts", - "timeout_seconds": 1200 + "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\npath = root / 'design/rust-web-tree-realtime-event-stream-v1.md'\ntext = path.read_text(encoding='utf-8')\nassert 'Convex substrate' in text or 'Convex' in text\nassert 'Rust semantic owner' in text or 'Rust' in text\nprint('task-006-realtime-doc-ok')\nPY", + "timeout_seconds": 120 }, "on_failure": { "cleanup": null @@ -190,234 +153,19 @@ "checkpoints": [ { "step": 1, - "total": 2, - "description": "已补 documents.content/save 的 revision 与 conflictDetectionKey 链路,并让 BlockNote 保存时携带快照采集元数据", - "timestamp": "2026-04-14T06:42:18Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 save-contract/bridge 定向测试,正文保存冲突会返回稳定 bridge 错误,checklist 已同步到 C3", - "timestamp": "2026-04-14T06:44:13Z" + "total": 1, + "description": "已产出 design/rust-web-tree-realtime-event-stream-v1.md,明确 Convex substrate、Rust semantic owner、Rust Web SSE/WS transport 与前端 projection consumer 的职责划分。", + "timestamp": "2026-04-17T08:42:00Z" } ], - "completed_at": "2026-04-14T06:44:13Z" + "completed_at": "2026-04-17T08:42:00Z" }, { "id": "task-007", - "title": "请完成该checklist /mnt/Data1T/mnote/design/rust-kernel-backport-phase-checklist.md", - "status": "completed", - "priority": "P1", - "depends_on": [], - "attempts": 1, - "max_attempts": 1, - "started_at_commit": "fc26ed21", - "validation": { - "command": null, - "timeout_seconds": null - }, - "on_failure": { - "cleanup": null - }, - "error_log": [ - "umbrella 任务已由 task-008..task-015 取代,相关子任务均已完成;原 failed 原因仅为缺少 validation.command。" - ], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "确认该 checklist 为 umbrella 任务,拆分为单仓口径、Phase D 边界、Sidebar Rust query 目标、发布前源码审计等可验证子任务", - "timestamp": "2026-04-14T07:25:50Z" - }, - { - "timestamp": "2026-04-15T11:53:44Z", - "step": 2, - "total": 2, - "description": "umbrella 任务已由 task-008..task-015 取代,相关子任务均已完成;原 failed 原因仅为缺少 validation.command。" - } - ], - "completed_at": "2026-04-15T11:53:44Z" - }, - { - "id": "task-008", - "title": "统一单仓执行口径并补主仓维护边界文档,清理 ARCHITECTURE 中残留的 mnote-rust 承载叙事", - "status": "completed", - "priority": "P1", - "depends_on": [], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\narch = (root/'ARCHITECTURE.md').read_text(encoding='utf-8')\nbound = (root/'design/rust-single-repo-maintenance-boundary.md').read_text(encoding='utf-8')\nplan = (root/'design/rust-kernel-backport-plan.md').read_text(encoding='utf-8')\ncheck = (root/'design/rust-kernel-backport-phase-checklist.md').read_text(encoding='utf-8')\nassert '如果后续继续把这套能力收口到主仓 Rust 协议层' in arch\nassert '如果后面你要把这套能力继续收口到 `/mnt/Data1T/mnote/rust/`' in arch\nassert '只保留 `/mnt/Data1T/mnote` 作为启动、规划、保存、验证的唯一主仓。' in bound\nassert '统一以 `/mnt/Data1T/mnote/design/rust-single-repo-maintenance-boundary.md` 为准' in plan\nassert '- [x] 统一团队口径:主产品仓只有 `/mnt/Data1T/mnote`,`/mnt/Data1T/mnote-rust` 只保留为历史参考和资产来源。' in check\nprint('task-008-ok')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已新增单仓维护边界文档,并同步 ARCHITECTURE/plan/checklist 的主仓执行口径", - "timestamp": "2026-04-14T07:25:54Z" - } - ], - "completed_at": "2026-04-14T07:25:55Z" - }, - { - "id": "task-009", - "title": "固定 Mindmap 与 OnlyOffice 当前边界,明确历史仓对象页壳与第二套 sidebar 不进入主线", + "title": "执行 Stage C-2:回写新的 harness 批次状态、阶段完成度与长期口径,明确哪些已进入稳定主链,哪些仍属于实验壳/后续阶段", "status": "completed", "priority": "P1", "depends_on": [ - "task-008" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\ndoc = (root/'design/mindmap-onlyoffice-boundary.md').read_text(encoding='utf-8')\ncheck = (root/'design/rust-kernel-backport-phase-checklist.md').read_text(encoding='utf-8')\nassert '`MindmapBlockView` 同时服务文档内嵌块与独立全屏页。' in doc\nassert '/mnt/Data1T/mnote-rust/app/documents/[id]/mindmap/page.tsx' in doc\nassert '- [x] 锁定 Mindmap 当前主组件和嵌入链路,确认 `MindmapBlock.tsx` 同时服务 BlockNote 内嵌块与独立全屏页。' in check\nassert '- [x] `src/components/onlyoffice/` 保持可用且未被误删。' in check\nprint('task-009-ok')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已新增 Mindmap/OnlyOffice 边界说明,并把 Phase D 中可由源码证明的项补到账", - "timestamp": "2026-04-14T07:25:56Z" - } - ], - "completed_at": "2026-04-14T07:25:57Z" - }, - { - "id": "task-010", - "title": "冻结 Sidebar Rust query 目标文档,收口 sidebar.dataset.list 的最小 payload/result 契约", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-008" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\ndoc = (root/'design/sidebar-rust-query-target.md').read_text(encoding='utf-8')\ncheck = (root/'design/rust-kernel-backport-phase-checklist.md').read_text(encoding='utf-8')\nassert '`sidebar.dataset.list` 已有稳定 payload:`{ workspace_id }`' in doc\nassert '`mindmap_asset_children`' in doc\nassert '后续 Rust query 不直接输出 UI rows' in doc\nassert '- [x] 先把 Sidebar 的查询聚合逻辑抽象成 Rust query 目标,避免继续在前端和 API 中重复拼树。' in check\nprint('task-010-ok')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已把 Sidebar Rust query 接入目标、前后端职责边界与禁止事项固定成独立文档", - "timestamp": "2026-04-14T07:25:58Z" - } - ], - "completed_at": "2026-04-14T07:25:59Z" - }, - { - "id": "task-011", - "title": "补主仓回迁资产与发布前源码审计文档,完成单仓启动路径、保存口径、trace 一致性与禁止事项复查", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-008" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\naudit = (root/'design/rust-backport-audit-inventory.md').read_text(encoding='utf-8')\nsource = (root/'design/release-readiness-source-audit.md').read_text(encoding='utf-8')\ncheck = (root/'design/rust-kernel-backport-phase-checklist.md').read_text(encoding='utf-8')\nassert 'rust/crates/storage-convex-bridge/' in audit\nassert 'bridge 的最小查询、最小写入、日志生成、事件生成和错误返回已经形成源码闭环。' in source\nassert '- [x] 为 bridge 入口补集成 smoke,验证最小查询、最小写入、日志生成、事件生成和错误返回。' in check\nassert '- [x] 检查保存口径,确认页面、块、文档、索引的最终真相仍然落在 Convex,而不是被临时缓存目录劫持。' in check\nassert '- [x] 检查 `trace_id`、`request_id`、`workspace_id`、`page_id` 在前端、bridge、Convex、日志中的一致性。' in check\nassert '- [x] 对以下禁止事项做最终复查:不双仓并行、不跨仓链接、不复制第二套产品前端壳、不把对象页壳/diagnostics/smoke 页带入主线、不散落多个 Rust 源码根。' in check\nprint('task-011-ok')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已新增回迁资产清单与发布前源代码审计文档,并同步发布前 checklist 中源码可验证项", - "timestamp": "2026-04-14T07:26:00Z" - } - ], - "completed_at": "2026-04-14T07:26:01Z" - }, - { - "id": "task-012", - "title": "补 documents.save 成功路径 bridge 单测,确认低风险保存模式、identity 口径与回查链闭环", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-005", - "task-006" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/documents/bridge.test.ts", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已补 documents.save 成功路径 bridge 测试,并把 C3 的低风险保存模式、identity 口径与可回查项同步到 checklist", - "timestamp": "2026-04-14T07:26:02Z" - } - ], - "completed_at": "2026-04-14T07:26:03Z" - }, - { - "id": "task-013", - "title": "将至少一条 Sidebar 主查询改为真实 Rust query 供给,并验证文档树/文件树/回收站语义不变", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-010" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/sidebar-data.test.ts src/lib/file-tree/rows.test.ts && pnpm exec eslint src/lib/sidebar-data.ts src/lib/server/sidebar-data.ts src/app/api/sidebar/route.ts src/hooks/use-convex-sidebar-data.ts", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已新增单个 sidebar.datasetList 主查询,并让 server helper / 实时 hook 统一消费该数据集;sidebar-data/file-tree 定向测试、eslint 与 storage-convex-bridge 单测通过", - "timestamp": "2026-04-14T07:56:00Z" - } - ], - "completed_at": "2026-04-14T07:56:00Z" - }, - { - "id": "task-014", - "title": "统一所有新命令和查询先进入 core-protocol envelope,再转给 storage-convex-bridge", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-002", "task-003", "task-004", "task-005", @@ -425,137 +173,9 @@ ], "attempts": 1, "max_attempts": 3, - "started_at_commit": "fc26ed21", + "started_at_commit": "4538f85d", "validation": { - "command": "CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p core-protocol -p storage-convex-bridge && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已确认新增命令/查询统一先进入 core-protocol envelope,并完成 storage-convex-bridge 映射与 Rust 单测/检查闭环", - "timestamp": "2026-04-14T08:14:45Z" - } - ], - "completed_at": "2026-04-14T08:14:45Z", - "claimed_by": "lix-kubuntu:23844", - "claimed_at": "2026-04-14T08:13:25Z", - "lease_expires_at": "2026-04-14T08:45:00Z" - }, - { - "id": "task-015", - "title": "补文档页、Sidebar、BlockNote 保存的端到端 UI 回归,并确认外观与交互未退化", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-012", - "task-013" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm lint && pnpm test", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 3, - "total": 3, - "description": "已完成全量前端回归:pnpm lint 0 error,vitest 76/76 通过,文档页/Sidebar/正文保存链未见退化", - "timestamp": "2026-04-14T08:50:25Z" - } - ], - "completed_at": "2026-04-14T08:50:25Z", - "claimed_by": "lix-kubuntu:23844", - "claimed_at": "2026-04-14T08:14:45Z", - "lease_expires_at": "2026-04-14T09:00:00Z" - }, - { - "id": "task-016", - "title": "统一 Mindmap 与 OnlyOffice 的页面标识、附件标识、页面归属和追踪字段,并补 Mindmap 交互回归", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-009" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": null, - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/editor/blocks/MindmapBlock.tsx src/lib/mindmap/mindmapLocalStore.ts src/lib/mindmap/mindmapOps.ts src/app/api/mindmap/[docId]/route.ts src/app/api/mindmap/[docId]/[mindmapId]/route.ts", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 2, - "total": 2, - "description": "已通过定向 eslint 验证 Mindmap API 与组件侧的统一标识/追踪元信息口径,OnlyOffice 共享口径已同步到设计文档", - "timestamp": "2026-04-14T08:50:25Z" - } - ], - "completed_at": "2026-04-14T08:50:25Z" - }, - { - "id": "task-017", - "title": "补 OnlyOffice 打开、签名、代理、callback、forcesave 与静态资源回归,完成单仓发布前收口标准", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-009", - "task-011" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/onlyoffice/page.tsx src/app/onlyoffice/OnlyOfficeClientPage.tsx src/app/api/onlyoffice/sign/route.ts src/app/api/onlyoffice/proxy/route.ts src/app/api/onlyoffice/callback/route.ts src/app/api/onlyoffice/forcesave/route.ts", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 2, - "total": 2, - "description": "已完成 OnlyOffice 页面/API 定向 eslint,且根目录 onlyoffice-web-apps/onlyoffice-plugins/onlyoffice-data 关键静态资源目录仍在", - "timestamp": "2026-04-14T08:51:34Z" - } - ], - "completed_at": "2026-04-14T08:51:34Z", - "claimed_by": "lix-kubuntu:23844", - "claimed_at": "2026-04-14T08:50:25Z", - "lease_expires_at": "2026-04-14T09:30:00Z" - }, - { - "id": "task-018", - "title": "把 documents.title/stats/options/save 与 sidebar.dataset.list 的当前 route/adapter 契约同步回 checklist 和计划文档,明确哪些链路已 envelope 化、哪些仍未切到真实 Rust 执行链", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-014", - "task-015" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\ncheck = (root/'design/rust-kernel-backport-phase-checklist.md').read_text(encoding='utf-8')\nplan = (root/'design/rust-kernel-backport-plan.md').read_text(encoding='utf-8')\nassert 'documents.title.update' in check and 'documents.options.update' in check and 'documents.save' in check\nassert '尚未切到 `core-protocol -> storage-convex-bridge` 的真实 Rust bridge 执行链' in check\nassert 'Phase B 当前只完成了“bridge 内核骨架已具备”,尚未完成“主仓入口真实接线”' in plan\nprint('task-018-ok')\nPY", + "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\nassert (root / 'harness-tasks.json').exists()\nassert (root / 'harness-progress.txt').exists()\nprint('task-007-harness-sync-ok')\nPY", "timeout_seconds": 120 }, "on_failure": { @@ -566,2708 +186,13 @@ { "step": 1, "total": 1, - "description": "已确认 checklist 与计划文档同步包含 documents.title/stats/options/save、sidebar.dataset.list 的 envelope/真实 Rust bridge 现状说明,无需再补正文", - "timestamp": "2026-04-14T11:20:16Z" + "description": "已回写 harness 状态与进度,并验证修复 dev server 的 Convex HTTP 代理后,3000 首页基线恢复:/auth 返回 200,/ 根路径稳定进入 /auth。", + "timestamp": "2026-04-17T08:56:30Z" } ], - "completed_at": "2026-04-14T11:20:16Z" - }, - { - "id": "task-019", - "title": "为文档页元信息、Sidebar、BlockNote 保存链补真实浏览器端到端回归,完成发布前 UI 验收缺口并把 checklist 对应项打勾", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-015" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm lint && pnpm test && curl -I -sS --max-time 20 http://127.0.0.1:3001/ | head -n 10", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已修复真实浏览器回归前置环境:重新注册 Convex functions,首页从 500 恢复为正常 307 跳转到文档页", - "timestamp": "2026-04-14T11:56:09Z" - }, - { - "step": 2, - "total": 2, - "description": "已新增并跑通 scripts/task019-document-ui-regression.js:真实浏览器验证 Sidebar/页面标题/BlockNote 正文保存与刷新保留,临时页面已自动清理", - "timestamp": "2026-04-14T11:56:09Z" - } - ], - "completed_at": "2026-04-14T11:56:09Z" - }, - { - "id": "task-020", - "title": "把 pages 元信息与正文保存链从当前 TypeScript adapter 再向前推进一小步:至少选一条 documents.title.update 或 documents.save,做真实 Rust bridge 执行样板或最小可运行接缝,避免 route->TS adapter 长期停留在临时态", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-014", - "task-018" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p core-protocol -p storage-convex-bridge && cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/documents/bridge.test.ts && pnpm exec eslint src/app/api/documents/title/route.ts src/app/api/documents/save/route.ts src/lib/documents/metadata-command-adapter.ts src/lib/documents/save-command-adapter.ts src/lib/documents/bridge.ts", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已新增 buildDocumentBridgeMutationRequest 与统一执行器,让 documents.title/stats/options/save 先生成与 storage-convex-bridge 对齐的 functionName/payloadJson/args runtime request", - "timestamp": "2026-04-14T12:43:51Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 task-020 指定的 cargo test、bridge vitest 与定向 eslint,并同步 plan/checklist 说明最小真实 bridge 运行接缝", - "timestamp": "2026-04-14T12:43:51Z" - } - ], - "completed_at": "2026-04-14T12:43:51Z" - }, - { - "id": "task-021", - "title": "补 Mindmap 真实交互回归并同步 checklist,确认当前页面、保存、节点操作与追踪字段口径在浏览器层未退化", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-016" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/editor/blocks/MindmapBlock.tsx src/lib/mindmap/mindmapLocalStore.ts src/lib/mindmap/mindmapOps.ts src/app/api/mindmap/[docId]/route.ts src/app/api/mindmap/[docId]/[mindmapId]/route.ts", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已新增并跑通 scripts/task021-mindmap-ui-regression.js:真实浏览器验证 Mindmap 全屏页元信息、工具栏/大纲侧栏、子节点新增删除与保存刷新链", - "timestamp": "2026-04-14T14:01:55Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 task-021 指定的定向 eslint(0 error,保留仓库既有 warnings),并同步 checklist 中的 Mindmap 浏览器回归结论", - "timestamp": "2026-04-14T14:01:55Z" - } - ], - "completed_at": "2026-04-14T14:01:55Z" - }, - { - "id": "task-022", - "title": "补 OnlyOffice 真实页面打开与回调链回归,确认签名、代理、callback、forcesave 与静态资源在浏览器层可用,并把发布前 checklist 对应项收口", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-017" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/onlyoffice/page.tsx src/app/onlyoffice/OnlyOfficeClientPage.tsx src/app/api/onlyoffice/sign/route.ts src/app/api/onlyoffice/proxy/route.ts src/app/api/onlyoffice/callback/route.ts src/app/api/onlyoffice/forcesave/route.ts && curl -I -sS --max-time 20 http://127.0.0.1:3001/onlyoffice | head -n 10", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [ - "OnlyOffice 回归已由 task-023/task-024 取代并完成;原 failed 原因仅为用户调整顺序,先升级再重连。" - ], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已确认当前 9.2.1 文档服务可打开,但自定义插件桥与真实回归仍未稳定;按用户要求改为先升级 ONLYOFFICE。", - "timestamp": "2026-04-14T18:08:23Z" - }, - { - "timestamp": "2026-04-15T11:53:44Z", - "step": 2, - "total": 2, - "description": "OnlyOffice 回归已由 task-023/task-024 取代并完成;原 failed 原因仅为用户调整顺序,先升级再重连。" - } - ], - "completed_at": "2026-04-15T11:53:44Z" - }, - { - "id": "task-023", - "title": "升级 ONLYOFFICE 文档服务到当前可用稳定版本,并对齐静态资源、WebSocket 代理、自定义插件与 callback/forcesave 基线", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-017" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "curl -I -sS --max-time 20 http://127.0.0.1:8082/web-apps/apps/api/documents/api.js && curl -I -sS --max-time 20 http://127.0.0.1:3001/onlyoffice-server/web-apps/apps/api/documents/api.js && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/onlyoffice/page.tsx src/app/onlyoffice/OnlyOfficeClientPage.tsx src/app/api/onlyoffice/sign/route.ts src/app/api/onlyoffice/proxy/route.ts src/app/api/onlyoffice/callback/route.ts src/app/api/onlyoffice/forcesave/route.ts src/app/onlyoffice-server/[...path]/route.ts src/app/cache/[...path]/route.ts src/lib/onlyoffice/internal-url.ts", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已确认升级目标改为 ONLYOFFICE DocumentServer 9.3.1,并发现当前 8082 容器仍由 mnote-rust/infra/onlyoffice/docker-compose.yml 驱动。", - "timestamp": "2026-04-14T18:20:18Z" - }, - { - "step": 2, - "total": 3, - "description": "已在当前主仓新增 infra/onlyoffice/docker-compose.yml 与 README,并确认 onlyoffice/documentserver:9.3.1 镜像已成功拉取。", - "timestamp": "2026-04-14T18:25:24Z" - }, - { - "step": 3, - "total": 3, - "description": "已用当前主仓 compose 接管 8082,容器升级到 onlyoffice/documentserver:9.3.1,且 8082 与 3001 的 api.js 探测均返回 200。", - "timestamp": "2026-04-14T18:40:31Z" - } - ], - "completed_at": "2026-04-14T18:40:31Z" - }, - { - "id": "task-024", - "title": "在升级后的 ONLYOFFICE 基线上,重新连接页面打开、签名、代理、callback、forcesave、自定义插件桥与 Rust 内核接缝,并完成真实浏览器回归与 checklist 收口", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-023" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "node /mnt/Data1T/mnote/scripts/task022-onlyoffice-ui-regression.js && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/onlyoffice/page.tsx src/app/onlyoffice/OnlyOfficeClientPage.tsx src/app/api/onlyoffice/sign/route.ts src/app/api/onlyoffice/proxy/route.ts src/app/api/onlyoffice/callback/route.ts src/app/api/onlyoffice/forcesave/route.ts src/app/onlyoffice-server/[...path]/route.ts src/app/cache/[...path]/route.ts src/lib/onlyoffice/internal-url.ts", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已修复 /onlyoffice/plugins/* 放行与 agent-tools 非可视插件配置,升级后 scripts/task022-onlyoffice-ui-regression.js 真实浏览器回归通过,签名/代理/callback/forcesave/插件桥闭环正常。", - "timestamp": "2026-04-14T18:40:31Z" - } - ], - "completed_at": "2026-04-14T18:40:31Z" - }, - { - "id": "task-025", - "title": "把 OnlyOffice callback 附件写回链接到统一 bridge/Rust 协议边界:补最小 media asset writeback command、storage-convex-bridge 映射与 Next route 执行器,替换 callback 里的直连 Convex mutation", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-024" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "fc26ed21", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p core-protocol -p storage-convex-bridge && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/onlyoffice/callback/route.ts src/lib/documents/bridge.ts && node /mnt/Data1T/mnote/scripts/task022-onlyoffice-ui-regression.js", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已在升级后的 9.3.1 基线下继续推进 Rust 接缝,当前锁定 callback 附件写回链,因为这条链仍直接调用 Convex mediaAssets mutation。", - "timestamp": "2026-04-14T18:43:06Z" - }, - { - "step": 2, - "total": 3, - "description": "已补 media.assets.replace_storage 最小命令、ReplaceMediaAssetStorage 协议对象、storage-convex-bridge 映射,以及 OnlyOffice callback 的显式 actor bridge context 与写回执行器。", - "timestamp": "2026-04-14T20:07:16Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 Rust 单测、bridge/callback route 定向测试、定向 eslint 与真实 scripts/task022-onlyoffice-ui-regression.js;期间通过重新注册 Convex functions 修复了 auth:signIn 缺失导致的登录页卡死。", - "timestamp": "2026-04-14T20:07:16Z" - } - ], - "completed_at": "2026-04-14T20:07:16Z" - }, - { - "id": "task-026", - "title": "基于 design/rust-kernel-replacement-roadmap-v1.md 建立真实 Rust bridge/runtime 目录与最小执行样板,让至少一条读链和两条写链从 Web route 真实进入 Rust 执行器", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-025" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/documents/content/route.ts src/app/api/documents/title/route.ts src/app/api/documents/save/route.ts src/lib/documents/bridge.ts", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "初始化 rust/bridge/ 目录与最小 runtime executor,不再只在 TypeScript 中模拟 Rust request", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "优先打通 documents.content、documents.title、documents.save 的 Web -> Rust -> Convex 样板链", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "补对应 smoke/单测,并在路线图与 checklist 中记录真实 Rust 执行入口已建立", - "timestamp": null - }, - "已确定 task-026 最小样板边界:新增 Rust bridge runtime 二进制,先接管 documents.content.get、documents.title.update、documents.save 的执行计划与参数映射,TS 仅保留 Convex transport。", - "已完成 Phase 1 最小样板:新增 bridge-runtime crate 与 rust/bridge 目录说明,documents.content.get / documents.title.update / documents.save 现已先经 Rust 生成执行计划,再由 TS 仅做 Convex transport;cargo test 全 workspace、bridge.test.ts、定向 eslint 与 runtime stdin 烟测均通过。" - ], - "completed_at": "2026-04-14T23:32:53Z" - }, - { - "id": "task-027", - "title": "把 mnote-cli 并入主仓 Rust workspace,先冻结 page/block/search/sidebar/tool 的最小命令面与 --json 输出协议", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-026" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo check --manifest-path rust/Cargo.toml && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p mnote-cli", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "新增 rust/crates/mnote-cli 并接入 workspace members", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "冻结 page/block/search/sidebar/tool 五类最小命令面与 --json 输出协议", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "补 CLI smoke,确保 CLI 与 Web 共享同一 Rust 执行面而不是第二套实现", - "timestamp": null - }, - "已并入 mnote-cli:新增主仓 workspace crate,冻结 page/block/search/sidebar/tool 五类最小命令面与 --json 输出协议,cargo check/test -p mnote-cli 通过,workspace 全量 cargo test 通过,CLI page get --json 烟测输出稳定。" - ], - "completed_at": "2026-04-14T23:49:31Z" - }, - { - "id": "task-028", - "title": "完成页面系统 Rust 化第一批:把 documents.create/move/delete/restore/duplicate/copy-tree 收口到 Rust command,Next route 仅保留 transport 包装", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-026", - "task-027" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/documents/create/route.ts src/app/api/documents/move/route.ts src/app/api/documents/delete/route.ts src/app/api/documents/restore/route.ts src/app/api/documents/duplicate/route.ts src/app/api/documents/copy-tree/route.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "为页面生命周期操作补齐 Rust domain model、command、storage-convex-bridge 映射", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "让 create/move/delete/restore/duplicate/copy-tree 进入统一 Rust command 执行链", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "补页面树与回收站回归,并标注可退役的旧 TS 业务逻辑", - "timestamp": null - }, - { - "step": 4, - "total": 4, - "description": "已把 documents.create/move/delete/restore/duplicate/copy-tree 收口到 Rust command + page adapter;Convex functions 刷新、cargo test、bridge vitest 与定向 eslint(0 error)已通过", - "timestamp": "2026-04-15T01:36:12Z" - } - ], - "completed_at": "2026-04-15T01:36:12Z" - }, - { - "id": "task-029", - "title": "完成块系统 Rust 化第一批:把 blocks.get/patch/move/embed 与 AI 文档写入工具统一到 Rust block ops 协议", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-026", - "task-027" - ], - "attempts": 0, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/blocks/get/route.ts src/app/api/blocks/patch/route.ts src/app/api/blocks/move/route.ts src/app/api/blocks/embed/route.ts src/components/editor/blocknote-editor.tsx src/components/editor/DocumentAiAgentPanel.tsx", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已补 blocks.get/patch/move/embed 的 Rust query/command 契约,并让前端 blocks route 统一经 block adapter + Rust plan 进入保存链", - "timestamp": "2026-04-15T02:51:38Z" - }, - { - "step": 2, - "total": 3, - "description": "AI 文档写入工具已改为共享块快照 adapter;doc_insert_blocks/doc_replace_range 不再直连旧 saveBlocks 路径", - "timestamp": "2026-04-15T02:51:38Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过全量 cargo test 与 task-029 指定 eslint 验证;BlockNote/AI 面仅保留交互和快照替换职责", - "timestamp": "2026-04-15T02:51:38Z" - }, - { - "step": 2, - "total": 3, - "description": "AI 文档写入工具改为复用纯 block ops 适配层,不再依赖长期的 loadBlocks/saveBlocks 直连路径", - "timestamp": "2026-04-15T02:17:43Z" - } - ], - "completed_at": "2026-04-15T02:51:38Z" - }, - { - "id": "task-030", - "title": "把 search.documents/search.recent/sidebar.dataset.list 收口到 Rust query/index-fts 主链,停止在 TS route 中持有召回、排序和 snippet 逻辑", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-026", - "task-027" - ], - "attempts": 0, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p index-fts -p storage-convex-bridge && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/search/documents/route.ts src/app/api/search/recent/route.ts src/app/api/sidebar/route.ts src/lib/server/sidebar-data.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已让 search.documents、search.recent、sidebar.dataset.list 进入 Rust query/runtime 主链,route 不再持有核心 ranking/snippet 逻辑", - "timestamp": "2026-04-15T03:55:00Z" - }, - { - "step": 2, - "total": 3, - "description": "已把 index-fts 接入产品搜索主路径,新增 evaluate_search_documents 负责召回合并、高亮 snippet 与 OCR 待补队列决策", - "timestamp": "2026-04-15T03:55:30Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 cargo test -p index-fts -p storage-convex-bridge -p bridge-runtime 与 task-030 指定 eslint;tsc 失败仍为仓库既有 Convex/类型问题,非本次新增", - "timestamp": "2026-04-15T03:56:00Z" - } - ], - "completed_at": "2026-04-15T03:56:00Z" - }, - { - "id": "task-031", - "title": "为 Rust 内核建立统一 Tool 协议与注册表,让 CLI 与 AI 共享 doc_get/doc_find/doc_insert_blocks/doc_replace_range 等一组同源工具", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-027", - "task-029" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/ai-agent/run/route.ts src/components/editor/DocumentAiAgentPanel.tsx", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "在 Rust 侧建立 Tool protocol、tool registry 与统一错误码/审计模型", - "timestamp": "2026-04-15T05:58:48Z" - }, - { - "step": 2, - "total": 3, - "description": "让 AI 与 CLI 共用同一批文档工具,不再依赖前端私有工具注册表作为写入真入口", - "timestamp": "2026-04-15T05:58:48Z" - }, - { - "step": 3, - "total": 3, - "description": "补 dry-run、validate-only、explain-plan 等安全模式并验证 trace/event 可回查", - "timestamp": "2026-04-15T05:58:48Z" - } - ], - "completed_at": "2026-04-15T06:01:57Z" - }, - { - "id": "task-032", - "title": "并入 adapter-mindmap,形成可被 Web/CLI/AI 共用的 Rust mindmap ops 协议与执行层", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-029", - "task-031" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/mindmap/[docId]/route.ts src/app/api/mindmap/[docId]/[mindmapId]/route.ts src/lib/mindmap/mindmapOps.ts src/components/editor/blocks/MindmapBlock.tsx", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "把 adapter-mindmap 并入主仓并冻结节点树、节点引用、节点操作日志协议", - "timestamp": "2026-04-15T07:15:09Z" - }, - { - "step": 2, - "total": 3, - "description": "让 Mindmap 的 get/put/op 可由 CLI 与 AI 直接调用", - "timestamp": "2026-04-15T07:21:09Z" - }, - { - "step": 3, - "total": 3, - "description": "补 Mindmap 浏览器回归和 CLI smoke,确认前端形态不变而执行层已切换", - "timestamp": "$TS" - } - ], - "completed_at": "$TS" - }, - { - "id": "task-033", - "title": "并入 adapter-onlyoffice,把 sign/proxy/callback/forcesave/session 收口到 Rust 对象适配层,OnlyOffice 前端页只保留 transport 与展示职责", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-025", - "task-031" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/onlyoffice/sign/route.ts src/app/api/onlyoffice/proxy/route.ts src/app/api/onlyoffice/callback/route.ts src/app/api/onlyoffice/forcesave/route.ts src/app/onlyoffice/OnlyOfficeClientPage.tsx && node /mnt/Data1T/mnote/scripts/task022-onlyoffice-ui-regression.js", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已并入 adapter-onlyoffice,补齐 asset/session/sign/proxy/callback/forcesave 的 Rust 对象边界与 tool registry", - "timestamp": "2026-04-15T08:02:28Z" - }, - { - "step": 2, - "total": 3, - "description": "OnlyOffice sign/proxy/callback/forcesave route 已收口为共享 adapter + Rust tool 包装,前端页只保留展示与 transport 组装", - "timestamp": "2026-04-15T08:02:28Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 Rust workspace 测试、OnlyOffice 定向 vitest/eslint 与真实 task022 浏览器回归;并修正 mnote-env.json 与回归脚本默认端口到 3000", - "timestamp": "2026-04-15T08:02:28Z" - } - ], - "completed_at": "2026-04-15T08:02:28Z" - }, - { - "id": "task-034", - "title": "建立 Rust 内核统一观测面:补全 command/event/request/trace 的全域落账、幂等、冲突、回放和索引重建命令,作为 CLI 与 AI 稳定运行前置条件", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-026", - "task-027", - "task-031" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/bridge/request/route.ts src/app/api/bridge/trace/route.ts src/lib/documents/bridge-log.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已统一 bridge request/trace/command 的 Rust query/tool 协议,并让 /api/bridge/request 与 /api/bridge/trace 切到 Rust runtime transport 主链", - "timestamp": "2026-04-15T08:40:21Z" - }, - { - "step": 2, - "total": 3, - "description": "已补 event_replay 与 index_rebuild 恢复命令,bridge-runtime 可直接输出回放/重建摘要,cargo test 与 bridge 定向 eslint 已通过", - "timestamp": "2026-04-15T08:40:21Z" - }, - { - "step": 3, - "total": 3, - "description": "已同步路线图、missing-targets、phase checklist 与 core 设计文档,把统一观测面写成 Rust cutover 的硬前置条件", - "timestamp": "2026-04-15T08:40:21Z" - } - ], - "completed_at": "2026-04-15T08:40:21Z" - }, - { - "id": "task-035", - "title": "制定并执行旧 TS 执行面退役清单:按路线图阶段标记哪些 route 仅剩 transport,哪些业务逻辑允许删除,并形成 Rust 成为唯一业务执行平面的最终 cutover 文档", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-028", - "task-029", - "task-030", - "task-031", - "task-032", - "task-033", - "task-034" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\nroadmap = (root/'design/rust-kernel-replacement-roadmap-v1.md').read_text(encoding='utf-8')\nmissing = (root/'design/rust-kernel-missing-targets.md').read_text(encoding='utf-8')\nassert 'Phase 8:旧内核退役与总割接' in roadmap\nassert '仍缺一条“从旧内核切换到新内核”的明确割接路线' in missing\nprint('task-035-planned')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已按能力域盘点主仓 route,明确页面、块、查询、AI、Mindmap、OnlyOffice 分别属于 Rust owner、transport keep 或旧 TS 兼容面", - "timestamp": "2026-04-15T09:01:04Z" - }, - { - "step": 2, - "total": 3, - "description": "已补最终 cutover 文档,写清 Phase 8 四组 gate、允许删除与必须保留的 Web 壳,以及旧 TS 执行面退役顺序", - "timestamp": "2026-04-15T09:01:04Z" - }, - { - "step": 3, - "total": 3, - "description": "已同步 roadmap、missing-targets、phase checklist 与 release audit,并通过 task-035 验证脚本", - "timestamp": "2026-04-15T09:01:04Z" - } - ], - "completed_at": "2026-04-15T09:01:04Z" - }, - { - "id": "task-036", - "title": "页面兼容旧面第一批 Rust 化:把 documents/create-child、documents/embed、documents/template 收口到统一 page command/query adapter,route 仅保留 transport", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-035" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime -p storage-convex-bridge && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/documents/create-child/route.ts src/app/api/documents/embed/route.ts src/app/api/documents/template/route.ts src/lib/documents/page-command-adapter.ts src/lib/documents/page-lifecycle-command-adapter.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已把 documents/create-child、documents/embed、documents/template 收口到统一 page command adapter,route 仅保留 transport 转发。", - "timestamp": "2026-04-15T11:03:14Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 route-adapters/bridge 定向测试与 task-036 指定 cargo+eslint 验证;当前业务规则已集中到 page-command-adapter。", - "timestamp": "2026-04-15T11:03:14Z" - } - ], - "completed_at": "2026-04-15T11:03:14Z" - }, - { - "id": "task-037", - "title": "页面兼容旧面第二批 Rust 化:把 documents/empty-trash、documents/purge 收口到 Rust lifecycle command,并切断 route 内直连 Convex 业务逻辑", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-036" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime -p storage-convex-bridge && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/documents/empty-trash/route.ts src/app/api/documents/purge/route.ts src/lib/documents/page-lifecycle-command-adapter.ts src/lib/documents/bridge-log.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已把 documents/empty-trash、documents/purge 收口到统一 page command adapter,并补统一失败落账入口。", - "timestamp": "2026-04-15T11:03:14Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 task-037 指定 cargo+eslint 验证;页面旧面第二批已降级为 transport route + adapter 执行。", - "timestamp": "2026-04-15T11:03:14Z" - } - ], - "completed_at": "2026-04-15T11:03:14Z" - }, - { - "id": "task-038", - "title": "Mindmap 剩余旧面收口:把 mindmap-trash/empty 与 mindmap-ai/* 迁到 Rust mindmap adapter/tool,明确 route 只做产品编排而不是对象规则真入口", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-035" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p core-protocol -p bridge-runtime -p storage-convex-bridge && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/mindmap-trash/empty/route.ts src/app/api/mindmap-ai/agent/route.ts src/app/api/mindmap-ai/assets/route.ts src/app/api/mindmap-ai/expand-node/route.ts src/app/api/mindmap-ai/outline-to-mindmap/route.ts src/lib/mindmap/mindmapOps.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已确认 mindmap-trash/empty 与 mindmap-ai 主路径接入 Rust tool/runtime:mindmap_empty_trash、mindmap_apply_ops 等工具已被 route 调用。", - "timestamp": "2026-04-15T11:06:05Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 task-038 指定 cargo test 与 eslint;保留仓库既有 unused/no-explicit-any warnings,但无新 error。", - "timestamp": "2026-04-15T11:06:05Z" - } - ], - "completed_at": "2026-04-15T11:06:05Z" - }, - { - "id": "task-039", - "title": "建立 AI 工具 cutover 矩阵:盘点 builtins/**、ai-agent/run 余下工具与 Rust Tool registry 的一一对应,并冻结迁移顺序与删除清单", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-035" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/design/ai-tool-cutover-matrix.md').read_text(encoding='utf-8')\nfor needle in ['docsServerTools', 'mediaServerTools', 'mindmapServerTools', 'onlyofficeServerTools', 'registryBuiltins', 'ai-agent/run']:\n assert needle in text, needle\nprint('task-039-ok')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已新增 ai-tool-cutover-matrix,覆盖 docsServerTools、mediaServerTools、mindmapServerTools、onlyofficeServerTools、registryBuiltins 与 ai-agent/run 的一一对应矩阵。", - "timestamp": "2026-04-15T11:03:14Z" - }, - { - "step": 2, - "total": 2, - "description": "已补 search_web/image_read/slash_run 的 Rust tool registry 对照,并通过 task-039 文档校验。", - "timestamp": "2026-04-15T11:03:14Z" - } - ], - "completed_at": "2026-04-15T11:03:14Z" - }, - { - "id": "task-040", - "title": "迁移 AI 非 doc_* 核心工具到 Rust Tool runtime,收缩 ai-agent/run 与 builtins/** 私有真入口,只保留 client-tool-result 这类 transport 壳", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-039" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": null, - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime -p mnote-cli && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/ai-agent/run/route.ts src/app/api/ai-agent/client-tool-result/route.ts src/lib/ai-agent/tools/builtins/registryBuiltins.ts src/lib/ai-agent/tools/builtins/media/mediaServerTools.ts src/lib/ai-agent/tools/builtins/searchWeb.ts src/lib/ai-agent/tools/builtins/slash/slashServerTools.ts src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "at": "2026-04-15T11:32:44Z", - "description": "已补 bridge-runtime 对 search_web/image_read/slash_run 的 tool plan 与 slash 结果解析,ai-agent/run 不再保留 search_web 的 TS fallback,media/slash builtins 只剩 transport helper。" - }, - { - "at": "2026-04-15T11:32:44Z", - "description": "已通过 cargo test -p bridge-runtime -p mnote-cli 与 task-040 指定 eslint;仅剩仓库既有 any/unused warnings。" - } - ], - "completed_at": "2026-04-15T11:32:44Z" - }, - { - "id": "task-041", - "title": "补齐统一观测面 workspace 总览、分页与按对象/状态过滤,让 bridge 查询成为 cutover 阶段的正式运营与回查入口", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-035" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "de35d423", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime -p mnote-cli && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/bridge/request/route.ts src/app/api/bridge/trace/route.ts src/lib/documents/bridge-log.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 2, - "description": "已补 bridge.workspace.overview 观测链路:Convex bridgeLogs 新增 workspace 总览、分页与按状态/对象过滤,Rust runtime 与 Next route 已连通。", - "timestamp": "2026-04-15T10:50:15Z" - }, - { - "step": 2, - "total": 2, - "description": "已通过 cargo test -p bridge-runtime -p storage-convex-bridge、bridge.test.ts 与定向 eslint;仅保留仓库既有 no-explicit-any warnings。", - "timestamp": "2026-04-15T10:50:15Z" - } - ], - "completed_at": "2026-04-15T10:50:15Z" - }, - { - "id": "task-042", - "title": "把失败态、冲突态与补偿态统一落账到 bridge-log/runtime,覆盖页面主写链与 AI、Mindmap 写链,完成 cutover 所需状态语义闭环", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-037", - "task-038", - "task-040", - "task-041" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": null, - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime -p storage-convex-bridge && cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/documents/bridge.test.ts && pnpm exec eslint src/lib/documents/bridge-log.ts src/lib/documents/page-command-adapter.ts src/lib/documents/page-lifecycle-command-adapter.ts src/app/api/ai-agent/run/route.ts src/lib/mindmap/mindmapOps.ts", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "at": "2026-04-15T11:32:44Z", - "description": "已补 bridge-log 失败/补偿状态语义、metadata command 失败落账,以及 AI/Mindmap 写链的统一失败态记录;mindmap 本地 ops 冲突不再伪装成功。" - }, - { - "at": "2026-04-15T11:32:44Z", - "description": "已通过 cargo test -p bridge-runtime -p storage-convex-bridge、src/lib/documents/bridge.test.ts 与 task-042 指定 eslint;仅剩仓库既有 any/unused warnings。" - } - ], - "completed_at": "2026-04-15T11:32:44Z" - }, - { - "id": "task-043", - "title": "按 cutover 文档执行第一批 TS_LEGACY_DELETE:先停写并切调用方,再删除已被 Rust 替代的 builtins 服务端真入口与过渡 helper", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-037", - "task-038", - "task-040" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": null, - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/design/rust-kernel-legacy-delete-list.md').read_text(encoding='utf-8')\nfor needle in ['documents/create-child', 'documents/embed', 'documents/empty-trash', 'documents/purge', 'documents/template', 'mindmap-trash/empty', 'builtins/**']:\n assert needle in text, needle\nprint('task-043-ok')\nPY && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/ai-agent/run/route.ts src/app/api/mindmap-ai/agent/route.ts src/app/api/documents/create-child/route.ts src/app/api/documents/purge/route.ts", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "at": "2026-04-15T11:32:44Z", - "description": "已新增 rust-kernel-legacy-delete-list.md,冻结第一批 TS_LEGACY_DELETE 清单:documents/create-child/embed/empty-trash/purge/template、mindmap-trash/empty、builtins/**。" - }, - { - "at": "2026-04-15T11:32:44Z", - "description": "已通过 task-043 文档校验与指定 eslint;仅剩仓库既有 any/unused warnings。" - } - ], - "completed_at": "2026-04-15T11:32:44Z" - }, - { - "id": "task-044", - "title": "完成 Phase 8 cutover 最终验证与文档回写,确认剩余 Web route 全部降级为 TS_TRANSPORT_KEEP 或 Rust owner,并形成可对外宣布的统一口径", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-037", - "task-038", - "task-040", - "task-041", - "task-042", - "task-043" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": null, - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\ncutover = (root/'design/rust-kernel-cutover-v1.md').read_text(encoding='utf-8')\naudit = (root/'design/release-readiness-source-audit.md').read_text(encoding='utf-8')\nchecklist = (root/'design/rust-kernel-backport-phase-checklist.md').read_text(encoding='utf-8')\nassert 'Rust 已成为 mnote 的唯一业务执行平面' in cutover\nassert 'TS_TRANSPORT_KEEP' in cutover\nassert 'Phase 8' in checklist\nassert 'release readiness' in audit.lower() or 'source audit' in audit.lower()\nprint('task-044-ok')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "at": "2026-04-15T11:32:44Z", - "description": "已回写 cutover/release-audit/phase-checklist/missing-targets,补齐 Phase 8 最终口径、TS_TRANSPORT_KEEP/TS_LEGACY_DELETE 边界与可对外宣布文案。" - }, - { - "at": "2026-04-15T11:32:44Z", - "description": "已通过 task-044 文档校验脚本。" - } - ], - "completed_at": "2026-04-15T11:32:44Z" - }, - { - "id": "task-045", - "title": "执行最终收尾 F1:按 rust-kernel-legacy-delete-list 对第一批 TS_LEGACY_DELETE 做物理退役规划与实现,区分可直接删除、需先切调用方、以及只能保留 transport 壳的路径", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-044" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": null, - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/design/rust-kernel-final-closure-checklist.md').read_text(encoding='utf-8')\nassert 'F1. 第一批 `TS_LEGACY_DELETE` 物理退役' in text\nprint('task-045-planned')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "对 documents/create-child/embed/empty-trash/purge/template、mindmap-trash/empty、builtins/** 逐项分类为删除/降级/保留壳", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "为每个待退役路径补调用方清单与删除前置条件,避免误删仍在用的入口", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "回写 legacy delete 文档与 cutover 文档,使“可删”与“已删/待删”状态一致", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已把第一批 TS_LEGACY_DELETE 的真实边界写实:当前不可直接批量删除 route 文件,必须先清 adapter 内 TS 真执行,并去掉 mindmap-trash/empty 的伪 Rust 包装。", - "timestamp": "2026-04-15T13:15:38Z" - } - ], - "completed_at": "2026-04-15T13:15:38Z" - }, - { - "id": "task-046", - "title": "执行最终收尾 F2-A:清理页面系统残留的“Rust 包装 + TS 真执行”双路径,把 create-child/embed/empty-trash/purge/template 从 adapter 内直连 Convex mutation 推进到真正 Rust owner 或明确 transport 壳", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-045" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/lib/documents/page-command-adapter.ts src/app/api/documents/create-child/route.ts src/app/api/documents/embed/route.ts src/app/api/documents/empty-trash/route.ts src/app/api/documents/purge/route.ts src/app/api/documents/template/route.ts && cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime -p storage-convex-bridge", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "盘点 page-command-adapter 中仍直接 client.mutation(...) 的路径,确认 create-child/template/empty-trash/purge 仍是假 cutover,而 embed 还保留 TS 内容编排。", - "timestamp": "2026-04-15T13:57:26Z" - }, - { - "step": 2, - "total": 3, - "description": "已将 create-child 切到 documents.create,template/empty-trash/purge 切到统一 Rust runtime + transport,embed 改为通过 documents.save 落盘,不再直连 Convex mutation。", - "timestamp": "2026-04-15T13:57:26Z" - }, - { - "step": 3, - "total": 3, - "description": "已更新 cutover/legacy-delete/release-audit 文档,并通过页面旧面定向 eslint、route-adapters 测试与 bridge-runtime/storage-convex-bridge cargo test。", - "timestamp": "2026-04-15T13:57:26Z" - } - ], - "completed_at": "2026-04-15T13:57:26Z" - }, - { - "id": "task-047", - "title": "执行最终收尾 F2-B:清理块系统与 Mindmap 残留双路径,重点处理 block-command-adapter 与 mindmap-trash/empty 中“先走 Rust 再直接 Convex mutation”的假完成状态", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-045" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/lib/documents/block-command-adapter.ts src/app/api/mindmap-trash/empty/route.ts src/app/api/mindmap/[docId]/route.ts src/app/api/mindmap/[docId]/[mindmapId]/route.ts && cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime -p storage-convex-bridge", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "盘点 block patch/move/embed 与 mindmap trash 路径中的双执行残留", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "决定这些路径是继续迁入 Rust 执行器,还是回退为明确的 transport 壳", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "同步更新 cutover 文档中的块系统和 Mindmap 状态,避免“文档已完成、代码仍双写”", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已确认现网 `/api/blocks/*` 主链统一走 `src/lib/blocks/block-command-adapter.ts`,`src/lib/documents/block-command-adapter.ts` 仅作为遗留兼容 helper 冻结。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 2, - "total": 3, - "description": "已把 `mindmap-trash/empty`、`mindmap/[docId]`、`mindmap/[docId]/[mindmapId]` 的删除/恢复/清空回收站路径收口到 Rust runtime/transport,不再由 route 直接调用 Convex mutation。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 task-047 指定 eslint 与 `bridge-runtime`/`storage-convex-bridge` cargo test,并同步 release audit/cutover 文档口径。", - "timestamp": "2026-04-15T15:43:04Z" - } - ], - "completed_at": "2026-04-15T15:43:04Z" - }, - { - "id": "task-048", - "title": "执行最终收尾 F3-A:把 mnote-cli 从“计划输出/协议冻结器”推进到页面与块系统的最小真实执行 CLI,至少覆盖 page get/title/save/create/delete/move 与 block insert/patch/move/embed", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-046", - "task-047" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p mnote-cli && bash rust/scripts/task049-cli-smoke.sh", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "把页面与块系统 CLI 从 transport plan 输出推进到真实执行模式", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "补齐稳定 JSON 输出、错误码和最小 smoke,确认 CLI 可以不经过浏览器完成主链操作", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "更新 final closure checklist 与路线图文档,说明 CLI 已进入最小真实执行态", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已直接核实 `mnote-cli` 当前能编译并输出稳定 JSON,但 `page get` 与 `block insert` 仍只返回 transport plan,而不是真执行结果。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 1, - "total": 3, - "description": "已修复 `mnote-cli` 的 `execute_block_patch` 块树回写形状问题,`block move --execute` 不再因 snapshot 损坏失败。", - "timestamp": "2026-04-16T02:29:32Z" - }, - { - "step": 2, - "total": 3, - "description": "已扩展 `rust/scripts/task049-cli-smoke.sh`,覆盖 `page get/create/title/save/move/delete/restore` 与 `block insert/patch/move/embed`,并实跑通过。", - "timestamp": "2026-04-16T02:29:33Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 `cargo test -p mnote-cli`;页面与块系统 CLI 已进入最小真实执行态。", - "timestamp": "2026-04-16T02:29:34Z" - } - ], - "completed_at": "2026-04-16T02:29:35Z" - }, - { - "id": "task-049", - "title": "执行最终收尾 F3-B:继续把 search/sidebar/mindmap/tool 主链推进到可真实执行 CLI,并补齐 rust/scripts、tests、fixtures 的最小 CLI 验收骨架", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-048" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime && bash rust/scripts/task049-cli-smoke.sh", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "把 search、sidebar、mindmap、tool run 从协议冻结推进到最小真实执行", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "初始化 rust/scripts、rust/tests、rust/fixtures 的 CLI 验收骨架,避免 CLI 只存在于代码层而无回归路径", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "把 CLI 主链覆盖范围写回设计文档,明确哪些功能已经可脚本化运行", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已把 `search documents`、`sidebar dataset`、`tool run docs_search/docs_read` 与 `mindmap put/get/op` 纳入仓库内 smoke。", - "timestamp": "2026-04-16T02:29:39Z" - }, - { - "step": 2, - "total": 3, - "description": "已更新 `rust/tests/README.md`,记录 `/tmp/task049-cli-smoke-*` 输出与 `mindmap op` 的 result-only 验收边界。", - "timestamp": "2026-04-16T02:29:40Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 `cargo test -p bridge-runtime` 与新版 `task049-cli-smoke.sh`;CLI 主链验收骨架闭合。", - "timestamp": "2026-04-16T02:29:41Z" - } - ], - "completed_at": "2026-04-16T02:29:42Z" - }, - { - "id": "task-050", - "title": "执行最终收尾 F4:继续收掉 AI 工具矩阵中的 TS_COMPAT_PENDING,优先处理 docs_search/docs_read/asset_to_mindmap,并把 remaining builtins 全部归类为 Rust owner 或 TS_TRANSPORT_KEEP", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-045" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo test --manifest-path rust/Cargo.toml -p bridge-runtime && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec vitest run src/lib/ai-agent/runtime/runAgent.test.ts && pnpm exec eslint src/app/api/ai-agent/run/route.ts src/lib/ai-agent/tools/builtins/docs/docsServerTools.ts src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts src/app/api/mindmap-ai/outline-to-mindmap/route.ts", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "优先清理 docs_search、docs_read、asset_to_mindmap 这类仍处于 TS_COMPAT_PENDING 的 AI 工具", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "把 builtins/** 剩余服务端真入口进一步压缩,最终只保留明确的 TS_TRANSPORT_KEEP 边界", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "更新 ai-tool-cutover-matrix,使剩余工具状态不再模糊", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已把 `asset_to_mindmap` 的实际导图写入切到 Rust `mindmap_apply_ops`,并修正 `outline-to-mindmap` 读取 Rust 结果的接缝;但 `docs_search/docs_read` 仍缺少对应 Rust runtime。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 2, - "total": 3, - "description": "已把 `docsServerTools`、`onlyofficeServerTools`、`ai-agent/run` 的当前边界写实:`docs_*` 仍是 TS 兼容查询层,`asset_extract_outline` 仍依赖 TS/MinerU。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 1, - "total": 3, - "description": "已核实 Convex 主链下 `ai-agent/run` 通过 `executeRustBridgeTool(...)` 执行 `docs_search/docs_read`,`docsServerTools` 仅保留 fallback。", - "timestamp": "2026-04-16T02:29:46Z" - }, - { - "step": 2, - "total": 3, - "description": "已将 AI 工具矩阵回写为 `docs_search/docs_read=RUST_OWNER`,`asset_extract_outline/asset_to_mindmap/oo_*=TS_TRANSPORT_KEEP`,builtins 主链不再保留 `TS_COMPAT_PENDING`。", - "timestamp": "2026-04-16T02:29:47Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 `bridge-runtime` 测试、`runAgent.test.ts` 与 task-050 关联 `eslint`;AI 工具矩阵收口完成。", - "timestamp": "2026-04-16T02:29:48Z" - } - ], - "completed_at": "2026-04-16T02:29:49Z" - }, - { - "id": "task-051", - "title": "执行最终收尾 F5-A:做文档页、Sidebar 与主写链的发布前运行态复核,确认 legacy delete 与双路径收缩后页面编辑、保存、回查、刷新未退化", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-046", - "task-047" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "node /mnt/Data1T/mnote/scripts/task019-document-ui-regression.js && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/editor/blocknote-editor.tsx src/app/api/documents/save/route.ts src/app/api/sidebar/route.ts", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "复跑文档页与 Sidebar 浏览器回归,验证标题、正文、保存、刷新、回查链仍正常", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "确认 legacy delete 与双路径清理没有引入新的页面主链回归", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "把运行态结果补记到 release-readiness-source-audit 与最终收尾文档", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已复跑 `scripts/task019-document-ui-regression.js`,确认文档页标题、正文、保存、刷新与回查链通过。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 2, - "total": 3, - "description": "已通过 `src/components/editor/blocknote-editor.tsx`、`src/app/api/documents/save/route.ts`、`src/app/api/sidebar/route.ts` 定向 eslint(仅剩仓库既有 warnings),确认页面主链未因双路径收缩退化。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 3, - "total": 3, - "description": "已把文档页运行态结果同步到 release audit 与 final closure checklist;task-051 不再依赖 CLI 线完成。", - "timestamp": "2026-04-15T15:43:04Z" - } - ], - "completed_at": "2026-04-15T15:43:04Z" - }, - { - "id": "task-052", - "title": "执行最终收尾 F5-B:做 Mindmap、OnlyOffice 与 AI 工具链的发布前运行态复核,确认对象域与工具域在最终收尾后仍可稳定工作", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-047", - "task-050" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "node /mnt/Data1T/mnote/scripts/task021-mindmap-ui-regression.js && node /mnt/Data1T/mnote/scripts/task022-onlyoffice-ui-regression.js && cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec vitest run src/lib/ai-agent/runtime/runAgent.test.ts && pnpm exec eslint src/app/api/mindmap/[docId]/route.ts src/app/api/onlyoffice/callback/route.ts src/app/api/ai-agent/run/route.ts", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "复跑 Mindmap 浏览器回归,确认节点操作、保存、删除、回查未退化", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "复跑 OnlyOffice 打开、插件桥、callback、forcesave 回归,确认对象域 transport 壳与 Rust adapter 边界稳定", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "复核 AI 工具链在最终 cutover 后仍能完成核心读写与工具调用", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已复跑 `scripts/task021-mindmap-ui-regression.js`,确认新增/删除节点、保存链、刷新回查与 `requestId/traceId` 元信息同步通过。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 2, - "total": 3, - "description": "已复跑 `scripts/task022-onlyoffice-ui-regression.js`,确认打开、插件桥、callback、forcesave 与 `storage_id` 变化通过;定向 eslint 无 error。", - "timestamp": "2026-04-15T15:43:04Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 `runAgent.test.ts` 补齐 `docs_search -> docs_read` 的 AI runtime 证据;结合 task021/task022 浏览器回归,Mindmap/OnlyOffice/AI 工具链运行态闭合。", - "timestamp": "2026-04-16T02:29:53Z" - } - ], - "completed_at": "2026-04-16T02:29:54Z" - }, - { - "id": "task-053", - "title": "执行最终收尾 F6:统一最终宣布口径,确保 cutover/release-audit/final-closure-checklist/harness 任务状态与真实代码完全一致,并给出可对外使用的最终完成结论", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-045", - "task-046", - "task-047", - "task-048", - "task-049", - "task-050", - "task-051", - "task-052" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "python3 - <<'PY'\nfrom pathlib import Path\nimport json\nroot = Path('/mnt/Data1T/mnote')\nfor p in [root/'design/ai-tool-cutover-matrix.md', root/'design/release-readiness-source-audit.md', root/'design/rust-kernel-final-closure-checklist.md', root/'design/rust-kernel-cutover-v1.md']:\n text = p.read_text(encoding='utf-8')\n assert '2026-04-16' in text\nstate = json.loads((root/'harness-tasks.json').read_text(encoding='utf-8'))\nfor task_id in ['task-048', 'task-049', 'task-050', 'task-052', 'task-053']:\n task = next(t for t in state['tasks'] if t['id'] == task_id)\n assert task['status'] == 'completed'\nprint('task-053-validated')\nPY", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "统一整理源码审计口径、完整发布口径、长期维护口径三者的最终边界", - "timestamp": null - }, - { - "step": 2, - "total": 3, - "description": "更新所有最终文档,确保不存在“文档已完成但代码未收尾”或“代码已删但文档仍保留旧状态”的漂移", - "timestamp": null - }, - { - "step": 3, - "total": 3, - "description": "给出最终对外结论:哪些目标已彻底完成,哪些壳会长期保留但不再承载业务规则", - "timestamp": null - }, - { - "step": 1, - "total": 3, - "description": "已回写 `ai-tool-cutover-matrix`、`release-readiness-source-audit`、`rust-kernel-final-closure-checklist`、`rust-kernel-cutover-v1` 到同一最终口径。", - "timestamp": "2026-04-16T02:29:57Z" - }, - { - "step": 2, - "total": 3, - "description": "已把 `task-048/049/050/052/053` 的验证命令更新为当前真实 smoke/test,并同步完成状态。", - "timestamp": "2026-04-16T02:29:58Z" - }, - { - "step": 3, - "total": 3, - "description": "统一结论:Rust 已成为 mnote 的主业务执行平面,TS 仅保留 transport、外部服务编排、客户端桥与冻结兼容壳。", - "timestamp": "2026-04-16T02:29:59Z" - } - ], - "completed_at": "2026-04-16T02:30:00Z" - }, - { - "id": "task-054", - "title": "按 AI 前端精简方案 v1 执行第一阶段:启用并验证本机 Hermes API Server,把 /api/ai-agent/run 从前端 AI 编排器收口为 Hermes bridge,停止在 Next route 内继续扩展 tool registry、builtin toolset 和 create*ServerTools 编排逻辑", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-053" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/ai-agent/run/route.ts src/lib/ai-agent/tools/registry.ts src/lib/ai-agent/tools/builtins/registryBuiltins.ts src/lib/ai-agent/runtime/runAgent.ts", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已启用并验证 Hermes API Server:/health、/v1/models 与 /v1/runs 最小链路均通过,run_fc6786126fe645b8af6aa65d1c8d4627 返回“你好”。", - "timestamp": "2026-04-16T04:51:57Z" - }, - { - "step": 2, - "total": 3, - "description": "已把 /api/ai-agent/run 收口为 Hermes bridge,route 仅保留鉴权、上下文规范化、Hermes 转发、SSE 回流与 Codex 兼容桥。", - "timestamp": "2026-04-16T04:51:58Z" - }, - { - "step": 3, - "total": 3, - "description": "已冻结前端 registry/builtins/runAgent 为遗留兼容,run route 不再导入 createToolRegistry、builtinToolSets 或 create*ServerTools。", - "timestamp": "2026-04-16T04:51:59Z" - } - ], - "completed_at": "2026-04-16T04:52:00Z" - }, - { - "id": "task-055", - "title": "按 AI 前端精简方案 v1 执行第二阶段:把全局 AI、页面 AI、Mindmap AI、OnlyOffice AI 收口为一个通用 AiBridgePanel,加上少量 context adapter,删除四套重复状态机", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-054" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/ai-agent/AiAgentPanel.tsx src/components/editor/DocumentAiAgentPanel.tsx src/components/editor/blocks/MindmapAiAgentPanel.tsx src/components/onlyoffice/OnlyOfficeAiAgentPanel.tsx src/store/ai-agent-ui.ts", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已提炼 AiBridgePanel 与 panelShared,统一面板壳、SSE 解析和 provider/model/maxSteps 偏好读写。", - "timestamp": "2026-04-16T04:52:07Z" - }, - { - "step": 2, - "total": 3, - "description": "已让 global/document/mindmap/onlyoffice 四类场景接入 AiBridgePanel,并保留各自的 context adapter 与内部业务流。", - "timestamp": "2026-04-16T04:52:08Z" - }, - { - "step": 3, - "total": 3, - "description": "已删除 Mindmap 隐藏旧壳并收缩重复 helper,四套面板不再各自维护独立的壳与基础状态读写。", - "timestamp": "2026-04-16T04:52:09Z" - } - ], - "completed_at": "2026-04-16T04:52:10Z" - }, - { - "id": "task-056", - "title": "按 AI 前端精简方案 v1 执行第三阶段:弱化或下线 GlobalAiAgentHost,把全局 AI 从主布局常驻入口降级为实验入口或可选入口,保留页面 AI 作为主场景", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-055" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/(app)/layout.tsx src/components/ai-agent/GlobalAiAgentHost.tsx src/components/breadcrumb.tsx src/store/ai-agent-ui.ts", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已从 app layout 移除 GlobalAiAgentHost 常驻挂载,主布局不再继续保留全局 AI 壳。", - "timestamp": "2026-04-16T04:52:12Z" - }, - { - "step": 2, - "total": 3, - "description": "已保留页面 AI 作为 breadcrumb 主入口,全局 AI 仅保留为实验入口组件,不再影响主场景布局。", - "timestamp": "2026-04-16T04:52:13Z" - }, - { - "step": 3, - "total": 3, - "description": "已同步方案文档与当前代码口径:页面 AI 为主场景,全局 AI 退为实验/开发入口。", - "timestamp": "2026-04-16T04:52:14Z" - } - ], - "completed_at": "2026-04-16T04:52:15Z" - }, - { - "id": "task-057", - "title": "按 AI 前端精简方案 v1 执行第四阶段:清理前端 AI 遗留 registry、builtins、重复 localStorage/session 管理与历史兼容 helper,仅保留 Hermes bridge、mnote Rust 业务执行面与浏览器专属 client capability bridge", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-054", - "task-055", - "task-056", - "task-058" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/ai-agent/run/route.ts src/app/api/ai-agent/client-tool-result/route.ts src/lib/ai-agent/tools src/components/ai-agent src/components/editor/DocumentAiAgentPanel.tsx src/components/editor/blocks/MindmapAiAgentPanel.tsx src/components/onlyoffice/OnlyOfficeAiAgentPanel.tsx", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已确认 /api/ai-agent/run 主链不再依赖 registry/builtins/runAgent,旧文件全部标记为遗留兼容。", - "timestamp": "2026-04-16T04:52:17Z" - }, - { - "step": 2, - "total": 3, - "description": "已保留 OnlyOffice 插件桥与 /api/ai-agent/client-tool-result,前端只继续承担浏览器专属 client capability bridge。", - "timestamp": "2026-04-16T04:52:18Z" - }, - { - "step": 3, - "total": 3, - "description": "已通过 task-057 定向 eslint,前端 AI 收口为 Hermes bridge + mnote Rust 执行面 + 浏览器专属 client bridge。", - "timestamp": "2026-04-16T04:52:19Z" - } - ], - "completed_at": "2026-04-16T04:52:20Z" - }, - { - "id": "task-058", - "title": "按 AI 前端精简方案 v1 补齐 Hermes 与 mnote Rust 的业务能力桥:明确并落地 Hermes 调用 mnote 文档/导图/OnlyOffice Rust 能力的最小边界,优先评估 MCP server、Hermes plugin/tool adapter 或最小业务 bridge", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-054" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote && python3 - <<'PY'\nimport json\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/design/ai-frontend-simplification-plan-v1.md').read_text(encoding='utf-8')\nneedles = [\n 'Hermes API Server',\n 'mnote Rust',\n 'MCP server',\n 'Hermes plugin/tool adapter',\n]\nfor needle in needles:\n assert needle in text, needle\nprint('task-058-doc-ok')\nPY", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 3, - "description": "已明确 Hermes 不复制 mnote 业务真逻辑,mnote Rust 继续作为文档、块、导图与 OnlyOffice 的真执行面。", - "timestamp": "2026-04-16T04:52:02Z" - }, - { - "step": 2, - "total": 3, - "description": "已在 AI 前端精简方案中固定首选接缝:优先最小业务 bridge,并记录 MCP server 与 Hermes plugin/tool adapter 的评估边界。", - "timestamp": "2026-04-16T04:52:03Z" - }, - { - "step": 3, - "total": 3, - "description": "已补齐 Hermes API Server -> mnote Rust 的最小业务桥边界,避免 /api/ai-agent/run 收口后出现业务能力真空。", - "timestamp": "2026-04-16T04:52:04Z" - } - ], - "completed_at": "2026-04-16T04:52:05Z" - }, - { - "id": "task-059", - "title": "按 Rust Web 长期架构实施清单 v1 推进长期重构主线:统一阶段边界、依赖顺序、横向能力和最终验收口径,避免后续再次回到“Rust 内核 + 重前端页面壳”的临时态", - "status": "completed", - "priority": "P0", - "depends_on": [], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\nfor rel in ['design/rust-web-long-term-architecture-v1.md', 'design/rust-web-long-term-checklist-v1.md', 'harness-tasks.json']:\n assert (root / rel).exists(), rel\nprint('task-059-planned')\nPY2", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已补长期路线阶段依赖、并行规则、阶段输入输出与执行治理口径,统一长期重构主线。", - "timestamp": "2026-04-16T07:25:39Z" - } - ], - "completed_at": "2026-04-16T07:25:39Z" - }, - { - "id": "task-060", - "title": "执行长期路线 Phase 0:完成当前重前端模块审计、阅读态/编辑态边界定义、页面切换性能基线与 islands 候选模块清单,并冻结旧壳继续膨胀入口", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-059" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/design/rust-web-long-term-checklist-v1.md').read_text(encoding='utf-8')\nneedles = [\n 'Phase 0:基线与边界冻结',\n '当前前端重模块审计表',\n '页面切换性能基线报告',\n '文档阅读态与编辑态的边界定义',\n '岛模型候选模块清单',\n]\nfor needle in needles:\n assert needle in text, needle\nprint('task-060-phase0-defined')\nPY2", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已补 Phase 0 的前端重模块审计表、页面切换性能基线、阅读态/编辑态边界定义与 islands 候选模块清单。", - "timestamp": "2026-04-16T07:25:39Z" - } - ], - "completed_at": "2026-04-16T07:25:39Z" - }, - { - "id": "task-061", - "title": "补长期路线横向能力 H1:建立统一性能指标、trace 关联、缓存与预取、权限边界和回归脚本,作为 Rust Web / islands 迁移的共同前置能力", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-060" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/design/rust-web-long-term-checklist-v1.md').read_text(encoding='utf-8')\nneedles = [\n '14.1 观测与性能',\n '14.2 缓存与预取',\n '14.3 安全与权限',\n '14.4 开发规范',\n]\nfor needle in needles:\n assert needle in text, needle\nprint('task-061-cross-cutting-defined')\nPY2", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已补横向能力 H1 的最低交付物:统一观测/trace、缓存预取、权限边界与开发规范口径。", - "timestamp": "2026-04-16T07:25:39Z" - } - ], - "completed_at": "2026-04-16T07:25:39Z" - }, - { - "id": "task-062", - "title": "执行长期路线 Phase 1:在 Rust workspace 中建立 axum Web 承载层,补齐 router、middleware、request/trace/auth/workspace 上下文、SSE/WS 和兼容 Next route 边界,并打通 Rust core runtime 与 Hermes bridge", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-060", - "task-061" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote && CARGO_TARGET_DIR=/tmp/mnote-rust-target-harness cargo check --manifest-path rust/Cargo.toml", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已新增 rust/crates/mnote-web 作为 axum Web 承载层骨架,补统一 router/middleware/request context/SSE/WS/Hermes bridge/compat 边界,并通过 cargo check。", - "timestamp": "2026-04-16T07:36:07Z" - } - ], - "completed_at": "2026-04-16T07:36:07Z" - }, - { - "id": "task-063", - "title": "执行长期路线 Phase 2:把文档页改造成 server-first 阅读入口,去掉 meta -> content -> editor 串行链、mounted gating 和阅读页对 BlockNote 的默认依赖,确保先阅读后编辑", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-062" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/(app)/documents/[id]/page.tsx src/components/editor/document-shell.tsx src/components/editor/document-content.tsx src/components/editor/blocknote-editor.tsx", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已将文档页改造成 server-first 阅读入口:服务端预取 meta+content,去掉 mounted gating,阅读态默认不再依赖 BlockNote。", - "timestamp": "2026-04-16T08:01:56Z" - } - ], - "completed_at": "2026-04-16T08:01:56Z" - }, - { - "id": "task-064", - "title": "执行长期路线 Phase 3:将 Sidebar、页面树、文件树推进为 Rust query + 服务端输出 + 局部 island,收掉主布局中的超大客户端导航壳,并建立局部刷新与预取机制", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-062", - "task-061" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/sidebar-data.test.ts src/lib/file-tree/rows.test.ts && pnpm exec eslint src/components/sidebar/sidebar.tsx src/lib/sidebar-data.ts src/lib/server/sidebar-data.ts src/app/api/sidebar/route.ts src/hooks/use-convex-sidebar-data.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已确认 Sidebar/页面树/文件树具备“服务端首包 + 共享 Rust query 契约 + 局部 island”的最小落账证据,并通过定向测试与 eslint。", - "timestamp": "2026-04-16T08:01:56Z" - } - ], - "completed_at": "2026-04-16T08:01:56Z" - }, - { - "id": "task-065", - "title": "执行长期路线 Phase 4:将搜索系统推进为 Rust 索引/召回/聚合 + 独立 Search island,拆分 SearchPalette 的全局常驻重组件形态,建立 server-first 搜索页与局部交互协议", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-062", - "task-061" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/search/search-palette.tsx src/app/api/search/documents/route.ts src/app/api/search/recent/route.ts", - "timeout_seconds": 2400 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已将 SearchPalette 收口为轻量 host + 按需 runtime island,修复自引用懒加载问题,主布局只保留轻接线。", - "timestamp": "2026-04-16T08:01:56Z" - } - ], - "completed_at": "2026-04-16T08:01:56Z" - }, - { - "id": "task-066", - "title": "执行长期路线 Phase 5:继续把 AI 面板收口为纯 Hermes / mnote Rust bridge island,统一最小会话、页面上下文、流式 token / tool event / client action 协议,消除残留重量级页面适配壳", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-057", - "task-058", - "task-062", - "task-061" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/api/ai-agent/run/route.ts src/app/api/ai-agent/client-tool-result/route.ts src/components/ai-agent src/components/editor/DocumentAiAgentPanel.tsx src/components/editor/blocks/MindmapAiAgentPanel.tsx src/components/onlyoffice/OnlyOfficeAiAgentPanel.tsx", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已将文档页/导图/OnlyOffice AI 面板收口为轻 host + runtime island,Hermes bridge 与 client action 主链保持不变。", - "timestamp": "2026-04-16T08:01:56Z" - } - ], - "completed_at": "2026-04-16T08:01:56Z" - }, - { - "id": "task-067", - "title": "执行长期路线 Phase 6:把 Mindmap 推进为独立 Rust 对象与独立页面能力,文档内嵌形态降级为轻预览或轻交互卡片,去掉对 BlockNote editor context 的强依赖", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-062", - "task-061", - "task-038" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/editor/blocks/MindmapBlock.tsx src/app/mindmap/[docId]/[mindmapId]/page.tsx src/app/api/mindmap/[docId]/route.ts src/app/api/mindmap/[docId]/[mindmapId]/route.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已把独立导图页切到 StandaloneMindmapView,不再依赖 BlockNote editor stub,并将文档内嵌形态收口为轻预览/轻交互入口。", - "timestamp": "2026-04-16T07:41:38Z" - } - ], - "completed_at": "2026-04-16T07:41:38Z" - }, - { - "id": "task-068", - "title": "执行长期路线 Phase 7:完成文档阅读态/编辑态分离与 BlockNote 孤岛化,让编辑器只在进入编辑态时挂载,并继续移出评论、历史、回链、AI、页面选项等外围初始化链", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-063", - "task-064", - "task-065", - "task-066", - "task-067" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/editor/blocknote-editor.tsx src/components/editor/document-content.tsx src/components/editor/document-shell.tsx src/app/(app)/documents/[id]/page.tsx", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已完成文档阅读态/编辑态分离与 BlockNote 孤岛化:默认阅读、显式进入编辑、grace unmount 与外围初始化链外移。", - "timestamp": "2026-04-16T08:01:56Z" - } - ], - "completed_at": "2026-04-16T08:01:56Z" - }, - { - "id": "task-069", - "title": "执行长期路线 Phase 8:清理旧 Next/React 页面壳与兼容 helper,完成 Rust Web 主路径切换、双栈收缩、最终验收与对外口径统一", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-062", - "task-063", - "task-064", - "task-065", - "task-066", - "task-067", - "task-068" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "4fbab6dd", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\nfor rel in ['design/rust-web-long-term-architecture-v1.md', 'design/rust-web-long-term-checklist-v1.md', 'harness-tasks.json']:\n assert (root / rel).exists(), rel\nprint('task-069-final-defined')\nPY2", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已统一回写长期 architecture/checklist 与 harness 状态,明确当前旧前端壳保留边界、实验入口边界与下一步 Phase 8 收缩口径。", - "timestamp": "2026-04-16T08:01:56Z" - } - ], - "completed_at": "2026-04-16T08:01:56Z" - }, - { - "id": "task-070", - "title": "执行 Tree-First Graph Kernel Phase 0:统一术语、事实源/投影/编辑器/外挂四层边界,并回写 tree-first 文档与 checklist 勾选", - "status": "completed", - "priority": "P0", - "depends_on": [], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "rg -n \"事实源 / 投影 / 编辑器 / 外挂|content node|reference edge|summary node|index node\" design/tree-first-graph-kernel-v1.md design/tree-first-graph-kernel-checklist-v1.md", - "timeout_seconds": 60 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已在 tree-first-graph-kernel-v1.md 冻结 node/edge/projection/subtree 等术语,并明确事实源/投影/编辑器/外挂四层边界与并入策略。", - "timestamp": "2026-04-16T09:30:55Z" - } - ], - "completed_at": "2026-04-16T09:30:55Z" - }, - { - "id": "task-071", - "title": "执行 Tree-First Graph Kernel Phase 1:在 Rust core-protocol 中落地统一 Node/Edge/Projection 模型、subtree/projection 标识与版本审计挂载", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-070" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cargo test -p core-protocol --manifest-path rust/Cargo.toml", - "timeout_seconds": 240 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已在 core-protocol 新增 kernel.rs,落地 KernelNode/KernelEdge/KernelProjectionRequest/KernelSubtreeRef/KernelAuditStamp 等统一类型。", - "timestamp": "2026-04-16T09:30:55Z" - } - ], - "completed_at": "2026-04-16T09:30:55Z" - }, - { - "id": "task-072", - "title": "执行 Tree-First Graph Kernel Phase 2:把 kernel 查询/写入协议接入 bridge-runtime,并用 sidebar dataset 落一条真实 subtree/projection 样板", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-071" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cargo test -p bridge-runtime --manifest-path rust/Cargo.toml", - "timeout_seconds": 360 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已在 bridge-runtime 接入 kernel.node.get/kernel.subtree.get/kernel.children.list/kernel.edges.list/kernel.graph.traverse/kernel.project_view 与对应写协议,并让 sidebar dataset 可投成统一 kernel subtree/projection 结果。", - "timestamp": "2026-04-16T09:30:55Z" - } - ], - "completed_at": "2026-04-16T09:30:55Z" - }, - { - "id": "task-073", - "title": "执行 Tree-First Graph Kernel Phase 3:让 mnote-web 提供 kernel query/subtree/projection route,补集成验证并回写 checklist 与 harness 勾选", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-072" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cargo test -p mnote-web --manifest-path rust/Cargo.toml && cargo check --manifest-path rust/Cargo.toml", - "timeout_seconds": 480 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已在 mnote-web 新增 /api/kernel/projections/sidebar、/api/kernel/subtree、/api/kernel/edges、/api/kernel/graph,并补路由测试与 checklist 回写。", - "timestamp": "2026-04-16T09:30:55Z" - } - ], - "completed_at": "2026-04-16T09:30:55Z" - }, - { - "id": "task-074", - "title": "按 Tree-First Graph 内核实施清单 v2 收尾 Kernel Phase 3:替换 mnote-web kernel route 中的 demo dataset,接入真实 workspace/storage/bridge 数据源,并让至少一条非 demo kernel 查询主链稳定运行", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-073" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/kernel.rs').read_text(encoding='utf-8')\nassert 'demo_sidebar_dataset' not in text\nprint('task-074-no-demo-dataset')\nPY2\ncargo test -p mnote-web --manifest-path rust/Cargo.toml", - "timeout_seconds": 900 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已删除 mnote-web kernel route 的 demo_sidebar_dataset,改为真实 sidebar.dataset.list -> kernel projection/subtree 归一化主链,并通过 cargo test -p mnote-web。", - "timestamp": "2026-04-16T11:52:00Z" - } - ], - "completed_at": "2026-04-16T11:52:00Z" - }, - { - "id": "task-075", - "title": "执行 Tree-First Graph Kernel Phase 4:让 Sidebar、页面树、文件树直接消费 kernel projection,移除主路径上的旧拼树逻辑,并把树交互收口为局部 UI 状态", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-074" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/sidebar-data.test.ts src/lib/file-tree/rows.test.ts && pnpm exec eslint src/components/sidebar/sidebar.tsx src/lib/sidebar-data.ts src/lib/server/sidebar-data.ts src/app/api/sidebar/route.ts src/hooks/use-convex-sidebar-data.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已让主 Sidebar、页面树与文件树切到 kernelSidebarProjection/kernelSidebarTree,主路径不再从 documents 调 buildDocumentTree,并通过 sidebar/file-tree 定向测试与 eslint(仅剩 warnings,无 error)。", - "timestamp": "2026-04-16T11:52:00Z" - } - ], - "completed_at": "2026-04-16T11:52:00Z" - }, - { - "id": "task-076", - "title": "执行 Tree-First Graph Kernel Phase 5:建立 cron 驱动的结构知识刷新与 kernel-aware 检索主链,把 summary node、ai_note node、index node、reference edge 与 BookMindmap/章节树接入统一索引", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-074" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cargo check --manifest-path rust/Cargo.toml && python3 - <<'PY2'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\nneedles = [\n root / 'design/tree-first-graph-kernel-checklist-v2.md',\n root / 'design/tree-first-graph-kernel-v1.md',\n]\nfor path in needles:\n assert path.exists(), path\nprint('task-076-kernel-aware-retrieval-planned')\nPY2", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已确认 cron 驱动的 kernel_aware_refresh 过渡链与搜索 nodeId/subtreeRootId/evidence 结果形状落地,并把 Kernel Phase 5 文档回写为 PARTIAL;cargo check 与定向 eslint 通过(仅 warnings,无 error)。", - "timestamp": "2026-04-16T13:18:26Z" - } - ], - "completed_at": "2026-04-16T13:18:27Z" - }, - { - "id": "task-077", - "title": "执行 Tree-First Graph Kernel Phase 6:把 Mindmap 从重前端对象页收口为 kernel subtree projection/editor,独立页读取 kernel projection,内嵌形态降为轻预览或轻编辑入口", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-075", - "task-076" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/editor/blocks/MindmapBlock.tsx src/app/mindmap/[docId]/[mindmapId]/page.tsx src/app/api/mindmap/[docId]/route.ts src/app/api/mindmap/[docId]/[mindmapId]/route.ts", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已确认独立导图页服务端 initial projection、StandaloneMindmapView 与 preview-first 内嵌入口已落地,并回写 Phase 6 文档;定向 eslint 通过(仅 warnings,无 error)。", - "timestamp": "2026-04-16T13:18:29Z" - } - ], - "completed_at": "2026-04-16T13:18:30Z" - }, - { - "id": "task-078", - "title": "执行 Tree-First Graph Kernel Phase 7:让文档阅读页与 AI 面板直接消费 page subtree projection 和 kernel-aware 检索结果,统一 node/subtree/edge 操作协议", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-075", - "task-076" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/app/(app)/documents/[id]/page.tsx src/components/editor/document-shell.tsx src/components/editor/document-content.tsx src/components/editor/document-read-view.tsx src/components/editor/DocumentAiAgentPanel.tsx src/components/ai-agent/GlobalAiAgentHost.tsx", - "timeout_seconds": 3600 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已确认 pageSubtree 阅读链与 AI node/subtree/outline/evidence Hermes 上下文链落地,并把 Kernel Phase 7 文档维持为 PARTIAL;定向 eslint 通过(仅 warnings,无 error)。", - "timestamp": "2026-04-16T13:18:32Z" - } - ], - "completed_at": "2026-04-16T13:18:33Z" - }, - { - "id": "task-079", - "title": "执行 Tree-First Graph Kernel Phase 8:定义 page subtree 与 content node 的边界,让 BlockNote 退化为内容节点编辑挂件,并继续拆走 DocumentContent 上的外围 panel/drawer", - "status": "completed", - "priority": "P1", - "depends_on": [ - "task-078" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec eslint src/components/editor/blocknote-editor.tsx src/components/editor/document-content.tsx src/components/editor/document-shell.tsx src/app/(app)/documents/[id]/page.tsx", - "timeout_seconds": 3000 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已回写 Phase 8 为 PARTIAL,明确 pageSubtree/content node 边界、阅读态先于编辑态与 BlockNote 按需挂载现状;定向 eslint 通过(仅 warnings,无 error)。", - "timestamp": "2026-04-16T13:18:35Z" - } - ], - "completed_at": "2026-04-16T13:18:36Z" - }, - { - "id": "task-080", - "title": "执行 Tree-First Graph Kernel Phase 9:在 kernel projection 成为主路径后收缩旧 Next/React 页面壳与旧对象模型,完成双栈切流、兼容层清理与最终架构口径统一", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-077", - "task-078", - "task-079" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "2c643e5f", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\nfor rel in [\n 'design/tree-first-graph-kernel-v1.md',\n 'design/tree-first-graph-kernel-checklist-v2.md',\n 'design/rust-web-long-term-checklist-v2.md',\n 'harness-tasks.json',\n]:\n assert (root / rel).exists(), rel\nprint('task-080-final-cutover-defined')\nPY2", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已统一回写 tree-first 与 rust-web long-term checklist,并同步 harness 状态,明确 Phase 9 仍 NOT_STARTED、当前不能宣称旧前端壳已下线;文档校验通过。", - "timestamp": "2026-04-16T13:18:38Z" - } - ], - "completed_at": "2026-04-16T13:18:39Z" - }, - { - "id": "task-081", - "title": "执行 Kernel Phase 3 P3-1/P3-2:把 mnote-web query transport 从 Sidebar 单点实现提升为通用 Convex query 执行层,并把 fixture fallback 收紧到测试/开发边界", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-080" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cargo test -p mnote-web --manifest-path rust/Cargo.toml && cargo check --manifest-path rust/Cargo.toml", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已把 mnote-web transport 收口为通用 execute_convex_query_plan,并以 allow_dev_fixtures + MNOTE_WEB_QUERY_FIXTURES_JSON 将 fixture 限定到测试/开发边界;mnote-web 定向 cargo test/check 通过。", - "timestamp": "2026-04-16T15:00:56Z" - } - ], - "completed_at": "2026-04-16T15:00:57Z" - }, - { - "id": "task-082", - "title": "执行 Kernel Phase 3 P3-3:为 mnote-web 新增 bridge.workspace/request/trace 真实查询主链,证明 Rust Web 不再只承接 sidebar 数据集", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-081" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cargo test -p mnote-web --manifest-path rust/Cargo.toml", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已新增 /api/bridge/workspace、/api/bridge/request、/api/bridge/trace,并复用通用 query transport 读取真实 bridgeLogs 查询;mnote-web route test 通过。", - "timestamp": "2026-04-16T15:00:59Z" - } - ], - "completed_at": "2026-04-16T15:01:00Z" - }, - { - "id": "task-083", - "title": "执行 Kernel Phase 3 P3-4/P3-5:补齐 workspace/request/trace/error 语义,并把 Sidebar 首包与 /api/sidebar 通过 compat sidebar 接到 mnote-web,形成第一条真实切流边界", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-081", - "task-082" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/sidebar-data.test.ts && pnpm exec eslint src/app/api/sidebar/route.ts src/lib/server/sidebar-data.ts src/lib/server/mnote-web.ts", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已统一 workspace 解析与错误码,transport 优先转发真实 Authorization;Sidebar 服务端首包与 /api/sidebar 在配置 MNOTE_WEB_BASE_URL 后默认经 /api/compat/next/sidebar 走 mnote-web。前端定向 vitest / eslint 通过(eslint 仅 baseline-browser-mapping 提示,无 error)。", - "timestamp": "2026-04-16T15:01:02Z" - } - ], - "completed_at": "2026-04-16T15:01:03Z" - }, - { - "id": "task-084", - "title": "执行 Kernel Phase 3 P3-6:回写 phase3 拆解/checklist 与 harness 状态,确认 Tree-First Graph Kernel Phase 3 已满足 DONE 判定", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-081", - "task-082", - "task-083" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\ncheck = (root / 'design/tree-first-graph-kernel-checklist-v2.md').read_text(encoding='utf-8')\nassert 'Kernel Phase 3`:`DONE`' in check or 'Kernel Phase 3:`DONE`' in check\nprint('task-084-phase3-docs-updated')\nPY2", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已回写 tree-first kernel checklist、phase3 breakdown 与 rust-web long-term checklist,统一 Phase 3 DONE / Rust Web Phase 1 仍 PARTIAL 的真实口径,并同步 harness 完成状态。", - "timestamp": "2026-04-16T15:01:04Z" - } - ], - "completed_at": "2026-04-16T15:01:05Z" - }, - { - "id": "task-085", - "title": "执行 Kernel Phase 4 P4-1:冻结树域 page_tree projection protocol,并补齐 Sidebar / 页面树共享字段与类型边界", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-084" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/tree-projection.test.ts src/lib/sidebar-data.test.ts", - "timeout_seconds": 1200 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已新增 tree-projection.ts,冻结 rowId/nodeId/parentNodeId/projectionKind/depth/position/capabilities/resourceMeta 等 page_tree protocol,并补充对应单测。", - "timestamp": "2026-04-16T16:11:20Z" - } - ], - "completed_at": "2026-04-16T16:11:21Z" - }, - { - "id": "task-086", - "title": "执行 Kernel Phase 4 P4-2:统一 Sidebar 主树与页面树 consumer,让 PrivateTree 与相关树列表直接消费 page_tree projection", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-085" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/tree-projection.test.ts src/lib/sidebar-data.test.ts && pnpm exec eslint src/components/sidebar/private-tree.tsx src/components/sidebar/sidebar.tsx src/lib/tree-projection.ts", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已让 Sidebar 顶部树列表、PrivateTree 与 convert-to-child 等页面树 consumer 统一消费 page_tree projection,可见行只保留 expanded 等 UI 状态。", - "timestamp": "2026-04-16T16:11:23Z" - } - ], - "completed_at": "2026-04-16T16:11:24Z" - }, - { - "id": "task-087", - "title": "执行 Kernel Phase 4 P4-3:把文件树收口为统一 projection consumer,让 buildVisibleRows 只负责 projection 到 visible rows 的映射", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-085", - "task-086" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/file-tree/rows.test.ts src/lib/tree-projection.test.ts && pnpm exec eslint src/lib/file-tree/rows.ts src/lib/file-tree/rows.test.ts src/components/sidebar/sidebar.tsx", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "文件树已改为只消费 page_tree projection + asset 映射;buildVisibleRows 不再递归定义树结构真相,相关 rows 单测通过。", - "timestamp": "2026-04-16T16:11:26Z" - } - ], - "completed_at": "2026-04-16T16:11:27Z" - }, - { - "id": "task-088", - "title": "执行 Kernel Phase 4 P4-4:统一 move/embed picker 等兼容树域,结束 documents[] 到 buildDocumentTree 的双轨主路径", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-085", - "task-086" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/tree-projection.test.ts src/lib/sidebar-data.test.ts && pnpm exec eslint src/components/documents/move-embed-picker-dialog.tsx src/lib/tree-projection.ts", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "move-embed picker 空查询态已直接消费 kernelSidebarTree -> page_tree projection,不再调用 buildDocumentTree(...)。", - "timestamp": "2026-04-16T16:11:29Z" - } - ], - "completed_at": "2026-04-16T16:11:30Z" - }, - { - "id": "task-089", - "title": "执行 Kernel Phase 4 P4-5:收敛旧 helper 与兼容逻辑,把 buildDocumentTree 退回兼容边界并收紧 sidebar-data 主路径投影要求", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-087", - "task-088" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/sidebar-data.test.ts src/lib/tree-projection.test.ts src/lib/file-tree/rows.test.ts && pnpm exec eslint src/lib/sidebar-data.ts src/components/sidebar/types.ts src/lib/documents.ts", - "timeout_seconds": 1800 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已将 SidebarInitialData 的 kernelSidebarProjection/kernelSidebarTree 收紧为主路径必备字段,并给 buildDocumentTree 标明仅保留兼容 helper 边界。", - "timestamp": "2026-04-16T16:11:32Z" - } - ], - "completed_at": "2026-04-16T16:11:33Z" - }, - { - "id": "task-090", - "title": "执行 Kernel Phase 4 P4-6:回写 Phase 4 checklist/breakdown 与 harness 状态,确认树域 consumer/protocol 已满足 DONE 判定并交接下一阶段 Rust Web tree shell 重构", - "status": "completed", - "priority": "P0", - "depends_on": [ - "task-085", - "task-086", - "task-087", - "task-088", - "task-089" - ], - "attempts": 1, - "max_attempts": 3, - "started_at_commit": "17afde7d", - "validation": { - "command": "python3 - <<'PY2'\nfrom pathlib import Path\nroot = Path('/mnt/Data1T/mnote')\ncheck = (root / 'design/tree-first-graph-kernel-checklist-v2.md').read_text(encoding='utf-8')\nbreakdown = (root / 'design/tree-first-graph-kernel-phase4-task-breakdown-v1.md').read_text(encoding='utf-8')\nassert 'Kernel Phase 4`:`DONE' in check or 'Kernel Phase 4:`DONE`' in check\nassert 'P4-6:Phase 4 验收矩阵与下一阶段交接' in breakdown\nassert 'sidebar-pagetree-filetree-rust-web-rebuild-v1.md' in breakdown\nprint('task-090-phase4-docs-updated')\nPY2", - "timeout_seconds": 120 - }, - "on_failure": { - "cleanup": null - }, - "error_log": [], - "checkpoints": [ - { - "step": 1, - "total": 1, - "description": "已回写 Phase 4 checklist/breakdown 与 harness 状态,统一 Phase 4 DONE 口径,并明确下一阶段才是 sidebar-pagetree-filetree-rust-web-rebuild-v1.md。", - "timestamp": "2026-04-16T16:11:35Z" - } - ], - "completed_at": "2026-04-16T16:11:36Z" + "completed_at": "2026-04-17T08:56:30Z" } ], - "session_count": 33, - "last_session": "2026-04-16T16:11:37Z" + "session_count": 2, + "last_session": "2026-04-17T08:56:30Z" } diff --git a/rust/crates/bridge-runtime/src/lib.rs b/rust/crates/bridge-runtime/src/lib.rs index 25125bb9..2c3e5b10 100644 --- a/rust/crates/bridge-runtime/src/lib.rs +++ b/rust/crates/bridge-runtime/src/lib.rs @@ -3362,6 +3362,12 @@ fn normalize_kernel_node_from_record( if let Some(is_starred) = bool_field(value, "is_starred").or_else(|| bool_field(value, "isStarred")) { extra.insert("isStarred".into(), json!(is_starred)); } + if let Some(sort_order) = record_field(value, "sort_order") + .and_then(|field| field.as_i64().or_else(|| field.as_f64().map(|number| number as i64))) + .or_else(|| record_field(value, "sortOrder").and_then(Value::as_i64)) + { + extra.insert("sortOrder".into(), json!(sort_order)); + } let content = value .get("content") @@ -5712,7 +5718,9 @@ mod tests { assert_eq!(result["projection"], json!("sidebar_tree")); assert_eq!(result["rootNodeId"], json!("page_root")); assert_eq!(result["items"][0]["nodeId"], json!("page_root")); + assert_eq!(result["items"][0]["position"], json!(0)); assert_eq!(result["items"][1]["parentNodeId"], json!("page_root")); + assert_eq!(result["items"][1]["position"], json!(1)); } #[test] diff --git a/rust/crates/mnote-web/src/app.rs b/rust/crates/mnote-web/src/app.rs index 4675a708..571d7ffb 100644 --- a/rust/crates/mnote-web/src/app.rs +++ b/rust/crates/mnote-web/src/app.rs @@ -2,6 +2,7 @@ use crate::middleware::request_context::inject_request_context; use crate::routes::build_router; use axum::Router; use std::env; +use std::fs; use std::sync::Arc; use tower_http::trace::TraceLayer; @@ -15,6 +16,8 @@ pub struct AppConfig { pub convex_url: Option, pub convex_admin_key: Option, pub allow_dev_fixtures: bool, + pub query_fixtures_json: Option, + pub mutation_fixtures_json: Option, pub dev_user_id: String, pub dev_user_name: String, pub dev_user_email: String, @@ -34,23 +37,74 @@ impl AppConfig { convex_url: env::var("CONVEX_SELF_HOSTED_URL") .ok() .or_else(|| env::var("NEXT_PUBLIC_CONVEX_URL").ok()) + .or_else(|| read_env_or_dotenv("CONVEX_SELF_HOSTED_URL")) + .or_else(|| read_env_or_dotenv("NEXT_PUBLIC_CONVEX_URL")) .map(|value| value.trim().trim_end_matches('/').to_string()) .filter(|value| !value.is_empty()), convex_admin_key: env::var("CONVEX_SELF_HOSTED_ADMIN_KEY") .ok() + .or_else(|| read_env_or_dotenv("CONVEX_SELF_HOSTED_ADMIN_KEY")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), allow_dev_fixtures: env::var("MNOTE_WEB_ALLOW_DEV_FIXTURES") .ok() .map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES")) .unwrap_or(false), - dev_user_id: env::var("DEV_USER_ID").unwrap_or_else(|_| "dev-user".into()), - dev_user_name: env::var("DEV_USER_NAME").unwrap_or_else(|_| "开发用户".into()), - dev_user_email: env::var("DEV_USER_EMAIL").unwrap_or_else(|_| "dev@mnote.local".into()), + query_fixtures_json: env::var("MNOTE_WEB_QUERY_FIXTURES_JSON") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()), + mutation_fixtures_json: env::var("MNOTE_WEB_MUTATION_FIXTURES_JSON") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()), + dev_user_id: env::var("DEV_USER_ID") + .ok() + .or_else(|| read_env_or_dotenv("DEV_USER_ID")) + .unwrap_or_else(|| "dev-user".into()), + dev_user_name: env::var("DEV_USER_NAME") + .ok() + .or_else(|| read_env_or_dotenv("DEV_USER_NAME")) + .unwrap_or_else(|| "开发用户".into()), + dev_user_email: env::var("DEV_USER_EMAIL") + .ok() + .or_else(|| read_env_or_dotenv("DEV_USER_EMAIL")) + .unwrap_or_else(|| "dev@mnote.local".into()), } } } +fn read_env_or_dotenv(key: &str) -> Option { + if let Ok(value) = env::var(key) { + let trimmed = value.trim().trim_matches('"').to_string(); + if !trimmed.is_empty() { + return Some(trimmed); + } + } + + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../..") + .join(".env.all"); + let content = fs::read_to_string(root).ok()?; + for line in content.lines() { + let line = line.trim_end_matches('\r'); + if line.starts_with('#') || line.trim().is_empty() { + continue; + } + let Some((k, v)) = line.split_once('=') else { + continue; + }; + if k.trim() != key { + continue; + } + let trimmed = v.trim().trim_matches('"').to_string(); + if !trimmed.is_empty() { + return Some(trimmed); + } + } + None +} + #[derive(Debug, Clone)] pub struct AppState { config: Arc, diff --git a/rust/crates/mnote-web/src/routes/bridge.rs b/rust/crates/mnote-web/src/routes/bridge.rs index 3f3c7001..870cf8ee 100644 --- a/rust/crates/mnote-web/src/routes/bridge.rs +++ b/rust/crates/mnote-web/src/routes/bridge.rs @@ -73,13 +73,13 @@ fn workspace_query_payload( } } -fn execute_bridge_query( +async fn execute_bridge_query( config: &AppConfig, context: &RequestContext, workspace_id: &str, query: RuntimeQueryEnvelopeWire, ) -> Result { - execute_runtime_query_via_convex(config, context, Some(workspace_id), query) + execute_runtime_query_via_convex(config, context, Some(workspace_id), query).await } pub async fn workspace( @@ -95,7 +95,8 @@ pub async fn workspace( &context, &effective_workspace_id, workspace_query_payload(&effective_workspace_id, &query), - )?; + ) + .await?; Ok(ok_response(&context, result)) } @@ -119,7 +120,8 @@ pub async fn request( "commandId": query.command_id, }), }, - )?; + ) + .await?; Ok(ok_response(&context, result)) } @@ -143,7 +145,8 @@ pub async fn trace( "commandId": query.command_id, }), }, - )?; + ) + .await?; Ok(ok_response(&context, result)) } @@ -155,10 +158,6 @@ mod tests { use tower::util::ServiceExt; fn app() -> axum::Router { - std::env::set_var( - "MNOTE_WEB_QUERY_FIXTURES_JSON", - r#"{"sidebar:datasetList":{"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}]},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#, - ); build_app(AppState::new(AppConfig { service_name: "mnote-web".into(), service_version: "0.1.0".into(), @@ -168,6 +167,8 @@ mod tests { convex_url: None, convex_admin_key: None, allow_dev_fixtures: true, + query_fixtures_json: Some(r#"{"sidebar:datasetList":{"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}]},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()), + mutation_fixtures_json: None, dev_user_id: "dev-user".into(), dev_user_name: "开发用户".into(), dev_user_email: "dev@mnote.local".into(), diff --git a/rust/crates/mnote-web/src/routes/command_support.rs b/rust/crates/mnote-web/src/routes/command_support.rs new file mode 100644 index 00000000..78e3e3c0 --- /dev/null +++ b/rust/crates/mnote-web/src/routes/command_support.rs @@ -0,0 +1,110 @@ +use crate::app::AppConfig; +use crate::context::RequestContext; +use crate::error::WebError; +use crate::transport::convex::execute_convex_command_plan; +use bridge_runtime::{ + execute_runtime_input, RuntimeActorWire, RuntimeBridgeContextWire, RuntimeCommandEnvelopeWire, + RuntimeCommandExecutionPlan, RuntimeExecutionPlan, RuntimeInput, RuntimeSourceWire, + RuntimeTargetWire, +}; +use serde_json::Value; + +pub fn runtime_context( + context: &RequestContext, + effective_workspace_id: Option<&str>, +) -> RuntimeBridgeContextWire { + RuntimeBridgeContextWire { + deployment_id: context.workspace.deployment_id.clone(), + project_id: context.workspace.project_id.clone(), + workspace_id: effective_workspace_id + .map(ToOwned::to_owned) + .or_else(|| context.workspace.workspace_id.clone()), + request_id: context.trace.request_id.clone(), + trace_id: context.trace.trace_id.clone(), + actor: RuntimeActorWire { + actor_type: context.auth.actor_type.clone(), + actor_id: context.auth.actor_id.clone(), + session_id: context.auth.session_id.clone(), + }, + source: RuntimeSourceWire { + channel: context.source.channel.clone(), + client: context.source.client.clone(), + }, + tenant_id: context.workspace.tenant_id.clone(), + auth_token: context.auth.authorization.clone(), + idempotency_key: context.source.idempotency_key.clone(), + validate_only: false, + dry_run: false, + } +} + +pub fn build_runtime_command_plan( + context: &RequestContext, + effective_workspace_id: Option<&str>, + command: RuntimeCommandEnvelopeWire, +) -> Result { + let runtime_input = RuntimeInput::Command { + context: runtime_context(context, effective_workspace_id), + command, + }; + let RuntimeExecutionPlan::Command(plan) = execute_runtime_input(runtime_input) + .map_err(|error| WebError::bad_request(error.message).with_context(context))? + else { + return Err(WebError::internal("runtime command 未返回 command plan").with_context(context)); + }; + + Ok(plan) +} + +pub async fn execute_runtime_command_via_convex( + config: &AppConfig, + context: &RequestContext, + effective_workspace_id: Option<&str>, + command: RuntimeCommandEnvelopeWire, +) -> Result { + let plan = build_runtime_command_plan(context, effective_workspace_id, command)?; + execute_convex_command_plan(config, context, &plan).await +} + +pub fn build_tree_target( + workspace_id: &str, + page_id: Option<&str>, + block_id: Option<&str>, +) -> RuntimeTargetWire { + RuntimeTargetWire { + workspace_id: Some(workspace_id.to_string()), + page_id: page_id.map(ToOwned::to_owned), + block_id: block_id.map(ToOwned::to_owned), + } +} + +pub fn ensure_non_empty(value: &str, field: &'static str, context: &RequestContext) -> Result { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err(WebError::bad_request_code( + "tree_command_validation", + format!("{field} 不能为空"), + ) + .with_context(context) + .with_header("x-error-phase", "tree_command_validate")); + } + Ok(trimmed.to_string()) +} + +pub fn read_optional_non_empty(value: Option) -> Option { + value + .map(|item| item.trim().to_string()) + .filter(|item| !item.is_empty()) +} + +pub fn ensure_sort_order(sort_order: i64, context: &RequestContext) -> Result { + if sort_order < 0 { + return Err(WebError::bad_request_code( + "tree_command_validation", + "sortOrder 不能小于 0", + ) + .with_context(context) + .with_header("x-error-phase", "tree_command_validate")); + } + Ok(sort_order) +} diff --git a/rust/crates/mnote-web/src/routes/compat.rs b/rust/crates/mnote-web/src/routes/compat.rs index a28aadb5..324622cf 100644 --- a/rust/crates/mnote-web/src/routes/compat.rs +++ b/rust/crates/mnote-web/src/routes/compat.rs @@ -69,7 +69,8 @@ pub async fn next_sidebar( "workspaceId": effective_workspace_id, }), }, - )?; + ) + .await?; let projection = execute_runtime_query_against_data( &context, Some(&effective_workspace_id), @@ -115,10 +116,6 @@ mod tests { use tower::util::ServiceExt; fn app() -> axum::Router { - std::env::set_var( - "MNOTE_WEB_QUERY_FIXTURES_JSON", - r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","workspaces":[],"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"trashed_documents":[],"media_assets":[],"trashed_media_assets":[],"mindmap_assets":[],"trashed_mindmap_assets":[],"table_assets":[],"trashed_table_assets":[],"mindmap_docs":[],"mindmap_asset_children":{}},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[],"domain_events":[],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#, - ); build_app(AppState::new(AppConfig { service_name: "mnote-web".into(), service_version: "0.1.0".into(), @@ -128,6 +125,8 @@ mod tests { convex_url: None, convex_admin_key: None, allow_dev_fixtures: true, + query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","workspaces":[],"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"trashed_documents":[],"media_assets":[],"trashed_media_assets":[],"mindmap_assets":[],"trashed_mindmap_assets":[],"table_assets":[],"trashed_table_assets":[],"mindmap_docs":[],"mindmap_asset_children":{}},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[],"domain_events":[],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()), + mutation_fixtures_json: None, dev_user_id: "dev-user".into(), dev_user_name: "开发用户".into(), dev_user_email: "dev@mnote.local".into(), diff --git a/rust/crates/mnote-web/src/routes/kernel.rs b/rust/crates/mnote-web/src/routes/kernel.rs index 72aff186..4ea1a007 100644 --- a/rust/crates/mnote-web/src/routes/kernel.rs +++ b/rust/crates/mnote-web/src/routes/kernel.rs @@ -52,7 +52,7 @@ fn sidebar_dataset_query(workspace_id: &str) -> RuntimeQueryEnvelopeWire { } } -fn load_sidebar_dataset( +async fn load_sidebar_dataset( config: &AppConfig, context: &RequestContext, workspace_id: &str, @@ -63,6 +63,7 @@ fn load_sidebar_dataset( Some(workspace_id), sidebar_dataset_query(workspace_id), ) + .await } fn execute_kernel_query( @@ -94,7 +95,7 @@ pub async fn project_sidebar( let effective_workspace_id = resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)? .expect("workspace_required 已确保存在"); - let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id)?; + let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id).await?; let result = execute_kernel_query( &context, &effective_workspace_id, @@ -124,7 +125,7 @@ pub async fn subtree( let effective_workspace_id = resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)? .expect("workspace_required 已确保存在"); - let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id)?; + let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id).await?; let result = execute_kernel_query( &context, &effective_workspace_id, @@ -151,7 +152,7 @@ pub async fn edges( let effective_workspace_id = resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)? .expect("workspace_required 已确保存在"); - let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id)?; + let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id).await?; let result = execute_kernel_query( &context, &effective_workspace_id, @@ -176,7 +177,7 @@ pub async fn graph( let effective_workspace_id = resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)? .expect("workspace_required 已确保存在"); - let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id)?; + let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id).await?; let result = execute_kernel_query( &context, &effective_workspace_id, @@ -202,10 +203,6 @@ mod tests { use tower::util::ServiceExt; fn app() -> axum::Router { - std::env::set_var( - "MNOTE_WEB_QUERY_FIXTURES_JSON", - r#"{"sidebar:datasetList":{"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}]},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#, - ); build_app(AppState::new(AppConfig { service_name: "mnote-web".into(), service_version: "0.1.0".into(), @@ -215,6 +212,8 @@ mod tests { convex_url: None, convex_admin_key: None, allow_dev_fixtures: true, + query_fixtures_json: Some(r#"{"sidebar:datasetList":{"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}]},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()), + mutation_fixtures_json: None, dev_user_id: "dev-user".into(), dev_user_name: "开发用户".into(), dev_user_email: "dev@mnote.local".into(), diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index ff169c4a..ada3c185 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -1,10 +1,12 @@ mod bridge; +mod command_support; mod compat; mod health; mod hermes; mod kernel; mod query_support; mod sse; +mod tree; mod ws; use crate::app::AppState; @@ -17,6 +19,7 @@ pub fn build_router(state: AppState) -> Router { Router::new() .route("/health", get(health::health)) + .route("/tree", get(tree::tree_shell)) .route( "/api/kernel/projections/sidebar", get(kernel::project_sidebar), @@ -24,6 +27,7 @@ pub fn build_router(state: AppState) -> Router { .route("/api/kernel/subtree", get(kernel::subtree)) .route("/api/kernel/edges", get(kernel::edges)) .route("/api/kernel/graph", get(kernel::graph)) + .route("/api/tree/commands", post(tree::tree_command)) .route("/api/bridge/workspace", get(bridge::workspace)) .route("/api/bridge/request", get(bridge::request)) .route("/api/bridge/trace", get(bridge::trace)) diff --git a/rust/crates/mnote-web/src/routes/query_support.rs b/rust/crates/mnote-web/src/routes/query_support.rs index 9ad9eb9d..72aee20b 100644 --- a/rust/crates/mnote-web/src/routes/query_support.rs +++ b/rust/crates/mnote-web/src/routes/query_support.rs @@ -96,14 +96,14 @@ pub fn build_runtime_query_plan( Ok(plan) } -pub fn fetch_query_data_via_convex( +pub async fn fetch_query_data_via_convex( config: &AppConfig, context: &RequestContext, effective_workspace_id: Option<&str>, query: RuntimeQueryEnvelopeWire, ) -> Result { let plan = build_runtime_query_plan(context, effective_workspace_id, query)?; - execute_convex_query_plan(config, context, &plan) + execute_convex_query_plan(config, context, &plan).await } pub fn execute_runtime_query_against_data( @@ -120,12 +120,12 @@ pub fn execute_runtime_query_against_data( .map_err(|error| WebError::bad_request(error.message).with_context(context)) } -pub fn execute_runtime_query_via_convex( +pub async fn execute_runtime_query_via_convex( config: &AppConfig, context: &RequestContext, effective_workspace_id: Option<&str>, query: RuntimeQueryEnvelopeWire, ) -> Result { - let data = fetch_query_data_via_convex(config, context, effective_workspace_id, query.clone())?; + let data = fetch_query_data_via_convex(config, context, effective_workspace_id, query.clone()).await?; execute_runtime_query_against_data(context, effective_workspace_id, query, data) } diff --git a/rust/crates/mnote-web/src/routes/tree.rs b/rust/crates/mnote-web/src/routes/tree.rs new file mode 100644 index 00000000..0b14507e --- /dev/null +++ b/rust/crates/mnote-web/src/routes/tree.rs @@ -0,0 +1,2384 @@ +use crate::app::AppState; +use crate::context::RequestContext; +use crate::error::WebError; +use crate::routes::command_support::{ + build_tree_target, ensure_non_empty, ensure_sort_order, execute_runtime_command_via_convex, + read_optional_non_empty, +}; +use crate::routes::query_support::{ + execute_runtime_query_against_data, fetch_query_data_via_convex, resolve_effective_workspace_id, +}; +use axum::extract::{Extension, Query, State}; +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::{Html, IntoResponse, Response}; +use axum::Json; +use bridge_runtime::RuntimeCommandEnvelopeWire; +use bridge_runtime::RuntimeQueryEnvelopeWire; +use core_protocol::KernelProjectionKind; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +static TREE_DOCUMENT_COUNTER: AtomicU64 = AtomicU64::new(1); + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TreeShellQuery { + pub workspace_id: Option, + pub root_node_id: Option, + pub depth: Option, + pub active_document_id: Option, + pub actor_id: Option, + pub channel: Option, + pub host: Option, + pub mode: Option, + pub allow_root_pick: Option, + pub exclude_ids: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TreeCommandEnvelope { + pub action: String, + pub workspace_id: Option, + pub document_id: Option, + pub parent_id: Option, + pub title: Option, + pub access_scope: Option, + pub content: Option, + pub sort_order: Option, +} + +#[derive(Debug)] +pub enum TreeCommandRequest { + Create { + workspace_id: Option, + document_id: String, + parent_id: Option, + title: String, + access_scope: Option, + content: Option, + }, + Rename { + workspace_id: Option, + document_id: String, + title: String, + }, + Move { + workspace_id: Option, + document_id: String, + parent_id: Option, + sort_order: i64, + }, +} + +fn tree_projection_query( + workspace_id: &str, + root_node_id: Option<&str>, + depth: Option, + mode: &str, +) -> RuntimeQueryEnvelopeWire { + RuntimeQueryEnvelopeWire { + name: "kernel.project_view".into(), + payload: json!({ + "projection": if mode == "filetree" { + KernelProjectionKind::FileTree + } else { + KernelProjectionKind::PageTree + }, + "workspaceId": workspace_id, + "rootNodeId": root_node_id, + "depth": depth, + "includeEdges": true, + "includeContent": false, + "nodeTypes": ["page"], + }), + } +} + +fn sidebar_dataset_query(workspace_id: &str) -> RuntimeQueryEnvelopeWire { + RuntimeQueryEnvelopeWire { + name: "sidebar.dataset.list".into(), + payload: json!({ + "workspaceId": workspace_id, + }), + } +} + +fn escape_html(input: &str) -> String { + input + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +fn escape_inline_json(input: &str) -> String { + input + .replace('&', "\\u0026") + .replace('<', "\\u003c") + .replace('>', "\\u003e") +} + +fn normalize_title(value: Option) -> String { + let trimmed = value.unwrap_or_default().trim().to_string(); + if trimmed.is_empty() { + "无标题".into() + } else { + trimmed + } +} + +fn normalize_channel(value: Option) -> String { + let trimmed = value.unwrap_or_default().trim().to_string(); + if trimmed.is_empty() { + "mnote-tree-shell-v1".into() + } else { + trimmed + } +} + +fn normalize_tree_mode(value: Option<&str>) -> &'static str { + match value.unwrap_or_default().trim() { + "picker" => "picker", + "filetree" => "filetree", + _ => "page", + } +} + +fn normalize_bool_flag(value: Option<&str>, default: bool) -> bool { + match value.unwrap_or_default().trim().to_ascii_lowercase().as_str() { + "1" | "true" | "yes" | "on" => true, + "0" | "false" | "no" | "off" => false, + _ => default, + } +} + +fn parse_exclude_ids(value: Option<&str>) -> Vec { + value + .unwrap_or_default() + .split(',') + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(ToOwned::to_owned) + .collect() +} + +fn override_actor_context(context: &RequestContext, actor_id: Option<&str>) -> RequestContext { + let mut next = context.clone(); + let actor_id = actor_id + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned); + if let Some(actor_id) = actor_id { + next.auth.actor_id = actor_id; + next.auth.actor_type = "user".into(); + } + next +} + +fn generate_tree_document_id() -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + let counter = TREE_DOCUMENT_COUNTER.fetch_add(1, Ordering::Relaxed); + format!("tree_{millis}_{counter}") +} + +fn build_tree_shell_html( + workspace_id: &str, + root_node_id: Option<&str>, + active_document_id: Option<&str>, + channel: &str, + host: Option<&str>, + context: &RequestContext, + projection: &Value, + mode: &str, + allow_root_pick: bool, + exclude_ids: &[String], + dataset: &Value, +) -> String { + let app_state = json!({ + "workspaceId": workspace_id, + "rootNodeId": root_node_id, + "activeDocumentId": active_document_id, + "actorId": context.auth.actor_id, + "channel": channel, + "host": host, + "mode": mode, + "allowRootPick": allow_root_pick, + "excludeIds": exclude_ids, + "requestId": context.trace.request_id, + "traceId": context.trace.trace_id, + "commandPath": "/api/tree/commands", + "items": projection.get("items").cloned().unwrap_or_else(|| Value::Array(Vec::new())), + "mediaAssets": dataset.get("media_assets").cloned().unwrap_or_else(|| Value::Array(Vec::new())), + "mindmapAssets": dataset.get("mindmap_assets").cloned().unwrap_or_else(|| Value::Array(Vec::new())), + "tableAssets": dataset.get("table_assets").cloned().unwrap_or_else(|| Value::Array(Vec::new())), + "mindmapAssetChildren": dataset.get("mindmap_asset_children").cloned().unwrap_or_else(|| json!({})), + }); + let app_state_json = serde_json::to_string(&app_state).unwrap_or_else(|_| "{}".into()); + let projection_json = serde_json::to_string_pretty(projection).unwrap_or_else(|_| "{}".into()); + let root_label = root_node_id.unwrap_or("workspace_root"); + let active_label = active_document_id.unwrap_or("未指定"); + let template = r##" + + + + + mnote Tree Shell + + + +
+
+
+
Sidebar / Page Tree
+
workspace=__WORKSPACE_ID__ · root=__ROOT_LABEL__ · active=__ACTIVE_LABEL__
+
+ +
+
+
+
+ + +
+ + + + + +"##; + + template + .replace("__WORKSPACE_ID__", &escape_html(workspace_id)) + .replace("__ROOT_LABEL__", &escape_html(root_label)) + .replace("__ACTIVE_LABEL__", &escape_html(active_label)) + .replace("__PROJECTION_JSON__", &escape_html(&projection_json)) + .replace("__APP_STATE__", &escape_inline_json(&app_state_json)) +} + +fn json_response(context: &RequestContext, result: Value) -> (StatusCode, Json) { + ( + StatusCode::OK, + Json(json!({ + "ok": true, + "requestId": context.trace.request_id, + "traceId": context.trace.trace_id, + "result": result, + })), + ) +} + +pub async fn tree_shell( + State(state): State, + Extension(context): Extension, + Query(query): Query, +) -> Result { + let effective_context = override_actor_context(&context, query.actor_id.as_deref()); + let effective_workspace_id = + resolve_effective_workspace_id(&effective_context, query.workspace_id.as_deref(), true)? + .expect("workspace_required 已确保存在"); + let mode = normalize_tree_mode(query.mode.as_deref()); + let allow_root_pick = normalize_bool_flag(query.allow_root_pick.as_deref(), false); + let exclude_ids = parse_exclude_ids(query.exclude_ids.as_deref()); + let dataset = fetch_query_data_via_convex( + state.config(), + &effective_context, + Some(&effective_workspace_id), + sidebar_dataset_query(&effective_workspace_id), + ) + .await?; + let projection = execute_runtime_query_against_data( + &effective_context, + Some(&effective_workspace_id), + tree_projection_query( + &effective_workspace_id, + query.root_node_id.as_deref(), + query.depth, + mode, + ), + dataset.clone(), + )?; + let html = build_tree_shell_html( + &effective_workspace_id, + query.root_node_id.as_deref(), + query.active_document_id.as_deref(), + &normalize_channel(query.channel), + query.host.as_deref(), + &effective_context, + &projection, + mode, + allow_root_pick, + &exclude_ids, + &dataset, + ); + let mut response = Html(html).into_response(); + response + .headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8")); + Ok(response) +} + +fn create_command_wire( + context: &RequestContext, + workspace_id: &str, + request: TreeCommandRequest, +) -> Result { + match request { + TreeCommandRequest::Create { + workspace_id: _, + document_id, + parent_id, + title, + access_scope, + content, + } => { + let document_id = ensure_non_empty(&document_id, "documentId", context)?; + let parent_id = read_optional_non_empty(parent_id); + let access_scope = read_optional_non_empty(access_scope).unwrap_or_else(|| "private".into()); + Ok(RuntimeCommandEnvelopeWire { + name: "documents.create".into(), + command_id: format!("tree_create_{}", context.trace.request_id), + idempotency_key: context.source.idempotency_key.clone(), + actor: bridge_runtime::RuntimeActorWire { + actor_type: context.auth.actor_type.clone(), + actor_id: context.auth.actor_id.clone(), + session_id: context.auth.session_id.clone(), + }, + source: bridge_runtime::RuntimeSourceWire { + channel: context.source.channel.clone(), + client: context.source.client.clone(), + }, + target: Some(build_tree_target(workspace_id, Some(document_id.as_str()), None)), + payload: json!({ + "documentId": document_id, + "workspaceId": workspace_id, + "parentId": parent_id, + "title": title, + "accessScope": access_scope, + "content": content.unwrap_or_else(|| Value::Array(Vec::new())), + }), + reason: Some("tree-shell create".into()), + refs: vec!["mnote-web-tree".into()], + dry_run: false, + validate_only: false, + }) + } + TreeCommandRequest::Rename { + workspace_id: _, + document_id, + title, + } => { + let document_id = ensure_non_empty(&document_id, "documentId", context)?; + Ok(RuntimeCommandEnvelopeWire { + name: "documents.title.update".into(), + command_id: format!("tree_rename_{}", context.trace.request_id), + idempotency_key: context.source.idempotency_key.clone(), + actor: bridge_runtime::RuntimeActorWire { + actor_type: context.auth.actor_type.clone(), + actor_id: context.auth.actor_id.clone(), + session_id: context.auth.session_id.clone(), + }, + source: bridge_runtime::RuntimeSourceWire { + channel: context.source.channel.clone(), + client: context.source.client.clone(), + }, + target: Some(build_tree_target(workspace_id, Some(document_id.as_str()), None)), + payload: json!({ + "documentId": document_id, + "title": title, + }), + reason: Some("tree-shell rename".into()), + refs: vec!["mnote-web-tree".into()], + dry_run: false, + validate_only: false, + }) + } + TreeCommandRequest::Move { + workspace_id: _, + document_id, + parent_id, + sort_order, + } => { + let document_id = ensure_non_empty(&document_id, "documentId", context)?; + let sort_order = ensure_sort_order(sort_order, context)?; + let parent_id = read_optional_non_empty(parent_id); + Ok(RuntimeCommandEnvelopeWire { + name: "documents.move".into(), + command_id: format!("tree_move_{}", context.trace.request_id), + idempotency_key: context.source.idempotency_key.clone(), + actor: bridge_runtime::RuntimeActorWire { + actor_type: context.auth.actor_type.clone(), + actor_id: context.auth.actor_id.clone(), + session_id: context.auth.session_id.clone(), + }, + source: bridge_runtime::RuntimeSourceWire { + channel: context.source.channel.clone(), + client: context.source.client.clone(), + }, + target: Some(build_tree_target(workspace_id, Some(document_id.as_str()), None)), + payload: json!({ + "documentId": document_id, + "parentId": parent_id, + "sortOrder": sort_order, + }), + reason: Some("tree-shell move".into()), + refs: vec!["mnote-web-tree".into()], + dry_run: false, + validate_only: false, + }) + } + } +} + +pub async fn tree_command( + State(state): State, + Extension(context): Extension, + body: String, +) -> Result<(StatusCode, Json), WebError> { + let raw_request: TreeCommandEnvelope = serde_json::from_str(&body).map_err(|error| { + WebError::bad_request_code( + "tree_command_invalid_json", + format!("tree command 请求体非法: {error}"), + ) + .with_context(&context) + .with_header("x-error-phase", "tree_command_decode") + })?; + let request = match raw_request.action.trim() { + "create" => TreeCommandRequest::Create { + workspace_id: raw_request.workspace_id, + document_id: read_optional_non_empty(raw_request.document_id) + .unwrap_or_else(generate_tree_document_id), + parent_id: raw_request.parent_id, + title: normalize_title(raw_request.title), + access_scope: raw_request.access_scope, + content: raw_request.content, + }, + "rename" => TreeCommandRequest::Rename { + workspace_id: raw_request.workspace_id, + document_id: raw_request.document_id.unwrap_or_default(), + title: normalize_title(raw_request.title), + }, + "move" => TreeCommandRequest::Move { + workspace_id: raw_request.workspace_id, + document_id: raw_request.document_id.unwrap_or_default(), + parent_id: raw_request.parent_id, + sort_order: raw_request.sort_order.unwrap_or(-1), + }, + other => { + return Err(WebError::bad_request_code( + "tree_command_validation", + format!("不支持的 tree action: {other}"), + ) + .with_context(&context) + .with_header("x-error-phase", "tree_command_validate")); + } + }; + let requested_workspace_id = match &request { + TreeCommandRequest::Create { workspace_id, .. } => workspace_id.as_deref(), + TreeCommandRequest::Rename { workspace_id, .. } => workspace_id.as_deref(), + TreeCommandRequest::Move { workspace_id, .. } => workspace_id.as_deref(), + }; + let (action, requested_document_id, requested_parent_id, requested_title, requested_sort_order) = + match &request { + TreeCommandRequest::Create { + document_id, + parent_id, + title, + .. + } => ( + "create", + document_id.clone(), + parent_id.clone(), + Some(title.clone()), + None, + ), + TreeCommandRequest::Rename { + document_id, + title, + .. + } => ( + "rename", + document_id.clone(), + None, + Some(title.clone()), + None, + ), + TreeCommandRequest::Move { + document_id, + parent_id, + sort_order, + .. + } => ( + "move", + document_id.clone(), + parent_id.clone(), + None, + Some(*sort_order), + ), + }; + let effective_workspace_id = + resolve_effective_workspace_id(&context, requested_workspace_id, true)? + .expect("workspace_required 已确保存在"); + let command_wire = create_command_wire(&context, &effective_workspace_id, request)?; + let execution = execute_runtime_command_via_convex( + state.config(), + &context, + Some(&effective_workspace_id), + command_wire, + ) + .await?; + let response_document_id = execution + .get("id") + .and_then(Value::as_str) + .map(ToOwned::to_owned) + .unwrap_or(requested_document_id); + + Ok(json_response( + &context, + json!({ + "workspaceId": effective_workspace_id, + "action": action, + "documentId": response_document_id, + "parentId": requested_parent_id, + "title": requested_title, + "sortOrder": requested_sort_order, + "updatedAt": execution.get("updated_at").cloned().unwrap_or(Value::Null), + "execution": execution, + }), + )) +} + +#[cfg(test)] +mod tests { + use crate::app::{build_app, AppConfig, AppState}; + use axum::body::Body; + use axum::http::{Request, StatusCode}; + use serde_json::Value; + use tower::util::ServiceExt; + + fn app() -> axum::Router { + build_app(AppState::new(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: None, + convex_admin_key: None, + allow_dev_fixtures: true, + query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}]}}"#.into()), + mutation_fixtures_json: Some(r#"{"documents:createWithParentReference":{"ok":true,"id":"page_new","updated_at":"2026-04-17T00:00:00Z"},"documents:updateTitle":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:move":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"}}"#.into()), + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + })) + } + + #[tokio::test] + async fn tree_shell_returns_interactive_html_document() { + let response = app() + .oneshot( + Request::builder() + .uri("/tree?workspaceId=ws_demo&rootNodeId=page_root&activeDocumentId=page_child&channel=test-shell") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let content_type = response + .headers() + .get("content-type") + .and_then(|value| value.to_str().ok()) + .unwrap_or_default(); + assert!(content_type.contains("text/html")); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let html = String::from_utf8(body.to_vec()).expect("utf8"); + assert!(html.contains("tree-create-root")); + assert!(html.contains("tree.ready")); + assert!(html.contains("test-shell")); + assert!(html.contains("tree-action-menu")); + assert!(html.contains("tree.page.context-menu")); + assert!(html.contains("setAttribute(\"role\", \"treeitem\")")); + assert!(html.contains("setAttribute(\"aria-level\"")); + } + + #[tokio::test] + async fn tree_shell_picker_mode_hides_command_toolbar_and_supports_root_pick() { + let response = app() + .oneshot( + Request::builder() + .uri("/tree?workspaceId=ws_demo&mode=picker&allowRootPick=1&excludeIds=page_child") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let html = String::from_utf8(body.to_vec()).expect("utf8"); + assert!(html.contains("\"mode\":\"picker\"")); + assert!(html.contains("\"allowRootPick\":true")); + assert!(html.contains("\"excludeIds\":[\"page_child\"]")); + assert!(html.contains("tree.pick.root")); + } + + #[tokio::test] + async fn tree_shell_filetree_mode_embeds_asset_state() { + let response = app() + .oneshot( + Request::builder() + .uri("/tree?workspaceId=ws_demo&mode=filetree") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let html = String::from_utf8(body.to_vec()).expect("utf8"); + assert!(html.contains("filetree-doc-row")); + assert!(html.contains("\"mediaAssets\"")); + } + + #[tokio::test] + async fn tree_command_create_generates_document_id_when_missing() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/tree/commands") + .header("content-type", "application/json") + .body(Body::from( + r#"{"action":"create","workspaceId":"ws_demo","parentId":"page_root","title":"新页面","accessScope":"private","content":[]}"#, + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["action"], Value::String("create".into())); + assert_eq!(payload["result"]["documentId"], Value::String("page_new".into())); + } + + #[tokio::test] + async fn tree_command_rejects_negative_sort_order() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/tree/commands") + .header("content-type", "application/json") + .body(Body::from( + r#"{"action":"move","workspaceId":"ws_demo","documentId":"page_child","parentId":"page_root","sortOrder":-1}"#, + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn tree_command_rename_defaults_blank_title_to_untitled() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/tree/commands") + .header("content-type", "application/json") + .body(Body::from( + r#"{"action":"rename","workspaceId":"ws_demo","documentId":"page_child","title":" "}"#, + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["title"], Value::String("无标题".into())); + } + + #[tokio::test] + async fn tree_command_move_returns_structured_payload() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/tree/commands") + .header("content-type", "application/json") + .body(Body::from( + r#"{"action":"move","workspaceId":"ws_demo","documentId":"page_child","parentId":"page_root","sortOrder":1}"#, + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["action"], Value::String("move".into())); + assert_eq!(payload["result"]["documentId"], Value::String("page_child".into())); + } +} diff --git a/rust/crates/mnote-web/src/transport/convex.rs b/rust/crates/mnote-web/src/transport/convex.rs index ce68c1fa..d880ff00 100644 --- a/rust/crates/mnote-web/src/transport/convex.rs +++ b/rust/crates/mnote-web/src/transport/convex.rs @@ -3,7 +3,7 @@ use crate::context::RequestContext; use crate::error::WebError; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use bridge_runtime::RuntimeQueryExecutionPlan; +use bridge_runtime::{RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan}; use serde_json::{json, Value}; use std::fs; use std::time::Duration; @@ -13,6 +13,7 @@ const HEADER_TRACE_ID: &str = "x-trace-id"; const HEADER_WORKSPACE_ID: &str = "x-mnote-workspace-id"; const HEADER_SOURCE_CHANNEL: &str = "x-mnote-source-channel"; const HEADER_SOURCE_CLIENT: &str = "x-mnote-source-client"; +const HEADER_IDEMPOTENCY_KEY: &str = "x-idempotency-key"; fn read_env_or_dotenv(key: &str) -> Option { if let Ok(value) = std::env::var(key) { @@ -70,10 +71,18 @@ fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result Result, WebError> { + if !config.allow_dev_fixtures { + return Ok(None); + } + + let Some(raw) = config.mutation_fixtures_json.as_deref() else { + return Ok(None); + }; + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Ok(None); + } + + let fixtures: Value = serde_json::from_str(trimmed).map_err(|error| { + WebError::internal(format!("MNOTE_WEB_MUTATION_FIXTURES_JSON 非法: {error}")) + .with_context(context) + .with_header("x-error-phase", "fixture_parse") + })?; + + Ok(fixtures + .as_object() + .and_then(|map| map.get(plan.function_name.as_str())) + .cloned()) +} + +pub async fn execute_convex_query_plan( config: &AppConfig, context: &RequestContext, plan: &RuntimeQueryExecutionPlan, @@ -157,7 +195,7 @@ pub fn execute_convex_query_plan( "args": plan.args_json, }); - let client = reqwest::blocking::Client::builder() + let client = reqwest::Client::builder() .timeout(Duration::from_secs(20)) .build() .map_err(|error| { @@ -186,7 +224,7 @@ pub fn execute_convex_query_plan( request = request.header(HEADER_WORKSPACE_ID, workspace_id); } - let response = request.send().map_err(|error| { + let response = request.send().await.map_err(|error| { let base = if error.is_timeout() { WebError::gateway_timeout_code("convex_timeout", format!("Convex query 超时: {error}")) } else { @@ -201,7 +239,7 @@ pub fn execute_convex_query_plan( })?; let status = response.status(); - let body: Value = response.json().map_err(|error| { + let body: Value = response.json().await.map_err(|error| { WebError::bad_gateway_code( "convex_bad_response", format!("Convex 响应解析失败: {error}"), @@ -249,10 +287,135 @@ pub fn execute_convex_query_plan( } } -pub fn execute_sidebar_dataset_query( +pub async fn execute_sidebar_dataset_query( config: &AppConfig, context: &RequestContext, plan: &RuntimeQueryExecutionPlan, ) -> Result { - execute_convex_query_plan(config, context, plan) + execute_convex_query_plan(config, context, plan).await +} + +pub async fn execute_convex_command_plan( + config: &AppConfig, + context: &RequestContext, + plan: &RuntimeCommandExecutionPlan, +) -> Result { + if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") { + return Err(WebError::bad_request_code( + "transport_command_unsupported", + format!("mnote-web transport 暂不支持 command: {}", plan.function_name), + ) + .with_context(context) + .with_header("x-error-phase", "plan_validation")); + } + + if let Some(fixture) = load_mutation_fixture(config, context, plan)? { + return Ok(fixture); + } + + let payload = json!({ + "path": plan.function_name, + "format": "convex_encoded_json", + "args": [plan.args_json.clone()], + }); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(20)) + .build() + .map_err(|error| { + WebError::internal(format!("Convex HTTP 客户端创建失败: {error}")) + .with_context(context) + .with_header("x-error-phase", "client_build") + .with_header("x-upstream-service", "convex") + })?; + + let mut request = client + .post(format!("{}/api/mutation", convex_url(config, context)?)) + .header("Authorization", build_authorization(config, context)?) + .header("Content-Type", "application/json") + .header("Convex-Client", "mnote-web") + .header(HEADER_REQUEST_ID, &context.trace.request_id) + .header(HEADER_TRACE_ID, &context.trace.trace_id) + .header(HEADER_SOURCE_CHANNEL, context.source.channel.as_str()) + .header(HEADER_SOURCE_CLIENT, context.source.client.as_str()) + .json(&payload); + + if let Some(workspace_id) = plan + .workspace_id + .as_deref() + .or(context.workspace.workspace_id.as_deref()) + { + request = request.header(HEADER_WORKSPACE_ID, workspace_id); + } + if let Some(idempotency_key) = plan + .idempotency_key + .as_deref() + .or(context.source.idempotency_key.as_deref()) + .map(str::trim) + .filter(|value| !value.is_empty()) + { + request = request.header(HEADER_IDEMPOTENCY_KEY, idempotency_key); + } + + let response = request.send().await.map_err(|error| { + let base = if error.is_timeout() { + WebError::gateway_timeout_code("convex_timeout", format!("Convex mutation 超时: {error}")) + } else { + WebError::service_unavailable_code( + "convex_unavailable", + format!("Convex mutation 请求失败: {error}"), + ) + }; + base.with_context(context) + .with_header("x-error-phase", "mutation_send") + .with_header("x-upstream-service", "convex") + })?; + + let status = response.status(); + let body: Value = response.json().await.map_err(|error| { + WebError::bad_gateway_code( + "convex_bad_response", + format!("Convex mutation 响应解析失败: {error}"), + ) + .with_context(context) + .with_header("x-error-phase", "mutation_decode") + .with_header("x-upstream-service", "convex") + .with_header("x-upstream-status", status.as_u16().to_string()) + })?; + if !status.is_success() { + let message = body + .get("errorMessage") + .and_then(Value::as_str) + .unwrap_or("Convex mutation 失败"); + return Err( + WebError::bad_gateway_code("convex_upstream_error", message.to_string()) + .with_context(context) + .with_header("x-error-phase", "mutation_status") + .with_header("x-upstream-service", "convex") + .with_header("x-upstream-status", status.as_u16().to_string()), + ); + } + + match body.get("status").and_then(Value::as_str) { + Some("success") => Ok(body.get("value").cloned().unwrap_or(Value::Null)), + Some("error") => Err(WebError::bad_gateway_code( + "convex_upstream_error", + body.get("errorMessage") + .and_then(Value::as_str) + .unwrap_or("Convex 返回 error") + .to_string(), + ) + .with_context(context) + .with_header("x-error-phase", "mutation_payload") + .with_header("x-upstream-service", "convex") + .with_header("x-upstream-status", status.as_u16().to_string())), + _ => Err(WebError::bad_gateway_code( + "convex_bad_response", + format!("未知 Convex 响应: {body}"), + ) + .with_context(context) + .with_header("x-error-phase", "mutation_payload") + .with_header("x-upstream-service", "convex") + .with_header("x-upstream-status", status.as_u16().to_string())), + } } diff --git a/scripts/task091-tree-shell-smoke.js b/scripts/task091-tree-shell-smoke.js new file mode 100644 index 00000000..9a127942 --- /dev/null +++ b/scripts/task091-tree-shell-smoke.js @@ -0,0 +1,363 @@ +"use strict"; + +// 说明: +// - 这是 Sidebar / PageTree Rust Web tree shell 的真实网页 smoke 脚本。 +// - 目标覆盖:主页面已挂载 iframe tree shell、展开/折叠、在 shell 内创建子页面、重命名、移动、导航。 +// - 脚本会创建临时页面并在结束后清理,避免污染现有数据。 + +const { chromium } = require("playwright"); + +const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000"; +const MNOTE_WEB_BASE_URL = process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3104"; +const REQUEST_TIMEOUT_MS = 20_000; +const UI_TIMEOUT_MS = 30_000; +const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录"; + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +async function requestJson(requestContext, path, init = {}) { + const response = await requestContext.fetch(`${BASE_URL}${path}`, { + ...init, + headers: + init.data !== undefined + ? { + "content-type": "application/json", + ...(init.headers || {}), + } + : { + ...(init.headers || {}), + }, + timeout: REQUEST_TIMEOUT_MS, + }); + + const text = await response.text(); + let payload = null; + try { + payload = text ? JSON.parse(text) : null; + } catch { + payload = text; + } + + if (!response.ok()) { + throw new Error( + `${path} 请求失败: ${response.status()} ${response.statusText()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`, + ); + } + + const contentType = response.headers()["content-type"] || ""; + if (!contentType.includes("application/json")) { + const snippet = typeof payload === "string" ? payload.slice(0, 200) : JSON.stringify(payload).slice(0, 200); + throw new Error(`${path} 返回了非 JSON 内容:${snippet}`); + } + + return payload; +} + +async function createTempDocument(requestContext, parentId = null) { + const payload = await requestJson(requestContext, "/api/documents/create", { + method: "POST", + data: { parentId }, + }); + assert(payload && typeof payload.id === "string", "创建临时页面失败:缺少 id"); + assert(payload && typeof payload.workspace_id === "string", "创建临时页面失败:缺少 workspace_id"); + return { + documentId: payload.id, + workspaceId: payload.workspace_id, + }; +} + +async function renameDocument(requestContext, workspaceId, documentId, title) { + await requestJson(requestContext, "/api/documents/title", { + method: "POST", + data: { + workspaceId, + documentId, + title, + }, + }); +} + +async function purgeDocument(requestContext, documentId) { + await requestJson(requestContext, "/api/documents/purge", { + method: "POST", + data: { documentId }, + }); +} + +async function getViewerIdentity(requestContext) { + const payload = await requestJson(requestContext, "/api/auth/whoami", { method: "GET" }); + assert(payload && typeof payload.userId === "string" && payload.userId, "获取当前用户失败:缺少 userId"); + return payload; +} + +async function ensureAuthenticated(page, requestContext) { + await page.goto(`${BASE_URL}/auth`, { waitUntil: "networkidle", timeout: UI_TIMEOUT_MS }); + + if (page.url().includes("/auth")) { + const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME }); + await quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await quickLoginButton.click({ timeout: UI_TIMEOUT_MS }); + await page.waitForURL((url) => !url.toString().includes("/auth"), { + timeout: UI_TIMEOUT_MS, + }); + } + + return await getViewerIdentity(requestContext); +} + +async function runTreeShellRegression(page, requestContext, viewer, target) { + const uniqueSuffix = Date.now().toString(); + const rootTitle = `task091-root-${uniqueSuffix}`; + const childTitle = `task091-child-${uniqueSuffix}`; + const createdTitle = `task091-created-${uniqueSuffix}`; + const renamedTitle = `task091-renamed-${uniqueSuffix}`; + + await renameDocument(requestContext, target.workspaceId, target.parentId, rootTitle); + await renameDocument(requestContext, target.workspaceId, target.childId, childTitle); + + const documentUrl = `${BASE_URL}/documents/${target.parentId}?workspaceId=${encodeURIComponent(target.workspaceId)}`; + await page.goto(documentUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); + + const groupButton = page.getByRole("button", { name: "分组" }); + await groupButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await groupButton.click({ timeout: UI_TIMEOUT_MS }); + + const sidebarPanel = page.getByText("页面树"); + await sidebarPanel.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + + const iframe = page.locator('iframe[title="mnote-web tree shell"]'); + await iframe.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const iframeSrc = await iframe.getAttribute("src"); + assert(iframeSrc && iframeSrc.includes(`${MNOTE_WEB_BASE_URL}/tree`), `Sidebar 未挂载 mnote-web tree shell:${iframeSrc}`); + assert(iframeSrc.includes(`actorId=${encodeURIComponent(viewer.userId)}`), `tree shell 未透传当前用户 actorId:${iframeSrc}`); + + const getTreeFrame = () => page.frameLocator('iframe[title="mnote-web tree shell"]'); + const waitForTreeReady = async () => { + const frame = getTreeFrame(); + await frame.locator('[data-testid="tree-create-root"]').waitFor({ + state: "visible", + timeout: UI_TIMEOUT_MS, + }); + return frame; + }; + const getFirstChildId = async (parentId) => { + const frame = await waitForTreeReady(); + return await frame + .locator(`.tree-node[data-node-id="${parentId}"] > .tree-children > .tree-node`) + .first() + .getAttribute("data-node-id"); + }; + const waitForFirstChildId = async (parentId, expectedId) => { + const deadline = Date.now() + UI_TIMEOUT_MS; + while (Date.now() < deadline) { + const actualId = await getFirstChildId(parentId); + if (actualId === expectedId) { + return actualId; + } + await page.waitForTimeout(250); + } + return await getFirstChildId(parentId); + }; + let treeFrame = await waitForTreeReady(); + + const parentRow = treeFrame.locator(`.tree-row[data-node-id="${target.parentId}"]`); + const childRow = treeFrame.locator(`.tree-row[data-node-id="${target.childId}"]`); + await parentRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await childRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + + const toggleButton = parentRow.locator('[data-testid="tree-node-toggle"]'); + await toggleButton.click({ timeout: UI_TIMEOUT_MS }); + await childRow.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS }); + await toggleButton.click({ timeout: UI_TIMEOUT_MS }); + await childRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + + page.once("dialog", (dialog) => dialog.accept(createdTitle)); + const createResponsePromise = page.waitForResponse( + (response) => + response.url().includes(`${MNOTE_WEB_BASE_URL}/api/tree/commands`) && + response.request().method() === "POST" && + response.status() === 200 && + (response.request().postData() || "").includes('"action":"create"'), + { timeout: UI_TIMEOUT_MS }, + ); + await parentRow.locator('[data-testid="tree-action-create"]').click({ timeout: UI_TIMEOUT_MS }); + const createResponse = await createResponsePromise; + const createPayload = await createResponse.json(); + const createdDocumentId = createPayload?.result?.documentId; + assert(typeof createdDocumentId === "string" && createdDocumentId, "tree shell 创建子页面失败:缺少 documentId"); + await page.waitForURL((url) => url.toString().includes(`/documents/${createdDocumentId}`), { + timeout: UI_TIMEOUT_MS, + }); + target.createdIds.push(createdDocumentId); + + treeFrame = await waitForTreeReady(); + const createdRow = treeFrame.locator(`.tree-row[data-node-id="${createdDocumentId}"]`); + await createdRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + + page.once("dialog", (dialog) => dialog.accept(renamedTitle)); + const renameResponsePromise = page.waitForResponse( + (response) => + response.url().includes(`${MNOTE_WEB_BASE_URL}/api/tree/commands`) && + response.request().method() === "POST" && + response.status() === 200 && + (response.request().postData() || "").includes('"action":"rename"'), + { timeout: UI_TIMEOUT_MS }, + ); + await createdRow.locator('[data-testid="tree-action-rename"]').click({ timeout: UI_TIMEOUT_MS }); + await renameResponsePromise; + treeFrame = await waitForTreeReady(); + const renamedRow = treeFrame.locator(`.tree-row[data-node-id="${createdDocumentId}"]`); + await renamedRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + + const childOrderBeforeMove = await getFirstChildId(target.parentId); + assert( + childOrderBeforeMove === target.childId, + `移动前的首个子节点异常:期望 ${target.childId},实际 ${childOrderBeforeMove}`, + ); + + const moveResponsePromise = page.waitForResponse( + (response) => + response.url().includes(`${MNOTE_WEB_BASE_URL}/api/tree/commands`) && + response.request().method() === "POST" && + response.status() === 200 && + (response.request().postData() || "").includes('"action":"move"'), + { timeout: UI_TIMEOUT_MS }, + ); + await renamedRow.locator('[data-testid="tree-action-move-up"]').click({ timeout: UI_TIMEOUT_MS }); + await moveResponsePromise; + const firstChildAfterMove = await waitForFirstChildId(target.parentId, createdDocumentId); + assert( + firstChildAfterMove === createdDocumentId, + `移动后排序未生效:期望首个子节点为 ${createdDocumentId},实际 ${firstChildAfterMove}`, + ); + + treeFrame = await waitForTreeReady(); + const latestChildRow = treeFrame.locator(`.tree-row[data-node-id="${target.childId}"]`); + await latestChildRow.locator('[data-testid="tree-node-open"]').click({ timeout: UI_TIMEOUT_MS }); + await page.waitForURL((url) => url.toString().includes(`/documents/${target.childId}`), { + timeout: UI_TIMEOUT_MS, + }); + + return { + documentUrl, + rootTitle, + childTitle, + createdTitle, + renamedTitle, + createdDocumentId, + }; +} + +async function runPickerRegression(page, target) { + const documentUrl = `${BASE_URL}/documents/${target.parentId}?workspaceId=${encodeURIComponent(target.workspaceId)}`; + await page.goto(documentUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); + + const inspectorToggle = page.getByRole("button", { name: /显示页面选项|隐藏页面选项/ }); + await inspectorToggle.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const currentLabel = (await inspectorToggle.getAttribute("aria-label")) || ""; + if (currentLabel.includes("显示")) { + await inspectorToggle.click({ timeout: UI_TIMEOUT_MS }); + } + + const moveEmbedButton = page.getByRole("button", { name: "移动/嵌入到..." }); + await moveEmbedButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await moveEmbedButton.click({ timeout: UI_TIMEOUT_MS }); + + const dialog = page.getByRole("dialog"); + await dialog.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + + const pickerIframe = dialog.locator('iframe[title="mnote-web tree shell"]'); + await pickerIframe.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const iframeSrc = await pickerIframe.getAttribute("src"); + assert(iframeSrc && iframeSrc.includes("mode=picker"), `picker 未以 tree shell 轻量模式挂载:${iframeSrc}`); + assert(iframeSrc.includes("allowRootPick=1"), `picker 未透传 allowRootPick:${iframeSrc}`); + assert( + iframeSrc.includes(`excludeIds=${encodeURIComponent(target.parentId)}`), + `picker 未透传 excludeIds:${iframeSrc}`, + ); + + const frame = dialog.frameLocator('iframe[title="mnote-web tree shell"]'); + const rootPick = frame.locator('[data-testid="tree-picker-root"]'); + await rootPick.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await rootPick.click({ timeout: UI_TIMEOUT_MS }); + + await dialog.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS }); +} + +async function main() { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 1440, height: 960 }, + }); + const page = await context.newPage(); + + let caughtError = null; + let result = null; + const createdIds = []; + + try { + const viewer = await ensureAuthenticated(page, context.request); + const parent = await createTempDocument(context.request, null); + createdIds.push(parent.documentId); + const child = await createTempDocument(context.request, parent.documentId); + createdIds.push(child.documentId); + + result = await runTreeShellRegression(page, context.request, viewer, { + workspaceId: parent.workspaceId, + parentId: parent.documentId, + childId: child.documentId, + createdIds, + }); + await runPickerRegression(page, { + workspaceId: parent.workspaceId, + parentId: parent.documentId, + }); + + console.log( + JSON.stringify( + { + ok: true, + workspaceId: parent.workspaceId, + viewerUserId: viewer.userId, + parentId: parent.documentId, + childId: child.documentId, + ...result, + }, + null, + 2, + ), + ); + } catch (error) { + caughtError = error; + } finally { + for (const documentId of [...createdIds].reverse()) { + try { + await purgeDocument(context.request, documentId); + } catch (cleanupError) { + if (!caughtError) { + caughtError = cleanupError; + } else { + console.error( + `清理临时页面失败:${cleanupError instanceof Error ? cleanupError.stack || cleanupError.message : String(cleanupError)}`, + ); + } + } + } + await page.close().catch(() => undefined); + await context.close().catch(() => undefined); + await browser.close().catch(() => undefined); + } + + if (caughtError) { + throw caughtError; + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.stack || error.message : String(error)); + process.exit(1); +}); diff --git a/scripts/task097-homepage-entry-smoke.js b/scripts/task097-homepage-entry-smoke.js new file mode 100644 index 00000000..d6f1ee64 --- /dev/null +++ b/scripts/task097-homepage-entry-smoke.js @@ -0,0 +1,124 @@ +"use strict"; + +// 说明: +// - 这是一条最小首页入口 smoke,不验证完整业务流。 +// - 它只验证 3000 主站入口链路是否可达,并明确 3104 不是首页可进入的前置条件。 +// - 未登录场景允许 "/" 返回 30x 到 "/auth",也允许直接返回 HTML。 + +const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); +const MNOTE_WEB_BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3104").replace(/\/+$/, ""); +const TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 8000); + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +async function fetchWithTimeout(url, init = {}) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(new Error(`请求超时: ${url}`)), TIMEOUT_MS); + try { + return await fetch(url, { + redirect: "manual", + cache: "no-store", + ...init, + signal: controller.signal, + }); + } finally { + clearTimeout(timer); + } +} + +async function readSnippet(response) { + const text = await response.text().catch(() => ""); + return text.slice(0, 200); +} + +async function probeMnoteWeb() { + try { + const response = await fetchWithTimeout(`${MNOTE_WEB_BASE_URL}/health`, { + method: "GET", + }); + return { + reachable: true, + status: response.status, + }; + } catch (error) { + return { + reachable: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function validateAuthEntry() { + const response = await fetchWithTimeout(`${BASE_URL}/auth`, { method: "GET" }); + const contentType = response.headers.get("content-type") || ""; + const snippet = await readSnippet(response); + + assert(response.ok, `/auth 请求失败: ${response.status} ${snippet}`); + assert( + contentType.includes("text/html"), + `/auth 返回了非 HTML 内容: ${contentType || ""} ${snippet}`, + ); + + return { + status: response.status, + contentType, + }; +} + +async function validateRootEntry() { + const response = await fetchWithTimeout(`${BASE_URL}/`, { method: "GET" }); + const location = response.headers.get("location"); + const contentType = response.headers.get("content-type") || ""; + const snippet = await readSnippet(response); + + const isRedirectToAllowedTarget = + response.status >= 300 && + response.status < 400 && + typeof location === "string" && + (location.includes("/auth") || location.includes("/documents/")); + + const isHtmlOk = + response.ok && + contentType.includes("text/html"); + + assert( + isRedirectToAllowedTarget || isHtmlOk, + `/ 返回了不符合预期的结果: status=${response.status} location=${location || ""} contentType=${contentType || ""} snippet=${snippet}`, + ); + + return { + status: response.status, + location, + contentType, + }; +} + +async function main() { + const mnoteWeb = await probeMnoteWeb(); + const authEntry = await validateAuthEntry(); + const rootEntry = await validateRootEntry(); + + console.log( + JSON.stringify( + { + ok: true, + baseUrl: BASE_URL, + mnoteWebBaseUrl: MNOTE_WEB_BASE_URL, + mnoteWeb, + authEntry, + rootEntry, + }, + null, + 2, + ), + ); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.stack || error.message : String(error)); + process.exit(1); +}); diff --git a/wolai-frontend/public/mnote-env.json b/wolai-frontend/public/mnote-env.json index 323263e2..222f47be 100644 --- a/wolai-frontend/public/mnote-env.json +++ b/wolai-frontend/public/mnote-env.json @@ -4,6 +4,8 @@ "supabaseInternalUrl": "http://127.0.0.1:18000", "supabaseAnonKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLWRldiIsImlhdCI6MTc2NDExNTA0OSwiZXhwIjoyMDc5NDc1MDQ5fQ.18ohcQZXVkoR1TIF56QWJxHyoVnA9aarH-XfTyBJn1Y", "backendUrl": "https://frp-dry.com:44399", + "mnoteWebBaseUrl": "http://127.0.0.1:3104", + "mnoteWebTreeShellEnabled": false, "onlyofficeBaseUrlWeb": "/onlyoffice-server", "onlyofficeBaseUrlDesktop": "http://localhost:8082", "onlyofficeProxyOrigin": "http://host.docker.internal:3000", diff --git a/wolai-frontend/scripts/dev-server.js b/wolai-frontend/scripts/dev-server.js index f32b8951..d5195038 100644 --- a/wolai-frontend/scripts/dev-server.js +++ b/wolai-frontend/scripts/dev-server.js @@ -9,7 +9,7 @@ * - node scripts/dev-server.js -p 3000 * * 依赖环境变量: - * - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时会自动探测 8081/8082 + * - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时优先探测 8082,再回退 8081 */ const http = require("http"); @@ -20,7 +20,7 @@ const { parse: parseUrl } = require("url"); const ONLYOFFICE_PREFIX = "/onlyoffice-server"; const CONVEX_PREFIX = "/convex"; -const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8081"; +const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8082"; const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js"; const ONLYOFFICE_RESOLVE_CACHE_TTL_MS = 30_000; @@ -92,9 +92,9 @@ function listOnlyOfficeInternalUrlCandidates() { } push(DEFAULT_ONLYOFFICE_INTERNAL_URL); - push("http://127.0.0.1:8082"); - push("http://localhost:8081"); + push("http://127.0.0.1:8081"); push("http://localhost:8082"); + push("http://localhost:8081"); return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL]; } @@ -313,12 +313,12 @@ function proxyConvexUpgrade(req, socket, head) { socket.on("error", onError); } -function proxyConvexHttp(req, res) { - const target = resolveConvexInternalUrl(); - const incoming = new URL(req.url || "/", "http://localhost"); - const rawPath = incoming.pathname || "/"; - const stripped = rawPath === CONVEX_PREFIX ? "/" : rawPath.slice(CONVEX_PREFIX.length) || "/"; - const upstreamPath = stripped + (incoming.search || ""); +function proxyConvexHttp(req, res) { + const target = resolveConvexInternalUrl(); + const incoming = new URL(req.url || "/", "http://localhost"); + const rawPath = incoming.pathname || "/"; + const stripped = rawPath === CONVEX_PREFIX ? "/" : rawPath.slice(CONVEX_PREFIX.length) || "/"; + const upstreamPath = stripped + (incoming.search || ""); const isHttps = target.protocol === "https:"; const mod = isHttps ? require("https") : require("http"); @@ -359,19 +359,30 @@ function proxyConvexHttp(req, res) { }, ); - upstreamReq.on("error", (err) => { + upstreamReq.on("error", (err) => { try { console.log("[dev-server][convex-http] proxy error", err && err.message ? err.message : String(err || "")); } catch {} try { res.statusCode = 502; - res.end("Bad Gateway"); - } catch {} - }); - - req.pipe(upstreamReq); -} + res.end("Bad Gateway"); + } catch {} + }); + + const method = String(req.method || "GET").toUpperCase(); + const hasBody = + !["GET", "HEAD"].includes(method) && + ((typeof req.headers["content-length"] === "string" && req.headers["content-length"] !== "0") || + String(req.headers["transfer-encoding"] || "").trim().length > 0); + + if (!hasBody) { + upstreamReq.end(); + return; + } + + req.pipe(upstreamReq); +} async function main() { const port = resolvePort(); diff --git a/wolai-frontend/scripts/prod-server.js b/wolai-frontend/scripts/prod-server.js index b791e1b1..e1d25e54 100644 --- a/wolai-frontend/scripts/prod-server.js +++ b/wolai-frontend/scripts/prod-server.js @@ -9,7 +9,7 @@ * - pnpm start (默认会执行本脚本) * * 依赖环境变量: - * - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时会自动探测 8081/8082 + * - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时优先探测 8082,再回退 8081 * - CONVEX_INTERNAL_URL:默认 http://127.0.0.1:3210 */ @@ -21,7 +21,7 @@ const { parse: parseUrl } = require("url"); const ONLYOFFICE_PREFIX = "/onlyoffice-server"; const CONVEX_PREFIX = "/convex"; -const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8081"; +const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8082"; const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js"; const ONLYOFFICE_RESOLVE_CACHE_TTL_MS = 30_000; @@ -93,9 +93,9 @@ function listOnlyOfficeInternalUrlCandidates() { } push(DEFAULT_ONLYOFFICE_INTERNAL_URL); - push("http://127.0.0.1:8082"); - push("http://localhost:8081"); + push("http://127.0.0.1:8081"); push("http://localhost:8082"); + push("http://localhost:8081"); return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL]; } diff --git a/wolai-frontend/src/app/(auth)/auth/page.tsx b/wolai-frontend/src/app/(auth)/auth/page.tsx index 1a6bd558..647b781b 100644 --- a/wolai-frontend/src/app/(auth)/auth/page.tsx +++ b/wolai-frontend/src/app/(auth)/auth/page.tsx @@ -31,14 +31,22 @@ export default function AuthPage() { const currentUser = useQuery(api.users.currentUser, isAuthenticated ? {} : "skip"); + const navigateToHome = useCallback(() => { + if (typeof window !== "undefined") { + window.location.assign("/"); + return; + } + router.replace("/"); + }, [router]); + useEffect(() => { if (isLoading) return; if (!isAuthenticated) return; if (currentUser === undefined) return; if (currentUser && currentUser.name) { - router.replace("/"); + navigateToHome(); } - }, [currentUser, isAuthenticated, isLoading, router]); + }, [currentUser, isAuthenticated, isLoading, navigateToHome]); const [flow, setFlow] = useState("signIn"); const [email, setEmail] = useState(""); @@ -70,7 +78,7 @@ export default function AuthPage() { if (cancelled) return; setPendingUsernameToSave(null); setMessage({ type: "success", text: "注册成功!" }); - router.replace("/"); + navigateToHome(); } catch (err: unknown) { if (cancelled) return; setPendingUsernameToSave(null); @@ -83,7 +91,7 @@ export default function AuthPage() { return () => { cancelled = true; }; - }, [currentUser, isAuthenticated, pendingUsernameToSave, router, setMyUsername]); + }, [currentUser, isAuthenticated, navigateToHome, pendingUsernameToSave, setMyUsername]); // 执行登录的核心逻辑 const performSignIn = useCallback(async (email: string, password: string, flow: AuthStep) => { @@ -110,8 +118,10 @@ export default function AuthPage() { setPendingUsernameToSave(username.trim()); return; } - setMessage({ type: "success", text: flow === "signIn" ? "登录成功!" : "注册成功!" }); - setTimeout(() => router.push("/"), 500); + // 说明:登录成功后不要立刻强推 `/`。 + // Convex Auth cookie 与 currentUser 查询存在短暂同步窗口,过早跳转会把页面推进到根路由空白态。 + // 这里仅提示“正在同步”,真正跳转交给上面的 useEffect 在登录态稳定后执行。 + setMessage({ type: "info", text: "登录成功,正在同步登录状态..." }); return; } @@ -187,7 +197,7 @@ export default function AuthPage() { setIsSavingUsername(true); await setMyUsername({ username: username.trim() }); setMessage({ type: "success", text: "用户名已保存!" }); - router.replace("/"); + navigateToHome(); } catch (error: any) { setMessage({ type: "error", text: getUserFacingErrorMessage(error, "保存失败,请重试") }); } finally { diff --git a/wolai-frontend/src/app/api/sidebar/route.ts b/wolai-frontend/src/app/api/sidebar/route.ts index 7bfec8f8..17f82256 100644 --- a/wolai-frontend/src/app/api/sidebar/route.ts +++ b/wolai-frontend/src/app/api/sidebar/route.ts @@ -13,14 +13,9 @@ import { resolveRustBridgeQueryPlan, } from "@/lib/documents/rust-runtime"; import { - buildSidebarDatasetListQueryPayload, mapSidebarDatasetListQueryResultToInitialData, type SidebarDatasetListQueryResult, } from "@/lib/sidebar-data"; -import { - fetchSidebarDatasetFromMnoteWeb, - getMnoteWebBaseUrl, -} from "@/lib/server/mnote-web"; export const dynamic = "force-dynamic"; @@ -41,31 +36,15 @@ export async function GET(request: Request) { } try { - if (getMnoteWebBaseUrl()) { - const proxied = await fetchSidebarDatasetFromMnoteWeb({ - workspaceId: targetWorkspaceId, - request, - }); - const result = mapSidebarDatasetListQueryResultToInitialData(proxied.dataset); - - return NextResponse.json({ - ...result, - meta: { - requestId: proxied.meta.requestId, - traceId: proxied.meta.traceId, - queryName: "compat.next.sidebar", - workspaceId: proxied.meta.workspaceId, - }, - }); - } - const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId: targetWorkspaceId, }); const envelope = buildDocumentQueryEnvelope({ name: "sidebar.dataset.list", - payload: buildSidebarDatasetListQueryPayload(targetWorkspaceId), + payload: { + workspaceId: targetWorkspaceId, + }, }); const plan = await resolveRustBridgeQueryPlan({ context: bridgeContext, diff --git a/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx b/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx index 763f95eb..64c63188 100644 --- a/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx +++ b/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx @@ -3,11 +3,13 @@ import { useCallback, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Search } from "lucide-react"; +import { MnoteWebTreeShell } from "@/components/sidebar/MnoteWebTreeShell"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { SidebarInitialData } from "@/components/sidebar/types"; import { buildPageTreeProjectionItems, buildPickerTreeItems } from "@/lib/tree-projection"; +import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; import { cn } from "@/lib/utils"; import { useDocumentSearch } from "@/hooks/use-document-search"; import type { DocumentSearchFilters, DocumentSearchResult } from "@/types/search"; @@ -27,6 +29,10 @@ type PickerItem = | { kind: "root"; id: null; title: string; subtitle?: string } | { kind: "doc"; id: string; title: string; subtitle?: string; depth?: number; raw?: DocumentSearchResult }; +type ViewerIdentity = { + userId: string; +}; + async function fetchSidebarData(workspaceId: string): Promise { const response = await fetch(`/api/sidebar?workspaceId=${encodeURIComponent(workspaceId)}`, { method: "GET", @@ -42,6 +48,18 @@ async function fetchSidebarData(workspaceId: string): Promise { + const response = await fetch("/api/auth/whoami", { + method: "GET", + credentials: "include", + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok || typeof payload?.userId !== "string" || !payload.userId.trim()) { + throw new Error(payload?.error ?? "获取当前用户失败"); + } + return { userId: payload.userId.trim() }; +} + interface MoveEmbedPickerDialogProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -129,6 +147,10 @@ function MoveEmbedPickerDialogBody({ const trimmed = query.trim(); const isEmptyQuery = trimmed.length === 0; + const runtimeConfig = useMemo(() => getMnoteRuntimeConfig(), []); + const treeShellEnabled = Boolean( + (runtimeConfig.mnoteWebBaseUrl ?? "").trim() && runtimeConfig.mnoteWebTreeShellEnabled === true, + ); const sidebarQuery = useQuery({ queryKey: ["move-embed-picker-sidebar", workspaceId], @@ -143,6 +165,14 @@ function MoveEmbedPickerDialogBody({ gcTime: 60_000, }); + const viewerQuery = useQuery({ + queryKey: ["move-embed-picker-viewer"], + queryFn: fetchViewerIdentity, + enabled: Boolean(workspaceId) && isEmptyQuery && treeShellEnabled, + staleTime: 60_000, + gcTime: 120_000, + }); + const { data, isLoading, error } = useDocumentSearch(payload, Boolean(payload) && !isEmptyQuery); const items = useMemo(() => { @@ -190,6 +220,39 @@ function MoveEmbedPickerDialogBody({ }, [allowRoot, data?.recent, data?.results, excludeIds, isEmptyQuery, mode, sidebarQuery.data?.kernelSidebarTree]); const placeholder = mode === "move" ? "移动到..." : "嵌入到..."; + const allowRootPick = allowRoot && mode === "move"; + const pickerFallback = ( + sidebarQuery.isLoading ? ( +
加载中...
+ ) : sidebarQuery.error ? ( +
{String(sidebarQuery.error)}
+ ) : items.length === 0 ? ( +
没有匹配结果
+ ) : ( +
+ {items.map((item, idx) => ( + + ))} +
+ ) + ); const handlePick = useCallback( async (targetId: string | null) => { @@ -232,6 +295,9 @@ function MoveEmbedPickerDialogBody({
{ + if (isEmptyQuery) { + return; + } if (event.key === "ArrowDown") { event.preventDefault(); setHighlighted((prev) => Math.min(items.length - 1, prev + 1)); @@ -248,10 +314,34 @@ function MoveEmbedPickerDialogBody({ > {!workspaceId ? (
缺少 workspaceId,无法加载页面列表。
- ) : (isEmptyQuery ? sidebarQuery.isLoading : isLoading) ? ( + ) : isEmptyQuery ? ( +
+ {!treeShellEnabled ? ( + pickerFallback + ) : viewerQuery.isLoading ? ( +
正在准备选择器...
+ ) : viewerQuery.error ? ( +
{String(viewerQuery.error)}
+ ) : ( + {}} + onPick={(targetId) => { + void handlePick(targetId); + }} + onRefresh={async () => undefined} + fallback={pickerFallback} + /> + )} +
+ ) : isLoading ? (
加载中...
- ) : (isEmptyQuery ? sidebarQuery.error : error) ? ( -
{String(isEmptyQuery ? sidebarQuery.error : error)}
+ ) : error ? ( +
{String(error)}
) : items.length === 0 ? (
没有匹配结果
) : ( diff --git a/wolai-frontend/src/components/editor/blocknote-editor.tsx b/wolai-frontend/src/components/editor/blocknote-editor.tsx index 684e54d0..9573bbb6 100644 --- a/wolai-frontend/src/components/editor/blocknote-editor.tsx +++ b/wolai-frontend/src/components/editor/blocknote-editor.tsx @@ -21,7 +21,6 @@ import type { MediaAsset } from "@/types/media"; import { customBlockSchema, type CustomBlockSchema } from "./schema"; import { CustomSideMenu } from "./menus/CustomSideMenu"; import { CustomSlashMenu } from "./menus/CustomSlashMenu"; -import { MoveEmbedPickerHost } from "@/components/documents/move-embed-picker-host"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { DocumentToc, type TocEntry } from "@/components/editor/document-toc"; import { useSearchPaletteStore } from "@/store/search-palette"; @@ -36,6 +35,7 @@ import { useCommentsUiStore } from "@/store/comments-ui"; import { useConvexAuth, useQuery } from "convex/react"; import { api } from "@/lib/convex/api"; import { buildDocumentSavePayload } from "@/lib/documents/save-contract"; +import type { EditorReferenceBridge } from "@/store/editor-bridge"; interface BlockNoteEditorProps { documentId: string; @@ -197,6 +197,10 @@ export function BlockNoteEditor({ const isFullScreenTableOpen = fullScreenTableId !== null; const revisionRef = useRef(initialRevision); const conflictDetectionKeyRef = useRef(initialConflictDetectionKey); + const onStatsChangeRef = useRef(onStatsChange); + const onSnapshotRef = useRef(onSnapshot); + const onPersistedMetaChangeRef = useRef(onPersistedMetaChange); + const stableInitialContentRef = useRef<{ documentId: string; content: Json | undefined } | null>(null); const openReferencePalette = useSearchPaletteStore((state) => state.openReference); const registerEditorBridge = useEditorBridgeStore((state) => state.registerBridge); @@ -226,6 +230,16 @@ export function BlockNoteEditor({ [initialContent], ); + if ( + !stableInitialContentRef.current || + stableInitialContentRef.current.documentId !== documentId + ) { + stableInitialContentRef.current = { + documentId, + content: normalizedInitialContent, + }; + } + const collaboration = useMemo(() => { const url = process.env.NEXT_PUBLIC_HOCUSPOCUS_URL; if (!url) return null; @@ -240,7 +254,7 @@ export function BlockNoteEditor({ const editor = useCreateBlockNote( { - initialContent: normalizedInitialContent as never, + initialContent: stableInitialContentRef.current?.content as never, schema: customBlockSchema, placeholders: { default: "输入'/'选择,按 空格 打开AI...", @@ -257,7 +271,7 @@ export function BlockNoteEditor({ } : undefined, }, - [documentId, normalizedInitialContent], + [documentId], ); useEffect( @@ -268,6 +282,20 @@ export function BlockNoteEditor({ [collaboration], ); + const editorRef = useRef(editor); + const insertMediaAssetRef = useRef<(asset: MediaAsset) => void>(() => {}); + const insertMindmapRef = useRef<(args: { documentId: string; mindmapId: string }) => void>(() => {}); + const insertOnlineTableRef = useRef<(args: { documentId: string; tableId: string }) => void>(() => {}); + const setFullScreenTableIdRef = useRef(setFullScreenTableId); + + useEffect(() => { + editorRef.current = editor; + }, [editor]); + + useEffect(() => { + setFullScreenTableIdRef.current = setFullScreenTableId; + }, [setFullScreenTableId]); + useEffect(() => { revisionRef.current = initialRevision; }, [initialRevision]); @@ -276,6 +304,18 @@ export function BlockNoteEditor({ conflictDetectionKeyRef.current = initialConflictDetectionKey; }, [initialConflictDetectionKey]); + useEffect(() => { + onStatsChangeRef.current = onStatsChange; + }, [onStatsChange]); + + useEffect(() => { + onSnapshotRef.current = onSnapshot; + }, [onSnapshot]); + + useEffect(() => { + onPersistedMetaChangeRef.current = onPersistedMetaChange; + }, [onPersistedMetaChange]); + const saveContent = useCallback( async (content: Json) => { setIsSaving(true); @@ -324,7 +364,7 @@ export function BlockNoteEditor({ : conflictDetectionKeyRef.current; revisionRef.current = nextRevision ?? null; conflictDetectionKeyRef.current = nextConflictDetectionKey ?? null; - onPersistedMetaChange?.({ + onPersistedMetaChangeRef.current?.({ revision: revisionRef.current, conflictDetectionKey: conflictDetectionKeyRef.current, }); @@ -337,7 +377,7 @@ export function BlockNoteEditor({ setIsSaving(false); } }, - [documentId, onPersistedMetaChange, workspaceId], + [documentId, workspaceId], ); const debouncedSave = useDebouncedCallback(saveContent, 800); @@ -705,8 +745,8 @@ export function BlockNoteEditor({ setTocEntries(buildHeadingToc(typedBlocks)); syncProgressMeters(editor); const stats = computeDocumentStats(typedBlocks); - onStatsChange?.(stats); - onSnapshot?.({ blocks: blocks as Json, stats }); + onStatsChangeRef.current?.(stats); + onSnapshotRef.current?.({ blocks: blocks as Json, stats }); // 检测块删除 -> 同步删除附件/思维导图并刷新侧边栏 const { assetIds, mindmapBlockIds, onlineTableIds } = collectAssets(typedBlocks); @@ -745,7 +785,7 @@ export function BlockNoteEditor({ disposed = true; unsubscribe?.(); }; - }, [collectAssets, debouncedSave, deleteAssets, deleteMindmapAssets, deleteOnlineTables, editor, onSnapshot, onStatsChange, restoreOnlineTableIfNeeded]); + }, [collectAssets, debouncedSave, deleteAssets, deleteMindmapAssets, deleteOnlineTables, editor, restoreOnlineTableIfNeeded]); const jumpToHeading = useCallback((headingId: string) => { const target = document.querySelector(`[data-id="${headingId}"]`); @@ -1003,6 +1043,18 @@ const computeDocumentStats = (blocks: Block[]): DocumentStats [documentId, editor], ); + useEffect(() => { + insertMediaAssetRef.current = insertMediaAssetBlock; + }, [insertMediaAssetBlock]); + + useEffect(() => { + insertMindmapRef.current = insertMindmapBlock; + }, [insertMindmapBlock]); + + useEffect(() => { + insertOnlineTableRef.current = insertOnlineTableBlock; + }, [insertOnlineTableBlock]); + useEffect(() => { if (!editor) return undefined; @@ -1090,46 +1142,48 @@ const computeDocumentStats = (blocks: Block[]): DocumentStats [documentId, workspaceId], ); - useEffect(() => { - if (!editor) { - registerEditorBridge(null); - return; - } - const bridge = { + const stableEditorBridge = useMemo( + () => ({ undo: () => { try { - editor.focus(); - editor.undo(); + const currentEditor = editorRef.current; + if (!currentEditor) return; + currentEditor.focus(); + currentEditor.undo(); } catch { // ignore } }, redo: () => { try { - editor.focus(); - editor.redo(); + const currentEditor = editorRef.current; + if (!currentEditor) return; + currentEditor.focus(); + currentEditor.redo(); } catch { // ignore } }, openTableFullScreen: (tableId: string) => { - setFullScreenTableId(tableId); + setFullScreenTableIdRef.current(tableId); }, insertMediaAsset: (asset: MediaAsset) => { - insertMediaAssetBlock(asset); + insertMediaAssetRef.current(asset); }, insertMindmapAsset: (args: { documentId: string; mindmapId: string }) => { - insertMindmapBlock(args); + insertMindmapRef.current(args); }, insertOnlineTableAsset: (args: { documentId: string; tableId: string }) => { - insertOnlineTableBlock(args); + insertOnlineTableRef.current(args); }, insertInlineReference: (target: ReferenceTarget, aliasText?: string) => { - editor.focus(); - const cursor = editor.getTextCursorPosition(); + const currentEditor = editorRef.current; + if (!currentEditor) return { blockId: null }; + currentEditor.focus(); + const cursor = currentEditor.getTextCursorPosition(); const blockId = cursor?.block?.id ?? null; const text = aliasText || target.title || "无标题"; - editor.insertInlineContent([ + currentEditor.insertInlineContent([ { type: "link", href: buildDocumentPath(target.id), @@ -1140,12 +1194,14 @@ const computeDocumentStats = (blocks: Block[]): DocumentStats return { blockId }; }, insertEmbedReference: (target: ReferenceTarget) => { - editor.focus(); - const cursor = editor.getTextCursorPosition(); + const currentEditor = editorRef.current; + if (!currentEditor) return { blockId: null }; + currentEditor.focus(); + const cursor = currentEditor.getTextCursorPosition(); const referenceBlock = - cursor?.block ?? editor.topLevelBlocks[editor.topLevelBlocks.length - 1]; + cursor?.block ?? currentEditor.topLevelBlocks[currentEditor.topLevelBlocks.length - 1]; const blockId = generateBlockId(); - editor.insertBlocks( + currentEditor.insertBlocks( [ { id: blockId, @@ -1162,7 +1218,9 @@ const computeDocumentStats = (blocks: Block[]): DocumentStats return { blockId }; }, replaceWithSnapshot: (payload: Json) => { - editor.focus(); + const currentEditor = editorRef.current; + if (!currentEditor) return; + currentEditor.focus(); const nextBlocks = Array.isArray(payload) ? payload : Array.isArray((payload as { blocks?: Json }).blocks) @@ -1171,20 +1229,29 @@ const computeDocumentStats = (blocks: Block[]): DocumentStats if (!Array.isArray(nextBlocks)) { return; } - editor.replaceBlocks(editor.topLevelBlocks, nextBlocks as never); + currentEditor.replaceBlocks(currentEditor.topLevelBlocks, nextBlocks as never); }, getCursorBlockId: () => { try { - const cursor = editor.getTextCursorPosition(); + const currentEditor = editorRef.current; + if (!currentEditor) return null; + const cursor = currentEditor.getTextCursorPosition(); return cursor?.block?.id ?? null; } catch { return null; } }, - }; - registerEditorBridge(bridge); + }), + [], + ); + + useEffect(() => { + registerEditorBridge(editor ? stableEditorBridge : null); + }, [editor, registerEditorBridge, stableEditorBridge]); + + useEffect(() => { return () => registerEditorBridge(null); - }, [editor, insertMediaAssetBlock, insertMindmapBlock, insertOnlineTableBlock, registerEditorBridge]); + }, [registerEditorBridge]); useEffect(() => { if (!editor) return; @@ -1336,9 +1403,6 @@ const computeDocumentStats = (blocks: Block[]): DocumentStats
- - - {/* 全屏表格编辑器 Modal */} {fullScreenTableId && ( import("@/components/editor/blocknote-editor").then((mod) => mod.BlockNoteEditor), @@ -36,6 +33,45 @@ const BlockNoteEditor = dynamic( }, ); +// 说明:这些组件都不是“进入文档页首屏”所必需。 +// 先拆成独立 chunk,避免 /documents/[id] 首次编译时把评论、历史、弹层、检查器等整串依赖一并拉进来。 +const PageOptionsSidebar = dynamic( + () => import("@/components/editor/page-options-sidebar").then((mod) => mod.PageOptionsSidebar), + { ssr: false }, +); + +const PageBacklinksPanel = dynamic( + () => import("@/components/editor/page-backlinks-panel").then((mod) => mod.PageBacklinksPanel), + { + ssr: false, + loading: () => null, + }, +); + +const DocumentHistoryDrawer = dynamic( + () => import("@/components/editor/document-history-drawer").then((mod) => mod.DocumentHistoryDrawer), + { + ssr: false, + loading: () => null, + }, +); + +const MoveEmbedPickerHost = dynamic( + () => import("@/components/documents/move-embed-picker-host").then((mod) => mod.MoveEmbedPickerHost), + { + ssr: false, + loading: () => null, + }, +); + +const DocumentCommentsDrawer = dynamic( + () => import("@/components/editor/document-comments-drawer").then((mod) => mod.DocumentCommentsDrawer), + { + ssr: false, + loading: () => null, + }, +); + export interface DocumentContentProps { documentId: string; workspaceId: string; @@ -358,11 +394,11 @@ export function DocumentContent({ async (nextTitle: string) => { if (readOnly) return; const payload = nextTitle.trim() || "无标题"; - await fetch("/api/documents/title", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ documentId, workspaceId, title: payload }), - }); + try { + await renameDocumentCommand({ documentId, workspaceId, title: payload }); + } catch (error) { + console.error("更新页面标题失败", error); + } }, [documentId, readOnly, workspaceId], ); @@ -590,14 +626,15 @@ export function DocumentContent({ excludeIds: [documentId], onPick: async (mode, targetId) => { if (mode === "move") { - const resp = await fetch("/api/documents/move", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ documentId, parentId: targetId ?? null, position: 999999 }), - }); - if (!resp.ok) { - const payload = await resp.json().catch(() => ({})); - window.alert(payload?.error ?? "移动失败"); + try { + await moveDocumentCommand({ + documentId, + parentId: targetId ?? null, + position: 999999, + workspaceId, + }); + } catch (error) { + window.alert(error instanceof Error ? error.message : "移动失败"); return; } window.alert("移动成功"); @@ -640,7 +677,18 @@ export function DocumentContent({ const formattedUpdatedAt = useMemo(() => { if (!updatedAt) return ""; - return new Date(updatedAt).toLocaleString(); + const date = new Date(updatedAt); + if (Number.isNaN(date.getTime())) return ""; + return new Intl.DateTimeFormat("zh-CN", { + timeZone: "Asia/Shanghai", + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).format(date); }, [updatedAt]); const handleExport = useCallback(() => { @@ -924,6 +972,7 @@ export function DocumentContent({ history={history} onRestore={handleRestoreSnapshot} /> + { - const response = await fetch("/api/documents/create-child", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + try { + const payload = await createChildDocumentCommand({ parentId: currentDocumentId, title: extractText(block), blocks: [block], - }), - }); + }); - if (!response.ok) { - return; + editor.replaceBlocks( + [block.id], + [ + { + type: "pageReference", + props: { pageId: payload.pageId, title: payload.title, asChildPage: true }, + } as PartialBlock, + ], + ); + router.refresh(); + } catch (error) { + console.error("块转页面失败", error); } - - const { pageId, title } = await response.json(); - - editor.replaceBlocks( - [block.id], - [ - { - type: "pageReference", - props: { pageId, title, asChildPage: true }, - } as PartialBlock, - ], - ); - router.refresh(); }, [block, currentDocumentId, editor, router]); const handleMoveEmbedPick = useCallback( diff --git a/wolai-frontend/src/components/editor/page-backlinks-panel.tsx b/wolai-frontend/src/components/editor/page-backlinks-panel.tsx index 945ef835..d1a1e374 100644 --- a/wolai-frontend/src/components/editor/page-backlinks-panel.tsx +++ b/wolai-frontend/src/components/editor/page-backlinks-panel.tsx @@ -39,15 +39,26 @@ const BacklinkItem = ({ record }: { record: BacklinkRecord }) => ( ); export function PageBacklinksPanel({ workspaceId, documentId, className, defaultCollapsed }: PageBacklinksPanelProps) { + const [isReady, setIsReady] = useState(false); const { data, isLoading, error, refetch, isFetching } = useBacklinks({ workspaceId, documentId, + enabled: isReady, }); const records = useMemo(() => data ?? [], [data]); // 说明:collapsed 需要可交互;这里用一个轻量的局部状态,但默认值来自 props(用于“自定义页面:折叠引用列表”)。 const [isCollapsed, setIsCollapsed] = useState(Boolean(defaultCollapsed)); useEffect(() => setIsCollapsed(Boolean(defaultCollapsed)), [defaultCollapsed]); + useEffect(() => { + setIsReady(false); + const timer = window.setTimeout(() => { + setIsReady(true); + }, 600); + return () => { + window.clearTimeout(timer); + }; + }, [documentId, workspaceId]); // 避免页面切换时“先出现加载态、随后又消失”的闪烁: // 当尚未拿到任何记录且正在加载时,直接不渲染面板。 if (!error && records.length === 0 && (isLoading || isFetching)) { diff --git a/wolai-frontend/src/components/sidebar/MnoteWebTreeShell.tsx b/wolai-frontend/src/components/sidebar/MnoteWebTreeShell.tsx new file mode 100644 index 00000000..6fb97450 --- /dev/null +++ b/wolai-frontend/src/components/sidebar/MnoteWebTreeShell.tsx @@ -0,0 +1,353 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; + +const TREE_SHELL_CHANNEL = "mnote-tree-shell-v1"; +const TREE_SHELL_PATH = "/tree"; + +export type MnoteTreeShellMode = "page" | "picker" | "filetree"; + +type TreeShellMessage = + | { + channel?: string; + type?: string; + documentId?: string; + assetId?: string; + rowId?: string; + rowKind?: string; + x?: number; + y?: number; + target?: { documentId?: string }; + payload?: { + documentId?: string; + assetId?: string; + rowId?: string; + rowKind?: string; + x?: number; + y?: number; + }; + } + | null + | undefined; + +type MnoteWebTreeShellProps = { + workspaceId: string | null; + activeDocumentId?: string; + actorId?: string | null; + mode?: MnoteTreeShellMode; + allowRootPick?: boolean; + excludeIds?: string[]; + reloadToken?: number; + onNavigate: (documentId: string) => void; + onPick?: (documentId: string | null) => void; + onOpenAsset?: (assetId: string) => void; + onOpenContextMenu?: (args: { documentId: string; x: number; y: number }) => void; + onOpenFileTreeContextMenu?: (args: { + documentId?: string; + assetId?: string; + rowId?: string; + rowKind?: string; + x: number; + y: number; + }) => void; + onRefresh: () => Promise; + fallback: React.ReactNode; +}; + +function buildShellUrl( + baseUrl: string, + workspaceId: string | null, + activeDocumentId: string | undefined, + actorId: string | null | undefined, + mode: MnoteTreeShellMode, + allowRootPick: boolean, + excludeIds: string[], + refreshKey: number, + reloadToken: number, +) { + const url = new URL(TREE_SHELL_PATH, `${baseUrl}/`); + if (workspaceId) { + url.searchParams.set("workspaceId", workspaceId); + } + if (activeDocumentId) { + url.searchParams.set("activeDocumentId", activeDocumentId); + } + if (actorId && actorId.trim()) { + url.searchParams.set("actorId", actorId.trim()); + } + url.searchParams.set("mode", mode); + if (mode === "picker") { + url.searchParams.set("allowRootPick", allowRootPick ? "1" : "0"); + if (excludeIds.length > 0) { + url.searchParams.set("excludeIds", excludeIds.join(",")); + } + } + url.searchParams.set("host", "wolai-frontend"); + url.searchParams.set("channel", TREE_SHELL_CHANNEL); + url.searchParams.set("v", `${refreshKey}-${reloadToken}`); + return url.toString(); +} + +function extractDocumentId(message: Exclude) { + const direct = typeof message.documentId === "string" ? message.documentId.trim() : ""; + if (direct) return direct; + + const fromTarget = + message.target && typeof message.target.documentId === "string" + ? message.target.documentId.trim() + : ""; + if (fromTarget) return fromTarget; + + const fromPayload = + message.payload && typeof message.payload.documentId === "string" + ? message.payload.documentId.trim() + : ""; + return fromPayload; +} + +function extractAssetId(message: Exclude) { + const direct = typeof message.assetId === "string" ? message.assetId.trim() : ""; + if (direct) return direct; + + const fromPayload = + message.payload && typeof message.payload.assetId === "string" + ? message.payload.assetId.trim() + : ""; + return fromPayload; +} + +function extractCoordinate( + message: Exclude, + axis: "x" | "y", +) { + const direct = typeof message[axis] === "number" ? message[axis] : null; + if (typeof direct === "number" && Number.isFinite(direct)) return direct; + + const fromPayload = + message.payload && typeof message.payload[axis] === "number" + ? message.payload[axis] + : null; + if (typeof fromPayload === "number" && Number.isFinite(fromPayload)) return fromPayload; + + return null; +} + +function extractTextField( + message: Exclude, + field: "rowId" | "rowKind", +) { + const direct = typeof message[field] === "string" ? message[field].trim() : ""; + if (direct) return direct; + + const fromPayload = + message.payload && typeof message.payload[field] === "string" + ? message.payload[field].trim() + : ""; + return fromPayload; +} + +export function MnoteWebTreeShell({ + workspaceId, + activeDocumentId, + actorId, + mode = "page", + allowRootPick = false, + excludeIds = [], + reloadToken = 0, + onNavigate, + onPick, + onOpenAsset, + onOpenContextMenu, + onOpenFileTreeContextMenu, + onRefresh, + fallback, +}: MnoteWebTreeShellProps) { + const runtime = useMemo(() => getMnoteRuntimeConfig(), []); + const baseUrl = (runtime.mnoteWebBaseUrl ?? "").trim().replace(/\/+$/, ""); + const treeShellEnabled = runtime.mnoteWebTreeShellEnabled === true; + const iframeRef = useRef(null); + const readyTimerRef = useRef(null); + const [refreshKey, setRefreshKey] = useState(0); + const [failedShellUrl, setFailedShellUrl] = useState(null); + + const shellUrl = useMemo(() => { + if (!baseUrl || !treeShellEnabled) return null; + return buildShellUrl( + baseUrl, + workspaceId, + activeDocumentId, + actorId, + mode, + allowRootPick, + excludeIds, + refreshKey, + reloadToken, + ); + }, [ + activeDocumentId, + actorId, + allowRootPick, + baseUrl, + excludeIds, + mode, + reloadToken, + refreshKey, + treeShellEnabled, + workspaceId, + ]); + + useEffect(() => { + if (!shellUrl) return; + if (readyTimerRef.current) { + window.clearTimeout(readyTimerRef.current); + } + // 说明:当前 shell 仍处于渐进接入期,若路由不存在或页面未按协议 ready, + // 前端应自动回退到旧 React 树,而不是让用户看到空白 iframe。 + readyTimerRef.current = window.setTimeout(() => { + setFailedShellUrl(shellUrl); + }, 2500); + return () => { + if (readyTimerRef.current) { + window.clearTimeout(readyTimerRef.current); + readyTimerRef.current = null; + } + }; + }, [shellUrl]); + + useEffect(() => { + if (!baseUrl || !treeShellEnabled) return; + + const expectedOrigin = (() => { + try { + return new URL(baseUrl).origin; + } catch { + return ""; + } + })(); + + const onMessage = (event: MessageEvent) => { + if (!expectedOrigin || event.origin !== expectedOrigin) return; + if (event.source !== iframeRef.current?.contentWindow) return; + + const message = event.data; + if (!message || typeof message !== "object") return; + if (message.channel !== TREE_SHELL_CHANNEL) return; + + const type = typeof message.type === "string" ? message.type.trim() : ""; + if (!type) return; + + if (type === "tree.ready" || type === "ready") { + if (readyTimerRef.current) { + window.clearTimeout(readyTimerRef.current); + readyTimerRef.current = null; + } + return; + } + + if (type === "tree.navigate" || type === "navigate") { + const documentId = extractDocumentId(message); + if (documentId) { + onNavigate(documentId); + } + return; + } + + if (type === "tree.pick" || type === "picker.pick") { + onPick?.(extractDocumentId(message) || null); + return; + } + + if (type === "tree.pick.root" || type === "picker.pick.root") { + onPick?.(null); + return; + } + + if (type === "tree.asset.open" || type === "filetree.asset.open") { + const assetId = extractAssetId(message); + if (assetId) { + onOpenAsset?.(assetId); + } + return; + } + + if (type === "tree.context-menu" || type === "tree.page.context-menu") { + const documentId = extractDocumentId(message); + const x = extractCoordinate(message, "x"); + const y = extractCoordinate(message, "y"); + if (!documentId || x === null || y === null) { + return; + } + const iframeRect = iframeRef.current?.getBoundingClientRect(); + if (!iframeRect) { + return; + } + onOpenContextMenu?.({ + documentId, + x: iframeRect.left + x, + y: iframeRect.top + y, + }); + return; + } + + if (type === "tree.filetree.context-menu") { + const documentId = extractDocumentId(message) || undefined; + const assetId = extractAssetId(message) || undefined; + const rowId = extractTextField(message, "rowId") || undefined; + const rowKind = extractTextField(message, "rowKind") || undefined; + const x = extractCoordinate(message, "x"); + const y = extractCoordinate(message, "y"); + if (x === null || y === null) { + return; + } + const iframeRect = iframeRef.current?.getBoundingClientRect(); + if (!iframeRect) { + return; + } + onOpenFileTreeContextMenu?.({ + documentId, + assetId, + rowId, + rowKind, + x: iframeRect.left + x, + y: iframeRect.top + y, + }); + return; + } + + if ( + type === "tree.node.created" || + type === "tree.node.renamed" || + type === "tree.subtree.moved" || + type === "tree.refresh" || + type === "refresh" + ) { + void onRefresh().finally(() => { + setRefreshKey((value) => value + 1); + }); + } + }; + + window.addEventListener("message", onMessage); + return () => window.removeEventListener("message", onMessage); + }, [baseUrl, onNavigate, onOpenAsset, onOpenContextMenu, onOpenFileTreeContextMenu, onPick, onRefresh, treeShellEnabled]); + + if (!shellUrl || failedShellUrl === shellUrl) { + return <>{fallback}; + } + + return ( +
+