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 {
...
@@ -371,6 +371,8 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
IgnoreCountTokensErrors
:
true
,
// count_tokens 404 是预期行为,默认忽略
IgnoreCountTokensErrors
:
true
,
// count_tokens 404 是预期行为,默认忽略
IgnoreContextCanceled
:
true
,
// Default to true - client disconnects are not errors
IgnoreContextCanceled
:
true
,
// Default to true - client disconnects are not errors
IgnoreNoAvailableAccounts
:
false
,
// Default to false - this is a real routing issue
IgnoreNoAvailableAccounts
:
false
,
// Default to false - this is a real routing issue
DisplayOpenAITokenStats
:
false
,
DisplayAlertEvents
:
true
,
AutoRefreshEnabled
:
false
,
AutoRefreshEnabled
:
false
,
AutoRefreshIntervalSec
:
30
,
AutoRefreshIntervalSec
:
30
,
}
}
...
@@ -438,7 +440,7 @@ func (s *OpsService) GetOpsAdvancedSettings(ctx context.Context) (*OpsAdvancedSe
...
@@ -438,7 +440,7 @@ func (s *OpsService) GetOpsAdvancedSettings(ctx context.Context) (*OpsAdvancedSe
return
nil
,
err
return
nil
,
err
}
}
cfg
:=
&
OpsAdvancedSettings
{}
cfg
:=
default
OpsAdvancedSettings
()
if
err
:=
json
.
Unmarshal
([]
byte
(
raw
),
cfg
);
err
!=
nil
{
if
err
:=
json
.
Unmarshal
([]
byte
(
raw
),
cfg
);
err
!=
nil
{
return
defaultCfg
,
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 {
...
@@ -98,6 +98,8 @@ type OpsAdvancedSettings struct {
IgnoreContextCanceled
bool
`json:"ignore_context_canceled"`
IgnoreContextCanceled
bool
`json:"ignore_context_canceled"`
IgnoreNoAvailableAccounts
bool
`json:"ignore_no_available_accounts"`
IgnoreNoAvailableAccounts
bool
`json:"ignore_no_available_accounts"`
IgnoreInvalidApiKeyErrors
bool
`json:"ignore_invalid_api_key_errors"`
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"`
AutoRefreshEnabled
bool
`json:"auto_refresh_enabled"`
AutoRefreshIntervalSec
int
`json:"auto_refresh_interval_seconds"`
AutoRefreshIntervalSec
int
`json:"auto_refresh_interval_seconds"`
}
}
...
...
frontend/src/api/admin/ops.ts
View file @
db1f6ded
...
@@ -841,6 +841,8 @@ export interface OpsAdvancedSettings {
...
@@ -841,6 +841,8 @@ export interface OpsAdvancedSettings {
ignore_context_canceled
:
boolean
ignore_context_canceled
:
boolean
ignore_no_available_accounts
:
boolean
ignore_no_available_accounts
:
boolean
ignore_invalid_api_key_errors
:
boolean
ignore_invalid_api_key_errors
:
boolean
display_openai_token_stats
:
boolean
display_alert_events
:
boolean
auto_refresh_enabled
:
boolean
auto_refresh_enabled
:
boolean
auto_refresh_interval_seconds
:
number
auto_refresh_interval_seconds
:
number
}
}
...
...
frontend/src/i18n/locales/en.ts
View file @
db1f6ded
...
@@ -3709,6 +3709,11 @@ export default {
...
@@ -3709,6 +3709,11 @@ export default {
refreshInterval15s
:
'
15 seconds
'
,
refreshInterval15s
:
'
15 seconds
'
,
refreshInterval30s
:
'
30 seconds
'
,
refreshInterval30s
:
'
30 seconds
'
,
refreshInterval60s
:
'
60 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
'
,
autoRefreshCountdown
:
'
Auto refresh: {seconds}s
'
,
validation
:
{
validation
:
{
title
:
'
Please fix the following issues
'
,
title
:
'
Please fix the following issues
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
db1f6ded
...
@@ -3883,6 +3883,11 @@ export default {
...
@@ -3883,6 +3883,11 @@ export default {
refreshInterval15s
:
'
15 秒
'
,
refreshInterval15s
:
'
15 秒
'
,
refreshInterval30s
:
'
30 秒
'
,
refreshInterval30s
:
'
30 秒
'
,
refreshInterval60s
:
'
60 秒
'
,
refreshInterval60s
:
'
60 秒
'
,
dashboardCards
:
'
仪表盘卡片
'
,
displayAlertEvents
:
'
展示告警事件
'
,
displayAlertEventsHint
:
'
控制运维监控仪表盘中告警事件卡片是否显示,默认开启。
'
,
displayOpenAITokenStats
:
'
展示 OpenAI Token 请求统计
'
,
displayOpenAITokenStatsHint
:
'
控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。
'
,
autoRefreshCountdown
:
'
自动刷新:{seconds}s
'
,
autoRefreshCountdown
:
'
自动刷新:{seconds}s
'
,
validation
:
{
validation
:
{
title
:
'
请先修正以下问题
'
,
title
:
'
请先修正以下问题
'
,
...
...
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
db1f6ded
...
@@ -85,7 +85,7 @@
...
@@ -85,7 +85,7 @@
</div>
</div>
<!-- Row: OpenAI Token Stats -->
<!-- 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
<OpsOpenAITokenStatsCard
:platform-filter=
"platform"
:platform-filter=
"platform"
:group-id-filter=
"groupId"
:group-id-filter=
"groupId"
...
@@ -94,7 +94,7 @@
...
@@ -94,7 +94,7 @@
</div>
</div>
<!-- Alert Events -->
<!-- Alert Events -->
<OpsAlertEventsCard
v-if=
"opsEnabled && !(loading && !hasLoadedOnce)"
/>
<OpsAlertEventsCard
v-if=
"opsEnabled &&
showAlertEvents &&
!(loading && !hasLoadedOnce)"
/>
<!-- System Logs -->
<!-- System Logs -->
<OpsSystemLogTable
<OpsSystemLogTable
...
@@ -381,6 +381,8 @@ const showSettingsDialog = ref(false)
...
@@ -381,6 +381,8 @@ const showSettingsDialog = ref(false)
const
showAlertRulesCard
=
ref
(
false
)
const
showAlertRulesCard
=
ref
(
false
)
// Auto refresh settings
// Auto refresh settings
const
showAlertEvents
=
ref
(
true
)
const
showOpenAITokenStats
=
ref
(
false
)
const
autoRefreshEnabled
=
ref
(
false
)
const
autoRefreshEnabled
=
ref
(
false
)
const
autoRefreshIntervalMs
=
ref
(
30000
)
// default 30 seconds
const
autoRefreshIntervalMs
=
ref
(
30000
)
// default 30 seconds
const
autoRefreshCountdown
=
ref
(
0
)
const
autoRefreshCountdown
=
ref
(
0
)
...
@@ -408,15 +410,22 @@ const { pause: pauseCountdown, resume: resumeCountdown } = useIntervalFn(
...
@@ -408,15 +410,22 @@ const { pause: pauseCountdown, resume: resumeCountdown } = useIntervalFn(
{
immediate
:
false
}
{
immediate
:
false
}
)
)
// Load
auto refresh
settings from backend
// Load
ops dashboard presentation
settings from backend
.
async
function
load
AutoRefresh
Settings
()
{
async
function
load
DashboardAdvanced
Settings
()
{
try
{
try
{
const
settings
=
await
opsAPI
.
getAdvancedSettings
()
const
settings
=
await
opsAPI
.
getAdvancedSettings
()
showAlertEvents
.
value
=
settings
.
display_alert_events
showOpenAITokenStats
.
value
=
settings
.
display_openai_token_stats
autoRefreshEnabled
.
value
=
settings
.
auto_refresh_enabled
autoRefreshEnabled
.
value
=
settings
.
auto_refresh_enabled
autoRefreshIntervalMs
.
value
=
settings
.
auto_refresh_interval_seconds
*
1000
autoRefreshIntervalMs
.
value
=
settings
.
auto_refresh_interval_seconds
*
1000
autoRefreshCountdown
.
value
=
settings
.
auto_refresh_interval_seconds
autoRefreshCountdown
.
value
=
settings
.
auto_refresh_interval_seconds
}
catch
(
err
)
{
}
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) {
...
@@ -464,7 +473,8 @@ function onCustomTimeRangeChange(startTime: string, endTime: string) {
customEndTime
.
value
=
endTime
customEndTime
.
value
=
endTime
}
}
function
onSettingsSaved
()
{
async
function
onSettingsSaved
()
{
await
loadDashboardAdvancedSettings
()
loadThresholds
()
loadThresholds
()
fetchData
()
fetchData
()
}
}
...
@@ -774,7 +784,7 @@ onMounted(async () => {
...
@@ -774,7 +784,7 @@ onMounted(async () => {
loadThresholds
()
loadThresholds
()
// Load auto refresh settings
// Load auto refresh settings
await
load
AutoRefresh
Settings
()
await
load
DashboardAdvanced
Settings
()
if
(
opsEnabled
.
value
)
{
if
(
opsEnabled
.
value
)
{
await
fetchData
()
await
fetchData
()
...
@@ -816,7 +826,7 @@ watch(autoRefreshEnabled, (enabled) => {
...
@@ -816,7 +826,7 @@ watch(autoRefreshEnabled, (enabled) => {
// Reload auto refresh settings after settings dialog is closed
// Reload auto refresh settings after settings dialog is closed
watch
(
showSettingsDialog
,
async
(
show
)
=>
{
watch
(
showSettingsDialog
,
async
(
show
)
=>
{
if
(
!
show
)
{
if
(
!
show
)
{
await
load
AutoRefresh
Settings
()
await
load
DashboardAdvanced
Settings
()
}
}
})
})
</
script
>
</
script
>
frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue
View file @
db1f6ded
...
@@ -208,9 +208,11 @@ function onNextPage() {
...
@@ -208,9 +208,11 @@ function onNextPage() {
:
description
=
"
t('admin.ops.openaiTokenStats.empty')
"
:
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
"
>
<
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
"
>
<
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.model
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.requestCount
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.requestCount
'
)
}}
<
/th
>
...
@@ -225,7 +227,7 @@ function onNextPage() {
...
@@ -225,7 +227,7 @@ function onNextPage() {
<
tr
<
tr
v
-
for
=
"
row in items
"
v
-
for
=
"
row in items
"
:
key
=
"
row.model
"
:
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 font-medium
"
>
{{
row
.
model
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
request_count
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
request_count
)
}}
<
/td
>
...
@@ -237,6 +239,8 @@ function onNextPage() {
...
@@ -237,6 +239,8 @@ function onNextPage() {
<
/tr
>
<
/tr
>
<
/tbody
>
<
/tbody
>
<
/table
>
<
/table
>
<
/div
>
<
/div
>
<
div
v
-
if
=
"
viewMode === 'topn'
"
class
=
"
mt-3 text-xs text-gray-500 dark:text-gray-400
"
>
<
div
v
-
if
=
"
viewMode === 'topn'
"
class
=
"
mt-3 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.ops.openaiTokenStats.totalModels
'
,
{
total
}
)
}}
{{
t
(
'
admin.ops.openaiTokenStats.totalModels
'
,
{
total
}
)
}}
<
/div
>
<
/div
>
...
...
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
View file @
db1f6ded
...
@@ -543,6 +543,31 @@ async function saveAllSettings() {
...
@@ -543,6 +543,31 @@ async function saveAllSettings() {
/>
/>
</div>
</div>
</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>
</div>
</details>
</details>
</div>
</div>
...
...
frontend/src/views/admin/ops/components/__tests__/OpsOpenAITokenStatsCard.spec.ts
View file @
db1f6ded
...
@@ -196,6 +196,23 @@ describe('OpsOpenAITokenStatsCard', () => {
...
@@ -196,6 +196,23 @@ describe('OpsOpenAITokenStatsCard', () => {
expect
(
wrapper
.
find
(
'
.empty-state
'
).
exists
()).
toBe
(
true
)
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
()
=>
{
it
(
'
接口异常时显示错误提示
'
,
async
()
=>
{
mockGetOpenAITokenStats
.
mockRejectedValue
(
new
Error
(
'
加载失败
'
))
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