creditdaddy 1.1.1 → 1.2.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,8 @@
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; }
117
129
  .seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
118
130
  .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
131
  .seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
@@ -160,6 +172,12 @@
160
172
  /* section card */
161
173
  .pcard { margin-top: 14px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 12px 14px; }
162
174
  .pcard-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 700; }
175
+ .fold-head { cursor: pointer; user-select: none; }
176
+ .fold-side { display: inline-flex; align-items: center; gap: 6px; color: var(--text-3); }
177
+ .fold-ico { display: inline-flex; }
178
+ .fold-ico svg { width: 14px; height: 14px; }
179
+ .pcard .fold-body { display: none; margin-top: 4px; }
180
+ .pcard.open .fold-body { display: block; }
163
181
  .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
182
  .logs .info { color: var(--text-2); } .logs .warn { color: var(--warn); } .logs .error { color: var(--err); } .logs .debug { color: var(--text-3); }
165
183
  .collapsed .logs { display: none; }
@@ -187,8 +205,9 @@
187
205
  .hint { font-size: 12px; color: var(--text-3); line-height: 1.7; }
188
206
  .hint a { color: #4f86ff; word-break: break-all; }
189
207
  .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; }
208
+ .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); }
209
+ .cand + .cand { margin-top: 8px; }
210
+ .cand .row { flex: none; flex-wrap: nowrap; gap: 6px; }
192
211
  .cand-main { min-width: 0; }
193
212
  .cand-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
194
213
  .cand-sub { font-size: 11px; color: var(--text-3); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -206,8 +225,15 @@
206
225
  .view { display: none; } .view.active { display: block; }
207
226
 
208
227
  /* 仪表盘 */
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; }
228
+ .dash-panel { margin-bottom: 12px; }
229
+ .dash-title { font-size: 13px; font-weight: 700; }
230
+ .dash-panel .toolbar { padding: 8px 16px; }
231
+ .dash-panel .kpis { display: flex; gap: 0; margin: 0; padding: 0 10px 8px; }
232
+ .dash-panel .kpi { background: none; border: none; border-radius: 0; padding: 6px 14px; flex: 1 1 0; min-width: 0; }
233
+ .dash-panel .kpi-value { font-size: 22px; margin-top: 2px; }
234
+ .dash-panel .kpi-sub { margin-top: 2px; }
235
+ .dash-panel .kpi + .kpi { border-left: 1px dashed var(--border-subtle); }
236
+ @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
237
  .kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; }
212
238
  .kpi { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; min-width: 0; }
213
239
  .kpi-label { color: var(--text-3); font-size: 12px; font-weight: 500; }
@@ -221,7 +247,7 @@
221
247
  .prod-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
222
248
  .prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
223
249
  .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; }
250
+ .dot.qoder { background: #7c3aed; } .dot.workbuddy { background: #14b8a6; } .dot.zcode { background: #2563eb; } .dot.mirasim { background: #64748b; } .dot.catpaw { background: #f97316; } .dot.trae { background: #10b981; }
225
251
  .prod-go { color: var(--text-3); font-size: 12px; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; }
226
252
  .prod-go svg { width: 12px; height: 12px; transition: transform .15s; }
227
253
  .prodcard:hover .prod-go { color: var(--text); }
@@ -253,7 +279,7 @@
253
279
  .tr-vrow { }
254
280
  .tr-vhead { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
255
281
  .tr-vhead .dim { font-size: 12px; }
256
- .tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 12px 4px; }
282
+ .tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 14px 12px 4px; }
257
283
  .tr-vcards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; align-items: stretch; }
