Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
46d9aee6
Commit
46d9aee6
authored
Feb 19, 2026
by
yangjianbo
Browse files
feat(proxy,sora): 增强代理质量检测与Sora稳定性并修复审查问题
parent
36a1a799
Changes
23
Hide whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/zh.ts
View file @
46d9aee6
...
...
@@ -2246,6 +2246,8 @@ export default {
noProxiesYet
:
'
暂无代理
'
,
createFirstProxy
:
'
添加您的第一个代理以开始使用。
'
,
testConnection
:
'
测试连接
'
,
qualityCheck
:
'
质量检测
'
,
batchQualityCheck
:
'
批量质量检测
'
,
batchTest
:
'
批量测试
'
,
testFailed
:
'
失败
'
,
latencyFailed
:
'
链接失败
'
,
...
...
@@ -2293,6 +2295,26 @@ export default {
proxyWorking
:
'
代理连接正常
'
,
proxyWorkingWithLatency
:
'
代理连接正常,延迟 {latency}ms
'
,
proxyTestFailed
:
'
代理测试失败
'
,
qualityCheckDone
:
'
质量检测完成:评分 {score}({grade})
'
,
qualityCheckFailed
:
'
代理质量检测失败
'
,
batchQualityDone
:
'
批量质量检测完成,共检测 {count} 个;优质 {healthy} 个,告警 {warn} 个,挑战 {challenge} 个,异常 {failed} 个
'
,
batchQualityFailed
:
'
批量质量检测失败
'
,
batchQualityEmpty
:
'
暂无可检测质量的代理
'
,
qualityReportTitle
:
'
代理质量检测报告
'
,
qualityGrade
:
'
等级 {grade}
'
,
qualityExitIP
:
'
出口 IP
'
,
qualityCountry
:
'
出口地区
'
,
qualityBaseLatency
:
'
基础延迟
'
,
qualityCheckedAt
:
'
检测时间
'
,
qualityTableTarget
:
'
检测项
'
,
qualityTableStatus
:
'
状态
'
,
qualityTableLatency
:
'
延迟
'
,
qualityTableMessage
:
'
说明
'
,
qualityStatusPass
:
'
通过
'
,
qualityStatusWarn
:
'
告警
'
,
qualityStatusFail
:
'
失败
'
,
qualityStatusChallenge
:
'
挑战
'
,
qualityTargetBase
:
'
基础连通性
'
,
proxyCreatedSuccess
:
'
代理添加成功
'
,
proxyUpdatedSuccess
:
'
代理更新成功
'
,
proxyDeletedSuccess
:
'
代理删除成功
'
,
...
...
frontend/src/types/index.ts
View file @
46d9aee6
...
...
@@ -524,6 +524,32 @@ export interface ProxyAccountSummary {
notes
?:
string
|
null
}
export
interface
ProxyQualityCheckItem
{
target
:
string
status
:
'
pass
'
|
'
warn
'
|
'
fail
'
|
'
challenge
'
http_status
?:
number
latency_ms
?:
number
message
?:
string
cf_ray
?:
string
}
export
interface
ProxyQualityCheckResult
{
proxy_id
:
number
score
:
number
grade
:
string
summary
:
string
exit_ip
?:
string
country
?:
string
country_code
?:
string
base_latency_ms
?:
number
passed_count
:
number
warn_count
:
number
failed_count
:
number
challenge_count
:
number
checked_at
:
number
items
:
ProxyQualityCheckItem
[]
}
// Gemini credentials structure for OAuth and API Key authentication
export
interface
GeminiCredentials
{
// API Key authentication
...
...
frontend/src/views/admin/ProxiesView.vue
View file @
46d9aee6
...
...
@@ -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
'
)
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment