xiaoyukkkk commited on
Commit
96d721a
·
verified ·
1 Parent(s): 990d959

Upload 3 files

Browse files
Files changed (1) hide show
  1. core/templates.py +289 -4
core/templates.py CHANGED
@@ -351,8 +351,9 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
351
 
352
  /* Account & Env Styles */
353
  .account-card .acc-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
354
- .acc-title {{ font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; }}
355
- .status-dot {{ width: 8px; height: 8px; border-radius: 50%; }}
 
356
  .acc-status {{ font-size: 12px; font-weight: 600; }}
357
  .acc-actions {{ display: flex; gap: 8px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
358
  .acc-body {{ }}
@@ -600,7 +601,8 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
600
  @media (max-width: 800px) {{
601
  .grid-3, .grid-env {{ grid-template-columns: 1fr; }}
602
  .header {{ flex-direction: column; align-items: flex-start; gap: 16px; }}
603
- .header-actions {{ width: 100%; justify-content: flex-start; }}
 
604
  .ep-table td {{ display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }}
605
  .ep-desc {{ margin-left: 0; }}
606
  }}
@@ -614,8 +616,11 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
614
  <div class="subtitle">多账户代理面板</div>
615
  </div>
616
  <div class="header-actions">
 
617
  <a href="/public/log/html" class="btn" target="_blank">📄 公开日志</a>
618
  <a href="/{main.PATH_PREFIX}/admin/log/html?key={main.ADMIN_KEY}" class="btn" target="_blank">🔧 管理日志</a>
 
 
619
  <button class="btn" onclick="showEditConfig()" id="edit-btn">✏️ 编辑配置</button>
620
  </div>
621
  </div>
@@ -627,7 +632,10 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
627
 
628
  <div class="section">
629
  <div class="section-title">账户状态 ({len(multi_account_mgr.accounts)} 个)</div>
630
- <div style="color: #6b6b6b; font-size: 12px; margin-bottom: 12px; padding-left: 4px;">过期时间为12小时,可以自行修改时间,脚本可能有误差。</div>
 
 
 
631
  <div class="account-grid">
632
  {accounts_html if accounts_html else '<div class="card"><p style="color: #6b6b6b; font-size: 14px; text-align:center;">暂无账户</p></div>'}
633
  </div>
@@ -808,6 +816,16 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
808
  <td><span class="ep-path">/public/log/html</span></td>
809
  <td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
810
  </tr>
 
 
 
 
 
 
 
 
 
 
811
  <tr>
812
  <td><span class="method m-get">GET</span></td>
813
  <td><span class="ep-path">/docs</span></td>
@@ -846,6 +864,7 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
846
  </div>
847
  </div>
848
 
 
849
  <script>
850
  let currentConfig = null;
851
 
@@ -985,6 +1004,90 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
985
  }}
986
  }}
987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
988
  // 点击模态框外部关闭