258
284
  @media (max-width: 1180px) { .tr-vcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
259
285
  @media (max-width: 820px) { .tr-vcards { grid-template-columns: 1fr; } }
@@ -297,7 +323,6 @@
297
323
  .tr-packs-head { font-size: 11px; color: var(--text-3); margin-bottom: 2px; }
298
324
  .tr-packs-head b { color: var(--text-2); }
299
325
  .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
326
  .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
327
  .chip.on { background: rgba(var(--accent-rgb), .14); color: var(--text); }
303
328
  .setup { padding: 22px; max-width: 620px; }
@@ -337,25 +362,28 @@
337
362
  <button data-view="zcode">ZCode<span class="cnt" id="cnt-zcode">0</span></button>
338
363
  <button data-view="mirasim">mirasim<span class="cnt" id="cnt-mirasim">0</span></button>
339
364
  <button data-view="catpaw">妙手<span class="cnt" id="cnt-catpaw">0</span></button>
365
+ <button data-view="trae">Trae<span class="cnt" id="cnt-trae">0</span></button>
340
366
  <button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
341
367
  </nav>
342
368
 
343
369
  <!-- 仪表盘 -->
344
370
  <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>
371
+ <div class="panel dash-panel">
372
+ <div class="toolbar">
373
+ <div class="summary"><span class="dash-title">总览</span></div>
374
+ <div class="actions">
375
+ <button class="btn primary" id="btn-checkin-all"></button>
376
+ <button class="btn" id="btn-add-dash"></button>
377
+ <button class="btn" id="btn-import-dash"></button>
378
+ <button class="btn" id="btn-export-dash"></button>
379
+ </div>
352
380
  </div>
381
+ <div class="kpis" id="kpis"></div>
353
382
  </div>
354
- <div class="kpis" id="kpis"></div>
355
383
  <div class="prods" id="prods"></div>
356
384
  <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>
385
+ <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>
386
+ <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
387
  </div>
360
388
  </section>
361
389
 
@@ -378,6 +406,15 @@
378
406
  </div>
379
407
  <div class="actions">
380
408
  <button class="btn primary" id="btn-checkin"></button>
409
+ <span id="z-actions" style="display:none">
410
+ <button class="btn" id="zt-ac"></button>
411
+ <button class="btn" id="zt-net"></button>
412
+ <button class="btn" id="zt-gw"></button>
413
+ <button class="btn" id="zt-open" style="display:none"></button>
414
+ </span>
415
+ <span id="t-actions" style="display:none">
416
+ <button class="btn" id="tt-open" style="display:none"></button>
417
+ </span>
381
418
  <button class="btn" id="btn-add"></button>
382
419
  <button class="btn" id="btn-import"></button>
383
420
  <button class="btn" id="btn-export"></button>
@@ -453,7 +490,7 @@
453
490
  <div class="modal-title"><span>导出账号</span><button class="close" data-close>✕</button></div>
454
491
  <div class="field">
455
492
  <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>
493
+ <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></select>
457
494
  </div>
458
495
  <div class="field">
459
496
  <label>加密口令(必填)</label>
@@ -479,26 +516,34 @@
479
516
  <div class="modal">
480
517
  <div class="modal-title"><span>面板设置</span><button class="close" data-close>✕</button></div>
481
518
  <div class="comp-menu" id="comp-menu"></div>
519
+ <div class="field" id="upd-entry" style="display:none;margin-top:12px">
520
+ <label>版本与更新</label>
521
+ <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>
522
+ </div>
482
523
  <div class="modal-actions"><button class="btn" data-close>关闭</button></div>
483
524
  </div>
484
525
  </div>
485
526
 
527
+ <!-- 检查更新(桌面版):状态由主进程 update:state 推送 -->
528
+ <div class="mask" id="m-update">
529
+ <div class="modal sm">
530
+ <div class="modal-title"><span>检查更新</span><button class="close" data-close>✕</button></div>
531
+ <div id="upd-body"></div>
532
+ <div class="modal-actions" id="upd-actions"></div>
533
+ </div>
534
+ </div>
535
+
486
536
  <!-- 10Router 设置 -->
487
537
  <div class="mask" id="m-10r">
488
538
  <div class="modal">
489
539
  <div class="modal-title"><span>10Router 连接设置</span><button class="close" data-close>✕</button></div>
490
540
  <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>
541
+ <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
542
  <div class="field"><label>虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
493
543
  <div class="field"><label>10Router 面板密码(可选)</label><input class="input mono" id="tr-admin-pw" type="password" placeholder="配置后「同步账号」走 OAuth 通道(带 refreshToken 自动续期);留空保持不变" autocomplete="new-password"></div>
494
544
  <div class="field"><label>账号同步</label>
495
545
  <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
546
  </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
547
  <div class="field"><label>显示</label>
503
548
  <div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
504
549
  </div>
@@ -511,6 +556,18 @@
511
556
  </div>
512
557
  </div>
513
558
 
559
+ <!-- 10Router 用量同步 -->
560
+ <div class="mask" id="m-trsync">
561
+ <div class="modal sm">
562
+ <div class="modal-title"><span>用量同步(同 10router-sync 插件)</span><button class="close" data-close>✕</button></div>
563
+ <div class="field"><label class="check"><input type="checkbox" id="tr-auto"> 每小时自动同步本机用量到 10Router</label></div>
564
+ <div class="field"><label>同步来源</label><div id="tr-sources"></div></div>
565
+ <div class="hint">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。「10Router 本机」只把实例原生产生的行标记为网关同步。10Router 按行签名去重,重复同步不会产生重复数据。</div>
566
+ <div class="hint" id="tr-sync-status" style="margin-top:8px"></div>
567
+ <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>
568
+ </div>
569
+ </div>
570
+
514
571
  <!-- ZCode 活动领取 -->
515
572
  <div class="mask" id="m-claim">
516
573
  <div class="modal">
@@ -530,6 +587,59 @@
530
587
  </div>
531
588
  </div>
532
589
 
590
+ <!-- ZCode 自动领取计划 -->
591
+ <div class="mask" id="m-zac">
592
+ <div class="modal sm">
593
+ <div class="modal-title"><span>ZCode 自动领取计划</span><button class="close" data-close>✕</button></div>
594
+ <div class="field">
595
+ <label>轮询周期(分钟,1–720)</label>
596
+ <input class="input" id="zac-interval" type="number" min="1" max="720" step="1">
597
+ </div>
598
+ <div class="field">
599
+ <label>单次运行时长(分钟,0 = 一直运行直到手动关闭)</label>
600
+ <input class="input" id="zac-window" type="number" min="0" max="1440" step="5">
601
+ </div>
602
+ <div class="hint">开启后按此周期检查活动资格并自动领取(首次检查在开启 2 分钟后开始)。活动包由服务端在每日 0 点左右刷新放出——若运行时段没有覆盖 0 点,会错过当次领取,建议把运行时长设为 0,或让时段跨过 0 点。需要验证码时无法自动通过,卡片上会提示手动领取。</div>
603
+ <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>
604
+ </div>
605
+ </div>
606
+
607
+ <!-- ZCode 网络出口 -->
608
+ <div class="mask" id="m-znet">
609
+ <div class="modal sm">
610
+ <div class="modal-title"><span>ZCode 网络出口</span><button class="close" data-close>✕</button></div>
611
+ <div class="field">
612
+ <label>上游接口访问顺序(失败 / 被拦时自动回退另一条)</label>
613
+ <label class="check"><input type="radio" name="znet-order" id="znet-direct"> 直连优先(代理兜底)</label>
614
+ <label class="check"><input type="radio" name="znet-order" id="znet-proxy"> 代理优先(直连兜底)</label>
615
+ </div>
616
+ <div class="field">
617
+ <label>出口代理(http://host:port,支持口令 http://user:pass@host:port)</label>
618
+ <input class="input" id="znet-url" placeholder="http://192.168.31.101:7890">
619
+ </div>
620
+ <div class="hint">留空 = 清除已保存的代理,退回 HTTPS_PROXY 环境变量;取消 = 不改动。当前代理只显示脱敏后的地址。</div>
621
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="znet-ok">保存</button></div>
622
+ </div>
623
+ </div>
624
+
625
+ <!-- ZCode 体验包接口 -->
626
+ <div class="mask" id="m-zgw">
627
+ <div class="modal sm">
628
+ <div class="modal-title"><span>ZCode 体验包接口</span><button class="close" data-close>✕</button></div>
629
+ <div class="hint" id="zgw-url" style="margin-bottom:10px"></div>
630
+ <div class="field">
631
+ <label class="check"><input type="checkbox" id="zgw-on"> 启用体验包接口(/gateway/v1/messages)</label>
632
+ </div>
633
+ <div class="field" id="zgw-lan-fields">
634
+ <label class="check"><input type="checkbox" id="zgw-lan"> 对局域网开放(重启后绑定 0.0.0.0)</label>
635
+ <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
636
+ <textarea class="input" id="zgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
637
+ </div>
638
+ <div class="hint">开启后,领到的体验包额度变成本机的一个补全接口——10Router 建一个 anthropic-compatible 节点指向本机 /gateway 即可调用(需要桌面版,隐藏窗口过验证码)。白名单内的机器<b>免密直连</b>:10Router 的 zcode-free 本就是免授权供应商,把 10Router 服务地址的 IP 加白即可;白名单外的请求一律拒绝。本机回环(127.0.0.1)始终放行。</div>
639
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="zgw-ok">保存</button></div>
640
+ </div>
641
+ </div>
642
+
533
643
  <input type="file" id="file-input" accept=".json,application/json" style="display:none">
534
644
  <div id="toast"></div>
535
645
 
@@ -560,7 +670,7 @@ const I = {
560
670
 
561
671
  const $ = (id) => document.getElementById(id);
562
672
  const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '' };
563
- const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'tenrouter'];
673
+ const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'tenrouter'];
564
674
  // 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
565
675
  const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
