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/zcodeLocal.js
CHANGED
|
@@ -1,319 +1,334 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 本机 ZCode 客户端凭据 — 读取已登录账号、切换当前账号。移植自 zcode-switch(MIT)src-tauri/src/store.rs。
|
|
3
|
-
*
|
|
4
|
-
* ZCode(智谱 GLM / Z.ai 桌面客户端)把当前登录态存在:
|
|
5
|
-
* ~/.zcode/v2/credentials.json 当前登录凭据(值多为 enc:v1: 加密,见 zcrypto.js)
|
|
6
|
-
* ~/.zcode/v2/config.json 当前 provider/模型配置(部分账号在此处存明文 API Key)
|
|
7
|
-
* ~/.zcode/v2/telemetry-state.json { deviceMid, lastDailyActiveDate }
|
|
8
|
-
* 只有一份"当前登录",没有像 WorkBuddy 那样的历史会话文件,所以 CreditDaddy 侧的多账号来自
|
|
9
|
-
* 每次 capture(拉取当前登录存一份)积累,而不是一次性读到全部账号。
|
|
10
|
-
*
|
|
11
|
-
* 切换账号(cold switch):
|
|
12
|
-
* - ZCode 正在运行时默认拒绝(避免运行中的客户端把内存里的旧登录覆盖回文件,即"assets 时间差"问题)
|
|
13
|
-
* - 写回前由调用方(daemon switch 路由)先把当前 live 登录同步 / 保存进账号库,绝不丢号
|
|
14
|
-
* - 每个 CreditDaddy 账号维护一个 virtual device_mid(随机 UUID,落盘进账号记录),切换时写回
|
|
15
|
-
* telemetry-state.json,让不同账号在 ZCode 风控眼里是不同设备,避免账号间互相牵连
|
|
16
|
-
* 不做 zcode-switch 的“热切换”(客户端运行中不重启直接换登录):那需要跟客户端内存状态打配合,
|
|
17
|
-
* 风险较高,这里只做“先确认客户端已退出”的冷切换,更符合“数据只读改,绝不直接动运行中的进程”的边界。
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import fs from 'node:fs';
|
|
21
|
-
import os from 'node:os';
|
|
22
|
-
import path from 'node:path';
|
|
23
|
-
import crypto from 'node:crypto';
|
|
24
|
-
import { execSync } from 'node:child_process';
|
|
25
|
-
import * as zc from './zcrypto.js';
|
|
26
|
-
|
|
27
|
-
function home() {
|
|
28
|
-
return process.env.ZCODE_HOME || os.homedir();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
export function
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 本机 ZCode 客户端凭据 — 读取已登录账号、切换当前账号。移植自 zcode-switch(MIT)src-tauri/src/store.rs。
|
|
3
|
+
*
|
|
4
|
+
* ZCode(智谱 GLM / Z.ai 桌面客户端)把当前登录态存在:
|
|
5
|
+
* ~/.zcode/v2/credentials.json 当前登录凭据(值多为 enc:v1: 加密,见 zcrypto.js)
|
|
6
|
+
* ~/.zcode/v2/config.json 当前 provider/模型配置(部分账号在此处存明文 API Key)
|
|
7
|
+
* ~/.zcode/v2/telemetry-state.json { deviceMid, lastDailyActiveDate }
|
|
8
|
+
* 只有一份"当前登录",没有像 WorkBuddy 那样的历史会话文件,所以 CreditDaddy 侧的多账号来自
|
|
9
|
+
* 每次 capture(拉取当前登录存一份)积累,而不是一次性读到全部账号。
|
|
10
|
+
*
|
|
11
|
+
* 切换账号(cold switch):
|
|
12
|
+
* - ZCode 正在运行时默认拒绝(避免运行中的客户端把内存里的旧登录覆盖回文件,即"assets 时间差"问题)
|
|
13
|
+
* - 写回前由调用方(daemon switch 路由)先把当前 live 登录同步 / 保存进账号库,绝不丢号
|
|
14
|
+
* - 每个 CreditDaddy 账号维护一个 virtual device_mid(随机 UUID,落盘进账号记录),切换时写回
|
|
15
|
+
* telemetry-state.json,让不同账号在 ZCode 风控眼里是不同设备,避免账号间互相牵连
|
|
16
|
+
* 不做 zcode-switch 的“热切换”(客户端运行中不重启直接换登录):那需要跟客户端内存状态打配合,
|
|
17
|
+
* 风险较高,这里只做“先确认客户端已退出”的冷切换,更符合“数据只读改,绝不直接动运行中的进程”的边界。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import crypto from 'node:crypto';
|
|
24
|
+
import { execSync } from 'node:child_process';
|
|
25
|
+
import * as zc from './zcrypto.js';
|
|
26
|
+
|
|
27
|
+
function home() {
|
|
28
|
+
return process.env.ZCODE_HOME || os.homedir();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ZCode 支持把数据目录整体挪盘,真实位置写在 ~/.zcode/v2/setting.json 的 dataBaseDir 里
|
|
33
|
+
* (口径对齐 pjpv/zcode-switch 的 resolve_data_root)。例如实测到 dataBaseDir = "D:\ZCodeData",
|
|
34
|
+
* 凭据于是在 D:\ZCodeData\.zcode\v2\credentials.json;而 ~/.zcode/v2/credentials.json
|
|
35
|
+
* 是挪盘前留下的陈旧副本,读它会一直看到旧账号、切换被误判成 alreadyActive。
|
|
36
|
+
*
|
|
37
|
+
* 只挪数据目录、不挪 home:凭据加密密钥仍由 os.homedir() 派生(见 zcrypto.defaultSecret),
|
|
38
|
+
* setting.json 也仍在 home 下(它就是引导信息本身)。
|
|
39
|
+
*/
|
|
40
|
+
function dataRoot(h) {
|
|
41
|
+
const s = readJson(path.join(h, '.zcode', 'v2', 'setting.json'));
|
|
42
|
+
const d = typeof s?.dataBaseDir === 'string' ? s.dataBaseDir.trim() : '';
|
|
43
|
+
return d && path.isAbsolute(d) ? d : h;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function zcodePaths() {
|
|
47
|
+
const h = home();
|
|
48
|
+
const v2 = path.join(dataRoot(h), '.zcode', 'v2');
|
|
49
|
+
return {
|
|
50
|
+
home: h,
|
|
51
|
+
credentials: path.join(v2, 'credentials.json'),
|
|
52
|
+
config: path.join(v2, 'config.json'),
|
|
53
|
+
setting: path.join(h, '.zcode', 'v2', 'setting.json'),
|
|
54
|
+
providerConfig: path.join(v2, 'provider_config.json'),
|
|
55
|
+
telemetry: path.join(v2, 'telemetry-state.json'),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readJson(file) {
|
|
60
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function atomicWriteJson(file, obj) {
|
|
64
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
65
|
+
const tmp = `${file}.tmp-${crypto.randomUUID()}`;
|
|
66
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
|
|
67
|
+
fs.renameSync(tmp, file);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function detectZcode() {
|
|
71
|
+
const p = zcodePaths();
|
|
72
|
+
const creds = readJson(p.credentials);
|
|
73
|
+
return {
|
|
74
|
+
dataDir: path.dirname(p.credentials),
|
|
75
|
+
exists: fs.existsSync(p.credentials),
|
|
76
|
+
signedIn: Boolean(creds && zc.isLoggedIn(creds)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 当前登录凭据 → 账号记录(供 addAccount),不做网络请求。返回 null 表示未登录或读取失败。 */
|
|
81
|
+
export function liveToAccount(secretOpts) {
|
|
82
|
+
const p = zcodePaths();
|
|
83
|
+
const creds = readJson(p.credentials);
|
|
84
|
+
if (!creds || !zc.isLoggedIn(creds)) return null;
|
|
85
|
+
const secret = zc.defaultSecret(p.home, secretOpts);
|
|
86
|
+
const id = zc.identityWithSecret(creds, secret);
|
|
87
|
+
return {
|
|
88
|
+
provider: 'zcode',
|
|
89
|
+
// ZCode 没有单一 access_token 字段可直接当 Bearer 用(quota 会在多个候选 token 里试),
|
|
90
|
+
// 这里存整份 creds 快照(仍是各字段各自的 enc:v1 密文),token 字段留一个可读标记供列表展示。
|
|
91
|
+
token: `zcode-creds:${id.userId || crypto.randomUUID()}`,
|
|
92
|
+
uid: id.userId,
|
|
93
|
+
name: zc.identityLabel(id),
|
|
94
|
+
email: id.email,
|
|
95
|
+
source: 'local-app',
|
|
96
|
+
meta: {
|
|
97
|
+
credentials: creds,
|
|
98
|
+
config: readJson(p.config),
|
|
99
|
+
setting: readJson(p.setting),
|
|
100
|
+
providerConfig: readJson(p.providerConfig),
|
|
101
|
+
canonicalHash: zc.canonicalHash(creds),
|
|
102
|
+
// 沿用本机当前的设备 ID:切回这个账号时还原成它原本的设备身份
|
|
103
|
+
deviceMid: readJson(p.telemetry)?.deviceMid || null,
|
|
104
|
+
capturedAt: new Date().toISOString(),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** ZCode 客户端当前登录账号的完整身份(未登录返回 null) */
|
|
110
|
+
export function currentZcodeIdentity() {
|
|
111
|
+
const p = zcodePaths();
|
|
112
|
+
const creds = readJson(p.credentials);
|
|
113
|
+
if (!creds || !zc.isLoggedIn(creds)) return null;
|
|
114
|
+
const id = zc.identityWithSecret(creds, zc.defaultSecret(p.home));
|
|
115
|
+
return { uid: id.userId ?? null, email: id.email || null, username: id.username || null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** ZCode 客户端当前登录账号的 uid(未登录返回 null) */
|
|
119
|
+
export function currentZcodeUid() {
|
|
120
|
+
const id = currentZcodeIdentity();
|
|
121
|
+
return id?.uid != null ? String(id.uid) : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 账号库里是否已有同一登录(先比规范化哈希,哈希对不上再比身份) */
|
|
125
|
+
export function findSameLogin(liveCreds, secret, existingAccounts) {
|
|
126
|
+
const hash = zc.canonicalHash(liveCreds);
|
|
127
|
+
const byHash = existingAccounts.find((a) => a.meta?.canonicalHash === hash);
|
|
128
|
+
if (byHash) return byHash;
|
|
129
|
+
const liveId = zc.identityWithSecret(liveCreds, secret);
|
|
130
|
+
if (!liveId.userId && !liveId.username && !liveId.email) return null;
|
|
131
|
+
return existingAccounts.find((a) => {
|
|
132
|
+
const cid = a.meta?.credentials && zc.identityWithSecret(a.meta.credentials, secret);
|
|
133
|
+
if (!cid) return false;
|
|
134
|
+
return Boolean(
|
|
135
|
+
(liveId.userId && cid.userId && liveId.userId === cid.userId)
|
|
136
|
+
|| (liveId.email && cid.email && liveId.email === cid.email)
|
|
137
|
+
|| (liveId.username && cid.username && liveId.username === cid.username),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const WIN_TASKLIST = () => {
|
|
143
|
+
try {
|
|
144
|
+
return execSync('tasklist /FI "IMAGENAME eq ZCode.exe" /NH', { windowsHide: true, encoding: 'utf8' });
|
|
145
|
+
} catch { return ''; }
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** ZCode 客户端是否在运行(仅 Windows 有实现;其他平台保守返回 false,即“允许切换”) */
|
|
149
|
+
export function zcodeRunning() {
|
|
150
|
+
if (process.platform !== 'win32') return false;
|
|
151
|
+
return /zcode\.exe/i.test(WIN_TASKLIST());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const nap = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 结束 ZCode 客户端进程(先优雅关闭,超时后强制 kill)。
|
|
158
|
+
* 只在用户明确选择「强制切换」时调用:运行中的客户端会把内存里的旧登录覆盖回
|
|
159
|
+
* credentials.json,只写文件不关进程等于没切换。
|
|
160
|
+
* 注意:不要在测试里调用——它会真的结束本机 ZCode。
|
|
161
|
+
*/
|
|
162
|
+
export function terminateZcode({ timeoutMs = 8000 } = {}) {
|
|
163
|
+
if (process.platform !== 'win32') return { closed: false, supported: false };
|
|
164
|
+
if (!zcodeRunning()) return { closed: false, running: false };
|
|
165
|
+
try { execSync('taskkill /IM ZCode.exe /T', { windowsHide: true, stdio: 'ignore' }); } catch {}
|
|
166
|
+
let deadline = Date.now() + timeoutMs;
|
|
167
|
+
while (Date.now() < deadline && zcodeRunning()) nap(250);
|
|
168
|
+
if (zcodeRunning()) {
|
|
169
|
+
try { execSync('taskkill /IM ZCode.exe /T /F', { windowsHide: true, stdio: 'ignore' }); } catch {}
|
|
170
|
+
deadline = Date.now() + 3000;
|
|
171
|
+
while (Date.now() < deadline && zcodeRunning()) nap(250);
|
|
172
|
+
}
|
|
173
|
+
return { closed: !zcodeRunning(), running: zcodeRunning() };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 切换到某个 CreditDaddy 账号对应的 ZCode 登录(冷切换)。
|
|
178
|
+
* @param {object} account 目标账号(需带 meta.credentials,来自本机导入)
|
|
179
|
+
* 调用方须在此之前把当前 live 登录保存进账号库(防丢号,见 daemon 的 switch 路由)。
|
|
180
|
+
* @param {{force?: boolean}} opts force:ZCode 正在运行时也强制写(不建议:客户端退出前可能把内存里的旧登录覆盖回文件)
|
|
181
|
+
* @returns {{switched: boolean, alreadyActive: boolean}}
|
|
182
|
+
*/
|
|
183
|
+
export function switchTo(account, { force = false } = {}) {
|
|
184
|
+
if (!account?.meta?.credentials) {
|
|
185
|
+
throw new Error('该账号没有保存 ZCode 凭据快照,无法切换(请重新从本机导入)');
|
|
186
|
+
}
|
|
187
|
+
const p = zcodePaths();
|
|
188
|
+
const secret = zc.defaultSecret(p.home);
|
|
189
|
+
const live = readJson(p.credentials);
|
|
190
|
+
const liveHash = live ? zc.canonicalHash(live) : null;
|
|
191
|
+
const targetHash = account.meta.canonicalHash || zc.canonicalHash(account.meta.credentials);
|
|
192
|
+
|
|
193
|
+
let alreadyActive = liveHash === targetHash;
|
|
194
|
+
if (!alreadyActive && live && zc.isLoggedIn(live)) {
|
|
195
|
+
const liveId = zc.identityWithSecret(live, secret);
|
|
196
|
+
const targetId = zc.identityWithSecret(account.meta.credentials, secret);
|
|
197
|
+
const hasSignal = (id) => Boolean(id.userId || id.username || id.email);
|
|
198
|
+
if (hasSignal(liveId) && hasSignal(targetId)) {
|
|
199
|
+
alreadyActive = Boolean(
|
|
200
|
+
(liveId.userId && targetId.userId && liveId.userId === targetId.userId)
|
|
201
|
+
|| (liveId.email && targetId.email && liveId.email === targetId.email),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (alreadyActive) {
|
|
206
|
+
writeVirtualDeviceMid(account);
|
|
207
|
+
return { switched: false, alreadyActive: true };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!force && zcodeRunning()) {
|
|
211
|
+
const err = new Error('ZCode 客户端正在运行,请先退出后再切换(或强制切换,但客户端可能把内存里的旧登录覆盖回文件)');
|
|
212
|
+
err.zcodeRunning = true;
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
atomicWriteJson(p.credentials, account.meta.credentials);
|
|
217
|
+
ensureCodingPlanIdentity(account, p);
|
|
218
|
+
|
|
219
|
+
// 深度对齐 config.json:保证目标账号对应渠道(zai / bigmodel)处于启用状态,避免出现套餐「未登录」
|
|
220
|
+
const creds = account.meta.credentials || {};
|
|
221
|
+
const isZai = Boolean(creds['oauth:zai:access_token'] || account.meta.loginProvider === 'zai' || Object.keys(creds).some((k) => k.includes('zai')));
|
|
222
|
+
const isBig = Boolean(creds['oauth:bigmodel:access_token'] || account.meta.loginProvider === 'bigmodel' || Object.keys(creds).some((k) => k.includes('bigmodel')));
|
|
223
|
+
|
|
224
|
+
const cfg = account.meta.config || readJson(p.config) || { provider: {} };
|
|
225
|
+
cfg.provider = cfg.provider || {};
|
|
226
|
+
if (isZai) {
|
|
227
|
+
if (cfg.provider['builtin:zai']) cfg.provider['builtin:zai'].enabled = true;
|
|
228
|
+
if (cfg.provider['builtin:zai-start-plan']) cfg.provider['builtin:zai-start-plan'].enabled = true;
|
|
229
|
+
if (cfg.provider['builtin:bigmodel']) cfg.provider['builtin:bigmodel'].enabled = false;
|
|
230
|
+
} else if (isBig) {
|
|
231
|
+
if (cfg.provider['builtin:bigmodel']) cfg.provider['builtin:bigmodel'].enabled = true;
|
|
232
|
+
if (cfg.provider['builtin:bigmodel-coding-plan']) cfg.provider['builtin:bigmodel-coding-plan'].enabled = true;
|
|
233
|
+
if (cfg.provider['builtin:zai']) cfg.provider['builtin:zai'].enabled = false;
|
|
234
|
+
if (cfg.provider['builtin:zai-start-plan']) cfg.provider['builtin:zai-start-plan'].enabled = false;
|
|
235
|
+
}
|
|
236
|
+
atomicWriteJson(p.config, cfg);
|
|
237
|
+
|
|
238
|
+
// 深度对齐 setting.json:即使账号未保存过 setting 快照,也自动修复选择器 key
|
|
239
|
+
const set = account.meta.setting || readJson(p.setting) || {};
|
|
240
|
+
set.modelProviderFamilyModes = set.modelProviderFamilyModes || {};
|
|
241
|
+
set.modelProviderFamilySelectedKeys = set.modelProviderFamilySelectedKeys || {};
|
|
242
|
+
// providerFamilyDomain 是客户端模型页的「家族显示开关」:
|
|
243
|
+
// 设为 zai 时整个 BigModel 家族被隐藏(反之亦然),切号必须与 active_provider 同步对齐,
|
|
244
|
+
// 否则客户端界面上目标渠道一栏直接消失 / 显示未登录(zcode-switch 同样写这个字段)。
|
|
245
|
+
if (isZai) {
|
|
246
|
+
set.modelProviderFamilyModes.zai = 'oauth';
|
|
247
|
+
set.modelProviderFamilySelectedKeys.zai = 'coding-plan:builtin:zai-start-plan';
|
|
248
|
+
set.providerFamilyDomain = 'zai';
|
|
249
|
+
} else if (isBig) {
|
|
250
|
+
set.modelProviderFamilyModes.bigmodel = 'oauth';
|
|
251
|
+
set.modelProviderFamilySelectedKeys.bigmodel = 'coding-plan:builtin:bigmodel-coding-plan';
|
|
252
|
+
set.providerFamilyDomain = 'bigmodel';
|
|
253
|
+
}
|
|
254
|
+
if (set.providerFamilyDomain) set.providerFamilyDomainUpdatedAt = Date.now();
|
|
255
|
+
atomicWriteJson(p.setting, set);
|
|
256
|
+
|
|
257
|
+
// 深度对齐 provider_config.json:默认模型选择指向目标账号的 Coding Plan,否则客户端模型页会判定「未登录」
|
|
258
|
+
if (account.meta.providerConfig) atomicWriteJson(p.providerConfig, account.meta.providerConfig);
|
|
259
|
+
alignDefaultModelSelection(account, isZai ? 'zai' : isBig ? 'bigmodel' : null);
|
|
260
|
+
|
|
261
|
+
writeVirtualDeviceMid(account);
|
|
262
|
+
return { switched: true, alreadyActive: false };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** ZCode 客户端判定套餐已登录的必要键:account-provider:<providerId>:identity(值为该账号身份 ID)。
|
|
266
|
+
* 缺失时客户端 nPn/oPn 读不到 accountIdentity,直接跳过该套餐 → 界面显示「未登录」。 */
|
|
267
|
+
export function ensureCodingPlanIdentity(account, paths = zcodePaths()) {
|
|
268
|
+
const creds = account?.meta?.credentials;
|
|
269
|
+
if (!creds) return;
|
|
270
|
+
const secret = zc.defaultSecret(paths.home);
|
|
271
|
+
let changed = false;
|
|
272
|
+
|
|
273
|
+
const putIdentity = (providerId, identity) => {
|
|
274
|
+
if (!identity) return;
|
|
275
|
+
const key = `account-provider:${providerId}:identity`;
|
|
276
|
+
if (creds[key]) return; // 已有则不覆盖
|
|
277
|
+
creds[key] = zc.encryptWithSecret(String(identity), secret);
|
|
278
|
+
changed = true;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// BigModel:身份 ID 取自 oauth:bigmodel:user_info.id
|
|
282
|
+
const bigInfo = zc.decryptJsonOpt(creds['oauth:bigmodel:user_info'], secret);
|
|
283
|
+
const bigUid = bigInfo?.id ?? bigInfo?.user_id ?? null;
|
|
284
|
+
if (bigUid) {
|
|
285
|
+
putIdentity('account:bigmodel-individual-coding-plan', bigUid);
|
|
286
|
+
putIdentity('account:bigmodel-team-coding-plan', bigUid);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Z.ai:身份 ID 取自 oauth:zai:user_info.user_id
|
|
290
|
+
const zaiInfo = zc.decryptJsonOpt(creds['oauth:zai:user_info'], secret);
|
|
291
|
+
const zaiUid = zaiInfo?.user_id ?? zaiInfo?.id ?? null;
|
|
292
|
+
if (zaiUid) {
|
|
293
|
+
putIdentity('account:zai-individual-coding-plan', zaiUid);
|
|
294
|
+
putIdentity('account:zai-team-coding-plan', zaiUid);
|
|
295
|
+
putIdentity('account:zai-start-plan', zaiUid);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (changed) atomicWriteJson(paths.credentials, creds);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 把 provider_config.json 的 defaultModelSelection 指向目标账号对应的 Coding Plan */
|
|
302
|
+
export function alignDefaultModelSelection(account, family) {
|
|
303
|
+
if (!family) return;
|
|
304
|
+
const p = zcodePaths();
|
|
305
|
+
const file = p.providerConfig;
|
|
306
|
+
if (!fs.existsSync(file)) return;
|
|
307
|
+
const cfg = readJson(file);
|
|
308
|
+
if (!cfg || typeof cfg !== 'object') return;
|
|
309
|
+
cfg.config = cfg.config || {};
|
|
310
|
+
|
|
311
|
+
const providerId = family === 'zai'
|
|
312
|
+
? 'account:zai-individual-coding-plan'
|
|
313
|
+
: 'account:bigmodel-individual-coding-plan';
|
|
314
|
+
const modelId = family === 'zai' ? 'glm-5.3-flash' : 'glm-5.3';
|
|
315
|
+
cfg.config.defaultModelSelection = { providerId, modelId };
|
|
316
|
+
atomicWriteJson(file, cfg);
|
|
317
|
+
return providerId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 每个账号的虚拟设备 ID(随机生成一次,落在 account.meta.deviceMid),切换时写回 telemetry-state.json */
|
|
321
|
+
export function ensureVirtualDeviceMid(account) {
|
|
322
|
+
if (account.meta?.deviceMid) return account.meta.deviceMid;
|
|
323
|
+
const mid = crypto.randomUUID();
|
|
324
|
+
account.meta = { ...(account.meta || {}), deviceMid: mid };
|
|
325
|
+
return mid;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function writeVirtualDeviceMid(account) {
|
|
329
|
+
const mid = ensureVirtualDeviceMid(account);
|
|
330
|
+
const p = zcodePaths();
|
|
331
|
+
const tele = readJson(p.telemetry) || {};
|
|
332
|
+
if (tele.deviceMid === mid) return;
|
|
333
|
+
atomicWriteJson(p.telemetry, { ...tele, deviceMid: mid });
|
|
334
|
+
}
|