diff --git a/index.js b/index.js index 9139997..ad0f547 100644 --- a/index.js +++ b/index.js @@ -257,6 +257,7 @@ import { writePersistedRecallToUserMessage, } from "./retrieval/recall-persistence.js"; import { resolveConfiguredTimeoutMs } from "./runtime/request-timeout.js"; +import { deriveAuthorityUpgradeState } from "./runtime/authority-upgrade-state.js"; import { createVectorSyncCoalescer as createImportedVectorSyncCoalescer } from "./runtime/vector-sync-coalescer.js"; import { defaultSettings, @@ -1585,6 +1586,36 @@ function getAuthorityRuntimeSnapshot(settings = getSettings()) { function buildAuthorityPersistenceStatePatch(settings = getSettings()) { const { capability, browserState } = getAuthorityRuntimeSnapshot(settings); + const upgradeState = + typeof deriveAuthorityUpgradeState === "function" + ? deriveAuthorityUpgradeState({ + settings, + capability, + browserState, + }) + : { + mode: capability?.serverPrimaryReady + ? "authority-enhanced" + : capability?.installed + ? "authority-degraded" + : "standalone", + text: capability?.serverPrimaryReady + ? "服务端增强已启用" + : capability?.installed + ? "已自动回退" + : "纯前端模式", + meta: capability?.serverPrimaryReady + ? "图谱与向量存储已自动升级到 DOA/Authority 增强路径" + : capability?.installed + ? `服务端增强暂不可用:${String(capability?.reason || capability?.lastError || "unknown")}` + : "未检测到 DOA/Authority,已自动使用本地稳定路径", + level: capability?.serverPrimaryReady + ? "success" + : capability?.installed + ? "warning" + : "idle", + ready: Boolean(capability?.serverPrimaryReady), + }; return { authority: cloneRuntimeDebugValue(capability, null), authorityBrowserState: cloneRuntimeDebugValue(browserState, null), @@ -1601,6 +1632,12 @@ function buildAuthorityPersistenceStatePatch(settings = getSettings()) { authorityDegradedReason: capability.serverPrimaryReady ? "" : String(capability.reason || capability.lastError || ""), + authorityUpgradeState: cloneRuntimeDebugValue(upgradeState, null), + authorityUpgradeMode: String(upgradeState.mode || "standalone"), + authorityUpgradeText: String(upgradeState.text || "纯前端模式"), + authorityUpgradeMeta: String(upgradeState.meta || ""), + authorityUpgradeLevel: String(upgradeState.level || "idle"), + authorityUpgradeReady: Boolean(upgradeState.ready), }; } diff --git a/runtime/authority-upgrade-state.js b/runtime/authority-upgrade-state.js new file mode 100644 index 0000000..476ba3f --- /dev/null +++ b/runtime/authority-upgrade-state.js @@ -0,0 +1,158 @@ +export const AUTHORITY_UPGRADE_MODES = Object.freeze({ + STANDALONE: "standalone", + PROBING: "probing", + SHADOW: "authority-shadow", + CANDIDATE: "authority-candidate", + ENHANCED: "authority-enhanced", + DEGRADED: "authority-degraded", +}); + +function normalizeString(value, fallback = "") { + const text = String(value ?? "").trim(); + return text || fallback; +} + +export function createAuthorityUpgradeState(overrides = {}) { + const mode = normalizeString(overrides.mode, AUTHORITY_UPGRADE_MODES.STANDALONE); + return { + mode, + text: normalizeString(overrides.text, "纯前端模式"), + meta: normalizeString(overrides.meta, "未检测到可用服务端增强,BME 将继续本地运行"), + level: normalizeString(overrides.level, "idle"), + ready: Boolean(overrides.ready), + reason: normalizeString(overrides.reason, "standalone"), + serverPrimaryReady: Boolean(overrides.serverPrimaryReady), + storageReady: Boolean(overrides.storageReady), + vectorReady: Boolean(overrides.vectorReady), + jobsReady: Boolean(overrides.jobsReady), + browserCacheMode: normalizeString(overrides.browserCacheMode, "minimal"), + updatedAt: normalizeString(overrides.updatedAt, new Date().toISOString()), + }; +} + +export function deriveAuthorityUpgradeState({ + settings = {}, + capability = {}, + browserState = {}, + now = Date.now(), +} = {}) { + const enabledMode = normalizeString(settings.authorityEnabled ?? capability.enabledMode, "auto"); + const primaryWhenAvailable = settings.authorityPrimaryWhenAvailable !== false; + const storageReady = Boolean(capability.storagePrimaryReady); + const vectorReady = Boolean(capability.triviumPrimaryReady); + const serverPrimaryReady = Boolean(capability.serverPrimaryReady || storageReady); + const jobsReady = Boolean(capability.jobsReady); + const browserCacheMode = normalizeString(browserState.mode, "minimal"); + const reason = normalizeString(capability.reason || capability.lastError, "standalone"); + const updatedAt = new Date(Number.isFinite(Number(now)) ? Number(now) : Date.now()).toISOString(); + + if (enabledMode === "off" || enabledMode === "false") { + return createAuthorityUpgradeState({ + mode: AUTHORITY_UPGRADE_MODES.STANDALONE, + text: "纯前端模式", + meta: "服务端增强已关闭,BME 将继续本地运行", + level: "idle", + reason: "authority-disabled", + browserCacheMode, + updatedAt, + }); + } + + if (!capability.installed) { + return createAuthorityUpgradeState({ + mode: AUTHORITY_UPGRADE_MODES.STANDALONE, + text: "纯前端模式", + meta: "未检测到 DOA/Authority,已自动使用本地稳定路径", + level: "idle", + reason, + browserCacheMode, + updatedAt, + }); + } + + if (!capability.healthy || !capability.sessionReady || !capability.permissionReady) { + return createAuthorityUpgradeState({ + mode: AUTHORITY_UPGRADE_MODES.DEGRADED, + text: "已自动回退", + meta: `服务端增强暂不可用:${reason}`, + level: "warning", + reason, + browserCacheMode, + updatedAt, + }); + } + + if (!primaryWhenAvailable) { + return createAuthorityUpgradeState({ + mode: AUTHORITY_UPGRADE_MODES.SHADOW, + text: "服务端影子同步", + meta: "DOA/Authority 可用,但当前仍以本地路径为主", + level: "info", + reason: "primary-disabled", + serverPrimaryReady, + storageReady, + vectorReady, + jobsReady, + browserCacheMode, + updatedAt, + }); + } + + if (storageReady && vectorReady) { + return createAuthorityUpgradeState({ + mode: AUTHORITY_UPGRADE_MODES.ENHANCED, + text: "服务端增强已启用", + meta: jobsReady + ? "图谱与向量存储已自动升级到 DOA/Authority 增强路径" + : "图谱与向量存储已增强,服务端后台任务能力暂不可用", + level: "success", + ready: true, + reason: "authority-ready", + serverPrimaryReady, + storageReady, + vectorReady, + jobsReady, + browserCacheMode, + updatedAt, + }); + } + + if (storageReady || vectorReady) { + return createAuthorityUpgradeState({ + mode: AUTHORITY_UPGRADE_MODES.CANDIDATE, + text: "服务端增强准备中", + meta: storageReady + ? "图谱服务端存储可用,向量增强仍在等待能力确认" + : "向量服务端能力可用,图谱服务端存储仍在等待能力确认", + level: "info", + reason: "partial-authority-ready", + serverPrimaryReady, + storageReady, + vectorReady, + jobsReady, + browserCacheMode, + updatedAt, + }); + } + + return createAuthorityUpgradeState({ + mode: AUTHORITY_UPGRADE_MODES.DEGRADED, + text: "已自动回退", + meta: `DOA/Authority 已连接,但关键能力未就绪:${reason}`, + level: "warning", + reason, + serverPrimaryReady, + storageReady, + vectorReady, + jobsReady, + browserCacheMode, + updatedAt, + }); +} + +export function formatAuthorityUpgradeMeta(meta = "", upgradeState = {}) { + const baseMeta = normalizeString(meta, "准备就绪"); + const text = normalizeString(upgradeState?.text, ""); + if (!text) return baseMeta; + return `${baseMeta} · ${text}`; +} diff --git a/tests/authority-upgrade-state.mjs b/tests/authority-upgrade-state.mjs new file mode 100644 index 0000000..2fe91c7 --- /dev/null +++ b/tests/authority-upgrade-state.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; + +import { + createAuthorityUpgradeState, + deriveAuthorityUpgradeState, + formatAuthorityUpgradeMeta, +} from "../runtime/authority-upgrade-state.js"; +import { createGraphPersistenceState } from "../ui/ui-status.js"; + +const initial = createAuthorityUpgradeState(); +assert.equal(initial.mode, "standalone"); +assert.equal(initial.ready, false); + +const graphState = createGraphPersistenceState(); +assert.equal(graphState.authorityUpgradeMode, "standalone"); +assert.equal(graphState.authorityUpgradeReady, false); +assert.equal(graphState.authorityUpgradeState.text, "纯前端模式"); + +const absent = deriveAuthorityUpgradeState({ + settings: { authorityEnabled: "auto" }, + capability: { installed: false, reason: "not-installed" }, + browserState: { mode: "minimal" }, +}); +assert.equal(absent.mode, "standalone"); +assert.equal(absent.text, "纯前端模式"); +assert.equal(absent.ready, false); + +const degraded = deriveAuthorityUpgradeState({ + settings: { authorityEnabled: "auto" }, + capability: { + installed: true, + healthy: false, + sessionReady: false, + permissionReady: false, + reason: "probe-failed", + }, +}); +assert.equal(degraded.mode, "authority-degraded"); +assert.equal(degraded.level, "warning"); +assert.match(degraded.meta, /probe-failed/); + +const enhanced = deriveAuthorityUpgradeState({ + settings: { + authorityEnabled: "auto", + authorityPrimaryWhenAvailable: true, + }, + capability: { + installed: true, + healthy: true, + sessionReady: true, + permissionReady: true, + serverPrimaryReady: true, + storagePrimaryReady: true, + triviumPrimaryReady: true, + jobsReady: true, + }, + browserState: { mode: "off" }, +}); +assert.equal(enhanced.mode, "authority-enhanced"); +assert.equal(enhanced.ready, true); +assert.equal(enhanced.text, "服务端增强已启用"); + +assert.equal( + formatAuthorityUpgradeMeta("准备就绪", enhanced), + "准备就绪 · 服务端增强已启用", +); + +console.log("authority-upgrade-state tests passed"); diff --git a/ui/panel.js b/ui/panel.js index b9ee6b0..0573e98 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -13949,14 +13949,18 @@ function _refreshCloudStorageModeUi(settings = _getSettings?.() || {}) { function _refreshRuntimeStatus() { const runtimeStatus = _getRuntimeStatus?.() || {}; + const graphPersistence = _getGraphPersistenceSnapshot?.() || {}; + const upgradeState = graphPersistence.authorityUpgradeState || {}; const text = runtimeStatus.text || "待命"; const meta = runtimeStatus.meta || "准备就绪"; + const upgradeText = upgradeState.text || graphPersistence.authorityUpgradeText || ""; + const displayMeta = upgradeText ? `${meta} · ${upgradeText}` : meta; _setText("bme-status-text", text); - _setText("bme-status-meta", meta); + _setText("bme-status-meta", displayMeta); _setText("bme-mobile-status-text", text); - _setText("bme-mobile-status-meta", meta); + _setText("bme-mobile-status-meta", displayMeta); _setText("bme-panel-status", text); - _renderCloudStorageModeStatus(_getSettings?.() || {}, _getGraphPersistenceSnapshot()); + _renderCloudStorageModeStatus(_getSettings?.() || {}, graphPersistence); _refreshGraphAvailabilityState(); } @@ -14551,4 +14555,3 @@ function _isMobile() { return window.innerWidth <= 768; } - diff --git a/ui/ui-status.js b/ui/ui-status.js index 7584d59..06fff3d 100644 --- a/ui/ui-status.js +++ b/ui/ui-status.js @@ -3,6 +3,7 @@ // 可被 index.js 及其他模块安全导入。 import { sanitizePlannerMessageText } from "../runtime/planner-tag-utils.js"; import { AUTHORITY_DIAGNOSTICS_MANIFEST_LIMIT } from "../maintenance/authority-diagnostics-bundle.js"; +import { createAuthorityUpgradeState } from "../runtime/authority-upgrade-state.js"; // ═══════════════════════════════════════════════════════════ // 常量 @@ -143,6 +144,12 @@ export function createGraphPersistenceState() { authorityOfflineQueueBytes: 0, authorityOfflineQueueItems: 0, authorityDegradedReason: "", + authorityUpgradeState: createAuthorityUpgradeState(), + authorityUpgradeMode: "standalone", + authorityUpgradeText: "纯前端模式", + authorityUpgradeMeta: "未检测到可用服务端增强,BME 将继续本地运行", + authorityUpgradeLevel: "idle", + authorityUpgradeReady: false, authorityMigrationState: "idle", authorityMigrationSource: "", authorityMigrationRevision: 0,