mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: add comprehensive 401 error handling and account status management
- Add 401 error detection and automatic account suspension after 3 consecutive failures - Implement account status reset functionality for clearing all error states - Enhance admin interface with status reset controls and improved status display - Upgrade service management script with backup protection and retry mechanisms - Add mandatory code formatting requirements using Prettier - Improve account selector with detailed status information and color coding 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2
web/admin-spa/package-lock.json
generated
2
web/admin-spa/package-lock.json
generated
@@ -3655,7 +3655,7 @@
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
|
||||
"integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -101,12 +101,14 @@
|
||||
<span
|
||||
class="ml-2 rounded-full px-2 py-0.5 text-xs"
|
||||
:class="
|
||||
account.status === 'active'
|
||||
account.isActive
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
: account.status === 'unauthorized'
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
"
|
||||
>
|
||||
{{ account.status === 'active' ? '正常' : '异常' }}
|
||||
{{ getAccountStatusText(account) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">
|
||||
@@ -134,12 +136,14 @@
|
||||
<span
|
||||
class="ml-2 rounded-full px-2 py-0.5 text-xs"
|
||||
:class="
|
||||
account.status === 'active'
|
||||
account.isActive
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
: account.status === 'unauthorized'
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
"
|
||||
>
|
||||
{{ account.status === 'active' ? '正常' : '异常' }}
|
||||
{{ getAccountStatusText(account) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">
|
||||
@@ -224,14 +228,38 @@ const selectedLabel = computed(() => {
|
||||
const account = props.accounts.find(
|
||||
(a) => a.id === accountId && a.platform === 'claude-console'
|
||||
)
|
||||
return account ? `${account.name} (${account.status === 'active' ? '正常' : '异常'})` : ''
|
||||
return account ? `${account.name} (${getAccountStatusText(account)})` : ''
|
||||
}
|
||||
|
||||
// OAuth 账号
|
||||
const account = props.accounts.find((a) => a.id === props.modelValue)
|
||||
return account ? `${account.name} (${account.status === 'active' ? '正常' : '异常'})` : ''
|
||||
return account ? `${account.name} (${getAccountStatusText(account)})` : ''
|
||||
})
|
||||
|
||||
// 获取账户状态文本
|
||||
const getAccountStatusText = (account) => {
|
||||
if (!account) return '未知'
|
||||
|
||||
// 优先使用 isActive 判断
|
||||
if (account.isActive === false) {
|
||||
// 根据 status 提供更详细的状态信息
|
||||
switch (account.status) {
|
||||
case 'unauthorized':
|
||||
return '未授权'
|
||||
case 'error':
|
||||
return 'Token错误'
|
||||
case 'created':
|
||||
return '待验证'
|
||||
case 'rate_limited':
|
||||
return '限流中'
|
||||
default:
|
||||
return '异常'
|
||||
}
|
||||
}
|
||||
|
||||
return '正常'
|
||||
}
|
||||
|
||||
// 按创建时间倒序排序账号
|
||||
const sortedAccounts = computed(() => {
|
||||
return [...props.accounts].sort((a, b) => {
|
||||
|
||||
@@ -248,9 +248,11 @@
|
||||
'inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold',
|
||||
account.status === 'blocked'
|
||||
? 'bg-orange-100 text-orange-800'
|
||||
: account.isActive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
: account.status === 'unauthorized'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: account.isActive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
@@ -258,13 +260,21 @@
|
||||
'mr-2 h-2 w-2 rounded-full',
|
||||
account.status === 'blocked'
|
||||
? 'bg-orange-500'
|
||||
: account.isActive
|
||||
? 'bg-green-500'
|
||||
: 'bg-red-500'
|
||||
: account.status === 'unauthorized'
|
||||
? 'bg-red-500'
|
||||
: account.isActive
|
||||
? 'bg-green-500'
|
||||
: 'bg-red-500'
|
||||
]"
|
||||
/>
|
||||
{{
|
||||
account.status === 'blocked' ? '已封锁' : account.isActive ? '正常' : '异常'
|
||||
account.status === 'blocked'
|
||||
? '已封锁'
|
||||
: account.status === 'unauthorized'
|
||||
? '异常'
|
||||
: account.isActive
|
||||
? '正常'
|
||||
: '异常'
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
@@ -413,6 +423,27 @@
|
||||
<i :class="['fas fa-sync-alt', account.isRefreshing ? 'animate-spin' : '']" />
|
||||
<span class="ml-1">刷新</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
account.platform === 'claude' &&
|
||||
(account.status === 'unauthorized' ||
|
||||
account.status !== 'active' ||
|
||||
account.rateLimitStatus?.isRateLimited ||
|
||||
!account.isActive)
|
||||
"
|
||||
:class="[
|
||||
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
|
||||
account.isResetting
|
||||
? 'cursor-not-allowed bg-gray-100 text-gray-400'
|
||||
: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
|
||||
]"
|
||||
:disabled="account.isResetting"
|
||||
:title="account.isResetting ? '重置中...' : '重置所有异常状态'"
|
||||
@click="resetAccountStatus(account)"
|
||||
>
|
||||
<i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" />
|
||||
<span class="ml-1">重置状态</span>
|
||||
</button>
|
||||
<button
|
||||
:class="[
|
||||
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
|
||||
@@ -1036,6 +1067,41 @@ const refreshToken = async (account) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 重置账户状态
|
||||
const resetAccountStatus = async (account) => {
|
||||
if (account.isResetting) return
|
||||
|
||||
let confirmed = false
|
||||
if (window.showConfirm) {
|
||||
confirmed = await window.showConfirm(
|
||||
'重置账户状态',
|
||||
'确定要重置此账户的所有异常状态吗?这将清除限流状态、401错误计数等所有异常标记。',
|
||||
'确定重置',
|
||||
'取消'
|
||||
)
|
||||
} else {
|
||||
confirmed = confirm('确定要重置此账户的所有异常状态吗?')
|
||||
}
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
try {
|
||||
account.isResetting = true
|
||||
const data = await apiClient.post(`/admin/claude-accounts/${account.id}/reset-status`)
|
||||
|
||||
if (data.success) {
|
||||
showToast('账户状态已重置', 'success')
|
||||
loadAccounts()
|
||||
} else {
|
||||
showToast(data.message || '状态重置失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('状态重置失败', 'error')
|
||||
} finally {
|
||||
account.isResetting = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换调度状态
|
||||
const toggleSchedulable = async (account) => {
|
||||
if (account.isTogglingSchedulable) return
|
||||
@@ -1090,6 +1156,8 @@ const handleEditSuccess = () => {
|
||||
const getAccountStatusText = (account) => {
|
||||
// 检查是否被封锁
|
||||
if (account.status === 'blocked') return '已封锁'
|
||||
// 检查是否未授权(401错误)
|
||||
if (account.status === 'unauthorized') return '异常'
|
||||
// 检查是否限流
|
||||
if (
|
||||
account.isRateLimited ||
|
||||
@@ -1110,6 +1178,9 @@ const getAccountStatusClass = (account) => {
|
||||
if (account.status === 'blocked') {
|
||||
return 'bg-red-100 text-red-800'
|
||||
}
|
||||
if (account.status === 'unauthorized') {
|
||||
return 'bg-red-100 text-red-800'
|
||||
}
|
||||
if (
|
||||
account.isRateLimited ||
|
||||
account.status === 'rate_limited' ||
|
||||
@@ -1131,6 +1202,9 @@ const getAccountStatusDotClass = (account) => {
|
||||
if (account.status === 'blocked') {
|
||||
return 'bg-red-500'
|
||||
}
|
||||
if (account.status === 'unauthorized') {
|
||||
return 'bg-red-500'
|
||||
}
|
||||
if (
|
||||
account.isRateLimited ||
|
||||
account.status === 'rate_limited' ||
|
||||
|
||||
Reference in New Issue
Block a user