mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
* 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.
98 lines
3.4 KiB
TypeScript
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 },
|
|
},
|
|
},
|
|
}));
|