creditdaddy 0.8.1 → 0.9.1
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 +6 -5
- package/package.json +1 -1
- package/src/checkin.js +31 -2
- package/src/daemon.js +16 -6
- package/src/panel.html +336 -73
- package/src/providers.js +3 -2
- package/src/store.js +28 -3
- package/src/workbuddyClient.js +45 -0
- package/src/zcodeAutoClaim.js +96 -0
- package/src/zcodeClient.js +32 -12
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
## 功能
|
|
12
12
|
|
|
13
13
|
- **仪表盘 + 分产品标签页**:首页仪表盘汇总账号数、今日签到进度、剩余积分、下次自动签到,各产品概况、需要处理的账号(token 失效 / 即将过期、签到失败)与最近签到记录;Qoder / WorkBuddy / ZCode 各自一个标签页,卡片式账号列表(参考 WorkDaddy),可按国际 / 国内版筛选,显示签到状态、连签天数、剩余积分、积分包到期、token 有效期,支持亮色 / 暗色
|
|
14
|
-
- **10Router 集成**:配置 10Router 地址 + 虚拟 key(sk-…)后,「10Router
|
|
14
|
+
- **10Router 集成**:配置 10Router 地址 + 虚拟 key(sk-…)后,「10Router」标签页**按供应商聚类**渲染 10Router 里其他供应商(CodeBuddy / Qoder / Claude / GLM …)的额度卡片(CodeBuddy / Qoder / GLM 系连接带**国内版 / 国际版**徽标,与账号卡同一语义),默认只显示主额度,其余收进一行摘要(明细可展开);额度告急 / 查询失败会进仪表盘「需要处理」,工具栏显示 10Router 健康(存储驱动降级有提示);并内置 10router-sync 插件的用量同步能力,每小时把本机 ZCode / OpenCode / mirasim / 小米 MiMo 的用量导入 10Router 的用量统计
|
|
15
15
|
- **本机导入**:一键读取本机 Qoder / Qoder CN 客户端(本地解密 `auth.v1.dat`)与 WorkBuddy 客户端(当前 + 历史会话)已登录的账号,token 不出机器;同一用户 token 续期时自动更新
|
|
16
16
|
- **WorkBuddy / ZCode 账号切换**:一键把客户端切换到选中的账号(WorkBuddy 自动应用;ZCode 需先退出客户端),切换前先保全当前登录,绝不丢号,每个账号独立设备指纹
|
|
17
17
|
- **隐私(无痕)登录窗口**:桌面版内置一次性会话的登录窗口,网页授权不带出系统浏览器里已登录的账号、Cookie 也不落盘,适合同一产品登录多个账号
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
- **浏览器登录**:`POST {域名}/v2/plugin/auth/state?platform=WorkBuddy` 取授权地址,用户登录后轮询 `/v2/plugin/auth/token?state=`(11217 = 等待)拿 token,再轮询 `/v2/plugin/login/account?state=`(12151 = 等待)与 `/v2/plugin/accounts` 补齐账号信息,组装成与 `workbuddy-desktop.info` 同结构的会话;国内版走 www.codebuddy.cn,国际版走 www.codebuddy.ai
|
|
34
34
|
- **token 刷新**:只在 token 过期或接口返回 401 时用 refreshToken 刷新(`/v2/plugin/auth/token/refresh`);若该账号正是 WorkBuddy 客户端当前登录的账号,会把新 token 同步写回客户端,避免客户端掉线。refreshToken 也失效时(如在别处登出),需要在 WorkBuddy 重新登录后再「本机导入」同步
|
|
35
35
|
- **切换账号**:写入 `workbuddy-desktop.info` 并清除登出标记,WorkBuddy 监听该文件并自动应用;切换前会先把客户端当前会话的最新 token 保存到 CreditDaddy
|
|
36
|
-
-
|
|
36
|
+
- **国际版「活跃领取」**:国际版没有签到接口,官方规则为「当天有 ≥1 次有效对话请求即视为活跃用户,发放每日赠送积分」。调度器每天为每个国际版账号发一条免费档模型(rateMultiplier 0,~0 消耗)的极短流式请求(`POST {域名}/v2/chat/completions`,system 提示 + typed blocks 是网关必需格式),2xx 即当日活跃达成;额度耗尽时会得到「等官方发放后再试」的明确提示
|
|
37
37
|
|
|
38
38
|
## ZCode 说明
|
|
39
39
|
|
|
40
40
|
- **账号来源**:ZCode 桌面客户端把当前登录存在 `~/.zcode/v2/credentials.json`(值用客户端私有的 `enc:v1:` AES-256-GCM 加密,密钥从机器信息派生),CreditDaddy 本地解密读取,token 不出机器
|
|
41
41
|
- **浏览器登录**:与客户端的 CLI 轮询登录同一协议 —— `POST https://zcode.z.ai/api/v1/oauth/cli/init`(本地随机 poll token)拿授权地址(回调指向 zcode.z.ai 服务端,无需 `zcode://` 协议,任何浏览器都能用),用户登录后轮询 `/api/v1/oauth/cli/poll/{flow_id}`;Z.ai 账号再用 `api.z.ai/api/auth/z/login` 换业务 token。拿到的凭据按 `credentials.json` 格式用本机密钥加密保存,可直接切换到 ZCode 客户端
|
|
42
|
-
-
|
|
42
|
+
- **没有每日签到,活动领取可自动轮询(开关默认关)**:ZCode 积分靠「活动领取」(billing/preview → billing/claim)。标签页打开「自动领取」后,每轮签到结束会轮询可领活动:先试免验证码领取,需要验证码时 —— 桌面版用隐藏窗口跑阿里云验证码 SDK 静默验证(触发风控时弹出窗口让人工完成,2 分钟超时),NAS / 纯 CLI 环境标记「需手动领取」;平时保持关闭即可,活动期前再开。也可点卡片上的领取按钮手动领取。ZCode 卡片不显示任何签到 / 领取徽标,结果只看运行日志与仪表盘最近记录
|
|
43
43
|
- **客户端版本跟随本机**:billing 类接口校验客户端版本(过低直接 3001),请求头里上报的版本取本机已安装 ZCode 的版本(Windows 注册表读取),没有客户端时用兜底值
|
|
44
|
-
- **出口可切换**:ZCode
|
|
44
|
+
- **出口可切换**:ZCode 上游请求默认「直连优先、代理兜底」,标签页提示条里可切「代理优先、直连兜底」。代理地址可在面板配置(`zcode-net.json`,0600,优先),未配置时取 `HTTPS_PROXY` 环境变量;http 代理 + CONNECT 隧道,纯 Node 内置实现;`NO_PROXY` 与 loopback 始终直连
|
|
45
45
|
- **额度展示**:查询 BigModel Coding Plan(quota/limit + subscription/list)与 Z.ai / Start Plan(billing/balance);balances 为空时从生效套餐的权益派生展示额度(未到生效时间的权益显示「xx 生效」);无有效套餐时显示「仅免费额度」
|
|
46
46
|
- **账号切换**:写回 `credentials.json` / `config.json`,并为每个账号写入独立的设备 ID(`telemetry-state.json` 的 deviceMid),避免账号间被风控关联;需先退出 ZCode 客户端(运行中会覆盖回内存里的旧登录)。切换前当前登录会先保存进 CreditDaddy,不会丢号
|
|
47
47
|
- 协议移植自 [zcode-switch](https://github.com/pjpv/zcode-switch)(MIT)
|
|
@@ -167,7 +167,8 @@ Qoder 侧(国际版 openapi.qoder.sh,国内版 openapi.qoder.com.cn):
|
|
|
167
167
|
src/workbuddyClient.js WorkBuddy API:签到、积分、token 刷新
|
|
168
168
|
src/workbuddyLocal.js 本机 WorkBuddy 会话读取与账号切换
|
|
169
169
|
src/zcrypto.js ZCode 本机凭据加解密(enc:v1: AES-256-GCM)
|
|
170
|
-
src/zcodeClient.js ZCode 额度查询 + 活动领取(billing preview/claim
|
|
170
|
+
src/zcodeClient.js ZCode 额度查询 + 活动领取(billing preview/claim、验证码配置、出口切换)
|
|
171
|
+
src/zcodeAutoClaim.js ZCode 活动自动轮询领取(免验证码 → 静默验证码 → 需手动)
|
|
171
172
|
src/zcodeLocal.js 本机 ZCode 凭据读取与账号切换
|
|
172
173
|
desktop/preload.js 桌面版 preload:暴露隐私登录窗口能力
|
|
173
174
|
src/checkin.js 调度器(2h tick + 当日去重 + 本机限领 + 串行执行)
|
package/package.json
CHANGED
package/src/checkin.js
CHANGED
|
@@ -14,6 +14,8 @@ import { productImpl } from './providers.js';
|
|
|
14
14
|
import { productOf } from './constants.js';
|
|
15
15
|
import { startUsageSyncScheduler } from './tenrouter.js';
|
|
16
16
|
import { refreshContext } from './accounts.js';
|
|
17
|
+
import { zcodeAutoClaim } from './zcodeAutoClaim.js';
|
|
18
|
+
import { autoClaimEnabled } from './zcodeClient.js';
|
|
17
19
|
import { logger } from './logger.js';
|
|
18
20
|
|
|
19
21
|
const TICK_MS = 2 * 60 * 60 * 1000; // 2 小时
|
|
@@ -70,15 +72,24 @@ async function runTickNow(opts) {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
async function runTickInner(opts) {
|
|
75
|
+
const allAccounts = await loadAccounts();
|
|
73
76
|
// 没有每日签到能力的产品(如 ZCode:积分靠需验证码的活动领取)不参与签到轮
|
|
74
|
-
const accounts =
|
|
77
|
+
const accounts = allAccounts.filter(
|
|
75
78
|
(a) => (!opts.provider || a.provider === opts.provider)
|
|
76
79
|
&& (!opts.product || productOf(a.provider) === opts.product)
|
|
77
80
|
&& (!opts.onlyAccountId || a.id === opts.onlyAccountId)
|
|
78
81
|
&& typeof productImpl(a.provider)?.checkin === 'function'
|
|
79
82
|
);
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
// ZCode 活动轮询领取:开关打开(标签页可切换,默认关)且未被筛选参数排除时参与
|
|
85
|
+
const zAccounts = (autoClaimEnabled()
|
|
86
|
+
&& !opts.onlyAccountId
|
|
87
|
+
&& (!opts.provider || opts.provider === 'zcode')
|
|
88
|
+
&& (!opts.product || opts.product === 'zcode'))
|
|
89
|
+
? allAccounts.filter((a) => a.provider === 'zcode')
|
|
90
|
+
: [];
|
|
91
|
+
|
|
92
|
+
if (accounts.length === 0 && zAccounts.length === 0) {
|
|
82
93
|
return { results: [], summary: '没有匹配的账号' };
|
|
83
94
|
}
|
|
84
95
|
|
|
@@ -152,6 +163,24 @@ async function runTickInner(opts) {
|
|
|
152
163
|
});
|
|
153
164
|
await saveState({ ...state, qoderDailyDone: memo, deviceClaim });
|
|
154
165
|
|
|
166
|
+
// ZCode 活动自动领取:每轮签到后顺带轮询(preview 为空时零请求副作用;需开关打开)。
|
|
167
|
+
// 桌面版已注册隐藏窗口验证码提供者,可静默过验证码;其它环境需要验证码时标记「需手动领取」。
|
|
168
|
+
for (const za of zAccounts) {
|
|
169
|
+
const label = za.name || za.uid || za.id;
|
|
170
|
+
try {
|
|
171
|
+
const r = await zcodeAutoClaim(za);
|
|
172
|
+
if (!r) continue;
|
|
173
|
+
results.push({ accountId: za.id, account: label, provider: 'zcode', status: r.status, message: r.message });
|
|
174
|
+
const lastResult = { status: r.status, message: r.message, amount: 0, at: new Date().toISOString() };
|
|
175
|
+
await withAccounts((list) => {
|
|
176
|
+
const cur = list.find((a) => a.id === za.id);
|
|
177
|
+
if (cur) cur.lastResult = lastResult;
|
|
178
|
+
});
|
|
179
|
+
} catch (e) {
|
|
180
|
+
logger.warn('ZCODE-CLAIM', `${label} 自动领取异常:${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
const claimed = results.filter((r) => r.status === 'checked-in');
|
|
156
185
|
const failed = results.filter((r) => r.status === 'failed');
|
|
157
186
|
const none = results.filter((r) => r.status === 'no-activity');
|
package/src/daemon.js
CHANGED
|
@@ -50,7 +50,7 @@ import { addAccount, importAccounts, refreshContext } from './accounts.js';
|
|
|
50
50
|
import { productImpl } from './providers.js';
|
|
51
51
|
import { readWorkbuddySessions, writeWorkbuddySession, workbuddyAuthDir, currentWorkbuddyUid } from './workbuddyLocal.js';
|
|
52
52
|
import { liveToAccount as zcodeLiveAccount, switchTo as zcodeSwitchTo, currentZcodeUid, currentZcodeIdentity, detectZcode, ensureVirtualDeviceMid, terminateZcode } from './zcodeLocal.js';
|
|
53
|
-
import { fetchClaimPlans, claimPlan, fetchCaptchaConfig, proxyFirst, setProxyFirst } from './zcodeClient.js';
|
|
53
|
+
import { fetchClaimPlans, claimPlan, fetchCaptchaConfig, proxyFirst, setProxyFirst, proxyUrl, setProxyUrl, autoClaimEnabled, setAutoClaimEnabled } from './zcodeClient.js';
|
|
54
54
|
import { exportAccounts, parseImport, TransferError } from './transfer.js';
|
|
55
55
|
import { runCheckinTick, getSchedulerInfo, dayKey } from './checkin.js';
|
|
56
56
|
import { detectQoderApps, readQoderAppAccounts, riskIdentityAvailable, riskIdentitySource } from './qoderApp.js';
|
|
@@ -246,6 +246,10 @@ async function handleApi(req, res, url) {
|
|
|
246
246
|
region: typeof body.region === 'string' ? body.region : '',
|
|
247
247
|
});
|
|
248
248
|
logger.info('DAEMON', `ZCode ${account.name || account.id} 领取成功:${outcome.planName}`);
|
|
249
|
+
await withAccounts((list) => {
|
|
250
|
+
const cur = list.find((a) => a.id === account.id);
|
|
251
|
+
if (cur) cur.lastResult = { status: 'checked-in', message: `已领取:${outcome.planName}`, amount: 0, at: new Date().toISOString() };
|
|
252
|
+
});
|
|
249
253
|
return json(res, 200, outcome);
|
|
250
254
|
} catch (e) {
|
|
251
255
|
logger.warn('DAEMON', `ZCode ${account.name || account.id} 领取失败:${e.message}`);
|
|
@@ -259,15 +263,21 @@ async function handleApi(req, res, url) {
|
|
|
259
263
|
return json(res, 502, { error: e.message });
|
|
260
264
|
}
|
|
261
265
|
}
|
|
262
|
-
// ZCode
|
|
266
|
+
// ZCode 出口偏好:默认「直连优先、代理兜底」,可切「代理优先、直连兜底」;代理地址可面板配置(存 zcode-net.json,0600)
|
|
263
267
|
if (p === '/api/zcode/net' && method === 'GET') {
|
|
264
|
-
|
|
268
|
+
const u = proxyUrl();
|
|
269
|
+
const masked = u ? (() => { try { const x = new URL(u); x.password = x.password ? '*'.repeat(4) : ''; return x.toString(); } catch { return '***'; } })() : null;
|
|
270
|
+
return json(res, 200, { proxyFirst: proxyFirst(), proxyUrlMasked: masked, autoClaim: autoClaimEnabled(), hasEnvProxy: Boolean(process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy) });
|
|
265
271
|
}
|
|
266
272
|
if (p === '/api/zcode/net' && method === 'PUT') {
|
|
267
273
|
const body = await readBody(req).catch(() => ({}));
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
274
|
+
try {
|
|
275
|
+
if (body?.proxyFirst !== undefined) setProxyFirst(body.proxyFirst === true);
|
|
276
|
+
if (body?.proxyUrl !== undefined) setProxyUrl(body.proxyUrl);
|
|
277
|
+
if (body?.autoClaim !== undefined) setAutoClaimEnabled(body.autoClaim === true);
|
|
278
|
+
} catch (e) { return json(res, 400, { error: e.message }); }
|
|
279
|
+
logger.info('DAEMON', `ZCode 出口:${proxyFirst() ? '代理优先' : '直连优先'},自动领取:${autoClaimEnabled() ? '开' : '关'},代理 ${proxyUrl() || '(环境变量/未设置)'}`);
|
|
280
|
+
return json(res, 200, { proxyFirst: proxyFirst(), autoClaim: autoClaimEnabled() });
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
// 切换 WorkBuddy / ZCode 客户端当前登录账号
|
package/src/panel.html
CHANGED
|
@@ -89,18 +89,18 @@
|
|
|
89
89
|
html[data-theme="dark"] .badge.prod.workbuddy { color: #5eead4; }
|
|
90
90
|
.badge.prod.zcode { color: #1d4ed8; background: rgba(37,99,235,.1); }
|
|
91
91
|
html[data-theme="dark"] .badge.prod.zcode { color: #93c5fd; }
|
|
92
|
-
.pill.cur { background: rgba(20,184,166,.12); color: #0f766e; }
|
|
93
|
-
html[data-theme="dark"] .pill.cur { color: #5eead4; }
|
|
94
92
|
.seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
|
|
95
93
|
.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; }
|
|
96
94
|
.seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
97
95
|
html[data-theme="dark"] .seg-ctl button.active { background: rgba(255,255,255,.1); }
|
|
98
|
-
|
|
99
|
-
.pill
|
|
100
|
-
|
|
101
|
-
.pill.pending {
|
|
102
|
-
.pill.limited {
|
|
103
|
-
.pill.fail {
|
|
96
|
+
/* 胶囊徽章:不要类型背景色,细边框 + 彩色文字即可 */
|
|
97
|
+
.pill { display: inline-flex; align-items: center; height: 22px; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; border: 1px solid var(--border); background: transparent; color: var(--text-2); }
|
|
98
|
+
.pill.ok { color: var(--text); border-color: var(--text); }
|
|
99
|
+
.pill.pending { color: var(--text-3); }
|
|
100
|
+
.pill.limited { color: var(--warn); border-color: var(--warn-soft); }
|
|
101
|
+
.pill.fail { color: var(--err); border-color: var(--err-soft); }
|
|
102
|
+
.pill.cur { color: #0f766e; border-color: rgba(20,184,166,.5); }
|
|
103
|
+
html[data-theme="dark"] .pill.cur { color: #5eead4; }
|
|
104
104
|
.ops { display: flex; gap: 6px; flex-shrink: 0; }
|
|
105
105
|
|
|
106
106
|
.meta { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; font-size: 12px; }
|
|
@@ -184,6 +184,7 @@
|
|
|
184
184
|
.kpi-label { color: var(--text-3); font-size: 12px; font-weight: 500; }
|
|
185
185
|
.kpi-value { font-size: 26px; font-weight: 700; letter-spacing: -.5px; margin-top: 4px; font-variant-numeric: tabular-nums; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
186
186
|
.kpi-value small { font-size: 14px; color: var(--text-3); font-weight: 600; letter-spacing: 0; }
|
|
187
|
+
.kpi-value .kpi-side { font-size: 13px; color: var(--text-2); font-weight: 600; letter-spacing: 0; }
|
|
187
188
|
.kpi-sub { color: var(--text-3); font-size: 11px; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
188
189
|
.prods { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; margin-bottom: 10px; }
|
|
189
190
|
.prodcard { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; cursor: pointer; transition: border-color .15s, box-shadow .15s; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
|
@@ -215,9 +216,15 @@
|
|
|
215
216
|
.badge.prod.tenrouter { color: #b45309; background: rgba(245,158,11,.14); }
|
|
216
217
|
html[data-theme="dark"] .badge.prod.tenrouter { color: #fcd34d; }
|
|
217
218
|
/* 瀑布流:卡片高低差异大(积分包多的卡很长),按列填充避免大片空白 */
|
|
218
|
-
.tr-grid {
|
|
219
|
-
.tr-
|
|
220
|
-
.tr-
|
|
219
|
+
.tr-grid { padding: 12px; display: flex; flex-direction: column; gap: 14px; }
|
|
220
|
+
.tr-vrow { }
|
|
221
|
+
.tr-vhead { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
|
|
222
|
+
.tr-vhead .dim { font-size: 12px; }
|
|
223
|
+
.tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 12px 4px; }
|
|
224
|
+
.tr-vcards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; align-items: stretch; }
|
|
225
|
+
@media (max-width: 1180px) { .tr-vcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
226
|
+
@media (max-width: 820px) { .tr-vcards { grid-template-columns: 1fr; } }
|
|
227
|
+
.tr-card { border: 1px solid var(--border-subtle); border-radius: 12px; padding: 11px 12px 12px; background: var(--surface); min-width: 0; }
|
|
221
228
|
.tr-card.off { opacity: .6; }
|
|
222
229
|
.tr-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
223
230
|
.tr-title { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; }
|
|
@@ -230,6 +237,9 @@
|
|
|
230
237
|
.tr-meter { height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); overflow: hidden; }
|
|
231
238
|
.tr-meter span { display: block; height: 100%; border-radius: 3px; background: rgb(var(--accent-rgb)); }
|
|
232
239
|
.tr-meter span.low { background: var(--err); }
|
|
240
|
+
.tr-meter span.mid { background: #d97706; }
|
|
241
|
+
.tr-reset { color: var(--text-3); font-weight: 400; }
|
|
242
|
+
.tr-reset.soon { color: var(--err); font-weight: 600; }
|
|
233
243
|
.tr-reset { font-size: 11px; color: var(--text-3); margin-top: 3px; }
|
|
234
244
|
.tr-primary { margin: 2px 0 6px; }
|
|
235
245
|
.tr-primary .lbl { max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -241,6 +251,8 @@
|
|
|
241
251
|
.tr-p-val.low b { color: var(--err); }
|
|
242
252
|
.tr-meter.big { height: 7px; }
|
|
243
253
|
.tr-line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px; margin-top: 6px; }
|
|
254
|
+
.tr-summary { display: flex; align-items: baseline; gap: 6px; font-size: 12px; margin-top: 6px; }
|
|
255
|
+
.tr-sum-txt { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
244
256
|
.low-text { color: var(--err); font-weight: 600; }
|
|
245
257
|
.tr-q-when { color: var(--text-3); font-weight: 400; }
|
|
246
258
|
.tr-q.spent .tr-q-name, .tr-q.spent .tr-q-val { color: var(--text-3); }
|
|
@@ -270,7 +282,7 @@
|
|
|
270
282
|
<img class="logo" alt="" src="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiPjxwYXRoIGQ9Ik0zNzIuNDggMTM4LjY2NjY2N0ExMjggMTI4IDAgMCAwIDI5OC42NjY2NjcgMTAxLjU0NjY2N2ExMTcuNzYgMTE3Ljc2IDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzNsNzEuMjUzMzMzIDkxLjUyLTgxLjI4IDI4LjM3MzMzM2ExMC42NjY2NjcgMTAuNjY2NjY3IDAgMCAwLTIuMzQ2NjY3IDE4Ljk4NjY2NyAyNDIuNTYgMjQyLjU2IDAgMCAwIDY5Ljc2IDMyLjg1MzMzMyAyNzMuNzA2NjY3IDI3My43MDY2NjcgMCAwIDAgNTYuOTYgOC4zMnoiIGZpbGw9IiNGRjhBMTQiPjwvcGF0aD48cGF0aCBkPSJNMjQ1Ljc2IDEzOC4wMjY2NjdhMTMwLjk4NjY2NyAxMzAuOTg2NjY3IDAgMCAxIDUyLjA1MzMzMyAyLjEzMzMzM0ExNDguMDUzMzMzIDE0OC4wNTMzMzMgMCAwIDEgMzY2LjcyIDE3MC42NjY2NjdsNS43Ni0zMi44NTMzMzRBMTI4IDEyOCAwIDAgMCAyOTguNjY2NjY3IDEwMS41NDY2NjdhMTE3Ljk3MzMzMyAxMTcuOTczMzMzIDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzN6IiBmaWxsPSIjRkZBQTU0Ij48L3BhdGg+PHBhdGggZD0iTTIzNC42NjY2NjcgOTcwLjY2NjY2N2EyNzcuMzMzMzMzIDMyIDAgMSAwIDU1NC42NjY2NjYgMCAyNzcuMzMzMzMzIDMyIDAgMSAwLTU1NC42NjY2NjYgMFoiIGZpbGw9IiM0NTQxM0MiIG9wYWNpdHk9Ii4xNSI+PC9wYXRoPjxwYXRoIGQ9Ik02MjAuMzczMzMzIDQ2MC41ODY2NjdzMTUxLjA0LTkwLjY2NjY2NyAxODEuMzMzMzM0LTc0LjY2NjY2Ny0xMi41ODY2NjcgMTc0LjI5MzMzMy0xMDYuNjY2NjY3IDIxMy4zMzMzMzMtNzQuNjY2NjY3LTEzOC42NjY2NjctNzQuNjY2NjY3LTEzOC42NjY2NjZ6IiBmaWxsPSIjRkZFNTAwIj48L3BhdGg+PHBhdGggZD0iTTYyMC4zNzMzMzMgNTEzLjkyczE1MS4wNC05MC42NjY2NjcgMTgxLjMzMzMzNC03NC42NjY2NjdhMTQuNzIgMTQuNzIgMCAwIDEgNS4zMzMzMzMgNS45NzMzMzRjNi44MjY2NjctMjkuODY2NjY3IDUuMzMzMzMzLTUzLjU0NjY2Ny01LjMzMzMzMy01OS4zMDY2NjctMzAuMjkzMzMzLTE2LTE4MS4zMzMzMzMgNzQuNjY2NjY3LTE4MS4zMzMzMzQgNzQuNjY2NjY3YTM4Ni45ODY2NjcgMzg2Ljk4NjY2NyAwIDAgMCAwIDYxLjg2NjY2NmMtMC40MjY2NjctNS4zMzMzMzMgMC04LjUzMzMzMyAwLTguNTMzMzMzeiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik00MDMuNjI2NjY3IDQ2MC41ODY2NjdzLTE1MS4wNC05MC42NjY2NjctMTgxLjMzMzMzNC03NC42NjY2NjcgMTIuMzczMzMzIDE3NC4yOTMzMzMgMTA2LjY2NjY2NyAyMTMuMzMzMzMzIDc0LjY2NjY2Ny0xMzguNjY2NjY3IDc0LjY2NjY2Ny0xMzguNjY2NjY2eiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik0yMjIuMjkzMzMzIDQzOS4yNTMzMzNjMzAuMjkzMzMzLTE2IDE4MS4zMzMzMzMgNzQuNjY2NjY3IDE4MS4zMzMzMzQgNzQuNjY2NjY3djguNTMzMzMzYTM3My41NDY2NjcgMzczLjU0NjY2NyAwIDAgMCAwLTYxLjg2NjY2NnMtMTUxLjA0LTkwLjY2NjY2Ny0xODEuMzMzMzM0LTc0LjY2NjY2N2MtMTAuNjY2NjY3IDUuNzYtMTIuMTYgMjkuNDQtNS4zMzMzMzMgNTkuMzA2NjY3YTEzLjQ0IDEzLjQ0IDAgMCAxIDUuMzMzMzMzLTUuOTczMzM0eiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik0yODggNjUwLjY2NjY2N2EyNjYuNjY2NjY3IDIyNCA5MCAxIDAgNDQ4IDAgMjY2LjY2NjY2NyAyMjQgOTAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik02ODAuMTA2NjY3IDQ3NC4yNGEyMjQgMjI0IDAgMCAxLTMzNi4yMTMzMzQgMEEyMTAuNTYgMjEwLjU2IDAgMCAxIDUxMiAzODRhMjEwLjU2IDIxMC41NiAwIDAgMSAxNjguMTA2NjY3IDkwLjI0eiIgZmlsbD0iI0VCQ0IwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMjY2LjY2NjY2N20tMjI0IDBhMjI0IDIyNCAwIDEgMCA0NDggMCAyMjQgMjI0IDAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMTAxLjk3MzMzM2EyMjQuMjEzMzMzIDIyNC4yMTMzMzMgMCAwIDEgMjIxLjg2NjY2NyAxOTQuMzQ2NjY3IDIyNCAyMjQgMCAxIDAtNDQzLjczMzMzNCAwQTIyMy43ODY2NjcgMjIzLjc4NjY2NyAwIDAgMSA1MTIgMTAxLjk3MzMzM3oiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48cGF0aCBkPSJNODMzLjQ5MzMzMyA1OTguODI2NjY3bC03MC40IDcwLjRhMTguMzQ2NjY3IDE4LjM0NjY2NyAwIDAgMS0yNS44MTMzMzMgMEw2NDQuMDUzMzMzIDU3NkExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAwIDYxOC42NjY2NjcgNTc2bC05My4wMTMzMzQgOTMuMjI2NjY3YTE4Ljc3MzMzMyAxOC43NzMzMzMgMCAwIDEtMjYuMDI2NjY2IDBMNDA1LjMzMzMzMyA1NzZhMTguMTMzMzMzIDE4LjEzMzMzMyAwIDAgMC0yNS44MTMzMzMgMGwtOTMuMjI2NjY3IDkzLjIyNjY2N2ExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwbC03MC40LTcwLjRhOS4xNzMzMzMgOS4xNzMzMzMgMCAwIDAtMTUuNzg2NjY3IDYuODI2NjY2QzE4OS44NjY2NjcgODQxLjgxMzMzMyAzMzQuOTMzMzMzIDk2MCA1MTIgOTYwczMyMi4xMzMzMzMtMTE4LjE4NjY2NyAzMzcuMjgtMzU0LjM0NjY2N2E5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZ6IiBmaWxsPSIjRjBGMEYwIj48L3BhdGg+PHBhdGggZD0iTTE5MC41MDY2NjcgNjY1LjgxMzMzM2w3MC40IDYxLjIyNjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDI1LjgxMzMzMyAwbDkzLjIyNjY2Ny04MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMSAyNS44MTMzMzMgMGw5My4yMjY2NjcgODEuMDY2NjY3YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMjYuMDI2NjY2IDBsOTMuNjUzMzM0LTgxLjA2NjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAxIDI2LjAyNjY2NiAwbDkzLjIyNjY2NyA4MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMCAyNS44MTMzMzMgMGw3MC40LTYxLjIyNjY2N2E5LjgxMzMzMyA5LjgxMzMzMyAwIDAgMSA4LjUzMzMzNC0xLjkyIDUzNi4xMDY2NjcgNTM2LjEwNjY2NyAwIDAgMCA3LjI1MzMzMy01OC4yNCA5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZsLTcwLjQgNzAuNGExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwTDY0NC4wNTMzMzMgNTc2QTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDAgNjE4LjY2NjY2NyA1NzZsLTkzLjAxMzMzNCA5My4yMjY2NjdhMTguNzczMzMzIDE4Ljc3MzMzMyAwIDAgMS0yNi4wMjY2NjYgMEw0MDUuMzMzMzMzIDU3NmExOC4xMzMzMzMgMTguMTMzMzMzIDAgMCAwLTI1LjgxMzMzMyAwbC05My4yMjY2NjcgOTMuMjI2NjY3YTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDEtMjUuODEzMzMzIDBsLTcwLjQtNzAuNGE5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjcgNi44MjY2NjYgNTM3LjE3MzMzMyA1MzcuMTczMzMzIDAgMCAwIDcuMDQgNTguMjQgMTAuMDI2NjY3IDEwLjAyNjY2NyAwIDAgMSA5LjE3MzMzNCAxLjkyeiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPjxwYXRoIGQ9Ik00OTguMTMzMzMzIDE4NC41MzMzMzNtLTI5LjAxMzMzMyAwYTI5LjAxMzMzMyAyOS4wMTMzMzMgMCAxIDAgNTguMDI2NjY3IDAgMjkuMDEzMzMzIDI5LjAxMzMzMyAwIDEgMC01OC4wMjY2NjcgMFoiIGZpbGw9IiM0NTQxM0MiPjwvcGF0aD48cGF0aCBkPSJNNTc2IDI5MC45ODY2NjdjMCAxMS43MzMzMzMtMTUuNzg2NjY3IDIxLjMzMzMzMy0zNC45ODY2NjcgMjEuMzMzMzMzcy0zNS4yLTkuNi0zNS4yLTIxLjMzMzMzMyAxNS43ODY2NjctMjEuMzMzMzMzIDM1LjItMjEuMzMzMzM0IDM0Ljk4NjY2NyA5LjYgMzQuOTg2NjY3IDIxLjMzMzMzNHoiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48L3N2Zz4=">
|
|
271
283
|
<div>
|
|
272
284
|
<div class="title">CreditDaddy</div>
|
|
273
|
-
<div class="ver" id="ver">Qoder / WorkBuddy / ZCode 多账号管理 ·
|
|
285
|
+
<div class="ver" id="ver">Qoder / WorkBuddy / ZCode 多账号管理 · 每日积分自动领取</div>
|
|
274
286
|
</div>
|
|
275
287
|
</div>
|
|
276
288
|
<div class="head-right">
|
|
@@ -302,8 +314,8 @@
|
|
|
302
314
|
<div class="kpis" id="kpis"></div>
|
|
303
315
|
<div class="prods" id="prods"></div>
|
|
304
316
|
<div class="dash-cols">
|
|
317
|
+
<section class="pcard"><div class="pcard-title"><span>最近记录</span><span class="hint" id="last-tick"></span></div><div id="recent"></div></section>
|
|
305
318
|
<section class="pcard"><div class="pcard-title"><span>需要处理</span><span class="hint" id="att-count"></span></div><div id="attention"></div></section>
|
|
306
|
-
<section class="pcard"><div class="pcard-title"><span>最近签到</span><span class="hint" id="last-tick"></span></div><div id="recent"></div></section>
|
|
307
319
|
</div>
|
|
308
320
|
</section>
|
|
309
321
|
|
|
@@ -315,7 +327,7 @@
|
|
|
315
327
|
<div class="stat"><span>账号</span><strong id="s-count">0</strong></div>
|
|
316
328
|
<div class="divider"></div>
|
|
317
329
|
<div id="s-done-wrap" style="display:contents">
|
|
318
|
-
<div class="stat"><span
|
|
330
|
+
<div class="stat"><span>今日已领</span><strong id="s-done">0</strong></div>
|
|
319
331
|
<div class="divider"></div>
|
|
320
332
|
</div>
|
|
321
333
|
<div class="stat"><span id="s-credits-label">剩余积分</span><strong id="s-credits">-</strong></div>
|
|
@@ -434,6 +446,9 @@
|
|
|
434
446
|
<div id="tr-sources" style="margin-top:6px"></div>
|
|
435
447
|
<div class="hint" style="margin-top:4px">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。10Router 按行签名去重,重复同步不会产生重复数据。</div>
|
|
436
448
|
</div>
|
|
449
|
+
<div class="field"><label>显示</label>
|
|
450
|
+
<div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
|
|
451
|
+
</div>
|
|
437
452
|
<div class="hint" id="tr-test-msg"></div>
|
|
438
453
|
<div class="modal-actions">
|
|
439
454
|
<button class="btn" id="tr-disconnect" style="margin-right:auto">断开</button>
|
|
@@ -583,6 +598,9 @@ async function api(p, opts = {}, attempt = 0) {
|
|
|
583
598
|
return data;
|
|
584
599
|
}
|
|
585
600
|
|
|
601
|
+
// 10Router 汇总卡「隐藏无额度」偏好(面板本地)
|
|
602
|
+
try { state.trHideEmpty = localStorage.getItem('qd_tr_hideempty') === '1'; } catch (e) {}
|
|
603
|
+
|
|
586
604
|
// ── 主题 ──
|
|
587
605
|
function applyTheme(t) {
|
|
588
606
|
document.documentElement.dataset.theme = t;
|
|
@@ -601,22 +619,25 @@ function initTheme() {
|
|
|
601
619
|
}
|
|
602
620
|
|
|
603
621
|
// ── 状态 / 渲染 ──
|
|
622
|
+
// 统一「领鸡蛋」语义:Qoder / WorkBuddy 国内/国际 = 领取积分;workbuddy-intl = 保持活跃(不保证奖励)
|
|
604
623
|
function statusOf(a) {
|
|
605
|
-
// ZCode
|
|
606
|
-
|
|
624
|
+
if (!CHECKIN_PRODUCTS.has(a.product)) return { cls: '', text: '', msg: '' }; // ZCode 无每日领取,不显示徽标
|
|
625
|
+
const intl = a.provider === 'workbuddy-intl';
|
|
607
626
|
const today = state.status && state.status.today;
|
|
608
627
|
const done = state.status && state.status.todayDone && state.status.todayDone[a.id] === today;
|
|
609
628
|
const r = a.lastResult;
|
|
610
629
|
const fresh = r && r.at && state.status && (Date.now() - new Date(r.at).getTime() < 26 * 3600e3);
|
|
611
630
|
if (r && fresh && r.status === 'limited' && done) return { cls: 'limited', text: '本机已领', msg: r.message };
|
|
612
631
|
if (done) {
|
|
632
|
+
if (intl) return { cls: 'ok', text: '今日活跃', msg: r && r.message ? r.message : '' };
|
|
613
633
|
const amt = r && r.status === 'checked-in' && fresh && r.amount ? ' +' + fmtNum(r.amount) : '';
|
|
614
|
-
const streak = r &&
|
|
615
|
-
return { cls: 'ok', text: '
|
|
634
|
+
const streak = r && !intl && r.streakDays ? ' · 连续 ' + r.streakDays + ' 天' : '';
|
|
635
|
+
return { cls: 'ok', text: '今日已领' + amt + streak, msg: r && r.message && !['checked-in', 'already'].includes(r.status) ? r.message : '' };
|
|
616
636
|
}
|
|
617
|
-
|
|
618
|
-
if (r && fresh && r.status === '
|
|
619
|
-
return { cls: 'pending', text: '
|
|
637
|
+
const failText = intl ? '活跃失败' : '领取失败';
|
|
638
|
+
if (r && fresh && r.status === 'failed') return { cls: 'fail', text: failText, msg: r.message, err: true };
|
|
639
|
+
if (r && fresh && r.status === 'no-activity') return { cls: 'pending', text: intl ? '待活跃' : '待领取', msg: r.message };
|
|
640
|
+
return { cls: 'pending', text: intl ? '未活跃' : '未领取', msg: '' };
|
|
620
641
|
}
|
|
621
642
|
|
|
622
643
|
function expiryCell(a) {
|
|
@@ -675,7 +696,7 @@ function cardHtml(a) {
|
|
|
675
696
|
+ (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>' : '')
|
|
676
697
|
+ '</div><div class="ops">'
|
|
677
698
|
+ (a.product === 'zcode' ? '<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">' + I.gift + '</button>' : '')
|
|
678
|
-
+ (canCheckin ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="
|
|
699
|
+
+ (canCheckin ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + (a.provider === 'workbuddy-intl' ? '发送活跃请求' : '立即领取') + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
|
|
679
700
|
+ (CLIENT_LABEL[a.product] && a.canSwitch && !isCurrent && st.cls !== 'fail' ? '<button class="icon-btn" data-act="switch" title="切换 ' + CLIENT_LABEL[a.product] + ' 客户端到此账号">' + I.swap + '</button>' : '')
|
|
680
701
|
+ '<button class="icon-btn" data-act="quota" title="刷新积分">' + I.refresh + '</button>'
|
|
681
702
|
+ '<button class="icon-btn del" data-act="del" title="删除(点两次确认)">' + I.trash + '</button>'
|
|
@@ -741,7 +762,17 @@ function renderProd() {
|
|
|
741
762
|
const canCheckin = CHECKIN_PRODUCTS.has(prod);
|
|
742
763
|
$('s-count').textContent = acc.length;
|
|
743
764
|
$('s-done-wrap').style.display = canCheckin ? 'contents' : 'none';
|
|
744
|
-
|
|
765
|
+
if (prod === 'workbuddy') {
|
|
766
|
+
// 国内版 = 每日签到送积分;国际版 = 保持活跃(活跃不等于必有奖励)
|
|
767
|
+
const cnAcc = acc.filter((a) => a.provider === 'workbuddy');
|
|
768
|
+
const intlAcc = acc.filter((a) => a.provider === 'workbuddy-intl');
|
|
769
|
+
$('s-done-wrap').innerHTML = (cnAcc.length ? '<div class="stat"><span>领取(国内)</span><strong>' + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '')
|
|
770
|
+
+ (intlAcc.length ? '<div class="stat"><span>活跃(国际)</span><strong>' + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
|
|
771
|
+
} else {
|
|
772
|
+
// 从 WorkBuddy 页切回来时 s-done 可能被 innerHTML 换没了,先重建结构再写值
|
|
773
|
+
if (!$('s-done')) $('s-done-wrap').innerHTML = '<div class="stat"><span>今日已领</span><strong id="s-done"></strong></div><div class="divider"></div>';
|
|
774
|
+
$('s-done').textContent = acc.filter(isDone).length + ' / ' + acc.length;
|
|
775
|
+
}
|
|
745
776
|
const c = creditSum(acc);
|
|
746
777
|
$('s-credits-label').textContent = prod === 'zcode' ? '有套餐' : '剩余积分';
|
|
747
778
|
$('s-credits').textContent = prod === 'zcode'
|
|
@@ -752,7 +783,10 @@ function renderProd() {
|
|
|
752
783
|
$('region-div').style.display = hasRegions ? '' : 'none';
|
|
753
784
|
document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
|
|
754
785
|
$('btn-checkin').style.display = canCheckin ? '' : 'none';
|
|
755
|
-
if (!$('btn-checkin').disabled)
|
|
786
|
+
if (!$('btn-checkin').disabled) {
|
|
787
|
+
const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl');
|
|
788
|
+
$('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? '领取 / 活跃全部' : '领取全部 ' + prodLabel(prod));
|
|
789
|
+
}
|
|
756
790
|
}
|
|
757
791
|
|
|
758
792
|
function kpi(label, value, sub) {
|
|
@@ -769,12 +803,21 @@ function renderDash() {
|
|
|
769
803
|
const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
|
|
770
804
|
const byProd = PRODUCTS.map((p) => p.label + ' ' + ofProduct(p.id).length).join(' · ');
|
|
771
805
|
const creditByProd = ['qoder', 'workbuddy'].map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
|
|
806
|
+
// 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
|
|
807
|
+
const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
|
|
808
|
+
const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
|
|
809
|
+
const doneDom = domAcc.filter(isDone).length;
|
|
810
|
+
const doneIntl = intlAcc.filter(isDone).length;
|
|
772
811
|
$('kpis').innerHTML =
|
|
773
812
|
kpi('账号', state.accounts.length, byProd)
|
|
774
|
-
+ kpi('
|
|
775
|
-
|
|
813
|
+
+ kpi('今日领取' + (intlAcc.length ? ' / 活跃' : ''), intlAcc.length
|
|
814
|
+
? doneDom + ' <small>/ ' + domAcc.length + '</small><small class="kpi-side"> · 活跃 ' + doneIntl + ' / ' + intlAcc.length + '</small>'
|
|
815
|
+
: done + ' <small>/ ' + signable.length + '</small>',
|
|
816
|
+
!signable.length ? '暂无可领取的账号'
|
|
817
|
+
: (doneDom === domAcc.length && doneIntl === intlAcc.length ? (intlAcc.length ? '今天全部完成(国际为活跃请求,不保证奖励)' : '今天全部签完')
|
|
818
|
+
: '待办 ' + (signable.length - done) + (failed ? ' · 失败 ' + failed : '')))
|
|
776
819
|
+ kpi('剩余积分', credit.known ? fmtNum(credit.sum) : '-', credit.loading ? '读取中(' + credit.loading + ' 个账号)…' : creditByProd)
|
|
777
|
-
+ kpi('
|
|
820
|
+
+ kpi('下次自动领取', sch.ticking ? '领取中…' : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
|
|
778
821
|
sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : '每 ~2 小时一轮');
|
|
779
822
|
|
|
780
823
|
$('prods').innerHTML = PRODUCTS.map((p) => {
|
|
@@ -785,12 +828,21 @@ function renderDash() {
|
|
|
785
828
|
} else if (CHECKIN_PRODUCTS.has(p.id)) {
|
|
786
829
|
const d = list.filter(isDone).length;
|
|
787
830
|
const c = creditSum(list);
|
|
788
|
-
|
|
789
|
-
|
|
831
|
+
if (p.id === 'workbuddy') {
|
|
832
|
+
// 国内 = 每日签到;国际 = 保持活跃(活跃不等于必有奖励)
|
|
833
|
+
const cnList = list.filter((a) => a.provider === 'workbuddy');
|
|
834
|
+
const intlList = list.filter((a) => a.provider === 'workbuddy-intl');
|
|
835
|
+
if (cnList.length) rows += '<span class="lbl">领取(国内)</span><span class="val">' + cnList.filter(isDone).length + ' / ' + cnList.length + '</span>';
|
|
836
|
+
if (intlList.length) rows += '<span class="lbl">活跃(国际)</span><span class="val">' + intlList.filter(isDone).length + ' / ' + intlList.length + '(不保证奖励)</span>';
|
|
837
|
+
} else {
|
|
838
|
+
rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>';
|
|
839
|
+
}
|
|
840
|
+
rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
|
|
790
841
|
+ '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
|
|
791
842
|
} else {
|
|
792
843
|
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
793
|
-
|
|
844
|
+
// ZCode 没有每日签到:不展示签到语义的行
|
|
845
|
+
rows = '<span class="lbl">活动领取</span><span class="val dim">手动领取,或在标签页开自动轮询</span>'
|
|
794
846
|
+ '<span class="lbl">有效套餐</span><span class="val">' + planned + ' / ' + list.length + '</span>';
|
|
795
847
|
}
|
|
796
848
|
if (p.id === 'qoder' && list.some((a) => a.provider === 'qoder')) {
|
|
@@ -808,7 +860,7 @@ function renderDash() {
|
|
|
808
860
|
+ '<span class="badge">' + list.length + ' 个账号</span></span><span class="prod-go">查看 →</span></div><div class="prod-rows">' + rows + '</div></div>';
|
|
809
861
|
}).join('') + trDashCard();
|
|
810
862
|
|
|
811
|
-
// 需要处理:token 失效 /
|
|
863
|
+
// 需要处理:token 失效 / 即将过期、领取失败、国际版缺风控身份、10Router 额度告急 / 查询失败
|
|
812
864
|
const att = [];
|
|
813
865
|
if (!s.riskIdentity && state.accounts.some((a) => a.provider === 'qoder')) {
|
|
814
866
|
att.push({ view: 'qoder', name: 'Qoder 国际版', badges: '', pill: 'limited', text: umidOffer(s) ? '安装设备身份组件后才能领取' : '未检测到 Qoder 客户端,国际版无法领取' });
|
|
@@ -820,7 +872,7 @@ function renderDash() {
|
|
|
820
872
|
if (a.verified === false) att.push(Object.assign({ pill: 'fail', text: 'token 无效' }, base));
|
|
821
873
|
else if (ex.label === '已过期') att.push(Object.assign({ pill: 'fail', text: 'token 已过期' }, base));
|
|
822
874
|
else if (ex.warn) att.push(Object.assign({ pill: 'limited', text: ex.label.slice(5, 10) + ' 过期' }, base));
|
|
823
|
-
if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: '
|
|
875
|
+
if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: a.provider === 'workbuddy-intl' ? '活跃失败' : '领取失败', title: st.msg }, base));
|
|
824
876
|
}
|
|
825
877
|
for (const c of (tr.quotas && tr.quotas.connections) || []) {
|
|
826
878
|
const base = { view: 'tenrouter', name: c.name || c.email || c.providerName, badges: '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>' };
|
|
@@ -834,19 +886,21 @@ function renderDash() {
|
|
|
834
886
|
+ (att.length > 8 ? '<div class="muted-empty">还有 ' + (att.length - 8) + ' 项</div>' : '')
|
|
835
887
|
: '<div class="muted-empty">' + (state.accounts.length ? '✓ 一切正常' : '还没有账号') + '</div>';
|
|
836
888
|
|
|
837
|
-
//
|
|
889
|
+
// 最近领取记录
|
|
838
890
|
const RES = { 'checked-in': ['ok', '领取成功'], already: ['ok', '已领过'], limited: ['limited', '本机已领'], 'no-activity': ['pending', '无可领活动'], failed: ['fail', '失败'] };
|
|
839
891
|
const recent = state.accounts.filter((a) => a.lastResult && a.lastResult.at).sort((x, y) => String(y.lastResult.at).localeCompare(String(x.lastResult.at))).slice(0, 8);
|
|
840
892
|
$('last-tick').textContent = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) : '';
|
|
841
893
|
$('recent').innerHTML = recent.length
|
|
842
894
|
? recent.map((a) => {
|
|
843
895
|
const r = a.lastResult;
|
|
844
|
-
|
|
896
|
+
let [cls, txt] = RES[r.status] || ['pending', r.status];
|
|
897
|
+
// 国际版 WorkBuddy 是保持活跃(不保证奖励),不叫「领取」
|
|
898
|
+
if (a.provider === 'workbuddy-intl' && r.status === 'checked-in') txt = '活跃达成';
|
|
845
899
|
const label = r.status === 'checked-in' && r.amount ? txt + ' +' + fmtNum(r.amount) : txt;
|
|
846
900
|
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>'
|
|
847
901
|
+ '<span class="item-side"><span class="pill ' + cls + '">' + esc(label) + '</span><span class="item-time">' + fmtTime(r.at).slice(5) + '</span></span></div>';
|
|
848
902
|
}).join('')
|
|
849
|
-
: '<div class="muted-empty"
|
|
903
|
+
: '<div class="muted-empty">还没有领取记录</div>';
|
|
850
904
|
}
|
|
851
905
|
|
|
852
906
|
function renderStatus() {
|
|
@@ -856,8 +910,8 @@ function renderStatus() {
|
|
|
856
910
|
document.title = 'CreditDaddy v' + s.version;
|
|
857
911
|
if (s.homepage) $('btn-home').href = s.homepage;
|
|
858
912
|
const sch = s.scheduler || {};
|
|
859
|
-
$('sched').textContent = sch.ticking ? '
|
|
860
|
-
: sch.nextTickAt ? '
|
|
913
|
+
$('sched').textContent = sch.ticking ? '正在领取…'
|
|
914
|
+
: sch.nextTickAt ? '下次自动领取 ' + fmtTime(sch.nextTickAt).slice(11) : '';
|
|
861
915
|
$('sched').title = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at) + ':' + sch.lastTick.summary : '';
|
|
862
916
|
// 产品相关提示只在对应标签页出现
|
|
863
917
|
const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
|
|
@@ -866,17 +920,21 @@ function renderStatus() {
|
|
|
866
920
|
if (state.view === 'zcode') {
|
|
867
921
|
n.style.display = '';
|
|
868
922
|
const pf = state.zProxyFirst === true;
|
|
869
|
-
|
|
870
|
-
|
|
923
|
+
const pu = state.zProxyUrl;
|
|
924
|
+
const ac = state.zAutoClaim === true;
|
|
925
|
+
n.innerHTML = 'ZCode 没有每日签到,积分靠「活动领取」。打开「自动领取」后,每轮签到结束会轮询可领活动并尝试领取(桌面版静默过验证码,需要人工时弹验证码窗口);平时保持关闭即可,活动期前再开。也可点卡片上的礼物图标手动领取。'
|
|
926
|
+
+ ' <button class="btn" id="btn-zac" style="margin-left:6px" title="ZCode 活动自动轮询领取(每轮签到后执行一次)">' + (ac ? '自动领取:开' : '自动领取:关') + '</button>'
|
|
927
|
+
+ ' <button class="btn" id="btn-znet" style="margin-left:4px" title="ZCode 上游接口的访问顺序:失败 / 被拦时自动回退另一条">出口:' + (pf ? '代理优先' : '直连优先') + '</button>'
|
|
928
|
+
+ ' <button class="btn" id="btn-zproxy" style="margin-left:4px" title="设置 ZCode 出口用的 http 代理(留空则清除;不设置时使用 HTTPS_PROXY 环境变量)">代理:' + esc(pu || (state.zHasEnvProxy ? '环境变量' : '未设置')) + '</button>';
|
|
871
929
|
} else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
|
|
872
930
|
n.style.display = '';
|
|
873
931
|
const busy = s.umid.installing || umidBusy;
|
|
874
932
|
n.innerHTML = 'Qoder 国际版只向带设备风控身份的请求下发「每天领 100 Credits」。这台 Linux / NAS 上没有 Qoder 客户端,'
|
|
875
|
-
+ '可以安装「设备身份组件」:从 npm 官方包 @qoder-ai/qodercli 提取其内置的 UMID 程序(约 30MB 下载,校验 npm
|
|
933
|
+
+ '可以安装「设备身份组件」:从 npm 官方包 @qoder-ai/qodercli 提取其内置的 UMID 程序(约 30MB 下载,校验 npm 完整性),装好后国际版即可领取(本机每天限领一个账号)。'
|
|
876
934
|
+ ' <button class="btn primary" id="btn-umid" style="margin-left:6px"' + (busy ? ' disabled' : '') + '>' + (busy ? '安装中…' : '安装设备身份组件') + '</button>';
|
|
877
935
|
} else if (hasIntl && !s.riskIdentity) {
|
|
878
936
|
n.style.display = '';
|
|
879
|
-
n.textContent = 'Qoder
|
|
937
|
+
n.textContent = 'Qoder 国际版领取需要本机安装 Qoder 客户端:Qoder 只向带设备风控身份(由客户端生成)的请求下发「每天领 100 Credits」。当前未检测到客户端,国际版账号将无法领取。';
|
|
880
938
|
} else if (hasIntl) {
|
|
881
939
|
n.style.display = '';
|
|
882
940
|
n.textContent = '提示:Qoder 国际版每台设备每天只能有一个账号领取每日 100 Credits(服务端按设备限领),其余国际版账号会显示「本机已领」。'
|
|
@@ -893,23 +951,51 @@ async function installUmid() {
|
|
|
893
951
|
umidBusy = true; renderStatus();
|
|
894
952
|
try {
|
|
895
953
|
const r = await api('/api/qoder/umid/install', { method: 'POST' });
|
|
896
|
-
toast('设备身份组件已安装(qodercli ' + r.installed.version + '),Qoder
|
|
954
|
+
toast('设备身份组件已安装(qodercli ' + r.installed.version + '),Qoder 国际版可以领取了');
|
|
897
955
|
} catch (e) { toast(e.message, false); }
|
|
898
956
|
umidBusy = false;
|
|
899
957
|
await loadStatus(); render(); loadLogs();
|
|
900
958
|
}
|
|
901
959
|
$('notice').addEventListener('click', (e) => { if (e.target.closest('#btn-umid')) installUmid(); });
|
|
902
960
|
$('notice').addEventListener('click', async (e) => {
|
|
903
|
-
if (
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
961
|
+
if (e.target.closest('#btn-zac')) {
|
|
962
|
+
try {
|
|
963
|
+
const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: state.zAutoClaim !== true } });
|
|
964
|
+
state.zAutoClaim = r.autoClaim;
|
|
965
|
+
renderStatus();
|
|
966
|
+
toast('ZCode 自动轮询领取:' + (r.autoClaim ? '已开启(每轮签到后执行)' : '已关闭'));
|
|
967
|
+
} catch (err) { toast(err.message, false); }
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (e.target.closest('#btn-znet')) {
|
|
971
|
+
try {
|
|
972
|
+
const r = await api('/api/zcode/net', { method: 'PUT', json: { proxyFirst: state.zProxyFirst !== true } });
|
|
973
|
+
state.zProxyFirst = r.proxyFirst;
|
|
974
|
+
renderStatus();
|
|
975
|
+
toast('ZCode 出口:' + (r.proxyFirst ? '代理优先(直连兜底)' : '直连优先(代理兜底)'));
|
|
976
|
+
} catch (err) { toast(err.message, false); }
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (e.target.closest('#btn-zproxy')) {
|
|
980
|
+
const cur = state.zProxyUrl || '';
|
|
981
|
+
const v = await uiPrompt('ZCode 出口代理(http://host:port,支持口令 http://user:pass@host:port)\n留空 = 清除,退回 HTTPS_PROXY 环境变量', cur, { okText: '保存' });
|
|
982
|
+
if (v === null) return;
|
|
983
|
+
try {
|
|
984
|
+
await api('/api/zcode/net', { method: 'PUT', json: { proxyUrl: v.trim() } });
|
|
985
|
+
await loadZNet(); renderStatus();
|
|
986
|
+
toast(v.trim() ? '代理已保存' : '代理已清除');
|
|
987
|
+
} catch (err) { toast(err.message, false); }
|
|
988
|
+
}
|
|
910
989
|
});
|
|
911
990
|
async function loadZNet() {
|
|
912
|
-
try {
|
|
991
|
+
try {
|
|
992
|
+
const d = await api('/api/zcode/net');
|
|
993
|
+
state.zProxyFirst = d.proxyFirst === true;
|
|
994
|
+
state.zProxyUrl = d.proxyUrlMasked || null;
|
|
995
|
+
state.zAutoClaim = d.autoClaim === true;
|
|
996
|
+
state.zHasEnvProxy = d.hasEnvProxy === true;
|
|
997
|
+
if (state.view === 'zcode') renderStatus();
|
|
998
|
+
} catch (e) {}
|
|
913
999
|
}
|
|
914
1000
|
|
|
915
1001
|
// ── 10Router ──
|
|
@@ -948,38 +1034,61 @@ function quotaValue(q) {
|
|
|
948
1034
|
}
|
|
949
1035
|
const resetLabel = (q) => (q.resetAt ? fmtTime(q.resetAt).slice(5) + (q.recurring === false ? ' 到期' : ' 重置') : '');
|
|
950
1036
|
|
|
1037
|
+
// 相对重置标签(CLIProxyAPI 管理中心风格):「3 天 后重置」「45 分钟 后到期」;一小时内标红,悬停看绝对时间
|
|
1038
|
+
function relReset(q) {
|
|
1039
|
+
if (!q.resetAt) return null;
|
|
1040
|
+
const ms = new Date(q.resetAt).getTime() - Date.now();
|
|
1041
|
+
if (!Number.isFinite(ms)) return null;
|
|
1042
|
+
if (ms <= 0) return { text: '即将' + (q.recurring === false ? '到期' : '重置'), soon: true };
|
|
1043
|
+
const m = Math.ceil(ms / 60000);
|
|
1044
|
+
const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60), mm = m % 60;
|
|
1045
|
+
const dur = d > 0 ? d + ' 天' + (h ? h + ' 小时' : '') : h > 0 ? h + ' 小时' + (mm ? mm + ' 分' : '') : Math.max(1, mm) + ' 分钟';
|
|
1046
|
+
return { text: dur + ' 后' + (q.recurring === false ? '到期' : '重置'), soon: ms < 3600e3 };
|
|
1047
|
+
}
|
|
1048
|
+
function relResetHtml(q) {
|
|
1049
|
+
const r = relReset(q);
|
|
1050
|
+
return r ? '<span class="tr-reset' + (r.soon ? ' soon' : '') + '" title="' + esc(fmtTime(q.resetAt)) + '">' + esc(r.text) + '</span>' : '';
|
|
1051
|
+
}
|
|
1052
|
+
// 三档着色水位条(≥70 绿 / ≥30 琥珀 / <30 红)
|
|
1053
|
+
const meterCls = (pct) => (pct >= 70 ? 'hi' : pct >= 30 ? 'mid' : 'low');
|
|
1054
|
+
function trMeter(pct) {
|
|
1055
|
+
if (pct === null) return '';
|
|
1056
|
+
const w = Math.min(100, Math.max(0, pct)).toFixed(1);
|
|
1057
|
+
return '<div class="tr-meter"><span class="' + meterCls(pct) + '" style="width:' + w + '%"></span></div>';
|
|
1058
|
+
}
|
|
1059
|
+
// 明细整行:名称 + 数值/相对重置 + 水位条(CLIProxyAPI quotaRow 版式)
|
|
1060
|
+
function trRow(q) {
|
|
1061
|
+
return trLine(q.name, lineVal(q)) + trMeter(quotaPct(q));
|
|
1062
|
+
}
|
|
1063
|
+
|
|
951
1064
|
// 紧凑行(周期额度 / 聚合积分包):名称在左、数值在右,不带进度条
|
|
952
1065
|
function trLine(name, valHtml, title) {
|
|
953
1066
|
return '<div class="tr-line"><span class="tr-q-name"' + (title ? ' title="' + esc(title) + '"' : '') + '>' + esc(name) + '</span><span class="tr-q-val">' + valHtml + '</span></div>';
|
|
954
1067
|
}
|
|
955
1068
|
function lineVal(q) {
|
|
956
1069
|
const pct = quotaPct(q);
|
|
957
|
-
const low = pct !== null && pct < LOW_PCT && q.percentScale;
|
|
958
1070
|
const txt = (q.percentScale || !(q.total > 0))
|
|
959
1071
|
? '剩 ' + (pct !== null ? fmtNum(pct) + '%' : fmtNum(quotaRemaining(q)))
|
|
960
1072
|
: '剩 ' + fmtNum(quotaRemaining(q)) + ' / ' + fmtNum(q.total || 0) + (q.unit ? ' ' + q.unit : '');
|
|
961
|
-
return '<span
|
|
1073
|
+
return '<span>' + esc(txt) + '</span> ' + relResetHtml(q);
|
|
962
1074
|
}
|
|
963
1075
|
|
|
964
|
-
//
|
|
1076
|
+
// 主额度行:名称 + 大值 + 相对重置 + 三档水位条
|
|
965
1077
|
function trPrimaryHtml(q) {
|
|
966
1078
|
const v = quotaValue(q);
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
+ '<span class="credit dim">' + (q.resetAt ? esc(resetLabel(q)) : '') + '</span></div>'
|
|
971
|
-
+ (pct01 !== null ? '<div class="bar"><span class="seg" style="flex:' + pct01.toFixed(4) + ' 1 0"></span><span class="seg used" style="flex:' + (1 - pct01).toFixed(4) + ' 1 0"></span></div>' : '')
|
|
1079
|
+
return '<div class="tr-primary"><div class="credit-line"><span class="credit"><span class="lbl">' + esc(q.name) + '</span><b>' + esc(v.big) + '</b> <small class="dim">' + esc(v.small) + '</small></span>'
|
|
1080
|
+
+ '<span class="credit dim">' + relResetHtml(q) + '</span></div>'
|
|
1081
|
+
+ (v.pct !== null ? trMeter(v.pct) : '')
|
|
972
1082
|
+ '</div>';
|
|
973
1083
|
}
|
|
974
1084
|
|
|
975
1085
|
function trQuotaHtml(q, { dimWhenEmpty = false } = {}) {
|
|
976
1086
|
const v = quotaValue(q);
|
|
977
1087
|
const used = v.pct !== null && v.pct <= 0;
|
|
978
|
-
const low = !used && v.pct !== null && v.pct < LOW_PCT && q.percentScale;
|
|
979
1088
|
const val = used && dimWhenEmpty ? '已用完' : (q.percentScale || !(q.total > 0) ? '剩余 ' + v.big : '剩余 ' + v.big + ' ' + v.small);
|
|
980
1089
|
return '<div class="tr-q' + (used && dimWhenEmpty ? ' spent' : '') + '"><div class="tr-q-line"><span class="tr-q-name" title="' + esc(q.name) + '">' + esc(q.name) + '</span>'
|
|
981
|
-
+ '<span class="tr-q-val
|
|
982
|
-
+ (
|
|
1090
|
+
+ '<span class="tr-q-val">' + esc(val) + relResetHtml(q) + '</span></div>'
|
|
1091
|
+
+ (!(used && dimWhenEmpty) ? trMeter(v.pct) : '')
|
|
983
1092
|
+ '</div>';
|
|
984
1093
|
}
|
|
985
1094
|
|
|
@@ -1034,19 +1143,15 @@ function trCardHtml(c) {
|
|
|
1034
1143
|
if (!(primary || windows.length || packs.length)) {
|
|
1035
1144
|
body = '<div class="msg">' + esc(/no separate quota|billed at standard/i.test(c.message || '') ? '按量计费,无额度可显示' : (c.message || '该供应商未返回额度数据')) + '</div>';
|
|
1036
1145
|
} else {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
+ '<div class="mi"><span class="lbl">额度</span><span class="val">' + quotaCount + ' 项</span></div>'
|
|
1041
|
-
+ '<div class="mi right"><span class="lbl">最早到期</span><span class="val' + (resets[0] && new Date(resets[0]) - Date.now() < 7 * 86400e3 ? ' warn' : '') + '">' + (resets[0] ? fmtTime(resets[0]).slice(5, 10) : '—') + '</span></div>'
|
|
1042
|
-
+ '</div>'
|
|
1043
|
-
+ (primary ? trPrimaryHtml(primary) : '')
|
|
1044
|
-
+ windows.map((q) => trLine(q.name, lineVal(q))).join('')
|
|
1146
|
+
// 与汇总卡同款默认视图:主额度 + 每个周 / 月窗口额度一条(带进度条);积分包走聚合行,「逐包明细」展开单包
|
|
1147
|
+
body = (primary ? trPrimaryHtml(primary) : '')
|
|
1148
|
+
+ windows.map((q) => trRow(q)).join('')
|
|
1045
1149
|
+ trPacksHtml(c, packs);
|
|
1046
1150
|
}
|
|
1047
1151
|
}
|
|
1048
1152
|
return '<div class="tr-card' + (c.isActive ? '' : ' off') + '"><div class="row1"><div class="name-group">'
|
|
1049
1153
|
+ '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>'
|
|
1154
|
+
+ (TR_REGION[c.provider] ? '<span class="badge ' + TR_REGION[c.provider][1] + '">' + TR_REGION[c.provider][0] + '</span>' : '')
|
|
1050
1155
|
+ '<span class="tr-name" title="' + esc([c.name, c.email].filter(Boolean).join(' · ')) + '">' + esc(who) + '</span>'
|
|
1051
1156
|
+ (showPlan ? '<span class="badge">' + esc(c.plan) + '</span>' : '')
|
|
1052
1157
|
+ (c.isActive ? '' : '<span class="pill pending">已停用</span>')
|
|
@@ -1059,6 +1164,119 @@ function trCardHtml(c) {
|
|
|
1059
1164
|
const trSorted = (list) => list.slice().sort((a, b) => (b.isActive - a.isActive)
|
|
1060
1165
|
|| String(a.providerName).localeCompare(String(b.providerName)) || String(a.name || a.email || '').localeCompare(String(b.name || b.email || '')));
|
|
1061
1166
|
|
|
1167
|
+
// 按供应商聚类:同一供应商的账号排在一行网格里,组内保持 trSorted 顺序
|
|
1168
|
+
function trGroups(list) {
|
|
1169
|
+
const map = new Map();
|
|
1170
|
+
for (const c of trSorted(list)) {
|
|
1171
|
+
const k = c.providerName || c.provider || '其他';
|
|
1172
|
+
if (!map.has(k)) map.set(k, []);
|
|
1173
|
+
map.get(k).push(c);
|
|
1174
|
+
}
|
|
1175
|
+
return [...map.entries()].map(([name, items]) => ({ name, items }));
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// 10Router 连接的国内 / 国际版标签(沿用账号卡的徽标语义)
|
|
1179
|
+
const TR_REGION = {
|
|
1180
|
+
qoder: ['国际版', 'intl'], 'qoder-cn': ['国内版', 'cn'],
|
|
1181
|
+
'codebuddy-intl': ['国际版', 'intl'], 'codebuddy-cn': ['国内版', 'cn'],
|
|
1182
|
+
'glm-cn': ['国内版', 'cn'],
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// 「全部」视图:一个供应商一张汇总卡。同名额度跨连接求和;百分比窗口(如限时窗口)取平均。
|
|
1186
|
+
// 命名含 Cycle/Capacity 的包本身是「全包汇总」,连接级聚合时跳过以避免重复计数(Total Points 这类)。
|
|
1187
|
+
const isAggregatePack = (name) => /cycle|capacity/i.test(String(name || ''));
|
|
1188
|
+
function trVendorCardHtml(g) {
|
|
1189
|
+
const items = g.items;
|
|
1190
|
+
const prov = items[0].provider;
|
|
1191
|
+
const lows = items.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
|
|
1192
|
+
const errs = items.filter((x) => x.error).length;
|
|
1193
|
+
|
|
1194
|
+
const splitOf = items.map((x) => ({ x, s: splitQuotas(x) }));
|
|
1195
|
+
// Total Points / 含 Cycle|Capacity 的包是全包汇总:连接级聚合跳过它们,避免与子包重复计数
|
|
1196
|
+
const isAgg = (q) => isAggregatePack(q && q.name);
|
|
1197
|
+
const primaries = splitOf.filter(({ s }) => s.primary && !isAgg(s.primary)).map(({ s }) => s.primary);
|
|
1198
|
+
let primary = null;
|
|
1199
|
+
if (primaries.length) {
|
|
1200
|
+
const name = primaries[0].name;
|
|
1201
|
+
const same = primaries.filter((p) => p.name === name);
|
|
1202
|
+
const total = same.reduce((n, p) => n + (p.total > 0 ? p.total : 0), 0);
|
|
1203
|
+
const isPct = !(total > 0);
|
|
1204
|
+
if (isPct) {
|
|
1205
|
+
// 限时百分比窗口(如 Claude 5h):跨连接取平均百分比,而不是加总
|
|
1206
|
+
const pcts = same.map((p) => quotaPct(p)).filter((x) => x !== null);
|
|
1207
|
+
primary = {
|
|
1208
|
+
name,
|
|
1209
|
+
total: 0,
|
|
1210
|
+
remaining: 0,
|
|
1211
|
+
used: 0,
|
|
1212
|
+
unit: '',
|
|
1213
|
+
percentScale: true,
|
|
1214
|
+
remainingPercentage: pcts.length ? pcts.reduce((a, b) => a + b, 0) / pcts.length : null,
|
|
1215
|
+
recurring: same[0].recurring,
|
|
1216
|
+
resetAt: same.map((p) => p.resetAt).filter(Boolean).sort()[0] || null,
|
|
1217
|
+
};
|
|
1218
|
+
} else {
|
|
1219
|
+
primary = {
|
|
1220
|
+
name,
|
|
1221
|
+
total,
|
|
1222
|
+
remaining: same.reduce((n, p) => n + quotaRemaining(p), 0),
|
|
1223
|
+
used: same.reduce((n, p) => n + (p.used || 0), 0),
|
|
1224
|
+
unit: same[0].unit || '',
|
|
1225
|
+
percentScale: false,
|
|
1226
|
+
recurring: same[0].recurring,
|
|
1227
|
+
resetAt: same.map((p) => p.resetAt).filter(Boolean).sort()[0] || null,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// 周期窗口:同名求和;无固定总额的按百分比平均
|
|
1233
|
+
const winMap = new Map();
|
|
1234
|
+
const packs = [];
|
|
1235
|
+
for (const { s } of splitOf) {
|
|
1236
|
+
for (const q of s.windows.filter((x) => !isAgg(x))) {
|
|
1237
|
+
if (!winMap.has(q.name)) winMap.set(q.name, { name: q.name, total: 0, rem: 0, anyTotal: false, resetAt: null, pcts: [] });
|
|
1238
|
+
const w = winMap.get(q.name);
|
|
1239
|
+
if (q.total > 0) { w.anyTotal = true; w.total += q.total; w.rem += quotaRemaining(q); }
|
|
1240
|
+
else { const pct = quotaPct(q); if (pct !== null) w.pcts.push(pct); }
|
|
1241
|
+
if (q.resetAt && (!w.resetAt || String(q.resetAt) < String(w.resetAt))) w.resetAt = q.resetAt;
|
|
1242
|
+
}
|
|
1243
|
+
packs.push(...s.packs.filter((x) => !isAgg(x)));
|
|
1244
|
+
}
|
|
1245
|
+
const wins = [...winMap.values()];
|
|
1246
|
+
const alivePacks = packs.filter((q) => quotaRemaining(q) > 0);
|
|
1247
|
+
const packRem = alivePacks.reduce((n, q) => n + quotaRemaining(q), 0);
|
|
1248
|
+
|
|
1249
|
+
// 周 / 月度等窗口额度也给进度条(同名跨连接已聚合好):有固定总额的加总,百分比窗口取平均
|
|
1250
|
+
const winRows = wins.map((w) => trRow(w.anyTotal
|
|
1251
|
+
? { name: w.name, total: w.total, remaining: w.rem, used: w.total - w.rem, unit: '', percentScale: false, resetAt: w.resetAt, recurring: true }
|
|
1252
|
+
: { name: w.name, total: 0, remainingPercentage: w.pcts.length ? w.pcts.reduce((a, b) => a + b, 0) / w.pcts.length : null, percentScale: true, resetAt: w.resetAt, recurring: true })).join('');
|
|
1253
|
+
|
|
1254
|
+
const briefs = [];
|
|
1255
|
+
if (alivePacks.length) briefs.push('积分包×' + alivePacks.length + ' 剩 ' + fmtNum(packRem));
|
|
1256
|
+
else if (packs.length) briefs.push('积分包已用完');
|
|
1257
|
+
const hasQuotas = Boolean(primary || wins.length || packs.length);
|
|
1258
|
+
let quotaHtml;
|
|
1259
|
+
if (errs === items.length) {
|
|
1260
|
+
quotaHtml = '<div class="msg err">全部连接查询失败</div>';
|
|
1261
|
+
} else if (!hasQuotas) {
|
|
1262
|
+
quotaHtml = '<div class="msg">该供应商没有可汇总的额度</div>';
|
|
1263
|
+
} else {
|
|
1264
|
+
// 「全部」视角不显示「最早到期」:每行自带相对重置时间,足够判断
|
|
1265
|
+
quotaHtml = (primary ? trPrimaryHtml(primary) : '')
|
|
1266
|
+
+ winRows
|
|
1267
|
+
+ (briefs.length
|
|
1268
|
+
? '<div class="tr-summary dim"><span class="tr-sum-txt">' + briefs.join(' · ') + '</span></div>'
|
|
1269
|
+
: '');
|
|
1270
|
+
}
|
|
1271
|
+
return '<div class="tr-card"><div class="row1"><div class="name-group">'
|
|
1272
|
+
+ '<span class="badge prod tenrouter">' + esc(g.name) + '</span>'
|
|
1273
|
+
+ (TR_REGION[prov] ? '<span class="badge ' + TR_REGION[prov][1] + '">' + TR_REGION[prov][0] + '</span>' : '')
|
|
1274
|
+
+ '<span class="badge">' + items.length + ' 个账号</span>'
|
|
1275
|
+
+ (lows ? '<span class="pill limited">告急 ×' + lows + '</span>' : '')
|
|
1276
|
+
+ (errs ? '<span class="pill fail" title="部分连接查询失败">失败 ×' + errs + '</span>' : '')
|
|
1277
|
+
+ '</div></div>' + quotaHtml + '</div>';
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1062
1280
|
function trSetupHtml() {
|
|
1063
1281
|
return '<div class="setup"><h3>接入 10Router</h3>'
|
|
1064
1282
|
+ '<div class="hint">填 10Router 的地址和一个<b>虚拟 key</b>(10Router 仪表盘 → API Keys 创建的 sk-…),就能在这里:</div>'
|
|
@@ -1081,10 +1299,41 @@ function renderTenrouter() {
|
|
|
1081
1299
|
return '<span class="chip' + (on ? ' on' : '') + '" title="' + (x.found ? (x.supported ? '已检测到' : '需要 Node 22.5+') : '本机未检测到') + '">' + esc(x.label) + (x.found ? '' : ' ·无') + '</span>';
|
|
1082
1300
|
}).join('');
|
|
1083
1301
|
let grid;
|
|
1302
|
+
let tabStrip = '';
|
|
1303
|
+
const groups = list.length ? trGroups(list) : [];
|
|
1304
|
+
const vendor = state.trVendor || '';
|
|
1305
|
+
// 「隐藏 0 / 无额度」与列表共用同一过滤:被隐藏的供应商不出现在标签条
|
|
1306
|
+
const shown = state.trHideEmpty
|
|
1307
|
+
? groups.filter((g) => {
|
|
1308
|
+
if (g.items.every((x) => x.error)) return true;
|
|
1309
|
+
for (const x of g.items) {
|
|
1310
|
+
if (x.error) continue;
|
|
1311
|
+
const s = splitQuotas(x);
|
|
1312
|
+
if (s.primary && quotaRemaining(s.primary) > 0) return true;
|
|
1313
|
+
if (s.windows.some((q) => quotaRemaining(q) > 0 || (quotaPct(q) || 0) > 0)) return true;
|
|
1314
|
+
if (s.packs.some((q) => quotaRemaining(q) > 0)) return true;
|
|
1315
|
+
}
|
|
1316
|
+
return false;
|
|
1317
|
+
})
|
|
1318
|
+
: groups;
|
|
1319
|
+
const activeVendor = vendor && shown.some((g) => g.name === vendor) ? vendor : '';
|
|
1320
|
+
if (groups.length) {
|
|
1321
|
+
tabStrip = '<div class="tr-tabs">'
|
|
1322
|
+
+ '<button class="btn' + (!activeVendor ? ' primary' : '') + '" data-tr="vendor" data-v="">全部</button>'
|
|
1323
|
+
+ shown.map((g) => '<button class="btn' + (activeVendor === g.name ? ' primary' : '') + '" data-tr="vendor" data-v="' + esc(g.name) + '">' + esc(g.name) + ' ' + g.items.length + '</button>').join('')
|
|
1324
|
+
+ '</div>';
|
|
1325
|
+
}
|
|
1084
1326
|
if (tr.error) grid = '<div class="empty">' + esc(tr.error) + '</div>';
|
|
1085
1327
|
else if (!tr.quotas) grid = '<div class="empty">正在读取 10Router 供应商额度…</div>';
|
|
1086
1328
|
else if (!list.length) grid = '<div class="empty">10Router 里还没有可查额度的供应商连接(OAuth 或支持额度查询的 API Key)。</div>';
|
|
1087
|
-
else
|
|
1329
|
+
else if (activeVendor) {
|
|
1330
|
+
const g = groups.find((x) => x.name === activeVendor) || { items: [] };
|
|
1331
|
+
grid = '<div class="tr-vcards">' + g.items.map(trCardHtml).join('') + '</div>';
|
|
1332
|
+
} else {
|
|
1333
|
+
grid = shown.length
|
|
1334
|
+
? '<div class="tr-vcards">' + shown.map(trVendorCardHtml).join('') + '</div>'
|
|
1335
|
+
: '<div class="empty">没有可显示的供应商(可在「设置」里关掉隐藏)</div>';
|
|
1336
|
+
}
|
|
1088
1337
|
const h = tr.health;
|
|
1089
1338
|
const healthStrong = !h ? '…' : h.ok ? '正常' : '异常';
|
|
1090
1339
|
const healthDetail = !h ? '' : h.ok
|
|
@@ -1105,6 +1354,7 @@ function renderTenrouter() {
|
|
|
1105
1354
|
+ '<span style="margin-left:auto" title="' + esc(ls ? ls.summary : '') + '">' + (ls ? '上次同步 ' + esc(fmtTime(ls.at).slice(5)) + ' · ' + esc(ls.summary.slice(0, 40)) : '还没有同步过') + '</span>'
|
|
1106
1355
|
+ (tr.quotas ? '<span>额度更新于 ' + esc(fmtTime(tr.quotas.generatedAt).slice(11)) + '</span>' : '')
|
|
1107
1356
|
+ '</div>'
|
|
1357
|
+
+ tabStrip
|
|
1108
1358
|
+ '<div class="tr-grid">' + grid + '</div>';
|
|
1109
1359
|
}
|
|
1110
1360
|
|
|
@@ -1152,6 +1402,7 @@ function openTrSettings() {
|
|
|
1152
1402
|
$('tr-key').value = '';
|
|
1153
1403
|
$('tr-key').placeholder = c.keyMasked ? '已保存 ' + c.keyMasked + '(留空保持不变)' : 'sk-…';
|
|
1154
1404
|
$('tr-auto').checked = c.configured ? c.sync.enabled : true;
|
|
1405
|
+
$('tr-hide-empty').checked = state.trHideEmpty === true;
|
|
1155
1406
|
$('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
|
|
1156
1407
|
+ ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
|
|
1157
1408
|
+ '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
|
|
@@ -1183,6 +1434,9 @@ async function trSave() {
|
|
|
1183
1434
|
const f = trForm();
|
|
1184
1435
|
if (!f.endpoint) return toast('请填写 10Router 地址', false);
|
|
1185
1436
|
if (!f.key && !(tr.config && tr.config.keyMasked)) return toast('请填写虚拟 key', false);
|
|
1437
|
+
// 显示偏好只存在面板本地(localStorage),不上服务端
|
|
1438
|
+
state.trHideEmpty = $('tr-hide-empty').checked;
|
|
1439
|
+
try { localStorage.setItem('qd_tr_hideempty', state.trHideEmpty ? '1' : '0'); } catch (e) {}
|
|
1186
1440
|
try {
|
|
1187
1441
|
tr.config = await api('/api/tenrouter', { method: 'PUT', json: f });
|
|
1188
1442
|
closeModal('m-10r');
|
|
@@ -1203,6 +1457,12 @@ $('tr-panel').addEventListener('click', (e) => {
|
|
|
1203
1457
|
if (b.dataset.tr === 'settings') openTrSettings();
|
|
1204
1458
|
else if (b.dataset.tr === 'refresh') loadTrQuotas(true);
|
|
1205
1459
|
else if (b.dataset.tr === 'sync') trSync();
|
|
1460
|
+
else if (b.dataset.tr === 'vendor') {
|
|
1461
|
+
const v = b.dataset.v || '';
|
|
1462
|
+
state.trVendor = v;
|
|
1463
|
+
history.replaceState(null, '', v ? '#tenrouter/' + encodeURIComponent(v) : '#tenrouter');
|
|
1464
|
+
render();
|
|
1465
|
+
}
|
|
1206
1466
|
});
|
|
1207
1467
|
$('tr-test').onclick = trTest;
|
|
1208
1468
|
$('tr-save').onclick = trSave;
|
|
@@ -1257,7 +1517,7 @@ async function checkinOne(id) {
|
|
|
1257
1517
|
}
|
|
1258
1518
|
async function checkinAll(btn, product) {
|
|
1259
1519
|
const label = btn.innerHTML;
|
|
1260
|
-
btn.disabled = true; btn.innerHTML = I.refresh + '
|
|
1520
|
+
btn.disabled = true; btn.innerHTML = I.refresh + '领取中…';
|
|
1261
1521
|
try {
|
|
1262
1522
|
const d = await api('/api/checkin', { json: { skipIfCheckedToday: false, product } });
|
|
1263
1523
|
toast(d.summary);
|
|
@@ -1700,7 +1960,7 @@ async function submitImport(password) {
|
|
|
1700
1960
|
}
|
|
1701
1961
|
|
|
1702
1962
|
// ── 绑定 ──
|
|
1703
|
-
$('btn-checkin-all').innerHTML = I.play + '
|
|
1963
|
+
$('btn-checkin-all').innerHTML = I.play + '全部领取';
|
|
1704
1964
|
for (const id of ['btn-add', 'btn-add-dash']) $(id).innerHTML = I.plus + '添加账号';
|
|
1705
1965
|
for (const id of ['btn-import', 'btn-import-dash']) { $(id).innerHTML = I.upload + '导入'; $(id).onclick = pickImport; }
|
|
1706
1966
|
for (const id of ['btn-export', 'btn-export-dash']) $(id).innerHTML = I.download + '导出';
|
|
@@ -1722,9 +1982,12 @@ $('f-token').addEventListener('keydown', (e) => { if (e.key === 'Enter') addToke
|
|
|
1722
1982
|
$('list').addEventListener('click', onCardClick);
|
|
1723
1983
|
// 标签页:用 URL hash 记住(刷新 / 前进后退保持)
|
|
1724
1984
|
function applyView() {
|
|
1725
|
-
const
|
|
1985
|
+
const raw = location.hash.slice(1);
|
|
1986
|
+
const [v, sub] = raw.split('/');
|
|
1726
1987
|
state.view = VIEWS.includes(v) ? v : 'dash';
|
|
1727
1988
|
state.region = '';
|
|
1989
|
+
// 10Router 供应商标签可用 hash 直达:#tenrouter/<供应商名>
|
|
1990
|
+
state.trVendor = state.view === 'tenrouter' && sub ? decodeURIComponent(sub) : '';
|
|
1728
1991
|
document.querySelectorAll('#nav button').forEach((x) => x.classList.toggle('active', x.dataset.view === state.view));
|
|
1729
1992
|
$('view-dash').classList.toggle('active', state.view === 'dash');
|
|
1730
1993
|
$('view-prod').classList.toggle('active', state.view !== 'dash' && state.view !== 'tenrouter');
|
package/src/providers.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { productOf } from './constants.js';
|
|
12
12
|
import { checkinOne as checkinQoder, fetchQuotaUsage, fetchUserinfo } from './qoderClient.js';
|
|
13
|
-
import { checkinWorkbuddy, fetchWorkbuddyQuota, inspectToken } from './workbuddyClient.js';
|
|
13
|
+
import { checkinWorkbuddy, checkinWorkbuddyIntl, fetchWorkbuddyQuota, inspectToken } from './workbuddyClient.js';
|
|
14
14
|
import { fetchZcodeQuota } from './zcodeClient.js';
|
|
15
15
|
|
|
16
16
|
/** 从 userinfo 响应中挑一个可读的显示名 */
|
|
@@ -51,7 +51,8 @@ const PRODUCTS = {
|
|
|
51
51
|
},
|
|
52
52
|
workbuddy: {
|
|
53
53
|
label: 'WorkBuddy',
|
|
54
|
-
|
|
54
|
+
// 国内版走 billing daily-checkin 签到;国际版没有签到接口,走「活跃领取」(免费档对话探测)
|
|
55
|
+
checkin: (account, ctx) => (account.provider === 'workbuddy-intl' ? checkinWorkbuddyIntl(account, ctx) : checkinWorkbuddy(account, ctx)),
|
|
55
56
|
quota: (account, ctx) => fetchWorkbuddyQuota(account, ctx),
|
|
56
57
|
verify: async (account) => {
|
|
57
58
|
const info = inspectToken(account.token);
|
package/src/store.js
CHANGED
|
@@ -44,14 +44,39 @@ async function ensureDir() {
|
|
|
44
44
|
await fs.mkdir(dataDir(), { recursive: true, mode: 0o700 });
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Windows 上 rename 覆盖已有文件时,外部程序(杀软扫描、同步盘、另一守护进程)瞬时占用
|
|
48
|
+
// 会直接抛 EPERM/EBUSY/EACCES:先按递增间隔重试,仍失败则兜底直写目标(牺牲原子性换可用性)。
|
|
49
|
+
const RENAME_TRANSIENT = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
50
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
51
|
+
|
|
52
|
+
let renameImpl = null;
|
|
53
|
+
export function _setRenameForTests(fn) { renameImpl = fn || null; }
|
|
54
|
+
|
|
55
|
+
/** 原子写:临时文件 + rename(带故障重试与直写兜底) */
|
|
56
|
+
export async function atomicWrite(file, content, { retries = 6 } = {}) {
|
|
49
57
|
const tmp = path.join(
|
|
50
58
|
path.dirname(file),
|
|
51
59
|
`.tmp-${path.basename(file)}-${crypto.randomBytes(4).toString('hex')}`
|
|
52
60
|
);
|
|
53
61
|
await fs.writeFile(tmp, content, { mode: 0o600 });
|
|
54
|
-
|
|
62
|
+
const doRename = renameImpl || ((a, b) => fs.rename(a, b));
|
|
63
|
+
let transientRetries = 0;
|
|
64
|
+
try {
|
|
65
|
+
for (let i = 0; i <= retries; i++) {
|
|
66
|
+
try { await doRename(tmp, file); return; } catch (e) {
|
|
67
|
+
if (!RENAME_TRANSIENT.has(e?.code)) throw e; // 非占用类错误立即走兜底(如 EXDEV)
|
|
68
|
+
transientRetries++;
|
|
69
|
+
if (i < retries) await sleep(120 * (i + 1));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch { /* 落入兜底 */ }
|
|
73
|
+
// 兜底:读回临时文件直接覆盖写目标,并清理临时文件(避免 .tmp-* 残留)
|
|
74
|
+
try {
|
|
75
|
+
const data = await fs.readFile(tmp, 'utf8');
|
|
76
|
+
await fs.writeFile(file, data, { mode: 0o600 });
|
|
77
|
+
} finally {
|
|
78
|
+
await fs.unlink(tmp).catch(() => {});
|
|
79
|
+
}
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
async function readJson(file, fallback) {
|
package/src/workbuddyClient.js
CHANGED
|
@@ -91,6 +91,51 @@ async function post(account, path, token) {
|
|
|
91
91
|
return { status: res.status, body, text };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// 国际版「活跃领取」探测:官方规则为「当天有 ≥1 次有效对话请求即视为活跃用户,发放每日赠送积分」。
|
|
95
|
+
// 没有签到接口,所以每天发一条免费档模型(rateMultiplier 0,~0 消耗)的极短流式请求。
|
|
96
|
+
// 对齐 10router src/sse/services/codebuddyCheckin.js 的 intl 探测(stream 网关的系统提示与 typed blocks 是必需的,否则 11101)。
|
|
97
|
+
const INTL_PROBE_MODEL = 'hy4-preview';
|
|
98
|
+
async function postIntlProbe(account, token) {
|
|
99
|
+
const res = await fetch(`https://${apiHost(account)}/v2/chat/completions`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { ...headersFor(account, token), 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
model: INTL_PROBE_MODEL,
|
|
104
|
+
stream: true,
|
|
105
|
+
max_tokens: 16,
|
|
106
|
+
messages: [
|
|
107
|
+
{ role: 'system', content: 'You are CodeBuddy Code.' },
|
|
108
|
+
{ role: 'user', content: [{ type: 'text', text: 'hi' }] },
|
|
109
|
+
],
|
|
110
|
+
}),
|
|
111
|
+
signal: AbortSignal.timeout(Math.max(FETCH_TIMEOUT_MS, 25_000)),
|
|
112
|
+
});
|
|
113
|
+
// 2xx = 会话建立成功,把 SSE 流排干(最多几个 token)
|
|
114
|
+
const text = await res.text().catch(() => '');
|
|
115
|
+
return { status: res.status, text };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 国际版「活跃领取」:一次免费档对话请求使账号成为当日活跃用户。
|
|
120
|
+
* 返回与每日签到一致的结果对象(session-ok → checked-in,claimedAmount 0 表示额度随官方结算发放)。
|
|
121
|
+
*/
|
|
122
|
+
export async function checkinWorkbuddyIntl(account, { onRefresh } = {}) {
|
|
123
|
+
const label = account.name || account.id;
|
|
124
|
+
const base = { accountId: account.id, account: label, provider: account.provider };
|
|
125
|
+
try {
|
|
126
|
+
const r = await withToken(account, (t) => postIntlProbe(account, t), onRefresh);
|
|
127
|
+
if (r.status === 401) return { ...base, status: 'failed', error: '鉴权失败(401),token 已失效,请在客户端重新登录后重新导入' };
|
|
128
|
+
if (r.status === 429) return { ...base, status: 'no-activity', message: '今日额度已耗尽,活跃请求暂不可达(等官方发放后再试)', claimedAmount: 0 };
|
|
129
|
+
if (r.status >= 200 && r.status < 300) {
|
|
130
|
+
return { ...base, uid: account.uid, status: 'checked-in', claimedAmount: 0, message: '已发送活跃请求(保持活跃,活跃不等于必有奖励)' };
|
|
131
|
+
}
|
|
132
|
+
const m = (() => { try { return JSON.parse(r.text)?.error?.data?.msg || ''; } catch { return ''; } })();
|
|
133
|
+
return { ...base, status: 'failed', error: `活跃请求失败:HTTP ${r.status} ${m || r.text.slice(0, 120)}`.trim() };
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return { ...base, status: 'failed', error: e?.message || '网络错误' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
94
139
|
export function isExpired(account, now = Date.now()) {
|
|
95
140
|
const exp = account.expiresAt || inspectToken(account.token).expiresAt;
|
|
96
141
|
return Boolean(exp) && new Date(exp).getTime() - EXPIRY_SKEW_MS <= now;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZCode 活动自动领取 — 签到调度器每轮调用。
|
|
3
|
+
*
|
|
4
|
+
* 策略(对齐用户手动路径,逐级尝试):
|
|
5
|
+
* 1. billing/preview 查可领活动;为空 → 无事可做(返回 null,不留痕迹)
|
|
6
|
+
* 2. 先直接 claim(不带验证码:服务端未风控时可直接成功)
|
|
7
|
+
* 3. 返回 3007/3001(需要验证码 / 参数被拒)且有验证码提供者 → 拉 captcha 配置,
|
|
8
|
+
* 让提供者(桌面版的隐藏窗口跑阿里云验证码 SDK 静默验证)拿 captchaVerifyParam 后重试
|
|
9
|
+
* 4. 没有提供者(NAS / 纯 CLI)或验证码需要人工 → 标记「需手动领取」,卡片上提示
|
|
10
|
+
* - 1003(已领取过)按成功口径处理并记忆
|
|
11
|
+
* 结果写入账号 lastResult(签到日历记忆用的是 status checked-in/already,第二天照常重查 preview)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { logger } from './logger.js';
|
|
15
|
+
import { fetchClaimPlans, claimPlan, fetchCaptchaConfig } from './zcodeClient.js';
|
|
16
|
+
|
|
17
|
+
// 桌面版(Electron 主进程)注册的隐藏窗口验证码实现;NAS / CLI 为 null(走「需手动领取」分支)
|
|
18
|
+
let captchaProvider = null;
|
|
19
|
+
export function setZcodeCaptchaProvider(fn) { captchaProvider = typeof fn === 'function' ? fn : null; }
|
|
20
|
+
export function hasZcodeCaptchaProvider() { return Boolean(captchaProvider); }
|
|
21
|
+
|
|
22
|
+
const grantsText = (plan) => (plan.grants && plan.grants.length ? plan.grants.join(';') : '');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 为单个 ZCode 账号执行一次自动领取。
|
|
26
|
+
* @returns {null | { status, message, claims: Array }} null = preview 为空或查询失败(已记日志,不上卡片)
|
|
27
|
+
*/
|
|
28
|
+
export async function zcodeAutoClaim(account) {
|
|
29
|
+
const label = account.name || account.uid || account.id;
|
|
30
|
+
let plans;
|
|
31
|
+
try {
|
|
32
|
+
plans = (await fetchClaimPlans(account)).plans;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
logger.debug('ZCODE-CLAIM', `${label} 活动查询失败:${e.message}`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!plans.length) return null;
|
|
38
|
+
|
|
39
|
+
const claims = [];
|
|
40
|
+
let anySuccess = false;
|
|
41
|
+
let needManual = false;
|
|
42
|
+
|
|
43
|
+
for (const plan of plans) {
|
|
44
|
+
const planName = plan.name || plan.planId;
|
|
45
|
+
// 第 2 步:先试不带验证码
|
|
46
|
+
try {
|
|
47
|
+
const r = await claimPlan(account, plan.planId, {});
|
|
48
|
+
claims.push({ plan: planName, via: 'direct', ok: true });
|
|
49
|
+
anySuccess = true;
|
|
50
|
+
logger.info('ZCODE-CLAIM', `${label} 已领取「${planName}」(免验证码)${grantsText(plan) ? ':' + grantsText(plan) : ''}`);
|
|
51
|
+
continue;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (e.code === 1003) {
|
|
54
|
+
claims.push({ plan: planName, ok: true, already: true });
|
|
55
|
+
logger.info('ZCODE-CLAIM', `${label}「${planName}」已领取过`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (e.code !== 3007 && e.code !== 3001) {
|
|
59
|
+
claims.push({ plan: planName, ok: false, message: e.message });
|
|
60
|
+
logger.warn('ZCODE-CLAIM', `${label} 领取「${planName}」失败:${e.message}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// 第 3 步:需要验证码
|
|
65
|
+
if (!captchaProvider) {
|
|
66
|
+
needManual = true;
|
|
67
|
+
claims.push({ plan: planName, ok: false, needManual: true });
|
|
68
|
+
logger.info('ZCODE-CLAIM', `${label}「${planName}」需要验证码,本环境无法自动通过,请在面板手动领取`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const cfg = await fetchCaptchaConfig();
|
|
73
|
+
if (!cfg.enabled || !cfg.sceneId) throw new Error('验证码配置不可用');
|
|
74
|
+
const { captchaParam, region } = await captchaProvider(cfg);
|
|
75
|
+
await claimPlan(account, plan.planId, { captchaParam, region });
|
|
76
|
+
claims.push({ plan: planName, via: 'captcha', ok: true });
|
|
77
|
+
anySuccess = true;
|
|
78
|
+
logger.info('ZCODE-CLAIM', `${label} 已领取「${planName}」(静默验证码)${grantsText(plan) ? ':' + grantsText(plan) : ''}`);
|
|
79
|
+
} catch (e2) {
|
|
80
|
+
needManual = true;
|
|
81
|
+
claims.push({ plan: planName, ok: false, needManual: true, message: e2.message });
|
|
82
|
+
logger.warn('ZCODE-CLAIM', `${label}「${planName}」验证码未完成:${e2.message}(可在面板手动领取)`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const names = claims.filter((c) => c.ok && !c.already).map((c) => c.plan);
|
|
87
|
+
const status = names.length ? 'checked-in'
|
|
88
|
+
: claims.every((c) => c.already) ? 'already'
|
|
89
|
+
: 'failed';
|
|
90
|
+
const message = status === 'checked-in'
|
|
91
|
+
? `已领取:${names.join('、')}${claims.some((c) => c.via === 'captcha') ? '(静默验证码)' : ''}`
|
|
92
|
+
: status === 'already' ? '活动均已领取过'
|
|
93
|
+
: needManual ? `有 ${claims.filter((c) => c.needManual).length} 个活动需要验证码,请在面板手动领取`
|
|
94
|
+
: `领取失败:${claims.filter((c) => !c.ok).map((c) => c.message).filter(Boolean)[0] || '未知错误'}`;
|
|
95
|
+
return { status, message, claims };
|
|
96
|
+
}
|
package/src/zcodeClient.js
CHANGED
|
@@ -59,26 +59,46 @@ export function zcodeAppVersion() {
|
|
|
59
59
|
export function _resetAppVersionCache(v) { appVersionCache = v || null; }
|
|
60
60
|
|
|
61
61
|
// ── HTTP 出口:直连优先 / 代理优先(可切换),任一路成功即返回 ──
|
|
62
|
+
// 代理地址:优先用面板里保存的 proxyUrl(zcode-net.json,0600),没有则退回 HTTPS_PROXY 环境变量
|
|
62
63
|
|
|
63
64
|
function codeHome() {
|
|
64
65
|
return process.env.CREDITDADDY_HOME || process.env.QODERDADDY_HOME || path.join(os.homedir(), '.creditdaddy');
|
|
65
66
|
}
|
|
66
67
|
const NET_PREFS_FILE = () => path.join(codeHome(), 'zcode-net.json');
|
|
67
68
|
|
|
68
|
-
let
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
try {
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
let netPrefCache = null;
|
|
70
|
+
function netPref() {
|
|
71
|
+
if (netPrefCache) return netPrefCache;
|
|
72
|
+
try {
|
|
73
|
+
const j = JSON.parse(fs.readFileSync(NET_PREFS_FILE(), 'utf8'));
|
|
74
|
+
netPrefCache = {
|
|
75
|
+
proxyFirst: j.proxyFirst === true,
|
|
76
|
+
proxyUrl: typeof j.proxyUrl === 'string' && j.proxyUrl.trim() ? j.proxyUrl.trim() : null,
|
|
77
|
+
autoClaim: j.autoClaim === true,
|
|
78
|
+
};
|
|
79
|
+
} catch { netPrefCache = { proxyFirst: false, proxyUrl: null, autoClaim: false }; }
|
|
80
|
+
return netPrefCache;
|
|
74
81
|
}
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
function writeNetPref(p) {
|
|
83
|
+
netPrefCache = p;
|
|
77
84
|
fs.mkdirSync(codeHome(), { recursive: true, mode: 0o700 });
|
|
78
|
-
fs.writeFileSync(NET_PREFS_FILE(), JSON.stringify({ proxyFirst:
|
|
85
|
+
fs.writeFileSync(NET_PREFS_FILE(), JSON.stringify({ proxyFirst: p.proxyFirst, proxyUrl: p.proxyUrl, autoClaim: p.autoClaim }, null, 2), { mode: 0o600 });
|
|
86
|
+
}
|
|
87
|
+
export function proxyFirst() { return netPref().proxyFirst; }
|
|
88
|
+
export function proxyUrl() { return netPref().proxyUrl; }
|
|
89
|
+
/** 活动自动轮询领取开关(默认关:活动期前再开,平时不轮询) */
|
|
90
|
+
export function autoClaimEnabled() { return netPref().autoClaim; }
|
|
91
|
+
export function setAutoClaimEnabled(v) { writeNetPref({ ...netPref(), autoClaim: v === true }); }
|
|
92
|
+
export function setProxyFirst(v) { writeNetPref({ ...netPref(), proxyFirst: v === true }); }
|
|
93
|
+
export function setProxyUrl(u) {
|
|
94
|
+
const t = typeof u === 'string' ? u.trim() : '';
|
|
95
|
+
if (t && !/^http:\/\/\S+/i.test(t)) throw new Error('目前只支持 http:// 代理地址');
|
|
96
|
+
writeNetPref({ ...netPref(), proxyUrl: t || null });
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
const envProxy = () => process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null;
|
|
100
|
+
/** 生效代理:面板保存的优先,其次环境变量 */
|
|
101
|
+
const effectiveProxy = () => proxyUrl() || envProxy();
|
|
82
102
|
|
|
83
103
|
/** 目标是否应绕过代理(loopback / NO_PROXY 后缀匹配) */
|
|
84
104
|
function proxyBypass(url) {
|
|
@@ -118,8 +138,8 @@ function connectTunnel(proxyUrl, target) {
|
|
|
118
138
|
}
|
|
119
139
|
|
|
120
140
|
async function fetchViaProxy(url, { method = 'GET', headers = {}, body = null, timeoutMs = 15000 } = {}) {
|
|
121
|
-
const proxy =
|
|
122
|
-
if (!proxy || !/^http:\/\//i.test(proxy)) throw new Error('没有可用的 http
|
|
141
|
+
const proxy = effectiveProxy();
|
|
142
|
+
if (!proxy || !/^http:\/\//i.test(proxy)) throw new Error('没有可用的 http 代理(面板配置或 HTTPS_PROXY 环境变量)');
|
|
123
143
|
const u = new URL(url);
|
|
124
144
|
const p = new URL(proxy);
|
|
125
145
|
const isHttps = u.protocol === 'https:';
|
|
@@ -163,7 +183,7 @@ export function _setViaProxyForTests(fn) { viaProxyImpl = fn || null; }
|
|
|
163
183
|
|
|
164
184
|
export async function fetchJsonRace(url, { method = 'GET', headers = {}, body = null, timeoutMs = 15000 } = {}) {
|
|
165
185
|
const base = { method, headers: { ...headers }, ...(body != null ? { body: typeof body === 'string' ? body : JSON.stringify(body) } : {}), signal: AbortSignal.timeout(timeoutMs) };
|
|
166
|
-
const useProxy = Boolean(
|
|
186
|
+
const useProxy = Boolean(effectiveProxy()) && !proxyBypass(url);
|
|
167
187
|
const direct = () => fetch(url, base);
|
|
168
188
|
const viaProxy = viaProxyImpl
|
|
169
189
|
? () => viaProxyImpl(url, { method, headers, body, timeoutMs })
|