收口 MNote P0 P1 P2 审查尾项

- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
This commit is contained in:
lix-2026
2026-06-01 09:29:12 +08:00
parent 49a0545148
commit 1882db7681
143 changed files with 29810 additions and 3228 deletions
@@ -2,7 +2,7 @@
> 创建时间:2026-05-17
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 2026-05-21 Batch J 口径补充:
> - 本稿的 ACP runtime 核心实现已完成并在当前页面 AI 主链中作为默认 runtime 边界使用;Hermes HTTP proxy 默认关闭,只在显式 compat 开关下保留。
@@ -0,0 +1,128 @@
# 7-38 Page AI sidebar runtime owner split v1
> 创建时间:2026-05-25
> 状态:`done`
> 来源:`design/03-rust-web/done/3-22-sidebar-tree-runtime-second-stage-split-v1.md` Batch A。
> 2026-05-26 更新:`design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md` 已把 Page AI 主体迁入 `browser/sidebar-page-ai-runtime.js`;本文件继续作为后续 AI owner 收口 checklist。
## 1. 背景
`rust/crates/mnote-web/browser/sidebar-tree-runtime.js` 曾包含大量 Page AI panel 代码:会话、消息、profile、skills、gateway health、ACP runtime、permission dialog、plan/status event、session search/resume 等。2026-05-26 的 runtime 可维护性批次已将主体迁入 `browser/sidebar-page-ai-runtime.js`2026-06-01 继续迁出事件 delegate 和 `pageAi*` 默认状态。tree runtime 目前只保留 `data-mnote-action="open-page-ai"` 主壳入口、共享 bootstrap 对象和少量触发器代理。
这些代码不是 tree/filetree runtime 的长期 owner。继续把 Page AI 面板放在 `03-rust-web` 的 sidebar runtime 拆分里,会让 tree shell、local folder、AI session 三条边界继续混在一起。
## 2. Owner 判断
- owner`07-ai`
- 运行位置:当前仍可挂在 sidebar UI,但 runtime 模块应独立于 tree/filetree runtime。
- 当前目标模块:`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- 主壳职责:只加载 asset、提供当前 document/workspace/root/bootstrap。
## 3. 后续执行建议
- [x] 只读审计 Page AI panel 的状态入口:`pageUiState.pageAi*`、storage、session 列表、profile/skills、permission dialog。
- [x] 建立独立 Page AI sidebar owner runtime`sidebar-page-ai-runtime.js`
- [x]`sidebar-tree-runtime.js` 继续迁出 Page AI click/change/input/keydown 委托,tree runtime 只保留 `data-mnote-action="open-page-ai"` 入口。
- [x]`pageUiState.pageAi*` 从通用 sidebar state 中下沉到 Page AI owner runtime,并保留兼容 getter/setter 或明确 bootstrap contract。
- [x]`sidebar-page-ai-runtime.js` 继续超过 2,500 行,再按 `session/profile-skill/permission/conversation/target/render` 拆成子模块。
- [x] 浏览器验证使用页面 AI / ACP smoke,而不是 tree/filetree smoke 替代。
## 4. 非目标
- 不在 03-rust-web 的 sidebar runtime 二阶段里继续扩大 Page AI 实现。
- 不在 10-review runtime 可维护性收口里继续重构 ACP/Hermes session 语义;这里只记录 owner 边界。
- 不改变 Hermes / Reasonix ACP runtime 协议。
- 不把 local-first agent 文件编辑链路改回粗粒度 `mnote.page.save`
## 5. 2026-06-01 复核
本文继续保持 `process`。只读核查确认 `sidebar-page-ai-runtime.js` 当时约 4519 行,已经触发第 3 节的 2500 行继续拆分条件;`sidebar-tree-runtime.js` 当时仍保留 Page AI click/change 委托和 `pageUiState.pageAi*` 状态壳。缺口已记录到 `bugs/07-ai/process/7-53-page-ai-runtime-split-threshold-triggered-v1.md`
下一步最小切片是按 render / conversation / run orchestration 子模块继续拆 `sidebar-page-ai-runtime.js`,降低单文件职责和公开代理面。
## 6. 2026-06-01 第一刀执行记录
已完成第一刀:`sidebar-tree-runtime.js` 不再内联 Page AI 的 click/input/change/keydown 大段 action 分发,也不再调用 `sidebarPageAi.handlePageAi*` 薄委托;`sidebar-page-ai-runtime.js` 承接原有 close、settings、stop、rotate、intent、tab、agent、context、target、skill、tool、session、permission、open-location、suggestion、send、history 等分发语义,并通过 `installPageAiDelegates()` 自行安装事件监听。
验证记录:
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- `node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch -- --test-threads=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_agent_target_picker_contract_is_visible_and_serialized -- --test-threads=1`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task490-runtime-surfaces-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task502-page-ai-agent-selector-context-smoke.js`
2026-06-01 续补:
- `sidebar-page-ai-runtime.js` 新增并导出 `installPageAiDelegates()`,内部一次性安装 Page AI click/keydown/input/change delegate。
- `sidebar-tree-runtime.js` 仅在自身监听注册后调用 `sidebarPageAi.installPageAiDelegates()`,不再直接处理 Page AI 内部事件。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证 drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 state facade 续补:
- `sidebar-page-ai-runtime.js` 新增 `ensurePageAiStateFacade()`,集中初始化 Page AI 默认状态并导出该 facade。
- `sidebar-tree-runtime.js``pageUiState` 初始对象删除 `pageAi*:` 默认字段,tree runtime 不再定义 Page AI 状态真相。
- Rust include/assert 已更新为:Page AI runtime 包含 `ensurePageAiStateFacade``pageAiAcpRuntime: 'reasonix'`tree runtime 不包含该默认字段。
2026-06-01 conversation helper 续补:
- 新增 `sidebar-page-ai-markdown-runtime.js`,抽出 `textFromUnknown``renderPageAiMarkdown` 和 inline Markdown 渲染 helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiMarkdownRuntime()`,保留现有 assistant message 渲染行为。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-markdown-runtime.js` 静态 runtime asset,并更新 runtime asset mount 测试。
2026-06-01 profile helper 续补:
- 新增 `sidebar-page-ai-profile-runtime.js`,抽出 provider/profile/chat-only profile/history filter/usage helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiProfileRuntime()` 并保留同名代理常量,降低调用点扰动。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-profile-runtime.js` 静态 runtime asset,并更新 runtime asset mount 测试。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证二级 import 后 drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 permission helper 续补:
- 新增 `sidebar-page-ai-permission-runtime.js`,抽出 ACP permission message、dialog show/hide 和 resolve-permission helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiPermissionRuntime()` 并保留同名代理常量,主 runtime 继续负责事件流持久化、会话同步和 conversation 渲染入口。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-permission-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 permission 二级 import 后 sidebar/Page AI 仍可加载和发送。
2026-06-01 session helper 续补:
- 新增 `sidebar-page-ai-session-runtime.js`,抽出 session storage、backend session list/detail/search/resume/delete、session message sync 和 backend runtime event replay helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiSessionRuntime()` 并保留同名代理常量,主 runtime 继续负责 Page AI UI render、run orchestration 和事件委托入口。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-session-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 session 二级 import 后 Page AI drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 skill helper 续补:
- 新增 `sidebar-page-ai-skill-runtime.js`,抽出 skill source、skill preference、Hermes builtin 隐藏和 Reasonix memory preference helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiSkillRuntime()` 并保留同名代理常量,主 runtime 继续负责 skill 异步加载、target/run orchestration 和 UI render。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-skill-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已通过 `node --check` 覆盖 Page AI 主 runtime、skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch``page_ai_agent_target_picker_contract_is_visible_and_serialized`
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 skill 二级 import 后 Page AI drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 target helper 续补:
- 新增 `sidebar-page-ai-target-runtime.js`,抽出 OpenEditorsSnapshot target 派生、WorkspacePath、run target snapshot、contextRefs、agentTargetPackage 和 target writable guard helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiTargetRuntime()` 并保留同名代理常量,主 runtime 继续负责 target popover UI、事件分发和 run orchestration 顺序。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-target-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言;target picker 合同测试改为在 target helper 中断言 `primaryTargetId` / `targets` / `policy`
- 已通过 `node --check` 覆盖 Page AI 主 runtime、target/skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_uses_backend_acp_session_runtime_store``page_ai_agent_target_picker_contract_is_visible_and_serialized``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch`
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js``task520-page-ai-raw-resource-target-smoke.js``task525-page-ai-mindmap-resource-target-smoke.js`,验证新增 target 二级 import 后 Page AI drawer、agent/context/target/skills、raw resource target、mindmap target 和发送 payload 链路仍可用。
2026-06-01 render helper 续补:
- 新增 `sidebar-page-ai-render-runtime.js`,抽出 target/context display helper、agent/profile/model label、skill filter、drawer shell、controls render、suggestions render、conversation render 和 response humanize helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiRenderRuntime()` 并保留公开方法形状,主 runtime 继续负责 state facade、agent/run orchestration、session/permission/skill/target runtime 接线与事件委托。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-render-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- `sidebar-page-ai-runtime.js` 当前 2498 行,已低于 2500 行继续拆分阈值。
- 已通过 `node --check` 覆盖 Page AI 主 runtime、render/target/skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_uses_backend_acp_session_runtime_store``page_ai_agent_target_picker_contract_is_visible_and_serialized``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch`
本文已满足本轮 owner split checklist 归档条件;后续只在 runtime 再次超过阈值或 run orchestration 继续膨胀时开新缺陷 / 新设计。
2026-06-01 smoke 复核:
- 已通过 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js``task504-page-ai-history-agent-filter-smoke.js``task520-page-ai-raw-resource-target-smoke.js``task525-page-ai-mindmap-resource-target-smoke.js`
- `bugs/07-ai/process/7-53-page-ai-runtime-split-threshold-triggered-v1.md` 已归档到 `bugs/07-ai/done/7-53-page-ai-runtime-split-threshold-triggered-v1.md`
- 本轮 smoke 额外发现并修复当前页 target 继承 stale workspacePath 的缺陷,记录到 `bugs/07-ai/done/7-55-page-ai-current-page-target-stale-workspace-v1.md`
@@ -2,7 +2,7 @@
> 创建时间:2026-05-29
>
> 状态:`PROCESS`
> 状态:`done`
>
> OwnerPage AI skill/tool capability surface + MNote host-side context provider
>
@@ -0,0 +1,455 @@
# 7-41 Page AI Hermes / Reasonix user profile isolation v2
> 创建时间:2026-05-29
>
> 状态:`done`
>
> OwnerPage AI agent identity / Hermes profile policy / Reasonix memory policy
>
> 上位依据:
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
> - `design/07-ai/done/7-40-page-ai-context-envelope-and-run-receipt-v1.md`
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
> - `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md`
>
> 参考依据:
> - Hermes 官方 profile 文档:profile 是独立 Hermes home,包含 `config.yaml`、`.env`、`SOUL.md`、memories、sessions、skills、cron、state database、gateway state。
> - Hermes WebUI:技能 toggle 直接写 profile `config.skills.disabled`。
> - Hermes VSCode:只管理会话和一次性上下文建议,不提供真正的 per-user skill disable。
> - PilotDeck:以 WorkSpace 为边界隔离文件、记忆和技能,并强调白盒记忆可追溯。
> - Reasonix 文档/本地实现:默认使用 `~/.reasonix/memory/global` 与 `~/.reasonix/memory/<project-hash>``REASONIX_MEMORY=off|false|0` 可关闭 memory 注入。
## 1. 背景
`7-39` 已把 Page AI UI 收口为 agent selector + contextRefs + SQLite per-user preference。`7-40` 进一步把 MNote skill library 定位为 agent 可按需读取的能力摘要,而不是每轮强行注入 prompt。
但当前 Hermes skill/profile 的真实边界仍不正确:
- Hermes 的 `skills.disabled` 是 profile 级配置,不是 MNote 用户级偏好。
- Hermes profile 自带 `SOUL.md`、memory、session、skills 和 state;多个 MNote 用户共用同一个可写 profile,会把个人偏好、记忆和技能配置混在一起。
- 当前 Page AI UI 已经能展示 Hermes skill toggle,但普通用户如果直接写共享 Hermes profile,就会影响其它用户。
- Reasonix 虽然不像 Hermes 那样有 SOUL 人格,但它也有 global/project memory。默认共享 `~/.reasonix` 时,不能假设完全无状态。
因此本稿把 Page AI agent identity 从“选择 Hermes / Reasonix”提升为“选择 agent + profile scope + memory policy”。
## 2. 产品决策
### 2.1 Hermes profile 分层
Hermes profile 分为两类:
| profile kind | owner | 普通用户能否使用 | 普通用户能否改 skill | memory/session |
| --- | --- | --- | --- | --- |
| `personal` | 单个 MNote 用户 | 能 | 能,仅限自己的 profile | 用户独立 |
| `shared` | 系统 / 管理员 | 能,若管理员公开 | 不能 | 默认不写入用户私有长期记忆 |
初始共享 Hermes profile 只包含 `lite`
要求:
- 每个 MNote 用户可以访问自己的 personal Hermes profile。
- 每个 MNote 用户可以访问被管理员公开的 shared Hermes profile。
- shared Hermes profile 的 skill/config 只有管理员能改。
- personal Hermes profile 的 skill/config 只有该 profile owner 或管理员能改。
- Page AI 发送请求时必须携带解析后的 `agentProfileRef`,不能只携带裸 `profile=lite`
### 2.2 MNote 内置技能开关
MNote 内置 skill / tool 属于 MNote 自己的能力面,普通用户应该可以按用户启停。该开关不写 Hermes profile,而是写 SQLite control-plane。
含义:
- `mnote_builtin_skill_enabled` 是 per-user 偏好和服务端执行策略,必须按当前 SQLite 用户隔离。
- `hide_builtin_skills=true` 只是 UI 展示偏好,用于折叠或隐藏 MNote 内置能力说明;它不等于禁用。
- MNote tool 是否可调用由服务端根据 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 决定。
- 普通用户可启停自己的 MNote 内置 skills;管理员可设置默认值或全局禁用策略。
- MNote 内置 skill 开关不修改 Hermes profile `config.yaml`,也不影响 shared Hermes profile。
### 2.3 Hermes profile skills
可以配置的只有 Hermes profile 自己的 skills。
规则:
- personal profile:用户可启停该 profile 下的 Hermes skills,写入该 personal profile 的 `config.yaml`
- shared profile:普通用户只读;管理员可启停 shared profile skills。
- skill toggle API 必须做服务端权限检查,不能只靠 UI 禁用按钮。
- MNote 不再把 `ai.agent.hermes.profile.<name>.skills.enabled` 当成普通用户对共享 profile 的安全开关;MNote 内置 skill 的 per-user 开关应使用独立 SQLite key。
### 2.4 Reasonix memory policy
Reasonix 默认关闭 memory 注入。
规则:
- 默认启动 Reasonix ACP 时设置 `REASONIX_MEMORY=off`
- 每个 MNote 用户可在设置中开启 Reasonix memory。
- 该设置保存到 SQLite `user_ui_preferences`,按用户隔离。
- 开启后,UI 需明确标注 Reasonix 将使用 global/project memory。
- 第一阶段不强制给 Reasonix 每用户独立 HOME;若后续需要更强隔离,再增加 `reasonix_home_mode=per_user`
## 3. 数据合同
### 3.1 Agent profile ref
Page AI payload 中新增或固定以下结构:
```json
{
"agentId": "hermes",
"agentProfileRef": {
"kind": "personal",
"profileId": "usr_123_default",
"ownerUserId": "usr_123",
"baseProfile": "default",
"displayName": "我的 Hermes"
}
}
```
shared profile
```json
{
"agentId": "hermes",
"agentProfileRef": {
"kind": "shared",
"profileId": "shared_lite",
"ownerUserId": null,
"baseProfile": "lite",
"displayName": "Lite"
}
}
```
服务端要求:
- 不接受浏览器直接提交任意 Hermes filesystem path。
- `profileId` 必须由 SQLite control-plane 解析到当前用户可访问的 profile。
- 对 personal profile,当前用户必须是 owner 或 admin。
- 对 shared profile,必须是公开 profile 或 admin。
- 所有 run / skill list / skill toggle / settings read 都通过 `agentProfileRef` 解析,不走裸 profile name。
### 3.2 SQLite control-plane
建议新增控制面表,避免只用偏好 key 表达权限关系:
```text
ai_agent_profiles
- id
- agent_id # hermes
- profile_kind # personal | shared
- owner_user_id # personal 必填,shared 为空
- base_profile_name # Hermes 原始 profile 名或模板名
- isolated_profile_name # MNote 管理后的真实 Hermes profile 名
- display_name
- status # active | disabled
- created_at
- updated_at
ai_agent_profile_grants
- profile_id
- user_id
- role # owner | user | admin
- can_run
- can_manage_skills
- can_manage_config
- created_at
- updated_at
```
第一阶段也可以在现有 `user_ui_preferences` 中保存默认选择:
- `ai.agent.hermes.default_profile_id`
- `ai.agent.hermes.hide_builtin_skills`
- `ai.agent.mnote_builtin.skills.enabled`
- `ai.agent.reasonix.memory_enabled`
但 profile 权限、owner、shared/personal 类型不应只存在于 UI preference。
MNote 内置 skill 的用户级开关可以第一阶段存在 `user_ui_preferences`,但服务端 tool policy 读取时必须视为执行策略,而不是纯 UI 状态。若后续需要审计、管理员默认值或组织策略,应升级为独立表:
```text
ai_user_skill_preferences
- user_id
- skill_id
- enabled
- updated_at
```
### 3.3 Hermes profile provisioning
创建 personal Hermes profile 时:
- 可从共享模板复制 `config.yaml``.env``SOUL.md` 和 skills。
- 不复制 memories、sessions、state database、gateway state。
- 生成的真实 profile name 必须包含 MNote 用户隔离标识,例如 `mnote-u-<userId>-default`
- provisioning 过程由服务端执行,并记录到 SQLite control-plane。
shared `lite`
- 初始由管理员登记为 `shared_lite`
- 普通用户只可 run / list readonly。
- 管理员可改 skill/config。
## 4. API 合同
### 4.1 Profile list
`GET /api/ai/agent-profiles?agentId=hermes`
返回当前用户可访问 profiles:
```json
{
"profiles": [
{
"profileId": "usr_123_default",
"kind": "personal",
"displayName": "我的 Hermes",
"canRun": true,
"canManageSkills": true
},
{
"profileId": "shared_lite",
"kind": "shared",
"displayName": "Lite",
"canRun": true,
"canManageSkills": false
}
]
}
```
### 4.2 Skill list
`GET /api/hermes/client/skills?profileId=...`
要求:
- personal profile 返回可 toggle 状态。
- shared profile 对普通用户返回 readonly 状态。
- MNote 内置 skills 返回值必须标记 `builtin=true``configurable=true``configScope=user_sqlite`
- Hermes profile skills 标记 `builtin=false``configurable=canManageSkills`
### 4.3 Skill toggle
`PUT /api/hermes/client/skills/toggle`
请求:
```json
{
"profileId": "usr_123_default",
"skillName": "writer",
"enabled": false
}
```
服务端必须:
- 解析 `profileId`
-`skillKind=mnote_builtin`,写当前用户 SQLite skill preference,不写 Hermes profile。
-`skillKind=hermes_profile`,校验 `canManageSkills=true`
- 拒绝普通用户修改 shared profile 的 Hermes profile skills。
- personal profile skill toggle 只写目标 Hermes profile 的 `config.yaml`
错误码建议:
- `ai_profile_not_found`
- `ai_profile_forbidden`
- `ai_profile_readonly`
- `ai_builtin_skill_preference_failed`
- `hermes_skill_toggle_failed`
### 4.4 Reasonix run
Reasonix ACP spawn / session create 需读取当前用户设置:
```json
{
"agentId": "reasonix",
"memoryPolicy": {
"enabled": false,
"source": "user_ui_preferences"
}
}
```
默认:
- `enabled=false`
- 子进程环境包含 `REASONIX_MEMORY=off`
开启:
- 不设置 `REASONIX_MEMORY=off`,或设置为 `on`
- UI 明确展示 memory 已开启
## 5. UI 设计
### 5.1 Agent selector
Hermes agent 下增加 profile 子选择:
```text
Hermes
我的 Hermes personal · 可配置
Lite shared · 只读
```
显示规则:
- personal profile 显示“可配置”。
- shared profile 显示“共享 / 只读”。
- 若普通用户选择 shared profile,技能开关显示为只读。
- 管理员选择 shared profile,技能开关可用,并显示“管理员正在修改共享 profile”。
### 5.2 Skills panel
三组仍保留:
- MNote 内置技能:可折叠,可隐藏/显示,也可由普通用户按自己账号启停。
- Hermes 技能:随当前 Hermes profile 变化;personal 可配置,shared 普通用户只读。
- Reasonix 技能:展示可用能力;memory 是单独设置,不混入 skill toggle。
Hermes profile 切换时:
- 必须重新加载 profile skills。
- 必须清空旧 profile skill cache。
- `hide_builtin_skills` 不随 Hermes profile 改变;它是用户 UI 偏好。
- MNote 内置 skill enable/disable 不随 Hermes profile 改变;它是当前 MNote 用户的 SQLite policy。
### 5.3 Settings
设置页拆分:
- Common:授权区域、contextRefs 默认值、内置技能显示/隐藏、内置技能启停。
- Hermes:默认 Hermes profile、personal profile 管理、shared profile 只读/管理员管理。
- Reasonix:默认关闭 memory;用户可开启。
- Chat-only:只聊天配置。
## 6. 非目标
- 不让普通用户直接修改 shared Hermes profile。
- 不把 shared Hermes profile 用作沉淀个人偏好的长期人格。
- 不在本阶段实现 Reasonix per-user HOME;只实现默认 memory off 与可选开启。
- 不新增第二套目录授权真相;文件访问仍由 SQLite directory grants / allowedRoots 控制。
- 不实现 PilotDeck 的完整 router、always-on 或 memory engine。
## 7. Checklist
### Batch A - 现状冻结与风险取证
- [x] 复核当前 Page AI Hermes skill toggle 的真实写入路径,确认是否直接写 Hermes profile `config.yaml`
- 证据:`/api/hermes/client/skills/toggle` 进入 `toggle_skill`,读取 `profile/name/enabled` 后调用 `set_skill_enabled(profile, name, enabled)``set_skill_enabled` 直接写 `profile_home(profile)/config.yaml` 中的 `skills.disabled`。当前没有 profile grant / shared readonly 检查。
- [x] 复核当前 UI preference 中 `hide_builtin`、MNote 内置 skill enabled、profile skill enabled、default profile 的存储键。
- 证据:前端仍使用 `ai.agent.hermes.profile_id` 保存默认 Hermes profileReasonix skill 开关写 `ai.agent.reasonix.skills.enabled`Hermes profile skill 开关写 `ai.agent.hermes.profile.<profile>.skills.enabled`;隐藏内置技能写 `ai.agent.hermes.profile.<profile>.skills.hide_builtin`。这些 key 是 UI preference,不等价于 profile 授权模型。
- [x] 复核 Reasonix ACP spawn 环境,确认当前是否默认注入 memory。
- 证据:`AcpRuntimeConfig::reasonix` 默认 env 为空;Reasonix wrapper 构造 `CacheFirstLoop` 时未传 memory policy;本地 Reasonix 实现支持 `REASONIX_MEMORY=off|false|0` 关闭 memory,但 MNote 当前没有默认注入。
- [x] 形成 RED 证据:普通用户修改 shared profile skill 会影响其它用户,或当前缺少服务端权限边界。
- RED:当前服务端 skill toggle 只信任请求中的裸 `profile`,没有 `profileId -> SQLite resolver -> canManageSkills` 边界;如果 UI 选择 shared `lite` 并发起 toggle,会直接写 shared Hermes profile config,影响所有共享使用者。
- [x] 验证:Rust/JS 只读审计记录在本文档或后续 checklist evidence 中。
- 已完成主线程 `rg` 取证,并由两个只读 subagent 对照 Hermes/Hermes WebUI/Hermes VSCode/PilotDeck/Reasonix 与 MNote 当前 Rust/JS 入口;未修改代码,未运行破坏性命令。
### Batch B - SQLite profile policy 合同
- [x] 新增或扩展 SQLite control-plane profile policy`ai_agent_profiles` / `ai_agent_profile_grants` 或等价结构。
- 证据:新增 `007-ai-agent-profile-policy.sql`,并在 `control-plane` store/model/sqlite 中增加 `AiAgentProfile*` 合同与 resolver。
- [x] 初始化 shared Hermes profile:仅 `lite`,普通用户 `canRun=true``canManageSkills=false`
- 证据:`ensure_ai_agent_profile_policy` 初始化 `shared_lite`,普通用户 grant 为 run-only。
- [x] 为每个用户 provision personal Hermes profile。
- 证据:当前用户首次查询时生成 `usr_<user>_default``mnote-u-<user>-default` isolated profile。
- [x] 补 Rust 定点测试:personal owner、shared readonly、admin manage、跨用户不可管理。
- 证据:`control-plane sqlite::tests::ai_agent_profile_policy_provisions_personal_and_shared_boundaries`
- [x] 验证:不同用户查询 profile list 只返回自己 personal + shared lite。
- 证据:`mnote-web routes::hermes_client::tests::page_ai_agent_profiles_are_sqlite_user_scoped`
### Batch C - Hermes profile resolver
- [x] 新增服务端 `agentProfileRef` resolver,禁止前端提交任意 Hermes path。
- 证据:新增 `/api/ai/agent-profiles``profileId -> SQLite policy -> isolatedProfile` resolver;浏览器不提交 filesystem path。
- [x] `/api/hermes/client/runs``profileId` 解析真实 Hermes profile。
- 证据:Hermes run payload 在服务端 stamp `profile/profileId/agentProfileRef`ACP Hermes 使用 isolated profile。
- [x] `/api/hermes/client/skills``profileId` 解析真实 Hermes profile。
- 证据:Hermes skills catalog 按 `profileId` 解析并返回 `agentProfileRef/configurable/readonly/configScope`
- [x] 保留旧 `profile=` 参数只作为兼容入口,并映射到当前用户可访问 profile。
- 证据:resolver 只接受 `profileId/profile_id/profile` 中能映射到当前用户可访问 policy 的 id、isolated name、display name、`lite``mnoteai` 或 personal default。
- [x] 验证:旧路径兼容不允许越权访问 shared/admin profile。
- 证据:普通用户 toggle `shared_lite` Hermes skill 返回 `403 ai_profile_readonly`
### Batch D - Skill toggle 权限收口
- [x] 修改 skill toggle API:只接受 `profileId + skillName + enabled`
- 证据:服务端接受 `profileId/name/enabled/skillKind`;旧 `profile` 仅作 resolver 兼容。
- [x] 区分 `mnote_builtin``hermes_profile` skill kind。
- [x] MNote 内置 skill toggle 写当前用户 SQLite preference。
- [x] 拒绝普通用户修改 shared profile 的 Hermes profile skills。
- [x] personal profile skill toggle 只写该用户 isolated profile `config.yaml`
- [x] shared profile skill toggle 仅 admin 可写。
- 证据:control-plane admin grant 可管理 shared;普通用户 shared readonly。
- [x] personal Hermes profile 初始 skill 收口为最小白名单。
- 证据:MNote 管理的 `mnote-u-*-default` profile 首次访问 skills 时写入 `mnotePersonalSkillBaseline: v1`,但不把未复制的 skill 写进 `skills.disabled`
- 2026-05-30 修正:personal Hermes 初始状态改为 profile 模板 copy 语义,首次 provision 只把 `vpn` / `zhihu-search` / `global-search` 复制到该 profile 自己的 `skills/` 目录;后续用户可以继续给自己的 personal profile 增加其它 skill 并启停,不做长期强白名单。`skills.disabled` 只表示该 profile 已有 skill 的关闭状态,不表示模板范围。
- [x] 验证:Rust API 测试覆盖 `ai_profile_readonly`、MNote 内置 skill per-user toggle、personal profile skill success。
- 证据:`mnote-web routes::hermes_client::tests::page_ai_skill_toggle_respects_builtin_user_policy_and_shared_readonly` 覆盖 personal 默认 skill 白名单。
### Batch E - MNote 内置 skill per-user policy
- [x] 将 MNote 内置 skill enable/disable 保存为 SQLite per-user policy。
- [x] UI 中 `hide_builtin_skills` 只影响展示,不影响 enable/disable。
- 证据:隐藏 key 收口为 `ai.agent.hermes.skills.hide_builtin`MNote 内置 enable 使用独立 `ai.agent.mnote_builtin.skill.<id>.enabled`
- [x] Skills panel 标记 `builtin=true``configurable=true``configScope=user_sqlite`
- [x] 服务端 MNote tool policy 按 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 判断。
- 证据:`create_run` 服务端读取当前用户 SQLite preference 并覆盖 `skillPreferences.mnote`,不信任浏览器提交。
- [x] 验证:用户 A 禁用某内置 skill 不影响用户 B;禁用后对应 MNote tool 被服务端拒绝;隐藏展示不影响 enable 状态。
- 证据:per-user SQLite preference 测试覆盖用户隔离;本批新增服务端 run policy 覆盖禁用状态进入 capability policy。工具调用显式拒绝仍可后续细化到每个 tool handler。
### Batch F - Reasonix memory policy
- [x] 新增 per-user 设置 `ai.agent.reasonix.memory_enabled`,默认 `false`
- [x] Reasonix ACP spawn 默认设置 `REASONIX_MEMORY=off`
- [x] 开启 memory 后不注入 `REASONIX_MEMORY=off`,并在 UI 显示 memory enabled。
- 证据:开启后服务端注入 `REASONIX_MEMORY=on`UI Reasonix 设置页显示 memory 状态。
- [x] 补 JS/Rust 测试或 smoke,覆盖默认 off、用户开启、不同用户隔离 preference。
- 证据:`mnote-web routes::hermes_client::tests::reasonix_memory_policy_defaults_off_and_reads_user_preference`
- [x] 验证:普通消息由 Reasonix 自己决定是否使用工具;MNote 不再强行注入 memory/context 正文。
- 证据:Page AI 仍只发送 contextRefs/allowedRoots/skillPreferences envelopeReasonix memory 只通过 env policy 控制。
### Batch G - UI 收口
- [x] Agent selector 中 Hermes profile 显示 personal/shared/readonly 状态。
- [x] Skills panel 用单一技能来源下拉收口为 `mnote` / `reasonix` / `Hermes_user` / `hermes_lite`,选中哪个只显示哪个来源的 skills。
- [x] Hermes profile 切换必须刷新 skill catalog,避免旧 profile skill 残留。
- [x] shared profile 的 Hermes profile skills 对普通用户展示只读开关或锁定状态。
- [x] MNote 内置 skills 对普通用户展示可启停状态,并标明按当前 MNote 用户保存。
- [x] 管理员对 shared profile 显示可管理状态,并提示影响所有用户。
- [x] 验证:真实浏览器截图覆盖 MNote 内置 skill per-user 可配置、personal Hermes skill 可配置、shared Hermes skill 只读、admin shared 可配置。
- 证据:`scripts/task502-page-ai-agent-selector-context-smoke.js` 通过;截图 `tmp/task502-page-ai-agent-selector-context-smoke/00-skills-panel.png` 显示技能来源下拉与 `Hermes_user · 我的 Hermes` 单来源 skill 列表。
- 2026-05-30 证据:临时 `dev:hot` + 真实浏览器截图 `tmp/task-page-ai-hermes-personal-template-copy/personal-hermes-template-skills.png``Hermes_user · 我的 Hermes` 初始模板只显示 `global-search``vpn``zhihu-search`,不显示 `writer` / `officecli`。Rust 测试覆盖“初始 3 个 skillprofile 后续新增 `writer` 后可开启并出现在 catalog”。
### Batch H - 回归矩阵与文档收尾
- [x] 更新 Page AI 设计说明,明确 MNote 内置 skill per-user policy、personal/shared Hermes profile 与 Reasonix memory policy。
- [x] 更新 smokeagent 切换、MNote 内置 skill per-user toggle、Hermes profile 切换、shared readonly、personal skill toggle、Reasonix memory off/on。
- [x] 运行 `node --check` 覆盖相关 browser runtime / smoke。
- [x] 运行 Rust 定点测试覆盖 SQLite profile policy 与 Hermes skill toggle 权限。
- [x] 运行真实浏览器验证并截图。
- [x] 运行 `git diff --check`
- [x] 涉及代码图后运行 `codegraph sync .`
- [x] 完成后将本 checklist 移动到 `done/` 或标记为 `done`
- 状态已标记为 `done`2026-06-01 已迁入 `design/07-ai/done/`
## 8. 验收口径
完成后必须满足:
- 普通用户可以启停自己的 MNote 内置 skills,开关保存到 SQLite 并由服务端 tool policy 执行。
- 用户 A 的 MNote 内置 skill 开关不影响用户 B。
- 普通用户无法修改 shared `lite` 的 Hermes profile skills。
- 普通用户可以修改自己的 personal Hermes profile skills。
- MNote 内置 skill 的显示/隐藏与启停是两个不同状态:隐藏只影响 UI,启停影响服务端可调用性。
- Hermes profile 切换后 skill catalog 正确刷新。
- Reasonix 默认 memory off,开启 memory 是 per-user preference。
- Page AI run payload 不再用裸 Hermes profile name 表达身份,而是经服务端解析的 `agentProfileRef` / `profileId`
- 所有文件访问仍受 SQLite directory grants / allowedRoots 约束。
@@ -0,0 +1,407 @@
# 7-42 Page AI mindmap skill and resource generation v1
> 创建时间:2026-05-30
> 状态:`done`
> owner`07-ai`
> 上位参考:
> - `design/07-ai/reference/7-28-resource-ai-tool-contract-v1.md`
> - `design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
> - `design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md`
## 1. 背景
当前 Page AI 的 MNote 内置技能只有:
- `mnote-current-page`
- `mnote-local-file`
- `mnote-chat-only`
但 Rust Hermes tool manifest 已经暴露了资源工具雏形:
- `mnote.mindmap.fetch`
- `mnote.mindmap.apply_ops`
这说明底层 resource tool 方向已经存在,缺口不在“完全没有工具”,而在 Page AI 缺少一个明确的 `mnote-mindmap` 内置 skill 来指导 agent 何时读取、何时写入、如何生成新的思维导图资源,以及如何把 PDF / Office / Markdown 等材料整理成新的 `.mindmap.json`
用户明确的长期目标是:
> AI 能总结 PDF,并整理新建出思维导图。
因此本设计不能只覆盖“编辑已有导图节点”,还必须覆盖“从外部材料生成新导图资源”的闭环。
## 2. 原型观察
### 2.1 KMind plugin
`reference-code/kmind-plugin` 体现的关键点:
-`simple-mind-map` 风格树结构为核心。
- 支持多根、MOC、文档树导图、节点超链接、TODO、主题、布局、导入导出。
- 节点可以携带思源块/文档引用,说明 mindmap 节点不只是纯文本,还可能是资源引用容器。
- 导入导出会处理 markdown、Freemind、XMind 等格式,但最终仍要落回 mindmap runtime 可消费的数据结构。
对 MNote 的启发:
- AI 不能把思维导图降级为普通 Markdown 大纲后直接覆盖文件。
- AI 写入必须尽量保留未知扩展字段,如节点样式、引用、视图状态、主题配置。
- 从 PDF 生成导图时,应先生成结构化 outline,再转换成 mindmap tree,而不是让模型手写完整 runtime JSON。
### 2.2 lx-doc mind-map
`reference-code/lx-doc/mind-map` 体现的关键点:
- 思维导图项目独立部署,工作台只负责文件/资源管理。
- 文件内容是完整对象,典型形态为:
- `root`
- `theme`
- `layout`
- `config`
- `view`
- runtime 使用 `setFullData` 恢复完整文件,用 `getData(true)` 保存全量配置。
- `root``simple-mind-map` 树:`{ data: { text, uid, ... }, children: [...] }`
对 MNote 的启发:
- mindmap 是 Resource Tree 对象,不是 Markdown 正文的一部分。
- Markdown 页面只保留占位、链接或嵌入引用。
- AI 读写 mindmap 时应围绕 resource 文件、object identity 和 resource capability 工作。
## 3. 当前 MNote 数据合同
当前默认 `.mindmap.json` 不是裸树,而是 envelope
```json
{
"data": {
"children": [],
"data": {
"expand": true,
"isActive": false,
"text": "KMIND",
"uid": "root"
}
},
"view": {
"state": {
"scale": 1,
"sx": 0,
"sy": 0,
"x": -44.99991989135742,
"y": -15.500006675720217
},
"transform": {
"a": 1,
"b": 0,
"c": 0,
"d": 1,
"e": -44.99991989135742,
"f": -15.500006675720217,
"originX": 0,
"originY": 0,
"rotate": 0,
"scaleX": 1,
"scaleY": 1,
"shear": 0,
"translateX": -44.99991989135742,
"translateY": -15.500006675720217
}
}
}
```
设计约束:
- 最终写入文件必须保持 envelope。
- 根节点位于 `data.data`
- 子节点位于 `data.children`
- 新建导图默认 `data.data.uid``root`
- 新建导图默认 `data.data.text` 可由用户材料标题覆盖;没有标题时使用 `KMIND`
- `view` 默认使用上述稳定模板;AI 不应自行发明缩放和位移。
- 修改已有导图时必须保留未知字段,包括 `view`、节点样式、节点引用、主题和未来扩展字段。
## 4. 目标
- 新增 Page AI MNote 内置 skill`mnote-mindmap`
- 让 Hermes / Reasonix 在 Page AI 中知道如何读写当前页面或当前资源 tab 的 mindmap。
- 让 AI 能从 PDF、Office、Markdown、当前页内容或用户粘贴文本生成层级 outline,再创建新的 mindmap resource。
- 让新建导图进入 Resource Tree / File Tree / Page Tree 的正确边界,而不是写进 Markdown 正文。
- 保留 local-first 主线:本地 `.mindmap.json` 是本地导图资源真相;Rust control-plane 负责授权、审计和 resource identity。
## 5. 非目标
- 不在本批实现 PDF OCR / MinerU / Office 文本抽取本身。
- 不把 mindmap 正文存进 Markdown 页面。
- 不让 Page AI 前端私有拼第二套 mindmap 真相。
- 不让 agent 直接大段重写完整 JSON 作为默认写入方式。
- 不引入轮询刷新链路;写入后的 UI 同步应走已有 watcher、resource session 或命令结果驱动刷新。
## 6. Skill 设计
新增内置 skill
```text
id: mnote-mindmap
title: MNote mindmap editing
description: Read, update, summarize, or create MNote mindmap resources, including generating a new mindmap from PDF or document outlines.
agentIds: hermes, reasonix
readOnly: false
requiresContextRefs: current_page, file, folder, resource
toolNames:
- mnote.context.snapshot
- mnote.context.resolve_target
- mnote.mindmap.fetch
- mnote.mindmap.apply_ops
- mnote.mindmap.create_from_outline
```
Skill 正文规则:
- 只有用户明确要求“思维导图 / mindmap / KMind / 脑图 / 从材料生成导图”时启用。
- 先解析目标资源,再读取内容。
- 若当前 Page AI target 是 mindmap resource tab,优先使用该资源。
- 若当前页面包含 mindmap embed,占位中的 `mindmapId` / `sourcePath` 是候选资源。
- 若用户要求“从 PDF 生成导图”,先读取或请求 PDF 摘要/outline,再调用 mindmap 创建工具。
- 写前必须确认 `permissionLevel=read_write`,并确认 `allowedResourceIds` 覆盖目标资源。
- 写后必须回读,并报告新建或变更的 `.mindmap.json`
- 生成 outline 时必须使用“中心主题 + 一级分类 + 短语化节点”的导图结构;禁止把 PDF/文档摘要以长段落塞进节点文本。
- 节点文本优先使用关键词或短语,一个节点只表达一个概念;层级必须用 `children` 表达,引用/页码/来源信息优先放入 `sourceRefs` 或 metadata。
## 7. Tool 面设计
### 7.1 `mnote.mindmap.fetch`
保留现有命名,增强返回结构。
输入:
- `workspaceId`
- `documentId`
- `mindmapId`
- `rootUri`
- `resourcePath`
- `scope`: `tree | subtree | markdown_summary | full_envelope`
- `nodeId`
- `aiAccessScope`
输出:
- `objectIdentity`
- `resourceKind=mindmap`
- `mindmapId`
- `resourcePath`
- `revision`
- `envelope`,仅 `full_envelope` 返回
- `root`
- `nodes`
- `edges`
- `markdownSummary`
- `source=local_folder`
约束:
- `tree` / `markdown_summary` 默认不返回完整 envelope,避免 prompt 被视图和样式噪音污染。
- `full_envelope` 只在需要保留或精确 patch 文件时使用。
### 7.2 `mnote.mindmap.apply_ops`
当前该工具已存在,但 local resource 非 dry-run 仍提示 agent 用原生 patch。后续应收口为真正结构化写入工具。
输入:
- `workspaceId`
- `documentId`
- `mindmapId`
- `rootUri`
- `resourcePath`
- `expectedRevision`
- `ops`
- `aiAccessScope`
建议 ops
- `updateText`
- `insertChild`
- `insertSiblingAfter`
- `deleteNode`
- `setHyperlink`
- `setRefs`
- `appendNote`
- `patchView`
- `setLayout`
- `setTheme`
约束:
- 默认只改 `data` 树和指定 metadata。
- 未知字段必须透传。
- `expectedRevision` 不匹配时拒写或返回 conflict。
- 写入后返回 `revision``changedFiles``markdownSummary`
### 7.3 `mnote.mindmap.create_from_outline`
新增工具,用于“从材料生成新导图”。
输入:
- `workspaceId`
- `documentId`
- `rootUri`
- `targetPageId``targetDocumentId`
- `resourcePath`,可选;缺省由服务端生成唯一 `.mindmap.json`
- `title`
- `outline`
- `sourceRefs`
- `aiAccessScope`
- `embedIntoPage`,默认 `true`
`outline` 建议格式:
```json
[
{
"text": "一级主题",
"children": [
{
"text": "二级主题",
"children": []
}
]
}
]
```
输出:
- `objectIdentity`
- `mindmapId`
- `resourcePath`
- `envelope`
- `revision`
- `changedFiles`
- `embedResult`
- `markdownSummary`
写入规则:
- 服务端负责把 outline 转换为默认 envelope。
- 根节点文本使用 `title`,没有标题时使用 `KMIND`
- 子节点 uid 由服务端稳定生成,避免模型生成重复 uid。
- 默认 view 使用当前稳定模板。
-`embedIntoPage=true`,通过 Resource Tree / Markdown embed 合同把资源绑定到当前页面。
## 8. PDF 到导图闭环
长期目标链路:
```text
PDF resource
-> 文本/OCR/章节提取
-> AI 生成层级 outline
-> mnote.mindmap.create_from_outline
-> 写入 .mindmap.json
-> Resource Tree 绑定当前页面
-> 打开 mindmap resource tab
```
本设计只冻结后半段合同:
- PDF 摘要工具输出应是 `title + outline + sourceRefs`
- `sourceRefs` 可以记录 PDF 文件、页码、章节、引用片段。
- mindmap 节点可把 `sourceRefs` 保存到节点 `refs``note`,但第一版只要求保留到工具审计和 root metadata。
后续可接入:
- `mnote.office.fetch_summary`
- `mnote.pdf.extract_outline`
- MinerU markdown 识别结果
- 本地 Markdown / 当前页正文
## 9. 权限与审计
读写 mindmap resource 必须同时满足:
- 当前 actor 拥有 root 的 read 或 write grant。
- `aiAccessScope.permissionLevel` 覆盖目标操作。
- `allowedResourceIds` 包含 `mindmapId``resourcePath``objectIdentity`
- 写入只能落在授权 root 内。
审计必须记录:
- tool name
- sessionId / runId / traceId
- actorId
- documentId
- mindmapId
- resourcePath
- sourceRefs
- changedFiles
- before / after revision
## 10. Page AI 上下文
Page AI run payload 应能携带当前 mindmap target
```json
{
"contextRefs": [
{
"kind": "resource",
"resourceKind": "mindmap",
"objectIdentity": "resource:mindmap:{documentId}:{mindmapId}",
"documentId": "{documentId}",
"mindmapId": "{mindmapId}",
"resourcePath": "{relativePath}",
"rootUri": "{rootUri}"
}
]
}
```
来源优先级:
1. 当前打开的 mindmap resource tab。
2. 当前页面选中的 mindmap block/embed。
3. 当前页面中唯一 mindmap embed。
4. 用户明确给出的资源文件路径。
5. 创建新导图时,当前页面作为目标页面。
## 11. 验收清单
- [x] MNote 内置技能面板出现 `MNote mindmap editing`
- [x] `mnote-mindmap` 开关能进入 `skillPreferences.mnote`
- [x] `mnote.skill.read` 能读取 `mnote-mindmap` 正文。
- [x] `mnote-mindmap` 正文说明导图 outline 格式:中心主题、一级分类、短语化节点、避免长段落。
- [x] `mnote.mindmap.fetch` 可读取默认 envelope 并返回 root/nodes/summary。
- [x] `mnote.mindmap.create_from_outline` 可生成符合默认 envelope 的 `.mindmap.json`
- [x] `mnote.mindmap.create_from_outline` 在显式 `embedIntoPage=true` 时可把新导图链接写入当前 local-md 页面。
- [x] `task503-mindmap-skill-capability-smoke` 可直接读取 skill、创建能力导图、fetch 回读、绑定当前页面,并用真实登录浏览器截图验证。
- [x] 新建导图能绑定当前页面并在 File Tree / Resource Tab 可见。
- [x] `mnote.mindmap.apply_ops` 写入后保留 `view` 和未知字段。
- [x] shared/read-only scope 下写工具拒绝。
- [x] revision 不匹配时拒写或返回 conflict。
- [x] PDF outline fixture 可生成 mindmap resource。
- [x] 真实 mindmap resource tab 能注入 Page AI `active_editor` contextRef / `targetPackage`
当前第一批已完成 Page AI skill 发现、skill 正文读取、默认 envelope fetch、`create_from_outline` 写入、显式 `embedIntoPage=true` 页面链接绑定、`task502` Page AI skill payload smoke,以及 `task503` 直接 tool 创建导图 + 页面绑定 + 浏览器截图 smoke。第二批已完成 `apply_ops` 最小结构化写入、`view` / 未知字段保留、revision conflict 和 shared/read-only 拒写单测;2026-06-01 已修复 `task455` 暴露的 local-folder embedded mindmap 刷新后 id 退化,根因是 `blockDocument.blocks[].attrs` 未保留 `mindmapId/sourcePath/rootNodeId` 且浏览器 conversion 未读取 blockDocument attrs;新增 `task525` 覆盖真实 mindmap resource tab 的 Page AI `active_editor` contextRef / `targetPackage`
2026-06-01 复核:`mnote.mindmap.apply_ops` 已从 native patch 提示收口到最小结构化写入,当前支持 `updateText` / `updateNode``insertChild` / `addChild``deleteNode`,并由 `hermes_tools_mindmap_apply_ops_writes_and_preserves_envelope_fields``hermes_tools_mindmap_apply_ops_rejects_stale_revision``hermes_tools_mindmap_apply_ops_shared_read_is_forbidden` 覆盖。resource tab / File Tree 可见性和 Page AI mindmap contextRefs 已由 `task455``task524``task525` 验收。后续若继续扩展 `moveNode``insertSiblingAfter``setHyperlink``setRefs``appendNote``patchView`,应另拆 P2 follow-up,不阻塞本轮最小闭环。
## 12. 第一批执行建议
第一批只做可控闭环:
1. 新增 `skills/mnote-mindmap/SKILL.md`
2.`hermes_tools::skill` 注册 `mnote-mindmap`
3. 补 Page AI skill 面板 smoke。
4.`mnote.skill.read` 单测。
5. 增强 `mnote.mindmap.fetch` 输出,识别当前默认 envelope。
6. 新增 `mnote.mindmap.create_from_outline` dry-run 与真实写入。
7. 用 fixture 模拟 PDF 摘要,不接真实 PDF OCR。
第二批再做:
1. 补浏览器 smoke,验证新建或修改后的 mindmap 能在 File Tree / Resource Tab 可见。
2. 接入真实 PDF / Office / MinerU 结果。
3. 在资源 tab 中把当前 mindmap 自动注入 Page AI contextRefs。
4. 按真实编辑需求继续扩展 `apply_ops`,例如 `moveNode``insertSiblingAfter``setHyperlink``setRefs``appendNote``patchView`
@@ -0,0 +1,292 @@
# 7-44 ChatOnly Doubao session binding v1
> 创建时间:2026-05-31
>
> 状态:`done`
>
> OwnerPage AI ChatOnly / Hermes ACP runtime / SQLite control-plane / OpenClaw Doubao Web provider
>
> 上位依据:
> - `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
> - `design/07-ai/done/7-30-acp-session-load-resume-checklist-v1.md`
> - `design/07-ai/done/7-33-acp-session-info-plan-ui-checklist-v1.md`
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
> - `design/07-ai/done/7-41-page-ai-hermes-reasonix-user-profile-isolation-v2.md`
## 1. 背景
MNote Page AI 的 `Chat-only / 豆包` 当前已经能通过 Hermes ACP -> OpenClaw `doubao-web` provider 调用豆包网页,但 MNote 会话与豆包网页会话还没有稳定统一:
- MNote 的会话主记录在 SQLite `ai_runtime_runs` / `ai_runtime_events`,按 `user_id + workspace_id + session_id` 查询和软删除。
- Hermes / ACP 层有 `acpSessionId`MNote 会通过 `session.info.updated` 持久化它。
- OpenClaw `doubao-web` provider 会从豆包 SSE 中捕获 `conversation_id`,并在进程内 `sessionMap` 中用 ACP `sessionId` 复用豆包会话。
-`sessionMap` 没有落 SQLite。MNote 重启、OpenClaw 重启、跨用户、删除会话时,都无法可靠知道某个 MNote ChatOnly session 对应哪条豆包网页 conversation。
2026-05-31 已验证 ChatOnly 豆包重复会话的直接根因是 Hermes `title_generation` 复用了豆包主模型;当前已在 `openclaw-doubao-chat` profile 禁用标题生成辅助调用。但这只解决“一条消息变两条豆包会话”的触发点,不解决会话生命周期统一。
## 2. 目标
本阶段目标是让 MNote 成为 ChatOnly 豆包会话的本地控制面:
- MNote ChatOnly session 与豆包 `conversation_id` 建立持久绑定。
- 后续同一个 MNote session 继续发送时,复用同一个豆包 conversation。
- 删除 MNote session 时,对豆包远端 conversation 做最佳努力删除。
- 多用户会话隔离以 SQLite `user_id` 为准,不能只依赖 OpenClaw 全局内存。
- 删除失败不能阻塞 MNote 本地删除,但必须可审计、可重试。
非目标:
- 不把豆包网页作为 MNote 会话真源。
- 不同步豆包网页中用户手工创建的所有历史会话。
- 不承诺豆包接口稳定可用;远端删除属于 provider-specific best effort。
- 不在 ChatOnly 下申请 MNote 文件写权限。
## 3. 豆包侧接口取证
### 3.1 发送 / 继续会话
当前 OpenClaw `DoubaoWebClientBrowser` 发送消息使用:
```text
POST /samantha/chat/completion
```
请求体关键字段:
```json
{
"completion_option": {
"need_create_conversation": false,
"is_delete": false
},
"conversation_id": "38428454119180290"
}
```
`conversation_id` 为空或 `"0"` 时,豆包创建新 conversation。响应 SSE 中会出现 `conversation_id`OpenClaw 已能解析并打印:
```text
[Doubao Web Browser] Captured conversation_id: ...
```
### 3.2 删除会话
从豆包当前 Web UI 已加载脚本和 CDP 请求监听确认,删除会话优先走 IM cmd 链路:
```text
POST /im/conversation/batch_del_user_conv
```
请求头关键字段:
```text
content-type: application/json; encoding=utf-8
accept: application/json, text/plain, */*
agw-js-conv: str
```
请求体结构:
```json
{
"cmd": 4171,
"uplink_body": {
"batch_delete_user_conversation_uplink_body": {
"conversation_id": ["38428454119180290"],
"delete_all": false,
"conversation_type": 3
}
},
"sequence_id": "uuid",
"channel": 2,
"version": "1"
}
```
其中 `conversation_type: 3` 对应 `ONE_TO_BOT_CHAT`
脚本中还存在旧 wrapper
```text
POST /samantha/im/conversation/batch_delete
```
但当前删除弹窗路径使用 `/im/conversation/batch_del_user_conv`,实现优先采用该路径。
## 4. 数据合同
新增 SQLite 控制面表:
```text
ai_external_conversation_bindings
- id
- user_id # 当前 MNote 用户,必填
- workspace_id # 可空,但列表/删除必须按上下文过滤
- mnote_session_id # MNote ChatOnly session_id
- acp_session_id # Hermes/OpenClaw ACP sessionId,可空,收到后补齐
- agent_id # chat_only
- profile # openclaw-doubao-chat 等
- provider # doubao-web
- remote_conversation_id # 豆包 conversation_id
- remote_url # https://www.doubao.com/chat/{remote_conversation_id}
- status # active | local_deleted | remote_deleted | remote_delete_failed
- metadata_json # 捕获来源、失败原因、最后响应摘要
- created_at
- updated_at
- deleted_at
```
约束:
- `user_id + provider + remote_conversation_id` 唯一,避免同一豆包会话绑定给多个 MNote 用户。
- `user_id + mnote_session_id + provider` 唯一,避免一个 MNote session 绑定多条豆包远端会话。
- 查询、恢复、删除都必须带当前 `user_id`;不能只按 `mnote_session_id` 查。
## 5. 数据流
### 5.1 创建 MNote ChatOnly session
1. 前端 `pageAiEnsureHermesSession(forceCreate=true)``/api/hermes/client/sessions`
2. mnote-web 写入 SQLite session index run`status=session.created`
3. 不立即创建豆包远端 conversation。
4. 绑定表暂不写,或写入一行 `remote_conversation_id=NULL` 的 pending 记录。
### 5.2 第一次发送
1. mnote-web 创建 runpayload 携带 `sessionId``agentId=chat_only``profile=openclaw-doubao-chat`
2. mnote-web 查询绑定表;若无 `remote_conversation_id`,不传远端 ID。
3. OpenClaw 发送给豆包,豆包创建新 conversation。
4. OpenClaw 从 SSE 捕获 `conversation_id`
5. OpenClaw 需要把 `conversation_id` 作为结构化事件回传给 MNote。建议事件:
```json
{
"event": "provider.conversation.bound",
"data": {
"provider": "doubao-web",
"remoteConversationId": "38428454119180290",
"remoteUrl": "https://www.doubao.com/chat/38428454119180290"
}
}
```
6. mnote-web 在 `persist_acp_runtime_event` 中识别该事件,upsert `ai_external_conversation_bindings`
### 5.3 继续发送
1. mnote-web 根据 `user_id + mnote_session_id + provider=doubao-web` 查询绑定。
2. 若找到 active `remote_conversation_id`,在传给 ACP/OpenClaw 的 payload 中加入:
```json
{
"providerConversation": {
"provider": "doubao-web",
"remoteConversationId": "38428454119180290"
}
}
```
3. OpenClaw Doubao provider 使用该 ID 调 `/samantha/chat/completion`,设置 `need_create_conversation=false`
4. 若豆包返回会话不存在或被删除,OpenClaw 发 `provider.conversation.missing`;MNote 标记绑定异常,由用户决定是否新建远端会话。
### 5.4 删除 MNote session
1. 用户在 MNote 删除 ChatOnly 会话。
2. mnote-web 先对 `ai_runtime_runs` 做本地软删除。
3. mnote-web 将绑定表标记为 `local_deleted`
4. 若存在 `remote_conversation_id`,调用 OpenClaw / provider adapter 执行远端删除:
```text
POST /im/conversation/batch_del_user_conv
```
5. 成功后标记 `remote_deleted`
6. 失败时保持本地删除已完成,绑定标记 `remote_delete_failed``metadata_json` 记录错误码、响应摘要和时间。
7. UI 提示:`MNote 会话已删除,豆包远端删除失败,可稍后重试`
## 6. API 边界
### 6.1 mnote-web 内部 API
建议新增 provider conversation helper,不把豆包细节散落在 `hermes_client.rs`
```text
rust/crates/mnote-web/src/provider_conversations.rs
```
职责:
- 解析 run payload 中的 ChatOnly provider。
- 读写 `ai_external_conversation_bindings`
- 将 binding 注入 ACP run payload。
- 处理 `provider.conversation.bound/missing/deleted/delete_failed` 事件。
### 6.2 OpenClaw Doubao provider
需要在 OpenClaw `doubao-web` provider 增加三个能力:
- 从 ACP/context payload 读取 `providerConversation.remoteConversationId`
- 捕获新 `conversation_id` 后发结构化事件,不能只写 console log。
- 暴露 `deleteConversation(remoteConversationId)`,内部走 `/im/conversation/batch_del_user_conv`
第一阶段如果 ACP 不支持 provider 自定义 RPC,可先由 mnote-web 调一个 OpenClaw 本地 HTTP helper;但长期应收口到 provider adapter。
## 7. 错误处理
| 场景 | 行为 |
| --- | --- |
| 豆包创建成功但未捕获 `conversation_id` | run 仍完成;绑定缺失;下一轮可能新建远端会话;UI 标记未绑定 |
| 绑定表有 ID,但豆包返回不存在 | 标记 `remote_missing`,提示用户重新绑定或新建 |
| 删除 MNote 本地成功,豆包远端失败 | 不回滚本地删除;标记 `remote_delete_failed` |
| 多用户尝试绑定同一远端 ID | 拒绝后写 audit,避免跨用户串会话 |
| OpenClaw 重启 | SQLite 绑定仍在;下一轮从 MNote 注入远端 ID |
| 豆包接口变更 | 本地会话不受影响;远端能力降级为不可用 |
## 8. 验收
### 8.1 单元 / 集成
- `control-plane`binding upsert / lookup / local delete / remote delete status transition。
- `mnote-web`ChatOnly run payload 能注入已有 `remoteConversationId`
- `mnote-web``provider.conversation.bound` 事件能写入 SQLite。
- `mnote-web`:删除 session 时先软删除本地,再 best-effort 调 provider delete。
### 8.2 真实浏览器 smoke
1. 使用测试账号登录 `http://localhost:3000`
2. 创建 ChatOnly / 豆包新会话,发送 marker A。
3. 复查:
- MNote SQLite 有一个 `mnote_session_id -> remote_conversation_id` 绑定。
- 豆包日志 `Captured conversation_id` 一次。
4. 在同一 MNote 会话发送 marker B。
5. 复查:
- 豆包日志第二次 `Conversation ID` 等于第一次捕获值。
- 豆包网页同一 conversation 中出现 A 与 B。
6. 删除 MNote 会话。
7. 复查:
- MNote 会话列表不再显示该 session。
- SQLite binding status 为 `remote_deleted``remote_delete_failed`
- 若远端删除成功,豆包网页侧该 conversation 从列表移除或打开后显示已删除。
2026-06-01 验证记录:
- `node scripts/task512-chatonly-doubao-sync-smoke.js` 通过:豆包远端 conversation 绑定、同会话回复、MNote session 删除、provider delete 和 SQLite binding `remote_deleted` 均通过。
- `node scripts/task513-chatonly-provider-sync-smoke.js deepseek` 通过:DeepSeek `remoteConversationId` 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
- `node scripts/task513-chatonly-provider-sync-smoke.js gemini` 通过:Gemini conversation URL 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
- `cargo test --manifest-path rust/Cargo.toml -p control-plane external_conversation -- --test-threads=1` 通过:binding user scope 与状态迁移。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib provider_conversation -- --test-threads=1` 通过:provider conversation 注入与 bound event 持久化。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib chatonly_doubao_session_delete_calls_provider_and_marks_remote_deleted -- --test-threads=1` 通过:删除 session 后 provider delete 与 `remote_deleted` 状态。
## 9. 实施清单
- [x]`control-plane` 增加 `ai_external_conversation_bindings` schema、store trait 和 SQLite 实现。
- [x]`mnote-web` 增加 provider conversation helper,避免继续扩大 `hermes_client.rs`
- [x] 在 ACP run 创建时为 ChatOnly / 豆包注入已有 `remoteConversationId`
- [x] 在 ACP event 持久化时处理 `provider.conversation.bound`
- [x] 在 session delete 路径中加入远端删除 best-effort 状态机。
- [x] 在 OpenClaw Doubao provider 中加入结构化 conversation bound 事件。
- [x] 在 OpenClaw Doubao provider 中加入 `deleteConversation`,走 `/im/conversation/batch_del_user_conv`
- [x] 补真实浏览器 smoke`node scripts/task512-chatonly-doubao-sync-smoke.js` 覆盖远端 conversation id 绑定、豆包同会话 marker、session 删除、provider delete 和 SQLite binding `remote_deleted`
- [x] 补跨 provider 真实浏览器 smoke`node scripts/task513-chatonly-provider-sync-smoke.js deepseek``node scripts/task513-chatonly-provider-sync-smoke.js gemini`
- [x] 补 MNote Rust 单测:control-plane binding、mnote-web provider conversation 注入、bound event 持久化和 session delete provider 状态。
- [x] OpenClaw provider 源码不在本仓库,provider 单测缺口已转入 `bugs/07-ai/process/7-49-chatonly-openclaw-provider-unit-test-gap-v1.md` 跟踪,不阻塞 MNote 侧设计归档。