creditdaddy 0.9.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +56 -8
- package/bin/creditdaddy.js +209 -191
- package/package.json +4 -3
- package/src/accounts.js +161 -143
- package/src/authDevice.js +205 -205
- package/src/catpawClient.js +103 -0
- package/src/catpawLocal.js +229 -0
- package/src/checkin.js +357 -230
- package/src/constants.js +97 -81
- package/src/daemon.js +935 -658
- package/src/localDetect.js +147 -147
- package/src/logger.js +39 -1
- package/src/mirasimClient.js +189 -189
- package/src/mirasimLocal.js +386 -307
- package/src/panel.html +765 -214
- package/src/providers.js +190 -100
- package/src/qoderApp.js +524 -332
- package/src/qoderClient.js +237 -199
- package/src/qoderUmid.js +198 -198
- package/src/store.js +238 -222
- package/src/tenrouter.js +263 -221
- package/src/tenrouterAccounts.js +163 -0
- package/src/traeClient.js +258 -0
- package/src/traeLocal.js +337 -0
- package/src/transfer.js +160 -141
- package/src/usageSync.js +505 -319
- package/src/workbuddyAuth.js +127 -126
- package/src/workbuddyClient.js +302 -302
- package/src/workbuddyLocal.js +133 -133
- package/src/zcodeAuth.js +131 -131
- package/src/zcodeAutoClaim.js +97 -96
- package/src/zcodeClient.js +580 -529
- package/src/zcodeGateway.js +570 -0
- package/src/zcodeLocal.js +334 -319
- package/src/zcodePlanShape.json +29 -0
- package/src/zcrypto.js +160 -160
package/src/store.js
CHANGED
|
@@ -1,222 +1,238 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 本地账号存储 — 数据全部留在本机(参考 WorkDaddy 的数据边界原则)。
|
|
3
|
-
*
|
|
4
|
-
* 目录:$CREDITDADDY_HOME(兼容旧的 $QODERDADDY_HOME;默认 ~/.creditdaddy)
|
|
5
|
-
* accounts.json 账号列表(含 token,0600 权限)
|
|
6
|
-
* state.json 运行状态(签到完成日历等)
|
|
7
|
-
* 项目曾名 QoderDaddy:默认目录不存在而 ~/.qoderdaddy 存在时,首次访问自动复制过来(旧目录保留)。
|
|
8
|
-
*
|
|
9
|
-
* 写入采用 原子写(临时文件 + rename),避免进程中断损坏数据。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from 'node:fs/promises';
|
|
13
|
-
import fsSync from 'node:fs';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import os from 'node:os';
|
|
16
|
-
import crypto from 'node:crypto';
|
|
17
|
-
import { PROVIDERS, productOf } from './constants.js';
|
|
18
|
-
|
|
19
|
-
let migrated = false;
|
|
20
|
-
|
|
21
|
-
/** 从旧版 ~/.qoderdaddy 复制数据到新目录(只在新目录尚不存在时执行一次) */
|
|
22
|
-
function migrateLegacyDir(target) {
|
|
23
|
-
migrated = true;
|
|
24
|
-
const legacy = path.join(os.homedir(), '.qoderdaddy');
|
|
25
|
-
try {
|
|
26
|
-
if (fsSync.existsSync(target) || !fsSync.existsSync(legacy)) return;
|
|
27
|
-
fsSync.cpSync(legacy, target, { recursive: true, errorOnExist: false });
|
|
28
|
-
fsSync.writeFileSync(path.join(target, 'MIGRATED_FROM_QODERDADDY'), new Date().toISOString());
|
|
29
|
-
} catch {}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function dataDir() {
|
|
33
|
-
const explicit = process.env.CREDITDADDY_HOME || process.env.QODERDADDY_HOME;
|
|
34
|
-
if (explicit) return explicit;
|
|
35
|
-
const target = path.join(os.homedir(), '.creditdaddy');
|
|
36
|
-
if (!migrated) migrateLegacyDir(target);
|
|
37
|
-
return target;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const ACCOUNTS_FILE = () => path.join(dataDir(), 'accounts.json');
|
|
41
|
-
const STATE_FILE = () => path.join(dataDir(), 'state.json');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
await fs.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
*
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 本地账号存储 — 数据全部留在本机(参考 WorkDaddy 的数据边界原则)。
|
|
3
|
+
*
|
|
4
|
+
* 目录:$CREDITDADDY_HOME(兼容旧的 $QODERDADDY_HOME;默认 ~/.creditdaddy)
|
|
5
|
+
* accounts.json 账号列表(含 token,0600 权限)
|
|
6
|
+
* state.json 运行状态(签到完成日历等)
|
|
7
|
+
* 项目曾名 QoderDaddy:默认目录不存在而 ~/.qoderdaddy 存在时,首次访问自动复制过来(旧目录保留)。
|
|
8
|
+
*
|
|
9
|
+
* 写入采用 原子写(临时文件 + rename),避免进程中断损坏数据。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import fsSync from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import { PROVIDERS, productOf } from './constants.js';
|
|
18
|
+
|
|
19
|
+
let migrated = false;
|
|
20
|
+
|
|
21
|
+
/** 从旧版 ~/.qoderdaddy 复制数据到新目录(只在新目录尚不存在时执行一次) */
|
|
22
|
+
function migrateLegacyDir(target) {
|
|
23
|
+
migrated = true;
|
|
24
|
+
const legacy = path.join(os.homedir(), '.qoderdaddy');
|
|
25
|
+
try {
|
|
26
|
+
if (fsSync.existsSync(target) || !fsSync.existsSync(legacy)) return;
|
|
27
|
+
fsSync.cpSync(legacy, target, { recursive: true, errorOnExist: false });
|
|
28
|
+
fsSync.writeFileSync(path.join(target, 'MIGRATED_FROM_QODERDADDY'), new Date().toISOString());
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function dataDir() {
|
|
33
|
+
const explicit = process.env.CREDITDADDY_HOME || process.env.QODERDADDY_HOME;
|
|
34
|
+
if (explicit) return explicit;
|
|
35
|
+
const target = path.join(os.homedir(), '.creditdaddy');
|
|
36
|
+
if (!migrated) migrateLegacyDir(target);
|
|
37
|
+
return target;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ACCOUNTS_FILE = () => path.join(dataDir(), 'accounts.json');
|
|
41
|
+
const STATE_FILE = () => path.join(dataDir(), 'state.json');
|
|
42
|
+
const SETTINGS_FILE = () => path.join(dataDir(), 'settings.json');
|
|
43
|
+
|
|
44
|
+
async function ensureDir() {
|
|
45
|
+
await fs.mkdir(dataDir(), { recursive: true, mode: 0o700 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Windows 上 rename 覆盖已有文件时,外部程序(杀软扫描、同步盘、另一守护进程)瞬时占用
|
|
49
|
+
// 会直接抛 EPERM/EBUSY/EACCES:先按递增间隔重试,仍失败则兜底直写目标(牺牲原子性换可用性)。
|
|
50
|
+
const RENAME_TRANSIENT = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
51
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
52
|
+
|
|
53
|
+
let renameImpl = null;
|
|
54
|
+
export function _setRenameForTests(fn) { renameImpl = fn || null; }
|
|
55
|
+
|
|
56
|
+
/** 原子写:临时文件 + rename(带故障重试与直写兜底) */
|
|
57
|
+
export async function atomicWrite(file, content, { retries = 6 } = {}) {
|
|
58
|
+
const tmp = path.join(
|
|
59
|
+
path.dirname(file),
|
|
60
|
+
`.tmp-${path.basename(file)}-${crypto.randomBytes(4).toString('hex')}`
|
|
61
|
+
);
|
|
62
|
+
await fs.writeFile(tmp, content, { mode: 0o600 });
|
|
63
|
+
const doRename = renameImpl || ((a, b) => fs.rename(a, b));
|
|
64
|
+
let transientRetries = 0;
|
|
65
|
+
try {
|
|
66
|
+
for (let i = 0; i <= retries; i++) {
|
|
67
|
+
try { await doRename(tmp, file); return; } catch (e) {
|
|
68
|
+
if (!RENAME_TRANSIENT.has(e?.code)) throw e; // 非占用类错误立即走兜底(如 EXDEV)
|
|
69
|
+
transientRetries++;
|
|
70
|
+
if (i < retries) await sleep(120 * (i + 1));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch { /* 落入兜底 */ }
|
|
74
|
+
// 兜底:读回临时文件直接覆盖写目标,并清理临时文件(避免 .tmp-* 残留)
|
|
75
|
+
try {
|
|
76
|
+
const data = await fs.readFile(tmp, 'utf8');
|
|
77
|
+
await fs.writeFile(file, data, { mode: 0o600 });
|
|
78
|
+
} finally {
|
|
79
|
+
await fs.unlink(tmp).catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function readJson(file, fallback) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (e.code === 'ENOENT') return fallback;
|
|
88
|
+
throw new Error(`读取 ${file} 失败: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── 账号 ───
|
|
93
|
+
|
|
94
|
+
export async function loadAccounts() {
|
|
95
|
+
const list = await readJson(ACCOUNTS_FILE(), []);
|
|
96
|
+
return Array.isArray(list) ? list : [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function saveAccounts(accounts) {
|
|
100
|
+
await ensureDir();
|
|
101
|
+
await atomicWrite(ACCOUNTS_FILE(), JSON.stringify(accounts, null, 2));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 进程内串行队列:所有「读-改-写」都经由 withAccounts,避免并发请求互相覆盖
|
|
105
|
+
let accountsQueue = Promise.resolve();
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 串行地读取 → 修改 → 保存账号列表。fn 可直接修改传入的数组(或返回新数组),
|
|
109
|
+
* 返回 fn 的结果。fn 内不要做网络请求,以免长时间占住队列。
|
|
110
|
+
*/
|
|
111
|
+
export function withAccounts(fn) {
|
|
112
|
+
const run = accountsQueue.then(async () => {
|
|
113
|
+
const accounts = await loadAccounts();
|
|
114
|
+
const result = await fn(accounts);
|
|
115
|
+
await saveAccounts(accounts);
|
|
116
|
+
return result;
|
|
117
|
+
});
|
|
118
|
+
accountsQueue = run.catch(() => {});
|
|
119
|
+
return run;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function newId() {
|
|
123
|
+
return crypto.randomUUID();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const optStr = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
127
|
+
|
|
128
|
+
/** 过期时间统一成 ISO 字符串(接受 ISO / 秒 / 毫秒) */
|
|
129
|
+
export function normalizeExpiry(v) {
|
|
130
|
+
if (v === null || v === undefined || v === '') return null;
|
|
131
|
+
const n = typeof v === 'number' ? v : /^\d+$/.test(String(v)) ? Number(v) : NaN;
|
|
132
|
+
const d = Number.isFinite(n) ? new Date(n < 1e12 ? n * 1000 : n) : new Date(String(v));
|
|
133
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 校验并构造账号记录(不含重复检查,由调用方负责)。
|
|
138
|
+
* 可选元数据:uid / email / refreshToken / expiresAt / source / meta(产品相关的非通用字段,如 WorkBuddy 会话信息)
|
|
139
|
+
*/
|
|
140
|
+
export function normalizeAccountInput({ name, provider, token, uid, email, refreshToken, expiresAt, source, meta }) {
|
|
141
|
+
if (!PROVIDERS.includes(provider)) {
|
|
142
|
+
throw new Error(`provider 必须是 ${PROVIDERS.join(' 或 ')}`);
|
|
143
|
+
}
|
|
144
|
+
const trimmed = String(token || '').trim();
|
|
145
|
+
if (!trimmed) throw new Error('token 不能为空');
|
|
146
|
+
return {
|
|
147
|
+
id: newId(),
|
|
148
|
+
name: String(name || '').trim() || null,
|
|
149
|
+
provider,
|
|
150
|
+
token: trimmed,
|
|
151
|
+
uid: optStr(uid),
|
|
152
|
+
email: optStr(email),
|
|
153
|
+
refreshToken: optStr(refreshToken),
|
|
154
|
+
expiresAt: normalizeExpiry(expiresAt),
|
|
155
|
+
source: optStr(source) || 'manual',
|
|
156
|
+
meta: meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : {},
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
lastCheckin: null,
|
|
159
|
+
lastResult: null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 相同 provider 下 token 相同、或 uid 相同(同一用户换了新 token)视为同一账号 */
|
|
164
|
+
export function findDuplicate(accounts, provider, token, uid = null) {
|
|
165
|
+
const t = String(token || '').trim();
|
|
166
|
+
return accounts.find((a) => a.provider === provider && (a.token === t || (uid && a.uid && a.uid === uid)));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function maskToken(token) {
|
|
170
|
+
if (typeof token !== 'string' || token.length < 12) return '***';
|
|
171
|
+
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const maskPhone = (p) => (typeof p === 'string' && p.length >= 7 ? p.slice(0, 3) + '****' + p.slice(-4) : null);
|
|
175
|
+
|
|
176
|
+
function zcodeFlavor(meta) {
|
|
177
|
+
if (!meta) return null;
|
|
178
|
+
const c = meta.credentials || {};
|
|
179
|
+
const keys = Object.keys(c);
|
|
180
|
+
const hasZai = Boolean(c['oauth:zai:access_token'] || meta.loginProvider === 'zai' || keys.some((k) => k.includes('zai')));
|
|
181
|
+
const hasBig = Boolean(c['oauth:bigmodel:access_token'] || meta.loginProvider === 'bigmodel' || keys.some((k) => k.includes('bigmodel')));
|
|
182
|
+
return hasZai && hasBig ? 'both' : hasZai ? 'zai' : hasBig ? 'bigmodel' : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 对外输出的脱敏账号视图 */
|
|
186
|
+
export function publicAccount(a) {
|
|
187
|
+
const meta = a.meta || {};
|
|
188
|
+
return {
|
|
189
|
+
id: a.id,
|
|
190
|
+
name: a.name,
|
|
191
|
+
provider: a.provider,
|
|
192
|
+
product: productOf(a.provider),
|
|
193
|
+
tokenMasked: maskToken(a.token),
|
|
194
|
+
isPat: typeof a.token === 'string' && a.token.startsWith('pt-'),
|
|
195
|
+
uid: a.uid || null,
|
|
196
|
+
email: a.email || null,
|
|
197
|
+
expiresAt: a.expiresAt || null,
|
|
198
|
+
hasRefreshToken: Boolean(a.refreshToken),
|
|
199
|
+
source: a.source || 'manual',
|
|
200
|
+
phone: maskPhone(meta.phone),
|
|
201
|
+
domain: meta.domain || null,
|
|
202
|
+
flavor: a.provider === 'zcode' ? zcodeFlavor(meta) : null,
|
|
203
|
+
canSwitch: Boolean(meta.session?.account || meta.credentials || meta.qoderAuth || a.provider === 'catpaw' || a.provider === 'trae'),
|
|
204
|
+
verified: a.verified ?? null,
|
|
205
|
+
createdAt: a.createdAt,
|
|
206
|
+
lastCheckin: a.lastCheckin,
|
|
207
|
+
lastResult: a.lastResult || null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── 面板设置(访问密码等;密码只存数据目录 settings.json,永不通过接口回显) ───
|
|
212
|
+
|
|
213
|
+
export async function loadSettings() {
|
|
214
|
+
const s = await readJson(SETTINGS_FILE(), {});
|
|
215
|
+
return s && typeof s === 'object' && !Array.isArray(s) ? s : {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 合并写入设置并返回合并后的完整对象(原子写) */
|
|
219
|
+
export async function saveSettings(patch) {
|
|
220
|
+
await ensureDir();
|
|
221
|
+
const next = Object.assign(await loadSettings(), patch);
|
|
222
|
+
await atomicWrite(SETTINGS_FILE(), JSON.stringify(next, null, 2));
|
|
223
|
+
return next;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── 运行状态(签到日历) ───
|
|
227
|
+
|
|
228
|
+
export async function loadState() {
|
|
229
|
+
const s = await readJson(STATE_FILE(), {});
|
|
230
|
+
return s && typeof s === 'object' ? s : {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function saveState(state) {
|
|
234
|
+
await ensureDir();
|
|
235
|
+
await atomicWrite(STATE_FILE(), JSON.stringify(state, null, 2));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 导入 / 导出(含 10router 互通)见 transfer.js
|