feat(web): 重排首页运行任务层级
This commit is contained in:
10 files changed
+274
-101
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import type { GroupSummary, HealthGroupDto } from '@/api/control/types'
|
||||
import { createTestAppI18n } from '@/test/i18n'
|
||||
|
||||
import GroupCard from './GroupCard.vue'
|
||||
|
||||
const longName = `Primary-${'segment-'.repeat(20)}Group`
|
||||
const group: GroupSummary = {
|
||||
id: 9,
|
||||
name: longName,
|
||||
upstream_url: `https://${'long-host-'.repeat(12)}example.com/v1`,
|
||||
protocols: ['openai', 'anthropic'],
|
||||
models: [],
|
||||
enabled: true,
|
||||
key_count: 2,
|
||||
}
|
||||
const health: HealthGroupDto = {
|
||||
id: 9,
|
||||
name: longName,
|
||||
enabled: true,
|
||||
counts: {
|
||||
total: 2,
|
||||
available: 0,
|
||||
cooldown: 2,
|
||||
blacklisted: 0,
|
||||
disabled: 0,
|
||||
},
|
||||
}
|
||||
|
||||
function mountCard(groupValue = group, healthValue: HealthGroupDto | undefined = health) {
|
||||
const appI18n = createTestAppI18n(undefined, 'en-US')
|
||||
return mount(GroupCard, {
|
||||
props: {
|
||||
group: groupValue,
|
||||
health: healthValue,
|
||||
},
|
||||
global: {
|
||||
plugins: [appI18n.plugin],
|
||||
stubs: {
|
||||
RouterLink: {
|
||||
props: ['to'],
|
||||
template: '<a :href="to"><slot /></a>',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('GroupCard', () => {
|
||||
it('preserves long identity text and translates protocol labels', () => {
|
||||
const wrapper = mountCard()
|
||||
|
||||
expect(wrapper.get('h3').text()).toBe(longName)
|
||||
expect(wrapper.text()).toContain('long-host-')
|
||||
expect(wrapper.text()).toContain('OpenAI')
|
||||
expect(wrapper.text()).toContain('Anthropic')
|
||||
expect(wrapper.get('[href="/groups/9?tab=keys"]').text()).toContain('View details')
|
||||
expect(wrapper.get('[href="/import?mode=existing&group_id=9"]').text()).toContain('Import keys')
|
||||
})
|
||||
|
||||
it('explains no-model and no-available-key serviceability separately', async () => {
|
||||
const wrapper = mountCard()
|
||||
expect(wrapper.get('[data-test="group-service-reason"]').text()).toContain('no models')
|
||||
|
||||
await wrapper.setProps({
|
||||
group: {
|
||||
...group,
|
||||
models: [{ id: 'gpt-real', alias: '' }],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="group-service-reason"]').text()).toMatch(
|
||||
/no upstream key is currently available/i,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -20,6 +20,13 @@ const serviceLabel = computed(() => {
|
||||
if (serviceable.value === undefined) return t('home.unknown')
|
||||
return serviceable.value ? t('home.serviceable') : t('home.unavailable')
|
||||
})
|
||||
const serviceReason = computed(() => {
|
||||
if (!props.group.enabled) return t('home.groupDisabledReason')
|
||||
if (props.group.models.length === 0) return t('home.noModelsReason')
|
||||
if (!props.health) return t('home.healthUnknownReason')
|
||||
if (props.health.counts.available === 0) return t('home.noAvailableKeysReason')
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,10 +44,14 @@ const serviceLabel = computed(() => {
|
||||
{{ group.enabled ? t('home.enabled') : t('home.disabled') }}
|
||||
</StatusBadge>
|
||||
<span v-for="protocol in group.protocols" :key="protocol" class="meta-tag">{{
|
||||
protocol
|
||||
t(`common.protocols.${protocol}`)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<p v-if="serviceReason" data-test="group-service-reason" class="group-card__reason">
|
||||
{{ serviceReason }}
|
||||
</p>
|
||||
|
||||
<div class="group-card__facts">
|
||||
<span>{{ t('home.models', { count: group.models.length }) }}</span>
|
||||
<span>{{ t('home.keys', { count: group.key_count }) }}</span>
|
||||
@@ -80,6 +91,7 @@ const serviceLabel = computed(() => {
|
||||
.group-card h3 {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.group-card__header p {
|
||||
margin: var(--space-1) 0 0;
|
||||
@@ -97,6 +109,10 @@ const serviceLabel = computed(() => {
|
||||
.group-card__facts {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.group-card__reason {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.group-card__facts span + span::before {
|
||||
content: '·';
|
||||
margin-right: var(--space-2);
|
||||
|
||||
@@ -102,6 +102,25 @@ async function mountHome(api: FakeApi, origin?: string, queryClient?: QueryClien
|
||||
}
|
||||
|
||||
describe('HomeView', () => {
|
||||
it('keeps the semantic task order Operational Overview, Groups, then Connection Setup', async () => {
|
||||
const api = new FakeApi()
|
||||
api.when('/api/groups').resolve([groupFixture])
|
||||
api.when('/api/health').resolve(healthFixture)
|
||||
api.when('/api/access-keys/options').resolve([accessKeyFixture])
|
||||
|
||||
const wrapper = await mountHome(api)
|
||||
const sections = wrapper.findAll(
|
||||
'[data-test="home-operational-overview"], [data-test="home-groups"], [data-test="home-connection"]',
|
||||
)
|
||||
|
||||
expect(sections.map((section) => section.attributes('data-test'))).toEqual([
|
||||
'home-operational-overview',
|
||||
'home-groups',
|
||||
'home-connection',
|
||||
])
|
||||
expect(sections[0]?.find('[data-test="home-usage-requests"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps Groups visible when Health fails', async () => {
|
||||
const api = new FakeApi()
|
||||
api.when('/api/groups').resolve([groupFixture])
|
||||
|
||||
@@ -65,102 +65,86 @@ const observedAt = computed(() => {
|
||||
<div class="home-page">
|
||||
<PageHeader :title="t('home.title')" :description="t('home.description')" />
|
||||
|
||||
<section class="home-overview" :aria-labelledby="'service-heading'">
|
||||
<SurfaceCard class="service-card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">{{ t('home.service') }}</p>
|
||||
<h2 id="service-heading">{{ t('home.service') }}</h2>
|
||||
</div>
|
||||
<StatusBadge v-if="healthQuery.isSuccess.value" tone="success">
|
||||
{{ t('home.online') }}
|
||||
</StatusBadge>
|
||||
<StatusBadge
|
||||
v-else-if="healthQuery.isError.value && healthQuery.data.value"
|
||||
tone="warning"
|
||||
>
|
||||
{{ t('home.healthStale') }}
|
||||
</StatusBadge>
|
||||
<StatusBadge v-else-if="healthQuery.isError.value" tone="warning">
|
||||
{{ healthErrorMessage }}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<section
|
||||
class="operational-overview"
|
||||
data-test="home-operational-overview"
|
||||
aria-labelledby="operational-overview-heading"
|
||||
>
|
||||
<header class="operational-overview__header">
|
||||
<h2 id="operational-overview-heading">{{ t('home.operationalOverview') }}</h2>
|
||||
<p>{{ t('home.operationalOverviewDescription') }}</p>
|
||||
</header>
|
||||
|
||||
<div class="home-overview">
|
||||
<SurfaceCard class="service-card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">{{ t('home.service') }}</p>
|
||||
<h3 id="service-heading">{{ t('home.service') }}</h3>
|
||||
</div>
|
||||
<StatusBadge v-if="healthQuery.isSuccess.value" tone="success">
|
||||
{{ t('home.online') }}
|
||||
</StatusBadge>
|
||||
<StatusBadge
|
||||
v-else-if="healthQuery.isError.value && healthQuery.data.value"
|
||||
tone="warning"
|
||||
>
|
||||
{{ t('home.healthStale') }}
|
||||
</StatusBadge>
|
||||
<StatusBadge v-else-if="healthQuery.isError.value" tone="warning">
|
||||
{{ healthErrorMessage }}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
|
||||
<QueryFeedback
|
||||
v-if="healthQuery.isPending.value"
|
||||
state="loading"
|
||||
:message="t('auth.checking')"
|
||||
/>
|
||||
<QueryFeedback
|
||||
v-else-if="healthQuery.isError.value && !healthQuery.data.value"
|
||||
state="error"
|
||||
:message="healthErrorMessage"
|
||||
:retry-label="t('common.retry')"
|
||||
@retry="healthQuery.refetch()"
|
||||
/>
|
||||
<template v-else-if="healthQuery.data.value">
|
||||
<QueryFeedback
|
||||
v-if="healthQuery.isError.value"
|
||||
state="stale"
|
||||
:message="t('home.healthStale')"
|
||||
v-if="healthQuery.isPending.value"
|
||||
state="loading"
|
||||
:message="t('auth.checking')"
|
||||
/>
|
||||
<QueryFeedback
|
||||
v-else-if="healthQuery.isError.value && !healthQuery.data.value"
|
||||
state="error"
|
||||
:message="healthErrorMessage"
|
||||
:retry-label="t('common.retry')"
|
||||
@retry="healthQuery.refetch()"
|
||||
/>
|
||||
<div class="health-meta">
|
||||
<span>{{
|
||||
t('home.revision', { revision: healthQuery.data.value.snapshot_revision })
|
||||
}}</span>
|
||||
<span>{{ t('home.observedAt', { time: observedAt }) }}</span>
|
||||
</div>
|
||||
<div class="health-counts">
|
||||
<span>{{ t('home.keyTotal', { count: healthQuery.data.value.counts.total }) }}</span>
|
||||
<span>{{
|
||||
t('home.keyAvailable', { count: healthQuery.data.value.counts.available })
|
||||
}}</span>
|
||||
<span>{{
|
||||
t('home.keyCooldown', { count: healthQuery.data.value.counts.cooldown })
|
||||
}}</span>
|
||||
<span>{{
|
||||
t('home.keyBlacklisted', { count: healthQuery.data.value.counts.blacklisted })
|
||||
}}</span>
|
||||
<span>{{
|
||||
t('home.keyDisabled', { count: healthQuery.data.value.counts.disabled })
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</SurfaceCard>
|
||||
<template v-else-if="healthQuery.data.value">
|
||||
<QueryFeedback
|
||||
v-if="healthQuery.isError.value"
|
||||
state="stale"
|
||||
:message="t('home.healthStale')"
|
||||
:retry-label="t('common.retry')"
|
||||
@retry="healthQuery.refetch()"
|
||||
/>
|
||||
<div class="health-meta">
|
||||
<span>{{
|
||||
t('home.revision', { revision: healthQuery.data.value.snapshot_revision })
|
||||
}}</span>
|
||||
<span>{{ t('home.observedAt', { time: observedAt }) }}</span>
|
||||
</div>
|
||||
<div class="health-counts">
|
||||
<span>{{ t('home.keyTotal', { count: healthQuery.data.value.counts.total }) }}</span>
|
||||
<span>{{
|
||||
t('home.keyAvailable', { count: healthQuery.data.value.counts.available })
|
||||
}}</span>
|
||||
<span>{{
|
||||
t('home.keyCooldown', { count: healthQuery.data.value.counts.cooldown })
|
||||
}}</span>
|
||||
<span>{{
|
||||
t('home.keyBlacklisted', { count: healthQuery.data.value.counts.blacklisted })
|
||||
}}</span>
|
||||
<span>{{
|
||||
t('home.keyDisabled', { count: healthQuery.data.value.counts.disabled })
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</SurfaceCard>
|
||||
|
||||
<QueryFeedback
|
||||
v-if="accessKeysQuery.isPending.value"
|
||||
state="loading"
|
||||
:message="t('auth.checking')"
|
||||
/>
|
||||
<QueryFeedback
|
||||
v-else-if="accessKeysQuery.isError.value && !accessKeysQuery.data.value"
|
||||
state="error"
|
||||
:message="t('home.accessKeysError')"
|
||||
:retry-label="t('common.retry')"
|
||||
@retry="accessKeysQuery.refetch()"
|
||||
/>
|
||||
<div v-else class="connection-section">
|
||||
<QueryFeedback
|
||||
v-if="accessKeysQuery.isError.value"
|
||||
state="stale"
|
||||
:message="t('home.accessKeysStale')"
|
||||
:retry-label="t('common.retry')"
|
||||
@retry="accessKeysQuery.refetch()"
|
||||
/>
|
||||
<ConnectionCard
|
||||
:keys="accessKeysQuery.data.value ?? []"
|
||||
:model-ids="modelIds"
|
||||
:origin="props.origin"
|
||||
/>
|
||||
<UsageSummaryCard heading-as="h3" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<UsageSummaryCard />
|
||||
|
||||
<section class="groups-section" aria-labelledby="groups-heading">
|
||||
<section class="groups-section" data-test="home-groups" aria-labelledby="groups-heading">
|
||||
<header class="groups-section__header">
|
||||
<div>
|
||||
<h2 id="groups-heading">{{ t('home.groups') }}</h2>
|
||||
@@ -208,6 +192,39 @@ const observedAt = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="connection-section"
|
||||
data-test="home-connection"
|
||||
:aria-label="t('home.connection')"
|
||||
>
|
||||
<QueryFeedback
|
||||
v-if="accessKeysQuery.isPending.value"
|
||||
state="loading"
|
||||
:message="t('auth.checking')"
|
||||
/>
|
||||
<QueryFeedback
|
||||
v-else-if="accessKeysQuery.isError.value && !accessKeysQuery.data.value"
|
||||
state="error"
|
||||
:message="t('home.accessKeysError')"
|
||||
:retry-label="t('common.retry')"
|
||||
@retry="accessKeysQuery.refetch()"
|
||||
/>
|
||||
<template v-else>
|
||||
<QueryFeedback
|
||||
v-if="accessKeysQuery.isError.value"
|
||||
state="stale"
|
||||
:message="t('home.accessKeysStale')"
|
||||
:retry-label="t('common.retry')"
|
||||
@retry="accessKeysQuery.refetch()"
|
||||
/>
|
||||
<ConnectionCard
|
||||
:keys="accessKeysQuery.data.value ?? []"
|
||||
:model-ids="modelIds"
|
||||
:origin="props.origin"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -216,14 +233,29 @@ const observedAt = computed(() => {
|
||||
display: grid;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
.operational-overview,
|
||||
.connection-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.operational-overview__header h2,
|
||||
.operational-overview__header p {
|
||||
margin: 0;
|
||||
}
|
||||
.operational-overview__header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.operational-overview__header p {
|
||||
margin-top: var(--space-1);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.home-overview {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.9fr);
|
||||
gap: var(--space-5);
|
||||
align-items: start;
|
||||
}
|
||||
.service-card,
|
||||
.connection-section {
|
||||
.service-card {
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
@@ -236,7 +268,7 @@ const observedAt = computed(() => {
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.section-heading h2,
|
||||
.section-heading h3,
|
||||
.groups-section h2 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('UsageSummaryCard', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('always renders three known zero quality counts when persisted usage has no quality gaps', async () => {
|
||||
it('keeps known zero quality counts neutral when persisted usage has no quality gaps', async () => {
|
||||
const report = usageReport({
|
||||
summary: {
|
||||
...aggregate,
|
||||
@@ -163,7 +163,7 @@ describe('UsageSummaryCard', () => {
|
||||
for (const quality of ['missing', 'partial', 'unpriced']) {
|
||||
const status = wrapper.get(`[data-test="home-usage-quality-${quality}"]`)
|
||||
expect(status.text()).toContain('0')
|
||||
expect(status.classes()).toContain('status-badge--success')
|
||||
expect(status.classes()).toContain('status-badge--neutral')
|
||||
}
|
||||
expect(wrapper.get('[data-test="home-usage-tokens"]').text()).toContain('0')
|
||||
})
|
||||
|
||||
@@ -14,6 +14,15 @@ import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import SurfaceCard from '@/components/ui/SurfaceCard.vue'
|
||||
import { formatEstimatedUSD } from '@/features/usage/estimated-cost'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
headingAs?: 'h2' | 'h3'
|
||||
}>(),
|
||||
{
|
||||
headingAs: 'h2',
|
||||
},
|
||||
)
|
||||
|
||||
const filters = { range: '24h' } as const
|
||||
const client = useApiClient()
|
||||
const { locale, t } = useI18n()
|
||||
@@ -71,7 +80,7 @@ function formatEstimatedCost(aggregate: UsageAggregateDto): string {
|
||||
<header class="usage-summary-card__header">
|
||||
<div>
|
||||
<p class="eyebrow">{{ t('home.usage.eyebrow') }}</p>
|
||||
<h2 id="home-usage-heading">{{ t('home.usage.title') }}</h2>
|
||||
<component :is="headingAs" id="home-usage-heading">{{ t('home.usage.title') }}</component>
|
||||
<p>{{ t('home.usage.description') }}</p>
|
||||
</div>
|
||||
<RouterLink
|
||||
@@ -137,21 +146,21 @@ function formatEstimatedCost(aggregate: UsageAggregateDto): string {
|
||||
>
|
||||
<StatusBadge
|
||||
data-test="home-usage-quality-missing"
|
||||
:tone="report.summary.usage_missing_count ? 'warning' : 'success'"
|
||||
:tone="report.summary.usage_missing_count ? 'warning' : 'neutral'"
|
||||
>
|
||||
{{ t('home.usage.quality.missing') }}
|
||||
{{ formatCount(report.summary.usage_missing_count) }}
|
||||
</StatusBadge>
|
||||
<StatusBadge
|
||||
data-test="home-usage-quality-partial"
|
||||
:tone="report.summary.partial_count ? 'warning' : 'success'"
|
||||
:tone="report.summary.partial_count ? 'warning' : 'neutral'"
|
||||
>
|
||||
{{ t('home.usage.quality.partial') }}
|
||||
{{ formatCount(report.summary.partial_count) }}
|
||||
</StatusBadge>
|
||||
<StatusBadge
|
||||
data-test="home-usage-quality-unpriced"
|
||||
:tone="report.summary.unpriced_request_count ? 'warning' : 'success'"
|
||||
:tone="report.summary.unpriced_request_count ? 'warning' : 'neutral'"
|
||||
>
|
||||
{{ t('home.usage.quality.unpriced') }}
|
||||
{{ formatCount(report.summary.unpriced_request_count) }}
|
||||
@@ -191,14 +200,14 @@ function formatEstimatedCost(aggregate: UsageAggregateDto): string {
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.usage-summary-card__header h2,
|
||||
.usage-summary-card__header :is(h2, h3),
|
||||
.usage-summary-card__header p {
|
||||
margin: 0;
|
||||
}
|
||||
.usage-summary-card__header h2 {
|
||||
.usage-summary-card__header :is(h2, h3) {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.usage-summary-card__header h2 + p {
|
||||
.usage-summary-card__header :is(h2, h3) + p {
|
||||
margin-top: var(--space-1);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
@@ -69,8 +69,9 @@ export function buildConnectionSnippet(input: ConnectionSnippetInput): Connectio
|
||||
}
|
||||
|
||||
export function isGroupServiceable(group: GroupSummary, counts?: KeyCounts): boolean | undefined {
|
||||
if (!group.enabled || group.models.length === 0) return false
|
||||
if (!counts) return undefined
|
||||
return group.enabled && group.models.length > 0 && counts.available > 0
|
||||
return counts.available > 0
|
||||
}
|
||||
|
||||
export function selectInitialAccessKey(keys: AccessKeyOptionDto[]): number | undefined {
|
||||
|
||||
@@ -76,6 +76,8 @@ export default {
|
||||
title: 'Operational overview',
|
||||
description:
|
||||
'Review real runtime health, Group serviceability, and client connection settings.',
|
||||
operationalOverview: 'Operational Overview',
|
||||
operationalOverviewDescription: 'Runtime health and persisted usage from the last 24 hours.',
|
||||
service: 'Management service',
|
||||
online: 'Online',
|
||||
healthUnavailable: 'The service responded, but health is currently unavailable.',
|
||||
@@ -103,6 +105,10 @@ export default {
|
||||
serviceable: 'Serviceable',
|
||||
unavailable: 'Unavailable',
|
||||
unknown: 'Status unknown',
|
||||
groupDisabledReason: 'This Group is disabled.',
|
||||
noModelsReason: 'This Group has no models configured.',
|
||||
healthUnknownReason: 'Current upstream-key availability is unknown.',
|
||||
noAvailableKeysReason: 'No upstream key is currently available.',
|
||||
models: '{count} models',
|
||||
keys: '{count} keys',
|
||||
details: 'View details',
|
||||
|
||||
@@ -76,6 +76,9 @@ export default {
|
||||
home: {
|
||||
title: '運用概要',
|
||||
description: '実際のランタイム状態、Group のサービス可否、クライアント接続設定を確認します。',
|
||||
operationalOverview: '運用概要',
|
||||
operationalOverviewDescription:
|
||||
'ランタイムの状態と過去 24 時間に永続化された使用量をまとめます。',
|
||||
service: '管理サービス',
|
||||
online: 'オンライン',
|
||||
healthUnavailable: 'サービスは応答しましたが、ヘルス状態を取得できません。',
|
||||
@@ -102,6 +105,10 @@ export default {
|
||||
serviceable: 'サービス可能',
|
||||
unavailable: 'サービス不可',
|
||||
unknown: '状態不明',
|
||||
groupDisabledReason: 'この Group は無効です。',
|
||||
noModelsReason: 'この Group にはモデルが設定されていません。',
|
||||
healthUnknownReason: '現在のアップストリームキーの可用性は不明です。',
|
||||
noAvailableKeysReason: '現在利用可能なアップストリームキーがありません。',
|
||||
models: '{count} モデル',
|
||||
keys: '{count} キー',
|
||||
details: '詳細を見る',
|
||||
|
||||
@@ -75,6 +75,8 @@ export default {
|
||||
home: {
|
||||
title: '运行概览',
|
||||
description: '查看真实运行健康状态、Group 服务能力和客户端连接配置。',
|
||||
operationalOverview: '运行概览',
|
||||
operationalOverviewDescription: '汇总运行健康状态与最近 24 小时已持久化用量。',
|
||||
service: '管理服务',
|
||||
online: '在线',
|
||||
healthUnavailable: '服务已响应,但健康状态暂不可用。',
|
||||
@@ -100,6 +102,10 @@ export default {
|
||||
serviceable: '可服务',
|
||||
unavailable: '不可服务',
|
||||
unknown: '状态未知',
|
||||
groupDisabledReason: '该 Group 已停用。',
|
||||
noModelsReason: '该 Group 尚未配置模型。',
|
||||
healthUnknownReason: '当前上游密钥可用性未知。',
|
||||
noAvailableKeysReason: '当前没有可用的上游密钥。',
|
||||
models: '{count} 个模型',
|
||||
keys: '{count} 个密钥',
|
||||
details: '查看详情',
|
||||
|
||||
Reference in new issue
Block a user