feat(web-shell): add token-usage analytics dashboard to Daemon Status - #6388
Conversation
Add a "统计 / Usage" tab to the Daemon Status page: a Today/7D/30D period toggle over the selected range's token totals and input/output/cache-read breakdown, a 12-month token heatmap (per-day tokens + cache-read tooltip, localized month labels), per-model token share, skill-call counts, and daily token/session charts. Backend: a new read-only GET /usage/dashboard daemon API backed by a core usage-dashboard service that aggregates the durable local usage history (cross-project ~/.qwen), reusing loadUsageHistory + aggregateUsage. Skill counts are threaded through the shared usage pipeline. No new instrumentation — every metric is read from data qwen-code already persists.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
This change touches core infrastructure at scale (725 lines added in packages/core/src/, spanning 5 packages: core, cli, sdk-typescript, web-shell, webui). Core refactors must be maintainer-initiated — please open an issue to discuss the design first.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
PR #6388 Review Summary
Build & Tests: ✅ Build passes, all 16 tests pass (9 in usage-dashboard-service.test.ts, 7 in usage-stats.test.ts).
Overall: This is a well-structured feature PR that adds a usage analytics dashboard across 5 packages. The TTL cache, input validation, i18n coverage, and test suite are solid. A few correctness and performance issues flagged below.
Findings
| # | Severity | File | Issue |
|---|---|---|---|
| 1 | 🔴 Critical | usage-dashboard-service.ts |
calculateStreaks off-by-one: longestStreak not updated when a streak ends at a gap |
| 2 | 🔴 Critical | usageHistoryService.ts |
topSkills unbounded — no .slice(0, 10) cap unlike topTools |
| 3 | 🔴 Critical | TokenHeatmap.tsx |
DST bug: new Date(gridStart.getTime() + i * MS_PER_DAY) drifts across DST transitions |
| 4 | 🟡 Suggestion | usage-stats.ts |
Cache keyed by range causes redundant full-disk loads on toggle |
| 5 | 🟡 Suggestion | usage-dashboard-service.ts |
loadUsageDashboard has zero logging — hard to diagnose issues |
| 6 | 🟡 Suggestion | usage-dashboard-service.ts / TokenHeatmap.tsx |
localDateKey + startOfLocalDay duplicated with different signatures |
| 7 | 🟡 Suggestion | i18n.tsx |
Dead i18n key daemon.usage.streak never consumed by any component |
| 8 | 🟡 Suggestion | usageHistoryService.ts |
aggregateUsage skill aggregation not unit-tested at the function level |
Nice-to-haves
calculateStreaksis triplicated acrossusage-dashboard-service.ts,statsDataService.ts, andDataProcessor.tsUsageRangetype defined separately in 3 places (route, SDK, core) with different value setsHEATMAP_DAYSderived viaMath.round(12 * 30.44)— magic number30.44lacks a commentdaemon.usage.heatmapSubi18n default of "6 months" doesn't match the component's constant of 12- React components could benefit from
useMemoon derived arrays passed to children - Cache has no background sweep; up to 1098 entries possible with distinct
heatmapDaysvalues
tanzhenxin
left a comment
There was a problem hiding this comment.
Design-level review — deliberately not a line-by-line pass.
The need is real and the foundation is right. The web shell had no historical usage view while the TUI has /stats; surfacing where tokens go is a legitimate gap. The best design property of this PR is what it doesn't do: no new instrumentation, no new persistence — it reads the same usage_record.jsonl the TUI /stats reads, through the same loadUsageHistory + aggregateUsage pipeline, so the two surfaces can't drift apart. Threading skills through the shared pipeline (the one field the persisted record was dropping) belongs in core regardless of this dashboard. Below the UI, the wiring follows the established daemon-feature pattern exactly — route → DaemonClient method → webui hook — the same shape as daemonStatus or workspaceSkills. I also checked the core/serve boundary: the new service is stateless, read-only aggregation over data core already owns, so I don't share the bot's "core refactor" objection; the footprint is additive read-side code sitting next to the format it reads. And since the replay path is a one-time migration fallback (rebuilt records are persisted), the steady-state cost is one file read — the TTL cache is a reasonable belt-and-suspenders on top.
The one design decision worth settling is the product placement. Everything below the tab is placement-agnostic; the single decision in this PR that isn't pattern-following is that usage analytics lives inside Daemon Status. That's the part I'd want to be deliberate about. The dialog's three existing tabs — overview, metrics, diagnostics — are all one thing: the live health of the serve process (ephemeral, process-scoped, poll-driven). The Usage tab is the opposite on every axis: historical, durable, machine-global, user-scoped. It answers "where did my tokens go", not "is the daemon healthy". Concretely that costs discoverability — a user wondering about token spend has no reason to open something called Daemon Status — and it constrains growth: this dashboard already carries model shares, a skills table, and streaks, and the obvious next asks (cost estimates, per-project or per-agent breakdowns) make it a product surface, not a status widget. A dialog tab is a cramped home for that trajectory.
The web shell already has the pattern to host it as a first-class surface: the in-place panels (Settings / Daemon Status, #6341) and the Scheduled Tasks page. UsageDashboardTab is self-contained, so giving it its own entry is a small change now — and the route/SDK/hook layers wouldn't change at all. Deciding later is cheap technically but not for users: people learn where things live.
So the one question I'd like to settle before merge: is Usage intended as a widget of daemon health, or as the seed of the web shell's usage/analytics surface? If the former, the current tab is fine as-is. If the latter — and the richness already in this PR suggests it is — let's give it its own home now, while the move costs almost nothing.
中文版本
设计层面的 review,有意不做逐行的代码审查。
需求是真实的,底子也是对的。 Web shell 一直没有历史用量视图,而 TUI 早就有 /stats,这个空白确实该补上。这个 PR 在设计上最让人放心的,恰恰是它「没做」的部分:没有加任何新埋点,也没有引入新的持久化 —— 读的就是 TUI /stats 那份 usage_record.jsonl,走的也是同一条 loadUsageHistory + aggregateUsage 管线,两边的数据口径天然一致,不会各说各话。把 skills 接进共享管线(持久化记录里此前唯一被丢掉的字段),这个改动本来就应该进 core,跟仪表盘本身没有关系。UI 之下的各层接线也完全沿用了现有 daemon 功能的套路 —— 路由 → DaemonClient 方法 → webui hook,和 daemonStatus、workspaceSkills 如出一辙。core/serve 的分层边界我也核对过:新服务是无状态的只读聚合,处理的本来就是 core 自己的数据,所以 bot 提的 "core refactor" 异议我并不认同 —— 这只是紧挨着数据格式的纯新增只读代码。另外,transcript 回放只是一次性的迁移兜底(重建出的记录会落盘),稳态下就是读一个文件,TTL 缓存只是多加的一道保险,并不是撑住性能的关键。
真正值得先讨论清楚的,是产品位置。 tab 之下的各层放哪儿都一样;整个 PR 里唯一一个不是「照既有模式办事」的决策,就是把用量分析放进了 Daemon Status 里面 —— 这一点我觉得值得想清楚再定。这个对话框现有的三个 tab(overview、metrics、diagnostics)讲的是同一件事:serve 进程当下的健康状况 —— 即时的、进程级的、靠轮询刷新的。而 Usage tab 在每个维度上都正好相反:历史的、持久的、跨项目全局的、面向用户本人的。它回答的问题是「我的 token 花哪儿了」,而不是「daemon 还好吗」。这带来两个实际代价:一是可发现性 —— 想查 token 消耗的用户,不会想到去点一个叫 Daemon Status 的入口;二是限制了后续发展 —— 这个仪表盘已经有了模型份额、技能调用表、连续使用天数,接下来顺理成章的需求(成本估算、按项目/按 agent 拆分)只会让它越长越像一个独立的产品功能,而不是状态页里的一个小板块。对话框里的一个 tab,撑不起这条路线。
而且 web shell 已经有承载独立入口的现成机制:in-place panel(Settings / Daemon Status,见 #6341)和 Scheduled Tasks 页面。UsageDashboardTab 本身是自包含的,现在给它一个独立入口改动很小,路由/SDK/hook 各层一行都不用动。以后再搬,技术上固然容易,但用户习惯不会跟着搬 —— 大家一旦记住了东西在哪儿,再挪就是折腾。
所以合并前想跟你对齐一个问题:Usage 到底是 Daemon Status 的一个附属板块,还是 web shell 用量分析功能的起点? 如果是前者,现在这个 tab 没问题;如果是后者 —— 这个 PR 本身的完成度已经在暗示是后者 —— 那不如趁现在搬家成本几乎为零,直接给它一个独立的家。
- cap `aggregateUsage` topSkills at 25 like topTools, so the aggregate and dashboard payload stay bounded - fix a DST drift in the heatmap grid: advance the day/month cursor by calendar day (setDate) instead of a fixed `i * MS_PER_DAY` offset - cache the loaded history once (range-independent) so toggling Today/7D/30D re-aggregates from a single disk read; split a pure `buildUsageDashboard(records, opts)` out of `loadUsageDashboard` - drop the unused per-day streak computation and the dead `daemon.usage.streak` i18n key - add debug logging to the dashboard builder and a direct `aggregateUsage`-skills unit test
tanzhenxin
left a comment
There was a problem hiding this comment.
Design question settled — keeping Usage under Daemon Status is a deliberate choice for now, and extracting it to its own surface later is a plain move (the tab is self-contained; route/SDK/hook layers are placement-agnostic). Approving.
Review feedback addressed (a72bea6)Thanks for the review. All 8 findings are handled — the 3 Critical items and the actionable suggestions are fixed and pushed, and each inline thread is resolved individually above. The full suite is still green (core 8 + usageHistory 16, route 6, webui 266, web-shell 1054) and I re-verified end-to-end against a real
Also from the nice-to-haves: removed the (now-triplicated) On the "core refactor — open an issue first" flagRespectfully, this is an additive feature, not a core refactor. Almost the entire 中文说明已处理审阅意见(a72bea6)感谢审阅。8 条发现全部处理:3 条 Critical 与可执行的建议均已修复并推送,上方每条行内线程也已逐条 resolve。全套仍绿(core 8+usageHistory 16、route 6、webui 266、web-shell 1054),并已对真实
nice-to-have 亦一并:删掉(现在三处重复的) 关于「core 大改,请先开 issue」这其实是新增功能而非 core 重构。 |
|
Thanks for the design-level review, @tanzhenxin — and for settling the placement question directly. Confirming the decision: Usage stays as a Daemon Status tab for now, deliberately. That was the placement chosen when scoping this PR, and your framing of the trade-off is exactly right — it's the one non-pattern-following decision here, and the extraction to a first-class surface later is a plain move: One note on your "richness" list: the automated review flagged the per-day streaks as computed-but-never-rendered, so I removed them in a72bea6 (along with the dead i18n key). Model shares, the skills table, and the daily token/session charts remain. All of that review's Critical + suggestion items are fixed in the same commit and the threads are resolved. 中文感谢 @tanzhenxin 的设计层 review,也感谢直接把「产品位置」这个问题拍板。 确认决策:Usage 目前有意保留为 Daemon Status 的一个 tab。 这也是本 PR 立项时选定的位置。你对取舍的判断完全准确 —— 这确实是整个 PR 里唯一一个「不照既有模式办事」的决策,而将来抽成独立入口是很轻的动作: 关于你提到的「richness」清单补一句:自动化 review 指出每日**连续使用天数(streaks)**是「算了但从没渲染」,所以我在 a72bea6 里把它连同那个死 i18n 键一起删了。模型份额、技能调用表、每日 token/会话图表都保留。那条 review 的全部 Critical + 建议项都在同一提交修复,线程也已 resolve。 |
doudouOUC
left a comment
There was a problem hiding this comment.
Review Summary
Overall this is a well-structured, high-quality feature PR. The architecture is clean (core service → route → SDK → React hook → UI), the caching strategy is solid, and the test coverage is thorough. One semantic inconsistency worth addressing before merge:
Issue: currentStreak doc vs implementation mismatch
File: packages/core/src/services/usage-dashboard-service.ts
The UsageDashboard interface doc says:
currentStreak: Consecutive days with activity ending today (0 if today has a gap).
But calculateStreaks() uses:
if (daysSinceLast > 1) currentStreak = 0;This means when the last active day is yesterday (daysSinceLast === 1), the streak is preserved — contradicting the "ending today" / "0 if today has a gap" description. The same semantic leaks into the SDK type DaemonUsageDashboard.currentStreak.
Fix: Either change > 1 to >= 1 (to match the doc — streak requires today), or update the JSDoc to say "ending at the most recent active day" (to match the code).
Minor observations (non-blocking)
-
topSkillsnot capped: UnliketopToolswhich does.slice(0, 10)inaggregateUsage,topSkillsreturns all entries. Fine for now (skill counts are small), but may want parity if skill count grows. -
Performance acknowledged: The cold-path
loadUsageHistory()replays all transcripts. The 60s TTL cache + on-demand loading (tab mount only) is the right mitigation. Long-term, incremental aggregation or a pre-computed summary file could help. -
Additive core change: The
skillsaddition toAggregatedReportis backward-compatible (existing consumers ignore it), and the two affected test fixtures are correctly updated.
Verdict
The PR is ready to merge after clarifying the streak semantics (doc or code fix — either direction is fine). Since it touches packages/core/src/services/usageHistoryService.ts (shared pipeline), maintainer sign-off is appropriate per project policy.
wenshao
left a comment
There was a problem hiding this comment.
Review event downgraded to COMMENT because presubmit reported a self-PR and CI is still running.
…oalescing - Make the daemon dashboard side-effect free: `loadUsageHistory` gains a `persistRebuild` option, and the route passes `persistRebuild: false`, so serving a GET never writes to `~/.qwen`. The transcript-rebuild fallback previously persisted rebuilt records (including an in-progress session), violating the read-only contract. - Fix cache coalescing on the slow path: a pending history load is now reused regardless of age (the TTL starts at settlement), so a request arriving after the TTL while the load is still pending no longer kicks off a second full load. - Tests: read-only rebuild writes nothing, `metricsToUsageRecord` copies `SessionMetrics.skills`, and a pending load is shared past the TTL.
Second review round addressed (178fb6c)Thanks for the follow-up pass. All three items are fixed and each thread is resolved:
Green after the changes: core usageHistory 19 + dashboard 8, route 7, statsData 17 — and re-verified end-to-end against a real 中文第二轮 3 条全部修复,线程均已 resolve:
修改后全绿(core usageHistory 19 + dashboard 8、route 7、statsData 17),并已对真实 |
doudouOUC
left a comment
There was a problem hiding this comment.
Re-review (post a72bea6 + 178fb6c)
All previous findings are addressed. The updated code is noticeably better than the initial version:
Changes since first review
-
currentStreak/longestStreakremoved entirely — the semantic ambiguity I flagged is gone; the feature (if desired later) can be re-introduced with clear semantics from scratch. -
Architecture improved:
buildUsageDashboard(pure) +loadUsageDashboard(async wrapper) — The core service now exposes a pure, synchronous-logic function that takes pre-loaded records. The route caches the loaded history once (range-independent) and re-runs the cheapbuildUsageDashboardper request. Today/7D/30D toggles no longer trigger redundant disk I/O. -
Read-only GET guarantee:
persistRebuild: false—loadUsageHistorynow accepts an option to skip persisting rebuilt records. The daemon route passespersistRebuild: false, ensuringGET /usage/dashboardnever writes to~/.qwen— even on the transcript-replay fallback path. This is properly tested (usageHistoryService.test.ts). -
topSkillscapped at 25 — Bounded liketopTools, with a test verifying the cap. -
DST-safe heatmap grid —
TokenHeatmapnow usescursor.setDate(cursor.getDate() + 1)instead ofgridStart + i * MS_PER_DAY, preventing DST offset drift. Same fix for the month-label cursor. -
Improved caching semantics in route — The
HistoryCachepattern properly handles: pending loads shared across concurrent requests (even past TTL),settledAt-based TTL that only starts after the load settles, and rejected loads clearing the cache for immediate retry. Theif (cache === entry)guards prevent stale callbacks from corrupting newer entries. -
Debug logging —
createDebugLogger('USAGE_DASHBOARD')added for observability. -
heatmapSub default fixed — Changed from
months ?? 6tomonths ?? 12matchingHEATMAP_MONTHS = 12.
Verdict
No remaining issues. The architecture is clean, the read-only guarantee is properly enforced and tested, the caching strategy is sound, and all previously raised concerns are resolved. LGTM.
wenshao
left a comment
There was a problem hiding this comment.
Second-pass review at 178fb6c — no high-confidence Critical findings. Suggestion-level recommendations are in the Suggestion summary comment below.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
|
Thanks for the PR, @ZevGit! Template looks good ✓ — all required sections present, bilingual, with concrete verification evidence. Problem: Observed and well-documented. The legacy terminal scrollback path causes visible flicker and scrollbar jumps during TUI updates on long sessions. Two maintainers independently verified this: @chiga0 identified the scrollbar/mouse-event issues (now fixed in #6002), and @wenshao confirmed the default flip with raw ANSI capture showing Direction: Aligned. The virtualized history path already exists, is already tested, and already avoids the physical clear/replay behavior that causes flicker. Making it the default reduces the number of users hitting the old path without requiring them to discover an obscure setting. The screen-reader carve-out and CI/non-interactive fallback are sensible. No CHANGELOG reference needed — this is a UX default flip, not a new feature. Approach: The scope is tight and correct. The core change is Moving on to code review. 🔍 中文说明感谢贡献,@ZevGit! 模板完整 ✓ — 所有必要部分齐全,双语,并提供了具体的验证证据。 问题: 已观测且有充分记录。旧的 terminal scrollback 路径在长会话的 TUI 更新期间会导致可见的闪烁和滚动条跳动。两位维护者独立验证了这一点:@chiga0 识别了滚动条/鼠标事件问题(已在 #6002 中修复),@wenshao 用原始 ANSI 捕获确认了默认翻转 — PR head 上发出了 方向: 对齐。虚拟化历史路径已经存在、已经过测试,并且已经避免了导致闪烁的物理清屏/重放行为。将其设为默认值可以减少遇到旧路径的用户数量,而不需要他们去发现一个隐蔽的设置。屏幕阅读器的例外和 CI/非交互式回退都是合理的。 方案: 范围紧凑且正确。核心改动是 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
2a. Code ReviewIndependent proposal (before reading the diff): I'd add a new read-only daemon route ( Comparison with the diff: The PR's approach matches this exactly — Core changes assessment:
No critical blockers found. The code is straightforward, follows project conventions (ESM, strict TS, license headers), and the cross-package boundaries are correct (aggregation in core, route in cli, UI in web-shell/webui). Reuse check: The dashboard service correctly reuses existing 2b. Real-Scenario TestingBuilt Unit Tests (all pass)
Endpoint Testing (tmux)All parameter edge cases handled correctly. The caching works as designed — first load is 10ms (this machine has a small usage history), subsequent loads are sub-2ms. The route is read-only (passes CI also passed on Ubuntu (25m59s full suite). 中文说明2a. 代码审查独立方案(读 diff 前): 新增只读 daemon 路由 与 diff 对比: PR 方案完全匹配 — Core 变更评估:
未发现关键阻塞问题。 代码简洁,遵循项目规范,跨包边界正确。 2b. 真实场景测试构建了 — Qwen Code · qwen3.7-max |
|
This PR is clean and ready to ship. The motivation is genuine — web-shell users had no historical usage view, and this fills the gap by reading data qwen-code already persists. No new instrumentation, no telemetry risk. The implementation matches what I'd propose independently: a pure aggregation function in core, a thin async wrapper with caching in the daemon route, and a new tab in the web-shell. The code is straightforward. The core changes are minimal and purely additive — Testing is thorough: 34 unit tests pass across core and cli, regression checks on existing stats components pass, and the real daemon endpoint returns correct data with proper parameter validation. The caching behavior matches the PR description (10ms first load, sub-2ms cached). CI passed on Ubuntu. The previous Stage 0 hard block (725 lines in core) was based on a larger earlier revision. The current diff has 365 lines of core source — well under the 500-line threshold. All prior review findings from @tanzhenxin and @doudouOUC have been addressed across two revision rounds. Approval guardrail check: this is a cross-repository PR, but the title is 中文说明这个 PR 已经就绪,可以合入。 动机真实 — web-shell 用户缺少历史用量视图,本 PR 通过读取 qwen-code 已持久化的数据填补这一空白,无新埋点,无遥测风险。实现方案与独立评估一致:core 中的纯聚合函数、daemon 路由中带缓存的薄异步包装、web-shell 中的新 tab。 代码简洁。core 变更极少且完全增量 — 测试充分:34 个单元测试通过,既有 stats 组件回归检查通过,真实 daemon 端点返回正确数据且参数校验正常。缓存行为与 PR 描述一致。CI Ubuntu 通过。 此前的 Stage 0 硬拦截(core 725 行)基于早期较大版本。当前 diff core 源码 365 行 — 远低于 500 行阈值。@tanzhenxin 和 @doudouOUC 此前两轮审查的所有发现均已处理。 审批护栏检查:这是跨仓库 PR,但标题是 — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
Adds a 统计 / Usage tab to the Daemon Status page (web shell) — a local token-usage analytics dashboard. It has a Today / 7D / 30D period toggle that drives the range's headline totals (tokens consumed, sessions, requests, tool calls, changes) and an input / output / cache-read breakdown; a 12-month token heatmap where each day's cell colors by total tokens and a hover tooltip shows the date, tokens, and that day's cache-read rate; a per-model token-share ranking (each bar carries a light-green overlay marking the model's cache-read fraction); a skill-call table; and daily token (line) and session (bar) charts for the selected range.
Backend: a new read-only
GET /usage/dashboarddaemon route backed by a new coreusage-dashboard-service. It aggregates the durable local usage history under~/.qwen(cross-project) by reusing the existingloadUsageHistory+aggregateUsage— the same source the TUI/statscommand reads. Skill-call counts are threaded through the shared usage pipeline (metricsToUsageRecord+aggregateUsage), which was the only field the persisted usage record had been dropping. There is no new instrumentation: every metric is read from data qwen-code already persists by default.Why it's needed
The Daemon Status page previously exposed live runtime metrics (concurrency, latency, memory) but no historical token-usage view. This surfaces where tokens actually go — over time, per model, per skill — entirely from already-persisted data, so web-shell users get the same usage insight the TUI
/statscommand provides, plus a heatmap and per-model/per-skill breakdowns.Reviewer Test Plan
How to verify
Automated:
npm run test --workspace @qwen-code/qwen-code-core— coreusage-dashboard-service(range windows, models/skills/daily, per-day heatmap cache rate).npm run test --workspace @qwen-code/qwen-code—usage-statsroute (range parsing, TTL cache, fallback) + updated stats fixtures.npm run test --workspace @qwen-code/webuiandnpm run test --workspace @qwen-code/web-shell— theUsageDashboardTab+TokenHeatmaprender tests (period toggle, model share, skill table, daily charts, tooltip with cache, localized month labels).Real daemon API (no model needed — the route reads local usage files):
@qwen-code/qwen-code-core,@qwen-code/sdk,@qwen-code/acp-bridge, then runnpx tsx packages/cli/src/cli.ts serve --web --port 8799.curl 'http://127.0.0.1:8799/usage/dashboard?range=7d&heatmapDays=365'returns{ range, summary, models, skills, daily, heatmap: { "YYYY-MM-DD": { tokens, cacheReadRate } }, heatmapDays, ... }.In the browser: run
qwen serve --web, open Daemon Status, click the 统计 / Usage tab. Toggle Today / 7D / 30D (headline totals + breakdown re-fetch and update); hover heatmap cells (tooltip shows date · tokens · cache); switch the UI language to 中文 (/language ui zh-CN) and confirm the heatmap month labels render as7月etc.Evidence (Before & After)
Before: Daemon Status had only
overview / metrics / diagnosticstabs. After: a new统计 / Usagetab renders the dashboard described above.I verified locally with the automated suites (core
usage-dashboard-service9,usage-statsroute 7, webui 266, web-shell 1053 incl.UsageDashboardTab8 /TokenHeatmap3) and by driving the real daemon over HTTP. Example real response forrange=todayon seeded data (today: gpt-5.5 515k + claude-opus-4-8 210k, plus 5-day / 20-day-old sessions):Screenshots — captured from a real local run (
qwen serve --web, ~220 days of seeded usage across 6 models, light theme, driven with Playwright + system Chrome):Full dashboard (Today) — hero + stat tiles + input/output/cache breakdown + 12-month heatmap + per-model share + skill calls:
Daily token line + session bars (30D) and the per-skill counts:
Heatmap hover tooltip — date · tokens · that day's cache-read rate:
Localized to 中文 — the heatmap month labels follow the UI language (
7月,8月, …):Tested on
Environment (optional)
Local:
npx tsx packages/cli/src/cli.ts serve --web(daemon reads~/.qwenusage files); Vitest for unit/integration.Risk & Scope
loadUsageHistoryreplays every project's transcripts on a cold call, so the first load on a large history can be slow. Mitigated by on-demand fetch (the tab loads on open + manual refresh, it does not poll), a 60s daemon-side TTL cache that coalesces concurrent requests, and a capped daily-series window.skillswas added to the sharedAggregatedReport(additive — the TUI/statsignores it); two existing TUI test fixtures were updated to include the new field.Linked Issues
N/A
中文说明
这个 PR 做了什么
给 Daemon Status 页面(web shell)新增一个 统计 / Usage tab —— 一个本地 token 用量分析仪表盘。包含:Today / 7D / 30D 周期切换,驱动该区间的总览数字(消耗 tokens、会话、请求、工具调用、改动)与 输入 / 输出 / 缓存读取 拆分;一张 12 个月 token 热力图,每格按当日总 token 着色,悬浮提示显示日期、tokens 和当日缓存命中比例;按模型的 token 份额 排行(每条进度条用浅绿覆盖标出该模型的缓存读取占比);一张 技能调用 表;以及所选区间的 每日 token(折线)与会话(柱状)图表。
后端:新增只读路由
GET /usage/dashboard,由新的 coreusage-dashboard-service支撑。它复用已有的loadUsageHistory+aggregateUsage聚合~/.qwen下的持久化本地用量历史(跨项目)—— 与 TUI/stats命令同源。技能调用次数被接入共享用量管线(metricsToUsageRecord+aggregateUsage),这是持久化用量记录此前唯一丢弃的字段。没有任何新埋点:每个指标都读自 qwen-code 默认已经持久化的数据。为什么需要
Daemon Status 页此前只暴露实时运行指标(并发、延迟、内存),没有历史 token 用量视图。本 PR 把 token 花在哪里(随时间、按模型、按技能)呈现出来,全部来自已持久化数据,让 web shell 用户获得与 TUI
/stats相同的用量洞察,并额外提供热力图和按模型/技能的拆分。如何验证
自动化:
npm run test --workspace @qwen-code/qwen-code-core(core 服务)、@qwen-code/qwen-code(路由 + stats 夹具)、@qwen-code/webui、@qwen-code/web-shell(UsageDashboardTab+TokenHeatmap渲染测试)。真实 daemon API(路由只读本地文件,无需模型):先 build core/sdk/acp-bridge,再
npx tsx packages/cli/src/cli.ts serve --web --port 8799,curl 'http://127.0.0.1:8799/usage/dashboard?range=7d&heatmapDays=365'返回{ range, summary, models, skills, daily, heatmap: {日期: {tokens, cacheReadRate}}, ... }。浏览器:
qwen serve --web→ 打开 Daemon Status → 点 统计 / Usage tab;切换 Today/7D/30D(总数与拆分会重新拉取更新);悬浮热力图格子(提示显示 日期 · tokens · 缓存);把界面切成中文(/language ui zh-CN)确认月份标签变成7月。证据
Before:Daemon Status 只有
overview / metrics / diagnostics。After:新增统计 / Usagetab。本地已用自动化套件(core 服务 9、路由 7、webui 266、web-shell 1053)+ 真实 daemon HTTP 验证。区间语义:today 725000/2、7D 975000/3(纳入 5 天前会话)、30D 1275000/4(再纳入 20 天前);heatmap[今天] = {tokens:725000, cacheReadRate:0.9657}。上方 Evidence 附了 4 张真实本地截图(serve --web真跑,Playwright + 系统 Chrome):完整仪表盘、每日 token 折线 + 会话柱状、热力图悬浮显示「日期 · tokens · 缓存」、以及中文本地化(热力图月份7月… 跟随界面语言)。风险与范围
loadUsageHistory冷调用会回放全项目 transcript,大历史下首次加载偏慢;已用按需拉取(打开 tab + 手动刷新,不轮询)、daemon 侧 60s TTL 缓存(合并并发)、每日序列封顶缓解。skills以 additive 方式加入共享AggregatedReport(TUI/stats忽略它),两个既有 TUI 测试夹具已补该字段。