creditdaddy 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/panel.html CHANGED
@@ -126,6 +126,8 @@
126
126
  html[data-theme="dark"] .badge.prod.catpaw { color: #fdba74; }
127
127
  .badge.prod.trae { color: #047857; background: rgba(16,185,129,.14); }
128
128
  html[data-theme="dark"] .badge.prod.trae { color: #6ee7b7; }
129
+ .badge.prod.minimax { color: #e11d48; background: rgba(244,63,94,.14); }
130
+ html[data-theme="dark"] .badge.prod.minimax { color: #fda4af; }
129
131
  .seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
130
132
  .seg-ctl button { border: none; background: transparent; color: var(--text-2); font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 7px; cursor: pointer; }
131
133
  .seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
@@ -247,7 +249,7 @@
247
249
  .prod-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
248
250
  .prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
249
251
  .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
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; }
252
+ .dot.qoder { background: #7c3aed; } .dot.workbuddy { background: #14b8a6; } .dot.zcode { background: #2563eb; } .dot.mirasim { background: #64748b; } .dot.catpaw { background: #f97316; } .dot.trae { background: #10b981; } .dot.minimax { background: #f43f5e; }
251
253
  .prod-go { color: var(--text-3); font-size: 12px; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; }
252
254
  .prod-go svg { width: 12px; height: 12px; transition: transform .15s; }
253
255
  .prodcard:hover .prod-go { color: var(--text); }
@@ -363,6 +365,7 @@
363
365
  <button data-view="mirasim">mirasim<span class="cnt" id="cnt-mirasim">0</span></button>
364
366
  <button data-view="catpaw">妙手<span class="cnt" id="cnt-catpaw">0</span></button>
365
367
  <button data-view="trae">Trae<span class="cnt" id="cnt-trae">0</span></button>
368
+ <button data-view="minimax">MiniMax<span class="cnt" id="cnt-minimax">0</span></button>
366
369
  <button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
367
370
  </nav>
368
371
 
@@ -415,6 +418,10 @@
415
418
  <span id="t-actions" style="display:none">
416
419
  <button class="btn" id="tt-open" style="display:none"></button>
417
420
  </span>
421
+ <span id="m-actions" style="display:none">
422
+ <button class="btn" id="mt-gw"></button>
423
+ <button class="btn" id="mt-open" style="display:none"></button>
424
+ </span>
418
425
  <button class="btn" id="btn-add"></button>
419
426
  <button class="btn" id="btn-import"></button>
420
427
  <button class="btn" id="btn-export"></button>
@@ -430,9 +437,14 @@
430
437
  <div class="panel" id="tr-panel"><div class="empty">加载中…</div></div>
431
438
  </section>
432
439
 
440
+ <!-- 日志卡:运行日志 + 各网关调用日志,卡内标签切换(默认跟随当前页,可手动切) -->
433
441
  <section class="pcard collapsed" id="log-card">
434
442
  <div class="pcard-title">
435
- <span>运行日志</span>
443
+ <div class="seg-ctl" id="log-tabs">
444
+ <button data-log-tab="run" class="active">运行日志</button>
445
+ <button data-log-tab="zcode">ZCode 网关</button>
446
+ <button data-log-tab="minimax">MiniMax 网关</button>
447
+ </div>
436
448
  <button class="btn" id="btn-logs">展开</button>
437
449
  </div>
438
450
  <div class="logs" id="logs">(暂无)</div>
@@ -463,6 +475,7 @@
463
475
  <option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option>
464
476
  <option value="workbuddy">WorkBuddy / CodeBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy / CodeBuddy 国际版</option>
465
477
  <option value="zcode-bigmodel">ZCode · BigModel(智谱)</option><option value="zcode-zai">ZCode · Z.ai(国际)</option>
478
+ <option value="minimax">MiniMax Code</option>
466
479
  </select>
467
480
  </div>
468
481
  <div class="row"><button class="btn primary" id="btn-device">生成授权链接</button><button class="btn" id="dev-cancel" style="display:none">取消</button></div>
@@ -475,7 +488,7 @@
475
488
  <div class="pane" data-pane="token">
476
489
  <div class="field">
477
490
  <label>版本</label>
478
- <select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy">WorkBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy 国际版</option></select>
491
+ <select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy">WorkBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy 国际版</option><option value="minimax">MiniMax</option></select>
479
492
  </div>
480
493
  <div class="field"><label>备注名(可选)</label><input class="input" id="f-name" placeholder="留空则自动读取昵称"></div>
481
494
  <div class="field"><label>Token</label><input class="input mono" id="f-token" placeholder="dt-… / jt-… 或 PAT(pt-…,自动兑换)"></div>
@@ -490,7 +503,7 @@
490
503
  <div class="modal-title"><span>导出账号</span><button class="close" data-close>✕</button></div>
491
504
  <div class="field">
492
505
  <label>范围</label>
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>
506
+ <select class="input" id="ex-provider"><option value="">全部账号</option><option value="p:qoder">Qoder(全部)</option><option value="qoder">仅 Qoder 国际版</option><option value="qoder-cn">仅 Qoder 国内版</option><option value="p:workbuddy">WorkBuddy(全部)</option><option value="workbuddy">仅 WorkBuddy 国内版</option><option value="workbuddy-intl">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option><option value="p:mirasim">mirasim</option><option value="p:catpaw">妙手</option><option value="p:trae">Trae</option><option value="p:minimax">MiniMax</option></select>
494
507
  </div>
495
508
  <div class="field">
496
509
  <label>加密口令(必填)</label>
@@ -640,6 +653,24 @@
640
653
  </div>
641
654
  </div>
642
655
 
656
+ <!-- MiniMax 本地接口 -->
657
+ <div class="mask" id="m-mgw">
658
+ <div class="modal sm">
659
+ <div class="modal-title"><span>MiniMax 本地接口</span><button class="close" data-close>✕</button></div>
660
+ <div class="hint" id="mgw-url" style="margin-bottom:10px"></div>
661
+ <div class="field">
662
+ <label class="check"><input type="checkbox" id="mgw-on"> 启用本地接口(/gateway/minimax/v1/messages)</label>
663
+ </div>
664
+ <div class="field" id="mgw-lan-fields">
665
+ <label class="check"><input type="checkbox" id="mgw-lan"> 对局域网开放(需全局局域网已开启)</label>
666
+ <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
667
+ <textarea class="input" id="mgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
668
+ </div>
669
+ <div class="hint">开启后,MiniMax 积分与模型能力变成本机的一个原生 Anthropic Messages 补全接口——在 10Router 或 Claude Code 等工具中建一个 anthropic-compatible 节点指向本机 <code>/gateway/minimax/v1/messages</code> 即可调用。支持多账号自动轮转、429 限流自动冷却 5 分钟、凭据失效自动静默刷新。白名单内的机器<b>免密直连</b>;本机回环(127.0.0.1)始终放行。</div>
670
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="mgw-ok">保存</button></div>
671
+ </div>
672
+ </div>
673
+
643
674
  <input type="file" id="file-input" accept=".json,application/json" style="display:none">
644
675
  <div id="toast"></div>
645
676
 
@@ -669,8 +700,8 @@ const I = {
669
700
  };
670
701
 
671
702
  const $ = (id) => document.getElementById(id);
672
- const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '' };
673
- const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'tenrouter'];
703
+ const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run' };
704
+ const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'tenrouter'];
674
705
  // 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
675
706
  const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
676
707
  const PRODUCTS = [
@@ -680,6 +711,7 @@ const PRODUCTS = [
680
711
  { id: 'mirasim', label: 'mirasim' },
681
712
  { id: 'catpaw', label: '妙手' },
682
713
  { id: 'trae', label: 'Trae' },
714
+ { id: 'minimax', label: 'MiniMax' },
683
715
  ];
684
716
  const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
685
717
 
@@ -691,6 +723,7 @@ const COMPONENTS = [
691
723
  { id: 'mirasim', label: 'mirasim' },
692
724
  { id: 'catpaw', label: '妙手' },
693
725
  { id: 'trae', label: 'Trae' },
726
+ { id: 'minimax', label: 'MiniMax' },
694
727
  { id: 'tenrouter', label: '10Router' },
695
728
  ];
696
729
  const HIDDEN_COMP_KEY = 'qd_hidden_components';
@@ -812,12 +845,15 @@ const PROV = {
812
845
  mirasim: { prod: 'mirasim', prodLabel: 'mirasim', region: '', cls: '' },
813
846
  catpaw: { prod: 'catpaw', prodLabel: '妙手', region: '', cls: '' },
814
847
  trae: { prod: 'trae', prodLabel: 'Trae', region: '', cls: '' },
848
+ minimax: { prod: 'minimax', prodLabel: 'MiniMax', region: '', cls: '' },
815
849
  };
816
850
  // 支持每日签到的产品(ZCode / mirasim / 妙手 额度靠套餐或滚动窗口发放,不做签到)
817
- const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy', 'trae']);
818
- const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae' };
851
+ const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy', 'trae', 'minimax']);
852
+ const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae', minimax: 'MiniMax' };
819
853
  // 额度单位为 Credits/时长而非「积分」口径的产品(不计入剩余积分合计)
820
854
  const CREDIT_VIEWS = new Set(['zcode', 'mirasim', 'catpaw']);
855
+ // 网关调用日志:tag → 所属产品页(无网关的产品不展示日志卡)
856
+ const GW_LOG_TAG = { zcode: 'ZCODE-GW', minimax: 'MINIMAX-GW' };
821
857
  const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
822
858
  const provBadges = (p, regionOnly, flavor) => {
823
859
  const v = provOf(p);
@@ -942,7 +978,8 @@ function initTheme() {
942
978
  function statusOf(a) {
943
979
  if (!CHECKIN_PRODUCTS.has(a.product)) return { cls: '', text: '', msg: '' }; // ZCode 无每日领取,不显示徽标
944
980
  const intl = a.provider === 'workbuddy-intl';
945
- const today = state.status && state.status.today;
981
+ const byProd = (state.status && state.status.todayByProduct) || {};
982
+ const today = byProd[a.product] || (state.status && state.status.today);
946
983
  const done = state.status && state.status.todayDone && state.status.todayDone[a.id] === today;
947
984
  const r = a.lastResult;
948
985
  const fresh = r && r.at && state.status && (Date.now() - new Date(r.at).getTime() < 26 * 3600e3);
@@ -1104,6 +1141,7 @@ function cardHtml(a) {
1104
1141
  + provBadges(a.provider, true, a.flavor)
1105
1142
  + (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
1106
1143
  + (a.product === 'zcode' && isGatewayCurrent(a) ? '<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1144
+ + (a.product === 'minimax' && isMiniMaxGatewayCurrent(a) ? '<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1107
1145
  + (a.isPat ? '<span class="badge">PAT</span>' : '')
1108
1146
  + (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
1109
1147
  + (st.text ? '<span class="pill ' + st.cls + '" title="' + esc(a.lastResult ? fmtTime(a.lastResult.at) + ' ' + (a.lastResult.message || a.lastResult.status) : '') + '">' + esc(st.text) + '</span>' : '')
@@ -1125,7 +1163,14 @@ function cardHtml(a) {
1125
1163
  + '</div>';
1126
1164
  }
1127
1165
 
1128
- const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
1166
+ const isDone = (a) => {
1167
+ if (!state.status || !state.status.todayDone) return false;
1168
+ const doneDay = state.status.todayDone[a.id];
1169
+ if (!doneDay) return false;
1170
+ const byProd = state.status.todayByProduct || {};
1171
+ const expectedDay = byProd[a.product] || state.status.today;
1172
+ return doneDay === expectedDay;
1173
+ };
1129
1174
  const ofProduct = (prod) => state.accounts.filter((a) => a.product === prod);
1130
1175
  const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : prod === 'catpaw' ? state.status.catpawCurrentUid : prod === 'trae' ? state.status.traeCurrentUid : null);
1131
1176
  /** 接口使用中判定:体验包接口最近一次补全用的账号(lastAccount 记的是 name || uid || id) */
@@ -1133,8 +1178,13 @@ function isGatewayCurrent(a) {
1133
1178
  const last = state.zGatewayStats && state.zGatewayStats.lastAccount;
1134
1179
  return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
1135
1180
  }
1181
+ /** MiniMax 本地接口使用中判定 */
1182
+ function isMiniMaxGatewayCurrent(a) {
1183
+ const last = state.mGatewayStats && state.mGatewayStats.lastAccount;
1184
+ return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
1185
+ }
1136
1186
 
1137
- // 客户端当前登录判定:WorkBuddy / mirasim / 妙手 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
1187
+ // 客户端当前登录判定:WorkBuddy / mirasim / 妙手 / MiniMax 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
1138
1188
  function isClientCurrent(a) {
1139
1189
  const s = state.status;
1140
1190
  if (!s) return false;
@@ -1146,6 +1196,7 @@ function isClientCurrent(a) {
1146
1196
  if (a.product === 'mirasim') return Boolean(a.uid && s.mirasimCurrentUid && s.mirasimCurrentUid === a.uid);
1147
1197
  if (a.product === 'catpaw') return Boolean(a.uid && s.catpawCurrentUid && s.catpawCurrentUid === a.uid);
1148
1198
  if (a.product === 'trae') return Boolean(a.uid && s.traeCurrentUid && s.traeCurrentUid === a.uid);
1199
+ if (a.product === 'minimax') return Boolean(a.uid && s.minimaxCurrentUid && s.minimaxCurrentUid === a.uid);
1149
1200
  if (a.product === 'zcode') {
1150
1201
  const id = s.zcodeCurrentIdentity;
1151
1202
  return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
@@ -1169,6 +1220,7 @@ function creditSum(list) {
1169
1220
  function render() {
1170
1221
  $('z-actions').style.display = 'none'; // 兜底:切到仪表盘 / 10Router 时 renderProd 不跑,先藏起来
1171
1222
  $('t-actions').style.display = 'none';
1223
+ $('m-actions').style.display = 'none';
1172
1224
  for (const p of PRODUCTS) $('cnt-' + p.id).textContent = ofProduct(p.id).length;
1173
1225
  $('cnt-tenrouter').textContent = tr.quotas ? tr.quotas.connections.length : tr.config && tr.config.configured ? '…' : '–';
1174
1226
  if (state.view === 'dash') renderDash();
@@ -1183,6 +1235,7 @@ const EMPTY_TIP = {
1183
1235
  mirasim: '还没有 mirasim 账号。<br>点「添加账号」可读取本机 mirasim 客户端当前登录的账号(本地解密,token 不出本机)。',
1184
1236
  catpaw: '还没有妙手账号。<br>点「添加账号」可读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密凭据,token 不出本机)。',
1185
1237
  trae: '还没有 Trae 账号。<br>点「添加账号 → 本机导入」可读取本机 Trae 客户端当前登录的账号(本地解密 storage.json 凭据,token 不出本机)。多个账号需先在 Trae 里分别登录并逐个导入。',
1238
+ minimax: '还没有 MiniMax 账号。<br>点「添加账号 → 本机导入」可读取本机 MiniMax Code 客户端当前登录的账号,或粘贴 Bearer Token 添加。',
1186
1239
  };
1187
1240
 
1188
1241
  function renderProd() {
@@ -1211,7 +1264,7 @@ function renderProd() {
1211
1264
  $('s-credits').textContent = CREDIT_VIEWS.has(prod)
1212
1265
  ? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
1213
1266
  : c.loading ? '…' : (c.known ? fmtNum(c.sum) : '-');
1214
- const hasRegions = !CREDIT_VIEWS.has(prod) && prod !== 'trae'; // Trae 不分国际 / 国内版
1267
+ const hasRegions = !CREDIT_VIEWS.has(prod) && prod !== 'trae' && prod !== 'minimax'; // Trae / MiniMax 不分国际 / 国内版
1215
1268
  $('region').style.display = hasRegions ? '' : 'none';
1216
1269
  $('region-div').style.display = hasRegions ? '' : 'none';
1217
1270
  document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
@@ -1250,6 +1303,19 @@ function renderProd() {
1250
1303
  $('tt-open').title = tc.running ? 'Trae 客户端运行中,点我唤到前台' : 'Trae 客户端未运行,点我启动';
1251
1304
  }
1252
1305
  }
1306
+ $('m-actions').style.display = prod === 'minimax' ? 'contents' : 'none';
1307
+ if (prod === 'minimax') {
1308
+ const gwOn = state.mGateway === true;
1309
+ $('mt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.mGatewayLan === true ? '·局域网' : '');
1310
+ $('mt-gw').title = 'MiniMax 本地接口:开关、局域网开放与 IP 白名单';
1311
+ const mc = (state.status && state.status.minimaxClient) || {};
1312
+ const canOpen = mc.installed && window.creditdaddy && typeof window.creditdaddy.openMiniMaxClient === 'function';
1313
+ $('mt-open').style.display = canOpen ? '' : 'none';
1314
+ if (canOpen) {
1315
+ $('mt-open').innerHTML = dot(mc.running === true, true) + '打开客户端';
1316
+ $('mt-open').title = mc.running ? 'MiniMax Code 客户端运行中,点我唤到前台' : 'MiniMax Code 客户端未运行,点我启动';
1317
+ }
1318
+ }
1253
1319
  }
1254
1320
 
1255
1321
  function kpi(label, value, sub, labelTitle) {
@@ -1266,7 +1332,7 @@ function renderDash() {
1266
1332
  const failed = signable.filter((a) => statusOf(a).cls === 'fail').length;
1267
1333
  const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
1268
1334
  const byProd = orderedVisibleComps().filter((id) => id !== 'tenrouter').map((id) => prodLabel(id) + ' ' + ofProduct(id).length).join(' · ');
1269
- const creditByProd = ['qoder', 'workbuddy'].filter(compVisible).map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
1335
+ const creditByProd = ['qoder', 'workbuddy', 'minimax'].filter(compVisible).map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
1270
1336
  // 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
1271
1337
  const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
1272
1338
  const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
@@ -1306,11 +1372,18 @@ function renderDash() {
1306
1372
  + (cnList.length && intlList.length ? ' · ' : '')
1307
1373
  + (intlList.length ? intlList.filter(isDone).length + ' / ' + intlList.length + ' 国际' : '')
1308
1374
  + '</span>';
1375
+ } else if (p.id === 'minimax') {
1376
+ const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
1377
+ rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>'
1378
+ + '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1379
+ + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1309
1380
  } else {
1310
1381
  rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>';
1311
1382
  }
1312
- rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1313
- + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1383
+ if (p.id !== 'minimax') {
1384
+ rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1385
+ + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1386
+ }
1314
1387
  } else if (p.id === 'mirasim') {
1315
1388
  const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
1316
1389
  rows = '<span class="lbl">额度体系</span><span class="val dim">5h / 7d 滚动窗口</span>'
@@ -1337,6 +1410,7 @@ function renderDash() {
1337
1410
  : p.id === 'mirasim' && s.mirasimCurrentUid ? '未导入的账号'
1338
1411
  : p.id === 'catpaw' && s.catpawClient && s.catpawClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1339
1412
  : p.id === 'trae' && s.traeClient && s.traeClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1413
+ : p.id === 'minimax' && s.minimaxClient && s.minimaxClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1340
1414
  : (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
1341
1415
  rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
1342
1416
  }
@@ -1409,8 +1483,11 @@ function renderStatus() {
1409
1483
  // 产品相关提示只在对应标签页出现
1410
1484
  const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
1411
1485
  const n = $('notice');
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') {
1486
+ n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || state.view === 'minimax' || (hasIntl && Boolean(s.riskIdentity)));
1487
+ if (state.view === 'minimax') {
1488
+ n.style.display = '';
1489
+ n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领 800~2000 积分 + 400 连签加成。支持自动检测本机 MiniMax Code 凭据与 Bearer Token 导入,自带标准 Anthropic 本地网关。</span></div>';
1490
+ } else if (state.view === 'trae') {
1414
1491
  n.style.display = '';
1415
1492
  n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领约 150 通用积分(7 天有效,多资源包自动归并)。凭据由本机 Trae 客户端解密导入,支持多账号快照一键切换。</span></div>';
1416
1493
  } else if (state.view === 'catpaw') {
@@ -1515,6 +1592,29 @@ document.addEventListener('click', async (e) => {
1515
1592
  } catch (err) { toast(String(err && err.message || err), false); }
1516
1593
  return;
1517
1594
  }
1595
+ if (e.target.closest('#mt-open')) {
1596
+ try {
1597
+ const r = await window.creditdaddy.openMiniMaxClient();
1598
+ if (r && r.ok) toast('已启动 MiniMax Code 客户端');
1599
+ else toast((r && r.error) || '无法打开 MiniMax Code 客户端', false);
1600
+ } catch (err) { toast(String(err && err.message || err), false); }
1601
+ return;
1602
+ }
1603
+ if (e.target.closest('#mt-gw')) {
1604
+ // MiniMax 本地接口弹窗:开关 + 局域网 + 白名单设置
1605
+ $('mgw-on').checked = state.mGateway === true;
1606
+ $('mgw-lan').checked = state.mGatewayLan === true;
1607
+ const allow = Array.isArray(state.mGatewayAllow) ? state.mGatewayAllow : [];
1608
+ $('mgw-allow').value = (allow.length ? allow : (state.mGatewaySuggest ? [state.mGatewaySuggest] : [])).join('\n');
1609
+ const st = state.mGatewayStats;
1610
+ $('mgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/minimax/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
1611
+ + (st && st.lastCallAt
1612
+ ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1613
+ : '<br>尚未有调用');
1614
+ syncMgwLanFields();
1615
+ openModal('m-mgw');
1616
+ return;
1617
+ }
1518
1618
  if (e.target.closest('#zt-ac')) {
1519
1619
  // 自动领取计划弹窗:周期 / 运行时长由用户决定
1520
1620
  $('zac-interval').value = state.zClaimIntervalMin || 2;
@@ -1570,6 +1670,19 @@ async function loadZGateway() {
1570
1670
  state.zGatewayHasCaptcha = d.hasCaptcha === true;
1571
1671
  state.zGatewayStats = d.stats || null;
1572
1672
  if (state.view === 'zcode') { renderStatus(); render(); }
1673
+ applyLogTabs(); // 网关开关变化影响日志标签显隐
1674
+ } catch (e) {}
1675
+ }
1676
+ async function loadMGateway() {
1677
+ try {
1678
+ const d = await api('/api/minimax-gateway');
1679
+ state.mGateway = d.enabled === true;
1680
+ state.mGatewayLan = d.lan === true;
1681
+ state.mGatewayAllow = d.allow || [];
1682
+ state.mGatewaySuggest = d.suggestAllow || '';
1683
+ state.mGatewayStats = d.stats || null;
1684
+ if (state.view === 'minimax') { renderStatus(); render(); }
1685
+ applyLogTabs(); // 网关开关变化影响日志标签显隐
1573
1686
  } catch (e) {}
1574
1687
  }
1575
1688
 
@@ -2080,12 +2193,46 @@ async function loadQuotas(ids) {
2080
2193
  };
2081
2194
  await Promise.all([worker(), worker(), worker()]);
2082
2195
  }
2196
+ let logTabView = null; // 当前日志标签所属的视图:切到别的视图时才重置跟随,同一视图内保留手动切换
2197
+ function followLogTab() {
2198
+ if (logTabView !== state.view) {
2199
+ state.logTab = (state.view !== 'dash' && GW_LOG_TAG[state.view]) ? state.view : 'run';
2200
+ logTabView = state.view;
2201
+ }
2202
+ }
2083
2203
  async function loadLogs() {
2204
+ followLogTab();
2205
+ applyLogTabs();
2206
+ await loadCurrentLogs();
2207
+ }
2208
+ function applyLogTabs() {
2209
+ // 网关开关关闭时,隐藏对应日志标签(无该网关时也看不到)
2210
+ document.querySelectorAll('#log-tabs button').forEach((b) => {
2211
+ const t = b.dataset.logTab;
2212
+ let show = true;
2213
+ if (t === 'zcode') show = Boolean(state.zGateway);
2214
+ if (t === 'minimax') show = Boolean(state.mGateway);
2215
+ b.style.display = show ? '' : 'none';
2216
+ b.classList.toggle('active', t === state.logTab);
2217
+ });
2218
+ }
2219
+ async function loadCurrentLogs() {
2220
+ const el = $('logs');
2221
+ if (!el) return;
2222
+ let logs = [];
2084
2223
  try {
2085
- const data = await api('/api/logs');
2086
- const el = $('logs');
2224
+ if (state.logTab === 'run') {
2225
+ const data = await api('/api/logs');
2226
+ logs = data.logs || [];
2227
+ } else {
2228
+ const data = await api('/api/logs?tag=' + encodeURIComponent(GW_LOG_TAG[state.logTab]));
2229
+ logs = data.logs || [];
2230
+ }
2087
2231
  const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
2088
- el.innerHTML = data.logs.length ? data.logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] ' + esc(l.tag) + ' · ' + esc(l.msg) + '</div>').join('') : '(暂无)';
2232
+ const withTag = state.logTab === 'run';
2233
+ el.innerHTML = logs.length
2234
+ ? logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] ' + (withTag ? esc(l.tag) + ' · ' : '') + esc(l.msg) + '</div>').join('')
2235
+ : '(暂无)';
2089
2236
  if (atBottom) el.scrollTop = el.scrollHeight;
2090
2237
  } catch (e) {}
2091
2238
  }
@@ -2498,6 +2645,27 @@ $('zgw-ok').addEventListener('click', async () => {
2498
2645
  } catch (err) { toast(err.message, false); }
2499
2646
  });
2500
2647
 
2648
+ // MiniMax 本地接口:开关 + 局域网开放 + 免密 IP 白名单一次保存
2649
+ function syncMgwLanFields() {
2650
+ const off = !$('mgw-on').checked;
2651
+ $('mgw-lan-fields').querySelectorAll('input, textarea').forEach((el) => { el.disabled = off; });
2652
+ $('mgw-lan-fields').style.opacity = off ? '.45' : '';
2653
+ }
2654
+ $('mgw-on').addEventListener('change', syncMgwLanFields);
2655
+ $('mgw-ok').addEventListener('click', async () => {
2656
+ const allow = $('mgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
2657
+ try {
2658
+ const r = await api('/api/minimax-gateway', { method: 'PUT', json: { enabled: $('mgw-on').checked, lan: $('mgw-lan').checked, allow } });
2659
+ state.mGateway = r.enabled === true;
2660
+ state.mGatewayLan = r.lan === true;
2661
+ state.mGatewayAllow = r.allow || [];
2662
+ renderStatus(); render();
2663
+ closeModal('m-mgw');
2664
+ toast('MiniMax 接口:' + (r.enabled ? '已开启' : '已关闭')
2665
+ + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : '') : ''));
2666
+ } catch (err) { toast(err.message, false); }
2667
+ });
2668
+
2501
2669
  // ZCode 网络出口:顺序 + 代理地址保存
2502
2670
  $('znet-ok').addEventListener('click', async () => {
2503
2671
  try {
@@ -2529,10 +2697,11 @@ const LOCAL_HINT = {
2529
2697
  mirasim: '读取本机 mirasim 客户端当前登录的账号(本地解密 ~/.mirasim/setting.json),token 不出本机。',
2530
2698
  catpaw: '读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密 %APPDATA%\\catpaw-moon\\catx-credential.json),token 不出本机。',
2531
2699
  trae: '读取本机 Trae 客户端当前登录的账号(本地解密 <客户端数据目录>\\User\\globalStorage\\storage.json 的 tc 信封),token 不出本机。',
2700
+ minimax: '读取本机 MiniMax Code 客户端当前登录的账号(~/.minimax/auth/.../auth.json),token 不出本机。',
2532
2701
  };
2533
2702
  function openAdd(product) {
2534
2703
  state.addProduct = product || '';
2535
- // Trae 凭据只能从本机客户端 storage.json 解密:不支持浏览器登录 / 粘贴 Token
2704
+ // 各产品支持的添加方式:MiniMax 三种都支持(浏览器登录 = 官方设备码授权,独立凭据链不与本机客户端互斥)
2536
2705
  const tabShown = { local: true, device: product !== 'mirasim' && product !== 'catpaw' && product !== 'trae', token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' && product !== 'trae' };
2537
2706
  document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
2538
2707
  $('local-hint').textContent = LOCAL_HINT[state.addProduct];
@@ -2562,7 +2731,7 @@ function openAdd(product) {
2562
2731
  async function localScan() {
2563
2732
  const box = $('scan-result');
2564
2733
  $('scan-status').textContent = '扫描中(最多约 10 秒)…';
2565
- box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae)…</div>';
2734
+ box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax)…</div>';
2566
2735
  try {
2567
2736
  const r = await api('/api/local/scan', { method: 'POST' });
2568
2737
  $('scan-status').textContent = '';
@@ -2583,9 +2752,9 @@ async function localScan() {
2583
2752
  html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
2584
2753
  }
2585
2754
  if (!cands.length) {
2586
- const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae';
2587
- const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' || state.addProduct === 'catpaw' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
2588
- html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录、Trae 数据目录'
2755
+ const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax';
2756
+ const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' || state.addProduct === 'catpaw' || state.addProduct === 'trae' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
2757
+ html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录、Trae 数据目录、MiniMax 数据目录'
2589
2758
  + (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
2590
2759
  }
2591
2760
  box.innerHTML = html;
@@ -3022,7 +3191,7 @@ $('btn-export-dash').onclick = () => { $('ex-provider').value = ''; openModal('m
3022
3191
  $('btn-export').onclick = () => {
3023
3192
  const v = state.view;
3024
3193
  // ZCode 是单一 provider,直接按 provider 选;其余产品页按产品线(p:);仪表盘 / 10Router 导出全部
3025
- $('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw' || v === 'trae') ? 'p:' + v : '';
3194
+ $('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw' || v === 'trae' || v === 'minimax') ? 'p:' + v : '';
3026
3195
  openModal('m-export');
3027
3196
  };
3028
3197
  $('btn-do-export').onclick = doExport;
@@ -3050,7 +3219,11 @@ function applyView() {
3050
3219
  $('view-prod').classList.toggle('active', state.view !== 'dash' && state.view !== 'tenrouter');
3051
3220
  $('view-10r').classList.toggle('active', state.view === 'tenrouter');
3052
3221
  if (state.view === 'tenrouter') loadZGateway();
3222
+ if (state.view === 'minimax') loadMGateway();
3053
3223
  if (state.view === 'tenrouter' && tr.config && tr.config.configured && !tr.quotas && !tr.loading) loadTrQuotas();
3224
+ followLogTab();
3225
+ applyLogTabs();
3226
+ loadCurrentLogs();
3054
3227
  render();
3055
3228
  renderStatus();
3056
3229
  }
@@ -3074,15 +3247,26 @@ $('region').addEventListener('click', (e) => {
3074
3247
  renderStatus();
3075
3248
  });
3076
3249
  $('f-provider').addEventListener('change', () => {
3077
- $('f-token').placeholder = $('f-provider').value.startsWith('workbuddy')
3250
+ $('f-token').placeholder = $('f-provider').value === 'minimax'
3251
+ ? 'MiniMax OAuth Bearer Token(mmoat_…)'
3252
+ : $('f-provider').value.startsWith('workbuddy')
3078
3253
  ? 'WorkBuddy / CodeBuddy 登录 token(eyJ…,可在 CodeBuddyExtension 的 auth/*.info 中找到)'
3079
3254
  : 'dt-… / jt-… 或 PAT(pt-…,自动兑换)';
3080
3255
  });
3081
3256
  $('btn-logs').onclick = () => {
3082
3257
  const c = $('log-card').classList.toggle('collapsed');
3083
3258
  $('btn-logs').textContent = c ? '展开' : '收起';
3084
- if (!c) { loadLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
3259
+ if (!c) { loadCurrentLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
3085
3260
  };
3261
+ $('log-tabs').addEventListener('click', (e) => {
3262
+ const b = e.target.closest('button[data-log-tab]');
3263
+ if (!b) return;
3264
+ state.logTab = b.dataset.logTab;
3265
+ logTabView = state.view; // 手动切换后,本视图内不再被 10s 轮询重置
3266
+ applyLogTabs();
3267
+ loadCurrentLogs();
3268
+ $('logs').scrollTop = $('logs').scrollHeight;
3269
+ });
3086
3270
 
3087
3271
  // ── 仪表盘底部两卡:默认收起,点标题展开,展开状态存本地 ──
3088
3272
  const DASH_FOLD_KEY = 'qd_dash_fold';
@@ -3115,6 +3299,7 @@ applyView();
3115
3299
  loadSettings().then(renderCompMenu);
3116
3300
  loadZNet();
3117
3301
  loadZGateway();
3302
+ loadMGateway();
3118
3303
  loadTrConfig().then(() => loadTrQuotas());
3119
3304
  setInterval(() => loadTrQuotas(), 5 * 60 * 1000);
3120
3305
  // 账号余额(ZCode 剩余 token 等)自动刷新:可见时每 2 分钟一轮;从托盘/后台切回时若超过 1 分钟没刷过立即补一轮
@@ -3124,7 +3309,7 @@ applyView();
3124
3309
  });
3125
3310
  loadLogs();
3126
3311
  setInterval(loadLogs, 10000);
3127
- setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); render(); }, 30000);
3312
+ setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); await loadMGateway(); render(); }, 30000);
3128
3313
  }
3129
3314
  // 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
3130
3315
  api('/api/status').then(start).catch((e) => toast(e.code === 'PANEL_KEY'
package/src/providers.js CHANGED
@@ -15,6 +15,7 @@ import { fetchZcodeQuota } from './zcodeClient.js';
15
15
  import { fetchMirasimQuota, fetchMirasimProfile } from './mirasimClient.js';
16
16
  import { fetchCatpawQuota, fetchCatpawProfile } from './catpawClient.js';
17
17
  import { fetchTraeQuota, checkinTrae, verifyTraeAccount } from './traeClient.js';
18
+ import { fetchMiniMaxQuota, checkinMiniMax, verifyMiniMaxAccount } from './minimaxClient.js';
18
19
  /** 从 userinfo 响应中挑一个可读的显示名 */
19
20
  export function displayNameFrom(ui) {
20
21
  const pick = [ui?.nickname, ui?.name, ui?.username, ui?.email]
@@ -183,8 +184,33 @@ const PRODUCTS = {
183
184
  quota: (account) => fetchTraeQuota(account),
184
185
  verify: (account) => verifyTraeAccount(account),
185
186
  },
187
+ minimax: {
188
+ label: 'MiniMax',
189
+ // token 约 1 小时过期:checkin / quota 内部 401 时用 refreshToken 自动续期(withMiniMaxAuth)。
190
+ // 本机导入账号刷新前会重读 ~/.minimax auth.json 对齐客户端最新 refreshToken;
191
+ // ctx.onRefresh 回写账号库后,这里再安全回写 auth.json(仅客户端未运行时,见 minimaxLocal.writeMiniMaxAuth)。
192
+ // device 登录账号自持独立凭据链(无 meta.authRecordKey),writeMiniMaxAuth 对其 no-op,不与本机客户端互斥。
193
+ checkin: (account, ctx = {}) => checkinMiniMax(account, wrapMiniMaxRefresh(account, ctx)),
194
+ quota: (account, ctx = {}) => fetchMiniMaxQuota(account, wrapMiniMaxRefresh(account, ctx)),
195
+ verify: (account) => verifyMiniMaxAccount(account),
196
+ },
186
197
  };
187
198
 
199
+ /** 包裹 MiniMax 的 ctx.onRefresh:回写账号库后,安全同步新凭据回本机 auth.json(仅本机导入账号且客户端未运行时) */
200
+ function wrapMiniMaxRefresh(account, ctx = {}) {
201
+ return {
202
+ ...ctx,
203
+ onRefresh: async (creds) => {
204
+ await ctx.onRefresh?.(creds);
205
+ try {
206
+ const { writeMiniMaxAuth } = await import('./minimaxLocal.js');
207
+ const synced = writeMiniMaxAuth({ ...account, ...creds });
208
+ if (synced) ctx.log?.('已同步新 token 到 MiniMax 客户端');
209
+ } catch {}
210
+ },
211
+ };
212
+ }
213
+
188
214
  export function productImpl(provider) {
189
215
  return PRODUCTS[productOf(provider)];
190
216
  }
package/src/qoderApp.js CHANGED
@@ -19,6 +19,7 @@ import path from 'node:path';
19
19
  import crypto from 'node:crypto';
20
20
  import { spawn, execFile, execFileSync } from 'node:child_process';
21
21
  import { dataDir } from './store.js';
22
+ import { anyProcRunning } from './winProc.js';
22
23
  import { installedUmid, CLI_RISK_ENV } from './qoderUmid.js';
23
24
 
24
25
  const RISK_TTL_MS = 50 * 60 * 1000; // 客户端每 60±5 分钟刷新一次,这里保守取 50 分钟
@@ -460,15 +461,8 @@ const QODER_EXES = ['Qoder.exe', 'Qoder CN.exe'];
460
461
 
461
462
  /** Qoder IDE(国际版 / 国内版)是否在运行 */
462
463
  export function qoderRunning() {
463
- if (process.platform !== 'win32') return false;
464
- for (const exe of QODER_EXES) {
465
- try {
466
- const out = execFileSync('tasklist.exe', ['/FI', `IMAGENAME eq ${exe}`, '/NH'],
467
- { windowsHide: true, encoding: 'buffer', timeout: 8000 }).toString('latin1');
468
- if (!/No Tasks|没有/i.test(out)) return true;
469
- } catch { /* 查不到就换下一名 */ }
470
- }
471
- return false;
464
+ // 走 winProc:这里曾经用 latin1 解 tasklist,中文 Windows 上恒为 true
465
+ return anyProcRunning(QODER_EXES);
472
466
  }
473
467
 
474
468
  const nap = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);