989
  document.getElementById('jsonModal').addEventListener('click', function(e) {{
990
  if (e.target === this) {{
@@ -2094,3 +2197,185 @@ async def get_public_logs_html():
2094
  </html>
2095
  """
2096
  return HTMLResponse(content=html_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
  /* Account & Env Styles */
353
  .account-card .acc-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
354
+ .acc-title {{ font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; overflow: hidden; }}
355
+ .acc-title span:last-child {{ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 120px; }}
356
+ .status-dot {{ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }}
357
  .acc-status {{ font-size: 12px; font-weight: 600; }}
358
  .acc-actions {{ display: flex; gap: 8px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
359
  .acc-body {{ }}
 
601
  @media (max-width: 800px) {{
602
  .grid-3, .grid-env {{ grid-template-columns: 1fr; }}
603
  .header {{ flex-direction: column; align-items: flex-start; gap: 16px; }}
604
+ .header-actions {{ width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }}
605
+ .header-actions .btn {{ justify-content: center; text-align: center; }}
606
  .ep-table td {{ display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }}
607
  .ep-desc {{ margin-left: 0; }}
608
  }}
 
616
  <div class="subtitle">多账户代理面板</div>
617
  </div>
618
  <div class="header-actions">
619
+ <a href="/public/uptime/html" class="btn" target="_blank">📊 状态监控</a>
620
  <a href="/public/log/html" class="btn" target="_blank">📄 公开日志</a>
621
  <a href="/{main.PATH_PREFIX}/admin/log/html?key={main.ADMIN_KEY}" class="btn" target="_blank">🔧 管理日志</a>
622
+ <button class="btn" onclick="document.getElementById('fileInput').click()">📥 批量上传</button>
623
+ <input type="file" id="fileInput" accept=".json" multiple style="display:none" onchange="handleFileUpload(event)">
624
  <button class="btn" onclick="showEditConfig()" id="edit-btn">✏️ 编辑配置</button>
625
  </div>
626
  </div>
 
632
 
633
  <div class="section">
634
  <div class="section-title">账户状态 ({len(multi_account_mgr.accounts)} 个)</div>
635
+ <div style="color: #6b6b6b; font-size: 12px; margin-bottom: 12px; padding-left: 4px;">
636
+ 过期时间为12小时,可以自行修改时间,脚本可能有误差。<br>
637
+ 批量上传格式:<code style="font-size: 11px;">[{{"secure_c_ses": "...", "csesidx": "...", "config_id": "...", "id": "account_1"}}]</code>(id 可选)
638
+ </div>
639
  <div class="account-grid">
640
  {accounts_html if accounts_html else '<div class="card"><p style="color: #6b6b6b; font-size: 14px; text-align:center;">暂无账户</p></div>'}
641
  </div>
 
816
  <td><span class="ep-path">/public/log/html</span></td>
817
  <td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
818
  </tr>
819
+ <tr>
820
+ <td><span class="method m-get">GET</span></td>
821
+ <td><span class="ep-path">/public/uptime</span></td>
822
+ <td><span class="ep-desc">实时状态监控 (JSON)</span></td>
823
+ </tr>
824
+ <tr>
825
+ <td><span class="method m-get">GET</span></td>
826
+ <td><span class="ep-path">/public/uptime/html</span></td>
827
+ <td><span class="ep-desc">实时状态监控页面 (HTML)</span></td>
828
+ </tr>
829
  <tr>
830
  <td><span class="method m-get">GET</span></td>
831
  <td><span class="ep-path">/docs</span></td>
 
864
  </div>
865
  </div>
866
 
867
+
868
  <script>
869
  let currentConfig = null;
870
 
 
1004
  }}
1005
  }}
1006
 
1007
+ // 批量上传相关函数
1008
+ async function handleFileUpload(event) {{
1009
+ const files = event.target.files;
1010
+ if (!files.length) return;
1011
+
1012
+ let newAccounts = [];
1013
+ for (const file of files) {{
1014
+ try {{
1015
+ const text = await file.text();
1016
+ const data = JSON.parse(text);
1017
+ if (Array.isArray(data)) {{
1018
+ newAccounts.push(...data);
1019
+ }} else {{
1020
+ newAccounts.push(data);
1021
+ }}
1022
+ }} catch (e) {{
1023
+ alert(`文件 ${{file.name}} 解析失败: ${{e.message}}`);
1024
+ event.target.value = '';
1025
+ return;
1026
+ }}
1027
+ }}
1028
+
1029
+ if (!newAccounts.length) {{
1030
+ alert('未找到有效账户数据');
1031
+ event.target.value = '';
1032
+ return;
1033
+ }}
1034
+
1035
+ try {{
1036
+ // 获取现有配置
1037
+ const configResp = await fetch('/{main.PATH_PREFIX}/admin/accounts-config?key={main.ADMIN_KEY}');
1038
+ const configData = await handleApiResponse(configResp);
1039
+ const existing = configData.accounts || [];
1040
+
1041
+ // 构建ID到索引的映射
1042
+ const idToIndex = new Map();
1043
+ existing.forEach((acc, idx) => {{
1044
+ if (acc.id) idToIndex.set(acc.id, idx);
1045
+ }});
1046
+
1047
+ // 合并:相同ID覆盖,新ID追加
1048
+ let added = 0;
1049
+ let updated = 0;
1050
+ for (const acc of newAccounts) {{
1051
+ if (!acc.secure_c_ses || !acc.csesidx || !acc.config_id) continue;
1052
+ const accId = acc.id || `account_${{existing.length + added + 1}}`;
1053
+ acc.id = accId;
1054
+
1055
+ if (idToIndex.has(accId)) {{
1056
+ // 覆盖已存在的账户
1057
+ existing[idToIndex.get(accId)] = acc;
1058
+ updated++;
1059
+ }} else {{
1060
+ // 追加新账户
1061
+ existing.push(acc);
1062
+ idToIndex.set(accId, existing.length - 1);
1063
+ added++;
1064
+ }}
1065
+ }}
1066
+
1067
+ if (added === 0 && updated === 0) {{
1068
+ alert('没有有效账户可导入');
1069
+ event.target.value = '';
1070
+ return;
1071
+ }}
1072
+
1073
+ // 保存合并后的配置
1074
+ const response = await fetch('/{main.PATH_PREFIX}/admin/accounts-config?key={main.ADMIN_KEY}', {{
1075
+ method: 'PUT',
1076
+ headers: {{'Content-Type': 'application/json'}},
1077
+ body: JSON.stringify(existing)
1078
+ }});
1079
+
1080
+ const result = await handleApiResponse(response);
1081
+ alert(`导入完成!\\n新增: ${{added}} 个\\n覆盖: ${{updated}} 个\\n当前账户数: ${{result.account_count}}`);
1082
+ event.target.value = '';
1083
+ setTimeout(refreshPage, 1000);
1084
+ }} catch (error) {{
1085
+ console.error('导入失败:', error);
1086
+ alert('导入失败: ' + error.message);
1087
+ event.target.value = '';
1088
+ }}
1089
+ }}
1090
+
1091
  // 点击模态框外部关闭
1092
  document.getElementById('jsonModal').addEventListener('click', function(e) {{
1093
  if (e.target === this) {{
 
2197
  </html>
2198
  """
2199
  return HTMLResponse(content=html_content)
2200
+
2201
+
2202
+ async def get_uptime_html():
2203
+ """Uptime 实时监控页面(类似 Uptime Kuma)"""
2204
+ html_content = """
2205
+ <!DOCTYPE html>
2206
+ <html lang="zh-CN">
2207
+ <head>
2208
+ <meta charset="UTF-8">
2209
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2210
+ <title>Gemini Status</title>
2211
+ <style>
2212
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2213
+ body {
2214
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2215
+ background: #f5f5f7;
2216
+ color: #1d1d1f;
2217
+ min-height: 100vh;
2218
+ padding: 20px;
2219
+ }
2220
+ .container { max-width: 1200px; margin: 0 auto; }
2221
+ h1 {
2222
+ font-size: 24px;
2223
+ font-weight: 600;
2224
+ margin-bottom: 8px;
2225
+ color: #1d1d1f;
2226
+ }
2227
+ .subtitle { color: #86868b; font-size: 14px; margin-bottom: 24px; }
2228
+ .update-time { color: #86868b; font-size: 12px; margin-bottom: 16px; }
2229
+ .grid {
2230
+ display: grid;
2231
+ grid-template-columns: repeat(2, 1fr);
2232
+ gap: 16px;
2233
+ }
2234
+ .card {
2235
+ background: #fff;
2236
+ border: 1px solid #e5e5e5;
2237
+ border-radius: 12px;
2238
+ padding: 16px;
2239
+ box-shadow: 0 1px 3px rgba(0,0,0,0.04);
2240
+ }
2241
+ .card:hover { border-color: #d4d4d4; }
2242
+ .card-header {
2243
+ display: flex;
2244
+ justify-content: space-between;
2245
+ align-items: center;
2246
+ margin-bottom: 12px;
2247
+ }
2248
+ .service-name { font-weight: 600; font-size: 14px; color: #1d1d1f; }
2249
+ .status-badge {
2250
+ padding: 2px 8px;
2251
+ border-radius: 12px;
2252
+ font-size: 11px;
2253
+ font-weight: 600;
2254
+ }
2255
+ .status-up { background: #d1fae5; color: #065f46; }
2256
+ .status-down { background: #fee2e2; color: #991b1b; }
2257
+ .status-unknown { background: #f3f4f6; color: #6b7280; }
2258
+ .stats {
2259
+ display: flex;
2260
+ gap: 16px;
2261
+ margin-bottom: 12px;
2262
+ font-size: 12px;
2263
+ color: #86868b;
2264
+ }
2265
+ .stat-value { color: #1d1d1f; font-weight: 600; }
2266
+ .heartbeat-bar {
2267
+ display: flex;
2268
+ gap: 2px;
2269
+ height: 24px;
2270
+ align-items: flex-end;
2271
+ }
2272
+ .beat {
2273
+ flex: 1;
2274
+ min-width: 4px;
2275
+ max-width: 8px;
2276
+ border-radius: 2px;
2277
+ transition: all 0.2s;
2278
+ position: relative;
2279
+ }
2280
+ .beat:hover { opacity: 0.8; transform: scaleY(1.1); }
2281
+ .beat.up { background: #34c759; height: 100%; }
2282
+ .beat.down { background: #ff3b30; height: 100%; }
2283
+ .beat.empty { background: #e5e5ea; height: 40%; }
2284
+ .beat .tooltip {
2285
+ position: absolute;
2286
+ bottom: 100%;
2287
+ left: 50%;
2288
+ transform: translateX(-50%);
2289
+ background: #1d1d1f;
2290
+ color: #fff;
2291
+ padding: 6px 10px;
2292
+ border-radius: 6px;
2293
+ font-size: 11px;
2294
+ white-space: nowrap;
2295
+ opacity: 0;
2296
+ pointer-events: none;
2297
+ transition: opacity 0.15s;
2298
+ margin-bottom: 6px;
2299
+ z-index: 100;
2300
+ }
2301
+ .beat .tooltip::after {
2302
+ content: '';
2303
+ position: absolute;
2304
+ top: 100%;
2305
+ left: 50%;
2306
+ transform: translateX(-50%);
2307
+ border: 5px solid transparent;
2308
+ border-top-color: #1d1d1f;
2309
+ }
2310
+ .beat:hover .tooltip { opacity: 1; }
2311
+ @media (max-width: 768px) {
2312
+ .grid { grid-template-columns: 1fr; }
2313
+ .beat { min-width: 3px; max-width: 6px; }
2314
+ }
2315
+ </style>
2316
+ </head>
2317
+ <body>
2318
+ <div class="container">
2319
+ <h1>Gemini Status</h1>
2320
+ <p class="subtitle">服务状态监控</p>
2321
+ <p class="update-time" id="update-time">更新中...</p>
2322
+ <div class="grid" id="services"></div>
2323
+ </div>
2324
+ <script>
2325
+ async function loadStatus() {
2326
+ try {
2327
+ const res = await fetch('/public/uptime');
2328
+ const data = await res.json();
2329
+ renderServices(data);
2330
+ document.getElementById('update-time').textContent = '更新于 ' + data.updated_at;
2331
+ } catch (e) {
2332
+ document.getElementById('services').innerHTML = '<div class="card">加载失败</div>';
2333
+ }
2334
+ }
2335
+
2336
+ function renderServices(data) {
2337
+ const container = document.getElementById('services');
2338
+ let html = '';
2339
+ for (const [id, svc] of Object.entries(data.services)) {
2340
+ const statusClass = svc.status === 'up' ? 'status-up' : svc.status === 'down' ? 'status-down' : 'status-unknown';
2341
+ const statusText = svc.status === 'up' ? '正常' : svc.status === 'down' ? '故障' : '未知';
2342
+
2343
+ // 生成心跳条
2344
+ let beats = '';
2345
+ const maxBeats = 60;
2346
+ const heartbeats = svc.heartbeats || [];
2347
+ for (let i = 0; i < maxBeats; i++) {
2348
+ if (i < heartbeats.length) {
2349
+ const beat = heartbeats[i];
2350
+ const status = beat.success ? '成功' : '失败';
2351
+ beats += `<div class="beat ${beat.success ? 'up' : 'down'}"><span class="tooltip">${beat.time} · ${status}</span></div>`;
2352
+ } else {
2353
+ beats += '<div class="beat empty"></div>';
2354
+ }
2355
+ }
2356
+
2357
+ html += `
2358
+ <div class="card">
2359
+ <div class="card-header">
2360
+ <span class="service-name">${svc.name}</span>
2361
+ <span class="status-badge ${statusClass}">${statusText}</span>
2362
+ </div>
2363
+ <div class="stats">
2364
+ <span>可用率 <span class="stat-value">${svc.uptime}%</span></span>
2365
+ <span>请求 <span class="stat-value">${svc.total}</span></span>
2366
+ <span>成功 <span class="stat-value">${svc.success}</span></span>
2367
+ </div>
2368
+ <div class="heartbeat-bar">${beats}</div>
2369
+ </div>
2370
+ `;
2371
+ }
2372
+ container.innerHTML = html;
2373
+ }
2374
+
2375
+ loadStatus();
2376
+ setInterval(loadStatus, 5000);
2377
+ </script>
2378
+ </body>
2379
+ </html>
2380
+ """
2381
+ return HTMLResponse(content=html_content)