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.
@@ -1,229 +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
- }
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
@@ -2,7 +2,7 @@
2
2
  * 签到调度:普通账号每 ~2h 扫描一次;ZCode 自动活动领取使用独立的短周期调度。
3
3
  */
4
4
 
5
- import { loadAccounts, loadState, saveState, withAccounts } from './store.js';
5
+ import { loadAccounts, loadState, withAccounts, withState } from './store.js';
6
6
  import { productImpl } from './providers.js';
7
7
  import { productOf } from './constants.js';
8
8
  import { startUsageSyncScheduler } from './tenrouter.js';
@@ -299,7 +299,12 @@ async function runTickInner(opts) {
299
299
  if (updated.uid && !current.uid) current.uid = updated.uid;
300
300
  }
301
301
  });
302
- await saveState({ ...state, qoderDailyDone: memo, deviceClaim });
302
+ await withState((fresh) => {
303
+ // 只覆盖签到轮自己负责的两个键:state.json 里还有网关写的 zcodeGatewayExhausted,
304
+ // 拿本轮开头读到的旧快照整份落盘会把别人刚写的东西抹掉
305
+ fresh.qoderDailyDone = memo;
306
+ fresh.deviceClaim = deviceClaim;
307
+ });
303
308
  }
304
309
 
305
310
  if (zAccounts.length) {
package/src/constants.js CHANGED
@@ -35,7 +35,7 @@ export const WEB_USAGE_PATH = '/api/v2/me/usages/big_model_credits';
35
35
  export const JOB_TOKEN_EXCHANGE_PATH = '/api/v1/jobToken/exchange';
36
36
 
37
37
  // 账号的 provider = 产品 + 区域。新增产品时在此登记,并在 providers.js 挂上对应实现。
38
- export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw', 'trae'];
38
+ export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax'];
39
39
  export const PROVIDER_LABEL = {
40
40
  qoder: 'Qoder 国际版',
41
41
  'qoder-cn': 'Qoder 国内版',
@@ -45,8 +45,9 @@ export const PROVIDER_LABEL = {
45
45
  mirasim: 'mirasim',
46
46
  catpaw: '妙手',
47
47
  trae: 'Trae',
48
+ minimax: 'MiniMax',
48
49
  };
49
- /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw / trae) */
50
+ /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw / trae / minimax) */
50
51
  export const productOf = (provider) => {
51
52
  const p = String(provider);
52
53
  if (p.startsWith('workbuddy')) return 'workbuddy';
@@ -54,6 +55,7 @@ export const productOf = (provider) => {
54
55
  if (p === 'mirasim') return 'mirasim';
55
56
  if (p === 'catpaw') return 'catpaw';
56
57
  if (p.startsWith('trae')) return 'trae';
58
+ if (p === 'minimax') return 'minimax';
57
59
  return 'qoder';
58
60
  };
59
61
  /** provider 是否国内版 */