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:
@@ -144,6 +144,13 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
|
||||
## 开发最佳实践
|
||||
|
||||
### 代码格式化要求
|
||||
- **必须使用 Prettier 格式化所有代码**
|
||||
- 后端代码(src/):运行 `npx prettier --write <file>` 格式化
|
||||
- 前端代码(web/admin-spa/):已安装 `prettier-plugin-tailwindcss`,运行 `npx prettier --write <file>` 格式化
|
||||
- 提交前检查格式:`npx prettier --check <file>`
|
||||
- 格式化所有文件:`npm run format`(如果配置了此脚本)
|
||||
|
||||
### 代码修改原则
|
||||
- 对现有文件进行修改时,首先检查代码库的现有模式和风格
|
||||
- 尽可能重用现有的服务和工具函数,避免重复代码
|
||||
|
||||
@@ -577,13 +577,67 @@ update_service() {
|
||||
cp config/config.js config/config.js.backup.$(date +%Y%m%d%H%M%S)
|
||||
fi
|
||||
|
||||
# 拉取最新代码
|
||||
print_info "拉取最新代码..."
|
||||
if ! git pull origin main; then
|
||||
print_error "拉取代码失败,请检查网络连接"
|
||||
# 检查本地修改
|
||||
print_info "检查本地文件修改..."
|
||||
local has_changes=false
|
||||
if git status --porcelain | grep -v "^??" | grep -q .; then
|
||||
has_changes=true
|
||||
print_warning "检测到本地文件已修改:"
|
||||
git status --short | grep -v "^??"
|
||||
echo ""
|
||||
echo -e "${YELLOW}警告:更新将使用远程版本覆盖本地修改!${NC}"
|
||||
|
||||
# 创建本地修改的备份
|
||||
local backup_branch="backup-$(date +%Y%m%d-%H%M%S)"
|
||||
print_info "创建本地修改备份分支: $backup_branch"
|
||||
git stash push -m "Backup before update $(date +%Y-%m-%d)" >/dev/null 2>&1
|
||||
git branch "$backup_branch" 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}已创建备份分支: $backup_branch${NC}"
|
||||
echo "如需恢复,可执行: git checkout $backup_branch"
|
||||
echo ""
|
||||
|
||||
echo -n "是否继续更新?(y/N): "
|
||||
read -n 1 confirm_update
|
||||
echo
|
||||
|
||||
if [[ ! "$confirm_update" =~ ^[Yy]$ ]]; then
|
||||
print_info "已取消更新"
|
||||
# 恢复 stash 的修改
|
||||
git stash pop >/dev/null 2>&1 || true
|
||||
# 如果之前在运行,重新启动服务
|
||||
if [ "$was_running" = true ]; then
|
||||
print_info "重新启动服务..."
|
||||
start_service
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 获取最新代码(强制使用远程版本)
|
||||
print_info "获取最新代码..."
|
||||
|
||||
# 先获取远程更新
|
||||
if ! git fetch origin main; then
|
||||
print_error "获取远程代码失败,请检查网络连接"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 强制重置到远程版本
|
||||
print_info "应用远程更新..."
|
||||
if ! git reset --hard origin/main; then
|
||||
print_error "重置到远程版本失败"
|
||||
# 尝试恢复
|
||||
print_info "尝试恢复..."
|
||||
git reset --hard HEAD
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 清理未跟踪的文件(可选,保留用户新建的文件)
|
||||
# git clean -fd # 注释掉,避免删除用户的新文件
|
||||
|
||||
print_success "代码已更新到最新版本"
|
||||
|
||||
# 更新依赖
|
||||
print_info "更新依赖..."
|
||||
npm install
|
||||
@@ -599,8 +653,14 @@ update_service() {
|
||||
# 创建目标目录
|
||||
mkdir -p web/admin-spa/dist
|
||||
|
||||
# 清理旧的前端文件
|
||||
rm -rf web/admin-spa/dist/*
|
||||
# 清理旧的前端文件(保留用户自定义文件)
|
||||
if [ -d "web/admin-spa/dist" ]; then
|
||||
print_info "清理旧的前端文件..."
|
||||
# 只删除已知的前端文件,保留用户可能添加的自定义文件
|
||||
rm -rf web/admin-spa/dist/assets 2>/dev/null
|
||||
rm -f web/admin-spa/dist/index.html 2>/dev/null
|
||||
rm -f web/admin-spa/dist/favicon.ico 2>/dev/null
|
||||
fi
|
||||
|
||||
# 从 web-dist 分支获取构建好的文件
|
||||
if git ls-remote --heads origin web-dist | grep -q web-dist; then
|
||||
@@ -609,14 +669,42 @@ update_service() {
|
||||
# 创建临时目录用于 clone
|
||||
TEMP_CLONE_DIR=$(mktemp -d)
|
||||
|
||||
# 使用 sparse-checkout 来只获取需要的文件
|
||||
git clone --depth 1 --branch web-dist --single-branch \
|
||||
https://github.com/Wei-Shaw/claude-relay-service.git \
|
||||
"$TEMP_CLONE_DIR" 2>/dev/null || {
|
||||
# 添加错误处理
|
||||
if [ ! -d "$TEMP_CLONE_DIR" ]; then
|
||||
print_error "无法创建临时目录"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 使用 sparse-checkout 来只获取需要的文件,添加重试机制
|
||||
local clone_success=false
|
||||
for attempt in 1 2 3; do
|
||||
print_info "尝试下载前端文件 (第 $attempt 次)..."
|
||||
|
||||
if git clone --depth 1 --branch web-dist --single-branch \
|
||||
https://github.com/Wei-Shaw/claude-relay-service.git \
|
||||
"$TEMP_CLONE_DIR" 2>/dev/null; then
|
||||
clone_success=true
|
||||
break
|
||||
fi
|
||||
|
||||
# 如果 HTTPS 失败,尝试使用当前仓库的 remote URL
|
||||
REPO_URL=$(git config --get remote.origin.url)
|
||||
git clone --depth 1 --branch web-dist --single-branch "$REPO_URL" "$TEMP_CLONE_DIR"
|
||||
}
|
||||
if git clone --depth 1 --branch web-dist --single-branch "$REPO_URL" "$TEMP_CLONE_DIR" 2>/dev/null; then
|
||||
clone_success=true
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $attempt -lt 3 ]; then
|
||||
print_warning "下载失败,等待 2 秒后重试..."
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$clone_success" = false ]; then
|
||||
print_error "无法下载前端文件"
|
||||
rm -rf "$TEMP_CLONE_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 复制文件到目标目录(排除 .git 和 README.md)
|
||||
rsync -av --exclude='.git' --exclude='README.md' "$TEMP_CLONE_DIR/" web/admin-spa/dist/ 2>/dev/null || {
|
||||
@@ -663,10 +751,32 @@ update_service() {
|
||||
|
||||
print_success "更新完成!"
|
||||
|
||||
# 显示更新摘要
|
||||
echo ""
|
||||
echo -e "${BLUE}=== 更新摘要 ===${NC}"
|
||||
|
||||
# 显示版本信息
|
||||
if [ -f "$APP_DIR/VERSION" ]; then
|
||||
echo -e "\n当前版本: ${GREEN}$(cat "$APP_DIR/VERSION")${NC}"
|
||||
echo -e "当前版本: ${GREEN}$(cat "$APP_DIR/VERSION")${NC}"
|
||||
fi
|
||||
|
||||
# 显示最新的提交信息
|
||||
local latest_commit=$(git log -1 --oneline 2>/dev/null)
|
||||
if [ -n "$latest_commit" ]; then
|
||||
echo -e "最新提交: ${GREEN}$latest_commit${NC}"
|
||||
fi
|
||||
|
||||
# 显示备份信息
|
||||
echo -e "\n${YELLOW}配置文件备份:${NC}"
|
||||
ls -la .env.backup.* 2>/dev/null | tail -3 || echo " 无备份文件"
|
||||
|
||||
# 提醒用户检查配置
|
||||
echo -e "\n${YELLOW}提示:${NC}"
|
||||
echo " - 配置文件已自动备份"
|
||||
echo " - 如有本地修改已保存到备份分支"
|
||||
echo " - 建议检查 .env 和 config/config.js 配置"
|
||||
|
||||
echo -e "\n${BLUE}==================${NC}"
|
||||
}
|
||||
|
||||
# 卸载服务
|
||||
|
||||
@@ -1341,6 +1341,21 @@ router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req
|
||||
}
|
||||
})
|
||||
|
||||
// 重置Claude账户状态(清除所有异常状态)
|
||||
router.post('/claude-accounts/:accountId/reset-status', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
|
||||
const result = await claudeAccountService.resetAccountStatus(accountId)
|
||||
|
||||
logger.success(`✅ Admin reset status for Claude account: ${accountId}`)
|
||||
return res.json({ success: true, data: result })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to reset Claude account status:', error)
|
||||
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 切换Claude账户调度状态
|
||||
router.put(
|
||||
'/claude-accounts/:accountId/toggle-schedulable',
|
||||
|
||||
@@ -1194,6 +1194,99 @@ class ClaudeAccountService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🚫 标记账户为未授权状态(401错误)
|
||||
async markAccountUnauthorized(accountId, sessionHash = null) {
|
||||
try {
|
||||
const accountData = await redis.getClaudeAccount(accountId)
|
||||
if (!accountData || Object.keys(accountData).length === 0) {
|
||||
throw new Error('Account not found')
|
||||
}
|
||||
|
||||
// 更新账户状态
|
||||
const updatedAccountData = { ...accountData }
|
||||
updatedAccountData.status = 'unauthorized'
|
||||
updatedAccountData.schedulable = 'false' // 设置为不可调度
|
||||
updatedAccountData.errorMessage = 'Account unauthorized (401 errors detected)'
|
||||
updatedAccountData.unauthorizedAt = new Date().toISOString()
|
||||
|
||||
// 保存更新后的账户数据
|
||||
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||
|
||||
// 如果有sessionHash,删除粘性会话映射
|
||||
if (sessionHash) {
|
||||
await redis.client.del(`sticky_session:${sessionHash}`)
|
||||
logger.info(`🗑️ Deleted sticky session mapping for hash: ${sessionHash}`)
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`⚠️ Account ${accountData.name} (${accountId}) marked as unauthorized and disabled for scheduling`
|
||||
)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 🔄 重置账户所有异常状态
|
||||
async resetAccountStatus(accountId) {
|
||||
try {
|
||||
const accountData = await redis.getClaudeAccount(accountId)
|
||||
if (!accountData || Object.keys(accountData).length === 0) {
|
||||
throw new Error('Account not found')
|
||||
}
|
||||
|
||||
// 重置账户状态
|
||||
const updatedAccountData = { ...accountData }
|
||||
|
||||
// 根据是否有有效的accessToken来设置status
|
||||
if (updatedAccountData.accessToken) {
|
||||
updatedAccountData.status = 'active'
|
||||
} else {
|
||||
updatedAccountData.status = 'created'
|
||||
}
|
||||
|
||||
// 恢复可调度状态
|
||||
updatedAccountData.schedulable = 'true'
|
||||
|
||||
// 清除错误相关字段
|
||||
delete updatedAccountData.errorMessage
|
||||
delete updatedAccountData.unauthorizedAt
|
||||
delete updatedAccountData.rateLimitedAt
|
||||
delete updatedAccountData.rateLimitStatus
|
||||
delete updatedAccountData.rateLimitEndAt
|
||||
|
||||
// 保存更新后的账户数据
|
||||
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||
|
||||
// 清除401错误计数
|
||||
const errorKey = `claude_account:${accountId}:401_errors`
|
||||
await redis.client.del(errorKey)
|
||||
|
||||
// 清除限流状态(如果存在)
|
||||
const rateLimitKey = `ratelimit:${accountId}`
|
||||
await redis.client.del(rateLimitKey)
|
||||
|
||||
logger.info(
|
||||
`✅ Successfully reset all error states for account ${accountData.name} (${accountId})`
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
account: {
|
||||
id: accountId,
|
||||
name: accountData.name,
|
||||
status: updatedAccountData.status,
|
||||
schedulable: updatedAccountData.schedulable === 'true'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to reset account status for ${accountId}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ClaudeAccountService()
|
||||
|
||||
@@ -169,13 +169,37 @@ class ClaudeRelayService {
|
||||
clientResponse.removeListener('close', handleClientDisconnect)
|
||||
}
|
||||
|
||||
// 检查响应是否为限流错误
|
||||
// 检查响应是否为限流错误或认证错误
|
||||
if (response.statusCode !== 200 && response.statusCode !== 201) {
|
||||
let isRateLimited = false
|
||||
let rateLimitResetTimestamp = null
|
||||
|
||||
// 检查是否为401状态码(未授权)
|
||||
if (response.statusCode === 401) {
|
||||
logger.warn(`🔐 Unauthorized error (401) detected for account ${accountId}`)
|
||||
|
||||
// 记录401错误
|
||||
await this.recordUnauthorizedError(accountId)
|
||||
|
||||
// 检查是否需要标记为异常(连续3次401)
|
||||
const errorCount = await this.getUnauthorizedErrorCount(accountId)
|
||||
logger.info(
|
||||
`🔐 Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes`
|
||||
)
|
||||
|
||||
if (errorCount >= 3) {
|
||||
logger.error(
|
||||
`❌ Account ${accountId} exceeded 401 error threshold (${errorCount} errors), marking as unauthorized`
|
||||
)
|
||||
await unifiedClaudeScheduler.markAccountUnauthorized(
|
||||
accountId,
|
||||
accountType,
|
||||
sessionHash
|
||||
)
|
||||
}
|
||||
}
|
||||
// 检查是否为429状态码
|
||||
if (response.statusCode === 429) {
|
||||
else if (response.statusCode === 429) {
|
||||
isRateLimited = true
|
||||
|
||||
// 提取限流重置时间戳
|
||||
@@ -224,6 +248,8 @@ class ClaudeRelayService {
|
||||
)
|
||||
}
|
||||
} else if (response.statusCode === 200 || response.statusCode === 201) {
|
||||
// 请求成功,清除401错误计数
|
||||
await this.clearUnauthorizedErrors(accountId)
|
||||
// 如果请求成功,检查并移除限流状态
|
||||
const isRateLimited = await unifiedClaudeScheduler.isAccountRateLimited(
|
||||
accountId,
|
||||
@@ -1295,6 +1321,49 @@ class ClaudeRelayService {
|
||||
throw lastError
|
||||
}
|
||||
|
||||
// 🔐 记录401未授权错误
|
||||
async recordUnauthorizedError(accountId) {
|
||||
try {
|
||||
const key = `claude_account:${accountId}:401_errors`
|
||||
const redis = require('../models/redis')
|
||||
|
||||
// 增加错误计数,设置5分钟过期时间
|
||||
await redis.client.incr(key)
|
||||
await redis.client.expire(key, 300) // 5分钟
|
||||
|
||||
logger.info(`📝 Recorded 401 error for account ${accountId}`)
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to record 401 error for account ${accountId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 🔍 获取401错误计数
|
||||
async getUnauthorizedErrorCount(accountId) {
|
||||
try {
|
||||
const key = `claude_account:${accountId}:401_errors`
|
||||
const redis = require('../models/redis')
|
||||
|
||||
const count = await redis.client.get(key)
|
||||
return parseInt(count) || 0
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to get 401 error count for account ${accountId}:`, error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 🧹 清除401错误计数
|
||||
async clearUnauthorizedErrors(accountId) {
|
||||
try {
|
||||
const key = `claude_account:${accountId}:401_errors`
|
||||
const redis = require('../models/redis')
|
||||
|
||||
await redis.client.del(key)
|
||||
logger.info(`✅ Cleared 401 error count for account ${accountId}`)
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to clear 401 errors for account ${accountId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 健康检查
|
||||
async healthCheck() {
|
||||
try {
|
||||
|
||||
@@ -551,6 +551,35 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// 🚫 标记账户为未授权状态(401错误)
|
||||
async markAccountUnauthorized(accountId, accountType, sessionHash = null) {
|
||||
try {
|
||||
// 只处理claude-official类型的账户,不处理claude-console和gemini
|
||||
if (accountType === 'claude-official') {
|
||||
await claudeAccountService.markAccountUnauthorized(accountId, sessionHash)
|
||||
|
||||
// 删除会话映射
|
||||
if (sessionHash) {
|
||||
await this._deleteSessionMapping(sessionHash)
|
||||
}
|
||||
|
||||
logger.warn(`🚫 Account ${accountId} marked as unauthorized due to consecutive 401 errors`)
|
||||
} else {
|
||||
logger.info(
|
||||
`ℹ️ Skipping unauthorized marking for non-Claude OAuth account: ${accountId} (${accountType})`
|
||||
)
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`❌ Failed to mark account as unauthorized: ${accountId} (${accountType})`,
|
||||
error
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 🚫 标记Claude Console账户为封锁状态(模型不支持)
|
||||
async blockConsoleAccount(accountId, reason) {
|
||||
try {
|
||||
|
||||
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