diff --git a/web/src/views/QuotaMonitor.vue b/web/src/views/QuotaMonitor.vue index 7e581f3..199a5d3 100644 --- a/web/src/views/QuotaMonitor.vue +++ b/web/src/views/QuotaMonitor.vue @@ -97,6 +97,67 @@ + +
+
+

🎯 任务级成本分析

+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
任务 IDAgent 链路调用次数Input TokensOutput Tokens总耗时总花费
+ {{ task.expanded ? '▼' : '▶' }} + {{ task.taskId }} + + + {{ a }} → + + {{ task.calls }}{{ (task.input / 1000).toFixed(0) }}k{{ (task.output / 1000).toFixed(0) }}k{{ (task.duration / 1000).toFixed(0) }}s + ${{ task.cost.toFixed(2) }} +
+
暂无任务数据
+
+
+

🕒 最近的大模型调用日志

@@ -145,6 +206,7 @@ const loading = ref(true) const rawData = ref([]) const timeScale = ref('day') // hour, day, week, month +const taskSort = ref('cost') // cost, tokens, time onMounted(async () => { try { @@ -223,6 +285,36 @@ const maxAgentTokens = computed(() => { return Math.max(...agentStats.value.map(a => a.tokens), 1) }) +// Task-level Breakdown +const taskStats = computed(() => { + const map = {} + rawData.value.forEach(d => { + // 从 convId 中提取 taskId + const convId = d.convId || '' + let taskId = null + const taskMatch = convId.match(/(?:task|callback)-?(T-[a-z0-9]+)/i) + if (taskMatch) taskId = taskMatch[1] + else if (convId.startsWith('tpl-train-')) taskId = convId + else return // 跳过非任务类型的记录(如 conv-xxx 用户对话) + + if (!map[taskId]) map[taskId] = { taskId, cost: 0, input: 0, output: 0, calls: 0, duration: 0, agentSet: new Set(), details: [], expanded: false, firstTime: d.timestamp } + map[taskId].cost += (d.costUsd || 0) + map[taskId].input += (d.usage?.input || 0) + map[taskId].output += (d.usage?.output || 0) + map[taskId].calls += 1 + map[taskId].duration += (d.durationMs || 0) + map[taskId].agentSet.add(d.agentId) + map[taskId].details.push(d) + if (d.timestamp < map[taskId].firstTime) map[taskId].firstTime = d.timestamp + }) + + const arr = Object.values(map).map(t => ({ ...t, agents: [...t.agentSet] })) + if (taskSort.value === 'cost') arr.sort((a, b) => b.cost - a.cost) + else if (taskSort.value === 'tokens') arr.sort((a, b) => (b.input + b.output) - (a.input + a.output)) + else arr.sort((a, b) => new Date(b.firstTime) - new Date(a.firstTime)) + return arr.slice(0, 50) +}) + // Recent Logs table const recentLogs = computed(() => rawData.value.slice(0, 50)) @@ -473,6 +565,54 @@ function formatTime(iso) { color: var(--text-faint); } +/* Task breakdown */ +.task-table-wrapper { + overflow-x: auto; + max-height: 600px; + overflow-y: auto; +} +.task-table tr { + cursor: pointer; + transition: background 0.15s; +} +.task-table tr:hover { + background: rgba(82, 168, 255, 0.05); +} +.task-id-cell { + font-family: monospace; + font-size: 12px; + white-space: nowrap; +} +.expand-icon { + font-size: 10px; + margin-right: 6px; + color: var(--text-secondary); +} +.agent-badge { + font-size: 12px; + color: var(--text-secondary); +} +.task-row-expensive { + background: rgba(255, 100, 50, 0.04); +} +.cost-warn { + color: #f59e0b !important; + font-weight: 700; +} +.cost-danger { + color: #ef4444 !important; + font-weight: 700; +} +.task-detail-row { + background: var(--bg-secondary) !important; + cursor: default !important; + font-size: 12px; +} +.task-detail-row td { + padding: 8px 12px; + color: var(--text-secondary); +} + .log-table-wrapper { overflow-x: auto; }