diff --git a/index.js b/index.js index 62ad3eb..4de1b72 100644 --- a/index.js +++ b/index.js @@ -7086,8 +7086,12 @@ async function resolveAuthorityCapabilityForStoreSelection(settings = getSetting function buildAuthorityGraphStoreOptions(settings = getSettings()) { const normalizedSettings = normalizeAuthoritySettings(settings); + const capability = normalizeAuthorityCapabilityState(authorityCapabilityState, settings); return { baseUrl: normalizedSettings.baseUrl, + bmeVectorManifestReady: Boolean(capability.bmeVectorManifestReady), + bmeVectorApplyReady: Boolean(capability.bmeVectorApplyReady), + bmeProtocolVersion: Math.max(0, Number(capability.bmeProtocolVersion) || 0), headerProvider: typeof getRequestHeaders === "function" ? () => getRequestHeaders() : null, }; diff --git a/tests/authority-bme-capabilities.mjs b/tests/authority-bme-capabilities.mjs index 7cb676c..7f9ede6 100644 --- a/tests/authority-bme-capabilities.mjs +++ b/tests/authority-bme-capabilities.mjs @@ -15,7 +15,7 @@ const capability = normalizeAuthorityProbeResponse({ bme: { protocolVersion: 1, vectorManifest: true, - vectorApply: false, + vectorApply: true, vectorApplyJobs: false, serverEmbeddingProbe: false, candidateSearch: false, @@ -25,7 +25,7 @@ const capability = normalizeAuthorityProbeResponse({ assert.equal(capability.bmeProtocolVersion, 1); assert.equal(capability.bmeVectorManifestReady, true); -assert.equal(capability.bmeVectorApplyReady, false); +assert.equal(capability.bmeVectorApplyReady, true); assert.equal(capability.bmeServerEmbeddingProbeReady, false); assert.ok(capability.features.includes("bme.vectormanifest")); assert.ok(capability.features.includes("bme.protocolversion")); diff --git a/tests/authority-vector-primary.mjs b/tests/authority-vector-primary.mjs index ae56de9..46c70d6 100644 --- a/tests/authority-vector-primary.mjs +++ b/tests/authority-vector-primary.mjs @@ -70,7 +70,7 @@ function createAuthorityVectorGraph() { return { graph, first, second }; } -function createMockTriviumClient({ failBulkUpsert = false, failSearch = false } = {}) { +function createMockTriviumClient({ failBulkUpsert = false, failSearch = false, failBmeVectorApply = false } = {}) { const calls = []; return { calls, @@ -146,6 +146,23 @@ function createMockTriviumClient({ failBulkUpsert = false, failSearch = false } calls.push(["stat", payload]); return { ok: true }; }, + async bmeVectorApply(payload) { + calls.push(["bmeVectorApply", payload]); + if (failBmeVectorApply) { + throw new AuthorityHttpError("bme apply missing", { + status: 404, + category: "validation", + path: "/bme/vector-apply", + }); + } + return { + ok: true, + database: payload.database || "st_bme_vectors", + manifest: { database: payload.database || "st_bme_vectors", exists: true }, + upsert: { successCount: payload.items?.length || 0, failureCount: 0 }, + links: { successCount: payload.links?.length || 0, failureCount: 0 }, + }; + }, }; } @@ -209,6 +226,46 @@ assert.equal(isAuthorityVectorConfig(config), true); assert.equal(result.timings.authorityDiagnostics.link.totalItems, 1); } +{ + const { graph } = createAuthorityVectorGraph(); + const triviumClient = createMockTriviumClient(); + const applyConfig = { ...config, bmeVectorApplyReady: true }; + const result = await syncGraphVectorIndexFromIndex(graph, applyConfig, { + chatId: "chat-authority-vector", + purge: true, + triviumClient, + }); + + assert.equal(result.stats.indexed, 2); + assert.equal(graph.vectorIndexState.dirty, false); + assert.equal(triviumClient.calls.filter(([name]) => name === "bmeVectorApply").length, 1); + assert.equal(triviumClient.calls.some(([name]) => name === "purge"), false); + assert.equal(triviumClient.calls.some(([name]) => name === "bulkUpsert"), false); + const applyCall = triviumClient.calls.find(([name]) => name === "bmeVectorApply")?.[1]; + assert.equal(applyCall.items.length, 2); + assert.equal(applyCall.links.length, 1); + assert.equal(applyCall.items.every((item) => Array.isArray(item.vector) && item.vector.length > 0), true); + assert.equal(result.timings.authorityDiagnostics.upsert.operation, "bmeVectorApply"); +} + +{ + const { graph } = createAuthorityVectorGraph(); + const triviumClient = createMockTriviumClient({ failBmeVectorApply: true }); + const applyConfig = { ...config, bmeVectorApplyReady: true }; + const result = await syncGraphVectorIndexFromIndex(graph, applyConfig, { + chatId: "chat-authority-vector", + purge: true, + triviumClient, + }); + + assert.equal(result.stats.indexed, 2); + assert.equal(graph.vectorIndexState.dirty, false); + assert.equal(triviumClient.calls.filter(([name]) => name === "bmeVectorApply").length, 1); + assert.equal(triviumClient.calls.filter(([name]) => name === "purge").length, 1); + assert.ok(triviumClient.calls.some(([name]) => name === "bulkUpsert")); + assert.ok(triviumClient.calls.some(([name]) => name === "linkMany")); +} + { const { graph, first, second } = createAuthorityVectorGraph(); const triviumClient = createMockTriviumClient(); diff --git a/vector/authority-vector-primary-adapter.js b/vector/authority-vector-primary-adapter.js index ade3bde..a8c6bf2 100644 --- a/vector/authority-vector-primary-adapter.js +++ b/vector/authority-vector-primary-adapter.js @@ -397,6 +397,9 @@ export function normalizeAuthorityVectorConfig(settings = {}, overrides = {}) { ), timeoutMs: Math.max(0, Number(source.timeoutMs || 0) || 0), failOpen: source.authorityVectorFailOpen !== false && source.failOpen !== false, + bmeVectorApplyReady: Boolean(source.bmeVectorApplyReady ?? source.authorityBmeVectorApplyReady), + bmeVectorManifestReady: Boolean(source.bmeVectorManifestReady ?? source.authorityBmeVectorManifestReady), + bmeProtocolVersion: Math.max(0, Number(source.bmeProtocolVersion ?? source.authorityBmeProtocolVersion) || 0), ...overrides, }; } @@ -676,6 +679,10 @@ export class AuthorityTriviumHttpClient { ...(payload.includeMappingIntegrity ? { includeMappingIntegrity: true } : {}), }); } + + async bmeVectorApply(payload = {}) { + return await this.requestV06("/bme/vector-apply", payload); + } } export function createAuthorityTriviumClient(config = {}, options = {}) { @@ -864,6 +871,79 @@ export async function upsertAuthorityTriviumEntries(graph, config = {}, entries }; } +export async function applyAuthorityBmeVectorManifest(graph, config = {}, entries = [], options = {}) { + const items = buildAuthorityVectorItems(graph, entries, options); + const links = buildAuthorityLinkItems(graph, options).map((link) => ({ + src: buildNodeReference(link.fromId, options.namespace), + dst: buildNodeReference(link.toId, options.namespace), + label: link.relation, + weight: link.weight, + })); + const missingVector = items.find((item) => !normalizeVector(item?.vector || item?.embedding).length); + if (missingVector) { + throw new Error("BME vector apply requires vector for every item"); + } + throwIfAborted(options.signal); + const client = createAuthorityTriviumClient(config, options); + const startedAt = nowMs(); + const estimatedBytes = estimateJsonBytes({ items, links }); + const result = await callClient( + client, + ["bmeVectorApply", "applyBmeVectorManifest", "vectorApply"], + "bmeVectorApply", + { + ...buildOpenOptions(config, options), + database: config.database, + namespace: options.namespace, + collectionId: options.collectionId, + chatId: options.chatId, + graphRevision: Math.max(0, Math.floor(Number(options.revision) || 0)), + modelScope: String(options.modelScope || ""), + embeddingMode: config.embeddingMode || "client", + items, + links, + idempotencyKey: [ + options.chatId || "chat", + options.collectionId || options.namespace || "collection", + options.revision || 0, + options.modelScope || "model", + items.length, + links.length, + ].join(":"), + }, + ); + const upserted = Number(result?.upsert?.successCount ?? result?.upserted ?? items.length) || 0; + const linked = Number(result?.links?.successCount ?? result?.linked ?? links.length) || 0; + const ok = result?.ok !== false && Number(result?.upsert?.failureCount || 0) === 0 && Number(result?.links?.failureCount || 0) === 0; + if (!ok) { + const error = new Error("BME vector apply returned failures"); + error.authorityDiagnostics = { + operation: "bmeVectorApply", + totalItems: items.length, + linkItems: links.length, + upsertFailures: Number(result?.upsert?.failureCount || 0), + linkFailures: Number(result?.links?.failureCount || 0), + result, + }; + throw error; + } + return { + ...result, + upserted, + linked, + diagnostics: { + operation: "bmeVectorApply", + totalItems: items.length, + linkItems: links.length, + upserted, + linked, + estimatedBytes, + manifest: result?.manifest || null, + totalMs: roundMs(nowMs() - startedAt), + }, + }; +} + export async function syncAuthorityTriviumLinks(graph, config = {}, options = {}) { const links = buildAuthorityLinkItems(graph, options); if (!links.length) { diff --git a/vector/vector-index.js b/vector/vector-index.js index dcd6d14..d9e7510 100644 --- a/vector/vector-index.js +++ b/vector/vector-index.js @@ -9,6 +9,7 @@ import { buildVectorCollectionId, stableHashString } from "../runtime/runtime-st import { AUTHORITY_VECTOR_MODE, AUTHORITY_VECTOR_SOURCE, + applyAuthorityBmeVectorManifest, deleteAuthorityTriviumNodes, isAuthorityVectorConfig, normalizeAuthorityVectorConfig, @@ -840,23 +841,50 @@ export async function syncGraphVectorIndex( if (embeddingResult.failures > 0) { throw createEmbeddingProviderError(embeddingResult.failures); } - const purgeStartedAt = nowMs(); - const purgeResult = await purgeAuthorityTriviumNamespace(config, authorityOptions); - authorityPurgeMs += nowMs() - purgeStartedAt; - authorityPurgeDiagnostics = purgeResult?.diagnostics || null; - if (purgeResult?.truncated) { - throw new Error(`Authority Trivium purge truncated after ${purgeResult.pages || 0} page(s)`); + let appliedViaBme = false; + if (config.bmeVectorApplyReady === true) { + try { + const applyStartedAt = nowMs(); + const applyResult = await applyAuthorityBmeVectorManifest( + graph, + config, + desiredEntries, + authorityOptions, + ); + authorityUpsertMs += nowMs() - applyStartedAt; + authorityUpsertDiagnostics = applyResult?.diagnostics || null; + authorityLinkDiagnostics = { + operation: "bmeVectorApply:links", + totalItems: Number(applyResult?.diagnostics?.linkItems || 0), + linked: Number(applyResult?.diagnostics?.linked || 0), + totalMs: 0, + }; + resetVectorMappings(graph, config, effectiveChatId); + appliedViaBme = true; + } catch (applyError) { + if (isAbortError(applyError)) throw applyError; + console.warn("[ST-BME] BME 服务端向量 apply 失败,回退 Authority Trivium 旧路径:", applyError); + } + } + if (!appliedViaBme) { + const purgeStartedAt = nowMs(); + const purgeResult = await purgeAuthorityTriviumNamespace(config, authorityOptions); + authorityPurgeMs += nowMs() - purgeStartedAt; + authorityPurgeDiagnostics = purgeResult?.diagnostics || null; + if (purgeResult?.truncated) { + throw new Error(`Authority Trivium purge truncated after ${purgeResult.pages || 0} page(s)`); + } + resetVectorMappings(graph, config, effectiveChatId); + const upsertStartedAt = nowMs(); + const upsertResult = await upsertAuthorityTriviumEntries( + graph, + config, + desiredEntries, + authorityOptions, + ); + authorityUpsertMs += nowMs() - upsertStartedAt; + authorityUpsertDiagnostics = upsertResult?.diagnostics || null; } - resetVectorMappings(graph, config, effectiveChatId); - const upsertStartedAt = nowMs(); - const upsertResult = await upsertAuthorityTriviumEntries( - graph, - config, - desiredEntries, - authorityOptions, - ); - authorityUpsertMs += nowMs() - upsertStartedAt; - authorityUpsertDiagnostics = upsertResult?.diagnostics || null; for (const entry of desiredEntries) { state.hashToNodeId[entry.hash] = entry.nodeId; state.nodeToHash[entry.nodeId] = entry.hash; @@ -905,19 +933,45 @@ export async function syncGraphVectorIndex( throw createEmbeddingProviderError(embeddingResult.failures); } deletedNodeCount = nodeIdsToDelete.length; - const deleteStartedAt = nowMs(); - const deleteResult = await deleteAuthorityTriviumNodes(config, nodeIdsToDelete, authorityOptions); - authorityDeleteMs += nowMs() - deleteStartedAt; - authorityDeleteDiagnostics = deleteResult?.diagnostics || null; - const upsertStartedAt = nowMs(); - const upsertResult = await upsertAuthorityTriviumEntries( - graph, - config, - entriesToUpsert, - authorityOptions, - ); - authorityUpsertMs += nowMs() - upsertStartedAt; - authorityUpsertDiagnostics = upsertResult?.diagnostics || null; + let appliedViaBme = false; + if (config.bmeVectorApplyReady === true && nodeIdsToDelete.length === 0) { + try { + const applyStartedAt = nowMs(); + const applyResult = await applyAuthorityBmeVectorManifest( + graph, + config, + entriesToUpsert, + authorityOptions, + ); + authorityUpsertMs += nowMs() - applyStartedAt; + authorityUpsertDiagnostics = applyResult?.diagnostics || null; + authorityLinkDiagnostics = { + operation: "bmeVectorApply:links", + totalItems: Number(applyResult?.diagnostics?.linkItems || 0), + linked: Number(applyResult?.diagnostics?.linked || 0), + totalMs: 0, + }; + appliedViaBme = true; + } catch (applyError) { + if (isAbortError(applyError)) throw applyError; + console.warn("[ST-BME] BME 服务端向量 apply 失败,回退 Authority Trivium 旧路径:", applyError); + } + } + if (!appliedViaBme) { + const deleteStartedAt = nowMs(); + const deleteResult = await deleteAuthorityTriviumNodes(config, nodeIdsToDelete, authorityOptions); + authorityDeleteMs += nowMs() - deleteStartedAt; + authorityDeleteDiagnostics = deleteResult?.diagnostics || null; + const upsertStartedAt = nowMs(); + const upsertResult = await upsertAuthorityTriviumEntries( + graph, + config, + entriesToUpsert, + authorityOptions, + ); + authorityUpsertMs += nowMs() - upsertStartedAt; + authorityUpsertDiagnostics = upsertResult?.diagnostics || null; + } for (const entry of entriesToUpsert) { state.hashToNodeId[entry.hash] = entry.nodeId; @@ -926,10 +980,12 @@ export async function syncGraphVectorIndex( } } - const linkStartedAt = nowMs(); - const linkResult = await syncAuthorityTriviumLinks(graph, config, authorityOptions); - authorityLinkMs += nowMs() - linkStartedAt; - authorityLinkDiagnostics = linkResult?.diagnostics || null; + if (!authorityLinkDiagnostics || authorityLinkDiagnostics.operation !== "bmeVectorApply:links") { + const linkStartedAt = nowMs(); + const linkResult = await syncAuthorityTriviumLinks(graph, config, authorityOptions); + authorityLinkMs += nowMs() - linkStartedAt; + authorityLinkDiagnostics = linkResult?.diagnostics || null; + } for (const node of graph.nodes || []) { if (Array.isArray(node.embedding) && node.embedding.length > 0) {