收口 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
@@ -142,6 +142,23 @@ async function main() {
body: JSON.stringify({ ok: true, tools: [] }),
});
});
await page.route("**/api/ai/agent-profiles**", async (route) => {
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
ok: true,
agentId: "hermes",
profiles: [
{ profileId: "shared_deepseek_chat", kind: "shared", displayName: "DeepSeek Chat", baseProfile: "deepseek-chat", isolatedProfile: "openclaw-deepseek-chat", canRun: true, canManageSkills: false, canManageConfig: false, readonly: true },
{ profileId: "shared_doubao_chat", kind: "shared", displayName: "豆包 Chat", baseProfile: "doubao-chat", isolatedProfile: "openclaw-doubao-chat", canRun: true, canManageSkills: false, canManageConfig: false, readonly: true },
{ profileId: "shared_gemini_chat", kind: "shared", displayName: "Gemini Chat", baseProfile: "gemini-chat", isolatedProfile: "openclaw-gemini-chat", canRun: true, canManageSkills: false, canManageConfig: false, readonly: true },
{ profileId: "usr_task502_default", kind: "personal", displayName: "我的 Hermes", baseProfile: "default", isolatedProfile: "mnote-u-task502-default", canRun: true, canManageSkills: true, canManageConfig: true },
{ profileId: "shared_lite", kind: "shared", displayName: "Lite", baseProfile: "lite", isolatedProfile: "lite", canRun: true, canManageSkills: false, canManageConfig: false, readonly: true },
],
}),
});
});
await page.route("**/api/hermes/client/profiles", async (route) => {
await route.fulfill({
status: 200,
@@ -184,12 +201,18 @@ async function main() {
return;
}
const runtime = url.searchParams.get("runtime");
const profile = url.searchParams.get("profile") || "mnoteai";
const profile = url.searchParams.get("profileId") || url.searchParams.get("profile") || "usr_task502_default";
const body = runtime === "mnote"
? {
ok: true,
runtime: "mnote",
categories: [{ name: "mnote", skills: [{ id: "mnote-current-page", name: "mnote-current-page", title: "当前页读取", description: "读取当前页", enabled: true, source: "mnote", origin: "builtin" }] }],
categories: [{
name: "mnote",
skills: [
{ id: "mnote-current-page", name: "mnote-current-page", title: "当前页读取", description: "读取当前页", enabled: true, source: "mnote", origin: "builtin", builtin: true, configurable: true, configScope: "user_sqlite", skillKind: "mnote_builtin" },
{ id: "mnote-mindmap", name: "mnote-mindmap", title: "思维导图读写", description: "读取、编辑或从 outline 生成思维导图", enabled: true, source: "mnote", origin: "builtin", builtin: true, configurable: true, configScope: "user_sqlite", skillKind: "mnote_builtin", toolNames: ["mnote.mindmap.fetch", "mnote.mindmap.create_from_outline"] },
],
}],
archived: [],
}
: runtime === "reasonix"
@@ -205,8 +228,8 @@ async function main() {
categories: [{
name: "writing",
skills: [
{ id: "hermes-builtin", name: "hermes-builtin", title: "Hermes builtin", description: `Builtin ${profile}`, enabled: true, source: "builtin", origin: "builtin" },
{ id: profile === "chemist" ? "hermes-chemist" : "hermes-writer", name: profile === "chemist" ? "hermes-chemist" : "hermes-writer", title: profile === "chemist" ? "Hermes chemist" : "Hermes writer", description: `Hermes ${profile}`, enabled: profile !== "chemist", source: "local", origin: "installed" },
{ id: "hermes-builtin", name: "hermes-builtin", title: "Hermes builtin", description: `Builtin ${profile}`, enabled: true, source: "builtin", origin: "builtin", skillKind: "hermes_profile", profileId: profile, configurable: profile !== "shared_lite" },
{ id: profile === "shared_lite" ? "hermes-lite" : "hermes-writer", name: profile === "shared_lite" ? "hermes-lite" : "hermes-writer", title: profile === "shared_lite" ? "Hermes lite" : "Hermes writer", description: `Hermes ${profile}`, enabled: profile !== "shared_lite", source: "local", origin: "installed", skillKind: "hermes_profile", profileId: profile, configurable: profile !== "shared_lite", readonly: profile === "shared_lite" },
],
}],
archived: [],
@@ -320,24 +343,59 @@ async function main() {
await agentButton.click({ timeout: UI_TIMEOUT_MS });
const agentPopover = page.locator("[data-page-ai-agent-popover]");
await agentPopover.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const agentIds = await page.$$eval("[data-page-ai-agent-id]", (nodes) =>
nodes.map((node) => node.getAttribute("data-page-ai-agent-id")).filter(Boolean),
await agentPopover.locator('[data-page-ai-agent-section="chat_only"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await agentPopover.locator('[data-page-ai-agent-section="hermes"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
assert.strictEqual(
await page.locator('[data-page-ai-agent-section="chat_only"] [data-page-ai-profile-id="shared_deepseek_chat"]').count(),
1,
"ChatOnly 二级菜单应包含 DeepSeek",
);
assert.deepStrictEqual(agentIds.sort(), ["chat_only", "hermes", "reasonix"]);
await page.locator('[data-page-ai-agent-id="hermes"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-agent-popover] [data-page-ai-profile-select]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-agent-popover] [data-page-ai-profile-select]').selectOption("chemist", { timeout: UI_TIMEOUT_MS });
assert.strictEqual(
await page.locator('[data-page-ai-agent-section="chat_only"] [data-page-ai-profile-id="shared_doubao_chat"]').count(),
1,
"ChatOnly 二级菜单应包含豆包",
);
assert.strictEqual(
await page.locator('[data-page-ai-agent-section="chat_only"] [data-page-ai-profile-id="shared_gemini_chat"]').count(),
1,
"ChatOnly 二级菜单应包含 Gemini",
);
assert.strictEqual(
await page.locator('[data-page-ai-agent-section="hermes"] [data-page-ai-profile-id="usr_task502_default"]').count(),
1,
"Hermes 二级菜单应包含个人 profile",
);
assert.strictEqual(
await page.locator('[data-page-ai-agent-section="hermes"] [data-page-ai-profile-id="shared_lite"]').count(),
1,
"Hermes 二级菜单应包含 shared_lite profile",
);
const agentChip = page.locator("[data-page-ai-agent-chip]");
await page.locator('[data-page-ai-agent-section="chat_only"] [data-page-ai-profile-id="shared_gemini_chat"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => document.documentElement.getAttribute("data-mnote-page-ai-profile") === "chemist",
() => document.documentElement.getAttribute("data-mnote-page-ai-agent-id") === "chat_only"
&& document.documentElement.getAttribute("data-mnote-page-ai-profile") === "shared_gemini_chat",
null,
{ timeout: UI_TIMEOUT_MS },
);
await page.locator('[data-page-ai-agent-id="reasonix"]').click({ timeout: UI_TIMEOUT_MS });
assert((await agentChip.innerText({ timeout: UI_TIMEOUT_MS })).includes("ChatOnly / Gemini"), "上下文按钮右侧标签应显示当前 ChatOnly agent");
await agentButton.click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-agent-section="hermes"] [data-page-ai-profile-id="shared_lite"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => document.documentElement.getAttribute("data-mnote-page-ai-agent-id") === "hermes"
&& document.documentElement.getAttribute("data-mnote-page-ai-profile") === "shared_lite",
null,
{ timeout: UI_TIMEOUT_MS },
);
assert((await agentChip.innerText({ timeout: UI_TIMEOUT_MS })).includes("Hermes / Lite"), "上下文按钮右侧标签应显示当前 Hermes profile");
await agentButton.click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-agent-section="reasonix"] [data-page-ai-agent-id="reasonix"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => document.documentElement.getAttribute("data-mnote-page-ai-agent-id") === "reasonix",
null,
{ timeout: UI_TIMEOUT_MS },
);
assert((await agentChip.innerText({ timeout: UI_TIMEOUT_MS })).includes("Reasonix"), "上下文按钮右侧标签应显示当前 Reasonix agent");
const contextRefs = page.locator("[data-page-ai-context-refs]");
assert.strictEqual(
@@ -351,6 +409,116 @@ async function main() {
await contextButton.evaluate((node) => Boolean(node.closest(".wolai-page-ai-composer-bar"))),
"上下文按钮应显示在输入区下方工具栏中",
);
const targetButton = page.locator("[data-page-ai-target-button]");
await targetButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
assert(
await targetButton.evaluate((node) => Boolean(node.closest(".wolai-page-ai-composer-bar"))),
"目标按钮应显示在输入区下方工具栏中",
);
const targetChip = page.locator("[data-page-ai-target-chip]");
await targetChip.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const targetChipText = (await targetChip.innerText({ timeout: UI_TIMEOUT_MS })).trim();
assert(targetChipText.includes("AgentContext") || targetChipText.includes("当前页"), `目标 chip 应展示当前写入目标: ${targetChipText}`);
await targetButton.click({ timeout: UI_TIMEOUT_MS });
const targetPopover = page.locator("[data-page-ai-target-popover]");
await targetPopover.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
assert(
await targetPopover.locator("[data-page-ai-target-option]").first().isVisible(),
"目标 popover 应提供至少一个可选目标",
);
await targetPopover.locator('[data-page-ai-action="close-target-popover"]').click({ timeout: UI_TIMEOUT_MS });
await page.evaluate(({ documentId, rootUri, workspaceId }) => {
const runtime = window.__mnoteDocumentPaneRuntime;
if (!runtime || typeof runtime.getOpenEditorsSnapshot !== "function") {
throw new Error("缺少 open editors snapshot runtime");
}
const original = runtime.getOpenEditorsSnapshot.bind(runtime);
runtime.getOpenEditorsSnapshot = () => {
const snapshot = original();
const mindmapResource = {
objectIdentity: "resource:mindmap:task502",
workspacePath: {
schema: "mnote.workspace_path.v1",
workspaceId,
sourceKind: "local_folder",
rootUri,
relativePath: "maps/Task502.mindmap.json",
documentId,
objectIdentity: "resource:mindmap:task502",
assetId: "task502-mindmap",
resourceKind: "mindmap",
},
paneRole: "primary",
documentId,
workspaceId,
title: "Task502 Mindmap",
kind: "mindmap",
editorKind: "mindmap",
active: false,
dirtyState: "",
preview: false,
pinned: true,
lastActiveAt: Date.now(),
assetId: "task502-mindmap",
path: "maps/Task502.mindmap.json",
};
const officeResource = {
objectIdentity: "resource:office:task502",
workspacePath: {
schema: "mnote.workspace_path.v1",
workspaceId,
sourceKind: "local_folder",
rootUri,
relativePath: "office/Task502 Deck.pptx",
documentId,
objectIdentity: "resource:office:task502",
assetId: "task502-office",
resourceKind: "office",
},
paneRole: "primary",
documentId,
workspaceId,
title: "Task502 Deck",
kind: "office",
editorKind: "office",
active: false,
dirtyState: "",
preview: false,
pinned: true,
lastActiveAt: Date.now(),
assetId: "task502-office",
path: "office/Task502 Deck.pptx",
onlyofficeSessionId: "mnote-oo-task502-office",
bridgeSessionId: "mnote-oo-task502-office",
bridgeSessionReady: true,
};
const resources = [mindmapResource, officeResource];
const withoutResource = (items) => (Array.isArray(items) ? items : [])
.filter((item) => !resources.some((resource) => item?.objectIdentity === resource.objectIdentity));
const groups = snapshot.groups || {};
const primary = groups.primary || {};
return {
...snapshot,
editors: [...withoutResource(snapshot.editors), ...resources],
resourceEditors: [...withoutResource(snapshot.resourceEditors), ...resources],
groups: {
...groups,
primary: {
...primary,
resourceEditors: [...withoutResource(primary.resourceEditors), ...resources],
},
},
};
};
}, { documentId, rootUri, workspaceId });
await targetButton.click({ timeout: UI_TIMEOUT_MS });
await targetPopover.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await targetPopover.locator('[data-page-ai-target-option="resource:office:task502"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => (document.querySelector("[data-page-ai-target-chip]")?.textContent || "").includes("Task502 Deck"),
null,
{ timeout: UI_TIMEOUT_MS },
);
assert.strictEqual(
await page.locator('.wolai-page-ai-composer-bar [data-page-ai-action="history"]').count(),
0,
@@ -366,39 +534,70 @@ async function main() {
);
await page.locator('.wolai-page-ai-header-actions [data-page-ai-tab="skills"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-panel="skills"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const skillSourceSelect = page.locator('[data-page-ai-panel="skills"] [data-page-ai-skill-source-select]');
await skillSourceSelect.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const skillSourceOptions = await skillSourceSelect.locator("option").evaluateAll((nodes) =>
nodes.map((node) => ({ value: node.value, text: node.textContent || "" })),
);
assert(skillSourceOptions.some((item) => item.value === "mnote" && item.text.includes("mnote")), "技能来源应包含 mnote");
assert(skillSourceOptions.some((item) => item.value === "reasonix" && item.text.includes("reasonix")), "技能来源应包含 reasonix");
assert(skillSourceOptions.some((item) => item.value === "hermes:usr_task502_default" && item.text.includes("Hermes_user")), "技能来源应包含个人 Hermes profile");
assert(skillSourceOptions.some((item) => item.value === "hermes:shared_lite" && item.text.includes("hermes_lite")), "技能来源应包含 shared lite profile");
await skillSourceSelect.selectOption("mnote", { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-current-page"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-mindmap"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const skillPanelText = await page.locator('[data-page-ai-panel="skills"]').innerText({ timeout: UI_TIMEOUT_MS });
assert(skillPanelText.includes("MNote 内置技能"), "技能面板应展示 MNote 内置技能分组");
assert(skillPanelText.includes("Reasonix 技能"), "技能面板应展示 Reasonix 技能分组");
assert(skillPanelText.includes("Hermes 技能"), "技能面板应展示 Hermes 技能分组");
assert(skillPanelText.includes("Hermes chemist"), "Hermes 技能应随 chemist profile 加载");
assert(!skillPanelText.includes("Hermes writer"), "Hermes profile 切到 chemist 后不应继续显示上一 profile 技能");
assert(skillPanelText.includes("mnote"), "技能面板应展示 mnote 来源");
assert(!skillPanelText.includes("Reasonix 技能"), "选择 mnote 时不应同时展示 Reasonix 技能");
assert(!skillPanelText.includes("Hermes 技能"), "选择 mnote 时不应同时展示 Hermes 技能");
await page.locator('[data-page-ai-skill-group-toggle="mnote"]').click({ timeout: UI_TIMEOUT_MS });
assert.strictEqual(
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-current-page"]').count(),
0,
"MNote 技能分组折叠后不应显示组内技能",
);
assert.strictEqual(
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-mindmap"]').count(),
0,
"MNote 技能分组折叠后不应显示 mindmap 技能",
);
await page.locator('[data-page-ai-skill-group-toggle="mnote"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-current-page"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-mindmap"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-current-page"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-mindmap"]').click({ timeout: UI_TIMEOUT_MS });
await skillSourceSelect.selectOption("reasonix", { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="reasonix"] [data-page-ai-skill-toggle="reasonix-review"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const reasonixOnlyText = await page.locator('[data-page-ai-panel="skills"]').innerText({ timeout: UI_TIMEOUT_MS });
assert(reasonixOnlyText.includes("reasonix"), "选择 reasonix 时应展示 Reasonix 来源");
assert(!reasonixOnlyText.includes("当前页读取"), "选择 reasonix 时不应残留 MNote 技能");
assert(!reasonixOnlyText.includes("Hermes writer"), "选择 reasonix 时不应残留 Hermes 技能");
await page.locator('[data-page-ai-skill-group="reasonix"] [data-page-ai-skill-toggle="reasonix-review"]').click({ timeout: UI_TIMEOUT_MS });
await skillSourceSelect.selectOption("hermes:shared_lite", { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-lite"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const sharedHermesText = await page.locator('[data-page-ai-panel="skills"]').innerText({ timeout: UI_TIMEOUT_MS });
assert(sharedHermesText.includes("hermes_lite"), "选择 hermes_lite 时应只展示 lite profile 技能");
assert(!sharedHermesText.includes("当前页读取"), "选择 hermes_lite 时不应残留 MNote 技能");
assert(!sharedHermesText.includes("reasonix-review"), "选择 hermes_lite 时不应残留 Reasonix 技能");
assert(
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-lite"]').isDisabled(),
"shared Hermes profile 普通用户应只读",
);
await page.locator('[data-page-ai-hide-hermes-builtin]').check({ timeout: UI_TIMEOUT_MS });
const hiddenBuiltinText = await page.locator('[data-page-ai-panel="skills"]').innerText({ timeout: UI_TIMEOUT_MS });
assert(!hiddenBuiltinText.includes("Hermes builtin"), "隐藏 Hermes 内置后不应显示 Hermes 内置技能");
await page.locator('[data-page-ai-hide-hermes-builtin]').uncheck({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-panel="skills"] [data-page-ai-profile-select]').selectOption("mnoteai", { timeout: UI_TIMEOUT_MS });
await skillSourceSelect.selectOption("hermes:usr_task502_default", { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-writer"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
assert.strictEqual(
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-chemist"]').count(),
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-lite"]').count(),
0,
"Hermes profile 切回 mnoteai 后不应残留 chemist 技能",
);
await page.locator('[data-page-ai-panel="skills"] [data-page-ai-profile-select]').selectOption("chemist", { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-chemist"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const skillsScreenshot = await saveScreenshot(page, "00-skills-panel");
await page.locator('[data-page-ai-skill-group="mnote"] [data-page-ai-skill-toggle="mnote-current-page"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="reasonix"] [data-page-ai-skill-toggle="reasonix-review"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-chemist"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-writer"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => document.documentElement.getAttribute("data-mnote-page-ai-skill-toggled") === "hermes-chemist",
() => document.documentElement.getAttribute("data-mnote-page-ai-skill-toggled") === "hermes-writer",
null,
{ timeout: UI_TIMEOUT_MS },
);
@@ -454,6 +653,16 @@ async function main() {
assert.strictEqual(ackRunBody.pageContext?.aiContext?.contextBlocks, undefined, "Page AI run 不应默认上传 contextBlocks");
assert.strictEqual(ackRunBody.pageContext?.evidence, undefined, "Page AI run 不应默认上传选区 evidence 正文");
assert.strictEqual(ackRunBody.selectedText, "", "Page AI run 不应默认上传 selectedText 正文");
assert.strictEqual(ackRunBody.targetPackage?.schema, "mnote.agent_target_package.v1", "纯聊天 run 也应冻结目标包合同");
assert(ackRunBody.targetPackage?.primaryTargetId, "targetPackage 应包含 primaryTargetId");
assert(Array.isArray(ackRunBody.targetPackage?.targets), "targetPackage 应包含 targets 数组");
assert.strictEqual(ackRunBody.targetPackage?.primaryTargetId, "resource:office:task502", "targetPackage 应冻结用户选择的 Office target");
assert.strictEqual(ackRunBody.targetPackage?.onlyofficeSessionId, "mnote-oo-task502-office", "Office targetPackage 应携带 bridge session");
assert(
ackRunBody.targetPackage.targets.some((target) => target.resourceKind === "only_office" && target.relativePath === "office/Task502 Deck.pptx" && target.onlyofficeSessionId === "mnote-oo-task502-office"),
`默认目标应标记 only_office resourceKind: ${JSON.stringify(ackRunBody.targetPackage)}`,
);
assert.strictEqual(ackRunBody.targetPackage?.policy?.writeRequiresExplicitTarget, true, "targetPackage policy 应要求显式目标");
await page.locator('.wolai-page-ai-header-actions [data-page-ai-tab="agent"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-panel="agent"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
@@ -495,6 +704,14 @@ async function main() {
`勾选变化后上下文按钮摘要应更新: ${updatedContextButtonLabel}`,
);
await contextPopover.locator('[data-page-ai-action="close-context-popover"]').click({ timeout: UI_TIMEOUT_MS });
await targetButton.click({ timeout: UI_TIMEOUT_MS });
await targetPopover.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await targetPopover.locator('[data-page-ai-target-option="resource:mindmap:task502"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => (document.querySelector("[data-page-ai-target-chip]")?.textContent || "").includes("Task502 Mindmap"),
null,
{ timeout: UI_TIMEOUT_MS },
);
const runsBeforeDocumentTask = captured.filter((item) => item.kind === "run").length;
await page.locator("[data-page-ai-input]").fill(`Task502 agent/context ${suffix}`, { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
@@ -520,20 +737,37 @@ async function main() {
assert.strictEqual(runBody.pageContext?.aiContext?.contextBlocks, undefined, "Page AI run 不应默认上传 contextBlocks");
assert.strictEqual(runBody.pageContext?.evidence, undefined, "Page AI run 不应默认上传选区 evidence 正文");
assert.strictEqual(runBody.selectedText, "", "Page AI run 不应默认上传 selectedText 正文");
assert(runBody.contextRefs.some((item) => item.kind === "active_editor" && item.documentId === documentId));
const activeEditorRef = runBody.contextRefs.find((item) => item.kind === "active_editor" && item.documentId === documentId);
assert(activeEditorRef, "run payload 应包含 active_editor contextRef");
assert.strictEqual(activeEditorRef.targetId, "resource:mindmap:task502", `active_editor contextRef 应冻结 mindmap targetId: ${JSON.stringify(activeEditorRef)}`);
assert.strictEqual(activeEditorRef.objectIdentity, "resource:mindmap:task502", `active_editor contextRef 应冻结 mindmap objectIdentity: ${JSON.stringify(activeEditorRef)}`);
assert.strictEqual(activeEditorRef.resourceKind, "mindmap", `active_editor contextRef 应标记 mindmap resourceKind: ${JSON.stringify(activeEditorRef)}`);
assert.strictEqual(activeEditorRef.assetId, "task502-mindmap", `active_editor contextRef 应携带 mindmap assetId: ${JSON.stringify(activeEditorRef)}`);
assert.strictEqual(activeEditorRef.relativePath, "maps/Task502.mindmap.json", `active_editor contextRef 应携带 mindmap relativePath: ${JSON.stringify(activeEditorRef)}`);
assert(runBody.contextRefs.some((item) => item.kind === "folder" && item.rootUri === rootUri));
assert(runBody.contextRefs.some((item) => item.kind === "changed_files"));
assert.strictEqual(runBody.skillPreferences?.mnote?.["mnote-current-page"], false, "MNote skill 开关应进入 run payload");
assert.strictEqual(runBody.skillPreferences?.mnote?.["mnote-mindmap"], false, "MNote mindmap skill 开关应进入 run payload");
assert.strictEqual(runBody.skillPreferences?.reasonix?.["reasonix-review"], false, "Reasonix skill 开关应进入 run payload");
assert.strictEqual(runBody.targetPackage?.schema, "mnote.agent_target_package.v1", "文档任务 run 应携带目标包");
assert(runBody.targetPackage?.primaryTargetId, "文档任务 targetPackage 应包含 primaryTargetId");
assert(Array.isArray(runBody.targetPackage?.targets), "文档任务 targetPackage 应包含 targets 数组");
assert.strictEqual(runBody.targetPackage?.primaryTargetId, "resource:mindmap:task502", "文档任务 targetPackage 应冻结用户选择的 mindmap target");
assert(
runBody.targetPackage.targets.some((target) => target.resourceKind === "mindmap" && target.relativePath === "maps/Task502.mindmap.json"),
`文档任务 targetPackage 应包含 mindmap 目标: ${JSON.stringify(runBody.targetPackage)}`,
);
assert.strictEqual(runBody.targetPackage?.policy?.writeRequiresExplicitTarget, true, "文档任务 targetPackage policy 应要求显式目标");
const preferenceBodies = captured
.filter((item) => item.kind === "ui-preferences" && item.method === "PUT")
.map((item) => JSON.parse(item.body || "{}"));
assert(preferenceBodies.some((body) => body.updates?.["ai.agent.hermes.profile_id"] === "chemist"), "Hermes profile 选择应写入 SQLite UI preference");
assert(preferenceBodies.some((body) => body.updates?.["ai.common.skills.mnote.enabled"]?.["mnote-current-page"] === false), "MNote skill 开关应写入 SQLite UI preference");
assert(preferenceBodies.some((body) => body.updates?.["ai.agent.hermes.profile_id"] === "shared_lite"), "Agent 内的 Hermes profile 选择应写入 SQLite UI preference");
assert(captured.some((item) => item.kind === "skill-toggle" && JSON.parse(item.body || "{}").skillKind === "mnote_builtin" && JSON.parse(item.body || "{}").name === "mnote-current-page"), "MNote skill 开关应调用服务端 per-user SQLite policy");
assert(preferenceBodies.some((body) => body.updates?.["ai.agent.reasonix.skills.enabled"]?.["reasonix-review"] === false), "Reasonix skill 开关应写入 SQLite UI preference");
assert(preferenceBodies.some((body) => body.updates?.["ai.common.skills.groups.collapsed"]?.mnote === true), "技能分组折叠状态应写入 SQLite UI preference");
assert(preferenceBodies.some((body) => body.updates?.["ai.agent.hermes.profile.chemist.skills.hide_builtin"] === true), "Hermes 隐藏内置技能应按 profile 写入 SQLite UI preference");
assert(captured.some((item) => item.kind === "skill-toggle" && JSON.parse(item.body || "{}").profile === "chemist"), "Hermes skill 开关应按 profile 调用");
assert(preferenceBodies.some((body) => body.updates?.["ai.common.skills.active_source"] === "hermes:usr_task502_default"), "技能来源选择应写入 SQLite UI preference");
assert(preferenceBodies.some((body) => body.updates?.["ai.agent.hermes.skills.hide_builtin"] === true), "Hermes 隐藏内置技能应写入用户 UI preference");
assert(captured.some((item) => item.kind === "skill-toggle" && JSON.parse(item.body || "{}").profileId === "usr_task502_default"), "Hermes skill 开关应按 profileId 调用");
assert(Array.isArray(runBody.allowedRoots), "run payload 必须包含 allowedRoots 数组");
assert(runBody.allowedRoots.some((item) =>
item.rootUri === rootUri