ai-zero-token 2.0.4 → 2.0.6

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 (37) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +23 -5
  3. package/README.zh-CN.md +24 -6
  4. package/admin-ui/dist/assets/StatCard-7TEzqn2i.js +1 -0
  5. package/admin-ui/dist/assets/accounts-bCDKXGg9.js +4 -0
  6. package/admin-ui/dist/assets/{docs-oNIugCIL.js → docs--eK_2fzC.js} +1 -1
  7. package/admin-ui/dist/assets/{image-bed-CQtIhjg_.js → image-bed-7wBZ1GhS.js} +1 -1
  8. package/admin-ui/dist/assets/index-C22_3Mxq.css +1 -0
  9. package/admin-ui/dist/assets/index-CdFYy5j6.js +10 -0
  10. package/admin-ui/dist/assets/{launch-B-2Zdz9m.js → launch-BiD1Khtg.js} +1 -1
  11. package/admin-ui/dist/assets/{logs-JFuSf56b.js → logs-BdoKDqh2.js} +1 -1
  12. package/admin-ui/dist/assets/{network-detect-SfvK6uhx.js → network-detect-BvKns5nQ.js} +1 -1
  13. package/admin-ui/dist/assets/overview-wm6M45fu.js +1 -0
  14. package/admin-ui/dist/assets/settings-DOOu7Kd8.js +5 -0
  15. package/admin-ui/dist/assets/{tester-ocpF053C.js → tester-NrARmlis.js} +1 -1
  16. package/admin-ui/dist/assets/usage-CdWRVMDV.js +1 -0
  17. package/admin-ui/dist/index.html +2 -2
  18. package/dist/core/context.js +3 -0
  19. package/dist/core/providers/http-client.js +247 -3
  20. package/dist/core/providers/openai-codex/chat.js +84 -23
  21. package/dist/core/providers/openai-codex/chatgpt-web-image.js +1404 -0
  22. package/dist/core/services/auth-service.js +64 -8
  23. package/dist/core/services/config-service.js +24 -5
  24. package/dist/core/services/image-service.js +31 -1
  25. package/dist/core/services/usage-service.js +349 -0
  26. package/dist/core/store/codex-auth-store.js +429 -4
  27. package/dist/core/store/settings-store.js +62 -26
  28. package/dist/core/store/state-paths.js +17 -1
  29. package/dist/server/app.js +1278 -119
  30. package/docs/API_USAGE.md +48 -1
  31. package/docs/DESKTOP_RELEASE.md +12 -1
  32. package/package.json +1 -1
  33. package/admin-ui/dist/assets/accounts-CTjk9c4F.js +0 -4
  34. package/admin-ui/dist/assets/index-By4r-wy3.css +0 -1
  35. package/admin-ui/dist/assets/index-rgcJgVAu.js +0 -10
  36. package/admin-ui/dist/assets/overview-X_WodIqE.js +0 -1
  37. package/admin-ui/dist/assets/settings-0eXUAvcm.js +0 -1
@@ -1,16 +1,301 @@
1
1
  #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
2
3
  import fs from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ const execFileAsync = promisify(execFile);
5
8
  function getCodexHomeDir() {
6
9
  return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
7
10
  }
11
+ const OPENAI_CODEX_PROVIDER_ID = "openai";
12
+ const LEGACY_CODEX_PROVIDER_ID = "ai-zero-token";
13
+ const DEFAULT_CODEX_PROVIDER_ID = OPENAI_CODEX_PROVIDER_ID;
8
14
  function getCodexAuthPath() {
9
15
  return path.join(getCodexHomeDir(), "auth.json");
10
16
  }
17
+ function getCodexConfigPath() {
18
+ return path.join(getCodexHomeDir(), "config.toml");
19
+ }
20
+ function getCodexStateDbPath() {
21
+ return path.join(getCodexHomeDir(), "state_5.sqlite");
22
+ }
11
23
  function createBackupSuffix() {
12
24
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13
25
  }
26
+ function escapeRegExp(value) {
27
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+ function formatTomlString(value) {
30
+ return JSON.stringify(value);
31
+ }
32
+ function validateProviderId(providerId) {
33
+ if (!/^[A-Za-z0-9_-]+$/.test(providerId)) {
34
+ throw new Error("Codex providerId \u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF\u3002");
35
+ }
36
+ }
37
+ async function fileExists(targetPath) {
38
+ try {
39
+ await fs.access(targetPath);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ function sqliteQuote(value) {
46
+ return `'${value.replace(/'/g, "''")}'`;
47
+ }
48
+ async function runSqlite(dbPath, sql) {
49
+ const { stdout } = await execFileAsync("sqlite3", [dbPath, sql], {
50
+ timeout: 15e3,
51
+ maxBuffer: 1024 * 1024
52
+ });
53
+ return stdout.trim();
54
+ }
55
+ async function migrateLegacyCodexHistoryProvider() {
56
+ const dbPath = getCodexStateDbPath();
57
+ if (!await fileExists(dbPath)) {
58
+ return {
59
+ path: dbPath,
60
+ migratedCount: 0,
61
+ skipped: true
62
+ };
63
+ }
64
+ try {
65
+ const countRaw = await runSqlite(
66
+ dbPath,
67
+ `select count(*) from threads where model_provider=${sqliteQuote(LEGACY_CODEX_PROVIDER_ID)};`
68
+ );
69
+ const migratedCount = Number.parseInt(countRaw, 10);
70
+ if (!Number.isFinite(migratedCount) || migratedCount <= 0) {
71
+ return {
72
+ path: dbPath,
73
+ migratedCount: 0,
74
+ skipped: true
75
+ };
76
+ }
77
+ const backupPath = `${dbPath}.azt-backup-${createBackupSuffix()}`;
78
+ await runSqlite(dbPath, `.backup ${sqliteQuote(backupPath)}`);
79
+ await runSqlite(
80
+ dbPath,
81
+ `update threads set model_provider=${sqliteQuote(OPENAI_CODEX_PROVIDER_ID)} where model_provider=${sqliteQuote(LEGACY_CODEX_PROVIDER_ID)};`
82
+ );
83
+ return {
84
+ path: dbPath,
85
+ backupPath,
86
+ migratedCount
87
+ };
88
+ } catch (error) {
89
+ return {
90
+ path: dbPath,
91
+ migratedCount: 0,
92
+ error: error instanceof Error ? error.message : String(error)
93
+ };
94
+ }
95
+ }
96
+ function normalizeCodexProviderBaseUrl(value) {
97
+ let normalized = value.trim();
98
+ if (!normalized) {
99
+ throw new Error("Codex provider base_url \u4E0D\u80FD\u4E3A\u7A7A\u3002");
100
+ }
101
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(normalized)) {
102
+ normalized = `http://${normalized}`;
103
+ }
104
+ let url;
105
+ try {
106
+ url = new URL(normalized);
107
+ } catch {
108
+ throw new Error("Codex provider base_url \u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u586B\u5199\u5B8C\u6574\u7684 http(s) URL\u3002");
109
+ }
110
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
111
+ throw new Error("Codex provider base_url \u5FC5\u987B\u662F http(s) URL\u3002");
112
+ }
113
+ url.hash = "";
114
+ url.search = "";
115
+ const path2 = url.pathname.replace(/\/+$/g, "");
116
+ if (!path2 || path2 === "/") {
117
+ url.pathname = "/codex/v1";
118
+ } else if (path2 === "/v1") {
119
+ url.pathname = "/codex/v1";
120
+ } else if (path2.endsWith("/codex")) {
121
+ url.pathname = `${path2}/v1`;
122
+ } else {
123
+ url.pathname = path2;
124
+ }
125
+ return url.toString().replace(/\/+$/g, "");
126
+ }
127
+ function parseTomlStringValue(value) {
128
+ const trimmed = value.trim().replace(/\s+#.*$/g, "");
129
+ if (!trimmed) {
130
+ return void 0;
131
+ }
132
+ if (trimmed.startsWith('"')) {
133
+ try {
134
+ const parsed = JSON.parse(trimmed);
135
+ return typeof parsed === "string" ? parsed : void 0;
136
+ } catch {
137
+ return void 0;
138
+ }
139
+ }
140
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
141
+ return trimmed.slice(1, -1);
142
+ }
143
+ return trimmed;
144
+ }
145
+ function findFirstTableLine(lines) {
146
+ const index = lines.findIndex((line) => /^\s*\[/.test(line));
147
+ return index === -1 ? lines.length : index;
148
+ }
149
+ function parseRootString(raw, key) {
150
+ const lines = raw.split(/\r?\n/);
151
+ const firstTableLine = findFirstTableLine(lines);
152
+ const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*(.+)$`);
153
+ for (let index = 0; index < firstTableLine; index += 1) {
154
+ const match = keyPattern.exec(lines[index] ?? "");
155
+ if (match) {
156
+ return parseTomlStringValue(match[1] ?? "");
157
+ }
158
+ }
159
+ return void 0;
160
+ }
161
+ function parseRootModelProvider(raw) {
162
+ return parseRootString(raw, "model_provider");
163
+ }
164
+ function parseGatewayProviderTable(raw, providerId) {
165
+ const lines = raw.split(/\r?\n/);
166
+ const tablePattern = new RegExp(`^\\s*\\[\\s*model_providers\\.${escapeRegExp(providerId)}\\s*\\]\\s*$`);
167
+ const start = lines.findIndex((line) => tablePattern.test(line));
168
+ if (start === -1) {
169
+ return { exists: false };
170
+ }
171
+ let baseUrl;
172
+ for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index] ?? ""); index += 1) {
173
+ const match = /^\s*base_url\s*=\s*(.+)$/.exec(lines[index] ?? "");
174
+ if (match) {
175
+ baseUrl = parseTomlStringValue(match[1] ?? "");
176
+ }
177
+ }
178
+ return { exists: true, baseUrl };
179
+ }
180
+ function upsertRootString(raw, key, value) {
181
+ if (!raw.trim()) {
182
+ return `${key} = ${formatTomlString(value)}
183
+ `;
184
+ }
185
+ const lines = raw.split(/\r?\n/);
186
+ const firstTableLine = findFirstTableLine(lines);
187
+ const nextLine = `${key} = ${formatTomlString(value)}`;
188
+ const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
189
+ for (let index = 0; index < firstTableLine; index += 1) {
190
+ if (keyPattern.test(lines[index])) {
191
+ lines[index] = nextLine;
192
+ return lines.join("\n");
193
+ }
194
+ }
195
+ lines.splice(firstTableLine, 0, nextLine, "");
196
+ return lines.join("\n");
197
+ }
198
+ function upsertRootModelProvider(raw, providerId) {
199
+ return upsertRootString(raw, "model_provider", providerId);
200
+ }
201
+ function buildGatewayProviderBlock(providerId, baseUrl) {
202
+ return [
203
+ "# AI Zero Token managed Codex provider",
204
+ `[model_providers.${providerId}]`,
205
+ 'name = "AI Zero Token"',
206
+ `base_url = ${formatTomlString(baseUrl)}`,
207
+ 'wire_api = "responses"',
208
+ "supports_websockets = false"
209
+ ];
210
+ }
211
+ function upsertGatewayProviderTable(raw, providerId, baseUrl) {
212
+ const lines = raw.split(/\r?\n/);
213
+ const tablePattern = new RegExp(`^\\s*\\[\\s*model_providers\\.${escapeRegExp(providerId)}\\s*\\]\\s*$`);
214
+ const start = lines.findIndex((line) => tablePattern.test(line));
215
+ const block = buildGatewayProviderBlock(providerId, baseUrl);
216
+ if (start === -1) {
217
+ const trimmed = raw.replace(/\s+$/g, "");
218
+ return `${trimmed}${trimmed ? "\n\n" : ""}${block.join("\n")}
219
+ `;
220
+ }
221
+ let end = start + 1;
222
+ while (end < lines.length && !/^\s*\[/.test(lines[end])) {
223
+ end += 1;
224
+ }
225
+ const replaceStart = start > 0 && /AI Zero Token managed Codex provider/.test(lines[start - 1]) ? start - 1 : start;
226
+ lines.splice(replaceStart, end - replaceStart, ...block, "");
227
+ return lines.join("\n").replace(/\s+$/g, "\n");
228
+ }
229
+ function applyGatewayProviderConfig(raw, providerId, baseUrl) {
230
+ const sanitizedRaw = removeOpenAIGatewayConfig(raw).raw;
231
+ return upsertGatewayProviderTable(upsertRootModelProvider(sanitizedRaw, providerId), providerId, baseUrl);
232
+ }
233
+ function applyOpenAIGatewayConfig(raw, baseUrl) {
234
+ const withoutLegacyProvider = removeGatewayProviderConfig(raw, LEGACY_CODEX_PROVIDER_ID).raw;
235
+ return upsertRootString(
236
+ upsertRootModelProvider(withoutLegacyProvider, OPENAI_CODEX_PROVIDER_ID),
237
+ "openai_base_url",
238
+ baseUrl
239
+ );
240
+ }
241
+ function removeRootString(raw, key, expectedValue) {
242
+ const lines = raw.split(/\r?\n/);
243
+ const firstTableLine = findFirstTableLine(lines);
244
+ const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*(.+)$`);
245
+ for (let index = 0; index < firstTableLine; index += 1) {
246
+ const match = keyPattern.exec(lines[index] ?? "");
247
+ if (!match) {
248
+ continue;
249
+ }
250
+ if (typeof expectedValue === "string" && parseTomlStringValue(match[1] ?? "") !== expectedValue) {
251
+ continue;
252
+ }
253
+ lines.splice(index, 1);
254
+ if (lines[index] === "" && (index === 0 || lines[index - 1] === "")) {
255
+ lines.splice(index, 1);
256
+ }
257
+ return { raw: lines.join("\n").replace(/^\s+\n/g, ""), removed: true };
258
+ }
259
+ return { raw, removed: false };
260
+ }
261
+ function removeRootModelProvider(raw, providerId) {
262
+ return removeRootString(raw, "model_provider", providerId);
263
+ }
264
+ function removeGatewayProviderTable(raw, providerId) {
265
+ const lines = raw.split(/\r?\n/);
266
+ const tablePattern = new RegExp(`^\\s*\\[\\s*model_providers\\.${escapeRegExp(providerId)}\\s*\\]\\s*$`);
267
+ const start = lines.findIndex((line) => tablePattern.test(line));
268
+ if (start === -1) {
269
+ return { raw, removed: false };
270
+ }
271
+ let end = start + 1;
272
+ while (end < lines.length && !/^\s*\[/.test(lines[end])) {
273
+ end += 1;
274
+ }
275
+ const replaceStart = start > 0 && /AI Zero Token managed Codex provider/.test(lines[start - 1]) ? start - 1 : start;
276
+ lines.splice(replaceStart, end - replaceStart);
277
+ return {
278
+ raw: lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/g, "\n"),
279
+ removed: true
280
+ };
281
+ }
282
+ function removeGatewayProviderConfig(raw, providerId) {
283
+ const rootRemoved = removeRootModelProvider(raw, providerId);
284
+ const tableRemoved = removeGatewayProviderTable(rootRemoved.raw, providerId);
285
+ return {
286
+ raw: tableRemoved.raw,
287
+ removed: rootRemoved.removed || tableRemoved.removed
288
+ };
289
+ }
290
+ function removeOpenAIGatewayConfig(raw) {
291
+ const openAIBaseRemoved = removeRootString(raw, "openai_base_url");
292
+ const openAIProviderRemoved = removeRootModelProvider(openAIBaseRemoved.raw, OPENAI_CODEX_PROVIDER_ID);
293
+ const legacyRemoved = removeGatewayProviderConfig(openAIProviderRemoved.raw, LEGACY_CODEX_PROVIDER_ID);
294
+ return {
295
+ raw: legacyRemoved.raw,
296
+ removed: openAIBaseRemoved.removed || openAIProviderRemoved.removed || legacyRemoved.removed
297
+ };
298
+ }
14
299
  function isRecord(value) {
15
300
  return typeof value === "object" && value !== null && !Array.isArray(value);
16
301
  }
@@ -23,14 +308,69 @@ async function readCodexAuth() {
23
308
  return null;
24
309
  }
25
310
  }
