feat(rag): harden post-LightRAG runtime
Retire legacy OCR/media/evidence fallbacks, add local-folder event bus and Page Aggregate guards, and archive completed design checklists. Validation: cargo test -p mnote-web -- --test-threads=1; cargo test --workspace -- --test-threads=1; git diff --check; codegraph sync .; codegraph_status.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
#!/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 ROOT = process.env.MNOTE_REPO_ROOT || "/mnt/Data1T/mnote";
|
||||
const { BASE_URL, UI_TIMEOUT_MS } = require(path.join(ROOT, "scripts", "tree-shell-smoke-helpers"));
|
||||
|
||||
const OUTPUT_DIR = path.join(ROOT, "tmp", "task531-lightrag-dashboard-ui-smoke");
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const CHROME = process.env.MNOTE_PAGE_AI_CHROME || "/usr/bin/google-chrome";
|
||||
|
||||
async function fetchDashboardStatus(context) {
|
||||
const response = await context.request.get(`${BASE_URL}/api/knowledge-rag/status`, {
|
||||
headers: {
|
||||
"x-mnote-actor-id": "mnote-e2e",
|
||||
"x-mnote-actor-type": "user",
|
||||
"x-mnote-workspace-id": "local-ws:mnote-e2e:my-space",
|
||||
},
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
const text = await response.text();
|
||||
assert(response.ok(), `/api/knowledge-rag/status 失败: ${response.status()} ${text.slice(0, 500)}`);
|
||||
const payload = JSON.parse(text);
|
||||
assert(payload.dashboardUrl, `status 缺少 dashboardUrl: ${text.slice(0, 500)}`);
|
||||
assert(payload.health?.healthy !== false, `LightRAG health 非 healthy: ${text.slice(0, 500)}`);
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function bodyText(page) {
|
||||
return await page.evaluate(() => document.body.innerText || "");
|
||||
}
|
||||
|
||||
async function screenshot(page, name) {
|
||||
const screenshotPath = path.join(OUTPUT_DIR, name);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
const browser = await chromium.launch({
|
||||
headless: process.env.MNOTE_DASHBOARD_SMOKE_HEADED !== "1",
|
||||
executablePath: fs.existsSync(CHROME) ? CHROME : undefined,
|
||||
});
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
|
||||
const page = await context.newPage();
|
||||
const pageErrors = [];
|
||||
const consoleErrors = [];
|
||||
page.on("pageerror", (error) => pageErrors.push(String(error?.message || error)));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") consoleErrors.push(message.text());
|
||||
});
|
||||
|
||||
try {
|
||||
const statusPayload = await fetchDashboardStatus(context);
|
||||
const dashboardUrl = statusPayload.dashboardUrl;
|
||||
const documentSummaries = Array.isArray(statusPayload.documents?.documents)
|
||||
? statusPayload.documents.documents
|
||||
: [];
|
||||
const rawStatusGroups = statusPayload.documents?.rawStatusGroups || {};
|
||||
const processedCount = Number(rawStatusGroups.processed || 0);
|
||||
const webuiUrl = dashboardUrl.endsWith("/webui/") ? dashboardUrl : `${dashboardUrl.replace(/\/+$/, "")}/webui/`;
|
||||
await page.goto(webuiUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForLoadState("networkidle", { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||||
await page.waitForFunction(
|
||||
() => (document.body.innerText || "").includes("Document Management"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
const documentsText = await bodyText(page);
|
||||
assert(documentsText.includes("Uploaded Documents"), "Documents 页缺少 Uploaded Documents");
|
||||
if (processedCount > 0) {
|
||||
assert(
|
||||
documentsText.includes(`Completed (${processedCount})`),
|
||||
`Documents 页未显示当前 completed 计数 ${processedCount}: ${documentsText.slice(0, 1000)}`,
|
||||
);
|
||||
}
|
||||
const visibleDocument = documentSummaries.find((doc) => {
|
||||
const id = String(doc.id || "");
|
||||
const filePath = String(doc.filePath || "");
|
||||
return (id && documentsText.includes(id)) || (filePath && documentsText.includes(filePath));
|
||||
});
|
||||
assert(
|
||||
visibleDocument,
|
||||
`Documents 页没有显示 status API 返回的任一当前文档: ${JSON.stringify(documentSummaries.slice(0, 5), null, 2)}\n${documentsText.slice(0, 1000)}`,
|
||||
);
|
||||
const documentsScreenshot = await screenshot(page, "documents.png");
|
||||
|
||||
await page.getByRole("tab", { name: /Knowledge Graph/i }).click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const text = document.body.innerText || "";
|
||||
return text.includes("Connected") && text.includes("D:");
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const graphText = await bodyText(page);
|
||||
assert(graphText.includes("Connected"), `Graph 页缺少连接状态: ${graphText.slice(0, 1000)}`);
|
||||
const graphScreenshot = await screenshot(page, "graph.png");
|
||||
|
||||
await page.getByRole("tab", { name: /Retrieval/i }).click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => (document.body.innerText || "").includes("Start a retrieval by typing your query below"),
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const retrievalText = await bodyText(page);
|
||||
assert(retrievalText.includes("Query Mode"), "Retrieval 页缺少 Query Mode 参数");
|
||||
assert(retrievalText.includes("KG Top K"), "Retrieval 页缺少 KG Top K 参数");
|
||||
assert(retrievalText.includes("Connected"), "Retrieval 页缺少连接状态");
|
||||
const retrievalScreenshot = await screenshot(page, "retrieval.png");
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
dashboardUrl,
|
||||
webuiUrl: page.url(),
|
||||
documents: {
|
||||
processedCount,
|
||||
completedVisible: processedCount > 0 ? documentsText.includes(`Completed (${processedCount})`) : null,
|
||||
visibleDocumentId: visibleDocument.id || null,
|
||||
visibleDocumentFilePath: visibleDocument.filePath || null,
|
||||
screenshot: documentsScreenshot,
|
||||
},
|
||||
graph: {
|
||||
connectedVisible: graphText.includes("Connected"),
|
||||
screenshot: graphScreenshot,
|
||||
},
|
||||
retrieval: {
|
||||
queryModeVisible: retrievalText.includes("Query Mode"),
|
||||
connectedVisible: retrievalText.includes("Connected"),
|
||||
screenshot: retrievalScreenshot,
|
||||
},
|
||||
pageErrors,
|
||||
consoleErrors,
|
||||
};
|
||||
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
await page.screenshot({ path: path.join(OUTPUT_DIR, "failure.png"), fullPage: true }).catch(() => undefined);
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, "failure.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.stack || error.message : String(error),
|
||||
pageErrors,
|
||||
consoleErrors,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user