fix: restore security hardening compatibility and validation

Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
This commit is contained in:
elky
2026-09-07 21:14:27 +08:00
parent a5c3699ae9
commit a90d564931
191 changed files with 6785 additions and 1643 deletions
@@ -1976,7 +1976,7 @@ const usageRuntimeWorkerFaults = computed(() => {
usage.workerReclaimFailuresTotal,
]
if (!values.some(value => value != null)) return null
return values.reduce((total, value) => total + (value ?? 0), 0)
return values.reduce<number>((total, value) => total + (value ?? 0), 0)
})
const usageQueueOldestPendingIdleText = computed(() => (
formatDurationSeconds((gatewayMetrics.value?.usageQueue.oldestPendingIdleMs ?? null) == null
@@ -2009,7 +2009,7 @@ const usageCounterOutboxFailures = computed(() => {
counter.cleanupFailedBatchesTotal,
]
if (!values.some(value => value != null)) return null
return values.reduce((total, value) => total + (value ?? 0), 0)
return values.reduce<number>((total, value) => total + (value ?? 0), 0)
})
const candidateQueueUtilization = computed(() => {
const depth = gatewayMetrics.value?.requestCandidateQueue.depth
+1
View File
@@ -1019,6 +1019,7 @@ function buildAdminWalletFromApiKey(apiKey: AdminApiKey): AdminWallet | null {
owner_type: 'api_key',
owner_name: apiKey.name || apiKey.key_display || null,
created_at: apiKey.created_at || apiKey.wallet.updated_at || '',
updated_at: apiKey.wallet.updated_at || apiKey.created_at || '',
}
}
+4 -4
View File
@@ -803,8 +803,8 @@ interface ModelProviderDisplay {
id: string
model_id?: string | null
name: string
provider_type: string
target_model: string
provider_type?: string
target_model?: string
is_active: boolean
input_price_per_1m?: number | null
output_price_per_1m?: number | null
@@ -935,7 +935,7 @@ function hasTieredPricing(model: GlobalModelResponse): boolean {
// 检测是否有视频分辨率计费配置
function hasVideoPricing(model: GlobalModelResponse): boolean {
const priceByResolution = model.config?.billing?.video?.price_per_second_by_resolution
return priceByResolution && typeof priceByResolution === 'object' && Object.keys(priceByResolution).length > 0
return !!priceByResolution && typeof priceByResolution === 'object' && Object.keys(priceByResolution).length > 0
}
// 获取视频分辨率计费的数量
@@ -1934,7 +1934,7 @@ function handleDrawerOpenChange(value: boolean) {
// 编辑提供商模型
function openEditProviderImplementation(provider: ModelProviderDisplay) {
editingProvider.value = provider
editingProvider.value = selectedModelProviders.value.find((item) => item.id === provider.id && item.model_id === provider.model_id) ?? provider
editProviderDialogOpen.value = true
}
+5 -4
View File
@@ -980,7 +980,7 @@
v-model="showAccountBatchDialog"
:provider-id="selectedProviderId"
:provider-name="selectedProviderData?.name || ''"
:provider-type="selectedProviderData?.provider_type || selectedProviderType"
:provider-type="selectedProviderData?.provider_type ?? selectedProviderOverview?.provider_type ?? undefined"
:batch-concurrency="selectedProviderConfig?.batch_concurrency"
:selected-keys="selectedPoolKeys"
:select-all-filtered="selectAllFilteredPoolKeys"
@@ -1004,7 +1004,7 @@
v-if="selectedProviderId"
:open="keyFormDialogOpen"
:endpoint="null"
:provider-type="selectedProviderData?.provider_type || selectedProviderType"
:provider-type="selectedProviderData?.provider_type ?? selectedProviderOverview?.provider_type ?? null"
:editing-key="editingKey"
:provider-id="selectedProviderId"
:available-api-formats="selectedProviderData?.api_formats || []"
@@ -1136,6 +1136,7 @@ import {
} from '@/features/pool/utils/poolManagementState'
import type { PoolBatchActionValue } from '@/features/pool/utils/poolBatchActions'
import {
buildAccountTotalStatsDisplay,
buildPoolStatsDisplay,
type PoolCodexCycleStatsGroup,
type PoolStatsDisplay,
@@ -1503,7 +1504,7 @@ function appendDemandMetricSample(overview: PoolOverviewItem | null): void {
const existing = providerDemandMetricSamples.value.filter(
sample => sample.providerId === overview.provider_id,
)
const lastSample = existing.at(-1)
const lastSample = existing[existing.length - 1]
if (
lastSample
&& nextSample.sampledAt - lastSample.sampledAt < 1000
@@ -2137,7 +2138,7 @@ function getPoolKeyAccountStatsMetrics(key: PoolKeyDetail): PoolStatsMetric[] {
const display = getPoolKeyStatsDisplay(key)
return display.kind === 'account_total'
? display.metrics
: buildPoolStatsDisplay(key, selectedProviderType.value, 'account_total').metrics
: buildAccountTotalStatsDisplay(key).metrics
}
const quotaRefreshSupported = computed(() => {
+4 -4
View File
@@ -752,8 +752,8 @@
:show-scheduling-mode="false"
:subtitle="`仅作用于 ${activePerModelPolicy.model}`"
@update:config="updateEditingConfig"
@update:priority-mode="mode => updateModelPriorityMode(activePerModelPolicy.model, mode)"
@update:scheduling-mode="mode => updateModelSchedulingMode(activePerModelPolicy.model, mode)"
@update:priority-mode="mode => activePerModelPolicy && updateModelPriorityMode(activePerModelPolicy.model, mode)"
@update:scheduling-mode="mode => activePerModelPolicy && updateModelSchedulingMode(activePerModelPolicy.model, mode)"
/>
</div>
</div>
@@ -1649,9 +1649,9 @@ async function saveDraft(): Promise<void> {
: undefined,
config_json: config,
}
const saved = wasCreating
const saved = wasCreating || !targetGroupId
? await createRoutingGroup(payload)
: await updateRoutingGroup(draft.value.id, payload)
: await updateRoutingGroup(targetGroupId, payload)
const sameDraftGeneration = draftGeneration === submittedGeneration
const stillEditingSubmittedDraft = wasCreating
+1 -1
View File
@@ -253,7 +253,7 @@ async function loadSummary() {
user_id: selectedUserId.value
})
if (requestId !== summaryRequestId) return
userSummary.value = summary
userSummary.value = { ...summary, error_rate: summary.error_rate ?? 0 }
} finally {
if (requestId === summaryRequestId) {
summaryLoading.value = false
+5 -3
View File
@@ -739,7 +739,7 @@ function editUser(user: User) {
editingUser.value = {
id: user.id,
username: user.username,
email: user.email,
email: user.email ?? '',
unlimited: user.unlimited,
role: user.role,
is_active: user.is_active,
@@ -1120,6 +1120,8 @@ async function closeNewApiKeyDialog() {
}
async function deleteApiKey(apiKey: ApiKey) {
const user = selectedUser.value
if (!user) return
const confirmed = await confirmDanger(
locale.value === 'en-US'
? `Delete this API key?\n\n${apiKey.key_display || '****'}\n\nThis action cannot be undone.`
@@ -1130,8 +1132,8 @@ async function deleteApiKey(apiKey: ApiKey) {
if (!confirmed) return
try {
await usersStore.deleteApiKey(selectedUser.value.id, apiKey.id)
await loadUserApiKeys(selectedUser.value.id)
await usersStore.deleteApiKey(user.id, apiKey.id)
if (selectedUser.value?.id === user.id) await loadUserApiKeys(user.id)
success(legacyT('API Key已删除'))
} catch (err: unknown) {
error(localizedApiError(err, '未知错误'), legacyT('删除 API Key 失败'))
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, nextTick, type App } from 'vue'
import type { ProviderType } from '@/api/endpoints/types'
import PoolManagement from '@/views/admin/PoolManagement.vue'
import type { PoolKeyDetail, PoolOverviewItem, PoolKeysPageResponse } from '@/api/endpoints/pool'
import { POOL_MANAGEMENT_VIEW_STORAGE_KEY } from '@/features/pool/utils/poolManagementState'
@@ -481,7 +482,7 @@ vi.mock('@/features/providers/components/ProxyNodeSelect.vue', async () => {
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
function createOverview(providerType: string): PoolOverviewItem {
function createOverview(providerType: ProviderType): PoolOverviewItem {
return {
provider_id: `${providerType}-provider`,
provider_name: `${providerType} Provider`,
@@ -875,8 +876,11 @@ describe('PoolManagement Codex cycle stats mode', () => {
const codexKey = createPoolKey('codex')
const resetKey = createPoolKey('codex', {
status_snapshot: {
...codexKey.status_snapshot,
oauth: codexKey.status_snapshot?.oauth ?? { code: 'none' },
account: codexKey.status_snapshot?.account ?? { code: 'ok', blocked: false },
quota: {
code: 'ok',
exhausted: false,
...codexKey.status_snapshot?.quota,
windows: [
{
@@ -996,7 +1000,8 @@ describe('PoolManagement Codex cycle stats mode', () => {
oauth_managed: true,
can_refresh_oauth: true,
status_snapshot: {
...createPoolKey('codex').status_snapshot,
account: { code: 'ok', blocked: false },
quota: { code: 'ok', exhausted: false },
oauth: { code: 'expired', expires_at: 1 },
},
})
@@ -1022,14 +1027,14 @@ describe('PoolManagement Codex cycle stats mode', () => {
})
it('hides the stats mode switch for non-Codex providers and keeps account totals', async () => {
const openaiKey = createPoolKey('openai', {
const customKey = createPoolKey('custom', {
request_count: 12,
total_tokens: 3456,
total_cost_usd: '1.25',
})
endpointMocks.getPoolOverview.mockResolvedValue({ items: [createOverview('openai')] })
endpointMocks.listPoolKeys.mockResolvedValue(createKeyPage(openaiKey))
endpointMocks.getProvider.mockResolvedValue(createProvider('openai'))
endpointMocks.getPoolOverview.mockResolvedValue({ items: [createOverview('custom')] })
endpointMocks.listPoolKeys.mockResolvedValue(createKeyPage(customKey))
endpointMocks.getProvider.mockResolvedValue(createProvider('custom'))
const root = mountPoolManagement()
await settle()
@@ -133,9 +133,9 @@ export function modelDirectiveBuiltInMappingPreview(
: undefined
}
const isReasoningEffort = REASONING_EFFORTS.some(effort => effort === normalizedSuffix)
const reasoningEffort = REASONING_EFFORTS.find(effort => effort === normalizedSuffix)
const isCodexUltra = normalizedSuffix === 'ultra'
if (!isReasoningEffort && !isCodexUltra) return undefined
if (!reasoningEffort && !isCodexUltra) return undefined
switch (apiFormat) {
case 'openai:chat':
@@ -145,22 +145,22 @@ export function modelDirectiveBuiltInMappingPreview(
case 'openai:search':
return { reasoning: { effort: normalizedSuffix } }
case 'claude:messages': {
if (!isReasoningEffort) return undefined
if (!reasoningEffort) return undefined
return {
output_config: { effort: claudeEffortValue(normalizedSuffix) },
output_config: { effort: claudeEffortValue(reasoningEffort) },
thinking: {
type: 'enabled',
budget_tokens: thinkingBudgetTokens(normalizedSuffix),
budget_tokens: thinkingBudgetTokens(reasoningEffort),
},
}
}
case 'gemini:generate_content': {
if (!isReasoningEffort) return undefined
if (!reasoningEffort) return undefined
return {
generationConfig: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: thinkingBudgetTokens(normalizedSuffix),
thinkingBudget: thinkingBudgetTokens(reasoningEffort),
},
},
}
@@ -55,7 +55,7 @@
<Select
:model-value="aggregateMergeMode"
:open="aggregateMergeModeSelectOpen"
@update:model-value="$emit('update:aggregateMergeMode', $event)"
@update:model-value="($event === 'skip' || $event === 'overwrite' || $event === 'error') && $emit('update:aggregateMergeMode', $event)"
@update:open="$emit('update:aggregateMergeModeSelectOpen', $event)"
>
<SelectTrigger>
@@ -448,7 +448,7 @@ defineEmits<{
const cleanupRuns = ref<CleanupRunRecord[]>([])
const cleanupRunsLoading = ref(false)
let cleanupRunsTimer: ReturnType<typeof window.setInterval> | null = null
let cleanupRunsTimer: number | null = null
const manualCleanupDialogOpen = ref(false)
const manualCleanupRunning = ref(false)
@@ -40,7 +40,7 @@
<Select
:model-value="mergeMode"
:open="mergeModeSelectOpen"
@update:model-value="$emit('update:mergeMode', $event)"
@update:model-value="($event === 'skip' || $event === 'overwrite' || $event === 'error') && $emit('update:mergeMode', $event)"
@update:open="$emit('update:mergeModeSelectOpen', $event)"
>
<SelectTrigger>
@@ -275,7 +275,7 @@ const taskError = ref<string | null>(null)
let previewDebounceTimer: ReturnType<typeof setTimeout> | null = null
let previewSeq = 0
let taskPollTimer: ReturnType<typeof window.setInterval> | null = null
let taskPollTimer: number | null = null
const modeOptions: Array<{ value: ManualCleanupMode; label: string; description: string }> = [
{ value: 'policy', label: '按当前策略', description: '沿用页面上配置的保留天数' },
@@ -465,7 +465,7 @@ async function handleConfirm() {
taskError.value = null
try {
const response = await adminApi.runManualUsageCleanup(buildRequest())
if ('detail' in response && response.detail === 'usage_cleanup_already_running') {
if ('detail' in response) {
taskError.value = response.message
emit('running-change', false)
return
@@ -33,7 +33,7 @@
<Select
:model-value="usersMergeMode"
:open="usersMergeModeSelectOpen"
@update:model-value="$emit('update:usersMergeMode', $event)"
@update:model-value="($event === 'skip' || $event === 'overwrite' || $event === 'error') && $emit('update:usersMergeMode', $event)"
@update:open="$emit('update:usersMergeModeSelectOpen', $event)"
>
<SelectTrigger>
@@ -41,7 +41,7 @@ describe('useSystemConfig', () => {
})
it('loads config keys in one request and keeps change detection disabled until the baseline is ready', async () => {
let resolveConfigs: ((value: Array<{ key: string, value: unknown, is_set?: boolean }>) => void) | null = null
let resolveConfigs!: (value: Array<{ key: string, value: unknown, is_set?: boolean }>) => void
getAllSystemConfigsMock.mockImplementation(() => new Promise((resolve) => {
resolveConfigs = resolve
}))
@@ -56,7 +56,7 @@ describe('useSystemConfig', () => {
expect(state.systemConfigLoading.value).toBe(true)
expect(state.hasLogConfigChanges.value).toBe(false)
resolveConfigs?.([
resolveConfigs([
{ key: 'request_record_level', value: 'basic' },
{ key: 'proxy_node_metrics_cleanup_batch_size', value: 5000 },
])
@@ -106,7 +106,7 @@ function downloadJson(data: unknown, filename: string) {
URL.revokeObjectURL(url)
}
export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
export function useConfigExportImport(systemConfig: { value: Pick<SystemConfig, 'site_name'> }) {
const { success, error } = useToast()
// 配置导出/导入相关
@@ -296,7 +296,7 @@ export function useSystemConfig() {
continue
}
if (response.value !== null && response.value !== undefined) {
; (nextConfig as Record<string, unknown>)[key] = response.value
Reflect.set(nextConfig, key, response.value)
}
} catch {
// 单个配置项加载失败时忽略,使用默认值