feat: Add account ban handling and UI updates (#11)

- Add ban status and reason fields to account configuration
- Add account ban status and details handling in API refresh account function.
- Add logic to handle account suspension and authentication errors, updating ban status accordingly.
- Add and style badge classes for different account statuses and modify account status display logic.
This commit is contained in:
hkxiaoyao
2026-02-10 12:23:39 +08:00
committed by GitHub
parent 306f49f9ac
commit 1afc82c29c
4 changed files with 142 additions and 7 deletions

View File

@@ -51,7 +51,10 @@ type Account struct {
MachineId string `json:"machineId,omitempty"` // UUID machine identifier for request tracking
// Account status
Enabled bool `json:"enabled"` // Whether account is active in the pool
Enabled bool `json:"enabled"` // Whether account is active in the pool
BanStatus string `json:"banStatus,omitempty"` // Ban status: "ACTIVE", "BANNED", "SUSPENDED"
BanReason string `json:"banReason,omitempty"` // Reason for ban/suspension
BanTime int64 `json:"banTime,omitempty"` // Timestamp when ban was detected
// Subscription information
SubscriptionType string `json:"subscriptionType,omitempty"` // Tier: FREE, PRO, PRO_PLUS, or POWER

View File

@@ -1401,6 +1401,9 @@ func (h *Handler) apiGetAccounts(w http.ResponseWriter, r *http.Request) {
"provider": a.Provider,
"region": a.Region,
"enabled": a.Enabled,
"banStatus": a.BanStatus,
"banReason": a.BanReason,
"banTime": a.BanTime,
"expiresAt": a.ExpiresAt,
"hasToken": a.AccessToken != "",
"machineId": a.MachineId,
@@ -1993,14 +1996,36 @@ func (h *Handler) apiRefreshAccount(w http.ResponseWriter, r *http.Request, id s
// 获取账户信息
info, err := RefreshAccountInfo(account)
if err != nil {
// 如果是 403/401说明 token 无效,尝试刷新后重试
// 检查是否为封禁相关错误
errMsg := err.Error()
if strings.Contains(errMsg, "TEMPORARILY_SUSPENDED") || strings.Contains(errMsg, "Account suspended") {
// 封禁状态已在 RefreshAccountInfo 中处理,静默返回成功
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Account status updated",
})
return
}
// 如果是 403/401说明 token 无效,尝试刷新后重试
if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "401") || strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "expired") {
if refreshErr := refreshTokenIfNeeded(); refreshErr == nil {
// 重试
info, err = RefreshAccountInfo(account)
if err != nil {
// 重试后仍然失败,检查是否为封禁状态
if strings.Contains(err.Error(), "TEMPORARILY_SUSPENDED") || strings.Contains(err.Error(), "Account suspended") {
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Account status updated",
})
return
}
}
}
}
// 其他错误才显示错误信息
if err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})

View File