311
+ async function getCodexGatewayProviderStatus(params) {
312
+ const requestedProviderId = params?.providerId?.trim();
313
+ const providerId = requestedProviderId || DEFAULT_CODEX_PROVIDER_ID;
314
+ validateProviderId(providerId);
315
+ const configPath = getCodexConfigPath();
316
+ let raw = "";
317
+ try {
318
+ raw = await fs.readFile(configPath, "utf8");
319
+ } catch {
320
+ return {
321
+ path: configPath,
322
+ providerId,
323
+ exists: false,
324
+ active: false
325
+ };
326
+ }
327
+ const modelProvider = parseRootModelProvider(raw);
328
+ if (providerId === OPENAI_CODEX_PROVIDER_ID) {
329
+ const openAIBaseUrl = parseRootString(raw, "openai_base_url");
330
+ if (openAIBaseUrl && (!modelProvider || modelProvider === OPENAI_CODEX_PROVIDER_ID)) {
331
+ return {
332
+ path: configPath,
333
+ providerId: OPENAI_CODEX_PROVIDER_ID,
334
+ exists: true,
335
+ active: !modelProvider || modelProvider === OPENAI_CODEX_PROVIDER_ID,
336
+ baseUrl: openAIBaseUrl,
337
+ modelProvider
338
+ };
339
+ }
340
+ const legacyTable = parseGatewayProviderTable(raw, LEGACY_CODEX_PROVIDER_ID);
341
+ if (!requestedProviderId && legacyTable.exists) {
342
+ return {
343
+ path: configPath,
344
+ providerId: LEGACY_CODEX_PROVIDER_ID,
345
+ exists: true,
346
+ active: modelProvider === LEGACY_CODEX_PROVIDER_ID,
347
+ baseUrl: legacyTable.baseUrl,
348
+ modelProvider
349
+ };
350
+ }
351
+ }
352
+ const table = parseGatewayProviderTable(raw, providerId);
353
+ return {
354
+ path: configPath,
355
+ providerId,
356
+ exists: table.exists,
357
+ active: modelProvider === providerId && table.exists,
358
+ baseUrl: table.baseUrl,
359
+ modelProvider
360
+ };
361
+ }
26
362
  async function getCodexAuthStatus() {
27
363
  const authPath = getCodexAuthPath();
28
- const auth = await readCodexAuth();
364
+ const [auth, gatewayProvider] = await Promise.all([
365
+ readCodexAuth(),
366
+ getCodexGatewayProviderStatus()
367
+ ]);
29
368
  if (!auth) {
30
369
  return {
31
370
  path: authPath,
32
371
  exists: false,
33
- hasIdToken: false
372
+ hasIdToken: false,
373
+ gatewayProvider
34
374
  };
35
375
  }
36
376
  const tokens = isRecord(auth.tokens) ? auth.tokens : {};
@@ -39,7 +379,8 @@ async function getCodexAuthStatus() {
39
379
  exists: true,
40
380
  accountId: typeof tokens.account_id === "string" ? tokens.account_id : void 0,
41
381
  hasIdToken: typeof tokens.id_token === "string" && tokens.id_token.length > 0,
42
- lastRefresh: typeof auth.last_refresh === "string" ? auth.last_refresh : void 0
382
+ lastRefresh: typeof auth.last_refresh === "string" ? auth.last_refresh : void 0,
383
+ gatewayProvider
43
384
  };
44
385
  }
