codex-endpoint-switcher 1.4.0 → 1.6.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 +16 -1
- package/bin/codex-switcher.js +21 -0
- package/package.json +1 -1
- package/src/main/auto-update-runner.js +189 -0
- package/src/main/main.js +12 -0
- package/src/main/preload.js +6 -0
- package/src/main/profile-manager.js +22 -2
- package/src/main/update-service.js +119 -0
- package/src/renderer/index.html +5 -0
- package/src/renderer/renderer.js +112 -5
- package/src/web/proxy-server.js +17 -3
- package/src/web/server.js +43 -3
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ codex-switcher status
|
|
|
50
50
|
codex-switcher restart
|
|
51
51
|
codex-switcher stop
|
|
52
52
|
codex-switcher check-update
|
|
53
|
+
codex-switcher self-update
|
|
53
54
|
codex-switcher sync-server
|
|
54
55
|
codex-switcher install-access
|
|
55
56
|
codex-switcher remove-access
|
|
@@ -63,6 +64,7 @@ codex-switcher remove-access
|
|
|
63
64
|
- `restart`:重启本地网页服务
|
|
64
65
|
- `stop`:关闭本地网页服务
|
|
65
66
|
- `check-update`:检查 npm 是否有新版本
|
|
67
|
+
- `self-update`:自动更新 npm 包并自动重启控制台
|
|
66
68
|
- `sync-server`:启动账号同步服务,服务端使用 SQLite 单文件数据库
|
|
67
69
|
- `install-access`:创建桌面快捷方式和开机启动项
|
|
68
70
|
- `remove-access`:移除桌面快捷方式和开机启动项
|
|
@@ -75,6 +77,11 @@ codex-switcher remove-access
|
|
|
75
77
|
http://localhost:3186
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
说明:
|
|
81
|
+
|
|
82
|
+
- 本地控制台和热更新代理默认只监听 `127.0.0.1`
|
|
83
|
+
- 不再暴露到局域网,避免其他设备直接访问你的本地 Key 管理接口
|
|
84
|
+
|
|
78
85
|
## 账号配置同步
|
|
79
86
|
|
|
80
87
|
网页控制台现在支持“账号 + 密码”的同步方式。
|
|
@@ -140,7 +147,9 @@ codex-switcher sync-server
|
|
|
140
147
|
|
|
141
148
|
- 控制台打开后会自动检查 npm 最新版本
|
|
142
149
|
- 如果你发布了新版本,其他用户打开控制台后会看到“发现新版本”的提示
|
|
143
|
-
-
|
|
150
|
+
- 用户可以直接点 `立即更新`,程序会自动关闭、更新并重新打开
|
|
151
|
+
- 也可以继续使用“复制升级命令”手动更新
|
|
152
|
+
- 上一次自动更新的成功或失败结果会记录到本地,下次打开控制台会显示结果
|
|
144
153
|
|
|
145
154
|
也可以用 CLI 手动检查:
|
|
146
155
|
|
|
@@ -148,6 +157,12 @@ codex-switcher sync-server
|
|
|
148
157
|
codex-switcher check-update
|
|
149
158
|
```
|
|
150
159
|
|
|
160
|
+
或者直接自动更新:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
codex-switcher self-update
|
|
164
|
+
```
|
|
165
|
+
|
|
151
166
|
## 更新 npm 包
|
|
152
167
|
|
|
153
168
|
先进入项目目录:
|
package/bin/codex-switcher.js
CHANGED
|
@@ -19,6 +19,7 @@ function printUsage() {
|
|
|
19
19
|
codex-switcher restart
|
|
20
20
|
codex-switcher stop
|
|
21
21
|
codex-switcher check-update
|
|
22
|
+
codex-switcher self-update
|
|
22
23
|
codex-switcher sync-server
|
|
23
24
|
codex-switcher install-access
|
|
24
25
|
codex-switcher remove-access
|
|
@@ -30,6 +31,7 @@ function printUsage() {
|
|
|
30
31
|
restart 重启本地网页服务
|
|
31
32
|
stop 关闭本地网页服务
|
|
32
33
|
check-update 检查 npm 最新版本
|
|
34
|
+
self-update 自动更新到最新版本并重启控制台
|
|
33
35
|
sync-server 启动账号同步服务(SQLite)
|
|
34
36
|
install-access 创建桌面快捷方式与开机启动项
|
|
35
37
|
remove-access 删除桌面快捷方式与开机启动项
|
|
@@ -110,6 +112,25 @@ async function main() {
|
|
|
110
112
|
console.log(`当前已是最新版:${status.currentVersion}`);
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
115
|
+
case "self-update": {
|
|
116
|
+
const status = await updateService.getUpdateStatus({ force: true });
|
|
117
|
+
if (!status.hasUpdate) {
|
|
118
|
+
if (status.lastError) {
|
|
119
|
+
console.log(`当前版本:${status.currentVersion}`);
|
|
120
|
+
console.log(`暂时无法检查更新:${status.lastError}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`当前已是最新版:${status.currentVersion}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = updateService.scheduleAutoUpdate({ reopen: true });
|
|
129
|
+
console.log(`已开始自动更新到 ${status.latestVersion}`);
|
|
130
|
+
console.log(`日志文件:${result.logPath}`);
|
|
131
|
+
await handleCommand("stop");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
113
134
|
case "sync-server": {
|
|
114
135
|
await runNodeScript(cloudSyncServerPath, ["--experimental-sqlite"]);
|
|
115
136
|
return;
|
package/package.json
CHANGED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const { spawn } = require("node:child_process");
|
|
3
|
+
|
|
4
|
+
function sleep(timeoutMs) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
setTimeout(resolve, timeoutMs);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function appendLog(logPath, text) {
|
|
11
|
+
await fs.appendFile(logPath, text, "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function writeState(statePath, payload) {
|
|
15
|
+
await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function runProcess(command, args, options = {}) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const child = spawn(command, args, {
|
|
21
|
+
cwd: options.cwd,
|
|
22
|
+
env: options.env,
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
|
|
30
|
+
child.stdout.on("data", (chunk) => {
|
|
31
|
+
stdout += chunk.toString("utf8");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.stderr.on("data", (chunk) => {
|
|
35
|
+
stderr += chunk.toString("utf8");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.on("exit", (code) => {
|
|
39
|
+
resolve({
|
|
40
|
+
code: Number(code || 0),
|
|
41
|
+
stdout,
|
|
42
|
+
stderr,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
child.on("error", (error) => {
|
|
47
|
+
resolve({
|
|
48
|
+
code: 1,
|
|
49
|
+
stdout,
|
|
50
|
+
stderr: `${stderr}${error instanceof Error ? error.message : String(error)}\n`,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function reopenConsole(payload) {
|
|
57
|
+
return runProcess(
|
|
58
|
+
payload.nodePath,
|
|
59
|
+
[payload.reopenBinPath, "open"],
|
|
60
|
+
{
|
|
61
|
+
cwd: process.cwd(),
|
|
62
|
+
env: process.env,
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function main() {
|
|
68
|
+
const encodedPayload = String(process.argv[2] || "").trim();
|
|
69
|
+
if (!encodedPayload) {
|
|
70
|
+
throw new Error("缺少自动更新参数。");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
|
|
74
|
+
const startedAt = new Date().toISOString();
|
|
75
|
+
|
|
76
|
+
await appendLog(
|
|
77
|
+
payload.logPath,
|
|
78
|
+
`[${startedAt}] 自动更新开始,当前版本 ${payload.currentVersion}\n`,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await writeState(payload.statePath, {
|
|
82
|
+
status: "running",
|
|
83
|
+
packageName: payload.packageName,
|
|
84
|
+
requestedVersion: "latest",
|
|
85
|
+
currentVersion: payload.currentVersion,
|
|
86
|
+
startedAt,
|
|
87
|
+
finishedAt: "",
|
|
88
|
+
logPath: payload.logPath,
|
|
89
|
+
reopen: payload.reopen,
|
|
90
|
+
lastError: "",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await sleep(Number(payload.waitSeconds || 3) * 1000);
|
|
94
|
+
|
|
95
|
+
const installResult = await runProcess(
|
|
96
|
+
payload.nodePath,
|
|
97
|
+
[payload.npmCliPath, "install", "-g", `${payload.packageName}@latest`],
|
|
98
|
+
{
|
|
99
|
+
cwd: process.cwd(),
|
|
100
|
+
env: process.env,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (installResult.stdout) {
|
|
105
|
+
await appendLog(payload.logPath, installResult.stdout);
|
|
106
|
+
}
|
|
107
|
+
if (installResult.stderr) {
|
|
108
|
+
await appendLog(payload.logPath, installResult.stderr);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (installResult.code !== 0) {
|
|
112
|
+
const finishedAt = new Date().toISOString();
|
|
113
|
+
const errorMessage = `npm 安装失败,退出码:${installResult.code}`;
|
|
114
|
+
await appendLog(payload.logPath, `[${finishedAt}] ${errorMessage}\n`);
|
|
115
|
+
await writeState(payload.statePath, {
|
|
116
|
+
status: "failed",
|
|
117
|
+
packageName: payload.packageName,
|
|
118
|
+
requestedVersion: "latest",
|
|
119
|
+
currentVersion: payload.currentVersion,
|
|
120
|
+
startedAt,
|
|
121
|
+
finishedAt,
|
|
122
|
+
logPath: payload.logPath,
|
|
123
|
+
reopen: payload.reopen,
|
|
124
|
+
lastError: errorMessage,
|
|
125
|
+
});
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let reopenResult = { code: 0, stdout: "", stderr: "" };
|
|
130
|
+
if (payload.reopen) {
|
|
131
|
+
reopenResult = await reopenConsole(payload);
|
|
132
|
+
if (reopenResult.stdout) {
|
|
133
|
+
await appendLog(payload.logPath, reopenResult.stdout);
|
|
134
|
+
}
|
|
135
|
+
if (reopenResult.stderr) {
|
|
136
|
+
await appendLog(payload.logPath, reopenResult.stderr);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const finishedAt = new Date().toISOString();
|
|
141
|
+
const finalStatus = reopenResult.code === 0 ? "succeeded" : "failed";
|
|
142
|
+
const lastError =
|
|
143
|
+
reopenResult.code === 0 ? "" : `更新已完成,但重新打开控制台失败,退出码:${reopenResult.code}`;
|
|
144
|
+
|
|
145
|
+
await appendLog(
|
|
146
|
+
payload.logPath,
|
|
147
|
+
`[${finishedAt}] ${finalStatus === "succeeded" ? "自动更新完成" : lastError}\n`,
|
|
148
|
+
);
|
|
149
|
+
await writeState(payload.statePath, {
|
|
150
|
+
status: finalStatus,
|
|
151
|
+
packageName: payload.packageName,
|
|
152
|
+
requestedVersion: "latest",
|
|
153
|
+
currentVersion: payload.currentVersion,
|
|
154
|
+
startedAt,
|
|
155
|
+
finishedAt,
|
|
156
|
+
logPath: payload.logPath,
|
|
157
|
+
reopen: payload.reopen,
|
|
158
|
+
lastError,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (finalStatus !== "succeeded") {
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main().catch(async (error) => {
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
if (process.argv[2]) {
|
|
169
|
+
try {
|
|
170
|
+
const payload = JSON.parse(Buffer.from(process.argv[2], "base64url").toString("utf8"));
|
|
171
|
+
const finishedAt = new Date().toISOString();
|
|
172
|
+
await appendLog(payload.logPath, `[${finishedAt}] 自动更新异常:${message}\n`);
|
|
173
|
+
await writeState(payload.statePath, {
|
|
174
|
+
status: "failed",
|
|
175
|
+
packageName: payload.packageName,
|
|
176
|
+
requestedVersion: "latest",
|
|
177
|
+
currentVersion: payload.currentVersion,
|
|
178
|
+
startedAt: finishedAt,
|
|
179
|
+
finishedAt,
|
|
180
|
+
logPath: payload.logPath,
|
|
181
|
+
reopen: payload.reopen,
|
|
182
|
+
lastError: message,
|
|
183
|
+
});
|
|
184
|
+
} catch {
|
|
185
|
+
// 参数损坏时无法再写状态,直接退出。
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|
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
|
});
|
|
@@ -106,6 +110,14 @@ function registerHandlers() {
|
|
|
106
110
|
ipcMain.handle("app:updateStatus", async (_event, payload) => {
|
|
107
111
|
return updateService.getUpdateStatus(payload || {});
|
|
108
112
|
});
|
|
113
|
+
|
|
114
|
+
ipcMain.handle("app:autoUpdate", async (_event, payload) => {
|
|
115
|
+
const result = updateService.scheduleAutoUpdate(payload || {});
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
app.quit();
|
|
118
|
+
}, 120);
|
|
119
|
+
return result;
|
|
120
|
+
});
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
app.whenReady().then(() => {
|
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
|
},
|
|
@@ -58,4 +61,7 @@ contextBridge.exposeInMainWorld("codexDesktop", {
|
|
|
58
61
|
getUpdateStatus(payload) {
|
|
59
62
|
return ipcRenderer.invoke("app:updateStatus", payload);
|
|
60
63
|
},
|
|
64
|
+
autoUpdate(payload) {
|
|
65
|
+
return ipcRenderer.invoke("app:autoUpdate", payload);
|
|
66
|
+
},
|
|
61
67
|
});
|
|
@@ -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,
|
|
@@ -1,12 +1,76 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const { spawn } = require("node:child_process");
|
|
1
5
|
const packageInfo = require("../../package.json");
|
|
2
6
|
|
|
7
|
+
const packageRoot = path.resolve(__dirname, "../..");
|
|
8
|
+
const runnerEntryPath = path.join(__dirname, "auto-update-runner.js");
|
|
3
9
|
const REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
4
10
|
const SUCCESS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
5
11
|
const ERROR_CACHE_TTL_MS = 30 * 1000;
|
|
6
12
|
const MAX_RETRY_TIMES = 3;
|
|
13
|
+
const AUTO_UPDATE_WAIT_SECONDS = 3;
|
|
7
14
|
|
|
8
15
|
let cachedStatus = null;
|
|
9
16
|
|
|
17
|
+
function getAutoUpdateLogPath() {
|
|
18
|
+
const logDir = path.join(os.homedir(), ".codex");
|
|
19
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
20
|
+
return path.join(logDir, "codex-switcher-auto-update.log");
|
|
21
|
+
}
|
|
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
|
+
|
|
10
74
|
function sleep(timeoutMs) {
|
|
11
75
|
return new Promise((resolve) => {
|
|
12
76
|
setTimeout(resolve, timeoutMs);
|
|
@@ -45,6 +109,7 @@ function buildStatus(payload = {}) {
|
|
|
45
109
|
const currentVersion = packageInfo.version;
|
|
46
110
|
const latestVersion = payload.latestVersion || currentVersion;
|
|
47
111
|
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
|
112
|
+
const autoUpdateState = payload.autoUpdateState || readAutoUpdateState();
|
|
48
113
|
|
|
49
114
|
return {
|
|
50
115
|
packageName: packageInfo.name,
|
|
@@ -55,6 +120,7 @@ function buildStatus(payload = {}) {
|
|
|
55
120
|
upgradeCommand: `npm install -g ${packageInfo.name}@latest`,
|
|
56
121
|
releaseUrl: `https://www.npmjs.com/package/${packageInfo.name}`,
|
|
57
122
|
lastError: payload.lastError || "",
|
|
123
|
+
autoUpdateState,
|
|
58
124
|
};
|
|
59
125
|
}
|
|
60
126
|
|
|
@@ -117,6 +183,59 @@ async function getUpdateStatus(options = {}) {
|
|
|
117
183
|
return cachedStatus;
|
|
118
184
|
}
|
|
119
185
|
|
|
186
|
+
function scheduleAutoUpdate(options = {}) {
|
|
187
|
+
const reopen = options.reopen !== false;
|
|
188
|
+
const logPath = getAutoUpdateLogPath();
|
|
189
|
+
const statePath = getAutoUpdateStatePath();
|
|
190
|
+
const runtimePaths = resolveRuntimePaths();
|
|
191
|
+
|
|
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({
|
|
206
|
+
packageName: packageInfo.name,
|
|
207
|
+
currentVersion: packageInfo.version,
|
|
208
|
+
waitSeconds: AUTO_UPDATE_WAIT_SECONDS,
|
|
209
|
+
logPath,
|
|
210
|
+
statePath,
|
|
211
|
+
reopen,
|
|
212
|
+
nodePath: runtimePaths.nodePath,
|
|
213
|
+
npmCliPath: runtimePaths.npmCliPath,
|
|
214
|
+
reopenBinPath: runtimePaths.reopenBinPath,
|
|
215
|
+
}),
|
|
216
|
+
"utf8",
|
|
217
|
+
).toString("base64url");
|
|
218
|
+
|
|
219
|
+
const child = spawn(process.execPath, [runnerEntryPath, payload], {
|
|
220
|
+
cwd: os.homedir(),
|
|
221
|
+
detached: true,
|
|
222
|
+
stdio: "ignore",
|
|
223
|
+
windowsHide: true,
|
|
224
|
+
env: process.env,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
child.unref();
|
|
228
|
+
return {
|
|
229
|
+
scheduled: true,
|
|
230
|
+
packageName: packageInfo.name,
|
|
231
|
+
currentVersion: packageInfo.version,
|
|
232
|
+
logPath,
|
|
233
|
+
statePath,
|
|
234
|
+
reopen,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
120
238
|
module.exports = {
|
|
121
239
|
getUpdateStatus,
|
|
240
|
+
scheduleAutoUpdate,
|
|
122
241
|
};
|
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,12 +117,16 @@
|
|
|
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>
|
|
122
124
|
</div>
|
|
123
125
|
<div class="update-actions">
|
|
124
126
|
<button id="checkUpdateButton" type="button" class="ghost-button">检查更新</button>
|
|
127
|
+
<button id="autoUpdateButton" type="button" class="primary-button" hidden>
|
|
128
|
+
立即更新
|
|
129
|
+
</button>
|
|
125
130
|
<button id="copyUpdateCommandButton" type="button" class="secondary-button" hidden>
|
|
126
131
|
复制升级命令
|
|
127
132
|
</button>
|
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
|
},
|
|
@@ -115,6 +118,12 @@ function createWebBridge() {
|
|
|
115
118
|
const query = searchParams.toString();
|
|
116
119
|
return request(`/api/app/update-status${query ? `?${query}` : ""}`);
|
|
117
120
|
},
|
|
121
|
+
autoUpdate(payload = {}) {
|
|
122
|
+
return request("/api/app/auto-update", {
|
|
123
|
+
method: "POST",
|
|
124
|
+
body: JSON.stringify(payload),
|
|
125
|
+
});
|
|
126
|
+
},
|
|
118
127
|
};
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -150,8 +159,13 @@ function formatDateTime(value) {
|
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
function setStatus(message, type = "info") {
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
const statusBox = $("#appStatusBox");
|
|
163
|
+
if (!statusBox) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
statusBox.textContent = message;
|
|
168
|
+
statusBox.className = `status-box ${type}`;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
function setAuthStatus(message, type = "info") {
|
|
@@ -304,13 +318,33 @@ function renderUpdateStatus() {
|
|
|
304
318
|
|
|
305
319
|
const summary = $("#updateSummary");
|
|
306
320
|
const detail = $("#updateDetail");
|
|
321
|
+
const history = $("#updateHistory");
|
|
307
322
|
const command = $("#updateCommand");
|
|
308
323
|
const copyButton = $("#copyUpdateCommandButton");
|
|
324
|
+
const autoUpdateButton = $("#autoUpdateButton");
|
|
309
325
|
|
|
310
|
-
if (!summary || !detail || !command || !copyButton) {
|
|
326
|
+
if (!summary || !detail || !history || !command || !copyButton || !autoUpdateButton) {
|
|
311
327
|
return;
|
|
312
328
|
}
|
|
313
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
|
+
|
|
314
348
|
command.textContent = update.upgradeCommand;
|
|
315
349
|
|
|
316
350
|
if (update.hasUpdate) {
|
|
@@ -318,6 +352,7 @@ function renderUpdateStatus() {
|
|
|
318
352
|
detail.textContent = `当前版本 ${update.currentVersion},可以直接升级到最新版本。`;
|
|
319
353
|
command.hidden = false;
|
|
320
354
|
copyButton.hidden = false;
|
|
355
|
+
autoUpdateButton.hidden = false;
|
|
321
356
|
return;
|
|
322
357
|
}
|
|
323
358
|
|
|
@@ -326,6 +361,7 @@ function renderUpdateStatus() {
|
|
|
326
361
|
detail.textContent = `当前版本 ${update.currentVersion},原因:${update.lastError}`;
|
|
327
362
|
command.hidden = true;
|
|
328
363
|
copyButton.hidden = true;
|
|
364
|
+
autoUpdateButton.hidden = true;
|
|
329
365
|
return;
|
|
330
366
|
}
|
|
331
367
|
|
|
@@ -333,6 +369,7 @@ function renderUpdateStatus() {
|
|
|
333
369
|
detail.textContent = "npm 包没有检测到更新,可以继续直接使用。";
|
|
334
370
|
command.hidden = true;
|
|
335
371
|
copyButton.hidden = true;
|
|
372
|
+
autoUpdateButton.hidden = true;
|
|
336
373
|
}
|
|
337
374
|
|
|
338
375
|
async function refreshUpdateStatus(force = false) {
|
|
@@ -362,6 +399,23 @@ async function refreshUpdateStatus(force = false) {
|
|
|
362
399
|
}
|
|
363
400
|
}
|
|
364
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
|
+
|
|
365
419
|
function createEndpointCard(endpoint) {
|
|
366
420
|
const card = document.createElement("article");
|
|
367
421
|
card.className = `profile-card ${endpoint.isActive ? "active" : ""}`;
|
|
@@ -407,8 +461,7 @@ function createEndpointCard(endpoint) {
|
|
|
407
461
|
editButton.className = "ghost-button";
|
|
408
462
|
editButton.textContent = "编辑";
|
|
409
463
|
editButton.addEventListener("click", () => {
|
|
410
|
-
|
|
411
|
-
openEndpointModal();
|
|
464
|
+
openEditEndpoint(endpoint.id, endpoint.note);
|
|
412
465
|
});
|
|
413
466
|
|
|
414
467
|
const deleteButton = document.createElement("button");
|
|
@@ -760,6 +813,59 @@ async function handleCopyUpdateCommand() {
|
|
|
760
813
|
}
|
|
761
814
|
}
|
|
762
815
|
|
|
816
|
+
async function handleAutoUpdate() {
|
|
817
|
+
const update = state.update;
|
|
818
|
+
if (!update?.hasUpdate) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const confirmed = window.confirm(
|
|
823
|
+
`确定现在自动更新到 ${update.latestVersion} 吗?程序会自动关闭,更新完成后重新打开。`,
|
|
824
|
+
);
|
|
825
|
+
if (!confirmed) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const summary = $("#updateSummary");
|
|
830
|
+
const detail = $("#updateDetail");
|
|
831
|
+
const checkButton = $("#checkUpdateButton");
|
|
832
|
+
const autoUpdateButton = $("#autoUpdateButton");
|
|
833
|
+
const copyButton = $("#copyUpdateCommandButton");
|
|
834
|
+
|
|
835
|
+
if (summary) {
|
|
836
|
+
summary.textContent = `正在自动更新到 ${update.latestVersion}...`;
|
|
837
|
+
}
|
|
838
|
+
if (detail) {
|
|
839
|
+
detail.textContent = "程序会先关闭,再自动安装最新 npm 包并重新打开控制台。";
|
|
840
|
+
}
|
|
841
|
+
if (checkButton) {
|
|
842
|
+
checkButton.disabled = true;
|
|
843
|
+
}
|
|
844
|
+
if (autoUpdateButton) {
|
|
845
|
+
autoUpdateButton.disabled = true;
|
|
846
|
+
}
|
|
847
|
+
if (copyButton) {
|
|
848
|
+
copyButton.disabled = true;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
const result = await bridge.autoUpdate({ reopen: true });
|
|
853
|
+
setStatus(`已开始自动更新,日志:${result.logPath}`, "success");
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (checkButton) {
|
|
856
|
+
checkButton.disabled = false;
|
|
857
|
+
}
|
|
858
|
+
if (autoUpdateButton) {
|
|
859
|
+
autoUpdateButton.disabled = false;
|
|
860
|
+
}
|
|
861
|
+
if (copyButton) {
|
|
862
|
+
copyButton.disabled = false;
|
|
863
|
+
}
|
|
864
|
+
setStatus(`自动更新失败:${error.message}`, "error");
|
|
865
|
+
await refreshUpdateStatus(true);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
763
869
|
function bindEvents() {
|
|
764
870
|
$("#authForm").addEventListener("submit", (event) => {
|
|
765
871
|
event.preventDefault();
|
|
@@ -810,6 +916,7 @@ function bindEvents() {
|
|
|
810
916
|
$("#checkUpdateButton").addEventListener("click", () => {
|
|
811
917
|
refreshUpdateStatus(true);
|
|
812
918
|
});
|
|
919
|
+
$("#autoUpdateButton").addEventListener("click", handleAutoUpdate);
|
|
813
920
|
$("#copyUpdateCommandButton").addEventListener("click", handleCopyUpdateCommand);
|
|
814
921
|
$("#closeAppButton").addEventListener("click", handleCloseApp);
|
|
815
922
|
window.addEventListener("keydown", (event) => {
|
package/src/web/proxy-server.js
CHANGED
|
@@ -10,6 +10,9 @@ const httpsAgent = new https.Agent({
|
|
|
10
10
|
keepAlive: true,
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
const UPSTREAM_REQUEST_TIMEOUT_MS = 90000;
|
|
14
|
+
const WARM_UP_TIMEOUT_MS = 12000;
|
|
15
|
+
|
|
13
16
|
function sleep(ms) {
|
|
14
17
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
18
|
}
|
|
@@ -60,11 +63,13 @@ function sendUpstreamRequest({
|
|
|
60
63
|
agent: upstreamUrl.protocol === "https:" ? httpsAgent : httpAgent,
|
|
61
64
|
},
|
|
62
65
|
(upstreamResponse) => {
|
|
66
|
+
upstreamResponse.socket?.setNoDelay?.(true);
|
|
63
67
|
resolve(upstreamResponse);
|
|
64
68
|
},
|
|
65
69
|
);
|
|
66
70
|
|
|
67
|
-
upstreamRequest.
|
|
71
|
+
upstreamRequest.setNoDelay?.(true);
|
|
72
|
+
upstreamRequest.setTimeout(UPSTREAM_REQUEST_TIMEOUT_MS, () => {
|
|
68
73
|
upstreamRequest.destroy(new Error("上游请求超时。"));
|
|
69
74
|
});
|
|
70
75
|
|
|
@@ -135,7 +140,7 @@ async function warmUpCurrentTarget() {
|
|
|
135
140
|
},
|
|
136
141
|
);
|
|
137
142
|
|
|
138
|
-
warmRequest.setTimeout(
|
|
143
|
+
warmRequest.setTimeout(WARM_UP_TIMEOUT_MS, () => {
|
|
139
144
|
warmRequest.destroy(new Error("预热请求超时。"));
|
|
140
145
|
});
|
|
141
146
|
warmRequest.on("error", reject);
|
|
@@ -144,8 +149,11 @@ async function warmUpCurrentTarget() {
|
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
function createProxyServer() {
|
|
147
|
-
|
|
152
|
+
const server = http.createServer(async (req, res) => {
|
|
148
153
|
try {
|
|
154
|
+
req.socket?.setNoDelay?.(true);
|
|
155
|
+
res.socket?.setNoDelay?.(true);
|
|
156
|
+
|
|
149
157
|
if (req.url === "/__switcher/health") {
|
|
150
158
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
151
159
|
res.end(JSON.stringify({ ok: true, proxyBaseUrl: profileManager.proxyBaseUrl }));
|
|
@@ -189,6 +197,12 @@ function createProxyServer() {
|
|
|
189
197
|
);
|
|
190
198
|
}
|
|
191
199
|
});
|
|
200
|
+
|
|
201
|
+
server.keepAliveTimeout = 90_000;
|
|
202
|
+
server.headersTimeout = 95_000;
|
|
203
|
+
server.requestTimeout = 0;
|
|
204
|
+
|
|
205
|
+
return server;
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
module.exports = {
|
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) => {
|
|
@@ -89,7 +97,23 @@ function createApp() {
|
|
|
89
97
|
"/api/endpoints/switch",
|
|
90
98
|
wrapAsync(async (req) => {
|
|
91
99
|
await ensureConsoleAccess();
|
|
92
|
-
|
|
100
|
+
const result = await profileManager.switchEndpoint(req.body.id);
|
|
101
|
+
if (result.switchedViaProxy) {
|
|
102
|
+
try {
|
|
103
|
+
await warmUpCurrentTarget();
|
|
104
|
+
return {
|
|
105
|
+
...result,
|
|
106
|
+
proxyWarmedUp: true,
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return {
|
|
110
|
+
...result,
|
|
111
|
+
proxyWarmedUp: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
93
117
|
}),
|
|
94
118
|
);
|
|
95
119
|
|
|
@@ -189,6 +213,21 @@ function createApp() {
|
|
|
189
213
|
}),
|
|
190
214
|
);
|
|
191
215
|
|
|
216
|
+
app.post(
|
|
217
|
+
"/api/app/auto-update",
|
|
218
|
+
wrapAsync(async (req) => {
|
|
219
|
+
const result = updateService.scheduleAutoUpdate({
|
|
220
|
+
reopen: req.body?.reopen !== false,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
process.emit("codex-switcher:shutdown");
|
|
225
|
+
}, 120);
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
192
231
|
app.post(
|
|
193
232
|
"/api/open-path",
|
|
194
233
|
wrapAsync(async (req) => {
|
|
@@ -222,10 +261,11 @@ function createApp() {
|
|
|
222
261
|
|
|
223
262
|
function startServer(options = {}) {
|
|
224
263
|
const port = Number(options.port || process.env.PORT || 3186);
|
|
264
|
+
const host = options.host || process.env.HOST || "127.0.0.1";
|
|
225
265
|
const app = createApp();
|
|
226
266
|
const proxyServer = createProxyServer();
|
|
227
267
|
const proxyPort = profileManager.proxyPort;
|
|
228
|
-
const webServer = app.listen(port, () => {
|
|
268
|
+
const webServer = app.listen(port, host, () => {
|
|
229
269
|
const url = `http://localhost:${port}`;
|
|
230
270
|
console.log(`Codex 网页控制台已启动:${url}`);
|
|
231
271
|
console.log(`Codex 热更新代理已启动:${profileManager.proxyBaseUrl}`);
|
|
@@ -235,7 +275,7 @@ function startServer(options = {}) {
|
|
|
235
275
|
}
|
|
236
276
|
});
|
|
237
277
|
|
|
238
|
-
proxyServer.listen(proxyPort);
|
|
278
|
+
proxyServer.listen(proxyPort, host);
|
|
239
279
|
warmUpCurrentTarget().catch(() => {
|
|
240
280
|
// 预热失败不阻断服务启动,真正请求时仍会走自动重试。
|
|
241
281
|
});
|