ai-zero-token 2.0.4 → 2.0.5

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +17 -2
  3. package/README.zh-CN.md +18 -3
  4. package/admin-ui/dist/assets/{accounts-CTjk9c4F.js → accounts-ABMyXo4H.js} +1 -1
  5. package/admin-ui/dist/assets/{docs-oNIugCIL.js → docs-Dh0aFha_.js} +1 -1
  6. package/admin-ui/dist/assets/{image-bed-CQtIhjg_.js → image-bed-C1M7-0q1.js} +1 -1
  7. package/admin-ui/dist/assets/{index-rgcJgVAu.js → index--rNjdmzf.js} +2 -2
  8. package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
  9. package/admin-ui/dist/assets/{launch-B-2Zdz9m.js → launch-pB7YlWFI.js} +1 -1
  10. package/admin-ui/dist/assets/{logs-JFuSf56b.js → logs-B7McijSi.js} +1 -1
  11. package/admin-ui/dist/assets/{network-detect-SfvK6uhx.js → network-detect-Bx3XmXPk.js} +1 -1
  12. package/admin-ui/dist/assets/{overview-X_WodIqE.js → overview-CV0H2Nsq.js} +1 -1
  13. package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
  14. package/admin-ui/dist/assets/{tester-ocpF053C.js → tester-BG-up8qP.js} +1 -1
  15. package/admin-ui/dist/index.html +2 -2
  16. package/dist/core/providers/http-client.js +228 -3
  17. package/dist/core/providers/openai-codex/chat.js +83 -23
  18. package/dist/core/services/auth-service.js +14 -5
  19. package/dist/core/services/config-service.js +15 -5
  20. package/dist/core/store/codex-auth-store.js +295 -4
  21. package/dist/core/store/settings-store.js +54 -24
  22. package/dist/server/app.js +410 -49
  23. package/docs/API_USAGE.md +18 -1
  24. package/docs/DESKTOP_RELEASE.md +12 -1
  25. package/package.json +1 -1
  26. package/admin-ui/dist/assets/settings-0eXUAvcm.js +0 -1
@@ -5,12 +5,197 @@ import path from "node:path";
5
5
  function getCodexHomeDir() {
6
6
  return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
7
7
  }
8
+ const DEFAULT_CODEX_PROVIDER_ID = "ai-zero-token";
8
9
  function getCodexAuthPath() {
9
10
  return path.join(getCodexHomeDir(), "auth.json");
10
11
  }
12
+ function getCodexConfigPath() {
13
+ return path.join(getCodexHomeDir(), "config.toml");
14
+ }
11
15
  function createBackupSuffix() {
12
16
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13
17
  }