45
386
  async function applyProfileToCodexAuth(profile) {
@@ -82,13 +423,97 @@ async function applyProfileToCodexAuth(profile) {
82
423
  accountId: profile.accountId,
83
424
  hasIdToken: true,
84
425
  lastRefresh: authFile.last_refresh,
426
+ gatewayProvider: await getCodexGatewayProviderStatus(),
85
427
  backupPath,
86
428
  appliedProfileId: profile.profileId,
87
429
  appliedEmail: profile.email
88
430
  };
89
431
  }
432
+ async function applyGatewayToCodexProviderConfig(params) {
433
+ const providerId = params.providerId?.trim() || DEFAULT_CODEX_PROVIDER_ID;
434
+ validateProviderId(providerId);
435
+ const baseUrl = normalizeCodexProviderBaseUrl(params.baseUrl);
436
+ const configPath = getCodexConfigPath();
437
+ const codexHomeDir = path.dirname(configPath);
438
+ await fs.mkdir(codexHomeDir, { recursive: true });
439
+ let raw = "";
440
+ let backupPath;
441
+ try {
442
+ raw = await fs.readFile(configPath, "utf8");
443
+ backupPath = `${configPath}.azt-backup-${createBackupSuffix()}`;
444
+ await fs.copyFile(configPath, backupPath);
445
+ } catch {
446
+ raw = "";
447
+ backupPath = void 0;
448
+ }
449
+ const useOpenAIProvider = providerId === OPENAI_CODEX_PROVIDER_ID;
450
+ const next = useOpenAIProvider ? applyOpenAIGatewayConfig(raw, baseUrl) : applyGatewayProviderConfig(raw, providerId, baseUrl);
451
+ const tmpPath = `${configPath}.tmp-${process.pid}`;
452
+ await fs.writeFile(tmpPath, next, {
453
+ encoding: "utf8",
454
+ mode: 384
455
+ });
456
+ await fs.rename(tmpPath, configPath);
457
+ await fs.chmod(configPath, 384);
458
+ return {
459
+ path: configPath,
460
+ backupPath,
461
+ providerId,
462
+ baseUrl,
463
+ historyMigration: useOpenAIProvider ? await migrateLegacyCodexHistoryProvider() : void 0
464
+ };
465
+ }
466
+ async function removeGatewayFromCodexProviderConfig(params) {
467
+ const providerId = params?.providerId?.trim() || DEFAULT_CODEX_PROVIDER_ID;
468
+ validateProviderId(providerId);
469
+ const configPath = getCodexConfigPath();
470
+ let raw = "";
471
+ try {
472
+ raw = await fs.readFile(configPath, "utf8");
473
+ } catch {
474
+ return {
475
+ path: configPath,
476
+ providerId,
477
+ removed: false
478
+ };
479
+ }
480
+ const sanitized = removeOpenAIGatewayConfig(raw);
481
+ const next = providerId === OPENAI_CODEX_PROVIDER_ID || providerId === LEGACY_CODEX_PROVIDER_ID ? sanitized : (() => {
482
+ const providerRemoved = removeGatewayProviderConfig(sanitized.raw, providerId);
483
+ return {
484
+ raw: providerRemoved.raw,
485
+ removed: sanitized.removed || providerRemoved.removed
486
+ };
487
+ })();
488
+ if (!next.removed) {
489
+ return {
490
+ path: configPath,
491
+ providerId,
492
+ removed: false
493
+ };
494
+ }
495
+ const backupPath = `${configPath}.azt-backup-${createBackupSuffix()}`;
496
+ await fs.copyFile(configPath, backupPath);
497
+ const tmpPath = `${configPath}.tmp-${process.pid}`;
498
+ await fs.writeFile(tmpPath, next.raw.trim() ? next.raw : "", {
499
+ encoding: "utf8",
500
+ mode: 384
501
+ });
502
+ await fs.rename(tmpPath, configPath);
503
+ await fs.chmod(configPath, 384);
504
+ return {
505
+ path: configPath,
506
+ backupPath,
507
+ providerId,
508
+ removed: true
509
+ };
510
+ }
90
511
  export {
512
+ applyGatewayToCodexProviderConfig,
91
513
  applyProfileToCodexAuth,
92
514
  getCodexAuthPath,
93
- getCodexAuthStatus
515
+ getCodexAuthStatus,
516
+ getCodexConfigPath,
517
+ getCodexGatewayProviderStatus,
518
+ removeGatewayFromCodexProviderConfig
94
519
  };
