creditdaddy 1.3.0 → 1.3.2

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
@@ -7,6 +7,7 @@
7
7
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiPjxwYXRoIGQ9Ik0zNzIuNDggMTM4LjY2NjY2N0ExMjggMTI4IDAgMCAwIDI5OC42NjY2NjcgMTAxLjU0NjY2N2ExMTcuNzYgMTE3Ljc2IDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzNsNzEuMjUzMzMzIDkxLjUyLTgxLjI4IDI4LjM3MzMzM2ExMC42NjY2NjcgMTAuNjY2NjY3IDAgMCAwLTIuMzQ2NjY3IDE4Ljk4NjY2NyAyNDIuNTYgMjQyLjU2IDAgMCAwIDY5Ljc2IDMyLjg1MzMzMyAyNzMuNzA2NjY3IDI3My43MDY2NjcgMCAwIDAgNTYuOTYgOC4zMnoiIGZpbGw9IiNGRjhBMTQiPjwvcGF0aD48cGF0aCBkPSJNMjQ1Ljc2IDEzOC4wMjY2NjdhMTMwLjk4NjY2NyAxMzAuOTg2NjY3IDAgMCAxIDUyLjA1MzMzMyAyLjEzMzMzM0ExNDguMDUzMzMzIDE0OC4wNTMzMzMgMCAwIDEgMzY2LjcyIDE3MC42NjY2NjdsNS43Ni0zMi44NTMzMzRBMTI4IDEyOCAwIDAgMCAyOTguNjY2NjY3IDEwMS41NDY2NjdhMTE3Ljk3MzMzMyAxMTcuOTczMzMzIDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzN6IiBmaWxsPSIjRkZBQTU0Ij48L3BhdGg+PHBhdGggZD0iTTIzNC42NjY2NjcgOTcwLjY2NjY2N2EyNzcuMzMzMzMzIDMyIDAgMSAwIDU1NC42NjY2NjYgMCAyNzcuMzMzMzMzIDMyIDAgMSAwLTU1NC42NjY2NjYgMFoiIGZpbGw9IiM0NTQxM0MiIG9wYWNpdHk9Ii4xNSI+PC9wYXRoPjxwYXRoIGQ9Ik02MjAuMzczMzMzIDQ2MC41ODY2NjdzMTUxLjA0LTkwLjY2NjY2NyAxODEuMzMzMzM0LTc0LjY2NjY2Ny0xMi41ODY2NjcgMTc0LjI5MzMzMy0xMDYuNjY2NjY3IDIxMy4zMzMzMzMtNzQuNjY2NjY3LTEzOC42NjY2NjctNzQuNjY2NjY3LTEzOC42NjY2NjZ6IiBmaWxsPSIjRkZFNTAwIj48L3BhdGg+PHBhdGggZD0iTTYyMC4zNzMzMzMgNTEzLjkyczE1MS4wNC05MC42NjY2NjcgMTgxLjMzMzMzNC03NC42NjY2NjdhMTQuNzIgMTQuNzIgMCAwIDEgNS4zMzMzMzMgNS45NzMzMzRjNi44MjY2NjctMjkuODY2NjY3IDUuMzMzMzMzLTUzLjU0NjY2Ny01LjMzMzMzMy01OS4zMDY2NjctMzAuMjkzMzMzLTE2LTE4MS4zMzMzMzMgNzQuNjY2NjY3LTE4MS4zMzMzMzQgNzQuNjY2NjY3YTM4Ni45ODY2NjcgMzg2Ljk4NjY2NyAwIDAgMCAwIDYxLjg2NjY2NmMtMC40MjY2NjctNS4zMzMzMzMgMC04LjUzMzMzMyAwLTguNTMzMzMzeiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik00MDMuNjI2NjY3IDQ2MC41ODY2NjdzLTE1MS4wNC05MC42NjY2NjctMTgxLjMzMzMzNC03NC42NjY2NjcgMTIuMzczMzMzIDE3NC4yOTMzMzMgMTA2LjY2NjY2NyAyMTMuMzMzMzMzIDc0LjY2NjY2Ny0xMzguNjY2NjY3IDc0LjY2NjY2Ny0xMzguNjY2NjY2eiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik0yMjIuMjkzMzMzIDQzOS4yNTMzMzNjMzAuMjkzMzMzLTE2IDE4MS4zMzMzMzMgNzQuNjY2NjY3IDE4MS4zMzMzMzQgNzQuNjY2NjY3djguNTMzMzMzYTM3My41NDY2NjcgMzczLjU0NjY2NyAwIDAgMCAwLTYxLjg2NjY2NnMtMTUxLjA0LTkwLjY2NjY2Ny0xODEuMzMzMzM0LTc0LjY2NjY2N2MtMTAuNjY2NjY3IDUuNzYtMTIuMTYgMjkuNDQtNS4zMzMzMzMgNTkuMzA2NjY3YTEzLjQ0IDEzLjQ0IDAgMCAxIDUuMzMzMzMzLTUuOTczMzM0eiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik0yODggNjUwLjY2NjY2N2EyNjYuNjY2NjY3IDIyNCA5MCAxIDAgNDQ4IDAgMjY2LjY2NjY2NyAyMjQgOTAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik02ODAuMTA2NjY3IDQ3NC4yNGEyMjQgMjI0IDAgMCAxLTMzNi4yMTMzMzQgMEEyMTAuNTYgMjEwLjU2IDAgMCAxIDUxMiAzODRhMjEwLjU2IDIxMC41NiAwIDAgMSAxNjguMTA2NjY3IDkwLjI0eiIgZmlsbD0iI0VCQ0IwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMjY2LjY2NjY2N20tMjI0IDBhMjI0IDIyNCAwIDEgMCA0NDggMCAyMjQgMjI0IDAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMTAxLjk3MzMzM2EyMjQuMjEzMzMzIDIyNC4yMTMzMzMgMCAwIDEgMjIxLjg2NjY2NyAxOTQuMzQ2NjY3IDIyNCAyMjQgMCAxIDAtNDQzLjczMzMzNCAwQTIyMy43ODY2NjcgMjIzLjc4NjY2NyAwIDAgMSA1MTIgMTAxLjk3MzMzM3oiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48cGF0aCBkPSJNODMzLjQ5MzMzMyA1OTguODI2NjY3bC03MC40IDcwLjRhMTguMzQ2NjY3IDE4LjM0NjY2NyAwIDAgMS0yNS44MTMzMzMgMEw2NDQuMDUzMzMzIDU3NkExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAwIDYxOC42NjY2NjcgNTc2bC05My4wMTMzMzQgOTMuMjI2NjY3YTE4Ljc3MzMzMyAxOC43NzMzMzMgMCAwIDEtMjYuMDI2NjY2IDBMNDA1LjMzMzMzMyA1NzZhMTguMTMzMzMzIDE4LjEzMzMzMyAwIDAgMC0yNS44MTMzMzMgMGwtOTMuMjI2NjY3IDkzLjIyNjY2N2ExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwbC03MC40LTcwLjRhOS4xNzMzMzMgOS4xNzMzMzMgMCAwIDAtMTUuNzg2NjY3IDYuODI2NjY2QzE4OS44NjY2NjcgODQxLjgxMzMzMyAzMzQuOTMzMzMzIDk2MCA1MTIgOTYwczMyMi4xMzMzMzMtMTE4LjE4NjY2NyAzMzcuMjgtMzU0LjM0NjY2N2E5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZ6IiBmaWxsPSIjRjBGMEYwIj48L3BhdGg+PHBhdGggZD0iTTE5MC41MDY2NjcgNjY1LjgxMzMzM2w3MC40IDYxLjIyNjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDI1LjgxMzMzMyAwbDkzLjIyNjY2Ny04MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMSAyNS44MTMzMzMgMGw5My4yMjY2NjcgODEuMDY2NjY3YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMjYuMDI2NjY2IDBsOTMuNjUzMzM0LTgxLjA2NjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAxIDI2LjAyNjY2NiAwbDkzLjIyNjY2NyA4MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMCAyNS44MTMzMzMgMGw3MC40LTYxLjIyNjY2N2E5LjgxMzMzMyA5LjgxMzMzMyAwIDAgMSA4LjUzMzMzNC0xLjkyIDUzNi4xMDY2NjcgNTM2LjEwNjY2NyAwIDAgMCA3LjI1MzMzMy01OC4yNCA5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZsLTcwLjQgNzAuNGExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwTDY0NC4wNTMzMzMgNTc2QTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDAgNjE4LjY2NjY2NyA1NzZsLTkzLjAxMzMzNCA5My4yMjY2NjdhMTguNzczMzMzIDE4Ljc3MzMzMyAwIDAgMS0yNi4wMjY2NjYgMEw0MDUuMzMzMzMzIDU3NmExOC4xMzMzMzMgMTguMTMzMzMzIDAgMCAwLTI1LjgxMzMzMyAwbC05My4yMjY2NjcgOTMuMjI2NjY3YTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDEtMjUuODEzMzMzIDBsLTcwLjQtNzAuNGE5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjcgNi44MjY2NjYgNTM3LjE3MzMzMyA1MzcuMTczMzMzIDAgMCAwIDcuMDQgNTguMjQgMTAuMDI2NjY3IDEwLjAyNjY2NyAwIDAgMSA5LjE3MzMzNCAxLjkyeiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPjxwYXRoIGQ9Ik00OTguMTMzMzMzIDE4NC41MzMzMzNtLTI5LjAxMzMzMyAwYTI5LjAxMzMzMyAyOS4wMTMzMzMgMCAxIDAgNTguMDI2NjY3IDAgMjkuMDEzMzMzIDI5LjAxMzMzMyAwIDEgMC01OC4wMjY2NjcgMFoiIGZpbGw9IiM0NTQxM0MiPjwvcGF0aD48cGF0aCBkPSJNNTc2IDI5MC45ODY2NjdjMCAxMS43MzMzMzMtMTUuNzg2NjY3IDIxLjMzMzMzMy0zNC45ODY2NjcgMjEuMzMzMzMzcy0zNS4yLTkuNi0zNS4yLTIxLjMzMzMzMyAxNS43ODY2NjctMjEuMzMzMzMzIDM1LjItMjEuMzMzMzM0IDM0Ljk4NjY2NyA5LjYgMzQuOTg2NjY3IDIxLjMzMzMzNHoiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48L3N2Zz4=">
8
8
  <style>
