feat: add master agent task recovery endpoint
This commit is contained in:
@@ -234,6 +234,94 @@ function riskAggregateValue(risks: Array<{ kind: string; title: string }>, match
|
||||
return risks.filter(matcher).length;
|
||||
}
|
||||
|
||||
function minutesSince(value?: string): number | null {
|
||||
if (!value) return null;
|
||||
const timestamp = Date.parse(value);
|
||||
if (!Number.isFinite(timestamp)) return null;
|
||||
return Math.max(0, Math.floor((Date.now() - timestamp) / 60_000));
|
||||
}
|
||||
|
||||
function buildDataSafetySummary(backupStatus: BossStateBackupStatus) {
|
||||
const ageMinutes = minutesSince(backupStatus.lastBackupAt);
|
||||
const healthLabel =
|
||||
backupStatus.status === "error"
|
||||
? "备份异常"
|
||||
: backupStatus.status === "empty" || !backupStatus.lastBackupAt
|
||||
? "暂无备份"
|
||||
: ageMinutes !== null && ageMinutes > 24 * 60
|
||||
? "备份过期"
|
||||
: "备份正常";
|
||||
const nextAction =
|
||||
healthLabel === "备份异常"
|
||||
? "检查备份目录与状态文件写入权限"
|
||||
: healthLabel === "暂无备份"
|
||||
? "立即创建首个状态快照"
|
||||
: healthLabel === "备份过期"
|
||||
? "补创建状态快照并检查自动备份任务"
|
||||
: "保持自动快照并定期演练恢复";
|
||||
|
||||
return {
|
||||
mode: backupStatus.mode,
|
||||
status: backupStatus.status,
|
||||
restorePointCount: backupStatus.restorePointCount,
|
||||
lastBackupAt: backupStatus.lastBackupAt ?? "",
|
||||
ageMinutes,
|
||||
healthLabel,
|
||||
rpoLabel: "文件 MVP:以最近成功快照为准",
|
||||
rtoLabel: "文件 MVP:人工恢复目标 30-60 分钟",
|
||||
nextAction,
|
||||
};
|
||||
}
|
||||
|
||||
function canRecoverActiveTask(task: BossState["masterAgentTasks"][number]) {
|
||||
if (task.recoverable) return true;
|
||||
if (task.phase === "recoverable_failed") return true;
|
||||
if (task.phase === "turn_started" || task.phase === "awaiting_reply" || task.phase === "completing") {
|
||||
return false;
|
||||
}
|
||||
const maxAttempts = task.maxAttempts ?? 1;
|
||||
return (task.attemptCount ?? 0) < maxAttempts;
|
||||
}
|
||||
|
||||
function buildTaskRiskSummary(state: BossState) {
|
||||
const activeStatuses = new Set(["queued", "running", "needs_user_action"]);
|
||||
const activeTasks = state.masterAgentTasks.filter((task) => activeStatuses.has(task.status));
|
||||
const rows = activeTasks
|
||||
.map((task) => {
|
||||
const activeAt = task.lastProgressAt ?? task.claimedAt ?? task.requestedAt;
|
||||
const ageMinutes = minutesSince(activeAt);
|
||||
const stale = ageMinutes !== null && ageMinutes > 10;
|
||||
const recoverable = canRecoverActiveTask(task);
|
||||
return {
|
||||
taskId: task.taskId,
|
||||
projectId: task.projectId,
|
||||
deviceId: task.deviceId,
|
||||
status: task.status,
|
||||
phase: task.phase ?? task.status,
|
||||
stale,
|
||||
recoverable,
|
||||
lastProgressAt: task.lastProgressAt ?? "",
|
||||
summary: task.requestText || task.errorMessage || task.taskType,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => Number(right.stale) - Number(left.stale) || right.taskId.localeCompare(left.taskId))
|
||||
.slice(0, 20);
|
||||
|
||||
return {
|
||||
counts: {
|
||||
active: activeTasks.length,
|
||||
stale: activeTasks.filter((task) => {
|
||||
const activeAt = task.lastProgressAt ?? task.claimedAt ?? task.requestedAt;
|
||||
const ageMinutes = minutesSince(activeAt);
|
||||
return ageMinutes !== null && ageMinutes > 10;
|
||||
}).length,
|
||||
recoverable: activeTasks.filter(canRecoverActiveTask).length,
|
||||
needsUserAction: activeTasks.filter((task) => task.status === "needs_user_action" || task.phase === "needs_user_action").length,
|
||||
},
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
function buildBackofficeInsights(state: BossState, options: { surface: BackofficeSurface; backupStatus: BossStateBackupStatus }) {
|
||||
const overview = buildAdminOverview(state);
|
||||
const devices = state.devices;
|
||||
@@ -344,6 +432,8 @@ function buildBackofficeInsights(state: BossState, options: { surface: Backoffic
|
||||
backupDir: options.backupStatus.backupDir,
|
||||
detail: options.backupStatus.detail,
|
||||
},
|
||||
dataSafetySummary: buildDataSafetySummary(options.backupStatus),
|
||||
taskRiskSummary: buildTaskRiskSummary(state),
|
||||
capabilitySummary: {
|
||||
guiReady,
|
||||
cliReady,
|
||||
|
||||
Reference in New Issue
Block a user