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.
@@ -64,6 +64,18 @@
64
64
  <span class="metric-label">模型</span>
65
65
  <strong id="currentModel">-</strong>
66
66
  </article>
67
+ <article class="metric-card metric-card-wide">
68
+ <span class="metric-label">热更新模式</span>
69
+ <strong id="proxyModeStatus">未开启</strong>
70
+ <p class="field-hint" id="proxyModeHint">
71
+ 当前还是直连模式。开启后,后续同一 Codex 会话可在下一次请求时热更新到新 URL/Key。
72
+ </p>
73
+ <div class="action-row metric-actions">
74
+ <button id="enableProxyModeButton" type="button" class="secondary-button">
75
+ 开启热更新模式
76
+ </button>
77
+ </div>
78
+ </article>
67
79
  </div>
68
80
  </section>
69
81
 
@@ -110,6 +122,77 @@
110
122
  <button id="clearFormButton" type="button" class="ghost-button">清空表单</button>
111
123
  </div>
112
124
  </form>
125
+
126
+ <div class="stack-form stack-form-alt">
127
+ <div>
128
+ <p class="panel-kicker">Cloud Sync</p>
129
+ <h3>账号配置同步</h3>
130
+ </div>
131
+ <label class="field">
132
+ <span>同步服务器</span>
133
+ <input
134
+ id="cloudServerUrl"
135
+ type="text"
136
+ placeholder="例如:http://127.0.0.1:3190"
137
+ autocomplete="off"
138
+ />
139
+ </label>
140
+ <label class="field">
141
+ <span>账号</span>
142
+ <input
143
+ id="cloudUsername"
144
+ type="text"
145
+ placeholder="例如:yourname"
146
+ autocomplete="off"
147
+ />
148
+ </label>
149
+ <label class="field">
150
+ <span>密码</span>
151
+ <input
152
+ id="cloudPassword"
153
+ type="password"
154
+ placeholder="输入同步账号密码"
155
+ autocomplete="off"
156
+ />
157
+ </label>
158
+ <p class="field-hint">
159
+ 账号空间里保存的是你的连接配置。推送是把本机连接上传到账号,拉取是把账号里的连接同步回本机。
160
+ </p>
161
+ <div class="sync-status-grid">
162
+ <article class="sync-status-card">
163
+ <span class="metric-label">登录状态</span>
164
+ <strong id="cloudAuthStatus">未登录</strong>
165
+ </article>
166
+ <article class="sync-status-card">
167
+ <span class="metric-label">服务器</span>
168
+ <strong id="cloudRemoteStatus">未设置</strong>
169
+ </article>
170
+ <article class="sync-status-card sync-status-card-wide">
171
+ <span class="metric-label">同步记录</span>
172
+ <strong id="cloudLastSyncStatus">还没有推送或拉取记录</strong>
173
+ </article>
174
+ </div>
175
+ <div class="action-row">
176
+ <button id="cloudRegisterButton" type="button" class="secondary-button">
177
+ 注册账号
178
+ </button>
179
+ <button id="cloudLoginButton" type="button" class="primary-button">
180
+ 登录账号
181
+ </button>
182
+ <button id="cloudLogoutButton" type="button" class="ghost-button">退出登录</button>
183
+ </div>
184
+ <div class="action-row">
185
+ <button id="cloudPushButton" type="button" class="secondary-button">
186
+ 推送当前连接
187
+ </button>
188
+ <button id="cloudPullMergeButton" type="button" class="primary-button">
189
+ 合并拉取
190
+ </button>
191
+ <button id="cloudPullReplaceButton" type="button" class="danger-button">
192
+ 覆盖拉取
193
+ </button>
194
+ </div>
195
+ </div>
113
196
  </section>
114
197
 
115
198
  <section class="panel profiles-panel">
@@ -2,6 +2,7 @@ const state = {
2
2
  current: null,
3
3
  endpoints: [],
4
4
  paths: null,
5
+ cloud: null,
5
6
  };
6
7
 
7
8
  function createWebBridge() {
@@ -51,6 +52,42 @@ function createWebBridge() {
51
52
  body: JSON.stringify(payload),
52
53
  });
53
54
  },
55
+ enableProxyMode() {
56
+ return request("/api/proxy/enable", {
57
+ method: "POST",
58
+ });
59
+ },
60
+ getCloudSyncStatus() {
61
+ return request("/api/cloud/status");
62
+ },
63
+ registerCloudAccount(payload) {
64
+ return request("/api/cloud/register", {
65
+ method: "POST",
66
+ body: JSON.stringify(payload),
67
+ });
68
+ },
69
+ loginCloudAccount(payload) {
70
+ return request("/api/cloud/login", {
71
+ method: "POST",
72
+ body: JSON.stringify(payload),
73
+ });
74
+ },
75
+ logoutCloudAccount() {
76
+ return request("/api/cloud/logout", {
77
+ method: "POST",
78
+ });
79
+ },
80
+ pushCloudConfig() {
81
+ return request("/api/cloud/push", {
82
+ method: "POST",
83
+ });
84
+ },
85
+ pullCloudConfig(payload) {
86
+ return request("/api/cloud/pull", {
87
+ method: "POST",
88
+ body: JSON.stringify(payload),
89
+ });
90
+ },
54
91
  getPaths() {
55
92
  return request("/api/paths");
56
93
  },
