diff --git a/cloud/console-react/src/sections/TaskDetailPanel.tsx b/cloud/console-react/src/sections/TaskDetailPanel.tsx
index 45b9f03..487ccf3 100644
--- a/cloud/console-react/src/sections/TaskDetailPanel.tsx
+++ b/cloud/console-react/src/sections/TaskDetailPanel.tsx
@@ -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 "操作与结果"
, 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() {
+ {(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 (
+ 状态 {t.status === "running" ? "进行中" : "排队中"} · 已用 {dur}{idle !== null ? " · 上次活动 " + fmtAge(idle) : ""}
+ {idle !== null && idle > 180
+ ? 超过 3 分钟无活动:可能在等上游模型响应(上游 503 时 worker 会重试后失败,失败后这里会显示失败原因;无活动不等于已断连,服务端没有“LLM 已连接”字段,不作此断言)
+ : 首轮无输出时属正常:模型第一轮回复到达前 trace 里没有消息;回复到达后会在会话区出现,成本里的 LLM 请求数随之增加
}
+ );
+ })() : null}
+ {t.error_text ? (
+ {errorSummary(t.error_text)}
+ {t.error_text.slice(0, 1000)}
+ 完整错误文本仍在下方“操作与结果”里;模型 {t.model || "默认"} · executor {t.executor || ""}
+ ) : null}
- 任务
- {t.id}
diff --git a/cloud/console-react/src/sections/TaskListSection.tsx b/cloud/console-react/src/sections/TaskListSection.tsx
index fecb93d..d6aca91 100644
--- a/cloud/console-react/src/sections/TaskListSection.tsx
+++ b/cloud/console-react/src/sections/TaskListSection.tsx
@@ -72,6 +72,9 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) {
{" · "}耗时 {dur}
{" · "}{tok}
+ {(t.status === "failed" || t.status === "interrupted" || t.status === "merge-failed") && t.error_text ? (
+ {/503|Service Unavailable/.test(t.error_text) ? "上游 503:模型服务暂时不可用" : t.error_text.replace(/\s+/g, " ").slice(0, 60)}
+ ) : null}
);
diff --git a/cloud/console.html b/cloud/console.html
index 4afe0e5..8331ec9 100644
--- a/cloud/console.html
+++ b/cloud/console.html
@@ -20,7 +20,7 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
- */var Uh;function By(){if(Uh)return ht;Uh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),h=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),S=Symbol.for("react.profiler"),N=Symbol.for("react.consumer"),A=Symbol.for("react.context"),p=Symbol.for("react.forward_ref"),H=Symbol.for("react.suspense"),O=Symbol.for("react.memo"),Y=Symbol.for("react.lazy"),j=Symbol.for("react.activity"),k=Symbol.for("react.view_transition"),K=Symbol.iterator;function L(y){return y===null||typeof y!="object"?null:(y=K&&y[K]||y["@@iterator"],typeof y=="function"?y:null)}var at={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},X=Object.assign,vt={};function bt(y,D,ut){this.props=y,this.context=D,this.refs=vt,this.updater=ut||at}bt.prototype.isReactComponent={},bt.prototype.setState=function(y,D){if(typeof y!="object"&&typeof y!="function"&&y!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,y,D,"setState")},bt.prototype.forceUpdate=function(y){this.updater.enqueueForceUpdate(this,y,"forceUpdate")};function zt(){}zt.prototype=bt.prototype;function J(y,D,ut){this.props=y,this.context=D,this.refs=vt,this.updater=ut||at}var Rt=J.prototype=new zt;Rt.constructor=J,X(Rt,bt.prototype),Rt.isPureReactComponent=!0;var it=Array.isArray;function Q(){}var rt={H:null,A:null,T:null,S:null},Ut=Object.prototype.hasOwnProperty;function Qt(y,D,ut){var nt=ut.ref;return{$$typeof:i,type:y,key:D,ref:nt!==void 0?nt:null,props:ut}}function Nt(y,D){return Qt(y.type,D,y.props)}function kt(y){return typeof y=="object"&&y!==null&&y.$$typeof===i}function B(y){var D={"=":"=0",":":"=2"};return"$"+y.replace(/[=:]/g,function(ut){return D[ut]})}var ot=/\/+/g;function st(y,D){return typeof y=="object"&&y!==null&&y.key!=null?B(""+y.key):D.toString(36)}function q(y){switch(y.status){case"fulfilled":return y.value;case"rejected":throw y.reason;default:switch(typeof y.status=="string"?y.then(Q,Q):(y.status="pending",y.then(function(D){y.status==="pending"&&(y.status="fulfilled",y.value=D)},function(D){y.status==="pending"&&(y.status="rejected",y.reason=D)})),y.status){case"fulfilled":return y.value;case"rejected":throw y.reason}}throw y}function C(y,D,ut,nt,Tt){var xt=typeof y;(xt==="undefined"||xt==="boolean")&&(y=null);var Ot=!1;if(y===null)Ot=!0;else switch(xt){case"bigint":case"string":case"number":Ot=!0;break;case"object":switch(y.$$typeof){case i:case r:Ot=!0;break;case Y:return Ot=y._init,C(Ot(y._payload),D,ut,nt,Tt)}}if(Ot)return Tt=Tt(y),Ot=nt===""?"."+st(y,0):nt,it(Tt)?(ut="",Ot!=null&&(ut=Ot.replace(ot,"$&/")+"/"),C(Tt,D,ut,"",function(ce){return ce})):Tt!=null&&(kt(Tt)&&(Tt=Nt(Tt,ut+(Tt.key==null||y&&y.key===Tt.key?"":(""+Tt.key).replace(ot,"$&/")+"/")+Ot)),D.push(Tt)),1;Ot=0;var $=nt===""?".":nt+":";if(it(y))for(var dt=0;dt>>1,yt=q[gt];if(0>>1;gtS(Ce,lt))yS(D,Ce)?(q[gt]=D,q[y]=lt,gt=y):(q[gt]=Ce,q[we]=lt,gt=we);else if(yS(D,lt))q[gt]=D,q[y]=lt,gt=y;else break t}}return C}function S(q,C){var lt=q.sortIndex-C.sortIndex;return lt!==0?lt:q.id-C.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var N=performance;i.unstable_now=function(){return N.now()}}else{var A=Date,p=A.now();i.unstable_now=function(){return A.now()-p}}var H=[],O=[],Y=1,j=null,k=3,K=!1,L=!1,at=!1,X=!1,vt=typeof setTimeout=="function"?setTimeout:null,bt=typeof clearTimeout=="function"?clearTimeout:null,zt=typeof setImmediate<"u"?setImmediate:null;function J(q){for(var C=h(O);C!==null;){if(C.callback===null)f(O);else if(C.startTime<=q)f(O),C.sortIndex=C.expirationTime,r(H,C);else break;C=h(O)}}function Rt(q){if(at=!1,J(q),!L)if(h(H)!==null)L=!0,it||(it=!0,kt());else{var C=h(O);C!==null&&st(Rt,C.startTime-q)}}var it=!1,Q=-1,rt=5,Ut=-1;function Qt(){return X?!0:!(i.unstable_now()-Ut