{"p":".clawd-todos.json","s":1,"e":52,"m":1776672601718.6682,"t":"[\n  {\n    \"content\": \"审计真实 documents.content/documents.save/Convex content contract\",\n    \"activeForm\": \"审计真实 documents.content/documents.save/Convex content contract\",\n    \"status\": \"completed\"\n  },\n  {\n    \"content\": \"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\n    \"activeForm\": \"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\n    \"status\": \"in_progress\"\n  },\n  {\n    \"content\": \"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\n    \"activeForm\": \"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\n    \"status\": \"in_progress\"\n  },\n  {\n    \"content\": \"补 iframe 高度同步\",\n    \"activeForm\": \"补 iframe 高度同步\",\n    \"status\": \"in_progress\"\n  },\n  {\n    \"content\": \"建立 stored content 到 tiptap doc 的正式 adapter\",\n    \"activeForm\": \"建立 stored content 到 tiptap doc 的正式 adapter\",\n    \"status\": \"pending\"\n  },\n  {\n    \"content\": \"建立 tiptap doc 到 stored content 的正式 adapter\",\n    \"activeForm\": \"建立 tiptap doc 到 stored content 的正式 adapter\",\n    \"status\": \"pending\"\n  },\n  {\n    \"content\": \"接通真实保存与 persisted meta 回写\",\n    \"activeForm\": \"接通真实保存与 persisted meta 回写\",\n    \"status\": \"pending\"\n  },\n  {\n    \"content\": \"固定 /documents/[id] 默认直接编辑态\",\n    \"activeForm\": \"固定 /documents/[id] 默认直接编辑态\",\n    \"status\": \"pending\"\n  },\n  {\n    \"content\": \"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\n    \"activeForm\": \"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\n    \"status\": \"pending\"\n  },\n  {\n    \"content\": \"补验收脚本并完成主编辑器人工验收准备\",\n    \"activeForm\": \"补验收脚本并完成主编辑器人工验收准备\",\n    \"status\": \"pending\"\n  }\n]","v":"[encoded data, 4096 characters]"}
{"p":".gitignore","s":1,"e":60,"m":1778065346968.3103,"t":"node_modules/\n**/node_modules/\n.next/\n**/.next/\ndist/\n**/dist/\nbuild/\n**/build/\n.turbo/\npnpm-debug.log*\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n*.log\n.DS_Store\n.env\n.env.*\nenv-archive/\n.vscode/\n*.local\ntmp/\ntemp/\n**/.venv/\n.venv/\n**/__pycache__/\n__pycache__/\n# Convex 本机开发临时目录\n**/.convex-tmp/\n# 大模型文件，防止超出 LFS 配额\nservices/mineru/models/\n*.safetensors\n# ONLYOFFICE 挂载/同步目录（体积很大，禁止入库）\n/onlyoffice-web-apps/\n/onlyoffice-plugins/\n/onlyoffice-data/\n/onlyoffice-fonts/\n/onlyoffice-pg-data/\n# ONLYOFFICE 现已移至 src/components/onlyoffice 下，整目录忽略\n/src/components/onlyoffice/\n# 参考资料目录\ncankao/\n# services 整体忽略（包含本地模型与临时服务）\n/services/\n**/test/\npw-tests/**\npw-tests/  \nwolai-frontend/public/documents/\nwolai-frontend/public/documents/**\nartifacts/\nartifacts/**\ntmp\n\n# Rust 本地构建与调试产物\n/rust/target/\n/rust/spikes/**/target/\n\n# Harness 本地运行状态/调试产物\n/.harness-active\n/.harness-stop-counter\n/harness-tasks.json.bak","v":"[encoded data, 4096 characters]"}
{"p":".gitignore","s":49,"e":80,"m":1778065346968.3103,"t":"artifacts/\nartifacts/**\ntmp\n\n# Rust 本地构建与调试产物\n/rust/target/\n/rust/spikes/**/target/\n\n# Harness 本地运行状态/调试产物\n/.harness-active\n/.harness-stop-counter\n/harness-tasks.json.bak\n\n# pnpm 本地缓存\n/.pnpm-store/\n.playwright-mcp\nrust/spikes/leptos-tiptap-spike/trunk-8123.err\nrust/spikes/leptos-tiptap-spike/trunk-8123.out\ndesign/05-editor-mainline/reference-code\n\n# 本地 Wolai 对标取证与截图产物\n/test-results/\n/wolai-*.png\n/wolai-*.md\n\n# 任务型浏览器 smoke 只作本地验收，不纳入项目运行面\n/scripts/task*-wolai-*.js\n/scripts/task*-rust-web-wolai-*-smoke.js\n\n# 下载的 Wolai 静态页面参考包，不作为可维护设计稿入库\ndesign/design/html/\nrecycle","v":"[encoded data, 4096 characters]"}
{"p":".mcp.json","s":1,"e":34,"m":1767229535357.0203,"t":"{\n  \"mcpServers\": {\n    \"supabase_local\": {\n      \"type\": \"stdio\",\n      \"command\": \"cmd\",\n      \"args\": [\"/c\", \"npx\", \"-y\", \"mcp-remote@latest\", \"http://127.0.0.1:18000/mcp\"],\n      \"startup_timeout_ms\": 60000,\n      \"env\": {\n        \"PROGRAMFILES\": \"C:\\\\Program Files\",\n        \"SystemRoot\": \"C:\\\\Windows\"\n      }\n    },\n    \"chrome-devtools\": {\n      \"type\": \"stdio\",\n      \"command\": \"cmd\",\n      \"args\": [\"/c\", \"npx\", \"-y\", \"chrome-devtools-mcp@latest\"],\n      \"startup_timeout_ms\": 60000,\n      \"env\": {\n        \"PROGRAMFILES\": \"C:\\\\Program Files\",\n        \"SystemRoot\": \"C:\\\\Windows\"\n      }\n    },\n    \"context7\": {\n      \"type\": \"stdio\",\n      \"command\": \"cmd\",\n      \"args\": [\"/c\", \"npx\", \"-y\", \"@upstash/context7-mcp\", \"--api-key\", \"ctx7sk-8beae3e8-38a0-497a-a44d-a67c05669318\"],\n      \"startup_timeout_ms\": 40000,\n      \"env\": {\n        \"SystemRoot\": \"C:\\\\Windows\"\n      }\n    }\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"AGENTS.md","s":55,"e":60,"m":1778973402636.99,"t":"- 页面 AI 编辑长期主路径为 `mnote.doc.markdown_edit`（search/replace 或 full_content，markdown 文本级），在线文档和本地 `.md` 文件共用。当前过渡实现为 `POST /api/page-ai/block-edit-workflow` fast-path（`local_rule` planner，Phase B 退役）。`mnote.block.*` 保留为结构性辅助（拖拽排序等）。两层操作模型已获 CLI Main（Lark Doc）参考实现验证。流式 apply + suggest/review（参考 BlockNote AI）作为 Phase C 设计冻结，当前不实施。复杂任务进入 `/api/hermes/client/runs`。模型不直接产出 unified diff 或 blockId 操作；最终定位、校验和写入由 Rust runtime 负责。\n- 需要架构判断时，优先参考：\n  - `/mnt/Data1T/mnote/ARCHITECTURE.md`\n  - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`\n  - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`\n  - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`","v":"[encoded data, 4096 characters]"}
{"p":"AGENTS.md","s":49,"e":108,"m":1778973402636.99,"t":"- 前端可以做展示、交互和局部适配，但不要新增第二套树真相、排序真相或 projection 契约。\n- `mnote-web` 的 `compat route` 可以承接过渡流量，但不要把新的长期业务逻辑继续堆进 compat。\n- 涉及文档页标题、页面设置、正文保存、页头与树一致性时，优先判断是否应收口到 `Page Aggregate`，不要继续在页面壳或 island 外侧拼第二份页面真相。\n- 涉及页面新建、重命名、移动、归档、恢复、嵌入时，优先沿 `tree.*` 正式命名推进；`documents.*` 只视为兼容层，不应继续扩写为长期命令面。\n- 涉及 AI 读取、定位或精确编辑页面块时，优先沿 `mnote.doc.*` / `mnote.block.*` Hermes tools 与 Rust `EditorCommand` 推进；`mnote.page.save` 只作为页面级兜底写入工具。\n- 涉及资源归属（哪个页面拥有哪个 mindmap/附件）、object identity 和资源生命周期时，优先沿 Resource Tree → File Tree projection → `tree.resource.*` 命令面推进；`tree.*` 是正式命令面，`documents.*` 只视为兼容层。\n- 页面 AI 编辑长期主路径为 `mnote.doc.markdown_edit`（search/replace 或 full_content，markdown 文本级），在线文档和本地 `.md` 文件共用。当前过渡实现为 `POST /api/page-ai/block-edit-workflow` fast-path（`local_rule` planner，Phase B 退役）。`mnote.block.*` 保留为结构性辅助（拖拽排序等）。两层操作模型已获 CLI Main（Lark Doc）参考实现验证。流式 apply + suggest/review（参考 BlockNote AI）作为 Phase C 设计冻结，当前不实施。复杂任务进入 `/api/hermes/client/runs`。模型不直接产出 unified diff 或 blockId 操作；最终定位、校验和写入由 Rust runtime 负责。\n- 需要架构判断时，优先参考：\n  - `/mnt/Data1T/mnote/ARCHITECTURE.md`\n  - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`\n  - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`\n  - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`\n  - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`\n  - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`\n  - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`\n  - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`\n  - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`\n  - `/mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md`\n  - `/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md`\n  - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`\n  - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md`\n  - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-12-main-editor-object-tab-resource-alignment-checklist-v1.md`\n\n## 设计稿目录规则\n\n- `design/01-*` 到 `design/07-*` 主线大类统一按 `process/` 与 `done/` 分层。\n- 主线设计稿在推进中时放到对应大类的 `process/`。\n- 主线设计稿在当前真实代码中已完成后，必须移动到对应大类的 `done/`。\n- 已被后续实现或更新稿明确覆盖、但又不属于 `done` 的旧主线稿，必须移动到 `design/old/` 并标记为 `[recycle]`，不要继续占用活跃 `process/`。\n- `design/old/` 下的历史废弃稿也统一按 `process/` 与 `done/` 分层，但标题继续标记 `[recycle]`。\n- `design/90-reference/` 只放参考资料，不参与 `process/done` 状态迁移。\n\n## Bugs 目录规则\n\n- `bugs/` 默认镜像 `design/` 的主线分类方式，按对应大类放置缺陷。\n- 每个大类继续按 `process/` 与 `done/` 分层：\n  - `process/`：缺陷已确认存在，仍在修复或验证中\n  - `done/`：缺陷已修复，且已有真实代码与验证证据\n- 新确认的缺陷先放 `process/`；修复完成后必须移动到同类目的 `done/`，不要在两个目录同时保留同一条缺陷。\n- 缺陷归类以真正 owner 和长期主线为准，不以表面症状命名：\n  - `sidebar/topbar/breadcrumb/文档页壳/Wolai 体验对齐` 优先归到 `05-editor-mainline/`\n  - `projection/row model/selection/focus/keyboard/DnD/tree command` 优先归到 `04-tree-domain/`\n\n## 协作边界\n\n- 仅修改与当前任务直接相关的文件。\n- 不要擅自恢复、覆盖、删除用户已有改动。\n- 若发现与当前任务无关的脏改动，保持不动；若怀疑会影响当前任务，先确认再处理。\n- 若当前问题只是 UI 表现异常，先确认是否是实验性 tree shell、compat 路径、轮询或 fallback 混入首屏主链，而不是直接怀疑 Convex 本身。\n\n## 常用命令\n\n- 根目录热启动：`npm run desktop:hot`\n- 前端开发：`cd /mnt/Data1T/mnote/wolai-frontend && pnpm dev`\n- 前端检查：`cd /mnt/Data1T/mnote/wolai-frontend && pnpm lint && pnpm test`\n- 后端开发：`cd /mnt/Data1T/mnote/wolai-backend && uvicorn app.main:app --reload --port 8000`\n- Convex 自托管：参考 `/mnt/Data1T/mnote/infra/convex/README.md`\n\n## 默认测试账号","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":1,"e":60,"m":1778945276797.0444,"t":"# MNOTE 当前架构梳理\n\n> 更新时间：2026-05-16\n>\n> **当前阶段：初步 MVP 已达成。** 3000 下文档页完整读写链路（标题、正文、页面设置、Page Aggregate）、Sidebar/File Tree/Page Tree 三层树模型、tree command（`tree.*` preferred）、tree realtime SSE（snapshot/delta/resync）、页面 AI 快速编辑（fast-path < 1s，当前 `local_rule` planner 为过渡实现，长期方向为 markdown 级编辑，见 7-14）均已通过真实浏览器 smoke 验证。剩余工作集中在单一真源收口、live cache 统一、AI 编辑路径从块级收敛到 markdown 文本层，而非继续证明架构可行性。\n\n本文只描述当前仓库中真实成立的主线结构，以及当前最优先的架构收口点。\n\n## 1. 当前主线结论\n\n当前仓库的主线不是：\n\n- `BlockNote-first`\n- `Mindmap-first`\n- “先拆掉 Convex 再谈 Rust”\n\n当前主线固定为：\n\n1. `tree-first graph kernel` 是长期对象真相层\n2. `Convex` 继续保留为当前自托管存储 / 实时 / 文件协作底座\n3. `mnote-web` 是当前 Rust Web 主执行面，负责 3000 gateway、server-first shell、query / command / projection / transport 与 realtime stream；Next App Router 已降为 legacy compat / island bundle source，不再是当前主入口\n4. 文档页默认主编辑器已切到页面内 `leptos-tiptap` island\n5. `BlockNote` 已退出文档页默认主路径，仅作为历史参考实现 / 对照材料保留\n6. 页面 AI 工具链已建立最小闭环：`POST /api/page-ai/block-edit-workflow` 提供快速编辑 route（当前 `local_rule` planner 为过渡态，长期方向为 `mnote.doc.markdown_edit` 文本级搜索替换，见 7-14）；Hermes tools（`mnote.doc.*` / `mnote.block.*`）承担复杂任务编排\n\n一句话收口：\n\n> **Rust 持有语义主导权，Convex 保留底座，前端逐步从重壳转向消费稳定 projection 与少量交互 island。**\n\n补充口径：\n\n> **主 Web 执行面当前以 `mnote-web` 为 3000 owner；Next App Router 只保留为 legacy compat、交互 island bundle source 与显式 debug/迁移辅助边界。**\n>\n> **页面 AI 块编辑当前以 fast-path `local_rule` planner（`/api/page-ai/block-edit-workflow`）为简单操作首选；Hermes agent（`mnote.doc.*` / `mnote.block.*` tools）保留为复杂任务编排器。**\n\n## 2. 当前主线目录\n\n- `/mnt/Data1T/mnote/wolai-frontend/`\n  当前 React island 与 legacy compat 代码来源；Next.js App Router 不再作为 3000 主入口，只在显式 legacy/debug 迁移边界继续服务重交互运行态与对照链。\n- `/mnt/Data1T/mnote/rust/crates/core-protocol/`\n  Kernel 类型、projection 协议、编辑器协议、树 / 图核心术语。\n- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/`\n  Kernel query / command、bridge transport、Tiptap <-> editor document 适配、命令解释层。\n- `/mnt/Data1T/mnote/rust/crates/mnote-web/`\n  Rust Web route、kernel projection、documents/search/mindmap shell、Hermes bridge、stream transport、compat，以及显式开启时的 debug shell；它是当前主 Web gateway / shell / transport owner。\n- `/mnt/Data1T/mnote/wolai-frontend/convex/`\n  仍在使用的 Convex functions 与数据侧逻辑。\n- `/mnt/Data1T/mnote/infra/convex/`\n  Convex 自托管部署与基础设施。\n- `/mnt/Data1T/mnote/wolai-backend/`\n  辅助后端与异步处理，不是当前页面主链。\n\n## 3. 当前页面运行结构\n\n### 3.1 根布局与 legacy island source\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/app/layout.tsx`\n\n职责：","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":49,"e":108,"m":1778945276797.0444,"t":"Convex 自托管部署与基础设施。\n- `/mnt/Data1T/mnote/wolai-backend/`\n  辅助后端与异步处理，不是当前页面主链。\n\n## 3. 当前页面运行结构\n\n### 3.1 根布局与 legacy island source\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/app/layout.tsx`\n\n职责：\n\n- 作为 legacy Next / React island bundle source 的根布局\n- 保留 Convex / Query Provider 等前端 island 运行时依赖\n- 服务显式 legacy/debug/迁移边界\n\n注意：\n\n> **3000 当前根入口与文档页 shell 由 `mnote-web` 持有；Next App Router 不再是 3000 主入口或页面根布局事实源。**\n\n### 3.2 工作区壳与 Sidebar\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx`\n- `/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-sidebar-data.ts`\n- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts`\n- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts`\n\n当前 Sidebar 不是单一路径，而是三层组合：\n\n1. 初始 SSR / route snapshot\n2. query / refetch snapshot\n3. tree stream live snapshot\n\n`PreferredSidebarSnapshotProvider` 负责在 query 与 tree stream 之间做 freshness 选择，再把同一份 preferred snapshot 同时给：\n\n- `Sidebar`\n- `Breadcrumb`\n- 文档页头标题消费链\n\n这说明当前工作区树链已经进入正式收口阶段：`3000` 主壳已直接接入 `/api/tree/events` 的 snapshot / delta / resync consumer，并有浏览器 smoke 验证；但 preferred snapshot、page subtree、filetree 与补偿链还没有完全统一成唯一 live cache。\n\n## 4. 文档页主链\n\n### 4.1 入口\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-loader.ts`\n- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`\n\n当前文档页读取主链已经不再由页面入口手工拼若干独立字段，而是：\n\n1. 页面 SSR 入口通过 `page-aggregate-loader.ts` 直接请求 Rust `/api/page-aggregate/:id`\n2. `DocumentContent` 的内容重试补拉也直接请求同一条 `/api/page-aggregate/:id` 正式读链\n3. 读取主链不再回退到 TS builder；snapshot 不可用或不可信时直接显式失败\n4. Next `/api/documents/page` 已退场为明确 `410` 的 compat 边界，不再参与文档页运行时主路径\n\n对应聚合类型定义：\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate.ts`\n\n### 4.2 文档壳","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":97,"e":156,"m":1778945276797.0444,"t":"当前文档页读取主链已经不再由页面入口手工拼若干独立字段，而是：\n\n1. 页面 SSR 入口通过 `page-aggregate-loader.ts` 直接请求 Rust `/api/page-aggregate/:id`\n2. `DocumentContent` 的内容重试补拉也直接请求同一条 `/api/page-aggregate/:id` 正式读链\n3. 读取主链不再回退到 TS builder；snapshot 不可用或不可信时直接显式失败\n4. Next `/api/documents/page` 已退场为明确 `410` 的 compat 边界，不再参与文档页运行时主路径\n\n对应聚合类型定义：\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate.ts`\n\n### 4.2 文档壳\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx`\n- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`\n\n`DocumentShell` 现在只是薄封装，文档页核心状态和交互壳集中在 `DocumentContent`。\n\n### 4.3 默认编辑器 host\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-config.ts`\n- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx`\n\n当前默认 host 为：\n\n- `leptos_tiptap_island`\n\n文档页主路径已不再暴露显式 debug host 选择；历史 `iframe_debug` 宿主仅剩源码参考，不再参与页面 host 选择面。\n\n### 4.4 正式主编辑器\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx`\n\n当前默认主编辑器已经是页面内 Leptos island，不再是 iframe bridge。\n\n它负责：\n\n- 加载 Leptos/WASM 编辑器本体\n- 接收初始化内容与 page options\n- 处理同页事件、命令、保存与状态同步\n\n### 4.5 BlockNote 的当前位置\n\n- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/editor/blocknote-editor.tsx`\n\n`BlockNote` 当前仍保留回收副本，但其定位已变成：\n\n- 历史参考实现\n- 对照材料 / 调试素材\n\n而不是默认主编辑器方向。\n\n## 5. 当前编辑器与页面聚合关系\n\n当前文档页已经开始消费统一的前端侧 `PageAggregateProjection`，但仍不能说“页面域已完全统一成 Rust 单一真源”。\n\n当前真实状态是：\n\n- 读取主链已优先消费 Rust `mnote.page_aggregate.v1` snapshot\n- Page Aggregate 已输出 `blockDocument`、`blockProjectionVersion` 与 `projectionSource`","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":145,"e":204,"m":1778945276797.0444,"t":"- 对照材料 / 调试素材\n\n而不是默认主编辑器方向。\n\n## 5. 当前编辑器与页面聚合关系\n\n当前文档页已经开始消费统一的前端侧 `PageAggregateProjection`，但仍不能说“页面域已完全统一成 Rust 单一真源”。\n\n当前真实状态是：\n\n- 读取主链已优先消费 Rust `mnote.page_aggregate.v1` snapshot\n- Page Aggregate 已输出 `blockDocument`、`blockProjectionVersion` 与 `projectionSource`\n- 标题链路已开始与工作区树 canonical snapshot 对齐\n- 正文默认由 `leptos-tiptap` island 编辑与保存\n- 页面设置已有一部分进入 island 运行时语义\n- `page_tree` / `pageSubtree` 已进入统一聚合入口\n- 页面/块 AI tools 已开始通过 Page Aggregate block projection 读取、定位、dry-run、替换、插入和受限移动块，并经 `page.body.save -> documents:updateContent` 持久化\n\n2026-05-16 更新：以下四项已通过真实 3000 browser smoke 验证（证据见 `tmp/page-aggregate-*` 目录）：\n\n- 标题写入后 Page Aggregate、页头、Breadcrumb、Sidebar、Page Tree、File Tree 一致性（`task110`）\n- 正文保存后 `body.revision` / `conflictDetectionKey` / `blockDocument` 同步（`task-page-aggregate-body-sync-smoke`）\n- 页面设置写入后 `layout.pageOptions` 与 island runtime DOM 同步，刷新后不退回（`task-page-aggregate-options-sync-smoke` / `task-page-aggregate-refresh-persistence-smoke`）\n- Convex 不可用时返回 degraded error（503 + `x-error-code=convex_unavailable`），不返回伪 fixture（route 级负向测试）\n\n仍未完成的关键点是：\n\n1. Rust 侧已提供最小 `Page Aggregate` snapshot 和 block projection v1，但当前 block projection 仍主要从 `documents.content` / local markdown content 投影（`projectionSource=documents.content`），不是 EditorBlockDocument 原生落库完成态\n2. 标题 / 正文 / 页面设置虽已收口到 `page.*` family 并通过 smoke，但客户端仍保留 `PageAggregateClientState` reducer（混合 server snapshot / draft title / local content），页面域单一真源仍未完全闭环\n3. AI 块工具最小闭环已启动且 fast-path < 1s，但 `scope=selection`、`format=page_xml/text`、多块插入、复杂块移动矩阵和持久审阅 UI 仍未完成\n4. Sidebar 仍通过 preferred snapshot（initial / query / tree_stream）做 freshness 选择，tree realtime live cache 未统一\n\n因此当前正确表述应是：\n\n> **文档页读取主链已进入 Rust-first `page aggregate` 过渡态，标题/正文/页面设置 smoke 已通过，但页面域单一真源（ClientState → Rust snapshot）与 tree realtime live cache 仍未闭环。**\n\n## 6. 当前树域结构\n\n### 6.1 Kernel projection 与 route\n\n- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/kernel.rs`\n\n当前 Rust 已提供：\n\n- `sidebar_tree`\n- `page_tree`\n- `file_tree`\n- `subtree`\n- `edges`\n- `graph`\n\n这些 route 说明树域 projection family 已经在 Rust 侧具备清晰边界。\n\n### 6.2 树命令现状\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tree-command-client.ts`\n- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs`\n- `/mnt/Data1T/mnote/rust/crates/storage-convex-bridge/src/mapping.rs`\n\n当前树命令已经开始以 `tree.*` 为 preferred command name，例如：","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":193,"e":252,"m":1778945276797.0444,"t":"- `edges`\n- `graph`\n\n这些 route 说明树域 projection family 已经在 Rust 侧具备清晰边界。\n\n### 6.2 树命令现状\n\n- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tree-command-client.ts`\n- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs`\n- `/mnt/Data1T/mnote/rust/crates/storage-convex-bridge/src/mapping.rs`\n\n当前树命令已经开始以 `tree.*` 为 preferred command name，例如：\n\n- `tree.node.create`\n- `tree.node.rename`\n- `tree.subtree.move`\n\n但兼容层仍广泛保留：\n\n- `documents.create`\n- `documents.title.update`\n- `documents.move`\n\n这说明 tree command cutover 已开始，但还没有完成。\n\n### 6.3 树 realtime 现状\n\n当前 Sidebar 已有：\n\n- query snapshot\n- tree stream\n- preferred snapshot 选择\n\n正式的 snapshot + delta 主链已经固定到 `mnote-web` 的 `/api/tree/events`，Next `/api/mnote-web/stream` 只保留 compat alias。\n\n因此当前正确表述应是：\n\n> **树域 realtime 主链由 Rust Web `/api/tree/events` 持有，前端只消费稳定 stream contract 与 projection。**\n\n补充：\n\n> **`3000` 主壳当前已不再通过 `/api/tree/projections/*` 额外维护第二条 runtime live fallback；snapshot / delta / resync 直接消费 `/api/tree/events` 载荷。**\n\n### 6.4 Resource Tree / File Tree / Page Tree 三层模型\n\n当前树域已建立三层语义（合同见 `design/04-tree-domain/done/4-24-*` 与 `design/05-editor-mainline/done/5-12-*`）：\n\n- **Resource Tree**：长期 canonical 对象组织树，Rust kernel 持有语义。资源类型包括 page、mindmap、attachment、onlyoffice、code 等（`KernelObjectIdentity`、`KernelProjectionResourceKind` 已在 `core-protocol` 落地）。\n- **File Tree**：Resource Tree 的主组织投影。展示页面文件夹、`{title}.md`（非 `index.md`）、mindmap、附件等资源。文件树行消费 Rust `/api/tree/projections/file` 或等价 projection，不在前端临时拼装第二真相。\n- **Page Tree**：面向阅读/导航的快捷投影。只显示页面关系和导航语义，不拥有排序、父子、附件归属的最终真相。\n\n关键规则：\n\n- 文件树点击 mindmap 打开 mindmap object editor，不被 `{title}.md` 吞掉\n- `{title}.md` 只代表页面正文（Page Aggregate body），mindmap / OnlyOffice / 附件是不同 object model\n- 页面树不持有独立结构真相，只显示文档导航关系\n\n## 7. Mindmap 与 OnlyOffice\n\n### 7.1 Mindmap","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":241,"e":300,"m":1778945276797.0444,"t":"- **File Tree**：Resource Tree 的主组织投影。展示页面文件夹、`{title}.md`（非 `index.md`）、mindmap、附件等资源。文件树行消费 Rust `/api/tree/projections/file` 或等价 projection，不在前端临时拼装第二真相。\n- **Page Tree**：面向阅读/导航的快捷投影。只显示页面关系和导航语义，不拥有排序、父子、附件归属的最终真相。\n\n关键规则：\n\n- 文件树点击 mindmap 打开 mindmap object editor，不被 `{title}.md` 吞掉\n- `{title}.md` 只代表页面正文（Page Aggregate body），mindmap / OnlyOffice / 附件是不同 object model\n- 页面树不持有独立结构真相，只显示文档导航关系\n\n## 7. Mindmap 与 OnlyOffice\n\n### 7.1 Mindmap\n\n当前 Mindmap 不再是系统中心，而是：\n\n- `tree-first graph kernel` 的一种视图 / 编辑挂件\n\n它仍有：\n\n- 文档内嵌块形态\n- 独立页面形态\n\n但不应再被理解为对象真相层。\n\n### 7.2 OnlyOffice\n\nOnlyOffice 仍然是：\n\n- 独立页面型编辑器\n\n它不直接嵌入正文主编辑画布；正文中通常通过附件块跳转进入。\n\n## 8. 当前最优先的架构收口\n\n当前最优先的三条主线，不是继续大规模 UI 重写，而是：\n\n### 8.1 Page Aggregate\n\n目标：\n\n- 让标题 / 页面设置 / 正文 / page tree 统一成同一组 page aggregate projection 与 command family\n- 前端不在页面壳、island 外侧、Sidebar preferred snapshot 外再拼第二份页面真相\n\n当前进度（2026-05-16）：\n\n- ✅ 文档页读取主链已 Rust-first，TS builder 退出 runtime，Next compat 返回 410\n- ✅ 标题单一真源 smoke 通过（`task110`）：页头/Breadcrumb/Sidebar/Page Tree/File Tree 一致\n- ✅ 正文写入后 `body.revision` / `blockDocument` 同步 smoke 通过\n- ✅ 页面设置写入后 `pageOptions` 同步与刷新持久化 smoke 通过\n- ✅ Convex 不可用时返回 degraded error，不返回伪 fixture\n- ⬜ block projection 从 EditorBlockDocument 原生落库（当前仍从 `documents.content` 投影）\n- ⬜ 客户端 `PageAggregateClientState` reducer 退役，页面域完全以 Rust snapshot 为单一运行时真相\n\n对应设计稿：\n\n- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`\n- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`\n- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`\n\n### 8.2 Tree Command Cutover","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":289,"e":348,"m":1778945276797.0444,"t":"- ✅ 页面设置写入后 `pageOptions` 同步与刷新持久化 smoke 通过\n- ✅ Convex 不可用时返回 degraded error，不返回伪 fixture\n- ⬜ block projection 从 EditorBlockDocument 原生落库（当前仍从 `documents.content` 投影）\n- ⬜ 客户端 `PageAggregateClientState` reducer 退役，页面域完全以 Rust snapshot 为单一运行时真相\n\n对应设计稿：\n\n- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`\n- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`\n- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`\n\n### 8.2 Tree Command Cutover\n\n目标：\n\n- 让 `tree.*` 成为正式命令面\n- `documents.*` 降为兼容层\n\n对应设计稿：\n\n- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`\n\n### 8.3 Tree Realtime Event Stream\n\n目标：\n\n- 让 snapshot + delta 正式成为树域实时主链\n- 减少当前 query/refetch/freshness 补偿链的长期存在\n\n对应设计稿：\n\n- `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`\n\n### 8.4 Page / Block AI Tooling\n\n目标：\n\n- 让 AI 读取、定位、dry-run 和精确块写入统一走 Rust Hermes tools、Page Aggregate block projection 与 `EditorCommand`\n- 把 `mnote.page.save` 固定为页面级兜底写入工具，不再代表长期精确块编辑主入口\n- 补齐 `scope=selection`、`format=page_xml/text`、多块插入、复杂块移动矩阵和持久审阅 UI\n\n当前进度（2026-05-16，按 7-14 v2 更新）：\n\n- ✅ 页面 AI 快速编辑 route 已上线：`POST /api/page-ai/block-edit-workflow`\n- ✅ 简单编辑本地 planner（`local_rule`）已验证：788ms（过渡实现，将被 `markdown_edit` 替代）\n- ✅ Hermes tools（`mnote.doc.*` / `mnote.block.*`）已可读取和写入块投影\n- ✅ 参考实现已确认两层模型可行性：CLI Main（Lark Doc）`str_replace` 对应 `markdown_edit`，`block_*` 对应 `apply_block_ops`\n- ⬜ **Phase A（当前唯一活跃实施）**：`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强（search/replace + `format: \"markdown\"` + local source + Hermes 注册）\n- ⬜ **Phase B（下一阶段）**：退役 `direct_block_edit_operations`，`page_ai_workflow.rs` 改走 `markdown_edit`，system prompt 产 search/replace 对\n- ⬜ `scope=selection`、`format=page_xml/text` 稳定输出\n- ⬜ tool manifest annotations（`readonly` / `destructive` / `requiresApproval` 等）\n- ⬜ **Phase C（设计冻结，不实施）**：`StreamApplyController` + `ReviewSession` + `GhostTextOverlay`（流式 apply + suggest/review）\n- ⬜ stale revision / idempotency 重放保护（已有 revision 乐观锁覆盖，补端到端 smoke）\n\n关键原则（2026-05-16 补充，按 7-14 v2 更新）：\n\n- **两层操作模型**（参考 CLI Main Lark Doc）：文本级 `mnote.doc.markdown_edit`（str_replace）为主路径（覆盖 80%+ 场景），块级 `mnote.doc.apply_block_ops` 为结构性辅助（< 20%）\n- AI 编辑主路径应从块级降维到 markdown 文本层：新增 `mnote.doc.markdown_edit`（search/replace 或 full_content）作为主要 AI 写入工具\n- `mnote.block.*` 降级为结构性辅助（拖拽排序、精确块删除等），不删除\n- 当前 `local_rule` planner（`direct_block_edit_operations`）是过渡实现，应退役并替换为模型产出 search/replace 对","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":337,"e":396,"m":1778945276797.0444,"t":"- ⬜ **Phase B（下一阶段）**：退役 `direct_block_edit_operations`，`page_ai_workflow.rs` 改走 `markdown_edit`，system prompt 产 search/replace 对\n- ⬜ `scope=selection`、`format=page_xml/text` 稳定输出\n- ⬜ tool manifest annotations（`readonly` / `destructive` / `requiresApproval` 等）\n- ⬜ **Phase C（设计冻结，不实施）**：`StreamApplyController` + `ReviewSession` + `GhostTextOverlay`（流式 apply + suggest/review）\n- ⬜ stale revision / idempotency 重放保护（已有 revision 乐观锁覆盖，补端到端 smoke）\n\n关键原则（2026-05-16 补充，按 7-14 v2 更新）：\n\n- **两层操作模型**（参考 CLI Main Lark Doc）：文本级 `mnote.doc.markdown_edit`（str_replace）为主路径（覆盖 80%+ 场景），块级 `mnote.doc.apply_block_ops` 为结构性辅助（< 20%）\n- AI 编辑主路径应从块级降维到 markdown 文本层：新增 `mnote.doc.markdown_edit`（search/replace 或 full_content）作为主要 AI 写入工具\n- `mnote.block.*` 降级为结构性辅助（拖拽排序、精确块删除等），不删除\n- 当前 `local_rule` planner（`direct_block_edit_operations`）是过渡实现，应退役并替换为模型产出 search/replace 对\n- 在线 Convex 文档和本地 `.md` 文件共用同一条 markdown AI 写入路径（`resolve_source` → Convex | LocalFS）\n- Markdown 既是 AI 编辑格式也是人类可读格式，不需要 XML 中间层\n- 流式 apply + suggest/review（参考 BlockNote AI 的 `StreamToolExecutor` + `suggestChanges`）仅作为 Phase C 设计冻结，当前不实施\n\n对应设计稿：\n\n- `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`\n- `/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md`\n- `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`\n\n### 8.5 定向 Bug Hunt 与质量基线\n\n目标：\n\n- 围绕已知架构风险做定向排查，而非泛泛\"找 bug\"\n\n优先关注：\n\n- Page Aggregate 回流不一致\n- AI 块写入 conflict / stale revision\n- File Tree `{title}.md` 与 page title 同步\n- tree stream resync / 双浏览器一致性\n- debug / compat / fallback 是否混入首屏主链\n\n已有 smoke 体系（`scripts/` 目录）：\n\n- `task110-page-title-single-truth-smoke.js`：标题全链路一致性\n- `task-page-aggregate-body-sync-smoke.js`：正文写入后 Page Aggregate 同步\n- `task-page-aggregate-options-sync-smoke.js`：页面设置写入后同步\n- `task-page-aggregate-refresh-persistence-smoke.js`：刷新持久化\n- `task123-rust-web-tree-live-stream-consumer-smoke.js`：tree realtime 消费\n- `task169-mindmap-realtime-smoke.js`：mindmap 隔离与持久化\n- `task179-tree-create-delete-no-reload-smoke.js`：树操作无刷新\n\n## 9. 当前不该再用的旧口径\n\n下面这些说法现在都不准确：\n\n- “默认文档页仍然是 `BlockNoteEditor`”\n- “主编辑器还没切到页面内 Leptos island”\n- “当前最优先是继续证明 `leptos-tiptap` 能不能跑”\n- “当前最优先是先做树域 UI 重构”\n- “Rust Web 还只是实验，没有接真实页面链路”\n- “页面 AI 编辑必须经过 Hermes agent，没有快路径”\n\n当前真正的卡点已经从“能不能跑”变成：\n\n> **单一真源如何收口（Page Aggregate ClientState → Rust snapshot），projection / command / realtime 三条链如何从兼容态进入正式主链，以及 AI 块编辑如何从“能写”变成产品级可靠（selection、review、conflict、idempotency）。**","v":"[encoded data, 4096 characters]"}
{"p":"ARCHITECTURE.md","s":385,"e":397,"m":1778945276797.0444,"t":"下面这些说法现在都不准确：\n\n- “默认文档页仍然是 `BlockNoteEditor`”\n- “主编辑器还没切到页面内 Leptos island”\n- “当前最优先是继续证明 `leptos-tiptap` 能不能跑”\n- “当前最优先是先做树域 UI 重构”\n- “Rust Web 还只是实验，没有接真实页面链路”\n- “页面 AI 编辑必须经过 Hermes agent，没有快路径”\n\n当前真正的卡点已经从“能不能跑”变成：\n\n> **单一真源如何收口（Page Aggregate ClientState → Rust snapshot），projection / command / realtime 三条链如何从兼容态进入正式主链，以及 AI 块编辑如何从“能写”变成产品级可靠（selection、review、conflict、idempotency）。**","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":1,"e":49,"m":1776084302914.8396,"t":"# Design System Inspired by Notion\n\n## 1. Visual Theme & Atmosphere\n\nNotion's website embodies the philosophy of the tool itself: a blank canvas that gets out of your way. The design system is built on warm neutrals rather than cold grays, creating a distinctly approachable minimalism that feels like quality paper rather than sterile glass. The page canvas is pure white (`#ffffff`) but the text isn't pure black -- it's a warm near-black (`rgba(0,0,0,0.95)`) that softens the reading experience imperceptibly. The warm gray scale (`#f6f5f4`, `#31302e`, `#615d59`, `#a39e98`) carries subtle yellow-brown undertones, giving the interface a tactile, almost analog warmth.\n\nThe custom NotionInter font (a modified Inter) is the backbone of the system. At display sizes (64px), it uses aggressive negative letter-spacing (-2.125px), creating headlines that feel compressed and precise. The weight range is broader than typical systems: 400 for body, 500 for UI elements, 600 for semi-bold labels, and 700 for display headings. OpenType features `\"lnum\"` (lining numerals) and `\"locl\"` (localized forms) are enabled on larger text, adding typographic sophistication that rewards close reading.\n\nWhat makes Notion's visual language distinctive is its border philosophy. Rather than heavy borders or shadows, Notion uses ultra-thin `1px solid rgba(0,0,0,0.1)` borders -- borders that exist as whispers, barely perceptible division lines that create structure without weight. The shadow system is equally restrained: multi-layer stacks with cumulative opacity never exceeding 0.05, creating depth that's felt rather than seen.\n\n**Key Characteristics:**\n- NotionInter (modified Inter) with negative letter-spacing at display sizes (-2.125px at 64px)\n- Warm neutral palette: grays carry yellow-brown undertones (`#f6f5f4` warm white, `#31302e` warm dark)\n- Near-black text via `rgba(0,0,0,0.95)` -- not pure black, creating micro-warmth\n- Ultra-thin borders: `1px solid rgba(0,0,0,0.1)` throughout -- whisper-weight division\n- Multi-layer shadow stacks with sub-0.05 opacity for barely-there depth\n- Notion Blue (`#0075de`) as the singular accent color for CTAs and interactive elements\n- Pill badges (9999px radius) with tinted blue backgrounds for status indicators\n- 8px base spacing unit with an organic, non-rigid scale\n\n## 2. Color Palette & Roles\n\n### Primary\n- **Notion Black** (`rgba(0,0,0,0.95)` / `#000000f2`): Primary text, headings, body copy. The 95% opacity softens pure black without sacrificing readability.\n- **Pure White** (`#ffffff`): Page background, card surfaces, button text on blue.\n- **Notion Blue** (`#0075de`): Primary CTA, link color, interactive accent -- the only saturated color in the core UI chrome.\n\n### Brand Secondary\n- **Deep Navy** (`#213183`): Secondary brand color, used sparingly for emphasis and dark feature sections.\n- **Active Blue** (`#005bab`): Button active/pressed state -- darker variant of Notion Blue.\n\n### Warm Neutral Scale\n- **Warm White** (`#f6f5f4`): Background surface tint, section alternation, subtle card fill. The yellow undertone is key.\n- **Warm Dark** (`#31302e`): Dark surface background, dark section text. Warmer than standard grays.\n- **Warm Gray 500** (`#615d59`): Secondary text, descriptions, muted labels.\n- **Warm Gray 300** (`#a39e98`): Placeholder text, disabled states, caption text.\n\n### Semantic Accent Colors\n- **Teal** (`#2a9d99`): Success states, positive indicators.\n- **Green** (`#1aae39`): Confirmation, completion badges.\n- **Orange** (`#dd5b00`): Warning states, attention indicators.\n- **Pink** (`#ff64c8`): Decorative accent, feature highlights.\n- **Purple** (`#391c57`): Premium features, deep accents.\n- **Brown** (`#523410`): Earthy accent, warm feature sections.\n\n### Interactive\n- **Link Blue** (`#0075de`): Primary link color with underline-on-hover.\n- **Link Light Blue** (`#62aef0`): Lighter link variant for dark backgrounds.\n- **Focus Blue** (`#097fe8`): Focus ring on interactive elements.","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":50,"e":60,"m":1776084302914.8396,"t":"- **Badge Blue Bg** (`#f2f9ff`): Pill badge background, tinted blue surface.\n- **Badge Blue Text** (`#097fe8`): Pill badge text, darker blue for readability.\n\n### Shadows & Depth\n- **Card Shadow** (`rgba(0,0,0,0.04) 0px 4px 18px, rgba(0,0,0,0.027) 0px 2.025px 7.84688px, rgba(0,0,0,0.02) 0px 0.8px 2.925px, rgba(0,0,0,0.01) 0px 0.175px 1.04062px`): Multi-layer card elevation.\n- **Deep Shadow** (`rgba(0,0,0,0.01) 0px 1px 3px, rgba(0,0,0,0.02) 0px 3px 7px, rgba(0,0,0,0.02) 0px 7px 15px, rgba(0,0,0,0.04) 0px 14px 28px, rgba(0,0,0,0.05) 0px 23px 52px`): Five-layer deep elevation for modals and featured content.\n- **Whisper Border** (`1px solid rgba(0,0,0,0.1)`): Standard division border -- cards, dividers, sections.\n\n## 3. Typography Rules\n\n### Font Family","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":49,"e":101,"m":1776084302914.8396,"t":"- **Focus Blue** (`#097fe8`): Focus ring on interactive elements.\n- **Badge Blue Bg** (`#f2f9ff`): Pill badge background, tinted blue surface.\n- **Badge Blue Text** (`#097fe8`): Pill badge text, darker blue for readability.\n\n### Shadows & Depth\n- **Card Shadow** (`rgba(0,0,0,0.04) 0px 4px 18px, rgba(0,0,0,0.027) 0px 2.025px 7.84688px, rgba(0,0,0,0.02) 0px 0.8px 2.925px, rgba(0,0,0,0.01) 0px 0.175px 1.04062px`): Multi-layer card elevation.\n- **Deep Shadow** (`rgba(0,0,0,0.01) 0px 1px 3px, rgba(0,0,0,0.02) 0px 3px 7px, rgba(0,0,0,0.02) 0px 7px 15px, rgba(0,0,0,0.04) 0px 14px 28px, rgba(0,0,0,0.05) 0px 23px 52px`): Five-layer deep elevation for modals and featured content.\n- **Whisper Border** (`1px solid rgba(0,0,0,0.1)`): Standard division border -- cards, dividers, sections.\n\n## 3. Typography Rules\n\n### Font Family\n- **Primary**: `NotionInter`, with fallbacks: `Inter, -apple-system, system-ui, Segoe UI, Helvetica, Apple Color Emoji, Arial, Segoe UI Emoji, Segoe UI Symbol`\n- **OpenType Features**: `\"lnum\"` (lining numerals) and `\"locl\"` (localized forms) enabled on display and heading text.\n\n### Hierarchy\n\n| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |\n|------|------|------|--------|-------------|----------------|-------|\n| Display Hero | NotionInter | 64px (4.00rem) | 700 | 1.00 (tight) | -2.125px | Maximum compression, billboard headlines |\n| Display Secondary | NotionInter | 54px (3.38rem) | 700 | 1.04 (tight) | -1.875px | Secondary hero, feature headlines |\n| Section Heading | NotionInter | 48px (3.00rem) | 700 | 1.00 (tight) | -1.5px | Feature section titles, with `\"lnum\"` |\n| Sub-heading Large | NotionInter | 40px (2.50rem) | 700 | 1.50 | normal | Card headings, feature sub-sections |\n| Sub-heading | NotionInter | 26px (1.63rem) | 700 | 1.23 (tight) | -0.625px | Section sub-titles, content headers |\n| Card Title | NotionInter | 22px (1.38rem) | 700 | 1.27 (tight) | -0.25px | Feature cards, list titles |\n| Body Large | NotionInter | 20px (1.25rem) | 600 | 1.40 | -0.125px | Introductions, feature descriptions |\n| Body | NotionInter | 16px (1.00rem) | 400 | 1.50 | normal | Standard reading text |\n| Body Medium | NotionInter | 16px (1.00rem) | 500 | 1.50 | normal | Navigation, emphasized UI text |\n| Body Semibold | NotionInter | 16px (1.00rem) | 600 | 1.50 | normal | Strong labels, active states |\n| Body Bold | NotionInter | 16px (1.00rem) | 700 | 1.50 | normal | Headlines at body size |\n| Nav / Button | NotionInter | 15px (0.94rem) | 600 | 1.33 | normal | Navigation links, button text |\n| Caption | NotionInter | 14px (0.88rem) | 500 | 1.43 | normal | Metadata, secondary labels |\n| Caption Light | NotionInter | 14px (0.88rem) | 400 | 1.43 | normal | Body captions, descriptions |\n| Badge | NotionInter | 12px (0.75rem) | 600 | 1.33 | 0.125px | Pill badges, tags, status labels |\n| Micro Label | NotionInter | 12px (0.75rem) | 400 | 1.33 | 0.125px | Small metadata, timestamps |\n\n### Principles\n- **Compression at scale**: NotionInter at display sizes uses -2.125px letter-spacing at 64px, progressively relaxing to -0.625px at 26px and normal at 16px. The compression creates density at headlines while maintaining readability at body sizes.\n- **Four-weight system**: 400 (body/reading), 500 (UI/interactive), 600 (emphasis/navigation), 700 (headings/display). The broader weight range compared to most systems allows nuanced hierarchy.\n- **Warm scaling**: Line height tightens as size increases -- 1.50 at body (16px), 1.23-1.27 at sub-headings, 1.00-1.04 at display. This creates denser, more impactful headlines.\n- **Badge micro-tracking**: The 12px badge text uses positive letter-spacing (0.125px) -- the only positive tracking in the system, creating wider, more legible small text.\n\n## 4. Component Stylings\n\n### Buttons\n\n**Primary Blue**\n- Background: `#0075de` (Notion Blue)\n- Text: `#ffffff`\n- Padding: 8px 16px\n- Radius: 4px (subtle)\n- Border: `1px solid transparent`\n- Hover: background darkens to `#005bab`","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":102,"e":108,"m":1776084302914.8396,"t":"- Active: scale(0.9) transform\n- Focus: `2px solid` focus outline, `var(--shadow-level-200)` shadow\n- Use: Primary CTA (\"Get Notion free\", \"Try it\")\n\n**Secondary / Tertiary**\n- Background: `rgba(0,0,0,0.05)` (translucent warm gray)\n- Text: `#000000` (near-black)","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":145,"e":204,"m":1776084302914.8396,"t":"### Navigation\n- Clean horizontal nav on white, not sticky\n- Brand logo left-aligned (33x34px icon + wordmark)\n- Links: NotionInter 15px weight 500-600, near-black text\n- Hover: color shift to `var(--color-link-primary-text-hover)`\n- CTA: blue pill button (\"Get Notion free\") right-aligned\n- Mobile: hamburger menu collapse\n- Product dropdowns with multi-level categorized menus\n\n### Image Treatment\n- Product screenshots with `1px solid rgba(0,0,0,0.1)` border\n- Top-rounded images: `12px 12px 0px 0px` radius\n- Dashboard/workspace preview screenshots dominate feature sections\n- Warm gradient backgrounds behind hero illustrations (decorative character illustrations)\n\n### Distinctive Components\n\n**Feature Cards with Illustrations**\n- Large illustrative headers (The Great Wave, product UI screenshots)\n- 12px radius card with whisper border\n- Title at 22px weight 700, description at 16px weight 400\n- Warm white (`#f6f5f4`) background variant for alternating sections\n\n**Trust Bar / Logo Grid**\n- Company logos (trusted teams section) in their brand colors\n- Horizontal scroll or grid layout with team counts\n- Metric display: large number + description pattern\n\n**Metric Cards**\n- Large number display (e.g., \"$4,200 ROI\")\n- NotionInter 40px+ weight 700 for the metric\n- Description below in warm gray body text\n- Whisper-bordered card container\n\n## 5. Layout Principles\n\n### Spacing System\n- Base unit: 8px\n- Scale: 2px, 3px, 4px, 5px, 6px, 7px, 8px, 11px, 12px, 14px, 16px, 24px, 32px\n- Non-rigid organic scale with fractional values (5.6px, 6.4px) for micro-adjustments\n\n### Grid & Container\n- Max content width: approximately 1200px\n- Hero: centered single-column with generous top padding (80-120px)\n- Feature sections: 2-3 column grids for cards\n- Full-width warm white (`#f6f5f4`) section backgrounds for alternation\n- Code/dashboard screenshots as contained with whisper border\n\n### Whitespace Philosophy\n- **Generous vertical rhythm**: 64-120px between major sections. Notion lets content breathe with vast vertical padding.\n- **Warm alternation**: White sections alternate with warm white (`#f6f5f4`) sections, creating gentle visual rhythm without harsh color breaks.\n- **Content-first density**: Body text blocks are compact (line-height 1.50) but surrounded by ample margin, creating islands of readable content in a sea of white space.\n\n### Border Radius Scale\n- Micro (4px): Buttons, inputs, functional interactive elements\n- Subtle (5px): Links, list items, menu items\n- Standard (8px): Small cards, containers, inline elements\n- Comfortable (12px): Standard cards, feature containers, image tops\n- Large (16px): Hero cards, featured content, promotional blocks","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":193,"e":252,"m":1776084302914.8396,"t":"### Whitespace Philosophy\n- **Generous vertical rhythm**: 64-120px between major sections. Notion lets content breathe with vast vertical padding.\n- **Warm alternation**: White sections alternate with warm white (`#f6f5f4`) sections, creating gentle visual rhythm without harsh color breaks.\n- **Content-first density**: Body text blocks are compact (line-height 1.50) but surrounded by ample margin, creating islands of readable content in a sea of white space.\n\n### Border Radius Scale\n- Micro (4px): Buttons, inputs, functional interactive elements\n- Subtle (5px): Links, list items, menu items\n- Standard (8px): Small cards, containers, inline elements\n- Comfortable (12px): Standard cards, feature containers, image tops\n- Large (16px): Hero cards, featured content, promotional blocks\n- Full Pill (9999px): Badges, pills, status indicators\n- Circle (100%): Tab indicators, avatars\n\n## 6. Depth & Elevation\n\n| Level | Treatment | Use |\n|-------|-----------|-----|\n| Flat (Level 0) | No shadow, no border | Page background, text blocks |\n| Whisper (Level 1) | `1px solid rgba(0,0,0,0.1)` | Standard borders, card outlines, dividers |\n| Soft Card (Level 2) | 4-layer shadow stack (max opacity 0.04) | Content cards, feature blocks |\n| Deep Card (Level 3) | 5-layer shadow stack (max opacity 0.05, 52px blur) | Modals, featured panels, hero elements |\n| Focus (Accessibility) | `2px solid var(--focus-color)` outline | Keyboard focus on all interactive elements |\n\n**Shadow Philosophy**: Notion's shadow system uses multiple layers with extremely low individual opacity (0.01 to 0.05) that accumulate into soft, natural-looking elevation. The 4-layer card shadow spans from 1.04px to 18px blur, creating a gradient of depth rather than a single hard shadow. The 5-layer deep shadow extends to 52px blur at 0.05 opacity, producing ambient occlusion that feels like natural light rather than computer-generated depth. This layered approach makes elements feel embedded in the page rather than floating above it.\n\n### Decorative Depth\n- Hero section: decorative character illustrations (playful, hand-drawn style)\n- Section alternation: white to warm white (`#f6f5f4`) background shifts\n- No hard section borders -- separation comes from background color changes and spacing\n\n## 7. Responsive Behavior\n\n### Breakpoints\n| Name | Width | Key Changes |\n|------|-------|-------------|\n| Mobile Small | <400px | Tight single column, minimal padding |\n| Mobile | 400-600px | Standard mobile, stacked layout |\n| Tablet Small | 600-768px | 2-column grids begin |\n| Tablet | 768-1080px | Full card grids, expanded padding |\n| Desktop Small | 1080-1200px | Standard desktop layout |\n| Desktop | 1200-1440px | Full layout, maximum content width |\n| Large Desktop | >1440px | Centered, generous margins |\n\n### Touch Targets\n- Buttons use comfortable padding (8px-16px vertical)\n- Navigation links at 15px with adequate spacing\n- Pill badges have 8px horizontal padding for tap targets\n- Mobile menu toggle uses standard hamburger button\n\n### Collapsing Strategy\n- Hero: 64px display -> scales to 40px -> 26px on mobile, maintains proportional letter-spacing\n- Navigation: horizontal links + blue CTA -> hamburger menu\n- Feature cards: 3-column -> 2-column -> single column stacked\n- Product screenshots: maintain aspect ratio with responsive images\n- Trust bar logos: grid -> horizontal scroll on mobile\n- Footer: multi-column -> stacked single column\n- Section spacing: 80px+ -> 48px on mobile","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":241,"e":300,"m":1776084302914.8396,"t":"- Pill badges have 8px horizontal padding for tap targets\n- Mobile menu toggle uses standard hamburger button\n\n### Collapsing Strategy\n- Hero: 64px display -> scales to 40px -> 26px on mobile, maintains proportional letter-spacing\n- Navigation: horizontal links + blue CTA -> hamburger menu\n- Feature cards: 3-column -> 2-column -> single column stacked\n- Product screenshots: maintain aspect ratio with responsive images\n- Trust bar logos: grid -> horizontal scroll on mobile\n- Footer: multi-column -> stacked single column\n- Section spacing: 80px+ -> 48px on mobile\n\n### Image Behavior\n- Workspace screenshots maintain whisper border at all sizes\n- Hero illustrations scale proportionally\n- Product screenshots use responsive images with consistent border radius\n- Full-width warm white sections maintain edge-to-edge treatment\n\n## 8. Accessibility & States\n\n### Focus System\n- All interactive elements receive visible focus indicators\n- Focus outline: `2px solid` with focus color + shadow level 200\n- Tab navigation supported throughout all interactive components\n- High contrast text: near-black on white exceeds WCAG AAA (>14:1 ratio)\n\n### Interactive States\n- **Default**: Standard appearance with whisper borders\n- **Hover**: Color shift on text, scale(1.05) on buttons, underline on links\n- **Active/Pressed**: scale(0.9) transform, darker background variant\n- **Focus**: Blue outline ring with shadow reinforcement\n- **Disabled**: Warm gray (`#a39e98`) text, reduced opacity\n\n### Color Contrast\n- Primary text (rgba(0,0,0,0.95)) on white: ~18:1 ratio\n- Secondary text (#615d59) on white: ~5.5:1 ratio (WCAG AA)\n- Blue CTA (#0075de) on white: ~4.6:1 ratio (WCAG AA for large text)\n- Badge text (#097fe8) on badge bg (#f2f9ff): ~4.5:1 ratio (WCAG AA for large text)\n\n## 9. Agent Prompt Guide\n\n### Quick Color Reference\n- Primary CTA: Notion Blue (`#0075de`)\n- Background: Pure White (`#ffffff`)\n- Alt Background: Warm White (`#f6f5f4`)\n- Heading text: Near-Black (`rgba(0,0,0,0.95)`)\n- Body text: Near-Black (`rgba(0,0,0,0.95)`)\n- Secondary text: Warm Gray 500 (`#615d59`)\n- Muted text: Warm Gray 300 (`#a39e98`)\n- Border: `1px solid rgba(0,0,0,0.1)`\n- Link: Notion Blue (`#0075de`)\n- Focus ring: Focus Blue (`#097fe8`)\n\n### Example Component Prompts\n- \"Create a hero section on white background. Headline at 64px NotionInter weight 700, line-height 1.00, letter-spacing -2.125px, color rgba(0,0,0,0.95). Subtitle at 20px weight 600, line-height 1.40, color #615d59. Blue CTA button (#0075de, 4px radius, 8px 16px padding, white text) and ghost button (transparent bg, near-black text, underline on hover).\"\n- \"Design a card: white background, 1px solid rgba(0,0,0,0.1) border, 12px radius. Use shadow stack: rgba(0,0,0,0.04) 0px 4px 18px, rgba(0,0,0,0.027) 0px 2.025px 7.85px, rgba(0,0,0,0.02) 0px 0.8px 2.93px, rgba(0,0,0,0.01) 0px 0.175px 1.04px. Title at 22px NotionInter weight 700, letter-spacing -0.25px. Body at 16px weight 400, color #615d59.\"\n- \"Build a pill badge: #f2f9ff background, #097fe8 text, 9999px radius, 4px 8px padding, 12px NotionInter weight 600, letter-spacing 0.125px.\"\n- \"Create navigation: white header. NotionInter 15px weight 600 for links, near-black text. Blue pill CTA 'Get Notion free' right-aligned (#0075de bg, white text, 4px radius).\"\n- \"Design an alternating section layout: white sections alternate with warm white (#f6f5f4) sections. Each section has 64-80px vertical padding, max-width 1200px centered. Section heading at 48px weight 700, line-height 1.00, letter-spacing -1.5px.\"","v":"[encoded data, 4096 characters]"}
{"p":"DESIGN.md","s":289,"e":310,"m":1776084302914.8396,"t":"- Muted text: Warm Gray 300 (`#a39e98`)\n- Border: `1px solid rgba(0,0,0,0.1)`\n- Link: Notion Blue (`#0075de`)\n- Focus ring: Focus Blue (`#097fe8`)\n\n### Example Component Prompts\n- \"Create a hero section on white background. Headline at 64px NotionInter weight 700, line-height 1.00, letter-spacing -2.125px, color rgba(0,0,0,0.95). Subtitle at 20px weight 600, line-height 1.40, color #615d59. Blue CTA button (#0075de, 4px radius, 8px 16px padding, white text) and ghost button (transparent bg, near-black text, underline on hover).\"\n- \"Design a card: white background, 1px solid rgba(0,0,0,0.1) border, 12px radius. Use shadow stack: rgba(0,0,0,0.04) 0px 4px 18px, rgba(0,0,0,0.027) 0px 2.025px 7.85px, rgba(0,0,0,0.02) 0px 0.8px 2.93px, rgba(0,0,0,0.01) 0px 0.175px 1.04px. Title at 22px NotionInter weight 700, letter-spacing -0.25px. Body at 16px weight 400, color #615d59.\"\n- \"Build a pill badge: #f2f9ff background, #097fe8 text, 9999px radius, 4px 8px padding, 12px NotionInter weight 600, letter-spacing 0.125px.\"\n- \"Create navigation: white header. NotionInter 15px weight 600 for links, near-black text. Blue pill CTA 'Get Notion free' right-aligned (#0075de bg, white text, 4px radius).\"\n- \"Design an alternating section layout: white sections alternate with warm white (#f6f5f4) sections. Each section has 64-80px vertical padding, max-width 1200px centered. Section heading at 48px weight 700, line-height 1.00, letter-spacing -1.5px.\"\n\n### Iteration Guide\n1. Always use warm neutrals -- Notion's grays have yellow-brown undertones (#f6f5f4, #31302e, #615d59, #a39e98), never blue-gray\n2. Letter-spacing scales with font size: -2.125px at 64px, -1.875px at 54px, -0.625px at 26px, normal at 16px\n3. Four weights: 400 (read), 500 (interact), 600 (emphasize), 700 (announce)\n4. Borders are whispers: 1px solid rgba(0,0,0,0.1) -- never heavier\n5. Shadows use 4-5 layers with individual opacity never exceeding 0.05\n6. The warm white (#f6f5f4) section background is essential for visual rhythm\n7. Pill badges (9999px) for status/tags, 4px radius for buttons and inputs\n8. Notion Blue (#0075de) is the only saturated color in core UI -- use it sparingly for CTAs and links","v":"[encoded data, 4096 characters]"}
{"p":"harness-init.sh","s":1,"e":28,"m":1778951184599.713,"t":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Harness 会在每次会话启动时执行此脚本，这里只做轻量且可重复执行的环境检查。\nROOT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$ROOT_DIR\"\n\ncommand -v git >/dev/null || {\n  echo \"ERROR: 缺少 git\"\n  exit 1\n}\n\ncommand -v python3 >/dev/null || {\n  echo \"ERROR: 缺少 python3\"\n  exit 1\n}\n\nif [ -d \"$ROOT_DIR/wolai-frontend\" ]; then\n  if ! command -v node >/dev/null; then\n    echo \"WARN: 缺少 node，前端相关任务可能无法执行\"\n  fi\n  if ! command -v pnpm >/dev/null; then\n    echo \"WARN: 缺少 pnpm，前端相关任务可能无法执行\"\n  fi\nfi\n\necho \"Harness 环境检查完成\"","v":"[encoded data, 4096 characters]"}
{"p":"harness-progress.txt","s":24,"e":52,"m":1778951184599.713,"t":"[2026-04-26T23:38:55Z] [SESSION-1] Completed [task-005] (commit skipped by repo rule; validated by mnote-web tree_shell_embeds_renderer_input_contract, tree-shell iframe/surface vitest, task112 and task113 smoke)\n[2026-04-26T23:38:55Z] [SESSION-1] Starting [task-006] Phase L：将 tree.subtree.move 最终写路径继续下沉 Rust kernel (base=6d89f5ca)\n[2026-04-26T23:38:55Z] [SESSION-1] STATS tasks_total=9 completed=5 failed=0 pending=3 in_progress=1 blocked=0 attempts_total=6 checkpoints=5\n[2026-04-26T23:52:20Z] [SESSION-2] LOCK acquired (pid=3897587)\n[2026-04-26T23:52:20Z] [SESSION-2] RECOVERY [task-006] action=\"validate existing uncommitted changes\" reason=\"previous session left task-006 in_progress after implementation and validation\"\n[2026-04-26T23:52:52Z] [SESSION-2] CHECKPOINT [task-006] step=1/1 \"已引入 Rust treeWriteOperation，Convex 优先执行 Rust write operation；目标验证通过。\"\n[2026-04-26T23:52:52Z] [SESSION-2] Completed [task-006] (commit skipped by repo rule; validated by bridge-runtime tree_subtree_move_command and 39 frontend target tests)\n[2026-04-26T23:52:52Z] [SESSION-2] Starting [task-007] Phase M：补齐 block/save-snapshot formal artifact 与 domain-event contract (base=6d89f5ca)\n[2026-04-26T23:52:52Z] [SESSION-2] STATS tasks_total=9 completed=6 failed=0 pending=2 in_progress=1 blocked=0 attempts_total=7 checkpoints=6\n[2026-04-27T00:09:12Z] [SESSION-2] CHECKPOINT [task-007] step=1/1 \"formal domainEventPlan payload schema 与 resync_required delta 已落地；目标验证通过。\"\n[2026-04-27T00:09:12Z] [SESSION-2] Completed [task-007] (commit skipped by repo rule; validated by bridge-runtime formal contract tests and frontend adapter/runtime tests)\n[2026-04-27T00:09:12Z] [SESSION-2] Starting [task-008] Phase N：继续收薄 3000 Next route 过渡职责 (base=6d89f5ca)\n[2026-04-27T00:09:12Z] [SESSION-2] STATS tasks_total=9 completed=7 failed=0 pending=1 in_progress=1 blocked=0 attempts_total=8 checkpoints=7\n[2026-04-27T00:18:38Z] [SESSION-3] LOCK acquired (pid=codex-session-3)\n[2026-04-27T00:18:38Z] [SESSION-3] RECOVERY [task-008] action=\"resume diagnostics with uncommitted changes\" reason=\"previous session implemented Phase N but task112 picker move wait timed out\"\n[2026-04-27T00:28:56Z] [SESSION-3] CHECKPOINT [task-008] step=1/2 \"已定位 task112 超时根因：move 请求实际发出但 Convex 运行态 documents.move validator 未加载 treeWriteOperation，执行 npx convex dev --once 刷新本地 functions。\"\n[2026-04-27T00:34:43Z] [SESSION-3] CHECKPOINT [task-008] step=2/2 \"目标 Vitest 与 task112 smoke 通过；补充 iframe ready 等待，运行态 Convex functions 已刷新。\"\n[2026-04-27T00:34:43Z] [SESSION-3] Completed [task-008] (commit skipped by repo rule; validated by tree route/delta/boundary vitest and task112 smoke)\n[2026-04-27T00:34:43Z] [SESSION-3] Starting [task-009] Phase O：搜索语义 hardening 与资源类型 fixture 补齐 (base=6d89f5ca)\n[2026-04-27T00:41:58Z] [SESSION-3] CHECKPOINT [task-009] step=1/1 \"file_tree 搜索 hardening 前端契约与资源 fixture 已完成；目标验证通过。\"\n[2026-04-27T00:41:58Z] [SESSION-3] Completed [task-009] (commit skipped by repo rule; validated by tree-delta, projection-client, kernel-file-tree tests and task112 smoke)\n[2026-04-27T00:41:58Z] [SESSION-3] STATS tasks_total=9 completed=9 failed=0 pending=0 in_progress=0 blocked=0 attempts_total=9 checkpoints=9\n[2026-04-27T00:41:58Z] [SESSION-3] LOCK released\n[2026-04-28T00:00:00Z] [SESSION-4] CORRECTION final-renderer-gate=\"task-001..task-009 were Rust/WASM reducer runtime completion, not final DOM shell completion\"\n[2026-04-28T00:00:00Z] [SESSION-4] INIT Added task-010..task-013 from design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\n[2026-04-28T00:00:00Z] [SESSION-4] NEXT task-010 must add negative gates so default page/filetree/picker success path cannot remain iframe_srcdoc inline DOM shell\n[2026-04-28T00:00:00Z] [SESSION-4] STATS tasks_total=13 completed=9 failed=0 pending=4 in_progress=0 blocked=0 attempts_total=9 checkpoints=9\n[2026-04-28T02:44:47Z] [SESSION-5] LOCK acquired (pid=manual-codex)\n[2026-04-28T02:44:47Z] [SESSION-5] INIT Harness environment check: PASS","v":"[encoded data, 4096 characters]"}
{"p":"harness-progress.txt","s":53,"e":60,"m":1778951184599.713,"t":"[2026-04-28T02:44:47Z] [SESSION-5] Starting [task-010] Phase P-0：补 final DOM shell 负向门禁，阻止 iframe_srcdoc 被误判为完成 (base=81ba21c5)\n[2026-04-28T02:52:03Z] [SESSION-6] LOCK acquired (pid=manual-codex-session-6)\n[2026-04-28T02:52:03Z] [SESSION-6] RECOVERY [task-010] action=\"resume in-progress final DOM shell gate\" reason=\"previous session lock stale; preserve existing uncommitted changes\"\n[2026-04-28T03:28:16.005752Z] [SESSION-6] CHECKPOINT [task-010] step=1/4 \"已新增默认非 iframe rust_wasm_dom_shell_host 与共享 DOM projection model，legacy iframe 改为显式环境开关。\"\n[2026-04-28T03:34:10.533972Z] [SESSION-6] CHECKPOINT [task-010] step=2/4 \"tree-shell-surface 与 legacy iframe host 目标 Vitest 通过；默认主路径断言已切成 rust_wasm_dom_shell_host 且负向排除 iframe_srcdoc。\"\n[2026-04-28T03:36:24.605315Z] [SESSION-6] CHECKPOINT [task-010] step=3/4 \"move/embed picker 测试已切到 DOM shell；默认 picker 不再以 iframe postMessage 为成功条件，目标前端 Vitest 29 项通过。\"\n[2026-04-28T04:40:14Z] [SESSION-7] RECOVERY [task-010] action=\"resume stale leased final DOM shell cutover\" reason=\"previous session left task-010 in_progress; existing changes validated without rollback\"\n[2026-04-28T04:40:14Z] [SESSION-7] CHECKPOINT [task-010] step=4/4 \"已补默认 DOM shell 负向门禁：tree-shell-host/surface/picker 测试与 smoke 默认拒绝 iframe_srcdoc 成功路径。\"","v":"[encoded data, 4096 characters]"}
{"p":"harness-progress.txt","s":72,"e":96,"m":1778951184599.713,"t":"[2026-04-28T06:29:15Z] [SESSION-7] INIT Added task-014..task-018 for remaining 4-16 final runtime closure: normalizedMove fallback removal, snapshot event/Page Aggregate, embed pageReference migration, and file_tree indexing visibility.\n[2026-04-28T06:29:15Z] [SESSION-7] Starting [task-014] Phase L2：删除 normalizedMove fallback，强制 move 主写路径使用 treeWriteOperation (base=81ba21c5)\n[2026-04-28T06:37:40Z] [SESSION-7] CHECKPOINT [task-014] step=1/1 \"已删除 normalizedMove fallback；bridge-runtime/TS transport/Convex move 写路径只保留 treeWriteOperation，目标测试通过。\"\n[2026-04-28T06:37:40Z] [SESSION-7] Completed [task-014] (commit skipped by repo rule; validated by bridge-runtime tree_subtree_move_command and 20 frontend target tests)\n[2026-04-28T06:37:40Z] [SESSION-7] Starting [task-015] Phase M2：定稿 document.snapshot.saved 独立事件并避免与 page.body.saved payload 重叠 (base=81ba21c5)\n[2026-04-28T07:44:37Z] [SESSION-8] CHECKPOINT [task-015] step=1/1 \"page.body.saved 与 document.snapshot.saved 已拆成 domainEventPlans 双事件；artifact writer 支持 domainEvents 批量落库，page.body.saved payload 不再夹带 snapshot。\"\n[2026-04-28T07:44:37Z] [SESSION-8] Completed [task-015] (commit skipped by repo rule; validated by bridge-runtime --lib and rust-runtime/bridge-log/page-write tests)\n[2026-04-28T07:44:37Z] [SESSION-8] Starting [task-016] Phase M3：把 snapshot 独立事件接入 Page Aggregate 深层收口文档与客户端边界 (base=81ba21c5)\n[2026-04-28T07:44:37Z] [SESSION-8] CHECKPOINT [task-016] step=1/1 \"Page Aggregate contract 与 5-6 清单已固定 page.body.saved / document.snapshot.saved 职责边界；4-16 snapshot 项已勾选。\"\n[2026-04-28T07:44:37Z] [SESSION-8] Completed [task-016] (commit skipped by repo rule; validated by page-aggregate-client-state/page-aggregate-builder/page-write tests and git diff --check)\n[2026-04-28T07:44:37Z] [SESSION-8] Starting [task-017] Phase N2：将 tree.node.embed 的 pageReference 组装迁到 Page Aggregate / Rust artifact 边界 (base=81ba21c5)\n[2026-04-28T07:44:37Z] [SESSION-8] CHECKPOINT [task-017] step=1/1 \"Rust pageAggregateEmbedPlan 已产出 pageReference block 与 next content；3000 route/documents embed adapter 只传 substrate preflight，tree-route-boundary compatPending 已清空。\"\n[2026-04-28T07:44:37Z] [SESSION-8] Completed [task-017] (commit skipped by repo rule; validated by bridge-runtime --lib, route tests, and tree-route-boundary tests)\n[2026-04-28T07:44:37Z] [SESSION-8] Starting [task-018] Phase O2：补 file_tree 搜索索引可见性指标边界并收口 4-16 (base=81ba21c5)\n[2026-04-28T07:44:37Z] [SESSION-8] CHECKPOINT [task-018] step=1/1 \"file_tree 搜索 indexingVisibility 已提升为 projection result meta；mnote-web 与 3000 同源 projection route 均透传该指标，4-16 无未勾选项。\"\n[2026-04-28T07:44:37Z] [SESSION-8] Completed [task-018] (commit skipped by repo rule; validated by bridge-runtime --lib, mnote-web file_tree_projection, file projection route/client tests)\n[2026-04-28T07:44:37Z] [SESSION-8] STATS tasks_total=18 completed=18 failed=0 pending=0 in_progress=0 blocked=0 attempts_total=18 checkpoints=20\n[2026-04-28T08:53:21Z] [SESSION-9] INIT Added task-019..task-026 from design/03-rust-web/process/3-6-rust-web-main-execution-plane-checklist-v1.md for Rust Web main execution plane cutover.\n[2026-04-28T08:53:21Z] [SESSION-9] NEXT task-019 must freeze main execution owner manifest before gateway implementation.\n[2026-04-28T08:53:21Z] [SESSION-9] STATS tasks_total=26 completed=18 failed=0 pending=8 in_progress=0 blocked=0 attempts_total=18 checkpoints=21\n[2026-04-28T09:06:04Z] [SESSION-9] LOCK acquired (pid=codex-main-session)\n[2026-04-28T09:06:04Z] [SESSION-9] Starting [task-019] Phase W-0：冻结 Rust Web 主执行面 owner manifest 与负向门禁 (base=9f6a2c86)\n[2026-04-28T09:07:58Z] [SESSION-9] CHECKPOINT [task-019] step=1/1 \"已新增 Rust Web 主执行面 owner manifest 与负向门禁；目标 Vitest 与 diff check 通过。\"\n[2026-04-28T09:07:58Z] [SESSION-9] Completed [task-019] (commit skipped by repo rule; validated by rust-web-main-execution-boundary/runtime-config vitest and git diff --check)\n[2026-04-28T09:07:58Z] [SESSION-9] Starting [task-020] Phase W-1：让 mnote-web 默认成为 3000 gateway owner (base=9f6a2c86)","v":"[encoded data, 4096 characters]"}
{"p":"harness-progress.txt","s":97,"e":120,"m":1778951184599.713,"t":"[2026-04-28T09:13:44Z] [SESSION-9] CHECKPOINT [task-020] step=1/1 \"mnote-web gateway owner、desktop-hot 默认启动计划与 task114 smoke 已落地；目标验证通过。\"\n[2026-04-28T09:13:44Z] [SESSION-9] Completed [task-020] (commit skipped by repo rule; validated by desktop-hot node tests, mnote-web gateway cargo tests, and task114 smoke)\n[2026-04-28T09:13:44Z] [SESSION-9] Starting [task-021] Phase W-2：把 runtime config 与 auth/session handoff 迁到 Rust Web (base=9f6a2c86)\n[2026-04-28T09:16:15Z] [SESSION-9] WARN validation.commands=\"修正 task-021..task-025 Cargo 多过滤词写法；原命令会被 cargo test 拒绝为 unexpected argument。\"\n[2026-04-28T09:16:28Z] [SESSION-9] CHECKPOINT [task-021] step=1/1 \"mnote-web runtime config/session routes 与 Next token compat/debug route 已落地；修正后目标验证通过。\"\n[2026-04-28T09:16:28Z] [SESSION-9] Completed [task-021] (commit skipped by repo rule; validated by mnote-web session/runtime_config tests and runtime-config/token vitest)\n[2026-04-28T09:16:28Z] [SESSION-9] Starting [task-022] Phase W-3：建立 Rust-owned 文档页 / Page Aggregate server-first shell (base=9f6a2c86)\n[2026-04-28T09:20:22Z] [SESSION-9] CHECKPOINT [task-022] step=1/1 \"Rust-owned document shell 与 Page Aggregate snapshot contract 已落地；目标验证通过。\"\n[2026-04-28T09:20:22Z] [SESSION-9] Completed [task-022] (commit skipped by repo rule; validated by document_shell/page_aggregate tests, document-content/page-aggregate-builder vitest, and task115 smoke)\n[2026-04-28T09:20:22Z] [SESSION-9] Starting [task-023] Phase W-4：把主 API transport 从 Next route 收薄为 Rust Web owned (base=9f6a2c86)\n[2026-04-28T09:26:14Z] [SESSION-9] CHECKPOINT [task-023] step=1/1 \"Rust Web documents/tree/search transport owner 口径已落地；目标验证通过，page_aggregate 过滤词命中 0 项已记录为配置缺口。\"\n[2026-04-28T09:26:14Z] [SESSION-9] Completed [task-023] (commit skipped by repo rule; validated by mnote-web documents_api/tree_commands/search tests, bridge-runtime tree_subtree_move_command tests, and frontend route/search tests)\n[2026-04-28T09:26:14Z] [SESSION-9] Starting [task-024] Phase W-5：把 tree realtime stream 固定为 Rust Web 主链 (base=9f6a2c86)\n[2026-04-28T12:33:41Z] [SESSION-10] CHECKPOINT [task-024] step=1/1 \"Rust Web /api/tree/events owner、snapshot/delta stream、Next /api/mnote-web/stream compat alias 与 consumer URL cutover 已通过验证；task112 legacy renderer crash 仅在 Target crashed 时新 context 恢复。\"\n[2026-04-28T12:33:41Z] [SESSION-10] Completed [task-024] (commit skipped by repo rule; validated by mnote-web tree_realtime/stream tests, frontend stream/documents route tests, and task112 smoke)\n[2026-04-28T12:33:41Z] [SESSION-10] NEXT task-025 Phase W-6：把 Search / AI / Mindmap 页面壳迁到 Rust Web shell + islands\n[2026-04-28T12:34:53Z] [SESSION-10] Starting [task-025] Phase W-6：把 Search / AI / Mindmap 页面壳迁到 Rust Web shell + islands (base=9f6a2c86)\n[2026-04-28T12:34:53Z] [SESSION-10] CHECKPOINT [task-025] step=0/4 \"初始 Rust 目标过滤词 search_shell/ai_bridge/mindmap_shell 命中 0 tests；先补 owner/contract 门禁。\"\n[2026-04-28T12:44:04Z] [SESSION-10] CHECKPOINT [task-025] step=1/1 \"Search / AI / Mindmap shell owner 已迁到 Rust Web；Hermes contract 固定 session/tool/client action owner；React runtime 标记为 island 或 legacy compat。\"\n[2026-04-28T12:44:04Z] [SESSION-10] Completed [task-025] (commit skipped by repo rule; validated by mnote-web search_shell/ai_bridge/mindmap_shell tests, frontend AI/search target tests, task116 smoke, and git diff --check)\n[2026-04-28T12:44:04Z] [SESSION-10] NEXT task-026 Phase W-7：退役 Next App Router 主入口并完成 03-rust-web 文档收口\n[2026-04-28T12:50:07Z] [SESSION-10] Starting [task-026] Phase W-7：退役 Next App Router 主入口并完成 03-rust-web 文档收口 (base=9f6a2c86)\n[2026-04-28T12:50:07Z] [SESSION-10] CHECKPOINT [task-026] step=1/1 \"task117 证明 SKIP_NEXT_LEGACY=1 核心 shell 可脱离 Next legacy；desktop-hot 跳过 Next legacy 分支、ARCHITECTURE/3-1 口径与 3-5/3-6 done 迁移已完成。\"\n[2026-04-28T12:50:07Z] [SESSION-10] Completed [task-026] (commit skipped by repo rule; validated by task097/task114/task115/task116/task117 and git diff --check)","v":"[encoded data, 4096 characters]"}
{"p":"harness-progress.txt","s":121,"e":122,"m":1778951184599.713,"t":"[2026-04-28T12:50:07Z] [SESSION-10] STATS tasks_total=26 completed=26 failed=0 pending=0 in_progress=0 blocked=0 attempts_total=26 checkpoints=29","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":1,"e":60,"m":1778951184600.0012,"t":"{\n  \"version\": 2,\n  \"created\": \"2026-04-26T23:16:13Z\",\n  \"session_config\": {\n    \"concurrency_mode\": \"exclusive\",\n    \"max_tasks_per_session\": 9,\n    \"max_sessions\": 80\n  },\n  \"tasks\": [\n    {\n      \"id\": \"task-001\",\n      \"title\": \"Phase K-1：定义 tree shell Rust runtime artifact 边界\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web renderer_input && cargo test -p mnote-web runtime_artifact && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx\",\n        \"timeout_seconds\": 1800\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已在 mnote-web rendererInput 中定义 rust_tree_shell_runtime_artifact_v1，覆盖 rendererInput/projection/expanded/selected/active/focused 输入、DOM patch/intent/command 输出和 focus/keyboard/expand/selection/context/drag/pick 事件边界；3000 inline host 同步输出同名 runtimeArtifact。\",\n          \"timestamp\": \"2026-04-26T23:22:39Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-26T23:22:39Z\"\n    },\n    {\n      \"id\": \"task-002\",\n      \"title\": \"Phase K-2：建立 page tree Rust-side runtime 状态机\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-001\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web page_renderer && cargo test -p mnote-web page_focus && cargo test -p mnote-web page_runtime && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx && node /mnt/Data1T/mnote/scripts/task112-tree-rust-family-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 page_runtime reducer，组合 focus/expand/open/context-menu/drop feedback/tree.subtree.move dispatch 输出，验证 page_renderer/page_focus/page_runtime、tree-shell iframe/surface vitest 与 task112 smoke 均通过。\",","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":49,"e":108,"m":1778951184600.0012,"t":"\"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web page_renderer && cargo test -p mnote-web page_focus && cargo test -p mnote-web page_runtime && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx && node /mnt/Data1T/mnote/scripts/task112-tree-rust-family-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 page_runtime reducer，组合 focus/expand/open/context-menu/drop feedback/tree.subtree.move dispatch 输出，验证 page_renderer/page_focus/page_runtime、tree-shell iframe/surface vitest 与 task112 smoke 均通过。\",\n          \"timestamp\": \"2026-04-26T23:27:43Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-26T23:27:43Z\"\n    },\n    {\n      \"id\": \"task-003\",\n      \"title\": \"Phase K-3：建立 file tree Rust-side runtime 状态机\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-001\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web filetree_renderer && cargo test -p mnote-web filetree_selection && cargo test -p mnote-web filetree_runtime && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/lib/file-tree/shell.test.ts src/lib/tree-stream/tree-delta.test.ts && node /mnt/Data1T/mnote/scripts/task112-tree-rust-family-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 filetree_runtime reducer，覆盖 selection/range/context/normalize、drag row ids、copy/move effect、drop target feedback，以及 doc/index/asset-folder/asset 的 open/context-menu intent；目标测试与 task112 smoke 通过。\",\n          \"timestamp\": \"2026-04-26T23:32:11Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-26T23:32:11Z\"\n    },\n    {\n      \"id\": \"task-004\",\n      \"title\": \"Phase K-4：建立 picker Rust-side runtime 状态机\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-001\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web picker_renderer && cargo test -p mnote-web picker_state && cargo test -p mnote-web picker_runtime && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/documents/move-embed-picker-dialog.test.tsx src/components/sidebar/tree-shell-iframe-host.test.tsx && node /mnt/Data1T/mnote/scripts/task113-picker-keyboard-regression-smoke.js\",\n        \"timeout_seconds\": 2400","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":97,"e":156,"m":1778951184600.0012,"t":"\"title\": \"Phase K-4：建立 picker Rust-side runtime 状态机\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-001\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web picker_renderer && cargo test -p mnote-web picker_state && cargo test -p mnote-web picker_runtime && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/documents/move-embed-picker-dialog.test.tsx src/components/sidebar/tree-shell-iframe-host.test.tsx && node /mnt/Data1T/mnote/scripts/task113-picker-keyboard-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 picker_runtime reducer，覆盖 normalize/next/previous/home/end/focus/pick、excluded ids、root pick，以及搜索输入不被键盘运行时抢 DOM 焦点的 focus_dom 输出边界；目标测试与 task113 smoke 通过。\",\n          \"timestamp\": \"2026-04-26T23:34:59Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-26T23:34:59Z\"\n    },\n    {\n      \"id\": \"task-005\",\n      \"title\": \"Phase K-5：3000 host 调用正式 runtime artifact，并将 mnote-web /tree 固定为 debug/internal\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-002\",\n        \"task-003\",\n        \"task-004\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx && node /mnt/Data1T/mnote/scripts/task112-tree-rust-family-regression-smoke.js && node /mnt/Data1T/mnote/scripts/task113-picker-keyboard-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已固定 3000 inline host 与 mnote-web debug shell 均输出 rust_tree_shell_runtime_artifact_v1，并通过 tree-shell 目标测试、task112 与 task113 证明 3000 主路径继续使用 inline iframe host，/api/tree/shell debug 直连默认禁用。\",\n          \"timestamp\": \"2026-04-26T23:38:55Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-26T23:38:55Z\"\n    },\n    {\n      \"id\": \"task-006\",","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":145,"e":204,"m":1778951184600.0012,"t":"\"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已固定 3000 inline host 与 mnote-web debug shell 均输出 rust_tree_shell_runtime_artifact_v1，并通过 tree-shell 目标测试、task112 与 task113 证明 3000 主路径继续使用 inline iframe host，/api/tree/shell debug 直连默认禁用。\",\n          \"timestamp\": \"2026-04-26T23:38:55Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-26T23:38:55Z\"\n    },\n    {\n      \"id\": \"task-006\",\n      \"title\": \"Phase L：将 tree.subtree.move 最终写路径继续下沉 Rust kernel\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-005\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime tree_subtree_move_command && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/app/api/tree/commands/route.test.ts src/lib/documents/tree-command-client.test.ts src/lib/tree-stream/tree-delta.test.ts\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已引入 Rust treeWriteOperation，Convex documents.move 优先执行 Rust write operation，normalizedMove 保留旧 fallback；bridge-runtime move 单测与前端 39 个目标测试通过。\",\n          \"timestamp\": \"2026-04-26T23:52:52Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-26T23:52:52Z\"\n    },\n    {\n      \"id\": \"task-007\",\n      \"title\": \"Phase M：补齐 block/save-snapshot formal artifact 与 domain-event contract\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-006\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime documents_save_command_plans_include_formal_domain_event_contract && cargo test -p bridge-runtime block_commands_include_formal_domain_event_contract && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/documents/rust-runtime.test.ts src/lib/documents/page-write-command-adapter.test.ts src/lib/blocks/block-command-adapter.test.ts\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":193,"e":252,"m":1778951184600.0012,"t":"\"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime documents_save_command_plans_include_formal_domain_event_contract && cargo test -p bridge-runtime block_commands_include_formal_domain_event_contract && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/documents/rust-runtime.test.ts src/lib/documents/page-write-command-adapter.test.ts src/lib/blocks/block-command-adapter.test.ts\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已将 page.body.saved、block.patched、block.moved、block.embedded 切到 formal domainEventPlan payload schema；复合命令 streamDelta 使用 resync_required 保守策略，command log/domain event 通过同一 command_id 关联；目标验证通过。\",\n          \"timestamp\": \"2026-04-27T00:09:12Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-27T00:09:12Z\"\n    },\n    {\n      \"id\": \"task-008\",\n      \"title\": \"Phase N：继续收薄 3000 Next route 过渡职责\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-007\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/app/api/tree/commands/route.test.ts src/lib/tree-stream/tree-delta.test.ts && node /mnt/Data1T/mnote/scripts/task112-tree-rust-family-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 2,\n          \"total\": 2,\n          \"description\": \"Phase N route thinning manifest 与 task112 iframe-ready smoke 同步已完成；刷新本地 Convex functions 后 move treeWriteOperation 运行态验证通过，目标 Vitest 与 task112 smoke 均通过。\",\n          \"timestamp\": \"2026-04-27T00:34:43Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-27T00:34:43Z\"\n    },\n    {\n      \"id\": \"task-009\",\n      \"title\": \"Phase O：搜索语义 hardening 与资源类型 fixture 补齐\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-008\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":241,"e":300,"m":1778951184600.0012,"t":"},\n    {\n      \"id\": \"task-009\",\n      \"title\": \"Phase O：搜索语义 hardening 与资源类型 fixture 补齐\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-008\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"6d89f5ca\",\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/tree-stream/tree-delta.test.ts\",\n        \"timeout_seconds\": 1800\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已补 file_tree 搜索 hardening 前端契约：projection-client 固定 maxResults=80、祖先补全/排序/异步可见性元数据，tree-delta fixture 覆盖 index、asset-folder、mindmap child、book、pdf；目标验证与 task112 smoke 通过。\",\n          \"timestamp\": \"2026-04-27T00:41:58Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-27T00:41:58Z\"\n    },\n    {\n      \"id\": \"task-010\",\n      \"title\": \"Phase P-0：补 final DOM shell 负向门禁，阻止 iframe_srcdoc 被误判为完成\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-009\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\",\n      \"scope\": [\n        \"wolai-frontend/src/components/sidebar/tree-shell-host*.test.tsx\",\n        \"wolai-frontend/src/components/sidebar/tree-shell-iframe-host.test.tsx\",\n        \"scripts/task112-tree-rust-family-regression-smoke.js\",\n        \"scripts/task113-picker-keyboard-regression-smoke.js\"\n      ],\n      \"acceptance\": [\n        \"默认 page/filetree/picker 主路径测试不得再把 srcDoc inline DOM shell 作为成功条件\",\n        \"默认主路径测试不得出现 data-tree-browser-bridge=\\\"iframe_srcdoc\\\" 正向断言\",\n        \"旧 TreeShellIframeHost 的 srcdoc 合同测试必须迁入 legacy/debug 分组或显式标注为 legacy\",\n        \"task112/task113 smoke 需要验证新 Rust/WASM DOM shell 主路径，而不是证明旧 inline iframe host 继续可用\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx && cd /mnt/Data1T/mnote && node scripts/task112-tree-rust-family-regression-smoke.js && node scripts/task113-picker-keyboard-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":289,"e":348,"m":1778951184600.0012,"t":"\"acceptance\": [\n        \"默认 page/filetree/picker 主路径测试不得再把 srcDoc inline DOM shell 作为成功条件\",\n        \"默认主路径测试不得出现 data-tree-browser-bridge=\\\"iframe_srcdoc\\\" 正向断言\",\n        \"旧 TreeShellIframeHost 的 srcdoc 合同测试必须迁入 legacy/debug 分组或显式标注为 legacy\",\n        \"task112/task113 smoke 需要验证新 Rust/WASM DOM shell 主路径，而不是证明旧 inline iframe host 继续可用\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx && cd /mnt/Data1T/mnote && node scripts/task112-tree-rust-family-regression-smoke.js && node scripts/task113-picker-keyboard-regression-smoke.js\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 4,\n          \"description\": \"已新增默认非 iframe rust_wasm_dom_shell_host 与共享 DOM projection model，legacy iframe 改为显式环境开关。\",\n          \"timestamp\": \"2026-04-28T03:28:16.005752Z\"\n        },\n        {\n          \"step\": 2,\n          \"total\": 4,\n          \"description\": \"tree-shell-surface 与 legacy iframe host 目标 Vitest 通过；默认主路径断言已切成 rust_wasm_dom_shell_host 且负向排除 iframe_srcdoc。\",\n          \"timestamp\": \"2026-04-28T03:34:10.533972Z\"\n        },\n        {\n          \"step\": 3,\n          \"total\": 4,\n          \"description\": \"move/embed picker 测试已切到 DOM shell；默认 picker 不再以 iframe postMessage 为成功条件，目标前端 Vitest 29 项通过。\",\n          \"timestamp\": \"2026-04-28T03:36:24.605315Z\"\n        },\n        {\n          \"step\": 4,\n          \"total\": 4,\n          \"description\": \"已补默认 DOM shell 负向门禁：tree-shell-host/surface/picker 测试与 smoke 默认拒绝 iframe_srcdoc 成功路径。\",\n          \"timestamp\": \"2026-04-28T04:40:14Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T04:40:14Z\"\n    },\n    {\n      \"id\": \"task-011\",\n      \"title\": \"Phase P-1：拆分默认 Rust DOM shell host 与 legacy TreeShellIframeHost\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-010\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\",\n      \"scope\": [\n        \"wolai-frontend/src/components/sidebar/tree-shell-host.tsx\",\n        \"wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsx\",\n        \"wolai-frontend/src/components/sidebar/tree-shell-*.test.tsx\"\n      ],\n      \"acceptance\": [","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":337,"e":396,"m":1778951184600.0012,"t":"\"task-010\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\",\n      \"scope\": [\n        \"wolai-frontend/src/components/sidebar/tree-shell-host.tsx\",\n        \"wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsx\",\n        \"wolai-frontend/src/components/sidebar/tree-shell-*.test.tsx\"\n      ],\n      \"acceptance\": [\n        \"TreeShellHost 默认选择 rust_wasm_dom_shell_host 或等价新 host\",\n        \"TreeShellIframeHost 改为 LegacyTreeShellIframeHost 或显式 legacy/debug host\",\n        \"旧 iframe srcdoc host 只能通过 debug/legacy flag 进入\",\n        \"默认 page tree 首屏不依赖 LOCAL_TREE_SHELL_TEMPLATE\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/sidebar/tree-shell-iframe-host.test.tsx\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"TreeShellHost 默认选择 rust_wasm_dom_shell_host；TreeShellIframeHost 仅由 NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1 进入。\",\n          \"timestamp\": \"2026-04-28T04:40:14Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T04:40:14Z\"\n    },\n    {\n      \"id\": \"task-012\",\n      \"title\": \"Phase Q：page / picker / filetree 迁入 Rust/WASM DOM shell 主路径\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-011\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\",\n      \"scope\": [\n        \"rust/crates/tree-shell-runtime-wasm\",\n        \"rust/crates/mnote-web/src/tree_shell\",\n        \"wolai-frontend/src/components/sidebar\"\n      ],\n      \"acceptance\": [\n        \"page tree 默认主路径由新 Rust/WASM DOM shell 渲染与绑定事件\",\n        \"picker 默认主路径由新 Rust/WASM DOM shell 渲染与绑定事件\",\n        \"filetree 默认主路径由新 Rust/WASM DOM shell 渲染与绑定事件，并保留 doc/index/asset-folder/asset 能力\",\n        \"3000 host 只保留挂载、auth、transport、文件读取、postMessage/command bridge\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web tree_shell && cargo test -p tree-shell-runtime-wasm -- --nocapture && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/documents/move-embed-picker-dialog.test.tsx && cd /mnt/Data1T/mnote && node scripts/task112-tree-rust-family-regression-smoke.js && node scripts/task113-picker-keyboard-regression-smoke.js\",","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":385,"e":444,"m":1778951184600.0012,"t":"\"rust/crates/tree-shell-runtime-wasm\",\n        \"rust/crates/mnote-web/src/tree_shell\",\n        \"wolai-frontend/src/components/sidebar\"\n      ],\n      \"acceptance\": [\n        \"page tree 默认主路径由新 Rust/WASM DOM shell 渲染与绑定事件\",\n        \"picker 默认主路径由新 Rust/WASM DOM shell 渲染与绑定事件\",\n        \"filetree 默认主路径由新 Rust/WASM DOM shell 渲染与绑定事件，并保留 doc/index/asset-folder/asset 能力\",\n        \"3000 host 只保留挂载、auth、transport、文件读取、postMessage/command bridge\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web tree_shell && cargo test -p tree-shell-runtime-wasm -- --nocapture && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/documents/move-embed-picker-dialog.test.tsx && cd /mnt/Data1T/mnote && node scripts/task112-tree-rust-family-regression-smoke.js && node scripts/task113-picker-keyboard-regression-smoke.js\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"page / filetree / picker 默认 DOM host 已消费 TreeShellRuntimeRequest/Result，并由 WASM artifact 或同源 Rust seam 驱动 hostEvents/commandEvents。\",\n          \"timestamp\": \"2026-04-28T04:40:14Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T04:40:14Z\"\n    },\n    {\n      \"id\": \"task-013\",\n      \"title\": \"Phase R/S：删除默认 JS DOM state machine 并完成文档收口\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-012\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\",\n      \"scope\": [\n        \"wolai-frontend/src/components/sidebar\",\n        \"design/04-tree-domain/process\",\n        \"design/04-tree-domain/done\"\n      ],\n      \"acceptance\": [\n        \"默认主路径不再调用 LOCAL_TREE_SHELL_TEMPLATE、buildInlinePageTreeHtml、buildInlineFileTreeHtml、buildInlinePickerHtml\",\n        \"默认主路径不再标注 data-tree-browser-bridge=\\\"iframe_srcdoc\\\"\",\n        \"旧 JS fallback reducer/state machine 只在 legacy/debug host 中存在\",\n        \"4-11、4-16、4-18 的 final DOM shell gate 按真实代码完成状态更新\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/documents/move-embed-picker-dialog.test.tsx && cd /mnt/Data1T/mnote && node scripts/task112-tree-rust-family-regression-smoke.js && node scripts/task113-picker-keyboard-regression-smoke.js && git diff --check -- wolai-frontend/src/components/sidebar design/04-tree-domain\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":433,"e":492,"m":1778951184600.0012,"t":"\"旧 JS fallback reducer/state machine 只在 legacy/debug host 中存在\",\n        \"4-11、4-16、4-18 的 final DOM shell gate 按真实代码完成状态更新\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/documents/move-embed-picker-dialog.test.tsx && cd /mnt/Data1T/mnote && node scripts/task112-tree-rust-family-regression-smoke.js && node scripts/task113-picker-keyboard-regression-smoke.js && git diff --check -- wolai-frontend/src/components/sidebar design/04-tree-domain\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已清理 1.md、4-11、4-16、4-18 的 final DOM shell 口径；旧 JS renderer/state machine 只作为 legacy iframe host 保留。\",\n          \"timestamp\": \"2026-04-28T04:40:14Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T04:40:14Z\"\n    },\n    {\n      \"id\": \"task-014\",\n      \"title\": \"Phase L2：删除 normalizedMove fallback，强制 move 主写路径使用 treeWriteOperation\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-013\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\",\n      \"scope\": [\n        \"rust/crates/bridge-runtime/src/lib.rs\",\n        \"wolai-frontend/convex/documents.ts\",\n        \"wolai-frontend/convex/_utils/documentMoveOrder.ts\",\n        \"wolai-frontend/src/lib/documents/rust-runtime.ts\",\n        \"wolai-frontend/src/lib/documents/*move*test.ts\",\n        \"wolai-frontend/src/lib/documents/rust-runtime.test.ts\"\n      ],\n      \"acceptance\": [\n        \"bridge-runtime 不再输出 normalizedMove 作为 documents.move 参数\",\n        \"Convex documents.move 不再接受 normalizedMove fallback；缺少 treeWriteOperation 时显式拒绝\",\n        \"TS Rust mutation transport 不再透传 normalizedMove\",\n        \"相关测试改为验证 treeWriteOperation 是唯一主写入计划\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime tree_subtree_move_command && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/documents/document-move-order.test.ts src/lib/documents/document-move-order-convex.test.ts src/lib/documents/rust-runtime.test.ts src/app/api/tree/commands/route.test.ts\",\n        \"timeout_seconds\": 3000\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已删除 normalizedMove fallback：Rust plan 不再输出 normalizedMove，TS transport 强制 treeWriteOperation，Convex documents.move 只接受 treeWriteOperation；目标 Rust/Vitest 通过。\",","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":481,"e":540,"m":1778951184600.0012,"t":"\"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime tree_subtree_move_command && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/documents/document-move-order.test.ts src/lib/documents/document-move-order-convex.test.ts src/lib/documents/rust-runtime.test.ts src/app/api/tree/commands/route.test.ts\",\n        \"timeout_seconds\": 3000\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已删除 normalizedMove fallback：Rust plan 不再输出 normalizedMove，TS transport 强制 treeWriteOperation，Convex documents.move 只接受 treeWriteOperation；目标 Rust/Vitest 通过。\",\n          \"timestamp\": \"2026-04-28T06:37:40Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T06:37:40Z\"\n    },\n    {\n      \"id\": \"task-015\",\n      \"title\": \"Phase M2：定稿 document.snapshot.saved 独立事件并避免与 page.body.saved payload 重叠\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-014\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\",\n      \"scope\": [\n        \"rust/crates/bridge-runtime/src/lib.rs\",\n        \"wolai-frontend/src/lib/documents/rust-runtime.ts\",\n        \"wolai-frontend/src/lib/documents/bridge-log.ts\",\n        \"wolai-frontend/src/lib/documents/*test.ts\"\n      ],\n      \"acceptance\": [\n        \"page.body.saved 保留正文保存事件语义\",\n        \"snapshot 版本/hash/updatedAt 拆入 document.snapshot.saved 独立 domain event plan 或 artifact plan\",\n        \"SSE 对 snapshot 独立事件有明确 resync 或 noop 策略\",\n        \"测试覆盖两个事件不再 payload 重叠\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime documents_save_command_plans_include_formal_domain_event_contract && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/documents/rust-runtime.test.ts src/lib/documents/bridge-log.test.ts src/lib/documents/page-write-command-adapter.test.ts\",\n        \"timeout_seconds\": 3000\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已将 page.body.saved 与 document.snapshot.saved 拆成 domainEventPlans 双事件；artifact writer 支持 domainEvents 批量落库，page.body.saved payload 不再夹带 snapshot。\",\n          \"timestamp\": \"2026-04-28T07:44:37Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T07:44:37Z\"\n    },\n    {","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":529,"e":588,"m":1778951184600.0012,"t":"\"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已将 page.body.saved 与 document.snapshot.saved 拆成 domainEventPlans 双事件；artifact writer 支持 domainEvents 批量落库，page.body.saved payload 不再夹带 snapshot。\",\n          \"timestamp\": \"2026-04-28T07:44:37Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T07:44:37Z\"\n    },\n    {\n      \"id\": \"task-016\",\n      \"title\": \"Phase M3：把 snapshot 独立事件接入 Page Aggregate 深层收口文档与客户端边界\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-015\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\",\n      \"scope\": [\n        \"wolai-frontend/src/components/editor/page-aggregate*\",\n        \"wolai-frontend/src/lib/documents/page-aggregate*\",\n        \"design/05-editor-mainline/process\",\n        \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\"\n      ],\n      \"acceptance\": [\n        \"Page Aggregate 文档明确 page.body.saved 与 document.snapshot.saved 的职责边界\",\n        \"客户端 Page Aggregate 状态不再把 snapshot 保存当作正文保存的附带语义\",\n        \"4-16 对应 snapshot/Page Aggregate 项可勾选或明确剩余迁移边界\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/editor/page-aggregate-client-state.test.ts src/lib/documents/page-aggregate-builder.test.ts src/lib/documents/page-write-command-adapter.test.ts && git diff --check -- design/04-tree-domain design/05-editor-mainline wolai-frontend/src/components/editor wolai-frontend/src/lib/documents\",\n        \"timeout_seconds\": 2400\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已更新 Page Aggregate contract 与 5-6 清单，固定 page.body.saved 与 document.snapshot.saved 的职责边界，并同步 4-16 勾选状态。\",\n          \"timestamp\": \"2026-04-28T07:44:37Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T07:44:37Z\"\n    },\n    {\n      \"id\": \"task-017\",\n      \"title\": \"Phase N2：将 tree.node.embed 的 pageReference 组装迁到 Page Aggregate / Rust artifact 边界\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-015\"\n      ],","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":577,"e":636,"m":1778951184600.0012,"t":"}\n      ],\n      \"completed_at\": \"2026-04-28T07:44:37Z\"\n    },\n    {\n      \"id\": \"task-017\",\n      \"title\": \"Phase N2：将 tree.node.embed 的 pageReference 组装迁到 Page Aggregate / Rust artifact 边界\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-015\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\",\n      \"scope\": [\n        \"rust/crates/bridge-runtime/src/lib.rs\",\n        \"wolai-frontend/src/app/api/tree/commands/route.ts\",\n        \"wolai-frontend/src/lib/documents/page-command-adapter.ts\",\n        \"wolai-frontend/src/lib/tree-route-boundary.ts\",\n        \"wolai-frontend/src/app/api/tree/commands/route.test.ts\"\n      ],\n      \"acceptance\": [\n        \"3000 route 不再本地决定 pageReference block 的结构与插入位置语义\",\n        \"Rust plan 或 Page Aggregate artifact 接收 source/target/anchor/content snapshot 并产出 next content 或 block patch plan\",\n        \"tree-route-boundary manifest 移除该 compatPending 项\",\n        \"测试覆盖 tree.node.embed 的 pageReference 组装来源已迁移\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime documents_embed_command_plan_maps_to_documents_update_content tree_embed_and_copy_aliases_keep_tree_command_names && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/app/api/tree/commands/route.test.ts src/lib/tree-route-boundary.test.ts src/lib/documents/rust-runtime.test.ts\",\n        \"timeout_seconds\": 3000\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 Rust pageAggregateEmbedPlan；3000 route 与 documents.embed adapter 只传 source/target/anchor/content snapshot preflight，不再本地决定 pageReference block 结构与插入位置。\",\n          \"timestamp\": \"2026-04-28T07:44:37Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T07:44:37Z\"\n    },\n    {\n      \"id\": \"task-018\",\n      \"title\": \"Phase O2：补 file_tree 搜索索引可见性指标边界并收口 4-16\",\n      \"status\": \"completed\",\n      \"priority\": \"P2\",\n      \"depends_on\": [\n        \"task-014\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\",\n      \"scope\": [","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":625,"e":684,"m":1778951184600.0012,"t":"\"id\": \"task-018\",\n      \"title\": \"Phase O2：补 file_tree 搜索索引可见性指标边界并收口 4-16\",\n      \"status\": \"completed\",\n      \"priority\": \"P2\",\n      \"depends_on\": [\n        \"task-014\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"81ba21c5\",\n      \"source_doc\": \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\",\n      \"scope\": [\n        \"rust/crates/bridge-runtime/src/lib.rs\",\n        \"rust/crates/mnote-web/src/routes/kernel.rs\",\n        \"wolai-frontend/src/lib/server/kernel-file-tree.ts\",\n        \"wolai-frontend/src/app/api/tree/projections/file/route.ts\",\n        \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\"\n      ],\n      \"acceptance\": [\n        \"file_tree 搜索 projection 输出稳定 indexing/visibility meta，不再只是临时 request meta\",\n        \"3000 同源 projection route 透传该 meta\",\n        \"move-embed picker/file tree 搜索可观察 index visibility 状态\",\n        \"4-16 中 Phase O 未完成项完成或明确转为非树域 runtime 后续\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime file_tree && cargo test -p mnote-web file_tree_projection && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/app/api/tree/projections/file/route.test.ts src/lib/tree-stream/tree-delta.test.ts\",\n        \"timeout_seconds\": 3000\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已把 file_tree 搜索 indexingVisibility 提升为 projection result meta；mnote-web 与 3000 同源 projection route 均透传该指标，4-16 Phase O 已收口。\",\n          \"timestamp\": \"2026-04-28T07:44:37Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T07:44:37Z\"\n    },\n    {\n      \"id\": \"task-019\",\n      \"title\": \"Phase W-0：冻结 Rust Web 主执行面 owner manifest 与负向门禁\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"wolai-frontend/src/lib/rust-web-main-execution-boundary.ts\",\n        \"wolai-frontend/src/lib/rust-web-main-execution-boundary.test.ts\",\n        \"ARCHITECTURE.md\",\n        \"design/03-rust-web/process/3-6-rust-web-main-execution-plane-checklist-v1.md\"\n      ],\n      \"acceptance\": [","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":673,"e":732,"m":1778951184600.0012,"t":"\"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"wolai-frontend/src/lib/rust-web-main-execution-boundary.ts\",\n        \"wolai-frontend/src/lib/rust-web-main-execution-boundary.test.ts\",\n        \"ARCHITECTURE.md\",\n        \"design/03-rust-web/process/3-6-rust-web-main-execution-plane-checklist-v1.md\"\n      ],\n      \"acceptance\": [\n        \"新增 RUST_WEB_MAIN_EXECUTION_BOUNDARY，固定 publicEntry=3000、mainWebOwner=mnote-web、legacyCompatOwner=next-app-router\",\n        \"测试禁止把 Next App Router 写回主 Web owner\",\n        \"ARCHITECTURE.md 明确树域完成不等于主 Web 执行面完成\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/rust-web-main-execution-boundary.test.ts src/lib/runtime-config.test.ts && cd /mnt/Data1T/mnote && git diff --check -- ARCHITECTURE.md design/03-rust-web wolai-frontend/src/lib\",\n        \"timeout_seconds\": 1800\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 Rust Web 主执行面 owner manifest 与负向门禁，并更新 ARCHITECTURE.md / 3-6 checklist 口径。\",\n          \"timestamp\": \"2026-04-28T09:07:58Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T09:07:58Z\"\n    },\n    {\n      \"id\": \"task-020\",\n      \"title\": \"Phase W-1：让 mnote-web 默认成为 3000 gateway owner\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-019\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"rust/crates/mnote-web/src/app.rs\",\n        \"rust/crates/mnote-web/src/routes/mod.rs\",\n        \"rust/crates/mnote-web/src/routes/gateway.rs\",\n        \"scripts/desktop-hot.js\",\n        \"scripts/desktop-hot.test.js\",\n        \"scripts/task114-rust-web-gateway-entry-smoke.js\"\n      ],\n      \"acceptance\": [\n        \"mnote-web 支持 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 并作为默认 3000 owner\",\n        \"Next dev 默认改为 legacy upstream port，不再拥有 3000\",\n        \"task114 smoke 证明 3000 响应来自 Rust gateway 且 3104 不作为公开入口\"\n      ],","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":721,"e":780,"m":1778951184600.0012,"t":"\"rust/crates/mnote-web/src/app.rs\",\n        \"rust/crates/mnote-web/src/routes/mod.rs\",\n        \"rust/crates/mnote-web/src/routes/gateway.rs\",\n        \"scripts/desktop-hot.js\",\n        \"scripts/desktop-hot.test.js\",\n        \"scripts/task114-rust-web-gateway-entry-smoke.js\"\n      ],\n      \"acceptance\": [\n        \"mnote-web 支持 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 并作为默认 3000 owner\",\n        \"Next dev 默认改为 legacy upstream port，不再拥有 3000\",\n        \"task114 smoke 证明 3000 响应来自 Rust gateway 且 3104 不作为公开入口\"\n      ],\n      \"validation\": {\n        \"command\": \"node --test /mnt/Data1T/mnote/scripts/desktop-hot.test.js && cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web gateway && cd /mnt/Data1T/mnote && node scripts/task114-rust-web-gateway-entry-smoke.js\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 mnote-web gateway route/owner header、desktop-hot Rust gateway 默认启动计划与 task114 smoke。\",\n          \"timestamp\": \"2026-04-28T09:13:44Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T09:13:44Z\"\n    },\n    {\n      \"id\": \"task-021\",\n      \"title\": \"Phase W-2：把 runtime config 与 auth/session handoff 迁到 Rust Web\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-020\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"rust/crates/mnote-web/src/routes/session.rs\",\n        \"rust/crates/mnote-web/src/routes/mod.rs\",\n        \"wolai-frontend/src/lib/runtime-config.ts\",\n        \"wolai-frontend/src/lib/runtime-config.test.ts\",\n        \"wolai-frontend/src/app/api/auth/mnote-web-token/route.ts\"\n      ],\n      \"acceptance\": [\n        \"mnote-web 输出 /api/runtime/config、/api/auth/session 与 session refresh/handoff route\",\n        \"浏览器 runtime config 不泄漏 internal MNOTE_WEB_* base URL\",\n        \"Next /api/auth/mnote-web-token 降级为 compat/debug\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web session && cargo test -p mnote-web runtime_config && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/runtime-config.test.ts src/app/api/auth/mnote-web-token/route.test.ts\",\n        \"timeout_seconds\": 3000\n      },\n      \"on_failure\": {","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":769,"e":828,"m":1778951184600.0012,"t":"\"wolai-frontend/src/app/api/auth/mnote-web-token/route.ts\"\n      ],\n      \"acceptance\": [\n        \"mnote-web 输出 /api/runtime/config、/api/auth/session 与 session refresh/handoff route\",\n        \"浏览器 runtime config 不泄漏 internal MNOTE_WEB_* base URL\",\n        \"Next /api/auth/mnote-web-token 降级为 compat/debug\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web session && cargo test -p mnote-web runtime_config && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/runtime-config.test.ts src/app/api/auth/mnote-web-token/route.test.ts\",\n        \"timeout_seconds\": 3000\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 mnote-web runtime config/session/refresh routes，并将 Next mnote-web-token route 标记为 compat/debug。\",\n          \"timestamp\": \"2026-04-28T09:16:28Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T09:16:28Z\"\n    },\n    {\n      \"id\": \"task-022\",\n      \"title\": \"Phase W-3：建立 Rust-owned 文档页 / Page Aggregate server-first shell\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-021\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"rust/crates/mnote-web/src/routes/web_shell.rs\",\n        \"rust/crates/mnote-web/src/routes/documents.rs\",\n        \"rust/crates/mnote-web/src/routes/mod.rs\",\n        \"wolai-frontend/src/components/editor/document-shell.tsx\",\n        \"wolai-frontend/src/components/editor/document-content.tsx\",\n        \"scripts/task115-rust-web-document-shell-smoke.js\"\n      ],\n      \"acceptance\": [\n        \"mnote-web 输出 /documents/:id server-first shell 与 page aggregate snapshot\",\n        \"Next 文档页降级为 legacy compat\",\n        \"task115 smoke 证明默认文档页由 Rust shell 服务且不挂载 mnote-web-document-shell iframe\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web document_shell && cargo test -p mnote-web page_aggregate && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/editor/document-content.test.ts src/lib/documents/page-aggregate-builder.test.ts && cd /mnt/Data1T/mnote && node scripts/task115-rust-web-document-shell-smoke.js\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":817,"e":876,"m":1778951184600.0012,"t":"\"Next 文档页降级为 legacy compat\",\n        \"task115 smoke 证明默认文档页由 Rust shell 服务且不挂载 mnote-web-document-shell iframe\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web document_shell && cargo test -p mnote-web page_aggregate && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/editor/document-content.test.ts src/lib/documents/page-aggregate-builder.test.ts && cd /mnt/Data1T/mnote && node scripts/task115-rust-web-document-shell-smoke.js\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已新增 mnote-web /documents/:id shell 与 /api/page-aggregate/:id snapshot contract，并补 task115 smoke。\",\n          \"timestamp\": \"2026-04-28T09:20:22Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T09:20:22Z\"\n    },\n    {\n      \"id\": \"task-023\",\n      \"title\": \"Phase W-4：把主 API transport 从 Next route 收薄为 Rust Web owned\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-022\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"rust/crates/mnote-web/src/routes/documents.rs\",\n        \"rust/crates/mnote-web/src/routes/tree.rs\",\n        \"rust/crates/mnote-web/src/routes/search.rs\",\n        \"wolai-frontend/src/lib/tree-route-boundary.ts\",\n        \"wolai-frontend/src/app/api/documents/route-adapters.test.ts\",\n        \"wolai-frontend/src/lib/search/request.test.ts\"\n      ],\n      \"acceptance\": [\n        \"Rust Web owner 覆盖 documents meta/content/save、tree commands/projections 与 search documents transport\",\n        \"Next route 只保留 cookie/header 透传和 legacy compat\",\n        \"TREE_3000_ROUTE_BOUNDARY_MANIFEST owner 口径改为 rust-web-gateway\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web documents_api && cargo test -p mnote-web tree_commands && cargo test -p mnote-web search && cargo test -p bridge-runtime page_aggregate && cargo test -p bridge-runtime tree_subtree_move_command && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/tree-route-boundary.test.ts src/app/api/documents/route-adapters.test.ts src/lib/search/request.test.ts\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [\n        \"[CONFIG] 原验证中的 cargo test -p bridge-runtime page_aggregate 过滤词当前命中 0 项；本任务实际覆盖 documents_api/tree_commands/search 与 route boundary。\"\n      ],\n      \"checkpoints\": [\n        {\n          \"step\": 1,","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":865,"e":924,"m":1778951184600.0012,"t":"\"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web documents_api && cargo test -p mnote-web tree_commands && cargo test -p mnote-web search && cargo test -p bridge-runtime page_aggregate && cargo test -p bridge-runtime tree_subtree_move_command && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/tree-route-boundary.test.ts src/app/api/documents/route-adapters.test.ts src/lib/search/request.test.ts\",\n        \"timeout_seconds\": 3600\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [\n        \"[CONFIG] 原验证中的 cargo test -p bridge-runtime page_aggregate 过滤词当前命中 0 项；本任务实际覆盖 documents_api/tree_commands/search 与 route boundary。\"\n      ],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"已补 Rust Web search.documents transport、documents owner headers 与 TREE_3000_ROUTE_BOUNDARY rust-web-gateway owner 口径。\",\n          \"timestamp\": \"2026-04-28T09:26:14Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T09:26:14Z\"\n    },\n    {\n      \"id\": \"task-024\",\n      \"title\": \"Phase W-5：把 tree realtime stream 固定为 Rust Web 主链\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-023\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"rust/crates/mnote-web/src/routes/sse.rs\",\n        \"rust/crates/mnote-web/src/routes/stream_support.rs\",\n        \"wolai-frontend/src/app/api/mnote-web/stream/route.ts\",\n        \"wolai-frontend/src/lib/tree-stream/protocol.ts\",\n        \"wolai-frontend/src/lib/tree-stream/tree-stream.test.ts\",\n        \"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx\"\n      ],\n      \"acceptance\": [\n        \"Rust Web 输出正式 /api/tree/events snapshot + delta stream\",\n        \"/api/mnote-web/stream 降级为 compat alias\",\n        \"Sidebar / page tree / file tree consumer 使用 Rust-owned stream\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web tree_realtime && cargo test -p mnote-web stream && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/app/api/mnote-web/stream/route.test.ts src/lib/tree-stream/tree-stream.test.ts src/lib/tree-stream/use-sidebar-tree-stream.test.tsx && cd /mnt/Data1T/mnote && node scripts/task112-tree-rust-family-regression-smoke.js\",\n        \"timeout_seconds\": 4200\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"Rust Web /api/tree/events 已固定为 tree realtime stream 主链，输出 rust-web owner、snapshot/delta stream；Next /api/mnote-web/stream 保留 compat alias，consumer URL 指向 /api/tree/events。task112 针对 legacy Chromium renderer crash 仅做一次新 context 恢复。\",\n          \"timestamp\": \"2026-04-28T12:33:41Z\"\n        }","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":913,"e":972,"m":1778951184600.0012,"t":"},\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"Rust Web /api/tree/events 已固定为 tree realtime stream 主链，输出 rust-web owner、snapshot/delta stream；Next /api/mnote-web/stream 保留 compat alias，consumer URL 指向 /api/tree/events。task112 针对 legacy Chromium renderer crash 仅做一次新 context 恢复。\",\n          \"timestamp\": \"2026-04-28T12:33:41Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T12:33:41Z\"\n    },\n    {\n      \"id\": \"task-025\",\n      \"title\": \"Phase W-6：把 Search / AI / Mindmap 页面壳迁到 Rust Web shell + islands\",\n      \"status\": \"completed\",\n      \"priority\": \"P1\",\n      \"depends_on\": [\n        \"task-024\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"rust/crates/mnote-web/src/routes/search.rs\",\n        \"rust/crates/mnote-web/src/routes/mindmap_shell.rs\",\n        \"rust/crates/mnote-web/src/routes/hermes.rs\",\n        \"wolai-frontend/src/components/search/SearchPaletteHost.tsx\",\n        \"wolai-frontend/src/components/editor/DocumentAiAgentPanel.runtime.tsx\",\n        \"wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/page.tsx\",\n        \"scripts/task116-rust-web-islands-smoke.js\"\n      ],\n      \"acceptance\": [\n        \"Rust Web 输出 Search server-first shell\",\n        \"Rust Web / Hermes 拥有 AI bridge session、tool event、client action contract\",\n        \"Rust Web 输出 Mindmap object shell，React runtime 只作为 island 或 legacy compat\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web search_shell && cargo test -p mnote-web ai_bridge && cargo test -p mnote-web mindmap_shell && cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/components/editor/DocumentAiAgentPanel.runtime.test.tsx src/lib/ai-agent/runtime/runAgent.test.ts src/lib/search/request.test.ts && cd /mnt/Data1T/mnote && node scripts/task116-rust-web-islands-smoke.js\",\n        \"timeout_seconds\": 4200\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"Search server-first shell、Hermes AI bridge owner contract、Mindmap object shell 已落到 Rust Web；React runtime 标记为 island/legacy compat；task116 smoke 证明三类 owner 已迁移。\",\n          \"timestamp\": \"2026-04-28T12:44:04Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T12:44:04Z\"\n    },","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":961,"e":1020,"m":1778951184600.0012,"t":"},\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"Search server-first shell、Hermes AI bridge owner contract、Mindmap object shell 已落到 Rust Web；React runtime 标记为 island/legacy compat；task116 smoke 证明三类 owner 已迁移。\",\n          \"timestamp\": \"2026-04-28T12:44:04Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T12:44:04Z\"\n    },\n    {\n      \"id\": \"task-026\",\n      \"title\": \"Phase W-7：退役 Next App Router 主入口并完成 03-rust-web 文档收口\",\n      \"status\": \"completed\",\n      \"priority\": \"P0\",\n      \"depends_on\": [\n        \"task-025\"\n      ],\n      \"attempts\": 1,\n      \"max_attempts\": 3,\n      \"started_at_commit\": \"9f6a2c86\",\n      \"source_doc\": \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n      \"plan_doc\": \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n      \"scope\": [\n        \"ARCHITECTURE.md\",\n        \"design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md\",\n        \"design/03-rust-web/done/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n        \"design/03-rust-web/done/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n        \"scripts/task117-next-retirement-guard.js\"\n      ],\n      \"acceptance\": [\n        \"task117 证明 SKIP_NEXT_LEGACY=1 时核心 smoke 仍可通过\",\n        \"默认启动链不再把 pnpm dev 作为 3000 owner，3104 不恢复为公开入口\",\n        \"ARCHITECTURE.md 与 3-1 同步 Next App Router legacy compat 口径，本 plan/checklist 移入 done\"\n      ],\n      \"validation\": {\n        \"command\": \"cd /mnt/Data1T/mnote && node scripts/task097-homepage-entry-smoke.js && node scripts/task114-rust-web-gateway-entry-smoke.js && node scripts/task115-rust-web-document-shell-smoke.js && node scripts/task116-rust-web-islands-smoke.js && node scripts/task117-next-retirement-guard.js && git diff --check -- ARCHITECTURE.md design/03-rust-web scripts wolai-frontend rust/crates/mnote-web\",\n        \"timeout_seconds\": 4800\n      },\n      \"on_failure\": {\n        \"cleanup\": null\n      },\n      \"error_log\": [],\n      \"checkpoints\": [\n        {\n          \"step\": 1,\n          \"total\": 1,\n          \"description\": \"task117 已证明 SKIP_NEXT_LEGACY=1 时核心 shell 不依赖 Next legacy；desktop-hot 支持跳过 Next legacy；ARCHITECTURE/3-1 已同步 Next App Router legacy compat 口径，3-5/3-6 已移入 done。\",\n          \"timestamp\": \"2026-04-28T12:50:07Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T12:50:07Z\"\n    }\n  ],\n  \"session_count\": 10,\n  \"last_session\": \"2026-04-28T12:50:07Z\",\n  \"final_renderer_gate\": {\n    \"source_doc\": \"design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\",","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":1009,"e":1068,"m":1778951184600.0012,"t":"\"total\": 1,\n          \"description\": \"task117 已证明 SKIP_NEXT_LEGACY=1 时核心 shell 不依赖 Next legacy；desktop-hot 支持跳过 Next legacy；ARCHITECTURE/3-1 已同步 Next App Router legacy compat 口径，3-5/3-6 已移入 done。\",\n          \"timestamp\": \"2026-04-28T12:50:07Z\"\n        }\n      ],\n      \"completed_at\": \"2026-04-28T12:50:07Z\"\n    }\n  ],\n  \"session_count\": 10,\n  \"last_session\": \"2026-04-28T12:50:07Z\",\n  \"final_renderer_gate\": {\n    \"source_doc\": \"design/04-tree-domain/process/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md\",\n    \"status\": \"completed\",\n    \"note\": \"task-001 至 task-009 只代表 Rust/WASM reducer runtime 阶段完成；不得再把 inline iframe srcdoc host 视为 final Rust DOM renderer。\"\n  },\n  \"final_runtime_remaining\": {\n    \"source_doc\": \"design/04-tree-domain/done/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md\",\n    \"status\": \"completed\",\n    \"tasks\": [\n      \"task-014\",\n      \"task-015\",\n      \"task-016\",\n      \"task-017\",\n      \"task-018\"\n    ]\n  },\n  \"main_web_execution_plane\": {\n    \"source_doc\": \"design/03-rust-web/process/3-6-rust-web-main-execution-plane-checklist-v1.md\",\n    \"plan_doc\": \"design/03-rust-web/process/3-5-rust-web-main-execution-plane-cutover-plan-v1.md\",\n    \"status\": \"planned\",\n    \"tasks\": [\n      \"task-019\",\n      \"task-020\",\n      \"task-021\",\n      \"task-022\",\n      \"task-023\",\n      \"task-024\",\n      \"task-025\",\n      \"task-026\"\n    ],\n    \"note\": \"树域 Rust family 默认执行面已完成；主 Web 执行面仍需通过 task-019 至 task-026 迁到 mnote-web / Rust family。\"\n  },\n  \"rust_web_3_4_undo\": {\n    \"source_doc\": \"design/03-rust-web/process/3-4-undo.md\",\n    \"status\": \"in_progress\",\n    \"dependency_chain\": \"A -> B/C -> D/E/F -> G -> H\",\n    \"tasks\": [\n      {\n        \"id\": \"task-3-4-A\",\n        \"title\": \"Page Aggregate 升级为 core-protocol projection contract\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol page_aggregate && cargo test -p bridge-runtime page_aggregate && cargo test -p mnote-web page_aggregate\"\n      },\n      {\n        \"id\": \"task-3-4-B\",\n        \"title\": \"page.* 成为页面默认写命令族\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-A\"],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime page_body_save && cargo test -p bridge-runtime page_head_update_title && cargo test -p bridge-runtime page_layout_update_options && cargo test -p mnote-web documents_save\"","v":"[encoded data, 4096 characters]"}
{"p":"harness-tasks.json","s":1057,"e":1115,"m":1778951184600.0012,"t":"\"id\": \"task-3-4-A\",\n        \"title\": \"Page Aggregate 升级为 core-protocol projection contract\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol page_aggregate && cargo test -p bridge-runtime page_aggregate && cargo test -p mnote-web page_aggregate\"\n      },\n      {\n        \"id\": \"task-3-4-B\",\n        \"title\": \"page.* 成为页面默认写命令族\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-A\"],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime page_body_save && cargo test -p bridge-runtime page_head_update_title && cargo test -p bridge-runtime page_layout_update_options && cargo test -p mnote-web documents_save\"\n      },\n      {\n        \"id\": \"task-3-4-C\",\n        \"title\": \"3000 Rust shell 消费 tree realtime event stream\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-A\"],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web tree_events && cargo test -p mnote-web tree_command && cd /mnt/Data1T/mnote && MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js\"\n      },\n      {\n        \"id\": \"task-3-4-D\",\n        \"title\": \"Search 收口为 server-first / kernel-aware 主链\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-B\", \"task-3-4-C\"],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol search && cargo test -p bridge-runtime search_documents_query && cargo test -p mnote-web search && cd /mnt/Data1T/mnote && MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task125-rust-web-search-server-first-smoke.js\"\n      },\n      {\n        \"id\": \"task-3-4-E\",\n        \"title\": \"AI bridge 与结构化写链收口\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-B\", \"task-3-4-C\"],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol ai && cargo test -p mnote-web hermes && cd /mnt/Data1T/mnote && MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task126-rust-web-ai-bridge-structured-write-smoke.js\"\n      },\n      {\n        \"id\": \"task-3-4-F\",\n        \"title\": \"Mindmap 成为 kernel projection / command truth\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-B\", \"task-3-4-C\"],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol mindmap && cargo test -p bridge-runtime mindmap && cargo test -p mnote-web mindmap && cd /mnt/Data1T/mnote && MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task127-rust-web-mindmap-kernel-projection-smoke.js\"\n      },\n      {\n        \"id\": \"task-3-4-G\",\n        \"title\": \"Legacy Next / React 兼容层退役 gate\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-D\", \"task-3-4-E\", \"task-3-4-F\"],\n        \"validation\": \"cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web gateway && cargo test -p mnote-web legacy_next && cd /mnt/Data1T/mnote && MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task117-next-retirement-guard.js\"\n      },\n      {\n        \"id\": \"task-3-4-H\",\n        \"title\": \"设计文档状态治理与执行节奏\",\n        \"status\": \"in_progress\",\n        \"depends_on\": [\"task-3-4-G\"],\n        \"validation\": \"cd /mnt/Data1T/mnote && rg -n \\\"\\\\\\\\[ \\\\\\\\]\\\" design/03-rust-web/process/3-4-undo.md && rg -n \\\"\\\\\\\\[done\\\\\\\\]|\\\\\\\\[recycle\\\\\\\\]\\\" design/03-rust-web design/old && git diff --check && git status --short --untracked-files=all\"\n      }\n    ]\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"package.json","s":1,"e":60,"m":1769360164441.8494,"t":"{\n  \"name\": \"mnote-desktop\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"main\": \"desktop-electron/main.js\",\n  \"scripts\": {\n    \"desktop\": \"node scripts/desktop-prod.js\",\n    \"desktop:hot\": \"node scripts/desktop-hot.js\",\n    \"desktop:local\": \"node scripts/desktop-local.js\",\n    \"desktop:electron\": \"node desktop-electron/dev-runner.js\",\n    \"build:desktop:next\": \"node scripts/prepare-desktop-next.js\",\n    \"dist:win\": \"pnpm run build:desktop:next && electron-builder --win nsis\"\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.56.1\",\n    \"electron\": \"^34.0.0\",\n    \"electron-builder\": \"^26.0.12\",\n    \"playwright\": \"^1.56.1\"\n  },\n  \"build\": {\n    \"appId\": \"org.aichem.mnote\",\n    \"productName\": \"MNOTE\",\n    \"asar\": true,\n    \"directories\": {\n      \"output\": \"dist\",\n      \"buildResources\": \"desktop-electron/build\"\n    },\n    \"files\": [\n      \"package.json\",\n      \"desktop-electron/**\",\n      \"!desktop-electron/desktop-next{,/**}\"\n    ],\n    \"extraResources\": [\n      {\n        \"from\": \"desktop-electron/desktop-next\",\n        \"to\": \"desktop-next\",\n        \"filter\": [\n          \"**/*\",\n          \"!node_modules{,/**}\"\n        ]\n      },\n      {\n        \"from\": \"desktop-electron/desktop-next/node_modules\",\n        \"to\": \"desktop-next/node_modules\"\n      }\n    ],\n    \"win\": {\n      \"target\": [\n        \"nsis\"\n      ]\n    },\n    \"nsis\": {\n      \"oneClick\": false,\n      \"allowToChangeInstallationDirectory\": true,\n      \"include\": \"desktop-electron/build/installer.nsh\"\n    }\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [","v":"[encoded data, 4096 characters]"}
{"p":"package.json","s":49,"e":66,"m":1769360164441.8494,"t":"\"target\": [\n        \"nsis\"\n      ]\n    },\n    \"nsis\": {\n      \"oneClick\": false,\n      \"allowToChangeInstallationDirectory\": true,\n      \"include\": \"desktop-electron/build/installer.nsh\"\n    }\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"electron\",\n      \"electron-winstaller\"\n    ]\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"playwright-snapshot-auth.md","s":1,"e":1,"m":1778542528420.6665,"t":"- generic [ref=e2] [box=0,13,1440,39]: \"{\\\"ok\\\":false,\\\"code\\\":\\\"convex_upstream_error\\\",\\\"message\\\":\\\"Convex mutation 失败\\\",\\\"requestId\\\":\\\"req_1778542522227_65\\\",\\\"traceId\\\":\\\"trace_1778542522227_66\\\"}\"","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/.gitignore","s":1,"e":48,"m":1773495424137.6084,"t":"# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n#test\n**/test/\npw-tests/**\npw-tests/  \nwolai-frontend/public/documents/**","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/components.json","s":1,"e":23,"m":1773495424146.6084,"t":"{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.err","s":1,"e":18,"m":1773495424157.6086,"t":"F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.err","s":19,"e":34,"m":1773495424157.6086,"t":"F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.err","s":35,"e":48,"m":1773495424157.6086,"t":"Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.err","s":49,"e":60,"m":1773495424157.6086,"t":"    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.err","s":49,"e":63,"m":1773495424157.6086,"t":"at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\node_modules__pnpm_ecee5139._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: Route \"/documents/[id]\" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis\n    at DocumentPage (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__d0423852._.js:120:150)","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.err","s":64,"e":79,"m":1773495424157.6086,"t":"Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\2536e_@blocknote_react_dist_blocknote-react_65a5c21f.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\node_modules\\.pnpm\\next@16.0.3_@babel+core@7.2_a6e7fe7b2107bfd5c9e45031e322c5c3\\node_modules\\next\\dist\\compiled\\next-server\\app-page-turbo.runtime.dev.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__c5e4d87b._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\n ⨯ ReferenceError: window is not defined\n    at <unknown> (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\2536e_@blocknote_react_dist_blocknote-react_65a5c21f.js:421:9)\n    at Se (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\2536e_@blocknote_react_dist_blocknote-react_65a5c21f.js:419:354)\n    at BlockNoteEditor (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__c5e4d87b._.js:87:315) {\n  digest: '2629247923'\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.out","s":1,"e":60,"m":1773495424171.6086,"t":"> wolai-frontend@0.1.0 dev F:\\SOFT\\MNOTE\\wolai-frontend\n> next dev \"--port\" \"3001\"\n\n   ▲ Next.js 16.0.3 (Turbopack)\n   - Local:         http://localhost:3001\n   - Network:       http://192.168.121.1:3001\n   - Environments: .env.local\n\n ✓ Starting...\n ✓ Ready in 729ms\n GET /login 200 in 2.3s (compile: 2.1s, render: 265ms)\n GET / 200 in 613ms (compile: 365ms, render: 248ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 3.4s (compile: 2.9s, render: 473ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 442ms (compile: 26ms, render: 415ms)\n GET /documents/1f256e20-0ef4-4090-9bec-954fcd799a4c 200 in 245ms (compile: 25ms, render: 220ms)\n POST /api/documents/create 200 in 424ms (compile: 311ms, render: 113ms)\n GET /documents/880f0a7a-e894-465b-a64d-bea1afa3da48 200 in 62ms (compile: 27ms, render: 34ms)\n GET /documents/1f256e20-0ef4-4090-9bec-954fcd799a4c 200 in 68ms (compile: 22ms, render: 47ms)\n GET /documents/e8b15472-3665-425e-b945-0de6bc9a15fb 200 in 56ms (compile: 22ms, render: 35ms)\n GET /documents/880f0a7a-e894-465b-a64d-bea1afa3da48 200 in 56ms (compile: 22ms, render: 34ms)\n GET / 307 in 297ms (compile: 2ms, render: 295ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 222ms (compile: 23ms, render: 199ms)\n GET / 307 in 102ms (compile: 3ms, render: 99ms)\n GET /login 200 in 53ms (compile: 3ms, render: 49ms)\n GET / 200 in 20ms (compile: 3ms, render: 17ms)\n GET /login 200 in 35ms (compile: 4ms, render: 31ms)\n GET /login 200 in 18ms (compile: 2ms, render: 15ms)\n GET / 200 in 20ms (compile: 3ms, render: 17ms)\n GET /login 200 in 34ms (compile: 3ms, render: 31ms)\n GET /login 200 in 18ms (compile: 2ms, render: 16ms)\n GET / 307 in 108ms (compile: 3ms, render: 105ms)\n GET /login 200 in 57ms (compile: 5ms, render: 52ms)\n GET / 307 in 193ms (compile: 3ms, render: 190ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 125ms (compile: 23ms, render: 102ms)\n GET / 200 in 27ms (compile: 4ms, render: 23ms)\n GET /login 200 in 34ms (compile: 3ms, render: 31ms)\n GET /login 200 in 19ms (compile: 3ms, render: 16ms)\n GET / 200 in 19ms (compile: 3ms, render: 16ms)\n GET /login 200 in 39ms (compile: 3ms, render: 36ms)\n GET /login 200 in 19ms (compile: 3ms, render: 16ms)\n GET / 200 in 20ms (compile: 3ms, render: 18ms)\n GET /login 200 in 40ms (compile: 9ms, render: 31ms)\n GET /login 200 in 19ms (compile: 2ms, render: 17ms)\n GET / 200 in 27ms (compile: 5ms, render: 22ms)\n GET /login 200 in 41ms (compile: 3ms, render: 37ms)\n GET /login 200 in 21ms (compile: 2ms, render: 18ms)\n GET / 200 in 23ms (compile: 3ms, render: 20ms)\n GET /login 200 in 40ms (compile: 3ms, render: 37ms)\n GET /login 200 in 22ms (compile: 3ms, render: 19ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 307 in 181ms (compile: 29ms, render: 152ms)\n GET /login 200 in 60ms (compile: 3ms, render: 57ms)\n ✓ Compiled in 90ms\n GET /login 200 in 235ms (compile: 93ms, render: 142ms)\n GET /login 200 in 42ms (compile: 3ms, render: 39ms)\n GET / 200 in 175ms (compile: 93ms, render: 82ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 313ms (compile: 125ms, render: 188ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 67ms (compile: 4ms, render: 64ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 235ms (compile: 118ms, render: 117ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 611ms (compile: 24ms, render: 587ms)","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/dev-3001.out","s":49,"e":61,"m":1773495424171.6086,"t":"GET /login 200 in 40ms (compile: 3ms, render: 37ms)\n GET /login 200 in 22ms (compile: 3ms, render: 19ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 307 in 181ms (compile: 29ms, render: 152ms)\n GET /login 200 in 60ms (compile: 3ms, render: 57ms)\n ✓ Compiled in 90ms\n GET /login 200 in 235ms (compile: 93ms, render: 142ms)\n GET /login 200 in 42ms (compile: 3ms, render: 39ms)\n GET / 200 in 175ms (compile: 93ms, render: 82ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 313ms (compile: 125ms, render: 188ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 67ms (compile: 4ms, render: 64ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 235ms (compile: 118ms, render: 117ms)\n GET /documents/2b01d6b7-dc39-49b9-97bb-0be59956e6fb 200 in 611ms (compile: 24ms, render: 587ms)","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/eslint.config.mjs","s":1,"e":34,"m":1773495424200.184,"t":"import { defineConfig, globalIgnores } from \"eslint/config\";\nimport nextVitals from \"eslint-config-next/core-web-vitals\";\nimport nextTs from \"eslint-config-next/typescript\";\n\nconst eslintConfig = defineConfig([\n  ...nextVitals,\n  ...nextTs,\n  {\n    // 说明：当前仓库存在大量历史代码与第三方/生成代码。\n    // 为避免 ESLint 在生产构建阶段被“风格型规则”阻塞，这里将部分规则从 error 降级为 warning。\n    name: \"mnote-lint-overrides\",\n    rules: {\n      \"@typescript-eslint/no-this-alias\": \"warn\",\n      \"@typescript-eslint/no-explicit-any\": \"warn\",\n      \"@typescript-eslint/no-require-imports\": \"warn\",\n      \"react-hooks/set-state-in-effect\": \"warn\",\n    },\n  },\n  // Override default ignores of eslint-config-next.\n  globalIgnores([\n    // Default ignores of eslint-config-next:\n    \".next/**\",\n    \"out/**\",\n    \"build/**\",\n    \"next-env.d.ts\",\n    // 说明：静态资源/第三方构建产物不参与 lint（否则会产生大量无意义报错）。\n    \"public/luckysheet/**\",\n    // 说明：Convex 生成代码不参与 lint。\n    \"convex/_generated/**\",\n  ]),\n]);\n\nexport default eslintConfig;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/frontend-3001.err","s":1,"e":1,"m":1773495424208.6091,"t":"^C","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/frontend-3001.out","s":1,"e":12,"m":1773495424215.6091,"t":"> wolai-frontend@0.1.0 dev F:\\SOFT\\MNOTE\\wolai-frontend\n> next dev \"--port\" \"3001\"\n\n   ▲ Next.js 16.0.3 (Turbopack)\n   - Local:         http://localhost:3001\n   - Network:       http://192.168.121.1:3001\n   - Environments: .env.local\n\n ✓ Starting...\n ✓ Ready in 876ms","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/frontend.err","s":1,"e":13,"m":1773495424224.6094,"t":"⨯ Failed to start server\nF:\\SOFT\\MNOTE\\wolai-frontend\\node_modules\\.pnpm\\next@16.0.3_@babel+core@7.2_a6e7fe7b2107bfd5c9e45031e322c5c3\\node_modules\\next\\dist\\server\\lib\\start-server.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\node_modules\\.pnpm\\next@16.0.3_@babel+core@7.2_a6e7fe7b2107bfd5c9e45031e322c5c3\\node_modules\\next\\dist\\trace\\trace.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nError: listen EADDRINUSE: address already in use :::3000\n    at <unknown> (Error: listen EADDRINUSE: address already in use :::3000)\n    at new Promise (<anonymous>) {\n  code: 'EADDRINUSE',\n  errno: -4091,\n  syscall: 'listen',\n  address: '::',\n  port: 3000\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/frontend.out","s":1,"e":7,"m":1773495424235.6094,"t":"> wolai-frontend@0.1.0 dev F:\\SOFT\\MNOTE\\wolai-frontend\n> next dev \"--port\" \"3000\"\n\n\u001b[?25h\n ELIFECYCLE  Command failed with exit code 1.","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/next-dev.err","s":46,"e":60,"m":1773495424243.6096,"t":"    at async (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\012f5__pnpm_791fff69._.js:12357:24)\n    at async (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\012f5__pnpm_791fff69._.js:8251:36) {\n  __isAuthError: true,\n  status: 400,\n  code: 'refresh_token_not_found'\n}\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\wolai-frontend_9f844c81._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__97b7743c._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\n ⨯ Error: 创建默认工作空间失败：new row violates row-level security policy for table \"workspaces\"\n    at ensureDefaultWorkspace (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\wolai-frontend_9f844c81._.js:172:15)\n    at async Home (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__97b7743c._.js:126:5) {\n  digest: '2664484505'\n}\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/next-dev.err","s":49,"e":71,"m":1773495424243.6096,"t":"status: 400,\n  code: 'refresh_token_not_found'\n}\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\wolai-frontend_9f844c81._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__97b7743c._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\n ⨯ Error: 创建默认工作空间失败：new row violates row-level security policy for table \"workspaces\"\n    at ensureDefaultWorkspace (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\wolai-frontend_9f844c81._.js:172:15)\n    at async Home (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__97b7743c._.js:126:5) {\n  digest: '2664484505'\n}\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\wolai-frontend_9f844c81._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\nF:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__97b7743c._.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: Error: sourceMapURL could not be parsed\n ⨯ Error: 创建默认工作空间失败：new row violates row-level security policy for table \"workspaces\"\n    at ensureDefaultWorkspace (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\wolai-frontend_9f844c81._.js:172:15)\n    at async Home (F:\\SOFT\\MNOTE\\wolai-frontend\\.next\\dev\\server\\chunks\\ssr\\[root-of-the-server]__97b7743c._.js:126:5) {\n  digest: '2664484505'\n}\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/next-dev.err","s":72,"e":104,"m":1773495424243.6096,"t":"Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\nUsing the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.\n\n<--- Last few GCs --->\n\n[30704:000001C2AAECC000]  3154411 ms: Mark-Compact 31843.8 (32807.5) -> 31742.3 (32803.9) MB, pooled: 0 MB, 10826.74 / 19.88 ms  (average mu = 0.281, current mu = 0.289) task; scavenge might not succeed\n[30704:000001C2AAECC000]  3167016 ms: Mark-Compact 31812.8 (32808.6) -> 31722.8 (32794.1) MB, pooled: 8 MB, 11662.27 / 4.09 ms  (average mu = 0.187, current mu = 0.075) task; scavenge might not succeed\n\nFATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory\n----- Native stack trace -----\n\n 1: 00007FF6DC3D7F8F node::OnFatalError+1343\n 2: 00007FF6DD01B1E7 v8::Function::NewInstance+423\n 3: 00007FF6DCE1BA97 v8::base::AddressSpaceReservation::AddressSpaceReservation+322071\n 4: 00007FF6DCE1F7A4 v8::base::AddressSpaceReservation::AddressSpaceReservation+337700\n 5: 00007FF6DCE2E73C v8::internal::StrongRootAllocatorBase::deallocate_impl+16604\n 6: 00007FF6DCE2DF7B v8::internal::StrongRootAllocatorBase::deallocate_impl+14619\n 7: 00007FF6DE295D2D v8::base::UnsignedDivisionByConstant<unsigned __int64>+2791341\n 8: 00007FF6DCE19560 v8::base::AddressSpaceReservation::AddressSpaceReservation+312544\n 9: 00007FF6DCDC9F12 EVP_PKEY_asn1_set_get_priv_key+86258\n10: 00007FF6DC32A48A node::GetNodeReport+98346\n11: 00007FF6DC328C70 node::GetNodeReport+92176\n12: 00007FF6DD082BCB uv_run+1867\n13: 00007FF6DD08273F uv_run+703\n14: 00007FF6DC4E4E3F node::DecodeWrite+367\n15: 00007FF6DC3659B2 node::MultiIsolatePlatform::DisposeIsolate+240642\n16: 00007FF6DC430F4C node::Start+1052\n17: 00007FF6DD449A82 AES_cbc_encrypt+2546\n18: 00007FF6DE29F434 v8::base::UnsignedDivisionByConstant<unsigned __int64>+2830004\n19: 00007FFEDDDAE8D7 BaseThreadInitThunk+23\n20: 00007FFEDFAAC53C RtlUserThreadStart+44","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/next-dev.out","s":1,"e":46,"m":1773495424255.6096,"t":"> wolai-frontend@0.1.0 dev F:\\SOFT\\MNOTE\\wolai-frontend\n> next dev\n\n   ▲ Next.js 16.0.3 (Turbopack)\n   - Local:         http://localhost:3000\n   - Network:       http://192.168.121.1:3000\n   - Environments: .env.local\n\n ✓ Starting...\n ✓ Ready in 983ms\n ○ Compiling /documents/[id] ...\n GET /login 200 in 2.4s (compile: 1432ms, render: 975ms)\n POST /api/auth/callback 200 in 460ms (compile: 439ms, render: 21ms)\n GET /login 200 in 632ms (compile: 31ms, render: 601ms)\n GET /documents/fe527e35-08c8-45db-8cc5-4e040b40973e 307 in 6.2s (compile: 5.1s, render: 1160ms)\n POST /api/auth/callback 200 in 42ms (compile: 29ms, render: 13ms)\n GET /login 200 in 639ms (compile: 35ms, render: 603ms)\n POST /api/auth/callback 200 in 48ms (compile: 22ms, render: 26ms)\n POST /api/auth/callback 200 in 146ms (compile: 45ms, render: 101ms)\n POST /api/auth/callback 200 in 200ms (compile: 49ms, render: 151ms)\n GET / 200 in 1154ms (compile: 432ms, render: 723ms)\n GET /login 200 in 699ms (compile: 29ms, render: 670ms)\n POST /api/auth/callback 200 in 55ms (compile: 44ms, render: 12ms)\n POST /api/auth/callback 200 in 98ms (compile: 47ms, render: 50ms)\n POST /api/auth/callback 200 in 132ms (compile: 36ms, render: 96ms)\n POST /api/auth/callback 200 in 133ms (compile: 40ms, render: 93ms)\n POST /api/auth/callback 200 in 141ms (compile: 39ms, render: 102ms)\n POST /api/auth/callback 200 in 188ms (compile: 44ms, render: 144ms)\n GET / 200 in 788ms (compile: 52ms, render: 736ms)\n GET /login 200 in 711ms (compile: 20ms, render: 691ms)\n POST /api/auth/callback 200 in 57ms (compile: 37ms, render: 21ms)\n POST /api/auth/callback 200 in 59ms (compile: 41ms, render: 18ms)\n POST /api/auth/callback 200 in 130ms (compile: 22ms, render: 108ms)\n POST /api/auth/callback 200 in 132ms (compile: 33ms, render: 99ms)\n POST /api/auth/callback 200 in 136ms (compile: 34ms, render: 102ms)\n POST /api/auth/callback 200 in 171ms (compile: 37ms, render: 134ms)\n GET / 200 in 900ms (compile: 44ms, render: 856ms)\n GET /documents/6fd575a8-1ca5-41f8-9366-2d8c400efd02 200 in 1754ms (compile: 65ms, render: 1689ms)\n GET /documents/6fd575a8-1ca5-41f8-9366-2d8c400efd02 200 in 1323ms (compile: 61ms, render: 1262ms)\n GET /api/references/backlinks?workspaceId=637d70cb-fbe1-4dc3-9f2c-ff8e099b1225&pageId=6fd575a8-1ca5-41f8-9366-2d8c400efd02 200 in 252ms (compile: 156ms, render: 96ms)\n POST /api/documents/save 200 in 174ms (compile: 134ms, render: 40ms)\n POST /api/documents/stats 200 in 192ms (compile: 156ms, render: 37ms)\n\u001b[?25h\n ELIFECYCLE  Command failed with exit code 134.","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/next-dev.pid","s":1,"e":2,"m":1773495424268.6099,"t":"27684","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/next.config.ts","s":1,"e":60,"m":1778399433063.417,"t":"import type { NextConfig } from \"next\";\nimport fs from \"fs\";\nimport path from \"path\";\n\nfunction loadEnvAll() {\n  // 约定：全局仅使用仓库根目录的 .env.all 作为配置来源（生产优先）。\n  // 说明：这里仅“补齐缺失项”，避免覆盖系统环境变量（方便部署/容器注入）。\n  const envAllPath = path.resolve(__dirname, \"..\", \".env.all\");\n  if (!fs.existsSync(envAllPath)) return;\n  const raw = fs.readFileSync(envAllPath, { encoding: \"utf8\" });\n  for (const line of raw.split(/\\r?\\n/)) {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"#\")) continue;\n    const idx = trimmed.indexOf(\"=\");\n    if (idx === -1) continue;\n    const key = trimmed.slice(0, idx).trim();\n    const value = trimmed.slice(idx + 1).trim();\n    if (!key) continue;\n    if (process.env[key] === undefined) {\n      process.env[key] = value;\n    }\n  }\n}\n\nloadEnvAll();\n\nconst nextConfig: NextConfig = {\n  // 说明：3000 Rust 网关会把 /_next/* dev 资源代理到 3100，浏览器 Origin 仍是 127.0.0.1:3000。\n  // Next dev 默认会拦截这类跨 origin HMR 请求，导致客户端 hydration 不执行。\n  allowedDevOrigins: [\"127.0.0.1\", \"localhost\"],\n  // 供桌面端打包使用（Electron 内置 Next server.js + 最小依赖）。\n  // 说明：`pnpm run build:desktop:next` 会依赖该产物。\n  output: \"standalone\",\n  turbopack: {\n    // 避免 monorepo/多 lockfile 场景下 root 误判，减少构建与热更新的不确定性\n    root: __dirname,\n  },\n  async headers() {\n    // 说明：ONLYOFFICE 文档服务器会在其 iframe 内跨域拉取插件 manifest/js/html。\n    // 这里对插件资源放开 CORS，避免 pluginsData 加载失败。\n    return [\n      // 说明：远程通过 Cloudflare Tunnel 访问时，Next.js（Turbopack dev）默认对\n      // `/_next/static/chunks/*` 返回 `no-store`，导致 Cloudflare 无法缓存大体积\n      // bundle，网络稍慢时会出现页面长期停留在“正在载入编辑器...”的现象。\n      // 这些资源带 hash 文件名，设置 immutable 不会导致更新不一致。\n      {\n        source: \"/_next/static/chunks/:path*\",\n        headers: [{ key: \"Cache-Control\", value: \"public, max-age=31536000, immutable\" }],\n      },\n      {\n        source: \"/_next/static/media/:path*\",\n        headers: [{ key: \"Cache-Control\", value: \"public, max-age=31536000, immutable\" }],\n      },\n      // Luckysheet 静态资源：文件位于 public/luckysheet，路径稳定（并且我们在 URL 上追加了版本参数），可长期缓存。\n      {\n        source: \"/luckysheet/:path*\",\n        headers: [{ key: \"Cache-Control\", value: \"public, max-age=31536000, immutable\" }],\n      },\n      {\n        source: \"/onlyoffice/plugins/:path*\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/next.config.ts","s":49,"e":72,"m":1778399433063.417,"t":"},\n      {\n        source: \"/_next/static/media/:path*\",\n        headers: [{ key: \"Cache-Control\", value: \"public, max-age=31536000, immutable\" }],\n      },\n      // Luckysheet 静态资源：文件位于 public/luckysheet，路径稳定（并且我们在 URL 上追加了版本参数），可长期缓存。\n      {\n        source: \"/luckysheet/:path*\",\n        headers: [{ key: \"Cache-Control\", value: \"public, max-age=31536000, immutable\" }],\n      },\n      {\n        source: \"/onlyoffice/plugins/:path*\",\n        headers: [\n          { key: \"Access-Control-Allow-Origin\", value: \"*\" },\n          { key: \"Access-Control-Allow-Methods\", value: \"GET,OPTIONS\" },\n          { key: \"Access-Control-Allow-Headers\", value: \"*\" },\n        ],\n      },\n    ];\n  },\n};\n\nexport default nextConfig;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/package.json","s":1,"e":60,"m":1778032105652.3577,"t":"{\n  \"name\": \"wolai-frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"node scripts/dev-server.js\",\n    \"build\": \"next build\",\n    \"start\": \"node scripts/prod-server.js\",\n    \"start:next\": \"next start\",\n    \"lint\": \"eslint\",\n    \"test\": \"vitest\"\n  },\n  \"dependencies\": {\n    \"@auth/core\": \"0.37.0\",\n    \"@blocknote/core\": \"^0.42.0\",\n    \"@blocknote/mantine\": \"0.42.0\",\n    \"@blocknote/react\": \"^0.42.0\",\n    \"@convex-dev/auth\": \"^0.0.91\",\n    \"@dnd-kit/core\": \"^6.3.1\",\n    \"@dnd-kit/sortable\": \"^10.0.0\",\n    \"@dnd-kit/utilities\": \"^3.2.2\",\n    \"@hello-pangea/dnd\": \"^18.0.1\",\n    \"@hocuspocus/provider\": \"^2.15.3\",\n    \"@radix-ui/react-avatar\": \"^1.1.11\",\n    \"@radix-ui/react-dialog\": \"^1.1.15\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.16\",\n    \"@radix-ui/react-hover-card\": \"^1.1.15\",\n    \"@radix-ui/react-label\": \"^2.1.8\",\n    \"@radix-ui/react-popover\": \"^1.1.15\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.10\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-tabs\": \"^1.1.13\",\n    \"@radix-ui/react-toggle\": \"^1.1.10\",\n    \"@supabase/auth-helpers-nextjs\": \"^0.10.0\",\n    \"@supabase/auth-helpers-react\": \"^0.5.0\",\n    \"@supabase/supabase-js\": \"^2.81.1\",\n    \"@svgdotjs/svg.js\": \"^3.2.5\",\n    \"@tanstack/react-query\": \"^5.90.10\",\n    \"@tanstack/react-query-devtools\": \"^5.90.2\",\n    \"@tanstack/react-virtual\": \"^3.11.0\",\n    \"@xyflow/react\": \"^12.9.3\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"cmdk\": \"^1.1.1\",\n    \"convex\": \"^1.35.1\",\n    \"jszip\": \"3.10.1\",\n    \"lucide-react\": \"^0.554.0\",\n    \"next\": \"16.2.4\",\n    \"pdfjs-dist\": \"^4.10.38\",\n    \"react\": \"19.2.0\",\n    \"react-dom\": \"19.2.0\",\n    \"react-dropzone\": \"^14.3.8\",\n    \"react-icons\": \"5.5.0\",\n    \"simple-mind-map\": \"0.14.0-fix.1\",\n    \"simple-mind-map-plugin-themes\": \"^1.0.1\",\n    \"sonner\": \"^2.0.7\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"uuid\": \"^13.0.0\",\n    \"vaul\": \"^1.1.2\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/package.json","s":49,"e":81,"m":1778032105652.3577,"t":"\"next\": \"16.2.4\",\n    \"pdfjs-dist\": \"^4.10.38\",\n    \"react\": \"19.2.0\",\n    \"react-dom\": \"19.2.0\",\n    \"react-dropzone\": \"^14.3.8\",\n    \"react-icons\": \"5.5.0\",\n    \"simple-mind-map\": \"0.14.0-fix.1\",\n    \"simple-mind-map-plugin-themes\": \"^1.0.1\",\n    \"sonner\": \"^2.0.7\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"uuid\": \"^13.0.0\",\n    \"vaul\": \"^1.1.2\",\n    \"y-protocols\": \"^1.0.6\",\n    \"yjs\": \"^13.6.27\",\n    \"zustand\": \"^5.0.8\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/next-on-pages\": \"^1.13.16\",\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"16.2.4\",\n    \"jose\": \"^6.1.3\",\n    \"jsdom\": \"^27.2.0\",\n    \"tailwindcss\": \"^4\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5\",\n    \"vitest\": \"^4.0.13\"\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/postcss.config.mjs","s":1,"e":8,"m":1773495424316.6104,"t":"const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/start-4000.out","s":1,"e":12,"m":1773495424334.6106,"t":"> wolai-frontend@0.1.0 start F:\\SOFT\\MNOTE\\wolai-frontend\n> next start \"--port\" \"4000\"\n\n   ▲ Next.js 16.0.3\n   - Local:         http://localhost:4000\n   - Network:       http://192.168.121.1:4000\n\n ✓ Starting...\n ✓ Ready in 785ms\n ELIFECYCLE  Command failed with exit code 4294967295.","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/tsconfig.json","s":1,"e":47,"m":1773495424357.6108,"t":"{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"types\": [\n      \"vitest/globals\"\n    ],\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\",\n    \"**/*.mts\",\n    \".next/dev/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/vitest.config.ts","s":1,"e":18,"m":1776442821892.3726,"t":"import { defineConfig } from \"vitest/config\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, resolve } from \"node:path\";\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@\": resolve(dirname(fileURLToPath(import.meta.url)), \"./src\"),\n      \"server-only\": resolve(dirname(fileURLToPath(import.meta.url)), \"./test/server-only.ts\"),\n    },\n  },\n  test: {\n    environment: \"jsdom\",\n    globals: true,\n    include: [\"src/**/*.test.ts\", \"src/**/*.test.tsx\"],\n  },\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/proxy.ts","s":1,"e":55,"m":1778395785622.2131,"t":"import { convexAuthNextjsMiddleware, createRouteMatcher, nextjsMiddlewareRedirect } from \"@convex-dev/auth/nextjs/server\";\nimport { isDevAuthEnabled } from \"@/lib/auth/devUser\";\n\n// 说明：\n// - 这里启用 Convex Auth 的 Next.js 中间件，负责：\n//   1) 代理 /api/auth 到 Convex（用于 signIn / signOut 等动作）\n//   2) 刷新/同步 auth cookies\n//   3) 对需要登录的页面做统一拦截\n// - 默认将未登录用户重定向到 /auth（Convex Auth 登录页）。\n\nconst isPublicRoute = createRouteMatcher([\n  \"/auth\",\n  \"/login\",\n  \"/api/auth(.*)\",\n  // 说明：ONLYOFFICE 文档服务器（容器/远端）拉取文件与回调保存不携带用户态，必须放行。\n  \"/api/onlyoffice/proxy(.*)\",\n  \"/api/onlyoffice/callback(.*)\",\n  // 说明：媒体附件 API 作为 Rust 3000 的 TS transport 壳保留；\n  // 先放过 Next middleware，真实鉴权由各 route 内部 requireAuthContext 执行。\n  \"/api/media(.*)\",\n  // 说明：/onlyoffice-server 与 /cache 主要承载 ONLYOFFICE 静态资源与二进制缓存。\n  // 这些资源不依赖用户态，且需要浏览器强缓存；若经过 Auth middleware 可能被追加 no-store，导致每次都重下几十 MB。\n  \"/onlyoffice-server(.*)\",\n  // 说明：本地自定义 ONLYOFFICE 插件页面运行在同源 /onlyoffice/plugins/* 下，由文档编辑器 iframe 直接加载。\n  // 若被鉴权重定向到 /auth，会导致插件桥永远收不到 ready。\n  \"/onlyoffice/plugins(.*)\",\n  \"/cache(.*)\",\n  \"/api/health(.*)\",\n  \"/_next(.*)\",\n  \"/favicon.ico\",\n]);\n\nexport default convexAuthNextjsMiddleware(async (request, ctx) => {\n  // 公共路由不做拦截（但 proxy 仍会处理 token 刷新/代理等）。\n  if (isPublicRoute(request)) return;\n\n  // 只在启用 Convex 模式时做鉴权拦截；否则走 Supabase 逻辑（页面内部自行处理）。\n  // 注意：proxy 运行在 Edge/Node 环境中，读取到的是运行期环境变量。\n  if (process.env.NEXT_PUBLIC_USE_CONVEX !== \"1\") return;\n\n  // 开发用户模式下允许“免登录”访问（主要用于迁移/联调与 E2E 回归）。\n  // 注意：生产环境请勿开启 MNOTE_DEV_AUTH。\n  if (isDevAuthEnabled()) return;\n\n  const authed = await ctx.convexAuth.isAuthenticated();\n  if (!authed) {\n    return nextjsMiddlewareRedirect(request, \"/auth\");\n  }\n});\n\nexport const config = {\n  // 说明：排除静态资源，避免无意义的 proxy 开销。\n  matcher: [\"/((?!_next/static|_next/image|favicon.ico).*)\"],\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/document.ts","s":1,"e":10,"m":1773495684481.4768,"t":"import type { DocumentStats } from \"@/types/page-options\";\nimport type { Json } from \"@/types/supabase\";\n\nexport interface DocumentSnapshot {\n  id: string;\n  timestamp: number;\n  blocks: Json;\n  stats: DocumentStats;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/jsdom.d.ts","s":1,"e":9,"m":1773495684487.4768,"t":"declare module \"jsdom\" {\n  export class JSDOM {\n    constructor(html?: string, options?: unknown);\n    // 说明：仅用于服务端导入时的最小类型声明（用于 window/document/DOMParser 等 DOM shim）。\n    // 若需更完整的类型能力，可改为使用 jsdom 自带的类型定义。\n    window: Window & typeof globalThis;\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/media.ts","s":1,"e":37,"m":1773495684493.477,"t":"export interface MediaAsset {\n  id: string;\n  workspace_id: string;\n  document_id: string;\n  asset_type: string;\n  file_url: string | null;\n  thumbnail_url: string | null;\n  storage_id?: string | null;\n  bucket?: string | null;\n  storage_path?: string | null;\n  file_name: string | null;\n  file_size: number | null;\n  mime_type: string | null;\n  ocr_payload?: unknown;\n  ocr_strategy?: string | null;\n  ocr_text: string | null;\n  ocr_status: string | null;\n  deleted_at?: string | null;\n  deleted_by?: string | null;\n  purged_at?: string | null;\n  signed_url?: string | null;\n  created_at: string;\n  updated_at: string;\n}\n\nexport interface MediaSelection {\n  assetId: string;\n  fileUrl: string;\n  thumbnailUrl?: string | null;\n  assetType?: string;\n  fileName?: string | null;\n  fileSize?: number | null;\n  mimeType?: string | null;\n}\n\nexport type MediaKind = \"image\" | \"video\" | \"audio\" | \"file\";","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/online-table.ts","s":1,"e":60,"m":1773495684499.477,"t":"export type ColumnType =\n  | \"text\"\n  | \"number\"\n  | \"currency\"\n  | \"date\"\n  | \"select\"\n  | \"checkbox\"\n  | \"formula\"\n  | \"reference\";\n\nexport interface TableColumn {\n  id: string; // Unique column ID\n  name: string; // Column display name\n  type: ColumnType;\n  width: number; // Column width for display\n  options?: { value: string; color: string }[]; // For 'select' type\n}\n\nexport interface TableSchema {\n  columns: TableColumn[];\n  frozenRowCount: number; // 冻结行数\n  frozenColCount: number; // 冻结列数\n  // 其他视图偏好设置...\n}\n\nexport type TableRowData = Record<string, string | number | boolean | null | undefined>;\n\nexport interface DocumentTableSnapshot {\n  rows?: TableRowData[];\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  luckysheet?: any;\n}\n\n// 对应 Supabase document_tables 表的元数据\nexport interface DocumentTable {\n  id: string;\n  document_id: string;\n  grid_key: string;\n  title: string;\n  schema: TableSchema;\n  snapshot?: DocumentTableSnapshot | null;\n  created_at: string;\n  updated_at: string;\n}\n\n// Blocknote 块的数据结构\nexport interface OnlineTableBlockProps {\n  tableId: string;\n  title: string; // 允许 Blocknote 渲染时显示表格名称\n}\n\n// Global declarations for Luckysheet (minimal definition)\ndeclare global {\n  interface Window {\n    luckysheet?: {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      create?: (options: any) => void;\n      destroy?: (id: string) => void;\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      getluckysheetfile?: () => any;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/online-table.ts","s":49,"e":68,"m":1773495684499.477,"t":"title: string; // 允许 Blocknote 渲染时显示表格名称\n}\n\n// Global declarations for Luckysheet (minimal definition)\ndeclare global {\n  interface Window {\n    luckysheet?: {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      create?: (options: any) => void;\n      destroy?: (id: string) => void;\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      getluckysheetfile?: () => any;\n      enterEditMode?: () => void;\n      getluckysheet_select_save?: () => unknown;\n      // 实际功能会通过动态加载的脚本注入\n      // 最小化声明以通过 TypeScript 检查（必须允许 undefined）\n    };\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/page-options.ts","s":1,"e":39,"m":1773495684505.477,"t":"export type PageFont = \"default\" | \"song\" | \"kai\";\n\nexport type PageLayoutDensity = \"compact\" | \"normal\" | \"spacious\";\n\nexport type BooleanPageOptionKey =\n  | \"wideLayout\"\n  | \"smallText\"\n  | \"showHeadingNumbers\"\n  | \"showToc\"\n  | \"protectEditing\"\n  | \"showWordCount\"\n  | \"collapseBacklinks\"\n  | \"hideChildPages\"\n  | \"showBlockRefCount\";\n\nexport interface PageOptionsState {\n  wideLayout: boolean;\n  smallText: boolean;\n  showHeadingNumbers: boolean;\n  showToc: boolean;\n  showStructure: boolean;\n  protectEditing: boolean;\n  showWordCount: boolean;\n  collapseBacklinks: boolean;\n  pageFont: PageFont;\n  layoutDensity: PageLayoutDensity;\n  hideChildPages: boolean;\n  showBlockRefCount: boolean;\n  embedDefaultBlockId: string | null;\n}\n\nexport interface DocumentStats {\n  wordCount: number;\n  characterCount: number;\n  blockCount: number;\n  todoTotal: number;\n  todoDone: number;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/references.ts","s":1,"e":14,"m":1773495684511.4773,"t":"import type { ReferenceInsertMode } from \"@/store/search-palette\";\n\nexport interface BacklinkRecord {\n  id: string;\n  sourcePageId: string;\n  sourceBlockId: string | null;\n  alias: string | null;\n  displayMode: ReferenceInsertMode | string;\n  isPreviewable: boolean;\n  createdAt: string;\n  updatedAt: string;\n  sourceTitle: string | null;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/search.ts","s":1,"e":57,"m":1777473555768.3516,"t":"export type DocumentSearchMatchField = \"title\" | \"content\" | \"recent\";\n\nexport type DocumentSearchTimeRange = \"any\" | \"7d\" | \"30d\";\nexport type DocumentSearchTimeField = \"updated\" | \"created\";\n\nexport interface DocumentSearchFilters {\n  titleOnly: boolean;\n  exact: boolean;\n  onlyCurrentPage: boolean;\n  includeOcr: boolean;\n  timeRange: DocumentSearchTimeRange;\n  timeField: DocumentSearchTimeField;\n  customRange?: {\n    from?: string;\n    to?: string;\n  };\n}\n\nexport interface DocumentSearchRequest {\n  workspaceId: string;\n  documentId?: string;\n  query?: string;\n  filters: DocumentSearchFilters;\n  limit?: number;\n}\n\nexport interface DocumentSearchResult {\n  id: string;\n  title: string | null;\n  snippet: string;\n  updatedAt: string | null;\n  createdAt: string | null;\n  matchField: DocumentSearchMatchField;\n  hasOcr: boolean;\n  publicPath: string;\n  score: number;\n  nodeId?: string | null;\n  subtreeRootId?: string | null;\n  projectionOwner?: \"rust-kernel\" | \"compat-index\";\n  evidence?: Array<{\n    kind: string;\n    nodeId?: string | null;\n    snippet: string;\n  }>;\n}\n\nexport interface DocumentSearchResponse {\n  projectionOwner?: \"rust-kernel\" | \"compat-index\";\n  results: DocumentSearchResult[];\n  recent: DocumentSearchResult[];\n}\n\nexport interface ReferenceTarget {\n  id: string;\n  title: string | null;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/simple-mind-map-internals.d.ts","s":1,"e":60,"m":1778397486166.5374,"t":"// 说明：\n// `simple-mind-map` 的 `src/*` 属于内部实现路径，npm 包通常不提供 TypeScript 声明文件。\n// 但在项目中我们需要按官方示例从 `src/plugins/*` 等路径引入插件，因此在此补充最小声明，\n// 仅用于消除 TS7016（找不到声明文件）报错。\n//\n// 注意：这里的类型刻意保持宽松（unknown），避免把内部 API 当作稳定公共接口依赖。\n\ndeclare module \"simple-mind-map/src/svg/icons.js\" {\n  export const nodeIconList: unknown[];\n}\n\ndeclare module \"simple-mind-map/src/utils/index.js\" {\n  export const mergerIconList: (icons: unknown[]) => unknown[];\n  export const createUid: () => string;\n}\n\ndeclare module \"simple-mind-map/src/plugins/Painter.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/AssociativeLine.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Search.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/OuterFrame.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Export.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Formula.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/RichText.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/MiniMap.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Select.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Drag.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/KeyboardNavigation.js\" {\n  const plugin: unknown;\n  export default plugin;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/simple-mind-map-internals.d.ts","s":49,"e":108,"m":1778397486166.5374,"t":"declare module \"simple-mind-map/src/plugins/Select.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Drag.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/KeyboardNavigation.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/NodeImgAdjust.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Scrollbar.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/RainbowLines.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Watermark.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/TouchEvent.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Cooperate.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/Demonstrate.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/MindMapLayoutPro.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/NodeBase64ImageStorage.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/ExportPDF.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/ExportXMind.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\n\ndeclare module \"simple-mind-map/src/core/render/node/MindMapNode.js\" {\n  const MindMapNode: unknown;\n  export default MindMapNode;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/simple-mind-map-internals.d.ts","s":97,"e":129,"m":1778397486166.5374,"t":"declare module \"simple-mind-map/src/plugins/ExportPDF.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\ndeclare module \"simple-mind-map/src/plugins/ExportXMind.js\" {\n  const plugin: unknown;\n  export default plugin;\n}\n\ndeclare module \"simple-mind-map/src/core/render/node/MindMapNode.js\" {\n  const MindMapNode: unknown;\n  export default MindMapNode;\n}\n\ndeclare module \"simple-mind-map/src/parse/xmind.js\" {\n  const xmind: {\n    parseXmindFile: (\n      file: Blob | ArrayBuffer | Uint8Array,\n      handleMultiCanvas?: (content: unknown[]) => unknown,\n    ) => Promise<unknown>;\n  };\n  export default xmind;\n}\n\ndeclare module \"simple-mind-map/src/parse/markdownTo.js\" {\n  export type MarkdownToMindmapResult = {\n    data?: Record<string, unknown>;\n    children?: unknown[];\n    [key: string]: unknown;\n  };\n  export const transformMarkdownTo: (md: string) => MarkdownToMindmapResult;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":1,"e":60,"m":1773495684530.4773,"t":"export type Json =\n  | string\n  | number\n  | boolean\n  | null\n  | { [key: string]: Json | undefined }\n  | Json[];\n\nexport type Database = {\n  public: {\n    Tables: {\n      documents: {\n        Row: {\n          access_scope: \"private\" | \"shared\" | \"public\";\n          content: Json | null;\n          created_at: string | null;\n          deleted_at: string | null;\n          deleted_by: string | null;\n          id: string;\n          index_status: string | null;\n          is_public: boolean | null;\n          is_starred: boolean | null;\n          is_template: boolean;\n          mindmap_data: Json | null;\n          parent_id: string | null;\n          raw_text: string | null;\n          sort_order: number;\n          title: string | null;\n          updated_at: string | null;\n          user_id: string;\n          workspace_id: string;\n          wide_layout: boolean;\n          use_small_text: boolean;\n          show_heading_numbers: boolean;\n          show_toc: boolean;\n          show_structure: boolean;\n          protect_editing: boolean;\n          show_word_count: boolean;\n          word_count: number;\n          character_count: number;\n          block_count: number;\n        };\n        Insert: {\n          access_scope?: \"private\" | \"shared\" | \"public\";\n          content?: Json | null;\n          created_at?: string | null;\n          deleted_at?: string | null;\n          deleted_by?: string | null;\n          id?: string;\n          index_status?: string | null;\n          is_public?: boolean | null;\n          is_starred?: boolean | null;\n          is_template?: boolean;\n          mindmap_data?: Json | null;\n          parent_id?: string | null;\n          raw_text?: string | null;\n          sort_order?: number;\n          title?: string | null;\n          updated_at?: string | null;\n          user_id: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":49,"e":108,"m":1773495684530.4773,"t":"id?: string;\n          index_status?: string | null;\n          is_public?: boolean | null;\n          is_starred?: boolean | null;\n          is_template?: boolean;\n          mindmap_data?: Json | null;\n          parent_id?: string | null;\n          raw_text?: string | null;\n          sort_order?: number;\n          title?: string | null;\n          updated_at?: string | null;\n          user_id: string;\n          workspace_id: string;\n          wide_layout?: boolean;\n          use_small_text?: boolean;\n          show_heading_numbers?: boolean;\n          show_toc?: boolean;\n          show_structure?: boolean;\n          protect_editing?: boolean;\n          show_word_count?: boolean;\n          word_count?: number;\n          character_count?: number;\n          block_count?: number;\n        };\n        Update: {\n          access_scope?: \"private\" | \"shared\" | \"public\";\n          content?: Json | null;\n          created_at?: string | null;\n          deleted_at?: string | null;\n          deleted_by?: string | null;\n          id?: string;\n          index_status?: string | null;\n          is_public?: boolean | null;\n          is_starred?: boolean | null;\n          is_template?: boolean;\n          mindmap_data?: Json | null;\n          parent_id?: string | null;\n          raw_text?: string | null;\n          sort_order?: number;\n          title?: string | null;\n          updated_at?: string | null;\n          user_id?: string;\n          workspace_id?: string;\n          wide_layout?: boolean;\n          use_small_text?: boolean;\n          show_heading_numbers?: boolean;\n          show_toc?: boolean;\n          show_structure?: boolean;\n          protect_editing?: boolean;\n          show_word_count?: boolean;\n          word_count?: number;\n          character_count?: number;\n          block_count?: number;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"documents_parent_id_fkey\";\n            columns: [\"parent_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":97,"e":156,"m":1773495684530.4773,"t":"protect_editing?: boolean;\n          show_word_count?: boolean;\n          word_count?: number;\n          character_count?: number;\n          block_count?: number;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"documents_parent_id_fkey\";\n            columns: [\"parent_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"documents_workspace_id_fkey\";\n            columns: [\"workspace_id\"];\n            referencedRelation: \"workspaces\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"documents_deleted_by_fkey\";\n            columns: [\"deleted_by\"];\n            referencedRelation: \"profiles\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      document_embeddings: {\n        Row: {\n          content_text: string;\n          document_id: string | null;\n          embedding: string | null;\n          id: string;\n        };\n        Insert: {\n          content_text: string;\n          document_id?: string | null;\n          embedding?: string | null;\n          id?: string;\n        };\n        Update: {\n          content_text?: string;\n          document_id?: string | null;\n          embedding?: string | null;\n          id?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"document_embeddings_document_id_fkey\";\n            columns: [\"document_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      entities: {\n        Row: {\n          document_ids: string[] | null;\n          id: string;\n          name: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":145,"e":204,"m":1773495684530.4773,"t":"foreignKeyName: \"document_embeddings_document_id_fkey\";\n            columns: [\"document_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      entities: {\n        Row: {\n          document_ids: string[] | null;\n          id: string;\n          name: string;\n          properties: Json | null;\n          type: string | null;\n          user_id: string;\n        };\n        Insert: {\n          document_ids?: string[] | null;\n          id?: string;\n          name: string;\n          properties?: Json | null;\n          type?: string | null;\n          user_id: string;\n        };\n        Update: {\n          document_ids?: string[] | null;\n          id?: string;\n          name?: string;\n          properties?: Json | null;\n          type?: string | null;\n          user_id?: string;\n        };\n        Relationships: [];\n      };\n      relationships: {\n        Row: {\n          document_ids: string[] | null;\n          from_entity: string | null;\n          id: string;\n          to_entity: string | null;\n          type: string | null;\n        };\n        Insert: {\n          document_ids?: string[] | null;\n          from_entity?: string | null;\n          id?: string;\n          to_entity?: string | null;\n          type?: string | null;\n        };\n        Update: {\n          document_ids?: string[] | null;\n          from_entity?: string | null;\n          id?: string;\n          to_entity?: string | null;\n          type?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"relationships_from_entity_fkey\";\n            columns: [\"from_entity\"];","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":193,"e":252,"m":1773495684530.4773,"t":"};\n        Update: {\n          document_ids?: string[] | null;\n          from_entity?: string | null;\n          id?: string;\n          to_entity?: string | null;\n          type?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"relationships_from_entity_fkey\";\n            columns: [\"from_entity\"];\n            referencedRelation: \"entities\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"relationships_to_entity_fkey\";\n            columns: [\"to_entity\"];\n            referencedRelation: \"entities\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      page_refs: {\n        Row: {\n          id: string;\n          workspace_id: string;\n          source_page_id: string;\n          source_block_id: string | null;\n          target_page_id: string;\n          alias: string | null;\n          display_mode: string;\n          is_previewable: boolean;\n          created_by: string;\n          created_at: string;\n          updated_at: string;\n        };\n        Insert: {\n          id?: string;\n          workspace_id: string;\n          source_page_id: string;\n          source_block_id?: string | null;\n          target_page_id: string;\n          alias?: string | null;\n          display_mode?: string;\n          is_previewable?: boolean;\n          created_by: string;\n          created_at?: string;\n          updated_at?: string;\n        };\n        Update: {\n          id?: string;\n          workspace_id?: string;\n          source_page_id?: string;\n          source_block_id?: string | null;\n          target_page_id?: string;\n          alias?: string | null;\n          display_mode?: string;\n          is_previewable?: boolean;\n          created_by?: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":241,"e":300,"m":1773495684530.4773,"t":"updated_at?: string;\n        };\n        Update: {\n          id?: string;\n          workspace_id?: string;\n          source_page_id?: string;\n          source_block_id?: string | null;\n          target_page_id?: string;\n          alias?: string | null;\n          display_mode?: string;\n          is_previewable?: boolean;\n          created_by?: string;\n          created_at?: string;\n          updated_at?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"page_refs_workspace_id_fkey\";\n            columns: [\"workspace_id\"];\n            referencedRelation: \"workspaces\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"page_refs_source_page_id_fkey\";\n            columns: [\"source_page_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"page_refs_target_page_id_fkey\";\n            columns: [\"target_page_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"page_refs_created_by_fkey\";\n            columns: [\"created_by\"];\n          referencedRelation: \"profiles\";\n          referencedColumns: [\"id\"];\n        },\n      ];\n      };\n      media_assets: {\n        Row: {\n          id: string;\n          workspace_id: string;\n          document_id: string;\n          asset_type: string;\n          file_url: string | null;\n          thumbnail_url: string | null;\n          bucket: string | null;\n          storage_path: string | null;\n          file_name: string | null;\n          file_size: number | null;\n          mime_type: string | null;\n          ocr_payload: Json | null;\n          ocr_strategy: string | null;\n          ocr_text: string | null;\n          ocr_status: string | null;\n          deleted_at: string | null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":289,"e":348,"m":1773495684530.4773,"t":"file_url: string | null;\n          thumbnail_url: string | null;\n          bucket: string | null;\n          storage_path: string | null;\n          file_name: string | null;\n          file_size: number | null;\n          mime_type: string | null;\n          ocr_payload: Json | null;\n          ocr_strategy: string | null;\n          ocr_text: string | null;\n          ocr_status: string | null;\n          deleted_at: string | null;\n          deleted_by: string | null;\n          purged_at: string | null;\n          created_by: string | null;\n          created_at: string;\n          updated_at: string;\n        };\n        Insert: {\n          id?: string;\n          workspace_id: string;\n          document_id: string;\n          asset_type?: string;\n          file_url?: string | null;\n          thumbnail_url?: string | null;\n          bucket?: string | null;\n          storage_path?: string | null;\n          file_name?: string | null;\n          file_size?: number | null;\n          mime_type?: string | null;\n          ocr_payload?: Json | null;\n          ocr_strategy?: string | null;\n          ocr_text?: string | null;\n          ocr_status?: string | null;\n          deleted_at?: string | null;\n          deleted_by?: string | null;\n          purged_at?: string | null;\n          created_by?: string | null;\n          created_at?: string;\n          updated_at?: string;\n        };\n        Update: {\n          id?: string;\n          workspace_id?: string;\n          document_id?: string;\n          asset_type?: string;\n          file_url?: string | null;\n          thumbnail_url?: string | null;\n          bucket?: string | null;\n          storage_path?: string | null;\n          file_name?: string | null;\n          file_size?: number | null;\n          mime_type?: string | null;\n          ocr_payload?: Json | null;\n          ocr_strategy?: string | null;\n          ocr_text?: string | null;\n          ocr_status?: string | null;\n          deleted_at?: string | null;\n          deleted_by?: string | null;\n          purged_at?: string | null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":337,"e":396,"m":1773495684530.4773,"t":"bucket?: string | null;\n          storage_path?: string | null;\n          file_name?: string | null;\n          file_size?: number | null;\n          mime_type?: string | null;\n          ocr_payload?: Json | null;\n          ocr_strategy?: string | null;\n          ocr_text?: string | null;\n          ocr_status?: string | null;\n          deleted_at?: string | null;\n          deleted_by?: string | null;\n          purged_at?: string | null;\n          created_by?: string | null;\n          created_at?: string;\n          updated_at?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"media_assets_workspace_id_fkey\";\n            columns: [\"workspace_id\"];\n            referencedRelation: \"workspaces\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"media_assets_document_id_fkey\";\n            columns: [\"document_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"media_assets_created_by_fkey\";\n            columns: [\"created_by\"];\n            referencedRelation: \"profiles\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"media_assets_deleted_by_fkey\";\n            columns: [\"deleted_by\"];\n            referencedRelation: \"profiles\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      user_recent_pages: {\n        Row: {\n          id: string;\n          user_id: string;\n          workspace_id: string;\n          document_id: string;\n          last_accessed_at: string;\n        };\n        Insert: {\n          id?: string;\n          user_id: string;\n          workspace_id: string;\n          document_id: string;\n          last_accessed_at?: string;\n        };\n        Update: {\n          id?: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":385,"e":444,"m":1773495684530.4773,"t":"document_id: string;\n          last_accessed_at: string;\n        };\n        Insert: {\n          id?: string;\n          user_id: string;\n          workspace_id: string;\n          document_id: string;\n          last_accessed_at?: string;\n        };\n        Update: {\n          id?: string;\n          user_id?: string;\n          workspace_id?: string;\n          document_id?: string;\n          last_accessed_at?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"user_recent_pages_document_id_fkey\";\n            columns: [\"document_id\"];\n            referencedRelation: \"documents\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"user_recent_pages_user_id_fkey\";\n            columns: [\"user_id\"];\n            referencedRelation: \"profiles\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"user_recent_pages_workspace_id_fkey\";\n            columns: [\"workspace_id\"];\n            referencedRelation: \"workspaces\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      [key: string]: any;\n    };\n    Views: {\n      [_ in never]: never;\n    };\n    Functions: {\n      get_documents_tree: {\n        Args: {\n          request_user_id: string;\n        };\n        Returns: {\n          depth: number;\n          id: string;\n          parent_id: string | null;\n          path: string[];\n          sort_order: number;\n          title: string | null;\n        }[];\n      };\n      workspaces: {\n        Row: {\n          created_at: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":433,"e":492,"m":1773495684530.4773,"t":"Returns: {\n          depth: number;\n          id: string;\n          parent_id: string | null;\n          path: string[];\n          sort_order: number;\n          title: string | null;\n        }[];\n      };\n      workspaces: {\n        Row: {\n          created_at: string;\n          created_by: string;\n          icon_url: string | null;\n          id: string;\n          name: string;\n          type: \"personal\" | \"team\";\n          updated_at: string;\n        };\n        Insert: {\n          created_at?: string;\n          created_by: string;\n          icon_url?: string | null;\n          id?: string;\n          name: string;\n          type?: \"personal\" | \"team\";\n          updated_at?: string;\n        };\n        Update: {\n          created_at?: string;\n          created_by?: string;\n          icon_url?: string | null;\n          id?: string;\n          name?: string;\n          type?: \"personal\" | \"team\";\n          updated_at?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"workspaces_created_by_fkey\";\n            columns: [\"created_by\"];\n            referencedRelation: \"profiles\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      workspace_members: {\n        Row: {\n          created_at: string;\n          id: string;\n          is_default: boolean;\n          role: string;\n          user_id: string;\n          workspace_id: string;\n        };\n        Insert: {\n          created_at?: string;\n          id?: string;\n          is_default?: boolean;\n          role?: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":481,"e":540,"m":1773495684530.4773,"t":"created_at: string;\n          id: string;\n          is_default: boolean;\n          role: string;\n          user_id: string;\n          workspace_id: string;\n        };\n        Insert: {\n          created_at?: string;\n          id?: string;\n          is_default?: boolean;\n          role?: string;\n          user_id: string;\n          workspace_id: string;\n        };\n        Update: {\n          created_at?: string;\n          id?: string;\n          is_default?: boolean;\n          role?: string;\n          user_id?: string;\n          workspace_id?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"workspace_members_user_id_fkey\";\n            columns: [\"user_id\"];\n            referencedRelation: \"profiles\";\n            referencedColumns: [\"id\"];\n          },\n          {\n          foreignKeyName: \"workspace_members_workspace_id_fkey\";\n          columns: [\"workspace_id\"];\n          referencedRelation: \"workspaces\";\n          referencedColumns: [\"id\"];\n        },\n      ];\n    };\n      record_page_ref: {\n        Args: {\n          p_workspace_id: string;\n          p_source_page_id: string;\n          p_source_block_id?: string | null;\n          p_target_page_id: string;\n          p_alias?: string | null;\n          p_display_mode?: string;\n          p_is_previewable?: boolean;\n          p_created_by?: string;\n        };\n        Returns: Database[\"public\"][\"Tables\"][\"page_refs\"][\"Row\"];\n      };\n      list_backlinks: {\n        Args: {\n          p_workspace_id: string;\n          p_target_page_id: string;\n          p_limit?: number;\n          p_offset?: number;\n        };\n        Returns: Array<{\n          id: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/supabase.ts","s":529,"e":561,"m":1773495684530.4773,"t":"};\n        Returns: Database[\"public\"][\"Tables\"][\"page_refs\"][\"Row\"];\n      };\n      list_backlinks: {\n        Args: {\n          p_workspace_id: string;\n          p_target_page_id: string;\n          p_limit?: number;\n          p_offset?: number;\n        };\n        Returns: Array<{\n          id: string;\n          source_page_id: string;\n          source_block_id: string | null;\n          alias: string | null;\n          display_mode: string;\n          is_previewable: boolean;\n          created_at: string;\n          updated_at: string;\n          source_title: string | null;\n          workspace_id: string;\n        }>;\n      };\n    };\n    Enums: {\n      [_ in never]: never;\n    };\n    CompositeTypes: {\n      [_ in never]: never;\n    };\n  };\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/types/window-debug.d.ts","s":1,"e":13,"m":1773495684536.6936,"t":"export {};\n\ndeclare global {\n  interface Window {\n    /**\n     * 仅用于开发调试：在控制台访问当前思维导图实例。\n     * 生产环境不依赖该字段。\n     */\n    __mindmapInstance?: unknown | null;\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/ai-agent-ui.test.ts","s":1,"e":27,"m":1776326028981.6023,"t":"import { beforeEach, describe, expect, it } from \"vitest\";\nimport { useAiAgentUiStore } from \"./ai-agent-ui\";\n\ndescribe(\"useAiAgentUiStore\", () => {\n  beforeEach(() => {\n    useAiAgentUiStore.setState({\n      documentAgentAvailable: false,\n      documentAgentOpen: false,\n      documentAgentActivated: false,\n      globalAgentOpen: false,\n      globalAgentActivated: false,\n    });\n  });\n\n  it(\"全局 AI 开关不依赖页面 AI 可用态\", () => {\n    const state = useAiAgentUiStore.getState();\n\n    state.toggleGlobalAgentOpen();\n    expect(useAiAgentUiStore.getState().globalAgentOpen).toBe(true);\n    expect(useAiAgentUiStore.getState().globalAgentActivated).toBe(true);\n\n    state.toggleGlobalAgentOpen();\n    expect(useAiAgentUiStore.getState().globalAgentOpen).toBe(false);\n    expect(useAiAgentUiStore.getState().globalAgentActivated).toBe(true);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/ai-agent-ui.ts","s":1,"e":51,"m":1776325987942.81,"t":"\"use client\";\n\nimport { create } from \"zustand\";\n\ntype AiAgentUiState = {\n  globalAgentOpen: boolean;\n  globalAgentActivated: boolean;\n  documentAgentAvailable: boolean;\n  documentAgentOpen: boolean;\n  documentAgentActivated: boolean;\n  setGlobalAgentOpen: (open: boolean) => void;\n  toggleGlobalAgentOpen: () => void;\n  setDocumentAgentAvailable: (available: boolean) => void;\n  setDocumentAgentOpen: (open: boolean) => void;\n  toggleDocumentAgentOpen: () => void;\n};\n\nexport const useAiAgentUiStore = create<AiAgentUiState>((set, get) => ({\n  globalAgentOpen: false,\n  globalAgentActivated: false,\n  documentAgentAvailable: false,\n  documentAgentOpen: false,\n  documentAgentActivated: false,\n  setGlobalAgentOpen: (open) =>\n    set((state) => ({\n      globalAgentOpen: open,\n      globalAgentActivated: state.globalAgentActivated || open,\n    })),\n  toggleGlobalAgentOpen: () => {\n    const s = get();\n    set({\n      globalAgentOpen: !s.globalAgentOpen,\n      globalAgentActivated: s.globalAgentActivated || !s.globalAgentOpen,\n    });\n  },\n  setDocumentAgentAvailable: (available) => set({ documentAgentAvailable: available }),\n  setDocumentAgentOpen: (open) =>\n    set((state) => ({\n      documentAgentOpen: open,\n      documentAgentActivated: state.documentAgentActivated || open,\n    })),\n  toggleDocumentAgentOpen: () => {\n    const s = get();\n    if (!s.documentAgentAvailable) return;\n    set({\n      documentAgentOpen: !s.documentAgentOpen,\n      documentAgentActivated: s.documentAgentActivated || !s.documentAgentOpen,\n    });\n  },\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/app-preferences.ts","s":1,"e":60,"m":1773495684425.4763,"t":"\"use client\";\n\nimport { create } from \"zustand\";\n\nexport type ThemeMode = \"system\" | \"light\" | \"dark\";\n\ntype PersistedPreferences = {\n  theme: ThemeMode;\n  showStructure: boolean;\n  spellCheck: boolean;\n  flightMode: boolean;\n};\n\nconst STORAGE_KEY = \"mnote:preferences:v1\";\n\nconst loadPersisted = (): PersistedPreferences | null => {\n  if (typeof window === \"undefined\") return null;\n  try {\n    const raw = window.localStorage.getItem(STORAGE_KEY);\n    if (!raw) return null;\n    const parsed = JSON.parse(raw) as Partial<PersistedPreferences>;\n    const theme = parsed.theme === \"light\" || parsed.theme === \"dark\" || parsed.theme === \"system\" ? parsed.theme : \"system\";\n    return {\n      theme,\n      showStructure: typeof parsed.showStructure === \"boolean\" ? parsed.showStructure : false,\n      spellCheck: typeof parsed.spellCheck === \"boolean\" ? parsed.spellCheck : false,\n      flightMode: typeof parsed.flightMode === \"boolean\" ? parsed.flightMode : false,\n    };\n  } catch {\n    return null;\n  }\n};\n\nconst persist = (value: PersistedPreferences) => {\n  if (typeof window === \"undefined\") return;\n  try {\n    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value));\n  } catch {\n    // ignore\n  }\n};\n\ninterface AppPreferencesState extends PersistedPreferences {\n  hydrated: boolean;\n  hydrate: () => void;\n  setTheme: (theme: ThemeMode) => void;\n  setShowStructure: (showStructure: boolean) => void;\n  setSpellCheck: (spellCheck: boolean) => void;\n  setFlightMode: (flightMode: boolean) => void;\n}\n\nexport const useAppPreferencesStore = create<AppPreferencesState>((set, get) => ({\n  hydrated: false,\n  theme: \"system\",\n  showStructure: false,\n  spellCheck: false,\n  flightMode: false,\n  hydrate: () => {\n    const current = get();\n    if (current.hydrated) return;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/app-preferences.ts","s":49,"e":108,"m":1773495684425.4763,"t":"setFlightMode: (flightMode: boolean) => void;\n}\n\nexport const useAppPreferencesStore = create<AppPreferencesState>((set, get) => ({\n  hydrated: false,\n  theme: \"system\",\n  showStructure: false,\n  spellCheck: false,\n  flightMode: false,\n  hydrate: () => {\n    const current = get();\n    if (current.hydrated) return;\n    const persisted = loadPersisted();\n    if (persisted) {\n      set({ ...persisted, hydrated: true });\n    } else {\n      set({ hydrated: true });\n    }\n  },\n  setTheme: (theme) =>\n    set((state) => {\n      const next = { ...state, theme };\n      persist({\n        theme: next.theme,\n        showStructure: next.showStructure,\n        spellCheck: next.spellCheck,\n        flightMode: next.flightMode,\n      });\n      return next;\n    }),\n  setShowStructure: (showStructure) =>\n    set((state) => {\n      const next = { ...state, showStructure };\n      persist({\n        theme: next.theme,\n        showStructure: next.showStructure,\n        spellCheck: next.spellCheck,\n        flightMode: next.flightMode,\n      });\n      return next;\n    }),\n  setSpellCheck: (spellCheck) =>\n    set((state) => {\n      const next = { ...state, spellCheck };\n      persist({\n        theme: next.theme,\n        showStructure: next.showStructure,\n        spellCheck: next.spellCheck,\n        flightMode: next.flightMode,\n      });\n      return next;\n    }),\n  setFlightMode: (flightMode) =>\n    set((state) => {\n      const next = { ...state, flightMode };\n      persist({\n        theme: next.theme,\n        showStructure: next.showStructure,\n        spellCheck: next.spellCheck,\n        flightMode: next.flightMode,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/app-preferences.ts","s":97,"e":113,"m":1773495684425.4763,"t":"flightMode: next.flightMode,\n      });\n      return next;\n    }),\n  setFlightMode: (flightMode) =>\n    set((state) => {\n      const next = { ...state, flightMode };\n      persist({\n        theme: next.theme,\n        showStructure: next.showStructure,\n        spellCheck: next.spellCheck,\n        flightMode: next.flightMode,\n      });\n      return next;\n    }),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/comments-ui.ts","s":1,"e":35,"m":1773495684431.4763,"t":"\"use client\";\n\nimport { create } from \"zustand\";\n\nexport type CommentTarget = {\n  workspaceId: string;\n  documentId: string;\n  blockId: string | null;\n};\n\ninterface CommentsUiState {\n  open: boolean;\n  target: CommentTarget | null;\n  openForPage: (args: { workspaceId: string; documentId: string }) => void;\n  openForBlock: (args: { workspaceId: string; documentId: string; blockId: string }) => void;\n  close: () => void;\n}\n\nexport const useCommentsUiStore = create<CommentsUiState>((set) => ({\n  open: false,\n  target: null,\n  openForPage: ({ workspaceId, documentId }) =>\n    set({\n      open: true,\n      target: { workspaceId, documentId, blockId: null },\n    }),\n  openForBlock: ({ workspaceId, documentId, blockId }) =>\n    set({\n      open: true,\n      target: { workspaceId, documentId, blockId },\n    }),\n  close: () => set({ open: false }),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/current-document.ts","s":1,"e":28,"m":1773495684437.4763,"t":"\"use client\";\n\nimport { create } from \"zustand\";\n\ninterface CurrentDocumentState {\n  documentId: string | null;\n  readOnly: boolean;\n  disableDownload: boolean;\n  disableCopy: boolean;\n  setCurrent: (documentId: string, readOnly: boolean, disableDownload: boolean, disableCopy: boolean) => void;\n  clearIfMatch: (documentId: string) => void;\n}\n\nexport const useCurrentDocumentStore = create<CurrentDocumentState>((set, get) => ({\n  documentId: null,\n  readOnly: false,\n  disableDownload: false,\n  disableCopy: false,\n  setCurrent: (documentId, readOnly, disableDownload, disableCopy) =>\n    set({ documentId, readOnly, disableDownload, disableCopy }),\n  clearIfMatch: (documentId) => {\n    const state = get();\n    if (state.documentId === documentId) {\n      set({ documentId: null, readOnly: false, disableDownload: false, disableCopy: false });\n    }\n  },\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/editor-bridge.ts","s":1,"e":52,"m":1778334083620.3638,"t":"\"use client\";\n\nimport { create } from \"zustand\";\nimport type { ReferenceTarget } from \"@/types/search\";\nimport type { Json } from \"@/types/supabase\";\nimport type { MediaAsset } from \"@/types/media\";\n\nexport interface EditorReferenceBridgeResult {\n  blockId: string | null;\n}\n\nexport interface EditorReferenceBridge {\n  insertInlineReference: (target: ReferenceTarget, alias?: string) => EditorReferenceBridgeResult;\n  insertEmbedReference: (target: ReferenceTarget) => EditorReferenceBridgeResult;\n  undo?: () => void;\n  redo?: () => void;\n  getCursorBlockId?: () => string | null;\n  /**\n   * 向当前打开的文档插入一个“附件/媒体”块。\n   * 主要用于侧边栏文件树拖拽上传后，把文件显示到主编辑区。\n   */\n  insertMediaAsset?: (asset: MediaAsset) => void;\n  /**\n   * 向当前打开的文档插入一个“思维导图”块（使用 mindmapId 作为 block.id）。\n   * 主要用于侧边栏垃圾桶“恢复思维导图”后，把导图重新显示到主编辑区。\n   */\n  insertMindmapAsset?: (args: { documentId: string; mindmapId: string }) => void;\n  /**\n   * 向当前打开的文档插入一个“在线表格（luckysheet）”块。\n   * 主要用于侧边栏垃圾桶“恢复表格”后，把表格重新显示到主编辑区。\n   */\n  insertOnlineTableAsset?: (args: { documentId: string; tableId: string }) => void;\n  replaceWithSnapshot: (blocks: Json) => void;\n  openTableFullScreen?: (tableId: string) => void;\n}\n\ninterface EditorBridgeState {\n  bridge: EditorReferenceBridge | null;\n  registerBridge: (bridge: EditorReferenceBridge | null) => void;\n}\n\nexport const useEditorBridgeStore = create<EditorBridgeState>((set) => ({\n  bridge: null,\n  registerBridge: (bridge) =>\n    set((state) => {\n      if (state.bridge === bridge) {\n        return state;\n      }\n      return { bridge };\n    }),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/move-embed-picker.ts","s":1,"e":54,"m":1773495684448.9375,"t":"\"use client\";\n\nimport { create } from \"zustand\";\nimport type { MoveEmbedMode } from \"@/components/documents/move-embed-picker-dialog\";\n\ntype OnPick = (mode: MoveEmbedMode, targetId: string | null) => void | Promise<void>;\n\ninterface MoveEmbedPickerState {\n  open: boolean;\n  workspaceId: string | null;\n  defaultMode: MoveEmbedMode;\n  modes: MoveEmbedMode[];\n  allowRoot: boolean;\n  excludeIds: string[];\n  onPick: OnPick | null;\n  setWorkspaceId: (workspaceId: string | null) => void;\n  openPicker: (args: {\n    workspaceId: string | null;\n    defaultMode: MoveEmbedMode;\n    modes?: MoveEmbedMode[];\n    allowRoot?: boolean;\n    excludeIds?: string[];\n    onPick: OnPick;\n  }) => void;\n  close: () => void;\n}\n\nexport const useMoveEmbedPickerStore = create<MoveEmbedPickerState>((set) => ({\n  open: false,\n  workspaceId: null,\n  defaultMode: \"move\",\n  modes: [\"move\", \"embed\"],\n  allowRoot: true,\n  excludeIds: [],\n  onPick: null,\n  setWorkspaceId: (workspaceId) => set({ workspaceId }),\n  openPicker: ({ workspaceId, defaultMode, modes, allowRoot, excludeIds, onPick }) =>\n    set({\n      open: true,\n      workspaceId,\n      defaultMode,\n      modes: modes ?? [\"move\", \"embed\"],\n      allowRoot: allowRoot ?? true,\n      excludeIds: excludeIds ?? [],\n      onPick,\n    }),\n  close: () =>\n    set({\n      open: false,\n      onPick: null,\n      excludeIds: [],\n    }),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/onlyoffice-ai-bridge.ts","s":1,"e":34,"m":1776325211125.3298,"t":"\"use client\";\n\nimport { create } from \"zustand\";\n\ntype OnlyOfficeAiBridgeState = {\n  pluginReady: boolean;\n  targetOrigin: string;\n  targetWindow: Window | null;\n  lastReadyAt: number | null;\n  captureReady: (payload: { targetOrigin: string; targetWindow: Window | null }) => void;\n  reset: () => void;\n};\n\nexport const useOnlyOfficeAiBridgeStore = create<OnlyOfficeAiBridgeState>((set) => ({\n  pluginReady: false,\n  targetOrigin: \"*\",\n  targetWindow: null,\n  lastReadyAt: null,\n  captureReady: ({ targetOrigin, targetWindow }) =>\n    set({\n      pluginReady: Boolean(targetWindow),\n      targetOrigin: targetOrigin || \"*\",\n      targetWindow,\n      lastReadyAt: Date.now(),\n    }),\n  reset: () =>\n    set({\n      pluginReady: false,\n      targetOrigin: \"*\",\n      targetWindow: null,\n      lastReadyAt: null,\n    }),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/page-layout.ts","s":1,"e":19,"m":1773495684454.4766,"t":"\"use client\";\n\nimport { create } from \"zustand\";\n\ninterface PageLayoutState {\n  showInspector: boolean;\n  toggleInspector: () => void;\n  setInspector: (visible: boolean) => void;\n}\n\nexport const usePageLayoutStore = create<PageLayoutState>((set) => ({\n  showInspector: false,\n  toggleInspector: () =>\n    set((state) => ({\n      showInspector: !state.showInspector,\n    })),\n  setInspector: (visible) => set({ showInspector: visible }),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/search-palette.ts","s":1,"e":60,"m":1776325987941.81,"t":"\"use client\";\n\nimport { create } from \"zustand\";\nimport type {\n  DocumentSearchFilters,\n  DocumentSearchResult,\n  DocumentSearchTimeField,\n  DocumentSearchTimeRange,\n} from \"@/types/search\";\n\nexport type SearchPaletteMode = \"search\" | \"reference\";\nexport type ReferenceInsertMode = \"inline\" | \"embed\";\nexport type FilterKey = \"titleOnly\" | \"exact\" | \"onlyCurrentPage\" | \"includeOcr\";\n\ninterface SearchPaletteState {\n  open: boolean;\n  activated: boolean;\n  mode: SearchPaletteMode;\n  query: string;\n  filters: Omit<DocumentSearchFilters, \"timeRange\">;\n  timeRange: DocumentSearchTimeRange;\n  referenceMode: ReferenceInsertMode;\n  alias: string;\n  recent: DocumentSearchResult[];\n  setRecent: (items: DocumentSearchResult[]) => void;\n  openSearch: () => void;\n  openReference: (options?: { referenceMode?: ReferenceInsertMode; aliasSeed?: string }) => void;\n  close: () => void;\n  setQuery: (value: string) => void;\n  toggleFilter: (key: FilterKey) => void;\n  setTimeRange: (range: DocumentSearchTimeRange) => void;\n  setTimeField: (field: DocumentSearchTimeField) => void;\n  setCustomRange: (range: { from?: string; to?: string } | null) => void;\n  setReferenceMode: (mode: ReferenceInsertMode) => void;\n  setAlias: (value: string) => void;\n  rememberResult: (result: DocumentSearchResult) => void;\n  resetFilters: () => void;\n}\n\nconst defaultFilters: Omit<DocumentSearchFilters, \"timeRange\"> = {\n  titleOnly: false,\n  exact: false,\n  onlyCurrentPage: false,\n  includeOcr: false,\n  timeField: \"updated\",\n  customRange: undefined,\n};\n\nexport const useSearchPaletteStore = create<SearchPaletteState>((set) => ({\n  open: false,\n  activated: false,\n  mode: \"search\",\n  query: \"\",\n  filters: { ...defaultFilters },\n  timeRange: \"any\",\n  referenceMode: \"inline\",\n  alias: \"\",\n  recent: [],\n  setRecent: (items) => set({ recent: items.slice(0, 10) }),\n  openSearch: () =>","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/search-palette.ts","s":49,"e":108,"m":1776325987941.81,"t":"export const useSearchPaletteStore = create<SearchPaletteState>((set) => ({\n  open: false,\n  activated: false,\n  mode: \"search\",\n  query: \"\",\n  filters: { ...defaultFilters },\n  timeRange: \"any\",\n  referenceMode: \"inline\",\n  alias: \"\",\n  recent: [],\n  setRecent: (items) => set({ recent: items.slice(0, 10) }),\n  openSearch: () =>\n    set((state) => ({\n      open: true,\n      activated: true,\n      mode: \"search\",\n      query: state.query,\n    })),\n  openReference: (options) =>\n    set({\n      open: true,\n      activated: true,\n      mode: \"reference\",\n      referenceMode: options?.referenceMode ?? \"inline\",\n      alias: options?.aliasSeed ?? \"\",\n    }),\n  close: () => set({ open: false }),\n  setQuery: (value) => set({ query: value }),\n  toggleFilter: (key) =>\n    set((state) => ({\n      filters: {\n        ...state.filters,\n        [key]: !state.filters[key],\n      },\n    })),\n  setTimeRange: (range) => set({ timeRange: range }),\n  setTimeField: (field) =>\n    set((state) => ({\n      filters: {\n        ...state.filters,\n        timeField: field,\n      },\n    })),\n  setCustomRange: (range) =>\n    set((state) => ({\n      filters: {\n        ...state.filters,\n        customRange: range ?? undefined,\n      },\n    })),\n  setReferenceMode: (mode) => set({ referenceMode: mode }),\n  setAlias: (value) => set({ alias: value }),\n  rememberResult: (result) =>\n    set((state) => {\n      const existing = state.recent.filter((item) => item.id !== result.id);\n      return {\n        recent: [result, ...existing].slice(0, 10),\n      };\n    }),\n  resetFilters: () =>","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/search-palette.ts","s":97,"e":114,"m":1776325987941.81,"t":"},\n    })),\n  setReferenceMode: (mode) => set({ referenceMode: mode }),\n  setAlias: (value) => set({ alias: value }),\n  rememberResult: (result) =>\n    set((state) => {\n      const existing = state.recent.filter((item) => item.id !== result.id);\n      return {\n        recent: [result, ...existing].slice(0, 10),\n      };\n    }),\n  resetFilters: () =>\n    set({\n      filters: { ...defaultFilters },\n      timeRange: \"any\",\n    }),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/sidebar.test.ts","s":1,"e":48,"m":1776751939630.8762,"t":"import { afterEach, describe, expect, it } from \"vitest\";\nimport { useSidebarStore } from \"@/store/sidebar\";\n\ndescribe(\"useSidebarStore hydration\", () => {\n  afterEach(() => {\n    localStorage.clear();\n  });\n\n  it(\"模块初始化时应保持默认首帧状态，避免先读持久化状态\", () => {\n    const state = useSidebarStore.getState();\n\n    expect(state.open).toBe(false);\n    expect(state.width).toBe(280);\n    expect(state.viewMode).toBe(\"section\");\n  });\n\n  it(\"手动 hydrate 后才应用持久化状态\", () => {\n    localStorage.setItem(\n      \"sidebar-ui\",\n      JSON.stringify({\n        state: {\n          open: true,\n          width: 360,\n          collapsedSections: {\n            starred: false,\n            public: false,\n            shared: false,\n            private: true,\n            templates: false,\n          },\n          trashConfirm: false,\n          viewMode: \"filesystem\",\n        },\n        version: 4,\n      }),\n    );\n\n    const store = useSidebarStore;\n    expect(store.persist.hasHydrated()).toBe(false);\n    void store.persist.rehydrate();\n\n    expect(store.getState().open).toBe(true);\n    expect(store.getState().width).toBe(360);\n    expect(store.getState().viewMode).toBe(\"filesystem\");\n    expect(store.persist.hasHydrated()).toBe(true);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/sidebar.ts","s":1,"e":60,"m":1776751643245.2312,"t":"import { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport type { SidebarSectionId } from \"@/components/sidebar/types\";\n\ninterface SidebarState {\n  open: boolean;\n  width: number;\n  collapsedSections: Record<SidebarSectionId, boolean>;\n  trashConfirm: boolean;\n  viewMode: \"section\" | \"filesystem\";\n  setOpen: (open: boolean) => void;\n  setWidth: (width: number) => void;\n  toggleSection: (section: SidebarSectionId) => void;\n  setSectionCollapsed: (section: SidebarSectionId, collapsed: boolean) => void;\n  setTrashConfirm: (value: boolean) => void;\n  setViewMode: (mode: SidebarState[\"viewMode\"]) => void;\n  setCollapsedSections: (collapsed: Record<SidebarSectionId, boolean>) => void;\n}\n\nconst sectionDefaults: Record<SidebarSectionId, boolean> = {\n  starred: true,\n  public: true,\n  shared: true,\n  private: false,\n  templates: true,\n};\n\nexport const useSidebarStore = create<SidebarState>()(\n  persist(\n    (set) => ({\n      open: false,\n      width: 280,\n      collapsedSections: { ...sectionDefaults },\n      trashConfirm: true,\n      viewMode: \"section\",\n      setOpen: (open) => set({ open }),\n      setWidth: (width) => set({ width }),\n      toggleSection: (section) =>\n        set((state) => ({\n          collapsedSections: {\n            ...state.collapsedSections,\n            [section]: !state.collapsedSections[section],\n          },\n        })),\n      setSectionCollapsed: (section, collapsed) =>\n        set((state) => ({\n          collapsedSections: {\n            ...state.collapsedSections,\n            [section]: collapsed,\n          },\n        })),\n      setTrashConfirm: (value) => set({ trashConfirm: value }),\n      setViewMode: (mode) => set({ viewMode: mode }),\n      setCollapsedSections: (collapsed) => set({ collapsedSections: collapsed }),\n    }),\n    {\n      name: \"sidebar-ui\",\n      version: 4,\n      skipHydration: true,\n      migrate: (persistedState, version) => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/store/sidebar.ts","s":49,"e":91,"m":1776751643245.2312,"t":"[section]: collapsed,\n          },\n        })),\n      setTrashConfirm: (value) => set({ trashConfirm: value }),\n      setViewMode: (mode) => set({ viewMode: mode }),\n      setCollapsedSections: (collapsed) => set({ collapsedSections: collapsed }),\n    }),\n    {\n      name: \"sidebar-ui\",\n      version: 4,\n      skipHydration: true,\n      migrate: (persistedState, version) => {\n        const state = persistedState as\n          | Partial<\n              Pick<\n                SidebarState,\n                \"width\" | \"collapsedSections\" | \"trashConfirm\" | \"viewMode\"\n              >\n            >\n          | undefined;\n        if (!state) return state;\n        if (version >= 4) return state;\n\n        const nextCollapsedSections = {\n          ...sectionDefaults,\n          ...(state.collapsedSections ?? {}),\n        };\n\n        return {\n          ...state,\n          collapsedSections: nextCollapsedSections,\n        };\n      },\n      partialize: (state) => ({\n        width: state.width,\n        collapsedSections: state.collapsedSections,\n        trashConfirm: state.trashConfirm,\n        viewMode: state.viewMode,\n      }),\n    },\n  ),\n);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/api-utils.ts","s":1,"e":60,"m":1773495684303.4749,"t":"/**\n * API 工具函数\n * 提供统一的错误处理和响应解析\n */\n\nimport { NextResponse } from \"next/server\";\n\n/**\n * API 错误类\n */\nexport class ApiError extends Error {\n  constructor(\n    message: string,\n    public status: number,\n    public details?: unknown,\n  ) {\n    super(message);\n    this.name = \"ApiError\";\n  }\n}\n\n/**\n * 统一的 API 错误响应格式\n */\nexport interface ApiErrorResponse {\n  error: string;\n  details?: unknown;\n}\n\n/**\n * 创建 API 错误响应\n * @param message 错误消息\n * @param status HTTP 状态码\n * @param details 额外的错误详情\n * @returns NextResponse\n */\nexport function apiErrorResponse(\n  message: string,\n  status = 500,\n  details?: unknown,\n): NextResponse<ApiErrorResponse> {\n  const payload: ApiErrorResponse =\n    typeof details === \"undefined\" ? { error: message } : { error: message, details };\n  return NextResponse.json(payload, { status });\n}\n\n/**\n * 常用错误响应的快捷方法\n */\nexport const errorResponses = {\n  /** 400 - 请求参数错误 */\n  badRequest: (message: string = \"请求参数错误\") => apiErrorResponse(message, 400),\n\n  /** 401 - 未登录 */\n  unauthorized: (message: string = \"未登录\") => apiErrorResponse(message, 401),\n\n  /** 403 - 无权限 */\n  forbidden: (message: string = \"无权限访问\") => apiErrorResponse(message, 403),\n\n  /** 404 - 资源不存在 */","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/api-utils.ts","s":49,"e":108,"m":1773495684303.4749,"t":"*/\nexport const errorResponses = {\n  /** 400 - 请求参数错误 */\n  badRequest: (message: string = \"请求参数错误\") => apiErrorResponse(message, 400),\n\n  /** 401 - 未登录 */\n  unauthorized: (message: string = \"未登录\") => apiErrorResponse(message, 401),\n\n  /** 403 - 无权限 */\n  forbidden: (message: string = \"无权限访问\") => apiErrorResponse(message, 403),\n\n  /** 404 - 资源不存在 */\n  notFound: (message: string = \"资源不存在\") => apiErrorResponse(message, 404),\n\n  /** 500 - 服务器错误 */\n  internalError: (message: string = \"服务器错误\") => apiErrorResponse(message, 500),\n\n  /** AI 配置错误 */\n  aiConfigError: (provider: \"online\" | \"local\") =>\n    apiErrorResponse(\n      provider === \"local\"\n        ? \"未找到本地 AI 配置（LOCAL_AI_BASE_URL/LOCAL_AI_MODEL 或 ai.local.md / ai-local.md）\"\n        : \"未找到在线 AI 配置（ai.md 或 ONLINE_AI_* 环境变量）\",\n      500,\n    ),\n} as const;\n\n/**\n * 处理 fetch 响应并解析 JSON\n * 如果响应不成功，抛出 ApiError\n * @param response Fetch Response 对象\n * @param defaultErrorMessage 默认错误消息\n * @returns 解析后的 JSON 数据\n */\nexport async function handleApiResponse<T>(\n  response: Response,\n  defaultErrorMessage: string = \"请求失败\",\n): Promise<T> {\n  if (!response.ok) {\n    let message = defaultErrorMessage;\n    let details: unknown;\n\n    try {\n      const payload = await response.json();\n      if (payload && typeof payload === \"object\") {\n        if (\"error\" in payload && typeof payload.error === \"string\") {\n          message = payload.error;\n        }\n        if (\"details\" in payload) {\n          details = payload.details;\n        }\n      }\n    } catch {\n      // JSON 解析失败，使用默认消息\n    }\n\n    throw new ApiError(message, response.status, details);\n  }\n\n  return response.json() as Promise<T>;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/api-utils.ts","s":97,"e":156,"m":1773495684303.4749,"t":"if (\"details\" in payload) {\n          details = payload.details;\n        }\n      }\n    } catch {\n      // JSON 解析失败，使用默认消息\n    }\n\n    throw new ApiError(message, response.status, details);\n  }\n\n  return response.json() as Promise<T>;\n}\n\n/**\n * 安全地解析 JSON，失败时返回 null\n * @param raw JSON 字符串\n * @returns 解析后的对象或 null\n */\nexport function safeParseJson<T = unknown>(raw: string | null | undefined): T | null {\n  if (!raw) return null;\n  try {\n    return JSON.parse(raw) as T;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * 验证请求体是否包含必需字段\n * @param body 请求体对象\n * @param requiredFields 必需字段列表\n * @returns 如果验证失败，返回错误响应；否则返回 null\n */\nexport function validateRequestBody<T extends Record<string, unknown>>(\n  body: T | null,\n  requiredFields: (keyof T)[],\n): NextResponse<ApiErrorResponse> | null {\n  if (!body) {\n    return apiErrorResponse(\"请求体为空\", 400);\n  }\n\n  for (const field of requiredFields) {\n    if (!(field in body) || body[field] === null || body[field] === undefined) {\n      return apiErrorResponse(`缺少必需字段: ${String(field)}`, 400);\n    }\n  }\n\n  return null;\n}\n\n/**\n * 从请求中安全地获取 JSON\n * @param request Next.js Request 对象\n * @returns 解析后的 JSON 或 null\n */\nexport async function safeGetJsonBody<T = unknown>(\n  request: Request,\n): Promise<T | null> {\n  try {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/api-utils.ts","s":145,"e":162,"m":1773495684303.4749,"t":"return null;\n}\n\n/**\n * 从请求中安全地获取 JSON\n * @param request Next.js Request 对象\n * @returns 解析后的 JSON 或 null\n */\nexport async function safeGetJsonBody<T = unknown>(\n  request: Request,\n): Promise<T | null> {\n  try {\n    return (await request.json()) as T;\n  } catch {\n    return null;\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/blocks.ts","s":1,"e":60,"m":1773495684309.4749,"t":"import type { Json } from \"@/types/supabase\";\n\ntype BlockLike = {\n  id: string;\n  type?: string;\n  props?: Record<string, unknown>;\n  content?: unknown;\n  children?: BlockLike[];\n};\n\nconst asBlockArray = (value: unknown): BlockLike[] => {\n  if (!Array.isArray(value)) return [];\n  return value.filter((b) => b && typeof b === \"object\" && typeof (b as { id?: unknown }).id === \"string\") as BlockLike[];\n};\n\nexport const findBlockInTree = (\n  blocks: BlockLike[],\n  blockId: string,\n): { block: BlockLike; parent: BlockLike | null; index: number } | null => {\n  const stack: Array<{ list: BlockLike[]; parent: BlockLike | null }> = [{ list: blocks, parent: null }];\n  while (stack.length) {\n    const item = stack.pop()!;\n    const list = item.list;\n    for (let i = 0; i < list.length; i += 1) {\n      const b = list[i]!;\n      if (b.id === blockId) {\n        return { block: b, parent: item.parent, index: i };\n      }\n      if (Array.isArray(b.children) && b.children.length > 0) {\n        stack.push({ list: b.children, parent: b });\n      }\n    }\n  }\n  return null;\n};\n\nconst cloneBlock = (block: BlockLike): BlockLike => {\n  return {\n    ...block,\n    props: block.props ? { ...block.props } : undefined,\n    content: Array.isArray(block.content) ? [...block.content] : block.content,\n    children: Array.isArray(block.children) ? block.children.map(cloneBlock) : block.children,\n  };\n};\n\nexport const removeBlockSubtree = (\n  blocks: BlockLike[],\n  blockId: string,\n): { removed: BlockLike | null; nextBlocks: BlockLike[] } => {\n  // 说明：这里不直接修改入参 blocks，返回新的 nextBlocks。\n  const nextTop = blocks.map(cloneBlock);\n  const hit = findBlockInTree(nextTop, blockId);\n  if (!hit) return { removed: null, nextBlocks: nextTop };\n\n  if (hit.parent) {\n    const parent = hit.parent;\n    const nextChildren = asBlockArray(parent.children).map(cloneBlock);\n    const removed = nextChildren.splice(hit.index, 1)[0] ?? null;\n    parent.children = nextChildren;\n    return { removed, nextBlocks: nextTop };","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/blocks.ts","s":49,"e":108,"m":1773495684309.4749,"t":"): { removed: BlockLike | null; nextBlocks: BlockLike[] } => {\n  // 说明：这里不直接修改入参 blocks，返回新的 nextBlocks。\n  const nextTop = blocks.map(cloneBlock);\n  const hit = findBlockInTree(nextTop, blockId);\n  if (!hit) return { removed: null, nextBlocks: nextTop };\n\n  if (hit.parent) {\n    const parent = hit.parent;\n    const nextChildren = asBlockArray(parent.children).map(cloneBlock);\n    const removed = nextChildren.splice(hit.index, 1)[0] ?? null;\n    parent.children = nextChildren;\n    return { removed, nextBlocks: nextTop };\n  }\n\n  const removed = nextTop.splice(hit.index, 1)[0] ?? null;\n  return { removed, nextBlocks: nextTop };\n};\n\nexport const replaceBlockInTree = (\n  blocks: BlockLike[],\n  blockId: string,\n  nextBlock: BlockLike,\n): { ok: boolean; nextBlocks: BlockLike[] } => {\n  const nextTop = blocks.map(cloneBlock);\n  const hit = findBlockInTree(nextTop, blockId);\n  if (!hit) return { ok: false, nextBlocks: nextTop };\n\n  const normalized = cloneBlock({ ...nextBlock, id: blockId });\n  if (hit.parent) {\n    const parent = hit.parent;\n    const nextChildren = asBlockArray(parent.children).map(cloneBlock);\n    nextChildren[hit.index] = normalized;\n    parent.children = nextChildren;\n    return { ok: true, nextBlocks: nextTop };\n  }\n\n  nextTop[hit.index] = normalized;\n  return { ok: true, nextBlocks: nextTop };\n};\n\nexport const extractBlockText = (block: BlockLike): string => {\n  const inline = Array.isArray(block.content) ? (block.content as Array<{ text?: unknown }>) : [];\n  const text = inline.map((n) => (typeof n?.text === \"string\" ? n.text : \"\")).join(\"\");\n  return text.trim();\n};\n\nexport const getBlocksFromDocumentContent = (content: unknown): BlockLike[] => {\n  if (Array.isArray(content)) return asBlockArray(content);\n  if (content && typeof content === \"object\") {\n    const blocks = (content as { blocks?: unknown }).blocks;\n    return asBlockArray(blocks);\n  }\n  return [];\n};\n\nexport const withBlocksWrittenBack = (content: unknown, blocks: BlockLike[]): Json => {\n  // 复用现有结构：数组或 {blocks: []}\n  if (Array.isArray(content)) {\n    return blocks as unknown as Json;\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/blocks.ts","s":97,"e":115,"m":1773495684309.4749,"t":"if (content && typeof content === \"object\") {\n    const blocks = (content as { blocks?: unknown }).blocks;\n    return asBlockArray(blocks);\n  }\n  return [];\n};\n\nexport const withBlocksWrittenBack = (content: unknown, blocks: BlockLike[]): Json => {\n  // 复用现有结构：数组或 {blocks: []}\n  if (Array.isArray(content)) {\n    return blocks as unknown as Json;\n  }\n  if (content && typeof content === \"object\") {\n    return { ...(content as Record<string, unknown>), blocks } as Json;\n  }\n  return { blocks } as Json;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/constants.ts","s":1,"e":60,"m":1773495684315.475,"t":"/**\n * 全局常量定义\n * 集中管理项目中的魔法数字和硬编码值\n */\n\n// ============================================\n// AI Agent 相关常量\n// ============================================\n\n/** AI Agent 默认最大步数 */\nexport const DEFAULT_AGENT_MAX_STEPS = 10;\n\n/** AI Agent 最大步数限制 */\nexport const MAX_AGENT_STEPS = 24;\n\n/** AI Agent 最小步数 */\nexport const MIN_AGENT_STEPS = 1;\n\n/** 客户端工具默认超时时间（毫秒） */\nexport const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 60_000;\n\n// ============================================\n// 网络请求相关常量\n// ============================================\n\n/** OpenAI 兼容 API 最小超时（毫秒） */\nexport const MIN_API_TIMEOUT_MS = 500;\n\n/** OpenAI 兼容 API 默认超时（毫秒） */\nexport const DEFAULT_API_TIMEOUT_MS = 20_000;\n\n/** OpenAI 兼容 API 最大超时（毫秒） */\nexport const MAX_API_TIMEOUT_MS = 120_000;\n\n/** OpenAI 最小输出 token 数 */\nexport const MIN_COMPLETION_TOKENS = 64;\n\n/** OpenAI 最大输出 token 数 */\nexport const MAX_COMPLETION_TOKENS = 16_000;\n\n// ============================================\n// 编辑器相关常量\n// ============================================\n\n/** 内容加载延迟时间（毫秒） */\nexport const CONTENT_LOADING_DELAY_MS = 200;\n\n/** 编辑器块标题最小级别 */\nexport const MIN_BLOCK_LEVEL = 1;\n\n/** 编辑器块标题最大级别 */\nexport const MAX_BLOCK_LEVEL = 5;\n\n/** 零延迟 setTimeout（用于将任务推入事件循环） */\nexport const ZERO_DELAY_MS = 0;\n\n// ============================================\n// 思维导图相关常量\n// ============================================","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/constants.ts","s":49,"e":108,"m":1773495684315.475,"t":"export const MIN_BLOCK_LEVEL = 1;\n\n/** 编辑器块标题最大级别 */\nexport const MAX_BLOCK_LEVEL = 5;\n\n/** 零延迟 setTimeout（用于将任务推入事件循环） */\nexport const ZERO_DELAY_MS = 0;\n\n// ============================================\n// 思维导图相关常量\n// ============================================\n\n/** 思维导图最大附件数量 */\nexport const MAX_MINDMAP_ATTACHMENTS = 12;\n\n/** 思维导图最大选中节点数 */\nexport const MAX_SELECTED_NODES = 6;\n\n// ============================================\n// RAG 搜索相关常量\n// ============================================\n\n/** RAG 默认搜索结果数量 */\nexport const DEFAULT_RAG_TOP_K = 12;\n\n/** RAG 默认 chunk 结果数量 */\nexport const DEFAULT_RAG_CHUNK_TOP_K = 12;\n\n/** 文档搜索默认结果数量 */\nexport const DEFAULT_DOCS_SEARCH_LIMIT = 12;\n\n/** 文档读取默认最大字符数 */\nexport const DEFAULT_DOCS_READ_MAX_CHARS = 2500;\n\n/** 文档获取默认最大块数 */\nexport const DEFAULT_DOC_GET_MAX_BLOCKS = 80;\n\n/** 文档查找默认最大结果数 */\nexport const DEFAULT_DOC_FIND_MAX_RESULTS = 8;\n\n// ============================================\n// 资产/附件相关常量\n// ============================================\n\n/** 思维导图从资产转换最大项目数 */\nexport const DEFAULT_ASSET_TO_MINDMAP_MAX_ITEMS = 120;\n\n/** 搜索默认结果数量 */\nexport const DEFAULT_SEARCH_COUNT = 6;\n\n// ============================================\n// UI 相关常量\n// ============================================\n\n/** 表格嵌入预览最小高度（像素） */\nexport const MIN_EMBED_HEIGHT = 120;\n\n/** 表格嵌入预览最大高度（像素） */\nexport const MAX_EMBED_HEIGHT = 600;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/constants.ts","s":97,"e":128,"m":1773495684315.475,"t":"export const DEFAULT_SEARCH_COUNT = 6;\n\n// ============================================\n// UI 相关常量\n// ============================================\n\n/** 表格嵌入预览最小高度（像素） */\nexport const MIN_EMBED_HEIGHT = 120;\n\n/** 表格嵌入预览最大高度（像素） */\nexport const MAX_EMBED_HEIGHT = 600;\n\n/** 表格嵌入预览默认高度（像素） */\nexport const DEFAULT_EMBED_HEIGHT = 300;\n\n/** 上下文菜单距离窗口边缘的最小内边距（像素） */\nexport const CONTEXT_MENU_PADDING = 12;\n\n// ============================================\n// 工具函数\n// ============================================\n\n/**\n * 将数值限制在指定范围内\n * @param n 输入值\n * @param min 最小值\n * @param max 最大值\n * @returns 限制后的值\n */\nexport const clamp = (n: number, min: number, max: number): number =>\n  Math.max(min, Math.min(max, n));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/document-content.ts","s":1,"e":31,"m":1773495684322.475,"t":"import type { Json } from \"@/types/supabase\";\n\n/**\n * 提取文档内容中的块数组，兼容数组和 { blocks: [] } 两种结构。\n */\nexport const extractBlocksFromContent = (content: unknown): Json[] => {\n  if (Array.isArray(content)) {\n    return content as Json[];\n  }\n  if (content && typeof content === \"object\" && Array.isArray((content as { blocks?: Json[] }).blocks)) {\n    return ((content as { blocks?: Json[] }).blocks ?? []) as Json[];\n  }\n  return [];\n};\n\n/**\n * 将新的块数组写回内容，保持原有结构额外字段。\n */\nexport const composeContentWithBlocks = (content: unknown, blocks: Json[]): Json => {\n  if (Array.isArray(content)) {\n    return blocks as Json;\n  }\n  if (content && typeof content === \"object\") {\n    return {\n      ...(content as Record<string, unknown>),\n      blocks,\n    } as Json;\n  }\n  return { blocks } as Json;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/document-tree.test.ts","s":1,"e":35,"m":1773495684329.475,"t":"import { describe, expect, it } from \"vitest\";\nimport { collectSubtree } from \"../../convex/_utils/documentTree\";\n\ndescribe(\"collectSubtree\", () => {\n  it(\"会收集根节点及其所有后代\", () => {\n    const rows = [\n      { id: \"A\", parent_id: null },\n      { id: \"B\", parent_id: \"A\" },\n      { id: \"C\", parent_id: \"B\" },\n      { id: \"D\", parent_id: \"A\" },\n      { id: \"E\", parent_id: null },\n    ] as const;\n\n    const subtree = collectSubtree(rows, \"A\");\n    expect(new Set(subtree.map((r) => r.id))).toEqual(new Set([\"A\", \"B\", \"C\", \"D\"]));\n  });\n\n  it(\"根节点不存在时返回空数组\", () => {\n    const rows = [{ id: \"A\", parent_id: null }] as const;\n    expect(collectSubtree(rows, \"missing\")).toEqual([]);\n  });\n\n  it(\"存在环时不会死循环\", () => {\n    const rows = [\n      { id: \"A\", parent_id: \"B\" },\n      { id: \"B\", parent_id: \"A\" },\n      { id: \"C\", parent_id: null },\n    ] as const;\n\n    const subtree = collectSubtree(rows, \"A\");\n    expect(new Set(subtree.map((r) => r.id))).toEqual(new Set([\"A\", \"B\"]));\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents.ts","s":1,"e":31,"m":1776376954067.3416,"t":"export interface DocumentRecord {\n  access_scope: \"private\" | \"shared\" | \"public\";\n  id: string;\n  workspace_id: string;\n  title: string | null;\n  parent_id: string | null;\n  sort_order: number | null;\n  is_starred: boolean | null;\n  is_template: boolean;\n  created_at: string;\n  updated_at: string | null;\n}\n\nexport interface DocumentNode extends DocumentRecord {\n  children: DocumentNode[];\n}\n\nexport function findBreadcrumb(records: DocumentRecord[], targetId: string): DocumentRecord[] {\n  const map = new Map<string, DocumentRecord>();\n  records.forEach((item) => map.set(item.id, item));\n  const path: DocumentRecord[] = [];\n  let current: DocumentRecord | undefined = map.get(targetId);\n\n  while (current) {\n    path.unshift(current);\n    current = current.parent_id ? map.get(current.parent_id) : undefined;\n  }\n\n  return path;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/events.ts","s":1,"e":42,"m":1773495684349.4753,"t":"/**\n * 侧边栏与编辑区之间的轻量事件总线\n */\nexport const ASSETS_CHANGED_EVENT = \"wolai:assets-changed\";\nexport const DOCUMENTS_CHANGED_EVENT = \"wolai:documents-changed\";\nexport const ASSETS_RESTORED_EVENT = \"wolai:assets-restored\";\n\ntype AssetsChangedPayload = {\n  docId?: string;\n  asset?: unknown;\n  assetIds?: string[];\n  mindmapDeleted?: boolean;\n  mindmapAssetIds?: string[];\n};\n\ntype AssetsRestoredPayload =\n  | { docId: string; kind: \"media\"; assetId: string; asset?: unknown }\n  | { docId: string; kind: \"mindmap\"; mindmapId: string }\n  | { docId: string; kind: \"table\"; tableId: string };\n\nexport function emitAssetsChanged(\n  docId?: string,\n  asset?: unknown,\n  assetIds?: string[],\n  mindmapDeleted?: boolean,\n  mindmapAssetIds?: string[],\n) {\n  if (typeof window === \"undefined\") return;\n  const detail: AssetsChangedPayload = { docId, asset, assetIds, mindmapDeleted, mindmapAssetIds };\n  window.dispatchEvent(new CustomEvent(ASSETS_CHANGED_EVENT, { detail }));\n}\n\nexport function emitAssetsRestored(payload: AssetsRestoredPayload) {\n  if (typeof window === \"undefined\") return;\n  window.dispatchEvent(new CustomEvent(ASSETS_RESTORED_EVENT, { detail: payload }));\n}\n\nexport function emitDocumentsChanged(docId?: string) {\n  if (typeof window === \"undefined\") return;\n  window.dispatchEvent(new CustomEvent(DOCUMENTS_CHANGED_EVENT, { detail: { docId } }));\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":1,"e":60,"m":1777361904183.93,"t":"import type { DocumentRecord } from \"@/lib/documents\";\nimport type { SidebarTreeNode } from \"@/lib/kernel-sidebar\";\nimport type {\n  TreeProjectionAssetKind,\n  TreeProjectionCapability,\n  TreeProjectionItemBase,\n  TreeProjectionNodeType,\n  TreeProjectionResourceKind,\n} from \"@/lib/tree-protocol\";\nimport type { MediaAsset } from \"@/types/media\";\n\nexport type KernelFileTreeProjectionRowKind =\n  | \"document\"\n  | \"index\"\n  | \"asset\"\n  | \"asset_folder\";\n\nexport type KernelFileTreeProjectionItem = TreeProjectionItemBase & {\n  projectionKind: \"file_tree\";\n  rowId: string;\n  rowKind: KernelFileTreeProjectionRowKind;\n};\n\nexport type KernelFileTreeProjectionEdge = {\n  id: string;\n  edgeType: \"parent_of\";\n  workspaceId: string | null;\n  fromNodeId: string;\n  toNodeId: string;\n};\n\nexport type KernelFileTreeIndexingVisibility = {\n  schema: \"mnote.file_tree.indexing_visibility\";\n  schemaVersion: 1;\n  source: \"kernel.project_view\";\n  status: \"visible\" | \"stale\" | \"refreshing\" | \"unknown\";\n  requestKey: string | null;\n  indexedResourceKinds: string[];\n  visibleResourceKinds: string[];\n  metrics: {\n    visibleRows: number;\n    visibleEdges: number;\n  };\n};\n\nexport type KernelFileTreeProjection = {\n  projectionId: string;\n  projection: \"file_tree\";\n  rootNodeId: string | null;\n  items: KernelFileTreeProjectionItem[];\n  edges: KernelFileTreeProjectionEdge[];\n  meta?: {\n    search?: {\n      query?: string | null;\n      maxResults?: number | null;\n      maxResultsRule?: \"matches_only_before_ancestor_completion\";\n      ancestorCompletion?: \"include_all_ancestors_after_match_truncation\";\n      ordering?: \"kernel_file_tree_preorder\";\n      indexingVisibility?: KernelFileTreeIndexingVisibility;\n    };","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":49,"e":108,"m":1777361904183.93,"t":"rootNodeId: string | null;\n  items: KernelFileTreeProjectionItem[];\n  edges: KernelFileTreeProjectionEdge[];\n  meta?: {\n    search?: {\n      query?: string | null;\n      maxResults?: number | null;\n      maxResultsRule?: \"matches_only_before_ancestor_completion\";\n      ancestorCompletion?: \"include_all_ancestors_after_match_truncation\";\n      ordering?: \"kernel_file_tree_preorder\";\n      indexingVisibility?: KernelFileTreeIndexingVisibility;\n    };\n  };\n};\n\ntype BuildKernelFileTreeProjectionInput = {\n  documents: DocumentRecord[];\n  mediaAssets?: MediaAsset[] | null;\n  mindmapAssets?: MediaAsset[] | null;\n  tableAssets?: MediaAsset[] | null;\n  mindmapAssetChildren?: Record<string, string[]> | null;\n  rootNodeId?: string | null;\n};\n\ntype NormalizedFileTreeAsset = {\n  id: string;\n  documentId: string;\n  workspaceId: string | null;\n  title: string;\n  resourceKind: TreeProjectionResourceKind;\n  assetKind: TreeProjectionAssetKind;\n  iconHint: string;\n  nodeType: TreeProjectionNodeType;\n};\n\nfunction readRowId(value: unknown): string | null {\n  return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\nfunction toMillis(value: string | null | undefined): number {\n  if (!value) return 0;\n  const parsed = Date.parse(value);\n  return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction sortDocuments(records: DocumentRecord[]) {\n  return [...records].sort((a, b) => {\n    const orderA = a.sort_order ?? Number.MAX_SAFE_INTEGER;\n    const orderB = b.sort_order ?? Number.MAX_SAFE_INTEGER;\n    if (orderA !== orderB) {\n      return orderA - orderB;\n    }\n    return toMillis(a.created_at) - toMillis(b.created_at);\n  });\n}\n\nfunction dedupeDocuments(records: DocumentRecord[]) {\n  const seen = new Set<string>();\n  const unique: DocumentRecord[] = [];\n  for (let index = records.length - 1; index >= 0; index -= 1) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":97,"e":156,"m":1777361904183.93,"t":"const orderB = b.sort_order ?? Number.MAX_SAFE_INTEGER;\n    if (orderA !== orderB) {\n      return orderA - orderB;\n    }\n    return toMillis(a.created_at) - toMillis(b.created_at);\n  });\n}\n\nfunction dedupeDocuments(records: DocumentRecord[]) {\n  const seen = new Set<string>();\n  const unique: DocumentRecord[] = [];\n  for (let index = records.length - 1; index >= 0; index -= 1) {\n    const record = records[index]!;\n    if (seen.has(record.id)) {\n      continue;\n    }\n    seen.add(record.id);\n    unique.push(record);\n  }\n  unique.reverse();\n  return unique;\n}\n\nfunction buildDocumentChildren(records: DocumentRecord[]) {\n  const childrenByParentId = new Map<string | null, DocumentRecord[]>();\n  const recordIds = new Set(records.map((record) => record.id));\n\n  for (const record of records) {\n    const parentId =\n      record.parent_id && recordIds.has(record.parent_id) ? record.parent_id : null;\n    const bucket = childrenByParentId.get(parentId) ?? [];\n    bucket.push(record);\n    childrenByParentId.set(parentId, bucket);\n  }\n\n  for (const [parentId, bucket] of childrenByParentId.entries()) {\n    childrenByParentId.set(parentId, sortDocuments(bucket));\n  }\n\n  return childrenByParentId;\n}\n\nfunction makeProjectionEdge(\n  fromNodeId: string,\n  toNodeId: string,\n  workspaceId: string | null,\n): KernelFileTreeProjectionEdge {\n  return {\n    id: `edge:${fromNodeId}:${toNodeId}:parent_of`,\n    edgeType: \"parent_of\",\n    workspaceId,\n    fromNodeId,\n    toNodeId,\n  };\n}\n\nfunction buildDocumentCapabilities(childCount: number): TreeProjectionCapability[] {\n  const capabilities: TreeProjectionCapability[] = [\n    \"open\",\n    \"drag\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":145,"e":204,"m":1777361904183.93,"t":"id: `edge:${fromNodeId}:${toNodeId}:parent_of`,\n    edgeType: \"parent_of\",\n    workspaceId,\n    fromNodeId,\n    toNodeId,\n  };\n}\n\nfunction buildDocumentCapabilities(childCount: number): TreeProjectionCapability[] {\n  const capabilities: TreeProjectionCapability[] = [\n    \"open\",\n    \"drag\",\n    \"drop\",\n    \"select\",\n    \"create-child\",\n    \"rename\",\n    \"archive\",\n    \"restore\",\n    \"context-menu\",\n    \"reorder\",\n  ];\n  if (childCount > 0) {\n    capabilities.unshift(\"expand\");\n  }\n  return capabilities;\n}\n\nfunction buildAssetFolderCapabilities(childCount: number): TreeProjectionCapability[] {\n  const capabilities: TreeProjectionCapability[] = [\n    \"open-asset\",\n    \"select\",\n    \"context-menu\",\n  ];\n  if (childCount > 0) {\n    capabilities.unshift(\"expand\");\n  }\n  return capabilities;\n}\n\nfunction buildLeafCapabilities(openAsset: boolean): TreeProjectionCapability[] {\n  const capabilities: TreeProjectionCapability[] = [\"select\", \"context-menu\"];\n  capabilities.unshift(openAsset ? \"open-asset\" : \"open\");\n  return capabilities;\n}\n\nfunction classifyGenericAssetKind(asset: MediaAsset): TreeProjectionAssetKind {\n  const assetType = String(asset.asset_type ?? \"\").trim().toLowerCase();\n  const name = String(asset.file_name ?? \"\").trim().toLowerCase();\n  const mimeType = String(asset.mime_type ?? \"\").trim().toLowerCase();\n  const ext = name.includes(\".\") ? name.split(\".\").pop() ?? \"\" : \"\";\n\n  if (assetType === \"mindmap\") return \"mindmap\";\n  if (assetType === \"luckysheet\") return \"table\";\n  if (ext === \"pdf\" || mimeType.includes(\"pdf\")) return \"pdf\";\n  if (ext === \"epub\" || mimeType.includes(\"epub\")) return \"book\";\n  if (mimeType.startsWith(\"image/\")) return \"image\";\n  if (mimeType.startsWith(\"video/\")) return \"video\";\n  if (mimeType.startsWith(\"audio/\")) return \"audio\";\n  return \"file\";\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":193,"e":252,"m":1777361904183.93,"t":"const mimeType = String(asset.mime_type ?? \"\").trim().toLowerCase();\n  const ext = name.includes(\".\") ? name.split(\".\").pop() ?? \"\" : \"\";\n\n  if (assetType === \"mindmap\") return \"mindmap\";\n  if (assetType === \"luckysheet\") return \"table\";\n  if (ext === \"pdf\" || mimeType.includes(\"pdf\")) return \"pdf\";\n  if (ext === \"epub\" || mimeType.includes(\"epub\")) return \"book\";\n  if (mimeType.startsWith(\"image/\")) return \"image\";\n  if (mimeType.startsWith(\"video/\")) return \"video\";\n  if (mimeType.startsWith(\"audio/\")) return \"audio\";\n  return \"file\";\n}\n\nfunction classifyResourceKind(assetKind: TreeProjectionAssetKind): TreeProjectionResourceKind {\n  switch (assetKind) {\n    case \"mindmap\":\n      return \"mindmap\";\n    case \"table\":\n      return \"table\";\n    case \"book\":\n      return \"book\";\n    case \"pdf\":\n      return \"pdf\";\n    default:\n      return \"asset\";\n  }\n}\n\nfunction classifyNodeType(assetKind: TreeProjectionAssetKind): TreeProjectionNodeType {\n  switch (assetKind) {\n    case \"mindmap\":\n      return \"mindmap\";\n    case \"table\":\n      return \"table\";\n    case \"book\":\n      return \"book\";\n    case \"pdf\":\n      return \"pdf\";\n    default:\n      return \"asset\";\n  }\n}\n\nfunction classifyIconHint(assetKind: TreeProjectionAssetKind): string {\n  switch (assetKind) {\n    case \"mindmap\":\n      return \"mindmap\";\n    case \"table\":\n      return \"table\";\n    case \"book\":\n      return \"book\";\n    case \"pdf\":\n      return \"pdf\";\n    case \"image\":\n      return \"image\";\n    case \"video\":\n      return \"video\";\n    case \"audio\":\n      return \"audio\";\n    default:","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":241,"e":300,"m":1777361904183.93,"t":"return \"table\";\n    case \"book\":\n      return \"book\";\n    case \"pdf\":\n      return \"pdf\";\n    case \"image\":\n      return \"image\";\n    case \"video\":\n      return \"video\";\n    case \"audio\":\n      return \"audio\";\n    default:\n      return \"file\";\n  }\n}\n\nfunction normalizeAsset(asset: MediaAsset): NormalizedFileTreeAsset {\n  const assetKind = classifyGenericAssetKind(asset);\n  return {\n    id: asset.id,\n    documentId: asset.document_id,\n    workspaceId: asset.workspace_id ?? null,\n    title: asset.file_name?.trim() || \"附件\",\n    resourceKind: classifyResourceKind(assetKind),\n    assetKind,\n    iconHint: classifyIconHint(assetKind),\n    nodeType: classifyNodeType(assetKind),\n  };\n}\n\nfunction buildAssetCollections(input: BuildKernelFileTreeProjectionInput) {\n  const assetsByDoc = new Map<string, NormalizedFileTreeAsset[]>();\n  const assetById = new Map<string, NormalizedFileTreeAsset>();\n  const seen = new Set<string>();\n\n  for (const asset of [\n    ...(input.mediaAssets ?? []),\n    ...(input.mindmapAssets ?? []),\n    ...(input.tableAssets ?? []),\n  ]) {\n    if (!asset?.id || seen.has(asset.id)) {\n      continue;\n    }\n    seen.add(asset.id);\n    const normalized = normalizeAsset(asset);\n    const bucket = assetsByDoc.get(normalized.documentId) ?? [];\n    bucket.push(normalized);\n    assetsByDoc.set(normalized.documentId, bucket);\n    assetById.set(normalized.id, normalized);\n  }\n\n  const childAssetIdsByParentId = new Map<string, string[]>();\n  Object.entries(input.mindmapAssetChildren ?? {}).forEach(([parentAssetId, childIds]) => {\n    childAssetIdsByParentId.set(\n      parentAssetId,\n      (childIds ?? []).filter((childId) => typeof childId === \"string\" && childId.trim().length > 0),\n    );\n  });\n\n  return {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":289,"e":348,"m":1777361904183.93,"t":"assetById.set(normalized.id, normalized);\n  }\n\n  const childAssetIdsByParentId = new Map<string, string[]>();\n  Object.entries(input.mindmapAssetChildren ?? {}).forEach(([parentAssetId, childIds]) => {\n    childAssetIdsByParentId.set(\n      parentAssetId,\n      (childIds ?? []).filter((childId) => typeof childId === \"string\" && childId.trim().length > 0),\n    );\n  });\n\n  return {\n    assetsByDoc,\n    assetById,\n    childAssetIdsByParentId,\n  };\n}\n\nfunction makeFallbackNode(item: KernelFileTreeProjectionItem): SidebarTreeNode {\n  const documentId =\n    item.resourceMeta.documentId ??\n    (item.rowKind === \"document\" ? item.nodeId : item.parentNodeId ?? item.nodeId);\n  return {\n    access_scope: \"private\",\n    id: documentId,\n    workspace_id: item.resourceMeta.workspaceId ?? \"\",\n    title: item.title,\n    parent_id: item.rowKind === \"document\" ? item.parentNodeId : documentId,\n    sort_order: item.position,\n    is_starred: false,\n    is_template: false,\n    created_at: \"\",\n    updated_at: null,\n    children: [],\n    kernel: {\n      nodeType: \"page\",\n      depth: item.depth,\n      position: item.position,\n      childCount: item.childCount,\n      expandedByDefault: item.expandedByDefault,\n    },\n  };\n}\n\nfunction makeFallbackAsset(item: KernelFileTreeProjectionItem): MediaAsset {\n  const assetKind = item.resourceMeta.assetKind ?? \"unknown\";\n  const resourceKind = item.resourceMeta.resourceKind;\n  const assetType =\n    assetKind === \"mindmap\"\n      ? \"mindmap\"\n      : assetKind === \"table\" || resourceKind === \"table\"\n        ? \"luckysheet\"\n        : assetKind;\n  return {\n    id: item.resourceMeta.assetId ?? item.nodeId,\n    workspace_id: item.resourceMeta.workspaceId ?? \"\",\n    document_id: item.resourceMeta.documentId ?? \"\",\n    asset_type: assetType,\n    file_url: null,\n    thumbnail_url: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":337,"e":396,"m":1777361904183.93,"t":"assetKind === \"mindmap\"\n      ? \"mindmap\"\n      : assetKind === \"table\" || resourceKind === \"table\"\n        ? \"luckysheet\"\n        : assetKind;\n  return {\n    id: item.resourceMeta.assetId ?? item.nodeId,\n    workspace_id: item.resourceMeta.workspaceId ?? \"\",\n    document_id: item.resourceMeta.documentId ?? \"\",\n    asset_type: assetType,\n    file_url: null,\n    thumbnail_url: null,\n    bucket: null,\n    storage_path: null,\n    file_name: item.title,\n    file_size: null,\n    mime_type: null,\n    ocr_payload: undefined,\n    ocr_strategy: null,\n    ocr_text: null,\n    ocr_status: null,\n    signed_url: null,\n    created_at: \"\",\n    updated_at: \"\",\n  };\n}\n\nexport function getDocIdFromFileTreeItem(item: KernelFileTreeProjectionItem): string {\n  if (item.resourceMeta.documentId) {\n    return item.resourceMeta.documentId;\n  }\n  if (item.rowKind === \"document\") {\n    return item.nodeId;\n  }\n  const rowId = readRowId(item.rowId);\n  if (rowId?.startsWith(\"index:\")) {\n    return rowId.slice(\"index:\".length);\n  }\n  return item.parentNodeId ?? item.nodeId;\n}\n\nexport function resolveFileTreeRowNode(\n  item: KernelFileTreeProjectionItem,\n  nodeById?: Map<string, SidebarTreeNode>,\n): SidebarTreeNode {\n  const documentId = getDocIdFromFileTreeItem(item);\n  return nodeById?.get(documentId) ?? makeFallbackNode(item);\n}\n\nexport function resolveFileTreeRowAsset(\n  item: KernelFileTreeProjectionItem,\n  assetById?: Map<string, MediaAsset>,\n): MediaAsset {\n  const assetId = item.resourceMeta.assetId ?? item.nodeId;\n  return assetById?.get(assetId) ?? makeFallbackAsset(item);\n}\n\nexport function isKernelFileTreeProjection(value: unknown): value is KernelFileTreeProjection {\n  return (\n    Boolean(value) &&","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":385,"e":444,"m":1777361904183.93,"t":"export function resolveFileTreeRowAsset(\n  item: KernelFileTreeProjectionItem,\n  assetById?: Map<string, MediaAsset>,\n): MediaAsset {\n  const assetId = item.resourceMeta.assetId ?? item.nodeId;\n  return assetById?.get(assetId) ?? makeFallbackAsset(item);\n}\n\nexport function isKernelFileTreeProjection(value: unknown): value is KernelFileTreeProjection {\n  return (\n    Boolean(value) &&\n    typeof value === \"object\" &&\n    (value as KernelFileTreeProjection).projection === \"file_tree\" &&\n    Array.isArray((value as KernelFileTreeProjection).items) &&\n    Array.isArray((value as KernelFileTreeProjection).edges)\n  );\n}\n\nexport function buildKernelFileTreeProjection(\n  input: BuildKernelFileTreeProjectionInput,\n): KernelFileTreeProjection {\n  const uniqueDocuments = dedupeDocuments(input.documents);\n  const documentById = new Map(uniqueDocuments.map((document) => [document.id, document]));\n  const childrenByParentId = buildDocumentChildren(uniqueDocuments);\n  const { assetsByDoc, assetById, childAssetIdsByParentId } = buildAssetCollections(input);\n  const rootNodeId = input.rootNodeId?.trim() || null;\n  const roots =\n    rootNodeId && documentById.has(rootNodeId)\n      ? [rootNodeId]\n      : sortDocuments(\n          uniqueDocuments.filter((document) => {\n            const parentId = document.parent_id?.trim() || null;\n            return !parentId || !documentById.has(parentId);\n          }),\n        ).map((document) => document.id);\n\n  const items: KernelFileTreeProjectionItem[] = [];\n  const edges: KernelFileTreeProjectionEdge[] = [];\n\n  const walk = (documentId: string, depth: number) => {\n    const document = documentById.get(documentId);\n    if (!document) {\n      return;\n    }\n\n    const childDocuments = childrenByParentId.get(documentId) ?? [];\n    const documentAssets = assetsByDoc.get(documentId) ?? [];\n    const nestedChildIds = new Set<string>();\n    documentAssets.forEach((asset) => {\n      const childIds = childAssetIdsByParentId.get(asset.id) ?? [];\n      childIds.forEach((childId) => nestedChildIds.add(childId));\n    });\n    const directAssets = documentAssets.filter((asset) => !nestedChildIds.has(asset.id));\n    const childCount = 1 + directAssets.length + childDocuments.length;\n    const parentNodeId = depth === 0 ? null : document.parent_id ?? null;\n    const workspaceId = document.workspace_id ?? null;\n\n    items.push({\n      rowId: `doc:${document.id}`,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":433,"e":492,"m":1777361904183.93,"t":"const nestedChildIds = new Set<string>();\n    documentAssets.forEach((asset) => {\n      const childIds = childAssetIdsByParentId.get(asset.id) ?? [];\n      childIds.forEach((childId) => nestedChildIds.add(childId));\n    });\n    const directAssets = documentAssets.filter((asset) => !nestedChildIds.has(asset.id));\n    const childCount = 1 + directAssets.length + childDocuments.length;\n    const parentNodeId = depth === 0 ? null : document.parent_id ?? null;\n    const workspaceId = document.workspace_id ?? null;\n\n    items.push({\n      rowId: `doc:${document.id}`,\n      rowKind: \"document\",\n      nodeId: document.id,\n      parentNodeId,\n      nodeType: \"page\",\n      projectionKind: \"file_tree\",\n      title: document.title ?? \"无标题\",\n      depth,\n      position: document.sort_order ?? null,\n      childCount,\n      expandable: childCount > 0,\n      expandedByDefault: true,\n      capabilities: buildDocumentCapabilities(childCount),\n      resourceMeta: {\n        resourceKind: \"document\",\n        documentId: document.id,\n        workspaceId,\n        iconHint: \"page\",\n      },\n      iconHint: \"page\",\n    });\n\n    const indexNodeId = `index:${document.id}`;\n    items.push({\n      rowId: indexNodeId,\n      rowKind: \"index\",\n      nodeId: indexNodeId,\n      parentNodeId: document.id,\n      nodeType: \"index\",\n      projectionKind: \"file_tree\",\n      title: \"index.md\",\n      depth: depth + 1,\n      position: 0,\n      childCount: 0,\n      expandable: false,\n      expandedByDefault: false,\n      capabilities: buildLeafCapabilities(false),\n      resourceMeta: {\n        resourceKind: \"index\",\n        documentId: document.id,\n        workspaceId,\n        iconHint: \"index\",\n      },\n      iconHint: \"index\",\n    });\n    edges.push(makeProjectionEdge(document.id, indexNodeId, workspaceId));\n\n    let assetPosition = 1;\n    directAssets.forEach((asset) => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":481,"e":540,"m":1777361904183.93,"t":"resourceMeta: {\n        resourceKind: \"index\",\n        documentId: document.id,\n        workspaceId,\n        iconHint: \"index\",\n      },\n      iconHint: \"index\",\n    });\n    edges.push(makeProjectionEdge(document.id, indexNodeId, workspaceId));\n\n    let assetPosition = 1;\n    directAssets.forEach((asset) => {\n      const childIds = childAssetIdsByParentId.get(asset.id) ?? [];\n      if (childIds.length > 0) {\n        const folderNodeId = `asset-folder:${asset.id}`;\n        items.push({\n          rowId: folderNodeId,\n          rowKind: \"asset_folder\",\n          nodeId: folderNodeId,\n          parentNodeId: document.id,\n          nodeType: \"mindmap\",\n          projectionKind: \"file_tree\",\n          title: asset.title.replace(/\\.json$/i, \"\"),\n          depth: depth + 1,\n          position: assetPosition,\n          childCount: childIds.length,\n          expandable: true,\n          expandedByDefault: false,\n          capabilities: buildAssetFolderCapabilities(childIds.length),\n          resourceMeta: {\n            resourceKind: asset.resourceKind,\n            documentId: asset.documentId,\n            assetId: asset.id,\n            workspaceId: asset.workspaceId,\n            assetKind: asset.assetKind,\n            iconHint: \"mindmap\",\n          },\n          iconHint: \"mindmap\",\n        });\n        edges.push(makeProjectionEdge(document.id, folderNodeId, workspaceId));\n\n        childIds.forEach((childAssetId, index) => {\n          const childAsset = assetById.get(childAssetId);\n          if (!childAsset) {\n            return;\n          }\n          const childNodeId = `asset:${childAsset.id}`;\n          items.push({\n            rowId: childNodeId,\n            rowKind: \"asset\",\n            nodeId: childNodeId,\n            parentNodeId: folderNodeId,\n            nodeType: childAsset.nodeType,\n            projectionKind: \"file_tree\",\n            title: childAsset.title,\n            depth: depth + 2,\n            position: index,\n            childCount: 0,\n            expandable: false,\n            expandedByDefault: false,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":529,"e":588,"m":1777361904183.93,"t":"rowId: childNodeId,\n            rowKind: \"asset\",\n            nodeId: childNodeId,\n            parentNodeId: folderNodeId,\n            nodeType: childAsset.nodeType,\n            projectionKind: \"file_tree\",\n            title: childAsset.title,\n            depth: depth + 2,\n            position: index,\n            childCount: 0,\n            expandable: false,\n            expandedByDefault: false,\n            capabilities: buildLeafCapabilities(true),\n            resourceMeta: {\n              resourceKind: childAsset.resourceKind,\n              documentId: childAsset.documentId,\n              assetId: childAsset.id,\n              workspaceId: childAsset.workspaceId,\n              assetKind: childAsset.assetKind,\n              iconHint: childAsset.iconHint,\n            },\n            iconHint: childAsset.iconHint,\n          });\n          edges.push(makeProjectionEdge(folderNodeId, childNodeId, workspaceId));\n        });\n\n        assetPosition += 1;\n        return;\n      }\n\n      const assetNodeId = `asset:${asset.id}`;\n      items.push({\n        rowId: assetNodeId,\n        rowKind: \"asset\",\n        nodeId: assetNodeId,\n        parentNodeId: document.id,\n        nodeType: asset.nodeType,\n        projectionKind: \"file_tree\",\n        title: asset.title,\n        depth: depth + 1,\n        position: assetPosition,\n        childCount: 0,\n        expandable: false,\n        expandedByDefault: false,\n        capabilities: buildLeafCapabilities(true),\n        resourceMeta: {\n          resourceKind: asset.resourceKind,\n          documentId: asset.documentId,\n          assetId: asset.id,\n          workspaceId: asset.workspaceId,\n          assetKind: asset.assetKind,\n          iconHint: asset.iconHint,\n        },\n        iconHint: asset.iconHint,\n      });\n      edges.push(makeProjectionEdge(document.id, assetNodeId, workspaceId));\n      assetPosition += 1;\n    });\n\n    childDocuments.forEach((childDocument) => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-file-tree.ts","s":577,"e":625,"m":1777361904183.93,"t":"assetId: asset.id,\n          workspaceId: asset.workspaceId,\n          assetKind: asset.assetKind,\n          iconHint: asset.iconHint,\n        },\n        iconHint: asset.iconHint,\n      });\n      edges.push(makeProjectionEdge(document.id, assetNodeId, workspaceId));\n      assetPosition += 1;\n    });\n\n    childDocuments.forEach((childDocument) => {\n      walk(childDocument.id, depth + 1);\n    });\n  };\n\n  roots.forEach((documentId) => walk(documentId, 0));\n\n  return {\n    projectionId: `kernel_projection:file_tree:${rootNodeId ?? \"root\"}`,\n    projection: \"file_tree\",\n    rootNodeId,\n    items,\n    edges,\n    meta: {\n      search: {\n        query: null,\n        maxResults: null,\n        maxResultsRule: \"matches_only_before_ancestor_completion\",\n        ancestorCompletion: \"include_all_ancestors_after_match_truncation\",\n        ordering: \"kernel_file_tree_preorder\",\n        indexingVisibility: {\n          schema: \"mnote.file_tree.indexing_visibility\",\n          schemaVersion: 1,\n          source: \"kernel.project_view\",\n          status: \"visible\",\n          requestKey: null,\n          indexedResourceKinds: [\"document\", \"index\", \"asset\", \"asset_folder\", \"mindmap\", \"book\", \"pdf\"],\n          visibleResourceKinds: [\"document\", \"index\", \"asset\", \"asset_folder\", \"mindmap\", \"book\", \"pdf\"],\n          metrics: {\n            visibleRows: items.length,\n            visibleEdges: edges.length,\n          },\n        },\n      },\n    },\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-sidebar.ts","s":1,"e":60,"m":1778339806503.0166,"t":"import type { DocumentRecord } from \"@/lib/documents\";\nimport type {\n  TreeProjectionCapability,\n  TreeProjectionResourceMeta,\n} from \"@/lib/tree-protocol\";\n\nexport type KernelSidebarProjectionEdge = {\n  id: string;\n  edgeType: \"parent_of\";\n  workspaceId: string | null;\n  fromNodeId: string;\n  toNodeId: string;\n};\n\nexport type KernelSidebarProjectionItem = {\n  nodeId: string;\n  parentNodeId: string | null;\n  nodeType: \"page\";\n  projectionKind: \"sidebar_tree\";\n  title: string | null;\n  depth: number;\n  position: number | null;\n  childCount: number;\n  expandable: boolean;\n  expandedByDefault: boolean;\n  capabilities: TreeProjectionCapability[];\n  resourceMeta: TreeProjectionResourceMeta;\n  iconHint: string | null;\n};\n\nexport type KernelSidebarProjection = {\n  projectionId: string;\n  projection: \"sidebar_tree\";\n  rootNodeId: string | null;\n  items: KernelSidebarProjectionItem[];\n  edges: KernelSidebarProjectionEdge[];\n};\n\nexport type SidebarTreeNode = DocumentRecord & {\n  children: SidebarTreeNode[];\n  kernel?: {\n    nodeType: \"page\";\n    depth: number;\n    position: number | null;\n    childCount: number;\n    expandedByDefault: boolean;\n  };\n};\n\nfunction sortRecords(records: DocumentRecord[]) {\n  return [...records].sort((a, b) => {\n    const orderA = a.sort_order ?? Number.MAX_SAFE_INTEGER;\n    const orderB = b.sort_order ?? Number.MAX_SAFE_INTEGER;\n    if (orderA !== orderB) {\n      return orderA - orderB;\n    }\n    return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();\n  });\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-sidebar.ts","s":49,"e":108,"m":1778339806503.0166,"t":"function sortRecords(records: DocumentRecord[]) {\n  return [...records].sort((a, b) => {\n    const orderA = a.sort_order ?? Number.MAX_SAFE_INTEGER;\n    const orderB = b.sort_order ?? Number.MAX_SAFE_INTEGER;\n    if (orderA !== orderB) {\n      return orderA - orderB;\n    }\n    return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();\n  });\n}\n\nfunction dedupeRecords(records: DocumentRecord[]) {\n  const seen = new Set<string>();\n  const unique: DocumentRecord[] = [];\n  for (let index = records.length - 1; index >= 0; index -= 1) {\n    const record = records[index]!;\n    if (seen.has(record.id)) {\n      continue;\n    }\n    seen.add(record.id);\n    unique.push(record);\n  }\n  unique.reverse();\n  return unique;\n}\n\nfunction buildSidebarCapabilities(childCount: number): TreeProjectionCapability[] {\n  const capabilities: TreeProjectionCapability[] = [\n    \"open\",\n    \"drag\",\n    \"drop\",\n    \"select\",\n    \"create-child\",\n    \"rename\",\n    \"archive\",\n    \"restore\",\n    \"context-menu\",\n    \"reorder\",\n  ];\n  if (childCount > 0) {\n    capabilities.unshift(\"expand\");\n  }\n  return capabilities;\n}\n\nfunction buildChildrenByParent(records: DocumentRecord[]) {\n  const childrenByParentId = new Map<string | null, DocumentRecord[]>();\n  const recordIds = new Set(records.map((record) => record.id));\n  for (const record of records) {\n    const parentId =\n      record.parent_id && recordIds.has(record.parent_id) ? record.parent_id : null;\n    const bucket = childrenByParentId.get(parentId) ?? [];\n    bucket.push(record);\n    childrenByParentId.set(parentId, bucket);\n  }\n  for (const [parentId, bucket] of childrenByParentId.entries()) {\n    childrenByParentId.set(parentId, sortRecords(bucket));\n  }\n  return childrenByParentId;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-sidebar.ts","s":97,"e":156,"m":1778339806503.0166,"t":"const recordIds = new Set(records.map((record) => record.id));\n  for (const record of records) {\n    const parentId =\n      record.parent_id && recordIds.has(record.parent_id) ? record.parent_id : null;\n    const bucket = childrenByParentId.get(parentId) ?? [];\n    bucket.push(record);\n    childrenByParentId.set(parentId, bucket);\n  }\n  for (const [parentId, bucket] of childrenByParentId.entries()) {\n    childrenByParentId.set(parentId, sortRecords(bucket));\n  }\n  return childrenByParentId;\n}\n\nexport function buildKernelSidebarProjection(\n  records: DocumentRecord[],\n): KernelSidebarProjection {\n  const uniqueRecords = dedupeRecords(records);\n  const recordById = new Map(uniqueRecords.map((record) => [record.id, record]));\n  const childrenByParentId = buildChildrenByParent(uniqueRecords);\n  const items: KernelSidebarProjectionItem[] = [];\n  const edges: KernelSidebarProjectionEdge[] = [];\n  const visited = new Set<string>();\n\n  const walk = (parentId: string | null, depth: number) => {\n    const children = childrenByParentId.get(parentId) ?? [];\n    for (const child of children) {\n      if (visited.has(child.id)) {\n        continue;\n      }\n      visited.add(child.id);\n      const childNodes = childrenByParentId.get(child.id) ?? [];\n      items.push({\n        nodeId: child.id,\n        parentNodeId: parentId,\n        nodeType: \"page\",\n        projectionKind: \"sidebar_tree\",\n        title: child.title ?? \"无标题\",\n        depth,\n        position: child.sort_order ?? null,\n        childCount: childNodes.length,\n        expandable: childNodes.length > 0,\n        expandedByDefault: true,\n        capabilities: buildSidebarCapabilities(childNodes.length),\n        resourceMeta: {\n          resourceKind: \"document\",\n          documentId: child.id,\n          workspaceId: child.workspace_id ?? null,\n          iconHint: \"page\",\n        },\n        iconHint: \"page\",\n      });\n      if (parentId && recordById.has(parentId)) {\n        edges.push({\n          id: `edge:${parentId}:${child.id}:parent_of`,\n          edgeType: \"parent_of\",\n          workspaceId: child.workspace_id ?? null,\n          fromNodeId: parentId,\n          toNodeId: child.id,\n        });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-sidebar.ts","s":145,"e":204,"m":1778339806503.0166,"t":"iconHint: \"page\",\n        },\n        iconHint: \"page\",\n      });\n      if (parentId && recordById.has(parentId)) {\n        edges.push({\n          id: `edge:${parentId}:${child.id}:parent_of`,\n          edgeType: \"parent_of\",\n          workspaceId: child.workspace_id ?? null,\n          fromNodeId: parentId,\n          toNodeId: child.id,\n        });\n      }\n      walk(child.id, depth + 1);\n    }\n  };\n\n  walk(null, 0);\n\n  return {\n    projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n    projection: \"sidebar_tree\",\n    rootNodeId: null,\n    items,\n    edges,\n  };\n}\n\nexport function buildSidebarTreeFromKernelProjection(input: {\n  records: DocumentRecord[];\n  projection: KernelSidebarProjection;\n}): SidebarTreeNode[] {\n  const projectionItems = Array.isArray(input.projection?.items)\n    ? input.projection.items\n    : [];\n  const recordById = new Map(input.records.map((record) => [record.id, record]));\n  const nodeMap = new Map<string, SidebarTreeNode>();\n  const itemById = new Map(projectionItems.map((item) => [item.nodeId, item]));\n\n  for (const item of projectionItems) {\n    const record = recordById.get(item.nodeId);\n    nodeMap.set(item.nodeId, {\n      access_scope: record?.access_scope ?? \"private\",\n      id: item.nodeId,\n      workspace_id: record?.workspace_id ?? \"\",\n      title: record?.title ?? item.title ?? \"无标题\",\n      parent_id: record?.parent_id ?? item.parentNodeId,\n      sort_order: record?.sort_order ?? item.position,\n      is_starred: record?.is_starred ?? false,\n      is_template: record?.is_template ?? false,\n      created_at: record?.created_at ?? \"\",\n      updated_at: record?.updated_at ?? null,\n      children: [],\n      kernel: {\n        nodeType: item.nodeType,\n        depth: item.depth,\n        position: item.position,\n        childCount: item.childCount,\n        expandedByDefault: item.expandedByDefault,\n      },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/kernel-sidebar.ts","s":193,"e":236,"m":1778339806503.0166,"t":"is_starred: record?.is_starred ?? false,\n      is_template: record?.is_template ?? false,\n      created_at: record?.created_at ?? \"\",\n      updated_at: record?.updated_at ?? null,\n      children: [],\n      kernel: {\n        nodeType: item.nodeType,\n        depth: item.depth,\n        position: item.position,\n        childCount: item.childCount,\n        expandedByDefault: item.expandedByDefault,\n      },\n    });\n  }\n\n  const roots: SidebarTreeNode[] = [];\n  for (const item of projectionItems) {\n    const node = nodeMap.get(item.nodeId);\n    if (!node) continue;\n    const parentId = item.parentNodeId;\n    if (parentId && nodeMap.has(parentId)) {\n      nodeMap.get(parentId)!.children.push(node);\n      continue;\n    }\n    roots.push(node);\n  }\n\n  const sortTree = (nodes: SidebarTreeNode[]) => {\n    nodes.sort((a, b) => {\n      const orderA = a.kernel?.position ?? a.sort_order ?? Number.MAX_SAFE_INTEGER;\n      const orderB = b.kernel?.position ?? b.sort_order ?? Number.MAX_SAFE_INTEGER;\n      if (orderA !== orderB) {\n        return orderA - orderB;\n      }\n      return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();\n    });\n    nodes.forEach((node) => sortTree(node.children));\n  };\n\n  sortTree(roots);\n\n  return roots;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/online-table.ts","s":1,"e":60,"m":1773495684355.4756,"t":"import { DocumentTable, DocumentTableSnapshot, TableRowData, TableSchema } from \"@/types/online-table\";\n\n// 默认表格结构：全部为空列，交由用户/全屏自行定义\nexport const DEFAULT_TABLE_SCHEMA: TableSchema = {\n  columns: [],\n  frozenRowCount: 0,\n  frozenColCount: 0,\n};\n\nexport const DEFAULT_TABLE_ROWS = 50;\nexport const DEFAULT_TABLE_COLUMNS = 15;\n\nexport const createDefaultTableSnapshot = (schema: TableSchema): DocumentTableSnapshot => {\n  const frozenRowCount = schema.frozenRowCount ?? 0;\n  const frozenColCount = schema.frozenColCount ?? 0;\n  return {\n    rows: [],\n    luckysheet: [\n      {\n        name: \"Sheet1\",\n        index: 0,\n        status: 1,\n        order: 0,\n        hide: 0,\n        row: DEFAULT_TABLE_ROWS,\n        column: DEFAULT_TABLE_COLUMNS,\n        defaultRowHeight: 19,\n        defaultColWidth: 73,\n        celldata: [],\n        config: {\n          columnlen: {},\n          rowlen: {},\n        },\n        frozen: {\n          type: frozenRowCount > 0 || frozenColCount > 0 ? \"both\" : undefined,\n          row_focus: frozenRowCount,\n          column_focus: frozenColCount,\n        },\n        scrollLeft: 0,\n        scrollTop: 0,\n        zoomRatio: 1,\n        showGridLines: true,\n      },\n    ],\n  };\n};\n\n/**\n * 在 Supabase 中创建一个新的在线表格并返回元数据。\n * @param documentId 当前文档的 ID\n * @param title 表格的初始标题\n * @returns 新创建的 DocumentTable\n */\nexport async function createOnlineTable(\n  documentId: string,\n  title: string = \"未命名表格\"\n): Promise<DocumentTable> {\n  // 假设 Next.js API 路由 /api/tables/create 负责与 Supabase 交互\n  const response = await fetch(\"/api/tables/create\", {\n    method: \"POST\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/online-table.ts","s":49,"e":108,"m":1773495684355.4756,"t":"* 在 Supabase 中创建一个新的在线表格并返回元数据。\n * @param documentId 当前文档的 ID\n * @param title 表格的初始标题\n * @returns 新创建的 DocumentTable\n */\nexport async function createOnlineTable(\n  documentId: string,\n  title: string = \"未命名表格\"\n): Promise<DocumentTable> {\n  // 假设 Next.js API 路由 /api/tables/create 负责与 Supabase 交互\n  const response = await fetch(\"/api/tables/create\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      documentId,\n      title,\n      schema: DEFAULT_TABLE_SCHEMA,\n      snapshot: createDefaultTableSnapshot(DEFAULT_TABLE_SCHEMA),\n    }),\n  });\n\n  if (!response.ok) {\n    throw new Error(\"Failed to create online table.\");\n  }\n\n  const newTable: DocumentTable = await response.json();\n  return newTable;\n}\n\n/**\n * 获取表格元数据 (用于紧凑模式渲染)\n * 实际实现中，这可能需要一个更复杂的获取逻辑，例如同时获取前 N 行数据\n */\nexport async function getDocumentTable(tableId: string): Promise<DocumentTable> {\n  const response = await fetch(`/api/tables/${tableId}`, {\n    method: \"GET\",\n    headers: { \"Content-Type\": \"application/json\" },\n  });\n\n  if (!response.ok) {\n    throw new Error(\"Failed to fetch document table.\");\n  }\n\n  return response.json() as Promise<DocumentTable>;\n}\n\n/**\n * 将表格的 snapshot/行数据回写 Supabase，保持内联与全屏视图一致。\n */\nexport async function saveOnlineTable(\n  tableId: string,\n  payload: {\n    title?: string;\n    schema?: TableSchema;\n    snapshot?: DocumentTableSnapshot | null;\n    rows?: TableRowData[];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    viewPreferences?: Record<string, any>;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/online-table.ts","s":97,"e":138,"m":1773495684355.4756,"t":"/**\n * 将表格的 snapshot/行数据回写 Supabase，保持内联与全屏视图一致。\n */\nexport async function saveOnlineTable(\n  tableId: string,\n  payload: {\n    title?: string;\n    schema?: TableSchema;\n    snapshot?: DocumentTableSnapshot | null;\n    rows?: TableRowData[];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    viewPreferences?: Record<string, any>;\n  },\n): Promise<DocumentTable> {\n  const response = await fetch(`/api/tables/${tableId}`, {\n    method: \"PATCH\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify(payload),\n  });\n\n  if (!response.ok) {\n    const message = await response.text().catch(() => \"\");\n    throw new Error(message || \"Failed to save online table.\");\n  }\n\n  return response.json() as Promise<DocumentTable>;\n}\n\n/**\n * 删除表格及其行数据。\n */\nexport async function deleteOnlineTable(tableId: string): Promise<void> {\n  const response = await fetch(`/api/tables/${tableId}`, {\n    method: \"DELETE\",\n    headers: { \"Content-Type\": \"application/json\" },\n  });\n\n  if (!response.ok) {\n    throw new Error(\"Failed to delete online table.\");\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.test.ts","s":1,"e":60,"m":1777077183977.7412,"t":"import { afterEach, describe, expect, it, vi } from \"vitest\";\nimport {\n  getMnotePublicRuntimeConfig,\n  getMnoteRuntimeConfig,\n} from \"@/lib/runtime-config\";\n\ndescribe(\"runtime-config public projection\", () => {\n  it(\"不再暴露 legacy mnote-web runtime 字段\", () => {\n    const runtime = getMnotePublicRuntimeConfig();\n    expect(\"mnoteWebBaseUrl\" in (runtime as Record<string, unknown>)).toBe(false);\n    expect(\"mnoteWebTreeShellEnabled\" in (runtime as Record<string, unknown>)).toBe(false);\n  });\n\n  it(\"允许通过新 runtime 配置显式选择树 renderer family\", () => {\n    window.__MNOTE_RUNTIME_CONFIG__ = {\n      treeRendererFamily: \"rust_family\",\n    };\n\n    const runtime = getMnoteRuntimeConfig();\n\n    expect(runtime.treeRendererFamily).toBe(\"rust_family\");\n  });\n\n  it(\"树 renderer family 别名应归一到 rust_family\", () => {\n    window.__MNOTE_RUNTIME_CONFIG__ = {\n      treeRendererFamily: \"tree_shell\" as \"rust_family\",\n    };\n\n    const runtime = getMnoteRuntimeConfig();\n\n    expect(runtime.treeRendererFamily).toBe(\"rust_family\");\n  });\n\n  it(\"树 renderer family 缺省时应回落到 rust_family\", () => {\n    const runtime = getMnoteRuntimeConfig();\n\n    expect(runtime.treeRendererFamily).toBe(\"rust_family\");\n  });\n\n  afterEach(() => {\n    vi.unstubAllGlobals();\n    delete process.env.NEXT_PUBLIC_MNOTE_WEB_BASE_URL;\n    delete process.env.MNOTE_WEB_BASE_URL;\n    delete process.env.NEXT_PUBLIC_MNOTE_WEB_TREE_SHELL_ENABLED;\n    delete process.env.MNOTE_WEB_TREE_SHELL_ENABLED;\n    delete process.env.NEXT_PUBLIC_TREE_RENDERER_FAMILY;\n    delete process.env.TREE_RENDERER_FAMILY;\n    delete window.__MNOTE_RUNTIME_CONFIG__;\n  });\n\n  it(\"即使保留 legacy env 也不应回注 mnote-web runtime\", () => {\n    process.env.NEXT_PUBLIC_MNOTE_WEB_BASE_URL = \"http://127.0.0.1:3104\";\n    process.env.MNOTE_WEB_BASE_URL = \"http://127.0.0.1:3104\";\n    process.env.NEXT_PUBLIC_MNOTE_WEB_TREE_SHELL_ENABLED = \"1\";\n    process.env.MNOTE_WEB_TREE_SHELL_ENABLED = \"1\";\n\n    const runtime = getMnoteRuntimeConfig();\n\n    expect(\"mnoteWebBaseUrl\" in (runtime as Record<string, unknown>)).toBe(false);\n    expect(\"mnoteWebTreeShellEnabled\" in (runtime as Record<string, unknown>)).toBe(false);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.test.ts","s":49,"e":63,"m":1777077183977.7412,"t":"});\n\n  it(\"即使保留 legacy env 也不应回注 mnote-web runtime\", () => {\n    process.env.NEXT_PUBLIC_MNOTE_WEB_BASE_URL = \"http://127.0.0.1:3104\";\n    process.env.MNOTE_WEB_BASE_URL = \"http://127.0.0.1:3104\";\n    process.env.NEXT_PUBLIC_MNOTE_WEB_TREE_SHELL_ENABLED = \"1\";\n    process.env.MNOTE_WEB_TREE_SHELL_ENABLED = \"1\";\n\n    const runtime = getMnoteRuntimeConfig();\n\n    expect(\"mnoteWebBaseUrl\" in (runtime as Record<string, unknown>)).toBe(false);\n    expect(\"mnoteWebTreeShellEnabled\" in (runtime as Record<string, unknown>)).toBe(false);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.ts","s":1,"e":60,"m":1778336284573.613,"t":"export type MnoteRuntimeConfig = {\n  /**\n   * 是否启用 Convex（自部署）链路。\n   * 说明：该字段由服务端在运行期注入到 window.__MNOTE_RUNTIME_CONFIG__，用于客户端按需关闭 Supabase 相关能力。\n   */\n  useConvex?: boolean;\n  supabaseUrl?: string;\n  /**\n   * 服务端/本机回源用的 Supabase 地址（通常是 HTTP），用于避免 FRP/自签证书导致 Node 侧 TLS 校验失败。\n   * 说明：该字段会出现在 public/mnote-env.json 中，但它主要给服务端读取使用。\n   */\n  supabaseInternalUrl?: string;\n  supabaseAnonKey?: string;\n  backendUrl?: string;\n  onlyofficeBaseUrl?: string;\n  onlyofficeBaseUrlWeb?: string;\n  onlyofficeBaseUrlDesktop?: string;\n  onlyofficeStorageHostOverride?: string;\n  onlyofficeStorageHostOverrideWeb?: string;\n  onlyofficeStorageHostOverrideDesktop?: string;\n  cloudflareAppOrigin?: string;\n  onlyofficeProxyOriginWeb?: string;\n  onlyofficeCallbackOriginWeb?: string;\n  onlyofficeCallbackOriginDesktop?: string;\n  /**\n   * 编辑器 host 选择。\n   * 说明：文档页正式主链只保留 leptos_tiptap_island。\n   */\n  documentEditorHost?: \"leptos_tiptap_island\";\n  /**\n   * 树域 renderer family 选择。\n   * 说明：默认主路径已切到 rust_family；React fallback 仍作为过渡兜底保留。\n   */\n  treeRendererFamily?: \"react\" | \"rust_family\";\n  /**\n   * 是否为桌面端（Electron）运行。\n   */\n  isDesktop?: boolean;\n  /**\n   * ONLYOFFICE 文档服务器拉取 document.url 时，如需使用 /api/onlyoffice/proxy\n   * 规避 signedUrl 的 token 参数冲突，可把代理地址指向一个“文档服务器可访问”的公网 Origin。\n   *\n   * - 桌面端本地运行（window.location.origin）：通常可不配置\n   * - 通过 Cloudflare Tunnel 使用远端 ONLYOFFICE：建议配置为 https://app.<你的域名>\n   */\n  onlyofficeProxyOrigin?: string;\n  onlyofficeCallbackOrigin?: string;\n};\n\nexport type MnotePublicRuntimeConfig = MnoteRuntimeConfig;\n\ndeclare global {\n  interface Window {\n    __MNOTE_RUNTIME_CONFIG__?: MnoteRuntimeConfig;\n  }\n}\n\nconst parseRuntimeBoolean = (value: unknown): boolean | undefined => {\n  if (typeof value === \"boolean\") {\n    return value;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.ts","s":49,"e":108,"m":1778336284573.613,"t":"export type MnotePublicRuntimeConfig = MnoteRuntimeConfig;\n\ndeclare global {\n  interface Window {\n    __MNOTE_RUNTIME_CONFIG__?: MnoteRuntimeConfig;\n  }\n}\n\nconst parseRuntimeBoolean = (value: unknown): boolean | undefined => {\n  if (typeof value === \"boolean\") {\n    return value;\n  }\n  if (typeof value !== \"string\") {\n    return undefined;\n  }\n  const normalized = value.trim().toLowerCase();\n  if (!normalized) {\n    return undefined;\n  }\n  if ([\"1\", \"true\", \"yes\", \"on\"].includes(normalized)) {\n    return true;\n  }\n  if ([\"0\", \"false\", \"no\", \"off\"].includes(normalized)) {\n    return false;\n  }\n  return undefined;\n};\n\nconst parseDocumentEditorHost = (\n  value: unknown,\n): MnoteRuntimeConfig[\"documentEditorHost\"] | undefined => {\n  if (typeof value !== \"string\") {\n    return undefined;\n  }\n  const normalized = value.trim().toLowerCase();\n  if (!normalized) {\n    return undefined;\n  }\n  if (\n    normalized === \"leptos_tiptap_island\" ||\n    normalized === \"leptos_tiptap_runtime\" ||\n    normalized === \"leptos_tiptap_inline\" ||\n    normalized === \"leptos_tiptap\"\n  ) {\n    return \"leptos_tiptap_island\";\n  }\n  return undefined;\n};\n\nconst parseTreeRendererFamily = (\n  value: unknown,\n): MnoteRuntimeConfig[\"treeRendererFamily\"] | undefined => {\n  if (typeof value !== \"string\") {\n    return undefined;\n  }\n  const normalized = value.trim().toLowerCase();\n  if (!normalized) {\n    return undefined;\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.ts","s":97,"e":156,"m":1778336284573.613,"t":"};\n\nconst parseTreeRendererFamily = (\n  value: unknown,\n): MnoteRuntimeConfig[\"treeRendererFamily\"] | undefined => {\n  if (typeof value !== \"string\") {\n    return undefined;\n  }\n  const normalized = value.trim().toLowerCase();\n  if (!normalized) {\n    return undefined;\n  }\n  if (normalized === \"rust_family\" || normalized === \"rust\" || normalized === \"tree_shell\") {\n    return \"rust_family\";\n  }\n  if (normalized === \"react\") {\n    return \"react\";\n  }\n  return undefined;\n};\n\nfunction getServerNodeBuiltin<T>(moduleName: string): T | null {\n  if (typeof window !== \"undefined\") {\n    return null;\n  }\n\n  const processWithBuiltin = process as NodeJS.Process & {\n    getBuiltinModule?: (id: string) => unknown;\n  };\n  if (typeof processWithBuiltin.getBuiltinModule === \"function\") {\n    const builtIn = processWithBuiltin.getBuiltinModule(moduleName);\n    if (builtIn) {\n      return builtIn as T;\n    }\n  }\n\n  try {\n    // 说明：某些 Node/打包环境仍保留 CommonJS require，这里只作为兜底。\n    const runtimeRequire = new Function(\n      'return typeof require === \"function\" ? require : null',\n    )() as NodeJS.Require | null;\n    if (runtimeRequire) {\n      return runtimeRequire(moduleName) as T;\n    }\n  } catch {\n    return null;\n  }\n\n  return null;\n}\n\nconst readFromEnv = (): MnoteRuntimeConfig => ({\n  useConvex: process.env.USE_CONVEX === \"1\" || process.env.NEXT_PUBLIC_USE_CONVEX === \"1\",\n  supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,\n  supabaseInternalUrl: process.env.SUPABASE_INTERNAL_URL,\n  supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,\n  backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL ?? process.env.BACKEND_URL,   \n  ...(parseDocumentEditorHost(\n    process.env.NEXT_PUBLIC_DOCUMENT_EDITOR_HOST ??\n      process.env.DOCUMENT_EDITOR_HOST,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.ts","s":145,"e":204,"m":1778336284573.613,"t":"return null;\n}\n\nconst readFromEnv = (): MnoteRuntimeConfig => ({\n  useConvex: process.env.USE_CONVEX === \"1\" || process.env.NEXT_PUBLIC_USE_CONVEX === \"1\",\n  supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,\n  supabaseInternalUrl: process.env.SUPABASE_INTERNAL_URL,\n  supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,\n  backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL ?? process.env.BACKEND_URL,   \n  ...(parseDocumentEditorHost(\n    process.env.NEXT_PUBLIC_DOCUMENT_EDITOR_HOST ??\n      process.env.DOCUMENT_EDITOR_HOST,\n  ) !== undefined\n    ? {\n        documentEditorHost: parseDocumentEditorHost(\n          process.env.NEXT_PUBLIC_DOCUMENT_EDITOR_HOST ??\n            process.env.DOCUMENT_EDITOR_HOST,\n        ),\n      }\n    : {}),\n  ...(parseTreeRendererFamily(\n    process.env.NEXT_PUBLIC_TREE_RENDERER_FAMILY ??\n      process.env.TREE_RENDERER_FAMILY,\n  ) !== undefined\n    ? {\n        treeRendererFamily: parseTreeRendererFamily(\n          process.env.NEXT_PUBLIC_TREE_RENDERER_FAMILY ??\n            process.env.TREE_RENDERER_FAMILY,\n        ),\n      }\n    : {}),\n  onlyofficeBaseUrl: process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL,\n  onlyofficeStorageHostOverride: process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE,\n  onlyofficeProxyOrigin: process.env.NEXT_PUBLIC_ONLYOFFICE_PROXY_ORIGIN,\n  onlyofficeCallbackOrigin: process.env.NEXT_PUBLIC_ONLYOFFICE_CALLBACK_ORIGIN,\n  cloudflareAppOrigin: process.env.NEXT_PUBLIC_CLOUDFLARE_APP_ORIGIN,\n});\n\nconst readFromPublicJson = (): Partial<MnoteRuntimeConfig> => {\n  if (typeof window !== \"undefined\") return {};\n  try {\n    // 说明：桌面端与网页端共用 public/mnote-env.json 作为“公共环境文件”。\n    // 桌面端运行时 process.cwd() 会被 Electron 切到 desktop-next 根目录。\n    // 网页端运行时 process.cwd() 通常为 wolai-frontend。\n    \n    // 说明：这里不能直接静态引入 `node:fs` / `node:path`，\n    // 否则客户端 bundle 在解析该模块时会把它们也当成浏览器依赖。\n    // Node 22 优先走 `process.getBuiltinModule`，其余环境再兜底到 runtime require。\n    const fs = getServerNodeBuiltin<typeof import(\"fs\")>(\"node:fs\");\n    const path = getServerNodeBuiltin<typeof import(\"path\")>(\"node:path\");\n    if (!fs || !path) {\n      return {};\n    }\n\n    // 说明：Next standalone 产物的 server.js 会执行 `process.chdir(__dirname)`，\n    // 导致 process.cwd() 变成 `.next/standalone`，此时 public/mnote-env.json 位于上层目录。\n    // 这里向上查找多级目录，确保能读取到真正的 public/mnote-env.json。\n    const candidates: string[] = [];\n    let dir = process.cwd();\n    for (let i = 0; i < 6; i += 1) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.ts","s":193,"e":252,"m":1778336284573.613,"t":"const fs = getServerNodeBuiltin<typeof import(\"fs\")>(\"node:fs\");\n    const path = getServerNodeBuiltin<typeof import(\"path\")>(\"node:path\");\n    if (!fs || !path) {\n      return {};\n    }\n\n    // 说明：Next standalone 产物的 server.js 会执行 `process.chdir(__dirname)`，\n    // 导致 process.cwd() 变成 `.next/standalone`，此时 public/mnote-env.json 位于上层目录。\n    // 这里向上查找多级目录，确保能读取到真正的 public/mnote-env.json。\n    const candidates: string[] = [];\n    let dir = process.cwd();\n    for (let i = 0; i < 6; i += 1) {\n      candidates.push(path.join(dir, \"public\", \"mnote-env.json\"));\n      const next = path.dirname(dir);\n      if (next === dir) break;\n      dir = next;\n    }\n    const existing = candidates.filter((p) => fs.existsSync(p));\n    if (existing.length === 0) return {};\n\n    // 说明：standalone 产物可能包含 `.next/standalone/public/mnote-env.json`，但它通常是构建时拷贝，\n    // 用户更希望修改“项目目录下”的 public/mnote-env.json 即可生效。\n    // 因此这里优先选择不在 `.next` 目录下的配置文件。\n    const isInNextDir = (p: string) => p.split(path.sep).includes(\".next\");\n    const filePath = existing.find((p) => !isInNextDir(p)) ?? existing[0];\n\n    const raw = fs.readFileSync(filePath, { encoding: \"utf8\" });\n    const parsed = JSON.parse(raw) as unknown;\n    if (!parsed || typeof parsed !== \"object\") return {};\n    return parsed as Partial<MnoteRuntimeConfig>;\n  } catch {\n    return {};\n  }\n};\n\nconst normalizeRuntimeConfig = (cfg: MnoteRuntimeConfig): MnoteRuntimeConfig => {\n  const isDesktop =\n    cfg.isDesktop ??\n    (typeof window === \"undefined\" ? process.env.MNOTE_DESKTOP === \"1\" : false);\n\n  const onlyofficeBaseUrl = isDesktop\n    ? cfg.onlyofficeBaseUrlDesktop ||\n      cfg.onlyofficeBaseUrl ||\n      cfg.onlyofficeBaseUrlWeb ||\n      \"\"\n    : cfg.onlyofficeBaseUrlWeb ||\n      cfg.onlyofficeBaseUrl ||\n      cfg.onlyofficeBaseUrlDesktop ||\n      \"\";\n\n  const onlyofficeStorageHostOverride = isDesktop\n    ? (cfg.onlyofficeStorageHostOverrideDesktop ??\n        cfg.onlyofficeStorageHostOverride ??\n        cfg.onlyofficeStorageHostOverrideWeb ??\n        \"\")\n    : (cfg.onlyofficeStorageHostOverrideWeb ??\n        cfg.onlyofficeStorageHostOverride ??\n        cfg.onlyofficeStorageHostOverrideDesktop ??\n        \"\");","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.ts","s":241,"e":300,"m":1778336284573.613,"t":"\"\";\n\n  const onlyofficeStorageHostOverride = isDesktop\n    ? (cfg.onlyofficeStorageHostOverrideDesktop ??\n        cfg.onlyofficeStorageHostOverride ??\n        cfg.onlyofficeStorageHostOverrideWeb ??\n        \"\")\n    : (cfg.onlyofficeStorageHostOverrideWeb ??\n        cfg.onlyofficeStorageHostOverride ??\n        cfg.onlyofficeStorageHostOverrideDesktop ??\n        \"\");\n\n  const onlyofficeProxyOrigin = isDesktop\n    ? (cfg.onlyofficeCallbackOriginDesktop ?? cfg.onlyofficeProxyOrigin ?? cfg.onlyofficeProxyOriginWeb)\n    : (cfg.onlyofficeProxyOriginWeb ?? cfg.onlyofficeProxyOrigin ?? cfg.onlyofficeCallbackOriginDesktop);\n\n  const onlyofficeCallbackOrigin = isDesktop\n    ? (cfg.onlyofficeCallbackOriginDesktop ?? cfg.onlyofficeCallbackOrigin ?? cfg.onlyofficeCallbackOriginWeb)\n    : (cfg.onlyofficeCallbackOriginWeb ?? cfg.onlyofficeCallbackOrigin ?? cfg.onlyofficeCallbackOriginDesktop);\n\n  const documentEditorHost =\n    parseDocumentEditorHost(cfg.documentEditorHost) ?? \"leptos_tiptap_island\";\n  const treeRendererFamily =\n    parseTreeRendererFamily(cfg.treeRendererFamily) ?? \"rust_family\";\n\n  return {\n    ...cfg,\n    isDesktop,\n    documentEditorHost,\n    treeRendererFamily,\n    onlyofficeBaseUrl,\n    onlyofficeStorageHostOverride,\n    onlyofficeProxyOrigin,\n    onlyofficeCallbackOrigin,\n  };\n};\n\nexport function getMnoteRuntimeConfig(): MnoteRuntimeConfig {\n  if (typeof window !== \"undefined\") {\n    // 说明：浏览器侧禁止再兜底读取 NEXT_PUBLIC_MNOTE_WEB_BASE_URL / MNOTE_WEB_BASE_URL，\n    // 否则会把 internal-only 的 mnote-web 边界重新泄漏回客户端。\n    // 客户端只信任服务端注入的 public runtime 配置。\n    return normalizeRuntimeConfig(window.__MNOTE_RUNTIME_CONFIG__ ?? {});\n  }\n  const isDesktop = process.env.MNOTE_DESKTOP === \"1\";\n  const publicRuntime = readFromPublicJson();\n  const envRuntime = readFromEnv();\n  // 说明：桌面端需要优先使用 public/mnote-env.json 来覆盖 build 时注入的 NEXT_PUBLIC_*。\n  // Web 端开发时则应优先使用环境变量（例如本机 http://127.0.0.1:18000），避免被\n  // public/mnote-env.json 中的远程/自签地址覆盖导致浏览器登录请求失败。\n  const merged: MnoteRuntimeConfig = isDesktop\n    ? {\n        ...envRuntime,\n        ...publicRuntime,\n        isDesktop,\n      }\n    : {\n        ...publicRuntime,\n        ...envRuntime,\n      };","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/runtime-config.ts","s":289,"e":307,"m":1778336284573.613,"t":"// Web 端开发时则应优先使用环境变量（例如本机 http://127.0.0.1:18000），避免被\n  // public/mnote-env.json 中的远程/自签地址覆盖导致浏览器登录请求失败。\n  const merged: MnoteRuntimeConfig = isDesktop\n    ? {\n        ...envRuntime,\n        ...publicRuntime,\n        isDesktop,\n      }\n    : {\n        ...publicRuntime,\n        ...envRuntime,\n      };\n  return normalizeRuntimeConfig(merged);\n}\n\nexport function getMnotePublicRuntimeConfig(): MnotePublicRuntimeConfig {\n  return getMnoteRuntimeConfig();\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/rust-web-main-execution-boundary.test.ts","s":1,"e":32,"m":1777367224054.7458,"t":"import { describe, expect, it } from \"vitest\";\nimport { RUST_WEB_MAIN_EXECUTION_BOUNDARY } from \"./rust-web-main-execution-boundary\";\n\ndescribe(\"RUST_WEB_MAIN_EXECUTION_BOUNDARY\", () => {\n  it(\"固定 3000 公开入口由 mnote-web 拥有，Next 只能作为 legacy compat\", () => {\n    expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.publicEntry).toBe(\"3000\");\n    expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.mainWebOwner).toBe(\"mnote-web\");\n    expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.legacyCompatOwner).toBe(\"next-app-router\");\n    expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.nextAllowedDuties).not.toContain(\"main_web_owner\");\n    expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.forbiddenNextSemantics).toContain(\"主 Web gateway owner\");\n  });\n\n  it(\"把主 Web 执行面和允许保留的 React island / legacy bundle 明确分层\", () => {\n    expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.rustOwnedPlanes).toEqual(\n      expect.arrayContaining([\n        \"web_gateway\",\n        \"document_shell\",\n        \"page_aggregate_transport\",\n        \"tree_realtime_stream\",\n        \"search_shell\",\n        \"ai_bridge_transport\",\n        \"mindmap_object_shell\",\n      ]),\n    );\n    expect(RUST_WEB_MAIN_EXECUTION_BOUNDARY.nextAllowedDuties).toEqual([\n      \"legacy_react_bundle\",\n      \"legacy_app_router_compat\",\n      \"island_asset_source_until_migrated\",\n    ]);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/rust-web-main-execution-boundary.ts","s":1,"e":36,"m":1777367210655.5774,"t":"export const RUST_WEB_MAIN_EXECUTION_BOUNDARY = {\n  schema: \"mnote.rust_web.main_execution_boundary.v1\",\n  publicEntry: \"3000\",\n  mainWebOwner: \"mnote-web\",\n  legacyCompatOwner: \"next-app-router\",\n  rustOwnedPlanes: [\n    \"web_gateway\",\n    \"runtime_config\",\n    \"auth_session_handoff\",\n    \"document_shell\",\n    \"page_aggregate_transport\",\n    \"tree_projection_transport\",\n    \"tree_command_transport\",\n    \"tree_realtime_stream\",\n    \"search_shell\",\n    \"ai_bridge_transport\",\n    \"mindmap_object_shell\",\n  ],\n  nextAllowedDuties: [\n    \"legacy_react_bundle\",\n    \"legacy_app_router_compat\",\n    \"island_asset_source_until_migrated\",\n  ],\n  forbiddenNextSemantics: [\n    \"主 Web gateway owner\",\n    \"页面事实源\",\n    \"树排序 canonical plan\",\n    \"Page Aggregate command family owner\",\n    \"tree realtime event contract owner\",\n    \"搜索事实源\",\n    \"AI tool protocol owner\",\n  ],\n} as const;\n\nexport type RustWebMainExecutionBoundary = typeof RUST_WEB_MAIN_EXECUTION_BOUNDARY;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server-cookies.ts","s":1,"e":60,"m":1773495684368.4756,"t":"\"use server\";\n\nimport { cookies } from \"next/headers\";\n\ntype RequestCookies = Awaited<ReturnType<typeof cookies>>;\n\nconst decodeValue = (value?: string) => {\n  if (!value) return value;\n  let v = value;\n  // 说明：部分环境下 cookies() 读取到的值仍是 URL 编码（例如 %5B%22...%22%5D），\n  // Supabase Auth Helpers 期望拿到可直接 JSON.parse 的字符串，因此这里做一次解码。\n  // 若不是合法的 URL 编码字符串，decodeURIComponent 会抛错，我们直接兜底返回原值。\n  if (v.includes(\"%\")) {\n    try {\n      v = decodeURIComponent(v);\n    } catch {\n      // ignore\n    }\n  }\n  return v.startsWith(\"base64-\") ? Buffer.from(v.slice(7), \"base64\").toString(\"utf8\") : v;\n};\n\nconst encodeValue = (value: string) => {\n  if (!value) return value;\n  // Next.js 会自动 base64 编码，我们只需处理已有 base64 前缀的情况\n  if (value.startsWith(\"base64-\")) {\n    return value;\n  }\n  return value;\n};\n\nconst wrapCookies = (store: RequestCookies) => {\n  return {\n    get: (name: string) => {\n      const cookie = store.get(name);\n      if (!cookie) return cookie;\n      return { ...cookie, value: decodeValue(cookie.value) };\n    },\n    getAll: (...args: Parameters<RequestCookies[\"getAll\"]>) =>\n      store.getAll(...args).map((cookie) => ({ ...cookie, value: decodeValue(cookie.value) })),\n    set: (...args: Parameters<RequestCookies[\"set\"]>) => {\n      const [name, value, options] = args as unknown as [\n        unknown,\n        unknown,\n        unknown,\n      ];\n      if (typeof name === \"string\" && typeof value === \"string\") {\n        (store as any).set(name, encodeValue(value), options as any);\n        return;\n      }\n      (store as any).set(...(args as any));\n    },\n    delete: (...args: Parameters<RequestCookies[\"delete\"]>) => store.delete(...args),\n    has: (...args: Parameters<RequestCookies[\"has\"]>) => store.has(...args),\n  };\n};\n\nexport const getDecodedCookies = async () => {\n  const store = await cookies();\n  return wrapCookies(store) as RequestCookies;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server-cookies.ts","s":49,"e":62,"m":1773495684368.4756,"t":"return;\n      }\n      (store as any).set(...(args as any));\n    },\n    delete: (...args: Parameters<RequestCookies[\"delete\"]>) => store.delete(...args),\n    has: (...args: Parameters<RequestCookies[\"has\"]>) => store.has(...args),\n  };\n};\n\nexport const getDecodedCookies = async () => {\n  const store = await cookies();\n  return wrapCookies(store) as RequestCookies;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":1,"e":60,"m":1778339865689.6956,"t":"import { describe, expect, it } from \"vitest\";\nimport type { DocumentRecord } from \"@/lib/documents\";\nimport type { WorkspaceSummary } from \"@/lib/workspaces\";\nimport type { SidebarInitialData } from \"@/components/sidebar/types\";\nimport {\n  buildSidebarDatasetListQueryPayload,\n  buildSidebarDatasetListQueryResult,\n  buildSidebarInitialData,\n  extractMindmapImageAssetIdsFromData,\n  mapSidebarDatasetListQueryResultToInitialData,\n} from \"@/lib/sidebar-data\";\n\ndescribe(\"extractMindmapImageAssetIdsFromData\", () => {\n  it(\"提取导图节点里的 asset 图片引用并去重\", () => {\n    const ids = extractMindmapImageAssetIdsFromData({\n      root: {\n        data: { image: \"asset:img_1\" },\n        children: [\n          { image: { url: \"asset:img_2\" } },\n          { data: { image: { url: \"asset:img_1\" } } },\n        ],\n      },\n    });\n\n    expect(ids).toEqual([\"img_1\", \"img_2\"]);\n  });\n});\n\ndescribe(\"buildSidebarInitialData\", () => {\n  it(\"统一组装 mindmap/table/media 相关侧边栏数据\", () => {\n    const workspaces: WorkspaceSummary[] = [\n      {\n        id: \"ws_1\",\n        name: \"工作区\",\n        type: \"personal\",\n        iconUrl: null,\n        memberCount: 1,\n        isDefault: true,\n      },\n    ];\n\n    const documents: DocumentRecord[] = [\n      {\n        id: \"doc_1\",\n        workspace_id: \"ws_1\",\n        title: \"页面 1\",\n        parent_id: null,\n        sort_order: 1,\n        is_starred: false,\n        access_scope: \"private\",\n        is_template: false,\n        created_at: \"2026-04-14T00:00:00Z\",\n        updated_at: null,\n      },\n    ];\n\n    const trashedDocuments: SidebarInitialData[\"trashedDocuments\"] = [];\n\n    const payload = buildSidebarInitialData({\n      activeWorkspaceId: \"ws_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":49,"e":108,"m":1778339865689.6956,"t":"is_starred: false,\n        access_scope: \"private\",\n        is_template: false,\n        created_at: \"2026-04-14T00:00:00Z\",\n        updated_at: null,\n      },\n    ];\n\n    const trashedDocuments: SidebarInitialData[\"trashedDocuments\"] = [];\n\n    const payload = buildSidebarInitialData({\n      activeWorkspaceId: \"ws_1\",\n      workspaces,\n      documents,\n      trashedDocuments,\n      mediaAssets: [\n        {\n          id: \"asset_file_1\",\n          workspace_id: \"ws_1\",\n          document_id: \"doc_1\",\n          asset_type: \"file\",\n          file_url: \"/file/1\",\n          thumbnail_url: null,\n          bucket: null,\n          storage_path: null,\n          file_name: \"a.txt\",\n          file_size: null,\n          mime_type: \"text/plain\",\n          ocr_payload: undefined,\n          ocr_strategy: null,\n          ocr_text: null,\n          ocr_status: null,\n          signed_url: null,\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: \"2026-04-14T00:00:00Z\",\n        },\n      ],\n      trashedMediaAssets: [],\n      mindmaps: [\n        {\n          mindmap_id: \"mind_1\",\n          workspace_id: \"ws_1\",\n          document_id: \"doc_1\",\n          data: {\n            root: {\n              data: { image: \"asset:img_a\" },\n              children: [{ image: { url: \"asset:img_b\" } }],\n            },\n          },\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: \"2026-04-14T00:00:00Z\",\n          deleted_at: null,\n        },\n        {\n          mindmap_id: \"mind_2\",\n          workspace_id: \"ws_1\",\n          document_id: \"doc_1\",\n          data: null,\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: \"2026-04-14T00:00:00Z\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":97,"e":156,"m":1778339865689.6956,"t":"},\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: \"2026-04-14T00:00:00Z\",\n          deleted_at: null,\n        },\n        {\n          mindmap_id: \"mind_2\",\n          workspace_id: \"ws_1\",\n          document_id: \"doc_1\",\n          data: null,\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: \"2026-04-14T00:00:00Z\",\n          deleted_at: \"2026-04-14T01:00:00Z\",\n          deleted_by: \"user_1\",\n        },\n      ],\n      tables: [\n        {\n          id: \"table_1\",\n          workspace_id: \"ws_1\",\n          document_id: \"doc_1\",\n          title: \"预算\",\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: \"2026-04-14T00:00:00Z\",\n          is_archived: false,\n        },\n        {\n          id: \"table_2\",\n          workspace_id: \"ws_1\",\n          document_id: \"doc_1\",\n          title: \"归档表格\",\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: \"2026-04-14T00:00:00Z\",\n          deleted_at: \"2026-04-14T01:00:00Z\",\n          deleted_by: \"user_1\",\n          purged_at: null,\n          is_archived: true,\n        },\n      ],\n    });\n\n    expect(payload.activeWorkspaceId).toBe(\"ws_1\");\n    expect(payload.kernelSidebarProjection?.projection).toBe(\"sidebar_tree\");\n    expect(payload.kernelFileTreeProjection?.projection).toBe(\"file_tree\");\n    expect(payload.kernelSidebarTree?.map((item) => item.id)).toEqual([\"doc_1\"]);\n    expect(payload.mindmapDocs).toEqual([\"doc_1\"]);\n    expect(payload.mindmapAssetChildren).toEqual({\n      mind_1: [\"img_a\", \"img_b\"],\n    });\n    expect(payload.mindmapAssets?.map((item) => item.id)).toEqual([\"mind_1\"]);\n    expect(payload.trashedMindmapAssets?.map((item) => item.id)).toEqual([\"mind_2\"]);\n    expect(payload.tableAssets?.map((item) => item.file_name)).toEqual([\"预算.luckysheet\"]);\n    expect(payload.trashedTableAssets?.map((item) => item.id)).toEqual([\"table_2\"]);\n    expect(payload.mediaAssets?.map((item) => item.id)).toEqual([\"asset_file_1\"]);\n  });\n\n  it(\"冻结 sidebar.dataset.list 的 Rust query 契约字段\", () => {\n    const queryPayload = buildSidebarDatasetListQueryPayload(\"  ws_1  \");\n    expect(queryPayload).toEqual({ workspace_id: \"ws_1\" });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":145,"e":204,"m":1778339865689.6956,"t":"});\n    expect(payload.mindmapAssets?.map((item) => item.id)).toEqual([\"mind_1\"]);\n    expect(payload.trashedMindmapAssets?.map((item) => item.id)).toEqual([\"mind_2\"]);\n    expect(payload.tableAssets?.map((item) => item.file_name)).toEqual([\"预算.luckysheet\"]);\n    expect(payload.trashedTableAssets?.map((item) => item.id)).toEqual([\"table_2\"]);\n    expect(payload.mediaAssets?.map((item) => item.id)).toEqual([\"asset_file_1\"]);\n  });\n\n  it(\"冻结 sidebar.dataset.list 的 Rust query 契约字段\", () => {\n    const queryPayload = buildSidebarDatasetListQueryPayload(\"  ws_1  \");\n    expect(queryPayload).toEqual({ workspace_id: \"ws_1\" });\n\n    const queryResult = buildSidebarDatasetListQueryResult({\n      activeWorkspaceId: \"ws_1\",\n      workspaces: [\n        {\n          id: \"ws_1\",\n          name: \"工作区\",\n          type: \"personal\",\n          iconUrl: null,\n          memberCount: 1,\n          isDefault: true,\n        },\n      ],\n      documents: [\n        {\n          id: \"doc_1\",\n          workspace_id: \"ws_1\",\n          title: \"页面 1\",\n          parent_id: null,\n          sort_order: 1,\n          is_starred: false,\n          access_scope: \"private\",\n          is_template: false,\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: null,\n        },\n      ],\n      trashedDocuments: [],\n      mindmaps: [],\n      mediaAssets: [],\n      trashedMediaAssets: [],\n      tables: [],\n    });\n\n    expect(queryResult).toMatchObject({\n      active_workspace_id: \"ws_1\",\n      workspaces: [\n        {\n          id: \"ws_1\",\n          name: \"工作区\",\n          type: \"personal\",\n          iconUrl: null,\n          memberCount: 1,\n          isDefault: true,\n        },\n      ],\n      documents: [\n        {\n          id: \"doc_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":193,"e":252,"m":1778339865689.6956,"t":"{\n          id: \"ws_1\",\n          name: \"工作区\",\n          type: \"personal\",\n          iconUrl: null,\n          memberCount: 1,\n          isDefault: true,\n        },\n      ],\n      documents: [\n        {\n          id: \"doc_1\",\n          workspace_id: \"ws_1\",\n          title: \"页面 1\",\n          parent_id: null,\n          sort_order: 1,\n          is_starred: false,\n          access_scope: \"private\",\n          is_template: false,\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: null,\n        },\n      ],\n      kernel_sidebar_projection: {\n        projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n        projection: \"sidebar_tree\",\n        rootNodeId: null,\n        items: [\n          {\n            nodeId: \"doc_1\",\n            parentNodeId: null,\n            nodeType: \"page\",\n            projectionKind: \"sidebar_tree\",\n            title: \"页面 1\",\n            depth: 0,\n            position: 1,\n            childCount: 0,\n            expandable: false,\n            expandedByDefault: true,\n            capabilities: [\n              \"open\",\n              \"drag\",\n              \"drop\",\n              \"select\",\n              \"create-child\",\n              \"rename\",\n              \"archive\",\n              \"restore\",\n              \"context-menu\",\n              \"reorder\",\n            ],\n            resourceMeta: {\n              resourceKind: \"document\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"page\",\n            },\n            iconHint: \"page\",\n          },\n        ],","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":241,"e":300,"m":1778339865689.6956,"t":"\"context-menu\",\n              \"reorder\",\n            ],\n            resourceMeta: {\n              resourceKind: \"document\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"page\",\n            },\n            iconHint: \"page\",\n          },\n        ],\n        edges: [],\n      },\n      kernel_file_tree_projection: {\n        projectionId: \"kernel_projection:file_tree:root\",\n        projection: \"file_tree\",\n        rootNodeId: null,\n        meta: {\n          search: {\n            ordering: \"kernel_file_tree_preorder\",\n          },\n        },\n        items: [\n          {\n            rowId: \"doc:doc_1\",\n            rowKind: \"document\",\n            nodeId: \"doc_1\",\n            parentNodeId: null,\n            nodeType: \"page\",\n            projectionKind: \"file_tree\",\n            title: \"页面 1\",\n            depth: 0,\n            position: 1,\n            childCount: 1,\n            expandable: true,\n            expandedByDefault: true,\n            capabilities: [\n              \"expand\",\n              \"open\",\n              \"drag\",\n              \"drop\",\n              \"select\",\n              \"create-child\",\n              \"rename\",\n              \"archive\",\n              \"restore\",\n              \"context-menu\",\n              \"reorder\",\n            ],\n            resourceMeta: {\n              resourceKind: \"document\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"page\",\n            },\n            iconHint: \"page\",\n          },\n          {\n            rowId: \"index:doc_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":289,"e":348,"m":1778339865689.6956,"t":"\"reorder\",\n            ],\n            resourceMeta: {\n              resourceKind: \"document\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"page\",\n            },\n            iconHint: \"page\",\n          },\n          {\n            rowId: \"index:doc_1\",\n            rowKind: \"index\",\n            nodeId: \"index:doc_1\",\n            parentNodeId: \"doc_1\",\n            nodeType: \"index\",\n            projectionKind: \"file_tree\",\n            title: \"index.md\",\n            depth: 1,\n            position: 0,\n            childCount: 0,\n            expandable: false,\n            expandedByDefault: false,\n            capabilities: [\"open\", \"select\", \"context-menu\"],\n            resourceMeta: {\n              resourceKind: \"index\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"index\",\n            },\n            iconHint: \"index\",\n          },\n        ],\n        edges: [\n          {\n            id: \"edge:doc_1:index:doc_1:parent_of\",\n            edgeType: \"parent_of\",\n            workspaceId: \"ws_1\",\n            fromNodeId: \"doc_1\",\n            toNodeId: \"index:doc_1\",\n          },\n        ],\n      },\n      trashed_documents: [],\n      media_assets: [],\n      trashed_media_assets: [],\n      mindmap_assets: [],\n      trashed_mindmap_assets: [],\n      table_assets: [],\n      trashed_table_assets: [],\n      mindmap_docs: [],\n      mindmap_asset_children: {},\n    });\n\n    expect(mapSidebarDatasetListQueryResultToInitialData(queryResult)).toMatchObject({\n      activeWorkspaceId: \"ws_1\",\n      workspaces: [\n        {\n          id: \"ws_1\",\n          name: \"工作区\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":337,"e":396,"m":1778339865689.6956,"t":"table_assets: [],\n      trashed_table_assets: [],\n      mindmap_docs: [],\n      mindmap_asset_children: {},\n    });\n\n    expect(mapSidebarDatasetListQueryResultToInitialData(queryResult)).toMatchObject({\n      activeWorkspaceId: \"ws_1\",\n      workspaces: [\n        {\n          id: \"ws_1\",\n          name: \"工作区\",\n          type: \"personal\",\n          iconUrl: null,\n          memberCount: 1,\n          isDefault: true,\n        },\n      ],\n      documents: [\n        {\n          id: \"doc_1\",\n          workspace_id: \"ws_1\",\n          title: \"页面 1\",\n          parent_id: null,\n          sort_order: 1,\n          is_starred: false,\n          access_scope: \"private\",\n          is_template: false,\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: null,\n        },\n      ],\n      kernelSidebarProjection: {\n        projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n        projection: \"sidebar_tree\",\n        rootNodeId: null,\n        items: [\n          {\n            nodeId: \"doc_1\",\n            parentNodeId: null,\n            nodeType: \"page\",\n            projectionKind: \"sidebar_tree\",\n            title: \"页面 1\",\n            depth: 0,\n            position: 1,\n            childCount: 0,\n            expandable: false,\n            expandedByDefault: true,\n            capabilities: [\n              \"open\",\n              \"drag\",\n              \"drop\",\n              \"select\",\n              \"create-child\",\n              \"rename\",\n              \"archive\",\n              \"restore\",\n              \"context-menu\",\n              \"reorder\",\n            ],","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":385,"e":444,"m":1778339865689.6956,"t":"capabilities: [\n              \"open\",\n              \"drag\",\n              \"drop\",\n              \"select\",\n              \"create-child\",\n              \"rename\",\n              \"archive\",\n              \"restore\",\n              \"context-menu\",\n              \"reorder\",\n            ],\n            resourceMeta: {\n              resourceKind: \"document\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"page\",\n            },\n            iconHint: \"page\",\n          },\n        ],\n        edges: [],\n      },\n      kernelSidebarTree: [\n        {\n          access_scope: \"private\",\n          id: \"doc_1\",\n          workspace_id: \"ws_1\",\n          title: \"页面 1\",\n          parent_id: null,\n          sort_order: 1,\n          is_starred: false,\n          is_template: false,\n          created_at: \"2026-04-14T00:00:00Z\",\n          updated_at: null,\n          children: [],\n          kernel: {\n            nodeType: \"page\",\n            depth: 0,\n            position: 1,\n            childCount: 0,\n            expandedByDefault: true,\n          },\n        },\n      ],\n      kernelFileTreeProjection: {\n        projectionId: \"kernel_projection:file_tree:root\",\n        projection: \"file_tree\",\n        rootNodeId: null,\n        meta: {\n          search: {\n            ordering: \"kernel_file_tree_preorder\",\n          },\n        },\n        items: [\n          {\n            rowId: \"doc:doc_1\",\n            rowKind: \"document\",\n            nodeId: \"doc_1\",\n            parentNodeId: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":433,"e":492,"m":1778339865689.6956,"t":"rootNodeId: null,\n        meta: {\n          search: {\n            ordering: \"kernel_file_tree_preorder\",\n          },\n        },\n        items: [\n          {\n            rowId: \"doc:doc_1\",\n            rowKind: \"document\",\n            nodeId: \"doc_1\",\n            parentNodeId: null,\n            nodeType: \"page\",\n            projectionKind: \"file_tree\",\n            title: \"页面 1\",\n            depth: 0,\n            position: 1,\n            childCount: 1,\n            expandable: true,\n            expandedByDefault: true,\n            capabilities: [\n              \"expand\",\n              \"open\",\n              \"drag\",\n              \"drop\",\n              \"select\",\n              \"create-child\",\n              \"rename\",\n              \"archive\",\n              \"restore\",\n              \"context-menu\",\n              \"reorder\",\n            ],\n            resourceMeta: {\n              resourceKind: \"document\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"page\",\n            },\n            iconHint: \"page\",\n          },\n          {\n            rowId: \"index:doc_1\",\n            rowKind: \"index\",\n            nodeId: \"index:doc_1\",\n            parentNodeId: \"doc_1\",\n            nodeType: \"index\",\n            projectionKind: \"file_tree\",\n            title: \"index.md\",\n            depth: 1,\n            position: 0,\n            childCount: 0,\n            expandable: false,\n            expandedByDefault: false,\n            capabilities: [\"open\", \"select\", \"context-menu\"],\n            resourceMeta: {\n              resourceKind: \"index\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"index\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":481,"e":540,"m":1778339865689.6956,"t":"title: \"index.md\",\n            depth: 1,\n            position: 0,\n            childCount: 0,\n            expandable: false,\n            expandedByDefault: false,\n            capabilities: [\"open\", \"select\", \"context-menu\"],\n            resourceMeta: {\n              resourceKind: \"index\",\n              documentId: \"doc_1\",\n              workspaceId: \"ws_1\",\n              iconHint: \"index\",\n            },\n            iconHint: \"index\",\n          },\n        ],\n        edges: [\n          {\n            id: \"edge:doc_1:index:doc_1:parent_of\",\n            edgeType: \"parent_of\",\n            workspaceId: \"ws_1\",\n            fromNodeId: \"doc_1\",\n            toNodeId: \"index:doc_1\",\n          },\n        ],\n      },\n      trashedDocuments: [],\n      trashedMediaAssets: [],\n      trashedMindmapAssets: [],\n      trashedTableAssets: [],\n      mindmapDocs: [],\n      mindmapAssets: [],\n      mindmapAssetChildren: {},\n      tableAssets: [],\n      mediaAssets: [],\n    });\n  });\n\n  it(\"缺少 kernel projection 时不再本地重建第二份 projection\", () => {\n    expect(\n      mapSidebarDatasetListQueryResultToInitialData({\n        active_workspace_id: \"ws_1\",\n        workspaces: [],\n        documents: [\n          {\n            id: \"doc_1\",\n            workspace_id: \"ws_1\",\n            title: \"页面 1\",\n            parent_id: null,\n            sort_order: 1,\n            is_starred: false,\n            access_scope: \"private\",\n            is_template: false,\n            created_at: \"2026-04-14T00:00:00Z\",\n            updated_at: null,\n          },\n          {\n            id: \"doc_2\",\n            workspace_id: \"ws_1\",\n            title: \"页面 2\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.test.ts","s":529,"e":572,"m":1778339865689.6956,"t":"parent_id: null,\n            sort_order: 1,\n            is_starred: false,\n            access_scope: \"private\",\n            is_template: false,\n            created_at: \"2026-04-14T00:00:00Z\",\n            updated_at: null,\n          },\n          {\n            id: \"doc_2\",\n            workspace_id: \"ws_1\",\n            title: \"页面 2\",\n            parent_id: \"doc_1\",\n            sort_order: 0,\n            is_starred: false,\n            access_scope: \"private\",\n            is_template: false,\n            created_at: \"2026-04-14T00:01:00Z\",\n            updated_at: null,\n          },\n        ],\n        trashed_documents: [],\n        media_assets: [],\n        trashed_media_assets: [],\n        mindmap_assets: [],\n        trashed_mindmap_assets: [],\n        table_assets: [],\n        trashed_table_assets: [],\n        mindmap_docs: [],\n        mindmap_asset_children: {},\n      }),\n    ).toMatchObject({\n      activeWorkspaceId: \"ws_1\",\n      kernelSidebarProjection: {\n        projectionId: \"kernel_projection:sidebar_tree:missing\",\n      },\n      kernelFileTreeProjection: {\n        projectionId: \"kernel_projection:file_tree:missing\",\n      },\n      kernelSidebarTree: [],\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":1,"e":60,"m":1778856784959.8887,"t":"import type { SidebarInitialData } from \"@/components/sidebar/types\";\nimport type { DocumentRecord } from \"@/lib/documents\";\nimport {\n  buildKernelFileTreeProjection,\n  isKernelFileTreeProjection,\n  type KernelFileTreeProjection,\n} from \"@/lib/kernel-file-tree\";\nimport {\n  buildKernelSidebarProjection as buildProjectionContract,\n  buildSidebarTreeFromKernelProjection,\n  type KernelSidebarProjection,\n} from \"@/lib/kernel-sidebar\";\nimport type { WorkspaceSummary } from \"@/lib/workspaces\";\nimport type { MediaAsset } from \"@/types/media\";\n\ntype MindmapRow = {\n  mindmap_id: string;\n  workspace_id?: string | null;\n  document_id: string;\n  data?: unknown;\n  created_at?: string | null;\n  updated_at?: string | null;\n  deleted_at?: string | null;\n  deleted_by?: string | null;\n};\n\ntype TableRow = {\n  id: string;\n  workspace_id?: string | null;\n  document_id: string;\n  title?: string | null;\n  created_at?: string | null;\n  updated_at?: string | null;\n  deleted_at?: string | null;\n  deleted_by?: string | null;\n  purged_at?: string | null;\n  is_archived?: boolean | null;\n};\n\nexport type SidebarDatasetInput = {\n  activeWorkspaceId: string;\n  workspaces: WorkspaceSummary[];\n  documents: DocumentRecord[];\n  trashedDocuments: SidebarInitialData[\"trashedDocuments\"];\n  mindmaps: MindmapRow[];\n  mediaAssets?: MediaAsset[] | null;\n  trashedMediaAssets?: MediaAsset[] | null;\n  tables?: TableRow[] | null;\n};\n\nexport type SidebarDatasetListQueryPayload = {\n  workspace_id: string;\n};\n\nexport type SidebarDatasetListQueryResult = {\n  active_workspace_id: string;\n  workspaces: WorkspaceSummary[];\n  documents: DocumentRecord[];\n  kernel_file_tree_projection?: KernelFileTreeProjection;\n  kernelFileTreeProjection?: KernelFileTreeProjection;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":49,"e":108,"m":1778856784959.8887,"t":"};\n\nexport type SidebarDatasetListQueryPayload = {\n  workspace_id: string;\n};\n\nexport type SidebarDatasetListQueryResult = {\n  active_workspace_id: string;\n  workspaces: WorkspaceSummary[];\n  documents: DocumentRecord[];\n  kernel_file_tree_projection?: KernelFileTreeProjection;\n  kernelFileTreeProjection?: KernelFileTreeProjection;\n  kernel_sidebar_projection?: KernelSidebarProjection;\n  kernelSidebarProjection?: KernelSidebarProjection;\n  trashed_documents: SidebarInitialData[\"trashedDocuments\"];\n  media_assets: MediaAsset[];\n  trashed_media_assets: MediaAsset[];\n  mindmap_assets: MediaAsset[];\n  trashed_mindmap_assets: MediaAsset[];\n  table_assets: MediaAsset[];\n  trashed_table_assets: MediaAsset[];\n  mindmap_docs: string[];\n  mindmap_asset_children: Record<string, string[]>;\n};\n\nconst EMPTY_KERNEL_SIDEBAR_PROJECTION: KernelSidebarProjection = {\n  projectionId: \"kernel_projection:sidebar_tree:missing\",\n  projection: \"sidebar_tree\",\n  rootNodeId: null,\n  items: [],\n  edges: [],\n};\n\nconst EMPTY_KERNEL_FILE_TREE_PROJECTION: KernelFileTreeProjection = {\n  projectionId: \"kernel_projection:file_tree:missing\",\n  projection: \"file_tree\",\n  rootNodeId: null,\n  items: [],\n  edges: [],\n};\n\nfunction isKernelSidebarProjection(value: unknown): value is KernelSidebarProjection {\n  return (\n    Boolean(value) &&\n    typeof value === \"object\" &&\n    Array.isArray((value as KernelSidebarProjection).items) &&\n    Array.isArray((value as KernelSidebarProjection).edges)\n  );\n}\n\nfunction readKernelSidebarProjection(\n  result: SidebarDatasetListQueryResult,\n): KernelSidebarProjection {\n  const snakeCaseProjection =\n    \"kernel_sidebar_projection\" in result ? result.kernel_sidebar_projection : undefined;\n  if (isKernelSidebarProjection(snakeCaseProjection)) {\n    return snakeCaseProjection;\n  }\n\n  const camelCaseProjection =","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":97,"e":156,"m":1778856784959.8887,"t":"}\n\nfunction readKernelSidebarProjection(\n  result: SidebarDatasetListQueryResult,\n): KernelSidebarProjection {\n  const snakeCaseProjection =\n    \"kernel_sidebar_projection\" in result ? result.kernel_sidebar_projection : undefined;\n  if (isKernelSidebarProjection(snakeCaseProjection)) {\n    return snakeCaseProjection;\n  }\n\n  const camelCaseProjection =\n    \"kernelSidebarProjection\" in result ? result.kernelSidebarProjection : undefined;\n  if (isKernelSidebarProjection(camelCaseProjection)) {\n    return camelCaseProjection;\n  }\n\n  return EMPTY_KERNEL_SIDEBAR_PROJECTION;\n}\n\nfunction readKernelFileTreeProjection(\n  result: SidebarDatasetListQueryResult,\n): KernelFileTreeProjection {\n  const snakeCaseProjection =\n    \"kernel_file_tree_projection\" in result ? result.kernel_file_tree_projection : undefined;\n  if (isKernelFileTreeProjection(snakeCaseProjection)) {\n    return snakeCaseProjection;\n  }\n\n  const camelCaseProjection =\n    \"kernelFileTreeProjection\" in result ? result.kernelFileTreeProjection : undefined;\n  if (isKernelFileTreeProjection(camelCaseProjection)) {\n    return camelCaseProjection;\n  }\n\n  return EMPTY_KERNEL_FILE_TREE_PROJECTION;\n}\n\nfunction normalizeStringArray(values: Iterable<string>): string[] {\n  return Array.from(new Set(values)).filter((value) => value.trim().length > 0);\n}\n\nexport function buildSidebarDatasetListQueryPayload(\n  workspaceId: string,\n): SidebarDatasetListQueryPayload {\n  return {\n    workspace_id: workspaceId.trim(),\n  };\n}\n\nexport function extractMindmapImageAssetIdsFromData(input: unknown): string[] {\n  const root = (() => {\n    if (!input || typeof input !== \"object\") return input;\n    const record = input as Record<string, unknown>;\n    return record && typeof record === \"object\" && \"root\" in record ? record.root : input;\n  })();\n\n  const ids: string[] = [];\n  const seen = new Set<string>();","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":145,"e":204,"m":1778856784959.8887,"t":"}\n\nexport function extractMindmapImageAssetIdsFromData(input: unknown): string[] {\n  const root = (() => {\n    if (!input || typeof input !== \"object\") return input;\n    const record = input as Record<string, unknown>;\n    return record && typeof record === \"object\" && \"root\" in record ? record.root : input;\n  })();\n\n  const ids: string[] = [];\n  const seen = new Set<string>();\n\n  const push = (value: unknown) => {\n    if (typeof value !== \"string\") return;\n    if (!value.startsWith(\"asset:\")) return;\n    const id = value.slice(\"asset:\".length).trim();\n    if (!id || seen.has(id)) return;\n    seen.add(id);\n    ids.push(id);\n  };\n\n  const get = (obj: unknown, key: string): unknown => {\n    if (!obj || typeof obj !== \"object\") return undefined;\n    return (obj as Record<string, unknown>)[key];\n  };\n\n  const walk = (node: unknown) => {\n    if (!node || typeof node !== \"object\") return;\n\n    const data = get(node, \"data\");\n    const image = get(node, \"image\");\n\n    push(get(data, \"image\"));\n    push(image);\n    push(get(image, \"url\"));\n    push(get(get(data, \"image\"), \"url\"));\n\n    const children = get(node, \"children\");\n    if (Array.isArray(children)) {\n      children.forEach(walk);\n    }\n  };\n\n  walk(root);\n  return ids;\n}\n\nfunction toMindmapAsset(row: MindmapRow, workspaceId: string): MediaAsset {\n  const isLegacy = row.mindmap_id.startsWith(\"legacy-\");\n  const suffix = row.mindmap_id.match(/(\\d{4,})$/)?.[1]?.slice(-4) ?? row.mindmap_id.replace(/^mindmap[_-]?/i, \"\").slice(-6);\n  const fileName = suffix ? `思维导图-${suffix}.json` : \"思维导图.json\";\n  return {\n    id: row.mindmap_id,\n    workspace_id: row.workspace_id ?? workspaceId,\n    document_id: row.document_id,\n    asset_type: \"mindmap\",\n    file_url: null,\n    thumbnail_url: null,\n    bucket: null,\n    storage_path: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":193,"e":252,"m":1778856784959.8887,"t":"const isLegacy = row.mindmap_id.startsWith(\"legacy-\");\n  const suffix = row.mindmap_id.match(/(\\d{4,})$/)?.[1]?.slice(-4) ?? row.mindmap_id.replace(/^mindmap[_-]?/i, \"\").slice(-6);\n  const fileName = suffix ? `思维导图-${suffix}.json` : \"思维导图.json\";\n  return {\n    id: row.mindmap_id,\n    workspace_id: row.workspace_id ?? workspaceId,\n    document_id: row.document_id,\n    asset_type: \"mindmap\",\n    file_url: null,\n    thumbnail_url: null,\n    bucket: null,\n    storage_path: null,\n    file_name: isLegacy ? \"思维导图.json\" : fileName,\n    file_size: null,\n    mime_type: \"application/json\",\n    ocr_payload: undefined,\n    ocr_strategy: null,\n    ocr_text: null,\n    ocr_status: null,\n    signed_url: null,\n    created_at: row.created_at ?? \"\",\n    updated_at: row.updated_at ?? \"\",\n  };\n}\n\nfunction toTrashedMindmapAsset(row: MindmapRow, workspaceId: string): MediaAsset {\n  return {\n    ...toMindmapAsset(row, workspaceId),\n    deleted_at: row.deleted_at ?? null,\n    deleted_by: row.deleted_by ?? null,\n    purged_at: null,\n  };\n}\n\nfunction toTableAsset(row: TableRow, workspaceId: string): MediaAsset {\n  const base = String(row.title ?? \"未命名表格\").trim() || \"未命名表格\";\n  const fileName = base.toLowerCase().endsWith(\".luckysheet\") ? base : `${base}.luckysheet`;\n  return {\n    id: row.id,\n    workspace_id: row.workspace_id ?? workspaceId,\n    document_id: row.document_id,\n    asset_type: \"luckysheet\",\n    file_url: null,\n    thumbnail_url: null,\n    bucket: null,\n    storage_path: null,\n    file_name: fileName,\n    file_size: null,\n    mime_type: \"application/json\",\n    ocr_payload: undefined,\n    ocr_strategy: null,\n    ocr_text: null,\n    ocr_status: null,\n    signed_url: null,\n    created_at: row.created_at ?? \"\",\n    updated_at: row.updated_at ?? \"\",\n  };\n}\n\nfunction toTrashedTableAsset(row: TableRow, workspaceId: string): MediaAsset {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":241,"e":300,"m":1778856784959.8887,"t":"mime_type: \"application/json\",\n    ocr_payload: undefined,\n    ocr_strategy: null,\n    ocr_text: null,\n    ocr_status: null,\n    signed_url: null,\n    created_at: row.created_at ?? \"\",\n    updated_at: row.updated_at ?? \"\",\n  };\n}\n\nfunction toTrashedTableAsset(row: TableRow, workspaceId: string): MediaAsset {\n  return {\n    ...toTableAsset(row, workspaceId),\n    deleted_at: row.deleted_at ?? row.updated_at ?? null,\n    deleted_by: row.deleted_by ?? null,\n    purged_at: row.purged_at ?? null,\n  };\n}\n\nfunction deriveSidebarDataset(input: SidebarDatasetInput) {\n  const activeMindmaps = input.mindmaps.filter((row) => !row.deleted_at);\n  const trashedMindmaps = input.mindmaps.filter((row) => Boolean(row.deleted_at));\n  const activeTables = (input.tables ?? []).filter((row) => !row.is_archived);\n  const trashedTables = (input.tables ?? []).filter((row) => Boolean(row.is_archived));\n\n  const mindmapAssetChildren: Record<string, string[]> = {};\n  activeMindmaps.forEach((row) => {\n    const ids = extractMindmapImageAssetIdsFromData(row.data);\n    if (ids.length > 0) {\n      mindmapAssetChildren[row.mindmap_id] = ids;\n    }\n  });\n\n  return {\n    mindmapAssetChildren,\n    mindmapAssets: activeMindmaps.map((row) => toMindmapAsset(row, input.activeWorkspaceId)),\n    mindmapDocs: normalizeStringArray(activeMindmaps.map((row) => row.document_id)),\n    tableAssets: activeTables.map((row) => toTableAsset(row, input.activeWorkspaceId)),\n    trashedMindmapAssets: trashedMindmaps.map((row) =>\n      toTrashedMindmapAsset(row, input.activeWorkspaceId),\n    ),\n    trashedTableAssets: trashedTables.map((row) =>\n      toTrashedTableAsset(row, input.activeWorkspaceId),\n    ),\n  };\n}\n\nexport function buildSidebarDatasetListQueryResult(\n  input: SidebarDatasetInput,\n): SidebarDatasetListQueryResult {\n  const derived = deriveSidebarDataset(input);\n  const kernelSidebarProjection = buildProjectionContract(input.documents);\n  const kernelFileTreeProjection = buildKernelFileTreeProjection({\n    documents: input.documents,\n    mediaAssets: input.mediaAssets,\n    mindmapAssets: derived.mindmapAssets,\n    tableAssets: derived.tableAssets,\n    mindmapAssetChildren: derived.mindmapAssetChildren,\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":289,"e":348,"m":1778856784959.8887,"t":"export function buildSidebarDatasetListQueryResult(\n  input: SidebarDatasetInput,\n): SidebarDatasetListQueryResult {\n  const derived = deriveSidebarDataset(input);\n  const kernelSidebarProjection = buildProjectionContract(input.documents);\n  const kernelFileTreeProjection = buildKernelFileTreeProjection({\n    documents: input.documents,\n    mediaAssets: input.mediaAssets,\n    mindmapAssets: derived.mindmapAssets,\n    tableAssets: derived.tableAssets,\n    mindmapAssetChildren: derived.mindmapAssetChildren,\n  });\n\n  return {\n    active_workspace_id: input.activeWorkspaceId,\n    workspaces: [...input.workspaces],\n    documents: [...input.documents],\n    kernel_file_tree_projection: kernelFileTreeProjection,\n    kernel_sidebar_projection: kernelSidebarProjection,\n    trashed_documents: [...input.trashedDocuments],\n    media_assets: [...(input.mediaAssets ?? [])],\n    trashed_media_assets: [...(input.trashedMediaAssets ?? [])],\n    mindmap_assets: derived.mindmapAssets,\n    trashed_mindmap_assets: derived.trashedMindmapAssets,\n    table_assets: derived.tableAssets,\n    trashed_table_assets: derived.trashedTableAssets,\n    mindmap_docs: derived.mindmapDocs,\n    mindmap_asset_children: { ...derived.mindmapAssetChildren },\n  };\n}\n\nexport function mapSidebarDatasetListQueryResultToInitialData(\n  result: SidebarDatasetListQueryResult,\n): SidebarInitialData {\n  const kernelSidebarProjection = readKernelSidebarProjection(result);\n  const kernelFileTreeProjection = readKernelFileTreeProjection(result);\n\n  return {\n    activeWorkspaceId: result.active_workspace_id,\n    workspaces: [...result.workspaces],\n    documents: [...result.documents],\n    kernelSidebarProjection,\n    kernelSidebarTree: buildSidebarTreeFromKernelProjection({\n      records: result.documents,\n      projection: kernelSidebarProjection,\n    }),\n    kernelFileTreeProjection,\n    trashedDocuments: [...result.trashed_documents],\n    trashedMediaAssets: [...result.trashed_media_assets],\n    trashedMindmapAssets: [...result.trashed_mindmap_assets],\n    trashedTableAssets: [...result.trashed_table_assets],\n    mindmapDocs: [...result.mindmap_docs],\n    mindmapAssets: [...result.mindmap_assets],\n    mindmapAssetChildren: { ...result.mindmap_asset_children },\n    tableAssets: [...result.table_assets],\n    mediaAssets: [...result.media_assets],\n  };\n}\n\nexport function buildSidebarInitialData(input: SidebarDatasetInput): SidebarInitialData {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-data.ts","s":337,"e":353,"m":1778856784959.8887,"t":"trashedMediaAssets: [...result.trashed_media_assets],\n    trashedMindmapAssets: [...result.trashed_mindmap_assets],\n    trashedTableAssets: [...result.trashed_table_assets],\n    mindmapDocs: [...result.mindmap_docs],\n    mindmapAssets: [...result.mindmap_assets],\n    mindmapAssetChildren: { ...result.mindmap_asset_children },\n    tableAssets: [...result.table_assets],\n    mediaAssets: [...result.media_assets],\n  };\n}\n\nexport function buildSidebarInitialData(input: SidebarDatasetInput): SidebarInitialData {\n  const queryResult = buildSidebarDatasetListQueryResult(input);\n\n  return mapSidebarDatasetListQueryResultToInitialData(queryResult);\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-tree.ts","s":1,"e":60,"m":1776339356046.7812,"t":"import type { DocumentRecord } from \"@/lib/documents\";\nimport type { SidebarSectionId, TrashRecord } from \"@/components/sidebar/types\";\nimport type { Database } from \"@/types/supabase\";\nimport type { MediaAsset } from \"@/types/media\";\nimport {\n  buildKernelSidebarProjection,\n  buildSidebarTreeFromKernelProjection,\n  type SidebarTreeNode,\n} from \"@/lib/kernel-sidebar\";\n\ntype TypedClient = {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  from: (table: keyof Database[\"public\"][\"Tables\"] | string) => any;\n};\n\nexport type SidebarTableRow = {\n  id: string;\n  workspace_id: string | null;\n  document_id: string;\n  title: string | null;\n  created_at: string | null;\n  updated_at: string | null;\n};\n\nexport interface SidebarDataset {\n  documents: DocumentRecord[];\n  trashedDocuments: TrashRecord[];\n  trashedMediaAssets: MediaAsset[];\n  /**\n   * 仅依据远端 mindmap_data 判定的页面 id 列表；本地文件检测由服务器侧辅助完成\n   */\n  mindmapDocs: string[];\n  mediaAssets?: MediaAsset[];\n  tables?: SidebarTableRow[];\n}\n\nexport async function fetchSidebarDataset(\n  client: TypedClient,\n  workspaceId: string,\n): Promise<SidebarDataset> {\n  const { data: documentRows, error } = await client\n    .from(\"documents\")\n    .select(\n      \"id,title,parent_id,sort_order,is_starred,access_scope,is_template,created_at,updated_at,workspace_id,mindmap_data\",\n    )\n    .eq(\"workspace_id\", workspaceId)\n    .is(\"deleted_at\", null)\n    .order(\"sort_order\", { ascending: true, nullsFirst: false })\n    .order(\"created_at\", { ascending: true });\n\n  if (error) {\n    throw new Error(`获取工作空间文档列表失败：${error.message}`);\n  }\n\n  const documentRowsAny = (documentRows ?? []) as any[];\n  const documents: DocumentRecord[] = documentRowsAny.map((row) => ({\n    access_scope: (row.access_scope ?? \"private\") as DocumentRecord[\"access_scope\"],\n    id: row.id,\n    workspace_id: row.workspace_id,\n    title: row.title ?? \"无标题\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-tree.ts","s":49,"e":108,"m":1776339356046.7812,"t":".order(\"created_at\", { ascending: true });\n\n  if (error) {\n    throw new Error(`获取工作空间文档列表失败：${error.message}`);\n  }\n\n  const documentRowsAny = (documentRows ?? []) as any[];\n  const documents: DocumentRecord[] = documentRowsAny.map((row) => ({\n    access_scope: (row.access_scope ?? \"private\") as DocumentRecord[\"access_scope\"],\n    id: row.id,\n    workspace_id: row.workspace_id,\n    title: row.title ?? \"无标题\",\n    parent_id: row.parent_id,\n    sort_order: row.sort_order,\n    is_starred: row.is_starred,\n    is_template: row.is_template ?? false,\n    created_at: row.created_at ?? \"\",\n    updated_at: row.updated_at ?? null,\n  }));\n\n  const { data: trashRows, error: trashError } = await client\n    .from(\"documents\")\n    .select(\"id,title,parent_id,deleted_at,access_scope\")\n    .eq(\"workspace_id\", workspaceId)\n    .not(\"deleted_at\", \"is\", null)\n    .order(\"deleted_at\", { ascending: false })\n    .limit(100);\n\n  if (trashError) {\n    throw new Error(`获取垃圾桶内容失败：${trashError.message}`);\n  }\n\n  const trashRowsAny = (trashRows ?? []) as any[];\n  const trashedDocuments: TrashRecord[] = trashRowsAny.map((row) => ({\n    id: row.id,\n    title: row.title,\n    parent_id: row.parent_id,\n    deleted_at: row.deleted_at!,\n    access_scope: (row.access_scope ?? \"private\") as DocumentRecord[\"access_scope\"],\n  }));\n\n  const mindmapDocs = documentRowsAny.filter((row) => row.mindmap_data != null).map((row) => row.id);\n\n  const { data: assetRows, error: assetError } = await client\n    .from(\"media_assets\")\n    .select(\"*\")\n    .eq(\"workspace_id\", workspaceId)\n    .is(\"deleted_at\", null)\n    .order(\"created_at\", { ascending: false });\n\n  if (assetError) {\n    throw new Error(`获取附件列表失败：${assetError.message}`);\n  }\n\n  const mediaAssets: MediaAsset[] = (assetRows ?? []) as MediaAsset[];\n\n  const { data: trashedAssetRows, error: trashedAssetError } = await client\n    .from(\"media_assets\")\n    .select(\"*\")\n    .eq(\"workspace_id\", workspaceId)","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-tree.ts","s":97,"e":156,"m":1776339356046.7812,"t":".order(\"created_at\", { ascending: false });\n\n  if (assetError) {\n    throw new Error(`获取附件列表失败：${assetError.message}`);\n  }\n\n  const mediaAssets: MediaAsset[] = (assetRows ?? []) as MediaAsset[];\n\n  const { data: trashedAssetRows, error: trashedAssetError } = await client\n    .from(\"media_assets\")\n    .select(\"*\")\n    .eq(\"workspace_id\", workspaceId)\n    .not(\"deleted_at\", \"is\", null)\n    .is(\"purged_at\", null)\n    .order(\"deleted_at\", { ascending: false })\n    .limit(200);\n\n  if (trashedAssetError) {\n    throw new Error(`获取附件垃圾桶失败：${trashedAssetError.message}`);\n  }\n\n  const trashedMediaAssets: MediaAsset[] = (trashedAssetRows ?? []) as MediaAsset[];\n\n  // 说明：当前 supabase types 可能未包含 document_tables；这里用 any 兜底，\n  // 避免类型缺失阻塞侧边栏功能。\n  const { data: tableRows, error: tableError } = await (client as any)\n    .from(\"document_tables\")\n    .select(\"id,workspace_id,document_id,title,created_at,updated_at,is_archived\")\n    .eq(\"workspace_id\", workspaceId)\n    .eq(\"is_archived\", false)\n    .order(\"created_at\", { ascending: true });\n\n  if (tableError) {\n    throw new Error(`获取在线表格列表失败：${tableError.message}`);\n  }\n\n  const tables: SidebarTableRow[] = (tableRows ?? []) as unknown as SidebarTableRow[];\n\n  return {\n    documents,\n    trashedDocuments,\n    trashedMediaAssets,\n    mindmapDocs,\n    mediaAssets,\n    tables,\n  };\n}\n\ntype NodePredicate = (node: SidebarTreeNode) => boolean;\n\nfunction projectTree(nodes: SidebarTreeNode[], predicate: NodePredicate): SidebarTreeNode[] {\n  const result: SidebarTreeNode[] = [];\n  nodes.forEach((node) => {\n    const projectedChildren = projectTree(node.children, predicate);\n    if (predicate(node)) {\n      result.push({\n        ...node,\n        children: projectedChildren,\n      });\n    } else {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-tree.ts","s":145,"e":204,"m":1776339356046.7812,"t":"type NodePredicate = (node: SidebarTreeNode) => boolean;\n\nfunction projectTree(nodes: SidebarTreeNode[], predicate: NodePredicate): SidebarTreeNode[] {\n  const result: SidebarTreeNode[] = [];\n  nodes.forEach((node) => {\n    const projectedChildren = projectTree(node.children, predicate);\n    if (predicate(node)) {\n      result.push({\n        ...node,\n        children: projectedChildren,\n      });\n    } else {\n      result.push(...projectedChildren);\n    }\n  });\n  return result;\n}\n\nexport function flattenDocumentTree(\n  nodes: SidebarTreeNode[],\n  expanded: Set<string>,\n  depth = 0,\n  parentId: string | null = null,\n): Array<{ node: SidebarTreeNode; depth: number; parentId: string | null }> {\n  const list: Array<{ node: SidebarTreeNode; depth: number; parentId: string | null }> = [];\n  nodes.forEach((node) => {\n    list.push({ node, depth, parentId });\n    if (node.children.length > 0 && expanded.has(node.id)) {\n      list.push(...flattenDocumentTree(node.children, expanded, depth + 1, node.id));\n    }\n  });\n  return list;\n}\n\nconst SECTION_META: Record<SidebarSectionId, { title: string; icon: string }> = {\n  starred: { title: \"星标置顶\", icon: \"star\" },\n  public: { title: \"公共页面\", icon: \"globe\" },\n  shared: { title: \"共享页面\", icon: \"users\" },\n  private: { title: \"私有 / 我的页面\", icon: \"lock\" },\n  templates: { title: \"模板中心\", icon: \"grid\" },\n};\n\ntype SidebarSectionSnapshot = {\n  id: SidebarSectionId;\n  title: string;\n  icon?: string;\n  nodes: SidebarTreeNode[];\n};\n\nexport function buildSidebarSections(records: DocumentRecord[]): SidebarSectionSnapshot[] {\n  const tree = buildSidebarTreeFromKernelProjection({\n    records,\n    projection: buildKernelSidebarProjection(records),\n  });\n  return buildSidebarSectionsFromTree(tree);\n}\n\nexport function buildSidebarSectionsFromTree(\n  tree: SidebarTreeNode[],\n): SidebarSectionSnapshot[] {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/sidebar-tree.ts","s":193,"e":220,"m":1776339356046.7812,"t":"export function buildSidebarSections(records: DocumentRecord[]): SidebarSectionSnapshot[] {\n  const tree = buildSidebarTreeFromKernelProjection({\n    records,\n    projection: buildKernelSidebarProjection(records),\n  });\n  return buildSidebarSectionsFromTree(tree);\n}\n\nexport function buildSidebarSectionsFromTree(\n  tree: SidebarTreeNode[],\n): SidebarSectionSnapshot[] {\n  const sections: Array<{ id: SidebarSectionId; predicate: NodePredicate }> = [\n    { id: \"starred\", predicate: (node) => Boolean(node.is_starred) },\n    { id: \"public\", predicate: (node) => node.access_scope === \"public\" },\n    { id: \"shared\", predicate: (node) => node.access_scope === \"shared\" },\n    { id: \"private\", predicate: (node) => node.access_scope === \"private\" },\n    { id: \"templates\", predicate: (node) => node.is_template },\n  ];\n\n  return sections.map(({ id, predicate }) => ({\n    id,\n    title: SECTION_META[id].title,\n    icon: SECTION_META[id].icon,\n    nodes: projectTree(tree, predicate),\n  }));\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection-contract.test.ts","s":1,"e":48,"m":1778081112880.5393,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildPageTreeProjectionItems } from \"@/lib/tree-projection\";\n\ndescribe(\"tree-projection contract\", () => {\n  it(\"page_tree projection item 持续输出共享 contract 字段\", () => {\n    const items = buildPageTreeProjectionItems([\n      {\n        access_scope: \"private\",\n        id: \"root\",\n        workspace_id: \"ws_1\",\n        title: \"Root\",\n        parent_id: null,\n        sort_order: 0,\n        is_starred: false,\n        is_template: false,\n        created_at: \"2026-04-18T00:00:00Z\",\n        updated_at: null,\n        children: [],\n      },\n    ]);\n\n    expect(items).toHaveLength(1);\n    expect(items[0]).toMatchObject({\n      rowId: \"page:root\",\n      nodeId: \"root\",\n      parentNodeId: null,\n      projectionKind: \"page_tree\",\n      expandable: false,\n      expandedByDefault: true,\n      iconHint: \"page\",\n      resourceMeta: {\n        resourceKind: \"document\",\n        documentId: \"root\",\n        iconHint: \"page\",\n        extra: {\n          source: {\n            sourceKind: \"convex_workspace\",\n            sourceUri: \"convex://workspace/ws_1/documents/root\",\n            storageIdentity: \"root\",\n            operationProfile: \"convex_workspace\",\n          },\n        },\n      },\n    });\n    expect(items[0]?.capabilities).toContain(\"open\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.test.ts","s":1,"e":60,"m":1776354871424.8655,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  buildPageTreeProjectionItems,\n  buildPickerTreeItems,\n  filterVisiblePageTreeProjectionItems,\n} from \"./tree-projection\";\n\ndescribe(\"buildPageTreeProjectionItems\", () => {\n  it(\"按当前树结构重建 parent/depth，而不是信任陈旧节点字段\", () => {\n    const rows = buildPageTreeProjectionItems([\n      {\n        access_scope: \"private\",\n        id: \"root\",\n        workspace_id: \"w\",\n        title: \"Root\",\n        parent_id: null,\n        sort_order: 0,\n        is_starred: false,\n        is_template: false,\n        created_at: \"\",\n        updated_at: null,\n        kernel: {\n          nodeType: \"page\",\n          depth: 0,\n          position: 0,\n          childCount: 1,\n          expandedByDefault: true,\n        },\n        children: [\n          {\n            access_scope: \"private\",\n            id: \"child\",\n            workspace_id: \"w\",\n            title: \"Child\",\n            parent_id: null,\n            sort_order: 9,\n            is_starred: false,\n            is_template: false,\n            created_at: \"\",\n            updated_at: null,\n            kernel: {\n              nodeType: \"page\",\n              depth: 0,\n              position: 9,\n              childCount: 0,\n              expandedByDefault: true,\n            },\n            children: [],\n          },\n        ],\n      },\n    ]);\n\n    expect(rows.map((item) => ({\n      nodeId: item.nodeId,\n      parentNodeId: item.parentNodeId,\n      depth: item.depth,\n    }))).toEqual([\n      { nodeId: \"root\", parentNodeId: null, depth: 0 },\n      { nodeId: \"child\", parentNodeId: \"root\", depth: 1 },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.test.ts","s":49,"e":108,"m":1776354871424.8655,"t":"},\n        ],\n      },\n    ]);\n\n    expect(rows.map((item) => ({\n      nodeId: item.nodeId,\n      parentNodeId: item.parentNodeId,\n      depth: item.depth,\n    }))).toEqual([\n      { nodeId: \"root\", parentNodeId: null, depth: 0 },\n      { nodeId: \"child\", parentNodeId: \"root\", depth: 1 },\n    ]);\n    expect(rows[1]?.node.parent_id).toBe(\"root\");\n    expect(rows[1]?.node.kernel?.depth).toBe(1);\n  });\n});\n\ndescribe(\"filterVisiblePageTreeProjectionItems\", () => {\n  it(\"只返回当前展开路径上的可见页面\", () => {\n    const items = buildPageTreeProjectionItems([\n      {\n        access_scope: \"private\",\n        id: \"root\",\n        workspace_id: \"w\",\n        title: \"Root\",\n        parent_id: null,\n        sort_order: 0,\n        is_starred: false,\n        is_template: false,\n        created_at: \"\",\n        updated_at: null,\n        children: [\n          {\n            access_scope: \"private\",\n            id: \"child\",\n            workspace_id: \"w\",\n            title: \"Child\",\n            parent_id: \"root\",\n            sort_order: 0,\n            is_starred: false,\n            is_template: false,\n            created_at: \"\",\n            updated_at: null,\n            children: [\n              {\n                access_scope: \"private\",\n                id: \"leaf\",\n                workspace_id: \"w\",\n                title: \"Leaf\",\n                parent_id: \"child\",\n                sort_order: 0,\n                is_starred: false,\n                is_template: false,\n                created_at: \"\",\n                updated_at: null,\n                children: [],\n              },\n            ],\n          },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.test.ts","s":97,"e":156,"m":1776354871424.8655,"t":"workspace_id: \"w\",\n                title: \"Leaf\",\n                parent_id: \"child\",\n                sort_order: 0,\n                is_starred: false,\n                is_template: false,\n                created_at: \"\",\n                updated_at: null,\n                children: [],\n              },\n            ],\n          },\n        ],\n      },\n    ]);\n\n    expect(filterVisiblePageTreeProjectionItems(items, new Set()).map((item) => item.nodeId)).toEqual([\"root\"]);\n    expect(filterVisiblePageTreeProjectionItems(items, new Set([\"root\"])).map((item) => item.nodeId)).toEqual([\n      \"root\",\n      \"child\",\n    ]);\n    expect(\n      filterVisiblePageTreeProjectionItems(items, new Set([\"root\", \"child\"])).map(\n        (item) => item.nodeId,\n      ),\n    ).toEqual([\"root\", \"child\", \"leaf\"]);\n  });\n});\n\ndescribe(\"buildPickerTreeItems\", () => {\n  it(\"复用 page_tree projection 生成 picker 列表\", () => {\n    const items = buildPageTreeProjectionItems([\n      {\n        access_scope: \"private\",\n        id: \"a\",\n        workspace_id: \"w\",\n        title: \"A\",\n        parent_id: null,\n        sort_order: 0,\n        is_starred: false,\n        is_template: false,\n        created_at: \"\",\n        updated_at: null,\n        children: [\n          {\n            access_scope: \"private\",\n            id: \"b\",\n            workspace_id: \"w\",\n            title: null,\n            parent_id: \"a\",\n            sort_order: 0,\n            is_starred: false,\n            is_template: false,\n            created_at: \"\",\n            updated_at: null,\n            children: [],\n          },\n        ],\n      },\n    ]);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.test.ts","s":145,"e":163,"m":1776354871424.8655,"t":"title: null,\n            parent_id: \"a\",\n            sort_order: 0,\n            is_starred: false,\n            is_template: false,\n            created_at: \"\",\n            updated_at: null,\n            children: [],\n          },\n        ],\n      },\n    ]);\n\n    expect(buildPickerTreeItems(items, [\"a\"])).toEqual([\n      { id: \"b\", title: \"无标题\", depth: 1 },\n    ]);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.ts","s":1,"e":60,"m":1778081152793.0762,"t":"import type { SidebarTreeNode } from \"@/lib/kernel-sidebar\";\nimport type {\n  TreeProjectionCapability,\n  TreeProjectionItemBase,\n  TreeProjectionSourceMetadata,\n} from \"@/lib/tree-protocol\";\n\nexport type PageTreeProjectionItem = {\n  rowId: `page:${string}`;\n} & TreeProjectionItemBase & {\n  node: SidebarTreeNode;\n};\n\nexport type PickerTreeItem = {\n  id: string;\n  title: string;\n  depth: number;\n};\n\nfunction buildPageCapabilities(childCount: number): TreeProjectionCapability[] {\n  const capabilities: TreeProjectionCapability[] = [\n    \"open\",\n    \"drag\",\n    \"drop\",\n    \"select\",\n    \"create-child\",\n    \"rename\",\n    \"archive\",\n    \"restore\",\n    \"context-menu\",\n    \"reorder\",\n  ];\n  if (childCount > 0) {\n    capabilities.unshift(\"expand\");\n  }\n  return capabilities;\n}\n\nfunction buildPageSourceMetadata(node: SidebarTreeNode): TreeProjectionSourceMetadata {\n  return {\n    sourceKind: \"convex_workspace\",\n    sourceUri: `convex://workspace/${node.workspace_id}/documents/${node.id}`,\n    relativePath: `documents/${node.id}`,\n    storageIdentity: node.id,\n    operationProfile: \"convex_workspace\",\n  };\n}\n\nexport function buildPageTreeProjectionItems(\n  nodes: SidebarTreeNode[],\n): PageTreeProjectionItem[] {\n  const items: PageTreeProjectionItem[] = [];\n  const visited = new Set<string>();\n\n  const walk = (\n    entries: SidebarTreeNode[],\n    depth: number,\n    parentNodeId: string | null,\n  ) => {\n    entries.forEach((node, index) => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.ts","s":49,"e":108,"m":1778081152793.0762,"t":"export function buildPageTreeProjectionItems(\n  nodes: SidebarTreeNode[],\n): PageTreeProjectionItem[] {\n  const items: PageTreeProjectionItem[] = [];\n  const visited = new Set<string>();\n\n  const walk = (\n    entries: SidebarTreeNode[],\n    depth: number,\n    parentNodeId: string | null,\n  ) => {\n    entries.forEach((node, index) => {\n      if (visited.has(node.id)) {\n        return;\n      }\n      visited.add(node.id);\n\n      const childCount = node.children.length;\n      items.push({\n        rowId: `page:${node.id}`,\n        nodeId: node.id,\n        parentNodeId,\n        nodeType: \"page\",\n        projectionKind: \"page_tree\",\n        depth,\n        position: node.kernel?.position ?? node.sort_order ?? index,\n        title: node.title ?? \"无标题\",\n        childCount,\n        expandable: childCount > 0,\n        expandedByDefault: node.kernel?.expandedByDefault ?? true,\n        capabilities: buildPageCapabilities(childCount),\n        resourceMeta: {\n          resourceKind: \"document\",\n          documentId: node.id,\n          workspaceId: node.workspace_id,\n          iconHint: \"page\",\n          extra: {\n            source: buildPageSourceMetadata(node),\n          },\n        },\n        iconHint: \"page\",\n        node: {\n          ...node,\n          parent_id: parentNodeId,\n          kernel: node.kernel\n            ? {\n                ...node.kernel,\n                depth,\n                position: node.kernel.position ?? node.sort_order ?? index,\n                childCount,\n              }\n            : {\n                nodeType: \"page\",\n                depth,\n                position: node.sort_order ?? index,\n                childCount,\n                expandedByDefault: true,\n              },\n        },\n      });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.ts","s":97,"e":156,"m":1778081152793.0762,"t":"position: node.kernel.position ?? node.sort_order ?? index,\n                childCount,\n              }\n            : {\n                nodeType: \"page\",\n                depth,\n                position: node.sort_order ?? index,\n                childCount,\n                expandedByDefault: true,\n              },\n        },\n      });\n      if (childCount > 0) {\n        walk(node.children, depth + 1, node.id);\n      }\n    });\n  };\n\n  walk(nodes, 0, null);\n  return items;\n}\n\nexport function filterVisiblePageTreeProjectionItems(\n  items: PageTreeProjectionItem[],\n  expanded: Set<string>,\n): PageTreeProjectionItem[] {\n  const visible: PageTreeProjectionItem[] = [];\n  const visibleIds = new Set<string>();\n\n  items.forEach((item) => {\n    if (!item.parentNodeId) {\n      visible.push(item);\n      visibleIds.add(item.nodeId);\n      return;\n    }\n\n    if (!visibleIds.has(item.parentNodeId)) {\n      return;\n    }\n\n    if (!expanded.has(item.parentNodeId)) {\n      return;\n    }\n\n    visible.push(item);\n    visibleIds.add(item.nodeId);\n  });\n\n  return visible;\n}\n\nexport function buildPickerTreeItems(\n  items: PageTreeProjectionItem[],\n  excludeIds: Iterable<string> = [],\n): PickerTreeItem[] {\n  const excluded = new Set(excludeIds);\n  return items\n    .filter((item) => !excluded.has(item.nodeId))\n    .map((item) => ({\n      id: item.nodeId,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-projection.ts","s":145,"e":161,"m":1778081152793.0762,"t":"return visible;\n}\n\nexport function buildPickerTreeItems(\n  items: PageTreeProjectionItem[],\n  excludeIds: Iterable<string> = [],\n): PickerTreeItem[] {\n  const excluded = new Set(excludeIds);\n  return items\n    .filter((item) => !excluded.has(item.nodeId))\n    .map((item) => ({\n      id: item.nodeId,\n      title: item.title,\n      depth: item.depth,\n    }));\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-protocol.test.ts","s":1,"e":60,"m":1778081105120.4348,"t":"import { describe, expect, it } from \"vitest\";\nimport type {\n  TreeProjectionItemBase,\n  WorkspaceSource,\n} from \"@/lib/tree-protocol\";\n\ndescribe(\"tree-protocol\", () => {\n  it(\"冻结 WorkspaceSource 合同字段\", () => {\n    const source: WorkspaceSource = {\n      sourceKind: \"convex_workspace\",\n      rootUri: \"convex://workspace/ws_1\",\n      workspaceId: \"ws_1\",\n      capabilities: [\n        \"load-snapshot\",\n        \"watch\",\n        \"preflight-command\",\n        \"execute-command\",\n        \"resolve-page-aggregate\",\n      ],\n    };\n\n    expect(source.sourceKind).toBe(\"convex_workspace\");\n    expect(source.rootUri).toContain(\"workspace/ws_1\");\n    expect(source.capabilities).toContain(\"load-snapshot\");\n  });\n\n  it(\"冻结共享 tree projection 字段与 file_tree 扩展语义\", () => {\n    const item: TreeProjectionItemBase = {\n      nodeId: \"doc_1\",\n      parentNodeId: null,\n      nodeType: \"page\",\n      projectionKind: \"file_tree\",\n      title: \"页面 1\",\n      depth: 0,\n      position: 1,\n      childCount: 2,\n      expandable: true,\n      expandedByDefault: true,\n      capabilities: [\"expand\", \"open\", \"drag\", \"drop\", \"select\", \"create-child\"],\n      resourceMeta: {\n        resourceKind: \"document\",\n        documentId: \"doc_1\",\n        assetKind: \"file\",\n        iconHint: \"page\",\n        extra: {\n          source: {\n            sourceKind: \"convex_workspace\",\n            sourceUri: \"convex://workspace/ws_1\",\n            relativePath: \"docs/doc_1.md\",\n            storageIdentity: \"doc_1\",\n            operationProfile: \"convex_workspace\",\n          },\n        },\n      },\n      iconHint: \"page\",\n    };\n\n    expect(item.resourceMeta.resourceKind).toBe(\"document\");\n    expect(item.resourceMeta.assetKind).toBe(\"file\");\n    expect(item.resourceMeta.extra?.source).toMatchObject({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-protocol.test.ts","s":49,"e":72,"m":1778081105120.4348,"t":"relativePath: \"docs/doc_1.md\",\n            storageIdentity: \"doc_1\",\n            operationProfile: \"convex_workspace\",\n          },\n        },\n      },\n      iconHint: \"page\",\n    };\n\n    expect(item.resourceMeta.resourceKind).toBe(\"document\");\n    expect(item.resourceMeta.assetKind).toBe(\"file\");\n    expect(item.resourceMeta.extra?.source).toMatchObject({\n      sourceKind: \"convex_workspace\",\n      sourceUri: \"convex://workspace/ws_1\",\n      relativePath: \"docs/doc_1.md\",\n      storageIdentity: \"doc_1\",\n      operationProfile: \"convex_workspace\",\n    });\n    expect(item.capabilities).toContain(\"expand\");\n    expect(item.expandable).toBe(true);\n    expect(item.iconHint).toBe(\"page\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-protocol.ts","s":1,"e":60,"m":1778081084648.1594,"t":"export type TreeProjectionKind = \"sidebar_tree\" | \"page_tree\" | \"file_tree\";\n\nexport type WorkspaceSourceKind = \"local_folder\" | \"convex_workspace\";\n\nexport type WorkspaceSourceCapability =\n  | \"load-snapshot\"\n  | \"watch\"\n  | \"preflight-command\"\n  | \"execute-command\"\n  | \"resolve-page-aggregate\";\n\nexport type WorkspaceSource = {\n  sourceKind: WorkspaceSourceKind;\n  rootUri: string;\n  workspaceId: string;\n  capabilities: WorkspaceSourceCapability[];\n};\n\nexport type TreeProjectionNodeType =\n  | \"workspace\"\n  | \"folder\"\n  | \"page\"\n  | \"section\"\n  | \"asset\"\n  | \"mindmap\"\n  | \"table\"\n  | \"book\"\n  | \"pdf\"\n  | \"index\";\n\nexport type TreeProjectionCapability =\n  | \"expand\"\n  | \"open\"\n  | \"drag\"\n  | \"drop\"\n  | \"select\"\n  | \"create-child\"\n  | \"rename\"\n  | \"archive\"\n  | \"restore\"\n  | \"context-menu\"\n  | \"reorder\"\n  | \"open-asset\"\n  | \"pick\";\n\nexport type TreeProjectionResourceKind =\n  | \"workspace\"\n  | \"document\"\n  | \"index\"\n  | \"asset\"\n  | \"asset_folder\"\n  | \"mindmap\"\n  | \"table\"\n  | \"book\"\n  | \"pdf\";\n\nexport type TreeProjectionAssetKind =\n  | \"file\"\n  | \"mindmap\"\n  | \"table\"","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-protocol.ts","s":49,"e":108,"m":1778081084648.1594,"t":"| \"index\"\n  | \"asset\"\n  | \"asset_folder\"\n  | \"mindmap\"\n  | \"table\"\n  | \"book\"\n  | \"pdf\";\n\nexport type TreeProjectionAssetKind =\n  | \"file\"\n  | \"mindmap\"\n  | \"table\"\n  | \"book\"\n  | \"pdf\"\n  | \"image\"\n  | \"video\"\n  | \"audio\"\n  | \"unknown\";\n\nexport type TreeProjectionSourceOperationProfile =\n  | \"local_readonly\"\n  | \"convex_workspace\";\n\nexport type TreeProjectionSourceMetadata = {\n  sourceKind: WorkspaceSourceKind;\n  sourceUri: string;\n  relativePath?: string | null;\n  storageIdentity?: string | null;\n  operationProfile?: TreeProjectionSourceOperationProfile | null;\n};\n\nexport type TreeProjectionResourceExtra = {\n  source?: TreeProjectionSourceMetadata | null;\n} & Record<string, unknown>;\n\nexport type TreeProjectionResourceMeta = {\n  resourceKind: TreeProjectionResourceKind;\n  documentId?: string | null;\n  assetId?: string | null;\n  workspaceId?: string | null;\n  assetKind?: TreeProjectionAssetKind | null;\n  iconHint?: string | null;\n  extra?: TreeProjectionResourceExtra;\n};\n\nexport type TreeProjectionItemBase = {\n  nodeId: string;\n  parentNodeId: string | null;\n  nodeType: TreeProjectionNodeType;\n  projectionKind: TreeProjectionKind;\n  title: string;\n  depth: number;\n  position: number | null;\n  childCount: number;\n  expandable: boolean;\n  expandedByDefault: boolean;\n  capabilities: TreeProjectionCapability[];\n  resourceMeta: TreeProjectionResourceMeta;\n  iconHint: string | null;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-protocol.ts","s":97,"e":109,"m":1778081084648.1594,"t":"nodeType: TreeProjectionNodeType;\n  projectionKind: TreeProjectionKind;\n  title: string;\n  depth: number;\n  position: number | null;\n  childCount: number;\n  expandable: boolean;\n  expandedByDefault: boolean;\n  capabilities: TreeProjectionCapability[];\n  resourceMeta: TreeProjectionResourceMeta;\n  iconHint: string | null;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-route-boundary.test.ts","s":1,"e":47,"m":1777368206756.113,"t":"import { describe, expect, it } from \"vitest\";\nimport { TREE_3000_ROUTE_BOUNDARY_MANIFEST } from \"./tree-route-boundary\";\n\ndescribe(\"TREE_3000_ROUTE_BOUNDARY_MANIFEST\", () => {\n  it(\"固定 3000 route 的 Rust Web gateway owner 与 legacy compat 边界\", () => {\n    expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST).toMatchObject({\n      schema: \"mnote.tree.3000_route_boundary\",\n      schemaVersion: 1,\n      publicEntry: \"3000\",\n    });\n\n    expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.routes).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          id: \"tree.commands\",\n          role: \"rust-owned\",\n          route: \"/api/tree/commands\",\n          owner: \"rust-web-gateway\",\n        }),\n        expect.objectContaining({\n          id: \"tree.stream\",\n          role: \"rust-owned\",\n          route: \"/api/tree/stream\",\n          owner: \"rust-web-gateway\",\n        }),\n        expect.objectContaining({\n          id: \"tree.shell.debug\",\n          role: \"compat-pending\",\n          route: \"/api/tree/shell\",\n          owner: \"next-app-router-legacy\",\n        }),\n      ]),\n    );\n    expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.rustOwnedSemantics).toContain(\"tree.subtree.move\");\n    expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.nextThinProxyDuties).toContain(\n      \"legacy route alias 与 header 透传\",\n    );\n    expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.browserSubstrateDuties).toContain(\n      \"新页面 scaffold 文件创建\",\n    );\n    expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.forbiddenNextSemantics).toContain(\"树排序 canonical plan\");\n    expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.forbiddenNextSemantics).toContain(\n      \"主 tree command transport owner\",\n    );\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-route-boundary.ts","s":1,"e":60,"m":1777368192731.9355,"t":"export type TreeRouteBoundaryRole =\n  | \"rust-owned\"\n  | \"next-thin-proxy\"\n  | \"legacy-thin-proxy\"\n  | \"browser-substrate\"\n  | \"compat-pending\";\n\nexport type TreeRouteBoundaryItem = {\n  id: string;\n  role: TreeRouteBoundaryRole;\n  route: string;\n  owner: \"rust-runtime\" | \"rust-web-gateway\" | \"next-app-router-legacy\" | \"convex-substrate\";\n  description: string;\n};\n\nexport const TREE_3000_ROUTE_BOUNDARY_MANIFEST = {\n  schema: \"mnote.tree.3000_route_boundary\",\n  schemaVersion: 1,\n  publicEntry: \"3000\",\n  routes: [\n    {\n      id: \"tree.commands\",\n      role: \"rust-owned\",\n      route: \"/api/tree/commands\",\n      owner: \"rust-web-gateway\",\n      description:\n        \"浏览器公开树命令入口由 Rust Web gateway 拥有；legacy Next 仅保留 cookie/header compat 和浏览器 substrate 能力。\",\n    },\n    {\n      id: \"tree.stream\",\n      role: \"rust-owned\",\n      route: \"/api/tree/stream\",\n      owner: \"rust-web-gateway\",\n      description:\n        \"浏览器 SSE/polling 入口由 Rust Web gateway 拥有，legacy Next 只作为迁移期 alias。\",\n    },\n    {\n      id: \"tree.shell.debug\",\n      role: \"compat-pending\",\n      route: \"/api/tree/shell\",\n      owner: \"next-app-router-legacy\",\n      description:\n        \"仅服务显式 debug/internal runtime 验证；3000 主路径使用 same-origin inline host，不应默认请求 mnote-web:3104。\",\n    },\n  ] satisfies TreeRouteBoundaryItem[],\n  rustOwnedSemantics: [\n    \"tree.node.create\",\n    \"tree.node.rename\",\n    \"tree.subtree.move\",\n    \"tree.node.archive\",\n    \"tree.node.restore\",\n    \"tree.node.purge\",\n    \"tree.subtree.copy\",\n    \"tree.node.embed\",\n  ],\n  nextThinProxyDuties: [\n    \"cookie/auth 读取与 Convex client 获取\",\n    \"workspace bootstrap\",\n    \"legacy route alias 与 header 透传\",\n    \"tree.subtree.move 的 sidebar snapshot preflight 数据采集\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-route-boundary.ts","s":49,"e":79,"m":1777368192731.9355,"t":"\"tree.subtree.move\",\n    \"tree.node.archive\",\n    \"tree.node.restore\",\n    \"tree.node.purge\",\n    \"tree.subtree.copy\",\n    \"tree.node.embed\",\n  ],\n  nextThinProxyDuties: [\n    \"cookie/auth 读取与 Convex client 获取\",\n    \"workspace bootstrap\",\n    \"legacy route alias 与 header 透传\",\n    \"tree.subtree.move 的 sidebar snapshot preflight 数据采集\",\n  ],\n  browserSubstrateDuties: [\n    \"新页面 scaffold 文件创建\",\n    \"复制页面后的 mindmap 文件复制\",\n    \"页面嵌入前读取目标内容、源页面标题与 anchor block，作为 Rust Page Aggregate embed plan 的 preflight substrate\",\n    \"文件字节读取、upload URL、cookie/auth 等浏览器入口能力\",\n  ],\n  compatPending: [] satisfies TreeRouteBoundaryItem[],\n  forbiddenNextSemantics: [\n    \"树合法性判断\",\n    \"树排序 canonical plan\",\n    \"长期 streamDelta 主语义拼装\",\n    \"tree shell renderer runtime\",\n    \"tree.node.embed 的 pageReference block 结构与插入位置语义\",\n    \"主 tree command transport owner\",\n    \"主 tree stream transport owner\",\n  ],\n} as const;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/type-guards.ts","s":1,"e":60,"m":1773495684381.4758,"t":"/**\n * 类型守卫和类型断言工具\n * 用于替代 `any` 类型，提供更安全的类型检查\n */\n\n/**\n * 检查值是否为普通对象（非 null、非数组）\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * 检查值是否为字符串\n */\nexport function isString(value: unknown): value is string {\n  return typeof value === \"string\";\n}\n\n/**\n * 检查值是否为数字（有限）\n */\nexport function isFiniteNumber(value: unknown): value is number {\n  return typeof value === \"number\" && Number.isFinite(value);\n}\n\n/**\n * 检查值是否为数组\n */\nexport function isArray<T = unknown>(value: unknown, itemGuard?: (item: unknown) => item is T): value is T[] {\n  if (!Array.isArray(value)) return false;\n  if (itemGuard) {\n    return value.every(itemGuard);\n  }\n  return true;\n}\n\n/**\n * 检查对象是否包含指定的属性\n */\nexport function hasProperty<K extends string>(\n  obj: unknown,\n  key: K,\n): obj is Record<K, unknown> {\n  return isPlainObject(obj) && key in obj;\n}\n\n/**\n * 检查对象是否包含多个指定的属性\n */\nexport function hasProperties<K extends string>(\n  obj: unknown,\n  keys: K[],\n): obj is Record<K, unknown> {\n  if (!isPlainObject(obj)) return false;\n  return keys.every(key => key in obj);\n}\n\n/**\n * 从对象中安全地提取字符串属性","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/type-guards.ts","s":49,"e":108,"m":1773495684381.4758,"t":"* 检查对象是否包含多个指定的属性\n */\nexport function hasProperties<K extends string>(\n  obj: unknown,\n  keys: K[],\n): obj is Record<K, unknown> {\n  if (!isPlainObject(obj)) return false;\n  return keys.every(key => key in obj);\n}\n\n/**\n * 从对象中安全地提取字符串属性\n */\nexport function getStringProperty(obj: unknown, key: string, defaultValue: string = \"\"): string {\n  if (!isPlainObject(obj)) return defaultValue;\n  const value = obj[key];\n  return isString(value) ? value : defaultValue;\n}\n\n/**\n * 从对象中安全地提取数字属性\n */\nexport function getNumberProperty(obj: unknown, key: string, defaultValue: number = 0): number {\n  if (!isPlainObject(obj)) return defaultValue;\n  const value = obj[key];\n  return isFiniteNumber(value) ? value : defaultValue;\n}\n\n/**\n * 从对象中安全地提取布尔属性\n */\nexport function getBooleanProperty(obj: unknown, key: string, defaultValue: boolean = false): boolean {\n  if (!isPlainObject(obj)) return defaultValue;\n  const value = obj[key];\n  return typeof value === \"boolean\" ? value : defaultValue;\n}\n\n/**\n * 从对象中安全地提取数组属性\n */\nexport function getArrayProperty<T = unknown>(\n  obj: unknown,\n  key: string,\n  defaultValue: T[] = [],\n): T[] {\n  if (!isPlainObject(obj)) return defaultValue;\n  const value = obj[key];\n  return Array.isArray(value) ? value as T[] : defaultValue;\n}\n\n/**\n * 检查是否为 Supabase 行对象（包含 id 属性）\n */\nexport function isDatabaseRow(value: unknown): value is { id: string | number; [key: string]: unknown } {\n  return isPlainObject(value) && (\"id\" in value);\n}\n\n/**\n * 检查是否为 Supabase 行数组\n */","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/type-guards.ts","s":97,"e":156,"m":1773495684381.4758,"t":"}\n\n/**\n * 检查是否为 Supabase 行对象（包含 id 属性）\n */\nexport function isDatabaseRow(value: unknown): value is { id: string | number; [key: string]: unknown } {\n  return isPlainObject(value) && (\"id\" in value);\n}\n\n/**\n * 检查是否为 Supabase 行数组\n */\nexport function isDatabaseRowArray(value: unknown): value is Array<{ id: string | number; [key: string]: unknown }> {\n  return isArray(value) && value.every(isDatabaseRow);\n}\n\n/**\n * 类型断言：确保值不为 null/undefined\n */\nexport function assertNotNullOrUndefined<T>(value: T | null | undefined, message?: string): T {\n  if (value === null || value === undefined) {\n    throw new Error(message ?? \"值不能为 null 或 undefined\");\n  }\n  return value;\n}\n\n/**\n * 将 unknown 转换为 Record<string, unknown>，如果类型不匹配则返回空对象\n */\nexport function toRecord(value: unknown): Record<string, unknown> {\n  return isPlainObject(value) ? value : {};\n}\n\n/**\n * 安全地访问嵌套对象属性\n * @example getNestedValue(obj, 'a.b.c') === obj?.a?.b?.c\n */\nexport function getNestedValue<T = unknown>(\n  obj: unknown,\n  path: string,\n  defaultValue?: T,\n): T | undefined {\n  const keys = path.split(\".\");\n  let current: unknown = obj;\n\n  for (const key of keys) {\n    if (!isPlainObject(current)) {\n      return defaultValue;\n    }\n    current = current[key];\n  }\n\n  return current as T ?? defaultValue;\n}\n\n/**\n * 检查响应是否为错误响应\n */\nexport function isErrorResponse(value: unknown): value is { error: string; details?: unknown } {\n  return isPlainObject(value) && isString(value.error);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/type-guards.ts","s":145,"e":188,"m":1773495684381.4758,"t":"}\n    current = current[key];\n  }\n\n  return current as T ?? defaultValue;\n}\n\n/**\n * 检查响应是否为错误响应\n */\nexport function isErrorResponse(value: unknown): value is { error: string; details?: unknown } {\n  return isPlainObject(value) && isString(value.error);\n}\n\n/**\n * 从 unknown 类型中提取工具参数\n * 用于 AI agent 工具调用时的类型安全\n */\nexport function getToolArgs(args: unknown): Record<string, unknown> {\n  if (isPlainObject(args)) {\n    return args;\n  }\n  // 如果是数组或其他类型，返回空对象\n  return {};\n}\n\n/**\n * 检查是否为 AI Agent 消息\n */\nexport function isAgentMessage(value: unknown): value is { role: \"user\" | \"assistant\"; content: string } {\n  return (\n    isPlainObject(value) &&\n    (value.role === \"user\" || value.role === \"assistant\") &&\n    isString(value.content)\n  );\n}\n\n/**\n * 检查是否为 AI Agent 消息数组\n */\nexport function isAgentMessageArray(value: unknown): value is Array<{ role: \"user\" | \"assistant\"; content: string }> {\n  return isArray(value) && value.every(isAgentMessage);\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/utils.ts","s":1,"e":7,"m":1773495684387.4758,"t":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/workspaces.ts","s":1,"e":60,"m":1773495684393.4758,"t":"type TypedClient = {\n  from: (table: string) => any;\n};\n\ninterface WorkspaceMembershipRow {\n  workspace_id: string;\n  is_default: boolean;\n  workspaces:\n    | {\n        id: string;\n        name: string;\n        type: \"personal\" | \"team\";\n        icon_url: string | null;\n      }\n    | Array<{\n        id: string;\n        name: string;\n        type: \"personal\" | \"team\";\n        icon_url: string | null;\n      }>\n    | null;\n}\n\nexport interface WorkspaceSummary {\n  id: string;\n  name: string;\n  type: \"personal\" | \"team\";\n  iconUrl: string | null;\n  memberCount: number;\n  isDefault: boolean;\n}\n\nexport async function ensureDefaultWorkspace(client: TypedClient, userId: string, fallbackName: string): Promise<void> {\n  const { data: memberships, error } = await client\n    .from(\"workspace_members\")\n    .select(\"workspace_id\")\n    .eq(\"user_id\", userId)\n    .limit(1);\n\n  if (error) {\n    throw new Error(`获取工作空间成员信息失败：${error.message}`);\n  }\n\n  if (memberships && memberships.length > 0) {\n    return;\n  }\n\n  const workspaceName = fallbackName.trim() ? `${fallbackName.trim()} 的空间` : \"我的空间\";\n\n  const { data: workspace, error: workspaceError } = await client\n    .from(\"workspaces\")\n    .insert({\n      name: workspaceName,\n      type: \"personal\",\n      created_by: userId,\n    })\n    .select(\"id\")\n    .single();\n\n  if (workspaceError || !workspace) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/workspaces.ts","s":49,"e":108,"m":1773495684393.4758,"t":"const { data: workspace, error: workspaceError } = await client\n    .from(\"workspaces\")\n    .insert({\n      name: workspaceName,\n      type: \"personal\",\n      created_by: userId,\n    })\n    .select(\"id\")\n    .single();\n\n  if (workspaceError || !workspace) {\n    throw new Error(`创建默认工作空间失败：${workspaceError?.message ?? \"未知错误\"}`);\n  }\n\n  const { error: memberError } = await client.from(\"workspace_members\").insert({\n    workspace_id: workspace.id,\n    user_id: userId,\n    role: \"owner\",\n    is_default: true,\n  });\n\n  if (memberError) {\n    throw new Error(`创建工作空间成员失败：${memberError.message}`);\n  }\n}\n\nexport async function fetchWorkspaceSummaries(\n  client: TypedClient,\n  userId: string,\n): Promise<{ workspaces: WorkspaceSummary[]; activeWorkspaceId: string }> {\n  const { data: memberRows, error } = await client\n    .from(\"workspace_members\")\n    .select(\"workspace_id,is_default,workspaces(id,name,type,icon_url)\")\n    .eq(\"user_id\", userId)\n    .order(\"created_at\", { ascending: true });\n\n  if (error) {\n    throw new Error(`拉取工作空间列表失败：${error.message}`);\n  }\n\n  const rows: WorkspaceMembershipRow[] = (memberRows ?? []) as any;\n  const workspaceIds = rows.map((row) => row.workspace_id);\n\n  const memberCountMap: Record<string, number> = {};\n  if (workspaceIds.length > 0) {\n    const { data: memberCounts, error: countError } = await client\n      .from(\"workspace_members\")\n      .select(\"workspace_id\")\n      .in(\"workspace_id\", workspaceIds);\n\n    if (countError) {\n      throw new Error(`统计成员数失败：${countError.message}`);\n    }\n\n    const countRows = (memberCounts ?? []) as Array<{ workspace_id: string }>;\n    countRows.forEach((item) => {\n      const workspaceId = item.workspace_id;\n      memberCountMap[workspaceId] = (memberCountMap[workspaceId] ?? 0) + 1;\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/workspaces.ts","s":97,"e":152,"m":1773495684393.4758,"t":".select(\"workspace_id\")\n      .in(\"workspace_id\", workspaceIds);\n\n    if (countError) {\n      throw new Error(`统计成员数失败：${countError.message}`);\n    }\n\n    const countRows = (memberCounts ?? []) as Array<{ workspace_id: string }>;\n    countRows.forEach((item) => {\n      const workspaceId = item.workspace_id;\n      memberCountMap[workspaceId] = (memberCountMap[workspaceId] ?? 0) + 1;\n    });\n  }\n\n  const summaries: WorkspaceSummary[] = rows\n    .map((row) => {\n      const workspace = Array.isArray(row.workspaces) ? row.workspaces[0] : row.workspaces;\n      if (!workspace) {\n        return null;\n      }\n      return {\n        id: workspace.id,\n        name: workspace.name,\n        type: workspace.type,\n        iconUrl: workspace.icon_url,\n        memberCount: memberCountMap[workspace.id] ?? 1,\n        isDefault: row.is_default,\n      } satisfies WorkspaceSummary;\n    })\n    .filter(Boolean) as WorkspaceSummary[];\n\n  const defaultWorkspace = summaries.find((workspace) => workspace.isDefault);\n  const activeWorkspaceId = defaultWorkspace?.id ?? summaries[0]?.id ?? \"\";\n\n  return {\n    workspaces: summaries,\n    activeWorkspaceId,\n  };\n}\n\nexport async function resolveActiveWorkspaceId(client: TypedClient, userId: string): Promise<string> {\n  const { data, error } = await client\n    .from(\"workspace_members\")\n    .select(\"workspace_id,is_default\")\n    .eq(\"user_id\", userId)\n    .order(\"is_default\", { ascending: false })\n    .order(\"created_at\", { ascending: true })\n    .limit(1);\n\n  if (error) {\n    throw new Error(`获取当前工作空间失败：${error.message}`);\n  }\n\n  return data?.[0]?.workspace_id ?? \"\";\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/url/browser-file-url.ts","s":1,"e":43,"m":1773495901228.865,"t":"\"use client\";\n\nconst isLocalHostname = (hostname: string) =>\n  hostname === \"127.0.0.1\" || hostname === \"localhost\" || hostname === \"host.docker.internal\";\n\nconst base64UrlEncodeUtf8 = (input: string) => {\n  const bytes = new TextEncoder().encode(input);\n  let binary = \"\";\n  bytes.forEach((b) => {\n    binary += String.fromCharCode(b);\n  });\n  const b64 = btoa(binary);\n  return b64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/g, \"\");\n};\n\n/**\n * 将“仅本机可达”的 URL（例如 http://127.0.0.1:3210/api/storage/...）转换为浏览器可访问的地址。\n *\n * - 本地访问（页面本身是 localhost/127）时不改写\n * - 外网访问时，若目标是 localhost/127/host.docker.internal，则改为走 /api/onlyoffice/proxy 由 Next 服务端回源\n */\nexport const toBrowserAccessibleUrl = (rawUrl: string | null | undefined): string | null => {\n  const input = String(rawUrl ?? \"\").trim();\n  if (!input) return rawUrl ?? null;\n  if (typeof window === \"undefined\") return input;\n\n  try {\n    if (input.startsWith(\"/api/onlyoffice/proxy\")) return input;\n    const pageHost = window.location.hostname;\n    if (isLocalHostname(pageHost)) return input;\n\n    const u = new URL(input);\n    if (!isLocalHostname(u.hostname)) return input;\n\n    const proxy = new URL(\"/api/onlyoffice/proxy\", window.location.origin);\n    proxy.searchParams.set(\"u\", base64UrlEncodeUtf8(input));\n    return proxy.toString();\n  } catch {\n    return input;\n  }\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/url/proxyForBrowser.test.ts","s":1,"e":54,"m":1773495901234.865,"t":"import { describe, expect, it } from \"vitest\";\nimport { maybeProxyForBrowserUrl } from \"@/lib/url/proxyForBrowser\";\n\nconst buildReq = () =>\n  new Request(\"https://example.com/api/test\", {\n    headers: {\n      \"x-forwarded-proto\": \"https\",\n      \"x-forwarded-host\": \"app.example.com\",\n    },\n  });\n\nconst base64UrlEncodeUtf8 = (input: string) => {\n  return Buffer.from(input, \"utf8\")\n    .toString(\"base64\")\n    .replace(/\\+/g, \"-\")\n    .replace(/\\//g, \"_\")\n    .replace(/=+$/g, \"\");\n};\n\ndescribe(\"maybeProxyForBrowserUrl\", () => {\n  it(\"对 localhost URL 生成 proxy URL\", () => {\n    const req = buildReq();\n    const raw = \"http://localhost:3210/api/storage/xxx\";\n    const out = maybeProxyForBrowserUrl(req, raw);\n    const expected = `https://app.example.com/api/onlyoffice/proxy?u=${base64UrlEncodeUtf8(raw)}`;\n    expect(out).toBe(expected);\n  });\n\n  it(\"对与 Convex Origin host 匹配的 URL 生成 proxy URL\", () => {\n    process.env.CONVEX_SELF_HOSTED_URL = \"http://backend:3210\";\n    const req = buildReq();\n    const raw = \"http://backend:3210/api/storage/yyy\";\n    const out = maybeProxyForBrowserUrl(req, raw);\n    const expected = `https://app.example.com/api/onlyoffice/proxy?u=${base64UrlEncodeUtf8(raw)}`;\n    expect(out).toBe(expected);\n  });\n\n  it(\"对外部 URL 不做处理\", () => {\n    process.env.CONVEX_SELF_HOSTED_URL = \"http://backend:3210\";\n    const req = buildReq();\n    const raw = \"https://cdn.example.com/image.png\";\n    const out = maybeProxyForBrowserUrl(req, raw);\n    expect(out).toBe(raw);\n  });\n\n  it(\"已经是 /api/onlyoffice/proxy 的 URL 不重复包裹\", () => {\n    const req = buildReq();\n    const raw = \"/api/onlyoffice/proxy?u=abc\";\n    const out = maybeProxyForBrowserUrl(req, raw);\n    expect(out).toBe(raw);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/url/proxyForBrowser.ts","s":1,"e":58,"m":1773495901242.2314,"t":"const base64UrlEncodeUtf8 = (input: string) => {\n  return Buffer.from(input, \"utf8\")\n    .toString(\"base64\")\n    .replace(/\\+/g, \"-\")\n    .replace(/\\//g, \"_\")\n    .replace(/=+$/g, \"\");\n};\n\nconst getRequestOrigin = (request: Request) => {\n  const xfProto = request.headers.get(\"x-forwarded-proto\")?.split(\",\")[0]?.trim();\n  const xfHost = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim();\n  const host = xfHost || request.headers.get(\"host\") || new URL(request.url).host;\n  const protocol = (xfProto || new URL(request.url).protocol.replace(/:$/, \"\")) + \":\";\n  return `${protocol}//${host}`;\n};\n\nconst isLocalHostname = (hostname: string) =>\n  hostname === \"127.0.0.1\" ||\n  hostname === \"localhost\" ||\n  hostname === \"host.docker.internal\" ||\n  hostname === \"0.0.0.0\";\n\nconst getConvexOriginHost = () => {\n  const raw = (process.env.CONVEX_SELF_HOSTED_URL ?? process.env.NEXT_PUBLIC_CONVEX_URL ?? \"\").trim();\n  if (!raw) return null;\n  try {\n    const u = new URL(raw);\n    return u.hostname;\n  } catch {\n    return null;\n  }\n};\n\n/**\n * 将“浏览器不可达”的内部回源 URL 包一层 `/api/onlyoffice/proxy?u=...`，避免前端直接请求内网地址。\n * 说明：该函数只做代理 URL 的生成；真正的 SSRF 防护在 `/api/onlyoffice/proxy` 内完成。\n */\nexport const maybeProxyForBrowserUrl = (request: Request, rawUrl: string) => {\n  const input = String(rawUrl || \"\").trim();\n  if (!input) return input;\n  if (input.startsWith(\"/api/onlyoffice/proxy\")) return input;\n\n  try {\n    const u = new URL(input);\n    const convexHost = getConvexOriginHost();\n    const shouldProxy = isLocalHostname(u.hostname) || (convexHost ? u.hostname === convexHost : false);\n    if (!shouldProxy) return input;\n\n    const origin = getRequestOrigin(request);\n    const proxy = new URL(\"/api/onlyoffice/proxy\", origin);\n    proxy.searchParams.set(\"u\", base64UrlEncodeUtf8(input));\n    return proxy.toString();\n  } catch {\n    return input;\n  }\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/url/rewritePublicOrigin.ts","s":1,"e":41,"m":1773495901249.9724,"t":"export const rewriteToPublicOrigin = (rawUrl: string, publicBaseUrl?: string) => {\n  if (!publicBaseUrl) return rawUrl;\n  const input = String(rawUrl || \"\").trim();\n  if (!input) return input;\n\n  try {\n    const u = new URL(input);\n    const pub = new URL(publicBaseUrl);\n\n    // 说明：如果是 Supabase 的典型路径（storage/auth/rest/functions/realtime），即使来源域名不同\n    // 也统一改为当前“公网可达”的 Supabase origin，避免历史/旧隧道域名导致不可访问。\n    const isSupabasePath =\n      u.pathname.startsWith(\"/storage/v1/\") ||\n      u.pathname.startsWith(\"/auth/v1/\") ||\n      u.pathname.startsWith(\"/rest/v1/\") ||\n      u.pathname.startsWith(\"/functions/v1/\") ||\n      u.pathname.startsWith(\"/realtime/v1/\");\n    if (isSupabasePath && u.host !== pub.host) {\n      u.protocol = pub.protocol;\n      u.host = pub.host;\n      return u.toString();\n    }\n\n    const isLocalHost =\n      u.hostname === \"127.0.0.1\" || u.hostname === \"localhost\" || u.hostname === \"host.docker.internal\";\n    const isLikelyInternalPort = u.port === \"18000\";\n\n    if (!isLocalHost && !isLikelyInternalPort) {\n      return input;\n    }\n\n    // 统一改为公网可达的 supabase origin（协议 + host + 端口）\n    u.protocol = pub.protocol;\n    u.host = pub.host;\n\n    return u.toString();\n  } catch {\n    return input;\n  }\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/protocol.ts","s":1,"e":60,"m":1777368440817.0728,"t":"import type { SidebarInitialData } from \"@/components/sidebar/types\";\nimport {\n  mapSidebarDatasetListQueryResultToInitialData,\n  type SidebarDatasetListQueryResult,\n} from \"@/lib/sidebar-data\";\n\nexport type TreeStreamKind = \"snapshot\" | \"delta\" | \"resync\" | \"heartbeat\";\n\nexport interface TreeStreamEnvelope {\n  stream: string;\n  workspaceId: string | null;\n  rootNodeId: string | null;\n  cursor: string | null;\n  kind: TreeStreamKind;\n  projection: string | null;\n  data: unknown;\n}\n\nconst TREE_STREAM_KINDS = new Set<TreeStreamKind>([\"snapshot\", \"delta\", \"resync\", \"heartbeat\"]);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction readString(value: unknown): string | null {\n  return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : null;\n}\n\nfunction readKind(value: unknown): TreeStreamKind | null {\n  if (typeof value !== \"string\") {\n    return null;\n  }\n  const normalized = value.trim().toLowerCase();\n  return TREE_STREAM_KINDS.has(normalized as TreeStreamKind)\n    ? (normalized as TreeStreamKind)\n    : null;\n}\n\nfunction readNestedPayload(record: Record<string, unknown>): unknown {\n  for (const key of [\"data\", \"payload\", \"snapshot\", \"dataset\", \"sidebar\"]) {\n    if (key in record) {\n      return record[key];\n    }\n  }\n  return record;\n}\n\nfunction looksLikeSidebarInitialData(value: unknown): value is SidebarInitialData {\n  return (\n    isRecord(value) &&\n    typeof value.activeWorkspaceId === \"string\" &&\n    Array.isArray(value.documents)\n  );\n}\n\nfunction looksLikeSidebarDatasetListQueryResult(\n  value: unknown,\n): value is SidebarDatasetListQueryResult {\n  return (\n    isRecord(value) &&","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/protocol.ts","s":49,"e":108,"m":1777368440817.0728,"t":"return (\n    isRecord(value) &&\n    typeof value.activeWorkspaceId === \"string\" &&\n    Array.isArray(value.documents)\n  );\n}\n\nfunction looksLikeSidebarDatasetListQueryResult(\n  value: unknown,\n): value is SidebarDatasetListQueryResult {\n  return (\n    isRecord(value) &&\n    typeof value.active_workspace_id === \"string\" &&\n    Array.isArray(value.documents)\n  );\n}\n\nexport function buildWorkspaceTreeStreamUrl(\n  workspaceId: string,\n  cursor?: string | null,\n): string {\n  const baseOrigin =\n    typeof window !== \"undefined\" ? window.location.origin : \"http://127.0.0.1:3000\";\n  const url = new URL(\"/api/tree/events\", baseOrigin);\n  url.searchParams.set(\"workspaceId\", workspaceId.trim());\n  if (typeof cursor === \"string\" && cursor.trim()) {\n    url.searchParams.set(\"cursor\", cursor.trim());\n  }\n  return url.toString();\n}\n\nexport function parseTreeStreamMessage(input: {\n  rawData: string;\n  eventType?: string | null;\n}): TreeStreamEnvelope | null {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(input.rawData);\n  } catch {\n    return null;\n  }\n\n  if (!isRecord(parsed)) {\n    return null;\n  }\n\n  const kind = readKind(parsed.kind) ?? readKind(input.eventType) ?? readKind(parsed.event);\n  if (!kind) {\n    return null;\n  }\n\n  return {\n    stream: readString(parsed.stream) ?? \"workspace\",\n    workspaceId: readString(parsed.workspaceId) ?? readString(parsed.workspace_id),\n    rootNodeId: readString(parsed.rootNodeId) ?? readString(parsed.root_node_id),\n    cursor: readString(parsed.cursor),\n    kind,\n    projection: readString(parsed.projection),\n    data: readNestedPayload(parsed),\n  };","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/protocol.ts","s":97,"e":134,"m":1777368440817.0728,"t":"return null;\n  }\n\n  return {\n    stream: readString(parsed.stream) ?? \"workspace\",\n    workspaceId: readString(parsed.workspaceId) ?? readString(parsed.workspace_id),\n    rootNodeId: readString(parsed.rootNodeId) ?? readString(parsed.root_node_id),\n    cursor: readString(parsed.cursor),\n    kind,\n    projection: readString(parsed.projection),\n    data: readNestedPayload(parsed),\n  };\n}\n\nexport function normalizeTreeStreamSnapshot(data: unknown): SidebarInitialData | null {\n  if (looksLikeSidebarInitialData(data)) {\n    return data;\n  }\n  if (looksLikeSidebarDatasetListQueryResult(data)) {\n    return mapSidebarDatasetListQueryResultToInitialData(data);\n  }\n  if (!isRecord(data)) {\n    return null;\n  }\n\n  for (const key of [\"snapshot\", \"dataset\", \"sidebar\", \"payload\", \"data\"]) {\n    if (!(key in data)) {\n      continue;\n    }\n    const nested = normalizeTreeStreamSnapshot(data[key]);\n    if (nested) {\n      return nested;\n    }\n  }\n\n  return null;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":1,"e":60,"m":1777163710956.9082,"t":"import { describe, expect, it, vi } from \"vitest\";\nimport { streamTreeFrames } from \"./server\";\nimport type { TreeStreamCommandLogCursorRow } from \"./server\";\n\nfunction buildOverview(rows: TreeStreamCommandLogCursorRow[]) {\n  return {\n    command_logs: rows,\n    domain_events: [],\n    has_more: false,\n    next_cursor: null,\n    generated_at: \"2026-04-24T00:00:00Z\",\n  };\n}\n\nasync function collectFrames<T>(generator: AsyncGenerator<T>) {\n  const frames: T[] = [];\n  for await (const frame of generator) {\n    frames.push(frame);\n  }\n  return frames;\n}\n\ndescribe(\"tree-stream/server\", () => {\n  it(\"workspace scope 首帧应发 snapshot，并固定 sidebar_tree + cursor\", async () => {\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 0,\n        loadOverview: vi.fn().mockResolvedValue(\n          buildOverview([{ id: \"clog_2\", created_at: \"2026-04-24T00:00:01Z\" }]),\n        ),\n        loadSnapshot: vi.fn().mockResolvedValue({\n          requestId: \"req_1\",\n          traceId: \"trace_1\",\n          data: { activeWorkspaceId: \"ws_1\", documents: [] },\n          snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [] }, tree: { items: [] } },\n        }),\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(1);\n    expect(frames[0]).toMatchObject({\n      event: \"snapshot\",\n      payload: {\n        kind: \"snapshot\",\n        stream: \"workspace\",\n        workspaceId: \"ws_1\",\n        rootNodeId: null,\n        projection: \"sidebar_tree\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:01Z\",\n          id: \"clog_2\",\n        }),\n      },\n    });\n  });\n\n  it(\"subtree scope 首帧应切到 subtree + page_tree\", async () => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":49,"e":108,"m":1777163710956.9082,"t":"workspaceId: \"ws_1\",\n        rootNodeId: null,\n        projection: \"sidebar_tree\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:01Z\",\n          id: \"clog_2\",\n        }),\n      },\n    });\n  });\n\n  it(\"subtree scope 首帧应切到 subtree + page_tree\", async () => {\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        rootNodeId: \"page_root\",\n        pollMs: 1,\n        maxPolls: 0,\n        loadOverview: vi.fn().mockResolvedValue(\n          buildOverview([{ id: \"clog_2\", created_at: \"2026-04-24T00:00:01Z\" }]),\n        ),\n        loadSnapshot: vi.fn().mockResolvedValue({\n          requestId: \"req_subtree_1\",\n          traceId: \"trace_subtree_1\",\n          data: { nodes: [] },\n          snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [] }, tree: { nodes: [] } },\n        }),\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(1);\n    expect(frames[0]).toMatchObject({\n      event: \"snapshot\",\n      payload: {\n        kind: \"snapshot\",\n        stream: \"subtree\",\n        workspaceId: \"ws_1\",\n        rootNodeId: \"page_root\",\n        projection: \"page_tree\",\n      },\n    });\n  });\n\n  it(\"检测到 cursor 之后出现新命令时，应发 resync 而不是静默结束\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          { id: \"clog_2\", created_at: \"2026-04-24T00:00:02Z\" },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi\n      .fn()\n      .mockResolvedValueOnce({\n        requestId: \"req_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":97,"e":156,"m":1777163710956.9082,"t":"buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          { id: \"clog_2\", created_at: \"2026-04-24T00:00:02Z\" },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi\n      .fn()\n      .mockResolvedValueOnce({\n        requestId: \"req_1\",\n        traceId: \"trace_1\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n      })\n      .mockResolvedValueOnce({\n        requestId: \"req_2\",\n        traceId: \"trace_2\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }, { id: \"page_2\" }] },\n        snapshot: {\n          dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }, { id: \"page_2\" }] },\n          tree: { items: [] },\n        },\n      });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[0]).toMatchObject({ event: \"snapshot\" });\n    expect(frames[1]).toMatchObject({\n      event: \"resync\",\n      payload: {\n        kind: \"resync\",\n        stream: \"workspace\",\n        workspaceId: \"ws_1\",\n        projection: \"sidebar_tree\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:02Z\",\n          id: \"clog_2\",\n        }),\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(2);\n  });\n\n  it(\"检测到带 streamDelta 的单条新命令时，应直接发 delta\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":145,"e":204,"m":1777163710956.9082,"t":"id: \"clog_2\",\n        }),\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(2);\n  });\n\n  it(\"检测到带 streamDelta 的单条新命令时，应直接发 delta\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          {\n            id: \"clog_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"tree.node.archive\",\n            payload: {\n              documentId: \"page_2\",\n              streamDelta: {\n                op: \"remove_document\",\n                documentId: \"page_2\",\n              },\n            },\n          },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:02Z\",\n          id: \"clog_2\",\n        }),\n        data: {\n          op: \"remove_document\",\n          documentId: \"page_2\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":193,"e":252,"m":1777163710956.9082,"t":"expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:02Z\",\n          id: \"clog_2\",\n        }),\n        data: {\n          op: \"remove_document\",\n          documentId: \"page_2\",\n        },\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"只有 domain event 推进时，也应刷新 cursor 并触发 resync\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce({\n        command_logs: [{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }],\n        domain_events: [\n          {\n            event_id: \"evt_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:02Z\",\n      });\n    const loadSnapshot = vi\n      .fn()\n      .mockResolvedValueOnce({\n        requestId: \"req_1\",\n        traceId: \"trace_1\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n      })\n      .mockResolvedValueOnce({\n        requestId: \"req_2\",\n        traceId: \"trace_2\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n      });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":241,"e":300,"m":1777163710956.9082,"t":"snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n      });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"resync\",\n      payload: {\n        kind: \"resync\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:02Z\",\n          id: \"domain_event:evt_2\",\n        }),\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(2);\n  });\n\n  it(\"单条新 domain event 携带 streamDelta 时，应直接发 delta\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce({\n        command_logs: [],\n        domain_events: [\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:01Z\",\n      })\n      .mockResolvedValueOnce({\n        command_logs: [],\n        domain_events: [\n          {\n            event_id: \"evt_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: {\n              command_name: \"tree.node.rename\",\n              streamDelta: {\n                op: \"upsert_document\",\n                document: {\n                  id: \"page_2\",\n                  title: \"新标题\",\n                },\n              },\n            },\n          },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":289,"e":348,"m":1777163710956.9082,"t":"created_at: \"2026-04-24T00:00:02Z\",\n            payload: {\n              command_name: \"tree.node.rename\",\n              streamDelta: {\n                op: \"upsert_document\",\n                document: {\n                  id: \"page_2\",\n                  title: \"新标题\",\n                },\n              },\n            },\n          },\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:02Z\",\n      });\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:02Z\",\n          id: \"domain_event:evt_2\",\n        }),\n        data: {\n          op: \"upsert_document\",\n          document: {\n            id: \"page_2\",\n            title: \"新标题\",\n          },\n        },\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":337,"e":396,"m":1777163710956.9082,"t":"data: {\n          op: \"upsert_document\",\n          document: {\n            id: \"page_2\",\n            title: \"新标题\",\n          },\n        },\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"单条资源 domain event 携带 upsert_assets 时，应直接发 delta\", async () => {\n    const streamDelta = {\n      op: \"upsert_assets\",\n      upsertAssets: [\n        {\n          id: \"asset_1\",\n          workspace_id: \"ws_1\",\n          document_id: \"doc_target\",\n          asset_type: \"file\",\n          file_url: \"/file.pdf\",\n          thumbnail_url: \"/file.pdf\",\n          file_name: \"file.pdf\",\n          file_size: 1024,\n          mime_type: \"application/pdf\",\n          created_at: \"2026-04-26T00:00:00Z\",\n          updated_at: \"2026-04-26T00:00:00Z\",\n        },\n      ],\n    };\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce({\n        command_logs: [],\n        domain_events: [\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:01Z\",\n      })\n      .mockResolvedValueOnce({\n        command_logs: [],\n        domain_events: [\n          {\n            event_id: \"evt_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: {\n              schema: \"mnote.tree.domain_event\",\n              eventType: \"tree.resource.moved\",\n              streamDelta,\n            },\n          },\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":385,"e":444,"m":1777163710956.9082,"t":"{\n            event_id: \"evt_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: {\n              schema: \"mnote.tree.domain_event\",\n              eventType: \"tree.resource.moved\",\n              streamDelta,\n            },\n          },\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:02Z\",\n      });\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        data: streamDelta,\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"单条新 domain event 缺少可识别 streamDelta 时，应保守回退 resync\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce({\n        command_logs: [],\n        domain_events: [\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":433,"e":492,"m":1777163710956.9082,"t":"const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce({\n        command_logs: [],\n        domain_events: [\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:01Z\",\n      })\n      .mockResolvedValueOnce({\n        command_logs: [],\n        domain_events: [\n          {\n            event_id: \"evt_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: {\n              schema: \"mnote.tree.domain_event\",\n              schemaVersion: 1,\n              eventType: \"tree.node.unknown\",\n            },\n          },\n          {\n            event_id: \"evt_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:02Z\",\n      });\n    const loadSnapshot = vi\n      .fn()\n      .mockResolvedValueOnce({\n        requestId: \"req_1\",\n        traceId: \"trace_1\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n      })\n      .mockResolvedValueOnce({\n        requestId: \"req_2\",\n        traceId: \"trace_2\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_2\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_2\" }] }, tree: { items: [] } },\n      });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":481,"e":540,"m":1777163710956.9082,"t":"});\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"resync\",\n      payload: {\n        kind: \"resync\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:02Z\",\n          id: \"domain_event:evt_2\",\n        }),\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(2);\n  });\n\n  it(\"同一命令的 command log 与 domain event 同时推进且 delta 一致时，应发一次 delta\", async () => {\n    const streamDelta = {\n      op: \"upsert_document\",\n      document: { id: \"page_2\", title: \"同一标题\" },\n    };\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", command_id: \"cmd_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce({\n        command_logs: [\n          {\n            id: \"clog_2\",\n            command_id: \"cmd_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"tree.node.rename\",\n            payload: { streamDelta },\n          },\n          { id: \"clog_1\", command_id: \"cmd_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ],\n        domain_events: [\n          {\n            event_id: \"evt_2\",\n            command_id: \"cmd_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: { streamDelta },\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:02Z\",\n      });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":529,"e":588,"m":1777163710956.9082,"t":"domain_events: [\n          {\n            event_id: \"evt_2\",\n            command_id: \"cmd_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: { streamDelta },\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:02Z\",\n      });\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        cursor: JSON.stringify({\n          createdAt: \"2026-04-24T00:00:02Z\",\n          id: \"clog_2\",\n        }),\n        data: streamDelta,\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"上一帧 cursor 来自 command log 时，应按时间排除旧 domain event 后再做双写去重\", async () => {\n    const streamDelta = {\n      op: \"move_document\",\n      documentId: \"page_2\",\n      parentId: \"page_1\",\n      sortOrder: 2,\n    };\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", command_id: \"cmd_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce({\n        command_logs: [\n          {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":577,"e":636,"m":1777163710956.9082,"t":"documentId: \"page_2\",\n      parentId: \"page_1\",\n      sortOrder: 2,\n    };\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", command_id: \"cmd_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce({\n        command_logs: [\n          {\n            id: \"clog_2\",\n            command_id: \"cmd_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"tree.subtree.move\",\n            payload: { streamDelta },\n          },\n          { id: \"clog_1\", command_id: \"cmd_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ],\n        domain_events: [\n          {\n            event_id: \"evt_2\",\n            command_id: \"cmd_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: { streamDelta },\n          },\n          {\n            event_id: \"evt_1\",\n            command_id: \"cmd_1\",\n            created_at: \"2026-04-24T00:00:01Z\",\n            payload: {\n              streamDelta: {\n                op: \"noop\",\n              },\n            },\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:02Z\",\n      });\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":625,"e":684,"m":1777163710956.9082,"t":"const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        data: streamDelta,\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"command log 与 domain event 同时推进且 delta 不一致时，应回退 resync 避免漏发\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce({\n        command_logs: [\n          {\n            id: \"clog_2\",\n            created_at: \"2026-04-24T00:00:03Z\",\n            command_name: \"tree.node.rename\",\n            payload: {\n              streamDelta: {\n                op: \"upsert_document\",\n                document: { id: \"page_2\", title: \"命令标题\" },\n              },\n            },\n          },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ],\n        domain_events: [\n          {\n            event_id: \"evt_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            payload: {\n              streamDelta: {\n                op: \"remove_document\",\n                documentId: \"page_3\",\n              },\n            },\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:03Z\",\n      });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":673,"e":732,"m":1777163710956.9082,"t":"payload: {\n              streamDelta: {\n                op: \"remove_document\",\n                documentId: \"page_3\",\n              },\n            },\n          },\n        ],\n        has_more: false,\n        next_cursor: null,\n        generated_at: \"2026-04-24T00:00:03Z\",\n      });\n    const loadSnapshot = vi\n      .fn()\n      .mockResolvedValueOnce({\n        requestId: \"req_1\",\n        traceId: \"trace_1\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n      })\n      .mockResolvedValueOnce({\n        requestId: \"req_2\",\n        traceId: \"trace_2\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_2\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_2\" }] }, tree: { items: [] } },\n      });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"resync\",\n      payload: {\n        kind: \"resync\",\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(2);\n  });\n\n  it(\"单条新命令缺少可稳定解释的 streamDelta 时，应回退 resync\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          {\n            id: \"clog_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"tree.subtree.move\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":721,"e":780,"m":1777163710956.9082,"t":"it(\"单条新命令缺少可稳定解释的 streamDelta 时，应回退 resync\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          {\n            id: \"clog_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"tree.subtree.move\",\n            payload: {\n              documentId: \"page_2\",\n            },\n          },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi\n      .fn()\n      .mockResolvedValueOnce({\n        requestId: \"req_1\",\n        traceId: \"trace_1\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n        snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n      })\n      .mockResolvedValueOnce({\n        requestId: \"req_2\",\n        traceId: \"trace_2\",\n        data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }, { id: \"page_2\" }] },\n        snapshot: {\n          dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }, { id: \"page_2\" }] },\n          tree: { items: [] },\n        },\n      });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"resync\",\n      payload: {\n        kind: \"resync\",\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(2);\n  });\n\n  it(\"正文保存这类无树结构影响的命令应降级为 noop delta，而不是触发 resync\", async () => {\n    const loadOverview = vi","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":769,"e":828,"m":1777163710956.9082,"t":"expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"resync\",\n      payload: {\n        kind: \"resync\",\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(2);\n  });\n\n  it(\"正文保存这类无树结构影响的命令应降级为 noop delta，而不是触发 resync\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          {\n            id: \"clog_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"page.body.save\",\n            payload: {\n              documentId: \"page_1\",\n            },\n          },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [{ id: \"page_1\" }] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [{ id: \"page_1\" }] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        data: {\n          op: \"noop\",\n        },\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":817,"e":876,"m":1777163710956.9082,"t":"expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        data: {\n          op: \"noop\",\n        },\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"move 这类附带 move_document 的新命令应直接发 delta，而不是触发 resync\", async () => {\n    const sidebarSnapshot = {\n      activeWorkspaceId: \"ws_1\",\n      workspaces: [],\n      documents: [\n        {\n          id: \"page_1\",\n          workspace_id: \"ws_1\",\n          title: \"页面 1\",\n          parent_id: null,\n          sort_order: 0,\n          access_scope: \"private\",\n          is_starred: false,\n          is_template: false,\n          created_at: \"2026-04-24T00:00:00Z\",\n          updated_at: \"2026-04-24T00:01:00Z\",\n        },\n      ],\n      kernelSidebarProjection: {\n        projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n        projection: \"sidebar_tree\",\n        rootNodeId: null,\n        items: [],\n        edges: [],\n      },\n      kernelFileTreeProjection: {\n        projectionId: \"kernel_projection:file_tree:workspace_root\",\n        projection: \"file_tree\",\n        rootNodeId: null,\n        items: [],\n        edges: [],\n      },\n      kernelSidebarTree: [],\n      trashedDocuments: [],\n      mediaAssets: [],\n      mindmapDocs: [],\n      mindmapAssets: [],\n      mindmapAssetChildren: {},\n      tableAssets: [],\n    };\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":865,"e":924,"m":1777163710956.9082,"t":"mindmapAssets: [],\n      mindmapAssetChildren: {},\n      tableAssets: [],\n    };\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          {\n            id: \"clog_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"tree.subtree.move\",\n            payload: {\n              documentId: \"page_1\",\n              streamDelta: {\n                op: \"move_document\",\n                documentId: \"page_1\",\n                parentId: null,\n                sortOrder: 0,\n                updatedAt: \"2026-04-24T00:01:00Z\",\n              },\n            },\n          },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(2);\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        data: {\n          op: \"move_document\",\n          documentId: \"page_1\",\n          parentId: null,\n          sortOrder: 0,\n          updatedAt: \"2026-04-24T00:01:00Z\",\n        },\n      },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":913,"e":972,"m":1777163710956.9082,"t":"expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        data: {\n          op: \"move_document\",\n          documentId: \"page_1\",\n          parentId: null,\n          sortOrder: 0,\n          updatedAt: \"2026-04-24T00:01:00Z\",\n        },\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"copy 这类附带 upsert_documents 的新命令应保留批量文档字段\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValueOnce(\n        buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]),\n      )\n      .mockResolvedValueOnce(\n        buildOverview([\n          {\n            id: \"clog_2\",\n            created_at: \"2026-04-24T00:00:02Z\",\n            command_name: \"tree.subtree.copy\",\n            payload: {\n              streamDelta: {\n                op: \"upsert_documents\",\n                upsertDocuments: [\n                  {\n                    id: \"copy_1\",\n                    workspace_id: \"ws_1\",\n                    title: \"Copy\",\n                    parent_id: null,\n                    sort_order: 2,\n                    is_starred: false,\n                    access_scope: \"private\",\n                    is_template: false,\n                    created_at: \"2026-04-24T00:00:02Z\",\n                    updated_at: \"2026-04-24T00:00:02Z\",\n                  },\n                ],\n              },\n            },\n          },\n          { id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":961,"e":1020,"m":1777163710956.9082,"t":"{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" },\n        ]),\n      );\n    const loadSnapshot = vi.fn().mockResolvedValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      data: { activeWorkspaceId: \"ws_1\", documents: [] },\n      snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [] }, tree: { items: [] } },\n    });\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot,\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames[1]).toMatchObject({\n      event: \"delta\",\n      payload: {\n        kind: \"delta\",\n        data: {\n          op: \"upsert_documents\",\n          upsertDocuments: [\n            expect.objectContaining({\n              id: \"copy_1\",\n              workspace_id: \"ws_1\",\n            }),\n          ],\n        },\n      },\n    });\n    expect(loadSnapshot).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"轮询期间没有新 cursor 时，不应额外发 resync\", async () => {\n    const loadOverview = vi\n      .fn()\n      .mockResolvedValue(buildOverview([{ id: \"clog_1\", created_at: \"2026-04-24T00:00:01Z\" }]));\n\n    const frames = await collectFrames(\n      streamTreeFrames({\n        workspaceId: \"ws_1\",\n        pollMs: 1,\n        maxPolls: 1,\n        loadOverview,\n        loadSnapshot: vi.fn().mockResolvedValue({\n          requestId: \"req_1\",\n          traceId: \"trace_1\",\n          data: { activeWorkspaceId: \"ws_1\", documents: [] },\n          snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [] }, tree: { items: [] } },\n        }),\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.test.ts","s":1009,"e":1025,"m":1777163710956.9082,"t":"maxPolls: 1,\n        loadOverview,\n        loadSnapshot: vi.fn().mockResolvedValue({\n          requestId: \"req_1\",\n          traceId: \"trace_1\",\n          data: { activeWorkspaceId: \"ws_1\", documents: [] },\n          snapshot: { dataset: { active_workspace_id: \"ws_1\", documents: [] }, tree: { items: [] } },\n        }),\n        sleep: vi.fn().mockResolvedValue(undefined),\n      }),\n    );\n\n    expect(frames).toHaveLength(1);\n    expect(frames[0]).toMatchObject({ event: \"snapshot\" });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":1,"e":60,"m":1777163745580.3542,"t":"import type { TreeStreamDeltaEvent } from \"./tree-delta\";\n\nexport type TreeStreamScope = \"workspace\" | \"subtree\";\nexport type TreeStreamProjection = \"sidebar_tree\" | \"page_tree\";\nexport type TreeStreamEventName = \"snapshot\" | \"delta\" | \"resync\";\n\nexport interface TreeStreamCommandLogCursorRow {\n  id?: string | null;\n  command_id?: string | null;\n  commandId?: string | null;\n  created_at?: string | null;\n  command_name?: string | null;\n  commandName?: string | null;\n  payload?: unknown;\n}\n\nexport interface TreeStreamOverview {\n  command_logs?: TreeStreamCommandLogCursorRow[] | null;\n  domain_events?: unknown[] | null;\n  next_cursor?: string | null;\n  has_more?: boolean | null;\n  generated_at?: string | null;\n}\n\nexport interface TreeStreamSnapshotPayload {\n  requestId: string;\n  traceId: string;\n  data: unknown;\n  snapshot: unknown;\n}\n\nexport interface TreeStreamEnvelope {\n  kind: TreeStreamEventName;\n  stream: TreeStreamScope;\n  workspaceId: string;\n  rootNodeId: string | null;\n  cursor: string | null;\n  projection: TreeStreamProjection;\n  requestId: string;\n  traceId: string;\n  data: unknown;\n  snapshot: unknown;\n  overview: TreeStreamOverview;\n}\n\nexport interface TreeStreamFrame {\n  event: TreeStreamEventName;\n  payload: TreeStreamEnvelope;\n}\n\nexport interface StreamTreeFramesInput {\n  workspaceId: string;\n  rootNodeId?: string | null;\n  initialCursor?: string | null;\n  pollMs?: number;\n  maxPolls?: number | null;\n  loadOverview: () => Promise<TreeStreamOverview>;\n  loadSnapshot: () => Promise<TreeStreamSnapshotPayload>;\n  sleep?: (ms: number) => Promise<void>;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":49,"e":108,"m":1777163745580.3542,"t":"}\n\nexport interface StreamTreeFramesInput {\n  workspaceId: string;\n  rootNodeId?: string | null;\n  initialCursor?: string | null;\n  pollMs?: number;\n  maxPolls?: number | null;\n  loadOverview: () => Promise<TreeStreamOverview>;\n  loadSnapshot: () => Promise<TreeStreamSnapshotPayload>;\n  sleep?: (ms: number) => Promise<void>;\n}\n\ntype DecodedTreeStreamCursor = {\n  createdAt: string;\n  id: string;\n};\n\ntype TreeStreamDomainEventCursorRow = {\n  id?: string | null;\n  event_id?: string | null;\n  command_id?: string | null;\n  commandId?: string | null;\n  created_at?: string | null;\n  createdAt?: string | null;\n  payload?: unknown;\n};\n\nconst TREE_STREAM_NOOP_COMMANDS = new Set([\n  \"page.body.save\",\n  \"page.layout.updateOptions\",\n  \"documents.stats.update\",\n  \"blocks.patch\",\n  \"blocks.move\",\n  \"blocks.embed\",\n]);\n\nfunction normalizeNodeId(value: string | null | undefined) {\n  const normalized = typeof value === \"string\" ? value.trim() : \"\";\n  return normalized || null;\n}\n\nexport function resolveTreeStreamContract(input: {\n  rootNodeId?: string | null;\n}): {\n  stream: TreeStreamScope;\n  projection: TreeStreamProjection;\n  rootNodeId: string | null;\n} {\n  const rootNodeId = normalizeNodeId(input.rootNodeId);\n  if (rootNodeId) {\n    return {\n      stream: \"subtree\",\n      projection: \"page_tree\",\n      rootNodeId,\n    };\n  }\n  return {\n    stream: \"workspace\",\n    projection: \"sidebar_tree\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":97,"e":156,"m":1777163745580.3542,"t":"} {\n  const rootNodeId = normalizeNodeId(input.rootNodeId);\n  if (rootNodeId) {\n    return {\n      stream: \"subtree\",\n      projection: \"page_tree\",\n      rootNodeId,\n    };\n  }\n  return {\n    stream: \"workspace\",\n    projection: \"sidebar_tree\",\n    rootNodeId: null,\n  };\n}\n\nexport function encodeTreeStreamCursor(row: TreeStreamCommandLogCursorRow | null | undefined) {\n  const id = typeof row?.id === \"string\" ? row.id.trim() : \"\";\n  const createdAt = typeof row?.created_at === \"string\" ? row.created_at.trim() : \"\";\n  if (!id || !createdAt) {\n    return null;\n  }\n  return JSON.stringify({\n    createdAt,\n    id,\n  });\n}\n\nfunction encodeTreeStreamDomainEventCursor(\n  row: TreeStreamDomainEventCursorRow | null | undefined,\n) {\n  const rawId =\n    typeof row?.event_id === \"string\"\n      ? row.event_id.trim()\n      : typeof row?.id === \"string\"\n        ? row.id.trim()\n        : \"\";\n  const createdAt =\n    typeof row?.created_at === \"string\"\n      ? row.created_at.trim()\n      : typeof row?.createdAt === \"string\"\n        ? row.createdAt.trim()\n        : \"\";\n  if (!rawId || !createdAt) {\n    return null;\n  }\n  return JSON.stringify({\n    createdAt,\n    id: `domain_event:${rawId}`,\n  });\n}\n\nfunction decodeTreeStreamCursor(raw: string | null | undefined): DecodedTreeStreamCursor | null {\n  if (!raw) {\n    return null;\n  }\n  try {\n    const parsed = JSON.parse(raw) as {\n      createdAt?: string | null;\n      id?: string | null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":145,"e":204,"m":1777163745580.3542,"t":"id: `domain_event:${rawId}`,\n  });\n}\n\nfunction decodeTreeStreamCursor(raw: string | null | undefined): DecodedTreeStreamCursor | null {\n  if (!raw) {\n    return null;\n  }\n  try {\n    const parsed = JSON.parse(raw) as {\n      createdAt?: string | null;\n      id?: string | null;\n    };\n    const createdAt = typeof parsed.createdAt === \"string\" ? parsed.createdAt.trim() : \"\";\n    const id = typeof parsed.id === \"string\" ? parsed.id.trim() : \"\";\n    return createdAt && id ? { createdAt, id } : null;\n  } catch {\n    return null;\n  }\n}\n\nexport function resolveOverviewCursor(\n  overview: TreeStreamOverview,\n  fallback?: string | null,\n) {\n  const commandRows = Array.isArray(overview.command_logs) ? overview.command_logs : [];\n  const eventRows = Array.isArray(overview.domain_events)\n    ? (overview.domain_events as TreeStreamDomainEventCursorRow[])\n    : [];\n  const commandCursor = encodeTreeStreamCursor(commandRows[0] ?? null);\n  const domainEventCursor = encodeTreeStreamDomainEventCursor(eventRows[0] ?? null);\n\n  if (!commandCursor) {\n    return domainEventCursor ?? fallback ?? null;\n  }\n  if (!domainEventCursor) {\n    return commandCursor ?? fallback ?? null;\n  }\n\n  const decodedCommandCursor = decodeTreeStreamCursor(commandCursor);\n  const decodedDomainEventCursor = decodeTreeStreamCursor(domainEventCursor);\n  if (!decodedCommandCursor) {\n    return domainEventCursor;\n  }\n  if (!decodedDomainEventCursor) {\n    return commandCursor;\n  }\n\n  return decodedDomainEventCursor.createdAt > decodedCommandCursor.createdAt\n    ? domainEventCursor\n    : commandCursor;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction readCommandPayloadDelta(row: TreeStreamCommandLogCursorRow): TreeStreamDeltaEvent | null {\n  const commandName =\n    typeof row.command_name === \"string\"","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":193,"e":252,"m":1777163745580.3542,"t":"return decodedDomainEventCursor.createdAt > decodedCommandCursor.createdAt\n    ? domainEventCursor\n    : commandCursor;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction readCommandPayloadDelta(row: TreeStreamCommandLogCursorRow): TreeStreamDeltaEvent | null {\n  const commandName =\n    typeof row.command_name === \"string\"\n      ? row.command_name.trim()\n      : typeof row.commandName === \"string\"\n        ? row.commandName.trim()\n        : \"\";\n  if (commandName && TREE_STREAM_NOOP_COMMANDS.has(commandName)) {\n    return {\n      op: \"noop\",\n    };\n  }\n  if (!isRecord(row.payload) || !(\"streamDelta\" in row.payload)) {\n    return null;\n  }\n  const candidate = row.payload.streamDelta;\n  if (!isRecord(candidate) || typeof candidate.op !== \"string\") {\n    return null;\n  }\n\n  return {\n    op: candidate.op as TreeStreamDeltaEvent[\"op\"],\n    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent[\"node\"]) : null,\n    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent[\"document\"]) : null,\n    documentId: typeof candidate.documentId === \"string\" ? candidate.documentId : null,\n    parentId: typeof candidate.parentId === \"string\" ? candidate.parentId : candidate.parentId === null ? null : undefined,\n    sortOrder:\n      typeof candidate.sortOrder === \"number\" && Number.isFinite(candidate.sortOrder)\n        ? candidate.sortOrder\n        : undefined,\n    updatedAt: typeof candidate.updatedAt === \"string\" ? candidate.updatedAt : null,\n    upsertDocuments: Array.isArray(candidate.upsertDocuments)\n      ? (candidate.upsertDocuments as TreeStreamDeltaEvent[\"upsertDocuments\"])\n      : null,\n    upsertAssets: Array.isArray(candidate.upsertAssets)\n      ? (candidate.upsertAssets as TreeStreamDeltaEvent[\"upsertAssets\"])\n      : null,\n    documents: Array.isArray(candidate.documents) ? (candidate.documents as TreeStreamDeltaEvent[\"documents\"]) : null,\n    sidebar: isRecord(candidate.sidebar) ? (candidate.sidebar as TreeStreamDeltaEvent[\"sidebar\"]) : null,\n  };\n}\n\nfunction readStreamDeltaCandidate(candidate: unknown): TreeStreamDeltaEvent | null {\n  if (!isRecord(candidate) || typeof candidate.op !== \"string\") {\n    return null;\n  }\n\n  return {\n    op: candidate.op as TreeStreamDeltaEvent[\"op\"],\n    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent[\"node\"]) : null,\n    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent[\"document\"]) : null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":241,"e":300,"m":1777163745580.3542,"t":"};\n}\n\nfunction readStreamDeltaCandidate(candidate: unknown): TreeStreamDeltaEvent | null {\n  if (!isRecord(candidate) || typeof candidate.op !== \"string\") {\n    return null;\n  }\n\n  return {\n    op: candidate.op as TreeStreamDeltaEvent[\"op\"],\n    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent[\"node\"]) : null,\n    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent[\"document\"]) : null,\n    documentId: typeof candidate.documentId === \"string\" ? candidate.documentId : null,\n    parentId: typeof candidate.parentId === \"string\" ? candidate.parentId : candidate.parentId === null ? null : undefined,\n    sortOrder:\n      typeof candidate.sortOrder === \"number\" && Number.isFinite(candidate.sortOrder)\n        ? candidate.sortOrder\n        : undefined,\n    updatedAt: typeof candidate.updatedAt === \"string\" ? candidate.updatedAt : null,\n    upsertDocuments: Array.isArray(candidate.upsertDocuments)\n      ? (candidate.upsertDocuments as TreeStreamDeltaEvent[\"upsertDocuments\"])\n      : null,\n    upsertAssets: Array.isArray(candidate.upsertAssets)\n      ? (candidate.upsertAssets as TreeStreamDeltaEvent[\"upsertAssets\"])\n      : null,\n    documents: Array.isArray(candidate.documents) ? (candidate.documents as TreeStreamDeltaEvent[\"documents\"]) : null,\n    sidebar: isRecord(candidate.sidebar) ? (candidate.sidebar as TreeStreamDeltaEvent[\"sidebar\"]) : null,\n  };\n}\n\nfunction readDomainEventPayloadDelta(row: TreeStreamDomainEventCursorRow): TreeStreamDeltaEvent | null {\n  if (!isRecord(row.payload)) {\n    return null;\n  }\n  return readStreamDeltaCandidate(row.payload.streamDelta ?? row.payload.stream_delta);\n}\n\nfunction readCommandRowCommandId(row: TreeStreamCommandLogCursorRow): string | null {\n  const raw =\n    typeof row.command_id === \"string\"\n      ? row.command_id\n      : typeof row.commandId === \"string\"\n        ? row.commandId\n        : typeof row.id === \"string\"\n          ? row.id\n          : \"\";\n  const trimmed = raw.trim();\n  return trimmed || null;\n}\n\nfunction readDomainEventCommandId(row: TreeStreamDomainEventCursorRow): string | null {\n  const raw =\n    typeof row.command_id === \"string\"\n      ? row.command_id\n      : typeof row.commandId === \"string\"\n        ? row.commandId\n        : \"\";\n  const trimmed = raw.trim();\n  return trimmed || null;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":289,"e":348,"m":1777163745580.3542,"t":"}\n\nfunction readDomainEventCommandId(row: TreeStreamDomainEventCursorRow): string | null {\n  const raw =\n    typeof row.command_id === \"string\"\n      ? row.command_id\n      : typeof row.commandId === \"string\"\n        ? row.commandId\n        : \"\";\n  const trimmed = raw.trim();\n  return trimmed || null;\n}\n\nfunction resolveMatchingCommandDomainEventDelta(input: {\n  commandRows: TreeStreamCommandLogCursorRow[];\n  domainEventRows: TreeStreamDomainEventCursorRow[];\n  commandDrifted: boolean;\n  domainEventDrifted: boolean;\n}): TreeStreamDeltaEvent | null {\n  if (\n    input.commandDrifted ||\n    input.commandRows.length !== 1 ||\n    input.domainEventRows.length !== 1\n  ) {\n    return null;\n  }\n\n  const commandId = readCommandRowCommandId(input.commandRows[0] ?? {});\n  const eventCommandId = readDomainEventCommandId(input.domainEventRows[0] ?? {});\n  if (!commandId || commandId !== eventCommandId) {\n    return null;\n  }\n\n  const commandDelta = readCommandPayloadDelta(input.commandRows[0] ?? {});\n  const eventDelta = readDomainEventPayloadDelta(input.domainEventRows[0] ?? {});\n  if (!commandDelta || !eventDelta) {\n    return null;\n  }\n\n  return JSON.stringify(commandDelta) === JSON.stringify(eventDelta) ? eventDelta : null;\n}\n\nfunction collectNewCommandLogs(input: {\n  rows: TreeStreamCommandLogCursorRow[];\n  previousCursor: string | null;\n}) {\n  const previousCursor = decodeTreeStreamCursor(input.previousCursor);\n  if (!previousCursor) {\n    return {\n      rows: input.rows,\n      drifted: false,\n    };\n  }\n\n  const previousIndex = input.rows.findIndex((row) => {\n    const id = typeof row.id === \"string\" ? row.id.trim() : \"\";\n    const createdAt = typeof row.created_at === \"string\" ? row.created_at.trim() : \"\";\n    return id === previousCursor.id && createdAt === previousCursor.createdAt;\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":337,"e":396,"m":1777163745580.3542,"t":"return {\n      rows: input.rows,\n      drifted: false,\n    };\n  }\n\n  const previousIndex = input.rows.findIndex((row) => {\n    const id = typeof row.id === \"string\" ? row.id.trim() : \"\";\n    const createdAt = typeof row.created_at === \"string\" ? row.created_at.trim() : \"\";\n    return id === previousCursor.id && createdAt === previousCursor.createdAt;\n  });\n\n  if (previousIndex >= 0) {\n    return {\n      rows: input.rows.slice(0, previousIndex),\n      drifted: false,\n    };\n  }\n\n  const newerRows = input.rows.filter((row) => {\n    const createdAt = typeof row.created_at === \"string\" ? row.created_at.trim() : \"\";\n    return createdAt > previousCursor.createdAt;\n  });\n  if (newerRows.length < input.rows.length) {\n    return {\n      rows: newerRows,\n      drifted: false,\n    };\n  }\n\n  return {\n    rows: input.rows,\n    drifted: input.rows.length > 0,\n  };\n}\n\nfunction collectNewDomainEvents(input: {\n  rows: TreeStreamDomainEventCursorRow[];\n  previousCursor: string | null;\n}) {\n  const previousCursor = decodeTreeStreamCursor(input.previousCursor);\n  if (!previousCursor) {\n    return {\n      rows: input.rows,\n      drifted: false,\n    };\n  }\n\n  const previousId = previousCursor.id.startsWith(\"domain_event:\")\n    ? previousCursor.id.slice(\"domain_event:\".length)\n    : previousCursor.id;\n  const previousIndex = input.rows.findIndex((row) => {\n    const id =\n      typeof row.event_id === \"string\"\n        ? row.event_id.trim()\n        : typeof row.id === \"string\"\n          ? row.id.trim()\n          : \"\";\n    const createdAt =\n      typeof row.created_at === \"string\"","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":385,"e":444,"m":1777163745580.3542,"t":"const previousId = previousCursor.id.startsWith(\"domain_event:\")\n    ? previousCursor.id.slice(\"domain_event:\".length)\n    : previousCursor.id;\n  const previousIndex = input.rows.findIndex((row) => {\n    const id =\n      typeof row.event_id === \"string\"\n        ? row.event_id.trim()\n        : typeof row.id === \"string\"\n          ? row.id.trim()\n          : \"\";\n    const createdAt =\n      typeof row.created_at === \"string\"\n        ? row.created_at.trim()\n        : typeof row.createdAt === \"string\"\n          ? row.createdAt.trim()\n          : \"\";\n    return id === previousId && createdAt === previousCursor.createdAt;\n  });\n\n  if (previousIndex >= 0) {\n    return {\n      rows: input.rows.slice(0, previousIndex),\n      drifted: false,\n    };\n  }\n\n  const newerRows = input.rows.filter((row) => {\n    const createdAt =\n      typeof row.created_at === \"string\"\n        ? row.created_at.trim()\n        : typeof row.createdAt === \"string\"\n          ? row.createdAt.trim()\n          : \"\";\n    return createdAt > previousCursor.createdAt;\n  });\n  if (newerRows.length < input.rows.length) {\n    return {\n      rows: newerRows,\n      drifted: false,\n    };\n  }\n\n  return {\n    rows: input.rows,\n    drifted: input.rows.length > 0,\n  };\n}\n\nfunction buildTreeStreamEnvelope(input: {\n  kind: TreeStreamEventName;\n  workspaceId: string;\n  rootNodeId: string | null;\n  projection: TreeStreamProjection;\n  cursor: string | null;\n  overview: TreeStreamOverview;\n  snapshot: TreeStreamSnapshotPayload;\n}): TreeStreamEnvelope {\n  return {\n    kind: input.kind,\n    stream: input.rootNodeId ? \"subtree\" : \"workspace\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":433,"e":492,"m":1777163745580.3542,"t":"function buildTreeStreamEnvelope(input: {\n  kind: TreeStreamEventName;\n  workspaceId: string;\n  rootNodeId: string | null;\n  projection: TreeStreamProjection;\n  cursor: string | null;\n  overview: TreeStreamOverview;\n  snapshot: TreeStreamSnapshotPayload;\n}): TreeStreamEnvelope {\n  return {\n    kind: input.kind,\n    stream: input.rootNodeId ? \"subtree\" : \"workspace\",\n    workspaceId: input.workspaceId,\n    rootNodeId: input.rootNodeId,\n    cursor: input.cursor,\n    projection: input.projection,\n    requestId: input.snapshot.requestId,\n    traceId: input.snapshot.traceId,\n    data: input.snapshot.data,\n    snapshot: input.snapshot.snapshot,\n    overview: input.overview,\n  };\n}\n\nfunction buildTreeStreamDeltaEnvelope(input: {\n  workspaceId: string;\n  rootNodeId: string | null;\n  projection: TreeStreamProjection;\n  cursor: string | null;\n  overview: TreeStreamOverview;\n  snapshot: TreeStreamSnapshotPayload;\n  delta: TreeStreamDeltaEvent;\n}): TreeStreamEnvelope {\n  return {\n    kind: \"delta\",\n    stream: input.rootNodeId ? \"subtree\" : \"workspace\",\n    workspaceId: input.workspaceId,\n    rootNodeId: input.rootNodeId,\n    cursor: input.cursor,\n    projection: input.projection,\n    requestId: input.snapshot.requestId,\n    traceId: input.snapshot.traceId,\n    data: input.delta,\n    snapshot: null,\n    overview: input.overview,\n  };\n}\n\nasync function defaultSleep(ms: number) {\n  await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport async function* streamTreeFrames(\n  input: StreamTreeFramesInput,\n): AsyncGenerator<TreeStreamFrame> {\n  const contract = resolveTreeStreamContract({\n    rootNodeId: input.rootNodeId,\n  });\n  const pollMs = Math.max(250, Math.floor(input.pollMs ?? 2000));\n  const maxPolls =","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":481,"e":540,"m":1777163745580.3542,"t":"async function defaultSleep(ms: number) {\n  await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport async function* streamTreeFrames(\n  input: StreamTreeFramesInput,\n): AsyncGenerator<TreeStreamFrame> {\n  const contract = resolveTreeStreamContract({\n    rootNodeId: input.rootNodeId,\n  });\n  const pollMs = Math.max(250, Math.floor(input.pollMs ?? 2000));\n  const maxPolls =\n    typeof input.maxPolls === \"number\" && Number.isFinite(input.maxPolls)\n      ? Math.max(0, Math.floor(input.maxPolls))\n      : null;\n  const sleep = input.sleep ?? defaultSleep;\n\n  let snapshot = await input.loadSnapshot();\n  let overview = await input.loadOverview();\n  let cursor = resolveOverviewCursor(overview, input.initialCursor ?? null);\n\n  yield {\n    event: \"snapshot\",\n    payload: buildTreeStreamEnvelope({\n      kind: \"snapshot\",\n      workspaceId: input.workspaceId,\n      rootNodeId: contract.rootNodeId,\n      projection: contract.projection,\n      cursor,\n      overview,\n      snapshot,\n    }),\n  };\n\n  let polls = 0;\n  while (maxPolls === null || polls < maxPolls) {\n    polls += 1;\n    await sleep(pollMs);\n\n    overview = await input.loadOverview();\n    const nextCursor = resolveOverviewCursor(overview, cursor);\n    if (nextCursor === cursor) {\n      continue;\n    }\n\n    const rows = Array.isArray(overview.command_logs) ? overview.command_logs : [];\n    const newRows = collectNewCommandLogs({\n      rows,\n      previousCursor: cursor,\n    });\n    const eventRows = Array.isArray(overview.domain_events)\n      ? (overview.domain_events as TreeStreamDomainEventCursorRow[])\n      : [];\n    const newEventRows = collectNewDomainEvents({\n      rows: eventRows,\n      previousCursor: cursor,\n    });\n    const hasNewCommandRows = newRows.rows.length > 0;\n    const hasNewDomainEventRows = newEventRows.rows.length > 0;\n    if (hasNewCommandRows && hasNewDomainEventRows) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":529,"e":588,"m":1777163745580.3542,"t":"previousCursor: cursor,\n    });\n    const eventRows = Array.isArray(overview.domain_events)\n      ? (overview.domain_events as TreeStreamDomainEventCursorRow[])\n      : [];\n    const newEventRows = collectNewDomainEvents({\n      rows: eventRows,\n      previousCursor: cursor,\n    });\n    const hasNewCommandRows = newRows.rows.length > 0;\n    const hasNewDomainEventRows = newEventRows.rows.length > 0;\n    if (hasNewCommandRows && hasNewDomainEventRows) {\n      const delta = resolveMatchingCommandDomainEventDelta({\n        commandRows: newRows.rows,\n        domainEventRows: newEventRows.rows,\n        commandDrifted: newRows.drifted,\n        domainEventDrifted: newEventRows.drifted,\n      });\n      if (delta) {\n        cursor = nextCursor;\n        yield {\n          event: \"delta\",\n          payload: buildTreeStreamDeltaEnvelope({\n            workspaceId: input.workspaceId,\n            rootNodeId: contract.rootNodeId,\n            projection: contract.projection,\n            cursor,\n            overview,\n            snapshot,\n            delta,\n          }),\n        };\n        continue;\n      }\n\n      snapshot = await input.loadSnapshot();\n      cursor = nextCursor;\n      yield {\n        event: \"resync\",\n        payload: buildTreeStreamEnvelope({\n          kind: \"resync\",\n          workspaceId: input.workspaceId,\n          rootNodeId: contract.rootNodeId,\n          projection: contract.projection,\n          cursor,\n          overview,\n          snapshot,\n        }),\n      };\n      continue;\n    }\n\n    if (!newRows.drifted && newRows.rows.length === 1) {\n      const delta = readCommandPayloadDelta(newRows.rows[0] ?? {});\n      if (delta) {\n        cursor = nextCursor;\n        yield {\n          event: \"delta\",\n          payload: buildTreeStreamDeltaEnvelope({\n            workspaceId: input.workspaceId,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":577,"e":636,"m":1777163745580.3542,"t":"};\n      continue;\n    }\n\n    if (!newRows.drifted && newRows.rows.length === 1) {\n      const delta = readCommandPayloadDelta(newRows.rows[0] ?? {});\n      if (delta) {\n        cursor = nextCursor;\n        yield {\n          event: \"delta\",\n          payload: buildTreeStreamDeltaEnvelope({\n            workspaceId: input.workspaceId,\n            rootNodeId: contract.rootNodeId,\n            projection: contract.projection,\n            cursor,\n            overview,\n            snapshot,\n            delta,\n          }),\n        };\n        continue;\n      }\n    }\n\n    if (!newEventRows.drifted && newRows.rows.length === 0 && newEventRows.rows.length === 1) {\n      const delta = readDomainEventPayloadDelta(newEventRows.rows[0] ?? {});\n      if (delta) {\n        cursor = nextCursor;\n        yield {\n          event: \"delta\",\n          payload: buildTreeStreamDeltaEnvelope({\n            workspaceId: input.workspaceId,\n            rootNodeId: contract.rootNodeId,\n            projection: contract.projection,\n            cursor,\n            overview,\n            snapshot,\n            delta,\n          }),\n        };\n        continue;\n      }\n    }\n\n    snapshot = await input.loadSnapshot();\n    cursor = nextCursor;\n\n    yield {\n      event: \"resync\",\n      payload: buildTreeStreamEnvelope({\n        kind: \"resync\",\n        workspaceId: input.workspaceId,\n        rootNodeId: contract.rootNodeId,\n        projection: contract.projection,\n        cursor,\n        overview,\n        snapshot,\n      }),\n    };\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/server.ts","s":625,"e":638,"m":1777163745580.3542,"t":"event: \"resync\",\n      payload: buildTreeStreamEnvelope({\n        kind: \"resync\",\n        workspaceId: input.workspaceId,\n        rootNodeId: contract.rootNodeId,\n        projection: contract.projection,\n        cursor,\n        overview,\n        snapshot,\n      }),\n    };\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":1,"e":60,"m":1777250310501.2493,"t":"import { describe, expect, it } from \"vitest\";\nimport type { SidebarInitialData } from \"@/components/sidebar/types\";\nimport {\n  applyTreeStreamDelta,\n  applyTreeStreamDeltaToProjectionState,\n} from \"./tree-delta\";\n\nconst baseSidebarData: SidebarInitialData = {\n  activeWorkspaceId: \"ws_1\",\n  workspaces: [],\n  documents: [\n    {\n      id: \"root\",\n      workspace_id: \"ws_1\",\n      title: \"Root\",\n      parent_id: null,\n      sort_order: 0,\n      is_starred: false,\n      access_scope: \"private\",\n      is_template: false,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: null,\n    },\n    {\n      id: \"child\",\n      workspace_id: \"ws_1\",\n      title: \"Child\",\n      parent_id: \"root\",\n      sort_order: 1,\n      is_starred: false,\n      access_scope: \"private\",\n      is_template: false,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: null,\n    },\n  ],\n  kernelFileTreeProjection: {\n    projectionId: \"kernel_projection:file_tree:root\",\n    projection: \"file_tree\",\n    rootNodeId: null,\n    items: [\n      {\n        rowId: \"doc:root\",\n        rowKind: \"document\",\n        nodeId: \"root\",\n        parentNodeId: null,\n        nodeType: \"page\",\n        projectionKind: \"file_tree\",\n        title: \"Root\",\n        depth: 0,\n        position: 0,\n        childCount: 2,\n        expandable: true,\n        expandedByDefault: true,\n        capabilities: [\"expand\", \"open\", \"select\"],\n        resourceMeta: {\n          resourceKind: \"document\",\n          documentId: \"root\",\n          workspaceId: \"ws_1\",\n          iconHint: \"page\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":49,"e":108,"m":1777250310501.2493,"t":"title: \"Root\",\n        depth: 0,\n        position: 0,\n        childCount: 2,\n        expandable: true,\n        expandedByDefault: true,\n        capabilities: [\"expand\", \"open\", \"select\"],\n        resourceMeta: {\n          resourceKind: \"document\",\n          documentId: \"root\",\n          workspaceId: \"ws_1\",\n          iconHint: \"page\",\n        },\n        iconHint: \"page\",\n      },\n      {\n        rowId: \"index:root\",\n        rowKind: \"index\",\n        nodeId: \"index:root\",\n        parentNodeId: \"root\",\n        nodeType: \"index\",\n        projectionKind: \"file_tree\",\n        title: \"index.md\",\n        depth: 1,\n        position: 0,\n        childCount: 0,\n        expandable: false,\n        expandedByDefault: false,\n        capabilities: [\"open\", \"select\"],\n        resourceMeta: {\n          resourceKind: \"index\",\n          documentId: \"root\",\n          workspaceId: \"ws_1\",\n          iconHint: \"index\",\n        },\n        iconHint: \"index\",\n      },\n    ],\n    edges: [],\n  },\n  kernelSidebarProjection: {\n    projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n    projection: \"sidebar_tree\",\n    rootNodeId: null,\n    items: [\n      {\n        nodeId: \"root\",\n        parentNodeId: null,\n        nodeType: \"page\",\n        projectionKind: \"sidebar_tree\",\n        title: \"Root\",\n        depth: 0,\n        position: 0,\n        childCount: 1,\n        expandable: true,\n        expandedByDefault: true,\n        capabilities: [\n          \"expand\",\n          \"open\",\n          \"drag\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":97,"e":156,"m":1777250310501.2493,"t":"nodeType: \"page\",\n        projectionKind: \"sidebar_tree\",\n        title: \"Root\",\n        depth: 0,\n        position: 0,\n        childCount: 1,\n        expandable: true,\n        expandedByDefault: true,\n        capabilities: [\n          \"expand\",\n          \"open\",\n          \"drag\",\n          \"drop\",\n          \"select\",\n          \"create-child\",\n          \"rename\",\n          \"archive\",\n          \"restore\",\n          \"context-menu\",\n          \"reorder\",\n        ],\n        resourceMeta: {\n          resourceKind: \"document\",\n          documentId: \"root\",\n          workspaceId: \"ws_1\",\n          iconHint: \"page\",\n        },\n        iconHint: \"page\",\n      },\n      {\n        nodeId: \"child\",\n        parentNodeId: \"root\",\n        nodeType: \"page\",\n        projectionKind: \"sidebar_tree\",\n        title: \"Child\",\n        depth: 1,\n        position: 1,\n        childCount: 0,\n        expandable: false,\n        expandedByDefault: true,\n        capabilities: [\n          \"open\",\n          \"drag\",\n          \"drop\",\n          \"select\",\n          \"create-child\",\n          \"rename\",\n          \"archive\",\n          \"restore\",\n          \"context-menu\",\n          \"reorder\",\n        ],\n        resourceMeta: {\n          resourceKind: \"document\",\n          documentId: \"child\",\n          workspaceId: \"ws_1\",\n          iconHint: \"page\",\n        },\n        iconHint: \"page\",\n      },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":145,"e":204,"m":1777250310501.2493,"t":"\"restore\",\n          \"context-menu\",\n          \"reorder\",\n        ],\n        resourceMeta: {\n          resourceKind: \"document\",\n          documentId: \"child\",\n          workspaceId: \"ws_1\",\n          iconHint: \"page\",\n        },\n        iconHint: \"page\",\n      },\n    ],\n    edges: [\n      {\n        id: \"edge:root:child:parent_of\",\n        edgeType: \"parent_of\",\n        workspaceId: \"ws_1\",\n        fromNodeId: \"root\",\n        toNodeId: \"child\",\n      },\n    ],\n  },\n  kernelSidebarTree: [],\n  trashedDocuments: [],\n  trashedMediaAssets: [],\n  trashedMindmapAssets: [],\n  trashedTableAssets: [],\n  tableAssets: [],\n  mindmapDocs: [],\n  mindmapAssets: [],\n  mindmapAssetChildren: {},\n  mediaAssets: [],\n};\n\nconst fileTreeProjectionBase: SidebarInitialData = {\n  ...baseSidebarData,\n  mediaAssets: [\n    {\n      id: \"asset_pdf\",\n      workspace_id: \"ws_1\",\n      document_id: \"root\",\n      asset_type: \"file\",\n      file_url: \"/manual.pdf\",\n      thumbnail_url: null,\n      bucket: null,\n      storage_path: \"documents/root/manual.pdf\",\n      file_name: \"manual.pdf\",\n      file_size: 1024,\n      mime_type: \"application/pdf\",\n      ocr_payload: undefined,\n      ocr_strategy: null,\n      ocr_text: null,\n      ocr_status: null,\n      signed_url: null,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: \"2026-04-18T00:00:00Z\",\n    },\n    {\n      id: \"asset_book\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":193,"e":252,"m":1777250310501.2493,"t":"file_size: 1024,\n      mime_type: \"application/pdf\",\n      ocr_payload: undefined,\n      ocr_strategy: null,\n      ocr_text: null,\n      ocr_status: null,\n      signed_url: null,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: \"2026-04-18T00:00:00Z\",\n    },\n    {\n      id: \"asset_book\",\n      workspace_id: \"ws_1\",\n      document_id: \"root\",\n      asset_type: \"file\",\n      file_url: \"/novel.epub\",\n      thumbnail_url: null,\n      bucket: null,\n      storage_path: \"documents/root/novel.epub\",\n      file_name: \"novel.epub\",\n      file_size: 2048,\n      mime_type: \"application/epub+zip\",\n      ocr_payload: undefined,\n      ocr_strategy: null,\n      ocr_text: null,\n      ocr_status: null,\n      signed_url: null,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: \"2026-04-18T00:00:00Z\",\n    },\n    {\n      id: \"asset_mindmap_child\",\n      workspace_id: \"ws_1\",\n      document_id: \"root\",\n      asset_type: \"image\",\n      file_url: \"/mindmap/concept.png\",\n      thumbnail_url: null,\n      bucket: null,\n      storage_path: \"documents/root/mindmaps/asset_mindmap/concept.png\",\n      file_name: \"concept.png\",\n      file_size: 512,\n      mime_type: \"image/png\",\n      ocr_payload: undefined,\n      ocr_strategy: null,\n      ocr_text: null,\n      ocr_status: null,\n      signed_url: null,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: \"2026-04-18T00:00:00Z\",\n    },\n  ],\n  tableAssets: [\n    {\n      id: \"asset_table\",\n      workspace_id: \"ws_1\",\n      document_id: \"root\",\n      asset_type: \"luckysheet\",\n      file_url: null,\n      thumbnail_url: null,\n      bucket: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":241,"e":300,"m":1777250310501.2493,"t":"updated_at: \"2026-04-18T00:00:00Z\",\n    },\n  ],\n  tableAssets: [\n    {\n      id: \"asset_table\",\n      workspace_id: \"ws_1\",\n      document_id: \"root\",\n      asset_type: \"luckysheet\",\n      file_url: null,\n      thumbnail_url: null,\n      bucket: null,\n      storage_path: null,\n      file_name: \"budget.luckysheet\",\n      file_size: null,\n      mime_type: \"application/json\",\n      ocr_payload: undefined,\n      ocr_strategy: null,\n      ocr_text: null,\n      ocr_status: null,\n      signed_url: null,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: \"2026-04-18T00:00:00Z\",\n    },\n  ],\n  mindmapAssets: [\n    {\n      id: \"asset_mindmap\",\n      workspace_id: \"ws_1\",\n      document_id: \"root\",\n      asset_type: \"mindmap\",\n      file_url: null,\n      thumbnail_url: null,\n      bucket: null,\n      storage_path: null,\n      file_name: \"mindmap.json\",\n      file_size: null,\n      mime_type: \"application/json\",\n      ocr_payload: undefined,\n      ocr_strategy: null,\n      ocr_text: null,\n      ocr_status: null,\n      signed_url: null,\n      created_at: \"2026-04-18T00:00:00Z\",\n      updated_at: \"2026-04-18T00:00:00Z\",\n    },\n  ],\n  mindmapAssetChildren: {\n    asset_mindmap: [\"asset_mindmap_child\"],\n  },\n};\n\ndescribe(\"tree-stream/tree-delta\", () => {\n  it(\"支持 upsert_document 重建 sidebar projection\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"upsert_document\",\n      document: {\n        id: \"leaf\",\n        workspace_id: \"ws_1\",\n        title: \"Leaf\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":289,"e":348,"m":1777250310501.2493,"t":"asset_mindmap: [\"asset_mindmap_child\"],\n  },\n};\n\ndescribe(\"tree-stream/tree-delta\", () => {\n  it(\"支持 upsert_document 重建 sidebar projection\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"upsert_document\",\n      document: {\n        id: \"leaf\",\n        workspace_id: \"ws_1\",\n        title: \"Leaf\",\n        parent_id: \"child\",\n        sort_order: 0,\n        is_starred: false,\n        access_scope: \"private\",\n        is_template: false,\n        created_at: \"2026-04-18T00:00:00Z\",\n        updated_at: null,\n      },\n    });\n\n    expect(next.documents.map((item) => item.id)).toEqual([\"root\", \"child\", \"leaf\"]);\n    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([\n      \"root\",\n      \"child\",\n      \"leaf\",\n    ]);\n  });\n\n  it(\"支持 remove_document 级联移除子树\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"remove_document\",\n      documentId: \"root\",\n    });\n\n    expect(next.documents).toEqual([]);\n    expect(next.kernelSidebarProjection.items).toEqual([]);\n  });\n\n  it(\"支持对已存在文档做局部 upsert patch，而不丢失原有排序字段\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"upsert_document\",\n      document: {\n        id: \"child\",\n        title: \"Child Renamed\",\n        updated_at: \"2026-04-18T00:05:00Z\",\n      },\n    });\n\n    expect(next.documents).toHaveLength(2);\n    expect(next.documents.find((item) => item.id === \"child\")).toMatchObject({\n      id: \"child\",\n      title: \"Child Renamed\",\n      parent_id: \"root\",\n      sort_order: 1,\n      workspace_id: \"ws_1\",\n    });\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":337,"e":396,"m":1777250310501.2493,"t":"});\n\n    expect(next.documents).toHaveLength(2);\n    expect(next.documents.find((item) => item.id === \"child\")).toMatchObject({\n      id: \"child\",\n      title: \"Child Renamed\",\n      parent_id: \"root\",\n      sort_order: 1,\n      workspace_id: \"ws_1\",\n    });\n  });\n\n  it(\"支持 upsert_documents 批量新增复制出的子树\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"upsert_documents\",\n      upsertDocuments: [\n        {\n          id: \"copy_root\",\n          workspace_id: \"ws_1\",\n          title: \"Copy Root\",\n          parent_id: null,\n          sort_order: 2,\n          is_starred: false,\n          access_scope: \"private\",\n          is_template: false,\n          created_at: \"2026-04-18T00:10:00Z\",\n          updated_at: \"2026-04-18T00:10:00Z\",\n        },\n        {\n          id: \"copy_child\",\n          workspace_id: \"ws_1\",\n          title: \"Copy Child\",\n          parent_id: \"copy_root\",\n          sort_order: 0,\n          is_starred: false,\n          access_scope: \"private\",\n          is_template: false,\n          created_at: \"2026-04-18T00:10:00Z\",\n          updated_at: \"2026-04-18T00:10:00Z\",\n        },\n      ],\n    });\n\n    expect(next.documents.map((item) => item.id)).toEqual([\n      \"root\",\n      \"child\",\n      \"copy_root\",\n      \"copy_child\",\n    ]);\n    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([\n      \"root\",\n      \"child\",\n      \"copy_root\",\n      \"copy_child\",\n    ]);\n    expect(\n      next.kernelSidebarProjection.items.find((item) => item.nodeId === \"copy_child\"),\n    ).toMatchObject({\n      parentNodeId: \"copy_root\",\n      depth: 1,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":385,"e":444,"m":1777250310501.2493,"t":"]);\n    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([\n      \"root\",\n      \"child\",\n      \"copy_root\",\n      \"copy_child\",\n    ]);\n    expect(\n      next.kernelSidebarProjection.items.find((item) => item.nodeId === \"copy_child\"),\n    ).toMatchObject({\n      parentNodeId: \"copy_root\",\n      depth: 1,\n    });\n  });\n\n  it(\"支持 move_document 细粒度更新父节点与排序字段\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"move_document\",\n      documentId: \"child\",\n      parentId: null,\n      sortOrder: 0,\n      updatedAt: \"2026-04-18T00:10:00Z\",\n    });\n\n    expect(next.documents.find((item) => item.id === \"child\")).toMatchObject({\n      id: \"child\",\n      parent_id: null,\n      sort_order: 0,\n      updated_at: \"2026-04-18T00:10:00Z\",\n    });\n    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([\n      \"root\",\n      \"child\",\n    ]);\n    expect(\n      next.kernelSidebarProjection.items.find((item) => item.nodeId === \"child\"),\n    ).toMatchObject({\n      parentNodeId: null,\n      depth: 0,\n    });\n  });\n\n  it(\"支持 replace_sidebar 直接切换到 resync snapshot\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"replace_sidebar\",\n      sidebar: {\n        activeWorkspaceId: \"ws_1\",\n        workspaces: [],\n        documents: [],\n        kernelSidebarProjection: {\n          projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n          projection: \"sidebar_tree\",\n          rootNodeId: null,\n          items: [],\n          edges: [],\n        },\n        kernelFileTreeProjection: {\n          projectionId: \"kernel_projection:file_tree:root\",\n          projection: \"file_tree\",\n          rootNodeId: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":433,"e":492,"m":1777250310501.2493,"t":"documents: [],\n        kernelSidebarProjection: {\n          projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n          projection: \"sidebar_tree\",\n          rootNodeId: null,\n          items: [],\n          edges: [],\n        },\n        kernelFileTreeProjection: {\n          projectionId: \"kernel_projection:file_tree:root\",\n          projection: \"file_tree\",\n          rootNodeId: null,\n          items: [],\n          edges: [],\n        },\n        kernelSidebarTree: [],\n        trashedDocuments: [],\n        trashedMediaAssets: [],\n        trashedMindmapAssets: [],\n        trashedTableAssets: [],\n        tableAssets: [],\n        mindmapDocs: [],\n        mindmapAssets: [],\n        mindmapAssetChildren: {},\n        mediaAssets: [],\n      },\n    });\n\n    expect(next.documents).toEqual([]);\n    expect(next.kernelSidebarProjection.items).toEqual([]);\n  });\n\n  it(\"支持 noop delta 仅推进 cursor，不修改当前 sidebar snapshot\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"noop\",\n    });\n\n    expect(next).toEqual(baseSidebarData);\n  });\n\n  it(\"支持 resync_required delta 仅推进 cursor，等待后续 snapshot/resync\", () => {\n    const next = applyTreeStreamDelta(baseSidebarData, {\n      op: \"resync_required\",\n    });\n\n    expect(next).toEqual(baseSidebarData);\n  });\n\n  it(\"为 page_tree 定义统一 delta 应用边界，并可稳定派生页面行\", () => {\n    const next = applyTreeStreamDeltaToProjectionState({\n      projection: \"page_tree\",\n      base: baseSidebarData,\n      event: {\n        op: \"upsert_document\",\n        document: {\n          id: \"leaf\",\n          workspace_id: \"ws_1\",\n          title: \"Leaf\",\n          parent_id: \"child\",\n          sort_order: 0,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":481,"e":540,"m":1777250310501.2493,"t":"it(\"为 page_tree 定义统一 delta 应用边界，并可稳定派生页面行\", () => {\n    const next = applyTreeStreamDeltaToProjectionState({\n      projection: \"page_tree\",\n      base: baseSidebarData,\n      event: {\n        op: \"upsert_document\",\n        document: {\n          id: \"leaf\",\n          workspace_id: \"ws_1\",\n          title: \"Leaf\",\n          parent_id: \"child\",\n          sort_order: 0,\n          is_starred: false,\n          access_scope: \"private\",\n          is_template: false,\n          created_at: \"2026-04-18T00:00:00Z\",\n          updated_at: null,\n        },\n      },\n    });\n\n    expect(next.projection).toBe(\"page_tree\");\n    expect(next.documents.map((item) => item.id)).toEqual([\"root\", \"child\", \"leaf\"]);\n    expect(next.pageTreeItems.map((item) => item.nodeId)).toEqual([\"root\", \"child\", \"leaf\"]);\n    expect(next.pageTreeItems.find((item) => item.nodeId === \"leaf\")).toMatchObject({\n      parentNodeId: \"child\",\n      depth: 2,\n      title: \"Leaf\",\n    });\n  });\n\n  it(\"为 file_tree 定义统一 delta 应用边界，并保留 doc/index/asset-folder/asset 行语义\", () => {\n    const next = applyTreeStreamDeltaToProjectionState({\n      projection: \"file_tree\",\n      base: fileTreeProjectionBase,\n      event: {\n        op: \"replace_documents\",\n        documents: [...fileTreeProjectionBase.documents],\n      },\n    });\n\n    expect(next.projection).toBe(\"file_tree\");\n    expect(next.fileTreeItems.map((item) => item.rowKind)).toEqual(\n      expect.arrayContaining([\"document\", \"index\", \"asset_folder\", \"asset\"]),\n    );\n    expect(\n      next.fileTreeItems.find((item) => item.resourceMeta.assetId === \"asset_mindmap\"),\n    ).toMatchObject({\n      rowKind: \"asset_folder\",\n      iconHint: \"mindmap\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"mindmap\",\n        assetKind: \"mindmap\",\n      }),\n    });\n    expect(\n      next.fileTreeItems.find((item) => item.resourceMeta.assetId === \"asset_pdf\"),\n    ).toMatchObject({\n      rowKind: \"asset\",\n      iconHint: \"pdf\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":529,"e":588,"m":1777250310501.2493,"t":"rowKind: \"asset_folder\",\n      iconHint: \"mindmap\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"mindmap\",\n        assetKind: \"mindmap\",\n      }),\n    });\n    expect(\n      next.fileTreeItems.find((item) => item.resourceMeta.assetId === \"asset_pdf\"),\n    ).toMatchObject({\n      rowKind: \"asset\",\n      iconHint: \"pdf\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"pdf\",\n        assetKind: \"pdf\",\n      }),\n    });\n    expect(\n      next.fileTreeItems.find((item) => item.resourceMeta.assetId === \"asset_book\"),\n    ).toMatchObject({\n      rowKind: \"asset\",\n      iconHint: \"book\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"book\",\n        assetKind: \"book\",\n      }),\n    });\n    expect(\n      next.fileTreeItems.find((item) => item.resourceMeta.assetId === \"asset_table\"),\n    ).toMatchObject({\n      rowKind: \"asset\",\n      iconHint: \"table\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"table\",\n        assetKind: \"table\",\n      }),\n    });\n  });\n\n  it(\"file_tree fixture 覆盖搜索 hardening 需要的 index、asset-folder、mindmap child、book 与 pdf 资源类型\", () => {\n    const next = applyTreeStreamDeltaToProjectionState({\n      projection: \"file_tree\",\n      base: fileTreeProjectionBase,\n      event: {\n        op: \"replace_documents\",\n        documents: [...fileTreeProjectionBase.documents],\n      },\n    });\n    const rowById = new Map(next.fileTreeItems.map((item) => [item.rowId, item]));\n\n    expect(rowById.get(\"index:root\")).toMatchObject({\n      rowKind: \"index\",\n      title: \"index.md\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"index\",\n        documentId: \"root\",\n      }),\n    });\n    expect(rowById.get(\"asset-folder:asset_mindmap\")).toMatchObject({\n      rowKind: \"asset_folder\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":577,"e":636,"m":1777250310501.2493,"t":"const rowById = new Map(next.fileTreeItems.map((item) => [item.rowId, item]));\n\n    expect(rowById.get(\"index:root\")).toMatchObject({\n      rowKind: \"index\",\n      title: \"index.md\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"index\",\n        documentId: \"root\",\n      }),\n    });\n    expect(rowById.get(\"asset-folder:asset_mindmap\")).toMatchObject({\n      rowKind: \"asset_folder\",\n      title: \"mindmap\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"mindmap\",\n        assetKind: \"mindmap\",\n      }),\n    });\n    expect(rowById.get(\"asset:asset_mindmap_child\")).toMatchObject({\n      rowKind: \"asset\",\n      parentNodeId: \"asset-folder:asset_mindmap\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"asset\",\n        assetKind: \"image\",\n      }),\n    });\n    expect(rowById.get(\"asset:asset_book\")).toMatchObject({\n      rowKind: \"asset\",\n      iconHint: \"book\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"book\",\n        assetKind: \"book\",\n      }),\n    });\n    expect(rowById.get(\"asset:asset_pdf\")).toMatchObject({\n      rowKind: \"asset\",\n      iconHint: \"pdf\",\n      resourceMeta: expect.objectContaining({\n        resourceKind: \"pdf\",\n        assetKind: \"pdf\",\n      }),\n    });\n  });\n\n  it(\"支持 upsert_assets 更新资源归属并重建 file_tree projection\", () => {\n    const next = applyTreeStreamDeltaToProjectionState({\n      projection: \"file_tree\",\n      base: fileTreeProjectionBase,\n      event: {\n        op: \"upsert_assets\",\n        upsertAssets: [\n          {\n            id: \"asset_pdf\",\n            workspace_id: \"ws_1\",\n            document_id: \"child\",\n            asset_type: \"file\",\n            file_url: \"/manual.pdf\",\n            thumbnail_url: \"/manual.pdf\",\n            bucket: null,\n            storage_path: \"documents/child/manual.pdf\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.test.ts","s":625,"e":664,"m":1777250310501.2493,"t":"event: {\n        op: \"upsert_assets\",\n        upsertAssets: [\n          {\n            id: \"asset_pdf\",\n            workspace_id: \"ws_1\",\n            document_id: \"child\",\n            asset_type: \"file\",\n            file_url: \"/manual.pdf\",\n            thumbnail_url: \"/manual.pdf\",\n            bucket: null,\n            storage_path: \"documents/child/manual.pdf\",\n            file_name: \"manual.pdf\",\n            file_size: 1024,\n            mime_type: \"application/pdf\",\n            ocr_text: null,\n            ocr_status: null,\n            created_at: \"2026-04-18T00:00:00Z\",\n            updated_at: \"2026-04-26T00:00:00Z\",\n          },\n        ],\n      },\n    });\n\n    expect(next.sidebar.mediaAssets?.find((asset) => asset.id === \"asset_pdf\")).toMatchObject({\n      document_id: \"child\",\n      updated_at: \"2026-04-26T00:00:00Z\",\n    });\n    expect(\n      next.fileTreeItems.find((item) => item.resourceMeta.assetId === \"asset_pdf\"),\n    ).toMatchObject({\n      parentNodeId: \"child\",\n      resourceMeta: expect.objectContaining({\n        documentId: \"child\",\n        resourceKind: \"pdf\",\n      }),\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":1,"e":60,"m":1777248223078.1016,"t":"import type { SidebarInitialData } from \"@/components/sidebar/types\";\nimport { buildKernelFileTreeProjection } from \"@/lib/kernel-file-tree\";\nimport type { SidebarDatasetListQueryResult } from \"@/lib/sidebar-data\";\nimport {\n  buildSidebarDatasetListQueryResult,\n  mapSidebarDatasetListQueryResultToInitialData,\n} from \"@/lib/sidebar-data\";\nimport type { DocumentRecord } from \"@/lib/documents\";\nimport {\n  buildSidebarTreeFromKernelProjection,\n  type KernelSidebarProjectionItem,\n} from \"@/lib/kernel-sidebar\";\nimport type { KernelFileTreeProjectionItem } from \"@/lib/kernel-file-tree\";\nimport {\n  buildPageTreeProjectionItems,\n  type PageTreeProjectionItem,\n} from \"@/lib/tree-projection\";\nimport type { WorkspaceSummary } from \"@/lib/workspaces\";\nimport type { MediaAsset } from \"@/types/media\";\n\nexport type TreeStreamDocumentPatch =\n  Partial<DocumentRecord> & Pick<DocumentRecord, \"id\">;\n\nexport type TreeStreamDeltaOp =\n  | \"noop\"\n  | \"resync_required\"\n  | \"upsert_document\"\n  | \"upsert_documents\"\n  | \"upsert_assets\"\n  | \"remove_document\"\n  | \"move_document\"\n  | \"replace_documents\"\n  | \"replace_sidebar\";\n\nexport type TreeStreamDeltaEvent = {\n  op: TreeStreamDeltaOp;\n  node?: TreeStreamDocumentPatch | null;\n  document?: TreeStreamDocumentPatch | null;\n  documentId?: string | null;\n  parentId?: string | null;\n  sortOrder?: number | null;\n  updatedAt?: string | null;\n  upsertDocuments?: TreeStreamDocumentPatch[] | null;\n  upsertAssets?: MediaAsset[] | null;\n  documents?: DocumentRecord[] | null;\n  sidebar?: SidebarDatasetListQueryResult | SidebarInitialData | null;\n};\n\nexport type TreeRendererProjection = \"sidebar_tree\" | \"page_tree\" | \"file_tree\";\n\nexport type TreeRendererDeltaState = {\n  projection: TreeRendererProjection;\n  sidebar: SidebarInitialData;\n  documents: DocumentRecord[];\n  sidebarItems: KernelSidebarProjectionItem[];\n  pageTreeItems: PageTreeProjectionItem[];\n  fileTreeItems: KernelFileTreeProjectionItem[];\n};\n\nfunction cloneSidebarData(data: SidebarInitialData): SidebarInitialData {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":49,"e":108,"m":1777248223078.1016,"t":"export type TreeRendererProjection = \"sidebar_tree\" | \"page_tree\" | \"file_tree\";\n\nexport type TreeRendererDeltaState = {\n  projection: TreeRendererProjection;\n  sidebar: SidebarInitialData;\n  documents: DocumentRecord[];\n  sidebarItems: KernelSidebarProjectionItem[];\n  pageTreeItems: PageTreeProjectionItem[];\n  fileTreeItems: KernelFileTreeProjectionItem[];\n};\n\nfunction cloneSidebarData(data: SidebarInitialData): SidebarInitialData {\n  return {\n    ...data,\n    workspaces: [...data.workspaces],\n    documents: [...data.documents],\n    kernelFileTreeProjection: {\n      ...data.kernelFileTreeProjection,\n      items: [...data.kernelFileTreeProjection.items],\n      edges: [...data.kernelFileTreeProjection.edges],\n    },\n    kernelSidebarProjection: {\n      ...data.kernelSidebarProjection,\n      items: [...data.kernelSidebarProjection.items],\n      edges: [...data.kernelSidebarProjection.edges],\n    },\n    kernelSidebarTree: [...data.kernelSidebarTree],\n    trashedDocuments: [...data.trashedDocuments],\n    trashedMediaAssets: [...(data.trashedMediaAssets ?? [])],\n    trashedMindmapAssets: [...(data.trashedMindmapAssets ?? [])],\n    trashedTableAssets: [...(data.trashedTableAssets ?? [])],\n    mindmapDocs: [...(data.mindmapDocs ?? [])],\n    mindmapAssets: [...(data.mindmapAssets ?? [])],\n    mindmapAssetChildren: { ...(data.mindmapAssetChildren ?? {}) },\n    tableAssets: [...(data.tableAssets ?? [])],\n    mediaAssets: [...(data.mediaAssets ?? [])],\n  };\n}\n\nfunction buildSidebarFromDocuments(input: {\n  base: SidebarInitialData;\n  documents: DocumentRecord[];\n}): SidebarInitialData {\n  const queryResult = buildSidebarDatasetListQueryResult({\n    activeWorkspaceId: input.base.activeWorkspaceId,\n    workspaces: input.base.workspaces as WorkspaceSummary[],\n    documents: input.documents,\n    trashedDocuments: input.base.trashedDocuments,\n    mindmaps: (input.base.mindmapAssets ?? []).map((asset) => ({\n      mindmap_id: asset.id,\n      workspace_id: asset.workspace_id,\n      document_id: asset.document_id,\n      created_at: asset.created_at,\n      updated_at: asset.updated_at,\n      deleted_at: asset.deleted_at ?? null,\n      deleted_by: asset.deleted_by ?? null,\n    })),\n    mediaAssets: input.base.mediaAssets as MediaAsset[] | null,\n    trashedMediaAssets: input.base.trashedMediaAssets as MediaAsset[] | null,\n    tables: (input.base.tableAssets ?? []).map((asset) => ({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":97,"e":156,"m":1777248223078.1016,"t":"mindmaps: (input.base.mindmapAssets ?? []).map((asset) => ({\n      mindmap_id: asset.id,\n      workspace_id: asset.workspace_id,\n      document_id: asset.document_id,\n      created_at: asset.created_at,\n      updated_at: asset.updated_at,\n      deleted_at: asset.deleted_at ?? null,\n      deleted_by: asset.deleted_by ?? null,\n    })),\n    mediaAssets: input.base.mediaAssets as MediaAsset[] | null,\n    trashedMediaAssets: input.base.trashedMediaAssets as MediaAsset[] | null,\n    tables: (input.base.tableAssets ?? []).map((asset) => ({\n      id: asset.id,\n      workspace_id: asset.workspace_id,\n      document_id: asset.document_id,\n      title: asset.file_name ?? null,\n      created_at: asset.created_at,\n      updated_at: asset.updated_at,\n      deleted_at: asset.deleted_at ?? null,\n      deleted_by: asset.deleted_by ?? null,\n      purged_at: asset.purged_at ?? null,\n      is_archived: Boolean(asset.deleted_at),\n    })),\n  });\n\n  if (\n    (input.base.mediaAssets?.length ?? 0) > 0 ||\n    (input.base.mindmapAssets?.length ?? 0) > 0 ||\n    (input.base.tableAssets?.length ?? 0) > 0 ||\n    Object.keys(input.base.mindmapAssetChildren ?? {}).length > 0\n  ) {\n    // 说明：stream delta 只替换 documents 时，仍要保留已有资源树语义；\n    // 否则 mindmap 子附件会在 resync 前退化成普通 asset。\n    const nextFileTreeProjection = buildKernelFileTreeProjection({\n      documents: input.documents,\n      mediaAssets: input.base.mediaAssets,\n      mindmapAssets: input.base.mindmapAssets,\n      tableAssets: input.base.tableAssets,\n      mindmapAssetChildren: input.base.mindmapAssetChildren,\n    });\n    queryResult.kernel_file_tree_projection = nextFileTreeProjection;\n    queryResult.kernelFileTreeProjection = nextFileTreeProjection;\n    queryResult.mindmap_asset_children = { ...(input.base.mindmapAssetChildren ?? {}) };\n  }\n\n  return mapSidebarDatasetListQueryResultToInitialData(queryResult);\n}\n\nfunction normalizeUpsertDocument(event: TreeStreamDeltaEvent): TreeStreamDocumentPatch | null {\n  const candidate = event.node ?? event.document ?? null;\n  if (!candidate || typeof candidate !== \"object\") {\n    return null;\n  }\n  return typeof candidate.id === \"string\" && candidate.id.trim()\n    ? ({ ...candidate, id: candidate.id.trim() } as TreeStreamDocumentPatch)\n    : null;\n}\n\nfunction normalizeDocumentId(event: TreeStreamDeltaEvent): string | null {\n  const candidate = typeof event.documentId === \"string\" ? event.documentId.trim() : \"\";","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":145,"e":204,"m":1777248223078.1016,"t":"function normalizeUpsertDocument(event: TreeStreamDeltaEvent): TreeStreamDocumentPatch | null {\n  const candidate = event.node ?? event.document ?? null;\n  if (!candidate || typeof candidate !== \"object\") {\n    return null;\n  }\n  return typeof candidate.id === \"string\" && candidate.id.trim()\n    ? ({ ...candidate, id: candidate.id.trim() } as TreeStreamDocumentPatch)\n    : null;\n}\n\nfunction normalizeDocumentId(event: TreeStreamDeltaEvent): string | null {\n  const candidate = typeof event.documentId === \"string\" ? event.documentId.trim() : \"\";\n  return candidate || null;\n}\n\nfunction isCompleteDocumentRecord(value: TreeStreamDocumentPatch): value is DocumentRecord {\n  return (\n    typeof value.workspace_id === \"string\" &&\n    typeof value.access_scope === \"string\" &&\n    typeof value.is_template === \"boolean\" &&\n    typeof value.created_at === \"string\" &&\n    \"parent_id\" in value &&\n    \"sort_order\" in value &&\n    \"is_starred\" in value &&\n    \"updated_at\" in value\n  );\n}\n\nfunction isCompleteMediaAsset(value: unknown): value is MediaAsset {\n  if (!value || typeof value !== \"object\") {\n    return false;\n  }\n  const asset = value as MediaAsset;\n  return (\n    typeof asset.id === \"string\" &&\n    typeof asset.workspace_id === \"string\" &&\n    typeof asset.document_id === \"string\" &&\n    typeof asset.asset_type === \"string\" &&\n    typeof asset.created_at === \"string\" &&\n    typeof asset.updated_at === \"string\" &&\n    \"file_name\" in value &&\n    \"file_url\" in value &&\n    \"thumbnail_url\" in value &&\n    \"file_size\" in value &&\n    \"mime_type\" in value\n  );\n}\n\nfunction buildSidebarFromAssets(input: {\n  base: SidebarInitialData;\n  mediaAssets: MediaAsset[];\n}): SidebarInitialData {\n  const next = cloneSidebarData(input.base);\n  next.mediaAssets = [...input.mediaAssets];\n  next.kernelFileTreeProjection = buildKernelFileTreeProjection({\n    documents: next.documents,\n    mediaAssets: next.mediaAssets,\n    mindmapAssets: next.mindmapAssets,\n    tableAssets: next.tableAssets,\n    mindmapAssetChildren: next.mindmapAssetChildren,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":193,"e":252,"m":1777248223078.1016,"t":"function buildSidebarFromAssets(input: {\n  base: SidebarInitialData;\n  mediaAssets: MediaAsset[];\n}): SidebarInitialData {\n  const next = cloneSidebarData(input.base);\n  next.mediaAssets = [...input.mediaAssets];\n  next.kernelFileTreeProjection = buildKernelFileTreeProjection({\n    documents: next.documents,\n    mediaAssets: next.mediaAssets,\n    mindmapAssets: next.mindmapAssets,\n    tableAssets: next.tableAssets,\n    mindmapAssetChildren: next.mindmapAssetChildren,\n  });\n  return next;\n}\n\nexport function deriveTreeRendererDeltaState(input: {\n  projection: TreeRendererProjection;\n  sidebar: SidebarInitialData;\n}): TreeRendererDeltaState {\n  const sidebar = input.sidebar;\n  const pageTreeSource =\n    sidebar.kernelSidebarTree.length > 0\n      ? sidebar.kernelSidebarTree\n      : buildSidebarTreeFromKernelProjection({\n          records: sidebar.documents,\n          projection: sidebar.kernelSidebarProjection,\n        });\n\n  return {\n    projection: input.projection,\n    sidebar,\n    documents: [...sidebar.documents],\n    sidebarItems: [...sidebar.kernelSidebarProjection.items],\n    pageTreeItems: buildPageTreeProjectionItems(pageTreeSource),\n    fileTreeItems: [...sidebar.kernelFileTreeProjection.items],\n  };\n}\n\nexport function applyTreeStreamDelta(\n  base: SidebarInitialData,\n  event: TreeStreamDeltaEvent,\n): SidebarInitialData {\n  if (event.op === \"noop\") {\n    return base;\n  }\n\n  if (event.op === \"resync_required\") {\n    return base;\n  }\n\n  if (event.op === \"replace_sidebar\" && event.sidebar) {\n    if (\"activeWorkspaceId\" in event.sidebar) {\n      return cloneSidebarData(event.sidebar as SidebarInitialData);\n    }\n    return mapSidebarDatasetListQueryResultToInitialData(event.sidebar as SidebarDatasetListQueryResult);\n  }\n\n  if (event.op === \"replace_documents\" && Array.isArray(event.documents)) {\n    return buildSidebarFromDocuments({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":241,"e":300,"m":1777248223078.1016,"t":"return base;\n  }\n\n  if (event.op === \"replace_sidebar\" && event.sidebar) {\n    if (\"activeWorkspaceId\" in event.sidebar) {\n      return cloneSidebarData(event.sidebar as SidebarInitialData);\n    }\n    return mapSidebarDatasetListQueryResultToInitialData(event.sidebar as SidebarDatasetListQueryResult);\n  }\n\n  if (event.op === \"replace_documents\" && Array.isArray(event.documents)) {\n    return buildSidebarFromDocuments({\n      base,\n      documents: [...event.documents],\n    });\n  }\n\n  if (event.op === \"upsert_document\") {\n    const documentPatch = normalizeUpsertDocument(event);\n    if (!documentPatch) {\n      return base;\n    }\n    const nextDocuments = [...base.documents];\n    const existingIndex = nextDocuments.findIndex((item) => item.id === documentPatch.id);\n    if (existingIndex >= 0) {\n      nextDocuments[existingIndex] = {\n        ...nextDocuments[existingIndex],\n        ...documentPatch,\n      };\n    } else {\n      if (!isCompleteDocumentRecord(documentPatch)) {\n        return base;\n      }\n      nextDocuments.push(documentPatch);\n    }\n    return buildSidebarFromDocuments({\n      base,\n      documents: nextDocuments,\n    });\n  }\n\n  if (event.op === \"upsert_documents\") {\n    const patches = Array.isArray(event.upsertDocuments) ? event.upsertDocuments : [];\n    if (patches.length === 0) {\n      return base;\n    }\n    let changed = false;\n    const nextDocuments = [...base.documents];\n    for (const rawPatch of patches) {\n      if (!rawPatch || typeof rawPatch.id !== \"string\" || !rawPatch.id.trim()) {\n        continue;\n      }\n      const documentPatch = {\n        ...rawPatch,\n        id: rawPatch.id.trim(),\n      } as TreeStreamDocumentPatch;\n      const existingIndex = nextDocuments.findIndex((item) => item.id === documentPatch.id);\n      if (existingIndex >= 0) {\n        nextDocuments[existingIndex] = {\n          ...nextDocuments[existingIndex],","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":289,"e":348,"m":1777248223078.1016,"t":"for (const rawPatch of patches) {\n      if (!rawPatch || typeof rawPatch.id !== \"string\" || !rawPatch.id.trim()) {\n        continue;\n      }\n      const documentPatch = {\n        ...rawPatch,\n        id: rawPatch.id.trim(),\n      } as TreeStreamDocumentPatch;\n      const existingIndex = nextDocuments.findIndex((item) => item.id === documentPatch.id);\n      if (existingIndex >= 0) {\n        nextDocuments[existingIndex] = {\n          ...nextDocuments[existingIndex],\n          ...documentPatch,\n        };\n        changed = true;\n        continue;\n      }\n      if (!isCompleteDocumentRecord(documentPatch)) {\n        continue;\n      }\n      nextDocuments.push(documentPatch);\n      changed = true;\n    }\n    if (!changed) {\n      return base;\n    }\n    return buildSidebarFromDocuments({\n      base,\n      documents: nextDocuments,\n    });\n  }\n\n  if (event.op === \"remove_document\") {\n    const documentId = normalizeDocumentId(event);\n    if (!documentId) {\n      return base;\n    }\n    const removedIds = new Set<string>([documentId]);\n    let changed = true;\n    while (changed) {\n      changed = false;\n      for (const document of base.documents) {\n        if (document.parent_id && removedIds.has(document.parent_id) && !removedIds.has(document.id)) {\n          removedIds.add(document.id);\n          changed = true;\n        }\n      }\n    }\n    return buildSidebarFromDocuments({\n      base,\n      documents: base.documents.filter((item) => !removedIds.has(item.id)),\n    });\n  }\n\n  if (event.op === \"move_document\") {\n    const documentId = normalizeDocumentId(event);\n    if (!documentId) {\n      return base;\n    }\n    const existingIndex = base.documents.findIndex((item) => item.id === documentId);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":337,"e":396,"m":1777248223078.1016,"t":"return buildSidebarFromDocuments({\n      base,\n      documents: base.documents.filter((item) => !removedIds.has(item.id)),\n    });\n  }\n\n  if (event.op === \"move_document\") {\n    const documentId = normalizeDocumentId(event);\n    if (!documentId) {\n      return base;\n    }\n    const existingIndex = base.documents.findIndex((item) => item.id === documentId);\n    if (existingIndex < 0) {\n      return base;\n    }\n    const nextDocuments = [...base.documents];\n    const existing = nextDocuments[existingIndex]!;\n    nextDocuments[existingIndex] = {\n      ...existing,\n      parent_id: \"parentId\" in event ? (event.parentId ?? null) : existing.parent_id,\n      sort_order:\n        typeof event.sortOrder === \"number\" && Number.isFinite(event.sortOrder)\n          ? event.sortOrder\n          : existing.sort_order,\n      updated_at:\n        typeof event.updatedAt === \"string\" && event.updatedAt.trim()\n          ? event.updatedAt.trim()\n          : existing.updated_at,\n    };\n    return buildSidebarFromDocuments({\n      base,\n      documents: nextDocuments,\n    });\n  }\n\n  if (event.op === \"upsert_assets\") {\n    const assets = Array.isArray(event.upsertAssets) ? event.upsertAssets : [];\n    if (assets.length === 0) {\n      return base;\n    }\n    let changed = false;\n    const nextAssets = [...(base.mediaAssets ?? [])];\n    for (const asset of assets) {\n      if (!isCompleteMediaAsset(asset)) {\n        continue;\n      }\n      const existingIndex = nextAssets.findIndex((item) => item.id === asset.id);\n      if (existingIndex >= 0) {\n        nextAssets[existingIndex] = {\n          ...nextAssets[existingIndex],\n          ...asset,\n        };\n      } else {\n        nextAssets.push(asset);\n      }\n      changed = true;\n    }\n    if (!changed) {\n      return base;\n    }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-delta.ts","s":385,"e":416,"m":1777248223078.1016,"t":"nextAssets[existingIndex] = {\n          ...nextAssets[existingIndex],\n          ...asset,\n        };\n      } else {\n        nextAssets.push(asset);\n      }\n      changed = true;\n    }\n    if (!changed) {\n      return base;\n    }\n    return buildSidebarFromAssets({\n      base,\n      mediaAssets: nextAssets,\n    });\n  }\n\n  return base;\n}\n\nexport function applyTreeStreamDeltaToProjectionState(input: {\n  projection: TreeRendererProjection;\n  base: SidebarInitialData;\n  event: TreeStreamDeltaEvent;\n}): TreeRendererDeltaState {\n  return deriveTreeRendererDeltaState({\n    projection: input.projection,\n    sidebar: applyTreeStreamDelta(input.base, input.event),\n  });\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-stream.test.ts","s":1,"e":60,"m":1777368453917.2385,"t":"import { afterEach, describe, expect, it, vi } from \"vitest\";\nimport {\n  buildWorkspaceTreeStreamUrl,\n  normalizeTreeStreamSnapshot,\n  parseTreeStreamMessage,\n} from \"./protocol\";\n\ndescribe(\"tree-stream/protocol\", () => {\n  afterEach(() => {\n    vi.unstubAllGlobals();\n  });\n\n  it(\"构造 workspace sidebar stream url\", () => {\n    vi.stubGlobal(\"window\", {\n      ...window,\n      location: {\n        ...window.location,\n        origin: \"http://127.0.0.1:3000\",\n      },\n    });\n    expect(\n      buildWorkspaceTreeStreamUrl(\" ws_1 \", \"evt_9\"),\n    ).toBe(\n      \"http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1&cursor=evt_9\",\n    );\n  });\n\n  it(\"固定走同源 stream route\", () => {\n    vi.stubGlobal(\"window\", {\n      ...window,\n      location: {\n        ...window.location,\n        origin: \"http://127.0.0.1:3000\",\n      },\n    });\n    expect(\n      buildWorkspaceTreeStreamUrl(\"ws_1\", null),\n    ).toBe(\"http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1\");\n  });\n\n  it(\"解析 snapshot / delta / resync 协议消息\", () => {\n    expect(\n      parseTreeStreamMessage({\n        eventType: \"snapshot\",\n        rawData: JSON.stringify({\n          stream: \"workspace\",\n          workspaceId: \"ws_1\",\n          cursor: \"evt_1\",\n          projection: \"sidebar_tree\",\n          data: {\n            active_workspace_id: \"ws_1\",\n            workspaces: [],\n            documents: [],\n            kernel_file_tree_projection: {\n              projectionId: \"kernel_projection:file_tree:root\",\n              projection: \"file_tree\",\n              rootNodeId: null,\n              items: [],\n              edges: [],\n            },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-stream.test.ts","s":49,"e":108,"m":1777368453917.2385,"t":"projection: \"sidebar_tree\",\n          data: {\n            active_workspace_id: \"ws_1\",\n            workspaces: [],\n            documents: [],\n            kernel_file_tree_projection: {\n              projectionId: \"kernel_projection:file_tree:root\",\n              projection: \"file_tree\",\n              rootNodeId: null,\n              items: [],\n              edges: [],\n            },\n            kernel_sidebar_projection: {\n              projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n              projection: \"sidebar_tree\",\n              rootNodeId: null,\n              items: [],\n              edges: [],\n            },\n            trashed_documents: [],\n            media_assets: [],\n            trashed_media_assets: [],\n            mindmap_assets: [],\n            trashed_mindmap_assets: [],\n            table_assets: [],\n            trashed_table_assets: [],\n            mindmap_docs: [],\n            mindmap_asset_children: {},\n          },\n        }),\n      }),\n    ).toMatchObject({\n      kind: \"snapshot\",\n      stream: \"workspace\",\n      workspaceId: \"ws_1\",\n      cursor: \"evt_1\",\n      projection: \"sidebar_tree\",\n    });\n\n    expect(\n      parseTreeStreamMessage({\n        eventType: \"heartbeat\",\n        rawData: JSON.stringify({\n          kind: \"heartbeat\",\n          workspaceId: \"ws_1\",\n          cursor: \"evt_3\",\n        }),\n      }),\n    ).toMatchObject({\n      kind: \"heartbeat\",\n      workspaceId: \"ws_1\",\n      cursor: \"evt_3\",\n    });\n\n    expect(\n      parseTreeStreamMessage({\n        eventType: \"delta\",\n        rawData: JSON.stringify({\n          kind: \"delta\",\n          workspace_id: \"ws_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-stream.test.ts","s":97,"e":156,"m":1777368453917.2385,"t":").toMatchObject({\n      kind: \"heartbeat\",\n      workspaceId: \"ws_1\",\n      cursor: \"evt_3\",\n    });\n\n    expect(\n      parseTreeStreamMessage({\n        eventType: \"delta\",\n        rawData: JSON.stringify({\n          kind: \"delta\",\n          workspace_id: \"ws_1\",\n          cursor: \"evt_2\",\n          payload: { op: \"remove_document\", documentId: \"doc_1\" },\n        }),\n      }),\n    ).toMatchObject({\n      kind: \"delta\",\n      workspaceId: \"ws_1\",\n      cursor: \"evt_2\",\n      data: { op: \"remove_document\", documentId: \"doc_1\" },\n    });\n  });\n\n  it(\"把 sidebar.dataset.list snapshot 归一化成 SidebarInitialData\", () => {\n    const snapshot = normalizeTreeStreamSnapshot({\n      active_workspace_id: \"ws_1\",\n      workspaces: [],\n      documents: [],\n      kernel_file_tree_projection: {\n        projectionId: \"kernel_projection:file_tree:root\",\n        projection: \"file_tree\",\n        rootNodeId: null,\n        items: [],\n        edges: [],\n      },\n      kernel_sidebar_projection: {\n        projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n        projection: \"sidebar_tree\",\n        rootNodeId: null,\n        items: [],\n        edges: [],\n      },\n      trashed_documents: [],\n      media_assets: [],\n      trashed_media_assets: [],\n      mindmap_assets: [],\n      trashed_mindmap_assets: [],\n      table_assets: [],\n      trashed_table_assets: [],\n      mindmap_docs: [],\n      mindmap_asset_children: {},\n    });\n\n    expect(snapshot).toMatchObject({\n      activeWorkspaceId: \"ws_1\",\n      kernelFileTreeProjection: {\n        projection: \"file_tree\",\n      },\n      kernelSidebarProjection: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/tree-stream.test.ts","s":145,"e":162,"m":1777368453917.2385,"t":"table_assets: [],\n      trashed_table_assets: [],\n      mindmap_docs: [],\n      mindmap_asset_children: {},\n    });\n\n    expect(snapshot).toMatchObject({\n      activeWorkspaceId: \"ws_1\",\n      kernelFileTreeProjection: {\n        projection: \"file_tree\",\n      },\n      kernelSidebarProjection: {\n        projection: \"sidebar_tree\",\n      },\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":1,"e":60,"m":1778818257423.53,"t":"import { act, useEffect } from \"react\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { useSidebarTreeStream } from \"./use-sidebar-tree-stream\";\nimport type { SidebarInitialData } from \"@/components/sidebar/types\";\n\ntype MockEventListener = (event: MessageEvent<string>) => void;\n\nclass MockEventSource {\n  static instances: MockEventSource[] = [];\n\n  readonly url: string;\n  readonly withCredentials: boolean;\n  readonly listeners = new Map<string, Set<MockEventListener>>();\n  onmessage: MockEventListener | null = null;\n  onerror: (() => void) | null = null;\n  closed = false;\n\n  constructor(url: string, options?: EventSourceInit) {\n    this.url = url;\n    this.withCredentials = Boolean(options?.withCredentials);\n    MockEventSource.instances.push(this);\n  }\n\n  addEventListener(type: string, listener: EventListener) {\n    const typedListener = listener as unknown as MockEventListener;\n    const next = this.listeners.get(type) ?? new Set<MockEventListener>();\n    next.add(typedListener);\n    this.listeners.set(type, next);\n  }\n\n  removeEventListener(type: string, listener: EventListener) {\n    const typedListener = listener as unknown as MockEventListener;\n    this.listeners.get(type)?.delete(typedListener);\n  }\n\n  close() {\n    this.closed = true;\n  }\n\n  emit(type: string, payload: unknown) {\n    const event = {\n      type,\n      data: JSON.stringify(payload),\n    } as MessageEvent<string>;\n    this.listeners.get(type)?.forEach((listener) => listener(event));\n    if (type === \"message\" && this.onmessage) {\n      this.onmessage(event);\n    }\n  }\n}\n\nfunction flush() {\n  return new Promise((resolve) => {\n    setTimeout(resolve, 0);\n  });\n}\n\nfunction buildInitialData(): SidebarInitialData {\n  return {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":49,"e":108,"m":1778818257423.53,"t":"}\n  }\n}\n\nfunction flush() {\n  return new Promise((resolve) => {\n    setTimeout(resolve, 0);\n  });\n}\n\nfunction buildInitialData(): SidebarInitialData {\n  return {\n    activeWorkspaceId: \"ws_1\",\n    workspaces: [],\n    documents: [],\n    kernelSidebarProjection: {\n      projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n      projection: \"sidebar_tree\",\n      rootNodeId: null,\n      items: [],\n      edges: [],\n    },\n    kernelSidebarTree: [],\n    kernelFileTreeProjection: {\n      projectionId: \"kernel_projection:file_tree:workspace_root\",\n      projection: \"file_tree\",\n      rootNodeId: null,\n      items: [],\n      edges: [],\n    },\n    trashedDocuments: [],\n    trashedMediaAssets: [],\n    trashedMindmapAssets: [],\n    trashedTableAssets: [],\n    tableAssets: [],\n    mindmapDocs: [],\n    mindmapAssets: [],\n    mindmapAssetChildren: {},\n    mediaAssets: [],\n  };\n}\n\nfunction buildSnapshotEnvelope(title = \"工作区首页\") {\n  return {\n    stream: \"workspace\",\n    workspaceId: \"ws_1\",\n    cursor: \"evt_2\",\n    projection: \"sidebar_tree\",\n    data: {\n      active_workspace_id: \"ws_1\",\n      workspaces: [],\n      documents: [\n        {\n          id: \"page_root\",\n          workspace_id: \"ws_1\",\n          title,\n          parent_id: null,\n          sort_order: 0,\n          is_starred: false,\n          access_scope: \"private\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":97,"e":156,"m":1778818257423.53,"t":"data: {\n      active_workspace_id: \"ws_1\",\n      workspaces: [],\n      documents: [\n        {\n          id: \"page_root\",\n          workspace_id: \"ws_1\",\n          title,\n          parent_id: null,\n          sort_order: 0,\n          is_starred: false,\n          access_scope: \"private\",\n          is_template: false,\n          created_at: \"2026-04-24T00:00:00Z\",\n          updated_at: \"2026-04-24T00:00:00Z\",\n        },\n      ],\n      kernel_file_tree_projection: {\n        projectionId: \"kernel_projection:file_tree:root\",\n        projection: \"file_tree\",\n        rootNodeId: null,\n        items: [],\n        edges: [],\n      },\n      kernel_sidebar_projection: {\n        projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n        projection: \"sidebar_tree\",\n        rootNodeId: null,\n        items: [],\n        edges: [],\n      },\n      trashed_documents: [],\n      media_assets: [],\n      trashed_media_assets: [],\n      mindmap_assets: [],\n      trashed_mindmap_assets: [],\n      table_assets: [],\n      trashed_table_assets: [],\n      mindmap_docs: [],\n      mindmap_asset_children: {},\n    },\n  };\n}\n\nfunction Harness({ onState }: { onState: (state: ReturnType<typeof useSidebarTreeStream>) => void }) {\n  const state = useSidebarTreeStream(buildInitialData());\n\n  useEffect(() => {\n    onState(state);\n  }, [onState, state]);\n\n  return null;\n}\n\ndescribe(\"useSidebarTreeStream\", () => {\n  let container: HTMLDivElement;\n  let root: Root;\n  const onState = vi.fn();\n\n  beforeEach(() => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":145,"e":204,"m":1778818257423.53,"t":"onState(state);\n  }, [onState, state]);\n\n  return null;\n}\n\ndescribe(\"useSidebarTreeStream\", () => {\n  let container: HTMLDivElement;\n  let root: Root;\n  const onState = vi.fn();\n\n  beforeEach(() => {\n    (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =\n      true;\n    vi.stubGlobal(\"window\", {\n      ...window,\n      location: {\n        ...window.location,\n        origin: \"http://127.0.0.1:3000\",\n      },\n    });\n    MockEventSource.instances = [];\n    vi.stubGlobal(\"EventSource\", MockEventSource as unknown as typeof EventSource);\n    container = document.createElement(\"div\");\n    document.body.appendChild(container);\n    root = createRoot(container);\n    onState.mockClear();\n  });\n\n  afterEach(() => {\n    act(() => {\n      root.unmount();\n    });\n    container.remove();\n  });\n\n  it(\"收到带 cursor 的 snapshot 后不应重建 EventSource\", async () => {\n    await act(async () => {\n      root.render(<Harness onState={onState} />);\n      await flush();\n      await flush();\n    });\n\n    expect(MockEventSource.instances).toHaveLength(1);\n\n    await act(async () => {\n      MockEventSource.instances[0]?.emit(\"snapshot\", {\n        stream: \"workspace\",\n        workspaceId: \"ws_1\",\n        cursor: \"evt_2\",\n        projection: \"sidebar_tree\",\n        data: {\n          active_workspace_id: \"ws_1\",\n          workspaces: [],\n          documents: [],\n          kernel_sidebar_projection: {\n            projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n            projection: \"sidebar_tree\",\n            rootNodeId: null,\n            items: [],","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":193,"e":252,"m":1778818257423.53,"t":"workspaceId: \"ws_1\",\n        cursor: \"evt_2\",\n        projection: \"sidebar_tree\",\n        data: {\n          active_workspace_id: \"ws_1\",\n          workspaces: [],\n          documents: [],\n          kernel_sidebar_projection: {\n            projectionId: \"kernel_projection:sidebar_tree:workspace_root\",\n            projection: \"sidebar_tree\",\n            rootNodeId: null,\n            items: [],\n            edges: [],\n          },\n          trashed_documents: [],\n          media_assets: [],\n          trashed_media_assets: [],\n          mindmap_assets: [],\n          trashed_mindmap_assets: [],\n          table_assets: [],\n          trashed_table_assets: [],\n          mindmap_docs: [],\n          mindmap_asset_children: {},\n        },\n      });\n      await flush();\n      await flush();\n    });\n\n    expect(MockEventSource.instances).toHaveLength(1);\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        cursor: \"evt_2\",\n        status: \"live\",\n      }),\n    );\n  });\n\n  it(\"首帧前连接报错时应进入 fallback\", async () => {\n    await act(async () => {\n      root.render(<Harness onState={onState} />);\n      await flush();\n      await flush();\n    });\n\n    expect(MockEventSource.instances).toHaveLength(1);\n\n    await act(async () => {\n      MockEventSource.instances[0]?.onerror?.();\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        data: null,\n        status: \"fallback\",\n        cursor: null,\n        error: expect.any(Error),\n      }),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":241,"e":300,"m":1778818257423.53,"t":"MockEventSource.instances[0]?.onerror?.();\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        data: null,\n        status: \"fallback\",\n        cursor: null,\n        error: expect.any(Error),\n      }),\n    );\n    expect(MockEventSource.instances[0]?.closed).toBe(true);\n  });\n\n  it(\"resync_required delta 只推进 cursor，后续 resync 才替换数据\", async () => {\n    await act(async () => {\n      root.render(<Harness onState={onState} />);\n      await flush();\n      await flush();\n    });\n\n    expect(MockEventSource.instances).toHaveLength(1);\n\n    await act(async () => {\n      MockEventSource.instances[0]?.emit(\"snapshot\", buildSnapshotEnvelope(\"旧标题\"));\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        status: \"live\",\n        cursor: \"evt_2\",\n        data: expect.objectContaining({\n          documents: [expect.objectContaining({ title: \"旧标题\" })],\n        }),\n      }),\n    );\n\n    await act(async () => {\n      MockEventSource.instances[0]?.emit(\"delta\", {\n        stream: \"workspace\",\n        workspaceId: \"ws_1\",\n        cursor: \"evt_3\",\n        projection: \"sidebar_tree\",\n        data: {\n          op: \"resync_required\",\n          reason: \"documents_empty_trash\",\n        },\n      });\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        status: \"live\",\n        cursor: \"evt_3\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":289,"e":348,"m":1778818257423.53,"t":"op: \"resync_required\",\n          reason: \"documents_empty_trash\",\n        },\n      });\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        status: \"live\",\n        cursor: \"evt_3\",\n        data: expect.objectContaining({\n          documents: [expect.objectContaining({ title: \"旧标题\" })],\n        }),\n      }),\n    );\n\n    await act(async () => {\n      MockEventSource.instances[0]?.emit(\"resync\", buildSnapshotEnvelope(\"新标题\"));\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        status: \"live\",\n        cursor: \"evt_2\",\n        data: expect.objectContaining({\n          documents: [expect.objectContaining({ title: \"新标题\" })],\n        }),\n      }),\n    );\n  });\n\n  it(\"收到 snapshot 后连接中断也应切到 fallback，并保留最近一次 stream 数据\", async () => {\n    await act(async () => {\n      root.render(<Harness onState={onState} />);\n      await flush();\n      await flush();\n    });\n\n    expect(MockEventSource.instances).toHaveLength(1);\n\n    await act(async () => {\n      MockEventSource.instances[0]?.emit(\"snapshot\", buildSnapshotEnvelope(\"来自 stream 的标题\"));\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        status: \"live\",\n        cursor: \"evt_2\",\n        data: expect.objectContaining({\n          documents: [expect.objectContaining({ title: \"来自 stream 的标题\" })],\n        }),\n      }),\n    );","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx","s":337,"e":368,"m":1778818257423.53,"t":"});\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        status: \"live\",\n        cursor: \"evt_2\",\n        data: expect.objectContaining({\n          documents: [expect.objectContaining({ title: \"来自 stream 的标题\" })],\n        }),\n      }),\n    );\n\n    await act(async () => {\n      MockEventSource.instances[0]?.onerror?.();\n      await flush();\n      await flush();\n    });\n\n    expect(onState).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        status: \"fallback\",\n        cursor: \"evt_2\",\n        error: expect.any(Error),\n        data: expect.objectContaining({\n          documents: [expect.objectContaining({ title: \"来自 stream 的标题\" })],\n        }),\n      }),\n    );\n    expect(MockEventSource.instances[0]?.closed).toBe(true);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts","s":1,"e":60,"m":1777074446839.6677,"t":"\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { SidebarInitialData } from \"@/components/sidebar/types\";\nimport {\n  buildWorkspaceTreeStreamUrl,\n  normalizeTreeStreamSnapshot,\n  parseTreeStreamMessage,\n} from \"@/lib/tree-stream/protocol\";\nimport { applyTreeStreamDelta, type TreeStreamDeltaEvent } from \"@/lib/tree-stream/tree-delta\";\n\nexport interface SidebarTreeStreamState {\n  data: SidebarInitialData | null;\n  status: \"idle\" | \"connecting\" | \"live\" | \"fallback\";\n  cursor: string | null;\n  error: Error | null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction normalizeDeltaEvent(input: unknown): TreeStreamDeltaEvent | null {\n  if (!isRecord(input) || typeof input.op !== \"string\") {\n    return null;\n  }\n  return {\n    op: input.op as TreeStreamDeltaEvent[\"op\"],\n    node: isRecord(input.node) ? (input.node as TreeStreamDeltaEvent[\"node\"]) : null,\n    document: isRecord(input.document) ? (input.document as TreeStreamDeltaEvent[\"document\"]) : null,\n    documentId: typeof input.documentId === \"string\" ? input.documentId : null,\n    documents: Array.isArray(input.documents) ? (input.documents as TreeStreamDeltaEvent[\"documents\"]) : null,\n    sidebar: isRecord(input.sidebar) ? (input.sidebar as TreeStreamDeltaEvent[\"sidebar\"]) : null,\n  };\n}\n\nexport function useSidebarTreeStream(initialData: SidebarInitialData): SidebarTreeStreamState {\n  const workspaceId = initialData.activeWorkspaceId;\n  const streamEnabled = Boolean(workspaceId);\n  const cursorRef = useRef<string | null>(null);\n\n  const [state, setState] = useState<SidebarTreeStreamState>({\n    data: null,\n    status: streamEnabled ? \"connecting\" : \"idle\",\n    cursor: null,\n    error: null,\n  });\n  const eventSourceRef = useRef<EventSource | null>(null);\n\n  useEffect(() => {\n    cursorRef.current = state.cursor;\n  }, [state.cursor]);\n\n  useEffect(() => {\n    if (!streamEnabled) {\n      cursorRef.current = null;\n      setState({\n        data: null,\n        status: \"idle\",\n        cursor: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts","s":49,"e":108,"m":1777074446839.6677,"t":"useEffect(() => {\n    cursorRef.current = state.cursor;\n  }, [state.cursor]);\n\n  useEffect(() => {\n    if (!streamEnabled) {\n      cursorRef.current = null;\n      setState({\n        data: null,\n        status: \"idle\",\n        cursor: null,\n        error: null,\n      });\n      return undefined;\n    }\n\n    let cancelled = false;\n    let eventSource: EventSource | null = null;\n\n    const handleMessage = (event: MessageEvent<string>) => {\n      const envelope = parseTreeStreamMessage({\n        rawData: event.data,\n        eventType: event.type,\n      });\n      if (!envelope) {\n        return;\n      }\n\n      setState((previous) => {\n        const nextCursor = envelope.cursor ?? previous.cursor;\n\n        if (envelope.kind === \"snapshot\" || envelope.kind === \"resync\") {\n          const snapshot = normalizeTreeStreamSnapshot(envelope.data);\n          if (!snapshot) {\n            return {\n              ...previous,\n              cursor: nextCursor,\n            };\n          }\n          return {\n            data: snapshot,\n            status: \"live\",\n            cursor: nextCursor,\n            error: null,\n          };\n        }\n\n        if (envelope.kind === \"delta\" && previous.data) {\n          const deltaEvent = normalizeDeltaEvent(envelope.data);\n          if (!deltaEvent) {\n            return {\n              ...previous,\n              cursor: nextCursor,\n            };\n          }\n          return {\n            data: applyTreeStreamDelta(previous.data, deltaEvent),\n            status: \"live\",\n            cursor: nextCursor,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts","s":97,"e":156,"m":1777074446839.6677,"t":"if (envelope.kind === \"delta\" && previous.data) {\n          const deltaEvent = normalizeDeltaEvent(envelope.data);\n          if (!deltaEvent) {\n            return {\n              ...previous,\n              cursor: nextCursor,\n            };\n          }\n          return {\n            data: applyTreeStreamDelta(previous.data, deltaEvent),\n            status: \"live\",\n            cursor: nextCursor,\n            error: null,\n          };\n        }\n\n        return {\n          ...previous,\n          cursor: nextCursor,\n        };\n      });\n    };\n\n    const handleError = () => {\n      setState((previous) => ({\n        ...previous,\n        status: \"fallback\",\n        error: previous.error ?? new Error(\"tree stream 连接失败\"),\n      }));\n      eventSource?.close();\n      if (eventSourceRef.current === eventSource) {\n        eventSourceRef.current = null;\n      }\n    };\n\n    if (cancelled) {\n      return undefined;\n    }\n\n    const url = buildWorkspaceTreeStreamUrl(workspaceId, cursorRef.current);\n    eventSource = new EventSource(url, { withCredentials: true });\n    eventSourceRef.current = eventSource;\n    eventSource.addEventListener(\"snapshot\", handleMessage as EventListener);\n    eventSource.addEventListener(\"delta\", handleMessage as EventListener);\n    eventSource.addEventListener(\"resync\", handleMessage as EventListener);\n    eventSource.onmessage = handleMessage;\n    eventSource.onerror = handleError;\n\n    return () => {\n      cancelled = true;\n      eventSource?.removeEventListener(\"snapshot\", handleMessage as EventListener);\n      eventSource?.removeEventListener(\"delta\", handleMessage as EventListener);\n      eventSource?.removeEventListener(\"resync\", handleMessage as EventListener);\n      eventSource?.close();\n      if (eventSourceRef.current === eventSource) {\n        eventSourceRef.current = null;\n      }\n    };\n  }, [streamEnabled, workspaceId]);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts","s":145,"e":159,"m":1777074446839.6677,"t":"return () => {\n      cancelled = true;\n      eventSource?.removeEventListener(\"snapshot\", handleMessage as EventListener);\n      eventSource?.removeEventListener(\"delta\", handleMessage as EventListener);\n      eventSource?.removeEventListener(\"resync\", handleMessage as EventListener);\n      eventSource?.close();\n      if (eventSourceRef.current === eventSource) {\n        eventSourceRef.current = null;\n      }\n    };\n  }, [streamEnabled, workspaceId]);\n\n  return state;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/document-ai-orchestrator.test.ts","s":1,"e":60,"m":1777693689004.9893,"t":"import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nconst { mockBuildForwardHeaders } = vi.hoisted(() => ({\n  mockBuildForwardHeaders: vi.fn(async () => new Headers({ \"x-forwarded-for\": \"127.0.0.1\" })),\n}));\n\nvi.mock(\"@/lib/runtime-config\", () => ({\n  getMnoteRuntimeConfig: () => ({ backendUrl: \"http://backend.test\" }),\n}));\n\nvi.mock(\"@/lib/server/forward-headers\", () => ({\n  buildForwardHeaders: mockBuildForwardHeaders,\n}));\n\nimport { startDocumentAiOrchestratorRun } from \"./document-ai-orchestrator\";\n\ndescribe(\"startDocumentAiOrchestratorRun\", () => {\n  beforeEach(() => {\n    vi.restoreAllMocks();\n    mockBuildForwardHeaders.mockClear();\n    delete process.env.BACKEND_URL;\n    delete process.env.BACKEND_INTERNAL_URL;\n    delete process.env.MNOTE_AI_ORCHESTRATOR_API_KEY;\n  });\n\n  it(\"应把 leptos-tiptap 的 block 级 AI 上下文透传给 sidecar\", async () => {\n    const fetchMock = vi.fn(async () =>\n      new Response('event: ready\\ndata: {\"ok\":true}\\n\\n', {\n        headers: { \"Content-Type\": \"text/event-stream; charset=utf-8\" },\n      }),\n    );\n    vi.stubGlobal(\"fetch\", fetchMock);\n\n    await startDocumentAiOrchestratorRun({\n      userId: \"user-1\",\n      payload: {\n        maxSteps: 8,\n        messages: [{ role: \"user\", content: \"改写当前块\" }],\n        context: {\n          source: \"leptos-tiptap-island\",\n          action: \"ask_ai\",\n          documentId: \"doc-1\",\n          workspaceId: \"ws-1\",\n          selectedBlockId: \"block-1\",\n          selectedUids: [\"block-1\"],\n          selectedText: \"旧段落\",\n          selection: { currentBlockId: \"block-1\", state: { from: 1, to: 4 } },\n          tiptapDocument: { type: \"doc\", content: [{ type: \"paragraph\" }] },\n          documentBlocks: { type: \"doc\", content: [{ type: \"paragraph\" }] },\n        },\n        options: {\n          ai: {\n            modelKey: \"gpt-5.3-codex\",\n            profileId: \"page_writer_polish\",\n            sessionId: \"doc_ai:doc-1:session-1\",\n          },\n        },\n      },\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/document-ai-orchestrator.test.ts","s":49,"e":80,"m":1777693689004.9893,"t":"documentBlocks: { type: \"doc\", content: [{ type: \"paragraph\" }] },\n        },\n        options: {\n          ai: {\n            modelKey: \"gpt-5.3-codex\",\n            profileId: \"page_writer_polish\",\n            sessionId: \"doc_ai:doc-1:session-1\",\n          },\n        },\n      },\n    });\n\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"http://backend.test/api/v1/ai-agent/document/run\",\n      expect.objectContaining({ method: \"POST\" }),\n    );\n    const body = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));\n    expect(body.context).toMatchObject({\n      source: \"leptos-tiptap-island\",\n      action: \"ask_ai\",\n      documentId: \"doc-1\",\n      workspaceId: \"ws-1\",\n      selectedBlockId: \"block-1\",\n      selectedUids: [\"block-1\"],\n      selectedText: \"旧段落\",\n      selection: { currentBlockId: \"block-1\", state: { from: 1, to: 4 } },\n      tiptapDocument: { type: \"doc\", content: [{ type: \"paragraph\" }] },\n      documentBlocks: { type: \"doc\", content: [{ type: \"paragraph\" }] },\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/document-ai-orchestrator.ts","s":1,"e":60,"m":1777693689004.8645,"t":"import { getMnoteRuntimeConfig } from \"@/lib/runtime-config\";\nimport { buildForwardHeaders } from \"@/lib/server/forward-headers\";\nimport { pickLeptosTiptapRuntimePageOptions } from \"@/lib/documents/page-option-semantics\";\nimport type { DocumentAiCapabilityConfig } from \"@/lib/ai-agent/document-config\";\nimport type { PageOptionsState } from \"@/types/page-options\";\n\ntype AgentMessage = { role: \"user\" | \"assistant\"; content: string };\n\ntype RequestPayload = {\n  maxSteps?: number;\n  messages: AgentMessage[];\n  context?: {\n    source?: string;\n    action?: string;\n    documentId?: string;\n    workspaceId?: string;\n    selectedBlockId?: string;\n    selectedBlockIndex?: number;\n    selectedUids?: string[];\n    selectedText?: string;\n    selection?: unknown;\n    tiptapDocument?: unknown;\n    documentBlocks?: unknown;\n    pageOptions?: PageOptionsState;\n    node?: unknown;\n    subtree?: unknown;\n    outline?: unknown;\n    evidence?: unknown;\n  };\n  options?: {\n    ai?: {\n      model?: string;\n      modelKey?: string;\n      profileId?: string;\n      sessionId?: string;\n    };\n  };\n};\n\nfunction resolveBackendUrl(): string | null {\n  const cfg = getMnoteRuntimeConfig();\n  const backendUrl =\n    process.env.BACKEND_INTERNAL_URL || process.env.BACKEND_URL || cfg.backendUrl;\n  return backendUrl?.trim().replace(/\\/+$/, \"\") || null;\n}\n\nfunction buildErrorMessage(payload: unknown, fallback: string): string {\n  if (payload && typeof payload === \"object\" && \"detail\" in payload) {\n    return String((payload as Record<string, unknown>).detail ?? fallback);\n  }\n  if (payload && typeof payload === \"object\" && \"message\" in payload) {\n    return String((payload as Record<string, unknown>).message ?? fallback);\n  }\n  if (payload && typeof payload === \"object\" && \"error\" in payload) {\n    return String((payload as Record<string, unknown>).error ?? fallback);\n  }\n  return fallback;\n}\n\nexport async function startDocumentAiOrchestratorRun(input: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/document-ai-orchestrator.ts","s":49,"e":108,"m":1777693689004.8645,"t":"return String((payload as Record<string, unknown>).detail ?? fallback);\n  }\n  if (payload && typeof payload === \"object\" && \"message\" in payload) {\n    return String((payload as Record<string, unknown>).message ?? fallback);\n  }\n  if (payload && typeof payload === \"object\" && \"error\" in payload) {\n    return String((payload as Record<string, unknown>).error ?? fallback);\n  }\n  return fallback;\n}\n\nexport async function startDocumentAiOrchestratorRun(input: {\n  request?: Request;\n  userId: string;\n  payload: RequestPayload;\n}): Promise<Response> {\n  const backendUrl = resolveBackendUrl();\n  if (!backendUrl) {\n    throw new Error(\"未配置 BACKEND_URL\");\n  }\n\n  const headers = await buildForwardHeaders(input.request);\n  headers.set(\"Content-Type\", \"application/json\");\n  const apiKey = (process.env.MNOTE_AI_ORCHESTRATOR_API_KEY || \"\").trim();\n  if (apiKey) {\n    headers.set(\"x-mnote-ai-key\", apiKey);\n  }\n\n  const response = await fetch(`${backendUrl}/api/v1/ai-agent/document/run`, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify({\n      userId: input.userId,\n      sessionId: String(input.payload.options?.ai?.sessionId ?? \"\").trim() || null,\n      model: String(input.payload.options?.ai?.model ?? \"\").trim() || null,\n      modelKey: String(input.payload.options?.ai?.modelKey ?? \"\").trim() || null,\n      profileId: String(input.payload.options?.ai?.profileId ?? \"\").trim() || null,\n      maxSteps: input.payload.maxSteps,\n      messages: input.payload.messages,\n      context: {\n        source: input.payload.context?.source ?? null,\n        action: input.payload.context?.action ?? null,\n        documentId: input.payload.context?.documentId ?? null,\n        workspaceId: input.payload.context?.workspaceId ?? null,\n        selectedBlockId: input.payload.context?.selectedBlockId ?? null,\n        selectedBlockIndex: input.payload.context?.selectedBlockIndex ?? null,\n        selectedUids: input.payload.context?.selectedUids ?? null,\n        selectedText: input.payload.context?.selectedText ?? null,\n        selection: input.payload.context?.selection ?? null,\n        tiptapDocument: input.payload.context?.tiptapDocument ?? null,\n        documentBlocks: input.payload.context?.documentBlocks ?? null,\n        node: input.payload.context?.node ?? null,\n        subtree: input.payload.context?.subtree ?? null,\n        outline: input.payload.context?.outline ?? null,\n        evidence: input.payload.context?.evidence ?? null,\n        pageOptions: input.payload.context?.pageOptions ?? null,\n        editorRuntimePageOptions: input.payload.context?.pageOptions\n          ? pickLeptosTiptapRuntimePageOptions(input.payload.context.pageOptions)\n          : null,\n      },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/document-ai-orchestrator.ts","s":97,"e":152,"m":1777693689004.8645,"t":"selection: input.payload.context?.selection ?? null,\n        tiptapDocument: input.payload.context?.tiptapDocument ?? null,\n        documentBlocks: input.payload.context?.documentBlocks ?? null,\n        node: input.payload.context?.node ?? null,\n        subtree: input.payload.context?.subtree ?? null,\n        outline: input.payload.context?.outline ?? null,\n        evidence: input.payload.context?.evidence ?? null,\n        pageOptions: input.payload.context?.pageOptions ?? null,\n        editorRuntimePageOptions: input.payload.context?.pageOptions\n          ? pickLeptosTiptapRuntimePageOptions(input.payload.context.pageOptions)\n          : null,\n      },\n    }),\n    cache: \"no-store\",\n  });\n\n  if (!response.ok) {\n    const payload = await response.json().catch(() => null);\n    throw new Error(buildErrorMessage(payload, `AI orchestrator 请求失败：HTTP ${response.status}`));\n  }\n\n  if (!response.body) {\n    throw new Error(\"AI orchestrator 未返回事件流\");\n  }\n\n  return response;\n}\n\nexport async function fetchDocumentAiOrchestratorConfig(input?: {\n  request?: Request;\n}): Promise<DocumentAiCapabilityConfig> {\n  const backendUrl = resolveBackendUrl();\n  if (!backendUrl) {\n    throw new Error(\"未配置 BACKEND_URL\");\n  }\n\n  const headers = await buildForwardHeaders(input?.request);\n  const apiKey = (process.env.MNOTE_AI_ORCHESTRATOR_API_KEY || \"\").trim();\n  if (apiKey) {\n    headers.set(\"x-mnote-ai-key\", apiKey);\n  }\n\n  const response = await fetch(`${backendUrl}/api/v1/ai-agent/document/config`, {\n    method: \"GET\",\n    headers,\n    cache: \"no-store\",\n  });\n\n  if (!response.ok) {\n    const payload = await response.json().catch(() => null);\n    throw new Error(buildErrorMessage(payload, `AI orchestrator config 请求失败：HTTP ${response.status}`));\n  }\n\n  return (await response.json()) as DocumentAiCapabilityConfig;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/forward-headers.ts","s":1,"e":56,"m":1776880970589.118,"t":"import { headers } from \"next/headers\";\nimport { convexAuthNextjsToken } from \"@convex-dev/auth/nextjs/server\";\nimport { isDevAuthEnabled } from \"@/lib/auth/devUser\";\nimport { getAuthContext } from \"@/lib/auth/authContext\";\n\nfunction copyHeaderIfPresent(target: Headers, source: Headers, name: string) {\n  const value = source.get(name);\n  if (value) {\n    target.set(name, value);\n  }\n}\n\nexport async function buildForwardHeaders(request?: Request): Promise<Headers> {\n  const source = request?.headers ?? new Headers(await headers());\n  const forwarded = new Headers();\n\n  copyHeaderIfPresent(forwarded, source, \"cookie\");\n  copyHeaderIfPresent(forwarded, source, \"authorization\");\n  copyHeaderIfPresent(forwarded, source, \"x-request-id\");\n  copyHeaderIfPresent(forwarded, source, \"x-trace-id\");\n  copyHeaderIfPresent(forwarded, source, \"x-session-id\");\n  copyHeaderIfPresent(forwarded, source, \"x-mnote-workspace-id\");\n  copyHeaderIfPresent(forwarded, source, \"x-mnote-source-channel\");\n  copyHeaderIfPresent(forwarded, source, \"x-mnote-source-client\");\n  copyHeaderIfPresent(forwarded, source, \"x-mnote-actor-id\");\n  copyHeaderIfPresent(forwarded, source, \"x-mnote-actor-type\");\n  copyHeaderIfPresent(forwarded, source, \"user-agent\");\n\n  if (!forwarded.has(\"authorization\") && !isDevAuthEnabled()) {\n    const token = await convexAuthNextjsToken();\n    if (token?.trim()) {\n      forwarded.set(\"authorization\", `Bearer ${token.trim()}`);\n    }\n  }\n\n  if (!forwarded.has(\"x-mnote-source-channel\")) {\n    forwarded.set(\"x-mnote-source-channel\", request ? \"next_route\" : \"next_server_component\");\n  }\n  if (!forwarded.has(\"x-mnote-source-client\")) {\n    forwarded.set(\"x-mnote-source-client\", \"wolai-frontend\");\n  }\n  if (!forwarded.has(\"x-mnote-actor-id\")) {\n    try {\n      const auth = await getAuthContext();\n      if (auth.userId?.trim()) {\n        forwarded.set(\"x-mnote-actor-id\", auth.userId.trim());\n        forwarded.set(\"x-mnote-actor-type\", \"user\");\n      }\n    } catch {\n      // 说明：未登录或当前上下文无法解析用户时，继续走已有 header / admin fallback。\n    }\n  }\n\n  return forwarded;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/kernel-file-tree.test.ts","s":1,"e":60,"m":1777161381402.8752,"t":"import { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { ConvexHttpClient } from \"convex/browser\";\nimport { resolveKernelFileTreeProjection } from \"./kernel-file-tree\";\n\nconst mockBuildDocumentBridgeContextWithActor = vi.fn();\nconst mockBuildDocumentQueryEnvelope = vi.fn();\nconst mockExecuteRustBridgeQuery = vi.fn();\n\nvi.mock(\"@/lib/documents/bridge\", () => ({\n  buildDocumentBridgeContextWithActor: (...args: unknown[]) =>\n    mockBuildDocumentBridgeContextWithActor(...args),\n  buildDocumentQueryEnvelope: (...args: unknown[]) => mockBuildDocumentQueryEnvelope(...args),\n}));\n\nvi.mock(\"@/lib/documents/rust-runtime\", () => ({\n  executeRustBridgeQuery: (...args: unknown[]) => mockExecuteRustBridgeQuery(...args),\n}));\n\ndescribe(\"resolveKernelFileTreeProjection\", () => {\n  beforeEach(() => {\n    mockBuildDocumentBridgeContextWithActor.mockReset().mockReturnValue({\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n    });\n    mockBuildDocumentQueryEnvelope.mockReset().mockImplementation((input) => input);\n    mockExecuteRustBridgeQuery.mockReset().mockResolvedValue({\n      projectionId: \"kernel_projection:file_tree:page_root\",\n      projection: \"file_tree\",\n      rootNodeId: \"page_root\",\n      items: [],\n      edges: [],\n    });\n  });\n\n  it(\"把搜索词传入 Rust kernel.project_view，而不是交给宿主裁剪\", async () => {\n    await resolveKernelFileTreeProjection({\n      client: { query: vi.fn() } as unknown as ConvexHttpClient,\n      request: new Request(\"http://127.0.0.1:3000/api/tree/projections/file\"),\n      workspaceId: \"ws_1\",\n      actor: {\n        actorType: \"user\",\n        actorId: \"user_1\",\n        sessionId: null,\n      },\n      dataset: {\n        active_workspace_id: \"ws_1\",\n        documents: [],\n      },\n      rootNodeId: \"page_root\",\n      depth: 2,\n      query: \"  预算  \",\n      maxResults: 12,\n    });\n\n    expect(mockBuildDocumentQueryEnvelope).toHaveBeenCalledWith({\n      name: \"kernel.project_view\",\n      payload: expect.objectContaining({\n        projection: \"file_tree\",\n        workspaceId: \"ws_1\",\n        rootNodeId: \"page_root\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/kernel-file-tree.test.ts","s":49,"e":69,"m":1777161381402.8752,"t":"rootNodeId: \"page_root\",\n      depth: 2,\n      query: \"  预算  \",\n      maxResults: 12,\n    });\n\n    expect(mockBuildDocumentQueryEnvelope).toHaveBeenCalledWith({\n      name: \"kernel.project_view\",\n      payload: expect.objectContaining({\n        projection: \"file_tree\",\n        workspaceId: \"ws_1\",\n        rootNodeId: \"page_root\",\n        depth: 2,\n        query: \"预算\",\n        maxResults: 12,\n      }),\n    });\n    expect(mockExecuteRustBridgeQuery).toHaveBeenCalledTimes(1);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/kernel-file-tree.ts","s":1,"e":60,"m":1777161587442.5288,"t":"import type { ConvexHttpClient } from \"convex/browser\";\nimport type { SidebarDatasetListQueryResult } from \"@/lib/sidebar-data\";\nimport type { KernelFileTreeProjection } from \"@/lib/kernel-file-tree\";\nimport {\n  buildDocumentBridgeContextWithActor,\n  buildDocumentQueryEnvelope,\n  type BridgeActor,\n} from \"@/lib/documents/bridge\";\nimport { executeRustBridgeQuery } from \"@/lib/documents/rust-runtime\";\n\nexport async function resolveKernelFileTreeProjection(input: {\n  client: ConvexHttpClient;\n  request: Request;\n  workspaceId: string;\n  actor: BridgeActor;\n  dataset: SidebarDatasetListQueryResult;\n  rootNodeId?: string | null;\n  depth?: number | null;\n  query?: string | null;\n  maxResults?: number | null;\n}): Promise<KernelFileTreeProjection> {\n  const context = buildDocumentBridgeContextWithActor({\n    request: input.request,\n    actor: input.actor,\n    workspaceId: input.workspaceId,\n  });\n  const query = input.query?.trim() || null;\n  const maxResults =\n    typeof input.maxResults === \"number\" && Number.isFinite(input.maxResults)\n      ? Math.max(1, Math.floor(input.maxResults))\n      : null;\n\n  return executeRustBridgeQuery<KernelFileTreeProjection>({\n    context,\n    envelope: buildDocumentQueryEnvelope({\n      name: \"kernel.project_view\",\n      payload: {\n        projection: \"file_tree\",\n        workspaceId: input.workspaceId,\n        rootNodeId: input.rootNodeId ?? null,\n        depth: input.depth ?? null,\n        query,\n        maxResults,\n        includeEdges: true,\n        includeContent: false,\n        nodeTypes: [\"page\"],\n      },\n    }),\n    data: input.dataset as unknown as Record<string, unknown>,\n  });\n}\n\nexport function attachKernelFileTreeProjection<T extends SidebarDatasetListQueryResult>(input: {\n  dataset: T;\n  projection: KernelFileTreeProjection;\n}): T {\n  return {\n    ...input.dataset,\n    kernel_file_tree_projection: input.projection,\n  };","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/kernel-file-tree.ts","s":49,"e":62,"m":1777161587442.5288,"t":"data: input.dataset as unknown as Record<string, unknown>,\n  });\n}\n\nexport function attachKernelFileTreeProjection<T extends SidebarDatasetListQueryResult>(input: {\n  dataset: T;\n  projection: KernelFileTreeProjection;\n}): T {\n  return {\n    ...input.dataset,\n    kernel_file_tree_projection: input.projection,\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/local-paths.ts","s":1,"e":32,"m":1773495901192.8645,"t":"import \"server-only\";\n\nimport path from \"path\";\n\n/**\n * 本地文件路径策略（服务端专用）\n *\n * 目标：\n * - 开发/传统模式：继续使用 `process.cwd()/public/*`（便于本地调试与静态访问）。\n * - 桌面端（Electron）：通过注入 `MNOTE_DATA_DIR=<安装目录>\\\\data`，把可变数据\n *   放到安装目录下（而不是 resources/app.asar），避免权限与覆盖问题。\n */\n\nfunction cleanEnvValue(value: unknown): string | null {\n  if (typeof value !== \"string\") return null;\n  const v = value.trim();\n  return v.length > 0 ? v : null;\n}\n\nexport function getDocumentsBaseDir(): string {\n  const dataDir = cleanEnvValue(process.env.MNOTE_DATA_DIR);\n  if (dataDir) return path.join(dataDir, \"documents\");\n  return path.join(process.cwd(), \"public\", \"documents\");\n}\n\nexport function getLegacyMindmapsBaseDir(): string {\n  const dataDir = cleanEnvValue(process.env.MNOTE_DATA_DIR);\n  if (dataDir) return path.join(dataDir, \"mindmaps\");\n  return path.join(process.cwd(), \"public\", \"mindmaps\");\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mindmap-files.ts","s":1,"e":60,"m":1773495901206.186,"t":"import \"server-only\";\n\nimport path from \"path\";\nimport { promises as fs } from \"fs\";\nimport type { MediaAsset } from \"@/types/media\";\nimport { getDocumentsBaseDir, getLegacyMindmapsBaseDir } from \"@/lib/server/local-paths\";\n\nconst tryAccess = async (file: string) => {\n  try {\n    await fs.access(file);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nexport async function detectLocalMindmapDocs(docIds: string[]): Promise<string[]> {\n  const results: string[] = [];\n  const preferredBaseDir = getDocumentsBaseDir();\n  const legacyBaseDir = getLegacyMindmapsBaseDir();\n  for (const id of docIds) {\n    const folder = path.join(preferredBaseDir, id);\n    const preferredLegacy = path.join(folder, \"mindmap.json\");\n    const legacy = path.join(legacyBaseDir, id, \"mindmap.json\");\n    let has = false;\n    if ((await tryAccess(preferredLegacy)) || (await tryAccess(legacy))) {\n      has = true;\n    } else {\n      try {\n        const entries = await fs.readdir(folder);\n        has = entries.some((name) => /^mindmap-.+\\.json$/i.test(name));\n      } catch {\n        has = false;\n      }\n    }\n    if (has) {\n      results.push(id);\n    }\n  }\n  return results;\n}\n\nexport type LocalMindmapFile = {\n  documentId: string;\n  mindmapId: string;\n  fileName: string;\n  source: \"preferred\" | \"legacy\";\n};\n\nexport async function detectLocalMindmapFiles(docIds: string[]): Promise<LocalMindmapFile[]> {\n  const results: LocalMindmapFile[] = [];\n  const preferredBaseDir = getDocumentsBaseDir();\n  const legacyBaseDir = getLegacyMindmapsBaseDir();\n  for (const id of docIds) {\n    const folder = path.join(preferredBaseDir, id);\n    try {\n      const entries = await fs.readdir(folder);\n      for (const name of entries) {\n        if (name === \"mindmap.json\") {\n          // mindmap.json 属于旧版单文件导图：mindmapId 必须包含 docId，避免不同页面间 ID 冲突","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mindmap-files.ts","s":49,"e":108,"m":1773495901206.186,"t":"export async function detectLocalMindmapFiles(docIds: string[]): Promise<LocalMindmapFile[]> {\n  const results: LocalMindmapFile[] = [];\n  const preferredBaseDir = getDocumentsBaseDir();\n  const legacyBaseDir = getLegacyMindmapsBaseDir();\n  for (const id of docIds) {\n    const folder = path.join(preferredBaseDir, id);\n    try {\n      const entries = await fs.readdir(folder);\n      for (const name of entries) {\n        if (name === \"mindmap.json\") {\n          // mindmap.json 属于旧版单文件导图：mindmapId 必须包含 docId，避免不同页面间 ID 冲突\n          results.push({\n            documentId: id,\n            mindmapId: `legacy-${id}`,\n            fileName: name,\n            source: \"preferred\",\n          });\n          continue;\n        }\n        const m = /^mindmap-(.+)\\.json$/i.exec(name);\n        if (m && m[1]) {\n          results.push({ documentId: id, mindmapId: m[1], fileName: name, source: \"preferred\" });\n        }\n      }\n    } catch {\n      // ignore\n    }\n\n    // 旧目录（仅支持 mindmap.json）\n    const legacy = path.join(legacyBaseDir, id, \"mindmap.json\");\n    if (await tryAccess(legacy)) {\n      results.push({\n        documentId: id,\n        mindmapId: `legacy-${id}`,\n        fileName: \"mindmap.json\",\n        source: \"legacy\",\n      });\n    }\n  }\n  return results;\n}\n\nfunction extractMindmapImageAssetIdsFromData(input: unknown): string[] {\n  const root = (() => {\n    if (!input || typeof input !== \"object\") return input;\n    const record = input;\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    return (record && typeof record === \"object\" && \"root\" in (record as any)) ? (record as any).root : input;\n  })();\n\n  const ids: string[] = [];\n  const seen = new Set<string>();\n\n  const push = (value: unknown) => {\n    if (typeof value !== \"string\") return;\n    if (!value.startsWith(\"asset:\")) return;\n    const id = value.slice(\"asset:\".length).trim();\n    if (!id) return;\n    if (seen.has(id)) return;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mindmap-files.ts","s":97,"e":156,"m":1773495901206.186,"t":"return (record && typeof record === \"object\" && \"root\" in (record as any)) ? (record as any).root : input;\n  })();\n\n  const ids: string[] = [];\n  const seen = new Set<string>();\n\n  const push = (value: unknown) => {\n    if (typeof value !== \"string\") return;\n    if (!value.startsWith(\"asset:\")) return;\n    const id = value.slice(\"asset:\".length).trim();\n    if (!id) return;\n    if (seen.has(id)) return;\n    seen.add(id);\n    ids.push(id);\n  };\n\n  const get = (obj: unknown, key: string): unknown => {\n    if (!obj || typeof obj !== \"object\") return undefined;\n    return (obj as Record<string, unknown>)[key];\n  };\n\n  const walk = (node: unknown) => {\n    if (!node || typeof node !== \"object\") return;\n\n    const data = get(node, \"data\");\n    const image = get(node, \"image\");\n\n    // 常见：node.data.image = \"asset:xxx\"\n    push(get(data, \"image\"));\n\n    // 兼容：node.image = \"asset:xxx\"\n    push(image);\n\n    // 兼容：node.image.url = \"asset:xxx\"\n    push(get(image, \"url\"));\n\n    // 兼容：node.data.image.url = \"asset:xxx\"\n    push(get(get(data, \"image\"), \"url\"));\n\n    const children = get(node, \"children\");\n    if (Array.isArray(children)) {\n      children.forEach(walk);\n    }\n  };\n\n  walk(root);\n  return ids;\n}\n\ntype TrashedMindmapMeta = {\n  docId: string;\n  mindmapId: string;\n  originalFileName: string;\n  originalPath: string;\n  trashedFileName: string;\n  deleted_at: string;\n};\n\nasync function tryReadJsonFile<T>(file: string): Promise<T | null> {\n  try {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mindmap-files.ts","s":145,"e":204,"m":1773495901206.186,"t":"type TrashedMindmapMeta = {\n  docId: string;\n  mindmapId: string;\n  originalFileName: string;\n  originalPath: string;\n  trashedFileName: string;\n  deleted_at: string;\n};\n\nasync function tryReadJsonFile<T>(file: string): Promise<T | null> {\n  try {\n    const content = await fs.readFile(file, \"utf8\");\n    return JSON.parse(content) as T;\n  } catch {\n    return null;\n  }\n}\n\nexport async function detectLocalMindmapImageAssetIdsByMindmapId(\n  files: LocalMindmapFile[],\n): Promise<Record<string, string[]>> {\n  const mapping: Record<string, string[]> = {};\n  const preferredBaseDir = getDocumentsBaseDir();\n  const legacyBaseDir = getLegacyMindmapsBaseDir();\n\n  for (const item of files) {\n    const baseDir = item.source === \"legacy\" ? legacyBaseDir : preferredBaseDir;\n    const filePath = path.join(baseDir, item.documentId, item.fileName);\n    const data = await tryReadJsonFile<unknown>(filePath);\n    if (!data) continue;\n    const ids = extractMindmapImageAssetIdsFromData(data);\n    if (ids.length > 0) {\n      mapping[item.mindmapId] = ids;\n    }\n  }\n\n  return mapping;\n}\n\nasync function listTrashMetasForFolder(folder: string): Promise<TrashedMindmapMeta[]> {\n  const trashDir = path.join(folder, \".trash\");\n  try {\n    const entries = await fs.readdir(trashDir);\n    const metas: TrashedMindmapMeta[] = [];\n    for (const name of entries) {\n      if (!name.endsWith(\".deleted.json\")) continue;\n      const metaPath = path.join(trashDir, name);\n      const meta = await tryReadJsonFile<TrashedMindmapMeta>(metaPath);\n      if (meta?.docId && meta?.mindmapId && meta?.trashedFileName && meta?.deleted_at) {\n        // 若原路径已存在（用户撤销/恢复），则不再展示为垃圾桶记录，避免堆积。\n        try {\n          await fs.access(meta.originalPath);\n          await fs.rm(path.join(trashDir, meta.trashedFileName), { force: true });\n          await fs.rm(metaPath, { force: true });\n          continue;\n        } catch {\n          // ignore\n        }\n        metas.push(meta);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mindmap-files.ts","s":193,"e":252,"m":1773495901206.186,"t":"const meta = await tryReadJsonFile<TrashedMindmapMeta>(metaPath);\n      if (meta?.docId && meta?.mindmapId && meta?.trashedFileName && meta?.deleted_at) {\n        // 若原路径已存在（用户撤销/恢复），则不再展示为垃圾桶记录，避免堆积。\n        try {\n          await fs.access(meta.originalPath);\n          await fs.rm(path.join(trashDir, meta.trashedFileName), { force: true });\n          await fs.rm(metaPath, { force: true });\n          continue;\n        } catch {\n          // ignore\n        }\n        metas.push(meta);\n      }\n    }\n    return metas;\n  } catch {\n    return [];\n  }\n}\n\nexport async function detectLocalTrashedMindmapAssets(\n  workspaceId: string,\n  docIds: string[],\n): Promise<MediaAsset[]> {\n  const results: MediaAsset[] = [];\n  const preferredBaseDir = getDocumentsBaseDir();\n  const legacyBaseDir = getLegacyMindmapsBaseDir();\n\n  for (const docId of docIds) {\n    const preferredFolder = path.join(preferredBaseDir, docId);\n    const legacyFolder = path.join(legacyBaseDir, docId);\n    const metas = [\n      ...(await listTrashMetasForFolder(preferredFolder)),\n      ...(await listTrashMetasForFolder(legacyFolder)),\n    ];\n\n    metas.forEach((meta) => {\n      results.push({\n        id: meta.mindmapId,\n        workspace_id: workspaceId,\n        document_id: docId,\n        asset_type: \"mindmap\",\n        file_url: null,\n        thumbnail_url: null,\n        bucket: null,\n        storage_path: null,\n        file_name: meta.originalFileName,\n        file_size: null,\n        mime_type: \"application/json\",\n        ocr_payload: undefined,\n        ocr_strategy: null,\n        ocr_text: null,\n        ocr_status: null,\n        deleted_at: meta.deleted_at,\n        deleted_by: null,\n        purged_at: null,\n        signed_url: null,\n        created_at: \"\",\n        updated_at: \"\",\n      });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mindmap-files.ts","s":241,"e":260,"m":1773495901206.186,"t":"mime_type: \"application/json\",\n        ocr_payload: undefined,\n        ocr_strategy: null,\n        ocr_text: null,\n        ocr_status: null,\n        deleted_at: meta.deleted_at,\n        deleted_by: null,\n        purged_at: null,\n        signed_url: null,\n        created_at: \"\",\n        updated_at: \"\",\n      });\n    });\n  }\n\n  results.sort((a, b) => (b.deleted_at ?? \"\").localeCompare(a.deleted_at ?? \"\"));\n  return results;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.test.ts","s":1,"e":60,"m":1778355539132.2737,"t":"import { EventEmitter } from \"node:events\";\nimport { PassThrough } from \"node:stream\";\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { PageOptionsState } from \"@/types/page-options\";\n\nconst {\n  mockSpawn,\n  mockBuildDocumentBridgeContext,\n  mockBuildDocumentCommandEnvelope,\n  mockExecutePageWriteBridgeCommand,\n} = vi.hoisted(() => ({\n  mockSpawn: vi.fn(),\n  mockBuildDocumentBridgeContext: vi.fn(),\n  mockBuildDocumentCommandEnvelope: vi.fn(),\n  mockExecutePageWriteBridgeCommand: vi.fn(),\n}));\n\nvi.mock(\"node:child_process\", () => ({\n  default: {\n    spawn: mockSpawn,\n  },\n  spawn: mockSpawn,\n}));\n\nvi.mock(\"next/server\", () => ({\n  NextResponse: class NextResponse extends Response {\n    static json(body: unknown, init?: ResponseInit) {\n      return new Response(JSON.stringify(body), {\n        ...init,\n        headers: {\n          \"Content-Type\": \"application/json\",\n          ...(init?.headers ?? {}),\n        },\n      });\n    }\n  },\n}));\n\nvi.mock(\"@/lib/documents/bridge\", () => ({\n  buildDocumentBridgeContext: mockBuildDocumentBridgeContext,\n  buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope,\n}));\n\nvi.mock(\"@/lib/documents/page-write-command-adapter\", () => ({\n  executePageWriteBridgeCommand: mockExecutePageWriteBridgeCommand,\n}));\n\nimport {\n  extractPageOptionsPatchFromAiMessage,\n  startMnoteCliAgentHostRun,\n} from \"./mnote-cli-agent-host\";\n\nfunction buildPageOptions(overrides: Partial<PageOptionsState> = {}): PageOptionsState {\n  return {\n    wideLayout: false,\n    smallText: false,\n    showHeadingNumbers: false,\n    showToc: false,\n    showStructure: false,\n    protectEditing: false,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.test.ts","s":49,"e":108,"m":1778355539132.2737,"t":"extractPageOptionsPatchFromAiMessage,\n  startMnoteCliAgentHostRun,\n} from \"./mnote-cli-agent-host\";\n\nfunction buildPageOptions(overrides: Partial<PageOptionsState> = {}): PageOptionsState {\n  return {\n    wideLayout: false,\n    smallText: false,\n    showHeadingNumbers: false,\n    showToc: false,\n    showStructure: false,\n    protectEditing: false,\n    showWordCount: true,\n    collapseBacklinks: false,\n    pageFont: \"default\",\n    layoutDensity: \"normal\",\n    hideChildPages: false,\n    showBlockRefCount: false,\n    embedDefaultBlockId: null,\n    ...overrides,\n  };\n}\n\nfunction createMockChild(stdoutText: string) {\n  const child = new EventEmitter() as EventEmitter & {\n    stdout: PassThrough;\n    stderr: PassThrough;\n    kill: ReturnType<typeof vi.fn>;\n  };\n  child.stdout = new PassThrough();\n  child.stderr = new PassThrough();\n  child.kill = vi.fn();\n  setTimeout(() => {\n    child.stdout.end(stdoutText);\n    child.stderr.end(\"\");\n    child.emit(\"close\", 0);\n  }, 0);\n  return child;\n}\n\ndescribe(\"startMnoteCliAgentHostRun\", () => {\n  beforeEach(() => {\n    mockSpawn.mockReset();\n    mockBuildDocumentBridgeContext.mockReset();\n    mockBuildDocumentCommandEnvelope.mockReset();\n    mockExecutePageWriteBridgeCommand.mockReset();\n    delete process.env.DEV_USER_ID;\n    delete process.env.DEV_USER_EMAIL;\n    delete process.env.DEV_USER_NAME;\n  });\n\n  it(\"启动 mnote-cli 时应把当前 Web 用户作为默认 CLI 写入身份下发\", async () => {\n    mockSpawn.mockImplementation(() => createMockChild('{\"ok\":true}\\n'));\n\n    const response = await startMnoteCliAgentHostRun({\n      request: new Request(\"http://127.0.0.1:3000/api/ai-agent/run\"),\n      userId: \"a4b72c17-49e3-46d3-8456-24a0a7044d64\",\n      userEmail: \"dev@mnote.local\",\n      userName: \"开发用户\",\n      payload: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.test.ts","s":97,"e":156,"m":1778355539132.2737,"t":"delete process.env.DEV_USER_NAME;\n  });\n\n  it(\"启动 mnote-cli 时应把当前 Web 用户作为默认 CLI 写入身份下发\", async () => {\n    mockSpawn.mockImplementation(() => createMockChild('{\"ok\":true}\\n'));\n\n    const response = await startMnoteCliAgentHostRun({\n      request: new Request(\"http://127.0.0.1:3000/api/ai-agent/run\"),\n      userId: \"a4b72c17-49e3-46d3-8456-24a0a7044d64\",\n      userEmail: \"dev@mnote.local\",\n      userName: \"开发用户\",\n      payload: {\n        stream: false,\n        messages: [{ role: \"user\", content: \"写入测试页\" }],\n        context: {\n          documentId: \"doc-1\",\n          workspaceId: \"ws_req_1778035004501_15\",\n        },\n      },\n    });\n\n    expect(response.status).toBe(200);\n    expect(mockSpawn).toHaveBeenCalledWith(\n      \"cargo\",\n      expect.arrayContaining([\n        \"--args-json\",\n        expect.stringContaining('\"workspaceId\":\"ws_req_1778035004501_15\"'),\n      ]),\n      expect.objectContaining({\n        env: expect.objectContaining({\n          DEV_USER_ID: \"a4b72c17-49e3-46d3-8456-24a0a7044d64\",\n          DEV_USER_EMAIL: \"dev@mnote.local\",\n          DEV_USER_NAME: \"开发用户\",\n          MNOTE_CLI_ALLOW_CREATE_PAGE: \"1\",\n          MNOTE_CLI_ALLOW_EDIT: \"1\",\n        }),\n      }),\n    );\n  });\n\n  it(\"应把当前页面 workspaceId 传入 CLI argsJson，避免 agent 写入错误工作空间\", async () => {\n    mockSpawn.mockImplementation(() => createMockChild('{\"ok\":true}\\n'));\n\n    await startMnoteCliAgentHostRun({\n      request: new Request(\"http://127.0.0.1:3000/api/ai-agent/run\"),\n      userId: \"a4b72c17-49e3-46d3-8456-24a0a7044d64\",\n      userEmail: \"dev@mnote.local\",\n      userName: \"开发用户\",\n      payload: {\n        stream: false,\n        messages: [{ role: \"user\", content: \"在当前空间写测试页\" }],\n        context: {\n          documentId: \"tree_1778036320856_1\",\n          workspaceId: \"ws_req_1778035004501_15\",\n        },\n      },\n    });\n\n    const args = mockSpawn.mock.calls[0]?.[1] as string[];\n    const argsJson = args[args.indexOf(\"--args-json\") + 1];","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.test.ts","s":145,"e":204,"m":1778355539132.2737,"t":"payload: {\n        stream: false,\n        messages: [{ role: \"user\", content: \"在当前空间写测试页\" }],\n        context: {\n          documentId: \"tree_1778036320856_1\",\n          workspaceId: \"ws_req_1778035004501_15\",\n        },\n      },\n    });\n\n    const args = mockSpawn.mock.calls[0]?.[1] as string[];\n    const argsJson = args[args.indexOf(\"--args-json\") + 1];\n    expect(JSON.parse(argsJson)).toMatchObject({\n      documentId: \"tree_1778036320856_1\",\n      workspaceId: \"ws_req_1778035004501_15\",\n    });\n  });\n\n  it(\"应从自然语言页面设置请求中提取 wired patch\", () => {\n    expect(\n      extractPageOptionsPatchFromAiMessage({\n        messages: [{ role: \"user\", content: \"请显示目录，改成紧凑排版，并切换成宋体\" }],\n        currentPageOptions: buildPageOptions(),\n      }),\n    ).toEqual({\n      showToc: true,\n      layoutDensity: \"compact\",\n      pageFont: \"song\",\n    });\n  });\n\n  it(\"命中页面设置 patch 时应直接发结构化 tool_result，而不是启动 cargo\", async () => {\n    mockBuildDocumentBridgeContext.mockResolvedValue({\n      workspaceId: \"ws-1\",\n      requestId: \"req-1\",\n      traceId: \"trace-1\",\n      actor: {\n        actorType: \"user\",\n        actorId: \"user-1\",\n        sessionId: null,\n      },\n      source: {\n        channel: \"next-route\",\n        client: \"wolai-frontend\",\n      },\n      deploymentId: null,\n      projectId: null,\n      tenantId: null,\n      authToken: null,\n      idempotencyKey: null,\n      validateOnly: false,\n      dryRun: false,\n    });\n    mockBuildDocumentCommandEnvelope.mockImplementation((input) => ({\n      ...input,\n      commandId: \"cmd-1\",\n      idempotencyKey: null,\n      actor: input.context.actor,\n      source: input.context.source,\n      target: input.target ?? null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.test.ts","s":193,"e":245,"m":1778355539132.2737,"t":"authToken: null,\n      idempotencyKey: null,\n      validateOnly: false,\n      dryRun: false,\n    });\n    mockBuildDocumentCommandEnvelope.mockImplementation((input) => ({\n      ...input,\n      commandId: \"cmd-1\",\n      idempotencyKey: null,\n      actor: input.context.actor,\n      source: input.context.source,\n      target: input.target ?? null,\n      preflightData: null,\n      reason: input.reason ?? null,\n      refs: input.refs ?? [],\n      dryRun: input.context.dryRun,\n      validateOnly: input.context.validateOnly,\n    }));\n    mockExecutePageWriteBridgeCommand.mockResolvedValue({\n      requestId: \"req-1\",\n      traceId: \"trace-1\",\n      commandId: \"cmd-1\",\n      commandName: \"page.layout.updateOptions\",\n      revision: null,\n      conflictDetectionKey: null,\n    });\n\n    const response = await startMnoteCliAgentHostRun({\n      request: new Request(\"http://127.0.0.1:3000/api/ai-agent/run\"),\n      userId: \"user-1\",\n      payload: {\n        stream: true,\n        messages: [{ role: \"user\", content: \"请显示目录，并切换成紧凑排版\" }],\n        context: {\n          documentId: \"doc-1\",\n          workspaceId: \"ws-1\",\n          pageOptions: buildPageOptions(),\n        },\n      },\n    });\n\n    const text = await response.text();\n\n    expect(mockSpawn).not.toHaveBeenCalled();\n    expect(mockExecutePageWriteBridgeCommand).toHaveBeenCalledTimes(1);\n    expect(text).toContain(\"event: tool_call\");\n    expect(text).toContain('\"tool\":\"page_options_patch\"');\n    expect(text).toContain('\"action\":\"update_page_options\"');\n    expect(text).toContain('\"showToc\":true');\n    expect(text).toContain('\"layoutDensity\":\"compact\"');\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":1,"e":60,"m":1778355614589.1711,"t":"import { spawn } from \"node:child_process\";\nimport { access } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { NextResponse } from \"next/server\";\nimport type { PageOptionsState } from \"@/types/page-options\";\nimport { pickLeptosTiptapRuntimePageOptions } from \"@/lib/documents/page-option-semantics\";\nimport { PAGE_OPTION_SEMANTICS } from \"@/lib/documents/page-option-semantics\";\nimport {\n  assertOptionsPatch,\n  buildDocumentBridgeContext,\n  buildDocumentCommandEnvelope,\n} from \"@/lib/documents/bridge\";\nimport { PAGE_COMMAND_NAMES } from \"@/lib/documents/page-command-contract\";\nimport { executePageWriteBridgeCommand } from \"@/lib/documents/page-write-command-adapter\";\n\ntype AgentMessage = { role: \"user\" | \"assistant\"; content: string };\n\nexport type MnoteCliAgentRunPayload = {\n  stream?: boolean;\n  maxSteps?: number;\n  scope?: string;\n  messages: AgentMessage[];\n  context?: {\n    documentId?: string;\n    workspaceId?: string;\n    documentBlocks?: unknown;\n    pageOptions?: PageOptionsState;\n    node?: unknown;\n    subtree?: unknown;\n    outline?: unknown;\n    evidence?: unknown;\n    selectedUids?: string[];\n  };\n  options?: {\n    ai?: {\n      provider?: string;\n      sessionId?: string;\n      modelKey?: string;\n      profileId?: string;\n      model?: string;\n    };\n  };\n};\n\ntype CliRunResult = {\n  code: number | null;\n  stdout: string;\n  stderr: string;\n};\n\nconst CLI_HOST_TIMEOUT_MS = 30_000;\nconst PAGE_OPTIONS_TOOL_NAME = \"page_options_patch\";\n\nconst toSseFrame = (event: string, data: unknown) => {\n  const json = JSON.stringify(data ?? null);\n  return `event: ${event}\\ndata: ${json}\\n\\n`;\n};\n\nconst AI_WRITABLE_BOOLEAN_PAGE_OPTIONS = [\n  \"wideLayout\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":49,"e":108,"m":1778355614589.1711,"t":"};\n\nconst CLI_HOST_TIMEOUT_MS = 30_000;\nconst PAGE_OPTIONS_TOOL_NAME = \"page_options_patch\";\n\nconst toSseFrame = (event: string, data: unknown) => {\n  const json = JSON.stringify(data ?? null);\n  return `event: ${event}\\ndata: ${json}\\n\\n`;\n};\n\nconst AI_WRITABLE_BOOLEAN_PAGE_OPTIONS = [\n  \"wideLayout\",\n  \"smallText\",\n  \"showHeadingNumbers\",\n  \"showToc\",\n  \"showWordCount\",\n  \"collapseBacklinks\",\n  \"hideChildPages\",\n] as const satisfies ReadonlyArray<keyof PageOptionsState>;\n\nconst AI_WRITABLE_PAGE_FONT_VALUES = [\"default\", \"song\", \"kai\"] as const;\nconst AI_WRITABLE_LAYOUT_DENSITY_VALUES = [\"compact\", \"normal\", \"spacious\"] as const;\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction normalizeAiWritablePageOptionsPatch(value: unknown): Partial<PageOptionsState> | null {\n  if (!isPlainRecord(value)) {\n    return null;\n  }\n\n  const patch: Partial<PageOptionsState> = {};\n  for (const key of AI_WRITABLE_BOOLEAN_PAGE_OPTIONS) {\n    if (PAGE_OPTION_SEMANTICS[key].runtimeSupport !== \"wired\") {\n      continue;\n    }\n    const nextValue = value[key];\n    if (typeof nextValue === \"boolean\") {\n      patch[key] = nextValue;\n    }\n  }\n\n  const rawPageFont = value.pageFont;\n  if (\n    PAGE_OPTION_SEMANTICS.pageFont.runtimeSupport === \"wired\" &&\n    typeof rawPageFont === \"string\" &&\n    (AI_WRITABLE_PAGE_FONT_VALUES as readonly string[]).includes(rawPageFont)\n  ) {\n    patch.pageFont = rawPageFont;\n  }\n\n  const rawLayoutDensity = value.layoutDensity;\n  if (\n    PAGE_OPTION_SEMANTICS.layoutDensity.runtimeSupport === \"wired\" &&\n    typeof rawLayoutDensity === \"string\" &&\n    (AI_WRITABLE_LAYOUT_DENSITY_VALUES as readonly string[]).includes(rawLayoutDensity)\n  ) {\n    patch.layoutDensity = rawLayoutDensity;\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":97,"e":156,"m":1778355614589.1711,"t":") {\n    patch.pageFont = rawPageFont;\n  }\n\n  const rawLayoutDensity = value.layoutDensity;\n  if (\n    PAGE_OPTION_SEMANTICS.layoutDensity.runtimeSupport === \"wired\" &&\n    typeof rawLayoutDensity === \"string\" &&\n    (AI_WRITABLE_LAYOUT_DENSITY_VALUES as readonly string[]).includes(rawLayoutDensity)\n  ) {\n    patch.layoutDensity = rawLayoutDensity;\n  }\n\n  const rawEmbedDefaultBlockId = value.embedDefaultBlockId;\n  if (PAGE_OPTION_SEMANTICS.embedDefaultBlockId.runtimeSupport === \"wired\") {\n    if (rawEmbedDefaultBlockId === null) {\n      patch.embedDefaultBlockId = null;\n    } else if (typeof rawEmbedDefaultBlockId === \"string\" && rawEmbedDefaultBlockId.trim()) {\n      patch.embedDefaultBlockId = rawEmbedDefaultBlockId.trim();\n    }\n  }\n\n  if (Object.keys(patch).length === 0) {\n    return null;\n  }\n  assertOptionsPatch(patch);\n  return patch;\n}\n\nfunction readLatestUserMessage(messages: AgentMessage[]): string {\n  for (let index = messages.length - 1; index >= 0; index -= 1) {\n    const message = messages[index];\n    if (message?.role === \"user\" && String(message.content ?? \"\").trim()) {\n      return String(message.content);\n    }\n  }\n  return \"\";\n}\n\nfunction extractStructuredPageOptionsPatch(text: string): Partial<PageOptionsState> | null {\n  const tagged = text.match(/<update_page_options>([\\s\\S]*?)<\\/update_page_options>/i);\n  const candidates = [tagged?.[1] ?? \"\", text];\n\n  for (const candidate of candidates) {\n    const trimmed = candidate.trim();\n    if (!trimmed) {\n      continue;\n    }\n    try {\n      const parsed = JSON.parse(trimmed) as unknown;\n      if (!isPlainRecord(parsed)) {\n        continue;\n      }\n      const payload =\n        String(parsed.action ?? \"\") === \"update_page_options\" && isPlainRecord(parsed.pageOptionsPatch)\n          ? parsed.pageOptionsPatch\n          : parsed;\n      const normalized = normalizeAiWritablePageOptionsPatch(payload);\n      if (normalized) {\n        return normalized;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":145,"e":204,"m":1778355614589.1711,"t":"try {\n      const parsed = JSON.parse(trimmed) as unknown;\n      if (!isPlainRecord(parsed)) {\n        continue;\n      }\n      const payload =\n        String(parsed.action ?? \"\") === \"update_page_options\" && isPlainRecord(parsed.pageOptionsPatch)\n          ? parsed.pageOptionsPatch\n          : parsed;\n      const normalized = normalizeAiWritablePageOptionsPatch(payload);\n      if (normalized) {\n        return normalized;\n      }\n    } catch {\n      // ignore\n    }\n  }\n\n  return null;\n}\n\nfunction extractNaturalLanguagePageOptionsPatch(text: string): Partial<PageOptionsState> | null {\n  const normalizedText = text.trim();\n  if (!normalizedText) {\n    return null;\n  }\n\n  const patch: Partial<PageOptionsState> = {};\n\n  if (/(显示|打开|开启).*(目录|标题目录)|显示标题目录/.test(normalizedText)) {\n    patch.showToc = true;\n  } else if (/(隐藏|关闭).*(目录|标题目录)|关闭标题目录/.test(normalizedText)) {\n    patch.showToc = false;\n  }\n\n  if (/(开启|打开|启用|改成|切换成).*(宽版|宽布局|自适应宽度)|使用宽版/.test(normalizedText)) {\n    patch.wideLayout = true;\n  } else if (/(关闭|取消|恢复).*(宽版|宽布局|自适应宽度)|恢复标准宽度/.test(normalizedText)) {\n    patch.wideLayout = false;\n  }\n\n  if (/(开启|打开|启用|改成|切换成).*(小字体)|使用小字体/.test(normalizedText)) {\n    patch.smallText = true;\n  } else if (/(关闭|取消|恢复).*(小字体)|恢复正常字体大小/.test(normalizedText)) {\n    patch.smallText = false;\n  }\n\n  if (/(显示|打开|开启).*(标题编号|标题自动编号)/.test(normalizedText)) {\n    patch.showHeadingNumbers = true;\n  } else if (/(隐藏|关闭).*(标题编号|标题自动编号)/.test(normalizedText)) {\n    patch.showHeadingNumbers = false;\n  }\n\n  if (/宋体/.test(normalizedText)) {\n    patch.pageFont = \"song\";\n  } else if (/楷体/.test(normalizedText)) {\n    patch.pageFont = \"kai\";\n  } else if (/(默认字体|恢复默认字体)/.test(normalizedText)) {\n    patch.pageFont = \"default\";\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":193,"e":252,"m":1778355614589.1711,"t":"patch.showHeadingNumbers = true;\n  } else if (/(隐藏|关闭).*(标题编号|标题自动编号)/.test(normalizedText)) {\n    patch.showHeadingNumbers = false;\n  }\n\n  if (/宋体/.test(normalizedText)) {\n    patch.pageFont = \"song\";\n  } else if (/楷体/.test(normalizedText)) {\n    patch.pageFont = \"kai\";\n  } else if (/(默认字体|恢复默认字体)/.test(normalizedText)) {\n    patch.pageFont = \"default\";\n  }\n\n  if (/紧凑/.test(normalizedText)) {\n    patch.layoutDensity = \"compact\";\n  } else if (/(宽松|疏朗)/.test(normalizedText)) {\n    patch.layoutDensity = \"spacious\";\n  } else if (/(默认间距|标准间距|正常间距)/.test(normalizedText)) {\n    patch.layoutDensity = \"normal\";\n  }\n\n  return Object.keys(patch).length > 0 ? patch : null;\n}\n\nexport function extractPageOptionsPatchFromAiMessage(input: {\n  messages: AgentMessage[];\n  currentPageOptions?: PageOptionsState | null;\n}): Partial<PageOptionsState> | null {\n  void input.currentPageOptions;\n  const latestUserMessage = readLatestUserMessage(input.messages);\n  return (\n    extractStructuredPageOptionsPatch(latestUserMessage) ??\n    extractNaturalLanguagePageOptionsPatch(latestUserMessage)\n  );\n}\n\nfunction describePageOptionsPatch(patch: Partial<PageOptionsState>): string {\n  const labels: string[] = [];\n  if (\"showToc\" in patch) labels.push(patch.showToc ? \"显示目录\" : \"隐藏目录\");\n  if (\"wideLayout\" in patch) labels.push(patch.wideLayout ? \"启用宽版\" : \"关闭宽版\");\n  if (\"smallText\" in patch) labels.push(patch.smallText ? \"启用小字体\" : \"关闭小字体\");\n  if (\"showHeadingNumbers\" in patch) {\n    labels.push(patch.showHeadingNumbers ? \"显示标题编号\" : \"隐藏标题编号\");\n  }\n  if (\"pageFont\" in patch) {\n    labels.push(\n      patch.pageFont === \"song\" ? \"切换为宋体\" : patch.pageFont === \"kai\" ? \"切换为楷体\" : \"恢复默认字体\",\n    );\n  }\n  if (\"layoutDensity\" in patch) {\n    labels.push(\n      patch.layoutDensity === \"compact\"\n        ? \"切换为紧凑排版\"\n        : patch.layoutDensity === \"spacious\"\n          ? \"切换为宽松排版\"\n          : \"恢复标准排版\",\n    );\n  }\n  if (\"embedDefaultBlockId\" in patch) {\n    labels.push(patch.embedDefaultBlockId ? \"更新嵌入默认位置\" : \"清除嵌入默认位置\");","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":241,"e":300,"m":1778355614589.1711,"t":"}\n  if (\"layoutDensity\" in patch) {\n    labels.push(\n      patch.layoutDensity === \"compact\"\n        ? \"切换为紧凑排版\"\n        : patch.layoutDensity === \"spacious\"\n          ? \"切换为宽松排版\"\n          : \"恢复标准排版\",\n    );\n  }\n  if (\"embedDefaultBlockId\" in patch) {\n    labels.push(patch.embedDefaultBlockId ? \"更新嵌入默认位置\" : \"清除嵌入默认位置\");\n  }\n  if (\"showWordCount\" in patch) labels.push(patch.showWordCount ? \"显示字数统计\" : \"隐藏字数统计\");\n  if (\"collapseBacklinks\" in patch) {\n    labels.push(patch.collapseBacklinks ? \"折叠反向引用\" : \"展开反向引用\");\n  }\n  if (\"hideChildPages\" in patch) labels.push(patch.hideChildPages ? \"隐藏子页面\" : \"显示子页面\");\n  return labels.length > 0 ? `已更新页面设置：${labels.join(\"，\")}。` : \"已更新页面设置。\";\n}\n\nasync function pathExists(targetPath: string) {\n  try {\n    await access(targetPath);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nasync function resolveRepoRoot() {\n  let dir = path.resolve(process.cwd());\n  for (let i = 0; i < 8; i += 1) {\n    if (await pathExists(path.join(dir, \"rust\", \"Cargo.toml\"))) {\n      return dir;\n    }\n    const parent = path.dirname(dir);\n    if (parent === dir) break;\n    dir = parent;\n  }\n  return path.resolve(process.cwd(), \"..\");\n}\n\nfunction buildCliArgs(input: { repoRoot: string; userId: string; payload: MnoteCliAgentRunPayload }) {\n  const sessionId = String(input.payload.options?.ai?.sessionId ?? \"\").trim() || `ai-${Date.now()}`;\n  const documentId = String(input.payload.context?.documentId ?? \"\").trim() || \"current\";\n  const workspaceId = String(input.payload.context?.workspaceId ?? \"\").trim() || null;\n  const argsJson = JSON.stringify({\n    pageId: documentId,\n    documentId,\n    workspaceId,\n    provider: input.payload.options?.ai?.provider ?? null,\n    modelKey: input.payload.options?.ai?.modelKey ?? null,\n    profileId: input.payload.options?.ai?.profileId ?? null,\n    selectedUids: input.payload.context?.selectedUids ?? null,\n    pageOptions: input.payload.context?.pageOptions ?? null,\n    editorRuntimePageOptions: input.payload.context?.pageOptions\n      ? pickLeptosTiptapRuntimePageOptions(input.payload.context.pageOptions)\n      : null,\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":289,"e":348,"m":1778355614589.1711,"t":"pageId: documentId,\n    documentId,\n    workspaceId,\n    provider: input.payload.options?.ai?.provider ?? null,\n    modelKey: input.payload.options?.ai?.modelKey ?? null,\n    profileId: input.payload.options?.ai?.profileId ?? null,\n    selectedUids: input.payload.context?.selectedUids ?? null,\n    pageOptions: input.payload.context?.pageOptions ?? null,\n    editorRuntimePageOptions: input.payload.context?.pageOptions\n      ? pickLeptosTiptapRuntimePageOptions(input.payload.context.pageOptions)\n      : null,\n  });\n\n  return [\n    \"run\",\n    \"--quiet\",\n    \"--manifest-path\",\n    path.join(input.repoRoot, \"rust\", \"Cargo.toml\"),\n    \"-p\",\n    \"mnote-cli\",\n    \"--\",\n    \"--json\",\n    \"--validate-only\",\n    \"--dry-run\",\n    \"--actor-id\",\n    input.userId,\n    \"--actor-type\",\n    \"user\",\n    \"--session-id\",\n    sessionId,\n    \"--reason\",\n    \"ai-agent-run:mnote-cli-host\",\n    \"tool\",\n    \"run\",\n    \"--tool-name\",\n    \"doc_get\",\n    \"--kind\",\n    \"query\",\n    \"--mode\",\n    \"explain-plan\",\n    \"--args-json\",\n    argsJson,\n  ];\n}\n\nasync function runMnoteCli(input: {\n  userId: string;\n  userEmail?: string;\n  userName?: string;\n  payload: MnoteCliAgentRunPayload;\n}): Promise<CliRunResult> {\n  const repoRoot = await resolveRepoRoot();\n  const args = buildCliArgs({ repoRoot, userId: input.userId, payload: input.payload });\n\n  return new Promise<CliRunResult>((resolve, reject) => {\n    const child = spawn(\"cargo\", args, {\n      cwd: repoRoot,\n      env: {\n        ...process.env,\n        CARGO_TERM_COLOR: \"never\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":337,"e":396,"m":1778355614589.1711,"t":"userName?: string;\n  payload: MnoteCliAgentRunPayload;\n}): Promise<CliRunResult> {\n  const repoRoot = await resolveRepoRoot();\n  const args = buildCliArgs({ repoRoot, userId: input.userId, payload: input.payload });\n\n  return new Promise<CliRunResult>((resolve, reject) => {\n    const child = spawn(\"cargo\", args, {\n      cwd: repoRoot,\n      env: {\n        ...process.env,\n        CARGO_TERM_COLOR: \"never\",\n        RUSTUP_TOOLCHAIN: process.env.RUSTUP_TOOLCHAIN?.trim() || \"1.89.0\",\n        DEV_USER_ID: input.userId,\n        DEV_USER_EMAIL: input.userEmail?.trim() || process.env.DEV_USER_EMAIL || \"dev@mnote.local\",\n        DEV_USER_NAME: input.userName?.trim() || process.env.DEV_USER_NAME || \"开发用户\",\n        MNOTE_CLI_ALLOW_CREATE_PAGE: process.env.MNOTE_CLI_ALLOW_CREATE_PAGE || \"1\",\n        MNOTE_CLI_ALLOW_EDIT: process.env.MNOTE_CLI_ALLOW_EDIT || \"1\",\n      },\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n    });\n\n    let stdout = \"\";\n    let stderr = \"\";\n    let timedOut = false;\n    const timeout = setTimeout(() => {\n      timedOut = true;\n      child.kill(\"SIGKILL\");\n    }, CLI_HOST_TIMEOUT_MS);\n\n    child.stdout.setEncoding(\"utf8\");\n    child.stderr.setEncoding(\"utf8\");\n    child.stdout.on(\"data\", (chunk: string) => {\n      stdout += chunk;\n    });\n    child.stderr.on(\"data\", (chunk: string) => {\n      stderr += chunk;\n    });\n    child.on(\"error\", (error) => {\n      clearTimeout(timeout);\n      reject(error);\n    });\n    child.on(\"close\", (code) => {\n      clearTimeout(timeout);\n      if (timedOut) {\n        reject(new Error(\"mnote-cli host 执行超时\"));\n        return;\n      }\n      resolve({ code, stdout, stderr });\n    });\n  });\n}\n\nasync function executeAiPageOptionsPatch(input: {\n  request: Request;\n  payload: MnoteCliAgentRunPayload;\n  patch: Partial<PageOptionsState>;\n}) {\n  const documentId = String(input.payload.context?.documentId ?? \"\").trim();\n  const workspaceId = String(input.payload.context?.workspaceId ?? \"\").trim() || null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":385,"e":444,"m":1778355614589.1711,"t":"resolve({ code, stdout, stderr });\n    });\n  });\n}\n\nasync function executeAiPageOptionsPatch(input: {\n  request: Request;\n  payload: MnoteCliAgentRunPayload;\n  patch: Partial<PageOptionsState>;\n}) {\n  const documentId = String(input.payload.context?.documentId ?? \"\").trim();\n  const workspaceId = String(input.payload.context?.workspaceId ?? \"\").trim() || null;\n  const bridgeContext = await buildDocumentBridgeContext({\n    request: input.request,\n    workspaceId,\n  });\n  const envelope = buildDocumentCommandEnvelope({\n    name: PAGE_COMMAND_NAMES.updateLayout,\n    payload: {\n      documentId,\n      workspaceId,\n      options: input.patch,\n    },\n    context: bridgeContext,\n    target: {\n      workspaceId,\n      pageId: documentId,\n    },\n    reason: \"ai-agent-run:mnote-cli-host:update-page-options\",\n    refs: [\"mnote-cli-host\", \"page_options_patch\"],\n  });\n  const result = await executePageWriteBridgeCommand({\n    context: bridgeContext,\n    envelope,\n  });\n  return {\n    documentId,\n    patch: input.patch,\n    meta: {\n      requestId: result.requestId,\n      traceId: result.traceId,\n      commandId: result.commandId,\n      commandName: result.commandName,\n    },\n    assistantText: describePageOptionsPatch(input.patch),\n  };\n}\n\nexport async function startMnoteCliAgentHostRun(input: {\n  request: Request;\n  userId: string;\n  userEmail?: string;\n  userName?: string;\n  payload: MnoteCliAgentRunPayload;\n}): Promise<Response> {\n  const pageOptionsPatch = extractPageOptionsPatchFromAiMessage({\n    messages: input.payload.messages,\n    currentPageOptions: input.payload.context?.pageOptions ?? null,\n  });\n  const shouldHandlePageOptionsPatch =","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":433,"e":492,"m":1778355614589.1711,"t":"export async function startMnoteCliAgentHostRun(input: {\n  request: Request;\n  userId: string;\n  userEmail?: string;\n  userName?: string;\n  payload: MnoteCliAgentRunPayload;\n}): Promise<Response> {\n  const pageOptionsPatch = extractPageOptionsPatchFromAiMessage({\n    messages: input.payload.messages,\n    currentPageOptions: input.payload.context?.pageOptions ?? null,\n  });\n  const shouldHandlePageOptionsPatch =\n    pageOptionsPatch &&\n    String(input.payload.context?.documentId ?? \"\").trim() &&\n    String(input.payload.context?.workspaceId ?? \"\").trim();\n\n  if (shouldHandlePageOptionsPatch) {\n    const toolCallId = `${PAGE_OPTIONS_TOOL_NAME}_${Date.now()}`;\n    const toolArgs = {\n      documentId: String(input.payload.context?.documentId ?? \"\").trim(),\n      pageOptionsPatch,\n    };\n    if (!input.payload.stream) {\n      const result = await executeAiPageOptionsPatch({\n        request: input.request,\n        payload: input.payload,\n        patch: pageOptionsPatch,\n      });\n      return NextResponse.json({\n        ok: true,\n        bridgeOwner: \"mnote-cli\",\n        text: result.assistantText,\n        toolResult: {\n          action: \"update_page_options\",\n          documentId: result.documentId,\n          pageOptionsPatch: result.patch,\n          meta: result.meta,\n        },\n      });\n    }\n\n    const body = new ReadableStream<Uint8Array>({\n      async start(controller) {\n        const encoder = new TextEncoder();\n        controller.enqueue(encoder.encode(toSseFrame(\"ready\", { ok: true, bridgeOwner: \"mnote-cli\" })));\n        controller.enqueue(\n          encoder.encode(\n            toSseFrame(\"tool_call\", {\n              id: toolCallId,\n              tool: PAGE_OPTIONS_TOOL_NAME,\n              args: toolArgs,\n            }),\n          ),\n        );\n        const startedAt = Date.now();\n        try {\n          const result = await executeAiPageOptionsPatch({\n            request: input.request,\n            payload: input.payload,\n            patch: pageOptionsPatch,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":481,"e":540,"m":1778355614589.1711,"t":"id: toolCallId,\n              tool: PAGE_OPTIONS_TOOL_NAME,\n              args: toolArgs,\n            }),\n          ),\n        );\n        const startedAt = Date.now();\n        try {\n          const result = await executeAiPageOptionsPatch({\n            request: input.request,\n            payload: input.payload,\n            patch: pageOptionsPatch,\n          });\n          const toolResult = {\n            action: \"update_page_options\",\n            documentId: result.documentId,\n            pageOptionsPatch: result.patch,\n            meta: result.meta,\n          };\n          controller.enqueue(\n            encoder.encode(\n              toSseFrame(\"tool_result\", {\n                id: toolCallId,\n                tool: PAGE_OPTIONS_TOOL_NAME,\n                ok: true,\n                ms: Math.max(0, Date.now() - startedAt),\n                result: toolResult,\n              }),\n            ),\n          );\n          controller.enqueue(encoder.encode(toSseFrame(\"assistant_message\", { text: result.assistantText })));\n          controller.enqueue(\n            encoder.encode(toSseFrame(\"completion\", { ok: true, text: result.assistantText, steps: 1 })),\n          );\n        } catch (error) {\n          const message = error instanceof Error ? error.message : String(error);\n          controller.enqueue(\n            encoder.encode(\n              toSseFrame(\"tool_result\", {\n                id: toolCallId,\n                tool: PAGE_OPTIONS_TOOL_NAME,\n                ok: false,\n                ms: Math.max(0, Date.now() - startedAt),\n                result: { error: message },\n              }),\n            ),\n          );\n          controller.enqueue(encoder.encode(toSseFrame(\"error\", { ok: false, message })));\n        } finally {\n          controller.close();\n        }\n      },\n    });\n\n    return new Response(body, {\n      headers: {\n        \"Content-Type\": \"text/event-stream; charset=utf-8\",\n        \"Cache-Control\": \"no-cache, no-transform\",\n        Connection: \"keep-alive\",\n        \"X-Accel-Buffering\": \"no\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":529,"e":588,"m":1778355614589.1711,"t":"} finally {\n          controller.close();\n        }\n      },\n    });\n\n    return new Response(body, {\n      headers: {\n        \"Content-Type\": \"text/event-stream; charset=utf-8\",\n        \"Cache-Control\": \"no-cache, no-transform\",\n        Connection: \"keep-alive\",\n        \"X-Accel-Buffering\": \"no\",\n        \"x-mnote-ai-execution-owner\": \"mnote-cli\",\n      },\n    });\n  }\n\n  const run = runMnoteCli({\n    userId: input.userId,\n    userEmail: input.userEmail,\n    userName: input.userName,\n    payload: input.payload,\n  });\n\n  if (!input.payload.stream) {\n    const result = await run;\n    if (result.code !== 0) {\n      return NextResponse.json({ error: result.stderr || \"mnote-cli 执行失败\" }, { status: 500 });\n    }\n    return NextResponse.json({\n      ok: true,\n      bridgeOwner: \"mnote-cli\",\n      text: result.stdout.trim(),\n    });\n  }\n\n  const body = new ReadableStream<Uint8Array>({\n    async start(controller) {\n      const encoder = new TextEncoder();\n      controller.enqueue(encoder.encode(toSseFrame(\"ready\", { ok: true, bridgeOwner: \"mnote-cli\" })));\n      try {\n        const result = await run;\n        if (result.code !== 0) {\n          controller.enqueue(\n            encoder.encode(toSseFrame(\"error\", { ok: false, message: result.stderr || \"mnote-cli 执行失败\" })),\n          );\n        } else {\n          const text = result.stdout.trim() || \"mnote-cli 无输出\";\n          controller.enqueue(encoder.encode(toSseFrame(\"assistant_message\", { text })));\n          controller.enqueue(encoder.encode(toSseFrame(\"completion\", { ok: true, text, steps: 1 })));\n        }\n      } catch (error) {\n        controller.enqueue(\n          encoder.encode(toSseFrame(\"error\", { ok: false, message: error instanceof Error ? error.message : String(error) })),\n        );\n      } finally {\n        controller.close();\n      }\n    },\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/mnote-cli-agent-host.ts","s":577,"e":600,"m":1778355614589.1711,"t":"controller.enqueue(encoder.encode(toSseFrame(\"assistant_message\", { text })));\n          controller.enqueue(encoder.encode(toSseFrame(\"completion\", { ok: true, text, steps: 1 })));\n        }\n      } catch (error) {\n        controller.enqueue(\n          encoder.encode(toSseFrame(\"error\", { ok: false, message: error instanceof Error ? error.message : String(error) })),\n        );\n      } finally {\n        controller.close();\n      }\n    },\n  });\n\n  return new Response(body, {\n    headers: {\n      \"Content-Type\": \"text/event-stream; charset=utf-8\",\n      \"Cache-Control\": \"no-cache, no-transform\",\n      Connection: \"keep-alive\",\n      \"X-Accel-Buffering\": \"no\",\n      \"x-mnote-ai-execution-owner\": \"mnote-cli\",\n    },\n  });\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/sidebar-data.ts","s":1,"e":60,"m":1776916598549.44,"t":"import { randomUUID } from \"crypto\";\nimport type { ConvexHttpClient } from \"convex/browser\";\nimport type { FunctionReference } from \"convex/server\";\nimport type { SidebarInitialData } from \"@/components/sidebar/types\";\nimport type { DocumentRecord } from \"@/lib/documents\";\nimport { api } from \"@/lib/convex/api\";\nimport {\n  mapSidebarDatasetListQueryResultToInitialData,\n  type SidebarDatasetListQueryResult,\n} from \"@/lib/sidebar-data\";\nimport type { WorkspaceSummary } from \"@/lib/workspaces\";\nimport {\n  attachKernelFileTreeProjection,\n  resolveKernelFileTreeProjection,\n} from \"@/lib/server/kernel-file-tree\";\nimport type { AuthContext } from \"@/lib/auth/types\";\n\ntype LoadSidebarDataFromConvexInput = {\n  client: ConvexHttpClient;\n  auth: AuthContext;\n  fallbackName: string;\n  requestedWorkspaceId?: string | null;\n};\n\ntype LoadSidebarDataFromConvexResult = {\n  workspaces: WorkspaceSummary[];\n  activeWorkspaceId: string;\n  targetWorkspaceId: string | null;\n  sidebarDataset: SidebarDatasetListQueryResult | null;\n  sidebarInitialData: SidebarInitialData | null;\n  documents: DocumentRecord[];\n};\n\nconst sidebarDatasetListQuery = ((api as unknown as Record<string, unknown>).sidebar as\n  | Record<string, unknown>\n  | undefined)?.datasetList as FunctionReference<\n  \"query\",\n  \"public\",\n  { workspaceId: string },\n  SidebarDatasetListQueryResult\n>;\n\nexport async function loadSidebarDataFromConvex(\n  input: LoadSidebarDataFromConvexInput,\n): Promise<LoadSidebarDataFromConvexResult> {\n  const bootstrap = await input.client.mutation(api.workspaces.ensureDefaultWorkspace, {\n    fallbackName: input.fallbackName,\n    workspaceIdIfCreate: randomUUID(),\n  });\n\n  const workspaces = bootstrap.workspaces;\n  const activeWorkspaceId = bootstrap.activeWorkspaceId;\n  const targetWorkspaceId = input.requestedWorkspaceId?.trim() || activeWorkspaceId || null;\n\n  if (!targetWorkspaceId) {\n    return {\n      workspaces,\n      activeWorkspaceId,\n      targetWorkspaceId: null,\n      sidebarDataset: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/server/sidebar-data.ts","s":49,"e":99,"m":1776916598549.44,"t":"});\n\n  const workspaces = bootstrap.workspaces;\n  const activeWorkspaceId = bootstrap.activeWorkspaceId;\n  const targetWorkspaceId = input.requestedWorkspaceId?.trim() || activeWorkspaceId || null;\n\n  if (!targetWorkspaceId) {\n    return {\n      workspaces,\n      activeWorkspaceId,\n      targetWorkspaceId: null,\n      sidebarDataset: null,\n      sidebarInitialData: null,\n      documents: [],\n    };\n  }\n\n  // 说明：SSR 首屏阶段必须优先保证“可进入页面”。\n  // mnote-web compat sidebar 仍处于渐进接入期，若这里优先走远端/Rust 兼容接口，\n  // 一旦 actor/header 权限链不一致，就会把整个 (app) layout 阻塞住，表现为“登录后进不去页面”。\n  // 因此服务端首屏一律先走稳定的 Convex 直查；mnote-web 仅允许用于客户端树壳渐进增强。\n  const sidebarDataset = (await input.client.query(sidebarDatasetListQuery, {\n    workspaceId: targetWorkspaceId,\n  })) as SidebarDatasetListQueryResult;\n  const syntheticRequest = new Request(`http://127.0.0.1:3000/api/sidebar?workspaceId=${targetWorkspaceId}`);\n  const sidebarDatasetWithFileTree = attachKernelFileTreeProjection({\n    dataset: sidebarDataset,\n    projection: await resolveKernelFileTreeProjection({\n      client: input.client,\n      request: syntheticRequest,\n      workspaceId: targetWorkspaceId,\n      actor: {\n        actorType: \"user\",\n        actorId: input.auth.userId,\n        sessionId: null,\n      },\n      dataset: sidebarDataset,\n    }),\n  });\n  const normalizedDocuments = (sidebarDatasetWithFileTree.documents ?? []) as DocumentRecord[];\n\n  return {\n    workspaces: sidebarDatasetWithFileTree.workspaces ?? workspaces,\n    activeWorkspaceId,\n    targetWorkspaceId,\n    sidebarDataset: sidebarDatasetWithFileTree,\n    sidebarInitialData: mapSidebarDatasetListQueryResultToInitialData(sidebarDatasetWithFileTree),\n    documents: normalizedDocuments,\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/record-recent.ts","s":1,"e":15,"m":1773495901125.8638,"t":"export async function recordRecentPage(workspaceId: string | null, documentId: string) {\n  if (!workspaceId) {\n    return;\n  }\n  try {\n    await fetch(\"/api/search/recent\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ workspaceId, documentId }),\n    });\n  } catch (error) {\n    console.error(\"recordRecentPage failed\", error);\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/request.test.ts","s":1,"e":60,"m":1777380064855.8035,"t":"import { describe, expect, it } from \"vitest\";\nimport { SEARCH_REQUEST_TRANSPORT_CONTRACT, buildSearchRequest } from \"./request\";\n\ndescribe(\"buildSearchRequest\", () => {\n  it(\"combines标题过滤与时间范围\", () => {\n    const payload = buildSearchRequest({\n      workspaceId: \"ws-1\",\n      activeDocumentId: \"doc-1\",\n      query: \"demo\",\n      filters: {\n        titleOnly: true,\n        exact: false,\n        onlyCurrentPage: false,\n        includeOcr: false,\n        timeField: \"updated\",\n        customRange: undefined,\n      },\n      timeRange: \"7d\",\n    });\n\n    expect(payload).not.toBeNull();\n    expect(payload?.filters.titleOnly).toBe(true);\n    expect(payload?.filters.timeRange).toBe(\"7d\");\n  });\n\n  it(\"自动重置仅当前页面过滤条件\", () => {\n    const payload = buildSearchRequest({\n      workspaceId: \"ws-2\",\n      activeDocumentId: null,\n      query: \"\",\n      filters: {\n        titleOnly: false,\n        exact: false,\n        onlyCurrentPage: true,\n        includeOcr: false,\n        timeField: \"created\",\n        customRange: { from: \"2025-02-01\", to: \"2025-02-07\" },\n      },\n      timeRange: \"any\",\n    });\n\n    expect(payload?.filters.onlyCurrentPage).toBe(false);\n    expect(payload?.documentId).toBeUndefined();\n  });\n\n  it(\"固定搜索请求将由 Rust Web search.documents transport 拥有\", () => {\n    const payload = buildSearchRequest({\n      workspaceId: \"ws-3\",\n      activeDocumentId: \"doc-3\",\n      query: \"Rust Web\",\n      filters: {\n        titleOnly: false,\n        exact: true,\n        onlyCurrentPage: true,\n        includeOcr: true,\n        timeField: \"updated\",\n        customRange: undefined,\n      },\n      timeRange: \"any\",\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/request.test.ts","s":49,"e":86,"m":1777380064855.8035,"t":"activeDocumentId: \"doc-3\",\n      query: \"Rust Web\",\n      filters: {\n        titleOnly: false,\n        exact: true,\n        onlyCurrentPage: true,\n        includeOcr: true,\n        timeField: \"updated\",\n        customRange: undefined,\n      },\n      timeRange: \"any\",\n    });\n\n    expect(payload).toMatchObject({\n      workspaceId: \"ws-3\",\n      documentId: \"doc-3\",\n      query: \"Rust Web\",\n      filters: {\n        exact: true,\n        includeOcr: true,\n        onlyCurrentPage: true,\n      },\n    });\n  });\n});\n\n\ndescribe(\"SEARCH_REQUEST_TRANSPORT_CONTRACT\", () => {\n  it(\"搜索请求 contract 固定为 Rust Web transport\", () => {\n    expect(SEARCH_REQUEST_TRANSPORT_CONTRACT).toEqual({\n      shellOwner: \"mnote-web\",\n      queryName: \"search.documents\",\n      endpoint: \"/api/search/documents\",\n      runtimeRole: \"rust_web_transport\",\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/request.ts","s":1,"e":46,"m":1777380064855.6667,"t":"import type {\n  DocumentSearchFilters,\n  DocumentSearchRequest,\n  DocumentSearchTimeRange,\n} from \"@/types/search\";\n\nexport const SEARCH_REQUEST_TRANSPORT_CONTRACT = {\n  shellOwner: \"mnote-web\",\n  queryName: \"search.documents\",\n  endpoint: \"/api/search/documents\",\n  runtimeRole: \"rust_web_transport\",\n} as const;\n\ninterface BuildSearchRequestArgs {\n  workspaceId: string | null;\n  activeDocumentId: string | null;\n  query: string;\n  filters: Omit<DocumentSearchFilters, \"timeRange\">;\n  timeRange: DocumentSearchTimeRange;\n}\n\nexport function buildSearchRequest({\n  workspaceId,\n  activeDocumentId,\n  query,\n  filters,\n  timeRange,\n}: BuildSearchRequestArgs): DocumentSearchRequest | null {\n  if (!workspaceId) {\n    return null;\n  }\n\n  const normalizedFilters: DocumentSearchFilters = {\n    ...filters,\n    timeRange,\n    onlyCurrentPage: filters.onlyCurrentPage && Boolean(activeDocumentId),\n  };\n\n  return {\n    workspaceId,\n    documentId: normalizedFilters.onlyCurrentPage ? activeDocumentId ?? undefined : undefined,\n    query,\n    filters: normalizedFilters,\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":1,"e":60,"m":1776341732431.2847,"t":"import \"server-only\";\n\nimport { api } from \"@/lib/convex/api\";\nimport { getAuthedConvexClient } from \"@/lib/convex/route\";\nimport {\n  buildDocumentBridgeContext,\n  buildDocumentQueryEnvelope,\n  DocumentBridgeError,\n} from \"@/lib/documents/bridge\";\nimport { executeRustBridgeQuery } from \"@/lib/documents/rust-runtime\";\nimport type { DocumentSearchFilters, DocumentSearchRequest, DocumentSearchResponse, DocumentSearchResult } from \"@/types/search\";\n\nconst MAX_LIMIT = 50;\n\nconst DEFAULT_FILTERS: DocumentSearchFilters = {\n  titleOnly: false,\n  exact: false,\n  onlyCurrentPage: false,\n  includeOcr: false,\n  timeRange: \"any\",\n  timeField: \"updated\",\n};\n\nfunction parseDateToIso(value: string | undefined): string | null {\n  const raw = String(value ?? \"\").trim();\n  if (!raw) {\n    return null;\n  }\n  const date = new Date(raw);\n  if (Number.isNaN(date.getTime())) {\n    return null;\n  }\n  return date.toISOString();\n}\n\ntype SearchDocumentsRustResult = {\n  results: Array<{\n    id: string;\n    title: string;\n    snippet: string;\n    updatedAt?: string | null;\n    createdAt?: string | null;\n    matchField: \"title\" | \"content\" | \"recent\";\n    hasOcr: boolean;\n    publicPath: string;\n    score: number;\n    nodeId?: string | null;\n    subtreeRootId?: string | null;\n    evidence?: Array<{\n      kind: string;\n      nodeId?: string | null;\n      snippet: string;\n    }>;\n  }>;\n  enqueueAssetIds?: string[];\n};\n\ntype SearchRecentRustResult = {\n  items: Array<{\n    documentId: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":49,"e":108,"m":1776341732431.2847,"t":"evidence?: Array<{\n      kind: string;\n      nodeId?: string | null;\n      snippet: string;\n    }>;\n  }>;\n  enqueueAssetIds?: string[];\n};\n\ntype SearchRecentRustResult = {\n  items: Array<{\n    documentId: string;\n  }>;\n};\n\ntype SearchDocumentRow = {\n  id: string;\n  workspace_id?: string | null;\n  title?: string | null;\n  raw_text?: string | null;\n  created_at?: string | null;\n  updated_at?: string | null;\n};\n\ntype SearchMindmapRow = {\n  document_id?: string | null;\n  data?: unknown;\n};\n\ntype SearchTableRow = {\n  id: string;\n  document_id?: string | null;\n  title?: string | null;\n};\n\ntype SearchTableContentRow = {\n  table_id?: string | null;\n  document_id?: string | null;\n  row_hash?: string | null;\n};\n\ntype SearchAssetRow = {\n  id: string;\n  document_id?: string | null;\n  asset_type?: string | null;\n  file_name?: string | null;\n  mime_type?: string | null;\n  ocr_text?: string | null;\n  ocr_status?: string | null;\n};\n\ntype RecentRow = {\n  document_id: string;\n  workspace_id: string;\n  last_accessed_at: string;\n};\n\nfunction normalizeWorkspaceId(workspaceId: string | null | undefined) {\n  const normalized = typeof workspaceId === \"string\" ? workspaceId.trim() : \"\";\n  if (!normalized) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":97,"e":156,"m":1776341732431.2847,"t":"ocr_status?: string | null;\n};\n\ntype RecentRow = {\n  document_id: string;\n  workspace_id: string;\n  last_accessed_at: string;\n};\n\nfunction normalizeWorkspaceId(workspaceId: string | null | undefined) {\n  const normalized = typeof workspaceId === \"string\" ? workspaceId.trim() : \"\";\n  if (!normalized) {\n    throw new DocumentBridgeError(\"缺少 workspaceId\", 400, \"VALIDATION_ERROR\");\n  }\n  return normalized;\n}\n\nfunction normalizeQuery(value: string | null | undefined) {\n  return String(value ?? \"\").trim();\n}\n\nfunction normalizeLimit(value: number | null | undefined) {\n  if (typeof value !== \"number\" || !Number.isFinite(value)) {\n    return 30;\n  }\n  return Math.max(1, Math.min(Math.floor(value), MAX_LIMIT));\n}\n\nfunction normalizeDocumentId(value: string | null | undefined) {\n  const normalized = typeof value === \"string\" ? value.trim() : \"\";\n  return normalized || null;\n}\n\nfunction normalizeFilters(input: DocumentSearchRequest[\"filters\"] | undefined): DocumentSearchFilters {\n  return {\n    ...DEFAULT_FILTERS,\n    ...input,\n  };\n}\n\nfunction mapRecentRowsToResults(input: {\n  documentIds: string[];\n  docs: SearchDocumentRow[];\n}): DocumentSearchResult[] {\n  const docMap = new Map(input.docs.map((item) => [item.id, item]));\n  return input.documentIds\n    .map((documentId) => docMap.get(documentId))\n    .filter((item): item is NonNullable<typeof item> => Boolean(item))\n    .map((item) => ({\n      id: item.id,\n      title: item.title ?? \"无标题\",\n      snippet: \"\",\n      updatedAt: item.updated_at ?? null,\n      createdAt: item.created_at ?? null,\n      matchField: \"recent\",\n      hasOcr: false,\n      publicPath: `/documents/${item.id}`,\n      score: 0,\n      nodeId: item.id,\n      subtreeRootId: item.id,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":145,"e":204,"m":1776341732431.2847,"t":".map((item) => ({\n      id: item.id,\n      title: item.title ?? \"无标题\",\n      snippet: \"\",\n      updatedAt: item.updated_at ?? null,\n      createdAt: item.created_at ?? null,\n      matchField: \"recent\",\n      hasOcr: false,\n      publicPath: `/documents/${item.id}`,\n      score: 0,\n      nodeId: item.id,\n      subtreeRootId: item.id,\n      evidence: [\n        {\n          kind: \"recent\",\n          nodeId: item.id,\n          snippet: item.title ?? \"最近访问页面\",\n        },\n      ],\n    }));\n}\n\nfunction mapRustResultsToResponse(\n  result: SearchDocumentsRustResult,\n): DocumentSearchResult[] {\n  return (result.results ?? []).map((item) => ({\n    id: item.id,\n    title: item.title ?? \"无标题\",\n    snippet: item.snippet,\n    updatedAt: item.updatedAt ?? null,\n    createdAt: item.createdAt ?? null,\n    matchField: item.matchField,\n    hasOcr: Boolean(item.hasOcr),\n    publicPath: item.publicPath,\n    score: item.score,\n    nodeId: item.nodeId ?? item.id,\n    subtreeRootId: item.subtreeRootId ?? item.id,\n    evidence:\n      Array.isArray(item.evidence) && item.evidence.length > 0\n        ? item.evidence\n        : [\n            {\n              kind: item.matchField,\n              nodeId: item.nodeId ?? item.id,\n              snippet: item.snippet || item.title || \"命中文档\",\n            },\n          ],\n  }));\n}\n\nasync function enqueueOcrJobs(assetIds: string[]) {\n  if (assetIds.length === 0) {\n    return;\n  }\n  const { auth, client } = await getAuthedConvexClient();\n  await Promise.all(\n    assetIds.slice(0, 3).map((assetId) =>\n      client.mutation(api.mediaAssets.enqueueExtractText, {\n        userId: auth.userId,\n        id: assetId,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":193,"e":252,"m":1776341732431.2847,"t":"}\n\nasync function enqueueOcrJobs(assetIds: string[]) {\n  if (assetIds.length === 0) {\n    return;\n  }\n  const { auth, client } = await getAuthedConvexClient();\n  await Promise.all(\n    assetIds.slice(0, 3).map((assetId) =>\n      client.mutation(api.mediaAssets.enqueueExtractText, {\n        userId: auth.userId,\n        id: assetId,\n      }).catch(() => null),\n    ),\n  );\n}\n\nasync function loadRecentResults(input: {\n  request: Request;\n  workspaceId: string;\n  docs: SearchDocumentRow[];\n}): Promise<DocumentSearchResult[]> {\n  const { client } = await getAuthedConvexClient();\n  const context = await buildDocumentBridgeContext({\n    request: input.request,\n    workspaceId: input.workspaceId,\n  });\n  const envelope = buildDocumentQueryEnvelope({\n    name: \"search.recent\",\n    payload: {\n      workspaceId: input.workspaceId,\n      limit: 10,\n      cursor: null,\n    },\n  });\n  const recentRows = (await client.query(api.recents.listByWorkspace, {\n    userId: context.actor.actorId,\n    workspaceId: input.workspaceId,\n    limit: 10,\n  })) as RecentRow[];\n  const result = await executeRustBridgeQuery<SearchRecentRustResult>({\n    context,\n    envelope,\n    data: {\n      recents: recentRows.map((row) => ({\n        documentId: row.document_id,\n        workspaceId: row.workspace_id,\n        lastAccessedAt: row.last_accessed_at,\n      })),\n    },\n  });\n\n  return mapRecentRowsToResults({\n    documentIds: (result.items ?? []).map((item) => item.documentId),\n    docs: input.docs,\n  });\n}\n\nexport async function executeDocumentSearchQuery(input: {\n  request: Request;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":241,"e":300,"m":1776341732431.2847,"t":"})),\n    },\n  });\n\n  return mapRecentRowsToResults({\n    documentIds: (result.items ?? []).map((item) => item.documentId),\n    docs: input.docs,\n  });\n}\n\nexport async function executeDocumentSearchQuery(input: {\n  request: Request;\n  payload: DocumentSearchRequest;\n}): Promise<{\n  response: DocumentSearchResponse;\n  meta: {\n    requestId: string;\n    traceId: string;\n    queryName: string;\n  };\n}> {\n  const workspaceId = normalizeWorkspaceId(input.payload.workspaceId);\n  const normalizedQuery = normalizeQuery(input.payload.query);\n  const filters = normalizeFilters(input.payload.filters);\n  const limit = normalizeLimit(input.payload.limit);\n  const documentId = filters.onlyCurrentPage ? normalizeDocumentId(input.payload.documentId) : null;\n  const { client } = await getAuthedConvexClient();\n  const context = await buildDocumentBridgeContext({\n    request: input.request,\n    workspaceId,\n  });\n  const customRangeFrom = parseDateToIso(filters.customRange?.from);\n  const customRangeTo = parseDateToIso(filters.customRange?.to);\n\n  const docs = (await client.query(api.documents.listSearchDataByWorkspace, {\n    workspaceId,\n  })) as SearchDocumentRow[];\n  const recent = await loadRecentResults({\n    request: input.request,\n    workspaceId,\n    docs,\n  });\n\n  if (!normalizedQuery) {\n    return {\n      response: {\n        results: [],\n        recent,\n      },\n      meta: {\n        requestId: context.requestId,\n        traceId: context.traceId,\n        queryName: \"search.documents\",\n      },\n    };\n  }\n\n  const [mindmaps, tables, tableRows, assets] = (await Promise.all([\n    client.query(api.mindmaps.listByWorkspace, { workspaceId, includeDeleted: false }).catch(() => []),\n    client.query(api.tables.listByWorkspaceForSearch, {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":289,"e":348,"m":1776341732431.2847,"t":"},\n      meta: {\n        requestId: context.requestId,\n        traceId: context.traceId,\n        queryName: \"search.documents\",\n      },\n    };\n  }\n\n  const [mindmaps, tables, tableRows, assets] = (await Promise.all([\n    client.query(api.mindmaps.listByWorkspace, { workspaceId, includeDeleted: false }).catch(() => []),\n    client.query(api.tables.listByWorkspaceForSearch, {\n      userId: context.actor.actorId,\n      workspaceId,\n      includeArchived: false,\n      limit: 3000,\n    }).catch(() => []),\n    client.query(api.tables.listRowsByWorkspaceForSearch, {\n      userId: context.actor.actorId,\n      workspaceId,\n      limit: 8000,\n    }).catch(() => []),\n    client.query(api.mediaAssets.listSearchDataByWorkspace, {\n      userId: context.actor.actorId,\n      workspaceId,\n      includeDeleted: false,\n      limit: 5000,\n    }).catch(() => []),\n  ])) as [SearchMindmapRow[], SearchTableRow[], SearchTableContentRow[], SearchAssetRow[]];\n\n  const envelope = buildDocumentQueryEnvelope({\n    name: \"search.documents\",\n    payload: {\n      query: normalizedQuery,\n      workspaceId,\n      pageId: documentId,\n      limit,\n      cursor: null,\n      titleOnly: filters.titleOnly,\n      exact: filters.exact,\n      includeOcr: filters.includeOcr,\n      timeRange: filters.timeRange,\n      timeField: filters.timeField,\n      customRangeFrom,\n      customRangeTo,\n    },\n  });\n\n  const result = await executeRustBridgeQuery<SearchDocumentsRustResult>({\n    context,\n    envelope,\n    data: {\n      documents: docs.map((item) => ({\n        id: item.id,\n        workspaceId: item.workspace_id ?? workspaceId,\n        title: item.title ?? null,\n        rawText: item.raw_text ?? null,\n        createdAt: item.created_at ?? null,\n        updatedAt: item.updated_at ?? null,\n      })),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/search-query-adapter.ts","s":337,"e":389,"m":1776341732431.2847,"t":"const result = await executeRustBridgeQuery<SearchDocumentsRustResult>({\n    context,\n    envelope,\n    data: {\n      documents: docs.map((item) => ({\n        id: item.id,\n        workspaceId: item.workspace_id ?? workspaceId,\n        title: item.title ?? null,\n        rawText: item.raw_text ?? null,\n        createdAt: item.created_at ?? null,\n        updatedAt: item.updated_at ?? null,\n      })),\n      mindmaps: mindmaps.map((item) => ({\n        documentId: item.document_id ?? \"\",\n        data: item.data ?? null,\n      })),\n      tables: tables.map((item) => ({\n        id: item.id,\n        documentId: item.document_id ?? \"\",\n        title: item.title ?? null,\n      })),\n      tableRows: tableRows.map((item) => ({\n        tableId: item.table_id ?? \"\",\n        documentId: item.document_id ?? \"\",\n        rowHash: item.row_hash ?? null,\n      })),\n      assets: assets.map((item) => ({\n        id: item.id,\n        documentId: item.document_id ?? \"\",\n        assetType: item.asset_type ?? null,\n        fileName: item.file_name ?? null,\n        mimeType: item.mime_type ?? null,\n        ocrText: item.ocr_text ?? null,\n        ocrStatus: item.ocr_status ?? null,\n      })),\n    },\n  });\n\n  await enqueueOcrJobs(result.enqueueAssetIds ?? []);\n\n  return {\n    response: {\n      results: mapRustResultsToResponse(result),\n      recent,\n    },\n    meta: {\n      requestId: context.requestId,\n      traceId: context.traceId,\n      queryName: envelope.name,\n    },\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/shortcuts.test.ts","s":1,"e":18,"m":1773495901152.4185,"t":"import { describe, expect, it } from \"vitest\";\nimport { resolveSearchOpenMode } from \"./shortcuts\";\n\ndescribe(\"resolveSearchOpenMode\", () => {\n  it(\"Ctrl/Cmd+Enter 打开新窗口\", () => {\n    expect(resolveSearchOpenMode({ ctrlKey: true })).toBe(\"new-window\");\n    expect(resolveSearchOpenMode({ metaKey: true, altKey: true })).toBe(\"new-window\");\n  });\n\n  it(\"Alt+Enter 打开右侧预览\", () => {\n    expect(resolveSearchOpenMode({ altKey: true })).toBe(\"sidebar\");\n  });\n\n  it(\"默认回车在当前窗口打开\", () => {\n    expect(resolveSearchOpenMode({})).toBe(\"main\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/shortcuts.ts","s":1,"e":18,"m":1773495901165.8643,"t":"export type SearchOpenMode = \"main\" | \"new-window\" | \"sidebar\";\n\ninterface ShortcutLikeEvent {\n  metaKey?: boolean;\n  ctrlKey?: boolean;\n  altKey?: boolean;\n}\n\nexport const resolveSearchOpenMode = (event: ShortcutLikeEvent): SearchOpenMode => {\n  if (event.metaKey || event.ctrlKey) {\n    return \"new-window\";\n  }\n  if (event.altKey) {\n    return \"sidebar\";\n  }\n  return \"main\";\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/snippet.test.ts","s":1,"e":15,"m":1773495901171.8643,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildSnippet } from \"./snippet\";\n\ndescribe(\"buildSnippet\", () => {\n  it(\"命中关键字时高亮 OCR 片段\", () => {\n    const snippet = buildSnippet(\"图像 OCR 测试文本，用于高亮\", \"OCR\");\n    expect(snippet).toContain(\"<mark>OCR</mark>\");\n  });\n\n  it(\"无关键字时返回截断文本\", () => {\n    const snippet = buildSnippet(\"一段很长的文字用于测试截断逻辑\", null);\n    expect(snippet.endsWith(\"…\") || snippet.length <= 120).toBe(true);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/search/snippet.ts","s":1,"e":25,"m":1773495901178.336,"t":"const escapeHtml = (value: string): string =>\n  value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n\nconst escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nexport const buildSnippet = (text: string | null, keyword: string | null): string => {\n  const source = (text ?? \"\").trim();\n  if (!source) {\n    return \"暂无正文内容\";\n  }\n  const safeSource = escapeHtml(source);\n  if (!keyword) {\n    return `${safeSource.slice(0, 120)}${safeSource.length > 120 ? \"…\" : \"\"}`;\n  }\n  const pattern = new RegExp(escapeRegExp(keyword), \"gi\");\n  const match = pattern.exec(safeSource);\n  if (!match) {\n    return `${safeSource.slice(0, 120)}${safeSource.length > 120 ? \"…\" : \"\"}`;\n  }\n  const start = Math.max(0, match.index - 20);\n  const end = Math.min(safeSource.length, match.index + keyword.length + 80);\n  const segment = safeSource.slice(start, end);\n  return segment.replace(pattern, (found) => `<mark>${found}</mark>`);\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.test.ts","s":1,"e":60,"m":1778599634625.124,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  buildOnlyOfficeAssetOpenUrl,\n  buildOnlyOfficeCallbackUrl,\n  buildOnlyOfficeForcesaveUrl,\n  buildOnlyOfficeOpenFileId,\n  docTypeFromExt,\n  inferOnlyOfficeFileType,\n  resolveOnlyOfficeDocumentUrl,\n} from \"@/lib/onlyoffice/client-session\";\n\ndescribe(\"onlyoffice client session helpers\", () => {\n  it(\"docTypeFromExt maps office extensions\", () => {\n    expect(docTypeFromExt(\"docx\")).toBe(\"word\");\n    expect(docTypeFromExt(\"xlsx\")).toBe(\"cell\");\n    expect(docTypeFromExt(\"pptx\")).toBe(\"slide\");\n    expect(docTypeFromExt(\"pdf\")).toBe(\"word\");\n  });\n\n  it(\"inferOnlyOfficeFileType detects only true Office assets from file name and MIME\", () => {\n    expect(inferOnlyOfficeFileType(\"demo.pptx\", null)).toBe(\"pptx\");\n    expect(inferOnlyOfficeFileType(\"demo\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\")).toBe(\"docx\");\n    expect(inferOnlyOfficeFileType(\"demo\", \"application/pdf\")).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.png\", \"image/png\")).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.pdf\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.toml\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.json\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.yaml\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.yml\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.md\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.txt\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.ts\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.js\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.py\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.rs\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.go\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.html\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.css\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.vue\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.svelte\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.proto\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.graphql\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.gradle\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.tf\", null)).toBeNull();\n    expect(inferOnlyOfficeFileType(\"demo.pdf\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\")).toBeNull();\n    expect(inferOnlyOfficeFileType(\"Dockerfile\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\")).toBeNull();\n    expect(inferOnlyOfficeFileType(\".gitignore\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\")).toBeNull();\n  });\n\n  it(\"buildOnlyOfficeAssetOpenUrl keeps attachment identity for callback writeback\", () => {\n    const result = buildOnlyOfficeAssetOpenUrl({\n      origin: \"https://mnote.example.com\",\n      fileUrl: \"https://storage.example.com/demo.pptx?token=abc\",\n      fileName: \"2023自我介绍PPT_李爱波0831.pptx\",\n      fileType: \"pptx\",\n      assetId: \"asset_ppt_1\",\n      documentId: \"doc_1\",\n      mode: \"edit\",\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.test.ts","s":49,"e":108,"m":1778599634625.124,"t":"it(\"buildOnlyOfficeAssetOpenUrl keeps attachment identity for callback writeback\", () => {\n    const result = buildOnlyOfficeAssetOpenUrl({\n      origin: \"https://mnote.example.com\",\n      fileUrl: \"https://storage.example.com/demo.pptx?token=abc\",\n      fileName: \"2023自我介绍PPT_李爱波0831.pptx\",\n      fileType: \"pptx\",\n      assetId: \"asset_ppt_1\",\n      documentId: \"doc_1\",\n      mode: \"edit\",\n    });\n\n    expect(result).toBe(\n      \"https://mnote.example.com/onlyoffice?fileUrl=https%3A%2F%2Fstorage.example.com%2Fdemo.pptx%3Ftoken%3Dabc&fileName=2023%E8%87%AA%E6%88%91%E4%BB%8B%E7%BB%8DPPT_%E6%9D%8E%E7%88%B1%E6%B3%A20831.pptx&fileType=pptx&assetId=asset_ppt_1&documentId=doc_1&mode=edit\",\n    );\n  });\n\n  it(\"resolveOnlyOfficeDocumentUrl wraps signed url with proxy\", () => {\n    const result = resolveOnlyOfficeDocumentUrl({\n      effectiveFileUrl: \"https://public.example.com/storage/v1/object/sign/documents/a.docx?token=abc\",\n      proxyOrigin: \"https://app.example.com\",\n      storageHostOverride: \"\",\n      runtimeSupabaseUrl: \"https://public.example.com\",\n      useConvex: false,\n    });\n    expect(result).toContain(\"https://app.example.com/api/onlyoffice/proxy?u=\");\n  });\n\n  it(\"buildOnlyOfficeCallbackUrl keeps asset and user\", () => {\n    const result = buildOnlyOfficeCallbackUrl({\n      callbackOrigin: \"http://host.docker.internal:3000\",\n      proxyOrigin: \"\",\n      windowOrigin: \"https://app.example.com\",\n      assetId: \"asset_1\",\n      userId: \"user_1\",\n    });\n    expect(result).toBe(\n      \"http://host.docker.internal:3000/api/onlyoffice/callback?assetId=asset_1&userId=user_1\",\n    );\n  });\n\n  it(\"buildOnlyOfficeForcesaveUrl keeps key\", () => {\n    const result = buildOnlyOfficeForcesaveUrl({\n      windowOrigin: \"https://app.example.com\",\n      assetId: \"asset_1\",\n      key: \"doc_key_1\",\n    });\n    expect(result).toBe(\n      \"https://app.example.com/api/onlyoffice/forcesave?assetId=asset_1&key=doc_key_1\",\n    );\n  });\n\n  it(\"buildOnlyOfficeOpenFileId prefers asset id\", () => {\n    expect(\n      buildOnlyOfficeOpenFileId({\n        assetId: \"asset_1\",\n        docKey: \"doc_key_1\",\n        resolvedFileUrl: \"https://app.example.com/file.docx\",\n        fileName: \"file.docx\",\n      }),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.test.ts","s":97,"e":112,"m":1778599634625.124,"t":"\"https://app.example.com/api/onlyoffice/forcesave?assetId=asset_1&key=doc_key_1\",\n    );\n  });\n\n  it(\"buildOnlyOfficeOpenFileId prefers asset id\", () => {\n    expect(\n      buildOnlyOfficeOpenFileId({\n        assetId: \"asset_1\",\n        docKey: \"doc_key_1\",\n        resolvedFileUrl: \"https://app.example.com/file.docx\",\n        fileName: \"file.docx\",\n      }),\n    ).toBe(\"asset_1\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.ts","s":1,"e":60,"m":1778599616352.8916,"t":"import { rewriteToPublicOrigin } from \"@/lib/url/rewritePublicOrigin\";\n\nexport const hashOnlyOfficeKey = (input: string) => {\n  let hash = 0;\n  for (let i = 0; i < input.length; i += 1) {\n    hash = (hash << 5) - hash + input.charCodeAt(i);\n    hash |= 0;\n  }\n  return Math.abs(hash).toString();\n};\n\nexport const base64UrlEncodeUtf8 = (input: string) => {\n  const bytes = new TextEncoder().encode(input);\n  let binary = \"\";\n  for (let i = 0; i < bytes.length; i += 1) {\n    binary += String.fromCharCode(bytes[i] as number);\n  }\n  return btoa(binary).replace(/=/g, \"\").replace(/\\+/g, \"-\").replace(/\\//g, \"_\");\n};\n\nexport const docTypeFromExt = (ext: string) => {\n  const word = [\"doc\", \"docx\", \"odt\", \"rtf\"];\n  const slide = [\"ppt\", \"pptx\", \"odp\"];\n  const sheet = [\"xls\", \"xlsx\", \"ods\", \"csv\"];\n  if (word.includes(ext)) return \"word\";\n  if (slide.includes(ext)) return \"slide\";\n  if (sheet.includes(ext)) return \"cell\";\n  return \"word\";\n};\n\nexport const inferOnlyOfficeFileType = (fileName: string | null | undefined, mimeType: string | null | undefined) => {\n  const name = (fileName ?? \"\").trim().toLowerCase();\n  const mt = (mimeType ?? \"\").trim().toLowerCase();\n  const ext = name.includes(\".\") ? name.split(\".\").pop() ?? \"\" : \"\";\n  const officeExts = [\"doc\", \"docx\", \"odt\", \"rtf\", \"ppt\", \"pptx\", \"odp\", \"xls\", \"xlsx\", \"ods\", \"csv\"];\n  const nonOfficeFileNames = [\n    \".dockerignore\",\n    \".editorconfig\",\n    \".env\",\n    \".eslintrc\",\n    \".gitattributes\",\n    \".gitignore\",\n    \".npmrc\",\n    \".prettierrc\",\n    \"cmakelists.txt\",\n    \"dockerfile\",\n    \"gemfile\",\n    \"makefile\",\n    \"procfile\",\n    \"rakefile\",\n  ];\n  const nonOfficeExts = [\n    \"pdf\",\n    \"toml\",\n    \"json\",\n    \"jsonc\",\n    \"json5\",\n    \"yaml\",\n    \"yml\",\n    \"md\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.ts","s":49,"e":108,"m":1778599616352.8916,"t":"\"procfile\",\n    \"rakefile\",\n  ];\n  const nonOfficeExts = [\n    \"pdf\",\n    \"toml\",\n    \"json\",\n    \"jsonc\",\n    \"json5\",\n    \"yaml\",\n    \"yml\",\n    \"md\",\n    \"markdown\",\n    \"txt\",\n    \"ini\",\n    \"env\",\n    \"xml\",\n    \"ts\",\n    \"tsx\",\n    \"js\",\n    \"jsx\",\n    \"mjs\",\n    \"cjs\",\n    \"py\",\n    \"rs\",\n    \"go\",\n    \"html\",\n    \"htm\",\n    \"css\",\n    \"scss\",\n    \"less\",\n    \"vue\",\n    \"svelte\",\n    \"astro\",\n    \"java\",\n    \"c\",\n    \"cpp\",\n    \"h\",\n    \"hpp\",\n    \"cs\",\n    \"php\",\n    \"rb\",\n    \"sh\",\n    \"bash\",\n    \"zsh\",\n    \"sql\",\n    \"lock\",\n    \"log\",\n    \"proto\",\n    \"graphql\",\n    \"gql\",\n    \"prisma\",\n    \"tf\",\n    \"tfvars\",\n    \"hcl\",\n    \"nix\",\n    \"gradle\",\n  ];\n  if (officeExts.includes(ext)) return ext;\n  if (nonOfficeFileNames.includes(name)) return null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.ts","s":97,"e":156,"m":1778599616352.8916,"t":"\"proto\",\n    \"graphql\",\n    \"gql\",\n    \"prisma\",\n    \"tf\",\n    \"tfvars\",\n    \"hcl\",\n    \"nix\",\n    \"gradle\",\n  ];\n  if (officeExts.includes(ext)) return ext;\n  if (nonOfficeFileNames.includes(name)) return null;\n  if (nonOfficeExts.includes(ext)) return null;\n  if (mt.includes(\"wordprocessingml\")) return \"docx\";\n  if (mt.includes(\"presentationml\")) return \"pptx\";\n  if (mt.includes(\"spreadsheetml\")) return \"xlsx\";\n  return null;\n};\n\nexport function buildOnlyOfficeAssetOpenUrl(input: {\n  origin: string;\n  fileUrl: string;\n  fileName: string;\n  fileType: string;\n  assetId?: string | null;\n  documentId?: string | null;\n  mode?: \"view\" | \"edit\";\n}): string {\n  const target = new URL(\"/onlyoffice\", input.origin);\n  target.searchParams.set(\"fileUrl\", input.fileUrl);\n  target.searchParams.set(\"fileName\", input.fileName);\n  target.searchParams.set(\"fileType\", input.fileType);\n  if (input.assetId) target.searchParams.set(\"assetId\", input.assetId);\n  if (input.documentId) target.searchParams.set(\"documentId\", input.documentId);\n  target.searchParams.set(\"mode\", input.mode ?? \"edit\");\n  return target.toString();\n}\n\nexport function resolveOnlyOfficeDocumentUrl(input: {\n  effectiveFileUrl: string;\n  proxyOrigin: string;\n  storageHostOverride: string;\n  runtimeSupabaseUrl: string;\n  useConvex: boolean;\n}): string {\n  const { effectiveFileUrl, proxyOrigin, storageHostOverride, runtimeSupabaseUrl, useConvex } = input;\n  if (!effectiveFileUrl) return \"\";\n\n  const isConvexStorageUrl = (() => {\n    try {\n      const u = new URL(effectiveFileUrl);\n      return u.pathname.startsWith(\"/api/storage/\");\n    } catch {\n      return false;\n    }\n  })();\n\n  let base =\n    storageHostOverride || useConvex || isConvexStorageUrl\n      ? effectiveFileUrl","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.ts","s":145,"e":204,"m":1778599616352.8916,"t":"const isConvexStorageUrl = (() => {\n    try {\n      const u = new URL(effectiveFileUrl);\n      return u.pathname.startsWith(\"/api/storage/\");\n    } catch {\n      return false;\n    }\n  })();\n\n  let base =\n    storageHostOverride || useConvex || isConvexStorageUrl\n      ? effectiveFileUrl\n      : rewriteToPublicOrigin(effectiveFileUrl, runtimeSupabaseUrl);\n  try {\n    const raw = new URL(base);\n    let alreadyProxy = raw.pathname.includes(\"/api/onlyoffice/proxy\");\n    const isLocalHost =\n      raw.hostname === \"127.0.0.1\" || raw.hostname === \"localhost\" || raw.hostname === \"host.docker.internal\";\n\n    if (alreadyProxy && proxyOrigin) {\n      const po = new URL(proxyOrigin);\n      raw.protocol = po.protocol;\n      raw.host = po.host;\n      base = raw.toString();\n    }\n\n    if (!alreadyProxy && proxyOrigin && isLocalHost) {\n      const proxy = new URL(\"/api/onlyoffice/proxy\", proxyOrigin || window.location.origin);\n      proxy.searchParams.set(\"u\", base64UrlEncodeUtf8(base));\n      base = proxy.toString();\n      alreadyProxy = true;\n    }\n    if (!alreadyProxy && raw.searchParams.has(\"token\")) {\n      const proxy = new URL(\"/api/onlyoffice/proxy\", proxyOrigin || window.location.origin);\n      proxy.searchParams.set(\"u\", base64UrlEncodeUtf8(base));\n      base = proxy.toString();\n      alreadyProxy = true;\n    }\n\n    const u = new URL(base);\n    if (!storageHostOverride || alreadyProxy) return u.toString();\n\n    if (/^https?:\\/\\//i.test(storageHostOverride)) {\n      const ov = new URL(storageHostOverride);\n      u.protocol = ov.protocol;\n      u.host = ov.host;\n      return u.toString();\n    }\n\n    u.hostname = storageHostOverride;\n    return u.toString();\n  } catch {\n    return base;\n  }\n}\n\nexport function buildOnlyOfficeCallbackUrl(input: {\n  callbackOrigin: string;\n  proxyOrigin: string;\n  windowOrigin: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/client-session.ts","s":193,"e":234,"m":1778599616352.8916,"t":"u.hostname = storageHostOverride;\n    return u.toString();\n  } catch {\n    return base;\n  }\n}\n\nexport function buildOnlyOfficeCallbackUrl(input: {\n  callbackOrigin: string;\n  proxyOrigin: string;\n  windowOrigin: string;\n  assetId: string;\n  userId: string;\n}): string {\n  const base = input.callbackOrigin || input.proxyOrigin || input.windowOrigin;\n  const callback = new URL(\"/api/onlyoffice/callback\", base);\n  if (input.assetId) callback.searchParams.set(\"assetId\", input.assetId);\n  if (input.userId) callback.searchParams.set(\"userId\", input.userId);\n  return callback.toString();\n}\n\nexport function buildOnlyOfficeForcesaveUrl(input: {\n  windowOrigin: string;\n  assetId: string;\n  key: string;\n}): string {\n  const url = new URL(\"/api/onlyoffice/forcesave\", input.windowOrigin);\n  url.searchParams.set(\"assetId\", input.assetId);\n  url.searchParams.set(\"key\", input.key);\n  return url.toString();\n}\n\nexport function buildOnlyOfficeOpenFileId(input: {\n  assetId: string;\n  docKey: string;\n  resolvedFileUrl: string;\n  fileName: string;\n}): string {\n  return input.assetId || `onlyoffice_${input.docKey || hashOnlyOfficeKey(`${input.resolvedFileUrl}-${input.fileName}`)}`;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/internal-url.ts","s":1,"e":60,"m":1776426825588.8555,"t":"const DEFAULT_ONLYOFFICE_INTERNAL_URL = \"http://127.0.0.1:8082\";\nconst ONLYOFFICE_PROBE_PATH = \"/web-apps/apps/api/documents/api.js\";\nconst RESOLVE_CACHE_TTL_MS = 30_000;\n\nconst DEFAULT_ONLYOFFICE_INTERNAL_URL_CANDIDATES = [\n  DEFAULT_ONLYOFFICE_INTERNAL_URL,\n  \"http://127.0.0.1:8081\",\n  \"http://localhost:8082\",\n  \"http://localhost:8081\",\n];\n\nlet cachedOnlyOfficeInternalUrl = \"\";\nlet cachedOnlyOfficeInternalUrlAt = 0;\nlet pendingOnlyOfficeInternalUrl: Promise<string> | null = null;\n\nconst normalizeOnlyOfficeInternalUrl = (raw?: string | null) => {\n  const value = String(raw || \"\").trim().replace(/\\/+$/, \"\");\n  if (!value) return \"\";\n  try {\n    const url = new URL(value);\n    if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return \"\";\n    return url.toString().replace(/\\/+$/, \"\");\n  } catch {\n    return \"\";\n  }\n};\n\nconst getOnlyOfficeInternalUrlCandidates = () => {\n  const candidates: string[] = [];\n  const push = (value?: string | null) => {\n    const normalized = normalizeOnlyOfficeInternalUrl(value);\n    if (!normalized) return;\n    if (!candidates.includes(normalized)) candidates.push(normalized);\n  };\n\n  push(process.env.ONLYOFFICE_INTERNAL_URL);\n\n  for (const raw of String(process.env.ONLYOFFICE_INTERNAL_URL_CANDIDATES || \"\").split(\",\")) {\n    push(raw);\n  }\n\n  for (const value of DEFAULT_ONLYOFFICE_INTERNAL_URL_CANDIDATES) {\n    push(value);\n  }\n\n  return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL];\n};\n\nconst probeOnlyOfficeInternalUrl = async (candidate: string) => {\n  try {\n    const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`);\n    const response = await fetch(probeUrl, {\n      method: \"HEAD\",\n      redirect: \"follow\",\n      cache: \"no-store\",\n      signal: AbortSignal.timeout(2_500),\n    });\n    return response.ok;\n  } catch {\n    return false;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/internal-url.ts","s":49,"e":93,"m":1776426825588.8555,"t":"const probeOnlyOfficeInternalUrl = async (candidate: string) => {\n  try {\n    const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`);\n    const response = await fetch(probeUrl, {\n      method: \"HEAD\",\n      redirect: \"follow\",\n      cache: \"no-store\",\n      signal: AbortSignal.timeout(2_500),\n    });\n    return response.ok;\n  } catch {\n    return false;\n  }\n};\n\nexport const resolveOnlyOfficeInternalUrl = async () => {\n  const now = Date.now();\n  if (cachedOnlyOfficeInternalUrl && now - cachedOnlyOfficeInternalUrlAt < RESOLVE_CACHE_TTL_MS) {\n    return cachedOnlyOfficeInternalUrl;\n  }\n\n  if (pendingOnlyOfficeInternalUrl) {\n    return pendingOnlyOfficeInternalUrl;\n  }\n\n  pendingOnlyOfficeInternalUrl = (async () => {\n    const candidates = getOnlyOfficeInternalUrlCandidates();\n    for (const candidate of candidates) {\n      if (await probeOnlyOfficeInternalUrl(candidate)) {\n        return candidate;\n      }\n    }\n    return candidates[0] || DEFAULT_ONLYOFFICE_INTERNAL_URL;\n  })();\n\n  try {\n    const resolved = await pendingOnlyOfficeInternalUrl;\n    cachedOnlyOfficeInternalUrl = resolved;\n    cachedOnlyOfficeInternalUrlAt = Date.now();\n    return resolved;\n  } finally {\n    pendingOnlyOfficeInternalUrl = null;\n  }\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/rust-adapter.ts","s":1,"e":60,"m":1776239508437.8945,"t":"import type { MnoteRuntimeConfig } from \"@/lib/runtime-config\";\nimport {\n  buildDocumentBridgeContextWithActor,\n  type BridgeContext,\n  type BridgeTarget,\n} from \"@/lib/documents/bridge\";\nimport { executeRustBridgeTool } from \"@/lib/documents/rust-runtime\";\n\ntype OnlyOfficeHeader = {\n  name: string;\n  value: string;\n};\n\ntype OnlyOfficeServiceActorInput = {\n  actorId?: string;\n  actorType?: string;\n  sessionId?: string | null;\n  workspaceId?: string | null;\n  sourceChannel: string;\n  sourceClient: string;\n  idempotencyKey?: string | null;\n};\n\nexport type OnlyOfficeSignResult = {\n  token: string | null;\n  documentToken: string | null;\n  editorConfigToken: string | null;\n};\n\nexport type OnlyOfficeProxyPreparationResult = {\n  targetUrl: string;\n  forwardHeaders: OnlyOfficeHeader[];\n};\n\nexport type OnlyOfficeCallbackPreparationResult = {\n  shouldWrite: boolean;\n  downloadUrl: string | null;\n  idempotencyKey: string | null;\n  locator: {\n    assetId: string;\n    workspaceId: string | null;\n    documentId: string | null;\n  };\n  session: {\n    sessionId: string;\n    assetId: string;\n    workspaceId: string | null;\n    documentId: string | null;\n    userId: string | null;\n  };\n};\n\nexport type OnlyOfficeForcesavePreparationResult = {\n  requests: Array<{\n    via: string;\n    url: string;\n    method: string;\n    headers: OnlyOfficeHeader[];\n    bodyJson: string;\n  }>;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/rust-adapter.ts","s":49,"e":108,"m":1776239508437.8945,"t":"userId: string | null;\n  };\n};\n\nexport type OnlyOfficeForcesavePreparationResult = {\n  requests: Array<{\n    via: string;\n    url: string;\n    method: string;\n    headers: OnlyOfficeHeader[];\n    bodyJson: string;\n  }>;\n};\n\nfunction buildOnlyOfficeServiceContext(request: Request, input: OnlyOfficeServiceActorInput): BridgeContext {\n  return buildDocumentBridgeContextWithActor({\n    request,\n    workspaceId: input.workspaceId ?? null,\n    actor: {\n      actorType: input.actorType ?? \"service\",\n      actorId: input.actorId?.trim() || \"onlyoffice-service\",\n      sessionId: input.sessionId ?? null,\n    },\n    source: {\n      channel: input.sourceChannel,\n      client: input.sourceClient,\n    },\n    idempotencyKey: input.idempotencyKey ?? null,\n  });\n}\n\nasync function runOnlyOfficeTool<TResult>(input: {\n  request: Request;\n  toolName:\n    | \"onlyoffice_session_resolve\"\n    | \"onlyoffice_sign\"\n    | \"onlyoffice_prepare_proxy\"\n    | \"onlyoffice_prepare_callback\"\n    | \"onlyoffice_prepare_forcesave\";\n  args?: Record<string, unknown>;\n  data?: Record<string, unknown>;\n  target?: BridgeTarget | null;\n  actor?: Partial<OnlyOfficeServiceActorInput>;\n}): Promise<TResult> {\n  const context = buildOnlyOfficeServiceContext(input.request, {\n    actorId: input.actor?.actorId,\n    actorType: input.actor?.actorType,\n    sessionId: input.actor?.sessionId ?? null,\n    workspaceId: input.actor?.workspaceId ?? null,\n    sourceChannel: input.actor?.sourceChannel?.trim() || `onlyoffice:${input.toolName}`,\n    sourceClient: input.actor?.sourceClient?.trim() || \"wolai-frontend\",\n    idempotencyKey: input.actor?.idempotencyKey ?? null,\n  });\n  const result = await executeRustBridgeTool<TResult>({\n    context,\n    toolName: input.toolName,\n    invocationKind: \"job\",\n    args: input.args ?? {},\n    data: input.data ?? {},\n    target: input.target ?? null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/rust-adapter.ts","s":97,"e":156,"m":1776239508437.8945,"t":"workspaceId: input.actor?.workspaceId ?? null,\n    sourceChannel: input.actor?.sourceChannel?.trim() || `onlyoffice:${input.toolName}`,\n    sourceClient: input.actor?.sourceClient?.trim() || \"wolai-frontend\",\n    idempotencyKey: input.actor?.idempotencyKey ?? null,\n  });\n  const result = await executeRustBridgeTool<TResult>({\n    context,\n    toolName: input.toolName,\n    invocationKind: \"job\",\n    args: input.args ?? {},\n    data: input.data ?? {},\n    target: input.target ?? null,\n    mode: \"result\",\n  });\n  return result.result;\n}\n\nexport async function signOnlyOfficeConfig(input: {\n  request: Request;\n  config: Record<string, unknown>;\n  secret?: string | null;\n}): Promise<OnlyOfficeSignResult> {\n  return await runOnlyOfficeTool<OnlyOfficeSignResult>({\n    request: input.request,\n    toolName: \"onlyoffice_sign\",\n    data: {\n      config: input.config,\n      secret: input.secret ?? \"\",\n    },\n    actor: {\n      sourceChannel: \"onlyoffice-sign\",\n      sourceClient: \"onlyoffice-sign-route\",\n    },\n  });\n}\n\nexport async function prepareOnlyOfficeProxyRequest(input: {\n  request: Request;\n  encodedUrl: string;\n  method: \"GET\" | \"HEAD\";\n  range: string | null;\n  runtimeConfig: MnoteRuntimeConfig;\n}): Promise<OnlyOfficeProxyPreparationResult> {\n  return await runOnlyOfficeTool<OnlyOfficeProxyPreparationResult>({\n    request: input.request,\n    toolName: \"onlyoffice_prepare_proxy\",\n    data: {\n      encodedUrl: input.encodedUrl,\n      method: input.method,\n      range: input.range,\n      supabaseUrl: input.runtimeConfig.supabaseUrl ?? \"\",\n      supabaseInternalUrl:\n        input.runtimeConfig.supabaseInternalUrl ?? process.env.SUPABASE_INTERNAL_URL ?? \"\",\n      onlyofficeStorageHostOverride: input.runtimeConfig.onlyofficeStorageHostOverride ?? \"\",\n      convexOrigin:\n        process.env.CONVEX_SELF_HOSTED_URL ?? process.env.NEXT_PUBLIC_CONVEX_URL ?? \"\",\n      supabaseAnonKey: input.runtimeConfig.supabaseAnonKey ?? \"\",\n    },\n    actor: {\n      sourceChannel: \"onlyoffice-proxy\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/rust-adapter.ts","s":145,"e":204,"m":1776239508437.8945,"t":"method: input.method,\n      range: input.range,\n      supabaseUrl: input.runtimeConfig.supabaseUrl ?? \"\",\n      supabaseInternalUrl:\n        input.runtimeConfig.supabaseInternalUrl ?? process.env.SUPABASE_INTERNAL_URL ?? \"\",\n      onlyofficeStorageHostOverride: input.runtimeConfig.onlyofficeStorageHostOverride ?? \"\",\n      convexOrigin:\n        process.env.CONVEX_SELF_HOSTED_URL ?? process.env.NEXT_PUBLIC_CONVEX_URL ?? \"\",\n      supabaseAnonKey: input.runtimeConfig.supabaseAnonKey ?? \"\",\n    },\n    actor: {\n      sourceChannel: \"onlyoffice-proxy\",\n      sourceClient: \"onlyoffice-proxy-route\",\n    },\n  });\n}\n\nexport async function prepareOnlyOfficeCallback(input: {\n  request: Request;\n  assetId: string;\n  documentId?: string | null;\n  workspaceId?: string | null;\n  userId?: string | null;\n  sessionId?: string | null;\n  status: number;\n  url?: string | null;\n  key?: string | null;\n  onlyofficeInternalUrl: string;\n}): Promise<OnlyOfficeCallbackPreparationResult> {\n  return await runOnlyOfficeTool<OnlyOfficeCallbackPreparationResult>({\n    request: input.request,\n    toolName: \"onlyoffice_prepare_callback\",\n    data: {\n      assetId: input.assetId,\n      documentId: input.documentId ?? null,\n      workspaceId: input.workspaceId ?? null,\n      userId: input.userId ?? null,\n      sessionId: input.sessionId ?? \"onlyoffice-callback\",\n      status: input.status,\n      url: input.url ?? null,\n      key: input.key ?? null,\n      onlyofficeInternalUrl: input.onlyofficeInternalUrl,\n    },\n    actor: {\n      actorId: input.userId?.trim() || \"onlyoffice-callback\",\n      sessionId: \"onlyoffice-callback\",\n      workspaceId: input.workspaceId ?? null,\n      idempotencyKey: input.key ?? null,\n      sourceChannel: \"onlyoffice-callback\",\n      sourceClient: \"onlyoffice-document-server\",\n    },\n    target: {\n      workspaceId: input.workspaceId ?? null,\n      pageId: input.documentId ?? null,\n      blockId: input.assetId,\n    },\n  });\n}\n\nexport async function prepareOnlyOfficeForcesave(input: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/onlyoffice/rust-adapter.ts","s":193,"e":238,"m":1776239508437.8945,"t":"sourceChannel: \"onlyoffice-callback\",\n      sourceClient: \"onlyoffice-document-server\",\n    },\n    target: {\n      workspaceId: input.workspaceId ?? null,\n      pageId: input.documentId ?? null,\n      blockId: input.assetId,\n    },\n  });\n}\n\nexport async function prepareOnlyOfficeForcesave(input: {\n  request: Request;\n  assetId: string;\n  key: string;\n  onlyofficeInternalUrl: string;\n  secret?: string | null;\n  actorId?: string;\n  workspaceId?: string | null;\n  documentId?: string | null;\n}): Promise<OnlyOfficeForcesavePreparationResult> {\n  return await runOnlyOfficeTool<OnlyOfficeForcesavePreparationResult>({\n    request: input.request,\n    toolName: \"onlyoffice_prepare_forcesave\",\n    data: {\n      assetId: input.assetId,\n      key: input.key,\n      onlyofficeInternalUrl: input.onlyofficeInternalUrl,\n      secret: input.secret ?? \"\",\n    },\n    actor: {\n      actorId: input.actorId?.trim() || \"onlyoffice-forcesave\",\n      sessionId: \"onlyoffice-forcesave\",\n      workspaceId: input.workspaceId ?? null,\n      idempotencyKey: input.key,\n      sourceChannel: \"onlyoffice-forcesave\",\n      sourceClient: \"onlyoffice-forcesave-route\",\n    },\n    target: {\n      workspaceId: input.workspaceId ?? null,\n      pageId: input.documentId ?? null,\n      blockId: input.assetId,\n    },\n  });\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mnote-web/internal-url.ts","s":1,"e":60,"m":1776951250026.5708,"t":"import \"server-only\";\n\nconst DEFAULT_MNOTE_WEB_INTERNAL_URL = \"http://127.0.0.1:3104\";\nconst DEFAULT_MNOTE_WEB_INTERNAL_URL_CANDIDATES = [\n  DEFAULT_MNOTE_WEB_INTERNAL_URL,\n  \"http://localhost:3104\",\n];\nconst MNOTE_WEB_PROBE_PATH = \"/health\";\nconst RESOLVE_CACHE_TTL_MS = 30_000;\n\nlet cachedMnoteWebInternalUrl = \"\";\nlet cachedMnoteWebInternalUrlAt = 0;\nlet pendingMnoteWebInternalUrl: Promise<string> | null = null;\n\nconst normalizeMnoteWebInternalUrl = (raw?: string | null) => {\n  const value = String(raw || \"\").trim().replace(/\\/+$/, \"\");\n  if (!value) return \"\";\n  try {\n    const url = new URL(value);\n    if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n      return \"\";\n    }\n    return url.toString().replace(/\\/+$/, \"\");\n  } catch {\n    return \"\";\n  }\n};\n\nconst getMnoteWebInternalUrlCandidates = () => {\n  const candidates: string[] = [];\n  const push = (value?: string | null) => {\n    const normalized = normalizeMnoteWebInternalUrl(value);\n    if (!normalized) return;\n    if (!candidates.includes(normalized)) {\n      candidates.push(normalized);\n    }\n  };\n\n  push(process.env.MNOTE_WEB_INTERNAL_URL);\n\n  for (const raw of String(process.env.MNOTE_WEB_INTERNAL_URL_CANDIDATES || \"\").split(\",\")) {\n    push(raw);\n  }\n\n  for (const candidate of DEFAULT_MNOTE_WEB_INTERNAL_URL_CANDIDATES) {\n    push(candidate);\n  }\n\n  return candidates.length > 0 ? candidates : [DEFAULT_MNOTE_WEB_INTERNAL_URL];\n};\n\nconst probeMnoteWebInternalUrl = async (candidate: string) => {\n  try {\n    const url = new URL(MNOTE_WEB_PROBE_PATH, `${candidate}/`);\n    const response = await fetch(url, {\n      method: \"GET\",\n      redirect: \"follow\",\n      cache: \"no-store\",\n      signal: AbortSignal.timeout(2_500),\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mnote-web/internal-url.ts","s":49,"e":96,"m":1776951250026.5708,"t":"return candidates.length > 0 ? candidates : [DEFAULT_MNOTE_WEB_INTERNAL_URL];\n};\n\nconst probeMnoteWebInternalUrl = async (candidate: string) => {\n  try {\n    const url = new URL(MNOTE_WEB_PROBE_PATH, `${candidate}/`);\n    const response = await fetch(url, {\n      method: \"GET\",\n      redirect: \"follow\",\n      cache: \"no-store\",\n      signal: AbortSignal.timeout(2_500),\n    });\n    return response.ok;\n  } catch {\n    return false;\n  }\n};\n\nexport const resolveMnoteWebInternalUrl = async () => {\n  const now = Date.now();\n  if (cachedMnoteWebInternalUrl && now - cachedMnoteWebInternalUrlAt < RESOLVE_CACHE_TTL_MS) {\n    return cachedMnoteWebInternalUrl;\n  }\n\n  if (pendingMnoteWebInternalUrl) {\n    return pendingMnoteWebInternalUrl;\n  }\n\n  pendingMnoteWebInternalUrl = (async () => {\n    const candidates = getMnoteWebInternalUrlCandidates();\n    for (const candidate of candidates) {\n      if (await probeMnoteWebInternalUrl(candidate)) {\n        return candidate;\n      }\n    }\n    return candidates[0] || DEFAULT_MNOTE_WEB_INTERNAL_URL;\n  })();\n\n  try {\n    const resolved = await pendingMnoteWebInternalUrl;\n    cachedMnoteWebInternalUrl = resolved;\n    cachedMnoteWebInternalUrlAt = Date.now();\n    return resolved;\n  } finally {\n    pendingMnoteWebInternalUrl = null;\n  }\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.test.ts","s":1,"e":60,"m":1778491178825.9175,"t":"import { describe, expect, it, vi } from \"vitest\";\nimport {\n  createMindmapAdapterProjectionEndpoint,\n  createMindmapCommandApplyEndpoint,\n  executeMindmapCommandApply,\n  executeMindmapCommandApplyAndRefreshProjection,\n  executeMindmapDataPutAndRefreshProjection,\n  isMindmapAdapterProjection,\n  requestMindmapAdapterProjection,\n} from \"./leptos-mindmap-adapter\";\nimport { createFallbackMindmapAdapterProjection } from \"./simple-mind-map-bridge\";\n\ndescribe(\"leptos-mindmap adapter projection\", () => {\n  it(\"识别 adapter projection contract\", () => {\n    const projection = createFallbackMindmapAdapterProjection({ mindmapId: \"mind_1\" });\n    expect(isMindmapAdapterProjection(projection)).toBe(true);\n    expect(isMindmapAdapterProjection({ ...projection, runtime: \"other\" })).toBe(false);\n  });\n\n  it(\"构建 simple_mind_map_scene 查询 endpoint\", () => {\n    expect(createMindmapAdapterProjectionEndpoint(\"doc 1\", \"mind/1\")).toBe(\n      \"/api/mindmap/doc%201/mind%2F1?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get\",\n    );\n    expect(createMindmapCommandApplyEndpoint(\"doc 1\", \"mind/1\")).toBe(\n      \"/api/mindmap/doc%201/mind%2F1\",\n    );\n  });\n\n  it(\"从 result wrapper 中读取 projection\", async () => {\n    const projection = createFallbackMindmapAdapterProjection({ mindmapId: \"mind_1\" });\n    const fetcher = vi.fn(async () => ({\n      ok: true,\n      json: async () => ({ result: projection }),\n    })) as unknown as typeof fetch;\n\n    await expect(\n      requestMindmapAdapterProjection({\n        documentId: \"doc_1\",\n        mindmapId: \"mind_1\",\n        fetcher,\n      }),\n    ).resolves.toEqual(projection);\n  });\n\n  it(\"command apply 成功后可刷新 adapter projection\", async () => {\n    const projection = createFallbackMindmapAdapterProjection({ mindmapId: \"mind_1\", kernelRevision: 12 });\n    const fetcher = vi\n      .fn()\n      .mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({ ok: true, kernelRevision: 11 }),\n      })\n      .mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({ result: projection }),\n      }) as unknown as typeof fetch;\n\n    await expect(\n      executeMindmapCommandApplyAndRefreshProjection({\n        documentId: \"doc_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.test.ts","s":49,"e":108,"m":1778491178825.9175,"t":".mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({ ok: true, kernelRevision: 11 }),\n      })\n      .mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({ result: projection }),\n      }) as unknown as typeof fetch;\n\n    await expect(\n      executeMindmapCommandApplyAndRefreshProjection({\n        documentId: \"doc_1\",\n        mindmapId: \"mind_1\",\n        commands: [{ type: \"updateText\", mindmapId: \"mind_1\", nodeId: \"root\", text: \"新标题\" }],\n        projectionRevision: 10,\n        fetcher,\n      }),\n    ).resolves.toEqual(projection);\n    expect(fetcher).toHaveBeenNthCalledWith(\n      1,\n      \"/api/mindmap/doc_1/mind_1\",\n      expect.objectContaining({\n        method: \"POST\",\n        body: expect.stringContaining('\"commandName\":\"mindmap.command.apply\"'),\n      }),\n    );\n  });\n\n  it(\"command apply 失败时抛出可定位 command_failed 错误\", async () => {\n    const fetcher = vi.fn(async () => ({\n      ok: false,\n      status: 409,\n      json: async () => ({ error: \"revision_conflict\" }),\n    })) as unknown as typeof fetch;\n\n    await expect(\n      executeMindmapCommandApply({\n        documentId: \"doc_1\",\n        mindmapId: \"mind_1\",\n        commands: [{ type: \"deleteNode\", mindmapId: \"mind_1\", nodeId: \"node_1\" }],\n        fetcher,\n      }),\n    ).rejects.toMatchObject({ code: \"command_failed\", status: 409, message: \"command_failed:409:revision_conflict\" });\n  });\n\n  it(\"data put 失败时透出 route 错误细节\", async () => {\n    const fetcher = vi.fn(async () => ({\n      ok: false,\n      status: 400,\n      json: async () => ({ error: \"mindmap_put_failed\", details: [\"missing_root_uid\"] }),\n    })) as unknown as typeof fetch;\n\n    await expect(\n      executeMindmapDataPutAndRefreshProjection({\n        documentId: \"doc_1\",\n        mindmapId: \"mind_1\",\n        data: { data: { text: \"中心主题\" }, children: [] },\n        fetcher,\n      }),\n    ).rejects.toMatchObject({ code: \"command_failed\", status: 400, message: \"command_failed:400:mindmap_put_failed\" });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.test.ts","s":97,"e":111,"m":1778491178825.9175,"t":"status: 400,\n      json: async () => ({ error: \"mindmap_put_failed\", details: [\"missing_root_uid\"] }),\n    })) as unknown as typeof fetch;\n\n    await expect(\n      executeMindmapDataPutAndRefreshProjection({\n        documentId: \"doc_1\",\n        mindmapId: \"mind_1\",\n        data: { data: { text: \"中心主题\" }, children: [] },\n        fetcher,\n      }),\n    ).rejects.toMatchObject({ code: \"command_failed\", status: 400, message: \"command_failed:400:mindmap_put_failed\" });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.ts","s":1,"e":60,"m":1778645763929.0535,"t":"import {\n  createSimpleMindMapBridge,\n  type MindmapAdapterProjection,\n  type SimpleMindMapBridge,\n  type SimpleMindMapBridgeEvent,\n  type SimpleMindMapPluginName,\n} from \"./simple-mind-map-bridge\";\nimport type { MindmapCompatPayloadPatch, MindmapKernelCommand } from \"./mindmap-command-diff\";\n\nexport type {\n  MindmapAdapterProjection,\n  SimpleMindMapBridge,\n  SimpleMindMapBridgeEvent,\n} from \"./simple-mind-map-bridge\";\n\nexport type LeptosMindmapAdapterOptions = {\n  el: HTMLElement;\n  projection: MindmapAdapterProjection;\n  mode?: \"edit\" | \"readonly\";\n  pluginNames?: SimpleMindMapPluginName[];\n  runtimeOptions?: Record<string, unknown>;\n  onEvent?: (event: SimpleMindMapBridgeEvent) => void;\n};\n\nexport type RequestMindmapAdapterProjectionInput = {\n  documentId: string;\n  mindmapId: string;\n  endpoint?: string;\n  fetcher?: typeof fetch;\n};\n\nexport type MindmapCommandApplyInput = {\n  documentId: string;\n  mindmapId: string;\n  commands: Array<MindmapKernelCommand | MindmapCompatPayloadPatch>;\n  projectionRevision?: number | null;\n  endpoint?: string;\n  fetcher?: typeof fetch;\n};\n\nexport type MindmapDataPutInput = {\n  documentId: string;\n  mindmapId: string;\n  data: unknown;\n  projectionEndpoint?: string;\n  endpoint?: string;\n  fetcher?: typeof fetch;\n};\n\nexport type MindmapCommandApplyResult = {\n  ok: true;\n  kernelRevision: number | null;\n  raw: unknown;\n};\n\nexport class MindmapCommandBridgeError extends Error {\n  readonly code: \"command_failed\";\n  readonly status: number | null;\n\n  constructor(message: string, status: number | null = null) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.ts","s":49,"e":108,"m":1778645763929.0535,"t":"export type MindmapCommandApplyResult = {\n  ok: true;\n  kernelRevision: number | null;\n  raw: unknown;\n};\n\nexport class MindmapCommandBridgeError extends Error {\n  readonly code: \"command_failed\";\n  readonly status: number | null;\n\n  constructor(message: string, status: number | null = null) {\n    super(message);\n    this.name = \"MindmapCommandBridgeError\";\n    this.code = \"command_failed\";\n    this.status = status;\n  }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst readErrorDetailFromPayload = (raw: unknown): string => {\n  if (!isRecord(raw)) return \"\";\n  const detail = [raw.error, raw.message, raw.details]\n    .map((value) => {\n      if (typeof value === \"string\") return value.trim();\n      if (Array.isArray(value) && value.length > 0) return value.join(\"|\");\n      return \"\";\n    })\n    .find(Boolean);\n  return detail ? `:${detail}` : \"\";\n};\n\nconst readErrorDetail = async (response: Response): Promise<string> => {\n  if (typeof response.text !== \"function\") {\n    const raw = (await response.json().catch(() => null)) as unknown;\n    return readErrorDetailFromPayload(raw);\n  }\n  const rawText = await response.text().catch(() => \"\");\n  if (!rawText.trim()) return \"\";\n  try {\n    return readErrorDetailFromPayload(JSON.parse(rawText) as unknown) || `:${rawText.trim()}`;\n  } catch {\n    return `:${rawText.trim()}`;\n  }\n};\n\nexport const isMindmapAdapterProjection = (value: unknown): value is MindmapAdapterProjection => {\n  if (!isRecord(value)) return false;\n  return (\n    value.schema === \"mnote.mindmap.simple_mind_map_scene.v1\" &&\n    value.runtime === \"simple-mind-map\" &&\n    \"root\" in value &&\n    typeof value.kernelRevision === \"number\"\n  );\n};\n\nexport const createMindmapAdapterProjectionEndpoint = (documentId: string, mindmapId: string) => {\n  const doc = encodeURIComponent(documentId);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.ts","s":97,"e":156,"m":1778645763929.0535,"t":"export const isMindmapAdapterProjection = (value: unknown): value is MindmapAdapterProjection => {\n  if (!isRecord(value)) return false;\n  return (\n    value.schema === \"mnote.mindmap.simple_mind_map_scene.v1\" &&\n    value.runtime === \"simple-mind-map\" &&\n    \"root\" in value &&\n    typeof value.kernelRevision === \"number\"\n  );\n};\n\nexport const createMindmapAdapterProjectionEndpoint = (documentId: string, mindmapId: string) => {\n  const doc = encodeURIComponent(documentId);\n  const map = encodeURIComponent(mindmapId);\n  return `/api/mindmap/${doc}/${map}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`;\n};\n\nexport const createMindmapCommandApplyEndpoint = (documentId: string, mindmapId: string) => {\n  const doc = encodeURIComponent(documentId);\n  const map = encodeURIComponent(mindmapId);\n  return `/api/mindmap/${doc}/${map}`;\n};\n\nexport const requestMindmapAdapterProjection = async (\n  input: RequestMindmapAdapterProjectionInput,\n): Promise<MindmapAdapterProjection> => {\n  const fetcher = input.fetcher ?? fetch;\n  const endpoint = input.endpoint ?? createMindmapAdapterProjectionEndpoint(input.documentId, input.mindmapId);\n  const response = await fetcher(endpoint, {\n    headers: { Accept: \"application/json\" },\n  });\n  if (!response.ok) {\n    throw new Error(`projection_load_failed:${response.status}`);\n  }\n  const payload = (await response.json()) as unknown;\n  const projection = isRecord(payload) && \"result\" in payload ? payload.result : payload;\n  if (!isMindmapAdapterProjection(projection)) {\n    throw new Error(\"projection_load_failed:invalid_adapter_projection\");\n  }\n  return projection;\n};\n\nconst readKernelRevision = (value: unknown): number | null => {\n  if (!isRecord(value)) return null;\n  const candidates = [\n    value.kernelRevision,\n    value.projectionRevision,\n    isRecord(value.result) ? value.result.kernelRevision : null,\n    isRecord(value.result) ? value.result.projectionRevision : null,\n  ];\n  for (const candidate of candidates) {\n    if (typeof candidate === \"number\" && Number.isFinite(candidate)) return candidate;\n  }\n  return null;\n};\n\nexport const executeMindmapCommandApply = async (\n  input: MindmapCommandApplyInput,\n): Promise<MindmapCommandApplyResult> => {\n  if (input.commands.length === 0) {\n    throw new MindmapCommandBridgeError(\"command_failed:empty_commands\");","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.ts","s":145,"e":204,"m":1778645763929.0535,"t":"];\n  for (const candidate of candidates) {\n    if (typeof candidate === \"number\" && Number.isFinite(candidate)) return candidate;\n  }\n  return null;\n};\n\nexport const executeMindmapCommandApply = async (\n  input: MindmapCommandApplyInput,\n): Promise<MindmapCommandApplyResult> => {\n  if (input.commands.length === 0) {\n    throw new MindmapCommandBridgeError(\"command_failed:empty_commands\");\n  }\n  const fetcher = input.fetcher ?? fetch;\n  const endpoint = input.endpoint ?? createMindmapCommandApplyEndpoint(input.documentId, input.mindmapId);\n  const requestBody = JSON.stringify({\n    commandName: \"mindmap.command.apply\",\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    commands: input.commands,\n    projectionRevision: input.projectionRevision ?? null,\n  });\n  const response = await fetcher(endpoint, {\n    method: \"POST\",\n    keepalive: requestBody.length <= 60_000,\n    headers: {\n      Accept: \"application/json\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: requestBody,\n  });\n  const raw = (await response.json().catch(() => null)) as unknown;\n  if (!response.ok) {\n    const detail = readErrorDetailFromPayload(raw);\n    throw new MindmapCommandBridgeError(\n      `command_failed:${response.status}${detail}`,\n      response.status,\n    );\n  }\n  return {\n    ok: true,\n    kernelRevision: readKernelRevision(raw),\n    raw,\n  };\n};\n\nexport const executeMindmapCommandApplyAndRefreshProjection = async (\n  input: MindmapCommandApplyInput & { projectionEndpoint?: string },\n): Promise<MindmapAdapterProjection> => {\n  await executeMindmapCommandApply(input);\n  return requestMindmapAdapterProjection({\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    endpoint: input.projectionEndpoint,\n    fetcher: input.fetcher,\n  });\n};\n\nexport const executeMindmapDataPutAndRefreshProjection = async (\n  input: MindmapDataPutInput,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.ts","s":193,"e":240,"m":1778645763929.0535,"t":"): Promise<MindmapAdapterProjection> => {\n  await executeMindmapCommandApply(input);\n  return requestMindmapAdapterProjection({\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    endpoint: input.projectionEndpoint,\n    fetcher: input.fetcher,\n  });\n};\n\nexport const executeMindmapDataPutAndRefreshProjection = async (\n  input: MindmapDataPutInput,\n): Promise<MindmapAdapterProjection> => {\n  const fetcher = input.fetcher ?? fetch;\n  const endpoint = input.endpoint ?? createMindmapCommandApplyEndpoint(input.documentId, input.mindmapId);\n  const response = await fetcher(endpoint, {\n    method: \"POST\",\n    headers: {\n      Accept: \"application/json\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      data: input.data,\n      createOnly: false,\n    }),\n  });\n  if (!response.ok) {\n    const detail = await readErrorDetail(response);\n    throw new MindmapCommandBridgeError(`command_failed:${response.status}${detail}`, response.status);\n  }\n  await response.json().catch(() => null);\n  return requestMindmapAdapterProjection({\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    endpoint: input.projectionEndpoint,\n    fetcher,\n  });\n};\n\nexport const createLeptosMindmapAdapter = async (\n  input: LeptosMindmapAdapterOptions,\n): Promise<SimpleMindMapBridge> => {\n  if (!isMindmapAdapterProjection(input.projection)) {\n    throw new Error(\"adapter_init_failed:invalid_projection\");\n  }\n  return createSimpleMindMapBridge(input);\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-action-map.test.ts","s":1,"e":60,"m":1778688996967.2717,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  getMindmapActionMapping,\n  listMindmapActionMappings,\n  mapMindmapActionToCommand,\n} from \"./mindmap-action-map\";\n\ndescribe(\"mindmap action map\", () => {\n  it(\"把核心节点动作映射到 runtime command 和 kernel command\", () => {\n    expect(mapMindmapActionToCommand({ actionId: \"insertChild\", mindmapId: \"mind_1\", activeNodeId: \"node_1\" })).toEqual({\n      runtimeCommand: \"INSERT_CHILD_NODE\",\n      command: {\n        type: \"insertChild\",\n        mindmapId: \"mind_1\",\n        parentNodeId: \"node_1\",\n        node: { text: \"新节点\" },\n      },\n    });\n    expect(\n      mapMindmapActionToCommand({ actionId: \"insertSiblingAfter\", mindmapId: \"mind_1\", activeNodeId: \"node_1\" }),\n    ).toEqual({\n      runtimeCommand: \"INSERT_NODE\",\n      command: {\n        type: \"insertSiblingAfter\",\n        mindmapId: \"mind_1\",\n        targetNodeId: \"node_1\",\n        node: { text: \"新节点\" },\n      },\n    });\n    expect(mapMindmapActionToCommand({ actionId: \"deleteNode\", mindmapId: \"mind_1\", activeNodeId: \"node_1\" })).toEqual({\n      runtimeCommand: \"REMOVE_NODE\",\n      command: { type: \"deleteNode\", mindmapId: \"mind_1\", nodeId: \"node_1\" },\n    });\n  });\n\n  it(\"把视图动作映射到 localView，不依赖 active node\", () => {\n    expect(getMindmapActionMapping(\"centerRoot\")).toMatchObject({ target: \"localView\", runtimeMethod: \"centerRoot\" });\n    expect(getMindmapActionMapping(\"zoomIn\")).toMatchObject({ target: \"localView\", runtimeMethod: \"zoomIn\" });\n    expect(getMindmapActionMapping(\"zoomOut\")).toMatchObject({ target: \"localView\", runtimeMethod: \"zoomOut\" });\n    expect(getMindmapActionMapping(\"fitView\")).toMatchObject({ target: \"localView\", runtimeMethod: \"fitView\" });\n    expect(getMindmapActionMapping(\"fullscreenCanvas\")).toMatchObject({ target: \"localView\", requiresActiveNode: false });\n    expect(getMindmapActionMapping(\"fullscreenPage\")).toMatchObject({ target: \"localView\", requiresActiveNode: false });\n    expect(getMindmapActionMapping(\"exitFullscreen\")).toMatchObject({ target: \"localView\", requiresActiveNode: false });\n    expect(getMindmapActionMapping(\"showMenu\")).toMatchObject({ target: \"localView\", requiresActiveNode: false });\n  });\n\n  it(\"全屏动作不产生 kernel command，readonly 下仍可使用\", () => {\n    expect(mapMindmapActionToCommand({ actionId: \"fullscreenCanvas\", mindmapId: \"mind_1\" })).toEqual({});\n    expect(mapMindmapActionToCommand({ actionId: \"fullscreenPage\", mindmapId: \"mind_1\" })).toEqual({});\n    expect(mapMindmapActionToCommand({ actionId: \"exitFullscreen\", mindmapId: \"mind_1\" })).toEqual({});\n    expect(mapMindmapActionToCommand({ actionId: \"showMenu\", mindmapId: \"mind_1\" })).toEqual({});\n    expect(getMindmapActionMapping(\"fullscreenCanvas\")).toMatchObject({ readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"fullscreenPage\")).toMatchObject({ readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"exitFullscreen\")).toMatchObject({ readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"showMenu\")).toMatchObject({ readonlyAllowed: true });\n  });\n\n  it(\"导出能力暂缓时不下发未受控 runtime command\", () => {\n    expect(getMindmapActionMapping(\"export\")).toMatchObject({ target: \"localView\", readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"export\")?.runtimeCommand).toBeUndefined();","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-action-map.test.ts","s":49,"e":107,"m":1778688996967.2717,"t":"expect(mapMindmapActionToCommand({ actionId: \"fullscreenPage\", mindmapId: \"mind_1\" })).toEqual({});\n    expect(mapMindmapActionToCommand({ actionId: \"exitFullscreen\", mindmapId: \"mind_1\" })).toEqual({});\n    expect(mapMindmapActionToCommand({ actionId: \"showMenu\", mindmapId: \"mind_1\" })).toEqual({});\n    expect(getMindmapActionMapping(\"fullscreenCanvas\")).toMatchObject({ readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"fullscreenPage\")).toMatchObject({ readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"exitFullscreen\")).toMatchObject({ readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"showMenu\")).toMatchObject({ readonlyAllowed: true });\n  });\n\n  it(\"导出能力暂缓时不下发未受控 runtime command\", () => {\n    expect(getMindmapActionMapping(\"export\")).toMatchObject({ target: \"localView\", readonlyAllowed: true });\n    expect(getMindmapActionMapping(\"export\")?.runtimeCommand).toBeUndefined();\n    expect(mapMindmapActionToCommand({ actionId: \"export\", mindmapId: \"mind_1\" })).toEqual({});\n  });\n\n  it(\"把主题、结构和扩展字段映射到 kernel/compat\", () => {\n    expect(mapMindmapActionToCommand({ actionId: \"setTheme\", mindmapId: \"mind_1\", value: \"classic4\" })).toEqual({\n      command: { type: \"setTheme\", mindmapId: \"mind_1\", theme: \"classic4\" },\n    });\n    expect(mapMindmapActionToCommand({ actionId: \"setLayout\", mindmapId: \"mind_1\", value: \"logicalStructure\" })).toEqual({\n      command: { type: \"setLayout\", mindmapId: \"mind_1\", layout: \"logicalStructure\" },\n    });\n    expect(mapMindmapActionToCommand({ actionId: \"note\", mindmapId: \"mind_1\", activeNodeId: \"node_1\", value: \"备注\" })).toEqual({\n      command: {\n        type: \"compatPayloadPatch\",\n        mindmapId: \"mind_1\",\n        path: \"nodes.node_1.data.note\",\n        value: \"备注\",\n        source: \"toolbar\",\n        actionId: \"note\",\n      },\n    });\n    expect(\n      mapMindmapActionToCommand({\n        actionId: \"painter\",\n        mindmapId: \"mind_1\",\n        activeNodeId: \"node_1\",\n        value: { fillColor: \"#dbeafe\" },\n        source: \"sidebar\",\n      }),\n    ).toEqual({\n      command: {\n        type: \"compatPayloadPatch\",\n        mindmapId: \"mind_1\",\n        path: \"nodes.node_1.data.style\",\n        value: { fillColor: \"#dbeafe\" },\n        source: \"sidebar\",\n        actionId: \"painter\",\n      },\n    });\n  });\n\n  it(\"所有 action id 映射唯一\", () => {\n    const mappings = listMindmapActionMappings();\n    const ids = mappings.map((mapping) => mapping.actionId);\n    expect(new Set(ids).size).toBe(ids.length);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-action-map.ts","s":1,"e":60,"m":1778688996967.2717,"t":"import {\n  createCompatPayloadPatch,\n  createLayoutCommand,\n  createThemeCommand,\n  createToolbarKernelCommand,\n  type MindmapCompatPayloadPatch,\n  type MindmapKernelCommand,\n  type MindmapNodeInput,\n} from \"./mindmap-command-diff\";\nimport type { MindmapUiActionId } from \"./mindmap-ui-schema\";\n\nexport type MindmapActionTarget = \"runtimeCommand\" | \"kernelCommand\" | \"compatPatch\" | \"localView\";\n\nexport type MindmapRuntimeViewMethod = \"centerRoot\" | \"zoomIn\" | \"zoomOut\" | \"fitView\";\n\nexport type MindmapActionMapping = {\n  actionId: MindmapUiActionId;\n  target: MindmapActionTarget;\n  runtimeCommand?: string;\n  runtimeMethod?: MindmapRuntimeViewMethod;\n  requiresActiveNode: boolean;\n  readonlyAllowed: boolean;\n  compatPath?: (activeNodeId: string | null) => string | null;\n};\n\nexport type MindmapActionCommandInput = {\n  actionId: MindmapUiActionId;\n  mindmapId: string;\n  activeNodeId?: string | null;\n  value?: unknown;\n  node?: MindmapNodeInput;\n  themeConfig?: unknown;\n  source?: MindmapCompatPayloadPatch[\"source\"];\n  runtimeRevision?: number | null;\n  kernelRevision?: number | null;\n};\n\nexport type MindmapActionCommandResult = {\n  runtimeCommand?: string;\n  command?: MindmapKernelCommand | MindmapCompatPayloadPatch;\n};\n\nconst nodeDataPath = (field: string) => (activeNodeId: string | null): string | null =>\n  activeNodeId ? `nodes.${activeNodeId}.data.${field}` : null;\n\nconst mappings: MindmapActionMapping[] = [\n  { actionId: \"undo\", target: \"runtimeCommand\", runtimeCommand: \"BACK\", requiresActiveNode: false, readonlyAllowed: false },\n  { actionId: \"redo\", target: \"runtimeCommand\", runtimeCommand: \"FORWARD\", requiresActiveNode: false, readonlyAllowed: false },\n  { actionId: \"editNode\", target: \"localView\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"insertSiblingAfter\", target: \"runtimeCommand\", runtimeCommand: \"INSERT_NODE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"insertChild\", target: \"runtimeCommand\", runtimeCommand: \"INSERT_CHILD_NODE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"deleteNode\", target: \"runtimeCommand\", runtimeCommand: \"REMOVE_NODE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"summary\", target: \"runtimeCommand\", runtimeCommand: \"ADD_GENERALIZATION\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"associativeLine\", target: \"compatPatch\", runtimeCommand: \"ADD_ASSOCIATIVE_LINE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"setTheme\", target: \"kernelCommand\", requiresActiveNode: false, readonlyAllowed: false },\n  { actionId: \"setLayout\", target: \"kernelCommand\", requiresActiveNode: false, readonlyAllowed: false },\n  { actionId: \"tag\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"tag\") },\n  { actionId: \"hyperlink\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"hyperlink\") },\n  { actionId: \"note\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"note\") },\n  { actionId: \"image\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"image\") },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-action-map.ts","s":49,"e":90,"m":1778688996967.2717,"t":"{ actionId: \"editNode\", target: \"localView\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"insertSiblingAfter\", target: \"runtimeCommand\", runtimeCommand: \"INSERT_NODE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"insertChild\", target: \"runtimeCommand\", runtimeCommand: \"INSERT_CHILD_NODE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"deleteNode\", target: \"runtimeCommand\", runtimeCommand: \"REMOVE_NODE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"summary\", target: \"runtimeCommand\", runtimeCommand: \"ADD_GENERALIZATION\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"associativeLine\", target: \"compatPatch\", runtimeCommand: \"ADD_ASSOCIATIVE_LINE\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"setTheme\", target: \"kernelCommand\", requiresActiveNode: false, readonlyAllowed: false },\n  { actionId: \"setLayout\", target: \"kernelCommand\", requiresActiveNode: false, readonlyAllowed: false },\n  { actionId: \"tag\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"tag\") },\n  { actionId: \"hyperlink\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"hyperlink\") },\n  { actionId: \"note\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"note\") },\n  { actionId: \"image\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"image\") },\n  { actionId: \"icon\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"icon\") },\n  { actionId: \"formula\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"formula\") },\n  { actionId: \"painter\", target: \"compatPatch\", requiresActiveNode: true, readonlyAllowed: false, compatPath: nodeDataPath(\"style\") },\n  { actionId: \"import\", target: \"compatPatch\", requiresActiveNode: false, readonlyAllowed: false, compatPath: () => \"import\" },\n  // 导出能力暂缓：UI state 会禁用该入口，这里不能继续下发未受控的 runtime EXPORT 命令。\n  { actionId: \"export\", target: \"localView\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"centerRoot\", target: \"localView\", runtimeMethod: \"centerRoot\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"zoomIn\", target: \"localView\", runtimeMethod: \"zoomIn\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"zoomOut\", target: \"localView\", runtimeMethod: \"zoomOut\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"fitView\", target: \"localView\", runtimeMethod: \"fitView\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"fullscreenCanvas\", target: \"localView\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"fullscreenPage\", target: \"localView\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"exitFullscreen\", target: \"localView\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"search\", target: \"localView\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"showMenu\", target: \"localView\", requiresActiveNode: false, readonlyAllowed: true },\n  { actionId: \"expandCollapse\", target: \"localView\", requiresActiveNode: true, readonlyAllowed: false },\n  { actionId: \"copyNodeText\", target: \"localView\", requiresActiveNode: true, readonlyAllowed: true },\n  { actionId: \"readonly\", target: \"localView\", requiresActiveNode: false, readonlyAllowed: true },\n];\n\nexport const listMindmapActionMappings = (): MindmapActionMapping[] => mappings;\n\nexport const getMindmapActionMapping = (actionId: MindmapUiActionId): MindmapActionMapping | null =>\n  mappings.find((mapping) => mapping.actionId === actionId) ?? null;\n\nconst readActiveNodeId = (value: string | null | undefined): string | null => {\n  const trimmed = value?.trim();\n  return trimmed ? trimmed : null;\n};\n","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-action-map.ts","s":91,"e":108,"m":1778688996967.2717,"t":"export const mapMindmapActionToCommand = (input: MindmapActionCommandInput): MindmapActionCommandResult | null => {\n  const mapping = getMindmapActionMapping(input.actionId);\n  if (!mapping) return null;\n\n  const activeNodeId = readActiveNodeId(input.activeNodeId);\n  if (mapping.requiresActiveNode && !activeNodeId) return null;\n\n  if (input.actionId === \"setTheme\") {\n    return { command: createThemeCommand(input.mindmapId, input.value, input.themeConfig) };\n  }\n  if (input.actionId === \"setLayout\") {\n    return { command: createLayoutCommand(input.mindmapId, input.value) };\n  }\n  if (input.actionId === \"editNode\") {\n    return {\n      command: {\n        type: \"updateText\",\n        mindmapId: input.mindmapId,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-action-map.ts","s":97,"e":147,"m":1778688996967.2717,"t":"if (input.actionId === \"setTheme\") {\n    return { command: createThemeCommand(input.mindmapId, input.value, input.themeConfig) };\n  }\n  if (input.actionId === \"setLayout\") {\n    return { command: createLayoutCommand(input.mindmapId, input.value) };\n  }\n  if (input.actionId === \"editNode\") {\n    return {\n      command: {\n        type: \"updateText\",\n        mindmapId: input.mindmapId,\n        nodeId: activeNodeId ?? \"root\",\n        text: typeof input.value === \"string\" ? input.value : \"\",\n      },\n    };\n  }\n\n  if (mapping.runtimeCommand && [\"INSERT_CHILD_NODE\", \"INSERT_NODE\", \"DELETE_NODE\", \"REMOVE_NODE\"].includes(mapping.runtimeCommand)) {\n    const command = createToolbarKernelCommand({\n      mindmapId: input.mindmapId,\n      runtimeCommand: mapping.runtimeCommand,\n      activeNodeId,\n      newNode: input.node,\n    });\n    return command ? { runtimeCommand: mapping.runtimeCommand, command } : { runtimeCommand: mapping.runtimeCommand };\n  }\n\n  if (mapping.compatPath) {\n    const path = mapping.compatPath(activeNodeId);\n    if (!path) return null;\n    return {\n      command: createCompatPayloadPatch({\n        mindmapId: input.mindmapId,\n        path,\n        value: input.value ?? true,\n        source: input.source ?? \"toolbar\",\n        actionId: input.actionId,\n        runtimeRevision: input.runtimeRevision,\n        kernelRevision: input.kernelRevision,\n      }),\n    };\n  }\n\n  if (mapping.runtimeCommand) {\n    return { runtimeCommand: mapping.runtimeCommand };\n  }\n\n  return {};\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.test.ts","s":1,"e":60,"m":1778506368629.5134,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  applyMindmapCommandsLocally,\n  applyMetadataOnlyMindmapCommands,\n  isLocallyApplicableMindmapCommandSet,\n  isMetadataOnlyMindmapCommandSet,\n} from \"./mindmap-command-apply-local\";\n\ndescribe(\"mindmap command apply local\", () => {\n  it(\"识别 metadata-only 命令集合\", () => {\n    expect(\n      isMetadataOnlyMindmapCommandSet([\n        { type: \"setLayout\", mindmapId: \"mind_1\", layout: \"mindMap\" },\n        { type: \"setTheme\", mindmapId: \"mind_1\", theme: \"dark\" },\n        { type: \"patchView\", mindmapId: \"mind_1\", patch: { state: { scale: 1.2 } } },\n        { type: \"compatPayloadPatch\", mindmapId: \"mind_1\", path: \"root.data.fillColor\", value: \"#dbeafe\" },\n      ]),\n    ).toBe(true);\n    expect(\n      isMetadataOnlyMindmapCommandSet([\n        { type: \"insertChild\", mindmapId: \"mind_1\", parentNodeId: \"root\", node: { text: \"新节点\" } },\n      ]),\n    ).toBe(false);\n  });\n\n  it(\"把 layout/theme/view/compat patch 合并回完整 mindmap blob\", () => {\n    const result = applyMetadataOnlyMindmapCommands(\n      {\n        data: { uid: \"root\", text: \"KMIND\" },\n        children: [{ data: { uid: \"node_1\", text: \"子节点\" }, children: [] }],\n        layout: \"logicalStructure\",\n        theme: \"classic\",\n        themeConfig: { lineColor: \"#60a5fa\" },\n        view: { state: { scale: 1 } },\n        compatPayload: { source: \"compat-blob\" },\n      },\n      [\n        { type: \"setLayout\", mindmapId: \"mind_1\", layout: \"mindMap\" },\n        { type: \"setTheme\", mindmapId: \"mind_1\", theme: \"dark\" },\n        { type: \"patchView\", mindmapId: \"mind_1\", patch: { state: { scale: 1.25, x: 10 } } },\n        { type: \"compatPayloadPatch\", mindmapId: \"mind_1\", path: \"nodes.node_1.data.fillColor\", value: \"#dbeafe\" },\n        { type: \"compatPayloadPatch\", mindmapId: \"mind_1\", path: \"style.map.backgroundColor\", value: \"#0f172a\" },\n      ],\n    );\n\n    expect(result.errors).toEqual([]);\n    expect(result.applied).toBe(5);\n    expect(result.data.layout).toBe(\"mindMap\");\n    expect(result.data.theme).toBe(\"dark\");\n    expect(result.data.view).toEqual({ state: { scale: 1.25, x: 10 } });\n    expect(result.data.children[0].data.fillColor).toBe(\"#dbeafe\");\n    expect(result.data.compatPayload).toEqual({\n      source: \"compat-blob\",\n      style: {\n        map: {\n          backgroundColor: \"#0f172a\",\n        },\n      },\n    });\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.test.ts","s":49,"e":108,"m":1778506368629.5134,"t":"expect(result.data.theme).toBe(\"dark\");\n    expect(result.data.view).toEqual({ state: { scale: 1.25, x: 10 } });\n    expect(result.data.children[0].data.fillColor).toBe(\"#dbeafe\");\n    expect(result.data.compatPayload).toEqual({\n      source: \"compat-blob\",\n      style: {\n        map: {\n          backgroundColor: \"#0f172a\",\n        },\n      },\n    });\n  });\n\n  it(\"识别可本地应用的结构命令集合\", () => {\n    expect(\n      isLocallyApplicableMindmapCommandSet([\n        { type: \"updateText\", mindmapId: \"mind_1\", nodeId: \"node_1\", text: \"改名\" },\n        { type: \"insertChild\", mindmapId: \"mind_1\", parentNodeId: \"node_1\", node: { uid: \"node_2\", text: \"子节点\" } },\n        { type: \"insertSiblingAfter\", mindmapId: \"mind_1\", targetNodeId: \"node_1\", node: { uid: \"node_3\", text: \"同级\" } },\n        { type: \"deleteNode\", mindmapId: \"mind_1\", nodeId: \"node_3\" },\n      ]),\n    ).toBe(true);\n    expect(\n      isLocallyApplicableMindmapCommandSet([\n        { type: \"moveNode\", mindmapId: \"mind_1\", nodeId: \"node_1\", newParentNodeId: \"root\" },\n      ]),\n    ).toBe(false);\n  });\n\n  it(\"可本地应用 update/insert/delete 到 mindmap tree，避免每次都整块重挂载\", () => {\n    const result = applyMindmapCommandsLocally(\n      {\n        data: { uid: \"root\", text: \"KMIND\" },\n        children: [\n          {\n            data: { uid: \"node_1\", text: \"节点 1\" },\n            children: [],\n          },\n          {\n            data: { uid: \"node_2\", text: \"节点 2\" },\n            children: [],\n          },\n        ],\n        layout: \"logicalStructure\",\n        theme: \"default\",\n      },\n      [\n        { type: \"updateText\", mindmapId: \"mind_1\", nodeId: \"node_1\", text: \"节点 1A\" },\n        {\n          type: \"insertChild\",\n          mindmapId: \"mind_1\",\n          parentNodeId: \"node_1\",\n          node: { uid: \"node_1_child\", text: \"子节点 A\" },\n        },\n        {\n          type: \"insertSiblingAfter\",\n          mindmapId: \"mind_1\",\n          targetNodeId: \"node_1\",\n          node: { uid: \"node_1_sibling\", text: \"同级 A\" },\n        },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.test.ts","s":97,"e":122,"m":1778506368629.5134,"t":"{\n          type: \"insertChild\",\n          mindmapId: \"mind_1\",\n          parentNodeId: \"node_1\",\n          node: { uid: \"node_1_child\", text: \"子节点 A\" },\n        },\n        {\n          type: \"insertSiblingAfter\",\n          mindmapId: \"mind_1\",\n          targetNodeId: \"node_1\",\n          node: { uid: \"node_1_sibling\", text: \"同级 A\" },\n        },\n        { type: \"deleteNode\", mindmapId: \"mind_1\", nodeId: \"node_2\" },\n      ],\n    );\n\n    expect(result.errors).toEqual([]);\n    expect(result.applied).toBe(4);\n    expect(result.data.children).toHaveLength(2);\n    expect(result.data.children[0].data.text).toBe(\"节点 1A\");\n    expect(result.data.children[0].children[0].data.uid).toBe(\"node_1_child\");\n    expect(result.data.children[1].data.uid).toBe(\"node_1_sibling\");\n    expect(result.data.children[1].data.text).toBe(\"同级 A\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.ts","s":1,"e":60,"m":1778506345309.2224,"t":"import type { MindmapCompatPayloadPatch, MindmapKernelCommand } from \"./mindmap-command-diff\";\n\nconst defaultMindmapData = {\n  data: { text: \"中心主题\" },\n  children: [],\n};\n\ntype MetadataOnlyMindmapCommand =\n  | Extract<MindmapKernelCommand, { type: \"setLayout\" | \"setTheme\" | \"patchView\" }>\n  | MindmapCompatPayloadPatch;\n\ntype LocallyApplicableMindmapCommand =\n  | Exclude<MindmapKernelCommand, { type: \"moveNode\" }>\n  | MindmapCompatPayloadPatch;\n\ntype ApplyMetadataOnlyMindmapCommandsResult = {\n  applied: number;\n  data: Record<string, unknown>;\n  errors: string[];\n};\n\ntype ApplyMindmapCommandsLocallyResult = ApplyMetadataOnlyMindmapCommandsResult;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;\n\nconst ensureMindmapBlob = (value: unknown): Record<string, unknown> => {\n  const next = isRecord(value) ? cloneJson(value) : cloneJson(defaultMindmapData);\n  if (!isRecord(next.data)) next.data = { text: \"中心主题\" };\n  if (!Array.isArray(next.children)) next.children = [];\n  return next;\n};\n\nconst mergeJsonObject = (base: unknown, patch: unknown): unknown => {\n  if (!isRecord(base)) return cloneJson(patch);\n  if (!isRecord(patch)) return cloneJson(patch);\n  const next = { ...base };\n  Object.entries(patch).forEach(([key, value]) => {\n    if (value === null) {\n      delete next[key];\n      return;\n    }\n    next[key] = isRecord(next[key]) && isRecord(value) ? mergeJsonObject(next[key], value) : cloneJson(value);\n  });\n  return next;\n};\n\nconst setNestedPath = (root: Record<string, unknown>, path: string, value: unknown): boolean => {\n  const segments = path.split(\".\").filter(Boolean);\n  if (segments.length === 0) return false;\n  let current: Record<string, unknown> = root;\n  for (const segment of segments.slice(0, -1)) {\n    if (!isRecord(current[segment])) current[segment] = {};\n    current = current[segment] as Record<string, unknown>;\n  }\n  current[segments[segments.length - 1]] = cloneJson(value);\n  return true;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.ts","s":49,"e":108,"m":1778506345309.2224,"t":"const setNestedPath = (root: Record<string, unknown>, path: string, value: unknown): boolean => {\n  const segments = path.split(\".\").filter(Boolean);\n  if (segments.length === 0) return false;\n  let current: Record<string, unknown> = root;\n  for (const segment of segments.slice(0, -1)) {\n    if (!isRecord(current[segment])) current[segment] = {};\n    current = current[segment] as Record<string, unknown>;\n  }\n  current[segments[segments.length - 1]] = cloneJson(value);\n  return true;\n};\n\nconst findMindmapNodeByUid = (node: unknown, uid: string): Record<string, unknown> | null => {\n  if (!isRecord(node)) return null;\n  const data = isRecord(node.data) ? node.data : null;\n  if (typeof data?.uid === \"string\" && data.uid === uid) return node;\n  const children = Array.isArray(node.children) ? node.children : [];\n  for (const child of children) {\n    const found = findMindmapNodeByUid(child, uid);\n    if (found) return found;\n  }\n  return null;\n};\n\nconst findMindmapNodeContainerByUid = (\n  nodes: unknown[],\n  uid: string,\n): { parentChildren: Record<string, unknown>[]; index: number } | null => {\n  for (let index = 0; index < nodes.length; index += 1) {\n    const candidate = nodes[index];\n    if (!isRecord(candidate)) continue;\n    const data = isRecord(candidate.data) ? candidate.data : null;\n    if (typeof data?.uid === \"string\" && data.uid === uid) {\n      return {\n        parentChildren: nodes as Record<string, unknown>[],\n        index,\n      };\n    }\n    const children = Array.isArray(candidate.children) ? candidate.children : [];\n    const found = findMindmapNodeContainerByUid(children, uid);\n    if (found) return found;\n  }\n  return null;\n};\n\nconst createMindmapNodeRecord = (node: {\n  uid?: string;\n  text: string;\n  hyperlink?: string;\n  note?: string;\n  refs?: unknown[];\n}): Record<string, unknown> => ({\n  data: {\n    uid: node.uid ?? `node_${Date.now().toString(36)}`,\n    text: node.text,\n    ...(typeof node.hyperlink === \"string\" ? { hyperlink: node.hyperlink } : {}),\n    ...(typeof node.note === \"string\" ? { note: node.note } : {}),\n    ...(Array.isArray(node.refs) ? { refs: cloneJson(node.refs) } : {}),\n  },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.ts","s":97,"e":156,"m":1778506345309.2224,"t":"text: string;\n  hyperlink?: string;\n  note?: string;\n  refs?: unknown[];\n}): Record<string, unknown> => ({\n  data: {\n    uid: node.uid ?? `node_${Date.now().toString(36)}`,\n    text: node.text,\n    ...(typeof node.hyperlink === \"string\" ? { hyperlink: node.hyperlink } : {}),\n    ...(typeof node.note === \"string\" ? { note: node.note } : {}),\n    ...(Array.isArray(node.refs) ? { refs: cloneJson(node.refs) } : {}),\n  },\n  children: [],\n});\n\nconst applyCompatPayloadPatch = (root: Record<string, unknown>, command: MindmapCompatPayloadPatch): boolean => {\n  const path = String(command.path || \"\").trim();\n  if (!path) return false;\n\n  if (path.startsWith(\"root.data.\")) {\n    const field = path.slice(\"root.data.\".length);\n    const rootData = isRecord(root.data) ? root.data : (root.data = {});\n    return setNestedPath(rootData as Record<string, unknown>, field, command.value);\n  }\n\n  if (path.startsWith(\"nodes.\")) {\n    const [, uid, scope, ...rest] = path.split(\".\");\n    if (!uid || scope !== \"data\" || rest.length === 0) return false;\n    const node = findMindmapNodeByUid(root, uid);\n    if (!node) return false;\n    const nodeData = isRecord(node.data) ? node.data : (node.data = {});\n    return setNestedPath(nodeData as Record<string, unknown>, rest.join(\".\"), command.value);\n  }\n\n  const compatPayload = isRecord(root.compatPayload) ? root.compatPayload : (root.compatPayload = {});\n  return setNestedPath(compatPayload as Record<string, unknown>, path, command.value);\n};\n\nexport const isMetadataOnlyMindmapCommandSet = (commands: unknown[]): commands is MetadataOnlyMindmapCommand[] =>\n  Array.isArray(commands) &&\n  commands.every((command) => {\n    if (!isRecord(command) || typeof command.type !== \"string\") return false;\n    return [\"setLayout\", \"setTheme\", \"patchView\", \"compatPayloadPatch\"].includes(command.type);\n  });\n\nexport const isLocallyApplicableMindmapCommandSet = (\n  commands: unknown[],\n): commands is LocallyApplicableMindmapCommand[] =>\n  Array.isArray(commands) &&\n  commands.every((command) => {\n    if (!isRecord(command) || typeof command.type !== \"string\") return false;\n    return [\n      \"updateText\",\n      \"insertChild\",\n      \"insertSiblingAfter\",\n      \"deleteNode\",\n      \"setLayout\",\n      \"setTheme\",\n      \"patchView\",\n      \"compatPayloadPatch\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.ts","s":145,"e":204,"m":1778506345309.2224,"t":"Array.isArray(commands) &&\n  commands.every((command) => {\n    if (!isRecord(command) || typeof command.type !== \"string\") return false;\n    return [\n      \"updateText\",\n      \"insertChild\",\n      \"insertSiblingAfter\",\n      \"deleteNode\",\n      \"setLayout\",\n      \"setTheme\",\n      \"patchView\",\n      \"compatPayloadPatch\",\n    ].includes(command.type);\n  });\n\nexport const applyMetadataOnlyMindmapCommands = (\n  currentData: unknown,\n  commands: MetadataOnlyMindmapCommand[],\n): ApplyMetadataOnlyMindmapCommandsResult => {\n  const nextData = ensureMindmapBlob(currentData);\n  const errors: string[] = [];\n  let applied = 0;\n\n  commands.forEach((command) => {\n    if (command.type === \"setLayout\") {\n      nextData.layout = cloneJson(command.layout);\n      applied += 1;\n      return;\n    }\n    if (command.type === \"setTheme\") {\n      nextData.theme = cloneJson(command.theme);\n      if (command.themeConfig !== undefined && command.themeConfig !== null) {\n        nextData.themeConfig = cloneJson(command.themeConfig);\n      }\n      applied += 1;\n      return;\n    }\n    if (command.type === \"patchView\") {\n      nextData.view = mergeJsonObject(nextData.view ?? {}, command.patch) as Record<string, unknown>;\n      applied += 1;\n      return;\n    }\n    if (command.type === \"compatPayloadPatch\") {\n      if (applyCompatPayloadPatch(nextData, command)) {\n        applied += 1;\n      } else {\n        errors.push(`无法应用 compatPayloadPatch:${command.path}`);\n      }\n    }\n  });\n\n  return {\n    applied,\n    data: nextData,\n    errors,\n  };\n};\n\nexport const applyMindmapCommandsLocally = (\n  currentData: unknown,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.ts","s":193,"e":252,"m":1778506345309.2224,"t":"}\n  });\n\n  return {\n    applied,\n    data: nextData,\n    errors,\n  };\n};\n\nexport const applyMindmapCommandsLocally = (\n  currentData: unknown,\n  commands: LocallyApplicableMindmapCommand[],\n): ApplyMindmapCommandsLocallyResult => {\n  const nextData = ensureMindmapBlob(currentData);\n  const errors: string[] = [];\n  let applied = 0;\n\n  commands.forEach((command) => {\n    if (command.type === \"setLayout\" || command.type === \"setTheme\" || command.type === \"patchView\" || command.type === \"compatPayloadPatch\") {\n      const result = applyMetadataOnlyMindmapCommands(nextData, [command]);\n      Object.assign(nextData, result.data);\n      applied += result.applied;\n      errors.push(...result.errors);\n      return;\n    }\n\n    if (command.type === \"updateText\") {\n      const node = findMindmapNodeByUid(nextData, command.nodeId);\n      if (!node) {\n        errors.push(`未找到节点:${command.nodeId}`);\n        return;\n      }\n      const data = isRecord(node.data) ? node.data : (node.data = {});\n      data.text = command.text;\n      applied += 1;\n      return;\n    }\n\n    if (command.type === \"insertChild\") {\n      const parent = findMindmapNodeByUid(nextData, command.parentNodeId);\n      if (!parent) {\n        errors.push(`未找到父节点:${command.parentNodeId}`);\n        return;\n      }\n      const children = Array.isArray(parent.children) ? parent.children : (parent.children = []);\n      children.push(createMindmapNodeRecord(command.node));\n      applied += 1;\n      return;\n    }\n\n    if (command.type === \"insertSiblingAfter\") {\n      const container = findMindmapNodeContainerByUid([nextData], command.targetNodeId);\n      if (!container) {\n        errors.push(`未找到同级节点:${command.targetNodeId}`);\n        return;\n      }\n      if (container.parentChildren[container.index] === nextData) {\n        errors.push(\"根节点不支持插入同级节点\");\n        return;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-apply-local.ts","s":241,"e":280,"m":1778506345309.2224,"t":"return;\n    }\n\n    if (command.type === \"insertSiblingAfter\") {\n      const container = findMindmapNodeContainerByUid([nextData], command.targetNodeId);\n      if (!container) {\n        errors.push(`未找到同级节点:${command.targetNodeId}`);\n        return;\n      }\n      if (container.parentChildren[container.index] === nextData) {\n        errors.push(\"根节点不支持插入同级节点\");\n        return;\n      }\n      container.parentChildren.splice(container.index + 1, 0, createMindmapNodeRecord(command.node));\n      applied += 1;\n      return;\n    }\n\n    if (command.type === \"deleteNode\") {\n      const container = findMindmapNodeContainerByUid([nextData], command.nodeId);\n      if (!container) {\n        errors.push(`未找到删除节点:${command.nodeId}`);\n        return;\n      }\n      if (container.parentChildren[container.index] === nextData) {\n        errors.push(\"根节点不支持删除\");\n        return;\n      }\n      container.parentChildren.splice(container.index, 1);\n      applied += 1;\n    }\n  });\n\n  return {\n    applied,\n    data: nextData,\n    errors,\n  };\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.test.ts","s":1,"e":60,"m":1778469876413.0076,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  createCompatPayloadPatch,\n  createLayoutCommand,\n  createThemeCommand,\n  createToolbarKernelCommand,\n  createViewPatchCommand,\n  diffMindmapRuntimeDataToKernelCommands,\n} from \"./mindmap-command-diff\";\n\ndescribe(\"mindmap command bridge mapping\", () => {\n  it(\"把明确 toolbar 动作映射到 kernel command\", () => {\n    expect(\n      createToolbarKernelCommand({\n        mindmapId: \"mind_1\",\n        runtimeCommand: \"INSERT_CHILD_NODE\",\n        activeNodeId: \"node_1\",\n      }),\n    ).toEqual({\n      type: \"insertChild\",\n      mindmapId: \"mind_1\",\n      parentNodeId: \"node_1\",\n      node: { text: \"新节点\" },\n    });\n\n    expect(\n      createToolbarKernelCommand({\n        mindmapId: \"mind_1\",\n        runtimeCommand: \"REMOVE_NODE\",\n        activeNodeId: \"node_2\",\n      }),\n    ).toEqual({\n      type: \"deleteNode\",\n      mindmapId: \"mind_1\",\n      nodeId: \"node_2\",\n    });\n  });\n\n  it(\"把暂未语义化的 toolbar 动作落入 compatPayload patch\", () => {\n    expect(\n      createToolbarKernelCommand({\n        mindmapId: \"mind_1\",\n        runtimeCommand: \"ADD_GENERALIZATION\",\n        activeNodeId: \"node_1\",\n      }),\n    ).toEqual({\n      type: \"compatPayloadPatch\",\n      mindmapId: \"mind_1\",\n      path: \"root.data.generalization\",\n      value: { text: \"概要\" },\n      source: \"toolbar\",\n    });\n  });\n\n  it(\"构建 view/theme/layout 和通用 compat patch command\", () => {\n    expect(createViewPatchCommand(\"mind_1\", { scale: 0.9 })).toEqual({\n      type: \"patchView\",\n      mindmapId: \"mind_1\",\n      patch: { scale: 0.9 },\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.test.ts","s":49,"e":108,"m":1778469876413.0076,"t":"path: \"root.data.generalization\",\n      value: { text: \"概要\" },\n      source: \"toolbar\",\n    });\n  });\n\n  it(\"构建 view/theme/layout 和通用 compat patch command\", () => {\n    expect(createViewPatchCommand(\"mind_1\", { scale: 0.9 })).toEqual({\n      type: \"patchView\",\n      mindmapId: \"mind_1\",\n      patch: { scale: 0.9 },\n    });\n    expect(createThemeCommand(\"mind_1\", \"classic4\", { root: true })).toEqual({\n      type: \"setTheme\",\n      mindmapId: \"mind_1\",\n      theme: \"classic4\",\n      themeConfig: { root: true },\n    });\n    expect(createLayoutCommand(\"mind_1\", \"logicalStructure\")).toEqual({\n      type: \"setLayout\",\n      mindmapId: \"mind_1\",\n      layout: \"logicalStructure\",\n    });\n    expect(\n      createCompatPayloadPatch({\n        mindmapId: \"mind_1\",\n        path: \"root.data.image\",\n        value: \"asset:img_1\",\n      source: \"sidebar\",\n      actionId: \"image\",\n      runtimeRevision: 7,\n      kernelRevision: 3,\n    }),\n  ).toEqual({\n      type: \"compatPayloadPatch\",\n      mindmapId: \"mind_1\",\n      path: \"root.data.image\",\n      value: \"asset:img_1\",\n      source: \"sidebar\",\n      actionId: \"image\",\n      runtimeRevision: 7,\n      kernelRevision: 3,\n    });\n  });\n\n  it(\"从 data_change diff 识别文本、插入、删除、移动和 compat 字段\", () => {\n    const previous = {\n      data: { uid: \"root\", text: \"KMIND\" },\n      children: [\n        { data: { uid: \"a\", text: \"A\", image: \"asset:old\" }, children: [] },\n        { data: { uid: \"b\", text: \"B\" }, children: [] },\n      ],\n    };\n    const next = {\n      data: { uid: \"root\", text: \"KMIND\" },\n      children: [\n        {\n          data: { uid: \"a\", text: \"A2\", image: \"asset:new\" },\n          children: [{ data: { uid: \"c\", text: \"C\" }, children: [] }],\n        },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.test.ts","s":97,"e":156,"m":1778469876413.0076,"t":"children: [\n        { data: { uid: \"a\", text: \"A\", image: \"asset:old\" }, children: [] },\n        { data: { uid: \"b\", text: \"B\" }, children: [] },\n      ],\n    };\n    const next = {\n      data: { uid: \"root\", text: \"KMIND\" },\n      children: [\n        {\n          data: { uid: \"a\", text: \"A2\", image: \"asset:new\" },\n          children: [{ data: { uid: \"c\", text: \"C\" }, children: [] }],\n        },\n      ],\n    };\n\n    expect(\n      diffMindmapRuntimeDataToKernelCommands({\n        mindmapId: \"mind_1\",\n        previous,\n        next,\n      }),\n    ).toEqual({\n      commands: [\n        { type: \"updateText\", mindmapId: \"mind_1\", nodeId: \"a\", text: \"A2\" },\n        {\n          type: \"insertChild\",\n          mindmapId: \"mind_1\",\n          parentNodeId: \"a\",\n          node: { uid: \"c\", text: \"C\" },\n        },\n        { type: \"deleteNode\", mindmapId: \"mind_1\", nodeId: \"b\" },\n      ],\n      compatPatches: [\n        {\n          type: \"compatPayloadPatch\",\n          mindmapId: \"mind_1\",\n          path: \"nodes.a.data.image\",\n          value: \"asset:new\",\n          source: \"adapter-diff\",\n        },\n      ],\n    });\n  });\n\n  it(\"从 data_change diff 识别节点移动\", () => {\n    const previous = {\n      data: { uid: \"root\", text: \"KMIND\" },\n      children: [\n        { data: { uid: \"a\", text: \"A\" }, children: [] },\n        { data: { uid: \"b\", text: \"B\" }, children: [] },\n      ],\n    };\n    const next = {\n      data: { uid: \"root\", text: \"KMIND\" },\n      children: [{ data: { uid: \"a\", text: \"A\" }, children: [{ data: { uid: \"b\", text: \"B\" }, children: [] }] }],\n    };\n\n    expect(\n      diffMindmapRuntimeDataToKernelCommands({\n        mindmapId: \"mind_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.test.ts","s":145,"e":169,"m":1778469876413.0076,"t":"{ data: { uid: \"a\", text: \"A\" }, children: [] },\n        { data: { uid: \"b\", text: \"B\" }, children: [] },\n      ],\n    };\n    const next = {\n      data: { uid: \"root\", text: \"KMIND\" },\n      children: [{ data: { uid: \"a\", text: \"A\" }, children: [{ data: { uid: \"b\", text: \"B\" }, children: [] }] }],\n    };\n\n    expect(\n      diffMindmapRuntimeDataToKernelCommands({\n        mindmapId: \"mind_1\",\n        previous,\n        next,\n      }).commands,\n    ).toContainEqual({\n      type: \"moveNode\",\n      mindmapId: \"mind_1\",\n      nodeId: \"b\",\n      newParentNodeId: \"a\",\n      order: 0,\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.ts","s":1,"e":60,"m":1778469876413.0076,"t":"import { canonicalizeMindmapData, type MindMapData } from \"./mindmap-projection\";\n\nexport type MindmapNodeInput = {\n  uid?: string;\n  text: string;\n  hyperlink?: string;\n  note?: string;\n  refs?: unknown[];\n};\n\nexport type MindmapKernelCommand =\n  | { type: \"updateText\"; mindmapId: string; nodeId: string; text: string }\n  | { type: \"insertChild\"; mindmapId: string; parentNodeId: string; node: MindmapNodeInput }\n  | { type: \"insertSiblingAfter\"; mindmapId: string; targetNodeId: string; node: MindmapNodeInput }\n  | { type: \"deleteNode\"; mindmapId: string; nodeId: string }\n  | { type: \"moveNode\"; mindmapId: string; nodeId: string; newParentNodeId: string; order?: number }\n  | { type: \"patchView\"; mindmapId: string; patch: Record<string, unknown> }\n  | { type: \"setLayout\"; mindmapId: string; layout: unknown }\n  | { type: \"setTheme\"; mindmapId: string; theme: unknown; themeConfig?: unknown };\n\nexport type MindmapCompatPayloadPatch = {\n  type: \"compatPayloadPatch\";\n  mindmapId: string;\n  path: string;\n  value: unknown;\n  source: \"toolbar\" | \"sidebar\" | \"runtime-event\" | \"adapter-diff\";\n  actionId?: string;\n  runtimeRevision?: number | null;\n  kernelRevision?: number | null;\n};\n\nexport type MindmapCommandDiffResult = {\n  commands: MindmapKernelCommand[];\n  compatPatches: MindmapCompatPayloadPatch[];\n};\n\nexport type ToolbarCommandInput = {\n  mindmapId: string;\n  runtimeCommand: string;\n  activeNodeId?: string | null;\n  newNode?: MindmapNodeInput;\n  layout?: unknown;\n  theme?: unknown;\n  themeConfig?: unknown;\n};\n\nconst defaultNode = (node?: MindmapNodeInput): MindmapNodeInput => ({\n  text: node?.text?.trim() || \"新节点\",\n  ...(node?.uid ? { uid: node.uid } : {}),\n  ...(node?.hyperlink ? { hyperlink: node.hyperlink } : {}),\n  ...(node?.note ? { note: node.note } : {}),\n  ...(node?.refs ? { refs: node.refs } : {}),\n});\n\nexport const createToolbarKernelCommand = (\n  input: ToolbarCommandInput,\n): MindmapKernelCommand | MindmapCompatPayloadPatch | null => {\n  const activeNodeId = input.activeNodeId?.trim();\n  switch (input.runtimeCommand) {\n    case \"INSERT_CHILD_NODE\":","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.ts","s":49,"e":108,"m":1778469876413.0076,"t":"...(node?.uid ? { uid: node.uid } : {}),\n  ...(node?.hyperlink ? { hyperlink: node.hyperlink } : {}),\n  ...(node?.note ? { note: node.note } : {}),\n  ...(node?.refs ? { refs: node.refs } : {}),\n});\n\nexport const createToolbarKernelCommand = (\n  input: ToolbarCommandInput,\n): MindmapKernelCommand | MindmapCompatPayloadPatch | null => {\n  const activeNodeId = input.activeNodeId?.trim();\n  switch (input.runtimeCommand) {\n    case \"INSERT_CHILD_NODE\":\n      if (!activeNodeId) return null;\n      return {\n        type: \"insertChild\",\n        mindmapId: input.mindmapId,\n        parentNodeId: activeNodeId,\n        node: defaultNode(input.newNode),\n      };\n    case \"INSERT_NODE\":\n      if (!activeNodeId) return null;\n      return {\n        type: \"insertSiblingAfter\",\n        mindmapId: input.mindmapId,\n        targetNodeId: activeNodeId,\n        node: defaultNode(input.newNode),\n      };\n    case \"REMOVE_NODE\":\n    case \"DELETE_NODE\":\n      if (!activeNodeId) return null;\n      return { type: \"deleteNode\", mindmapId: input.mindmapId, nodeId: activeNodeId };\n    case \"ADD_GENERALIZATION\":\n      return {\n        type: \"compatPayloadPatch\",\n        mindmapId: input.mindmapId,\n        path: \"root.data.generalization\",\n        value: { text: \"概要\" },\n        source: \"toolbar\",\n      };\n    case \"ADD_OUTER_FRAME\":\n      return {\n        type: \"compatPayloadPatch\",\n        mindmapId: input.mindmapId,\n        path: \"outerFrame\",\n        value: { enabled: true, activeNodeId },\n        source: \"toolbar\",\n      };\n    default:\n      return null;\n  }\n};\n\nexport const createViewPatchCommand = (\n  mindmapId: string,\n  patch: Record<string, unknown>,\n): MindmapKernelCommand => ({\n  type: \"patchView\",\n  mindmapId,\n  patch,\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.ts","s":97,"e":156,"m":1778469876413.0076,"t":"return null;\n  }\n};\n\nexport const createViewPatchCommand = (\n  mindmapId: string,\n  patch: Record<string, unknown>,\n): MindmapKernelCommand => ({\n  type: \"patchView\",\n  mindmapId,\n  patch,\n});\n\nexport const createThemeCommand = (\n  mindmapId: string,\n  theme: unknown,\n  themeConfig?: unknown,\n): MindmapKernelCommand => ({\n  type: \"setTheme\",\n  mindmapId,\n  theme,\n  themeConfig,\n});\n\nexport const createLayoutCommand = (mindmapId: string, layout: unknown): MindmapKernelCommand => ({\n  type: \"setLayout\",\n  mindmapId,\n  layout,\n});\n\nexport const createCompatPayloadPatch = (input: {\n  mindmapId: string;\n  path: string;\n  value: unknown;\n  source: MindmapCompatPayloadPatch[\"source\"];\n  actionId?: string;\n  runtimeRevision?: number | null;\n  kernelRevision?: number | null;\n}): MindmapCompatPayloadPatch => ({\n  type: \"compatPayloadPatch\",\n  mindmapId: input.mindmapId,\n  path: input.path,\n  value: input.value,\n  source: input.source,\n  ...(input.actionId ? { actionId: input.actionId } : {}),\n  ...(typeof input.runtimeRevision === \"number\" ? { runtimeRevision: input.runtimeRevision } : {}),\n  ...(typeof input.kernelRevision === \"number\" ? { kernelRevision: input.kernelRevision } : {}),\n});\n\ntype IndexedRuntimeNode = {\n  uid: string;\n  parentUid: string | null;\n  order: number;\n  text: string;\n  data: Record<string, unknown>;\n  node: MindMapData;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.ts","s":145,"e":204,"m":1778469876413.0076,"t":"type IndexedRuntimeNode = {\n  uid: string;\n  parentUid: string | null;\n  order: number;\n  text: string;\n  data: Record<string, unknown>;\n  node: MindMapData;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst readNodeUid = (node: MindMapData, fallback: string): string => {\n  const uid = node.data?.uid;\n  return typeof uid === \"string\" && uid.trim() ? uid.trim() : fallback;\n};\n\nconst readNodeText = (node: MindMapData): string => {\n  const text = node.data?.text;\n  return typeof text === \"string\" ? text : String(text ?? \"\");\n};\n\nconst indexMindmapRuntimeTree = (input: unknown): Map<string, IndexedRuntimeNode> => {\n  const root = canonicalizeMindmapData(input);\n  const index = new Map<string, IndexedRuntimeNode>();\n  const visit = (node: MindMapData, parentUid: string | null, order: number, path: string) => {\n    const uid = readNodeUid(node, path);\n    const data = isRecord(node.data) ? node.data : {};\n    index.set(uid, {\n      uid,\n      parentUid,\n      order,\n      text: readNodeText(node),\n      data,\n      node,\n    });\n    const children = Array.isArray(node.children) ? node.children : [];\n    children.forEach((child, childIndex) => {\n      visit(canonicalizeMindmapData(child), uid, childIndex, `${path}.${childIndex}`);\n    });\n  };\n  visit(root, null, 0, \"root\");\n  return index;\n};\n\nconst serializeComparable = (value: unknown): string => JSON.stringify(value ?? null);\n\nconst buildNodeInputFromRuntimeNode = (node: IndexedRuntimeNode): MindmapNodeInput => ({\n  uid: node.uid,\n  text: node.text || \"新节点\",\n  ...(typeof node.data.hyperlink === \"string\" ? { hyperlink: node.data.hyperlink } : {}),\n  ...(typeof node.data.note === \"string\" ? { note: node.data.note } : {}),\n  ...(Array.isArray(node.data.refs) ? { refs: node.data.refs } : {}),\n});\n\nconst pushCompatDataPatches = (\n  mindmapId: string,\n  prev: IndexedRuntimeNode,\n  next: IndexedRuntimeNode,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.ts","s":193,"e":252,"m":1778469876413.0076,"t":"const buildNodeInputFromRuntimeNode = (node: IndexedRuntimeNode): MindmapNodeInput => ({\n  uid: node.uid,\n  text: node.text || \"新节点\",\n  ...(typeof node.data.hyperlink === \"string\" ? { hyperlink: node.data.hyperlink } : {}),\n  ...(typeof node.data.note === \"string\" ? { note: node.data.note } : {}),\n  ...(Array.isArray(node.data.refs) ? { refs: node.data.refs } : {}),\n});\n\nconst pushCompatDataPatches = (\n  mindmapId: string,\n  prev: IndexedRuntimeNode,\n  next: IndexedRuntimeNode,\n  compatPatches: MindmapCompatPayloadPatch[],\n) => {\n  const semanticKeys = new Set([\"uid\", \"text\", \"refs\"]);\n  const keys = new Set([...Object.keys(prev.data), ...Object.keys(next.data)]);\n  keys.forEach((key) => {\n    if (semanticKeys.has(key)) return;\n    if (serializeComparable(prev.data[key]) === serializeComparable(next.data[key])) return;\n    compatPatches.push(\n      createCompatPayloadPatch({\n        mindmapId,\n        path: `nodes.${next.uid}.data.${key}`,\n        value: next.data[key],\n        source: \"adapter-diff\",\n      }),\n    );\n  });\n};\n\nexport const diffMindmapRuntimeDataToKernelCommands = (input: {\n  mindmapId: string;\n  previous: unknown;\n  next: unknown;\n}): MindmapCommandDiffResult => {\n  const previousIndex = indexMindmapRuntimeTree(input.previous);\n  const nextIndex = indexMindmapRuntimeTree(input.next);\n  const commands: MindmapKernelCommand[] = [];\n  const compatPatches: MindmapCompatPayloadPatch[] = [];\n\n  nextIndex.forEach((nextNode, uid) => {\n    const previousNode = previousIndex.get(uid);\n    if (!previousNode) {\n      const previousSibling = Array.from(nextIndex.values()).find(\n        (candidate) =>\n          candidate.parentUid === nextNode.parentUid &&\n          candidate.order === nextNode.order - 1 &&\n          previousIndex.has(candidate.uid),\n      );\n      if (previousSibling) {\n        commands.push({\n          type: \"insertSiblingAfter\",\n          mindmapId: input.mindmapId,\n          targetNodeId: previousSibling.uid,\n          node: buildNodeInputFromRuntimeNode(nextNode),\n        });\n      } else if (nextNode.parentUid) {\n        commands.push({\n          type: \"insertChild\",\n          mindmapId: input.mindmapId,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-command-diff.ts","s":241,"e":293,"m":1778469876413.0076,"t":");\n      if (previousSibling) {\n        commands.push({\n          type: \"insertSiblingAfter\",\n          mindmapId: input.mindmapId,\n          targetNodeId: previousSibling.uid,\n          node: buildNodeInputFromRuntimeNode(nextNode),\n        });\n      } else if (nextNode.parentUid) {\n        commands.push({\n          type: \"insertChild\",\n          mindmapId: input.mindmapId,\n          parentNodeId: nextNode.parentUid,\n          node: buildNodeInputFromRuntimeNode(nextNode),\n        });\n      }\n      return;\n    }\n\n    if (previousNode.text !== nextNode.text) {\n      commands.push({\n        type: \"updateText\",\n        mindmapId: input.mindmapId,\n        nodeId: uid,\n        text: nextNode.text,\n      });\n    }\n\n    if (\n      previousNode.parentUid !== nextNode.parentUid ||\n      previousNode.order !== nextNode.order\n    ) {\n      commands.push({\n        type: \"moveNode\",\n        mindmapId: input.mindmapId,\n        nodeId: uid,\n        newParentNodeId: nextNode.parentUid ?? \"\",\n        order: nextNode.order,\n      });\n    }\n\n    pushCompatDataPatches(input.mindmapId, previousNode, nextNode, compatPatches);\n  });\n\n  previousIndex.forEach((previousNode, uid) => {\n    if (!nextIndex.has(uid) && previousNode.parentUid) {\n      commands.push({ type: \"deleteNode\", mindmapId: input.mindmapId, nodeId: uid });\n    }\n  });\n\n  return { commands, compatPatches };\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-initial-sync.test.ts","s":1,"e":31,"m":1778540603961.099,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildInitialMindmapAssetRefreshPayload } from \"./mindmap-initial-sync\";\n\ndescribe(\"buildInitialMindmapAssetRefreshPayload\", () => {\n  it(\"远端 createOnly 成功时返回文件树刷新 payload\", () => {\n    expect(\n      buildInitialMindmapAssetRefreshPayload({\n        ok: true,\n        docId: \"doc-1\",\n        mindmapId: \"mind-1\",\n      }),\n    ).toEqual({\n      id: \"mind-1\",\n      document_id: \"doc-1\",\n      asset_type: \"mindmap\",\n      file_name: \"mindmap-mind-1.json\",\n      file_url: \"/documents/doc-1/mindmap-mind-1.json\",\n    });\n  });\n\n  it(\"远端 createOnly 失败时不应伪造文件树刷新事件\", () => {\n    expect(\n      buildInitialMindmapAssetRefreshPayload({\n        ok: false,\n        docId: \"doc-1\",\n        mindmapId: \"mind-1\",\n      }),\n    ).toBeNull();\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-initial-sync.ts","s":1,"e":26,"m":1778540730992.721,"t":"export type InitialMindmapAssetRefreshPayload = {\n  id: string;\n  document_id: string;\n  asset_type: \"mindmap\";\n  file_name: string;\n  file_url: string;\n};\n\nexport function buildInitialMindmapAssetRefreshPayload(input: {\n  ok: boolean;\n  docId: string;\n  mindmapId: string;\n}): InitialMindmapAssetRefreshPayload | null {\n  if (!input.ok) {\n    return null;\n  }\n  const fileName = `mindmap-${input.mindmapId}.json`;\n  return {\n    id: input.mindmapId,\n    document_id: input.docId,\n    asset_type: \"mindmap\",\n    file_name: fileName,\n    file_url: `/documents/${input.docId}/${fileName}`,\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":1,"e":60,"m":1778497748182.916,"t":"// 说明：这里保留的是 legacy/blob compat projection 工具，服务导图导入、\n// 旧 React mindmap 参考实现和独立页兼容。Phase 6 文档页默认编辑主链\n// 使用 Rust kernel projection 与 simple-mind-map adapter projection。\nexport type MindMapData = {\n  data: Record<string, unknown>;\n  children?: unknown[];\n};\n\nexport type MindmapRouteMeta = {\n  requestId?: string;\n  traceId?: string;\n  workspaceId?: string | null;\n  documentId?: string;\n  pageId?: string;\n  mindmapId?: string;\n  attachmentId?: string;\n  updatedAt?: string | null;\n};\n\nexport type MindmapProjectionNode = {\n  uid: string;\n  text: string;\n  depth: number;\n  childCount: number;\n};\n\nexport type MindmapProjection = {\n  schema: \"mnote.mindmap_projection.v1\";\n  projectionId: string;\n  projection: \"mindmap_subtree\";\n  source: string;\n  owner: string;\n  documentId: string;\n  mindmapId: string;\n  rootNodeId: string | null;\n  title: string;\n  nodeCount: number;\n  nodes: MindmapProjectionNode[];\n  data: MindMapData;\n  meta: MindmapRouteMeta | null;\n};\n\nexport type MindmapEditorSceneNode = MindmapProjectionNode & {\n  id: string;\n  parentId: string | null;\n};\n\nexport type MindmapEditorSceneEdge = {\n  id: string;\n  source: string;\n  target: string;\n};\n\nexport type MindmapEditorScene = {\n  schema: \"mnote.mindmap_editor_scene.v1\";\n  source: string;\n  owner: string;\n  documentId: string;\n  mindmapId: string;\n  rootNodeId: string | null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":49,"e":108,"m":1778497748182.916,"t":"id: string;\n  source: string;\n  target: string;\n};\n\nexport type MindmapEditorScene = {\n  schema: \"mnote.mindmap_editor_scene.v1\";\n  source: string;\n  owner: string;\n  documentId: string;\n  mindmapId: string;\n  rootNodeId: string | null;\n  title: string;\n  nodes: MindmapEditorSceneNode[];\n  edges: MindmapEditorSceneEdge[];\n  capabilities: {\n    canEditText: boolean;\n    canAddChild: boolean;\n    canAddSiblingAfter: boolean;\n    canDeleteNode: boolean;\n  };\n  data: MindMapData;\n  meta: MindmapRouteMeta | null;\n};\n\nexport type MindmapSimpleMindMapScene = {\n  schema: \"mnote.mindmap.simple_mind_map_scene.v1\";\n  runtime: \"simple-mind-map\";\n  documentId: string;\n  mindmapId: string;\n  rootNodeId: string | null;\n  root: MindMapData;\n  layout: unknown;\n  theme: unknown;\n  themeConfig: unknown;\n  view: unknown;\n  config: unknown;\n  compatPayload: unknown;\n  kernelRevision: number;\n  source: string;\n  owner: string;\n  meta: MindmapRouteMeta | null;\n};\n\nexport const defaultMindmapData: MindMapData = {\n  data: { text: \"中心主题\" },\n  children: [],\n};\n\nexport const DEFAULT_MINDMAP_LAYOUT = \"logicalStructure\";\nexport const DEFAULT_MINDMAP_THEME = \"default\";\n\nexport const createDefaultMindmapThemeConfig = (): Record<string, unknown> => ({\n  lineColor: \"#7aa2ff\",\n  lineStyle: \"curve\",\n  rootLineKeepSameInCurve: true,\n  rootLineStartPositionKeepSameInCurve: true,\n  generalizationLineColor: \"#ef6a5b\",\n  backgroundColor: \"#f6f8fc\",\n  root: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":97,"e":156,"m":1778497748182.916,"t":"export const DEFAULT_MINDMAP_LAYOUT = \"logicalStructure\";\nexport const DEFAULT_MINDMAP_THEME = \"default\";\n\nexport const createDefaultMindmapThemeConfig = (): Record<string, unknown> => ({\n  lineColor: \"#7aa2ff\",\n  lineStyle: \"curve\",\n  rootLineKeepSameInCurve: true,\n  rootLineStartPositionKeepSameInCurve: true,\n  generalizationLineColor: \"#ef6a5b\",\n  backgroundColor: \"#f6f8fc\",\n  root: {\n    fillColor: \"#e25563\",\n    color: \"#ffffff\",\n    fontWeight: \"bold\",\n    borderColor: \"transparent\",\n    borderWidth: 0,\n    borderRadius: 8,\n  },\n  second: {\n    fillColor: \"#4f7df3\",\n    color: \"#ffffff\",\n    borderColor: \"transparent\",\n    borderWidth: 0,\n    borderRadius: 8,\n  },\n  node: {\n    fillColor: \"transparent\",\n    color: \"#315aa9\",\n    borderColor: \"transparent\",\n    borderWidth: 0,\n  },\n  generalization: {\n    fillColor: \"#ffffff\",\n    color: \"#ef6a5b\",\n    borderColor: \"#ef6a5b\",\n    borderWidth: 1,\n    borderRadius: 8,\n  },\n});\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n// simple-mind-map 在文本字段缺失时会直接崩溃，这里统一做兜底归一化。\nexport const normalizeMindmapData = (input: unknown): unknown => {\n  if (!input || typeof input !== \"object\") return defaultMindmapData;\n  const root = (input as { root?: unknown }).root ?? input;\n\n  const walk = (node: unknown) => {\n    if (!isRecord(node)) return;\n    const data = isRecord(node.data) ? node.data : {};\n    node.data = data;\n    const rawText = data.text;\n    data.text = typeof rawText === \"string\" ? rawText : String(rawText ?? \"\");\n\n    const gen = data.generalization;\n    const fixGen = (value: unknown) => {\n      if (!isRecord(value)) return;\n      const text = value.text;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":145,"e":204,"m":1778497748182.916,"t":"const walk = (node: unknown) => {\n    if (!isRecord(node)) return;\n    const data = isRecord(node.data) ? node.data : {};\n    node.data = data;\n    const rawText = data.text;\n    data.text = typeof rawText === \"string\" ? rawText : String(rawText ?? \"\");\n\n    const gen = data.generalization;\n    const fixGen = (value: unknown) => {\n      if (!isRecord(value)) return;\n      const text = value.text;\n      value.text = typeof text === \"string\" ? text : String(text ?? \"\");\n    };\n\n    if (Array.isArray(gen)) gen.forEach(fixGen);\n    else fixGen(gen);\n\n    if (Array.isArray(node.children)) {\n      node.children.forEach(walk);\n    }\n  };\n\n  walk(root);\n  return input;\n};\n\n// 持久化/初始化统一使用根节点对象，避免 wrapper 误传给 simple-mind-map。\nexport const canonicalizeMindmapData = (input: unknown): MindMapData => {\n  const normalized = (normalizeMindmapData(input) ?? defaultMindmapData) as Record<string, unknown>;\n  const root =\n    normalized && typeof normalized === \"object\" && \"root\" in normalized\n      ? normalized.root\n      : normalized;\n  return (normalizeMindmapData(root) ?? defaultMindmapData) as MindMapData;\n};\n\nexport const extractMindmapTitle = (input: unknown): string => {\n  const canonical = canonicalizeMindmapData(input);\n  const text = canonical?.data?.text;\n  if (typeof text === \"string\" && text.trim()) {\n    return text.trim();\n  }\n  return \"未命名导图\";\n};\n\nexport const summarizeMindmapProjectionNodes = (input: unknown): MindmapProjectionNode[] => {\n  const root = canonicalizeMindmapData(input);\n  const queue: Array<{ node: MindMapData; depth: number }> = [{ node: root, depth: 0 }];\n  const nodes: MindmapProjectionNode[] = [];\n\n  while (queue.length > 0) {\n    const current = queue.shift();\n    if (!current) break;\n    const uidRaw = current.node?.data?.uid;\n    const textRaw = current.node?.data?.text;\n    const children = Array.isArray(current.node?.children) ? current.node.children : [];\n    nodes.push({\n      uid:\n        typeof uidRaw === \"string\" && uidRaw.trim()","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":193,"e":252,"m":1778497748182.916,"t":"const queue: Array<{ node: MindMapData; depth: number }> = [{ node: root, depth: 0 }];\n  const nodes: MindmapProjectionNode[] = [];\n\n  while (queue.length > 0) {\n    const current = queue.shift();\n    if (!current) break;\n    const uidRaw = current.node?.data?.uid;\n    const textRaw = current.node?.data?.text;\n    const children = Array.isArray(current.node?.children) ? current.node.children : [];\n    nodes.push({\n      uid:\n        typeof uidRaw === \"string\" && uidRaw.trim()\n          ? uidRaw\n          : `depth:${current.depth}:index:${nodes.length}`,\n      text: typeof textRaw === \"string\" && textRaw.trim() ? textRaw.trim() : \"未命名节点\",\n      depth: current.depth,\n      childCount: children.length,\n    });\n    children.forEach((child) => {\n      queue.push({\n        node: canonicalizeMindmapData(child),\n        depth: current.depth + 1,\n      });\n    });\n  }\n\n  return nodes;\n};\n\nexport const buildMindmapProjection = (input: {\n  documentId: string;\n  mindmapId: string;\n  data: unknown;\n  source?: string;\n  owner?: string;\n  meta?: unknown;\n}): MindmapProjection => {\n  const data = canonicalizeMindmapData(input.data);\n  const nodes = summarizeMindmapProjectionNodes(data);\n  const meta = isRecord(input.meta) ? (input.meta as MindmapRouteMeta) : null;\n  const rootNodeId = nodes[0]?.uid ?? null;\n\n  return {\n    schema: \"mnote.mindmap_projection.v1\",\n    projectionId: `mindmap_projection:${input.documentId}:${input.mindmapId}`,\n    projection: \"mindmap_subtree\",\n    source: input.source ?? \"rust-kernel\",\n    owner: input.owner ?? \"rust-kernel\",\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    rootNodeId,\n    title: extractMindmapTitle(data),\n    nodeCount: nodes.length,\n    nodes,\n    data,\n    meta,\n  };\n};\n\nexport const buildMindmapEditorScene = (input: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":241,"e":300,"m":1778497748182.916,"t":"documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    rootNodeId,\n    title: extractMindmapTitle(data),\n    nodeCount: nodes.length,\n    nodes,\n    data,\n    meta,\n  };\n};\n\nexport const buildMindmapEditorScene = (input: {\n  documentId: string;\n  mindmapId: string;\n  rootNodeId?: string | null;\n  data: unknown;\n  source?: string;\n  owner?: string;\n  meta?: unknown;\n}): MindmapEditorScene => {\n  const data = canonicalizeMindmapData(input.data);\n  const meta = isRecord(input.meta) ? (input.meta as MindmapRouteMeta) : null;\n  const nodes: MindmapEditorSceneNode[] = [];\n  const edges: MindmapEditorSceneEdge[] = [];\n\n  const walk = (node: MindMapData, depth: number, parentId: string | null) => {\n    const uidRaw = node?.data?.uid;\n    const textRaw = node?.data?.text;\n    const id =\n      typeof uidRaw === \"string\" && uidRaw.trim()\n        ? uidRaw.trim()\n        : `depth:${depth}:index:${nodes.length}`;\n    const children = Array.isArray(node?.children) ? node.children : [];\n    nodes.push({\n      id,\n      uid: id,\n      text: typeof textRaw === \"string\" && textRaw.trim() ? textRaw.trim() : \"未命名节点\",\n      depth,\n      childCount: children.length,\n      parentId,\n    });\n    if (parentId) {\n      edges.push({\n        id: `${parentId}->${id}`,\n        source: parentId,\n        target: id,\n      });\n    }\n    children.forEach((child) => {\n      walk(canonicalizeMindmapData(child), depth + 1, id);\n    });\n  };\n\n  walk(data, 0, null);\n  const fallbackRootId = nodes[0]?.id ?? null;\n  const rootNodeId = input.rootNodeId && input.rootNodeId.trim() ? input.rootNodeId.trim() : fallbackRootId;\n\n  return {\n    schema: \"mnote.mindmap_editor_scene.v1\",\n    source: input.source ?? \"rust-kernel\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":289,"e":348,"m":1778497748182.916,"t":"children.forEach((child) => {\n      walk(canonicalizeMindmapData(child), depth + 1, id);\n    });\n  };\n\n  walk(data, 0, null);\n  const fallbackRootId = nodes[0]?.id ?? null;\n  const rootNodeId = input.rootNodeId && input.rootNodeId.trim() ? input.rootNodeId.trim() : fallbackRootId;\n\n  return {\n    schema: \"mnote.mindmap_editor_scene.v1\",\n    source: input.source ?? \"rust-kernel\",\n    owner: input.owner ?? \"rust-kernel\",\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    rootNodeId,\n    title: extractMindmapTitle(data),\n    nodes,\n    edges,\n    capabilities: {\n      canEditText: true,\n      canAddChild: true,\n      canAddSiblingAfter: true,\n      canDeleteNode: true,\n    },\n    data,\n    meta,\n  };\n};\n\nexport const buildMindmapSimpleMindMapScene = (input: {\n  documentId: string;\n  mindmapId: string;\n  data: unknown;\n  source?: string;\n  owner?: string;\n  meta?: unknown;\n}): MindmapSimpleMindMapScene => {\n  const root = canonicalizeMindmapData(input.data);\n  const meta = isRecord(input.meta) ? (input.meta as MindmapRouteMeta) : null;\n  const rootNodeIdRaw = root?.data?.uid;\n  const rootNodeId = typeof rootNodeIdRaw === \"string\" && rootNodeIdRaw.trim() ? rootNodeIdRaw.trim() : \"root\";\n  if (!root.data.uid) {\n    root.data.uid = rootNodeId;\n  }\n\n  return {\n    schema: \"mnote.mindmap.simple_mind_map_scene.v1\",\n    runtime: \"simple-mind-map\",\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    rootNodeId,\n    root,\n    layout: DEFAULT_MINDMAP_LAYOUT,\n    theme: DEFAULT_MINDMAP_THEME,\n    themeConfig: createDefaultMindmapThemeConfig(),\n    view: { x: 0, y: 0, scale: 1 },\n    config: {},\n    compatPayload: {},\n    kernelRevision: 1,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-projection.ts","s":337,"e":354,"m":1778497748182.916,"t":"runtime: \"simple-mind-map\",\n    documentId: input.documentId,\n    mindmapId: input.mindmapId,\n    rootNodeId,\n    root,\n    layout: DEFAULT_MINDMAP_LAYOUT,\n    theme: DEFAULT_MINDMAP_THEME,\n    themeConfig: createDefaultMindmapThemeConfig(),\n    view: { x: 0, y: 0, scale: 1 },\n    config: {},\n    compatPayload: {},\n    kernelRevision: 1,\n    source: input.source ?? \"rust-kernel\",\n    owner: input.owner ?? \"rust-kernel\",\n    meta,\n  };\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-shortcuts.test.ts","s":1,"e":41,"m":1778506398734.8892,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  resolveMindmapShortcutAction,\n  shouldInterceptMindmapShortcut,\n} from \"./mindmap-shortcuts\";\n\ndescribe(\"mindmap shortcuts\", () => {\n  it(\"把基础快捷键解析为导图动作\", () => {\n    expect(resolveMindmapShortcutAction({ key: \"Enter\" })).toBe(\"insertSiblingAfter\");\n    expect(resolveMindmapShortcutAction({ key: \"Tab\" })).toBe(\"insertChild\");\n    expect(resolveMindmapShortcutAction({ key: \"Delete\" })).toBe(\"deleteNode\");\n    expect(resolveMindmapShortcutAction({ key: \"F2\" })).toBe(\"editNode\");\n    expect(resolveMindmapShortcutAction({ key: \"Enter\", ctrlKey: true })).toBeNull();\n  });\n\n  it(\"已进入 mindmap 时即使暂时没有 active node，也要拦截危险按键，避免 ProseMirror 误删整个 block\", () => {\n    expect(\n      shouldInterceptMindmapShortcut({\n        debugChromeEnabled: false,\n        bridgeReady: true,\n        readonly: false,\n        isComposing: false,\n        isEditableTarget: false,\n        targetInsideRoot: false,\n        keyboardShortcutArmed: true,\n      }),\n    ).toBe(true);\n    expect(\n      shouldInterceptMindmapShortcut({\n        debugChromeEnabled: false,\n        bridgeReady: true,\n        readonly: false,\n        isComposing: false,\n        isEditableTarget: false,\n        targetInsideRoot: false,\n        keyboardShortcutArmed: false,\n      }),\n    ).toBe(false);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-shortcuts.ts","s":1,"e":41,"m":1778506385980.73,"t":"import type { MindmapUiActionId } from \"./mindmap-ui-schema\";\n\ntype MindmapShortcutEventLike = {\n  key: string;\n  shiftKey?: boolean;\n  ctrlKey?: boolean;\n  metaKey?: boolean;\n  altKey?: boolean;\n};\n\nexport type MindmapShortcutGateInput = {\n  debugChromeEnabled: boolean;\n  bridgeReady: boolean;\n  readonly: boolean;\n  isComposing: boolean;\n  isEditableTarget: boolean;\n  targetInsideRoot: boolean;\n  keyboardShortcutArmed: boolean;\n};\n\nexport const resolveMindmapShortcutAction = (\n  event: MindmapShortcutEventLike,\n): MindmapUiActionId | null => {\n  if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return null;\n  if (event.key === \"Enter\") return \"insertSiblingAfter\";\n  if (event.key === \"Tab\" || event.key === \"Insert\") return \"insertChild\";\n  if (event.key === \"Delete\" || event.key === \"Backspace\") return \"deleteNode\";\n  if (event.key === \"F2\") return \"editNode\";\n  return null;\n};\n\nexport const shouldInterceptMindmapShortcut = (\n  input: MindmapShortcutGateInput,\n): boolean => {\n  if (input.debugChromeEnabled) return false;\n  if (!input.bridgeReady || input.readonly || input.isComposing) return false;\n  if (input.isEditableTarget) return false;\n  if (input.targetInsideRoot) return true;\n  return input.keyboardShortcutArmed;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.test.ts","s":1,"e":60,"m":1778495651424.7449,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  MINDMAP_TOOLBAR_MORE_ACTION_ID,\n  MINDMAP_DEBUG_CHROME_QUERY_PARAM,\n  isMindmapDebugChromeEnabled,\n  mindmapDefaultUiSchema,\n  mindmapToolbarFileActionOrder,\n  mindmapToolbarPrimaryActionOrder,\n} from \"./mindmap-ui-schema\";\nimport { getMindmapActionMapping } from \"./mindmap-action-map\";\n\ndescribe(\"mindmap UI schema\", () => {\n  it(\"默认 schema 暴露第一阶段 UI shell 分区\", () => {\n    expect(mindmapDefaultUiSchema.toolbarGroups.map((group) => group.id)).toEqual([\n      \"history\",\n      \"node\",\n      \"insert\",\n      \"file\",\n      \"view\",\n    ]);\n    expect(mindmapDefaultUiSchema.sidebarPanels.map((panel) => panel.id)).toEqual([\n      \"nodeStyle\",\n      \"baseStyle\",\n      \"theme\",\n      \"structure\",\n      \"outline\",\n      \"shortcutKey\",\n      \"settings\",\n    ]);\n    expect(mindmapDefaultUiSchema.navigatorItems.map((item) => item.id)).toEqual([\n      \"stats\",\n      \"centerRoot\",\n      \"search\",\n      \"zoomOut\",\n      \"zoom\",\n      \"zoomIn\",\n      \"fullscreen\",\n      \"readonly\",\n    ]);\n  });\n\n  it(\"默认不启用旧手写 chrome，只有显式 debug 参数才启用\", () => {\n    expect(MINDMAP_DEBUG_CHROME_QUERY_PARAM).toBe(\"mnoteMindmapDebugChrome\");\n    expect(isMindmapDebugChromeEnabled(\"http://127.0.0.1:3000/documents/doc_1\")).toBe(false);\n    expect(\n      isMindmapDebugChromeEnabled(\"http://127.0.0.1:3000/documents/doc_1?mnoteMindmapDebugChrome=1\"),\n    ).toBe(true);\n    expect(\n      isMindmapDebugChromeEnabled(\"http://127.0.0.1:3000/documents/doc_1?mnoteMindmapDebugChrome=true\"),\n    ).toBe(true);\n  });\n\n  it(\"toolbar action id 不重复\", () => {\n    const actionIds = mindmapDefaultUiSchema.toolbarGroups.flatMap((group) => group.actions);\n    expect(new Set(actionIds).size).toBe(actionIds.length);\n  });\n\n  it(\"单行 toolbar 主按钮顺序对齐 lx-doc/KMind\", () => {\n    expect([...mindmapToolbarPrimaryActionOrder, MINDMAP_TOOLBAR_MORE_ACTION_ID]).toEqual([\n      \"undo\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.test.ts","s":49,"e":108,"m":1778495651424.7449,"t":"isMindmapDebugChromeEnabled(\"http://127.0.0.1:3000/documents/doc_1?mnoteMindmapDebugChrome=true\"),\n    ).toBe(true);\n  });\n\n  it(\"toolbar action id 不重复\", () => {\n    const actionIds = mindmapDefaultUiSchema.toolbarGroups.flatMap((group) => group.actions);\n    expect(new Set(actionIds).size).toBe(actionIds.length);\n  });\n\n  it(\"单行 toolbar 主按钮顺序对齐 lx-doc/KMind\", () => {\n    expect([...mindmapToolbarPrimaryActionOrder, MINDMAP_TOOLBAR_MORE_ACTION_ID]).toEqual([\n      \"undo\",\n      \"redo\",\n      \"editNode\",\n      \"insertSiblingAfter\",\n      \"deleteNode\",\n      \"insertChild\",\n      \"tag\",\n      \"hyperlink\",\n      \"note\",\n      \"image\",\n      \"icon\",\n      \"summary\",\n      \"associativeLine\",\n      \"formula\",\n      \"more\",\n    ]);\n  });\n\n  it(\"toolbar 元信息包含图标、短标签、长标签、优先级和溢出归类\", () => {\n    const primaryMeta = mindmapToolbarPrimaryActionOrder.map((actionId) => mindmapDefaultUiSchema.toolbarActionMeta[actionId]);\n    expect(primaryMeta.map((meta) => meta.priority)).toEqual([...primaryMeta.map((meta) => meta.priority)].sort((a, b) => a - b));\n    expect(primaryMeta.every((meta) => meta.cluster === \"main\")).toBe(true);\n    expect(primaryMeta.every((meta) => meta.iconKey && meta.shortLabel && meta.longLabel && meta.overflowGroup)).toBe(true);\n  });\n\n  it(\"导入导出属于右侧独立 toolbar cluster\", () => {\n    expect([...mindmapToolbarFileActionOrder]).toEqual([\"import\", \"export\"]);\n    expect(mindmapToolbarFileActionOrder.map((actionId) => mindmapDefaultUiSchema.toolbarActionMeta[actionId].cluster)).toEqual([\n      \"file\",\n      \"file\",\n    ]);\n  });\n\n  it(\"默认 schema 中所有 action 都有 action map 映射\", () => {\n    const actionIds = [\n      ...mindmapDefaultUiSchema.toolbarGroups.flatMap((group) => group.actions),\n      ...mindmapDefaultUiSchema.navigatorItems.flatMap((item) => (item.actionId ? [item.actionId] : [])),\n      ...mindmapDefaultUiSchema.contextMenuItems.map((item) => item.actionId),\n    ];\n\n    expect(actionIds.filter((actionId) => getMindmapActionMapping(actionId) === null)).toEqual([]);\n  });\n\n  it(\"toolbar 真实可见 action 都有 action map，more 仅作为 shell 虚拟按钮\", () => {\n    const visibleActionIds = [\n      ...mindmapToolbarPrimaryActionOrder,\n      ...mindmapToolbarFileActionOrder,\n    ];","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.test.ts","s":97,"e":156,"m":1778495651424.7449,"t":"...mindmapDefaultUiSchema.contextMenuItems.map((item) => item.actionId),\n    ];\n\n    expect(actionIds.filter((actionId) => getMindmapActionMapping(actionId) === null)).toEqual([]);\n  });\n\n  it(\"toolbar 真实可见 action 都有 action map，more 仅作为 shell 虚拟按钮\", () => {\n    const visibleActionIds = [\n      ...mindmapToolbarPrimaryActionOrder,\n      ...mindmapToolbarFileActionOrder,\n    ];\n\n    expect(visibleActionIds.filter((actionId) => getMindmapActionMapping(actionId) === null)).toEqual([]);\n    expect(mindmapDefaultUiSchema.toolbarActionMeta).not.toHaveProperty(MINDMAP_TOOLBAR_MORE_ACTION_ID);\n  });\n\n  it(\"第一阶段 context menu 覆盖节点和画布动作\", () => {\n    expect(mindmapDefaultUiSchema.contextMenuItems.filter((item) => item.requiresNode).map((item) => item.actionId)).toEqual([\n      \"insertChild\",\n      \"insertSiblingAfter\",\n      \"deleteNode\",\n      \"summary\",\n      \"associativeLine\",\n      \"expandCollapse\",\n      \"copyNodeText\",\n    ]);\n    expect(mindmapDefaultUiSchema.contextMenuItems.filter((item) => !item.requiresNode).map((item) => item.actionId)).toEqual([\n      \"centerRoot\",\n      \"fitView\",\n      \"search\",\n      \"readonly\",\n      \"showMenu\",\n    ]);\n  });\n\n  it(\"第一阶段 sidebar panel 数量受控\", () => {\n    expect(mindmapDefaultUiSchema.sidebarPanels).toHaveLength(7);\n  });\n\n  it(\"第一阶段 sidebar 暴露主题、结构和 compat patch 选项\", () => {\n    const themePanel = mindmapDefaultUiSchema.sidebarPanels.find((panel) => panel.id === \"theme\");\n    expect(themePanel?.options.map((option) => option.value)).toEqual([\"classic\", \"classic4\", \"simple\", \"dark\"]);\n\n    const structurePanel = mindmapDefaultUiSchema.sidebarPanels.find((panel) => panel.id === \"structure\");\n    expect(structurePanel?.options.map((option) => option.value)).toEqual([\n      \"logicalStructure\",\n      \"mindMap\",\n      \"organizationStructure\",\n      \"catalogOrganization\",\n      \"timeline\",\n      \"fishbone\",\n    ]);\n    expect(structurePanel?.options.every((option) => option.controlType === \"layoutCard\")).toBe(true);\n\n    const nodeStylePanel = mindmapDefaultUiSchema.sidebarPanels.find((panel) => panel.id === \"nodeStyle\");\n    const baseStylePanel = mindmapDefaultUiSchema.sidebarPanels.find((panel) => panel.id === \"baseStyle\");\n    expect(nodeStylePanel?.options.every((option) => Boolean(option.compatPath))).toBe(true);\n    expect(baseStylePanel?.options.every((option) => Boolean(option.compatPath))).toBe(true);\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.test.ts","s":145,"e":178,"m":1778495651424.7449,"t":"\"catalogOrganization\",\n      \"timeline\",\n      \"fishbone\",\n    ]);\n    expect(structurePanel?.options.every((option) => option.controlType === \"layoutCard\")).toBe(true);\n\n    const nodeStylePanel = mindmapDefaultUiSchema.sidebarPanels.find((panel) => panel.id === \"nodeStyle\");\n    const baseStylePanel = mindmapDefaultUiSchema.sidebarPanels.find((panel) => panel.id === \"baseStyle\");\n    expect(nodeStylePanel?.options.every((option) => Boolean(option.compatPath))).toBe(true);\n    expect(baseStylePanel?.options.every((option) => Boolean(option.compatPath))).toBe(true);\n  });\n\n  it(\"每个 sidebar option 要么可写，要么显式标记只读\", () => {\n    const options = mindmapDefaultUiSchema.sidebarPanels.flatMap((panel) => panel.options);\n    expect(options.length).toBeGreaterThan(0);\n    expect(options.every((option) => option.actionId !== null || option.readonly === true)).toBe(true);\n  });\n\n  it(\"可写 sidebar option 必须能映射到 compat patch 或 kernel command\", () => {\n    const writableOptions = mindmapDefaultUiSchema.sidebarPanels.flatMap((panel) =>\n      panel.options.filter((option) => option.readonly !== true),\n    );\n    expect(writableOptions.length).toBeGreaterThan(0);\n    expect(\n      writableOptions.every((option) => {\n        if (option.compatPath) return true;\n        if (!option.actionId) return false;\n        const mapping = getMindmapActionMapping(option.actionId);\n        return mapping?.target === \"kernelCommand\" || mapping?.target === \"compatPatch\";\n      }),\n    ).toBe(true);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":1,"e":60,"m":1778497092829.736,"t":"export const MINDMAP_DEBUG_CHROME_QUERY_PARAM = \"mnoteMindmapDebugChrome\";\n\nexport type MindmapUiActionId =\n  | \"undo\"\n  | \"redo\"\n  | \"editNode\"\n  | \"insertSiblingAfter\"\n  | \"insertChild\"\n  | \"deleteNode\"\n  | \"tag\"\n  | \"hyperlink\"\n  | \"note\"\n  | \"image\"\n  | \"icon\"\n  | \"summary\"\n  | \"associativeLine\"\n  | \"formula\"\n  | \"painter\"\n  | \"import\"\n  | \"export\"\n  | \"setTheme\"\n  | \"setLayout\"\n  | \"zoomIn\"\n  | \"zoomOut\"\n  | \"fitView\"\n  | \"centerRoot\"\n  | \"fullscreenCanvas\"\n  | \"fullscreenPage\"\n  | \"exitFullscreen\"\n  | \"search\"\n  | \"showMenu\"\n  | \"expandCollapse\"\n  | \"copyNodeText\"\n  | \"readonly\";\n\nexport const MINDMAP_TOOLBAR_MORE_ACTION_ID = \"more\" as const;\n\nexport type MindmapToolbarVirtualActionId = typeof MINDMAP_TOOLBAR_MORE_ACTION_ID;\n\nexport type MindmapToolbarActionId = MindmapUiActionId | MindmapToolbarVirtualActionId;\n\nexport type MindmapToolbarActionCluster = \"main\" | \"file\" | \"view\";\n\nexport type MindmapToolbarOverflowGroup = \"history\" | \"node\" | \"insert\" | \"file\" | \"view\";\n\nexport type MindmapToolbarActionMeta = {\n  id: MindmapUiActionId;\n  iconKey: string;\n  shortLabel: string;\n  longLabel: string;\n  priority: number;\n  overflowGroup: MindmapToolbarOverflowGroup;\n  cluster: MindmapToolbarActionCluster;\n};\n\nexport type MindmapToolbarGroup = {\n  id: MindmapToolbarOverflowGroup;\n  label: string;\n  actions: MindmapUiActionId[];\n  collapsePriority: number;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":49,"e":108,"m":1778497092829.736,"t":"shortLabel: string;\n  longLabel: string;\n  priority: number;\n  overflowGroup: MindmapToolbarOverflowGroup;\n  cluster: MindmapToolbarActionCluster;\n};\n\nexport type MindmapToolbarGroup = {\n  id: MindmapToolbarOverflowGroup;\n  label: string;\n  actions: MindmapUiActionId[];\n  collapsePriority: number;\n};\n\nexport type MindmapSidebarPanel = {\n  id: MindmapSidebarPanelId;\n  kind: MindmapSidebarPanelId;\n  label: string;\n  icon: string;\n  runtimeCapability: string;\n  phase: 1;\n  options: MindmapSidebarOption[];\n};\n\nexport type MindmapSidebarPanelId =\n  | \"nodeStyle\"\n  | \"baseStyle\"\n  | \"theme\"\n  | \"structure\"\n  | \"outline\"\n  | \"shortcutKey\"\n  | \"settings\";\n\nexport type MindmapSidebarOptionControlType =\n  | \"button\"\n  | \"swatch\"\n  | \"segmented\"\n  | \"slider\"\n  | \"numberInput\"\n  | \"select\"\n  | \"layoutCard\"\n  | \"treeItem\"\n  | \"toggle\";\n\nexport type MindmapSidebarOption = {\n  id: string;\n  label: string;\n  actionId: MindmapUiActionId | null;\n  value: unknown;\n  controlType: MindmapSidebarOptionControlType;\n  preview?: string;\n  description?: string;\n  readonly?: boolean;\n  compatPath?: string;\n};\n\nexport type MindmapNavigatorItem = {\n  id: \"stats\" | \"centerRoot\" | \"search\" | \"zoomOut\" | \"zoom\" | \"zoomIn\" | \"fullscreen\" | \"readonly\";\n  label: string;\n  actionId: MindmapUiActionId | null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":97,"e":156,"m":1778497092829.736,"t":"value: unknown;\n  controlType: MindmapSidebarOptionControlType;\n  preview?: string;\n  description?: string;\n  readonly?: boolean;\n  compatPath?: string;\n};\n\nexport type MindmapNavigatorItem = {\n  id: \"stats\" | \"centerRoot\" | \"search\" | \"zoomOut\" | \"zoom\" | \"zoomIn\" | \"fullscreen\" | \"readonly\";\n  label: string;\n  actionId: MindmapUiActionId | null;\n  readOnly: boolean;\n  displayMode: \"text\" | \"button\" | \"input\";\n};\n\nexport type MindmapContextMenuItem = {\n  id: string;\n  label: string;\n  actionId: MindmapUiActionId;\n  requiresNode: boolean;\n  phase: 1;\n};\n\nexport type MindmapUiSchema = {\n  toolbarGroups: MindmapToolbarGroup[];\n  toolbarActionMeta: Record<MindmapUiActionId, MindmapToolbarActionMeta>;\n  sidebarPanels: MindmapSidebarPanel[];\n  navigatorItems: MindmapNavigatorItem[];\n  contextMenuItems: MindmapContextMenuItem[];\n};\n\nexport const mindmapToolbarPrimaryActionOrder = [\n  \"undo\",\n  \"redo\",\n  \"editNode\",\n  \"insertSiblingAfter\",\n  \"deleteNode\",\n  \"insertChild\",\n  \"tag\",\n  \"hyperlink\",\n  \"note\",\n  \"image\",\n  \"icon\",\n  \"summary\",\n  \"associativeLine\",\n  \"formula\",\n] as const satisfies readonly MindmapUiActionId[];\n\nexport const mindmapToolbarFileActionOrder = [\"import\", \"export\"] as const satisfies readonly MindmapUiActionId[];\n\nconst createToolbarActionMeta = (\n  id: MindmapUiActionId,\n  iconKey: string,\n  shortLabel: string,\n  longLabel: string,\n  priority: number,\n  overflowGroup: MindmapToolbarOverflowGroup,\n  cluster: MindmapToolbarActionCluster,\n): MindmapToolbarActionMeta => ({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":145,"e":204,"m":1778497092829.736,"t":"export const mindmapToolbarFileActionOrder = [\"import\", \"export\"] as const satisfies readonly MindmapUiActionId[];\n\nconst createToolbarActionMeta = (\n  id: MindmapUiActionId,\n  iconKey: string,\n  shortLabel: string,\n  longLabel: string,\n  priority: number,\n  overflowGroup: MindmapToolbarOverflowGroup,\n  cluster: MindmapToolbarActionCluster,\n): MindmapToolbarActionMeta => ({\n  id,\n  iconKey,\n  shortLabel,\n  longLabel,\n  priority,\n  overflowGroup,\n  cluster,\n});\n\nexport const mindmapDefaultUiSchema: MindmapUiSchema = {\n  toolbarGroups: [\n    {\n      id: \"history\",\n      label: \"历史\",\n      actions: [\"undo\", \"redo\"],\n      collapsePriority: 4,\n    },\n    {\n      id: \"node\",\n      label: \"节点\",\n      actions: [\"editNode\", \"insertSiblingAfter\", \"deleteNode\", \"insertChild\"],\n      collapsePriority: 1,\n    },\n    {\n      id: \"insert\",\n      label: \"插入\",\n      actions: [\"tag\", \"hyperlink\", \"note\", \"image\", \"icon\", \"summary\", \"associativeLine\", \"formula\"],\n      collapsePriority: 2,\n    },\n    {\n      id: \"file\",\n      label: \"文件\",\n      actions: [\"import\", \"export\"],\n      collapsePriority: 5,\n    },\n    {\n      id: \"view\",\n      label: \"视图\",\n      actions: [\"painter\", \"centerRoot\", \"zoomOut\", \"zoomIn\", \"search\", \"readonly\"],\n      collapsePriority: 3,\n    },\n  ],\n  toolbarActionMeta: {\n    undo: createToolbarActionMeta(\"undo\", \"undo\", \"撤销\", \"撤销\", 10, \"history\", \"main\"),\n    redo: createToolbarActionMeta(\"redo\", \"redo\", \"重做\", \"重做\", 20, \"history\", \"main\"),\n    editNode: createToolbarActionMeta(\"editNode\", \"type\", \"编辑\", \"编辑节点\", 30, \"node\", \"main\"),\n    insertSiblingAfter: createToolbarActionMeta(\"insertSiblingAfter\", \"sibling\", \"同级\", \"插入同级节点\", 40, \"node\", \"main\"),\n    deleteNode: createToolbarActionMeta(\"deleteNode\", \"trash\", \"删除\", \"删除节点\", 50, \"node\", \"main\"),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":193,"e":243,"m":1778497092829.736,"t":"id: \"view\",\n      label: \"视图\",\n      actions: [\"painter\", \"centerRoot\", \"zoomOut\", \"zoomIn\", \"search\", \"readonly\"],\n      collapsePriority: 3,\n    },\n  ],\n  toolbarActionMeta: {\n    undo: createToolbarActionMeta(\"undo\", \"undo\", \"撤销\", \"撤销\", 10, \"history\", \"main\"),\n    redo: createToolbarActionMeta(\"redo\", \"redo\", \"重做\", \"重做\", 20, \"history\", \"main\"),\n    editNode: createToolbarActionMeta(\"editNode\", \"type\", \"编辑\", \"编辑节点\", 30, \"node\", \"main\"),\n    insertSiblingAfter: createToolbarActionMeta(\"insertSiblingAfter\", \"sibling\", \"同级\", \"插入同级节点\", 40, \"node\", \"main\"),\n    deleteNode: createToolbarActionMeta(\"deleteNode\", \"trash\", \"删除\", \"删除节点\", 50, \"node\", \"main\"),\n    insertChild: createToolbarActionMeta(\"insertChild\", \"child\", \"子级\", \"插入子节点\", 60, \"node\", \"main\"),\n    tag: createToolbarActionMeta(\"tag\", \"tag\", \"标签\", \"标签\", 70, \"insert\", \"main\"),\n    hyperlink: createToolbarActionMeta(\"hyperlink\", \"link\", \"链接\", \"超链接\", 80, \"insert\", \"main\"),\n    note: createToolbarActionMeta(\"note\", \"note\", \"备注\", \"备注\", 90, \"insert\", \"main\"),\n    image: createToolbarActionMeta(\"image\", \"image\", \"图片\", \"图片\", 100, \"insert\", \"main\"),\n    icon: createToolbarActionMeta(\"icon\", \"smile\", \"图标\", \"图标\", 110, \"insert\", \"main\"),\n    summary: createToolbarActionMeta(\"summary\", \"summary\", \"概要\", \"概要\", 120, \"insert\", \"main\"),\n    associativeLine: createToolbarActionMeta(\"associativeLine\", \"route\", \"关联\", \"关联线\", 130, \"insert\", \"main\"),\n    formula: createToolbarActionMeta(\"formula\", \"formula\", \"公式\", \"公式\", 140, \"insert\", \"main\"),\n    painter: createToolbarActionMeta(\"painter\", \"paintbrush\", \"格式\", \"格式刷\", 210, \"view\", \"view\"),\n    import: createToolbarActionMeta(\"import\", \"import\", \"导入\", \"导入\", 310, \"file\", \"file\"),\n    export: createToolbarActionMeta(\"export\", \"export\", \"导出\", \"导出\", 320, \"file\", \"file\"),\n    setTheme: createToolbarActionMeta(\"setTheme\", \"palette\", \"主题\", \"主题\", 410, \"view\", \"view\"),\n    setLayout: createToolbarActionMeta(\"setLayout\", \"layout\", \"结构\", \"结构\", 420, \"view\", \"view\"),\n    zoomIn: createToolbarActionMeta(\"zoomIn\", \"zoom-in\", \"放大\", \"放大\", 510, \"view\", \"view\"),\n    zoomOut: createToolbarActionMeta(\"zoomOut\", \"zoom-out\", \"缩小\", \"缩小\", 500, \"view\", \"view\"),\n    fitView: createToolbarActionMeta(\"fitView\", \"fit\", \"适应\", \"适应画布\", 520, \"view\", \"view\"),\n    centerRoot: createToolbarActionMeta(\"centerRoot\", \"target\", \"回根\", \"回到根节点\", 490, \"view\", \"view\"),\n    fullscreenCanvas: createToolbarActionMeta(\"fullscreenCanvas\", \"fullscreen\", \"全屏\", \"全屏查看\", 550, \"view\", \"view\"),\n    fullscreenPage: createToolbarActionMeta(\"fullscreenPage\", \"fullscreen-page\", \"全页\", \"全屏编辑\", 560, \"view\", \"view\"),\n    exitFullscreen: createToolbarActionMeta(\"exitFullscreen\", \"exit-fullscreen\", \"退出\", \"退出全屏\", 570, \"view\", \"view\"),\n    search: createToolbarActionMeta(\"search\", \"search\", \"搜索\", \"搜索\", 530, \"view\", \"view\"),\n    showMenu: createToolbarActionMeta(\"showMenu\", \"menu\", \"菜单\", \"显示菜单\", 580, \"view\", \"view\"),\n    expandCollapse: createToolbarActionMeta(\"expandCollapse\", \"expand\", \"展开\", \"展开/收起\", 610, \"view\", \"view\"),\n    copyNodeText: createToolbarActionMeta(\"copyNodeText\", \"copy\", \"复制\", \"复制文本\", 620, \"view\", \"view\"),\n    readonly: createToolbarActionMeta(\"readonly\", \"lock\", \"只读\", \"只读\", 540, \"view\", \"view\"),\n  },\n  sidebarPanels: [\n    {\n      id: \"nodeStyle\",\n      kind: \"nodeStyle\",\n      label: \"节点样式\",\n      icon: \"palette\",\n      runtimeCapability: \"node-style\",\n      phase: 1,\n      options: [\n        { id: \"node-fill-blue\", label: \"海蓝\", actionId: \"painter\", value: \"#dbeafe\", controlType: \"swatch\", preview: \"#dbeafe\", compatPath: \"nodes.$active.data.fillColor\" },\n        { id: \"node-fill-green\", label: \"薄荷\", actionId: \"painter\", value: \"#dcfce7\", controlType: \"swatch\", preview: \"#dcfce7\", compatPath: \"nodes.$active.data.fillColor\" },\n        { id: \"node-fill-amber\", label: \"暖黄\", actionId: \"painter\", value: \"#fef3c7\", controlType: \"swatch\", preview: \"#fef3c7\", compatPath: \"nodes.$active.data.fillColor\" },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":244,"e":252,"m":1778497092829.736,"t":"        { id: \"node-text-dark\", label: \"深色文字\", actionId: \"painter\", value: \"#0f172a\", controlType: \"swatch\", preview: \"#0f172a\", compatPath: \"nodes.$active.data.color\" },\n        { id: \"node-text-blue\", label: \"蓝色文字\", actionId: \"painter\", value: \"#1d4ed8\", controlType: \"swatch\", preview: \"#1d4ed8\", compatPath: \"nodes.$active.data.color\" },\n        { id: \"node-font-14\", label: \"14 px\", actionId: \"painter\", value: 14, controlType: \"segmented\", compatPath: \"nodes.$active.data.fontSize\" },\n        { id: \"node-font-18\", label: \"18 px\", actionId: \"painter\", value: 18, controlType: \"segmented\", compatPath: \"nodes.$active.data.fontSize\" },\n        { id: \"node-font-bold\", label: \"加粗\", actionId: \"painter\", value: true, controlType: \"toggle\", compatPath: \"nodes.$active.data.fontWeight\" },\n        { id: \"node-font-italic\", label: \"斜体\", actionId: \"painter\", value: true, controlType: \"toggle\", compatPath: \"nodes.$active.data.fontStyle\" },\n        { id: \"node-shape-round\", label: \"圆角矩形\", actionId: \"painter\", value: \"roundedRectangle\", controlType: \"button\", compatPath: \"nodes.$active.data.shape\" },\n        { id: \"node-shape-rect\", label: \"矩形\", actionId: \"painter\", value: \"rectangle\", controlType: \"button\", compatPath: \"nodes.$active.data.shape\" },\n        { id: \"node-border-blue\", label: \"蓝色边框\", actionId: \"painter\", value: \"#60a5fa\", controlType: \"swatch\", preview: \"#60a5fa\", compatPath: \"nodes.$active.data.borderColor\" },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":241,"e":282,"m":1778497092829.736,"t":"{ id: \"node-fill-blue\", label: \"海蓝\", actionId: \"painter\", value: \"#dbeafe\", controlType: \"swatch\", preview: \"#dbeafe\", compatPath: \"nodes.$active.data.fillColor\" },\n        { id: \"node-fill-green\", label: \"薄荷\", actionId: \"painter\", value: \"#dcfce7\", controlType: \"swatch\", preview: \"#dcfce7\", compatPath: \"nodes.$active.data.fillColor\" },\n        { id: \"node-fill-amber\", label: \"暖黄\", actionId: \"painter\", value: \"#fef3c7\", controlType: \"swatch\", preview: \"#fef3c7\", compatPath: \"nodes.$active.data.fillColor\" },\n        { id: \"node-text-dark\", label: \"深色文字\", actionId: \"painter\", value: \"#0f172a\", controlType: \"swatch\", preview: \"#0f172a\", compatPath: \"nodes.$active.data.color\" },\n        { id: \"node-text-blue\", label: \"蓝色文字\", actionId: \"painter\", value: \"#1d4ed8\", controlType: \"swatch\", preview: \"#1d4ed8\", compatPath: \"nodes.$active.data.color\" },\n        { id: \"node-font-14\", label: \"14 px\", actionId: \"painter\", value: 14, controlType: \"segmented\", compatPath: \"nodes.$active.data.fontSize\" },\n        { id: \"node-font-18\", label: \"18 px\", actionId: \"painter\", value: 18, controlType: \"segmented\", compatPath: \"nodes.$active.data.fontSize\" },\n        { id: \"node-font-bold\", label: \"加粗\", actionId: \"painter\", value: true, controlType: \"toggle\", compatPath: \"nodes.$active.data.fontWeight\" },\n        { id: \"node-font-italic\", label: \"斜体\", actionId: \"painter\", value: true, controlType: \"toggle\", compatPath: \"nodes.$active.data.fontStyle\" },\n        { id: \"node-shape-round\", label: \"圆角矩形\", actionId: \"painter\", value: \"roundedRectangle\", controlType: \"button\", compatPath: \"nodes.$active.data.shape\" },\n        { id: \"node-shape-rect\", label: \"矩形\", actionId: \"painter\", value: \"rectangle\", controlType: \"button\", compatPath: \"nodes.$active.data.shape\" },\n        { id: \"node-border-blue\", label: \"蓝色边框\", actionId: \"painter\", value: \"#60a5fa\", controlType: \"swatch\", preview: \"#60a5fa\", compatPath: \"nodes.$active.data.borderColor\" },\n        { id: \"node-line-teal\", label: \"青色分支线\", actionId: \"painter\", value: \"#14b8a6\", controlType: \"swatch\", preview: \"#14b8a6\", compatPath: \"nodes.$active.data.lineColor\" },\n        { id: \"node-line-width-2\", label: \"边线 2\", actionId: \"painter\", value: 2, controlType: \"segmented\", compatPath: \"nodes.$active.data.lineWidth\" },\n      ],\n    },\n    {\n      id: \"baseStyle\",\n      kind: \"baseStyle\",\n      label: \"导图样式\",\n      icon: \"sliders\",\n      runtimeCapability: \"base-style\",\n      phase: 1,\n      options: [\n        { id: \"base-curve-line\", label: \"曲线\", actionId: \"painter\", value: \"curve\", controlType: \"segmented\", compatPath: \"style.map.lineStyle\" },\n        { id: \"base-direct-line\", label: \"直线\", actionId: \"painter\", value: \"straight\", controlType: \"segmented\", compatPath: \"style.map.lineStyle\" },\n        { id: \"base-rainbow-lines\", label: \"彩虹线条\", actionId: \"painter\", value: { enabled: true }, controlType: \"toggle\", compatPath: \"style.map.rainbowLines\" },\n        { id: \"base-line-width-2\", label: \"线宽 2\", actionId: \"painter\", value: 2, controlType: \"segmented\", compatPath: \"style.map.lineWidth\" },\n        { id: \"base-line-width-4\", label: \"线宽 4\", actionId: \"painter\", value: 4, controlType: \"segmented\", compatPath: \"style.map.lineWidth\" },\n        { id: \"base-background-light\", label: \"浅色背景\", actionId: \"painter\", value: \"#f8fafc\", controlType: \"swatch\", preview: \"#f8fafc\", compatPath: \"style.map.backgroundColor\" },\n        { id: \"base-node-spacing-36\", label: \"节点间距 36\", actionId: \"painter\", value: 36, controlType: \"numberInput\", compatPath: \"style.map.nodeSpacing\" },\n        { id: \"base-summary-bracket\", label: \"括号概要\", actionId: \"painter\", value: \"bracket\", controlType: \"select\", compatPath: \"style.map.summaryStyle\" },\n      ],\n    },\n    {\n      id: \"theme\",\n      kind: \"theme\",\n      label: \"主题\",\n      icon: \"swatch\",\n      runtimeCapability: \"theme\",\n      phase: 1,\n      options: [","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":283,"e":300,"m":1778497092829.736,"t":"        { id: \"theme-classic\", label: \"Classic\", actionId: \"setTheme\", value: \"classic\", controlType: \"swatch\", preview: \"#60a5fa\", description: \"默认主题\" },\n        { id: \"theme-classic4\", label: \"KMind\", actionId: \"setTheme\", value: \"classic4\", controlType: \"swatch\", preview: \"#22c55e\", description: \"KMind-like\" },\n        { id: \"theme-simple\", label: \"Simple\", actionId: \"setTheme\", value: \"simple\", controlType: \"swatch\", preview: \"#f59e0b\", description: \"轻量主题\" },\n        { id: \"theme-dark\", label: \"Dark\", actionId: \"setTheme\", value: \"dark\", controlType: \"swatch\", preview: \"#334155\", description: \"深色主题\" },\n      ],\n    },\n    {\n      id: \"structure\",\n      kind: \"structure\",\n      label: \"结构\",\n      icon: \"layout\",\n      runtimeCapability: \"layout\",\n      phase: 1,\n      options: [\n        { id: \"layout-logical\", label: \"逻辑结构图\", actionId: \"setLayout\", value: \"logicalStructure\", controlType: \"layoutCard\", preview: \"logicalStructure\" },\n        { id: \"layout-mind-map\", label: \"思维导图\", actionId: \"setLayout\", value: \"mindMap\", controlType: \"layoutCard\", preview: \"mindMap\" },\n        { id: \"layout-organization\", label: \"组织结构图\", actionId: \"setLayout\", value: \"organizationStructure\", controlType: \"layoutCard\", preview: \"organizationStructure\" },\n        { id: \"layout-catalog\", label: \"目录组织图\", actionId: \"setLayout\", value: \"catalogOrganization\", controlType: \"layoutCard\", preview: \"catalogOrganization\" },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":289,"e":348,"m":1778497092829.736,"t":"{\n      id: \"structure\",\n      kind: \"structure\",\n      label: \"结构\",\n      icon: \"layout\",\n      runtimeCapability: \"layout\",\n      phase: 1,\n      options: [\n        { id: \"layout-logical\", label: \"逻辑结构图\", actionId: \"setLayout\", value: \"logicalStructure\", controlType: \"layoutCard\", preview: \"logicalStructure\" },\n        { id: \"layout-mind-map\", label: \"思维导图\", actionId: \"setLayout\", value: \"mindMap\", controlType: \"layoutCard\", preview: \"mindMap\" },\n        { id: \"layout-organization\", label: \"组织结构图\", actionId: \"setLayout\", value: \"organizationStructure\", controlType: \"layoutCard\", preview: \"organizationStructure\" },\n        { id: \"layout-catalog\", label: \"目录组织图\", actionId: \"setLayout\", value: \"catalogOrganization\", controlType: \"layoutCard\", preview: \"catalogOrganization\" },\n        { id: \"layout-timeline\", label: \"时间轴\", actionId: \"setLayout\", value: \"timeline\", controlType: \"layoutCard\", preview: \"timeline\" },\n        { id: \"layout-fishbone\", label: \"鱼骨图\", actionId: \"setLayout\", value: \"fishbone\", controlType: \"layoutCard\", preview: \"fishbone\" },\n      ],\n    },\n    {\n      id: \"outline\",\n      kind: \"outline\",\n      label: \"大纲\",\n      icon: \"list-tree\",\n      runtimeCapability: \"outline\",\n      phase: 1,\n      options: [],\n    },\n    {\n      id: \"shortcutKey\",\n      kind: \"shortcutKey\",\n      label: \"快捷键\",\n      icon: \"sparkles\",\n      runtimeCapability: \"shortcut-key\",\n      phase: 1,\n      options: [\n        { id: \"shortcut-insert-child\", label: \"Tab\", description: \"插入子节点\", actionId: null, value: null, controlType: \"treeItem\", readonly: true },\n        { id: \"shortcut-insert-sibling\", label: \"Enter\", description: \"插入同级节点\", actionId: null, value: null, controlType: \"treeItem\", readonly: true },\n        { id: \"shortcut-delete\", label: \"Delete\", description: \"删除节点\", actionId: null, value: null, controlType: \"treeItem\", readonly: true },\n      ],\n    },\n    {\n      id: \"settings\",\n      kind: \"settings\",\n      label: \"设置\",\n      icon: \"hexagon\",\n      runtimeCapability: \"settings\",\n      phase: 1,\n      options: [\n        { id: \"settings-readonly-hint\", label: \"只读模式\", description: \"导航栏切换\", actionId: null, value: null, controlType: \"toggle\", readonly: true },\n        { id: \"settings-mouse\", label: \"鼠标行为\", description: \"左键选中，右键拖拽\", actionId: null, value: \"leftSelectRightDrag\", controlType: \"select\", readonly: true },\n      ],\n    },\n  ],\n  navigatorItems: [\n    { id: \"stats\", label: \"统计\", actionId: null, readOnly: true, displayMode: \"text\" },\n    { id: \"centerRoot\", label: \"回根节点\", actionId: \"centerRoot\", readOnly: true, displayMode: \"button\" },\n    { id: \"search\", label: \"搜索\", actionId: \"search\", readOnly: true, displayMode: \"button\" },\n    { id: \"zoomOut\", label: \"缩小\", actionId: \"zoomOut\", readOnly: true, displayMode: \"button\" },\n    { id: \"zoom\", label: \"缩放\", actionId: null, readOnly: true, displayMode: \"input\" },\n    { id: \"zoomIn\", label: \"放大\", actionId: \"zoomIn\", readOnly: true, displayMode: \"button\" },\n    { id: \"fullscreen\", label: \"全屏\", actionId: \"fullscreenCanvas\", readOnly: true, displayMode: \"button\" },\n    { id: \"readonly\", label: \"只读\", actionId: \"readonly\", readOnly: true, displayMode: \"button\" },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-schema.ts","s":337,"e":374,"m":1778497092829.736,"t":"],\n    },\n  ],\n  navigatorItems: [\n    { id: \"stats\", label: \"统计\", actionId: null, readOnly: true, displayMode: \"text\" },\n    { id: \"centerRoot\", label: \"回根节点\", actionId: \"centerRoot\", readOnly: true, displayMode: \"button\" },\n    { id: \"search\", label: \"搜索\", actionId: \"search\", readOnly: true, displayMode: \"button\" },\n    { id: \"zoomOut\", label: \"缩小\", actionId: \"zoomOut\", readOnly: true, displayMode: \"button\" },\n    { id: \"zoom\", label: \"缩放\", actionId: null, readOnly: true, displayMode: \"input\" },\n    { id: \"zoomIn\", label: \"放大\", actionId: \"zoomIn\", readOnly: true, displayMode: \"button\" },\n    { id: \"fullscreen\", label: \"全屏\", actionId: \"fullscreenCanvas\", readOnly: true, displayMode: \"button\" },\n    { id: \"readonly\", label: \"只读\", actionId: \"readonly\", readOnly: true, displayMode: \"button\" },\n  ],\n  contextMenuItems: [\n    { id: \"insertChild\", label: \"插入子节点\", actionId: \"insertChild\", requiresNode: true, phase: 1 },\n    { id: \"insertSiblingAfter\", label: \"插入同级节点\", actionId: \"insertSiblingAfter\", requiresNode: true, phase: 1 },\n    { id: \"deleteNode\", label: \"删除节点\", actionId: \"deleteNode\", requiresNode: true, phase: 1 },\n    { id: \"summary\", label: \"概要\", actionId: \"summary\", requiresNode: true, phase: 1 },\n    { id: \"associativeLine\", label: \"关联线\", actionId: \"associativeLine\", requiresNode: true, phase: 1 },\n    { id: \"expandCollapse\", label: \"展开/收起\", actionId: \"expandCollapse\", requiresNode: true, phase: 1 },\n    { id: \"copyNodeText\", label: \"复制文本\", actionId: \"copyNodeText\", requiresNode: true, phase: 1 },\n    { id: \"centerRoot\", label: \"回根节点\", actionId: \"centerRoot\", requiresNode: false, phase: 1 },\n    { id: \"fitView\", label: \"适应画布\", actionId: \"fitView\", requiresNode: false, phase: 1 },\n    { id: \"search\", label: \"搜索\", actionId: \"search\", requiresNode: false, phase: 1 },\n    { id: \"readonly\", label: \"只读切换\", actionId: \"readonly\", requiresNode: false, phase: 1 },\n    { id: \"showMenu\", label: \"显示菜单\", actionId: \"showMenu\", requiresNode: false, phase: 1 },\n  ],\n};\n\nexport const isMindmapDebugChromeEnabled = (href: string): boolean => {\n  try {\n    const value = new URL(href).searchParams.get(MINDMAP_DEBUG_CHROME_QUERY_PARAM);\n    return value === \"1\" || value === \"true\";\n  } catch {\n    return false;\n  }\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-state.test.ts","s":1,"e":60,"m":1778688996967.2717,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  createDefaultMindmapShellInteractionState,\n  deriveMindmapUiState,\n  reduceMindmapChromeVisibility,\n} from \"./mindmap-ui-state\";\n\ndescribe(\"mindmap UI state\", () => {\n  it(\"无 active node 时禁用节点编辑动作，但保留视图动作\", () => {\n    const state = deriveMindmapUiState({ activeNodeId: null, readonly: false });\n\n    expect(state.disabledActions.insertChild).toBe(true);\n    expect(state.disabledActions.insertSiblingAfter).toBe(true);\n    expect(state.disabledActions.deleteNode).toBe(true);\n    expect(state.disabledActions.expandCollapse).toBe(true);\n    expect(state.disabledActions.zoomIn).toBe(false);\n    expect(state.disabledActions.centerRoot).toBe(false);\n    expect(state.disabledActions.fullscreenCanvas).toBe(false);\n    expect(state.disabledActions.exitFullscreen).toBe(false);\n  });\n\n  it(\"readonly 时禁用编辑动作，保留搜索和视图动作\", () => {\n    const state = deriveMindmapUiState({ activeNodeId: \"node_1\", readonly: true });\n\n    expect(state.disabledActions.insertChild).toBe(true);\n    expect(state.disabledActions.note).toBe(true);\n    expect(state.disabledActions.setTheme).toBe(true);\n    expect(state.disabledActions.expandCollapse).toBe(true);\n    expect(state.disabledActions.copyNodeText).toBe(false);\n    expect(state.disabledActions.search).toBe(false);\n    expect(state.disabledActions.zoomOut).toBe(false);\n    expect(state.disabledActions.fullscreenCanvas).toBe(false);\n    expect(state.disabledActions.fullscreenPage).toBe(false);\n  });\n\n  it(\"缺少 runtime capability 时禁用对应 action\", () => {\n    const state = deriveMindmapUiState({\n      activeNodeId: \"node_1\",\n      readonly: false,\n      runtimeCapabilities: [\"runtimeCommand\", \"kernelCommand\"],\n    });\n\n    expect(state.disabledActions.note).toBe(true);\n    expect(state.disabledActions.insertChild).toBe(false);\n  });\n\n  it(\"导出能力暂缓时在 UI state 中保持禁用\", () => {\n    const state = deriveMindmapUiState({ activeNodeId: \"node_1\", readonly: false });\n\n    expect(state.disabledActions.export).toBe(true);\n  });\n\n  it(\"派生默认 shell state，给 Leptos shell 提供稳定合同\", () => {\n    const state = deriveMindmapUiState({\n      activeNodeId: \"node_1\",\n      readonly: true,\n      shell: {\n        chromeVisibility: \"hiddenByPointerLeave\",\n        toolbarOverflow: {\n          availableWidth: 640,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-state.test.ts","s":49,"e":108,"m":1778688996967.2717,"t":"expect(state.disabledActions.export).toBe(true);\n  });\n\n  it(\"派生默认 shell state，给 Leptos shell 提供稳定合同\", () => {\n    const state = deriveMindmapUiState({\n      activeNodeId: \"node_1\",\n      readonly: true,\n      shell: {\n        chromeVisibility: \"hiddenByPointerLeave\",\n        toolbarOverflow: {\n          availableWidth: 640,\n          visibleActionIds: [\"undo\", \"redo\"],\n          overflowActionIds: [\"image\"],\n          moreOpen: true,\n        },\n        fullscreen: {\n          mode: \"canvas\",\n          isFullscreen: true,\n          apiAvailable: false,\n        },\n        sidebar: {\n          triggerVisible: false,\n          panelOpen: false,\n          activePanelId: \"structure\",\n          collapsedByToggle: true,\n        },\n        navigator: {\n          searchOpen: true,\n          minimapOpen: true,\n          zoomPercent: 138.4,\n        },\n      },\n    });\n\n    expect(state.shell.chromeVisibility).toBe(\"hiddenByPointerLeave\");\n    expect(state.shell.toolbarOverflow).toMatchObject({\n      availableWidth: 640,\n      visibleActionIds: [\"undo\", \"redo\"],\n      overflowActionIds: [\"image\"],\n      moreOpen: true,\n    });\n    expect(state.shell.fullscreen).toMatchObject({\n      mode: \"canvas\",\n      isFullscreen: true,\n      target: \"mindmap-root\",\n      apiAvailable: false,\n    });\n    expect(state.shell.sidebar).toMatchObject({\n      triggerVisible: false,\n      panelOpen: false,\n      activePanelId: \"structure\",\n      drawerWidth: 300,\n      collapsedByToggle: true,\n    });\n    expect(state.shell.navigator).toMatchObject({\n      searchOpen: true,\n      minimapOpen: true,\n      readonly: true,\n      zoomPercent: 138,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-state.test.ts","s":97,"e":146,"m":1778688996967.2717,"t":"expect(state.shell.sidebar).toMatchObject({\n      triggerVisible: false,\n      panelOpen: false,\n      activePanelId: \"structure\",\n      drawerWidth: 300,\n      collapsedByToggle: true,\n    });\n    expect(state.shell.navigator).toMatchObject({\n      searchOpen: true,\n      minimapOpen: true,\n      readonly: true,\n      zoomPercent: 138,\n      mouseBehavior: \"leftSelectRightDrag\",\n    });\n  });\n\n  it(\"鼠标移出隐藏 chrome，鼠标移入不自动恢复，点击才恢复\", () => {\n    const hidden = reduceMindmapChromeVisibility(\"visible\", \"pointerLeave\");\n    expect(hidden).toBe(\"hiddenByPointerLeave\");\n    expect(reduceMindmapChromeVisibility(hidden, \"pointerEnter\")).toBe(\"hiddenByPointerLeave\");\n    expect(reduceMindmapChromeVisibility(hidden, \"restoreClick\")).toBe(\"visible\");\n  });\n\n  it(\"sidebar 隐藏触发条后保留最近 active panel\", () => {\n    const state = createDefaultMindmapShellInteractionState({\n      sidebar: {\n        triggerVisible: false,\n        panelOpen: false,\n        activePanelId: \"structure\",\n        collapsedByToggle: true,\n      },\n    });\n\n    expect(state.sidebar.triggerVisible).toBe(false);\n    expect(state.sidebar.panelOpen).toBe(false);\n    expect(state.sidebar.activePanelId).toBe(\"structure\");\n    expect(state.sidebar.collapsedByToggle).toBe(true);\n\n    const restored = createDefaultMindmapShellInteractionState({\n      sidebar: {\n        ...state.sidebar,\n        triggerVisible: true,\n        panelOpen: true,\n        collapsedByToggle: false,\n      },\n    });\n    expect(restored.sidebar.activePanelId).toBe(\"structure\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-state.ts","s":1,"e":60,"m":1778504167974.0461,"t":"import { getMindmapActionMapping, listMindmapActionMappings, type MindmapActionTarget } from \"./mindmap-action-map\";\nimport { mindmapDefaultUiSchema, type MindmapUiActionId } from \"./mindmap-ui-schema\";\n\nexport type MindmapRuntimeCapability = MindmapActionTarget;\n\nexport type MindmapChromeVisibilityState =\n  | \"visible\"\n  | \"hiddenByPointerLeave\"\n  | \"hiddenByToggle\"\n  | \"hiddenByFullscreen\";\n\nexport type MindmapToolbarOverflowState = {\n  availableWidth: number | null;\n  visibleActionIds: MindmapUiActionId[];\n  overflowActionIds: MindmapUiActionId[];\n  moreOpen: boolean;\n};\n\nexport type MindmapFullscreenState = {\n  mode: \"none\" | \"canvas\" | \"page\";\n  isFullscreen: boolean;\n  target: \"mindmap-root\" | \"document-body\";\n  apiAvailable: boolean;\n};\n\nexport type MindmapSidebarState = {\n  triggerVisible: boolean;\n  panelOpen: boolean;\n  activePanelId: string | null;\n  drawerWidth: number;\n  collapsedByToggle: boolean;\n};\n\nexport type MindmapNavigatorState = {\n  searchOpen: boolean;\n  minimapOpen: boolean;\n  readonly: boolean;\n  zoomPercent: number;\n  mouseBehavior: \"leftSelectRightDrag\" | \"leftDragRightMenu\";\n};\n\nexport type MindmapShellInteractionState = {\n  chromeVisibility: MindmapChromeVisibilityState;\n  toolbarOverflow: MindmapToolbarOverflowState;\n  fullscreen: MindmapFullscreenState;\n  sidebar: MindmapSidebarState;\n  navigator: MindmapNavigatorState;\n};\n\nexport type MindmapUiStateInput = {\n  activeNodeId?: string | null;\n  readonly: boolean;\n  runtimeCapabilities?: MindmapRuntimeCapability[];\n  shell?: PartialMindmapShellInteractionState;\n};\n\nexport type MindmapUiState = {\n  activeNodeId: string | null;\n  readonly: boolean;\n  disabledActions: Record<MindmapUiActionId, boolean>;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-state.ts","s":49,"e":108,"m":1778504167974.0461,"t":"export type MindmapUiStateInput = {\n  activeNodeId?: string | null;\n  readonly: boolean;\n  runtimeCapabilities?: MindmapRuntimeCapability[];\n  shell?: PartialMindmapShellInteractionState;\n};\n\nexport type MindmapUiState = {\n  activeNodeId: string | null;\n  readonly: boolean;\n  disabledActions: Record<MindmapUiActionId, boolean>;\n  shell: MindmapShellInteractionState;\n};\n\nexport type PartialMindmapShellInteractionState = {\n  chromeVisibility?: MindmapChromeVisibilityState;\n  toolbarOverflow?: Partial<MindmapToolbarOverflowState>;\n  fullscreen?: Partial<MindmapFullscreenState>;\n  sidebar?: Partial<MindmapSidebarState>;\n  navigator?: Partial<MindmapNavigatorState>;\n};\n\nexport type MindmapChromeVisibilityEvent =\n  | \"pointerLeave\"\n  | \"pointerEnter\"\n  | \"restoreClick\"\n  | \"hideToggle\"\n  | \"showToggle\"\n  | \"enterFullscreen\"\n  | \"exitFullscreen\";\n\nconst allActionIds = (): MindmapUiActionId[] => {\n  const ids = [\n    ...mindmapDefaultUiSchema.toolbarGroups.flatMap((group) => group.actions),\n    ...mindmapDefaultUiSchema.navigatorItems.flatMap((item) => (item.actionId ? [item.actionId] : [])),\n    ...mindmapDefaultUiSchema.contextMenuItems.map((item) => item.actionId),\n    ...listMindmapActionMappings().map((mapping) => mapping.actionId),\n  ];\n  return [...new Set(ids)];\n};\n\nconst normalizeActiveNodeId = (value: string | null | undefined): string | null => {\n  const trimmed = value?.trim();\n  return trimmed ? trimmed : null;\n};\n\nconst normalizeZoomPercent = (value: number | undefined): number => {\n  if (typeof value !== \"number\" || !Number.isFinite(value)) return 100;\n  return Math.max(10, Math.min(500, Math.round(value)));\n};\n\nconst unsupportedActionIds = new Set<MindmapUiActionId>([\n  \"tag\",\n  \"hyperlink\",\n  \"note\",\n  \"image\",\n  \"icon\",\n  \"associativeLine\",\n  \"formula\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-state.ts","s":97,"e":156,"m":1778504167974.0461,"t":"if (typeof value !== \"number\" || !Number.isFinite(value)) return 100;\n  return Math.max(10, Math.min(500, Math.round(value)));\n};\n\nconst unsupportedActionIds = new Set<MindmapUiActionId>([\n  \"tag\",\n  \"hyperlink\",\n  \"note\",\n  \"image\",\n  \"icon\",\n  \"associativeLine\",\n  \"formula\",\n  \"import\",\n  \"export\",\n]);\n\nexport const createDefaultMindmapShellInteractionState = (\n  input: PartialMindmapShellInteractionState = {},\n): MindmapShellInteractionState => ({\n  chromeVisibility: input.chromeVisibility ?? \"visible\",\n  toolbarOverflow: {\n    availableWidth: input.toolbarOverflow?.availableWidth ?? null,\n    visibleActionIds: input.toolbarOverflow?.visibleActionIds ?? [],\n    overflowActionIds: input.toolbarOverflow?.overflowActionIds ?? [],\n    moreOpen: input.toolbarOverflow?.moreOpen ?? false,\n  },\n  fullscreen: {\n    mode: input.fullscreen?.mode ?? \"none\",\n    isFullscreen: input.fullscreen?.isFullscreen ?? false,\n    target: input.fullscreen?.target ?? \"mindmap-root\",\n    apiAvailable: input.fullscreen?.apiAvailable ?? true,\n  },\n  sidebar: {\n    triggerVisible: input.sidebar?.triggerVisible ?? true,\n    panelOpen: input.sidebar?.panelOpen ?? true,\n    activePanelId: input.sidebar?.activePanelId ?? mindmapDefaultUiSchema.sidebarPanels[0]?.id ?? null,\n    drawerWidth: input.sidebar?.drawerWidth ?? 300,\n    collapsedByToggle: input.sidebar?.collapsedByToggle ?? false,\n  },\n  navigator: {\n    searchOpen: input.navigator?.searchOpen ?? false,\n    minimapOpen: input.navigator?.minimapOpen ?? false,\n    readonly: input.navigator?.readonly ?? false,\n    zoomPercent: normalizeZoomPercent(input.navigator?.zoomPercent),\n    mouseBehavior: input.navigator?.mouseBehavior ?? \"leftSelectRightDrag\",\n  },\n});\n\nexport const reduceMindmapChromeVisibility = (\n  state: MindmapChromeVisibilityState,\n  event: MindmapChromeVisibilityEvent,\n): MindmapChromeVisibilityState => {\n  if (event === \"pointerLeave\") return \"hiddenByPointerLeave\";\n  if (event === \"pointerEnter\") return state;\n  if (event === \"restoreClick\") return \"visible\";\n  if (event === \"hideToggle\") return \"hiddenByToggle\";\n  if (event === \"showToggle\") return \"visible\";\n  if (event === \"enterFullscreen\") return state === \"visible\" ? \"visible\" : \"hiddenByFullscreen\";\n  if (event === \"exitFullscreen\") return state === \"hiddenByFullscreen\" ? \"visible\" : state;\n  return state;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmap-ui-state.ts","s":145,"e":203,"m":1778504167974.0461,"t":"export const reduceMindmapChromeVisibility = (\n  state: MindmapChromeVisibilityState,\n  event: MindmapChromeVisibilityEvent,\n): MindmapChromeVisibilityState => {\n  if (event === \"pointerLeave\") return \"hiddenByPointerLeave\";\n  if (event === \"pointerEnter\") return state;\n  if (event === \"restoreClick\") return \"visible\";\n  if (event === \"hideToggle\") return \"hiddenByToggle\";\n  if (event === \"showToggle\") return \"visible\";\n  if (event === \"enterFullscreen\") return state === \"visible\" ? \"visible\" : \"hiddenByFullscreen\";\n  if (event === \"exitFullscreen\") return state === \"hiddenByFullscreen\" ? \"visible\" : state;\n  return state;\n};\n\nexport const deriveMindmapUiState = (input: MindmapUiStateInput): MindmapUiState => {\n  const activeNodeId = normalizeActiveNodeId(input.activeNodeId);\n  const capabilities = input.runtimeCapabilities ? new Set<MindmapRuntimeCapability>(input.runtimeCapabilities) : null;\n  const disabledActions = {} as Record<MindmapUiActionId, boolean>;\n  const shell = createDefaultMindmapShellInteractionState({\n    ...input.shell,\n    navigator: {\n      ...input.shell?.navigator,\n      readonly: input.readonly,\n    },\n  });\n\n  allActionIds().forEach((actionId) => {\n    const mapping = getMindmapActionMapping(actionId);\n    if (!mapping) {\n      disabledActions[actionId] = true;\n      return;\n    }\n    if (mapping.requiresActiveNode && !activeNodeId) {\n      disabledActions[actionId] = true;\n      return;\n    }\n    if (input.readonly && !mapping.readonlyAllowed) {\n      disabledActions[actionId] = true;\n      return;\n    }\n    if (capabilities && !capabilities.has(mapping.target)) {\n      disabledActions[actionId] = true;\n      return;\n    }\n    if (unsupportedActionIds.has(actionId)) {\n      disabledActions[actionId] = true;\n      return;\n    }\n    disabledActions[actionId] = false;\n  });\n\n  return {\n    activeNodeId,\n    readonly: input.readonly,\n    disabledActions,\n    shell,\n  };\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapLocalStore.ts","s":1,"e":60,"m":1778395128227.066,"t":"import \"server-only\";\n\nimport path from \"path\";\nimport { promises as fs } from \"fs\";\nimport { getDocumentsBaseDir, getLegacyMindmapsBaseDir } from \"@/lib/server/local-paths\";\n\nexport function resolveMindmapFileName(mindmapId: string) {\n  if (mindmapId === \"legacy\" || mindmapId.startsWith(\"legacy-\")) {\n    return \"mindmap.json\";\n  }\n  return `mindmap-${mindmapId}.json`;\n}\n\nasync function ensureDir(dir: string) {\n  await fs.mkdir(dir, { recursive: true });\n}\n\nasync function ensureIndexFile(folder: string, title = \"无标题\") {\n  const indexFile = path.join(folder, \"index.md\");\n  try {\n    await fs.access(indexFile);\n  } catch {\n    await fs.writeFile(indexFile, `# ${title}\\n`, \"utf8\");\n  }\n}\n\nasync function tryReadJson(file: string) {\n  try {\n    const content = await fs.readFile(file, \"utf8\");\n    return JSON.parse(content);\n  } catch {\n    return null;\n  }\n}\n\nexport type ReadMindmapResult =\n  | { ok: true; data: any; source: \"preferred\" | \"legacy\" }\n  | { ok: false; data: null; source: null };\n\nexport async function readMindmapLocal(docId: string, mindmapId: string): Promise<ReadMindmapResult> {\n  const preferredBaseDir = getDocumentsBaseDir();\n  const legacyBaseDir = getLegacyMindmapsBaseDir();\n  const preferredFolder = path.join(preferredBaseDir, docId);\n  const preferredFile = path.join(preferredFolder, resolveMindmapFileName(mindmapId));\n  const preferredLegacy = path.join(preferredFolder, \"mindmap.json\");\n  const legacyDirFile =\n    path.basename(preferredFile) === \"mindmap.json\"\n      ? path.join(legacyBaseDir, docId, \"mindmap.json\")\n      : null;\n\n  const data =\n    (await tryReadJson(preferredFile)) ??\n    (await tryReadJson(preferredLegacy)) ??\n    (legacyDirFile ? await tryReadJson(legacyDirFile) : null);\n\n  if (data) return { ok: true, data, source: \"preferred\" };\n  return { ok: false, data: null, source: null };\n}\n\nexport async function writeMindmapLocal(","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapLocalStore.ts","s":49,"e":73,"m":1778395128227.066,"t":": null;\n\n  const data =\n    (await tryReadJson(preferredFile)) ??\n    (await tryReadJson(preferredLegacy)) ??\n    (legacyDirFile ? await tryReadJson(legacyDirFile) : null);\n\n  if (data) return { ok: true, data, source: \"preferred\" };\n  return { ok: false, data: null, source: null };\n}\n\nexport async function writeMindmapLocal(\n  docId: string,\n  mindmapId: string,\n  data: unknown,\n  docTitle: string,\n) {\n  const folder = path.join(getDocumentsBaseDir(), docId);\n  const file = path.join(folder, resolveMindmapFileName(mindmapId));\n  await ensureDir(folder);\n  await ensureIndexFile(folder, docTitle || \"无标题\");\n  await fs.writeFile(file, JSON.stringify(data ?? { data: { text: \"中心主题\" }, children: [] }, null, 2), \"utf8\");\n  return { folder, file };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapOps.ts","s":1,"e":60,"m":1776252513515.9155,"t":"import \"server-only\";\n\nexport type NodeRef = {\n  kind: \"pdf\" | \"docx\" | \"pptx\" | \"url\";\n  assetId?: string;\n  fileUrl?: string;\n  page?: number; // 1-based\n  slide?: number; // 1-based\n  title?: string;\n  snippet?: string;\n};\n\nexport type MindmapNodeData = {\n  uid?: string;\n  text?: string;\n  hyperlink?: string;\n  note?: string;\n  refs?: NodeRef[];\n  [k: string]: unknown;\n};\n\nexport type MindmapTreeNode = {\n  data: MindmapNodeData;\n  children?: MindmapTreeNode[];\n  [k: string]: unknown;\n};\n\nexport type MindmapOp =\n  | {\n      op: \"addChild\";\n      parentUid: string;\n      node: { uid?: string; text: string; hyperlink?: string; refs?: NodeRef[]; note?: string };\n    }\n  | {\n      op: \"addSiblingAfter\";\n      targetUid: string;\n      node: { uid?: string; text: string; hyperlink?: string; refs?: NodeRef[]; note?: string };\n    }\n  | { op: \"updateText\"; uid: string; text: string }\n  | { op: \"setHyperlink\"; uid: string; hyperlink: string | null }\n  | { op: \"setRefs\"; uid: string; refs: NodeRef[] }\n  | { op: \"appendNote\"; uid: string; markdown: string }\n  | { op: \"deleteNode\"; uid: string };\n\nexport type MindmapOpErrorKind = \"rejected\" | \"failed\";\n\nconst createUid = () => {\n  const anyCrypto = globalThis.crypto as unknown as { randomUUID?: () => string } | undefined;\n  if (anyCrypto?.randomUUID) return anyCrypto.randomUUID();\n  return Math.random().toString(36).slice(2);\n};\n\nexport const ensureMindmapUids = (root: MindmapTreeNode) => {\n  const walk = (node: MindmapTreeNode) => {\n    if (!node.data) node.data = {};\n    if (!node.data.uid) node.data.uid = createUid();\n    if (typeof node.data.text !== \"string\") node.data.text = String(node.data.text ?? \"新节点\");\n    if (!Array.isArray(node.children)) node.children = [];\n    node.children.forEach(walk);\n  };","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapOps.ts","s":49,"e":108,"m":1776252513515.9155,"t":"if (anyCrypto?.randomUUID) return anyCrypto.randomUUID();\n  return Math.random().toString(36).slice(2);\n};\n\nexport const ensureMindmapUids = (root: MindmapTreeNode) => {\n  const walk = (node: MindmapTreeNode) => {\n    if (!node.data) node.data = {};\n    if (!node.data.uid) node.data.uid = createUid();\n    if (typeof node.data.text !== \"string\") node.data.text = String(node.data.text ?? \"新节点\");\n    if (!Array.isArray(node.children)) node.children = [];\n    node.children.forEach(walk);\n  };\n  walk(root);\n  return root;\n};\n\ntype Indexed = {\n  node: MindmapTreeNode;\n  parent: MindmapTreeNode | null;\n  index: number;\n};\n\nconst buildIndex = (root: MindmapTreeNode) => {\n  const map = new Map<string, Indexed>();\n  const walk = (node: MindmapTreeNode, parent: MindmapTreeNode | null) => {\n    const uid = String(node?.data?.uid || \"\");\n    if (uid) map.set(uid, { node, parent, index: -1 });\n    const children = Array.isArray(node.children) ? node.children : [];\n    children.forEach((child, idx) => {\n      const cuid = String(child?.data?.uid || \"\");\n      if (cuid) map.set(cuid, { node: child, parent: node, index: idx });\n      walk(child, node);\n    });\n  };\n  walk(root, null);\n  return map;\n};\n\nconst safeUrlOrNull = (value: unknown) => {\n  const s = typeof value === \"string\" ? value.trim() : \"\";\n  if (!s) return null;\n  try {\n    const u = new URL(s);\n    if (u.protocol !== \"http:\" && u.protocol !== \"https:\") return null;\n    return u.toString();\n  } catch {\n    return null;\n  }\n};\n\nexport const classifyMindmapOpErrors = (errors: string[]): MindmapOpErrorKind | null => {\n  if (!Array.isArray(errors) || errors.length === 0) return null;\n  const rejectedPatterns = [\n    \"无效 op\",\n    \"找不到\",\n    \"不能删除根节点\",\n    \"不支持的 op\",\n  ];\n  const rejectedOnly = errors.every((error) =>\n    rejectedPatterns.some((pattern) => String(error || \"\").includes(pattern)),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapOps.ts","s":97,"e":156,"m":1776252513515.9155,"t":"};\n\nexport const classifyMindmapOpErrors = (errors: string[]): MindmapOpErrorKind | null => {\n  if (!Array.isArray(errors) || errors.length === 0) return null;\n  const rejectedPatterns = [\n    \"无效 op\",\n    \"找不到\",\n    \"不能删除根节点\",\n    \"不支持的 op\",\n  ];\n  const rejectedOnly = errors.every((error) =>\n    rejectedPatterns.some((pattern) => String(error || \"\").includes(pattern)),\n  );\n  return rejectedOnly ? \"rejected\" : \"failed\";\n};\n\nexport const applyMindmapOps = (\n  raw: unknown,\n  ops: MindmapOp[],\n): { data: MindmapTreeNode; applied: number; errors: string[] } => {\n  const root = (raw && typeof raw === \"object\" ? (raw as MindmapTreeNode) : null) ?? {\n    data: { text: \"中心主题\" },\n    children: [],\n  };\n  ensureMindmapUids(root);\n\n  const errors: string[] = [];\n  let applied = 0;\n\n  for (const op of ops) {\n    ensureMindmapUids(root);\n    const index = buildIndex(root);\n\n    if (!op || typeof op !== \"object\" || !(\"op\" in op)) {\n      errors.push(\"无效 op\");\n      continue;\n    }\n\n    if (op.op === \"addChild\") {\n      const parent = index.get(op.parentUid)?.node ?? null;\n      if (!parent) {\n        errors.push(`addChild: 找不到 parentUid=${op.parentUid}`);\n        continue;\n      }\n      if (!Array.isArray(parent.children)) parent.children = [];\n      const uid = op.node.uid || createUid();\n      parent.children.push({\n        data: {\n          uid,\n          text: String(op.node.text ?? \"新节点\"),\n          ...(op.node.note ? { note: String(op.node.note) } : {}),\n          ...(op.node.refs ? { refs: op.node.refs } : {}),\n          ...(op.node.hyperlink ? { hyperlink: safeUrlOrNull(op.node.hyperlink) ?? undefined } : {}),\n        },\n        children: [],\n      });\n      applied += 1;\n      continue;\n    }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapOps.ts","s":145,"e":204,"m":1776252513515.9155,"t":"uid,\n          text: String(op.node.text ?? \"新节点\"),\n          ...(op.node.note ? { note: String(op.node.note) } : {}),\n          ...(op.node.refs ? { refs: op.node.refs } : {}),\n          ...(op.node.hyperlink ? { hyperlink: safeUrlOrNull(op.node.hyperlink) ?? undefined } : {}),\n        },\n        children: [],\n      });\n      applied += 1;\n      continue;\n    }\n\n    if (op.op === \"addSiblingAfter\") {\n      const hit = index.get(op.targetUid);\n      if (!hit?.parent) {\n        errors.push(`addSiblingAfter: 找不到 targetUid=${op.targetUid} 或无父节点（不能对根节点加同级）`);\n        continue;\n      }\n      if (!Array.isArray(hit.parent.children)) hit.parent.children = [];\n      const uid = op.node.uid || createUid();\n      const insertAt = Math.max(0, Math.min(hit.parent.children.length, hit.index + 1));\n      hit.parent.children.splice(insertAt, 0, {\n        data: {\n          uid,\n          text: String(op.node.text ?? \"新节点\"),\n          ...(op.node.note ? { note: String(op.node.note) } : {}),\n          ...(op.node.refs ? { refs: op.node.refs } : {}),\n          ...(op.node.hyperlink ? { hyperlink: safeUrlOrNull(op.node.hyperlink) ?? undefined } : {}),\n        },\n        children: [],\n      });\n      applied += 1;\n      continue;\n    }\n\n    if (op.op === \"updateText\") {\n      const hit = index.get(op.uid)?.node ?? null;\n      if (!hit) {\n        errors.push(`updateText: 找不到 uid=${op.uid}`);\n        continue;\n      }\n      hit.data = hit.data || {};\n      hit.data.text = String(op.text ?? \"\");\n      applied += 1;\n      continue;\n    }\n\n    if (op.op === \"setHyperlink\") {\n      const hit = index.get(op.uid)?.node ?? null;\n      if (!hit) {\n        errors.push(`setHyperlink: 找不到 uid=${op.uid}`);\n        continue;\n      }\n      hit.data = hit.data || {};\n      const url = safeUrlOrNull(op.hyperlink);\n      if (!url) {\n        delete (hit.data as any).hyperlink;\n      } else {\n        hit.data.hyperlink = url;\n      }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapOps.ts","s":193,"e":252,"m":1776252513515.9155,"t":"const hit = index.get(op.uid)?.node ?? null;\n      if (!hit) {\n        errors.push(`setHyperlink: 找不到 uid=${op.uid}`);\n        continue;\n      }\n      hit.data = hit.data || {};\n      const url = safeUrlOrNull(op.hyperlink);\n      if (!url) {\n        delete (hit.data as any).hyperlink;\n      } else {\n        hit.data.hyperlink = url;\n      }\n      applied += 1;\n      continue;\n    }\n\n    if (op.op === \"setRefs\") {\n      const hit = index.get(op.uid)?.node ?? null;\n      if (!hit) {\n        errors.push(`setRefs: 找不到 uid=${op.uid}`);\n        continue;\n      }\n      hit.data = hit.data || {};\n      hit.data.refs = Array.isArray(op.refs) ? op.refs : [];\n      applied += 1;\n      continue;\n    }\n\n    if (op.op === \"appendNote\") {\n      const hit = index.get(op.uid)?.node ?? null;\n      if (!hit) {\n        errors.push(`appendNote: 找不到 uid=${op.uid}`);\n        continue;\n      }\n      hit.data = hit.data || {};\n      const prev = typeof hit.data.note === \"string\" ? hit.data.note : \"\";\n      const next = String(op.markdown ?? \"\");\n      hit.data.note = prev ? `${prev}\\n\\n${next}` : next;\n      applied += 1;\n      continue;\n    }\n\n    if (op.op === \"deleteNode\") {\n      const hit = index.get(op.uid);\n      if (!hit) {\n        errors.push(`deleteNode: 找不到 uid=${op.uid}`);\n        continue;\n      }\n      if (!hit.parent) {\n        errors.push(\"deleteNode: 不能删除根节点\");\n        continue;\n      }\n      if (!Array.isArray(hit.parent.children)) hit.parent.children = [];\n      hit.parent.children = hit.parent.children.filter((c) => String(c?.data?.uid || \"\") !== op.uid);\n      applied += 1;\n      continue;\n    }\n\n    errors.push(`不支持的 op: ${(op as any).op}`);\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapOps.ts","s":241,"e":257,"m":1776252513515.9155,"t":"if (!hit.parent) {\n        errors.push(\"deleteNode: 不能删除根节点\");\n        continue;\n      }\n      if (!Array.isArray(hit.parent.children)) hit.parent.children = [];\n      hit.parent.children = hit.parent.children.filter((c) => String(c?.data?.uid || \"\") !== op.uid);\n      applied += 1;\n      continue;\n    }\n\n    errors.push(`不支持的 op: ${(op as any).op}`);\n  }\n\n  ensureMindmapUids(root);\n  return { data: root, applied, errors };\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/mindmapRouteMeta.ts","s":1,"e":57,"m":1776156450198.092,"t":"import { randomUUID } from \"crypto\";\n\nexport type MindmapRouteMeta = {\n  requestId: string;\n  traceId: string;\n  workspaceId: string | null;\n  documentId: string;\n  pageId: string;\n  mindmapId: string;\n  attachmentId: string;\n  ownerUserId: string;\n  source: \"convex\";\n};\n\nfunction readHeaderValue(headerList: Headers, ...candidates: string[]): string | null {\n  for (const candidate of candidates) {\n    const value = headerList.get(candidate);\n    if (typeof value === \"string\" && value.trim()) {\n      return value.trim();\n    }\n  }\n  return null;\n}\n\nfunction makeFallbackId(prefix: string): string {\n  return `${prefix}_${randomUUID()}`;\n}\n\nexport function buildMindmapRouteMeta(\n  request: Request,\n  input: {\n    workspaceId?: string | null;\n    documentId: string;\n    mindmapId: string;\n    ownerUserId: string;\n  },\n): MindmapRouteMeta {\n  const requestId =\n    readHeaderValue(request.headers, \"x-request-id\", \"x-mnote-request-id\") ??\n    makeFallbackId(\"req\");\n  const traceId =\n    readHeaderValue(request.headers, \"x-trace-id\", \"x-mnote-trace-id\", \"x-request-id\") ??\n    makeFallbackId(\"trace\");\n\n  return {\n    requestId,\n    traceId,\n    workspaceId: input.workspaceId?.trim() || null,\n    documentId: input.documentId,\n    pageId: input.documentId,\n    mindmapId: input.mindmapId,\n    attachmentId: input.mindmapId,\n    ownerUserId: input.ownerUserId,\n    source: \"convex\",\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.test.ts","s":1,"e":60,"m":1778497754330.9927,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  SIMPLE_MIND_MAP_BRIDGE_EVENTS,\n  SIMPLE_MIND_MAP_REQUIRED_PLUGINS,\n  buildSimpleMindMapOptions,\n  createFallbackMindmapAdapterProjection,\n  createSafeMindmapCommandExecutor,\n  defaultSimpleMindMapPluginNames,\n} from \"./simple-mind-map-bridge\";\n\ndescribe(\"simple-mind-map bridge contract\", () => {\n  it(\"登记 Phase 6 第一阶段必需插件和事件\", () => {\n    expect(SIMPLE_MIND_MAP_REQUIRED_PLUGINS).toEqual([\n      \"Drag\",\n      \"KeyboardNavigation\",\n      \"Export\",\n      \"Select\",\n      \"AssociativeLine\",\n      \"Search\",\n      \"OuterFrame\",\n    ]);\n    expect(defaultSimpleMindMapPluginNames()).toEqual(\n      expect.arrayContaining([\"Scrollbar\", \"MiniMap\", \"Painter\", \"Formula\"]),\n    );\n    expect(defaultSimpleMindMapPluginNames()).not.toContain(\"RichText\");\n    expect(SIMPLE_MIND_MAP_BRIDGE_EVENTS).toEqual([\n      \"data_change\",\n      \"view_data_change\",\n      \"node_active\",\n      \"back_forward\",\n      \"scale\",\n      \"translate\",\n    ]);\n  });\n\n  it(\"从 adapter projection 构建 simple-mind-map 初始化参数\", () => {\n    const projection = createFallbackMindmapAdapterProjection({\n      mindmapId: \"mind_1\",\n      root: { data: { text: \"KMIND\" }, children: [] },\n      kernelRevision: 7,\n    });\n    const options = buildSimpleMindMapOptions({\n      el: {} as HTMLElement,\n      projection: {\n        ...projection,\n        theme: { template: \"classic4\" },\n        themeConfig: { root: { fillColor: \"#c62828\" } },\n        view: { state: { scale: 0.8, x: 1, y: 2 }, transform: { scaleX: 0.8, scaleY: 0.8 } },\n      },\n    });\n\n    expect(options.data).toEqual({ data: { text: \"KMIND\" }, children: [] });\n    expect(options.layout).toBe(\"logicalStructure\");\n    expect(options.theme).toBe(\"classic4\");\n    expect(options.themeConfig).toEqual({ root: { fillColor: \"#c62828\" } });\n    expect(options.viewData).toEqual({ state: { scale: 0.8, x: 1, y: 2 }, transform: { scaleX: 0.8, scaleY: 0.8 } });\n  });\n\n  it(\"fallback projection 默认使用 KMind 风格主题与右向逻辑结构\", () => {\n    const projection = createFallbackMindmapAdapterProjection({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.test.ts","s":49,"e":108,"m":1778497754330.9927,"t":"},\n    });\n\n    expect(options.data).toEqual({ data: { text: \"KMIND\" }, children: [] });\n    expect(options.layout).toBe(\"logicalStructure\");\n    expect(options.theme).toBe(\"classic4\");\n    expect(options.themeConfig).toEqual({ root: { fillColor: \"#c62828\" } });\n    expect(options.viewData).toEqual({ state: { scale: 0.8, x: 1, y: 2 }, transform: { scaleX: 0.8, scaleY: 0.8 } });\n  });\n\n  it(\"fallback projection 默认使用 KMind 风格主题与右向逻辑结构\", () => {\n    const projection = createFallbackMindmapAdapterProjection({\n      mindmapId: \"mind_1\",\n      root: { data: { text: \"KMIND\" }, children: [] },\n    });\n\n    expect(projection.layout).toBe(\"logicalStructure\");\n    expect(projection.theme).toBe(\"default\");\n    expect(projection.themeConfig).toMatchObject({\n      lineStyle: \"curve\",\n      root: {\n        fillColor: \"#e25563\",\n        color: \"#ffffff\",\n        fontWeight: \"bold\",\n      },\n      second: {\n        fillColor: \"#4f7df3\",\n        color: \"#ffffff\",\n      },\n      node: {\n        color: \"#315aa9\",\n      },\n      generalizationLineColor: \"#ef6a5b\",\n    });\n  });\n\n  it(\"保留 runtimeOptions 中的 fit 设置，避免初始导图被裁切\", () => {\n    const projection = createFallbackMindmapAdapterProjection({\n      mindmapId: \"mind_1\",\n      root: { data: { text: \"KMIND\" }, children: [] },\n      kernelRevision: 7,\n    });\n\n    expect(\n      buildSimpleMindMapOptions({\n        el: {} as HTMLElement,\n        projection,\n      }).fit,\n    ).toBe(true);\n    expect(\n      buildSimpleMindMapOptions({\n        el: {} as HTMLElement,\n        projection,\n        runtimeOptions: { fit: false },\n      }).fit,\n    ).toBe(false);\n  });\n\n  it(\"忽略无效 viewData，避免 simple-mind-map 读取 viewData.state 时崩溃\", () => {\n    const projection = createFallbackMindmapAdapterProjection({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.test.ts","s":97,"e":136,"m":1778497754330.9927,"t":").toBe(true);\n    expect(\n      buildSimpleMindMapOptions({\n        el: {} as HTMLElement,\n        projection,\n        runtimeOptions: { fit: false },\n      }).fit,\n    ).toBe(false);\n  });\n\n  it(\"忽略无效 viewData，避免 simple-mind-map 读取 viewData.state 时崩溃\", () => {\n    const projection = createFallbackMindmapAdapterProjection({\n      mindmapId: \"mind_1\",\n      root: { data: { text: \"KMIND\" }, children: [] },\n      kernelRevision: 7,\n    });\n    const options = buildSimpleMindMapOptions({\n      el: {} as HTMLElement,\n      projection: { ...projection, view: {} },\n    });\n\n    expect(options.viewData).toBeNull();\n  });\n\n  it(\"安全命令包装只允许受控 runtime command\", () => {\n    const calls: unknown[][] = [];\n    const exec = createSafeMindmapCommandExecutor({\n      execCommand: (...args) => calls.push(args),\n    });\n\n    expect(exec(\"INSERT_CHILD_NODE\", \"node_1\")).toMatchObject({ ok: true });\n    expect(exec(\"SAVE_FULL_BLOB\", { data: {} })).toEqual({\n      ok: false,\n      command: \"SAVE_FULL_BLOB\",\n      error: \"unsupported_command\",\n    });\n    expect(calls).toEqual([[\"INSERT_CHILD_NODE\", \"node_1\"]]);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.ts","s":1,"e":60,"m":1778649500089.8625,"t":"import {\n  canonicalizeMindmapData,\n  createDefaultMindmapThemeConfig,\n  DEFAULT_MINDMAP_LAYOUT,\n  DEFAULT_MINDMAP_THEME,\n  defaultMindmapData,\n  type MindMapData,\n} from \"./mindmap-projection\";\n\nexport type MindmapAdapterProjection = {\n  schema: \"mnote.mindmap.simple_mind_map_scene.v1\";\n  runtime: \"simple-mind-map\" | string;\n  root: unknown;\n  layout?: unknown;\n  theme?: unknown;\n  themeConfig?: unknown;\n  view?: unknown;\n  config?: unknown;\n  compatPayload?: unknown;\n  kernelRevision: number;\n};\n\nexport type SimpleMindMapInstance = {\n  execCommand?: (command: string, ...args: unknown[]) => unknown;\n  destroy?: () => void;\n  getData?: (withConfig?: boolean) => unknown;\n  getLayout?: () => unknown;\n  getTheme?: () => unknown;\n  getThemeConfig?: (prop?: unknown) => unknown;\n  on?: (event: string, handler: (...args: unknown[]) => void) => void;\n  off?: (event: string, handler: (...args: unknown[]) => void) => void;\n  setData?: (data: unknown) => void;\n  setLayout?: (layout: unknown, notRender?: boolean) => void;\n  setMode?: (mode: \"edit\" | \"readonly\") => void;\n  setTheme?: (theme: unknown, notRender?: boolean) => void;\n  setThemeConfig?: (config: Record<string, unknown>, notRender?: boolean) => void;\n  updateData?: (data: unknown) => void;\n  updateConfig?: (config?: Record<string, unknown>) => void;\n  view?: {\n    scale?: number;\n    enlarge?: () => void;\n    narrow?: () => void;\n    getTransformData?: () => unknown;\n    setScale?: (scale: number, cx: number, cy: number) => void;\n    fit?: () => void;\n  };\n  renderer?: {\n    setRootNodeCenter?: () => void;\n    textEdit?: {\n      hideEditTextBox?: () => void;\n      isShowTextEdit?: () => boolean;\n    };\n    clearActiveNodeList?: () => void;\n    addNodeToActiveList?: (node: unknown, skipBeforeEvent?: boolean) => void;\n    emitNodeActiveEvent?: (node?: unknown, activeNodeList?: unknown[]) => void;\n    findNodeByUid?: (uid: string) => unknown;\n    activeNodeList?: unknown[];\n    lastActiveNodeList?: unknown[];\n    root?: unknown;\n    renderTree?: { _node?: unknown };","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.ts","s":49,"e":108,"m":1778649500089.8625,"t":"textEdit?: {\n      hideEditTextBox?: () => void;\n      isShowTextEdit?: () => boolean;\n    };\n    clearActiveNodeList?: () => void;\n    addNodeToActiveList?: (node: unknown, skipBeforeEvent?: boolean) => void;\n    emitNodeActiveEvent?: (node?: unknown, activeNodeList?: unknown[]) => void;\n    findNodeByUid?: (uid: string) => unknown;\n    activeNodeList?: unknown[];\n    lastActiveNodeList?: unknown[];\n    root?: unknown;\n    renderTree?: { _node?: unknown };\n  };\n};\n\nexport type SimpleMindMapCtor = new (options: Record<string, unknown>) => SimpleMindMapInstance;\n\nexport type SimpleMindMapRuntime = {\n  MindMap: SimpleMindMapCtor;\n  registeredPlugins: string[];\n};\n\nexport type SimpleMindMapBridgeEventName =\n  | \"data_change\"\n  | \"view_data_change\"\n  | \"node_active\"\n  | \"back_forward\"\n  | \"scale\"\n  | \"translate\";\n\nexport type SimpleMindMapBridgeEvent = {\n  type: SimpleMindMapBridgeEventName;\n  args: unknown[];\n  snapshot?: unknown;\n  kernelRevision: number;\n};\n\nexport type SimpleMindMapBridge = {\n  instance: SimpleMindMapInstance;\n  execCommand: (command: string, ...args: unknown[]) => SimpleMindMapSafeCommandResult;\n  getSnapshot: () => unknown;\n  refreshProjection?: (reason?: string) => Promise<void>;\n  destroy: () => void;\n};\n\nexport type SimpleMindMapSafeCommandResult =\n  | { ok: true; command: string; result: unknown }\n  | { ok: false; command: string; error: \"unsupported_command\" | \"runtime_unavailable\" | \"command_failed\" };\n\nexport const SIMPLE_MIND_MAP_REQUIRED_PLUGINS = [\n  \"Drag\",\n  \"KeyboardNavigation\",\n  \"Export\",\n  \"Select\",\n  \"AssociativeLine\",\n  \"Search\",\n  \"OuterFrame\",\n] as const;\n\nexport const SIMPLE_MIND_MAP_OPTIONAL_PLUGINS = [","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.ts","s":97,"e":156,"m":1778649500089.8625,"t":"export const SIMPLE_MIND_MAP_REQUIRED_PLUGINS = [\n  \"Drag\",\n  \"KeyboardNavigation\",\n  \"Export\",\n  \"Select\",\n  \"AssociativeLine\",\n  \"Search\",\n  \"OuterFrame\",\n] as const;\n\nexport const SIMPLE_MIND_MAP_OPTIONAL_PLUGINS = [\n  // RichText 会把纯文本节点转成 HTML 字符串；Phase 6.1 先保护 kernel 文本语义。\n  \"Scrollbar\",\n  \"MiniMap\",\n  \"Painter\",\n  \"Formula\",\n] as const;\n\nexport const SIMPLE_MIND_MAP_BRIDGE_EVENTS: SimpleMindMapBridgeEventName[] = [\n  \"data_change\",\n  \"view_data_change\",\n  \"node_active\",\n  \"back_forward\",\n  \"scale\",\n  \"translate\",\n];\n\nexport const SIMPLE_MIND_MAP_SAFE_COMMANDS = new Set([\n  \"BACK\",\n  \"FORWARD\",\n  \"INSERT_NODE\",\n  \"INSERT_CHILD_NODE\",\n  \"REMOVE_NODE\",\n  \"DELETE_NODE\",\n  \"ADD_GENERALIZATION\",\n  \"ADD_OUTER_FRAME\",\n  \"SET_NOTATION\",\n]);\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst readString = (value: unknown, fallback: string): string => {\n  if (typeof value === \"string\" && value.trim()) return value.trim();\n  if (isRecord(value)) {\n    const template = value.template;\n    if (typeof template === \"string\" && template.trim()) return template.trim();\n  }\n  return fallback;\n};\n\nconst readRecord = (value: unknown): Record<string, unknown> => {\n  if (isRecord(value)) return value;\n  return {};\n};\n\nconst readThemeConfig = (value: unknown): Record<string, unknown> => {\n  if (!isRecord(value) || Object.keys(value).length === 0) return createDefaultMindmapThemeConfig();\n  return value;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.ts","s":145,"e":204,"m":1778649500089.8625,"t":"}\n  return fallback;\n};\n\nconst readRecord = (value: unknown): Record<string, unknown> => {\n  if (isRecord(value)) return value;\n  return {};\n};\n\nconst readThemeConfig = (value: unknown): Record<string, unknown> => {\n  if (!isRecord(value) || Object.keys(value).length === 0) return createDefaultMindmapThemeConfig();\n  return value;\n};\n\nconst readViewData = (value: unknown): Record<string, unknown> | null => {\n  if (!isRecord(value) || !isRecord(value.state)) return null;\n  return {\n    ...value,\n    state: value.state,\n    transform: isRecord(value.transform) ? value.transform : {},\n  };\n};\n\nexport const buildSimpleMindMapOptions = (input: {\n  el: HTMLElement;\n  projection: MindmapAdapterProjection;\n  runtimeOptions?: Record<string, unknown>;\n}): Record<string, unknown> => {\n  const root = canonicalizeMindmapData(input.projection.root ?? defaultMindmapData);\n  const config = readRecord(input.projection.config);\n  const runtimeOptions = readRecord(input.runtimeOptions);\n  return {\n    ...config,\n    ...runtimeOptions,\n    el: input.el,\n    data: root,\n    fit:\n      typeof runtimeOptions.fit === \"boolean\"\n        ? runtimeOptions.fit\n        : typeof config.fit === \"boolean\"\n          ? config.fit\n          : true,\n    layout: readString(input.projection.layout, DEFAULT_MINDMAP_LAYOUT),\n    theme: readString(input.projection.theme, DEFAULT_MINDMAP_THEME),\n    themeConfig: readThemeConfig(input.projection.themeConfig),\n    viewData: readViewData(input.projection.view),\n    initRootNodePosition: [\"center\", \"center\"],\n  };\n};\n\nconst pluginImporters = {\n  Drag: () => import(\"simple-mind-map/src/plugins/Drag.js\"),\n  KeyboardNavigation: () => import(\"simple-mind-map/src/plugins/KeyboardNavigation.js\"),\n  Export: () => import(\"simple-mind-map/src/plugins/Export.js\"),\n  Select: () => import(\"simple-mind-map/src/plugins/Select.js\"),\n  AssociativeLine: () => import(\"simple-mind-map/src/plugins/AssociativeLine.js\"),\n  Search: () => import(\"simple-mind-map/src/plugins/Search.js\"),\n  OuterFrame: () => import(\"simple-mind-map/src/plugins/OuterFrame.js\"),\n  Scrollbar: () => import(\"simple-mind-map/src/plugins/Scrollbar.js\"),\n  MiniMap: () => import(\"simple-mind-map/src/plugins/MiniMap.js\"),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.ts","s":193,"e":252,"m":1778649500089.8625,"t":"};\n\nconst pluginImporters = {\n  Drag: () => import(\"simple-mind-map/src/plugins/Drag.js\"),\n  KeyboardNavigation: () => import(\"simple-mind-map/src/plugins/KeyboardNavigation.js\"),\n  Export: () => import(\"simple-mind-map/src/plugins/Export.js\"),\n  Select: () => import(\"simple-mind-map/src/plugins/Select.js\"),\n  AssociativeLine: () => import(\"simple-mind-map/src/plugins/AssociativeLine.js\"),\n  Search: () => import(\"simple-mind-map/src/plugins/Search.js\"),\n  OuterFrame: () => import(\"simple-mind-map/src/plugins/OuterFrame.js\"),\n  Scrollbar: () => import(\"simple-mind-map/src/plugins/Scrollbar.js\"),\n  MiniMap: () => import(\"simple-mind-map/src/plugins/MiniMap.js\"),\n  Painter: () => import(\"simple-mind-map/src/plugins/Painter.js\"),\n  Formula: () => import(\"simple-mind-map/src/plugins/Formula.js\"),\n} satisfies Record<string, () => Promise<{ default: unknown }>>;\n\nexport type SimpleMindMapPluginName = keyof typeof pluginImporters;\n\nexport const defaultSimpleMindMapPluginNames = (): SimpleMindMapPluginName[] => [\n  ...SIMPLE_MIND_MAP_REQUIRED_PLUGINS,\n  ...SIMPLE_MIND_MAP_OPTIONAL_PLUGINS,\n];\n\nexport const loadSimpleMindMapRuntime = async (\n  pluginNames: SimpleMindMapPluginName[] = defaultSimpleMindMapPluginNames(),\n): Promise<SimpleMindMapRuntime> => {\n  if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n    throw new Error(\"simple_mind_map_browser_required\");\n  }\n\n  const [{ default: MindMap }, pluginModules] = await Promise.all([\n    import(\"simple-mind-map\"),\n    Promise.all(pluginNames.map(async (name) => [name, (await pluginImporters[name]()).default] as const)),\n  ]);\n\n  const MindMapCtor = MindMap as unknown as SimpleMindMapCtor & {\n    hasPlugin?: (plugin: unknown) => number;\n    usePlugin?: (plugin: unknown) => unknown;\n  };\n\n  const registeredPlugins: string[] = [];\n  pluginModules.forEach(([name, plugin]) => {\n    if (!plugin || typeof MindMapCtor.usePlugin !== \"function\") return;\n    const registered =\n      typeof MindMapCtor.hasPlugin === \"function\" ? MindMapCtor.hasPlugin(plugin) !== -1 : false;\n    if (!registered) {\n      MindMapCtor.usePlugin(plugin);\n    }\n    registeredPlugins.push(name);\n  });\n\n  return {\n    MindMap: MindMapCtor,\n    registeredPlugins,\n  };\n};\n\nexport const createSafeMindmapCommandExecutor =\n  (instance: SimpleMindMapInstance) =>\n  (command: string, ...args: unknown[]): SimpleMindMapSafeCommandResult => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.ts","s":241,"e":300,"m":1778649500089.8625,"t":"registeredPlugins.push(name);\n  });\n\n  return {\n    MindMap: MindMapCtor,\n    registeredPlugins,\n  };\n};\n\nexport const createSafeMindmapCommandExecutor =\n  (instance: SimpleMindMapInstance) =>\n  (command: string, ...args: unknown[]): SimpleMindMapSafeCommandResult => {\n    if (!SIMPLE_MIND_MAP_SAFE_COMMANDS.has(command)) {\n      return { ok: false, command, error: \"unsupported_command\" };\n    }\n    if (typeof instance.execCommand !== \"function\") {\n      return { ok: false, command, error: \"runtime_unavailable\" };\n    }\n    try {\n      return { ok: true, command, result: instance.execCommand(command, ...args) };\n    } catch {\n      return { ok: false, command, error: \"command_failed\" };\n    }\n  };\n\nexport const attachSimpleMindMapEventListeners = (input: {\n  instance: SimpleMindMapInstance;\n  projection: MindmapAdapterProjection;\n  onEvent?: (event: SimpleMindMapBridgeEvent) => void;\n}): (() => void) => {\n  const cleanup: Array<() => void> = [];\n  SIMPLE_MIND_MAP_BRIDGE_EVENTS.forEach((eventName) => {\n    const handler = (...args: unknown[]) => {\n      const snapshot =\n        eventName === \"data_change\"\n          ? input.instance.getData?.()\n          : eventName === \"view_data_change\"\n          ? input.instance.getData?.(true) ?? input.instance.getData?.()\n          : undefined;\n      input.onEvent?.({\n        type: eventName,\n        args,\n        snapshot,\n        kernelRevision: input.projection.kernelRevision,\n      });\n    };\n    input.instance.on?.(eventName, handler);\n    cleanup.push(() => input.instance.off?.(eventName, handler));\n  });\n  return () => cleanup.splice(0).forEach((dispose) => dispose());\n};\n\nexport const createSimpleMindMapBridge = async (input: {\n  el: HTMLElement;\n  projection: MindmapAdapterProjection;\n  pluginNames?: SimpleMindMapPluginName[];\n  runtimeOptions?: Record<string, unknown>;\n  mode?: \"edit\" | \"readonly\";\n  onEvent?: (event: SimpleMindMapBridgeEvent) => void;\n}): Promise<SimpleMindMapBridge> => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/mindmap/simple-mind-map-bridge.ts","s":289,"e":347,"m":1778649500089.8625,"t":"});\n  return () => cleanup.splice(0).forEach((dispose) => dispose());\n};\n\nexport const createSimpleMindMapBridge = async (input: {\n  el: HTMLElement;\n  projection: MindmapAdapterProjection;\n  pluginNames?: SimpleMindMapPluginName[];\n  runtimeOptions?: Record<string, unknown>;\n  mode?: \"edit\" | \"readonly\";\n  onEvent?: (event: SimpleMindMapBridgeEvent) => void;\n}): Promise<SimpleMindMapBridge> => {\n  const runtime = await loadSimpleMindMapRuntime(input.pluginNames);\n  const themeConfig = readThemeConfig(input.projection.themeConfig);\n  const options = buildSimpleMindMapOptions({\n    el: input.el,\n    projection: input.projection,\n    runtimeOptions: input.runtimeOptions,\n  });\n  const instance = new runtime.MindMap(options);\n  instance.setThemeConfig?.(themeConfig);\n  if (input.mode) instance.setMode?.(input.mode);\n\n  const detachEvents = attachSimpleMindMapEventListeners({\n    instance,\n    projection: input.projection,\n    onEvent: input.onEvent,\n  });\n  const execCommand = createSafeMindmapCommandExecutor(instance);\n\n  return {\n    instance,\n    execCommand,\n    getSnapshot: () => instance.getData?.(true) ?? instance.getData?.(),\n    destroy: () => {\n      instance.renderer?.textEdit?.hideEditTextBox?.();\n      detachEvents();\n      instance.destroy?.();\n    },\n  };\n};\n\nexport const createFallbackMindmapAdapterProjection = (input: {\n  mindmapId: string;\n  root?: MindMapData;\n  kernelRevision?: number;\n}): MindmapAdapterProjection => ({\n  schema: \"mnote.mindmap.simple_mind_map_scene.v1\",\n  runtime: \"simple-mind-map\",\n  root: input.root ?? defaultMindmapData,\n  layout: DEFAULT_MINDMAP_LAYOUT,\n  theme: DEFAULT_MINDMAP_THEME,\n  themeConfig: createDefaultMindmapThemeConfig(),\n  view: {},\n  config: {},\n  compatPayload: { source: \"frontend-fallback\", mindmapId: input.mindmapId },\n  kernelRevision: input.kernelRevision ?? 1,\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/asset.test.ts","s":1,"e":34,"m":1773495900906.8613,"t":"import { describe, expect, test } from \"vitest\";\nimport type { MediaAsset } from \"@/types/media\";\nimport { isRealFileAsset, parseSupabaseStorageObjectUrl } from \"./asset\";\n\ndescribe(\"file-tree/asset\", () => {\n  test(\"parseSupabaseStorageObjectUrl\", () => {\n    expect(\n      parseSupabaseStorageObjectUrl(\n        \"https://xxx.supabase.co/storage/v1/object/public/documents/a/b/c.txt\",\n      ),\n    ).toEqual({ bucket: \"documents\", path: \"a/b/c.txt\" });\n    expect(\n      parseSupabaseStorageObjectUrl(\n        \"https://xxx.supabase.co/storage/v1/object/sign/documents/a/b/c.txt?token=abc\",\n      ),\n    ).toEqual({ bucket: \"documents\", path: \"a/b/c.txt\" });\n    expect(parseSupabaseStorageObjectUrl(\"/documents/123\")).toBe(null);\n  });\n\n  test(\"isRealFileAsset\", () => {\n    const base = { id: \"1\" } as MediaAsset;\n    expect(isRealFileAsset({ ...base, storage_path: \"a/b\", file_url: null } as any)).toBe(true);\n    expect(isRealFileAsset({ ...base, storage_path: null, file_url: \"/documents/123\" } as any)).toBe(false);\n    expect(\n      isRealFileAsset({\n        ...base,\n        storage_path: null,\n        file_url: \"https://xxx.supabase.co/storage/v1/object/sign/documents/a/b/c.txt?token=abc\",\n      } as any),\n    ).toBe(true);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/asset.ts","s":1,"e":28,"m":1773495900912.8616,"t":"import type { MediaAsset } from \"@/types/media\";\n\nexport function parseSupabaseStorageObjectUrl(\n  fileUrl: string,\n): { bucket: string; path: string } | null {\n  try {\n    const url = new URL(fileUrl);\n    const segments = url.pathname.split(\"/\").filter(Boolean);\n    const objectIdx = segments.findIndex((seg) => seg === \"object\");\n    if (objectIdx === -1 || objectIdx + 2 >= segments.length) return null;\n    const mode = segments[objectIdx + 1];\n    if (mode !== \"public\" && mode !== \"sign\") return null;\n    const bucket = segments[objectIdx + 2];\n    const p = segments.slice(objectIdx + 3).join(\"/\");\n    return p ? { bucket, path: p } : null;\n  } catch {\n    return null;\n  }\n}\n\nexport function isRealFileAsset(asset: MediaAsset): boolean {\n  if (Boolean(asset.storage_path)) return true;\n  const url = asset.file_url ?? \"\";\n  if (!url.startsWith(\"http\")) return false;\n  return parseSupabaseStorageObjectUrl(url) !== null;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/clipboard.test.ts","s":1,"e":58,"m":1778799362804.9617,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  clearFileTreeClipboardPayload,\n  decodeFileTreeClipboardPayload,\n  encodeFileTreeClipboardPayload,\n  inferPasteTargetDocId,\n  isTextInputTarget,\n} from \"./clipboard\";\n\ndescribe(\"file-tree clipboard payload\", () => {\n  it(\"可编码/解码\", () => {\n    const payload = { type: \"mnote-file-tree\" as const, version: 1 as const, action: \"copy\" as const, rowIds: [\"doc:a\", \"asset:x\"] };\n    const text = encodeFileTreeClipboardPayload(payload);\n    expect(decodeFileTreeClipboardPayload(text)).toEqual(payload);\n    const cutPayload = { ...payload, action: \"cut\" as const };\n    expect(decodeFileTreeClipboardPayload(encodeFileTreeClipboardPayload(cutPayload))).toEqual(cutPayload);\n    expect(\n      decodeFileTreeClipboardPayload(\n        encodeFileTreeClipboardPayload({ ...payload, action: \"move\" as any }),\n      ),\n    ).toBeNull();\n    expect(decodeFileTreeClipboardPayload(\"not-a-payload\")).toBeNull();\n  });\n\n  it(\"可清空内存剪贴板\", async () => {\n    await clearFileTreeClipboardPayload();\n  });\n});\n\ndescribe(\"inferPasteTargetDocId\", () => {\n  it(\"focused 为 doc/index/asset 三分支覆盖\", () => {\n    const rowById = new Map<string, any>();\n    rowById.set(\"asset:x\", { kind: \"asset\", rowId: \"asset:x\", docId: \"d1\", asset: { id: \"x\" } });\n    expect(inferPasteTargetDocId({ focusedRowId: \"doc:d2\", rowById, activeDocId: \"active\" })).toBe(\"d2\");\n    expect(inferPasteTargetDocId({ focusedRowId: \"index:d3\", rowById, activeDocId: \"active\" })).toBe(\"d3\");\n    expect(inferPasteTargetDocId({ focusedRowId: \"asset:x\", rowById, activeDocId: \"active\" })).toBe(\"d1\");\n  });\n\n  it(\"无 focused 时回退 activeDocId\", () => {\n    const rowById = new Map<string, any>();\n    expect(inferPasteTargetDocId({ focusedRowId: null, rowById, activeDocId: \"active\" })).toBe(\"active\");\n    expect(inferPasteTargetDocId({ focusedRowId: null, rowById, activeDocId: null })).toBeNull();\n  });\n});\n\ndescribe(\"isTextInputTarget\", () => {\n  it(\"input/textarea/contenteditable 不拦截快捷键\", () => {\n    const input = document.createElement(\"input\");\n    const textarea = document.createElement(\"textarea\");\n    const div = document.createElement(\"div\");\n    div.setAttribute(\"contenteditable\", \"true\");\n    expect(isTextInputTarget(input)).toBe(true);\n    expect(isTextInputTarget(textarea)).toBe(true);\n    expect(isTextInputTarget(div)).toBe(true);\n    expect(isTextInputTarget(document.createElement(\"button\"))).toBe(false);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/clipboard.ts","s":1,"e":60,"m":1778799222408.1265,"t":"\"use client\";\n\nimport type { FileTreeRow } from \"./types\";\nimport { parseFileTreeRowId } from \"./types\";\n\nexport type FileTreeClipboardAction = \"copy\" | \"cut\";\n\nexport type FileTreeClipboardPayloadV1 = {\n  type: \"mnote-file-tree\";\n  version: 1;\n  action: FileTreeClipboardAction;\n  rowIds: string[];\n};\n\nconst PREFIX = \"mnote-file-tree-clipboard:v1:\";\n\nlet memoryClipboardText: string | null = null;\n\nfunction encodeBase64(text: string): string {\n  const bytes = new TextEncoder().encode(text);\n  let binary = \"\";\n  bytes.forEach((b) => {\n    binary += String.fromCharCode(b);\n  });\n  return btoa(binary);\n}\n\nfunction decodeBase64(base64: string): string {\n  const binary = atob(base64);\n  const bytes = new Uint8Array(binary.length);\n  for (let i = 0; i < binary.length; i += 1) {\n    bytes[i] = binary.charCodeAt(i);\n  }\n  return new TextDecoder().decode(bytes);\n}\n\nexport function encodeFileTreeClipboardPayload(payload: FileTreeClipboardPayloadV1): string {\n  return `${PREFIX}${encodeBase64(JSON.stringify(payload))}`;\n}\n\nexport function decodeFileTreeClipboardPayload(text: string): FileTreeClipboardPayloadV1 | null {\n  if (!text || !text.startsWith(PREFIX)) return null;\n  const base64 = text.slice(PREFIX.length);\n  try {\n    const raw = decodeBase64(base64);\n    const parsed = JSON.parse(raw) as Partial<FileTreeClipboardPayloadV1>;\n    if (parsed?.type !== \"mnote-file-tree\" || parsed.version !== 1) return null;\n    if (parsed.action !== \"copy\" && parsed.action !== \"cut\") return null;\n    if (!Array.isArray(parsed.rowIds) || parsed.rowIds.some((id) => typeof id !== \"string\")) return null;\n    return parsed as FileTreeClipboardPayloadV1;\n  } catch {\n    return null;\n  }\n}\n\nexport async function writeFileTreeClipboardPayload(payload: FileTreeClipboardPayloadV1): Promise<void> {\n  const text = encodeFileTreeClipboardPayload(payload);\n  memoryClipboardText = text;\n  if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n    try {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/clipboard.ts","s":49,"e":108,"m":1778799222408.1265,"t":"if (!Array.isArray(parsed.rowIds) || parsed.rowIds.some((id) => typeof id !== \"string\")) return null;\n    return parsed as FileTreeClipboardPayloadV1;\n  } catch {\n    return null;\n  }\n}\n\nexport async function writeFileTreeClipboardPayload(payload: FileTreeClipboardPayloadV1): Promise<void> {\n  const text = encodeFileTreeClipboardPayload(payload);\n  memoryClipboardText = text;\n  if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n    try {\n      await navigator.clipboard.writeText(text);\n    } catch {\n      // ignore, fallback to memory\n    }\n  }\n}\n\nexport async function readFileTreeClipboardPayload(): Promise<FileTreeClipboardPayloadV1 | null> {\n  let text: string | null = memoryClipboardText;\n  if (typeof navigator !== \"undefined\" && navigator.clipboard?.readText) {\n    try {\n      text = await navigator.clipboard.readText();\n    } catch {\n      // ignore, fallback to memory\n    }\n  }\n  if (!text) return null;\n  return decodeFileTreeClipboardPayload(text);\n}\n\nexport async function clearFileTreeClipboardPayload(): Promise<void> {\n  memoryClipboardText = null;\n  if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n    try {\n      await navigator.clipboard.writeText(\"\");\n    } catch {\n      // ignore, memory fallback 已清空\n    }\n  }\n}\n\nexport function isTextInputTarget(target: EventTarget | null): boolean {\n  const el = target as HTMLElement | null;\n  if (!el) return false;\n  if (el.isContentEditable) return true;\n  const contentEditable = el.getAttribute?.(\"contenteditable\");\n  if (contentEditable && contentEditable.toLowerCase() !== \"false\") {\n    return true;\n  }\n  const tag = el.tagName?.toLowerCase();\n  return tag === \"input\" || tag === \"textarea\" || el.getAttribute?.(\"role\") === \"textbox\";\n}\n\nexport function inferPasteTargetDocId({\n  focusedRowId,\n  rowById,\n  activeDocId,\n}: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/clipboard.ts","s":97,"e":128,"m":1778799222408.1265,"t":"if (contentEditable && contentEditable.toLowerCase() !== \"false\") {\n    return true;\n  }\n  const tag = el.tagName?.toLowerCase();\n  return tag === \"input\" || tag === \"textarea\" || el.getAttribute?.(\"role\") === \"textbox\";\n}\n\nexport function inferPasteTargetDocId({\n  focusedRowId,\n  rowById,\n  activeDocId,\n}: {\n  focusedRowId: string | null;\n  rowById: Map<string, FileTreeRow>;\n  activeDocId: string | null;\n}): string | null {\n  if (focusedRowId) {\n    const parsed = parseFileTreeRowId(focusedRowId);\n    if (parsed?.kind === \"doc\") return parsed.docId;\n    if (parsed?.kind === \"index\") return parsed.docId;\n    if (parsed?.kind === \"asset-folder\") {\n      const row = rowById.get(focusedRowId);\n      return row?.kind === \"asset-folder\" ? row.docId : null;\n    }\n    if (parsed?.kind === \"asset\") {\n      const row = rowById.get(focusedRowId);\n      return row?.kind === \"asset\" ? row.docId : null;\n    }\n  }\n  return activeDocId || null;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/delete.test.ts","s":1,"e":60,"m":1773495900933.8618,"t":"import { describe, expect, it } from \"vitest\";\nimport type { FileTreeRow } from \"@/lib/file-tree/types\";\nimport { buildParentById } from \"./dnd\";\nimport { computeFileTreeDeleteTargets } from \"./delete\";\n\nfunction makeDoc(id: string, parent_id: string | null): any {\n  return {\n    id,\n    parent_id,\n    title: id,\n    access_scope: \"private\",\n    icon: null,\n    cover: null,\n    is_template: false,\n    user_id: \"u1\",\n    workspace_id: \"w1\",\n    created_at: \"\",\n    updated_at: \"\",\n    children: [],\n  };\n}\n\ndescribe(\"computeFileTreeDeleteTargets\", () => {\n  it(\"去掉被父级页面覆盖的子页面\", () => {\n    const docA = makeDoc(\"A\", null);\n    const docB = makeDoc(\"B\", \"A\");\n    const rows: FileTreeRow[] = [\n      { rowId: \"doc:A\", kind: \"doc\", docId: \"A\", parentDocId: null, node: docA, depth: 0, hasChildren: true, isExpanded: true },\n      { rowId: \"index:A\", kind: \"index\", docId: \"A\", parentDocId: \"A\", node: docA, depth: 1 },\n      { rowId: \"doc:B\", kind: \"doc\", docId: \"B\", parentDocId: \"A\", node: docB, depth: 1, hasChildren: false, isExpanded: false },\n      { rowId: \"index:B\", kind: \"index\", docId: \"B\", parentDocId: \"B\", node: docB, depth: 2 },\n    ];\n    const parentById = buildParentById([\n      { id: \"A\", parentId: null },\n      { id: \"B\", parentId: \"A\" },\n    ]);\n\n    const selectedRowIds = new Set([\"doc:A\", \"doc:B\"]);\n    const result = computeFileTreeDeleteTargets({ visibleRows: rows, selectedRowIds, parentById });\n    expect(result.docIds).toEqual([\"A\"]);\n  });\n\n  it(\"选中 index 行等价于选中页面本身（去重）\", () => {\n    const docA = makeDoc(\"A\", null);\n    const rows: FileTreeRow[] = [\n      { rowId: \"doc:A\", kind: \"doc\", docId: \"A\", parentDocId: null, node: docA, depth: 0, hasChildren: false, isExpanded: false },\n      { rowId: \"index:A\", kind: \"index\", docId: \"A\", parentDocId: \"A\", node: docA, depth: 1 },\n    ];\n    const parentById = buildParentById([{ id: \"A\", parentId: null }]);\n    const selectedRowIds = new Set([\"doc:A\", \"index:A\"]);\n    const result = computeFileTreeDeleteTargets({ visibleRows: rows, selectedRowIds, parentById });\n    expect(result.docIds).toEqual([\"A\"]);\n  });\n\n  it(\"如果页面被删除，则跳过同页面下的附件删除（避免重复/无效操作）\", () => {\n    const docA = makeDoc(\"A\", null);\n    const rows: FileTreeRow[] = [\n      { rowId: \"doc:A\", kind: \"doc\", docId: \"A\", parentDocId: null, node: docA, depth: 0, hasChildren: true, isExpanded: true },\n      { rowId: \"index:A\", kind: \"index\", docId: \"A\", parentDocId: \"A\", node: docA, depth: 1 },\n      {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/delete.test.ts","s":49,"e":95,"m":1773495900933.8618,"t":"const parentById = buildParentById([{ id: \"A\", parentId: null }]);\n    const selectedRowIds = new Set([\"doc:A\", \"index:A\"]);\n    const result = computeFileTreeDeleteTargets({ visibleRows: rows, selectedRowIds, parentById });\n    expect(result.docIds).toEqual([\"A\"]);\n  });\n\n  it(\"如果页面被删除，则跳过同页面下的附件删除（避免重复/无效操作）\", () => {\n    const docA = makeDoc(\"A\", null);\n    const rows: FileTreeRow[] = [\n      { rowId: \"doc:A\", kind: \"doc\", docId: \"A\", parentDocId: null, node: docA, depth: 0, hasChildren: true, isExpanded: true },\n      { rowId: \"index:A\", kind: \"index\", docId: \"A\", parentDocId: \"A\", node: docA, depth: 1 },\n      {\n        rowId: \"asset:1\",\n        kind: \"asset\",\n        docId: \"A\",\n        parentDocId: \"A\",\n        asset: {\n          id: \"1\",\n          workspace_id: \"w1\",\n          document_id: \"A\",\n          asset_type: \"file\",\n          file_url: \"https://example.com/1\",\n          thumbnail_url: null,\n          bucket: \"b\",\n          storage_path: \"p\",\n          file_name: \"a.txt\",\n          file_size: 1,\n          mime_type: \"text/plain\",\n          ocr_payload: undefined,\n          ocr_strategy: null,\n          ocr_text: null,\n          ocr_status: null,\n          signed_url: null,\n          created_at: \"\",\n          updated_at: \"\",\n        },\n        depth: 2,\n      },\n    ];\n    const parentById = buildParentById([{ id: \"A\", parentId: null }]);\n    const selectedRowIds = new Set([\"doc:A\", \"asset:1\"]);\n    const result = computeFileTreeDeleteTargets({ visibleRows: rows, selectedRowIds, parentById });\n    expect(result.docIds).toEqual([\"A\"]);\n    expect(result.assetIds).toEqual([]);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/delete.ts","s":1,"e":51,"m":1773495900943.2424,"t":"\"use client\";\n\nimport type { FileTreeRow } from \"@/lib/file-tree/types\";\nimport { filterTopLevelDocIds } from \"./dnd\";\n\nexport type FileTreeDeleteTargets = {\n  docIds: string[];\n  assetIds: string[];\n};\n\nexport function computeFileTreeDeleteTargets(args: {\n  visibleRows: FileTreeRow[];\n  selectedRowIds: Set<string>;\n  parentById: Map<string, string | null>;\n}): FileTreeDeleteTargets {\n  const { visibleRows, selectedRowIds, parentById } = args;\n\n  const docCandidates: string[] = [];\n  const assetCandidates: string[] = [];\n  const assetDocIdByAssetId = new Map<string, string>();\n\n  for (const row of visibleRows) {\n    if (!selectedRowIds.has(row.rowId)) continue;\n\n    if (row.kind === \"doc\" || row.kind === \"index\") {\n      docCandidates.push(row.docId);\n      continue;\n    }\n\n    if (row.kind === \"asset\" || row.kind === \"asset-folder\") {\n      assetCandidates.push(row.asset.id);\n      assetDocIdByAssetId.set(row.asset.id, row.docId);\n    }\n  }\n\n  const docIds = filterTopLevelDocIds(docCandidates, parentById);\n  const docIdSet = new Set(docIds);\n\n  const seenAssets = new Set<string>();\n  const assetIds: string[] = [];\n  for (const assetId of assetCandidates) {\n    if (seenAssets.has(assetId)) continue;\n    seenAssets.add(assetId);\n    const docId = assetDocIdByAssetId.get(assetId);\n    if (docId && docIdSet.has(docId)) continue;\n    assetIds.push(assetId);\n  }\n\n  return { docIds, assetIds };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/dnd.test.ts","s":1,"e":47,"m":1773495900948.8618,"t":"import { describe, expect, test } from \"vitest\";\nimport type { FileTreeRow } from \"@/lib/file-tree/types\";\nimport { buildParentById, filterTopLevelDocIds, inferDropTargetDocId, isInvalidDocDrop } from \"./dnd\";\n\ndescribe(\"file-tree/dnd\", () => {\n  test(\"inferDropTargetDocId\", () => {\n    const docRow: FileTreeRow = {\n      kind: \"doc\",\n      rowId: \"doc:a\",\n      docId: \"a\",\n      parentDocId: null,\n      depth: 0,\n      isExpanded: false,\n      hasChildren: false,\n      node: {} as any,\n    };\n    const indexRow: FileTreeRow = { kind: \"index\", rowId: \"index:a\", docId: \"a\", parentDocId: \"a\", depth: 1, node: {} as any };\n    const assetRow: FileTreeRow = { kind: \"asset\", rowId: \"asset:x\", docId: \"a\", parentDocId: \"a\", depth: 1, asset: {} as any };\n    expect(inferDropTargetDocId(docRow)).toBe(\"a\");\n    expect(inferDropTargetDocId(indexRow)).toBe(\"a\");\n    expect(inferDropTargetDocId(assetRow)).toBe(\"a\");\n    expect(inferDropTargetDocId(null)).toBe(null);\n  });\n\n  test(\"filterTopLevelDocIds removes descendants\", () => {\n    const parentById = buildParentById([\n      { id: \"a\", parentId: null },\n      { id: \"b\", parentId: \"a\" },\n      { id: \"c\", parentId: \"b\" },\n      { id: \"d\", parentId: null },\n    ]);\n    expect(filterTopLevelDocIds([\"b\", \"a\", \"c\", \"d\"], parentById)).toEqual([\"a\", \"d\"]);\n    expect(filterTopLevelDocIds([\"b\", \"c\"], parentById)).toEqual([\"b\"]);\n  });\n\n  test(\"isInvalidDocDrop blocks self/descendant\", () => {\n    const parentById = buildParentById([\n      { id: \"a\", parentId: null },\n      { id: \"b\", parentId: \"a\" },\n      { id: \"c\", parentId: \"b\" },\n    ]);\n    expect(isInvalidDocDrop({ sourceDocIds: [\"a\"], targetParentId: \"a\", parentById })).toBe(true);\n    expect(isInvalidDocDrop({ sourceDocIds: [\"a\"], targetParentId: \"b\", parentById })).toBe(true);\n    expect(isInvalidDocDrop({ sourceDocIds: [\"b\"], targetParentId: \"a\", parentById })).toBe(false);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/dnd.ts","s":1,"e":60,"m":1773495900954.862,"t":"import type { FileTreeRow } from \"@/lib/file-tree/types\";\n\nexport function inferDropTargetDocId(targetRow: FileTreeRow | null): string | null {\n  if (!targetRow) return null;\n  return targetRow.docId ?? null;\n}\n\nexport type ParentEdge = { id: string; parentId: string | null };\n\nexport function buildParentById(edges: ParentEdge[]): Map<string, string | null> {\n  const map = new Map<string, string | null>();\n  edges.forEach((edge) => {\n    map.set(edge.id, edge.parentId ?? null);\n  });\n  return map;\n}\n\nexport function isAncestorOf(\n  ancestorId: string,\n  nodeId: string,\n  parentById: Map<string, string | null>,\n): boolean {\n  let current: string | null | undefined = nodeId;\n  while (current) {\n    const parent = parentById.get(current);\n    if (!parent) return false;\n    if (parent === ancestorId) return true;\n    current = parent;\n  }\n  return false;\n}\n\nexport function filterTopLevelDocIds(\n  docIds: string[],\n  parentById: Map<string, string | null>,\n): string[] {\n  const unique = Array.from(new Set(docIds));\n  const selected = new Set(unique);\n  return unique.filter((id) => {\n    let current: string | null | undefined = id;\n    while (current) {\n      const parent = parentById.get(current);\n      if (!parent) return true;\n      if (selected.has(parent)) return false;\n      current = parent;\n    }\n    return true;\n  });\n}\n\nexport function isInvalidDocDrop(args: {\n  sourceDocIds: string[];\n  targetParentId: string | null;\n  parentById: Map<string, string | null>;\n}): boolean {\n  const { sourceDocIds, targetParentId, parentById } = args;\n  if (!targetParentId) return false;\n  const sources = new Set(sourceDocIds);\n  if (sources.has(targetParentId)) return true;\n  for (const sourceId of sources) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/dnd.ts","s":49,"e":66,"m":1773495900954.862,"t":"}\n\nexport function isInvalidDocDrop(args: {\n  sourceDocIds: string[];\n  targetParentId: string | null;\n  parentById: Map<string, string | null>;\n}): boolean {\n  const { sourceDocIds, targetParentId, parentById } = args;\n  if (!targetParentId) return false;\n  const sources = new Set(sourceDocIds);\n  if (sources.has(targetParentId)) return true;\n  for (const sourceId of sources) {\n    if (isAncestorOf(sourceId, targetParentId, parentById)) return true;\n  }\n  return false;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/naming.test.ts","s":1,"e":22,"m":1773495900962.862,"t":"import { describe, expect, test } from \"vitest\";\nimport { makeUniqueFileName, makeUniqueTitle } from \"./naming\";\n\ndescribe(\"file-tree/naming\", () => {\n  test(\"makeUniqueTitle\", () => {\n    const existing = new Set<string>([\"无标题\", \"无标题 副本\"]);\n    expect(makeUniqueTitle(\"无标题\", existing)).toBe(\"无标题 副本 2\");\n    expect(makeUniqueTitle(\"Hello\", existing)).toBe(\"Hello\");\n    expect(makeUniqueTitle(\"Hello\", existing)).toBe(\"Hello 副本\");\n  });\n\n  test(\"makeUniqueFileName keeps extension\", () => {\n    const existing = new Set<string>([\"a.txt\", \"a 副本.txt\"]);\n    expect(makeUniqueFileName(\"a.txt\", existing)).toBe(\"a 副本 2.txt\");\n    expect(makeUniqueFileName(\"图片.png\", existing)).toBe(\"图片.png\");\n    expect(makeUniqueFileName(\"图片.png\", existing)).toBe(\"图片 副本.png\");\n    expect(makeUniqueFileName(\"无扩展名\", existing)).toBe(\"无扩展名\");\n    expect(makeUniqueFileName(\"无扩展名\", existing)).toBe(\"无扩展名 副本\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/naming.ts","s":1,"e":57,"m":1773495900968.862,"t":"function splitExtension(fileName: string): { base: string; ext: string } {\n  const safe = fileName.trim();\n  const lastDot = safe.lastIndexOf(\".\");\n  if (lastDot <= 0 || lastDot === safe.length - 1) {\n    return { base: safe, ext: \"\" };\n  }\n  return { base: safe.slice(0, lastDot), ext: safe.slice(lastDot) };\n}\n\nexport function makeUniqueTitle(baseTitle: string, existing: Set<string>): string {\n  const base = baseTitle.trim() || \"无标题\";\n  if (!existing.has(base)) {\n    existing.add(base);\n    return base;\n  }\n  const first = `${base} 副本`;\n  if (!existing.has(first)) {\n    existing.add(first);\n    return first;\n  }\n  for (let i = 2; i < 1000; i += 1) {\n    const candidate = `${base} 副本 ${i}`;\n    if (!existing.has(candidate)) {\n      existing.add(candidate);\n      return candidate;\n    }\n  }\n  const fallback = `${base} 副本 ${Date.now()}`;\n  existing.add(fallback);\n  return fallback;\n}\n\nexport function makeUniqueFileName(fileName: string, existing: Set<string>): string {\n  const safe = fileName.trim() || \"附件\";\n  if (!existing.has(safe)) {\n    existing.add(safe);\n    return safe;\n  }\n  const { base, ext } = splitExtension(safe);\n  const first = `${base} 副本${ext}`;\n  if (!existing.has(first)) {\n    existing.add(first);\n    return first;\n  }\n  for (let i = 2; i < 1000; i += 1) {\n    const candidate = `${base} 副本 ${i}${ext}`;\n    if (!existing.has(candidate)) {\n      existing.add(candidate);\n      return candidate;\n    }\n  }\n  const fallback = `${base} 副本 ${Date.now()}${ext}`;\n  existing.add(fallback);\n  return fallback;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/projection-client.test.ts","s":1,"e":60,"m":1777361951720.5408,"t":"import { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport {\n  FILE_TREE_SEARCH_MAX_RESULTS,\n  buildFileTreeProjectionSearchRequestMeta,\n  fetchKernelFileTreeProjection,\n} from \"./projection-client\";\n\ndescribe(\"fetchKernelFileTreeProjection\", () => {\n  beforeEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it(\"通过 3000 同源 file_tree projection endpoint 请求 Rust 搜索 projection\", async () => {\n    const fetchMock = vi.fn().mockResolvedValue(\n      new Response(\n        JSON.stringify({\n          ok: true,\n          result: {\n            projectionId: \"kernel_projection:file_tree:page_root\",\n            projection: \"file_tree\",\n            rootNodeId: \"page_root\",\n            items: [{ rowId: \"asset:table_1\" }],\n            edges: [],\n            meta: {\n              search: {\n                indexingVisibility: {\n                  schema: \"mnote.file_tree.indexing_visibility\",\n                  schemaVersion: 1,\n                  source: \"kernel.project_view\",\n                  status: \"visible\",\n                  requestKey: \"page_root:预算\",\n                  indexedResourceKinds: [\"document\", \"index\", \"asset\"],\n                  visibleResourceKinds: [\"document\", \"index\", \"asset\"],\n                  metrics: {\n                    visibleRows: 1,\n                    visibleEdges: 0,\n                  },\n                },\n              },\n            },\n          },\n        }),\n        { status: 200 },\n      ),\n    );\n    vi.stubGlobal(\"fetch\", fetchMock);\n\n    const result = await fetchKernelFileTreeProjection({\n      workspaceId: \"ws_1\",\n      rootNodeId: \"page_root\",\n      depth: 3,\n      query: \"  预算  \",\n      maxResults: 12,\n    });\n\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/tree/projections/file?workspaceId=ws_1&rootNodeId=page_root&depth=3&query=%E9%A2%84%E7%AE%97&maxResults=12\",\n      expect.objectContaining({\n        method: \"GET\",\n        credentials: \"include\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/projection-client.test.ts","s":49,"e":108,"m":1777361951720.5408,"t":"workspaceId: \"ws_1\",\n      rootNodeId: \"page_root\",\n      depth: 3,\n      query: \"  预算  \",\n      maxResults: 12,\n    });\n\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/tree/projections/file?workspaceId=ws_1&rootNodeId=page_root&depth=3&query=%E9%A2%84%E7%AE%97&maxResults=12\",\n      expect.objectContaining({\n        method: \"GET\",\n        credentials: \"include\",\n      }),\n    );\n    expect(result.items.map((item) => item.rowId)).toEqual([\"asset:table_1\"]);\n    expect(result.meta?.search?.indexingVisibility).toMatchObject({\n      schema: \"mnote.file_tree.indexing_visibility\",\n      status: \"visible\",\n      requestKey: \"page_root:预算\",\n    });\n  });\n\n  it(\"固定 file_tree 搜索语义边界：命中数先截断，祖先补全不计入 maxResults\", () => {\n    expect(FILE_TREE_SEARCH_MAX_RESULTS).toBe(80);\n    expect(\n      buildFileTreeProjectionSearchRequestMeta({\n        workspaceId: \"ws_1\",\n        query: \"  rust  \",\n        maxResults: 500,\n      }),\n    ).toEqual({\n      query: \"rust\",\n      maxResults: 80,\n      maxResultsRule: \"matches_only_before_ancestor_completion\",\n      ancestorCompletion: \"include_all_ancestors_after_match_truncation\",\n      ordering: \"kernel_file_tree_preorder\",\n      emptyStateText: \"没有匹配结果\",\n      asyncVisibility: {\n        source: \"kernel.project_view\",\n        requestKey: \"ws_1:rust\",\n      },\n      coveredResourceKinds: [\"index\", \"asset\", \"asset_folder\", \"mindmap\", \"book\", \"pdf\"],\n    });\n  });\n\n  it(\"搜索请求会把 maxResults 限制在 sidebar 使用的稳定上限内\", async () => {\n    const fetchMock = vi.fn().mockResolvedValue(\n      new Response(\n        JSON.stringify({\n          ok: true,\n          result: {\n            projectionId: \"kernel_projection:file_tree:root\",\n            projection: \"file_tree\",\n            rootNodeId: null,\n            items: [],\n            edges: [],\n          },\n        }),\n        { status: 200 },\n      ),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/projection-client.test.ts","s":97,"e":137,"m":1777361951720.5408,"t":"JSON.stringify({\n          ok: true,\n          result: {\n            projectionId: \"kernel_projection:file_tree:root\",\n            projection: \"file_tree\",\n            rootNodeId: null,\n            items: [],\n            edges: [],\n          },\n        }),\n        { status: 200 },\n      ),\n    );\n    vi.stubGlobal(\"fetch\", fetchMock);\n\n    await fetchKernelFileTreeProjection({\n      workspaceId: \"ws_1\",\n      query: \"rust\",\n      maxResults: 500,\n    });\n\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/tree/projections/file?workspaceId=ws_1&query=rust&maxResults=80\",\n      expect.anything(),\n    );\n  });\n\n  it(\"失败时透出服务端错误消息\", async () => {\n    vi.stubGlobal(\n      \"fetch\",\n      vi.fn().mockResolvedValue(\n        new Response(JSON.stringify({ error: \"获取 projection 失败\" }), { status: 502 }),\n      ),\n    );\n\n    await expect(fetchKernelFileTreeProjection({ workspaceId: \"ws_1\" })).rejects.toThrow(\n      \"获取 projection 失败\",\n    );\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/projection-client.ts","s":1,"e":60,"m":1777250215773.0188,"t":"import type { KernelFileTreeProjection } from \"@/lib/kernel-file-tree\";\n\nexport const FILE_TREE_SEARCH_MAX_RESULTS = 80;\nexport const FILE_TREE_SEARCH_EMPTY_STATE_TEXT = \"没有匹配结果\";\n\nexport type FileTreeProjectionSearchRequestMeta = {\n  query: string | null;\n  maxResults: number | null;\n  maxResultsRule: \"matches_only_before_ancestor_completion\";\n  ancestorCompletion: \"include_all_ancestors_after_match_truncation\";\n  ordering: \"kernel_file_tree_preorder\";\n  emptyStateText: typeof FILE_TREE_SEARCH_EMPTY_STATE_TEXT;\n  asyncVisibility: {\n    source: \"kernel.project_view\";\n    requestKey: string | null;\n  };\n  coveredResourceKinds: [\"index\", \"asset\", \"asset_folder\", \"mindmap\", \"book\", \"pdf\"];\n};\n\nexport type FetchKernelFileTreeProjectionInput = {\n  workspaceId: string;\n  rootNodeId?: string | null;\n  depth?: number | null;\n  query?: string | null;\n  maxResults?: number | null;\n};\n\nfunction normalizeQuery(value: string | null | undefined): string | null {\n  const query = value?.trim() ?? \"\";\n  return query.length > 0 ? query : null;\n}\n\nfunction normalizeMaxResults(value: number | null | undefined): number | null {\n  if (typeof value !== \"number\" || !Number.isFinite(value)) {\n    return null;\n  }\n  return Math.min(FILE_TREE_SEARCH_MAX_RESULTS, Math.max(1, Math.floor(value)));\n}\n\nexport function buildFileTreeProjectionSearchRequestMeta(\n  input: Pick<FetchKernelFileTreeProjectionInput, \"workspaceId\" | \"query\" | \"maxResults\">,\n): FileTreeProjectionSearchRequestMeta {\n  const query = normalizeQuery(input.query);\n  const maxResults = normalizeMaxResults(input.maxResults);\n  return {\n    query,\n    maxResults,\n    maxResultsRule: \"matches_only_before_ancestor_completion\",\n    ancestorCompletion: \"include_all_ancestors_after_match_truncation\",\n    ordering: \"kernel_file_tree_preorder\",\n    emptyStateText: FILE_TREE_SEARCH_EMPTY_STATE_TEXT,\n    asyncVisibility: {\n      source: \"kernel.project_view\",\n      requestKey: query ? `${input.workspaceId}:${query}` : null,\n    },\n    coveredResourceKinds: [\"index\", \"asset\", \"asset_folder\", \"mindmap\", \"book\", \"pdf\"],\n  };\n}\n\nexport async function fetchKernelFileTreeProjection(","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/projection-client.ts","s":49,"e":99,"m":1777250215773.0188,"t":"ancestorCompletion: \"include_all_ancestors_after_match_truncation\",\n    ordering: \"kernel_file_tree_preorder\",\n    emptyStateText: FILE_TREE_SEARCH_EMPTY_STATE_TEXT,\n    asyncVisibility: {\n      source: \"kernel.project_view\",\n      requestKey: query ? `${input.workspaceId}:${query}` : null,\n    },\n    coveredResourceKinds: [\"index\", \"asset\", \"asset_folder\", \"mindmap\", \"book\", \"pdf\"],\n  };\n}\n\nexport async function fetchKernelFileTreeProjection(\n  input: FetchKernelFileTreeProjectionInput,\n): Promise<KernelFileTreeProjection> {\n  const params = new URLSearchParams();\n  params.set(\"workspaceId\", input.workspaceId);\n  const rootNodeId = input.rootNodeId?.trim();\n  if (rootNodeId) {\n    params.set(\"rootNodeId\", rootNodeId);\n  }\n  if (typeof input.depth === \"number\" && Number.isFinite(input.depth)) {\n    params.set(\"depth\", String(input.depth));\n  }\n  const query = normalizeQuery(input.query);\n  if (query) {\n    params.set(\"query\", query);\n  }\n  const maxResults = normalizeMaxResults(input.maxResults);\n  if (maxResults != null) {\n    params.set(\"maxResults\", String(maxResults));\n  }\n\n  const response = await fetch(`/api/tree/projections/file?${params.toString()}`, {\n    method: \"GET\",\n    credentials: \"include\",\n  });\n\n  if (!response.ok) {\n    const payload = await response.json().catch(() => ({}));\n    const message =\n      typeof payload?.error === \"string\" ? payload.error : \"获取 file_tree projection 失败\";\n    throw new Error(message);\n  }\n\n  const payload = (await response.json()) as { result?: KernelFileTreeProjection };\n  if (!payload.result) {\n    throw new Error(\"file_tree projection 响应缺少 result\");\n  }\n  return payload.result;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.test.ts","s":1,"e":60,"m":1777173532465.9417,"t":"import { afterEach, describe, expect, it, vi } from \"vitest\";\nimport {\n  copyFileTreeResourceAssets,\n  deleteFileTreeResourceAssets,\n  preflightFileTreeDelete,\n  preflightFileTreeInternalDrop,\n  preflightFileTreePaste,\n  preflightFileTreeUploadTarget,\n  moveFileTreeResourceAssets,\n  renameFileTreeResourceAsset,\n  restoreFileTreeResourceAssets,\n  uploadFileTreeResourceAsset,\n} from \"./resource-command-client\";\n\ndescribe(\"file-tree resource command client\", () => {\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it(\"copy/move 应通过统一资源 command client 发送到 media batch route\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({ items: [{ id: \"asset_1\" }] }),\n    } as Response);\n\n    await copyFileTreeResourceAssets({\n      assetIds: [\"asset_1\", \"asset_1\", \" asset_2 \"],\n      targetDocumentId: \"doc_target\",\n      targetSubPath: \"mindmaps/mind_1\",\n    });\n    await moveFileTreeResourceAssets({\n      assetIds: [\"asset_3\"],\n      targetDocumentId: \"doc_target_2\",\n    });\n\n    expect(fetchMock).toHaveBeenCalledTimes(2);\n    expect(fetchMock).toHaveBeenNthCalledWith(\n      1,\n      \"/api/media/batch\",\n      expect.objectContaining({\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          action: \"copy\",\n          assetIds: [\"asset_1\", \"asset_2\"],\n          targetDocumentId: \"doc_target\",\n          targetSubPath: \"mindmaps/mind_1\",\n        }),\n      }),\n    );\n    expect(fetchMock).toHaveBeenNthCalledWith(\n      2,\n      \"/api/media/batch\",\n      expect.objectContaining({\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          action: \"move\",\n          assetIds: [\"asset_3\"],\n          targetDocumentId: \"doc_target_2\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.test.ts","s":49,"e":108,"m":1777173532465.9417,"t":"}),\n    );\n    expect(fetchMock).toHaveBeenNthCalledWith(\n      2,\n      \"/api/media/batch\",\n      expect.objectContaining({\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          action: \"move\",\n          assetIds: [\"asset_3\"],\n          targetDocumentId: \"doc_target_2\",\n        }),\n      }),\n    );\n  });\n\n  it(\"rename/delete/restore 也应复用同一 batch transport 边界\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({ ok: true }),\n    } as Response);\n\n    await renameFileTreeResourceAsset({ assetId: \"asset_1\", newName: \"新文件.pdf\" });\n    await deleteFileTreeResourceAssets([\"asset_1\", \"asset_2\"]);\n    await restoreFileTreeResourceAssets([\"asset_3\"]);\n\n    expect(fetchMock.mock.calls.map((call) => JSON.parse(String(call[1]?.body)))).toEqual([\n      { action: \"rename\", assetIds: [\"asset_1\"], newName: \"新文件.pdf\" },\n      { action: \"delete\", assetIds: [\"asset_1\", \"asset_2\"] },\n      { action: \"restore\", assetIds: [\"asset_3\"] },\n    ]);\n  });\n\n  it(\"后端返回错误时应抛出稳定 fallback 或服务端消息\", async () => {\n    vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: false,\n      json: async () => ({ error: \"Rust resource preflight rejected\" }),\n    } as Response);\n\n    await expect(\n      moveFileTreeResourceAssets({\n        assetIds: [\"asset_1\"],\n        targetDocumentId: \"doc_target\",\n      }),\n    ).rejects.toThrow(\"Rust resource preflight rejected\");\n  });\n\n  it(\"upload 应通过统一资源 command client 构造 FormData transport\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({ asset: { id: \"asset_upload_1\" } }),\n    } as Response);\n    const file = new File([\"content\"], \"demo.pdf\", { type: \"application/pdf\" });\n\n    await uploadFileTreeResourceAsset({\n      file,\n      workspaceId: \"ws_1\",\n      documentId: \"doc_1\",\n      mindmapId: \"mind_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.test.ts","s":97,"e":156,"m":1777173532465.9417,"t":"it(\"upload 应通过统一资源 command client 构造 FormData transport\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({ asset: { id: \"asset_upload_1\" } }),\n    } as Response);\n    const file = new File([\"content\"], \"demo.pdf\", { type: \"application/pdf\" });\n\n    await uploadFileTreeResourceAsset({\n      file,\n      workspaceId: \"ws_1\",\n      documentId: \"doc_1\",\n      mindmapId: \"mind_1\",\n    });\n\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/media/upload\",\n      expect.objectContaining({\n        method: \"POST\",\n        body: expect.any(FormData),\n      }),\n    );\n    const body = fetchMock.mock.calls[0]?.[1]?.body as FormData;\n    expect(body.get(\"file\")).toBe(file);\n    expect(body.get(\"workspaceId\")).toBe(\"ws_1\");\n    expect(body.get(\"documentId\")).toBe(\"doc_1\");\n    expect(body.get(\"mindmapId\")).toBe(\"mind_1\");\n  });\n\n  it(\"internal drop preflight 应发送到 tree filetree drop route 并返回 Rust plan\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({\n        plan: {\n          copy: false,\n          targetDocumentId: \"doc_target\",\n          targetMindmapId: null,\n          targetSubPath: null,\n          rowIds: [\"doc:doc_1\"],\n          docIds: [\"doc_1\"],\n          topLevelDocIds: [\"doc_1\"],\n          copyableAssetIds: [],\n          sourceAssetDocumentIds: [],\n          documentTransferPlan: {\n            action: \"move\",\n            targetParentId: \"doc_target\",\n            documentIds: [\"doc_1\"],\n            topLevelDocumentIds: [\"doc_1\"],\n            copyItems: [{ documentId: \"doc_1\", recursive: true }],\n          },\n          resourceTransferPlan: null,\n        },\n      }),\n    } as Response);\n\n    const plan = await preflightFileTreeInternalDrop({\n      workspaceId: \"ws_1\",\n      copy: false,\n      targetDocumentId: \"doc_target\",\n      targetRowId: null,\n      focusedRowId: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.test.ts","s":145,"e":204,"m":1777173532465.9417,"t":"},\n          resourceTransferPlan: null,\n        },\n      }),\n    } as Response);\n\n    const plan = await preflightFileTreeInternalDrop({\n      workspaceId: \"ws_1\",\n      copy: false,\n      targetDocumentId: \"doc_target\",\n      targetRowId: null,\n      focusedRowId: null,\n      activeDocumentId: null,\n      rowIds: [\"doc:doc_1\"],\n      rows: [],\n      documentParents: [],\n    });\n\n    expect(plan.topLevelDocIds).toEqual([\"doc_1\"]);\n    expect(plan.documentTransferPlan?.topLevelDocumentIds).toEqual([\"doc_1\"]);\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/tree/filetree/drop-preflight\",\n      expect.objectContaining({\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          workspaceId: \"ws_1\",\n          copy: false,\n          targetDocumentId: \"doc_target\",\n          targetRowId: null,\n          focusedRowId: null,\n          activeDocumentId: null,\n          rowIds: [\"doc:doc_1\"],\n          rows: [],\n          documentParents: [],\n        }),\n      }),\n    );\n  });\n\n  it(\"delete preflight 应发送到 tree filetree delete route 并返回 Rust plan\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({\n        plan: {\n          rowIds: [\"doc:doc_1\", \"asset:asset_1\"],\n          docIds: [\"doc_1\"],\n          assetIds: [\"asset_1\"],\n          assetDocumentIds: [\"doc_other\"],\n        },\n      }),\n    } as Response);\n\n    const plan = await preflightFileTreeDelete({\n      workspaceId: \"ws_1\",\n      rowIds: [\"doc:doc_1\", \"asset:asset_1\"],\n      rows: [],\n      documentParents: [],\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.test.ts","s":193,"e":252,"m":1777173532465.9417,"t":"assetDocumentIds: [\"doc_other\"],\n        },\n      }),\n    } as Response);\n\n    const plan = await preflightFileTreeDelete({\n      workspaceId: \"ws_1\",\n      rowIds: [\"doc:doc_1\", \"asset:asset_1\"],\n      rows: [],\n      documentParents: [],\n    });\n\n    expect(plan.docIds).toEqual([\"doc_1\"]);\n    expect(plan.assetIds).toEqual([\"asset_1\"]);\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/tree/filetree/delete-preflight\",\n      expect.objectContaining({\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          workspaceId: \"ws_1\",\n          rowIds: [\"doc:doc_1\", \"asset:asset_1\"],\n          rows: [],\n          documentParents: [],\n        }),\n      }),\n    );\n  });\n\n  it(\"paste preflight 应发送到 tree filetree paste route 并返回 Rust plan\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({\n        plan: {\n          targetDocumentId: \"doc_target\",\n          targetMindmapId: \"mind_1\",\n          targetSubPath: \"mindmaps/mind_1\",\n          rowIds: [\"index:doc_1\", \"asset:asset_1\"],\n          docItems: [{ documentId: \"doc_1\", recursive: false }],\n          copyableAssetIds: [\"asset_1\"],\n          resourceTransferPlan: {\n            action: \"copy\",\n            assetIds: [\"asset_1\"],\n            targetDocumentId: \"doc_target\",\n            targetSubPath: \"mindmaps/mind_1\",\n          },\n        },\n      }),\n    } as Response);\n\n    const plan = await preflightFileTreePaste({\n      workspaceId: \"ws_1\",\n      targetDocumentId: null,\n      focusedRowId: \"asset-folder:mind_1\",\n      activeDocumentId: \"doc_active\",\n      rowIds: [\"index:doc_1\", \"asset:asset_1\"],\n      rows: [],\n    });\n\n    expect(plan.docItems).toEqual([{ documentId: \"doc_1\", recursive: false }]);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.test.ts","s":241,"e":300,"m":1777173532465.9417,"t":"} as Response);\n\n    const plan = await preflightFileTreePaste({\n      workspaceId: \"ws_1\",\n      targetDocumentId: null,\n      focusedRowId: \"asset-folder:mind_1\",\n      activeDocumentId: \"doc_active\",\n      rowIds: [\"index:doc_1\", \"asset:asset_1\"],\n      rows: [],\n    });\n\n    expect(plan.docItems).toEqual([{ documentId: \"doc_1\", recursive: false }]);\n    expect(plan.resourceTransferPlan?.targetSubPath).toBe(\"mindmaps/mind_1\");\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/tree/filetree/paste-preflight\",\n      expect.objectContaining({\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          workspaceId: \"ws_1\",\n          targetDocumentId: null,\n          focusedRowId: \"asset-folder:mind_1\",\n          activeDocumentId: \"doc_active\",\n          rowIds: [\"index:doc_1\", \"asset:asset_1\"],\n          rows: [],\n        }),\n      }),\n    );\n  });\n\n  it(\"upload target preflight 应发送到 tree filetree upload-target route 并返回 Rust plan\", async () => {\n    const fetchMock = vi.spyOn(globalThis, \"fetch\").mockResolvedValue({\n      ok: true,\n      json: async () => ({\n        plan: {\n          workspaceId: \"ws_1\",\n          targetDocumentId: \"doc_target\",\n          targetMindmapId: \"mind_1\",\n          targetSubPath: \"mindmaps/mind_1\",\n        },\n      }),\n    } as Response);\n\n    const plan = await preflightFileTreeUploadTarget({\n      workspaceId: \"ws_fallback\",\n      targetDocumentId: null,\n      targetRowId: \"asset:asset_child_1\",\n      focusedRowId: null,\n      activeDocumentId: \"doc_active\",\n      rows: [],\n      documentWorkspaces: [{ documentId: \"doc_target\", workspaceId: \"ws_1\" }],\n    });\n\n    expect(plan).toEqual({\n      workspaceId: \"ws_1\",\n      targetDocumentId: \"doc_target\",\n      targetMindmapId: \"mind_1\",\n      targetSubPath: \"mindmaps/mind_1\",\n    });\n    expect(fetchMock).toHaveBeenCalledWith(","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.test.ts","s":289,"e":318,"m":1777173532465.9417,"t":"activeDocumentId: \"doc_active\",\n      rows: [],\n      documentWorkspaces: [{ documentId: \"doc_target\", workspaceId: \"ws_1\" }],\n    });\n\n    expect(plan).toEqual({\n      workspaceId: \"ws_1\",\n      targetDocumentId: \"doc_target\",\n      targetMindmapId: \"mind_1\",\n      targetSubPath: \"mindmaps/mind_1\",\n    });\n    expect(fetchMock).toHaveBeenCalledWith(\n      \"/api/tree/filetree/upload-target-preflight\",\n      expect.objectContaining({\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          workspaceId: \"ws_fallback\",\n          targetDocumentId: null,\n          targetRowId: \"asset:asset_child_1\",\n          focusedRowId: null,\n          activeDocumentId: \"doc_active\",\n          rows: [],\n          documentWorkspaces: [{ documentId: \"doc_target\", workspaceId: \"ws_1\" }],\n        }),\n      }),\n    );\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.ts","s":1,"e":60,"m":1778802816694.9639,"t":"\"use client\";\n\nimport type { MediaAsset } from \"@/types/media\";\nimport type {\n  FileTreeShellDeletePreflightPayload,\n  FileTreeShellInternalDropPreflightPayload,\n  FileTreeShellPastePreflightPayload,\n  FileTreeShellUploadTargetPreflightPayload,\n} from \"@/lib/file-tree/shell\";\n\ntype ResourceCommandAction = \"copy\" | \"move\" | \"rename\" | \"delete\" | \"restore\";\n\ntype ResourceCommandErrorPayload = {\n  error?: string;\n};\n\ntype ResourceCommandResponse = {\n  ok?: boolean;\n  items?: MediaAsset[];\n  asset?: MediaAsset;\n};\n\ntype TransferResourceAssetsInput = {\n  assetIds: readonly string[];\n  targetDocumentId: string;\n  targetSubPath?: string | null;\n};\n\ntype RenameResourceAssetInput = {\n  assetId: string;\n  newName: string;\n};\n\ntype UploadResourceAssetInput = {\n  file: File;\n  workspaceId: string;\n  documentId: string;\n  mindmapId?: string | null;\n};\n\nexport type FileTreeInternalDropPreflightPlan = {\n  allowed?: boolean;\n  blockedReason?: string | null;\n  requiresConfirmation?: boolean;\n  conflicts?: Array<{\n    rowKind: string;\n    sourceRowId: string;\n    sourceDocumentId: string | null;\n    sourceAssetId: string | null;\n    existingDocumentId: string | null;\n    existingAssetId: string | null;\n    targetDocumentId: string;\n    title: string;\n    policy: string;\n  }>;\n  copy: boolean;\n  targetDocumentId: string;\n  targetMindmapId: string | null;\n  targetSubPath?: string | null;\n  rowIds: string[];","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.ts","s":49,"e":108,"m":1778802816694.9639,"t":"sourceAssetId: string | null;\n    existingDocumentId: string | null;\n    existingAssetId: string | null;\n    targetDocumentId: string;\n    title: string;\n    policy: string;\n  }>;\n  copy: boolean;\n  targetDocumentId: string;\n  targetMindmapId: string | null;\n  targetSubPath?: string | null;\n  rowIds: string[];\n  docIds: string[];\n  topLevelDocIds: string[];\n  copyableAssetIds: string[];\n  sourceAssetDocumentIds: string[];\n  documentTransferPlan?: {\n    action: \"copy\" | \"move\";\n    targetParentId: string;\n    documentIds: string[];\n    topLevelDocumentIds: string[];\n    copyItems: Array<{ documentId: string; recursive: boolean }>;\n  } | null;\n  resourceTransferPlan?: {\n    action: \"copy\" | \"move\";\n    assetIds: string[];\n    targetDocumentId: string;\n    targetSubPath?: string | null;\n  } | null;\n};\n\ntype FileTreeInternalDropPreflightResponse = {\n  plan?: FileTreeInternalDropPreflightPlan;\n};\n\nexport type FileTreeDeletePreflightPlan = {\n  rowIds: string[];\n  docIds: string[];\n  assetIds: string[];\n  assetDocumentIds: string[];\n};\n\ntype FileTreeDeletePreflightResponse = {\n  plan?: FileTreeDeletePreflightPlan;\n};\n\nexport type FileTreePastePreflightPlan = {\n  targetDocumentId: string;\n  targetMindmapId: string | null;\n  targetSubPath?: string | null;\n  rowIds: string[];\n  docItems: Array<{ documentId: string; recursive: boolean }>;\n  copyableAssetIds: string[];\n  resourceTransferPlan?: {\n    action: \"copy\";\n    assetIds: string[];\n    targetDocumentId: string;\n    targetSubPath?: string | null;\n  } | null;\n};","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.ts","s":97,"e":156,"m":1778802816694.9639,"t":"targetMindmapId: string | null;\n  targetSubPath?: string | null;\n  rowIds: string[];\n  docItems: Array<{ documentId: string; recursive: boolean }>;\n  copyableAssetIds: string[];\n  resourceTransferPlan?: {\n    action: \"copy\";\n    assetIds: string[];\n    targetDocumentId: string;\n    targetSubPath?: string | null;\n  } | null;\n};\n\ntype FileTreePastePreflightResponse = {\n  plan?: FileTreePastePreflightPlan;\n};\n\nexport type FileTreeUploadTargetPreflightPlan = {\n  workspaceId: string;\n  targetDocumentId: string;\n  targetMindmapId: string | null;\n  targetSubPath?: string | null;\n};\n\ntype FileTreeUploadTargetPreflightResponse = {\n  plan?: FileTreeUploadTargetPreflightPlan;\n};\n\nfunction normalizeAssetIds(assetIds: readonly string[]): string[] {\n  return Array.from(\n    new Set(\n      assetIds\n        .map((assetId) => (typeof assetId === \"string\" ? assetId.trim() : \"\"))\n        .filter(Boolean),\n    ),\n  );\n}\n\nasync function postResourceCommand<TResult>(\n  payload: Record<string, unknown>,\n  fallbackMessage: string,\n  path = \"/api/media/batch\",\n): Promise<TResult> {\n  const response = await fetch(path, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify(payload),\n  });\n\n  const body = (await response.json().catch(() => null)) as\n    | TResult\n    | ResourceCommandErrorPayload\n    | null;\n  if (!response.ok) {\n    const message =\n      body && typeof body === \"object\" && \"error\" in body && typeof body.error === \"string\"\n        ? body.error\n        : fallbackMessage;\n    throw new Error(message);\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.ts","s":145,"e":204,"m":1778802816694.9639,"t":"const body = (await response.json().catch(() => null)) as\n    | TResult\n    | ResourceCommandErrorPayload\n    | null;\n  if (!response.ok) {\n    const message =\n      body && typeof body === \"object\" && \"error\" in body && typeof body.error === \"string\"\n        ? body.error\n        : fallbackMessage;\n    throw new Error(message);\n  }\n\n  return body as TResult;\n}\n\nasync function postResourceForm<TResult>(\n  path: string,\n  formData: FormData,\n  fallbackMessage: string,\n): Promise<TResult> {\n  const response = await fetch(path, {\n    method: \"POST\",\n    body: formData,\n  });\n\n  const body = (await response.json().catch(() => null)) as\n    | TResult\n    | ResourceCommandErrorPayload\n    | null;\n  if (!response.ok) {\n    const message =\n      body && typeof body === \"object\" && \"error\" in body && typeof body.error === \"string\"\n        ? body.error\n        : fallbackMessage;\n    throw new Error(message);\n  }\n\n  return body as TResult;\n}\n\nfunction buildTransferPayload(\n  action: Extract<ResourceCommandAction, \"copy\" | \"move\">,\n  input: TransferResourceAssetsInput,\n) {\n  const payload: Record<string, unknown> = {\n    action,\n    assetIds: normalizeAssetIds(input.assetIds),\n    targetDocumentId: input.targetDocumentId,\n  };\n  const targetSubPath = input.targetSubPath?.trim();\n  if (targetSubPath) {\n    payload.targetSubPath = targetSubPath;\n  }\n  return payload;\n}\n\nexport async function copyFileTreeResourceAssets(\n  input: TransferResourceAssetsInput,\n): Promise<ResourceCommandResponse> {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.ts","s":193,"e":252,"m":1778802816694.9639,"t":"targetDocumentId: input.targetDocumentId,\n  };\n  const targetSubPath = input.targetSubPath?.trim();\n  if (targetSubPath) {\n    payload.targetSubPath = targetSubPath;\n  }\n  return payload;\n}\n\nexport async function copyFileTreeResourceAssets(\n  input: TransferResourceAssetsInput,\n): Promise<ResourceCommandResponse> {\n  return postResourceCommand<ResourceCommandResponse>(\n    buildTransferPayload(\"copy\", input),\n    \"复制附件失败\",\n  );\n}\n\nexport async function moveFileTreeResourceAssets(\n  input: TransferResourceAssetsInput,\n): Promise<ResourceCommandResponse> {\n  return postResourceCommand<ResourceCommandResponse>(\n    buildTransferPayload(\"move\", input),\n    \"移动附件失败\",\n  );\n}\n\nexport async function renameFileTreeResourceAsset(\n  input: RenameResourceAssetInput,\n): Promise<ResourceCommandResponse> {\n  return postResourceCommand<ResourceCommandResponse>(\n    {\n      action: \"rename\",\n      assetIds: normalizeAssetIds([input.assetId]),\n      newName: input.newName,\n    },\n    \"重命名失败\",\n  );\n}\n\nexport async function deleteFileTreeResourceAssets(\n  assetIds: readonly string[],\n): Promise<ResourceCommandResponse> {\n  return postResourceCommand<ResourceCommandResponse>(\n    {\n      action: \"delete\",\n      assetIds: normalizeAssetIds(assetIds),\n    },\n    \"删除失败\",\n  );\n}\n\nexport async function restoreFileTreeResourceAssets(\n  assetIds: readonly string[],\n): Promise<ResourceCommandResponse> {\n  return postResourceCommand<ResourceCommandResponse>(\n    {\n      action: \"restore\",\n      assetIds: normalizeAssetIds(assetIds),\n    },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.ts","s":241,"e":300,"m":1778802816694.9639,"t":"\"删除失败\",\n  );\n}\n\nexport async function restoreFileTreeResourceAssets(\n  assetIds: readonly string[],\n): Promise<ResourceCommandResponse> {\n  return postResourceCommand<ResourceCommandResponse>(\n    {\n      action: \"restore\",\n      assetIds: normalizeAssetIds(assetIds),\n    },\n    \"恢复附件失败，请稍后再试\",\n  );\n}\n\nexport async function uploadFileTreeResourceAsset(\n  input: UploadResourceAssetInput,\n): Promise<ResourceCommandResponse> {\n  const formData = new FormData();\n  formData.append(\"file\", input.file);\n  formData.append(\"workspaceId\", input.workspaceId);\n  formData.append(\"documentId\", input.documentId);\n  const mindmapId = input.mindmapId?.trim();\n  if (mindmapId) {\n    formData.append(\"mindmapId\", mindmapId);\n  }\n  return postResourceForm<ResourceCommandResponse>(\n    \"/api/media/upload\",\n    formData,\n    \"上传失败\",\n  );\n}\n\nexport async function preflightFileTreeInternalDrop(\n  input: FileTreeShellInternalDropPreflightPayload,\n): Promise<FileTreeInternalDropPreflightPlan> {\n  const response = await postResourceCommand<FileTreeInternalDropPreflightResponse>(\n    input,\n    \"文件树拖放预检失败\",\n    \"/api/tree/filetree/drop-preflight\",\n  );\n  if (!response.plan) {\n    throw new Error(\"文件树拖放预检失败\");\n  }\n  return response.plan;\n}\n\nexport async function preflightFileTreeDelete(\n  input: FileTreeShellDeletePreflightPayload,\n): Promise<FileTreeDeletePreflightPlan> {\n  const response = await postResourceCommand<FileTreeDeletePreflightResponse>(\n    input,\n    \"文件树删除预检失败\",\n    \"/api/tree/filetree/delete-preflight\",\n  );\n  if (!response.plan) {\n    throw new Error(\"文件树删除预检失败\");\n  }\n  return response.plan;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/resource-command-client.ts","s":289,"e":330,"m":1778802816694.9639,"t":"export async function preflightFileTreeDelete(\n  input: FileTreeShellDeletePreflightPayload,\n): Promise<FileTreeDeletePreflightPlan> {\n  const response = await postResourceCommand<FileTreeDeletePreflightResponse>(\n    input,\n    \"文件树删除预检失败\",\n    \"/api/tree/filetree/delete-preflight\",\n  );\n  if (!response.plan) {\n    throw new Error(\"文件树删除预检失败\");\n  }\n  return response.plan;\n}\n\nexport async function preflightFileTreePaste(\n  input: FileTreeShellPastePreflightPayload,\n): Promise<FileTreePastePreflightPlan> {\n  const response = await postResourceCommand<FileTreePastePreflightResponse>(\n    input,\n    \"文件树粘贴预检失败\",\n    \"/api/tree/filetree/paste-preflight\",\n  );\n  if (!response.plan) {\n    throw new Error(\"文件树粘贴预检失败\");\n  }\n  return response.plan;\n}\n\nexport async function preflightFileTreeUploadTarget(\n  input: FileTreeShellUploadTargetPreflightPayload,\n): Promise<FileTreeUploadTargetPreflightPlan> {\n  const response = await postResourceCommand<FileTreeUploadTargetPreflightResponse>(\n    input,\n    \"文件树上传目标预检失败\",\n    \"/api/tree/filetree/upload-target-preflight\",\n  );\n  if (!response.plan) {\n    throw new Error(\"文件树上传目标预检失败\");\n  }\n  return response.plan;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.test.ts","s":1,"e":60,"m":1778081169468.3005,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildVisibleRows } from \"./rows\";\nimport { parseFileTreeRowId } from \"./types\";\n\ndescribe(\"buildVisibleRows\", () => {\n  it(\"缺少 kernel file_tree items 时不再回退 pageRows + assets 重建对象语义\", () => {\n    const a = {\n      access_scope: \"private\" as const,\n      id: \"a\",\n      workspace_id: \"w\",\n      title: \"A\",\n      parent_id: null,\n      sort_order: 0,\n      is_starred: null,\n      is_template: false,\n      created_at: \"\",\n      updated_at: null,\n      children: [\n        {\n          access_scope: \"private\" as const,\n          id: \"b\",\n          workspace_id: \"w\",\n          title: \"B\",\n          parent_id: \"a\",\n          sort_order: 0,\n          is_starred: null,\n          is_template: false,\n          created_at: \"\",\n          updated_at: null,\n          children: [],\n        },\n      ],\n    };\n\n    const rows = buildVisibleRows({\n      pageRows: [\n        {\n          nodeId: a.id,\n          parentNodeId: null,\n          depth: 0,\n          childCount: 1,\n          position: 0,\n          title: a.title,\n          node: a,\n        },\n      ],\n      expanded: new Set([\"a\"]),\n      assetsByDoc: {\n        a: [\n          {\n            id: \"x\",\n            workspace_id: \"w\",\n            document_id: \"a\",\n            asset_type: \"file\",\n            file_url: null,\n            thumbnail_url: null,\n            bucket: \"workspace\",\n            storage_path: \"x\",\n            file_name: \"x.png\",\n            file_size: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.test.ts","s":49,"e":108,"m":1778081169468.3005,"t":"a: [\n          {\n            id: \"x\",\n            workspace_id: \"w\",\n            document_id: \"a\",\n            asset_type: \"file\",\n            file_url: null,\n            thumbnail_url: null,\n            bucket: \"workspace\",\n            storage_path: \"x\",\n            file_name: \"x.png\",\n            file_size: null,\n            mime_type: \"image/png\",\n            ocr_payload: undefined,\n            ocr_strategy: null,\n            ocr_text: null,\n            ocr_status: null,\n            signed_url: null,\n            created_at: \"\",\n            updated_at: \"\",\n          },\n          {\n            id: \"y\",\n            workspace_id: \"w\",\n            document_id: \"a\",\n            asset_type: \"file\",\n            file_url: null,\n            thumbnail_url: null,\n            bucket: \"workspace\",\n            storage_path: \"y\",\n            file_name: \"y.pdf\",\n            file_size: null,\n            mime_type: \"application/pdf\",\n            ocr_payload: undefined,\n            ocr_strategy: null,\n            ocr_text: null,\n            ocr_status: null,\n            signed_url: null,\n            created_at: \"\",\n            updated_at: \"\",\n          },\n        ],\n      },\n    });\n\n    expect(rows).toEqual([]);\n  });\n\n  it(\"优先消费 Rust file_tree projection 并保留 index 与资源文件夹语义\", () => {\n    const rows = buildVisibleRows({\n      fileTreeItems: [\n        {\n          rowId: \"doc:page_root\",\n          rowKind: \"document\",\n          nodeId: \"page_root\",\n          parentNodeId: null,\n          nodeType: \"page\",\n          projectionKind: \"file_tree\",\n          title: \"根页面\",\n          depth: 0,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.test.ts","s":97,"e":156,"m":1778081169468.3005,"t":"it(\"优先消费 Rust file_tree projection 并保留 index 与资源文件夹语义\", () => {\n    const rows = buildVisibleRows({\n      fileTreeItems: [\n        {\n          rowId: \"doc:page_root\",\n          rowKind: \"document\",\n          nodeId: \"page_root\",\n          parentNodeId: null,\n          nodeType: \"page\",\n          projectionKind: \"file_tree\",\n          title: \"根页面\",\n          depth: 0,\n          position: 0,\n          childCount: 4,\n          expandable: true,\n          expandedByDefault: true,\n          capabilities: [\"expand\", \"open\", \"select\"],\n          resourceMeta: {\n            resourceKind: \"document\",\n            documentId: \"page_root\",\n            workspaceId: \"ws_1\",\n            iconHint: \"page\",\n            extra: {\n              source: {\n                sourceKind: \"convex_workspace\",\n                sourceUri: \"convex://workspace/ws_1/documents/page_root\",\n                relativePath: \"documents/page_root\",\n                storageIdentity: \"page_root\",\n                operationProfile: \"convex_workspace\",\n              },\n            },\n          },\n          iconHint: \"page\",\n        },\n        {\n          rowId: \"index:page_root\",\n          rowKind: \"index\",\n          nodeId: \"index:page_root\",\n          parentNodeId: \"page_root\",\n          nodeType: \"index\",\n          projectionKind: \"file_tree\",\n          title: \"index.md\",\n          depth: 1,\n          position: 0,\n          childCount: 0,\n          expandable: false,\n          expandedByDefault: false,\n          capabilities: [\"open\", \"select\"],\n          resourceMeta: {\n            resourceKind: \"index\",\n            documentId: \"page_root\",\n            workspaceId: \"ws_1\",\n            iconHint: \"index\",\n            extra: {\n              source: {\n                sourceKind: \"convex_workspace\",\n                sourceUri: \"convex://workspace/ws_1/documents/page_root/index\",\n                relativePath: \"documents/page_root/index\",\n                storageIdentity: \"page_root:index\",\n                operationProfile: \"convex_workspace\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.test.ts","s":145,"e":204,"m":1778081169468.3005,"t":"resourceMeta: {\n            resourceKind: \"index\",\n            documentId: \"page_root\",\n            workspaceId: \"ws_1\",\n            iconHint: \"index\",\n            extra: {\n              source: {\n                sourceKind: \"convex_workspace\",\n                sourceUri: \"convex://workspace/ws_1/documents/page_root/index\",\n                relativePath: \"documents/page_root/index\",\n                storageIdentity: \"page_root:index\",\n                operationProfile: \"convex_workspace\",\n              },\n            },\n          },\n          iconHint: \"index\",\n        },\n        {\n          rowId: \"asset-folder:mind_1\",\n          rowKind: \"asset_folder\",\n          nodeId: \"asset-folder:mind_1\",\n          parentNodeId: \"page_root\",\n          nodeType: \"mindmap\",\n          projectionKind: \"file_tree\",\n          title: \"头脑风暴\",\n          depth: 1,\n          position: 1,\n          childCount: 1,\n          expandable: true,\n          expandedByDefault: false,\n          capabilities: [\"expand\", \"open-asset\", \"select\"],\n          resourceMeta: {\n            resourceKind: \"mindmap\",\n            documentId: \"page_root\",\n            assetId: \"mind_1\",\n            workspaceId: \"ws_1\",\n            assetKind: \"mindmap\",\n            iconHint: \"mindmap\",\n            extra: {\n              source: {\n                sourceKind: \"local_folder\",\n                sourceUri: \"file:///workspace/ws_1/page_root/mindmap.json\",\n                relativePath: \"page_root/mindmap.json\",\n                storageIdentity: \"mind_1\",\n                operationProfile: \"local_readonly\",\n              },\n            },\n          },\n          iconHint: \"mindmap\",\n        },\n        {\n          rowId: \"asset:asset_child_1\",\n          rowKind: \"asset\",\n          nodeId: \"asset:asset_child_1\",\n          parentNodeId: \"asset-folder:mind_1\",\n          nodeType: \"asset\",\n          projectionKind: \"file_tree\",\n          title: \"节点图片.png\",\n          depth: 2,\n          position: 0,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.test.ts","s":193,"e":252,"m":1778081169468.3005,"t":"iconHint: \"mindmap\",\n        },\n        {\n          rowId: \"asset:asset_child_1\",\n          rowKind: \"asset\",\n          nodeId: \"asset:asset_child_1\",\n          parentNodeId: \"asset-folder:mind_1\",\n          nodeType: \"asset\",\n          projectionKind: \"file_tree\",\n          title: \"节点图片.png\",\n          depth: 2,\n          position: 0,\n          childCount: 0,\n          expandable: false,\n          expandedByDefault: false,\n          capabilities: [\"open-asset\", \"select\"],\n          resourceMeta: {\n            resourceKind: \"asset\",\n            documentId: \"page_root\",\n            assetId: \"asset_child_1\",\n            workspaceId: \"ws_1\",\n            assetKind: \"image\",\n            iconHint: \"image\",\n            extra: {\n              source: {\n                sourceKind: \"local_folder\",\n                sourceUri: \"file:///workspace/ws_1/page_root/assets/node.png\",\n                relativePath: \"page_root/assets/node.png\",\n                storageIdentity: \"asset_child_1\",\n                operationProfile: \"local_readonly\",\n              },\n            },\n          },\n          iconHint: \"image\",\n        },\n        {\n          rowId: \"asset:table_1\",\n          rowKind: \"asset\",\n          nodeId: \"asset:table_1\",\n          parentNodeId: \"page_root\",\n          nodeType: \"table\",\n          projectionKind: \"file_tree\",\n          title: \"预算.luckysheet\",\n          depth: 1,\n          position: 2,\n          childCount: 0,\n          expandable: false,\n          expandedByDefault: false,\n          capabilities: [\"open-asset\", \"select\"],\n          resourceMeta: {\n            resourceKind: \"table\",\n            documentId: \"page_root\",\n            assetId: \"table_1\",\n            workspaceId: \"ws_1\",\n            assetKind: \"table\",\n            iconHint: \"table\",\n            extra: {\n              source: {\n                sourceKind: \"convex_workspace\",\n                sourceUri: \"convex://workspace/ws_1/assets/table_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.test.ts","s":241,"e":300,"m":1778081169468.3005,"t":"capabilities: [\"open-asset\", \"select\"],\n          resourceMeta: {\n            resourceKind: \"table\",\n            documentId: \"page_root\",\n            assetId: \"table_1\",\n            workspaceId: \"ws_1\",\n            assetKind: \"table\",\n            iconHint: \"table\",\n            extra: {\n              source: {\n                sourceKind: \"convex_workspace\",\n                sourceUri: \"convex://workspace/ws_1/assets/table_1\",\n                relativePath: \"assets/table_1\",\n                storageIdentity: \"table_1\",\n                operationProfile: \"convex_workspace\",\n              },\n            },\n          },\n          iconHint: \"table\",\n        },\n      ],\n      expanded: new Set([\"page_root\"]),\n      expandedAssetFolderIds: new Set([\"mind_1\"]),\n    });\n\n    expect(rows.map((row) => `${row.kind}:${row.depth}:${row.rowId}`)).toEqual([\n      \"doc:0:doc:page_root\",\n      \"index:1:index:page_root\",\n      \"asset-folder:1:asset-folder:mind_1\",\n      \"asset:2:asset:asset_child_1\",\n      \"asset:1:asset:table_1\",\n    ]);\n    expect(rows[0]).toMatchObject({\n      kind: \"doc\",\n      docId: \"page_root\",\n      hasChildren: true,\n      isExpanded: true,\n    });\n    expect(rows[2]).toMatchObject({\n      kind: \"asset-folder\",\n      docId: \"page_root\",\n      asset: {\n        id: \"mind_1\",\n        asset_type: \"mindmap\",\n        file_name: \"头脑风暴\",\n      },\n      hasChildren: true,\n      isExpanded: true,\n    });\n    expect(rows[3]).toMatchObject({\n      kind: \"asset\",\n      asset: {\n        id: \"asset_child_1\",\n        asset_type: \"image\",\n        file_name: \"节点图片.png\",\n      },\n    });\n    expect(rows[4]).toMatchObject({\n      kind: \"asset\",\n      asset: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.test.ts","s":289,"e":319,"m":1778081169468.3005,"t":"});\n    expect(rows[3]).toMatchObject({\n      kind: \"asset\",\n      asset: {\n        id: \"asset_child_1\",\n        asset_type: \"image\",\n        file_name: \"节点图片.png\",\n      },\n    });\n    expect(rows[4]).toMatchObject({\n      kind: \"asset\",\n      asset: {\n        id: \"table_1\",\n        asset_type: \"luckysheet\",\n        file_name: \"预算.luckysheet\",\n      },\n    });\n  });\n\n});\n\ndescribe(\"parseFileTreeRowId\", () => {\n  it(\"可逆解析 rowId\", () => {\n    expect(parseFileTreeRowId(\"doc:abc\")).toEqual({ kind: \"doc\", docId: \"abc\" });\n    expect(parseFileTreeRowId(\"index:abc\")).toEqual({ kind: \"index\", docId: \"abc\" });\n    expect(parseFileTreeRowId(\"asset:xyz\")).toEqual({ kind: \"asset\", assetId: \"xyz\" });\n    expect(parseFileTreeRowId(\"bad\")).toBeNull();\n    expect(parseFileTreeRowId(\"doc:\")).toBeNull();\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.ts","s":1,"e":60,"m":1777152884646.45,"t":"\"use client\";\n\nimport type { KernelFileTreeProjectionItem } from \"@/lib/kernel-file-tree\";\nimport {\n  getDocIdFromFileTreeItem,\n  resolveFileTreeRowAsset,\n  resolveFileTreeRowNode,\n} from \"@/lib/kernel-file-tree\";\nimport type { SidebarTreeNode } from \"@/lib/kernel-sidebar\";\nimport type { PageTreeProjectionItem } from \"@/lib/tree-projection\";\nimport type { MediaAsset } from \"@/types/media\";\nimport type { FileTreeRow } from \"./types\";\nimport { makeAssetFolderRowId, makeAssetRowId, makeDocRowId, makeIndexRowId } from \"./types\";\n\nfunction buildRowsFromKernelFileTreeProjection(input: {\n  fileTreeItems: KernelFileTreeProjectionItem[];\n  expanded: Set<string>;\n  expandedAssetFolderIds?: Set<string>;\n  nodeById?: Map<string, SidebarTreeNode>;\n  assetById?: Map<string, MediaAsset>;\n}): FileTreeRow[] {\n  const rows: FileTreeRow[] = [];\n  const seen = new Set<string>();\n\n  input.fileTreeItems.forEach((item) => {\n    if (!item?.rowId || seen.has(item.rowId)) {\n      return;\n    }\n    seen.add(item.rowId);\n    const docId = getDocIdFromFileTreeItem(item);\n\n    switch (item.rowKind) {\n      case \"document\": {\n        const node = resolveFileTreeRowNode(item, input.nodeById);\n        rows.push({\n          kind: \"doc\",\n          rowId: makeDocRowId(docId),\n          depth: item.depth,\n          docId,\n          parentDocId: item.parentNodeId,\n          node,\n          hasChildren: item.childCount > 0,\n          isExpanded: input.expanded.has(docId),\n        });\n        return;\n      }\n      case \"index\": {\n        const node = resolveFileTreeRowNode(item, input.nodeById);\n        rows.push({\n          kind: \"index\",\n          rowId: makeIndexRowId(docId),\n          depth: item.depth,\n          docId,\n          parentDocId: docId,\n          node,\n        });\n        return;\n      }\n      case \"asset_folder\": {\n        const asset = resolveFileTreeRowAsset(item, input.assetById);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.ts","s":49,"e":108,"m":1777152884646.45,"t":"rows.push({\n          kind: \"index\",\n          rowId: makeIndexRowId(docId),\n          depth: item.depth,\n          docId,\n          parentDocId: docId,\n          node,\n        });\n        return;\n      }\n      case \"asset_folder\": {\n        const asset = resolveFileTreeRowAsset(item, input.assetById);\n        rows.push({\n          kind: \"asset-folder\",\n          rowId: makeAssetFolderRowId(asset.id),\n          depth: item.depth,\n          docId,\n          parentDocId: docId,\n          asset,\n          hasChildren: item.childCount > 0,\n          isExpanded: input.expandedAssetFolderIds?.has(asset.id) ?? false,\n        });\n        return;\n      }\n      case \"asset\": {\n        const asset = resolveFileTreeRowAsset(item, input.assetById);\n        rows.push({\n          kind: \"asset\",\n          rowId: makeAssetRowId(asset.id),\n          depth: item.depth,\n          docId,\n          parentDocId: docId,\n          asset,\n        });\n        return;\n      }\n    }\n  });\n\n  return rows;\n}\n\nexport function buildVisibleRows({\n  fileTreeItems,\n  expanded,\n  expandedAssetFolderIds,\n  nodeById,\n  assetById,\n}: {\n  fileTreeItems?: KernelFileTreeProjectionItem[];\n  // 兼容旧调用签名；主路径必须提供 kernel file_tree items，不能再从这些字段重建对象语义。\n  pageRows?: PageTreeProjectionItem[];\n  expanded: Set<string>;\n  assetsByDoc?: Record<string, MediaAsset[]>;\n  assetChildrenByAssetId?: Record<string, MediaAsset[]>;\n  expandedAssetFolderIds?: Set<string>;\n  nodeById?: Map<string, SidebarTreeNode>;\n  assetById?: Map<string, MediaAsset>;\n}): FileTreeRow[] {\n  if (fileTreeItems && fileTreeItems.length > 0) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/rows.ts","s":97,"e":120,"m":1777152884646.45,"t":"}: {\n  fileTreeItems?: KernelFileTreeProjectionItem[];\n  // 兼容旧调用签名；主路径必须提供 kernel file_tree items，不能再从这些字段重建对象语义。\n  pageRows?: PageTreeProjectionItem[];\n  expanded: Set<string>;\n  assetsByDoc?: Record<string, MediaAsset[]>;\n  assetChildrenByAssetId?: Record<string, MediaAsset[]>;\n  expandedAssetFolderIds?: Set<string>;\n  nodeById?: Map<string, SidebarTreeNode>;\n  assetById?: Map<string, MediaAsset>;\n}): FileTreeRow[] {\n  if (fileTreeItems && fileTreeItems.length > 0) {\n    return buildRowsFromKernelFileTreeProjection({\n      fileTreeItems,\n      expanded,\n      expandedAssetFolderIds,\n      nodeById,\n      assetById,\n    });\n  }\n\n  return [];\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/selection-source.test.ts","s":1,"e":60,"m":1777170954828.7944,"t":"import { describe, expect, it } from \"vitest\";\nimport type { FileTreeSelectionState } from \"./selection\";\nimport {\n  createEmptyFileTreeSelectionState,\n  materializeRendererSelectionSnapshot,\n  resolveActiveFileTreeSelection,\n} from \"./selection-source\";\n\nfunction selection(\n  selectedRowIds: string[],\n  anchorRowId: string | null,\n  focusedRowId: string | null,\n): FileTreeSelectionState {\n  return {\n    selectedRowIds: new Set(selectedRowIds),\n    anchorRowId,\n    focusedRowId,\n  };\n}\n\ndescribe(\"selection-source\", () => {\n  it(\"rust_family 应优先消费 renderer selection snapshot\", () => {\n    const legacySelection = selection([\"legacy\"], \"legacy\", \"legacy\");\n    const rendererSelection = selection([\"renderer\"], \"renderer\", \"renderer\");\n\n    expect(\n      resolveActiveFileTreeSelection({\n        preferRendererSnapshot: true,\n        legacySelection,\n        rendererSelection,\n      }),\n    ).toBe(rendererSelection);\n\n    expect(\n      resolveActiveFileTreeSelection({\n        preferRendererSnapshot: false,\n        legacySelection,\n        rendererSelection,\n      }),\n    ).toBe(legacySelection);\n  });\n\n  it(\"renderer event snapshot 只过滤未知 row，不在宿主侧重算 focus/anchor\", () => {\n    const snapshot = materializeRendererSelectionSnapshot({\n      payload: {\n        selectedRowIds: [\"doc:a\", \"missing\"],\n        anchorRowId: \"missing\",\n        focusedRowId: \"doc:a\",\n      },\n      hasRowId: (rowId) => rowId === \"doc:a\",\n    });\n\n    expect(Array.from(snapshot.selectedRowIds)).toEqual([\"doc:a\"]);\n    expect(snapshot.anchorRowId).toBeNull();\n    expect(snapshot.focusedRowId).toBe(\"doc:a\");\n  });\n\n  it(\"空 selection 工厂应返回互不共享的 Set 实例\", () => {\n    const a = createEmptyFileTreeSelectionState();\n    const b = createEmptyFileTreeSelectionState();","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/selection-source.test.ts","s":49,"e":69,"m":1777170954828.7944,"t":"},\n      hasRowId: (rowId) => rowId === \"doc:a\",\n    });\n\n    expect(Array.from(snapshot.selectedRowIds)).toEqual([\"doc:a\"]);\n    expect(snapshot.anchorRowId).toBeNull();\n    expect(snapshot.focusedRowId).toBe(\"doc:a\");\n  });\n\n  it(\"空 selection 工厂应返回互不共享的 Set 实例\", () => {\n    const a = createEmptyFileTreeSelectionState();\n    const b = createEmptyFileTreeSelectionState();\n\n    a.selectedRowIds.add(\"doc:a\");\n\n    expect(a.selectedRowIds.has(\"doc:a\")).toBe(true);\n    expect(b.selectedRowIds.has(\"doc:a\")).toBe(false);\n    expect(a.selectedRowIds).not.toBe(b.selectedRowIds);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/selection-source.ts","s":1,"e":47,"m":1777170976202.07,"t":"import type { FileTreeSelectionState } from \"./selection\";\n\nexport type FileTreeSelectionSnapshotPayload = {\n  selectedRowIds: readonly string[];\n  anchorRowId: string | null;\n  focusedRowId: string | null;\n};\n\nexport function createEmptyFileTreeSelectionState(): FileTreeSelectionState {\n  return {\n    selectedRowIds: new Set<string>(),\n    anchorRowId: null,\n    focusedRowId: null,\n  };\n}\n\nexport function materializeRendererSelectionSnapshot(input: {\n  payload: FileTreeSelectionSnapshotPayload;\n  hasRowId: (rowId: string) => boolean;\n}): FileTreeSelectionState {\n  const selectedRowIds = new Set(\n    input.payload.selectedRowIds.filter((rowId) => input.hasRowId(rowId)),\n  );\n\n  return {\n    selectedRowIds,\n    anchorRowId:\n      input.payload.anchorRowId && input.hasRowId(input.payload.anchorRowId)\n        ? input.payload.anchorRowId\n        : null,\n    focusedRowId:\n      input.payload.focusedRowId && input.hasRowId(input.payload.focusedRowId)\n        ? input.payload.focusedRowId\n        : null,\n  };\n}\n\nexport function resolveActiveFileTreeSelection(input: {\n  preferRendererSnapshot: boolean;\n  legacySelection: FileTreeSelectionState;\n  rendererSelection: FileTreeSelectionState;\n}): FileTreeSelectionState {\n  return input.preferRendererSnapshot\n    ? input.rendererSelection\n    : input.legacySelection;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/selection.test.ts","s":1,"e":60,"m":1773495900990.8623,"t":"import { describe, expect, it } from \"vitest\";\nimport { normalizeFileTreeSelectionForVisibleRows, reduceFileTreeSelection } from \"./selection\";\n\ndescribe(\"reduceFileTreeSelection\", () => {\n  const visible = [\"a\", \"b\", \"c\", \"d\"];\n\n  it(\"单击：清空并仅选中当前；更新 anchor/focus\", () => {\n    const next = reduceFileTreeSelection(\n      { selectedRowIds: new Set([\"x\"]), anchorRowId: \"x\", focusedRowId: \"x\" },\n      { type: \"click\", rowId: \"b\", visibleRowIds: visible, modifiers: {} },\n    );\n    expect(Array.from(next.selectedRowIds)).toEqual([\"b\"]);\n    expect(next.anchorRowId).toBe(\"b\");\n    expect(next.focusedRowId).toBe(\"b\");\n  });\n\n  it(\"Ctrl/Cmd+单击：切换选中；不清空其他\", () => {\n    const next = reduceFileTreeSelection(\n      { selectedRowIds: new Set([\"b\"]), anchorRowId: \"b\", focusedRowId: \"b\" },\n      { type: \"click\", rowId: \"d\", visibleRowIds: visible, modifiers: { ctrlKey: true } },\n    );\n    expect(next.selectedRowIds.has(\"b\")).toBe(true);\n    expect(next.selectedRowIds.has(\"d\")).toBe(true);\n  });\n\n  it(\"Shift+单击：按 visibleRows 做区间选择（覆盖式）\", () => {\n    const next = reduceFileTreeSelection(\n      { selectedRowIds: new Set([\"b\"]), anchorRowId: \"b\", focusedRowId: \"b\" },\n      { type: \"click\", rowId: \"d\", visibleRowIds: visible, modifiers: { shiftKey: true } },\n    );\n    expect(Array.from(next.selectedRowIds)).toEqual([\"b\", \"c\", \"d\"]);\n    expect(next.focusedRowId).toBe(\"d\");\n  });\n\n  it(\"右键：未选中项右键 → 先切为单选；已选中项 → 保持多选集合\", () => {\n    const a = reduceFileTreeSelection(\n      { selectedRowIds: new Set([\"b\", \"c\"]), anchorRowId: \"b\", focusedRowId: \"c\" },\n      { type: \"contextmenu\", rowId: \"d\" },\n    );\n    expect(Array.from(a.selectedRowIds)).toEqual([\"d\"]);\n\n    const b = reduceFileTreeSelection(\n      { selectedRowIds: new Set([\"b\", \"c\"]), anchorRowId: \"b\", focusedRowId: \"c\" },\n      { type: \"contextmenu\", rowId: \"c\" },\n    );\n    expect(Array.from(b.selectedRowIds).sort()).toEqual([\"b\", \"c\"]);\n    expect(b.focusedRowId).toBe(\"c\");\n  });\n\n  it(\"空白处单击清空选择\", () => {\n    const next = reduceFileTreeSelection(\n      { selectedRowIds: new Set([\"b\"]), anchorRowId: \"b\", focusedRowId: \"b\" },\n      { type: \"clear\" },\n    );\n    expect(next.selectedRowIds.size).toBe(0);\n    expect(next.anchorRowId).toBeNull();\n    expect(next.focusedRowId).toBeNull();\n  });\n\n  it(\"可见行变化时保留仍可见的选择\", () => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/selection.test.ts","s":49,"e":70,"m":1773495900990.8623,"t":"it(\"空白处单击清空选择\", () => {\n    const next = reduceFileTreeSelection(\n      { selectedRowIds: new Set([\"b\"]), anchorRowId: \"b\", focusedRowId: \"b\" },\n      { type: \"clear\" },\n    );\n    expect(next.selectedRowIds.size).toBe(0);\n    expect(next.anchorRowId).toBeNull();\n    expect(next.focusedRowId).toBeNull();\n  });\n\n  it(\"可见行变化时保留仍可见的选择\", () => {\n    const next = normalizeFileTreeSelectionForVisibleRows(\n      { selectedRowIds: new Set([\"b\", \"x\"]), anchorRowId: \"b\", focusedRowId: \"x\" },\n      [\"a\", \"b\", \"c\"],\n    );\n    expect(Array.from(next.selectedRowIds)).toEqual([\"b\"]);\n    expect(next.anchorRowId).toBe(\"b\");\n    expect(next.focusedRowId).toBe(\"b\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/selection.ts","s":49,"e":108,"m":1773495900999.348,"t":"};\n    }\n    case \"click\": {\n      const { rowId, visibleRowIds, modifiers } = action;\n      const withMeta = Boolean(modifiers.metaKey);\n      const withCtrl = Boolean(modifiers.ctrlKey);\n      const withShift = Boolean(modifiers.shiftKey);\n      const withToggle = withMeta || withCtrl;\n\n      if (withShift) {\n        const anchor = prev.anchorRowId ?? prev.focusedRowId ?? rowId;\n        const range = getRangeRowIds(visibleRowIds, anchor, rowId);\n        const next = withToggle ? new Set(prev.selectedRowIds) : new Set<string>();\n        range.forEach((id) => next.add(id));\n        return {\n          selectedRowIds: next,\n          anchorRowId: prev.anchorRowId ?? anchor,\n          focusedRowId: rowId,\n        };\n      }\n\n      if (withToggle) {\n        const next = new Set(prev.selectedRowIds);\n        if (next.has(rowId)) {\n          next.delete(rowId);\n        } else {\n          next.add(rowId);\n        }\n        return { selectedRowIds: next, anchorRowId: rowId, focusedRowId: rowId };\n      }\n\n      return { selectedRowIds: new Set([rowId]), anchorRowId: rowId, focusedRowId: rowId };\n    }\n  }\n}\n\nexport function normalizeFileTreeSelectionForVisibleRows(\n  prev: FileTreeSelectionState,\n  visibleRowIds: string[],\n): FileTreeSelectionState {\n  const visibleSet = new Set(visibleRowIds);\n\n  let selectedRowIds = prev.selectedRowIds;\n  let removedAny = false;\n  const kept: string[] = [];\n  prev.selectedRowIds.forEach((id) => {\n    if (visibleSet.has(id)) {\n      kept.push(id);\n      return;\n    }\n    removedAny = true;\n  });\n  if (removedAny) {\n    selectedRowIds = new Set(kept);\n  }\n\n  let anchorRowId = prev.anchorRowId;\n  if (anchorRowId && !visibleSet.has(anchorRowId)) {\n    anchorRowId = null;\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/selection.ts","s":97,"e":133,"m":1773495900999.348,"t":"return;\n    }\n    removedAny = true;\n  });\n  if (removedAny) {\n    selectedRowIds = new Set(kept);\n  }\n\n  let anchorRowId = prev.anchorRowId;\n  if (anchorRowId && !visibleSet.has(anchorRowId)) {\n    anchorRowId = null;\n  }\n\n  let focusedRowId = prev.focusedRowId;\n  if (focusedRowId && !visibleSet.has(focusedRowId)) {\n    focusedRowId = null;\n  }\n\n  if (!focusedRowId && selectedRowIds.size > 0) {\n    focusedRowId = visibleRowIds.find((id) => selectedRowIds.has(id)) ?? null;\n  }\n\n  if (!anchorRowId && focusedRowId) {\n    anchorRowId = focusedRowId;\n  }\n\n  if (\n    selectedRowIds === prev.selectedRowIds &&\n    anchorRowId === prev.anchorRowId &&\n    focusedRowId === prev.focusedRowId\n  ) {\n    return prev;\n  }\n\n  return { selectedRowIds, anchorRowId, focusedRowId };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":1,"e":60,"m":1778802928217.177,"t":"import { describe, expect, it } from \"vitest\";\nimport type { KernelFileTreeProjectionItem } from \"@/lib/kernel-file-tree\";\nimport type { SidebarTreeNode } from \"@/lib/kernel-sidebar\";\nimport type { MediaAsset } from \"@/types/media\";\nimport {\n  buildFileTreeShellDeletePreflightPayload,\n  buildFileTreeShellInternalDropPreflightPayload,\n  buildFileTreeShellPastePreflightPayload,\n  buildFileTreeShellUploadTargetPreflightPayload,\n  buildFileTreeShellRowById,\n  buildFileTreeShellVisibleRowIds,\n  collectFileTreeShellAssetHints,\n  getOrderedFileTreeShellRows,\n  inferFileTreeShellTargetDocumentId,\n  resolveFileTreeShellMindmapTargetId,\n} from \"./shell\";\n\ndescribe(\"file-tree shell helpers\", () => {\n  const nodeById = new Map<string, SidebarTreeNode>([\n    [\n      \"doc_root\",\n      {\n        id: \"doc_root\",\n        title: \"根页面\",\n      } as SidebarTreeNode,\n    ],\n  ]);\n\n  const assetById = new Map<string, MediaAsset>([\n    [\n      \"mind_1\",\n      {\n        id: \"mind_1\",\n        document_id: \"doc_root\",\n        workspace_id: \"ws_1\",\n        asset_type: \"mindmap\",\n        file_name: \"mindmap.json\",\n        storage_path: \"mindmaps/mind_1/mindmap.json\",\n      } as MediaAsset,\n    ],\n    [\n      \"asset_child_1\",\n      {\n        id: \"asset_child_1\",\n        document_id: \"doc_root\",\n        workspace_id: \"ws_1\",\n        asset_type: \"file\",\n        file_name: \"node.png\",\n        storage_path: \"mindmaps/mind_1/assets/node.png\",\n      } as MediaAsset,\n    ],\n    [\n      \"pdf_1\",\n      {\n        id: \"pdf_1\",\n        document_id: \"doc_root\",\n        workspace_id: \"ws_1\",\n        asset_type: \"file\",\n        file_name: \"guide.pdf\",\n        storage_path: \"uploads/guide.pdf\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":49,"e":108,"m":1778802928217.177,"t":"storage_path: \"mindmaps/mind_1/assets/node.png\",\n      } as MediaAsset,\n    ],\n    [\n      \"pdf_1\",\n      {\n        id: \"pdf_1\",\n        document_id: \"doc_root\",\n        workspace_id: \"ws_1\",\n        asset_type: \"file\",\n        file_name: \"guide.pdf\",\n        storage_path: \"uploads/guide.pdf\",\n      } as MediaAsset,\n    ],\n  ]);\n\n  const fileTreeItems: KernelFileTreeProjectionItem[] = [\n    {\n      rowId: \"doc:doc_root\",\n      rowKind: \"document\",\n      nodeId: \"doc_root\",\n      parentNodeId: null,\n      nodeType: \"page\",\n      projectionKind: \"file_tree\",\n      title: \"根页面\",\n      depth: 0,\n      position: 0,\n      childCount: 3,\n      expandable: true,\n      expandedByDefault: true,\n      capabilities: [\"expand\", \"open\", \"select\"],\n      resourceMeta: {\n        resourceKind: \"document\",\n        documentId: \"doc_root\",\n        workspaceId: \"ws_1\",\n        iconHint: \"page\",\n      },\n      iconHint: \"page\",\n    },\n    {\n      rowId: \"index:doc_root\",\n      rowKind: \"index\",\n      nodeId: \"index:doc_root\",\n      parentNodeId: \"doc_root\",\n      nodeType: \"index\",\n      projectionKind: \"file_tree\",\n      title: \"index.md\",\n      depth: 1,\n      position: 0,\n      childCount: 0,\n      expandable: false,\n      expandedByDefault: false,\n      capabilities: [\"open\", \"select\"],\n      resourceMeta: {\n        resourceKind: \"index\",\n        documentId: \"doc_root\",\n        workspaceId: \"ws_1\",\n        iconHint: \"index\",\n      },\n      iconHint: \"index\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":97,"e":156,"m":1778802928217.177,"t":"position: 0,\n      childCount: 0,\n      expandable: false,\n      expandedByDefault: false,\n      capabilities: [\"open\", \"select\"],\n      resourceMeta: {\n        resourceKind: \"index\",\n        documentId: \"doc_root\",\n        workspaceId: \"ws_1\",\n        iconHint: \"index\",\n      },\n      iconHint: \"index\",\n    },\n    {\n      rowId: \"asset-folder:mind_1\",\n      rowKind: \"asset_folder\",\n      nodeId: \"asset-folder:mind_1\",\n      parentNodeId: \"doc_root\",\n      nodeType: \"mindmap\",\n      projectionKind: \"file_tree\",\n      title: \"mindmap\",\n      depth: 1,\n      position: 1,\n      childCount: 1,\n      expandable: true,\n      expandedByDefault: false,\n      capabilities: [\"expand\", \"open-asset\", \"select\"],\n      resourceMeta: {\n        resourceKind: \"mindmap\",\n        documentId: \"doc_root\",\n        assetId: \"mind_1\",\n        workspaceId: \"ws_1\",\n        assetKind: \"mindmap\",\n        iconHint: \"mindmap\",\n      },\n      iconHint: \"mindmap\",\n    },\n    {\n      rowId: \"asset:asset_child_1\",\n      rowKind: \"asset\",\n      nodeId: \"asset:asset_child_1\",\n      parentNodeId: \"asset-folder:mind_1\",\n      nodeType: \"asset\",\n      projectionKind: \"file_tree\",\n      title: \"node.png\",\n      depth: 2,\n      position: 0,\n      childCount: 0,\n      expandable: false,\n      expandedByDefault: false,\n      capabilities: [\"open-asset\", \"select\"],\n      resourceMeta: {\n        resourceKind: \"asset\",\n        documentId: \"doc_root\",\n        assetId: \"asset_child_1\",\n        workspaceId: \"ws_1\",\n        assetKind: \"image\",\n        iconHint: \"image\",\n      },\n      iconHint: \"image\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":145,"e":204,"m":1778802928217.177,"t":"expandable: false,\n      expandedByDefault: false,\n      capabilities: [\"open-asset\", \"select\"],\n      resourceMeta: {\n        resourceKind: \"asset\",\n        documentId: \"doc_root\",\n        assetId: \"asset_child_1\",\n        workspaceId: \"ws_1\",\n        assetKind: \"image\",\n        iconHint: \"image\",\n      },\n      iconHint: \"image\",\n    },\n    {\n      rowId: \"asset:pdf_1\",\n      rowKind: \"asset\",\n      nodeId: \"asset:pdf_1\",\n      parentNodeId: \"doc_root\",\n      nodeType: \"pdf\",\n      projectionKind: \"file_tree\",\n      title: \"guide.pdf\",\n      depth: 1,\n      position: 2,\n      childCount: 0,\n      expandable: false,\n      expandedByDefault: false,\n      capabilities: [\"open-asset\", \"select\"],\n      resourceMeta: {\n        resourceKind: \"pdf\",\n        documentId: \"doc_root\",\n        assetId: \"pdf_1\",\n        workspaceId: \"ws_1\",\n        assetKind: \"pdf\",\n        iconHint: \"pdf\",\n      },\n      iconHint: \"pdf\",\n    },\n  ];\n\n  it(\"应直接从 kernel file_tree items 构造宿主 row map\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    expect(buildFileTreeShellVisibleRowIds([...fileTreeItems])).toEqual([\n      \"doc:doc_root\",\n      \"index:doc_root\",\n      \"asset-folder:mind_1\",\n      \"asset:asset_child_1\",\n      \"asset:pdf_1\",\n    ]);\n\n    expect(rowById.get(\"doc:doc_root\")).toMatchObject({\n      rowId: \"doc:doc_root\",\n      rowKind: \"doc\",\n      documentId: \"doc_root\",\n      node: expect.objectContaining({\n        id: \"doc_root\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":193,"e":252,"m":1778802928217.177,"t":"\"index:doc_root\",\n      \"asset-folder:mind_1\",\n      \"asset:asset_child_1\",\n      \"asset:pdf_1\",\n    ]);\n\n    expect(rowById.get(\"doc:doc_root\")).toMatchObject({\n      rowId: \"doc:doc_root\",\n      rowKind: \"doc\",\n      documentId: \"doc_root\",\n      node: expect.objectContaining({\n        id: \"doc_root\",\n      }),\n    });\n    expect(rowById.get(\"asset-folder:mind_1\")).toMatchObject({\n      rowId: \"asset-folder:mind_1\",\n      rowKind: \"asset-folder\",\n      documentId: \"doc_root\",\n      assetId: \"mind_1\",\n      asset: expect.objectContaining({\n        id: \"mind_1\",\n      }),\n    });\n  });\n\n  it(\"应正确解析 file tree shell 的导图投放目标\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    expect(resolveFileTreeShellMindmapTargetId(rowById.get(\"asset-folder:mind_1\") ?? null)).toBe(\n      \"mind_1\",\n    );\n    expect(resolveFileTreeShellMindmapTargetId(rowById.get(\"asset:asset_child_1\") ?? null)).toBe(\n      \"mind_1\",\n    );\n    expect(resolveFileTreeShellMindmapTargetId(rowById.get(\"asset:pdf_1\") ?? null)).toBeNull();\n  });\n\n  it(\"应能仅凭 focusedRowId 从 shell row map 推回目标页面\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    expect(\n      inferFileTreeShellTargetDocumentId({\n        focusedRowId: \"asset-folder:mind_1\",\n        rowById,\n        activeDocId: null,\n      }),\n    ).toBe(\"doc_root\");\n    expect(\n      inferFileTreeShellTargetDocumentId({\n        focusedRowId: \"asset:pdf_1\",\n        rowById,\n        activeDocId: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":241,"e":300,"m":1778802928217.177,"t":"expect(\n      inferFileTreeShellTargetDocumentId({\n        focusedRowId: \"asset-folder:mind_1\",\n        rowById,\n        activeDocId: null,\n      }),\n    ).toBe(\"doc_root\");\n    expect(\n      inferFileTreeShellTargetDocumentId({\n        focusedRowId: \"asset:pdf_1\",\n        rowById,\n        activeDocId: null,\n      }),\n    ).toBe(\"doc_root\");\n    expect(\n      inferFileTreeShellTargetDocumentId({\n        focusedRowId: null,\n        rowById,\n        activeDocId: \"doc_root\",\n      }),\n    ).toBe(\"doc_root\");\n  });\n\n  it(\"应按当前可见顺序返回选中的 shell rows\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    expect(\n      getOrderedFileTreeShellRows({\n        rowIds: [\"asset:pdf_1\", \"doc:doc_root\", \"missing\"],\n        visibleRowIds: buildFileTreeShellVisibleRowIds([...fileTreeItems]),\n        rowById,\n      }).map((row) => row.rowId),\n    ).toEqual([\"doc:doc_root\", \"asset:pdf_1\"]);\n  });\n\n  it(\"内部拖放 preflight payload 应只收集 Rust 所需的行与父子快照\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    const result = buildFileTreeShellInternalDropPreflightPayload({\n      workspaceId: \"ws_1\",\n      copy: false,\n      targetDocumentId: null,\n      targetRowId: \"asset-folder:mind_1\",\n      focusedRowId: null,\n      activeDocId: null,\n      rowIds: [\"asset:asset_child_1\", \"doc:doc_root\", \"asset:pdf_1\", \"asset:pdf_1\"],\n      rowById,\n      parentById: new Map([\n        [\"doc_root\", null],\n        [\"doc_child\", \"doc_root\"],\n      ]),\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":289,"e":348,"m":1778802928217.177,"t":"copy: false,\n      targetDocumentId: null,\n      targetRowId: \"asset-folder:mind_1\",\n      focusedRowId: null,\n      activeDocId: null,\n      rowIds: [\"asset:asset_child_1\", \"doc:doc_root\", \"asset:pdf_1\", \"asset:pdf_1\"],\n      rowById,\n      parentById: new Map([\n        [\"doc_root\", null],\n        [\"doc_child\", \"doc_root\"],\n      ]),\n    });\n\n    expect(result).toEqual({\n      workspaceId: \"ws_1\",\n      copy: false,\n      sourceCapabilities: [\"read\", \"write\", \"move\"],\n      targetCapabilities: [\"read\", \"write\", \"drop\"],\n      targetDocumentId: \"doc_root\",\n      targetRowId: \"asset-folder:mind_1\",\n      focusedRowId: null,\n      activeDocumentId: null,\n      rowIds: [\"asset:asset_child_1\", \"doc:doc_root\", \"asset:pdf_1\", \"asset:pdf_1\"],\n      rows: [\n        {\n          rowId: \"asset-folder:mind_1\",\n          rowKind: \"asset-folder\",\n          documentId: \"doc_root\",\n          assetId: \"mind_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"mindmap\",\n          storagePath: \"mindmaps/mind_1/mindmap.json\",\n          title: \"mindmap.json\",\n        },\n        {\n          rowId: \"asset:asset_child_1\",\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"asset_child_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"file\",\n          storagePath: \"mindmaps/mind_1/assets/node.png\",\n          title: \"node.png\",\n        },\n        {\n          rowId: \"doc:doc_root\",\n          rowKind: \"doc\",\n          documentId: \"doc_root\",\n          assetId: null,\n          assetDocumentId: null,\n          assetType: null,\n          storagePath: null,\n          title: \"根页面\",\n        },\n        {\n          rowId: \"asset:pdf_1\",\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"pdf_1\",\n          assetDocumentId: \"doc_root\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":337,"e":396,"m":1778802928217.177,"t":"assetId: null,\n          assetDocumentId: null,\n          assetType: null,\n          storagePath: null,\n          title: \"根页面\",\n        },\n        {\n          rowId: \"asset:pdf_1\",\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"pdf_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"file\",\n          storagePath: \"uploads/guide.pdf\",\n          title: \"guide.pdf\",\n        },\n      ],\n      targetChildren: [\n        {\n          rowKind: \"asset-folder\",\n          documentId: \"doc_root\",\n          assetId: \"mind_1\",\n          title: \"mindmap.json\",\n        },\n        {\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"asset_child_1\",\n          title: \"node.png\",\n        },\n        {\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"pdf_1\",\n          title: \"guide.pdf\",\n        },\n      ],\n      documentParents: [\n        { documentId: \"doc_root\", parentId: null },\n        { documentId: \"doc_child\", parentId: \"doc_root\" },\n      ],\n      conflictPolicy: \"prompt\",\n    });\n  });\n\n  it(\"删除 preflight payload 应只收集选中行与父子快照\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    const result = buildFileTreeShellDeletePreflightPayload({\n      workspaceId: \"ws_1\",\n      rowIds: [\"doc:doc_root\", \"asset:pdf_1\", \"missing\"],\n      rowById,\n      parentById: new Map([\n        [\"doc_root\", null],\n        [\"doc_child\", \"doc_root\"],\n      ]),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":385,"e":444,"m":1778802928217.177,"t":"nodeById,\n      assetById,\n    });\n\n    const result = buildFileTreeShellDeletePreflightPayload({\n      workspaceId: \"ws_1\",\n      rowIds: [\"doc:doc_root\", \"asset:pdf_1\", \"missing\"],\n      rowById,\n      parentById: new Map([\n        [\"doc_root\", null],\n        [\"doc_child\", \"doc_root\"],\n      ]),\n    });\n\n    expect(result).toEqual({\n      workspaceId: \"ws_1\",\n      rowIds: [\"doc:doc_root\", \"asset:pdf_1\", \"missing\"],\n      rows: [\n        {\n          rowId: \"doc:doc_root\",\n          rowKind: \"doc\",\n          documentId: \"doc_root\",\n          assetId: null,\n          assetDocumentId: null,\n          assetType: null,\n          storagePath: null,\n          title: \"根页面\",\n        },\n        {\n          rowId: \"asset:pdf_1\",\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"pdf_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"file\",\n          storagePath: \"uploads/guide.pdf\",\n          title: \"guide.pdf\",\n        },\n      ],\n      documentParents: [\n        { documentId: \"doc_root\", parentId: null },\n        { documentId: \"doc_child\", parentId: \"doc_root\" },\n      ],\n    });\n  });\n\n  it(\"粘贴 preflight payload 应只收集剪贴板行与当前 focused 目标行\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    const result = buildFileTreeShellPastePreflightPayload({\n      workspaceId: \"ws_1\",\n      targetDocumentId: null,\n      focusedRowId: \"asset-folder:mind_1\",\n      activeDocId: \"doc_active\",\n      rowIds: [\"index:doc_root\", \"asset:pdf_1\", \"missing\"],\n      rowById,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":433,"e":492,"m":1778802928217.177,"t":"fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    const result = buildFileTreeShellPastePreflightPayload({\n      workspaceId: \"ws_1\",\n      targetDocumentId: null,\n      focusedRowId: \"asset-folder:mind_1\",\n      activeDocId: \"doc_active\",\n      rowIds: [\"index:doc_root\", \"asset:pdf_1\", \"missing\"],\n      rowById,\n    });\n\n    expect(result).toEqual({\n      workspaceId: \"ws_1\",\n      targetDocumentId: null,\n      focusedRowId: \"asset-folder:mind_1\",\n      activeDocumentId: \"doc_active\",\n      rowIds: [\"index:doc_root\", \"asset:pdf_1\", \"missing\"],\n      rows: [\n        {\n          rowId: \"asset-folder:mind_1\",\n          rowKind: \"asset-folder\",\n          documentId: \"doc_root\",\n          assetId: \"mind_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"mindmap\",\n          storagePath: \"mindmaps/mind_1/mindmap.json\",\n          title: \"mindmap.json\",\n        },\n        {\n          rowId: \"index:doc_root\",\n          rowKind: \"index\",\n          documentId: \"doc_root\",\n          assetId: null,\n          assetDocumentId: null,\n          assetType: null,\n          storagePath: null,\n          title: \"根页面\",\n        },\n        {\n          rowId: \"asset:pdf_1\",\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"pdf_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"file\",\n          storagePath: \"uploads/guide.pdf\",\n          title: \"guide.pdf\",\n        },\n      ],\n    });\n  });\n\n  it(\"上传目标 preflight payload 应只收集目标行、focused 行与文档工作区快照\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":481,"e":540,"m":1778802928217.177,"t":"storagePath: \"uploads/guide.pdf\",\n          title: \"guide.pdf\",\n        },\n      ],\n    });\n  });\n\n  it(\"上传目标 preflight payload 应只收集目标行、focused 行与文档工作区快照\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    const result = buildFileTreeShellUploadTargetPreflightPayload({\n      workspaceId: \"ws_fallback\",\n      targetDocumentId: null,\n      targetRowId: \"asset:asset_child_1\",\n      focusedRowId: \"asset-folder:mind_1\",\n      activeDocId: \"doc_active\",\n      rowById,\n      documentWorkspaceById: new Map([\n        [\"doc_root\", \"ws_1\"],\n        [\"doc_active\", \"ws_active\"],\n      ]),\n    });\n\n    expect(result).toEqual({\n      workspaceId: \"ws_fallback\",\n      targetDocumentId: null,\n      targetRowId: \"asset:asset_child_1\",\n      focusedRowId: \"asset-folder:mind_1\",\n      activeDocumentId: \"doc_active\",\n      rows: [\n        {\n          rowId: \"asset:asset_child_1\",\n          rowKind: \"asset\",\n          documentId: \"doc_root\",\n          assetId: \"asset_child_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"file\",\n          storagePath: \"mindmaps/mind_1/assets/node.png\",\n          title: \"node.png\",\n        },\n        {\n          rowId: \"asset-folder:mind_1\",\n          rowKind: \"asset-folder\",\n          documentId: \"doc_root\",\n          assetId: \"mind_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"mindmap\",\n          storagePath: \"mindmaps/mind_1/mindmap.json\",\n          title: \"mindmap.json\",\n        },\n      ],\n      documentWorkspaces: [\n        { documentId: \"doc_root\", workspaceId: \"ws_1\" },\n        { documentId: \"doc_active\", workspaceId: \"ws_active\" },\n      ],\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.test.ts","s":529,"e":558,"m":1778802928217.177,"t":"assetId: \"mind_1\",\n          assetDocumentId: \"doc_root\",\n          assetType: \"mindmap\",\n          storagePath: \"mindmaps/mind_1/mindmap.json\",\n          title: \"mindmap.json\",\n        },\n      ],\n      documentWorkspaces: [\n        { documentId: \"doc_root\", workspaceId: \"ws_1\" },\n        { documentId: \"doc_active\", workspaceId: \"ws_active\" },\n      ],\n    });\n  });\n\n  it(\"应能按 assetId 从 shell row map 回填 asset 与 asset-folder 的提示元数据\", () => {\n    const rowById = buildFileTreeShellRowById({\n      fileTreeItems: [...fileTreeItems],\n      nodeById,\n      assetById,\n    });\n\n    expect(\n      collectFileTreeShellAssetHints({\n        rowById,\n        assetIds: [\"mind_1\", \"pdf_1\", \"missing\"],\n      }).map((asset) => asset.id),\n    ).toEqual([\"mind_1\", \"pdf_1\"]);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":1,"e":60,"m":1778803002813.1008,"t":"\"use client\";\n\nimport type { KernelFileTreeProjectionItem } from \"@/lib/kernel-file-tree\";\nimport {\n  getDocIdFromFileTreeItem,\n  resolveFileTreeRowAsset,\n  resolveFileTreeRowNode,\n} from \"@/lib/kernel-file-tree\";\nimport type { SidebarTreeNode } from \"@/lib/kernel-sidebar\";\nimport type { MediaAsset } from \"@/types/media\";\n\nexport type FileTreeShellRowKind = \"doc\" | \"index\" | \"asset\" | \"asset-folder\";\n\nexport type FileTreeShellRow = {\n  rowId: string;\n  rowKind: FileTreeShellRowKind;\n  documentId: string;\n  assetId: string | null;\n  node: SidebarTreeNode | null;\n  asset: MediaAsset | null;\n};\n\nexport type FileTreeShellInternalDropPreflightRow = {\n  rowId: string;\n  rowKind: FileTreeShellRowKind;\n  documentId: string | null;\n  assetId: string | null;\n  assetDocumentId: string | null;\n  assetType: string | null;\n  storagePath: string | null;\n  title: string | null;\n  operationProfile?: string | null;\n};\n\nexport type FileTreeShellInternalDropPreflightTargetChild = {\n  rowKind: FileTreeShellRowKind;\n  documentId: string | null;\n  assetId: string | null;\n  title: string | null;\n};\n\nexport type FileTreeShellInternalDropPreflightPayload = {\n  workspaceId: string | null;\n  copy: boolean;\n  sourceCapabilities: string[];\n  targetCapabilities: string[];\n  targetDocumentId: string | null;\n  targetRowId: string | null;\n  focusedRowId: string | null;\n  activeDocumentId: string | null;\n  rowIds: string[];\n  rows: FileTreeShellInternalDropPreflightRow[];\n  targetChildren: FileTreeShellInternalDropPreflightTargetChild[];\n  documentParents: Array<{ documentId: string; parentId: string | null }>;\n  conflictPolicy: \"prompt\";\n};\n\nexport type FileTreeShellDeletePreflightPayload = {\n  workspaceId: string | null;\n  rowIds: string[];","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":49,"e":108,"m":1778803002813.1008,"t":"focusedRowId: string | null;\n  activeDocumentId: string | null;\n  rowIds: string[];\n  rows: FileTreeShellInternalDropPreflightRow[];\n  targetChildren: FileTreeShellInternalDropPreflightTargetChild[];\n  documentParents: Array<{ documentId: string; parentId: string | null }>;\n  conflictPolicy: \"prompt\";\n};\n\nexport type FileTreeShellDeletePreflightPayload = {\n  workspaceId: string | null;\n  rowIds: string[];\n  rows: FileTreeShellInternalDropPreflightRow[];\n  documentParents: Array<{ documentId: string; parentId: string | null }>;\n};\n\nexport type FileTreeShellPastePreflightPayload = {\n  workspaceId: string | null;\n  targetDocumentId: string | null;\n  focusedRowId: string | null;\n  activeDocumentId: string | null;\n  rowIds: string[];\n  rows: FileTreeShellInternalDropPreflightRow[];\n};\n\nexport type FileTreeShellUploadTargetPreflightPayload = {\n  workspaceId: string | null;\n  targetDocumentId: string | null;\n  targetRowId: string | null;\n  focusedRowId: string | null;\n  activeDocumentId: string | null;\n  rows: FileTreeShellInternalDropPreflightRow[];\n  documentWorkspaces: Array<{ documentId: string; workspaceId: string | null }>;\n};\n\nfunction toShellRowKind(item: KernelFileTreeProjectionItem): FileTreeShellRowKind {\n  switch (item.rowKind) {\n    case \"document\":\n      return \"doc\";\n    case \"asset_folder\":\n      return \"asset-folder\";\n    default:\n      return item.rowKind;\n  }\n}\n\nexport function buildFileTreeShellVisibleRowIds(\n  fileTreeItems: readonly KernelFileTreeProjectionItem[],\n): string[] {\n  return fileTreeItems\n    .map((item) => item.rowId)\n    .filter((rowId): rowId is string => typeof rowId === \"string\" && rowId.trim().length > 0);\n}\n\nexport function buildFileTreeShellRowById(input: {\n  fileTreeItems: readonly KernelFileTreeProjectionItem[];\n  nodeById?: Map<string, SidebarTreeNode>;\n  assetById?: Map<string, MediaAsset>;\n}): Map<string, FileTreeShellRow> {\n  const rowById = new Map<string, FileTreeShellRow>();","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":97,"e":156,"m":1778803002813.1008,"t":"): string[] {\n  return fileTreeItems\n    .map((item) => item.rowId)\n    .filter((rowId): rowId is string => typeof rowId === \"string\" && rowId.trim().length > 0);\n}\n\nexport function buildFileTreeShellRowById(input: {\n  fileTreeItems: readonly KernelFileTreeProjectionItem[];\n  nodeById?: Map<string, SidebarTreeNode>;\n  assetById?: Map<string, MediaAsset>;\n}): Map<string, FileTreeShellRow> {\n  const rowById = new Map<string, FileTreeShellRow>();\n\n  input.fileTreeItems.forEach((item) => {\n    const rowId = typeof item.rowId === \"string\" ? item.rowId.trim() : \"\";\n    if (!rowId || rowById.has(rowId)) {\n      return;\n    }\n\n    const rowKind = toShellRowKind(item);\n    const documentId = getDocIdFromFileTreeItem(item);\n    const isDocumentRow = rowKind === \"doc\" || rowKind === \"index\";\n\n    rowById.set(rowId, {\n      rowId,\n      rowKind,\n      documentId,\n      assetId: isDocumentRow ? null : item.resourceMeta.assetId ?? null,\n      node: isDocumentRow ? resolveFileTreeRowNode(item, input.nodeById) : null,\n      asset: isDocumentRow ? null : resolveFileTreeRowAsset(item, input.assetById),\n    });\n  });\n\n  return rowById;\n}\n\nexport function getOrderedFileTreeShellRows(input: {\n  rowIds: Iterable<string>;\n  visibleRowIds: readonly string[];\n  rowById: Map<string, FileTreeShellRow>;\n}): FileTreeShellRow[] {\n  const selectedRowIds = new Set<string>();\n  for (const rowId of input.rowIds) {\n    if (typeof rowId !== \"string\" || rowId.trim().length === 0) {\n      continue;\n    }\n    if (!input.rowById.has(rowId)) {\n      continue;\n    }\n    selectedRowIds.add(rowId);\n  }\n\n  return input.visibleRowIds\n    .map((rowId) => (selectedRowIds.has(rowId) ? input.rowById.get(rowId) ?? null : null))\n    .filter((row): row is FileTreeShellRow => Boolean(row));\n}\n\nexport function extractMindmapAssetIdFromStoragePath(\n  storagePath: string | null | undefined,\n): string | null {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":145,"e":204,"m":1778803002813.1008,"t":"}\n    selectedRowIds.add(rowId);\n  }\n\n  return input.visibleRowIds\n    .map((rowId) => (selectedRowIds.has(rowId) ? input.rowById.get(rowId) ?? null : null))\n    .filter((row): row is FileTreeShellRow => Boolean(row));\n}\n\nexport function extractMindmapAssetIdFromStoragePath(\n  storagePath: string | null | undefined,\n): string | null {\n  if (!storagePath) return null;\n  const normalized = storagePath.replaceAll(\"\\\\\", \"/\");\n\n  const prefix = \"mindmaps/\";\n  if (normalized.startsWith(prefix)) {\n    const rest = normalized.slice(prefix.length);\n    const id = rest.split(\"/\")[0];\n    return id ? id : null;\n  }\n\n  const marker = \"/mindmaps/\";\n  const idx = normalized.indexOf(marker);\n  if (idx === -1) return null;\n  const rest = normalized.slice(idx + marker.length);\n  const id = rest.split(\"/\")[0];\n  return id ? id : null;\n}\n\nexport function resolveFileTreeShellMindmapTargetId(\n  row: FileTreeShellRow | null,\n): string | null {\n  if (!row?.asset) {\n    return null;\n  }\n  if (row.rowKind === \"asset-folder\" && row.asset.asset_type === \"mindmap\") {\n    return row.asset.id;\n  }\n  if (row.rowKind === \"asset\") {\n    return extractMindmapAssetIdFromStoragePath(row.asset.storage_path);\n  }\n  return null;\n}\n\nexport function inferFileTreeShellTargetDocumentId(input: {\n  focusedRowId: string | null;\n  rowById: Map<string, FileTreeShellRow>;\n  activeDocId: string | null;\n}): string | null {\n  if (input.focusedRowId) {\n    const row = input.rowById.get(input.focusedRowId) ?? null;\n    if (row?.documentId) {\n      return row.documentId;\n    }\n  }\n  return input.activeDocId || null;\n}\n\nfunction normalizeShellText(value: string | null | undefined): string | null {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":193,"e":252,"m":1778803002813.1008,"t":"activeDocId: string | null;\n}): string | null {\n  if (input.focusedRowId) {\n    const row = input.rowById.get(input.focusedRowId) ?? null;\n    if (row?.documentId) {\n      return row.documentId;\n    }\n  }\n  return input.activeDocId || null;\n}\n\nfunction normalizeShellText(value: string | null | undefined): string | null {\n  if (typeof value !== \"string\") return null;\n  const trimmed = value.trim();\n  return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction getFileTreeShellRowTitle(row: FileTreeShellRow): string | null {\n  if (row.rowKind === \"doc\" || row.rowKind === \"index\") {\n    return normalizeShellText(row.node?.title ?? null);\n  }\n  return normalizeShellText(row.asset?.file_name ?? null);\n}\n\nfunction buildFileTreeShellDropPreflightRow(\n  row: FileTreeShellRow,\n): FileTreeShellInternalDropPreflightRow {\n  return {\n    rowId: row.rowId,\n    rowKind: row.rowKind,\n    documentId: normalizeShellText(row.documentId),\n    assetId: normalizeShellText(row.assetId),\n    assetDocumentId: normalizeShellText(row.asset?.document_id ?? null),\n    assetType: normalizeShellText(row.asset?.asset_type ?? null),\n    storagePath: normalizeShellText(row.asset?.storage_path ?? null),\n    title: getFileTreeShellRowTitle(row),\n  };\n}\n\nfunction buildFileTreeShellTargetChildren(input: {\n  targetDocumentId: string | null;\n  rowById: Map<string, FileTreeShellRow>;\n  parentById: Map<string, string | null>;\n}): FileTreeShellInternalDropPreflightTargetChild[] {\n  const targetDocumentId = normalizeShellText(input.targetDocumentId);\n  if (!targetDocumentId) {\n    return [];\n  }\n  return Array.from(input.rowById.values())\n    .filter((row) => {\n      if (row.rowKind === \"doc\" || row.rowKind === \"index\") {\n        return input.parentById.get(row.documentId) === targetDocumentId;\n      }\n      return row.documentId === targetDocumentId;\n    })\n    .map((row) => ({\n      rowKind: row.rowKind,\n      documentId: normalizeShellText(row.documentId),\n      assetId: normalizeShellText(row.assetId),\n      title: getFileTreeShellRowTitle(row),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":241,"e":300,"m":1778803002813.1008,"t":"return Array.from(input.rowById.values())\n    .filter((row) => {\n      if (row.rowKind === \"doc\" || row.rowKind === \"index\") {\n        return input.parentById.get(row.documentId) === targetDocumentId;\n      }\n      return row.documentId === targetDocumentId;\n    })\n    .map((row) => ({\n      rowKind: row.rowKind,\n      documentId: normalizeShellText(row.documentId),\n      assetId: normalizeShellText(row.assetId),\n      title: getFileTreeShellRowTitle(row),\n    }));\n}\n\nexport function buildFileTreeShellInternalDropPreflightPayload(input: {\n  workspaceId: string | null;\n  copy: boolean;\n  targetDocumentId: string | null;\n  targetRowId: string | null;\n  focusedRowId: string | null;\n  activeDocId: string | null;\n  rowIds: readonly string[];\n  rowById: Map<string, FileTreeShellRow>;\n  parentById: Map<string, string | null>;\n}): FileTreeShellInternalDropPreflightPayload {\n  const rowIdSet = new Set<string>();\n  const appendRowId = (value: string | null | undefined) => {\n    const rowId = normalizeShellText(value);\n    if (rowId) {\n      rowIdSet.add(rowId);\n    }\n  };\n  const rowIds = input.rowIds.map((rowId) => (typeof rowId === \"string\" ? rowId : \"\"));\n  const targetRowId = normalizeShellText(input.targetRowId);\n  const focusedRowId = normalizeShellText(input.focusedRowId);\n  appendRowId(targetRowId);\n  appendRowId(focusedRowId);\n  rowIds.forEach(appendRowId);\n\n  const rows = Array.from(rowIdSet)\n    .map((rowId) => input.rowById.get(rowId) ?? null)\n    .filter((row): row is FileTreeShellRow => Boolean(row))\n    .map(buildFileTreeShellDropPreflightRow);\n  const targetDocumentId =\n    normalizeShellText(input.targetDocumentId) ??\n    inferFileTreeShellTargetDocumentId({\n      focusedRowId: targetRowId ?? focusedRowId,\n      rowById: input.rowById,\n      activeDocId: input.activeDocId,\n    });\n\n  return {\n    workspaceId: normalizeShellText(input.workspaceId),\n    copy: input.copy,\n    sourceCapabilities: [\"read\", \"write\", input.copy ? \"copy\" : \"move\"],\n    targetCapabilities: [\"read\", \"write\", \"drop\"],\n    targetDocumentId,\n    targetRowId,\n    focusedRowId,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":289,"e":348,"m":1778803002813.1008,"t":"rowById: input.rowById,\n      activeDocId: input.activeDocId,\n    });\n\n  return {\n    workspaceId: normalizeShellText(input.workspaceId),\n    copy: input.copy,\n    sourceCapabilities: [\"read\", \"write\", input.copy ? \"copy\" : \"move\"],\n    targetCapabilities: [\"read\", \"write\", \"drop\"],\n    targetDocumentId,\n    targetRowId,\n    focusedRowId,\n    activeDocumentId: normalizeShellText(input.activeDocId),\n    rowIds,\n    rows,\n    targetChildren: buildFileTreeShellTargetChildren({\n      targetDocumentId,\n      rowById: input.rowById,\n      parentById: input.parentById,\n    }),\n    documentParents: Array.from(input.parentById.entries()).map(([documentId, parentId]) => ({\n      documentId,\n      parentId: normalizeShellText(parentId),\n    })),\n    conflictPolicy: \"prompt\",\n  };\n}\n\nexport function buildFileTreeShellDeletePreflightPayload(input: {\n  workspaceId: string | null;\n  rowIds: readonly string[];\n  rowById: Map<string, FileTreeShellRow>;\n  parentById: Map<string, string | null>;\n}): FileTreeShellDeletePreflightPayload {\n  const rowIds = input.rowIds.map((rowId) => (typeof rowId === \"string\" ? rowId : \"\"));\n  const rowIdSet = new Set<string>();\n  rowIds.forEach((rowId) => {\n    const normalized = normalizeShellText(rowId);\n    if (normalized) {\n      rowIdSet.add(normalized);\n    }\n  });\n\n  const rows = Array.from(rowIdSet)\n    .map((rowId) => input.rowById.get(rowId) ?? null)\n    .filter((row): row is FileTreeShellRow => Boolean(row))\n    .map(buildFileTreeShellDropPreflightRow);\n\n  return {\n    workspaceId: normalizeShellText(input.workspaceId),\n    rowIds,\n    rows,\n    documentParents: Array.from(input.parentById.entries()).map(([documentId, parentId]) => ({\n      documentId,\n      parentId: normalizeShellText(parentId),\n    })),\n  };\n}\n\nexport function buildFileTreeShellPastePreflightPayload(input: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":337,"e":396,"m":1778803002813.1008,"t":"return {\n    workspaceId: normalizeShellText(input.workspaceId),\n    rowIds,\n    rows,\n    documentParents: Array.from(input.parentById.entries()).map(([documentId, parentId]) => ({\n      documentId,\n      parentId: normalizeShellText(parentId),\n    })),\n  };\n}\n\nexport function buildFileTreeShellPastePreflightPayload(input: {\n  workspaceId: string | null;\n  targetDocumentId: string | null;\n  focusedRowId: string | null;\n  activeDocId: string | null;\n  rowIds: readonly string[];\n  rowById: Map<string, FileTreeShellRow>;\n}): FileTreeShellPastePreflightPayload {\n  const rowIds = input.rowIds.map((rowId) => (typeof rowId === \"string\" ? rowId : \"\"));\n  const rowIdSet = new Set<string>();\n  const focusedRowId = normalizeShellText(input.focusedRowId);\n  if (focusedRowId) {\n    rowIdSet.add(focusedRowId);\n  }\n  rowIds.forEach((rowId) => {\n    const normalized = normalizeShellText(rowId);\n    if (normalized) {\n      rowIdSet.add(normalized);\n    }\n  });\n\n  const rows = Array.from(rowIdSet)\n    .map((rowId) => input.rowById.get(rowId) ?? null)\n    .filter((row): row is FileTreeShellRow => Boolean(row))\n    .map(buildFileTreeShellDropPreflightRow);\n\n  return {\n    workspaceId: normalizeShellText(input.workspaceId),\n    targetDocumentId: normalizeShellText(input.targetDocumentId),\n    focusedRowId,\n    activeDocumentId: normalizeShellText(input.activeDocId),\n    rowIds,\n    rows,\n  };\n}\n\nexport function buildFileTreeShellUploadTargetPreflightPayload(input: {\n  workspaceId: string | null;\n  targetDocumentId: string | null;\n  targetRowId: string | null;\n  focusedRowId: string | null;\n  activeDocId: string | null;\n  rowById: Map<string, FileTreeShellRow>;\n  documentWorkspaceById: Map<string, string | null>;\n}): FileTreeShellUploadTargetPreflightPayload {\n  const rowIdSet = new Set<string>();\n  const appendRowId = (value: string | null | undefined) => {\n    const rowId = normalizeShellText(value);\n    if (rowId) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":385,"e":444,"m":1778803002813.1008,"t":"workspaceId: string | null;\n  targetDocumentId: string | null;\n  targetRowId: string | null;\n  focusedRowId: string | null;\n  activeDocId: string | null;\n  rowById: Map<string, FileTreeShellRow>;\n  documentWorkspaceById: Map<string, string | null>;\n}): FileTreeShellUploadTargetPreflightPayload {\n  const rowIdSet = new Set<string>();\n  const appendRowId = (value: string | null | undefined) => {\n    const rowId = normalizeShellText(value);\n    if (rowId) {\n      rowIdSet.add(rowId);\n    }\n  };\n  const targetRowId = normalizeShellText(input.targetRowId);\n  const focusedRowId = normalizeShellText(input.focusedRowId);\n  appendRowId(targetRowId);\n  appendRowId(focusedRowId);\n\n  const rows = Array.from(rowIdSet)\n    .map((rowId) => input.rowById.get(rowId) ?? null)\n    .filter((row): row is FileTreeShellRow => Boolean(row))\n    .map(buildFileTreeShellDropPreflightRow);\n\n  return {\n    workspaceId: normalizeShellText(input.workspaceId),\n    targetDocumentId: normalizeShellText(input.targetDocumentId),\n    targetRowId,\n    focusedRowId,\n    activeDocumentId: normalizeShellText(input.activeDocId),\n    rows,\n    documentWorkspaces: Array.from(input.documentWorkspaceById.entries()).map(\n      ([documentId, workspaceId]) => ({\n        documentId,\n        workspaceId: normalizeShellText(workspaceId),\n      }),\n    ),\n  };\n}\n\nexport function collectFileTreeShellAssetHints(input: {\n  rowById: Map<string, FileTreeShellRow>;\n  assetIds: readonly string[];\n}): MediaAsset[] {\n  const hints: MediaAsset[] = [];\n  const seen = new Set<string>();\n  input.assetIds.forEach((assetId) => {\n    const normalized = normalizeShellText(assetId);\n    if (!normalized || seen.has(normalized)) {\n      return;\n    }\n    const asset =\n      input.rowById.get(`asset:${normalized}`)?.asset ??\n      input.rowById.get(`asset-folder:${normalized}`)?.asset ??\n      null;\n    if (!asset) {\n      return;\n    }\n    seen.add(normalized);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/shell.ts","s":433,"e":449,"m":1778803002813.1008,"t":"const normalized = normalizeShellText(assetId);\n    if (!normalized || seen.has(normalized)) {\n      return;\n    }\n    const asset =\n      input.rowById.get(`asset:${normalized}`)?.asset ??\n      input.rowById.get(`asset-folder:${normalized}`)?.asset ??\n      null;\n    if (!asset) {\n      return;\n    }\n    seen.add(normalized);\n    hints.push(asset);\n  });\n  return hints;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/types.ts","s":1,"e":60,"m":1776339382549.111,"t":"\"use client\";\n\nimport type { SidebarTreeNode } from \"@/lib/kernel-sidebar\";\nimport type { MediaAsset } from \"@/types/media\";\n\nexport type FileTreeRowKind = \"doc\" | \"index\" | \"asset\" | \"asset-folder\";\n\nexport type FileTreeRowId =\n  | `doc:${string}`\n  | `index:${string}`\n  | `asset:${string}`\n  | `asset-folder:${string}`;\n\nexport type ParsedFileTreeRowId =\n  | { kind: \"doc\"; docId: string }\n  | { kind: \"index\"; docId: string }\n  | { kind: \"asset\"; assetId: string }\n  | { kind: \"asset-folder\"; assetId: string };\n\nexport function makeDocRowId(docId: string): FileTreeRowId {\n  return `doc:${docId}`;\n}\n\nexport function makeIndexRowId(docId: string): FileTreeRowId {\n  return `index:${docId}`;\n}\n\nexport function makeAssetRowId(assetId: string): FileTreeRowId {\n  return `asset:${assetId}`;\n}\n\nexport function makeAssetFolderRowId(assetId: string): FileTreeRowId {\n  return `asset-folder:${assetId}`;\n}\n\nexport function parseFileTreeRowId(rowId: string): ParsedFileTreeRowId | null {\n  const idx = rowId.indexOf(\":\");\n  if (idx <= 0) return null;\n  const prefix = rowId.slice(0, idx);\n  const rest = rowId.slice(idx + 1);\n  if (!rest) return null;\n  switch (prefix) {\n    case \"doc\":\n      return { kind: \"doc\", docId: rest };\n    case \"index\":\n      return { kind: \"index\", docId: rest };\n    case \"asset\":\n      return { kind: \"asset\", assetId: rest };\n    case \"asset-folder\":\n      return { kind: \"asset-folder\", assetId: rest };\n    default:\n      return null;\n  }\n}\n\nexport type FileTreeRow =\n  | {\n      kind: \"doc\";\n      rowId: FileTreeRowId;\n      depth: number;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/types.ts","s":49,"e":108,"m":1776339382549.111,"t":"case \"asset-folder\":\n      return { kind: \"asset-folder\", assetId: rest };\n    default:\n      return null;\n  }\n}\n\nexport type FileTreeRow =\n  | {\n      kind: \"doc\";\n      rowId: FileTreeRowId;\n      depth: number;\n      docId: string;\n      parentDocId: string | null;\n      node: SidebarTreeNode;\n      hasChildren: boolean;\n      isExpanded: boolean;\n    }\n  | {\n      kind: \"index\";\n      rowId: FileTreeRowId;\n      depth: number;\n      docId: string;\n      parentDocId: string;\n      node: SidebarTreeNode;\n    }\n  | {\n      kind: \"asset-folder\";\n      rowId: FileTreeRowId;\n      depth: number;\n      docId: string;\n      parentDocId: string;\n      asset: MediaAsset;\n      hasChildren: boolean;\n      isExpanded: boolean;\n    }\n  | {\n      kind: \"asset\";\n      rowId: FileTreeRowId;\n      depth: number;\n      docId: string;\n      parentDocId: string;\n      asset: MediaAsset;\n    };\n\nexport function getFileTreeRowLabel(row: FileTreeRow): string {\n  switch (row.kind) {\n    case \"doc\":\n      return row.node.title || \"无标题\";\n    case \"index\":\n      return \"index.md\";\n    case \"asset-folder\": {\n      const name = row.asset.file_name || \"附件\";\n      // 思维导图展示为“文件夹”时，去掉 .json 结尾更直观\n      return row.asset.asset_type === \"mindmap\" ? name.replace(/\\.json$/i, \"\") : name;\n    }\n    case \"asset\":\n      return row.asset.file_name || \"附件\";\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/file-tree/types.ts","s":97,"e":120,"m":1776339382549.111,"t":"return row.node.title || \"无标题\";\n    case \"index\":\n      return \"index.md\";\n    case \"asset-folder\": {\n      const name = row.asset.file_name || \"附件\";\n      // 思维导图展示为“文件夹”时，去掉 .json 结尾更直观\n      return row.asset.asset_type === \"mindmap\" ? name.replace(/\\.json$/i, \"\") : name;\n    }\n    case \"asset\":\n      return row.asset.file_name || \"附件\";\n  }\n}\n\nexport function getOwningDocId(row: FileTreeRow): string {\n  switch (row.kind) {\n    case \"doc\":\n    case \"index\":\n      return row.docId;\n    case \"asset-folder\":\n    case \"asset\":\n      return row.asset.document_id;\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.test.ts","s":1,"e":60,"m":1777197370737.2551,"t":"import { afterEach, describe, expect, it, vi } from \"vitest\";\nimport type { ConvexHttpClient } from \"convex/browser\";\n\nvi.mock(\"@/lib/auth/authContext\", () => ({\n  HttpError: class HttpError extends Error {\n    status: number;\n\n    constructor(status: number, message: string) {\n      super(message);\n      this.status = status;\n    }\n  },\n  requireAuthContext: vi.fn(async () => ({\n    userId: \"user_1\",\n  })),\n}));\n\nvi.mock(\"@/lib/api-utils\", () => ({\n  apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({\n    message,\n    status,\n    details,\n  })),\n}));\n\nvi.mock(\"@/lib/convex/api\", () => ({\n  api: {\n    documents: {\n      getContent: \"documents:getContent\",\n    },\n  },\n}));\n\nvi.mock(\"@/lib/convex/route\", () => ({\n  getAuthedConvexClient: vi.fn(),\n}));\n\nvi.mock(\"@/lib/documents/bridge-log\", () => ({\n  recordBridgeCommandArtifacts: vi.fn(),\n  recordBridgeCommandFailureArtifacts: vi.fn(),\n  recordRustBridgeCommandArtifacts: vi.fn(),\n}));\n\nvi.mock(\"@/lib/documents/rust-runtime\", () => ({\n  resolveRustBridgeCommandPlan: vi.fn(),\n  resolveRustBridgeQueryPlan: vi.fn(),\n  executeRustBridgeQueryTransport: vi.fn(),\n  executeRustBridgeMutationTransport: vi.fn(),\n}));\n\nvi.mock(\"@/lib/document-content\", () => ({\n  extractBlocksFromContent: vi.fn(),\n}));\n\nafterEach(() => {\n  vi.clearAllMocks();\n});\n\ndescribe(\"documents/block-command-adapter\", () => {\n  it(\"executeBlockPatchBridgeCommand 应改走 Rust artifact writer\", async () => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.test.ts","s":49,"e":108,"m":1777197370737.2551,"t":"}));\n\nvi.mock(\"@/lib/document-content\", () => ({\n  extractBlocksFromContent: vi.fn(),\n}));\n\nafterEach(() => {\n  vi.clearAllMocks();\n});\n\ndescribe(\"documents/block-command-adapter\", () => {\n  it(\"executeBlockPatchBridgeCommand 应改走 Rust artifact writer\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const {\n      recordBridgeCommandArtifacts,\n      recordRustBridgeCommandArtifacts,\n    } = await import(\"@/lib/documents/bridge-log\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    const { extractBlocksFromContent } = await import(\"@/lib/document-content\");\n    const { executeBlockPatchBridgeCommand } = await import(\"./block-command-adapter\");\n\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        query: vi.fn().mockResolvedValue({\n          content: { type: \"doc\" },\n        }),\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(extractBlocksFromContent).mockReturnValue([\n      {\n        id: \"blk_1\",\n        type: \"paragraph\",\n      },\n    ]);\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"blocks.patch\",\n      commandId: \"cmd_block_patch_1\",\n      functionName: \"documents:updateContent\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        blockId: \"blk_1\",\n        nextBlock: {\n          id: \"blk_1\",\n          type: \"heading\",\n        },\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.test.ts","s":97,"e":146,"m":1777197370737.2551,"t":"idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        blockId: \"blk_1\",\n        nextBlock: {\n          id: \"blk_1\",\n          type: \"heading\",\n        },\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      revision: 8,\n      conflict_detection_key: \"doc_1:8\",\n    });\n\n    const result = await executeBlockPatchBridgeCommand({\n      request: new Request(\"http://127.0.0.1:3000/api/documents/blocks/patch\", {\n        method: \"POST\",\n      }),\n      sourceDocumentId: \"doc_1\",\n      workspaceId: \"ws_1\",\n      blockId: \"blk_1\",\n      nextBlock: {\n        id: \"blk_1\",\n        type: \"heading\",\n      },\n    });\n\n    expect(result.commandName).toBe(\"blocks.patch\");\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: expect.objectContaining({\n        workspaceId: \"ws_1\",\n      }),\n      envelope: expect.objectContaining({\n        name: \"blocks.patch\",\n      }),\n      plan: expect.objectContaining({\n        commandName: \"blocks.patch\",\n      }),\n      result: {\n        revision: 8,\n        conflict_detection_key: \"doc_1:8\",\n      },\n    });\n    expect(recordBridgeCommandArtifacts).not.toHaveBeenCalled();\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.ts","s":1,"e":60,"m":1777197392904.5415,"t":"import { api } from \"@/lib/convex/api\";\nimport { getAuthedConvexClient } from \"@/lib/convex/route\";\nimport {\n  buildDocumentBridgeContext,\n  buildDocumentCommandEnvelope,\n  buildDocumentQueryEnvelope,\n  documentBridgeErrorResponse,\n  type BridgeContext,\n} from \"@/lib/documents/bridge\";\nimport {\n  recordBridgeCommandFailureArtifacts,\n  recordRustBridgeCommandArtifacts,\n} from \"@/lib/documents/bridge-log\";\nimport {\n  executeRustBridgeMutationTransport,\n  executeRustBridgeQueryTransport,\n  resolveRustBridgeCommandPlan,\n  resolveRustBridgeQueryPlan,\n} from \"@/lib/documents/rust-runtime\";\nimport { extractBlocksFromContent } from \"@/lib/document-content\";\n\ntype BlockLike = {\n  id: string;\n  type?: string;\n  props?: Record<string, unknown>;\n  content?: unknown;\n  children?: BlockLike[];\n};\n\nfunction cloneBlock(block: BlockLike): BlockLike {\n  return {\n    ...block,\n    props: block.props ? { ...block.props } : undefined,\n    content: Array.isArray(block.content) ? [...block.content] : block.content,\n    children: Array.isArray(block.children) ? block.children.map((item) => cloneBlock(item)) : block.children,\n  };\n}\n\nfunction findBlockInTree(\n  blocks: BlockLike[],\n  blockId: string,\n): { block: BlockLike; parent: BlockLike | null; index: number } | null {\n  const stack: Array<{ list: BlockLike[]; parent: BlockLike | null }> = [{ list: blocks, parent: null }];\n  while (stack.length > 0) {\n    const current = stack.pop();\n    if (!current) continue;\n    for (let i = 0; i < current.list.length; i += 1) {\n      const block = current.list[i]!;\n      if (block.id === blockId) {\n        return { block, parent: current.parent, index: i };\n      }\n      if (Array.isArray(block.children) && block.children.length > 0) {\n        stack.push({ list: block.children, parent: block });\n      }\n    }\n  }\n  return null;\n}\n\nfunction removeBlockSubtree(blocks: BlockLike[], blockId: string) {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.ts","s":49,"e":108,"m":1777197392904.5415,"t":"if (block.id === blockId) {\n        return { block, parent: current.parent, index: i };\n      }\n      if (Array.isArray(block.children) && block.children.length > 0) {\n        stack.push({ list: block.children, parent: block });\n      }\n    }\n  }\n  return null;\n}\n\nfunction removeBlockSubtree(blocks: BlockLike[], blockId: string) {\n  const nextTop = blocks.map((item) => cloneBlock(item));\n  const hit = findBlockInTree(nextTop, blockId);\n  if (!hit) {\n    return { removed: null as BlockLike | null, nextBlocks: nextTop };\n  }\n  if (hit.parent) {\n    const nextChildren = Array.isArray(hit.parent.children) ? hit.parent.children.map((item) => cloneBlock(item)) : [];\n    const removed = nextChildren.splice(hit.index, 1)[0] ?? null;\n    hit.parent.children = nextChildren;\n    return { removed, nextBlocks: nextTop };\n  }\n  const removed = nextTop.splice(hit.index, 1)[0] ?? null;\n  return { removed, nextBlocks: nextTop };\n}\n\nfunction replaceBlockInTree(blocks: BlockLike[], blockId: string, nextBlock: unknown) {\n  const nextTop = blocks.map((item) => cloneBlock(item));\n  const hit = findBlockInTree(nextTop, blockId);\n  if (!hit || !nextBlock || typeof nextBlock !== \"object\" || Array.isArray(nextBlock)) {\n    return { ok: false, nextBlocks: nextTop };\n  }\n  const normalized = cloneBlock({ ...(nextBlock as BlockLike), id: blockId });\n  if (hit.parent) {\n    const nextChildren = Array.isArray(hit.parent.children) ? hit.parent.children.map((item) => cloneBlock(item)) : [];\n    nextChildren[hit.index] = normalized;\n    hit.parent.children = nextChildren;\n    return { ok: true, nextBlocks: nextTop };\n  }\n  nextTop[hit.index] = normalized;\n  return { ok: true, nextBlocks: nextTop };\n}\n\nasync function buildBridgeContext(request: Request, workspaceId: string | null): Promise<BridgeContext> {\n  return await buildDocumentBridgeContext({ request, workspaceId });\n}\n\nexport async function executeBlockGetBridgeQuery(input: {\n  request: Request;\n  sourceDocumentId: string;\n  blockId: string;\n}) {\n  const context = await buildBridgeContext(input.request, null);\n  const envelope = buildDocumentQueryEnvelope({\n    name: \"blocks.get\",\n    payload: {\n      sourceDocumentId: input.sourceDocumentId,\n      blockId: input.blockId,\n    },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.ts","s":97,"e":156,"m":1777197392904.5415,"t":"export async function executeBlockGetBridgeQuery(input: {\n  request: Request;\n  sourceDocumentId: string;\n  blockId: string;\n}) {\n  const context = await buildBridgeContext(input.request, null);\n  const envelope = buildDocumentQueryEnvelope({\n    name: \"blocks.get\",\n    payload: {\n      sourceDocumentId: input.sourceDocumentId,\n      blockId: input.blockId,\n    },\n  });\n  const plan = await resolveRustBridgeQueryPlan({ context, envelope });\n  const { client } = await getAuthedConvexClient();\n  const doc = await executeRustBridgeQueryTransport<{ content?: unknown } | null>({\n    client,\n    plan,\n  });\n  if (!doc) {\n    throw new Error(\"页面不存在或无权限\");\n  }\n  const blocks = extractBlocksFromContent(doc.content) as BlockLike[];\n  const hit = findBlockInTree(blocks, input.blockId);\n  if (!hit) {\n    throw new Error(\"块不存在或无权限\");\n  }\n  return {\n    requestId: context.requestId,\n    traceId: context.traceId,\n    queryName: envelope.name,\n    result: { block: hit.block },\n  };\n}\n\nexport async function executeBlockPatchBridgeCommand(input: {\n  request: Request;\n  sourceDocumentId: string;\n  workspaceId: string | null;\n  blockId: string;\n  nextBlock: unknown;\n}) {\n  const context = await buildBridgeContext(input.request, input.workspaceId);\n  const envelope = buildDocumentCommandEnvelope({\n    name: \"blocks.patch\",\n    payload: {\n      documentId: input.sourceDocumentId,\n      workspaceId: input.workspaceId,\n      blockId: input.blockId,\n      nextBlock: input.nextBlock,\n    },\n    context,\n    target: {\n      workspaceId: input.workspaceId,\n      pageId: input.sourceDocumentId,\n      blockId: input.blockId,\n    },\n  });\n  const plan = await resolveRustBridgeCommandPlan({ context, envelope });\n  const { client } = await getAuthedConvexClient();","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.ts","s":145,"e":204,"m":1777197392904.5415,"t":"blockId: input.blockId,\n      nextBlock: input.nextBlock,\n    },\n    context,\n    target: {\n      workspaceId: input.workspaceId,\n      pageId: input.sourceDocumentId,\n      blockId: input.blockId,\n    },\n  });\n  const plan = await resolveRustBridgeCommandPlan({ context, envelope });\n  const { client } = await getAuthedConvexClient();\n  const doc = await client.query(api.documents.getContent, { id: input.sourceDocumentId });\n  if (!doc) {\n    throw new Error(\"页面不存在或无权限\");\n  }\n  const blocks = extractBlocksFromContent(doc.content) as BlockLike[];\n  const replaced = replaceBlockInTree(blocks, input.blockId, input.nextBlock);\n  if (!replaced.ok) {\n    throw new Error(\"块不存在或无权限\");\n  }\n  try {\n    const transportResult = await executeRustBridgeMutationTransport({\n      client,\n      plan,\n    });\n    await recordRustBridgeCommandArtifacts({\n      client,\n      context,\n      envelope,\n      plan,\n      result: transportResult,\n    });\n  } catch (error) {\n    await recordBridgeCommandFailureArtifacts({\n      client,\n      context,\n      envelope,\n      error,\n    });\n    throw error;\n  }\n  return {\n    requestId: context.requestId,\n    traceId: context.traceId,\n    commandId: envelope.commandId,\n    commandName: envelope.name,\n    result: { ok: true as const },\n  };\n}\n\nexport async function executeBlockMoveBridgeCommand(input: {\n  request: Request;\n  sourceDocumentId: string;\n  targetDocumentId: string;\n  blockId: string;\n}) {\n  const { client } = await getAuthedConvexClient();\n  const source = await client.query(api.documents.getContent, { id: input.sourceDocumentId });\n  if (!source) throw new Error(\"源页面不存在或无权限\");","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.ts","s":193,"e":252,"m":1777197392904.5415,"t":"};\n}\n\nexport async function executeBlockMoveBridgeCommand(input: {\n  request: Request;\n  sourceDocumentId: string;\n  targetDocumentId: string;\n  blockId: string;\n}) {\n  const { client } = await getAuthedConvexClient();\n  const source = await client.query(api.documents.getContent, { id: input.sourceDocumentId });\n  if (!source) throw new Error(\"源页面不存在或无权限\");\n  const target = await client.query(api.documents.getContent, { id: input.targetDocumentId });\n  if (!target) throw new Error(\"目标页面不存在或无权限\");\n  const sourceBlocks = extractBlocksFromContent(source.content) as BlockLike[];\n  const removedRes = removeBlockSubtree(sourceBlocks, input.blockId);\n  if (!removedRes.removed) throw new Error(\"源块不存在或无权限\");\n  const context = await buildBridgeContext(input.request, null);\n  const envelope = buildDocumentCommandEnvelope({\n    name: \"blocks.move\",\n    payload: {\n      sourceDocumentId: input.sourceDocumentId,\n      targetDocumentId: input.targetDocumentId,\n      blockId: input.blockId,\n    },\n    context,\n    target: {\n      pageId: input.targetDocumentId,\n      blockId: input.blockId,\n    },\n  });\n  const plan = await resolveRustBridgeCommandPlan({ context, envelope });\n  try {\n    const transportResult = await executeRustBridgeMutationTransport({ client, plan });\n    await recordRustBridgeCommandArtifacts({\n      client,\n      context,\n      envelope,\n      plan,\n      result: transportResult,\n    });\n  } catch (error) {\n    await recordBridgeCommandFailureArtifacts({\n      client,\n      context,\n      envelope,\n      error,\n    });\n    throw error;\n  }\n  return {\n    requestId: context.requestId,\n    traceId: context.traceId,\n    commandId: envelope.commandId,\n    commandName: envelope.name,\n    result: { ok: true as const },\n  };\n}\n\nexport async function executeBlockEmbedBridgeCommand(input: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.ts","s":241,"e":300,"m":1777197392904.5415,"t":"throw error;\n  }\n  return {\n    requestId: context.requestId,\n    traceId: context.traceId,\n    commandId: envelope.commandId,\n    commandName: envelope.name,\n    result: { ok: true as const },\n  };\n}\n\nexport async function executeBlockEmbedBridgeCommand(input: {\n  request: Request;\n  sourceDocumentId: string;\n  targetDocumentId: string;\n  blockId: string;\n}) {\n  const { client } = await getAuthedConvexClient();\n  const source = await client.query(api.documents.getContent, { id: input.sourceDocumentId });\n  if (!source) throw new Error(\"源页面不存在或无权限\");\n  const target = await client.query(api.documents.getContent, { id: input.targetDocumentId });\n  if (!target) throw new Error(\"目标页面不存在或无权限\");\n  const targetMeta = await client.query(api.documents.getMeta, { id: input.targetDocumentId });\n  const sourceBlocks = extractBlocksFromContent(source.content) as BlockLike[];\n  const hit = findBlockInTree(sourceBlocks, input.blockId);\n  if (!hit) throw new Error(\"源块不存在或无权限\");\n  const anchorId = (targetMeta as { embed_default_block_id?: string | null } | null)?.embed_default_block_id ?? null;\n  const anchorIndex =\n    typeof anchorId === \"string\" && anchorId.trim()\n      ? extractBlocksFromContent(target.content).findIndex((block) => String((block as BlockLike).id ?? \"\") === anchorId)\n      : -1;\n  const targetBlocks = extractBlocksFromContent(target.content) as BlockLike[];\n  void targetBlocks;\n  void anchorIndex;\n  const context = await buildBridgeContext(input.request, null);\n  const envelope = buildDocumentCommandEnvelope({\n    name: \"blocks.embed\",\n    payload: {\n      sourceDocumentId: input.sourceDocumentId,\n      targetDocumentId: input.targetDocumentId,\n      blockId: input.blockId,\n    },\n    context,\n    target: {\n      pageId: input.targetDocumentId,\n      blockId: input.blockId,\n    },\n  });\n  const plan = await resolveRustBridgeCommandPlan({ context, envelope });\n  try {\n    const transportResult = await executeRustBridgeMutationTransport({ client, plan });\n    await recordRustBridgeCommandArtifacts({\n      client,\n      context,\n      envelope,\n      plan,\n      result: transportResult,\n    });\n  } catch (error) {\n    await recordBridgeCommandFailureArtifacts({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/block-command-adapter.ts","s":289,"e":320,"m":1777197392904.5415,"t":"const plan = await resolveRustBridgeCommandPlan({ context, envelope });\n  try {\n    const transportResult = await executeRustBridgeMutationTransport({ client, plan });\n    await recordRustBridgeCommandArtifacts({\n      client,\n      context,\n      envelope,\n      plan,\n      result: transportResult,\n    });\n  } catch (error) {\n    await recordBridgeCommandFailureArtifacts({\n      client,\n      context,\n      envelope,\n      error,\n    });\n    throw error;\n  }\n  return {\n    requestId: context.requestId,\n    traceId: context.traceId,\n    commandId: envelope.commandId,\n    commandName: envelope.name,\n    result: { ok: true as const },\n  };\n}\n\nexport function handleBlockBridgeError(error: unknown) {\n  return documentBridgeErrorResponse(error);\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.test.ts","s":1,"e":60,"m":1777168988355.2844,"t":"import { describe, expect, it, vi } from \"vitest\";\nimport type { ConvexHttpClient } from \"convex/browser\";\nimport type { BridgeContext, CommandEnvelope } from \"@/lib/documents/bridge\";\nimport { recordBridgeCommandArtifacts } from \"@/lib/documents/bridge-log\";\n\nvi.mock(\"@/lib/convex/api\", () => ({\n  api: {\n    bridgeLogs: {\n      recordCommandLog: \"bridgeLogs.recordCommandLog\",\n      recordDomainEvent: \"bridgeLogs.recordDomainEvent\",\n    },\n  },\n}));\n\nvi.mock(\"@/lib/convex/route\", () => ({\n  getAuthedConvexClient: vi.fn(),\n}));\n\nvi.mock(\"@/lib/documents/bridge\", () => ({\n  DocumentBridgeError: class DocumentBridgeError extends Error {\n    constructor(\n      message: string,\n      public readonly status: number,\n      public readonly code: string,\n      public readonly details?: unknown,\n    ) {\n      super(message);\n      this.name = \"DocumentBridgeError\";\n    }\n  },\n}));\n\nconst context: BridgeContext = {\n  deploymentId: null,\n  projectId: null,\n  workspaceId: \"ws_1\",\n  requestId: \"req_1\",\n  traceId: \"trace_1\",\n  actor: {\n    actorType: \"user\",\n    actorId: \"user_1\",\n    sessionId: \"sess_1\",\n  },\n  source: {\n    channel: \"next-route\",\n    client: \"vitest\",\n  },\n  tenantId: null,\n  authToken: null,\n  idempotencyKey: \"idem_1\",\n  validateOnly: false,\n  dryRun: false,\n};\n\nconst envelope: CommandEnvelope<{ documentId: string }> = {\n  name: \"tree.node.archive\",\n  commandId: \"cmd_1\",\n  idempotencyKey: \"idem_1\",\n  actor: context.actor,\n  source: context.source,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.test.ts","s":49,"e":108,"m":1777168988355.2844,"t":"authToken: null,\n  idempotencyKey: \"idem_1\",\n  validateOnly: false,\n  dryRun: false,\n};\n\nconst envelope: CommandEnvelope<{ documentId: string }> = {\n  name: \"tree.node.archive\",\n  commandId: \"cmd_1\",\n  idempotencyKey: \"idem_1\",\n  actor: context.actor,\n  source: context.source,\n  target: {\n    workspaceId: \"ws_1\",\n    pageId: \"page_1\",\n  },\n  payload: {\n    documentId: \"page_1\",\n  },\n  reason: null,\n  refs: [\"test\"],\n  dryRun: false,\n  validateOnly: false,\n};\n\ndescribe(\"bridge-log\", () => {\n  it(\"记录成功 artifact 时应把 streamDelta 同步写入 domain event payload\", async () => {\n    const mutation = vi.fn().mockResolvedValue({ ok: true });\n\n    await recordBridgeCommandArtifacts({\n      context,\n      envelope,\n      client: { mutation } as unknown as ConvexHttpClient,\n      now: \"2026-04-26T00:00:00.000Z\",\n      domainEventType: \"tree.node.archived\",\n      commandPayload: {\n        documentId: \"page_1\",\n        streamDelta: {\n          op: \"remove_document\",\n          documentId: \"page_1\",\n        },\n      },\n    });\n\n    expect(mutation).toHaveBeenNthCalledWith(\n      2,\n      \"bridgeLogs.recordDomainEvent\",\n      expect.objectContaining({\n        eventType: \"tree.node.archived\",\n        payload: expect.objectContaining({\n          schema: \"mnote.tree.domain_event\",\n          schemaVersion: 1,\n          eventType: \"tree.node.archived\",\n          aggregate: {\n            type: \"page\",\n            id: \"page_1\",\n          },\n          command_id: \"cmd_1\",\n          command_name: \"tree.node.archive\",\n          command: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.test.ts","s":97,"e":156,"m":1777168988355.2844,"t":"eventType: \"tree.node.archived\",\n        payload: expect.objectContaining({\n          schema: \"mnote.tree.domain_event\",\n          schemaVersion: 1,\n          eventType: \"tree.node.archived\",\n          aggregate: {\n            type: \"page\",\n            id: \"page_1\",\n          },\n          command_id: \"cmd_1\",\n          command_name: \"tree.node.archive\",\n          command: {\n            id: \"cmd_1\",\n            name: \"tree.node.archive\",\n            idempotencyKey: \"idem_1\",\n          },\n          trace: {\n            requestId: \"req_1\",\n            traceId: \"trace_1\",\n          },\n          error: null,\n          streamDelta: {\n            op: \"remove_document\",\n            documentId: \"page_1\",\n          },\n        }),\n      }),\n    );\n  });\n\n  it(\"记录 artifact 时应优先消费 Rust domainEventPlan\", async () => {\n    const mutation = vi.fn().mockResolvedValue({ ok: true });\n\n    await recordBridgeCommandArtifacts({\n      context,\n      envelope,\n      client: { mutation } as unknown as ConvexHttpClient,\n      now: \"2026-04-26T00:00:00.000Z\",\n      domainEventPlan: {\n        family: \"tree\",\n        schema: \"mnote.tree.domain_event\",\n        schemaVersion: 1,\n        eventType: \"tree.subtree.moved\",\n        streamDelta: {\n          op: \"move_document\",\n          documentId: \"page_1\",\n          parentId: \"parent_1\",\n          sortOrder: 0,\n        },\n      },\n      domainEventType: \"legacy.should_not_win\",\n      commandPayload: {\n        documentId: \"page_1\",\n      },\n    });\n\n    expect(mutation).toHaveBeenNthCalledWith(\n      2,\n      \"bridgeLogs.recordDomainEvent\",\n      expect.objectContaining({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.test.ts","s":145,"e":173,"m":1777168988355.2844,"t":"},\n      },\n      domainEventType: \"legacy.should_not_win\",\n      commandPayload: {\n        documentId: \"page_1\",\n      },\n    });\n\n    expect(mutation).toHaveBeenNthCalledWith(\n      2,\n      \"bridgeLogs.recordDomainEvent\",\n      expect.objectContaining({\n        eventType: \"tree.subtree.moved\",\n        payload: expect.objectContaining({\n          schema: \"mnote.tree.domain_event\",\n          schemaVersion: 1,\n          eventType: \"tree.subtree.moved\",\n          streamDelta: {\n            op: \"move_document\",\n            documentId: \"page_1\",\n            parentId: \"parent_1\",\n            sortOrder: 0,\n          },\n        }),\n      }),\n    );\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.ts","s":1,"e":60,"m":1777233691556.8743,"t":"import { api } from \"@/lib/convex/api\";\nimport {\n  DocumentBridgeError,\n  type BridgeContext,\n  type BridgeTarget,\n  type CommandEnvelope,\n} from \"@/lib/documents/bridge\";\nimport { getAuthedConvexClient } from \"@/lib/convex/route\";\nimport {\n  recordRustBridgeCommandArtifacts as recordRustBridgeCommandArtifactsFromRuntime,\n  type RustTreeDomainEventPlan,\n} from \"@/lib/documents/rust-runtime\";\nimport type { ConvexHttpClient } from \"convex/browser\";\n\nexport const recordRustBridgeCommandArtifacts = recordRustBridgeCommandArtifactsFromRuntime;\n\nexport type BridgeCommandLogStatus = \"pending\" | \"succeeded\" | \"failed\" | \"rolled_back\";\nexport type BridgeDomainEventStatus = \"pending\" | \"committed\" | \"rejected\" | \"failed\";\n\nfunction buildPayloadSummary(commandName: string, context: BridgeContext): string {\n  return `command=${commandName};request_id=${context.requestId};trace_id=${context.traceId}`;\n}\n\nfunction normalizeWorkspaceId(context: BridgeContext, target?: BridgeTarget | null): string | null {\n  return target?.workspaceId?.trim() || context.workspaceId?.trim() || null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction readStreamDelta(payload: unknown): unknown {\n  if (!isRecord(payload)) {\n    return null;\n  }\n  return payload.streamDelta ?? payload.stream_delta ?? null;\n}\n\nfunction normalizeDomainEventType(raw: unknown): string | null {\n  if (typeof raw !== \"string\") {\n    return null;\n  }\n  const trimmed = raw.trim();\n  return trimmed || null;\n}\n\nfunction buildTreeDomainEventPayload(input: {\n  context: BridgeContext;\n  commandName: string;\n  commandId: string;\n  idempotencyKey?: string | null;\n  eventType: string;\n  aggregateType: string;\n  aggregateId: string;\n  streamDelta: unknown;\n  error?: string | null;\n}) {\n  const payload: Record<string, unknown> = {\n    schema: \"mnote.tree.domain_event\",\n    schemaVersion: 1,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.ts","s":49,"e":108,"m":1777233691556.8743,"t":"commandName: string;\n  commandId: string;\n  idempotencyKey?: string | null;\n  eventType: string;\n  aggregateType: string;\n  aggregateId: string;\n  streamDelta: unknown;\n  error?: string | null;\n}) {\n  const payload: Record<string, unknown> = {\n    schema: \"mnote.tree.domain_event\",\n    schemaVersion: 1,\n    eventType: input.eventType,\n    aggregate: {\n      type: input.aggregateType,\n      id: input.aggregateId,\n    },\n    trace: {\n      requestId: input.context.requestId,\n      traceId: input.context.traceId,\n    },\n    command: {\n      id: input.commandId,\n      name: input.commandName,\n      idempotencyKey: input.idempotencyKey ?? null,\n    },\n    error: input.error ?? null,\n    // 兼容既有观测与 SSE 解析字段，正式消费者应优先使用上面的结构化字段。\n    request_id: input.context.requestId,\n    trace_id: input.context.traceId,\n    command_id: input.commandId,\n    command_name: input.commandName,\n    idempotency_key: input.idempotencyKey ?? null,\n  };\n  if (input.streamDelta) {\n    payload.streamDelta = input.streamDelta;\n  }\n  return payload;\n}\n\nexport async function recordBridgeCommandArtifacts<T>(input: {\n  context: BridgeContext;\n  envelope: CommandEnvelope<T>;\n  client?: ConvexHttpClient;\n  commandPayload?: unknown;\n  domainEventPlan?: RustTreeDomainEventPlan | null;\n  domainEventType?: string | null;\n  status?: BridgeCommandLogStatus;\n  eventStatus?: BridgeDomainEventStatus;\n  error?: string | null;\n  now?: string;\n}): Promise<void> {\n  const workspaceId = normalizeWorkspaceId(input.context, input.envelope.target);\n  if (!workspaceId) return;\n\n  const client = input.client ?? (await getAuthedConvexClient()).client;\n  const commandLogId = `clog_${input.envelope.commandId}`;\n  const eventId = `evt_${input.envelope.commandId}`;\n  const now = input.now ?? new Date().toISOString();\n  const payload = input.commandPayload ?? input.envelope.payload;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.ts","s":97,"e":156,"m":1777233691556.8743,"t":"eventStatus?: BridgeDomainEventStatus;\n  error?: string | null;\n  now?: string;\n}): Promise<void> {\n  const workspaceId = normalizeWorkspaceId(input.context, input.envelope.target);\n  if (!workspaceId) return;\n\n  const client = input.client ?? (await getAuthedConvexClient()).client;\n  const commandLogId = `clog_${input.envelope.commandId}`;\n  const eventId = `evt_${input.envelope.commandId}`;\n  const now = input.now ?? new Date().toISOString();\n  const payload = input.commandPayload ?? input.envelope.payload;\n  const status = input.status ?? \"succeeded\";\n  const eventStatus =\n    input.eventStatus ??\n    (status === \"pending\" ? \"pending\" : status === \"failed\" || status === \"rolled_back\" ? \"failed\" : \"committed\");\n  const aggregateType = input.envelope.target?.blockId ? \"block\" : input.envelope.target?.pageId ? \"page\" : \"workspace\";\n  const aggregateId =\n    input.envelope.target?.blockId ?? input.envelope.target?.pageId ?? workspaceId;\n  const streamDelta = input.domainEventPlan?.streamDelta ?? readStreamDelta(payload);\n  const eventType =\n    normalizeDomainEventType(input.domainEventPlan?.eventType) ??\n    normalizeDomainEventType(input.domainEventType) ?? `${input.envelope.name}.requested`;\n\n  await client.mutation(api.bridgeLogs.recordCommandLog, {\n    workspaceId,\n    id: commandLogId,\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    commandId: input.envelope.commandId,\n    commandName: input.envelope.name,\n    actorId: input.context.actor.actorId,\n    actorType: input.context.actor.actorType,\n    sourceChannel: input.context.source.channel,\n    sourceClient: input.context.source.client,\n    status,\n    targetPageId: input.envelope.target?.pageId ?? null,\n    targetBlockId: input.envelope.target?.blockId ?? null,\n    payload,\n    payloadSummary: buildPayloadSummary(input.envelope.name, input.context),\n    refs: input.envelope.refs,\n    idempotencyKey: input.envelope.idempotencyKey,\n    error: input.error ?? null,\n    createdAt: now,\n    finishedAt: status === \"pending\" ? null : now,\n  });\n\n  await client.mutation(api.bridgeLogs.recordDomainEvent, {\n    workspaceId,\n    id: eventId,\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    commandId: input.envelope.commandId,\n    commandLogId,\n    eventType,\n    aggregateType,\n    aggregateId,\n    eventVersion: 1,\n    status: eventStatus,\n    actorType: input.context.actor.actorType,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.ts","s":145,"e":204,"m":1777233691556.8743,"t":"workspaceId,\n    id: eventId,\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    commandId: input.envelope.commandId,\n    commandLogId,\n    eventType,\n    aggregateType,\n    aggregateId,\n    eventVersion: 1,\n    status: eventStatus,\n    actorType: input.context.actor.actorType,\n    payload: buildTreeDomainEventPayload({\n      context: input.context,\n      commandName: input.envelope.name,\n      commandId: input.envelope.commandId,\n      idempotencyKey: input.envelope.idempotencyKey,\n      eventType,\n      aggregateType,\n      aggregateId,\n      streamDelta,\n      error: input.error ?? null,\n    }),\n    createdAt: now,\n  });\n}\n\nfunction normalizeBridgeErrorMessage(error: unknown) {\n  if (error instanceof Error) {\n    return error.message;\n  }\n  if (typeof error === \"string\" && error.trim()) {\n    return error.trim();\n  }\n  return \"未知 bridge 错误\";\n}\n\nfunction resolveFailureStatuses(error: unknown): {\n  status: BridgeCommandLogStatus;\n  eventStatus: BridgeDomainEventStatus;\n} {\n  const details =\n    error instanceof DocumentBridgeError && error.details && typeof error.details === \"object\"\n      ? (error.details as Record<string, unknown>)\n      : null;\n  const reason = typeof details?.reason === \"string\" ? details.reason.trim() : \"\";\n  if (reason === \"rolled_back\" || reason === \"compensation_applied\") {\n    return {\n      status: \"rolled_back\",\n      eventStatus: \"failed\",\n    };\n  }\n  if (error instanceof DocumentBridgeError && error.code === \"REJECTED\") {\n    return {\n      status: \"failed\",\n      eventStatus: \"rejected\",\n    };\n  }\n  return {\n    status: \"failed\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-log.ts","s":193,"e":229,"m":1777233691556.8743,"t":"status: \"rolled_back\",\n      eventStatus: \"failed\",\n    };\n  }\n  if (error instanceof DocumentBridgeError && error.code === \"REJECTED\") {\n    return {\n      status: \"failed\",\n      eventStatus: \"rejected\",\n    };\n  }\n  return {\n    status: \"failed\",\n    eventStatus: \"failed\",\n  };\n}\n\nexport async function recordBridgeCommandFailureArtifacts<T>(input: {\n  context: BridgeContext;\n  envelope: CommandEnvelope<T>;\n  client?: ConvexHttpClient;\n  error: unknown;\n}): Promise<void> {\n  const { status, eventStatus } = resolveFailureStatuses(input.error);\n  try {\n    await recordBridgeCommandArtifacts({\n      context: input.context,\n      envelope: input.envelope,\n      client: input.client,\n      status,\n      eventStatus,\n      error: normalizeBridgeErrorMessage(input.error),\n    });\n  } catch (loggingError) {\n    console.warn(\"[bridge-log] failure artifacts skipped:\", loggingError);\n  }\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-server.ts","s":1,"e":60,"m":1776316465819.7732,"t":"import { headers } from \"next/headers\";\nimport { ApiError } from \"@/lib/api-utils\";\n\ntype BridgeMeta = {\n  requestId: string;\n  traceId: string;\n  queryName: string;\n};\n\ntype DocumentMetaResponse<T> = {\n  doc: T;\n  meta: BridgeMeta;\n};\n\nfunction getHeaderFirstValue(value: string | null): string {\n  return String(value || \"\")\n    .split(\",\")[0]\n    .trim();\n}\n\nfunction parseHostParts(host: string): { hostname: string; port: string } {\n  try {\n    const parsed = new URL(`http://${host}`);\n    return {\n      hostname: parsed.hostname,\n      port: parsed.port,\n    };\n  } catch {\n    return {\n      hostname: host,\n      port: \"\",\n    };\n  }\n}\n\nfunction isLocalHostname(hostname: string): boolean {\n  const normalized = hostname.replace(/^\\[(.*)\\]$/, \"$1\").trim().toLowerCase();\n  return (\n    normalized === \"localhost\" ||\n    normalized === \"127.0.0.1\" ||\n    normalized === \"::1\" ||\n    normalized === \"0.0.0.0\" ||\n    normalized === \"::\"\n  );\n}\n\nfunction buildServerRequestOriginCandidates(headerList: Headers): string[] {\n  const forwardedProto = getHeaderFirstValue(headerList.get(\"x-forwarded-proto\"));\n  const forwardedHost = getHeaderFirstValue(headerList.get(\"x-forwarded-host\"));\n  const host = forwardedHost || getHeaderFirstValue(headerList.get(\"host\"));\n\n  if (!host) {\n    throw new Error(\"缺少 host 头，无法构造 bridge 请求地址\");\n  }\n\n  const { hostname, port } = parseHostParts(host);\n  const candidates = new Set<string>();\n  const addCandidate = (proto: string, candidateHost: string) => {\n    const normalizedProto = String(proto || \"http\").trim().toLowerCase() || \"http\";\n    const normalizedHost = String(candidateHost || \"\").trim();","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-server.ts","s":49,"e":108,"m":1776316465819.7732,"t":"const forwardedHost = getHeaderFirstValue(headerList.get(\"x-forwarded-host\"));\n  const host = forwardedHost || getHeaderFirstValue(headerList.get(\"host\"));\n\n  if (!host) {\n    throw new Error(\"缺少 host 头，无法构造 bridge 请求地址\");\n  }\n\n  const { hostname, port } = parseHostParts(host);\n  const candidates = new Set<string>();\n  const addCandidate = (proto: string, candidateHost: string) => {\n    const normalizedProto = String(proto || \"http\").trim().toLowerCase() || \"http\";\n    const normalizedHost = String(candidateHost || \"\").trim();\n    if (!normalizedHost) return;\n    candidates.add(`${normalizedProto}://${normalizedHost}`);\n  };\n\n  // 说明：优先保留浏览器实际请求的 origin，用于正常的同源自调用。\n  addCandidate(forwardedProto || \"http\", host);\n\n  // 说明：某些反代会把 x-forwarded-proto 设成 https，但本机 dev server 实际只监听 http。\n  // 这里补一个 http 候选，避免 SSR 自请求被错误协议直接打挂。\n  if ((forwardedProto || \"\").trim().toLowerCase() === \"https\") {\n    addCandidate(\"http\", host);\n  }\n\n  // 说明：localhost / 0.0.0.0 / ::1 这类本机地址在 Node 侧自调用时最容易踩解析差异，\n  // 统一补 127.0.0.1 与 localhost 两个稳定候选，避免 fetch 因回环地址选择失败。\n  if (isLocalHostname(hostname)) {\n    const portSuffix = port ? `:${port}` : \"\";\n    addCandidate(\"http\", `127.0.0.1${portSuffix}`);\n    addCandidate(\"http\", `localhost${portSuffix}`);\n  }\n\n  return [...candidates];\n}\n\nfunction copyHeaderIfPresent(target: Headers, source: Headers, name: string) {\n  const value = source.get(name);\n  if (value) {\n    target.set(name, value);\n  }\n}\n\nasync function fetchDocumentMetaResponse(url: URL, headersToSend: Headers): Promise<Response> {\n  return await fetch(url, {\n    method: \"GET\",\n    headers: headersToSend,\n    cache: \"no-store\",\n  });\n}\n\nexport async function fetchDocumentMetaViaBridge<T>(input: {\n  documentId: string;\n  workspaceId?: string | null;\n}): Promise<DocumentMetaResponse<T> | null> {\n  const headerList = await headers();\n  const requestHeaders = new Headers();\n  const originCandidates = buildServerRequestOriginCandidates(headerList);\n\n  copyHeaderIfPresent(requestHeaders, headerList, \"cookie\");","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-server.ts","s":97,"e":156,"m":1776316465819.7732,"t":"});\n}\n\nexport async function fetchDocumentMetaViaBridge<T>(input: {\n  documentId: string;\n  workspaceId?: string | null;\n}): Promise<DocumentMetaResponse<T> | null> {\n  const headerList = await headers();\n  const requestHeaders = new Headers();\n  const originCandidates = buildServerRequestOriginCandidates(headerList);\n\n  copyHeaderIfPresent(requestHeaders, headerList, \"cookie\");\n  copyHeaderIfPresent(requestHeaders, headerList, \"authorization\");\n  copyHeaderIfPresent(requestHeaders, headerList, \"x-request-id\");\n  copyHeaderIfPresent(requestHeaders, headerList, \"x-trace-id\");\n  copyHeaderIfPresent(requestHeaders, headerList, \"x-session-id\");\n  copyHeaderIfPresent(requestHeaders, headerList, \"x-source-channel\");\n  copyHeaderIfPresent(requestHeaders, headerList, \"x-source-client\");\n  copyHeaderIfPresent(requestHeaders, headerList, \"user-agent\");\n\n  let response: Response | null = null;\n  let lastError: unknown = null;\n  const errorMessages: string[] = [];\n\n  for (const origin of originCandidates) {\n    const url = new URL(\"/api/documents/meta\", origin);\n    url.searchParams.set(\"documentId\", input.documentId);\n    if (input.workspaceId?.trim()) {\n      url.searchParams.set(\"workspaceId\", input.workspaceId.trim());\n    }\n\n    try {\n      response = await fetchDocumentMetaResponse(url, requestHeaders);\n      break;\n    } catch (error) {\n      lastError = error;\n      const message = error instanceof Error ? error.message : String(error);\n      errorMessages.push(`${url.toString()} => ${message}`);\n    }\n  }\n\n  if (!response) {\n    const message =\n      errorMessages.length > 0\n        ? `bridge 自请求失败：${errorMessages.join(\" | \")}`\n        : \"bridge 自请求失败：未生成可用的请求地址\";\n    throw new Error(message, { cause: lastError instanceof Error ? lastError : undefined });\n  }\n\n  if (response.status === 404) {\n    return null;\n  }\n\n  if (!response.ok) {\n    let message = \"加载页面元信息失败\";\n    try {\n      const payload = (await response.json()) as { error?: string };\n      if (typeof payload?.error === \"string\" && payload.error.trim()) {\n        message = payload.error;\n      }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge-server.ts","s":145,"e":165,"m":1776316465819.7732,"t":"if (response.status === 404) {\n    return null;\n  }\n\n  if (!response.ok) {\n    let message = \"加载页面元信息失败\";\n    try {\n      const payload = (await response.json()) as { error?: string };\n      if (typeof payload?.error === \"string\" && payload.error.trim()) {\n        message = payload.error;\n      }\n    } catch {\n      // 说明：这里保留默认错误消息，避免 JSON 解析失败覆盖真实状态码。\n    }\n    throw new ApiError(message, response.status);\n  }\n\n  return (await response.json()) as DocumentMetaResponse<T>;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":1,"e":60,"m":1777197044697.0403,"t":"import { afterEach, describe, expect, it, vi } from \"vitest\";\nimport type { ConvexHttpClient } from \"convex/browser\";\n\nvi.mock(\"@/lib/auth/authContext\", () => ({\n  HttpError: class HttpError extends Error {\n    status: number;\n\n    constructor(status: number, message: string) {\n      super(message);\n      this.status = status;\n    }\n  },\n  requireAuthContext: vi.fn(),\n}));\n\nvi.mock(\"@/lib/api-utils\", () => ({\n  apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({\n    message,\n    status,\n    details,\n  })),\n}));\n\nimport {\n  DocumentBridgeError,\n  assertDocumentId,\n  assertBlockId,\n  assertNextBlock,\n  assertOptionsPatch,\n  assertStats,\n  assertTitle,\n  buildDocumentBridgeMutationRequest,\n  buildDocumentBridgeContextWithActor,\n  buildDocumentCommandEnvelope,\n  buildDocumentQueryEnvelope,\n  type BridgeContext,\n} from \"@/lib/documents/bridge\";\nimport { executeMediaAssetWritebackBridgeCommand } from \"@/lib/documents/media-asset-command-adapter\";\nimport { executeMetadataBridgeCommand } from \"@/lib/documents/metadata-command-adapter\";\nimport { executePageLifecycleBridgeCommand } from \"@/lib/documents/page-command-adapter\";\nimport { executeSaveBridgeCommand } from \"@/lib/documents/save-command-adapter\";\nimport { buildDocumentSavePayload } from \"@/lib/documents/save-contract\";\n\nvi.mock(\"@/lib/convex/route\", () => ({\n  getAuthedConvexClient: vi.fn(),\n}));\n\nvi.mock(\"@/lib/documents/bridge-log\", () => ({\n  recordBridgeCommandArtifacts: vi.fn(),\n  recordBridgeCommandFailureArtifacts: vi.fn(),\n  recordRustBridgeCommandArtifacts: vi.fn(),\n}));\n\nvi.mock(\"@/lib/documents/rust-runtime\", () => ({\n  resolveRustBridgeCommandPlan: vi.fn(),\n  executeRustBridgeMutationTransport: vi.fn(),\n  resolveRustBridgeQueryPlan: vi.fn(),\n  executeRustBridgeQueryTransport: vi.fn(),\n}));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":49,"e":108,"m":1777197044697.0403,"t":"recordBridgeCommandArtifacts: vi.fn(),\n  recordBridgeCommandFailureArtifacts: vi.fn(),\n  recordRustBridgeCommandArtifacts: vi.fn(),\n}));\n\nvi.mock(\"@/lib/documents/rust-runtime\", () => ({\n  resolveRustBridgeCommandPlan: vi.fn(),\n  executeRustBridgeMutationTransport: vi.fn(),\n  resolveRustBridgeQueryPlan: vi.fn(),\n  executeRustBridgeQueryTransport: vi.fn(),\n}));\n\nconst mockContext: BridgeContext = {\n  deploymentId: null,\n  projectId: null,\n  workspaceId: \"ws_1\",\n  requestId: \"req_1\",\n  traceId: \"trace_1\",\n  actor: {\n    actorType: \"user\",\n    actorId: \"user_1\",\n    sessionId: \"sess_1\",\n  },\n  source: {\n    channel: \"next-route\",\n    client: \"vitest\",\n  },\n  tenantId: null,\n  authToken: null,\n  idempotencyKey: \"idem_1\",\n  validateOnly: false,\n  dryRun: false,\n};\n\nafterEach(() => {\n  vi.clearAllMocks();\n});\n\ndescribe(\"documents bridge helpers\", () => {\n  it(\"assertDocumentId returns trimmed id\", () => {\n    expect(assertDocumentId(\"  doc_1  \")).toBe(\"doc_1\");\n  });\n\n  it(\"assertDocumentId throws on empty value\", () => {\n    expect(() => assertDocumentId(\"   \")).toThrow(DocumentBridgeError);\n  });\n\n  it(\"assertTitle normalizes empty title\", () => {\n    expect(assertTitle(\"   \")).toBe(\"无标题\");\n  });\n\n  it(\"assertBlockId returns trimmed block id\", () => {\n    expect(assertBlockId(\"  blk_1  \")).toBe(\"blk_1\");\n  });\n\n  it(\"assertNextBlock accepts plain object\", () => {\n    expect(() => assertNextBlock({ id: \"blk_1\" })).not.toThrow();\n  });\n\n  it(\"assertStats accepts finite numeric stats\", () => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":97,"e":156,"m":1777197044697.0403,"t":"expect(assertTitle(\"   \")).toBe(\"无标题\");\n  });\n\n  it(\"assertBlockId returns trimmed block id\", () => {\n    expect(assertBlockId(\"  blk_1  \")).toBe(\"blk_1\");\n  });\n\n  it(\"assertNextBlock accepts plain object\", () => {\n    expect(() => assertNextBlock({ id: \"blk_1\" })).not.toThrow();\n  });\n\n  it(\"assertStats accepts finite numeric stats\", () => {\n    expect(() =>\n      assertStats({\n        wordCount: 1,\n        characterCount: 2,\n        blockCount: 3,\n        todoTotal: 4,\n        todoDone: 5,\n      }),\n    ).not.toThrow();\n  });\n\n  it(\"assertOptionsPatch accepts stable page options patch\", () => {\n    expect(() =>\n      assertOptionsPatch({\n        showToc: true,\n        layoutDensity: \"compact\",\n        embedDefaultBlockId: null,\n      }),\n    ).not.toThrow();\n  });\n\n  it(\"buildDocumentCommandEnvelope keeps context flags\", () => {\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"documents.save\",\n      payload: { documentId: \"doc_1\" },\n      context: mockContext,\n      target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n    });\n\n    expect(envelope.name).toBe(\"documents.save\");\n    expect(envelope.actor.actorId).toBe(\"user_1\");\n    expect(envelope.idempotencyKey).toBe(\"idem_1\");\n    expect(envelope.target?.pageId).toBe(\"doc_1\");\n  });\n\n  it(\"buildDocumentQueryEnvelope keeps payload\", () => {\n    const envelope = buildDocumentQueryEnvelope({\n      name: \"documents.content.get\",\n      payload: { documentId: \"doc_1\" },\n    });\n\n    expect(envelope.payload).toEqual({ documentId: \"doc_1\" });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds title update runtime request\", () => {\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"documents.title.update\",\n      payload: {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":145,"e":204,"m":1777197044697.0403,"t":"const envelope = buildDocumentQueryEnvelope({\n      name: \"documents.content.get\",\n      payload: { documentId: \"doc_1\" },\n    });\n\n    expect(envelope.payload).toEqual({ documentId: \"doc_1\" });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds title update runtime request\", () => {\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"documents.title.update\",\n      payload: {\n        documentId: \"doc_1\",\n        workspaceId: \"ws_1\",\n        title: \"新标题\",\n      },\n      context: mockContext,\n      target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n    });\n\n    const request = buildDocumentBridgeMutationRequest({\n      context: mockContext,\n      envelope,\n      mapConvexArgs: (payload) => ({\n        id: payload.documentId,\n        title: payload.title,\n      }),\n    });\n\n    expect(request.functionName).toBe(\"documents:updateTitle\");\n    expect(request.workspaceId).toBe(\"ws_1\");\n    expect(request.args).toEqual({\n      id: \"doc_1\",\n      title: \"新标题\",\n    });\n    expect(JSON.parse(request.payloadJson)).toEqual({\n      kind: \"command\",\n      name: \"documents.title.update\",\n      request_id: \"req_1\",\n      trace_id: \"trace_1\",\n      deployment_id: null,\n      project_id: null,\n      workspace_id: \"ws_1\",\n      tenant_id: null,\n      idempotency_key: \"idem_1\",\n      actor: {\n        type: \"user\",\n        id: \"user_1\",\n        session_id: \"sess_1\",\n      },\n      source: {\n        channel: \"next-route\",\n        client: \"vitest\",\n      },\n    });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds documents.save runtime request\", () => {\n    const payload = buildDocumentSavePayload({\n      documentId: \"doc_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":193,"e":252,"m":1777197044697.0403,"t":"session_id: \"sess_1\",\n      },\n      source: {\n        channel: \"next-route\",\n        client: \"vitest\",\n      },\n    });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds documents.save runtime request\", () => {\n    const payload = buildDocumentSavePayload({\n      documentId: \"doc_1\",\n      workspaceId: \"ws_1\",\n      revision: 7,\n      content: [{ id: \"block_1\" }],\n      conflictDetectionKey: \"conflict_1\",\n      snapshotCapturedAt: \"2026-04-14T14:30:00.000Z\",\n      blockCount: 1,\n    });\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"documents.save\",\n      payload,\n      context: mockContext,\n      target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n    });\n\n    const request = buildDocumentBridgeMutationRequest({\n      context: mockContext,\n      envelope,\n      mapConvexArgs: (nextPayload) => ({\n        id: nextPayload.documentId,\n        content: nextPayload.content,\n        expectedRevision: nextPayload.revision,\n        conflictDetectionKey: nextPayload.conflictDetectionKey,\n      }),\n    });\n\n    expect(request.functionName).toBe(\"documents:updateContent\");\n    expect(request.workspaceId).toBe(\"ws_1\");\n    expect(request.args).toEqual({\n      id: \"doc_1\",\n      content: [{ id: \"block_1\" }],\n      expectedRevision: 7,\n      conflictDetectionKey: \"conflict_1\",\n    });\n    expect(JSON.parse(request.payloadJson)).toMatchObject({\n      kind: \"command\",\n      name: \"documents.save\",\n      workspace_id: \"ws_1\",\n      request_id: \"req_1\",\n      trace_id: \"trace_1\",\n    });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds documents.embed runtime request\", () => {\n    const payload = {\n      ...buildDocumentSavePayload({\n        documentId: \"doc_2\",\n        workspaceId: \"ws_1\",\n        revision: 3,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":241,"e":300,"m":1777197044697.0403,"t":"workspace_id: \"ws_1\",\n      request_id: \"req_1\",\n      trace_id: \"trace_1\",\n    });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds documents.embed runtime request\", () => {\n    const payload = {\n      ...buildDocumentSavePayload({\n        documentId: \"doc_2\",\n        workspaceId: \"ws_1\",\n        revision: 3,\n        content: [{ id: \"block_2\", type: \"pageReference\" }],\n        conflictDetectionKey: \"conflict_3\",\n        blockCount: 1,\n      }),\n      sourceDocumentId: \"doc_1\",\n      targetDocumentId: \"doc_2\",\n      anchorBlockId: \"anchor_1\",\n    };\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"documents.embed\",\n      payload,\n      context: mockContext,\n      target: { workspaceId: \"ws_1\", pageId: \"doc_2\" },\n    });\n\n    const request = buildDocumentBridgeMutationRequest({\n      context: mockContext,\n      envelope,\n      mapConvexArgs: (nextPayload) => ({\n        id: nextPayload.documentId,\n        content: nextPayload.content,\n        expectedRevision: nextPayload.revision,\n        conflictDetectionKey: nextPayload.conflictDetectionKey,\n        sourceDocumentId: nextPayload.sourceDocumentId,\n        targetDocumentId: nextPayload.targetDocumentId,\n        anchorBlockId: nextPayload.anchorBlockId,\n      }),\n    });\n\n    expect(request.functionName).toBe(\"documents:updateContent\");\n    expect(request.workspaceId).toBe(\"ws_1\");\n    expect(request.args).toEqual({\n      id: \"doc_2\",\n      content: [{ id: \"block_2\", type: \"pageReference\" }],\n      expectedRevision: 3,\n      conflictDetectionKey: \"conflict_3\",\n      sourceDocumentId: \"doc_1\",\n      targetDocumentId: \"doc_2\",\n      anchorBlockId: \"anchor_1\",\n    });\n    expect(JSON.parse(request.payloadJson)).toMatchObject({\n      kind: \"command\",\n      name: \"documents.embed\",\n      workspace_id: \"ws_1\",\n      request_id: \"req_1\",\n      trace_id: \"trace_1\",\n    });\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":289,"e":348,"m":1777197044697.0403,"t":"sourceDocumentId: \"doc_1\",\n      targetDocumentId: \"doc_2\",\n      anchorBlockId: \"anchor_1\",\n    });\n    expect(JSON.parse(request.payloadJson)).toMatchObject({\n      kind: \"command\",\n      name: \"documents.embed\",\n      workspace_id: \"ws_1\",\n      request_id: \"req_1\",\n      trace_id: \"trace_1\",\n    });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds media asset writeback runtime request\", () => {\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"media.assets.replace_storage\",\n      payload: {\n        assetId: \"asset_1\",\n        documentId: \"doc_1\",\n        workspaceId: \"ws_1\",\n        storageId: \"storage_1\",\n        userId: \"user_1\",\n      },\n      context: mockContext,\n      target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n    });\n\n    const request = buildDocumentBridgeMutationRequest({\n      context: mockContext,\n      envelope,\n      mapConvexArgs: (payload) => ({\n        userId: payload.userId,\n        id: payload.assetId,\n        storageId: payload.storageId,\n      }),\n    });\n\n    expect(request.functionName).toBe(\"mediaAssets:replaceStorageFromUpload\");\n    expect(request.workspaceId).toBe(\"ws_1\");\n    expect(request.args).toEqual({\n      userId: \"user_1\",\n      id: \"asset_1\",\n      storageId: \"storage_1\",\n    });\n    expect(JSON.parse(request.payloadJson)).toMatchObject({\n      kind: \"command\",\n      name: \"media.assets.replace_storage\",\n      workspace_id: \"ws_1\",\n      request_id: \"req_1\",\n      trace_id: \"trace_1\",\n    });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds page lifecycle runtime request\", () => {\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"documents.create\",\n      payload: {\n        documentId: \"doc_1\",\n        workspaceId: \"ws_1\",\n        parentId: \"parent_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":337,"e":396,"m":1777197044697.0403,"t":"request_id: \"req_1\",\n      trace_id: \"trace_1\",\n    });\n  });\n\n  it(\"buildDocumentBridgeMutationRequest builds page lifecycle runtime request\", () => {\n    const envelope = buildDocumentCommandEnvelope({\n      name: \"documents.create\",\n      payload: {\n        documentId: \"doc_1\",\n        workspaceId: \"ws_1\",\n        parentId: \"parent_1\",\n        title: \"无标题\",\n        accessScope: \"private\",\n        content: [],\n      },\n      context: mockContext,\n      target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n    });\n\n    const request = buildDocumentBridgeMutationRequest({\n      context: mockContext,\n      envelope,\n      mapConvexArgs: (payload) => ({\n        id: payload.documentId,\n        workspaceId: payload.workspaceId,\n        parentId: payload.parentId,\n        title: payload.title,\n        accessScope: payload.accessScope,\n        content: payload.content,\n      }),\n    });\n\n    expect(request.functionName).toBe(\"documents:createWithParentReference\");\n    expect(request.args).toEqual({\n      id: \"doc_1\",\n      workspaceId: \"ws_1\",\n      parentId: \"parent_1\",\n      title: \"无标题\",\n      accessScope: \"private\",\n      content: [],\n    });\n  });\n\n  it(\"buildDocumentBridgeContextWithActor keeps explicit actor and source\", () => {\n    const request = new Request(\"http://127.0.0.1:3001/api/onlyoffice/callback\", {\n      headers: {\n        \"x-request-id\": \"req_oo_1\",\n        \"x-trace-id\": \"trace_oo_1\",\n      },\n    });\n\n    const context = buildDocumentBridgeContextWithActor({\n      request,\n      workspaceId: \"ws_1\",\n      actor: {\n        actorType: \"service\",\n        actorId: \"user_1\",\n        sessionId: \"onlyoffice-callback\",\n      },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":385,"e":444,"m":1777197044697.0403,"t":"\"x-trace-id\": \"trace_oo_1\",\n      },\n    });\n\n    const context = buildDocumentBridgeContextWithActor({\n      request,\n      workspaceId: \"ws_1\",\n      actor: {\n        actorType: \"service\",\n        actorId: \"user_1\",\n        sessionId: \"onlyoffice-callback\",\n      },\n      source: {\n        channel: \"onlyoffice-callback\",\n        client: \"onlyoffice-document-server\",\n      },\n      idempotencyKey: \"asset_1\",\n    });\n\n    expect(context.requestId).toBe(\"req_oo_1\");\n    expect(context.traceId).toBe(\"trace_oo_1\");\n    expect(context.actor).toEqual({\n      actorType: \"service\",\n      actorId: \"user_1\",\n      sessionId: \"onlyoffice-callback\",\n    });\n    expect(context.source).toEqual({\n      channel: \"onlyoffice-callback\",\n      client: \"onlyoffice-document-server\",\n    });\n    expect(context.idempotencyKey).toBe(\"asset_1\");\n  });\n\n  it(\"executeMetadataBridgeCommand routes title update through adapter\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.title.update\",\n      commandId: \"cmd_title_1\",\n      functionName: \"documents:updateTitle\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        title: \"新标题\",\n      },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":433,"e":492,"m":1777197044697.0403,"t":"commandId: \"cmd_title_1\",\n      functionName: \"documents:updateTitle\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        title: \"新标题\",\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({ ok: true });\n\n    const result = await executeMetadataBridgeCommand({\n      context: mockContext,\n      envelope: buildDocumentCommandEnvelope({\n        name: \"documents.title.update\",\n        payload: {\n          documentId: \"doc_1\",\n          workspaceId: \"ws_1\",\n          title: \"新标题\",\n        },\n        context: mockContext,\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n      }),\n    });\n\n    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.title.update\",\n      }),\n    });\n    expect(executeRustBridgeMutationTransport).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      plan: expect.objectContaining({\n        functionName: \"documents:updateTitle\",\n        argsJson: {\n          id: \"doc_1\",\n          title: \"新标题\",\n        },\n      }),\n    });\n    expect(result.commandName).toBe(\"documents.title.update\");\n  });\n\n  it(\"executeMetadataBridgeCommand routes options update through adapter\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const { recordRustBridgeCommandArtifacts } = await import(\"@/lib/documents/bridge-log\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":481,"e":540,"m":1777197044697.0403,"t":"it(\"executeMetadataBridgeCommand routes options update through adapter\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const { recordRustBridgeCommandArtifacts } = await import(\"@/lib/documents/bridge-log\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.options.update\",\n      commandId: \"cmd_options_1\",\n      functionName: \"documents:updateOptions\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        workspaceId: \"ws_1\",\n        options: {\n          showToc: true,\n          layoutDensity: \"compact\",\n          embedDefaultBlockId: null,\n        },\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      ok: true,\n      updated_at: \"2026-04-26T00:00:00.000Z\",\n    });\n\n    const result = await executeMetadataBridgeCommand({\n      context: mockContext,\n      envelope: buildDocumentCommandEnvelope({\n        name: \"documents.options.update\",\n        payload: {\n          documentId: \"doc_1\",\n          workspaceId: \"ws_1\",\n          options: {\n            showToc: true,\n            layoutDensity: \"compact\",\n            embedDefaultBlockId: null,\n          },\n        },\n        context: mockContext,\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n      }),\n    });\n\n    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({\n      context: mockContext,\n      envelope: expect.objectContaining({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":529,"e":588,"m":1777197044697.0403,"t":"layoutDensity: \"compact\",\n            embedDefaultBlockId: null,\n          },\n        },\n        context: mockContext,\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n      }),\n    });\n\n    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.options.update\",\n      }),\n    });\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.options.update\",\n      }),\n      plan: expect.objectContaining({\n        commandName: \"documents.options.update\",\n        functionName: \"documents:updateOptions\",\n      }),\n      result: {\n        ok: true,\n        updated_at: \"2026-04-26T00:00:00.000Z\",\n      },\n    });\n    expect(result.commandName).toBe(\"documents.options.update\");\n  });\n\n  it(\"executeMetadataBridgeCommand routes stats update through rust runtime\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const { recordRustBridgeCommandArtifacts } = await import(\"@/lib/documents/bridge-log\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        mutation: vi.fn().mockResolvedValue({\n          ok: true,\n          updated_at: \"2026-04-26T00:00:00.000Z\",\n        }),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.stats.update\",\n      commandId: \"cmd_stats_1\",\n      functionName: \"documents:updateStats\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":577,"e":636,"m":1777197044697.0403,"t":"});\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.stats.update\",\n      commandId: \"cmd_stats_1\",\n      functionName: \"documents:updateStats\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        workspaceId: \"ws_1\",\n        wordCount: 12,\n        characterCount: 34,\n        blockCount: 5,\n        todoTotal: 6,\n        todoDone: 2,\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      ok: true,\n      updated_at: \"2026-04-26T00:00:00.000Z\",\n    });\n\n    const result = await executeMetadataBridgeCommand({\n      context: mockContext,\n      envelope: buildDocumentCommandEnvelope({\n        name: \"documents.stats.update\",\n        payload: {\n          documentId: \"doc_1\",\n          workspaceId: \"ws_1\",\n          stats: {\n            wordCount: 12,\n            characterCount: 34,\n            blockCount: 5,\n            todoTotal: 6,\n            todoDone: 2,\n          },\n        },\n        context: mockContext,\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n      }),\n    });\n\n    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.stats.update\",\n      }),\n    });\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.stats.update\",\n      }),\n      plan: expect.objectContaining({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":625,"e":684,"m":1777197044697.0403,"t":"context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.stats.update\",\n      }),\n    });\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.stats.update\",\n      }),\n      plan: expect.objectContaining({\n        commandName: \"documents.stats.update\",\n        functionName: \"documents:updateStats\",\n      }),\n      result: {\n        ok: true,\n        updated_at: \"2026-04-26T00:00:00.000Z\",\n      },\n    });\n    expect(result.commandName).toBe(\"documents.stats.update\");\n  });\n\n  it(\"executeSaveBridgeCommand routes documents.save through adapter\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const { recordRustBridgeCommandArtifacts } = await import(\"@/lib/documents/bridge-log\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(recordRustBridgeCommandArtifacts).mockResolvedValue(undefined);\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.save\",\n      commandId: \"cmd_save_1\",\n      functionName: \"documents:updateContent\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        content: [{ id: \"block_1\" }],\n        expectedRevision: 7,\n        conflictDetectionKey: \"conflict_1\",\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      revision: 7,\n      conflict_detection_key: \"conflict_1\",\n    });\n    const previousBridgeArtifactCalls = vi.mocked(recordRustBridgeCommandArtifacts).mock.calls.length;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":673,"e":732,"m":1777197044697.0403,"t":"argsJson: {\n        id: \"doc_1\",\n        content: [{ id: \"block_1\" }],\n        expectedRevision: 7,\n        conflictDetectionKey: \"conflict_1\",\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      revision: 7,\n      conflict_detection_key: \"conflict_1\",\n    });\n    const previousBridgeArtifactCalls = vi.mocked(recordRustBridgeCommandArtifacts).mock.calls.length;\n\n    const payload = buildDocumentSavePayload({\n      documentId: \"doc_1\",\n      workspaceId: \"ws_1\",\n      revision: 7,\n      content: [{ id: \"block_1\" }],\n      conflictDetectionKey: \"conflict_1\",\n      snapshotCapturedAt: \"2026-04-14T14:30:00.000Z\",\n      blockCount: 1,\n    });\n\n    const result = await executeSaveBridgeCommand({\n      context: mockContext,\n      envelope: buildDocumentCommandEnvelope({\n        name: \"documents.save\",\n        payload,\n        context: mockContext,\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n      }),\n    });\n\n    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.save\",\n      }),\n    });\n    expect(executeRustBridgeMutationTransport).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      plan: expect.objectContaining({\n        functionName: \"documents:updateContent\",\n        argsJson: {\n          id: \"doc_1\",\n          content: [{ id: \"block_1\" }],\n          expectedRevision: 7,\n          conflictDetectionKey: \"conflict_1\",\n        },\n      }),\n    });\n    expect(vi.mocked(recordRustBridgeCommandArtifacts).mock.calls.length).toBe(\n      previousBridgeArtifactCalls + 1,\n    );\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.save\",\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":721,"e":780,"m":1777197044697.0403,"t":"},\n      }),\n    });\n    expect(vi.mocked(recordRustBridgeCommandArtifacts).mock.calls.length).toBe(\n      previousBridgeArtifactCalls + 1,\n    );\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.save\",\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n        payload,\n      }),\n      plan: expect.objectContaining({\n        commandName: \"documents.save\",\n        functionName: \"documents:updateContent\",\n      }),\n      result: {\n        revision: 7,\n        conflict_detection_key: \"conflict_1\",\n      },\n    });\n    expect(result.requestId).toBe(\"req_1\");\n    expect(result.traceId).toBe(\"trace_1\");\n    expect(result.commandName).toBe(\"documents.save\");\n  });\n\n  it(\"executeSaveBridgeCommand 将冲突错误归一为 bridge rejected\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.save\",\n      commandId: \"cmd_save_1\",\n      functionName: \"documents:updateContent\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        content: [{ id: \"block_1\" }],\n        expectedRevision: 7,\n        conflictDetectionKey: \"conflict_1\",\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockRejectedValue(\n      new Error(\"正文内容已变更，请刷新后重试\"),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":769,"e":828,"m":1777197044697.0403,"t":"actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        content: [{ id: \"block_1\" }],\n        expectedRevision: 7,\n        conflictDetectionKey: \"conflict_1\",\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockRejectedValue(\n      new Error(\"正文内容已变更，请刷新后重试\"),\n    );\n\n    const payload = buildDocumentSavePayload({\n      documentId: \"doc_1\",\n      workspaceId: \"ws_1\",\n      revision: 7,\n      content: [{ id: \"block_1\" }],\n      conflictDetectionKey: \"conflict_1\",\n    });\n\n    await expect(\n      executeSaveBridgeCommand({\n        context: mockContext,\n        envelope: buildDocumentCommandEnvelope({\n          name: \"documents.save\",\n          payload,\n          context: mockContext,\n          target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n        }),\n      }),\n    ).rejects.toMatchObject({\n      name: \"DocumentBridgeError\",\n      status: 409,\n      code: \"REJECTED\",\n    });\n  });\n\n  it(\"executePageLifecycleBridgeCommand routes page mutation through rust runtime\", async () => {\n    const { getAuthedConvexClient } = await import(\"@/lib/convex/route\");\n    const { recordRustBridgeCommandArtifacts } = await import(\"@/lib/documents/bridge-log\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(getAuthedConvexClient).mockResolvedValue({\n      auth: { userId: \"user_1\" },\n      client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(recordRustBridgeCommandArtifacts).mockResolvedValue(undefined);\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.create\",\n      commandId: \"cmd_create_1\",\n      functionName: \"documents:createWithParentReference\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":817,"e":876,"m":1777197044697.0403,"t":"client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n    });\n    vi.mocked(recordRustBridgeCommandArtifacts).mockResolvedValue(undefined);\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"documents.create\",\n      commandId: \"cmd_create_1\",\n      functionName: \"documents:createWithParentReference\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        id: \"doc_1\",\n        workspaceId: \"ws_1\",\n        parentId: \"parent_1\",\n        title: \"无标题\",\n        accessScope: \"private\",\n        content: [],\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      id: \"doc_1\",\n      title: \"无标题\",\n    });\n\n    const result = await executePageLifecycleBridgeCommand({\n      context: mockContext,\n      envelope: buildDocumentCommandEnvelope({\n        name: \"documents.create\",\n        payload: {\n          documentId: \"doc_1\",\n          workspaceId: \"ws_1\",\n          parentId: \"parent_1\",\n          title: \"无标题\",\n          accessScope: \"private\",\n          content: [],\n        },\n        context: mockContext,\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n      }),\n    });\n\n    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.create\",\n      }),\n    });\n    expect(executeRustBridgeMutationTransport).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      plan: expect.objectContaining({\n        functionName: \"documents:createWithParentReference\",\n      }),\n    });\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":865,"e":924,"m":1777197044697.0403,"t":"context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.create\",\n      }),\n    });\n    expect(executeRustBridgeMutationTransport).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      plan: expect.objectContaining({\n        functionName: \"documents:createWithParentReference\",\n      }),\n    });\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"documents.create\",\n      }),\n      plan: expect.objectContaining({\n        commandName: \"documents.create\",\n        functionName: \"documents:createWithParentReference\",\n      }),\n      result: {\n        id: \"doc_1\",\n        title: \"无标题\",\n      },\n    });\n    expect(result.result).toEqual({\n      id: \"doc_1\",\n      title: \"无标题\",\n    });\n  });\n\n  it(\"executeMediaAssetWritebackBridgeCommand routes callback writeback through adapter\", async () => {\n    const { recordRustBridgeCommandArtifacts } = await import(\"@/lib/documents/bridge-log\");\n    const {\n      resolveRustBridgeCommandPlan,\n      executeRustBridgeMutationTransport,\n    } = await import(\"@/lib/documents/rust-runtime\");\n    vi.mocked(recordRustBridgeCommandArtifacts).mockResolvedValue(undefined);\n    vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({\n      kind: \"command\",\n      commandName: \"media.assets.replace_storage\",\n      commandId: \"cmd_asset_1\",\n      functionName: \"mediaAssets:replaceStorageFromUpload\",\n      workspaceId: \"ws_1\",\n      requestId: \"req_1\",\n      traceId: \"trace_1\",\n      actorId: \"user_1\",\n      idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        userId: \"user_1\",\n        id: \"asset_1\",\n        documentId: \"doc_1\",\n        workspaceId: \"ws_1\",\n        storageId: \"storage_1\",\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      ok: true,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.test.ts","s":913,"e":970,"m":1777197044697.0403,"t":"idempotencyKey: \"idem_1\",\n      payloadJson: \"{\\\"kind\\\":\\\"command\\\"}\",\n      argsJson: {\n        userId: \"user_1\",\n        id: \"asset_1\",\n        documentId: \"doc_1\",\n        workspaceId: \"ws_1\",\n        storageId: \"storage_1\",\n      },\n    });\n    vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({\n      ok: true,\n      fileUrl: \"https://example.com/file.docx\",\n    });\n\n    await executeMediaAssetWritebackBridgeCommand({\n      client: {\n        mutation: vi.fn(),\n      } as unknown as ConvexHttpClient,\n      context: mockContext,\n      envelope: buildDocumentCommandEnvelope({\n        name: \"media.assets.replace_storage\",\n        payload: {\n          assetId: \"asset_1\",\n          documentId: \"doc_1\",\n          workspaceId: \"ws_1\",\n          storageId: \"storage_1\",\n          userId: \"user_1\",\n        },\n        context: mockContext,\n        target: { workspaceId: \"ws_1\", pageId: \"doc_1\" },\n      }),\n    });\n\n    expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"media.assets.replace_storage\",\n      }),\n    });\n    expect(recordRustBridgeCommandArtifacts).toHaveBeenCalledWith({\n      client: expect.any(Object),\n      context: mockContext,\n      envelope: expect.objectContaining({\n        name: \"media.assets.replace_storage\",\n      }),\n      plan: expect.objectContaining({\n        commandName: \"media.assets.replace_storage\",\n        functionName: \"mediaAssets:replaceStorageFromUpload\",\n      }),\n      result: {\n        ok: true,\n        fileUrl: \"https://example.com/file.docx\",\n      },\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":1,"e":60,"m":1777105111244.5793,"t":"import { randomUUID } from \"crypto\";\nimport type { ConvexHttpClient } from \"convex/browser\";\nimport { HttpError, requireAuthContext } from \"@/lib/auth/authContext\";\nimport { apiErrorResponse } from \"@/lib/api-utils\";\nimport type { PageOptionsState } from \"@/types/page-options\";\n\nexport type BridgeActor = {\n  actorType: string;\n  actorId: string;\n  sessionId: string | null;\n};\n\nexport type BridgeSource = {\n  channel: string;\n  client: string;\n};\n\nexport type BridgeTarget = {\n  workspaceId?: string | null;\n  pageId?: string | null;\n  blockId?: string | null;\n};\n\nexport type BridgeRequestMeta = {\n  idempotencyKey: string | null;\n  validateOnly: boolean;\n  dryRun: boolean;\n};\n\nexport type BridgeContext = {\n  deploymentId: string | null;\n  projectId: string | null;\n  workspaceId: string | null;\n  requestId: string;\n  traceId: string;\n  actor: BridgeActor;\n  source: BridgeSource;\n  tenantId: string | null;\n  authToken: string | null;\n  idempotencyKey: string | null;\n  validateOnly: boolean;\n  dryRun: boolean;\n};\n\nexport type BridgeErrorCode =\n  | \"VALIDATION_ERROR\"\n  | \"UNAUTHORIZED\"\n  | \"FORBIDDEN\"\n  | \"NOT_FOUND\"\n  | \"TRANSPORT_ERROR\"\n  | \"REJECTED\";\n\nexport class DocumentBridgeError extends Error {\n  constructor(\n    message: string,\n    public readonly status: number,\n    public readonly code: BridgeErrorCode,\n    public readonly details?: unknown,\n  ) {\n    super(message);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":49,"e":108,"m":1777105111244.5793,"t":"| \"NOT_FOUND\"\n  | \"TRANSPORT_ERROR\"\n  | \"REJECTED\";\n\nexport class DocumentBridgeError extends Error {\n  constructor(\n    message: string,\n    public readonly status: number,\n    public readonly code: BridgeErrorCode,\n    public readonly details?: unknown,\n  ) {\n    super(message);\n    this.name = \"DocumentBridgeError\";\n  }\n}\n\nexport type CommandEnvelope<T> = {\n  name: string;\n  commandId: string;\n  idempotencyKey: string | null;\n  actor: BridgeActor;\n  source: BridgeSource;\n  target: BridgeTarget | null;\n  payload: T;\n  preflightData?: Record<string, unknown> | null;\n  reason: string | null;\n  refs: string[];\n  dryRun: boolean;\n  validateOnly: boolean;\n};\n\nexport type QueryEnvelope<T> = {\n  name: string;\n  payload: T;\n};\n\nexport type DocumentBridgeMutationRequest<\n  TArgs extends Record<string, unknown> = Record<string, unknown>,\n> = {\n  functionName: string;\n  deploymentId: string | null;\n  projectId: string | null;\n  workspaceId: string | null;\n  requestId: string;\n  traceId: string;\n  idempotencyKey: string | null;\n  actorId: string;\n  payloadJson: string;\n  args: TArgs;\n};\n\nexport type DocumentBridgeQueryRequest<\n  TArgs extends Record<string, unknown> = Record<string, unknown>,\n> = {\n  functionName: string;\n  deploymentId: string | null;\n  projectId: string | null;\n  workspaceId: string | null;\n  requestId: string;\n  traceId: string;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":97,"e":156,"m":1777105111244.5793,"t":"args: TArgs;\n};\n\nexport type DocumentBridgeQueryRequest<\n  TArgs extends Record<string, unknown> = Record<string, unknown>,\n> = {\n  functionName: string;\n  deploymentId: string | null;\n  projectId: string | null;\n  workspaceId: string | null;\n  requestId: string;\n  traceId: string;\n  actorId: string;\n  payloadJson: string;\n  args: TArgs;\n};\n\nconst DOCUMENT_BRIDGE_QUERY_FUNCTIONS = {\n  \"documents.content.get\": \"documents:getContent\",\n  \"documents.meta.get\": \"documents:getMeta\",\n  \"blocks.get\": \"documents:getContent\",\n} as const satisfies Record<string, string>;\n\nconst DOCUMENT_BRIDGE_MUTATION_FUNCTIONS = {\n  \"documents.create\": \"documents:createWithParentReference\",\n  \"documents.move\": \"documents:move\",\n  \"documents.delete\": \"documents:softDelete\",\n  \"documents.restore\": \"documents:restore\",\n  \"documents.duplicate\": \"documents:duplicateWithMindmaps\",\n  \"documents.copy_tree\": \"documents:copyTree\",\n  \"documents.template\": \"documents:setTemplate\",\n  \"documents.emptyTrashByWorkspace\": \"documents:emptyTrashByWorkspace\",\n  \"documents.purge\": \"documents:purge\",\n  \"documents.embed\": \"documents:updateContent\",\n  \"blocks.patch\": \"documents:updateContent\",\n  \"blocks.move\": \"documents:updateContent\",\n  \"blocks.embed\": \"documents:updateContent\",\n  \"documents.title.update\": \"documents:updateTitle\",\n  \"documents.stats.update\": \"documents:updateStats\",\n  \"documents.options.update\": \"documents:updateOptions\",\n  \"documents.save\": \"documents:updateContent\",\n  \"page.head.updateTitle\": \"documents:updateTitle\",\n  \"page.layout.updateOptions\": \"documents:updateOptions\",\n  \"page.body.save\": \"documents:updateContent\",\n  \"mindmaps.delete\": \"mindmaps:softDelete\",\n  \"mindmaps.restore\": \"mindmaps:restore\",\n  \"mindmaps.purge\": \"mindmaps:purge\",\n  \"mindmaps.emptyTrashByWorkspace\": \"mindmaps:emptyTrashByWorkspace\",\n  \"media.assets.replace_storage\": \"mediaAssets:replaceStorageFromUpload\",\n} as const satisfies Record<string, string>;\n\nfunction readHeaderValue(headerList: Headers, ...candidates: string[]): string | null {\n  for (const candidate of candidates) {\n    const value = headerList.get(candidate);\n    if (typeof value === \"string\" && value.trim()) {\n      return value.trim();\n    }\n  }\n  return null;\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":145,"e":204,"m":1777105111244.5793,"t":"\"media.assets.replace_storage\": \"mediaAssets:replaceStorageFromUpload\",\n} as const satisfies Record<string, string>;\n\nfunction readHeaderValue(headerList: Headers, ...candidates: string[]): string | null {\n  for (const candidate of candidates) {\n    const value = headerList.get(candidate);\n    if (typeof value === \"string\" && value.trim()) {\n      return value.trim();\n    }\n  }\n  return null;\n}\n\nfunction toBooleanFlag(raw: string | null): boolean {\n  if (!raw) return false;\n  return raw === \"1\" || raw.toLowerCase() === \"true\";\n}\n\nfunction makeFallbackId(prefix: string): string {\n  return `${prefix}_${randomUUID()}`;\n}\n\nexport async function buildDocumentBridgeContext(input: {\n  request: Request;\n  workspaceId?: string | null;\n  idempotencyKey?: string | null;\n  validateOnly?: boolean;\n  dryRun?: boolean;\n}): Promise<BridgeContext> {\n  let auth;\n  try {\n    auth = await requireAuthContext();\n  } catch (error) {\n    if (error instanceof HttpError) {\n      throw new DocumentBridgeError(error.message || \"未登录\", error.status, \"UNAUTHORIZED\");\n    }\n    throw new DocumentBridgeError(\"未登录\", 401, \"UNAUTHORIZED\");\n  }\n\n  const headerList = input.request.headers;\n  const requestId =\n    readHeaderValue(headerList, \"x-request-id\", \"x-mnote-request-id\") ?? makeFallbackId(\"req\");\n  const traceId =\n    readHeaderValue(headerList, \"x-trace-id\", \"x-mnote-trace-id\", \"x-request-id\") ?? makeFallbackId(\"trace\");\n  const sessionId = readHeaderValue(headerList, \"x-session-id\", \"x-mnote-session-id\");\n  const sourceChannel = readHeaderValue(headerList, \"x-source-channel\") ?? \"next-route\";\n  const sourceClient = readHeaderValue(headerList, \"x-source-client\", \"user-agent\") ?? \"wolai-frontend\";\n  const deploymentId =\n    readHeaderValue(headerList, \"x-deployment-id\") ?? process.env.VERCEL_DEPLOYMENT_ID ?? null;\n  const projectId = readHeaderValue(headerList, \"x-project-id\") ?? process.env.VERCEL_PROJECT_ID ?? null;\n  const tenantId = readHeaderValue(headerList, \"x-tenant-id\");\n  const authToken = readHeaderValue(headerList, \"authorization\");\n  const idempotencyKey =\n    input.idempotencyKey ?? readHeaderValue(headerList, \"idempotency-key\", \"x-idempotency-key\");\n  const validateOnly = input.validateOnly ?? toBooleanFlag(readHeaderValue(headerList, \"x-validate-only\"));\n  const dryRun = input.dryRun ?? toBooleanFlag(readHeaderValue(headerList, \"x-dry-run\"));\n\n  return {\n    deploymentId,\n    projectId,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":193,"e":252,"m":1777105111244.5793,"t":"readHeaderValue(headerList, \"x-deployment-id\") ?? process.env.VERCEL_DEPLOYMENT_ID ?? null;\n  const projectId = readHeaderValue(headerList, \"x-project-id\") ?? process.env.VERCEL_PROJECT_ID ?? null;\n  const tenantId = readHeaderValue(headerList, \"x-tenant-id\");\n  const authToken = readHeaderValue(headerList, \"authorization\");\n  const idempotencyKey =\n    input.idempotencyKey ?? readHeaderValue(headerList, \"idempotency-key\", \"x-idempotency-key\");\n  const validateOnly = input.validateOnly ?? toBooleanFlag(readHeaderValue(headerList, \"x-validate-only\"));\n  const dryRun = input.dryRun ?? toBooleanFlag(readHeaderValue(headerList, \"x-dry-run\"));\n\n  return {\n    deploymentId,\n    projectId,\n    workspaceId: input.workspaceId ?? null,\n    requestId,\n    traceId,\n    actor: {\n      actorType: \"user\",\n      actorId: auth.userId,\n      sessionId,\n    },\n    source: {\n      channel: sourceChannel,\n      client: sourceClient,\n    },\n    tenantId,\n    authToken,\n    idempotencyKey,\n    validateOnly,\n    dryRun,\n  };\n}\n\nexport function buildDocumentBridgeContextWithActor(input: {\n  request: Request;\n  actor: BridgeActor;\n  workspaceId?: string | null;\n  idempotencyKey?: string | null;\n  validateOnly?: boolean;\n  dryRun?: boolean;\n  source?: Partial<BridgeSource>;\n  authToken?: string | null;\n}): BridgeContext {\n  const headerList = input.request.headers;\n  const requestId =\n    readHeaderValue(headerList, \"x-request-id\", \"x-mnote-request-id\") ?? makeFallbackId(\"req\");\n  const traceId =\n    readHeaderValue(headerList, \"x-trace-id\", \"x-mnote-trace-id\", \"x-request-id\") ?? makeFallbackId(\"trace\");\n  const sourceChannel = input.source?.channel?.trim() || readHeaderValue(headerList, \"x-source-channel\") || \"next-route\";\n  const sourceClient =\n    input.source?.client?.trim() ||\n    readHeaderValue(headerList, \"x-source-client\", \"user-agent\") ||\n    \"wolai-frontend\";\n  const deploymentId =\n    readHeaderValue(headerList, \"x-deployment-id\") ?? process.env.VERCEL_DEPLOYMENT_ID ?? null;\n  const projectId = readHeaderValue(headerList, \"x-project-id\") ?? process.env.VERCEL_PROJECT_ID ?? null;\n  const tenantId = readHeaderValue(headerList, \"x-tenant-id\");\n  const authToken = input.authToken ?? readHeaderValue(headerList, \"authorization\");\n  const idempotencyKey =\n    input.idempotencyKey ?? readHeaderValue(headerList, \"idempotency-key\", \"x-idempotency-key\");\n  const validateOnly = input.validateOnly ?? toBooleanFlag(readHeaderValue(headerList, \"x-validate-only\"));","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":241,"e":300,"m":1777105111244.5793,"t":"const sourceClient =\n    input.source?.client?.trim() ||\n    readHeaderValue(headerList, \"x-source-client\", \"user-agent\") ||\n    \"wolai-frontend\";\n  const deploymentId =\n    readHeaderValue(headerList, \"x-deployment-id\") ?? process.env.VERCEL_DEPLOYMENT_ID ?? null;\n  const projectId = readHeaderValue(headerList, \"x-project-id\") ?? process.env.VERCEL_PROJECT_ID ?? null;\n  const tenantId = readHeaderValue(headerList, \"x-tenant-id\");\n  const authToken = input.authToken ?? readHeaderValue(headerList, \"authorization\");\n  const idempotencyKey =\n    input.idempotencyKey ?? readHeaderValue(headerList, \"idempotency-key\", \"x-idempotency-key\");\n  const validateOnly = input.validateOnly ?? toBooleanFlag(readHeaderValue(headerList, \"x-validate-only\"));\n  const dryRun = input.dryRun ?? toBooleanFlag(readHeaderValue(headerList, \"x-dry-run\"));\n\n  return {\n    deploymentId,\n    projectId,\n    workspaceId: input.workspaceId ?? null,\n    requestId,\n    traceId,\n    actor: input.actor,\n    source: {\n      channel: sourceChannel,\n      client: sourceClient,\n    },\n    tenantId,\n    authToken,\n    idempotencyKey,\n    validateOnly,\n    dryRun,\n  };\n}\n\nexport function buildDocumentCommandEnvelope<T>(input: {\n  name: string;\n  payload: T;\n  context: BridgeContext;\n  target?: BridgeTarget | null;\n  preflightData?: Record<string, unknown> | null;\n  reason?: string | null;\n  refs?: string[];\n}): CommandEnvelope<T> {\n  return {\n    name: input.name,\n    commandId: makeFallbackId(\"cmd\"),\n    idempotencyKey: input.context.idempotencyKey,\n    actor: input.context.actor,\n    source: input.context.source,\n    target: input.target ?? null,\n    payload: input.payload,\n    preflightData: input.preflightData ?? null,\n    reason: input.reason ?? null,\n    refs: input.refs ?? [],\n    dryRun: input.context.dryRun,\n    validateOnly: input.context.validateOnly,\n  };\n}\n\nexport function buildDocumentQueryEnvelope<T>(input: { name: string; payload: T }): QueryEnvelope<T> {\n  return {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":289,"e":348,"m":1777105111244.5793,"t":"target: input.target ?? null,\n    payload: input.payload,\n    preflightData: input.preflightData ?? null,\n    reason: input.reason ?? null,\n    refs: input.refs ?? [],\n    dryRun: input.context.dryRun,\n    validateOnly: input.context.validateOnly,\n  };\n}\n\nexport function buildDocumentQueryEnvelope<T>(input: { name: string; payload: T }): QueryEnvelope<T> {\n  return {\n    name: input.name,\n    payload: input.payload,\n  };\n}\n\nfunction getDocumentBridgeMutationFunctionName(commandName: string): string {\n  const functionName =\n    DOCUMENT_BRIDGE_MUTATION_FUNCTIONS[\n      commandName as keyof typeof DOCUMENT_BRIDGE_MUTATION_FUNCTIONS\n    ];\n  if (!functionName) {\n    throw new DocumentBridgeError(\n      `未注册文档 bridge mutation: ${commandName}`,\n      500,\n      \"TRANSPORT_ERROR\",\n    );\n  }\n  return functionName;\n}\n\nfunction buildDocumentCommandPayloadJson(input: {\n  context: BridgeContext;\n  commandName: string;\n  workspaceId: string | null;\n  idempotencyKey: string | null;\n}): string {\n  return JSON.stringify({\n    kind: \"command\",\n    name: input.commandName,\n    request_id: input.context.requestId,\n    trace_id: input.context.traceId,\n    deployment_id: input.context.deploymentId,\n    project_id: input.context.projectId,\n    workspace_id: input.workspaceId,\n    tenant_id: input.context.tenantId,\n    idempotency_key: input.idempotencyKey,\n    actor: {\n      type: input.context.actor.actorType,\n      id: input.context.actor.actorId,\n      session_id: input.context.actor.sessionId,\n    },\n    source: {\n      channel: input.context.source.channel,\n      client: input.context.source.client,\n    },\n  });\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":337,"e":396,"m":1777105111244.5793,"t":"actor: {\n      type: input.context.actor.actorType,\n      id: input.context.actor.actorId,\n      session_id: input.context.actor.sessionId,\n    },\n    source: {\n      channel: input.context.source.channel,\n      client: input.context.source.client,\n    },\n  });\n}\n\nfunction buildDocumentQueryPayloadJson(input: {\n  context: BridgeContext;\n  queryName: string;\n  workspaceId: string | null;\n}): string {\n  return JSON.stringify({\n    kind: \"query\",\n    name: input.queryName,\n    request_id: input.context.requestId,\n    trace_id: input.context.traceId,\n    deployment_id: input.context.deploymentId,\n    project_id: input.context.projectId,\n    workspace_id: input.workspaceId,\n    tenant_id: input.context.tenantId,\n    actor_id: input.context.actor.actorId,\n    source: {\n      channel: input.context.source.channel,\n      client: input.context.source.client,\n    },\n  });\n}\n\nexport function buildDocumentBridgeMutationRequest<\n  TPayload,\n  TArgs extends Record<string, unknown>,\n>(input: {\n  context: BridgeContext;\n  envelope: CommandEnvelope<TPayload>;\n  mapConvexArgs: (payload: TPayload) => TArgs;\n}): DocumentBridgeMutationRequest<TArgs> {\n  const workspaceId = input.envelope.target?.workspaceId ?? input.context.workspaceId ?? null;\n  const idempotencyKey = input.envelope.idempotencyKey ?? input.context.idempotencyKey;\n\n  return {\n    functionName: getDocumentBridgeMutationFunctionName(input.envelope.name),\n    deploymentId: input.context.deploymentId,\n    projectId: input.context.projectId,\n    workspaceId,\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    idempotencyKey,\n    actorId: input.context.actor.actorId,\n    payloadJson: buildDocumentCommandPayloadJson({\n      context: input.context,\n      commandName: input.envelope.name,\n      workspaceId,\n      idempotencyKey,\n    }),","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":385,"e":444,"m":1777105111244.5793,"t":"projectId: input.context.projectId,\n    workspaceId,\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    idempotencyKey,\n    actorId: input.context.actor.actorId,\n    payloadJson: buildDocumentCommandPayloadJson({\n      context: input.context,\n      commandName: input.envelope.name,\n      workspaceId,\n      idempotencyKey,\n    }),\n    args: input.mapConvexArgs(input.envelope.payload),\n  };\n}\n\nfunction getDocumentBridgeQueryFunctionName(queryName: string): string {\n  const functionName =\n    DOCUMENT_BRIDGE_QUERY_FUNCTIONS[\n      queryName as keyof typeof DOCUMENT_BRIDGE_QUERY_FUNCTIONS\n    ];\n  if (!functionName) {\n    throw new DocumentBridgeError(\n      `未注册文档 bridge query: ${queryName}`,\n      500,\n      \"TRANSPORT_ERROR\",\n    );\n  }\n  return functionName;\n}\n\nexport function buildDocumentBridgeQueryRequest<\n  TPayload,\n  TArgs extends Record<string, unknown>,\n>(input: {\n  context: BridgeContext;\n  envelope: QueryEnvelope<TPayload>;\n  mapConvexArgs: (payload: TPayload) => TArgs;\n}): DocumentBridgeQueryRequest<TArgs> {\n  const workspaceId = input.context.workspaceId ?? null;\n  return {\n    functionName: getDocumentBridgeQueryFunctionName(input.envelope.name),\n    deploymentId: input.context.deploymentId,\n    projectId: input.context.projectId,\n    workspaceId,\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    actorId: input.context.actor.actorId,\n    payloadJson: buildDocumentQueryPayloadJson({\n      context: input.context,\n      queryName: input.envelope.name,\n      workspaceId,\n    }),\n    args: input.mapConvexArgs(input.envelope.payload),\n  };\n}\n\nexport async function executeDocumentBridgeMutationRequest<\n  TArgs extends Record<string, unknown>,\n  TResult,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":433,"e":492,"m":1777105111244.5793,"t":"payloadJson: buildDocumentQueryPayloadJson({\n      context: input.context,\n      queryName: input.envelope.name,\n      workspaceId,\n    }),\n    args: input.mapConvexArgs(input.envelope.payload),\n  };\n}\n\nexport async function executeDocumentBridgeMutationRequest<\n  TArgs extends Record<string, unknown>,\n  TResult,\n>(input: {\n  client: ConvexHttpClient;\n  mutation: unknown;\n  request: DocumentBridgeMutationRequest<TArgs>;\n}): Promise<TResult> {\n  const mutate = input.client.mutation.bind(input.client) as (\n    mutation: unknown,\n    args: TArgs,\n  ) => Promise<TResult>;\n  return mutate(input.mutation, input.request.args);\n}\n\nexport function assertDocumentId(documentId: string | null | undefined): string {\n  const normalized = typeof documentId === \"string\" ? documentId.trim() : \"\";\n  if (!normalized) {\n    throw new DocumentBridgeError(\"缺少 documentId\", 400, \"VALIDATION_ERROR\", [\n      { field: \"documentId\", reason: \"required\" },\n    ]);\n  }\n  return normalized;\n}\n\nexport function assertTitle(title: string | null | undefined): string {\n  if (typeof title !== \"string\") {\n    throw new DocumentBridgeError(\"缺少 title\", 400, \"VALIDATION_ERROR\", [\n      { field: \"title\", reason: \"required\" },\n    ]);\n  }\n  const normalized = title.trim() || \"无标题\";\n  return normalized;\n}\n\nexport function assertBlockId(blockId: string | null | undefined): string {\n  const normalized = typeof blockId === \"string\" ? blockId.trim() : \"\";\n  if (!normalized) {\n    throw new DocumentBridgeError(\"缺少 blockId\", 400, \"VALIDATION_ERROR\", [\n      { field: \"blockId\", reason: \"required\" },\n    ]);\n  }\n  return normalized;\n}\n\nexport function assertNextBlock(nextBlock: unknown): asserts nextBlock is Record<string, unknown> {\n  if (!nextBlock || typeof nextBlock !== \"object\" || Array.isArray(nextBlock)) {\n    throw new DocumentBridgeError(\"缺少 nextBlock\", 400, \"VALIDATION_ERROR\", [\n      { field: \"nextBlock\", reason: \"required object\" },\n    ]);\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":481,"e":540,"m":1777105111244.5793,"t":"{ field: \"blockId\", reason: \"required\" },\n    ]);\n  }\n  return normalized;\n}\n\nexport function assertNextBlock(nextBlock: unknown): asserts nextBlock is Record<string, unknown> {\n  if (!nextBlock || typeof nextBlock !== \"object\" || Array.isArray(nextBlock)) {\n    throw new DocumentBridgeError(\"缺少 nextBlock\", 400, \"VALIDATION_ERROR\", [\n      { field: \"nextBlock\", reason: \"required object\" },\n    ]);\n  }\n}\n\nexport function assertStats(stats: unknown): asserts stats is {\n  wordCount: number;\n  characterCount: number;\n  blockCount: number;\n  todoTotal: number;\n  todoDone: number;\n} {\n  if (!stats || typeof stats !== \"object\") {\n    throw new DocumentBridgeError(\"缺少 stats\", 400, \"VALIDATION_ERROR\", [\n      { field: \"stats\", reason: \"required\" },\n    ]);\n  }\n  const record = stats as Record<string, unknown>;\n  const fields = [\"wordCount\", \"characterCount\", \"blockCount\", \"todoTotal\", \"todoDone\"] as const;\n  for (const field of fields) {\n    if (typeof record[field] !== \"number\" || !Number.isFinite(record[field] as number)) {\n      throw new DocumentBridgeError(\"stats 字段非法\", 400, \"VALIDATION_ERROR\", [\n        { field, reason: \"must be finite number\" },\n      ]);\n    }\n  }\n}\n\nconst PAGE_FONT_VALUES = new Set<PageOptionsState[\"pageFont\"]>([\"default\", \"song\", \"kai\"]);\nconst PAGE_LAYOUT_DENSITY_VALUES = new Set<PageOptionsState[\"layoutDensity\"]>([\"compact\", \"normal\", \"spacious\"]);\nconst PAGE_OPTION_BOOLEAN_FIELDS = [\n  \"wideLayout\",\n  \"smallText\",\n  \"showHeadingNumbers\",\n  \"showToc\",\n  \"showStructure\",\n  \"protectEditing\",\n  \"showWordCount\",\n  \"collapseBacklinks\",\n  \"hideChildPages\",\n  \"showBlockRefCount\",\n] as const satisfies readonly (keyof PageOptionsState)[];\nconst PAGE_OPTION_ALLOWED_FIELDS = new Set<keyof PageOptionsState>([\n  ...PAGE_OPTION_BOOLEAN_FIELDS,\n  \"pageFont\",\n  \"layoutDensity\",\n  \"embedDefaultBlockId\",\n]);\n\nexport function assertOptionsPatch(\n  options: unknown,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":529,"e":588,"m":1777105111244.5793,"t":"\"hideChildPages\",\n  \"showBlockRefCount\",\n] as const satisfies readonly (keyof PageOptionsState)[];\nconst PAGE_OPTION_ALLOWED_FIELDS = new Set<keyof PageOptionsState>([\n  ...PAGE_OPTION_BOOLEAN_FIELDS,\n  \"pageFont\",\n  \"layoutDensity\",\n  \"embedDefaultBlockId\",\n]);\n\nexport function assertOptionsPatch(\n  options: unknown,\n): asserts options is Partial<Pick<PageOptionsState, keyof PageOptionsState>> {\n  if (!options || typeof options !== \"object\" || Array.isArray(options)) {\n    throw new DocumentBridgeError(\"缺少 options\", 400, \"VALIDATION_ERROR\", [\n      { field: \"options\", reason: \"required object\" },\n    ]);\n  }\n\n  const record = options as Record<string, unknown>;\n  const entries = Object.entries(record);\n  if (entries.length === 0) {\n    throw new DocumentBridgeError(\"缺少 options\", 400, \"VALIDATION_ERROR\", [\n      { field: \"options\", reason: \"must not be empty\" },\n    ]);\n  }\n\n  for (const [field, value] of entries) {\n    if (!PAGE_OPTION_ALLOWED_FIELDS.has(field as keyof PageOptionsState)) {\n      throw new DocumentBridgeError(\"options 字段非法\", 400, \"VALIDATION_ERROR\", [\n        { field, reason: \"unexpected field\" },\n      ]);\n    }\n\n    if ((PAGE_OPTION_BOOLEAN_FIELDS as readonly string[]).includes(field)) {\n      if (typeof value !== \"boolean\") {\n        throw new DocumentBridgeError(\"options 字段非法\", 400, \"VALIDATION_ERROR\", [\n          { field, reason: \"must be boolean\" },\n        ]);\n      }\n      continue;\n    }\n\n    if (field === \"pageFont\") {\n      if (typeof value !== \"string\" || !PAGE_FONT_VALUES.has(value as PageOptionsState[\"pageFont\"])) {\n        throw new DocumentBridgeError(\"options 字段非法\", 400, \"VALIDATION_ERROR\", [\n          { field, reason: \"must be one of default/song/kai\" },\n        ]);\n      }\n      continue;\n    }\n\n    if (field === \"layoutDensity\") {\n      if (\n        typeof value !== \"string\" ||\n        !PAGE_LAYOUT_DENSITY_VALUES.has(value as PageOptionsState[\"layoutDensity\"])\n      ) {\n        throw new DocumentBridgeError(\"options 字段非法\", 400, \"VALIDATION_ERROR\", [\n          { field, reason: \"must be one of compact/normal/spacious\" },\n        ]);","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/bridge.ts","s":577,"e":613,"m":1777105111244.5793,"t":"}\n      continue;\n    }\n\n    if (field === \"layoutDensity\") {\n      if (\n        typeof value !== \"string\" ||\n        !PAGE_LAYOUT_DENSITY_VALUES.has(value as PageOptionsState[\"layoutDensity\"])\n      ) {\n        throw new DocumentBridgeError(\"options 字段非法\", 400, \"VALIDATION_ERROR\", [\n          { field, reason: \"must be one of compact/normal/spacious\" },\n        ]);\n      }\n      continue;\n    }\n\n    if (field === \"embedDefaultBlockId\" && value !== null && typeof value !== \"string\") {\n      throw new DocumentBridgeError(\"options 字段非法\", 400, \"VALIDATION_ERROR\", [\n        { field, reason: \"must be string or null\" },\n      ]);\n    }\n  }\n}\n\nexport function documentBridgeErrorResponse(error: unknown) {\n  if (error instanceof DocumentBridgeError) {\n    return apiErrorResponse(error.message, error.status, {\n      code: error.code,\n      details: error.details,\n    });\n  }\n  if (error instanceof HttpError) {\n    return apiErrorResponse(error.message || \"未登录\", error.status);\n  }\n  return apiErrorResponse(error instanceof Error ? error.message : \"服务器错误\", 500);\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-move-order-convex.test.ts","s":1,"e":60,"m":1777358057097.1814,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  assertDocumentMoveWriteOperationMatches,\n  buildDocumentMoveOrderPlanFromDocuments,\n} from \"../../../convex/_utils/documentMoveOrder\";\n\ndescribe(\"documentMoveOrder Convex helper\", () => {\n  it(\"assertDocumentMoveWriteOperationMatches 应只接受 Rust tree write operation，并返回内部 move plan\", () => {\n    const actual = buildDocumentMoveOrderPlanFromDocuments({\n      documents: [\n        {\n          id: \"doc_a\",\n          parent_id: \"source\",\n          sort_order: 0,\n          created_at: \"2026-04-25T00:00:01Z\",\n        },\n        {\n          id: \"doc_b\",\n          parent_id: \"source\",\n          sort_order: 1,\n          created_at: \"2026-04-25T00:00:02Z\",\n        },\n      ],\n      documentId: \"doc_b\",\n      parentId: null,\n      sortOrder: 0,\n    });\n\n    const normalized = assertDocumentMoveWriteOperationMatches(\n      {\n        family: \"tree\",\n        schema: \"mnote.tree.write_operation\",\n        schemaVersion: 1,\n        operation: \"tree.subtree.move.write\",\n        workspaceId: \"ws_1\",\n        documentId: \"doc_b\",\n        fromParentId: \"source\",\n        toParentId: null,\n        requestedSortOrder: 0,\n        normalizedSortOrder: 0,\n        patches: [\n          {\n            documentId: \"doc_b\",\n            parentId: null,\n            sortOrder: 0,\n            moved: true,\n          },\n        ],\n      },\n      actual,\n    );\n\n    expect(normalized).toEqual({\n      documentId: \"doc_b\",\n      fromParentId: \"source\",\n      toParentId: null,\n      requestedSortOrder: 0,\n      normalizedSortOrder: 0,\n      patches: [\n        {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-move-order-convex.test.ts","s":49,"e":100,"m":1777358057097.1814,"t":"},\n      actual,\n    );\n\n    expect(normalized).toEqual({\n      documentId: \"doc_b\",\n      fromParentId: \"source\",\n      toParentId: null,\n      requestedSortOrder: 0,\n      normalizedSortOrder: 0,\n      patches: [\n        {\n          documentId: \"doc_b\",\n          parentId: null,\n          sortOrder: 0,\n          moved: true,\n        },\n      ],\n    });\n\n    expect(() => assertDocumentMoveWriteOperationMatches({ ...actual, operation: \"documents.move\" }, actual)).toThrow(\n      \"Rust move write operation 与 Convex 当前排序状态不一致\",\n    );\n  });\n\n  it(\"assertDocumentMoveWriteOperationMatches 应拒绝缺少正式 write operation 外壳的旧 move plan\", () => {\n    const actual = buildDocumentMoveOrderPlanFromDocuments({\n      documents: [\n        {\n          id: \"doc_a\",\n          parent_id: \"source\",\n          sort_order: 0,\n          created_at: \"2026-04-25T00:00:01Z\",\n        },\n        {\n          id: \"doc_b\",\n          parent_id: \"source\",\n          sort_order: 1,\n          created_at: \"2026-04-25T00:00:02Z\",\n        },\n      ],\n      documentId: \"doc_b\",\n      parentId: null,\n      sortOrder: 0,\n    });\n\n    expect(() => assertDocumentMoveWriteOperationMatches(actual, actual)).toThrow(\n      \"Rust move write operation 与 Convex 当前排序状态不一致\",\n    );\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-move-order.test.ts","s":1,"e":60,"m":1777358079798.475,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  assertDocumentMoveWriteOperationMatches,\n  buildDocumentMoveOrderPlanFromDocuments,\n} from \"../../../convex/_utils/documentMoveOrder\";\n\ndescribe(\"documentMoveOrder\", () => {\n  it(\"按 Rust canonical move order plan 计算跨父移动和越界 clamp\", () => {\n    const plan = buildDocumentMoveOrderPlanFromDocuments({\n      documents: [\n        {\n          id: \"target\",\n          parent_id: null,\n          sort_order: 0,\n          created_at: \"2026-04-25T00:00:00Z\",\n        },\n        {\n          id: \"doc_a\",\n          parent_id: \"source\",\n          sort_order: 0,\n          created_at: \"2026-04-25T00:00:01Z\",\n        },\n        {\n          id: \"doc_b\",\n          parent_id: \"source\",\n          sort_order: 1,\n          created_at: \"2026-04-25T00:00:02Z\",\n        },\n        {\n          id: \"doc_c\",\n          parent_id: \"target\",\n          sort_order: 0,\n          created_at: \"2026-04-25T00:00:03Z\",\n        },\n      ],\n      documentId: \"doc_b\",\n      parentId: \"target\",\n      sortOrder: -2,\n    });\n\n    expect(plan).toEqual({\n      documentId: \"doc_b\",\n      fromParentId: \"source\",\n      toParentId: \"target\",\n      requestedSortOrder: -2,\n      normalizedSortOrder: 0,\n      patches: [\n        {\n          documentId: \"doc_b\",\n          parentId: \"target\",\n          sortOrder: 0,\n          moved: true,\n        },\n        {\n          documentId: \"doc_c\",\n          parentId: \"target\",\n          sortOrder: 1,\n          moved: false,\n        },\n      ],","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-move-order.test.ts","s":49,"e":108,"m":1777358079798.475,"t":"documentId: \"doc_b\",\n          parentId: \"target\",\n          sortOrder: 0,\n          moved: true,\n        },\n        {\n          documentId: \"doc_c\",\n          parentId: \"target\",\n          sortOrder: 1,\n          moved: false,\n        },\n      ],\n    });\n  });\n\n  it(\"treeWriteOperation 与当前排序状态不一致时拒绝执行\", () => {\n    const actual = buildDocumentMoveOrderPlanFromDocuments({\n      documents: [\n        {\n          id: \"doc_a\",\n          parent_id: \"source\",\n          sort_order: 0,\n          created_at: \"2026-04-25T00:00:01Z\",\n        },\n        {\n          id: \"doc_b\",\n          parent_id: \"source\",\n          sort_order: 1,\n          created_at: \"2026-04-25T00:00:02Z\",\n        },\n        {\n          id: \"doc_c\",\n          parent_id: \"target\",\n          sort_order: 0,\n          created_at: \"2026-04-25T00:00:03Z\",\n        },\n      ],\n      documentId: \"doc_b\",\n      parentId: \"target\",\n      sortOrder: 0,\n    });\n\n    expect(() =>\n      assertDocumentMoveWriteOperationMatches(\n        {\n          family: \"tree\",\n          schema: \"mnote.tree.write_operation\",\n          schemaVersion: 1,\n          operation: \"tree.subtree.move.write\",\n          workspaceId: \"ws_1\",\n          ...actual,\n          patches: actual.patches.map((patch) =>\n            patch.documentId === \"doc_c\" ? { ...patch, sortOrder: 9 } : patch,\n          ),\n        },\n        actual,\n      ),\n    ).toThrow(\"Rust move write operation 与 Convex 当前排序状态不一致\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-move-order.test.ts","s":97,"e":109,"m":1777358079798.475,"t":"operation: \"tree.subtree.move.write\",\n          workspaceId: \"ws_1\",\n          ...actual,\n          patches: actual.patches.map((patch) =>\n            patch.documentId === \"doc_c\" ? { ...patch, sortOrder: 9 } : patch,\n          ),\n        },\n        actual,\n      ),\n    ).toThrow(\"Rust move write operation 与 Convex 当前排序状态不一致\");\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-record.test.ts","s":1,"e":60,"m":1777070758266.136,"t":"import { describe, expect, it } from \"vitest\";\nimport {\n  compareDocumentCanonicalOrder,\n  getCanonicalDocumentByBusinessId,\n  getCanonicalParentDocumentId,\n  pickCanonicalDocumentRecordsByBusinessId,\n  pickCanonicalDocumentRecord,\n} from \"../../../convex/_utils/documentRecord\";\n\ndescribe(\"pickCanonicalDocumentRecord\", () => {\n  it(\"同一 business id 出现重复记录时优先选择最新未删除记录\", () => {\n    const selected = pickCanonicalDocumentRecord([\n      {\n        _id: \"doc_old\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:01.000Z\",\n      },\n      {\n        _id: \"doc_deleted\",\n        deleted_at: \"2026-04-14T00:00:03.000Z\",\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n      {\n        _id: \"doc_new\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:04.000Z\",\n      },\n    ]);\n\n    expect(selected?._id).toBe(\"doc_new\");\n  });\n\n  it(\"全部已删除时仍稳定选择最新记录，避免 first() 命中漂移\", () => {\n    const selected = pickCanonicalDocumentRecord([\n      {\n        _id: \"doc_deleted_old\",\n        deleted_at: \"2026-04-14T00:00:01.000Z\",\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:01.000Z\",\n      },\n      {\n        _id: \"doc_deleted_new\",\n        deleted_at: \"2026-04-14T00:00:03.000Z\",\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n    ]);\n\n    expect(selected?._id).toBe(\"doc_deleted_new\");\n  });\n\n  it(\"时间戳相同时用 _id 稳定打破平手，避免 collect 后结果不稳定\", () => {\n    const selected = pickCanonicalDocumentRecord([\n      {\n        _id: \"doc_b\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:00.000Z\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-record.test.ts","s":49,"e":108,"m":1777070758266.136,"t":"},\n    ]);\n\n    expect(selected?._id).toBe(\"doc_deleted_new\");\n  });\n\n  it(\"时间戳相同时用 _id 稳定打破平手，避免 collect 后结果不稳定\", () => {\n    const selected = pickCanonicalDocumentRecord([\n      {\n        _id: \"doc_b\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:00.000Z\",\n      },\n      {\n        _id: \"doc_a\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:00.000Z\",\n      },\n    ]);\n\n    expect(selected?._id).toBe(\"doc_a\");\n  });\n});\n\ndescribe(\"compareDocumentCanonicalOrder\", () => {\n  it(\"未删除记录始终排在已删除记录前面，供页面权限/内容读链复用\", () => {\n    const records = [\n      {\n        _id: \"deleted\",\n        deleted_at: \"2026-04-14T00:00:03.000Z\",\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n      {\n        _id: \"alive\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:01.000Z\",\n        updated_at: \"2026-04-14T00:00:01.000Z\",\n      },\n    ];\n\n    const sorted = [...records].sort(compareDocumentCanonicalOrder);\n    expect(sorted.map((record) => record._id)).toEqual([\"alive\", \"deleted\"]);\n  });\n});\n\ndescribe(\"canonical document helper\", () => {\n  const createCtx = (records: Array<Record<string, unknown>>) => ({\n    db: {\n      query: () => ({\n        withIndex: () => ({\n          collect: async () => records,\n        }),\n      }),\n    },\n  });\n\n  it(\"按 business id 查询时返回 canonical 记录，避免高风险读链命中旧记录\", async () => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-record.test.ts","s":97,"e":156,"m":1777070758266.136,"t":"describe(\"canonical document helper\", () => {\n  const createCtx = (records: Array<Record<string, unknown>>) => ({\n    db: {\n      query: () => ({\n        withIndex: () => ({\n          collect: async () => records,\n        }),\n      }),\n    },\n  });\n\n  it(\"按 business id 查询时返回 canonical 记录，避免高风险读链命中旧记录\", async () => {\n    const ctx = createCtx([\n      {\n        _id: \"doc_old\",\n        id: \"doc_1\",\n        parent_id: \"parent_old\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:01.000Z\",\n      },\n      {\n        _id: \"doc_new\",\n        id: \"doc_1\",\n        parent_id: \"parent_new\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n    ]);\n\n    const doc = await getCanonicalDocumentByBusinessId<any>(ctx, \"doc_1\");\n    expect(doc?._id).toBe(\"doc_new\");\n  });\n\n  it(\"父链辅助函数返回 canonical 父页面 id，供共享/评论祖先扫描复用\", async () => {\n    const ctx = createCtx([\n      {\n        _id: \"doc_old\",\n        id: \"doc_1\",\n        parent_id: \"parent_old\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:01.000Z\",\n      },\n      {\n        _id: \"doc_new\",\n        id: \"doc_1\",\n        parent_id: \"parent_new\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n    ]);\n\n    const parentId = await getCanonicalParentDocumentId(ctx, \"doc_1\");\n    expect(parentId).toBe(\"parent_new\");\n  });\n\n  it(\"父链辅助函数命中已删除旧记录时仍回退到最新未删除父级，供收藏祖先权限复用\", async () => {","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-record.test.ts","s":145,"e":204,"m":1777070758266.136,"t":"parent_id: \"parent_new\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n    ]);\n\n    const parentId = await getCanonicalParentDocumentId(ctx, \"doc_1\");\n    expect(parentId).toBe(\"parent_new\");\n  });\n\n  it(\"父链辅助函数命中已删除旧记录时仍回退到最新未删除父级，供收藏祖先权限复用\", async () => {\n    const ctx = createCtx([\n      {\n        _id: \"doc_deleted\",\n        id: \"doc_2\",\n        parent_id: \"parent_deleted\",\n        deleted_at: \"2026-04-14T00:00:04.000Z\",\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:04.000Z\",\n      },\n      {\n        _id: \"doc_alive\",\n        id: \"doc_2\",\n        parent_id: \"parent_alive\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:01.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n    ]);\n\n    const parentId = await getCanonicalParentDocumentId(ctx, \"doc_2\");\n    expect(parentId).toBe(\"parent_alive\");\n  });\n});\n\ndescribe(\"pickCanonicalDocumentRecordsByBusinessId\", () => {\n  it(\"同一 workspace 扫描结果里 business id 重复时应先折叠成 canonical 记录，供树命令写链复用\", () => {\n    const records = pickCanonicalDocumentRecordsByBusinessId([\n      {\n        _id: \"doc_old\",\n        id: \"doc_1\",\n        parent_id: \"parent_old\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:00.000Z\",\n        updated_at: \"2026-04-14T00:00:01.000Z\",\n      },\n      {\n        _id: \"doc_new\",\n        id: \"doc_1\",\n        parent_id: \"parent_new\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n      {\n        _id: \"doc_2\",\n        id: \"doc_2\",\n        parent_id: null,\n        deleted_at: null,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-record.test.ts","s":193,"e":214,"m":1777070758266.136,"t":"_id: \"doc_new\",\n        id: \"doc_1\",\n        parent_id: \"parent_new\",\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:02.000Z\",\n        updated_at: \"2026-04-14T00:00:03.000Z\",\n      },\n      {\n        _id: \"doc_2\",\n        id: \"doc_2\",\n        parent_id: null,\n        deleted_at: null,\n        created_at: \"2026-04-14T00:00:04.000Z\",\n        updated_at: \"2026-04-14T00:00:05.000Z\",\n      },\n    ]);\n\n    expect(records).toHaveLength(2);\n    expect(records.map((record) => record._id)).toEqual([\"doc_new\", \"doc_2\"]);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-tree.test.ts","s":1,"e":30,"m":1777070758267.136,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildParentById, isAncestorOf } from \"../../../convex/_utils/documentTree\";\n\ndescribe(\"document tree helper\", () => {\n  it(\"构建父链映射时保留页面与父页面关系，供树命令 legality 复用\", () => {\n    const parentById = buildParentById([\n      { id: \"root\", parent_id: null },\n      { id: \"child\", parent_id: \"root\" },\n      { id: \"leaf\", parent_id: \"child\" },\n    ]);\n\n    expect(parentById.get(\"root\")).toBeNull();\n    expect(parentById.get(\"child\")).toBe(\"root\");\n    expect(parentById.get(\"leaf\")).toBe(\"child\");\n  });\n\n  it(\"祖先判断应能识别多级后代，避免把页面移动到自己的子树下面\", () => {\n    const parentById = buildParentById([\n      { id: \"root\", parent_id: null },\n      { id: \"child\", parent_id: \"root\" },\n      { id: \"leaf\", parent_id: \"child\" },\n    ]);\n\n    expect(isAncestorOf(\"root\", \"leaf\", parentById)).toBe(true);\n    expect(isAncestorOf(\"child\", \"leaf\", parentById)).toBe(true);\n    expect(isAncestorOf(\"leaf\", \"root\", parentById)).toBe(false);\n    expect(isAncestorOf(\"missing\", \"leaf\", parentById)).toBe(false);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/document-visibility.test.ts","s":1,"e":48,"m":1778540818478.8376,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildVisibleDocumentIds } from \"../../../convex/_utils/documentVisibility\";\n\ndescribe(\"buildVisibleDocumentIds\", () => {\n  const docs = [\n    { id: \"owner-root\", user_id: \"u1\", parent_id: null, access_scope: \"private\", deleted_at: null },\n    { id: \"public-root\", user_id: \"u2\", parent_id: null, access_scope: \"public\", deleted_at: null },\n    { id: \"shared-root\", user_id: \"u2\", parent_id: null, access_scope: \"private\", deleted_at: null },\n    { id: \"shared-child\", user_id: \"u2\", parent_id: \"shared-root\", access_scope: \"private\", deleted_at: null },\n    { id: \"group-root\", user_id: \"u3\", parent_id: null, access_scope: \"private\", deleted_at: null },\n    { id: \"group-child\", user_id: \"u3\", parent_id: \"group-root\", access_scope: \"private\", deleted_at: null },\n    { id: \"deleted-public\", user_id: \"u4\", parent_id: null, access_scope: \"public\", deleted_at: \"2026-05-12T00:00:00.000Z\" },\n  ] as const;\n\n  it(\"允许 owner、public、直接分享与继承分享的文档可见\", () => {\n    const visible = buildVisibleDocumentIds({\n      userId: \"u1\",\n      docs,\n      directShares: [{ document_id: \"shared-root\", include_descendants: true }],\n      directGroupShares: [],\n    });\n\n    expect(Array.from(visible).sort()).toEqual([\n      \"owner-root\",\n      \"public-root\",\n      \"shared-child\",\n      \"shared-root\",\n    ]);\n  });\n\n  it(\"允许群组分享向下继承，但不会包含已删除文档\", () => {\n    const visible = buildVisibleDocumentIds({\n      userId: \"u9\",\n      docs,\n      directShares: [],\n      directGroupShares: [{ document_id: \"group-root\", include_descendants: true }],\n      includeDeletedDocuments: false,\n    });\n\n    expect(Array.from(visible).sort()).toEqual([\n      \"group-child\",\n      \"group-root\",\n      \"public-root\",\n    ]);\n    expect(visible.has(\"deleted-public\")).toBe(false);\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/media-asset-command-adapter.ts","s":1,"e":60,"m":1777196225009.4397,"t":"import type { ConvexHttpClient } from \"convex/browser\";\nimport {\n  type CommandEnvelope,\n  type BridgeContext,\n} from \"@/lib/documents/bridge\";\nimport {\n  recordBridgeCommandFailureArtifacts,\n  recordRustBridgeCommandArtifacts,\n} from \"@/lib/documents/bridge-log\";\nimport {\n  executeRustBridgeMutationTransport,\n  resolveRustBridgeCommandPlan,\n} from \"@/lib/documents/rust-runtime\";\n\nexport type MediaAssetReplaceStoragePayload = {\n  assetId: string;\n  documentId: string;\n  workspaceId: string | null;\n  storageId: string;\n  userId: string;\n};\n\nexport type MediaAssetWritebackExecutionResult = {\n  requestId: string;\n  traceId: string;\n  commandId: string;\n  commandName: string;\n};\n\nexport async function executeMediaAssetWritebackBridgeCommand(input: {\n  client: ConvexHttpClient;\n  context: BridgeContext;\n  envelope: CommandEnvelope<MediaAssetReplaceStoragePayload>;\n}): Promise<MediaAssetWritebackExecutionResult> {\n  const plan = await resolveRustBridgeCommandPlan({\n    context: input.context,\n    envelope: input.envelope,\n  });\n  try {\n    const transportResult = await executeRustBridgeMutationTransport({\n      client: input.client,\n      plan,\n    });\n    await recordRustBridgeCommandArtifacts({\n      client: input.client,\n      context: input.context,\n      envelope: input.envelope,\n      plan,\n      result: transportResult,\n    });\n  } catch (error) {\n    await recordBridgeCommandFailureArtifacts({\n      client: input.client,\n      context: input.context,\n      envelope: input.envelope,\n      error,\n    });\n    throw error;\n  }","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/media-asset-command-adapter.ts","s":49,"e":68,"m":1777196225009.4397,"t":"result: transportResult,\n    });\n  } catch (error) {\n    await recordBridgeCommandFailureArtifacts({\n      client: input.client,\n      context: input.context,\n      envelope: input.envelope,\n      error,\n    });\n    throw error;\n  }\n\n  return {\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    commandId: input.envelope.commandId,\n    commandName: input.envelope.name,\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/metadata-command-adapter.ts","s":1,"e":60,"m":1777197186979.8804,"t":"import { getAuthedConvexClient } from \"@/lib/convex/route\";\nimport {\n  type CommandEnvelope,\n  type BridgeContext,\n} from \"@/lib/documents/bridge\";\nimport {\n  recordBridgeCommandFailureArtifacts,\n  recordRustBridgeCommandArtifacts,\n} from \"@/lib/documents/bridge-log\";\nimport {\n  executeRustBridgeMutationTransport,\n  resolveRustBridgeCommandPlan,\n} from \"@/lib/documents/rust-runtime\";\nimport type { PageOptionsState } from \"@/types/page-options\";\n\nexport type DocumentTitleUpdatePayload = {\n  documentId: string;\n  workspaceId: string | null;\n  title: string;\n};\n\nexport type DocumentStatsUpdatePayload = {\n  documentId: string;\n  workspaceId: string | null;\n  stats: {\n    wordCount: number;\n    characterCount: number;\n    blockCount: number;\n    todoTotal: number;\n    todoDone: number;\n  };\n};\n\nexport type DocumentOptionsUpdatePayload = {\n  documentId: string;\n  workspaceId: string | null;\n  options: Partial<PageOptionsState>;\n};\n\nexport type MetadataCommandExecutionResult = {\n  requestId: string;\n  traceId: string;\n  commandId: string;\n  commandName: string;\n};\n\nexport async function executeMetadataBridgeCommand<TPayload>(input: {\n  context: BridgeContext;\n  envelope: CommandEnvelope<TPayload>;\n}): Promise<MetadataCommandExecutionResult> {\n  const { client } = await getAuthedConvexClient();\n  try {\n    const plan = await resolveRustBridgeCommandPlan({\n      context: input.context,\n      envelope: input.envelope,\n    });\n    const transportResult = await executeRustBridgeMutationTransport({\n      client,\n      plan,\n    });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/metadata-command-adapter.ts","s":49,"e":85,"m":1777197186979.8804,"t":"envelope: CommandEnvelope<TPayload>;\n}): Promise<MetadataCommandExecutionResult> {\n  const { client } = await getAuthedConvexClient();\n  try {\n    const plan = await resolveRustBridgeCommandPlan({\n      context: input.context,\n      envelope: input.envelope,\n    });\n    const transportResult = await executeRustBridgeMutationTransport({\n      client,\n      plan,\n    });\n    await recordRustBridgeCommandArtifacts({\n      client,\n      context: input.context,\n      envelope: input.envelope,\n      plan,\n      result: transportResult,\n    });\n  } catch (error) {\n    await recordBridgeCommandFailureArtifacts({\n      context: input.context,\n      envelope: input.envelope,\n      client,\n      error,\n    });\n    throw error;\n  }\n\n  return {\n    requestId: input.context.requestId,\n    traceId: input.context.traceId,\n    commandId: input.envelope.commandId,\n    commandName: input.envelope.name,\n  };\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-builder.test.ts","s":1,"e":60,"m":1776812315374.967,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildPageAggregateFromDocumentPayloads } from \"@/lib/documents/page-aggregate-builder\";\n\ndescribe(\"page-aggregate-builder\", () => {\n  it(\"会把 documents meta 与 content 收口为统一 page aggregate\", () => {\n    const aggregate = buildPageAggregateFromDocumentPayloads({\n      meta: {\n        id: \"doc_1\",\n        workspace_id: \"ws_1\",\n        title: \" 页面标题 \",\n        updated_at: \"2026-04-22T12:00:00.000Z\",\n        can_edit: false,\n        disable_download: true,\n        disable_copy: false,\n        wide_layout: true,\n        use_small_text: true,\n        show_heading_numbers: false,\n        show_toc: true,\n        show_structure: false,\n        protect_editing: false,\n        show_word_count: true,\n        collapse_backlinks: true,\n        page_font: \"default\",\n        layout_density: \"compact\",\n        hide_child_pages: true,\n        show_block_ref_count: true,\n        embed_default_block_id: \"block_1\",\n        word_count: 20,\n        character_count: 40,\n        block_count: 2,\n        todo_total: 3,\n        todo_done: 1,\n      },\n      contentPayload: {\n        content: [{ id: \"block_1\", type: \"paragraph\", content: [] }],\n        revision: 8,\n        conflict_detection_key: \"doc_1:8\",\n        page_subtree: null,\n      },\n    });\n\n    expect(aggregate.identity).toEqual({\n      documentId: \"doc_1\",\n      workspaceId: \"ws_1\",\n    });\n    expect(aggregate.head).toEqual({\n      title: \"页面标题\",\n      updatedAt: \"2026-04-22T12:00:00.000Z\",\n      permissions: {\n        readOnly: true,\n        disableDownload: true,\n        disableCopy: false,\n      },\n    });\n    expect(aggregate.layout.pageOptions).toMatchObject({\n      wideLayout: true,\n      smallText: true,\n      showHeadingNumbers: false,\n      showToc: true,\n      collapseBacklinks: true,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-builder.test.ts","s":49,"e":80,"m":1776812315374.967,"t":"permissions: {\n        readOnly: true,\n        disableDownload: true,\n        disableCopy: false,\n      },\n    });\n    expect(aggregate.layout.pageOptions).toMatchObject({\n      wideLayout: true,\n      smallText: true,\n      showHeadingNumbers: false,\n      showToc: true,\n      collapseBacklinks: true,\n      layoutDensity: \"compact\",\n      hideChildPages: true,\n      showBlockRefCount: true,\n      embedDefaultBlockId: \"block_1\",\n    });\n    expect(aggregate.body).toEqual({\n      content: [{ id: \"block_1\", type: \"paragraph\", content: [] }],\n      revision: 8,\n      conflictDetectionKey: \"doc_1:8\",\n    });\n    expect(aggregate.stats).toEqual({\n      wordCount: 20,\n      characterCount: 40,\n      blockCount: 2,\n      todoTotal: 3,\n      todoDone: 1,\n    });\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-builder.ts","s":1,"e":60,"m":1778165093745.1453,"t":"import { buildPageAggregate, type PageAggregateProjection } from \"@/lib/documents/page-aggregate\";\nimport { normalizeDocumentContentResponse } from \"@/lib/documents/page-subtree-response\";\nimport type { PageSubtreeProjection } from \"@/lib/documents/page-subtree\";\nimport type {\n  DocumentStats,\n  PageFont,\n  PageLayoutDensity,\n  PageOptionsState,\n} from \"@/types/page-options\";\n\nexport type DocumentMetaPayload = {\n  id: string;\n  workspace_id: string;\n  title: string | null;\n  updated_at: string | null;\n  can_edit?: boolean | null;\n  disable_download?: boolean | null;\n  disable_copy?: boolean | null;\n  wide_layout?: boolean | null;\n  use_small_text?: boolean | null;\n  show_heading_numbers?: boolean | null;\n  show_toc?: boolean | null;\n  show_structure?: boolean | null;\n  protect_editing?: boolean | null;\n  show_word_count?: boolean | null;\n  collapse_backlinks?: boolean | null;\n  page_font?: PageFont | null;\n  layout_density?: PageLayoutDensity | null;\n  hide_child_pages?: boolean | null;\n  show_block_ref_count?: boolean | null;\n  embed_default_block_id?: string | null;\n  word_count?: number | null;\n  character_count?: number | null;\n  block_count?: number | null;\n  todo_total?: number | null;\n  todo_total_count?: number | null;\n  todo_done?: number | null;\n  todo_done_count?: number | null;\n};\n\nexport type DocumentContentPayload = {\n  content?: unknown;\n  revision?: number | null;\n  conflict_detection_key?: string | null;\n  conflictDetectionKey?: string | null;\n  page_subtree?: PageSubtreeProjection | null;\n  pageSubtree?: PageSubtreeProjection | null;\n  title?: string | null;\n};\n\nexport function buildPageAggregateFromDocumentPayloads(input: {\n  meta: DocumentMetaPayload;\n  contentPayload?: DocumentContentPayload | null;\n}): PageAggregateProjection {\n  const normalizedContent = normalizeDocumentContentResponse({\n    documentId: input.meta.id,\n    title: input.meta.title ?? \"无标题\",\n    payload: input.contentPayload,\n  });","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-builder.ts","s":49,"e":101,"m":1778165093745.1453,"t":"};\n\nexport function buildPageAggregateFromDocumentPayloads(input: {\n  meta: DocumentMetaPayload;\n  contentPayload?: DocumentContentPayload | null;\n}): PageAggregateProjection {\n  const normalizedContent = normalizeDocumentContentResponse({\n    documentId: input.meta.id,\n    title: input.meta.title ?? \"无标题\",\n    payload: input.contentPayload,\n  });\n\n  const pageOptions: PageOptionsState = {\n    wideLayout: input.meta.wide_layout ?? false,\n    smallText: input.meta.use_small_text ?? false,\n    showHeadingNumbers: input.meta.show_heading_numbers ?? false,\n    showToc: input.meta.show_toc ?? false,\n    showStructure: input.meta.show_structure ?? false,\n    protectEditing: input.meta.protect_editing ?? false,\n    showWordCount: input.meta.show_word_count ?? true,\n    collapseBacklinks: input.meta.collapse_backlinks ?? false,\n    pageFont: input.meta.page_font ?? \"default\",\n    layoutDensity: input.meta.layout_density ?? \"normal\",\n    hideChildPages: input.meta.hide_child_pages ?? false,\n    showBlockRefCount: input.meta.show_block_ref_count ?? false,\n    embedDefaultBlockId: input.meta.embed_default_block_id ?? null,\n  };\n\n  const stats: DocumentStats = {\n    wordCount: input.meta.word_count ?? 0,\n    characterCount: input.meta.character_count ?? 0,\n    blockCount: input.meta.block_count ?? 0,\n    todoTotal: input.meta.todo_total ?? input.meta.todo_total_count ?? 0,\n    todoDone: input.meta.todo_done ?? input.meta.todo_done_count ?? 0,\n  };\n\n  return buildPageAggregate({\n    documentId: input.meta.id,\n    workspaceId: input.meta.workspace_id,\n    title: input.meta.title ?? \"无标题\",\n    updatedAt: input.meta.updated_at,\n    readOnly: input.meta.can_edit === false,\n    disableDownload: Boolean(input.meta.disable_download),\n    disableCopy: Boolean(input.meta.disable_copy),\n    pageOptions,\n    content: normalizedContent.content,\n    revision: normalizedContent.revision,\n    conflictDetectionKey: normalizedContent.conflictDetectionKey,\n    pageSubtree: normalizedContent.pageSubtree,\n    stats,\n  });\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-loader.ts","s":1,"e":60,"m":1778334631652.6506,"t":"import { headers } from \"next/headers\";\nimport { resolveMnoteWebInternalUrl } from \"@/lib/mnote-web/internal-url\";\nimport type { PageAggregateProjection } from \"@/lib/documents/page-aggregate\";\n\nconst FORWARDED_REQUEST_HEADERS = [\n  \"cookie\",\n  \"authorization\",\n  \"x-request-id\",\n  \"x-mnote-request-id\",\n  \"x-trace-id\",\n  \"x-mnote-trace-id\",\n  \"x-session-id\",\n  \"x-mnote-session-id\",\n  \"x-source-channel\",\n  \"x-mnote-source-channel\",\n  \"x-source-client\",\n  \"x-mnote-source-client\",\n  \"user-agent\",\n] as const;\n\nconst RUST_PAGE_AGGREGATE_TIMEOUT_MS = 3_000;\n\nexport type LoadedPageAggregate = {\n  page: PageAggregateProjection;\n  bridge: {\n    requestId: string;\n    traceId: string;\n    queryName: \"documents.page.get\";\n  };\n};\n\nfunction copyForwardHeaderIfPresent(target: Headers, source: Headers, name: string) {\n  const value = source.get(name);\n  if (value) {\n    target.set(name, value);\n  }\n}\n\nfunction readHeaderAlias(source: Headers, ...names: string[]) {\n  for (const name of names) {\n    const value = source.get(name);\n    if (value) return value;\n  }\n  return \"\";\n}\n\nfunction copyNormalizedHeader(target: Headers, source: Headers, targetName: string, ...sourceNames: string[]) {\n  const value = readHeaderAlias(source, ...sourceNames);\n  if (value) {\n    target.set(targetName, value);\n  }\n}\n\nfunction buildForwardHeaders(source: Headers) {\n  const requestHeaders = new Headers({\n    accept: \"application/json\",\n  });\n  copyForwardHeaderIfPresent(requestHeaders, source, \"cookie\");\n  copyForwardHeaderIfPresent(requestHeaders, source, \"authorization\");\n  copyNormalizedHeader(requestHeaders, source, \"x-request-id\", \"x-request-id\", \"x-mnote-request-id\");","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-loader.ts","s":49,"e":108,"m":1778334631652.6506,"t":"if (value) {\n    target.set(targetName, value);\n  }\n}\n\nfunction buildForwardHeaders(source: Headers) {\n  const requestHeaders = new Headers({\n    accept: \"application/json\",\n  });\n  copyForwardHeaderIfPresent(requestHeaders, source, \"cookie\");\n  copyForwardHeaderIfPresent(requestHeaders, source, \"authorization\");\n  copyNormalizedHeader(requestHeaders, source, \"x-request-id\", \"x-request-id\", \"x-mnote-request-id\");\n  copyNormalizedHeader(requestHeaders, source, \"x-mnote-request-id\", \"x-mnote-request-id\", \"x-request-id\");\n  copyNormalizedHeader(requestHeaders, source, \"x-trace-id\", \"x-trace-id\", \"x-mnote-trace-id\");\n  copyNormalizedHeader(requestHeaders, source, \"x-mnote-trace-id\", \"x-mnote-trace-id\", \"x-trace-id\");\n  copyNormalizedHeader(requestHeaders, source, \"x-session-id\", \"x-session-id\", \"x-mnote-session-id\");\n  copyNormalizedHeader(requestHeaders, source, \"x-mnote-session-id\", \"x-mnote-session-id\", \"x-session-id\");\n  copyNormalizedHeader(requestHeaders, source, \"x-source-channel\", \"x-source-channel\", \"x-mnote-source-channel\");\n  copyNormalizedHeader(requestHeaders, source, \"x-mnote-source-channel\", \"x-mnote-source-channel\", \"x-source-channel\");\n  copyNormalizedHeader(requestHeaders, source, \"x-source-client\", \"x-source-client\", \"x-mnote-source-client\");\n  copyNormalizedHeader(requestHeaders, source, \"x-mnote-source-client\", \"x-mnote-source-client\", \"x-source-client\");\n  copyForwardHeaderIfPresent(requestHeaders, source, \"user-agent\");\n  return requestHeaders;\n}\n\ntype RustPageAggregateSnapshotResponse = {\n  ok?: boolean;\n  schema?: string;\n  result?: unknown;\n  requestId?: string | null;\n  traceId?: string | null;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return Boolean(value) && typeof value === \"object\";\n}\n\nfunction isBoolean(value: unknown) {\n  return typeof value === \"boolean\";\n}\n\nfunction isNullableNumber(value: unknown) {\n  return value === null || typeof value === \"number\";\n}\n\nfunction isNullableString(value: unknown) {\n  return value === null || typeof value === \"string\";\n}\n\nfunction isTrustedMnoteWebInternalUrl(raw: string) {\n  try {\n    const url = new URL(raw);\n    const hostname = url.hostname.replace(/^\\[(.*)\\]$/, \"$1\").trim().toLowerCase();\n    return (\n      hostname === \"localhost\" ||\n      hostname === \"127.0.0.1\" ||\n      hostname === \"::1\" ||\n      hostname === \"0.0.0.0\" ||\n      hostname === \"::\"\n    );","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-loader.ts","s":97,"e":156,"m":1778334631652.6506,"t":"function isTrustedMnoteWebInternalUrl(raw: string) {\n  try {\n    const url = new URL(raw);\n    const hostname = url.hostname.replace(/^\\[(.*)\\]$/, \"$1\").trim().toLowerCase();\n    return (\n      hostname === \"localhost\" ||\n      hostname === \"127.0.0.1\" ||\n      hostname === \"::1\" ||\n      hostname === \"0.0.0.0\" ||\n      hostname === \"::\"\n    );\n  } catch {\n    return false;\n  }\n}\n\nfunction isRustPageAggregateProjection(\n  value: unknown,\n  input: { documentId: string; workspaceId?: string | null },\n): value is PageAggregateProjection {\n  if (!isRecord(value)) return false;\n  const record = value as Record<string, unknown>;\n  const identity = isRecord(record.identity) ? record.identity : null;\n  const head = isRecord(record.head) ? record.head : null;\n  const permissions = head && isRecord(head.permissions) ? head.permissions : null;\n  const layout = isRecord(record.layout) ? record.layout : null;\n  const pageOptions = layout && isRecord(layout.pageOptions) ? layout.pageOptions : null;\n  const body = isRecord(record.body) ? record.body : null;\n  const tree = isRecord(record.tree) ? record.tree : null;\n  const stats = isRecord(record.stats) ? record.stats : null;\n  const requestedWorkspaceId = input.workspaceId?.trim() || null;\n  return (\n    record.schema === \"mnote.page_aggregate.v1\" &&\n    typeof record.projectionVersion === \"number\" &&\n    typeof record.source === \"string\" &&\n    Boolean(identity) &&\n    identity.documentId === input.documentId &&\n    typeof identity.workspaceId === \"string\" &&\n    (!requestedWorkspaceId || identity.workspaceId === requestedWorkspaceId) &&\n    Boolean(head) &&\n    typeof head.title === \"string\" &&\n    isNullableString(head.updatedAt) &&\n    Boolean(permissions) &&\n    isBoolean(permissions.readOnly) &&\n    isBoolean(permissions.disableDownload) &&\n    isBoolean(permissions.disableCopy) &&\n    Boolean(layout) &&\n    Boolean(pageOptions) &&\n    Boolean(body) &&\n    \"content\" in body &&\n    isNullableNumber(body.revision) &&\n    isNullableString(body.conflictDetectionKey) &&\n    Boolean(tree) &&\n    Object.prototype.hasOwnProperty.call(tree, \"pageSubtree\") &&\n    Boolean(stats) &&\n    typeof stats.wordCount === \"number\" &&\n    typeof stats.characterCount === \"number\" &&\n    typeof stats.blockCount === \"number\" &&\n    typeof stats.todoTotal === \"number\" &&","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-loader.ts","s":145,"e":204,"m":1778334631652.6506,"t":"Boolean(pageOptions) &&\n    Boolean(body) &&\n    \"content\" in body &&\n    isNullableNumber(body.revision) &&\n    isNullableString(body.conflictDetectionKey) &&\n    Boolean(tree) &&\n    Object.prototype.hasOwnProperty.call(tree, \"pageSubtree\") &&\n    Boolean(stats) &&\n    typeof stats.wordCount === \"number\" &&\n    typeof stats.characterCount === \"number\" &&\n    typeof stats.blockCount === \"number\" &&\n    typeof stats.todoTotal === \"number\" &&\n    typeof stats.todoDone === \"number\"\n  );\n}\n\nexport async function loadPageAggregateFromRustSnapshot(input: {\n  request: Request;\n  documentId: string;\n  workspaceId?: string | null;\n}): Promise<LoadedPageAggregate | null> {\n  const internalBaseUrl = await resolveMnoteWebInternalUrl();\n  if (!isTrustedMnoteWebInternalUrl(internalBaseUrl)) {\n    return null;\n  }\n  const url = new URL(`/api/page-aggregate/${encodeURIComponent(input.documentId)}`, `${internalBaseUrl}/`);\n  const workspaceId = input.workspaceId?.trim();\n  if (workspaceId) {\n    url.searchParams.set(\"workspaceId\", workspaceId);\n  }\n\n  let response: Response;\n  try {\n    response = await fetch(url.toString(), {\n      method: \"GET\",\n      headers: buildForwardHeaders(input.request.headers),\n      cache: \"no-store\",\n      signal: AbortSignal.timeout(RUST_PAGE_AGGREGATE_TIMEOUT_MS),\n    });\n  } catch {\n    return null;\n  }\n\n  if (!response.ok) {\n    return null;\n  }\n\n  const payload = await response.json().catch(() => null) as RustPageAggregateSnapshotResponse | null;\n  const page = payload?.result;\n  if (\n    payload?.schema !== \"mnote.page_aggregate.v1\" ||\n    !isRustPageAggregateProjection(page, input)\n  ) {\n    return null;\n  }\n\n  return {\n    page,\n    bridge: {\n      requestId: typeof payload.requestId === \"string\" && payload.requestId.trim() ? payload.requestId : \"unknown\",","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate-loader.ts","s":193,"e":244,"m":1778334631652.6506,"t":"const page = payload?.result;\n  if (\n    payload?.schema !== \"mnote.page_aggregate.v1\" ||\n    !isRustPageAggregateProjection(page, input)\n  ) {\n    return null;\n  }\n\n  return {\n    page,\n    bridge: {\n      requestId: typeof payload.requestId === \"string\" && payload.requestId.trim() ? payload.requestId : \"unknown\",\n      traceId: typeof payload.traceId === \"string\" && payload.traceId.trim() ? payload.traceId : \"unknown\",\n      queryName: \"documents.page.get\",\n    },\n  };\n}\n\nasync function buildServerBridgeRequest(pathname: string): Promise<Request> {\n  const headerList = await headers();\n  const requestHeaders = new Headers();\n  FORWARDED_REQUEST_HEADERS.forEach((name) => {\n    copyForwardHeaderIfPresent(requestHeaders, headerList, name);\n  });\n  const internalBaseUrl = await resolveMnoteWebInternalUrl();\n\n  return new Request(new URL(pathname, `${internalBaseUrl}/`).toString(), {\n    method: \"GET\",\n    headers: requestHeaders,\n  });\n}\n\nexport async function loadPageAggregate(input: {\n  request: Request;\n  documentId: string;\n  workspaceId?: string | null;\n}): Promise<LoadedPageAggregate | null> {\n  return loadPageAggregateFromRustSnapshot(input);\n}\n\nexport async function loadPageAggregateFromNextHeaders(input: {\n  documentId: string;\n  workspaceId?: string | null;\n}): Promise<LoadedPageAggregate | null> {\n  const request = await buildServerBridgeRequest(\"/documents/page\");\n  return loadPageAggregate({\n    request,\n    documentId: input.documentId,\n    workspaceId: input.workspaceId,\n  });\n}","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate.test.ts","s":1,"e":60,"m":1776767538812.7153,"t":"import { describe, expect, it } from \"vitest\";\nimport { buildPageAggregate } from \"@/lib/documents/page-aggregate\";\nimport type { PageOptionsState } from \"@/types/page-options\";\n\nconst pageOptions: PageOptionsState = {\n  wideLayout: true,\n  smallText: false,\n  showHeadingNumbers: true,\n  showToc: true,\n  showStructure: false,\n  protectEditing: false,\n  showWordCount: true,\n  collapseBacklinks: false,\n  pageFont: \"default\",\n  layoutDensity: \"normal\",\n  hideChildPages: false,\n  showBlockRefCount: false,\n  embedDefaultBlockId: null,\n};\n\ndescribe(\"page-aggregate\", () => {\n  it(\"将页面 route 所需真相收口为统一聚合对象\", () => {\n    const aggregate = buildPageAggregate({\n      documentId: \"doc_1\",\n      workspaceId: \"ws_1\",\n      title: \" 页面标题 \",\n      updatedAt: \"2026-04-21T12:00:00.000Z\",\n      readOnly: true,\n      disableDownload: true,\n      disableCopy: false,\n      pageOptions,\n      content: [{ id: \"block_1\", type: \"paragraph\", content: [] }],\n      revision: 7,\n      conflictDetectionKey: \"doc_1:7\",\n      pageSubtree: null,\n      stats: {\n        wordCount: 10,\n        characterCount: 20,\n        blockCount: 1,\n        todoTotal: 0,\n        todoDone: 0,\n      },\n    });\n\n    expect(aggregate).toEqual({\n      identity: {\n        documentId: \"doc_1\",\n        workspaceId: \"ws_1\",\n      },\n      head: {\n        title: \"页面标题\",\n        updatedAt: \"2026-04-21T12:00:00.000Z\",\n        permissions: {\n          readOnly: true,\n          disableDownload: true,\n          disableCopy: false,\n        },\n      },\n      layout: {\n        pageOptions,","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate.test.ts","s":49,"e":103,"m":1776767538812.7153,"t":"},\n      head: {\n        title: \"页面标题\",\n        updatedAt: \"2026-04-21T12:00:00.000Z\",\n        permissions: {\n          readOnly: true,\n          disableDownload: true,\n          disableCopy: false,\n        },\n      },\n      layout: {\n        pageOptions,\n      },\n      body: {\n        content: [{ id: \"block_1\", type: \"paragraph\", content: [] }],\n        revision: 7,\n        conflictDetectionKey: \"doc_1:7\",\n      },\n      tree: {\n        pageSubtree: null,\n      },\n      stats: {\n        wordCount: 10,\n        characterCount: 20,\n        blockCount: 1,\n        todoTotal: 0,\n        todoDone: 0,\n      },\n    });\n  });\n\n  it(\"缺省标题与可选字段时回退到稳定默认值\", () => {\n    const aggregate = buildPageAggregate({\n      documentId: \"doc_2\",\n      workspaceId: \"ws_2\",\n      pageOptions,\n      content: null,\n    });\n\n    expect(aggregate.head.title).toBe(\"无标题\");\n    expect(aggregate.head.permissions).toEqual({\n      readOnly: false,\n      disableDownload: false,\n      disableCopy: false,\n    });\n    expect(aggregate.body).toEqual({\n      content: null,\n      revision: null,\n      conflictDetectionKey: null,\n    });\n    expect(aggregate.tree.pageSubtree).toBeNull();\n    expect(aggregate.stats).toBeNull();\n  });\n});","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate.ts","s":1,"e":60,"m":1778301273994.0415,"t":"import type { PageSubtreeProjection } from \"@/lib/documents/page-subtree\";\nimport type { DocumentStats, PageOptionsState } from \"@/types/page-options\";\n\nexport interface PageAggregateIdentity {\n  documentId: string;\n  workspaceId: string;\n}\n\nexport interface PageAggregatePermissions {\n  readOnly: boolean;\n  disableDownload: boolean;\n  disableCopy: boolean;\n}\n\nexport interface PageAggregateHead {\n  title: string;\n  updatedAt: string | null;\n  permissions: PageAggregatePermissions;\n}\n\nexport interface PageAggregateLayout {\n  pageOptions: PageOptionsState;\n}\n\nexport interface PageAggregateBody {\n  content: unknown;\n  revision: number | null;\n  conflictDetectionKey: string | null;\n}\n\nexport interface PageAggregateTree {\n  pageSubtree: PageSubtreeProjection | null;\n}\n\nexport interface PageAggregateProjection {\n  schema?: \"mnote.page_aggregate.v1\";\n  projectionVersion?: number;\n  source?: \"KernelProjection\" | \"CompatMetaContentJoin\" | \"Fixture\" | string;\n  identity: PageAggregateIdentity;\n  head: PageAggregateHead;\n  layout: PageAggregateLayout;\n  body: PageAggregateBody;\n  tree: PageAggregateTree;\n  stats: DocumentStats | null;\n}\n\nexport function buildPageAggregate(input: {\n  documentId: string;\n  workspaceId: string;\n  title?: string | null;\n  updatedAt?: string | null;\n  readOnly?: boolean;\n  disableDownload?: boolean;\n  disableCopy?: boolean;\n  pageOptions: PageOptionsState;\n  content: unknown;\n  revision?: number | null;\n  conflictDetectionKey?: string | null;\n  pageSubtree?: PageSubtreeProjection | null;\n  stats?: DocumentStats | null;","v":"[encoded data, 4096 characters]"}
{"p":"wolai-frontend/src/lib/documents/page-aggregate.ts","s":49,"e":90,"m":1778301273994.0415,"t":"workspaceId: string;\n  title?: string | null;\n  updatedAt?: string | null;\n  readOnly?: boolean;\n  disableDownload?: boolean;\n  disableCopy?: boolean;\n  pageOptions: PageOptionsState;\n  content: unknown;\n  revision?: number | null;\n  conflictDetectionKey?: string | null;\n  pageSubtree?: PageSubtreeProjection | null;\n  stats?: DocumentStats | null;\n}): PageAggregateProjection {\n  return {\n    identity: {\n      documentId: input.documentId,\n      workspaceId: input.workspaceId,\n    },\n    head: {\n      title: input.title?.trim() || \"无标题\",\n      updatedAt: input.updatedAt ?? null,\n      permissions: {\n        readOnly: Boolean(input.readOnly),\n        disableDownload: Boolean(input.disableDownload),\n        disableCopy: Boolean(input.disableCopy),\n      },\n    },\n    layout: {\n      pageOptions: input.pageOptions,\n    },\n    body: {\n      content: input.content ?? null,\n      revision: input.revision ?? null,\n      conflictDetectionKey: input.conflictDetectionKey ?? null,\n    },\n    tree: {\n      pageSubtree: input.pageSubtree ?? null,\n    },\n    stats: input.stats ?? null,\n  };\n}","v":"[encoded data, 4096 characters]"}
