feat: add master agent task recovery endpoint

This commit is contained in:
AI Bot
2026-06-06 19:05:42 +08:00
parent 755e30612c
commit 643da5b738
6 changed files with 945 additions and 168 deletions

View File

@@ -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,