mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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 || '',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
// 单个配置项加载失败时忽略,使用默认值
|
||||
|
||||
Reference in New Issue
Block a user