codex-endpoint-switcher 1.2.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 +2 -0
- package/bin/codex-switcher.js +4 -1
- package/package.json +3 -3
- package/src/main/cloud-sync-client.js +1 -1
- package/src/main/main.js +10 -0
- package/src/main/preload.js +3 -0
- package/src/renderer/index.html +13 -40
- package/src/renderer/renderer.js +45 -70
- package/src/renderer/styles.css +79 -134
- package/src/web/launcher.js +105 -1
- package/src/web/server.js +23 -1
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`:移除桌面快捷方式和开机启动项
|
package/bin/codex-switcher.js
CHANGED
|
@@ -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
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "用于切换 Codex URL 和 Key 的本地网页控制台与 npm CLI",
|
|
5
5
|
"main": "src/main/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"x64"
|
|
59
59
|
]
|
|
60
60
|
}
|
|
61
|
-
]
|
|
62
|
-
"artifactName": "CodexProfileDesktop-${version}-portable.${ext}"
|
|
61
|
+
]
|
|
63
62
|
},
|
|
64
63
|
"portable": {
|
|
65
64
|
"artifactName": "CodexProfileDesktop-${version}-portable.${ext}"
|
|
66
65
|
},
|
|
67
66
|
"nsis": {
|
|
67
|
+
"artifactName": "CodexProfileDesktop-${version}-installer.${ext}",
|
|
68
68
|
"oneClick": false,
|
|
69
69
|
"allowToChangeInstallationDirectory": true
|
|
70
70
|
}
|
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(() => {
|
package/src/main/preload.js
CHANGED
package/src/renderer/index.html
CHANGED
|
@@ -62,29 +62,6 @@
|
|
|
62
62
|
</section>
|
|
63
63
|
|
|
64
64
|
<div id="appShell" class="app-shell" hidden>
|
|
65
|
-
<header class="workspace-bar panel">
|
|
66
|
-
<div class="workspace-main">
|
|
67
|
-
<div class="workspace-copy">
|
|
68
|
-
<p class="eyebrow">CODEX ENDPOINT SWITCHER</p>
|
|
69
|
-
<div class="workspace-title-row">
|
|
70
|
-
<h1>连接控制台</h1>
|
|
71
|
-
<div class="hero-badge workspace-badge" id="heroBadge">读取中</div>
|
|
72
|
-
</div>
|
|
73
|
-
<p class="workspace-text">
|
|
74
|
-
这里只管理 <code>备注</code>、<code>URL</code>、<code>Key</code>。切换时只更新
|
|
75
|
-
Codex 当前使用的 <code>base_url</code> 和 <code>OPENAI_API_KEY</code>。
|
|
76
|
-
</p>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<div class="workspace-actions">
|
|
80
|
-
<button id="refreshButton" class="ghost-button">刷新状态</button>
|
|
81
|
-
<button id="openCodexRootButton" class="ghost-button">打开 Codex 目录</button>
|
|
82
|
-
<button id="openEndpointStoreButton" class="ghost-button">打开连接数据文件</button>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
<div id="statusBox" class="status-box info workspace-status">应用已启动,正在读取当前连接。</div>
|
|
86
|
-
</header>
|
|
87
|
-
|
|
88
65
|
<main class="content-grid">
|
|
89
66
|
<div class="left-column">
|
|
90
67
|
<section class="panel current-panel">
|
|
@@ -95,6 +72,18 @@
|
|
|
95
72
|
</div>
|
|
96
73
|
<span class="panel-tag" id="activeEndpointTag">未识别</span>
|
|
97
74
|
</div>
|
|
75
|
+
<div class="current-summary">
|
|
76
|
+
<p class="current-copy">
|
|
77
|
+
这里只管理 <code>备注</code>、<code>URL</code>、<code>Key</code>。切换时只更新
|
|
78
|
+
Codex 当前使用的 <code>base_url</code> 和 <code>OPENAI_API_KEY</code>。
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="current-tools">
|
|
82
|
+
<button id="refreshButton" class="ghost-button">刷新状态</button>
|
|
83
|
+
<button id="openCodexRootButton" class="ghost-button">打开 Codex 目录</button>
|
|
84
|
+
<button id="openEndpointStoreButton" class="ghost-button">打开连接数据文件</button>
|
|
85
|
+
<button id="closeAppButton" class="danger-button">关闭程序</button>
|
|
86
|
+
</div>
|
|
98
87
|
<div class="current-grid">
|
|
99
88
|
<article class="metric-card">
|
|
100
89
|
<span class="metric-label">当前备注</span>
|
|
@@ -112,22 +101,6 @@
|
|
|
112
101
|
<span class="metric-label">模型</span>
|
|
113
102
|
<strong id="currentModel">-</strong>
|
|
114
103
|
</article>
|
|
115
|
-
<article class="metric-card metric-card-wide">
|
|
116
|
-
<span class="metric-label">当前 URL</span>
|
|
117
|
-
<strong id="currentBaseUrl">-</strong>
|
|
118
|
-
</article>
|
|
119
|
-
<article class="metric-card metric-card-wide">
|
|
120
|
-
<span class="metric-label">热更新模式</span>
|
|
121
|
-
<strong id="proxyModeStatus">未开启</strong>
|
|
122
|
-
<p class="field-hint" id="proxyModeHint">
|
|
123
|
-
当前还是直连模式。开启后,后续同一 Codex 会话可在下一次请求时热更新到新 URL/Key。
|
|
124
|
-
</p>
|
|
125
|
-
<div class="action-row metric-actions">
|
|
126
|
-
<button id="enableProxyModeButton" type="button" class="secondary-button">
|
|
127
|
-
开启热更新模式
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
</article>
|
|
131
104
|
</div>
|
|
132
105
|
</section>
|
|
133
106
|
|
|
@@ -160,7 +133,7 @@
|
|
|
160
133
|
<span class="metric-label">同步服务</span>
|
|
161
134
|
<strong id="cloudServerStatus">自动连接</strong>
|
|
162
135
|
</article>
|
|
163
|
-
<article class="account-inline-card
|
|
136
|
+
<article class="account-inline-card">
|
|
164
137
|
<span class="metric-label">同步记录</span>
|
|
165
138
|
<strong id="cloudLastSyncStatus">还没有推送或拉取记录</strong>
|
|
166
139
|
</article>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -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
|
|
|
@@ -109,6 +114,15 @@ function $(selector) {
|
|
|
109
114
|
return document.querySelector(selector);
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
function setText(selector, value) {
|
|
118
|
+
const element = $(selector);
|
|
119
|
+
if (!element) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
element.textContent = value;
|
|
124
|
+
}
|
|
125
|
+
|
|
112
126
|
function formatDateTime(value) {
|
|
113
127
|
if (!value) {
|
|
114
128
|
return "-";
|
|
@@ -126,23 +140,18 @@ function formatDateTime(value) {
|
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
function setStatus(message, type = "info") {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
statusBox.textContent = message;
|
|
135
|
-
statusBox.className = `status-box ${type}`;
|
|
143
|
+
void message;
|
|
144
|
+
void type;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
function setAuthStatus(message, type = "info") {
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
148
|
+
const authStatusElement = $("#authStatusBox");
|
|
149
|
+
if (!authStatusElement) {
|
|
141
150
|
return;
|
|
142
151
|
}
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
authStatusElement.textContent = message;
|
|
154
|
+
authStatusElement.className = `status-box ${type} auth-status`;
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
function syncFieldValue(selector, value) {
|
|
@@ -228,25 +237,11 @@ function renderCurrent() {
|
|
|
228
237
|
return;
|
|
229
238
|
}
|
|
230
239
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
$("#currentModel").textContent = current.model || "-";
|
|
237
|
-
$("#proxyModeStatus").textContent = current.proxyModeEnabled
|
|
238
|
-
? `已开启,固定代理:${current.proxyBaseUrl}`
|
|
239
|
-
: "未开启";
|
|
240
|
-
$("#proxyModeHint").textContent = current.proxyModeEnabled
|
|
241
|
-
? "当前 Codex 只要已经接入本地代理,后续同一会话内切换连接会在下一次请求时热更新。"
|
|
242
|
-
: "当前还是直连模式。开启后需要把已打开的旧 Codex 会话重开一次;之后同一会话即可热更新。";
|
|
243
|
-
$("#enableProxyModeButton").disabled = current.proxyModeEnabled;
|
|
244
|
-
$("#enableProxyModeButton").textContent = current.proxyModeEnabled
|
|
245
|
-
? "热更新模式已开启"
|
|
246
|
-
: "开启热更新模式";
|
|
247
|
-
$("#heroBadge").textContent = current.currentNote
|
|
248
|
-
? `${current.proxyModeEnabled ? "热更新中" : "当前连接"}:${current.currentNote}`
|
|
249
|
-
: "当前未匹配到已保存连接";
|
|
240
|
+
setText("#activeEndpointTag", current.currentNote || "未识别");
|
|
241
|
+
setText("#currentNote", current.currentNote || "未识别");
|
|
242
|
+
setText("#currentProvider", current.provider || "-");
|
|
243
|
+
setText("#currentKeyMasked", current.currentKeyMasked || "-");
|
|
244
|
+
setText("#currentModel", current.model || "-");
|
|
250
245
|
}
|
|
251
246
|
|
|
252
247
|
function renderCloudStatus() {
|
|
@@ -258,22 +253,12 @@ function renderCloudStatus() {
|
|
|
258
253
|
syncFieldValue("#authUsername", cloud.username);
|
|
259
254
|
|
|
260
255
|
const accountName = cloud.remoteUser || cloud.username || "-";
|
|
261
|
-
const serverText =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
: "等待登录";
|
|
268
|
-
const sessionText = !cloud.serverUrl
|
|
269
|
-
? "未配置服务器"
|
|
270
|
-
: cloud.loggedIn
|
|
271
|
-
? cloud.tokenExpiresAt
|
|
272
|
-
? `已登录 · 到期 ${formatDateTime(cloud.tokenExpiresAt)}`
|
|
273
|
-
: "已登录"
|
|
274
|
-
: cloud.hasToken
|
|
275
|
-
? "登录态失效,请重新登录"
|
|
276
|
-
: "未登录";
|
|
256
|
+
const serverText = cloud.loggedIn ? "已连接" : cloud.lastError ? "连接异常" : "等待登录";
|
|
257
|
+
const sessionText = cloud.loggedIn
|
|
258
|
+
? "已登录"
|
|
259
|
+
: cloud.hasToken
|
|
260
|
+
? "登录态失效,请重新登录"
|
|
261
|
+
: "未登录";
|
|
277
262
|
|
|
278
263
|
const syncStatusParts = [];
|
|
279
264
|
if (cloud.lastPushAt) {
|
|
@@ -483,29 +468,6 @@ async function handleSwitchEndpoint(id, note) {
|
|
|
483
468
|
}
|
|
484
469
|
}
|
|
485
470
|
|
|
486
|
-
async function handleEnableProxyMode() {
|
|
487
|
-
try {
|
|
488
|
-
setStatus("正在切到热更新代理模式 ...", "info");
|
|
489
|
-
const result = await bridge.enableProxyMode();
|
|
490
|
-
await refreshDashboard(false);
|
|
491
|
-
if (result.oneTimeRestartRequired) {
|
|
492
|
-
setStatus(
|
|
493
|
-
"热更新模式已开启。请把当前已经打开着的旧 Codex 会话重开一次;之后同一会话内即可热更新。",
|
|
494
|
-
"success",
|
|
495
|
-
);
|
|
496
|
-
} else {
|
|
497
|
-
setStatus("热更新模式已开启。当前会话后续请求可直接热更新到新连接。", "success");
|
|
498
|
-
}
|
|
499
|
-
} catch (error) {
|
|
500
|
-
if (isUnauthorizedError(error)) {
|
|
501
|
-
await handleAccessRevoked(error.message);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
setStatus(`开启热更新模式失败:${error.message}`, "error");
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
471
|
function getAuthFormPayload() {
|
|
510
472
|
return {
|
|
511
473
|
username: $("#authUsername").value.trim(),
|
|
@@ -684,6 +646,19 @@ async function handleOpenPath(targetPath, label) {
|
|
|
684
646
|
}
|
|
685
647
|
}
|
|
686
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
|
+
|
|
687
662
|
function bindEvents() {
|
|
688
663
|
$("#authForm").addEventListener("submit", (event) => {
|
|
689
664
|
event.preventDefault();
|
|
@@ -712,7 +687,6 @@ function bindEvents() {
|
|
|
712
687
|
resetForm();
|
|
713
688
|
setStatus("已重置表单,可作为新连接保存。", "info");
|
|
714
689
|
});
|
|
715
|
-
$("#enableProxyModeButton").addEventListener("click", handleEnableProxyMode);
|
|
716
690
|
$("#cloudLogoutButton").addEventListener("click", handleCloudLogout);
|
|
717
691
|
$("#openSwitchAccountButton").addEventListener("click", handleCloudLogout);
|
|
718
692
|
$("#cloudPushButton").addEventListener("click", handleCloudPush);
|
|
@@ -732,6 +706,7 @@ function bindEvents() {
|
|
|
732
706
|
handleOpenPath(state.paths.endpointStorePath, " 连接数据文件");
|
|
733
707
|
}
|
|
734
708
|
});
|
|
709
|
+
$("#closeAppButton").addEventListener("click", handleCloseApp);
|
|
735
710
|
window.addEventListener("keydown", (event) => {
|
|
736
711
|
if (event.key === "Escape" && !$("#endpointModal").hidden) {
|
|
737
712
|
closeEndpointModal();
|
package/src/renderer/styles.css
CHANGED
|
@@ -61,13 +61,7 @@ code {
|
|
|
61
61
|
|
|
62
62
|
.app-shell {
|
|
63
63
|
height: 100%;
|
|
64
|
-
display:
|
|
65
|
-
grid-template-rows: auto minmax(0, 1fr);
|
|
66
|
-
gap: 16px;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.mobile-section-nav {
|
|
70
|
-
display: none;
|
|
64
|
+
display: block;
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
.app-shell[hidden],
|
|
@@ -75,7 +69,6 @@ code {
|
|
|
75
69
|
display: none;
|
|
76
70
|
}
|
|
77
71
|
|
|
78
|
-
.workspace-bar,
|
|
79
72
|
.panel {
|
|
80
73
|
backdrop-filter: blur(18px);
|
|
81
74
|
-webkit-backdrop-filter: blur(18px);
|
|
@@ -93,7 +86,6 @@ code {
|
|
|
93
86
|
font-weight: 700;
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
.workspace-copy h1,
|
|
97
89
|
.panel-header h2,
|
|
98
90
|
.profile-heading h3,
|
|
99
91
|
.modal-header h2 {
|
|
@@ -143,67 +135,6 @@ code {
|
|
|
143
135
|
margin-top: 2px;
|
|
144
136
|
}
|
|
145
137
|
|
|
146
|
-
.workspace-bar {
|
|
147
|
-
display: grid;
|
|
148
|
-
gap: 14px;
|
|
149
|
-
padding: 18px 20px;
|
|
150
|
-
border-radius: var(--radius-xl);
|
|
151
|
-
background:
|
|
152
|
-
linear-gradient(135deg, rgba(255, 252, 248, 0.94), rgba(247, 238, 230, 0.9)),
|
|
153
|
-
var(--panel);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.workspace-main {
|
|
157
|
-
display: grid;
|
|
158
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
159
|
-
gap: 18px;
|
|
160
|
-
align-items: start;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.workspace-copy h1 {
|
|
164
|
-
font-size: clamp(1.5rem, 2.2vw, 2.2rem);
|
|
165
|
-
line-height: 1.05;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.workspace-title-row {
|
|
169
|
-
display: flex;
|
|
170
|
-
align-items: center;
|
|
171
|
-
justify-content: space-between;
|
|
172
|
-
gap: 12px;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.workspace-text {
|
|
176
|
-
max-width: 760px;
|
|
177
|
-
margin: 10px 0 0;
|
|
178
|
-
color: var(--muted);
|
|
179
|
-
font-size: 0.94rem;
|
|
180
|
-
line-height: 1.55;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.workspace-actions {
|
|
184
|
-
display: flex;
|
|
185
|
-
flex-wrap: wrap;
|
|
186
|
-
justify-content: flex-end;
|
|
187
|
-
gap: 10px;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.hero-badge {
|
|
191
|
-
display: inline-flex;
|
|
192
|
-
align-items: center;
|
|
193
|
-
padding: 12px 18px;
|
|
194
|
-
border-radius: 999px;
|
|
195
|
-
background: rgba(255, 255, 255, 0.82);
|
|
196
|
-
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
197
|
-
color: var(--text);
|
|
198
|
-
font-weight: 700;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.workspace-badge {
|
|
202
|
-
flex: 0 0 auto;
|
|
203
|
-
padding: 10px 14px;
|
|
204
|
-
white-space: nowrap;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
138
|
.content-grid {
|
|
208
139
|
display: grid;
|
|
209
140
|
grid-template-columns: 0.84fr 1.16fr;
|
|
@@ -216,7 +147,7 @@ code {
|
|
|
216
147
|
.left-column {
|
|
217
148
|
min-height: 0;
|
|
218
149
|
display: grid;
|
|
219
|
-
grid-template-rows:
|
|
150
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
220
151
|
gap: 20px;
|
|
221
152
|
overflow: hidden;
|
|
222
153
|
}
|
|
@@ -259,30 +190,28 @@ code {
|
|
|
259
190
|
font-weight: 700;
|
|
260
191
|
}
|
|
261
192
|
|
|
262
|
-
.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
border-radius: 999px;
|
|
267
|
-
background: rgba(255, 255, 255, 0.68);
|
|
268
|
-
color: var(--text);
|
|
269
|
-
cursor: pointer;
|
|
270
|
-
transition:
|
|
271
|
-
background 0.18s ease,
|
|
272
|
-
color 0.18s ease,
|
|
273
|
-
box-shadow 0.18s ease;
|
|
193
|
+
.current-grid {
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
196
|
+
gap: 6px;
|
|
274
197
|
}
|
|
275
198
|
|
|
276
|
-
.
|
|
277
|
-
|
|
278
|
-
color: #fff;
|
|
279
|
-
box-shadow: 0 12px 28px rgba(179, 67, 29, 0.24);
|
|
199
|
+
.current-summary {
|
|
200
|
+
margin-bottom: 6px;
|
|
280
201
|
}
|
|
281
202
|
|
|
282
|
-
.current-
|
|
203
|
+
.current-copy {
|
|
204
|
+
margin: 0;
|
|
205
|
+
color: var(--muted);
|
|
206
|
+
font-size: 0.84rem;
|
|
207
|
+
line-height: 1.4;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.current-tools {
|
|
283
211
|
display: grid;
|
|
284
|
-
grid-template-columns: repeat(
|
|
212
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
285
213
|
gap: 8px;
|
|
214
|
+
margin-bottom: 6px;
|
|
286
215
|
}
|
|
287
216
|
|
|
288
217
|
.metric-card {
|
|
@@ -300,30 +229,32 @@ code {
|
|
|
300
229
|
}
|
|
301
230
|
|
|
302
231
|
.current-panel .panel-header {
|
|
303
|
-
margin-bottom:
|
|
232
|
+
margin-bottom: 6px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.current-panel {
|
|
236
|
+
align-self: start;
|
|
237
|
+
display: grid;
|
|
238
|
+
align-content: start;
|
|
304
239
|
}
|
|
305
240
|
|
|
306
241
|
.current-panel .metric-card {
|
|
307
|
-
padding:
|
|
242
|
+
padding: 5px 8px;
|
|
308
243
|
}
|
|
309
244
|
|
|
310
245
|
.current-panel .metric-card strong {
|
|
311
|
-
margin-top:
|
|
312
|
-
font-size: 0.
|
|
246
|
+
margin-top: 2px;
|
|
247
|
+
font-size: 0.88rem;
|
|
313
248
|
}
|
|
314
249
|
|
|
315
250
|
.current-panel .metric-label {
|
|
316
|
-
font-size: 0.
|
|
251
|
+
font-size: 0.76rem;
|
|
317
252
|
}
|
|
318
253
|
|
|
319
254
|
.current-panel .secondary-button {
|
|
320
255
|
height: 32px;
|
|
321
256
|
}
|
|
322
257
|
|
|
323
|
-
.metric-card-wide {
|
|
324
|
-
grid-column: 1 / -1;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
258
|
.metric-label {
|
|
328
259
|
color: var(--muted);
|
|
329
260
|
font-size: 0.88rem;
|
|
@@ -452,7 +383,7 @@ input[readonly] {
|
|
|
452
383
|
|
|
453
384
|
.account-inline-grid {
|
|
454
385
|
display: grid;
|
|
455
|
-
grid-template-columns:
|
|
386
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
456
387
|
gap: 8px;
|
|
457
388
|
}
|
|
458
389
|
|
|
@@ -558,16 +489,20 @@ input[readonly] {
|
|
|
558
489
|
|
|
559
490
|
.profiles-list {
|
|
560
491
|
display: grid;
|
|
492
|
+
grid-auto-rows: minmax(190px, auto);
|
|
493
|
+
align-content: start;
|
|
561
494
|
flex: 1 1 auto;
|
|
562
495
|
gap: 14px;
|
|
563
496
|
min-height: 0;
|
|
564
497
|
overflow: auto;
|
|
565
498
|
padding-right: 6px;
|
|
499
|
+
padding-bottom: 6px;
|
|
566
500
|
}
|
|
567
501
|
|
|
568
502
|
.profiles-panel {
|
|
569
503
|
display: flex;
|
|
570
504
|
flex-direction: column;
|
|
505
|
+
min-height: 0;
|
|
571
506
|
overflow: hidden;
|
|
572
507
|
}
|
|
573
508
|
|
|
@@ -592,11 +527,14 @@ input[readonly] {
|
|
|
592
527
|
|
|
593
528
|
.profile-card {
|
|
594
529
|
display: grid;
|
|
530
|
+
grid-template-rows: auto auto;
|
|
595
531
|
gap: 14px;
|
|
532
|
+
min-height: 100%;
|
|
596
533
|
padding: 18px;
|
|
597
534
|
border-radius: var(--radius-md);
|
|
598
535
|
background: var(--panel-strong);
|
|
599
536
|
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
537
|
+
overflow: visible;
|
|
600
538
|
}
|
|
601
539
|
|
|
602
540
|
.profile-card.active {
|
|
@@ -621,6 +559,7 @@ input[readonly] {
|
|
|
621
559
|
.profile-heading {
|
|
622
560
|
display: grid;
|
|
623
561
|
gap: 8px;
|
|
562
|
+
min-width: 0;
|
|
624
563
|
}
|
|
625
564
|
|
|
626
565
|
.profile-badges {
|
|
@@ -646,7 +585,10 @@ input[readonly] {
|
|
|
646
585
|
|
|
647
586
|
.profile-meta {
|
|
648
587
|
display: grid;
|
|
588
|
+
grid-template-rows: repeat(3, auto);
|
|
649
589
|
gap: 8px;
|
|
590
|
+
min-height: 0;
|
|
591
|
+
align-content: start;
|
|
650
592
|
}
|
|
651
593
|
|
|
652
594
|
.profile-meta span {
|
|
@@ -654,10 +596,14 @@ input[readonly] {
|
|
|
654
596
|
gap: 4px;
|
|
655
597
|
color: var(--muted);
|
|
656
598
|
font-size: 0.92rem;
|
|
599
|
+
min-width: 0;
|
|
657
600
|
}
|
|
658
601
|
|
|
659
602
|
.profile-meta strong {
|
|
660
603
|
color: var(--text);
|
|
604
|
+
white-space: nowrap;
|
|
605
|
+
overflow: hidden;
|
|
606
|
+
text-overflow: ellipsis;
|
|
661
607
|
}
|
|
662
608
|
|
|
663
609
|
.profile-path {
|
|
@@ -700,12 +646,6 @@ input[readonly] {
|
|
|
700
646
|
color: var(--error);
|
|
701
647
|
}
|
|
702
648
|
|
|
703
|
-
.workspace-status {
|
|
704
|
-
min-height: 0;
|
|
705
|
-
padding: 12px 14px;
|
|
706
|
-
background: rgba(255, 255, 255, 0.72);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
649
|
.account-panel {
|
|
710
650
|
display: grid;
|
|
711
651
|
gap: 7px;
|
|
@@ -787,10 +727,6 @@ input[readonly] {
|
|
|
787
727
|
margin-top: 0;
|
|
788
728
|
}
|
|
789
729
|
|
|
790
|
-
#proxyModeHint {
|
|
791
|
-
display: none;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
730
|
@media (max-width: 1160px) {
|
|
795
731
|
body {
|
|
796
732
|
overflow: auto;
|
|
@@ -806,7 +742,6 @@ input[readonly] {
|
|
|
806
742
|
height: auto;
|
|
807
743
|
}
|
|
808
744
|
|
|
809
|
-
.workspace-main,
|
|
810
745
|
.content-grid {
|
|
811
746
|
grid-template-columns: 1fr;
|
|
812
747
|
}
|
|
@@ -816,12 +751,6 @@ input[readonly] {
|
|
|
816
751
|
overflow: visible;
|
|
817
752
|
}
|
|
818
753
|
|
|
819
|
-
#proxyModeHint {
|
|
820
|
-
display: block;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
.workspace-title-row,
|
|
824
|
-
.workspace-actions,
|
|
825
754
|
.panel-header-actions {
|
|
826
755
|
justify-content: flex-start;
|
|
827
756
|
}
|
|
@@ -869,28 +798,21 @@ input[readonly] {
|
|
|
869
798
|
}
|
|
870
799
|
|
|
871
800
|
.auth-form-card,
|
|
872
|
-
.workspace-bar,
|
|
873
801
|
.panel {
|
|
874
802
|
border-radius: 20px;
|
|
875
803
|
padding: 14px;
|
|
876
804
|
}
|
|
877
805
|
|
|
878
|
-
.workspace-title-row {
|
|
879
|
-
flex-direction: column;
|
|
880
|
-
align-items: flex-start;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
.workspace-text,
|
|
884
806
|
.account-hint {
|
|
885
807
|
font-size: 0.82rem;
|
|
886
808
|
line-height: 1.4;
|
|
887
809
|
}
|
|
888
810
|
|
|
889
|
-
.
|
|
890
|
-
|
|
811
|
+
.current-tools {
|
|
812
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
891
813
|
}
|
|
892
814
|
|
|
893
|
-
.
|
|
815
|
+
.current-tools button,
|
|
894
816
|
.action-row button,
|
|
895
817
|
.account-actions-grid button,
|
|
896
818
|
.card-actions button {
|
|
@@ -902,6 +824,12 @@ input[readonly] {
|
|
|
902
824
|
gap: 12px;
|
|
903
825
|
}
|
|
904
826
|
|
|
827
|
+
.current-tools {
|
|
828
|
+
display: grid;
|
|
829
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
830
|
+
gap: 8px;
|
|
831
|
+
}
|
|
832
|
+
|
|
905
833
|
.current-panel,
|
|
906
834
|
.account-panel,
|
|
907
835
|
.profiles-panel {
|
|
@@ -941,6 +869,7 @@ input[readonly] {
|
|
|
941
869
|
.profile-card {
|
|
942
870
|
gap: 10px;
|
|
943
871
|
padding: 14px;
|
|
872
|
+
min-height: 0;
|
|
944
873
|
}
|
|
945
874
|
|
|
946
875
|
.profile-topline {
|
|
@@ -964,6 +893,30 @@ input[readonly] {
|
|
|
964
893
|
}
|
|
965
894
|
}
|
|
966
895
|
|
|
896
|
+
@media (max-width: 560px) {
|
|
897
|
+
.current-tools {
|
|
898
|
+
grid-template-columns: 1fr;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.profiles-list {
|
|
902
|
+
grid-auto-rows: auto;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.profile-card {
|
|
906
|
+
height: auto;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.profile-meta {
|
|
910
|
+
grid-template-rows: none;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.profile-meta strong {
|
|
914
|
+
white-space: normal;
|
|
915
|
+
overflow: visible;
|
|
916
|
+
text-overflow: clip;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
967
920
|
@media (max-height: 920px) and (min-width: 1161px) {
|
|
968
921
|
.page-shell {
|
|
969
922
|
padding: 12px;
|
|
@@ -974,18 +927,10 @@ input[readonly] {
|
|
|
974
927
|
padding: 16px;
|
|
975
928
|
}
|
|
976
929
|
|
|
977
|
-
.workspace-bar {
|
|
978
|
-
padding: 16px 18px;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
930
|
.panel {
|
|
982
931
|
padding: 16px;
|
|
983
932
|
}
|
|
984
933
|
|
|
985
|
-
.workspace-copy h1 {
|
|
986
|
-
font-size: clamp(1.35rem, 1.8vw, 1.9rem);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
934
|
.metric-card {
|
|
990
935
|
padding: 12px;
|
|
991
936
|
}
|
package/src/web/launcher.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|