codex-endpoint-switcher 1.0.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 +70 -0
- package/bin/codex-switcher.js +89 -0
- package/install-web-access.ps1 +61 -0
- package/open-web-console.cmd +3 -0
- package/open-web-console.vbs +2 -0
- package/package.json +74 -0
- package/remove-web-access.ps1 +17 -0
- package/src/main/main.js +74 -0
- package/src/main/preload.js +28 -0
- package/src/main/profile-manager.js +439 -0
- package/src/renderer/index.html +140 -0
- package/src/renderer/renderer.js +340 -0
- package/src/renderer/styles.css +507 -0
- package/src/web/launcher.js +248 -0
- package/src/web/server.js +132 -0
- package/start-web-background.cmd +3 -0
- package/start-web-background.vbs +2 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const http = require("node:http");
|
|
4
|
+
const { exec, spawn } = require("node:child_process");
|
|
5
|
+
|
|
6
|
+
const projectRoot = path.resolve(__dirname, "../..");
|
|
7
|
+
const runtimeDir = path.join(projectRoot, "runtime");
|
|
8
|
+
const serverEntryPath = path.join(__dirname, "server.js");
|
|
9
|
+
const port = Number(process.env.PORT || 3186);
|
|
10
|
+
const healthUrl = `http://127.0.0.1:${port}/api/health`;
|
|
11
|
+
const consoleUrl = `http://localhost:${port}`;
|
|
12
|
+
|
|
13
|
+
function ensureRuntimeDir() {
|
|
14
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requestJson(url, timeoutMs = 1200) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const request = http.get(url, (response) => {
|
|
20
|
+
let data = "";
|
|
21
|
+
response.setEncoding("utf8");
|
|
22
|
+
response.on("data", (chunk) => {
|
|
23
|
+
data += chunk;
|
|
24
|
+
});
|
|
25
|
+
response.on("end", () => {
|
|
26
|
+
try {
|
|
27
|
+
resolve(JSON.parse(data));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
reject(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
request.setTimeout(timeoutMs, () => {
|
|
35
|
+
request.destroy(new Error("请求超时"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
request.on("error", reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 检查本地网页服务是否已经可用。
|
|
44
|
+
*/
|
|
45
|
+
async function checkServerHealth() {
|
|
46
|
+
try {
|
|
47
|
+
const payload = await requestJson(healthUrl);
|
|
48
|
+
return Boolean(payload && payload.ok);
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sleep(ms) {
|
|
55
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function waitForServer(maxWaitMs = 20000) {
|
|
59
|
+
const startedAt = Date.now();
|
|
60
|
+
|
|
61
|
+
while (Date.now() - startedAt < maxWaitMs) {
|
|
62
|
+
if (await checkServerHealth()) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await sleep(500);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 以脱离终端的方式启动本地网页服务。
|
|
74
|
+
*/
|
|
75
|
+
function spawnServerInBackground() {
|
|
76
|
+
ensureRuntimeDir();
|
|
77
|
+
const stdoutPath = path.join(runtimeDir, "web-server.out.log");
|
|
78
|
+
const stderrPath = path.join(runtimeDir, "web-server.err.log");
|
|
79
|
+
const stdoutFd = fs.openSync(stdoutPath, "a");
|
|
80
|
+
const stderrFd = fs.openSync(stderrPath, "a");
|
|
81
|
+
|
|
82
|
+
const child = spawn(process.execPath, [serverEntryPath], {
|
|
83
|
+
cwd: projectRoot,
|
|
84
|
+
detached: true,
|
|
85
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
86
|
+
env: {
|
|
87
|
+
...process.env,
|
|
88
|
+
AUTO_OPEN_BROWSER: "false",
|
|
89
|
+
PORT: String(port),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
child.unref();
|
|
94
|
+
return child.pid;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function ensureServerRunning() {
|
|
98
|
+
if (await checkServerHealth()) {
|
|
99
|
+
return {
|
|
100
|
+
started: false,
|
|
101
|
+
running: true,
|
|
102
|
+
url: consoleUrl,
|
|
103
|
+
port,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pid = spawnServerInBackground();
|
|
108
|
+
const ready = await waitForServer();
|
|
109
|
+
|
|
110
|
+
if (!ready) {
|
|
111
|
+
throw new Error("本地网页服务启动失败,请检查 runtime 日志。");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
started: true,
|
|
116
|
+
running: true,
|
|
117
|
+
url: consoleUrl,
|
|
118
|
+
port,
|
|
119
|
+
pid,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function openBrowser(url) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
exec(`cmd /c start "" "${url}"`, (error) => {
|
|
126
|
+
if (error) {
|
|
127
|
+
reject(error);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
resolve();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getListeningPid() {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
exec(`cmd /c netstat -ano | findstr :${port}`, (error, stdout) => {
|
|
139
|
+
if (error) {
|
|
140
|
+
resolve(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const lines = stdout
|
|
145
|
+
.split(/\r?\n/)
|
|
146
|
+
.map((line) => line.trim())
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const parts = line.split(/\s+/);
|
|
151
|
+
const state = parts[3];
|
|
152
|
+
const pid = Number(parts[4]);
|
|
153
|
+
if (state === "LISTENING" && Number.isFinite(pid)) {
|
|
154
|
+
resolve(pid);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resolve(null);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function restartServer() {
|
|
165
|
+
const pid = await getListeningPid();
|
|
166
|
+
if (pid) {
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
exec(`cmd /c taskkill /PID ${pid} /F`, (error) => {
|
|
169
|
+
if (error) {
|
|
170
|
+
reject(error);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
await sleep(800);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return ensureServerRunning();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function handleCommand(command) {
|
|
184
|
+
switch (command) {
|
|
185
|
+
case "ensure":
|
|
186
|
+
case "start": {
|
|
187
|
+
const result = await ensureServerRunning();
|
|
188
|
+
console.log(JSON.stringify(result, null, 2));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case "open": {
|
|
192
|
+
const result = await ensureServerRunning();
|
|
193
|
+
await openBrowser(result.url);
|
|
194
|
+
console.log(
|
|
195
|
+
JSON.stringify(
|
|
196
|
+
{
|
|
197
|
+
...result,
|
|
198
|
+
opened: true,
|
|
199
|
+
},
|
|
200
|
+
null,
|
|
201
|
+
2,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
case "status": {
|
|
207
|
+
const running = await checkServerHealth();
|
|
208
|
+
console.log(
|
|
209
|
+
JSON.stringify(
|
|
210
|
+
{
|
|
211
|
+
running,
|
|
212
|
+
url: consoleUrl,
|
|
213
|
+
port,
|
|
214
|
+
},
|
|
215
|
+
null,
|
|
216
|
+
2,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
case "restart": {
|
|
222
|
+
const result = await restartServer();
|
|
223
|
+
console.log(JSON.stringify({ ...result, restarted: true }, null, 2));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
throw new Error("不支持的命令。可用命令:ensure、open、status、restart");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (require.main === module) {
|
|
232
|
+
const command = process.argv[2] || "status";
|
|
233
|
+
handleCommand(command).catch((error) => {
|
|
234
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
checkServerHealth,
|
|
241
|
+
ensureServerRunning,
|
|
242
|
+
handleCommand,
|
|
243
|
+
restartServer,
|
|
244
|
+
openBrowser,
|
|
245
|
+
runtimeDir,
|
|
246
|
+
consoleUrl,
|
|
247
|
+
port,
|
|
248
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
const { exec } = require("node:child_process");
|
|
3
|
+
const express = require("express");
|
|
4
|
+
const profileManager = require("../main/profile-manager");
|
|
5
|
+
|
|
6
|
+
function wrapAsync(handler) {
|
|
7
|
+
return async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const data = await handler(req, res);
|
|
10
|
+
res.json({ ok: true, data });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
res.status(400).json({
|
|
13
|
+
ok: false,
|
|
14
|
+
error: error instanceof Error ? error.message : String(error),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createApp() {
|
|
21
|
+
const app = express();
|
|
22
|
+
const rendererRoot = path.join(__dirname, "../renderer");
|
|
23
|
+
|
|
24
|
+
app.use(express.json({ limit: "2mb" }));
|
|
25
|
+
app.use(express.static(rendererRoot));
|
|
26
|
+
|
|
27
|
+
app.get("/api/health", (_req, res) => {
|
|
28
|
+
res.json({ ok: true, data: { status: "ok" } });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.get(
|
|
32
|
+
"/api/current",
|
|
33
|
+
wrapAsync(async () => {
|
|
34
|
+
return profileManager.getCurrentEndpointSummary();
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
app.get(
|
|
39
|
+
"/api/endpoints",
|
|
40
|
+
wrapAsync(async () => {
|
|
41
|
+
return profileManager.listEndpoints();
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
app.post(
|
|
46
|
+
"/api/endpoints",
|
|
47
|
+
wrapAsync(async (req) => {
|
|
48
|
+
return profileManager.createEndpoint(req.body);
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
app.put(
|
|
53
|
+
"/api/endpoints/:id",
|
|
54
|
+
wrapAsync(async (req) => {
|
|
55
|
+
return profileManager.updateEndpoint(req.params.id, req.body);
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
app.delete(
|
|
60
|
+
"/api/endpoints/:id",
|
|
61
|
+
wrapAsync(async (req) => {
|
|
62
|
+
return profileManager.deleteEndpoint(req.params.id);
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
app.post(
|
|
67
|
+
"/api/endpoints/switch",
|
|
68
|
+
wrapAsync(async (req) => {
|
|
69
|
+
return profileManager.switchEndpoint(req.body.id);
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
app.get(
|
|
74
|
+
"/api/paths",
|
|
75
|
+
wrapAsync(async () => {
|
|
76
|
+
return profileManager.getManagedPaths();
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
app.post(
|
|
81
|
+
"/api/open-path",
|
|
82
|
+
wrapAsync(async (req) => {
|
|
83
|
+
const targetPath = String(req.body.targetPath || "").trim();
|
|
84
|
+
if (!targetPath) {
|
|
85
|
+
throw new Error("缺少要打开的路径。");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
exec(`cmd /c start "" "${targetPath}"`, (error) => {
|
|
90
|
+
if (error) {
|
|
91
|
+
reject(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resolve();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { targetPath };
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
app.get("/{*any}", (_req, res) => {
|
|
104
|
+
res.sendFile(path.join(rendererRoot, "index.html"));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return app;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function startServer(options = {}) {
|
|
111
|
+
const port = Number(options.port || process.env.PORT || 3186);
|
|
112
|
+
const app = createApp();
|
|
113
|
+
const server = app.listen(port, () => {
|
|
114
|
+
const url = `http://localhost:${port}`;
|
|
115
|
+
console.log(`Codex 网页控制台已启动:${url}`);
|
|
116
|
+
|
|
117
|
+
if (options.autoOpen !== false && process.env.AUTO_OPEN_BROWSER !== "false") {
|
|
118
|
+
exec(`cmd /c start "" "${url}"`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return server;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (require.main === module) {
|
|
126
|
+
startServer();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
createApp,
|
|
131
|
+
startServer,
|
|
132
|
+
};
|