收口 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:
@@ -0,0 +1,394 @@
|
||||
# Page AI Mindmap Skill Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add the `mnote-mindmap` built-in Page AI skill and a first working `mnote.mindmap.create_from_outline` tool that generates MNote-compatible `.mindmap.json` envelopes from outlines.
|
||||
|
||||
**Architecture:** Keep Page AI skill discovery in `hermes_tools::skill`, keep tool manifest shape in `hermes_tools::manifest`, and keep resource file behavior in `hermes_tools::resource`. The first implementation uses local-folder resource files and does not introduce PDF OCR; PDF is represented by outline fixtures that later PDF/Office tools can feed into `create_from_outline`.
|
||||
|
||||
**Tech Stack:** Rust `mnote-web`, Axum route tests, serde_json, local-folder access guards, existing Hermes tool call pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
- Create: `skills/mnote-mindmap/SKILL.md`
|
||||
- Modify: `rust/crates/mnote-web/src/hermes_tools/skill.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/hermes_tools/resource.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- Modify: `scripts/task502-page-ai-agent-selector-context-smoke.js`
|
||||
- Test: `cargo test -p mnote-web --lib hermes_tools_mindmap --manifest-path rust/Cargo.toml`
|
||||
- Test: `cargo test -p mnote-web --lib skill_registry --manifest-path rust/Cargo.toml`
|
||||
- Test: `cargo test -p mnote-web --lib skill_read_returns_mindmap_skill_content --manifest-path rust/Cargo.toml`
|
||||
- Test: `cargo test -p mnote-web --lib page_ai_skill_toggle_respects_builtin_user_policy_and_shared_readonly --manifest-path rust/Cargo.toml`
|
||||
- Test: `node --check scripts/task502-page-ai-agent-selector-context-smoke.js`
|
||||
- Smoke: `node scripts/task502-page-ai-agent-selector-context-smoke.js`
|
||||
- Check: `git diff --check -- skills/mnote-mindmap/SKILL.md rust/crates/mnote-web/src/hermes_tools/skill.rs rust/crates/mnote-web/src/hermes_tools/manifest.rs rust/crates/mnote-web/src/hermes_tools/resource.rs rust/crates/mnote-web/src/routes/hermes_tools.rs rust/crates/mnote-web/src/routes/hermes_client.rs scripts/task502-page-ai-agent-selector-context-smoke.js docs/superpowers/plans/2026-05-30-page-ai-mindmap-skill.md design/07-ai/process/7-42-page-ai-mindmap-skill-and-resource-generation-v1.md`
|
||||
|
||||
## Task 1: Built-in Skill Registration
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/mnote-mindmap/SKILL.md`
|
||||
- Modify: `rust/crates/mnote-web/src/hermes_tools/skill.rs`
|
||||
|
||||
- [x] **Step 1: Write the failing skill registry test**
|
||||
|
||||
Add a test in `rust/crates/mnote-web/src/hermes_tools/skill.rs`:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn skill_registry_exposes_mindmap_skill_to_agents() {
|
||||
let reasonix_skills = skill_summaries_for_agent(Some("reasonix"));
|
||||
let skill = reasonix_skills
|
||||
.iter()
|
||||
.find(|skill| skill["id"] == "mnote-mindmap")
|
||||
.expect("reasonix should see mindmap skill");
|
||||
assert_eq!(skill["readOnly"], false);
|
||||
assert!(
|
||||
skill["requiresContextRefs"]
|
||||
.as_array()
|
||||
.expect("context refs")
|
||||
.iter()
|
||||
.any(|value| value == "resource")
|
||||
);
|
||||
assert!(
|
||||
skill["toolNames"]
|
||||
.as_array()
|
||||
.expect("tool names")
|
||||
.iter()
|
||||
.any(|name| name == "mnote.mindmap.create_from_outline")
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run the targeted failing test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib skill_registry_exposes_mindmap_skill_to_agents --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: FAIL because `mnote-mindmap` is not registered.
|
||||
|
||||
- [x] **Step 3: Add the skill file**
|
||||
|
||||
Create `skills/mnote-mindmap/SKILL.md` with the tool decision rules from `design/07-ai/process/7-42-page-ai-mindmap-skill-and-resource-generation-v1.md`.
|
||||
|
||||
- [x] **Step 4: Register the skill**
|
||||
|
||||
Add a `MnoteSkill` entry in `rust/crates/mnote-web/src/hermes_tools/skill.rs`:
|
||||
|
||||
```rust
|
||||
MnoteSkill {
|
||||
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.",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: false,
|
||||
requires_context_refs: &["current_page", "file", "folder", "resource"],
|
||||
tool_names: &[
|
||||
"mnote.context.snapshot",
|
||||
"mnote.context.resolve_target",
|
||||
"mnote.mindmap.fetch",
|
||||
"mnote.mindmap.apply_ops",
|
||||
"mnote.mindmap.create_from_outline",
|
||||
],
|
||||
content: include_str!("../../../../../skills/mnote-mindmap/SKILL.md"),
|
||||
},
|
||||
```
|
||||
|
||||
- [x] **Step 5: Re-run the skill tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib skill_registry --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: PASS for the skill registry tests.
|
||||
|
||||
## Task 2: Tool Manifest Contract
|
||||
|
||||
**Files:**
|
||||
- Modify: `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
|
||||
- [x] **Step 1: Write the failing manifest test**
|
||||
|
||||
Extend `hermes_tools_manifest_returns_first_batch_tools` or add a dedicated test that asserts:
|
||||
|
||||
```rust
|
||||
let create_tool = tools
|
||||
.iter()
|
||||
.find(|tool| tool["name"] == "mnote.mindmap.create_from_outline")
|
||||
.expect("create_from_outline tool");
|
||||
assert_eq!(
|
||||
create_tool["inputSchema"]["properties"]["outline"]["type"],
|
||||
"array"
|
||||
);
|
||||
assert!(
|
||||
create_tool["capabilityScope"]
|
||||
.as_array()
|
||||
.expect("scope")
|
||||
.iter()
|
||||
.any(|scope| scope == "mindmap.write")
|
||||
);
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run the failing manifest test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib hermes_tools_manifest_returns_first_batch_tools --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: FAIL because the manifest does not expose `mnote.mindmap.create_from_outline`.
|
||||
|
||||
- [x] **Step 3: Add `mindmap_create_from_outline_tool()`**
|
||||
|
||||
Add a write tool with required fields `documentId`, `mindmapId`, `outline`, `sessionId`, `runId`, `toolCallId`, `traceId`, and `actorId`. Include optional `title`, `rootUri`, `resourcePath`, `sourceRefs`, `embedIntoPage`, and `aiAccessScope`.
|
||||
|
||||
- [x] **Step 4: Add routing for the new tool**
|
||||
|
||||
In `execute_mnote_tool_call`, route:
|
||||
|
||||
```rust
|
||||
"mnote.mindmap.create_from_outline" => resource::mindmap_create_from_outline(&context, &input).await,
|
||||
```
|
||||
|
||||
- [x] **Step 5: Re-run the manifest test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib hermes_tools_manifest_returns_first_batch_tools --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: PASS for manifest exposure.
|
||||
|
||||
## Task 3: Mindmap Envelope Generation
|
||||
|
||||
**Files:**
|
||||
- Modify: `rust/crates/mnote-web/src/hermes_tools/resource.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
|
||||
- [x] **Step 1: Write the failing create-from-outline route test**
|
||||
|
||||
Add an Axum test in `rust/crates/mnote-web/src/routes/hermes_tools.rs` that calls `mnote.mindmap.create_from_outline` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"mindmapId": "maps/generated.mindmap.json",
|
||||
"resourcePath": "maps/generated.mindmap.json",
|
||||
"title": "PDF 摘要导图",
|
||||
"outline": [
|
||||
{
|
||||
"text": "章节一",
|
||||
"children": [
|
||||
{ "text": "要点 1", "children": [] }
|
||||
]
|
||||
}
|
||||
],
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "read_write",
|
||||
"allowedResourceIds": ["maps/generated.mindmap.json"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Assert the response and written file:
|
||||
|
||||
```rust
|
||||
assert_eq!(payload["result"]["resourceKind"], "mindmap");
|
||||
assert_eq!(payload["result"]["root"]["data"]["text"], "PDF 摘要导图");
|
||||
assert_eq!(payload["result"]["root"]["children"][0]["data"]["text"], "章节一");
|
||||
assert_eq!(payload["result"]["envelope"]["data"]["data"]["uid"], "root");
|
||||
assert!(root.join("maps").join("generated.mindmap.json").exists());
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run the failing create test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib hermes_tools_mindmap_create_from_outline_writes_default_envelope --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: FAIL because the tool does not exist.
|
||||
|
||||
- [x] **Step 3: Implement outline conversion helpers**
|
||||
|
||||
Implement helpers in `resource.rs`:
|
||||
|
||||
- `default_mindmap_view() -> Value`
|
||||
- `mindmap_envelope_from_outline(title: &str, outline: &Value) -> Result<Value, WebError>`
|
||||
- `mindmap_outline_items_to_children(outline: &[Value], path: &str) -> Vec<Value>`
|
||||
|
||||
Generated child node shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"expand": true,
|
||||
"isActive": false,
|
||||
"text": "节点文本",
|
||||
"uid": "node_1_1"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 4: Implement `mindmap_create_from_outline`**
|
||||
|
||||
Use existing write guard and path guard:
|
||||
|
||||
- call `ensure_resource_write_contract`
|
||||
- build `ResourceToolTarget`
|
||||
- call `ensure_resource_scope_allowed`
|
||||
- resolve write path under authorized root
|
||||
- create parent directory
|
||||
- write pretty JSON
|
||||
- return envelope, root, markdown summary, revision, and changed file path
|
||||
|
||||
- [x] **Step 5: Re-run the create test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib hermes_tools_mindmap_create_from_outline_writes_default_envelope --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 4: Fetch Envelope Awareness
|
||||
|
||||
**Files:**
|
||||
- Modify: `rust/crates/mnote-web/src/hermes_tools/resource.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
|
||||
- [x] **Step 1: Write a failing fetch test for the default envelope**
|
||||
|
||||
Add a test that writes a file shaped as:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"children": [],
|
||||
"data": {
|
||||
"expand": true,
|
||||
"isActive": false,
|
||||
"text": "KMIND",
|
||||
"uid": "root"
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"state": { "scale": 1, "sx": 0, "sy": 0, "x": 0, "y": 0 },
|
||||
"transform": { "a": 1, "b": 0, "c": 0, "d": 1, "e": 0, "f": 0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then call `mnote.mindmap.fetch` with `scope=full_envelope` and assert:
|
||||
|
||||
```rust
|
||||
assert_eq!(payload["result"]["root"]["data"]["text"], "KMIND");
|
||||
assert_eq!(payload["result"]["envelope"]["data"]["data"]["uid"], "root");
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run the failing fetch test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib hermes_tools_mindmap_fetch_reads_default_envelope --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: FAIL if fetch only understands naked simple-mind-map trees.
|
||||
|
||||
- [x] **Step 3: Normalize envelope root**
|
||||
|
||||
Update `collect_mindmap_nodes` and response building to use:
|
||||
|
||||
```rust
|
||||
fn mindmap_root_value(value: &Value) -> &Value {
|
||||
value.get("data")
|
||||
.filter(|data| data.get("data").is_some() || data.get("children").is_some())
|
||||
.unwrap_or(value)
|
||||
}
|
||||
```
|
||||
|
||||
Return `envelope` only when `scope == "full_envelope"`.
|
||||
|
||||
- [x] **Step 4: Re-run fetch tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib hermes_tools_mindmap_fetch --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: PASS for existing and envelope fetch tests.
|
||||
|
||||
## Task 5: Final Verification
|
||||
|
||||
**Files:**
|
||||
- All touched files from prior tasks.
|
||||
|
||||
- [x] **Step 1: Run non-mutating Rust format audit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo fmt --manifest-path rust/Cargo.toml --all --check
|
||||
```
|
||||
|
||||
Actual: command reports existing workspace-wide rustfmt drift, including unrelated dirty files and pre-existing touched-file formatting. Do not run mutating `cargo fmt` in this dirty worktree without an explicit cleanup decision.
|
||||
|
||||
- [x] **Step 2: Run targeted Rust tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web --lib skill_registry --manifest-path rust/Cargo.toml
|
||||
cargo test -p mnote-web --lib skill_read_returns_mindmap_skill_content --manifest-path rust/Cargo.toml
|
||||
cargo test -p mnote-web --lib hermes_tools_manifest_returns_first_batch_tools --manifest-path rust/Cargo.toml
|
||||
cargo test -p mnote-web --lib hermes_tools_mindmap --manifest-path rust/Cargo.toml
|
||||
cargo test -p mnote-web --lib page_ai_skill_toggle_respects_builtin_user_policy_and_shared_readonly --manifest-path rust/Cargo.toml
|
||||
```
|
||||
|
||||
Expected: all targeted tests pass.
|
||||
|
||||
- [x] **Step 3: Run JS syntax and browser smoke checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --check scripts/task502-page-ai-agent-selector-context-smoke.js
|
||||
node scripts/task502-page-ai-agent-selector-context-smoke.js
|
||||
```
|
||||
|
||||
Expected: syntax check passes; smoke captures `skillPreferences.mnote["mnote-mindmap"] = false` in the Page AI run payload.
|
||||
|
||||
- [x] **Step 4: Run diff whitespace check**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check -- skills/mnote-mindmap/SKILL.md rust/crates/mnote-web/src/hermes_tools/skill.rs rust/crates/mnote-web/src/hermes_tools/manifest.rs rust/crates/mnote-web/src/hermes_tools/resource.rs rust/crates/mnote-web/src/routes/hermes_tools.rs rust/crates/mnote-web/src/routes/hermes_client.rs scripts/task502-page-ai-agent-selector-context-smoke.js docs/superpowers/plans/2026-05-30-page-ai-mindmap-skill.md design/07-ai/process/7-42-page-ai-mindmap-skill-and-resource-generation-v1.md
|
||||
```
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [x] **Step 5: Inspect scoped git diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff -- skills/mnote-mindmap/SKILL.md rust/crates/mnote-web/src/hermes_tools/skill.rs rust/crates/mnote-web/src/hermes_tools/manifest.rs rust/crates/mnote-web/src/hermes_tools/resource.rs rust/crates/mnote-web/src/routes/hermes_tools.rs rust/crates/mnote-web/src/routes/hermes_client.rs scripts/task502-page-ai-agent-selector-context-smoke.js docs/superpowers/plans/2026-05-30-page-ai-mindmap-skill.md design/07-ai/process/7-42-page-ai-mindmap-skill-and-resource-generation-v1.md
|
||||
```
|
||||
|
||||
Expected: diff only contains `mnote-mindmap`, `create_from_outline`, envelope fetch support, tests, and docs.
|
||||
Reference in New Issue
Block a user