creditdaddy 0.8.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -224
- package/bin/creditdaddy.js +9 -9
- package/package.json +1 -1
- package/src/checkin.js +31 -2
- package/src/daemon.js +16 -6
- package/src/panel.html +2065 -1785
- 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/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 })
|