9
9
  :root {
10
+ color-scheme: light;
10
11
  --bg: #f5f6f8; --surface: #ffffff; --surface-2: #fafafa; --tertiary: #f0f0f0; --hover: #f5f6f8; --active: #eef0f3;
11
12
  --border-subtle: #eeeeee; --border: #e5e5e5; --border-strong: #cccccc;
12
13
  --text: #1f1f1f; --text-2: #5f6368; --text-3: #999999;
@@ -17,6 +18,7 @@
17
18
  --mask: rgba(0, 0, 0, .4);
18
19
  }
19
20
  html[data-theme="dark"] {
21
+ color-scheme: dark;
20
22
  --bg: #0a0a0c; --surface: #131316; --surface-2: #17171b; --tertiary: #1b1b20; --hover: rgba(255,255,255,.06); --active: rgba(255,255,255,.1);
21
23
  --border-subtle: #202025; --border: rgba(255,255,255,.13); --border-strong: #3a3a42;
22
24
  --text: #e6e6e9; --text-2: rgba(230,230,233,.72); --text-3: rgba(230,230,233,.5);
@@ -32,7 +34,7 @@
32
34
  font-size: 13px; color: var(--text); background: var(--bg); min-height: 100vh;
33
35
  -webkit-font-smoothing: antialiased; transition: background .2s;
34
36
  }
35
- button, input, select { font-family: inherit; font-size: inherit; color: inherit; }
37
+ button, input, select, textarea { font-family: inherit; font-size: inherit; color: inherit; }
36
38
  .wrap { max-width: 1120px; margin: 0 auto; padding: 18px 22px 40px; }
37
39
  .num { font-variant-numeric: tabular-nums; }
38
40
 
