codex-endpoint-switcher 1.0.0 → 1.1.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 +103 -0
- package/bin/codex-switcher.js +27 -0
- package/package.json +6 -1
- package/src/main/cloud-sync-client.js +328 -0
- package/src/main/main.js +37 -0
- package/src/main/preload.js +27 -0
- package/src/main/profile-manager.js +478 -42
- package/src/renderer/index.html +83 -0
- package/src/renderer/renderer.js +230 -6
- package/src/renderer/styles.css +45 -2
- package/src/web/cloud-sync-server.js +419 -0
- package/src/web/launcher.js +6 -1
- package/src/web/proxy-server.js +69 -0
- package/src/web/server.js +100 -2
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const { randomBytes, scryptSync, timingSafeEqual } = require("node:crypto");
|
|
5
|
+
const express = require("express");
|
|
6
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
7
|
+
|
|
8
|
+
const defaultRoot = path.join(os.homedir(), ".codex-cloud-sync");
|
|
9
|
+
const defaultDbPath = path.join(defaultRoot, "cloud-sync.db");
|
|
10
|
+
const defaultHost = process.env.SYNC_SERVER_HOST || "127.0.0.1";
|
|
11
|
+
const defaultPort = Number(process.env.SYNC_SERVER_PORT || 3190);
|
|
12
|
+
const sessionTtlDays = Number(process.env.SYNC_SERVER_SESSION_DAYS || 30);
|
|
13
|
+
|
|
14
|
+
function wrapAsync(handler) {
|
|
15
|
+
return async (req, res, next) => {
|
|
16
|
+
try {
|
|
17
|
+
const data = await handler(req, res);
|
|
18
|
+
res.json({ ok: true, data });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
next(error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nowIso() {
|
|
26
|
+
return new Date().toISOString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function addDaysToNow(days) {
|
|
30
|
+
return new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeUsername(value) {
|
|
34
|
+
const username = String(value || "").trim();
|
|
35
|
+
if (!username) {
|
|
36
|
+
throw new Error("账号不能为空。");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (username.length < 3 || username.length > 64) {
|
|
40
|
+
throw new Error("账号长度需要在 3 到 64 个字符之间。");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (/\s/.test(username)) {
|
|
44
|
+
throw new Error("账号不能包含空白字符。");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return username;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizePassword(value) {
|
|
51
|
+
const password = String(value || "");
|
|
52
|
+
if (!password.trim()) {
|
|
53
|
+
throw new Error("密码不能为空。");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (password.length < 6) {
|
|
57
|
+
throw new Error("密码至少需要 6 位。");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return password;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hashPassword(password, salt) {
|
|
64
|
+
return scryptSync(password, salt, 64).toString("base64url");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function verifyPassword(password, salt, expectedHash) {
|
|
68
|
+
const actual = Buffer.from(hashPassword(password, salt), "base64url");
|
|
69
|
+
const expected = Buffer.from(String(expectedHash || ""), "base64url");
|
|
70
|
+
|
|
71
|
+
if (actual.length !== expected.length) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return timingSafeEqual(actual, expected);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ensureParentDir(filePath) {
|
|
79
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createDatabase(dbPath) {
|
|
83
|
+
ensureParentDir(dbPath);
|
|
84
|
+
const db = new DatabaseSync(dbPath);
|
|
85
|
+
|
|
86
|
+
db.exec(`
|
|
87
|
+
PRAGMA journal_mode = WAL;
|
|
88
|
+
PRAGMA foreign_keys = ON;
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
username TEXT NOT NULL UNIQUE,
|
|
93
|
+
password_salt TEXT NOT NULL,
|
|
94
|
+
password_hash TEXT NOT NULL,
|
|
95
|
+
created_at TEXT NOT NULL,
|
|
96
|
+
updated_at TEXT NOT NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
100
|
+
token TEXT PRIMARY KEY,
|
|
101
|
+
user_id INTEGER NOT NULL,
|
|
102
|
+
expires_at TEXT NOT NULL,
|
|
103
|
+
created_at TEXT NOT NULL,
|
|
104
|
+
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
CREATE TABLE IF NOT EXISTS sync_profiles (
|
|
108
|
+
user_id INTEGER PRIMARY KEY,
|
|
109
|
+
sync_code TEXT NOT NULL,
|
|
110
|
+
endpoint_count INTEGER NOT NULL DEFAULT 0,
|
|
111
|
+
updated_at TEXT NOT NULL,
|
|
112
|
+
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
113
|
+
);
|
|
114
|
+
`);
|
|
115
|
+
|
|
116
|
+
return db;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function clearExpiredSessions(db) {
|
|
120
|
+
db.prepare("DELETE FROM sessions WHERE expires_at <= ?").run(nowIso());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createSession(db, userId) {
|
|
124
|
+
clearExpiredSessions(db);
|
|
125
|
+
const token = randomBytes(32).toString("base64url");
|
|
126
|
+
const createdAt = nowIso();
|
|
127
|
+
const expiresAt = addDaysToNow(sessionTtlDays);
|
|
128
|
+
|
|
129
|
+
db.prepare(
|
|
130
|
+
`
|
|
131
|
+
INSERT INTO sessions (token, user_id, expires_at, created_at)
|
|
132
|
+
VALUES (?, ?, ?, ?)
|
|
133
|
+
`,
|
|
134
|
+
).run(token, userId, expiresAt, createdAt);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
token,
|
|
138
|
+
expiresAt,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getUserByUsername(db, username) {
|
|
143
|
+
return (
|
|
144
|
+
db
|
|
145
|
+
.prepare(
|
|
146
|
+
`
|
|
147
|
+
SELECT id, username, password_salt AS passwordSalt, password_hash AS passwordHash
|
|
148
|
+
FROM users
|
|
149
|
+
WHERE username = ?
|
|
150
|
+
`,
|
|
151
|
+
)
|
|
152
|
+
.get(username) || null
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getSessionWithUser(db, token) {
|
|
157
|
+
clearExpiredSessions(db);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
db
|
|
161
|
+
.prepare(
|
|
162
|
+
`
|
|
163
|
+
SELECT
|
|
164
|
+
sessions.token AS token,
|
|
165
|
+
sessions.expires_at AS expiresAt,
|
|
166
|
+
users.id AS userId,
|
|
167
|
+
users.username AS username
|
|
168
|
+
FROM sessions
|
|
169
|
+
JOIN users ON users.id = sessions.user_id
|
|
170
|
+
WHERE sessions.token = ? AND sessions.expires_at > ?
|
|
171
|
+
`,
|
|
172
|
+
)
|
|
173
|
+
.get(token, nowIso()) || null
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getSyncProfile(db, userId) {
|
|
178
|
+
return (
|
|
179
|
+
db
|
|
180
|
+
.prepare(
|
|
181
|
+
`
|
|
182
|
+
SELECT
|
|
183
|
+
sync_code AS syncCode,
|
|
184
|
+
endpoint_count AS endpointCount,
|
|
185
|
+
updated_at AS updatedAt
|
|
186
|
+
FROM sync_profiles
|
|
187
|
+
WHERE user_id = ?
|
|
188
|
+
`,
|
|
189
|
+
)
|
|
190
|
+
.get(userId) || null
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildAuthMiddleware(db) {
|
|
195
|
+
return (req, _res, next) => {
|
|
196
|
+
const authorization = String(req.headers.authorization || "").trim();
|
|
197
|
+
const matched = authorization.match(/^Bearer\s+(.+)$/i);
|
|
198
|
+
if (!matched) {
|
|
199
|
+
const error = new Error("缺少登录令牌。");
|
|
200
|
+
error.statusCode = 401;
|
|
201
|
+
next(error);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const session = getSessionWithUser(db, matched[1].trim());
|
|
206
|
+
if (!session) {
|
|
207
|
+
const error = new Error("登录令牌无效或已过期,请重新登录。");
|
|
208
|
+
error.statusCode = 401;
|
|
209
|
+
next(error);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
req.auth = session;
|
|
214
|
+
next();
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function createCloudSyncApp(options = {}) {
|
|
219
|
+
const dbPath = path.resolve(options.dbPath || process.env.SYNC_SERVER_DB_PATH || defaultDbPath);
|
|
220
|
+
const host = options.host || defaultHost;
|
|
221
|
+
const port = Number(options.port || defaultPort);
|
|
222
|
+
const db = createDatabase(dbPath);
|
|
223
|
+
const authRequired = buildAuthMiddleware(db);
|
|
224
|
+
const app = express();
|
|
225
|
+
|
|
226
|
+
app.use(express.json({ limit: "2mb" }));
|
|
227
|
+
|
|
228
|
+
app.get("/api/health", (_req, res) => {
|
|
229
|
+
res.json({
|
|
230
|
+
ok: true,
|
|
231
|
+
data: {
|
|
232
|
+
status: "ok",
|
|
233
|
+
host,
|
|
234
|
+
port,
|
|
235
|
+
dbPath,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
app.post(
|
|
241
|
+
"/api/auth/register",
|
|
242
|
+
wrapAsync(async (req) => {
|
|
243
|
+
const username = normalizeUsername(req.body.username);
|
|
244
|
+
const password = normalizePassword(req.body.password);
|
|
245
|
+
const existing = getUserByUsername(db, username);
|
|
246
|
+
|
|
247
|
+
if (existing) {
|
|
248
|
+
const error = new Error("这个账号已经存在,请直接登录。");
|
|
249
|
+
error.statusCode = 409;
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const salt = randomBytes(16).toString("base64url");
|
|
254
|
+
const createdAt = nowIso();
|
|
255
|
+
const inserted = db
|
|
256
|
+
.prepare(
|
|
257
|
+
`
|
|
258
|
+
INSERT INTO users (username, password_salt, password_hash, created_at, updated_at)
|
|
259
|
+
VALUES (?, ?, ?, ?, ?)
|
|
260
|
+
`,
|
|
261
|
+
)
|
|
262
|
+
.run(username, salt, hashPassword(password, salt), createdAt, createdAt);
|
|
263
|
+
const userId = Number(inserted.lastInsertRowid);
|
|
264
|
+
const session = createSession(db, userId);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
token: session.token,
|
|
268
|
+
expiresAt: session.expiresAt,
|
|
269
|
+
user: {
|
|
270
|
+
id: userId,
|
|
271
|
+
username,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
app.post(
|
|
278
|
+
"/api/auth/login",
|
|
279
|
+
wrapAsync(async (req) => {
|
|
280
|
+
const username = normalizeUsername(req.body.username);
|
|
281
|
+
const password = normalizePassword(req.body.password);
|
|
282
|
+
const user = getUserByUsername(db, username);
|
|
283
|
+
|
|
284
|
+
if (!user || !verifyPassword(password, user.passwordSalt, user.passwordHash)) {
|
|
285
|
+
const error = new Error("账号或密码不正确。");
|
|
286
|
+
error.statusCode = 401;
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const session = createSession(db, user.id);
|
|
291
|
+
return {
|
|
292
|
+
token: session.token,
|
|
293
|
+
expiresAt: session.expiresAt,
|
|
294
|
+
user: {
|
|
295
|
+
id: user.id,
|
|
296
|
+
username: user.username,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
app.post(
|
|
303
|
+
"/api/auth/logout",
|
|
304
|
+
authRequired,
|
|
305
|
+
wrapAsync(async (req) => {
|
|
306
|
+
db.prepare("DELETE FROM sessions WHERE token = ?").run(req.auth.token);
|
|
307
|
+
return {
|
|
308
|
+
loggedOut: true,
|
|
309
|
+
};
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
app.get(
|
|
314
|
+
"/api/auth/me",
|
|
315
|
+
authRequired,
|
|
316
|
+
wrapAsync(async (req) => {
|
|
317
|
+
const profile = getSyncProfile(db, req.auth.userId);
|
|
318
|
+
return {
|
|
319
|
+
user: {
|
|
320
|
+
id: req.auth.userId,
|
|
321
|
+
username: req.auth.username,
|
|
322
|
+
},
|
|
323
|
+
session: {
|
|
324
|
+
expiresAt: req.auth.expiresAt,
|
|
325
|
+
},
|
|
326
|
+
syncProfileUpdatedAt: profile ? profile.updatedAt : "",
|
|
327
|
+
};
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
app.put(
|
|
332
|
+
"/api/sync/config",
|
|
333
|
+
authRequired,
|
|
334
|
+
wrapAsync(async (req) => {
|
|
335
|
+
const syncCode = String(req.body.syncCode || "").trim();
|
|
336
|
+
const endpointCount = Math.max(0, Number(req.body.endpointCount || 0));
|
|
337
|
+
|
|
338
|
+
if (!syncCode) {
|
|
339
|
+
throw new Error("缺少要同步的配置数据。");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const updatedAt = nowIso();
|
|
343
|
+
db.prepare(
|
|
344
|
+
`
|
|
345
|
+
INSERT INTO sync_profiles (user_id, sync_code, endpoint_count, updated_at)
|
|
346
|
+
VALUES (?, ?, ?, ?)
|
|
347
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
348
|
+
sync_code = excluded.sync_code,
|
|
349
|
+
endpoint_count = excluded.endpoint_count,
|
|
350
|
+
updated_at = excluded.updated_at
|
|
351
|
+
`,
|
|
352
|
+
).run(req.auth.userId, syncCode, endpointCount, updatedAt);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
endpointCount,
|
|
356
|
+
updatedAt,
|
|
357
|
+
};
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
app.get(
|
|
362
|
+
"/api/sync/config",
|
|
363
|
+
authRequired,
|
|
364
|
+
wrapAsync(async (req) => {
|
|
365
|
+
const profile = getSyncProfile(db, req.auth.userId);
|
|
366
|
+
if (!profile) {
|
|
367
|
+
const error = new Error("当前账号还没有保存过同步配置。");
|
|
368
|
+
error.statusCode = 404;
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return profile;
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
app.use((error, _req, res, _next) => {
|
|
377
|
+
const statusCode = Number(error.statusCode || 400);
|
|
378
|
+
res.status(statusCode).json({
|
|
379
|
+
ok: false,
|
|
380
|
+
error: error instanceof Error ? error.message : String(error),
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
app,
|
|
386
|
+
db,
|
|
387
|
+
dbPath,
|
|
388
|
+
host,
|
|
389
|
+
port,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function startCloudSyncServer(options = {}) {
|
|
394
|
+
const context = createCloudSyncApp(options);
|
|
395
|
+
const server = context.app.listen(context.port, context.host, () => {
|
|
396
|
+
console.log(`Codex 账号同步服务已启动:http://${context.host}:${context.port}`);
|
|
397
|
+
console.log(`SQLite 数据库:${context.dbPath}`);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
...context,
|
|
402
|
+
server,
|
|
403
|
+
close(callback) {
|
|
404
|
+
server.close((error) => {
|
|
405
|
+
context.db.close();
|
|
406
|
+
callback(error);
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (require.main === module) {
|
|
413
|
+
startCloudSyncServer();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = {
|
|
417
|
+
createCloudSyncApp,
|
|
418
|
+
startCloudSyncServer,
|
|
419
|
+
};
|
package/src/web/launcher.js
CHANGED
|
@@ -7,7 +7,9 @@ const projectRoot = path.resolve(__dirname, "../..");
|
|
|
7
7
|
const runtimeDir = path.join(projectRoot, "runtime");
|
|
8
8
|
const serverEntryPath = path.join(__dirname, "server.js");
|
|
9
9
|
const port = Number(process.env.PORT || 3186);
|
|
10
|
+
const proxyPort = 3187;
|
|
10
11
|
const healthUrl = `http://127.0.0.1:${port}/api/health`;
|
|
12
|
+
const proxyHealthUrl = `http://127.0.0.1:${proxyPort}/__switcher/health`;
|
|
11
13
|
const consoleUrl = `http://localhost:${port}`;
|
|
12
14
|
|
|
13
15
|
function ensureRuntimeDir() {
|
|
@@ -45,7 +47,8 @@ function requestJson(url, timeoutMs = 1200) {
|
|
|
45
47
|
async function checkServerHealth() {
|
|
46
48
|
try {
|
|
47
49
|
const payload = await requestJson(healthUrl);
|
|
48
|
-
|
|
50
|
+
const proxyPayload = await requestJson(proxyHealthUrl);
|
|
51
|
+
return Boolean(payload && payload.ok && proxyPayload && proxyPayload.ok);
|
|
49
52
|
} catch {
|
|
50
53
|
return false;
|
|
51
54
|
}
|
|
@@ -211,6 +214,7 @@ async function handleCommand(command) {
|
|
|
211
214
|
running,
|
|
212
215
|
url: consoleUrl,
|
|
213
216
|
port,
|
|
217
|
+
proxyPort,
|
|
214
218
|
},
|
|
215
219
|
null,
|
|
216
220
|
2,
|
|
@@ -245,4 +249,5 @@ module.exports = {
|
|
|
245
249
|
runtimeDir,
|
|
246
250
|
consoleUrl,
|
|
247
251
|
port,
|
|
252
|
+
proxyPort,
|
|
248
253
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const http = require("node:http");
|
|
2
|
+
const https = require("node:https");
|
|
3
|
+
const profileManager = require("../main/profile-manager");
|
|
4
|
+
|
|
5
|
+
function createProxyServer() {
|
|
6
|
+
return http.createServer(async (req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
if (req.url === "/__switcher/health") {
|
|
9
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
10
|
+
res.end(JSON.stringify({ ok: true, proxyBaseUrl: profileManager.proxyBaseUrl }));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const target = await profileManager.getProxyTarget();
|
|
15
|
+
const upstreamBase = new URL(target.url);
|
|
16
|
+
const requestPath = req.url && req.url.startsWith("/") ? req.url : `/${req.url || ""}`;
|
|
17
|
+
const upstreamUrl = new URL(requestPath, upstreamBase);
|
|
18
|
+
const requestModule = upstreamUrl.protocol === "https:" ? https : http;
|
|
19
|
+
const headers = {
|
|
20
|
+
...req.headers,
|
|
21
|
+
host: upstreamUrl.host,
|
|
22
|
+
authorization: `Bearer ${target.key}`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
delete headers.connection;
|
|
26
|
+
|
|
27
|
+
const upstreamRequest = requestModule.request(
|
|
28
|
+
upstreamUrl,
|
|
29
|
+
{
|
|
30
|
+
method: req.method,
|
|
31
|
+
headers,
|
|
32
|
+
},
|
|
33
|
+
(upstreamResponse) => {
|
|
34
|
+
res.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
|
|
35
|
+
upstreamResponse.pipe(res);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
upstreamRequest.on("error", (error) => {
|
|
40
|
+
if (!res.headersSent) {
|
|
41
|
+
res.writeHead(502, { "Content-Type": "application/json; charset=utf-8" });
|
|
42
|
+
}
|
|
43
|
+
res.end(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
ok: false,
|
|
46
|
+
error: `代理转发失败:${error.message}`,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
req.pipe(upstreamRequest);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (!res.headersSent) {
|
|
54
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
res.end(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
ok: false,
|
|
60
|
+
error: error instanceof Error ? error.message : String(error),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
createProxyServer,
|
|
69
|
+
};
|
package/src/web/server.js
CHANGED
|
@@ -2,6 +2,8 @@ const path = require("node:path");
|
|
|
2
2
|
const { exec } = require("node:child_process");
|
|
3
3
|
const express = require("express");
|
|
4
4
|
const profileManager = require("../main/profile-manager");
|
|
5
|
+
const cloudSyncClient = require("../main/cloud-sync-client");
|
|
6
|
+
const { createProxyServer } = require("./proxy-server");
|
|
5
7
|
|
|
6
8
|
function wrapAsync(handler) {
|
|
7
9
|
return async (req, res) => {
|
|
@@ -77,6 +79,69 @@ function createApp() {
|
|
|
77
79
|
}),
|
|
78
80
|
);
|
|
79
81
|
|
|
82
|
+
app.post(
|
|
83
|
+
"/api/proxy/enable",
|
|
84
|
+
wrapAsync(async () => {
|
|
85
|
+
return profileManager.enableProxyMode();
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
app.get(
|
|
90
|
+
"/api/sync/export",
|
|
91
|
+
wrapAsync(async () => {
|
|
92
|
+
return profileManager.exportSyncPackage();
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
app.post(
|
|
97
|
+
"/api/sync/import",
|
|
98
|
+
wrapAsync(async (req) => {
|
|
99
|
+
return profileManager.importSyncPackage(req.body.syncCode, req.body.mode);
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
app.get(
|
|
104
|
+
"/api/cloud/status",
|
|
105
|
+
wrapAsync(async () => {
|
|
106
|
+
return cloudSyncClient.getCloudSyncStatus();
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
app.post(
|
|
111
|
+
"/api/cloud/register",
|
|
112
|
+
wrapAsync(async (req) => {
|
|
113
|
+
return cloudSyncClient.registerAccount(req.body);
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
app.post(
|
|
118
|
+
"/api/cloud/login",
|
|
119
|
+
wrapAsync(async (req) => {
|
|
120
|
+
return cloudSyncClient.loginAccount(req.body);
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
app.post(
|
|
125
|
+
"/api/cloud/logout",
|
|
126
|
+
wrapAsync(async () => {
|
|
127
|
+
return cloudSyncClient.logoutAccount();
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
app.post(
|
|
132
|
+
"/api/cloud/push",
|
|
133
|
+
wrapAsync(async () => {
|
|
134
|
+
return cloudSyncClient.pushCurrentConfig();
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
app.post(
|
|
139
|
+
"/api/cloud/pull",
|
|
140
|
+
wrapAsync(async (req) => {
|
|
141
|
+
return cloudSyncClient.pullRemoteConfig(req.body.mode);
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
80
145
|
app.post(
|
|
81
146
|
"/api/open-path",
|
|
82
147
|
wrapAsync(async (req) => {
|
|
@@ -110,16 +175,49 @@ function createApp() {
|
|
|
110
175
|
function startServer(options = {}) {
|
|
111
176
|
const port = Number(options.port || process.env.PORT || 3186);
|
|
112
177
|
const app = createApp();
|
|
113
|
-
const
|
|
178
|
+
const proxyServer = createProxyServer();
|
|
179
|
+
const proxyPort = profileManager.proxyPort;
|
|
180
|
+
const webServer = app.listen(port, () => {
|
|
114
181
|
const url = `http://localhost:${port}`;
|
|
115
182
|
console.log(`Codex 网页控制台已启动:${url}`);
|
|
183
|
+
console.log(`Codex 热更新代理已启动:${profileManager.proxyBaseUrl}`);
|
|
116
184
|
|
|
117
185
|
if (options.autoOpen !== false && process.env.AUTO_OPEN_BROWSER !== "false") {
|
|
118
186
|
exec(`cmd /c start "" "${url}"`);
|
|
119
187
|
}
|
|
120
188
|
});
|
|
121
189
|
|
|
122
|
-
|
|
190
|
+
proxyServer.listen(proxyPort);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
webServer,
|
|
194
|
+
proxyServer,
|
|
195
|
+
close(callback) {
|
|
196
|
+
let pending = 2;
|
|
197
|
+
let closed = false;
|
|
198
|
+
|
|
199
|
+
function done(error) {
|
|
200
|
+
if (closed) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (error) {
|
|
205
|
+
closed = true;
|
|
206
|
+
callback(error);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
pending -= 1;
|
|
211
|
+
if (pending === 0) {
|
|
212
|
+
closed = true;
|
|
213
|
+
callback();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
webServer.close((error) => done(error));
|
|
218
|
+
proxyServer.close((error) => done(error));
|
|
219
|
+
},
|
|
220
|
+
};
|
|
123
221
|
}
|
|
124
222
|
|
|
125
223
|
if (require.main === module) {
|