集成 OpenHub 与 WeKnora Page AI
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { request } = require("playwright");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const ROOT_PATH = process.env.MNOTE_KNOWLEDGE_RAG_ROOT_PATH || "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space";
|
||||
const ROOT_URI = process.env.MNOTE_KNOWLEDGE_RAG_ROOT_URI || `file://${ROOT_PATH}`;
|
||||
const WORKSPACE_ID = process.env.MNOTE_KNOWLEDGE_RAG_WORKSPACE_ID || "local-ws:mnote-e2e:my-space";
|
||||
const FIXTURE_DIR = process.env.MNOTE_TASK781_FIXTURE_DIR || "knowledge-rag-fixtures-7-68";
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task781-knowledge-bases-registry-smoke");
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const FAILURE_PATH = path.join(OUTPUT_DIR, "failure.json");
|
||||
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||
const POLL_TIMEOUT_MS = Number(process.env.MNOTE_TASK781_WEKNORA_TIMEOUT_MS || 240_000);
|
||||
const POLL_INTERVAL_MS = Number(process.env.MNOTE_TASK781_WEKNORA_POLL_MS || 5_000);
|
||||
|
||||
function writeJson(filePath, payload) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function failLayer(layer, message, details = {}) {
|
||||
const error = new Error(message);
|
||||
error.layer = layer;
|
||||
error.details = details;
|
||||
return error;
|
||||
}
|
||||
|
||||
async function signIn(context) {
|
||||
const response = await context.post(`${BASE_URL}/api/auth`, {
|
||||
data: {
|
||||
action: "auth:signIn",
|
||||
args: {
|
||||
provider: "password",
|
||||
params: {
|
||||
email: "mnote.e2e@example.com",
|
||||
password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
|
||||
flow: "signIn",
|
||||
},
|
||||
},
|
||||
},
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
assert(response.ok(), `登录失败: ${response.status()} ${await response.text()}`);
|
||||
}
|
||||
|
||||
async function apiJson(context, method, url, data) {
|
||||
const response = await context.fetch(url, {
|
||||
method,
|
||||
data,
|
||||
headers: {
|
||||
"x-mnote-actor-id": "mnote-e2e",
|
||||
"x-mnote-actor-type": "user",
|
||||
"x-mnote-workspace-id": WORKSPACE_ID,
|
||||
accept: "application/json",
|
||||
},
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
payload = { rawText: text };
|
||||
}
|
||||
return { ok: response.ok(), status: response.status(), payload, text };
|
||||
}
|
||||
|
||||
async function status(context) {
|
||||
const params = new URLSearchParams({ rootUri: ROOT_URI, workspaceId: WORKSPACE_ID });
|
||||
const result = await apiJson(context, "GET", `${BASE_URL}/api/knowledge-rag/status?${params.toString()}`);
|
||||
assert(result.ok, `status HTTP ${result.status}: ${JSON.stringify(result.payload || result.text).slice(0, 1000)}`);
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
function assertKnowledgeBases(payload) {
|
||||
assert.equal(payload?.provider, "weknora", `status provider 应为 weknora: ${JSON.stringify(payload?.providerConfig || payload, null, 2).slice(0, 1000)}`);
|
||||
assert.equal(payload?.knowledgeBases?.schema, "mnote.knowledge_bases.registry.v1", "status 应返回 mnote_knowledge_bases registry schema");
|
||||
assert.equal(payload?.registry?.schema, "mnote.knowledge_rag.source_registry.v1", "status 应保留旧 source registry schema");
|
||||
assert.equal(payload?.providerConfig?.weknora?.knowledgeBaseRegistry, "mnote_knowledge_bases", "providerConfig 应暴露 KB registry 边界");
|
||||
assert.equal(payload?.providerConfig?.weknora?.sourceRegistry, "mnote_knowledge_sources", "providerConfig 应暴露 source registry 边界");
|
||||
const bases = payload?.knowledgeBases?.bases || [];
|
||||
assert(Array.isArray(bases) && bases.length > 0, `knowledgeBases.bases 应至少包含默认 WeKnora KB: ${JSON.stringify(payload?.knowledgeBases, null, 2)}`);
|
||||
const base = bases.find((item) => item.provider === "weknora" && item.providerKbId);
|
||||
assert(base, `缺少 provider-neutral WeKnora KB: ${JSON.stringify(bases, null, 2)}`);
|
||||
assert.equal(base.defaultToolEnabled, true, "默认 WeKnora KB 应开启 tool 可见性");
|
||||
return base;
|
||||
}
|
||||
|
||||
async function ingest(context, sourcePath) {
|
||||
const result = await apiJson(context, "POST", `${BASE_URL}/api/knowledge-rag/ingest`, {
|
||||
rootUri: ROOT_URI,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
force: true,
|
||||
sources: [{ sourcePath }],
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw failLayer("mnote_ingest_http", `ingest HTTP ${result.status}`, { response: result.payload || result.text });
|
||||
}
|
||||
const payload = result.payload;
|
||||
assert.equal(payload?.provider, "weknora", "ingest provider 应为 weknora");
|
||||
assert.equal(payload?.knowledgeBases?.schema, "mnote.knowledge_bases.registry.v1", "ingest 应返回 knowledgeBases registry");
|
||||
const item = (payload.configuredSources || []).find((source) => source.sourceRootRelativePath === sourcePath);
|
||||
assert(item, `ingest 结果缺少目标 source: ${JSON.stringify(payload.configuredSources, null, 2).slice(0, 1000)}`);
|
||||
assert.equal(item.mappingStatus, "provider_mapped", `ingest 应 provider_mapped: ${JSON.stringify(item, null, 2)}`);
|
||||
assert(item.providerKnowledgeBaseId, `ingest 缺少 providerKnowledgeBaseId: ${JSON.stringify(item, null, 2)}`);
|
||||
assert(item.providerKnowledgeId, `ingest 缺少 providerKnowledgeId: ${JSON.stringify(item, null, 2)}`);
|
||||
const registryEntry = (payload.registry?.entries || []).find((entry) => entry.sourceRootRelativePath === sourcePath);
|
||||
assert(registryEntry, "source registry 应写入目标 source");
|
||||
assert.equal(registryEntry.provider, "weknora", `source registry 应写 provider: ${JSON.stringify(registryEntry, null, 2)}`);
|
||||
assert.equal(registryEntry.providerKnowledgeBaseId, item.providerKnowledgeBaseId, "source registry 应写 providerKnowledgeBaseId");
|
||||
assert.equal(registryEntry.providerKnowledgeId, item.providerKnowledgeId, "source registry 应写 providerKnowledgeId");
|
||||
assert.equal(registryEntry.providerSourceId, item.providerKnowledgeId, "source registry 应写 providerSourceId");
|
||||
assert(registryEntry.lightRagDocId, "旧 lightRagDocId 兼容字段仍应保留 provider knowledge id");
|
||||
return { payload, item, registryEntry };
|
||||
}
|
||||
|
||||
async function search(context, marker, sourcePath) {
|
||||
const startedAt = Date.now();
|
||||
let lastPayload = null;
|
||||
while (Date.now() - startedAt < POLL_TIMEOUT_MS) {
|
||||
const result = await apiJson(context, "POST", `${BASE_URL}/api/knowledge-rag/search`, {
|
||||
rootUri: ROOT_URI,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
query: marker,
|
||||
mode: "hybrid",
|
||||
topK: 10,
|
||||
chunkTopK: 10,
|
||||
includeChunkContent: true,
|
||||
sourcePaths: [sourcePath],
|
||||
});
|
||||
assert(result.ok, `search HTTP ${result.status}: ${JSON.stringify(result.payload || result.text).slice(0, 1000)}`);
|
||||
lastPayload = result.payload;
|
||||
const hit = (lastPayload.references || []).find((reference) => {
|
||||
const quote = `${reference.rawQuote || ""}\n${reference.displayQuote || ""}\n${reference.quote || ""}`;
|
||||
return reference.provider === "weknora" && (reference.sourceRootRelativePath === sourcePath || quote.includes(marker));
|
||||
});
|
||||
if (hit) return { payload: lastPayload, reference: hit };
|
||||
await sleep(POLL_INTERVAL_MS);
|
||||
}
|
||||
throw failLayer("weknora_search_timeout", "等待 WeKnora search 返回 task781 source 超时", { marker, sourcePath, lastPayload });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
if (!fs.existsSync(ROOT_PATH)) {
|
||||
throw failLayer("allowed_root_missing", `allowed root 不存在: ${ROOT_PATH}`);
|
||||
}
|
||||
|
||||
const context = await request.newContext({ baseURL: BASE_URL });
|
||||
try {
|
||||
await signIn(context);
|
||||
const beforeStatus = await status(context);
|
||||
const baseBefore = assertKnowledgeBases(beforeStatus);
|
||||
|
||||
const startedAt = Date.now();
|
||||
const marker = `task781 knowledge bases registry marker ${startedAt}`;
|
||||
const sourcePath = `${FIXTURE_DIR}/task781-knowledge-base-registry-${startedAt}.md`;
|
||||
const absoluteSourcePath = path.join(ROOT_PATH, sourcePath);
|
||||
fs.mkdirSync(path.dirname(absoluteSourcePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
absoluteSourcePath,
|
||||
[
|
||||
"# Task 781 Knowledge Base Registry",
|
||||
"",
|
||||
marker,
|
||||
"",
|
||||
"This file verifies MNote provider-neutral knowledge base and source registry boundaries.",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const ingestResult = await ingest(context, sourcePath);
|
||||
const searchResult = await search(context, marker, sourcePath);
|
||||
const afterStatus = await status(context);
|
||||
const baseAfter = assertKnowledgeBases(afterStatus);
|
||||
|
||||
const persistedRegistryPath = path.join(ROOT_PATH, ".mnote", "index", "mnote-knowledge-bases.json");
|
||||
assert(fs.existsSync(persistedRegistryPath), `应持久化 mnote_knowledge_bases registry: ${persistedRegistryPath}`);
|
||||
const persistedRegistry = JSON.parse(fs.readFileSync(persistedRegistryPath, "utf8"));
|
||||
assert.equal(persistedRegistry.schema, "mnote.knowledge_bases.registry.v1", "持久化 KB registry schema 不正确");
|
||||
assert((persistedRegistry.bases || []).some((base) => base.provider === "weknora" && base.providerKbId), "持久化 KB registry 缺少 WeKnora base");
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
task: "task781-knowledge-bases-registry-smoke",
|
||||
baseUrl: BASE_URL,
|
||||
rootUri: ROOT_URI,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
sourcePath,
|
||||
absoluteSourcePath,
|
||||
marker,
|
||||
knowledgeBase: {
|
||||
before: {
|
||||
baseId: baseBefore.baseId,
|
||||
provider: baseBefore.provider,
|
||||
providerKbId: baseBefore.providerKbId,
|
||||
defaultToolEnabled: baseBefore.defaultToolEnabled,
|
||||
},
|
||||
after: {
|
||||
baseId: baseAfter.baseId,
|
||||
provider: baseAfter.provider,
|
||||
providerKbId: baseAfter.providerKbId,
|
||||
defaultToolEnabled: baseAfter.defaultToolEnabled,
|
||||
sourceCount: baseAfter.sourceCount,
|
||||
},
|
||||
persistedRegistryPath,
|
||||
},
|
||||
sourceRegistry: {
|
||||
provider: ingestResult.registryEntry.provider,
|
||||
providerStatus: ingestResult.registryEntry.providerStatus,
|
||||
providerKnowledgeBaseId: ingestResult.registryEntry.providerKnowledgeBaseId,
|
||||
providerKnowledgeId: ingestResult.registryEntry.providerKnowledgeId,
|
||||
providerSourceId: ingestResult.registryEntry.providerSourceId,
|
||||
lightRagDocIdCompat: ingestResult.registryEntry.lightRagDocId,
|
||||
},
|
||||
search: {
|
||||
provider: searchResult.payload.provider,
|
||||
referenceProvider: searchResult.reference.provider,
|
||||
sourceRootRelativePath: searchResult.reference.sourceRootRelativePath,
|
||||
providerKnowledgeBaseId: searchResult.reference.providerKnowledgeBaseId,
|
||||
providerKnowledgeId: searchResult.reference.providerKnowledgeId,
|
||||
providerChunkId: searchResult.reference.providerChunkId,
|
||||
},
|
||||
};
|
||||
writeJson(RESULT_PATH, result);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} finally {
|
||||
await context.dispose().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
const payload = {
|
||||
ok: false,
|
||||
task: "task781-knowledge-bases-registry-smoke",
|
||||
layer: error.layer || "unexpected",
|
||||
error: error.stack || error.message || String(error),
|
||||
details: error.details || null,
|
||||
baseUrl: BASE_URL,
|
||||
rootUri: ROOT_URI,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
};
|
||||
writeJson(FAILURE_PATH, payload);
|
||||
console.error(JSON.stringify(payload, null, 2));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user