mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-02-21 08:04:40 +08:00
feat: 增强Token禁用状态管理,区分失效与禁用
This commit is contained in:
@@ -60,3 +60,15 @@ call_mode = "default"
|
|||||||
# 常用时区:中国/新加坡 +8, 日本/韩国 +9, 印度 +5.5, 伦敦 0, 纽约 -5, 洛杉矶 -8
|
# 常用时区:中国/新加坡 +8, 日本/韩国 +9, 印度 +5.5, 伦敦 0, 纽约 -5, 洛杉矶 -8
|
||||||
timezone_offset = 8
|
timezone_offset = 8
|
||||||
|
|
||||||
|
[pow_service]
|
||||||
|
# beta测试,目前仍处于测试阶段
|
||||||
|
# POW 计算模式:local(本地计算)或 external(外部服务)
|
||||||
|
mode = "external"
|
||||||
|
# 外部 POW 服务地址(仅在 external 模式下使用)
|
||||||
|
server_url = "http://localhost:8002"
|
||||||
|
# 外部 POW 服务访问密钥(仅在 external 模式下使用)
|
||||||
|
api_key = "your-secure-api-key-here"
|
||||||
|
# POW 代理配置
|
||||||
|
proxy_enabled = false
|
||||||
|
proxy_url = ""
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,10 @@ async def get_tokens(token: str = Depends(verify_admin_token)) -> List[dict]:
|
|||||||
"video_enabled": token.video_enabled,
|
"video_enabled": token.video_enabled,
|
||||||
# 并发限制
|
# 并发限制
|
||||||
"image_concurrency": token.image_concurrency,
|
"image_concurrency": token.image_concurrency,
|
||||||
"video_concurrency": token.video_concurrency
|
"video_concurrency": token.video_concurrency,
|
||||||
|
# 过期和禁用信息
|
||||||
|
"is_expired": token.is_expired,
|
||||||
|
"disabled_reason": token.disabled_reason
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -393,7 +393,8 @@ class Database:
|
|||||||
video_enabled BOOLEAN DEFAULT 1,
|
video_enabled BOOLEAN DEFAULT 1,
|
||||||
image_concurrency INTEGER DEFAULT -1,
|
image_concurrency INTEGER DEFAULT -1,
|
||||||
video_concurrency INTEGER DEFAULT -1,
|
video_concurrency INTEGER DEFAULT -1,
|
||||||
is_expired BOOLEAN DEFAULT 0
|
is_expired BOOLEAN DEFAULT 0,
|
||||||
|
disabled_reason TEXT
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -586,6 +587,22 @@ class Database:
|
|||||||
if not await self._column_exists(db, "admin_config", "auto_disable_on_401"):
|
if not await self._column_exists(db, "admin_config", "auto_disable_on_401"):
|
||||||
await db.execute("ALTER TABLE admin_config ADD COLUMN auto_disable_on_401 BOOLEAN DEFAULT 1")
|
await db.execute("ALTER TABLE admin_config ADD COLUMN auto_disable_on_401 BOOLEAN DEFAULT 1")
|
||||||
|
|
||||||
|
# Migration: Add disabled_reason column to tokens table if it doesn't exist
|
||||||
|
if not await self._column_exists(db, "tokens", "disabled_reason"):
|
||||||
|
await db.execute("ALTER TABLE tokens ADD COLUMN disabled_reason TEXT")
|
||||||
|
# For existing disabled tokens without a reason, set to 'manual'
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE tokens
|
||||||
|
SET disabled_reason = 'manual'
|
||||||
|
WHERE is_active = 0 AND disabled_reason IS NULL
|
||||||
|
""")
|
||||||
|
# For existing expired tokens, set to 'expired'
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE tokens
|
||||||
|
SET disabled_reason = 'expired'
|
||||||
|
WHERE is_expired = 1 AND disabled_reason IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True):
|
async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True):
|
||||||
@@ -698,27 +715,35 @@ class Database:
|
|||||||
""", (token_id,))
|
""", (token_id,))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def update_token_status(self, token_id: int, is_active: bool):
|
async def update_token_status(self, token_id: int, is_active: bool, disabled_reason: Optional[str] = None):
|
||||||
"""Update token status"""
|
"""Update token status and disabled reason"""
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
await db.execute("""
|
||||||
UPDATE tokens SET is_active = ? WHERE id = ?
|
UPDATE tokens SET is_active = ?, disabled_reason = ? WHERE id = ?
|
||||||
""", (is_active, token_id))
|
""", (is_active, disabled_reason, token_id))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def mark_token_expired(self, token_id: int):
|
async def mark_token_expired(self, token_id: int):
|
||||||
"""Mark token as expired and disable it"""
|
"""Mark token as expired and disable it with reason"""
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
await db.execute("""
|
||||||
UPDATE tokens SET is_expired = 1, is_active = 0 WHERE id = ?
|
UPDATE tokens SET is_expired = 1, is_active = 0, disabled_reason = ? WHERE id = ?
|
||||||
""", (token_id,))
|
""", ("expired", token_id))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
async def mark_token_invalid(self, token_id: int):
|
||||||
|
"""Mark token as invalid (401 error) and disable it"""
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE tokens SET is_expired = 1, is_active = 0, disabled_reason = ? WHERE id = ?
|
||||||
|
""", ("token_invalid", token_id))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def clear_token_expired(self, token_id: int):
|
async def clear_token_expired(self, token_id: int):
|
||||||
"""Clear token expired flag"""
|
"""Clear token expired flag and disabled reason"""
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
await db.execute("""
|
||||||
UPDATE tokens SET is_expired = 0 WHERE id = ?
|
UPDATE tokens SET is_expired = 0, disabled_reason = NULL WHERE id = ?
|
||||||
""", (token_id,))
|
""", (token_id,))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class Token(BaseModel):
|
|||||||
video_concurrency: int = -1 # 视频并发数限制,-1表示不限制
|
video_concurrency: int = -1 # 视频并发数限制,-1表示不限制
|
||||||
# 过期标记
|
# 过期标记
|
||||||
is_expired: bool = False # Token是否已过期(401 token_invalidated)
|
is_expired: bool = False # Token是否已过期(401 token_invalidated)
|
||||||
|
# 禁用原因: manual=手动禁用, error_limit=错误次数超限, token_invalid=Token失效, expired=过期失效
|
||||||
|
disabled_reason: Optional[str] = None
|
||||||
|
|
||||||
class TokenStats(BaseModel):
|
class TokenStats(BaseModel):
|
||||||
"""Token statistics"""
|
"""Token statistics"""
|
||||||
|
|||||||
@@ -946,19 +946,21 @@ class TokenManager:
|
|||||||
|
|
||||||
async def update_token_status(self, token_id: int, is_active: bool):
|
async def update_token_status(self, token_id: int, is_active: bool):
|
||||||
"""Update token active status"""
|
"""Update token active status"""
|
||||||
await self.db.update_token_status(token_id, is_active)
|
# When manually changing status, set appropriate disabled_reason
|
||||||
|
disabled_reason = None if is_active else "manual"
|
||||||
|
await self.db.update_token_status(token_id, is_active, disabled_reason)
|
||||||
|
|
||||||
async def enable_token(self, token_id: int):
|
async def enable_token(self, token_id: int):
|
||||||
"""Enable a token and reset error count"""
|
"""Enable a token and reset error count"""
|
||||||
await self.db.update_token_status(token_id, True)
|
await self.db.update_token_status(token_id, True, None) # Clear disabled_reason
|
||||||
# Reset error count when enabling (in token_stats table)
|
# Reset error count when enabling (in token_stats table)
|
||||||
await self.db.reset_error_count(token_id)
|
await self.db.reset_error_count(token_id)
|
||||||
# Clear expired flag when enabling
|
# Clear expired flag when enabling
|
||||||
await self.db.clear_token_expired(token_id)
|
await self.db.clear_token_expired(token_id)
|
||||||
|
|
||||||
async def disable_token(self, token_id: int):
|
async def disable_token(self, token_id: int):
|
||||||
"""Disable a token"""
|
"""Disable a token (manual disable)"""
|
||||||
await self.db.update_token_status(token_id, False)
|
await self.db.update_token_status(token_id, False, "manual")
|
||||||
|
|
||||||
async def test_token(self, token_id: int) -> dict:
|
async def test_token(self, token_id: int) -> dict:
|
||||||
"""Test if a token is valid by calling Sora API and refresh account info (subscription + Sora2)"""
|
"""Test if a token is valid by calling Sora API and refresh account info (subscription + Sora2)"""
|
||||||
@@ -1048,6 +1050,14 @@ class TokenManager:
|
|||||||
"valid": False,
|
"valid": False,
|
||||||
"message": "Token已过期(token_invalidated)"
|
"message": "Token已过期(token_invalidated)"
|
||||||
}
|
}
|
||||||
|
# Check if error is "Failed to get user info:401"
|
||||||
|
if "Failed to get user info:401" in error_msg or "Failed to get user info: 401" in error_msg:
|
||||||
|
# Mark token as invalid and disable it
|
||||||
|
await self.db.mark_token_invalid(token_id)
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"message": "Token无效: Token is invalid: Failed to get user info:401"
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"valid": False,
|
"valid": False,
|
||||||
"message": f"Token is invalid: {error_msg}"
|
"message": f"Token is invalid: {error_msg}"
|
||||||
@@ -1077,7 +1087,8 @@ class TokenManager:
|
|||||||
admin_config = await self.db.get_admin_config()
|
admin_config = await self.db.get_admin_config()
|
||||||
|
|
||||||
if stats and stats.consecutive_error_count >= admin_config.error_ban_threshold:
|
if stats and stats.consecutive_error_count >= admin_config.error_ban_threshold:
|
||||||
await self.db.update_token_status(token_id, False)
|
# Disable token with error_limit reason
|
||||||
|
await self.db.update_token_status(token_id, False, "error_limit")
|
||||||
|
|
||||||
async def record_success(self, token_id: int, is_video: bool = False):
|
async def record_success(self, token_id: int, is_video: bool = False):
|
||||||
"""Record successful request (reset error count)"""
|
"""Record successful request (reset error count)"""
|
||||||
|
|||||||
@@ -1058,19 +1058,21 @@
|
|||||||
const $=(id)=>document.getElementById(id),
|
const $=(id)=>document.getElementById(id),
|
||||||
checkAuth=()=>{const t=localStorage.getItem('adminToken');return t||(location.href='/login',null),t},
|
checkAuth=()=>{const t=localStorage.getItem('adminToken');return t||(location.href='/login',null),t},
|
||||||
apiRequest=async(url,opts={})=>{const t=checkAuth();if(!t)return null;const r=await fetch(url,{...opts,headers:{...opts.headers,Authorization:`Bearer ${t}`,'Content-Type':'application/json'}});return r.status===401?(localStorage.removeItem('adminToken'),location.href='/login',null):r},
|
apiRequest=async(url,opts={})=>{const t=checkAuth();if(!t)return null;const r=await fetch(url,{...opts,headers:{...opts.headers,Authorization:`Bearer ${t}`,'Content-Type':'application/json'}});return r.status===401?(localStorage.removeItem('adminToken'),location.href='/login',null):r},
|
||||||
|
// 获取token的状态文本和样式
|
||||||
|
getTokenStatus=(token)=>{if(token.is_active){return{text:'活跃',class:'bg-green-50 text-green-700'}}const reason=token.disabled_reason;if(reason==='token_invalid'||reason==='expired'){return{text:'失效',class:'bg-gray-100 text-gray-700'}}if(reason==='manual'||reason==='error_limit'){return{text:'禁用',class:'bg-gray-100 text-gray-700'}}return{text:'禁用',class:'bg-gray-100 text-gray-700'}},
|
||||||
loadStats=async()=>{try{const r=await apiRequest('/api/stats');if(!r)return;const d=await r.json();$('statTotal').textContent=d.total_tokens||0;$('statActive').textContent=d.active_tokens||0;$('statImages').textContent=(d.today_images||0)+'/'+(d.total_images||0);$('statVideos').textContent=(d.today_videos||0)+'/'+(d.total_videos||0);$('statErrors').textContent=(d.today_errors||0)+'/'+(d.total_errors||0)}catch(e){console.error('加载统计失败:',e)}},
|
loadStats=async()=>{try{const r=await apiRequest('/api/stats');if(!r)return;const d=await r.json();$('statTotal').textContent=d.total_tokens||0;$('statActive').textContent=d.active_tokens||0;$('statImages').textContent=(d.today_images||0)+'/'+(d.total_images||0);$('statVideos').textContent=(d.today_videos||0)+'/'+(d.total_videos||0);$('statErrors').textContent=(d.today_errors||0)+'/'+(d.total_errors||0)}catch(e){console.error('加载统计失败:',e)}},
|
||||||
loadTokens=async()=>{try{const r=await apiRequest('/api/tokens');if(!r)return;allTokens=await r.json();updateStatusFilterOptions();renderTokens()}catch(e){console.error('加载Token失败:',e)}},
|
loadTokens=async()=>{try{const r=await apiRequest('/api/tokens');if(!r)return;allTokens=await r.json();updateStatusFilterOptions();renderTokens()}catch(e){console.error('加载Token失败:',e)}},
|
||||||
updateStatusFilterOptions=()=>{const statusSet=new Set();allTokens.forEach(t=>{const statusText=t.is_expired?'已过期':(t.is_active?'活跃':'禁用');statusSet.add(statusText)});const dropdown=$('statusFilterDropdown');if(!dropdown)return;const statuses=Array.from(statusSet).sort();dropdown.innerHTML='<div class="py-1"><button onclick="selectStatusFilter(\'\')" class="w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors" data-filter-option="">全部</button>'+statuses.map(s=>`<button onclick="selectStatusFilter('${s}')" class="w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors" data-filter-option="${s}">${s}</button>`).join('')+'</div>';updateStatusFilterLabel()},
|
updateStatusFilterOptions=()=>{const statusSet=new Set();allTokens.forEach(t=>{const status=getTokenStatus(t);statusSet.add(status.text)});const dropdown=$('statusFilterDropdown');if(!dropdown)return;const statuses=Array.from(statusSet).sort();dropdown.innerHTML='<div class="py-1"><button onclick="selectStatusFilter(\'\')" class="w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors" data-filter-option="">全部</button>'+statuses.map(s=>`<button onclick="selectStatusFilter('${s}')" class="w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors" data-filter-option="${s}">${s}</button>`).join('')+'</div>';updateStatusFilterLabel()},
|
||||||
updateStatusFilterLabel=()=>{const label=$('statusFilterLabel');if(label){label.textContent=currentStatusFilter||'全部'}},
|
updateStatusFilterLabel=()=>{const label=$('statusFilterLabel');if(label){label.textContent=currentStatusFilter||'全部'}},
|
||||||
toggleStatusFilterDropdown=()=>{const dropdown=$('statusFilterDropdown');if(!dropdown)return;dropdown.classList.toggle('hidden')},
|
toggleStatusFilterDropdown=()=>{const dropdown=$('statusFilterDropdown');if(!dropdown)return;dropdown.classList.toggle('hidden')},
|
||||||
selectStatusFilter=(status)=>{currentStatusFilter=status;currentPage=1;updateStatusFilterLabel();toggleStatusFilterDropdown();renderTokens()},
|
selectStatusFilter=(status)=>{currentStatusFilter=status;currentPage=1;updateStatusFilterLabel();toggleStatusFilterDropdown();renderTokens()},
|
||||||
applyStatusFilter=()=>{currentPage=1;renderTokens()},
|
applyStatusFilter=()=>{currentPage=1;renderTokens()},
|
||||||
getFilteredTokens=()=>{if(!currentStatusFilter)return allTokens;return allTokens.filter(t=>{const statusText=t.is_expired?'已过期':(t.is_active?'活跃':'禁用');return statusText===currentStatusFilter})},
|
getFilteredTokens=()=>{if(!currentStatusFilter)return allTokens;return allTokens.filter(t=>{const status=getTokenStatus(t);return status.text===currentStatusFilter})},
|
||||||
formatExpiry=exp=>{if(!exp)return'-';const d=new Date(exp),now=new Date(),diff=d-now;const dateStr=d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-');const timeStr=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',hour12:false});if(diff<0)return`<span class="text-red-600">${dateStr} ${timeStr}</span>`;const days=Math.floor(diff/864e5);if(days<7)return`<span class="text-orange-600">${dateStr} ${timeStr}</span>`;return`${dateStr} ${timeStr}`},
|
formatExpiry=exp=>{if(!exp)return'-';const d=new Date(exp),now=new Date(),diff=d-now;const dateStr=d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-');const timeStr=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',hour12:false});if(diff<0)return`<span class="text-red-600">${dateStr} ${timeStr}</span>`;const days=Math.floor(diff/864e5);if(days<7)return`<span class="text-orange-600">${dateStr} ${timeStr}</span>`;return`${dateStr} ${timeStr}`},
|
||||||
formatPlanType=type=>{if(!type)return'-';const typeMap={'chatgpt_team':'Team','chatgpt_plus':'Plus','chatgpt_pro':'Pro','chatgpt_free':'Free'};return typeMap[type]||type},
|
formatPlanType=type=>{if(!type)return'-';const typeMap={'chatgpt_team':'Team','chatgpt_plus':'Plus','chatgpt_pro':'Pro','chatgpt_free':'Free'};return typeMap[type]||type},
|
||||||
formatPlanTypeWithTooltip=(t)=>{const tooltipText=t.subscription_end?`套餐到期: ${new Date(t.subscription_end).toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-')} ${new Date(t.subscription_end).toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',hour12:false})}`:'';return`<span class="inline-flex items-center rounded px-2 py-0.5 text-xs bg-blue-50 text-blue-700 cursor-pointer" title="${tooltipText||t.plan_title||'-'}">${formatPlanType(t.plan_type)}</span>`},
|
formatPlanTypeWithTooltip=(t)=>{const tooltipText=t.subscription_end?`套餐到期: ${new Date(t.subscription_end).toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-')} ${new Date(t.subscription_end).toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',hour12:false})}`:'';return`<span class="inline-flex items-center rounded px-2 py-0.5 text-xs bg-blue-50 text-blue-700 cursor-pointer" title="${tooltipText||t.plan_title||'-'}">${formatPlanType(t.plan_type)}</span>`},
|
||||||
formatClientId=(clientId)=>{if(!clientId)return'-';const short=clientId.substring(0,8)+'...';return`<span class="text-xs font-mono cursor-pointer hover:text-primary" title="${clientId}" onclick="navigator.clipboard.writeText('${clientId}').then(()=>showToast('已复制','success'))">${short}</span>`},
|
formatClientId=(clientId)=>{if(!clientId)return'-';const short=clientId.substring(0,8)+'...';return`<span class="text-xs font-mono cursor-pointer hover:text-primary" title="${clientId}" onclick="navigator.clipboard.writeText('${clientId}').then(()=>showToast('已复制','success'))">${short}</span>`},
|
||||||
renderTokens=()=>{const filteredTokens=getFilteredTokens();const start=(currentPage-1)*pageSize,end=start+pageSize,paginatedTokens=filteredTokens.slice(start,end);const tb=$('tokenTableBody');tb.innerHTML=paginatedTokens.map(t=>{const imageDisplay=t.image_enabled?`${t.image_count||0}`:'-';const videoDisplay=t.video_enabled?`${t.video_count||0}`:'-';const remainingCount=t.sora2_remaining_count!==undefined&&t.sora2_remaining_count!==null?t.sora2_remaining_count:'-';const statusText=t.is_expired?'已过期':(t.is_active?'活跃':'禁用');const statusClass=t.is_expired?'bg-gray-100 text-gray-700':(t.is_active?'bg-green-50 text-green-700':'bg-gray-100 text-gray-700');return`<tr><td class=\"py-2.5 px-3\"><input type=\"checkbox\" class=\"token-checkbox h-4 w-4 rounded border-gray-300\" data-token-id=\"${t.id}\" onchange=\"toggleTokenSelection(${t.id},this.checked)\" ${selectedTokenIds.has(t.id)?'checked':''}></td><td class=\"py-2.5 px-3\">${t.email}</td><td class=\"py-2.5 px-3\"><span class=\"inline-flex items-center rounded px-2 py-0.5 text-xs ${statusClass}\">${statusText}</span></td><td class="py-2.5 px-3">${formatClientId(t.client_id)}</td><td class="py-2.5 px-3 text-xs">${formatExpiry(t.expiry_time)}</td><td class="py-2.5 px-3 text-xs">${formatPlanTypeWithTooltip(t)}</td><td class="py-2.5 px-3">${remainingCount}</td><td class="py-2.5 px-3">${imageDisplay}</td><td class="py-2.5 px-3">${videoDisplay}</td><td class="py-2.5 px-3">${t.error_count||0}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${t.remark||'-'}</td><td class="py-2.5 px-3 text-right"><button onclick="testToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs mr-1">测试</button><button onclick="openEditModal(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-green-50 hover:text-green-700 h-7 px-2 text-xs mr-1">编辑</button><button onclick="toggleToken(${t.id},${t.is_active})" class="inline-flex items-center justify-center rounded-md hover:bg-accent h-7 px-2 text-xs mr-1">${t.is_active?'禁用':'启用'}</button><button onclick="deleteToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-destructive/10 hover:text-destructive h-7 px-2 text-xs">删除</button></td></tr>`}).join('');renderPagination()},
|
renderTokens=()=>{const filteredTokens=getFilteredTokens();const start=(currentPage-1)*pageSize,end=start+pageSize,paginatedTokens=filteredTokens.slice(start,end);const tb=$('tokenTableBody');tb.innerHTML=paginatedTokens.map(t=>{const imageDisplay=t.image_enabled?`${t.image_count||0}`:'-';const videoDisplay=t.video_enabled?`${t.video_count||0}`:'-';const remainingCount=t.sora2_remaining_count!==undefined&&t.sora2_remaining_count!==null?t.sora2_remaining_count:'-';const status=getTokenStatus(t);const statusText=status.text;const statusClass=status.class;return`<tr><td class=\"py-2.5 px-3\"><input type=\"checkbox\" class=\"token-checkbox h-4 w-4 rounded border-gray-300\" data-token-id=\"${t.id}\" onchange=\"toggleTokenSelection(${t.id},this.checked)\" ${selectedTokenIds.has(t.id)?'checked':''}></td><td class=\"py-2.5 px-3\">${t.email}</td><td class=\"py-2.5 px-3\"><span class=\"inline-flex items-center rounded px-2 py-0.5 text-xs ${statusClass}\">${statusText}</span></td><td class="py-2.5 px-3">${formatClientId(t.client_id)}</td><td class="py-2.5 px-3 text-xs">${formatExpiry(t.expiry_time)}</td><td class="py-2.5 px-3 text-xs">${formatPlanTypeWithTooltip(t)}</td><td class="py-2.5 px-3">${remainingCount}</td><td class="py-2.5 px-3">${imageDisplay}</td><td class="py-2.5 px-3">${videoDisplay}</td><td class="py-2.5 px-3">${t.error_count||0}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${t.remark||'-'}</td><td class="py-2.5 px-3 text-right"><button onclick="testToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs mr-1">测试</button><button onclick="openEditModal(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-green-50 hover:text-green-700 h-7 px-2 text-xs mr-1">编辑</button><button onclick="toggleToken(${t.id},${t.is_active})" class="inline-flex items-center justify-center rounded-md hover:bg-accent h-7 px-2 text-xs mr-1">${t.is_active?'禁用':'启用'}</button><button onclick="deleteToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-destructive/10 hover:text-destructive h-7 px-2 text-xs">删除</button></td></tr>`}).join('');renderPagination()},
|
||||||
refreshTokens=async()=>{await loadTokens();await loadStats()},
|
refreshTokens=async()=>{await loadTokens();await loadStats()},
|
||||||
changePage=(page)=>{currentPage=page;renderTokens()},
|
changePage=(page)=>{currentPage=page;renderTokens()},
|
||||||
changePageSize=(size)=>{pageSize=parseInt(size);currentPage=1;renderTokens()},
|
changePageSize=(size)=>{pageSize=parseInt(size);currentPage=1;renderTokens()},
|
||||||
|
|||||||
Reference in New Issue
Block a user