收口 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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user