Files
mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md
T
lix-2026 5f97800489 chore: align local-first control plane and editor fixes
- wire SQLite control-plane access/session paths into Rust web local-folder routes

- preserve local Markdown attachment semantics across upload, reload, and secondary-pane resource tabs

- refresh design governance docs, Reasonix task templates, and bug records

- retire root .mcp.json local MCP config
2026-05-23 23:38:42 +08:00

759 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 7-14 [recycle][process] Local-first AI Markdown 编辑路径收敛 v2
> 创建时间:2026-05-16
>
> 更新时间:2026-05-22recycle:本文仍受 CLI Main / tool-first 旧口径影响,已移入 old,不再作为当前 AI 编辑实现依据)
>
> 归档说明(2026-05-22):本文试图从旧在线 / 本地收敛稿修正为 local-first,但仍把 CLI Main 与 `mnote.doc.*` 兼容工具放在过高位置,不符合当前 SQLite control-plane + local-first `.md` + agent 原生文件 patch/diff 主线。后续应新建更小的 active 设计稿,直接基于 SQLite、allowed roots、file refs、watcher、BufferStore、Page Aggregate 前台同步和审计链设计。
>
> 2026-05-18 口径补充:
> - local-first workspace 已成为早期产品默认形态;本地 `.md` 是默认 AI 编辑目标;旧 Convex 文档只作为历史迁移源或显式 compat/source replica 边界。
> - 本文早期把 `mnote.doc.markdown_edit` 描述为统一主路径;最新口径改为:local-first 普通 Markdown 编辑优先给 agent 授权文件引用,由 agent 使用自身成熟的 diff / apply_patch / 文件编辑能力完成;`mnote.doc.markdown_edit` 保留为显式 remote agent / compat fallback。
> - 页面内图片 / 附件上传已在 local source 下写入 `{mdBase}.assets/` 并保存相对 Markdown 路径,AI 后续处理附件引用时也应保留相对路径,不改写为历史 media asset。
>
> 2026-05-22 口径纠正:
> - 当前 Convex runtime / functions 已退役,不再作为当前正文主链、默认控制面、AI 会话主存储或新增能力目标。
> - 本文中的“在线文档”仅可理解为历史迁移源、显式 cloud compat / remote agent 边界或未来 sync replica,不再与 local-first `.md` 并列为当前主路径。
> - 当前主线应写成:本地 `.md` 文件 + Rust SQLite control-plane + allowed roots / file references + agent 原生 patch/diff + watcher / BufferStore / Page Aggregate 前台同步。
>
> 当前状态:`PROCESS`
>
> 本稿目的:
> 1. 纠正 7-9 / 7-10 / 7-12 / 7-13 中隐含的「块级编辑是 AI 唯一写入路径」假设
> 2. 基于 CLI Main 参考实现,确立 mnote 的「agent 原生文件 patch/diff 为 local-first 默认路径,MNote 文本级兼容工具 + 块级结构性操作为 fallback / 辅助」两层模型
> 3. 规划 BlockNote AI 流式/review 能力的远期方向(当前不实施)
> 4. 明确历史 cloud / remote / compat 文档边界,并把当前 AI 读取、权限、冲突与回读口径收口到 local-first `.md` 文件
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-13-page-block-editor-runtime-actor-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-22-local-folder-convex-unified-tree-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/bugs/07-ai/process/page-ai-block-edit-model-fallback-missing-content.md`
>
> 参考实现(已分析):
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main/` — **主参考**skill 系统、scope/detail 上下文、两层操作、工作流、参考文件模式、CI 校验
> - `skills/lark-doc/SKILL.md` + `references/`
> - `skill-template/`master-skill-template.md、skill-template.md
> - `shortcuts/doc/`、`internal/skillscheck/`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocknote-ai/packages/xl-ai/` — 远期参考:流式/review
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-ai-autocomplete/` — 独立功能:内联补全
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
---
## 1. 结论
**当前事实源是 local-first `.md` 文件,Tiptap 只是块级 UI 表现层。** Convex 相关正文链已经退役为历史 / compat 边界,不能再和本地 `.md` 并列描述为当前主路径。既然默认对象已经是普通 Markdown 文件,就不需要再为常规正文编辑发明一套 MNote 专用块级工具。当前 07-ai 设计把 AI 编辑契约钉死在 `EditorBlockDocument` 的块级操作上,导致三个连锁问题:
1. **AI 被迫在块级操作**:对「改写这段话」「补充一段总结」「调整语气」「把所有 TODO 改成 DONE」等自然请求,AI 必须产出 `{ op: "replace", blockId: "block_1", ... }` 格式的块操作,而不能直接产出修改后的文本或搜索替换对。这增加了 AI 的认知负担和出错概率。
2. **旧块级工具不适合 local-first `.md`**:本地文件没有稳定的 `blockId`(每次解析重新分配),因此块级工具不能作为普通 Markdown 编辑默认路径。
3. **历史 compat 容易污染当前主线**:如果继续把 Convex / online 文档写成并列路径,后续实现会误把 `mnote.doc.*``documents.save` 当成新增能力入口,而不是 local-first 的 fallback / 迁移边界。
### 参考实现的验证
分析了三个参考实现后,确认 mnote 的方向是正确的:
| 参考实现 | AI 编辑模型 | mnote 采纳 |
|----------|-----------|-----------|
| **CLI Main (Lark Doc)** | 文本级 `str_replace` + 块级 `block_replace/insert_after/delete/move_after` 两层操作 | ✅ **主参考**:两层模型 + AI skill 合同设计 |
| **BlockNote AI** | 纯块级 `add/update/delete`(依赖 blockId),流式 apply + suggest/review | ✅ **远期参考**:流式/review 能力,Phase C 规划 |
| **Tiptap AI Autocomplete** | 纯文本补全,单句接龙,无工具 | ⏳ 独立功能:内联 AI 补全(非本文讨论范围) |
**CLI Main 最关键的设计决策**:它不把 AI 钉死在某一层。`str_replace` 用于文本级修改(不需要 blockId),`block_*` 用于精确块结构调整。格式上,**XML 用于精确编辑场景,Markdown 用于整篇导入/导出**。mnote 的不同之处在于:local-first 之后,默认对象就是本地 `.md` 文件,因此可以直接复用 Codex / Hermes / Reasonix 自身成熟的 diff、apply_patch、文件编辑能力;MNote 的职责收口为权限沙箱、文件引用解析、审计和刷新。
### 正确方向
> mnote 的 AI 编辑路线:**本地授权文件 + agent 原生 patch/diff 为主;MNote 兼容工具仅用于显式 cloud/remote compat 或结构化辅助;BlockNote AI 的流式/review 只作为远期交互参考。**
具体:
- **当前实施**Phase A/B):local-first 页面 AI 只传当前文件引用、可选 selection 和用户指令;MNote 校验 `AiAccessScope` 后让 agent 在受限目录中使用原生 patch/diff 编辑 `.md``mnote.doc.markdown_edit` 只作为显式 cloud / remote agent / compat fallback`mnote.block.*` 只作为复杂结构辅助。
- **远期规划**Phase C):BlockNote AI 的流式增量 apply + suggest/review 层。当前先设计,不实施。
---
## 2. 参考实现详解
### 2.1 CLI MainLark Doc)— 两层操作模型
CLI Main 的 `skills/lark-doc/SKILL.md` 是飞书文档的 AI skill 定义。它把文档操作分成两层:
**文本级操作**(不需要 blockId):
```
docs +update --command str_replace --search "原文片段" --replace "新文本"
```
AI 只需要匹配文本,不需要知道块在哪儿。适用于所有内容修改场景。
**块级操作**(需要 blockId):
```
docs +update --command block_replace --block-id "block_1" --content "<xml>...</xml>"
docs +update --command block_insert_after --block-id "block_1" --content "<xml>...</xml>"
docs +update --command block_delete --block-id "block_1"
docs +update --command block_move_after --block-id "block_3" --target-block-id "block_1"
```
适用于精确块结构调整。AI 需要知道 `blockId`,因此依赖前置的 `docs +fetch` 返回带 id 的内容。
**格式选择规则**CLI Main 的 skill 中内联):
| 场景 | 格式 | 原因 |
|------|------|------|
| 精确块编辑(替换/插入/删除指定块) | XML | XML 保留块 id、类型、属性 |
| 整篇文档导入/导出 | Markdown | 人类可读,和 `.md` 文件互通 |
| AI 对话中的内容引用 | Markdown | 模型理解 markdown 远好于 XML |
**对 mnote 的启示**
1. **两层操作模型直接适用**。mnote 的 `markdown_edit` = CLI Main 的 `str_replace`(文本级),`apply_block_ops` = CLI Main 的 `block_*`(块级)。
2. **但 mnote 的主格式是 markdown 而非 XML**。当前 local-first 正文事实源就是本地 `.md`,历史 cloud / compat 内容也只能先投影为 markdown 后进入兼容工具。所以我们不需要 XML 这一层——markdown 既是 AI 编辑格式,也是人类可读格式。
3. **CLI Main 的 skill 格式**YAML frontmatter + Markdown body,定义 tool shortcuts + 上下文格式规范)值得学习。mnote 的 Hermes `mnote` plugin 可以用类似方式组织。
#### 2.1.1 Skill YAML 前端元数据
> 参考文件:`skill-template/master-skill-template.md`、`skill-template/skill-template.md`、`scripts/skill-format-check/index.js`
每个 skill 文件以 YAML frontmatter`---` 包裹)开头:
```yaml
---
name: lark-doc
description: "飞书云文档 / Docx / 知识库 Wiki 文档(v2):创建、打开、读取..."
metadata:
requires:
bins: ["lark-cli"]
cliHelp: "lark-cli docs --api-version v2 --help; ..."
---
```
CI 自动化校验(`skill-format-check.yml` + `scripts/skill-format-check/index.js`)确保 `name``description` 必填。
**→ mnote 采纳**Hermes 的 `mnote` plugin 已采用 YAML frontmatter`/home/lix/.hermes/plugins/mnote/plugin.yaml`)。在此基础上规范化:
- `name``mnote`
- `description`:一段完整的 skill 描述,含覆盖的工具和适用场景
- `metadata.domain``mnote-doc`(文档域),后续可扩展 `mnote-tree``mnote-file`
- CI 校验:确保每个 plugin 的 `plugin.yaml` 含必填字段
#### 2.1.2 Scope / Detail 上下文控制
> 参考文件:`skills/lark-doc/references/lark-doc-fetch.md`、`shortcuts/doc/docs_fetch_v2.go`
CLI Main 的 `docs +fetch` 支持五个 `--scope` 级别,精确控制注入 AI 上下文的文档量:
| `--scope` | 返回内容 | AI 场景 |
|-----------|---------|--------|
| `outline` | 标题树(h1-hN+ blockId | "给我看目录" |
| `section` | 指定标题下的完整节 | "精读第三章" |
| `range` | blockId 区间 | "给我 100-200 行" |
| `keyword` | 关键词周围最小片段 | "找提到 deployment 的地方" |
| `full`(默认) | 全文 | 最后手段 |
三个 `--detail` 级别控制元数据量:
| `--detail` | blockId | 样式属性 | AI 场景 |
|-----------|---------|---------|--------|
| `simple` | ❌ | ❌ | 只读、总结 |
| `with-ids` | ✅ | ❌ | 需要精确寻址时 |
| `full` | ✅ | ✅ | 即将编辑该节 |
片段包装:`<fragment requested-start="..." requested-end="...">``<excerpt top-block-id="..." parent-block-path="...">` 标记告知 AI"这是部分视图,非完整块"。
**→ mnote 采纳**(高优先级):
```
mnote.doc.fetch({
scope: "section" | "outline" | "keyword" | "full",
detail: "simple" | "with_ids" | "full",
format: "markdown",
maxChars: 8000
})
```
- `scope` 的四级映射:outline(标题树)、section(当前节)、keyword(搜索关键词)、full(全文)
- `detail` 的三级映射:simple(纯文本)、with_ids(带块标记)、full(带属性)
- 片段包装:markdown 中用 `<!-- fragment: section "标题" -->` 注释标记部分视图边界
- 实现位置:`mnote-web``mnote.doc.fetch` handler
#### 2.1.3 更新工作流:Code-Act Loop
> 参考文件:`skills/lark-doc/references/style/lark-doc-update-workflow.md`
CLI Main 的文档编辑遵循 **Plan → Execute → Observe → Iterate** 四步循环:
```
1. Plan(先读后改):
docs +fetch --scope section --detail full
→ 分析当前状态 → 制定操作序列
2. Execute(精准手术,不全量覆盖):
默认用 str_replace / block_insert_after / block_delete
block_move_after 用于重排
overwrite 仅在有明确指令时使用
append + block_delete 组合优于 overwrite
3. Observe(每次写后回读):
docs +fetch --scope section
→ 确认修改正确
4. Iterate(修复差异):
如发现偏差 → 回到 Plan
```
更新命令决策树:
```
需要修改文本内容(不改块结构)?
→ str_replace(文本级,不需要 blockId
需要整段替换?
→ block_replace --block-id xxx(需要先 fetch --detail with_ids
需要插入/删除?
→ block_insert_after / block_delete
需要重排结构?
→ block_move_after / block_copy_insert_after
```
**→ mnote 采纳**(当前 Hermes 的 `mnote` plugin 应内置此工作流):
- Hermes `mnote` skill 的 `SKILL.md` 中内联 Code-Act Loop 指导
- `mnote.doc.markdown_edit`(文本级搜索替换)优先于 `mnote.doc.apply_block_ops`(块级精确操作)
- 模型 system prompt 追加:"永远不要在不确定时使用全文覆盖;优先搜索替换;每次写入后回读确认"
#### 2.1.4 参考文件分离模式
> 参考文件:`skills/lark-doc/references/`24 个独立 .md 文件)
CLI Main 把详细工具规格从主 `SKILL.md` 中分离到 `references/` 子目录:
```
skills/lark-doc/
SKILL.md ← 技能概述 + 决策表 + 路由规则
references/
lark-doc-fetch.md ← fetch 工具详细规格
lark-doc-update.md ← update 工具详细规格
lark-doc-xml.md ← XML 块语法参考
lark-doc-md.md ← Markdown 格式规则
style/
lark-doc-update-workflow.md ← 编辑工作流
lark-doc-style.md ← 写作风格指南
```
**→ mnote 采纳**Hermes `mnote` plugin 应采用相同结构:
```
~/.hermes/plugins/mnote/
plugin.yaml ← YAML 前端元数据
SKILL.md ← 技能概述 + 工具决策表
references/
mnote-doc-fetch.md ← fetch 详细规格(scope/detail/format
mnote-doc-update.md ← 更新命令规格(search/replace + block ops
mnote-doc-workflow.md ← Code-Act Loop
mnote-doc-context.md ← 上下文冻结与格式化规则
```
#### 2.1.5 跨域资源路由
> 参考文件:`skills/lark-doc/SKILL.md`(嵌入式资源路由表)
CLI Main 的 lark-doc skill 定义了嵌入式资源的显式路由表:
```
| 嵌入标签 | 提取字段 | 代理 skill |
|-----------------------------|--------------------------|---------------|
| <sheet token="..." ...> | token → spreadsheet_token | lark-sheets |
| <bitable token="..." ...> | token → app_token | lark-base |
| <whiteboard token="..."> | board_token | lark-whiteboard|
```
**→ mnote 采纳**(远期,当文档内嵌入其他资源类型时):
- local-first Markdown 中嵌入的思维导图资源 → 路由到 `mnote-mindmap` skill
- 嵌入的附件/文件 → 路由到 `mnote-file` skill
- 当前 Phase A 不实施,预留路由表字段
#### 2.1.6 格式策略:XML vs Markdown
> 参考文件:`skills/lark-doc/references/lark-doc-xml.md`、`lark-doc-md.md`
CLI Main 的格式选择规则:
| 场景 | 格式 | 原因 |
|------|------|------|
| 精确块编辑 | XML(默认) | 保留 blockId、样式属性、结构 |
| 整篇导入/导出 | Markdown | 人类可读,和 `.md` 互通 |
| AI 对话引用 | Markdown | 模型理解 markdown 远好于 XML |
**→ mnote 采纳**CLI Main 用 XML 做默认格式是因为飞书文档本身是 XML 存储。mnote 当前主存储是 local-first `.md` 文件,历史 cloud / compat 内容也必须先投影成 markdown 后再进入 AI 编辑面,因此 markdown 是 mnote 的默认和唯一 AI 格式。这是正确的差异化决策。
#### 2.1.7 Skill CI 校验
> 参考文件:`.github/workflows/skill-format-check.yml`、`scripts/skill-format-check/`
CLI Main 的 CI 自动检查每个 `SKILL.md`
1.`---\n` 开头
2. YAML frontmatter 语法有效
3. `name``description` 必填
4. `metadata` 缺失仅为警告
**→ mnote 采纳**:对 Hermes `mnote` plugin 和所有 `~/.hermes/skills/note-taking/mnote-*/SKILL.md` 执行同等校验。在 CI 中增加 `skill-format-check` 步骤。
### 2.2 BlockNote AI — 流式 apply + suggest/review
BlockNote AI 的 `StreamTool` 实现了一套完整的 AI 编辑体验闭环:
**核心机制**
```
LLM 流式输出 partial JSON
→ StreamToolExecutor 逐 chunk 解析
→ 匹配 operation type → validate → execute
→ ProseMirror 事务逐条 apply(带延迟,模拟"AI 正在打字"
→ suggestChanges 标记 AI 编辑为 suggestion(红绿对比)
→ 用户逐条 accept / reject
→ 确认后才写入持久层
```
**关键组件**
| 组件 | 功能 | mnote 远期对标 |
|------|------|---------------|
| `StreamTool` | 单操作的定义:name + inputSchema + validate + execute | mnote 已有(`mnote.block.*` / `mnote.doc.*`),不需要重构 |
| `StreamToolExecutor` | 流式解析 partial JSON → 逐条 enqueue → 按顺序 execute | Phase C 新增:`StreamApplyController` |
| `suggestChanges` | ProseMirror suggestion marksAI 编辑不直接落盘,先标记为待审阅 | Phase C 新增:local-first `ReviewSession` / 临时审阅层 |
| `RebaseTool` | 协作场景:用户同时在编辑 → AI 操作 rebase 到最新文档状态 | Phase C 考虑;local-first 下优先基于 file version / BufferStore 冲突模型 |
| `delayAgentStep` | 逐条 apply 之间加 50-200ms 延迟,给用户"AI 正在操作"的可见性 | Phase C 可选改善 |
**BlockNote AI 不适合直接搬的原因**:它的所有操作都通过 `id`blockId)寻址。`add` 需要 `referenceId``update` 需要 `id``delete` 需要 `id`。这意味着:
- 本地 `.md` 文件无法使用(没有稳定 blockId)
- 跨块内容修改("把所有 TODO 改成 DONE")需要模型逐一产出 N 个带 blockId 的操作
- 这和 mnote 的"markdown 优先"方向背道而驰
**但我们可以在 markdown_edit 之上叠加流式/review**Phase C 时,`mnote.doc.markdown_edit` 的 apply 过程可以流式化——逐条 search/replace 执行后推送 BlockDelta,编辑器逐条渲染,用户可逐条撤回。这和 BlockNote AI 的体验效果一致,但底层是文本级操作而非块级操作。
### 2.3 Tiptap AI Autocomplete — 内联补全(独立功能)
纯文本补全器:发送光标前文本 → 返回下一句。不是文档编辑方案。mnote 将来可以考虑作为独立的"内联 AI 补全"功能,和本文讨论的文档编辑工具分开设计。
---
## 3. 当前问题
### 3.1 块级编辑是 AI 的过度抽象
> 参考对照:CLI Main 证明 `str_replace` 足以覆盖大多数 AI 编辑场景,不需要强迫模型理解 blockId。
当前 AI 写入链路的问题不在于技术实现,而在于**模型必须理解 blockId 这个 UI 层的概念**
```
用户: "把第一段改简洁一些"
→ 当前路径:模型 → { op: "replace", blockId: "block_1", content: "..." }
→ 期望路径:模型 → { search: "第一段原文", replace: "改写后文本" }
```
「把文章中所有 TODO 改成 DONE」这类跨块请求,当前需要逐一产出 N 个 `{ op: "replace", blockId: "..." }`。文本级 search/replace 只需一条:`{ search: "TODO", replace: "DONE" }`
### 3.2 旧块级工具没有 local-first 默认写入语义
> 参考对照:CLI Main 的 skill 可以操作任何 `doc-token`(包括本地文件和云端文档),因为它用的是文本级 + XML 级工具,不依赖特定存储。
mnote 当前主线:页面已能定位 local folder `.md`,但普通 AI 正文编辑不能回到“必须构造完整 page context 或 block ops”的旧模型。AI 视角应看到受控文件引用、selection 和 allowed roots,而不是一份由前端拼出的第二事实源。
### 3.3 两套体系互不相认
| | 历史 cloud / compat 文档 | 当前 local-first `.md` 文件 |
|---|---|---|
| 存储 | 历史迁移源 / 显式 cloud compat / sync replica | 文件系统 `*.md` |
| AI 读取 | `mnote.doc.fetch` 兼容投影 | 当前文件引用 / Page Aggregate / local markdown |
| AI 写入 | `mnote.doc.*``mnote.block.*` 兼容 fallback | agent 原生 patch/diff + watcher 同步 |
| 写入粒度 | 工具代理,不能作为新增默认入口 | 文件级 patch + selection 辅助 |
| 设计定位 | 历史 / compat 边界 | 当前主路径 |
旧设计把 compat 文档和本地文件写成两套体系,容易让后续实现继续往旧 cloud / block 工具堆逻辑。当前必须收敛:**local-first `.md` 是默认事实源,markdown 是共同格式,compat 只服务迁移、显式 cloud 或 remote agent 边界。**
---
## 4. 设计原则
### 4.1 Markdown 是 AI 编辑的第一公民
> 参考对照:CLI Main 用 Markdown 做整篇导入/导出和对话引用,用 XML 做精确块编辑。mnote 直接用 Markdown 做所有 AI 操作,因为 mnote 没有 XML 存储层。
AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是 AI 概念。当前 local-first 持久化格式就是 markdown;历史 cloud / compat 内容若需要进入 AI 编辑,也必须先投影成 markdown。
### 4.2 两层操作模型(兼容层)
| 层 | 工具 | 寻址方式 | 适用场景 | 占比 |
|----|------|---------|---------|------|
| **文件级(主)** | agent 原生 `diff/apply_patch/文件编辑` | 授权文件引用 / selection | local-first 普通 Markdown 改写 | 80%+ |
| **文本级(兼容)** | `mnote.doc.markdown_edit` | search/replace 文本对 | 显式 cloud / remote agent / 兼容旧页面 AI | 次要 |
| **块级(辅助)** | `mnote.doc.apply_block_ops` | blockId / matchText | "把第三块拖到第一块后面"、"精确删除引用块" | <20% |
### 4.3 local-first 主写入路径
```
页面 AI / ACP
→ resolve_current_file(documentId, sourceKind=local_folder, rootUri)
→ 传授权文件引用 / selection / allowed roots 给 agent runtime
→ agent 原生 patch/diff 写入文件
→ MNote 做权限 / 审计 / watcher refresh / BufferStore 冲突
→ 仅在显式 cloud / remote compat 时 fallback 到 mnote.doc.markdown_edit
```
当前执行层不再二选一。默认只有 local-first 授权文件路径;cloud 或受限 remote runtime 无法直接访问本地文件时,才显式走 `mnote.doc.markdown_edit` 代理。这和 CLI Main 的 `docs +update` 可以操作任何 `doc-token` 的设计一致,只是 mnote 进一步把“编辑算法”让渡给 agent runtime。
### 4.4 Diff 是内部实现细节
对兼容 `mnote.doc.markdown_edit` 来说,AI **不**产出 unified diff(行号/上下文极易出错),而是产出两种形式之一:
| 形式 | 适用场景 | AI 负担 |
|------|---------|---------|
| `operations: [{ search, replace }]` | 局部修改 | 低:只需找原文片段 |
| `full_content: "..."` | 小文档全文改写 | 低:直接写完整 markdown |
而在 local-first 主路径中,agent runtime 自己可以安全使用成熟的 diff / apply_patch / 直接文件编辑能力;MNote 只要求这些写入被限制在授权路径内,并把 changed files / diff 摘要收回审计。
---
## 5. 工具合同
### 5.1 `mnote.doc.fetch`(已有,增强)
```json
{
"toolName": "mnote.doc.fetch",
"args": {
"documentId": "tree_xxx 或 /path/to/file.md",
"format": "markdown",
"scope": "full",
"query": "TODO",
"maxChars": 8000
}
}
```
增强点:
- `format: "markdown"` — 兼容值。local-first 文档直接返回 `.md` 原文;历史 cloud / compat 文档必须经 Page Aggregate 投影成 markdown。
- `documentId` — 当前默认解析为 local-first 文档;只有显式 sourceKind / compat 标记允许进入 cloud adapter。
返回:
```json
{
"ok": true,
"source": "local_folder",
"documentId": "local-md:README.md",
"fileVersion": "local-md:README.md:...",
"format": "markdown",
"content": "# 标题\n\n段落内容...\n\n## 子标题\n\n...",
"truncated": false,
"charCount": 1234
}
```
### 5.2 `mnote.doc.markdown_edit`(新增,兼容 / fallback
```json
{
"toolName": "mnote.doc.markdown_edit",
"args": {
"documentId": "tree_xxx 或 /path/to/file.md",
"operations": [
{ "search": "原文片段", "replace": "新文本" },
{ "search": "另一段", "replace": "改写后的内容" }
]
}
}
```
服务端处理流程:
```
1. resolve_source(documentId, sourceKind, rootUri) → LocalFSAdapter | explicit CompatAdapter
2. 读取当前 markdown 全文
3. 逐条 search_replace(精确匹配 → fuzzy fallback
4. 计算内部 diff(用于 BlockDelta 推送)
5. 写入目标
6. 返回结果
```
返回:
```json
{
"ok": true,
"source": "local_folder",
"documentId": "local-md:README.md",
"fileVersion": { "before": "local-md:README.md:...", "after": "local-md:README.md:..." },
"operationsApplied": 2,
"operationsFailed": 0,
"failedOperations": [],
"changedText": "已将「原文片段」替换为「新文本」\n已将「另一段」替换为「改写后的内容」",
"blockDelta": {
"documentId": "local-md:README.md",
"fileVersion": "local-md:README.md:...",
"operations": [...]
}
}
```
### 5.3 `mnote.doc.apply_block_ops`(已有,辅助路径)
保留不变。用于精确块结构调整(拖拽排序、指定 blockId 的精确删除)。在 Hermes tool manifest 中 `markdown_edit` 排在 `apply_block_ops` 前面。
---
## 6. 搜索替换语义
> 参考对照:CLI Main 的 `str_replace` 使用精确子串匹配。mnote 增加 fuzzy fallback 以提高模型输出容错率。
| 优先级 | 策略 | 说明 |
|--------|------|------|
| 1 | 精确匹配 | 原文字串精确匹配,区分大小写 |
| 2 | 宽松匹配 | 忽略首尾空白、全角/半角差异后匹配 |
| 3 | 段落 fuzzy | 按换行分段,每段独立 fuzzy match(允许 30% 字符差异) |
| 4 | 失败 | 返回 `operationsFailed`,列出无法匹配的 search 和原因 |
多条 operation 按数组顺序串行执行。前一条的替换结果对后一条可见(和 sed 语义一致)。
**乐观锁**local-first 下执行前用 file version / conflict detection key 检测。如果写入时文件版本已过期,返回冲突错误,让 AI 重新 fetch + edit;历史 cloud compat 才能使用旧 revision 语义。
---
## 7. 远期规划:流式 apply + suggest/reviewPhase C,当前不实施)
> 参考对照:BlockNote AI 的 `StreamToolExecutor` + `suggestChanges` + `delayAgentStep`。本节仅做设计规划,不列入当前实施阶段。
### 7.1 目标
当用户通过页面 AI 面板发起编辑后,不是等所有操作完成后一次性刷新,而是:
1. AI 逐条产出 search/replace 对(流式)
2. 服务端逐条 apply 并推送 BlockDelta
3. 编辑器逐条渲染修改(带延迟,模拟"AI 正在编辑"
4. 用户可逐条 accept / rejectsuggestion 模式)
5. 确认后才最终写入本地 `.md` 或显式 compat 目标(review 模式)
### 7.2 与 BlockNote AI 的差异
| | BlockNote AI | mnote Phase C |
|---|---|---|
| 操作粒度 | 块级(add/update/delete block | 文本级(search/replace 文本对) |
| blockId 依赖 | 必须 | 不需要(文本匹配) |
| 本地文件支持 | 不支持(无稳定 blockId) | 支持(文本匹配不依赖 blockId) |
| Suggestion 层 | ProseMirror `suggestChanges` marks | Tiptap 的 suggestion 扩展或自建 |
| 写入时机 | 用户 accept 后统一写入 | 同 |
### 7.3 组件设计(草图)
```
StreamApplyController(新增)
输入:LLM 流式输出的 partial operations JSON
处理:
1. 解析 partial JSON → 提取已完成的 operation
2. 执行 search_replace → 产生 BlockDelta
3. 通过 SSE 推送 delta 到编辑器
4. 可选:注入 delay50-200ms)模拟人类编辑节奏
输出:逐条 BlockDelta
ReviewSession(新增,local-first 审阅层)
状态:pending → accepted | rejected
存储:SQLite control-plane / 本地 transient review state;不依赖 Convex
生命周期:
- AI 编辑 → 创建 ReviewSession → 所有修改标记为 pending
- 用户逐条操作 → accept/reject → 更新 session 状态
- 全部处理或用户确认 → 最终写入 → 关闭 session
GhostTextOverlay(新增,编辑器)
参考:Tiptap AI Autocomplete 的 ghost text 覆盖层
用于:展示 AI 修改前后的 diff(红删绿增),用户 hover 查看详情
```
### 7.4 分阶段实施顺序
| 阶段 | 内容 | 依赖 |
|------|------|------|
| C-1 | `StreamApplyController`:流式 apply + SSE 逐条推送 delta | 7-13 EditorRuntimeActor delta 通道 |
| C-2 | `GhostTextOverlay`:编辑器内 diff 展示(红删绿增) | leptos-tiptap 的 decoration 能力 |
| C-3 | `ReviewSession`local-first accept/reject session | SQLite control-plane / editor transient review state 设计 |
| C-4 | `delayAgentStep`:可选的编辑节奏模拟 | C-1 完成 |
**当前不做实施决策**。Phase C 的启动时机以 Phase A/B 完成后,编辑器 decoration 能力、BufferStore/file version 冲突模型和 SQLite control-plane review state 设计就绪为前置条件。
---
## 8. 实施阶段(当前)
### Phase A:文件引用主路径 + `mnote.doc.*` 兼容层
- [x] `mnote.doc.fetch` 增加 `format: "markdown"`local-first `.md` / Page Aggregate markdown 投影)
- [x] `mnote.doc.fetch` 增加本地文件 source 路由(`local_folder` / `rootUri` / `local-md:*`
- [x] 实现 `resolve_source(documentId, sourceKind, rootUri)` — 默认 `local_folder`;历史 cloud / compat 必须显式进入 compat adapter
- [ ] 页面 AI / ACP 普通正文编辑默认只传当前文件引用、可选 selection 和用户指令,不再默认构造完整 page context
- [ ] 本地 agent runtime 在 `allowed_roots / allowed_file_paths` 内执行 patch/diff,并把 changed files / diff 摘要回传 MNote
- [x] 实现 `search_replace(text, operations)` — 四级匹配策略(精确→宽松→段落 fuzzy→失败)
- [x] 历史 cloud / compat 写入 adapter 已降级为 fallback,不再作为 local-first 默认入口
- [x] 实现本地文件写入 adapter`mnote.doc.markdown_edit` 检测到本地文件路径时直接 `fs::write` 写回,不经过 Convex
- [ ] 内部 diff 生成 + BlockDelta 推送(复用 7-13 delta 基础设施,待 Phase C 实现)
- [x] Hermes tool manifest 注册 `mnote.doc.markdown_edit`
- [x] Hermes `mnote` plugin 更新:`mnote_doc_fetch` schema + `mnote_doc_markdown_edit` 新增
- [x] 乐观锁:local-first 使用 file version / conflict detection keycompat 路径才保留 revision 语义
- [x] 历史 browser smokecloud compat `format: "markdown"` + `markdown_edit` 搜索替换)仅作兼容证据,不代表当前主路径
- [x] 单元测试(`search_replace` 精确/失败/全文、`blocks_to_markdown` with_ids/heading 共 5 测试通过)
- [x] 浏览器 smoke(本地 `.md` 文件读取 `mnote_doc_fetch` + 写入 `mnote_doc_markdown_edit``full_content` 创建 + `operations` 搜索替换 + 回读验证全部通过)
### Phase B`page_ai_workflow.rs` 收口
- [x] 退役 `direct_block_edit_operations`(正则抠「」的快路径,代码保留但路由跳过)
- [x] `/api/page-ai/block-edit-workflow` 不再被当作 local-first 主路径
- [x] 模型 system prompt 重构:从产块操作 JSON 改为产 search/replace 文本对
- [x] 补全 operation schema`extract_markdown_operations_from_model_text` 处理新旧格式
- [x] 浏览器 smoke`markdown_edit` 搜索替换通过,自然语言编辑路径可用
### Phase C:流式/review(规划中,不实施)
- [ ] 流式 apply`StreamApplyController`
- [ ] suggest/review`ReviewSession` + `GhostTextOverlay`
- [ ] `delayAgentStep` 编辑节奏模拟
> 见 §7。当前仅做设计规划,不实施。
---
## 9. 与参考实现和其他设计稿的关系
### 9.1 与 CLI Main 的关系
| CLI Main 概念 | mnote 对应 |
|--------------|-----------|
| `str_replace`(文本级) | `mnote.doc.markdown_edit`(兼容 / fallback |
| `block_replace/insert_after/delete/move_after`(块级) | `mnote.doc.apply_block_ops`(辅助路径) |
| XML 用于精确编辑 | mnote 不用 XML(没有 XML 存储层),direct path 退役后全部走 markdown |
| Markdown 用于导入/导出/对话引用 | mnote 全部 AI 交互走 markdown |
| Skill 格式(YAML + Markdown + tool shortcuts | Hermes `mnote` plugin`plugin.yaml` + `SKILL.md` + tool manifest |
### 9.2 与 BlockNote AI 的关系
| BlockNote AI 概念 | mnote 远期对应 | 状态 |
|-------------------|---------------|------|
| `StreamTool` + `StreamToolExecutor` | `StreamApplyController` | Phase C 规划 |
| `suggestChanges` + `AIExtension` state machine | `ReviewSession` + `GhostTextOverlay` | Phase C 规划 |
| `delayAgentStep` | 可选改善 | Phase C 规划 |
| `RebaseTool` | revision 乐观锁(已有) | 当前已覆盖 |
| 纯 blockId 寻址 | ❌ 不采用。mnote 用文本匹配 | — |
### 9.3 与其他设计稿的关系
| 设计稿 | 关系 | 修正状态 |
|--------|------|---------|
| 7-9 路线图 | 块级编辑降级为辅助,local-first 普通编辑改为授权文件 + agent patch/diffmarkdown_edit 退到兼容层 | ✅ 已修正 |
| 7-10 checklist | 新增 Phase 9 markdown_edit | ✅ 已修正 |
| 7-12 工具路由 | PageAICommandRouter 主输出改为 markdown_edit | ✅ 已修正 |
| 7-13 EditorRuntimeActor | 补充 markdown_edit 的 delta 适配 | ✅ 已修正 |
| 3-13 本地 markdown | 新增 AI 工具接入章节 | ✅ 已修正 |
---
## 10. 禁止项
- 不删除 `mnote.block.*` 工具(保留为辅助路径)。
- 不强迫 local-first agent 产出 MNote 自定义 diffHermes / Reasonix 可使用自身成熟 patch / diff / apply_patch 能力,MNote 负责白名单权限、文件版本冲突和审计。
- 不要求本地文件有稳定的 `blockId`(本地文件没有 block identity)。
- 不在 markdown_edit 内部引入新的 AI 模型调用(diff 是确定性算法)。
- 不把 Convex / `documents:updateContent` 重新提升为当前正文主存储;`markdown_edit` 只在显式 cloud / compat 场景复用受控保存路径。
- 不把 `mnote.page.save` 重新描述为精确编辑主入口(它仍是兜底工具)。
- **不照搬 BlockNote AI 的纯 blockId 寻址模式**(与 mnote 的 markdown 优先策略冲突)。
---
## 11. 成功标准
- [x] `mnote.doc.fetch(documentId, format: "markdown")` 对历史 cloud / compat 文档返回正确 markdown(兼容证据)
- [ ] `mnote.doc.fetch(documentId, format: "markdown")` 对本地 `.md` 文件返回正确 markdown(兼容路径)
- [x] `mnote.doc.markdown_edit` 的简单搜索替换(1 条 operation)浏览器 smoke 通过
- [ ] `mnote.doc.markdown_edit` 的复杂改写(3+ 条 operations)成功率 > 80%
- [ ] 本地 `.md` 文件通过页面 AI 面板可被读取和写入,且默认通过 agent 原生 patch/diff 完成
- [x] 历史 cloud / compat markdown_edit 不增加额外 RTT(兼容证据,不代表当前主路径)
- [x] `direct_block_edit_operations` 已退役(路由跳过,代码保留)
- [x] `page_ai_workflow.rs` 的 system prompt 已补全 search/replace schema
- [x] Phase C(流式/review)的设计已冻结,不阻塞 A/B 实施
---
## 12. CLI Main 成熟度采纳清单
以下清单按优先级排列,标注当前状态和对应 CLI Main 参考位置。
### 12.1 当前 Phase A/B 必须完成的
| # | CLI Main 模式 | mnote 实施项 | 状态 | 参考文件 |
|---|-------------|-------------|------|---------|
| 1 | 两层操作模型 | local-first 默认 agent 原生 patch/diff`markdown_edit`compat fallback+ `apply_block_ops`(结构辅助)已在 7-10 Phase 9 登记 | 📋 设计完成,已按 2-2 改口径 | `lark-doc-update.md` |
| 2 | Scope 四级控制 | `mnote.doc.fetch` 增加 `scope: full/section/outline/keyword` | 🔜 Phase A | `lark-doc-fetch.md` |
| 3 | Detail 三级控制 | `mnote.doc.fetch` 增加 `detail: simple/with_ids/full` | 🔜 Phase A | `lark-doc-fetch.md` |
| 4 | 片段包装 | fetch 返回中标记 `<!-- fragment -->` / `<!-- excerpt -->` 告知 AI 部分视图 | 🔜 Phase A | `lark-doc-fetch.md`fragment/excerpt 模式) |
| 5 | Code-Act Loop | Hermes `mnote` plugin SKILL.md 中内联 Plan→Execute→Observe→Iterate 指导 | ✅ 已写入 `~/.hermes/skills/note-taking/mnote-block-ai/SKILL.md` | `lark-doc-update-workflow.md` |
| 6 | 更新命令决策树 | 模型 system prompt 追加"优先搜索替换,不全文覆盖"规则 | 🔜 Phase B | `lark-doc-update.md`str_replace vs block_* 决策) |
| 7 | 写后回读 | Hermes 每次写操作结束后自动 `mnote.doc.fetch` 回读 | 🔜 Phase B | `lark-doc-update-workflow.md` |
| 8 | Markdown 优先格式策略 | mnote 默认和唯一 AI 格式为 markdown(不需要 XML | ✅ 已确认 | `lark-doc-md.md`(对比 XML 路线) |
### 12.2 Phase A/B 完成后应补充的
| # | CLI Main 模式 | mnote 实施项 | 状态 | 参考文件 |
|---|-------------|-------------|------|---------|
| 9 | Skill YAML 规范化 | Hermes `mnote` plugin 的 `plugin.yaml` 增加 `description``metadata.domain` | ✅ 已更新 v0.2.0 | `master-skill-template.md` |
| 10 | 参考文件分离 | `~/.hermes/skills/note-taking/mnote-block-ai/references/` 目录已创建,`mnote-doc-fetch.md` 已写入 | ✅ 已创建(fetch),其余按需补充 | `skills/lark-doc/references/` |
| 11 | Skill CI 校验 | CI 步骤:检查所有 plugin.yaml 和 SKILL.md 的 YAML frontmatter 有效性 | 📋 待实施 | `skill-format-check.yml` |
| 12 | 格式规则文档 | `references/mnote-doc-md.md`:定义 markdown 中 AI 应理解的特殊语法(任务列表标记 `- [ ]`、附件链接 `[file:...]` 等) | 📋 待实施 | `lark-doc-md.md` |
| 13 | 上下文冻结规范 | `references/mnote-doc-context.md`:冻结时保留哪些字段、截断规则、revision 注入方式 | 📋 待实施 | `lark-doc-fetch.md`fetch body 构建) |
### 12.3 远期规划(Phase C 及以后)
| # | CLI Main 模式 | mnote 实施项 | 状态 | 参考文件 |
|---|-------------|-------------|------|---------|
| 14 | 跨域资源路由 | 文档内嵌思维导图/附件时,路由到对应 skill(见 §2.1.5 | ⏳ Phase C 后 | `lark-doc/SKILL.md`(路由表) |
| 15 | 流式增量 apply | BlockNote AI 的 `StreamToolExecutor` 模式,见 §7 | ⏳ Phase C 规划 | `blocknote-ai/packages/xl-ai/` |
| 16 | suggest/review | BlockNote AI 的 `suggestChanges` 模式,见 §7 | ⏳ Phase C 规划 | `blocknote-ai/packages/xl-ai/` |
### 12.4 与 BlockNote AI / Tiptap AI Autocomplete 的采纳清单
| # | 参考模式 | mnote 采纳 | 时机 | 参考文件 |
|---|---------|-----------|------|---------|
| B1 | `StreamTool` 流式 apply | `StreamApplyController`(§7.3 | Phase C | `blocknote-ai/packages/xl-ai/src/streamTool/` |
| B2 | `suggestChanges` review 层 | `ReviewSession` + `GhostTextOverlay`(§7.3 | Phase C | `blocknote-ai/packages/xl-ai/src/AIExtension.ts` |
| B3 | `delayAgentStep` 编辑节奏 | 可选改善 | Phase C | `blocknote-ai/packages/xl-ai/src/streamTool/` |
| T1 | ghost text 内联补全 | 独立功能(非本文档范围) | 独立立项 | `tiptap-ai-autocomplete/src/` |
### 12.5 实施优先级总结
```
Phase A(当前立即)
├── #2 Scope 四级控制 ← mnote.doc.fetch 增强
├── #3 Detail 三级控制 ← mnote.doc.fetch 增强
├── #4 片段包装 ← fetch 返回值增强
├── #8 Markdown 优先 ← 已确认
└── 授权文件引用 + agent 原生 patch/diff 主路径;mnote.doc.markdown_edit 作为 compat fallback
Phase BPhase A 完成后)
├── #5 Code-Act Loop ← Hermes plugin SKILL.md
├── #6 更新决策树 ← 模型 system prompt
└── #7 写后回读 ← Hermes tool loop 逻辑
Phase A/B 完成后补充
├── #9-#13 Skill 规范化、参考文件分离、CI 校验、格式文档、上下文规范
Phase C(远期)
├── #14 跨域路由(Phase C 后)
├── #15 流式 applyPhase C
└── #16 suggest/reviewPhase C
```