diff --git a/README.md b/README.md index 1d4e156..3287b1f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ - 由于Flow增加了额外的验证码,你可以自行选择使用浏览器打码或第三发打码: 注册[YesCaptcha](https://yescaptcha.com/i/13Xd8K)并获取api key,将其填入系统配置页面```YesCaptcha API密钥```区域 +- 自动更新st浏览器拓展:[Flow2API-Token-Updater](https://github.com/TheSmallHanCat/Flow2API-Token-Updater) + ### 方式一:Docker 部署(推荐) #### 标准模式(不使用代理) diff --git a/src/api/admin.py b/src/api/admin.py index 6e59300..94deac2 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,11 +1,12 @@ """Admin API routes""" -from fastapi import APIRouter, Depends, HTTPException, Header +from fastapi import APIRouter, Depends, HTTPException, Header, Request from fastapi.responses import JSONResponse from pydantic import BaseModel from typing import Optional, List import secrets from ..core.auth import AuthManager from ..core.database import Database +from ..core.config import config from ..services.token_manager import TokenManager from ..services.proxy_manager import ProxyManager @@ -646,15 +647,25 @@ async def get_logs( "operation": log.get("operation"), "status_code": log.get("status_code"), "duration": log.get("duration"), - "created_at": log.get("created_at") + "created_at": log.get("created_at"), + "request_body": log.get("request_body"), + "response_body": log.get("response_body") } for log in logs] +@router.delete("/api/logs") +async def clear_logs(token: str = Depends(verify_admin_token)): + """Clear all logs""" + try: + await db.clear_all_logs() + return {"success": True, "message": "所有日志已清空"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/api/admin/config") async def get_admin_config(token: str = Depends(verify_admin_token)): """Get admin configuration""" - from ..core.config import config - admin_config = await db.get_admin_config() return { @@ -708,11 +719,9 @@ async def update_debug_config( ): """Update debug configuration""" try: - # Update debug config in database - await db.update_debug_config(enabled=request.enabled) - - # 🔥 Hot reload: sync database config to memory - await db.reload_config_to_memory() + # Update in-memory config only (not database) + # This ensures debug mode is automatically disabled on restart + config.set_debug_enabled(request.enabled) status = "enabled" if request.enabled else "disabled" return {"success": True, "message": f"Debug mode {status}", "enabled": request.enabled} @@ -883,26 +892,35 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)): # ========== Plugin Configuration Endpoints ========== @router.get("/api/plugin/config") -async def get_plugin_config(token: str = Depends(verify_admin_token)): +async def get_plugin_config(request: Request, token: str = Depends(verify_admin_token)): """Get plugin configuration""" plugin_config = await db.get_plugin_config() - # Get server host and port from config - from ..core.config import config - server_host = config.server_host - server_port = config.server_port + # Get the actual domain and port from the request + # This allows the connection URL to reflect the user's actual access path + host_header = request.headers.get("host", "") - # Generate connection URL - if server_host == "0.0.0.0": - connection_url = f"http://127.0.0.1:{server_port}/api/plugin/update-token" + # Generate connection URL based on actual request + if host_header: + # Use the actual domain/IP and port from the request + connection_url = f"http://{host_header}/api/plugin/update-token" else: - connection_url = f"http://{server_host}:{server_port}/api/plugin/update-token" + # Fallback to config-based URL + from ..core.config import config + server_host = config.server_host + server_port = config.server_port + + if server_host == "0.0.0.0": + connection_url = f"http://127.0.0.1:{server_port}/api/plugin/update-token" + else: + connection_url = f"http://{server_host}:{server_port}/api/plugin/update-token" return { "success": True, "config": { "connection_token": plugin_config.connection_token, - "connection_url": connection_url + "connection_url": connection_url, + "auto_enable_on_update": plugin_config.auto_enable_on_update } } @@ -914,17 +932,22 @@ async def update_plugin_config( ): """Update plugin configuration""" connection_token = request.get("connection_token", "") + auto_enable_on_update = request.get("auto_enable_on_update", True) # 默认开启 # Generate random token if empty if not connection_token: connection_token = secrets.token_urlsafe(32) - await db.update_plugin_config(connection_token=connection_token) + await db.update_plugin_config( + connection_token=connection_token, + auto_enable_on_update=auto_enable_on_update + ) return { "success": True, "message": "插件配置更新成功", - "connection_token": connection_token + "connection_token": connection_token, + "auto_enable_on_update": auto_enable_on_update } @@ -989,6 +1012,16 @@ async def plugin_update_token(request: dict, authorization: Optional[str] = Head at_expires=at_expires ) + # Check if auto-enable is enabled and token is disabled + if plugin_config.auto_enable_on_update and not existing_token.is_active: + await token_manager.enable_token(existing_token.id) + return { + "success": True, + "message": f"Token updated and auto-enabled for {email}", + "action": "updated", + "auto_enabled": True + } + return { "success": True, "message": f"Token updated for {email}", diff --git a/src/core/database.py b/src/core/database.py index 8f1cfb1..2d4c808 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -305,6 +305,20 @@ class Database: except Exception as e: print(f" ✗ Failed to add column '{col_name}': {e}") + # Check and add missing columns to plugin_config table + if await self._table_exists(db, "plugin_config"): + plugin_columns_to_add = [ + ("auto_enable_on_update", "BOOLEAN DEFAULT 1"), # 默认开启 + ] + + for col_name, col_type in plugin_columns_to_add: + if not await self._column_exists(db, "plugin_config", col_name): + try: + await db.execute(f"ALTER TABLE plugin_config ADD COLUMN {col_name} {col_type}") + print(f" ✓ Added column '{col_name}' to plugin_config table") + except Exception as e: + print(f" ✗ Failed to add column '{col_name}': {e}") + # ========== Step 3: Ensure all config tables have default rows ========== # Note: This will NOT overwrite existing config rows # It only ensures missing rows are created with default values from setting.toml @@ -996,6 +1010,12 @@ class Database: rows = await cursor.fetchall() return [dict(row) for row in rows] + async def clear_all_logs(self): + """Clear all request logs""" + async with aiosqlite.connect(self.db_path) as db: + await db.execute("DELETE FROM request_logs") + await db.commit() + async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True): """ Initialize database configuration from setting.toml @@ -1227,7 +1247,7 @@ class Database: return PluginConfig(**dict(row)) return PluginConfig() - async def update_plugin_config(self, connection_token: str): + async def update_plugin_config(self, connection_token: str, auto_enable_on_update: bool = True): """Update plugin configuration""" async with aiosqlite.connect(self.db_path) as db: db.row_factory = aiosqlite.Row @@ -1237,13 +1257,13 @@ class Database: if row: await db.execute(""" UPDATE plugin_config - SET connection_token = ?, updated_at = CURRENT_TIMESTAMP + SET connection_token = ?, auto_enable_on_update = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1 - """, (connection_token,)) + """, (connection_token, auto_enable_on_update)) else: await db.execute(""" - INSERT INTO plugin_config (id, connection_token) - VALUES (1, ?) - """, (connection_token,)) + INSERT INTO plugin_config (id, connection_token, auto_enable_on_update) + VALUES (1, ?, ?) + """, (connection_token, auto_enable_on_update)) await db.commit() diff --git a/src/core/models.py b/src/core/models.py index 27c19a8..75f957f 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -162,6 +162,7 @@ class PluginConfig(BaseModel): """Plugin connection configuration""" id: int = 1 connection_token: str = "" # 插件连接token + auto_enable_on_update: bool = True # 更新token时自动启用(默认开启) created_at: Optional[datetime] = None updated_at: Optional[datetime] = None diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index b9611fe..a1d8aed 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -102,6 +102,33 @@ MODEL_CONFIG = { "supports_images": False }, + # veo_3_1_t2v_fast_portrait_ultra (竖屏) + "veo_3_1_t2v_fast_portrait_ultra": { + "type": "video", + "video_type": "t2v", + "model_key": "veo_3_1_t2v_fast_portrait_ultra", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": False + }, + + # veo_3_1_t2v_fast_portrait_ultra_relaxed (竖屏) + "veo_3_1_t2v_fast_portrait_ultra_relaxed": { + "type": "video", + "video_type": "t2v", + "model_key": "veo_3_1_t2v_fast_portrait_ultra_relaxed", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": False + }, + + # veo_3_1_t2v_portrait (竖屏) + "veo_3_1_t2v_portrait": { + "type": "video", + "video_type": "t2v", + "model_key": "veo_3_1_t2v_portrait", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": False + }, + # ========== 首尾帧模型 (I2V - Image to Video) ========== # 支持1-2张图片:1张作为首帧,2张作为首尾帧 @@ -165,6 +192,66 @@ MODEL_CONFIG = { "max_images": 2 }, + # veo_3_1_i2v_s_fast_ultra (需要新增横竖屏) + "veo_3_1_i2v_s_fast_ultra_portrait": { + "type": "video", + "video_type": "i2v", + "model_key": "veo_3_1_i2v_s_fast_ultra", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": True, + "min_images": 1, + "max_images": 2 + }, + "veo_3_1_i2v_s_fast_ultra_landscape": { + "type": "video", + "video_type": "i2v", + "model_key": "veo_3_1_i2v_s_fast_ultra", + "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE", + "supports_images": True, + "min_images": 1, + "max_images": 2 + }, + + # veo_3_1_i2v_s_fast_ultra_relaxed (需要新增横竖屏) + "veo_3_1_i2v_s_fast_ultra_relaxed_portrait": { + "type": "video", + "video_type": "i2v", + "model_key": "veo_3_1_i2v_s_fast_ultra_relaxed", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": True, + "min_images": 1, + "max_images": 2 + }, + "veo_3_1_i2v_s_fast_ultra_relaxed_landscape": { + "type": "video", + "video_type": "i2v", + "model_key": "veo_3_1_i2v_s_fast_ultra_relaxed", + "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE", + "supports_images": True, + "min_images": 1, + "max_images": 2 + }, + + # veo_3_1_i2v_s (需要新增横竖屏) + "veo_3_1_i2v_s_portrait": { + "type": "video", + "video_type": "i2v", + "model_key": "veo_3_1_i2v_s", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": True, + "min_images": 1, + "max_images": 2 + }, + "veo_3_1_i2v_s_landscape": { + "type": "video", + "video_type": "i2v", + "model_key": "veo_3_1_i2v_s", + "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE", + "supports_images": True, + "min_images": 1, + "max_images": 2 + }, + # ========== 多图生成 (R2V - Reference Images to Video) ========== # 支持多张图片,不限制数量 @@ -186,6 +273,46 @@ MODEL_CONFIG = { "supports_images": True, "min_images": 0, "max_images": None # 不限制 + }, + + # veo_3_0_r2v_fast_ultra (需要新增横竖屏) + "veo_3_0_r2v_fast_ultra_portrait": { + "type": "video", + "video_type": "r2v", + "model_key": "veo_3_0_r2v_fast_ultra", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": True, + "min_images": 0, + "max_images": None # 不限制 + }, + "veo_3_0_r2v_fast_ultra_landscape": { + "type": "video", + "video_type": "r2v", + "model_key": "veo_3_0_r2v_fast_ultra", + "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE", + "supports_images": True, + "min_images": 0, + "max_images": None # 不限制 + }, + + # veo_3_0_r2v_fast_ultra_relaxed (需要新增横竖屏) + "veo_3_0_r2v_fast_ultra_relaxed_portrait": { + "type": "video", + "video_type": "r2v", + "model_key": "veo_3_0_r2v_fast_ultra_relaxed", + "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT", + "supports_images": True, + "min_images": 0, + "max_images": None # 不限制 + }, + "veo_3_0_r2v_fast_ultra_relaxed_landscape": { + "type": "video", + "video_type": "r2v", + "model_key": "veo_3_0_r2v_fast_ultra_relaxed", + "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE", + "supports_images": True, + "min_images": 0, + "max_images": None # 不限制 } } @@ -343,11 +470,25 @@ class GenerationHandler: # 7. 记录成功日志 duration = time.time() - start_time + + # 构建响应数据,包含生成的URL + response_data = { + "status": "success", + "model": model, + "prompt": prompt[:100] + } + + # 添加生成的URL(如果有) + if hasattr(self, '_last_generated_url') and self._last_generated_url: + response_data["url"] = self._last_generated_url + # 清除临时存储 + self._last_generated_url = None + await self._log_request( token.id, f"generate_{generation_type}", {"model": model, "prompt": prompt[:100], "has_images": images is not None and len(images) > 0}, - {"status": "success"}, + response_data, 200, duration ) @@ -358,12 +499,8 @@ class GenerationHandler: if stream: yield self._create_stream_chunk(f"❌ {error_msg}\n") if token: - # 检测429错误,立即禁用token - if "429" in str(e) or "HTTP Error 429" in str(e): - debug_logger.log_warning(f"[429_BAN] Token {token.id} 遇到429错误,立即禁用") - await self.token_manager.ban_token_for_429(token.id) - else: - await self.token_manager.record_error(token.id) + # 记录错误(所有错误统一处理,不再特殊处理429) + await self.token_manager.record_error(token.id) yield self._create_error_response(error_msg) # 记录失败日志 @@ -464,6 +601,9 @@ class GenerationHandler: yield self._create_stream_chunk("缓存已关闭,正在返回源链接...\n") # 返回结果 + # 存储URL用于日志记录 + self._last_generated_url = local_url + if stream: yield self._create_stream_chunk( f"![Generated Image]({local_url})", @@ -732,6 +872,9 @@ class GenerationHandler: completed_at=time.time() ) + # 存储URL用于日志记录 + self._last_generated_url = local_url + # 返回结果 if stream: yield self._create_stream_chunk( diff --git a/static/manage.html b/static/manage.html index c8c0b26..d4d7440 100644 --- a/static/manage.html +++ b/static/manage.html @@ -339,6 +339,13 @@

用于验证Chrome扩展插件的身份,留空将自动生成随机token

+
+ +

当插件更新token时,如果该token被禁用,则自动启用它

+

ℹ️ 使用说明:安装Chrome扩展后,将连接接口和Token配置到插件中,插件会自动提取Google Labs的cookie并更新到系统 @@ -392,11 +399,19 @@

请求日志

- +
+ + +
@@ -407,6 +422,7 @@ + @@ -417,6 +433,26 @@ + + +

© 2025 TheSmallHanCat && Tibbar. All rights reserved.

@@ -666,14 +702,17 @@ toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)}, loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}}, saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,apiKey=$('cfgYescaptchaApiKey').value.trim(),baseUrl=$('cfgYescaptchaBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,apiKey,baseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:apiKey,yescaptcha_base_url:baseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}}, - loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||''}}catch(e){console.error('加载插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}}, - savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}}, + loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||'';$('cfgAutoEnableOnUpdate').checked=d.config.auto_enable_on_update||false}}catch(e){console.error('加载插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}}, + savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();const autoEnable=$('cfgAutoEnableOnUpdate').checked;try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token,auto_enable_on_update:autoEnable})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}}, copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))}, copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))}, toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}}, loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}}, - loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`
`).join('')}catch(e){console.error('加载日志失败:',e)}}, + loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>``).join('')}catch(e){console.error('加载日志失败:',e)}}, refreshLogs=async()=>{await loadLogs()}, + clearAllLogs=async()=>{if(!confirm('确定要清空所有日志吗?此操作不可恢复!'))return;try{const r=await apiRequest('/api/logs',{method:'DELETE'});if(!r)return;const d=await r.json();if(d.success){showToast('日志已清空','success');await loadLogs()}else{showToast('清空失败: '+(d.message||'未知错误'),'error')}}catch(e){showToast('清空失败: '+e.message,'error')}}, + showLogDetail=(logId)=>{const log=window.allLogs.find(l=>l.id===logId);if(!log){showToast('日志不存在','error');return}const content=$('logDetailContent');let detailHtml='';if(log.status_code===200){try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody){if(responseBody.url){detailHtml+=`

生成结果

`}else if(responseBody.data&&responseBody.data.length>0){const item=responseBody.data[0];if(item.url){detailHtml+=`

生成结果

文件URL:

${item.url}
`}else{detailHtml+=`

响应数据

${JSON.stringify(responseBody,null,2)}
`}}else{detailHtml+=`

响应数据

${JSON.stringify(responseBody,null,2)}
`}}else{detailHtml+=`

响应信息

无响应数据

`}}catch(e){detailHtml+=`

响应数据

${log.response_body||'无'}
`}}else{try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody&&responseBody.error){detailHtml+=`

错误原因

${responseBody.error.message||responseBody.error||'未知错误'}

`}else if(log.response_body&&log.response_body!=='{}'){detailHtml+=`

错误信息

${log.response_body}
`}}catch(e){if(log.response_body&&log.response_body!=='{}'){detailHtml+=`

错误信息

${log.response_body}
`}}}detailHtml+=`

基本信息

操作: ${log.operation}
状态码: ${log.status_code}
耗时: ${log.duration.toFixed(2)}秒
时间: ${log.created_at?new Date(log.created_at).toLocaleString('zh-CN'):'-'}
`;content.innerHTML=detailHtml;$('logDetailModal').classList.remove('hidden')}, + closeLogDetailModal=()=>{$('logDetailModal').classList.add('hidden')}, showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)}, logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'}, switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadCacheConfig();loadGenerationTimeout();loadCaptchaConfig();loadPluginConfig();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
状态码 耗时(秒) 时间详情
${l.operation}${l.token_email||'未知'}${l.status_code}${l.duration.toFixed(2)}${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}
${l.operation}${l.token_email||'未知'}${l.status_code}${l.duration.toFixed(2)}${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}