creditdaddy 0.9.3 → 1.1.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 +289 -234
- package/bin/creditdaddy.js +205 -187
- package/package.json +2 -2
- package/src/accounts.js +25 -7
- package/src/catpawClient.js +103 -0
- package/src/catpawLocal.js +229 -0
- package/src/checkin.js +178 -67
- package/src/constants.js +18 -2
- package/src/daemon.js +819 -596
- package/src/localDetect.js +2 -2
- package/src/logger.js +43 -2
- package/src/mirasimClient.js +189 -0
- package/src/mirasimLocal.js +386 -0
- package/src/panel.html +881 -155
- package/src/providers.js +113 -8
- package/src/qoderApp.js +438 -330
- package/src/qoderClient.js +40 -2
- package/src/qoderUmid.js +25 -4
- package/src/store.js +27 -1
- package/src/tenrouter.js +253 -221
- package/src/tenrouterAccounts.js +163 -0
- package/src/transfer.js +24 -5
- package/src/usageSync.js +214 -28
- package/src/workbuddyAuth.js +127 -126
- package/src/workbuddyClient.js +302 -302
- package/src/zcodeAutoClaim.js +1 -0
- package/src/zcodeClient.js +549 -515
- package/src/zcodeGateway.js +574 -0
- package/src/zcodeLocal.js +103 -1
- package/src/zcodePlanShape.json +29 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 妙手(美团 CatPaw)桌面客户端本机凭据读取与切换。
|
|
3
|
+
*
|
|
4
|
+
* 凭据存储(Windows):
|
|
5
|
+
* %APPDATA%\catpaw-moon\catx-credential.json { ssoTokenEnc }
|
|
6
|
+
* ssoTokenEnc = base64( iv(12B) ‖ gcmTag(16B) ‖ ciphertext ),AES-256-GCM
|
|
7
|
+
* key = sha256( machineId + ":catpaw-desk-token-v2" )
|
|
8
|
+
* machineId = HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid 的值(node-machine-id 同源)
|
|
9
|
+
* 明文结构 { access_token, modified_at }
|
|
10
|
+
*
|
|
11
|
+
* 额度 / 版本 / 用户信息走桌面网关(见 catpawClient.js):
|
|
12
|
+
* token 以 X-Auth-Token 头发送到 https://catx.nocode.cn/api/gateway/*
|
|
13
|
+
*
|
|
14
|
+
* 账号切换:妙手启动时读取该文件恢复登录,因此写入目标账号的 access_token
|
|
15
|
+
* 再重启客户端即为换号。切换前先把当前登录同步进 CreditDaddy 账号库(防丢号)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import crypto from 'node:crypto';
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
|
|
24
|
+
const EXE_NAME = '妙手.exe';
|
|
25
|
+
|
|
26
|
+
function credentialFile() {
|
|
27
|
+
if (process.env.CATPAW_CREDENTIAL_FILE) return process.env.CATPAW_CREDENTIAL_FILE;
|
|
28
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
29
|
+
return path.join(appData, 'catpaw-moon', 'catx-credential.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJson(file) {
|
|
33
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── 密钥派生(MachineGuid → sha256) ──
|
|
37
|
+
|
|
38
|
+
let keyCache = null; // Buffer | null(null = 本机不可用),false = 已尝试失败
|
|
39
|
+
let machineIdOverride = null; // 测试钩子:注入固定机器 ID(null = 未注入)
|
|
40
|
+
let runningOverride = null; // 测试钩子:true/false 强制运行状态(null = 真实探测)
|
|
41
|
+
|
|
42
|
+
function machineId() {
|
|
43
|
+
if (machineIdOverride) return machineIdOverride;
|
|
44
|
+
if (process.platform !== 'win32') return null;
|
|
45
|
+
try {
|
|
46
|
+
const out = execFileSync('REG.exe',
|
|
47
|
+
['QUERY', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'],
|
|
48
|
+
{ windowsHide: true, timeout: 10_000 }).toString();
|
|
49
|
+
// MachineGuid REG_SZ {xxxxxxxx-....}(与 node-machine-id 一致:去空白 + 小写)
|
|
50
|
+
return out.split('REG_SZ')[1].replace(/\r+|\n+|\s+/g, '').toLowerCase();
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tokenKey() {
|
|
55
|
+
if (machineIdOverride) {
|
|
56
|
+
return crypto.createHash('sha256').update(machineIdOverride + ':catpaw-desk-token-v2').digest();
|
|
57
|
+
}
|
|
58
|
+
if (keyCache) return keyCache;
|
|
59
|
+
if (keyCache === false) return null;
|
|
60
|
+
const id = machineId();
|
|
61
|
+
if (!id) { keyCache = false; return null; }
|
|
62
|
+
keyCache = crypto.createHash('sha256').update(id + ':catpaw-desk-token-v2').digest();
|
|
63
|
+
return keyCache;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 解密本机妙手登录凭据 → { accessToken, modifiedAt };不可用 / 未登录返回 null */
|
|
67
|
+
export function readCredential() {
|
|
68
|
+
const file = credentialFile();
|
|
69
|
+
const j = readJson(file);
|
|
70
|
+
if (!j || typeof j.ssoTokenEnc !== 'string' || !j.ssoTokenEnc) return null;
|
|
71
|
+
const key = tokenKey();
|
|
72
|
+
if (!key) return null;
|
|
73
|
+
try {
|
|
74
|
+
const blob = Buffer.from(j.ssoTokenEnc, 'base64');
|
|
75
|
+
if (blob.length < 29) return null;
|
|
76
|
+
const d = crypto.createDecipheriv('aes-256-gcm', key, blob.subarray(0, 12));
|
|
77
|
+
d.setAuthTag(blob.subarray(12, 28));
|
|
78
|
+
const plain = JSON.parse(Buffer.concat([d.update(blob.subarray(28)), d.final()]).toString('utf8'));
|
|
79
|
+
if (!plain || typeof plain.access_token !== 'string' || !plain.access_token) return null;
|
|
80
|
+
return { accessToken: plain.access_token, modifiedAt: plain.modified_at ?? null };
|
|
81
|
+
} catch { return null; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 加密写回凭据文件(保留文件其余字段;妙手重启后即为新登录) */
|
|
85
|
+
function writeCredential(accessToken) {
|
|
86
|
+
const file = credentialFile();
|
|
87
|
+
const key = tokenKey();
|
|
88
|
+
if (!key) throw new Error('无法派生本机密钥(仅支持 Windows 本机)');
|
|
89
|
+
const existing = readJson(file) || {};
|
|
90
|
+
const plain = JSON.stringify({ access_token: accessToken, modified_at: Date.now() });
|
|
91
|
+
const iv = crypto.randomBytes(12);
|
|
92
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
93
|
+
const ct = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
|
|
94
|
+
const enc = Buffer.concat([iv, cipher.getAuthTag(), ct]).toString('base64');
|
|
95
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
96
|
+
const tmp = `${file}.tmp-${crypto.randomUUID()}`;
|
|
97
|
+
fs.writeFileSync(tmp, JSON.stringify({ ...existing, ssoTokenEnc: enc }, null, 2) + '\n', { mode: 0o600 });
|
|
98
|
+
fs.renameSync(tmp, file);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 本机探测 ──
|
|
102
|
+
|
|
103
|
+
function decodeTasklist(buf) {
|
|
104
|
+
// Windows 控制台代码页(简体中文 GBK)输出,中文名需按 gbk 解码
|
|
105
|
+
try { return new TextDecoder('gbk').decode(buf); } catch { return buf.toString('utf8'); }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function catpawRunning() {
|
|
109
|
+
if (runningOverride !== null) return runningOverride; // 测试钩子
|
|
110
|
+
if (process.platform !== 'win32') return false;
|
|
111
|
+
try {
|
|
112
|
+
const out = decodeTasklist(execFileSync('tasklist.exe',
|
|
113
|
+
['/FI', `IMAGENAME eq ${EXE_NAME}`, '/NH'],
|
|
114
|
+
{ windowsHide: true, encoding: 'buffer', timeout: 8000 }));
|
|
115
|
+
// 无匹配时 tasklist 输出 INFO 行(中文系统为 GBK 代码页,见 decodeTasklist)
|
|
116
|
+
return out.toLowerCase().includes(EXE_NAME.toLowerCase()) && !/no tasks|没有找到|没有任务/i.test(out);
|
|
117
|
+
} catch { return false; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function candidateClientPath() {
|
|
121
|
+
if (process.platform !== 'win32') return null;
|
|
122
|
+
const local = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
123
|
+
const p = path.join(local, '妙手', EXE_NAME);
|
|
124
|
+
return fs.existsSync(p) ? p : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function detectCatpaw() {
|
|
128
|
+
const file = credentialFile();
|
|
129
|
+
const cred = readCredential();
|
|
130
|
+
const clientPath = candidateClientPath();
|
|
131
|
+
return {
|
|
132
|
+
dataDir: path.dirname(file),
|
|
133
|
+
exists: fs.existsSync(file),
|
|
134
|
+
signedIn: Boolean(cred),
|
|
135
|
+
clientInstalled: Boolean(clientPath),
|
|
136
|
+
clientPath,
|
|
137
|
+
running: catpawRunning(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const nap = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
142
|
+
|
|
143
|
+
export function terminateCatpaw({ timeoutMs = 8000 } = {}) {
|
|
144
|
+
if (process.platform !== 'win32') return { closed: false, supported: false };
|
|
145
|
+
if (!catpawRunning()) return { closed: false, running: false };
|
|
146
|
+
try { execFileSync('taskkill.exe', ['/IM', EXE_NAME, '/T'], { windowsHide: true, stdio: 'ignore', timeout: 15_000 }); } catch {}
|
|
147
|
+
let deadline = Date.now() + timeoutMs;
|
|
148
|
+
while (Date.now() < deadline && catpawRunning()) nap(250);
|
|
149
|
+
if (catpawRunning()) {
|
|
150
|
+
try { execFileSync('taskkill.exe', ['/IM', EXE_NAME, '/T', '/F'], { windowsHide: true, stdio: 'ignore', timeout: 15_000 }); } catch {}
|
|
151
|
+
deadline = Date.now() + 3000;
|
|
152
|
+
while (Date.now() < deadline && catpawRunning()) nap(250);
|
|
153
|
+
}
|
|
154
|
+
return { closed: !catpawRunning(), running: catpawRunning() };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 当前客户端登录的 token(面板「客户端当前」标识用,避免额外网络请求) */
|
|
158
|
+
export function currentCatpawToken() {
|
|
159
|
+
try { return readCredential()?.accessToken || null; } catch { return null; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── 当前会话读取与切换 ──
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 读取妙手客户端当前登录,返回标准化账号对象(供 addAccount / scan)。
|
|
166
|
+
* uid / name 通过网关 auth/current-user 补全(离线时降级为只带 token)。
|
|
167
|
+
*/
|
|
168
|
+
export async function liveToAccount() {
|
|
169
|
+
const cred = readCredential();
|
|
170
|
+
if (!cred) return null;
|
|
171
|
+
const account = {
|
|
172
|
+
provider: 'catpaw',
|
|
173
|
+
token: cred.accessToken,
|
|
174
|
+
refreshToken: null,
|
|
175
|
+
uid: null,
|
|
176
|
+
name: null,
|
|
177
|
+
expiresAt: null,
|
|
178
|
+
source: 'local-app',
|
|
179
|
+
meta: { capturedAt: new Date().toISOString() },
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
const { fetchCatpawProfile } = await import('./catpawClient.js');
|
|
183
|
+
const p = await fetchCatpawProfile(cred.accessToken);
|
|
184
|
+
if (p) {
|
|
185
|
+
account.uid = p.userId ? String(p.userId) : null;
|
|
186
|
+
account.name = p.name || null;
|
|
187
|
+
if (p.mobile) account.meta.phone = p.mobile;
|
|
188
|
+
if (p.avatarUrl) account.meta.avatarUrl = p.avatarUrl;
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
return account;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 切换妙手客户端当前登录账号(写入加密凭据;客户端重启后生效)。
|
|
196
|
+
* @param {object} account 目标账号
|
|
197
|
+
* @param {{ force?: boolean }} opts
|
|
198
|
+
*/
|
|
199
|
+
export async function switchTo(account, { force = false } = {}) {
|
|
200
|
+
const cred = readCredential();
|
|
201
|
+
if (!cred) throw new Error('本机妙手客户端没有可用的登录凭据(无法派生本机密钥或未登录过)');
|
|
202
|
+
if (account.token === cred.accessToken) return { switched: false, alreadyActive: true };
|
|
203
|
+
|
|
204
|
+
if (!force && catpawRunning()) {
|
|
205
|
+
const err = new Error('妙手客户端正在运行,请先退出后再切换(或强制切换,切换后自动重新拉起客户端)');
|
|
206
|
+
err.catpawRunning = true;
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
writeCredential(account.token);
|
|
210
|
+
return { switched: true, alreadyActive: false };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── 测试钩子(仅单元测试使用;不影响默认路径) ──
|
|
214
|
+
|
|
215
|
+
/** 注入固定机器 ID 派生密钥(null 恢复真实探测);同时复位密钥缓存 */
|
|
216
|
+
export function _setMachineIdForTests(id) {
|
|
217
|
+
machineIdOverride = id || null;
|
|
218
|
+
keyCache = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 设置/复位「妙手是否正在运行」的探测结果(null = 真实探测) */
|
|
222
|
+
export function _setRunningForTests(running) {
|
|
223
|
+
runningOverride = running === null || running === undefined ? null : Boolean(running);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 写出加密凭据(等价于切换流程里的落盘一步) */
|
|
227
|
+
export function _writeCredentialForTests(accessToken) {
|
|
228
|
+
writeCredential(accessToken);
|
|
229
|
+
}
|
package/src/checkin.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* - 一次 tick 扫描全部账号
|
|
4
|
-
* - 已确认完成今日签到的账号当天跳过(state.json 记忆)
|
|
5
|
-
* - "无可领活动" 不记忆(每日 10:00 (UTC+8) 刷新积分,之后还会出现可领活动)
|
|
6
|
-
* - "签到日" 以积分刷新时刻 10:00 (UTC+8) 为界,与本机时区无关
|
|
7
|
-
* - 国际版每台设备每天只能领一次(服务端按设备风控身份限领):本机已有账号领取后,
|
|
8
|
-
* 其余国际版账号标记为 limited 并当日记忆,不再反复请求
|
|
9
|
-
* - 定时器每 ~2h + 抖动执行一次;多轮签到(定时 / 手动)串行执行
|
|
2
|
+
* 签到调度:普通账号每 ~2h 扫描一次;ZCode 自动活动领取使用独立的短周期调度。
|
|
10
3
|
*/
|
|
11
4
|
|
|
12
5
|
import { loadAccounts, loadState, saveState, withAccounts } from './store.js';
|
|
@@ -15,30 +8,51 @@ import { productOf } from './constants.js';
|
|
|
15
8
|
import { startUsageSyncScheduler } from './tenrouter.js';
|
|
16
9
|
import { refreshContext } from './accounts.js';
|
|
17
10
|
import { zcodeAutoClaim } from './zcodeAutoClaim.js';
|
|
18
|
-
import {
|
|
11
|
+
import { clearQuotaMark } from './zcodeGateway.js';
|
|
12
|
+
import {
|
|
13
|
+
autoClaimEnabled, autoClaimUntil, enableAutoClaimFor, setAutoClaimEnabled,
|
|
14
|
+
warmZcodeAppVersion,
|
|
15
|
+
} from './zcodeClient.js';
|
|
19
16
|
import { logger } from './logger.js';
|
|
20
17
|
|
|
21
|
-
const TICK_MS = 2 * 60 * 60 * 1000;
|
|
22
|
-
const TICK_JITTER_MS = 10 * 60 * 1000;
|
|
18
|
+
const TICK_MS = 2 * 60 * 60 * 1000;
|
|
19
|
+
const TICK_JITTER_MS = 10 * 60 * 1000;
|
|
20
|
+
const ZCODE_TICK_MS = 2 * 60 * 1000;
|
|
21
|
+
const ZCODE_JITTER_MS = 15 * 1000;
|
|
22
|
+
const ZCODE_FIRST_DELAY_MS = 2 * 60 * 1000;
|
|
23
|
+
const ZCODE_AUTO_CLAIM_WINDOW_MS = 60 * 60 * 1000;
|
|
23
24
|
|
|
24
25
|
// 每日积分刷新:10:00 (UTC+8) = 02:00 UTC。签到日 = (now - 2h) 的 UTC 日期
|
|
25
26
|
const REFRESH_UTC_OFFSET_MS = 2 * 60 * 60 * 1000;
|
|
26
27
|
|
|
27
28
|
let timerHandle = null;
|
|
29
|
+
let zcodeTimerHandle = null;
|
|
30
|
+
let zcodeExpiryTimerHandle = null;
|
|
31
|
+
let zcodeExpiryAt = null;
|
|
32
|
+
let zcodeNextTickAt = null;
|
|
33
|
+
let zcodeLastTick = null;
|
|
34
|
+
let zcodeTicking = false;
|
|
35
|
+
let zcodeQueue = Promise.resolve();
|
|
28
36
|
let tickQueue = Promise.resolve();
|
|
29
37
|
let nextTickAt = null;
|
|
30
|
-
let lastTick = null;
|
|
38
|
+
let lastTick = null;
|
|
31
39
|
let ticking = false;
|
|
32
40
|
|
|
33
41
|
/** 调度器状态(面板展示用) */
|
|
34
42
|
export function getSchedulerInfo() {
|
|
35
|
-
|
|
43
|
+
const enabled = autoClaimEnabled();
|
|
44
|
+
return {
|
|
45
|
+
running: Boolean(timerHandle), nextTickAt, lastTick, ticking,
|
|
46
|
+
zcode: {
|
|
47
|
+
enabled,
|
|
48
|
+
autoOffAt: enabled ? autoClaimUntil() : null,
|
|
49
|
+
nextTickAt: enabled ? zcodeNextTickAt : null,
|
|
50
|
+
lastTick: zcodeLastTick,
|
|
51
|
+
ticking: zcodeTicking,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
36
54
|
}
|
|
37
55
|
|
|
38
|
-
/**
|
|
39
|
-
* 当前所属的"签到日"(YYYY-MM-DD)。以 10:00 (UTC+8) 为日界:
|
|
40
|
-
* 刷新前领过的记录不会让刷新后的新一轮被跳过;也不受 NAS / 海外机器时区影响。
|
|
41
|
-
*/
|
|
42
56
|
export function dayKey(nowMs = Date.now()) {
|
|
43
57
|
return new Date(nowMs - REFRESH_UTC_OFFSET_MS).toISOString().slice(0, 10);
|
|
44
58
|
}
|
|
@@ -47,19 +61,131 @@ export function msUntilNextTick(nowMs = Date.now(), rand = Math.random) {
|
|
|
47
61
|
return Math.max(TICK_MS + Math.floor(rand() * TICK_JITTER_MS), 1000);
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
export function zcodeMsUntilNextTick(rand = Math.random) {
|
|
65
|
+
return Math.max(ZCODE_TICK_MS + Math.floor(rand() * ZCODE_JITTER_MS), 1000);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 开启 ZCode 自动领取一小时;首次资格检查两分钟后开始。 */
|
|
69
|
+
export function enableZcodeAutoClaimForOneHour() {
|
|
70
|
+
enableAutoClaimFor(ZCODE_AUTO_CLAIM_WINDOW_MS);
|
|
71
|
+
syncZcodeScheduler(true);
|
|
72
|
+
return autoClaimUntil();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 更新 ZCode 定时器,autoClaimUntil 是持久化的到期时间,重启后仍会生效。 */
|
|
76
|
+
export function refreshZcodeScheduler() {
|
|
77
|
+
syncZcodeScheduler();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function clearZcodeTimers() {
|
|
81
|
+
if (zcodeTimerHandle) clearTimeout(zcodeTimerHandle);
|
|
82
|
+
if (zcodeExpiryTimerHandle) clearTimeout(zcodeExpiryTimerHandle);
|
|
83
|
+
zcodeTimerHandle = null;
|
|
84
|
+
zcodeExpiryTimerHandle = null;
|
|
85
|
+
zcodeExpiryAt = null;
|
|
86
|
+
zcodeNextTickAt = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scheduleZcodeNextTick(delay = zcodeMsUntilNextTick()) {
|
|
90
|
+
if (!autoClaimEnabled()) {
|
|
91
|
+
syncZcodeScheduler();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (zcodeTimerHandle) clearTimeout(zcodeTimerHandle);
|
|
95
|
+
zcodeNextTickAt = new Date(Date.now() + delay).toISOString();
|
|
96
|
+
zcodeTimerHandle = setTimeout(() => {
|
|
97
|
+
zcodeTimerHandle = null;
|
|
98
|
+
zcodeNextTickAt = null;
|
|
99
|
+
runZcodeTick().catch((err) => logger.error('ZCODE-CLAIM', `资格轮询失败:${err?.message || err}`))
|
|
100
|
+
.finally(() => scheduleZcodeNextTick());
|
|
101
|
+
}, delay);
|
|
102
|
+
zcodeTimerHandle.unref?.();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function syncZcodeScheduler(startFresh = false) {
|
|
106
|
+
if (!autoClaimEnabled()) {
|
|
107
|
+
clearZcodeTimers();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const expiresAt = autoClaimUntil();
|
|
112
|
+
if (startFresh && zcodeTimerHandle) {
|
|
113
|
+
clearTimeout(zcodeTimerHandle);
|
|
114
|
+
zcodeTimerHandle = null;
|
|
115
|
+
zcodeNextTickAt = null;
|
|
116
|
+
}
|
|
117
|
+
if (!zcodeTimerHandle && !zcodeTicking) scheduleZcodeNextTick(ZCODE_FIRST_DELAY_MS);
|
|
118
|
+
|
|
119
|
+
if (expiresAt && zcodeExpiryAt !== expiresAt) {
|
|
120
|
+
if (zcodeExpiryTimerHandle) clearTimeout(zcodeExpiryTimerHandle);
|
|
121
|
+
zcodeExpiryAt = expiresAt;
|
|
122
|
+
zcodeExpiryTimerHandle = setTimeout(() => {
|
|
123
|
+
zcodeExpiryTimerHandle = null;
|
|
124
|
+
setAutoClaimEnabled(false);
|
|
125
|
+
clearZcodeTimers();
|
|
126
|
+
logger.info('ZCODE-CLAIM', '一小时自动领取时段已结束,自动领取已关闭');
|
|
127
|
+
}, Math.max(0, expiresAt - Date.now()));
|
|
128
|
+
zcodeExpiryTimerHandle.unref?.();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function runZcodeTick(accountIds, { force = false } = {}) {
|
|
133
|
+
const run = zcodeQueue.then(async () => {
|
|
134
|
+
if (!force && !autoClaimEnabled()) return { results: [], summary: 'ZCode 自动领取已关闭' };
|
|
135
|
+
zcodeTicking = true;
|
|
136
|
+
const results = [];
|
|
137
|
+
const claimedIds = [];
|
|
138
|
+
try {
|
|
139
|
+
const accounts = (await loadAccounts()).filter((a) => a.provider === 'zcode'
|
|
140
|
+
&& (!accountIds || accountIds.has(a.id)));
|
|
141
|
+
for (const account of accounts) {
|
|
142
|
+
if (!force && !autoClaimEnabled()) break;
|
|
143
|
+
const label = account.name || account.uid || account.id;
|
|
144
|
+
try {
|
|
145
|
+
const outcome = await zcodeAutoClaim(account);
|
|
146
|
+
if (!outcome) continue;
|
|
147
|
+
if (outcome.status === 'checked-in') {
|
|
148
|
+
claimedIds.push(account.id);
|
|
149
|
+
await clearQuotaMark(account.id);
|
|
150
|
+
}
|
|
151
|
+
results.push({ accountId: account.id, account: label, provider: 'zcode', status: outcome.status, message: outcome.message });
|
|
152
|
+
const lastResult = { status: outcome.status, message: outcome.message, amount: 0, at: new Date().toISOString() };
|
|
153
|
+
await withAccounts((list) => {
|
|
154
|
+
const current = list.find((a) => a.id === account.id);
|
|
155
|
+
if (current) current.lastResult = lastResult;
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
logger.warn('ZCODE-CLAIM', `${label} 自动领取异常:${err?.message || err}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
zcodeLastTick = {
|
|
162
|
+
at: new Date().toISOString(),
|
|
163
|
+
summary: `ZCode 资格轮询完成:${results.length} 个账号返回活动结果`,
|
|
164
|
+
claimedIds, // 面板据此即时刷新这些账号的额度
|
|
165
|
+
};
|
|
166
|
+
return { results, summary: zcodeLastTick.summary };
|
|
167
|
+
} finally {
|
|
168
|
+
zcodeTicking = false;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
zcodeQueue = run.catch(() => {});
|
|
172
|
+
return run;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 手动触发一次 ZCode 资格轮询(面板单账号 / 全部领取走这里),与定时轮共用同一队列;手动轮不受开关限制。 */
|
|
176
|
+
export function pollZcodeNow(accountIds) {
|
|
177
|
+
return runZcodeTick(accountIds && accountIds.length ? new Set(accountIds) : null, { force: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
50
180
|
async function getDoneMap(state) {
|
|
51
181
|
const today = dayKey();
|
|
52
182
|
const raw = state?.qoderDailyDone;
|
|
53
183
|
const map = raw && typeof raw === 'object' && !Array.isArray(raw) ? { ...raw } : {};
|
|
54
|
-
|
|
55
|
-
for (const k of Object.keys(map)) if (map[k] !== today) delete map[k];
|
|
184
|
+
for (const key of Object.keys(map)) if (map[key] !== today) delete map[key];
|
|
56
185
|
return map;
|
|
57
186
|
}
|
|
58
187
|
|
|
59
|
-
/**
|
|
60
|
-
* 执行一轮签到。多次调用会排队串行执行,避免同一账号被并发领取、state.json 互相覆盖。
|
|
61
|
-
* @param {{provider?: string, product?: string, skipIfCheckedToday?: boolean, onlyAccountId?: string}} opts
|
|
62
|
-
*/
|
|
188
|
+
/** 多次调用排队串行执行,避免并发签到和 state.json 覆盖。 */
|
|
63
189
|
export function runCheckinTick(opts = {}) {
|
|
64
190
|
const run = tickQueue.then(() => runTickNow(opts));
|
|
65
191
|
tickQueue = run.catch(() => {});
|
|
@@ -73,16 +199,13 @@ async function runTickNow(opts) {
|
|
|
73
199
|
|
|
74
200
|
async function runTickInner(opts) {
|
|
75
201
|
const allAccounts = await loadAccounts();
|
|
76
|
-
// 没有每日签到能力的产品(如 ZCode:积分靠需验证码的活动领取)不参与签到轮
|
|
77
202
|
const accounts = allAccounts.filter(
|
|
78
203
|
(a) => (!opts.provider || a.provider === opts.provider)
|
|
79
204
|
&& (!opts.product || productOf(a.provider) === opts.product)
|
|
80
205
|
&& (!opts.onlyAccountId || a.id === opts.onlyAccountId)
|
|
81
206
|
&& typeof productImpl(a.provider)?.checkin === 'function'
|
|
82
207
|
);
|
|
83
|
-
|
|
84
|
-
// ZCode 活动轮询领取:开关打开(标签页可切换,默认关)且未被筛选参数排除时参与
|
|
85
|
-
const zAccounts = (autoClaimEnabled()
|
|
208
|
+
const zAccounts = (opts.includeZcode !== false && autoClaimEnabled()
|
|
86
209
|
&& !opts.onlyAccountId
|
|
87
210
|
&& (!opts.provider || opts.provider === 'zcode')
|
|
88
211
|
&& (!opts.product || opts.product === 'zcode'))
|
|
@@ -97,7 +220,6 @@ async function runTickInner(opts) {
|
|
|
97
220
|
const memo = await getDoneMap(state);
|
|
98
221
|
const today = dayKey();
|
|
99
222
|
const results = [];
|
|
100
|
-
// 本设备今日领取国际版每日积分的账号 { day, accountId, name }
|
|
101
223
|
let deviceClaim = state?.deviceClaim?.day === today ? state.deviceClaim : null;
|
|
102
224
|
|
|
103
225
|
for (const account of accounts) {
|
|
@@ -108,7 +230,7 @@ async function runTickInner(opts) {
|
|
|
108
230
|
continue;
|
|
109
231
|
}
|
|
110
232
|
|
|
111
|
-
const ctx = refreshContext(account, (
|
|
233
|
+
const ctx = refreshContext(account, (message) => logger.info('CHECKIN', `${label}:${message}`));
|
|
112
234
|
let outcome = await productImpl(account.provider).checkin(account, ctx);
|
|
113
235
|
if (account.provider === 'qoder' && outcome.risk) {
|
|
114
236
|
if (outcome.status === 'checked-in') {
|
|
@@ -139,7 +261,6 @@ async function runTickInner(opts) {
|
|
|
139
261
|
memo[account.id] = today;
|
|
140
262
|
logger.info('CHECKIN', `${label}:${outcome.message}`);
|
|
141
263
|
} else if (outcome.status === 'no-activity') {
|
|
142
|
-
// 不记忆——积分窗口(10:00 UTC+8)打开后还会出现可领活动
|
|
143
264
|
logger.debug('CHECKIN', `${label}:${outcome.message || '当前无可领取的活动'}`);
|
|
144
265
|
} else {
|
|
145
266
|
logger.warn('CHECKIN', `${label} 领取失败:${outcome.error || outcome.status}`);
|
|
@@ -151,34 +272,22 @@ async function runTickInner(opts) {
|
|
|
151
272
|
}
|
|
152
273
|
}
|
|
153
274
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
275
|
+
if (accounts.length) {
|
|
276
|
+
await withAccounts((all) => {
|
|
277
|
+
for (const updated of accounts) {
|
|
278
|
+
const current = all.find((a) => a.id === updated.id);
|
|
279
|
+
if (!current) continue;
|
|
280
|
+
current.lastCheckin = updated.lastCheckin;
|
|
281
|
+
if (updated.lastResult) current.lastResult = updated.lastResult;
|
|
282
|
+
if (updated.uid && !current.uid) current.uid = updated.uid;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
await saveState({ ...state, qoderDailyDone: memo, deviceClaim });
|
|
286
|
+
}
|
|
165
287
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
}
|
|
288
|
+
if (zAccounts.length) {
|
|
289
|
+
const zResult = await runZcodeTick(new Set(zAccounts.map((a) => a.id)));
|
|
290
|
+
results.push(...zResult.results);
|
|
182
291
|
}
|
|
183
292
|
|
|
184
293
|
const claimed = results.filter((r) => r.status === 'checked-in');
|
|
@@ -186,44 +295,46 @@ async function runTickInner(opts) {
|
|
|
186
295
|
const none = results.filter((r) => r.status === 'no-activity');
|
|
187
296
|
const limited = results.filter((r) => r.status === 'limited');
|
|
188
297
|
const already = results.length - claimed.length - failed.length - none.length - limited.length;
|
|
189
|
-
const totalCredits = claimed.reduce((
|
|
298
|
+
const totalCredits = claimed.reduce((sum, r) => sum + (r.claimedAmount || 0), 0);
|
|
190
299
|
const summary = `签到汇总:成功 ${claimed.length}(+${totalCredits} Credits)、已领 ${already}`
|
|
191
300
|
+ (limited.length ? `、本机限领 ${limited.length}` : '')
|
|
192
301
|
+ `、无活动 ${none.length}、失败 ${failed.length}`;
|
|
193
302
|
logger.info('CHECKIN', summary);
|
|
194
303
|
lastTick = { at: new Date().toISOString(), summary };
|
|
195
|
-
|
|
196
304
|
return { results, summary };
|
|
197
305
|
}
|
|
198
306
|
|
|
199
|
-
/**
|
|
307
|
+
/** 启动普通签到调度与 ZCode 独立轮询调度。 */
|
|
200
308
|
export function startScheduler() {
|
|
201
309
|
if (timerHandle) return;
|
|
202
|
-
startUsageSyncScheduler();
|
|
310
|
+
startUsageSyncScheduler();
|
|
311
|
+
warmZcodeAppVersion();
|
|
203
312
|
const scheduleNext = () => {
|
|
204
313
|
const delay = msUntilNextTick();
|
|
205
314
|
nextTickAt = new Date(Date.now() + delay).toISOString();
|
|
206
315
|
timerHandle = setTimeout(async () => {
|
|
207
316
|
try {
|
|
208
|
-
await runCheckinTick({ skipIfCheckedToday: true });
|
|
317
|
+
await runCheckinTick({ skipIfCheckedToday: true, includeZcode: false });
|
|
209
318
|
} catch (err) {
|
|
210
319
|
logger.error('CHECKIN', `定时轮失败:${err?.message || err}`);
|
|
211
320
|
} finally {
|
|
212
321
|
scheduleNext();
|
|
213
322
|
}
|
|
214
323
|
}, delay);
|
|
215
|
-
|
|
324
|
+
timerHandle.unref?.();
|
|
216
325
|
logger.info('CHECKIN', `定时签到已启动,下次执行约 ${Math.round(delay / 60000)} 分钟后`);
|
|
217
326
|
};
|
|
218
327
|
scheduleNext();
|
|
219
|
-
|
|
328
|
+
syncZcodeScheduler();
|
|
220
329
|
setTimeout(() => {
|
|
221
|
-
runCheckinTick({ skipIfCheckedToday: true }).catch((
|
|
222
|
-
logger.error('CHECKIN', `引导轮失败:${
|
|
330
|
+
runCheckinTick({ skipIfCheckedToday: true, includeZcode: false }).catch((err) =>
|
|
331
|
+
logger.error('CHECKIN', `引导轮失败:${err?.message || err}`));
|
|
223
332
|
}, 15000).unref?.();
|
|
224
333
|
}
|
|
225
334
|
|
|
226
335
|
export function stopScheduler() {
|
|
227
|
-
if (timerHandle)
|
|
336
|
+
if (timerHandle) clearTimeout(timerHandle);
|
|
337
|
+
timerHandle = null;
|
|
228
338
|
nextTickAt = null;
|
|
339
|
+
clearZcodeTimers();
|
|
229
340
|
}
|
package/src/constants.js
CHANGED
|
@@ -13,6 +13,10 @@ import { readFileSync } from 'node:fs';
|
|
|
13
13
|
export const OPENAPI_BASE = 'https://openapi.qoder.sh';
|
|
14
14
|
export const CN_OPENAPI_BASE = 'https://openapi.qoder.com.cn';
|
|
15
15
|
|
|
16
|
+
// 网页端(账号设置 → 用量明细)同源 API:逐资源包明细只在这里提供,且只认浏览器登录后的会话 Cookie。
|
|
17
|
+
export const WEB_BASE = 'https://qoder.com';
|
|
18
|
+
export const CN_WEB_BASE = 'https://qoder.cn';
|
|
19
|
+
|
|
16
20
|
export const LOGIN_URL = 'https://qoder.com/device/selectAccounts';
|
|
17
21
|
export const CN_LOGIN_URL = 'https://qoder.cn/device/selectAccounts';
|
|
18
22
|
|
|
@@ -23,29 +27,41 @@ export const CAMPAIGN_CLAIM_PATH = (id) => `/sash/api/v1/me/campaigns/${encodeUR
|
|
|
23
27
|
// 账号信息与配额
|
|
24
28
|
export const USERINFO_PATH = '/api/v1/userinfo';
|
|
25
29
|
export const QUOTA_USAGE_PATH = '/api/v2/quota/usage';
|
|
30
|
+
// 网页端逐资源包用量明细(plan / 个人资源包 / 组织资源包,含每包到期时间)。
|
|
31
|
+
// 仅接受浏览器登录的会话 Cookie(dt- / jt- token 一律 401),见 qoderClient.fetchUsageDetail。
|
|
32
|
+
export const WEB_USAGE_PATH = '/api/v2/me/usages/big_model_credits';
|
|
26
33
|
|
|
27
34
|
// PAT (pt-...) → 短期 job token (jt-...) 兑换(普通 JSON POST,无需 COSY 签名)
|
|
28
35
|
export const JOB_TOKEN_EXCHANGE_PATH = '/api/v1/jobToken/exchange';
|
|
29
36
|
|
|
30
37
|
// 账号的 provider = 产品 + 区域。新增产品时在此登记,并在 providers.js 挂上对应实现。
|
|
31
|
-
export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode'];
|
|
38
|
+
export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw'];
|
|
32
39
|
export const PROVIDER_LABEL = {
|
|
33
40
|
qoder: 'Qoder 国际版',
|
|
34
41
|
'qoder-cn': 'Qoder 国内版',
|
|
35
42
|
workbuddy: 'WorkBuddy 国内版',
|
|
36
43
|
'workbuddy-intl': 'WorkBuddy 国际版',
|
|
37
44
|
zcode: 'ZCode',
|
|
45
|
+
mirasim: 'mirasim',
|
|
46
|
+
catpaw: '妙手',
|
|
38
47
|
};
|
|
39
|
-
/** provider → 产品线(qoder / workbuddy / zcode) */
|
|
48
|
+
/** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw) */
|
|
40
49
|
export const productOf = (provider) => {
|
|
41
50
|
const p = String(provider);
|
|
42
51
|
if (p.startsWith('workbuddy')) return 'workbuddy';
|
|
43
52
|
if (p.startsWith('zcode')) return 'zcode';
|
|
53
|
+
if (p === 'mirasim') return 'mirasim';
|
|
54
|
+
if (p === 'catpaw') return 'catpaw';
|
|
44
55
|
return 'qoder';
|
|
45
56
|
};
|
|
46
57
|
/** provider 是否国内版 */
|
|
47
58
|
export const isCnProvider = (provider) => provider === 'qoder-cn' || provider === 'workbuddy';
|
|
48
59
|
|
|
60
|
+
/** Qoder 网页端同源 API 的 host(用量明细逐资源包接口在这里,只认浏览器会话 Cookie) */
|
|
61
|
+
export function webBase(provider) {
|
|
62
|
+
return provider === 'qoder-cn' ? CN_WEB_BASE : WEB_BASE;
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
/** 签到请求头(源自 10router qoderCheckin.js,clientType=10 与官方客户端一致) */
|
|
50
66
|
export function buildQoderHeaders(token) {
|
|
51
67
|
return {
|