mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat: 完善前置拦截审核运行态
This commit is contained in:
@@ -132,6 +132,16 @@ export interface ContentModerationRuntimeStatus {
|
||||
dropped: number
|
||||
processed: number
|
||||
errors: number
|
||||
pre_block_active: number
|
||||
pre_block_checked: number
|
||||
pre_block_allowed: number
|
||||
pre_block_blocked: number
|
||||
pre_block_errors: number
|
||||
pre_block_avg_latency_ms: number
|
||||
pre_block_api_key_active: number
|
||||
pre_block_api_key_available_count: number
|
||||
pre_block_api_key_total_calls: number
|
||||
pre_block_api_key_loads: ContentModerationAPIKeyLoad[]
|
||||
api_key_statuses: ContentModerationAPIKeyStatus[]
|
||||
flagged_hash_count: number
|
||||
last_cleanup_at?: string
|
||||
@@ -139,6 +149,20 @@ export interface ContentModerationRuntimeStatus {
|
||||
last_cleanup_deleted_non_hit: number
|
||||
}
|
||||
|
||||
export interface ContentModerationAPIKeyLoad {
|
||||
index: number
|
||||
key_hash: string
|
||||
masked: string
|
||||
status: ContentModerationAPIKeyStatusValue
|
||||
active: number
|
||||
total: number
|
||||
success: number
|
||||
errors: number
|
||||
avg_latency_ms: number
|
||||
last_latency_ms: number
|
||||
last_http_status: number
|
||||
}
|
||||
|
||||
export interface ContentModerationLog {
|
||||
id: number
|
||||
request_id: string
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import en from '../locales/en'
|
||||
import zh from '../locales/zh'
|
||||
|
||||
describe('risk control locale copy', () => {
|
||||
it('describes worker runtime as audit and pre-block record processing', () => {
|
||||
expect(zh.admin.riskControl.workerStatusHint).toContain('前置拦截记录任务')
|
||||
expect(zh.admin.riskControl.workerStatusHint).not.toContain('异步观察任务')
|
||||
expect(en.admin.riskControl.workerStatusHint).toContain('pre-block record tasks')
|
||||
expect(en.admin.riskControl.workerStatusHint).not.toContain('observation tasks')
|
||||
})
|
||||
|
||||
it('keeps pre-block audit key summary aware of async worker load', () => {
|
||||
expect(zh.admin.riskControl.preBlockAPIKeyLoadSummary).toContain('worker:{workerActive} / {workerTotal}')
|
||||
expect(en.admin.riskControl.preBlockAPIKeyLoadSummary).toContain('worker: {workerActive} / {workerTotal}')
|
||||
})
|
||||
|
||||
it('does not describe pre-block audit key polling as bypassing the worker pool', () => {
|
||||
expect(zh.admin.riskControl.preBlockAPIKeyLoadHint).toBe('同步前置拦截直接轮询可用审核 Key。')
|
||||
expect(zh.admin.riskControl.preBlockAPIKeyLoadHint).not.toContain('Worker 池')
|
||||
expect(en.admin.riskControl.preBlockAPIKeyLoadHint).not.toContain('worker pool')
|
||||
})
|
||||
})
|
||||
@@ -2599,14 +2599,37 @@ export default {
|
||||
modelFilterIncludeSummary: 'Applies to {count} models',
|
||||
modelFilterExcludeSummary: 'Excludes {count} models',
|
||||
emptyLogs: 'No audit records',
|
||||
preBlockSyncStatus: 'Pre-Block Sync Status',
|
||||
preBlockSyncHint: 'Live counters for the synchronous moderation path, excluding async record tasks.',
|
||||
preBlockActive: 'Sync Processing',
|
||||
preBlockActiveHint: 'Currently checking',
|
||||
preBlockChecked: 'Checked',
|
||||
preBlockCheckedHint: 'Entered pre-block path',
|
||||
preBlockAllowed: 'Allowed',
|
||||
preBlockAllowedHint: 'No block triggered',
|
||||
preBlockBlocked: 'Blocked',
|
||||
preBlockBlockedHint: 'Rejected after hit',
|
||||
preBlockErrors: 'Audit Errors',
|
||||
preBlockErrorsHint: 'Failed or no usable key',
|
||||
preBlockAvgLatency: 'Avg Latency',
|
||||
preBlockAvgLatencyHint: 'Synchronous path average',
|
||||
preBlockAPIKeyLoad: 'Audit Key Load',
|
||||
preBlockAPIKeyLoadHint: 'Synchronous pre-block checks round-robin usable audit keys directly.',
|
||||
preBlockAPIKeyLoadSummary: 'Sync active {active} / usable keys {available}, {total} total, worker: {workerActive} / {workerTotal}',
|
||||
preBlockAPIKeyTotals: 'Total {total}, success {success}, errors {errors}',
|
||||
preBlockAPIKeyLoadEmpty: 'No audit key load data yet',
|
||||
preBlockKeyActiveShort: 'Active',
|
||||
preBlockKeyTotalShort: 'Total',
|
||||
preBlockKeyAvgShort: 'Avg',
|
||||
preBlockKeyLastShort: 'Last',
|
||||
workerStatus: 'Worker Runtime',
|
||||
workerStatusHint: 'Queue and worker pool status for asynchronous observation tasks.',
|
||||
workerStatusHint: 'Queue and worker pool status for async audit tasks and pre-block record tasks, excluding synchronous pre-block checks.',
|
||||
workerPool: 'Worker Pool',
|
||||
workerPoolMeta: '{active} processing, {idle} idle and ready, {total} total',
|
||||
queueUsage: 'Queue Usage',
|
||||
activeWorkers: 'Processing',
|
||||
idleWorkers: 'Idle Ready',
|
||||
workerActive: 'Processing an asynchronous audit task',
|
||||
workerActive: 'Processing an async audit or record task',
|
||||
workerIdle: 'Started, idle and ready',
|
||||
workerDisabled: 'Risk control or content audit is disabled',
|
||||
processed: 'Processed',
|
||||
|
||||
@@ -2676,14 +2676,37 @@ export default {
|
||||
modelFilterIncludeSummary: '仅 {count} 个模型生效',
|
||||
modelFilterExcludeSummary: '排除 {count} 个模型',
|
||||
emptyLogs: '暂无审核记录',
|
||||
preBlockSyncStatus: '前置拦截同步状态',
|
||||
preBlockSyncHint: '同步审核链路的实时计数,不包含异步写记录任务。',
|
||||
preBlockActive: '同步处理中',
|
||||
preBlockActiveHint: '当前正在审核',
|
||||
preBlockChecked: '已检查',
|
||||
preBlockCheckedHint: '进入前置拦截链路',
|
||||
preBlockAllowed: '已放行',
|
||||
preBlockAllowedHint: '未触发拦截',
|
||||
preBlockBlocked: '已拦截',
|
||||
preBlockBlockedHint: '命中后拒绝请求',
|
||||
preBlockErrors: '审核异常',
|
||||
preBlockErrorsHint: '失败或无可用 Key',
|
||||
preBlockAvgLatency: '平均耗时',
|
||||
preBlockAvgLatencyHint: '同步链路平均值',
|
||||
preBlockAPIKeyLoad: '审核 Key 负载',
|
||||
preBlockAPIKeyLoadHint: '同步前置拦截直接轮询可用审核 Key。',
|
||||
preBlockAPIKeyLoadSummary: '同步并发 {active} / 可用 Key {available},累计 {total} 次,worker:{workerActive} / {workerTotal}',
|
||||
preBlockAPIKeyTotals: '累计 {total},成功 {success},异常 {errors}',
|
||||
preBlockAPIKeyLoadEmpty: '暂无审核 Key 负载数据',
|
||||
preBlockKeyActiveShort: '并发',
|
||||
preBlockKeyTotalShort: '累计',
|
||||
preBlockKeyAvgShort: '平均',
|
||||
preBlockKeyLastShort: '最近',
|
||||
workerStatus: 'Worker 运行状态',
|
||||
workerStatusHint: '异步观察任务的队列和 worker 池状态。',
|
||||
workerStatusHint: '异步审计任务和前置拦截记录任务的队列与 Worker 池状态,不包含同步前置拦截审核请求。',
|
||||
workerPool: 'Worker 池',
|
||||
workerPoolMeta: '{active} 个处理中,{idle} 个空闲可用,共 {total} 个',
|
||||
queueUsage: '队列占用',
|
||||
activeWorkers: '处理中',
|
||||
idleWorkers: '空闲可用',
|
||||
workerActive: '正在处理异步审计任务',
|
||||
workerActive: '正在处理异步审计或记录任务',
|
||||
workerIdle: '已启动,当前空闲可用',
|
||||
workerDisabled: '风控或内容审计未启用',
|
||||
processed: '已处理',
|
||||
|
||||
@@ -53,7 +53,105 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div
|
||||
v-if="showPreBlockRuntimeCard"
|
||||
data-test="pre-block-runtime-cards"
|
||||
class="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,520px)_minmax(0,1fr)]"
|
||||
>
|
||||
<div data-test="pre-block-sync-card" class="card">
|
||||
<div class="flex flex-col gap-4 border-b border-gray-100 px-6 py-4 dark:border-dark-700 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('admin.riskControl.preBlockSyncStatus') }}</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ t('admin.riskControl.preBlockSyncHint') }}</p>
|
||||
</div>
|
||||
<span class="inline-flex w-fit items-center rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-600 dark:bg-dark-700 dark:text-gray-300">
|
||||
{{ modeLabel(status?.mode ?? configForm.mode) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div data-test="pre-block-metric-grid" class="grid grid-cols-2 gap-3 md:grid-cols-3">
|
||||
<div
|
||||
v-for="item in preBlockMetricItems"
|
||||
:key="item.key"
|
||||
class="rounded-lg p-4"
|
||||
:class="item.class"
|
||||
>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ item.label }}</p>
|
||||
<p class="mt-2 truncate text-2xl font-semibold leading-8" :class="item.valueClass">{{ item.value }}</p>
|
||||
<p v-if="item.meta" class="mt-1 truncate text-xs text-gray-500 dark:text-gray-400">{{ item.meta }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-test="pre-block-api-key-load-card" class="card">
|
||||
<div class="flex flex-col gap-4 border-b border-gray-100 px-6 py-4 dark:border-dark-700 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('admin.riskControl.preBlockAPIKeyLoad') }}</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.riskControl.preBlockAPIKeyLoadHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex w-fit items-center rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-600 dark:bg-dark-700 dark:text-gray-300">
|
||||
{{ preBlockAPIKeyLoadSummaryText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div
|
||||
v-if="preBlockAPIKeyLoads.length > 0"
|
||||
data-test="pre-block-api-key-load-list"
|
||||
class="max-h-[280px] space-y-3 overflow-y-auto pr-1"
|
||||
>
|
||||
<div
|
||||
v-for="item in preBlockAPIKeyLoads"
|
||||
:key="item.key_hash || item.index"
|
||||
class="rounded-lg bg-gray-50 p-3 dark:bg-dark-700/50"
|
||||
>
|
||||
<div class="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="font-mono text-sm font-semibold text-gray-900 dark:text-white">#{{ item.index + 1 }}</span>
|
||||
<span class="truncate font-mono text-sm text-gray-700 dark:text-gray-200">{{ item.masked || '-' }}</span>
|
||||
<span class="h-2 w-2 flex-shrink-0 rounded-full" :class="apiKeyStatusDotClass(item.status)"></span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.riskControl.preBlockAPIKeyTotals', { total: formatNumber(item.total), success: formatNumber(item.success), errors: formatNumber(item.errors) }) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2 text-right text-xs text-gray-500 dark:text-gray-400 sm:min-w-[280px]">
|
||||
<div>
|
||||
<p>{{ t('admin.riskControl.preBlockKeyActiveShort') }}</p>
|
||||
<p class="mt-1 text-sm font-semibold text-sky-700 dark:text-sky-300">{{ formatNumber(item.active) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ t('admin.riskControl.preBlockKeyTotalShort') }}</p>
|
||||
<p class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(item.total) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ t('admin.riskControl.preBlockKeyAvgShort') }}</p>
|
||||
<p class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(item.avg_latency_ms) }} ms</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ t('admin.riskControl.preBlockKeyLastShort') }}</p>
|
||||
<p class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(item.last_latency_ms) }} ms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 h-1.5 overflow-hidden rounded-full bg-white dark:bg-dark-900">
|
||||
<div class="h-full rounded-full bg-sky-500" :style="{ width: preBlockAPIKeyLoadWidth(item.total) }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="rounded-lg bg-gray-50 p-4 text-sm text-gray-500 dark:bg-dark-700/50 dark:text-gray-400">
|
||||
{{ t('admin.riskControl.preBlockAPIKeyLoadEmpty') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showWorkerRuntimeCard" class="card">
|
||||
<div class="flex flex-col gap-4 border-b border-gray-100 px-6 py-4 dark:border-dark-700 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('admin.riskControl.workerStatus') }}</h2>
|
||||
@@ -1013,6 +1111,7 @@ import Pagination from '@/components/common/Pagination.vue'
|
||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type {
|
||||
ContentModerationAPIKeyLoad,
|
||||
ContentModerationAPIKeyStatus,
|
||||
ContentModerationConfig,
|
||||
ContentModerationLog,
|
||||
@@ -1472,6 +1571,81 @@ const queueUsageStyle = computed(() => ({
|
||||
width: queueUsagePercent.value,
|
||||
}))
|
||||
|
||||
const runtimeMode = computed<ModerationMode>(() => status.value?.mode ?? configForm.mode)
|
||||
|
||||
const showPreBlockRuntimeCard = computed(() => runtimeMode.value === 'pre_block')
|
||||
|
||||
const showWorkerRuntimeCard = computed(() => runtimeMode.value === 'observe')
|
||||
|
||||
const preBlockMetricItems = computed(() => [
|
||||
{
|
||||
key: 'active',
|
||||
label: t('admin.riskControl.preBlockActive'),
|
||||
value: formatNumber(status.value?.pre_block_active ?? 0),
|
||||
meta: t('admin.riskControl.preBlockActiveHint'),
|
||||
class: 'bg-sky-50 dark:bg-sky-900/10',
|
||||
valueClass: 'text-sky-700 dark:text-sky-300',
|
||||
},
|
||||
{
|
||||
key: 'checked',
|
||||
label: t('admin.riskControl.preBlockChecked'),
|
||||
value: formatNumber(status.value?.pre_block_checked ?? 0),
|
||||
meta: t('admin.riskControl.preBlockCheckedHint'),
|
||||
class: 'bg-gray-50 dark:bg-dark-700/50',
|
||||
valueClass: 'text-gray-900 dark:text-white',
|
||||
},
|
||||
{
|
||||
key: 'allowed',
|
||||
label: t('admin.riskControl.preBlockAllowed'),
|
||||
value: formatNumber(status.value?.pre_block_allowed ?? 0),
|
||||
meta: t('admin.riskControl.preBlockAllowedHint'),
|
||||
class: 'bg-emerald-50 dark:bg-emerald-900/10',
|
||||
valueClass: 'text-emerald-700 dark:text-emerald-300',
|
||||
},
|
||||
{
|
||||
key: 'blocked',
|
||||
label: t('admin.riskControl.preBlockBlocked'),
|
||||
value: formatNumber(status.value?.pre_block_blocked ?? 0),
|
||||
meta: t('admin.riskControl.preBlockBlockedHint'),
|
||||
class: 'bg-rose-50 dark:bg-rose-900/10',
|
||||
valueClass: 'text-rose-700 dark:text-rose-300',
|
||||
},
|
||||
{
|
||||
key: 'errors',
|
||||
label: t('admin.riskControl.preBlockErrors'),
|
||||
value: formatNumber(status.value?.pre_block_errors ?? 0),
|
||||
meta: t('admin.riskControl.preBlockErrorsHint'),
|
||||
class: 'bg-amber-50 dark:bg-amber-900/10',
|
||||
valueClass: 'text-amber-700 dark:text-amber-300',
|
||||
},
|
||||
{
|
||||
key: 'latency',
|
||||
label: t('admin.riskControl.preBlockAvgLatency'),
|
||||
value: `${formatNumber(status.value?.pre_block_avg_latency_ms ?? 0)} ms`,
|
||||
meta: t('admin.riskControl.preBlockAvgLatencyHint'),
|
||||
class: 'bg-violet-50 dark:bg-violet-900/10',
|
||||
valueClass: 'text-violet-700 dark:text-violet-300',
|
||||
},
|
||||
])
|
||||
|
||||
const preBlockAPIKeyLoads = computed<ContentModerationAPIKeyLoad[]>(() => (
|
||||
[...(status.value?.pre_block_api_key_loads ?? [])].sort((a, b) => a.index - b.index)
|
||||
))
|
||||
|
||||
const preBlockAPIKeyMaxTotal = computed(() => Math.max(1, ...preBlockAPIKeyLoads.value.map((item) => item.total || 0)))
|
||||
|
||||
const preBlockAPIKeyLoadSummaryText = computed(() => t('admin.riskControl.preBlockAPIKeyLoadSummary', {
|
||||
active: formatNumber(status.value?.pre_block_api_key_active ?? 0),
|
||||
available: formatNumber(status.value?.pre_block_api_key_available_count ?? 0),
|
||||
total: formatNumber(status.value?.pre_block_api_key_total_calls ?? 0),
|
||||
workerActive: formatNumber(status.value?.active_workers ?? 0),
|
||||
workerTotal: formatNumber(status.value?.worker_count ?? configForm.worker_count),
|
||||
}))
|
||||
|
||||
function preBlockAPIKeyLoadWidth(total: number): string {
|
||||
return `${Math.min(100, Math.max(0, (total / preBlockAPIKeyMaxTotal.value) * 100)).toFixed(1)}%`
|
||||
}
|
||||
|
||||
const workerSlots = computed(() => {
|
||||
const total = Math.max(0, status.value?.worker_count ?? configForm.worker_count)
|
||||
const active = Math.max(0, status.value?.active_workers ?? 0)
|
||||
|
||||
@@ -58,8 +58,12 @@ vi.mock('vue-i18n', async () => {
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string, params?: Record<string, string | number>) =>
|
||||
key.replace(/\{(\w+)\}/g, (_, token) => String(params?.[token] ?? `{${token}}`)),
|
||||
t: (key: string, params?: Record<string, string | number>) => {
|
||||
if (key === 'admin.riskControl.preBlockAPIKeyLoadSummary') {
|
||||
return `同步并发 ${params?.active} / 可用 Key ${params?.available},累计 ${params?.total} 次,worker:${params?.workerActive} / ${params?.workerTotal}`
|
||||
}
|
||||
return key.replace(/\{(\w+)\}/g, (_, token) => String(params?.[token] ?? `{${token}}`))
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
@@ -118,6 +122,16 @@ const runtimeStatus = () => ({
|
||||
dropped: 0,
|
||||
processed: 0,
|
||||
errors: 0,
|
||||
pre_block_active: 0,
|
||||
pre_block_checked: 0,
|
||||
pre_block_allowed: 0,
|
||||
pre_block_blocked: 0,
|
||||
pre_block_errors: 0,
|
||||
pre_block_avg_latency_ms: 0,
|
||||
pre_block_api_key_active: 0,
|
||||
pre_block_api_key_available_count: 0,
|
||||
pre_block_api_key_total_calls: 0,
|
||||
pre_block_api_key_loads: [],
|
||||
api_key_statuses: [],
|
||||
flagged_hash_count: 0,
|
||||
last_cleanup_deleted_hit: 0,
|
||||
@@ -261,4 +275,133 @@ describe('admin RiskControlView', () => {
|
||||
}))
|
||||
expect(showError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('describes worker runtime as async audit and pre-block record processing', async () => {
|
||||
getStatus.mockResolvedValue({
|
||||
...runtimeStatus(),
|
||||
mode: 'observe',
|
||||
processed: 12,
|
||||
queue_length: 2,
|
||||
})
|
||||
|
||||
const wrapper = mount(RiskControlView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: AppLayoutStub,
|
||||
BaseDialog: BaseDialogStub,
|
||||
Icon: true,
|
||||
Select: true,
|
||||
Toggle: true,
|
||||
Pagination: true,
|
||||
ModelWhitelistSelector: ModelWhitelistSelectorStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('admin.riskControl.workerStatusHint')
|
||||
expect(wrapper.text()).not.toContain('admin.riskControl.preBlockSyncStatus')
|
||||
expect(wrapper.text()).toContain('admin.riskControl.records')
|
||||
expect(wrapper.text()).toContain('12')
|
||||
expect(wrapper.text()).toContain('2 / 32,768')
|
||||
})
|
||||
|
||||
it('shows pre-block synchronous moderation metrics separately from worker queue', async () => {
|
||||
getStatus.mockResolvedValue({
|
||||
...runtimeStatus(),
|
||||
pre_block_active: 2,
|
||||
pre_block_checked: 128,
|
||||
pre_block_allowed: 120,
|
||||
pre_block_blocked: 8,
|
||||
pre_block_errors: 1,
|
||||
pre_block_avg_latency_ms: 86,
|
||||
pre_block_api_key_active: 2,
|
||||
pre_block_api_key_available_count: 2,
|
||||
pre_block_api_key_total_calls: 128,
|
||||
active_workers: 3,
|
||||
worker_count: 7,
|
||||
pre_block_api_key_loads: [
|
||||
{
|
||||
index: 0,
|
||||
key_hash: 'hash-one',
|
||||
masked: 'sk-...one',
|
||||
status: 'ok',
|
||||
active: 1,
|
||||
total: 72,
|
||||
success: 70,
|
||||
errors: 2,
|
||||
avg_latency_ms: 84,
|
||||
last_latency_ms: 80,
|
||||
last_http_status: 200,
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
key_hash: 'hash-two',
|
||||
masked: 'sk-...two',
|
||||
status: 'ok',
|
||||
active: 1,
|
||||
total: 56,
|
||||
success: 56,
|
||||
errors: 0,
|
||||
avg_latency_ms: 90,
|
||||
last_latency_ms: 92,
|
||||
last_http_status: 200,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const wrapper = mount(RiskControlView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: AppLayoutStub,
|
||||
BaseDialog: BaseDialogStub,
|
||||
Icon: true,
|
||||
Select: true,
|
||||
Toggle: true,
|
||||
Pagination: true,
|
||||
ModelWhitelistSelector: ModelWhitelistSelectorStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('admin.riskControl.preBlockSyncStatus')
|
||||
expect(wrapper.text()).toContain('admin.riskControl.preBlockSyncHint')
|
||||
expect(wrapper.text()).not.toContain('admin.riskControl.workerStatus')
|
||||
expect(wrapper.text()).toContain('admin.riskControl.records')
|
||||
expect(wrapper.text()).toContain('128')
|
||||
expect(wrapper.text()).toContain('120')
|
||||
expect(wrapper.text()).toContain('8')
|
||||
expect(wrapper.text()).toContain('86 ms')
|
||||
expect(wrapper.text()).toContain('admin.riskControl.preBlockAPIKeyLoad')
|
||||
expect(wrapper.text()).toContain('sk-...one')
|
||||
expect(wrapper.text()).toContain('sk-...two')
|
||||
expect(wrapper.text()).toContain('72')
|
||||
expect(wrapper.text()).toContain('56')
|
||||
expect(wrapper.text()).toContain('同步并发 2 / 可用 Key 2,累计 128 次,worker:3 / 7')
|
||||
|
||||
const runtimeCards = wrapper.get('[data-test="pre-block-runtime-cards"]')
|
||||
const syncCard = wrapper.get('[data-test="pre-block-sync-card"]')
|
||||
const apiKeyLoadCard = wrapper.get('[data-test="pre-block-api-key-load-card"]')
|
||||
|
||||
expect(runtimeCards.classes()).toEqual(expect.arrayContaining([
|
||||
'grid',
|
||||
'grid-cols-1',
|
||||
'xl:grid-cols-[minmax(0,520px)_minmax(0,1fr)]',
|
||||
]))
|
||||
expect(syncCard.element.parentElement).toBe(runtimeCards.element)
|
||||
expect(apiKeyLoadCard.element.parentElement).toBe(runtimeCards.element)
|
||||
expect(syncCard.classes()).toContain('card')
|
||||
expect(apiKeyLoadCard.classes()).toContain('card')
|
||||
expect(syncCard.get('h2').text()).toBe('admin.riskControl.preBlockSyncStatus')
|
||||
expect(syncCard.text()).toContain('admin.riskControl.preBlockSyncHint')
|
||||
expect(apiKeyLoadCard.get('h2').text()).toBe('admin.riskControl.preBlockAPIKeyLoad')
|
||||
expect(apiKeyLoadCard.text()).toContain('admin.riskControl.preBlockAPIKeyLoadHint')
|
||||
expect(wrapper.get('[data-test="pre-block-api-key-load-list"]').classes()).toEqual(expect.arrayContaining([
|
||||
'max-h-[280px]',
|
||||
'overflow-y-auto',
|
||||
]))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user