feat(desktop): conductor token stats summary row in token page

- conductor.py: install cost_tracker + expose /token-stats endpoint (5 lines)
- app.js: fetch conductor token totals and display as a single summary row at top of token table (not expandable, with tooltip noting it's excluded from cumulative totals)
- styles.css: conductor row styling
This commit is contained in:
yiqi-017 committed 2026-05-29 23:03:53 +08:00
1 parent db7dd70ab3
commit 90a37236aa
3 files changed
+33 -1

No files matched your search

+6
View File
@@ -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)
+24 -1
View File
@@ -2953,7 +2953,30 @@ function tokRenderTable(records) {
if(tokPager){tokPager.innerHTML='';if(totalPages>1)for(let i=0;i<totalPages;i++){const b=document.createElement('button');b.textContent=i+1;if(i===_tokPage)b.className='active';b.addEventListener('click',()=>{_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 = `<td>🎛 Conductor (${recs.length} threads)</td><td>${fmtTok(totalIn)}</td><td>${fmtTok(totalOut)}</td><td>${fmtTok(totalCc)}</td><td>${fmtTok(totalCr)}</td><td>¥${cost.toFixed(2)}</td>`;
tokTbody.insertBefore(tr, tokTbody.firstChild);
} catch (_) {}
}
/* ─── Conductor token tab (disabled — pending time tracking) ─── */
/*
+3
View File
@@ -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; }