codex-endpoint-switcher 1.3.1 → 1.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-endpoint-switcher",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "用于切换 Codex URL 和 Key 的本地网页控制台与 npm CLI",
5
5
  "main": "src/main/main.js",
6
6
  "bin": {
@@ -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>
@@ -101,27 +101,87 @@ code {
101
101
  }
102
102
 
103
103
  .auth-card {
104
- width: min(560px, 100%);
104
+ width: min(980px, 100%);
105
105
  min-height: auto;
106
- padding: 20px;
106
+ padding: 22px;
107
107
  border-radius: 36px;
108
- display: block;
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: 14px;
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;
@@ -742,6 +806,11 @@ input[readonly] {
742
806
  height: auto;
743
807
  }
744
808
 
809
+ .auth-card {
810
+ width: min(720px, 100%);
811
+ grid-template-columns: 1fr;
812
+ }
813
+
745
814
  .content-grid {
746
815
  grid-template-columns: 1fr;
747
816
  }
@@ -793,16 +862,40 @@ input[readonly] {
793
862
 
794
863
  .auth-card {
795
864
  width: 100%;
796
- padding: 10px;
865
+ padding: 12px;
797
866
  border-radius: 24px;
867
+ gap: 12px;
798
868
  }
799
869
 
870
+ .auth-hero-card,
800
871
  .auth-form-card,
801
872
  .panel {
802
873
  border-radius: 20px;
803
874
  padding: 14px;
804
875
  }
805
876
 
877
+ .auth-hero-card {
878
+ gap: 14px;
879
+ }
880
+
881
+ .auth-hero-title {
882
+ font-size: 1.8rem;
883
+ }
884
+
885
+ .auth-hero-copy {
886
+ font-size: 0.9rem;
887
+ line-height: 1.6;
888
+ }
889
+
890
+ .auth-highlight-list {
891
+ gap: 8px;
892
+ }
893
+
894
+ .auth-highlight-item {
895
+ padding: 12px;
896
+ border-radius: 18px;
897
+ }
898
+
806
899
  .account-hint {
807
900
  font-size: 0.82rem;
808
901
  line-height: 1.4;
@@ -925,6 +1018,7 @@ input[readonly] {
925
1018
  .auth-card {
926
1019
  min-height: auto;
927
1020
  padding: 16px;
1021
+ gap: 14px;
928
1022
  }
929
1023
 
930
1024
  .panel {
@@ -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
- method: req.method,
31
- headers,
32
- },
33
- (upstreamResponse) => {
34
- res.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
35
- upstreamResponse.pipe(res);
36
- },
37
- );
38
-
39
- upstreamRequest.on("error", (error) => {
40
- if (!res.headersSent) {
41
- res.writeHead(502, { "Content-Type": "application/json; charset=utf-8" });
42
- }
43
- res.end(
44
- JSON.stringify({
45
- ok: false,
46
- error: `代理转发失败:${error.message}`,
47
- }),
48
- );
171
+ requestModule,
172
+ headers,
173
+ method: req.method,
174
+ bodyBuffer,
49
175
  });
50
176
 
51
- req.pipe(upstreamRequest);
177
+ res.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
178
+ upstreamResponse.pipe(res);
52
179
  } catch (error) {
53
180
  if (!res.headersSent) {
54
- res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
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,7 @@ 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 { createProxyServer } = require("./proxy-server");
6
+ const { createProxyServer, warmUpCurrentTarget } = require("./proxy-server");
7
7
 
8
8
  function wrapAsync(handler) {
9
9
  return async (req, res) => {
@@ -226,6 +226,9 @@ function startServer(options = {}) {
226
226
  });
227
227
 
228
228
  proxyServer.listen(proxyPort);
229
+ warmUpCurrentTarget().catch(() => {
230
+ // 预热失败不阻断服务启动,真正请求时仍会走自动重试。
231
+ });
229
232
 
230
233
  const controller = {
231
234
  webServer,