18
+ function escapeRegExp(value) {
19
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+ }
21
+ function formatTomlString(value) {
22
+ return JSON.stringify(value);
23
+ }
24
+ function validateProviderId(providerId) {
25
+ if (!/^[A-Za-z0-9_-]+$/.test(providerId)) {
26
+ throw new Error("Codex providerId \u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF\u3002");
27
+ }
28
+ }
29
+ function normalizeCodexProviderBaseUrl(value) {
30
+ let normalized = value.trim();
31
+ if (!normalized) {
32
+ throw new Error("Codex provider base_url \u4E0D\u80FD\u4E3A\u7A7A\u3002");
33
+ }
34
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(normalized)) {
35
+ normalized = `http://${normalized}`;
36
+ }
37
+ let url;
38
+ try {
39
+ url = new URL(normalized);
40
+ } catch {
41
+ throw new Error("Codex provider base_url \u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u586B\u5199\u5B8C\u6574\u7684 http(s) URL\u3002");
42
+ }
43
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
44
+ throw new Error("Codex provider base_url \u5FC5\u987B\u662F http(s) URL\u3002");
45
+ }
46
+ url.hash = "";
47
+ url.search = "";
48
+ const path2 = url.pathname.replace(/\/+$/g, "");
49
+ if (!path2 || path2 === "/") {
50
+ url.pathname = "/codex/v1";
51
+ } else if (path2 === "/v1") {
52
+ url.pathname = "/codex/v1";
53
+ } else if (path2.endsWith("/codex")) {
54
+ url.pathname = `${path2}/v1`;
55
+ } else {
56
+ url.pathname = path2;
57
+ }
58
+ return url.toString().replace(/\/+$/g, "");
59
+ }
60
+ function parseTomlStringValue(value) {
61
+ const trimmed = value.trim().replace(/\s+#.*$/g, "");
62
+ if (!trimmed) {
63
+ return void 0;
64
+ }
65
+ if (trimmed.startsWith('"')) {
66
+ try {
67
+ const parsed = JSON.parse(trimmed);
68
+ return typeof parsed === "string" ? parsed : void 0;
69
+ } catch {
70
+ return void 0;
71
+ }
72
+ }
73
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
74
+ return trimmed.slice(1, -1);
75
+ }
76
+ return trimmed;
77
+ }
78
+ function findFirstTableLine(lines) {
79
+ const index = lines.findIndex((line) => /^\s*\[/.test(line));
80
+ return index === -1 ? lines.length : index;
81
+ }
82
+ function parseRootModelProvider(raw) {
83
+ const lines = raw.split(/\r?\n/);
84
+ const firstTableLine = findFirstTableLine(lines);
85
+ for (let index = 0; index < firstTableLine; index += 1) {
86
+ const match = /^\s*model_provider\s*=\s*(.+)$/.exec(lines[index] ?? "");
87
+ if (match) {
88
+ return parseTomlStringValue(match[1] ?? "");
89
+ }
90
+ }
91
+ return void 0;
92
+ }
93
+ function parseGatewayProviderTable(raw, providerId) {
94
+ const lines = raw.split(/\r?\n/);
95
+ const tablePattern = new RegExp(`^\\s*\\[\\s*model_providers\\.${escapeRegExp(providerId)}\\s*\\]\\s*$`);
96
+ const start = lines.findIndex((line) => tablePattern.test(line));
97
+ if (start === -1) {
98
+ return { exists: false };
99
+ }
100
+ let baseUrl;
101
+ for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index] ?? ""); index += 1) {
102
+ const match = /^\s*base_url\s*=\s*(.+)$/.exec(lines[index] ?? "");
103
+ if (match) {
104
+ baseUrl = parseTomlStringValue(match[1] ?? "");
105
+ }
106
+ }
107
+ return { exists: true, baseUrl };
108
+ }
109
+ function upsertRootModelProvider(raw, providerId) {
110
+ if (!raw.trim()) {
111
+ return `model_provider = ${formatTomlString(providerId)}
112
+ `;
113
+ }
114
+ const lines = raw.split(/\r?\n/);
115
+ const firstTableLine = findFirstTableLine(lines);
116
+ const nextLine = `model_provider = ${formatTomlString(providerId)}`;
117
+ for (let index = 0; index < firstTableLine; index += 1) {
118
+ if (/^\s*model_provider\s*=/.test(lines[index])) {
119
+ lines[index] = nextLine;
120
+ return lines.join("\n");
121
+ }
122
+ }
123
+ lines.splice(firstTableLine, 0, nextLine, "");
124
+ return lines.join("\n");
125
+ }
126
+ function buildGatewayProviderBlock(providerId, baseUrl) {
127
+ return [
128
+ "# AI Zero Token managed Codex provider",
129
+ `[model_providers.${providerId}]`,
130
+ 'name = "AI Zero Token"',
131
+ `base_url = ${formatTomlString(baseUrl)}`,
132
+ 'wire_api = "responses"',
133
+ "supports_websockets = false"
134
+ ];
135
+ }
136
+ function upsertGatewayProviderTable(raw, providerId, baseUrl) {
137
+ const lines = raw.split(/\r?\n/);
138
+ const tablePattern = new RegExp(`^\\s*\\[\\s*model_providers\\.${escapeRegExp(providerId)}\\s*\\]\\s*$`);
139
+ const start = lines.findIndex((line) => tablePattern.test(line));
140
+ const block = buildGatewayProviderBlock(providerId, baseUrl);
141
+ if (start === -1) {
142
+ const trimmed = raw.replace(/\s+$/g, "");
143
+ return `${trimmed}${trimmed ? "\n\n" : ""}${block.join("\n")}
144
+ `;
145
+ }
146
+ let end = start + 1;
147
+ while (end < lines.length && !/^\s*\[/.test(lines[end])) {
148
+ end += 1;
149
+ }
150
+ const replaceStart = start > 0 && /AI Zero Token managed Codex provider/.test(lines[start - 1]) ? start - 1 : start;
151
+ lines.splice(replaceStart, end - replaceStart, ...block, "");
152
+ return lines.join("\n").replace(/\s+$/g, "\n");
153
+ }
154
+ function applyGatewayProviderConfig(raw, providerId, baseUrl) {
155
+ return upsertGatewayProviderTable(upsertRootModelProvider(raw, providerId), providerId, baseUrl);
156
+ }
157
+ function removeRootModelProvider(raw, providerId) {
158
+ const lines = raw.split(/\r?\n/);
159
+ const firstTableLine = findFirstTableLine(lines);
160
+ for (let index = 0; index < firstTableLine; index += 1) {
161
+ const match = /^\s*model_provider\s*=\s*(.+)$/.exec(lines[index] ?? "");
162
+ if (!match || parseTomlStringValue(match[1] ?? "") !== providerId) {
163
+ continue;
164
+ }
165
+ lines.splice(index, 1);
166
+ if (lines[index] === "" && (index === 0 || lines[index - 1] === "")) {
167
+ lines.splice(index, 1);
168
+ }
169
+ return { raw: lines.join("\n").replace(/^\s+\n/g, ""), removed: true };
170
+ }
171
+ return { raw, removed: false };
172
+ }
173
+ function removeGatewayProviderTable(raw, providerId) {
174
+ const lines = raw.split(/\r?\n/);
175
+ const tablePattern = new RegExp(`^\\s*\\[\\s*model_providers\\.${escapeRegExp(providerId)}\\s*\\]\\s*$`);
176
+ const start = lines.findIndex((line) => tablePattern.test(line));
177
+ if (start === -1) {
178
+ return { raw, removed: false };
179
+ }
180
+ let end = start + 1;
181
+ while (end < lines.length && !/^\s*\[/.test(lines[end])) {
182
+ end += 1;
183
+ }
184
+ const replaceStart = start > 0 && /AI Zero Token managed Codex provider/.test(lines[start - 1]) ? start - 1 : start;
185
+ lines.splice(replaceStart, end - replaceStart);
186
+ return {
187
+ raw: lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/g, "\n"),
188
+ removed: true
189
+ };
190
+ }
191
+ function removeGatewayProviderConfig(raw, providerId) {
192
+ const rootRemoved = removeRootModelProvider(raw, providerId);
193
+ const tableRemoved = removeGatewayProviderTable(rootRemoved.raw, providerId);
194
+ return {
195
+ raw: tableRemoved.raw,
196
+ removed: rootRemoved.removed || tableRemoved.removed
197
+ };
198
+ }
14
199
  function isRecord(value) {
15
200
  return typeof value === "object" && value !== null && !Array.isArray(value);
16
201
  }
@@ -23,14 +208,44 @@ async function readCodexAuth() {
23
208
  return null;
24
209
  }
25
210
  }
211
+ async function getCodexGatewayProviderStatus(params) {
212
+ const providerId = params?.providerId?.trim() || DEFAULT_CODEX_PROVIDER_ID;
213
+ validateProviderId(providerId);
214
+ const configPath = getCodexConfigPath();
215
+ let raw = "";
216
+ try {
217
+ raw = await fs.readFile(configPath, "utf8");
218
+ } catch {
219
+ return {
220
+ path: configPath,
221
+ providerId,
222
+ exists: false,
223
+ active: false
224
+ };
225
+ }
226
+ const modelProvider = parseRootModelProvider(raw);
227
+ const table = parseGatewayProviderTable(raw, providerId);
228
+ return {
229
+ path: configPath,
230
+ providerId,
231
+ exists: table.exists,
232
+ active: modelProvider === providerId && table.exists,
233
+ baseUrl: table.baseUrl,
234
+ modelProvider
235
+ };
236
+ }
26
237
  async function getCodexAuthStatus() {
27
238
  const authPath = getCodexAuthPath();
28
- const auth = await readCodexAuth();
239
+ const [auth, gatewayProvider] = await Promise.all([
240
+ readCodexAuth(),
241
+ getCodexGatewayProviderStatus()
242
+ ]);
29
243
  if (!auth) {
30
244
  return {
31
245
  path: authPath,
32
246
  exists: false,
33
- hasIdToken: false
247
+ hasIdToken: false,
248
+ gatewayProvider
34
249
  };
35
250
  }
36
251
  const tokens = isRecord(auth.tokens) ? auth.tokens : {};
@@ -39,7 +254,8 @@ async function getCodexAuthStatus() {
39
254
  exists: true,
40
255
  accountId: typeof tokens.account_id === "string" ? tokens.account_id : void 0,
41
256
  hasIdToken: typeof tokens.id_token === "string" && tokens.id_token.length > 0,
42
- lastRefresh: typeof auth.last_refresh === "string" ? auth.last_refresh : void 0
257
+ lastRefresh: typeof auth.last_refresh === "string" ? auth.last_refresh : void 0,
258
+ gatewayProvider
43
259
  };
44
260
  }
