import { useCallback, useEffect, useState } from "react"; import { Activity, X } from "lucide-react"; import { adminApi } from "../../api"; import { Pager } from "../../components/pager"; import { IconKitSvg } from "../../components/IconKitSvg"; import { SystemLoading } from "../../components/loading"; import type { AdminTask, AdminTaskDetail } from "../../types"; import { pts } from "../stage-config"; type Notify = (type: "success" | "error" | "info", text: string) => void; const PAGE_SIZE = 10; const TABS = [ { key: "", label: "全部" }, { key: "failed", label: "失败" }, { key: "succeeded", label: "成功" } ]; function fmtDate(iso: string) { if (!iso) return "—"; const d = new Date(iso); const p = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } function statusPill(status: string) { if (status === "succeeded") return 成功; if (status === "failed") return 失败; if (["cancelled", "compensating"].includes(status)) return {status}; return 进行中; } function attemptKind(attempt: AdminTaskDetail["attempts"][number]) { if (attempt.is_fallback) return "Fallback"; if (attempt.is_retry) return "原模型重试"; return "首次调用"; } export function AdminTasksPage({ notify }: { notify: Notify }) { const [tasks, setTasks] = useState([]); const [count, setCount] = useState(0); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const [tab, setTab] = useState(""); const [anomalyOnly, setAnomalyOnly] = useState(false); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [busy, setBusy] = useState(false); const load = useCallback(async () => { setLoading(true); try { const res = await adminApi.tasks({ status: tab || undefined, anomaly: anomalyOnly ? "1" : undefined, page, page_size: PAGE_SIZE }); if (res.results.length === 0 && res.count > 0 && page > 1) { setPage((p) => Math.max(1, p - 1)); return; } setTasks(res.results); setCount(res.count); } catch { notify("error", "加载任务失败"); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [tab, anomalyOnly, page]); useEffect(() => { void load(); }, [load]); async function openDetail(id: string) { setDetailLoading(true); setDetail(null); try { setDetail(await adminApi.taskDetail(id)); } catch { notify("error", "加载详情失败"); } finally { setDetailLoading(false); } } async function retry(t: AdminTask) { if (busy) return; setBusy(true); try { await adminApi.retryTask(t.id); notify("success", "已重投,稍后刷新查看"); await load(); } catch (e) { notify("error", e instanceof Error ? e.message : "重投失败"); } finally { setBusy(false); } } // 手动回收僵尸任务:卡在「已预留」超 10 分钟(worker 从未认领)→ 标失败 + 退还冻结积分。 // 自动回收只在同团队下次提交时触发,团队弃用该功能时积分会永久冻结,这是超管兜底出口。 async function reap(t: AdminTask) { if (busy) return; setBusy(true); try { await adminApi.reapTask(t.id); notify("success", "已回收:任务标记失败,预留积分已退还"); await load(); } catch (e) { notify("error", e instanceof Error ? e.message : "回收失败"); } finally { setBusy(false); } } return ( <>

任务监控

// {count} 个任务 · 全局 AI 任务 · 成本异常 · 失败重投
{TABS.map((t) => ( ))}
{loading ? ( ) : tasks.length === 0 ? (

暂无任务

// no tasks

) : (
{tasks.map((t) => ( ))}
类型团队模型状态成本(预估→实际)时间操作
{t.task_type} {t.team_name || —} {t.model_name || —} {statusPill(t.status)} {pts(t.estimated_cost)} → {pts(t.actual_cost)} 积分{t.margin_yuan != null ? ` · 毛利 ¥${t.margin_yuan}` : ""} {t.cost_anomaly && 异常} {fmtDate(t.created_at)} {t.status === "failed" && ( )} {t.reapable && ( )}
)} {(detail || detailLoading) && (
{ if (e.target === e.currentTarget) setDetail(null); }}>

任务详情

{detailLoading || !detail ? ( ) : ( <> {(() => { const attempts = detail.attempts || []; const finalAttempt = [...attempts].reverse().find((attempt) => attempt.status === "succeeded"); const fallbackUsed = attempts.some((attempt) => attempt.is_fallback); return (
状态{statusPill(detail.status)}
团队{detail.team_name || "—"}
模型{detail.model_name || "—"}
计价{pts(detail.estimated_cost)} → {pts(detail.actual_cost)} 积分{detail.cost_anomaly ? " ⚠" : ""}
平台成本{Number(detail.base_cost || 0) > 0 ? `¥${detail.base_cost} · 毛利 ${detail.margin_yuan != null ? `¥${detail.margin_yuan}` : "—"}` : "未知"}
模型调用{attempts.length ? `${attempts.length} 次 · ${fallbackUsed ? "发生 Fallback" : "未切换"}` : "旧任务 · 无尝试链"}
{finalAttempt &&
最终成功模型{finalAttempt.provider_display_name || finalAttempt.provider_name} / {finalAttempt.model_display_name || finalAttempt.model_name}
}
); })()} {detail.attempts?.length > 0 && ( <>
模型调用链 · {detail.attempts.length} 次
{detail.attempts.map((attempt) => (
// {String(attempt.sequence).padStart(2, "0")} {statusPill(attempt.status)} {attemptKind(attempt)}
{attempt.provider_display_name || attempt.provider_name} / {attempt.model_display_name || attempt.model_name}
{attempt.operation} {attempt.duration_ms == null ? "耗时 —" : `耗时 ${attempt.duration_ms} ms`} {Number(attempt.platform_cost || 0) > 0 ? `平台成本 ¥${attempt.platform_cost}` : "平台成本未知"} {attempt.provider_task_id && Provider ID {attempt.provider_task_id}}
{(attempt.error_type || attempt.raw_error) && (
[{attempt.error_type || "unknown"}{attempt.provider_error_code ? ` · ${attempt.provider_error_code}` : ""}] {attempt.raw_error || attempt.safe_error_summary}
)}
))}
)} {detail.error_message && ( <>
错误
{detail.error_message}
)}
请求 payload
{JSON.stringify(detail.request_payload, null, 2)}
响应 payload
{JSON.stringify(detail.response_payload, null, 2)}
)}
)} ); }