#!/usr/bin/env node "use strict"; const fs = require("node:fs"); const path = require("node:path"); const vm = require("node:vm"); const repoRoot = path.resolve(__dirname, ".."); const openHubRoot = process.env.OPENHUB_RESEARCH_ROOT || "/tmp/mnote-openhub-research/OpenHub"; const backendBridgePath = path.join(openHubRoot, "smart-query-backend/app/services/mnote_weknora.py"); const backendApiPath = path.join(openHubRoot, "smart-query-backend/app/api/mnote_tools.py"); const backendStreamPath = path.join(openHubRoot, "smart-query-backend/app/services/stream.py"); const backendScopePath = path.join(openHubRoot, "smart-query-backend/app/core/mnote_scope.py"); const frontendEmbedPath = path.join(openHubRoot, "smart-query-frontend/src/mnoteEmbed.js"); const assistantMessagePath = path.join(openHubRoot, "smart-query-frontend/src/components/AssistantMessage.jsx"); const smartQueryPath = path.join(openHubRoot, "smart-query-frontend/src/pages/SmartQueryPage.jsx"); const mnoteRuntimePath = path.join(repoRoot, "rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js"); const checklistPath = path.join(repoRoot, "design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-checklist-v1.md"); const outputDir = path.join(repoRoot, "tmp", "7-68-runtime"); const resultPath = path.join(outputDir, "openhub-weknora-tool-citation-bridge-smoke-result.json"); function read(filePath) { return fs.readFileSync(filePath, "utf8"); } function assertCheck(failures, name, passed, details = undefined) { if (!passed) failures.push({ name, details }); } function base64UrlJson(value) { return Buffer.from(JSON.stringify(value), "utf8") .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, ""); } function extractFunction(source, name) { const start = source.indexOf(`export const ${name}`); if (start < 0) throw new Error(`找不到 ${name}`); const next = source.indexOf("\nexport const ", start + 1); return source.slice(start, next > start ? next : undefined); } async function probeEmbedOpenReference(embedSource) { const scope = { workspaceScope: { rootUri: "file:///tmp/mnote-openhub-task782", workspaceId: "ws-task782", pageResourceId: "page-task782", }, }; const posted = []; const sandbox = { console, TextDecoder, URLSearchParams, Uint8Array, window: { location: { pathname: "/page-ai/openhub/ai", search: `?scope=session-task782&mnoteScope=${base64UrlJson(scope)}`, origin: "http://127.0.0.1:3000", }, parent: { postMessage: (message, origin) => posted.push({ message, origin }), }, atob: (value) => Buffer.from(value, "base64").toString("binary"), }, module: { exports: {} }, exports: {}, }; const transformed = embedSource .slice(0, embedSource.indexOf("const compactObject")) .replace(/import\.meta\.env\.VITE_API_BASE_URL/g, "undefined") .replace(/\bexport const /g, "const "); vm.runInNewContext( `${transformed}\nmodule.exports = { postMNoteOpenReference };`, sandbox, { filename: frontendEmbedPath } ); const ok = sandbox.module.exports.postMNoteOpenReference({ schema: "openhub.weknora_citation_locator.v1", citation: { provider: "weknora", sourceRootRelativePath: "knowledge-rag-fixtures-7-68/task782.md", citationLabel: "task782.md", }, }); return { ok, posted }; } async function main() { const failures = []; const backendBridge = read(backendBridgePath); const backendApi = read(backendApiPath); const backendStream = read(backendStreamPath); const backendScope = read(backendScopePath); const frontendEmbed = read(frontendEmbedPath); const assistantMessage = read(assistantMessagePath); const smartQuery = read(smartQueryPath); const mnoteRuntime = read(mnoteRuntimePath); const checklist = read(checklistPath); assertCheck( failures, "OpenHub backend exposes MNote WeKnora readonly facade endpoint", backendApi.includes("/api/mnote/tools/call") && backendApi.includes("call_mnote_weknora_tool") && backendBridge.includes("MNOTE_TOOL_CALL_ENDPOINT = \"/api/hermes/tools/mnote/call\"") ); assertCheck( failures, "backend tool call carries scope and does not expose WeKnora API key", backendBridge.includes("capabilityScope") && backendBridge.includes("knowledge_rag.read") && backendBridge.includes("allowedRoots") && backendBridge.includes("mnote_cookie") && !backendBridge.includes("MNOTE_WEKNORA_API_KEY") ); assertCheck( failures, "OpenHub stream auto-bridges KB queries through MNote WeKnora", backendStream.includes("auto_search_for_question") && backendStream.includes("") && backendStream.includes("_push_mnote_weknora_tool_event") && backendStream.includes("mnote.weknora.search") ); assertCheck( failures, "MNote session cookie is only forwarded as trusted server header", backendScope.includes("X-MNote-Session-Cookie") && mnoteRuntime.includes("mnote:open-reference") && !frontendEmbed.includes("mnote_session=") ); assertCheck( failures, "OpenHub frontend renders clickable WeKnora citations", assistantMessage.includes("data-mnote-openhub-citation") && assistantMessage.includes("openhub.weknora_citation_locator.v1") && assistantMessage.includes("onOpenMNoteCitation") && smartQuery.includes("postMNoteOpenReference") ); assertCheck( failures, "MNote host accepts citation open-reference postMessage bridge", mnoteRuntime.includes("message.type !== 'mnote:open-file' && message.type !== 'mnote:open-reference'") && mnoteRuntime.includes("openhub-citation") && mnoteRuntime.includes("pageAiOpenOpencodeChangedFile") ); const runtimeProbe = await probeEmbedOpenReference(frontendEmbed); assertCheck( failures, "runtime postMNoteOpenReference posts mnote:open-reference", runtimeProbe.ok && runtimeProbe.posted.length === 1 && runtimeProbe.posted[0].message.type === "mnote:open-reference" && runtimeProbe.posted[0].message.source === "openhub-citation" && runtimeProbe.posted[0].message.path.endsWith("task782.md"), runtimeProbe ); assertCheck( failures, "checklist has task782 completion note", checklist.includes("task782-openhub-weknora-tool-citation-bridge-smoke.js") ); fs.mkdirSync(outputDir, { recursive: true }); const result = { ok: failures.length === 0, task: "task782-openhub-weknora-tool-citation-bridge-smoke", runtimeProbe, failures, }; fs.writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); if (failures.length) { console.error(JSON.stringify(result, null, 2)); process.exit(1); } console.log(JSON.stringify(result, null, 2)); } main().catch((error) => { console.error(error); process.exit(1); });