feat(proxy,sora): 增强代理质量检测与Sora稳定性并修复审查问题

This commit is contained in:
yangjianbo
2026-02-19 21:18:35 +08:00
parent 36a1a7998b
commit 46d9aee6dd
23 changed files with 1408 additions and 45 deletions

View File

@@ -55,6 +55,15 @@
<Icon name="play" size="md" class="mr-2" />
{{ t('admin.proxies.testConnection') }}
</button>
<button
@click="handleBatchQualityCheck"
:disabled="batchQualityChecking || loading"
class="btn btn-secondary"
:title="t('admin.proxies.batchQualityCheck')"
>
<Icon name="shield" size="md" class="mr-2" :class="batchQualityChecking ? 'animate-pulse' : ''" />
{{ t('admin.proxies.batchQualityCheck') }}
</button>
<button
@click="openBatchDelete"
:disabled="selectedCount === 0"
@@ -203,6 +212,34 @@
<Icon v-else name="checkCircle" size="sm" />
<span class="text-xs">{{ t('admin.proxies.testConnection') }}</span>
</button>
<button
@click="handleQualityCheck(row)"
:disabled="qualityCheckingProxyIds.has(row.id)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
>
<svg
v-if="qualityCheckingProxyIds.has(row.id)"
class="h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<Icon v-else name="shield" size="sm" />
<span class="text-xs">{{ t('admin.proxies.qualityCheck') }}</span>
</button>
<button
@click="handleEdit(row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400"
@@ -623,6 +660,82 @@
@imported="handleDataImported"
/>
<BaseDialog
:show="showQualityReportDialog"
:title="t('admin.proxies.qualityReportTitle')"
width="normal"
@close="closeQualityReportDialog"
>
<div v-if="qualityReport" class="space-y-4">
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700">
<div class="flex items-center justify-between gap-4">
<div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ qualityReportProxy?.name || '-' }}
</div>
<div class="mt-1 text-sm text-gray-700 dark:text-gray-200">
{{ qualityReport.summary }}
</div>
</div>
<div class="text-right">
<div class="text-2xl font-semibold text-gray-900 dark:text-white">
{{ qualityReport.score }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.proxies.qualityGrade', { grade: qualityReport.grade }) }}
</div>
</div>
</div>
<div class="mt-3 grid grid-cols-2 gap-2 text-xs text-gray-600 dark:text-gray-300">
<div>{{ t('admin.proxies.qualityExitIP') }}: {{ qualityReport.exit_ip || '-' }}</div>
<div>{{ t('admin.proxies.qualityCountry') }}: {{ qualityReport.country || '-' }}</div>
<div>
{{ t('admin.proxies.qualityBaseLatency') }}:
{{ typeof qualityReport.base_latency_ms === 'number' ? `${qualityReport.base_latency_ms}ms` : '-' }}
</div>
<div>{{ t('admin.proxies.qualityCheckedAt') }}: {{ new Date(qualityReport.checked_at * 1000).toLocaleString() }}</div>
</div>
</div>
<div class="max-h-80 overflow-auto rounded-lg border border-gray-200 dark:border-dark-600">
<table class="min-w-full divide-y divide-gray-200 text-sm dark:divide-dark-700">
<thead class="bg-gray-50 text-xs uppercase text-gray-500 dark:bg-dark-800 dark:text-dark-400">
<tr>
<th class="px-3 py-2 text-left">{{ t('admin.proxies.qualityTableTarget') }}</th>
<th class="px-3 py-2 text-left">{{ t('admin.proxies.qualityTableStatus') }}</th>
<th class="px-3 py-2 text-left">HTTP</th>
<th class="px-3 py-2 text-left">{{ t('admin.proxies.qualityTableLatency') }}</th>
<th class="px-3 py-2 text-left">{{ t('admin.proxies.qualityTableMessage') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dark-700 dark:bg-dark-900">
<tr v-for="item in qualityReport.items" :key="item.target">
<td class="px-3 py-2 text-gray-900 dark:text-white">{{ qualityTargetLabel(item.target) }}</td>
<td class="px-3 py-2">
<span class="badge" :class="qualityStatusClass(item.status)">{{ qualityStatusLabel(item.status) }}</span>
</td>
<td class="px-3 py-2 text-gray-600 dark:text-gray-300">{{ item.http_status ?? '-' }}</td>
<td class="px-3 py-2 text-gray-600 dark:text-gray-300">
{{ typeof item.latency_ms === 'number' ? `${item.latency_ms}ms` : '-' }}
</td>
<td class="px-3 py-2 text-gray-600 dark:text-gray-300">
<span>{{ item.message || '-' }}</span>
<span v-if="item.cf_ray" class="ml-1 text-xs text-gray-400">(cf-ray: {{ item.cf_ray }})</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<button @click="closeQualityReportDialog" class="btn btn-secondary">
{{ t('common.close') }}
</button>
</div>
</template>
</BaseDialog>
<!-- Proxy Accounts Dialog -->
<BaseDialog
:show="showAccountsModal"
@@ -675,7 +788,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
import type { Proxy, ProxyAccountSummary, ProxyProtocol } from '@/types'
import type { Proxy, ProxyAccountSummary, ProxyProtocol, ProxyQualityCheckResult } from '@/types'
import type { Column } from '@/components/common/types'
import AppLayout from '@/components/layout/AppLayout.vue'
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
@@ -756,13 +869,18 @@ const showAccountsModal = ref(false)
const submitting = ref(false)
const exportingData = ref(false)
const testingProxyIds = ref<Set<number>>(new Set())
const qualityCheckingProxyIds = ref<Set<number>>(new Set())
const batchTesting = ref(false)
const batchQualityChecking = ref(false)
const selectedProxyIds = ref<Set<number>>(new Set())
const accountsProxy = ref<Proxy | null>(null)
const proxyAccounts = ref<ProxyAccountSummary[]>([])
const accountsLoading = ref(false)
const editingProxy = ref<Proxy | null>(null)
const deletingProxy = ref<Proxy | null>(null)
const showQualityReportDialog = ref(false)
const qualityReportProxy = ref<Proxy | null>(null)
const qualityReport = ref<ProxyQualityCheckResult | null>(null)
const selectedCount = computed(() => selectedProxyIds.value.size)
const allVisibleSelected = computed(() => {
@@ -1150,6 +1268,16 @@ const stopTestingProxy = (proxyId: number) => {
testingProxyIds.value = next
}
const startQualityCheckingProxy = (proxyId: number) => {
qualityCheckingProxyIds.value = new Set([...qualityCheckingProxyIds.value, proxyId])
}
const stopQualityCheckingProxy = (proxyId: number) => {
const next = new Set(qualityCheckingProxyIds.value)
next.delete(proxyId)
qualityCheckingProxyIds.value = next
}
const runProxyTest = async (proxyId: number, notify: boolean) => {
startTestingProxy(proxyId)
try {
@@ -1183,6 +1311,134 @@ const handleTestConnection = async (proxy: Proxy) => {
await runProxyTest(proxy.id, true)
}
const handleQualityCheck = async (proxy: Proxy) => {
startQualityCheckingProxy(proxy.id)
try {
const result = await adminAPI.proxies.checkProxyQuality(proxy.id)
qualityReportProxy.value = proxy
qualityReport.value = result
showQualityReportDialog.value = true
const baseStep = result.items.find((item) => item.target === 'base_connectivity')
if (baseStep && baseStep.status === 'pass') {
applyLatencyResult(proxy.id, {
success: true,
latency_ms: result.base_latency_ms,
message: result.summary,
ip_address: result.exit_ip,
country: result.country,
country_code: result.country_code
})
}
appStore.showSuccess(
t('admin.proxies.qualityCheckDone', { score: result.score, grade: result.grade })
)
} catch (error: any) {
const message = error.response?.data?.detail || t('admin.proxies.qualityCheckFailed')
appStore.showError(message)
console.error('Error checking proxy quality:', error)
} finally {
stopQualityCheckingProxy(proxy.id)
}
}
const runBatchProxyQualityChecks = async (ids: number[]) => {
if (ids.length === 0) return { total: 0, healthy: 0, warn: 0, challenge: 0, failed: 0 }
const concurrency = 3
let index = 0
let healthy = 0
let warn = 0
let challenge = 0
let failed = 0
const worker = async () => {
while (index < ids.length) {
const current = ids[index]
index++
startQualityCheckingProxy(current)
try {
const result = await adminAPI.proxies.checkProxyQuality(current)
const target = proxies.value.find((proxy) => proxy.id === current)
if (target) {
const baseStep = result.items.find((item) => item.target === 'base_connectivity')
if (baseStep && baseStep.status === 'pass') {
applyLatencyResult(current, {
success: true,
latency_ms: result.base_latency_ms,
message: result.summary,
ip_address: result.exit_ip,
country: result.country,
country_code: result.country_code
})
}
}
if (result.challenge_count > 0) {
challenge++
} else if (result.failed_count > 0) {
failed++
} else if (result.warn_count > 0) {
warn++
} else {
healthy++
}
} catch {
failed++
} finally {
stopQualityCheckingProxy(current)
}
}
}
const workers = Array.from({ length: Math.min(concurrency, ids.length) }, () => worker())
await Promise.all(workers)
return {
total: ids.length,
healthy,
warn,
challenge,
failed
}
}
const closeQualityReportDialog = () => {
showQualityReportDialog.value = false
qualityReportProxy.value = null
qualityReport.value = null
}
const qualityStatusClass = (status: string) => {
if (status === 'pass') return 'badge-success'
if (status === 'warn') return 'badge-warning'
if (status === 'challenge') return 'badge-danger'
return 'badge-danger'
}
const qualityStatusLabel = (status: string) => {
if (status === 'pass') return t('admin.proxies.qualityStatusPass')
if (status === 'warn') return t('admin.proxies.qualityStatusWarn')
if (status === 'challenge') return t('admin.proxies.qualityStatusChallenge')
return t('admin.proxies.qualityStatusFail')
}
const qualityTargetLabel = (target: string) => {
switch (target) {
case 'base_connectivity':
return t('admin.proxies.qualityTargetBase')
case 'openai':
return 'OpenAI'
case 'anthropic':
return 'Anthropic'
case 'gemini':
return 'Gemini'
case 'sora':
return 'Sora'
default:
return target
}
}
const fetchAllProxiesForBatch = async (): Promise<Proxy[]> => {
const pageSize = 200
const result: Proxy[] = []
@@ -1253,6 +1509,43 @@ const handleBatchTest = async () => {
}
}
const handleBatchQualityCheck = async () => {
if (batchQualityChecking.value) return
batchQualityChecking.value = true
try {
let ids: number[] = []
if (selectedCount.value > 0) {
ids = Array.from(selectedProxyIds.value)
} else {
const allProxies = await fetchAllProxiesForBatch()
ids = allProxies.map((proxy) => proxy.id)
}
if (ids.length === 0) {
appStore.showInfo(t('admin.proxies.batchQualityEmpty'))
return
}
const summary = await runBatchProxyQualityChecks(ids)
appStore.showSuccess(
t('admin.proxies.batchQualityDone', {
count: summary.total,
healthy: summary.healthy,
warn: summary.warn,
challenge: summary.challenge,
failed: summary.failed
})
)
loadProxies()
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.proxies.batchQualityFailed'))
console.error('Error batch checking quality:', error)
} finally {
batchQualityChecking.value = false
}
}
const formatExportTimestamp = () => {
const now = new Date()
const pad2 = (value: number) => String(value).padStart(2, '0')