集成 OpenHub 与 WeKnora Page AI
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const http = require("node:http");
|
||||
const { spawn } = require("node:child_process");
|
||||
|
||||
const OPENHUB_BACKEND_DIR = process.env.OPENHUB_BACKEND_DIR || "/tmp/mnote-openhub-research/OpenHub/smart-query-backend";
|
||||
const OPENCODE_PORT = Number(process.env.TASK793_OPENCODE_PORT || 19096);
|
||||
const OPENHUB_PORT = Number(process.env.TASK793_OPENHUB_PORT || 18181);
|
||||
const SESSION_ID = "ses_mnote_context_fullchain";
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
let data = "";
|
||||
req.on("data", (chunk) => { data += chunk; });
|
||||
req.on("end", () => resolve(data));
|
||||
});
|
||||
}
|
||||
|
||||
function sseEvent(type, properties) {
|
||||
return `data: ${JSON.stringify({ payload: { type, properties } })}\n\n`;
|
||||
}
|
||||
|
||||
function startFakeOpencode() {
|
||||
const capturedPrompts = [];
|
||||
let eventResponse = null;
|
||||
function sendEvents() {
|
||||
if (!eventResponse) return false;
|
||||
eventResponse.write(sseEvent("message.updated", {
|
||||
sessionID: SESSION_ID,
|
||||
info: { id: "msg_assistant", role: "assistant", sessionID: SESSION_ID },
|
||||
}));
|
||||
eventResponse.write(sseEvent("message.part.updated", {
|
||||
sessionID: SESSION_ID,
|
||||
part: { id: "prt_text", messageID: "msg_assistant", sessionID: SESSION_ID, type: "text", text: "OK" },
|
||||
}));
|
||||
eventResponse.write(sseEvent("session.status", {
|
||||
sessionID: SESSION_ID,
|
||||
status: { type: "idle" },
|
||||
}));
|
||||
eventResponse.end();
|
||||
eventResponse = null;
|
||||
return true;
|
||||
}
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://127.0.0.1:${OPENCODE_PORT}`);
|
||||
if (req.method === "GET" && url.pathname === "/global/health") {
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && url.pathname === "/session") {
|
||||
await readBody(req);
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ id: SESSION_ID }));
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && url.pathname === "/global/event") {
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
connection: "keep-alive",
|
||||
});
|
||||
eventResponse = res;
|
||||
eventResponse.write(`data: ${JSON.stringify({ payload: { type: "ready", properties: { sessionID: SESSION_ID } } })}\n\n`);
|
||||
if (capturedPrompts.length) {
|
||||
setTimeout(sendEvents, 50);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && url.pathname === `/session/${SESSION_ID}/prompt_async`) {
|
||||
const body = JSON.parse(await readBody(req) || "{}");
|
||||
capturedPrompts.push(body);
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
setTimeout(sendEvents, 50);
|
||||
return;
|
||||
}
|
||||
res.writeHead(404, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "not_found", path: url.pathname }));
|
||||
});
|
||||
return {
|
||||
capturedPrompts,
|
||||
listen: () => new Promise((resolve) => server.listen(OPENCODE_PORT, "127.0.0.1", resolve)),
|
||||
close: () => new Promise((resolve) => server.close(resolve)),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForHealth(url, timeoutMs = 20_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(2_000) });
|
||||
if (response.status < 500) return;
|
||||
} catch {}
|
||||
await wait(250);
|
||||
}
|
||||
throw new Error(`等待服务超时: ${url}`);
|
||||
}
|
||||
|
||||
async function sendOpenHub(payload, extraHeaders = {}) {
|
||||
const body = JSON.stringify(payload);
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({
|
||||
hostname: "127.0.0.1",
|
||||
port: OPENHUB_PORT,
|
||||
path: "/api/query/stream",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"content-length": Buffer.byteLength(body),
|
||||
"x-mnote-user-key": "openhub_user_test",
|
||||
"x-mnote-workspace-key": "workspace_test",
|
||||
"x-mnote-session-scope": `session_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
||||
"x-mnote-root-uri": "file:///tmp/mnote-fullchain-workspace",
|
||||
"x-mnote-page-resource-id": "local-md:Inbox~2FPage.md",
|
||||
...extraHeaders,
|
||||
},
|
||||
}, (res) => {
|
||||
let text = "";
|
||||
const timeout = setTimeout(() => {
|
||||
req.destroy();
|
||||
resolve(text);
|
||||
}, 10_000);
|
||||
res.setEncoding("utf8");
|
||||
res.on("data", (chunk) => {
|
||||
text += chunk;
|
||||
if (text.includes("message_complete")) {
|
||||
clearTimeout(timeout);
|
||||
req.destroy();
|
||||
resolve(text);
|
||||
}
|
||||
});
|
||||
res.on("end", () => {
|
||||
clearTimeout(timeout);
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
reject(new Error(`OpenHub HTTP ${res.statusCode}: ${text.slice(0, 500)}`));
|
||||
} else {
|
||||
resolve(text);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", (error) => {
|
||||
if (error.code === "ECONNRESET") return;
|
||||
reject(error);
|
||||
});
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fakeOpencode = startFakeOpencode();
|
||||
let uvicorn = null;
|
||||
let stderr = "";
|
||||
await fakeOpencode.listen();
|
||||
try {
|
||||
uvicorn = spawn(".venv/bin/uvicorn", ["app.main:app", "--host", "127.0.0.1", "--port", String(OPENHUB_PORT)], {
|
||||
cwd: OPENHUB_BACKEND_DIR,
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCODE_BASE_URL: `http://localhost:${OPENCODE_PORT}`,
|
||||
MNOTE_OPENHUB_TOOL_BRIDGE_AUTO: "0",
|
||||
SQLITE_DB_PATH: "/tmp/openhub-fullchain-smoke.db",
|
||||
NO_PROXY: "127.0.0.1,localhost",
|
||||
no_proxy: "127.0.0.1,localhost",
|
||||
},
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
uvicorn.stderr.on("data", (chunk) => { stderr += String(chunk); });
|
||||
await waitForHealth(`http://127.0.0.1:${OPENHUB_PORT}/api/health`);
|
||||
|
||||
const tabUrl = "http://127.0.0.1:3000/documents/local-md:Inbox~2FPage.md?sourceKind=local_folder&rootUri=file:///tmp/mnote-fullchain-workspace";
|
||||
await sendOpenHub({
|
||||
question: "只回答 OK",
|
||||
conversation_id: "",
|
||||
agent: "build",
|
||||
mnote_context: {
|
||||
kind: "tab",
|
||||
value: tabUrl,
|
||||
tabUrl,
|
||||
rootUri: "file:///tmp/mnote-fullchain-workspace",
|
||||
documentId: "local-md:Inbox~2FPage.md",
|
||||
relativePath: "Inbox/Page.md",
|
||||
},
|
||||
});
|
||||
await wait(200);
|
||||
await sendOpenHub({ question: "只回答 OK fallback", conversation_id: "", agent: "build" });
|
||||
await wait(200);
|
||||
|
||||
const explicitPrompt = fakeOpencode.capturedPrompts[0]?.parts?.[0]?.text || "";
|
||||
const fallbackPrompt = fakeOpencode.capturedPrompts[1]?.parts?.[0]?.text || "";
|
||||
const result = {
|
||||
ok: false,
|
||||
promptCount: fakeOpencode.capturedPrompts.length,
|
||||
explicitContextOk: explicitPrompt.includes("<mnote_current_context>")
|
||||
&& explicitPrompt.includes("当前页面: http://127.0.0.1:3000/documents/")
|
||||
&& explicitPrompt.includes("relativePath: Inbox/Page.md"),
|
||||
fallbackContextOk: fallbackPrompt.includes("<mnote_current_context>")
|
||||
&& fallbackPrompt.includes("source: mnote_scope_fallback")
|
||||
&& fallbackPrompt.includes("file:///tmp/mnote-fullchain-workspace"),
|
||||
explicitPromptPreview: explicitPrompt.slice(0, 700),
|
||||
fallbackPromptPreview: fallbackPrompt.slice(0, 700),
|
||||
};
|
||||
result.ok = result.promptCount >= 2 && result.explicitContextOk && result.fallbackContextOk;
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.ok) process.exitCode = 1;
|
||||
} finally {
|
||||
if (uvicorn) {
|
||||
uvicorn.kill("SIGTERM");
|
||||
await wait(400);
|
||||
}
|
||||
await fakeOpencode.close();
|
||||
if (process.exitCode) process.stderr.write(stderr.slice(-3000));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user