45
261
  async function applyProfileToCodexAuth(profile) {
@@ -82,13 +298,88 @@ async function applyProfileToCodexAuth(profile) {
82
298
  accountId: profile.accountId,
83
299
  hasIdToken: true,
84
300
  lastRefresh: authFile.last_refresh,
301
+ gatewayProvider: await getCodexGatewayProviderStatus(),
85
302
  backupPath,
86
303
  appliedProfileId: profile.profileId,
87
304
  appliedEmail: profile.email
88
305
  };
89
306
  }
307
+ async function applyGatewayToCodexProviderConfig(params) {
308
+ const providerId = params.providerId?.trim() || DEFAULT_CODEX_PROVIDER_ID;
309
+ validateProviderId(providerId);
310
+ const baseUrl = normalizeCodexProviderBaseUrl(params.baseUrl);
311
+ const configPath = getCodexConfigPath();
312
+ const codexHomeDir = path.dirname(configPath);
313
+ await fs.mkdir(codexHomeDir, { recursive: true });
314
+ let raw = "";
315
+ let backupPath;
316
+ try {
317
+ raw = await fs.readFile(configPath, "utf8");
318
+ backupPath = `${configPath}.azt-backup-${createBackupSuffix()}`;
319
+ await fs.copyFile(configPath, backupPath);
320
+ } catch {
321
+ raw = "";
322
+ backupPath = void 0;
323
+ }
324
+ const next = applyGatewayProviderConfig(raw, providerId, baseUrl);
325
+ const tmpPath = `${configPath}.tmp-${process.pid}`;
326
+ await fs.writeFile(tmpPath, next, {
327
+ encoding: "utf8",
328
+ mode: 384
329
+ });
330
+ await fs.rename(tmpPath, configPath);
331
+ await fs.chmod(configPath, 384);
332
+ return {
333
+ path: configPath,
334
+ backupPath,
335
+ providerId,
336
+ baseUrl
337
+ };
338
+ }
339
+ async function removeGatewayFromCodexProviderConfig(params) {
340
+ const providerId = params?.providerId?.trim() || DEFAULT_CODEX_PROVIDER_ID;
341
+ validateProviderId(providerId);
342
+ const configPath = getCodexConfigPath();
343
+ let raw = "";
344
+ try {
345
+ raw = await fs.readFile(configPath, "utf8");
346
+ } catch {
347
+ return {
348
+ path: configPath,
349
+ providerId,
350
+ removed: false
351
+ };
352
+ }
353
+ const next = removeGatewayProviderConfig(raw, providerId);
354
+ if (!next.removed) {
355
+ return {
356
+ path: configPath,
357
+ providerId,
358
+ removed: false
359
+ };
360
+ }
361
+ const backupPath = `${configPath}.azt-backup-${createBackupSuffix()}`;
362
+ await fs.copyFile(configPath, backupPath);
363
+ const tmpPath = `${configPath}.tmp-${process.pid}`;
364
+ await fs.writeFile(tmpPath, next.raw.trim() ? next.raw : "", {
365
+ encoding: "utf8",
366
+ mode: 384
367
+ });
368
+ await fs.rename(tmpPath, configPath);
369
+ await fs.chmod(configPath, 384);
370
+ return {
371
+ path: configPath,
372
+ backupPath,
373
+ providerId,
374
+ removed: true
375
+ };
376
+ }
90
377
  export {
378
+ applyGatewayToCodexProviderConfig,
91
379
  applyProfileToCodexAuth,
92
380
  getCodexAuthPath,
93
- getCodexAuthStatus
381
+ getCodexAuthStatus,
382
+ getCodexConfigPath,
383
+ getCodexGatewayProviderStatus,
384
+ removeGatewayFromCodexProviderConfig
94
385
  };
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
2
3
  import fs from "node:fs/promises";
3
4
  import {
4
5
  ensureStateMigrated,
@@ -16,7 +17,8 @@ function createDefaultSettings() {
16
17
  noProxy: "localhost,127.0.0.1,::1"
17
18
  },
18
19
  autoSwitch: {
19
- enabled: false
20
+ enabled: false,
21
+ excludedProfileIds: []
20
22
  },
21
23
  runtime: {
22
24
  quotaSyncConcurrency: 16
@@ -27,32 +29,36 @@ function createDefaultSettings() {
27
29
  }
28
30
  };
29
31
  }
