feat: 任务详情 LLM 进度与失败原因标识(列表失败原因摘要)

This commit is contained in:
host committed 2026-10-30 14:38:23 +08:00
1 parent b3f4597d3f
commit 70ad8d7d09
3 files changed
+54 -12

No files matched your search

@@ -23,6 +23,29 @@ import { CoordPanel, AgentsPanel, ApprovalPanel, PeerPanel, ThreadPanel } from "
// 只是没被挂进页签)。
// Data, actions and sub-sections are unchanged; only grouping
// and visibility changed.
// LLM failure summary: the raw error_text is usually a codex JSON
// blob (e.g. a 503 from the relay). The overview must say, at a
// glance, whether the model ever ran and why it stopped — previously
// the only copy lived in the "操作与结果" <pre>, so a failed task
// looked like "failed" with no reason. This is a display mapping
// only; it never claims a connection state the server does not
// report (there is no "LLM connected" field — running progress
// below is elapsed time + last activity, not a live socket state).
function errorSummary(err: string): string {
if (/503|Service Unavailable/.test(err)) return "上游模型服务暂时不可用(503):模型一轮都没跑出来,不是任务逻辑失败;等上游恢复或换模型重试";
if (/429|rate limit/i.test(err)) return "上游限流(429):模型拒绝了本次调用,可稍后重试";
if (/timeout|timed out/i.test(err)) return "上游调用超时:模型响应未在限时内返回";
if (/401|403|Invalid API key|unauthorized/i.test(err)) return "上游鉴权失败:网关 key 或权限有问题";
if (/ECONNREFUSED|ENOTFOUND|network|fetch failed/i.test(err)) return "连不上上游模型服务:网络或网关地址有问题";
return "任务失败(原因见原始错误文本)";
}
function fmtAge(sec: number | null): string {
if (sec === null || sec < 0) return "未知";
if (sec < 60) return Math.floor(sec) + " 秒前";
if (sec < 3600) return Math.floor(sec / 60) + " 分钟前";
return (sec / 3600).toFixed(1) + " 小时前";
}
type DetailTab = "overview" | "collab" | "review" | "threads" | "browser";
const DETAIL_TABS: Array<[DetailTab, string]> = [
["overview", "概览"], ["collab", "协作"], ["review", "评审与审批"], ["threads", "线程"], ["browser", "浏览器"],
@@ -82,6 +105,22 @@ export default function TaskDetailPanel() {
</div>
</div>
</Card>
{(t.status === "running" || t.status === "queued") ? (() => {
const now = Date.now() / 1000;
const last = t.last_heartbeat || t.updated_at || null;
const idle = last ? now - last : null;
return (<Card title="LLM 进度">
<div>状态 {t.status === "running" ? "进行中" : "排队中"} · 已用 {dur}{idle !== null ? " · 上次活动 " + fmtAge(idle) : ""}</div>
{idle !== null && idle > 180
? <div className="muted">超过 3 分钟无活动:可能在等上游模型响应(上游 503 时 worker 会重试后失败,失败后这里会显示失败原因;无活动不等于已断连,服务端没有“LLM 已连接”字段,不作此断言)</div>
: <div className="muted">首轮无输出时属正常:模型第一轮回复到达前 trace 里没有消息;回复到达后会在会话区出现,成本里的 LLM 请求数随之增加</div>}
</Card>);
})() : null}
{t.error_text ? (<Card title="失败原因">
<div>{errorSummary(t.error_text)}</div>
<pre style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{t.error_text.slice(0, 1000)}</pre>
<div className="muted">完整错误文本仍在下方“操作与结果”里;模型 {t.model || "默认"} · executor {t.executor || ""}</div>
</Card>) : null}
<Card title="基本信息">
<dl className="detail-kv">
<div><dt>任务</dt><dd>{t.id}</dd></div>
@@ -72,6 +72,9 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) {
{" · "}<span title={durTitle}>耗时 {dur}</span>
{" · "}<span title={tokTitle}>{tok}</span>
</div>
{(t.status === "failed" || t.status === "interrupted" || t.status === "merge-failed") && t.error_text ? (
<div className="issue-meta" title={t.error_text}>{/503|Service Unavailable/.test(t.error_text) ? "上游 503:模型服务暂时不可用" : t.error_text.replace(/\s+/g, " ").slice(0, 60)}</div>
) : null}
</span>
</li>
);