From 2547cb741f87adea08d867d2b99ce0827debb8cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:41:57 +0000 Subject: [PATCH 1/8] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 2cff03f..fba8ff6 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "7.5.0", + "version": "7.5.1", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From 8d36b8daa6bcabc1117b229ed28e9048e3a57555 Mon Sep 17 00:00:00 2001 From: youzini Date: Tue, 9 Jun 2026 11:32:18 +0000 Subject: [PATCH 2/8] fix(host): restore event priority and EJS rendering --- ena-planner/ena-planner.js | 19 +++-- host/event-binding.js | 23 +++--- tests/ena-planner-ejs.mjs | 70 ++++++++++++++++++ tests/event-binding-priority.mjs | 117 +++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 tests/ena-planner-ejs.mjs create mode 100644 tests/event-binding-priority.mjs diff --git a/ena-planner/ena-planner.js b/ena-planner/ena-planner.js index ce8743a..92ef842 100644 --- a/ena-planner/ena-planner.js +++ b/ena-planner/ena-planner.js @@ -1145,7 +1145,11 @@ function getChatVariables() { } function buildEjsContext() { - const vars = getChatVariables(); + return createEnaPlannerEjsContext(getChatVariables()); +} + +export function createEnaPlannerEjsContext(varsInput = {}) { + const vars = varsInput && typeof varsInput === 'object' ? { ...varsInput } : {}; // getvar: read a chat variable (supports dot-path for nested objects) function getvar(name) { @@ -1195,7 +1199,7 @@ function shouldSkipSyncEjsPreRender(template) { return false; } -function renderEjsTemplate(template, ctx, templateLabel = '') { +export function renderEjsTemplate(template, ctx, templateLabel = '') { const labelSuffix = templateLabel ? ` (${templateLabel})` : ''; if (shouldSkipSyncEjsPreRender(template)) { @@ -1205,7 +1209,15 @@ function renderEjsTemplate(template, ctx, templateLabel = '') { // Try window.ejs first (ST loads this library) if (window.ejs?.render) { try { - return window.ejs.render(template, ctx, { async: false }); + const renderCtx = ctx && typeof ctx === 'object' ? { ...ctx } : ctx; + if (renderCtx && typeof renderCtx === 'object') { + delete renderCtx.__append; + delete renderCtx.print; + } + return window.ejs.render(template, renderCtx, { + async: false, + outputFunctionName: 'print', + }); } catch (e) { console.warn(`[EnaPlanner] EJS render failed${labelSuffix}, template returned as-is:`, e?.message); return template; @@ -2017,4 +2029,3 @@ export function cleanupEnaPlanner() { delete window.stBmeEnaPlanner; _bmeRuntime = null; } - diff --git a/host/event-binding.js b/host/event-binding.js index 981fa0c..37a35f2 100644 --- a/host/event-binding.js +++ b/host/event-binding.js @@ -34,25 +34,28 @@ function isTavernHelperPromptViewerSyntheticGeneration(runtime) { } export function registerBeforeCombinePromptsController(runtime, listener) { - const makeFirst = runtime.getEventMakeFirst(); + const eventName = runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS; + const eventSourceMakeFirst = runtime.eventSource?.makeFirst; + const makeFirst = + typeof eventSourceMakeFirst === "function" + ? eventSourceMakeFirst.bind(runtime.eventSource) + : runtime.getEventMakeFirst?.(); if (typeof makeFirst === "function") { - return makeFirst( - runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS, - listener, - ); + return makeFirst(eventName, listener); } runtime.console.warn("[ST-BME] eventMakeFirst 不可用,回退到普通事件注册"); - runtime.eventSource.on( - runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS, - listener, - ); + runtime.eventSource.on(eventName, listener); return null; } export function registerGenerationAfterCommandsController(runtime, listener) { - const makeFirst = runtime.getEventMakeFirst(); const eventName = runtime.eventTypes.GENERATION_AFTER_COMMANDS; + const eventSourceMakeFirst = runtime.eventSource?.makeFirst; + const makeFirst = + typeof eventSourceMakeFirst === "function" + ? eventSourceMakeFirst.bind(runtime.eventSource) + : runtime.getEventMakeFirst?.(); if (typeof makeFirst === "function") { const cleanup = makeFirst(eventName, listener); return cleanup; diff --git a/tests/ena-planner-ejs.mjs b/tests/ena-planner-ejs.mjs new file mode 100644 index 0000000..b2517fe --- /dev/null +++ b/tests/ena-planner-ejs.mjs @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { + installResolveHooks, + toDataModuleUrl, +} from "./helpers/register-hooks-compat.mjs"; + +const extensionsShimSource = [ + "export const extension_settings = {};", +].join("\n"); +const scriptShimSource = [ + "export function getRequestHeaders() { return {}; }", + "export function saveSettingsDebounced() {}", + "export function substituteParamsExtended(text = '') { return String(text ?? ''); }", +].join("\n"); + +installResolveHooks([ + { + specifiers: ["../../../../extensions.js"], + url: toDataModuleUrl(extensionsShimSource), + }, + { + specifiers: ["../../../../../script.js"], + url: toDataModuleUrl(scriptShimSource), + }, +]); + +const require = createRequire(import.meta.url); +const ejs = require("../vendor/ejs.js"); +const originalWindow = globalThis.window; +const originalWarn = console.warn; +const warnings = []; + +try { + globalThis.window = { ...(originalWindow || {}), ejs }; + console.warn = (...args) => warnings.push(args); + + const { createEnaPlannerEjsContext, renderEjsTemplate } = await import( + "../ena-planner/ena-planner.js" + ); + + const ctx = createEnaPlannerEjsContext({ x: "alpha" }); + assert.equal(renderEjsTemplate("<%= getvar('x') %>", ctx), "alpha"); + assert.equal(renderEjsTemplate("<% print(getvar('x')) %>", ctx), "alpha"); + + const pollutedCtx = { + ...createEnaPlannerEjsContext({ x: "safe" }), + __append() { + throw new Error("locals __append should not shadow EJS output"); + }, + print() { + throw new Error("locals print should not shadow EJS output function"); + }, + }; + assert.equal(renderEjsTemplate("<%= getvar('x') %>", pollutedCtx), "safe"); + assert.equal(renderEjsTemplate("<% print(getvar('x')) %>", pollutedCtx), "safe"); + + const invalidTemplate = "before <% if ( %> after"; + assert.equal(renderEjsTemplate(invalidTemplate, ctx, "invalid"), invalidTemplate); + assert.ok(warnings.some((args) => String(args[0]).includes("EJS render failed"))); +} finally { + console.warn = originalWarn; + if (originalWindow === undefined) { + delete globalThis.window; + } else { + globalThis.window = originalWindow; + } +} + +console.log("ena-planner-ejs tests passed"); diff --git a/tests/event-binding-priority.mjs b/tests/event-binding-priority.mjs new file mode 100644 index 0000000..8d5cda3 --- /dev/null +++ b/tests/event-binding-priority.mjs @@ -0,0 +1,117 @@ +import assert from "node:assert/strict"; +import { + registerBeforeCombinePromptsController, + registerGenerationAfterCommandsController, +} from "../host/event-binding.js"; + +function createRuntime(eventSource, overrides = {}) { + return { + console: { warn() {} }, + eventSource, + eventTypes: { + GENERATE_BEFORE_COMBINE_PROMPTS: "before-combine", + GENERATION_AFTER_COMMANDS: "after-commands", + }, + getEventMakeFirst: () => undefined, + ...overrides, + }; +} + +function testEventSourceMakeFirstWinsAndIsBound() { + const calls = []; + const fallbackCalls = []; + const eventSource = { + marker: "event-source", + makeFirst(eventName, listener) { + assert.equal(this, eventSource); + calls.push({ eventName, listener }); + return () => calls.push({ cleanup: eventName }); + }, + on() { + throw new Error("ordinary .on should not be used when makeFirst exists"); + }, + }; + const runtime = createRuntime(eventSource, { + getEventMakeFirst: () => (...args) => fallbackCalls.push(args), + }); + const beforeListener = () => {}; + const afterListener = () => {}; + + const beforeCleanup = registerBeforeCombinePromptsController( + runtime, + beforeListener, + ); + const afterCleanup = registerGenerationAfterCommandsController( + runtime, + afterListener, + ); + + assert.equal(typeof beforeCleanup, "function"); + assert.equal(typeof afterCleanup, "function"); + assert.deepEqual(calls, [ + { eventName: "before-combine", listener: beforeListener }, + { eventName: "after-commands", listener: afterListener }, + ]); + assert.deepEqual(fallbackCalls, []); +} + +function testRuntimeMakeFirstFallback() { + const calls = []; + const eventSource = { + on() { + throw new Error("ordinary .on should not be used when fallback exists"); + }, + }; + const runtime = createRuntime(eventSource, { + getEventMakeFirst: () => (eventName, listener) => { + calls.push({ eventName, listener }); + return `cleanup:${eventName}`; + }, + }); + const beforeListener = () => {}; + const afterListener = () => {}; + + assert.equal( + registerBeforeCombinePromptsController(runtime, beforeListener), + "cleanup:before-combine", + ); + assert.equal( + registerGenerationAfterCommandsController(runtime, afterListener), + "cleanup:after-commands", + ); + assert.deepEqual(calls, [ + { eventName: "before-combine", listener: beforeListener }, + { eventName: "after-commands", listener: afterListener }, + ]); +} + +function testOrdinaryOnFallback() { + const calls = []; + const eventSource = { + on(eventName, listener) { + calls.push({ eventName, listener }); + }, + }; + const runtime = createRuntime(eventSource); + const beforeListener = () => {}; + const afterListener = () => {}; + + assert.equal( + registerBeforeCombinePromptsController(runtime, beforeListener), + null, + ); + assert.equal( + registerGenerationAfterCommandsController(runtime, afterListener), + null, + ); + assert.deepEqual(calls, [ + { eventName: "before-combine", listener: beforeListener }, + { eventName: "after-commands", listener: afterListener }, + ]); +} + +testEventSourceMakeFirstWinsAndIsBound(); +testRuntimeMakeFirstFallback(); +testOrdinaryOnFallback(); + +console.log("event-binding-priority tests passed"); From 792d1ec309f1e9e303870a8a618cc4215c9d525c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:32:40 +0000 Subject: [PATCH 3/8] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index fba8ff6..44bf0f7 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "7.5.1", + "version": "7.5.2", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From e59d8628fabb2d1a1dc9c08040ccbbce6cbebc5b Mon Sep 17 00:00:00 2001 From: youzini Date: Tue, 9 Jun 2026 11:33:23 +0000 Subject: [PATCH 4/8] fix(vector): align authority apply item identity --- tests/authority-vector-primary.mjs | 9 +++++++++ vector/authority-vector-primary-adapter.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/authority-vector-primary.mjs b/tests/authority-vector-primary.mjs index 68a14ed..b6b4f67 100644 --- a/tests/authority-vector-primary.mjs +++ b/tests/authority-vector-primary.mjs @@ -169,6 +169,10 @@ function createMockTriviumClient({ path: "/bme/vector-apply", }); } + const itemWithTopLevelId = payload.items?.find((item) => item?.id !== undefined); + if (itemWithTopLevelId) { + throw new Error("bmeVectorApply items must not send top-level Trivium id"); + } return { ok: true, database: payload.database || "st_bme_vectors", @@ -266,6 +270,11 @@ assert.equal(isAuthorityVectorConfig(config), true); assert.equal(applyCall.items.every((item) => item.payload?.vectorSpaceId === applyCall.vectorSpaceId), true); assert.equal(applyCall.items.every((item) => item.payload?.observedDim === 2), true); assert.equal(applyCall.items.every((item) => Array.isArray(item.vector) && item.vector.length > 0), true); + assert.equal(applyCall.items[0].id, undefined); + assert.equal(applyCall.items[0].externalId, "node-a"); + assert.equal(applyCall.items[0].nodeId, "node-a"); + assert.equal(applyCall.items[0].payload.nodeId, "node-a"); + assert.equal(applyCall.items[0].payload.externalId, "node-a"); assert.equal(result.timings.authorityDiagnostics.upsert.operation, "bmeVectorApply"); } diff --git a/vector/authority-vector-primary-adapter.js b/vector/authority-vector-primary-adapter.js index 3ff18b9..7f0ec6d 100644 --- a/vector/authority-vector-primary-adapter.js +++ b/vector/authority-vector-primary-adapter.js @@ -261,6 +261,7 @@ function buildAuthorityNodePayload(node = {}, entry = {}, { chatId = "", modelSc return { chatId, nodeId: normalizeRecordId(node?.id || entry?.nodeId), + externalId: normalizeRecordId(node?.id || entry?.nodeId), type: String(node?.type || ""), archived: Boolean(node?.archived), seqStart: Number(seqRange[0] ?? node?.seq ?? 0) || 0, @@ -295,7 +296,6 @@ function buildAuthorityVectorItems(graph, entries = [], options = {}) { if (!node) return null; const payload = buildAuthorityNodePayload(node, entry, options); return { - id: nodeId, externalId: nodeId, nodeId, text: String(entry?.text || ""), From 46cc7b5094d635d8d92e28f5baaaccd0b4507dd2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:33:46 +0000 Subject: [PATCH 5/8] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 44bf0f7..5be7b58 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "7.5.2", + "version": "7.5.3", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From b56103c6fd579ee5fba34fcf767242097b4bebed Mon Sep 17 00:00:00 2001 From: youzini Date: Tue, 9 Jun 2026 11:36:20 +0000 Subject: [PATCH 6/8] fix(vector): scope authority search by namespace --- tests/authority-vector-primary.mjs | 47 ++++++++++++++++++++++ vector/authority-vector-primary-adapter.js | 28 +++++++++++-- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/tests/authority-vector-primary.mjs b/tests/authority-vector-primary.mjs index b6b4f67..68dc8d8 100644 --- a/tests/authority-vector-primary.mjs +++ b/tests/authority-vector-primary.mjs @@ -33,6 +33,7 @@ const { isAuthorityVectorConfig, normalizeAuthorityVectorConfig, queryAuthorityTriviumNeighbors, + searchAuthorityTriviumNodes, applyAuthorityBmeVectorManifest, } = await import("../vector/authority-vector-primary-adapter.js"); const { @@ -125,6 +126,7 @@ function createMockTriviumClient({ } return { results: [ + { nodeId: "node-a", namespace: "other-chat", score: 0.95 }, { nodeId: "node-b", score: 0.91 }, { nodeId: "node-outside", score: 0.88 }, ], @@ -378,6 +380,7 @@ assert.equal(isAuthorityVectorConfig(config), true); assert.deepEqual(results, [{ nodeId: "node-b", score: 0.91 }]); const searchCall = triviumClient.calls.find(([name]) => name === "search"); assert.deepEqual(searchCall?.[1]?.candidateIds.sort(), ["node-a", "node-b"]); + assert.equal(searchCall?.[1]?.namespace, "st-bme::chat-authority-vector"); assert.equal(Array.isArray(searchCall?.[1]?.queryVector), true); assert.ok(searchCall?.[1]?.queryVector.length > 0); assert.equal(graph.vectorIndexState.lastSearchTimings.mode, "authority"); @@ -445,6 +448,50 @@ assert.equal(isAuthorityVectorConfig(config), true); } } +{ + const { graph } = createAuthorityVectorGraph(); + const fetchCalls = []; + const fetchImpl = async (url, options = {}) => { + const body = JSON.parse(String(options.body || "{}")); + fetchCalls.push({ url: String(url), body }); + if (String(url).endsWith("/session/init")) { + return { + ok: true, + status: 200, + json: async () => ({ sessionToken: "test-session" }), + }; + } + return { + ok: true, + status: 200, + json: async () => ({ + results: [ + { externalId: "node-a", namespace: "other-chat", score: 0.99 }, + { externalId: "node-b", namespace: "st-bme::chat-authority-vector", score: 0.93 }, + { externalId: "node-c", score: 0.72 }, + ], + }), + }; + }; + + const results = await searchAuthorityTriviumNodes(graph, "archive door", config, { + namespace: "st-bme::chat-authority-vector", + collectionId: "st-bme::chat-authority-vector", + chatId: "chat-authority-vector", + queryVector: [1, 0, 0], + topK: 5, + fetchImpl, + }); + const searchCall = fetchCalls.find((call) => call.url.endsWith("/trivium/search-hybrid")); + assert.equal(searchCall?.body?.namespace, "st-bme::chat-authority-vector"); + assert.equal(searchCall?.body?.collectionId, "st-bme::chat-authority-vector"); + assert.equal(searchCall?.body?.chatId, "chat-authority-vector"); + assert.deepEqual( + results.map((entry) => entry.nodeId), + ["node-b", "node-c"], + ); +} + { const { graph, first, second } = createAuthorityVectorGraph(); const triviumClient = createMockTriviumClient({ failSearch: true }); diff --git a/vector/authority-vector-primary-adapter.js b/vector/authority-vector-primary-adapter.js index 7f0ec6d..782c804 100644 --- a/vector/authority-vector-primary-adapter.js +++ b/vector/authority-vector-primary-adapter.js @@ -140,6 +140,15 @@ function normalizeNodeResultId(item = null) { ); } +function normalizeSearchResultNamespace(item = null) { + return normalizeRecordId( + item?.namespace || + item?.collectionId || + readNestedValue(item, ["payload", "namespace"]) || + readNestedValue(item, ["payload", "collectionId"]), + ); +} + function readResultRows(payload = null) { if (Array.isArray(payload)) return payload; if (!payload || typeof payload !== "object") return []; @@ -211,12 +220,17 @@ function getNodeFieldText(node = {}, keys = []) { return ""; } -function normalizeSearchResults(payload = null) { +function normalizeSearchResults(payload = null, { namespace = "" } = {}) { const rows = readResultRows(payload); + const expectedNamespace = normalizeRecordId(namespace); return rows .map((item, index) => { const nodeId = normalizeNodeResultId(item); if (!nodeId) return null; + const resultNamespace = normalizeSearchResultNamespace(item); + if (expectedNamespace && resultNamespace && resultNamespace !== expectedNamespace) { + return null; + } const rawScore = Number(item?.score ?? item?.similarity ?? item?.rankScore); const distance = Number(item?.distance); const score = Number.isFinite(rawScore) @@ -224,7 +238,11 @@ function normalizeSearchResults(payload = null) { : Number.isFinite(distance) ? 1 / (1 + Math.max(0, distance)) : Math.max(0.01, 1 - index / Math.max(1, rows.length)); - return { nodeId, score }; + return { + nodeId, + score, + ...(resultNamespace ? { namespace: resultNamespace } : {}), + }; }) .filter(Boolean); } @@ -597,8 +615,12 @@ export class AuthorityTriviumHttpClient { throw new Error("Authority Trivium v0.6 search requires vector"); } const queryText = String(payload.queryText || payload.text || payload.searchText || payload.query || ""); + const namespace = getNamespace(payload); const body = { ...this.buildOpenOptions(payload), + ...(namespace ? { namespace } : {}), + ...(payload.collectionId ? { collectionId: String(payload.collectionId) } : {}), + ...(payload.chatId ? { chatId: String(payload.chatId) } : {}), vector, topK: Number(payload.topK || payload.limit || 0) || undefined, expandDepth: Number(payload.expandDepth || payload.depth || 0) || undefined, @@ -1045,7 +1067,7 @@ export async function searchAuthorityTriviumNodes(graph, text, config = {}, opti topK: Math.max(1, Math.floor(Number(options.topK) || 1)), candidateIds: toArray(options.candidateIds).map(normalizeRecordId).filter(Boolean), }); - return normalizeSearchResults(payload); + return normalizeSearchResults(payload, { namespace: options.namespace }); } export async function testAuthorityTriviumConnection(config = {}, options = {}) { From 63c19a2ecc750c86457d659134ddd1d9049dbb03 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:36:41 +0000 Subject: [PATCH 7/8] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 5be7b58..cc91937 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "7.5.3", + "version": "7.5.4", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From 091ee17f8b1a397053bece949bdd4d8d1d616df5 Mon Sep 17 00:00:00 2001 From: youzini Date: Tue, 9 Jun 2026 11:38:27 +0000 Subject: [PATCH 8/8] test(vector): lock authority protocol contracts --- docs/algorithms/vector-and-embedding.md | 4 ++++ docs/architecture/server-integration.md | 6 ++++++ tests/authority-vector-primary.mjs | 10 ++++++++++ tests/e2e/authority-server-primary.mjs | 9 +++++++++ 4 files changed, 29 insertions(+) diff --git a/docs/algorithms/vector-and-embedding.md b/docs/algorithms/vector-and-embedding.md index e777e8f..92bf065 100644 --- a/docs/algorithms/vector-and-embedding.md +++ b/docs/algorithms/vector-and-embedding.md @@ -51,6 +51,10 @@ HTTP 错误(400/401/403/429/502 等)会带状态码和响应体抛出,而 > BME 在 payload 里发送 `vectorSpaceId` 和 `observedDim`(顶层 + 每项元数据)。DOA 按批校验 vectorSpaceId/observedDim 一致性,拒绝混合维度,返回带类型的校验错误。失败/404/旧 DOA 时回退到旧 Authority Trivium 路径或本地。 +协议身份字段有严格分工:BME 节点 id 只作为 `externalId` / `nodeId` / `payload.nodeId` 发送;`id` 保留给 Trivium 内部 numeric id,BME 不会把字符串 node id 塞进顶层 `id`。边关系使用 Trivium public reference 形状:`src/dst = { externalId, namespace }`,由 Authority 解析到内部 id。 + +搜索请求也必须携带 `namespace` / `collectionId` / `chatId`。如果 Authority 返回带 namespace 的命中,BME 会保守过滤掉不属于当前 namespace 的结果;老后端不返回 namespace 时结果仍保留,以避免过度破坏兼容性。 + ## 连接测试 `testVectorConnection()` 测的是**真实批量 embedding 路径**(走 `embedBatch`),而不是单条短文本——因为"测试通过但实际 embedding 失败"的根因就是测试只测了单条短文本而运行时用的是批量长文本。 diff --git a/docs/architecture/server-integration.md b/docs/architecture/server-integration.md index c065354..97fe411 100644 --- a/docs/architecture/server-integration.md +++ b/docs/architecture/server-integration.md @@ -65,6 +65,12 @@ Authority 的后台 job 系统只支持特定 job 类型。ST-BME 不能盲目 > 该端点按批校验 `vectorSpaceId` / `observedDim` 一致性,拒绝混合维度,返回带类型的校验错误。 +BME → Authority 的向量协议约定: + +- 节点身份使用 `externalId` / `nodeId`;顶层 `id` 是 Trivium 内部 numeric id,BME 不发送字符串 node id 到该字段。 +- link 使用 `{ src: { externalId, namespace }, dst: { externalId, namespace }, label, weight }`,由 Authority 在服务端解析成 Trivium 内部 id。 +- search 请求携带 `namespace` / `collectionId` / `chatId`;返回结果若带 namespace,BME 会过滤掉非当前 namespace 的命中,避免多聊天/多集合污染。 + 向量空间身份和维度门禁的算法见 [`../algorithms/vector-and-embedding.md`](../algorithms/vector-and-embedding.md)。 ## Authority SQL 图谱存储选择 diff --git a/tests/authority-vector-primary.mjs b/tests/authority-vector-primary.mjs index 68dc8d8..f430767 100644 --- a/tests/authority-vector-primary.mjs +++ b/tests/authority-vector-primary.mjs @@ -267,6 +267,16 @@ assert.equal(isAuthorityVectorConfig(config), true); const applyCall = triviumClient.calls.find(([name]) => name === "bmeVectorApply")?.[1]; assert.equal(applyCall.items.length, 2); assert.equal(applyCall.links.length, 1); + assert.deepEqual(applyCall.links[0].src, { + externalId: "node-a", + namespace: "st-bme::chat-authority-vector", + }); + assert.deepEqual(applyCall.links[0].dst, { + externalId: "node-b", + namespace: "st-bme::chat-authority-vector", + }); + assert.equal(applyCall.links[0].label, "related"); + assert.equal(applyCall.links[0].weight, 0.75); assert.equal(applyCall.observedDim, 2); assert.equal(String(applyCall.vectorSpaceId || "").startsWith("vs_"), true); assert.equal(applyCall.items.every((item) => item.payload?.vectorSpaceId === applyCall.vectorSpaceId), true); diff --git a/tests/e2e/authority-server-primary.mjs b/tests/e2e/authority-server-primary.mjs index 0669ee5..1c879b8 100644 --- a/tests/e2e/authority-server-primary.mjs +++ b/tests/e2e/authority-server-primary.mjs @@ -158,6 +158,7 @@ await runAuthorityE2eStep("trivium", async () => { headerProvider, }); assert.equal(linkResult.linked, graph.edges.length); + assert.ok(linkResult.linked >= 1, "Authority Trivium link sync should create at least one edge"); const searchResults = await searchAuthorityTriviumNodes(graph, "Authority E2E Alpha", config, { namespace, @@ -169,6 +170,10 @@ await runAuthorityE2eStep("trivium", async () => { headerProvider, }); assert.ok(Array.isArray(searchResults)); + assert.ok( + searchResults.every((result) => typeof result?.nodeId === "string" && result.nodeId.trim()), + "Authority Trivium search results should expose external node ids", + ); const filteredIds = await filterAuthorityTriviumNodes(config, { namespace, @@ -191,6 +196,10 @@ await runAuthorityE2eStep("trivium", async () => { headerProvider, }); assert.ok(Array.isArray(neighborIds)); + assert.ok( + neighborIds.includes(graph.nodes[1].id), + "Authority Trivium neighbors should resolve links through external ids", + ); return { upserted: upsertResult.upserted,