From 762743c3236a41f729d0200204f273b8b11ebef1 Mon Sep 17 00:00:00 2001 From: ila Date: Wed, 26 Aug 2026 11:36:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=94=9F=E6=88=90?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=80=97=E6=97=B6=E5=B1=95=E7=A4=BA=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/chorus/generations/helpers.js | 55 +++++++++++- .../src/views/chorus/generations/index.vue | 24 ++--- admin-ui/src/views/chorus/shared.js | 4 +- .../unit/chorus/generation-attempts.spec.js | 83 ++++++++++------- admin/app/chorus/service.go | 89 ++++++++++++++++++- admin/app/chorus/service_test.go | 25 +++++- admin/app/chorus/types.go | 31 ++++--- docs/02-architecture-and-code-map.md | 15 +++- docs/09-product-requirements-overview.md | 15 +++- portal/handler/router.go | 12 ++- portal/handler/router_test.go | 24 ++++- portal/openapi/openapi.json | 1 + portal/openapi/spec_test.go | 19 ++++ portal/web/e2e/fixture/main.go | 7 +- portal/web/e2e/portal.spec.js | 8 ++ portal/web/static/app.js | 53 ++++++++++- portal/web/templates/app.html | 4 +- portal/web/templates/result.html | 4 +- portal/web/web.go | 80 +++++++++++++++++ portal/web/web_test.go | 51 +++++++++++ 20 files changed, 526 insertions(+), 78 deletions(-) diff --git a/admin-ui/src/views/chorus/generations/helpers.js b/admin-ui/src/views/chorus/generations/helpers.js index 974426c..b997665 100644 --- a/admin-ui/src/views/chorus/generations/helpers.js +++ b/admin-ui/src/views/chorus/generations/helpers.js @@ -1,19 +1,70 @@ +const parseTime = value => { + if (!value) return null + const timestamp = new Date(value).getTime() + return Number.isFinite(timestamp) ? timestamp : null +} + +export function durationText(milliseconds) { + if (!Number.isFinite(milliseconds) || milliseconds < 0) return '时间异常' + if (milliseconds < 100) return '<0.1 秒' + const totalSeconds = milliseconds / 1000 + if (totalSeconds < 60) return `${totalSeconds.toFixed(1)} 秒` + const minutes = Math.floor(totalSeconds / 60) + const seconds = (totalSeconds - minutes * 60).toFixed(1).padStart(4, '0') + if (minutes < 60) return `${minutes} 分 ${seconds} 秒` + const hours = Math.floor(minutes / 60) + return `${hours} 小时 ${String(minutes % 60).padStart(2, '0')} 分 ${String(seconds).padStart(2, '0')} 秒` +} + +const between = (start, end) => { + const startTime = parseTime(start) + const endTime = parseTime(end) + if (startTime === null || endTime === null) return '-' + return durationText(endTime - startTime) +} + +export function generationTiming(generation, now = Date.now()) { + const row = generation || {} + const terminal = row.status === 'succeeded' || row.status === 'failed' + const end = terminal ? row.completed_at : new Date(now).toISOString() + const providerAttempts = Array.isArray(row.attempts) + ? row.attempts.filter(item => item && item.type === 'provider' && Number.isFinite(Number(item.latency_ms)) && Number(item.latency_ms) >= 0) + : [] + const upstreamMS = providerAttempts.reduce((sum, item) => sum + Number(item.latency_ms), 0) + return { + total: between(row.created_at, end), + queue: row.started_at ? between(row.created_at, row.started_at) : '-', + processing: row.started_at ? between(row.started_at, end) : '-', + upstream: providerAttempts.length ? durationText(upstreamMS) : '-' + } +} + +export function kindText(value) { + return ({ text: '文本', image: '图片' })[value] || '未知' +} + const eventTitle = attempt => { if (attempt.type === 'lease') return '任务认领' if (attempt.type === 'local_rate_limit') return '本地限流' - if (attempt.type === 'provider') return attempt.provider_model_id ? `模型 #${attempt.provider_model_id}` : '模型未记录' + if (attempt.type === 'provider') { + const provider = attempt.provider_name || 'Provider 未记录' + const model = attempt.model_name || (attempt.provider_model_id ? `模型 #${attempt.provider_model_id}` : '模型未记录') + return `${provider} · ${model}` + } return '系统事件' } export function attemptPresentation(value) { const attempt = value && typeof value === 'object' ? value : {} + const isProvider = attempt.type === 'provider' const presentation = { + label: isProvider ? (attempt.provider_ordinal ? `尝试 ${attempt.provider_ordinal}` : 'Provider 尝试') : '系统事件', title: eventTitle(attempt), status: '进行中', + duration: Number.isFinite(Number(attempt.latency_ms)) && Number(attempt.latency_ms) >= 0 ? durationText(Number(attempt.latency_ms)) : between(attempt.started_at, attempt.finished_at), type: 'warning', timestamp: attempt.finished_at || attempt.started_at || attempt.deferred_until || '' } - if (attempt.error_code) { presentation.status = attempt.error_code presentation.type = 'danger' diff --git a/admin-ui/src/views/chorus/generations/index.vue b/admin-ui/src/views/chorus/generations/index.vue index c1a1693..e5a8d7e 100644 --- a/admin-ui/src/views/chorus/generations/index.vue +++ b/admin-ui/src/views/chorus/generations/index.vue @@ -1,17 +1,18 @@