mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 14:58:23 +08:00
fix(monitor-ui): localize the "quota" placeholder model across monitor views
纯配额模式的主模型存储值是占位符 "quota"(数据源是关联账号而非模型), 此前在管理列表主模型列、用户端监控卡片、用户端详情弹窗、运行结果弹窗 四处被当作真实模型名直接渲染,读起来像假模型。 新增 useChannelMonitorFormat.formatMonitorModel:占位符替换为本地化 「配额」标签(复用既有 monitorCommon.checkMode.quota,零新增 i18n 键), quota_probe/probe 的真实模型名不受影响;纯匹配逻辑(如 15d/30d 可用率 按模型名回查 detail)仍用原始值。 问题定位备注:另一反馈「设置页看不到 channel_monitor_show_quota 开关」 经查为部署镜像过旧(开关随 #5761 于 08-18 合入,反馈环境容器构建于 08-15),上游代码中开关完整存在,更新镜像即可,无需改码。
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-900 dark:text-gray-100">{{ row.primary_model }}</span>
|
||||
<!-- 纯配额模式主模型是占位符 "quota"(数据源是账号不是模型),展示层替换为本地化标签 -->
|
||||
<span class="text-sm text-gray-900 dark:text-gray-100">{{ formatMonitorModel(row.primary_model) }}</span>
|
||||
<HelpTooltip>
|
||||
<template #trigger>
|
||||
<span
|
||||
@@ -13,7 +14,7 @@
|
||||
</template>
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold text-gray-100">
|
||||
{{ row.primary_model }}
|
||||
{{ formatMonitorModel(row.primary_model) }}
|
||||
<span
|
||||
class="ml-1 inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium"
|
||||
:class="statusBadgeClass(row.primary_status)"
|
||||
@@ -72,5 +73,5 @@ defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { statusLabel, statusBadgeClass, formatLatency } = useChannelMonitorFormat()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatMonitorModel } = useChannelMonitorFormat()
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
class="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2 text-sm dark:border-dark-600"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ r.model }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ formatMonitorModel(r.model) }}</span>
|
||||
<span v-if="r.message" class="text-xs text-gray-500 dark:text-gray-400">{{ r.message }}</span>
|
||||
<MonitorQuotaView :snapshot="r.quota" class="mt-1" />
|
||||
</div>
|
||||
@@ -54,5 +54,5 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { statusLabel, statusBadgeClass, formatLatency } = useChannelMonitorFormat()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatMonitorModel } = useChannelMonitorFormat()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ChannelMonitor } from '@/api/admin/channelMonitor'
|
||||
import MonitorPrimaryModelCell from '../MonitorPrimaryModelCell.vue'
|
||||
|
||||
// 占位符 "quota" 是纯配额模式主模型的存储值(数据源是账号不是模型),
|
||||
// 展示层必须替换为本地化标签,不得作为假模型名透出。
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}
|
||||
})
|
||||
|
||||
function makeRow(overrides: Partial<ChannelMonitor> = {}): ChannelMonitor {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'kimi-coding',
|
||||
provider: 'kimi',
|
||||
api_mode: 'chat_completions',
|
||||
endpoint: '',
|
||||
api_key_masked: '',
|
||||
primary_model: 'quota',
|
||||
extra_models: [],
|
||||
group_name: '',
|
||||
enabled: true,
|
||||
interval_seconds: 60,
|
||||
jitter_seconds: 0,
|
||||
last_checked_at: null,
|
||||
created_by: 1,
|
||||
created_at: '2026-08-18T00:00:00Z',
|
||||
updated_at: '2026-08-18T00:00:00Z',
|
||||
primary_status: 'operational',
|
||||
primary_latency_ms: null,
|
||||
availability_7d: 100,
|
||||
extra_models_status: [],
|
||||
template_id: null,
|
||||
extra_headers: {},
|
||||
body_override_mode: 'off',
|
||||
body_override: null,
|
||||
check_mode: 'quota',
|
||||
account_id: 5,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function mountCell(row: ChannelMonitor) {
|
||||
return mount(MonitorPrimaryModelCell, {
|
||||
props: { row },
|
||||
global: {
|
||||
stubs: { MonitorQuotaView: true, HelpTooltip: false },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('MonitorPrimaryModelCell placeholder model display', () => {
|
||||
it('replaces the quota placeholder with the localized mode label', () => {
|
||||
const wrapper = mountCell(makeRow())
|
||||
expect(wrapper.text()).toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
|
||||
it('keeps the real model for quota_probe and probe rows', () => {
|
||||
const wrapper = mountCell(makeRow({
|
||||
check_mode: 'quota_probe',
|
||||
primary_model: 'claude-sonnet-4-5',
|
||||
}))
|
||||
expect(wrapper.text()).toContain('claude-sonnet-4-5')
|
||||
expect(wrapper.text()).not.toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
})
|
||||
@@ -30,7 +30,7 @@
|
||||
:key="m.model"
|
||||
class="border-b border-gray-100 dark:border-dark-800"
|
||||
>
|
||||
<td class="py-2 pr-3 font-medium text-gray-900 dark:text-gray-100">{{ m.model }}</td>
|
||||
<td class="py-2 pr-3 font-medium text-gray-900 dark:text-gray-100">{{ formatMonitorModel(m.model) }}</td>
|
||||
<td class="py-2 pr-3">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px]"
|
||||
@@ -83,7 +83,7 @@ defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatPercent } = useChannelMonitorFormat()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatPercent, formatMonitorModel } = useChannelMonitorFormat()
|
||||
|
||||
const detail = ref<UserMonitorDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -88,4 +88,16 @@ describe('MonitorCard quota snapshot visibility', () => {
|
||||
const wrapper = mountCard(makeItem())
|
||||
expect(wrapper.find('[data-testid="monitor-quota-view"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 占位符 "quota" 是主模型存储值,不得作为假模型名直接透出到用户端。
|
||||
it('shows the localized quota label instead of the raw placeholder model', () => {
|
||||
const wrapper = mountCard(makeItem())
|
||||
expect(wrapper.text()).toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
|
||||
it('keeps the real model name for probe monitors', () => {
|
||||
const wrapper = mountCard(makeItem({ primary_model: 'claude-sonnet-4-5' }))
|
||||
expect(wrapper.text()).toContain('claude-sonnet-4-5')
|
||||
expect(wrapper.text()).not.toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
>
|
||||
{{ providerLabel(item.provider) }}
|
||||
</span>
|
||||
<!-- 纯配额模式主模型是占位符 "quota",展示层替换为本地化「配额」标签 -->
|
||||
<span class="font-mono text-xs truncate text-gray-500 dark:text-gray-400">
|
||||
{{ item.primary_model }}
|
||||
{{ formatMonitorModel(item.primary_model) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.group_name"
|
||||
@@ -120,6 +121,7 @@ const {
|
||||
providerLabel,
|
||||
providerBadgeClass,
|
||||
formatLatency,
|
||||
formatMonitorModel,
|
||||
} = useChannelMonitorFormat()
|
||||
|
||||
const providerTintClass = computed(() =>
|
||||
|
||||
@@ -79,6 +79,21 @@ export function useChannelMonitorFormat() {
|
||||
return m || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* Display label for a monitor's primary model. Pure-quota monitors carry the
|
||||
* literal placeholder "quota" (the probe target is an account, not a model),
|
||||
* which must not leak into the UI as a fake model name — render the
|
||||
* localized mode label instead. quota_probe keeps a real model name.
|
||||
*/
|
||||
const QUOTA_MODEL_PLACEHOLDER = 'quota'
|
||||
|
||||
function formatMonitorModel(model: string): string {
|
||||
if (model === QUOTA_MODEL_PLACEHOLDER) {
|
||||
return t('monitorCommon.checkMode.quota')
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
function providerBadgeClass(p: Provider | string): string {
|
||||
switch (p) {
|
||||
case PROVIDER_OPENAI:
|
||||
@@ -200,6 +215,7 @@ export function useChannelMonitorFormat() {
|
||||
statusBadgeClass,
|
||||
providerLabel,
|
||||
checkModeLabel,
|
||||
formatMonitorModel,
|
||||
providerBadgeClass,
|
||||
checkModeBadgeClass,
|
||||
providerPickerClass,
|
||||
|
||||
Reference in New Issue
Block a user