From af9c4a7dd01d1c4741a3b0b0b4ae2a9a05b0332e Mon Sep 17 00:00:00 2001 From: Peter <1tRq4X287b7W7sfKf9GsWI+Peter@noreply.cnb.cool> Date: Fri, 13 Mar 2026 04:11:58 +0800 Subject: [PATCH 1/2] feat(ops): make openai token stats optional --- backend/internal/service/ops_settings.go | 1 + .../service/ops_settings_advanced_test.go | 46 ++++++++++++++ .../internal/service/ops_settings_models.go | 1 + frontend/src/api/admin/ops.ts | 1 + frontend/src/i18n/locales/en.ts | 3 + frontend/src/i18n/locales/zh.ts | 3 + frontend/src/views/admin/ops/OpsDashboard.vue | 21 ++++--- .../components/OpsOpenAITokenStatsCard.vue | 62 ++++++++++--------- .../ops/components/OpsSettingsDialog.vue | 15 +++++ .../__tests__/OpsOpenAITokenStatsCard.spec.ts | 17 +++++ 10 files changed, 134 insertions(+), 36 deletions(-) create mode 100644 backend/internal/service/ops_settings_advanced_test.go diff --git a/backend/internal/service/ops_settings.go b/backend/internal/service/ops_settings.go index 7514cc80..a8f6e95d 100644 --- a/backend/internal/service/ops_settings.go +++ b/backend/internal/service/ops_settings.go @@ -371,6 +371,7 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings { IgnoreCountTokensErrors: true, // count_tokens 404 是预期行为,默认忽略 IgnoreContextCanceled: true, // Default to true - client disconnects are not errors IgnoreNoAvailableAccounts: false, // Default to false - this is a real routing issue + DisplayOpenAITokenStats: false, AutoRefreshEnabled: false, AutoRefreshIntervalSec: 30, } diff --git a/backend/internal/service/ops_settings_advanced_test.go b/backend/internal/service/ops_settings_advanced_test.go new file mode 100644 index 00000000..d5b09604 --- /dev/null +++ b/backend/internal/service/ops_settings_advanced_test.go @@ -0,0 +1,46 @@ +package service + +import ( + "context" + "testing" +) + +func TestGetOpsAdvancedSettings_DefaultHidesOpenAITokenStats(t *testing.T) { + repo := newRuntimeSettingRepoStub() + svc := &OpsService{settingRepo: repo} + + cfg, err := svc.GetOpsAdvancedSettings(context.Background()) + if err != nil { + t.Fatalf("GetOpsAdvancedSettings() error = %v", err) + } + if cfg.DisplayOpenAITokenStats { + t.Fatalf("DisplayOpenAITokenStats = true, want false by default") + } + if repo.setCalls != 1 { + t.Fatalf("expected defaults to be persisted once, got %d", repo.setCalls) + } +} + +func TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility(t *testing.T) { + repo := newRuntimeSettingRepoStub() + svc := &OpsService{settingRepo: repo} + + cfg := defaultOpsAdvancedSettings() + cfg.DisplayOpenAITokenStats = true + + updated, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg) + if err != nil { + t.Fatalf("UpdateOpsAdvancedSettings() error = %v", err) + } + if !updated.DisplayOpenAITokenStats { + t.Fatalf("DisplayOpenAITokenStats = false, want true") + } + + reloaded, err := svc.GetOpsAdvancedSettings(context.Background()) + if err != nil { + t.Fatalf("GetOpsAdvancedSettings() after update error = %v", err) + } + if !reloaded.DisplayOpenAITokenStats { + t.Fatalf("reloaded DisplayOpenAITokenStats = false, want true") + } +} diff --git a/backend/internal/service/ops_settings_models.go b/backend/internal/service/ops_settings_models.go index 8b5359e3..9a6dff9a 100644 --- a/backend/internal/service/ops_settings_models.go +++ b/backend/internal/service/ops_settings_models.go @@ -98,6 +98,7 @@ type OpsAdvancedSettings struct { IgnoreContextCanceled bool `json:"ignore_context_canceled"` IgnoreNoAvailableAccounts bool `json:"ignore_no_available_accounts"` IgnoreInvalidApiKeyErrors bool `json:"ignore_invalid_api_key_errors"` + DisplayOpenAITokenStats bool `json:"display_openai_token_stats"` AutoRefreshEnabled bool `json:"auto_refresh_enabled"` AutoRefreshIntervalSec int `json:"auto_refresh_interval_seconds"` } diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index b8d1691f..fb520fd6 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -841,6 +841,7 @@ export interface OpsAdvancedSettings { ignore_context_canceled: boolean ignore_no_available_accounts: boolean ignore_invalid_api_key_errors: boolean + display_openai_token_stats: boolean auto_refresh_enabled: boolean auto_refresh_interval_seconds: number } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 9fd0c006..6894daba 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3651,6 +3651,9 @@ export default { refreshInterval15s: '15 seconds', refreshInterval30s: '30 seconds', refreshInterval60s: '60 seconds', + dashboardCards: 'Dashboard Cards', + displayOpenAITokenStats: 'Display OpenAI token request stats', + displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.', autoRefreshCountdown: 'Auto refresh: {seconds}s', validation: { title: 'Please fix the following issues', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index d139cd34..fadabd39 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -3825,6 +3825,9 @@ export default { refreshInterval15s: '15 秒', refreshInterval30s: '30 秒', refreshInterval60s: '60 秒', + dashboardCards: '仪表盘卡片', + displayOpenAITokenStats: '展示 OpenAI Token 请求统计', + displayOpenAITokenStatsHint: '控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。', autoRefreshCountdown: '自动刷新:{seconds}s', validation: { title: '请先修正以下问题', diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index c9424f31..c5329b5d 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -85,7 +85,7 @@ -
+
{ loadThresholds() // Load auto refresh settings - await loadAutoRefreshSettings() + await loadDashboardAdvancedSettings() if (opsEnabled.value) { await fetchData() @@ -816,7 +823,7 @@ watch(autoRefreshEnabled, (enabled) => { // Reload auto refresh settings after settings dialog is closed watch(showSettingsDialog, async (show) => { if (!show) { - await loadAutoRefreshSettings() + await loadDashboardAdvancedSettings() } }) diff --git a/frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue b/frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue index 5b53555f..7f68594b 100644 --- a/frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue +++ b/frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue @@ -208,35 +208,39 @@ function onNextPage() { :description="t('admin.ops.openaiTokenStats.empty')" /> -
- - - - - - - - - - - - - - - - - - - - - - - -
{{ t('admin.ops.openaiTokenStats.table.model') }}{{ t('admin.ops.openaiTokenStats.table.requestCount') }}{{ t('admin.ops.openaiTokenStats.table.avgTokensPerSec') }}{{ t('admin.ops.openaiTokenStats.table.avgFirstTokenMs') }}{{ t('admin.ops.openaiTokenStats.table.totalOutputTokens') }}{{ t('admin.ops.openaiTokenStats.table.avgDurationMs') }}{{ t('admin.ops.openaiTokenStats.table.requestsWithFirstToken') }}
{{ row.model }}{{ formatInt(row.request_count) }}{{ formatRate(row.avg_tokens_per_sec) }}{{ formatRate(row.avg_first_token_ms) }}{{ formatInt(row.total_output_tokens) }}{{ formatInt(row.avg_duration_ms) }}{{ formatInt(row.requests_with_first_token) }}
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
{{ t('admin.ops.openaiTokenStats.table.model') }}{{ t('admin.ops.openaiTokenStats.table.requestCount') }}{{ t('admin.ops.openaiTokenStats.table.avgTokensPerSec') }}{{ t('admin.ops.openaiTokenStats.table.avgFirstTokenMs') }}{{ t('admin.ops.openaiTokenStats.table.totalOutputTokens') }}{{ t('admin.ops.openaiTokenStats.table.avgDurationMs') }}{{ t('admin.ops.openaiTokenStats.table.requestsWithFirstToken') }}
{{ row.model }}{{ formatInt(row.request_count) }}{{ formatRate(row.avg_tokens_per_sec) }}{{ formatRate(row.avg_first_token_ms) }}{{ formatInt(row.total_output_tokens) }}{{ formatInt(row.avg_duration_ms) }}{{ formatInt(row.requests_with_first_token) }}
+
+
{{ t('admin.ops.openaiTokenStats.totalModels', { total }) }}
diff --git a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue index 3bec6d0d..9a1d99e4 100644 --- a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue +++ b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue @@ -543,6 +543,21 @@ async function saveAllSettings() { />
+ + +
+
{{ t('admin.ops.settings.dashboardCards') }}
+ +
+
+ +

+ {{ t('admin.ops.settings.displayOpenAITokenStatsHint') }} +

+
+ +
+
diff --git a/frontend/src/views/admin/ops/components/__tests__/OpsOpenAITokenStatsCard.spec.ts b/frontend/src/views/admin/ops/components/__tests__/OpsOpenAITokenStatsCard.spec.ts index 3e95f460..5804e176 100644 --- a/frontend/src/views/admin/ops/components/__tests__/OpsOpenAITokenStatsCard.spec.ts +++ b/frontend/src/views/admin/ops/components/__tests__/OpsOpenAITokenStatsCard.spec.ts @@ -196,6 +196,23 @@ describe('OpsOpenAITokenStatsCard', () => { expect(wrapper.find('.empty-state').exists()).toBe(true) }) + it('数据表使用固定高度滚动容器,避免纵向无限增长', async () => { + mockGetOpenAITokenStats.mockResolvedValue(sampleResponse) + + const wrapper = mount(OpsOpenAITokenStatsCard, { + props: { refreshToken: 0 }, + global: { + stubs: { + Select: SelectStub, + EmptyState: EmptyStateStub, + }, + }, + }) + await flushPromises() + + expect(wrapper.find('.max-h-\\[420px\\]').exists()).toBe(true) + }) + it('接口异常时显示错误提示', async () => { mockGetOpenAITokenStats.mockRejectedValue(new Error('加载失败')) From 29b0e4a8a5c0b778583289c4b0fcc715fbd9cd9d Mon Sep 17 00:00:00 2001 From: Peter <1tRq4X287b7W7sfKf9GsWI+Peter@noreply.cnb.cool> Date: Fri, 13 Mar 2026 17:18:04 +0800 Subject: [PATCH 2/2] feat(ops): allow hiding alert events --- backend/internal/service/ops_settings.go | 3 +- .../service/ops_settings_advanced_test.go | 51 +++++++++++++++++++ .../internal/service/ops_settings_models.go | 1 + frontend/src/api/admin/ops.ts | 1 + frontend/src/i18n/locales/en.ts | 2 + frontend/src/i18n/locales/zh.ts | 2 + frontend/src/views/admin/ops/OpsDashboard.vue | 5 +- .../ops/components/OpsSettingsDialog.vue | 10 ++++ 8 files changed, 73 insertions(+), 2 deletions(-) diff --git a/backend/internal/service/ops_settings.go b/backend/internal/service/ops_settings.go index a8f6e95d..93815887 100644 --- a/backend/internal/service/ops_settings.go +++ b/backend/internal/service/ops_settings.go @@ -372,6 +372,7 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings { IgnoreContextCanceled: true, // Default to true - client disconnects are not errors IgnoreNoAvailableAccounts: false, // Default to false - this is a real routing issue DisplayOpenAITokenStats: false, + DisplayAlertEvents: true, AutoRefreshEnabled: false, AutoRefreshIntervalSec: 30, } @@ -439,7 +440,7 @@ func (s *OpsService) GetOpsAdvancedSettings(ctx context.Context) (*OpsAdvancedSe return nil, err } - cfg := &OpsAdvancedSettings{} + cfg := defaultOpsAdvancedSettings() if err := json.Unmarshal([]byte(raw), cfg); err != nil { return defaultCfg, nil } diff --git a/backend/internal/service/ops_settings_advanced_test.go b/backend/internal/service/ops_settings_advanced_test.go index d5b09604..06cc545b 100644 --- a/backend/internal/service/ops_settings_advanced_test.go +++ b/backend/internal/service/ops_settings_advanced_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "testing" ) @@ -16,6 +17,9 @@ func TestGetOpsAdvancedSettings_DefaultHidesOpenAITokenStats(t *testing.T) { if cfg.DisplayOpenAITokenStats { t.Fatalf("DisplayOpenAITokenStats = true, want false by default") } + if !cfg.DisplayAlertEvents { + t.Fatalf("DisplayAlertEvents = false, want true by default") + } if repo.setCalls != 1 { t.Fatalf("expected defaults to be persisted once, got %d", repo.setCalls) } @@ -27,6 +31,7 @@ func TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility(t *testing cfg := defaultOpsAdvancedSettings() cfg.DisplayOpenAITokenStats = true + cfg.DisplayAlertEvents = false updated, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg) if err != nil { @@ -35,6 +40,9 @@ func TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility(t *testing if !updated.DisplayOpenAITokenStats { t.Fatalf("DisplayOpenAITokenStats = false, want true") } + if updated.DisplayAlertEvents { + t.Fatalf("DisplayAlertEvents = true, want false") + } reloaded, err := svc.GetOpsAdvancedSettings(context.Background()) if err != nil { @@ -43,4 +51,47 @@ func TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility(t *testing if !reloaded.DisplayOpenAITokenStats { t.Fatalf("reloaded DisplayOpenAITokenStats = false, want true") } + if reloaded.DisplayAlertEvents { + t.Fatalf("reloaded DisplayAlertEvents = true, want false") + } +} + +func TestGetOpsAdvancedSettings_BackfillsNewDisplayFlagsFromDefaults(t *testing.T) { + repo := newRuntimeSettingRepoStub() + svc := &OpsService{settingRepo: repo} + + legacyCfg := map[string]any{ + "data_retention": map[string]any{ + "cleanup_enabled": false, + "cleanup_schedule": "0 2 * * *", + "error_log_retention_days": 30, + "minute_metrics_retention_days": 30, + "hourly_metrics_retention_days": 30, + }, + "aggregation": map[string]any{ + "aggregation_enabled": false, + }, + "ignore_count_tokens_errors": true, + "ignore_context_canceled": true, + "ignore_no_available_accounts": false, + "ignore_invalid_api_key_errors": false, + "auto_refresh_enabled": false, + "auto_refresh_interval_seconds": 30, + } + raw, err := json.Marshal(legacyCfg) + if err != nil { + t.Fatalf("marshal legacy config: %v", err) + } + repo.values[SettingKeyOpsAdvancedSettings] = string(raw) + + cfg, err := svc.GetOpsAdvancedSettings(context.Background()) + if err != nil { + t.Fatalf("GetOpsAdvancedSettings() error = %v", err) + } + if cfg.DisplayOpenAITokenStats { + t.Fatalf("DisplayOpenAITokenStats = true, want false default backfill") + } + if !cfg.DisplayAlertEvents { + t.Fatalf("DisplayAlertEvents = false, want true default backfill") + } } diff --git a/backend/internal/service/ops_settings_models.go b/backend/internal/service/ops_settings_models.go index 9a6dff9a..c8b9fcd1 100644 --- a/backend/internal/service/ops_settings_models.go +++ b/backend/internal/service/ops_settings_models.go @@ -99,6 +99,7 @@ type OpsAdvancedSettings struct { IgnoreNoAvailableAccounts bool `json:"ignore_no_available_accounts"` IgnoreInvalidApiKeyErrors bool `json:"ignore_invalid_api_key_errors"` DisplayOpenAITokenStats bool `json:"display_openai_token_stats"` + DisplayAlertEvents bool `json:"display_alert_events"` AutoRefreshEnabled bool `json:"auto_refresh_enabled"` AutoRefreshIntervalSec int `json:"auto_refresh_interval_seconds"` } diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index fb520fd6..11699c79 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -842,6 +842,7 @@ export interface OpsAdvancedSettings { ignore_no_available_accounts: boolean ignore_invalid_api_key_errors: boolean display_openai_token_stats: boolean + display_alert_events: boolean auto_refresh_enabled: boolean auto_refresh_interval_seconds: number } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 6894daba..a609436a 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3652,6 +3652,8 @@ export default { refreshInterval30s: '30 seconds', refreshInterval60s: '60 seconds', dashboardCards: 'Dashboard Cards', + displayAlertEvents: 'Display alert events', + displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.', displayOpenAITokenStats: 'Display OpenAI token request stats', displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.', autoRefreshCountdown: 'Auto refresh: {seconds}s', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index fadabd39..bada940e 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -3826,6 +3826,8 @@ export default { refreshInterval30s: '30 秒', refreshInterval60s: '60 秒', dashboardCards: '仪表盘卡片', + displayAlertEvents: '展示告警事件', + displayAlertEventsHint: '控制运维监控仪表盘中告警事件卡片是否显示,默认开启。', displayOpenAITokenStats: '展示 OpenAI Token 请求统计', displayOpenAITokenStatsHint: '控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。', autoRefreshCountdown: '自动刷新:{seconds}s', diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index c5329b5d..50bc5249 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -94,7 +94,7 @@ - +
{{ t('admin.ops.settings.dashboardCards') }}
+
+
+ +

+ {{ t('admin.ops.settings.displayAlertEventsHint') }} +

+
+ +
+