@@ -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,10 +17,14 @@ 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
- quotaSyncConcurrency: 16
24
+ quotaSyncConcurrency: 3
25
+ },
26
+ image: {
27
+ freeAccountWebGenerationEnabled: false
23
28
  },
24
29
  server: {
25
30
  host: "0.0.0.0",
@@ -27,48 +32,79 @@ function createDefaultSettings() {
27
32
  }
28
33
  };
29
34
  }
35
+ function normalizeSettings(parsed) {
36
+ const defaults = createDefaultSettings();
37
+ return {
38
+ version: 1,
39
+ defaultProvider: parsed.defaultProvider ?? defaults.defaultProvider,
40
+ defaultModel: parsed.defaultModel ?? defaults.defaultModel,
41
+ networkProxy: {
42
+ enabled: parsed.networkProxy?.enabled ?? defaults.networkProxy.enabled,
43
+ url: parsed.networkProxy?.url ?? defaults.networkProxy.url,
44
+ noProxy: parsed.networkProxy?.noProxy ?? defaults.networkProxy.noProxy
45
+ },
46
+ autoSwitch: {
47
+ enabled: parsed.autoSwitch?.enabled ?? defaults.autoSwitch.enabled,
48
+ excludedProfileIds: normalizeStringList(parsed.autoSwitch?.excludedProfileIds)
49
+ },
50
+ runtime: {
51
+ quotaSyncConcurrency: normalizeQuotaSyncConcurrency(parsed.runtime?.quotaSyncConcurrency, defaults.runtime.quotaSyncConcurrency)
52
+ },
53
+ image: {
54
+ freeAccountWebGenerationEnabled: parsed.image?.freeAccountWebGenerationEnabled ?? defaults.image.freeAccountWebGenerationEnabled
55
+ },
56
+ server: {
57
+ host: parsed.server?.host ?? defaults.server.host,
58
+ port: parsed.server?.port ?? defaults.server.port
59
+ }
60
+ };
61
+ }
30
62
  async function loadSettings() {
31
63
  try {
32
64
  await ensureStateMigrated();
33
65
  const raw = await fs.readFile(getSettingsPath(), "utf8");
34
66
  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
- };
67
+ return normalizeSettings(parsed);
56
68
  } catch {
57
69
  return createDefaultSettings();
58
70
  }
