codex-endpoint-switcher 1.3.1 → 1.4.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 +15 -2
- package/bin/codex-switcher.js +21 -0
- package/package.json +1 -1
- package/src/main/main.js +5 -0
- package/src/main/preload.js +3 -0
- package/src/main/update-service.js +122 -0
- package/src/renderer/index.html +44 -1
- package/src/renderer/renderer.js +108 -3
- package/src/renderer/styles.css +185 -18
- package/src/web/proxy-server.js +152 -24
- package/src/web/server.js +14 -1
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ codex-switcher start
|
|
|
49
49
|
codex-switcher status
|
|
50
50
|
codex-switcher restart
|
|
51
51
|
codex-switcher stop
|
|
52
|
+
codex-switcher check-update
|
|
52
53
|
codex-switcher sync-server
|
|
53
54
|
codex-switcher install-access
|
|
54
55
|
codex-switcher remove-access
|
|
@@ -61,6 +62,7 @@ codex-switcher remove-access
|
|
|
61
62
|
- `status`:查看当前服务状态
|
|
62
63
|
- `restart`:重启本地网页服务
|
|
63
64
|
- `stop`:关闭本地网页服务
|
|
65
|
+
- `check-update`:检查 npm 是否有新版本
|
|
64
66
|
- `sync-server`:启动账号同步服务,服务端使用 SQLite 单文件数据库
|
|
65
67
|
- `install-access`:创建桌面快捷方式和开机启动项
|
|
66
68
|
- `remove-access`:移除桌面快捷方式和开机启动项
|
|
@@ -75,7 +77,7 @@ http://localhost:3186
|
|
|
75
77
|
|
|
76
78
|
## 账号配置同步
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
网页控制台现在支持“账号 + 密码”的同步方式。
|
|
79
81
|
|
|
80
82
|
### 1. 启动同步服务
|
|
81
83
|
|
|
@@ -115,7 +117,6 @@ codex-switcher sync-server
|
|
|
115
117
|
|
|
116
118
|
在客户端网页控制台里填写:
|
|
117
119
|
|
|
118
|
-
- 同步服务器地址
|
|
119
120
|
- 账号
|
|
120
121
|
- 密码
|
|
121
122
|
|
|
@@ -135,6 +136,18 @@ codex-switcher sync-server
|
|
|
135
136
|
- 拉取前会自动备份本机 `~/.codex/endpoint-presets.json` 和 `~/.codex/endpoint-switcher-state.json`
|
|
136
137
|
- 服务端数据库是 SQLite,部署只需要一个 Node 进程和一个 `.db` 文件
|
|
137
138
|
|
|
139
|
+
## 版本更新提示
|
|
140
|
+
|
|
141
|
+
- 控制台打开后会自动检查 npm 最新版本
|
|
142
|
+
- 如果你发布了新版本,其他用户打开控制台后会看到“发现新版本”的提示
|
|
143
|
+
- 用户可以直接复制升级命令执行
|
|
144
|
+
|
|
145
|
+
也可以用 CLI 手动检查:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
codex-switcher check-update
|
|
149
|
+
```
|
|
150
|
+
|
|
138
151
|
## 更新 npm 包
|
|
139
152
|
|
|
140
153
|
先进入项目目录:
|
package/bin/codex-switcher.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
const { spawn } = require("node:child_process");
|
|
5
5
|
const { handleCommand } = require("../src/web/launcher");
|
|
6
|
+
const updateService = require("../src/main/update-service");
|
|
6
7
|
|
|
7
8
|
const projectRoot = path.resolve(__dirname, "..");
|
|
8
9
|
const installScriptPath = path.join(projectRoot, "install-web-access.ps1");
|
|
@@ -17,6 +18,7 @@ function printUsage() {
|
|
|
17
18
|
codex-switcher status
|
|
18
19
|
codex-switcher restart
|
|
19
20
|
codex-switcher stop
|
|
21
|
+
codex-switcher check-update
|
|
20
22
|
codex-switcher sync-server
|
|
21
23
|
codex-switcher install-access
|
|
22
24
|
codex-switcher remove-access
|
|
@@ -27,6 +29,7 @@ function printUsage() {
|
|
|
27
29
|
status 查看服务状态
|
|
28
30
|
restart 重启本地网页服务
|
|
29
31
|
stop 关闭本地网页服务
|
|
32
|
+
check-update 检查 npm 最新版本
|
|
30
33
|
sync-server 启动账号同步服务(SQLite)
|
|
31
34
|
install-access 创建桌面快捷方式与开机启动项
|
|
32
35
|
remove-access 删除桌面快捷方式与开机启动项
|
|
@@ -89,6 +92,24 @@ async function main() {
|
|
|
89
92
|
await handleCommand(command === "start" ? "ensure" : command);
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
95
|
+
case "check-update": {
|
|
96
|
+
const status = await updateService.getUpdateStatus({ force: true });
|
|
97
|
+
if (status.hasUpdate) {
|
|
98
|
+
console.log(`发现新版本:${status.latestVersion}`);
|
|
99
|
+
console.log(`当前版本:${status.currentVersion}`);
|
|
100
|
+
console.log(`升级命令:${status.upgradeCommand}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (status.lastError) {
|
|
105
|
+
console.log(`当前版本:${status.currentVersion}`);
|
|
106
|
+
console.log(`暂时无法检查更新:${status.lastError}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`当前已是最新版:${status.currentVersion}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
92
113
|
case "sync-server": {
|
|
93
114
|
await runNodeScript(cloudSyncServerPath, ["--experimental-sqlite"]);
|
|
94
115
|
return;
|
package/package.json
CHANGED
package/src/main/main.js
CHANGED
|
@@ -2,6 +2,7 @@ const path = require("node:path");
|
|
|
2
2
|
const { app, BrowserWindow, ipcMain, shell } = require("electron");
|
|
3
3
|
const profileManager = require("./profile-manager");
|
|
4
4
|
const cloudSyncClient = require("./cloud-sync-client");
|
|
5
|
+
const updateService = require("./update-service");
|
|
5
6
|
|
|
6
7
|
function createWindow() {
|
|
7
8
|
const mainWindow = new BrowserWindow({
|
|
@@ -101,6 +102,10 @@ function registerHandlers() {
|
|
|
101
102
|
closing: true,
|
|
102
103
|
};
|
|
103
104
|
});
|
|
105
|
+
|
|
106
|
+
ipcMain.handle("app:updateStatus", async (_event, payload) => {
|
|
107
|
+
return updateService.getUpdateStatus(payload || {});
|
|
108
|
+
});
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
app.whenReady().then(() => {
|
package/src/main/preload.js
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const packageInfo = require("../../package.json");
|
|
2
|
+
|
|
3
|
+
const REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
4
|
+
const SUCCESS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
5
|
+
const ERROR_CACHE_TTL_MS = 30 * 1000;
|
|
6
|
+
const MAX_RETRY_TIMES = 3;
|
|
7
|
+
|
|
8
|
+
let cachedStatus = null;
|
|
9
|
+
|
|
10
|
+
function sleep(timeoutMs) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
setTimeout(resolve, timeoutMs);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseVersion(version) {
|
|
17
|
+
return String(version || "")
|
|
18
|
+
.trim()
|
|
19
|
+
.split(".")
|
|
20
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareVersions(left, right) {
|
|
24
|
+
const leftParts = parseVersion(left);
|
|
25
|
+
const rightParts = parseVersion(right);
|
|
26
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
27
|
+
|
|
28
|
+
for (let index = 0; index < length; index += 1) {
|
|
29
|
+
const leftValue = leftParts[index] || 0;
|
|
30
|
+
const rightValue = rightParts[index] || 0;
|
|
31
|
+
|
|
32
|
+
if (leftValue > rightValue) {
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (leftValue < rightValue) {
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildStatus(payload = {}) {
|
|
45
|
+
const currentVersion = packageInfo.version;
|
|
46
|
+
const latestVersion = payload.latestVersion || currentVersion;
|
|
47
|
+
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
packageName: packageInfo.name,
|
|
51
|
+
currentVersion,
|
|
52
|
+
latestVersion,
|
|
53
|
+
hasUpdate,
|
|
54
|
+
checkedAt: new Date().toISOString(),
|
|
55
|
+
upgradeCommand: `npm install -g ${packageInfo.name}@latest`,
|
|
56
|
+
releaseUrl: `https://www.npmjs.com/package/${packageInfo.name}`,
|
|
57
|
+
lastError: payload.lastError || "",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function fetchLatestPackageVersion() {
|
|
62
|
+
let lastError = null;
|
|
63
|
+
|
|
64
|
+
for (let attempt = 1; attempt <= MAX_RETRY_TIMES; attempt += 1) {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(`${REGISTRY_BASE_URL}/${packageInfo.name}/latest`, {
|
|
67
|
+
headers: {
|
|
68
|
+
Accept: "application/json",
|
|
69
|
+
"User-Agent": `${packageInfo.name}/${packageInfo.version}`,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`npm 仓库请求失败:${response.status}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const payload = await response.json();
|
|
78
|
+
const latestVersion = String(payload.version || "").trim();
|
|
79
|
+
if (!latestVersion) {
|
|
80
|
+
throw new Error("npm 仓库没有返回可用版本号。");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return latestVersion;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
lastError = error;
|
|
86
|
+
|
|
87
|
+
if (attempt < MAX_RETRY_TIMES) {
|
|
88
|
+
await sleep(300 * attempt);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getUpdateStatus(options = {}) {
|
|
97
|
+
const force = Boolean(options.force);
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
!force &&
|
|
101
|
+
cachedStatus &&
|
|
102
|
+
Date.now() - new Date(cachedStatus.checkedAt).getTime() <
|
|
103
|
+
(cachedStatus.lastError ? ERROR_CACHE_TTL_MS : SUCCESS_CACHE_TTL_MS)
|
|
104
|
+
) {
|
|
105
|
+
return cachedStatus;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const latestVersion = await fetchLatestPackageVersion();
|
|
110
|
+
cachedStatus = buildStatus({ latestVersion });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
cachedStatus = buildStatus({
|
|
113
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return cachedStatus;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
getUpdateStatus,
|
|
122
|
+
};
|
package/src/renderer/index.html
CHANGED
|
@@ -14,10 +14,35 @@
|
|
|
14
14
|
<div class="page-shell">
|
|
15
15
|
<section id="authGate" class="auth-gate">
|
|
16
16
|
<div class="auth-card panel">
|
|
17
|
+
<section class="auth-hero-card">
|
|
18
|
+
<p class="eyebrow">CODEX ENDPOINT SWITCHER</p>
|
|
19
|
+
<h1 class="auth-hero-title">登录后直接进入连接控制台</h1>
|
|
20
|
+
<p class="auth-hero-copy">
|
|
21
|
+
连接切换、账号同步、远端拉取都绑定到同一账号空间里,进入后即可直接管理。
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<div class="auth-highlight-list">
|
|
25
|
+
<article class="auth-highlight-item">
|
|
26
|
+
<span class="auth-highlight-label">登录后可用</span>
|
|
27
|
+
<strong>推送当前连接</strong>
|
|
28
|
+
<p>把本机 URL、Key、备注同步到账号空间。</p>
|
|
29
|
+
</article>
|
|
30
|
+
<article class="auth-highlight-item">
|
|
31
|
+
<span class="auth-highlight-label">同步能力</span>
|
|
32
|
+
<strong>合并或覆盖拉取</strong>
|
|
33
|
+
<p>在不同设备之间快速恢复同一套连接配置。</p>
|
|
34
|
+
</article>
|
|
35
|
+
<article class="auth-highlight-item">
|
|
36
|
+
<span class="auth-highlight-label">固定服务</span>
|
|
37
|
+
<strong>自动连接账号同步服务</strong>
|
|
38
|
+
<p>这里只需要输入账号和密码,不需要再配置服务地址。</p>
|
|
39
|
+
</article>
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
17
43
|
<div class="auth-form-card">
|
|
18
44
|
<div class="panel-header auth-panel-header">
|
|
19
45
|
<div>
|
|
20
|
-
<p class="eyebrow">CODEX ENDPOINT SWITCHER</p>
|
|
21
46
|
<p class="panel-kicker">Account Access</p>
|
|
22
47
|
<h2>账号登录</h2>
|
|
23
48
|
</div>
|
|
@@ -84,6 +109,24 @@
|
|
|
84
109
|
<button id="openEndpointStoreButton" class="ghost-button">打开连接数据文件</button>
|
|
85
110
|
<button id="closeAppButton" class="danger-button">关闭程序</button>
|
|
86
111
|
</div>
|
|
112
|
+
<section class="update-strip" aria-label="版本更新状态">
|
|
113
|
+
<div class="update-copy">
|
|
114
|
+
<span class="metric-label">版本更新</span>
|
|
115
|
+
<strong id="updateSummary">正在检查版本...</strong>
|
|
116
|
+
<span id="updateDetail" class="update-detail">
|
|
117
|
+
当前版本 -,正在连接 npm 仓库。
|
|
118
|
+
</span>
|
|
119
|
+
<code id="updateCommand" class="update-command" hidden>
|
|
120
|
+
npm install -g codex-endpoint-switcher@latest
|
|
121
|
+
</code>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="update-actions">
|
|
124
|
+
<button id="checkUpdateButton" type="button" class="ghost-button">检查更新</button>
|
|
125
|
+
<button id="copyUpdateCommandButton" type="button" class="secondary-button" hidden>
|
|
126
|
+
复制升级命令
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</section>
|
|
87
130
|
<div class="current-grid">
|
|
88
131
|
<article class="metric-card">
|
|
89
132
|
<span class="metric-label">当前备注</span>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -3,6 +3,7 @@ const state = {
|
|
|
3
3
|
endpoints: [],
|
|
4
4
|
paths: null,
|
|
5
5
|
cloud: null,
|
|
6
|
+
update: null,
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
function createWebBridge() {
|
|
@@ -105,6 +106,15 @@ function createWebBridge() {
|
|
|
105
106
|
method: "POST",
|
|
106
107
|
});
|
|
107
108
|
},
|
|
109
|
+
getUpdateStatus(options = {}) {
|
|
110
|
+
const searchParams = new URLSearchParams();
|
|
111
|
+
if (options.force) {
|
|
112
|
+
searchParams.set("force", "1");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const query = searchParams.toString();
|
|
116
|
+
return request(`/api/app/update-status${query ? `?${query}` : ""}`);
|
|
117
|
+
},
|
|
108
118
|
};
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -286,6 +296,72 @@ function renderCloudStatus() {
|
|
|
286
296
|
$("#cloudPullReplaceButton").disabled = !cloud.loggedIn;
|
|
287
297
|
}
|
|
288
298
|
|
|
299
|
+
function renderUpdateStatus() {
|
|
300
|
+
const update = state.update;
|
|
301
|
+
if (!update) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const summary = $("#updateSummary");
|
|
306
|
+
const detail = $("#updateDetail");
|
|
307
|
+
const command = $("#updateCommand");
|
|
308
|
+
const copyButton = $("#copyUpdateCommandButton");
|
|
309
|
+
|
|
310
|
+
if (!summary || !detail || !command || !copyButton) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
command.textContent = update.upgradeCommand;
|
|
315
|
+
|
|
316
|
+
if (update.hasUpdate) {
|
|
317
|
+
summary.textContent = `发现新版本 ${update.latestVersion}`;
|
|
318
|
+
detail.textContent = `当前版本 ${update.currentVersion},可以直接升级到最新版本。`;
|
|
319
|
+
command.hidden = false;
|
|
320
|
+
copyButton.hidden = false;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (update.lastError) {
|
|
325
|
+
summary.textContent = "暂时无法检查更新";
|
|
326
|
+
detail.textContent = `当前版本 ${update.currentVersion},原因:${update.lastError}`;
|
|
327
|
+
command.hidden = true;
|
|
328
|
+
copyButton.hidden = true;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
summary.textContent = `当前已是最新版 ${update.currentVersion}`;
|
|
333
|
+
detail.textContent = "npm 包没有检测到更新,可以继续直接使用。";
|
|
334
|
+
command.hidden = true;
|
|
335
|
+
copyButton.hidden = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function refreshUpdateStatus(force = false) {
|
|
339
|
+
const summary = $("#updateSummary");
|
|
340
|
+
const detail = $("#updateDetail");
|
|
341
|
+
|
|
342
|
+
if (summary) {
|
|
343
|
+
summary.textContent = force ? "正在重新检查版本..." : "正在检查版本...";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (detail) {
|
|
347
|
+
detail.textContent = "正在连接 npm 仓库,请稍候。";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
state.update = await bridge.getUpdateStatus({ force });
|
|
352
|
+
renderUpdateStatus();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
state.update = {
|
|
355
|
+
currentVersion: "-",
|
|
356
|
+
latestVersion: "-",
|
|
357
|
+
hasUpdate: false,
|
|
358
|
+
upgradeCommand: "npm install -g codex-endpoint-switcher@latest",
|
|
359
|
+
lastError: error.message,
|
|
360
|
+
};
|
|
361
|
+
renderUpdateStatus();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
289
365
|
function createEndpointCard(endpoint) {
|
|
290
366
|
const card = document.createElement("article");
|
|
291
367
|
card.className = `profile-card ${endpoint.isActive ? "active" : ""}`;
|
|
@@ -348,9 +424,18 @@ function createEndpointCard(endpoint) {
|
|
|
348
424
|
const meta = document.createElement("div");
|
|
349
425
|
meta.className = "profile-meta";
|
|
350
426
|
meta.innerHTML = `
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
427
|
+
<div class="profile-meta-item">
|
|
428
|
+
<span class="profile-meta-label">URL</span>
|
|
429
|
+
<strong class="profile-meta-value">${endpoint.url || "-"}</strong>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="profile-meta-item">
|
|
432
|
+
<span class="profile-meta-label">Key</span>
|
|
433
|
+
<strong class="profile-meta-value">${endpoint.maskedKey || "-"}</strong>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="profile-meta-item">
|
|
436
|
+
<span class="profile-meta-label">更新时间</span>
|
|
437
|
+
<strong class="profile-meta-value">${formatDateTime(endpoint.updatedAt)}</strong>
|
|
438
|
+
</div>
|
|
354
439
|
`;
|
|
355
440
|
|
|
356
441
|
card.append(top, meta);
|
|
@@ -392,6 +477,7 @@ async function bootstrapConsoleAccess() {
|
|
|
392
477
|
const cloud = await bridge.getCloudSyncStatus();
|
|
393
478
|
state.cloud = cloud;
|
|
394
479
|
renderCloudStatus();
|
|
480
|
+
void refreshUpdateStatus();
|
|
395
481
|
|
|
396
482
|
if (!cloud.loggedIn) {
|
|
397
483
|
applyAccessState(false);
|
|
@@ -408,6 +494,7 @@ async function bootstrapConsoleAccess() {
|
|
|
408
494
|
setStatus("已通过账号校验,连接控制台已解锁。", "success");
|
|
409
495
|
} catch (error) {
|
|
410
496
|
applyAccessState(false);
|
|
497
|
+
void refreshUpdateStatus();
|
|
411
498
|
setAuthStatus(`登录校验失败:${error.message}`, "error");
|
|
412
499
|
}
|
|
413
500
|
}
|
|
@@ -659,6 +746,20 @@ async function handleCloseApp() {
|
|
|
659
746
|
}
|
|
660
747
|
}
|
|
661
748
|
|
|
749
|
+
async function handleCopyUpdateCommand() {
|
|
750
|
+
const command = state.update?.upgradeCommand;
|
|
751
|
+
if (!command) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
await navigator.clipboard.writeText(command);
|
|
757
|
+
setStatus("升级命令已复制。", "success");
|
|
758
|
+
} catch (error) {
|
|
759
|
+
setStatus(`复制失败:${error.message}`, "error");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
662
763
|
function bindEvents() {
|
|
663
764
|
$("#authForm").addEventListener("submit", (event) => {
|
|
664
765
|
event.preventDefault();
|
|
@@ -706,6 +807,10 @@ function bindEvents() {
|
|
|
706
807
|
handleOpenPath(state.paths.endpointStorePath, " 连接数据文件");
|
|
707
808
|
}
|
|
708
809
|
});
|
|
810
|
+
$("#checkUpdateButton").addEventListener("click", () => {
|
|
811
|
+
refreshUpdateStatus(true);
|
|
812
|
+
});
|
|
813
|
+
$("#copyUpdateCommandButton").addEventListener("click", handleCopyUpdateCommand);
|
|
709
814
|
$("#closeAppButton").addEventListener("click", handleCloseApp);
|
|
710
815
|
window.addEventListener("keydown", (event) => {
|
|
711
816
|
if (event.key === "Escape" && !$("#endpointModal").hidden) {
|
package/src/renderer/styles.css
CHANGED
|
@@ -101,27 +101,87 @@ code {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
.auth-card {
|
|
104
|
-
width: min(
|
|
104
|
+
width: min(980px, 100%);
|
|
105
105
|
min-height: auto;
|
|
106
|
-
padding:
|
|
106
|
+
padding: 22px;
|
|
107
107
|
border-radius: 36px;
|
|
108
|
-
display:
|
|
108
|
+
display: grid;
|
|
109
|
+
grid-template-columns: minmax(0, 1.04fr) minmax(360px, 0.96fr);
|
|
110
|
+
gap: 18px;
|
|
111
|
+
align-items: stretch;
|
|
109
112
|
background:
|
|
110
113
|
radial-gradient(circle at top left, rgba(216, 97, 53, 0.18), transparent 34%),
|
|
111
114
|
radial-gradient(circle at bottom right, rgba(46, 139, 139, 0.16), transparent 32%),
|
|
112
115
|
rgba(255, 249, 243, 0.9);
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
.auth-hero-card,
|
|
115
119
|
.auth-form-card {
|
|
116
120
|
display: grid;
|
|
117
121
|
align-content: center;
|
|
118
122
|
gap: 14px;
|
|
119
|
-
padding:
|
|
123
|
+
padding: 20px;
|
|
120
124
|
border-radius: 30px;
|
|
121
125
|
background: rgba(255, 252, 248, 0.82);
|
|
122
126
|
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
.auth-hero-card {
|
|
130
|
+
background:
|
|
131
|
+
radial-gradient(circle at top left, rgba(216, 97, 53, 0.18), transparent 32%),
|
|
132
|
+
linear-gradient(165deg, rgba(255, 250, 245, 0.96), rgba(248, 239, 232, 0.9));
|
|
133
|
+
gap: 18px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.auth-hero-title {
|
|
137
|
+
margin: 0;
|
|
138
|
+
font-size: clamp(2rem, 3.2vw, 3rem);
|
|
139
|
+
line-height: 1.02;
|
|
140
|
+
font-family: "Bahnschrift", "Microsoft YaHei UI", sans-serif;
|
|
141
|
+
letter-spacing: 0.01em;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.auth-hero-copy {
|
|
145
|
+
margin: 0;
|
|
146
|
+
max-width: 34rem;
|
|
147
|
+
color: var(--muted);
|
|
148
|
+
font-size: 0.98rem;
|
|
149
|
+
line-height: 1.75;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.auth-highlight-list {
|
|
153
|
+
display: grid;
|
|
154
|
+
gap: 12px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.auth-highlight-item {
|
|
158
|
+
display: grid;
|
|
159
|
+
gap: 6px;
|
|
160
|
+
padding: 14px 16px;
|
|
161
|
+
border-radius: 22px;
|
|
162
|
+
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
163
|
+
background: rgba(255, 255, 255, 0.6);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.auth-highlight-label {
|
|
167
|
+
color: var(--accent-deep);
|
|
168
|
+
font-size: 0.76rem;
|
|
169
|
+
font-weight: 700;
|
|
170
|
+
letter-spacing: 0.14em;
|
|
171
|
+
text-transform: uppercase;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.auth-highlight-item strong {
|
|
175
|
+
font-size: 1.08rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.auth-highlight-item p {
|
|
179
|
+
margin: 0;
|
|
180
|
+
color: var(--muted);
|
|
181
|
+
line-height: 1.55;
|
|
182
|
+
font-size: 0.9rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
125
185
|
.auth-panel-header {
|
|
126
186
|
margin-bottom: 0;
|
|
127
187
|
}
|
|
@@ -130,6 +190,10 @@ code {
|
|
|
130
190
|
margin-top: 0;
|
|
131
191
|
}
|
|
132
192
|
|
|
193
|
+
.auth-form-card .panel-header {
|
|
194
|
+
margin-bottom: 4px;
|
|
195
|
+
}
|
|
196
|
+
|
|
133
197
|
.auth-form .field-hint,
|
|
134
198
|
.auth-status {
|
|
135
199
|
margin-top: 2px;
|
|
@@ -214,6 +278,57 @@ code {
|
|
|
214
278
|
margin-bottom: 6px;
|
|
215
279
|
}
|
|
216
280
|
|
|
281
|
+
.update-strip {
|
|
282
|
+
display: grid;
|
|
283
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
284
|
+
gap: 12px;
|
|
285
|
+
align-items: center;
|
|
286
|
+
margin-bottom: 6px;
|
|
287
|
+
padding: 10px 12px;
|
|
288
|
+
border-radius: var(--radius-md);
|
|
289
|
+
background: rgba(255, 255, 255, 0.56);
|
|
290
|
+
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.update-copy {
|
|
294
|
+
display: grid;
|
|
295
|
+
gap: 4px;
|
|
296
|
+
min-width: 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.update-copy strong {
|
|
300
|
+
display: block;
|
|
301
|
+
font-size: 0.92rem;
|
|
302
|
+
line-height: 1.25;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.update-detail {
|
|
306
|
+
color: var(--muted);
|
|
307
|
+
font-size: 0.8rem;
|
|
308
|
+
line-height: 1.35;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.update-command {
|
|
312
|
+
display: inline-flex;
|
|
313
|
+
width: fit-content;
|
|
314
|
+
max-width: 100%;
|
|
315
|
+
overflow: hidden;
|
|
316
|
+
text-overflow: ellipsis;
|
|
317
|
+
white-space: nowrap;
|
|
318
|
+
padding: 0.2rem 0.46rem;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.update-command[hidden] {
|
|
322
|
+
display: none !important;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.update-actions {
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-wrap: wrap;
|
|
328
|
+
justify-content: flex-end;
|
|
329
|
+
gap: 8px;
|
|
330
|
+
}
|
|
331
|
+
|
|
217
332
|
.metric-card {
|
|
218
333
|
padding: 14px;
|
|
219
334
|
border-radius: var(--radius-md);
|
|
@@ -488,9 +603,9 @@ input[readonly] {
|
|
|
488
603
|
}
|
|
489
604
|
|
|
490
605
|
.profiles-list {
|
|
491
|
-
display:
|
|
492
|
-
|
|
493
|
-
align-
|
|
606
|
+
display: flex;
|
|
607
|
+
flex-direction: column;
|
|
608
|
+
align-items: stretch;
|
|
494
609
|
flex: 1 1 auto;
|
|
495
610
|
gap: 14px;
|
|
496
611
|
min-height: 0;
|
|
@@ -529,8 +644,9 @@ input[readonly] {
|
|
|
529
644
|
display: grid;
|
|
530
645
|
grid-template-rows: auto auto;
|
|
531
646
|
gap: 14px;
|
|
532
|
-
min-height:
|
|
647
|
+
min-height: 176px;
|
|
533
648
|
padding: 18px;
|
|
649
|
+
box-sizing: border-box;
|
|
534
650
|
border-radius: var(--radius-md);
|
|
535
651
|
background: var(--panel-strong);
|
|
536
652
|
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
@@ -585,22 +701,27 @@ input[readonly] {
|
|
|
585
701
|
|
|
586
702
|
.profile-meta {
|
|
587
703
|
display: grid;
|
|
588
|
-
grid-template-
|
|
589
|
-
gap:
|
|
704
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
705
|
+
gap: 10px 14px;
|
|
590
706
|
min-height: 0;
|
|
591
|
-
align-
|
|
707
|
+
align-items: start;
|
|
592
708
|
}
|
|
593
709
|
|
|
594
|
-
.profile-meta
|
|
710
|
+
.profile-meta-item {
|
|
595
711
|
display: grid;
|
|
596
712
|
gap: 4px;
|
|
713
|
+
min-width: 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.profile-meta-label {
|
|
597
717
|
color: var(--muted);
|
|
598
718
|
font-size: 0.92rem;
|
|
599
|
-
min-width: 0;
|
|
600
719
|
}
|
|
601
720
|
|
|
602
|
-
.profile-meta
|
|
721
|
+
.profile-meta-value {
|
|
603
722
|
color: var(--text);
|
|
723
|
+
display: block;
|
|
724
|
+
min-width: 0;
|
|
604
725
|
white-space: nowrap;
|
|
605
726
|
overflow: hidden;
|
|
606
727
|
text-overflow: ellipsis;
|
|
@@ -742,6 +863,11 @@ input[readonly] {
|
|
|
742
863
|
height: auto;
|
|
743
864
|
}
|
|
744
865
|
|
|
866
|
+
.auth-card {
|
|
867
|
+
width: min(720px, 100%);
|
|
868
|
+
grid-template-columns: 1fr;
|
|
869
|
+
}
|
|
870
|
+
|
|
745
871
|
.content-grid {
|
|
746
872
|
grid-template-columns: 1fr;
|
|
747
873
|
}
|
|
@@ -793,16 +919,40 @@ input[readonly] {
|
|
|
793
919
|
|
|
794
920
|
.auth-card {
|
|
795
921
|
width: 100%;
|
|
796
|
-
padding:
|
|
922
|
+
padding: 12px;
|
|
797
923
|
border-radius: 24px;
|
|
924
|
+
gap: 12px;
|
|
798
925
|
}
|
|
799
926
|
|
|
927
|
+
.auth-hero-card,
|
|
800
928
|
.auth-form-card,
|
|
801
929
|
.panel {
|
|
802
930
|
border-radius: 20px;
|
|
803
931
|
padding: 14px;
|
|
804
932
|
}
|
|
805
933
|
|
|
934
|
+
.auth-hero-card {
|
|
935
|
+
gap: 14px;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.auth-hero-title {
|
|
939
|
+
font-size: 1.8rem;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.auth-hero-copy {
|
|
943
|
+
font-size: 0.9rem;
|
|
944
|
+
line-height: 1.6;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.auth-highlight-list {
|
|
948
|
+
gap: 8px;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.auth-highlight-item {
|
|
952
|
+
padding: 12px;
|
|
953
|
+
border-radius: 18px;
|
|
954
|
+
}
|
|
955
|
+
|
|
806
956
|
.account-hint {
|
|
807
957
|
font-size: 0.82rem;
|
|
808
958
|
line-height: 1.4;
|
|
@@ -813,6 +963,7 @@ input[readonly] {
|
|
|
813
963
|
}
|
|
814
964
|
|
|
815
965
|
.current-tools button,
|
|
966
|
+
.update-actions button,
|
|
816
967
|
.action-row button,
|
|
817
968
|
.account-actions-grid button,
|
|
818
969
|
.card-actions button {
|
|
@@ -830,6 +981,15 @@ input[readonly] {
|
|
|
830
981
|
gap: 8px;
|
|
831
982
|
}
|
|
832
983
|
|
|
984
|
+
.update-strip {
|
|
985
|
+
grid-template-columns: 1fr;
|
|
986
|
+
gap: 10px;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.update-actions {
|
|
990
|
+
justify-content: flex-start;
|
|
991
|
+
}
|
|
992
|
+
|
|
833
993
|
.current-panel,
|
|
834
994
|
.account-panel,
|
|
835
995
|
.profiles-panel {
|
|
@@ -898,8 +1058,14 @@ input[readonly] {
|
|
|
898
1058
|
grid-template-columns: 1fr;
|
|
899
1059
|
}
|
|
900
1060
|
|
|
1061
|
+
.update-command {
|
|
1062
|
+
white-space: normal;
|
|
1063
|
+
overflow: visible;
|
|
1064
|
+
text-overflow: clip;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
901
1067
|
.profiles-list {
|
|
902
|
-
|
|
1068
|
+
gap: 12px;
|
|
903
1069
|
}
|
|
904
1070
|
|
|
905
1071
|
.profile-card {
|
|
@@ -907,10 +1073,10 @@ input[readonly] {
|
|
|
907
1073
|
}
|
|
908
1074
|
|
|
909
1075
|
.profile-meta {
|
|
910
|
-
grid-template-
|
|
1076
|
+
grid-template-columns: 1fr;
|
|
911
1077
|
}
|
|
912
1078
|
|
|
913
|
-
.profile-meta
|
|
1079
|
+
.profile-meta-value {
|
|
914
1080
|
white-space: normal;
|
|
915
1081
|
overflow: visible;
|
|
916
1082
|
text-overflow: clip;
|
|
@@ -925,6 +1091,7 @@ input[readonly] {
|
|
|
925
1091
|
.auth-card {
|
|
926
1092
|
min-height: auto;
|
|
927
1093
|
padding: 16px;
|
|
1094
|
+
gap: 14px;
|
|
928
1095
|
}
|
|
929
1096
|
|
|
930
1097
|
.panel {
|
package/src/web/proxy-server.js
CHANGED
|
@@ -2,6 +2,147 @@ const http = require("node:http");
|
|
|
2
2
|
const https = require("node:https");
|
|
3
3
|
const profileManager = require("../main/profile-manager");
|
|
4
4
|
|
|
5
|
+
const httpAgent = new http.Agent({
|
|
6
|
+
keepAlive: true,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const httpsAgent = new https.Agent({
|
|
10
|
+
keepAlive: true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isRetryableTlsError(error) {
|
|
18
|
+
const code = String(error?.code || "").trim().toUpperCase();
|
|
19
|
+
const message = String(error?.message || "");
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
code === "ECONNRESET" ||
|
|
23
|
+
code === "ETIMEDOUT" ||
|
|
24
|
+
code === "EPIPE" ||
|
|
25
|
+
code === "EPROTO" ||
|
|
26
|
+
message.includes("secure TLS connection was established") ||
|
|
27
|
+
message.includes("socket hang up")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readRequestBody(req) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
|
|
35
|
+
req.on("data", (chunk) => {
|
|
36
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
req.on("end", () => {
|
|
40
|
+
resolve(chunks.length ? Buffer.concat(chunks) : Buffer.alloc(0));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
req.on("error", reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sendUpstreamRequest({
|
|
48
|
+
upstreamUrl,
|
|
49
|
+
requestModule,
|
|
50
|
+
headers,
|
|
51
|
+
method,
|
|
52
|
+
bodyBuffer,
|
|
53
|
+
}) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const upstreamRequest = requestModule.request(
|
|
56
|
+
upstreamUrl,
|
|
57
|
+
{
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
agent: upstreamUrl.protocol === "https:" ? httpsAgent : httpAgent,
|
|
61
|
+
},
|
|
62
|
+
(upstreamResponse) => {
|
|
63
|
+
resolve(upstreamResponse);
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
upstreamRequest.setTimeout(12000, () => {
|
|
68
|
+
upstreamRequest.destroy(new Error("上游请求超时。"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
upstreamRequest.on("error", reject);
|
|
72
|
+
|
|
73
|
+
if (bodyBuffer?.length) {
|
|
74
|
+
upstreamRequest.end(bodyBuffer);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
upstreamRequest.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function forwardWithRetry({
|
|
83
|
+
upstreamUrl,
|
|
84
|
+
requestModule,
|
|
85
|
+
headers,
|
|
86
|
+
method,
|
|
87
|
+
bodyBuffer,
|
|
88
|
+
retryCount = 2,
|
|
89
|
+
}) {
|
|
90
|
+
let lastError = null;
|
|
91
|
+
|
|
92
|
+
for (let attempt = 0; attempt <= retryCount; attempt += 1) {
|
|
93
|
+
try {
|
|
94
|
+
return await sendUpstreamRequest({
|
|
95
|
+
upstreamUrl,
|
|
96
|
+
requestModule,
|
|
97
|
+
headers,
|
|
98
|
+
method,
|
|
99
|
+
bodyBuffer,
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
lastError = error;
|
|
103
|
+
if (attempt >= retryCount || !isRetryableTlsError(error)) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await sleep(220 * (attempt + 1));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw lastError || new Error("代理转发失败。");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function warmUpCurrentTarget() {
|
|
115
|
+
const target = await profileManager.getProxyTarget();
|
|
116
|
+
const upstreamUrl = new URL(target.url);
|
|
117
|
+
const requestModule = upstreamUrl.protocol === "https:" ? https : http;
|
|
118
|
+
|
|
119
|
+
const warmUrl = new URL("/", upstreamUrl);
|
|
120
|
+
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
const warmRequest = requestModule.request(
|
|
123
|
+
warmUrl,
|
|
124
|
+
{
|
|
125
|
+
method: "HEAD",
|
|
126
|
+
headers: {
|
|
127
|
+
authorization: `Bearer ${target.key}`,
|
|
128
|
+
host: warmUrl.host,
|
|
129
|
+
},
|
|
130
|
+
agent: upstreamUrl.protocol === "https:" ? httpsAgent : httpAgent,
|
|
131
|
+
},
|
|
132
|
+
(response) => {
|
|
133
|
+
response.resume();
|
|
134
|
+
resolve(response.statusCode || 200);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
warmRequest.setTimeout(6000, () => {
|
|
139
|
+
warmRequest.destroy(new Error("预热请求超时。"));
|
|
140
|
+
});
|
|
141
|
+
warmRequest.on("error", reject);
|
|
142
|
+
warmRequest.end();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
5
146
|
function createProxyServer() {
|
|
6
147
|
return http.createServer(async (req, res) => {
|
|
7
148
|
try {
|
|
@@ -16,42 +157,28 @@ function createProxyServer() {
|
|
|
16
157
|
const requestPath = req.url && req.url.startsWith("/") ? req.url : `/${req.url || ""}`;
|
|
17
158
|
const upstreamUrl = new URL(requestPath, upstreamBase);
|
|
18
159
|
const requestModule = upstreamUrl.protocol === "https:" ? https : http;
|
|
160
|
+
const bodyBuffer = await readRequestBody(req);
|
|
19
161
|
const headers = {
|
|
20
162
|
...req.headers,
|
|
21
163
|
host: upstreamUrl.host,
|
|
22
164
|
authorization: `Bearer ${target.key}`,
|
|
165
|
+
"content-length": String(bodyBuffer.length),
|
|
23
166
|
};
|
|
24
167
|
|
|
25
168
|
delete headers.connection;
|
|
26
|
-
|
|
27
|
-
const upstreamRequest = requestModule.request(
|
|
169
|
+
const upstreamResponse = await forwardWithRetry({
|
|
28
170
|
upstreamUrl,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
);
|
|
171
|
+
requestModule,
|
|
172
|
+
headers,
|
|
173
|
+
method: req.method,
|
|
174
|
+
bodyBuffer,
|
|
49
175
|
});
|
|
50
176
|
|
|
51
|
-
|
|
177
|
+
res.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
|
|
178
|
+
upstreamResponse.pipe(res);
|
|
52
179
|
} catch (error) {
|
|
53
180
|
if (!res.headersSent) {
|
|
54
|
-
res.writeHead(
|
|
181
|
+
res.writeHead(502, { "Content-Type": "application/json; charset=utf-8" });
|
|
55
182
|
}
|
|
56
183
|
|
|
57
184
|
res.end(
|
|
@@ -66,4 +193,5 @@ function createProxyServer() {
|
|
|
66
193
|
|
|
67
194
|
module.exports = {
|
|
68
195
|
createProxyServer,
|
|
196
|
+
warmUpCurrentTarget,
|
|
69
197
|
};
|
package/src/web/server.js
CHANGED
|
@@ -3,7 +3,8 @@ const { exec } = require("node:child_process");
|
|
|
3
3
|
const express = require("express");
|
|
4
4
|
const profileManager = require("../main/profile-manager");
|
|
5
5
|
const cloudSyncClient = require("../main/cloud-sync-client");
|
|
6
|
-
const
|
|
6
|
+
const updateService = require("../main/update-service");
|
|
7
|
+
const { createProxyServer, warmUpCurrentTarget } = require("./proxy-server");
|
|
7
8
|
|
|
8
9
|
function wrapAsync(handler) {
|
|
9
10
|
return async (req, res) => {
|
|
@@ -179,6 +180,15 @@ function createApp() {
|
|
|
179
180
|
}),
|
|
180
181
|
);
|
|
181
182
|
|
|
183
|
+
app.get(
|
|
184
|
+
"/api/app/update-status",
|
|
185
|
+
wrapAsync(async (req) => {
|
|
186
|
+
return updateService.getUpdateStatus({
|
|
187
|
+
force: String(req.query.force || "").trim() === "1",
|
|
188
|
+
});
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
182
192
|
app.post(
|
|
183
193
|
"/api/open-path",
|
|
184
194
|
wrapAsync(async (req) => {
|
|
@@ -226,6 +236,9 @@ function startServer(options = {}) {
|
|
|
226
236
|
});
|
|
227
237
|
|
|
228
238
|
proxyServer.listen(proxyPort);
|
|
239
|
+
warmUpCurrentTarget().catch(() => {
|
|
240
|
+
// 预热失败不阻断服务启动,真正请求时仍会走自动重试。
|
|
241
|
+
});
|
|
229
242
|
|
|
230
243
|
const controller = {
|
|
231
244
|
webServer,
|