trace: 标注被截断的链路消息,避免误读为模型未写完
2026-10-29 实弹:468 字 final_answer 在链路里只剩前 300 字、 断在 fu 后,控制台照原样当助手回复展示,用户误以为模型 没写完。截断本身是设计(防链路文件写爆),缺的是标记。 - host.py _summarize_trace_item:agentMessage 超 300 字时 条目带 truncated=True,短消息不带,完整回复仍在任务 result_text 里,不在链路条目里。 - 控制台:TraceEntry 加 truncated 字段,mapTrace 与 traceLineText 在被截断的消息后标注“链路摘要已截断至 300 字,完整回复见右侧任务结果”,console.html 为构建产物同步。 - 单测:tests/test_host_trace.py 新增截断标记用例(468 字 截 300 字且带标记、短消息不带标记)。 验证:host 10 项、cloud test_task_trace 14 项全过; React tsc+vite 构建通过,产物 322151 字节、含截断标注、 含 #150 的 total_tokens/首字/总耗时,无回退。
This commit is contained in:
1 parent
fd9fe5f254
commit
ce85043a24
6 files changed
+36
-7
No files matched your search
@@ -117,7 +117,7 @@ export function traceLineText(e: TraceEntry): string {
|
||||
let body = "";
|
||||
if (e.kind === "exec") body = (e.command || "") + "(exit=" + (e.exitCode ?? "") + "," + (e.durationMs ?? "") + "ms,cwd=" + (e.cwd || "") + ")";
|
||||
else if (e.kind === "file") body = (e.path || "") + " " + (e.change || "") + " " + (e.status || "");
|
||||
else if (e.kind === "message") body = (e.phase ? "[" + e.phase + "] " : "") + (e.text || "");
|
||||
else if (e.kind === "message") body = (e.phase ? "[" + e.phase + "] " : "") + (e.text || "") + (e.truncated ? "(链路摘要已截断至 300 字,完整回复见右侧任务结果)" : "");
|
||||
else if (e.kind === "reasoning") body = e.summary || "";
|
||||
else if (e.kind === "tool") body = (e.server || "") + "/" + (e.tool || "") + " " + (e.status || "");
|
||||
else if (e.kind === "call") body = e.name || "";
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface Automation { id: string; name?: string; status?: string; due?:
|
||||
export interface Member { user_id: string; username?: string; role?: string }
|
||||
export interface SkillItem { name?: string; id?: string; description?: string; detail?: string }
|
||||
export interface McpServer { name?: string; id?: string; status?: string; detail?: string; details?: string; state?: string }
|
||||
export interface TraceEntry { id?: string; kind?: string; command?: string; exitCode?: number | string; durationMs?: number | string; cwd?: string; path?: string; change?: string; status?: string; phase?: string; text?: string; summary?: string; server?: string; tool?: string; name?: string; prompt?: string }
|
||||
export interface TraceEntry { id?: string; kind?: string; command?: string; exitCode?: number | string; durationMs?: number | string; cwd?: string; path?: string; change?: string; status?: string; phase?: string; text?: string; summary?: string; server?: string; tool?: string; name?: string; prompt?: string; truncated?: boolean }
|
||||
export interface Agent { kind?: string; name?: string; status?: string; prompt?: string; updated_ts?: number; calls?: TraceEntry[] }
|
||||
export interface ThreadInfo { id?: string; name?: string; thread_name?: string; archived?: boolean | null; cwd?: string; turns?: number; turn_count?: number; turns_count?: number }
|
||||
export interface Triage {
|
||||
|
||||
@@ -13,7 +13,12 @@ export interface ChatMessage { id: string; role: ChatRole; text: string; label?:
|
||||
export function traceEntryToChatMessage(e: TraceEntry, i: number): ChatMessage {
|
||||
const id = e.id || "trace-" + i;
|
||||
if (e.kind === "message") {
|
||||
return { id, role: "assistant", text: (e.phase ? "[" + e.phase + "] " : "") + (e.text || ""), label: "助手" };
|
||||
// 链路消息是 host 侧截断后的摘要(消息截 300 字),不是模型
|
||||
// 的完整回复:被截断时必须标注,否则半个词结尾会被误读
|
||||
// 成模型没写完(2026-10-29 实弹:468 字 final_answer 断在 fu)。
|
||||
// 完整回复在任务的 result_text(右侧详情),不在链路条目里。
|
||||
const text = (e.phase ? "[" + e.phase + "] " : "") + (e.text || "");
|
||||
return { id, role: "assistant", text: e.truncated ? text + "\n(链路摘要已截断至 300 字,完整回复见右侧任务结果)" : text, label: "助手" };
|
||||
}
|
||||
if (e.kind === "exec" || e.kind === "file" || e.kind === "tool" || e.kind === "call") {
|
||||
const label = e.kind === "exec" ? "命令" : e.kind === "file" ? "文件" : e.kind === "tool" ? "工具" : "调用";
|
||||
|
||||
+3
-2
@@ -52,7 +52,7 @@
|
||||
`+d[a].replace(" at new "," at ");return t.displayName&&E.includes("<anonymous>")&&(E=E.replace("<anonymous>",t.displayName)),E}while(1<=a&&0<=n);break}}}finally{sl=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?ue(l):""}function da(t,e){switch(t.tag){case 26:case 27:case 5:return ue(t.type);case 16:return ue("Lazy");case 13:return t.child!==e&&e!==null?ue("Suspense Fallback"):ue("Suspense");case 19:return ue("SuspenseList");case 0:case 15:return Jl(t.type,!1);case 11:return Jl(t.type.render,!1);case 1:return Jl(t.type,!0);case 31:return ue("Activity");case 30:return ue("ViewTransition");default:return""}}function Dl(t){try{var e="",l=null;do e+=da(t,l),l=t,t=t.return;while(t);return e}catch(a){return`
|
||||
Error generating stack: `+a.message+`
|
||||
`+a.stack}}var Zt=Object.prototype.hasOwnProperty,Pe=i.unstable_scheduleCallback,qe=i.unstable_cancelCallback,Tl=i.unstable_shouldYield,fl=i.unstable_requestPaint,Gt=i.unstable_now,Ul=i.unstable_getCurrentPriorityLevel,mn=i.unstable_ImmediatePriority,Qa=i.unstable_UserBlockingPriority,ge=i.unstable_NormalPriority,Xa=i.unstable_LowPriority,ma=i.unstable_IdlePriority,su=i.log,fu=i.unstable_setDisableYieldValue,Fl=null,Ee=null;function rl(t){if(typeof su=="function"&&fu(t),Ee&&typeof Ee.setStrictMode=="function")try{Ee.setStrictMode(Fl,t)}catch{}}var Ne=Math.clz32?Math.clz32:du,ru=Math.log,ou=Math.LN2;function du(t){return t>>>=0,t===0?32:31-(ru(t)/ou|0)|0}var H=256,D=262144,X=4194304;function K(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&-t;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function O(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,c=t.pingedLanes;t=t.warmLanes;var f=a&134217727;return f!==0?(a=f&~u,a!==0?n=K(a):(c&=f,c!==0?n=K(c):l||(l=f&~t,l!==0&&(n=K(l))))):(f=a&~u,f!==0?n=K(f):c!==0?n=K(c):l||(l=a&~t,l!==0&&(n=K(l)))),n===0?0:e!==0&&e!==n&&(e&u)===0&&(u=n&-n,l=e&-e,u>=l||u===32&&(l&4194048)!==0)?e:n}function k(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function J(t,e){(e&8)!==0&&(e|=e&32);var l=t.entangledLanes;if(l!==0)for(t=t.entanglements,l&=e;0<l;){var a=31-Ne(l),n=1<<a;e|=t[a],l&=~n}return e}function At(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Et(){var t=X;return X<<=1,(X&62914560)===0&&(X=4194304),t}function Se(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function pe(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Rc(t,e,l,a,n,u){var c=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var f=t.entanglements,d=t.expirationTimes,p=t.hiddenUpdates;for(l=c&~l;0<l;){var E=31-Ne(l),z=1<<E;f[E]=0,d[E]=-1;var g=p[E];if(g!==null)for(p[E]=null,E=0;E<g.length;E++){var T=g[E];T!==null&&(T.lane&=-536870913)}l&=~z}a!==0&&ci(t,a,0),u!==0&&n===0&&t.tag!==0&&(t.suspendedLanes|=u&~(c&~e))}function ci(t,e,l){t.pendingLanes|=e,t.suspendedLanes&=~e;var a=31-Ne(e);t.entangledLanes|=e,t.entanglements[a]=t.entanglements[a]|1073741824|l&261930}function hn(t,e){var l=t.entangledLanes|=e;for(t=t.entanglements;l;){var a=31-Ne(l),n=1<<a;n&e|t[a]&e&&(t[a]|=e),l&=~n}}function Wl(t,e){var l=e&-e;return l=(l&42)!==0?1:Hl(l),(l&(t.suspendedLanes|e))!==0?0:l}function Hl(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function mu(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function hu(){var t=ut.p;return t!==0?t:(t=window.event,t===void 0?32:xh(t.type))}function Le(t,e){var l=ut.p;try{return ut.p=t,e()}finally{ut.p=l}}var $t=Math.random().toString(36).slice(2),de="__reactFiber$"+$t,Ze="__reactProps$"+$t,vn="__reactContainer$"+$t,vr="__reactEvents$"+$t,Kh="__reactListeners$"+$t,Jh="__reactHandles$"+$t,yr="__reactResources$"+$t,vu="__reactMarker$"+$t,si="__reactLoad$"+$t;function fi(t){delete t[de],delete t[Ze],delete t[Kh],delete t[Jh]}function Va(t){var e;if(e=t[de])return e;for(var l=t.parentNode;l;){if(e=l[vn]||l[de]){if(l=e.alternate,e.child!==null||l!==null&&l.child!==null)for(t=eh(t);t!==null;){if(l=t[de])return l;t=eh(t)}return e}t=l,l=t.parentNode}return null}function yn(t){if(t=t[de]||t[vn]){var e=t.tag;if(e===5||e===6||e===13||e===31||e===26||e===27||e===3)return t}return null}function yu(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t.stateNode;throw Error(o(33))}function gn(t){var e=t[yr];return e||(e=t[yr]={hoistableStyles:new Map,hoistableScripts:new Map}),e}function be(t){t[vu]=!0}function gr(t){t[si]=void 0}var Sr=new Set,pr={};function La(t,e){Sn(t,e),Sn(t+"Capture",e)}function Sn(t,e){for(pLine truncated
|
||||
`).replace(H0,"")}function Hm(t,e){return e=Um(e),Um(t)===e}function qt(t,e,l,a,n,u){switch(l){case"children":if(typeof a=="string")e==="body"||e==="textarea"&&a===""||bn(t,a);else if(typeof a=="number"||typeof a=="bigint")e!=="body"&&bn(t,""+a);else return;break;case"className":oi(t,"class",a);break;case"tabIndex":oi(t,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":oi(t,l,a);break;case"style":Or(t,a,u);return;case"data":if(e!=="object"){oi(t,"data",a);break}case"src":case"href":if(a===""&&(e!=="a"||l!=="href")){t.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=di(a),t.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){t.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof u=="function"&&(l==="formAction"?(e!=="input"&&qt(t,e,"name",n.name,n,null),qt(t,e,"formEncType",n.formEncType,n,null),qt(t,e,"formMethod",n.formMethod,n,null),qt(t,e,"formTarget",n.formTarget,n,null)):(qt(t,e,"encType",n.encType,n,null),qt(t,e,"method",n.method,n,null),qt(t,e,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=di(a),t.setAttribute(l,a);break;case"onClick":a!=null&&(t.onclick=Bl);return;case"onScroll":a!=null&&xt("scroll",t);return;case"onScrollEnd":a!=null&&xt("scrollend",t);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(o(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(o(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"multiple":t.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":t.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){t.removeAttribute("xlink:href");break}l=di(a),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,""):t.removeAttribute(l);break;case"capture":case"download":a===!0?t.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?t.setAttribute(l,a):t.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?t.removeAttribute(l):t.setAttribute(l,a);break;case"popover":xt("beforetoggle",t),xt("toggle",t),ri(t,"popover",a);break;case"xlinkActuate":$l(t,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":$l(t,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":$l(t,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":$l(t,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":$l(t,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":$l(t,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":$l(t,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":$l(t,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":$l(t,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":ri(t,"is",a);break;case"innerText":case"textContent":return;default:if(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")l=tv.get(l)||l,ri(t,l,a);else return}Ct=!0}function Hf(t,e,l,a,n,u){switch(l){case"style":Or(t,a,u);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(o(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(o(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"children":if(typeof a=="string")bn(t,a);else if(typeof a=="number"||typeof a=="bigint")bn(t,""+a);else return;break;case"onScroll":a!=null&&xt("scroll",t);return;case"onScrollEnd":a!=null&&xt("scroLine truncated
|
||||
`).replace(H0,"")}function Hm(t,e){return e=Um(e),Um(t)===e}function qt(t,e,l,a,n,u){switch(l){case"children":if(typeof a=="string")e==="body"||e==="textarea"&&a===""||bn(t,a);else if(typeof a=="number"||typeof a=="bigint")e!=="body"&&bn(t,""+a);else return;break;case"className":oi(t,"class",a);break;case"tabIndex":oi(t,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":oi(t,l,a);break;case"style":Or(t,a,u);return;case"data":if(e!=="object"){oi(t,"data",a);break}case"src":case"href":if(a===""&&(e!=="a"||l!=="href")){t.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=di(a),t.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){t.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof u=="function"&&(l==="formAction"?(e!=="input"&&qt(t,e,"name",n.name,n,null),qt(t,e,"formEncType",n.formEncType,n,null),qt(t,e,"formMethod",n.formMethod,n,null),qt(t,e,"formTarget",n.formTarget,n,null)):(qt(t,e,"encType",n.encType,n,null),qt(t,e,"method",n.method,n,null),qt(t,e,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=di(a),t.setAttribute(l,a);break;case"onClick":a!=null&&(t.onclick=Bl);return;case"onScroll":a!=null&&xt("scroll",t);return;case"onScrollEnd":a!=null&&xt("scrollend",t);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(o(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(o(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"multiple":t.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":t.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){t.removeAttribute("xlink:href");break}l=di(a),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,""):t.removeAttribute(l);break;case"capture":case"download":a===!0?t.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?t.setAttribute(l,a):t.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?t.removeAttribute(l):t.setAttribute(l,a);break;case"popover":xt("beforetoggle",t),xt("toggle",t),ri(t,"popover",a);break;case"xlinkActuate":$l(t,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":$l(t,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":$l(t,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":$l(t,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":$l(t,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":$l(t,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":$l(t,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":$l(t,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":$l(t,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":ri(t,"is",a);break;case"innerText":case"textContent":return;default:if(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")l=tv.get(l)||l,ri(t,l,a);else return}Ct=!0}function Hf(t,e,l,a,n,u){switch(l){case"style":Or(t,a,u);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(o(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(o(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"children":if(typeof a=="string")bn(t,a);else if(typeof a=="number"||typeof a=="bigint")bn(t,""+a);else return;break;case"onScroll":a!=null&&xt("scroll",t);return;case"onScrollEnd":a!=null&&xt("scroLine truncated
|
||||
`;At&&(Se+=At+`
|
||||
`),Et&&(Se+=(Array.isArray(Et)?Et.join(`
|
||||
`):String(Et))+`
|
||||
@@ -68,7 +68,8 @@ Error generating stack: `+a.message+`
|
||||
`+D.result_text.slice(0,8e3)+`
|
||||
`),D.error_text&&(X+=`=== error ===
|
||||
`+D.error_text.slice(0,8e3)+`
|
||||
`),C(X||"(暂无 result/error 输出)")}catch{}Zt(H.id),Pe(H.id),qe(H.id),Tl(H.id),da(H),sl(H.id),fl(H.id),Gt(H.id),Ul(H.id),Dl(H.id)}},[Zt,Pe,qe,Tl,da,sl,fl,Gt,Ul,Dl]),ma=v.useCallback(async H=>{const D=ne.current;if(!D)return;const X={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(X[H]&&!window.confirm(X[H])))try{const K=await P($.taskAction(D.id,H),"POST",{});x(H+" 已提交:"+JSON.stringify(K).slice(0,300)),r(),await ge(D.id)}catch(K){x(H+" 失败:"+K.message)}},[ge,r]),su=v.useCallback(async()=>{const H=ne.current;if(H)try{const D=await P($.taskCost(H.id));b(JSON.stringify(D,null,2).slice(0,4e3))}catch(D){b("查成本失败:"+D.message)}},[]),fu=v.useCallback(async H=>{const D=ne.current;if(D){if(!H.trim()){dt("分支消息必填");return}if(!uu.includes(D.status)){dt("仅终态任务可分支");return}try{const X=await P($.taskFork(D.id),"POST",{message:H.trim()});dt("分支任务已建:"+X.id.slice(0,8)),r()}catch(X){dt("分支失败:"+X.message)}}},[r]),Fl=v.useCallback(async H=>{const D=ne.current;if(D)try{await P($.taskReviews(D.id),"POST",H),Vt("评审已登记(requested)"),await qe(D.id)}catch(X){Vt("登记评审需求失败:"+X.message)}},[qe]),Ee=v.useCallback(H=>{const D=ne.current;if(!D)return;const X=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");X!==null&&P($.taskReviewFindings(D.id,H),"POST",{findings:X}).then(()=>void qe(D.id)).catch(K=>Vt("写 findings 失败:"+K.message))},[qe]),rl=v.useCallback((H,D)=>{const X=ne.current;if(!X)return;const K=D==="approved"?"approve":"deny";P($.taskApprovalDecision(X.id,H,K),"POST",{}).then(()=>{mt("已记录"+(D==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),Tl(X.id)}).catch(O=>mt("决定失败:"+O.message))},[Tl]),Ne=v.useCallback(async(H,D,X)=>{const K=ne.current;if(K){if(!D.trim()){ae("目标任务 id 不能为空");return}if(!X.trim()){ae("消息正文不能为空");return}try{const O=await P($.taskMessagesTo(D.trim()),"POST",{from_task_id:K.id,body:X.trim(),mode:H});ae("已发送("+O.delivery+")"),await fl(K.id)}catch(O){ae("发送失败:"+O.message)}}},[fl]),ru=v.useCallback((H,D)=>{const X=ne.current;if(!X)return;if(!["completed","failed","interrupted"].includes(X.status)){oe("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const K={};if(H==="rename"){if(!D.trim()){oe("新线程名不能为空");return}K.name=D.trim()}P($.taskThreadAction(X.id,H),"POST",K).then(O=>{oe(H+" 已提交:"+JSON.stringify(O).slice(0,300)),Gt(X.id),Ul(X.id)}).catch(O=>oe(H+" 失败:"+O.message))},[Gt,Ul]),ou=v.useCallback(async H=>{const D=ne.current;if(!D)return;const X=H.trim();if(!X){Ve("消息不能为空");return}try{uu.includes(D.status)&&D.status!=="merged"?(await P($.taskContinue(D.id),"POST",{message:X}),Ve("已重新排队,同一会话继续"),r(),await ge(D.id)):["queued","running"].includes(D.status)?(await P($.taskFollowups(D.id),"POST",{body:X,mode:"direct"}),Ve("已直接发送"),await Zt(D.id),await Pe(D.id)):Ve("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(K){Ve("发送失败:"+K.message)}},[ge,r,Zt,Pe]),du=v.useCallback(async()=>{const H=ne.current;if(!H){_e("先选一个任务");return}try{const D=await P($.taskTrace(H.id)),X=new Blob([JSON.stringify({task_id:H.id,status:D.status,note:D.note||"",count:(D.entries||[]).length,entries:D.entries||[]},null,2)],{type:"application/json"}),K=document.createElement("a");K.href=URL.createObjectURL(X),K.download="trace-"+H.id.slice(0,8)+".json",document.body.appendChild(K),K.click(),K.remove(),setTimeout(()=>URL.revokeObjectURL(K.href),1e3),_e("已下载完整链路 "+(D.entries||[]).length+" 条")}catch(D){_e("下载失败:"+D.message)}},[]);return v.useEffect(()=>()=>ue(),[ue]),{currentTask:m,detailBody:_,actionMsg:N,actionReasons:w,costBox:G,costSummary:B,forkVisible:at,setForkVisible:et,forkMsg:L,coordChildren:zt,agents:nt,agentsNote:te,reviews:yt,reviewMsg:he,reviewError:Ft,traceEntries:Ie,traceNote:Y,traceDone:lt,traceKind:St,setTraceKind:ve,traceMsg:Ue,followups:y,approvals:W,approvalMsg:pt,peerMessages:_t,peerMsg:ut,threadText:ka,threadGateNote:Xe,threadMsg:Rt,threadList:Kl,chatMsg:oa,chatHistory:zl,showDetail:ge,refreshDetailLists:Xa,doAction:ma,taskCost:su,forkTask:fu,postReview:Fl,submitFindings:Ee,decideApproval:rl,postPeer:Ne,threadAction:ru,sendChatMessage:ou,downloadTrace:du,refreshReviews:()=>void qe(),refreshApprovals:()=>void Tl()}}function Py(i){const[r,m]=v.useState([]),[o,_]=v.useState(""),[C,N]=v.useState(null),[x,w]=v.useState(""),U=v.useCallback(async tt=>{const at=tt!==void 0?tt:i();if(!at){m([]);return}try{const et=await P($.projectAutomations(at));m(et.automations||[])}catch(et){_("自动化加载失败:"+et.message)}},[i]),G=v.useCallback(aLine truncated
|
||||
`),C(X||"(暂无 result/error 输出)")}catch{}Zt(H.id),Pe(H.id),qe(H.id),Tl(H.id),da(H),sl(H.id),fl(H.id),Gt(H.id),Ul(H.id),Dl(H.id)}},[Zt,Pe,qe,Tl,da,sl,fl,Gt,Ul,Dl]),ma=v.useCallback(async H=>{const D=ne.current;if(!D)return;const X={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(X[H]&&!window.confirm(X[H])))try{const K=await P($.taskAction(D.id,H),"POST",{});x(H+" 已提交:"+JSON.stringify(K).slice(0,300)),r(),await ge(D.id)}catch(K){x(H+" 失败:"+K.message)}},[ge,r]),su=v.useCallback(async()=>{const H=ne.current;if(H)try{const D=await P($.taskCost(H.id));b(JSON.stringify(D,null,2).slice(0,4e3))}catch(D){b("查成本失败:"+D.message)}},[]),fu=v.useCallback(async H=>{const D=ne.current;if(D){if(!H.trim()){dt("分支消息必填");return}if(!uu.includes(D.status)){dt("仅终态任务可分支");return}try{const X=await P($.taskFork(D.id),"POST",{message:H.trim()});dt("分支任务已建:"+X.id.slice(0,8)),r()}catch(X){dt("分支失败:"+X.message)}}},[r]),Fl=v.useCallback(async H=>{const D=ne.current;if(D)try{await P($.taskReviews(D.id),"POST",H),Vt("评审已登记(requested)"),await qe(D.id)}catch(X){Vt("登记评审需求失败:"+X.message)}},[qe]),Ee=v.useCallback(H=>{const D=ne.current;if(!D)return;const X=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");X!==null&&P($.taskReviewFindings(D.id,H),"POST",{findings:X}).then(()=>void qe(D.id)).catch(K=>Vt("写 findings 失败:"+K.message))},[qe]),rl=v.useCallback((H,D)=>{const X=ne.current;if(!X)return;const K=D==="approved"?"approve":"deny";P($.taskApprovalDecision(X.id,H,K),"POST",{}).then(()=>{mt("已记录"+(D==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),Tl(X.id)}).catch(O=>mt("决定失败:"+O.message))},[Tl]),Ne=v.useCallback(async(H,D,X)=>{const K=ne.current;if(K){if(!D.trim()){ae("目标任务 id 不能为空");return}if(!X.trim()){ae("消息正文不能为空");return}try{const O=await P($.taskMessagesTo(D.trim()),"POST",{from_task_id:K.id,body:X.trim(),mode:H});ae("已发送("+O.delivery+")"),await fl(K.id)}catch(O){ae("发送失败:"+O.message)}}},[fl]),ru=v.useCallback((H,D)=>{const X=ne.current;if(!X)return;if(!["completed","failed","interrupted"].includes(X.status)){oe("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const K={};if(H==="rename"){if(!D.trim()){oe("新线程名不能为空");return}K.name=D.trim()}P($.taskThreadAction(X.id,H),"POST",K).then(O=>{oe(H+" 已提交:"+JSON.stringify(O).slice(0,300)),Gt(X.id),Ul(X.id)}).catch(O=>oe(H+" 失败:"+O.message))},[Gt,Ul]),ou=v.useCallback(async H=>{const D=ne.current;if(!D)return;const X=H.trim();if(!X){Ve("消息不能为空");return}try{uu.includes(D.status)&&D.status!=="merged"?(await P($.taskContinue(D.id),"POST",{message:X}),Ve("已重新排队,同一会话继续"),r(),await ge(D.id)):["queued","running"].includes(D.status)?(await P($.taskFollowups(D.id),"POST",{body:X,mode:"direct"}),Ve("已直接发送"),await Zt(D.id),await Pe(D.id)):Ve("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(K){Ve("发送失败:"+K.message)}},[ge,r,Zt,Pe]),du=v.useCallback(async()=>{const H=ne.current;if(!H){_e("先选一个任务");return}try{const D=await P($.taskTrace(H.id)),X=new Blob([JSON.stringify({task_id:H.id,status:D.status,note:D.note||"",count:(D.entries||[]).length,entries:D.entries||[]},null,2)],{type:"application/json"}),K=document.createElement("a");K.href=URL.createObjectURL(X),K.download="trace-"+H.id.slice(0,8)+".json",document.body.appendChild(K),K.click(),K.remove(),setTimeout(()=>URL.revokeObjectURL(K.href),1e3),_e("已下载完整链路 "+(D.entries||[]).length+" 条")}catch(D){_e("下载失败:"+D.message)}},[]);return v.useEffect(()=>()=>ue(),[ue]),{currentTask:m,detailBody:_,actionMsg:N,actionReasons:w,costBox:G,costSummary:B,forkVisible:at,setForkVisible:et,forkMsg:L,coordChildren:zt,agents:nt,agentsNote:te,reviews:yt,reviewMsg:he,reviewError:Ft,traceEntries:Ie,traceNote:Y,traceDone:lt,traceKind:St,setTraceKind:ve,traceMsg:Ue,followups:y,approvals:W,approvalMsg:pt,peerMessages:_t,peerMsg:ut,threadText:ka,threadGateNote:Xe,threadMsg:Rt,threadList:Kl,chatMsg:oa,chatHistory:zl,showDetail:ge,refreshDetailLists:Xa,doAction:ma,taskCost:su,forkTask:fu,postReview:Fl,submitFindings:Ee,decideApproval:rl,postPeer:Ne,threadAction:ru,sendChatMessage:ou,downloadTrace:du,refreshReviews:()=>void qe(),refreshApprovals:()=>void Tl()}}function Py(i){const[r,m]=v.useState([]),[o,_]=v.useState(""),[C,N]=v.useState(null),[x,w]=v.useState(""),U=v.useCallback(async tt=>{const at=tt!==void 0?tt:i();if(!at){m([]);return}try{const et=await P($.projectAutomations(at));m(et.automations||[])}catch(et){_("自动化加载失败:"+et.message)}},[i]),G=v.useCallback(aLine truncated
|
||||
(链路摘要已截断至 300 字,完整回复见右侧任务结果)`:o,label:"助手"}}if(i.kind==="exec"||i.kind==="file"||i.kind==="tool"||i.kind==="call"){const o=i.kind==="exec"?"命令":i.kind==="file"?"文件":i.kind==="tool"?"工具":"调用";return{id:m,role:"tool",text:dr(i),label:o}}return i.kind==="plan"?{id:m,role:"status",text:i.text||""}:i.kind==="reasoning"?{id:m,role:"status",text:"思考:"+(i.summary||"")}:i.kind==="agent-start"?{id:m,role:"status",text:"子代理开始:"+(i.name||"")+(i.prompt?"("+i.prompt+")":"")}:i.kind==="agent-done"?{id:m,role:"status",text:"子代理结束:"+(i.name||"")+" "+(i.status||"")}:{id:m,role:"status",text:dr(i)}}function og(){const i=Mt(),r=v.useMemo(()=>{const m=i.currentTask,o=m&&m.prompt?[{id:"task-prompt",role:"user",text:m.prompt}]:[],C=(i.traceKind?i.traceEntries.filter(x=>x.kind===i.traceKind):i.traceEntries).map((x,w)=>Zh(x,w)),N=i.traceDone?[{id:"trace-done",role:"status",text:"链路已结束("+i.traceDone+")"}]:[];return[...o,...C,...N]},[i.currentTask,i.traceEntries,i.traceKind,i.traceDone]);return s.jsxs(s.Fragment,{children:[s.jsx("div",{className:"muted",children:"实时链路"}),s.jsxs(Ut,{children:[s.jsxs("select",{"aria-label":"链路类型",value:i.traceKind,onChange:m=>i.setTraceKind(m.target.value),children:[s.jsx("option",{value:"",children:"链路类型:全部"}),["exec","file","call","tool","message","reasoning","plan","agent-start","agent-done"].map(m=>s.jsx("option",{value:m,children:m==="exec"?"命令":m==="file"?"文件":m==="call"?"调用":m==="tool"?"工具":m==="message"?"消息":m==="reasoning"?"思考":m==="plan"?"计划":"子代理"},m))]}),s.jsx("button",{onClick:i.downloadTrace,children:"下载完整链路"}),s.jsx("span",{className:"muted",children:i.traceMsg})]}),i.traceNote?s.jsx("div",{children:s.jsx("span",{className:"err",children:i.traceNote})}):null,s.jsx("div",{className:"chat-column",children:s.jsx(Lh,{messages:r,empty:"(暂无链路条目)"})}),s.jsx("div",{className:"muted",children:"对话时间线:首条是本任务 prompt(用户);助手回复来自 message 条目,命令/文件/工具/调用是工具评论框,计划/思考/子代理开始结束是时间线上的状态文字。流式进行中时新条目实时追加;你在底部时自动跟随,往上翻时暂停跟随,点“回到最新”恢复。"})]})}function dg(){const i=Mt(),r=i.currentTask,m=v.useMemo(()=>{if(!r)return[];if(i.chatHistory)return i.chatHistory.messages.map(b=>({id:b.id,role:b.role,text:b.text,label:b.label}));const w=r.prompt?[{id:"task-prompt",role:"user",text:r.prompt}]:[],U=i.traceEntries.filter(b=>b.kind==="message").map((b,B)=>Zh(b,B)),G=i.followups.map((b,B)=>({id:"followup-"+B,role:"user",text:String(b.body||""),label:"后续消息"}));return[...w,...U,...G]},[r,i.chatHistory,i.traceEntries,i.followups]);if(!r)return s.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示继续会话"});const o=uu.includes(r.status),_=["queued","running"].includes(r.status),C=o||_,N=o?"终态任务:发送走继续接口(同一会话再跑一轮)":_?"运行中/排队中:发送走后续消息直接发送(跑完当前轮后优先执行)":"merged/merge-failed 任务不可继续,也不可发后续消息",x=i.chatHistory?i.chatHistory.legacy_continue_count??null:null;return s.jsxs("div",{className:"chat-column",children:[x!==null&&x>0?s.jsxs("div",{className:"muted",children:["此前有 ",x," 条继续消息发生在持久化之前,未入库,上面历史不完整;部署后新发的继续消息会正常显示。"]}):null,i.chatHistory&&i.chatHistory.trace_note?s.jsx("div",{className:"muted",children:i.chatHistory.trace_note}):null,s.jsx(Lh,{messages:m,empty:"(暂无会话消息)"}),s.jsx(rg,{disabled:!C,placeholder:"消息",onSend:w=>void i.sendChatMessage(w)}),s.jsxs("div",{className:"muted",children:[N,"。",i.chatMsg]})]})}function mg(){return Mt().currentTask?s.jsxs("div",{className:"conversation",children:[s.jsx(og,{}),s.jsx("div",{className:"conv-divider"}),s.jsx(dg,{})]}):s.jsx("div",{className:"conversation",children:s.jsx("div",{className:"muted",children:"在任务列表里点一条任务,对话和详情就切到该任务;也可以在上面派发一个新任务。"})})}function hg(){const i=Mt(),r=i.currentTask,[m,o]=v.useState("");if(!r)return null;const _=uu.includes(r.status),C=["failed","interrupted","merge-failed"].includes(r.status);return s.jsxs(s.Fragment,{children:[s.jsxs(Ut,{children:[s.jsx("button",{disabled:!C,title:C?"":"仅 failed/interrupted/merge-failed 可重试",onClick:()=>i.doAction("retry"),children:"重试"}),s.jsx("button",{className:"danger",disabled:_,title:_?"终态任务不可取消":"",onClick:()=>i.doAction("cancel"),children:"取消"}),s.jsx("button",{disabled:r.status!=="completed",title:r.status!=="completed"?"仅 completed 可合并":"",onClick:()=>i.doAction("merge"),children:"合并(completed → merged)"})Line truncated
|
||||
<style rel="stylesheet" crossorigin>:root{--text:#1f2328;--muted:#656d76;--subtle:#6e7781;--line:#d0d7de;--line-muted:#d8dee4;--paper:#ffffff;--card:#ffffff;--subtle-bg:#f6f8fa;--inset-bg:#f6f8fa;--accent:#0969da;--accent-emphasis:#0969da;--green:#2da44e;--green-hover:#2c974b;--success:#1a7f37;--danger:#d1242f;--danger-bg:#ffebe9;--attention:#9a6700;--done:#8250df;--selected-bg:#ddf4ff;--radius:6px;--shadow:0 1px 0 rgba(27,31,36,.04);--sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;--mono:ui-monospace,SFMono-Regular,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;--ctl:32px}*{box-sizing:border-box}html,body,#root{height:100%}body{font-family:var(--sans);margin:0;color:var(--text);background:#fff;line-height:1.5;font-size:14px}a{color:var(--accent);text-underline-offset:3px}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}.muted{color:var(--muted);font-size:12px;line-height:1.5}.err{color:var(--danger)}.ok{color:var(--success)}button{cursor:pointer;font-weight:500;border-radius:var(--radius);border:1px solid var(--line);background:#fff;color:var(--text);box-shadow:var(--shadow)}button:hover{background:var(--subtle-bg)}button:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}button.primary,button.seal{background:var(--green);border-color:#1b1f2426;color:#fff}button.primary:hover,button.seal:hover{background:var(--green-hover)}button.danger{color:var(--danger);border-color:var(--line);background:#fff}button.danger:hover{background:var(--danger-bg);color:var(--danger)}.icon-btn{width:32px;height:32px;padding:0;display:grid;place-items:center;border:1px solid var(--line);border-radius:var(--radius);background:#fff;color:var(--text);font-size:16px;line-height:1}.icon-btn:hover{background:var(--subtle-bg)}.dash-topbar{position:sticky;top:0;z-index:10;background:#24292f;color:#fff;border-bottom:1px solid rgba(255,255,255,.15);padding:10px 20px 8px}.dash-topbar .muted{color:#ffffffbf}.dash-topbar-row{display:flex;gap:14px;align-items:center;flex-wrap:wrap}.dash-brand{font-weight:600;font-size:16px;color:#fff}.dash-status{font-size:13px;font-weight:600;color:#ffffffbf}.dash-status.ok{color:#2da44e}.dash-stamp{margin-left:auto}.dash-nav{display:flex;gap:4px;flex-wrap:wrap;margin-top:8px}.dash-nav a{color:#fff;text-decoration:none;font-size:13px;font-weight:600;padding:6px 12px;border-radius:var(--radius)}.dash-nav a:hover{background:#ffffff1f;text-decoration:none}.dash-page{max-width:1180px;margin:0 auto;padding:20px;display:flex;flex-direction:column;gap:20px}.dash-page>*{min-width:0}.dash-section>*{min-width:0}.dash-section{scroll-margin-top:130px;display:flex;flex-direction:column;gap:16px}.dash-section.card{padding:16px}.dash-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.dash-card{background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:16px;min-width:0}.dash-card-label{color:var(--muted);font-size:12px;font-weight:600}.dash-card-value{font-size:24px;font-weight:600;line-height:1.3;overflow-wrap:anywhere}.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:none;padding:16px}.card h2{font-size:14px;margin:0 0 12px;font-weight:600}.card h3{font-size:14px;margin:24px 0 12px;padding-top:16px;border-top:1px solid var(--line);font-weight:600}.card h3:first-of-type{border-top:0;padding-top:0}.composer h2{font-size:16px}.composer textarea.prompt{min-height:148px;font-size:14px;padding:12px;border-radius:var(--radius)}.composer textarea.coord{font-family:var(--mono);font-size:13px}.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:12px 0}.bar>button,.bar>label{display:inline-flex;align-items:center;height:var(--ctl);min-height:var(--ctl);padding:0 16px;box-sizing:border-box;white-space:nowrap}.bar>input,.bar>select{height:var(--ctl);min-height:var(--ctl)}input,select,textarea,button{font:inherit;font-size:14px;padding:5px 12px;border-radius:var(--radius);border:1px solid var(--line);background:#fff;color:var(--text)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px #0969da4d}select{-moz-appearance:none;appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:34px;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1l5 5 5-5' fill='none' stroke='%23656d76' stroke-width='1.5' stroke-linecap='round'/></svg>");background-repeat:no-repeat;background-position:right 12px center}select::-ms-expand{display:none}select:disabled,input:disabled{color:var(--muted);background:var(--subtle-bg);cursor:not-allowed}input[type=checkbox]{width:16px;height:16px;accent-color:var(--accent)}input[type=file]{height:var(--ctl);display:inline-flex;align-items:center;padding:0 12px;border:1px dashed var(--line);border-radius:var(-Line truncated
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -241,6 +241,9 @@ def _summarize_trace_item(method, params):
|
||||
|
||||
只取过程事实,不取大文本:命令截 500 字、消息截 300 字、
|
||||
思考摘要截 200 字;fileChange 一项一变化拆多条。
|
||||
消息被截断时条目带 truncated=True(控制台据此标注“链路
|
||||
摘要已截断”,不得把截断后的文本当成模型的完整回复;
|
||||
完整回复在任务的 result_text 里,不在链路条目里)。
|
||||
返回 [] 表示该通知不进链路(userMessage 等)。
|
||||
ts 取通知的 completedAtMs/startedAtMs(毫秒 epoch),
|
||||
没有时为 None,服务端展示时按无时间处理。
|
||||
@@ -278,8 +281,11 @@ def _summarize_trace_item(method, params):
|
||||
text = item.get("text") or ""
|
||||
if not text:
|
||||
return []
|
||||
return [{"kind": "message", "id": item_id, "ts": ts, "thread": thread,
|
||||
"phase": item.get("phase") or "", "text": text[:300]}]
|
||||
entry = {"kind": "message", "id": item_id, "ts": ts, "thread": thread,
|
||||
"phase": item.get("phase") or "", "text": text[:300]}
|
||||
if len(text) > 300:
|
||||
entry["truncated"] = True
|
||||
return [entry]
|
||||
if item_type == "reasoning":
|
||||
summary = item.get("summary") or item.get("content") or []
|
||||
text = " ".join(s for s in summary if isinstance(s, str)).strip()
|
||||
|
||||
@@ -69,6 +69,23 @@ class SummarizeTraceItemTestCase(unittest.TestCase):
|
||||
self.assertEqual(len(entries), 1)
|
||||
self.assertEqual(entries[0]["kind"], kind)
|
||||
|
||||
def test_message_truncation_is_flagged(self):
|
||||
# 2026-10-29 实弹:468 字 final_answer 在链路里只剩前
|
||||
# 300 字、断在“fu”后,控制台照原样当助手回复展示,
|
||||
# 用户误以为模型没写完。截断本身是设计(防链路文件
|
||||
# 写爆),但必须带标记,短消息不带标记。
|
||||
long_entries = _summarize_trace_item(
|
||||
"item/completed",
|
||||
{"item": {"type": "agentMessage", "id": "m-long",
|
||||
"phase": "final_answer", "text": "x" * 468}})
|
||||
self.assertEqual(len(long_entries[0]["text"]), 300)
|
||||
self.assertTrue(long_entries[0].get("truncated"))
|
||||
short_entries = _summarize_trace_item(
|
||||
"item/completed",
|
||||
{"item": {"type": "agentMessage", "id": "m-short",
|
||||
"phase": "final_answer", "text": "done"}})
|
||||
self.assertNotIn("truncated", short_entries[0])
|
||||
|
||||
def test_skipped_notifications(self):
|
||||
self.assertEqual(_summarize_trace_item(
|
||||
"item/completed", {"item": {"type": "userMessage", "id": "u1",
|
||||
|
||||
Reference in new issue
Block a user