feat(authority): track job status in panel

This commit is contained in:
Youzini-afk
2026-04-28 15:35:02 +08:00
parent 6a583fa1f6
commit cabbe72e23
5 changed files with 527 additions and 104 deletions

311
index.js
View File

@@ -374,6 +374,7 @@ import {
createAuthorityJobAdapter,
normalizeAuthorityJobConfig,
} from "./maintenance/authority-job-adapter.js";
import { trackAuthorityJobUntilTerminal } from "./maintenance/authority-job-tracker.js";
import {
createAuthorityBlobAdapter,
normalizeAuthorityBlobConfig,
@@ -1295,6 +1296,10 @@ let pendingGraphPersistRetryTimer = null;
let pendingGraphPersistRetryChatId = "";
let pendingGraphPersistRetryAttempt = 0;
let pendingAutoExtractionTimer = null;
let authorityJobPollAbortController = null;
let authorityJobPollJobId = "";
let authorityJobPollChatId = "";
let authorityJobPollPromise = null;
let pendingAutoExtraction = {
chatId: "",
messageId: null,
@@ -2180,110 +2185,13 @@ async function readAuthorityLukerCheckpointBlob(chatId = "", options = {}) {
}
}
async function exportAuthorityDiagnosticsBundle({
chatId = "",
reason = "diagnostics-bundle",
signal = undefined,
refreshHost = false,
} = {}) {
const normalizedChatId =
normalizeChatIdCandidate(chatId) ||
normalizeChatIdCandidate(graphPersistenceState.chatId) ||
normalizeChatIdCandidate(getCurrentChatId());
if (!normalizedChatId) {
return {
ok: false,
reason: "missing-chat-id",
};
}
if (!shouldUseAuthorityDiagnosticsBundle()) {
return {
ok: false,
reason: "authority-diagnostics-unavailable",
};
}
const normalizedReason = String(reason || "diagnostics-bundle").trim() || "diagnostics-bundle";
const path = buildAuthorityDiagnosticsBundlePath(normalizedChatId, normalizedReason);
try {
const bundle = buildAuthorityDiagnosticsBundle({
chatId: normalizedChatId,
reason: normalizedReason,
settings: getSettings(),
runtimeStatus: getPanelRuntimeStatus(),
runtimeDebug: getPanelRuntimeDebugSnapshot({ refreshHost }),
graphPersistence: getGraphPersistenceLiveState(),
graph: currentGraph,
lastExtractionStatus,
lastVectorStatus,
lastRecallStatus,
lastBatchStatus: currentGraph?.historyState?.lastBatchStatus || null,
lastInjection: lastInjectionContent,
lastExtract: lastExtractedItems,
lastRecall: lastRecalledItems,
});
const adapter = getAuthorityBlobAdapter();
const result = await writeAuthorityDiagnosticsBundleFile(adapter, bundle, {
chatId: normalizedChatId,
reason: normalizedReason,
path,
signal,
});
const bundleSize = Math.max(
0,
Number(result?.result?.size || 0) || JSON.stringify(bundle).length || 0,
);
recordAuthorityBlobSnapshot({
action: "diagnostics-write",
ok: result?.ok !== false,
backend: "authority-blob",
path: result?.path || path,
reason: normalizedReason,
size: bundleSize,
});
updateGraphPersistenceState({
authorityDiagnosticsBundlePath: String(result?.path || path),
authorityDiagnosticsBundleReason: normalizedReason,
authorityDiagnosticsBundleUpdatedAt: new Date().toISOString(),
authorityDiagnosticsBundleSize: bundleSize,
});
return {
ok: result?.ok !== false,
path: String(result?.path || path),
size: bundleSize,
result,
};
} catch (error) {
const message = error?.message || String(error) || "Authority diagnostics bundle failed";
recordAuthorityBlobSnapshot({
action: "diagnostics-write",
ok: false,
backend: "authority-blob",
path,
reason: normalizedReason,
error: message,
});
updateGraphPersistenceState({
authorityDiagnosticsBundlePath: path,
authorityDiagnosticsBundleReason: normalizedReason,
authorityDiagnosticsBundleUpdatedAt: new Date().toISOString(),
authorityDiagnosticsBundleSize: 0,
});
return {
ok: false,
path,
reason: "authority-diagnostics-bundle-error",
error,
};
}
}
async function readLukerGraphSidecarV2WithAuthorityBlob(context = null, options = {}) {
const sidecar = await readLukerGraphSidecarV2(context, options);
if (sidecar?.checkpoint) return sidecar;
const chatId =
normalizeChatIdCandidate(options.chatId) ||
normalizeChatIdCandidate(sidecar?.manifest?.chatId) ||
normalizeChatIdCandidate(getCurrentChatId(context));
normalizeChatIdCandidate(getCurrentChatId());
const blobResult = await readAuthorityLukerCheckpointBlob(chatId);
if (!blobResult?.exists || !blobResult?.checkpoint) return sidecar;
return {
@@ -2363,6 +2271,7 @@ async function submitAuthorityVectorRebuildJob({
"running",
{ syncRuntime: true },
);
void startTrackingAuthorityJob(job, { kind, chatId });
return {
submitted: true,
fallbackRequired: false,
@@ -2385,11 +2294,217 @@ async function submitAuthorityVectorRebuildJob({
}
}
function createAbortTrackingError(reason = "authority-job-tracking-stopped") {
if (typeof DOMException !== "undefined") {
return new DOMException(reason, "AbortError");
}
return Object.assign(new Error(reason), { name: "AbortError" });
}
function stopTrackingAuthorityJob(reason = "authority-job-tracking-stopped") {
if (authorityJobPollAbortController) {
try {
authorityJobPollAbortController.abort(createAbortTrackingError(reason));
} catch {
}
}
authorityJobPollAbortController = null;
authorityJobPollJobId = "";
authorityJobPollChatId = "";
authorityJobPollPromise = null;
}
function buildAuthorityJobStatusMeta(job = null, fallbackKind = "") {
const normalizedJob =
job && typeof job === "object" && !Array.isArray(job) ? job : {};
const progress = Number(normalizedJob.progress || 0);
return [
String(fallbackKind || normalizedJob.kind || "").trim(),
normalizedJob.id ? `job ${normalizedJob.id}` : "",
String(normalizedJob.status || "").trim(),
Number.isFinite(progress) && progress > 0
? `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`
: "",
String(normalizedJob.error || "").trim(),
]
.filter(Boolean)
.join(" · ");
}
function syncAuthorityVectorJobState(job = null) {
if (!currentGraph?.vectorIndexState) return;
const normalizedJob =
job && typeof job === "object" && !Array.isArray(job) ? job : {};
currentGraph.vectorIndexState.lastRebuildJob =
cloneRuntimeDebugValue(normalizedJob, null);
currentGraph.vectorIndexState.lastAuthorityJobId = String(normalizedJob.id || "");
currentGraph.vectorIndexState.lastAuthorityJobStatus = String(
normalizedJob.status || "",
);
currentGraph.vectorIndexState.lastAuthorityJobProgress = Number(
normalizedJob.progress || 0,
);
if (!normalizedJob.id) {
return;
}
if (normalizedJob.terminal) {
if (normalizedJob.success) {
currentGraph.vectorIndexState.dirty = false;
currentGraph.vectorIndexState.dirtyReason = "";
currentGraph.vectorIndexState.lastWarning = "";
} else {
currentGraph.vectorIndexState.dirty = true;
currentGraph.vectorIndexState.dirtyReason =
String(normalizedJob.status || "failed") || "failed";
currentGraph.vectorIndexState.lastWarning =
String(normalizedJob.error || normalizedJob.status || "Authority Job 失败") ||
"Authority Job 失败";
}
return;
}
currentGraph.vectorIndexState.dirty = true;
currentGraph.vectorIndexState.dirtyReason =
"authority-vector-rebuild-job-running";
currentGraph.vectorIndexState.lastWarning =
buildAuthorityJobStatusMeta(normalizedJob, normalizedJob.kind) ||
"Authority Job 运行中";
}
async function startTrackingAuthorityJob(job = null, options = {}) {
const normalizedJob =
job && typeof job === "object" && !Array.isArray(job) ? job : {};
const jobId = String(normalizedJob.id || "").trim();
const trackedChatId =
normalizeChatIdCandidate(options.chatId) ||
normalizeChatIdCandidate(getCurrentChatId()) ||
normalizeChatIdCandidate(graphPersistenceState.chatId);
if (!jobId || !trackedChatId) {
return null;
}
stopTrackingAuthorityJob("authority-job-replaced");
const controller = new AbortController();
authorityJobPollAbortController = controller;
authorityJobPollJobId = jobId;
authorityJobPollChatId = trackedChatId;
const effectiveKind = String(options.kind || normalizedJob.kind || "").trim();
const jobConfig = normalizeAuthorityJobConfig(getSettings());
const applyTrackedJobUpdate = async (nextJob, state = {}) => {
const normalizedNextJob =
nextJob && typeof nextJob === "object" && !Array.isArray(nextJob) ? nextJob : {};
const queueState = normalizedNextJob.terminal
? normalizedNextJob.success
? "success"
: "failed"
: normalizedNextJob.id
? "running"
: "idle";
recordAuthorityJobSnapshot(normalizedNextJob, {
kind: effectiveKind || normalizedNextJob.kind || "",
queueState,
});
syncAuthorityVectorJobState(normalizedNextJob);
const meta = buildAuthorityJobStatusMeta(
normalizedNextJob,
effectiveKind || normalizedNextJob.kind,
);
if (normalizedNextJob.terminal) {
setLastVectorStatus(
normalizedNextJob.success ? "Authority Job 已完成" : "Authority Job 失败",
meta,
normalizedNextJob.success ? "success" : "error",
{ syncRuntime: true },
);
const activeChatId =
normalizeChatIdCandidate(getCurrentChatId()) ||
normalizeChatIdCandidate(graphPersistenceState.chatId);
if (activeChatId && activeChatId === trackedChatId) {
saveGraphToChat({
reason: normalizedNextJob.success
? "authority-vector-rebuild-job-completed"
: "authority-vector-rebuild-job-failed",
});
}
} else {
setLastVectorStatus(
state.phase === "initial" ? "Authority Job 已提交" : "Authority Job 运行中",
meta,
"running",
{ syncRuntime: true },
);
}
refreshPanelLiveState();
};
authorityJobPollPromise = trackAuthorityJobUntilTerminal({
initialJob: normalizedJob,
pollIntervalMs: jobConfig.pollIntervalMs,
timeoutMs: jobConfig.waitTimeoutMs,
signal: controller.signal,
loadJob: async (targetJobId) => {
const activeChatId =
normalizeChatIdCandidate(getCurrentChatId()) ||
normalizeChatIdCandidate(graphPersistenceState.chatId);
if (activeChatId && activeChatId !== trackedChatId) {
controller.abort(createAbortTrackingError("authority-job-chat-changed"));
throw controller.signal.reason;
}
const adapter = getAuthorityJobAdapter();
return await adapter.get(targetJobId, { signal: controller.signal });
},
onUpdate: applyTrackedJobUpdate,
})
.catch((error) => {
if (isAbortError(error)) {
return null;
}
const message = error?.message || String(error) || "Authority Job 状态轮询失败";
const failedJob = {
...normalizedJob,
id: jobId,
kind: effectiveKind || normalizedJob.kind || "",
status: "error",
terminal: true,
success: false,
error: message,
};
recordAuthorityJobSnapshot(failedJob, {
kind: effectiveKind || normalizedJob.kind || "",
queueState: "error",
});
syncAuthorityVectorJobState(failedJob);
setLastVectorStatus(
"Authority Job 失败",
buildAuthorityJobStatusMeta(failedJob, effectiveKind || normalizedJob.kind),
"error",
{ syncRuntime: true },
);
refreshPanelLiveState();
return failedJob;
})
.finally(() => {
if (authorityJobPollAbortController === controller) {
authorityJobPollAbortController = null;
authorityJobPollJobId = "";
authorityJobPollChatId = "";
authorityJobPollPromise = null;
}
});
return authorityJobPollPromise;
}
async function requeueAuthorityJob(jobId, options = {}) {
try {
const adapter = getAuthorityJobAdapter();
const job = await adapter.requeue(jobId, options);
recordAuthorityJobSnapshot(job, { queueState: "running" });
syncAuthorityVectorJobState(job);
saveGraphToChat({ reason: "authority-vector-rebuild-job-requeued" });
void startTrackingAuthorityJob(job, {
kind: job?.kind || graphPersistenceState.authorityLastJobKind,
chatId: getCurrentChatId(),
});
return { success: true, job };
} catch (error) {
const message = error?.message || String(error) || "Authority Job 重试失败";

View File

@@ -0,0 +1,99 @@
function throwIfAborted(signal) {
if (signal?.aborted) {
throw signal.reason instanceof Error
? signal.reason
: Object.assign(new Error("操作已终止"), { name: "AbortError" });
}
}
function sleep(ms, signal) {
if (!Number.isFinite(Number(ms)) || Number(ms) <= 0) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, Math.max(0, Math.floor(Number(ms))));
if (signal) {
signal.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(
signal.reason instanceof Error
? signal.reason
: Object.assign(new Error("操作已终止"), { name: "AbortError" }),
);
},
{ once: true },
);
}
});
}
export async function trackAuthorityJobUntilTerminal({
initialJob = null,
loadJob,
onUpdate = null,
pollIntervalMs = 1200,
timeoutMs = 0,
signal = undefined,
} = {}) {
if (typeof loadJob !== "function") {
throw new Error("Authority job loader unavailable");
}
const initial =
initialJob && typeof initialJob === "object" && !Array.isArray(initialJob)
? initialJob
: {};
const jobId = String(initial.id || "").trim();
if (!jobId) {
return initial;
}
const startedAt = Date.now();
let latest = { ...initial };
if (typeof onUpdate === "function") {
await onUpdate(latest, {
phase: "initial",
elapsedMs: 0,
});
}
if (latest.terminal) {
return latest;
}
while (true) {
throwIfAborted(signal);
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
latest = {
...latest,
status: "timeout",
terminal: true,
success: false,
error: String(latest?.error || "wait timeout"),
};
if (typeof onUpdate === "function") {
await onUpdate(latest, {
phase: "timeout",
elapsedMs: Date.now() - startedAt,
});
}
return latest;
}
await sleep(pollIntervalMs, signal);
throwIfAborted(signal);
latest = await loadJob(jobId, {
signal,
previousJob: latest,
elapsedMs: Date.now() - startedAt,
});
if (typeof onUpdate === "function") {
await onUpdate(latest, {
phase: latest?.terminal ? "terminal" : "poll",
elapsedMs: Date.now() - startedAt,
});
}
if (latest?.terminal) {
return latest;
}
}
}

View File

@@ -7,6 +7,7 @@ import {
normalizeAuthorityJobList,
normalizeAuthorityJobRecord,
} from "../maintenance/authority-job-adapter.js";
import { trackAuthorityJobUntilTerminal } from "../maintenance/authority-job-tracker.js";
import { onRebuildVectorIndexController } from "../ui/ui-actions-controller.js";
function createMockJobClient() {
@@ -120,6 +121,77 @@ assert.deepEqual(client.calls.map(([name]) => name), [
"requeue",
]);
const trackerPhases = [];
let trackerLoadCount = 0;
const trackedJob = await trackAuthorityJobUntilTerminal({
initialJob: {
id: "job-track",
kind: "authority.vector.rebuild",
status: "queued",
progress: 0,
terminal: false,
success: false,
},
pollIntervalMs: 0,
timeoutMs: 1000,
async loadJob(jobId) {
trackerLoadCount += 1;
if (trackerLoadCount === 1) {
return {
id: jobId,
kind: "authority.vector.rebuild",
status: "running",
progress: 0.4,
terminal: false,
success: false,
};
}
return {
id: jobId,
kind: "authority.vector.rebuild",
status: "completed",
progress: 1,
terminal: true,
success: true,
};
},
async onUpdate(job, state) {
trackerPhases.push([state.phase, job.status, Number(job.progress || 0)]);
},
});
assert.equal(trackedJob.status, "completed");
assert.equal(trackedJob.success, true);
assert.equal(trackerLoadCount, 2);
assert.deepEqual(trackerPhases, [
["initial", "queued", 0],
["poll", "running", 0.4],
["terminal", "completed", 1],
]);
const timedOutJob = await trackAuthorityJobUntilTerminal({
initialJob: {
id: "job-timeout",
status: "running",
progress: 0.2,
terminal: false,
success: false,
},
pollIntervalMs: 5,
timeoutMs: 1,
async loadJob(jobId) {
return {
id: jobId,
status: "running",
progress: 0.3,
terminal: false,
success: false,
};
},
});
assert.equal(timedOutJob.status, "timeout");
assert.equal(timedOutJob.terminal, true);
assert.equal(timedOutJob.success, false);
function createVectorControllerRuntime(overrides = {}) {
const calls = [];
const signal = {};

View File

@@ -265,6 +265,10 @@
<label>最近向量</label>
<div class="bme-recent-meta" id="bme-status-last-vector"></div>
</div>
<div class="bme-config-row">
<label>Authority Job</label>
<div class="bme-recent-meta" id="bme-status-authority-job"></div>
</div>
<div class="bme-config-row">
<label>最近召回</label>
<div class="bme-recent-meta" id="bme-status-last-recall"></div>

View File

@@ -1513,6 +1513,75 @@ function _resolvePipelineStatus(statusObj) {
return { label: text || "IDLE", color, detail: meta };
}
function _buildAuthorityJobUiState(loadInfo = _getGraphPersistenceSnapshot()) {
const snapshot =
loadInfo && typeof loadInfo === "object" && !Array.isArray(loadInfo)
? loadInfo
: {};
const job =
snapshot.authorityLastJob &&
typeof snapshot.authorityLastJob === "object" &&
!Array.isArray(snapshot.authorityLastJob)
? snapshot.authorityLastJob
: {};
const jobId = String(job.id || snapshot.authorityLastJobId || "").trim();
const kind = String(job.kind || snapshot.authorityLastJobKind || "").trim();
const status = String(
job.status || snapshot.authorityLastJobStatus || snapshot.authorityJobQueueState || "",
).trim();
const error = String(job.error || snapshot.authorityLastJobError || "").trim();
const progressRaw = Number(job.progress ?? snapshot.authorityLastJobProgress);
const progress = Number.isFinite(progressRaw)
? Math.max(0, Math.min(1, progressRaw))
: 0;
const queueState = String(snapshot.authorityJobQueueState || "").trim() ||
(error
? "error"
: status === "completed" || status === "succeeded" || status === "success"
? "success"
: status === "failed" || status === "error" || status === "timeout"
? "failed"
: jobId
? "running"
: "idle");
const progressText = progress > 0 ? `${Math.round(progress * 100)}%` : "";
const detail = [
jobId ? `job ${jobId}` : "",
kind,
status || queueState,
progressText,
error,
]
.filter(Boolean)
.join(" · ");
return {
jobId,
kind,
status,
error,
progress,
progressText,
queueState,
label: jobId
? queueState === "success"
? "已完成"
: queueState === "failed" || queueState === "error"
? "失败"
: queueState === "running"
? "运行中"
: "空闲"
: snapshot.authorityJobsReady
? "空闲"
: "未就绪",
detail:
detail ||
(snapshot.authorityJobsReady ? "Authority Jobs ready" : "Authority Jobs unavailable"),
canRequeue: Boolean(
jobId && (queueState === "failed" || queueState === "error"),
),
};
}
function _readPersistenceDiagnosticObject(snapshot = null) {
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
return null;
@@ -1957,10 +2026,19 @@ function _refreshTaskPipelineOverview() {
const graph = _getGraph?.() || {};
const historyState = graph.runtimeState?.historyState || graph.historyState || {};
const loadInfo = _getGraphPersistenceSnapshot();
const authorityJobUi = _buildAuthorityJobUiState(loadInfo);
const extraction = _resolvePipelineStatus(_getLastExtractionStatus?.());
const vector = _resolvePipelineStatus(_getLastVectorStatus?.());
const recall = _resolvePipelineStatus(_getLastRecallStatus?.());
const authorityJobStatus = _resolvePipelineStatus({
text: authorityJobUi.label,
meta: authorityJobUi.detail,
level:
authorityJobUi.queueState === "failed" || authorityJobUi.queueState === "error"
? "error"
: "info",
});
const persistLevel = loadInfo.loadState === "loaded" ? "info" : loadInfo.loadState === "loading" ? "info" : "warn";
const persistenceMetaParts = [`rev ${loadInfo.revision || 0}`];
const pipelineLoadMeta = _formatPipelineLoadDiagnosticsMeta(
@@ -2046,13 +2124,9 @@ function _refreshTaskPipelineOverview() {
{
label: "向量",
color: vector.color,
value:
vector.label +
(vector.detail ? `${vector.detail}` : "") +
(authorityJobParts.length
? ` · Authority ${authorityJobParts.join(" · ")}`
: ""),
value: vector.label + (vector.detail ? `${vector.detail}` : ""),
},
{ label: "Authority Job", color: authorityJobStatus.color, value: authorityJobUi.detail },
{ label: "召回", color: recall.color, value: recall.label + (recall.detail ? `${recall.detail}` : "") },
{ label: "持久化", color: persistence.color, value: persistence.label + (persistence.detail ? `${persistence.detail}` : "") },
];
@@ -2067,10 +2141,31 @@ function _refreshTaskPipelineOverview() {
</div>
</div>`;
const authorityJobProgressColor =
authorityJobUi.queueState === "success"
? "#2ecc71"
: authorityJobUi.queueState === "failed" || authorityJobUi.queueState === "error"
? "#e74c3c"
: authorityJobUi.queueState === "running"
? "#00d4ff"
: "#7f8c8d";
const authorityJobProgressWidth = authorityJobUi.queueState === "success"
? 100
: authorityJobUi.queueState === "running"
? Math.max(8, Math.round(authorityJobUi.progress * 100))
: Math.round(authorityJobUi.progress * 100);
const authorityJobActions = authorityJobUi.canRequeue && typeof _actionHandlers.requeueAuthorityJob === "function"
? `
<div style="margin-top:10px;display:flex;justify-content:flex-end">
<button class="bme-config-secondary-btn" type="button" data-authority-job-action="requeue" data-job-id="${_escHtml(authorityJobUi.jobId)}">重试 Authority Job</button>
</div>`
: "";
el.innerHTML = `
<div class="bme-pipeline-grid">
${pipelineCard("提取 Extraction", extraction, "scissors")}
${pipelineCard("向量 Vector", vector, "share-nodes")}
${pipelineCard("Authority Jobs", authorityJobStatus, "server")}
${pipelineCard("召回 Recall", recall, "magnifying-glass")}
${pipelineCard("持久化 Persistence", persistence, "database")}
</div>
@@ -2091,7 +2186,42 @@ function _refreshTaskPipelineOverview() {
</div>
`).join("")}
</div>
<div class="bme-batch-progress" style="margin-top:12px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<span style="font-size:12px;font-weight:700;color:var(--bme-on-surface)"><i class="fa-solid fa-server" style="margin-right:6px;color:var(--bme-primary)"></i>Authority Job</span>
<span style="font-size:10px;color:var(--bme-on-surface-dim)">${_escHtml(authorityJobUi.label)}</span>
</div>
<div class="bme-config-help" style="margin-bottom:10px">${_escHtml(authorityJobUi.detail)}</div>
<div style="height:8px;border-radius:999px;background:rgba(255,255,255,0.08);overflow:hidden">
<div style="height:100%;width:${authorityJobProgressWidth}%;background:${authorityJobProgressColor};transition:width .2s ease"></div>
</div>
${authorityJobActions}
</div>
`;
el
.querySelector('[data-authority-job-action="requeue"]')
?.addEventListener("click", async (event) => {
const button = event.currentTarget;
const jobId = String(button?.dataset?.jobId || "").trim();
if (!jobId || typeof _actionHandlers.requeueAuthorityJob !== "function") return;
if (button.disabled) return;
button.disabled = true;
try {
toastr.info("Authority Job 重试中…", "ST-BME", { timeOut: 2000 });
const result = await _actionHandlers.requeueAuthorityJob(jobId);
if (result?.success) {
toastr.success(`Authority Job 已重试:${result?.job?.id || jobId}`, "ST-BME");
} else {
toastr.warning(`Authority Job 重试失败:${result?.error || "unknown"}`, "ST-BME");
}
} catch (error) {
toastr.error(`Authority Job 重试失败: ${error?.message || error}`, "ST-BME");
} finally {
_refreshDashboard();
_refreshTaskMonitor();
}
});
}
// ---------- Task Timeline ----------
@@ -3672,6 +3802,7 @@ function _refreshDashboard() {
_setText("bme-status-last-extract", "等待聊天图谱元数据加载");
_setText("bme-status-last-persist", "等待聊天图谱元数据加载");
_setText("bme-status-last-vector", "等待聊天图谱元数据加载");
_setText("bme-status-authority-job", "等待聊天图谱元数据加载");
_setText("bme-status-last-recall", "等待聊天图谱元数据加载");
_refreshPersistenceRepairUi(loadInfo, null);
_renderStatefulListPlaceholder(
@@ -3708,6 +3839,7 @@ function _refreshDashboard() {
const lastBatchStatus = _getLatestBatchStatusSnapshot();
const vectorStatus = _getLastVectorStatus?.() || {};
const recallStatus = _getLastRecallStatus?.() || {};
const authorityJobUi = _buildAuthorityJobUiState(loadInfo);
const historyPrefix =
loadInfo.loadState === "shadow-restored"
? "临时恢复 · "
@@ -3751,6 +3883,7 @@ function _refreshDashboard() {
);
_refreshPersistenceRepairUi(loadInfo, lastBatchStatus);
_setText("bme-status-last-vector", vectorStatus.meta || "尚未执行向量任务");
_setText("bme-status-authority-job", authorityJobUi.detail || authorityJobUi.label);
_setText("bme-status-last-recall", recallStatus.meta || "尚未执行召回");
_refreshCognitionDashboard(graph);