From 55e469c7fe35ee4fcfab714e132d17bd1f8d16b6 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:26:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(ops):=20=E4=BC=98=E5=8C=96=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=97=A5=E5=BF=97=E8=BF=87=E6=BB=A4=E5=92=8C=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端改动: - 添加 resolved 参数默认值处理(向后兼容,默认显示未解决错误) - 新增 status_codes_other 查询参数支持 - 移除 service 层的高级设置过滤逻辑,简化错误日志查询流程 前端改动: - 完善错误日志相关组件的国际化支持 - 优化 Ops 监控面板和设置对话框的用户体验 --- backend/internal/handler/admin/ops_handler.go | 17 +++ backend/internal/repository/ops_repo.go | 12 +- backend/internal/service/ops_models.go | 13 +- backend/internal/service/ops_service.go | 55 -------- frontend/src/api/admin/ops.ts | 1 + frontend/src/i18n/locales/en.ts | 87 ++++++++++++- frontend/src/i18n/locales/zh.ts | 87 ++++++++++++- .../ops/components/OpsDashboardHeader.vue | 64 ++++----- .../ops/components/OpsErrorDetailModal.vue | 122 +++++++++--------- .../ops/components/OpsErrorDetailsModal.vue | 26 ++-- .../admin/ops/components/OpsErrorLogTable.vue | 14 +- .../ops/components/OpsRuntimeSettingsCard.vue | 28 ++-- .../ops/components/OpsSettingsDialog.vue | 40 +++--- 13 files changed, 349 insertions(+), 217 deletions(-) diff --git a/backend/internal/handler/admin/ops_handler.go b/backend/internal/handler/admin/ops_handler.go index 9349838a..c76b6a60 100644 --- a/backend/internal/handler/admin/ops_handler.go +++ b/backend/internal/handler/admin/ops_handler.go @@ -110,6 +110,12 @@ func (h *OpsHandler) GetErrorLogs(c *gin.Context) { filter.Source = source } filter.View = parseOpsViewParam(c) + + // Legacy endpoint default: unresolved only (backward-compatible). + { + b := false + filter.Resolved = &b + } if v := strings.TrimSpace(c.Query("resolved")); v != "" { switch strings.ToLower(v) { case "1", "true", "yes": @@ -143,6 +149,17 @@ func (h *OpsHandler) GetErrorLogs(c *gin.Context) { } filter.StatusCodes = out } + if v := strings.TrimSpace(c.Query("status_codes_other")); v != "" { + switch strings.ToLower(v) { + case "1", "true", "yes": + filter.StatusCodesOther = true + case "0", "false", "no": + filter.StatusCodesOther = false + default: + response.BadRequest(c, "Invalid status_codes_other") + return + } + } result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter) if err != nil { diff --git a/backend/internal/repository/ops_repo.go b/backend/internal/repository/ops_repo.go index c9cca1d5..0535547d 100644 --- a/backend/internal/repository/ops_repo.go +++ b/backend/internal/repository/ops_repo.go @@ -132,7 +132,6 @@ func (r *opsRepository) ListErrorLogs(ctx context.Context, filter *service.OpsEr pageSize = 500 } - // buildOpsErrorLogsWhere may mutate filter (default resolved filter). where, args := buildOpsErrorLogsWhere(filter) countSQL := "SELECT COUNT(*) FROM ops_error_logs e " + where @@ -933,15 +932,11 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) { } // ops_error_logs stores client-visible error requests (status>=400), // but we also persist "recovered" upstream errors (status<400) for upstream health visibility. - // By default, keep list endpoints scoped to unresolved records if the caller didn't specify. + // If Resolved is not specified, do not filter by resolved state (backward-compatible). resolvedFilter := (*bool)(nil) if filter != nil { resolvedFilter = filter.Resolved } - if resolvedFilter == nil { - f := false - resolvedFilter = &f - } // Keep list endpoints scoped to client errors unless explicitly filtering upstream phase. if phaseFilter != "upstream" { clauses = append(clauses, "COALESCE(status_code, 0) >= 400") @@ -1007,6 +1002,11 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) { if len(filter.StatusCodes) > 0 { args = append(args, pq.Array(filter.StatusCodes)) clauses = append(clauses, "COALESCE(upstream_status_code, status_code, 0) = ANY($"+itoa(len(args))+")") + } else if filter.StatusCodesOther { + // "Other" means: status codes not in the common list. + known := []int{400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504, 529} + args = append(args, pq.Array(known)) + clauses = append(clauses, "NOT (COALESCE(upstream_status_code, status_code, 0) = ANY($"+itoa(len(args))+"))") } if q := strings.TrimSpace(filter.Query); q != "" { like := "%" + q + "%" diff --git a/backend/internal/service/ops_models.go b/backend/internal/service/ops_models.go index c48c9b56..ebdf148f 100644 --- a/backend/internal/service/ops_models.go +++ b/backend/internal/service/ops_models.go @@ -86,12 +86,13 @@ type OpsErrorLogFilter struct { GroupID *int64 AccountID *int64 - StatusCodes []int - Phase string - Owner string - Source string - Resolved *bool - Query string + StatusCodes []int + StatusCodesOther bool + Phase string + Owner string + Source string + Resolved *bool + Query string // View controls error categorization for list endpoints. // - errors: show actionable errors (exclude business-limited / 429 / 529) diff --git a/backend/internal/service/ops_service.go b/backend/internal/service/ops_service.go index d606ba09..915be5df 100644 --- a/backend/internal/service/ops_service.go +++ b/backend/internal/service/ops_service.go @@ -261,64 +261,9 @@ func (s *OpsService) GetErrorLogs(ctx context.Context, filter *OpsErrorLogFilter return nil, err } - // Apply error filtering based on settings (for historical data) - result = s.filterErrorLogsBySettings(ctx, result) return result, nil } -// filterErrorLogsBySettings filters error logs based on advanced settings. -// This ensures that historical errors are also filtered when viewing the dashboard. -func (s *OpsService) filterErrorLogsBySettings(ctx context.Context, result *OpsErrorLogList) *OpsErrorLogList { - if result == nil || len(result.Errors) == 0 { - return result - } - - settings, err := s.GetOpsAdvancedSettings(ctx) - if err != nil || settings == nil { - // If we can't get settings, return unfiltered (fail open) - return result - } - - filtered := make([]*OpsErrorLog, 0, len(result.Errors)) - for _, errLog := range result.Errors { - if shouldFilterErrorLog(settings, errLog) { - continue // Skip this error - } - filtered = append(filtered, errLog) - } - - // Update total count to reflect filtered results - result.Errors = filtered - result.Total = len(filtered) - return result -} - -// shouldFilterErrorLog determines if an error log should be filtered based on settings. -func shouldFilterErrorLog(settings *OpsAdvancedSettings, errLog *OpsErrorLog) bool { - if settings == nil || errLog == nil { - return false - } - - msgLower := strings.ToLower(errLog.Message) - - // Check if count_tokens errors should be ignored - if settings.IgnoreCountTokensErrors && strings.Contains(errLog.RequestPath, "/count_tokens") { - return true - } - - // Check if context canceled errors should be ignored - if settings.IgnoreContextCanceled && strings.Contains(msgLower, "context canceled") { - return true - } - - // Check if "no available accounts" errors should be ignored - if settings.IgnoreNoAvailableAccounts && strings.Contains(msgLower, "no available accounts") { - return true - } - - return false -} - func (s *OpsService) GetErrorLogByID(ctx context.Context, id int64) (*OpsErrorLogDetail, error) { if err := s.RequireMonitoringEnabled(ctx); err != nil { return nil, err diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index 4ec560a4..0ac54db6 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -965,6 +965,7 @@ export type OpsErrorListQueryParams = { q?: string status_codes?: string + status_codes_other?: string } // Legacy unified endpoints diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 993a18c2..936d6bfa 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2009,6 +2009,11 @@ export default { // Error Log errorLog: { timeId: 'Time / ID', + commonErrors: { + contextDeadlineExceeded: 'context deadline exceeded', + connectionRefused: 'connection refused', + rateLimit: 'rate limit' + }, time: 'Time', type: 'Type', context: 'Context', @@ -2038,12 +2043,64 @@ export default { requestErrors: 'Request Errors', unresolved: 'Unresolved', resolved: 'Resolved', + viewErrors: 'Errors', + viewExcluded: 'Excluded', + statusCodeOther: 'Other', + owner: { + provider: 'Provider', + client: 'Client', + platform: 'Platform' + }, + phase: { + request: 'Request', + auth: 'Auth', + routing: 'Routing', + upstream: 'Upstream', + network: 'Network', + internal: 'Internal' + }, total: 'Total:', searchPlaceholder: 'Search request_id / client_request_id / message', accountIdPlaceholder: 'account_id' }, // Error Detail Modal errorDetail: { + title: 'Error Detail', + titleWithId: 'Error #{id}', + noErrorSelected: 'No error selected.', + resolution: 'Resolved:', + pinnedToOriginalAccountId: 'Pinned to original account_id', + missingUpstreamRequestBody: 'Missing upstream request body', + failedToLoadRetryHistory: 'Failed to load retry history', + failedToUpdateResolvedStatus: 'Failed to update resolved status', + unsupportedRetryMode: 'Unsupported retry mode', + classificationKeys: { + phase: 'Phase', + owner: 'Owner', + source: 'Source', + retryable: 'Retryable', + resolvedAt: 'Resolved At', + resolvedBy: 'Resolved By', + resolvedRetryId: 'Resolved Retry', + retryCount: 'Retry Count' + }, + upstreamKeys: { + status: 'Status', + message: 'Message', + detail: 'Detail', + upstreamErrors: 'Upstream Errors' + }, + upstreamEvent: { + account: 'Account', + status: 'Status', + requestId: 'Request ID' + }, + retryMeta: { + http: 'HTTP', + used: 'Used', + success: 'Success', + pinned: 'Pinned' + }, loading: 'Loading…', requestId: 'Request ID', time: 'Time', @@ -2053,6 +2110,8 @@ export default { basicInfo: 'Basic Info', platform: 'Platform', model: 'Model', + group: 'Group', + account: 'Account', latency: 'Request Duration', ttft: 'TTFT', businessLimited: 'Business Limited', @@ -2083,6 +2142,7 @@ export default { retryNote1: 'Retry will use the same request body and parameters', retryNote2: 'If the original request failed due to account issues, pinned retry may still fail', retryNote3: 'Client retry will reselect an account', + retryNote4: 'You can force retry for non-retryable errors, but it is not recommended', confirmRetryMessage: 'Confirm retry this request?', confirmRetryHint: 'Will resend with the same request parameters', forceRetry: 'I understand and want to force retry', @@ -2337,7 +2397,11 @@ export default { lockKeyRequired: 'Distributed lock key is required when lock is enabled', lockKeyPrefix: 'Distributed lock key must start with "{prefix}"', lockKeyHint: 'Recommended: start with "{prefix}" to avoid conflicts', - lockTtlRange: 'Distributed lock TTL must be between 1 and 86400 seconds' + lockTtlRange: 'Distributed lock TTL must be between 1 and 86400 seconds', + slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', + ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', + requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', + upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' } }, email: { @@ -2420,9 +2484,28 @@ export default { aggregation: 'Pre-aggregation Tasks', enableAggregation: 'Enable Pre-aggregation', aggregationHint: 'Pre-aggregation improves query performance for long time windows', + errorFiltering: 'Error Filtering', + ignoreCountTokensErrors: 'Ignore count_tokens errors', + ignoreCountTokensErrorsHint: 'When enabled, errors from count_tokens requests will not be written to the error log.', + ignoreContextCanceled: 'Ignore client disconnect errors', + ignoreContextCanceledHint: 'When enabled, client disconnect (context canceled) errors will not be written to the error log.', + ignoreNoAvailableAccounts: 'Ignore no available accounts errors', + ignoreNoAvailableAccountsHint: 'When enabled, "No available accounts" errors will not be written to the error log (not recommended; usually a config issue).', + autoRefresh: 'Auto Refresh', + enableAutoRefresh: 'Enable auto refresh', + enableAutoRefreshHint: 'Automatically refresh dashboard data at a fixed interval.', + refreshInterval: 'Refresh Interval', + refreshInterval15s: '15 seconds', + refreshInterval30s: '30 seconds', + refreshInterval60s: '60 seconds', + autoRefreshCountdown: 'Auto refresh: {seconds}s', validation: { title: 'Please fix the following issues', - retentionDaysRange: 'Retention days must be between 1-365 days' + retentionDaysRange: 'Retention days must be between 1-365 days', + slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', + ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', + requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', + upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' } }, concurrency: { diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index fcc84b19..85270e02 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2153,6 +2153,11 @@ export default { // Error Log errorLog: { timeId: '时间 / ID', + commonErrors: { + contextDeadlineExceeded: '请求超时', + connectionRefused: '连接被拒绝', + rateLimit: '触发限流' + }, time: '时间', type: '类型', context: '上下文', @@ -2182,12 +2187,64 @@ export default { requestErrors: '请求错误', unresolved: '未解决', resolved: '已解决', + viewErrors: '错误', + viewExcluded: '排除项', + statusCodeOther: '其他', + owner: { + provider: '服务商', + client: '客户端', + platform: '平台' + }, + phase: { + request: '请求', + auth: '认证', + routing: '路由', + upstream: '上游', + network: '网络', + internal: '内部' + }, total: '总计:', searchPlaceholder: '搜索 request_id / client_request_id / message', accountIdPlaceholder: 'account_id' }, // Error Detail Modal errorDetail: { + title: '错误详情', + titleWithId: '错误 #{id}', + noErrorSelected: '未选择错误。', + resolution: '已解决:', + pinnedToOriginalAccountId: '固定到原 account_id', + missingUpstreamRequestBody: '缺少上游请求体', + failedToLoadRetryHistory: '加载重试历史失败', + failedToUpdateResolvedStatus: '更新解决状态失败', + unsupportedRetryMode: '不支持的重试模式', + classificationKeys: { + phase: '阶段', + owner: '归属方', + source: '来源', + retryable: '可重试', + resolvedAt: '解决时间', + resolvedBy: '解决人', + resolvedRetryId: '解决重试ID', + retryCount: '重试次数' + }, + upstreamKeys: { + status: '状态码', + message: '消息', + detail: '详情', + upstreamErrors: '上游错误列表' + }, + upstreamEvent: { + account: '账号', + status: '状态码', + requestId: '请求ID' + }, + retryMeta: { + http: 'HTTP', + used: '使用账号', + success: '成功', + pinned: '固定账号' + }, loading: '加载中…', requestId: '请求 ID', time: '时间', @@ -2197,6 +2254,8 @@ export default { basicInfo: '基本信息', platform: '平台', model: '模型', + group: '分组', + account: '账号', latency: '请求时长', ttft: 'TTFT', businessLimited: '业务限制', @@ -2227,6 +2286,7 @@ export default { retryNote1: '重试会使用相同的请求体和参数', retryNote2: '如果原请求失败是因为账号问题,固定重试可能仍会失败', retryNote3: '客户端重试会重新选择账号', + retryNote4: '对不可重试的错误可以强制重试,但不推荐', confirmRetryMessage: '确认要重试该请求吗?', confirmRetryHint: '将使用相同的请求参数重新发送', forceRetry: '我已确认并理解强制重试风险', @@ -2481,7 +2541,11 @@ export default { lockKeyRequired: '启用分布式锁时必须填写 Lock Key', lockKeyPrefix: '分布式锁 Key 必须以「{prefix}」开头', lockKeyHint: '建议以「{prefix}」开头以避免冲突', - lockTtlRange: '分布式锁 TTL 必须在 1 到 86400 秒之间' + lockTtlRange: '分布式锁 TTL 必须在 1 到 86400 秒之间', + slaMinPercentRange: 'SLA 最低值必须在 0-100 之间', + ttftP99MaxRange: 'TTFT P99 最大值必须大于或等于 0', + requestErrorRateMaxRange: '请求错误率最大值必须在 0-100 之间', + upstreamErrorRateMaxRange: '上游错误率最大值必须在 0-100 之间' } }, email: { @@ -2564,9 +2628,28 @@ export default { aggregation: '预聚合任务', enableAggregation: '启用预聚合任务', aggregationHint: '预聚合可提升长时间窗口查询性能', + errorFiltering: '错误过滤', + ignoreCountTokensErrors: '忽略 count_tokens 错误', + ignoreCountTokensErrorsHint: '启用后,count_tokens 请求的错误将不会写入错误日志。', + ignoreContextCanceled: '忽略客户端断连错误', + ignoreContextCanceledHint: '启用后,客户端主动断开连接(context canceled)的错误将不会写入错误日志。', + ignoreNoAvailableAccounts: '忽略无可用账号错误', + ignoreNoAvailableAccountsHint: '启用后,“No available accounts” 错误将不会写入错误日志(不推荐,这通常是配置问题)。', + autoRefresh: '自动刷新', + enableAutoRefresh: '启用自动刷新', + enableAutoRefreshHint: '自动刷新仪表板数据,启用后会定期拉取最新数据。', + refreshInterval: '刷新间隔', + refreshInterval15s: '15 秒', + refreshInterval30s: '30 秒', + refreshInterval60s: '60 秒', + autoRefreshCountdown: '自动刷新:{seconds}s', validation: { title: '请先修正以下问题', - retentionDaysRange: '保留天数必须在1-365天之间' + retentionDaysRange: '保留天数必须在1-365天之间', + slaMinPercentRange: 'SLA最低百分比必须在0-100之间', + ttftP99MaxRange: 'TTFT P99最大值必须大于等于0', + requestErrorRateMaxRange: '请求错误率最大值必须在0-100之间', + upstreamErrorRateMaxRange: '上游错误率最大值必须在0-100之间' } }, concurrency: { diff --git a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue index f92c6c50..c50524ac 100644 --- a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue +++ b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue @@ -826,7 +826,7 @@ function handleToolbarRefresh() { - 自动刷新: {{ props.autoRefreshCountdown }}s + {{ t('admin.ops.settings.autoRefreshCountdown', { seconds: props.autoRefreshCountdown }) }} @@ -1084,11 +1084,11 @@ function handleToolbarRefresh() {
{{ displayRealTimeQps.toFixed(1) }} - QPS + {{ t('admin.ops.qps') }}
{{ displayRealTimeTps.toFixed(1) }} - TPS + {{ t('admin.ops.tps') }}
@@ -1101,11 +1101,11 @@ function handleToolbarRefresh() {
{{ realtimeQpsPeakLabel }} - QPS + {{ t('admin.ops.qps') }}
{{ realtimeTpsPeakLabel }} - TPS + {{ t('admin.ops.tps') }}
@@ -1116,11 +1116,11 @@ function handleToolbarRefresh() {
{{ realtimeQpsAvgLabel }} - QPS + {{ t('admin.ops.qps') }}
{{ realtimeTpsAvgLabel }} - TPS + {{ t('admin.ops.tps') }}
@@ -1195,7 +1195,7 @@ function handleToolbarRefresh() {
- SLA + {{ t('admin.ops.sla') }}
@@ -1242,33 +1242,33 @@ function handleToolbarRefresh() {
{{ durationP99Ms ?? '-' }}
- ms (P99) + {{ t('admin.ops.msP99') }}
- P95: + {{ t('admin.ops.p95') }} {{ durationP95Ms ?? '-' }} - ms + {{ t('admin.ops.ms') }}
- P90: + {{ t('admin.ops.p90') }} {{ durationP90Ms ?? '-' }} - ms + {{ t('admin.ops.ms') }}
- P50: + {{ t('admin.ops.p50') }} {{ durationP50Ms ?? '-' }} - ms + {{ t('admin.ops.ms') }}
Avg: {{ durationAvgMs ?? '-' }} - ms + {{ t('admin.ops.ms') }}
Max: {{ durationMaxMs ?? '-' }} - ms + {{ t('admin.ops.ms') }}
@@ -1277,14 +1277,14 @@ function handleToolbarRefresh() {
- TTFT + {{ t('admin.ops.ttft') }}
@@ -1293,33 +1293,33 @@ function handleToolbarRefresh() {
{{ ttftP99Ms ?? '-' }}
- ms (P99) + {{ t('admin.ops.msP99') }}
- P95: + {{ t('admin.ops.p95') }} {{ ttftP95Ms ?? '-' }} - ms + {{ t('admin.ops.ms') }}
- P90: + {{ t('admin.ops.p90') }} {{ ttftP90Ms ?? '-' }} - ms + {{ t('admin.ops.ms') }}
- P50: + {{ t('admin.ops.p50') }} {{ ttftP50Ms ?? '-' }} - ms + {{ t('admin.ops.ms') }}
Avg: {{ ttftAvgMs ?? '-' }} - ms + {{ t('admin.ops.ms') }}
Max: {{ ttftMaxMs ?? '-' }} - ms + {{ t('admin.ops.ms') }}
@@ -1384,7 +1384,7 @@ function handleToolbarRefresh() {
-
CPU
+
{{ t('admin.ops.cpu') }}
@@ -1398,7 +1398,7 @@ function handleToolbarRefresh() {
-
MEM
+
{{ t('admin.ops.mem') }}
@@ -1416,7 +1416,7 @@ function handleToolbarRefresh() {
-
DB
+
{{ t('admin.ops.db') }}
@@ -1433,7 +1433,7 @@ function handleToolbarRefresh() {
-
Redis
+
{{ t('admin.ops.redis') }}
diff --git a/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue b/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue index db9cb80c..88af52e5 100644 --- a/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue +++ b/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue @@ -15,9 +15,9 @@
- Resolved: + {{ t('admin.ops.errorDetail.resolution') }} - {{ (detail as any).resolved ? 'true' : 'false' }} + {{ (detail as any).resolved ? t('admin.ops.errorDetails.resolved') : t('admin.ops.errorDetails.unresolved') }}
@@ -28,7 +28,7 @@ :disabled="loading" @click="markResolved(true)" > - {{ t('admin.ops.errorDetail.markResolved') || 'Mark resolved' }} + {{ t('admin.ops.errorDetail.markResolved') }}
- - - - + + + +
@@ -102,7 +102,7 @@
-

{{ t('admin.ops.errorDetail.suggestion') || 'Suggestion' }}

+

{{ t('admin.ops.errorDetail.suggestion') }}

{{ handlingSuggestion }}
@@ -110,41 +110,41 @@
-

{{ t('admin.ops.errorDetail.classification') || 'Classification' }}

+

{{ t('admin.ops.errorDetail.classification') }}

-
phase
+
{{ t('admin.ops.errorDetail.classificationKeys.phase') }}
{{ detail.phase || '—' }}
-
owner
+
{{ t('admin.ops.errorDetail.classificationKeys.owner') }}
{{ (detail as any).error_owner || '—' }}
-
source
+
{{ t('admin.ops.errorDetail.classificationKeys.source') }}
{{ (detail as any).error_source || '—' }}
-
retryable
-
{{ (detail as any).is_retryable ? '✓' : '✗' }}
+
{{ t('admin.ops.errorDetail.classificationKeys.retryable') }}
+
{{ (detail as any).is_retryable ? t('common.yes') : t('common.no') }}
-
resolved_at
+
{{ t('admin.ops.errorDetail.classificationKeys.resolvedAt') }}
{{ (detail as any).resolved_at || '—' }}
-
resolved_by
+
{{ t('admin.ops.errorDetail.classificationKeys.resolvedBy') }}
{{ (detail as any).resolved_by_user_id ?? '—' }}
-
resolved_retry_id
+
{{ t('admin.ops.errorDetail.classificationKeys.resolvedRetryId') }}
{{ (detail as any).resolved_retry_id ?? '—' }}
-
retry_count
+
{{ t('admin.ops.errorDetail.classificationKeys.retryCount') }}
{{ (detail as any).retry_count ?? '—' }}
@@ -165,7 +165,7 @@
{{ t('admin.ops.errorDetail.group') }}
- + {{ detail.group_name || detail.group_id }} @@ -174,7 +174,7 @@
{{ t('admin.ops.errorDetail.account') }}
- + {{ detail.account_name || detail.account_id }} @@ -257,7 +257,7 @@
@@ -268,7 +268,7 @@
- pinned to original account_id + {{ t('admin.ops.errorDetail.pinnedToOriginalAccountId') }}
@@ -294,13 +294,13 @@
-
status
+
{{ t('admin.ops.errorDetail.upstreamKeys.status') }}
{{ detail.upstream_status_code != null ? detail.upstream_status_code : '—' }}
-
message
+
{{ t('admin.ops.errorDetail.upstreamKeys.message') }}
{{ detail.upstream_error_message || '—' }}
@@ -308,14 +308,14 @@
-
detail
+
{{ t('admin.ops.errorDetail.upstreamKeys.detail') }}
{{ prettyJSON(detail.upstream_error_detail) }}
-
upstream_errors
+
{{ t('admin.ops.errorDetail.upstreamKeys.upstreamErrors') }}
{{ t('admin.ops.errorDetail.retryUpstream') }} #{{ idx + 1 }} @@ -346,15 +346,15 @@
- account: - + {{ t('admin.ops.errorDetail.upstreamEvent.account') }}: + {{ ev.account_name || ev.account_id }}
-
status: {{ ev.upstream_status_code ?? '—' }}
+
{{ t('admin.ops.errorDetail.upstreamEvent.status') }}: {{ ev.upstream_status_code ?? '—' }}
- request_id: {{ ev.upstream_request_id || '—' }} + {{ t('admin.ops.errorDetail.upstreamEvent.requestId') }}: {{ ev.upstream_request_id || '—' }}
@@ -403,7 +403,7 @@
-
{{ t('admin.ops.errorDetail.retryHistory') || 'Retry History' }}
+
{{ t('admin.ops.errorDetail.retryHistory') }}
@@ -415,14 +415,14 @@
-
{{ t('admin.ops.errorDetail.compareA') || 'Compare A' }}
+
{{ t('admin.ops.errorDetail.compareA') }}
-
{{ t('admin.ops.errorDetail.compareB') || 'Compare B' }}
+
{{ t('admin.ops.errorDetail.compareB') }}
- {{ t('admin.ops.errorDetail.forceRetry') || 'I understand and want to force retry' }} + {{ t('admin.ops.errorDetail.forceRetry') }}
@@ -578,11 +578,11 @@ const compareB = ref(null) const pinnedAccountIdInput = ref('') const title = computed(() => { - if (!props.errorId) return 'Error Detail' - return `Error #${props.errorId}` + if (!props.errorId) return t('admin.ops.errorDetail.title') + return t('admin.ops.errorDetail.titleWithId', { id: String(props.errorId) }) }) -const emptyText = computed(() => 'No error selected.') +const emptyText = computed(() => t('admin.ops.errorDetail.noErrorSelected')) type UpstreamErrorEvent = { at_unix_ms?: number @@ -630,20 +630,20 @@ const handlingSuggestion = computed(() => { if (owner === 'provider' && phase === 'upstream') { if (retryHistory.value.some((r) => r.success === true) && d.resolved) { - return t('admin.ops.errorDetail.suggestUpstreamResolved') || '✓ Upstream error resolved by retry; no action needed.' + return t('admin.ops.errorDetail.suggestUpstreamResolved') } - return t('admin.ops.errorDetail.suggestUpstream') || 'Upstream instability: consider checking upstream account status, switching accounts, or retrying.' + return t('admin.ops.errorDetail.suggestUpstream') } if (owner === 'client' && phase === 'request') { - return t('admin.ops.errorDetail.suggestRequest') || 'Client request validation error: contact customer to fix request parameters.' + return t('admin.ops.errorDetail.suggestRequest') } if (owner === 'client' && phase === 'auth') { - return t('admin.ops.errorDetail.suggestAuth') || 'Auth failed: verify API key/credentials.' + return t('admin.ops.errorDetail.suggestAuth') } if (owner === 'platform') { - return t('admin.ops.errorDetail.suggestPlatform') || 'Platform error: prioritize investigation and fix.' + return t('admin.ops.errorDetail.suggestPlatform') } - return t('admin.ops.errorDetail.suggestGeneric') || 'See details for more context.' + return t('admin.ops.errorDetail.suggestGeneric') }) async function fetchDetail(id: number) { @@ -709,7 +709,7 @@ async function loadRetryHistory() { retryHistory.value = [] compareA.value = null compareB.value = null - appStore.showError(err?.message || 'Failed to load retry history') + appStore.showError(err?.message || t('admin.ops.errorDetail.failedToLoadRetryHistory')) } finally { retryHistoryLoading.value = false } @@ -732,7 +732,7 @@ const responseTabHint = computed(() => { if (succeeded?.response_preview) { return t('admin.ops.errorDetail.responseHintSucceeded', { id: String(succeeded.id) }) || `Showing succeeded retry response_preview (#${succeeded.id})` } - return t('admin.ops.errorDetail.responseHintFallback') || 'No succeeded retry found; showing stored error_body' + return t('admin.ops.errorDetail.responseHintFallback') }) async function markResolved(resolved: boolean) { @@ -745,9 +745,9 @@ async function markResolved(resolved: boolean) { await opsAPI.updateRequestErrorResolved(props.errorId, resolved) } await fetchDetail(props.errorId) - appStore.showSuccess(resolved ? (t('admin.ops.errorDetails.resolved') || 'Resolved') : (t('admin.ops.errorDetails.unresolved') || 'Unresolved')) + appStore.showSuccess(resolved ? t('admin.ops.errorDetails.resolved') : t('admin.ops.errorDetails.unresolved')) } catch (err: any) { - appStore.showError(err?.message || 'Failed to update resolved status') + appStore.showError(err?.message || t('admin.ops.errorDetail.failedToUpdateResolvedStatus')) } } @@ -755,7 +755,7 @@ const retryConfirmMessage = computed(() => { const mode = pendingRetryMode.value const retryable = !!(detail.value as any)?.is_retryable if (!retryable) { - return t('admin.ops.errorDetail.forceRetryHint') || 'This error is not recommended to retry. Check the box to force retry.' + return t('admin.ops.errorDetail.forceRetryHint') } if (mode === 'upstream') { return t('admin.ops.errorDetail.confirmRetryMessage') @@ -781,7 +781,7 @@ async function runConfirmedRetry() { const mode = pendingRetryMode.value const retryable = !!(detail.value as any)?.is_retryable if (!retryable && !forceRetryAck.value) { - appStore.showError(t('admin.ops.errorDetail.forceRetryNeedAck') || 'Please confirm you want to force retry') + appStore.showError(t('admin.ops.errorDetail.forceRetryNeedAck')) return } @@ -799,7 +799,7 @@ async function runConfirmedRetry() { if (mode === 'client') { res = await opsAPI.retryRequestErrorClient(props.errorId) } else { - throw new Error('Unsupported retry mode') + throw new Error(t('admin.ops.errorDetail.unsupportedRetryMode')) } } diff --git a/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue b/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue index 4ff2ec0f..8c6c116b 100644 --- a/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue +++ b/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue @@ -29,7 +29,7 @@ const page = ref(1) const pageSize = ref(20) const q = ref('') -const statusCode = ref(null) +const statusCode = ref(null) const phase = ref('') const errorOwner = ref('') const resolvedStatus = ref('unresolved') @@ -44,16 +44,17 @@ const statusCodeSelectOptions = computed(() => { const codes = [400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504, 529] return [ { value: null, label: t('common.all') }, - ...codes.map((c) => ({ value: c, label: String(c) })) + ...codes.map((c) => ({ value: c, label: String(c) })), + { value: 'other', label: t('admin.ops.errorDetails.statusCodeOther') || 'Other' } ] }) const ownerSelectOptions = computed(() => { return [ { value: '', label: t('common.all') }, - { value: 'provider', label: 'provider' }, - { value: 'client', label: 'client' }, - { value: 'platform', label: 'platform' } + { value: 'provider', label: t('admin.ops.errorDetails.owner.provider') || 'provider' }, + { value: 'client', label: t('admin.ops.errorDetails.owner.client') || 'client' }, + { value: 'platform', label: t('admin.ops.errorDetails.owner.platform') || 'platform' } ] }) @@ -76,12 +77,12 @@ const viewModeSelectOptions = computed(() => { const phaseSelectOptions = computed(() => { const options = [ { value: '', label: t('common.all') }, - { value: 'request', label: 'request' }, - { value: 'auth', label: 'auth' }, - { value: 'routing', label: 'routing' }, - { value: 'upstream', label: 'upstream' }, - { value: 'network', label: 'network' }, - { value: 'internal', label: 'internal' } + { value: 'request', label: t('admin.ops.errorDetails.phase.request') || 'request' }, + { value: 'auth', label: t('admin.ops.errorDetails.phase.auth') || 'auth' }, + { value: 'routing', label: t('admin.ops.errorDetails.phase.routing') || 'routing' }, + { value: 'upstream', label: t('admin.ops.errorDetails.phase.upstream') || 'upstream' }, + { value: 'network', label: t('admin.ops.errorDetails.phase.network') || 'network' }, + { value: 'internal', label: t('admin.ops.errorDetails.phase.internal') || 'internal' } ] return options }) @@ -107,7 +108,8 @@ async function fetchErrorLogs() { if (typeof props.groupId === 'number' && props.groupId > 0) params.group_id = props.groupId if (q.value.trim()) params.q = q.value.trim() - if (typeof statusCode.value === 'number') params.status_codes = String(statusCode.value) + if (statusCode.value === 'other') params.status_codes_other = '1' + else if (typeof statusCode.value === 'number') params.status_codes = String(statusCode.value) const phaseVal = String(phase.value || '').trim() if (phaseVal) params.phase = phaseVal diff --git a/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue b/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue index 3e7424df..76922524 100644 --- a/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue +++ b/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue @@ -93,7 +93,7 @@ - + {{ log.group_name || '-' }} @@ -103,7 +103,7 @@ - + {{ log.account_name || '-' }} @@ -195,8 +195,8 @@ function getTypeBadge(log: OpsErrorLog): { label: string; className: string } { return { label: t('admin.ops.errorLog.typeInternal'), className: 'bg-gray-100 text-gray-800 ring-gray-600/20 dark:bg-dark-700 dark:text-gray-200 dark:ring-dark-500/40' } } - const fallback = phase || owner || 'unknown' - return { label: fallback, className: 'bg-gray-50 text-gray-700 ring-gray-600/10 dark:bg-dark-900 dark:text-gray-300 dark:ring-dark-700' } + const fallback = phase || owner || t('common.unknown') + return { label: fallback, className: 'bg-gray-50 text-gray-700 ring-gray-600/10 dark:bg-dark-900 dark:text-gray-300 dark:ring-dark-700' } } interface Props { @@ -238,9 +238,9 @@ function formatSmartMessage(msg: string): string { } } - if (msg.includes('context deadline exceeded')) return 'context deadline exceeded' - if (msg.includes('connection refused')) return 'connection refused' - if (msg.toLowerCase().includes('rate limit')) return 'rate limit' + if (msg.includes('context deadline exceeded')) return t('admin.ops.errorLog.commonErrors.contextDeadlineExceeded') + if (msg.includes('connection refused')) return t('admin.ops.errorLog.commonErrors.connectionRefused') + if (msg.toLowerCase().includes('rate limit')) return t('admin.ops.errorLog.commonErrors.rateLimit') return msg.length > 200 ? msg.substring(0, 200) + '...' : msg } diff --git a/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue b/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue index d64ae390..d9bcbd51 100644 --- a/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue +++ b/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue @@ -50,22 +50,22 @@ function validateRuntimeSettings(settings: OpsAlertRuntimeSettings): ValidationR if (thresholds) { if (thresholds.sla_percent_min != null) { if (!Number.isFinite(thresholds.sla_percent_min) || thresholds.sla_percent_min < 0 || thresholds.sla_percent_min > 100) { - errors.push('SLA 最低值必须在 0-100 之间') + errors.push(t('admin.ops.runtime.validation.slaMinPercentRange')) } } if (thresholds.ttft_p99_ms_max != null) { if (!Number.isFinite(thresholds.ttft_p99_ms_max) || thresholds.ttft_p99_ms_max < 0) { - errors.push('TTFT P99 最大值必须大于或等于 0') + errors.push(t('admin.ops.runtime.validation.ttftP99MaxRange')) } } if (thresholds.request_error_rate_percent_max != null) { if (!Number.isFinite(thresholds.request_error_rate_percent_max) || thresholds.request_error_rate_percent_max < 0 || thresholds.request_error_rate_percent_max > 100) { - errors.push('请求错误率最大值必须在 0-100 之间') + errors.push(t('admin.ops.runtime.validation.requestErrorRateMaxRange')) } } if (thresholds.upstream_error_rate_percent_max != null) { if (!Number.isFinite(thresholds.upstream_error_rate_percent_max) || thresholds.upstream_error_rate_percent_max < 0 || thresholds.upstream_error_rate_percent_max > 100) { - errors.push('上游错误率最大值必须在 0-100 之间') + errors.push(t('admin.ops.runtime.validation.upstreamErrorRateMaxRange')) } } } @@ -329,12 +329,12 @@ onMounted(() => {
-
指标阈值配置
-

配置各项指标的告警阈值。超出阈值的指标将在看板上以红色显示。

+
{{ t('admin.ops.runtime.metricThresholds') }}
+

{{ t('admin.ops.runtime.metricThresholdsHint') }}

-
SLA 最低值 (%)
+
{{ t('admin.ops.runtime.slaMinPercent') }}
{ class="input" placeholder="99.5" /> -

SLA 低于此值时将显示为红色

+

{{ t('admin.ops.runtime.slaMinPercentHint') }}

-
TTFT P99 最大值 (ms)
+
{{ t('admin.ops.runtime.ttftP99MaxMs') }}
{ class="input" placeholder="500" /> -

TTFT P99 高于此值时将显示为红色

+

{{ t('admin.ops.runtime.ttftP99MaxMsHint') }}

-
请求错误率最大值 (%)
+
{{ t('admin.ops.runtime.requestErrorRateMaxPercent') }}
{ class="input" placeholder="5" /> -

请求错误率高于此值时将显示为红色

+

{{ t('admin.ops.runtime.requestErrorRateMaxPercentHint') }}

-
上游错误率最大值 (%)
+
{{ t('admin.ops.runtime.upstreamErrorRateMaxPercent') }}
{ class="input" placeholder="5" /> -

上游错误率高于此值时将显示为红色

+

{{ t('admin.ops.runtime.upstreamErrorRateMaxPercentHint') }}

diff --git a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue index 4d737c1b..53ab6683 100644 --- a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue +++ b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue @@ -157,16 +157,16 @@ const validation = computed(() => { // 验证指标阈值 if (metricThresholds.value.sla_percent_min != null && (metricThresholds.value.sla_percent_min < 0 || metricThresholds.value.sla_percent_min > 100)) { - errors.push('SLA最低百分比必须在0-100之间') + errors.push(t('admin.ops.settings.validation.slaMinPercentRange')) } if (metricThresholds.value.ttft_p99_ms_max != null && metricThresholds.value.ttft_p99_ms_max < 0) { - errors.push('TTFT P99最大值必须大于等于0') + errors.push(t('admin.ops.settings.validation.ttftP99MaxRange')) } if (metricThresholds.value.request_error_rate_percent_max != null && (metricThresholds.value.request_error_rate_percent_max < 0 || metricThresholds.value.request_error_rate_percent_max > 100)) { - errors.push('请求错误率最大值必须在0-100之间') + errors.push(t('admin.ops.settings.validation.requestErrorRateMaxRange')) } if (metricThresholds.value.upstream_error_rate_percent_max != null && (metricThresholds.value.upstream_error_rate_percent_max < 0 || metricThresholds.value.upstream_error_rate_percent_max > 100)) { - errors.push('上游错误率最大值必须在0-100之间') + errors.push(t('admin.ops.settings.validation.upstreamErrorRateMaxRange')) } return { valid: errors.length === 0, errors } @@ -472,15 +472,15 @@ async function saveAllSettings() {
- +
-
错误过滤
+
{{ t('admin.ops.settings.errorFiltering') }}
- +

- 启用后,count_tokens 请求的错误将不会写入错误日志 + {{ t('admin.ops.settings.ignoreCountTokensErrorsHint') }}

@@ -488,9 +488,9 @@ async function saveAllSettings() {
- +

- 启用后,客户端主动断开连接(context canceled)的错误将不会写入错误日志 + {{ t('admin.ops.settings.ignoreContextCanceledHint') }}

@@ -498,37 +498,37 @@ async function saveAllSettings() {
- +

- 启用后,"No available accounts" 错误将不会写入错误日志(不推荐,这通常是配置问题) + {{ t('admin.ops.settings.ignoreNoAvailableAccountsHint') }}

- +
-
自动刷新
+
{{ t('admin.ops.settings.autoRefresh') }}
- +

- 自动刷新仪表板数据,启用后会定期拉取最新数据 + {{ t('admin.ops.settings.enableAutoRefreshHint') }}

- +