advance 1-8 post-mvp execution batches
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
# 7-27 [done] 在线 Markdown_edit 以最终 Markdown 为写回真源 v2
|
||||
|
||||
> 更新:2026-05-18(v2:整合 CLI Main 参考实现分析,确认方向,补充见解)
|
||||
>
|
||||
> 2026-05-19 local-first 口径补充:
|
||||
> - 本文仍适用于 `convex_workspace` / 在线文档的 `mnote.doc.markdown_edit` 修复,但当前默认产品形态已切到 local-first workspace。
|
||||
> - 本地 `.md` 路径的默认 AI 编辑主路径是“授权文件引用 + agent 原生 patch/diff + watcher 同步”;`mnote.doc.markdown_edit` 只作为本地受控代理 fallback、cloud / remote agent 或 compat 路径。
|
||||
> - 后续新增 AI 编辑能力默认先保证本地 `.md` 与 `{mdBase}.assets/` 相对路径不被改写为 Convex media asset;在线 Convex 文档路径只作为可选 cloud / sync / share source。
|
||||
>
|
||||
> 当前状态:`DONE`(已归档,代码验证通过 2026-05-21)
|
||||
>
|
||||
> 关联缺陷:`bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md`
|
||||
> `bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md`
|
||||
> `bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md`
|
||||
>
|
||||
> 上位设计:`design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
>
|
||||
> 参考实现:`design/05-editor-mainline/reference-code/cli-main/shortcuts/doc/`(str_replace skill)
|
||||
> `rust/crates/mnote-web/src/routes/local_markdown_parser.rs`(已有 GFM→blocks 解析器)
|
||||
|
||||
---
|
||||
|
||||
## 0. 参考实现分析:CLI Main(Lark Doc)怎么做?
|
||||
|
||||
### 0.1 实现结构
|
||||
|
||||
CLI Main 的 `docs +update --api-version v2` 将 `str_replace`、`block_replace`、`block_insert_after` 等命令以 PUT 请求发送到 Lark OpenAPI:
|
||||
|
||||
```go
|
||||
// docs_update_v2.go:140-157
|
||||
body := map[string]interface{}{
|
||||
"format": runtime.Str("doc-format"),
|
||||
"command": cmd, // "str_replace" | "block_replace" | ...
|
||||
}
|
||||
body["pattern"] = runtime.Str("pattern") // str_replace 的搜索文本
|
||||
body["content"] = runtime.Str("content") // 替换文本或新内容
|
||||
body["block_id"] = blockID // block_* 操作的目标块
|
||||
body["revision_id"] = runtime.Int("revision-id")
|
||||
|
||||
// API: PUT /open-apis/docs_ai/v1/documents/{id}
|
||||
```
|
||||
|
||||
**关键:`str_replace` 只传 `pattern` + `content`,不传 `final_md`。** 服务端(Lark OpenAPI)在收到 `command: "str_replace"` 后,由服务端负责在文档的 XML/block 存储中找到匹配的文本并替换。
|
||||
|
||||
### 0.2 与 mnote 的架构对应
|
||||
|
||||
| CLI Main (Lark Doc) | mnote | 角色 |
|
||||
|---|---|---|
|
||||
| Lark OpenAPI 服务端 | Rust `doc_markdown_edit` + `execute_page_body_save` | 执行文本替换→block 持久化 |
|
||||
| `str_replace` API 端点 | `mnote.doc.markdown_edit` 工具 | AI 调用的文本级编辑入口 |
|
||||
| 块操作 API 端点 | `mnote.doc.apply_block_ops` + `mnote.block.*` | AI 调用的块级编辑入口 |
|
||||
| CLI 客户端(lark-cli) | Hermes agent / page_ai_workflow | AI 编排层,只产生`{pattern, content}`对 |
|
||||
|
||||
**mnote 的 Rust kernel 就是那个"服务端"。** 所以在 Rust 层实现 markdown→block content 的转换是正确的方向——这不是"在客户端做服务端的事",而是 mnote 的 Rust 层本来就承担服务端职责。
|
||||
|
||||
### 0.3 CLI Main 对我们设计的启发
|
||||
|
||||
1. **`str_replace` 是不可拆分的原子操作** — CLI Main 的 `str_replace` 由服务端完整执行。一旦 `pattern` + `content` 送出,客户端不需要处理块映射。**mnote 的 `doc_markdown_edit` 也应该是一条完整的原子路径**:接收 `operations` → 搜索替换 → 产生 final_md → **直接转换 final_md 为 blocks 并写回**。不应该把 operations 暴露给下游做二次推导。
|
||||
|
||||
2. **文档中的所有纯文本搜索替换只需一条 API 调用** — CLI Main 不支持在一次请求中组合多个 `str_replace`。mnote 支持多条 operation 是更灵活的设计,但兑现这个灵活性的前提是:**多条 operation 在服务端累积应用到 `md` 后,只产出一组最终的 block content 写回**,而不是每条 operation 独立映射到 block。
|
||||
|
||||
3. **`pattern` 不限制为精确文本** — CLI Main 的 `str_replace` 文档鼓励 Markdown 模式下的"前缀...后缀"省略号语法(`"start...end"`),允许模糊定位。这和我们已有的四级匹配策略(精确→忽略空白→段落 fuzzy→失败)方向一致。
|
||||
|
||||
4. **CLI Main 在客户端做预检查** — `docs_update_check.go` 中有 `checkDocsUpdateReplaceMultilineMarkdown`,在发送请求前就检查 Markdown 中的空行是否会违反服务端的单块内文本替换限制。**同理,mnote 在转换 final_md 时也应该做预检**:发现跨块内容变化时,要么走 GFM 解析器重建多块 content,要么清楚告知 AI "这条 operation 需要多块替换"。
|
||||
|
||||
5. **mnote 的 final_md 方法比 CLI Main 更可靠** — CLI Main 把 `pattern` 发给服务端,由服务端重新执行匹配。如果服务端的匹配策略与模型预期的不同,结果会意外。mnote 的做法(在 Rust 层计算 final_md,然后用 final_md 生成 block content)把匹配阶段和写回阶段解耦——匹配结果对 AI 可见(文档中说"已验证将 X 替换为 Y"),不会出现「客户端匹配成功、服务端匹配失败」的不一致。
|
||||
|
||||
### 0.4 与 CLI Main 的根本差异:为何不能照搬
|
||||
|
||||
CLI Main 的方案(客户端只传 `pattern` + `content`,服务端做匹配+转换)依赖 Lark OpenAPI 对文档 block 存储的完全控制。mnote 的 Convex 后台(`documents:updateContent`)只接受完整的 blocks 数组作为 `content`,没有 `str_replace` 端点。
|
||||
|
||||
**这迫使 mnote 的 Rust 层必须自己做 final_md → block content 的转换。这个转换就是我们的"服务端逻辑",是正确且必要的。**
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题
|
||||
|
||||
### 1.1 当前架构
|
||||
|
||||
`mnote.doc.markdown_edit` 在在线 Convex 文档路径下有两套编辑结果:
|
||||
|
||||
```
|
||||
搜索替换 → 最终 md ✅(正确的结果)
|
||||
|
||||
↓
|
||||
build_block_ops_from_markdown_edit(blocks, operations, applied)
|
||||
↓ 二次推导
|
||||
doc_apply_block_ops → page.body.save
|
||||
```
|
||||
|
||||
- **第一层**: `search_replace(&md, &search, &replace)` — 在完整的 markdown 字符串上顺序执行操作,支持精确/忽略空白/段落 fuzzy 四级匹配。
|
||||
- **第二层**: `build_block_ops_from_markdown_edit` — 不从最终 `md` 提取修改,而是从原始 blocks 和原始 operations 重新推导每次命中。
|
||||
|
||||
**最终 markdown 被丢弃了。** 写入层使用的不是计算好的文本结果,而是从原始 operations 反查 blocks 的二次推导。
|
||||
|
||||
### 1.2 由此导致的已知缺陷
|
||||
|
||||
| 缺陷 | 表现 | 根因 |
|
||||
|------|------|------|
|
||||
| 7-17 同块多操作覆盖 | 两次 op 命中同块 → 第二 op 从原始文本计算,覆盖第一 op | 推导层不累积 |
|
||||
| 7-24 在线不以最终 md 为真源 | `full_content` 无法映射到块 operations → 全部拒绝 | 推导层不支持全文替换 |
|
||||
| 7-25 命中后空 block_ops | markdown 层匹配成功,block 映射失败 → 空 ops | 两套匹配规则不一致 |
|
||||
|
||||
当前「修复」是安全降级:推导失败从伪成功变成明确错误码,但推导本身仍然存在。
|
||||
|
||||
### 1.3 为什么推导不可靠
|
||||
|
||||
`build_block_ops_from_markdown_edit` 当前采用「每 operation 从 `block.text` 反查 + 维护 block_states 累积文本」策略 [doc.rs:1048-1103],已在共享匹配函数和累积方面改进,但根本问题仍在:
|
||||
|
||||
1. **full_content 模式必须跳过推导**:全文替换的最终 md 与原始 blocks 没有逐 op 对应关系
|
||||
2. **跨块替换不可映射**:一条 `search: "TODO"` 命中多个块时,推导层只产生一次 replace op
|
||||
3. **格式变化丢失**:heading/todo 前缀在 markdown 中记录了结构语义,推导层只传文本
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
### 2.1 核心原则
|
||||
|
||||
> **最终 markdown 是写回的唯一真源。** 文本层算出的结果不经二次推导直接落地。
|
||||
|
||||
### 2.2 目标范围
|
||||
|
||||
- 在线 Convex 文档的 `mnote.doc.markdown_edit` 以最终 `md` 为唯一真源生成 `page.body.save` 的 `content` 载荷
|
||||
- 本地文件路径不变(已经以最终 md 直接 `fs::write`)
|
||||
- `mnote.block.*` / `mnote.doc.apply_block_ops` 不做改动
|
||||
- `full_content` 模式不再被拒绝,支持
|
||||
- 复杂块(mindmap、resource、table、image)在往返中保持不变
|
||||
|
||||
### 2.3 非目标(Phase C 范畴)
|
||||
|
||||
- 流式 apply(见 7-14 §7)
|
||||
- suggest/review 模式
|
||||
- 本地 `.md` 文件的块标识持久化(本地文件没有 block identity)
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案:Markdown → Block Content 直接写回
|
||||
|
||||
### 3.1 流程图(替换后)
|
||||
|
||||
```
|
||||
搜索替换 → 最终 md ✅
|
||||
↓
|
||||
parse_final_markdown_to_blocks(&md, &original_blocks)
|
||||
↓
|
||||
[blocks with preserved IDs + metadata + new blocks]
|
||||
↓
|
||||
build_page_content(&original_body_content, &parsed_blocks)
|
||||
↓
|
||||
execute_page_body_save_from_aggregate(state, context, input, &aggregate, next_content, changed_blocks)
|
||||
```
|
||||
|
||||
### 3.2 数据流
|
||||
|
||||
```
|
||||
原始 blocks (来自 aggregate.body.blockDocument.blocks):
|
||||
b1: { type: "paragraph", text: "第一段", revisionRef: "r1" }
|
||||
b2: { type: "heading", text: "标题", revisionRef: "r2", props: { level: 2 } }
|
||||
b3: { type: "resource", ... } ← 复杂块,全文不做修改
|
||||
|
||||
↓ blocks_to_markdown (当前,增强前)
|
||||
|
||||
当前 md (include_ids=true):
|
||||
第一段 <!-- block:b1 -->
|
||||
标题 <!-- block:b2 -->
|
||||
[resource: 资源块] <!-- block:b3 -->
|
||||
|
||||
↓ 搜索替换
|
||||
|
||||
最终 md:
|
||||
新第一段 <!-- block:b1 -->
|
||||
新标题 <!-- block:b2 -->
|
||||
[resource: 资源块] <!-- block:b3 -->
|
||||
|
||||
↓ parse_final_markdown_to_blocks (新增)
|
||||
|
||||
解析后的 blocks:
|
||||
b1: { type: "paragraph", text: "新第一段", revisionRef: "r1" } ← 文本更新,其他不变
|
||||
b2: { type: "heading", text: "新标题", revisionRef: "r2", props: { level: 2 } }
|
||||
b3: { type: "resource", ... } ← 保持原始内容不动(复杂块不可编辑)
|
||||
|
||||
↓ build_page_content (新增)
|
||||
|
||||
最终 content (aggregate.body.content 格式):
|
||||
[b1_legacy, b2_legacy, b3_legacy] ← 复用原始块的非文本属性
|
||||
```
|
||||
|
||||
### 3.3 组件设计
|
||||
|
||||
#### 3.3.1 `blocks_to_markdown` 增强(已有函数,扩展)
|
||||
|
||||
当前只输出 `<!-- block:ID -->`,需要扩展为带类型和 revisionRef:
|
||||
|
||||
```rust
|
||||
// 当前:
|
||||
format!("{text} <!-- block:{id} -->")
|
||||
|
||||
// 增强后:
|
||||
format!("{text} <!-- block:{id}:{block_type}:{revision_ref} -->")
|
||||
|
||||
对 heading 块额外附加 `level` 字段:
|
||||
|
||||
```rust
|
||||
// heading 块:
|
||||
format!("{text} <!-- block:{id}:heading:{revision_ref}:level={level} -->")
|
||||
```
|
||||
|
||||
解析器还原后从 `level` 字段恢复正确的 heading 级别。
|
||||
```
|
||||
|
||||
对 heading 和 todo 等结构化块,前缀已经正确输出(`## `、`- [ ] `),解析器据此还原类型。
|
||||
|
||||
对复杂块(mindmap、resource、table、image)——即 `is_editable == false` 或在 `block_projection_blocks` 中标记了 `unsupportedReason` 的块——输出特殊标记使其不会被解析器修改:
|
||||
|
||||
```text
|
||||
[mnote-raw-block:block_id] <!-- block:{id}:{block_type}:{revision_ref} -->
|
||||
```
|
||||
|
||||
然后写回层把这些块从原始 blocks 中按 ID 复制,不做任何修改。
|
||||
|
||||
#### 3.3.2 `parse_final_markdown_to_blocks`(新增函数)
|
||||
|
||||
签名:
|
||||
|
||||
```rust
|
||||
/// 解析最终 markdown 为块列表,保留块元数据
|
||||
///
|
||||
/// * `final_md` — 搜索替换后的 markdown
|
||||
/// * `original_blocks` — 从 aggregate.body.blockDocument.blocks 读取的原始块
|
||||
///
|
||||
/// 返回解析后的块列表,每个块包含后续写回所需的所有字段
|
||||
fn parse_final_markdown_to_blocks(
|
||||
final_md: &str,
|
||||
original_blocks: &[Value],
|
||||
) -> Vec<ParsedBlockInfo> {
|
||||
// 1. 利用 comrak 或逐行解析提取
|
||||
// - `<!-- block:id:type:rev -->` 中的块元数据
|
||||
// - 对带 ID 的块:保留原始块的属性
|
||||
// - 对无 ID 的文本块:标记为"新块"
|
||||
// - 对 [mnote-raw-block:...] 标记:原样保留原始块
|
||||
// 2. 返回重建后的块列表
|
||||
}
|
||||
```
|
||||
|
||||
返回值 `ParsedBlockInfo`:
|
||||
|
||||
```rust
|
||||
struct ParsedBlockInfo {
|
||||
block_id: Option<String>, // None = 新块(无原始 ID)
|
||||
block_type: String, // "paragraph" / "heading" / ...
|
||||
text: String,
|
||||
block_revision_ref: Option<String>, // 从注释或原始块继承
|
||||
is_new: bool, // true = 非原始块,需要分配新 ID
|
||||
original_block: Option<Value>, // 从原始 blocks 复制(若 block_id 匹配)
|
||||
props: Option<Value>, // 从原始块保留的属性
|
||||
}
|
||||
```
|
||||
|
||||
解析步骤:
|
||||
|
||||
1. **按换行分割 markdown 行**,忽略空行
|
||||
2. **对每行提取 `<!-- block:id:type:rev -->` 注释**(正则:`<!-- block:([^:]+):([^:]+):([^: ]+) -->` 或类似)
|
||||
3. **若无注释但有 `[mnote-raw-block:id]` 标记 → 从原始 blocks 按 ID 复制**
|
||||
4. **若既无注释也无原始标记 → 创建新 paragraph 块**,无 `blockId`(由写入层生成)
|
||||
5. **从注释中获取 `block_type`;若无注释,从行前缀推断**(`## ` → heading,`- [ ] ` → todo)
|
||||
6. **用行中 `<!--` 前的部分作为文本内容**
|
||||
7. **对带 ID 的块,从原始 blocks 查找并复制 `revisionRef`、`props`**
|
||||
|
||||
#### 3.3.3 `build_page_content`(新增函数)
|
||||
|
||||
将解析后的块列表与原始 `body/content` 合并,生成最终的 Convex `content` 数组:
|
||||
|
||||
```rust
|
||||
fn build_page_content(
|
||||
original_content: &Value, // 原始的 aggregate.body.content
|
||||
parsed_blocks: &[ParsedBlockInfo],
|
||||
) -> Value {
|
||||
// 按 parsed_blocks 顺序遍历
|
||||
// - 若 block_id 在 original 中存在:
|
||||
// 复制 original 中该块的完整结构,仅替换 text
|
||||
// - 若 block_id 为 None(新块):
|
||||
// 生成新 block_id,作为 paragraph 追加
|
||||
// - 若 original 中的复杂块不在 parsed 中:
|
||||
// 保留(markdown_edit 不主动删块)
|
||||
// 返回 Value::Array
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.4 在 `doc_markdown_edit` 中的集成(替换现有 online 写回)
|
||||
|
||||
```rust
|
||||
// 当前(doc.rs ~line 856):
|
||||
let block_ops = build_block_ops_from_markdown_edit(&blocks, &operations, applied);
|
||||
if applied > 0 && block_ops.is_empty() { ... }
|
||||
let revision = ...;
|
||||
let conflict_detection_key = ...;
|
||||
let apply_result = doc_apply_block_ops_with_meta(state, context, input, &aggregate, block_ops, revision, conflict_detection_key).await?;
|
||||
|
||||
// 替换为:
|
||||
let parsed = parse_final_markdown_to_blocks(&md, &blocks);
|
||||
let next_content = build_page_content(¤t_body_content(&aggregate), &parsed);
|
||||
let changed_blocks = build_changed_blocks_summary(&original_blocks, &parsed);
|
||||
execute_page_body_save_from_aggregate(state, context, input, &aggregate, next_content, changed_blocks).await?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 边界情况处理
|
||||
|
||||
### 4.1 原始块 ID 在最终 md 中消失
|
||||
|
||||
可能发生在:
|
||||
- 用户手动删除了 `<!-- block:id -->` 注释(极罕见,通过 AI 输出不可能)
|
||||
- 模型输出的 search/replace 跨段合并了内容
|
||||
|
||||
**策略**:保留该原始块在 `content` 中的位置。`build_page_content` 对在 original 中存在但不在 parsed 中的块,按原样复制到最终 content。
|
||||
|
||||
### 4.2 新建块(无原始 ID)
|
||||
|
||||
用户可能要求"在末尾加一段总结"。模型输出 `full_content` 或 search/replace 产生的新文本带 `<!-- block:new_paragraph -->`。解析器标记为 `is_new=true`,写入层分配新的 `blockId`。
|
||||
|
||||
**策略**:新块使用临时 ID 格式 `ai_block_{timestamp}_{counter}`,与现有 `mnote.block.insert_after` 的块 ID 风格一致。
|
||||
|
||||
### 4.3 复杂块保留
|
||||
|
||||
resource/mindmap/table/image 块在 `blocks_to_markdown` 中被序列化为特殊标记 `[mnote-raw-block:id]`。搜索替换通常不会命中这些行(因为它们不含普通文本),但如果意外命中:
|
||||
|
||||
**策略**:在 `search_replace` 中,如果 search 路径包含了复杂块的特殊标记,整条 operation 标记为 failed。解析器遇到 `[mnote-raw-block:...]` 标记时,直接从 original blocks 复制。
|
||||
|
||||
### 4.4 搜索替换未命中任何块(applied > 0 但所有命中都是新文本)
|
||||
|
||||
当模型输出的 full_content 完全不同于原文时可能出现。
|
||||
|
||||
**策略**:`parse_final_markdown_to_blocks` 对每一行都产生块。如果没有任何 `<!-- block:id -->` 注释,所有块标记为 `is_new=true`。`build_page_content` 追加新块在后面,同时保留所有原始复杂块。
|
||||
|
||||
### 4.5 revision / conflictDetectionKey 一致性
|
||||
|
||||
`build_block_ops_from_markdown_edit` 当前依赖 `doc_apply_block_ops_with_meta` 来传递 revision。替换为直接调用 `execute_page_body_save_from_aggregate`,它从 aggregate 读取 revision/conflictDetectionKey [block.rs:1178-1189]。
|
||||
|
||||
**策略**:新路径从已经读到的 `aggregate` 中获取 revision/conflictDetectionKey,与现有路径完全一致。
|
||||
|
||||
### 4.6 dryRun 模式
|
||||
|
||||
当前 online 路径中 dry_run 由 `doc_apply_block_ops` 内部处理(不真正写入)。新路径也需要支持:
|
||||
|
||||
**策略**:若 `dryRun == true`,调用 `build_page_content` 但不调用 `execute_page_body_save_from_aggregate`,返回 diff 预览。diff 格式与当前 `docs.md:plan_update` 的 diff 格式一致。
|
||||
|
||||
---
|
||||
|
||||
## 5. 与已有代码的互动
|
||||
|
||||
### 5.1 `build_block_ops_from_markdown_edit` 的删除
|
||||
|
||||
该函数不再被 `doc_markdown_edit` 调用。它是一个私有 `fn`(仅 `doc.rs` 内部可见),唯一调用者被移除后成为死代码。
|
||||
|
||||
**策略**:直接删除函数体。保留调用处的行作为注释(`// 退役:7-27 改为 final_md→blocks 直接写回`),供后续参考。
|
||||
|
||||
### 5.2 `block_projection_blocks` 的继续使用
|
||||
|
||||
仍然需要原始 blocks 作为元数据源(提取 revisionRef、props、复杂块)。不改变。
|
||||
|
||||
### 5.3 `local_markdown_parser` 的复用
|
||||
|
||||
`parse_final_markdown_to_blocks` 可以复用 `local_markdown_parser::markdown_to_blocks` 对 GFM 结构的解析(heading、todo、code block、list),但需要用自己的逻辑提取 `<!-- block:... -->` 注释。不一定要用 comrak 的 `HtmlBlock` 解析;更可靠的方法是正则提取注释,然后从剩余内容中推断块类型。
|
||||
|
||||
**策略**:默认使用简单的行级处理(非 comrak),因为 `blocks_to_markdown` 的输出是每行一块的简单格式,不需要完整的 GFM AST。
|
||||
|
||||
> **⚠️ 跨行约束**:行级处理仅适用于 `blocks_to_markdown` 产出的单行块格式。以下情况需要 fallback 到 `local_markdown_parser::markdown_to_blocks`:
|
||||
> - 代码块(`block_text` 含 `\n`)
|
||||
> - 用户通过 `full_content` 自由书写的多段 markdown
|
||||
> - 解析跳过了 `<!-- block:id -->` 的行之间的纯段落
|
||||
>
|
||||
> fallback 策略:对无 `<!-- block:... -->` 注释的连续行,收集后一次性通过 GFM 解析器分割为多个常规块。
|
||||
|
||||
### 5.4 `changed_blocks` 摘要生成
|
||||
|
||||
当前返回的 `applyResult.changedBlocks` 由 `doc_apply_block_ops` 产生。新路径需要自己生成:
|
||||
|
||||
```rust
|
||||
fn build_changed_blocks_summary(
|
||||
original_blocks: &[Value],
|
||||
parsed_blocks: &[ParsedBlockInfo],
|
||||
) -> Vec<Value> {
|
||||
// 对比原始 blocks 和解析后的 blocks
|
||||
// 对文本改变的块输出 { op: "replace", blockId, content }
|
||||
}
|
||||
```
|
||||
|
||||
响应体中的 `changedBlocks` 字段格式不变,保持与下游消费者(SSE delta 等)的兼容。
|
||||
|
||||
---
|
||||
|
||||
## 6. 实施计划
|
||||
|
||||
### 步骤 1:增强 `blocks_to_markdown`(小)
|
||||
|
||||
**文件**: `doc.rs:574`
|
||||
|
||||
改动:
|
||||
- 注释格式从 `<!-- block:{id} -->` 改为 `<!-- block:{id}:{type}:{rev} -->`
|
||||
- 对复杂块(`editable==false` 或 `unsupportedReason!=null`)输出 `[mnote-raw-block:{id}] <!-- block:{id}:{type}:{rev} -->`
|
||||
|
||||
**风险**: 低。纯格式变更,向前兼容——旧注释格式的 md 在解析器看来只是缺类型/rev,可通过 fallback 从原始 blocks 查。
|
||||
|
||||
**测试**: 更新现有 `test_blocks_to_markdown_with_ids`,验证新格式。
|
||||
|
||||
### 步骤 2:实现 `parse_final_markdown_to_blocks`(中)
|
||||
|
||||
**新建模块**:`doc_md_to_blocks.rs` 或放在 `doc.rs` 末尾
|
||||
|
||||
核心逻辑(~120 行):
|
||||
|
||||
```rust
|
||||
struct ParsedBlock {
|
||||
block_id: Option<String>,
|
||||
block_type: String,
|
||||
text: String,
|
||||
revision_ref: Option<String>,
|
||||
original: Option<Value>,
|
||||
is_new: bool,
|
||||
}
|
||||
|
||||
fn parse_final_markdown_to_blocks(md: &str, originals: &[Value]) -> Vec<ParsedBlock> {
|
||||
// 1. 为 originals 建立 block_id → Value 的 HashMap
|
||||
// 2. 将 md 按行分割
|
||||
// 3. 对每行:
|
||||
// a. 用正则提取 <!-- block:id:type:rev --> 或 [mnote-raw-block:id]
|
||||
// b. 若无注释 → is_new=true, type="paragraph"
|
||||
// c. 若有注释 → 查 originals_map 继承 revision_ref/props
|
||||
// d. 提取 <!-- 前的纯文本
|
||||
// 4. 判断块类型:若注释中有 type 则用注释的,否则从行前缀推断
|
||||
// 5. 返回 ParsedBlock 列表
|
||||
}
|
||||
```
|
||||
|
||||
**正则示例**:
|
||||
|
||||
```rust
|
||||
lazy_static! {
|
||||
static ref BLOCK_COMMENT_RE: Regex = Regex::new(
|
||||
r"<!--\s*block:([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]*)\s*-->"
|
||||
).unwrap();
|
||||
static ref RAW_BLOCK_RE: Regex = Regex::new(
|
||||
r"\[mnote-raw-block:([a-zA-Z0-9_-]+)\]"
|
||||
).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
**测试**:
|
||||
- `test_parse_empty_md` → 空输入 → 空输出
|
||||
- `test_parse_with_block_ids` → 包含 `<!-- block:b1:paragraph:r1 -->` → 返回正确 ParsedBlock
|
||||
- `test_parse_no_ids` → 纯文本 → 全部 is_new
|
||||
- `test_parse_raw_block` → `[mnote-raw-block:b3]` → 从 originals 复制
|
||||
- `test_parse_type_from_prefix` → `## Title` → type="heading"
|
||||
- `test_parse_todo_prefix` → `- [x] Done` → type="todo"
|
||||
|
||||
### 步骤 3:实现 `build_page_content`(中)
|
||||
|
||||
**文件**: 与步骤 2 同模块(~80 行)
|
||||
|
||||
```rust
|
||||
fn build_page_content(original_content: &Value, parsed: &[ParsedBlock]) -> Value {
|
||||
// 1. 为 original_content 建立 block_id → full_block 映射
|
||||
// 2. 遍历 parsed_blocks:
|
||||
// - 有 block_id 且 original 中存在 → 复制 original 块,更新 text
|
||||
// - 无 block_id → 创建新 paragraph 块
|
||||
// - 有 block_id 但不在 original 中 → 复制 original 中能找到的(从已处理集合中移除)
|
||||
// 3. 遍历 original_content 中的复杂块(不在 parsed 中出现的)→ 追加
|
||||
// 4. 返回 Value::Array
|
||||
}
|
||||
```
|
||||
|
||||
**测试**:
|
||||
- `test_build_content_preserves_unmodified_blocks`
|
||||
- `test_build_content_updates_text`
|
||||
- `test_build_content_preserves_complex_blocks`
|
||||
- `test_build_content_new_blocks_appended`
|
||||
|
||||
### 步骤 4:替换 `doc_markdown_edit` 的 online 写回路径(小)
|
||||
|
||||
**文件**: `doc.rs:854-900`
|
||||
|
||||
将现有的:
|
||||
```rust
|
||||
let aggregate = aggregate_value(state, context, input).await?;
|
||||
let blocks = block_projection_blocks(&aggregate);
|
||||
let block_ops = build_block_ops_from_markdown_edit(&blocks, &operations, applied);
|
||||
if applied > 0 && block_ops.is_empty() { ... }
|
||||
let apply_result = doc_apply_block_ops_with_meta(...).await?;
|
||||
```
|
||||
|
||||
替换为:
|
||||
```rust
|
||||
let aggregate = aggregate_value(state, context, input).await?;
|
||||
let blocks = block_projection_blocks(&aggregate);
|
||||
let parsed = parse_final_markdown_to_blocks(&md, &blocks);
|
||||
let original_content = current_body_content(&aggregate);
|
||||
let next_content = build_page_content(&original_content, &parsed);
|
||||
let changed_blocks = build_changed_blocks_summary(&blocks, &parsed);
|
||||
let apply_result = execute_page_body_save_from_aggregate(
|
||||
state, context, input, &aggregate, next_content, changed_blocks
|
||||
).await?;
|
||||
```
|
||||
|
||||
新增 `build_changed_blocks_summary`(~30 行)。
|
||||
|
||||
**风险**: 中。第一次替换时需要与现有测试对照输出,确保 changed_blocks 格式一致。
|
||||
|
||||
**测试**:
|
||||
- 现有 `hermes_tools_markdown_edit_*` 测试全部重新运行——断言现有行为不变
|
||||
- 新增 `test_markdown_edit_online_single_replacement`:验证单块替换后 content 正确
|
||||
- 新增 `test_markdown_edit_online_full_content`:验证全文替换不再被拒绝
|
||||
- 新增 `test_markdown_edit_online_preserves_complex_blocks`:验证 mindmap 块不变
|
||||
|
||||
### 步骤 5:更新 full_content 处理(小)
|
||||
|
||||
**文件**: `doc.rs:785-800`
|
||||
|
||||
当前 `full_content` 构造一个 `search: current_md.trim(), replace: full.trim()` 的 operation。替换后不再需要这个折中——直接解析 full_content 为 blocks。
|
||||
|
||||
改动:在 `parse_final_markdown_to_blocks` 中,若没有任何 `<!-- block:id -->` 注释,所有块标记为 is_new。`build_page_content` 只追加新块 + 保留原始复杂块。
|
||||
|
||||
**测试**:
|
||||
- `test_markdown_edit_online_full_content_creates_new_blocks`
|
||||
- `test_markdown_edit_online_full_content_preserves_complex`
|
||||
|
||||
### 步骤 6:清理(小)
|
||||
|
||||
- 删除 `build_block_ops_from_markdown_edit` 函数体(私有 fn,唯一调用者已移除)
|
||||
- 在原调用位置保留注释:`// 退役:7-27 改为 final_md→blocks 直接写回`
|
||||
- 更新 manifest/guidance 文字:full_content 不再被拒绝
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试矩阵
|
||||
|
||||
| 场景 | 输入 | 期望输出 | 优先级 |
|
||||
|------|------|---------|--------|
|
||||
| 单块精确 search/replace | `{search:"第一段", replace:"新内容"}` | 文本更新,blockId/revisionRef 不变 | P0 |
|
||||
| 单块忽略空白 match | `{search:"第 一 段", replace:"新内容"}` | 文本更新(原块中"第一段"→"新内容") | P0 |
|
||||
| 多 operation 命中同块 | `[{op1},{op2}]` 同块 | 最终文本 = 两次替换累积结果 | P0 |
|
||||
| full_content | `"新全文"` | 新文本创建新块,原复杂块保留 | P0 |
|
||||
| 影响 heading | search 命中 heading 文本 | heading 类型不变,文本更新 | P1 |
|
||||
| 包含复杂块文档的替换 | 只改 paragraph 文本 | resource/mindmap 块不变 | P1 |
|
||||
| dryRun 不落盘 | `dryRun: true` | 返回 diff,无写入 | P1 |
|
||||
| revision 过期 | aggregate revision 已过期 | 冲突错误,不写入 | P1 |
|
||||
| 原始块 ID 消失 | 搜索替换删除了 `<!-- block -->` 注释 | 该原始块保留(不从 content 删除) | P2 |
|
||||
| 空 operations | `[]` | 错误码 | P2 |
|
||||
| 纯文本 md(无块注释) | `"摘要\n\n补充"` | 全部 is_new,追加到末尾 | P2 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 灰度/回滚
|
||||
|
||||
不设 feature flag。直接替换 online 写回路径。理由:
|
||||
|
||||
- 旧路径(7-24/7-25 修复后)在 `full_content` 等场景是**安全降级(拒绝写入)**,新路径是功能增强。不会出现「旧路径能写入、新路径不能」的退化。
|
||||
- 现有 `hermes_tools_markdown_edit_*` 测试集 + 新增 5 个测试组合足够兜底。
|
||||
- 若确实需要回滚,用 `git revert` 回退本次改动即可。
|
||||
|
||||
灰度策略:默认开启,观察到新增测试全部 pass 后合入主线。
|
||||
|
||||
---
|
||||
|
||||
## 9. 关联文件清单
|
||||
|
||||
| 文件 | 变更类型 | 变更内容 |
|
||||
|------|---------|---------|
|
||||
| `doc.rs` `blocks_to_markdown` | 修改 | 注释格式增强 |
|
||||
| `doc.rs` `doc_markdown_edit` | 修改 | online 写回路径 |
|
||||
| 新建 `doc_md_to_blocks.rs` | 新增 | `parse_final_markdown_to_blocks` + `build_page_content` + `build_changed_blocks_summary` |
|
||||
| `doc.rs` `build_block_ops_from_markdown_edit` | 标记 | `#[deprecated]` |
|
||||
| `hermes_tools.rs` tests | 新增 | ~5 个新测试 |
|
||||
| `manifest.rs` / guidance.md | 微调 | full_content 不再标记为拒绝 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 开放问题
|
||||
|
||||
1. **`<!-- block:id:type:rev -->` 注释在行中可能被模型生成的 search/replace 破坏**。例如模型输出 `{search: "第一段 <!-- block:", replace: "新段"}` 前半个注释。这是用户级错误(模型错误地替换了元数据),写入层应在 parse 阶段检测不完整的注释并报错。
|
||||
2. **`full_content` 场景下的块类型保留**。如果用户要求"把整个文档改写成大纲格式",模型输出不包含 `<!-- block-->` 注释的全部新文本,此时 heading/todo 类型从 GFM 前缀推断。但列表、引用、代码块等格式需确认 `local_markdown_parser` 的 `markdown_to_blocks` 返回的结构是否足够完整。
|
||||
3. **行内格式(bold、italic、link)能否在往返中保持**。当前 `blocks_to_markdown` 只输出文本(`block_text`),丢弃所有 marks。在线文档的 text 存储包含 marks(bold/italic/code/link),当前 `doc_markdown_edit` 的整体设计不保证行内格式——这超出了 7-24/7-25 的范围,但建议在 7-14 的 Phase A/B 之后评估。
|
||||
@@ -0,0 +1,121 @@
|
||||
# 7-29 [done] Batch I AI tool final-content / ACP tail 收口 checklist v1
|
||||
|
||||
> 创建时间:2026-05-21
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> 上位入口:`design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
|
||||
>
|
||||
> 阶段:Batch I / 1-8 Batch E AI 与资源工具收口
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本批次只处理 `1-8` Batch E 中已经有明确代码痕迹、但文档状态仍在 `process/` 的 AI 工具尾项:
|
||||
|
||||
1. 复核 `7-27`:`mnote.doc.markdown_edit` 在线写回是否已经以最终 markdown 为真源,是否可归档。
|
||||
2. 复核 `7-15`:ACP runtime 统一层 Step 15-17 的真实剩余缺口,拆成下一批可执行小任务。
|
||||
3. 复核 `7-12`:Manifest / review surface / state event 中哪些已由当前 Hermes tools / ACP runtime 覆盖,哪些仍冻结或待拆。
|
||||
|
||||
本批次不实现 Phase C Review Mode,不扩新 AI 产品面,不恢复旧 HTTP proxy 为主路径,不把 `mnote.doc.markdown_edit` 升回 local-first 普通正文默认主路径。
|
||||
|
||||
## 2. 已有本地证据
|
||||
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --test-threads=1`:16 passed。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_manifest_describes_markdown_edit_write_contract -- --test-threads=1`:1 passed。
|
||||
- `rust/crates/mnote-web/src/hermes_tools/doc.rs` 已包含 `parse_final_markdown_to_blocks`、`build_page_content`、`build_changed_blocks_summary`、`mnote.doc.markdown_edit (7-27)` 写回 reason。
|
||||
|
||||
## 3. Reasonix Worker 拆分
|
||||
|
||||
### Worker A:`7-27` 归档性审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md`
|
||||
- 若确认可归档,可移动到 `design/07-ai/done/7-27-online-markdown-writeback-final-content-truth-v2.md`
|
||||
|
||||
只读参考:
|
||||
|
||||
- `rust/crates/mnote-web/src/hermes_tools/doc.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
|
||||
目标:
|
||||
|
||||
- 对照 `7-27` 的步骤 1-6、测试矩阵、开放问题,判断当前代码是否已满足归档条件。
|
||||
- 如果可归档,只更新设计文档状态并移动到 `done/`。
|
||||
- 如果不可归档,只写明最小剩余缺口,不改 runtime 代码。
|
||||
|
||||
验收:
|
||||
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --test-threads=1`
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_manifest_describes_markdown_edit_write_contract -- --test-threads=1`
|
||||
- `git diff --check`
|
||||
|
||||
### Worker B:`7-27` 代码/测试缺口审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-i-worker-b-7-27-code-test-gap.md`
|
||||
|
||||
只读参考:
|
||||
|
||||
- `rust/crates/mnote-web/src/hermes_tools/doc.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
|
||||
目标:
|
||||
|
||||
- 不修改代码。
|
||||
- 审查当前 `7-27` 实现是否存在明显缺口:注释格式、revisionRef 保留、legacy `content` / `contentNodes` 文本读取、复杂块保留、selection 范围、dryRun、full_content。
|
||||
- 输出“必须修复才能归档 / 可作为后续增强 / 无问题”的分级表。
|
||||
|
||||
验收:
|
||||
|
||||
- 结果文件必须引用具体文件和函数。
|
||||
- `git diff --check`
|
||||
|
||||
### Worker C:`7-15 / 7-12` 后续拆分审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-i-worker-c-acp-review-tail.md`
|
||||
|
||||
只读参考:
|
||||
|
||||
- `design/07-ai/process/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
- `rust/crates/mnote-web/src/acp_client.rs`
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/acp_runtime.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
|
||||
目标:
|
||||
|
||||
- 不修改代码和主设计文档。
|
||||
- 判断 `7-15` Step 15-17 是否仍是当前应执行项,还是应该拆成压力测试、旧 HTTP proxy 瘦身、Reasonix cache benchmark 三个独立 checklist。
|
||||
- 判断 `7-12` Phase B-F 中哪些已由当前 manifest / runtime selector / tool guard 覆盖,哪些仍冻结。
|
||||
- 输出下一批建议顺序和可派发 worker 方向。
|
||||
|
||||
验收:
|
||||
|
||||
- 结果文件必须区分 P0/P1/P2。
|
||||
- 不把 Phase C Review Mode 解冻。
|
||||
- `git diff --check`
|
||||
|
||||
## 4. Codex 复核项
|
||||
|
||||
- [x] 读取 Worker A/B/C 的 `final.md` / `result.json` / diff。
|
||||
- [x] 独立复核 `7-27` 相关测试,不凭 Worker A 归档结论直接验收。
|
||||
- [x] 若 Worker B 找到必须修复缺口,由 Codex 本地补最小修复或拆下一轮 worker。
|
||||
- [x] 若 `7-27` 可归档,更新 `1-8` Batch E 当前状态。
|
||||
- [x] 根据 Worker C 结果决定 Batch I 下一步拆分。
|
||||
- [ ] 运行 `git diff --check`、必要 targeted tests、`codegraph sync .`。
|
||||
|
||||
## 5. 本轮执行记录
|
||||
|
||||
- 2026-05-21:Codex 建立 Batch I checklist,准备派发 Worker A/B/C。
|
||||
- 2026-05-21:Worker A/B 均确认 `7-27` 可归档;Codex 复跑 `markdown_edit` 与 manifest write contract targeted tests 后确认无 P0 缺口,`7-27` 已移动到 `design/07-ai/done/`。
|
||||
- 2026-05-21:Worker B 记录的 `revisionRef` 注释可见性、复杂 GFM fallback、多余退役函数体均列为后续增强,不阻塞归档。
|
||||
- 2026-05-21:Worker C 确认 `7-15` Step 15-17 与 `7-12` Phase B/F 应拆成独立稳定化 checklist;Phase C Review Mode 继续冻结。
|
||||
- 2026-05-21:下一批不在本文件继续膨胀,改拆 `7-34` 处理 ACP runtime cleanup / availability / 稳定性验证尾项。
|
||||
@@ -0,0 +1,138 @@
|
||||
# 7-34 [done] ACP runtime cleanup / availability / stability 尾项 checklist v1
|
||||
|
||||
> 创建时间:2026-05-21
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> 上位入口:`design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
|
||||
>
|
||||
> 来源:`design/07-ai/process/7-29-batch-i-ai-tool-final-content-acp-tail-closure-v1.md`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本清单承接 `7-29` 的 Batch I 结论,只处理 ACP runtime 与 Hermes tool contract 的尾项收口:
|
||||
|
||||
1. 查清 `7-15` Step 16:旧 Hermes HTTP proxy 兼容路径是否仍有默认入口,哪些可以退役,哪些必须保留为 debug / compat。
|
||||
2. 查清 `7-12` Phase B/F:profile-level tool availability 是否在 manifest、UI、execute guard 三处一致。
|
||||
3. 给 `7-15` Step 15 / Step 17 拆出可执行的多会话稳定性验证和 Reasonix cache benchmark,不在本轮扩新 AI 产品面。
|
||||
|
||||
本清单不解冻 Phase C Review Mode,不新增审阅 UI,不把 `mnote.doc.markdown_edit` 升回 local-first 普通 Markdown 默认主路径。
|
||||
|
||||
## 2. 当前已知事实
|
||||
|
||||
- `7-27` 已归档,`mnote.doc.markdown_edit` 在线写回已以最终 markdown 为真源。
|
||||
- ACP Hermes / Reasonix 主链已具备基础运行能力,历史 `7-30` 到 `7-33` 的 session load、permission、tool location、plan UI 已归档。
|
||||
- `hermes_client.rs` 中仍可能保留旧 HTTP proxy 兼容分支;是否仍被默认 runtime 触达需要本轮确认。
|
||||
- `hermes_tools/manifest.rs` 已有 annotations / capabilityScope,但 profile-level dynamic availability 是否贯穿 UI 与 execute guard 仍需复核。
|
||||
|
||||
## 3. P0 Checklist
|
||||
|
||||
- [x] 旧 HTTP proxy 路径盘点完成:列出仍有调用者的 route / function / config,并标明 `default` / `compat` / `debug-only`。
|
||||
- [x] 确认 `page_ai_workflow.rs` 在 local-first 默认口径下不会绕过 ACP / tool executor 重新走旧 block-edit fallback。
|
||||
- [x] `7-15` 文档更新为“核心 ACP runtime 已完成,Step 15-17 拆到 7-34”,避免继续显示为整体未完成。
|
||||
- [x] `7-12` Phase B/F 状态更新:已覆盖项、冻结项、待实现项分开写清楚。
|
||||
|
||||
## 4. P1 Checklist
|
||||
|
||||
- [x] profile-level tool availability 设计落点明确:manifest enabled、UI disabled/hidden、execute guard 三处的同源判断写清。
|
||||
- [x] 多会话稳定性测试方案明确:至少覆盖 3 个并发 ACP session、cancel、子进程异常、事件去重。
|
||||
- [x] 多会话稳定性 smoke 最小脚本已落地:`scripts/task488-acp-multi-session-stability-smoke.js`,覆盖 session/run/events/abort API 与 SSE terminal event 证据。
|
||||
- [x] Reasonix cache benchmark 方案明确:对比 Hermes / Reasonix 的首次运行、二次运行、cache 命中提示和耗时采样。
|
||||
|
||||
## 5. P2 / 后续增强
|
||||
|
||||
- [ ] `7-27` 后续增强是否单独立项:`revisionRef` 注释可见性、复杂 GFM fallback、多余退役函数体删除。
|
||||
- [ ] 若旧 HTTP proxy 不能立即删除,补 `debug/internal only` 标记和 route 文档。
|
||||
- [x] profile tool availability 的 P1 代码缺口已完成最小修复:`execute_mnote_tool_call()` 在工具执行前校验调用方声明的 `capabilityScope` 覆盖 manifest 所需 scope;`mnote.page.get` 补齐只读 annotations。
|
||||
|
||||
## 6. Reasonix Worker 拆分
|
||||
|
||||
### Worker A:旧 HTTP proxy / fallback 调用链审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-j-worker-a-http-proxy-fallback.md`
|
||||
|
||||
只读范围:
|
||||
|
||||
- `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- `rust/crates/mnote-web/src/page_ai_workflow.rs`
|
||||
- `rust/crates/mnote-web/src/acp_runtime.rs`
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/routes/mod.rs`
|
||||
|
||||
要求:
|
||||
|
||||
- 不修改代码。
|
||||
- 输出旧 HTTP proxy / page AI fallback 的真实调用链、默认入口、环境变量或 profile 开关。
|
||||
- 分级为:必须退役 / compat 保留 / debug-only / 未被调用。
|
||||
|
||||
### Worker B:tool availability 三处一致性审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-j-worker-b-tool-availability.md`
|
||||
|
||||
只读范围:
|
||||
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/*.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
|
||||
要求:
|
||||
|
||||
- 不修改代码。
|
||||
- 审查 manifest / UI / execute guard 是否消费同一能力判断。
|
||||
- 输出 P0 缺口和建议测试名,不直接实现。
|
||||
|
||||
### Worker C:ACP 稳定性与 benchmark checklist 草案
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-j-worker-c-acp-stability-benchmark.md`
|
||||
|
||||
只读范围:
|
||||
|
||||
- `design/07-ai/process/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
|
||||
- `scripts/`
|
||||
- `rust/crates/mnote-web/src/acp_*.rs`
|
||||
|
||||
要求:
|
||||
|
||||
- 不修改代码。
|
||||
- 给出多会话稳定性 smoke / benchmark 的最小脚本方案、浏览器验证点、输出证据格式。
|
||||
- 不要求立即实现压测脚本。
|
||||
|
||||
## 7. Codex 复核项
|
||||
|
||||
- [x] 等待 Worker A/B/C completion hook,单次最长 30 分钟,不短轮询。
|
||||
- [x] 独立核查 Worker 结论涉及的关键调用链。
|
||||
- [x] 更新本清单 P0/P1 状态。
|
||||
- [x] 若出现 P0 代码缺口,拆下一轮实现 worker 或由 Codex 本地最小修复。
|
||||
- [x] 跑 `git diff --check` 与相关 targeted tests。
|
||||
- [x] 更新 `1-8` Batch E 状态。
|
||||
|
||||
## 8. 本轮执行记录
|
||||
|
||||
- 2026-05-21:Codex 创建 `7-34`,准备派发 Worker A/B/C 做只读审查与下一步测试方案拆分。
|
||||
- 2026-05-21:Worker A/B/C 均通过 completion hook 回传。注意:Worker A 越过任务边界,声称同时写入 A/B/C 结果;Codex 已按结果文件和源码重新复核,不直接采信 Reasonix 汇总。
|
||||
- 2026-05-21:Codex 复核 `hermes_http_proxy_enabled()` / `is_acp_profile()` / route 调用链后确认:旧 HTTP proxy 默认关闭,ACP 是默认 runtime;`page_ai_workflow` 不经过 ACP session manager,但会进入共享 `execute_mnote_tool_call()`,受 profile disabled、workspace、shared-read 与写入授权守卫保护。本轮无 P0 退役阻塞。
|
||||
- 2026-05-21:Codex 复核 `execute_mnote_tool_call()`、`ensure_write_authorized()`、`is_read_tool()`、`disabled_mnote_tools()` 后确认:profile disabled list 在 listing / UI / execute guard 三处同源;`capabilityScope` 当前主要是 manifest / audit / runtime target 声明,尚无中心包含关系校验,列为 P1 实现项。
|
||||
- 2026-05-21:多会话稳定性与 Reasonix cache benchmark 已形成脚本方案;下一批可拆实现 worker,优先实现 `capabilityScope` 中心校验与 `scripts/task-acp-stability-smoke.js`,cache benchmark 作为 P2 度量。
|
||||
- 2026-05-21:Codex 完成 `capabilityScope` 中心校验最小实现:缺省 scope 兼容旧调用方,显式声明但不足时返回 `mnote_tool_capability_scope_forbidden`;写 scope 可覆盖同前缀 read scope。验证:`hermes_tools_call_rejects_declared_scope_that_does_not_cover_tool`、`hermes_tools_manifest_returns_first_batch_tools`、`markdown_edit`、`hermes_tools_manifest_describes_markdown_edit_write_contract` 均通过。
|
||||
- 2026-05-21:Codex 新增 `scripts/task488-acp-multi-session-stability-smoke.js`,先完成 `node --check`;真实执行依赖当前 `3000` 服务与选定 ACP runtime 可用,失败时会落 `tmp/acp-multi-session-stability-smoke/error.json`,成功时落 `result.json` 与每个 run 的事件 JSON。
|
||||
- 2026-05-21:Codex 修复 `task488` smoke 的 SSE 读取策略:从 Playwright `context.request.fetch` 改为 Node 原生 `fetch` + 登录 cookie,读取到 `run.completed` / `run.failed` / `run.aborted` terminal event 后主动关闭流,避免 Playwright context 关闭造成假失败。
|
||||
- 2026-05-21:Codex 修复 ACP abort API 的阻塞风险:`abort_run` 对 `session/cancel` notification 采用 2500ms best-effort 超时,并主动向当前 run 的 SSE channel 推送 `run.aborted`;后续 prompt 结束时若 runtime 已 abort,不会再覆盖为 completed。
|
||||
- 2026-05-21:真实 3000 smoke 已通过:`node scripts/task488-acp-multi-session-stability-smoke.js` 创建 3 个 `reasonix` ACP session / run,取消第 2 个 run,最终事件为 `run.completed`、`run.aborted`、`run.completed`;证据写入 `tmp/acp-multi-session-stability-smoke/result.json` 和 `events-*.json`。
|
||||
|
||||
## 9. 归档说明
|
||||
|
||||
本清单 P0/P1 已完成并有代码与 smoke 证据,归档到 `design/07-ai/done/`。
|
||||
|
||||
仍保留为后续增强的事项:
|
||||
|
||||
- `7-27` 的 `revisionRef` 注释可见性、复杂 GFM fallback、多余退役函数体删除。
|
||||
- 旧 Hermes HTTP proxy 的更细 debug/internal only 文档标记。
|
||||
- Reasonix cache benchmark 的长期耗时采样;当前只冻结方案,不作为 1-8 Batch E 阻塞项。
|
||||
Reference in New Issue
Block a user