59
71
  }
60
- function normalizeQuotaSyncConcurrency(value, fallback = 16) {
72
+ function normalizeQuotaSyncConcurrency(value, fallback = 3) {
61
73
  const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : fallback;
62
74
  if (!Number.isFinite(parsed)) {
63
75
  return fallback;
64
76
  }
65
77
  return Math.min(32, Math.max(1, Math.trunc(parsed)));
66
78
  }
67
- async function saveSettings(settings) {
79
+ function normalizeStringList(value) {
80
+ if (!Array.isArray(value)) {
81
+ return [];
82
+ }
83
+ return Array.from(
84
+ new Set(
85
+ value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)
86
+ )
87
+ );
88
+ }
89
+ let settingsSaveQueue = Promise.resolve();
90
+ async function writeSettingsAtomic(settings) {
68
91
  await ensureStateMigrated();
69
92
  await fs.mkdir(getStateDir(), { recursive: true });
70
- await fs.writeFile(getSettingsPath(), `${JSON.stringify(settings, null, 2)}
93
+ const settingsPath = getSettingsPath();
94
+ const tempPath = `${settingsPath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
95
+ try {
96
+ await fs.writeFile(tempPath, `${JSON.stringify(settings, null, 2)}
71
97
  `, "utf8");
98
+ await fs.rename(tempPath, settingsPath);
99
+ } catch (error) {
100
+ await fs.rm(tempPath, { force: true }).catch(() => void 0);
101
+ throw error;
102
+ }
103
+ }
104
+ async function saveSettings(settings) {
105
+ const nextSave = settingsSaveQueue.then(() => writeSettingsAtomic(settings), () => writeSettingsAtomic(settings));
106
+ settingsSaveQueue = nextSave.catch(() => void 0);
107
+ await nextSave;
72
108
  }
73
109
  export {
74
110
  createDefaultSettings,
@@ -34,6 +34,18 @@ function getStorePath() {
34
34
  function getSettingsPath() {
35
35
  return path.join(stateDir, "settings.json");
36
36
  }
37
+ function getUsageDir() {
38
+ return path.join(stateDir, "usage");
39
+ }
40
+ function getUsageEventsDir() {
41
+ return path.join(getUsageDir(), "events");
42
+ }
43
+ function getUsageDailyPath() {
44
+ return path.join(getUsageDir(), "daily.json");
45
+ }
46
+ function getUsageLifetimePath() {
47
+ return path.join(getUsageDir(), "lifetime.json");
48
+ }
37
49
  async function ensureStateMigrated() {
38
50
  if (!migrationPromise) {
39
51
  migrationPromise = (async () => {
@@ -50,5 +62,9 @@ export {
50
62
  ensureStateMigrated,
51
63
  getSettingsPath,
52
64
  getStateDir,
53
- getStorePath
65
+ getStorePath,
66
+ getUsageDailyPath,
67
+ getUsageDir,
68
+ getUsageEventsDir,
69
+ getUsageLifetimePath
54
70
  };