@agent-proxy/zcode 0.0.0-stage → 0.11.1

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/dist/config.js ADDED
@@ -0,0 +1,944 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { decryptZcodeCredential } from "./credential-cipher.js";
4
+ import { readZcodeIndividualCredential } from "./individual-credentials.js";
5
+ import { readZcodeTeamCredentialInputs, resolveZcodeTeamApiKey, } from "./team-credentials.js";
6
+ export class ZcodeConfigError extends Error {
7
+ }
8
+ function record(value) {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function invalid(message) {
12
+ throw new ZcodeConfigError(`${message};请在 ZCode 中修复当前 provider 配置。`);
13
+ }
14
+ function readJson(file) {
15
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
16
+ try {
17
+ const value = JSON.parse(fs.readFileSync(fd, "utf8"));
18
+ if (!record(value))
19
+ invalid("ZCode 配置必须是 JSON 对象");
20
+ return value;
21
+ }
22
+ finally {
23
+ fs.closeSync(fd);
24
+ }
25
+ }
26
+ /** 三个文件各自选择生效位置;损坏、权限和符号链接不能触发回退。 */
27
+ function readPreferred(home, name) {
28
+ try {
29
+ try {
30
+ return readJson(path.join(home, name));
31
+ }
32
+ catch (error) {
33
+ if (error.code !== "ENOENT")
34
+ throw error;
35
+ }
36
+ return readJson(path.join(home, "v2", name));
37
+ }
38
+ catch (error) {
39
+ if (error.code === "ELOOP")
40
+ invalid("ZCode 配置文件不能是符号链接");
41
+ invalid(`无法读取 ZCode ${name}`);
42
+ }
43
+ }
44
+ /** 仅文件缺失时返回空对象;损坏、权限和符号链接仍由 readPreferred 明确报错。 */
45
+ function readOptionalPreferred(home, name) {
46
+ const present = [path.join(home, name), path.join(home, "v2", name)].some((file) => {
47
+ try {
48
+ fs.lstatSync(file);
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ return error.code !== "ENOENT";
53
+ }
54
+ });
55
+ return present ? readPreferred(home, name) : {};
56
+ }
57
+ function baseURL(value) {
58
+ if (typeof value !== "string" || !value.trim())
59
+ invalid("当前 provider 缺少 Anthropic baseURL");
60
+ let url;
61
+ try {
62
+ url = new URL(value);
63
+ }
64
+ catch {
65
+ invalid("当前 provider 的 Anthropic baseURL 无效");
66
+ }
67
+ const allowed = ["z.ai", "bigmodel.cn"].some((domain) => url.hostname === domain || url.hostname.endsWith(`.${domain}`));
68
+ if (url.protocol !== "https:" || url.username || url.password || !allowed || url.search || url.hash
69
+ || !/\/anthropic(?:\/v1)?\/*$/.test(url.pathname)) {
70
+ invalid("当前 provider 必须使用官方 HTTPS Anthropic 基址");
71
+ }
72
+ return url.href.replace(/\/+$/, "");
73
+ }
74
+ const OFFICIAL_API_BASE_URLS = {
75
+ zai: ["https://api.z.ai/api/anthropic", "https://api.z.ai/api/anthropic/v1"],
76
+ bigmodel: ["https://open.bigmodel.cn/api/anthropic", "https://open.bigmodel.cn/api/anthropic/v1"],
77
+ };
78
+ const RESERVED_API_PROVIDER_IDS = new Set(["individual-coding-plan", "team-coding-plan", "start-plan"]);
79
+ function isPlan(selection) {
80
+ return selection.kind !== "api-key";
81
+ }
82
+ /**
83
+ * Start Plan 的请求鉴权走套餐服务凭证:`/api/auth/z/login` 用 OAuth token 换出的
84
+ * biz JWT(credentials 的 zcodejwttoken)。实测 OAuth access token 会被 zcode-plan
85
+ * 中继与 billing 一并 401;镜像里的旧 JWT 仅作官方同款回退(readRoute 提供)。
86
+ */
87
+ function startPlanApiKey(credentials, _family, env = process.env) {
88
+ const value = credentials.zcodejwttoken;
89
+ if (typeof value !== "string" || !value.trim())
90
+ return "";
91
+ try {
92
+ return decryptZcodeCredential(value, env).trim();
93
+ }
94
+ catch {
95
+ invalid("Start Plan 凭据解密失败");
96
+ }
97
+ }
98
+ /**
99
+ * ZCode 3.12.3 起切换套餐只写 `providerFamilyConnectionSelections[family].kind`,
100
+ * legacy `modelProviderFamilySelectedKeys` 已冻结。在入口把已知 kind 归一化为与 legacy
101
+ * 解析同构的 providerID,下游镜像查找、套餐判定与协议转换零改动。
102
+ * 条目缺失或形状非法时返回 undefined,由 legacy 路径接管:3.12.3 的迁移是惰性持久化,
103
+ * 未重启 / api-key 模式被跳过 / team 连接未解析三个窗口下磁盘长期只有 legacy。
104
+ */
105
+ function connectionSelection(setting, family) {
106
+ const selections = setting.providerFamilyConnectionSelections;
107
+ if (!record(selections))
108
+ return undefined;
109
+ const entry = selections[family];
110
+ if (!record(entry) || typeof entry.kind !== "string" || !entry.kind)
111
+ return undefined;
112
+ return entry;
113
+ }
114
+ function teamContext(entry) {
115
+ const values = [entry.productId, entry.organizationId, entry.projectId].map((value) => {
116
+ if (typeof value !== "string")
117
+ return "";
118
+ return value.trim();
119
+ });
120
+ if (!values.every((value) => value && !/[\x00-\x1f\x7f]/.test(value))) {
121
+ invalid("团队套餐缺少有效的 productId、organizationId 或 projectId");
122
+ }
123
+ return { productId: values[0], organizationId: values[1], projectId: values[2] };
124
+ }
125
+ function legacyKind(family, providerID) {
126
+ if (providerID === `builtin:${family}-coding-plan`)
127
+ return "individual-coding-plan";
128
+ if (providerID === `builtin:${family}-start-plan`)
129
+ return "start-plan";
130
+ return "api-key";
131
+ }
132
+ function selectionIdentity(selection) {
133
+ return JSON.stringify([selection.family, selection.kind, selection.providerID, selection.team ?? null]);
134
+ }
135
+ /**
136
+ * ZCode 3.14 起 `providerFamilyConnectionSelections[family].kind` 只表达当前渠道
137
+ * 的连接形态,切换渠道后另一渠道的旧连接可能继续留在磁盘里。只有该渠道仍持有
138
+ * OAuth access token 时才把槽位暴露给会话;否则个人/团队请求会命中已失效连接。
139
+ * 各渠道严格读取自己的 key,不能回退到另一渠道。
140
+ */
141
+ function hasOAuthCredential(credentials, family) {
142
+ const token = credentials[`oauth:${family}:access_token`];
143
+ return typeof token === "string" && token.trim().length > 0;
144
+ }
145
+ /**
146
+ * 单个渠道的双源选择解析:3.12.3 的 `providerFamilyConnectionSelections[family].kind`
147
+ * 优先,条目缺失或形状非法时回退该渠道的 legacy `modelProviderFamilySelectedKeys`。
148
+ * `scope` 只用于报错文案:当前渠道沿用「当前渠道」,另一渠道写明渠道名。
149
+ */
150
+ function familySelection(setting, family, scope, officialAPIKeyProviderID) {
151
+ const officialAPISelection = () => {
152
+ const providerID = officialAPIKeyProviderID?.();
153
+ if (providerID)
154
+ return { family, kind: "api-key", providerID };
155
+ invalid(`ZCode ${scope}未找到可用的官方 API Key provider`);
156
+ };
157
+ const entry = connectionSelection(setting, family);
158
+ if (entry) {
159
+ const kind = entry.kind;
160
+ if (kind === "individual-coding-plan")
161
+ return { family, kind, providerID: `builtin:${family}-coding-plan` };
162
+ if (kind === "team-coding-plan") {
163
+ return { family, kind, providerID: `builtin:${family}-coding-plan`, team: teamContext(entry) };
164
+ }
165
+ if (kind === "start-plan")
166
+ return { family, kind, providerID: `builtin:${family}-start-plan` };
167
+ if (kind === "api-key") {
168
+ // 新版 ZCode 可能把 API Key 也表达为连接形态,但条目本身不携带 provider ID;
169
+ // 此时从 provider_config 中按官方模板或官方 baseURL 定位。
170
+ return officialAPISelection();
171
+ }
172
+ // 条目存在且 kind 已解析,说明用户做了真实的新选择;回退会静默跟随冻结的旧渠道。
173
+ invalid(`ZCode ${scope}的选择类型 "${kind}" 暂不被网关支持,请在 ZCode 中切换到 Coding Plan 或 Start Plan`);
174
+ }
175
+ const selected = record(setting.modelProviderFamilySelectedKeys) ? setting.modelProviderFamilySelectedKeys[family] : undefined;
176
+ if (typeof selected !== "string" || selected.indexOf(":") < 1)
177
+ invalid(`ZCode ${scope}缺少有效 provider 选择`);
178
+ const providerID = selected.slice(selected.indexOf(":") + 1);
179
+ if (!providerID)
180
+ invalid(`ZCode ${scope}的 provider ID 为空`);
181
+ const kind = legacyKind(family, providerID);
182
+ // 仅官方 API Key 的 builtin:zai / builtin:bigmodel 已被 ZCode 忽略;它们只反映
183
+ // 手工修改过的陈旧磁盘状态。Coding Plan / Start Plan 的 builtin 套餐镜像不受影响。
184
+ if (kind === "api-key" && providerID === `builtin:${family}`)
185
+ return officialAPISelection();
186
+ return { family, kind, providerID };
187
+ }
188
+ const PLAN_UNAVAILABLE = {
189
+ "individual-coding-plan": "没有可用的个人 Coding Plan 连接,请在 ZCode 中连接个人套餐",
190
+ "team-coding-plan": "没有可用的团队 Coding Plan 连接,请在 ZCode 中连接团队套餐",
191
+ "start-plan": "没有可用的 Start Plan 连接,请在 ZCode 中连接后重试",
192
+ "api-key": "ZCode 当前渠道未找到可用的官方 API Key provider",
193
+ };
194
+ function providerConfigRules(value) {
195
+ const config = record(value.config) ? value.config : undefined;
196
+ const rulesConfig = config && record(config.providerConfigRules) ? config.providerConfigRules : undefined;
197
+ const rules = rulesConfig && Array.isArray(rulesConfig.providerRules) ? rulesConfig.providerRules : undefined;
198
+ return rules?.filter(record) ?? [];
199
+ }
200
+ function providerModelIds(config) {
201
+ const models = modelIds(config?.modelOrder);
202
+ return models.length ? models : modelIds(config?.personalModelIds);
203
+ }
204
+ function officialAPIProviderRoute(rule, family) {
205
+ if (rule.enabled === false)
206
+ return undefined;
207
+ const providerID = typeof rule.providerId === "string" ? rule.providerId.trim() : "";
208
+ const rawProviderName = typeof rule.providerName === "string" ? rule.providerName.trim() : "";
209
+ // 显示名只做目录标签;形状不可展示时回退 provider ID,不让请求凭据失效。
210
+ const providerName = rawProviderName && rawProviderName.length <= 128 && !/[\x00-\x1f\x7f]/.test(rawProviderName)
211
+ ? rawProviderName
212
+ : undefined;
213
+ const config = record(rule.config) ? rule.config : undefined;
214
+ const access = config && record(config.access) ? config.access : undefined;
215
+ const apiKey = access?.apiKey;
216
+ if (!providerID || typeof apiKey !== "string" || !apiKey || /[^\x21-\x7e]/.test(apiKey))
217
+ return undefined;
218
+ const officialTemplateID = `${family}-api`;
219
+ if (rule.templateId === officialTemplateID) {
220
+ return { providerID, ...(providerName ? { providerName } : {}), apiKey, baseURL: OFFICIAL_API_BASE_URLS[family][0],
221
+ modelIds: providerModelIds(config), officialTemplate: true };
222
+ }
223
+ const api = config && record(config.api) ? config.api : undefined;
224
+ if (api?.type !== "anthropic-messages" || typeof api.baseUrl !== "string")
225
+ return undefined;
226
+ let url;
227
+ try {
228
+ url = baseURL(api.baseUrl);
229
+ }
230
+ catch {
231
+ return undefined;
232
+ }
233
+ if (!OFFICIAL_API_BASE_URLS[family].includes(url))
234
+ return undefined;
235
+ return { providerID, ...(providerName ? { providerName } : {}), apiKey, baseURL: url,
236
+ modelIds: providerModelIds(config), officialTemplate: false };
237
+ }
238
+ function officialAPIKeyProviderID(value, family) {
239
+ const routes = providerConfigRules(value).flatMap((rule) => {
240
+ const route = officialAPIProviderRoute(rule, family);
241
+ return route ? [route] : [];
242
+ });
243
+ return (routes.find((route) => route.officialTemplate) ?? routes[0])?.providerID;
244
+ }
245
+ /** 列出 provider_config 中全部可用官方 API Key,供目录为每个 Key 建独立路由。 */
246
+ export function readZcodeAPIKeyProviders(homeDirectory) {
247
+ const home = path.resolve(homeDirectory);
248
+ const providerConfig = readOptionalPreferred(home, "provider_config.json");
249
+ const orderConfig = record(providerConfig.config) ? providerConfig.config : undefined;
250
+ const order = orderConfig && Array.isArray(orderConfig.providerOrder)
251
+ ? orderConfig.providerOrder.filter((id) => typeof id === "string")
252
+ : [];
253
+ const rank = new Map(order.map((id, index) => [id, index]));
254
+ const providers = ["zai", "bigmodel"].flatMap((family) => providerConfigRules(providerConfig).flatMap((rule) => {
255
+ const route = officialAPIProviderRoute(rule, family);
256
+ return route ? [{ family, ...route }] : [];
257
+ }));
258
+ const seen = new Set();
259
+ const result = providers.filter((provider) => {
260
+ const identity = provider.providerID.toLowerCase();
261
+ if (RESERVED_API_PROVIDER_IDS.has(identity)) {
262
+ invalid(`ZCode 官方 API Key provider ID "${provider.providerID}" 与套餐前缀冲突`);
263
+ }
264
+ if (seen.has(identity)) {
265
+ invalid(`ZCode provider_config 中官方 API Key provider ID重复: ${provider.providerID}`);
266
+ }
267
+ seen.add(identity);
268
+ return true;
269
+ }).map(({ family, providerID, providerName }) => ({ family, providerID, ...(providerName ? { providerName } : {}) }));
270
+ return result.sort((a, b) => (rank.get(a.providerID) ?? order.length) - (rank.get(b.providerID) ?? order.length)
271
+ || a.providerID.localeCompare(b.providerID));
272
+ }
273
+ function readAPIKeyRoute(home, selection) {
274
+ const providerConfig = readPreferred(home, "provider_config.json");
275
+ const route = providerConfigRules(providerConfig)
276
+ .map((rule) => officialAPIProviderRoute(rule, selection.family))
277
+ .find((candidate) => candidate?.providerID === selection.providerID);
278
+ if (!route)
279
+ invalid("找不到 ZCode provider_config 中当前可用的官方 API Key provider");
280
+ return {
281
+ family: selection.family,
282
+ providerID: route.providerID,
283
+ plan: "api-key",
284
+ ...(route.providerName ? { providerName: route.providerName } : {}),
285
+ apiKey: route.apiKey,
286
+ baseURL: route.baseURL,
287
+ modelIds: route.modelIds,
288
+ };
289
+ }
290
+ function readZcodePlanSelections(home, requestedPlan) {
291
+ const setting = readPreferred(home, "setting.json");
292
+ const domain = setting.providerFamilyDomain;
293
+ if (domain !== "zai" && domain !== "bigmodel")
294
+ invalid("ZCode 未选择 zai 或 bigmodel 渠道");
295
+ const plans = {};
296
+ const failures = {};
297
+ let providerConfig;
298
+ const readProviderConfig = () => providerConfig ??= readOptionalPreferred(home, "provider_config.json");
299
+ const other = domain === "zai" ? "bigmodel" : "zai";
300
+ // 文件缺失只撤销 OAuth 套餐槽位;损坏或权限问题仍需明确报错,不能静默降级。
301
+ const credentials = readOptionalPreferred(home, "credentials.json");
302
+ for (const family of [domain, other]) {
303
+ try {
304
+ const selection = familySelection(setting, family, family === domain ? "当前渠道" : `${family} 渠道`, () => officialAPIKeyProviderID(readProviderConfig(), family));
305
+ if (selection.kind === "api-key") {
306
+ if (family === domain)
307
+ plans["api-key"] ??= selection;
308
+ }
309
+ else if (hasOAuthCredential(credentials, family)) {
310
+ plans[selection.kind] ??= selection;
311
+ }
312
+ }
313
+ catch (error) {
314
+ failures[family] = error instanceof ZcodeConfigError ? error : new ZcodeConfigError(`无法解析 ZCode ${family} 渠道选择`);
315
+ }
316
+ }
317
+ // Start Plan 不依赖连接形态:账号态 OAuth 即可鉴权,资格由 billing/balance 判定、
318
+ // 上游最终拒绝;槽位兜底指向当前渠道的 start-plan provider,让免费档始终可选。
319
+ plans["start-plan"] ??= { family: domain, kind: "start-plan", providerID: `builtin:${domain}-start-plan` };
320
+ // API Key 只读 provider_config;即使当前渠道正在使用套餐,也允许会话级路由
321
+ // 主动选择官方 API Key 槽位。
322
+ if (requestedPlan === "api-key" && !plans["api-key"]) {
323
+ const providerID = officialAPIKeyProviderID(readProviderConfig(), domain);
324
+ if (providerID)
325
+ plans["api-key"] = { family: domain, kind: "api-key", providerID };
326
+ }
327
+ return { domain, plans, failures };
328
+ }
329
+ function readAPIKeySelection(home, providerID) {
330
+ const provider = readZcodeAPIKeyProviders(home).find((candidate) => candidate.providerID === providerID);
331
+ if (!provider)
332
+ invalid(`找不到 ZCode provider_config 中官方 API Key provider ${providerID}`);
333
+ return { family: provider.family, kind: "api-key", providerID };
334
+ }
335
+ function readSelection(home, plan, apiProviderID) {
336
+ if (plan === "api-key" && apiProviderID)
337
+ return readAPIKeySelection(home, apiProviderID);
338
+ if (plan === undefined) {
339
+ const setting = readPreferred(home, "setting.json");
340
+ const family = setting.providerFamilyDomain;
341
+ if (family !== "zai" && family !== "bigmodel")
342
+ invalid("ZCode 未选择 zai 或 bigmodel 渠道");
343
+ return familySelection(setting, family, "当前渠道", () => officialAPIKeyProviderID(readPreferred(home, "provider_config.json"), family));
344
+ }
345
+ const { domain, plans, failures } = readZcodePlanSelections(home, plan);
346
+ const selection = plans[plan];
347
+ if (selection)
348
+ return selection;
349
+ // 槽位缺失时优先透出当前渠道的解析根因(如未知 kind),比笼统的缺连接提示更可
350
+ // 执行;另一渠道的失败与本套餐无关,不劫持提示。根因消息自带修复指引,不再叠
351
+ // 加 invalid 的后缀。
352
+ throw new ZcodeConfigError(failures[domain]?.message ?? PLAN_UNAVAILABLE[plan]);
353
+ }
354
+ /** models 的字典键才是上游 ID;显示名和其他元数据不参与路由投影。 */
355
+ function modelIds(value) {
356
+ // 新版 ZCode 会把 models 写成数组;旧版是模型 ID 到元数据的对象。
357
+ const values = Array.isArray(value)
358
+ ? value.filter((id) => typeof id === "string")
359
+ : record(value) ? Object.keys(value) : [];
360
+ const ids = values.map((id) => id.trim()).filter(Boolean).sort((a, b) => {
361
+ const lowerA = a.toLowerCase();
362
+ const lowerB = b.toLowerCase();
363
+ // 同一 ID 的不同拼写也稳定排序,避免字典插入顺序改变所选拼写。
364
+ return lowerA < lowerB ? -1 : lowerA > lowerB ? 1 : a < b ? -1 : a > b ? 1 : 0;
365
+ });
366
+ const seen = new Set();
367
+ return Object.freeze(ids.filter((id) => {
368
+ const normalized = id.toLowerCase();
369
+ if (seen.has(normalized))
370
+ return false;
371
+ seen.add(normalized);
372
+ return true;
373
+ }));
374
+ }
375
+ function readRoute(home, selection) {
376
+ const { family, providerID, kind: plan } = selection;
377
+ if (selection.kind === "api-key")
378
+ return readAPIKeyRoute(home, selection);
379
+ const config = readPreferred(home, "config.json");
380
+ const provider = record(config.provider) && Object.hasOwn(config.provider, providerID) ? config.provider[providerID] : undefined;
381
+ if (!record(provider) || !record(provider.options))
382
+ invalid("找不到 ZCode 当前 provider 的 options");
383
+ if (selection.kind === "team-coding-plan") {
384
+ // team 的 enabled 镜像仍跟随个人 OAuth 状态;团队权益由官方项目接口校验。
385
+ return { family, providerID, plan, baseURL: baseURL(provider.options.baseURL), modelIds: modelIds(provider.models) };
386
+ }
387
+ if (selection.kind === "start-plan") {
388
+ // start-plan 的镜像状态与旧 JWT 都可能冻结在个人 Coding Plan 结果;鉴权首选
389
+ // credentials 的 zcodejwttoken(biz JWT),镜像 JWT 仅作官方同款回退,权益由
390
+ // ZCode 自己的 billing/balance 判定,上游仍会最终拒绝。
391
+ const mirror = provider.options.apiKey;
392
+ return { family, providerID, plan,
393
+ ...(typeof mirror === "string" && mirror && !/[^\x21-\x7e]/.test(mirror) ? { apiKey: mirror } : {}),
394
+ baseURL: baseURL(provider.options.baseURL), modelIds: modelIds(provider.models) };
395
+ }
396
+ if (provider.enabled === false || (typeof provider.systemDisabledReason === "string" && provider.systemDisabledReason.length > 0)) {
397
+ invalid("ZCode 当前 provider 已停用");
398
+ }
399
+ const apiKey = provider.options.apiKey;
400
+ if (typeof apiKey !== "string" || !apiKey || /[^\x21-\x7e]/.test(apiKey))
401
+ invalid("当前 provider 缺少有效 options.apiKey");
402
+ return { family, providerID, plan, apiKey, baseURL: baseURL(provider.options.baseURL), modelIds: modelIds(provider.models) };
403
+ }
404
+ function expiresAt(apiKey) {
405
+ const parts = apiKey.split(".");
406
+ if (parts.length !== 3 || !parts.every((part) => /^[A-Za-z0-9_-]+$/.test(part)))
407
+ return;
408
+ try {
409
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
410
+ if (record(payload) && typeof payload.exp === "number" && Number.isFinite(payload.exp)
411
+ && Number.isFinite(payload.exp * 1000))
412
+ return payload.exp * 1000;
413
+ }
414
+ catch { /* 非 JWT Key 不推测有效期。 */ }
415
+ }
416
+ /**
417
+ * 本机 ZCode 配置是否就绪:setting.json 与 config.json/provider_config.json 至少
418
+ * 一组可用路由都存在才算可用。只做存在性探测,不读文件内容;home 与 v2 两种布局
419
+ * 各自按 readPreferred 的回退顺序判定。
420
+ */
421
+ export function zcodeConfigPresent(homeDirectory) {
422
+ const home = path.resolve(homeDirectory);
423
+ const locations = (name) => [path.join(home, name), path.join(home, "v2", name)];
424
+ return locations("setting.json").some((file) => fs.existsSync(file))
425
+ && ["config.json", "provider_config.json"]
426
+ .some((name) => locations(name).some((file) => fs.existsSync(file)));
427
+ }
428
+ /** 文件检查同步完成,凭证仅按 Key 的实际变化构建;事件不废弃有效快照。 */
429
+ export function createZcodeConfigCache(homeDirectory, dependencies = {}) {
430
+ const home = path.resolve(homeDirectory);
431
+ const plan = dependencies.plan;
432
+ const apiProviderID = dependencies.apiProviderID;
433
+ const now = dependencies.now ?? Date.now;
434
+ const watch = dependencies.watch ?? fs.watch;
435
+ const stat = dependencies.stat ?? fs.statSync;
436
+ const names = new Map();
437
+ let child = path.join(home, "v2");
438
+ while (path.dirname(child) !== child) {
439
+ const parent = path.dirname(child);
440
+ names.set(parent, new Set([path.basename(child)]));
441
+ child = parent;
442
+ }
443
+ for (const directory of [home, path.join(home, "v2")]) {
444
+ const entries = names.get(directory) ?? new Set();
445
+ for (const file of ["setting.json", "config.json", "provider_config.json", "credentials.json"])
446
+ entries.add(file);
447
+ names.set(directory, entries);
448
+ }
449
+ const directories = [...names.keys()].sort((a, b) => a.length - b.length);
450
+ const watchers = new Map();
451
+ let snapshot;
452
+ // 即使路由暂时失效也保留上次凭证语义,恢复同值配置不会重新计算 exp。
453
+ let credential;
454
+ let failure;
455
+ let closed = false;
456
+ const dirtyFiles = new Set(["setting.json", "config.json", "provider_config.json", "credentials.json"]);
457
+ let selection;
458
+ let route;
459
+ let routeFailure;
460
+ let oauthProjection;
461
+ let teamCredential;
462
+ let teamCredentialFingerprint;
463
+ let teamForceRefresh = false;
464
+ let credentialGeneration = 0;
465
+ let resolving;
466
+ let firstEventAt;
467
+ let debounce;
468
+ let expiryTimer;
469
+ let expiryCheckedKey;
470
+ let waiting;
471
+ /** 最近一次已通知调用方的快照;仅按对象身份比较,避免重复重建目录。 */
472
+ let notifiedSnapshot;
473
+ let refreshPromise;
474
+ let initialized = false;
475
+ function finish() { const previous = waiting; waiting = undefined; previous?.resolve(); }
476
+ function stop(message) {
477
+ if (closed)
478
+ return;
479
+ closed = true;
480
+ dirtyFiles.clear();
481
+ selection = undefined;
482
+ route = undefined;
483
+ oauthProjection = undefined;
484
+ snapshot = undefined;
485
+ credential = undefined;
486
+ teamCredential = undefined;
487
+ teamCredentialFingerprint = "";
488
+ teamForceRefresh = false;
489
+ credentialGeneration++;
490
+ resolving = undefined;
491
+ expiryCheckedKey = undefined;
492
+ failure = new ZcodeConfigError(message);
493
+ clearTimeout(debounce);
494
+ clearTimeout(expiryTimer);
495
+ for (const entry of watchers.values()) {
496
+ entry.closing = true;
497
+ entry.watcher.close();
498
+ }
499
+ watchers.clear();
500
+ finish();
501
+ }
502
+ function markDirty(file) {
503
+ if (closed || (file === "credentials.json" && selection && !isPlan(selection)))
504
+ return;
505
+ if (file === "credentials.json" && selection?.kind === "team-coding-plan") {
506
+ credentialGeneration++;
507
+ resolving = undefined;
508
+ teamCredential = undefined;
509
+ credential = undefined;
510
+ snapshot = undefined;
511
+ expiryCheckedKey = undefined;
512
+ clearTimeout(expiryTimer);
513
+ }
514
+ if (file === "setting.json" && selection) {
515
+ try {
516
+ const next = readSelection(home, plan, apiProviderID);
517
+ if (selectionIdentity(next) !== selectionIdentity(selection)) {
518
+ credentialGeneration++;
519
+ resolving = undefined;
520
+ teamCredential = undefined;
521
+ teamCredentialFingerprint = "";
522
+ teamForceRefresh = false;
523
+ credential = undefined;
524
+ snapshot = undefined;
525
+ expiryCheckedKey = undefined;
526
+ clearTimeout(expiryTimer);
527
+ }
528
+ }
529
+ catch {
530
+ snapshot = undefined;
531
+ credential = undefined;
532
+ }
533
+ }
534
+ if (file)
535
+ dirtyFiles.add(file);
536
+ else
537
+ for (const name of ["setting.json", "config.json", "provider_config.json", "credentials.json"])
538
+ dirtyFiles.add(name);
539
+ firstEventAt ??= now();
540
+ clearTimeout(debounce);
541
+ debounce = setTimeout(check, Math.max(0, Math.min(100, 500 - (now() - firstEventAt))));
542
+ debounce.unref?.();
543
+ }
544
+ function reconcile() {
545
+ const present = new Set();
546
+ for (const directory of directories) {
547
+ let info;
548
+ try {
549
+ info = stat(directory);
550
+ }
551
+ catch (error) {
552
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
553
+ continue;
554
+ throw error;
555
+ }
556
+ if (!info.isDirectory())
557
+ continue;
558
+ present.add(directory);
559
+ const identity = `${info.dev}:${info.ino}`;
560
+ const previous = watchers.get(directory);
561
+ if (previous?.identity === identity)
562
+ continue;
563
+ if (previous) {
564
+ previous.closing = true;
565
+ previous.watcher.close();
566
+ watchers.delete(directory);
567
+ }
568
+ const watcher = watch(directory, { persistent: false }, (_event, filename) => {
569
+ const name = filename?.toString();
570
+ if (name === undefined || names.get(directory).has(name)) {
571
+ markDirty(name && ["setting.json", "config.json", "provider_config.json", "credentials.json"].includes(name) ? name : undefined);
572
+ }
573
+ });
574
+ const entry = { watcher, identity, closing: false };
575
+ const failed = () => { if (!entry.closing)
576
+ stop("无法持续监听 ZCode 配置,请检查权限并重启网关"); };
577
+ watcher.on("error", failed);
578
+ watcher.on("close", failed);
579
+ watchers.set(directory, entry);
580
+ }
581
+ for (const [directory, entry] of watchers) {
582
+ if (!present.has(directory)) {
583
+ entry.closing = true;
584
+ entry.watcher.close();
585
+ watchers.delete(directory);
586
+ }
587
+ }
588
+ }
589
+ function armExpiry() {
590
+ clearTimeout(expiryTimer);
591
+ expiryTimer = undefined;
592
+ if (!snapshot || snapshot.expiresAt === undefined || failure)
593
+ return;
594
+ const remaining = snapshot.expiresAt - now();
595
+ if (remaining <= 0)
596
+ return;
597
+ // Node/Bun 对超过有符号 32 位毫秒数的定时器会截断,分段等待。
598
+ expiryTimer = setTimeout(() => {
599
+ expiryTimer = undefined;
600
+ if (closed || !snapshot)
601
+ return;
602
+ if (snapshot.expiresAt <= now())
603
+ expire();
604
+ else
605
+ armExpiry();
606
+ }, Math.min(remaining, 2_147_483_647));
607
+ expiryTimer.unref?.();
608
+ }
609
+ function check() {
610
+ if (closed)
611
+ return;
612
+ initialized = true;
613
+ const beforeSnapshot = snapshot;
614
+ clearTimeout(debounce);
615
+ debounce = undefined;
616
+ firstEventAt = undefined;
617
+ const changed = new Set(dirtyFiles);
618
+ dirtyFiles.clear();
619
+ try {
620
+ reconcile();
621
+ }
622
+ catch {
623
+ stop("无法持续监听 ZCode 配置,请检查权限并重启网关");
624
+ return;
625
+ }
626
+ try {
627
+ let selectionChanged = false;
628
+ if (!selection || changed.has("setting.json") || (isPlan(selection) && changed.has("credentials.json"))
629
+ // API Key 槽位可能由官方 baseURL 扫描得到,provider 镜像变化也要重新选路。
630
+ || (selection.kind === "api-key" && changed.has("provider_config.json"))) {
631
+ let next;
632
+ try {
633
+ next = readSelection(home, plan, apiProviderID);
634
+ }
635
+ catch (error) {
636
+ const hadSelection = selection !== undefined;
637
+ selection = undefined;
638
+ // 选择不可解析时旧套餐目录必须立即失效,不能继续留在客户端缓存里。
639
+ if (hadSelection)
640
+ dependencies.onSelectionChange?.();
641
+ throw error;
642
+ }
643
+ const hadSelection = selection !== undefined;
644
+ selectionChanged = !selection || selectionIdentity(next) !== selectionIdentity(selection);
645
+ if (hadSelection && selectionChanged)
646
+ dependencies.onSelectionChange?.();
647
+ if (selectionChanged) {
648
+ credentialGeneration++;
649
+ resolving = undefined;
650
+ teamCredential = undefined;
651
+ teamCredentialFingerprint = "";
652
+ teamForceRefresh = false;
653
+ credential = undefined;
654
+ snapshot = undefined;
655
+ expiryCheckedKey = undefined;
656
+ clearTimeout(expiryTimer);
657
+ }
658
+ selection = next;
659
+ }
660
+ let oauthChanged = false;
661
+ let personalApiKey;
662
+ let teamInputs;
663
+ if (isPlan(selection) && (selectionChanged || changed.has("credentials.json"))) {
664
+ if (selectionChanged) {
665
+ oauthProjection = selection.kind === "team-coding-plan" ? undefined : "[null,null,null]";
666
+ teamCredentialFingerprint = "";
667
+ }
668
+ try {
669
+ const credentials = readOptionalPreferred(home, "credentials.json");
670
+ if (selection.kind === "team-coding-plan") {
671
+ teamInputs = readZcodeTeamCredentialInputs(credentials, selection.family, dependencies.env);
672
+ const nextFingerprint = teamInputs.fingerprint;
673
+ if (teamCredentialFingerprint !== nextFingerprint) {
674
+ teamCredentialFingerprint = nextFingerprint;
675
+ oauthChanged = true;
676
+ }
677
+ }
678
+ else {
679
+ // 只保存当前计划渠道的授权字段与个人账号 Key 投影;全局 active_provider 不能改变模型选择。
680
+ const individual = selection.kind === "individual-coding-plan"
681
+ ? readZcodeIndividualCredential(credentials, selection.family, dependencies.env)
682
+ : undefined;
683
+ const key = individual?.state === "available"
684
+ ? individual.apiKey
685
+ : individual?.state === "unavailable"
686
+ ? ""
687
+ : selection.kind === "start-plan"
688
+ ? startPlanApiKey(credentials, selection.family, dependencies.env)
689
+ : "";
690
+ // 账号身份存在却拿不到该账号的 Key 时禁止镜像回退:切换账号后
691
+ // config.json 里的旧 Key 可能属于上一账号。
692
+ if (individual?.state === "unavailable") {
693
+ throw new ZcodeConfigError("ZCode 个人套餐凭据不可用;请在 ZCode 中重新连接当前账号");
694
+ }
695
+ personalApiKey = key || undefined;
696
+ const projection = JSON.stringify([
697
+ credentials[`oauth:${selection.family}:access_token`],
698
+ credentials[`oauth:${selection.family}:refresh_token`],
699
+ credentials.zcodejwttoken,
700
+ individual?.state === "available" ? individual.identity : individual?.state ?? null,
701
+ key,
702
+ ]);
703
+ oauthChanged = projection !== oauthProjection;
704
+ oauthProjection = projection;
705
+ }
706
+ }
707
+ catch (error) {
708
+ if (selection.kind === "team-coding-plan") {
709
+ snapshot = undefined;
710
+ credential = undefined;
711
+ throw error instanceof ZcodeConfigError ? error : new ZcodeConfigError("无法读取 ZCode 团队凭据");
712
+ }
713
+ if (selection.kind === "start-plan") {
714
+ snapshot = undefined;
715
+ credential = undefined;
716
+ throw error instanceof ZcodeConfigError ? error : new ZcodeConfigError("无法读取 ZCode Start Plan 凭据");
717
+ }
718
+ if (error instanceof ZcodeConfigError) {
719
+ snapshot = undefined;
720
+ credential = undefined;
721
+ throw error;
722
+ }
723
+ /* 无账号身份(旧安装)仍保留 config 镜像里的独立业务 Key。 */
724
+ }
725
+ }
726
+ else if (selectionChanged)
727
+ oauthProjection = undefined;
728
+ if (selectionChanged || changed.has("config.json") || changed.has("provider_config.json") || oauthChanged
729
+ || (!route && !routeFailure)) {
730
+ dependencies.onConfigRead?.();
731
+ try {
732
+ route = readRoute(home, selection);
733
+ routeFailure = undefined;
734
+ }
735
+ catch (error) {
736
+ route = undefined;
737
+ routeFailure = error instanceof ZcodeConfigError ? error : new ZcodeConfigError("无法读取 ZCode provider 配置");
738
+ }
739
+ }
740
+ if (routeFailure)
741
+ throw routeFailure;
742
+ if (!route)
743
+ invalid("ZCode 当前路由不可用");
744
+ const next = route;
745
+ if (selection.kind === "team-coding-plan") {
746
+ const identity = selectionIdentity(selection);
747
+ const fingerprint = teamCredentialFingerprint;
748
+ const expired = credential?.expiresAt !== undefined && credential.expiresAt <= now();
749
+ const mustResolve = teamForceRefresh || expired || !teamCredential
750
+ || teamCredential.identity !== identity || teamCredential.fingerprint !== fingerprint;
751
+ if (resolving) {
752
+ snapshot = undefined;
753
+ return;
754
+ }
755
+ if (!teamInputs || !fingerprint || mustResolve) {
756
+ if (!teamInputs || !fingerprint) {
757
+ const credentials = readOptionalPreferred(home, "credentials.json");
758
+ teamInputs = readZcodeTeamCredentialInputs(credentials, selection.family, dependencies.env);
759
+ teamCredentialFingerprint = teamInputs.fingerprint;
760
+ }
761
+ const generation = ++credentialGeneration;
762
+ const targetInputs = teamInputs;
763
+ snapshot = undefined;
764
+ credential = undefined;
765
+ expiryCheckedKey = undefined;
766
+ teamForceRefresh = false;
767
+ clearTimeout(expiryTimer);
768
+ const promise = (async () => {
769
+ try {
770
+ const apiKey = await resolveZcodeTeamApiKey(selection, targetInputs, dependencies);
771
+ if (credentialGeneration !== generation || selection.kind !== "team-coding-plan")
772
+ return;
773
+ dependencies.onCredentialBuild?.();
774
+ credential = { apiKey, expiresAt: expiresAt(apiKey) };
775
+ teamCredential = { identity, fingerprint: teamCredentialFingerprint, apiKey };
776
+ if (credential.expiresAt !== undefined && credential.expiresAt <= now())
777
+ invalid("ZCode 当前业务 Key 已过期");
778
+ const currentRoute = route;
779
+ if (!currentRoute)
780
+ invalid("ZCode 当前路由不可用");
781
+ snapshot = Object.freeze({ ...currentRoute, apiKey,
782
+ ...(credential.expiresAt === undefined ? {} : { expiresAt: credential.expiresAt }) });
783
+ failure = undefined;
784
+ armExpiry();
785
+ }
786
+ catch {
787
+ if (credentialGeneration !== generation)
788
+ return;
789
+ snapshot = undefined;
790
+ credential = undefined;
791
+ teamCredential = undefined;
792
+ failure = new ZcodeConfigError("无法读取 ZCode 团队项目凭据;请在 ZCode 中重新连接团队套餐");
793
+ clearTimeout(expiryTimer);
794
+ }
795
+ finally {
796
+ if (credentialGeneration === generation)
797
+ resolving = undefined;
798
+ finish();
799
+ }
800
+ })();
801
+ resolving = { generation, promise };
802
+ return;
803
+ }
804
+ const cachedTeamCredential = teamCredential;
805
+ if (!cachedTeamCredential)
806
+ invalid("ZCode 团队项目凭据不可用");
807
+ const apiKey = cachedTeamCredential.apiKey;
808
+ const expires = expiresAt(apiKey);
809
+ credential = { apiKey, expiresAt: expires };
810
+ if (expires !== undefined && expires <= now()) {
811
+ expiryCheckedKey = apiKey;
812
+ invalid("ZCode 当前业务 Key 已过期");
813
+ }
814
+ snapshot = Object.freeze({ ...next, apiKey, ...(expires === undefined ? {} : { expiresAt: expires }) });
815
+ failure = undefined;
816
+ armExpiry();
817
+ return;
818
+ }
819
+ // start-plan:上次套餐凭据优先于镜像 JWT(镜像只是无 zcodejwttoken 时的引导回退),
820
+ // 否则不重读凭据的检查轮次会把 key 静默换成可能冻结的镜像值。
821
+ const routeApiKey = personalApiKey
822
+ ?? (selection.kind === "start-plan" ? credential?.apiKey ?? next.apiKey : next.apiKey);
823
+ if (!routeApiKey)
824
+ invalid(selection.kind === "start-plan" ? "Start Plan 缺少可用的 zcode JWT 凭据" : "当前 provider 缺少有效 options.apiKey");
825
+ const hadFailure = failure !== undefined;
826
+ const keyChanged = !credential || credential.apiKey !== routeApiKey;
827
+ if (keyChanged) {
828
+ dependencies.onCredentialBuild?.();
829
+ credential = { apiKey: routeApiKey, expiresAt: expiresAt(routeApiKey) };
830
+ expiryCheckedKey = undefined;
831
+ }
832
+ const currentCredential = credential;
833
+ if (!currentCredential)
834
+ invalid("ZCode 当前业务 Key 不可用");
835
+ if (currentCredential.expiresAt !== undefined && currentCredential.expiresAt <= now()) {
836
+ expiryCheckedKey = routeApiKey;
837
+ invalid("ZCode 当前业务 Key 已过期");
838
+ }
839
+ if (!snapshot || snapshot.family !== next.family || snapshot.providerID !== next.providerID
840
+ || snapshot.plan !== next.plan
841
+ || snapshot.providerName !== next.providerName
842
+ || snapshot.apiKey !== routeApiKey || snapshot.baseURL !== next.baseURL
843
+ || snapshot.modelIds.length !== next.modelIds.length
844
+ || snapshot.modelIds.some((id, index) => id !== next.modelIds[index])) {
845
+ snapshot = Object.freeze({ family: next.family, providerID: next.providerID, plan: next.plan, apiKey: routeApiKey,
846
+ baseURL: next.baseURL, modelIds: next.modelIds,
847
+ ...(next.providerName ? { providerName: next.providerName } : {}),
848
+ ...(currentCredential.expiresAt === undefined ? {} : { expiresAt: currentCredential.expiresAt }) });
849
+ }
850
+ failure = undefined;
851
+ if (keyChanged || hadFailure)
852
+ armExpiry();
853
+ }
854
+ catch (error) {
855
+ failure = error instanceof ZcodeConfigError ? error : new ZcodeConfigError("无法检查 ZCode 当前配置,请修复配置");
856
+ clearTimeout(expiryTimer);
857
+ }
858
+ finally {
859
+ // 快照身份变化才通知:watch 驱动的账号/套餐/模型集合变化由此重建对外目录。
860
+ if (snapshot !== beforeSnapshot && snapshot !== notifiedSnapshot) {
861
+ notifiedSnapshot = snapshot;
862
+ if (snapshot)
863
+ dependencies.onSnapshotChange?.(snapshot);
864
+ }
865
+ finish();
866
+ }
867
+ }
868
+ function expire() {
869
+ if (!snapshot || snapshot.expiresAt === undefined || snapshot.expiresAt > now())
870
+ return;
871
+ // 第一个到期请求执行共享重读;同值过期之后只等待文件事件。
872
+ if (expiryCheckedKey !== snapshot.apiKey) {
873
+ expiryCheckedKey = snapshot.apiKey;
874
+ dirtyFiles.add(selection?.kind === "api-key" ? "provider_config.json" : "config.json");
875
+ if (selection?.kind === "team-coding-plan") {
876
+ dirtyFiles.add("credentials.json");
877
+ teamForceRefresh = true;
878
+ }
879
+ check();
880
+ }
881
+ }
882
+ if (dependencies.refreshOnStart !== false)
883
+ check();
884
+ const get = async () => {
885
+ if (!closed && !initialized)
886
+ check();
887
+ if (!closed)
888
+ expire();
889
+ if ((!snapshot || failure) && dirtyFiles.size > 0) {
890
+ if (!waiting) {
891
+ let resolve;
892
+ const promise = new Promise((done) => { resolve = done; });
893
+ waiting = { promise, resolve };
894
+ }
895
+ await waiting.promise;
896
+ }
897
+ if (failure)
898
+ throw failure;
899
+ if (resolving)
900
+ await resolving.promise;
901
+ if (failure)
902
+ throw failure;
903
+ if (!snapshot)
904
+ throw new ZcodeConfigError("ZCode 配置缓存不可用");
905
+ if (snapshot.expiresAt !== undefined && snapshot.expiresAt <= now()) {
906
+ throw new ZcodeConfigError("ZCode 当前业务 Key 已过期,请执行模型同步命令刷新凭据");
907
+ }
908
+ return snapshot;
909
+ };
910
+ const reread = () => {
911
+ if (refreshPromise)
912
+ return refreshPromise;
913
+ // 启动凭据请求与显式更新共用进行中的请求,防止重复授权。
914
+ const promise = (async () => {
915
+ if (resolving)
916
+ await resolving.promise;
917
+ if (closed)
918
+ return get();
919
+ for (const name of ["setting.json", "config.json", "provider_config.json", "credentials.json"])
920
+ dirtyFiles.add(name);
921
+ check();
922
+ return get();
923
+ })();
924
+ refreshPromise = promise;
925
+ void promise.finally(() => { if (refreshPromise === promise)
926
+ refreshPromise = undefined; }).catch(() => { });
927
+ return promise;
928
+ };
929
+ return {
930
+ get: () => get(),
931
+ refresh: reread,
932
+ async reload() {
933
+ if (closed)
934
+ throw new ZcodeConfigError("ZCode 配置监听已关闭,请重启网关");
935
+ // 目录采集器无需团队业务 Key。读取套餐模型时不初始化或改写认证缓存,
936
+ // 后续真实请求仍由 get() 执行正常授权与凭据续期。
937
+ const next = readRoute(home, readSelection(home, plan, apiProviderID));
938
+ const cachedKey = snapshot?.providerID === next.providerID && snapshot.plan === next.plan
939
+ && snapshot.family === next.family ? snapshot.apiKey : undefined;
940
+ return Object.freeze({ ...next, apiKey: next.apiKey ?? cachedKey ?? "" });
941
+ },
942
+ close() { stop("ZCode 配置监听已关闭,请重启网关"); },
943
+ };
944
+ }