fix(desktop): 审批条提交后不再永卡「提交中」+ 按 taskId 重挂载
复现:登录恢复待审任务时若捞到一个无法续跑的任务(如早期 KV 冒号 bug 留下的无
resume 记录的孤儿),点批准 → 决定发出但消费者找不到记录 → 任务永远 waiting →
审批条 busy 状态成功后从不复位 → 永卡「提交中…」spinner,看似整个应用卡死。
- decide 成功后置 submitted 并在 finally 复位 busy(此前只在 catch 复位)。
- submitted 时显示「决定已发出,等待续跑…(若长时间无响应,该任务可能已失效)」
而非停在 spinner —— 诚实反映状态,不误导成 hang。
- Bar 加 key={taskId}:换审批任务时重挂载,避免上一个的 submitted 残留。
(孤儿任务本身已在 PG 标记 failed 清理;冒号 bug 早已修复,不再产生新孤儿。)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,14 @@ export function ApprovalBar({ run }: { run: RunState }) {
|
||||
? pendingApproval(run.exec) ?? { node: "", title: run.detail || "人工审批", summary: "" }
|
||||
: null;
|
||||
if (!approval || !run.taskId) return null;
|
||||
return <Bar taskId={run.taskId} node={approval.node} title={approval.title} summary={approval.summary} />;
|
||||
// key=taskId:换任务时重新挂载,重置 submitted 等本地状态,避免上一个审批的「已提交」残留。
|
||||
return <Bar key={run.taskId} taskId={run.taskId} node={approval.node} title={approval.title} summary={approval.summary} />;
|
||||
}
|
||||
|
||||
function Bar({ taskId, node, title, summary }: { taskId: string; node: string; title: string; summary: string }) {
|
||||
const [note, setNote] = useState("");
|
||||
const [busy, setBusy] = useState<"approve" | "reject" | null>(null);
|
||||
const [submitted, setSubmitted] = useState(false); // 决定已发出;正常情况下任务续跑后本条随 waiting 解除自动消失
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const decide = async (approved: boolean) => {
|
||||
@@ -26,8 +28,10 @@ function Bar({ taskId, node, title, summary }: { taskId: string; node: string; t
|
||||
setErr("");
|
||||
try {
|
||||
await approveTask(taskId, approved, { node, note });
|
||||
setSubmitted(true); // 关键:成功后置「已提交」而非停在 spinner —— 否则任务若恢复不了会永远卡「提交中…」
|
||||
} catch (e) {
|
||||
setErr((e as Error).message);
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
@@ -38,25 +42,29 @@ function Bar({ taskId, node, title, summary }: { taskId: string; node: string; t
|
||||
<ShieldCheck className="h-4 w-4 text-amber-400" strokeWidth={2.2} />
|
||||
<span className="font-semibold">人工审批</span>
|
||||
<span className="text-amber-300/80">{title}</span>
|
||||
<Badge tone="warn">等待决定</Badge>
|
||||
<Badge tone="warn">{submitted ? "已提交决定" : "等待决定"}</Badge>
|
||||
</div>
|
||||
{summary && <pre className="max-h-20 overflow-auto whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-amber-100/80">{summary}</pre>}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="备注(可选,拒绝原因等)"
|
||||
className="min-w-0 flex-1 rounded border border-line bg-ink-950/60 px-2 py-1 text-[11px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500/50 focus:outline-none"
|
||||
/>
|
||||
<button onClick={() => decide(true)} disabled={busy !== null}
|
||||
className="flex items-center gap-1 rounded bg-success/20 px-2.5 py-1 text-[11px] font-medium text-success hover:bg-success/30 disabled:opacity-50">
|
||||
<Check className="h-3.5 w-3.5" /> {busy === "approve" ? "提交中…" : "批准"}
|
||||
</button>
|
||||
<button onClick={() => decide(false)} disabled={busy !== null}
|
||||
className="flex items-center gap-1 rounded bg-danger/20 px-2.5 py-1 text-[11px] font-medium text-danger hover:bg-danger/30 disabled:opacity-50">
|
||||
<X className="h-3.5 w-3.5" /> {busy === "reject" ? "提交中…" : "拒绝"}
|
||||
</button>
|
||||
</div>
|
||||
{submitted ? (
|
||||
<p className="text-[11px] text-amber-200/80">决定已发出,等待任务续跑…(若长时间无响应,该任务可能已失效)</p>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="备注(可选,拒绝原因等)"
|
||||
className="min-w-0 flex-1 rounded border border-line bg-ink-950/60 px-2 py-1 text-[11px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500/50 focus:outline-none"
|
||||
/>
|
||||
<button onClick={() => decide(true)} disabled={busy !== null}
|
||||
className="flex items-center gap-1 rounded bg-success/20 px-2.5 py-1 text-[11px] font-medium text-success hover:bg-success/30 disabled:opacity-50">
|
||||
<Check className="h-3.5 w-3.5" /> {busy === "approve" ? "提交中…" : "批准"}
|
||||
</button>
|
||||
<button onClick={() => decide(false)} disabled={busy !== null}
|
||||
className="flex items-center gap-1 rounded bg-danger/20 px-2.5 py-1 text-[11px] font-medium text-danger hover:bg-danger/30 disabled:opacity-50">
|
||||
<X className="h-3.5 w-3.5" /> {busy === "reject" ? "提交中…" : "拒绝"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{err && <p className="text-[11px] text-danger">提交失败:{err}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user