diff --git a/frontends/conductor.py b/frontends/conductor.py index afeb204f..2b10c455 100644 --- a/frontends/conductor.py +++ b/frontends/conductor.py @@ -343,8 +343,14 @@ def conductor_loop(): async def on_startup(): global main_loop main_loop = asyncio.get_running_loop() + import cost_tracker; cost_tracker.install() threading.Thread(target=conductor_loop, name="conductor-loop", daemon=True).start() +@app.get("/token-stats") +def conductor_token_stats(): + import cost_tracker + return {"records": [{"thread": k, "input": v.input, "output": v.output, "cacheCreate": v.cache_create, "cacheRead": v.cache_read} for k, v in cost_tracker.all_trackers().items()]} + @app.get("/") def index(): return FileResponse(HTML_PATH) diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index 10df45d5..5f7e830c 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -2953,7 +2953,30 @@ function tokRenderTable(records) { if(tokPager){tokPager.innerHTML='';if(totalPages>1)for(let i=0;i{_tokPage=i;tokRenderTable(records);});tokPager.appendChild(b);}} } -async function loadTokenPage(){await tokPollBridge();const f=tokGetFiltered();const all=tokLoadHistory();tokRenderStats(f,all);tokRenderTable(f);} +async function loadTokenPage(){await tokPollBridge();const f=tokGetFiltered();const all=tokLoadHistory();tokRenderStats(f,all);tokRenderTable(f);await tokRenderConductorRow();} + +let _conductorRowGen = 0; +async function tokRenderConductorRow() { + if (!tokTbody) return; + const gen = ++_conductorRowGen; + tokTbody.querySelectorAll('.tok-row-conductor').forEach(el => el.remove()); + try { + const data = await (await fetch(`http://${location.hostname}:8900/token-stats`)).json(); + if (gen !== _conductorRowGen) return; + tokTbody.querySelectorAll('.tok-row-conductor').forEach(el => el.remove()); + const recs = (data.records || []).filter(r => r.thread === 'conductor-agent' || r.thread.startsWith('subagent-')); + if (!recs.length) return; + let totalIn = 0, totalOut = 0, totalCc = 0, totalCr = 0, cost = 0; + for (const r of recs) { + totalIn += r.input || 0; totalOut += r.output || 0; totalCc += r.cacheCreate || 0; totalCr += r.cacheRead || 0; + cost += parseFloat(estCost(r.input || 0, r.output || 0, r.model || '', r.cacheRead || 0, r.cacheCreate || 0)); + } + const tr = document.createElement('tr'); tr.className = 'tok-row-session tok-row-conductor'; + tr.title = 'Conductor 消耗的 token 不计入累计 token 中'; + tr.innerHTML = `🎛 Conductor (${recs.length} threads)${fmtTok(totalIn)}${fmtTok(totalOut)}${fmtTok(totalCc)}${fmtTok(totalCr)}¥${cost.toFixed(2)}`; + tokTbody.insertBefore(tr, tokTbody.firstChild); + } catch (_) {} +} /* ─── Conductor token tab (disabled — pending time tracking) ─── */ /* diff --git a/frontends/desktop/static/styles.css b/frontends/desktop/static/styles.css index 1e3692cd..c396ae68 100644 --- a/frontends/desktop/static/styles.css +++ b/frontends/desktop/static/styles.css @@ -844,6 +844,9 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; } .tok-row-session:hover td{ background:var(--hover-dim); } .tok-row-session td:first-child::before{ content:'▶'; margin-right:6px; font-size:10px; color:var(--muted2); transition:transform .15s; display:inline-block; } .tok-row-session.open td:first-child::before{ transform:rotate(90deg); } +.tok-row-conductor{ cursor:default; background:var(--blue-soft); } +.tok-row-conductor td:first-child::before{ content:''; margin:0; } +.tok-row-conductor:hover td{ background:var(--blue-soft); } .tok-detail td{ padding:5px 12px 5px 32px; font-size:11px; color:var(--muted); border-bottom:1px solid var(--line-soft); background:var(--field-bg); } .tok-detail td:not(:first-child){ text-align:right; } .tok-detail[hidden]{ display:none; }