xiaoyukkkk commited on
Commit
326836f
·
verified ·
1 Parent(s): 4f80088

Upload 8 files

Browse files
Files changed (2) hide show
  1. core/account.py +12 -24
  2. core/templates.py +921 -89
core/account.py CHANGED
@@ -311,37 +311,25 @@ def save_accounts_to_file(accounts_data: list):
311
 
312
 
313
  def load_accounts_from_source() -> list:
314
- """优先从文件加载,从环境变量加载"""
315
- # 优先从文件加载
316
  if os.path.exists(ACCOUNTS_FILE):
317
  try:
318
  with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
319
  accounts_data = json.load(f)
320
- logger.info(f"[CONFIG] 从文件加载配置: {ACCOUNTS_FILE}")
 
 
 
321
  return accounts_data
322
  except Exception as e:
323
- logger.warning(f"[CONFIG] 文件加载失败,尝试环境变量: {str(e)}")
324
-
325
- # 从环境变量加载
326
- accounts_json = os.getenv("ACCOUNTS_CONFIG")
327
- if not accounts_json:
328
- raise ValueError(
329
- "未找到配置文件或 ACCOUNTS_CONFIG 环境变量。\n"
330
- "请在环境变量中配置 JSON 格式的账户列表,格式示例:\n"
331
- '[{"id":"account_1","csesidx":"xxx","config_id":"yyy","secure_c_ses":"zzz","host_c_oses":null,"expires_at":"2025-12-23 10:59:21"}]'
332
- )
333
 
334
- try:
335
- accounts_data = json.loads(accounts_json)
336
- if not isinstance(accounts_data, list):
337
- raise ValueError("ACCOUNTS_CONFIG 必须是 JSON 数组格式")
338
- # 首次从环境变量加载后,保存到文件
339
- save_accounts_to_file(accounts_data)
340
- logger.info(f"[CONFIG] 从环境变量加载配置并保存到文件")
341
- return accounts_data
342
- except json.JSONDecodeError as e:
343
- logger.error(f"[CONFIG] ACCOUNTS_CONFIG JSON 解析失败: {str(e)}")
344
- raise ValueError(f"ACCOUNTS_CONFIG 格式错误: {str(e)}")
345
 
346
 
347
  def get_account_id(acc: dict, index: int) -> str:
 
311
 
312
 
313
  def load_accounts_from_source() -> list:
314
+ """从文件加载账户配置文件不存在创建空配置"""
315
+ # 从文件加载
316
  if os.path.exists(ACCOUNTS_FILE):
317
  try:
318
  with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
319
  accounts_data = json.load(f)
320
+ if accounts_data:
321
+ logger.info(f"[CONFIG] 从文件加载配置: {ACCOUNTS_FILE},共 {len(accounts_data)} 个账户")
322
+ else:
323
+ logger.warning(f"[CONFIG] 账户配置为空,请在管理面板添加账户或编辑 {ACCOUNTS_FILE}")
324
  return accounts_data
325
  except Exception as e:
326
+ logger.warning(f"[CONFIG] 文件加载失败: {str(e)},创建空配置")
 
 
 
 
 
 
 
 
 
327
 
328
+ # 文件不存在,创建空配置
329
+ logger.warning(f"[CONFIG] 未找到 {ACCOUNTS_FILE},已创建空配置文件")
330
+ logger.info(f"[CONFIG] 💡 请在管理面板添加账户,或直接编辑 {ACCOUNTS_FILE},或使用批量上传功能")
331
+ save_accounts_to_file([])
332
+ return []
 
 
 
 
 
 
333
 
334
 
335
  def get_account_id(acc: dict, index: int) -> str:
core/templates.py CHANGED
@@ -103,15 +103,13 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
103
  <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-2.5-flash</span>
104
  <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-2.5-pro</span>
105
  <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-3-flash-preview</span>
106
- <span style="background: #eef7ff; color: #0071e3; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; border: 1px solid #dcebfb; font-weight: 500;">gemini-3-pro-preview</span>
107
  </div>
108
  </div>
109
  <div style="background: rgba(0,0,0,0.03); padding: 10px; border-radius: 6px;">
110
- <div style="font-size: 11px; color: #1d1d1f; margin-bottom: 4px; font-weight: 600;">📸 图片生成说明</div>
111
  <div style="font-size: 11px; color: #86868b; line-height: 1.6;">
112
- • 仅 <code style="font-size: 10px; background: rgba(0,0,0,0.08); padding: 1px 4px; border-radius: 3px;">gemini-3-pro-preview</code> 支持绘图<br>
113
- • 存储路径:<code style="font-size: 10px; background: rgba(0,0,0,0.08); padding: 1px 4px; border-radius: 3px;">./images</code><br>
114
- • 存储类型:临时(服务重启后丢失)
115
  </div>
116
  </div>
117
  </div>
@@ -435,6 +433,11 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
435
  border-bottom: 2px solid transparent;
436
  position: relative;
437
  top: 1px;
 
 
 
 
 
438
  }}
439
  .tab-button:hover {{
440
  color: #1d1d1f;
@@ -561,7 +564,7 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
561
  width: 100%;
562
  flex: 1;
563
  min-height: 300px;
564
- font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
565
  font-size: 13px;
566
  padding: 16px;
567
  border: 1px solid #e5e5e5;
@@ -569,6 +572,7 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
569
  background: #fafaf9;
570
  color: #1a1a1a;
571
  line-height: 1.6;
 
572
  overflow-y: auto;
573
  resize: none;
574
  scrollbar-width: thin;
@@ -611,20 +615,62 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
611
  }}
