From 3311bd036658d7d33426f777cbf630a14e9a5014 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Sun, 17 May 2026 23:09:56 +0800 Subject: [PATCH] chore: document architecture gaps and add dev hot reload - add npm dev:hot wrapper using cargo-watch and page reload polling - add mnote-web dev hot reload endpoint and coverage - record current architecture review and tracked bug findings across realtime, tree, editor, and AI runtimes Verification: - node scripts/task-dev-hot-plan-test.js - node --check scripts/dev-hot.js - cargo test -p mnote-web dev_hot -- --nocapture --- CURRENT_ARCHITECTURE.md | 161 ++++++++++++++++++ ...e-realtime-ws-sse-doc-contract-drift-v1.md | 34 ++++ ...3-17-sse-push-skips-polling-fallback-v1.md | 32 ++++ .../3-18-ws-sse-delta-contract-split-v1.md | 33 ++++ ...ix-tool-call-missing-identity-fields-v1.md | 31 ++++ ...0-acp-request-permission-no-response-v1.md | 31 ++++ ...acp-run-payload-consumed-and-removed-v1.md | 32 ++++ ...e-resource-row-document-command-leak-v1.md | 37 ++++ ...-aggregate-client-state-second-truth-v1.md | 33 ++++ ...snapshot-query-overrides-live-stream-v1.md | 32 ++++ ...-source-drift-live-sidebar-over-head-v1.md | 32 ++++ ...does-not-sync-page-aggregate-content-v1.md | 33 ++++ ...wn-edit-normalized-search-byte-index-v1.md | 61 +++++++ ...n-edit-same-block-multi-op-overwrite-v1.md | 75 ++++++++ ...down-edit-phase-state-contract-drift-v1.md | 35 ++++ ...uidance-markdown-edit-contract-drift-v1.md | 35 ++++ ...rkflow-bypasses-hermes-tool-executor-v1.md | 35 ++++ ...own-edit-local-write-contract-bypass-v1.md | 33 ++++ ...lock-ops-missing-write-preconditions-v1.md | 35 ++++ ...-edit-manifest-schema-contract-drift-v1.md | 34 ++++ ...ne-write-does-not-use-final-markdown-v1.md | 35 ++++ design/10-review/README.md | 2 + .../10-current-mnote-ai-runtime-review-v1.md | 88 ++++++++++ .../11-current-full-architecture-review-v1.md | 34 ++++ package.json | 1 + rust/crates/mnote-web/src/routes/dev_hot.rs | 51 ++++++ rust/crates/mnote-web/src/routes/mod.rs | 16 ++ rust/crates/mnote-web/src/ssr/pages/layout.rs | 38 +++++ rust/target/.rustc_info.json | 2 +- scripts/dev-hot.js | 72 ++++++++ scripts/task-dev-hot-plan-test.js | 18 ++ 31 files changed, 1220 insertions(+), 1 deletion(-) create mode 100644 CURRENT_ARCHITECTURE.md create mode 100644 bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md create mode 100644 bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md create mode 100644 bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md create mode 100644 bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md create mode 100644 bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md create mode 100644 bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md create mode 100644 bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md create mode 100644 bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md create mode 100644 bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md create mode 100644 bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md create mode 100644 bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md create mode 100644 bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md create mode 100644 bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md create mode 100644 bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md create mode 100644 bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md create mode 100644 bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md create mode 100644 bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md create mode 100644 bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md create mode 100644 bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md create mode 100644 bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md create mode 100644 design/10-review/process/10-current-mnote-ai-runtime-review-v1.md create mode 100644 design/10-review/process/11-current-full-architecture-review-v1.md create mode 100644 rust/crates/mnote-web/src/routes/dev_hot.rs create mode 100644 scripts/dev-hot.js create mode 100644 scripts/task-dev-hot-plan-test.js diff --git a/CURRENT_ARCHITECTURE.md b/CURRENT_ARCHITECTURE.md new file mode 100644 index 00000000..d4040bfc --- /dev/null +++ b/CURRENT_ARCHITECTURE.md @@ -0,0 +1,161 @@ +# 当前完整架构 + +> 更新时间:2026-05-17 +> +> 范围:`/mnt/Data1T/mnote` 当前可见实现的完整架构、冲突口径、过渡态和缺失功能。 + +## 1. 结论 + +当前系统已经形成一条清晰的主线: + +`Convex / 本地文件` -> `Rust kernel / bridge-runtime` -> `mnote-web` -> `前端壳与编辑器` -> `AI runtime` + +但它还不是单一真源闭环。现在同时存在三类并行真相: + +1. `tree` / `page` / `resource` 的 Rust 语义真相。 +2. 文档页与 Sidebar 的前端本地派生真相。 +3. AI 写入链路中的 markdown / block 双合同真相。 + +因此,项目当前更像“主线已经立住,但收口尚未完成”的状态,而不是“架构已统一完成”的状态。 + +## 2. 分层架构 + +### 2.1 事实存储层 + +- `Convex` 仍是在线协作、文档、媒体、树数据的实际后端存储底座。 +- 本地 `.md` 文件是 `mnote.doc.fetch` / `mnote.doc.markdown_edit` 的另一条合法输入输出面。 +- 本地文件路径与在线文档路径在工具层已经分叉,不能再假设只有一种存储后端。 + +### 2.2 Kernel / Projection 层 + +- `rust/crates/core-protocol/src/kernel.rs` 定义 `KernelProjectionKind`、`KernelProjectionResourceKind`、`KernelObjectIdentity` 等协议语义。 +- `rust/crates/bridge-runtime/src/lib.rs` 负责从 Convex 侧数据归一化出 kernel nodes / edges / projections,并生成 command plan。 +- 语义主导权已经明显从前端迁到 Rust,但前端仍保留若干本地派生投影。 + +### 2.3 Tree / Command 层 + +- 正式命令面应落在 `tree.*` 与 `tree.resource.*`。 +- `mnote-web` 同时暴露 `tree` 命令路由与兼容/过渡路由。 +- 当前最大冲突是 FileTree 资源行仍可能被当成页面命令对象处理,违反 Resource Tree / File Tree / Page Tree 分层。 + +### 2.4 Transport / Realtime 层 + +- Rust Web 已同时注册 `/api/tree/events` 与 `/api/realtime/ws`。 +- 但当前前端真实消费链路仍以 `EventSource('/api/tree/events')` 为主,未见主链 WS consumer。 +- SSE 内部还保留 push/polling 两种语义,WS 与 SSE 的 delta / snapshot 结构也未完全收敛。 + +### 2.5 Page Aggregate 层 + +- 文档页入口已经优先消费 Rust `page-aggregate` 快照。 +- 但前端仍保留 `PageAggregateClientState` reducer 和本地 `page tree` 投影。 +- 当前 page aggregate 是“Rust join + 前端局部状态”的过渡闭环,不是 EditorBlockDocument 原生单一真源闭环。 + +### 2.6 Editor Runtime 层 + +- `leptos-tiptap` island 已是文档页默认编辑 host。 +- 保存正文仍会经过 `documents/save` 兼容面。 +- 这意味着编辑器体验已经切主,但写回语义还没有完全切到唯一主命令面。 + +### 2.7 AI Runtime 层 + +- `mnote.doc.fetch`、`mnote.doc.markdown_edit`、`mnote.doc.apply_block_ops`、`page_ai_workflow`、Hermes / ACP / Reasonix 构成当前 AI 主链。 +- 这里存在最密集的合同漂移:工具权限、写入幂等、dryRun、revision 校验、manifest schema、tool guidance、runtime owner 都还没有统一。 + +## 3. 当前成立的事实与过渡态 + +### 3.1 已成立事实 + +- Rust 协议层已经成为语义主线,不再主要依赖前端拼装。 +- Tree 主链已经从旧兼容入口退向 Rust Web。 +- 文档页主编辑器已经切到 `leptos-tiptap` island。 +- AI 页面编辑已经不再是纯前端本地逻辑。 + +### 3.2 过渡态 + +- Page Aggregate 仍从 `documents.content` 侧 join 构造,而不是原生 EditorBlockDocument 真源。 +- 前端 Sidebar 仍有本地投影与多源抢真相。 +- `mnote.doc.markdown_edit` 已进入主线,但写入合同尚不完整。 +- ACP / Reasonix 已接入,但调用字段和运行时所有权仍不统一。 + +## 4. 架构冲突矩阵 + +### 4.1 Tree realtime + +- 事实:Rust Web 已注册 WS。 +- 事实:前端仍只见 SSE consumer。 +- 冲突:系统口径已升级,但前端主链未同步切换。 +- 影响:live cache、调试、回放与验收都会出现双口径。 + +### 4.2 FileTree 资源行 + +- 事实:资源投影行带 `documentId`。 +- 事实:`tree.tsx` 相关路径会把它当作页面对象处理。 +- 冲突:资源树与页面树边界被突破。 +- 影响:误删、误重命名、误移动风险上升。 + +### 4.3 Page Aggregate + +- 事实:Rust snapshot 已是入口事实。 +- 事实:前端 reducer 与本地 page tree 还在派生第二份状态。 +- 冲突:页面标题、正文、树投影可能各自取源。 +- 影响:AI context、Sidebar、正文显示不一定同源。 + +### 4.4 AI 写入 + +- 事实:`markdown_edit` 已是主推路径。 +- 事实:Hermes guidance、manifest、page_ai_workflow、apply_block_ops 仍在不同层保留旧习惯。 +- 冲突:同一条编辑链路里存在 markdown / block / tool executor 三套合同。 +- 影响:写入正确性、幂等性、回读一致性都不稳定。 + +## 5. 缺失功能 + +- 前端树流仍缺统一的 WS 主 consumer。 +- Page Aggregate 仍缺前端第二真相收口。 +- FileTree 资源行仍缺严格的命令面隔离。 +- `mnote.doc.markdown_edit` 仍缺完整写入合同。 +- ACP / Reasonix 仍缺统一身份、会话、运行、幂等字段。 +- 兼容路由仍偏多,正式命令面与过渡面边界还不够硬。 + +## 6. 推荐收口顺序 + +1. 先收 `tree` 实时链路口径,让前端主消费与后端主发布一致。 +2. 再收 FileTree 资源行命令边界,避免资源对象误走页面命令。 +3. 再收 Page Aggregate 单一真源,减少 Sidebar / 文档页 / AI context 多源抢真相。 +4. 最后收 AI 写入合同,把 `markdown_edit`、`apply_block_ops`、Hermes / ACP / Reasonix 的调用协议统一起来。 + +## 7. 相关审查与缺陷 + +- [设计审查:当前 mnote 项目 AI / Page Aggregate 定向 Review](./design/10-review/process/10-current-mnote-ai-runtime-review-v1.md) +- [设计审查:当前完整架构 Review](./design/10-review/process/11-current-full-architecture-review-v1.md) + +## 8. 本次落档缺陷索引 + +### 8.1 Rust Web / Realtime / ACP + +- [3-16 tree realtime WS 主链口径与前端 SSE 实现不一致](./bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md) +- [3-17 SSE push 模式跳过 polling safety net](./bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md) +- [3-18 WS 与 SSE delta 载荷合同分裂](./bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md) +- [3-19 Reasonix ACP wrapper 调 mnote tool 缺少身份与幂等字段](./bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md) +- [3-20 ACP incoming request 只记录日志不响应](./bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md) +- [3-21 ACP run payload 被第一次 stream_events 消费后移除](./bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md) + +### 8.2 Tree Domain + +- [4-46 FileTree 资源行被当成页面命令对象处理](./bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md) + +### 8.3 Editor Mainline + +- [5-15 PageAggregateClientState 仍在前端生成第二份 page tree 真相](./bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md) +- [5-16 Sidebar preferred snapshot 中 query 可覆盖 live stream](./bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md) +- [5-17 文档页标题优先 liveSidebarTitle 而非 Page Aggregate head](./bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md) +- [5-18 AI 写正文后本地 Page Aggregate content 可能不刷新](./bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md) + +### 8.4 AI + +- [7-18 AI markdown_edit 阶段状态合同漂移](./bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md) +- [7-19 Hermes 工具指导仍优先 apply_block_ops 而非 markdown_edit](./bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md) +- [7-20 page_ai_workflow 绕过 Hermes tool executor / audit / toggle](./bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md) +- [7-21 mnote.doc.markdown_edit 本地文件写入绕过 dryRun / idempotency](./bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md) +- [7-22 mnote.doc.apply_block_ops 批量写入缺少 revision / conflictDetectionKey 校验](./bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md) +- [7-23 mnote.doc.markdown_edit manifest schema 缺少 required 与写入字段](./bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md) +- [7-24 在线 markdown_edit 写回不以最终 Markdown 为真源](./bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md) diff --git a/bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md b/bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md new file mode 100644 index 00000000..4d53d641 --- /dev/null +++ b/bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md @@ -0,0 +1,34 @@ +# 3-16 [process][bug] tree realtime WS 主链口径与前端 SSE 实现不一致 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +当前架构口径已描述 `/api/realtime/ws` 是树实时 push 主链,`/api/tree/events` 是 SSE fallback。但前端真实代码仍只发现 `EventSource('/api/tree/events')` 消费链路,没有发现 `/api/realtime/ws` 或 `new WebSocket` 主 consumer。 + +## 2. 证据 + +- [routes/mod.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/mod.rs:170) 注册 `/api/tree/events`。 +- [routes/mod.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/mod.rs:172) 注册 `/api/realtime/ws`。 +- [use-sidebar-tree-stream.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts:137) 实际创建 `EventSource`。 +- [protocol.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/protocol.ts:72) 固定构造 `/api/tree/events`。 + +## 3. 影响 + +- 架构文档与前端运行路径不一致。 +- live cache 收口时无法判断真实主链。 +- 回归测试如果只检查后端 WS route 存在,会误判前端已切换完成。 + +## 4. 建议修复 + +- 明确当前阶段:要么更新架构口径为“前端仍 SSE,WS route 已准备”,要么实现前端 WS consumer。 +- 增加浏览器 smoke,断言网络中实际使用的是 WS 主链还是 SSE fallback。 +- 文档与测试必须以真实前端消费路径为准。 + +## 5. 状态 + +已确认口径与前端源码不一致,尚未修复。 diff --git a/bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md b/bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md new file mode 100644 index 00000000..e62b626c --- /dev/null +++ b/bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md @@ -0,0 +1,32 @@ +# 3-17 [process][bug] SSE push 模式跳过 polling safety net v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +SSE route 注释和架构口径暗示 polling 可作为 safety net,但实现中只要 `stream_delta_rx.is_some()`,循环就进入 push/heartbeat 分支并跳过 polling。 + +## 2. 证据 + +- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:35) 附近说明 SSE 事件路径。 +- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:116) 附近在 `stream_delta_rx` 存在时走 heartbeat / push 分支。 + +## 3. 影响 + +- broadcast 丢失、进程重启、跨进程实例不共享 channel 时,SSE 连接可能无法靠 Convex bridgeLogs 追上。 +- fallback 名义存在,但实际恢复能力不足。 +- 客户端可能长期停在旧 snapshot。 + +## 4. 建议修复 + +- push 模式也应按 cursor 周期性检查 bridgeLogs,或在 heartbeat 周期中合并轻量 polling。 +- 增加测试:模拟 `stream_delta_rx` 无消息但 bridgeLogs 有新事件,SSE 应产出 delta/resync。 +- 明确 `pollMs` 与 push channel 同时存在时的优先级。 + +## 5. 状态 + +已确认 push 分支跳过 polling fallback,尚未修复。 diff --git a/bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md b/bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md new file mode 100644 index 00000000..add7d23a --- /dev/null +++ b/bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md @@ -0,0 +1,33 @@ +# 3-18 [process][bug] WS 与 SSE delta 载荷合同分裂 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +SSE polling 可以从 bridgeLogs / domainEvents 解析 `streamDelta` 并附带 snapshot / resync 信息;WS 当前广播的是 `command_committed` 外壳,两者不是同一套 delta 合同。 + +## 2. 证据 + +- [stream_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/stream_support.rs:577) 附近解析 `streamDelta`。 +- [command_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/command_support.rs:102) 附近广播 command committed 事件。 +- [ws.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/ws.rs:40) 处理 WS socket。 + +## 3. 影响 + +- 同一 tree command 在 WS 与 SSE 下可能给客户端不同结构。 +- 前端 live cache 需要维护两套 reducer,增加状态分裂风险。 +- WS 切主时,SSE 已覆盖的 snapshot/resync 语义可能丢失。 + +## 4. 建议修复 + +- 抽出统一的 tree realtime envelope,WS 与 SSE 只作为 transport。 +- 对同一命令分别跑 WS / SSE 合同测试,断言事件 kind、cursor、snapshot、resync 语义一致。 +- 前端 reducer 只消费统一 envelope,不感知 transport 差异。 + +## 5. 状态 + +已确认 WS / SSE 事件结构不统一,尚未修复。 diff --git a/bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md b/bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md new file mode 100644 index 00000000..fdf9db3d --- /dev/null +++ b/bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md @@ -0,0 +1,31 @@ +# 3-19 [process][bug] Reasonix ACP wrapper 调 mnote tool 缺少身份与幂等字段 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +Reasonix ACP wrapper 调用 mnote tool 时只发送 `toolName`、`args`、`workspaceId`,没有传递 `actorId`、`sessionId`、`runId`、`toolCallId`、`traceId`、`dryRun`、`idempotencyKey` 等工具合同字段。 + +## 2. 证据 + +- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:230) 附近构造 `callMnoteTool` 请求体。 + +## 3. 影响 + +- Rust tool executor 难以稳定进行鉴权、审计、追踪和幂等。 +- 写工具可能被拒绝,或在缺少幂等键时产生重复写入风险。 +- Reasonix 与 Hermes 对同一 tool 的调用合同不一致。 + +## 4. 建议修复 + +- wrapper 必须从 ACP run/session 中透传 actor、session、run、toolCall、trace、dryRun、idempotency 字段。 +- 写工具缺失必要字段时应 fail fast。 +- 增加 Reasonix 端到端 smoke,断言请求字段完整。 + +## 5. 状态 + +已确认 wrapper 请求字段不完整,尚未修复。 diff --git a/bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md b/bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md new file mode 100644 index 00000000..3c7de742 --- /dev/null +++ b/bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md @@ -0,0 +1,31 @@ +# 3-20 [process][bug] ACP incoming request 只记录日志不响应 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +ACP client 收到 `session/request_permission` 等 incoming request 时当前只记录日志,没有返回协议响应。 + +## 2. 证据 + +- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:347) 附近处理 incoming request。 + +## 3. 影响 + +- ACP agent 侧可能等待到超时。 +- 权限请求无法进入用户确认或自动拒绝流程。 +- run 状态可能卡在进行中,SSE 输出也无法表达真实阻塞原因。 + +## 4. 建议修复 + +- 为已知 request type 实现明确响应:允许、拒绝或要求人工确认。 +- 未支持的 request type 也应返回结构化 error,而不是只日志。 +- 增加 ACP request/response 协议测试。 + +## 5. 状态 + +已确认 incoming request 缺少响应处理,尚未修复。 diff --git a/bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md b/bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md new file mode 100644 index 00000000..129b7db5 --- /dev/null +++ b/bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md @@ -0,0 +1,32 @@ +# 3-21 [process][bug] ACP run payload 被第一次 stream_events 消费后移除 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +ACP run 的 payload 在 `stream_events` 路径被消费后从内存表移除。刷新页面、多个 SSE 订阅、断线重连时,后续订阅可能无法再取到同一个 run 的 payload。 + +## 2. 证据 + +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:789) 进入 `stream_events`。 +- ACP 分支中存在 `ACP_RUN_PAYLOADS.remove` 语义,payload 生命周期与第一次 stream 订阅绑定。 + +## 3. 影响 + +- 浏览器刷新或 SSE 重连可能导致同一 run 无法恢复。 +- 多个客户端观察同一 run 时只有第一个订阅者成功。 +- abort / retry / reconnect 语义不可靠。 + +## 4. 建议修复 + +- payload 应有 run 级持久生命周期,至少保留到 run completed / failed / aborted 后短 TTL。 +- stream subscription 不应拥有 payload 的删除权。 +- 增加 SSE 断线重连与重复订阅测试。 + +## 5. 状态 + +已确认 payload 生命周期与 stream 订阅耦合,尚未修复。 diff --git a/bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md b/bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md new file mode 100644 index 00000000..617af938 --- /dev/null +++ b/bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md @@ -0,0 +1,37 @@ +# 4-46 [process][bug] FileTree 资源行被当成页面命令对象处理 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`04-tree-domain` + +## 1. 问题定义 + +FileTree asset row 的 `resourceMeta` 同时带 `documentId` 和 `assetId`,但 tree shell 的部分交互会优先读取 `documentId`,从而把资源行当成页面行执行删除、复制、移动、重命名等页面命令。 + +这直接违反 Resource Tree / File Tree / Page Tree 的分层边界。 + +## 2. 证据 + +- [bridge-runtime/src/lib.rs](/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs:8499) 附近构造 asset row resource meta。 +- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:2019) `getFileTreeRowDocumentId()` 优先返回资源行中的 `documentId`。 +- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:2881) 键盘删除路径使用 documentId。 +- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:2962) 剪贴板路径使用 documentId。 +- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:4630) F2 重命名路径使用 documentId。 + +## 3. 影响 + +- 选中 mindmap / table / media asset 行后,可能误删或误重命名所属页面。 +- 资源归属与页面结构命令混淆。 +- `tree.resource.*` 的正式命令面无法成为资源操作唯一入口。 + +## 4. 建议修复 + +- FileTree row model 必须区分 `document row` 与 `resource row`,不要对 resource row 暴露页面 document command。 +- Delete / F2 / copy / move 对资源行应走 `tree.resource.*`,或在未实现时显式禁用。 +- 增加浏览器 smoke:资源行上 Delete、F2、Ctrl+C/X/V 不得触发页面命令。 + +## 5. 状态 + +已确认 row model 与命令分发存在泄漏,尚未修复。 diff --git a/bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md b/bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md new file mode 100644 index 00000000..71f4a3b8 --- /dev/null +++ b/bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md @@ -0,0 +1,33 @@ +# 5-15 [process][bug] PageAggregateClientState 仍在前端生成第二份 page tree 真相 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`05-editor-mainline` + +## 1. 问题定义 + +文档页已经从 Rust `page-aggregate` 读取快照,但前端 `PageAggregateClientState` reducer 在标题或正文变化后仍会本地构建 page subtree projection,并把它作为 AI context 等链路的输入。 + +## 2. 证据 + +- [document-content.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx:144) 初始化 page aggregate client reducer。 +- [page-aggregate-client-state.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/page-aggregate-client-state.ts:179) 到 [page-aggregate-client-state.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/page-aggregate-client-state.ts:197) 本地构建 subtree projection。 +- [page-subtree.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-subtree.ts:246) 仍保留 TS 本地 page tree builder。 + +## 3. 影响 + +- 文档页、AI context、Rust Page Aggregate 可能看到不同 page tree。 +- 前端本地派生逻辑会继续承载长期语义,违反 Page Aggregate 单一真源收口。 +- 后续 Debug 时很难判断页面结构到底来自 Rust 还是 TS builder。 + +## 4. 建议修复 + +- 明确 reducer 只能作为 UI 草稿态,不得生成长期 page tree projection。 +- AI context 应优先读取 Rust Page Aggregate 的稳定 projection。 +- 增加测试:本地编辑后 AI context 与 Rust 回读 projection 必须一致。 + +## 5. 状态 + +已确认前端仍生成第二份 page tree,尚未收口。 diff --git a/bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md b/bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md new file mode 100644 index 00000000..04085c30 --- /dev/null +++ b/bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md @@ -0,0 +1,32 @@ +# 5-16 [process][bug] Sidebar preferred snapshot 中 query 可覆盖 live stream v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`05-editor-mainline` + +## 1. 问题定义 + +Sidebar 同时启动 query/live/initial 与 tree stream,并通过 preferred snapshot 选择最终数据源。当 `query` 与 `tree_stream` 版本相同但内容不同,当前 freshness / syncKey 逻辑可能让 query 压过 live stream。 + +## 2. 证据 + +- [app-layout-shell.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx:20) 同时使用 Sidebar data 与 tree stream。 +- [use-preferred-sidebar-snapshot.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts:32) 到 [use-preferred-sidebar-snapshot.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts:52) 进行 source 选择。 + +## 3. 影响 + +- Sidebar 仍存在 Convex live / HTTP / Rust stream 多链抢真相。 +- 同一版本但不同内容时,用户可能看到旧树或非 Rust stream 结果。 +- WS / SSE 收口后仍可能被 query source 覆盖。 + +## 4. 建议修复 + +- 明确唯一预期来源:当 Rust stream 可用时,应由 stream 成为主真相,query 仅初始化或 fallback。 +- 同版本不同内容时必须记录诊断事件,而不是静默按 freshness 选择。 +- 增加单测覆盖同版本冲突。 + +## 5. 状态 + +已确认多源选择策略仍可能覆盖 live stream,尚未修复。 diff --git a/bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md b/bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md new file mode 100644 index 00000000..d97c3c1a --- /dev/null +++ b/bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md @@ -0,0 +1,32 @@ +# 5-17 [process][bug] 文档页标题优先 liveSidebarTitle 而非 Page Aggregate head v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`05-editor-mainline` + +## 1. 问题定义 + +文档页标题显示仍可能优先读取 `liveSidebarTitle`,而不是 Page Aggregate `head.title`。当 Sidebar 多源延迟或抢源时,文档页标题可能被 Sidebar snapshot 覆盖。 + +## 2. 证据 + +- [document-content.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx:159) 使用 live sidebar title 参与标题状态。 +- [page-aggregate-client-state.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/page-aggregate-client-state.ts:144) 标题逻辑仍接受外部 live title。 + +## 3. 影响 + +- 文档页标题、Sidebar 标题、Page Aggregate head 可能短暂或长期不一致。 +- 标题保存后回读来源不唯一,影响面包屑、AI context、页面 chrome。 +- Page Aggregate 单一真源收口被 Sidebar 侧状态反向覆盖。 + +## 4. 建议修复 + +- 文档页自身标题应以 Page Aggregate head 为主,Sidebar title 只作为导航视图投影。 +- Sidebar title 与 Page Aggregate head 冲突时记录诊断并触发 resync。 +- 增加 smoke:改标题后 Sidebar、Breadcrumb、文档页标题同源一致。 + +## 5. 状态 + +已确认标题来源仍有漂移风险,尚未修复。 diff --git a/bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md b/bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md new file mode 100644 index 00000000..39347026 --- /dev/null +++ b/bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md @@ -0,0 +1,33 @@ +# 5-18 [process][bug] AI 写正文后本地 Page Aggregate content 可能不刷新 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`05-editor-mainline` + +## 1. 问题定义 + +页面 AI 写正文后,前端本地 Page Aggregate state 只更新 persisted meta,不保证同步正文快照。`editorBridge.replaceWithSnapshot` 在 editor host 内派发 runtime command,但没有直接回写 reducer content。 + +## 2. 证据 + +- [DocumentAiAgentPanel.runtime.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/DocumentAiAgentPanel.runtime.tsx:913) 附近处理 AI 写入后的前端状态。 +- [document-content.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx:876) 附近接收 AI 结果。 +- [leptos-tiptap-island-editor-host.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx:772) `replaceWithSnapshot` 只通过 editor runtime command 更新 host。 + +## 3. 影响 + +- 用户连续两次问 AI 时,第二次 AI context 可能仍读旧 `pageClientState.content`。 +- 编辑器显示与 Page Aggregate reducer 状态不一致。 +- AI 写入验收如果只看编辑器 DOM,可能漏掉 context stale 问题。 + +## 4. 建议修复 + +- AI 写入成功后应触发 Page Aggregate 回读或统一 reducer content 更新。 +- AI context 应从最新 Rust Page Aggregate snapshot 读取,而不是依赖可能过期的本地 reducer。 +- 增加 smoke:AI 修改正文后不刷新页面连续再问一次 AI,断言第二次 context 包含新内容。 + +## 5. 状态 + +已确认 AI 写入后本地 Page Aggregate content 可能滞后,尚未修复。 diff --git a/bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md b/bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md new file mode 100644 index 00000000..2e35849e --- /dev/null +++ b/bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md @@ -0,0 +1,61 @@ +# 7-16 [process][bug] mnote.doc.markdown_edit 中文归一化替换 byte index 误用 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` +> +> 关联代码: +> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:895-956` — `search_replace` +> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:920-936` — 归一化匹配后的切片计算 + +## 1. 问题定义 + +`mnote.doc.markdown_edit` 的 `search_replace` 在 Level 2 “忽略首尾空白和全角 / 半角差异”分支中,先对 `norm_line` 调用 `find(&norm_search)`,得到的是 UTF-8 byte offset: + +```rust +let start = norm_line.find(&norm_search).unwrap(); +let end = start + norm_search.len(); +``` + +随后代码把 `start / end` 当成字符序号传给 `line.char_indices().nth(...)`: + +```rust +&line[..line.char_indices().nth(start).map(|(i, _)| i).unwrap_or(0)] +&line[line.char_indices().nth(end).map(|(i, _)| i).unwrap_or(line.len())..] +``` + +这在 ASCII 文本里不明显,但中文、中文标点、全角字符都是多字节。byte offset 不等于字符序号,最终替换范围会偏移。 + +## 2. 影响 + +- 页面 AI 对中文正文执行 `mnote.doc.markdown_edit` 时,归一化匹配可能替换错位置。 +- 本地 `.md` 文件和在线 Convex 文档共用该工具,因此两条 AI 编辑路径都会受影响。 +- 如果替换结果继续写回,用户看到的正文可能被局部破坏,而不是简单失败。 + +## 3. 复现思路 + +构造一行中文正文,让精确匹配失败但归一化匹配命中,例如带首尾空白或全角 / 半角差异的 search: + +```text +原文:第一段内容 +search: 一段 +replace:二段 +``` + +`find()` 得到的是 byte offset;当前代码按字符序号切片后,替换边界会落到错误字符位置。 + +## 4. 根因 + +`str::find` 返回 byte index;`char_indices().nth(n)` 的 `n` 是第几个字符。当前实现把两种索引体系混用。 + +## 5. 建议修复 + +- 在归一化时保留原文字符到归一化字符的 offset map。 +- 或者只在同一字符串上使用 byte index,并确保切片边界来自同一个原始字符串的 `char_indices` 映射。 +- 增加中文、多字节标点、全角英文 / 数字混排的 `search_replace` 单测。 + +## 6. 状态 + +已确认源码层缺陷,尚未修复。进入 `process` 等待实现和验证。 diff --git a/bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md b/bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md new file mode 100644 index 00000000..d9740bbb --- /dev/null +++ b/bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md @@ -0,0 +1,75 @@ +# 7-17 [process][bug] mnote.doc.markdown_edit 同块多操作写回会覆盖前序修改 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` +> +> 关联代码: +> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:779-824` — markdown 字符串层顺序执行 operations +> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:854-878` — Convex 写回重新转换成 block ops +> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:976-1008` — `build_block_ops_from_markdown_edit` + +## 1. 问题定义 + +`doc_markdown_edit` 会先把所有 search/replace 顺序应用到 `md`: + +```rust +match search_replace(&md, &search, &replace) { + Ok(new_md) => { + md = new_md; + applied += 1; + } + ... +} +``` + +但在线 Convex 文档写回时没有使用这个最终 `md`。当前实现重新读取 Page Aggregate blocks,然后对每条 operation 基于原始 block 文本生成一个 replace block op: + +```rust +let block_text = block.get("text").and_then(Value::as_str).unwrap_or(""); +let new_text = block_text.replacen(search, replace, 1); +``` + +如果两条 operation 命中同一个 block,第二条 operation 的 `new_text` 仍从原始 `block_text` 计算,会覆盖第一条 operation 的结果。 + +## 2. 影响 + +用户在页面 AI 中一次提出多个同段修改时,可能只保留最后一个修改。例如: + +```json +[ + { "search": "A", "replace": "B" }, + { "search": "C", "replace": "D" } +] +``` + +若 `A` 和 `C` 都在同一个 block 中,markdown 层预期最终是 `B ... D`,但写回层可能生成两个 replace block op: + +```text +op1 content = 原始文本中 A -> B +op2 content = 原始文本中 C -> D +``` + +最终第二次 replace 会把 block 写成只包含 `C -> D` 的版本,前一次 `A -> B` 被丢失。 + +## 3. 根因 + +`doc_markdown_edit` 有两套编辑结果: + +- `md`:顺序应用 operations 后的真实 markdown 结果。 +- `block_ops`:从原始 blocks 和原始 operations 重新推导出来的写回操作。 + +Convex 写回使用的是 `block_ops`,而不是 `md`。这让“markdown 编辑主路径”在在线文档里退化成了不完整的 block ops 转换层。 + +## 4. 建议修复 + +- 最小修复:`build_block_ops_from_markdown_edit` 先按 block 聚合 operations,并基于当前已更新文本连续计算同块最终 content。 +- 更稳妥修复:以最终 `md` 作为单一结果,走 markdown -> EditorBlockDocument / page.body.save 的正式转换链,不从原始 operations 二次推导写回结果。 +- 增加同一 block 多 operation 的单测和真实 Page Aggregate 回读 smoke。 +- 明确部分失败是否允许写入;默认建议任一 operation 失败时不写入,除非调用方显式允许 partial apply。 + +## 5. 状态 + +已确认源码层缺陷,尚未修复。进入 `process` 等待实现和验证。 diff --git a/bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md b/bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md new file mode 100644 index 00000000..83583442 --- /dev/null +++ b/bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md @@ -0,0 +1,35 @@ +# 7-18 [process][bug] AI markdown_edit 阶段状态合同漂移 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +当前仓库的顶层协作口径仍描述 AI 侧“当前只做 Phase A,Phase B 退役 `local_rule` planner 仍未实施”,但 `design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` 与源码已经把 `mnote.doc.markdown_edit` 推到实际运行链路。 + +这造成阶段合同漂移:文档与实现同时在表达“仍处 Phase A”和“Phase A / B 已开始切主”两种状态。 + +## 2. 影响 + +- 后续开发者无法判断 `page_ai_workflow` 是否应继续保留 `local_rule` 快路径。 +- AI 编辑验收标准会混乱:到底验 `apply_block_ops`,还是验 `markdown_edit` 回读一致性。 +- 新功能容易继续堆到过渡路径,而不是收敛到正式工具合同。 + +## 3. 证据 + +- `rust/crates/mnote-web/src/routes/page_ai_workflow.rs` 已直接调用 `doc::doc_markdown_edit`。 +- `design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` 已记录在线 / 本地 markdown 编辑收敛方向。 +- 根目录协作口径仍强调 Phase B 退役 `local_rule` planner 属于后续项。 + +## 4. 建议修复 + +- 明确当前真实阶段:`markdown_edit` 是否已经是页面 AI 简单编辑主路径。 +- 若已切主,应把 `page_ai_workflow`、Hermes guidance、manifest 和 smoke 都同步到该口径。 +- 若未切主,应限制 `markdown_edit` 的 runtime 使用面,并把设计稿状态回退为实验态。 + +## 5. 状态 + +已确认设计口径与源码使用状态不一致,尚未完成统一。 diff --git a/bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md b/bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md new file mode 100644 index 00000000..077cf22c --- /dev/null +++ b/bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md @@ -0,0 +1,35 @@ +# 7-19 [process][bug] Hermes 工具指导仍优先 apply_block_ops 而非 markdown_edit v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`mnote.doc.markdown_edit` 已被设计为普通页面正文编辑的主路径,但 Hermes run 的工具指导仍要求简单正文编辑优先调用 `mnote_doc_apply_block_ops`。 + +这导致模型策略与工具主线冲突。 + +## 2. 证据 + +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2351) 构造工具指导。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2357) 要求上下文足够时直接调用 `mnote_doc_apply_block_ops`。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2360) 明确“简单小段落编辑”优先用 `apply_block_ops`。 + +## 3. 影响 + +- Hermes 与 page AI fast-path 会走不同编辑工具。 +- markdown 级 search/replace 的收敛设计无法成为唯一验收口径。 +- `apply_block_ops` 的 revision / conflictDetectionKey 合同缺陷会被继续放大。 + +## 4. 建议修复 + +- 将普通正文 search/replace、局部段落替换、全文 markdown 替换统一引导到 `mnote_doc_markdown_edit`。 +- 仅在需要结构性块移动、资源块、复杂定位时才引导模型使用 `apply_block_ops` 或 `mnote.block.*`。 +- 同步更新 smoke:模型生成工具调用时应优先产出 `mnote_doc_markdown_edit`。 + +## 5. 状态 + +已确认源码指导文本与当前设计主线不一致,尚未修复。 diff --git a/bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md b/bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md new file mode 100644 index 00000000..b8b1d73c --- /dev/null +++ b/bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md @@ -0,0 +1,35 @@ +# 7-20 [process][bug] page_ai_workflow 绕过 Hermes tool executor / audit / toggle v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`/api/page-ai/block-edit-workflow` 仍是独立模型调用链:直接读取 Hermes profile、调用 chat/completions、解析模型 JSON,并直接调用 Rust 工具函数。 + +它没有通过 `/api/hermes/tools/mnote/call` 的工具执行壳,因此绕过了 tool toggle、audit、统一幂等和统一调用追踪。 + +## 2. 证据 + +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:69) 直接调用上游模型。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) 在 route 内构造 `ToolCallInput`。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:109) 直接调用 `doc::doc_markdown_edit`。 + +## 3. 影响 + +- 关闭或限制 mnote tool 时,该 fast-path 仍可能写入。 +- 工具调用审计与普通 Hermes run 不一致。 +- 幂等、dryRun、runId、toolCallId 等字段无法统一治理。 + +## 4. 建议修复 + +- 将 fast-path 的工具写入改为调用统一 tool executor。 +- fast-path 只负责 prompt / plan,不直接执行写入函数。 +- 增加 smoke:当对应 tool disabled 时,`page_ai_workflow` 不得绕过限制写入。 + +## 5. 状态 + +已确认 route 级绕过统一执行壳,尚未修复。 diff --git a/bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md b/bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md new file mode 100644 index 00000000..b790ec9f --- /dev/null +++ b/bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md @@ -0,0 +1,33 @@ +# 7-21 [process][bug] mnote.doc.markdown_edit 本地文件写入绕过 dryRun / idempotency v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`mnote.doc.markdown_edit` 的本地文件分支没有调用统一写入合同校验,也没有检查 `dryRun` 与 `idempotencyKey`,在匹配成功后直接 `fs::write`。 + +## 2. 证据 + +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:714) 进入 `doc_markdown_edit` 后没有先调用 `ensure_write_contract`。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:842) 注释说明本地文件直接写回。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:845) 对本地路径执行 `fs::write`。 + +## 3. 影响 + +- `dryRun=true` 时也可能落盘,违反工具预览语义。 +- 缺少幂等键会让重试请求重复写入,尤其是 ACP / Hermes 断线重试场景。 +- 本地 `.md` 与在线 Convex 文档的写入安全级别不一致。 + +## 4. 建议修复 + +- 在 `doc_markdown_edit` 写入前统一调用 `ensure_write_contract`。 +- 对 `dryRun=true` 返回 diff / preview,不落盘。 +- 增加本地 `.md` dryRun 单测,断言文件内容不变。 + +## 5. 状态 + +已确认源码层缺陷,尚未修复。 diff --git a/bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md b/bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md new file mode 100644 index 00000000..dc00817a --- /dev/null +++ b/bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md @@ -0,0 +1,35 @@ +# 7-22 [process][bug] mnote.doc.apply_block_ops 批量写入缺少 revision / conflictDetectionKey 校验 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`mnote.doc.apply_block_ops` 的批量入口只校验 dryRun / idempotency 写入合同,没有调用页面 revision、conflictDetectionKey、blockRevisionRef 的写前校验。 + +同文件中已有 `ensure_page_write_preconditions`,但该批量入口没有使用它。 + +## 2. 证据 + +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:385) 定义 `doc_apply_block_ops`。 +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:390) 仅调用 `ensure_write_contract`。 +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:774) 存在 `ensure_page_write_preconditions`,但批量入口未调用。 + +## 3. 影响 + +- AI 可能基于旧 `PageAggregate` 对正文写入,覆盖用户新编辑。 +- 多轮 Hermes / ACP run 同时写入时缺少冲突保护。 +- `markdown_edit` 在线分支会转调 `apply_block_ops`,因此该缺陷会影响 markdown 主路径。 + +## 4. 建议修复 + +- `doc_apply_block_ops` 真实写入前必须校验 page revision 与 conflictDetectionKey。 +- 块级操作应支持并校验 blockRevisionRef 或等价版本字段。 +- 增加负向测试:缺少 revision / conflictDetectionKey 时真实写入必须拒绝。 + +## 5. 状态 + +已确认批量写入入口缺少必要冲突校验,尚未修复。 diff --git a/bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md b/bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md new file mode 100644 index 00000000..3ce485df --- /dev/null +++ b/bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md @@ -0,0 +1,34 @@ +# 7-23 [process][bug] mnote.doc.markdown_edit manifest schema 缺少 required 与写入字段 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`mnote.doc.markdown_edit` manifest 的 input schema 没有完整声明 `required` 字段,也缺少 `dryRun`、`idempotencyKey` 等写入合同字段。 + +这会让模型、ACP wrapper 和外部调用方无法从 manifest 得知真实写入要求。 + +## 2. 证据 + +- [manifest.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/manifest.rs:413) 附近定义 `mnote.doc.markdown_edit` manifest。 +- 当前 schema 更像参数提示,没有完整表达写入前置条件。 + +## 3. 影响 + +- 调用方可能不传幂等字段或 dryRun 字段。 +- tool toggle / schema validation 无法提前拒绝不合规请求。 +- Reasonix / ACP 等多 runtime 接入时会继续复制不完整合同。 + +## 4. 建议修复 + +- 为 `mnote.doc.markdown_edit` 补全 JSON Schema:`required`、互斥的 `operations` / `full_content`、写入合同字段。 +- manifest 与 Rust runtime 校验保持一致。 +- 增加 manifest snapshot 测试,防止字段再次漂移。 + +## 5. 状态 + +已确认 manifest 合同不完整,尚未修复。 diff --git a/bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md b/bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md new file mode 100644 index 00000000..e86491ec --- /dev/null +++ b/bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md @@ -0,0 +1,35 @@ +# 7-24 [process][bug] 在线 markdown_edit 写回不以最终 Markdown 为真源 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[process]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +在线 Convex 文档的 `mnote.doc.markdown_edit` 先在 `md` 字符串上顺序应用 search/replace,但写回时没有用最终 `md` 生成新的文档真相,而是重新读取原始 blocks,并用原始 operations 反推 `apply_block_ops`。 + +因此 markdown 层已经算出的最终结果不一定能被忠实落库。 + +## 2. 证据 + +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:811) 在 `md` 上执行 search/replace。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:855) 写回前重新读取 aggregate / blocks。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:857) 使用原始 operations 构造 block ops,而不是解析最终 `md`。 + +## 3. 影响 + +- 跨块替换、全文替换、标题 / 列表格式变化可能在 markdown 层成功,但无法正确转换成 block ops。 +- 部分失败仍可能返回成功写入,用户看到的正文与工具返回摘要不一致。 +- 该问题与 `7-17` 的同块多操作覆盖同源,但影响面更宽。 + +## 4. 建议修复 + +- 在线分支应明确选择一种真源:要么最终 `md` 可被解析并完整生成 EditorBlockDocument,要么限制 `markdown_edit` 只支持可安全映射的单块操作。 +- 对 full_content、跨块 search/replace、列表/标题变更增加回读一致性测试。 +- route 成功条件必须从“operationsApplied > 0”提升为“Page Aggregate 回读等于预期最终 markdown”。 + +## 5. 状态 + +已确认写回层没有使用最终 markdown 真相,尚未修复。 diff --git a/design/10-review/README.md b/design/10-review/README.md index 84aa7d6e..326dfed0 100644 --- a/design/10-review/README.md +++ b/design/10-review/README.md @@ -6,6 +6,8 @@ - [Kernel 架构收口与下一阶段优先级 Review / Checklist](./process/08-kernel-architecture-next-priority-review-and-checklist.md) - [页面 AI 快速块编辑 Runtime Review](./process/09-page-ai-fast-block-edit-runtime-review.md) +- [当前 mnote 项目 AI / Page Aggregate 定向 Review](./process/10-current-mnote-ai-runtime-review-v1.md) +- [当前完整架构 Review](./process/11-current-full-architecture-review-v1.md) 最新归档审查: diff --git a/design/10-review/process/10-current-mnote-ai-runtime-review-v1.md b/design/10-review/process/10-current-mnote-ai-runtime-review-v1.md new file mode 100644 index 00000000..1304fa39 --- /dev/null +++ b/design/10-review/process/10-current-mnote-ai-runtime-review-v1.md @@ -0,0 +1,88 @@ +# 10 [process] 当前 mnote 项目 AI / Page Aggregate 定向 Review v1 + +> 更新时间:2026-05-17 +> +> 执行状态:`process` +> +> 范围:当前主线中 Page Aggregate 单一真源、页面 AI 快速编辑、`mnote.doc.markdown_edit` 与 ACP / Hermes runtime 的源码级定向审查。 + +## 1. 本轮结论 + +本轮没有发现 Page Aggregate 读取主链重新回退到 Next compat builder 的证据;`wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 仍只走 Rust `/api/page-aggregate/:documentId`,`/api/documents/page` compat 读链保持 `410` 退场口径。 + +新的风险集中在 `07-ai` 的 markdown 编辑收敛实现: + +- `mnote.doc.markdown_edit` 已进入实际写入主线,并被 `/api/page-ai/block-edit-workflow` 调用。 +- 该工具当前先在 markdown 字符串上顺序执行 search/replace,再把原始 operations 重新转换成 `mnote.doc.apply_block_ops`。 +- 转换层仍按“单块文本包含 search”查找目标块,无法忠实表达 markdown 层已经算出的最终结果。 + +因此,当前 AI 编辑主线不能只看 `operationsApplied > 0` 或 route 返回 `ok=true`。需要优先补充 `markdown_edit` 的中文、多操作、同块、多块、full_content 与失败原子性测试。 + +## 2. 发现的问题 + +### R-01 P0:中文归一化匹配使用 byte index 当 char index + +关联缺陷: + +- `bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md` + +证据: + +- `rust/crates/mnote-web/src/hermes_tools/doc.rs:920-936` + +`search_replace` 的 Level 2 先在 `norm_line` 上用 `find()` 得到 UTF-8 byte offset,却把这个 offset 传给 `line.char_indices().nth(start)` 当作字符序号使用。中文、多字节标点或中英混排时,替换范围会偏移,可能替错片段或生成错误内容。 + +影响: + +- 页面 AI 对中文正文的“忽略空白 / 全半角差异”替换不可靠。 +- 该问题位于 `mnote.doc.markdown_edit` 的基础 search/replace 层,会同时影响在线文档和本地 `.md` 文件。 + +### R-02 P0:同一块内多次 markdown_edit 会被后续 block op 覆盖 + +关联缺陷: + +- `bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md` + +证据: + +- `rust/crates/mnote-web/src/hermes_tools/doc.rs:779-824` +- `rust/crates/mnote-web/src/hermes_tools/doc.rs:854-878` +- `rust/crates/mnote-web/src/hermes_tools/doc.rs:976-1008` + +`doc_markdown_edit` 已经在 `md` 变量中顺序应用了所有 search/replace,但 Convex 写入时没有使用这个最终 markdown。它重新读取原始 Page Aggregate blocks,并对每个 operation 用原始 block 文本生成一个 replace block op。 + +如果两个 operation 命中同一个 block,第二个 block op 的 `content` 仍从原始文本计算,会覆盖第一个 operation 的结果。也就是说,markdown 层算出的最终结果与实际落库结果可能不一致。 + +影响: + +- AI 一次请求中常见的“把同一段里的 A 改成 B,同时把 C 改成 D”可能只保留最后一次修改。 +- 前端快路径仍可能显示“已通过页面 markdown 编辑快路径完成写入”,但正文只部分生效。 + +## 3. 次级风险 + +- `doc_markdown_edit` 当前允许部分 operation 失败后继续写入已成功的子集,并返回 `ok=true`。如果这是有意设计,需要在 manifest / UI 中明确“非原子”;如果不是,应改为任一 operation 失败时不写入。 +- `changedText` 通过 `operations.iter().take(applied)` 生成摘要;当前如果第一个 operation 失败、第二个成功,摘要会错误地展示第一个失败 operation。 +- `/api/page-ai/block-edit-workflow` 的系统 prompt 已改成 search/replace,但仍强依赖模型精确复制 `pageText`;一旦模型输出跨块片段,Convex 写回层不能表达该编辑。 + +## 4. 建议下一步 + +1. 先给 `search_replace` 补中文归一化替换单测,再修正 byte / char offset 映射。 +2. 给 `doc_markdown_edit` 补同一 block 多 operation 的失败用例,明确应以最终 markdown 生成落库内容,或在转换层合并同块操作。 +3. 明确 `markdown_edit` 的失败原子性:默认建议任一 operation 失败时不写入,除非请求显式允许 partial apply。 +4. 将 `/api/page-ai/block-edit-workflow` 的验收从“route 成功”提升为“Page Aggregate 回读与预期最终 markdown 一致”。 + +## 5. 本轮验证 + +静态检查范围: + +```bash +rg -n "markdown_edit|apply_block_ops|block-edit-workflow|local_rule|page_ai_workflow|mnote\\.doc\\.fetch" rust wolai-frontend scripts design/07-ai design/10-review --glob '!rust/target/**' --glob '!node_modules/**' +rg -n "page-aggregate|pageAggregate|PageAggregate|blockDocument|projectionSource|documents/save|documents/options|/api/page-aggregate" rust/crates/mnote-web rust/crates/bridge-runtime wolai-frontend/src scripts --glob '!rust/target/**' --glob '!node_modules/**' +``` + +本轮是 review / bug hunt,没有修改 runtime 源码。后续修复应至少补: + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit +cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow +``` diff --git a/design/10-review/process/11-current-full-architecture-review-v1.md b/design/10-review/process/11-current-full-architecture-review-v1.md new file mode 100644 index 00000000..7a0d86e4 --- /dev/null +++ b/design/10-review/process/11-current-full-architecture-review-v1.md @@ -0,0 +1,34 @@ +# 11 [process] 当前完整架构 Review v1 + +> 更新时间:2026-05-17 +> +> 状态:`process` +> +> 参考总文档:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md) + +## 1. 结论 + +当前 mnote 的主线架构已经成形,但还没有完成单一真源收口。最关键的未闭合点有四个: + +1. Tree realtime 的发布口径与前端消费口径不一致。 +2. FileTree 资源行仍可能误走页面命令。 +3. Page Aggregate 仍保留前端第二真相。 +4. AI 写入链路在 `markdown_edit`、`apply_block_ops`、Hermes / ACP / Reasonix 之间存在合同漂移。 + +## 2. 关键判断 + +- Rust kernel 与 bridge-runtime 已经承担语义主导权。 +- 前端仍保留若干本地派生状态,适合过渡,不适合作为长期真相层。 +- `mnote.doc.markdown_edit` 已是主推路径,但工具级 contract 还没收口。 +- `page_ai_workflow` 与 ACP / Reasonix 已接入主链,但运行时 owner 与写入字段不统一。 + +## 3. 需要继续跟踪的 bug + +- Rust Web / realtime / ACP:`bugs/03-rust-web/process/3-16` 到 `3-21` +- Tree domain:`bugs/04-tree-domain/process/4-46` +- Editor mainline:`bugs/05-editor-mainline/process/5-15` 到 `5-18` +- AI:`bugs/07-ai/process/7-18` 到 `7-24` + +## 4. 备注 + +本次审查的详细架构说明已统一写入根目录:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md)。 diff --git a/package.json b/package.json index 1bb63837..c91d5cbb 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "desktop": "node scripts/desktop-prod.js", "desktop:hot": "node scripts/desktop-hot.js", + "dev:hot": "node scripts/dev-hot.js", "desktop:local": "node scripts/desktop-local.js", "desktop:electron": "node desktop-electron/dev-runner.js", "build:desktop:next": "node scripts/prepare-desktop-next.js", diff --git a/rust/crates/mnote-web/src/routes/dev_hot.rs b/rust/crates/mnote-web/src/routes/dev_hot.rs new file mode 100644 index 00000000..3633d881 --- /dev/null +++ b/rust/crates/mnote-web/src/routes/dev_hot.rs @@ -0,0 +1,51 @@ +use axum::http::{HeaderMap, HeaderValue}; +use axum::response::IntoResponse; +use axum::Json; +use serde::Serialize; +use std::sync::LazyLock; +use std::time::{SystemTime, UNIX_EPOCH}; + +const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner"; + +static DEV_HOT_BOOT_ID: LazyLock = LazyLock::new(|| { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis()) + .unwrap_or_default(); + format!("{}-{now}", std::process::id()) +}); + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DevHotReloadPayload { + ok: bool, + enabled: bool, + boot_id: &'static str, +} + +pub fn dev_hot_reload_enabled() -> bool { + matches!( + std::env::var("MNOTE_WEB_DEV_HOT_RELOAD") + .unwrap_or_default() + .trim() + .to_ascii_lowercase() + .as_str(), + "1" | "true" | "yes" | "on" + ) +} + +pub async fn hot_reload() -> impl IntoResponse { + let mut headers = HeaderMap::new(); + headers.insert( + HEADER_MNOTE_WEB_OWNER, + HeaderValue::from_static("mnote-web"), + ); + ( + headers, + Json(DevHotReloadPayload { + ok: true, + enabled: dev_hot_reload_enabled(), + boot_id: DEV_HOT_BOOT_ID.as_str(), + }), + ) +} diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index 2c7dab0f..74eca79d 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -1,6 +1,7 @@ mod bridge; pub(crate) mod command_support; mod compat; +mod dev_hot; mod documents; mod editor; mod gateway; @@ -80,6 +81,7 @@ pub fn build_router(state: AppState) -> Router { ) .route("/api/search/documents", post(search::documents)) .route("/api/gateway/health", get(gateway::gateway_health)) + .route("/api/dev/hot-reload", get(dev_hot::hot_reload)) .route("/api/runtime/config", get(session::runtime_config)) .route("/api/auth", post(gateway::auth_api)) .route("/api/auth/session", get(session::session)) @@ -350,4 +352,18 @@ mod tests { ); } } + + #[tokio::test] + async fn dev_hot_reload_endpoint_is_mounted() { + let response = app(false) + .oneshot( + Request::builder() + .uri("/api/dev/hot-reload") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + } } diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index dab61f43..a495a6bd 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -6745,6 +6745,36 @@ const SIDEBAR_TREE_JS: &str = r##" } scheduleInitializePageUiSurfaces(); + function installMnoteDevHotReload() { + var bootId = ''; + var failedOnce = false; + var timer = 0; + function tick() { + fetch('/api/dev/hot-reload', { cache: 'no-store', headers: { accept: 'application/json' } }) + .then(function(response) { return response.ok ? response.json() : null; }) + .then(function(payload) { + if (!payload || payload.enabled !== true || !payload.bootId) { + if (!bootId && timer) window.clearInterval(timer); + return; + } + document.documentElement.setAttribute('data-mnote-dev-hot-reload', 'enabled'); + if (!bootId) { + bootId = String(payload.bootId); + return; + } + if (failedOnce || String(payload.bootId) !== bootId) { + window.location.assign(window.location.href); + } + }) + .catch(function() { + if (bootId) failedOnce = true; + }); + } + tick(); + timer = window.setInterval(tick, 1000); + } + installMnoteDevHotReload(); + function readPageDragNodeId(event) { var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : ''; return (fromTransfer || draggingPageNodeId || '').trim(); @@ -7437,6 +7467,14 @@ mod tests { assert!(!SIDEBAR_TREE_JS.contains("window.location.reload")); } + #[test] + fn sidebar_tree_runtime_contains_dev_hot_reload_client() { + assert!(SIDEBAR_TREE_JS.contains("installMnoteDevHotReload")); + assert!(SIDEBAR_TREE_JS.contains("/api/dev/hot-reload")); + assert!(SIDEBAR_TREE_JS.contains("data-mnote-dev-hot-reload")); + assert!(SIDEBAR_TREE_JS.contains("window.clearInterval(timer)")); + } + #[test] fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() { assert!(SIDEBAR_TREE_JS.contains("openTreeContextMenu")); diff --git a/rust/target/.rustc_info.json b/rust/target/.rustc_info.json index c433414b..b9d4ae92 100644 --- a/rust/target/.rustc_info.json +++ b/rust/target/.rustc_info.json @@ -1 +1 @@ -{"rustc_fingerprint":10059341515723286937,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/lix/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.95.0 (59807616e 2026-04-14)\nbinary: rustc\ncommit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860\ncommit-date: 2026-04-14\nhost: x86_64-unknown-linux-gnu\nrelease: 1.95.0\nLLVM version: 22.1.2\n","stderr":""}},"successes":{}} \ No newline at end of file +{"rustc_fingerprint":10059341515723286937,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.95.0 (59807616e 2026-04-14)\nbinary: rustc\ncommit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860\ncommit-date: 2026-04-14\nhost: x86_64-unknown-linux-gnu\nrelease: 1.95.0\nLLVM version: 22.1.2\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/lix/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/scripts/dev-hot.js b/scripts/dev-hot.js new file mode 100644 index 00000000..e49ed49f --- /dev/null +++ b/scripts/dev-hot.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node +"use strict"; + +/** + * dev:hot 启动入口。 + * + * 目标: + * - 不改动 desktop:hot 的默认行为。 + * - 使用 cargo-watch 自动重编译并重启 mnote-web。 + * - 通过 MNOTE_WEB_DEV_HOT_RELOAD 启用页面端轻量 reload 轮询。 + */ + +const { spawn } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); + +const rootDir = path.resolve(__dirname, ".."); +const desktopHotPath = path.join(rootDir, "scripts", "desktop-hot.js"); + +function cargoWatchCommand(env = process.env) { + const extraArgs = String(env.MNOTE_WEB_DEV_HOT_CARGO_WATCH_ARGS || "").trim(); + const watchArgs = [ + "-w crates/mnote-web/src", + "-w crates/mnote-web/Cargo.toml", + "-w Cargo.toml", + "-x \"run -p mnote-web --bin mnote-web\"", + ]; + if (extraArgs) watchArgs.unshift(extraArgs); + return `cargo watch ${watchArgs.join(" ")}`; +} + +function buildDevHotEnv(baseEnv = process.env) { + return { + ...baseEnv, + MNOTE_WEB_DEV_HOT_RELOAD: "1", + MNOTE_WEB_CMD: String(baseEnv.MNOTE_WEB_CMD || "").trim() || cargoWatchCommand(baseEnv), + }; +} + +function main() { + if (!fs.existsSync(desktopHotPath)) { + throw new Error(`缺少 desktop-hot 入口:${desktopHotPath}`); + } + + const child = spawn(process.execPath, [desktopHotPath], { + cwd: rootDir, + env: buildDevHotEnv(process.env), + stdio: "inherit", + }); + + child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); + }); + + child.on("error", (error) => { + console.error(`[dev-hot] 启动失败:${error.message}`); + process.exit(1); + }); +} + +if (require.main === module) { + main(); +} + +module.exports = { + buildDevHotEnv, + cargoWatchCommand, +}; diff --git a/scripts/task-dev-hot-plan-test.js b/scripts/task-dev-hot-plan-test.js new file mode 100644 index 00000000..49c1f668 --- /dev/null +++ b/scripts/task-dev-hot-plan-test.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node +"use strict"; + +const assert = require("node:assert"); +const { buildDevHotEnv } = require("./dev-hot.js"); + +const env = buildDevHotEnv({ + FRONTEND_PORT: "3200", + MNOTE_WEB_CMD: "", +}); + +assert.equal(env.MNOTE_WEB_DEV_HOT_RELOAD, "1"); +assert.match(env.MNOTE_WEB_CMD, /cargo watch/); +assert.match(env.MNOTE_WEB_CMD, /run -p mnote-web --bin mnote-web/); +assert.match(env.MNOTE_WEB_CMD, /crates\/mnote-web\/src/); +assert.equal(env.FRONTEND_PORT, "3200"); + +console.log(JSON.stringify({ ok: true, command: env.MNOTE_WEB_CMD }, null, 2));