32
+ function normalizeSettings(parsed) {
33
+ const defaults = createDefaultSettings();
34
+ return {
35
+ version: 1,
36
+ defaultProvider: parsed.defaultProvider ?? defaults.defaultProvider,
37
+ defaultModel: parsed.defaultModel ?? defaults.defaultModel,
38
+ networkProxy: {
39
+ enabled: parsed.networkProxy?.enabled ?? defaults.networkProxy.enabled,
40
+ url: parsed.networkProxy?.url ?? defaults.networkProxy.url,
41
+ noProxy: parsed.networkProxy?.noProxy ?? defaults.networkProxy.noProxy
42
+ },
43
+ autoSwitch: {
44
+ enabled: parsed.autoSwitch?.enabled ?? defaults.autoSwitch.enabled,
45
+ excludedProfileIds: normalizeStringList(parsed.autoSwitch?.excludedProfileIds)
46
+ },
47
+ runtime: {
48
+ quotaSyncConcurrency: normalizeQuotaSyncConcurrency(parsed.runtime?.quotaSyncConcurrency, defaults.runtime.quotaSyncConcurrency)
49
+ },
50
+ server: {
51
+ host: parsed.server?.host ?? defaults.server.host,
52
+ port: parsed.server?.port ?? defaults.server.port
53
+ }
54
+ };
55
+ }
30
56
  async function loadSettings() {
31
57
  try {
32
58
  await ensureStateMigrated();
33
59
  const raw = await fs.readFile(getSettingsPath(), "utf8");
34
60
  const parsed = JSON.parse(raw);
35
- const defaults = createDefaultSettings();
36
- return {
37
- version: 1,
38
- defaultProvider: parsed.defaultProvider ?? defaults.defaultProvider,
39
- defaultModel: parsed.defaultModel ?? defaults.defaultModel,
40
- networkProxy: {
41
- enabled: parsed.networkProxy?.enabled ?? defaults.networkProxy.enabled,
42
- url: parsed.networkProxy?.url ?? defaults.networkProxy.url,
43
- noProxy: parsed.networkProxy?.noProxy ?? defaults.networkProxy.noProxy
44
- },
45
- autoSwitch: {
46
- enabled: parsed.autoSwitch?.enabled ?? defaults.autoSwitch.enabled
47
- },
48
- runtime: {
49
- quotaSyncConcurrency: normalizeQuotaSyncConcurrency(parsed.runtime?.quotaSyncConcurrency, defaults.runtime.quotaSyncConcurrency)
50
- },
51
- server: {
52
- host: parsed.server?.host ?? defaults.server.host,
53
- port: parsed.server?.port ?? defaults.server.port
54
- }
55
- };
61
+ return normalizeSettings(parsed);
56
62
  } catch {
57
63
  return createDefaultSettings();
58
64
  }
@@ -64,11 +70,35 @@ function normalizeQuotaSyncConcurrency(value, fallback = 16) {
64
70
  }
65
71
  return Math.min(32, Math.max(1, Math.trunc(parsed)));
66
72
  }
67
- async function saveSettings(settings) {
73
+ function normalizeStringList(value) {
74
+ if (!Array.isArray(value)) {
75
+ return [];
76
+ }
77
+ return Array.from(
78
+ new Set(
79
+ value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)
80
+ )
81
+ );
82
+ }
83
+ let settingsSaveQueue = Promise.resolve();
84
+ async function writeSettingsAtomic(settings) {
68
85
  await ensureStateMigrated();
69
86
  await fs.mkdir(getStateDir(), { recursive: true });
70
- await fs.writeFile(getSettingsPath(), `${JSON.stringify(settings, null, 2)}
87
+ const settingsPath = getSettingsPath();
88
+ const tempPath = `${settingsPath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
89
+ try {
90
+ await fs.writeFile(tempPath, `${JSON.stringify(settings, null, 2)}
71
91
  `, "utf8");
92
+ await fs.rename(tempPath, settingsPath);
93
+ } catch (error) {
94
+ await fs.rm(tempPath, { force: true }).catch(() => void 0);
95
+ throw error;
96
+ }
97
+ }
98
+ async function saveSettings(settings) {
99
+ const nextSave = settingsSaveQueue.then(() => writeSettingsAtomic(settings), () => writeSettingsAtomic(settings));
100
+ settingsSaveQueue = nextSave.catch(() => void 0);
101
+ await nextSave;
72
102
  }
73
103
  export {
74
104
  createDefaultSettings,