feat: add dispatch retry and import recovery flows

This commit is contained in:
kris
2026-03-31 22:10:03 +08:00
parent be31503d22
commit dcbff3cc7d
15 changed files with 776 additions and 23 deletions

View File

@@ -819,9 +819,11 @@ type PendingDispatchPlanState = {
export function ChatComposer({
projectId,
initialPendingDispatchPlan,
initialRejectedDispatchPlan,
}: {
projectId: string;
initialPendingDispatchPlan?: PendingDispatchPlanState | null;
initialRejectedDispatchPlan?: PendingDispatchPlanState | null;
}) {
const router = useRouter();
const [value, setValue] = useState("");
@@ -830,12 +832,16 @@ export function ChatComposer({
const [loading, setLoading] = useState(false);
const [localPendingDispatchPlan, setLocalPendingDispatchPlan] =
useState<PendingDispatchPlanState | null>(null);
const [localRejectedDispatchPlan, setLocalRejectedDispatchPlan] =
useState<PendingDispatchPlanState | null>(null);
const [dismissedPendingPlanId, setDismissedPendingPlanId] = useState<string | null>(null);
const pendingDispatchPlan =
localPendingDispatchPlan ??
(initialPendingDispatchPlan && initialPendingDispatchPlan.planId !== dismissedPendingPlanId
? initialPendingDispatchPlan
: null);
const rejectedDispatchPlan =
pendingDispatchPlan ? null : localRejectedDispatchPlan ?? initialRejectedDispatchPlan ?? null;
async function confirmDispatchPlan() {
if (!pendingDispatchPlan) return;
@@ -863,12 +869,102 @@ export function ChatComposer({
}
const executionCount = result.executions?.length ?? extractApprovedTargetProjectIds(pendingDispatchPlan).length;
setLocalPendingDispatchPlan(null);
setLocalRejectedDispatchPlan(null);
setDismissedPendingPlanId(pendingDispatchPlan.planId);
setMessageTone("success");
setMessage(`已确认下发到 ${executionCount} 个线程。`);
router.refresh();
}
async function retryDispatchPlan() {
if (!rejectedDispatchPlan) return;
setLoading(true);
const response = await fetch(
`/api/v1/projects/${projectId}/dispatch-plans/${rejectedDispatchPlan.planId}/retry`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
},
);
const result = (await response.json()) as {
ok: boolean;
dispatchPlan?: {
planId: string;
summary?: string;
targets?: Array<{ projectId: string; threadDisplayName: string }>;
} | null;
collaborationGate?: {
requiresMasterAgentApproval?: boolean;
};
message?: string;
};
setLoading(false);
if (!result.ok) {
setMessageTone("error");
setMessage(result.message ?? "重新生成推荐失败,请稍后重试。");
return;
}
setLocalRejectedDispatchPlan(null);
setLocalPendingDispatchPlan(
result.dispatchPlan
? {
planId: result.dispatchPlan.planId,
summary: result.dispatchPlan.summary,
targets: result.dispatchPlan.targets ?? [],
}
: null,
);
setDismissedPendingPlanId(null);
setMessageTone("success");
setMessage(
result.collaborationGate?.requiresMasterAgentApproval
? "主 Agent 已重新生成推荐,等待你确认下发。"
: "主 Agent 已重新生成推荐。",
);
router.refresh();
}
async function rejectDispatchPlan() {
if (!pendingDispatchPlan) return;
setLoading(true);
const response = await fetch(
`/api/v1/projects/${projectId}/dispatch-plans/${pendingDispatchPlan.planId}/reject`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
},
);
const result = (await response.json()) as {
ok: boolean;
plan?: {
planId: string;
summary?: string;
targets?: Array<{ projectId: string; threadDisplayName: string }>;
} | null;
message?: string;
};
setLoading(false);
if (!result.ok) {
setMessageTone("error");
setMessage(result.message ?? "拒绝失败,请稍后重试。");
return;
}
setLocalPendingDispatchPlan(null);
setLocalRejectedDispatchPlan(
result.plan
? {
planId: result.plan.planId,
summary: result.plan.summary,
targets: result.plan.targets ?? pendingDispatchPlan.targets,
}
: pendingDispatchPlan,
);
setDismissedPendingPlanId(pendingDispatchPlan.planId);
setMessageTone("success");
setMessage("已拒绝主 Agent 推荐。");
router.refresh();
}
async function send(kind: "text" | "voice_intent" | "image_intent" | "video_intent") {
setLoading(true);
const response = await fetch(`/api/v1/projects/${projectId}/messages`, {
@@ -907,6 +1003,7 @@ export function ChatComposer({
summary: result.dispatchPlan.summary,
targets: result.dispatchPlan.targets ?? [],
});
setLocalRejectedDispatchPlan(null);
setDismissedPendingPlanId(null);
setMessage(
result.collaborationGate?.requiresMasterAgentApproval
@@ -997,6 +1094,14 @@ export function ChatComposer({
>
</button>
<button
type="button"
disabled={loading}
onClick={() => void rejectDispatchPlan()}
className="rounded-full border border-[#F0B5B5] px-4 py-2 text-[13px] font-semibold text-[#CF1322]"
>
</button>
<button
type="button"
disabled={loading}
@@ -1011,6 +1116,23 @@ export function ChatComposer({
</div>
</div>
) : null}
{rejectedDispatchPlan ? (
<div className="mt-3 rounded-2xl border border-[#F3D19C] bg-[#FFF7E6] px-4 py-4 text-[12px] leading-6 text-[#8D5D00]">
<div className="text-[14px] font-semibold text-[#111111]"></div>
<div className="mt-2 whitespace-pre-line">{summarizeDispatchPlan(rejectedDispatchPlan)}</div>
<div className="mt-2"></div>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
disabled={loading}
onClick={() => void retryDispatchPlan()}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:bg-[#B7E6C9]"
>
</button>
</div>
</div>
) : null}
</div>
);
}