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,340 @@
|
|
|
1
|
+
const state = {
|
|
2
|
+
current: null,
|
|
3
|
+
endpoints: [],
|
|
4
|
+
paths: null,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function createWebBridge() {
|
|
8
|
+
async function request(url, options = {}) {
|
|
9
|
+
const response = await fetch(url, {
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
},
|
|
13
|
+
...options,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const payload = await response.json();
|
|
17
|
+
if (!response.ok || !payload.ok) {
|
|
18
|
+
throw new Error(payload.error || `请求失败:${response.status}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return payload.data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
listEndpoints() {
|
|
26
|
+
return request("/api/endpoints");
|
|
27
|
+
},
|
|
28
|
+
getCurrentConfig() {
|
|
29
|
+
return request("/api/current");
|
|
30
|
+
},
|
|
31
|
+
createEndpoint(payload) {
|
|
32
|
+
return request("/api/endpoints", {
|
|
33
|
+
method: "POST",
|
|
34
|
+
body: JSON.stringify(payload),
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
updateEndpoint(payload) {
|
|
38
|
+
return request(`/api/endpoints/${payload.id}`, {
|
|
39
|
+
method: "PUT",
|
|
40
|
+
body: JSON.stringify(payload),
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
deleteEndpoint(payload) {
|
|
44
|
+
return request(`/api/endpoints/${payload.id}`, {
|
|
45
|
+
method: "DELETE",
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
switchEndpoint(payload) {
|
|
49
|
+
return request("/api/endpoints/switch", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: JSON.stringify(payload),
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
getPaths() {
|
|
55
|
+
return request("/api/paths");
|
|
56
|
+
},
|
|
57
|
+
openPath(targetPath) {
|
|
58
|
+
return request("/api/open-path", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: JSON.stringify({ targetPath }),
|
|
61
|
+
}).then(() => "");
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const bridge = window.codexDesktop || createWebBridge();
|
|
67
|
+
|
|
68
|
+
function $(selector) {
|
|
69
|
+
return document.querySelector(selector);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatDateTime(value) {
|
|
73
|
+
if (!value) {
|
|
74
|
+
return "-";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const date = new Date(value);
|
|
78
|
+
if (Number.isNaN(date.getTime())) {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
83
|
+
dateStyle: "medium",
|
|
84
|
+
timeStyle: "short",
|
|
85
|
+
}).format(date);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setStatus(message, type = "info") {
|
|
89
|
+
const statusBox = $("#statusBox");
|
|
90
|
+
statusBox.textContent = message;
|
|
91
|
+
statusBox.className = `status-box ${type}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resetForm() {
|
|
95
|
+
$("#endpointId").value = "";
|
|
96
|
+
$("#endpointNote").value = "";
|
|
97
|
+
$("#endpointUrl").value = "";
|
|
98
|
+
$("#endpointKey").value = "";
|
|
99
|
+
$("#saveEndpointButton").textContent = "新增连接";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function fillForm(endpoint) {
|
|
103
|
+
$("#endpointId").value = endpoint.id;
|
|
104
|
+
$("#endpointNote").value = endpoint.note;
|
|
105
|
+
$("#endpointUrl").value = endpoint.url;
|
|
106
|
+
$("#endpointKey").value = endpoint.key;
|
|
107
|
+
$("#saveEndpointButton").textContent = "保存修改";
|
|
108
|
+
setStatus(`正在编辑:${endpoint.note}`, "info");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderCurrent() {
|
|
112
|
+
const current = state.current;
|
|
113
|
+
if (!current) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$("#activeEndpointTag").textContent = current.currentNote || "未识别";
|
|
118
|
+
$("#currentNote").textContent = current.currentNote || "未识别";
|
|
119
|
+
$("#currentProvider").textContent = current.provider || "-";
|
|
120
|
+
$("#currentBaseUrl").textContent = current.currentUrl || "-";
|
|
121
|
+
$("#currentKeyMasked").textContent = current.currentKeyMasked || "-";
|
|
122
|
+
$("#currentModel").textContent = current.model || "-";
|
|
123
|
+
$("#heroBadge").textContent = current.currentNote
|
|
124
|
+
? `当前连接:${current.currentNote}`
|
|
125
|
+
: "当前未匹配到已保存连接";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createEndpointCard(endpoint) {
|
|
129
|
+
const card = document.createElement("article");
|
|
130
|
+
card.className = `profile-card ${endpoint.isActive ? "active" : ""}`;
|
|
131
|
+
|
|
132
|
+
const top = document.createElement("div");
|
|
133
|
+
top.className = "profile-topline";
|
|
134
|
+
|
|
135
|
+
const heading = document.createElement("div");
|
|
136
|
+
heading.className = "profile-heading";
|
|
137
|
+
|
|
138
|
+
const title = document.createElement("h3");
|
|
139
|
+
title.textContent = endpoint.note;
|
|
140
|
+
|
|
141
|
+
const badges = document.createElement("div");
|
|
142
|
+
badges.className = "profile-badges";
|
|
143
|
+
|
|
144
|
+
if (endpoint.isActive) {
|
|
145
|
+
const activeBadge = document.createElement("span");
|
|
146
|
+
activeBadge.className = "mini-badge active";
|
|
147
|
+
activeBadge.textContent = "当前使用中";
|
|
148
|
+
badges.appendChild(activeBadge);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const urlBadge = document.createElement("span");
|
|
152
|
+
urlBadge.className = "mini-badge";
|
|
153
|
+
urlBadge.textContent = "URL/Key";
|
|
154
|
+
badges.appendChild(urlBadge);
|
|
155
|
+
|
|
156
|
+
heading.append(title, badges);
|
|
157
|
+
|
|
158
|
+
const actions = document.createElement("div");
|
|
159
|
+
actions.className = "card-actions";
|
|
160
|
+
|
|
161
|
+
const switchButton = document.createElement("button");
|
|
162
|
+
switchButton.className = endpoint.isActive ? "ghost-button" : "primary-button";
|
|
163
|
+
switchButton.textContent = endpoint.isActive ? "当前连接" : "切换";
|
|
164
|
+
switchButton.disabled = endpoint.isActive;
|
|
165
|
+
switchButton.addEventListener("click", async () => {
|
|
166
|
+
await handleSwitchEndpoint(endpoint.id, endpoint.note);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const editButton = document.createElement("button");
|
|
170
|
+
editButton.className = "ghost-button";
|
|
171
|
+
editButton.textContent = "编辑";
|
|
172
|
+
editButton.addEventListener("click", () => {
|
|
173
|
+
fillForm(endpoint);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const deleteButton = document.createElement("button");
|
|
177
|
+
deleteButton.className = "danger-button";
|
|
178
|
+
deleteButton.textContent = "删除";
|
|
179
|
+
deleteButton.addEventListener("click", async () => {
|
|
180
|
+
await handleDeleteEndpoint(endpoint.id, endpoint.note);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
actions.append(switchButton, editButton, deleteButton);
|
|
184
|
+
top.append(heading, actions);
|
|
185
|
+
|
|
186
|
+
const meta = document.createElement("div");
|
|
187
|
+
meta.className = "profile-meta";
|
|
188
|
+
meta.innerHTML = `
|
|
189
|
+
<span><strong>URL</strong>${endpoint.url || "-"}</span>
|
|
190
|
+
<span><strong>Key</strong>${endpoint.maskedKey || "-"}</span>
|
|
191
|
+
<span><strong>更新时间</strong>${formatDateTime(endpoint.updatedAt)}</span>
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
card.append(top, meta);
|
|
195
|
+
return card;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderEndpoints() {
|
|
199
|
+
const list = $("#endpointsList");
|
|
200
|
+
list.innerHTML = "";
|
|
201
|
+
|
|
202
|
+
if (state.endpoints.length === 0) {
|
|
203
|
+
const empty = document.createElement("div");
|
|
204
|
+
empty.className = "empty-state";
|
|
205
|
+
empty.textContent = "当前还没有连接,先在右侧新增一条。";
|
|
206
|
+
list.appendChild(empty);
|
|
207
|
+
} else {
|
|
208
|
+
state.endpoints.forEach((endpoint) => {
|
|
209
|
+
list.appendChild(createEndpointCard(endpoint));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
$("#endpointCount").textContent = `${state.endpoints.length} 条`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function refreshDashboard(showMessage = true) {
|
|
217
|
+
try {
|
|
218
|
+
const [current, endpoints, paths] = await Promise.all([
|
|
219
|
+
bridge.getCurrentConfig(),
|
|
220
|
+
bridge.listEndpoints(),
|
|
221
|
+
bridge.getPaths(),
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
state.current = current;
|
|
225
|
+
state.endpoints = endpoints;
|
|
226
|
+
state.paths = paths;
|
|
227
|
+
|
|
228
|
+
renderCurrent();
|
|
229
|
+
renderEndpoints();
|
|
230
|
+
|
|
231
|
+
if (showMessage) {
|
|
232
|
+
setStatus("已刷新当前连接状态。", "success");
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
setStatus(`刷新失败:${error.message}`, "error");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function handleSwitchEndpoint(id, note) {
|
|
240
|
+
try {
|
|
241
|
+
setStatus(`正在切换到:${note} ...`, "info");
|
|
242
|
+
const result = await bridge.switchEndpoint({ id });
|
|
243
|
+
await refreshDashboard(false);
|
|
244
|
+
setStatus(
|
|
245
|
+
`已切换到 ${note}。已备份 config.toml 和 auth.json,下一条 codex 命令直接生效。`,
|
|
246
|
+
"success",
|
|
247
|
+
);
|
|
248
|
+
return result;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
setStatus(`切换失败:${error.message}`, "error");
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function handleSubmitEndpoint(event) {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
const id = $("#endpointId").value.trim();
|
|
258
|
+
const note = $("#endpointNote").value.trim();
|
|
259
|
+
const url = $("#endpointUrl").value.trim();
|
|
260
|
+
const key = $("#endpointKey").value.trim();
|
|
261
|
+
|
|
262
|
+
if (!note || !url || !key) {
|
|
263
|
+
setStatus("备注、URL、Key 都必须填写。", "error");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
if (id) {
|
|
269
|
+
await bridge.updateEndpoint({ id, note, url, key });
|
|
270
|
+
setStatus(`已更新连接:${note}`, "success");
|
|
271
|
+
} else {
|
|
272
|
+
await bridge.createEndpoint({ note, url, key });
|
|
273
|
+
setStatus(`已新增连接:${note}`, "success");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
resetForm();
|
|
277
|
+
await refreshDashboard(false);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
setStatus(`保存失败:${error.message}`, "error");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function handleDeleteEndpoint(id, note) {
|
|
284
|
+
const confirmed = window.confirm(`确定删除连接“${note}”吗?`);
|
|
285
|
+
if (!confirmed) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await bridge.deleteEndpoint({ id });
|
|
291
|
+
if ($("#endpointId").value.trim() === id) {
|
|
292
|
+
resetForm();
|
|
293
|
+
}
|
|
294
|
+
await refreshDashboard(false);
|
|
295
|
+
setStatus(`已删除连接:${note}`, "success");
|
|
296
|
+
} catch (error) {
|
|
297
|
+
setStatus(`删除失败:${error.message}`, "error");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function handleOpenPath(targetPath, label) {
|
|
302
|
+
try {
|
|
303
|
+
const errorMessage = await bridge.openPath(targetPath);
|
|
304
|
+
if (errorMessage) {
|
|
305
|
+
throw new Error(errorMessage);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
setStatus(`已打开${label}。`, "success");
|
|
309
|
+
} catch (error) {
|
|
310
|
+
setStatus(`打开${label}失败:${error.message}`, "error");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function bindEvents() {
|
|
315
|
+
$("#refreshButton").addEventListener("click", () => {
|
|
316
|
+
refreshDashboard();
|
|
317
|
+
});
|
|
318
|
+
$("#endpointForm").addEventListener("submit", handleSubmitEndpoint);
|
|
319
|
+
$("#clearFormButton").addEventListener("click", () => {
|
|
320
|
+
resetForm();
|
|
321
|
+
setStatus("已清空表单。", "info");
|
|
322
|
+
});
|
|
323
|
+
$("#openCodexRootButton").addEventListener("click", () => {
|
|
324
|
+
if (state.paths?.codexRoot) {
|
|
325
|
+
handleOpenPath(state.paths.codexRoot, " Codex 目录");
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
$("#openEndpointStoreButton").addEventListener("click", () => {
|
|
329
|
+
if (state.paths?.endpointStorePath) {
|
|
330
|
+
handleOpenPath(state.paths.endpointStorePath, " 连接数据文件");
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
window.addEventListener("DOMContentLoaded", async () => {
|
|
336
|
+
bindEvents();
|
|
337
|
+
resetForm();
|
|
338
|
+
await refreshDashboard(false);
|
|
339
|
+
setStatus("应用已就绪,你可以直接新增 URL / Key / 备注 并切换。", "success");
|
|
340
|
+
});
|