cc-haha/src/server/api/activityStats.ts
程序员阿江(Relakkes) ace7f7b657 feat: add desktop token usage activity view
Expose local Claude Code CLI transcript usage in Settings so users can inspect recent token consumption and daily activity without leaving the desktop app.

The page uses server-side transcript aggregation for session, message, tool, model-token, and subagent token data. Daily token buckets use assistant message timestamps, and daily session counts use active parent sessions for the same date bucket so resumed sessions and cross-midnight work do not produce token-only days. Cache accounting is bumped to v5 to force recomputation under the corrected daily semantics.

Constraint: Usage data must come from local Claude Code CLI transcripts rather than mock/demo data.
Constraint: Desktop navigation keeps Token usage directly above Diagnostics.
Rejected: Bucket all token usage by session start date | hides resumed-session and cross-midnight consumption from the actual day it was spent.
Confidence: high
Scope-risk: moderate
Directive: Keep daily token and daily session counts on the same date-bucketing semantics.
Tested: bun run check:desktop
Tested: bun run check:server
Tested: Browser verification for Token usage in English and Chinese locale date labels
Not-tested: Full bun run verify quality gate
2026-05-10 11:37:49 +08:00

44 lines
1.1 KiB
TypeScript

import {
aggregateClaudeCodeStatsForRange,
type StatsDateRange,
} from '../../utils/stats.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
const VALID_RANGES = new Set<StatsDateRange>(['7d', '30d', 'all'])
export async function handleActivityStatsApi(
req: Request,
_url: URL,
segments: string[],
): Promise<Response> {
try {
if (req.method !== 'GET') {
throw methodNotAllowed(req.method)
}
const requestedRange = segments[2]
const range: StatsDateRange = requestedRange === undefined ? 'all' : parseRange(requestedRange)
const stats = await aggregateClaudeCodeStatsForRange(range)
return Response.json({
stats,
range,
generatedAt: new Date().toISOString(),
})
} catch (error) {
return errorResponse(error)
}
}
function parseRange(range: string): StatsDateRange {
if (VALID_RANGES.has(range as StatsDateRange)) {
return range as StatsDateRange
}
throw ApiError.badRequest(`Unknown activity stats range: ${range}`)
}
function methodNotAllowed(method: string): ApiError {
return new ApiError(405, `Method ${method} not allowed`, 'METHOD_NOT_ALLOWED')
}