mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
feat(authority): track job status in panel
This commit is contained in:
311
index.js
311
index.js
@@ -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 重试失败";
|
||||
|
||||
99
maintenance/authority-job-tracker.js
Normal file
99
maintenance/authority-job-tracker.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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>
|
||||
|
||||
145
ui/panel.js
145
ui/panel.js
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user