feat(web+store): needs_attention 任务显示在审核区,附失败原因

- pendingApprovals 扩展到包含 needs_attention,subquery 取最近 executor 失败 error
- Task 类型增加 lastRunError 字段,mappers 映射
- 审核区(renderGates)将 needs_attention 渲染为独立橙色卡片,
  展示「需人工确认」标签 + 具体失败原因 + 重新排队/取消按钮
- i18n zh/en 补充 attnLabel/attnReason/attnRequeue/attnCancel/toastRequeued

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-14 14:46:21 +08:00
parent 4a2b4e1094
commit 433b2dc232
5 changed files with 59 additions and 6 deletions
+1
View File
@@ -76,6 +76,7 @@ export interface Task {
assignee: 'agent' | 'human' | null;
retryBaseline: number; // 上次手动重投时已有的失败 run 数(重置重试计数用)
nextEligibleAt: string | null; // 持久化退避:早于此时间不被领取(重试退避,重启不丢);null=即刻可领
lastRunError?: string | null; // needs_attention 时:最近一次失败 run 的错误信息(供审核区展示)
createdAt: string;
updatedAt: string;
}
+2
View File
@@ -20,6 +20,7 @@ export interface TaskRow {
next_eligible_at: string | null;
created_at: string; updated_at: string;
source_ref: string | null;
last_run_error?: string | null; // pendingApprovals 扩展字段(subquery
}
export interface ApprovalRow {
id: string; task_id: string; gate: string; action: string;
@@ -75,6 +76,7 @@ export function rowToTask(r: TaskRow, approvals: ApprovalRecord[] = []): Task {
approvals, result: r.result ? parseResult(r.result) : null,
assignee: r.assignee as Task['assignee'], retryBaseline: r.retry_baseline ?? 0,
nextEligibleAt: r.next_eligible_at ?? null,
lastRunError: r.last_run_error ?? null,
createdAt: r.created_at, updatedAt: r.updated_at,
};
}
+5 -2
View File
@@ -825,9 +825,12 @@ export class Store {
/** 列出所有处于审批闸状态(plan_review/spec_review/exec_review)的任务。 */
pendingApprovals(projectId?: string): Task[] {
// 包含 needs_attention:让审核区展示失败任务供人工确认/重排
// last_run_error 由子查询取最近一条 failed executor run 的 error
const sub = `(SELECT error FROM runs WHERE task_id = t.id AND status = 'failed' AND kind = 'executor' ORDER BY ended_at DESC LIMIT 1)`;
const sql = projectId
? `SELECT * FROM tasks WHERE project_id = ? AND status IN ('plan_review','spec_review','exec_review') ORDER BY updated_at`
: `SELECT * FROM tasks WHERE status IN ('plan_review','spec_review','exec_review') ORDER BY updated_at`;
? `SELECT t.*, ${sub} AS last_run_error FROM tasks t WHERE t.project_id = ? AND t.status IN ('plan_review','spec_review','exec_review','needs_attention') ORDER BY t.updated_at`
: `SELECT t.*, ${sub} AS last_run_error FROM tasks t WHERE t.status IN ('plan_review','spec_review','exec_review','needs_attention') ORDER BY t.updated_at`;
const rows = (projectId
? this.db.prepare(sql).all(projectId)
: this.db.prepare(sql).all()) as TaskRow[];