ai-zero-token 2.0.3 → 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.
- package/CHANGELOG.md +19 -0
- package/README.md +34 -2
- package/README.zh-CN.md +35 -3
- package/admin-ui/dist/assets/{accounts-DymL4WIa.js → accounts-ABMyXo4H.js} +3 -1
- package/admin-ui/dist/assets/{docs-DtO-AOWU.js → docs-Dh0aFha_.js} +3 -3
- package/admin-ui/dist/assets/{image-bed-yIVQ4dKs.js → image-bed-C1M7-0q1.js} +1 -1
- package/admin-ui/dist/assets/index--rNjdmzf.js +10 -0
- package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
- package/admin-ui/dist/assets/{launch-CQXYrl-h.js → launch-pB7YlWFI.js} +1 -1
- package/admin-ui/dist/assets/logs-B7McijSi.js +1 -0
- package/admin-ui/dist/assets/{network-detect-sSrnwZqf.js → network-detect-Bx3XmXPk.js} +1 -1
- package/admin-ui/dist/assets/{overview-BbSON0Jl.js → overview-CV0H2Nsq.js} +1 -1
- package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
- package/admin-ui/dist/assets/{tester-CftPgRE9.js → tester-BG-up8qP.js} +1 -1
- package/admin-ui/dist/index.html +2 -2
- package/build/tray-icon-template.png +0 -0
- package/dist/core/providers/http-client.js +228 -3
- package/dist/core/providers/openai-codex/chat.js +160 -23
- package/dist/core/services/auth-service.js +14 -5
- package/dist/core/services/chat-service.js +1 -0
- package/dist/core/services/config-service.js +15 -5
- package/dist/core/store/codex-auth-store.js +295 -4
- package/dist/core/store/settings-store.js +54 -24
- package/dist/desktop/main.js +616 -15
- package/dist/server/app.js +859 -91
- package/dist/server/index.js +2 -1
- package/docs/API_USAGE.md +82 -1
- package/docs/DESKTOP_RELEASE.md +24 -0
- package/package.json +3 -1
- package/admin-ui/dist/assets/index-DRe-tByu.js +0 -10
- package/admin-ui/dist/assets/logs-awABDg1C.js +0 -1
- package/admin-ui/dist/assets/settings-DvRiHS7i.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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|