fix(canvas): 任务取消 - 向异步 Provider 传播取消并完成费用对账 (#142)

Fixes #142
This commit is contained in:
ddCat
2026-08-06 10:42:39 +08:00
parent 97e6cd6002
commit 670595a3aa
13 changed files with 634 additions and 89 deletions

View File

@@ -190,7 +190,11 @@ export default function TasksPage() {
setStatusFilter("active");
setPage(1);
}
message.success(action === "retry" ? "任务已重新入队" : "任务已取消");
if (action === "retry") message.success("任务已重新入队");
else if (next.providerCancelStatus === "requested") message.info("已请求上游取消,正在确认费用状态");
else if (next.providerCancelStatus === "confirmed") message.success("上游已确认取消,积分已退回");
else if (next.providerCancelStatus === "uncertain") message.warning("任务已取消,上游费用待核对");
else message.success("任务已取消,积分已退回");
} catch (error) {
message.error(error instanceof Error ? error.message : "操作失败");
} finally {
@@ -452,6 +456,8 @@ export default function TasksPage() {
<InfoItem label="模型" value={formatModelName(effectiveConfig, detailTask)} />
<InfoItem label="尝试次数" value={`第 ${detailTask.attempts || 1} 次`} />
<InfoItem label="创建时间" value={formatDate(detailTask.createdAt)} />
{detailTask.providerCancelStatus ? <InfoItem label="上游取消" value={providerCancelStatusLabel(detailTask)} /> : null}
{detailTask.providerCancelRequestedAt ? <InfoItem label="请求取消时间" value={formatDate(detailTask.providerCancelRequestedAt)} /> : null}
</div>
{canQueryProviderTask(detailTask) ? <div className="flex justify-end"><Button icon={<RefreshCw className="size-4" />} loading={actingId === detailTask.id} onClick={() => void queryProviderTask(detailTask)}>手动查询任务</Button></div> : null}
{detailTask.error ? <pre className="max-h-28 overflow-auto whitespace-pre-wrap border-l-2 border-red-500 bg-red-50 px-3 py-2 text-xs text-red-700 dark:bg-red-950/30 dark:text-red-300">{generationErrorMessage(detailTask.error)}</pre> : null}
@@ -573,12 +579,23 @@ function getTaskCanvasContext(task: GenerationTask, canvasById: Map<string, { ti
}
function taskAttentionReason(task: GenerationTask) {
if (task.status === "cancelled") return "任务已取消,可按原输入重新提交";
if (task.status === "cancelled") return providerCancelStatusLabel(task);
if (task.errorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(task.error)) return "内容审核未通过,请修改输入后新建任务";
if (task.error) return generationErrorMessage(task.error);
return task.stage || "生成失败,打开详情查看原因";
}
function providerCancelStatusLabel(task: GenerationTask) {
if (task.providerCancelStatus === "requested") return "已请求上游取消,正在等待确认";
if (task.providerCancelStatus === "confirmed") return "上游已确认取消,积分已退回";
if (task.providerCancelStatus === "uncertain") {
if (task.billing?.status === "settled") return "上游未能取消,费用已结算";
if (task.billing?.status === "refunded") return "上游取消结果未确认,积分已退回";
return task.providerCancelError || "上游无法确认取消,费用待核对";
}
return task.billing?.status === "refunded" ? "任务在调用上游前取消,积分已退回" : "任务已取消,可按原输入重新提交";
}
function taskEmptyState(status: TaskStatusFilter) {
if (status === "all") return { title: "还没有任务", description: "新提交的生成会在这里显示状态和实时进度。" };
if (status === "active") return { title: "没有运行中的任务", description: "新提交的生成会在这里显示排队状态和实时进度。" };

View File

@@ -4,6 +4,7 @@ import { generationErrorMessage } from "@/lib/generation-error";
export type TaskStatus = "queued" | "running" | "succeeded" | "failed" | "cancelled";
export type TaskBillingStatus = "reserved" | "running" | "settled" | "refunded" | "uncertain";
export type ProviderCancelStatus = "requested" | "confirmed" | "uncertain";
export type AgentSessionStatus = "active" | "completed" | "failed";
export type BackendEnvelope<T> = {
@@ -25,6 +26,11 @@ export type GenerationTask = {
provider?: string;
model?: string;
providerRequestId?: string;
providerCancelStatus?: ProviderCancelStatus;
providerCancelError?: string;
providerCancelAttempts?: number;
providerCancelRequestedAt?: string;
providerCancelledAt?: string;
errorCode?: string;
previewUrl?: string;
previewKind?: "image" | "video";