集成 OpenHub 与 WeKnora Page AI
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const TASK = "task775-openhub-mnote-scope-isolation-smoke";
|
||||
const OPENHUB_BASE_URL = (process.env.MNOTE_OPENHUB_BASE_URL || "http://127.0.0.1:18080").replace(/\/+$/, "");
|
||||
const WORKSPACE_ROOT = process.env.MNOTE_OPENHUB_SMOKE_ROOT
|
||||
|| "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space";
|
||||
const ROOT_URI = process.env.MNOTE_OPENHUB_SCOPE_ROOT_URI || `file://${WORKSPACE_ROOT}`;
|
||||
const WORKSPACE_KEY = process.env.MNOTE_OPENHUB_SCOPE_WORKSPACE_KEY || "local-ws:mnote-e2e:my-space";
|
||||
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_OPENHUB_SCOPE_ISOLATION_TIMEOUT_MS || 15_000);
|
||||
const STREAM_PRIME_MS = Number(process.env.MNOTE_OPENHUB_SCOPE_ISOLATION_STREAM_PRIME_MS || 3_000);
|
||||
|
||||
class SmokeFailure extends Error {
|
||||
constructor(kind, message, details = {}) {
|
||||
super(message);
|
||||
this.name = "SmokeFailure";
|
||||
this.kind = kind;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function buildScopeHeaders(ownerLabel, sessionScope) {
|
||||
return {
|
||||
"content-type": "application/json",
|
||||
"X-MNote-User-Key": `task775:${ownerLabel}`,
|
||||
"X-MNote-Workspace-Key": WORKSPACE_KEY,
|
||||
"X-MNote-Session-Scope": sessionScope,
|
||||
"X-MNote-Root-Uri": ROOT_URI,
|
||||
"X-MNote-Page-Resource-Id": "task775-openhub-scope-isolation",
|
||||
"X-MNote-Tool-Permission-Scope": JSON.stringify({
|
||||
source: TASK,
|
||||
allowedRoots: [ROOT_URI],
|
||||
}),
|
||||
"X-MNote-WeKnora-Tool-Scope": JSON.stringify({
|
||||
source: TASK,
|
||||
enabled: false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function readJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
async function assertOpenHubReachable(headers) {
|
||||
const response = await fetchWithTimeout(`${OPENHUB_BASE_URL}/api/sessions?page=1&page_size=1`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
const payload = await readJsonResponse(response);
|
||||
if (!response.ok) {
|
||||
throw new SmokeFailure("openhub_unreachable", `OpenHub MNote scope API 不可用:HTTP ${response.status}`, {
|
||||
baseUrl: OPENHUB_BASE_URL,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
return {
|
||||
status: response.status,
|
||||
success: payload?.success === true,
|
||||
};
|
||||
}
|
||||
|
||||
async function primeSessionWithOwnerA(headers, sessionId, marker) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), STREAM_PRIME_MS);
|
||||
let response;
|
||||
let streamSnippet = "";
|
||||
try {
|
||||
response = await fetch(`${OPENHUB_BASE_URL}/api/query/stream`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
question: `请只回复 ${marker},不要解释。`,
|
||||
conversation_id: sessionId,
|
||||
agent: "build",
|
||||
model: {
|
||||
providerID: process.env.MNOTE_OPENHUB_SCOPE_MODEL_PROVIDER || "opencodego",
|
||||
modelID: process.env.MNOTE_OPENHUB_SCOPE_MODEL_ID || "deepseek-v4-flash",
|
||||
currentUsage: 0,
|
||||
monthlyLimit: 0,
|
||||
},
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await readJsonResponse(response);
|
||||
throw new SmokeFailure("query_stream_failed", `Owner A 创建 session/message 失败:HTTP ${response.status}`, {
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
while (streamSnippet.length < 2_000) {
|
||||
const readResult = await Promise.race([
|
||||
reader.read(),
|
||||
delay(250).then(() => ({ timedOut: true })),
|
||||
]);
|
||||
if (readResult.timedOut) break;
|
||||
if (readResult.done) break;
|
||||
streamSnippet += decoder.decode(readResult.value, { stream: true });
|
||||
if (streamSnippet.includes(marker) || streamSnippet.includes("\"type\"")) break;
|
||||
}
|
||||
} finally {
|
||||
await reader.cancel().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.name !== "AbortError") {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
return {
|
||||
status: response?.status || 0,
|
||||
streamSnippet: streamSnippet.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchMessages(headers, sessionId) {
|
||||
const response = await fetchWithTimeout(
|
||||
`${OPENHUB_BASE_URL}/api/sessions/${encodeURIComponent(sessionId)}/messages`,
|
||||
{
|
||||
method: "GET",
|
||||
headers,
|
||||
},
|
||||
);
|
||||
const payload = await readJsonResponse(response);
|
||||
return {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForOwnerAMessages(headers, sessionId, marker) {
|
||||
let last = null;
|
||||
for (let attempt = 0; attempt < 12; attempt += 1) {
|
||||
last = await fetchMessages(headers, sessionId);
|
||||
const messages = Array.isArray(last.payload?.data) ? last.payload.data : [];
|
||||
const hasMarker = messages.some((message) => String(message.content || "").includes(marker));
|
||||
if (last.ok && hasMarker) {
|
||||
return {
|
||||
status: last.status,
|
||||
readable: true,
|
||||
count: messages.length,
|
||||
roles: messages.map((message) => message.role),
|
||||
markerFound: true,
|
||||
sample: messages.slice(-3).map((message) => ({
|
||||
role: message.role,
|
||||
content: String(message.content || "").slice(0, 240),
|
||||
})),
|
||||
};
|
||||
}
|
||||
await delay(500);
|
||||
}
|
||||
|
||||
throw new SmokeFailure("owner_a_messages_not_readable", "Owner A 未能读取到自己创建的 session/messages", {
|
||||
last,
|
||||
});
|
||||
}
|
||||
|
||||
function assertOwnerBBlocked(ownerBResult) {
|
||||
if (ownerBResult.status === 403 || ownerBResult.status === 404) {
|
||||
return {
|
||||
blocked: true,
|
||||
status: ownerBResult.status,
|
||||
detail: ownerBResult.payload?.detail || ownerBResult.payload,
|
||||
};
|
||||
}
|
||||
|
||||
throw new SmokeFailure("owner_b_not_blocked", "Owner B 读取到了或可访问 Owner A 的 session/messages", {
|
||||
status: ownerBResult.status,
|
||||
payload: ownerBResult.payload,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const runId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
const sessionId = `task775-openhub-scope-${runId}`;
|
||||
const sessionScope = `task775-openhub-scope-isolation:${runId}`;
|
||||
const marker = `MNOTE_OPENHUB_SCOPE_ISOLATION_${runId}`;
|
||||
const headersA = buildScopeHeaders("owner-a", sessionScope);
|
||||
const headersB = buildScopeHeaders("owner-b", sessionScope);
|
||||
let result = {
|
||||
ok: false,
|
||||
task: TASK,
|
||||
mode: "direct-openhub-mnote-headers-backend-scope-isolation",
|
||||
openhubBaseUrl: OPENHUB_BASE_URL,
|
||||
sessionId,
|
||||
};
|
||||
|
||||
try {
|
||||
const health = await assertOpenHubReachable(headersA);
|
||||
const stream = await primeSessionWithOwnerA(headersA, sessionId, marker);
|
||||
const ownerA = await waitForOwnerAMessages(headersA, sessionId, marker);
|
||||
const ownerB = assertOwnerBBlocked(await fetchMessages(headersB, sessionId));
|
||||
result = {
|
||||
...result,
|
||||
ok: true,
|
||||
health,
|
||||
stream,
|
||||
ownerA,
|
||||
ownerB,
|
||||
summary: {
|
||||
sessionId,
|
||||
ownerAReadable: ownerA.readable,
|
||||
ownerBBlocked: ownerB.blocked,
|
||||
ownerBStatus: ownerB.status,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
result = {
|
||||
...result,
|
||||
ok: false,
|
||||
failureKind: error instanceof SmokeFailure ? error.kind : "unexpected_error",
|
||||
error: error instanceof Error ? error.stack || error.message : String(error),
|
||||
details: error instanceof SmokeFailure ? error.details : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.ok) process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user