qwen-code/packages/web-shell/vite.config.ts
Shaojin Wen 350191e101
feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388)
* feat(web-shell): add token-usage analytics dashboard to Daemon Status

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.

* fix(web-shell): address usage-dashboard review feedback

- 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

* fix(usage-dashboard): make the dashboard load read-only + fix cache coalescing

- 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.
2026-07-06 13:43:41 +00:00

98 lines
3.4 KiB
TypeScript

import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import type { ProxyOptions } from 'vite';
import react from '@vitejs/plugin-react';
import pkg from './package.json' with { type: 'json' };
const daemonProxy: ProxyOptions = {
target: process.env['QWEN_DAEMON_URL'] ?? 'http://127.0.0.1:4170',
changeOrigin: true,
bypass: (req) => {
if (req.url?.startsWith('/api/')) return undefined;
const fetchMode = req.headers['sec-fetch-mode'];
const fetchDest = req.headers['sec-fetch-dest'];
const accept = req.headers.accept ?? '';
const isDocumentNavigation =
fetchMode === 'navigate' ||
fetchDest === 'document' ||
accept.trim().toLowerCase().startsWith('text/html');
if (isDocumentNavigation) {
return '/index.html';
}
return undefined;
},
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
proxyReq.removeHeader('origin');
proxyReq.removeHeader('referer');
});
},
};
export default defineConfig(({ command }) => ({
root: 'client',
plugins: [react()],
resolve: {
alias:
command === 'serve'
? {
'@qwen-code/webui/daemon-react-sdk': resolve(
__dirname,
'../webui/src/daemon-react-sdk.ts',
),
'@qwen-code/webui': resolve(__dirname, '../webui/src/index.ts'),
'@qwen-code/sdk/daemon': resolve(
__dirname,
'../sdk-typescript/src/daemon/index.ts',
),
'@qwen-code/sdk': resolve(
__dirname,
'../sdk-typescript/src/index.ts',
),
}
: {},
dedupe: ['react', 'react-dom', '@qwen-code/webui', '@qwen-code/sdk'],
},
build: {
outDir: '../dist',
emptyOutDir: true,
},
define: {
__WEB_SHELL_VERSION__: JSON.stringify(pkg.version),
},
server: {
cors: false,
port: 5173,
proxy: {
'/health': daemonProxy,
'/capabilities': daemonProxy,
// Daemon status report; scoped to the exact route the dashboard uses (a
// bare `/daemon` prefix would proxy unrelated `/daemon/*` paths). Without
// it the SPA fallback answers with index.html and the dialog fails JSON
// parsing in dev.
'/daemon/status': daemonProxy,
'/session': daemonProxy,
'/permission': daemonProxy,
'/workspace': daemonProxy,
'/file': daemonProxy,
'/stat': daemonProxy,
'/list': daemonProxy,
'/glob': daemonProxy,
// Scheduled-tasks CRUD (the Scheduled Tasks dialog). Prefix-matches
// `/scheduled-tasks` and `/scheduled-tasks/:id`. Like the routes above,
// without it the SPA fallback returns index.html in dev and the dialog
// fails JSON parsing / reports an HTTP error on open.
'/scheduled-tasks': daemonProxy,
// Token-usage dashboard (Daemon Status "统计" tab). Same reason as the
// routes above — without it the SPA fallback returns index.html in dev and
// the tab fails JSON parsing on `GET /usage/dashboard`.
'/usage': daemonProxy,
// Voice dictation is a WebSocket (`/voice/stream`); `ws: true` makes the
// dev proxy forward the HTTP upgrade to the daemon. Scope it to the exact
// path — a bare `/voice` prefix would shadow the client's own
// `client/voice/*` source modules (e.g. `/voice/voiceModels.ts`), which
// vite must serve, and blanks the page.
'/voice/stream': { ...daemonProxy, ws: true },
},
},
}));