@@ -136,9 +136,61 @@ func RefreshAccountInfo(account *config.Account) (*config.AccountInfo, error) {
// 获取使用量和订阅信息
usage, err := GetUsageLimits(account)
if err != nil {
// 检测封禁状态
errMsg := err.Error()
if strings.Contains(errMsg, "TEMPORARILY_SUSPENDED") {
// 账户被暂时封禁,自动禁用并标记封禁状态
fmt.Printf("[RefreshAccountInfo] Account %s is temporarily suspended: %v\n", account.Email, err)
// 更新账户封禁状态并自动禁用
updatedAccount := *account
updatedAccount.Enabled = false
updatedAccount.BanStatus = "BANNED"
updatedAccount.BanReason = "AWS temporarily suspended - unusual user activity detected"
updatedAccount.BanTime = time.Now().Unix()
// 保存更新后的账户状态
if updateErr := config.UpdateAccount(account.ID, updatedAccount); updateErr != nil {
fmt.Printf("[RefreshAccountInfo] Failed to update account ban status: %v\n", updateErr)
}
return nil, fmt.Errorf("Account suspended: %w", err)
} else if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "401") ||
strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "expired") {
// Token 相关错误,可能需要重新认证
fmt.Printf("[RefreshAccountInfo] Authentication error for %s: %v\n", account.Email, err)
// 更新账户封禁状态为认证失败并自动禁用
updatedAccount := *account
updatedAccount.Enabled = false
updatedAccount.BanStatus = "BANNED"
updatedAccount.BanReason = "Authentication failed - token invalid or expired"
updatedAccount.BanTime = time.Now().Unix()
// 保存更新后的账户状态
if updateErr := config.UpdateAccount(account.ID, updatedAccount); updateErr != nil {
fmt.Printf("[RefreshAccountInfo] Failed to update account ban status: %v\n", updateErr)
}
}
return nil, fmt.Errorf("GetUsageLimits: %w", err)
}
// 如果成功获取信息,清除封禁状态(如果之前被标记)
if account.BanStatus != "" && account.BanStatus != "ACTIVE" {
fmt.Printf("[RefreshAccountInfo] Account %s is now active, clearing ban status\n", account.Email)
updatedAccount := *account
updatedAccount.BanStatus = "ACTIVE"
updatedAccount.BanReason = ""
updatedAccount.BanTime = 0
// 保存更新后的账户状态
if updateErr := config.UpdateAccount(account.ID, updatedAccount); updateErr != nil {
fmt.Printf("[RefreshAccountInfo] Failed to clear account ban status: %v\n", updateErr)
}
}
// 解析用户信息
if usage.UserInfo != nil {
info.Email = usage.UserInfo.Email

View File

@@ -241,6 +241,21 @@
color: white;
}
.badge-trial {
background: #10b981;
color: white;
}
.badge-banned {
background: #dc2626;
color: white;
}
.badge-suspended {
background: #f59e0b;
color: white;
}
.modal {
display: none;
position: fixed;
@@ -1062,6 +1077,9 @@
'accounts.expired': '已过期',
'accounts.disabled': '已禁用',
'accounts.normal': '正常',
'accounts.enabled': '已启用',
'accounts.banned': '已封禁',
'accounts.suspended': '已暂停',
'accounts.refreshFailed': '刷新失败',
'accounts.confirmDelete': '确定删除?',
'accounts.mainQuota': '主配额',
@@ -1251,6 +1269,9 @@
'accounts.expired': 'Expired',
'accounts.disabled': 'Disabled',
'accounts.normal': 'Active',
'accounts.enabled': 'Enabled',
'accounts.banned': 'Banned',
'accounts.suspended': 'Suspended',
'accounts.refreshFailed': 'Refresh failed',
'accounts.confirmDelete': 'Confirm delete?',
'time.expired': 'Expired',
@@ -1596,7 +1617,9 @@
'<div class="account-actions">' +
'<button class="btn btn-sm btn-icon btn-secondary" onclick="refreshAccount(\'' + a.id + '\')" title="' + t('accounts.refresh') + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg></button>' +
'<button class="btn btn-sm btn-icon btn-secondary" onclick="showDetail(\'' + a.id + '\')" title="' + t('accounts.detail') + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></button>' +
'<button class="btn btn-sm ' + (a.enabled ? 'btn-secondary' : 'btn-primary') + '" onclick="toggleAccount(\'' + a.id + '\',' + !a.enabled + ')">' + (a.enabled ? t('accounts.disable') : t('accounts.enable')) + '</button>' +
// 封禁账户不显示启用/禁用按钮
(a.banStatus && a.banStatus !== 'ACTIVE' ? '' :
'<button class="btn btn-sm ' + (a.enabled ? 'btn-secondary' : 'btn-primary') + '" onclick="toggleAccount(\'' + a.id + '\',' + !a.enabled + ')">' + (a.enabled ? t('accounts.disable') : t('accounts.enable')) + '</button>') +
'<button class="btn btn-sm btn-danger" onclick="deleteAccount(\'' + a.id + '\')">' + t('accounts.delete') + '</button>' +
'</div>' +
'</div>' +
@@ -1644,10 +1667,42 @@
return method;
}
function getStatusBadge(a) {
if (!a.hasToken) return '<span class="badge badge-error">' + t('accounts.noToken') + '</span>';
if (a.expiresAt && a.expiresAt < Date.now() / 1000) return '<span class="badge badge-warning">' + t('accounts.expired') + '</span>';
if (!a.enabled) return '<span class="badge badge-warning">' + t('accounts.disabled') + '</span>';
return '<span class="badge badge-success">' + t('accounts.normal') + '</span>';
let badges = [];
// 检查是否为封禁状态
const isBanned = a.banStatus && a.banStatus !== 'ACTIVE';
if (isBanned) {
// 封禁账号:显示"封禁 + 禁用"
if (a.banStatus === 'BANNED') {
badges.push('<span class="badge badge-banned">' + t('accounts.banned') + '</span>');
} else if (a.banStatus === 'SUSPENDED') {
badges.push('<span class="badge badge-suspended">' + t('accounts.suspended') + '</span>');
}
// 封禁账号必定显示禁用状态
badges.push('<span class="badge badge-warning">' + t('accounts.disabled') + '</span>');
} else {
// 正常账号:显示"正常 + 启用/禁用"
// 检查Token状态
if (!a.hasToken) {
badges.push('<span class="badge badge-error">' + t('accounts.noToken') + '</span>');
} else if (a.expiresAt && a.expiresAt < Date.now() / 1000) {
badges.push('<span class="badge badge-warning">' + t('accounts.expired') + '</span>');
} else {
// 有效Token的正常账号显示"正常"
badges.push('<span class="badge badge-success">' + t('accounts.normal') + '</span>');
}
// 显示启用/禁用状态
if (a.enabled) {
badges.push('<span class="badge badge-info">' + t('accounts.enabled') + '</span>');
} else {
badges.push('<span class="badge badge-warning">' + t('accounts.disabled') + '</span>');
}
}
return badges.join('');
}
function formatTokenExpiry(ts) {
if (!ts) return '-';