mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(proxy,oauth,pool): H2 头过滤、OAuth 过期分级标记与批量操作进度条
- proxy: 屏蔽 host/content-length 头转发,避免 H2 PROTOCOL_ERROR - oauth: 区分 [REFRESH_FAILED] 与 [OAUTH_EXPIRED] 标记,token 过期 自动阻止调度但不停用账号,便于管理员恢复 - pool/account_state: 识别新增的 OAUTH_EXPIRED/REFRESH_FAILED 前缀 - 前端: 批量操作显示实时进度条,倍率编辑 Escape/blur 竞态修复, OAuth 刷新失败后自动刷新列表
This commit is contained in:
@@ -223,7 +223,22 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="lastResultMessage"
|
||||
v-if="executing && progressTotal > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ progressLabel }}</span>
|
||||
<span>{{ progressDone }} / {{ progressTotal }}</span>
|
||||
</div>
|
||||
<div class="h-1.5 w-full rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full bg-primary transition-all duration-150"
|
||||
:style="{ width: `${Math.round((progressDone / progressTotal) * 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="lastResultMessage"
|
||||
class="rounded-md border bg-background px-3 py-2 text-xs text-muted-foreground"
|
||||
>
|
||||
{{ lastResultMessage }}
|
||||
@@ -322,6 +337,9 @@ const searchText = ref('')
|
||||
const selectedAction = ref<BatchActionValue>('delete')
|
||||
const proxyNodeIdForAction = ref('')
|
||||
const lastResultMessage = ref('')
|
||||
const progressTotal = ref(0)
|
||||
const progressDone = ref(0)
|
||||
const progressLabel = ref('')
|
||||
const activeQuickSelectors = ref<QuickSelectorValue[]>([])
|
||||
const currentPage = ref(1)
|
||||
const PAGE_SIZE = 50
|
||||
@@ -571,6 +589,12 @@ async function executeAction(): Promise<void> {
|
||||
let failedCount = 0
|
||||
let skippedCount = 0
|
||||
|
||||
const actionLabel = ACTION_OPTIONS.find((a) => a.value === selectedAction.value)?.label || '执行'
|
||||
progressDone.value = 0
|
||||
progressTotal.value = selectedKeys.length
|
||||
progressLabel.value = `正在${actionLabel}...`
|
||||
lastResultMessage.value = ''
|
||||
|
||||
try {
|
||||
if (selectedAction.value === 'refresh_quota') {
|
||||
const targetIds = selectedKeys.map((key) => key.key_id)
|
||||
@@ -578,6 +602,7 @@ async function executeAction(): Promise<void> {
|
||||
successCount = Number(result.success || 0)
|
||||
failedCount = Number(result.failed || 0)
|
||||
skippedCount = Math.max(0, targetIds.length - Number(result.total || 0))
|
||||
progressDone.value = targetIds.length
|
||||
} else {
|
||||
const CONCURRENCY = props.batchConcurrency || 8
|
||||
const taskForKey = (key: PoolKeyDetail): (() => Promise<'success' | 'skip'>) | null => {
|
||||
@@ -609,8 +634,12 @@ async function executeAction(): Promise<void> {
|
||||
for (const key of selectedKeys) {
|
||||
const task = taskForKey(key)
|
||||
if (task) tasks.push(task)
|
||||
else skippedCount += 1
|
||||
else {
|
||||
skippedCount += 1
|
||||
progressDone.value += 1
|
||||
}
|
||||
}
|
||||
progressTotal.value = selectedKeys.length
|
||||
|
||||
// 并发执行,限制并发数
|
||||
let cursor = 0
|
||||
@@ -623,6 +652,7 @@ async function executeAction(): Promise<void> {
|
||||
} catch {
|
||||
failedCount += 1
|
||||
}
|
||||
progressDone.value += 1
|
||||
}
|
||||
}
|
||||
const workers = Array.from({ length: Math.min(CONCURRENCY, tasks.length) }, () => runNext())
|
||||
@@ -647,6 +677,9 @@ async function executeAction(): Promise<void> {
|
||||
showError(parseApiError(err, '批量操作失败'))
|
||||
} finally {
|
||||
executing.value = false
|
||||
progressTotal.value = 0
|
||||
progressDone.value = 0
|
||||
progressLabel.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2059,19 +2059,16 @@ function startEditMultiplier(key: EndpointAPIKey, format: string) {
|
||||
function cancelEditMultiplier() {
|
||||
editingMultiplierKey.value = null
|
||||
editingMultiplierFormat.value = null
|
||||
multiplierSaving.value = false
|
||||
}
|
||||
|
||||
function handleMultiplierKeydown(e: KeyboardEvent, key: EndpointAPIKey, format: string) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!multiplierSaving.value) {
|
||||
multiplierSaving.value = true
|
||||
saveMultiplier(key, format)
|
||||
}
|
||||
saveMultiplier(key, format)
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
multiplierSaving.value = true // 阻止 blur 触发保存
|
||||
cancelEditMultiplier()
|
||||
}
|
||||
}
|
||||
@@ -2082,7 +2079,7 @@ function handleMultiplierBlur(key: EndpointAPIKey, format: string) {
|
||||
}
|
||||
|
||||
async function saveMultiplier(key: EndpointAPIKey, format: string) {
|
||||
// 防止重复调用
|
||||
// 防止重复调用(Enter 触发后阻止 blur 再次进入)
|
||||
if (multiplierSaving.value) return
|
||||
multiplierSaving.value = true
|
||||
|
||||
@@ -2093,6 +2090,7 @@ async function saveMultiplier(key: EndpointAPIKey, format: string) {
|
||||
if (!keyId || isNaN(newMultiplier)) {
|
||||
showError('请输入有效的倍率值')
|
||||
cancelEditMultiplier()
|
||||
multiplierSaving.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2100,6 +2098,7 @@ async function saveMultiplier(key: EndpointAPIKey, format: string) {
|
||||
if (newMultiplier <= 0 || newMultiplier > 100) {
|
||||
showError('倍率必须在 0.01 到 100 之间')
|
||||
cancelEditMultiplier()
|
||||
multiplierSaving.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2107,6 +2106,7 @@ async function saveMultiplier(key: EndpointAPIKey, format: string) {
|
||||
const currentMultiplier = getKeyRateMultiplier(key, format)
|
||||
if (Math.abs(currentMultiplier - newMultiplier) < 0.0001) {
|
||||
cancelEditMultiplier()
|
||||
multiplierSaving.value = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,13 @@ export function isAccountLevelBlockReason(reason: string | null | undefined): bo
|
||||
const text = reason.trim()
|
||||
if (!text) return false
|
||||
if (text.startsWith('[ACCOUNT_BLOCK]')) return true
|
||||
if (text.startsWith('[OAUTH_EXPIRED]')) return true
|
||||
const lowered = text.toLowerCase()
|
||||
return ACCOUNT_BLOCK_REASON_KEYWORDS.some(keyword => lowered.includes(keyword))
|
||||
}
|
||||
|
||||
export function classifyAccountBlockLabel(reason: string): string {
|
||||
if (reason.trim().startsWith('[OAUTH_EXPIRED]')) return 'Token 失效'
|
||||
const lowered = reason.toLowerCase()
|
||||
if (KEYWORDS_VERIFICATION.some(kw => lowered.includes(kw))) return '需要验证'
|
||||
if (KEYWORDS_DISABLED.some(kw => lowered.includes(kw))) return '账号停用'
|
||||
@@ -57,5 +59,15 @@ export function classifyAccountBlockLabel(reason: string): string {
|
||||
}
|
||||
|
||||
export function cleanAccountBlockReason(reason: string): string {
|
||||
return reason.replace(/^\[ACCOUNT_BLOCK\]\s*/i, '').trim()
|
||||
return reason.replace(/^\[(ACCOUNT_BLOCK|OAUTH_EXPIRED)\]\s*/i, '').trim()
|
||||
}
|
||||
|
||||
export function isRefreshFailedReason(reason: string | null | undefined): boolean {
|
||||
if (!reason) return false
|
||||
return reason.trim().startsWith('[REFRESH_FAILED]')
|
||||
}
|
||||
|
||||
export function isOAuthExpiredReason(reason: string | null | undefined): boolean {
|
||||
if (!reason) return false
|
||||
return reason.trim().startsWith('[OAUTH_EXPIRED]')
|
||||
}
|
||||
|
||||
@@ -1963,6 +1963,7 @@ async function handleRefreshOAuth(key: PoolKeyDetail) {
|
||||
await loadKeys()
|
||||
} catch (err) {
|
||||
showError(parseApiError(err, 'Token 刷新失败'))
|
||||
await loadKeys()
|
||||
} finally {
|
||||
refreshingOAuthKeyId.value = null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user