creditdaddy 1.6.2 → 1.6.5
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/README.md +1 -1
- package/bin/creditdaddy.js +22 -0
- package/build.json +3 -3
- package/package.json +2 -2
- package/src/checkin.js +4 -0
- package/src/constants.js +3 -1
- package/src/cookieSites.js +8 -1
- package/src/daemon.js +2251 -2062
- package/src/doubaoClient.js +0 -1
- package/src/i18n/en.json +94 -1
- package/src/i18n/ja.json +94 -1
- package/src/i18n/ko.json +94 -1
- package/src/i18n/zh-TW.json +94 -1
- package/src/loomyAuth.js +230 -0
- package/src/loomyClient.js +306 -0
- package/src/loomyLocal.js +188 -0
- package/src/minimaxGateway.js +61 -11
- package/src/panel.html +350 -59
- package/src/providers.js +9 -0
- package/src/qoderClient.js +15 -6
- package/src/quotaRefresh.js +5 -0
- package/src/store.js +9 -2
- package/src/tenrouter.js +37 -14
- package/src/tenrouterAccounts.js +1 -0
- package/src/traeClient.js +18 -5
- package/src/traeGateway.js +69 -13
- package/src/transfer.js +3 -0
- package/src/usageSync.js +11 -1
- package/src/zcodeAutoClaim.js +15 -2
- package/src/zcodeClient.js +40 -7
- package/src/zcodeGateway.js +32 -2
package/src/panel.html
CHANGED
|
@@ -197,6 +197,8 @@
|
|
|
197
197
|
html[data-theme="dark"] .badge.prod.trae { color: #6ee7b7; }
|
|
198
198
|
.badge.prod.minimax { color: #e11d48; background: rgba(244,63,94,.14); }
|
|
199
199
|
html[data-theme="dark"] .badge.prod.minimax { color: #fda4af; }
|
|
200
|
+
.badge.prod.loomy { color: #0284c7; background: rgba(14,165,233,.14); }
|
|
201
|
+
html[data-theme="dark"] .badge.prod.loomy { color: #38bdf8; }
|
|
200
202
|
.seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
|
|
201
203
|
.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; }
|
|
202
204
|
.seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
@@ -341,7 +343,7 @@
|
|
|
341
343
|
.prod-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
342
344
|
.prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
|
|
343
345
|
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
344
|
-
.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; }
|
|
346
|
+
.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; } .dot.loomy { background: #0284c7; }
|
|
345
347
|
.prod-go { color: var(--text-3); font-size: 12px; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; }
|
|
346
348
|
.prod-go svg { width: 12px; height: 12px; transition: transform .15s; }
|
|
347
349
|
.prodcard:hover .prod-go { color: var(--text); }
|
|
@@ -555,6 +557,7 @@
|
|
|
555
557
|
<button data-view="catpaw"><span data-i18n="妙手">妙手</span><span class="cnt" id="cnt-catpaw">0</span></button>
|
|
556
558
|
<button data-view="trae">Trae<span class="cnt" id="cnt-trae">0</span></button>
|
|
557
559
|
<button data-view="minimax">MiniMax<span class="cnt" id="cnt-minimax">0</span></button>
|
|
560
|
+
<button data-view="loomy">Loomy<span class="cnt" id="cnt-loomy">0</span></button>
|
|
558
561
|
<button data-view="free"><span data-i18n="网页会话">网页会话</span><span class="cnt" id="cnt-free">0</span></button>
|
|
559
562
|
<button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
|
|
560
563
|
</nav>
|
|
@@ -612,6 +615,9 @@
|
|
|
612
615
|
<button class="btn" id="mt-gw"></button>
|
|
613
616
|
<button class="btn" id="mt-open" style="display:none"></button>
|
|
614
617
|
</span>
|
|
618
|
+
<span id="l-actions" style="display:none">
|
|
619
|
+
<button class="btn" id="lt-open" style="display:none"></button>
|
|
620
|
+
</span>
|
|
615
621
|
<span id="w-actions" style="display:none">
|
|
616
622
|
<button class="btn" id="wt-act" title="领取临时活动(额外补偿 / 首月赠礼),用已存凭据批量领,免逐个网页点击"></button>
|
|
617
623
|
</span>
|
|
@@ -680,6 +686,7 @@
|
|
|
680
686
|
<div class="modal-title"><span data-i18n="添加账号">添加账号</span><button class="close" data-close>✕</button></div>
|
|
681
687
|
<div class="tabs">
|
|
682
688
|
<button class="tab active" data-tab="local" data-i18n="本机导入">本机导入</button>
|
|
689
|
+
<button class="tab" data-tab="sms" data-i18n="手机登录" id="tab-sms">手机登录</button>
|
|
683
690
|
<button class="tab" data-tab="device" data-i18n="浏览器登录">浏览器登录</button>
|
|
684
691
|
<button class="tab" data-tab="token" data-i18n="粘贴 Token">粘贴 Token</button>
|
|
685
692
|
</div>
|
|
@@ -690,6 +697,24 @@
|
|
|
690
697
|
<div class="box" id="scan-result"><div class="hint" data-i18n="点击「重新扫描」">点击「重新扫描」</div></div>
|
|
691
698
|
</div>
|
|
692
699
|
|
|
700
|
+
<div class="pane" data-pane="sms">
|
|
701
|
+
<div class="hint" id="sms-hint" style="margin-bottom:10px">讯飞 Loomy 手机号直登:无需打开桌面客户端,输入手机号获取验证码,直接换取 14 天有效 Session 并自动存入账号库。</div>
|
|
702
|
+
<div class="field">
|
|
703
|
+
<label>手机号</label>
|
|
704
|
+
<div style="display:flex;gap:8px">
|
|
705
|
+
<input class="input" id="loomy-phone" placeholder="请输入 11 位手机号" maxlength="11">
|
|
706
|
+
<button class="btn" id="btn-loomy-sms" style="white-space:nowrap;width:120px">获取验证码</button>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
<div class="field" id="loomy-code-field" style="display:none">
|
|
710
|
+
<label>短信验证码</label>
|
|
711
|
+
<input class="input" id="loomy-code" placeholder="请输入 6 位短信验证码" maxlength="6">
|
|
712
|
+
</div>
|
|
713
|
+
<div class="modal-actions" id="loomy-login-actions" style="display:none">
|
|
714
|
+
<button class="btn primary" id="btn-loomy-login">登录并导入</button>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
693
718
|
<div class="pane" data-pane="device">
|
|
694
719
|
<div class="hint" id="dev-hint" style="margin-bottom:10px"></div>
|
|
695
720
|
<div class="field">
|
|
@@ -711,7 +736,7 @@
|
|
|
711
736
|
<div class="pane" data-pane="token">
|
|
712
737
|
<div class="field">
|
|
713
738
|
<label data-i18n="版本">版本</label>
|
|
714
|
-
<select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy" data-i18n="WorkBuddy 国内版">WorkBuddy 国内版</option><option value="workbuddy-intl" data-i18n="WorkBuddy 国际版">WorkBuddy 国际版</option><option value="minimax">MiniMax</option></select>
|
|
739
|
+
<select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy" data-i18n="WorkBuddy 国内版">WorkBuddy 国内版</option><option value="workbuddy-intl" data-i18n="WorkBuddy 国际版">WorkBuddy 国际版</option><option value="minimax">MiniMax</option><option value="loomy">讯飞 Loomy</option></select>
|
|
715
740
|
</div>
|
|
716
741
|
<div class="field"><label data-i18n="备注名(可选)">备注名(可选)</label><input class="input" id="f-name" placeholder="留空则自动读取昵称"></div>
|
|
717
742
|
<div class="field"><label>Token</label><input class="input mono" id="f-token" placeholder="dt-… / jt-… 或 PAT(pt-…,自动兑换)"></div>
|
|
@@ -726,7 +751,7 @@
|
|
|
726
751
|
<div class="modal-title"><span data-i18n="导出账号">导出账号</span><button class="close" data-close>✕</button></div>
|
|
727
752
|
<div class="field">
|
|
728
753
|
<label data-i18n="范围">范围</label>
|
|
729
|
-
<select class="input" id="ex-provider"><option value="" data-i18n="全部账号">全部账号</option><option value="p:qoder" data-i18n="Qoder(全部)">Qoder(全部)</option><option value="qoder" data-i18n="仅 Qoder 国际版">仅 Qoder 国际版</option><option value="qoder-cn" data-i18n="仅 Qoder 国内版">仅 Qoder 国内版</option><option value="p:workbuddy" data-i18n="WorkBuddy(全部)">WorkBuddy(全部)</option><option value="workbuddy" data-i18n="仅 WorkBuddy 国内版">仅 WorkBuddy 国内版</option><option value="workbuddy-intl" data-i18n="仅 WorkBuddy 国际版">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option><option value="p:mirasim">mirasim</option><option value="p:catpaw" data-i18n="妙手">妙手</option><option value="p:trae">Trae</option><option value="p:minimax">MiniMax</option></select>
|
|
754
|
+
<select class="input" id="ex-provider"><option value="" data-i18n="全部账号">全部账号</option><option value="p:qoder" data-i18n="Qoder(全部)">Qoder(全部)</option><option value="qoder" data-i18n="仅 Qoder 国际版">仅 Qoder 国际版</option><option value="qoder-cn" data-i18n="仅 Qoder 国内版">仅 Qoder 国内版</option><option value="p:workbuddy" data-i18n="WorkBuddy(全部)">WorkBuddy(全部)</option><option value="workbuddy" data-i18n="仅 WorkBuddy 国内版">仅 WorkBuddy 国内版</option><option value="workbuddy-intl" data-i18n="仅 WorkBuddy 国际版">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option><option value="p:mirasim">mirasim</option><option value="p:catpaw" data-i18n="妙手">妙手</option><option value="p:trae">Trae</option><option value="p:minimax">MiniMax</option><option value="p:loomy">Loomy</option></select>
|
|
730
755
|
</div>
|
|
731
756
|
<div class="field">
|
|
732
757
|
<label data-i18n="加密口令(必填)">加密口令(必填)</label>
|
|
@@ -1033,7 +1058,7 @@ const I = {
|
|
|
1033
1058
|
|
|
1034
1059
|
const $ = (id) => document.getElementById(id);
|
|
1035
1060
|
const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run', logLevel: 'all', locale: 'zh-CN' };
|
|
1036
|
-
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'free', 'tenrouter'];
|
|
1061
|
+
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'loomy', 'free', 'tenrouter'];
|
|
1037
1062
|
// 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
|
|
1038
1063
|
const tr = { config: null, quotas: null, error: null, loading: false, syncing: false, accountsSyncing: false };
|
|
1039
1064
|
const PRODUCTS = [
|
|
@@ -1044,6 +1069,7 @@ const PRODUCTS = [
|
|
|
1044
1069
|
{ id: 'catpaw', label: '妙手' },
|
|
1045
1070
|
{ id: 'trae', label: 'Trae' },
|
|
1046
1071
|
{ id: 'minimax', label: 'MiniMax' },
|
|
1072
|
+
{ id: 'loomy', label: 'Loomy' },
|
|
1047
1073
|
];
|
|
1048
1074
|
const prodLabel = (id) => T((PRODUCTS.find((x) => x.id === id) || { label: id }).label);
|
|
1049
1075
|
|
|
@@ -1056,6 +1082,7 @@ const COMPONENTS = [
|
|
|
1056
1082
|
{ id: 'catpaw', label: '妙手' },
|
|
1057
1083
|
{ id: 'trae', label: 'Trae' },
|
|
1058
1084
|
{ id: 'minimax', label: 'MiniMax' },
|
|
1085
|
+
{ id: 'loomy', label: 'Loomy' },
|
|
1059
1086
|
{ id: 'free', label: '网页会话' },
|
|
1060
1087
|
{ id: 'tenrouter', label: '10Router' },
|
|
1061
1088
|
];
|
|
@@ -1182,12 +1209,13 @@ const PROV = {
|
|
|
1182
1209
|
catpaw: { prod: 'catpaw', prodLabel: '妙手', region: '', cls: '' },
|
|
1183
1210
|
trae: { prod: 'trae', prodLabel: 'Trae', region: '', cls: '' },
|
|
1184
1211
|
minimax: { prod: 'minimax', prodLabel: 'MiniMax', region: '', cls: '' },
|
|
1212
|
+
loomy: { prod: 'loomy', prodLabel: 'Loomy', region: '', cls: '' },
|
|
1185
1213
|
};
|
|
1186
1214
|
// 支持每日签到的产品(ZCode / mirasim / 妙手 额度靠套餐或滚动窗口发放,不做签到)
|
|
1187
1215
|
const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy', 'trae', 'minimax']);
|
|
1188
|
-
const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae', minimax: 'MiniMax' };
|
|
1216
|
+
const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae', minimax: 'MiniMax', loomy: 'Loomy' };
|
|
1189
1217
|
// 额度单位为 Credits/时长而非「积分」口径的产品(不计入剩余积分合计)
|
|
1190
|
-
const CREDIT_VIEWS = new Set(['zcode', 'mirasim', 'catpaw']);
|
|
1218
|
+
const CREDIT_VIEWS = new Set(['zcode', 'mirasim', 'catpaw', 'loomy']);
|
|
1191
1219
|
// 网关调用日志:tag → 所属产品页(无网关的产品不展示日志卡)
|
|
1192
1220
|
const GW_LOG_TAG = { zcode: 'ZCODE-GW', minimax: 'MINIMAX-GW', trae: 'TRAE-GW' };
|
|
1193
1221
|
const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
|
|
@@ -1260,6 +1288,21 @@ function promptKey(invalid) {
|
|
|
1260
1288
|
}
|
|
1261
1289
|
return keyPrompt;
|
|
1262
1290
|
}
|
|
1291
|
+
// 自动适配反向代理/统一网关子路径(例如 fnOS 统一网关 /app/creditdaddy/):
|
|
1292
|
+
// 当页面在子路径下渲染时,自动给 /api/* 等绝对路径补齐前缀,避免越过前缀直接请求根路径返回 404
|
|
1293
|
+
const apiPrefix = (() => {
|
|
1294
|
+
const p = (typeof window !== 'undefined' && window.location && window.location.pathname) || '';
|
|
1295
|
+
const dir = p.replace(/\/[^/]*\.[^/]+$/, '');
|
|
1296
|
+
const clean = dir.replace(/\/+$/, '');
|
|
1297
|
+
return clean && clean !== '/' ? clean : '';
|
|
1298
|
+
})();
|
|
1299
|
+
function resolveApiPath(url) {
|
|
1300
|
+
if (typeof url !== 'string' || !url.startsWith('/')) return url;
|
|
1301
|
+
if (apiPrefix && !url.startsWith(apiPrefix + '/')) {
|
|
1302
|
+
return apiPrefix + url;
|
|
1303
|
+
}
|
|
1304
|
+
return url;
|
|
1305
|
+
}
|
|
1263
1306
|
async function api(p, opts = {}, attempt = 0) {
|
|
1264
1307
|
const o = Object.assign({}, opts, { headers: Object.assign({}, opts.headers, { 'x-qd-key': getKey() }) });
|
|
1265
1308
|
if (opts.json !== undefined) {
|
|
@@ -1268,7 +1311,18 @@ async function api(p, opts = {}, attempt = 0) {
|
|
|
1268
1311
|
o.body = JSON.stringify(opts.json);
|
|
1269
1312
|
}
|
|
1270
1313
|
delete o.json;
|
|
1271
|
-
|
|
1314
|
+
// 挂钟超时:daemon 卡死 / 休眠唤醒后半开连接时 fetch 永不落定,30s 轮询链会无限堆积。
|
|
1315
|
+
// 慢操作(全量签到、10R 推送/同步的同步长请求)用 opts.timeoutMs 放宽
|
|
1316
|
+
const timeoutMs = Number(opts.timeoutMs) || 120_000;
|
|
1317
|
+
let res;
|
|
1318
|
+
try {
|
|
1319
|
+
res = await fetch(resolveApiPath(p), { ...o, signal: opts.signal || AbortSignal.timeout(timeoutMs) });
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
if (e && (e.name === 'TimeoutError' || /timeout|timed out/i.test(e.message || ''))) {
|
|
1322
|
+
throw new Error(T('请求超时') + '(' + Math.round(timeoutMs / 1000) + 's): ' + p);
|
|
1323
|
+
}
|
|
1324
|
+
throw e;
|
|
1325
|
+
}
|
|
1272
1326
|
const data = await res.json().catch(() => ({}));
|
|
1273
1327
|
// 密码缺失 / 不正确:弹框要密码,输入后自动重试这次请求(不再只报一句错误)
|
|
1274
1328
|
if (res.status === 401 && data.code === 'PANEL_KEY' && attempt < 3) {
|
|
@@ -1699,6 +1753,7 @@ function cardHtml(a) {
|
|
|
1699
1753
|
+ (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>' : '')
|
|
1700
1754
|
+ '</div><div class="ops">'
|
|
1701
1755
|
+ (a.product === 'zcode' ? T('<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">') + I.gift + '</button>' : '')
|
|
1756
|
+
+ (a.product === 'loomy' ? T('<button class="icon-btn" data-act="invite" title="填写邀请码(绑定后 +5000 永久积分,每号仅一次)">') + I.gift + '</button>' : '')
|
|
1702
1757
|
+ (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>' : '')
|
|
1703
1758
|
+ (a.qoderWebDead ? '<button class="icon-btn" data-act="webrenew" title="' + T('网页会话已失效——打开登录窗口重新授权,关闭窗口时自动补 cookie(API token 不受影响)') + '">' + I.swap + '</button>' : '')
|
|
1704
1759
|
+ (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>' : '')
|
|
@@ -1763,6 +1818,8 @@ function isClientCurrent(a) {
|
|
|
1763
1818
|
if (a.product === 'catpaw') return Boolean(a.uid && s.catpawCurrentUid && s.catpawCurrentUid === a.uid);
|
|
1764
1819
|
if (a.product === 'trae') return Boolean(a.uid && s.traeCurrentUid && s.traeCurrentUid === a.uid);
|
|
1765
1820
|
if (a.product === 'minimax') return Boolean(a.uid && s.minimaxCurrentUid && s.minimaxCurrentUid === a.uid);
|
|
1821
|
+
// Loomy 按 uid 比对:publicAccount 不下发 token(面板侧 a.token 恒为空,按 token 比对徽章永远不亮)
|
|
1822
|
+
if (a.product === 'loomy') return Boolean(a.uid && s.loomyCurrentUid && String(s.loomyCurrentUid) === String(a.uid));
|
|
1766
1823
|
if (a.product === 'zcode') {
|
|
1767
1824
|
const id = s.zcodeCurrentIdentity;
|
|
1768
1825
|
return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
|
|
@@ -1806,6 +1863,7 @@ const EMPTY_TIP = {
|
|
|
1806
1863
|
catpaw: '还没有妙手账号。<br>点「添加账号」可读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密凭据,token 不出本机)。',
|
|
1807
1864
|
trae: '还没有 Trae 账号。<br>点「添加账号 → 本机导入」可读取本机 Trae 客户端当前登录的账号(本地解密 storage.json 凭据,token 不出本机)。多个账号需先在 Trae 里分别登录并逐个导入。',
|
|
1808
1865
|
minimax: '还没有 MiniMax 账号。<br>点「添加账号 → 本机导入」可读取本机 MiniMax Code 客户端当前登录的账号,或用浏览器登录 / 粘贴 Bearer Token 添加。',
|
|
1866
|
+
loomy: '还没有 Loomy 账号。<br>点「添加账号 → 本机导入」可读取本机 Loomy 客户端当前登录的账号(Token 有效期约 14 天,免客户端运行),支持多账号保存切换,并可一键同步给 10Router 网关。',
|
|
1809
1867
|
};
|
|
1810
1868
|
|
|
1811
1869
|
function renderProd() {
|
|
@@ -1838,7 +1896,8 @@ function renderProd() {
|
|
|
1838
1896
|
document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
|
|
1839
1897
|
$('btn-checkin').style.display = canCheckin ? '' : 'none';
|
|
1840
1898
|
if (!$('btn-checkin').disabled) {
|
|
1841
|
-
|
|
1899
|
+
// 已在对应产品页内,不再重复品牌名(如「领取全部 WorkBuddy」)——直接「领取全部」
|
|
1900
|
+
$('btn-checkin').innerHTML = I.play + T('领取全部');
|
|
1842
1901
|
}
|
|
1843
1902
|
// 顶栏产品专属入口:ZCode 设置组、Trae 打开客户端,带状态灯(运行中绿点呼吸)
|
|
1844
1903
|
const dot = (on, pulse) => '<span class="dot' + (on ? ' on' : '') + (on && pulse ? ' pulse' : '') + '"></span>';
|
|
@@ -1884,6 +1943,16 @@ function renderProd() {
|
|
|
1884
1943
|
$('mt-open').title = mc.running ? T('MiniMax Code 客户端运行中,点我唤到前台') : T('MiniMax Code 客户端未运行,点我启动');
|
|
1885
1944
|
}
|
|
1886
1945
|
}
|
|
1946
|
+
$('l-actions').style.display = prod === 'loomy' ? 'contents' : 'none';
|
|
1947
|
+
if (prod === 'loomy') {
|
|
1948
|
+
const lc = (state.status && state.status.loomyClient) || {};
|
|
1949
|
+
const canOpen = lc.installed && window.creditdaddy && typeof window.creditdaddy.openLoomyClient === 'function';
|
|
1950
|
+
$('lt-open').style.display = canOpen ? '' : 'none';
|
|
1951
|
+
if (canOpen) {
|
|
1952
|
+
$('lt-open').innerHTML = dot(lc.running === true, true) + T('打开客户端');
|
|
1953
|
+
$('lt-open').title = lc.running ? T('Loomy 客户端运行中,点我唤到前台') : T('Loomy 客户端未运行,点我启动');
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1887
1956
|
// WorkBuddy 临时活动(额外补偿 / 首月赠礼)批量领取入口——用已存凭据,免逐个网页点击
|
|
1888
1957
|
$('w-actions').style.display = prod === 'workbuddy' ? 'contents' : 'none';
|
|
1889
1958
|
if (prod === 'workbuddy') {
|
|
@@ -1956,6 +2025,18 @@ function renderDash() {
|
|
|
1956
2025
|
const credits = withPlan.reduce((s, a) => s + (Number(state.quotas[a.id].remaining) || 0), 0);
|
|
1957
2026
|
rows = T('<span class="lbl">额度体系</span><span class="val dim">Credits 余额 + 套餐版本</span>')
|
|
1958
2027
|
+ (withPlan.length ? T('<span class="lbl">可用额度</span><span class="val">') + fmtNum(credits) + T(' Credits(') + esc(withPlan.map((a) => state.quotas[a.id].plan).join(' / ')) + T(')</span>') : '');
|
|
2028
|
+
} else if (p.id === 'loomy') {
|
|
2029
|
+
// 积分口径产品:聚合各账号剩余积分(永久 + 每日);没有签到语义的行
|
|
2030
|
+
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
2031
|
+
let sum = 0, known = 0;
|
|
2032
|
+
for (const a of list) {
|
|
2033
|
+
const q = state.quotas[a.id];
|
|
2034
|
+
if (!q || q.error || q.empty) continue;
|
|
2035
|
+
known++;
|
|
2036
|
+
sum += Number(q.remaining) || 0;
|
|
2037
|
+
}
|
|
2038
|
+
rows = T('<span class="lbl">积分合计</span><span class="val">') + (known ? fmtNum(sum) : '-') + '</span>'
|
|
2039
|
+
+ T('<span class="lbl">有效套餐</span><span class="val">') + planned + ' / ' + list.length + '</span>';
|
|
1959
2040
|
} else {
|
|
1960
2041
|
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
1961
2042
|
// ZCode 没有每日签到:不展示签到语义的行
|
|
@@ -2057,8 +2138,12 @@ function renderStatus() {
|
|
|
2057
2138
|
// 产品相关提示只在对应标签页出现
|
|
2058
2139
|
const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
|
|
2059
2140
|
const n = $('notice');
|
|
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)));
|
|
2061
|
-
if (state.view === '
|
|
2141
|
+
n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || state.view === 'minimax' || state.view === 'loomy' || (state.view === 'workbuddy' && s.workbuddyClient && s.workbuddyClient.installed === false) || (hasIntl && Boolean(s.riskIdentity)));
|
|
2142
|
+
if (state.view === 'loomy') {
|
|
2143
|
+
n.style.display = '';
|
|
2144
|
+
n.innerHTML = T('<div class="z-row"><span class="z-label">本机凭据</span><span class="z-text">由本机 Loomy 客户端读取导入,约 14 天有效;支持多账号切换,可一键同步 10Router。</span></div>')
|
|
2145
|
+
+ loomyClientChip(s);
|
|
2146
|
+
} else if (state.view === 'minimax') {
|
|
2062
2147
|
n.style.display = '';
|
|
2063
2148
|
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>');
|
|
2064
2149
|
} else if (state.view === 'trae') {
|
|
@@ -2123,6 +2208,13 @@ function catpawClientChip(s) {
|
|
|
2123
2208
|
+ (canOpen ? T(' <button class="btn" id="btn-cpopen" style="margin-left:4px" title="打开本机妙手客户端">打开客户端</button>') : '');
|
|
2124
2209
|
}
|
|
2125
2210
|
|
|
2211
|
+
// Loomy 客户端状态芯片
|
|
2212
|
+
function loomyClientChip(s) {
|
|
2213
|
+
const c = s.loomyClient || {};
|
|
2214
|
+
if (!c.installed) return T(' <span class="dim">客户端:未安装(本机导入 / 切换需要 Windows Loomy 客户端)</span>');
|
|
2215
|
+
return T(' <span class="dim">客户端:') + (c.running ? T('运行中') : T('未运行')) + (c.signedIn ? '' : T(' · 未登录')) + '</span>';
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2126
2218
|
// Qoder 设备身份组件(Linux / fnOS)
|
|
2127
2219
|
const umidOffer = (s) => Boolean(s && s.umid && s.umid.supported && !s.umid.installed);
|
|
2128
2220
|
let umidBusy = false;
|
|
@@ -2186,6 +2278,14 @@ document.addEventListener('click', async (e) => {
|
|
|
2186
2278
|
} catch (err) { toast(String(err && err.message || err), false); }
|
|
2187
2279
|
return;
|
|
2188
2280
|
}
|
|
2281
|
+
if (e.target.closest('#lt-open')) {
|
|
2282
|
+
try {
|
|
2283
|
+
const r = await window.creditdaddy.openLoomyClient();
|
|
2284
|
+
if (r && r.ok) toast(T('已启动 Loomy 客户端'));
|
|
2285
|
+
else toast((r && r.error) || T('无法打开 Loomy 客户端'), false);
|
|
2286
|
+
} catch (err) { toast(String(err && err.message || err), false); }
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2189
2289
|
if (e.target.closest('#tt-open')) {
|
|
2190
2290
|
try {
|
|
2191
2291
|
const r = await window.creditdaddy.openTraeClient();
|
|
@@ -2648,6 +2748,9 @@ function renderFreePool() {
|
|
|
2648
2748
|
const clearBtn = canCapture
|
|
2649
2749
|
? ' <button class="btn" data-clear-session="' + esc(site.id) + '" title="' + T('抹掉本机为该站点留存的登录会话(下次捕获需要重新登录)') + '">' + T('清除已存会话') + '</button>'
|
|
2650
2750
|
: '';
|
|
2751
|
+
// 站点级红字警示(site.warnHtml,站点配置声明、此处统一渲染):
|
|
2752
|
+
// 用于「接入该站点有工具之外的代价」这类必须前置告知的情况(如注册新号可能被电话推广)
|
|
2753
|
+
const warn = site.warnHtml ? '<div style="margin:2px 0 6px;font-size:12px;font-weight:600;color:var(--danger,#e5484d)">' + site.warnHtml + '</div>' : '';
|
|
2651
2754
|
const hint = site.hint ? '<div class="hint" style="margin:2px 0 6px">' + esc(T(site.hint)) + '</div>' : '';
|
|
2652
2755
|
|
|
2653
2756
|
const fmtBalanceText = (acc) => {
|
|
@@ -2757,20 +2860,8 @@ function renderFreePool() {
|
|
|
2757
2860
|
+ accRows
|
|
2758
2861
|
+ '</div>';
|
|
2759
2862
|
}).join('');
|
|
2760
|
-
bindCardDrag(body
|
|
2761
|
-
|
|
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
|
-
});
|
|
2863
|
+
// 拖拽绑定已上移到启动区(bindCardDrag($('free-pool-body'))):容器元素持久、
|
|
2864
|
+
// onReorder 闭包在触发时才读 state,渲染函数里重复绑定只会让监听器越积越多
|
|
2774
2865
|
}
|
|
2775
2866
|
|
|
2776
2867
|
function renderCookieSites() {
|
|
@@ -2788,6 +2879,7 @@ function renderCookieSites() {
|
|
|
2788
2879
|
const btn = canCapture
|
|
2789
2880
|
? '<button class="btn" data-capture-site="' + esc(site.id) + '">' + T('网页版') + '</button>'
|
|
2790
2881
|
: T('<button class="btn" disabled title="需要桌面版 CreditDaddy(无痕登录窗)">网页版</button>');
|
|
2882
|
+
const warn = site.warnHtml ? '<div style="font-size:12px;font-weight:600;color:var(--danger,#e5484d)">' + site.warnHtml + '</div>' : '';
|
|
2791
2883
|
const hint = site.hint ? T('<div class="hint">') + esc(site.hint) + '</div>' : '';
|
|
2792
2884
|
const accRows = rows.map((acc) =>
|
|
2793
2885
|
'<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px">'
|
|
@@ -2796,7 +2888,7 @@ function renderCookieSites() {
|
|
|
2796
2888
|
return '<div style="border:1px solid var(--border-subtle);border-radius:10px;padding:10px 12px;display:grid;gap:6px">'
|
|
2797
2889
|
+ '<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">'
|
|
2798
2890
|
+ '<span><b>' + esc(site.label || site.name) + '</b> · ' + stateTxt + '</span>' + btn + '</div>'
|
|
2799
|
-
+ hint + accRows + '</div>';
|
|
2891
|
+
+ warn + hint + accRows + '</div>';
|
|
2800
2892
|
}).join('');
|
|
2801
2893
|
}
|
|
2802
2894
|
/**
|
|
@@ -2821,6 +2913,7 @@ const AM_CHANNELS = [
|
|
|
2821
2913
|
{ provider: 'qoder-cn', label: 'Qoder 国内版', syncable: true },
|
|
2822
2914
|
{ provider: 'workbuddy', label: 'WorkBuddy 国内版', syncable: true },
|
|
2823
2915
|
{ provider: 'workbuddy-intl', label: 'WorkBuddy 国际版', syncable: true },
|
|
2916
|
+
{ provider: 'loomy', label: '讯飞 Loomy', syncable: true },
|
|
2824
2917
|
];
|
|
2825
2918
|
|
|
2826
2919
|
function openTrAccountManage() {
|
|
@@ -2891,7 +2984,7 @@ async function trSyncAccountsFromModal() {
|
|
|
2891
2984
|
tr.accountsSyncing = true;
|
|
2892
2985
|
renderTenrouter();
|
|
2893
2986
|
try {
|
|
2894
|
-
const r = await api('/api/tenrouter/sync-accounts', { method: 'POST', json: pw ? { adminPassword: pw } : {} });
|
|
2987
|
+
const r = await api('/api/tenrouter/sync-accounts', { method: 'POST', json: pw ? { adminPassword: pw } : {}, timeoutMs: 300_000 });
|
|
2895
2988
|
msg.textContent = r.summary || '';
|
|
2896
2989
|
toast(r.summary, !/失败/.test(r.summary || ''));
|
|
2897
2990
|
if (pw) { await loadTrConfig(); $('am-pw').value = ''; }
|
|
@@ -2947,13 +3040,14 @@ $('btn-free-sync-10r')?.addEventListener('click', async () => {
|
|
|
2947
3040
|
if (!(tr.config && tr.config.configured)) return toast(T('请先保存 10Router 设置'), false);
|
|
2948
3041
|
btn.disabled = true;
|
|
2949
3042
|
try {
|
|
2950
|
-
//
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
const r = await api('/api/tenrouter/push-quota', { method: 'POST' });
|
|
3043
|
+
// 只推网页会话渠道的额度卡(webSessionOnly)——账号连接同步不在这里做:那是
|
|
3044
|
+
// 10Router 页「账号管理」弹窗的职责(还带渠道开关);本机凭据线(如 minimax-free)
|
|
3045
|
+
// 也不搭车,本页的「同步」语义就只管网页会话卡。
|
|
3046
|
+
const r = await api('/api/tenrouter/push-quota', { method: 'POST', json: { webSessionOnly: true }, timeoutMs: 300_000 });
|
|
2954
3047
|
const failed = (r.results || []).filter((x) => x.status === 'failed');
|
|
2955
3048
|
if (failed.length) toast(T('额度卡部分推送失败:') + failed.map((x) => x.source + '(' + x.error + ')').join(';'), false);
|
|
2956
3049
|
else if ((r.results || []).length) toast(T('额度卡已推送到 10Router 配额页'), true);
|
|
3050
|
+
else toast(T('没有可推送的额度卡(网页会话账号为空或推送开关已关闭)'), false);
|
|
2957
3051
|
} catch (e) { toast(e.message, false); }
|
|
2958
3052
|
btn.disabled = false;
|
|
2959
3053
|
});
|
|
@@ -2991,7 +3085,8 @@ $('free-settings-push-now')?.addEventListener('click', async () => {
|
|
|
2991
3085
|
const btn = $('free-settings-push-now');
|
|
2992
3086
|
btn.disabled = true;
|
|
2993
3087
|
try {
|
|
2994
|
-
|
|
3088
|
+
// 与本页「同步到 10Router」同口径:只推网页会话渠道的额度卡
|
|
3089
|
+
const r = await api('/api/tenrouter/push-quota', { method: 'POST', json: { webSessionOnly: true }, timeoutMs: 300_000 });
|
|
2995
3090
|
const failed = (r.results || []).filter((x) => x.status === 'failed');
|
|
2996
3091
|
if (failed.length) toast(T('额度卡部分推送失败:') + failed.map((x) => x.source + '(' + x.error + ')').join(';'), false);
|
|
2997
3092
|
else if ((r.results || []).length) toast(T('额度卡已推送到 10Router 配额页'), true);
|
|
@@ -3066,6 +3161,21 @@ if (window.creditdaddy && typeof window.creditdaddy.onCookieCaptured === 'functi
|
|
|
3066
3161
|
});
|
|
3067
3162
|
});
|
|
3068
3163
|
}
|
|
3164
|
+
// Qoder 网页会话收割绑定完成(桌面壳在收割上报返回 bound>0 时推送):
|
|
3165
|
+
// 「缺网页会话」徽章即时消失,并按**手动刷新同款口径**重查额度(live 真探上游
|
|
3166
|
+
// 重拉逐资源包明细)——网页会话刚绑定,缓存快照里还是旧数据/无明细,只把徽章
|
|
3167
|
+
// 藏掉等于没修。此前靠登录成功后的 12s/30s setTimeout 猜收割时机(收割在登录窗
|
|
3168
|
+
// 关闭后才走,时机不稳),猜错徽章挂到 30s 周期轮、明细要等手动刷新才出来。
|
|
3169
|
+
// 网页面板(npm/CLI 场景无 preload)收不到事件,仍由周期轮兜底。
|
|
3170
|
+
if (window.creditdaddy && typeof window.creditdaddy.onQoderWebSessionBound === 'function') {
|
|
3171
|
+
window.creditdaddy.onQoderWebSessionBound(() => {
|
|
3172
|
+
refreshAccountList().then(() => {
|
|
3173
|
+
const qids = (state.accounts || []).filter((a) => String(a.provider).startsWith('qoder')).map((a) => a.id);
|
|
3174
|
+
if (qids.length) loadQuotas(qids, { live: true }).then(render);
|
|
3175
|
+
else render();
|
|
3176
|
+
});
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3069
3179
|
|
|
3070
3180
|
function renderTenrouter() {
|
|
3071
3181
|
const box = $('tr-panel');
|
|
@@ -3170,7 +3280,7 @@ async function loadTrQuotas(force) {
|
|
|
3170
3280
|
}
|
|
3171
3281
|
async function trSync() {
|
|
3172
3282
|
tr.syncing = true; render();
|
|
3173
|
-
try { const r = await api('/api/tenrouter/sync', { method: 'POST' }); toast(r.summary, !r.results.some((x) => x.status === 'failed')); }
|
|
3283
|
+
try { const r = await api('/api/tenrouter/sync', { method: 'POST', timeoutMs: 300_000 }); toast(r.summary, !r.results.some((x) => x.status === 'failed')); }
|
|
3174
3284
|
catch (e) { toast(e.message, false); }
|
|
3175
3285
|
tr.syncing = false;
|
|
3176
3286
|
await loadTrConfig(); loadLogs();
|
|
@@ -3282,22 +3392,23 @@ $('tr-disconnect').onclick = trDisconnect;
|
|
|
3282
3392
|
// ── 数据加载 ──
|
|
3283
3393
|
async function loadStatus() {
|
|
3284
3394
|
try { state.status = await api('/api/status'); renderStatus(); } catch (e) {}
|
|
3285
|
-
syncAfterZcodeClaims();
|
|
3395
|
+
await syncAfterZcodeClaims();
|
|
3286
3396
|
}
|
|
3287
3397
|
// 后台自动领取成功后,立即刷新这些账号的额度与卡片(不必等 2 分钟一轮的余额刷新)
|
|
3288
3398
|
let seenZcodeTickAt = null;
|
|
3289
3399
|
async function syncAfterZcodeClaims() {
|
|
3290
3400
|
const z = state.status && state.status.scheduler && state.status.scheduler.zcode;
|
|
3291
3401
|
const t = z && z.lastTick;
|
|
3292
|
-
if (!t || t.at === seenZcodeTickAt) return;
|
|
3293
|
-
const
|
|
3402
|
+
if (!t || !t.at || t.at === seenZcodeTickAt) return;
|
|
3403
|
+
const isInit = seenZcodeTickAt === null;
|
|
3294
3404
|
seenZcodeTickAt = t.at;
|
|
3295
|
-
|
|
3405
|
+
// 避免首屏重复重刷:只有当进入页面时该轮领取已是 2 分钟前的陈旧历史才跳过(首屏 boot 已有全量覆盖);
|
|
3406
|
+
// 若是刚发生不久的领取,无论是否初次判定都必须立即刷新!
|
|
3407
|
+
if (isInit && (Date.now() - new Date(t.at).getTime() > 120_000)) return;
|
|
3408
|
+
if (!t.claimedIds || !t.claimedIds.length) return;
|
|
3296
3409
|
try { state.accounts = (await api('/api/accounts')).accounts; } catch (e) {}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
// 卡片从「无有效套餐」变成「待生效 N Token · HH:MM 到账」,不重绘就一直停在旧样子,
|
|
3300
|
-
// 用户以为「领取成功了却不显示」——只能自己手动点刷新。
|
|
3410
|
+
// 必须带 live: true,与手动点击「刷新积分」动作完全一致,真探上游重算待生效活动权益
|
|
3411
|
+
await loadQuotas(t.claimedIds, { live: true });
|
|
3301
3412
|
render();
|
|
3302
3413
|
renderStatus();
|
|
3303
3414
|
loadZGateway();
|
|
@@ -3326,9 +3437,10 @@ async function loadQuotas(ids, opts) {
|
|
|
3326
3437
|
const worker = async () => {
|
|
3327
3438
|
while (queue.length && run === quotaRun) {
|
|
3328
3439
|
const id = queue.shift();
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3440
|
+
const runAtFetch = run; // fetch 期间可能有新一轮 loadQuotas 进来(如卡片点「刷新」走 live)
|
|
3441
|
+
try { if (runAtFetch === quotaRun) state.quotas[id] = (await api('/api/accounts/' + encodeURIComponent(id) + '/quota' + (live ? '?live=1' : ''))).quota; }
|
|
3442
|
+
catch (e) { if (runAtFetch === quotaRun) state.quotas[id] = { error: e.message }; }
|
|
3443
|
+
if (runAtFetch === quotaRun) render();
|
|
3332
3444
|
}
|
|
3333
3445
|
};
|
|
3334
3446
|
await Promise.all([worker(), worker(), worker()]);
|
|
@@ -3421,7 +3533,7 @@ async function checkinAll(btn, product) {
|
|
|
3421
3533
|
const label = btn.innerHTML;
|
|
3422
3534
|
btn.disabled = true; btn.innerHTML = I.refresh + T('领取中…');
|
|
3423
3535
|
try {
|
|
3424
|
-
const d = await api('/api/checkin', { json: { skipIfCheckedToday: false, product } });
|
|
3536
|
+
const d = await api('/api/checkin', { json: { skipIfCheckedToday: false, product }, timeoutMs: 600_000 });
|
|
3425
3537
|
toast(d.summary);
|
|
3426
3538
|
} catch (e) { toast(e.message, false); }
|
|
3427
3539
|
btn.disabled = false; btn.innerHTML = label;
|
|
@@ -3432,7 +3544,7 @@ async function claimWbActivities(btn) {
|
|
|
3432
3544
|
const label = btn.innerHTML;
|
|
3433
3545
|
btn.disabled = true; btn.innerHTML = I.refresh + T('领取中…');
|
|
3434
3546
|
try {
|
|
3435
|
-
const d = await api('/api/workbuddy/claim-activities', { method: 'POST', json: {} });
|
|
3547
|
+
const d = await api('/api/workbuddy/claim-activities', { method: 'POST', json: {}, timeoutMs: 300_000 });
|
|
3436
3548
|
const s = d.summary || {};
|
|
3437
3549
|
const claimed = (d.results || []).flatMap((r) => r.claimed || []).filter((x) => x.status === 'checked-in');
|
|
3438
3550
|
const amount = claimed.reduce((sum, x) => sum + (Number(x.claimedAmount) || 0), 0);
|
|
@@ -3443,6 +3555,22 @@ async function claimWbActivities(btn) {
|
|
|
3443
3555
|
btn.disabled = false; btn.innerHTML = label;
|
|
3444
3556
|
await loadStatus(); await loadAccounts(); loadLogs();
|
|
3445
3557
|
}
|
|
3558
|
+
async function loomyInvite(id) {
|
|
3559
|
+
const a = state.accounts.find((x) => x.id === id);
|
|
3560
|
+
if (!a) return;
|
|
3561
|
+
const code = await uiPrompt(T('为 {name} 填写邀请码', { name: a.name || id }), '', {
|
|
3562
|
+
placeholder: T('输入邀请码,如 8YZCH5(绑定后 +5000 永久积分,每号仅一次)'),
|
|
3563
|
+
okText: T('绑定'),
|
|
3564
|
+
});
|
|
3565
|
+
const clean = String(code || '').trim();
|
|
3566
|
+
if (!clean) return;
|
|
3567
|
+
try {
|
|
3568
|
+
const r = await api('/api/accounts/' + encodeURIComponent(id) + '/loomy/invite', { json: { code: clean } });
|
|
3569
|
+
const act = r && r.activation;
|
|
3570
|
+
toast(act && act.activated ? T('邀请码已绑定:{code}', { code: act.appliedInvitationCode || clean }) : T('邀请码已提交'), true);
|
|
3571
|
+
loadQuotas([id], { live: true }).then(render); // 绑定后永久积分 +5000,立即刷新卡片
|
|
3572
|
+
} catch (e) { toast(e.message, false); }
|
|
3573
|
+
}
|
|
3446
3574
|
async function onCardClick(e) {
|
|
3447
3575
|
const btn = e.target.closest('[data-act]');
|
|
3448
3576
|
if (!btn) return;
|
|
@@ -3460,6 +3588,7 @@ async function onCardClick(e) {
|
|
|
3460
3588
|
return deviceLogin();
|
|
3461
3589
|
}
|
|
3462
3590
|
if (act === 'quota') { loadQuotas([id], { live: true }).then(render); return; }
|
|
3591
|
+
if (act === 'invite') return loomyInvite(id);
|
|
3463
3592
|
if (act === 'claim') return openClaim(id);
|
|
3464
3593
|
if (act === 'switch') {
|
|
3465
3594
|
const a = state.accounts.find((x) => x.id === id);
|
|
@@ -3475,7 +3604,9 @@ async function onCardClick(e) {
|
|
|
3475
3604
|
? T('\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。')
|
|
3476
3605
|
: a && a.product === 'catpaw'
|
|
3477
3606
|
? T('\n需先退出妙手;切换后重新打开妙手即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。')
|
|
3478
|
-
: a && a.product === '
|
|
3607
|
+
: a && a.product === 'loomy'
|
|
3608
|
+
? T('\n切换会先关闭并重新打开 Loomy 客户端。注意:客户端界面的登录态存在其内部存储,若重开后仍显示旧账号,需在客户端内「退出登录」后重新登录。')
|
|
3609
|
+
: a && a.product === 'trae'
|
|
3479
3610
|
? T('\n需先退出 Trae;切换后重新打开 Trae 即为新账号。当前登录会先自动快照保存到 CreditDaddy,不会丢号。')
|
|
3480
3611
|
: a && a.product === 'qoder'
|
|
3481
3612
|
? T('\n需先退出 Qoder;切换后重新打开 Qoder 即为新账号。运行中的 Qoder 退出时会把旧登录写回去,可选「关闭并强制切换」代为关进程。')
|
|
@@ -3486,7 +3617,7 @@ async function onCardClick(e) {
|
|
|
3486
3617
|
let r;
|
|
3487
3618
|
try { r = await doSwitch(false); }
|
|
3488
3619
|
catch (err) {
|
|
3489
|
-
if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING' && err.code !== 'TRAE_RUNNING' && err.code !== 'QODER_RUNNING') throw err;
|
|
3620
|
+
if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING' && err.code !== 'TRAE_RUNNING' && err.code !== 'QODER_RUNNING' && err.code !== 'LOOMY_RUNNING') throw err;
|
|
3490
3621
|
if (!(await uiConfirm(err.message + T('\n\n仍要强制切换吗?强制切换会先关闭客户端再写入登录,重新打开客户端即为新账号。'), client + T(' 正在运行'), T('关闭并强制切换')))) return;
|
|
3491
3622
|
r = await doSwitch(true);
|
|
3492
3623
|
}
|
|
@@ -3538,6 +3669,17 @@ async function onCardClick(e) {
|
|
|
3538
3669
|
} catch (e) {}
|
|
3539
3670
|
}, 800);
|
|
3540
3671
|
}
|
|
3672
|
+
if (a && a.product === 'loomy' && r && !r.alreadyActive && window.creditdaddy && typeof window.creditdaddy.openLoomyClient === 'function') {
|
|
3673
|
+
// 只要真切换了就自动拉起客户端(此前只在客户端原本运行时才拉,客户端没跑时切完毫无动静)
|
|
3674
|
+
setTimeout(async () => {
|
|
3675
|
+
try {
|
|
3676
|
+
const re = await window.creditdaddy.openLoomyClient();
|
|
3677
|
+
if (re && re.ok) {
|
|
3678
|
+
toast(T('已重新打开 Loomy 客户端。注意:Loomy 的界面登录态存在客户端内部存储——若仍显示旧账号,请在客户端「退出登录」后重新登录目标账号'), false);
|
|
3679
|
+
} else toast((re && re.error) || T('无法打开 Loomy 客户端(桌面版才能自动拉起)'), false);
|
|
3680
|
+
} catch (e) {}
|
|
3681
|
+
}, 800);
|
|
3682
|
+
}
|
|
3541
3683
|
} catch (err) { toast(err.message, false); }
|
|
3542
3684
|
return;
|
|
3543
3685
|
}
|
|
@@ -3636,7 +3778,34 @@ async function openClaim(id) {
|
|
|
3636
3778
|
|
|
3637
3779
|
function renderClaimBody() {
|
|
3638
3780
|
if (!claimState.plans.length) {
|
|
3639
|
-
$('claim-body').innerHTML = T('<div class="muted-empty">当前没有可领取的活动</div>')
|
|
3781
|
+
$('claim-body').innerHTML = T('<div class="muted-empty">当前没有可领取的活动</div>')
|
|
3782
|
+
+ '<div class="row" style="margin-top:10px;justify-content:flex-end"><button class="btn" id="btn-claim-known">' + T('尝试已知活动') + '</button><span class="hint" id="claim-known-msg"></span></div>';
|
|
3783
|
+
$('btn-claim-known')?.addEventListener('click', async () => {
|
|
3784
|
+
const btn = $('btn-claim-known');
|
|
3785
|
+
const msg = $('claim-known-msg');
|
|
3786
|
+
btn.disabled = true;
|
|
3787
|
+
btn.textContent = T('尝试中…');
|
|
3788
|
+
try {
|
|
3789
|
+
const r = await api('/api/accounts/' + encodeURIComponent(claimState.accountId) + '/zcode/claim-known', { method: 'POST', json: {}, timeoutMs: 60_000 });
|
|
3790
|
+
const bits = [];
|
|
3791
|
+
if (r.claimed.length) bits.push(T('领取成功:') + r.claimed.join(T('、')));
|
|
3792
|
+
if (r.already.length) bits.push(T('已领过:') + r.already.join(T('、')));
|
|
3793
|
+
if (r.skipped.length) bits.push(T('暂不可领:') + r.skipped.length + T(' 个'));
|
|
3794
|
+
msg.textContent = bits.join(T(';')) || T('没有可领取的活动');
|
|
3795
|
+
if (r.claimed.length) {
|
|
3796
|
+
toast(T('已领取:') + r.claimed.join(T('、')), true);
|
|
3797
|
+
closeModal('m-claim');
|
|
3798
|
+
await loadStatus();
|
|
3799
|
+
await loadAccounts();
|
|
3800
|
+
await loadQuotas([claimState.accountId], { live: true });
|
|
3801
|
+
render();
|
|
3802
|
+
}
|
|
3803
|
+
} catch (e) {
|
|
3804
|
+
msg.textContent = e.message;
|
|
3805
|
+
}
|
|
3806
|
+
btn.disabled = false;
|
|
3807
|
+
btn.textContent = T('尝试已知活动');
|
|
3808
|
+
});
|
|
3640
3809
|
return;
|
|
3641
3810
|
}
|
|
3642
3811
|
$('claim-body').innerHTML = claimState.plans.map((pl, i) =>
|
|
@@ -3797,7 +3966,7 @@ async function restartCreditDaddy() {
|
|
|
3797
3966
|
const poll = setInterval(async () => {
|
|
3798
3967
|
attempts++;
|
|
3799
3968
|
try {
|
|
3800
|
-
const res = await fetch('/api/status', { cache: 'no-store' });
|
|
3969
|
+
const res = await fetch(resolveApiPath('/api/status'), { cache: 'no-store' });
|
|
3801
3970
|
if (res.ok) {
|
|
3802
3971
|
clearInterval(poll);
|
|
3803
3972
|
location.reload();
|
|
@@ -4066,11 +4235,18 @@ const LOCAL_HINT = {
|
|
|
4066
4235
|
catpaw: '读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密 %APPDATA%\\catpaw-moon\\catx-credential.json),token 不出本机。',
|
|
4067
4236
|
trae: '读取本机 Trae 客户端当前登录的账号(本地解密 <客户端数据目录>\\User\\globalStorage\\storage.json 的 tc 信封),token 不出本机。',
|
|
4068
4237
|
minimax: '读取本机 MiniMax Code 客户端当前登录的账号(~/.minimax/auth/.../auth.json),token 不出本机。',
|
|
4238
|
+
loomy: '读取本机 Loomy 客户端当前登录的账号(自动读取 Windows 本机 Public\\Loomy 用户凭据),Session 有效期约 14 天,支持一键同步至 10Router。',
|
|
4069
4239
|
};
|
|
4070
4240
|
function openAdd(product) {
|
|
4071
4241
|
state.addProduct = product || '';
|
|
4072
|
-
|
|
4073
|
-
|
|
4242
|
+
const isLoomy = product === 'loomy';
|
|
4243
|
+
// 各产品支持的添加方式
|
|
4244
|
+
const tabShown = {
|
|
4245
|
+
local: true,
|
|
4246
|
+
device: product !== 'mirasim' && product !== 'catpaw' && product !== 'trae' && product !== 'loomy',
|
|
4247
|
+
token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' && product !== 'trae',
|
|
4248
|
+
sms: isLoomy || !product,
|
|
4249
|
+
};
|
|
4074
4250
|
document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
|
|
4075
4251
|
$('local-hint').textContent = T(LOCAL_HINT[state.addProduct]);
|
|
4076
4252
|
let first = null;
|
|
@@ -4091,8 +4267,8 @@ function openAdd(product) {
|
|
|
4091
4267
|
$('dev-hint').textContent = (hasIncognito() ? T('在内置隐私窗口里登录(不带出系统浏览器已登录的账号),') : T('在浏览器里登录并授权,'))
|
|
4092
4268
|
+ T('完成后账号自动添加。') + (product === 'qoder' ? '' : T('WorkBuddy / ZCode 用浏览器登录的账号同样可以一键切换客户端。'));
|
|
4093
4269
|
openModal('m-add');
|
|
4094
|
-
switchTab('local');
|
|
4095
|
-
localScan();
|
|
4270
|
+
switchTab(isLoomy ? 'sms' : 'local');
|
|
4271
|
+
if (!isLoomy) localScan();
|
|
4096
4272
|
}
|
|
4097
4273
|
|
|
4098
4274
|
// 本机导入
|
|
@@ -4170,10 +4346,12 @@ async function deviceLogin() {
|
|
|
4170
4346
|
pollDevice(flow.sessionId, 0, ++devGen);
|
|
4171
4347
|
} catch (e) { toast(T('发起授权失败:') + e.message, false); }
|
|
4172
4348
|
}
|
|
4349
|
+
let pollFails = 0; // 连续轮询失败计数(成功清零;连续 5 次才终止登录流程)
|
|
4173
4350
|
async function pollDevice(sessionId, n, gen) {
|
|
4174
4351
|
if (gen !== devGen) return;
|
|
4175
4352
|
try {
|
|
4176
4353
|
const r = await api('/api/auth/device/poll', { json: { sessionId } });
|
|
4354
|
+
pollFails = 0;
|
|
4177
4355
|
if (gen !== devGen) return;
|
|
4178
4356
|
if (r.status === 'pending') {
|
|
4179
4357
|
if (n > 450) return deviceStop(T('授权超时,请重试'));
|
|
@@ -4184,8 +4362,9 @@ async function pollDevice(sessionId, n, gen) {
|
|
|
4184
4362
|
toast(r.already ? T('已更新「') + (r.account.name || r.account.id) + T('」的登录凭据') : T('已添加:') + (r.account.name || r.account.id));
|
|
4185
4363
|
if (r.account.id) delete state.quotas[r.account.id]; // 旧 token 的积分查询结果(可能是失败)作废,马上重查
|
|
4186
4364
|
await loadStatus(); await loadAccounts();
|
|
4187
|
-
// 网页会话 Cookie
|
|
4188
|
-
//
|
|
4365
|
+
// 网页会话 Cookie 在登录窗「关闭」后才收割绑定(晚于本提示数秒且时机不稳)——
|
|
4366
|
+
// 到位时桌面壳推 qoder-web-session:bound 事件刷新(onQoderWebSessionBound),
|
|
4367
|
+
// 这里只保留两轮轻量补拉兜住非桌面场景(网页面板无 preload 收不到事件)。
|
|
4189
4368
|
setTimeout(refreshAccountList, 12000);
|
|
4190
4369
|
setTimeout(refreshAccountList, 30000);
|
|
4191
4370
|
} else if (r.status === 'expired') {
|
|
@@ -4193,10 +4372,19 @@ async function pollDevice(sessionId, n, gen) {
|
|
|
4193
4372
|
} else {
|
|
4194
4373
|
deviceStop(T('授权失败:') + (r.error || r.status));
|
|
4195
4374
|
}
|
|
4196
|
-
} catch (e) {
|
|
4375
|
+
} catch (e) {
|
|
4376
|
+
// 单次轮询失败(daemon 忙时瞬时 502/超时)不该终止整个登录:服务端会话仍然有效,
|
|
4377
|
+
// 下一个 2s tick 就可能成功。连续多次失败才放弃(上限 5 次 ≈ 10s+超时重试)。
|
|
4378
|
+
if (gen !== devGen) return;
|
|
4379
|
+
pollFails++;
|
|
4380
|
+
if (pollFails >= 5) { pollFails = 0; return deviceStop(T('轮询异常:') + e.message); }
|
|
4381
|
+
$('dev-status').textContent = T('轮询异常,正在重试…(') + pollFails + '/5)';
|
|
4382
|
+
devTimer = setTimeout(() => pollDevice(sessionId, n + 1, gen), 2000);
|
|
4383
|
+
}
|
|
4197
4384
|
}
|
|
4198
4385
|
function deviceStop(msg) {
|
|
4199
4386
|
devGen++;
|
|
4387
|
+
pollFails = 0;
|
|
4200
4388
|
if (devTimer) { clearTimeout(devTimer); devTimer = null; }
|
|
4201
4389
|
$('dev-cancel').style.display = 'none';
|
|
4202
4390
|
if (msg) $('dev-status').textContent = msg;
|
|
@@ -4217,6 +4405,75 @@ async function addToken() {
|
|
|
4217
4405
|
b.disabled = false;
|
|
4218
4406
|
}
|
|
4219
4407
|
|
|
4408
|
+
// ── 讯飞 Loomy 手机号直登 ──
|
|
4409
|
+
let loomyMsgId = '';
|
|
4410
|
+
let loomyCountdown = 0;
|
|
4411
|
+
let loomyTimer = null;
|
|
4412
|
+
|
|
4413
|
+
$('btn-loomy-sms').addEventListener('click', async () => {
|
|
4414
|
+
const phone = $('loomy-phone').value.trim();
|
|
4415
|
+
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
|
4416
|
+
return toast(T('请输入正确的 11 位手机号'), false);
|
|
4417
|
+
}
|
|
4418
|
+
const btn = $('btn-loomy-sms');
|
|
4419
|
+
btn.disabled = true;
|
|
4420
|
+
btn.textContent = T('发送中…');
|
|
4421
|
+
try {
|
|
4422
|
+
const r = await api('/api/loomy/sms/send', { json: { phone }, timeoutMs: 45_000 });
|
|
4423
|
+
loomyMsgId = r.msgid;
|
|
4424
|
+
toast(T('验证码已发送至手机,请查收短信'));
|
|
4425
|
+
$('loomy-code-field').style.display = '';
|
|
4426
|
+
$('loomy-login-actions').style.display = '';
|
|
4427
|
+
loomyCountdown = 60;
|
|
4428
|
+
btn.textContent = '60s';
|
|
4429
|
+
if (loomyTimer) clearInterval(loomyTimer);
|
|
4430
|
+
loomyTimer = setInterval(() => {
|
|
4431
|
+
loomyCountdown--;
|
|
4432
|
+
if (loomyCountdown <= 0) {
|
|
4433
|
+
clearInterval(loomyTimer);
|
|
4434
|
+
loomyTimer = null;
|
|
4435
|
+
btn.disabled = false;
|
|
4436
|
+
btn.textContent = T('重新获取');
|
|
4437
|
+
} else {
|
|
4438
|
+
btn.textContent = `${loomyCountdown}s`;
|
|
4439
|
+
}
|
|
4440
|
+
}, 1000);
|
|
4441
|
+
} catch (e) {
|
|
4442
|
+
btn.disabled = false;
|
|
4443
|
+
btn.textContent = T('获取验证码');
|
|
4444
|
+
toast(e.message, false);
|
|
4445
|
+
}
|
|
4446
|
+
});
|
|
4447
|
+
|
|
4448
|
+
$('btn-loomy-login').addEventListener('click', async () => {
|
|
4449
|
+
const phone = $('loomy-phone').value.trim();
|
|
4450
|
+
const code = $('loomy-code').value.trim();
|
|
4451
|
+
if (!phone || !code || !loomyMsgId) {
|
|
4452
|
+
return toast(T('请输入手机号并完成验证码验证'), false);
|
|
4453
|
+
}
|
|
4454
|
+
const btn = $('btn-loomy-login');
|
|
4455
|
+
btn.disabled = true;
|
|
4456
|
+
btn.textContent = T('登录中…');
|
|
4457
|
+
try {
|
|
4458
|
+
const r = await api('/api/loomy/sms/login', { json: { phone, code, msgid: loomyMsgId }, timeoutMs: 45_000 });
|
|
4459
|
+
toast(r.duplicate ? T('账号已存在,登录凭据已更新') : T('Loomy 账号登录成功!已存入账号库'));
|
|
4460
|
+
closeModal('m-add');
|
|
4461
|
+
$('loomy-phone').value = '';
|
|
4462
|
+
$('loomy-code').value = '';
|
|
4463
|
+
$('loomy-code-field').style.display = 'none';
|
|
4464
|
+
$('loomy-login-actions').style.display = 'none';
|
|
4465
|
+
if (loomyTimer) { clearInterval(loomyTimer); loomyTimer = null; }
|
|
4466
|
+
$('btn-loomy-sms').disabled = false;
|
|
4467
|
+
$('btn-loomy-sms').textContent = T('获取验证码');
|
|
4468
|
+
await loadAccounts();
|
|
4469
|
+
render();
|
|
4470
|
+
} catch (e) {
|
|
4471
|
+
btn.disabled = false;
|
|
4472
|
+
btn.textContent = T('登录并导入');
|
|
4473
|
+
toast(e.message, false);
|
|
4474
|
+
}
|
|
4475
|
+
});
|
|
4476
|
+
|
|
4220
4477
|
// Qoder 卡片徽章:网页会话(Cookie)缺失或超 7 天时点名提醒——
|
|
4221
4478
|
// 10Router 的「套餐内 Credits」逐资源包明细依赖它(同 qoderWebSessionNote 口径)。
|
|
4222
4479
|
// 正常状态不出徽章,只在需要「浏览器登录」补 cookie 时出橙色警示,避免噪音。
|
|
@@ -4422,6 +4679,22 @@ bindCardDrag($('tr-panel'), {
|
|
|
4422
4679
|
renderTenrouter();
|
|
4423
4680
|
},
|
|
4424
4681
|
});
|
|
4682
|
+
// 免费额度页站点卡拖拽:与其他两处一样只在启动时绑一次。此前在 renderFreePool 里绑定,
|
|
4683
|
+
// 而 render() 每次刷新都调它——监听器按渲染次数无限累积,一次 drop 触发 N 次重排
|
|
4684
|
+
bindCardDrag($('free-pool-body'), {
|
|
4685
|
+
kind: 'free-site',
|
|
4686
|
+
onReorder: (drag, targetId) => {
|
|
4687
|
+
if (!drag.id) return;
|
|
4688
|
+
const ids = state.freeOrder.slice();
|
|
4689
|
+
const from = ids.indexOf(drag.id);
|
|
4690
|
+
if (from >= 0) ids.splice(from, 1);
|
|
4691
|
+
const ti = ids.indexOf(targetId);
|
|
4692
|
+
ids.splice(ti < 0 ? ids.length : ti, 0, drag.id);
|
|
4693
|
+
state.freeOrder = ids;
|
|
4694
|
+
saveFreeOrder();
|
|
4695
|
+
renderFreePool();
|
|
4696
|
+
},
|
|
4697
|
+
});
|
|
4425
4698
|
|
|
4426
4699
|
function syncCompMenu() { renderCompMenu(); }
|
|
4427
4700
|
function applyComponents() {
|
|
@@ -4822,15 +5095,33 @@ function hideBootBusy() { const b = $('boot-busy'); if (!b) return; clearTimeout
|
|
|
4822
5095
|
loadMGateway();
|
|
4823
5096
|
loadTGateway();
|
|
4824
5097
|
loadTrConfig().then(() => loadTrQuotas());
|
|
4825
|
-
|
|
5098
|
+
let trBusy = false;
|
|
5099
|
+
setInterval(async () => {
|
|
5100
|
+
if (trBusy || document.hidden) return;
|
|
5101
|
+
trBusy = true;
|
|
5102
|
+
try { await loadTrQuotas(); } finally { trBusy = false; }
|
|
5103
|
+
}, 5 * 60 * 1000);
|
|
4826
5104
|
// 账号余额(ZCode 剩余 token 等)自动刷新:可见时每 2 分钟一轮;从托盘/后台切回时若超过 1 分钟没刷过立即补一轮
|
|
4827
5105
|
setInterval(() => { if (!document.hidden) loadQuotas(); }, 2 * 60 * 1000);
|
|
4828
5106
|
document.addEventListener('visibilitychange', () => {
|
|
4829
5107
|
if (!document.hidden && Date.now() - quotaLoadedAt > 60 * 1000) loadQuotas();
|
|
4830
5108
|
});
|
|
4831
5109
|
loadLogs();
|
|
4832
|
-
|
|
4833
|
-
|
|
5110
|
+
// 轮询单飞:上一轮没结束就跳过本轮。没有这个守卫 + api() 挂钟超时,daemon 卡住
|
|
5111
|
+
// 时每 10s/30s 的轮询会无限堆积,恢复后一次性齐发还互相踩状态
|
|
5112
|
+
let logsBusy = false;
|
|
5113
|
+
setInterval(async () => {
|
|
5114
|
+
if (logsBusy || document.hidden) return;
|
|
5115
|
+
logsBusy = true;
|
|
5116
|
+
try { await loadLogs(); } finally { logsBusy = false; }
|
|
5117
|
+
}, 10000);
|
|
5118
|
+
let statusBusy = false;
|
|
5119
|
+
setInterval(async () => {
|
|
5120
|
+
if (statusBusy || document.hidden) return;
|
|
5121
|
+
statusBusy = true;
|
|
5122
|
+
try { await loadStatus(); await loadZNet(); await loadZGateway(); await loadMGateway(); await loadTGateway(); await refreshAccountList(); render(); }
|
|
5123
|
+
finally { statusBusy = false; }
|
|
5124
|
+
}, 30000);
|
|
4834
5125
|
});
|
|
4835
5126
|
}
|
|
4836
5127
|
// 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
|