612
  .btn-secondary:hover {{ background: #e5e5e5; }}
613
 
614
- .env-var {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }}
 
 
 
 
 
 
615
  .env-var:last-child {{ border-bottom: none; }}
616
- .env-name {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: var(--text-main); font-weight: 600; }}
617
- .env-desc {{ font-size: 11px; color: var(--text-sec); margin-top: 2px; }}
618
- .env-value {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: var(--text-sec); text-align: right; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
  .badge {{ display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; vertical-align: middle; margin-left: 6px; }}
621
  .badge-required {{ background: #ffebeb; color: #c62828; }}
622
  .badge-optional {{ background: #e8f5e9; color: #2e7d32; }}
623
 
624
- code {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; background: #f5f5f7; padding: 2px 6px; border-radius: 4px; font-size: 12px; color: var(--blue); }}
 
 
 
 
 
 
 
 
 
625
  a {{ color: var(--blue); text-decoration: none; }}
626
  a:hover {{ text-decoration: underline; }}
627
- .font-mono {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; }}
 
 
 
 
628
 
629
  /* --- Service Info Styles --- */
630
  .model-grid {{ display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }}
@@ -634,8 +680,10 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
634
  padding: 4px 10px;
635
  border-radius: 6px;
636
  font-size: 12px;
637
- font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
638
  border: 1px solid transparent;
 
 
639
  }}
640
  .model-tag.highlight {{ background: #eef7ff; color: #0071e3; border-color: #dcebfb; font-weight: 500; }}
641
 
@@ -643,6 +691,37 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
643
  .info-box-title {{ font-weight: 600; font-size: 12px; color: #1d1d1f; margin-bottom: 6px; }}
644
  .info-box-text {{ font-size: 12px; color: #86868b; line-height: 1.5; }}
645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  .ep-table {{
647
  width: 100%;
648
  border-collapse: collapse;
@@ -671,7 +750,14 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
671
  .m-get {{ background: #eff6ff; color: #1e40af; border: 1px solid #dbeafe; }}
672
  .m-del {{ background: #fef2f2; color: #991b1b; border: 1px solid #fee2e2; }}
673
 
674
- .ep-path {{ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; color: #1d1d1f; margin-right: 8px; font-size: 12px; }}
 
 
 
 
 
 
 
675
  .ep-desc {{ color: #86868b; font-size: 12px; margin-left: auto; }}
676
 
677
  .current-url-row {{
@@ -704,6 +790,7 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
704
  min-width: 100px;
705
  padding: 10px 16px;
706
  font-size: 13px;
 
707
  }}
708
 
709
  /* Account Table Mobile - Card Layout */
@@ -826,6 +913,7 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
826
  <button class="tab-button active" onclick="switchTab('accounts')">📋 账户管理</button>
827
  <button class="tab-button" onclick="switchTab('api')">📚 API文档</button>
828
  <button class="tab-button" onclick="switchTab('config')">⚙️ 系统配置</button>
 
829
  </div>
830
 
831
  <!-- Tab 1: 账户管理 -->
@@ -936,110 +1024,209 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
936
  <!-- Tab 3: 系统配置 -->
937
  <div id="tab-config" class="tab-content">
938
  <div class="section">
939
- <div class="section-title">环境变量配置</div>
940
  <div class="grid-env">
941
  <div class="stack-col">
942
  <div class="card">
943
- <h3>必需变量 <span class="badge badge-required">REQUIRED</span></h3>
944
  <div style="margin-top: 12px;">
945
  <div class="env-var">
946
- <div><div class="env-name">ACCOUNTS_CONFIG</div><div class="env-desc">JSON格式账户列表</div></div>
 
947
  </div>
948
  <div class="env-var">
949
  <div><div class="env-name">PATH_PREFIX</div><div class="env-desc">API路径前缀</div></div>
950
- <div class="env-value">当前: {main.PATH_PREFIX}</div>
951
  </div>
 
 
 
 
 
 
952
  <div class="env-var">
953
- <div><div class="env-name">ADMIN_KEY</div><div class="env-desc">管理员密钥</div></div>
954
- <div class="env-value">已设置</div>
 
 
 
 
 
 
 
 
955
  </div>
956
  </div>
957
  </div>
 
958
 
959
  <div class="card">
960
- <h3>重试配置 <span class="badge badge-optional">OPTIONAL</span></h3>
961
  <div style="margin-top: 12px;">
962
  <div class="env-var">
963
- <div><div class="env-name">MAX_NEW_SESSION_TRIES</div><div class="env-desc">新会话尝试账户数</div></div>
964
  <div class="env-value">{main.MAX_NEW_SESSION_TRIES}</div>
965
  </div>
966
  <div class="env-var">
967
- <div><div class="env-name">MAX_REQUEST_RETRIES</div><div class="env-desc">请求失败重试次数</div></div>
968
  <div class="env-value">{main.MAX_REQUEST_RETRIES}</div>
969
  </div>
970
  <div class="env-var">
971
- <div><div class="env-name">MAX_ACCOUNT_SWITCH_TRIES</div><div class="env-desc">每次重试查找账户次数</div></div>
972
  <div class="env-value">{main.MAX_ACCOUNT_SWITCH_TRIES}</div>
973
  </div>
974
  <div class="env-var">
975
- <div><div class="env-name">ACCOUNT_FAILURE_THRESHOLD</div><div class="env-desc">账户失败阈值</div></div>
976
  <div class="env-value">{main.ACCOUNT_FAILURE_THRESHOLD} 次</div>
977
  </div>
978
  <div class="env-var">
979
- <div><div class="env-name">RATE_LIMIT_COOLDOWN_SECONDS</div><div class="env-desc">429限流冷却时间</div></div>
980
  <div class="env-value">{main.RATE_LIMIT_COOLDOWN_SECONDS} 秒</div>
981
  </div>
 
 
 
 
982
  </div>
983
  </div>
984
- </div>
985
 
986
  <div class="card">
987
- <h3>可选变量 <span class="badge badge-optional">OPTIONAL</span></h3>
988
  <div style="margin-top: 12px;">
989
  <div class="env-var">
990
- <div><div class="env-name">API_KEY</div><div class="env-desc">API访问密钥</div></div>
991
- <div class="env-value">{'已设置' if main.API_KEY else '未设置'}</div>
992
- </div>
993
- <div class="env-var">
994
- <div><div class="env-name">BASE_URL</div><div class="env-desc">图片URL生成(推荐设置)</div></div>
995
- <div class="env-value">{'已设置' if main.BASE_URL else '未设置(自动检测)'}</div>
996
- </div>
997
- <div class="env-var">
998
- <div><div class="env-name">PROXY</div><div class="env-desc">代理地址</div></div>
999
- <div class="env-value">{'已设置' if main.PROXY else '未设置'}</div>
1000
- </div>
1001
- <div class="env-var">
1002
- <div><div class="env-name">SESSION_CACHE_TTL_SECONDS</div><div class="env-desc">会话缓存过期时间</div></div>
1003
- <div class="env-value">{main.SESSION_CACHE_TTL_SECONDS} 秒</div>
1004
- </div>
1005
- <div class="env-var">
1006
- <div><div class="env-name">LOGO_URL</div><div class="env-desc">Logo URL(公开,为空则不显示)</div></div>
1007
  <div class="env-value">{'已设置' if main.LOGO_URL else '未设置'}</div>
1008
  </div>
1009
  <div class="env-var">
1010
- <div><div class="env-name">CHAT_URL</div><div class="env-desc">开始对话链接(公开,为空则不显示)</div></div>
1011
  <div class="env-value">{'已设置' if main.CHAT_URL else '未设置'}</div>
1012
  </div>
1013
- <div class="env-var">
1014
- <div><div class="env-name">MODEL_NAME</div><div class="env-desc">模型名称(公开)</div></div>
1015
- <div class="env-value">{main.MODEL_NAME}</div>
1016
- </div>
1017
  </div>
1018
  </div>
1019
  </div>
1020
  </div>
1021
 
 
 
1022
  <div class="section">
1023
- <div class="section-title">服务信息</div>
1024
- <div class="card">
1025
- <h3>支持的模型</h3>
1026
- <div class="model-grid">
1027
- <span class="model-tag">gemini-auto</span>
1028
- <span class="model-tag">gemini-2.5-flash</span>
1029
- <span class="model-tag">gemini-2.5-pro</span>
1030
- <span class="model-tag">gemini-3-flash-preview</span>
1031
- <span class="model-tag highlight">gemini-3-pro-preview</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032
  </div>
1033
 
1034
- <div class="info-box">
1035
- <div class="info-box-title">📸 图片生成说明</div>
1036
- <div class="info-box-text">
1037
- <code style="background:none;padding:0;color:#0071e3;">gemini-3-pro-preview</code> 支持绘图。<br>
1038
- 路径: <code>{main.IMAGE_DIR}</code><br>
1039
- 类型: {'<span style="color: #34c759; font-weight: 600;">持久化(重启保留)</span>' if main.IMAGE_DIR == '/data/images' else '<span style="color: #ff3b30; font-weight: 600;">临时(重启丢失)</span>'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1040
  </div>
1041
  </div>
1042
  </div>
 
 
 
 
 
1043
  </div>
1044
  </div>
1045
  </div>
@@ -1304,6 +1491,98 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
1304
  closeModal();
1305
  }}
1306
  }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1307
  </script>
1308
  </body>
1309
  </html>
@@ -2179,7 +2458,6 @@ async def get_login_html(request: Request, error: str = None) -> HTMLResponse:
2179
  async def admin_logs_html_no_auth(request):
2180
  """返回美化的 HTML 日志查看界面(无需认证)"""
2181
  from fastapi.responses import HTMLResponse
2182
-
2183
  html_content = r"""
2184
  <!DOCTYPE html>
2185
  <html>
@@ -2189,54 +2467,608 @@ async def admin_logs_html_no_auth(request):
2189
  <title>日志查看器</title>
2190
  <style>
2191
  * { margin: 0; padding: 0; box-sizing: border-box; }
 
2192
  body {
2193
- font-family: "Consolas", "Monaco", monospace;
2194
  background: #fafaf9;
2195
- padding: 20px;
 
 
 
2196
  }
2197
  .container {
 
2198
  max-width: 1400px;
2199
- margin: 0 auto;
2200
  background: white;
2201
  border-radius: 16px;
2202
  padding: 30px;
2203
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2204
  }
2205
- h1 { color: #1a1a1a; font-size: 22px; margin-bottom: 20px; }
2206
- .log-item { padding: 10px; border-bottom: 1px solid #e5e5e5; font-size: 12px; }
2207
- .log-time { color: #6b6b6b; margin-right: 10px; }
2208
- .log-level { padding: 2px 6px; border-radius: 4px; margin-right: 10px; }
2209
  .log-level.INFO { background: #e3f2fd; color: #1976d2; }
 
2210
  .log-level.WARNING { background: #fff3e0; color: #f57c00; }
2211
- .log-level.ERROR { background: #ffebee; color: #c62828; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2212
  </style>
2213
  </head>
2214
  <body>
2215
  <div class="container">
2216
- <h1>📋 系统日志</h1>
2217
- <div id="logs">加载中...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2218
  </div>
2219
  <script>
 
2220
  async function loadLogs() {
 
 
 
 
 
 
 
 
 
 
 
2221
  try {
2222
- const path = window.location.pathname.replace("/log/html", "/log");
2223
- const res = await fetch(path);
2224
- const data = await res.json();
2225
- let html = "";
2226
- for (const log of data.logs.reverse()) {
2227
- html += `<div class="log-item">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2228
  <span class="log-time">${log.time}</span>
2229
  <span class="log-level ${log.level}">${log.level}</span>
2230
- <span>${log.message}</span>
2231
- </div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2232
  }
2233
- document.getElementById("logs").innerHTML = html || "暂无日志";
2234
- } catch (e) {
2235
- document.getElementById("logs").innerHTML = "加载失败";
2236
  }
2237
  }
2238
- loadLogs();
2239
- setInterval(loadLogs, 5000);
 
 
 
 
 
 
 
2240
  </script>
2241
  </body>
2242
  </html>
 
103
  <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-2.5-flash</span>
104
  <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-2.5-pro</span>
105
  <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-3-flash-preview</span>
106
+ <span style="background: #f0f0f2; color: #1d1d1f; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;">gemini-3-pro-preview</span>
107
  </div>
108
  </div>
109
  <div style="background: rgba(0,0,0,0.03); padding: 10px; border-radius: 6px;">
110
+ <div style="font-size: 11px; color: #1d1d1f; margin-bottom: 4px; font-weight: 600;">📸 图片生成</div>
111
  <div style="font-size: 11px; color: #86868b; line-height: 1.6;">
112
+ 可在"系统设置"标签页自定义配置
 
 
113
  </div>
114
  </div>
115
  </div>
 
433
  border-bottom: 2px solid transparent;
434
  position: relative;
435
  top: 1px;
436
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
437
+ line-height: 1.5;
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 6px;
441
  }}
442
  .tab-button:hover {{
443
  color: #1d1d1f;
 
564
  width: 100%;
565
  flex: 1;
566
  min-height: 300px;
567
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Courier New", monospace;
568
  font-size: 13px;
569
  padding: 16px;
570
  border: 1px solid #e5e5e5;
 
572
  background: #fafaf9;
573
  color: #1a1a1a;
574
  line-height: 1.6;
575
+ letter-spacing: 0.3px;
576
  overflow-y: auto;
577
  resize: none;
578
  scrollbar-width: thin;
 
615
  }}
616
  .btn-secondary:hover {{ background: #e5e5e5; }}
617
 
618
+ .env-var {{
619
+ display: flex;
620
+ justify-content: space-between;
621
+ align-items: center;
622
+ padding: 12px 0;
623
+ border-bottom: 1px solid #f5f5f5;
624
+ }}
625
  .env-var:last-child {{ border-bottom: none; }}
626
+ .env-name {{
627
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Courier New", monospace;
628
+ font-size: 12px;
629
+ color: var(--text-main);
630
+ font-weight: 600;
631
+ letter-spacing: 0.3px;
632
+ line-height: 1.5;
633
+ }}
634
+ .env-desc {{
635
+ font-size: 11px;
636
+ color: var(--text-sec);
637
+ margin-top: 3px;
638
+ line-height: 1.4;
639
+ }}
640
+ .env-value {{
641
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Courier New", monospace;
642
+ font-size: 12px;
643
+ color: var(--text-sec);
644
+ text-align: right;
645
+ max-width: 50%;
646
+ overflow: hidden;
647
+ text-overflow: ellipsis;
648
+ white-space: nowrap;
649
+ letter-spacing: 0.3px;
650
+ line-height: 1.5;
651
+ }}
652
 
653
  .badge {{ display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; vertical-align: middle; margin-left: 6px; }}
654
  .badge-required {{ background: #ffebeb; color: #c62828; }}
655
  .badge-optional {{ background: #e8f5e9; color: #2e7d32; }}
656
 
657
+ code {{
658
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Courier New", monospace;
659
+ background: #f5f5f7;
660
+ padding: 2px 6px;
661
+ border-radius: 4px;
662
+ font-size: 12px;
663
+ color: var(--blue);
664
+ letter-spacing: 0.3px;
665
+ line-height: 1.5;
666
+ }}
667
  a {{ color: var(--blue); text-decoration: none; }}
668
  a:hover {{ text-decoration: underline; }}
669
+ .font-mono {{
670
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Courier New", monospace;
671
+ letter-spacing: 0.3px;
672
+ line-height: 1.5;
673
+ }}
674
 
675
  /* --- Service Info Styles --- */
676
  .model-grid {{ display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }}
 
680
  padding: 4px 10px;
681
  border-radius: 6px;
682
  font-size: 12px;
683
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Courier New", monospace;
684
  border: 1px solid transparent;
685
+ letter-spacing: 0.3px;
686
+ line-height: 1.5;
687
  }}
688
  .model-tag.highlight {{ background: #eef7ff; color: #0071e3; border-color: #dcebfb; font-weight: 500; }}
689
 
 
691
  .info-box-title {{ font-weight: 600; font-size: 12px; color: #1d1d1f; margin-bottom: 6px; }}
692
  .info-box-text {{ font-size: 12px; color: #86868b; line-height: 1.5; }}
693
 
694
+ /* Settings Form Styles */
695
+ .setting-item {{
696
+ margin-bottom: 14px;
697
+ }}
698
+ .setting-item label {{
699
+ display: block;
700
+ font-size: 12px;
701
+ font-weight: 600;
702
+ color: #1d1d1f;
703
+ margin-bottom: 6px;
704
+ }}
705
+ .setting-item input[type="text"],
706
+ .setting-item input[type="password"],
707
+ .setting-item input[type="number"] {{
708
+ width: 100%;
709
+ padding: 8px 12px;
710
+ border: 1px solid #d4d4d4;
711
+ border-radius: 6px;
712
+ font-size: 13px;
713
+ color: #1d1d1f;
714
+ background: #fff;
715
+ transition: border-color 0.15s;
716
+ }}
717
+ .setting-item input:focus {{
718
+ outline: none;
719
+ border-color: #0071e3;
720
+ }}
721
+ .setting-item input::placeholder {{
722
+ color: #c7c7cc;
723
+ }}
724
+
725
  .ep-table {{
726
  width: 100%;
727
  border-collapse: collapse;
 
750
  .m-get {{ background: #eff6ff; color: #1e40af; border: 1px solid #dbeafe; }}
751
  .m-del {{ background: #fef2f2; color: #991b1b; border: 1px solid #fee2e2; }}
752
 
753
+ .ep-path {{
754
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Courier New", monospace;
755
+ color: #1d1d1f;
756
+ margin-right: 8px;
757
+ font-size: 12px;
758
+ letter-spacing: 0.3px;
759
+ line-height: 1.5;
760
+ }}
761
  .ep-desc {{ color: #86868b; font-size: 12px; margin-left: auto; }}
762
 
763
  .current-url-row {{
 
790
  min-width: 100px;
791
  padding: 10px 16px;
792
  font-size: 13px;
793
+ justify-content: center;
794
  }}
795
 
796
  /* Account Table Mobile - Card Layout */
 
913
  <button class="tab-button active" onclick="switchTab('accounts')">📋 账户管理</button>
914
  <button class="tab-button" onclick="switchTab('api')">📚 API文档</button>
915
  <button class="tab-button" onclick="switchTab('config')">⚙️ 系统配置</button>
916
+ <button class="tab-button" onclick="switchTab('settings')">🔧 系统设置</button>
917
  </div>
918
 
919
  <!-- Tab 1: 账户管理 -->
 
1024
  <!-- Tab 3: 系统配置 -->
1025
  <div id="tab-config" class="tab-content">
1026
  <div class="section">
1027
+ <div class="section-title">当前配置状态</div>
1028
  <div class="grid-env">
1029
  <div class="stack-col">
1030
  <div class="card">
1031
+ <h3>环境变量 <span class="badge badge-required">ENV</span></h3>
1032
  <div style="margin-top: 12px;">
1033
  <div class="env-var">
1034
+ <div><div class="env-name">ADMIN_KEY</div><div class="env-desc">管理员密钥</div></div>
1035
+ <div class="env-value">已设置</div>
1036
  </div>
1037
  <div class="env-var">
1038
  <div><div class="env-name">PATH_PREFIX</div><div class="env-desc">API路径前缀</div></div>
1039
+ <div class="env-value">{main.PATH_PREFIX or '未设置'}</div>
1040
  </div>
1041
+ </div>
1042
+ </div>
1043
+
1044
+ <div class="card">
1045
+ <h3>基础配置 <span class="badge badge-optional">YAML</span></h3>
1046
+ <div style="margin-top: 12px;">
1047
  <div class="env-var">
1048
+ <div><div class="env-name">API_KEY</div><div class="env-desc">API访问密钥</div></div>
1049
+ <div class="env-value">{'已设置' if main.API_KEY else '未设置(公开访问)'}</div>
1050
+ </div>
1051
+ <div class="env-var">
1052
+ <div><div class="env-name">BASE_URL</div><div class="env-desc">服务器URL</div></div>
1053
+ <div class="env-value">{'已设置' if main.BASE_URL else '自动检测'}</div>
1054
+ </div>
1055
+ <div class="env-var">
1056
+ <div><div class="env-name">PROXY</div><div class="env-desc">代理地址</div></div>
1057
+ <div class="env-value">{'已设置' if main.PROXY else '未设置'}</div>
1058
  </div>
1059
  </div>
1060
  </div>
1061
+ </div>
1062
 
1063
  <div class="card">
1064
+ <h3>重试策略 <span class="badge badge-optional">YAML</span></h3>
1065
  <div style="margin-top: 12px;">
1066
  <div class="env-var">
1067
+ <div><div class="env-name">max_new_session_tries</div><div class="env-desc">新会话尝试数</div></div>
1068
  <div class="env-value">{main.MAX_NEW_SESSION_TRIES}</div>
1069
  </div>
1070
  <div class="env-var">
1071
+ <div><div class="env-name">max_request_retries</div><div class="env-desc">请求重试次数</div></div>
1072
  <div class="env-value">{main.MAX_REQUEST_RETRIES}</div>
1073
  </div>
1074
  <div class="env-var">
1075
+ <div><div class="env-name">max_account_switch_tries</div><div class="env-desc">账户切换次数</div></div>
1076
  <div class="env-value">{main.MAX_ACCOUNT_SWITCH_TRIES}</div>
1077
  </div>
1078
  <div class="env-var">
1079
+ <div><div class="env-name">account_failure_threshold</div><div class="env-desc">失败阈值</div></div>
1080
  <div class="env-value">{main.ACCOUNT_FAILURE_THRESHOLD} 次</div>
1081
  </div>
1082
  <div class="env-var">
1083
+ <div><div class="env-name">rate_limit_cooldown_seconds</div><div class="env-desc">429冷却时间</div></div>
1084
  <div class="env-value">{main.RATE_LIMIT_COOLDOWN_SECONDS} 秒</div>
1085
  </div>
1086
+ <div class="env-var">
1087
+ <div><div class="env-name">session_cache_ttl_seconds</div><div class="env-desc">会话缓存时间</div></div>
1088
+ <div class="env-value">{main.SESSION_CACHE_TTL_SECONDS} 秒</div>
1089
+ </div>
1090
  </div>
1091
  </div>
 
1092
 
1093
  <div class="card">
1094
+ <h3>公开展示 <span class="badge badge-optional">YAML</span></h3>
1095
  <div style="margin-top: 12px;">
1096
  <div class="env-var">
1097
+ <div><div class="env-name">LOGO_URL</div><div class="env-desc">Logo图片</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1098
  <div class="env-value">{'已设置' if main.LOGO_URL else '未设置'}</div>
1099
  </div>
1100
  <div class="env-var">
1101
+ <div><div class="env-name">CHAT_URL</div><div class="env-desc">对话链接</div></div>
1102
  <div class="env-value">{'已设置' if main.CHAT_URL else '未设置'}</div>
1103
  </div>
 
 
 
 
1104
  </div>
1105
  </div>
1106
  </div>
1107
  </div>
1108
 
1109
+ <!-- Tab 4: 系统设置 -->
1110
+ <div id="tab-settings" class="tab-content">
1111
  <div class="section">
1112
+ <div class="section-title">系统设置</div>
1113
+ <div style="color: #6b6b6b; font-size: 12px; margin-bottom: 16px; padding-left: 4px;">
1114
+ ✅ 所有配置修改后<strong>立即生效</strong>,无需重启服务。配置保存在 <code>data/settings.yaml</code><br>
1115
+ ⚠️ 页面显示需要重启服务后才会更新
1116
+ </div>
1117
+
1118
+ <div class="grid-env">
1119
+ <div class="stack-col">
1120
+ <!-- 基础配置 -->
1121
+ <div class="card">
1122
+ <h3>基础配置</h3>
1123
+ <div style="margin-top: 12px;">
1124
+ <div class="setting-item">
1125
+ <label>API 访问密钥</label>
1126
+ <input type="text" id="setting-api-key" placeholder="留空则公开访问" />
1127
+ </div>
1128
+ <div class="setting-item">
1129
+ <label>服务器 URL</label>
1130
+ <input type="text" id="setting-base-url" placeholder="留空则自动检测" />
1131
+ </div>
1132
+ <div class="setting-item">
1133
+ <label>代理地址</label>
1134
+ <input type="text" id="setting-proxy" placeholder="如 http://127.0.0.1:7890" />
1135
+ </div>
1136
+ </div>
1137
+ </div>
1138
+
1139
+ <!-- 图片生成配置 -->
1140
+ <div class="card">
1141
+ <h3>📸 图片生成配置</h3>
1142
+ <div style="margin-top: 12px;">
1143
+ <div class="setting-item">
1144
+ <label style="display: flex; align-items: center; gap: 8px;">
1145
+ <input type="checkbox" id="setting-image-enabled" style="width: auto;" />
1146
+ 启用图片生成
1147
+ </label>
1148
+ </div>
1149
+ <div class="setting-item">
1150
+ <label>支持的模型</label>
1151
+ <div id="setting-image-models" style="display: flex; flex-direction: column; gap: 6px; margin-top: 6px;">
1152
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
1153
+ <input type="checkbox" value="gemini-3-pro-preview" style="width: auto;" /> gemini-3-pro-preview
1154
+ </label>
1155
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
1156
+ <input type="checkbox" value="gemini-2.5-pro" style="width: auto;" /> gemini-2.5-pro
1157
+ </label>
1158
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
1159
+ <input type="checkbox" value="gemini-2.5-flash" style="width: auto;" /> gemini-2.5-flash
1160
+ </label>
1161
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
1162
+ <input type="checkbox" value="gemini-3-flash-preview" style="width: auto;" /> gemini-3-flash-preview
1163
+ </label>
1164
+ <label style="display: flex; align-items: center; gap: 6px; font-weight: normal; font-size: 12px;">
1165
+ <input type="checkbox" value="gemini-auto" style="width: auto;" /> gemini-auto
1166
+ </label>
1167
+ </div>
1168
+ </div>
1169
+ </div>
1170
+ </div>
1171
  </div>
1172
 
1173
+ <div class="stack-col">
1174
+ <!-- 重试策略配置 -->
1175
+ <div class="card">
1176
+ <h3>🔄 重试策略配置</h3>
1177
+ <div style="margin-top: 12px;">
1178
+ <div class="setting-item">
1179
+ <label>新会话尝试账户数</label>
1180
+ <input type="number" id="setting-max-new-session" min="1" max="20" />
1181
+ </div>
1182
+ <div class="setting-item">
1183
+ <label>请求失败重试次数</label>
1184
+ <input type="number" id="setting-max-retries" min="1" max="10" />
1185
+ </div>
1186
+ <div class="setting-item">
1187
+ <label>账户切换尝试次数</label>
1188
+ <input type="number" id="setting-max-switch" min="1" max="20" />
1189
+ </div>
1190
+ <div class="setting-item">
1191
+ <label>账户失败阈值(次)</label>
1192
+ <input type="number" id="setting-failure-threshold" min="1" max="10" />
1193
+ </div>
1194
+ <div class="setting-item">
1195
+ <label>429 冷却时间(秒)</label>
1196
+ <input type="number" id="setting-cooldown" min="60" max="3600" />
1197
+ </div>
1198
+ <div class="setting-item">
1199
+ <label>会话缓存时间(秒)</label>
1200
+ <input type="number" id="setting-cache-ttl" min="300" max="86400" />
1201
+ </div>
1202
+ </div>
1203
+ </div>
1204
+
1205
+ <!-- 公开展示配置 -->
1206
+ <div class="card">
1207
+ <h3>🎨 公开展示配置</h3>
1208
+ <div style="margin-top: 12px;">
1209
+ <div class="setting-item">
1210
+ <label>Logo URL</label>
1211
+ <input type="text" id="setting-logo-url" placeholder="留空则不显示" />
1212
+ </div>
1213
+ <div class="setting-item">
1214
+ <label>开始对话链接</label>
1215
+ <input type="text" id="setting-chat-url" placeholder="留空则不显示" />
1216
+ </div>
1217
+ <div class="setting-item">
1218
+ <label>Session 过期时间(小时)</label>
1219
+ <input type="number" id="setting-session-hours" min="1" max="168" />
1220
+ </div>
1221
+ </div>
1222
  </div>
1223
  </div>
1224
  </div>
1225
+
1226
+ <div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
1227
+ <button class="btn btn-secondary" onclick="loadSettings()">重置</button>
1228
+ <button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
1229
+ </div>
1230
  </div>
1231
  </div>
1232
  </div>
 
1491
  closeModal();
1492
  }}
1493
  }});
1494
+
1495
+ // ========== 系统设置相关函数 ==========
1496
+ async function loadSettings() {{
1497
+ try {{
1498
+ const response = await fetch('/{admin_path_segment}/settings');
1499
+ const settings = await handleApiResponse(response);
1500
+
1501
+ // 基础配置
1502
+ document.getElementById('setting-api-key').value = settings.basic?.api_key || '';
1503
+ document.getElementById('setting-base-url').value = settings.basic?.base_url || '';
1504
+ document.getElementById('setting-proxy').value = settings.basic?.proxy || '';
1505
+
1506
+ // 图片生成配置
1507
+ document.getElementById('setting-image-enabled').checked = settings.image_generation?.enabled ?? true;
1508
+ const supportedModels = settings.image_generation?.supported_models || [];
1509
+ document.querySelectorAll('#setting-image-models input[type="checkbox"]').forEach(cb => {{
1510
+ cb.checked = supportedModels.includes(cb.value);
1511
+ }});
1512
+
1513
+ // 重试策略配置
1514
+ document.getElementById('setting-max-new-session').value = settings.retry?.max_new_session_tries || 5;
1515
+ document.getElementById('setting-max-retries').value = settings.retry?.max_request_retries || 3;
1516
+ document.getElementById('setting-max-switch').value = settings.retry?.max_account_switch_tries || 5;
1517
+ document.getElementById('setting-failure-threshold').value = settings.retry?.account_failure_threshold || 3;
1518
+ document.getElementById('setting-cooldown').value = settings.retry?.rate_limit_cooldown_seconds || 600;
1519
+ document.getElementById('setting-cache-ttl').value = settings.retry?.session_cache_ttl_seconds || 3600;
1520
+
1521
+ // 公开展示配置
1522
+ document.getElementById('setting-logo-url').value = settings.public_display?.logo_url || '';
1523
+ document.getElementById('setting-chat-url').value = settings.public_display?.chat_url || '';
1524
+ document.getElementById('setting-session-hours').value = settings.session?.expire_hours || 24;
1525
+ }} catch (error) {{
1526
+ console.error('加载设置失败:', error);
1527
+ alert('加载设置失败: ' + error.message);
1528
+ }}
1529
+ }}
1530
+
1531
+ async function saveSettings() {{
1532
+ try {{
1533
+ // 收集图片生成支持的模型
1534
+ const supportedModels = [];
1535
+ document.querySelectorAll('#setting-image-models input[type="checkbox"]:checked').forEach(cb => {{
1536
+ supportedModels.push(cb.value);
1537
+ }});
1538
+
1539
+ const settings = {{
1540
+ basic: {{
1541
+ api_key: document.getElementById('setting-api-key').value,
1542
+ base_url: document.getElementById('setting-base-url').value,
1543
+ proxy: document.getElementById('setting-proxy').value
1544
+ }},
1545
+ image_generation: {{
1546
+ enabled: document.getElementById('setting-image-enabled').checked,
1547
+ supported_models: supportedModels
1548
+ }},
1549
+ retry: {{
1550
+ max_new_session_tries: parseInt(document.getElementById('setting-max-new-session').value) || 5,
1551
+ max_request_retries: parseInt(document.getElementById('setting-max-retries').value) || 3,
1552
+ max_account_switch_tries: parseInt(document.getElementById('setting-max-switch').value) || 5,
1553
+ account_failure_threshold: parseInt(document.getElementById('setting-failure-threshold').value) || 3,
1554
+ rate_limit_cooldown_seconds: parseInt(document.getElementById('setting-cooldown').value) || 600,
1555
+ session_cache_ttl_seconds: parseInt(document.getElementById('setting-cache-ttl').value) || 3600
1556
+ }},
1557
+ public_display: {{
1558
+ logo_url: document.getElementById('setting-logo-url').value,
1559
+ chat_url: document.getElementById('setting-chat-url').value
1560
+ }},
1561
+ session: {{
1562
+ expire_hours: parseInt(document.getElementById('setting-session-hours').value) || 24
1563
+ }}
1564
+ }};
1565
+
1566
+ const response = await fetch('/{admin_path_segment}/settings', {{
1567
+ method: 'PUT',
1568
+ headers: {{'Content-Type': 'application/json'}},
1569
+ body: JSON.stringify(settings)
1570
+ }});
1571
+
1572
+ await handleApiResponse(response);
1573
+
1574
+ // 直接刷新页面,不显示弹窗
1575
+ window.location.reload();
1576
+ }} catch (error) {{
1577
+ console.error('保存设置失败:', error);
1578
+ alert('保存设置失败: ' + error.message);
1579
+ }}
1580
+ }}
1581
+
1582
+ // 页面加载时自动加载设置
1583
+ document.addEventListener('DOMContentLoaded', function() {{
1584
+ loadSettings();
1585
+ }});
1586
  </script>
1587
  </body>
1588
  </html>
 
2458
  async def admin_logs_html_no_auth(request):
2459
  """返回美化的 HTML 日志查看界面(无需认证)"""
2460
  from fastapi.responses import HTMLResponse
 
2461
  html_content = r"""
2462
  <!DOCTYPE html>
2463
  <html>
 
2467
  <title>日志查看器</title>
2468
  <style>
2469
  * { margin: 0; padding: 0; box-sizing: border-box; }
2470
+ html, body { height: 100%; overflow: hidden; }
2471
  body {
2472
+ font-family: 'Consolas', 'Monaco', monospace;
2473
  background: #fafaf9;
2474
+ display: flex;
2475
+ align-items: center;
2476
+ justify-content: center;
2477
+ padding: 15px;
2478
  }
2479
  .container {
2480
+ width: 100%;
2481
  max-width: 1400px;
2482
+ height: calc(100vh - 30px);
2483
  background: white;
2484
  border-radius: 16px;
2485
  padding: 30px;
2486
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
2487
+ display: flex;
2488
+ flex-direction: column;
2489
+ }
2490
+ h1 { color: #1a1a1a; font-size: 22px; font-weight: 600; margin-bottom: 20px; text-align: center; }
2491
+ .stats {
2492
+ display: grid;
2493
+ grid-template-columns: repeat(6, 1fr);
2494
+ gap: 12px;
2495
+ margin-bottom: 16px;
2496
+ }
2497
+ .stat {
2498
+ background: #fafaf9;
2499
+ padding: 12px;
2500
+ border: 1px solid #e5e5e5;
2501
+ border-radius: 8px;
2502
+ text-align: center;
2503
+ transition: all 0.15s ease;
2504
+ }
2505
+ .stat:hover { border-color: #d4d4d4; }
2506
+ .stat-label { color: #6b6b6b; font-size: 11px; margin-bottom: 4px; }
2507
+ .stat-value { color: #1a1a1a; font-size: 18px; font-weight: 600; }
2508
+ .controls {
2509
+ display: flex;
2510
+ gap: 8px;
2511
+ margin-bottom: 16px;
2512
+ flex-wrap: wrap;
2513
+ }
2514
+ .controls input, .controls select, .controls button {
2515
+ padding: 6px 10px;
2516
+ border: 1px solid #e5e5e5;
2517
+ border-radius: 8px;
2518
+ font-size: 13px;
2519
+ }
2520
+ .controls select {
2521
+ appearance: none;
2522
+ -webkit-appearance: none;
2523
+ -moz-appearance: none;
2524
+ background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 5L6 8L9 5' stroke='%236b6b6b' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
2525
+ background-repeat: no-repeat;
2526
+ background-position: right 12px center;
2527
+ padding-right: 32px;
2528
+ }
2529
+ .controls input[type="text"] { flex: 1; min-width: 150px; }
2530
+ .controls button {
2531
+ background: #1a73e8;
2532
+ color: white;
2533
+ border: none;
2534
+ cursor: pointer;
2535
+ font-weight: 500;
2536
+ transition: background 0.15s ease;
2537
+ display: flex;
2538
+ align-items: center;
2539
+ gap: 6px;
2540
+ }
2541
+ .controls button:hover { background: #1557b0; }
2542
+ .controls button.danger { background: #dc2626; }
2543
+ .controls button.danger:hover { background: #b91c1c; }
2544
+ .controls button svg { flex-shrink: 0; }
2545
+ .log-container {
2546
+ flex: 1;
2547
+ background: #fafaf9;
2548
+ border: 1px solid #e5e5e5;
2549
+ border-radius: 8px;
2550
+ padding: 12px;
2551
+ overflow-y: auto;
2552
+ scrollbar-width: thin;
2553
+ scrollbar-color: rgba(0,0,0,0.15) transparent;
2554
+ }
2555
+ /* Webkit 滚动条样式 - 更窄且不占位 */
2556
+ .log-container::-webkit-scrollbar {
2557
+ width: 4px;
2558
+ }
2559
+ .log-container::-webkit-scrollbar-track {
2560
+ background: transparent;
2561
+ }
2562
+ .log-container::-webkit-scrollbar-thumb {
2563
+ background: rgba(0,0,0,0.15);
2564
+ border-radius: 2px;
2565
+ }
2566
+ .log-container::-webkit-scrollbar-thumb:hover {
2567
+ background: rgba(0,0,0,0.3);
2568
+ }
2569
+ .log-entry {
2570
+ padding: 8px 10px;
2571
+ margin-bottom: 4px;
2572
+ background: white;
2573
+ border-radius: 6px;
2574
+ border: 1px solid #e5e5e5;
2575
+ font-size: 12px;
2576
+ color: #1a1a1a;
2577
+ display: flex;
2578
+ align-items: center;
2579
+ gap: 8px;
2580
+ word-break: break-word;
2581
+ }
2582
+ .log-entry > div:first-child {
2583
+ display: flex;
2584
+ align-items: center;
2585
+ gap: 8px;
2586
+ }
2587
+ .log-message {
2588
+ flex: 1;
2589
+ overflow: hidden;
2590
+ text-overflow: ellipsis;
2591
+ }
2592
+ .log-entry:hover { border-color: #d4d4d4; }
2593
+ .log-time { color: #6b6b6b; }
2594
+ .log-level {
2595
+ display: flex;
2596
+ align-items: center;
2597
+ gap: 4px;
2598
+ padding: 2px 6px;
2599
+ border-radius: 3px;
2600
+ font-size: 10px;
2601
+ font-weight: 600;
2602
+ }
2603
+ .log-level::before {
2604
+ content: '';
2605
+ width: 6px;
2606
+ height: 6px;
2607
+ border-radius: 50%;
2608
  }
 
 
 
 
2609
  .log-level.INFO { background: #e3f2fd; color: #1976d2; }
2610
+ .log-level.INFO::before { background: #1976d2; }
2611
  .log-level.WARNING { background: #fff3e0; color: #f57c00; }
2612
+ .log-level.WARNING::before { background: #f57c00; }
2613
+ .log-level.ERROR { background: #ffebee; color: #d32f2f; }
2614
+ .log-level.ERROR::before { background: #d32f2f; }
2615
+ .log-level.DEBUG { background: #f3e5f5; color: #7b1fa2; }
2616
+ .log-level.DEBUG::before { background: #7b1fa2; }
2617
+ .log-group {
2618
+ margin-bottom: 8px;
2619
+ border: 1px solid #e5e5e5;
2620
+ border-radius: 8px;
2621
+ background: white;
2622
+ }
2623
+ .log-group-header {
2624
+ padding: 10px 12px;
2625
+ background: #f9f9f9;
2626
+ border-radius: 8px 8px 0 0;
2627
+ cursor: pointer;
2628
+ display: flex;
2629
+ align-items: center;
2630
+ gap: 8px;
2631
+ transition: background 0.15s ease;
2632
+ }
2633
+ .log-group-header:hover {
2634
+ background: #f0f0f0;
2635
+ }
2636
+ .log-group-content {
2637
+ padding: 8px;
2638
+ }
2639
+ .log-group .log-entry {
2640
+ margin-bottom: 4px;
2641
+ }
2642
+ .log-group .log-entry:last-child {
2643
+ margin-bottom: 0;
2644
+ }
2645
+ .toggle-icon {
2646
+ display: inline-block;
2647
+ transition: transform 0.2s ease;
2648
+ }
2649
+ .toggle-icon.collapsed {
2650
+ transform: rotate(-90deg);
2651
+ }
2652
+ @media (max-width: 768px) {
2653
+ body { padding: 0; }
2654
+ .container { padding: 15px; height: 100vh; border-radius: 0; max-width: 100%; }
2655
+ h1 { font-size: 18px; margin-bottom: 12px; }
2656
+ .stats { grid-template-columns: repeat(3, 1fr); gap: 8px; }
2657
+ .stat { padding: 8px; }
2658
+ .controls { gap: 6px; }
2659
+ .controls input, .controls select { min-height: 38px; }
2660
+ .controls select { flex: 0 0 auto; }
2661
+ .controls input[type="text"] { flex: 1 1 auto; min-width: 80px; }
2662
+ .controls input[type="number"] { flex: 0 0 60px; }
2663
+ .controls button { padding: 10px 8px; font-size: 12px; flex: 1 1 22%; justify-content: center; min-height: 38px; }
2664
+ .log-entry {
2665
+ font-size: 12px;
2666
+ padding: 10px;
2667
+ gap: 8px;
2668
+ flex-direction: column;
2669
+ align-items: flex-start;
2670
+ }
2671
+ .log-entry > div:first-child {
2672
+ display: flex;
2673
+ align-items: center;
2674
+ gap: 6px;
2675
+ width: 100%;
2676
+ flex-wrap: wrap;
2677
+ }
2678
+ .log-time { font-size: 11px; color: #9e9e9e; }
2679
+ .log-level { font-size: 10px; }
2680
+ .log-message {
2681
+ width: 100%;
2682
+ white-space: normal;
2683
+ word-break: break-word;
2684
+ line-height: 1.5;
2685
+ margin-top: 4px;
2686
+ }
2687
+ }
2688
  </style>
2689
  </head>
2690
  <body>
2691
  <div class="container">
2692
+ <h1>Gemini API 日志查看器</h1>
2693
+ <div class="stats">
2694
+ <div class="stat">
2695
+ <div class="stat-label">总数</div>
2696
+ <div class="stat-value" id="total-count">-</div>
2697
+ </div>
2698
+ <div class="stat">
2699
+ <div class="stat-label">对话</div>
2700
+ <div class="stat-value" id="chat-count">-</div>
2701
+ </div>
2702
+ <div class="stat">
2703
+ <div class="stat-label">INFO</div>
2704
+ <div class="stat-value" id="info-count">-</div>
2705
+ </div>
2706
+ <div class="stat">
2707
+ <div class="stat-label">WARNING</div>
2708
+ <div class="stat-value" id="warning-count">-</div>
2709
+ </div>
2710
+ <div class="stat">
2711
+ <div class="stat-label">ERROR</div>
2712
+ <div class="stat-value" id="error-count">-</div>
2713
+ </div>
2714
+ <div class="stat">
2715
+ <div class="stat-label">更新</div>
2716
+ <div class="stat-value" id="last-update" style="font-size: 11px;">-</div>
2717
+ </div>
2718
+ </div>
2719
+ <div class="controls">
2720
+ <select id="level-filter">
2721
+ <option value="">全部</option>
2722
+ <option value="INFO">INFO</option>
2723
+ <option value="WARNING">WARNING</option>
2724
+ <option value="ERROR">ERROR</option>
2725
+ </select>
2726
+ <input type="text" id="search-input" placeholder="搜索...">
2727
+ <input type="number" id="limit-input" value="1500" min="10" max="3000" step="100" style="width: 80px;">
2728
+ <button onclick="loadLogs()">
2729
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2730
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
2731
+ </svg>
2732
+ 查询
2733
+ </button>
2734
+ <button onclick="exportJSON()">
2735
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2736
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
2737
+ </svg>
2738
+ 导出
2739
+ </button>
2740
+ <button id="auto-refresh-btn" onclick="toggleAutoRefresh()">
2741
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2742
+ <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"/>
2743
+ </svg>
2744
+ 自动刷新
2745
+ </button>
2746
+ <button onclick="clearAllLogs()" class="danger">
2747
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2748
+ <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"/>
2749
+ </svg>
2750
+ 清空
2751
+ </button>
2752
+ </div>
2753
+ <div class="log-container" id="log-container">
2754
+ <div style="color: #6b6b6b;">正在加载...</div>
2755
+ </div>
2756
  </div>
2757
  <script>
2758
+ let autoRefreshTimer = null;
2759
  async function loadLogs() {
2760
+ const level = document.getElementById('level-filter').value;
2761
+ const search = document.getElementById('search-input').value;
2762
+ const limit = document.getElementById('limit-input').value;
2763
+ // 从当前 URL 获取 key 参数
2764
+ const urlParams = new URLSearchParams(window.location.search);
2765
+ const key = urlParams.get('key');
2766
+ // 构建 API URL(直接使用 /admin/log)
2767
+ let url = `/admin/log?limit=${limit}`;
2768
+ if (key) url += `&key=${key}`;
2769
+ if (level) url += `&level=${level}`;
2770
+ if (search) url += `&search=${encodeURIComponent(search)}`;
2771
  try {
2772
+ const response = await fetch(url);
2773
+ if (!response.ok) {
2774
+ throw new Error(`HTTP ${response.status}`);
2775
+ }
2776
+ const data = await response.json();
2777
+ if (data && data.logs) {
2778
+ displayLogs(data.logs);
2779
+ updateStats(data.stats);
2780
+ document.getElementById('last-update').textContent = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
2781
+ } else {
2782
+ throw new Error('Invalid data format');
2783
+ }
2784
+ } catch (error) {
2785
+ document.getElementById('log-container').innerHTML = '<div class="log-entry ERROR">加载失败: ' + error.message + '</div>';
2786
+ }
2787
+ }
2788
+ function updateStats(stats) {
2789
+ document.getElementById('total-count').textContent = stats.memory.total;
2790
+ document.getElementById('info-count').textContent = stats.memory.by_level.INFO || 0;
2791
+ document.getElementById('warning-count').textContent = stats.memory.by_level.WARNING || 0;
2792
+ const errorCount = document.getElementById('error-count');
2793
+ errorCount.textContent = stats.memory.by_level.ERROR || 0;
2794
+ if (stats.errors && stats.errors.count > 0) errorCount.style.color = '#dc2626';
2795
+ document.getElementById('chat-count').textContent = stats.chat_count || 0;
2796
+ }
2797
+ // 分类颜色配置(提取到外部避免重复定义)
2798
+ const CATEGORY_COLORS = {
2799
+ 'SYSTEM': '#9e9e9e',
2800
+ 'CONFIG': '#607d8b',
2801
+ 'LOG': '#9e9e9e',
2802
+ 'AUTH': '#4caf50',
2803
+ 'SESSION': '#00bcd4',
2804
+ 'FILE': '#ff9800',
2805
+ 'CHAT': '#2196f3',
2806
+ 'API': '#8bc34a',
2807
+ 'CACHE': '#9c27b0',
2808
+ 'ACCOUNT': '#f44336',
2809
+ 'MULTI': '#673ab7'
2810
+ };
2811
+
2812
+ // 账户颜色配置(提取到外部避免重复定义)
2813
+ const ACCOUNT_COLORS = {
2814
+ 'account_1': '#9c27b0',
2815
+ 'account_2': '#e91e63',
2816
+ 'account_3': '#00bcd4',
2817
+ 'account_4': '#4caf50',
2818
+ 'account_5': '#ff9800'
2819
+ };
2820
+
2821
+ function getCategoryColor(category) {
2822
+ return CATEGORY_COLORS[category] || '#757575';
2823
+ }
2824
+
2825
+ function getAccountColor(accountId) {
2826
+ return ACCOUNT_COLORS[accountId] || '#757575';
2827
+ }
2828
+
2829
+ function displayLogs(logs) {
2830
+ const container = document.getElementById('log-container');
2831
+ if (logs.length === 0) {
2832
+ container.innerHTML = '<div class="log-entry">暂无日志</div>';
2833
+ return;
2834
+ }
2835
+
2836
+ // 按请求ID分组
2837
+ const groups = {};
2838
+ const ungrouped = [];
2839
+
2840
+ logs.forEach(log => {
2841
+ const msg = escapeHtml(log.message);
2842
+ const reqMatch = msg.match(/\[req_([a-z0-9]+)\]/);
2843
+
2844
+ if (reqMatch) {
2845
+ const reqId = reqMatch[1];
2846
+ if (!groups[reqId]) {
2847
+ groups[reqId] = [];
2848
+ }
2849
+ groups[reqId].push(log);
2850
+ } else {
2851
+ ungrouped.push(log);
2852
+ }
2853
+ });
2854
+
2855
+ // 渲染分组
2856
+ let html = '';
2857
+
2858
+ // 先渲染未分组的日志
2859
+ ungrouped.forEach(log => {
2860
+ html += renderLogEntry(log);
2861
+ });
2862
+
2863
+ // 读取折叠状态
2864
+ const foldState = JSON.parse(localStorage.getItem('log-fold-state') || '{}');
2865
+
2866
+ // 按请求ID分组渲染(最新的组在下面)
2867
+ Object.keys(groups).forEach(reqId => {
2868
+ const groupLogs = groups[reqId];
2869
+ const firstLog = groupLogs[0];
2870
+ const lastLog = groupLogs[groupLogs.length - 1];
2871
+
2872
+ // 判断状态
2873
+ let status = 'in_progress';
2874
+ let statusColor = '#ff9800';
2875
+ let statusText = '进行中';
2876
+
2877
+ if (lastLog.message.includes('响应完成') || lastLog.message.includes('非流式响应完成')) {
2878
+ status = 'success';
2879
+ statusColor = '#4caf50';
2880
+ statusText = '成功';
2881
+ } else if (lastLog.level === 'ERROR' || lastLog.message.includes('失败')) {
2882
+ status = 'error';
2883
+ statusColor = '#f44336';
2884
+ statusText = '失败';
2885
+ } else {
2886
+ // 检查超时(最后日志超过 5 分钟)
2887
+ const lastLogTime = new Date(lastLog.time);
2888
+ const now = new Date();
2889
+ const diffMinutes = (now - lastLogTime) / 1000 / 60;
2890
+ if (diffMinutes > 5) {
2891
+ status = 'timeout';
2892
+ statusColor = '#ffc107';
2893
+ statusText = '超时';
2894
+ }
2895
+ }
2896
+
2897
+ // 提取账户ID和模型
2898
+ const accountMatch = firstLog.message.match(/\[account_(\d+)\]/);
2899
+ const modelMatch = firstLog.message.match(/收到请求: ([^ ]+)/);
2900
+ const accountId = accountMatch ? `account_${accountMatch[1]}` : '';
2901
+ const model = modelMatch ? modelMatch[1] : '';
2902
+
2903
+ // 检查折叠状态
2904
+ const isCollapsed = foldState[reqId] === true;
2905
+ const contentStyle = isCollapsed ? 'style="display: none;"' : '';
2906
+ const iconClass = isCollapsed ? 'class="toggle-icon collapsed"' : 'class="toggle-icon"';
2907
+
2908
+ html += `
2909
+ <div class="log-group" data-req-id="${reqId}">
2910
+ <div class="log-group-header" onclick="toggleGroup('${reqId}')">
2911
+ <span style="color: ${statusColor}; font-weight: 600; font-size: 11px;">⬤ ${statusText}</span>
2912
+ <span style="color: #666; font-size: 11px; margin-left: 8px;">req_${reqId}</span>
2913
+ ${accountId ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; margin-left: 8px;">${accountId}</span>` : ''}
2914
+ ${model ? `<span style="color: #999; font-size: 11px; margin-left: 8px;">${model}</span>` : ''}
2915
+ <span style="color: #999; font-size: 11px; margin-left: 8px;">${groupLogs.length}条日志</span>
2916
+ <span ${iconClass} style="margin-left: auto; color: #999;">▼</span>
2917
+ </div>
2918
+ <div class="log-group-content" ${contentStyle}>
2919
+ ${groupLogs.map(log => renderLogEntry(log)).join('')}
2920
+ </div>
2921
+ </div>
2922
+ `;
2923
+ });
2924
+
2925
+ container.innerHTML = html;
2926
+
2927
+ // 自动滚动到底部,显示最新日志
2928
+ container.scrollTop = container.scrollHeight;
2929
+ }
2930
+
2931
+ function renderLogEntry(log) {
2932
+ const msg = escapeHtml(log.message);
2933
+ let displayMsg = msg;
2934
+ let categoryTags = [];
2935
+ let accountId = null;
2936
+
2937
+ // 解析所有标签:[CATEGORY1] [CATEGORY2] [account_X] [req_X] message
2938
+ let remainingMsg = msg;
2939
+ const tagRegex = /^\[([A-Z_a-z0-9]+)\]/;
2940
+
2941
+ while (true) {
2942
+ const match = remainingMsg.match(tagRegex);
2943
+ if (!match) break;
2944
+
2945
+ const tag = match[1];
2946
+ remainingMsg = remainingMsg.substring(match[0].length).trim();
2947
+
2948
+ // 跳过req_标签(已在组头部显示)
2949
+ if (tag.startsWith('req_')) {
2950
+ continue;
2951
+ }
2952
+ // 判断是否为账户ID
2953
+ else if (tag.startsWith('account_')) {
2954
+ accountId = tag;
2955
+ } else {
2956
+ // 普通分类标签
2957
+ categoryTags.push(tag);
2958
+ }
2959
+ }
2960
+
2961
+ displayMsg = remainingMsg;
2962
+
2963
+ // 生成分类标签HTML
2964
+ const categoryTagsHtml = categoryTags.map(cat =>
2965
+ `<span class="log-category" style="background: ${getCategoryColor(cat)}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 2px;">${cat}</span>`
2966
+ ).join('');
2967
+
2968
+ // 生成账户标签HTML
2969
+ const accountTagHtml = accountId
2970
+ ? `<span style="color: ${getAccountColor(accountId)}; font-size: 11px; font-weight: 600; margin-left: 2px;">${accountId}</span>`
2971
+ : '';
2972
+
2973
+ return `
2974
+ <div class="log-entry ${log.level}">
2975
+ <div>
2976
  <span class="log-time">${log.time}</span>
2977
  <span class="log-level ${log.level}">${log.level}</span>
2978
+ ${categoryTagsHtml}
2979
+ ${accountTagHtml}
2980
+ </div>
2981
+ <div class="log-message">${displayMsg}</div>
2982
+ </div>
2983
+ `;
2984
+ }
2985
+
2986
+ function toggleGroup(reqId) {
2987
+ const group = document.querySelector(`.log-group[data-req-id="${reqId}"]`);
2988
+ const content = group.querySelector('.log-group-content');
2989
+ const icon = group.querySelector('.toggle-icon');
2990
+
2991
+ const isCollapsed = content.style.display === 'none';
2992
+ if (isCollapsed) {
2993
+ content.style.display = 'block';
2994
+ icon.classList.remove('collapsed');
2995
+ } else {
2996
+ content.style.display = 'none';
2997
+ icon.classList.add('collapsed');
2998
+ }
2999
+
3000
+ // 保存折叠状态到 localStorage
3001
+ const foldState = JSON.parse(localStorage.getItem('log-fold-state') || '{}');
3002
+ foldState[reqId] = !isCollapsed;
3003
+ localStorage.setItem('log-fold-state', JSON.stringify(foldState));
3004
+ }
3005
+ function escapeHtml(text) {
3006
+ const div = document.createElement('div');
3007
+ div.textContent = text;
3008
+ return div.innerHTML;
3009
+ }
3010
+ async function exportJSON() {
3011
+ try {
3012
+ const urlParams = new URLSearchParams(window.location.search);
3013
+ const key = urlParams.get('key');
3014
+ let url = `/admin/log?limit=3000`;
3015
+ if (key) url += `&key=${key}`;
3016
+ const response = await fetch(url);
3017
+ const data = await response.json();
3018
+ const blob = new Blob([JSON.stringify({exported_at: new Date().toISOString(), logs: data.logs}, null, 2)], {type: 'application/json'});
3019
+ const blobUrl = URL.createObjectURL(blob);
3020
+ const a = document.createElement('a');
3021
+ a.href = blobUrl;
3022
+ a.download = 'logs_' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.json';
3023
+ a.click();
3024
+ URL.revokeObjectURL(blobUrl);
3025
+ alert('导出成功');
3026
+ } catch (error) {
3027
+ alert('导出失败: ' + error.message);
3028
+ }
3029
+ }
3030
+ async function clearAllLogs() {
3031
+ if (!confirm('确定清空所有日志?')) return;
3032
+ try {
3033
+ const urlParams = new URLSearchParams(window.location.search);
3034
+ const key = urlParams.get('key');
3035
+ let url = `/admin/log?confirm=yes`;
3036
+ if (key) url += `&key=${key}`;
3037
+ const response = await fetch(url, {method: 'DELETE'});
3038
+ if (response.ok) {
3039
+ alert('已清空');
3040
+ loadLogs();
3041
+ } else {
3042
+ alert('清空失败');
3043
+ }
3044
+ } catch (error) {
3045
+ alert('清空失败: ' + error.message);
3046
+ }
3047
+ }
3048
+ let autoRefreshEnabled = true;
3049
+ function toggleAutoRefresh() {
3050
+ autoRefreshEnabled = !autoRefreshEnabled;
3051
+ const btn = document.getElementById('auto-refresh-btn');
3052
+ if (autoRefreshEnabled) {
3053
+ btn.style.background = '#1a73e8';
3054
+ autoRefreshTimer = setInterval(loadLogs, 5000);
3055
+ } else {
3056
+ btn.style.background = '#6b6b6b';
3057
+ if (autoRefreshTimer) {
3058
+ clearInterval(autoRefreshTimer);
3059
+ autoRefreshTimer = null;
3060
  }
 
 
 
3061
  }
3062
  }
3063
+ document.addEventListener('DOMContentLoaded', () => {
3064
+ loadLogs();
3065
+ autoRefreshTimer = setInterval(loadLogs, 5000);
3066
+ document.getElementById('search-input').addEventListener('keypress', (e) => {
3067
+ if (e.key === 'Enter') loadLogs();
3068
+ });
3069
+ document.getElementById('level-filter').addEventListener('change', loadLogs);
3070
+ document.getElementById('limit-input').addEventListener('change', loadLogs);
3071
+ });
3072
  </script>
3073
  </body>
3074
  </html>