creditdaddy 1.2.0 → 1.3.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 +16 -8
- package/package.json +2 -2
- package/src/accounts.js +18 -7
- package/src/authDevice.js +3 -0
- package/src/catpawClient.js +103 -103
- package/src/catpawLocal.js +229 -229
- package/src/checkin.js +7 -2
- package/src/constants.js +4 -2
- package/src/daemon.js +73 -11
- package/src/localDetect.js +147 -147
- package/src/logger.js +60 -7
- package/src/minimaxAuth.js +127 -0
- package/src/minimaxClient.js +330 -0
- package/src/minimaxGateway.js +404 -0
- package/src/minimaxLocal.js +259 -0
- package/src/mirasimClient.js +189 -189
- package/src/panel.html +214 -29
- package/src/providers.js +26 -0
- package/src/qoderApp.js +3 -9
- package/src/qoderClient.js +237 -237
- package/src/qoderUmid.js +198 -198
- package/src/store.js +49 -6
- package/src/tenrouter.js +8 -0
- package/src/tenrouterAccounts.js +7 -0
- package/src/transfer.js +24 -1
- package/src/usageSync.js +505 -505
- package/src/winProc.js +36 -0
- package/src/workbuddyLocal.js +133 -133
- package/src/zcodeAuth.js +131 -131
- package/src/zcodeAutoClaim.js +97 -97
- package/src/zcodeClient.js +30 -13
- package/src/zcodeGateway.js +79 -33
- package/src/zcrypto.js +160 -160
package/src/qoderUmid.js
CHANGED
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qoder 设备身份组件 —— 给没有 Qoder 客户端的 Linux(fnOS / NAS)提供国际版签到所需的风控身份。
|
|
3
|
-
*
|
|
4
|
-
* Qoder 国际版只向带 Cosy-MachineToken / Code / Type 的请求下发「每天领 100 Credits」,
|
|
5
|
-
* 这三个值由 Qoder 的 UMID 程序(runtime-info)生成。桌面端客户端自带它;官方 CLI
|
|
6
|
-
* @qoder-ai/qodercli 则把各平台的 UMID 二进制以 base64 内嵌在 bundle/qodercli.js 里,Linux x64 / arm64 都有。
|
|
7
|
-
*
|
|
8
|
-
* 这里在用户确认后从 npm 仓库下载官方 qodercli 包(校验 npm 的 sha512 integrity),
|
|
9
|
-
* 按本机 CPU 架构取出对应的 ELF,试跑一次确认能产出身份,再存到 ~/.creditdaddy/qoder-umid/。
|
|
10
|
-
* 不随 CreditDaddy 分发该二进制;调用方式与 qodercli 一致:runtime-info <env>,第一行输出 JSON。
|
|
11
|
-
* env:国际版 4(SINGAPORE,qodercli 的全球区取值),国内版 0(ONLINE)。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import fs from 'node:fs';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
import crypto from 'node:crypto';
|
|
17
|
-
import zlib from 'node:zlib';
|
|
18
|
-
import { execFile } from 'node:child_process';
|
|
19
|
-
import { dataDir } from './store.js';
|
|
20
|
-
import { logger } from './logger.js';
|
|
21
|
-
|
|
22
|
-
const PKG = '@qoder-ai/qodercli';
|
|
23
|
-
const REGISTRIES = ['https://registry.npmmirror.com', 'https://registry.npmjs.org'];
|
|
24
|
-
const AUTH_REGISTRY = 'https://registry.npmjs.org'; // integrity 的权威来源(镜像只用于加速下载)
|
|
25
|
-
const ELF_MACHINE = { x64: 62, arm64: 183 };
|
|
26
|
-
const BUNDLE_ENTRY = 'package/bundle/qodercli.js';
|
|
27
|
-
const FETCH_TIMEOUT_MS = 180_000;
|
|
28
|
-
const META_TIMEOUT_MS = 15_000;
|
|
29
|
-
export const CLI_RISK_ENV = { qoder: 4, 'qoder-cn': 0 };
|
|
30
|
-
|
|
31
|
-
export function umidSupported(platform = process.platform, arch = process.arch) {
|
|
32
|
-
return platform === 'linux' && Boolean(ELF_MACHINE[arch]);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const dir = () => path.join(dataDir(), 'qoder-umid');
|
|
36
|
-
const binPath = () => path.join(dir(), 'runtime-info');
|
|
37
|
-
const manifestPath = () => path.join(dir(), 'manifest.json');
|
|
38
|
-
|
|
39
|
-
/** 已安装的组件:{ path, version, arch, sha256, installedAt, registry };未安装返回 null */
|
|
40
|
-
export function installedUmid() {
|
|
41
|
-
if (!umidSupported()) return null;
|
|
42
|
-
try {
|
|
43
|
-
const m = JSON.parse(fs.readFileSync(manifestPath(), 'utf8'));
|
|
44
|
-
if (!fs.existsSync(binPath()) || m.arch !== process.arch) return null;
|
|
45
|
-
return { ...m, path: binPath() };
|
|
46
|
-
} catch { return null; }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── 纯函数(便于单测) ──
|
|
50
|
-
|
|
51
|
-
/** 最小 tar 解析:按名字取一个普通文件的内容 */
|
|
52
|
-
export function tarEntry(tar, wanted) {
|
|
53
|
-
let off = 0;
|
|
54
|
-
const cstr = (b, s, n) => { const x = b.subarray(s, s + n); const z = x.indexOf(0); return x.subarray(0, z < 0 ? n : z).toString('utf8'); };
|
|
55
|
-
let longName = null;
|
|
56
|
-
while (off + 512 <= tar.length) {
|
|
57
|
-
const h = tar.subarray(off, off + 512);
|
|
58
|
-
if (h.every((b) => b === 0)) break;
|
|
59
|
-
const size = parseInt(cstr(h, 124, 12).trim() || '0', 8);
|
|
60
|
-
const type = String.fromCharCode(h[156] || 48);
|
|
61
|
-
const prefix = cstr(h, 345, 155);
|
|
62
|
-
let name = longName || (prefix ? prefix + '/' : '') + cstr(h, 0, 100);
|
|
63
|
-
longName = null;
|
|
64
|
-
const body = tar.subarray(off + 512, off + 512 + size);
|
|
65
|
-
off += 512 + Math.ceil(size / 512) * 512;
|
|
66
|
-
if (type === 'L') { longName = body.toString('utf8').replace(/\0+$/, ''); continue; }
|
|
67
|
-
if (type === 'x') { // pax 扩展头里的 path 覆盖下一个条目的名字
|
|
68
|
-
const m = /\d+ path=([^\n]+)\n/.exec(body.toString('utf8'));
|
|
69
|
-
if (m) longName = m[1];
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if ((type === '0' || type === '\0') && name === wanted) return body;
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** 在 qodercli bundle 文本里找内嵌的 ELF(base64 字面量),按 e_machine 挑出本机架构 */
|
|
78
|
-
export function extractElf(jsText, machine) {
|
|
79
|
-
const MARK = '"f0VMRgIB'; // "\x7fELF" + 64 位 + 小端 的 base64 开头
|
|
80
|
-
let i = jsText.indexOf(MARK);
|
|
81
|
-
while (i >= 0) {
|
|
82
|
-
const start = i + 1;
|
|
83
|
-
const end = jsText.indexOf('"', start);
|
|
84
|
-
if (end < 0) break;
|
|
85
|
-
const head = Buffer.from(jsText.slice(start, start + 28), 'base64');
|
|
86
|
-
if (head.length >= 20 && head.readUInt16LE(18) === machine) {
|
|
87
|
-
return Buffer.from(jsText.slice(start, end), 'base64');
|
|
88
|
-
}
|
|
89
|
-
i = jsText.indexOf(MARK, end);
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** npm integrity(sha512-<base64>)校验 */
|
|
95
|
-
export function checkIntegrity(buf, integrity) {
|
|
96
|
-
const m = /^sha512-(.+)$/.exec(String(integrity || '').split(/\s+/)[0]);
|
|
97
|
-
if (!m) return false;
|
|
98
|
-
return crypto.createHash('sha512').update(buf).digest('base64') === m[1];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── 安装 ──
|
|
102
|
-
|
|
103
|
-
async function fetchBuf(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
104
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
105
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}:${url}`);
|
|
106
|
-
return Buffer.from(await res.arrayBuffer());
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 从 npmjs 拉权威 integrity。镜像(npmmirror)的元数据与 tarball 同源,
|
|
111
|
-
* 镜像被污染时它自己的 integrity 校验形同虚设,所以校验值以 npmjs 为准;
|
|
112
|
-
* npmjs 不可达时返回 null,退回镜像自身 integrity(与旧行为一致)。
|
|
113
|
-
*/
|
|
114
|
-
async function authoritativeIntegrity() {
|
|
115
|
-
try {
|
|
116
|
-
const meta = JSON.parse((await fetchBuf(`${AUTH_REGISTRY}/${PKG.replace('/', '%2f')}/latest`, META_TIMEOUT_MS)).toString('utf8'));
|
|
117
|
-
if (meta?.dist?.integrity && meta?.version) return { integrity: meta.dist.integrity, version: meta.version };
|
|
118
|
-
} catch {}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function testRun(file) {
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
execFile(file, [String(CLI_RISK_ENV.qoder)], { timeout: 30_000, maxBuffer: 1 << 20 }, (err, stdout) => {
|
|
125
|
-
try {
|
|
126
|
-
const j = JSON.parse(String(stdout).split('\n')[0]);
|
|
127
|
-
if (typeof j.machineToken === 'string' && j.machineToken && j.machineCode && j.machineType) return resolve(j);
|
|
128
|
-
} catch {}
|
|
129
|
-
reject(new Error('组件试运行失败' + (err ? ':' + err.message : '(输出里没有设备身份)')));
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let installing = null;
|
|
135
|
-
|
|
136
|
-
/** 下载官方 qodercli 包并提取本机架构的 UMID;并发调用共用同一次安装 */
|
|
137
|
-
export function installUmid() {
|
|
138
|
-
if (!installing) installing = doInstall().finally(() => { installing = null; });
|
|
139
|
-
return installing;
|
|
140
|
-
}
|
|
141
|
-
export const umidInstalling = () => Boolean(installing);
|
|
142
|
-
|
|
143
|
-
async function doInstall() {
|
|
144
|
-
if (!umidSupported()) throw new Error(`当前平台(${process.platform}/${process.arch})不需要或不支持该组件`);
|
|
145
|
-
const authority = await authoritativeIntegrity();
|
|
146
|
-
const errors = [];
|
|
147
|
-
for (const registry of REGISTRIES) {
|
|
148
|
-
try {
|
|
149
|
-
const meta = JSON.parse((await fetchBuf(`${registry}/${PKG.replace('/', '%2f')}/latest`, META_TIMEOUT_MS)).toString('utf8'));
|
|
150
|
-
const { tarball, integrity } = meta.dist || {};
|
|
151
|
-
if (!tarball || !integrity) throw new Error('包元数据缺少 tarball / integrity');
|
|
152
|
-
// 镜像版本与 npmjs 不一致时没有权威校验值可比,跳过镜像直接用官方源
|
|
153
|
-
if (authority && registry !== AUTH_REGISTRY && meta.version !== authority.version) {
|
|
154
|
-
throw new Error(`镜像版本(${meta.version})与 npmjs(${authority.version})不一致`);
|
|
155
|
-
}
|
|
156
|
-
const expected = authority && authority.version === meta.version ? authority.integrity : integrity;
|
|
157
|
-
logger.info('UMID', `下载 ${PKG}@${meta.version}(${registry})`);
|
|
158
|
-
const tgz = await fetchBuf(tarball);
|
|
159
|
-
if (!checkIntegrity(tgz, expected)) throw new Error('下载内容与 npm integrity 不一致');
|
|
160
|
-
const bundle = tarEntry(zlib.gunzipSync(tgz), BUNDLE_ENTRY);
|
|
161
|
-
if (!bundle) throw new Error(`包里没有 ${BUNDLE_ENTRY}(qodercli 结构可能变化)`);
|
|
162
|
-
const elf = extractElf(bundle.toString('latin1'), ELF_MACHINE[process.arch]);
|
|
163
|
-
if (!elf) throw new Error(`qodercli ${meta.version} 里没有 ${process.arch} 的 UMID 程序(结构可能变化)`);
|
|
164
|
-
|
|
165
|
-
fs.mkdirSync(dir(), { recursive: true, mode: 0o700 });
|
|
166
|
-
const tmp = binPath() + '.' + process.pid + '.tmp';
|
|
167
|
-
fs.writeFileSync(tmp, elf, { mode: 0o755 });
|
|
168
|
-
try { await testRun(tmp); } catch (e) { fs.rmSync(tmp, { force: true }); throw e; }
|
|
169
|
-
fs.renameSync(tmp, binPath());
|
|
170
|
-
const manifest = {
|
|
171
|
-
version: meta.version, arch: process.arch, registry,
|
|
172
|
-
sha256: crypto.createHash('sha256').update(elf).digest('hex'),
|
|
173
|
-
installedAt: new Date().toISOString(),
|
|
174
|
-
};
|
|
175
|
-
fs.writeFileSync(manifestPath(), JSON.stringify(manifest, null, 2), { mode: 0o600 });
|
|
176
|
-
logger.info('UMID', `设备身份组件已安装:qodercli ${meta.version} / ${process.arch}`);
|
|
177
|
-
return { ...manifest, path: binPath() };
|
|
178
|
-
} catch (e) {
|
|
179
|
-
errors.push(`${registry}:${e.message}`);
|
|
180
|
-
logger.warn('UMID', `从 ${registry} 安装失败:${e.message}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
throw new Error('设备身份组件安装失败 —— ' + errors.join(';'));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function removeUmid() {
|
|
187
|
-
fs.rmSync(dir(), { recursive: true, force: true });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** 面板 / 状态接口用的摘要 */
|
|
191
|
-
export function umidInfo() {
|
|
192
|
-
const m = installedUmid();
|
|
193
|
-
return {
|
|
194
|
-
supported: umidSupported(),
|
|
195
|
-
installing: umidInstalling(),
|
|
196
|
-
installed: m ? { version: m.version, arch: m.arch, installedAt: m.installedAt } : null,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Qoder 设备身份组件 —— 给没有 Qoder 客户端的 Linux(fnOS / NAS)提供国际版签到所需的风控身份。
|
|
3
|
+
*
|
|
4
|
+
* Qoder 国际版只向带 Cosy-MachineToken / Code / Type 的请求下发「每天领 100 Credits」,
|
|
5
|
+
* 这三个值由 Qoder 的 UMID 程序(runtime-info)生成。桌面端客户端自带它;官方 CLI
|
|
6
|
+
* @qoder-ai/qodercli 则把各平台的 UMID 二进制以 base64 内嵌在 bundle/qodercli.js 里,Linux x64 / arm64 都有。
|
|
7
|
+
*
|
|
8
|
+
* 这里在用户确认后从 npm 仓库下载官方 qodercli 包(校验 npm 的 sha512 integrity),
|
|
9
|
+
* 按本机 CPU 架构取出对应的 ELF,试跑一次确认能产出身份,再存到 ~/.creditdaddy/qoder-umid/。
|
|
10
|
+
* 不随 CreditDaddy 分发该二进制;调用方式与 qodercli 一致:runtime-info <env>,第一行输出 JSON。
|
|
11
|
+
* env:国际版 4(SINGAPORE,qodercli 的全球区取值),国内版 0(ONLINE)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import zlib from 'node:zlib';
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { dataDir } from './store.js';
|
|
20
|
+
import { logger } from './logger.js';
|
|
21
|
+
|
|
22
|
+
const PKG = '@qoder-ai/qodercli';
|
|
23
|
+
const REGISTRIES = ['https://registry.npmmirror.com', 'https://registry.npmjs.org'];
|
|
24
|
+
const AUTH_REGISTRY = 'https://registry.npmjs.org'; // integrity 的权威来源(镜像只用于加速下载)
|
|
25
|
+
const ELF_MACHINE = { x64: 62, arm64: 183 };
|
|
26
|
+
const BUNDLE_ENTRY = 'package/bundle/qodercli.js';
|
|
27
|
+
const FETCH_TIMEOUT_MS = 180_000;
|
|
28
|
+
const META_TIMEOUT_MS = 15_000;
|
|
29
|
+
export const CLI_RISK_ENV = { qoder: 4, 'qoder-cn': 0 };
|
|
30
|
+
|
|
31
|
+
export function umidSupported(platform = process.platform, arch = process.arch) {
|
|
32
|
+
return platform === 'linux' && Boolean(ELF_MACHINE[arch]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dir = () => path.join(dataDir(), 'qoder-umid');
|
|
36
|
+
const binPath = () => path.join(dir(), 'runtime-info');
|
|
37
|
+
const manifestPath = () => path.join(dir(), 'manifest.json');
|
|
38
|
+
|
|
39
|
+
/** 已安装的组件:{ path, version, arch, sha256, installedAt, registry };未安装返回 null */
|
|
40
|
+
export function installedUmid() {
|
|
41
|
+
if (!umidSupported()) return null;
|
|
42
|
+
try {
|
|
43
|
+
const m = JSON.parse(fs.readFileSync(manifestPath(), 'utf8'));
|
|
44
|
+
if (!fs.existsSync(binPath()) || m.arch !== process.arch) return null;
|
|
45
|
+
return { ...m, path: binPath() };
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 纯函数(便于单测) ──
|
|
50
|
+
|
|
51
|
+
/** 最小 tar 解析:按名字取一个普通文件的内容 */
|
|
52
|
+
export function tarEntry(tar, wanted) {
|
|
53
|
+
let off = 0;
|
|
54
|
+
const cstr = (b, s, n) => { const x = b.subarray(s, s + n); const z = x.indexOf(0); return x.subarray(0, z < 0 ? n : z).toString('utf8'); };
|
|
55
|
+
let longName = null;
|
|
56
|
+
while (off + 512 <= tar.length) {
|
|
57
|
+
const h = tar.subarray(off, off + 512);
|
|
58
|
+
if (h.every((b) => b === 0)) break;
|
|
59
|
+
const size = parseInt(cstr(h, 124, 12).trim() || '0', 8);
|
|
60
|
+
const type = String.fromCharCode(h[156] || 48);
|
|
61
|
+
const prefix = cstr(h, 345, 155);
|
|
62
|
+
let name = longName || (prefix ? prefix + '/' : '') + cstr(h, 0, 100);
|
|
63
|
+
longName = null;
|
|
64
|
+
const body = tar.subarray(off + 512, off + 512 + size);
|
|
65
|
+
off += 512 + Math.ceil(size / 512) * 512;
|
|
66
|
+
if (type === 'L') { longName = body.toString('utf8').replace(/\0+$/, ''); continue; }
|
|
67
|
+
if (type === 'x') { // pax 扩展头里的 path 覆盖下一个条目的名字
|
|
68
|
+
const m = /\d+ path=([^\n]+)\n/.exec(body.toString('utf8'));
|
|
69
|
+
if (m) longName = m[1];
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if ((type === '0' || type === '\0') && name === wanted) return body;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 在 qodercli bundle 文本里找内嵌的 ELF(base64 字面量),按 e_machine 挑出本机架构 */
|
|
78
|
+
export function extractElf(jsText, machine) {
|
|
79
|
+
const MARK = '"f0VMRgIB'; // "\x7fELF" + 64 位 + 小端 的 base64 开头
|
|
80
|
+
let i = jsText.indexOf(MARK);
|
|
81
|
+
while (i >= 0) {
|
|
82
|
+
const start = i + 1;
|
|
83
|
+
const end = jsText.indexOf('"', start);
|
|
84
|
+
if (end < 0) break;
|
|
85
|
+
const head = Buffer.from(jsText.slice(start, start + 28), 'base64');
|
|
86
|
+
if (head.length >= 20 && head.readUInt16LE(18) === machine) {
|
|
87
|
+
return Buffer.from(jsText.slice(start, end), 'base64');
|
|
88
|
+
}
|
|
89
|
+
i = jsText.indexOf(MARK, end);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** npm integrity(sha512-<base64>)校验 */
|
|
95
|
+
export function checkIntegrity(buf, integrity) {
|
|
96
|
+
const m = /^sha512-(.+)$/.exec(String(integrity || '').split(/\s+/)[0]);
|
|
97
|
+
if (!m) return false;
|
|
98
|
+
return crypto.createHash('sha512').update(buf).digest('base64') === m[1];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 安装 ──
|
|
102
|
+
|
|
103
|
+
async function fetchBuf(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
104
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
105
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}:${url}`);
|
|
106
|
+
return Buffer.from(await res.arrayBuffer());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 从 npmjs 拉权威 integrity。镜像(npmmirror)的元数据与 tarball 同源,
|
|
111
|
+
* 镜像被污染时它自己的 integrity 校验形同虚设,所以校验值以 npmjs 为准;
|
|
112
|
+
* npmjs 不可达时返回 null,退回镜像自身 integrity(与旧行为一致)。
|
|
113
|
+
*/
|
|
114
|
+
async function authoritativeIntegrity() {
|
|
115
|
+
try {
|
|
116
|
+
const meta = JSON.parse((await fetchBuf(`${AUTH_REGISTRY}/${PKG.replace('/', '%2f')}/latest`, META_TIMEOUT_MS)).toString('utf8'));
|
|
117
|
+
if (meta?.dist?.integrity && meta?.version) return { integrity: meta.dist.integrity, version: meta.version };
|
|
118
|
+
} catch {}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function testRun(file) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
execFile(file, [String(CLI_RISK_ENV.qoder)], { timeout: 30_000, maxBuffer: 1 << 20 }, (err, stdout) => {
|
|
125
|
+
try {
|
|
126
|
+
const j = JSON.parse(String(stdout).split('\n')[0]);
|
|
127
|
+
if (typeof j.machineToken === 'string' && j.machineToken && j.machineCode && j.machineType) return resolve(j);
|
|
128
|
+
} catch {}
|
|
129
|
+
reject(new Error('组件试运行失败' + (err ? ':' + err.message : '(输出里没有设备身份)')));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let installing = null;
|
|
135
|
+
|
|
136
|
+
/** 下载官方 qodercli 包并提取本机架构的 UMID;并发调用共用同一次安装 */
|
|
137
|
+
export function installUmid() {
|
|
138
|
+
if (!installing) installing = doInstall().finally(() => { installing = null; });
|
|
139
|
+
return installing;
|
|
140
|
+
}
|
|
141
|
+
export const umidInstalling = () => Boolean(installing);
|
|
142
|
+
|
|
143
|
+
async function doInstall() {
|
|
144
|
+
if (!umidSupported()) throw new Error(`当前平台(${process.platform}/${process.arch})不需要或不支持该组件`);
|
|
145
|
+
const authority = await authoritativeIntegrity();
|
|
146
|
+
const errors = [];
|
|
147
|
+
for (const registry of REGISTRIES) {
|
|
148
|
+
try {
|
|
149
|
+
const meta = JSON.parse((await fetchBuf(`${registry}/${PKG.replace('/', '%2f')}/latest`, META_TIMEOUT_MS)).toString('utf8'));
|
|
150
|
+
const { tarball, integrity } = meta.dist || {};
|
|
151
|
+
if (!tarball || !integrity) throw new Error('包元数据缺少 tarball / integrity');
|
|
152
|
+
// 镜像版本与 npmjs 不一致时没有权威校验值可比,跳过镜像直接用官方源
|
|
153
|
+
if (authority && registry !== AUTH_REGISTRY && meta.version !== authority.version) {
|
|
154
|
+
throw new Error(`镜像版本(${meta.version})与 npmjs(${authority.version})不一致`);
|
|
155
|
+
}
|
|
156
|
+
const expected = authority && authority.version === meta.version ? authority.integrity : integrity;
|
|
157
|
+
logger.info('UMID', `下载 ${PKG}@${meta.version}(${registry})`);
|
|
158
|
+
const tgz = await fetchBuf(tarball);
|
|
159
|
+
if (!checkIntegrity(tgz, expected)) throw new Error('下载内容与 npm integrity 不一致');
|
|
160
|
+
const bundle = tarEntry(zlib.gunzipSync(tgz), BUNDLE_ENTRY);
|
|
161
|
+
if (!bundle) throw new Error(`包里没有 ${BUNDLE_ENTRY}(qodercli 结构可能变化)`);
|
|
162
|
+
const elf = extractElf(bundle.toString('latin1'), ELF_MACHINE[process.arch]);
|
|
163
|
+
if (!elf) throw new Error(`qodercli ${meta.version} 里没有 ${process.arch} 的 UMID 程序(结构可能变化)`);
|
|
164
|
+
|
|
165
|
+
fs.mkdirSync(dir(), { recursive: true, mode: 0o700 });
|
|
166
|
+
const tmp = binPath() + '.' + process.pid + '.tmp';
|
|
167
|
+
fs.writeFileSync(tmp, elf, { mode: 0o755 });
|
|
168
|
+
try { await testRun(tmp); } catch (e) { fs.rmSync(tmp, { force: true }); throw e; }
|
|
169
|
+
fs.renameSync(tmp, binPath());
|
|
170
|
+
const manifest = {
|
|
171
|
+
version: meta.version, arch: process.arch, registry,
|
|
172
|
+
sha256: crypto.createHash('sha256').update(elf).digest('hex'),
|
|
173
|
+
installedAt: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
fs.writeFileSync(manifestPath(), JSON.stringify(manifest, null, 2), { mode: 0o600 });
|
|
176
|
+
logger.info('UMID', `设备身份组件已安装:qodercli ${meta.version} / ${process.arch}`);
|
|
177
|
+
return { ...manifest, path: binPath() };
|
|
178
|
+
} catch (e) {
|
|
179
|
+
errors.push(`${registry}:${e.message}`);
|
|
180
|
+
logger.warn('UMID', `从 ${registry} 安装失败:${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
throw new Error('设备身份组件安装失败 —— ' + errors.join(';'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function removeUmid() {
|
|
187
|
+
fs.rmSync(dir(), { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 面板 / 状态接口用的摘要 */
|
|
191
|
+
export function umidInfo() {
|
|
192
|
+
const m = installedUmid();
|
|
193
|
+
return {
|
|
194
|
+
supported: umidSupported(),
|
|
195
|
+
installing: umidInstalling(),
|
|
196
|
+
installed: m ? { version: m.version, arch: m.arch, installedAt: m.installedAt } : null,
|
|
197
|
+
};
|
|
198
|
+
}
|
package/src/store.js
CHANGED
|
@@ -215,12 +215,37 @@ export async function loadSettings() {
|
|
|
215
215
|
return s && typeof s === 'object' && !Array.isArray(s) ? s : {};
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
// 与 accountsQueue 同样的串行队列:saveSettings 内部是「读 → 合并 → 写」三步,
|
|
219
|
+
// 不串行的话两个并发写(面板改密码 + 网关切局域网)会各自基于旧快照落盘,互相覆盖。
|
|
220
|
+
let settingsQueue = Promise.resolve();
|
|
221
|
+
|
|
222
|
+
/** 合并写入设置并返回合并后的完整对象(原子写,串行执行) */
|
|
223
|
+
export function saveSettings(patch) {
|
|
224
|
+
const run = settingsQueue.then(async () => {
|
|
225
|
+
await ensureDir();
|
|
226
|
+
const next = Object.assign(await loadSettings(), patch);
|
|
227
|
+
await atomicWrite(SETTINGS_FILE(), JSON.stringify(next, null, 2));
|
|
228
|
+
return next;
|
|
229
|
+
});
|
|
230
|
+
settingsQueue = run.catch(() => {});
|
|
231
|
+
return run;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 串行地读取 → 修改 settings.json。fn 直接改传入对象即可,返回 fn 的结果。
|
|
236
|
+
* 只用于「读出来算一算再写回去」的调用方(单次 saveSettings 已经是原子的)。
|
|
237
|
+
* 锁顺序:先 accountsQueue 再 settingsQueue,禁止反向嵌套,否则会死锁。
|
|
238
|
+
*/
|
|
239
|
+
export function withSettings(fn) {
|
|
240
|
+
const run = settingsQueue.then(async () => {
|
|
241
|
+
await ensureDir();
|
|
242
|
+
const settings = await loadSettings();
|
|
243
|
+
const result = await fn(settings);
|
|
244
|
+
await atomicWrite(SETTINGS_FILE(), JSON.stringify(settings, null, 2));
|
|
245
|
+
return result;
|
|
246
|
+
});
|
|
247
|
+
settingsQueue = run.catch(() => {});
|
|
248
|
+
return run;
|
|
224
249
|
}
|
|
225
250
|
|
|
226
251
|
// ─── 运行状态(签到日历) ───
|
|
@@ -235,4 +260,22 @@ export async function saveState(state) {
|
|
|
235
260
|
await atomicWrite(STATE_FILE(), JSON.stringify(state, null, 2));
|
|
236
261
|
}
|
|
237
262
|
|
|
263
|
+
// state.json 有多个互不相干的写入方(签到轮写 qoderDailyDone / deviceClaim,
|
|
264
|
+
// 网关写 zcodeGatewayExhausted),各自「读 → 改几个键 → 写回」会丢更新:
|
|
265
|
+
// 签到轮拿旧快照落盘,就把网关刚写的耗尽打标抹掉了。与 settings 共用一个队列。
|
|
266
|
+
let stateQueue = Promise.resolve();
|
|
267
|
+
|
|
268
|
+
/** 串行地读取 → 修改 → 保存运行状态;fn 直接改传入对象即可,返回 fn 的结果。 */
|
|
269
|
+
export function withState(fn) {
|
|
270
|
+
const run = stateQueue.then(async () => {
|
|
271
|
+
await ensureDir();
|
|
272
|
+
const state = await loadState();
|
|
273
|
+
const result = await fn(state);
|
|
274
|
+
await atomicWrite(STATE_FILE(), JSON.stringify(state, null, 2));
|
|
275
|
+
return result;
|
|
276
|
+
});
|
|
277
|
+
stateQueue = run.catch(() => {});
|
|
278
|
+
return run;
|
|
279
|
+
}
|
|
280
|
+
|
|
238
281
|
// 导入 / 导出(含 10router 互通)见 transfer.js
|
package/src/tenrouter.js
CHANGED
|
@@ -123,8 +123,16 @@ async function call(c, pathname, { method = 'GET', body, timeout = QUOTA_TIMEOUT
|
|
|
123
123
|
headers: { Authorization: `Bearer ${c.key}`, Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}) },
|
|
124
124
|
body: body ? JSON.stringify(body) : undefined,
|
|
125
125
|
signal: AbortSignal.timeout(timeout),
|
|
126
|
+
// 请求体里是本机各账号的 token/用量,Authorization 是 10Router 的 key。
|
|
127
|
+
// fetch 默认跟随重定向,而 307/308 会把请求体原样重放到新地址(Authorization 会被
|
|
128
|
+
// 剥掉,但 body 不会)——一个被 307 的 10Router 就足以把凭据送到别处。直接拒绝重定向。
|
|
129
|
+
redirect: 'error',
|
|
126
130
|
});
|
|
127
131
|
} catch (e) {
|
|
132
|
+
// redirect:'error' 抛的是 cause.message === 'unexpected redirect'(顶层 message 只是 "fetch failed")
|
|
133
|
+
if (e?.cause?.message === 'unexpected redirect') {
|
|
134
|
+
throw new Error(`10Router(${c.endpoint})返回了重定向,已拒绝跟随:凭据只发往你填写的地址,请把地址改成最终地址`);
|
|
135
|
+
}
|
|
128
136
|
throw new Error(`无法连接 10Router(${c.endpoint}):${e.cause?.code || e.message}`);
|
|
129
137
|
}
|
|
130
138
|
const text = await res.text();
|
package/src/tenrouterAccounts.js
CHANGED
|
@@ -41,8 +41,15 @@ async function trFetch(url, { key, cookie, method = 'GET', body } = {}) {
|
|
|
41
41
|
},
|
|
42
42
|
body: body ? JSON.stringify(body) : undefined,
|
|
43
43
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
44
|
+
// 这里的 body 是账号明文 token(/api/oauth/transfer/import)与 apiKey(/api/providers)。
|
|
45
|
+
// 307/308 重定向会把 body 原样重放到新地址,Authorization 反而会被剥掉——所以必须拒绝。
|
|
46
|
+
redirect: 'error',
|
|
44
47
|
});
|
|
45
48
|
} catch (e) {
|
|
49
|
+
// redirect:'error' 抛的是 cause.message === 'unexpected redirect'(顶层 message 只是 "fetch failed")
|
|
50
|
+
if (e?.cause?.message === 'unexpected redirect') {
|
|
51
|
+
throw new Error(`10Router 返回了重定向,已拒绝跟随:账号凭据只发往你填写的地址,请把地址改成最终地址`);
|
|
52
|
+
}
|
|
46
53
|
throw new Error(`无法连接 10Router:${e.cause?.code || e.message}`);
|
|
47
54
|
}
|
|
48
55
|
const text = await res.text();
|
package/src/transfer.js
CHANGED
|
@@ -37,6 +37,28 @@ function deriveKey(password, salt, kdf = KDF) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* 导入文件里的 kdf 参数不可信:文件自报 {N:2,r:1,p:1} 会让 scrypt 瞬间算完(0ms vs 42ms),
|
|
42
|
+
* 密钥强度被文件作者单方面降级,而且没有任何地方会提示。KDF 是本格式的固定契约(见文件头),
|
|
43
|
+
* 不一致就拒收。注意 kdf 整个对象缺失时要报「参数不对」而不是「口令错误」——后者会把
|
|
44
|
+
* 用户的排查方向带偏。
|
|
45
|
+
*/
|
|
46
|
+
function checkKdf(blob) {
|
|
47
|
+
const k = blob?.kdf;
|
|
48
|
+
if (!k || typeof k !== 'object') {
|
|
49
|
+
throw new TransferError('BAD_KDF', '迁移文件缺少 KDF 参数,可能不是本工具导出的文件');
|
|
50
|
+
}
|
|
51
|
+
if (k.alg !== KDF.alg) {
|
|
52
|
+
throw new TransferError('BAD_KDF', `不支持的 KDF 算法:${k.alg}`);
|
|
53
|
+
}
|
|
54
|
+
for (const f of ['N', 'r', 'p', 'keyLen']) {
|
|
55
|
+
if (k[f] !== KDF[f]) {
|
|
56
|
+
throw new TransferError('BAD_KDF', `KDF 参数 ${f} 与本工具不一致(文件 ${k[f]},期望 ${KDF[f]}):为避免密钥强度被文件单方面降级,已拒绝导入`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return k;
|
|
60
|
+
}
|
|
61
|
+
|
|
40
62
|
export function sealTransfer(payload, password) {
|
|
41
63
|
if (typeof password !== 'string' || password.length < 4) {
|
|
42
64
|
throw new TransferError('PASSPHRASE_TOO_SHORT', '加密口令至少 4 个字符');
|
|
@@ -58,8 +80,9 @@ export function sealTransfer(payload, password) {
|
|
|
58
80
|
export function openTransfer(blob, password) {
|
|
59
81
|
if (blob?.format !== TRANSFER_FORMAT) throw new TransferError('UNSUPPORTED_FORMAT', '不是加密迁移文件');
|
|
60
82
|
if (!password) throw new TransferError('NEED_PASSWORD', '该文件已加密,请输入导出时设置的口令');
|
|
83
|
+
const kdf = checkKdf(blob);
|
|
61
84
|
try {
|
|
62
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(password, unb64(
|
|
85
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(password, unb64(kdf.salt), kdf), unb64(blob.iv));
|
|
63
86
|
decipher.setAuthTag(unb64(blob.tag));
|
|
64
87
|
const plain = Buffer.concat([decipher.update(unb64(blob.payload)), decipher.final()]);
|
|
65
88
|
return JSON.parse(plain.toString('utf8'));
|