Improve local evidence search and AI capabilities
This commit is contained in:
@@ -55,6 +55,15 @@ async function saveScreenshot(page, name) {
|
||||
return target;
|
||||
}
|
||||
|
||||
async function waitForCapturedRunCount(captured, minCount, timeoutMs = UI_TIMEOUT_MS) {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
if (captured.filter((item) => item.kind === "run").length >= minCount) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
throw new Error(`captured run 数量不足,期望至少 ${minCount},实际 ${captured.filter((item) => item.kind === "run").length}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
const suffix = Date.now().toString(36);
|
||||
@@ -189,6 +198,44 @@ async function main() {
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/hermes/client/capabilities/toggle", async (route) => {
|
||||
captured.push({ kind: "capability-toggle", method: route.request().method(), body: route.request().postData() || "" });
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/hermes/client/capabilities**", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
if (url.pathname.endsWith("/toggle")) {
|
||||
captured.push({ kind: "capability-toggle", method: route.request().method(), body: route.request().postData() || "" });
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
ok: true,
|
||||
runtime: "mnote",
|
||||
categories: [{
|
||||
name: "mnote",
|
||||
title: "MNote AI 能力",
|
||||
capabilities: [
|
||||
{ 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_capability", uiKind: "ai_capability", toolCount: 1, tools: [{ name: "mnote.context.snapshot", enabled: true }] },
|
||||
{ id: "mnote-mindmap", name: "mnote-mindmap", title: "思维导图读写", description: "读取、编辑或从 outline 生成思维导图", enabled: true, source: "mnote", origin: "builtin", builtin: true, configurable: true, configScope: "user_sqlite", skillKind: "mnote_capability", uiKind: "ai_capability", toolNames: ["mnote.mindmap.fetch", "mnote.mindmap.create_from_outline"], toolCount: 2, tools: [{ name: "mnote.mindmap.fetch", enabled: true }, { name: "mnote.mindmap.create_from_outline", enabled: true }] },
|
||||
],
|
||||
}],
|
||||
archived: [],
|
||||
};
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/hermes/client/skills**", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
if (url.pathname.endsWith("/toggle")) {
|
||||
@@ -492,7 +539,38 @@ async function main() {
|
||||
bridgeSessionId: "mnote-oo-task502-office",
|
||||
bridgeSessionReady: true,
|
||||
};
|
||||
const resources = [mindmapResource, officeResource];
|
||||
const officePreviewResource = {
|
||||
objectIdentity: "resource:office-preview:task502",
|
||||
workspacePath: {
|
||||
schema: "mnote.workspace_path.v1",
|
||||
workspaceId,
|
||||
sourceKind: "local_folder",
|
||||
rootUri,
|
||||
relativePath: "office/Task502 Preview.docx",
|
||||
documentId,
|
||||
objectIdentity: "resource:office-preview:task502",
|
||||
assetId: "task502-office-preview",
|
||||
resourceKind: "attachment",
|
||||
},
|
||||
paneRole: "primary",
|
||||
documentId,
|
||||
workspaceId,
|
||||
title: "Task502 Preview DOCX",
|
||||
kind: "office",
|
||||
editorKind: "office",
|
||||
active: false,
|
||||
dirtyState: "",
|
||||
preview: true,
|
||||
pinned: true,
|
||||
lastActiveAt: Date.now(),
|
||||
assetId: "task502-office-preview",
|
||||
path: "office/Task502 Preview.docx",
|
||||
officeOpenMode: "preview",
|
||||
onlyofficeSessionId: "",
|
||||
bridgeSessionId: "",
|
||||
bridgeSessionReady: false,
|
||||
};
|
||||
const resources = [mindmapResource, officeResource, officePreviewResource];
|
||||
const withoutResource = (items) => (Array.isArray(items) ? items : [])
|
||||
.filter((item) => !resources.some((resource) => item?.objectIdentity === resource.objectIdentity));
|
||||
const groups = snapshot.groups || {};
|
||||
@@ -535,21 +613,24 @@ 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 });
|
||||
await skillSourceSelect.waitFor({ state: "attached", 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 });
|
||||
assert(skillSourceOptions.some((item) => item.value === "mnote" && item.text.includes("MNote 公共能力")), "能力来源应包含 MNote");
|
||||
assert(skillSourceOptions.some((item) => item.value === "reasonix" && item.text.includes("Reasonix skill")), "能力来源应包含 Reasonix 自带 skill 查看入口");
|
||||
assert(skillSourceOptions.some((item) => item.value === "hermes:usr_task502_default" && item.text.includes("Hermes skill")), "能力来源应包含个人 Hermes profile skill 查看入口");
|
||||
assert(!skillSourceOptions.some((item) => item.value === "hermes:shared_deepseek_chat"), "Chat-only Hermes profile 不应作为能力来源展示");
|
||||
assert(!skillSourceOptions.some((item) => item.value === "hermes:shared_lite"), "Hermes Lite chat-only profile 不应作为能力来源展示");
|
||||
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 技能"), "选择 mnote 时不应同时展示 Reasonix 技能");
|
||||
assert(!skillPanelText.includes("Hermes 技能"), "选择 mnote 时不应同时展示 Hermes 技能");
|
||||
assert(skillPanelText.includes("MNote 公共能力"), "能力面板应展示 MNote 来源");
|
||||
assert(!skillPanelText.includes("user_sqlite"), "MNote 能力面板不应展示内部 SQLite policy 细节");
|
||||
assert(!skillPanelText.includes("profile_tool_policy"), "MNote 能力面板不应展示内部 profile policy 细节");
|
||||
assert(!skillPanelText.includes("reasonix-review"), "选择 MNote 时不应同时展示 Reasonix 能力条目");
|
||||
assert(!skillPanelText.includes("Hermes writer"), "选择 MNote 时不应同时展示 Hermes 能力条目");
|
||||
const mnoteSkillsScreenshot = await saveScreenshot(page, "00-mnote-skills-panel");
|
||||
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(),
|
||||
@@ -567,40 +648,20 @@ async function main() {
|
||||
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 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 });
|
||||
await page.locator('[data-page-ai-skill-group="reasonix"] .wolai-page-ai-skill-name', { hasText: "reasonix-review" }).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
assert.strictEqual(
|
||||
await page.locator('[data-page-ai-skill-group="hermes"] [data-page-ai-skill-toggle="hermes-lite"]').count(),
|
||||
await page.locator('[data-page-ai-skill-group="reasonix"] [data-page-ai-skill-toggle="reasonix-review"]').count(),
|
||||
0,
|
||||
"Hermes profile 切回 mnoteai 后不应残留 chemist 技能",
|
||||
"Reasonix 自带 skill 在能力页只读查看,不显示开关",
|
||||
);
|
||||
const skillsScreenshot = await saveScreenshot(page, "00-skills-panel");
|
||||
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-writer",
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
await skillSourceSelect.selectOption("hermes:usr_task502_default", { timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-skill-group="hermes"] .wolai-page-ai-skill-name', { hasText: "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-writer"]').count(),
|
||||
0,
|
||||
"Hermes profile skill 在能力页只读查看,不显示开关",
|
||||
);
|
||||
const skillsScreenshot = await saveScreenshot(page, "00-hermes-skills-panel");
|
||||
await page.locator('[data-page-ai-panel="skills"] [data-page-ai-tab="chat"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-panel="chat"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const contextButtonText = await contextButton.innerText({ timeout: UI_TIMEOUT_MS });
|
||||
@@ -663,6 +724,39 @@ async function main() {
|
||||
`默认目标应标记 only_office resourceKind: ${JSON.stringify(ackRunBody.targetPackage)}`,
|
||||
);
|
||||
assert.strictEqual(ackRunBody.targetPackage?.policy?.writeRequiresExplicitTarget, true, "targetPackage policy 应要求显式目标");
|
||||
await page.waitForFunction(
|
||||
() => ["completed", "idle"].includes(document.documentElement.getAttribute("data-mnote-page-ai-run-status") || ""),
|
||||
null,
|
||||
{ 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:office-preview:task502"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector("[data-page-ai-target-chip]")?.textContent || "").includes("Task502 Preview DOCX"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const previewRunCountBefore = captured.filter((item) => item.kind === "run").length;
|
||||
await page.locator("[data-page-ai-input]").fill("预览 docx 请正常回复", { timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await waitForCapturedRunCount(captured, previewRunCountBefore + 1);
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "").includes("Task502 response"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const previewRuns = captured.filter((item) => item.kind === "run");
|
||||
assert(previewRuns.length >= 2, "未捕获 Office 预览 Page AI run payload");
|
||||
const previewRunBody = JSON.parse(previewRuns[previewRuns.length - 1].body);
|
||||
assert.strictEqual(previewRunBody.targetPackage?.primaryTargetId, "resource:office-preview:task502", "Office 预览 targetPackage 应冻结预览资源");
|
||||
assert.strictEqual(previewRunBody.targetPackage?.resourceKind, "attachment", `Office 预览不应被归一为 only_office: ${JSON.stringify(previewRunBody.targetPackage)}`);
|
||||
assert.strictEqual(previewRunBody.targetPackage?.onlyofficeSessionId, "", "Office 预览 targetPackage 不应要求 bridge session");
|
||||
assert(
|
||||
previewRunBody.targetPackage.targets.some((target) => target.resourceKind === "attachment" && target.relativePath === "office/Task502 Preview.docx" && !target.onlyofficeSessionId),
|
||||
`Office 预览 target 应作为普通附件上下文发送: ${JSON.stringify(previewRunBody.targetPackage)}`,
|
||||
);
|
||||
|
||||
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 });
|
||||
@@ -748,7 +842,6 @@ async function main() {
|
||||
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 数组");
|
||||
@@ -762,12 +855,10 @@ async function main() {
|
||||
.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"] === "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.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(captured.some((item) => item.kind === "capability-toggle" && JSON.parse(item.body || "{}").id === "mnote-current-page"), "MNote 能力开关应调用服务端 per-user SQLite policy");
|
||||
assert(preferenceBodies.some((body) => body.updates?.["ai.common.skills.groups.collapsed"]?.mnote === true), "能力分组折叠状态应写入 SQLite UI preference");
|
||||
assert(!preferenceBodies.some((body) => body.updates?.["ai.agent.reasonix.skills.enabled"]), "能力页不应再写入 Reasonix 自带 skill 偏好");
|
||||
assert(!preferenceBodies.some((body) => body.updates?.["ai.agent.hermes.skills.hide_builtin"] === true), "能力页不应再写入 Hermes 内置 skill 过滤偏好");
|
||||
assert(Array.isArray(runBody.allowedRoots), "run payload 必须包含 allowedRoots 数组");
|
||||
assert(runBody.allowedRoots.some((item) =>
|
||||
item.rootUri === rootUri
|
||||
@@ -794,6 +885,7 @@ async function main() {
|
||||
root,
|
||||
rootUri,
|
||||
documentId,
|
||||
mnoteSkillsScreenshot,
|
||||
screenshot,
|
||||
skillsScreenshot,
|
||||
captured,
|
||||
|
||||
Reference in New Issue
Block a user