Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
db1f6ded
Unverified
Commit
db1f6ded
authored
Mar 14, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 14, 2026
Browse files
Merge pull request #961 from 0xObjc/codex/ops-openai-token-visibility
feat(ops): make OpenAI token stats optional
parents
2e3e8687
29b0e4a8
Changes
10
Show whitespace changes
Inline
Side-by-side
backend/internal/service/ops_settings.go
View file @
db1f6ded
...
...
@@ -371,6 +371,8 @@ 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
,
DisplayAlertEvents
:
true
,
AutoRefreshEnabled
:
false
,
AutoRefreshIntervalSec
:
30
,
}
...
...
@@ -438,7 +440,7 @@ func (s *OpsService) GetOpsAdvancedSettings(ctx context.Context) (*OpsAdvancedSe
return
nil
,
err
}
cfg
:=
&
OpsAdvancedSettings
{}
cfg
:=
default
OpsAdvancedSettings
()
if
err
:=
json
.
Unmarshal
([]
byte
(
raw
),
cfg
);
err
!=
nil
{
return
defaultCfg
,
nil
}
...
...
backend/internal/service/ops_settings_advanced_test.go
0 → 100644
View file @
db1f6ded
package
service
import
(
"context"
"encoding/json"
"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
!
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
)
}
}
func
TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
svc
:=
&
OpsService
{
settingRepo
:
repo
}
cfg
:=
defaultOpsAdvancedSettings
()
cfg
.
DisplayOpenAITokenStats
=
true
cfg
.
DisplayAlertEvents
=
false
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"
)
}
if
updated
.
DisplayAlertEvents
{
t
.
Fatalf
(
"DisplayAlertEvents = true, want false"
)
}
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"
)
}
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"
)
}
}
backend/internal/service/ops_settings_models.go
View file @
db1f6ded
...
...
@@ -98,6 +98,8 @@ 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"`
DisplayAlertEvents
bool
`json:"display_alert_events"`
AutoRefreshEnabled
bool
`json:"auto_refresh_enabled"`
AutoRefreshIntervalSec
int
`json:"auto_refresh_interval_seconds"`
}
...
...
frontend/src/api/admin/ops.ts
View file @
db1f6ded
...
...
@@ -841,6 +841,8 @@ export interface OpsAdvancedSettings {
ignore_context_canceled
:
boolean
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
}
...
...
frontend/src/i18n/locales/en.ts
View file @
db1f6ded
...
...
@@ -3709,6 +3709,11 @@ export default {
refreshInterval15s
:
'
15 seconds
'
,
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
'
,
validation
:
{
title
:
'
Please fix the following issues
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
db1f6ded
...
...
@@ -3883,6 +3883,11 @@ export default {
refreshInterval15s
:
'
15 秒
'
,
refreshInterval30s
:
'
30 秒
'
,
refreshInterval60s
:
'
60 秒
'
,
dashboardCards
:
'
仪表盘卡片
'
,
displayAlertEvents
:
'
展示告警事件
'
,
displayAlertEventsHint
:
'
控制运维监控仪表盘中告警事件卡片是否显示,默认开启。
'
,
displayOpenAITokenStats
:
'
展示 OpenAI Token 请求统计
'
,
displayOpenAITokenStatsHint
:
'
控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。
'
,
autoRefreshCountdown
:
'
自动刷新:{seconds}s
'
,
validation
:
{
title
:
'
请先修正以下问题
'
,
...
...
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
db1f6ded
...
...
@@ -85,7 +85,7 @@
</div>
<!-- Row: OpenAI Token Stats -->
<div
v-if=
"opsEnabled && !(loading && !hasLoadedOnce)"
class=
"grid grid-cols-1 gap-6"
>
<div
v-if=
"opsEnabled &&
showOpenAITokenStats &&
!(loading && !hasLoadedOnce)"
class=
"grid grid-cols-1 gap-6"
>
<OpsOpenAITokenStatsCard
:platform-filter=
"platform"
:group-id-filter=
"groupId"
...
...
@@ -94,7 +94,7 @@
</div>
<!-- Alert Events -->
<OpsAlertEventsCard
v-if=
"opsEnabled && !(loading && !hasLoadedOnce)"
/>
<OpsAlertEventsCard
v-if=
"opsEnabled &&
showAlertEvents &&
!(loading && !hasLoadedOnce)"
/>
<!-- System Logs -->
<OpsSystemLogTable
...
...
@@ -381,6 +381,8 @@ const showSettingsDialog = ref(false)
const
showAlertRulesCard
=
ref
(
false
)
// Auto refresh settings
const
showAlertEvents
=
ref
(
true
)
const
showOpenAITokenStats
=
ref
(
false
)
const
autoRefreshEnabled
=
ref
(
false
)
const
autoRefreshIntervalMs
=
ref
(
30000
)
// default 30 seconds
const
autoRefreshCountdown
=
ref
(
0
)
...
...
@@ -408,15 +410,22 @@ const { pause: pauseCountdown, resume: resumeCountdown } = useIntervalFn(
{
immediate
:
false
}
)
// Load
auto refresh
settings from backend
async
function
load
AutoRefresh
Settings
()
{
// Load
ops dashboard presentation
settings from backend
.
async
function
load
DashboardAdvanced
Settings
()
{
try
{
const
settings
=
await
opsAPI
.
getAdvancedSettings
()
showAlertEvents
.
value
=
settings
.
display_alert_events
showOpenAITokenStats
.
value
=
settings
.
display_openai_token_stats
autoRefreshEnabled
.
value
=
settings
.
auto_refresh_enabled
autoRefreshIntervalMs
.
value
=
settings
.
auto_refresh_interval_seconds
*
1000
autoRefreshCountdown
.
value
=
settings
.
auto_refresh_interval_seconds
}
catch
(
err
)
{
console
.
error
(
'
[OpsDashboard] Failed to load auto refresh settings
'
,
err
)
console
.
error
(
'
[OpsDashboard] Failed to load dashboard advanced settings
'
,
err
)
showAlertEvents
.
value
=
true
showOpenAITokenStats
.
value
=
false
autoRefreshEnabled
.
value
=
false
autoRefreshIntervalMs
.
value
=
30000
autoRefreshCountdown
.
value
=
0
}
}
...
...
@@ -464,7 +473,8 @@ function onCustomTimeRangeChange(startTime: string, endTime: string) {
customEndTime
.
value
=
endTime
}
function
onSettingsSaved
()
{
async
function
onSettingsSaved
()
{
await
loadDashboardAdvancedSettings
()
loadThresholds
()
fetchData
()
}
...
...
@@ -774,7 +784,7 @@ onMounted(async () => {
loadThresholds
()
// Load auto refresh settings
await
load
AutoRefresh
Settings
()
await
load
DashboardAdvanced
Settings
()
if
(
opsEnabled
.
value
)
{
await
fetchData
()
...
...
@@ -816,7 +826,7 @@ watch(autoRefreshEnabled, (enabled) => {
// Reload auto refresh settings after settings dialog is closed
watch
(
showSettingsDialog
,
async
(
show
)
=>
{
if
(
!
show
)
{
await
load
AutoRefresh
Settings
()
await
load
DashboardAdvanced
Settings
()
}
})
</
script
>
frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue
View file @
db1f6ded
...
...
@@ -208,9 +208,11 @@ function onNextPage() {
:
description
=
"
t('admin.ops.openaiTokenStats.empty')
"
/>
<
div
v
-
else
class
=
"
overflow-x-auto
"
>
<
div
v
-
else
class
=
"
space-y-3
"
>
<
div
class
=
"
overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700
"
>
<
div
class
=
"
max-h-[420px] overflow-auto
"
>
<
table
class
=
"
min-w-full text-left text-xs md:text-sm
"
>
<
thead
>
<
thead
class
=
"
sticky top-0 z-10 bg-white dark:bg-dark-800
"
>
<
tr
class
=
"
border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-gray-400
"
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.model
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.requestCount
'
)
}}
<
/th
>
...
...
@@ -225,7 +227,7 @@ function onNextPage() {
<
tr
v
-
for
=
"
row in items
"
:
key
=
"
row.model
"
class
=
"
border-b border-gray-100 text-gray-700 dark:border-dark-800 dark:text-gray-200
"
class
=
"
border-b border-gray-100 text-gray-700
last:border-b-0
dark:border-dark-800 dark:text-gray-200
"
>
<
td
class
=
"
px-2 py-2 font-medium
"
>
{{
row
.
model
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
request_count
)
}}
<
/td
>
...
...
@@ -237,6 +239,8 @@ function onNextPage() {
<
/tr
>
<
/tbody
>
<
/table
>
<
/div
>
<
/div
>
<
div
v
-
if
=
"
viewMode === 'topn'
"
class
=
"
mt-3 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.ops.openaiTokenStats.totalModels
'
,
{
total
}
)
}}
<
/div
>
...
...
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
View file @
db1f6ded
...
...
@@ -543,6 +543,31 @@ async function saveAllSettings() {
/>
</div>
</div>
<!-- Dashboard Cards -->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.dashboardCards
'
)
}}
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.displayAlertEvents
'
)
}}
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.displayAlertEventsHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"advancedSettings.display_alert_events"
/>
</div>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.displayOpenAITokenStats
'
)
}}
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.displayOpenAITokenStatsHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"advancedSettings.display_openai_token_stats"
/>
</div>
</div>
</div>
</details>
</div>
...
...
frontend/src/views/admin/ops/components/__tests__/OpsOpenAITokenStatsCard.spec.ts
View file @
db1f6ded
...
...
@@ -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
(
'
加载失败
'
))
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment