TheSmallHanCat commited on
Commit
d96fe18
·
1 Parent(s): 35d960d

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

Browse files
README.md CHANGED
@@ -33,6 +33,8 @@
33
  - 由于Flow增加了额外的验证码,你可以自行选择使用浏览器打码或第三发打码:
34
  注册[YesCaptcha](https://yescaptcha.com/i/13Xd8K)并获取api key,将其填入系统配置页面```YesCaptcha API密钥```区域
35
 
 
 
36
  ### 方式一:Docker 部署(推荐)
37
 
38
  #### 标准模式(不使用代理)
 
33
  - 由于Flow增加了额外的验证码,你可以自行选择使用浏览器打码或第三发打码:
34
  注册[YesCaptcha](https://yescaptcha.com/i/13Xd8K)并获取api key,将其填入系统配置页面```YesCaptcha API密钥```区域
35
 
36
+ - 自动更新st浏览器拓展:[Flow2API-Token-Updater](https://github.com/TheSmallHanCat/Flow2API-Token-Updater)
37
+
38
  ### 方式一:Docker 部署(推荐)
39
 
40
  #### 标准模式(不使用代理)
src/api/admin.py CHANGED
@@ -1,11 +1,12 @@
1
  """Admin API routes"""
2
- from fastapi import APIRouter, Depends, HTTPException, Header
3
  from fastapi.responses import JSONResponse
4
  from pydantic import BaseModel
5
  from typing import Optional, List
6
  import secrets
7
  from ..core.auth import AuthManager
8
  from ..core.database import Database
 
9
  from ..services.token_manager import TokenManager
10
  from ..services.proxy_manager import ProxyManager
11
 
@@ -646,15 +647,25 @@ async def get_logs(
646
  "operation": log.get("operation"),
647
  "status_code": log.get("status_code"),
648
  "duration": log.get("duration"),
649
- "created_at": log.get("created_at")
 
 
650
  } for log in logs]
651
 
652
 
 
 
 
 
 
 
 
 
 
 
653
  @router.get("/api/admin/config")
654
  async def get_admin_config(token: str = Depends(verify_admin_token)):
655
  """Get admin configuration"""
656
- from ..core.config import config
657
-
658
  admin_config = await db.get_admin_config()
659
 
660
  return {
@@ -708,11 +719,9 @@ async def update_debug_config(
708
  ):
709
  """Update debug configuration"""
710
  try:
711
- # Update debug config in database
712
- await db.update_debug_config(enabled=request.enabled)
713
-
714
- # 🔥 Hot reload: sync database config to memory
715
- await db.reload_config_to_memory()
716
 
717
  status = "enabled" if request.enabled else "disabled"
718
  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)):
883
  # ========== Plugin Configuration Endpoints ==========
884
 
885
  @router.get("/api/plugin/config")
886
- async def get_plugin_config(token: str = Depends(verify_admin_token)):
887
  """Get plugin configuration"""
888
  plugin_config = await db.get_plugin_config()
889
 
890
- # Get server host and port from config
891
- from ..core.config import config
892
- server_host = config.server_host
893
- server_port = config.server_port
894
 
895
- # Generate connection URL
896
- if server_host == "0.0.0.0":
897
- connection_url = f"http://127.0.0.1:{server_port}/api/plugin/update-token"
 
898
  else:
899
- connection_url = f"http://{server_host}:{server_port}/api/plugin/update-token"
 
 
 
 
 
 
 
 
900
 
901
  return {
902
  "success": True,
903
  "config": {
904
  "connection_token": plugin_config.connection_token,
905
- "connection_url": connection_url
 
906
  }
907
  }
908
 
@@ -914,17 +932,22 @@ async def update_plugin_config(
914
  ):
915
  """Update plugin configuration"""
916
  connection_token = request.get("connection_token", "")
 
917
 
918
  # Generate random token if empty
919
  if not connection_token:
920
  connection_token = secrets.token_urlsafe(32)
921
 
922
- await db.update_plugin_config(connection_token=connection_token)
 
 
 
923
 
924
  return {
925
  "success": True,
926
  "message": "插件配置更新成功",
927
- "connection_token": connection_token
 
928
  }
929
 
930
 
@@ -989,6 +1012,16 @@ async def plugin_update_token(request: dict, authorization: Optional[str] = Head
989
  at_expires=at_expires
990
  )
991
 
 
 
 
 
 
 
 
 
 
 
992
  return {
993
  "success": True,
994
  "message": f"Token updated for {email}",
 
1
  """Admin API routes"""
2
+ from fastapi import APIRouter, Depends, HTTPException, Header, Request
3
  from fastapi.responses import JSONResponse
4
  from pydantic import BaseModel
5
  from typing import Optional, List
6
  import secrets
7
  from ..core.auth import AuthManager
8
  from ..core.database import Database
9
+ from ..core.config import config
10
  from ..services.token_manager import TokenManager
11
  from ..services.proxy_manager import ProxyManager
12
 
 
647
  "operation": log.get("operation"),
648
  "status_code": log.get("status_code"),
649
  "duration": log.get("duration"),
650
+ "created_at": log.get("created_at"),
651
+ "request_body": log.get("request_body"),
652
+ "response_body": log.get("response_body")
653
  } for log in logs]
654
 
655
 
656
+ @router.delete("/api/logs")
657
+ async def clear_logs(token: str = Depends(verify_admin_token)):
658
+ """Clear all logs"""
659
+ try:
660
+ await db.clear_all_logs()
661
+ return {"success": True, "message": "所有日志已清空"}
662
+ except Exception as e:
663
+ raise HTTPException(status_code=500, detail=str(e))
664
+
665
+
666
  @router.get("/api/admin/config")
667
  async def get_admin_config(token: str = Depends(verify_admin_token)):
668
  """Get admin configuration"""
 
 
669
  admin_config = await db.get_admin_config()
670
 
671
  return {
 
719
  ):
720
  """Update debug configuration"""
721
  try:
722
+ # Update in-memory config only (not database)
723
+ # This ensures debug mode is automatically disabled on restart
724
+ config.set_debug_enabled(request.enabled)
 
 
725
 
726
  status = "enabled" if request.enabled else "disabled"
727
  return {"success": True, "message": f"Debug mode {status}", "enabled": request.enabled}
 
892
  # ========== Plugin Configuration Endpoints ==========
893
 
894
  @router.get("/api/plugin/config")
895
+ async def get_plugin_config(request: Request, token: str = Depends(verify_admin_token)):
896
  """Get plugin configuration"""
897
  plugin_config = await db.get_plugin_config()
898
 
899
+ # Get the actual domain and port from the request
900
+ # This allows the connection URL to reflect the user's actual access path
901
+ host_header = request.headers.get("host", "")
 
902
 
903
+ # Generate connection URL based on actual request
904
+ if host_header:
905
+ # Use the actual domain/IP and port from the request
906
+ connection_url = f"http://{host_header}/api/plugin/update-token"
907
  else:
908
+ # Fallback to config-based URL
909
+ from ..core.config import config
910
+ server_host = config.server_host
911
+ server_port = config.server_port
912
+
913
+ if server_host == "0.0.0.0":
914
+ connection_url = f"http://127.0.0.1:{server_port}/api/plugin/update-token"
915
+ else:
916
+ connection_url = f"http://{server_host}:{server_port}/api/plugin/update-token"
917
 
918
  return {
919
  "success": True,
920
  "config": {
921
  "connection_token": plugin_config.connection_token,
922
+ "connection_url": connection_url,
923
+ "auto_enable_on_update": plugin_config.auto_enable_on_update
924
  }
925
  }
926
 
 
932
  ):
933
  """Update plugin configuration"""
934
  connection_token = request.get("connection_token", "")
935
+ auto_enable_on_update = request.get("auto_enable_on_update", True) # 默认开启
936
 
937
  # Generate random token if empty
938
  if not connection_token:
939
  connection_token = secrets.token_urlsafe(32)
940
 
941
+ await db.update_plugin_config(
942
+ connection_token=connection_token,
943
+ auto_enable_on_update=auto_enable_on_update
944
+ )
945
 
946
  return {
947
  "success": True,
948
  "message": "插件配置更新成功",
949
+ "connection_token": connection_token,
950
+ "auto_enable_on_update": auto_enable_on_update
951
  }
952
 
953
 
 
1012
  at_expires=at_expires
1013
  )
1014
 
1015
+ # Check if auto-enable is enabled and token is disabled
1016
+ if plugin_config.auto_enable_on_update and not existing_token.is_active:
1017
+ await token_manager.enable_token(existing_token.id)
1018
+ return {
1019
+ "success": True,
1020
+ "message": f"Token updated and auto-enabled for {email}",
1021
+ "action": "updated",
1022
+ "auto_enabled": True
1023
+ }
1024
+
1025
  return {
1026
  "success": True,
1027
  "message": f"Token updated for {email}",
src/core/database.py CHANGED
@@ -305,6 +305,20 @@ class Database:
305
  except Exception as e:
306
  print(f" ✗ Failed to add column '{col_name}': {e}")
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  # ========== Step 3: Ensure all config tables have default rows ==========
309
  # Note: This will NOT overwrite existing config rows
310
  # It only ensures missing rows are created with default values from setting.toml
@@ -996,6 +1010,12 @@ class Database:
996
  rows = await cursor.fetchall()
997
  return [dict(row) for row in rows]
998
 
 
 
 
 
 
 
999
  async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True):
1000
  """
1001
  Initialize database configuration from setting.toml
@@ -1227,7 +1247,7 @@ class Database:
1227
  return PluginConfig(**dict(row))
1228
  return PluginConfig()
1229
 
1230
- async def update_plugin_config(self, connection_token: str):
1231
  """Update plugin configuration"""
1232
  async with aiosqlite.connect(self.db_path) as db:
1233
  db.row_factory = aiosqlite.Row
@@ -1237,13 +1257,13 @@ class Database:
1237
  if row:
1238
  await db.execute("""
1239
  UPDATE plugin_config
1240
- SET connection_token = ?, updated_at = CURRENT_TIMESTAMP
1241
  WHERE id = 1
1242
- """, (connection_token,))
1243
  else:
1244
  await db.execute("""
1245
- INSERT INTO plugin_config (id, connection_token)
1246
- VALUES (1, ?)
1247
- """, (connection_token,))
1248
 
1249
  await db.commit()
 
305
  except Exception as e:
306
  print(f" ✗ Failed to add column '{col_name}': {e}")
307
 
308
+ # Check and add missing columns to plugin_config table
309
+ if await self._table_exists(db, "plugin_config"):
310
+ plugin_columns_to_add = [
311
+ ("auto_enable_on_update", "BOOLEAN DEFAULT 1"), # 默认开启
312
+ ]
313
+
314
+ for col_name, col_type in plugin_columns_to_add:
315
+ if not await self._column_exists(db, "plugin_config", col_name):
316
+ try:
317
+ await db.execute(f"ALTER TABLE plugin_config ADD COLUMN {col_name} {col_type}")
318
+ print(f" ✓ Added column '{col_name}' to plugin_config table")
319
+ except Exception as e:
320
+ print(f" ✗ Failed to add column '{col_name}': {e}")
321
+
322
  # ========== Step 3: Ensure all config tables have default rows ==========
323
  # Note: This will NOT overwrite existing config rows
324
  # It only ensures missing rows are created with default values from setting.toml
 
1010
  rows = await cursor.fetchall()
1011
  return [dict(row) for row in rows]
1012
 
1013
+ async def clear_all_logs(self):
1014
+ """Clear all request logs"""
1015
+ async with aiosqlite.connect(self.db_path) as db:
1016
+ await db.execute("DELETE FROM request_logs")
1017
+ await db.commit()
1018
+
1019
  async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True):
1020
  """
1021
  Initialize database configuration from setting.toml
 
1247
  return PluginConfig(**dict(row))
1248
  return PluginConfig()
1249
 
1250
+ async def update_plugin_config(self, connection_token: str, auto_enable_on_update: bool = True):
1251
  """Update plugin configuration"""
1252
  async with aiosqlite.connect(self.db_path) as db:
1253
  db.row_factory = aiosqlite.Row
 
1257
  if row:
1258
  await db.execute("""
1259
  UPDATE plugin_config
1260
+ SET connection_token = ?, auto_enable_on_update = ?, updated_at = CURRENT_TIMESTAMP
1261
  WHERE id = 1
1262
+ """, (connection_token, auto_enable_on_update))
1263
  else:
1264
  await db.execute("""
1265
+ INSERT INTO plugin_config (id, connection_token, auto_enable_on_update)
1266
+ VALUES (1, ?, ?)
1267
+ """, (connection_token, auto_enable_on_update))
1268
 
1269
  await db.commit()
src/core/models.py CHANGED
@@ -162,6 +162,7 @@ class PluginConfig(BaseModel):
162
  """Plugin connection configuration"""
163
  id: int = 1
164
  connection_token: str = "" # 插件连接token
 
165
  created_at: Optional[datetime] = None
166
  updated_at: Optional[datetime] = None
167
 
 
162
  """Plugin connection configuration"""
163
  id: int = 1
164
  connection_token: str = "" # 插件连接token
165
+ auto_enable_on_update: bool = True # 更新token时自动启用(默认开启)
166
  created_at: Optional[datetime] = None
167
  updated_at: Optional[datetime] = None
168
 
src/services/generation_handler.py CHANGED
@@ -102,6 +102,33 @@ MODEL_CONFIG = {
102
  "supports_images": False
103
  },
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  # ========== 首尾帧模型 (I2V - Image to Video) ==========
106
  # 支持1-2张图片:1张作为首帧,2张作为首尾帧
107
 
@@ -165,6 +192,66 @@ MODEL_CONFIG = {
165
  "max_images": 2
166
  },
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  # ========== 多图生成 (R2V - Reference Images to Video) ==========
169
  # 支持多张图片,不限制数量
170
 
@@ -186,6 +273,46 @@ MODEL_CONFIG = {
186
  "supports_images": True,
187
  "min_images": 0,
188
  "max_images": None # 不限制
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  }
190
  }
191
 
@@ -343,11 +470,25 @@ class GenerationHandler:
343
 
344
  # 7. 记录成功日志
345
  duration = time.time() - start_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  await self._log_request(
347
  token.id,
348
  f"generate_{generation_type}",
349
  {"model": model, "prompt": prompt[:100], "has_images": images is not None and len(images) > 0},
350
- {"status": "success"},
351
  200,
352
  duration
353
  )
@@ -358,12 +499,8 @@ class GenerationHandler:
358
  if stream:
359
  yield self._create_stream_chunk(f"❌ {error_msg}\n")
360
  if token:
361
- # 检测429错误,立即禁用token
362
- if "429" in str(e) or "HTTP Error 429" in str(e):
363
- debug_logger.log_warning(f"[429_BAN] Token {token.id} 遇到429错误,立即禁用")
364
- await self.token_manager.ban_token_for_429(token.id)
365
- else:
366
- await self.token_manager.record_error(token.id)
367
  yield self._create_error_response(error_msg)
368
 
369
  # 记录失败日志
@@ -464,6 +601,9 @@ class GenerationHandler:
464
  yield self._create_stream_chunk("缓存已关闭,正在返回源链接...\n")
465
 
466
  # 返回结果
 
 
 
467
  if stream:
468
  yield self._create_stream_chunk(
469
  f"![Generated Image]({local_url})",
@@ -732,6 +872,9 @@ class GenerationHandler:
732
  completed_at=time.time()
733
  )
734
 
 
 
 
735
  # 返回结果
736
  if stream:
737
  yield self._create_stream_chunk(
 
102
  "supports_images": False
103
  },
104
 
105
+ # veo_3_1_t2v_fast_portrait_ultra (竖屏)
106
+ "veo_3_1_t2v_fast_portrait_ultra": {
107
+ "type": "video",
108
+ "video_type": "t2v",
109
+ "model_key": "veo_3_1_t2v_fast_portrait_ultra",
110
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
111
+ "supports_images": False
112
+ },
113
+
114
+ # veo_3_1_t2v_fast_portrait_ultra_relaxed (竖屏)
115
+ "veo_3_1_t2v_fast_portrait_ultra_relaxed": {
116
+ "type": "video",
117
+ "video_type": "t2v",
118
+ "model_key": "veo_3_1_t2v_fast_portrait_ultra_relaxed",
119
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
120
+ "supports_images": False
121
+ },
122
+
123
+ # veo_3_1_t2v_portrait (竖屏)
124
+ "veo_3_1_t2v_portrait": {
125
+ "type": "video",
126
+ "video_type": "t2v",
127
+ "model_key": "veo_3_1_t2v_portrait",
128
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
129
+ "supports_images": False
130
+ },
131
+
132
  # ========== 首尾帧模型 (I2V - Image to Video) ==========
133
  # 支持1-2张图片:1张作为首帧,2张作为首尾帧
134
 
 
192
  "max_images": 2
193
  },
194
 
195
+ # veo_3_1_i2v_s_fast_ultra (需要新增横竖屏)
196
+ "veo_3_1_i2v_s_fast_ultra_portrait": {
197
+ "type": "video",
198
+ "video_type": "i2v",
199
+ "model_key": "veo_3_1_i2v_s_fast_ultra",
200
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
201
+ "supports_images": True,
202
+ "min_images": 1,
203
+ "max_images": 2
204
+ },
205
+ "veo_3_1_i2v_s_fast_ultra_landscape": {
206
+ "type": "video",
207
+ "video_type": "i2v",
208
+ "model_key": "veo_3_1_i2v_s_fast_ultra",
209
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
210
+ "supports_images": True,
211
+ "min_images": 1,
212
+ "max_images": 2
213
+ },
214
+
215
+ # veo_3_1_i2v_s_fast_ultra_relaxed (需要新增横竖屏)
216
+ "veo_3_1_i2v_s_fast_ultra_relaxed_portrait": {
217
+ "type": "video",
218
+ "video_type": "i2v",
219
+ "model_key": "veo_3_1_i2v_s_fast_ultra_relaxed",
220
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
221
+ "supports_images": True,
222
+ "min_images": 1,
223
+ "max_images": 2
224
+ },
225
+ "veo_3_1_i2v_s_fast_ultra_relaxed_landscape": {
226
+ "type": "video",
227
+ "video_type": "i2v",
228
+ "model_key": "veo_3_1_i2v_s_fast_ultra_relaxed",
229
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
230
+ "supports_images": True,
231
+ "min_images": 1,
232
+ "max_images": 2
233
+ },
234
+
235
+ # veo_3_1_i2v_s (需要新增横竖屏)
236
+ "veo_3_1_i2v_s_portrait": {
237
+ "type": "video",
238
+ "video_type": "i2v",
239
+ "model_key": "veo_3_1_i2v_s",
240
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
241
+ "supports_images": True,
242
+ "min_images": 1,
243
+ "max_images": 2
244
+ },
245
+ "veo_3_1_i2v_s_landscape": {
246
+ "type": "video",
247
+ "video_type": "i2v",
248
+ "model_key": "veo_3_1_i2v_s",
249
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
250
+ "supports_images": True,
251
+ "min_images": 1,
252
+ "max_images": 2
253
+ },
254
+
255
  # ========== 多图生成 (R2V - Reference Images to Video) ==========
256
  # 支持多张图片,不限制数量
257
 
 
273
  "supports_images": True,
274
  "min_images": 0,
275
  "max_images": None # 不限制
276
+ },
277
+
278
+ # veo_3_0_r2v_fast_ultra (需要新增横竖屏)
279
+ "veo_3_0_r2v_fast_ultra_portrait": {
280
+ "type": "video",
281
+ "video_type": "r2v",
282
+ "model_key": "veo_3_0_r2v_fast_ultra",
283
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
284
+ "supports_images": True,
285
+ "min_images": 0,
286
+ "max_images": None # 不限制
287
+ },
288
+ "veo_3_0_r2v_fast_ultra_landscape": {
289
+ "type": "video",
290
+ "video_type": "r2v",
291
+ "model_key": "veo_3_0_r2v_fast_ultra",
292
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
293
+ "supports_images": True,
294
+ "min_images": 0,
295
+ "max_images": None # 不限制
296
+ },
297
+
298
+ # veo_3_0_r2v_fast_ultra_relaxed (需要新增横竖屏)
299
+ "veo_3_0_r2v_fast_ultra_relaxed_portrait": {
300
+ "type": "video",
301
+ "video_type": "r2v",
302
+ "model_key": "veo_3_0_r2v_fast_ultra_relaxed",
303
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
304
+ "supports_images": True,
305
+ "min_images": 0,
306
+ "max_images": None # 不限制
307
+ },
308
+ "veo_3_0_r2v_fast_ultra_relaxed_landscape": {
309
+ "type": "video",
310
+ "video_type": "r2v",
311
+ "model_key": "veo_3_0_r2v_fast_ultra_relaxed",
312
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
313
+ "supports_images": True,
314
+ "min_images": 0,
315
+ "max_images": None # 不限制
316
  }
317
  }
318
 
 
470
 
471
  # 7. 记录成功日志
472
  duration = time.time() - start_time
473
+
474
+ # 构建响应数据,包含生成的URL
475
+ response_data = {
476
+ "status": "success",
477
+ "model": model,
478
+ "prompt": prompt[:100]
479
+ }
480
+
481
+ # 添加生成的URL(如果有)
482
+ if hasattr(self, '_last_generated_url') and self._last_generated_url:
483
+ response_data["url"] = self._last_generated_url
484
+ # 清除临时存储
485
+ self._last_generated_url = None
486
+
487
  await self._log_request(
488
  token.id,
489
  f"generate_{generation_type}",
490
  {"model": model, "prompt": prompt[:100], "has_images": images is not None and len(images) > 0},
491
+ response_data,
492
  200,
493
  duration
494
  )
 
499
  if stream:
500
  yield self._create_stream_chunk(f"❌ {error_msg}\n")
501
  if token:
502
+ # 记录错误(所有错误统一处理不再特殊处理429)
503
+ await self.token_manager.record_error(token.id)
 
 
 
 
504
  yield self._create_error_response(error_msg)
505
 
506
  # 记录失败日志
 
601
  yield self._create_stream_chunk("缓存已关闭,正在返回源链接...\n")
602
 
603
  # 返回结果
604
+ # 存储URL用于日志记录
605
+ self._last_generated_url = local_url
606
+
607
  if stream:
608
  yield self._create_stream_chunk(
609
  f"![Generated Image]({local_url})",
 
872
  completed_at=time.time()
873
  )
874
 
875
+ # 存储URL用于日志记录
876
+ self._last_generated_url = local_url
877
+
878
  # 返回结果
879
  if stream:
880
  yield self._create_stream_chunk(
static/manage.html CHANGED
@@ -339,6 +339,13 @@
339
  </div>
340
  <p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
341
  </div>
 
 
 
 
 
 
 
342
  <div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
343
  <p class="text-xs text-blue-800 dark:text-blue-200">
344
  ℹ️ <strong>使用说明:</strong>安装Chrome扩展后,将连接接口和Token配置到插件中,插件会自动提取Google Labs的cookie并更新到系统
@@ -392,11 +399,19 @@
392
  <div class="rounded-lg border border-border bg-background">
393
  <div class="flex items-center justify-between gap-4 p-4 border-b border-border">
394
  <h3 class="text-lg font-semibold">请求日志</h3>
395
- <button onclick="refreshLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-accent h-8 w-8" title="刷新">
396
- <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">
397
- <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"/>
398
- </svg>
399
- </button>
 
 
 
 
 
 
 
 
400
  </div>
401
  <div class="relative w-full overflow-auto max-h-[600px]">
402
  <table class="w-full text-sm">
@@ -407,6 +422,7 @@
407
  <th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">状态码</th>
408
  <th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">耗时(秒)</th>
409
  <th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">时间</th>
 
410
  </tr>
411
  </thead>
412
  <tbody id="logsTableBody" class="divide-y divide-border">
@@ -417,6 +433,26 @@
417
  </div>
418
  </div>
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  <!-- 页脚 -->
421
  <footer class="mt-12 pt-6 border-t border-border text-center text-xs text-muted-foreground">
422
  <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 @@
666
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
667
  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')}},
668
  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')}},
669
- 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')}},
670
- 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')}},
671
  copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
672
  copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
673
  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}},
