codex-endpoint-switcher 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/bin/codex-switcher.js +89 -0
- package/install-web-access.ps1 +61 -0
- package/open-web-console.cmd +3 -0
- package/open-web-console.vbs +2 -0
- package/package.json +74 -0
- package/remove-web-access.ps1 +17 -0
- package/src/main/main.js +74 -0
- package/src/main/preload.js +28 -0
- package/src/main/profile-manager.js +439 -0
- package/src/renderer/index.html +140 -0
- package/src/renderer/renderer.js +340 -0
- package/src/renderer/styles.css +507 -0
- package/src/web/launcher.js +248 -0
- package/src/web/server.js +132 -0
- package/start-web-background.cmd +3 -0
- package/start-web-background.vbs +2 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const { randomUUID } = require("node:crypto");
|
|
5
|
+
|
|
6
|
+
const codexRoot = path.join(os.homedir(), ".codex");
|
|
7
|
+
const backupDir = path.join(codexRoot, "backup");
|
|
8
|
+
const mainConfigPath = path.join(codexRoot, "config.toml");
|
|
9
|
+
const authConfigPath = path.join(codexRoot, "auth.json");
|
|
10
|
+
const endpointStorePath = path.join(codexRoot, "endpoint-presets.json");
|
|
11
|
+
|
|
12
|
+
function escapeRegExp(value) {
|
|
13
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function escapeTomlString(value) {
|
|
17
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripQuotedValue(value) {
|
|
21
|
+
if (typeof value !== "string") {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
27
|
+
return trimmed.slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return trimmed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function maskKey(value) {
|
|
34
|
+
const key = String(value || "").trim();
|
|
35
|
+
if (!key) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (key.length <= 10) {
|
|
40
|
+
return `${key.slice(0, 3)}***${key.slice(-2)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `${key.slice(0, 6)}***${key.slice(-4)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeUrl(url) {
|
|
47
|
+
const trimmed = String(url || "").trim();
|
|
48
|
+
let parsedUrl;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
parsedUrl = new URL(trimmed);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error("URL 格式不正确。");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
57
|
+
throw new Error("URL 仅支持 http 或 https。");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parsedUrl.toString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateEndpointPayload(payload) {
|
|
64
|
+
const note = String(payload.note || "").trim();
|
|
65
|
+
const url = normalizeUrl(payload.url);
|
|
66
|
+
const key = String(payload.key || "").trim();
|
|
67
|
+
|
|
68
|
+
if (!note) {
|
|
69
|
+
throw new Error("备注不能为空。");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!key) {
|
|
73
|
+
throw new Error("Key 不能为空。");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { note, url, key };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function ensureDirectory(targetPath) {
|
|
80
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function ensureFileExists(targetPath, errorMessage) {
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(targetPath);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(errorMessage);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readTextFile(targetPath) {
|
|
92
|
+
return fs.readFile(targetPath, "utf8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function writeTextFile(targetPath, content) {
|
|
96
|
+
await fs.writeFile(targetPath, content, "utf8");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readJsonFile(targetPath, fallbackValue) {
|
|
100
|
+
try {
|
|
101
|
+
const content = await readTextFile(targetPath);
|
|
102
|
+
return JSON.parse(content);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error.code === "ENOENT") {
|
|
105
|
+
return fallbackValue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fallbackValue !== undefined) {
|
|
109
|
+
return fallbackValue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function writeJsonFile(targetPath, value) {
|
|
117
|
+
await writeTextFile(targetPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readTomlScalar(content, key) {
|
|
121
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*(.+?)\\s*$`, "m");
|
|
122
|
+
const matched = content.match(pattern);
|
|
123
|
+
if (!matched) {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return stripQuotedValue(matched[1]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readProviderBaseUrl(content, provider) {
|
|
131
|
+
if (!provider) {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lines = content.split(/\r?\n/);
|
|
136
|
+
let insideProviderSection = false;
|
|
137
|
+
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (/^\[.+\]$/.test(trimmed)) {
|
|
141
|
+
insideProviderSection = trimmed === `[model_providers.${provider}]`;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!insideProviderSection) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const matched = line.match(/^\s*base_url\s*=\s*"(.+?)"\s*$/);
|
|
150
|
+
if (matched) {
|
|
151
|
+
return matched[1].trim();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function updateProviderBaseUrl(content, provider, nextUrl) {
|
|
159
|
+
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
160
|
+
const lines = content.split(/\r?\n/);
|
|
161
|
+
const output = [];
|
|
162
|
+
const replacementLine = `base_url = "${escapeTomlString(nextUrl)}"`;
|
|
163
|
+
let foundSection = false;
|
|
164
|
+
let insideProviderSection = false;
|
|
165
|
+
let replaced = false;
|
|
166
|
+
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
const trimmed = line.trim();
|
|
169
|
+
if (/^\[.+\]$/.test(trimmed)) {
|
|
170
|
+
if (insideProviderSection && !replaced) {
|
|
171
|
+
output.push(replacementLine);
|
|
172
|
+
replaced = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
insideProviderSection = trimmed === `[model_providers.${provider}]`;
|
|
176
|
+
if (insideProviderSection) {
|
|
177
|
+
foundSection = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
output.push(line);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (insideProviderSection && /^\s*base_url\s*=/.test(line)) {
|
|
185
|
+
output.push(replacementLine);
|
|
186
|
+
replaced = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
output.push(line);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (insideProviderSection && !replaced) {
|
|
194
|
+
output.push(replacementLine);
|
|
195
|
+
replaced = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!foundSection) {
|
|
199
|
+
throw new Error(`未找到模型提供商配置段:${provider}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!replaced) {
|
|
203
|
+
throw new Error("未能更新 base_url。");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return output.join(newline);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildEndpointResponse(endpoint, activeId) {
|
|
210
|
+
return {
|
|
211
|
+
id: endpoint.id,
|
|
212
|
+
note: endpoint.note,
|
|
213
|
+
url: endpoint.url,
|
|
214
|
+
key: endpoint.key,
|
|
215
|
+
maskedKey: maskKey(endpoint.key),
|
|
216
|
+
createdAt: endpoint.createdAt,
|
|
217
|
+
updatedAt: endpoint.updatedAt,
|
|
218
|
+
isActive: endpoint.id === activeId,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function ensureEndpointStore() {
|
|
223
|
+
await ensureFileExists(mainConfigPath, `未找到 Codex 主配置文件:${mainConfigPath}`);
|
|
224
|
+
await ensureDirectory(backupDir);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await fs.access(endpointStorePath);
|
|
228
|
+
} catch {
|
|
229
|
+
await writeJsonFile(endpointStorePath, []);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function readEndpointStore() {
|
|
234
|
+
await ensureEndpointStore();
|
|
235
|
+
const data = await readJsonFile(endpointStorePath, []);
|
|
236
|
+
return Array.isArray(data) ? data : [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function writeEndpointStore(endpoints) {
|
|
240
|
+
await writeJsonFile(endpointStorePath, endpoints);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function readCurrentConfigAndAuth() {
|
|
244
|
+
await ensureEndpointStore();
|
|
245
|
+
const configContent = await readTextFile(mainConfigPath);
|
|
246
|
+
const authConfig = await readJsonFile(authConfigPath, {});
|
|
247
|
+
const provider = readTomlScalar(configContent, "model_provider");
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
configContent,
|
|
251
|
+
authConfig,
|
|
252
|
+
provider,
|
|
253
|
+
model: readTomlScalar(configContent, "model"),
|
|
254
|
+
baseUrl: readProviderBaseUrl(configContent, provider),
|
|
255
|
+
apiKey: String(authConfig.OPENAI_API_KEY || "").trim(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function backupCurrentState() {
|
|
260
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
261
|
+
const configBackupPath = path.join(backupDir, `config.toml.switch-${timestamp}.bak`);
|
|
262
|
+
const authBackupPath = path.join(backupDir, `auth.json.switch-${timestamp}.bak`);
|
|
263
|
+
|
|
264
|
+
await fs.copyFile(mainConfigPath, configBackupPath);
|
|
265
|
+
try {
|
|
266
|
+
await fs.copyFile(authConfigPath, authBackupPath);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error.code !== "ENOENT") {
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
configBackupPath,
|
|
275
|
+
authBackupPath,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function getCurrentEndpointSummary() {
|
|
280
|
+
const [currentState, endpoints] = await Promise.all([
|
|
281
|
+
readCurrentConfigAndAuth(),
|
|
282
|
+
readEndpointStore(),
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
const activeEndpoint = endpoints.find(
|
|
286
|
+
(item) => item.url === currentState.baseUrl && item.key === currentState.apiKey,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
model: currentState.model,
|
|
291
|
+
provider: currentState.provider,
|
|
292
|
+
currentUrl: currentState.baseUrl,
|
|
293
|
+
currentKeyMasked: maskKey(currentState.apiKey),
|
|
294
|
+
currentNote: activeEndpoint ? activeEndpoint.note : "未命名连接",
|
|
295
|
+
activeEndpointId: activeEndpoint ? activeEndpoint.id : "",
|
|
296
|
+
endpointStorePath,
|
|
297
|
+
authConfigPath,
|
|
298
|
+
mainConfigPath,
|
|
299
|
+
totalEndpoints: endpoints.length,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function listEndpoints() {
|
|
304
|
+
const [currentState, endpoints] = await Promise.all([
|
|
305
|
+
readCurrentConfigAndAuth(),
|
|
306
|
+
readEndpointStore(),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
const activeEndpoint = endpoints.find(
|
|
310
|
+
(item) => item.url === currentState.baseUrl && item.key === currentState.apiKey,
|
|
311
|
+
);
|
|
312
|
+
const activeId = activeEndpoint ? activeEndpoint.id : "";
|
|
313
|
+
|
|
314
|
+
return endpoints
|
|
315
|
+
.slice()
|
|
316
|
+
.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)))
|
|
317
|
+
.map((endpoint) => buildEndpointResponse(endpoint, activeId));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function createEndpoint(payload) {
|
|
321
|
+
const endpoint = validateEndpointPayload(payload);
|
|
322
|
+
const endpoints = await readEndpointStore();
|
|
323
|
+
const now = new Date().toISOString();
|
|
324
|
+
|
|
325
|
+
const nextItem = {
|
|
326
|
+
id: randomUUID(),
|
|
327
|
+
note: endpoint.note,
|
|
328
|
+
url: endpoint.url,
|
|
329
|
+
key: endpoint.key,
|
|
330
|
+
createdAt: now,
|
|
331
|
+
updatedAt: now,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
endpoints.push(nextItem);
|
|
335
|
+
await writeEndpointStore(endpoints);
|
|
336
|
+
return buildEndpointResponse(nextItem, "");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function updateEndpoint(id, payload) {
|
|
340
|
+
const safeId = String(id || "").trim();
|
|
341
|
+
if (!safeId) {
|
|
342
|
+
throw new Error("缺少要更新的端点 ID。");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const endpoint = validateEndpointPayload(payload);
|
|
346
|
+
const endpoints = await readEndpointStore();
|
|
347
|
+
const index = endpoints.findIndex((item) => item.id === safeId);
|
|
348
|
+
|
|
349
|
+
if (index === -1) {
|
|
350
|
+
throw new Error("未找到要更新的端点。");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
endpoints[index] = {
|
|
354
|
+
...endpoints[index],
|
|
355
|
+
note: endpoint.note,
|
|
356
|
+
url: endpoint.url,
|
|
357
|
+
key: endpoint.key,
|
|
358
|
+
updatedAt: new Date().toISOString(),
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
await writeEndpointStore(endpoints);
|
|
362
|
+
return buildEndpointResponse(endpoints[index], "");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function deleteEndpoint(id) {
|
|
366
|
+
const safeId = String(id || "").trim();
|
|
367
|
+
if (!safeId) {
|
|
368
|
+
throw new Error("缺少要删除的端点 ID。");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const endpoints = await readEndpointStore();
|
|
372
|
+
const index = endpoints.findIndex((item) => item.id === safeId);
|
|
373
|
+
|
|
374
|
+
if (index === -1) {
|
|
375
|
+
throw new Error("未找到要删除的端点。");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const [removed] = endpoints.splice(index, 1);
|
|
379
|
+
await writeEndpointStore(endpoints);
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
id: removed.id,
|
|
383
|
+
note: removed.note,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function switchEndpoint(id) {
|
|
388
|
+
const safeId = String(id || "").trim();
|
|
389
|
+
if (!safeId) {
|
|
390
|
+
throw new Error("缺少要切换的端点 ID。");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const endpoints = await readEndpointStore();
|
|
394
|
+
const target = endpoints.find((item) => item.id === safeId);
|
|
395
|
+
|
|
396
|
+
if (!target) {
|
|
397
|
+
throw new Error("未找到要切换的端点。");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const { configContent, authConfig, provider } = await readCurrentConfigAndAuth();
|
|
401
|
+
if (!provider) {
|
|
402
|
+
throw new Error("当前 config.toml 未找到 model_provider。");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const nextConfigContent = updateProviderBaseUrl(configContent, provider, target.url);
|
|
406
|
+
const nextAuthConfig = {
|
|
407
|
+
...authConfig,
|
|
408
|
+
OPENAI_API_KEY: target.key,
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const backupPaths = await backupCurrentState();
|
|
412
|
+
await writeTextFile(mainConfigPath, nextConfigContent);
|
|
413
|
+
await writeJsonFile(authConfigPath, nextAuthConfig);
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
...backupPaths,
|
|
417
|
+
...(await getCurrentEndpointSummary()),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getManagedPaths() {
|
|
422
|
+
return {
|
|
423
|
+
codexRoot,
|
|
424
|
+
backupDir,
|
|
425
|
+
mainConfigPath,
|
|
426
|
+
authConfigPath,
|
|
427
|
+
endpointStorePath,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = {
|
|
432
|
+
createEndpoint,
|
|
433
|
+
deleteEndpoint,
|
|
434
|
+
getCurrentEndpointSummary,
|
|
435
|
+
getManagedPaths,
|
|
436
|
+
listEndpoints,
|
|
437
|
+
switchEndpoint,
|
|
438
|
+
updateEndpoint,
|
|
439
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
http-equiv="Content-Security-Policy"
|
|
7
|
+
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';"
|
|
8
|
+
/>
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
|
+
<title>Codex 连接切换器</title>
|
|
11
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div class="page-shell">
|
|
15
|
+
<header class="hero-card">
|
|
16
|
+
<div class="hero-copy">
|
|
17
|
+
<p class="eyebrow">CODEX ENDPOINT SWITCHER</p>
|
|
18
|
+
<h1>只切换 URL 和 Key</h1>
|
|
19
|
+
<p class="hero-text">
|
|
20
|
+
你只需要维护 <code>备注</code>、<code>URL</code>、<code>Key</code>。
|
|
21
|
+
切换时应用只会更新 <code>config.toml</code> 的 <code>base_url</code> 和
|
|
22
|
+
<code>auth.json</code> 的 <code>OPENAI_API_KEY</code>,其它配置保持不动。
|
|
23
|
+
</p>
|
|
24
|
+
<div class="hero-actions">
|
|
25
|
+
<button id="refreshButton" class="ghost-button">刷新状态</button>
|
|
26
|
+
<button id="openCodexRootButton" class="ghost-button">打开 Codex 目录</button>
|
|
27
|
+
<button id="openEndpointStoreButton" class="ghost-button">打开连接数据文件</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="hero-side">
|
|
31
|
+
<div class="orb orb-a"></div>
|
|
32
|
+
<div class="orb orb-b"></div>
|
|
33
|
+
<div class="hero-badge" id="heroBadge">读取中</div>
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<main class="content-grid">
|
|
38
|
+
<section class="panel current-panel">
|
|
39
|
+
<div class="panel-header">
|
|
40
|
+
<div>
|
|
41
|
+
<p class="panel-kicker">当前状态</p>
|
|
42
|
+
<h2>当前生效连接</h2>
|
|
43
|
+
</div>
|
|
44
|
+
<span class="panel-tag" id="activeEndpointTag">未识别</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="current-grid">
|
|
47
|
+
<article class="metric-card">
|
|
48
|
+
<span class="metric-label">当前备注</span>
|
|
49
|
+
<strong id="currentNote">-</strong>
|
|
50
|
+
</article>
|
|
51
|
+
<article class="metric-card">
|
|
52
|
+
<span class="metric-label">提供商</span>
|
|
53
|
+
<strong id="currentProvider">-</strong>
|
|
54
|
+
</article>
|
|
55
|
+
<article class="metric-card metric-card-wide">
|
|
56
|
+
<span class="metric-label">当前 URL</span>
|
|
57
|
+
<strong id="currentBaseUrl">-</strong>
|
|
58
|
+
</article>
|
|
59
|
+
<article class="metric-card">
|
|
60
|
+
<span class="metric-label">当前 Key</span>
|
|
61
|
+
<strong id="currentKeyMasked">-</strong>
|
|
62
|
+
</article>
|
|
63
|
+
<article class="metric-card">
|
|
64
|
+
<span class="metric-label">模型</span>
|
|
65
|
+
<strong id="currentModel">-</strong>
|
|
66
|
+
</article>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section class="panel action-panel">
|
|
71
|
+
<div class="panel-header">
|
|
72
|
+
<div>
|
|
73
|
+
<p class="panel-kicker">编辑区</p>
|
|
74
|
+
<h2>新增或编辑连接</h2>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<form id="endpointForm" class="stack-form">
|
|
79
|
+
<input id="endpointId" type="hidden" />
|
|
80
|
+
<label class="field">
|
|
81
|
+
<span>备注</span>
|
|
82
|
+
<input
|
|
83
|
+
id="endpointNote"
|
|
84
|
+
type="text"
|
|
85
|
+
placeholder="例如:公司 / 家里 / 备用"
|
|
86
|
+
autocomplete="off"
|
|
87
|
+
/>
|
|
88
|
+
</label>
|
|
89
|
+
<label class="field">
|
|
90
|
+
<span>URL</span>
|
|
91
|
+
<input
|
|
92
|
+
id="endpointUrl"
|
|
93
|
+
type="text"
|
|
94
|
+
placeholder="例如:https://sub.jlypx.de/"
|
|
95
|
+
autocomplete="off"
|
|
96
|
+
/>
|
|
97
|
+
</label>
|
|
98
|
+
<label class="field">
|
|
99
|
+
<span>Key</span>
|
|
100
|
+
<input
|
|
101
|
+
id="endpointKey"
|
|
102
|
+
type="password"
|
|
103
|
+
placeholder="输入 OPENAI_API_KEY"
|
|
104
|
+
autocomplete="off"
|
|
105
|
+
/>
|
|
106
|
+
</label>
|
|
107
|
+
<p class="field-hint">连接数据仅保存在本机 `~/.codex/endpoint-presets.json`。</p>
|
|
108
|
+
<div class="action-row">
|
|
109
|
+
<button id="saveEndpointButton" type="submit" class="primary-button">新增连接</button>
|
|
110
|
+
<button id="clearFormButton" type="button" class="ghost-button">清空表单</button>
|
|
111
|
+
</div>
|
|
112
|
+
</form>
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
<section class="panel profiles-panel">
|
|
116
|
+
<div class="panel-header">
|
|
117
|
+
<div>
|
|
118
|
+
<p class="panel-kicker">Endpoints</p>
|
|
119
|
+
<h2>连接列表</h2>
|
|
120
|
+
</div>
|
|
121
|
+
<span class="profile-count" id="endpointCount">0 条</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div id="endpointsList" class="profiles-list"></div>
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<section class="panel footer-panel">
|
|
127
|
+
<div class="panel-header">
|
|
128
|
+
<div>
|
|
129
|
+
<p class="panel-kicker">输出</p>
|
|
130
|
+
<h2>执行结果</h2>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div id="statusBox" class="status-box info">应用已启动,正在读取当前连接。</div>
|
|
134
|
+
</section>
|
|
135
|
+
</main>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<script src="./renderer.js"></script>
|
|
139
|
+
</body>
|
|
140
|
+
</html>
|