creditdaddy 1.5.2 → 1.6.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
@@ -224,6 +224,8 @@
224
224
  .credit { display: inline-flex; align-items: center; gap: 4px; font-variant-numeric: tabular-nums; }
225
225
  .credit b { font-weight: 700; }
226
226
  .credit svg { width: 13px; height: 13px; color: rgb(var(--accent-rgb)); }
227
+ .credit.pending b { color: var(--text-3); }
228
+ .seg.pending-ghost { background: repeating-linear-gradient(90deg, color-mix(in srgb, var(--text-3) 30%, transparent) 0 6px, transparent 6px 10px); }
227
229
  .dim { color: var(--text-3); }
228
230
  .claim-plan { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 2px; border-bottom: 1px solid var(--border); }
229
231
  .claim-plan:last-of-type { border-bottom: 0; }
@@ -472,11 +474,6 @@
472
474
  </div>
473
475
  <div class="hint" data-i18n="作用于全部产品的定时签到轮(ZCode 活动领取在自身页签单独配置);修改立即生效,下一轮按新间隔执行。">作用于全部产品的定时签到轮(ZCode 活动领取在自身页签单独配置);修改立即生效,下一轮按新间隔执行。</div>
474
476
  </div>
475
- <div class="field">
476
- <label data-i18n="WorkBuddy 国际版 活跃请求">WorkBuddy 国际版 活跃请求</label>
477
- <label class="check"><input type="checkbox" id="cfg-wb-activity"> <span data-i18n="定期发送活跃请求(保持资格,不保证奖励)">定期发送活跃请求(保持资格,不保证奖励)</span></label>
478
- <div class="hint" data-i18n="关闭后本机不再对国际版账号自动/手动发活跃请求;卡片与仪表盘的「活跃」入口一同隐藏。">关闭后本机不再对国际版账号自动/手动发活跃请求;卡片与仪表盘的「活跃」入口一同隐藏。</div>
479
- </div>
480
477
  <div class="field">
481
478
  <label data-i18n="面板访问密码">面板访问密码</label>
482
479
  <div id="sec-block"></div>
@@ -769,6 +766,35 @@
769
766
  </div>
770
767
  </div>
771
768
 
769
+ <!-- 捐赠支持(面向国内用户:爱发电 / 支付宝 / 微信;渠道与 10Router 一致,UI 照 CD 设计语言) -->
770
+ <div class="mask" id="m-donate">
771
+ <div class="modal">
772
+ <div class="modal-title"><span data-i18n="捐赠支持">捐赠支持</span><button class="close" data-close>✕</button></div>
773
+ <div class="hint" data-i18n="如果 CreditDaddy 对你有帮助,欢迎支持一下开发——免费渠道的每一行代码都是咖啡驱动的。">如果 CreditDaddy 对你有帮助,欢迎支持一下开发——免费渠道的每一行代码都是咖啡驱动的。</div>
774
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:14px">
775
+ <div style="display:flex;flex-direction:column;align-items:center;gap:8px;padding:14px 10px;border:1px solid var(--border-subtle);border-radius:10px">
776
+ <span style="font-size:26px">⚡</span>
777
+ <div style="font-weight:700">AiFaDian</div>
778
+ <div class="hint" data-i18n="爱发电 · Power me ⚡">爱发电 · Power me ⚡</div>
779
+ <img src="/assets/donate-ifdian.svg?v=3" alt="AiFaDian" style="width:140px;height:140px;margin-top:auto;border-radius:8px;padding:6px;flex:none">
780
+ </div>
781
+ <div style="display:flex;flex-direction:column;align-items:center;gap:8px;padding:14px 10px;border:1px solid var(--border-subtle);border-radius:10px">
782
+ <span style="font-size:26px">🅰️</span>
783
+ <div style="font-weight:700">Alipay</div>
784
+ <div class="hint" data-i18n="支付宝扫码打赏">支付宝扫码打赏</div>
785
+ <img src="/assets/donate-alipay.svg?v=3" alt="Alipay" style="width:140px;height:140px;margin-top:auto;border-radius:8px;padding:6px;flex:none">
786
+ </div>
787
+ <div style="display:flex;flex-direction:column;align-items:center;gap:8px;padding:14px 10px;border:1px solid var(--border-subtle);border-radius:10px">
788
+ <span style="font-size:26px">💚</span>
789
+ <div style="font-weight:700">WeChat Pay</div>
790
+ <div class="hint" data-i18n="微信扫码打赏">微信扫码打赏</div>
791
+ <img src="/assets/donate-wechat.svg?v=3" alt="WeChat Pay" style="width:140px;height:140px;margin-top:auto;border-radius:8px;padding:6px;flex:none">
792
+ </div>
793
+ </div>
794
+ <div class="modal-actions"><button class="btn primary" data-close data-i18n="关闭">关闭</button></div>
795
+ </div>
796
+ </div>
797
+
772
798
  <!-- 10Router 设置 -->
773
799
  <div class="mask" id="m-10r">
774
800
  <div class="modal">
@@ -777,9 +803,6 @@
777
803
  <div class="field" style="margin-top:12px"><label data-i18n="10Router 地址">10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128(站点根地址,不要带 /v1)"></div>
778
804
  <div class="field"><label data-i18n="虚拟 key">虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
779
805
  <div class="field"><label data-i18n="10Router 面板密码(可选)">10Router 面板密码(可选)</label><input class="input mono" id="tr-admin-pw" type="password" placeholder="配置后「同步账号」走 OAuth 通道(带 refreshToken 自动续期);留空保持不变" autocomplete="new-password"></div>
780
- <div class="field"><label data-i18n="账号同步">账号同步</label>
781
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button class="btn" id="tr-sync-accounts" data-i18n="同步账号到 10R">同步账号到 10R</button><span class="hint" data-i18n="把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接;zcode 待反代就绪后支持">把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接;zcode 待反代就绪后支持</span></div>
782
- </div>
783
806
  <div class="field"><label data-i18n="显示">显示</label>
784
807
  <div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
785
808
  </div>
@@ -807,9 +830,9 @@
807
830
  <!-- 免费额度池:推送设置 -->
808
831
  <div class="mask" id="m-free-settings">
809
832
  <div class="modal sm">
810
- <div class="modal-title"><span data-i18n="免费额度推送设置">免费额度推送设置</span><button class="close" data-close>✕</button></div>
833
+ <div class="modal-title"><span data-i18n="额度卡片推送设置">额度卡片推送设置</span><button class="close" data-close>✕</button></div>
811
834
  <div class="field">
812
- <label class="check"><input type="checkbox" id="free-push-auto"> <span data-i18n="自动推送免费额度卡到 10Router(Token Harbor / MiniMax)">自动推送免费额度卡到 10Router(Token Harbor / MiniMax)</span></label>
835
+ <label class="check"><input type="checkbox" id="free-push-auto"> <span data-i18n="自动推送额度卡片到 10Router(Token Harbor / MiniMax / LongCat 等)">自动推送额度卡片到 10Router(Token Harbor / MiniMax / LongCat 等)</span></label>
813
836
  </div>
814
837
  <div class="field">
815
838
  <label data-i18n="推送频率">推送频率</label>
@@ -821,7 +844,7 @@
821
844
  <option value="240" data-i18n="每 4 小时">每 4 小时</option>
822
845
  </select>
823
846
  </div>
824
- <div class="hint" data-i18n="把 CreditDaddy 解析的免费额度数字(不含凭据)推到 10Router 配额页出卡;卡片在 10Router 具 7 天有效期。点「立即同步一次」或 10Router 页「刷新额度」也会立即推送。">把 CreditDaddy 解析的免费额度数字(不含凭据)推到 10Router 配额页出卡;卡片在 10Router 具 7 天有效期。点「立即同步一次」或 10Router 页「刷新额度」也会立即推送。</div>
847
+ <div class="hint" data-i18n="把 CreditDaddy 解析的额度数字(不含凭据)推到 10Router 配额页出卡;卡片在 10Router 具 7 天有效期。点「立即同步一次」或 10Router 页「刷新额度」也会立即推送。">把 CreditDaddy 解析的额度数字(不含凭据)推到 10Router 配额页出卡;卡片在 10Router 具 7 天有效期。点「立即同步一次」或 10Router 页「刷新额度」也会立即推送。</div>
825
848
  <div class="hint" id="free-settings-status" style="margin-top:8px"></div>
826
849
  <div class="modal-actions">
827
850
  <button class="btn" style="margin-right:auto" id="free-settings-push-now" data-i18n="立即推送一次">立即推送一次</button>
@@ -840,15 +863,26 @@
840
863
  </div>
841
864
  </div>
842
865
 
843
- <div class="mask" id="m-cookie">
866
+ <!-- 10Router 账号管理:账号同步 + 网页会话 Cookie 捕获(原「Cookie 会话」按钮与「设置」里的账号同步合并) -->
867
+ <div class="mask" id="m-am">
844
868
  <div class="modal">
845
- <div class="modal-title"><span data-i18n="Cookie 会话捕获">Cookie 会话捕获</span><button class="close" data-close>✕</button></div>
846
- <div class="hint" data-i18n="为「额度只在网页 dashboard」的供应商捕获登录会话(cookie),同步给 10Router 点亮配额卡片;凭据全程只存本机(账号库加密)。">为「额度只在网页 dashboard」的供应商捕获登录会话(cookie),同步给 10Router 点亮配额卡片;凭据全程只存本机(账号库加密)。</div>
847
- <div id="cookie-sites-body" style="margin-top:10px;display:grid;gap:8px"></div>
869
+ <div class="modal-title"><span data-i18n="账号管理">账号管理</span><button class="close" data-close>✕</button></div>
870
+ <div class="field"><label data-i18n="账号同步">账号同步</label>
871
+ <div class="hint" style="margin-bottom:8px" data-i18n="把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接。">把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接。</div>
872
+ <input class="input mono" id="am-pw" type="password" autocomplete="new-password">
873
+ <div style="display:flex;gap:8px;align-items:center;margin-top:8px;flex-wrap:wrap">
874
+ <button class="btn" id="am-sync"><span class="dot"></span><span class="lbl">同步账号到 10R</span></button>
875
+ <span class="hint" id="am-sync-msg"></span>
876
+ </div>
877
+ <div style="margin-top:10px" id="am-sync-channels"></div>
878
+ </div>
879
+ <div class="field" style="margin-top:14px"><label data-i18n="网页会话">网页会话</label>
880
+ <div class="hint" data-i18n="为「额度只在网页 dashboard」的供应商捕获登录会话(cookie),同步给 10Router 点亮配额卡片;凭据全程只存本机(账号库加密)。">为「额度只在网页 dashboard」的供应商捕获登录会话(cookie),同步给 10Router 点亮配额卡片;凭据全程只存本机(账号库加密)。</div>
881
+ <div id="cookie-sites-body" style="margin-top:10px;display:grid;gap:8px"></div>
882
+ </div>
848
883
  <div class="modal-actions"><button class="btn" data-close data-i18n="关闭">关闭</button></div>
