mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-02-04 10:14:41 +08:00
feat: 支持纯RT导入
This commit is contained in:
112
src/api/admin.py
112
src/api/admin.py
@@ -115,6 +115,13 @@ class ImportTokensRequest(BaseModel):
|
||||
tokens: List[ImportTokenItem]
|
||||
mode: str = "at" # Import mode: offline/at/st/rt
|
||||
|
||||
class PureRtImportRequest(BaseModel):
|
||||
refresh_tokens: List[str] # List of Refresh Tokens
|
||||
client_id: str # Client ID (required)
|
||||
proxy_url: Optional[str] = None # Proxy URL (optional)
|
||||
image_concurrency: int = 1 # Image concurrency limit (default: 1)
|
||||
video_concurrency: int = 3 # Video concurrency limit (default: 3)
|
||||
|
||||
class UpdateAdminConfigRequest(BaseModel):
|
||||
error_ban_threshold: int
|
||||
task_retry_enabled: Optional[bool] = None
|
||||
@@ -662,6 +669,111 @@ async def import_tokens(request: ImportTokensRequest, token: str = Depends(verif
|
||||
"results": results
|
||||
}
|
||||
|
||||
@router.post("/api/tokens/import/pure-rt")
|
||||
async def import_pure_rt(request: PureRtImportRequest, token: str = Depends(verify_admin_token)):
|
||||
"""Import tokens using pure RT mode (batch RT conversion and import)"""
|
||||
added_count = 0
|
||||
updated_count = 0
|
||||
failed_count = 0
|
||||
results = []
|
||||
|
||||
for rt in request.refresh_tokens:
|
||||
try:
|
||||
# Step 1: Use RT + client_id + proxy to refresh and get AT
|
||||
rt_result = await token_manager.rt_to_at(
|
||||
rt,
|
||||
client_id=request.client_id,
|
||||
proxy_url=request.proxy_url
|
||||
)
|
||||
|
||||
access_token = rt_result.get("access_token")
|
||||
new_refresh_token = rt_result.get("refresh_token", rt) # Use new RT if returned, else use original
|
||||
|
||||
if not access_token:
|
||||
raise ValueError("Failed to get access_token from RT conversion")
|
||||
|
||||
# Step 2: Parse AT to get user info (email)
|
||||
# The rt_to_at already includes email in the response
|
||||
email = rt_result.get("email")
|
||||
|
||||
# If email not in rt_result, parse it from access_token
|
||||
if not email:
|
||||
import jwt
|
||||
try:
|
||||
decoded = jwt.decode(access_token, options={"verify_signature": False})
|
||||
email = decoded.get("https://api.openai.com/profile", {}).get("email")
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to parse email from access_token: {str(e)}")
|
||||
|
||||
if not email:
|
||||
raise ValueError("Failed to extract email from access_token")
|
||||
|
||||
# Step 3: Check if token with this email already exists
|
||||
existing_token = await db.get_token_by_email(email)
|
||||
|
||||
if existing_token:
|
||||
# Update existing token
|
||||
await token_manager.update_token(
|
||||
token_id=existing_token.id,
|
||||
token=access_token,
|
||||
st=None, # No ST in pure RT mode
|
||||
rt=new_refresh_token, # Use refreshed RT
|
||||
client_id=request.client_id,
|
||||
proxy_url=request.proxy_url,
|
||||
remark=None, # Keep existing remark
|
||||
image_enabled=True,
|
||||
video_enabled=True,
|
||||
image_concurrency=request.image_concurrency,
|
||||
video_concurrency=request.video_concurrency,
|
||||
skip_status_update=False # Update status with new AT
|
||||
)
|
||||
updated_count += 1
|
||||
results.append({
|
||||
"email": email,
|
||||
"status": "updated",
|
||||
"message": "Token updated successfully"
|
||||
})
|
||||
else:
|
||||
# Add new token
|
||||
new_token = await token_manager.add_token(
|
||||
token_value=access_token,
|
||||
st=None, # No ST in pure RT mode
|
||||
rt=new_refresh_token, # Use refreshed RT
|
||||
client_id=request.client_id,
|
||||
proxy_url=request.proxy_url,
|
||||
remark=None,
|
||||
update_if_exists=False,
|
||||
image_enabled=True,
|
||||
video_enabled=True,
|
||||
image_concurrency=request.image_concurrency,
|
||||
video_concurrency=request.video_concurrency,
|
||||
skip_status_update=False, # Update status with new AT
|
||||
email=email # Pass email for new token
|
||||
)
|
||||
added_count += 1
|
||||
results.append({
|
||||
"email": email,
|
||||
"status": "added",
|
||||
"message": "Token added successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"email": "unknown",
|
||||
"status": "failed",
|
||||
"message": str(e)
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Pure RT import completed: {added_count} added, {updated_count} updated, {failed_count} failed",
|
||||
"added": added_count,
|
||||
"updated": updated_count,
|
||||
"failed": failed_count,
|
||||
"results": results
|
||||
}
|
||||
|
||||
@router.put("/api/tokens/{token_id}")
|
||||
async def update_token(
|
||||
token_id: int,
|
||||
|
||||
@@ -840,11 +840,41 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<div id="jsonFileSection">
|
||||
<label class="text-sm font-medium mb-2 block">选择 JSON 文件</label>
|
||||
<input type="file" id="importFile" accept=".json" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<p class="text-xs text-muted-foreground mt-1">选择导出的 Token JSON 文件进行导入</p>
|
||||
</div>
|
||||
|
||||
<!-- 纯RT导入输入框区域 -->
|
||||
<div id="pureRtSection" class="hidden space-y-2.5">
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-1.5 block">Refresh Token 列表</label>
|
||||
<textarea id="pureRtInput" rows="3" placeholder="每行一个 RT" class="flex w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm resize-none"></textarea>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2.5">
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-1.5 block">Client ID(可选)</label>
|
||||
<input type="text" id="pureRtClientId" placeholder="留空使用默认值" class="flex h-8 w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-1.5 block">代理地址(可选)</label>
|
||||
<input type="text" id="pureRtProxy" placeholder="http://127.0.0.1:7890" class="flex h-8 w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2.5">
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-1.5 block">图片并发</label>
|
||||
<input type="number" id="pureRtImageConcurrency" value="1" min="-1" class="flex h-8 w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-1.5 block">视频并发</label>
|
||||
<input type="number" id="pureRtVideoConcurrency" value="3" min="-1" class="flex h-8 w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">💡 提示:自动刷新并批量导入,并发 -1 表示不限制</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-2 block">选择导入模式</label>
|
||||
<select id="importMode" onchange="updateImportModeHint()" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
@@ -852,10 +882,11 @@
|
||||
<option value="offline">离线导入(不更新账号状态)</option>
|
||||
<option value="st">优先使用ST导入</option>
|
||||
<option value="rt">优先使用RT导入</option>
|
||||
<option value="pure_rt">纯RT导入</option>
|
||||
</select>
|
||||
<p id="importModeHint" class="text-xs text-muted-foreground mt-1">使用AT更新账号状态(订阅信息、Sora2次数等)</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-gray-50 dark:bg-gray-900/20 p-3 border border-gray-200 dark:border-gray-800">
|
||||
<div id="importModeHelpSection" class="rounded-md bg-gray-50 dark:bg-gray-900/20 p-3 border border-gray-200 dark:border-gray-800">
|
||||
<p class="text-xs font-semibold text-gray-900 dark:text-gray-100 mb-2">📋 导入模式说明</p>
|
||||
<div class="space-y-1.5 text-xs text-gray-700 dark:text-gray-300">
|
||||
<div class="flex items-start gap-2">
|
||||
@@ -874,6 +905,10 @@
|
||||
<span class="font-medium min-w-[100px]">RT导入:</span>
|
||||
<span>适用于只有RT没有AT,自动转换为AT</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium min-w-[100px]">纯RT导入:</span>
|
||||
<span>手动输入RT列表(一行一个),自动转换并批量导入</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
💡 提示:离线导入后可使用"测试"按钮更新账号信息,功能不稳定有bug问猫猫
|
||||
@@ -993,7 +1028,7 @@
|
||||
openBatchProxyModal=()=>{if(selectedTokenIds.size===0){showToast('请先选择要修改的Token','info');return}$('batchProxyCount').textContent=selectedTokenIds.size;$('batchProxyUrl').value='';$('batchProxyModal').classList.remove('hidden')},
|
||||
closeBatchProxyModal=()=>{$('batchProxyModal').classList.add('hidden');$('batchProxyUrl').value=''},
|
||||
submitBatchProxy=async()=>{const proxyUrl=$('batchProxyUrl').value.trim();const btn=$('batchProxyBtn'),btnText=$('batchProxyBtnText'),btnSpinner=$('batchProxyBtnSpinner');btn.disabled=true;btnText.textContent='修改中...';btnSpinner.classList.remove('hidden');try{const r=await apiRequest('/api/tokens/batch/update-proxy',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds),proxy_url:proxyUrl})});if(!r){btn.disabled=false;btnText.textContent='确认修改';btnSpinner.classList.add('hidden');return}const d=await r.json();if(d.success){closeBatchProxyModal();selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('修改失败: '+(d.detail||d.message||'未知错误'),'error')}}catch(e){showToast('修改失败: '+e.message,'error')}finally{btn.disabled=false;btnText.textContent='确认修改';btnSpinner.classList.add('hidden')}},
|
||||
showImportProgress=(results,added,updated,failed)=>{const summary=$('importProgressSummary'),list=$('importProgressList');summary.innerHTML=`<div class="text-sm"><strong>导入完成</strong><br/>新增: ${added} | 更新: ${updated} | 失败: ${failed}</div>`;list.innerHTML=results.map(r=>{const statusColor=r.success?(r.status==='added'?'text-green-600':'text-blue-600'):'text-red-600';const statusText=r.status==='added'?'新增':r.status==='updated'?'更新':'失败';return`<div class="p-3 rounded-md border ${r.success?'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800':'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'}"><div class="flex items-center justify-between"><span class="font-medium">${r.email}</span><span class="${statusColor} text-sm font-medium">${statusText}</span></div>${r.error?`<div class="text-xs text-red-600 dark:text-red-400 mt-1">${r.error}</div>`:''}</div>`}).join('');openImportProgressModal()},
|
||||
showImportProgress=(results,added,updated,failed)=>{const summary=$('importProgressSummary'),list=$('importProgressList');summary.innerHTML=`<div class="text-sm"><strong>导入完成</strong><br/>新增: ${added} | 更新: ${updated} | 失败: ${failed}</div>`;list.innerHTML=results.map(r=>{const isFailed=r.status==='failed';const isAdded=r.status==='added';const statusColor=isFailed?'text-red-600':(isAdded?'text-green-600':'text-blue-600');const statusText=isAdded?'新增':(r.status==='updated'?'更新':'失败');const bgColor=isFailed?'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800':(isAdded?'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800':'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800');const errorMsg=r.message&&isFailed?r.message:(r.error||'');return`<div class="p-3 rounded-md border ${bgColor}"><div class="flex items-center justify-between"><span class="font-medium">${r.email}</span><span class="${statusColor} text-sm font-medium">${statusText}</span></div>${errorMsg?`<div class="text-xs text-red-600 dark:text-red-400 mt-1 whitespace-pre-wrap">${errorMsg}</div>`:''}</div>`}).join('');openImportProgressModal()},
|
||||
exportTokens=()=>{if(allTokens.length===0){showToast('没有Token可导出','error');return}const exportData=allTokens.map(t=>({email:t.email,access_token:t.token,session_token:t.st||null,refresh_token:t.rt||null,client_id:t.client_id||null,proxy_url:t.proxy_url||null,remark:t.remark||null,is_active:t.is_active,image_enabled:t.image_enabled!==false,video_enabled:t.video_enabled!==false,image_concurrency:t.image_concurrency||(-1),video_concurrency:t.video_concurrency||(-1)}));const dataStr=JSON.stringify(exportData,null,2);const dataBlob=new Blob([dataStr],{type:'application/json'});const url=URL.createObjectURL(dataBlob);const link=document.createElement('a');link.href=url;link.download=`tokens_${new Date().toISOString().split('T')[0]}.json`;document.body.appendChild(link);link.click();document.body.removeChild(link);URL.revokeObjectURL(url);showToast(`已导出 ${allTokens.length} 个Token`,'success')},
|
||||
batchTestUpdate=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要测试的Token','info');return}if(!confirm(`⚠️ 警告\n\n此操作将请求上游获取选中的 ${selectedTokenIds.size} 个Token的状态信息,可能需要较长时间。\n\n确定要继续吗?`)){return}showToast('正在测试更新选中的Token...','info');try{const r=await apiRequest('/api/tokens/batch/test-update',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('测试更新失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('测试更新失败: '+e.message,'error')}},
|
||||
batchEnableAll=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要启用的Token','info');return}if(!confirm(`确定要启用选中的 ${selectedTokenIds.size} 个Token吗?\n\n此操作将重置这些Token的错误计数。`)){return}showToast('正在批量启用Token...','info');try{const r=await apiRequest('/api/tokens/batch/enable-all',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('批量启用失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('批量启用失败: '+e.message,'error')}},
|
||||
@@ -1002,8 +1037,8 @@
|
||||
toggleSelectAll=()=>{const checkbox=$('selectAllCheckbox');const checkboxes=document.querySelectorAll('.token-checkbox');if(checkbox.checked){checkboxes.forEach(cb=>{cb.checked=true;const tokenId=parseInt(cb.getAttribute('data-token-id'));selectedTokenIds.add(tokenId)})}else{checkboxes.forEach(cb=>{cb.checked=false});selectedTokenIds.clear()}},
|
||||
toggleTokenSelection=(tokenId,checked)=>{if(checked){selectedTokenIds.add(tokenId)}else{selectedTokenIds.delete(tokenId)}const allCheckboxes=document.querySelectorAll('.token-checkbox');const allChecked=Array.from(allCheckboxes).every(cb=>cb.checked);$('selectAllCheckbox').checked=allChecked},
|
||||
batchDisableSelected=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要禁用的Token','info');return}if(!confirm(`确定要禁用选中的 ${selectedTokenIds.size} 个Token吗?`)){return}showToast('正在批量禁用Token...','info');try{const r=await apiRequest('/api/tokens/batch/disable-selected',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('批量禁用失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('批量禁用失败: '+e.message,'error')}},
|
||||
updateImportModeHint=()=>{const mode=$('importMode').value,hint=$('importModeHint'),hints={at:'使用AT更新账号状态(订阅信息、Sora2次数等)',offline:'离线导入,不更新账号状态,动态字段显示为-',st:'自动将ST转换为AT,然后更新账号状态',rt:'自动将RT转换为AT(并刷新RT),然后更新账号状态'};hint.textContent=hints[mode]||''},
|
||||
submitImportTokens=async()=>{const fileInput=$('importFile');if(!fileInput.files||fileInput.files.length===0){showToast('请选择文件','error');return}const file=fileInput.files[0];if(!file.name.endsWith('.json')){showToast('请选择JSON文件','error');return}const mode=$('importMode').value;try{const fileContent=await file.text();const importData=JSON.parse(fileContent);if(!Array.isArray(importData)){showToast('JSON格式错误:应为数组','error');return}if(importData.length===0){showToast('JSON文件为空','error');return}for(let item of importData){if(!item.email){showToast('导入数据缺少必填字段: email','error');return}if(mode==='offline'||mode==='at'){if(!item.access_token){showToast(`${item.email} 缺少必填字段: access_token`,'error');return}}else if(mode==='st'){if(!item.session_token){showToast(`${item.email} 缺少必填字段: session_token`,'error');return}}else if(mode==='rt'){if(!item.refresh_token){showToast(`${item.email} 缺少必填字段: refresh_token`,'error');return}}}const btn=$('importBtn'),btnText=$('importBtnText'),btnSpinner=$('importBtnSpinner');btn.disabled=true;btnText.textContent='导入中...';btnSpinner.classList.remove('hidden');try{const r=await apiRequest('/api/tokens/import',{method:'POST',body:JSON.stringify({tokens:importData,mode:mode})});if(!r){btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden');return}const d=await r.json();if(d.success){closeImportModal();await refreshTokens();showImportProgress(d.results||[],d.added||0,d.updated||0,d.failed||0)}else{showToast('导入失败: '+(d.detail||d.message||'未知错误'),'error')}}catch(e){showToast('导入失败: '+e.message,'error')}finally{btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden')}}catch(e){showToast('文件解析失败: '+e.message,'error')}},
|
||||
updateImportModeHint=()=>{const mode=$('importMode').value,hint=$('importModeHint'),jsonSection=$('jsonFileSection'),pureRtSection=$('pureRtSection'),helpSection=$('importModeHelpSection'),hints={at:'使用AT更新账号状态(订阅信息、Sora2次数等)',offline:'离线导入,不更新账号状态,动态字段显示为-',st:'自动将ST转换为AT,然后更新账号状态',rt:'自动将RT转换为AT(并刷新RT),然后更新账号状态',pure_rt:'手动输入RT列表,自动刷新并批量导入'};hint.textContent=hints[mode]||'';if(mode==='pure_rt'){jsonSection.classList.add('hidden');pureRtSection.classList.remove('hidden');helpSection.classList.add('hidden')}else{jsonSection.classList.remove('hidden');pureRtSection.classList.add('hidden');helpSection.classList.remove('hidden')}},
|
||||
submitImportTokens=async()=>{const mode=$('importMode').value;const btn=$('importBtn'),btnText=$('importBtnText'),btnSpinner=$('importBtnSpinner');if(mode==='pure_rt'){const rtInput=$('pureRtInput').value.trim();if(!rtInput){showToast('请输入 Refresh Token','error');return}const clientId=$('pureRtClientId').value.trim()||'app_LlGpXReQgckcGGUo2JrYvtJK';const proxy=$('pureRtProxy').value.trim()||null;const imageConcurrency=parseInt($('pureRtImageConcurrency').value)||1;const videoConcurrency=parseInt($('pureRtVideoConcurrency').value)||3;const rtList=rtInput.split('\n').map(rt=>rt.trim()).filter(rt=>rt.length>0);if(rtList.length===0){showToast('请输入至少一个 Refresh Token','error');return}btn.disabled=true;btnText.textContent='导入中...';btnSpinner.classList.remove('hidden');try{const r=await apiRequest('/api/tokens/import/pure-rt',{method:'POST',body:JSON.stringify({refresh_tokens:rtList,client_id:clientId,proxy_url:proxy,image_concurrency:imageConcurrency,video_concurrency:videoConcurrency})});if(!r){btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden');return}const d=await r.json();if(d.success){closeImportModal();await refreshTokens();showImportProgress(d.results||[],d.added||0,d.updated||0,d.failed||0)}else{showToast('导入失败: '+(d.detail||d.message||'未知错误'),'error')}}catch(e){showToast('导入失败: '+e.message,'error')}finally{btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden')}return}const fileInput=$('importFile');if(!fileInput.files||fileInput.files.length===0){showToast('请选择文件','error');return}const file=fileInput.files[0];if(!file.name.endsWith('.json')){showToast('请选择JSON文件','error');return}try{const fileContent=await file.text();const importData=JSON.parse(fileContent);if(!Array.isArray(importData)){showToast('JSON格式错误:应为数组','error');return}if(importData.length===0){showToast('JSON文件为空','error');return}for(let item of importData){if(!item.email){showToast('导入数据缺少必填字段: email','error');return}if(mode==='offline'||mode==='at'){if(!item.access_token){showToast(`${item.email} 缺少必填字段: access_token`,'error');return}}else if(mode==='st'){if(!item.session_token){showToast(`${item.email} 缺少必填字段: session_token`,'error');return}}else if(mode==='rt'){if(!item.refresh_token){showToast(`${item.email} 缺少必填字段: refresh_token`,'error');return}}}btn.disabled=true;btnText.textContent='导入中...';btnSpinner.classList.remove('hidden');try{const r=await apiRequest('/api/tokens/import',{method:'POST',body:JSON.stringify({tokens:importData,mode:mode})});if(!r){btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden');return}const d=await r.json();if(d.success){closeImportModal();await refreshTokens();showImportProgress(d.results||[],d.added||0,d.updated||0,d.failed||0)}else{showToast('导入失败: '+(d.detail||d.message||'未知错误'),'error')}}catch(e){showToast('导入失败: '+e.message,'error')}finally{btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden')}}catch(e){showToast('文件解析失败: '+e.message,'error')}},
|
||||
loadAdminConfig=async()=>{try{const r=await apiRequest('/api/admin/config');if(!r)return;const d=await r.json();$('cfgErrorBan').value=d.error_ban_threshold||3;$('cfgTaskRetryEnabled').checked=d.task_retry_enabled||false;$('cfgTaskMaxRetries').value=d.task_max_retries||3;$('cfgAutoDisableOn401').checked=d.auto_disable_on_401||false;$('cfgAdminUsername').value=d.admin_username||'admin';$('cfgCurrentAPIKey').value=d.api_key||'';$('cfgDebugEnabled').checked=d.debug_enabled||false}catch(e){console.error('加载配置失败:',e)}},
|
||||
saveAdminConfig=async()=>{try{const r=await apiRequest('/api/admin/config',{method:'POST',body:JSON.stringify({error_ban_threshold:parseInt($('cfgErrorBan').value)||3,task_retry_enabled:$('cfgTaskRetryEnabled').checked,task_max_retries:parseInt($('cfgTaskMaxRetries').value)||3,auto_disable_on_401:$('cfgAutoDisableOn401').checked})});if(!r)return;const d=await r.json();d.success?showToast('配置保存成功','success'):showToast('保存失败','error')}catch(e){showToast('保存失败: '+e.message,'error')}},
|
||||
updateAdminPassword=async()=>{const username=$('cfgAdminUsername').value.trim(),oldPwd=$('cfgOldPassword').value.trim(),newPwd=$('cfgNewPassword').value.trim();if(!oldPwd||!newPwd)return showToast('请输入旧密码和新密码','error');if(newPwd.length<4)return showToast('新密码至少4个字符','error');try{const r=await apiRequest('/api/admin/password',{method:'POST',body:JSON.stringify({username:username||undefined,old_password:oldPwd,new_password:newPwd})});if(!r)return;const d=await r.json();if(d.success){showToast('密码修改成功,请重新登录','success');setTimeout(()=>{localStorage.removeItem('adminToken');location.href='/login'},2000)}else{showToast('修改失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('修改失败: '+e.message,'error')}},
|
||||
|
||||
Reference in New Issue
Block a user