feat: 浏览器拓展更新st自动启用token、新增ultra模型、完善日志记录显示

This commit is contained in:
TheSmallHanCat
2026-01-04 17:22:19 +08:00
parent 35d960d238
commit d96fe18fa5
6 changed files with 280 additions and 42 deletions

View File

@@ -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 部署(推荐)
#### 标准模式(不使用代理)

View File

@@ -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}",

View File

@@ -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()

View File

@@ -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

View File

@@ -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(

View File

@@ -339,6 +339,13 @@
</div>
<p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份留空将自动生成随机token</p>
</div>
<div>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="cfgAutoEnableOnUpdate" class="h-4 w-4 rounded border-input">
<span class="text-sm font-medium">更新token时自动启用</span>
</label>
<p class="text-xs text-muted-foreground mt-2">当插件更新token时如果该token被禁用则自动启用它</p>
</div>
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
<p class="text-xs text-blue-800 dark:text-blue-200">
<strong>使用说明:</strong>安装Chrome扩展后将连接接口和Token配置到插件中插件会自动提取Google Labs的cookie并更新到系统
@@ -392,11 +399,19 @@
<div class="rounded-lg border border-border bg-background">
<div class="flex items-center justify-between gap-4 p-4 border-b border-border">
<h3 class="text-lg font-semibold">请求日志</h3>
<button onclick="refreshLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-accent h-8 w-8" title="刷新">
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
</button>
<div class="flex gap-2">
<button onclick="clearAllLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-red-50 hover:text-red-700 h-8 px-3 text-sm" title="清空日志">
<svg class="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
清空
</button>
<button onclick="refreshLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-accent h-8 w-8" title="刷新">
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
</button>
</div>
</div>
<div class="relative w-full overflow-auto max-h-[600px]">
<table class="w-full text-sm">
@@ -407,6 +422,7 @@
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">状态码</th>
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">耗时(秒)</th>
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">时间</th>
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">详情</th>
</tr>
</thead>
<tbody id="logsTableBody" class="divide-y divide-border">
@@ -417,6 +433,26 @@
</div>
</div>
<!-- 日志详情模态框 -->
<div id="logDetailModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
<div class="bg-background rounded-lg border border-border w-full max-w-3xl shadow-xl max-h-[80vh] flex flex-col">
<div class="flex items-center justify-between p-5 border-b border-border">
<h3 class="text-lg font-semibold">日志详情</h3>
<button onclick="closeLogDetailModal()" class="text-muted-foreground hover:text-foreground">
<svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="p-5 overflow-y-auto">
<div id="logDetailContent" class="space-y-4">
<!-- 动态填充 -->
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="mt-12 pt-6 border-t border-border text-center text-xs text-muted-foreground">
<p>© 2025 <a href="https://linux.do/u/thesmallhancat/summary" target="_blank" class="no-underline hover:underline" style="color: inherit;">TheSmallHanCat</a> && <a href="https://linux.do/u/tibbar/summary" target="_blank" class="no-underline hover:underline" style="color: inherit;">Tibbar</a>. All rights reserved.</p>
@@ -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=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td></tr>`).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=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).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+=`<div class="space-y-2"><h4 class="font-medium text-sm">生成结果</h4><div class="rounded-md border border-border p-3 bg-muted/30"><p class="text-sm mb-2"><span class="font-medium">文件URL:</span></p><a href="${responseBody.url}" target="_blank" class="text-blue-600 hover:underline text-xs break-all">${responseBody.url}</a></div></div>`}else if(responseBody.data&&responseBody.data.length>0){const item=responseBody.data[0];if(item.url){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">生成结果</h4><div class="rounded-md border border-border p-3 bg-muted/30"><p class="text-sm mb-2"><span class="font-medium">文件URL:</span></p><a href="${item.url}" target="_blank" class="text-blue-600 hover:underline text-xs break-all">${item.url}</a></div></div>`}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应信息</h4><p class="text-sm text-muted-foreground">无响应数据</p></div>`}}catch(e){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${log.response_body||'无'}</pre></div>`}}else{try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody&&responseBody.error){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误原因</h4><div class="rounded-md border border-red-200 p-3 bg-red-50"><p class="text-sm text-red-700">${responseBody.error.message||responseBody.error||'未知错误'}</p></div></div>`}else if(log.response_body&&log.response_body!=='{}'){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body}</pre></div>`}}catch(e){if(log.response_body&&log.response_body!=='{}'){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body}</pre></div>`}}}detailHtml+=`<div class="space-y-2 pt-4 border-t border-border"><h4 class="font-medium text-sm">基本信息</h4><div class="grid grid-cols-2 gap-2 text-sm"><div><span class="text-muted-foreground">操作:</span> ${log.operation}</div><div><span class="text-muted-foreground">状态码:</span> <span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${log.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${log.status_code}</span></div><div><span class="text-muted-foreground">耗时:</span> ${log.duration.toFixed(2)}秒</div><div><span class="text-muted-foreground">时间:</span> ${log.created_at?new Date(log.created_at).toLocaleString('zh-CN'):'-'}</div></div></div>`;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()}};