@@ -116,6 +118,8 @@
116
118
  .badge.intl { color: #3b82f6; background: rgba(59,130,246,.1); }
117
119
  .badge.cn { color: #d97706; background: rgba(245,158,11,.12); }
118
120
  .badge.prod { font-weight: 600; }
121
+ .badge.web { font-weight: 600; cursor: help; }
122
+ .badge.web-warn { color: var(--warn); background: var(--warn-soft); }
119
123
  .badge.prod.qoder { color: #6d28d9; background: rgba(124,58,237,.1); }
120
124
  .badge.prod.workbuddy { color: #0f766e; background: rgba(20,184,166,.12); }
121
125
  html[data-theme="dark"] .badge.prod.qoder { color: #b8a4ff; }
@@ -182,6 +186,13 @@
182
186
  .pcard.open .fold-body { display: block; }
183
187
  .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; }
184
188
  .logs .info { color: var(--text-2); } .logs .warn { color: var(--warn); } .logs .error { color: var(--err); } .logs .debug { color: var(--text-3); }
189
+ .logs .lvl-ico { margin-right: 5px; font-weight: 700; }
190
+ .logs .warn .lvl-ico { color: var(--warn); }
191
+ .logs .error .lvl-ico { color: var(--err); }
192
+ .log-filter { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
193
+ .log-filter button { border: none; background: transparent; color: var(--text-2); font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 7px; cursor: pointer; }
194
+ .log-filter button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
195
+ html[data-theme="dark"] .log-filter button.active { background: rgba(255,255,255,.1); }
185
196
  .collapsed .logs { display: none; }
186
197
 
187
198
  /* modal */
@@ -200,8 +211,10 @@
200
211
  .pane { display: none; } .pane.active { display: block; }
201
212
  .field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }
202
213
  .field label { font-size: 12px; color: var(--text-2); font-weight: 600; }
203
- .input { width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-2); outline: none; transition: border-color .15s; }
214
+ .input { width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-2); color: var(--text); outline: none; transition: border-color .15s; }
215
+ .input::placeholder { color: var(--text-3); opacity: .7; }
204
216
  .input:focus { border-color: #4f86ff; }
217
+ .input:disabled { opacity: .6; cursor: not-allowed; }
205
218
  .input.mono { font-family: ui-monospace, Consolas, monospace; }
206
219
  .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
207
220
  .hint { font-size: 12px; color: var(--text-3); line-height: 1.7; }
@@ -416,6 +429,7 @@
416
429
  <button class="btn" id="zt-open" style="display:none"></button>
417
430
  </span>
418
431
  <span id="t-actions" style="display:none">
432
+ <button class="btn" id="tt-gw"></button>
419
433
  <button class="btn" id="tt-open" style="display:none"></button>
420
434
  </span>
421
435
  <span id="m-actions" style="display:none">
@@ -444,6 +458,11 @@
444
458
  <button data-log-tab="run" class="active">运行日志</button>
445
459
  <button data-log-tab="zcode">ZCode 网关</button>
446
460
  <button data-log-tab="minimax">MiniMax 网关</button>
461
+ <button data-log-tab="trae">Trae 网关</button>
462
+ </div>
463
+ <div class="log-filter" id="log-filter">
464
+ <button data-log-level="all" class="active">全部</button>
465
+ <button data-log-level="warn">只看提醒</button>
447
466
  </div>
448
467
  <button class="btn" id="btn-logs">展开</button>
449
468
  </div>
@@ -510,6 +529,7 @@
510
529
  <input class="input" id="ex-password" type="password" placeholder="至少 4 位" autocomplete="new-password">
511
530
  </div>
512
531
  <div class="hint">导出文件一律加密(10router 迁移格式,scrypt + AES-256-GCM),可直接在 10router 对应提供商页导入(WorkBuddy 对应 10router 的 CodeBuddy)。10router 按提供商导入,建议按单个版本导出。<b>口令不会被保存</b>,请牢记——遗失后文件无法解开、无法导入。</div>
532
+ <div class="notice" id="ex-session-note" style="display:none"></div>
513
533
  <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-do-export">导出</button></div>
514
534
  </div>
515
535
  </div>
@@ -533,6 +553,12 @@
533
553
  <label>版本与更新</label>
534
554
  <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
555
  </div>
556
+ <div class="field" style="margin-top:12px;padding-top:10px;border-top:1px dashed var(--border-subtle)">
557
+ <label>服务维护</label>
558
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
559
+ <button class="btn" id="btn-restart-app">重启 CreditDaddy</button>
560
+ </div>
561
+ </div>
536
562
  <div class="modal-actions"><button class="btn" data-close>关闭</button></div>
537
563
  </div>
538
564
  </div>
@@ -641,14 +667,14 @@
641
667
  <div class="modal-title"><span>ZCode 体验包接口</span><button class="close" data-close>✕</button></div>
642
668
  <div class="hint" id="zgw-url" style="margin-bottom:10px"></div>
643
669
  <div class="field">
644
- <label class="check"><input type="checkbox" id="zgw-on"> 启用体验包接口(/gateway/v1/messages)</label>
670
+ <label class="check"><input type="checkbox" id="zgw-on"> 启用体验包接口(/gateway/zcode/v1/messages)</label>
645
671
  </div>
646
672
  <div class="field" id="zgw-lan-fields">
647
673
  <label class="check"><input type="checkbox" id="zgw-lan"> 对局域网开放(重启后绑定 0.0.0.0)</label>
648
674
  <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
649
675
  <textarea class="input" id="zgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
650
676
  </div>
651
- <div class="hint">开启后,领到的体验包额度变成本机的一个补全接口——10Router 建一个 anthropic-compatible 节点指向本机 /gateway 即可调用(需要桌面版,隐藏窗口过验证码)。白名单内的机器<b>免密直连</b>:10Router 的 zcode-free 本就是免授权供应商,把 10Router 服务地址的 IP 加白即可;白名单外的请求一律拒绝。本机回环(127.0.0.1)始终放行。</div>
677
+ <div class="hint">开启后,领到的体验包额度变成本机的一个补全接口——10Router 建一个 anthropic-compatible 节点指向本机 <code>/gateway/zcode/v1/messages</code> 即可调用(需要桌面版,隐藏窗口过验证码)。白名单内的机器<b>免密直连</b>:10Router 的 zcode-free 本就是免授权供应商,把 10Router 服务地址的 IP 加白即可;白名单外的请求一律拒绝。本机回环(127.0.0.1)始终放行。</div>
652
678
  <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="zgw-ok">保存</button></div>
653
679
  </div>
654
680
  </div>
@@ -662,7 +688,7 @@
662
688
  <label class="check"><input type="checkbox" id="mgw-on"> 启用本地接口(/gateway/minimax/v1/messages)</label>
663
689
  </div>
664
690
  <div class="field" id="mgw-lan-fields">
665
- <label class="check"><input type="checkbox" id="mgw-lan"> 对局域网开放(需全局局域网已开启)</label>
691
+ <label class="check"><input type="checkbox" id="mgw-lan"> 对局域网开放(重启后绑定 0.0.0.0)</label>
666
692
  <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
667
693
  <textarea class="input" id="mgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
668
694
  </div>
@@ -671,6 +697,24 @@
671
697
  </div>
672
698
  </div>
673
699
 
700
+ <!-- Trae 本地接口 -->
701
+ <div class="mask" id="m-tgw">
702
+ <div class="modal sm">
703
+ <div class="modal-title"><span>Trae 本地接口</span><button class="close" data-close>✕</button></div>
704
+ <div class="hint" id="tgw-url" style="margin-bottom:10px"></div>
705
+ <div class="field">
706
+ <label class="check"><input type="checkbox" id="tgw-on"> 启用本地接口(/gateway/trae/v1/messages)</label>
707
+ </div>
708
+ <div class="field" id="tgw-lan-fields">
709
+ <label class="check"><input type="checkbox" id="tgw-lan"> 对局域网开放(重启后绑定 0.0.0.0)</label>
710
+ <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
711
+ <textarea class="input" id="tgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
712
+ </div>
713
+ <div class="hint">开启后,Trae SOLO 积分与模型能力(Doubao-Seed / GLM-5.1 / Kimi-K2.6 / DeepSeek-V4 等 12 款模型)变成本机的一个原生 Anthropic Messages 补全接口——在 10Router 或 Claude Code 等工具中建一个 anthropic-compatible 节点指向本机 <code>/gateway/trae/v1/messages</code> 即可调用。支持多账号自动加权轮转、429 限流自动冷却 5 分钟、流式增量与完整思考过程(thinking_delta)。白名单内的机器<b>免密直连</b>;本机回环(127.0.0.1)始终放行。</div>
714
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="tgw-ok">保存</button></div>
715
+ </div>
716
+ </div>
717
+
674
718
  <input type="file" id="file-input" accept=".json,application/json" style="display:none">
675
719
  <div id="toast"></div>
676
720
 
@@ -700,7 +744,7 @@ const I = {
700
744
  };
701
745
 
702
746
  const $ = (id) => document.getElementById(id);
703
- const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run' };
747
+ const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run', logLevel: 'all' };
704
748
  const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'tenrouter'];
705
749
  // 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
706
750
  const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
@@ -853,7 +897,7 @@ const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', m
853
897
  // 额度单位为 Credits/时长而非「积分」口径的产品(不计入剩余积分合计)
854
898
  const CREDIT_VIEWS = new Set(['zcode', 'mirasim', 'catpaw']);
855
899
  // 网关调用日志:tag → 所属产品页(无网关的产品不展示日志卡)
856
- const GW_LOG_TAG = { zcode: 'ZCODE-GW', minimax: 'MINIMAX-GW' };
900
+ const GW_LOG_TAG = { zcode: 'ZCODE-GW', minimax: 'MINIMAX-GW', trae: 'TRAE-GW' };
857
901
  const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
858
902
  const provBadges = (p, regionOnly, flavor) => {
859
903
  const v = provOf(p);
@@ -964,8 +1008,20 @@ function applyTheme(t) {
964
1008
  function initTheme() {
965
1009
  let t = null;
966
1010
  try { t = localStorage.getItem('qd_theme'); } catch (e) {}
967
- if (!t) t = window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1011
+ const mq = window.matchMedia ? matchMedia('(prefers-color-scheme: dark)') : null;
1012
+ if (!t) t = mq && mq.matches ? 'dark' : 'light';
968
1013
  applyTheme(t);
1014
+ // 自动跟随系统:用户没手动切过(localStorage 无值)时,系统换深色/浅色实时响应,
1015
+ // 不用刷新页面;手动切过则尊重用户选择,不再跟系统跑。
1016
+ const onSysTheme = (e) => {
1017
+ let saved = null;
1018
+ try { saved = localStorage.getItem('qd_theme'); } catch (err) {}
1019
+ if (!saved) applyTheme(e.matches ? 'dark' : 'light');
1020
+ };
1021
+ if (mq) {
1022
+ if (mq.addEventListener) mq.addEventListener('change', onSysTheme);
1023
+ else if (mq.addListener) mq.addListener(onSysTheme); // 旧 Safari
1024
+ }
969
1025
  $('btn-theme').onclick = () => {
970
1026
  const n = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
971
1027
  applyTheme(n);
@@ -1142,7 +1198,9 @@ function cardHtml(a) {
1142
1198
  + (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
1143
1199
  + (a.product === 'zcode' && isGatewayCurrent(a) ? '<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1144
1200
  + (a.product === 'minimax' && isMiniMaxGatewayCurrent(a) ? '<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1201
+ + (a.product === 'trae' && isTraeGatewayCurrent(a) ? '<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1145
1202
  + (a.isPat ? '<span class="badge">PAT</span>' : '')
1203
+ + qoderWebBadge(a)
1146
1204
  + (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
1147
1205
  + (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>' : '')
1148
1206
  + '</div><div class="ops">'
@@ -1183,6 +1241,11 @@ function isMiniMaxGatewayCurrent(a) {
1183
1241
  const last = state.mGatewayStats && state.mGatewayStats.lastAccount;
1184
1242
  return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
1185
1243
  }
1244
+ /** Trae 本地接口使用中判定 */
1245
+ function isTraeGatewayCurrent(a) {
1246
+ const last = state.tGatewayStats && state.tGatewayStats.lastAccount;
1247
+ return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
1248
+ }
1186
1249
 
1187
1250
  // 客户端当前登录判定:WorkBuddy / mirasim / 妙手 / MiniMax 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
1188
1251
  function isClientCurrent(a) {
@@ -1295,6 +1358,9 @@ function renderProd() {
1295
1358
  }
1296
1359
  $('t-actions').style.display = prod === 'trae' ? 'contents' : 'none';
1297
1360
  if (prod === 'trae') {
1361
+ const gwOn = state.tGateway === true;
1362
+ $('tt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.tGatewayLan === true ? '·局域网' : '');
1363
+ $('tt-gw').title = 'Trae 本地接口:开关、局域网开放与 IP 白名单';
1298
1364
  const tc = (state.status && state.status.traeClient) || {};
1299
1365
  const canOpen = tc.installed && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function';
1300
1366
  $('tt-open').style.display = canOpen ? '' : 'none';
@@ -1600,6 +1666,21 @@ document.addEventListener('click', async (e) => {
1600
1666
  } catch (err) { toast(String(err && err.message || err), false); }
1601
1667
  return;
1602
1668
  }
1669
+ if (e.target.closest('#tt-gw')) {
1670
+ // Trae 本地接口弹窗:开关 + 局域网 + 白名单设置
1671
+ $('tgw-on').checked = state.tGateway === true;
1672
+ $('tgw-lan').checked = state.tGatewayLan === true;
1673
+ const allow = Array.isArray(state.tGatewayAllow) ? state.tGatewayAllow : [];
1674
+ $('tgw-allow').value = (allow.length ? allow : (state.tGatewaySuggest ? [state.tGatewaySuggest] : [])).join('\n');
1675
+ const st = state.tGatewayStats;
1676
+ $('tgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/trae/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
1677
+ + (st && st.lastCallAt
1678
+ ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1679
+ : '<br>尚未有调用');
1680
+ syncTgwLanFields();
1681
+ openModal('m-tgw');
1682
+ return;
1683
+ }
1603
1684
  if (e.target.closest('#mt-gw')) {
1604
1685
  // MiniMax 本地接口弹窗:开关 + 局域网 + 白名单设置
1605
1686
  $('mgw-on').checked = state.mGateway === true;
@@ -1631,7 +1712,7 @@ document.addEventListener('click', async (e) => {
1631
1712
  const allow = Array.isArray(state.zGatewayAllow) ? state.zGatewayAllow : [];
1632
1713
  $('zgw-allow').value = (allow.length ? allow : (state.zGatewaySuggest ? [state.zGatewaySuggest] : [])).join('\n');
1633
1714
  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)'
1715
+ $('zgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/zcode/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
1635
1716
  + (st && st.lastCallAt
1636
1717
  ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1637
1718
  : '<br>尚未有调用');
@@ -1685,6 +1766,18 @@ async function loadMGateway() {
1685
1766
  applyLogTabs(); // 网关开关变化影响日志标签显隐
1686
1767
  } catch (e) {}
1687
1768
  }
1769
+ async function loadTGateway() {
1770
+ try {
1771
+ const d = await api('/api/trae-gateway');
1772
+ state.tGateway = d.enabled === true;
1773
+ state.tGatewayLan = d.lan === true;
1774
+ state.tGatewayAllow = d.allow || [];
1775
+ state.tGatewaySuggest = d.suggestAllow || '';
1776
+ state.tGatewayStats = d.stats || null;
1777
+ if (state.view === 'trae') { renderStatus(); render(); }
1778
+ applyLogTabs(); // 网关开关变化影响日志标签显隐
1779
+ } catch (e) {}
1780
+ }
1688
1781
 
1689
1782
  // ── 10Router ──
1690
1783
  function quotaPct(q) {
@@ -2212,9 +2305,13 @@ function applyLogTabs() {
2212
2305
  let show = true;
2213
2306
  if (t === 'zcode') show = Boolean(state.zGateway);
2214
2307
  if (t === 'minimax') show = Boolean(state.mGateway);
2308
+ if (t === 'trae') show = Boolean(state.tGateway);
2215
2309
  b.style.display = show ? '' : 'none';
2216
2310
  b.classList.toggle('active', t === state.logTab);
2217
2311
  });
2312
+ document.querySelectorAll('#log-filter button').forEach((b) => {
2313
+ b.classList.toggle('active', b.dataset.logLevel === state.logLevel);
2314
+ });
2218
2315
  }
2219
2316
  async function loadCurrentLogs() {
2220
2317
  const el = $('logs');
@@ -2228,11 +2325,15 @@ async function loadCurrentLogs() {
2228
2325
  const data = await api('/api/logs?tag=' + encodeURIComponent(GW_LOG_TAG[state.logTab]));
2229
2326
  logs = data.logs || [];
2230
2327
  }
2328
+ if (state.logLevel !== 'all') {
2329
+ logs = logs.filter((l) => l.level === 'warn' || l.level === 'error');
2330
+ }
2231
2331
  const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
2232
2332
  const withTag = state.logTab === 'run';
2333
+ const ico = (lvl) => lvl === 'error' ? '✗' : lvl === 'warn' ? '⚠' : lvl === 'info' ? '·' : '';
2233
2334
  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
- : '(暂无)';
2335
+ ? logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] <span class="lvl-ico">' + ico(l.level) + '</span>' + (withTag ? esc(l.tag) + ' · ' : '') + esc(l.msg) + '</div>').join('')
2336
+ : (state.logLevel === 'all' ? '(暂无)' : '(暂无警告或错误)');
2236
2337
  if (atBottom) el.scrollTop = el.scrollHeight;
2237
2338
  } catch (e) {}
2238
2339
  }
@@ -2528,6 +2629,57 @@ function okDialog() { const v = $('dlg-field').style.display === 'none' ? true :
2528
2629
  $('dlg-ok').addEventListener('click', okDialog);
2529
2630
  $('dlg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') okDialog(); });
2530
2631
 
2632
+ // ── 重启服务(桌面客户端或 CLI) ──
2633
+ async function restartCreditDaddy() {
2634
+ const mask = document.createElement('div');
2635
+ mask.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-size:15px;gap:12px;backdrop-filter:blur(4px);user-select:none;';
2636
+ mask.innerHTML = '<div style="font-size:28px">⏳</div><div style="font-weight:600">正在重启 CreditDaddy,请稍候…</div><div style="font-size:12px;color:rgba(255,255,255,.6)">服务恢复后将自动刷新页面</div>';
2637
+ document.body.appendChild(mask);
2638
+
2639
+ try {
2640
+ if (window.creditdaddy && typeof window.creditdaddy.restartApp === 'function') {
2641
+ await window.creditdaddy.restartApp();
2642
+ } else {
2643
+ await api('/api/system/restart', { method: 'POST' });
2644
+ }
2645
+ } catch {}
2646
+
2647
+ let attempts = 0;
2648
+ const poll = setInterval(async () => {
2649
+ attempts++;
2650
+ try {
2651
+ const res = await fetch('/api/status', { cache: 'no-store' });
2652
+ if (res.ok) {
2653
+ clearInterval(poll);
2654
+ location.reload();
2655
+ }
2656
+ } catch {}
2657
+ if (attempts > 30) {
2658
+ clearInterval(poll);
2659
+ mask.innerHTML = '<div>重启超时或端口已改变</div><button class="btn primary" onclick="location.reload()" style="margin-top:10px">手动刷新</button>';
2660
+ }
2661
+ }, 1000);
2662
+ }
2663
+
2664
+ async function maybePromptRestartForLan(oldLan, newLan) {
2665
+ if (oldLan !== newLan) {
2666
+ const ok = await uiConfirm('局域网开放绑定改动需重启服务生效,是否立即重启 CreditDaddy?', '配置已保存', '立即重启');
2667
+ if (ok) {
2668
+ await restartCreditDaddy();
2669
+ return true;
2670
+ }
2671
+ }
2672
+ return false;
2673
+ }
2674
+
2675
+ $('btn-restart-app').addEventListener('click', async () => {
2676
+ const ok = await uiConfirm('确定要重启 CreditDaddy 服务吗?正在进行的任务可能会中断。', '重启服务', '重启');
2677
+ if (ok) {
2678
+ closeModal('m-settings');
2679
+ await restartCreditDaddy();
2680
+ }
2681
+ });
2682
+
2531
2683
  // ── 检查更新(桌面版):主进程推状态(update:state),面板只管渲染 ──
2532
2684
  const updBridge = window.creditdaddy && window.creditdaddy.update ? window.creditdaddy.update : null;
2533
2685
  let updState = null;
@@ -2633,6 +2785,7 @@ function syncZgwLanFields() {
2633
2785
  $('zgw-on').addEventListener('change', syncZgwLanFields);
2634
2786
  $('zgw-ok').addEventListener('click', async () => {
2635
2787
  const allow = $('zgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
2788
+ const oldLan = state.zGatewayLan;
2636
2789
  try {
2637
2790
  const r = await api('/api/zcode-gateway', { method: 'PUT', json: { enabled: $('zgw-on').checked, lan: $('zgw-lan').checked, allow } });
2638
2791
  state.zGateway = r.enabled === true;
@@ -2640,6 +2793,7 @@ $('zgw-ok').addEventListener('click', async () => {
2640
2793
  state.zGatewayAllow = r.allow || [];
2641
2794
  renderStatus(); render();
2642
2795
  closeModal('m-zgw');
2796
+ if (await maybePromptRestartForLan(oldLan, state.zGatewayLan)) return;
2643
2797
  toast('体验包接口:' + (r.enabled ? '已开启' : '已关闭')
2644
2798
  + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : ',白名单为空——白名单外需虚拟 key') + ';绑定改动重启后生效' : ''));
2645
2799
  } catch (err) { toast(err.message, false); }
@@ -2654,6 +2808,7 @@ function syncMgwLanFields() {
2654
2808
  $('mgw-on').addEventListener('change', syncMgwLanFields);
2655
2809
  $('mgw-ok').addEventListener('click', async () => {
2656
2810
  const allow = $('mgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
2811
+ const oldLan = state.mGatewayLan;
2657
2812
  try {
2658
2813
  const r = await api('/api/minimax-gateway', { method: 'PUT', json: { enabled: $('mgw-on').checked, lan: $('mgw-lan').checked, allow } });
2659
2814
  state.mGateway = r.enabled === true;
@@ -2661,8 +2816,32 @@ $('mgw-ok').addEventListener('click', async () => {
2661
2816
  state.mGatewayAllow = r.allow || [];
2662
2817
  renderStatus(); render();
2663
2818
  closeModal('m-mgw');
2819
+ if (await maybePromptRestartForLan(oldLan, state.mGatewayLan)) return;
2664
2820
  toast('MiniMax 接口:' + (r.enabled ? '已开启' : '已关闭')
2665
- + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : '') : ''));
2821
+ + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : '') + ';绑定改动重启后生效' : ''));
2822
+ } catch (err) { toast(err.message, false); }
2823
+ });
2824
+
2825
+ // Trae 本地接口:开关 + 局域网开放 + 免密 IP 白名单一次保存
2826
+ function syncTgwLanFields() {
2827
+ const off = !$('tgw-on').checked;
2828
+ $('tgw-lan-fields').querySelectorAll('input, textarea').forEach((el) => { el.disabled = off; });
2829
+ $('tgw-lan-fields').style.opacity = off ? '.45' : '';
2830
+ }
2831
+ $('tgw-on').addEventListener('change', syncTgwLanFields);
2832
+ $('tgw-ok').addEventListener('click', async () => {
2833
+ const allow = $('tgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
2834
+ const oldLan = state.tGatewayLan;
2835
+ try {
2836
+ const r = await api('/api/trae-gateway', { method: 'PUT', json: { enabled: $('tgw-on').checked, lan: $('tgw-lan').checked, allow } });
2837
+ state.tGateway = r.enabled === true;
2838
+ state.tGatewayLan = r.lan === true;
2839
+ state.tGatewayAllow = r.allow || [];
2840
+ renderStatus(); render();
2841
+ closeModal('m-tgw');
2842
+ if (await maybePromptRestartForLan(oldLan, state.tGatewayLan)) return;
2843
+ toast('Trae 接口:' + (r.enabled ? '已开启' : '已关闭')
2844
+ + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : '') + ';绑定改动重启后生效' : ''));
2666
2845
  } catch (err) { toast(err.message, false); }
2667
2846
  });
2668
2847
 
@@ -2844,6 +3023,53 @@ async function addToken() {
2844
3023
  b.disabled = false;
2845
3024
  }
2846
3025
 
3026
+ // Qoder 卡片徽章:网页会话(Cookie)缺失或超 7 天时点名提醒——
3027
+ // 10Router 的「套餐内 Credits」逐资源包明细依赖它(同 qoderWebSessionNote 口径)。
3028
+ // 正常状态不出徽章,只在需要「浏览器登录」补 cookie 时出橙色警示,避免噪音。
3029
+ function qoderWebBadge(a) {
3030
+ if (!String(a.provider || '').startsWith('qoder')) return '';
3031
+ const at = a.qoderWebCapturedAt;
3032
+ if (at && Date.now() - new Date(at).getTime() <= 7 * 86400000) return '';
3033
+ const stale = Boolean(at);
3034
+ const tip = stale
3035
+ ? '网页会话已超过 7 天(' + fmtTime(at).slice(0, 10) + ' 抓取),可能已过期:导出到 10Router 后「套餐内 Credits」可能缺失,用「浏览器登录」重新登录一次 Qoder 即可补上'
3036
+ : '缺少 Qoder 网页会话(Cookie):导出到 10Router 后「套餐内 Credits」可能缺失,用「浏览器登录」登录一次 Qoder 即可自动抓取';
3037
+ return '<span class="badge web web-warn" title="' + esc(tip) + '">' + (stale ? '网页会话过期' : '缺网页会话') + '</span>';
3038
+ }
3039
+
3040
+ // 导出弹窗里的网页会话提示(10Router issue #44):
3041
+ // 10Router 的「套餐内 Credits」逐资源包明细依赖随账号导出的 Qoder 网页会话
3042
+ // (openapi 聚合值在部分账号上会归零)。会话缺失或明显过期 → 导出前点名提醒,
3043
+ // 否则用户在 10Router 端只会看到套餐积分凭空消失。
3044
+ function qoderWebSessionNote() {
3045
+ const sel = $('ex-provider').value;
3046
+ const list = state.accounts.filter((a) => {
3047
+ if (!String(a.provider || '').startsWith('qoder')) return false;
3048
+ if (!sel || sel === 'p:qoder') return true;
3049
+ return a.provider === sel;
3050
+ });
3051
+ if (!list.length) return '';
3052
+ const missing = [], stale = [];
3053
+ for (const a of list) {
3054
+ const at = a.qoderWebCapturedAt;
3055
+ if (!at) missing.push(a);
3056
+ else if (Date.now() - new Date(at).getTime() > 7 * 86400000) stale.push(a);
3057
+ }
3058
+ if (!missing.length && !stale.length) return '';
3059
+ const names = (arr) => arr.map((x) => x.name || x.email || x.uid || x.id).join('、');
3060
+ const bits = [];
3061
+ if (missing.length) bits.push('没有网页会话:' + names(missing));
3062
+ if (stale.length) bits.push('会话已超过 7 天:' + names(stale));
3063
+ return '10router 的「套餐内 Credits」逐资源包明细依赖 Qoder 网页会话(' + bits.join(';')
3064
+ + ')。这些账号过去后套餐积分可能显示缺失——在「浏览器登录」重新登录一次 Qoder 再导出即可补上。';
3065
+ }
3066
+ function refreshExportNote() {
3067
+ const msg = qoderWebSessionNote();
3068
+ const el = $('ex-session-note');
3069
+ el.textContent = msg;
3070
+ el.style.display = msg ? '' : 'none';
3071
+ }
3072
+
2847
3073
  // 导出
2848
3074
  async function doExport() {
2849
3075
  const password = $('ex-password').value;
@@ -3187,13 +3413,15 @@ $('btn-checkin-all').onclick = () => checkinAll($('btn-checkin-all'));
3187
3413
  $('btn-checkin').onclick = () => checkinAll($('btn-checkin'), state.view);
3188
3414
  $('btn-add-dash').onclick = () => openAdd('');
3189
3415
  $('btn-add').onclick = () => openAdd(state.view);
3190
- $('btn-export-dash').onclick = () => { $('ex-provider').value = ''; openModal('m-export'); };
3416
+ $('btn-export-dash').onclick = () => { $('ex-provider').value = ''; refreshExportNote(); openModal('m-export'); };
3191
3417
  $('btn-export').onclick = () => {
3192
3418
  const v = state.view;
3193
3419
  // ZCode 是单一 provider,直接按 provider 选;其余产品页按产品线(p:);仪表盘 / 10Router 导出全部
3194
3420
  $('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw' || v === 'trae' || v === 'minimax') ? 'p:' + v : '';
3421
+ refreshExportNote();
3195
3422
  openModal('m-export');
3196
3423
  };
3424
+ $('ex-provider').addEventListener('change', refreshExportNote);
3197
3425
  $('btn-do-export').onclick = doExport;
3198
3426
  $('btn-pass-ok').onclick = () => submitImport($('pass-input').value);
3199
3427
  $('pass-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') submitImport($('pass-input').value); });
@@ -3220,6 +3448,7 @@ function applyView() {
3220
3448
  $('view-10r').classList.toggle('active', state.view === 'tenrouter');
3221
3449
  if (state.view === 'tenrouter') loadZGateway();
3222
3450
  if (state.view === 'minimax') loadMGateway();
3451
+ if (state.view === 'trae') loadTGateway();
3223
3452
  if (state.view === 'tenrouter' && tr.config && tr.config.configured && !tr.quotas && !tr.loading) loadTrQuotas();
3224
3453
  followLogTab();
3225
3454
  applyLogTabs();
@@ -3267,6 +3496,14 @@ $('log-tabs').addEventListener('click', (e) => {
3267
3496
  loadCurrentLogs();
3268
3497
  $('logs').scrollTop = $('logs').scrollHeight;
3269
3498
  });
3499
+ $('log-filter').addEventListener('click', (e) => {
3500
+ const b = e.target.closest('button[data-log-level]');
3501
+ if (!b) return;
3502
+ state.logLevel = b.dataset.logLevel;
3503
+ applyLogTabs();
3504
+ loadCurrentLogs();
3505
+ $('logs').scrollTop = $('logs').scrollHeight;
3506
+ });
3270
3507
 
3271
3508
  // ── 仪表盘底部两卡:默认收起,点标题展开,展开状态存本地 ──
3272
3509
  const DASH_FOLD_KEY = 'qd_dash_fold';
@@ -3300,6 +3537,7 @@ applyView();
3300
3537
  loadZNet();
3301
3538
  loadZGateway();
3302
3539
  loadMGateway();
3540
+ loadTGateway();
3303
3541
  loadTrConfig().then(() => loadTrQuotas());
3304
3542
  setInterval(() => loadTrQuotas(), 5 * 60 * 1000);
3305
3543
  // 账号余额(ZCode 剩余 token 等)自动刷新:可见时每 2 分钟一轮;从托盘/后台切回时若超过 1 分钟没刷过立即补一轮
@@ -3309,7 +3547,7 @@ applyView();
3309
3547
  });
3310
3548
  loadLogs();
3311
3549
  setInterval(loadLogs, 10000);
3312
- setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); await loadMGateway(); render(); }, 30000);
3550
+ setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); await loadMGateway(); await loadTGateway(); render(); }, 30000);
3313
3551
  }
3314
3552
  // 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
3315
3553
  api('/api/status').then(start).catch((e) => toast(e.code === 'PANEL_KEY'
package/src/store.js CHANGED
@@ -98,7 +98,16 @@ export async function loadAccounts() {
98
98
 
99
99
  export async function saveAccounts(accounts) {
100
100
  await ensureDir();
101
- await atomicWrite(ACCOUNTS_FILE(), JSON.stringify(accounts, null, 2));
101
+ const file = ACCOUNTS_FILE();
102
+ const bak = file + '.bak';
103
+ try {
104
+ // 只有当已有账号文件存在且非空时才建立/更新 .bak 备份
105
+ const existing = await fs.readFile(file, 'utf8').catch(() => null);
106
+ if (existing && existing.trim().length > 2) {
107
+ await atomicWrite(bak, existing);
108
+ }
109
+ } catch {}
110
+ await atomicWrite(file, JSON.stringify(accounts, null, 2));
102
111
  }
103
112
 
104
113
  // 进程内串行队列:所有「读-改-写」都经由 withAccounts,避免并发请求互相覆盖
@@ -200,6 +209,12 @@ export function publicAccount(a) {
200
209
  phone: maskPhone(meta.phone),
201
210
  domain: meta.domain || null,
202
211
  flavor: a.provider === 'zcode' ? zcodeFlavor(meta) : null,
212
+ // Qoder 网页会话新鲜度(10Router issue #44):只回时间戳、绝不回 Cookie。
213
+ // 10Router 的「套餐内 Credits」明细依赖导出的这份会话;面板据此在
214
+ // 导出/同步缺会话或明显过期的 Qoder 账号时给出提示。
215
+ qoderWebCapturedAt: String(a.provider).startsWith('qoder')
216
+ ? (meta.qoderWebSession?.capturedAt || null)
217
+ : null,
203
218
  canSwitch: Boolean(meta.session?.account || meta.credentials || meta.qoderAuth || a.provider === 'catpaw' || a.provider === 'trae'),
204
219
  verified: a.verified ?? null,
205
220
  createdAt: a.createdAt,
@@ -92,6 +92,9 @@ async function syncViaTransfer({ endpoint, cookie, groups }) {
92
92
  results.push({
93
93
  provider: target, count: list.length, status: (d.failed || 0) ? 'partial' : 'ok',
94
94
  imported: d.imported || 0, updated: d.updated || 0, skipped: d.skipped || 0, failed: d.failed || 0,
95
+ // 10Router issue #44:导入端会探测刚同步过去的网页会话是否仍可用,
96
+ // 失败清单直接回显到本端摘要(见 syncAccountsTo10r 的 sessionWarns)。
97
+ webSessions: d.webSessions || null,
95
98
  });
96
99
  }
97
100
  return results;
@@ -103,9 +106,15 @@ async function syncViaProviders({ endpoint, key, groups }) {
103
106
  for (const [target, list] of groups) {
104
107
  const row = { provider: target, count: list.length, status: 'ok', imported: 0, skipped: 0, failed: 0 };
105
108
  for (const a of list) {
106
- const r = await trFetch(`${endpoint}/api/providers`, {
107
- key, method: 'POST', body: { provider: target, apiKey: a.token, name: connName(a) },
108
- });
109
+ // issue #44:qoder 的 apikey 连接同样按 providerSpecificData 读网页会话
110
+ // (usage 侧 getQoderUsage 不区分 authType),所以新建连接时把会话一并
111
+ // 挂上;已存在的同名连接此接口不更新(409),缺失由同步摘要点名提醒。
112
+ const web = a.meta?.qoderWebSession?.cookie && String(a.provider).startsWith('qoder')
113
+ ? { creditDaddyWebSession: { cookie: a.meta.qoderWebSession.cookie, capturedAt: a.meta.qoderWebSession.capturedAt || null, userId: a.uid || null } }
114
+ : null;
115
+ const body = { provider: target, apiKey: a.token, name: connName(a) };
116
+ if (web) body.providerSpecificData = { userId: a.uid || null, ...web };
117
+ const r = await trFetch(`${endpoint}/api/providers`, { key, method: 'POST', body });
109
118
  if (r.status === 201 || r.status === 200) row.imported++;
110
119
  else if (r.status === 409) row.skipped++;
111
120
  else { row.failed++; row.error = r.data?.error || `HTTP ${r.status}`; }
@@ -163,6 +172,25 @@ export async function syncAccountsTo10r({ endpoint, key, adminPassword, accounts
163
172
  if (sum('skipped')) parts.push(`已存在 ${sum('skipped')}`);
164
173
  if (sum('failed')) parts.push(`失败 ${sum('failed')}`);
165
174
  if (unmapped.size) parts.push('跳过 ' + [...unmapped].map(([p, n]) => `${p} ×${n}`).join('、'));
175
+ // issue #44:Qoder 网页会话决定 10Router 端「套餐内 Credits」能不能显示。
176
+ // OAuth 通道:10Router 导入时已逐账号探测,失效清单原样带回(deadNames);
177
+ // 两条通道都要点名本地就没有会话的账号(apikey 通道 webSessions 恒为
178
+ // null,条件自动适用;OAuth 通道里已被探测过的组不重复报)。
179
+ const deadNames = [];
180
+ for (const r of results) {
181
+ for (const f of r.webSessions?.failures || []) deadNames.push(f.name);
182
+ }
183
+ const sessionless = [];
184
+ for (const [target, list] of groups) {
185
+ if (!String(target).startsWith('qoder')) continue;
186
+ for (const a of list) {
187
+ if (results.find((r) => r.provider === target)?.webSessions == null && !a.meta?.qoderWebSession?.cookie) {
188
+ sessionless.push(connName(a));
189
+ }
190
+ }
191
+ }
192
+ if (sessionless.length) parts.push(`未带网页会话 ${sessionless.length}(${sessionless.join('、')},在 CreditDaddy 重新浏览器登录 Qoder 后再同步可补齐逐资源包明细)`);
193
+ if (deadNames.length) parts.push(`网页会话已失效 ${deadNames.length}(${deadNames.join('、')},重新登录 Qoder 网页后再次同步)`);
166
194
  const summary = (channel === 'oauth' ? '账号同步完成(OAuth 通道):' : '账号同步完成(apikey 通道,token 过期后需重新同步):')
167
195
  + (parts.join(',') || '无变化')
168
196
  + (failed.length ? `;${failed.map((r) => r.provider + ':' + (r.error || r.status)).join(';')}` : '');
package/src/traeClient.js CHANGED
@@ -223,6 +223,11 @@ export async function checkinTrae(account, ctx = {}) {
223
223
  return { ...base, status: 'already', message: '今天已签到', uid };
224
224
  }
225
225
 
226
+ // 签到应得额度:status 接口里通常预告 credits / extra_credits(如 100 积分),
227
+ // claim 接口有时只返回 { code: 0, message: "success" } 而不包含 credits 字段,作为 fallback 避免 +0
228
+ const TRAE_DEFAULT_DAILY_CREDITS = 100;
229
+ const expectedCredits = Number(findField(r.body, 'credits') ?? findField(r.body, 'extra_credits')) || TRAE_DEFAULT_DAILY_CREDITS;
230
+
226
231
  const c = await post(CLAIM_URL, account);
227
232
  if (c.code !== 0) {
228
233
  ctx.log?.(`Trae 领取失败:${c.message || `code ${c.code}`}`);
@@ -233,7 +238,7 @@ export async function checkinTrae(account, ctx = {}) {
233
238
  uid,
234
239
  };
235
240
  }
236
- const claimed = Number(findField(c.body, 'credits')) || 0;
241
+ const claimed = Number(findField(c.body, 'credits')) || expectedCredits;
237
242
  return { ...base, status: 'checked-in', claimedAmount: claimed, message: claimed ? `签到成功,+${claimed} 积分` : '签到成功', uid };
238
243
  }
239
244