creditdaddy 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/panel.html CHANGED
@@ -48,6 +48,16 @@
48
48
  .comp-menu-title { font-size: 11px; color: var(--text-3); margin: 10px 0 4px; padding-top: 8px; border-top: 1px dashed var(--border-subtle); }
49
49
  .comp-menu-title:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
50
50
  .comp-menu .check { margin-right: 0; font-size: 12.5px; color: var(--text); }
51
+ /* 产品页 notice 的功能分区行 */
52
+ .z-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
53
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-3); opacity: .45; flex: none; }
54
+ .dot.on { background: var(--ok); opacity: 1; box-shadow: 0 0 5px var(--ok); }
55
+ .dot.pulse { animation: dot-pulse 2.2s ease-out infinite; }
56
+ @keyframes dot-pulse { 0% { box-shadow: 0 0 0 0 rgba(46, 229, 157, .45); } 70% { box-shadow: 0 0 0 5px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
57
+ .z-row + .z-row { margin-top: 7px; padding-top: 7px; border-top: 1px dashed var(--border-subtle); }
58
+ .z-label { font-size: 11px; color: var(--text-3); flex: none; width: 68px; }
59
+ .z-text { flex: 1 1 320px; min-width: 0; }
60
+ /* 10Router 状态条分两行 */
51
61
  .sec-status { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--text-3); }
52
62
  .sec-status.on { color: var(--ok); border-color: rgba(var(--accent-rgb), .45); }
53
63
  .comp-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
@@ -95,9 +105,9 @@
95
105
  .notice { margin: 12px 16px 0; padding: 9px 12px; border-radius: 10px; background: var(--warn-soft); color: var(--warn); font-size: 12px; line-height: 1.6; }
96
106
 
97
107
  /* cards */
98
- .list { display: grid; grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); gap: 4px 10px; padding: 10px; }
99
- .card { position: relative; padding: 11px 12px 12px; border-radius: 12px; transition: background .12s; min-width: 0; }
100
- .card:hover { background: var(--hover); }
108
+ .list { display: grid; grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); gap: 10px; padding: 10px; }
109
+ .card { position: relative; padding: 11px 12px 12px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border-subtle); transition: background .12s, border-color .12s; min-width: 0; }
110
+ .card:hover { background: var(--hover); border-color: var(--border-strong); }
101
111
  .row1 { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 28px; margin-bottom: 7px; }
102
112
  .name-group { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; }