@@ -91,6 +128,19 @@ function setStatus(message, type = "info") {
91
128
  statusBox.className = `status-box ${type}`;
92
129
  }
93
130
 
131
+ function syncFieldValue(selector, value) {
132
+ const input = $(selector);
133
+ if (!input) {
134
+ return;
135
+ }
136
+
137
+ if (document.activeElement === input && String(input.value || "").trim()) {
138
+ return;
139
+ }
140
+
141
+ input.value = value || "";
142
+ }
143
+
94
144
  function resetForm() {
95
145
  $("#endpointId").value = "";
96
146
  $("#endpointNote").value = "";
@@ -120,11 +170,64 @@ function renderCurrent() {
120
170
  $("#currentBaseUrl").textContent = current.currentUrl || "-";
121
171
  $("#currentKeyMasked").textContent = current.currentKeyMasked || "-";
122
172
  $("#currentModel").textContent = current.model || "-";
173
+ $("#proxyModeStatus").textContent = current.proxyModeEnabled
174
+ ? `已开启,固定代理:${current.proxyBaseUrl}`
175
+ : "未开启";
176
+ $("#proxyModeHint").textContent = current.proxyModeEnabled
177
+ ? "当前 Codex 只要已经接入本地代理,后续同一会话内切换连接会在下一次请求时热更新。"
178
+ : "当前还是直连模式。开启后需要把已打开的旧 Codex 会话重开一次;之后同一会话即可热更新。";
179
+ $("#enableProxyModeButton").disabled = current.proxyModeEnabled;
180
+ $("#enableProxyModeButton").textContent = current.proxyModeEnabled
181
+ ? "热更新模式已开启"
182
+ : "开启热更新模式";
123
183
  $("#heroBadge").textContent = current.currentNote
124
- ? `当前连接:${current.currentNote}`
184
+ ? `${current.proxyModeEnabled ? "热更新中" : "当前连接"}:${current.currentNote}`
125
185
  : "当前未匹配到已保存连接";
126
186
  }
127
187
 
188
+ function renderCloudStatus() {
189
+ const cloud = state.cloud;
190
+ if (!cloud) {
191
+ return;
192
+ }
193
+
194
+ syncFieldValue("#cloudServerUrl", cloud.serverUrl);
195
+ syncFieldValue("#cloudUsername", cloud.username);
196
+
197
+ const authText = !cloud.serverUrl
198
+ ? "未配置同步服务器"
199
+ : cloud.loggedIn
200
+ ? `已登录:${cloud.remoteUser || cloud.username}`
201
+ : cloud.hasToken
202
+ ? "本地已保存登录态,但远端校验未通过"
203
+ : "未登录";
204
+
205
+ const remoteText = cloud.serverUrl
206
+ ? cloud.lastError
207
+ ? `${cloud.serverUrl} · ${cloud.lastError}`
208
+ : cloud.serverUrl
209
+ : "未设置";
210
+
211
+ const syncStatusParts = [];
212
+ if (cloud.lastPushAt) {
213
+ syncStatusParts.push(`最近推送:${formatDateTime(cloud.lastPushAt)}`);
214
+ }
215
+ if (cloud.lastPullAt) {
216
+ syncStatusParts.push(`最近拉取:${formatDateTime(cloud.lastPullAt)}`);
217
+ }
218
+ if (!syncStatusParts.length) {
219
+ syncStatusParts.push("还没有推送或拉取记录");
220
+ }
221
+
222
+ $("#cloudAuthStatus").textContent = authText;
223
+ $("#cloudRemoteStatus").textContent = remoteText;
224
+ $("#cloudLastSyncStatus").textContent = syncStatusParts.join(" / ");
225
+ $("#cloudLogoutButton").disabled = !cloud.hasToken;
226
+ $("#cloudPushButton").disabled = !cloud.loggedIn;
227
+ $("#cloudPullMergeButton").disabled = !cloud.loggedIn;
228
+ $("#cloudPullReplaceButton").disabled = !cloud.loggedIn;
229
+ }
230
+
128
231
  function createEndpointCard(endpoint) {
129
232
  const card = document.createElement("article");
130
233
  card.className = `profile-card ${endpoint.isActive ? "active" : ""}`;
@@ -215,18 +318,21 @@ function renderEndpoints() {
215
318
 
216
319
  async function refreshDashboard(showMessage = true) {
217
320
  try {
218
- const [current, endpoints, paths] = await Promise.all([
321
+ const [current, endpoints, paths, cloud] = await Promise.all([
219
322
  bridge.getCurrentConfig(),
220
323
  bridge.listEndpoints(),
221
324
  bridge.getPaths(),
325
+ bridge.getCloudSyncStatus(),
222
326
  ]);
223
327
 
224
328
  state.current = current;
225
329
  state.endpoints = endpoints;
226
330
  state.paths = paths;
331
+ state.cloud = cloud;
227
332
 
228
333
  renderCurrent();
229
334
  renderEndpoints();
335
+ renderCloudStatus();
230
336
 
231
337
  if (showMessage) {
232
338
  setStatus("已刷新当前连接状态。", "success");
@@ -241,10 +347,14 @@ async function handleSwitchEndpoint(id, note) {
241
347
  setStatus(`正在切换到:${note} ...`, "info");
242
348
  const result = await bridge.switchEndpoint({ id });
243
349
  await refreshDashboard(false);
244
- setStatus(
245
- `已切换到 ${note}。已备份 config.toml 和 auth.json,下一条 codex 命令直接生效。`,
246
- "success",
247
- );
350
+ if (result.switchedViaProxy) {
351
+ setStatus(`已切换到 ${note}。当前会话下一次请求会直接走新的 URL/Key。`, "success");
352
+ } else {
353
+ setStatus(
354
+ `已切换到 ${note}。当前还是直连模式,只有新开的 Codex 会话会立即使用新配置。`,
355
+ "success",
356
+ );
357
+ }
248
358
  return result;
249
359
  } catch (error) {
250
360
  setStatus(`切换失败:${error.message}`, "error");
@@ -252,6 +362,109 @@ async function handleSwitchEndpoint(id, note) {
252
362
  }
253
363
  }
254
364
 
365
+ async function handleEnableProxyMode() {
366
+ try {
367
+ setStatus("正在切到热更新代理模式 ...", "info");
368
+ const result = await bridge.enableProxyMode();
369
+ await refreshDashboard(false);
370
+ if (result.oneTimeRestartRequired) {
371
+ setStatus(
372
+ "热更新模式已开启。请把当前已经打开着的旧 Codex 会话重开一次;之后同一会话内即可热更新。",
373
+ "success",
374
+ );
375
+ } else {
376
+ setStatus("热更新模式已开启。当前会话后续请求可直接热更新到新连接。", "success");
377
+ }
378
+ } catch (error) {
379
+ setStatus(`开启热更新模式失败:${error.message}`, "error");
380
+ }
381
+ }
382
+
383
+ function getCloudFormPayload() {
384
+ return {
385
+ serverUrl: $("#cloudServerUrl").value.trim(),
386
+ username: $("#cloudUsername").value.trim(),
387
+ password: $("#cloudPassword").value,
388
+ };
389
+ }
390
+
391
+ async function handleCloudRegister() {
392
+ try {
393
+ setStatus("正在注册同步账号 ...", "info");
394
+ const payload = getCloudFormPayload();
395
+ const result = await bridge.registerCloudAccount(payload);
396
+ $("#cloudPassword").value = "";
397
+ await refreshDashboard(false);
398
+ setStatus(result.message || "账号注册成功。", "success");
399
+ } catch (error) {
400
+ setStatus(`注册失败:${error.message}`, "error");
401
+ }
402
+ }
403
+
404
+ async function handleCloudLogin() {
405
+ try {
406
+ setStatus("正在登录同步账号 ...", "info");
407
+ const payload = getCloudFormPayload();
408
+ const result = await bridge.loginCloudAccount(payload);
409
+ $("#cloudPassword").value = "";
410
+ await refreshDashboard(false);
411
+ setStatus(result.message || "账号登录成功。", "success");
412
+ } catch (error) {
413
+ setStatus(`登录失败:${error.message}`, "error");
414
+ }
415
+ }
416
+
417
+ async function handleCloudLogout() {
418
+ try {
419
+ setStatus("正在退出同步账号 ...", "info");
420
+ const result = await bridge.logoutCloudAccount();
421
+ await refreshDashboard(false);
422
+ setStatus(result.message || "已退出同步账号。", "success");
423
+ } catch (error) {
424
+ setStatus(`退出失败:${error.message}`, "error");
425
+ }
426
+ }
427
+
428
+ async function handleCloudPush() {
429
+ try {
430
+ setStatus("正在把当前连接推送到账号空间 ...", "info");
431
+ const result = await bridge.pushCloudConfig();
432
+ await refreshDashboard(false);
433
+ setStatus(
434
+ result.message || `已推送 ${result.endpointCount || 0} 条连接到账号空间。`,
435
+ "success",
436
+ );
437
+ } catch (error) {
438
+ setStatus(`推送失败:${error.message}`, "error");
439
+ }
440
+ }
441
+
442
+ async function handleCloudPull(mode) {
443
+ if (mode === "replace") {
444
+ const confirmed = window.confirm(
445
+ "覆盖拉取会直接替换本机保存的连接列表。拉取前会自动备份,确定继续吗?",
446
+ );
447
+ if (!confirmed) {
448
+ return;
449
+ }
450
+ }
451
+
452
+ try {
453
+ setStatus(mode === "merge" ? "正在从账号空间合并拉取 ..." : "正在从账号空间覆盖拉取 ...", "info");
454
+ const result = await bridge.pullCloudConfig({ mode });
455
+ await refreshDashboard(false);
456
+ const activeHint = result.activeEndpointNote
457
+ ? `当前生效连接记录:${result.activeEndpointNote}。`
458
+ : "当前没有匹配到生效连接记录。";
459
+ setStatus(
460
+ `${result.message || "拉取完成。"} 新增 ${result.addedCount} 条,更新 ${result.updatedCount} 条。${activeHint}`,
461
+ "success",
462
+ );
463
+ } catch (error) {
464
+ setStatus(`拉取失败:${error.message}`, "error");
465
+ }
466
+ }
467
+
255
468
  async function handleSubmitEndpoint(event) {
256
469
  event.preventDefault();
257
470
  const id = $("#endpointId").value.trim();
@@ -320,6 +533,17 @@ function bindEvents() {
320
533
  resetForm();
321
534
  setStatus("已清空表单。", "info");
322
535
  });
536
+ $("#enableProxyModeButton").addEventListener("click", handleEnableProxyMode);
537
+ $("#cloudRegisterButton").addEventListener("click", handleCloudRegister);
538
+ $("#cloudLoginButton").addEventListener("click", handleCloudLogin);
539
+ $("#cloudLogoutButton").addEventListener("click", handleCloudLogout);
540
+ $("#cloudPushButton").addEventListener("click", handleCloudPush);
541
+ $("#cloudPullMergeButton").addEventListener("click", () => {
542
+ handleCloudPull("merge");
543
+ });
544
+ $("#cloudPullReplaceButton").addEventListener("click", () => {
545
+ handleCloudPull("replace");
546
+ });
323
547
  $("#openCodexRootButton").addEventListener("click", () => {
324
548
  if (state.paths?.codexRoot) {
325
549
  handleOpenPath(state.paths.codexRoot, " Codex 目录");
@@ -33,7 +33,8 @@ body {
33
33
  }
34
34
 
35
35
  button,
36
- input {
36
+ input,
37
+ textarea {
37
38
  font: inherit;
38
39
  }
39
40
 
@@ -259,7 +260,8 @@ code {
259
260
 
260
261
  input[type="text"],
261
262
  input[type="password"],
262
- input[type="file"] {
263
+ input[type="file"],
264
+ textarea {
263
265
  width: 100%;
264
266
  min-height: 46px;
265
267
  padding: 10px 14px;
@@ -269,6 +271,12 @@ input[type="file"] {
269
271
  color: var(--text);
270
272
  }
271
273
 
274
+ textarea {
275
+ min-height: 168px;
276
+ resize: vertical;
277
+ line-height: 1.6;
278
+ }
279
+
272
280
  input[readonly] {
273
281
  color: var(--muted);
274
282
  background: rgba(249, 244, 238, 0.95);
@@ -300,6 +308,33 @@ input[readonly] {
300
308
  gap: 10px;
301
309
  }
302
310
 
311
+ .sync-status-grid {
312
+ display: grid;
313
+ grid-template-columns: repeat(2, minmax(0, 1fr));
314
+ gap: 10px;
315
+ }
316
+
317
+ .sync-status-card {
318
+ padding: 14px;
319
+ border-radius: 14px;
320
+ background: rgba(255, 255, 255, 0.72);
321
+ border: 1px solid rgba(47, 36, 30, 0.08);
322
+ }
323
+
324
+ .sync-status-card strong {
325
+ display: block;
326
+ margin-top: 8px;
327
+ word-break: break-all;
328
+ }
329
+
330
+ .sync-status-card-wide {
331
+ grid-column: span 2;
332
+ }
333
+
334
+ .metric-actions {
335
+ margin-top: 12px;
336
+ }
337
+
303
338
  .primary-button,
304
339
  .secondary-button,
305
340
  .ghost-button {
@@ -504,4 +539,12 @@ input[readonly] {
504
539
  .hero-side {
505
540
  min-height: 180px;
506
541
  }
542
+
543
+ .sync-status-grid {
544
+ grid-template-columns: 1fr;
545
+ }
546
+
547
+ .sync-status-card-wide {
548
+ grid-column: auto;
549
+ }
507
550
  }