server: 任务列表一次聚合回 total_tokens
_list_tasks 在取附件的同时用 _task_tokens_map 一次查 cost_records 按 task_id 求和(GROUP BY),一屏只多一条 查询,不按任务逐个调成本接口。无记录计 0、表不存在时 全计 0 且列表不失败;_task_to_dict 新增 total_tokens 参数,默认 None(非列表调用方不把“未算”写成 0)。 前端 TaskListSection 的 token 栏位此前已接好 total_tokens, 补上后列表行直接显示真数,不再显示 token —;前端注释 与占位文案同步更新。 新测试 test_task_list_tokens.py 3 项:HTTP 列表聚合 (actual+estimated 加总、无记录为 0)、缺表不炸、 _task_to_dict 默认 None。
This commit is contained in:
1 parent
65a875ada9
commit
9eedee5a3d
4 files changed
+168
-14
No files matched your search
+1
-1
@@ -68,7 +68,7 @@ Error generating stack: `+a.message+`
|
||||
`+D.result_text.slice(0,8e3)+`
|
||||
`),D.error_text&&(L+=`=== error ===
|
||||
`+D.error_text.slice(0,8e3)+`
|
||||
`),C(L||"(暂无 result/error 输出)")}catch{}dl(H.id),Bl(H.id),Ye(H.id),Vt(H.id),Hl(H),ol(H.id),ll(H.id),ml(H.id),ql(H.id)}},[dl,Bl,Ye,Vt,Hl,ol,ll,ml,ql]),gn=v.useCallback(async H=>{const D=ne.current;if(!D)return;const L={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(L[H]&&!window.confirm(L[H])))try{const J=await tt($.taskAction(D.id,H),"POST",{});x(H+" 已提交:"+JSON.stringify(J).slice(0,300)),r(),await Wl(D.id)}catch(J){x(H+" 失败:"+J.message)}},[Wl,r]),Ke=v.useCallback(async()=>{const H=ne.current;if(H)try{const D=await tt($.taskCost(H.id));b(JSON.stringify(D,null,2).slice(0,4e3))}catch(D){b("查成本失败:"+D.message)}},[]),Za=v.useCallback(async H=>{const D=ne.current;if(D){if(!H.trim()){I("分支消息必填");return}if(!su.includes(D.status)){I("仅终态任务可分支");return}try{const L=await tt($.taskFork(D.id),"POST",{message:H.trim()});I("分支任务已建:"+L.id.slice(0,8)),r()}catch(L){I("分支失败:"+L.message)}}},[r]),va=v.useCallback(async H=>{const D=ne.current;if(D)try{await tt($.taskReviews(D.id),"POST",H),le("评审已登记(requested)"),await Ye(D.id)}catch(L){le("登记评审需求失败:"+L.message)}},[Ye]),ou=v.useCallback(H=>{const D=ne.current;if(!D)return;const L=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");L!==null&&tt($.taskReviewFindings(D.id,H),"POST",{findings:L}).then(()=>void Ye(D.id)).catch(J=>le("写 findings 失败:"+J.message))},[Ye]),du=v.useCallback((H,D)=>{const L=ne.current;if(!L)return;const J=D==="approved"?"approve":"deny";tt($.taskApprovalDecision(L.id,H,J),"POST",{}).then(()=>{K("已记录"+(D==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),Vt(L.id)}).catch(Ct=>K("决定失败:"+Ct.message))},[Vt]),$l=v.useCallback(async(H,D,L)=>{const J=ne.current;if(J){if(!D.trim()){k("目标任务 id 不能为空");return}if(!L.trim()){k("消息正文不能为空");return}try{const Ct=await tt($.taskMessagesTo(D.trim()),"POST",{from_task_id:J.id,body:L.trim(),mode:H});k("已发送("+Ct.delivery+")"),await ll(J.id)}catch(Ct){k("发送失败:"+Ct.message)}}},[ll]),Se=v.useCallback((H,D)=>{const L=ne.current;if(!L)return;if(!["completed","failed","interrupted"].includes(L.status)){Gt("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const J={};if(H==="rename"){if(!D.trim()){Gt("新线程名不能为空");return}J.name=D.trim()}tt($.taskThreadAction(L.id,H),"POST",J).then(Ct=>{Gt(H+" 已提交:"+JSON.stringify(Ct).slice(0,300)),ml(L.id),ql(L.id)}).catch(Ct=>Gt(H+" 失败:"+Ct.message))},[ml,ql]),hl=v.useCallback(async H=>{const D=ne.current;if(!D)return;const L=H.trim();if(!L){ae("消息不能为空");return}try{su.includes(D.status)&&D.status!=="merged"?(await tt($.taskContinue(D.id),"POST",{message:L}),ae("已重新排队,同一会话继续"),r(),await Wl(D.id)):["queued","running"].includes(D.status)?(await tt($.taskFollowups(D.id),"POST",{body:L,mode:"direct"}),ae("已直接发送"),await dl(D.id),await Bl(D.id)):ae("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(J){ae("发送失败:"+J.message)}},[Wl,r,dl,Bl]),be=v.useCallback(async()=>{const H=ne.current;if(!H){ie("先选一个任务");return}try{const D=await tt($.taskTrace(H.id)),L=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"}),J=document.createElement("a");J.href=URL.createObjectURL(L),J.download="trace-"+H.id.slice(0,8)+".json",document.body.appendChild(J),J.click(),J.remove(),setTimeout(()=>URL.revokeObjectURL(J.href),1e3),ie("已下载完整链路 "+(D.entries||[]).length+" 条")}catch(D){ie("下载失败:"+D.message)}},[]);return v.useEffect(()=>()=>qe(),[qe]),{currentTask:m,detailBody:_,actionMsg:N,actionReasons:G,costBox:Q,forkVisible:B,setForkVisible:lt,forkMsg:at,coordChildren:V,agents:Ot,agentsNote:nt,reviews:ee,reviewMsg:yt,reviewError:de,traceEntries:Ft,traceNote:tl,traceDone:Y,traceKind:et,setTraceKind:Tt,traceMsg:gt,followups:Oe,approvals:y,approvalMsg:F,peerMessages:pt,peerMsg:_t,threadText:ut,threadGateNote:Va,threadMsg:Le,threadList:zt,chatMsg:Fl,chatHistory:ha,showDetail:Wl,refreshDetailLists:yn,doAction:gn,taskCost:Ke,forkTask:Za,postReview:va,submitFindings:ou,decideApproval:du,postPeer:$l,threadAction:Se,sendChatMessage:hl,downloadTrace:be,refreshReviews:()=>void Ye(),refreshApprovals:()=>void Vt()}}function Wy(i){const[r,m]=v.useState([]),[o,_]=v.useState(""),[C,N]=v.useState(null),[x,G]=v.useState(""),U=v.useCallback(async lt=>{const at=lt!==void 0?lt:i();if(!at){m([]);return}try{const I=await tt($.projectAutomations(at));m(I.automations||[])}catch(I){_("自动化加载失败:"+I.message)}},[i]),Q=v.useCallback(async(lt,at,I,V)=>{cLine truncated
|
||||
`),C(L||"(暂无 result/error 输出)")}catch{}dl(H.id),Bl(H.id),Ye(H.id),Vt(H.id),Hl(H),ol(H.id),ll(H.id),ml(H.id),ql(H.id)}},[dl,Bl,Ye,Vt,Hl,ol,ll,ml,ql]),gn=v.useCallback(async H=>{const D=ne.current;if(!D)return;const L={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(L[H]&&!window.confirm(L[H])))try{const J=await tt($.taskAction(D.id,H),"POST",{});x(H+" 已提交:"+JSON.stringify(J).slice(0,300)),r(),await Wl(D.id)}catch(J){x(H+" 失败:"+J.message)}},[Wl,r]),Ke=v.useCallback(async()=>{const H=ne.current;if(H)try{const D=await tt($.taskCost(H.id));b(JSON.stringify(D,null,2).slice(0,4e3))}catch(D){b("查成本失败:"+D.message)}},[]),Za=v.useCallback(async H=>{const D=ne.current;if(D){if(!H.trim()){I("分支消息必填");return}if(!su.includes(D.status)){I("仅终态任务可分支");return}try{const L=await tt($.taskFork(D.id),"POST",{message:H.trim()});I("分支任务已建:"+L.id.slice(0,8)),r()}catch(L){I("分支失败:"+L.message)}}},[r]),va=v.useCallback(async H=>{const D=ne.current;if(D)try{await tt($.taskReviews(D.id),"POST",H),le("评审已登记(requested)"),await Ye(D.id)}catch(L){le("登记评审需求失败:"+L.message)}},[Ye]),ou=v.useCallback(H=>{const D=ne.current;if(!D)return;const L=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");L!==null&&tt($.taskReviewFindings(D.id,H),"POST",{findings:L}).then(()=>void Ye(D.id)).catch(J=>le("写 findings 失败:"+J.message))},[Ye]),du=v.useCallback((H,D)=>{const L=ne.current;if(!L)return;const J=D==="approved"?"approve":"deny";tt($.taskApprovalDecision(L.id,H,J),"POST",{}).then(()=>{K("已记录"+(D==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),Vt(L.id)}).catch(Ct=>K("决定失败:"+Ct.message))},[Vt]),$l=v.useCallback(async(H,D,L)=>{const J=ne.current;if(J){if(!D.trim()){k("目标任务 id 不能为空");return}if(!L.trim()){k("消息正文不能为空");return}try{const Ct=await tt($.taskMessagesTo(D.trim()),"POST",{from_task_id:J.id,body:L.trim(),mode:H});k("已发送("+Ct.delivery+")"),await ll(J.id)}catch(Ct){k("发送失败:"+Ct.message)}}},[ll]),Se=v.useCallback((H,D)=>{const L=ne.current;if(!L)return;if(!["completed","failed","interrupted"].includes(L.status)){Gt("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const J={};if(H==="rename"){if(!D.trim()){Gt("新线程名不能为空");return}J.name=D.trim()}tt($.taskThreadAction(L.id,H),"POST",J).then(Ct=>{Gt(H+" 已提交:"+JSON.stringify(Ct).slice(0,300)),ml(L.id),ql(L.id)}).catch(Ct=>Gt(H+" 失败:"+Ct.message))},[ml,ql]),hl=v.useCallback(async H=>{const D=ne.current;if(!D)return;const L=H.trim();if(!L){ae("消息不能为空");return}try{su.includes(D.status)&&D.status!=="merged"?(await tt($.taskContinue(D.id),"POST",{message:L}),ae("已重新排队,同一会话继续"),r(),await Wl(D.id)):["queued","running"].includes(D.status)?(await tt($.taskFollowups(D.id),"POST",{body:L,mode:"direct"}),ae("已直接发送"),await dl(D.id),await Bl(D.id)):ae("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(J){ae("发送失败:"+J.message)}},[Wl,r,dl,Bl]),be=v.useCallback(async()=>{const H=ne.current;if(!H){ie("先选一个任务");return}try{const D=await tt($.taskTrace(H.id)),L=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"}),J=document.createElement("a");J.href=URL.createObjectURL(L),J.download="trace-"+H.id.slice(0,8)+".json",document.body.appendChild(J),J.click(),J.remove(),setTimeout(()=>URL.revokeObjectURL(J.href),1e3),ie("已下载完整链路 "+(D.entries||[]).length+" 条")}catch(D){ie("下载失败:"+D.message)}},[]);return v.useEffect(()=>()=>qe(),[qe]),{currentTask:m,detailBody:_,actionMsg:N,actionReasons:G,costBox:Q,forkVisible:B,setForkVisible:lt,forkMsg:at,coordChildren:V,agents:Ot,agentsNote:nt,reviews:ee,reviewMsg:yt,reviewError:de,traceEntries:Ft,traceNote:tl,traceDone:Y,traceKind:et,setTraceKind:Tt,traceMsg:gt,followups:Oe,approvals:y,approvalMsg:F,peerMessages:pt,peerMsg:_t,threadText:ut,threadGateNote:Va,threadMsg:Le,threadList:zt,chatMsg:Fl,chatHistory:ha,showDetail:Wl,refreshDetailLists:yn,doAction:gn,taskCost:Ke,forkTask:Za,postReview:va,submitFindings:ou,decideApproval:du,postPeer:$l,threadAction:Se,sendChatMessage:hl,downloadTrace:be,refreshReviews:()=>void Ye(),refreshApprovals:()=>void Vt()}}function Wy(i){const[r,m]=v.useState([]),[o,_]=v.useState(""),[C,N]=v.useState(null),[x,G]=v.useState(""),U=v.useCallback(async lt=>{const at=lt!==void 0?lt:i();if(!at){m([]);return}try{const I=await tt($.projectAutomations(at));m(I.automations||[])}catch(I){_("自动化加载失败:"+I.message)}},[i]),Q=v.useCallback(async(lt,at,I,V)=>{cLine 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>
|
||||
|
||||
Reference in new issue
Block a user