566
676
  const PRODUCTS = [
@@ -569,6 +679,7 @@ const PRODUCTS = [
569
679
  { id: 'zcode', label: 'ZCode' },
570
680
  { id: 'mirasim', label: 'mirasim' },
571
681
  { id: 'catpaw', label: '妙手' },
682
+ { id: 'trae', label: 'Trae' },
572
683
  ];
573
684
  const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
574
685
 
@@ -579,6 +690,7 @@ const COMPONENTS = [
579
690
  { id: 'zcode', label: 'ZCode' },
580
691
  { id: 'mirasim', label: 'mirasim' },
581
692
  { id: 'catpaw', label: '妙手' },
693
+ { id: 'trae', label: 'Trae' },
582
694
  { id: 'tenrouter', label: '10Router' },
583
695
  ];
584
696
  const HIDDEN_COMP_KEY = 'qd_hidden_components';
@@ -699,10 +811,11 @@ const PROV = {
699
811
  zcode: { prod: 'zcode', prodLabel: 'ZCode', region: '', cls: '' },
700
812
  mirasim: { prod: 'mirasim', prodLabel: 'mirasim', region: '', cls: '' },
701
813
  catpaw: { prod: 'catpaw', prodLabel: '妙手', region: '', cls: '' },
814
+ trae: { prod: 'trae', prodLabel: 'Trae', region: '', cls: '' },
702
815
  };
703
816
  // 支持每日签到的产品(ZCode / mirasim / 妙手 额度靠套餐或滚动窗口发放,不做签到)
704
- const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy']);
705
- const CLIENT_LABEL = { workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手' };
817
+ const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy', 'trae']);
818
+ const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae' };
706
819
  // 额度单位为 Credits/时长而非「积分」口径的产品(不计入剩余积分合计)
707
820
  const CREDIT_VIEWS = new Set(['zcode', 'mirasim', 'catpaw']);
708
821
  const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
@@ -813,17 +926,10 @@ function applyTheme(t) {
813
926
  $('btn-theme').innerHTML = t === 'dark' ? I.sun : I.moon;
814
927
  }
815
928
  function initTheme() {
816
- const mq = window.matchMedia ? matchMedia('(prefers-color-scheme: dark)') : null;
817
929
  let t = null;
818
930
  try { t = localStorage.getItem('qd_theme'); } catch (e) {}
819
- if (!t) t = mq && mq.matches ? 'dark' : 'light';
931
+ if (!t) t = window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
820
932
  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
933
  $('btn-theme').onclick = () => {
828
934
  const n = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
829
935
  applyTheme(n);
@@ -1004,7 +1110,7 @@ function cardHtml(a) {
1004
1110
  + '</div><div class="ops">'
1005
1111
  + (a.product === 'zcode' ? '<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">' + I.gift + '</button>' : '')
1006
1112
  + (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>' : '')
1113
+ + (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
1114
  + '<button class="icon-btn" data-act="quota" title="刷新积分">' + I.refresh + '</button>'
1009
1115
  + '<button class="icon-btn del" data-act="del" title="删除(点两次确认)">' + I.trash + '</button>'
1010
1116
  + '</div></div>'
@@ -1021,7 +1127,7 @@ function cardHtml(a) {
1021
1127
 
1022
1128
  const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
1023
1129
  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);
1130
+ 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
1131
  /** 接口使用中判定:体验包接口最近一次补全用的账号(lastAccount 记的是 name || uid || id) */
1026
1132
  function isGatewayCurrent(a) {
1027
1133
  const last = state.zGatewayStats && state.zGatewayStats.lastAccount;
@@ -1033,8 +1139,13 @@ function isClientCurrent(a) {
1033
1139
  const s = state.status;
1034
1140
  if (!s) return false;
1035
1141
  if (a.product === 'workbuddy') return Boolean(a.uid && s.workbuddyCurrentUid && s.workbuddyCurrentUid === a.uid);
1142
+ if (a.product === 'qoder') {
1143
+ const cur = s.qoderCurrent || {};
1144
+ return Boolean(a.uid && cur[a.provider] && String(cur[a.provider]) === String(a.uid));
1145
+ }
1036
1146
  if (a.product === 'mirasim') return Boolean(a.uid && s.mirasimCurrentUid && s.mirasimCurrentUid === a.uid);
1037
1147
  if (a.product === 'catpaw') return Boolean(a.uid && s.catpawCurrentUid && s.catpawCurrentUid === a.uid);
1148
+ if (a.product === 'trae') return Boolean(a.uid && s.traeCurrentUid && s.traeCurrentUid === a.uid);
1038
1149
  if (a.product === 'zcode') {
1039
1150
  const id = s.zcodeCurrentIdentity;
1040
1151
  return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
@@ -1056,6 +1167,8 @@ function creditSum(list) {
1056
1167
  }
1057
1168
 
1058
1169
  function render() {
1170
+ $('z-actions').style.display = 'none'; // 兜底:切到仪表盘 / 10Router 时 renderProd 不跑,先藏起来
1171
+ $('t-actions').style.display = 'none';
1059
1172
  for (const p of PRODUCTS) $('cnt-' + p.id).textContent = ofProduct(p.id).length;
1060
1173
  $('cnt-tenrouter').textContent = tr.quotas ? tr.quotas.connections.length : tr.config && tr.config.configured ? '…' : '–';
1061
1174
  if (state.view === 'dash') renderDash();
@@ -1069,6 +1182,7 @@ const EMPTY_TIP = {
1069
1182
  zcode: '还没有 ZCode 账号。<br>点「添加账号」可读取本机 ZCode 客户端已登录的账号,或用浏览器登录(BigModel / Z.ai),多个账号可一键切换。',
1070
1183
  mirasim: '还没有 mirasim 账号。<br>点「添加账号」可读取本机 mirasim 客户端当前登录的账号(本地解密,token 不出本机)。',
1071
1184
  catpaw: '还没有妙手账号。<br>点「添加账号」可读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密凭据,token 不出本机)。',
1185
+ trae: '还没有 Trae 账号。<br>点「添加账号 → 本机导入」可读取本机 Trae 客户端当前登录的账号(本地解密 storage.json 凭据,token 不出本机)。多个账号需先在 Trae 里分别登录并逐个导入。',
1072
1186
  };
1073
1187
 
1074
1188
  function renderProd() {
@@ -1097,7 +1211,7 @@ function renderProd() {
1097
1211
  $('s-credits').textContent = CREDIT_VIEWS.has(prod)
1098
1212
  ? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
1099
1213
  : c.loading ? '…' : (c.known ? fmtNum(c.sum) : '-');
1100
- const hasRegions = !CREDIT_VIEWS.has(prod);
1214
+ const hasRegions = !CREDIT_VIEWS.has(prod) && prod !== 'trae'; // Trae 不分国际 / 国内版
1101
1215
  $('region').style.display = hasRegions ? '' : 'none';
1102
1216
  $('region-div').style.display = hasRegions ? '' : 'none';
1103
1217
  document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
@@ -1106,6 +1220,36 @@ function renderProd() {
1106
1220
  const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl');
1107
1221
  $('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? '领取 / 活跃全部' : '领取全部 ' + prodLabel(prod));
1108
1222
  }
1223
+ // 顶栏产品专属入口:ZCode 设置组、Trae 打开客户端,带状态灯(运行中绿点呼吸)
1224
+ const dot = (on, pulse) => '<span class="dot' + (on ? ' on' : '') + (on && pulse ? ' pulse' : '') + '"></span>';
1225
+ $('z-actions').style.display = prod === 'zcode' ? 'contents' : 'none';
1226
+ if (prod === 'zcode') {
1227
+ const acOn = state.zAutoClaim === true;
1228
+ const gwOn = state.zGateway === true;
1229
+ $('zt-ac').innerHTML = dot(acOn) + '自动领取';
1230
+ $('zt-ac').title = acOn ? '自动领取运行中——点我调整轮询周期 / 运行时长' : '自动领取已关闭——点我设置轮询周期与运行时长并开启';
1231
+ $('zt-net').innerHTML = '出口:<b style="font-weight:700;color:var(--text)">' + (state.zProxyFirst === true ? '代理优先' : '直连优先') + '</b>';
1232
+ $('zt-net').title = '网络出口:直连 / 代理顺序与代理地址';
1233
+ $('zt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.zGatewayLan === true ? '·局域网' : '');
1234
+ $('zt-gw').title = '体验包接口:开关、局域网开放与 IP 白名单';
1235
+ const zc = (state.status && state.status.zcodeClient) || {};
1236
+ const canOpen = zc.installed && window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function';
1237
+ $('zt-open').style.display = canOpen ? '' : 'none';
1238
+ if (canOpen) {
1239
+ $('zt-open').innerHTML = dot(zc.running === true, true) + '打开客户端';
1240
+ $('zt-open').title = zc.running ? 'ZCode 客户端运行中,点我唤到前台' : 'ZCode 客户端未运行,点我启动';
1241
+ }
1242
+ }
1243
+ $('t-actions').style.display = prod === 'trae' ? 'contents' : 'none';
1244
+ if (prod === 'trae') {
1245
+ const tc = (state.status && state.status.traeClient) || {};
1246
+ const canOpen = tc.installed && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function';
1247
+ $('tt-open').style.display = canOpen ? '' : 'none';
1248
+ if (canOpen) {
1249
+ $('tt-open').innerHTML = dot(tc.running === true, true) + '打开客户端';
1250
+ $('tt-open').title = tc.running ? 'Trae 客户端运行中,点我唤到前台' : 'Trae 客户端未运行,点我启动';
1251
+ }
1252
+ }
1109
1253
  }
1110
1254
 
1111
1255
  function kpi(label, value, sub, labelTitle) {
@@ -1192,6 +1336,7 @@ function renderDash() {
1192
1336
  : zid && (zid.email || zid.uid || zid.username) ? '未导入的账号(' + esc(zid.email || zid.username || zid.uid) + ')'
1193
1337
  : p.id === 'mirasim' && s.mirasimCurrentUid ? '未导入的账号'
1194
1338
  : p.id === 'catpaw' && s.catpawClient && s.catpawClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1339
+ : p.id === 'trae' && s.traeClient && s.traeClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1195
1340
  : (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
1196
1341
  rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
1197
1342
  }
@@ -1264,8 +1409,11 @@ function renderStatus() {
1264
1409
  // 产品相关提示只在对应标签页出现
1265
1410
  const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
1266
1411
  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') {
1412
+ n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || (hasIntl && Boolean(s.riskIdentity)));
1413
+ if (state.view === 'trae') {
1414
+ n.style.display = '';
1415
+ n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领约 150 通用积分(7 天有效,多资源包自动归并)。凭据由本机 Trae 客户端解密导入,支持多账号快照一键切换。</span></div>';
1416
+ } else if (state.view === 'catpaw') {
1269
1417
  n.style.display = '';
1270
1418
  n.innerHTML = '妙手(美团 CatPaw)按套餐发放 Credits(体验版 / 专业版…),没有每日签到。凭据由妙手客户端登录管理:读取额度用本机解密出的登录 token 直连桌面网关,token 失效时到妙手客户端重新登录后再「本机导入」即可。'
1271
1419
  + '多账号切换:写入本机凭据后重启妙手客户端即生效(切换会自动保存当前登录,不丢号)。'
@@ -1277,23 +1425,12 @@ function renderStatus() {
1277
1425
  + mirasimClientChip(s);
1278
1426
  } else if (state.view === 'zcode') {
1279
1427
  n.style.display = '';
1280
- const pf = state.zProxyFirst === true;
1281
- const pu = state.zProxyUrl;
1282
1428
  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);
1429
+ const ziv = state.zClaimIntervalMin || 2;
1430
+ const zwin = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
1431
+ n.innerHTML = '<div class="z-row"><span class="z-label">活动领取</span><span class="z-text">活动包积分,每日 0 点左右刷新(无每日签到)。自动领取'
1432
+ + (ac ? '每 ' + ziv + ' 分钟检查资格' + (zwin > 0 ? ',本次运行 ' + zwin + ' 分钟' : ',持续运行') : '未开启(右上角设置轮询计划)')
1433
+ + ';礼物图标 = 手动临时领。</span></div>';
1297
1434
  } else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
1298
1435
  n.style.display = '';
1299
1436
  const busy = s.umid.installing || umidBusy;
@@ -1312,15 +1449,6 @@ function renderStatus() {
1312
1449
  }
1313
1450
  }
1314
1451
 
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
1452
  // mirasim 客户端状态芯片 + 「打开客户端」按钮
1325
1453
  function mirasimClientChip(s) {
1326
1454
  const c = s.mirasimClient || {};
@@ -1330,6 +1458,7 @@ function mirasimClientChip(s) {
1330
1458
  + (canOpen ? ' <button class="btn" id="btn-miraopen" style="margin-left:4px" title="打开本机 Mirasim 客户端">打开客户端</button>' : '');
1331
1459
  }
1332
1460
 
1461
+
1333
1462
  // 妙手客户端状态芯片 + 「打开客户端」按钮
1334
1463
  function catpawClientChip(s) {
1335
1464
  const c = s.catpawClient || {};
@@ -1352,8 +1481,9 @@ async function installUmid() {
1352
1481
  await loadStatus(); render(); loadLogs();
1353
1482
  }
1354
1483
  $('notice').addEventListener('click', (e) => { if (e.target.closest('#btn-umid')) installUmid(); });
1355
- $('notice').addEventListener('click', async (e) => {
1356
- if (e.target.closest('#btn-zopen')) {
1484
+ // 覆盖 #notice(各产品横幅)与 #z-actions(ZCode 工具栏按钮组)的点击
1485
+ document.addEventListener('click', async (e) => {
1486
+ if (e.target.closest('#zt-open')) {
1357
1487
  try {
1358
1488
  const r = await window.creditdaddy.openZcodeClient();
1359
1489
  if (r && r.ok) toast('已启动 ZCode 客户端');
@@ -1377,51 +1507,44 @@ $('notice').addEventListener('click', async (e) => {
1377
1507
  } catch (err) { toast(String(err && err.message || err), false); }
1378
1508
  return;
1379
1509
  }
1380
- if (e.target.closest('#btn-zac')) {
1510
+ if (e.target.closest('#tt-open')) {
1381
1511
  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); }
1512
+ const r = await window.creditdaddy.openTraeClient();
1513
+ if (r && r.ok) toast('已启动 Trae 客户端');
1514
+ else toast((r && r.error) || '无法打开 Trae 客户端', false);
1515
+ } catch (err) { toast(String(err && err.message || err), false); }
1387
1516
  return;
1388
1517
  }
1389
- if (e.target.closest('#btn-zgwlan')) {
1390
- 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); }
1518
+ if (e.target.closest('#zt-ac')) {
1519
+ // 自动领取计划弹窗:周期 / 运行时长由用户决定
1520
+ $('zac-interval').value = state.zClaimIntervalMin || 2;
1521
+ $('zac-window').value = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
1522
+ $('zac-off').style.display = state.zAutoClaim === true ? '' : 'none';
1523
+ $('zac-ok').textContent = state.zAutoClaim === true ? '保存' : '保存并启用';
1524
+ openModal('m-zac');
1396
1525
  return;
1397
1526
  }
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); }
1527
+ if (e.target.closest('#zt-gw')) {
1528
+ // 体验包接口弹窗:开关 + 局域网 + 白名单一次设置;白名单默认预填 10Router 服务地址的主机
1529
+ $('zgw-on').checked = state.zGateway === true;
1530
+ $('zgw-lan').checked = state.zGatewayLan === true;
1531
+ const allow = Array.isArray(state.zGatewayAllow) ? state.zGatewayAllow : [];
1532
+ $('zgw-allow').value = (allow.length ? allow : (state.zGatewaySuggest ? [state.zGatewaySuggest] : [])).join('\n');
1533
+ const st = state.zGatewayStats;
1534
+ $('zgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
1535
+ + (st && st.lastCallAt
1536
+ ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1537
+ : '<br>尚未有调用');
1538
+ syncZgwLanFields();
1539
+ openModal('m-zgw');
1405
1540
  return;
1406
1541
  }
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); }
1414
- return;
1415
- }
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); }
1542
+ if (e.target.closest('#zt-net')) {
1543
+ // 出口弹窗:顺序 + 代理地址一起保存;代理只回填到 placeholder(脱敏),留空即清除
1544
+ (state.zProxyFirst === true ? $('znet-proxy') : $('znet-direct')).checked = true;
1545
+ $('znet-url').value = '';
1546
+ $('znet-url').placeholder = state.zProxyUrl || (state.zHasEnvProxy ? '跟随环境变量 HTTPS_PROXY' : 'http://192.168.31.101:7890');
1547
+ openModal('m-znet');
1425
1548
  return;
1426
1549
  }
1427
1550
  });
@@ -1431,8 +1554,10 @@ async function loadZNet() {
1431
1554
  state.zProxyFirst = d.proxyFirst === true;
1432
1555
  state.zProxyUrl = d.proxyUrlMasked || null;
1433
1556
  state.zAutoClaim = d.autoClaim === true;
1557
+ state.zClaimIntervalMin = d.claimIntervalMin;
1558
+ state.zClaimWindowMin = d.claimWindowMin;
1434
1559
  state.zHasEnvProxy = d.hasEnvProxy === true;
1435
- if (state.view === 'zcode') renderStatus();
1560
+ if (state.view === 'zcode') { renderStatus(); render(); }
1436
1561
  } catch (e) {}
1437
1562
  }
1438
1563
  async function loadZGateway() {
@@ -1440,9 +1565,11 @@ async function loadZGateway() {
1440
1565
  const d = await api('/api/zcode-gateway');
1441
1566
  state.zGateway = d.enabled === true;
1442
1567
  state.zGatewayLan = d.lan === true;
1568
+ state.zGatewayAllow = d.allow || [];
1569
+ state.zGatewaySuggest = d.suggestAllow || '';
1443
1570
  state.zGatewayHasCaptcha = d.hasCaptcha === true;
1444
1571
  state.zGatewayStats = d.stats || null;
1445
- if (state.view === 'zcode') renderStatus();
1572
+ if (state.view === 'zcode') { renderStatus(); render(); }
1446
1573
  } catch (e) {}
1447
1574
  }
1448
1575
 
@@ -1701,17 +1828,14 @@ function renderTenrouter() {
1701
1828
  const c = tr.config;
1702
1829
  if (!c) { box.innerHTML = '<div class="empty">加载中…</div>'; return; }
1703
1830
  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>'
1831
+ ? '<div class="gw-line"><span class="dot on pulse"></span> ZCode 体验包接口运行中'
1832
+ + (state.zGatewayStats && state.zGatewayStats.lastCallAt ? ' · 最近调用 ' + esc(String(state.zGatewayStats.lastCallAt).slice(11, 19)) + ' · ' + state.zGatewayStats.calls + ' 次 · 最后用号 ' + esc(state.zGatewayStats.lastAccount || '-') : ' · 尚未有调用')
1833
+ + ' · 模型 <code>zcode-free/glm-5.3-flash</code></div>'
1705
1834
  : '';
1706
1835
  if (!c.configured) { box.innerHTML = gwLine + trSetupHtml(); return; }
1707
1836
  const list = (tr.quotas && tr.quotas.connections) || [];
1708
1837
  const low = list.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
1709
1838
  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
1839
  let grid;
1716
1840
  let tabStrip = '';
1717
1841
  const groups = list.length ? trGroups(list) : [];
@@ -1759,15 +1883,11 @@ function renderTenrouter() {
1759
1883
  + '<div class="stat"><span>额度告急</span><strong>' + (tr.quotas ? low : '-') + '</strong></div><div class="divider"></div>'
1760
1884
  + '<div class="stat"><span>查询失败</span><strong>' + (tr.quotas ? failed : '-') + '</strong></div>'
1761
1885
  + '</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>'
1886
+ + '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + (tr.loading ? '刷新中…' : '刷新额度') + '</button>'
1887
+ + '<button class="btn" data-tr="sync" title="用量同步:自动开关、同步来源与手动同步(点我打开)"' + (tr.syncing || c.syncing ? ' disabled' : '') + '>'
1888
+ + '<span class="dot' + (c.sync.enabled ? ' on' : '') + '"></span>' + (tr.syncing || c.syncing ? '同步中…' : '用量同步') + '</button>'
1764
1889
  + '<button class="btn" data-tr="settings">设置</button>'
1765
1890
  + '</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
1891
  + tabStrip
1772
1892
  + '<div class="tr-grid">' + grid + '</div>';
1773
1893
  }
@@ -1817,23 +1937,36 @@ function openTrSettings() {
1817
1937
  $('tr-key').placeholder = c.keyMasked ? '已保存 ' + c.keyMasked + '(留空保持不变)' : 'sk-…';
1818
1938
  $('tr-admin-pw').value = '';
1819
1939
  $('tr-admin-pw').placeholder = c.hasAdminPassword ? '已保存(留空保持不变)' : '可选:10Router 面板密码';
1820
- $('tr-auto').checked = c.configured ? c.sync.enabled : true;
1821
1940
  $('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
1941
  $('tr-disconnect').style.display = c.configured ? '' : 'none';
1826
1942
  $('tr-test-msg').textContent = '';
1827
1943
  openModal('m-10r');
1828
1944
  setTimeout(() => $(c.configured ? 'tr-key' : 'tr-endpoint').focus(), 30);
1829
1945
  }
1946
+ // 用量同步弹窗:自动开关 + 来源勾选 + 手动同步
1947
+ function openTrSync() {
1948
+ const c = tr.config || { sync: { enabled: true, sources: [] }, sources: [] };
1949
+ $('tr-auto').checked = c.configured ? c.sync.enabled : true;
1950
+ $('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
1951
+ + ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
1952
+ + '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
1953
+ const ls = c.lastSync;
1954
+ $('tr-sync-status').textContent = ls ? '上次同步 ' + fmtTime(ls.at).slice(5) + ':' + ls.summary : '还没有同步过';
1955
+ $('tr-sync-now').disabled = tr.syncing || c.syncing === true;
1956
+ openModal('m-trsync');
1957
+ }
1958
+ async function trSyncSave(close) {
1959
+ try {
1960
+ 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) } });
1961
+ if (close) { closeModal('m-trsync'); toast('用量同步设置已保存'); render(); }
1962
+ } catch (e) { toast(e.message, false); throw e; }
1963
+ }
1830
1964
  function trForm() {
1831
1965
  return {
1832
- endpoint: $('tr-endpoint').value.trim(),
1966
+ // 从 LLM 客户端复制的 base URL 常带 /v1 后缀——10Router 只认站点根地址,带 /v1 会让所有 /api/* 调用 404、误报「需要 1.2.1+」
1967
+ endpoint: $('tr-endpoint').value.trim().replace(/\/+$/, '').replace(/\/v1$/, ''),
1833
1968
  key: $('tr-key').value.trim(),
1834
1969
  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
1970
  };
1838
1971
  }
1839
1972
  async function trTest() {
@@ -1886,7 +2019,7 @@ $('tr-panel').addEventListener('click', (e) => {
1886
2019
  if (b.dataset.tr === 'toggle') { state.trOpen = Object.assign({}, state.trOpen, { [b.dataset.id]: !(state.trOpen && state.trOpen[b.dataset.id]) }); render(); return; }
1887
2020
  if (b.dataset.tr === 'settings') openTrSettings();
1888
2021
  else if (b.dataset.tr === 'refresh') loadTrQuotas(true);
1889
- else if (b.dataset.tr === 'sync') trSync();
2022
+ else if (b.dataset.tr === 'sync') openTrSync();
1890
2023
  else if (b.dataset.tr === 'vendor') {
1891
2024
  const v = b.dataset.v || '';
1892
2025
  state.trVendor = v;
@@ -1895,6 +2028,12 @@ $('tr-panel').addEventListener('click', (e) => {
1895
2028
  }
1896
2029
  });
1897
2030
  $('tr-test').onclick = trTest;
2031
+ $('tr-sync-save').onclick = () => trSyncSave(true);
2032
+ $('tr-sync-now').onclick = async () => {
2033
+ try { await trSyncSave(false); } catch (e) { return; }
2034
+ closeModal('m-trsync');
2035
+ trSync();
2036
+ };
1898
2037
  $('tr-sync-accounts').onclick = trSyncAccounts;
1899
2038
  $('tr-save').onclick = trSave;
1900
2039
  $('tr-disconnect').onclick = trDisconnect;
@@ -1957,7 +2096,9 @@ async function checkinOne(id) {
1957
2096
  try {
1958
2097
  const d = await api('/api/accounts/' + encodeURIComponent(id) + '/checkin', { method: 'POST' });
1959
2098
  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'));
2099
+ const accObj = state.accounts.find((x) => x.id === id);
2100
+ const accLabel = (r && r.account) || (accObj && (accObj.name || accObj.id)) || '该账号';
2101
+ toast(r ? (accLabel + ':' + (r.status === 'checked-in' ? '领取成功 +' + r.claimedAmount + ' Credits' : (r.error || r.message || r.status))) : d.summary, !(r && r.status === 'failed'));
1961
2102
  } catch (e) { toast(e.message, false); }
1962
2103
  delete state.busy[id];
1963
2104
  await loadStatus(); await loadAccounts(); loadLogs();
@@ -1982,21 +2123,25 @@ async function onCardClick(e) {
1982
2123
  if (act === 'claim') return openClaim(id);
1983
2124
  if (act === 'switch') {
1984
2125
  const a = state.accounts.find((x) => x.id === id);
1985
- const client = CLIENT_LABEL[a && a.product] || '客户端';
2126
+ const client = CLIENT_LABEL[a && a.product] || (a && prodLabel(a.product)) || '客户端';
1986
2127
  const tip = a && a.product === 'zcode'
1987
2128
  ? '\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
1988
2129
  : a && a.product === 'mirasim'
1989
2130
  ? '\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
1990
2131
  : a && a.product === 'catpaw'
1991
2132
  ? '\n需先退出妙手;切换后重新打开妙手即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
1992
- : '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
2133
+ : a && a.product === 'trae'
2134
+ ? '\n需先退出 Trae;切换后重新打开 Trae 即为新账号。当前登录会先自动快照保存到 CreditDaddy,不会丢号。'
2135
+ : a && a.product === 'qoder'
2136
+ ? '\n需先退出 Qoder;切换后重新打开 Qoder 即为新账号。运行中的 Qoder 退出时会把旧登录写回去,可选「关闭并强制切换」代为关进程。'
2137
+ : '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
1993
2138
  if (!(await uiConfirm('把 ' + client + ' 切换到「' + (a && a.name || id) + '」?' + tip, '切换账号', '切换'))) return;
1994
2139
  const doSwitch = (force) => api('/api/accounts/' + encodeURIComponent(id) + '/switch', { json: { force } });
1995
2140
  try {
1996
2141
  let r;
1997
2142
  try { r = await doSwitch(false); }
1998
2143
  catch (err) {
1999
- if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING') throw err;
2144
+ if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING' && err.code !== 'TRAE_RUNNING' && err.code !== 'QODER_RUNNING') throw err;
2000
2145
  if (!(await uiConfirm(err.message + '\n\n仍要强制切换吗?强制切换会先关闭客户端再写入登录,重新打开客户端即为新账号。', client + ' 正在运行', '关闭并强制切换'))) return;
2001
2146
  r = await doSwitch(true);
2002
2147
  }
@@ -2029,6 +2174,15 @@ async function onCardClick(e) {
2029
2174
  } catch (e) {}
2030
2175
  }, 800);
2031
2176
  }
2177
+ if (a && a.product === 'trae' && r && !r.alreadyActive && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function') {
2178
+ setTimeout(async () => {
2179
+ try {
2180
+ const re = await window.creditdaddy.openTraeClient();
2181
+ if (re && re.ok) toast('已重新启动 Trae 客户端');
2182
+ else toast((re && re.error) || '无法打开 Trae 客户端(桌面版才能自动拉起)', false);
2183
+ } catch (e) {}
2184
+ }, 800);
2185
+ }
2032
2186
  } catch (err) { toast(err.message, false); }
2033
2187
  return;
2034
2188
  }
@@ -2226,6 +2380,135 @@ const uiPrompt = (title, value, opts = {}) => uiDialog({ title, message: opts.me
2226
2380
  function okDialog() { const v = $('dlg-field').style.display === 'none' ? true : $('dlg-input').value; $('m-dialog').classList.remove('show'); settleDialog(v); }
2227
2381
  $('dlg-ok').addEventListener('click', okDialog);
2228
2382
  $('dlg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') okDialog(); });
2383
+
2384
+ // ── 检查更新(桌面版):主进程推状态(update:state),面板只管渲染 ──
2385
+ const updBridge = window.creditdaddy && window.creditdaddy.update ? window.creditdaddy.update : null;
2386
+ let updState = null;
2387
+ function updActBtn(id, text, cls, fn) {
2388
+ return '<button class="btn' + (cls ? ' ' + cls : '') + '" id="' + id + '">' + text + '</button>';
2389
+ }
2390
+ function renderUpdateModal(s) {
2391
+ updState = s;
2392
+ const body = $('upd-body');
2393
+ const acts = $('upd-actions');
2394
+ const cur = '当前 v' + (s.current || '?');
2395
+ const rel = '<button class="btn" id="upd-rel">打开 Releases</button>';
2396
+ const close = '<button class="btn" data-close-uu>稍后</button>';
2397
+ switch (s.phase) {
2398
+ case 'unsupported':
2399
+ body.innerHTML = '<div class="hint">当前环境不支持应用内更新(开发模式 / Portable / 未签名的 macOS 版),请从 GitHub Releases 手动下载。</div>';
2400
+ acts.innerHTML = rel + '<button class="btn" data-close-uu>取消</button>';
2401
+ break;
2402
+ case 'checking':
2403
+ body.innerHTML = '<div class="hint">正在检查更新…</div>';
2404
+ acts.innerHTML = close;
2405
+ break;
2406
+ case 'latest':
2407
+ body.innerHTML = '<div class="hint"><span class="dot on"></span> 已是最新版本(' + cur + ')</div>';
2408
+ acts.innerHTML = '<button class="btn primary" data-close-uu>好的</button>';
2409
+ break;
2410
+ case 'available':
2411
+ body.innerHTML = s.via === 'updater'
2412
+ ? '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + ')——正在后台下载,完成后退出 CreditDaddy 时自动安装。</div>'
2413
+ : '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + (s.sizeMB ? ',约 ' + s.sizeMB + ' MB' : '') + '),可由本机直接下载安装。</div>';
2414
+ acts.innerHTML = (s.via === 'github' ? updActBtn('upd-go', '立即下载安装', 'primary') : '') + rel + close;
2415
+ break;
2416
+ case 'downloading':
2417
+ body.innerHTML = '<div class="hint">正在下载 v' + esc(s.version) + '</div>'
2418
+ + '<div class="meter" style="margin:8px 0 4px"><span style="width:' + (s.percent || 0) + '%"></span></div>'
2419
+ + '<div class="hint">' + (s.percent != null ? s.percent + '%' : '') + '(' + (s.mb || '…') + ' MB)' + (s.percent == null ? ' 获取大小中…' : '') + '</div>';
2420
+ acts.innerHTML = '<button class="btn" data-close-uu>后台继续</button>';
2421
+ break;
2422
+ case 'downloaded':
2423
+ body.innerHTML = '<div class="hint"><span class="dot on pulse"></span> <b>v' + esc(s.version) + '</b> '
2424
+ + (s.via === 'updater' ? '已下载完成,重启即完成更新(退出时也会自动安装)。' : '安装包已下载完成,安装时 CreditDaddy 会先退出。') + '</div>';
2425
+ acts.innerHTML = updActBtn('upd-go', s.via === 'updater' ? '立即重启更新' : '立即安装', 'primary') + close;
2426
+ break;
2427
+ case 'error':
2428
+ default:
2429
+ body.innerHTML = '<div class="hint" style="color:var(--err)">检查更新失败:' + esc(s.error || '未知错误') + '</div>';
2430
+ acts.innerHTML = updActBtn('upd-retry', '重试', 'primary') + rel + '<button class="btn" data-close-uu>取消</button>';
2431
+ break;
2432
+ }
2433
+ const relBtn = $('upd-rel');
2434
+ if (relBtn) relBtn.onclick = () => updBridge.openReleases();
2435
+ const goBtn = $('upd-go');
2436
+ if (goBtn) { goBtn.disabled = true; goBtn.textContent = '处理中…'; updBridge.install().then(renderUpdateModal).catch((e) => { goBtn.disabled = false; goBtn.textContent = '重试'; }); }
2437
+ const retryBtn = $('upd-retry');
2438
+ if (retryBtn) retryBtn.onclick = () => updBridge.check().then(renderUpdateModal).catch(() => {});
2439
+ acts.querySelectorAll('[data-close-uu]').forEach((b) => { b.onclick = () => closeModal('m-update'); });
2440
+ }
2441
+ async function openUpdateModal(autoCheck) {
2442
+ if (!updBridge) return;
2443
+ openModal('m-update');
2444
+ try {
2445
+ updState = await updBridge.state();
2446
+ renderUpdateModal(updState);
2447
+ if (autoCheck) renderUpdateModal(await updBridge.check());
2448
+ } catch (e) {
2449
+ renderUpdateModal({ phase: 'error', error: String((e && e.message) || e), current: (updState && updState.current) || '' });
2450
+ }
2451
+ }
2452
+ if (updBridge) {
2453
+ updBridge.onState((s) => { updState = s; $('upd-ver') && ($('upd-ver').textContent = '当前 v' + (s.current || '?')); if (document.getElementById('m-update').classList.contains('show')) renderUpdateModal(s); });
2454
+ updBridge.onOpen(() => openUpdateModal(true));
2455
+ }
2456
+ $('btn-update').onclick = () => openUpdateModal(true);
2457
+
2458
+ // ZCode 自动领取计划:保存(按新配置启用)/ 停用
2459
+ $('zac-ok').addEventListener('click', async () => {
2460
+ try {
2461
+ const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: true, claimIntervalMin: Number($('zac-interval').value), claimWindowMin: Number($('zac-window').value) } });
2462
+ state.zAutoClaim = r.autoClaim === true;
2463
+ state.zClaimIntervalMin = r.claimIntervalMin;
2464
+ state.zClaimWindowMin = r.claimWindowMin;
2465
+ renderStatus(); render();
2466
+ closeModal('m-zac');
2467
+ toast('自动领取已开启:每 ' + r.claimIntervalMin + ' 分钟检查' + (r.claimWindowMin > 0 ? ',运行 ' + r.claimWindowMin + ' 分钟后自动关闭' : ',一直运行直到手动关闭'));
2468
+ } catch (err) { toast(err.message, false); }
2469
+ });
2470
+ $('zac-off').addEventListener('click', async () => {
2471
+ try {
2472
+ const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: false } });
2473
+ state.zAutoClaim = r.autoClaim === true;
2474
+ renderStatus(); render();
2475
+ closeModal('m-zac');
2476
+ toast('ZCode 自动领取:已关闭');
2477
+ } catch (err) { toast(err.message, false); }
2478
+ });
2479
+
2480
+ // ZCode 体验包接口:开关 + 局域网开放 + 免密 IP 白名单一次保存
2481
+ function syncZgwLanFields() {
2482
+ const off = !$('zgw-on').checked;
2483
+ $('zgw-lan-fields').querySelectorAll('input, textarea').forEach((el) => { el.disabled = off; });
2484
+ $('zgw-lan-fields').style.opacity = off ? '.45' : '';
2485
+ }
2486
+ $('zgw-on').addEventListener('change', syncZgwLanFields);
2487
+ $('zgw-ok').addEventListener('click', async () => {
2488
+ const allow = $('zgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
2489
+ try {
2490
+ const r = await api('/api/zcode-gateway', { method: 'PUT', json: { enabled: $('zgw-on').checked, lan: $('zgw-lan').checked, allow } });
2491
+ state.zGateway = r.enabled === true;
2492
+ state.zGatewayLan = r.lan === true;
2493
+ state.zGatewayAllow = r.allow || [];
2494
+ renderStatus(); render();
2495
+ closeModal('m-zgw');
2496
+ toast('体验包接口:' + (r.enabled ? '已开启' : '已关闭')
2497
+ + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : ',白名单为空——白名单外需虚拟 key') + ';绑定改动重启后生效' : ''));
2498
+ } catch (err) { toast(err.message, false); }
2499
+ });
2500
+
2501
+ // ZCode 网络出口:顺序 + 代理地址保存
2502
+ $('znet-ok').addEventListener('click', async () => {
2503
+ try {
2504
+ const body = { proxyFirst: $('znet-proxy').checked, proxyUrl: $('znet-url').value.trim() };
2505
+ const r = await api('/api/zcode/net', { method: 'PUT', json: body });
2506
+ await loadZNet();
2507
+ renderStatus(); render();
2508
+ closeModal('m-znet');
2509
+ toast('ZCode 出口:' + (r.proxyFirst ? '代理优先(直连兜底)' : '直连优先(代理兜底)') + (body.proxyUrl === '' ? ',代理已清除' : ''));
2510
+ } catch (err) { toast(err.message, false); }
2511
+ });
2229
2512
  document.querySelectorAll('.mask').forEach((m) => {
2230
2513
  m.addEventListener('mousedown', (e) => { if (e.target === m) closeModal(m.id); });
2231
2514
  m.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => closeModal(m.id)));
@@ -2245,10 +2528,12 @@ const LOCAL_HINT = {
2245
2528
  zcode: '读取本机 ZCode 客户端当前登录的账号(本地解密 ~/.zcode/v2/credentials.json),token 不出本机。',
2246
2529
  mirasim: '读取本机 mirasim 客户端当前登录的账号(本地解密 ~/.mirasim/setting.json),token 不出本机。',
2247
2530
  catpaw: '读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密 %APPDATA%\\catpaw-moon\\catx-credential.json),token 不出本机。',
2531
+ trae: '读取本机 Trae 客户端当前登录的账号(本地解密 <客户端数据目录>\\User\\globalStorage\\storage.json 的 tc 信封),token 不出本机。',
2248
2532
  };
2249
2533
  function openAdd(product) {
2250
2534
  state.addProduct = product || '';
2251
- const tabShown = { local: true, device: product !== 'mirasim' && product !== 'catpaw', token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' };
2535
+ // Trae 凭据只能从本机客户端 storage.json 解密:不支持浏览器登录 / 粘贴 Token
2536
+ const tabShown = { local: true, device: product !== 'mirasim' && product !== 'catpaw' && product !== 'trae', token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' && product !== 'trae' };
2252
2537
  document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
2253
2538
  $('local-hint').textContent = LOCAL_HINT[state.addProduct];
2254
2539
  let first = null;
@@ -2277,7 +2562,7 @@ function openAdd(product) {
2277
2562
  async function localScan() {
2278
2563
  const box = $('scan-result');
2279
2564
  $('scan-status').textContent = '扫描中(最多约 10 秒)…';
2280
- box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手)…</div>';
2565
+ box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae)…</div>';
2281
2566
  try {
2282
2567
  const r = await api('/api/local/scan', { method: 'POST' });
2283
2568
  $('scan-status').textContent = '';
@@ -2298,9 +2583,9 @@ async function localScan() {
2298
2583
  html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
2299
2584
  }
2300
2585
  if (!cands.length) {
2301
- const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手';
2586
+ const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae';
2302
2587
  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 数据目录、妙手凭据目录'
2588
+ html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录、Trae 数据目录'
2304
2589
  + (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
2305
2590
  }
2306
2591
  box.innerHTML = html;
@@ -2639,11 +2924,24 @@ function renderCompMenu() {
2639
2924
  + '<div class="hint">开启后每次打开面板都需要输入密码;密码可随时在此修改或关闭。'
2640
2925
  + (state.sec.enabled ? '' : '账号导出不受影响(导出口令始终必填)。') + '</div>'
2641
2926
  + '<div class="row">' + renderSecActions() + '</div>'
2927
+ + '<div class="comp-menu-title">自动签到</div>'
2928
+ + '<div class="hint">' + autoCheckinHint() + '</div>'
2642
2929
  + '<div class="comp-menu-title">组件顺序(仪表盘与标签栏同步)</div>' + compRows
2643
2930
  + '<div class="comp-menu-title">账号卡片排序</div>'
2644
2931
  + (sortSections || '<div class="sort-hint">暂无账号</div>');
2645
2932
  }
2646
2933
 
2934
+ /** 自动签到口径说明:调度周期 + 各产品业务日刷新时点 */
2935
+ function autoCheckinHint() {
2936
+ const sch = state.status && state.status.scheduler;
2937
+ const next = sch && sch.nextTickAt ? ',下次执行 ' + esc(String(sch.nextTickAt).slice(11, 16)) : '';
2938
+ const zc = sch && sch.zcode;
2939
+ const zcText = zc && zc.enabled
2940
+ ? 'ZCode 自动领取运行中(每 ' + (zc.intervalMin || 2) + ' 分钟' + (zc.autoOffAt ? ',约 ' + Math.max(1, Math.ceil((new Date(zc.autoOffAt).getTime() - Date.now()) / 60000)) + ' 分钟后自动关闭' : ',一直运行') + '),周期可在 ZCode 页「自动领取计划」里改'
2941
+ : 'ZCode 活动领取默认关闭,可在 ZCode 页「自动领取计划」里开启并配置轮询周期';
2942
+ return '守护进程每 ~2 小时自动扫描执行一轮全部账号' + next + '。各产品「业务日」刷新时点(UTC+8):<b>Qoder 每日 10:00</b>(每台设备每日限领一个国际版账号)、<b>WorkBuddy 每日 00:00</b>,其余产品按 00:00;已领成功的账号按对应业务日记忆跳过,跨过刷新点后当天照常重试。' + esc(zcText) + '。';
2943
+ }
2944
+
2647
2945
  /** 在手动排序列表里移动一个 id;列表不存在时先按当前生效顺序物化一份 */
2648
2946
  function moveAccountId(prod, aid, dir) {
2649
2947
  const baseIds = sortedAccounts(prod, ofProduct(prod)).map((a) => a.id);
@@ -2662,6 +2960,11 @@ $('btn-comp').addEventListener('click', async () => {
2662
2960
  openModal('m-settings');
2663
2961
  await loadSettings();
2664
2962
  renderCompMenu();
2963
+ // 桌面版才有检查更新入口(走 preload 桥到主进程的更新状态机)
2964
+ if (updBridge) {
2965
+ $('upd-entry').style.display = '';
2966
+ try { const s = await updBridge.state(); updState = s; $('upd-ver').textContent = '当前 v' + (s.current || '?'); } catch {}
2967
+ }
2665
2968
  });
2666
2969
  $('comp-menu').addEventListener('change', (e) => {
2667
2970
  const c = e.target.dataset && e.target.dataset.comp;
@@ -2719,7 +3022,7 @@ $('btn-export-dash').onclick = () => { $('ex-provider').value = ''; openModal('m
2719
3022
  $('btn-export').onclick = () => {
2720
3023
  const v = state.view;
2721
3024
  // ZCode 是单一 provider,直接按 provider 选;其余产品页按产品线(p:);仪表盘 / 10Router 导出全部
2722
- $('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw') ? 'p:' + v : '';
3025
+ $('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw' || v === 'trae') ? 'p:' + v : '';
2723
3026
  openModal('m-export');
2724
3027
  };
2725
3028
  $('btn-do-export').onclick = doExport;
@@ -2781,11 +3084,31 @@ $('btn-logs').onclick = () => {
2781
3084
  if (!c) { loadLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
2782
3085
  };
2783
3086
 
3087
+ // ── 仪表盘底部两卡:默认收起,点标题展开,展开状态存本地 ──
3088
+ const DASH_FOLD_KEY = 'qd_dash_fold';
3089
+ state.dashFold = (() => { try { return JSON.parse(localStorage.getItem(DASH_FOLD_KEY) || '{}'); } catch { return {}; } })();
3090
+ function applyDashFold() {
3091
+ document.querySelectorAll('.dash-cols .pcard[data-fold]').forEach((el) => {
3092
+ const open = state.dashFold[el.dataset.fold] === true;
3093
+ el.classList.toggle('open', open);
3094
+ el.querySelector('.fold-ico').innerHTML = open ? I.chevUp : I.chevDown;
3095
+ });
3096
+ }
3097
+ document.querySelectorAll('.dash-cols .fold-head').forEach((h) => {
3098
+ h.title = '展开 / 收起';
3099
+ h.addEventListener('click', () => {
3100
+ const k = h.closest('.pcard').dataset.fold;
3101
+ state.dashFold[k] = state.dashFold[k] !== true;
3102
+ try { localStorage.setItem(DASH_FOLD_KEY, JSON.stringify(state.dashFold)); } catch {}
3103
+ applyDashFold();
3104
+ });
3105
+ });
3106
+ applyDashFold();
3107
+
2784
3108
  // ── 启动:先直接请求(本机模式无需密钥);只有服务端返回 401 时才询问密钥 ──
2785
3109
  initTheme();
2786
3110
  $('btn-home').innerHTML = I.github;
2787
3111
  applyView();
2788
-
2789
3112
  (function boot() {
2790
3113
  function start() {
2791
3114
  loadStatus().then(loadAccounts).catch((e) => toast(e.message, false));
@@ -2801,7 +3124,7 @@ applyView();
2801
3124
  });
2802
3125
  loadLogs();
2803
3126
  setInterval(loadLogs, 10000);
2804
- setInterval(async () => { await loadStatus(); await loadZGateway(); render(); }, 30000);
3127
+ setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); render(); }, 30000);
2805
3128
  }
2806
3129
  // 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
2807
3130
  api('/api/status').then(start).catch((e) => toast(e.code === 'PANEL_KEY'