Files
mnote/scripts/task543-knowledge-rag-pyrrolidine-top5-locator-smoke.js
T

259 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const fs = require("node:fs");
const path = require("node:path");
const { chromium } = require("playwright");
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const ROOT_URI = process.env.MNOTE_KNOWLEDGE_RAG_ROOT_URI || "file:///mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space";
const WORKSPACE_ID = process.env.MNOTE_KNOWLEDGE_RAG_WORKSPACE_ID || "local-ws:mnote-e2e:my-space";
const QUERY = process.env.MNOTE_KNOWLEDGE_RAG_PYRROLIDINE_QUERY || "吡咯烷";
const EXPECTED_RESOURCE = process.env.MNOTE_KNOWLEDGE_RAG_EXPECTED_RESOURCE
|| "有机合成中的保护基/[OCR]_有机合成中的保护基-酚羰基羧基巯基的保护_20250201_1908.layered_删减-2025-02-04 18-59-42.docx";
const TOP_N = Number(process.env.MNOTE_KNOWLEDGE_RAG_TOP_N || 5);
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task543-knowledge-rag-pyrrolidine-top5-locator-smoke");
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
.find((candidate) => fs.existsSync(candidate));
function normalizeText(value) {
return String(value || "")
.replace(/<[^>]+>/g, " ")
.replace(/[#*_`~>\[\](){}]+/g, " ")
.replace(/[\u200B-\u200D\uFEFF]/g, "")
.replace(/\s+/g, " ")
.trim();
}
function compactText(value) {
return normalizeText(value).replace(/[0-9-]+/g, "").replace(/[\s\p{P}\p{S}]+/gu, "");
}
function longestCommonSubstringLength(left, right) {
const a = compactText(left);
const b = compactText(right);
if (!a || !b) return 0;
const shorter = a.length <= b.length ? a : b;
const longer = a.length <= b.length ? b : a;
for (let len = Math.min(80, shorter.length); len >= 6; len -= 1) {
for (let start = 0; start + len <= shorter.length; start += 1) {
if (longer.includes(shorter.slice(start, start + len))) return len;
}
}
return 0;
}
async function signIn(context) {
const auth = await context.request.post(`${BASE_URL}/api/auth`, {
data: {
action: "auth:signIn",
args: {
provider: "password",
params: {
email: "mnote.e2e@example.com",
password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
flow: "signIn",
},
},
},
});
assert(auth.ok(), `登录失败: ${auth.status()} ${await auth.text()}`);
}
async function apiSearch(context) {
const response = await context.request.post(`${BASE_URL}/api/knowledge-rag/search`, {
data: {
rootUri: ROOT_URI,
workspaceId: WORKSPACE_ID,
query: QUERY,
mode: "mix",
topK: 20,
chunkTopK: 20,
includeChunkContent: true,
sourcePaths: [EXPECTED_RESOURCE],
},
});
assert(response.ok(), `knowledge-rag search 失败: ${response.status()} ${await response.text()}`);
const payload = await response.json();
const results = Array.isArray(payload.results) ? payload.results : [];
assert(results.length >= TOP_N, `资料库结果不足 ${TOP_N} 条: ${JSON.stringify(payload, null, 2).slice(0, 3000)}`);
assert(Array.isArray(payload.citations) && payload.citations.length >= TOP_N, `资料库结果缺少 citations[]: ${JSON.stringify(payload, null, 2).slice(0, 3000)}`);
results.slice(0, TOP_N).forEach((item, index) => {
assert(item.displayQuote && item.locatorEvidenceText, `第 ${index + 1} 条缺少 displayQuote/locatorEvidenceText: ${JSON.stringify(item, null, 2)}`);
assert(!/(?:<\/?e(?:q(?:uation)?)?\b|<\/?drawing\b|format=["']?latex|\blatex\b)/i.test(String(item.displayQuote || "")), `第 ${index + 1} 条 displayQuote 仍暴露原始公式/绘图标记: ${item.displayQuote}`);
});
const blockIds = results.map((item) => item?.locator?.blockId).filter(Boolean);
assert.equal(new Set(blockIds).size, blockIds.length, `搜索结果仍有同段落重复: ${JSON.stringify(blockIds)}`);
return results.slice(0, TOP_N);
}
async function openSearchPanel(page, navigate = false) {
if (navigate) {
await page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.evaluate(() => {
window.localStorage?.setItem("mnote.search.collapseSourcesDefault.v1", "0");
});
}
const inputVisible = await page.locator('[data-testid="wolai-search-input"]').isVisible().catch(() => false);
if (!inputVisible) {
await page.waitForSelector('[data-mnote-action="open-search-modal"]', { timeout: UI_TIMEOUT_MS });
await page.click('[data-mnote-action="open-search-modal"]');
}
await page.waitForSelector('[data-testid="wolai-search-input"]', { timeout: UI_TIMEOUT_MS });
await page.fill('[data-testid="wolai-search-input"]', QUERY);
await page.evaluate(() => {
const button = document.querySelector('[data-search-switch="knowledge"]');
if (button instanceof HTMLElement) {
button.setAttribute("aria-checked", "true");
button.classList.add("is-on");
}
document.querySelector('[data-testid="wolai-search-input"]')
?.dispatchEvent(new Event("input", { bubbles: true }));
});
await page.waitForFunction(
(topN) => document.querySelectorAll('[data-testid="wolai-search-result-row"]').length >= topN,
TOP_N,
{ timeout: UI_TIMEOUT_MS },
);
}
async function clickAndAuditRow(page, index, apiResult) {
const rows = await page.$$('[data-testid="wolai-search-result-row"]');
assert(rows[index], `缺少第 ${index + 1} 条搜索结果`);
const rowText = await rows[index].evaluate((node) => node.textContent || "");
assert(
!/(?:<\/?equation\b|format=["']?latex|<\/?drawing\b|\blatex\b)/i.test(rowText),
`第 ${index + 1} 条搜索结果仍暴露原始公式/绘图标记: ${rowText}`,
);
const locator = await rows[index].evaluate((node) => {
const raw = node.getAttribute("data-evidence-locator") || "";
try {
return raw ? JSON.parse(raw) : null;
} catch (_) {
return null;
}
});
assert(locator?.blockId, `第 ${index + 1} 条缺少 locator blockId: ${rowText}`);
assert.equal(locator.blockId, apiResult?.locator?.blockId, `第 ${index + 1} 条 UI/API blockId 不一致`);
await rows[index].click();
await page.waitForFunction(
({ expectedResource, expectedBlockId }) => {
const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
return panel && !panel.hidden && panel.getAttribute("data-mnote-evidence-block-id") === expectedBlockId;
},
{ expectedResource: EXPECTED_RESOURCE, expectedBlockId: locator.blockId },
{ timeout: UI_TIMEOUT_MS },
);
await page.waitForTimeout(1200);
await page.waitForFunction(
(expectedResource) => {
const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
const frame = panel?.querySelector("iframe.mnote-resource-tab-frame");
const doc = frame?.contentDocument;
return doc?.documentElement?.getAttribute("data-mnote-office-evidence-applied") === "true";
},
EXPECTED_RESOURCE,
{ timeout: UI_TIMEOUT_MS },
);
const state = await page.evaluate((expectedResource) => {
const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
const frame = panel?.querySelector("iframe.mnote-resource-tab-frame");
const doc = frame?.contentDocument;
const highlighted = doc?.querySelector('[data-mnote-office-evidence-target="true"]');
return {
panelBlockId: panel?.getAttribute("data-mnote-evidence-block-id") || "",
panelEvidenceText: panel?.getAttribute("data-mnote-evidence-text") || "",
iframeApplied: doc?.documentElement?.getAttribute("data-mnote-office-evidence-applied") || "",
highlightedText: highlighted?.textContent || "",
highlightedTag: highlighted?.tagName || "",
highlightedTop: highlighted ? Math.round(highlighted.getBoundingClientRect().top) : null,
};
}, EXPECTED_RESOURCE);
const evidenceText = locator?.openAction?.params?.evidenceText || apiResult?.locator?.openAction?.params?.evidenceText || apiResult?.quote || rowText;
const overlap = longestCommonSubstringLength(state.highlightedText, evidenceText);
assert.equal(state.panelBlockId, locator.blockId, `第 ${index + 1} 条 panel blockId 不一致: ${JSON.stringify(state, null, 2)}`);
assert(state.highlightedText.includes(QUERY), `第 ${index + 1} 条定位高亮未包含 query: ${JSON.stringify(state, null, 2)}`);
assert(
normalizeText(state.highlightedText).length >= 24 || state.highlightedTag === "P",
`第 ${index + 1} 条仍是短词高亮,不是段落级高亮: ${JSON.stringify(state, null, 2)}`,
);
assert(
overlap >= 10 || (
state.highlightedTag === "P"
&& compactText(state.highlightedText).includes(compactText(QUERY))
&& compactText(state.highlightedText).length >= 6
),
`第 ${index + 1} 条搜索结果与实际定位上下文不一致: ${JSON.stringify({ rowText, evidenceText, overlap, state }, null, 2)}`,
);
return {
index: index + 1,
rowText: normalizeText(rowText).slice(0, 220),
apiBlockId: apiResult?.locator?.blockId || "",
uiBlockId: locator.blockId,
panelBlockId: state.panelBlockId,
highlightedText: normalizeText(state.highlightedText).slice(0, 220),
highlightedTag: state.highlightedTag,
overlap,
};
}
async function main() {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true, executablePath: CHROMIUM_EXECUTABLE_PATH });
const context = await browser.newContext({ viewport: { width: 1360, height: 900 } });
try {
await signIn(context);
const topResults = await apiSearch(context);
const page = await context.newPage();
const pageErrors = [];
const consoleErrors = [];
page.on("pageerror", (error) => pageErrors.push(String(error?.stack || error?.message || error)));
page.on("console", (message) => {
if (["error", "warning"].includes(message.type())) consoleErrors.push(`${message.type()}: ${message.text()}`);
});
await openSearchPanel(page, true);
const audits = [];
for (let index = 0; index < TOP_N; index += 1) {
await openSearchPanel(page, false);
audits.push(await clickAndAuditRow(page, index, topResults[index]));
}
await page.screenshot({ path: path.join(OUTPUT_DIR, "top5-after-last-click.png"), fullPage: true });
const result = {
ok: true,
baseUrl: BASE_URL,
rootUri: ROOT_URI,
workspaceId: WORKSPACE_ID,
query: QUERY,
topN: TOP_N,
audits,
pageErrors,
consoleErrors,
};
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
console.log(JSON.stringify(result, null, 2));
} finally {
await browser.close().catch(() => undefined);
}
}
main().catch((error) => {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.writeFileSync(
path.join(OUTPUT_DIR, "failure.json"),
`${JSON.stringify({ ok: false, error: error.stack || error.message || String(error) }, null, 2)}\n`,
"utf8",
);
console.error(error.stack || error.message || String(error));
process.exit(1);
});