codex-endpoint-switcher 1.0.0 → 1.0.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/README.md +39 -0
- package/package.json +5 -1
- package/src/main/main.js +4 -0
- package/src/main/preload.js +3 -0
- package/src/main/profile-manager.js +230 -42
- package/src/renderer/index.html +12 -0
- package/src/renderer/renderer.js +43 -5
- package/src/renderer/styles.css +4 -0
- package/src/web/launcher.js +6 -1
- package/src/web/proxy-server.js +69 -0
- package/src/web/server.js +43 -2
package/README.md
CHANGED
|
@@ -68,3 +68,42 @@ codex-switcher remove-access
|
|
|
68
68
|
```text
|
|
69
69
|
http://localhost:3186
|
|
70
70
|
```
|
|
71
|
+
|
|
72
|
+
## 更新 npm 包
|
|
73
|
+
|
|
74
|
+
先进入项目目录:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd C:\Users\33825\Desktop\codex-profile-desktop
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
发布前先检查这次会打进 npm 包里的文件:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm run release:check
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
如果只是小修复,直接发补丁版本:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm run release:patch
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
如果是新增功能但兼容旧版:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run release:minor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
如果有破坏性改动:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run release:major
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
说明:
|
|
105
|
+
|
|
106
|
+
- `release:patch` 会自动把版本从 `1.0.0` 升到 `1.0.1` 这一类补丁版本,然后执行 `npm publish`
|
|
107
|
+
- `release:minor` 会自动把版本从 `1.0.0` 升到 `1.1.0` 这一类功能版本,然后执行 `npm publish`
|
|
108
|
+
- `release:major` 会自动把版本从 `1.0.0` 升到 `2.0.0` 这一类大版本,然后执行 `npm publish`
|
|
109
|
+
- 如果 npm 开启了 2FA,发布时仍然会要求输入验证码
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-endpoint-switcher",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "用于切换 Codex URL 和 Key 的本地网页控制台与 npm CLI",
|
|
5
5
|
"main": "src/main/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"dev:web": "node src/web/server.js",
|
|
24
24
|
"cli": "node bin/codex-switcher.js",
|
|
25
25
|
"pack:npm": "npm pack",
|
|
26
|
+
"release:check": "npm pack --dry-run",
|
|
27
|
+
"release:patch": "npm version patch --no-git-tag-version && npm publish",
|
|
28
|
+
"release:minor": "npm version minor --no-git-tag-version && npm publish",
|
|
29
|
+
"release:major": "npm version major --no-git-tag-version && npm publish",
|
|
26
30
|
"build:portable": "electron-builder --win portable",
|
|
27
31
|
"build:installer": "electron-builder --win nsis"
|
|
28
32
|
},
|
package/src/main/main.js
CHANGED
|
@@ -46,6 +46,10 @@ function registerHandlers() {
|
|
|
46
46
|
return profileManager.switchEndpoint(payload.id);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
ipcMain.handle("proxy:enable", async () => {
|
|
50
|
+
return profileManager.enableProxyMode();
|
|
51
|
+
});
|
|
52
|
+
|
|
49
53
|
ipcMain.handle("paths:get", async () => {
|
|
50
54
|
return profileManager.getManagedPaths();
|
|
51
55
|
});
|
package/src/main/preload.js
CHANGED
|
@@ -19,6 +19,9 @@ contextBridge.exposeInMainWorld("codexDesktop", {
|
|
|
19
19
|
switchEndpoint(payload) {
|
|
20
20
|
return ipcRenderer.invoke("endpoints:switch", payload);
|
|
21
21
|
},
|
|
22
|
+
enableProxyMode() {
|
|
23
|
+
return ipcRenderer.invoke("proxy:enable");
|
|
24
|
+
},
|
|
22
25
|
getPaths() {
|
|
23
26
|
return ipcRenderer.invoke("paths:get");
|
|
24
27
|
},
|
|
@@ -8,6 +8,9 @@ const backupDir = path.join(codexRoot, "backup");
|
|
|
8
8
|
const mainConfigPath = path.join(codexRoot, "config.toml");
|
|
9
9
|
const authConfigPath = path.join(codexRoot, "auth.json");
|
|
10
10
|
const endpointStorePath = path.join(codexRoot, "endpoint-presets.json");
|
|
11
|
+
const endpointStatePath = path.join(codexRoot, "endpoint-switcher-state.json");
|
|
12
|
+
const proxyPort = 3187;
|
|
13
|
+
const proxyBaseUrl = `http://127.0.0.1:${proxyPort}/`;
|
|
11
14
|
|
|
12
15
|
function escapeRegExp(value) {
|
|
13
16
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -206,20 +209,12 @@ function updateProviderBaseUrl(content, provider, nextUrl) {
|
|
|
206
209
|
return output.join(newline);
|
|
207
210
|
}
|
|
208
211
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
};
|
|
212
|
+
function isProxyBaseUrl(url) {
|
|
213
|
+
const normalized = String(url || "").replace(/\/+$/, "");
|
|
214
|
+
return normalized === proxyBaseUrl.replace(/\/+$/, "");
|
|
220
215
|
}
|
|
221
216
|
|
|
222
|
-
async function
|
|
217
|
+
async function ensureStorage() {
|
|
223
218
|
await ensureFileExists(mainConfigPath, `未找到 Codex 主配置文件:${mainConfigPath}`);
|
|
224
219
|
await ensureDirectory(backupDir);
|
|
225
220
|
|
|
@@ -228,10 +223,19 @@ async function ensureEndpointStore() {
|
|
|
228
223
|
} catch {
|
|
229
224
|
await writeJsonFile(endpointStorePath, []);
|
|
230
225
|
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await fs.access(endpointStatePath);
|
|
229
|
+
} catch {
|
|
230
|
+
await writeJsonFile(endpointStatePath, {
|
|
231
|
+
activeEndpointId: "",
|
|
232
|
+
proxyModeEnabled: false,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
async function readEndpointStore() {
|
|
234
|
-
await
|
|
238
|
+
await ensureStorage();
|
|
235
239
|
const data = await readJsonFile(endpointStorePath, []);
|
|
236
240
|
return Array.isArray(data) ? data : [];
|
|
237
241
|
}
|
|
@@ -240,19 +244,41 @@ async function writeEndpointStore(endpoints) {
|
|
|
240
244
|
await writeJsonFile(endpointStorePath, endpoints);
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
async function readEndpointState() {
|
|
248
|
+
await ensureStorage();
|
|
249
|
+
const state = await readJsonFile(endpointStatePath, {
|
|
250
|
+
activeEndpointId: "",
|
|
251
|
+
proxyModeEnabled: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
activeEndpointId: String(state.activeEndpointId || "").trim(),
|
|
256
|
+
proxyModeEnabled: Boolean(state.proxyModeEnabled),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function writeEndpointState(state) {
|
|
261
|
+
await writeJsonFile(endpointStatePath, {
|
|
262
|
+
activeEndpointId: String(state.activeEndpointId || "").trim(),
|
|
263
|
+
proxyModeEnabled: Boolean(state.proxyModeEnabled),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
243
267
|
async function readCurrentConfigAndAuth() {
|
|
244
|
-
await
|
|
268
|
+
await ensureStorage();
|
|
245
269
|
const configContent = await readTextFile(mainConfigPath);
|
|
246
270
|
const authConfig = await readJsonFile(authConfigPath, {});
|
|
247
271
|
const provider = readTomlScalar(configContent, "model_provider");
|
|
272
|
+
const baseUrl = readProviderBaseUrl(configContent, provider);
|
|
248
273
|
|
|
249
274
|
return {
|
|
250
275
|
configContent,
|
|
251
276
|
authConfig,
|
|
252
277
|
provider,
|
|
253
278
|
model: readTomlScalar(configContent, "model"),
|
|
254
|
-
baseUrl
|
|
279
|
+
baseUrl,
|
|
255
280
|
apiKey: String(authConfig.OPENAI_API_KEY || "").trim(),
|
|
281
|
+
proxyModeEnabled: isProxyBaseUrl(baseUrl),
|
|
256
282
|
};
|
|
257
283
|
}
|
|
258
284
|
|
|
@@ -276,45 +302,78 @@ async function backupCurrentState() {
|
|
|
276
302
|
};
|
|
277
303
|
}
|
|
278
304
|
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
function buildEndpointResponse(endpoint, activeId) {
|
|
306
|
+
return {
|
|
307
|
+
id: endpoint.id,
|
|
308
|
+
note: endpoint.note,
|
|
309
|
+
url: endpoint.url,
|
|
310
|
+
key: endpoint.key,
|
|
311
|
+
maskedKey: maskKey(endpoint.key),
|
|
312
|
+
createdAt: endpoint.createdAt,
|
|
313
|
+
updatedAt: endpoint.updatedAt,
|
|
314
|
+
isActive: endpoint.id === activeId,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function resolveActiveEndpointContext() {
|
|
319
|
+
const [currentState, endpoints, state] = await Promise.all([
|
|
281
320
|
readCurrentConfigAndAuth(),
|
|
282
321
|
readEndpointStore(),
|
|
322
|
+
readEndpointState(),
|
|
283
323
|
]);
|
|
284
324
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
325
|
+
let activeEndpoint = null;
|
|
326
|
+
|
|
327
|
+
if (currentState.proxyModeEnabled && state.activeEndpointId) {
|
|
328
|
+
activeEndpoint = endpoints.find((item) => item.id === state.activeEndpointId) || null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!activeEndpoint) {
|
|
332
|
+
activeEndpoint =
|
|
333
|
+
endpoints.find(
|
|
334
|
+
(item) => item.url === currentState.baseUrl && item.key === currentState.apiKey,
|
|
335
|
+
) || null;
|
|
336
|
+
}
|
|
288
337
|
|
|
289
338
|
return {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
currentNote: activeEndpoint ? activeEndpoint.note : "未命名连接",
|
|
339
|
+
currentState,
|
|
340
|
+
endpoints,
|
|
341
|
+
state,
|
|
342
|
+
activeEndpoint,
|
|
295
343
|
activeEndpointId: activeEndpoint ? activeEndpoint.id : "",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function getCurrentEndpointSummary() {
|
|
348
|
+
const context = await resolveActiveEndpointContext();
|
|
349
|
+
const currentNote = context.activeEndpoint ? context.activeEndpoint.note : "未命名连接";
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
model: context.currentState.model,
|
|
353
|
+
provider: context.currentState.provider,
|
|
354
|
+
currentUrl: context.activeEndpoint ? context.activeEndpoint.url : context.currentState.baseUrl,
|
|
355
|
+
currentKeyMasked: context.activeEndpoint
|
|
356
|
+
? maskKey(context.activeEndpoint.key)
|
|
357
|
+
: maskKey(context.currentState.apiKey),
|
|
358
|
+
currentNote,
|
|
359
|
+
activeEndpointId: context.activeEndpointId,
|
|
296
360
|
endpointStorePath,
|
|
361
|
+
endpointStatePath,
|
|
297
362
|
authConfigPath,
|
|
298
363
|
mainConfigPath,
|
|
299
|
-
totalEndpoints: endpoints.length,
|
|
364
|
+
totalEndpoints: context.endpoints.length,
|
|
365
|
+
proxyModeEnabled: context.currentState.proxyModeEnabled,
|
|
366
|
+
proxyBaseUrl,
|
|
367
|
+
hotReloadReady: context.currentState.proxyModeEnabled,
|
|
300
368
|
};
|
|
301
369
|
}
|
|
302
370
|
|
|
303
371
|
async function listEndpoints() {
|
|
304
|
-
const
|
|
305
|
-
|
|
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
|
|
372
|
+
const context = await resolveActiveEndpointContext();
|
|
373
|
+
return context.endpoints
|
|
315
374
|
.slice()
|
|
316
375
|
.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)))
|
|
317
|
-
.map((endpoint) => buildEndpointResponse(endpoint,
|
|
376
|
+
.map((endpoint) => buildEndpointResponse(endpoint, context.activeEndpointId));
|
|
318
377
|
}
|
|
319
378
|
|
|
320
379
|
async function createEndpoint(payload) {
|
|
@@ -378,6 +437,14 @@ async function deleteEndpoint(id) {
|
|
|
378
437
|
const [removed] = endpoints.splice(index, 1);
|
|
379
438
|
await writeEndpointStore(endpoints);
|
|
380
439
|
|
|
440
|
+
const state = await readEndpointState();
|
|
441
|
+
if (state.activeEndpointId === removed.id) {
|
|
442
|
+
await writeEndpointState({
|
|
443
|
+
...state,
|
|
444
|
+
activeEndpointId: "",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
381
448
|
return {
|
|
382
449
|
id: removed.id,
|
|
383
450
|
note: removed.note,
|
|
@@ -397,24 +464,121 @@ async function switchEndpoint(id) {
|
|
|
397
464
|
throw new Error("未找到要切换的端点。");
|
|
398
465
|
}
|
|
399
466
|
|
|
400
|
-
const
|
|
401
|
-
|
|
467
|
+
const currentState = await readCurrentConfigAndAuth();
|
|
468
|
+
|
|
469
|
+
if (currentState.proxyModeEnabled) {
|
|
470
|
+
await writeEndpointState({
|
|
471
|
+
activeEndpointId: target.id,
|
|
472
|
+
proxyModeEnabled: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
...(await getCurrentEndpointSummary()),
|
|
477
|
+
switchedViaProxy: true,
|
|
478
|
+
nextRequestHotReloaded: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!currentState.provider) {
|
|
402
483
|
throw new Error("当前 config.toml 未找到 model_provider。");
|
|
403
484
|
}
|
|
404
485
|
|
|
405
|
-
const nextConfigContent = updateProviderBaseUrl(
|
|
486
|
+
const nextConfigContent = updateProviderBaseUrl(
|
|
487
|
+
currentState.configContent,
|
|
488
|
+
currentState.provider,
|
|
489
|
+
target.url,
|
|
490
|
+
);
|
|
406
491
|
const nextAuthConfig = {
|
|
407
|
-
...authConfig,
|
|
492
|
+
...currentState.authConfig,
|
|
408
493
|
OPENAI_API_KEY: target.key,
|
|
409
494
|
};
|
|
410
495
|
|
|
411
496
|
const backupPaths = await backupCurrentState();
|
|
412
497
|
await writeTextFile(mainConfigPath, nextConfigContent);
|
|
413
498
|
await writeJsonFile(authConfigPath, nextAuthConfig);
|
|
499
|
+
await writeEndpointState({
|
|
500
|
+
activeEndpointId: target.id,
|
|
501
|
+
proxyModeEnabled: false,
|
|
502
|
+
});
|
|
414
503
|
|
|
415
504
|
return {
|
|
416
505
|
...backupPaths,
|
|
417
506
|
...(await getCurrentEndpointSummary()),
|
|
507
|
+
switchedViaProxy: false,
|
|
508
|
+
nextRequestHotReloaded: false,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function enableProxyMode() {
|
|
513
|
+
const [currentState, endpoints, state] = await Promise.all([
|
|
514
|
+
readCurrentConfigAndAuth(),
|
|
515
|
+
readEndpointStore(),
|
|
516
|
+
readEndpointState(),
|
|
517
|
+
]);
|
|
518
|
+
|
|
519
|
+
if (!currentState.provider) {
|
|
520
|
+
throw new Error("当前 config.toml 未找到 model_provider。");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let workingEndpoints = endpoints.slice();
|
|
524
|
+
let activeEndpoint =
|
|
525
|
+
workingEndpoints.find(
|
|
526
|
+
(item) => item.url === currentState.baseUrl && item.key === currentState.apiKey,
|
|
527
|
+
) || null;
|
|
528
|
+
|
|
529
|
+
if (!activeEndpoint && !currentState.proxyModeEnabled) {
|
|
530
|
+
const now = new Date().toISOString();
|
|
531
|
+
activeEndpoint = {
|
|
532
|
+
id: randomUUID(),
|
|
533
|
+
note: "当前连接",
|
|
534
|
+
url: currentState.baseUrl,
|
|
535
|
+
key: currentState.apiKey,
|
|
536
|
+
createdAt: now,
|
|
537
|
+
updatedAt: now,
|
|
538
|
+
};
|
|
539
|
+
workingEndpoints.push(activeEndpoint);
|
|
540
|
+
await writeEndpointStore(workingEndpoints);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const nextActiveId = activeEndpoint
|
|
544
|
+
? activeEndpoint.id
|
|
545
|
+
: state.activeEndpointId || (workingEndpoints[0] ? workingEndpoints[0].id : "");
|
|
546
|
+
|
|
547
|
+
if (!nextActiveId) {
|
|
548
|
+
throw new Error("请先新增至少一条连接,再开启热更新模式。");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!currentState.proxyModeEnabled) {
|
|
552
|
+
const backupPaths = await backupCurrentState();
|
|
553
|
+
const nextConfigContent = updateProviderBaseUrl(
|
|
554
|
+
currentState.configContent,
|
|
555
|
+
currentState.provider,
|
|
556
|
+
proxyBaseUrl,
|
|
557
|
+
);
|
|
558
|
+
await writeTextFile(mainConfigPath, nextConfigContent);
|
|
559
|
+
await writeEndpointState({
|
|
560
|
+
activeEndpointId: nextActiveId,
|
|
561
|
+
proxyModeEnabled: true,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
...backupPaths,
|
|
566
|
+
...(await getCurrentEndpointSummary()),
|
|
567
|
+
oneTimeRestartRequired: true,
|
|
568
|
+
message:
|
|
569
|
+
"已切到本地代理模式。已经打开着的旧 Codex 会话需要手动重开一次;之后同一会话内即可热更新。",
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
await writeEndpointState({
|
|
574
|
+
activeEndpointId: nextActiveId,
|
|
575
|
+
proxyModeEnabled: true,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
...(await getCurrentEndpointSummary()),
|
|
580
|
+
oneTimeRestartRequired: false,
|
|
581
|
+
message: "热更新模式已经开启。",
|
|
418
582
|
};
|
|
419
583
|
}
|
|
420
584
|
|
|
@@ -425,15 +589,39 @@ function getManagedPaths() {
|
|
|
425
589
|
mainConfigPath,
|
|
426
590
|
authConfigPath,
|
|
427
591
|
endpointStorePath,
|
|
592
|
+
endpointStatePath,
|
|
593
|
+
proxyBaseUrl,
|
|
594
|
+
proxyPort,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function getProxyTarget() {
|
|
599
|
+
const context = await resolveActiveEndpointContext();
|
|
600
|
+
if (!context.activeEndpoint) {
|
|
601
|
+
throw new Error("当前没有激活的连接,无法进行代理转发。");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
id: context.activeEndpoint.id,
|
|
606
|
+
note: context.activeEndpoint.note,
|
|
607
|
+
url: context.activeEndpoint.url,
|
|
608
|
+
key: context.activeEndpoint.key,
|
|
609
|
+
proxyModeEnabled: context.currentState.proxyModeEnabled,
|
|
610
|
+
proxyBaseUrl,
|
|
611
|
+
proxyPort,
|
|
428
612
|
};
|
|
429
613
|
}
|
|
430
614
|
|
|
431
615
|
module.exports = {
|
|
432
616
|
createEndpoint,
|
|
433
617
|
deleteEndpoint,
|
|
618
|
+
enableProxyMode,
|
|
434
619
|
getCurrentEndpointSummary,
|
|
435
620
|
getManagedPaths,
|
|
621
|
+
getProxyTarget,
|
|
436
622
|
listEndpoints,
|
|
623
|
+
proxyBaseUrl,
|
|
624
|
+
proxyPort,
|
|
437
625
|
switchEndpoint,
|
|
438
626
|
updateEndpoint,
|
|
439
627
|
};
|
package/src/renderer/index.html
CHANGED
|
@@ -64,6 +64,18 @@
|
|
|
64
64
|
<span class="metric-label">模型</span>
|
|
65
65
|
<strong id="currentModel">-</strong>
|
|
66
66
|
</article>
|
|
67
|
+
<article class="metric-card metric-card-wide">
|
|
68
|
+
<span class="metric-label">热更新模式</span>
|
|
69
|
+
<strong id="proxyModeStatus">未开启</strong>
|
|
70
|
+
<p class="field-hint" id="proxyModeHint">
|
|
71
|
+
当前还是直连模式。开启后,后续同一 Codex 会话可在下一次请求时热更新到新 URL/Key。
|
|
72
|
+
</p>
|
|
73
|
+
<div class="action-row metric-actions">
|
|
74
|
+
<button id="enableProxyModeButton" type="button" class="secondary-button">
|
|
75
|
+
开启热更新模式
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</article>
|
|
67
79
|
</div>
|
|
68
80
|
</section>
|
|
69
81
|
|
package/src/renderer/renderer.js
CHANGED
|
@@ -51,6 +51,11 @@ function createWebBridge() {
|
|
|
51
51
|
body: JSON.stringify(payload),
|
|
52
52
|
});
|
|
53
53
|
},
|
|
54
|
+
enableProxyMode() {
|
|
55
|
+
return request("/api/proxy/enable", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
});
|
|
58
|
+
},
|
|
54
59
|
getPaths() {
|
|
55
60
|
return request("/api/paths");
|
|
56
61
|
},
|
|
@@ -120,8 +125,18 @@ function renderCurrent() {
|
|
|
120
125
|
$("#currentBaseUrl").textContent = current.currentUrl || "-";
|
|
121
126
|
$("#currentKeyMasked").textContent = current.currentKeyMasked || "-";
|
|
122
127
|
$("#currentModel").textContent = current.model || "-";
|
|
128
|
+
$("#proxyModeStatus").textContent = current.proxyModeEnabled
|
|
129
|
+
? `已开启,固定代理:${current.proxyBaseUrl}`
|
|
130
|
+
: "未开启";
|
|
131
|
+
$("#proxyModeHint").textContent = current.proxyModeEnabled
|
|
132
|
+
? "当前 Codex 只要已经接入本地代理,后续同一会话内切换连接会在下一次请求时热更新。"
|
|
133
|
+
: "当前还是直连模式。开启后需要把已打开的旧 Codex 会话重开一次;之后同一会话即可热更新。";
|
|
134
|
+
$("#enableProxyModeButton").disabled = current.proxyModeEnabled;
|
|
135
|
+
$("#enableProxyModeButton").textContent = current.proxyModeEnabled
|
|
136
|
+
? "热更新模式已开启"
|
|
137
|
+
: "开启热更新模式";
|
|
123
138
|
$("#heroBadge").textContent = current.currentNote
|
|
124
|
-
?
|
|
139
|
+
? `${current.proxyModeEnabled ? "热更新中" : "当前连接"}:${current.currentNote}`
|
|
125
140
|
: "当前未匹配到已保存连接";
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -241,10 +256,14 @@ async function handleSwitchEndpoint(id, note) {
|
|
|
241
256
|
setStatus(`正在切换到:${note} ...`, "info");
|
|
242
257
|
const result = await bridge.switchEndpoint({ id });
|
|
243
258
|
await refreshDashboard(false);
|
|
244
|
-
|
|
245
|
-
`已切换到 ${note}
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
if (result.switchedViaProxy) {
|
|
260
|
+
setStatus(`已切换到 ${note}。当前会话下一次请求会直接走新的 URL/Key。`, "success");
|
|
261
|
+
} else {
|
|
262
|
+
setStatus(
|
|
263
|
+
`已切换到 ${note}。当前还是直连模式,只有新开的 Codex 会话会立即使用新配置。`,
|
|
264
|
+
"success",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
248
267
|
return result;
|
|
249
268
|
} catch (error) {
|
|
250
269
|
setStatus(`切换失败:${error.message}`, "error");
|
|
@@ -252,6 +271,24 @@ async function handleSwitchEndpoint(id, note) {
|
|
|
252
271
|
}
|
|
253
272
|
}
|
|
254
273
|
|
|
274
|
+
async function handleEnableProxyMode() {
|
|
275
|
+
try {
|
|
276
|
+
setStatus("正在切到热更新代理模式 ...", "info");
|
|
277
|
+
const result = await bridge.enableProxyMode();
|
|
278
|
+
await refreshDashboard(false);
|
|
279
|
+
if (result.oneTimeRestartRequired) {
|
|
280
|
+
setStatus(
|
|
281
|
+
"热更新模式已开启。请把当前已经打开着的旧 Codex 会话重开一次;之后同一会话内即可热更新。",
|
|
282
|
+
"success",
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
setStatus("热更新模式已开启。当前会话后续请求可直接热更新到新连接。", "success");
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
setStatus(`开启热更新模式失败:${error.message}`, "error");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
255
292
|
async function handleSubmitEndpoint(event) {
|
|
256
293
|
event.preventDefault();
|
|
257
294
|
const id = $("#endpointId").value.trim();
|
|
@@ -320,6 +357,7 @@ function bindEvents() {
|
|
|
320
357
|
resetForm();
|
|
321
358
|
setStatus("已清空表单。", "info");
|
|
322
359
|
});
|
|
360
|
+
$("#enableProxyModeButton").addEventListener("click", handleEnableProxyMode);
|
|
323
361
|
$("#openCodexRootButton").addEventListener("click", () => {
|
|
324
362
|
if (state.paths?.codexRoot) {
|
|
325
363
|
handleOpenPath(state.paths.codexRoot, " Codex 目录");
|
package/src/renderer/styles.css
CHANGED
package/src/web/launcher.js
CHANGED
|
@@ -7,7 +7,9 @@ const projectRoot = path.resolve(__dirname, "../..");
|
|
|
7
7
|
const runtimeDir = path.join(projectRoot, "runtime");
|
|
8
8
|
const serverEntryPath = path.join(__dirname, "server.js");
|
|
9
9
|
const port = Number(process.env.PORT || 3186);
|
|
10
|
+
const proxyPort = 3187;
|
|
10
11
|
const healthUrl = `http://127.0.0.1:${port}/api/health`;
|
|
12
|
+
const proxyHealthUrl = `http://127.0.0.1:${proxyPort}/__switcher/health`;
|
|
11
13
|
const consoleUrl = `http://localhost:${port}`;
|
|
12
14
|
|
|
13
15
|
function ensureRuntimeDir() {
|
|
@@ -45,7 +47,8 @@ function requestJson(url, timeoutMs = 1200) {
|
|
|
45
47
|
async function checkServerHealth() {
|
|
46
48
|
try {
|
|
47
49
|
const payload = await requestJson(healthUrl);
|
|
48
|
-
|
|
50
|
+
const proxyPayload = await requestJson(proxyHealthUrl);
|
|
51
|
+
return Boolean(payload && payload.ok && proxyPayload && proxyPayload.ok);
|
|
49
52
|
} catch {
|
|
50
53
|
return false;
|
|
51
54
|
}
|
|
@@ -211,6 +214,7 @@ async function handleCommand(command) {
|
|
|
211
214
|
running,
|
|
212
215
|
url: consoleUrl,
|
|
213
216
|
port,
|
|
217
|
+
proxyPort,
|
|
214
218
|
},
|
|
215
219
|
null,
|
|
216
220
|
2,
|
|
@@ -245,4 +249,5 @@ module.exports = {
|
|
|
245
249
|
runtimeDir,
|
|
246
250
|
consoleUrl,
|
|
247
251
|
port,
|
|
252
|
+
proxyPort,
|
|
248
253
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const http = require("node:http");
|
|
2
|
+
const https = require("node:https");
|
|
3
|
+
const profileManager = require("../main/profile-manager");
|
|
4
|
+
|
|
5
|
+
function createProxyServer() {
|
|
6
|
+
return http.createServer(async (req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
if (req.url === "/__switcher/health") {
|
|
9
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
10
|
+
res.end(JSON.stringify({ ok: true, proxyBaseUrl: profileManager.proxyBaseUrl }));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const target = await profileManager.getProxyTarget();
|
|
15
|
+
const upstreamBase = new URL(target.url);
|
|
16
|
+
const requestPath = req.url && req.url.startsWith("/") ? req.url : `/${req.url || ""}`;
|
|
17
|
+
const upstreamUrl = new URL(requestPath, upstreamBase);
|
|
18
|
+
const requestModule = upstreamUrl.protocol === "https:" ? https : http;
|
|
19
|
+
const headers = {
|
|
20
|
+
...req.headers,
|
|
21
|
+
host: upstreamUrl.host,
|
|
22
|
+
authorization: `Bearer ${target.key}`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
delete headers.connection;
|
|
26
|
+
|
|
27
|
+
const upstreamRequest = requestModule.request(
|
|
28
|
+
upstreamUrl,
|
|
29
|
+
{
|
|
30
|
+
method: req.method,
|
|
31
|
+
headers,
|
|
32
|
+
},
|
|
33
|
+
(upstreamResponse) => {
|
|
34
|
+
res.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
|
|
35
|
+
upstreamResponse.pipe(res);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
upstreamRequest.on("error", (error) => {
|
|
40
|
+
if (!res.headersSent) {
|
|
41
|
+
res.writeHead(502, { "Content-Type": "application/json; charset=utf-8" });
|
|
42
|
+
}
|
|
43
|
+
res.end(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
ok: false,
|
|
46
|
+
error: `代理转发失败:${error.message}`,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
req.pipe(upstreamRequest);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (!res.headersSent) {
|
|
54
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
res.end(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
ok: false,
|
|
60
|
+
error: error instanceof Error ? error.message : String(error),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
createProxyServer,
|
|
69
|
+
};
|
package/src/web/server.js
CHANGED
|
@@ -2,6 +2,7 @@ const path = require("node:path");
|
|
|
2
2
|
const { exec } = require("node:child_process");
|
|
3
3
|
const express = require("express");
|
|
4
4
|
const profileManager = require("../main/profile-manager");
|
|
5
|
+
const { createProxyServer } = require("./proxy-server");
|
|
5
6
|
|
|
6
7
|
function wrapAsync(handler) {
|
|
7
8
|
return async (req, res) => {
|
|
@@ -77,6 +78,13 @@ function createApp() {
|
|
|
77
78
|
}),
|
|
78
79
|
);
|
|
79
80
|
|
|
81
|
+
app.post(
|
|
82
|
+
"/api/proxy/enable",
|
|
83
|
+
wrapAsync(async () => {
|
|
84
|
+
return profileManager.enableProxyMode();
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
80
88
|
app.post(
|
|
81
89
|
"/api/open-path",
|
|
82
90
|
wrapAsync(async (req) => {
|
|
@@ -110,16 +118,49 @@ function createApp() {
|
|
|
110
118
|
function startServer(options = {}) {
|
|
111
119
|
const port = Number(options.port || process.env.PORT || 3186);
|
|
112
120
|
const app = createApp();
|
|
113
|
-
const
|
|
121
|
+
const proxyServer = createProxyServer();
|
|
122
|
+
const proxyPort = profileManager.proxyPort;
|
|
123
|
+
const webServer = app.listen(port, () => {
|
|
114
124
|
const url = `http://localhost:${port}`;
|
|
115
125
|
console.log(`Codex 网页控制台已启动:${url}`);
|
|
126
|
+
console.log(`Codex 热更新代理已启动:${profileManager.proxyBaseUrl}`);
|
|
116
127
|
|
|
117
128
|
if (options.autoOpen !== false && process.env.AUTO_OPEN_BROWSER !== "false") {
|
|
118
129
|
exec(`cmd /c start "" "${url}"`);
|
|
119
130
|
}
|
|
120
131
|
});
|
|
121
132
|
|
|
122
|
-
|
|
133
|
+
proxyServer.listen(proxyPort);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
webServer,
|
|
137
|
+
proxyServer,
|
|
138
|
+
close(callback) {
|
|
139
|
+
let pending = 2;
|
|
140
|
+
let closed = false;
|
|
141
|
+
|
|
142
|
+
function done(error) {
|
|
143
|
+
if (closed) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (error) {
|
|
148
|
+
closed = true;
|
|
149
|
+
callback(error);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pending -= 1;
|
|
154
|
+
if (pending === 0) {
|
|
155
|
+
closed = true;
|
|
156
|
+
callback();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
webServer.close((error) => done(error));
|
|
161
|
+
proxyServer.close((error) => done(error));
|
|
162
|
+
},
|
|
163
|
+
};
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
if (require.main === module) {
|