674
  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)}},
675
- 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)}},
676
  refreshLogs=async()=>{await loadLogs()},
 
 
 
677
  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)},
678
  logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
679
  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()}};
 
339
  </div>
340
  <p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
341
  </div>
342
+ <div>
343
+ <label class="inline-flex items-center gap-2 cursor-pointer">
344
+ <input type="checkbox" id="cfgAutoEnableOnUpdate" class="h-4 w-4 rounded border-input">
345
+ <span class="text-sm font-medium">更新token时自动启用</span>
346
+ </label>
347
+ <p class="text-xs text-muted-foreground mt-2">当插件更新token时,如果该token被禁用,则自动启用它</p>
348
+ </div>
349
  <div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
350
  <p class="text-xs text-blue-800 dark:text-blue-200">
351
  ℹ️ <strong>使用说明:</strong>安装Chrome扩展后,将连接接口和Token配置到插件中,插件会自动提取Google Labs的cookie并更新到系统
 
399
  <div class="rounded-lg border border-border bg-background">
400
  <div class="flex items-center justify-between gap-4 p-4 border-b border-border">
401
  <h3 class="text-lg font-semibold">请求日志</h3>
402
+ <div class="flex gap-2">
403
+ <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="清空日志">
404
+ <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">
405
+ <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"/>
406
+ </svg>
407
+ 清空
408
+ </button>
409
+ <button onclick="refreshLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-accent h-8 w-8" title="刷新">
410
+ <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">
411
+ <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"/>
412
+ </svg>
413
+ </button>
414
+ </div>
415
  </div>
416
  <div class="relative w-full overflow-auto max-h-[600px]">
417
  <table class="w-full text-sm">
 
422
  <th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">状态码</th>
423
  <th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">耗时(秒)</th>
424
  <th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">时间</th>
425
+ <th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">详情</th>
426
  </tr>
427
  </thead>
428
  <tbody id="logsTableBody" class="divide-y divide-border">
 
433
  </div>
434
  </div>
435
 
436
+ <!-- 日志详情模态框 -->
437
+ <div id="logDetailModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
438
+ <div class="bg-background rounded-lg border border-border w-full max-w-3xl shadow-xl max-h-[80vh] flex flex-col">
439
+ <div class="flex items-center justify-between p-5 border-b border-border">
440
+ <h3 class="text-lg font-semibold">日志详情</h3>
441
+ <button onclick="closeLogDetailModal()" class="text-muted-foreground hover:text-foreground">
442
+ <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">
443
+ <line x1="18" y1="6" x2="6" y2="18"/>
444
+ <line x1="6" y1="6" x2="18" y2="18"/>
445
+ </svg>
446
+ </button>
447
+ </div>
448
+ <div class="p-5 overflow-y-auto">
449
+ <div id="logDetailContent" class="space-y-4">
450
+ <!-- 动态填充 -->
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+
456
  <!-- 页脚 -->
457
  <footer class="mt-12 pt-6 border-t border-border text-center text-xs text-muted-foreground">
458
  <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>
 
702
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
703
  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')}},
704
  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')}},
705
+ 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')}},
706
+ 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')}},
707
  copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
708
  copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
709
  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}},
710
  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)}},
711
+ 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)}},
712
  refreshLogs=async()=>{await loadLogs()},
713
+ 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')}},
714
+ 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')},
715
+ closeLogDetailModal=()=>{$('logDetailModal').classList.add('hidden')},
716
  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)},
717
  logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
718
  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()}};