103
113
  .name { font-size: 14px; font-weight: 600; cursor: text; border-radius: 5px; padding: 0 2px; margin: 0 -2px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -114,6 +124,10 @@
114
124
  html[data-theme="dark"] .badge.prod.zcode { color: #93c5fd; }
115
125
  .badge.prod.catpaw { color: #c2410c; background: rgba(249,115,22,.14); }
116
126
  html[data-theme="dark"] .badge.prod.catpaw { color: #fdba74; }
127
+ .badge.prod.trae { color: #047857; background: rgba(16,185,129,.14); }
128
+ html[data-theme="dark"] .badge.prod.trae { color: #6ee7b7; }
129
+ .badge.prod.minimax { color: #e11d48; background: rgba(244,63,94,.14); }
130
+ html[data-theme="dark"] .badge.prod.minimax { color: #fda4af; }
117
131
  .seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
118
132
  .seg-ctl button { border: none; background: transparent; color: var(--text-2); font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 7px; cursor: pointer; }
119
133
  .seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
@@ -160,6 +174,12 @@
160
174
  /* section card */
161
175
  .pcard { margin-top: 14px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 12px 14px; }
162
176
  .pcard-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 700; }
177
+ .fold-head { cursor: pointer; user-select: none; }
178
+ .fold-side { display: inline-flex; align-items: center; gap: 6px; color: var(--text-3); }
179
+ .fold-ico { display: inline-flex; }
180
+ .fold-ico svg { width: 14px; height: 14px; }
181
+ .pcard .fold-body { display: none; margin-top: 4px; }
182
+ .pcard.open .fold-body { display: block; }
163
183
  .logs { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; line-height: 1.65; max-height: 240px; overflow-y: auto; background: var(--tertiary); border-radius: 9px; padding: 8px 10px; }
164
184
  .logs .info { color: var(--text-2); } .logs .warn { color: var(--warn); } .logs .error { color: var(--err); } .logs .debug { color: var(--text-3); }
165
185
  .collapsed .logs { display: none; }
@@ -187,8 +207,9 @@
187
207
  .hint { font-size: 12px; color: var(--text-3); line-height: 1.7; }
188
208
  .hint a { color: #4f86ff; word-break: break-all; }
189
209
  .box { border: 1px solid var(--border-subtle); border-radius: 10px; padding: 10px 12px; margin-top: 10px; background: var(--surface-2); }
190
- .cand { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border-subtle); }
191
- .cand:last-child { border-bottom: none; }
210
+ .cand { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border: 1px solid var(--border-subtle); border-radius: 10px; background: var(--surface); }
211
+ .cand + .cand { margin-top: 8px; }
212
+ .cand .row { flex: none; flex-wrap: nowrap; gap: 6px; }
192
213
  .cand-main { min-width: 0; }
193
214
  .cand-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
194
215
  .cand-sub { font-size: 11px; color: var(--text-3); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -206,8 +227,15 @@
206
227
  .view { display: none; } .view.active { display: block; }
207
228
 
208
229
  /* 仪表盘 */
209
- .dash-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
210
- .dash-title { font-size: 15px; font-weight: 700; }
230
+ .dash-panel { margin-bottom: 12px; }
231
+ .dash-title { font-size: 13px; font-weight: 700; }
232
+ .dash-panel .toolbar { padding: 8px 16px; }
233
+ .dash-panel .kpis { display: flex; gap: 0; margin: 0; padding: 0 10px 8px; }
234
+ .dash-panel .kpi { background: none; border: none; border-radius: 0; padding: 6px 14px; flex: 1 1 0; min-width: 0; }
235
+ .dash-panel .kpi-value { font-size: 22px; margin-top: 2px; }
236
+ .dash-panel .kpi-sub { margin-top: 2px; }
237
+ .dash-panel .kpi + .kpi { border-left: 1px dashed var(--border-subtle); }
238
+ @media (max-width: 900px) { .dash-panel .kpis { flex-wrap: wrap; } .dash-panel .kpi { flex: 1 1 40%; } .dash-panel .kpi + .kpi { border-left: none; } }
211
239
  .kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; }
212
240
  .kpi { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; min-width: 0; }
213
241
  .kpi-label { color: var(--text-3); font-size: 12px; font-weight: 500; }
@@ -221,7 +249,7 @@
221
249
  .prod-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
222
250
  .prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
223
251
  .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
224
- .dot.qoder { background: #7c3aed; } .dot.workbuddy { background: #14b8a6; } .dot.zcode { background: #2563eb; } .dot.mirasim { background: #64748b; } .dot.catpaw { background: #f97316; }
252
+ .dot.qoder { background: #7c3aed; } .dot.workbuddy { background: #14b8a6; } .dot.zcode { background: #2563eb; } .dot.mirasim { background: #64748b; } .dot.catpaw { background: #f97316; } .dot.trae { background: #10b981; } .dot.minimax { background: #f43f5e; }
225
253
  .prod-go { color: var(--text-3); font-size: 12px; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; }
226
254
  .prod-go svg { width: 12px; height: 12px; transition: transform .15s; }
227
255
  .prodcard:hover .prod-go { color: var(--text); }
@@ -253,7 +281,7 @@
253
281
  .tr-vrow { }
254
282
  .tr-vhead { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
255
283
  .tr-vhead .dim { font-size: 12px; }
256
- .tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 12px 4px; }
284
+ .tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 14px 12px 4px; }
257
285
  .tr-vcards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; align-items: stretch; }
258
286
  @media (max-width: 1180px) { .tr-vcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
259
287
  @media (max-width: 820px) { .tr-vcards { grid-template-columns: 1fr; } }
@@ -297,7 +325,6 @@
297
325
  .tr-packs-head { font-size: 11px; color: var(--text-3); margin-bottom: 2px; }
298
326
  .tr-packs-head b { color: var(--text-2); }
299
327
  .tr-more { margin-top: 7px; border: none; background: none; color: #4f86ff; font-size: 12px; cursor: pointer; padding: 0; }
300
- .tr-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); font-size: 12px; color: var(--text-2); }
301
328
  .chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: var(--tertiary); color: var(--text-3); }
302
329
  .chip.on { background: rgba(var(--accent-rgb), .14); color: var(--text); }
303
330
  .setup { padding: 22px; max-width: 620px; }
@@ -337,25 +364,29 @@
337
364
  <button data-view="zcode">ZCode<span class="cnt" id="cnt-zcode">0</span></button>
338
365
  <button data-view="mirasim">mirasim<span class="cnt" id="cnt-mirasim">0</span></button>
339
366
  <button data-view="catpaw">妙手<span class="cnt" id="cnt-catpaw">0</span></button>
367
+ <button data-view="trae">Trae<span class="cnt" id="cnt-trae">0</span></button>
368
+ <button data-view="minimax">MiniMax<span class="cnt" id="cnt-minimax">0</span></button>
340
369
  <button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
341
370
  </nav>
342
371
 
343
372
  <!-- 仪表盘 -->
344
373
  <section class="view active" id="view-dash">
345
- <div class="dash-head">
346
- <div class="dash-title">总览</div>
347
- <div class="actions">
348
- <button class="btn primary" id="btn-checkin-all"></button>
349
- <button class="btn" id="btn-add-dash"></button>
350
- <button class="btn" id="btn-import-dash"></button>
351
- <button class="btn" id="btn-export-dash"></button>
374
+ <div class="panel dash-panel">
375
+ <div class="toolbar">
376
+ <div class="summary"><span class="dash-title">总览</span></div>
377
+ <div class="actions">
378
+ <button class="btn primary" id="btn-checkin-all"></button>
379
+ <button class="btn" id="btn-add-dash"></button>
380
+ <button class="btn" id="btn-import-dash"></button>
381
+ <button class="btn" id="btn-export-dash"></button>
382
+ </div>
352
383
  </div>
384
+ <div class="kpis" id="kpis"></div>
353
385
  </div>
354
- <div class="kpis" id="kpis"></div>
355
386
  <div class="prods" id="prods"></div>
356
387
  <div class="dash-cols">
357
- <section class="pcard"><div class="pcard-title"><span>最近记录</span><span class="hint" id="last-tick"></span></div><div id="recent"></div></section>
358
- <section class="pcard"><div class="pcard-title"><span>需要处理</span><span class="hint" id="att-count"></span></div><div id="attention"></div></section>
388
+ <section class="pcard" data-fold="recent"><div class="pcard-title fold-head"><span>最近记录</span><span class="fold-side"><span class="hint" id="last-tick"></span><span class="fold-ico"></span></span></div><div class="fold-body"><div id="recent"></div></div></section>
389
+ <section class="pcard" data-fold="attention"><div class="pcard-title fold-head"><span>需要处理</span><span class="fold-side"><span class="hint" id="att-count"></span><span class="fold-ico"></span></span></div><div class="fold-body"><div id="attention"></div></div></section>
359
390
  </div>
360
391
  </section>
361
392
 
@@ -378,6 +409,19 @@
378
409
  </div>
379
410
  <div class="actions">
380
411
  <button class="btn primary" id="btn-checkin"></button>
412
+ <span id="z-actions" style="display:none">
413
+ <button class="btn" id="zt-ac"></button>
414
+ <button class="btn" id="zt-net"></button>
415
+ <button class="btn" id="zt-gw"></button>
416
+ <button class="btn" id="zt-open" style="display:none"></button>
417
+ </span>
418
+ <span id="t-actions" style="display:none">
419
+ <button class="btn" id="tt-open" style="display:none"></button>
420
+ </span>
421
+ <span id="m-actions" style="display:none">
422
+ <button class="btn" id="mt-gw"></button>
423
+ <button class="btn" id="mt-open" style="display:none"></button>
424
+ </span>
381
425
  <button class="btn" id="btn-add"></button>
382
426
  <button class="btn" id="btn-import"></button>
383
427
  <button class="btn" id="btn-export"></button>
@@ -393,9 +437,14 @@
393
437
  <div class="panel" id="tr-panel"><div class="empty">加载中…</div></div>
394
438
  </section>
395
439
 
440
+ <!-- 日志卡:运行日志 + 各网关调用日志,卡内标签切换(默认跟随当前页,可手动切) -->
396
441
  <section class="pcard collapsed" id="log-card">
397
442
  <div class="pcard-title">
398
- <span>运行日志</span>
443
+ <div class="seg-ctl" id="log-tabs">
444
+ <button data-log-tab="run" class="active">运行日志</button>
445
+ <button data-log-tab="zcode">ZCode 网关</button>
446
+ <button data-log-tab="minimax">MiniMax 网关</button>
447
+ </div>
399
448
  <button class="btn" id="btn-logs">展开</button>
400
449
  </div>
401
450
  <div class="logs" id="logs">(暂无)</div>
@@ -426,6 +475,7 @@
426
475
  <option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option>
427
476
  <option value="workbuddy">WorkBuddy / CodeBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy / CodeBuddy 国际版</option>
428
477
  <option value="zcode-bigmodel">ZCode · BigModel(智谱)</option><option value="zcode-zai">ZCode · Z.ai(国际)</option>
478
+ <option value="minimax">MiniMax Code</option>
429
479
  </select>
430
480
  </div>
431
481
  <div class="row"><button class="btn primary" id="btn-device">生成授权链接</button><button class="btn" id="dev-cancel" style="display:none">取消</button></div>
@@ -438,7 +488,7 @@
438
488
  <div class="pane" data-pane="token">
439
489
  <div class="field">
440
490
  <label>版本</label>
441
- <select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy">WorkBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy 国际版</option></select>
491
+ <select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy">WorkBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy 国际版</option><option value="minimax">MiniMax</option></select>
442
492
  </div>
443
493
  <div class="field"><label>备注名(可选)</label><input class="input" id="f-name" placeholder="留空则自动读取昵称"></div>
444
494
  <div class="field"><label>Token</label><input class="input mono" id="f-token" placeholder="dt-… / jt-… 或 PAT(pt-…,自动兑换)"></div>
@@ -453,7 +503,7 @@
453
503
  <div class="modal-title"><span>导出账号</span><button class="close" data-close>✕</button></div>
454
504
  <div class="field">
455
505
  <label>范围</label>
456
- <select class="input" id="ex-provider"><option value="">全部账号</option><option value="p:qoder">Qoder(全部)</option><option value="qoder">仅 Qoder 国际版</option><option value="qoder-cn">仅 Qoder 国内版</option><option value="p:workbuddy">WorkBuddy(全部)</option><option value="workbuddy">仅 WorkBuddy 国内版</option><option value="workbuddy-intl">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option><option value="p:mirasim">mirasim</option><option value="p:catpaw">妙手</option></select>
506
+ <select class="input" id="ex-provider"><option value="">全部账号</option><option value="p:qoder">Qoder(全部)</option><option value="qoder">仅 Qoder 国际版</option><option value="qoder-cn">仅 Qoder 国内版</option><option value="p:workbuddy">WorkBuddy(全部)</option><option value="workbuddy">仅 WorkBuddy 国内版</option><option value="workbuddy-intl">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option><option value="p:mirasim">mirasim</option><option value="p:catpaw">妙手</option><option value="p:trae">Trae</option><option value="p:minimax">MiniMax</option></select>
457
507
  </div>
458
508
  <div class="field">
459
509
  <label>加密口令(必填)</label>
@@ -479,26 +529,34 @@
479
529
  <div class="modal">
480
530
  <div class="modal-title"><span>面板设置</span><button class="close" data-close>✕</button></div>
481
531
  <div class="comp-menu" id="comp-menu"></div>
532
+ <div class="field" id="upd-entry" style="display:none;margin-top:12px">
533
+ <label>版本与更新</label>
534
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><span class="dim" id="upd-ver"></span><button class="btn" id="btn-update">检查更新</button></div>
535
+ </div>
482
536
  <div class="modal-actions"><button class="btn" data-close>关闭</button></div>
483
537
  </div>
484
538
  </div>
485
539
 
540
+ <!-- 检查更新(桌面版):状态由主进程 update:state 推送 -->
541
+ <div class="mask" id="m-update">
542
+ <div class="modal sm">
543
+ <div class="modal-title"><span>检查更新</span><button class="close" data-close>✕</button></div>
544
+ <div id="upd-body"></div>
545
+ <div class="modal-actions" id="upd-actions"></div>
546
+ </div>
547
+ </div>
548
+
486
549
  <!-- 10Router 设置 -->
487
550
  <div class="mask" id="m-10r">
488
551
  <div class="modal">
489
552
  <div class="modal-title"><span>10Router 连接设置</span><button class="close" data-close>✕</button></div>
490
553
  <div class="hint">填 10Router 的访问地址和仪表盘里创建的<b>虚拟 key(sk-…)</b>。key 只保存在 CreditDaddy 守护进程的数据目录(0600),面板里只显示脱敏值。</div>
491
- <div class="field" style="margin-top:12px"><label>10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128 或 http://nas:20128"></div>
554
+ <div class="field" style="margin-top:12px"><label>10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128(站点根地址,不要带 /v1)"></div>
492
555
  <div class="field"><label>虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
493
556
  <div class="field"><label>10Router 面板密码(可选)</label><input class="input mono" id="tr-admin-pw" type="password" placeholder="配置后「同步账号」走 OAuth 通道(带 refreshToken 自动续期);留空保持不变" autocomplete="new-password"></div>
494
557
  <div class="field"><label>账号同步</label>
495
558
  <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button class="btn" id="tr-sync-accounts">同步账号到 10R</button><span class="hint">把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接;zcode 待反代就绪后支持</span></div>
496
559
  </div>
497
- <div class="field"><label>用量同步(同 10router-sync 插件)</label>
498
- <div><label class="check"><input type="checkbox" id="tr-auto"> 每小时自动同步本机用量到 10Router</label></div>
499
- <div id="tr-sources" style="margin-top:6px"></div>
500
- <div class="hint" style="margin-top:4px">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。「10Router 本机」只把实例原生产生的行标记为网关同步。10Router 按行签名去重,重复同步不会产生重复数据。</div>
501
- </div>
502
560
  <div class="field"><label>显示</label>
503
561
  <div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
504
562
  </div>
@@ -511,6 +569,18 @@
511
569
  </div>
512
570
  </div>
513
571
 
572
+ <!-- 10Router 用量同步 -->
573
+ <div class="mask" id="m-trsync">
574
+ <div class="modal sm">
575
+ <div class="modal-title"><span>用量同步(同 10router-sync 插件)</span><button class="close" data-close>✕</button></div>
576
+ <div class="field"><label class="check"><input type="checkbox" id="tr-auto"> 每小时自动同步本机用量到 10Router</label></div>
577
+ <div class="field"><label>同步来源</label><div id="tr-sources"></div></div>
578
+ <div class="hint">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。「10Router 本机」只把实例原生产生的行标记为网关同步。10Router 按行签名去重,重复同步不会产生重复数据。</div>
579
+ <div class="hint" id="tr-sync-status" style="margin-top:8px"></div>
580
+ <div class="modal-actions"><button class="btn" style="margin-right:auto" id="tr-sync-now">立即同步一次</button><button class="btn" data-close>取消</button><button class="btn primary" id="tr-sync-save">保存</button></div>
581
+ </div>
582
+ </div>
583
+
514
584
  <!-- ZCode 活动领取 -->
515
585
  <div class="mask" id="m-claim">
516
586
  <div class="modal">
@@ -530,6 +600,77 @@
530
600
  </div>
531
601
  </div>
532
602
 
603
+ <!-- ZCode 自动领取计划 -->
604
+ <div class="mask" id="m-zac">
605
+ <div class="modal sm">
606
+ <div class="modal-title"><span>ZCode 自动领取计划</span><button class="close" data-close>✕</button></div>
607
+ <div class="field">
608
+ <label>轮询周期(分钟,1–720)</label>
609
+ <input class="input" id="zac-interval" type="number" min="1" max="720" step="1">
610
+ </div>
611
+ <div class="field">
612
+ <label>单次运行时长(分钟,0 = 一直运行直到手动关闭)</label>
613
+ <input class="input" id="zac-window" type="number" min="0" max="1440" step="5">
614
+ </div>
615
+ <div class="hint">开启后按此周期检查活动资格并自动领取(首次检查在开启 2 分钟后开始)。活动包由服务端在每日 0 点左右刷新放出——若运行时段没有覆盖 0 点,会错过当次领取,建议把运行时长设为 0,或让时段跨过 0 点。需要验证码时无法自动通过,卡片上会提示手动领取。</div>
616
+ <div class="modal-actions"><button class="btn" id="zac-off" style="display:none">停用</button><button class="btn" data-close>取消</button><button class="btn primary" id="zac-ok">保存并启用</button></div>
617
+ </div>
618
+ </div>
619
+
620
+ <!-- ZCode 网络出口 -->
621
+ <div class="mask" id="m-znet">
622
+ <div class="modal sm">
623
+ <div class="modal-title"><span>ZCode 网络出口</span><button class="close" data-close>✕</button></div>
624
+ <div class="field">
625
+ <label>上游接口访问顺序(失败 / 被拦时自动回退另一条)</label>
626
+ <label class="check"><input type="radio" name="znet-order" id="znet-direct"> 直连优先(代理兜底)</label>
627
+ <label class="check"><input type="radio" name="znet-order" id="znet-proxy"> 代理优先(直连兜底)</label>
628
+ </div>
629
+ <div class="field">
630
+ <label>出口代理(http://host:port,支持口令 http://user:pass@host:port)</label>
631
+ <input class="input" id="znet-url" placeholder="http://192.168.31.101:7890">
632
+ </div>
633
+ <div class="hint">留空 = 清除已保存的代理,退回 HTTPS_PROXY 环境变量;取消 = 不改动。当前代理只显示脱敏后的地址。</div>
634
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="znet-ok">保存</button></div>
635
+ </div>
636
+ </div>
637
+
638
+ <!-- ZCode 体验包接口 -->
639
+ <div class="mask" id="m-zgw">
640
+ <div class="modal sm">
641
+ <div class="modal-title"><span>ZCode 体验包接口</span><button class="close" data-close>✕</button></div>
642
+ <div class="hint" id="zgw-url" style="margin-bottom:10px"></div>
643
+ <div class="field">
644
+ <label class="check"><input type="checkbox" id="zgw-on"> 启用体验包接口(/gateway/v1/messages)</label>
645
+ </div>
646
+ <div class="field" id="zgw-lan-fields">
647
+ <label class="check"><input type="checkbox" id="zgw-lan"> 对局域网开放(重启后绑定 0.0.0.0)</label>
648
+ <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
649
+ <textarea class="input" id="zgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
650
+ </div>
651
+ <div class="hint">开启后,领到的体验包额度变成本机的一个补全接口——10Router 建一个 anthropic-compatible 节点指向本机 /gateway 即可调用(需要桌面版,隐藏窗口过验证码)。白名单内的机器<b>免密直连</b>:10Router 的 zcode-free 本就是免授权供应商,把 10Router 服务地址的 IP 加白即可;白名单外的请求一律拒绝。本机回环(127.0.0.1)始终放行。</div>
652
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="zgw-ok">保存</button></div>
653
+ </div>
654
+ </div>
655
+
656
+ <!-- MiniMax 本地接口 -->
657
+ <div class="mask" id="m-mgw">
658
+ <div class="modal sm">
659
+ <div class="modal-title"><span>MiniMax 本地接口</span><button class="close" data-close>✕</button></div>
660
+ <div class="hint" id="mgw-url" style="margin-bottom:10px"></div>
661
+ <div class="field">
662
+ <label class="check"><input type="checkbox" id="mgw-on"> 启用本地接口(/gateway/minimax/v1/messages)</label>
663
+ </div>
664
+ <div class="field" id="mgw-lan-fields">
665
+ <label class="check"><input type="checkbox" id="mgw-lan"> 对局域网开放(需全局局域网已开启)</label>
666
+ <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
667
+ <textarea class="input" id="mgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
668
+ </div>
669
+ <div class="hint">开启后,MiniMax 积分与模型能力变成本机的一个原生 Anthropic Messages 补全接口——在 10Router 或 Claude Code 等工具中建一个 anthropic-compatible 节点指向本机 <code>/gateway/minimax/v1/messages</code> 即可调用。支持多账号自动轮转、429 限流自动冷却 5 分钟、凭据失效自动静默刷新。白名单内的机器<b>免密直连</b>;本机回环(127.0.0.1)始终放行。</div>
670
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="mgw-ok">保存</button></div>
671
+ </div>
672
+ </div>
673
+
533
674
  <input type="file" id="file-input" accept=".json,application/json" style="display:none">
534
675
  <div id="toast"></div>
535
676
 
@@ -559,8 +700,8 @@ const I = {
559
700
  };
560
701
 
561
702
  const $ = (id) => document.getElementById(id);
562
- const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '' };
563
- const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'tenrouter'];
703
+ const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run' };
704
+ const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'tenrouter'];
564
705
  // 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
565
706
  const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
566
707
  const PRODUCTS = [
@@ -569,6 +710,8 @@ const PRODUCTS = [
569
710
  { id: 'zcode', label: 'ZCode' },
570
711
  { id: 'mirasim', label: 'mirasim' },
571
712
  { id: 'catpaw', label: '妙手' },
713
+ { id: 'trae', label: 'Trae' },
714
+ { id: 'minimax', label: 'MiniMax' },
572
715
  ];
573
716
  const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
574
717
 
@@ -579,6 +722,8 @@ const COMPONENTS = [
579
722
  { id: 'zcode', label: 'ZCode' },
580
723
  { id: 'mirasim', label: 'mirasim' },
581
724
  { id: 'catpaw', label: '妙手' },
725
+ { id: 'trae', label: 'Trae' },
726
+ { id: 'minimax', label: 'MiniMax' },
582
727
  { id: 'tenrouter', label: '10Router' },
583
728
  ];
584
729
  const HIDDEN_COMP_KEY = 'qd_hidden_components';
@@ -699,12 +844,16 @@ const PROV = {
699
844
  zcode: { prod: 'zcode', prodLabel: 'ZCode', region: '', cls: '' },
700
845
  mirasim: { prod: 'mirasim', prodLabel: 'mirasim', region: '', cls: '' },
701
846
  catpaw: { prod: 'catpaw', prodLabel: '妙手', region: '', cls: '' },
847
+ trae: { prod: 'trae', prodLabel: 'Trae', region: '', cls: '' },
848
+ minimax: { prod: 'minimax', prodLabel: 'MiniMax', region: '', cls: '' },
702
849
  };
703
850
  // 支持每日签到的产品(ZCode / mirasim / 妙手 额度靠套餐或滚动窗口发放,不做签到)
704
- const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy']);
705
- const CLIENT_LABEL = { workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手' };
851
+ const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy', 'trae', 'minimax']);
852
+ const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae', minimax: 'MiniMax' };
706
853
  // 额度单位为 Credits/时长而非「积分」口径的产品(不计入剩余积分合计)
707
854
  const CREDIT_VIEWS = new Set(['zcode', 'mirasim', 'catpaw']);
855
+ // 网关调用日志:tag → 所属产品页(无网关的产品不展示日志卡)
856
+ const GW_LOG_TAG = { zcode: 'ZCODE-GW', minimax: 'MINIMAX-GW' };
708
857
  const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
709
858
  const provBadges = (p, regionOnly, flavor) => {
710
859
  const v = provOf(p);
@@ -813,17 +962,10 @@ function applyTheme(t) {
813
962
  $('btn-theme').innerHTML = t === 'dark' ? I.sun : I.moon;
814
963
  }
815
964
  function initTheme() {
816
- const mq = window.matchMedia ? matchMedia('(prefers-color-scheme: dark)') : null;
817
965
  let t = null;
818
966
  try { t = localStorage.getItem('qd_theme'); } catch (e) {}
819
- if (!t) t = mq && mq.matches ? 'dark' : 'light';
967
+ if (!t) t = window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
820
968
  applyTheme(t);
821
- // 未手动锁定主题(localStorage 无记录)时,实时跟随系统日夜切换
822
- if (mq && mq.addEventListener) mq.addEventListener('change', (e) => {
823
- let saved = null;
824
- try { saved = localStorage.getItem('qd_theme'); } catch (err) {}
825
- if (!saved) applyTheme(e.matches ? 'dark' : 'light');
826
- });
827
969
  $('btn-theme').onclick = () => {
828
970
  const n = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
829
971
  applyTheme(n);
@@ -836,7 +978,8 @@ function initTheme() {
836
978
  function statusOf(a) {
837
979
  if (!CHECKIN_PRODUCTS.has(a.product)) return { cls: '', text: '', msg: '' }; // ZCode 无每日领取,不显示徽标
838
980
  const intl = a.provider === 'workbuddy-intl';
839
- const today = state.status && state.status.today;
981
+ const byProd = (state.status && state.status.todayByProduct) || {};
982
+ const today = byProd[a.product] || (state.status && state.status.today);
840
983
  const done = state.status && state.status.todayDone && state.status.todayDone[a.id] === today;
841
984
  const r = a.lastResult;
842
985
  const fresh = r && r.at && state.status && (Date.now() - new Date(r.at).getTime() < 26 * 3600e3);
@@ -998,13 +1141,14 @@ function cardHtml(a) {
998
1141
  + provBadges(a.provider, true, a.flavor)
999
1142
  + (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
1000
1143
  + (a.product === 'zcode' && isGatewayCurrent(a) ? '<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1144
+ + (a.product === 'minimax' && isMiniMaxGatewayCurrent(a) ? '<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1001
1145
  + (a.isPat ? '<span class="badge">PAT</span>' : '')
1002
1146
  + (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
1003
1147
  + (st.text ? '<span class="pill ' + st.cls + '" title="' + esc(a.lastResult ? fmtTime(a.lastResult.at) + ' ' + (a.lastResult.message || a.lastResult.status) : '') + '">' + esc(st.text) + '</span>' : '')
1004
1148
  + '</div><div class="ops">'
1005
1149
  + (a.product === 'zcode' ? '<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">' + I.gift + '</button>' : '')
1006
1150
  + (canCheckin ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + (a.provider === 'workbuddy-intl' ? '发送活跃请求' : '立即领取') + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
1007
- + (CLIENT_LABEL[a.product] && a.canSwitch && !isCurrent && st.cls !== 'fail' ? '<button class="icon-btn" data-act="switch" title="切换 ' + CLIENT_LABEL[a.product] + ' 客户端到此账号">' + I.swap + '</button>' : '')
1151
+ + (a.canSwitch && !isCurrent && st.cls !== 'fail' ? '<button class="icon-btn" data-act="switch" title="切换 ' + (CLIENT_LABEL[a.product] || prodLabel(a.product)) + ' 客户端到此账号">' + I.swap + '</button>' : '')
1008
1152
  + '<button class="icon-btn" data-act="quota" title="刷新积分">' + I.refresh + '</button>'
1009
1153
  + '<button class="icon-btn del" data-act="del" title="删除(点两次确认)">' + I.trash + '</button>'
1010
1154
  + '</div></div>'
@@ -1019,22 +1163,40 @@ function cardHtml(a) {
1019
1163
  + '</div>';
1020
1164
  }
1021
1165
 
1022
- const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
1166
+ const isDone = (a) => {
1167
+ if (!state.status || !state.status.todayDone) return false;
1168
+ const doneDay = state.status.todayDone[a.id];
1169
+ if (!doneDay) return false;
1170
+ const byProd = state.status.todayByProduct || {};
1171
+ const expectedDay = byProd[a.product] || state.status.today;
1172
+ return doneDay === expectedDay;
1173
+ };
1023
1174
  const ofProduct = (prod) => state.accounts.filter((a) => a.product === prod);
1024
- const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : prod === 'catpaw' ? state.status.catpawCurrentUid : null);
1175
+ const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : prod === 'catpaw' ? state.status.catpawCurrentUid : prod === 'trae' ? state.status.traeCurrentUid : null);
1025
1176
  /** 接口使用中判定:体验包接口最近一次补全用的账号(lastAccount 记的是 name || uid || id) */
1026
1177
  function isGatewayCurrent(a) {
1027
1178
  const last = state.zGatewayStats && state.zGatewayStats.lastAccount;
1028
1179
  return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
1029
1180
  }
1181
+ /** MiniMax 本地接口使用中判定 */
1182
+ function isMiniMaxGatewayCurrent(a) {
1183
+ const last = state.mGatewayStats && state.mGatewayStats.lastAccount;
1184
+ return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
1185
+ }
1030
1186
 
1031
- // 客户端当前登录判定:WorkBuddy / mirasim / 妙手 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
1187
+ // 客户端当前登录判定:WorkBuddy / mirasim / 妙手 / MiniMax 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
1032
1188
  function isClientCurrent(a) {
1033
1189
  const s = state.status;
1034
1190
  if (!s) return false;
1035
1191
  if (a.product === 'workbuddy') return Boolean(a.uid && s.workbuddyCurrentUid && s.workbuddyCurrentUid === a.uid);
1192
+ if (a.product === 'qoder') {
1193
+ const cur = s.qoderCurrent || {};
1194
+ return Boolean(a.uid && cur[a.provider] && String(cur[a.provider]) === String(a.uid));
1195
+ }
1036
1196
  if (a.product === 'mirasim') return Boolean(a.uid && s.mirasimCurrentUid && s.mirasimCurrentUid === a.uid);
1037
1197
  if (a.product === 'catpaw') return Boolean(a.uid && s.catpawCurrentUid && s.catpawCurrentUid === a.uid);
1198
+ if (a.product === 'trae') return Boolean(a.uid && s.traeCurrentUid && s.traeCurrentUid === a.uid);
1199
+ if (a.product === 'minimax') return Boolean(a.uid && s.minimaxCurrentUid && s.minimaxCurrentUid === a.uid);
1038
1200
  if (a.product === 'zcode') {
1039
1201
  const id = s.zcodeCurrentIdentity;
1040
1202
  return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
@@ -1056,6 +1218,9 @@ function creditSum(list) {
1056
1218
  }
1057
1219
 
1058
1220
  function render() {
1221
+ $('z-actions').style.display = 'none'; // 兜底:切到仪表盘 / 10Router 时 renderProd 不跑,先藏起来
1222
+ $('t-actions').style.display = 'none';
1223
+ $('m-actions').style.display = 'none';
1059
1224
  for (const p of PRODUCTS) $('cnt-' + p.id).textContent = ofProduct(p.id).length;
1060
1225
  $('cnt-tenrouter').textContent = tr.quotas ? tr.quotas.connections.length : tr.config && tr.config.configured ? '…' : '–';
1061
1226
  if (state.view === 'dash') renderDash();
@@ -1069,6 +1234,8 @@ const EMPTY_TIP = {
1069
1234
  zcode: '还没有 ZCode 账号。<br>点「添加账号」可读取本机 ZCode 客户端已登录的账号,或用浏览器登录(BigModel / Z.ai),多个账号可一键切换。',
1070
1235
  mirasim: '还没有 mirasim 账号。<br>点「添加账号」可读取本机 mirasim 客户端当前登录的账号(本地解密,token 不出本机)。',
1071
1236
  catpaw: '还没有妙手账号。<br>点「添加账号」可读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密凭据,token 不出本机)。',
1237
+ trae: '还没有 Trae 账号。<br>点「添加账号 → 本机导入」可读取本机 Trae 客户端当前登录的账号(本地解密 storage.json 凭据,token 不出本机)。多个账号需先在 Trae 里分别登录并逐个导入。',
1238
+ minimax: '还没有 MiniMax 账号。<br>点「添加账号 → 本机导入」可读取本机 MiniMax Code 客户端当前登录的账号,或粘贴 Bearer Token 添加。',
1072
1239
  };
1073
1240
 
1074
1241
  function renderProd() {
@@ -1097,7 +1264,7 @@ function renderProd() {
1097
1264
  $('s-credits').textContent = CREDIT_VIEWS.has(prod)
1098
1265
  ? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
1099
1266
  : c.loading ? '…' : (c.known ? fmtNum(c.sum) : '-');
1100
- const hasRegions = !CREDIT_VIEWS.has(prod);
1267
+ const hasRegions = !CREDIT_VIEWS.has(prod) && prod !== 'trae' && prod !== 'minimax'; // Trae / MiniMax 不分国际 / 国内版
1101
1268
  $('region').style.display = hasRegions ? '' : 'none';
1102
1269
  $('region-div').style.display = hasRegions ? '' : 'none';
1103
1270
  document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
@@ -1106,6 +1273,49 @@ function renderProd() {
1106
1273
  const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl');
1107
1274
  $('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? '领取 / 活跃全部' : '领取全部 ' + prodLabel(prod));
1108
1275
  }
1276
+ // 顶栏产品专属入口:ZCode 设置组、Trae 打开客户端,带状态灯(运行中绿点呼吸)
1277
+ const dot = (on, pulse) => '<span class="dot' + (on ? ' on' : '') + (on && pulse ? ' pulse' : '') + '"></span>';
1278
+ $('z-actions').style.display = prod === 'zcode' ? 'contents' : 'none';
1279
+ if (prod === 'zcode') {
1280
+ const acOn = state.zAutoClaim === true;
1281
+ const gwOn = state.zGateway === true;
1282
+ $('zt-ac').innerHTML = dot(acOn) + '自动领取';
1283
+ $('zt-ac').title = acOn ? '自动领取运行中——点我调整轮询周期 / 运行时长' : '自动领取已关闭——点我设置轮询周期与运行时长并开启';
1284
+ $('zt-net').innerHTML = '出口:<b style="font-weight:700;color:var(--text)">' + (state.zProxyFirst === true ? '代理优先' : '直连优先') + '</b>';
1285
+ $('zt-net').title = '网络出口:直连 / 代理顺序与代理地址';
1286
+ $('zt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.zGatewayLan === true ? '·局域网' : '');
1287
+ $('zt-gw').title = '体验包接口:开关、局域网开放与 IP 白名单';
1288
+ const zc = (state.status && state.status.zcodeClient) || {};
1289
+ const canOpen = zc.installed && window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function';
1290
+ $('zt-open').style.display = canOpen ? '' : 'none';
1291
+ if (canOpen) {
1292
+ $('zt-open').innerHTML = dot(zc.running === true, true) + '打开客户端';
1293
+ $('zt-open').title = zc.running ? 'ZCode 客户端运行中,点我唤到前台' : 'ZCode 客户端未运行,点我启动';
1294
+ }
1295
+ }
1296
+ $('t-actions').style.display = prod === 'trae' ? 'contents' : 'none';
1297
+ if (prod === 'trae') {
1298
+ const tc = (state.status && state.status.traeClient) || {};
1299
+ const canOpen = tc.installed && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function';
1300
+ $('tt-open').style.display = canOpen ? '' : 'none';
1301
+ if (canOpen) {
1302
+ $('tt-open').innerHTML = dot(tc.running === true, true) + '打开客户端';
1303
+ $('tt-open').title = tc.running ? 'Trae 客户端运行中,点我唤到前台' : 'Trae 客户端未运行,点我启动';
1304
+ }
1305
+ }
1306
+ $('m-actions').style.display = prod === 'minimax' ? 'contents' : 'none';
1307
+ if (prod === 'minimax') {
1308
+ const gwOn = state.mGateway === true;
1309
+ $('mt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.mGatewayLan === true ? '·局域网' : '');
1310
+ $('mt-gw').title = 'MiniMax 本地接口:开关、局域网开放与 IP 白名单';
1311
+ const mc = (state.status && state.status.minimaxClient) || {};
1312
+ const canOpen = mc.installed && window.creditdaddy && typeof window.creditdaddy.openMiniMaxClient === 'function';
1313
+ $('mt-open').style.display = canOpen ? '' : 'none';
1314
+ if (canOpen) {
1315
+ $('mt-open').innerHTML = dot(mc.running === true, true) + '打开客户端';
1316
+ $('mt-open').title = mc.running ? 'MiniMax Code 客户端运行中,点我唤到前台' : 'MiniMax Code 客户端未运行,点我启动';
1317
+ }
1318
+ }
1109
1319
  }
1110
1320
 
1111
1321
  function kpi(label, value, sub, labelTitle) {
@@ -1122,7 +1332,7 @@ function renderDash() {
1122
1332
  const failed = signable.filter((a) => statusOf(a).cls === 'fail').length;
1123
1333
  const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
1124
1334
  const byProd = orderedVisibleComps().filter((id) => id !== 'tenrouter').map((id) => prodLabel(id) + ' ' + ofProduct(id).length).join(' · ');
1125
- const creditByProd = ['qoder', 'workbuddy'].filter(compVisible).map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
1335
+ const creditByProd = ['qoder', 'workbuddy', 'minimax'].filter(compVisible).map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
1126
1336
  // 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
1127
1337
  const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
1128
1338
  const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
@@ -1162,11 +1372,18 @@ function renderDash() {
1162
1372
  + (cnList.length && intlList.length ? ' · ' : '')
1163
1373
  + (intlList.length ? intlList.filter(isDone).length + ' / ' + intlList.length + ' 国际' : '')
1164
1374
  + '</span>';
1375
+ } else if (p.id === 'minimax') {
1376
+ const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
1377
+ rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>'
1378
+ + '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1379
+ + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1165
1380
  } else {
1166
1381
  rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>';
1167
1382
  }
1168
- rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1169
- + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1383
+ if (p.id !== 'minimax') {
1384
+ rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1385
+ + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1386
+ }
1170
1387
  } else if (p.id === 'mirasim') {
1171
1388
  const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
1172
1389
  rows = '<span class="lbl">额度体系</span><span class="val dim">5h / 7d 滚动窗口</span>'
@@ -1192,6 +1409,8 @@ function renderDash() {
1192
1409
  : zid && (zid.email || zid.uid || zid.username) ? '未导入的账号(' + esc(zid.email || zid.username || zid.uid) + ')'
1193
1410
  : p.id === 'mirasim' && s.mirasimCurrentUid ? '未导入的账号'
1194
1411
  : p.id === 'catpaw' && s.catpawClient && s.catpawClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1412
+ : p.id === 'trae' && s.traeClient && s.traeClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1413
+ : p.id === 'minimax' && s.minimaxClient && s.minimaxClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1195
1414
  : (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
1196
1415
  rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
1197
1416
  }
@@ -1264,8 +1483,14 @@ function renderStatus() {
1264
1483
  // 产品相关提示只在对应标签页出现
1265
1484
  const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
1266
1485
  const n = $('notice');
1267
- n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || (hasIntl && Boolean(s.riskIdentity)));
1268
- if (state.view === 'catpaw') {
1486
+ n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || state.view === 'minimax' || (hasIntl && Boolean(s.riskIdentity)));
1487
+ if (state.view === 'minimax') {
1488
+ n.style.display = '';
1489
+ n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领 800~2000 积分 + 400 连签加成。支持自动检测本机 MiniMax Code 凭据与 Bearer Token 导入,自带标准 Anthropic 本地网关。</span></div>';
1490
+ } else if (state.view === 'trae') {
1491
+ n.style.display = '';
1492
+ n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领约 150 通用积分(7 天有效,多资源包自动归并)。凭据由本机 Trae 客户端解密导入,支持多账号快照一键切换。</span></div>';
1493
+ } else if (state.view === 'catpaw') {
1269
1494
  n.style.display = '';
1270
1495
  n.innerHTML = '妙手(美团 CatPaw)按套餐发放 Credits(体验版 / 专业版…),没有每日签到。凭据由妙手客户端登录管理:读取额度用本机解密出的登录 token 直连桌面网关,token 失效时到妙手客户端重新登录后再「本机导入」即可。'
1271
1496
  + '多账号切换:写入本机凭据后重启妙手客户端即生效(切换会自动保存当前登录,不丢号)。'
@@ -1277,23 +1502,12 @@ function renderStatus() {
1277
1502
  + mirasimClientChip(s);
1278
1503
  } else if (state.view === 'zcode') {
1279
1504
  n.style.display = '';
1280
- const pf = state.zProxyFirst === true;
1281
- const pu = state.zProxyUrl;
1282
1505
  const ac = state.zAutoClaim === true;
1283
- const autoOffAt = state.status.scheduler && state.status.scheduler.zcode && state.status.scheduler.zcode.autoOffAt;
1284
- const expiryText = ac && autoOffAt ? '(约 ' + Math.max(1, Math.ceil((new Date(autoOffAt).getTime() - Date.now()) / 60000)) + ' 分钟后自动关闭)' : '';
1285
- n.innerHTML = 'ZCode 没有每日签到,积分靠「活动领取」。开启后每 2 分钟检查活动资格,首次检查在 2 分钟后开始;本次自动领取最多运行 1 小时' + expiryText + '。也可点卡片上的礼物图标手动领取。'
1286
- + ' <button class="btn" id="btn-zac" style="margin-left:6px" title="ZCode 活动资格每 2 分钟轮询,开启 1 小时后自动关闭">' + (ac ? '自动领取:开' : '自动领取:关') + '</button>'
1287
- + ' <button class="btn" id="btn-znet" style="margin-left:4px" title="ZCode 上游接口的访问顺序:失败 / 被拦时自动回退另一条">出口:' + (pf ? '代理优先' : '直连优先') + '</button>'
1288
- + ' <button class="btn" id="btn-zproxy" style="margin-left:4px" title="设置 ZCode 出口用的 http 代理(留空则清除;不设置时使用 HTTPS_PROXY 环境变量)">代理:' + esc(pu || (state.zHasEnvProxy ? '环境变量' : '未设置')) + '</button>'
1289
- + ' <button class="btn" id="btn-zgw" style="margin-left:4px" title="开启后,领到的体验包额度会变成本机的一个补全接口(/gateway/v1/messages);10Router 建一个 anthropic-compatible 节点指向 http://127.0.0.1:' + location.port + '/gateway 即可调用。需要桌面版(隐藏窗口过验证码)">'
1290
- + (state.zGateway === true ? '体验包接口:开' : '体验包接口:关') + '</button>'
1291
- + ' <button class="btn" id="btn-zgwlan" style="margin-left:4px" title="允许局域网其他设备的 10Router 直连本机的体验包接口(daemon 重启后生效)。远程调用需要用「10Router 连接设置」里保存的虚拟 key 鉴权,因此需先配置 10Router 连接并设置面板访问密码">'
1292
- + (state.zGatewayLan === true ? '局域网:开' : '局域网:关') + '</button>'
1293
- + (state.zGateway === true && state.zGatewayStats && state.zGatewayStats.lastAccount ? ' <span class="chip on" style="margin-left:6px" title="体验包接口有调用记录,10Router 正在调用本机接口(最近用的账号见账号卡片上的「接口使用中」标记)">10Router 已接入</span>' : '')
1294
- + (state.zGateway === true ? ' <span class="ver" style="margin-left:6px">POST http://127.0.0.1:' + location.port + '/gateway/v1/messages</span>' : '')
1295
- + (state.zGateway === true && state.zGatewayStats && state.zGatewayStats.lastCallAt ? ' <span class="ver" style="margin-left:6px">最近调用 ' + esc(String(state.zGatewayStats.lastCallAt).slice(11, 19)) + ' · 共 ' + state.zGatewayStats.calls + ' 次</span>' : (state.zGateway === true ? ' <span class="ver" style="margin-left:6px">尚未有调用</span>' : ''))
1296
- + zcodeClientChip(s);
1506
+ const ziv = state.zClaimIntervalMin || 2;
1507
+ const zwin = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
1508
+ n.innerHTML = '<div class="z-row"><span class="z-label">活动领取</span><span class="z-text">活动包积分,每日 0 点左右刷新(无每日签到)。自动领取'
1509
+ + (ac ? '每 ' + ziv + ' 分钟检查资格' + (zwin > 0 ? ',本次运行 ' + zwin + ' 分钟' : ',持续运行') : '未开启(右上角设置轮询计划)')
1510
+ + ';礼物图标 = 手动临时领。</span></div>';
1297
1511
  } else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
1298
1512
  n.style.display = '';
1299
1513
  const busy = s.umid.installing || umidBusy;
@@ -1312,15 +1526,6 @@ function renderStatus() {
1312
1526
  }
1313
1527
  }
1314
1528
 
1315
- // ZCode 客户端状态芯片 + 「打开客户端」按钮(仅桌面版:面板里有 Electron 桥才显示按钮)
1316
- function zcodeClientChip(s) {
1317
- const c = s.zcodeClient || {};
1318
- if (!c.installed) return ' <span class="dim">客户端:未安装</span>';
1319
- const canOpen = window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function';
1320
- return ' <span class="dim">客户端:' + (c.running ? '运行中' : '未运行') + '</span>'
1321
- + (canOpen ? ' <button class="btn" id="btn-zopen" style="margin-left:4px" title="打开本机 ZCode 客户端">打开客户端</button>' : '');
1322
- }
1323
-
1324
1529
  // mirasim 客户端状态芯片 + 「打开客户端」按钮
1325
1530
  function mirasimClientChip(s) {
1326
1531
  const c = s.mirasimClient || {};
@@ -1330,6 +1535,7 @@ function mirasimClientChip(s) {
1330
1535
  + (canOpen ? ' <button class="btn" id="btn-miraopen" style="margin-left:4px" title="打开本机 Mirasim 客户端">打开客户端</button>' : '');
1331
1536
  }
1332
1537
 
1538
+
1333
1539
  // 妙手客户端状态芯片 + 「打开客户端」按钮
1334
1540
  function catpawClientChip(s) {
1335
1541
  const c = s.catpawClient || {};
@@ -1352,8 +1558,9 @@ async function installUmid() {
1352
1558
  await loadStatus(); render(); loadLogs();
1353
1559
  }
1354
1560
  $('notice').addEventListener('click', (e) => { if (e.target.closest('#btn-umid')) installUmid(); });
1355
- $('notice').addEventListener('click', async (e) => {
1356
- if (e.target.closest('#btn-zopen')) {
1561
+ // 覆盖 #notice(各产品横幅)与 #z-actions(ZCode 工具栏按钮组)的点击
1562
+ document.addEventListener('click', async (e) => {
1563
+ if (e.target.closest('#zt-open')) {
1357
1564
  try {
1358
1565
  const r = await window.creditdaddy.openZcodeClient();
1359
1566
  if (r && r.ok) toast('已启动 ZCode 客户端');
@@ -1377,51 +1584,67 @@ $('notice').addEventListener('click', async (e) => {
1377
1584
  } catch (err) { toast(String(err && err.message || err), false); }
1378
1585
  return;
1379
1586
  }
1380
- if (e.target.closest('#btn-zac')) {
1587
+ if (e.target.closest('#tt-open')) {
1381
1588
  try {
1382
- const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: state.zAutoClaim !== true } });
1383
- state.zAutoClaim = r.autoClaim;
1384
- renderStatus();
1385
- toast('ZCode 自动领取:' + (r.autoClaim ? '已开启(每 2 分钟检查,1 小时后自动关闭)' : '已关闭'));
1386
- } catch (err) { toast(err.message, false); }
1589
+ const r = await window.creditdaddy.openTraeClient();
1590
+ if (r && r.ok) toast('已启动 Trae 客户端');
1591
+ else toast((r && r.error) || '无法打开 Trae 客户端', false);
1592
+ } catch (err) { toast(String(err && err.message || err), false); }
1387
1593
  return;
1388
1594
  }
1389
- if (e.target.closest('#btn-zgwlan')) {
1595
+ if (e.target.closest('#mt-open')) {
1390
1596
  try {
1391
- const r = await api('/api/zcode-gateway', { method: 'PUT', json: { lan: state.zGatewayLan !== true } });
1392
- state.zGatewayLan = r.lan === true;
1393
- renderStatus();
1394
- toast(r.lan ? '已允许局域网访问:重启 CreditDaddy 后生效(远程调用用 10Router 虚拟 key 鉴权)' : '已限制为本机访问');
1395
- } catch (err) { toast(err.message, false); }
1597
+ const r = await window.creditdaddy.openMiniMaxClient();
1598
+ if (r && r.ok) toast('已启动 MiniMax Code 客户端');
1599
+ else toast((r && r.error) || '无法打开 MiniMax Code 客户端', false);
1600
+ } catch (err) { toast(String(err && err.message || err), false); }
1396
1601
  return;
1397
1602
  }
1398
- if (e.target.closest('#btn-zgw')) {
1399
- try {
1400
- const r = await api('/api/zcode-gateway', { method: 'PUT', json: { enabled: state.zGateway !== true } });
1401
- state.zGateway = r.enabled === true;
1402
- renderStatus();
1403
- toast('ZCode 体验包接口:' + (r.enabled ? '已开启(10Router 建 anthropic-compatible 节点指向本机 /gateway/v1/messages)' : '已关闭'));
1404
- } catch (err) { toast(err.message, false); }
1603
+ if (e.target.closest('#mt-gw')) {
1604
+ // MiniMax 本地接口弹窗:开关 + 局域网 + 白名单设置
1605
+ $('mgw-on').checked = state.mGateway === true;
1606
+ $('mgw-lan').checked = state.mGatewayLan === true;
1607
+ const allow = Array.isArray(state.mGatewayAllow) ? state.mGatewayAllow : [];
1608
+ $('mgw-allow').value = (allow.length ? allow : (state.mGatewaySuggest ? [state.mGatewaySuggest] : [])).join('\n');
1609
+ const st = state.mGatewayStats;
1610
+ $('mgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/minimax/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
1611
+ + (st && st.lastCallAt
1612
+ ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1613
+ : '<br>尚未有调用');
1614
+ syncMgwLanFields();
1615
+ openModal('m-mgw');
1405
1616
  return;
1406
1617
  }
1407
- if (e.target.closest('#btn-znet')) {
1408
- try {
1409
- const r = await api('/api/zcode/net', { method: 'PUT', json: { proxyFirst: state.zProxyFirst !== true } });
1410
- state.zProxyFirst = r.proxyFirst;
1411
- renderStatus();
1412
- toast('ZCode 出口:' + (r.proxyFirst ? '代理优先(直连兜底)' : '直连优先(代理兜底)'));
1413
- } catch (err) { toast(err.message, false); }
1618
+ if (e.target.closest('#zt-ac')) {
1619
+ // 自动领取计划弹窗:周期 / 运行时长由用户决定
1620
+ $('zac-interval').value = state.zClaimIntervalMin || 2;
1621
+ $('zac-window').value = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
1622
+ $('zac-off').style.display = state.zAutoClaim === true ? '' : 'none';
1623
+ $('zac-ok').textContent = state.zAutoClaim === true ? '保存' : '保存并启用';
1624
+ openModal('m-zac');
1414
1625
  return;
1415
1626
  }
1416
- if (e.target.closest('#btn-zproxy')) {
1417
- const cur = state.zProxyUrl || '';
1418
- const v = await uiPrompt('ZCode 出口代理(http://host:port,支持口令 http://user:pass@host:port)\n留空 = 清除,退回 HTTPS_PROXY 环境变量', cur, { okText: '保存' });
1419
- if (v === null) return;
1420
- try {
1421
- await api('/api/zcode/net', { method: 'PUT', json: { proxyUrl: v.trim() } });
1422
- await loadZNet(); renderStatus();
1423
- toast(v.trim() ? '代理已保存' : '代理已清除');
1424
- } catch (err) { toast(err.message, false); }
1627
+ if (e.target.closest('#zt-gw')) {
1628
+ // 体验包接口弹窗:开关 + 局域网 + 白名单一次设置;白名单默认预填 10Router 服务地址的主机
1629
+ $('zgw-on').checked = state.zGateway === true;
1630
+ $('zgw-lan').checked = state.zGatewayLan === true;
1631
+ const allow = Array.isArray(state.zGatewayAllow) ? state.zGatewayAllow : [];
1632
+ $('zgw-allow').value = (allow.length ? allow : (state.zGatewaySuggest ? [state.zGatewaySuggest] : [])).join('\n');
1633
+ const st = state.zGatewayStats;
1634
+ $('zgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
1635
+ + (st && st.lastCallAt
1636
+ ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1637
+ : '<br>尚未有调用');
1638
+ syncZgwLanFields();
1639
+ openModal('m-zgw');
1640
+ return;
1641
+ }
1642
+ if (e.target.closest('#zt-net')) {
1643
+ // 出口弹窗:顺序 + 代理地址一起保存;代理只回填到 placeholder(脱敏),留空即清除
1644
+ (state.zProxyFirst === true ? $('znet-proxy') : $('znet-direct')).checked = true;
1645
+ $('znet-url').value = '';
1646
+ $('znet-url').placeholder = state.zProxyUrl || (state.zHasEnvProxy ? '跟随环境变量 HTTPS_PROXY' : 'http://192.168.31.101:7890');
1647
+ openModal('m-znet');
1425
1648
  return;
1426
1649
  }
1427
1650
  });
@@ -1431,8 +1654,10 @@ async function loadZNet() {
1431
1654
  state.zProxyFirst = d.proxyFirst === true;
1432
1655
  state.zProxyUrl = d.proxyUrlMasked || null;
1433
1656
  state.zAutoClaim = d.autoClaim === true;
1657
+ state.zClaimIntervalMin = d.claimIntervalMin;
1658
+ state.zClaimWindowMin = d.claimWindowMin;
1434
1659
  state.zHasEnvProxy = d.hasEnvProxy === true;
1435
- if (state.view === 'zcode') renderStatus();
1660
+ if (state.view === 'zcode') { renderStatus(); render(); }
1436
1661
  } catch (e) {}
1437
1662
  }
1438
1663
  async function loadZGateway() {
@@ -1440,9 +1665,24 @@ async function loadZGateway() {
1440
1665
  const d = await api('/api/zcode-gateway');
1441
1666
  state.zGateway = d.enabled === true;
1442
1667
  state.zGatewayLan = d.lan === true;
1668
+ state.zGatewayAllow = d.allow || [];
1669
+ state.zGatewaySuggest = d.suggestAllow || '';
1443
1670
  state.zGatewayHasCaptcha = d.hasCaptcha === true;
1444
1671
  state.zGatewayStats = d.stats || null;
1445
- if (state.view === 'zcode') renderStatus();
1672
+ if (state.view === 'zcode') { renderStatus(); render(); }
1673
+ applyLogTabs(); // 网关开关变化影响日志标签显隐
1674
+ } catch (e) {}
1675
+ }
1676
+ async function loadMGateway() {
1677
+ try {
1678
+ const d = await api('/api/minimax-gateway');
1679
+ state.mGateway = d.enabled === true;
1680
+ state.mGatewayLan = d.lan === true;
1681
+ state.mGatewayAllow = d.allow || [];
1682
+ state.mGatewaySuggest = d.suggestAllow || '';
1683
+ state.mGatewayStats = d.stats || null;
1684
+ if (state.view === 'minimax') { renderStatus(); render(); }
1685
+ applyLogTabs(); // 网关开关变化影响日志标签显隐
1446
1686
  } catch (e) {}
1447
1687
  }
1448
1688
 
@@ -1701,17 +1941,14 @@ function renderTenrouter() {
1701
1941
  const c = tr.config;
1702
1942
  if (!c) { box.innerHTML = '<div class="empty">加载中…</div>'; return; }
1703
1943
  const gwLine = state.zGateway === true
1704
- ? '<div class="gw-line">ZCode 体验包接口:<b>运行中</b>' + (state.zGatewayStats && state.zGatewayStats.lastCallAt ? ' · 最近调用 ' + esc(String(state.zGatewayStats.lastCallAt).slice(11, 19)) + ' · ' + state.zGatewayStats.calls + ' 次 · 最后用号 ' + esc(state.zGatewayStats.lastAccount || '-') : '(尚未有调用)') + ' — 体验包模型 <code>zcode-free/glm-5.3-flash</code></div>'
1944
+ ? '<div class="gw-line"><span class="dot on pulse"></span> ZCode 体验包接口运行中'
1945
+ + (state.zGatewayStats && state.zGatewayStats.lastCallAt ? ' · 最近调用 ' + esc(String(state.zGatewayStats.lastCallAt).slice(11, 19)) + ' · ' + state.zGatewayStats.calls + ' 次 · 最后用号 ' + esc(state.zGatewayStats.lastAccount || '-') : ' · 尚未有调用')
1946
+ + ' · 模型 <code>zcode-free/glm-5.3-flash</code></div>'
1705
1947
  : '';
1706
1948
  if (!c.configured) { box.innerHTML = gwLine + trSetupHtml(); return; }
1707
1949
  const list = (tr.quotas && tr.quotas.connections) || [];
1708
1950
  const low = list.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
1709
1951
  const failed = list.filter((x) => x.error).length;
1710
- const ls = c.lastSync;
1711
- const srcChips = (c.sources || []).map((x) => {
1712
- const on = x.found && c.sync.sources.includes(x.id);
1713
- return '<span class="chip' + (on ? ' on' : '') + '" title="' + (x.found ? (x.supported ? '已检测到' : '需要 Node 22.5+') : '本机未检测到') + '">' + esc(x.label) + (x.found ? '' : ' ·无') + '</span>';
1714
- }).join('');
1715
1952
  let grid;
1716
1953
  let tabStrip = '';
1717
1954
  const groups = list.length ? trGroups(list) : [];
@@ -1759,15 +1996,11 @@ function renderTenrouter() {
1759
1996
  + '<div class="stat"><span>额度告急</span><strong>' + (tr.quotas ? low : '-') + '</strong></div><div class="divider"></div>'
1760
1997
  + '<div class="stat"><span>查询失败</span><strong>' + (tr.quotas ? failed : '-') + '</strong></div>'
1761
1998
  + '</div><div class="actions">'
1762
- + '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + I.refresh + (tr.loading ? '刷新中…' : '刷新额度') + '</button>'
1763
- + '<button class="btn" data-tr="sync"' + (tr.syncing || c.syncing ? ' disabled' : '') + '>' + I.upload + (tr.syncing || c.syncing ? '同步中…' : '同步用量') + '</button>'
1999
+ + '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + (tr.loading ? '刷新中…' : '刷新额度') + '</button>'
2000
+ + '<button class="btn" data-tr="sync" title="用量同步:自动开关、同步来源与手动同步(点我打开)"' + (tr.syncing || c.syncing ? ' disabled' : '') + '>'
2001
+ + '<span class="dot' + (c.sync.enabled ? ' on' : '') + '"></span>' + (tr.syncing || c.syncing ? '同步中…' : '用量同步') + '</button>'
1764
2002
  + '<button class="btn" data-tr="settings">设置</button>'
1765
2003
  + '</div></div>'
1766
- + '<div class="tr-bar"><span title="' + esc(c.endpoint) + '">' + esc(c.endpoint) + ' · ' + esc(c.keyMasked) + '</span>'
1767
- + '<span>用量同步:' + (c.sync.enabled ? '每小时自动' : '手动') + '</span>' + srcChips
1768
- + '<span style="margin-left:auto" title="' + esc(ls ? ls.summary : '') + '">' + (ls ? '上次同步 ' + esc(fmtTime(ls.at).slice(5)) + ' · ' + esc(ls.summary.slice(0, 40)) : '还没有同步过') + '</span>'
1769
- + (tr.quotas ? '<span>额度更新于 ' + esc(fmtTime(tr.quotas.generatedAt).slice(11)) + '</span>' : '')
1770
- + '</div>'
1771
2004
  + tabStrip
1772
2005
  + '<div class="tr-grid">' + grid + '</div>';
1773
2006
  }
@@ -1817,23 +2050,36 @@ function openTrSettings() {
1817
2050
  $('tr-key').placeholder = c.keyMasked ? '已保存 ' + c.keyMasked + '(留空保持不变)' : 'sk-…';
1818
2051
  $('tr-admin-pw').value = '';
1819
2052
  $('tr-admin-pw').placeholder = c.hasAdminPassword ? '已保存(留空保持不变)' : '可选:10Router 面板密码';
1820
- $('tr-auto').checked = c.configured ? c.sync.enabled : true;
1821
2053
  $('tr-hide-empty').checked = state.trHideEmpty === true;
1822
- $('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
1823
- + ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
1824
- + '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
1825
2054
  $('tr-disconnect').style.display = c.configured ? '' : 'none';
1826
2055
  $('tr-test-msg').textContent = '';
1827
2056
  openModal('m-10r');
1828
2057
  setTimeout(() => $(c.configured ? 'tr-key' : 'tr-endpoint').focus(), 30);
1829
2058
  }
2059
+ // 用量同步弹窗:自动开关 + 来源勾选 + 手动同步
2060
+ function openTrSync() {
2061
+ const c = tr.config || { sync: { enabled: true, sources: [] }, sources: [] };
2062
+ $('tr-auto').checked = c.configured ? c.sync.enabled : true;
2063
+ $('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
2064
+ + ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
2065
+ + '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
2066
+ const ls = c.lastSync;
2067
+ $('tr-sync-status').textContent = ls ? '上次同步 ' + fmtTime(ls.at).slice(5) + ':' + ls.summary : '还没有同步过';
2068
+ $('tr-sync-now').disabled = tr.syncing || c.syncing === true;
2069
+ openModal('m-trsync');
2070
+ }
2071
+ async function trSyncSave(close) {
2072
+ try {
2073
+ tr.config = await api('/api/tenrouter', { method: 'PUT', json: { syncEnabled: $('tr-auto').checked, sources: [...document.querySelectorAll('#tr-sources input[data-src]:checked')].map((x) => x.dataset.src) } });
2074
+ if (close) { closeModal('m-trsync'); toast('用量同步设置已保存'); render(); }
2075
+ } catch (e) { toast(e.message, false); throw e; }
2076
+ }
1830
2077
  function trForm() {
1831
2078
  return {
1832
- endpoint: $('tr-endpoint').value.trim(),
2079
+ // 从 LLM 客户端复制的 base URL 常带 /v1 后缀——10Router 只认站点根地址,带 /v1 会让所有 /api/* 调用 404、误报「需要 1.2.1+」
2080
+ endpoint: $('tr-endpoint').value.trim().replace(/\/+$/, '').replace(/\/v1$/, ''),
1833
2081
  key: $('tr-key').value.trim(),
1834
2082
  adminPassword: $('tr-admin-pw').value, // 留空 = 服务端保持原值
1835
- syncEnabled: $('tr-auto').checked,
1836
- sources: [...document.querySelectorAll('#tr-sources input[data-src]:checked')].map((x) => x.dataset.src),
1837
2083
  };
1838
2084
  }
1839
2085
  async function trTest() {
@@ -1886,7 +2132,7 @@ $('tr-panel').addEventListener('click', (e) => {
1886
2132
  if (b.dataset.tr === 'toggle') { state.trOpen = Object.assign({}, state.trOpen, { [b.dataset.id]: !(state.trOpen && state.trOpen[b.dataset.id]) }); render(); return; }
1887
2133
  if (b.dataset.tr === 'settings') openTrSettings();
1888
2134
  else if (b.dataset.tr === 'refresh') loadTrQuotas(true);
1889
- else if (b.dataset.tr === 'sync') trSync();
2135
+ else if (b.dataset.tr === 'sync') openTrSync();
1890
2136
  else if (b.dataset.tr === 'vendor') {
1891
2137
  const v = b.dataset.v || '';
1892
2138
  state.trVendor = v;
@@ -1895,6 +2141,12 @@ $('tr-panel').addEventListener('click', (e) => {
1895
2141
  }
1896
2142
  });
1897
2143
  $('tr-test').onclick = trTest;
2144
+ $('tr-sync-save').onclick = () => trSyncSave(true);
2145
+ $('tr-sync-now').onclick = async () => {
2146
+ try { await trSyncSave(false); } catch (e) { return; }
2147
+ closeModal('m-trsync');
2148
+ trSync();
2149
+ };
1898
2150
  $('tr-sync-accounts').onclick = trSyncAccounts;
1899
2151
  $('tr-save').onclick = trSave;
1900
2152
  $('tr-disconnect').onclick = trDisconnect;
@@ -1941,12 +2193,46 @@ async function loadQuotas(ids) {
1941
2193
  };
1942
2194
  await Promise.all([worker(), worker(), worker()]);
1943
2195
  }
2196
+ let logTabView = null; // 当前日志标签所属的视图:切到别的视图时才重置跟随,同一视图内保留手动切换
2197
+ function followLogTab() {
2198
+ if (logTabView !== state.view) {
2199
+ state.logTab = (state.view !== 'dash' && GW_LOG_TAG[state.view]) ? state.view : 'run';
2200
+ logTabView = state.view;
2201
+ }
2202
+ }
1944
2203
  async function loadLogs() {
2204
+ followLogTab();
2205
+ applyLogTabs();
2206
+ await loadCurrentLogs();
2207
+ }
2208
+ function applyLogTabs() {
2209
+ // 网关开关关闭时,隐藏对应日志标签(无该网关时也看不到)
2210
+ document.querySelectorAll('#log-tabs button').forEach((b) => {
2211
+ const t = b.dataset.logTab;
2212
+ let show = true;
2213
+ if (t === 'zcode') show = Boolean(state.zGateway);
2214
+ if (t === 'minimax') show = Boolean(state.mGateway);
2215
+ b.style.display = show ? '' : 'none';
2216
+ b.classList.toggle('active', t === state.logTab);
2217
+ });
2218
+ }
2219
+ async function loadCurrentLogs() {
2220
+ const el = $('logs');
2221
+ if (!el) return;
2222
+ let logs = [];
1945
2223
  try {
1946
- const data = await api('/api/logs');
1947
- const el = $('logs');
2224
+ if (state.logTab === 'run') {
2225
+ const data = await api('/api/logs');
2226
+ logs = data.logs || [];
2227
+ } else {
2228
+ const data = await api('/api/logs?tag=' + encodeURIComponent(GW_LOG_TAG[state.logTab]));
2229
+ logs = data.logs || [];
2230
+ }
1948
2231
  const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
1949
- el.innerHTML = data.logs.length ? data.logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] ' + esc(l.tag) + ' · ' + esc(l.msg) + '</div>').join('') : '(暂无)';
2232
+ const withTag = state.logTab === 'run';
2233
+ el.innerHTML = logs.length
2234
+ ? logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] ' + (withTag ? esc(l.tag) + ' · ' : '') + esc(l.msg) + '</div>').join('')
2235
+ : '(暂无)';
1950
2236
  if (atBottom) el.scrollTop = el.scrollHeight;
1951
2237
  } catch (e) {}
1952
2238
  }
@@ -1957,7 +2243,9 @@ async function checkinOne(id) {
1957
2243
  try {
1958
2244
  const d = await api('/api/accounts/' + encodeURIComponent(id) + '/checkin', { method: 'POST' });
1959
2245
  const r = d.results && d.results[0];
1960
- toast(r ? (r.account + ':' + (r.status === 'checked-in' ? '领取成功 +' + r.claimedAmount + ' Credits' : (r.error || r.message || r.status))) : d.summary, !(r && r.status === 'failed'));
2246
+ const accObj = state.accounts.find((x) => x.id === id);
2247
+ const accLabel = (r && r.account) || (accObj && (accObj.name || accObj.id)) || '该账号';
2248
+ toast(r ? (accLabel + ':' + (r.status === 'checked-in' ? '领取成功 +' + r.claimedAmount + ' Credits' : (r.error || r.message || r.status))) : d.summary, !(r && r.status === 'failed'));
1961
2249
  } catch (e) { toast(e.message, false); }
1962
2250
  delete state.busy[id];
1963
2251
  await loadStatus(); await loadAccounts(); loadLogs();
@@ -1982,21 +2270,25 @@ async function onCardClick(e) {
1982
2270
  if (act === 'claim') return openClaim(id);
1983
2271
  if (act === 'switch') {
1984
2272
  const a = state.accounts.find((x) => x.id === id);
1985
- const client = CLIENT_LABEL[a && a.product] || '客户端';
2273
+ const client = CLIENT_LABEL[a && a.product] || (a && prodLabel(a.product)) || '客户端';
1986
2274
  const tip = a && a.product === 'zcode'
1987
2275
  ? '\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
1988
2276
  : a && a.product === 'mirasim'
1989
2277
  ? '\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
1990
2278
  : a && a.product === 'catpaw'
1991
2279
  ? '\n需先退出妙手;切换后重新打开妙手即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
1992
- : '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
2280
+ : a && a.product === 'trae'
2281
+ ? '\n需先退出 Trae;切换后重新打开 Trae 即为新账号。当前登录会先自动快照保存到 CreditDaddy,不会丢号。'
2282
+ : a && a.product === 'qoder'
2283
+ ? '\n需先退出 Qoder;切换后重新打开 Qoder 即为新账号。运行中的 Qoder 退出时会把旧登录写回去,可选「关闭并强制切换」代为关进程。'
2284
+ : '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
1993
2285
  if (!(await uiConfirm('把 ' + client + ' 切换到「' + (a && a.name || id) + '」?' + tip, '切换账号', '切换'))) return;
1994
2286
  const doSwitch = (force) => api('/api/accounts/' + encodeURIComponent(id) + '/switch', { json: { force } });
1995
2287
  try {
1996
2288
  let r;
1997
2289
  try { r = await doSwitch(false); }
1998
2290
  catch (err) {
1999
- if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING') throw err;
2291
+ if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING' && err.code !== 'TRAE_RUNNING' && err.code !== 'QODER_RUNNING') throw err;
2000
2292
  if (!(await uiConfirm(err.message + '\n\n仍要强制切换吗?强制切换会先关闭客户端再写入登录,重新打开客户端即为新账号。', client + ' 正在运行', '关闭并强制切换'))) return;
2001
2293
  r = await doSwitch(true);
2002
2294
  }
@@ -2029,6 +2321,15 @@ async function onCardClick(e) {
2029
2321
  } catch (e) {}
2030
2322
  }, 800);
2031
2323
  }
2324
+ if (a && a.product === 'trae' && r && !r.alreadyActive && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function') {
2325
+ setTimeout(async () => {
2326
+ try {
2327
+ const re = await window.creditdaddy.openTraeClient();
2328
+ if (re && re.ok) toast('已重新启动 Trae 客户端');
2329
+ else toast((re && re.error) || '无法打开 Trae 客户端(桌面版才能自动拉起)', false);
2330
+ } catch (e) {}
2331
+ }, 800);
2332
+ }
2032
2333
  } catch (err) { toast(err.message, false); }
2033
2334
  return;
2034
2335
  }
@@ -2226,6 +2527,156 @@ const uiPrompt = (title, value, opts = {}) => uiDialog({ title, message: opts.me
2226
2527
  function okDialog() { const v = $('dlg-field').style.display === 'none' ? true : $('dlg-input').value; $('m-dialog').classList.remove('show'); settleDialog(v); }
2227
2528
  $('dlg-ok').addEventListener('click', okDialog);
2228
2529
  $('dlg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') okDialog(); });
2530
+
2531
+ // ── 检查更新(桌面版):主进程推状态(update:state),面板只管渲染 ──
2532
+ const updBridge = window.creditdaddy && window.creditdaddy.update ? window.creditdaddy.update : null;
2533
+ let updState = null;
2534
+ function updActBtn(id, text, cls, fn) {
2535
+ return '<button class="btn' + (cls ? ' ' + cls : '') + '" id="' + id + '">' + text + '</button>';
2536
+ }
2537
+ function renderUpdateModal(s) {
2538
+ updState = s;
2539
+ const body = $('upd-body');
2540
+ const acts = $('upd-actions');
2541
+ const cur = '当前 v' + (s.current || '?');
2542
+ const rel = '<button class="btn" id="upd-rel">打开 Releases</button>';
2543
+ const close = '<button class="btn" data-close-uu>稍后</button>';
2544
+ switch (s.phase) {
2545
+ case 'unsupported':
2546
+ body.innerHTML = '<div class="hint">当前环境不支持应用内更新(开发模式 / Portable / 未签名的 macOS 版),请从 GitHub Releases 手动下载。</div>';
2547
+ acts.innerHTML = rel + '<button class="btn" data-close-uu>取消</button>';
2548
+ break;
2549
+ case 'checking':
2550
+ body.innerHTML = '<div class="hint">正在检查更新…</div>';
2551
+ acts.innerHTML = close;
2552
+ break;
2553
+ case 'latest':
2554
+ body.innerHTML = '<div class="hint"><span class="dot on"></span> 已是最新版本(' + cur + ')</div>';
2555
+ acts.innerHTML = '<button class="btn primary" data-close-uu>好的</button>';
2556
+ break;
2557
+ case 'available':
2558
+ body.innerHTML = s.via === 'updater'
2559
+ ? '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + ')——正在后台下载,完成后退出 CreditDaddy 时自动安装。</div>'
2560
+ : '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + (s.sizeMB ? ',约 ' + s.sizeMB + ' MB' : '') + '),可由本机直接下载安装。</div>';
2561
+ acts.innerHTML = (s.via === 'github' ? updActBtn('upd-go', '立即下载安装', 'primary') : '') + rel + close;
2562
+ break;
2563
+ case 'downloading':
2564
+ body.innerHTML = '<div class="hint">正在下载 v' + esc(s.version) + '</div>'
2565
+ + '<div class="meter" style="margin:8px 0 4px"><span style="width:' + (s.percent || 0) + '%"></span></div>'
2566
+ + '<div class="hint">' + (s.percent != null ? s.percent + '%' : '') + '(' + (s.mb || '…') + ' MB)' + (s.percent == null ? ' 获取大小中…' : '') + '</div>';
2567
+ acts.innerHTML = '<button class="btn" data-close-uu>后台继续</button>';
2568
+ break;
2569
+ case 'downloaded':
2570
+ body.innerHTML = '<div class="hint"><span class="dot on pulse"></span> <b>v' + esc(s.version) + '</b> '
2571
+ + (s.via === 'updater' ? '已下载完成,重启即完成更新(退出时也会自动安装)。' : '安装包已下载完成,安装时 CreditDaddy 会先退出。') + '</div>';
2572
+ acts.innerHTML = updActBtn('upd-go', s.via === 'updater' ? '立即重启更新' : '立即安装', 'primary') + close;
2573
+ break;
2574
+ case 'error':
2575
+ default:
2576
+ body.innerHTML = '<div class="hint" style="color:var(--err)">检查更新失败:' + esc(s.error || '未知错误') + '</div>';
2577
+ acts.innerHTML = updActBtn('upd-retry', '重试', 'primary') + rel + '<button class="btn" data-close-uu>取消</button>';
2578
+ break;
2579
+ }
2580
+ const relBtn = $('upd-rel');
2581
+ if (relBtn) relBtn.onclick = () => updBridge.openReleases();
2582
+ const goBtn = $('upd-go');
2583
+ if (goBtn) { goBtn.disabled = true; goBtn.textContent = '处理中…'; updBridge.install().then(renderUpdateModal).catch((e) => { goBtn.disabled = false; goBtn.textContent = '重试'; }); }
2584
+ const retryBtn = $('upd-retry');
2585
+ if (retryBtn) retryBtn.onclick = () => updBridge.check().then(renderUpdateModal).catch(() => {});
2586
+ acts.querySelectorAll('[data-close-uu]').forEach((b) => { b.onclick = () => closeModal('m-update'); });
2587
+ }
2588
+ async function openUpdateModal(autoCheck) {
2589
+ if (!updBridge) return;
2590
+ openModal('m-update');
2591
+ try {
2592
+ updState = await updBridge.state();
2593
+ renderUpdateModal(updState);
2594
+ if (autoCheck) renderUpdateModal(await updBridge.check());
2595
+ } catch (e) {
2596
+ renderUpdateModal({ phase: 'error', error: String((e && e.message) || e), current: (updState && updState.current) || '' });
2597
+ }
2598
+ }
2599
+ if (updBridge) {
2600
+ updBridge.onState((s) => { updState = s; $('upd-ver') && ($('upd-ver').textContent = '当前 v' + (s.current || '?')); if (document.getElementById('m-update').classList.contains('show')) renderUpdateModal(s); });
2601
+ updBridge.onOpen(() => openUpdateModal(true));
2602
+ }
2603
+ $('btn-update').onclick = () => openUpdateModal(true);
2604
+
2605
+ // ZCode 自动领取计划:保存(按新配置启用)/ 停用
2606
+ $('zac-ok').addEventListener('click', async () => {
2607
+ try {
2608
+ const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: true, claimIntervalMin: Number($('zac-interval').value), claimWindowMin: Number($('zac-window').value) } });
2609
+ state.zAutoClaim = r.autoClaim === true;
2610
+ state.zClaimIntervalMin = r.claimIntervalMin;
2611
+ state.zClaimWindowMin = r.claimWindowMin;
2612
+ renderStatus(); render();
2613
+ closeModal('m-zac');
2614
+ toast('自动领取已开启:每 ' + r.claimIntervalMin + ' 分钟检查' + (r.claimWindowMin > 0 ? ',运行 ' + r.claimWindowMin + ' 分钟后自动关闭' : ',一直运行直到手动关闭'));
2615
+ } catch (err) { toast(err.message, false); }
2616
+ });
2617
+ $('zac-off').addEventListener('click', async () => {
2618
+ try {
2619
+ const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: false } });
2620
+ state.zAutoClaim = r.autoClaim === true;
2621
+ renderStatus(); render();
2622
+ closeModal('m-zac');
2623
+ toast('ZCode 自动领取:已关闭');
2624
+ } catch (err) { toast(err.message, false); }
2625
+ });
2626
+
2627
+ // ZCode 体验包接口:开关 + 局域网开放 + 免密 IP 白名单一次保存
2628
+ function syncZgwLanFields() {
2629
+ const off = !$('zgw-on').checked;
2630
+ $('zgw-lan-fields').querySelectorAll('input, textarea').forEach((el) => { el.disabled = off; });
2631
+ $('zgw-lan-fields').style.opacity = off ? '.45' : '';
2632
+ }
2633
+ $('zgw-on').addEventListener('change', syncZgwLanFields);
2634
+ $('zgw-ok').addEventListener('click', async () => {
2635
+ const allow = $('zgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
2636
+ try {
2637
+ const r = await api('/api/zcode-gateway', { method: 'PUT', json: { enabled: $('zgw-on').checked, lan: $('zgw-lan').checked, allow } });
2638
+ state.zGateway = r.enabled === true;
2639
+ state.zGatewayLan = r.lan === true;
2640
+ state.zGatewayAllow = r.allow || [];
2641
+ renderStatus(); render();
2642
+ closeModal('m-zgw');
2643
+ toast('体验包接口:' + (r.enabled ? '已开启' : '已关闭')
2644
+ + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : ',白名单为空——白名单外需虚拟 key') + ';绑定改动重启后生效' : ''));
2645
+ } catch (err) { toast(err.message, false); }
2646
+ });
2647
+
2648
+ // MiniMax 本地接口:开关 + 局域网开放 + 免密 IP 白名单一次保存
2649
+ function syncMgwLanFields() {
2650
+ const off = !$('mgw-on').checked;
2651
+ $('mgw-lan-fields').querySelectorAll('input, textarea').forEach((el) => { el.disabled = off; });
2652
+ $('mgw-lan-fields').style.opacity = off ? '.45' : '';
2653
+ }
2654
+ $('mgw-on').addEventListener('change', syncMgwLanFields);
2655
+ $('mgw-ok').addEventListener('click', async () => {
2656
+ const allow = $('mgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
2657
+ try {
2658
+ const r = await api('/api/minimax-gateway', { method: 'PUT', json: { enabled: $('mgw-on').checked, lan: $('mgw-lan').checked, allow } });
2659
+ state.mGateway = r.enabled === true;
2660
+ state.mGatewayLan = r.lan === true;
2661
+ state.mGatewayAllow = r.allow || [];
2662
+ renderStatus(); render();
2663
+ closeModal('m-mgw');
2664
+ toast('MiniMax 接口:' + (r.enabled ? '已开启' : '已关闭')
2665
+ + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : '') : ''));
2666
+ } catch (err) { toast(err.message, false); }
2667
+ });
2668
+
2669
+ // ZCode 网络出口:顺序 + 代理地址保存
2670
+ $('znet-ok').addEventListener('click', async () => {
2671
+ try {
2672
+ const body = { proxyFirst: $('znet-proxy').checked, proxyUrl: $('znet-url').value.trim() };
2673
+ const r = await api('/api/zcode/net', { method: 'PUT', json: body });
2674
+ await loadZNet();
2675
+ renderStatus(); render();
2676
+ closeModal('m-znet');
2677
+ toast('ZCode 出口:' + (r.proxyFirst ? '代理优先(直连兜底)' : '直连优先(代理兜底)') + (body.proxyUrl === '' ? ',代理已清除' : ''));
2678
+ } catch (err) { toast(err.message, false); }
2679
+ });
2229
2680
  document.querySelectorAll('.mask').forEach((m) => {
2230
2681
  m.addEventListener('mousedown', (e) => { if (e.target === m) closeModal(m.id); });
2231
2682
  m.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => closeModal(m.id)));
@@ -2245,10 +2696,13 @@ const LOCAL_HINT = {
2245
2696
  zcode: '读取本机 ZCode 客户端当前登录的账号(本地解密 ~/.zcode/v2/credentials.json),token 不出本机。',
2246
2697
  mirasim: '读取本机 mirasim 客户端当前登录的账号(本地解密 ~/.mirasim/setting.json),token 不出本机。',
2247
2698
  catpaw: '读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密 %APPDATA%\\catpaw-moon\\catx-credential.json),token 不出本机。',
2699
+ trae: '读取本机 Trae 客户端当前登录的账号(本地解密 <客户端数据目录>\\User\\globalStorage\\storage.json 的 tc 信封),token 不出本机。',
2700
+ minimax: '读取本机 MiniMax Code 客户端当前登录的账号(~/.minimax/auth/.../auth.json),token 不出本机。',
2248
2701
  };
2249
2702
  function openAdd(product) {
2250
2703
  state.addProduct = product || '';
2251
- const tabShown = { local: true, device: product !== 'mirasim' && product !== 'catpaw', token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' };
2704
+ // 各产品支持的添加方式:MiniMax 三种都支持(浏览器登录 = 官方设备码授权,独立凭据链不与本机客户端互斥)
2705
+ const tabShown = { local: true, device: product !== 'mirasim' && product !== 'catpaw' && product !== 'trae', token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' && product !== 'trae' };
2252
2706
  document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
2253
2707
  $('local-hint').textContent = LOCAL_HINT[state.addProduct];
2254
2708
  let first = null;
@@ -2277,7 +2731,7 @@ function openAdd(product) {
2277
2731
  async function localScan() {
2278
2732
  const box = $('scan-result');
2279
2733
  $('scan-status').textContent = '扫描中(最多约 10 秒)…';
2280
- box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手)…</div>';
2734
+ box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax)…</div>';
2281
2735
  try {
2282
2736
  const r = await api('/api/local/scan', { method: 'POST' });
2283
2737
  $('scan-status').textContent = '';
@@ -2298,9 +2752,9 @@ async function localScan() {
2298
2752
  html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
2299
2753
  }
2300
2754
  if (!cands.length) {
2301
- const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手';
2302
- const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' || state.addProduct === 'catpaw' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
2303
- html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录'
2755
+ const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax';
2756
+ const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' || state.addProduct === 'catpaw' || state.addProduct === 'trae' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
2757
+ html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录、Trae 数据目录、MiniMax 数据目录'
2304
2758
  + (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
2305
2759
  }
2306
2760
  box.innerHTML = html;
@@ -2639,11 +3093,24 @@ function renderCompMenu() {
2639
3093
  + '<div class="hint">开启后每次打开面板都需要输入密码;密码可随时在此修改或关闭。'
2640
3094
  + (state.sec.enabled ? '' : '账号导出不受影响(导出口令始终必填)。') + '</div>'
2641
3095
  + '<div class="row">' + renderSecActions() + '</div>'
3096
+ + '<div class="comp-menu-title">自动签到</div>'
3097
+ + '<div class="hint">' + autoCheckinHint() + '</div>'
2642
3098
  + '<div class="comp-menu-title">组件顺序(仪表盘与标签栏同步)</div>' + compRows
2643
3099
  + '<div class="comp-menu-title">账号卡片排序</div>'
2644
3100
  + (sortSections || '<div class="sort-hint">暂无账号</div>');
2645
3101
  }
2646
3102
 
3103
+ /** 自动签到口径说明:调度周期 + 各产品业务日刷新时点 */
3104
+ function autoCheckinHint() {
3105
+ const sch = state.status && state.status.scheduler;
3106
+ const next = sch && sch.nextTickAt ? ',下次执行 ' + esc(String(sch.nextTickAt).slice(11, 16)) : '';
3107
+ const zc = sch && sch.zcode;
3108
+ const zcText = zc && zc.enabled
3109
+ ? 'ZCode 自动领取运行中(每 ' + (zc.intervalMin || 2) + ' 分钟' + (zc.autoOffAt ? ',约 ' + Math.max(1, Math.ceil((new Date(zc.autoOffAt).getTime() - Date.now()) / 60000)) + ' 分钟后自动关闭' : ',一直运行') + '),周期可在 ZCode 页「自动领取计划」里改'
3110
+ : 'ZCode 活动领取默认关闭,可在 ZCode 页「自动领取计划」里开启并配置轮询周期';
3111
+ return '守护进程每 ~2 小时自动扫描执行一轮全部账号' + next + '。各产品「业务日」刷新时点(UTC+8):<b>Qoder 每日 10:00</b>(每台设备每日限领一个国际版账号)、<b>WorkBuddy 每日 00:00</b>,其余产品按 00:00;已领成功的账号按对应业务日记忆跳过,跨过刷新点后当天照常重试。' + esc(zcText) + '。';
3112
+ }
3113
+
2647
3114
  /** 在手动排序列表里移动一个 id;列表不存在时先按当前生效顺序物化一份 */
2648
3115
  function moveAccountId(prod, aid, dir) {
2649
3116
  const baseIds = sortedAccounts(prod, ofProduct(prod)).map((a) => a.id);
@@ -2662,6 +3129,11 @@ $('btn-comp').addEventListener('click', async () => {
2662
3129
  openModal('m-settings');
2663
3130
  await loadSettings();
2664
3131
  renderCompMenu();
3132
+ // 桌面版才有检查更新入口(走 preload 桥到主进程的更新状态机)
3133
+ if (updBridge) {
3134
+ $('upd-entry').style.display = '';
3135
+ try { const s = await updBridge.state(); updState = s; $('upd-ver').textContent = '当前 v' + (s.current || '?'); } catch {}
3136
+ }
2665
3137
  });
2666
3138
  $('comp-menu').addEventListener('change', (e) => {
2667
3139
  const c = e.target.dataset && e.target.dataset.comp;
@@ -2719,7 +3191,7 @@ $('btn-export-dash').onclick = () => { $('ex-provider').value = ''; openModal('m
2719
3191
  $('btn-export').onclick = () => {
2720
3192
  const v = state.view;
2721
3193
  // ZCode 是单一 provider,直接按 provider 选;其余产品页按产品线(p:);仪表盘 / 10Router 导出全部
2722
- $('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw') ? 'p:' + v : '';
3194
+ $('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw' || v === 'trae' || v === 'minimax') ? 'p:' + v : '';
2723
3195
  openModal('m-export');
2724
3196
  };
2725
3197
  $('btn-do-export').onclick = doExport;
@@ -2747,7 +3219,11 @@ function applyView() {
2747
3219
  $('view-prod').classList.toggle('active', state.view !== 'dash' && state.view !== 'tenrouter');
2748
3220
  $('view-10r').classList.toggle('active', state.view === 'tenrouter');
2749
3221
  if (state.view === 'tenrouter') loadZGateway();
3222
+ if (state.view === 'minimax') loadMGateway();
2750
3223
  if (state.view === 'tenrouter' && tr.config && tr.config.configured && !tr.quotas && !tr.loading) loadTrQuotas();
3224
+ followLogTab();
3225
+ applyLogTabs();
3226
+ loadCurrentLogs();
2751
3227
  render();
2752
3228
  renderStatus();
2753
3229
  }
@@ -2771,27 +3247,59 @@ $('region').addEventListener('click', (e) => {
2771
3247
  renderStatus();
2772
3248
  });
2773
3249
  $('f-provider').addEventListener('change', () => {
2774
- $('f-token').placeholder = $('f-provider').value.startsWith('workbuddy')
3250
+ $('f-token').placeholder = $('f-provider').value === 'minimax'
3251
+ ? 'MiniMax OAuth Bearer Token(mmoat_…)'
3252
+ : $('f-provider').value.startsWith('workbuddy')
2775
3253
  ? 'WorkBuddy / CodeBuddy 登录 token(eyJ…,可在 CodeBuddyExtension 的 auth/*.info 中找到)'
2776
3254
  : 'dt-… / jt-… 或 PAT(pt-…,自动兑换)';
2777
3255
  });
2778
3256
  $('btn-logs').onclick = () => {
2779
3257
  const c = $('log-card').classList.toggle('collapsed');
2780
3258
  $('btn-logs').textContent = c ? '展开' : '收起';
2781
- if (!c) { loadLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
3259
+ if (!c) { loadCurrentLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
2782
3260
  };
3261
+ $('log-tabs').addEventListener('click', (e) => {
3262
+ const b = e.target.closest('button[data-log-tab]');
3263
+ if (!b) return;
3264
+ state.logTab = b.dataset.logTab;
3265
+ logTabView = state.view; // 手动切换后,本视图内不再被 10s 轮询重置
3266
+ applyLogTabs();
3267
+ loadCurrentLogs();
3268
+ $('logs').scrollTop = $('logs').scrollHeight;
3269
+ });
3270
+
3271
+ // ── 仪表盘底部两卡:默认收起,点标题展开,展开状态存本地 ──
3272
+ const DASH_FOLD_KEY = 'qd_dash_fold';
3273
+ state.dashFold = (() => { try { return JSON.parse(localStorage.getItem(DASH_FOLD_KEY) || '{}'); } catch { return {}; } })();
3274
+ function applyDashFold() {
3275
+ document.querySelectorAll('.dash-cols .pcard[data-fold]').forEach((el) => {
3276
+ const open = state.dashFold[el.dataset.fold] === true;
3277
+ el.classList.toggle('open', open);
3278
+ el.querySelector('.fold-ico').innerHTML = open ? I.chevUp : I.chevDown;
3279
+ });
3280
+ }
3281
+ document.querySelectorAll('.dash-cols .fold-head').forEach((h) => {
3282
+ h.title = '展开 / 收起';
3283
+ h.addEventListener('click', () => {
3284
+ const k = h.closest('.pcard').dataset.fold;
3285
+ state.dashFold[k] = state.dashFold[k] !== true;
3286
+ try { localStorage.setItem(DASH_FOLD_KEY, JSON.stringify(state.dashFold)); } catch {}
3287
+ applyDashFold();
3288
+ });
3289
+ });
3290
+ applyDashFold();
2783
3291
 
2784
3292
  // ── 启动:先直接请求(本机模式无需密钥);只有服务端返回 401 时才询问密钥 ──
2785
3293
  initTheme();
2786
3294
  $('btn-home').innerHTML = I.github;
2787
3295
  applyView();
2788
-
2789
3296
  (function boot() {
2790
3297
  function start() {
2791
3298
  loadStatus().then(loadAccounts).catch((e) => toast(e.message, false));
2792
3299
  loadSettings().then(renderCompMenu);
2793
3300
  loadZNet();
2794
3301
  loadZGateway();
3302
+ loadMGateway();
2795
3303
  loadTrConfig().then(() => loadTrQuotas());
2796
3304
  setInterval(() => loadTrQuotas(), 5 * 60 * 1000);
2797
3305
  // 账号余额(ZCode 剩余 token 等)自动刷新:可见时每 2 分钟一轮;从托盘/后台切回时若超过 1 分钟没刷过立即补一轮
@@ -2801,7 +3309,7 @@ applyView();
2801
3309
  });
2802
3310
  loadLogs();
2803
3311
  setInterval(loadLogs, 10000);
2804
- setInterval(async () => { await loadStatus(); await loadZGateway(); render(); }, 30000);
3312
+ setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); await loadMGateway(); render(); }, 30000);
2805
3313
  }
2806
3314
  // 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
2807
3315
  api('/api/status').then(start).catch((e) => toast(e.code === 'PANEL_KEY'