codex-endpoint-switcher 1.5.0 → 1.6.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 +6 -0
- package/package.json +1 -1
- package/src/main/auto-update-runner.js +242 -0
- package/src/main/main.js +4 -0
- package/src/main/preload.js +3 -0
- package/src/main/profile-manager.js +22 -2
- package/src/main/update-service.js +84 -30
- package/src/renderer/index.html +2 -0
- package/src/renderer/renderer.js +50 -6
- package/src/web/server.js +18 -2
package/README.md
CHANGED
|
@@ -77,6 +77,11 @@ codex-switcher remove-access
|
|
|
77
77
|
http://localhost:3186
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
说明:
|
|
81
|
+
|
|
82
|
+
- 本地控制台和热更新代理默认只监听 `127.0.0.1`
|
|
83
|
+
- 不再暴露到局域网,避免其他设备直接访问你的本地 Key 管理接口
|
|
84
|
+
|
|
80
85
|
## 账号配置同步
|
|
81
86
|
|
|
82
87
|
网页控制台现在支持“账号 + 密码”的同步方式。
|
|
@@ -144,6 +149,7 @@ codex-switcher sync-server
|
|
|
144
149
|
- 如果你发布了新版本,其他用户打开控制台后会看到“发现新版本”的提示
|
|
145
150
|
- 用户可以直接点 `立即更新`,程序会自动关闭、更新并重新打开
|
|
146
151
|
- 也可以继续使用“复制升级命令”手动更新
|
|
152
|
+
- 上一次自动更新的成功或失败结果会记录到本地,下次打开控制台会显示结果
|
|
147
153
|
|
|
148
154
|
也可以用 CLI 手动检查:
|
|
149
155
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const { spawn } = require("node:child_process");
|
|
3
|
+
|
|
4
|
+
const WAIT_PARENT_EXIT_TIMEOUT_MS = 20000;
|
|
5
|
+
const WAIT_PARENT_POLL_MS = 400;
|
|
6
|
+
|
|
7
|
+
function sleep(timeoutMs) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
setTimeout(resolve, timeoutMs);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function appendLog(logPath, text) {
|
|
14
|
+
await fs.appendFile(logPath, text, "utf8");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function writeState(statePath, payload) {
|
|
18
|
+
await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runProcess(command, args, options = {}) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const child = spawn(command, args, {
|
|
24
|
+
cwd: options.cwd,
|
|
25
|
+
env: options.env,
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
windowsHide: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let stdout = "";
|
|
31
|
+
let stderr = "";
|
|
32
|
+
|
|
33
|
+
child.stdout.on("data", (chunk) => {
|
|
34
|
+
stdout += chunk.toString("utf8");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
child.stderr.on("data", (chunk) => {
|
|
38
|
+
stderr += chunk.toString("utf8");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
child.on("exit", (code) => {
|
|
42
|
+
resolve({
|
|
43
|
+
code: Number(code || 0),
|
|
44
|
+
stdout,
|
|
45
|
+
stderr,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
child.on("error", (error) => {
|
|
50
|
+
resolve({
|
|
51
|
+
code: 1,
|
|
52
|
+
stdout,
|
|
53
|
+
stderr: `${stderr}${error instanceof Error ? error.message : String(error)}\n`,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isProcessAlive(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function forceTerminateProcess(pid) {
|
|
69
|
+
if (!pid || pid <= 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
await runProcess("cmd.exe", ["/d", "/s", "/c", `taskkill /PID ${pid} /F`]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, "SIGTERM");
|
|
80
|
+
} catch {
|
|
81
|
+
// 进程已结束时忽略。
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function waitForProcessExit(pid, logPath) {
|
|
86
|
+
if (!pid || pid <= 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const startedAt = Date.now();
|
|
91
|
+
while (Date.now() - startedAt < WAIT_PARENT_EXIT_TIMEOUT_MS) {
|
|
92
|
+
if (!isProcessAlive(pid)) {
|
|
93
|
+
await appendLog(logPath, `[${new Date().toISOString()}] 旧进程已退出:${pid}\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await sleep(WAIT_PARENT_POLL_MS);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await appendLog(
|
|
101
|
+
logPath,
|
|
102
|
+
`[${new Date().toISOString()}] 旧进程超时未退出,开始强制结束:${pid}\n`,
|
|
103
|
+
);
|
|
104
|
+
await forceTerminateProcess(pid);
|
|
105
|
+
await sleep(1200);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function reopenConsole(payload) {
|
|
109
|
+
return runProcess(
|
|
110
|
+
payload.nodePath,
|
|
111
|
+
[payload.reopenBinPath, "open"],
|
|
112
|
+
{
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
env: process.env,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const encodedPayload = String(process.argv[2] || "").trim();
|
|
121
|
+
if (!encodedPayload) {
|
|
122
|
+
throw new Error("缺少自动更新参数。");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
|
|
126
|
+
const startedAt = new Date().toISOString();
|
|
127
|
+
|
|
128
|
+
await appendLog(
|
|
129
|
+
payload.logPath,
|
|
130
|
+
`[${startedAt}] 自动更新开始,当前版本 ${payload.currentVersion}\n`,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await writeState(payload.statePath, {
|
|
134
|
+
status: "running",
|
|
135
|
+
packageName: payload.packageName,
|
|
136
|
+
requestedVersion: "latest",
|
|
137
|
+
currentVersion: payload.currentVersion,
|
|
138
|
+
startedAt,
|
|
139
|
+
finishedAt: "",
|
|
140
|
+
logPath: payload.logPath,
|
|
141
|
+
reopen: payload.reopen,
|
|
142
|
+
lastError: "",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await sleep(Number(payload.waitSeconds || 3) * 1000);
|
|
146
|
+
await waitForProcessExit(Number(payload.parentPid || 0), payload.logPath);
|
|
147
|
+
|
|
148
|
+
const installResult = await runProcess(
|
|
149
|
+
payload.nodePath,
|
|
150
|
+
[payload.npmCliPath, "install", "-g", `${payload.packageName}@latest`],
|
|
151
|
+
{
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
env: process.env,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (installResult.stdout) {
|
|
158
|
+
await appendLog(payload.logPath, installResult.stdout);
|
|
159
|
+
}
|
|
160
|
+
if (installResult.stderr) {
|
|
161
|
+
await appendLog(payload.logPath, installResult.stderr);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (installResult.code !== 0) {
|
|
165
|
+
const finishedAt = new Date().toISOString();
|
|
166
|
+
const errorMessage = `npm 安装失败,退出码:${installResult.code}`;
|
|
167
|
+
await appendLog(payload.logPath, `[${finishedAt}] ${errorMessage}\n`);
|
|
168
|
+
await writeState(payload.statePath, {
|
|
169
|
+
status: "failed",
|
|
170
|
+
packageName: payload.packageName,
|
|
171
|
+
requestedVersion: "latest",
|
|
172
|
+
currentVersion: payload.currentVersion,
|
|
173
|
+
startedAt,
|
|
174
|
+
finishedAt,
|
|
175
|
+
logPath: payload.logPath,
|
|
176
|
+
reopen: payload.reopen,
|
|
177
|
+
lastError: errorMessage,
|
|
178
|
+
});
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let reopenResult = { code: 0, stdout: "", stderr: "" };
|
|
183
|
+
if (payload.reopen) {
|
|
184
|
+
reopenResult = await reopenConsole(payload);
|
|
185
|
+
if (reopenResult.stdout) {
|
|
186
|
+
await appendLog(payload.logPath, reopenResult.stdout);
|
|
187
|
+
}
|
|
188
|
+
if (reopenResult.stderr) {
|
|
189
|
+
await appendLog(payload.logPath, reopenResult.stderr);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const finishedAt = new Date().toISOString();
|
|
194
|
+
const finalStatus = reopenResult.code === 0 ? "succeeded" : "failed";
|
|
195
|
+
const lastError =
|
|
196
|
+
reopenResult.code === 0 ? "" : `更新已完成,但重新打开控制台失败,退出码:${reopenResult.code}`;
|
|
197
|
+
|
|
198
|
+
await appendLog(
|
|
199
|
+
payload.logPath,
|
|
200
|
+
`[${finishedAt}] ${finalStatus === "succeeded" ? "自动更新完成" : lastError}\n`,
|
|
201
|
+
);
|
|
202
|
+
await writeState(payload.statePath, {
|
|
203
|
+
status: finalStatus,
|
|
204
|
+
packageName: payload.packageName,
|
|
205
|
+
requestedVersion: "latest",
|
|
206
|
+
currentVersion: payload.currentVersion,
|
|
207
|
+
startedAt,
|
|
208
|
+
finishedAt,
|
|
209
|
+
logPath: payload.logPath,
|
|
210
|
+
reopen: payload.reopen,
|
|
211
|
+
lastError,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (finalStatus !== "succeeded") {
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main().catch(async (error) => {
|
|
220
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
221
|
+
if (process.argv[2]) {
|
|
222
|
+
try {
|
|
223
|
+
const payload = JSON.parse(Buffer.from(process.argv[2], "base64url").toString("utf8"));
|
|
224
|
+
const finishedAt = new Date().toISOString();
|
|
225
|
+
await appendLog(payload.logPath, `[${finishedAt}] 自动更新异常:${message}\n`);
|
|
226
|
+
await writeState(payload.statePath, {
|
|
227
|
+
status: "failed",
|
|
228
|
+
packageName: payload.packageName,
|
|
229
|
+
requestedVersion: "latest",
|
|
230
|
+
currentVersion: payload.currentVersion,
|
|
231
|
+
startedAt: finishedAt,
|
|
232
|
+
finishedAt,
|
|
233
|
+
logPath: payload.logPath,
|
|
234
|
+
reopen: payload.reopen,
|
|
235
|
+
lastError: message,
|
|
236
|
+
});
|
|
237
|
+
} catch {
|
|
238
|
+
// 参数损坏时无法再写状态,直接退出。
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
process.exit(1);
|
|
242
|
+
});
|
package/src/main/main.js
CHANGED
|
@@ -28,6 +28,10 @@ function registerHandlers() {
|
|
|
28
28
|
return profileManager.listEndpoints();
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
ipcMain.handle("endpoints:get", async (_event, payload) => {
|
|
32
|
+
return profileManager.getEndpointById(payload.id);
|
|
33
|
+
});
|
|
34
|
+
|
|
31
35
|
ipcMain.handle("endpoints:current", async () => {
|
|
32
36
|
return profileManager.getCurrentEndpointSummary();
|
|
33
37
|
});
|
package/src/main/preload.js
CHANGED
|
@@ -4,6 +4,9 @@ contextBridge.exposeInMainWorld("codexDesktop", {
|
|
|
4
4
|
listEndpoints() {
|
|
5
5
|
return ipcRenderer.invoke("endpoints:list");
|
|
6
6
|
},
|
|
7
|
+
getEndpoint(payload) {
|
|
8
|
+
return ipcRenderer.invoke("endpoints:get", payload);
|
|
9
|
+
},
|
|
7
10
|
getCurrentConfig() {
|
|
8
11
|
return ipcRenderer.invoke("endpoints:current");
|
|
9
12
|
},
|
|
@@ -440,12 +440,14 @@ async function backupEndpointData() {
|
|
|
440
440
|
};
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
function buildEndpointResponse(endpoint, activeId) {
|
|
443
|
+
function buildEndpointResponse(endpoint, activeId, options = {}) {
|
|
444
|
+
const includeSensitive = Boolean(options.includeSensitive);
|
|
445
|
+
|
|
444
446
|
return {
|
|
445
447
|
id: endpoint.id,
|
|
446
448
|
note: endpoint.note,
|
|
447
449
|
url: endpoint.url,
|
|
448
|
-
key: endpoint.key,
|
|
450
|
+
...(includeSensitive ? { key: endpoint.key } : {}),
|
|
449
451
|
maskedKey: maskKey(endpoint.key),
|
|
450
452
|
createdAt: endpoint.createdAt,
|
|
451
453
|
updatedAt: endpoint.updatedAt,
|
|
@@ -514,6 +516,23 @@ async function listEndpoints() {
|
|
|
514
516
|
.map((endpoint) => buildEndpointResponse(endpoint, context.activeEndpointId));
|
|
515
517
|
}
|
|
516
518
|
|
|
519
|
+
async function getEndpointById(id) {
|
|
520
|
+
const safeId = String(id || "").trim();
|
|
521
|
+
if (!safeId) {
|
|
522
|
+
throw new Error("缺少要读取的端点 ID。");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const context = await resolveActiveEndpointContext();
|
|
526
|
+
const endpoint = context.endpoints.find((item) => item.id === safeId);
|
|
527
|
+
if (!endpoint) {
|
|
528
|
+
throw new Error("未找到要读取的端点。");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return buildEndpointResponse(endpoint, context.activeEndpointId, {
|
|
532
|
+
includeSensitive: true,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
517
536
|
async function createEndpoint(payload) {
|
|
518
537
|
const endpoint = validateEndpointPayload(payload);
|
|
519
538
|
const endpoints = await readEndpointStore();
|
|
@@ -863,6 +882,7 @@ module.exports = {
|
|
|
863
882
|
deleteEndpoint,
|
|
864
883
|
enableProxyMode,
|
|
865
884
|
exportSyncPackage,
|
|
885
|
+
getEndpointById,
|
|
866
886
|
getCurrentEndpointSummary,
|
|
867
887
|
getManagedPaths,
|
|
868
888
|
getProxyTarget,
|
|
@@ -4,6 +4,8 @@ const fs = require("node:fs");
|
|
|
4
4
|
const { spawn } = require("node:child_process");
|
|
5
5
|
const packageInfo = require("../../package.json");
|
|
6
6
|
|
|
7
|
+
const packageRoot = path.resolve(__dirname, "../..");
|
|
8
|
+
const runnerEntryPath = path.join(__dirname, "auto-update-runner.js");
|
|
7
9
|
const REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
8
10
|
const SUCCESS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
9
11
|
const ERROR_CACHE_TTL_MS = 30 * 1000;
|
|
@@ -18,6 +20,57 @@ function getAutoUpdateLogPath() {
|
|
|
18
20
|
return path.join(logDir, "codex-switcher-auto-update.log");
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function getAutoUpdateStatePath() {
|
|
24
|
+
const stateDir = path.join(os.homedir(), ".codex");
|
|
25
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
26
|
+
return path.join(stateDir, "codex-switcher-auto-update.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readAutoUpdateState() {
|
|
30
|
+
try {
|
|
31
|
+
const content = fs.readFileSync(getAutoUpdateStatePath(), "utf8");
|
|
32
|
+
const parsed = JSON.parse(content);
|
|
33
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeAutoUpdateState(payload) {
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
getAutoUpdateStatePath(),
|
|
42
|
+
`${JSON.stringify(payload, null, 2)}\n`,
|
|
43
|
+
"utf8",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveRuntimePaths() {
|
|
48
|
+
const nodePath = process.execPath;
|
|
49
|
+
const nodeDir = path.dirname(nodePath);
|
|
50
|
+
const globalRootCandidates =
|
|
51
|
+
process.platform === "win32"
|
|
52
|
+
? [path.join(nodeDir, "node_modules")]
|
|
53
|
+
: [path.join(nodeDir, "../lib/node_modules"), path.join(nodeDir, "node_modules")];
|
|
54
|
+
|
|
55
|
+
const npmCliPath = globalRootCandidates
|
|
56
|
+
.map((basePath) => path.resolve(basePath, "npm/bin/npm-cli.js"))
|
|
57
|
+
.find((targetPath) => fs.existsSync(targetPath));
|
|
58
|
+
|
|
59
|
+
if (!npmCliPath) {
|
|
60
|
+
throw new Error("未找到当前运行时对应的 npm-cli.js,无法自动更新。");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const installedPackageBinPath = globalRootCandidates
|
|
64
|
+
.map((basePath) => path.resolve(basePath, `${packageInfo.name}/bin/codex-switcher.js`))
|
|
65
|
+
.find((targetPath) => fs.existsSync(targetPath));
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
nodePath,
|
|
69
|
+
npmCliPath,
|
|
70
|
+
reopenBinPath: installedPackageBinPath || path.join(packageRoot, "bin/codex-switcher.js"),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
function sleep(timeoutMs) {
|
|
22
75
|
return new Promise((resolve) => {
|
|
23
76
|
setTimeout(resolve, timeoutMs);
|
|
@@ -56,6 +109,7 @@ function buildStatus(payload = {}) {
|
|
|
56
109
|
const currentVersion = packageInfo.version;
|
|
57
110
|
const latestVersion = payload.latestVersion || currentVersion;
|
|
58
111
|
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
|
112
|
+
const autoUpdateState = payload.autoUpdateState || readAutoUpdateState();
|
|
59
113
|
|
|
60
114
|
return {
|
|
61
115
|
packageName: packageInfo.name,
|
|
@@ -66,6 +120,7 @@ function buildStatus(payload = {}) {
|
|
|
66
120
|
upgradeCommand: `npm install -g ${packageInfo.name}@latest`,
|
|
67
121
|
releaseUrl: `https://www.npmjs.com/package/${packageInfo.name}`,
|
|
68
122
|
lastError: payload.lastError || "",
|
|
123
|
+
autoUpdateState,
|
|
69
124
|
};
|
|
70
125
|
}
|
|
71
126
|
|
|
@@ -131,44 +186,42 @@ async function getUpdateStatus(options = {}) {
|
|
|
131
186
|
function scheduleAutoUpdate(options = {}) {
|
|
132
187
|
const reopen = options.reopen !== false;
|
|
133
188
|
const logPath = getAutoUpdateLogPath();
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (process.platform === "win32") {
|
|
138
|
-
const command =
|
|
139
|
-
`echo [%date% %time%] 开始自动更新 ${packageInfo.name} > "${logPath}" ` +
|
|
140
|
-
`&& timeout /t ${AUTO_UPDATE_WAIT_SECONDS} /nobreak >nul ` +
|
|
141
|
-
`&& ${installCommand} >> "${logPath}" 2>&1` +
|
|
142
|
-
`${restartCommand ? `${restartCommand} >> "${logPath}" 2>&1` : ""}`;
|
|
143
|
-
|
|
144
|
-
const child = spawn("cmd.exe", ["/d", "/s", "/c", command], {
|
|
145
|
-
cwd: os.homedir(),
|
|
146
|
-
detached: true,
|
|
147
|
-
stdio: "ignore",
|
|
148
|
-
windowsHide: true,
|
|
149
|
-
env: process.env,
|
|
150
|
-
});
|
|
189
|
+
const statePath = getAutoUpdateStatePath();
|
|
190
|
+
const runtimePaths = resolveRuntimePaths();
|
|
151
191
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
192
|
+
writeAutoUpdateState({
|
|
193
|
+
status: "scheduled",
|
|
194
|
+
packageName: packageInfo.name,
|
|
195
|
+
requestedVersion: "latest",
|
|
196
|
+
currentVersion: packageInfo.version,
|
|
197
|
+
startedAt: new Date().toISOString(),
|
|
198
|
+
finishedAt: "",
|
|
199
|
+
logPath,
|
|
200
|
+
reopen,
|
|
201
|
+
lastError: "",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const payload = Buffer.from(
|
|
205
|
+
JSON.stringify({
|
|
155
206
|
packageName: packageInfo.name,
|
|
156
207
|
currentVersion: packageInfo.version,
|
|
208
|
+
parentPid: process.pid,
|
|
209
|
+
waitSeconds: AUTO_UPDATE_WAIT_SECONDS,
|
|
157
210
|
logPath,
|
|
211
|
+
statePath,
|
|
158
212
|
reopen,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const child = spawn("sh", ["-lc", command], {
|
|
213
|
+
nodePath: runtimePaths.nodePath,
|
|
214
|
+
npmCliPath: runtimePaths.npmCliPath,
|
|
215
|
+
reopenBinPath: runtimePaths.reopenBinPath,
|
|
216
|
+
}),
|
|
217
|
+
"utf8",
|
|
218
|
+
).toString("base64url");
|
|
219
|
+
|
|
220
|
+
const child = spawn(process.execPath, [runnerEntryPath, payload], {
|
|
169
221
|
cwd: os.homedir(),
|
|
170
222
|
detached: true,
|
|
171
223
|
stdio: "ignore",
|
|
224
|
+
windowsHide: true,
|
|
172
225
|
env: process.env,
|
|
173
226
|
});
|
|
174
227
|
|
|
@@ -178,6 +231,7 @@ function scheduleAutoUpdate(options = {}) {
|
|
|
178
231
|
packageName: packageInfo.name,
|
|
179
232
|
currentVersion: packageInfo.version,
|
|
180
233
|
logPath,
|
|
234
|
+
statePath,
|
|
181
235
|
reopen,
|
|
182
236
|
};
|
|
183
237
|
}
|
package/src/renderer/index.html
CHANGED
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
<button id="openEndpointStoreButton" class="ghost-button">打开连接数据文件</button>
|
|
110
110
|
<button id="closeAppButton" class="danger-button">关闭程序</button>
|
|
111
111
|
</div>
|
|
112
|
+
<div id="appStatusBox" class="status-box info">控制台已就绪。</div>
|
|
112
113
|
<section class="update-strip" aria-label="版本更新状态">
|
|
113
114
|
<div class="update-copy">
|
|
114
115
|
<span class="metric-label">版本更新</span>
|
|
@@ -116,6 +117,7 @@
|
|
|
116
117
|
<span id="updateDetail" class="update-detail">
|
|
117
118
|
当前版本 -,正在连接 npm 仓库。
|
|
118
119
|
</span>
|
|
120
|
+
<span id="updateHistory" class="update-detail" hidden></span>
|
|
119
121
|
<code id="updateCommand" class="update-command" hidden>
|
|
120
122
|
npm install -g codex-endpoint-switcher@latest
|
|
121
123
|
</code>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -30,6 +30,9 @@ function createWebBridge() {
|
|
|
30
30
|
listEndpoints() {
|
|
31
31
|
return request("/api/endpoints");
|
|
32
32
|
},
|
|
33
|
+
getEndpoint(payload) {
|
|
34
|
+
return request(`/api/endpoints/${payload.id}`);
|
|
35
|
+
},
|
|
33
36
|
getCurrentConfig() {
|
|
34
37
|
return request("/api/current");
|
|
35
38
|
},
|
|
@@ -156,8 +159,13 @@ function formatDateTime(value) {
|
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
function setStatus(message, type = "info") {
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
const statusBox = $("#appStatusBox");
|
|
163
|
+
if (!statusBox) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
statusBox.textContent = message;
|
|
168
|
+
statusBox.className = `status-box ${type}`;
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
function setAuthStatus(message, type = "info") {
|
|
@@ -310,14 +318,33 @@ function renderUpdateStatus() {
|
|
|
310
318
|
|
|
311
319
|
const summary = $("#updateSummary");
|
|
312
320
|
const detail = $("#updateDetail");
|
|
321
|
+
const history = $("#updateHistory");
|
|
313
322
|
const command = $("#updateCommand");
|
|
314
323
|
const copyButton = $("#copyUpdateCommandButton");
|
|
315
324
|
const autoUpdateButton = $("#autoUpdateButton");
|
|
316
325
|
|
|
317
|
-
if (!summary || !detail || !command || !copyButton || !autoUpdateButton) {
|
|
326
|
+
if (!summary || !detail || !history || !command || !copyButton || !autoUpdateButton) {
|
|
318
327
|
return;
|
|
319
328
|
}
|
|
320
329
|
|
|
330
|
+
history.hidden = true;
|
|
331
|
+
history.textContent = "";
|
|
332
|
+
|
|
333
|
+
if (update.autoUpdateState?.status) {
|
|
334
|
+
const autoUpdateState = update.autoUpdateState;
|
|
335
|
+
if (autoUpdateState.status === "succeeded") {
|
|
336
|
+
history.textContent = `上次自动更新已完成:${formatDateTime(autoUpdateState.finishedAt)}`;
|
|
337
|
+
history.hidden = false;
|
|
338
|
+
} else if (autoUpdateState.status === "failed") {
|
|
339
|
+
history.textContent =
|
|
340
|
+
`上次自动更新失败:${autoUpdateState.lastError || "请查看日志"}。日志:${autoUpdateState.logPath}`;
|
|
341
|
+
history.hidden = false;
|
|
342
|
+
} else if (autoUpdateState.status === "running" || autoUpdateState.status === "scheduled") {
|
|
343
|
+
history.textContent = "检测到自动更新任务正在执行,请稍候。";
|
|
344
|
+
history.hidden = false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
321
348
|
command.textContent = update.upgradeCommand;
|
|
322
349
|
|
|
323
350
|
if (update.hasUpdate) {
|
|
@@ -372,6 +399,23 @@ async function refreshUpdateStatus(force = false) {
|
|
|
372
399
|
}
|
|
373
400
|
}
|
|
374
401
|
|
|
402
|
+
async function openEditEndpoint(id, note) {
|
|
403
|
+
try {
|
|
404
|
+
setStatus(`正在加载连接:${note} ...`, "info");
|
|
405
|
+
const endpoint = await bridge.getEndpoint({ id });
|
|
406
|
+
fillForm(endpoint);
|
|
407
|
+
openEndpointModal();
|
|
408
|
+
setStatus(`正在编辑:${note}`, "info");
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (isUnauthorizedError(error)) {
|
|
411
|
+
await handleAccessRevoked(error.message);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
setStatus(`读取连接失败:${error.message}`, "error");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
375
419
|
function createEndpointCard(endpoint) {
|
|
376
420
|
const card = document.createElement("article");
|
|
377
421
|
card.className = `profile-card ${endpoint.isActive ? "active" : ""}`;
|
|
@@ -417,8 +461,7 @@ function createEndpointCard(endpoint) {
|
|
|
417
461
|
editButton.className = "ghost-button";
|
|
418
462
|
editButton.textContent = "编辑";
|
|
419
463
|
editButton.addEventListener("click", () => {
|
|
420
|
-
|
|
421
|
-
openEndpointModal();
|
|
464
|
+
openEditEndpoint(endpoint.id, endpoint.note);
|
|
422
465
|
});
|
|
423
466
|
|
|
424
467
|
const deleteButton = document.createElement("button");
|
|
@@ -793,7 +836,8 @@ async function handleAutoUpdate() {
|
|
|
793
836
|
summary.textContent = `正在自动更新到 ${update.latestVersion}...`;
|
|
794
837
|
}
|
|
795
838
|
if (detail) {
|
|
796
|
-
detail.textContent =
|
|
839
|
+
detail.textContent =
|
|
840
|
+
"程序会先关闭,再自动安装最新 npm 包并重新打开控制台。若 10 秒内没有自动关闭,请查看本地更新日志。";
|
|
797
841
|
}
|
|
798
842
|
if (checkButton) {
|
|
799
843
|
checkButton.disabled = true;
|
package/src/web/server.js
CHANGED
|
@@ -61,6 +61,14 @@ function createApp() {
|
|
|
61
61
|
}),
|
|
62
62
|
);
|
|
63
63
|
|
|
64
|
+
app.get(
|
|
65
|
+
"/api/endpoints/:id",
|
|
66
|
+
wrapAsync(async (req) => {
|
|
67
|
+
await ensureConsoleAccess();
|
|
68
|
+
return profileManager.getEndpointById(req.params.id);
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
64
72
|
app.post(
|
|
65
73
|
"/api/endpoints",
|
|
66
74
|
wrapAsync(async (req) => {
|
|
@@ -253,10 +261,11 @@ function createApp() {
|
|
|
253
261
|
|
|
254
262
|
function startServer(options = {}) {
|
|
255
263
|
const port = Number(options.port || process.env.PORT || 3186);
|
|
264
|
+
const host = options.host || process.env.HOST || "127.0.0.1";
|
|
256
265
|
const app = createApp();
|
|
257
266
|
const proxyServer = createProxyServer();
|
|
258
267
|
const proxyPort = profileManager.proxyPort;
|
|
259
|
-
const webServer = app.listen(port, () => {
|
|
268
|
+
const webServer = app.listen(port, host, () => {
|
|
260
269
|
const url = `http://localhost:${port}`;
|
|
261
270
|
console.log(`Codex 网页控制台已启动:${url}`);
|
|
262
271
|
console.log(`Codex 热更新代理已启动:${profileManager.proxyBaseUrl}`);
|
|
@@ -266,7 +275,7 @@ function startServer(options = {}) {
|
|
|
266
275
|
}
|
|
267
276
|
});
|
|
268
277
|
|
|
269
|
-
proxyServer.listen(proxyPort);
|
|
278
|
+
proxyServer.listen(proxyPort, host);
|
|
270
279
|
warmUpCurrentTarget().catch(() => {
|
|
271
280
|
// 预热失败不阻断服务启动,真正请求时仍会走自动重试。
|
|
272
281
|
});
|
|
@@ -303,7 +312,14 @@ function startServer(options = {}) {
|
|
|
303
312
|
|
|
304
313
|
process.removeAllListeners("codex-switcher:shutdown");
|
|
305
314
|
process.on("codex-switcher:shutdown", () => {
|
|
315
|
+
const forceExitTimer = setTimeout(() => {
|
|
316
|
+
webServer.closeAllConnections?.();
|
|
317
|
+
proxyServer.closeAllConnections?.();
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}, 2500);
|
|
320
|
+
|
|
306
321
|
controller.close(() => {
|
|
322
|
+
clearTimeout(forceExitTimer);
|
|
307
323
|
process.exit(0);
|
|
308
324
|
});
|
|
309
325
|
});
|