codex-endpoint-switcher 1.4.0 → 1.5.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 +10 -1
- package/bin/codex-switcher.js +21 -0
- package/package.json +1 -1
- package/src/main/main.js +8 -0
- package/src/main/preload.js +3 -0
- package/src/main/update-service.js +66 -0
- package/src/renderer/index.html +3 -0
- package/src/renderer/renderer.js +65 -1
- package/src/web/proxy-server.js +17 -3
- package/src/web/server.js +32 -1
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`:移除桌面快捷方式和开机启动项
|
|
@@ -140,7 +142,8 @@ codex-switcher sync-server
|
|
|
140
142
|
|
|
141
143
|
- 控制台打开后会自动检查 npm 最新版本
|
|
142
144
|
- 如果你发布了新版本,其他用户打开控制台后会看到“发现新版本”的提示
|
|
143
|
-
-
|
|
145
|
+
- 用户可以直接点 `立即更新`,程序会自动关闭、更新并重新打开
|
|
146
|
+
- 也可以继续使用“复制升级命令”手动更新
|
|
144
147
|
|
|
145
148
|
也可以用 CLI 手动检查:
|
|
146
149
|
|
|
@@ -148,6 +151,12 @@ codex-switcher sync-server
|
|
|
148
151
|
codex-switcher check-update
|
|
149
152
|
```
|
|
150
153
|
|
|
154
|
+
或者直接自动更新:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
codex-switcher self-update
|
|
158
|
+
```
|
|
159
|
+
|
|
151
160
|
## 更新 npm 包
|
|
152
161
|
|
|
153
162
|
先进入项目目录:
|
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
package/src/main/main.js
CHANGED
|
@@ -106,6 +106,14 @@ function registerHandlers() {
|
|
|
106
106
|
ipcMain.handle("app:updateStatus", async (_event, payload) => {
|
|
107
107
|
return updateService.getUpdateStatus(payload || {});
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
ipcMain.handle("app:autoUpdate", async (_event, payload) => {
|
|
111
|
+
const result = updateService.scheduleAutoUpdate(payload || {});
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
app.quit();
|
|
114
|
+
}, 120);
|
|
115
|
+
return result;
|
|
116
|
+
});
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
app.whenReady().then(() => {
|
package/src/main/preload.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
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
|
|
|
3
7
|
const REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
4
8
|
const SUCCESS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
5
9
|
const ERROR_CACHE_TTL_MS = 30 * 1000;
|
|
6
10
|
const MAX_RETRY_TIMES = 3;
|
|
11
|
+
const AUTO_UPDATE_WAIT_SECONDS = 3;
|
|
7
12
|
|
|
8
13
|
let cachedStatus = null;
|
|
9
14
|
|
|
15
|
+
function getAutoUpdateLogPath() {
|
|
16
|
+
const logDir = path.join(os.homedir(), ".codex");
|
|
17
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
18
|
+
return path.join(logDir, "codex-switcher-auto-update.log");
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
function sleep(timeoutMs) {
|
|
11
22
|
return new Promise((resolve) => {
|
|
12
23
|
setTimeout(resolve, timeoutMs);
|
|
@@ -117,6 +128,61 @@ async function getUpdateStatus(options = {}) {
|
|
|
117
128
|
return cachedStatus;
|
|
118
129
|
}
|
|
119
130
|
|
|
131
|
+
function scheduleAutoUpdate(options = {}) {
|
|
132
|
+
const reopen = options.reopen !== false;
|
|
133
|
+
const logPath = getAutoUpdateLogPath();
|
|
134
|
+
const installCommand = `npm install -g ${packageInfo.name}@latest`;
|
|
135
|
+
const restartCommand = reopen ? " && codex-switcher open" : "";
|
|
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
|
+
});
|
|
151
|
+
|
|
152
|
+
child.unref();
|
|
153
|
+
return {
|
|
154
|
+
scheduled: true,
|
|
155
|
+
packageName: packageInfo.name,
|
|
156
|
+
currentVersion: packageInfo.version,
|
|
157
|
+
logPath,
|
|
158
|
+
reopen,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const command =
|
|
163
|
+
`sleep ${AUTO_UPDATE_WAIT_SECONDS}; ` +
|
|
164
|
+
`echo "[auto-update] start ${packageInfo.name}" > "${logPath}"; ` +
|
|
165
|
+
`${installCommand} >> "${logPath}" 2>&1` +
|
|
166
|
+
(reopen ? ` && codex-switcher open >> "${logPath}" 2>&1` : "");
|
|
167
|
+
|
|
168
|
+
const child = spawn("sh", ["-lc", command], {
|
|
169
|
+
cwd: os.homedir(),
|
|
170
|
+
detached: true,
|
|
171
|
+
stdio: "ignore",
|
|
172
|
+
env: process.env,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
child.unref();
|
|
176
|
+
return {
|
|
177
|
+
scheduled: true,
|
|
178
|
+
packageName: packageInfo.name,
|
|
179
|
+
currentVersion: packageInfo.version,
|
|
180
|
+
logPath,
|
|
181
|
+
reopen,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
120
185
|
module.exports = {
|
|
121
186
|
getUpdateStatus,
|
|
187
|
+
scheduleAutoUpdate,
|
|
122
188
|
};
|
package/src/renderer/index.html
CHANGED
|
@@ -122,6 +122,9 @@
|
|
|
122
122
|
</div>
|
|
123
123
|
<div class="update-actions">
|
|
124
124
|
<button id="checkUpdateButton" type="button" class="ghost-button">检查更新</button>
|
|
125
|
+
<button id="autoUpdateButton" type="button" class="primary-button" hidden>
|
|
126
|
+
立即更新
|
|
127
|
+
</button>
|
|
125
128
|
<button id="copyUpdateCommandButton" type="button" class="secondary-button" hidden>
|
|
126
129
|
复制升级命令
|
|
127
130
|
</button>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -115,6 +115,12 @@ function createWebBridge() {
|
|
|
115
115
|
const query = searchParams.toString();
|
|
116
116
|
return request(`/api/app/update-status${query ? `?${query}` : ""}`);
|
|
117
117
|
},
|
|
118
|
+
autoUpdate(payload = {}) {
|
|
119
|
+
return request("/api/app/auto-update", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: JSON.stringify(payload),
|
|
122
|
+
});
|
|
123
|
+
},
|
|
118
124
|
};
|
|
119
125
|
}
|
|
120
126
|
|
|
@@ -306,8 +312,9 @@ function renderUpdateStatus() {
|
|
|
306
312
|
const detail = $("#updateDetail");
|
|
307
313
|
const command = $("#updateCommand");
|
|
308
314
|
const copyButton = $("#copyUpdateCommandButton");
|
|
315
|
+
const autoUpdateButton = $("#autoUpdateButton");
|
|
309
316
|
|
|
310
|
-
if (!summary || !detail || !command || !copyButton) {
|
|
317
|
+
if (!summary || !detail || !command || !copyButton || !autoUpdateButton) {
|
|
311
318
|
return;
|
|
312
319
|
}
|
|
313
320
|
|
|
@@ -318,6 +325,7 @@ function renderUpdateStatus() {
|
|
|
318
325
|
detail.textContent = `当前版本 ${update.currentVersion},可以直接升级到最新版本。`;
|
|
319
326
|
command.hidden = false;
|
|
320
327
|
copyButton.hidden = false;
|
|
328
|
+
autoUpdateButton.hidden = false;
|
|
321
329
|
return;
|
|
322
330
|
}
|
|
323
331
|
|
|
@@ -326,6 +334,7 @@ function renderUpdateStatus() {
|
|
|
326
334
|
detail.textContent = `当前版本 ${update.currentVersion},原因:${update.lastError}`;
|
|
327
335
|
command.hidden = true;
|
|
328
336
|
copyButton.hidden = true;
|
|
337
|
+
autoUpdateButton.hidden = true;
|
|
329
338
|
return;
|
|
330
339
|
}
|
|
331
340
|
|
|
@@ -333,6 +342,7 @@ function renderUpdateStatus() {
|
|
|
333
342
|
detail.textContent = "npm 包没有检测到更新,可以继续直接使用。";
|
|
334
343
|
command.hidden = true;
|
|
335
344
|
copyButton.hidden = true;
|
|
345
|
+
autoUpdateButton.hidden = true;
|
|
336
346
|
}
|
|
337
347
|
|
|
338
348
|
async function refreshUpdateStatus(force = false) {
|
|
@@ -760,6 +770,59 @@ async function handleCopyUpdateCommand() {
|
|
|
760
770
|
}
|
|
761
771
|
}
|
|
762
772
|
|
|
773
|
+
async function handleAutoUpdate() {
|
|
774
|
+
const update = state.update;
|
|
775
|
+
if (!update?.hasUpdate) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const confirmed = window.confirm(
|
|
780
|
+
`确定现在自动更新到 ${update.latestVersion} 吗?程序会自动关闭,更新完成后重新打开。`,
|
|
781
|
+
);
|
|
782
|
+
if (!confirmed) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const summary = $("#updateSummary");
|
|
787
|
+
const detail = $("#updateDetail");
|
|
788
|
+
const checkButton = $("#checkUpdateButton");
|
|
789
|
+
const autoUpdateButton = $("#autoUpdateButton");
|
|
790
|
+
const copyButton = $("#copyUpdateCommandButton");
|
|
791
|
+
|
|
792
|
+
if (summary) {
|
|
793
|
+
summary.textContent = `正在自动更新到 ${update.latestVersion}...`;
|
|
794
|
+
}
|
|
795
|
+
if (detail) {
|
|
796
|
+
detail.textContent = "程序会先关闭,再自动安装最新 npm 包并重新打开控制台。";
|
|
797
|
+
}
|
|
798
|
+
if (checkButton) {
|
|
799
|
+
checkButton.disabled = true;
|
|
800
|
+
}
|
|
801
|
+
if (autoUpdateButton) {
|
|
802
|
+
autoUpdateButton.disabled = true;
|
|
803
|
+
}
|
|
804
|
+
if (copyButton) {
|
|
805
|
+
copyButton.disabled = true;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const result = await bridge.autoUpdate({ reopen: true });
|
|
810
|
+
setStatus(`已开始自动更新,日志:${result.logPath}`, "success");
|
|
811
|
+
} catch (error) {
|
|
812
|
+
if (checkButton) {
|
|
813
|
+
checkButton.disabled = false;
|
|
814
|
+
}
|
|
815
|
+
if (autoUpdateButton) {
|
|
816
|
+
autoUpdateButton.disabled = false;
|
|
817
|
+
}
|
|
818
|
+
if (copyButton) {
|
|
819
|
+
copyButton.disabled = false;
|
|
820
|
+
}
|
|
821
|
+
setStatus(`自动更新失败:${error.message}`, "error");
|
|
822
|
+
await refreshUpdateStatus(true);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
763
826
|
function bindEvents() {
|
|
764
827
|
$("#authForm").addEventListener("submit", (event) => {
|
|
765
828
|
event.preventDefault();
|
|
@@ -810,6 +873,7 @@ function bindEvents() {
|
|
|
810
873
|
$("#checkUpdateButton").addEventListener("click", () => {
|
|
811
874
|
refreshUpdateStatus(true);
|
|
812
875
|
});
|
|
876
|
+
$("#autoUpdateButton").addEventListener("click", handleAutoUpdate);
|
|
813
877
|
$("#copyUpdateCommandButton").addEventListener("click", handleCopyUpdateCommand);
|
|
814
878
|
$("#closeAppButton").addEventListener("click", handleCloseApp);
|
|
815
879
|
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
|
@@ -89,7 +89,23 @@ function createApp() {
|
|
|
89
89
|
"/api/endpoints/switch",
|
|
90
90
|
wrapAsync(async (req) => {
|
|
91
91
|
await ensureConsoleAccess();
|
|
92
|
-
|
|
92
|
+
const result = await profileManager.switchEndpoint(req.body.id);
|
|
93
|
+
if (result.switchedViaProxy) {
|
|
94
|
+
try {
|
|
95
|
+
await warmUpCurrentTarget();
|
|
96
|
+
return {
|
|
97
|
+
...result,
|
|
98
|
+
proxyWarmedUp: true,
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
...result,
|
|
103
|
+
proxyWarmedUp: false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
93
109
|
}),
|
|
94
110
|
);
|
|
95
111
|
|
|
@@ -189,6 +205,21 @@ function createApp() {
|
|
|
189
205
|
}),
|
|
190
206
|
);
|
|
191
207
|
|
|
208
|
+
app.post(
|
|
209
|
+
"/api/app/auto-update",
|
|
210
|
+
wrapAsync(async (req) => {
|
|
211
|
+
const result = updateService.scheduleAutoUpdate({
|
|
212
|
+
reopen: req.body?.reopen !== false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
process.emit("codex-switcher:shutdown");
|
|
217
|
+
}, 120);
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
192
223
|
app.post(
|
|
193
224
|
"/api/open-path",
|
|
194
225
|
wrapAsync(async (req) => {
|