codex-endpoint-switcher 1.3.0 → 1.3.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 CHANGED
@@ -48,6 +48,7 @@ codex-switcher open
48
48
  codex-switcher start
49
49
  codex-switcher status
50
50
  codex-switcher restart
51
+ codex-switcher stop
51
52
  codex-switcher sync-server
52
53
  codex-switcher install-access
53
54
  codex-switcher remove-access
@@ -59,6 +60,7 @@ codex-switcher remove-access
59
60
  - `start`:仅在后台启动本地网页服务
60
61
  - `status`:查看当前服务状态
61
62
  - `restart`:重启本地网页服务
63
+ - `stop`:关闭本地网页服务
62
64
  - `sync-server`:启动账号同步服务,服务端使用 SQLite 单文件数据库
63
65
  - `install-access`:创建桌面快捷方式和开机启动项
64
66
  - `remove-access`:移除桌面快捷方式和开机启动项
@@ -16,6 +16,7 @@ function printUsage() {
16
16
  codex-switcher start
17
17
  codex-switcher status
18
18
  codex-switcher restart
19
+ codex-switcher stop
19
20
  codex-switcher sync-server
20
21
  codex-switcher install-access
21
22
  codex-switcher remove-access
@@ -25,6 +26,7 @@ function printUsage() {
25
26
  start 仅在后台启动本地网页服务
26
27
  status 查看服务状态
27
28
  restart 重启本地网页服务
29
+ stop 关闭本地网页服务
28
30
  sync-server 启动账号同步服务(SQLite)
29
31
  install-access 创建桌面快捷方式与开机启动项
30
32
  remove-access 删除桌面快捷方式与开机启动项
@@ -82,7 +84,8 @@ async function main() {
82
84
  case "open":
83
85
  case "start":
84
86
  case "status":
85
- case "restart": {
87
+ case "restart":
88
+ case "stop": {
86
89
  await handleCommand(command === "start" ? "ensure" : command);
87
90
  return;
88
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-endpoint-switcher",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "用于切换 Codex URL 和 Key 的本地网页控制台与 npm CLI",
5
5
  "main": "src/main/main.js",
6
6
  "bin": {
package/src/main/main.js CHANGED
@@ -91,6 +91,16 @@ function registerHandlers() {
91
91
  const openError = await shell.openPath(targetPath);
92
92
  return openError || "";
93
93
  });
94
+
95
+ ipcMain.handle("app:close", async () => {
96
+ setTimeout(() => {
97
+ app.quit();
98
+ }, 60);
99
+
100
+ return {
101
+ closing: true,
102
+ };
103
+ });
94
104
  }
95
105
 
96
106
  app.whenReady().then(() => {
@@ -52,4 +52,7 @@ contextBridge.exposeInMainWorld("codexDesktop", {
52
52
  openPath(targetPath) {
53
53
  return ipcRenderer.invoke("shell:openPath", targetPath);
54
54
  },
55
+ closeApp() {
56
+ return ipcRenderer.invoke("app:close");
57
+ },
55
58
  });
@@ -82,6 +82,7 @@
82
82
  <button id="refreshButton" class="ghost-button">刷新状态</button>
83
83
  <button id="openCodexRootButton" class="ghost-button">打开 Codex 目录</button>
84
84
  <button id="openEndpointStoreButton" class="ghost-button">打开连接数据文件</button>
85
+ <button id="closeAppButton" class="danger-button">关闭程序</button>
85
86
  </div>
86
87
  <div class="current-grid">
87
88
  <article class="metric-card">
@@ -100,6 +100,11 @@ function createWebBridge() {
100
100
  body: JSON.stringify({ targetPath }),
101
101
  }).then(() => "");
102
102
  },
103
+ closeApp() {
104
+ return request("/api/app/close", {
105
+ method: "POST",
106
+ });
107
+ },
103
108
  };
104
109
  }
105
110
 
@@ -641,6 +646,19 @@ async function handleOpenPath(targetPath, label) {
641
646
  }
642
647
  }
643
648
 
649
+ async function handleCloseApp() {
650
+ const confirmed = window.confirm("确定要关闭当前程序吗?");
651
+ if (!confirmed) {
652
+ return;
653
+ }
654
+
655
+ try {
656
+ await bridge.closeApp();
657
+ } catch {
658
+ // 服务关闭过程会中断当前页面请求,这里不再额外提示。
659
+ }
660
+ }
661
+
644
662
  function bindEvents() {
645
663
  $("#authForm").addEventListener("submit", (event) => {
646
664
  event.preventDefault();
@@ -688,6 +706,7 @@ function bindEvents() {
688
706
  handleOpenPath(state.paths.endpointStorePath, " 连接数据文件");
689
707
  }
690
708
  });
709
+ $("#closeAppButton").addEventListener("click", handleCloseApp);
691
710
  window.addEventListener("keydown", (event) => {
692
711
  if (event.key === "Escape" && !$("#endpointModal").hidden) {
693
712
  closeEndpointModal();
@@ -9,6 +9,7 @@ const serverEntryPath = path.join(__dirname, "server.js");
9
9
  const port = Number(process.env.PORT || 3186);
10
10
  const proxyPort = 3187;
11
11
  const healthUrl = `http://127.0.0.1:${port}/api/health`;
12
+ const closeUrl = `http://127.0.0.1:${port}/api/app/close`;
12
13
  const proxyHealthUrl = `http://127.0.0.1:${proxyPort}/__switcher/health`;
13
14
  const consoleUrl = `http://localhost:${port}`;
14
15
 
@@ -41,6 +42,44 @@ function requestJson(url, timeoutMs = 1200) {
41
42
  });
42
43
  }
43
44
 
45
+ function postJson(url, timeoutMs = 2000) {
46
+ return new Promise((resolve, reject) => {
47
+ const target = new URL(url);
48
+ const request = http.request(
49
+ {
50
+ hostname: target.hostname,
51
+ port: target.port,
52
+ path: target.pathname,
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ },
57
+ },
58
+ (response) => {
59
+ let data = "";
60
+ response.setEncoding("utf8");
61
+ response.on("data", (chunk) => {
62
+ data += chunk;
63
+ });
64
+ response.on("end", () => {
65
+ try {
66
+ resolve(JSON.parse(data));
67
+ } catch (error) {
68
+ reject(error);
69
+ }
70
+ });
71
+ },
72
+ );
73
+
74
+ request.setTimeout(timeoutMs, () => {
75
+ request.destroy(new Error("请求超时"));
76
+ });
77
+
78
+ request.on("error", reject);
79
+ request.end("{}");
80
+ });
81
+ }
82
+
44
83
  /**
45
84
  * 检查本地网页服务是否已经可用。
46
85
  */
@@ -183,6 +222,65 @@ async function restartServer() {
183
222
  return ensureServerRunning();
184
223
  }
185
224
 
225
+ function killListeningPid(targetPort) {
226
+ return new Promise((resolve, reject) => {
227
+ exec(`cmd /c netstat -ano | findstr :${targetPort}`, (error, stdout) => {
228
+ if (error) {
229
+ resolve(false);
230
+ return;
231
+ }
232
+
233
+ const lines = stdout
234
+ .split(/\r?\n/)
235
+ .map((line) => line.trim())
236
+ .filter(Boolean);
237
+
238
+ const pids = [...new Set(lines
239
+ .map((line) => line.split(/\s+/))
240
+ .filter((parts) => parts[3] === "LISTENING")
241
+ .map((parts) => Number(parts[4]))
242
+ .filter((pid) => Number.isFinite(pid)))];
243
+
244
+ if (!pids.length) {
245
+ resolve(false);
246
+ return;
247
+ }
248
+
249
+ exec(`cmd /c taskkill ${pids.map((pid) => `/PID ${pid}`).join(" ")} /F`, (killError) => {
250
+ if (killError) {
251
+ reject(killError);
252
+ return;
253
+ }
254
+
255
+ resolve(true);
256
+ });
257
+ });
258
+ });
259
+ }
260
+
261
+ async function stopServer() {
262
+ if (await checkServerHealth()) {
263
+ try {
264
+ await postJson(closeUrl);
265
+ } catch {
266
+ // 服务正在退出时,可能来不及返回完整响应,这里继续走兜底检查。
267
+ }
268
+
269
+ await sleep(1000);
270
+ }
271
+
272
+ const webKilled = await killListeningPid(port);
273
+ const proxyKilled = await killListeningPid(proxyPort);
274
+
275
+ return {
276
+ stopped: true,
277
+ webKilled,
278
+ proxyKilled,
279
+ port,
280
+ proxyPort,
281
+ };
282
+ }
283
+
186
284
  async function handleCommand(command) {
187
285
  switch (command) {
188
286
  case "ensure":
@@ -227,8 +325,13 @@ async function handleCommand(command) {
227
325
  console.log(JSON.stringify({ ...result, restarted: true }, null, 2));
228
326
  return;
229
327
  }
328
+ case "stop": {
329
+ const result = await stopServer();
330
+ console.log(JSON.stringify(result, null, 2));
331
+ return;
332
+ }
230
333
  default:
231
- throw new Error("不支持的命令。可用命令:ensure、open、status、restart");
334
+ throw new Error("不支持的命令。可用命令:ensure、open、status、restart、stop");
232
335
  }
233
336
  }
234
337
 
@@ -245,6 +348,7 @@ module.exports = {
245
348
  ensureServerRunning,
246
349
  handleCommand,
247
350
  restartServer,
351
+ stopServer,
248
352
  openBrowser,
249
353
  runtimeDir,
250
354
  consoleUrl,
package/src/web/server.js CHANGED
@@ -166,6 +166,19 @@ function createApp() {
166
166
  }),
167
167
  );
168
168
 
169
+ app.post(
170
+ "/api/app/close",
171
+ wrapAsync(async () => {
172
+ setTimeout(() => {
173
+ process.emit("codex-switcher:shutdown");
174
+ }, 80);
175
+
176
+ return {
177
+ closing: true,
178
+ };
179
+ }),
180
+ );
181
+
169
182
  app.post(
170
183
  "/api/open-path",
171
184
  wrapAsync(async (req) => {
@@ -214,7 +227,7 @@ function startServer(options = {}) {
214
227
 
215
228
  proxyServer.listen(proxyPort);
216
229
 
217
- return {
230
+ const controller = {
218
231
  webServer,
219
232
  proxyServer,
220
233
  close(callback) {
@@ -243,6 +256,15 @@ function startServer(options = {}) {
243
256
  proxyServer.close((error) => done(error));
244
257
  },
245
258
  };
259
+
260
+ process.removeAllListeners("codex-switcher:shutdown");
261
+ process.on("codex-switcher:shutdown", () => {
262
+ controller.close(() => {
263
+ process.exit(0);
264
+ });
265
+ });
266
+
267
+ return controller;
246
268
  }
247
269
 
248
270
  if (require.main === module) {