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') }}
+
+
+
+
+