feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1

Merged
Blizzard merged 181 commits from feat/wails3 into main 2026-07-17 01:12:32 +00:00
Showing only changes of commit e8dc2e02c3 - Show all commits
@@ -13,12 +13,14 @@ export function ApprovalBar({ run }: { run: RunState }) {
? pendingApproval(run.exec) ?? { node: "", title: run.detail || "人工审批", summary: "" } ? pendingApproval(run.exec) ?? { node: "", title: run.detail || "人工审批", summary: "" }
: null; : null;
if (!approval || !run.taskId) return 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 }) { function Bar({ taskId, node, title, summary }: { taskId: string; node: string; title: string; summary: string }) {
const [note, setNote] = useState(""); const [note, setNote] = useState("");
const [busy, setBusy] = useState<"approve" | "reject" | null>(null); const [busy, setBusy] = useState<"approve" | "reject" | null>(null);
const [submitted, setSubmitted] = useState(false); // 决定已发出;正常情况下任务续跑后本条随 waiting 解除自动消失
const [err, setErr] = useState(""); const [err, setErr] = useState("");
const decide = async (approved: boolean) => { const decide = async (approved: boolean) => {
@@ -26,8 +28,10 @@ function Bar({ taskId, node, title, summary }: { taskId: string; node: string; t
setErr(""); setErr("");
try { try {
await approveTask(taskId, approved, { node, note }); await approveTask(taskId, approved, { node, note });
setSubmitted(true); // 关键:成功后置「已提交」而非停在 spinner —— 否则任务若恢复不了会永远卡「提交中…」
} catch (e) { } catch (e) {
setErr((e as Error).message); setErr((e as Error).message);
} finally {
setBusy(null); 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} /> <ShieldCheck className="h-4 w-4 text-amber-400" strokeWidth={2.2} />
<span className="font-semibold"></span> <span className="font-semibold"></span>
<span className="text-amber-300/80">{title}</span> <span className="text-amber-300/80">{title}</span>
<Badge tone="warn"></Badge> <Badge tone="warn">{submitted ? "已提交决定" : "等待决定"}</Badge>
</div> </div>
{summary && <pre className="max-h-20 overflow-auto whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-amber-100/80">{summary}</pre>} {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"> {submitted ? (
<input <p className="text-[11px] text-amber-200/80"></p>
value={note} ) : (
onChange={(e) => setNote(e.target.value)} <div className="flex items-center gap-2">
placeholder="备注(可选,拒绝原因等)" <input
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" value={note}
/> onChange={(e) => setNote(e.target.value)}
<button onClick={() => decide(true)} disabled={busy !== null} placeholder="备注(可选,拒绝原因等)"
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"> 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"
<Check className="h-3.5 w-3.5" /> {busy === "approve" ? "提交中…" : "批准"} />
</button> <button onClick={() => decide(true)} disabled={busy !== null}
<button onClick={() => decide(false)} 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">
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"> <Check className="h-3.5 w-3.5" /> {busy === "approve" ? "提交中…" : "批准"}
<X className="h-3.5 w-3.5" /> {busy === "reject" ? "提交中…" : "拒绝"} </button>
</button> <button onClick={() => decide(false)} disabled={busy !== null}
</div> 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>} {err && <p className="text-[11px] text-danger">{err}</p>}
</div> </div>
); );