226 lines
7.8 KiB
JavaScript
226 lines
7.8 KiB
JavaScript
#!/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 || "/mnt/Data1T/Mnote_data/openhub/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);
|
|
});
|