849
884
  </div>
850
885
  </div>
851
-
852
886
  <!-- 通用对话框 -->
853
887
  <div class="mask" id="m-dialog">
854
888
  <div class="modal sm">
@@ -969,6 +1003,8 @@ const I = {
969
1003
  play: svg('<polygon points="6 4 20 12 6 20 6 4"/>'),
970
1004
  refresh: svg('<path d="M21 12a9 9 0 1 1-2.64-6.36L21 8"/><path d="M21 3v5h-5"/>'),
971
1005
  trash: svg('<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/>'),
1006
+ eye: svg('<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'),
1007
+ eyeOff: svg('<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/>'),
972
1008
  plus: svg('<path d="M12 5v14M5 12h14"/>'),
973
1009
  upload: svg('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/>'),
974
1010
  download: svg('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/>'),
@@ -999,7 +1035,7 @@ const $ = (id) => document.getElementById(id);
999
1035
  const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run', logLevel: 'all', locale: 'zh-CN' };
1000
1036
  const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'free', 'tenrouter'];
1001
1037
  // 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
1002
- const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
1038
+ const tr = { config: null, quotas: null, error: null, loading: false, syncing: false, accountsSyncing: false };
1003
1039
  const PRODUCTS = [
1004
1040
  { id: 'qoder', label: 'Qoder' },
1005
1041
  { id: 'workbuddy', label: 'WorkBuddy' },
@@ -1023,6 +1059,9 @@ const COMPONENTS = [
1023
1059
  { id: 'free', label: '网页会话' },
1024
1060
  { id: 'tenrouter', label: '10Router' },
1025
1061
  ];
1062
+ // 捐赠二维码随日夜主题切换(顶层定义:applyTheme 的调用早于原块级定义,块内 function 不可达)
1063
+ // 捐赠二维码统一低饱和金黄(透明底),日夜主题通用,无需随主题切换
1064
+ function applyDonateQrTheme() { /* 统一金码:无主题切换 */ }
1026
1065
  const HIDDEN_COMP_KEY = 'qd_hidden_components';
1027
1066
  const DEFAULT_HIDDEN_COMP = ['catpaw']; // 妙手默认收起,右上角设置里可打开
1028
1067
  state.hidden = (() => {
@@ -1250,7 +1289,6 @@ async function loadSettings() {
1250
1289
  try {
1251
1290
  const d = await api('/api/settings');
1252
1291
  state.sec = { enabled: Boolean(d.panelKeyEnabled), loaded: true };
1253
- state.wbActivity = d.workbuddyActivity !== false; // WB 国际版活跃请求总开关(默认开)
1254
1292
  if (!d.panelKeyEnabled) setKey(''); // 关闭后清掉本地缓存的旧密钥
1255
1293
  if (d.locale) renderLocale(d.locale);
1256
1294
  } catch (e) { /* 401(密码开启但未输入)等状态由 promptKey 流程处理 */ }
@@ -1283,15 +1321,6 @@ async function applyDictFromServer() {
1283
1321
  document.getElementById('set-locale')?.addEventListener('change', (ev) => setLocale(ev.target.value));
1284
1322
 
1285
1323
  // ── 通用设置:全局自动签到(开关 + 频率,落盘立刻重调度)、日志默认显示方式(本机偏好)──
1286
- const wbActOn = () => state.wbActivity !== false;
1287
- $('cfg-wb-activity').addEventListener('change', async (e) => {
1288
- try {
1289
- await api('/api/settings', { method: 'PUT', json: { workbuddyActivity: e.target.checked } });
1290
- await loadSettings();
1291
- render(); renderStatus();
1292
- toast(T('已保存'), true);
1293
- } catch (err) { toast(err.message, false); }
1294
- });
1295
1324
  function fillAutoCheckin() {
1296
1325
  const sc = state.status && state.status.scheduler;
1297
1326
  if (!sc) return;
@@ -1329,6 +1358,7 @@ applyLogsDisplay(logsDisplayDefault());
1329
1358
  // 由 renderThemeMode() 反映到菜单里的分段控件(主题只有那一个入口)。
1330
1359
  function applyTheme(t) {
1331
1360
  document.documentElement.dataset.theme = t;
1361
+ if (typeof applyDonateQrTheme === 'function') { try { applyDonateQrTheme(t); } catch {} }
1332
1362
  }
1333
1363
  function initTheme() {
1334
1364
  let t = null;
@@ -1425,6 +1455,7 @@ function initTheme() {
1425
1455
  // 需要跟随系统走菜单里的三态分段控件。
1426
1456
  const themeBtn = $('btn-theme');
1427
1457
  function resolvedTheme() { return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'; }
1458
+
1428
1459
  function renderThemeButton() {
1429
1460
  if (!themeBtn) return;
1430
1461
  const cur = resolvedTheme();
@@ -1455,20 +1486,28 @@ function initTheme() {
1455
1486
  else window.open(url, '_blank');
1456
1487
  };
1457
1488
 
1458
- // 捐赠(桌面版走 IPC 桥;网页直接开浏览器)
1459
- $('donate-btn').onclick = () => {
1460
- closeHmenu();
1461
- if (window.creditdaddy && typeof window.creditdaddy.shellOpen === 'function')
1462
- window.creditdaddy.shellOpen('https://github.com/sponsors/techysy');
1463
- else window.open('https://github.com/sponsors/techysy', '_blank');
1464
- };
1489
+ // 捐赠:弹窗展示三渠道(爱发电 / 支付宝 / 微信——国内用户为主,与 10Router 一致);
1490
+ // Sponsors 按钮跳转(桌面版走 IPC 桥,网页直接开浏览器)
1491
+ $('donate-btn').onclick = () => { closeHmenu(); openModal('m-donate'); };
1465
1492
  }
1466
1493
 
1467
1494
  // ── 状态 / 渲染 ──
1468
- // 统一「领鸡蛋」语义:Qoder / WorkBuddy 国内/国际 = 领取积分;workbuddy-intl = 保持活跃(不保证奖励)
1495
+ // 统一「领鸡蛋」语义:全部签到产品线 = 领取积分(workbuddy-intl 的「活跃」语义随活跃领取线删除)
1469
1496
  function statusOf(a) {
1470
- if (!CHECKIN_PRODUCTS.has(a.product)) return { cls: '', text: '', msg: '' }; // ZCode 无每日领取,不显示徽标
1471
- const intl = a.provider === 'workbuddy-intl';
1497
+ if (!CHECKIN_PRODUCTS.has(a.product)) {
1498
+ // ZCode 无「每日签到」口径,但活动领取的结果必须落到徽标上:
1499
+ // 否则卡片上唯一能证明「领到了」的东西只有额度数字,而活动额度常带
1500
+ // effective_at(当天稍晚到账),在那之前卡片看起来毫无变化。
1501
+ if (a.product === 'zcode') {
1502
+ const r = a.lastResult;
1503
+ const fresh = r && r.at && Date.now() - new Date(r.at).getTime() < 26 * 3600e3;
1504
+ if (r && fresh && r.status === 'checked-in') return { cls: 'ok', text: T('已领取'), msg: r.message };
1505
+ if (r && fresh && r.status === 'already') return { cls: 'ok', text: T('已领过'), msg: r.message };
1506
+ if (r && fresh && r.status === 'failed') return { cls: 'fail', text: T('领取失败'), msg: r.message, err: true };
1507
+ }
1508
+ return { cls: '', text: '', msg: '' };
1509
+ }
1510
+ if (a.provider === 'workbuddy-intl') return { cls: '', text: '', msg: '' }; // 国际版无签到动作(活跃领取线已删),不显示徽标
1472
1511
  const byProd = (state.status && state.status.todayByProduct) || {};
1473
1512
  const today = byProd[a.product] || (state.status && state.status.today);
1474
1513
  const done = state.status && state.status.todayDone && state.status.todayDone[a.id] === today;
@@ -1476,15 +1515,13 @@ function statusOf(a) {
1476
1515
  const fresh = r && r.at && state.status && (Date.now() - new Date(r.at).getTime() < 26 * 3600e3);
1477
1516
  if (r && fresh && r.status === 'limited' && done) return { cls: 'limited', text: T('本机已领'), msg: r.message };
1478
1517
  if (done) {
1479
- if (intl) return { cls: 'ok', text: T('今日活跃'), msg: r && r.message ? r.message : '' };
1480
1518
  const amt = r && r.status === 'checked-in' && fresh && r.amount ? ' +' + fmtNum(r.amount) : '';
1481
- const streak = r && !intl && r.streakDays ? T(' · 连续 ') + r.streakDays + T(' 天') : '';
1519
+ const streak = r && r.streakDays ? T(' · 连续 ') + r.streakDays + T(' 天') : '';
1482
1520
  return { cls: 'ok', text: T('今日已领') + amt + streak, msg: r && r.message && !['checked-in', 'already'].includes(r.status) ? r.message : '' };
1483
1521
  }
1484
- const failText = intl ? T('活跃失败') : T('领取失败');
1485
- if (r && fresh && r.status === 'failed') return { cls: 'fail', text: failText, msg: r.message, err: true };
1486
- if (r && fresh && r.status === 'no-activity') return { cls: 'pending', text: intl ? T('待活跃') : T('待领取'), msg: r.message };
1487
- return { cls: 'pending', text: intl ? T('未活跃') : T('未领取'), msg: '' };
1522
+ if (r && fresh && r.status === 'failed') return { cls: 'fail', text: T('领取失败'), msg: r.message, err: true };
1523
+ if (r && fresh && r.status === 'no-activity') return { cls: 'pending', text: T('待领取'), msg: r.message };
1524
+ return { cls: 'pending', text: T('未领取'), msg: '' };
1488
1525
  }
1489
1526
 
1490
1527
  function expiryCell(a) {
@@ -1567,8 +1604,11 @@ function creditHtml(a) {
1567
1604
  const planBadge = q.plan ? '<span class="badge">' + esc(T(q.plan)) + T(' 套餐</span>') : '';
1568
1605
  const expLabel = q.planExpiresAt ? '<span class="dim">' + fmtTime(q.planExpiresAt).slice(0, 10) + T(' 到期</span>') : '';
1569
1606
  // 状态采用温和的徽章,避免整行大红字制造恐慌
1607
+ // pill 不再用 mirasimClient 拼好的 bottleneckLabel(上游窗口名原文 + '已用尽' 拼串,
1608
+ // 英文界面会出「30d已用尽」这类混语)——改面板侧按词典分别 T():窗口名走词条
1609
+ // ('5小时窗口'→'5h window' 等,上游新名如 '30d' 拉丁原样),「已用尽」单独走词条。
1570
1610
  const topStatus = isExhausted
1571
- ? '<span class="pill pending">' + esc(T(q.bottleneckLabel || '') || T('额度已用尽')) + '</span>'
1611
+ ? '<span class="pill pending">' + esc(q.bottleneck ? T(q.bottleneck.name) + T('已用尽') : T('额度已用尽')) + '</span>'
1572
1612
  : T('<span class="lbl">可用额度</span><b>') + fmtNum(q.remaining) + '%</b>';
1573
1613
 
1574
1614
  const fmtUnits = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + T('万') : fmtNum(Math.round(n)));
@@ -1591,15 +1631,32 @@ function creditHtml(a) {
1591
1631
 
1592
1632
  const unit = q.unit ? ' ' + T(q.unit) : '';
1593
1633
  if (q.unit) {
1634
+ // 待生效权益单独计数:活动发的额度要到生效点才进余额,把它算成「已用完」会让人以为丢了
1635
+ const pendingParts = q.parts.filter((x) => x.pending);
1636
+ const effParts = q.parts.filter((x) => !x.pending);
1594
1637
  const items = q.parts.map((x) => {
1595
1638
  const base = esc(T(x.name)) + ' ' + fmtNum(x.remaining) + '/' + fmtNum(x.total) + (x.unit ? ' ' + esc(T(x.unit)) : '');
1596
1639
  if (x.pending && x.startsAt) return base + T('(') + fmtTime(x.startsAt).slice(5, 16) + T(' 生效)');
1597
1640
  return base + (x.expiresAt ? T('(') + fmtTime(x.expiresAt).slice(5, 16) + (x.recurring ? T(' 重置)') : T(' 到期)')) : '');
1598
1641
  }).join(' · ');
1642
+ const atText = q.pendingAt ? fmtTime(q.pendingAt) : '';
1643
+ const isToday = atText.slice(0, 10) === fmtTime(new Date().toISOString()).slice(0, 10);
1644
+ const when = atText ? (isToday ? atText.slice(11) : atText.slice(0, 10) + ' ' + atText.slice(11)) : '';
1645
+ const pendingTotal = q.pendingTotal || pendingParts.reduce((s, x) => s + (x.total || 0), 0);
1646
+ const pendingLine = '<div class="credit-line"><span class="credit pending">' + T('<span class="lbl">待生效</span>')
1647
+ + '<b>' + fmtNum(pendingTotal) + '</b>' + esc(unit)
1648
+ + '</span><span class="credit dim" title="' + items + '">'
1649
+ + (q.plan ? esc(q.plan) + ' · ' : '') + (when ? when + ' ' + T('到账') : T('即将到账')) + '</span></div>';
1599
1650
  const pct = q.total > 0 ? Math.min(1, q.remaining / q.total) : 0;
1651
+ const mainLine = () => T('<div class="credit-line"><span class="credit"><span class="lbl">剩余</span><b>') + fmtNum(q.remaining) + '</b>' + esc(unit) + '</span>'
1652
+ + '<span class="credit dim" title="' + items + '">' + (q.plan ? esc(q.plan) + ' · ' : '') + items + '</span></div>';
1653
+ // 全部待生效(活动刚发、还没到点):主行显示待生效,别只显示一个 0
1654
+ if (pendingParts.length && !effParts.length) {
1655
+ return pendingLine + '<div class="bar"><span class="seg pending-ghost" style="flex:1 1 0"></span></div>';
1656
+ }
1600
1657
  // 满额(100%)不再渲染灰色「已使用」尾段——它只占 0 宽也会造成几像素的灰边
1601
- return T('<div class="credit-line"><span class="credit"><span class="lbl">剩余</span><b>') + fmtNum(q.remaining) + '</b>' + esc(unit) + '</span>'
1602
- + '<span class="credit dim" title="' + items + '">' + (q.plan ? esc(q.plan) + ' · ' : '') + items + '</span></div>'
1658
+ return mainLine()
1659
+ + (pendingParts.length ? pendingLine : '')
1603
1660
  + '<div class="bar"><span class="seg" style="flex:' + pct.toFixed(4) + ' 1 0"></span>'
1604
1661
  + (pct < 0.9999 ? '<span class="seg used" style="flex:' + (1 - pct).toFixed(4) + ' 1 0"></span>' : '')
1605
1662
  + '</div>';
@@ -1631,7 +1688,9 @@ function cardHtml(a) {
1631
1688
  + T('<span class="name" data-act="rename" title="点击修改备注名">') + esc(a.name || T('(未命名)')) + '</span>'
1632
1689
  + provBadges(a.provider, true, a.flavor)
1633
1690
  + (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] ? T(CLIENT_LABEL[a.product]) : '') + T(' 客户端当前登录的账号">客户端当前</span>') : '')
1634
- + (a.product === 'zcode' && isGatewayCurrent(a) ? T('<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>') : '')
1691
+ + (a.product === 'zcode' && isGatewayCurrent(a) ? (zgwAccountCount() > 1
1692
+ ? T('<span class="pill gw" title="体验包接口最近一次补全用的账号({n} 个 ZCode 账号按轮转分摊请求,每个请求换一个,与客户端登录无关)">接口使用中</span>', { n: zgwAccountCount() })
1693
+ : T('<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮转自动选择,与客户端登录无关)">接口使用中</span>')) : '')
1635
1694
  + (a.product === 'minimax' && isMiniMaxGatewayCurrent(a) ? T('<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>') : '')
1636
1695
  + (a.product === 'trae' && isTraeGatewayCurrent(a) ? T('<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>') : '')
1637
1696
  + (a.isPat ? '<span class="badge">PAT</span>' : '')
@@ -1640,7 +1699,7 @@ function cardHtml(a) {
1640
1699
  + (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>' : '')
1641
1700
  + '</div><div class="ops">'
1642
1701
  + (a.product === 'zcode' ? T('<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">') + I.gift + '</button>' : '')
1643
- + (canCheckin && (a.provider !== 'workbuddy-intl' || wbActOn()) ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + (a.provider === 'workbuddy-intl' ? T('发送活跃请求') : T('立即领取')) + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
1702
+ + (canCheckin && a.provider !== 'workbuddy-intl' ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + T('立即领取') + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
1644
1703
  + (a.qoderWebDead ? '<button class="icon-btn" data-act="webrenew" title="' + T('网页会话已失效——打开登录窗口重新授权,关闭窗口时自动补 cookie(API token 不受影响)') + '">' + I.swap + '</button>' : '')
1645
1704
  + (a.canSwitch && !isCurrent && st.cls !== 'fail' ? T('<button class="icon-btn" data-act="switch" title="切换 ') + (CLIENT_LABEL[a.product] ? T(CLIENT_LABEL[a.product]) : prodLabel(a.product)) + T(' 客户端到此账号">') + I.swap + '</button>' : '')
1646
1705
  + T('<button class="icon-btn" data-act="quota" title="刷新积分">') + I.refresh + '</button>'
@@ -1672,6 +1731,10 @@ function isGatewayCurrent(a) {
1672
1731
  const last = state.zGatewayStats && state.zGatewayStats.lastAccount;
1673
1732
  return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
1674
1733
  }
1734
+ /** 网关账号总数(多账号时徽标注明「轮转中」,避免用户以为只有一个号在服役) */
1735
+ function zgwAccountCount() {
1736
+ return state.accounts.filter((a) => a.product === 'zcode').length;
1737
+ }
1675
1738
  /** MiniMax 本地接口使用中判定 */
1676
1739
  function isMiniMaxGatewayCurrent(a) {
1677
1740
  const last = state.mGatewayStats && state.mGatewayStats.lastAccount;
@@ -1689,8 +1752,12 @@ function isClientCurrent(a) {
1689
1752
  if (!s) return false;
1690
1753
  if (a.product === 'workbuddy') return Boolean(a.uid && s.workbuddyCurrentUid && s.workbuddyCurrentUid === a.uid);
1691
1754
  if (a.product === 'qoder') {
1755
+ // 服务端按「本机 auth.v1.dat 解出的 token」对齐出账号 id(token 优先、uid 兜底)——
1756
+ // 只看 uid 的话客户端静默刷新 token 后会把别的同 uid 记录误当成当前账号
1692
1757
  const cur = s.qoderCurrent || {};
1693
- return Boolean(a.uid && cur[a.provider] && String(cur[a.provider]) === String(a.uid));
1758
+ if (cur[a.provider]) return String(cur[a.provider]) === String(a.id);
1759
+ const uid = s.qoderCurrentUid || {};
1760
+ return Boolean(a.uid && uid[a.provider] && String(uid[a.provider]) === String(a.uid));
1694
1761
  }
1695
1762
  if (a.product === 'mirasim') return Boolean(a.uid && s.mirasimCurrentUid && s.mirasimCurrentUid === a.uid);
1696
1763
  if (a.product === 'catpaw') return Boolean(a.uid && s.catpawCurrentUid && s.catpawCurrentUid === a.uid);
@@ -1752,11 +1819,9 @@ function renderProd() {
1752
1819
  $('s-count').textContent = acc.length;
1753
1820
  $('s-done-wrap').style.display = canCheckin ? 'contents' : 'none';
1754
1821
  if (prod === 'workbuddy') {
1755
- // 国内版 = 每日签到送积分;国际版 = 保持活跃(活跃不等于必有奖励)
1822
+ // 国内版 = 每日签到送积分;国际版无签到动作(活跃领取线已删),只统计国内
1756
1823
  const cnAcc = acc.filter((a) => a.provider === 'workbuddy');
1757
- const intlAcc = acc.filter((a) => a.provider === 'workbuddy-intl' && wbActOn());
1758
- $('s-done-wrap').innerHTML = (cnAcc.length ? T('<div class="stat"><span title="国内版每日签到送积分">领取(国内)</span><strong>') + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '')
1759
- + (intlAcc.length ? T('<div class="stat"><span title="国际版定期发送活跃请求保持资格;活跃不等于必有奖励">活跃(国际)</span><strong>') + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
1824
+ $('s-done-wrap').innerHTML = (cnAcc.length ? T('<div class="stat"><span title="国内版每日签到送积分">领取(国内)</span><strong>') + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '');
1760
1825
  } else {
1761
1826
  // 从 WorkBuddy 页切回来时 s-done 可能被 innerHTML 换没了,先重建结构再写值
1762
1827
  if (!$('s-done')) $('s-done-wrap').innerHTML = T('<div class="stat"><span>今日已领</span><strong id="s-done"></strong></div><div class="divider"></div>');
@@ -1773,8 +1838,7 @@ function renderProd() {
1773
1838
  document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
1774
1839
  $('btn-checkin').style.display = canCheckin ? '' : 'none';
1775
1840
  if (!$('btn-checkin').disabled) {
1776
- const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl') && wbActOn();
1777
- $('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? T('领取 / 活跃全部') : T('领取全部 ') + prodLabel(prod));
1841
+ $('btn-checkin').innerHTML = I.play + T('领取全部 ') + prodLabel(prod);
1778
1842
  }
1779
1843
  // 顶栏产品专属入口:ZCode 设置组、Trae 打开客户端,带状态灯(运行中绿点呼吸)
1780
1844
  const dot = (on, pulse) => '<span class="dot' + (on ? ' on' : '') + (on && pulse ? ' pulse' : '') + '"></span>';
@@ -1842,23 +1906,19 @@ function renderDash() {
1842
1906
  const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
1843
1907
  const byProd = orderedVisibleComps().filter((id) => id !== 'tenrouter').map((id) => prodLabel(id) + ' ' + ofProduct(id).length).join(' · ');
1844
1908
  const creditByProd = ['qoder', 'workbuddy', 'minimax'].filter(compVisible).map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
1845
- // 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
1846
- const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl' && wbActOn());
1847
- const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
1848
- const doneDom = domAcc.filter(isDone).length;
1849
- const doneIntl = intlAcc.filter(isDone).length;
1909
+ // 今日进度:国际版 WorkBuddy 无签到动作(活跃领取线已删),signable 天然只剩可领取账号
1850
1910
  $('kpis').innerHTML =
1851
1911
  kpi(T('账号'), state.accounts.length, byProd)
1852
- + kpi(T('今日领取') + (intlAcc.length ? T(' / 活跃') : ''), intlAcc.length
1853
- ? doneDom + ' <small>/ ' + domAcc.length + '</small><small class="kpi-side">' + T(' · 活跃 {n} / {m}', { n: doneIntl, m: intlAcc.length }) + '</small>'
1854
- : done + ' <small>/ ' + signable.length + '</small>',
1912
+ + kpi(T('今日领取'), done + ' <small>/ ' + signable.length + '</small>',
1855
1913
  !signable.length ? T('暂无可领取的账号')
1856
- : (doneDom === domAcc.length && doneIntl === intlAcc.length ? (intlAcc.length ? T('今天全部完成(国际为活跃请求,不保证奖励)') : T('今天全部签完'))
1914
+ : (done === signable.length ? T('今天全部签完')
1857
1915
  : T('待办 ') + (signable.length - done) + (failed ? T(' · 失败 ') + failed : '')),
1858
- T('领取 = 国内版每日签到;活跃 = 国际版定期活跃请求(保持资格,不保证奖励)'))
1916
+ T('领取 = 每日签到'))
1859
1917
  + kpi(T('剩余积分'), credit.loading ? '…' : (credit.known ? fmtNum(credit.sum) : '-'), credit.loading ? T('正在读取各账号额度…') : creditByProd)
1860
1918
  + kpi(T('下次自动领取'), sch.ticking ? T('领取中…') : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
1861
- sch.lastTick ? T('上一轮 ') + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : T('每 ~2 小时一轮'));
1919
+ sch.lastTick
1920
+ ? T('上一轮 ') + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summaryKey ? T(sch.lastTick.summaryKey, sch.lastTick.summaryArgs) : (sch.lastTick.summary || ''))
1921
+ : T('每 ~2 小时一轮'));
1862
1922
 
1863
1923
  // 仪表盘产品卡按组件设置里的顺序渲染(与标签栏同步),10Router 汇总卡按其顺序插入
1864
1924
  const prodCardHtml = (p) => {
@@ -1870,16 +1930,10 @@ function renderDash() {
1870
1930
  const d = list.filter(isDone).length;
1871
1931
  const c = creditSum(list);
1872
1932
  if (p.id === 'workbuddy') {
1873
- // 国内 = 每日领取;国际 = 保持活跃(活跃不等于必有奖励)——合并一行展示
1933
+ // 国内 = 每日领取;国际无签到动作(活跃领取线已删),统计只覆盖国内账号
1874
1934
  const cnList = list.filter((a) => a.provider === 'workbuddy');
1875
- const intlList = list.filter((a) => a.provider === 'workbuddy-intl');
1876
- const parts = [];
1877
- if (cnList.length) parts.push(cnList.filter(isDone).length + ' / ' + cnList.length + T('(国内)'));
1878
- if (intlList.length) parts.push(intlList.filter(isDone).length + ' / ' + intlList.length + T('(国际 · 不保证奖励)'));
1879
- rows += T('<span class="lbl">领取 / 活跃</span><span class="val" title="') + esc(parts.join(T(',')) + (intlList.length ? T('(国际为活跃请求,不保证奖励)') : '')) + '">'
1880
- + (cnList.length ? cnList.filter(isDone).length + ' / ' + cnList.length + T(' 国内') : '')
1881
- + (cnList.length && intlList.length ? ' · ' : '')
1882
- + (intlList.length ? intlList.filter(isDone).length + ' / ' + intlList.length + T(' 国际') : '')
1935
+ rows += T('<span class="lbl">领取</span><span class="val" title="') + esc(cnList.length ? cnList.filter(isDone).length + ' / ' + cnList.length + T('(国内)') : '') + '">'
1936
+ + (cnList.length ? cnList.filter(isDone).length + ' / ' + cnList.length + T(' 国内') : T('—'))
1883
1937
  + '</span>';
1884
1938
  } else if (p.id === 'minimax') {
1885
1939
  const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
@@ -1944,7 +1998,7 @@ function renderDash() {
1944
1998
  if (a.verified === false) att.push(Object.assign({ pill: 'fail', text: T('token 无效') }, base));
1945
1999
  else if (ex.label === '已过期') att.push(Object.assign({ pill: 'fail', text: T('token 已过期') }, base));
1946
2000
  else if (ex.warn) att.push(Object.assign({ pill: 'limited', text: ex.label.slice(5, 10) + T(' 过期') }, base));
1947
- if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: a.provider === 'workbuddy-intl' ? T('活跃失败') : T('领取失败'), title: st.msg }, base));
2001
+ if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: T('领取失败'), title: st.msg }, base));
1948
2002
  }
1949
2003
  for (const c of (tr.quotas && tr.quotas.connections) || []) {
1950
2004
  if (!compVisible('tenrouter')) break;
@@ -1970,8 +2024,6 @@ function renderDash() {
1970
2024
  ? recent.map((a) => {
1971
2025
  const r = a.lastResult;
1972
2026
  let [cls, txt] = RES[r.status] || ['pending', r.status];
1973
- // 国际版 WorkBuddy 是保持活跃(不保证奖励),不叫「领取」
1974
- if (a.provider === 'workbuddy-intl' && r.status === 'checked-in') txt = T('活跃达成');
1975
2027
  const label = r.status === 'checked-in' && r.amount ? txt + ' +' + fmtNum(r.amount) : txt;
1976
2028
  return '<div class="item" data-view="' + a.product + '" title="' + esc(r.message || '') + '"><span class="item-main"><span class="item-name">' + esc(a.name || a.email || a.id) + '</span>' + provBadges(a.provider) + '</span>'
1977
2029
  + '<span class="item-side"><span class="pill ' + cls + '">' + esc(label) + '</span><span class="item-time">' + fmtTime(r.at).slice(5) + '</span></span></div>';
@@ -2001,11 +2053,11 @@ function renderStatus() {
2001
2053
  } else {
2002
2054
  chip.innerHTML = '';
2003
2055
  }
2004
- chip.title = sch.lastTick ? T('上一轮 ') + fmtTime(sch.lastTick.at) + T(':') + sch.lastTick.summary : '';
2056
+ chip.title = sch.lastTick ? T('上一轮 ') + fmtTime(sch.lastTick.at) + T(':') + (sch.lastTick.summaryKey ? T(sch.lastTick.summaryKey, sch.lastTick.summaryArgs) : sch.lastTick.summary || '') : '';
2005
2057
  // 产品相关提示只在对应标签页出现
2006
2058
  const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
2007
2059
  const n = $('notice');
2008
- n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || state.view === 'minimax' || (hasIntl && Boolean(s.riskIdentity)));
2060
+ n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || state.view === 'minimax' || (state.view === 'workbuddy' && s.workbuddyClient && s.workbuddyClient.installed === false) || (hasIntl && Boolean(s.riskIdentity)));
2009
2061
  if (state.view === 'minimax') {
2010
2062
  n.style.display = '';
2011
2063
  n.innerHTML = T('<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领 800~2000 积分 + 400 连签加成。支持自动检测本机 MiniMax Code 凭据与 Bearer Token 导入,自带标准 Anthropic 本地网关。</span></div>');
@@ -2030,6 +2082,10 @@ function renderStatus() {
2030
2082
  n.innerHTML = T('<div class="z-row"><span class="z-label">活动领取</span><span class="z-text">活动包积分,每日 0 点左右刷新(无每日签到)。自动领取')
2031
2083
  + (ac ? T('每 ') + ziv + T(' 分钟检查资格') + (zwin > 0 ? T(',本次运行 ') + zwin + T(' 分钟') : T(',持续运行')) : T('未开启(右上角设置轮询计划)'))
2032
2084
  + T(';礼物图标 = 手动临时领。</span></div>');
2085
+ } else if (state.view === 'workbuddy' && s.workbuddyClient && s.workbuddyClient.installed === false) {
2086
+ // WorkBuddy 客户端未安装:切换线是死路(写凭据没人应用),进页就明说而不是切了才失败
2087
+ n.style.display = '';
2088
+ n.innerHTML = T('<div class="z-row"><span class="z-label">客户端</span><span class="z-text">本机未安装 WorkBuddy / CodeBuddy 客户端:Token 导入、额度读取与每日领取不受影响;但「切换账号」需要本机客户端(切换是写入客户端凭据由其应用)。请先安装并登录客户端,再「本机导入」。</span></div>');
2033
2089
  } else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
2034
2090
  n.style.display = '';
2035
2091
  const busy = s.umid.installing || umidBusy;
@@ -2082,6 +2138,30 @@ async function installUmid() {
2082
2138
  $('notice').addEventListener('click', (e) => { if (e.target.closest('#btn-umid')) installUmid(); });
2083
2139
  // 覆盖 #notice(各产品横幅)与 #z-actions(ZCode 工具栏按钮组)的点击
2084
2140
  document.addEventListener('click', async (e) => {
2141
+ // 免费额度页:站点卡隐藏 / 全部显示(拖拽排序在 renderFreePool 里经 bindCardDrag 绑定)
2142
+ const hideBtn = e.target.closest('[data-free-hide]');
2143
+ if (hideBtn) {
2144
+ state.hiddenFreeSites.add(hideBtn.dataset.freeHide);
2145
+ saveHiddenFreeSites();
2146
+ renderFreePool();
2147
+ return;
2148
+ }
2149
+ const doubaoBtn = e.target.closest('[data-open-doubao]');
2150
+ if (doubaoBtn) {
2151
+ if (window.creditdaddy && typeof window.creditdaddy.openDoubaoClient === 'function') {
2152
+ window.creditdaddy.openDoubaoClient().then((r) => {
2153
+ if (r && r.ok) toast(T('已启动豆包客户端'), true);
2154
+ else toast((r && r.error) || T('无法启动豆包客户端'), false);
2155
+ }).catch((err) => toast(String(err && err.message || err), false));
2156
+ } else toast(T('需要桌面版 CreditDaddy'), false);
2157
+ return;
2158
+ }
2159
+ if (e.target.closest('#free-show-all')) {
2160
+ state.hiddenFreeSites.clear();
2161
+ saveHiddenFreeSites();
2162
+ renderFreePool();
2163
+ return;
2164
+ }
2085
2165
  if (e.target.closest('#zt-open')) {
2086
2166
  try {
2087
2167
  const r = await window.creditdaddy.openZcodeClient();
@@ -2488,30 +2568,78 @@ async function loadCookieSites(force) {
2488
2568
  return state.cookieSites;
2489
2569
  }
2490
2570
 
2571
+ // 免费额度页站点卡:顺序 / 显隐(本机持久化;拖拽复用 bindCardDrag,眼睛按钮切换显隐)
2572
+ const FREE_ORDER_KEY = 'qd_free_site_order';
2573
+ const FREE_HIDDEN_KEY = 'qd_hidden_free_sites';
2574
+ state.freeOrder = (() => {
2575
+ let arr = [];
2576
+ try { arr = JSON.parse(localStorage.getItem(FREE_ORDER_KEY) || '[]'); } catch {}
2577
+ return Array.isArray(arr) ? arr : [];
2578
+ })();
2579
+ function saveFreeOrder() { try { localStorage.setItem(FREE_ORDER_KEY, JSON.stringify(state.freeOrder)); } catch {} }
2580
+ state.hiddenFreeSites = (() => {
2581
+ let list = [];
2582
+ try { list = JSON.parse(localStorage.getItem(FREE_HIDDEN_KEY) || '[]'); } catch {}
2583
+ return new Set(Array.isArray(list) ? list : []);
2584
+ })();
2585
+ function saveHiddenFreeSites() { try { localStorage.setItem(FREE_HIDDEN_KEY, JSON.stringify([...state.hiddenFreeSites])); } catch {} }
2586
+ const freeVisible = (id) => !state.hiddenFreeSites.has(id);
2587
+
2588
+ function orderedFreeSites(sites) {
2589
+ const ids = sites.map((s) => s.id);
2590
+ const arr = state.freeOrder.filter((id) => ids.includes(id));
2591
+ for (const id of ids) if (!arr.includes(id)) arr.push(id);
2592
+ state.freeOrder = arr;
2593
+ return arr.map((id) => sites.find((s) => s.id === id)).filter(Boolean);
2594
+ }
2595
+
2491
2596
  function renderFreePool() {
2492
2597
  const body = $('free-pool-body');
2493
2598
  if (!body) return;
2494
2599
  const sites = state.cookieSites;
2495
2600
  const canCapture = Boolean(window.creditdaddy && typeof window.creditdaddy.openAuthWindow === 'function');
2496
2601
  if (!sites) { body.innerHTML = T('<div class="dim">加载中…</div>'); loadCookieSites().then(() => renderFreePool()); return; }
2497
- const totalAccounts = sites.reduce((sum, s) => sum + (s.accounts?.length || 0), 0);
2602
+ const enabledSites = sites.filter((s) => s.disabled !== true);
2603
+ const totalAccounts = enabledSites.reduce((sum, s) => sum + (s.accounts?.length || 0), 0);
2498
2604
  if ($('free-total-count')) $('free-total-count').textContent = totalAccounts;
2499
- if ($('free-sites-count')) $('free-sites-count').textContent = sites.length;
2605
+ if ($('free-sites-count')) $('free-sites-count').textContent = enabledSites.length;
2500
2606
  if ($('cnt-free')) $('cnt-free').textContent = totalAccounts;
2501
2607
 
2502
- if (!sites.length) {
2608
+ if (!enabledSites.length) {
2503
2609
  body.innerHTML = T('<div class="muted-empty">暂无可捕获的供应商</div>');
2504
2610
  return;
2505
2611
  }
2506
2612
 
2507
- body.innerHTML = sites.map((site) => {
2613
+ const hiddenCount = enabledSites.filter((s) => !freeVisible(s.id)).length;
2614
+ const showHiddenBar = hiddenCount > 0;
2615
+ const visibleSites = orderedFreeSites(sites).filter((s) => freeVisible(s.id) && s.disabled !== true);
2616
+
2617
+ body.innerHTML = (showHiddenBar
2618
+ ? T('<div class="hint" style="grid-column:1/-1;display:flex;align-items:center;gap:8px">') + T('{n} 个站点已隐藏', { n: hiddenCount })
2619
+ + ' <button class="btn" id="free-show-all" style="padding:2px 10px;font-size:12px">' + T('全部显示') + '</button></div>'
2620
+ : '')
2621
+ + visibleSites.map((site) => {
2622
+ // entryOnly 站点(豆包):纯快捷入口卡——只出「打开」按钮与说明,无捕获/账号
2623
+ if (site.entryOnly === true) {
2624
+ const openBtn = canCapture
2625
+ ? '<button class="btn primary" data-open-doubao="1">' + I.play + T('打开豆包') + '</button>'
2626
+ : T('<button class="btn" disabled title="需要桌面版 CreditDaddy">打开豆包</button>');
2627
+ return '<div class="card" style="display:flex;flex-direction:column;justify-content:flex-start;gap:8px;padding:14px 16px;background:var(--surface);border:1px solid var(--border-subtle);border-radius:14px;box-shadow:var(--shadow)">'
2628
+ + '<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">'
2629
+ + '<span style="font-size:14px;font-weight:700">' + esc(site.label || site.name) + '</span>' + openBtn + '</div>'
2630
+ + (site.hint ? '<div class="hint" style="margin:2px 0 0">' + esc(T(site.hint)) + '</div>' : '')
2631
+ + '</div>';
2632
+ }
2508
2633
  const rows = site.accounts || [];
2509
2634
  const stateTxt = rows.length
2510
2635
  ? T('<span class="badge ok">已捕获 ') + rows.length + T(' 个会话</span>')
2511
2636
  : T('<span class="badge limited">未捕获</span>');
2512
- const btn = canCapture
2513
- ? '<button class="btn primary" data-capture-site="' + esc(site.id) + '">' + I.plus + T('登录捕获') + '</button>'
2514
- : T('<button class="btn" disabled title="需要桌面版 CreditDaddy(无痕登录窗)">登录捕获</button>');
2637
+ const siteDisabled = site.disabled === true;
2638
+ const btn = siteDisabled
2639
+ ? T('<button class="btn" disabled title="网页 cookie 无法参与该站活动(活动校验客户端环境)">网页版(暂不可用)</button>')
2640
+ : canCapture
2641
+ ? '<button class="btn primary" data-capture-site="' + esc(site.id) + '">' + I.plus + T('网页版') + '</button>'
2642
+ : T('<button class="btn" disabled title="需要桌面版 CreditDaddy(无痕登录窗)">网页版</button>');
2515
2643
  // 「粘贴捕获」:部分站点(小米账号 SSO)登录态挂设备指纹,CD 无痕窗也会被静默 SSO
2516
2644
  // 拉回同一账号——多号并存走系统浏览器登录新号后手动粘 cookie(不依赖登录窗)。
2517
2645
  const pasteBtn = ' <button class="btn" data-paste-capture="' + esc(site.id) + '" title="' + T('在其他浏览器登录该站点后,把会话 cookie 粘贴进来捕获(多账号并存的可靠路径)') + '">' + T('粘贴捕获') + '</button>';
@@ -2605,28 +2733,44 @@ function renderFreePool() {
2605
2733
  + '</div>'
2606
2734
  + '<div style="display:flex;align-items:center;gap:8px;flex:none">'
2607
2735
  + '<span class="dim" style="font-size:11.5px">' + (acc.capturedAt ? esc(fmtTime(acc.capturedAt)) : '–') + '</span>'
2736
+ + (site.id === 'doubao' && canCapture ? '<button class="icon-btn" data-open-doubao="1" title="' + T('打开豆包客户端(签到请在客户端内完成)') + '">' + I.play + '</button>' : '')
2608
2737
  + '<button class="icon-btn" data-act="reprobe" data-site="' + esc(site.id) + '" data-id="' + esc(acc.id) + '" title="' + T('就地重探(免登录,用已存会话刷新身份与额度)') + '">' + I.refresh + '</button>'
2609
2738
  + '<button class="icon-btn del" data-act="del" data-id="' + esc(acc.id) + '" title="' + T('删除此账号') + '">' + I.trash + '</button>'
2610
2739
  + '</div></div>'
2611
2740
  + (metricHtml ? '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding-left:14px">' + metricHtml + '</div>' : '')
2612
2741
  + '</div>';
2613
2742
  }).join('') + '</div>'
2614
- : T('<div class="muted-empty" style="padding:16px 0 6px">还没有捕获该站点的账号,点右上角「登录捕获」开始</div>');
2743
+ : T('<div class="muted-empty" style="padding:16px 0 6px">还没有该站点的账号,点右上角「网页版」登录后自动捕获</div>');
2615
2744
 
2616
2745
  // justify-content 不用 space-between:网格行高被最高卡撑齐后,单行账号会被推到卡底(沉底)
2617
- return '<div class="card" style="display:flex;flex-direction:column;justify-content:flex-start;gap:8px;padding:14px 16px;background:var(--surface);border:1px solid var(--border-subtle);border-radius:14px;box-shadow:var(--shadow)">'
2746
+ const eyeBtn = '<button class="icon-btn" data-free-hide="' + esc(site.id) + '" title="' + T('隐藏此站点卡片(「全部显示」可恢复)') + '">' + I.eyeOff + '</button>';
2747
+ return '<div class="card" draggable="true" data-id="' + esc(site.id) + '" style="display:flex;flex-direction:column;justify-content:flex-start;gap:8px;padding:14px 16px;background:var(--surface);border:1px solid var(--border-subtle);border-radius:14px;box-shadow:var(--shadow)">'
2618
2748
  + '<div><div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px">'
2619
2749
  + '<div style="display:flex;align-items:center;gap:8px">'
2620
2750
  + '<span style="font-size:14px;font-weight:700">' + esc(site.label || site.name) + '</span>'
2621
2751
  + stateTxt
2622
2752
  + '</div>'
2623
- + '<div style="display:flex;align-items:center;gap:6px">' + btn + pasteBtn + clearBtn + '</div>'
2753
+ + '<div style="display:flex;align-items:center;gap:6px">' + btn + pasteBtn + clearBtn + eyeBtn + '</div>'
2624
2754
  + '</div>'
2625
2755
  + hint
2626
2756
  + '</div>'
2627
2757
  + accRows
2628
2758
  + '</div>';
2629
2759
  }).join('');
2760
+ bindCardDrag(body, {
2761
+ kind: 'free-site',
2762
+ onReorder: (drag, targetId) => {
2763
+ if (!drag.id) return;
2764
+ const ids = state.freeOrder.slice();
2765
+ const from = ids.indexOf(drag.id);
2766
+ if (from >= 0) ids.splice(from, 1);
2767
+ const ti = ids.indexOf(targetId);
2768
+ ids.splice(ti < 0 ? ids.length : ti, 0, drag.id);
2769
+ state.freeOrder = ids;
2770
+ saveFreeOrder();
2771
+ renderFreePool();
2772
+ },
2773
+ });
2630
2774
  }
2631
2775
 
2632
2776
  function renderCookieSites() {
@@ -2642,8 +2786,8 @@ function renderCookieSites() {
2642
2786
  ? T('<span class="dim">已捕获 ') + rows.length + T(' 个会话</span>')
2643
2787
  : T('<span class="dim">未捕获</span>');
2644
2788
  const btn = canCapture
2645
- ? '<button class="btn" data-capture-site="' + esc(site.id) + '">' + T('登录捕获') + '</button>'
2646
- : T('<button class="btn" disabled title="需要桌面版 CreditDaddy(无痕登录窗)">登录捕获</button>');
2789
+ ? '<button class="btn" data-capture-site="' + esc(site.id) + '">' + T('网页版') + '</button>'
2790
+ : T('<button class="btn" disabled title="需要桌面版 CreditDaddy(无痕登录窗)">网页版</button>');
2647
2791
  const hint = site.hint ? T('<div class="hint">') + esc(site.hint) + '</div>' : '';
2648
2792
  const accRows = rows.map((acc) =>
2649
2793
  '<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px">'
@@ -2655,13 +2799,115 @@ function renderCookieSites() {
2655
2799
  + hint + accRows + '</div>';
2656
2800
  }).join('');
2657
2801
  }
2658
- function openCookieSitesModal() {
2659
- const body = $('cookie-sites-body');
2660
- if (body) body.innerHTML = T('<div class="dim">加载中…</div>');
2661
- openModal('m-cookie');
2662
- loadCookieSites(true).then(() => renderCookieSites());
2802
+ /**
2803
+ * 「账号管理」弹窗:把两件同性质的事收在一处——
2804
+ * 1. 账号同步:本机 Qoder / WorkBuddy 账号推为 10Router 上游连接(原藏在「设置」弹窗底部)
2805
+ * 2. Cookie 会话:网页会话型供应商的捕获入口(原名「Cookie 会话」按钮)
2806
+ * 面板密码栏同步展示:账号同步走 OAuth 通道时要它(带 refreshToken,自动续期)。
2807
+ *
2808
+ * 渠道开关(每渠道一行勾选):取消勾选 = 两边都不再推——
2809
+ * 账号连接(sync-accounts)跳过该渠道的账号;额度卡推送跳过该渠道账号的免费卡。
2810
+ * 场景:10Router 已单独配了同源渠道(如 minimax-code 连接),CD 侧维护同名免费卡
2811
+ * (minimax-free-*)纯属重复——两边卡片并存且数字各刷各的。
2812
+ */
2813
+ const AM_CHANNELS = [
2814
+ { provider: 'minimax', label: 'MiniMax', syncable: false },
2815
+ { provider: 'tokenharbor', label: 'Token Harbor', syncable: true },
2816
+ { provider: 'xiaomi-mimo', label: '小米 MiMo', syncable: false },
2817
+ { provider: 'river', label: 'River AI', syncable: false },
2818
+ { provider: 'modelscope', label: 'ModelScope 魔搭', syncable: false },
2819
+ { provider: 'longcat', label: 'LongCat', syncable: false },
2820
+ { provider: 'qoder', label: 'Qoder 国际版', syncable: true },
2821
+ { provider: 'qoder-cn', label: 'Qoder 国内版', syncable: true },
2822
+ { provider: 'workbuddy', label: 'WorkBuddy 国内版', syncable: true },
2823
+ { provider: 'workbuddy-intl', label: 'WorkBuddy 国际版', syncable: true },
2824
+ ];
2825
+
2826
+ function openTrAccountManage() {
2827
+ if (!(tr.config && tr.config.configured)) return toast(T('请先保存 10Router 设置'), false);
2828
+ $('am-sync-msg').textContent = '';
2829
+ $('am-pw').value = '';
2830
+ $('am-pw').placeholder = T('填 10Router 面板密码,走 OAuth 通道(带 refreshToken 自动续期);不填则按已保存的走');
2831
+ const btn = $('am-sync');
2832
+ btn.disabled = false;
2833
+ btn.querySelector('.dot').className = 'dot' + (tr.config.configured ? ' on' : '');
2834
+ btn.querySelector('.lbl').textContent = T('同步账号到 10R');
2835
+ renderAmChannels();
2836
+ loadCookieSites(true).then(() => renderCookieSites()).catch(() => {});
2837
+ openModal('m-am');
2838
+ }
2839
+
2840
+ function renderAmChannels() {
2841
+ const c = tr.config || {};
2842
+ const enabled = c.accountSync?.enabled !== false;
2843
+ const excluded = (c.quotaPushDisabledProviders || []);
2844
+ const have = new Set((state.accounts || []).map((a) => a.provider));
2845
+ const box = $('am-sync-channels');
2846
+ if (!box) return;
2847
+ const rows = AM_CHANNELS.filter((ch) => have.has(ch.provider)).map((ch) => {
2848
+ // 勾选语义 = 推送该渠道:账号同步看 accountSync(总开关关了整组禁用),
2849
+ // 额度卡看 quotaPushDisabledProviders 反集
2850
+ const syncOn = enabled && ((c.accountSync?.providers || []).length
2851
+ ? (c.accountSync.providers.includes(ch.provider)) : ch.syncable);
2852
+ const pushOn = !excluded.includes(ch.provider);
2853
+ return '<label class="check" style="display:flex;gap:6px;align-items:center">'
2854
+ + '<input type="checkbox" data-am-push="' + ch.provider + '"' + (pushOn ? ' checked' : '') + '> ' + esc(T(ch.label))
2855
+ + '<label class="check" style="margin-left:auto;gap:4px"><input type="checkbox" data-am-sync="' + ch.provider + '"' + (syncOn ? ' checked' : '') + (!ch.syncable || !enabled ? ' disabled' : '') + '> <span class="dim">连接</span></label>'
2856
+ + '</label>';
2857
+ });
2858
+ box.innerHTML = rows.length
2859
+ ? '<div class="hint" style="margin-bottom:4px">' + T('勾选 = 同步该渠道(「连接」= 账号连接;主勾 = 额度卡推送)。10Router 已单独配了同源渠道时,取消对应渠道即可去掉重复卡片。') + '</div>'
2860
+ + '<div style="display:grid;gap:4px">' + rows.join('') + '</div>'
2861
+ + '<div style="margin-top:6px"><button class="btn" id="am-channels-save">' + T('保存渠道设置') + '</button></div>'
2862
+ : '';
2863
+ $('am-channels-save')?.addEventListener('click', saveAmChannels);
2864
+ }
2865
+
2866
+ async function saveAmChannels() {
2867
+ const push = [...document.querySelectorAll('#am-sync-channels input[data-am-push]:checked')].map((x) => x.dataset.amPush);
2868
+ const sync = [...document.querySelectorAll('#am-sync-channels input[data-am-sync]:checked')].map((x) => x.dataset.amSync);
2869
+ try {
2870
+ tr.config = await api('/api/tenrouter', {
2871
+ method: 'PUT',
2872
+ json: {
2873
+ accountSyncEnabled: true,
2874
+ accountSyncProviders: sync,
2875
+ // 主勾没勾的 = 排除推送
2876
+ quotaPushDisabledProviders: AM_CHANNELS.map((c) => c.provider).filter((p) => !push.includes(p)),
2877
+ },
2878
+ });
2879
+ toast(T('渠道同步设置已保存'), true);
2880
+ renderAmChannels();
2881
+ } catch (e) { toast(e.message, false); }
2882
+ }
2883
+ async function trSyncAccountsFromModal() {
2884
+ if (!(tr.config && tr.config.configured)) return toast(T('请先保存 10Router 设置'), false);
2885
+ const btn = $('am-sync');
2886
+ const msg = $('am-sync-msg');
2887
+ const pw = $('am-pw').value;
2888
+ btn.disabled = true;
2889
+ btn.querySelector('.dot').className = 'dot on pulse';
2890
+ btn.querySelector('.lbl').textContent = T('同步账号中…');
2891
+ tr.accountsSyncing = true;
2892
+ renderTenrouter();
2893
+ try {
2894
+ const r = await api('/api/tenrouter/sync-accounts', { method: 'POST', json: pw ? { adminPassword: pw } : {} });
2895
+ msg.textContent = r.summary || '';
2896
+ toast(r.summary, !/失败/.test(r.summary || ''));
2897
+ if (pw) { await loadTrConfig(); $('am-pw').value = ''; }
2898
+ loadLogs();
2899
+ } catch (e) {
2900
+ msg.textContent = e.message;
2901
+ toast(e.message, false);
2902
+ } finally {
2903
+ tr.accountsSyncing = false;
2904
+ btn.disabled = false;
2905
+ btn.querySelector('.dot').className = 'dot on';
2906
+ btn.querySelector('.lbl').textContent = T('同步账号到 10R');
2907
+ renderTenrouter();
2908
+ }
2663
2909
  }
2664
- document.getElementById('m-cookie')?.addEventListener('click', (e) => {
2910
+ document.getElementById('m-am')?.addEventListener('click', (e) => {
2665
2911
  const b = e.target.closest && e.target.closest('[data-capture-site]');
2666
2912
  if (b) cookieSiteCapture(b.dataset.captureSite);
2667
2913
  });
@@ -2697,11 +2943,13 @@ $('btn-free-refresh')?.addEventListener('click', () => {
2697
2943
  $('btn-free-settings')?.addEventListener('click', openFreeSettings);
2698
2944
  // 「同步到 10Router」= 推连接(会话凭据)+ 推额度卡(issue #24 数字快照),一键两路
2699
2945
  $('btn-free-sync-10r')?.addEventListener('click', async () => {
2700
- await trSyncAccounts();
2701
2946
  const btn = $('btn-free-sync-10r');
2702
- if (!(tr.config && tr.config.configured)) return;
2947
+ if (!(tr.config && tr.config.configured)) return toast(T('请先保存 10Router 设置'), false);
2703
2948
  btn.disabled = true;
2704
2949
  try {
2950
+ // 先推账号连接(原 trSyncAccounts 的职责),失败不阻断额度卡推送
2951
+ const sa = await api('/api/tenrouter/sync-accounts', { method: 'POST', json: {} }).catch((e) => ({ summary: e.message, error: true }));
2952
+ if (sa && sa.summary) toast(sa.summary, !sa.error && !/失败/.test(sa.summary));
2705
2953
  const r = await api('/api/tenrouter/push-quota', { method: 'POST' });
2706
2954
  const failed = (r.results || []).filter((x) => x.status === 'failed');
2707
2955
  if (failed.length) toast(T('额度卡部分推送失败:') + failed.map((x) => x.source + '(' + x.error + ')').join(';'), false);
@@ -2736,7 +2984,7 @@ $('free-settings-save')?.addEventListener('click', async () => {
2736
2984
  },
2737
2985
  });
2738
2986
  closeModal('m-free-settings');
2739
- toast(T('免费额度推送设置已保存'));
2987
+ toast(T('额度卡片推送设置已保存'));
2740
2988
  } catch (e) { toast(e.message, false); }
2741
2989
  });
2742
2990
  $('free-settings-push-now')?.addEventListener('click', async () => {
@@ -2780,7 +3028,10 @@ async function cookieSitePasteCapture(siteId) {
2780
3028
  try {
2781
3029
  const r = await api('/api/cookie-sites/' + encodeURIComponent(siteId) + '/capture', { json: { cookie: cookie.trim() } });
2782
3030
  if (r && r.ok) {
2783
- toast(T('{name} cookie 捕获成功,同步到 10Router 后卡片点亮', { name: site.name }), true);
3031
+ // 豆包不推 10R(无外部配额卡),文案不带「同步 10Router」以免误导
3032
+ toast(site.no10rSync
3033
+ ? T('{name} cookie 捕获成功,额度卡片已就绪', { name: site.name })
3034
+ : T('{name} cookie 捕获成功,同步到 10Router 后卡片点亮', { name: site.name }), true);
2784
3035
  state.cookieSites = null;
2785
3036
  loadCookieSites(true).then(() => { if (state.view === 'free') renderFreePool(); });
2786
3037
  } else toast(T('捕获失败'), false);
@@ -2796,7 +3047,10 @@ if (window.creditdaddy && typeof window.creditdaddy.onCookieCaptured === 'functi
2796
3047
  window.creditdaddy.onCookieCaptured((payload) => {
2797
3048
  if (payload && payload.ok === false) { toast(payload.error || T('cookie 捕获失败'), false); return; }
2798
3049
  const name = payload && payload.name;
2799
- toast(T('{name} cookie 捕获成功,同步到 10Router 后卡片点亮', { name: name || '' }), true);
3050
+ const site2 = (state.cookieSites || []).find((s) => s.name === name) || null;
3051
+ toast(site2?.no10rSync
3052
+ ? T('{name} cookie 捕获成功,额度卡片已就绪', { name: name || '' })
3053
+ : T('{name} cookie 捕获成功,同步到 10Router 后卡片点亮', { name: name || '' }), true);
2800
3054
  state.cookieSites = null;
2801
3055
  Promise.allSettled([loadCookieSites(true), refreshAccountList(), loadTrQuotas(true)])
2802
3056
  .then(() => {
@@ -2804,6 +3058,11 @@ if (window.creditdaddy && typeof window.creditdaddy.onCookieCaptured === 'functi
2804
3058
  if (state.view === 'free') renderFreePool();
2805
3059
  renderCookieSites();
2806
3060
  renderDash();
3061
+ // 收割时余额快照已随账号落库——账号卡上的余额读 state.quotas 缓存,
3062
+ // 不重拉就显示旧数字(实测:小米收割成功后卡面 ¥0.00 挂到手动刷新)。
3063
+ // 非 live 读快照零网络,成本可忽略;只刷 cookie-site 型账号。
3064
+ const ids = (state.accounts || []).filter((a) => a.meta?.cookieSiteId).map((a) => a.id);
3065
+ if (ids.length) loadQuotas(ids).then(renderDash);
2807
3066
  });
2808
3067
  });
2809
3068
  }
@@ -2871,8 +3130,8 @@ function renderTenrouter() {
2871
3130
  + '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + (tr.loading ? T('刷新中…') : T('刷新额度')) + '</button>'
2872
3131
  + T('<button class="btn" data-tr="sync" title="用量同步:自动开关、同步来源与手动同步(点我打开)"') + (tr.syncing || c.syncing ? ' disabled' : '') + '>'
2873
3132
  + '<span class="dot' + (c.sync.enabled ? ' on' : '') + '"></span>' + (tr.syncing || c.syncing ? T('同步中…') : T('用量同步')) + '</button>'
3133
+ + '<button class="btn" data-tr="account-manage" title="' + T('账号同步与网页会话 Cookie 捕获') + '">' + T('账号管理') + '</button>'
2874
3134
  + T('<button class="btn" data-tr="settings">设置</button>')
2875
- + T('<button class="btn" data-tr="cookie-sites" title="Token Harbor 等网页会话型供应商">Cookie 会话</button>')
2876
3135
  + '</div></div>'
2877
3136
  + tabStrip
2878
3137
  + '<div class="tr-grid">' + grid + '</div>';
@@ -2993,19 +3252,6 @@ async function trDisconnect() {
2993
3252
  try { tr.config = await api('/api/tenrouter', { method: 'DELETE' }); tr.quotas = null; tr.error = null; closeModal('m-10r'); render(); }
2994
3253
  catch (e) { toast(e.message, false); }
2995
3254
  }
2996
- async function trSyncAccounts() {
2997
- if (!(tr.config && tr.config.configured)) return toast(T('请先保存 10Router 设置'), false);
2998
- const btn = $('tr-sync-accounts');
2999
- const pw = $('tr-admin-pw').value;
3000
- btn.disabled = true;
3001
- try {
3002
- const r = await api('/api/tenrouter/sync-accounts', { method: 'POST', json: pw ? { adminPassword: pw } : {} });
3003
- toast(r.summary, !/失败/.test(r.summary));
3004
- if (pw) await loadTrConfig();
3005
- loadLogs();
3006
- } catch (e) { toast(e.message, false); }
3007
- btn.disabled = false;
3008
- }
3009
3255
  $('tr-panel').addEventListener('click', (e) => {
3010
3256
  const b = e.target.closest('[data-tr]');
3011
3257
  if (!b) return;
@@ -3013,7 +3259,7 @@ $('tr-panel').addEventListener('click', (e) => {
3013
3259
  if (b.dataset.tr === 'settings') openTrSettings();
3014
3260
  else if (b.dataset.tr === 'refresh') loadTrQuotas(true);
3015
3261
  else if (b.dataset.tr === 'sync') openTrSync();
3016
- else if (b.dataset.tr === 'cookie-sites') goView('free');
3262
+ else if (b.dataset.tr === 'account-manage') openTrAccountManage();
3017
3263
  else if (b.dataset.tr === 'vendor') {
3018
3264
  const v = b.dataset.v || '';
3019
3265
  state.trVendor = v;
@@ -3029,7 +3275,7 @@ $('tr-sync-now').onclick = async () => {
3029
3275
  trSync();
3030
3276
  api('/api/tenrouter/push-quota', { method: 'POST' }).catch(() => {});
3031
3277
  };
3032
- $('tr-sync-accounts').onclick = trSyncAccounts;
3278
+ $('am-sync').onclick = trSyncAccountsFromModal;
3033
3279
  $('tr-save').onclick = trSave;
3034
3280
  $('tr-disconnect').onclick = trDisconnect;
3035
3281
 
@@ -3048,7 +3294,12 @@ async function syncAfterZcodeClaims() {
3048
3294
  seenZcodeTickAt = t.at;
3049
3295
  if (first || !(t.claimedIds && t.claimedIds.length)) return; // 首次加载已全量拉取
3050
3296
  try { state.accounts = (await api('/api/accounts')).accounts; } catch (e) {}
3051
- loadQuotas(t.claimedIds);
3297
+ await loadQuotas(t.claimedIds);
3298
+ // 额度拿到后必须重绘:ZCode 活动发的额度常带 effective_at(如 23:00 到账),
3299
+ // 卡片从「无有效套餐」变成「待生效 N Token · HH:MM 到账」,不重绘就一直停在旧样子,
3300
+ // 用户以为「领取成功了却不显示」——只能自己手动点刷新。
3301
+ render();
3302
+ renderStatus();
3052
3303
  loadZGateway();
3053
3304
  }
3054
3305
  async function loadAccounts() {
@@ -3208,10 +3459,15 @@ async function onCardClick(e) {
3208
3459
  if ([...sel.options].some((o) => o.value === a.provider && !o.hidden)) sel.value = a.provider;
3209
3460
  return deviceLogin();
3210
3461
  }
3211
- if (act === 'quota') { loadQuotas([id], { live: true }); return; }
3462
+ if (act === 'quota') { loadQuotas([id], { live: true }).then(render); return; }
3212
3463
  if (act === 'claim') return openClaim(id);
3213
3464
  if (act === 'switch') {
3214
3465
  const a = state.accounts.find((x) => x.id === id);
3466
+ // 本机没装客户端时先拦下(WorkBuddy 此前静默硬写凭据目录,面板显示已切换但客户端永远不应用)
3467
+ if (a && a.product === 'workbuddy' && state.status && state.status.workbuddyClient && state.status.workbuddyClient.installed === false) {
3468
+ toast(T('本机未安装 WorkBuddy / CodeBuddy 客户端,无法切换:切换是把登录写入客户端凭据,由客户端应用。请先安装客户端并登录,再「本机导入」。'), false);
3469
+ return;
3470
+ }
3215
3471
  const client = (CLIENT_LABEL[a && a.product] ? T(CLIENT_LABEL[a && a.product]) : '') || (a && prodLabel(a.product)) || T('客户端');
3216
3472
  const tip = a && a.product === 'zcode'
3217
3473
  ? T('\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。')
@@ -3263,6 +3519,16 @@ async function onCardClick(e) {
3263
3519
  } catch (e) {}
3264
3520
  }, 800);
3265
3521
  }
3522
+ if (a && a.product === 'qoder' && r && !r.alreadyActive && window.creditdaddy && typeof window.creditdaddy.openQoderClient === 'function') {
3523
+ // 强制切换会先关掉 Qoder:写完登录自动拉起来,不然用户得自己找快捷方式
3524
+ setTimeout(async () => {
3525
+ try {
3526
+ const re = await window.creditdaddy.openQoderClient(a.provider);
3527
+ if (re && re.ok) toast(T('已重新启动 Qoder 客户端'));
3528
+ else toast((re && re.error) || T('无法打开 Qoder 客户端(桌面版才能自动拉起)'), false);
3529
+ } catch (e) {}
3530
+ }, 800);
3531
+ }
3266
3532
  if (a && a.product === 'trae' && r && !r.alreadyActive && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function') {
3267
3533
  setTimeout(async () => {
3268
3534
  try {
@@ -3458,7 +3724,8 @@ async function doClaim(i) {
3458
3724
  });
3459
3725
  toast(T('领取成功:') + (r.planName || pl.name || pl.planId));
3460
3726
  closeModal('m-claim');
3461
- loadQuotas([claimState.accountId]);
3727
+ await loadStatus(); // 徽标(已领取)在账号的 lastResult 上,必须先重拉账号
3728
+ await loadAccounts(); // loadQuotas 之后必须重绘,否则卡片停在旧样子
3462
3729
  loadLogs();
3463
3730
  } catch (e) {
3464
3731
  const errBox = $('claim-status');
@@ -3657,7 +3924,7 @@ function openChangelogModal() {
3657
3924
  + '<div style="font-weight:700;margin:0 0 4px">v' + esc(s.version)
3658
3925
  + (s.date ? ' <span class="dim" style="font-weight:400;font-size:11px">' + esc(s.date) + '</span>' : '') + '</div>'
3659
3926
  + (s.theme ? '<div class="hint">' + strip(s.theme) + '</div>' : '')
3660
- + (s.topics && s.topics.length ? '<div class="hint" style="margin-top:4px">' + s.topics.map(strip).join(' · ') + '</div>' : '')
3927
+ + (s.topics && s.topics.length ? '<div class="hint" style="margin-top:4px">' + s.topics.map((t) => strip(T(t.trim()))).join(' · ') + '</div>' : '')
3661
3928
  + '</div>').join('')
3662
3929
  : T('<div class="hint">暂无本地更新日志,请到 GitHub Releases 查看。</div>');
3663
3930
  }).catch(openReleases);
@@ -3767,7 +4034,7 @@ $('znet-ok').addEventListener('click', async () => {
3767
4034
  state.zProxyFirst = r.proxyFirst === true;
3768
4035
  await loadZNet();
3769
4036
  $('znet-url').value = '';
3770
- $('znet-url').placeholder = state.zProxyUrl || (state.zHasEnvProxy ? T('跟随环境变量 HTTPS_PROXY') : 'http://192.168.31.101:7890');
4037
+ $('znet-url').placeholder = state.zProxyUrl ? T('当前代理:{url}(输入新值可更换,留空清除)', { url: state.zProxyUrl }) : (state.zHasEnvProxy ? T('跟随环境变量 HTTPS_PROXY') : 'http://192.168.31.101:7890');
3771
4038
  $('znet-state').textContent = T('已保存:') + (state.zProxyFirst === true ? T('代理优先') : T('直连优先'))
3772
4039
  + (state.zProxyUrl ? T(' · 代理 ') + state.zProxyUrl : '');
3773
4040
  toast(T('网络出口:') + (r.proxyFirst ? T('代理优先(直连兜底)') : T('直连优先(代理兜底)')) + (body.proxyUrl === '' ? T(',代理已清除') : T(',代理已保存')));
@@ -3844,10 +4111,11 @@ async function localScan() {
3844
4111
  const sub = c.kind === 'app'
3845
4112
  ? esc(c.source) + (c.email ? ' · ' + esc(mask(c.email)) : c.phone ? ' · ' + esc(c.phone) : '') + (c.expiresAt ? T(' · 有效期至 ') + esc(fmtTime(c.expiresAt)) : '')
3846
4113
  : esc(c.source) + ' · ' + esc(String(c.file || '').split(/[\\/]/).pop()) + T('(') + c.hits + T(' 处)');
4114
+ const intl = c.intlWarning ? '<div class="hint" style="color:var(--warn,#d4b83a);margin-top:4px">⚠ ' + esc(c.intlWarning) + '</div>' : '';
3847
4115
  const btns = c.imported ? T('<span class="pill ok">已导入</span>') + (c.provider ? '<button class="btn" data-cid="' + esc(c.id) + T('" title="用本机凭据更新 token / 有效期 / 邮箱">同步信息</button>') : '')
3848
4116
  : c.provider ? '<button class="btn primary" data-cid="' + esc(c.id) + T('">导入</button>')
3849
4117
  : '<button class="btn" data-cid="' + esc(c.id) + T('" data-prov="qoder">导入·国际</button><button class="btn" data-cid="') + esc(c.id) + T('" data-prov="qoder-cn">导入·国内</button>');
3850
- html += '<div class="cand"><div class="cand-main"><div class="cand-name">' + title + prov + '</div><div class="cand-sub">' + sub + '</div></div><div class="row">' + btns + '</div></div>';
4118
+ html += '<div class="cand"><div class="cand-main"><div class="cand-name">' + title + prov + '</div><div class="cand-sub">' + sub + '</div>' + intl + '</div><div class="row">' + btns + '</div></div>';
3851
4119
  }
3852
4120
  for (const e of r.errors || []) {
3853
4121
  html += T('<div class="hint" style="color:var(--err)">读取失败:') + esc(e.file) + T(':') + esc(e.error) + '</div>';
@@ -4308,7 +4576,6 @@ $('btn-comp').addEventListener('click', async () => {
4308
4576
  renderCompMenu();
4309
4577
  fillAutoCheckin();
4310
4578
  $('cfg-logs-display').value = logsDisplayDefault();
4311
- $('cfg-wb-activity').checked = wbActOn();
4312
4579
  // 网络出口(全局):预填当前顺序与脱敏代理地址
4313
4580
  await loadZNet();
4314
4581
  (state.zProxyFirst === true ? $('znet-proxy') : $('znet-direct')).checked = true;