249 lines
11 KiB
JavaScript
249 lines
11 KiB
JavaScript
#!/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 OUTPUT_DIR = path.join(process.cwd(), "tmp", "task785-weknora-create-kb-folder-ingest-search-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_TASK785_WEKNORA_TIMEOUT_MS || 240_000);
|
|
const POLL_INTERVAL_MS = Number(process.env.MNOTE_TASK785_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;
|
|
}
|
|
|
|
async function createKnowledgeBase(context, name, description) {
|
|
const result = await apiJson(context, "POST", `${BASE_URL}/api/knowledge-rag/knowledge-bases`, {
|
|
rootUri: ROOT_URI,
|
|
workspaceId: WORKSPACE_ID,
|
|
name,
|
|
description,
|
|
defaultToolEnabled: true,
|
|
});
|
|
if (!result.ok) {
|
|
throw failLayer("mnote_create_kb_http", `create KB HTTP ${result.status}`, { response: result.payload || result.text });
|
|
}
|
|
const payload = result.payload;
|
|
assert.equal(payload?.provider, "weknora", "create KB provider 应为 weknora");
|
|
assert.equal(payload?.schema, "mnote.knowledge_rag.create_knowledge_base_result.v1", "create KB schema 不正确");
|
|
assert(payload?.providerKnowledgeBaseId, `create KB 缺少 providerKnowledgeBaseId: ${JSON.stringify(payload, null, 2).slice(0, 1200)}`);
|
|
assert.equal(payload?.knowledgeBase?.provider, "weknora", "knowledgeBase provider 应为 weknora");
|
|
assert.equal(payload?.knowledgeBase?.providerKbId, payload.providerKnowledgeBaseId, "knowledgeBase.providerKbId 应匹配 providerKnowledgeBaseId");
|
|
assert.equal(payload?.knowledgeBase?.defaultToolEnabled, true, "新 KB 应默认启用 tool");
|
|
return payload;
|
|
}
|
|
|
|
async function ingestFolder(context, folderPath, providerKnowledgeBaseId) {
|
|
const result = await apiJson(context, "POST", `${BASE_URL}/api/knowledge-rag/ingest`, {
|
|
rootUri: ROOT_URI,
|
|
workspaceId: WORKSPACE_ID,
|
|
force: true,
|
|
providerKnowledgeBaseId,
|
|
sources: [{ sourcePath: folderPath }],
|
|
});
|
|
if (!result.ok) {
|
|
throw failLayer("mnote_ingest_folder_http", `ingest folder HTTP ${result.status}`, { response: result.payload || result.text });
|
|
}
|
|
const payload = result.payload;
|
|
assert.equal(payload?.provider, "weknora", "ingest provider 应为 weknora");
|
|
const configured = payload.configuredSources || [];
|
|
assert(configured.length >= 2, `文件夹入库应至少展开两个文件: ${JSON.stringify(configured, null, 2).slice(0, 1200)}`);
|
|
const mapped = configured.filter((item) => item.mappingStatus === "provider_mapped" && item.providerKnowledgeBaseId === providerKnowledgeBaseId);
|
|
assert(mapped.length >= 2, `文件夹入库未全部映射到新 KB: ${JSON.stringify(configured, null, 2).slice(0, 1600)}`);
|
|
const registryEntries = payload.registry?.entries || [];
|
|
const folderEntries = registryEntries.filter((entry) => String(entry.sourceRootRelativePath || "").startsWith(`${folderPath}/`));
|
|
assert(folderEntries.length >= 2, "source registry 应写入文件夹下的 source");
|
|
assert(folderEntries.every((entry) => entry.provider === "weknora"), "source registry provider 应为 weknora");
|
|
assert(folderEntries.every((entry) => entry.providerKnowledgeBaseId === providerKnowledgeBaseId), "source registry 应写入新 KB id");
|
|
return { payload, configured, mapped, folderEntries };
|
|
}
|
|
|
|
async function searchUntilHit(context, query, folderPath, providerKnowledgeBaseId) {
|
|
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,
|
|
mode: "hybrid",
|
|
topK: 10,
|
|
chunkTopK: 10,
|
|
includeChunkContent: true,
|
|
providerKnowledgeBaseId,
|
|
sourcePaths: [folderPath],
|
|
});
|
|
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 pathText = String(reference.sourceRootRelativePath || "");
|
|
const quote = `${reference.rawQuote || ""}\n${reference.displayQuote || ""}\n${reference.quote || ""}`;
|
|
return reference.provider === "weknora"
|
|
&& reference.providerKnowledgeBaseId === providerKnowledgeBaseId
|
|
&& (pathText.startsWith(`${folderPath}/`) || quote.includes(query));
|
|
});
|
|
if (hit) return { payload: lastPayload, reference: hit };
|
|
await sleep(POLL_INTERVAL_MS);
|
|
}
|
|
throw failLayer("weknora_new_kb_folder_search_timeout", "等待新 KB 文件夹入库内容可搜索超时", {
|
|
query,
|
|
folderPath,
|
|
providerKnowledgeBaseId,
|
|
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 startedAt = Date.now();
|
|
const markerA = `task785 create kb folder marker alpha ${startedAt}`;
|
|
const markerB = `task785 create kb folder marker beta ${startedAt}`;
|
|
const folderPath = `knowledge-rag-fixtures-7-68/task785-folder-${startedAt}`;
|
|
const absoluteFolderPath = path.join(ROOT_PATH, folderPath);
|
|
fs.mkdirSync(absoluteFolderPath, { recursive: true });
|
|
fs.writeFileSync(path.join(absoluteFolderPath, "alpha.md"), `# Task 785 Alpha\n\n${markerA}\n`, "utf8");
|
|
fs.writeFileSync(path.join(absoluteFolderPath, "beta.md"), `# Task 785 Beta\n\n${markerB}\n`, "utf8");
|
|
|
|
const name = `task785-weknora-kb-${startedAt}`;
|
|
const createResult = await createKnowledgeBase(context, name, "MNote task785 temporary KB created through MNote facade");
|
|
const providerKnowledgeBaseId = createResult.providerKnowledgeBaseId;
|
|
const ingestResult = await ingestFolder(context, folderPath, providerKnowledgeBaseId);
|
|
const searchResult = await searchUntilHit(context, markerA, folderPath, providerKnowledgeBaseId);
|
|
const afterStatus = await status(context);
|
|
const statusBase = (afterStatus.knowledgeBases?.bases || []).find((base) => base.providerKbId === providerKnowledgeBaseId);
|
|
assert(statusBase, "status knowledgeBases 应包含新建 KB");
|
|
assert(statusBase.sourceCount >= 2, `新建 KB sourceCount 应 >= 2: ${JSON.stringify(statusBase, null, 2)}`);
|
|
|
|
const persistedRegistryPath = path.join(ROOT_PATH, ".mnote", "index", "mnote-knowledge-bases.json");
|
|
const persistedRegistry = JSON.parse(fs.readFileSync(persistedRegistryPath, "utf8"));
|
|
assert((persistedRegistry.bases || []).some((base) => base.providerKbId === providerKnowledgeBaseId), "持久化 KB registry 应包含新建 KB");
|
|
|
|
const result = {
|
|
ok: true,
|
|
task: "task785-weknora-create-kb-folder-ingest-search-smoke",
|
|
baseUrl: BASE_URL,
|
|
rootUri: ROOT_URI,
|
|
workspaceId: WORKSPACE_ID,
|
|
folderPath,
|
|
absoluteFolderPath,
|
|
markers: [markerA, markerB],
|
|
knowledgeBase: {
|
|
name,
|
|
providerKnowledgeBaseId,
|
|
baseId: createResult.knowledgeBase?.baseId,
|
|
defaultToolEnabled: createResult.knowledgeBase?.defaultToolEnabled,
|
|
sourceCount: statusBase.sourceCount,
|
|
status: statusBase.status,
|
|
},
|
|
ingest: {
|
|
configuredCount: ingestResult.configured.length,
|
|
mappedCount: ingestResult.mapped.length,
|
|
sourcePaths: ingestResult.mapped.map((item) => item.sourceRootRelativePath),
|
|
},
|
|
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,
|
|
},
|
|
persistedRegistryPath,
|
|
};
|
|
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: "task785-weknora-create-kb-folder-ingest-search-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);
|
|
});
|