easyrouter-config 1.0.2 → 1.0.4

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.
Files changed (2) hide show
  1. package/dist/index.js +161 -67
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16,9 +16,11 @@ import {
16
16
  import { execa } from "execa";
17
17
  import pc from "picocolors";
18
18
  import { parseArgs } from "util";
19
- var VERSION = "0.1.0";
20
- var CLAUDE_BASE_URL = "https://easyrouter.io";
21
- var CODEX_BASE_URL = "https://easyrouter.io/v1";
19
+ var VERSION = true ? "1.0.4" : "0.0.0-dev";
20
+ var PRIMARY_HOST = "https://easyrouter.io";
21
+ var FALLBACK_HOSTS = ["https://ezr.sh"];
22
+ var PROBE_TIMEOUT_MS = 5e3;
23
+ var CODEX_DEFAULT_MODEL = "gpt-5-codex";
22
24
  function parseCliArgs() {
23
25
  try {
24
26
  const { values } = parseArgs({
@@ -60,76 +62,165 @@ ${pc.bold("\u53C2\u6570:")}
60
62
  -k, --api-key <key> EasyRouter API Key\uFF08\u53EF\u7701\u7565 sk- \u524D\u7F00\uFF09
61
63
  --only <tool> claude | codex \uFF08\u53EA\u914D\u5176\u4E00\uFF09
62
64
  --skip-verify \u8DF3\u8FC7\u8FDE\u901A\u6027\u9A8C\u8BC1
63
- -v, --verbose \u663E\u793A zcf \u5B50\u8FDB\u7A0B\u8BE6\u7EC6\u8F93\u51FA
65
+ -v, --verbose \u663E\u793A\u5E95\u5C42\u7EC6\u8282\uFF08\u5305\u62EC BaseURL \u63A2\u6D4B\u8FC7\u7A0B\uFF09
64
66
  -h, --help \u663E\u793A\u5E2E\u52A9
65
67
  -V, --version \u663E\u793A\u7248\u672C
66
68
 
67
69
  ${pc.bold("\u5185\u7F6E BaseURL:")}
68
- Claude Code \u2192 ${CLAUDE_BASE_URL}
69
- Codex \u2192 ${CODEX_BASE_URL}
70
+ Claude Code \u2192 ${PRIMARY_HOST}
71
+ Codex \u2192 ${PRIMARY_HOST}/v1
70
72
 
71
73
  ${pc.bold("\u539F\u7406:")}
72
- \u672C\u5DE5\u5177\u662F ${pc.underline("zcf")} (https://github.com/UfoMiao/zcf) \u7684\u8584\u5C01\u88C5\u3002
73
- \u5B9E\u9645\u5199\u5165\u7684\u914D\u7F6E\u5B8C\u5168\u7531 zcf \u5B8C\u6210\uFF1A
74
+ Claude Code \u914D\u7F6E\u7531 ${pc.underline("zcf")} (https://github.com/UfoMiao/zcf) \u81EA\u52A8\u5199\u5165
75
+ Codex \u914D\u7F6E\u7531\u672C\u5DE5\u5177\u76F4\u63A5\u5199\u5165\uFF08\u66F4\u53EF\u63A7\u3001\u66F4\u7A33\u5B9A\uFF09
76
+
77
+ \u5B9E\u9645\u5199\u5165\u7684\u6587\u4EF6\uFF1A
74
78
  \u2022 Claude Code \u2192 ~/.claude/settings.json
75
79
  \u2022 Codex \u2192 ~/.codex/config.toml + ~/.codex/auth.json
76
80
  `);
77
81
  }
78
- async function runZcf(opts) {
82
+ async function pickAvailableHost(verbose) {
83
+ const candidates = [PRIMARY_HOST, ...FALLBACK_HOSTS];
84
+ for (const host of candidates) {
85
+ const ok = await probeHost(host);
86
+ if (verbose) {
87
+ const tag = ok ? pc.green("\u2713") : pc.red("\u2717");
88
+ log.info(`${tag} \u63A2\u6D4B ${host} \u2192 ${ok ? "\u53EF\u8FBE" : "\u4E0D\u53EF\u8FBE"}`);
89
+ }
90
+ if (ok) return host;
91
+ }
92
+ if (verbose) log.warn(`\u6240\u6709\u5019\u9009\u57DF\u540D\u5747\u4E0D\u53EF\u8FBE\uFF0C\u4F7F\u7528\u4E3B\u57DF\u540D ${PRIMARY_HOST}`);
93
+ return PRIMARY_HOST;
94
+ }
95
+ async function probeHost(host) {
96
+ const url = host.replace(/\/+$/, "") + "/v1/models";
97
+ try {
98
+ const ctrl = new AbortController();
99
+ const timer = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT_MS);
100
+ const res = await fetch(url, { method: "HEAD", signal: ctrl.signal });
101
+ clearTimeout(timer);
102
+ return res.status > 0;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+ var IDLE_TIMEOUT_MS = 6e4;
108
+ var HARD_TIMEOUT_MS = 3e5;
109
+ async function configureViaZcf(opts) {
79
110
  const args = [
80
111
  "-y",
81
112
  "zcf",
82
113
  "init",
83
114
  "-s",
84
- // --skip-prompt
85
- "--code-type",
86
- opts.codeType,
87
- "-p",
88
- "custom",
115
+ // --skip-prompt 非交互模式
116
+ "-T",
117
+ opts.target,
118
+ // 'cc' | 'codex'(用文档推荐的简写 -T)
119
+ // EasyRouter 是 OpenAI 兼容网关,统一用 api_key(Bearer Authorization)
89
120
  "--api-type",
90
- "auth_token",
91
- // EasyRouter 走 Bearer
92
- "-u",
93
- opts.baseUrl,
94
- "-k",
121
+ "api_key",
122
+ "--api-key",
95
123
  opts.apiKey,
124
+ "--api-url",
125
+ opts.baseUrl,
96
126
  "-r",
97
127
  "backup",
98
128
  // 已有配置自动备份
99
129
  "--mcp-services",
100
130
  "skip",
101
- // 不装额外 MCP,加速
131
+ // 不装 MCP(即便 zcf 偶尔忽略也无伤)
102
132
  "--workflows",
103
133
  "skip",
104
- // 不装工作流模板
105
134
  "--output-styles",
106
135
  "skip",
107
- // 不装输出样式
108
136
  "--install-cometix-line",
109
137
  "false"
110
- // 不装状态栏
111
138
  ];
139
+ if (opts.target === "codex") {
140
+ args.push("--api-model", CODEX_DEFAULT_MODEL);
141
+ }
142
+ const isWin = process.platform === "win32";
143
+ const cmd = isWin ? "npx.cmd" : "npx";
112
144
  let capturedOut = "";
113
145
  let capturedErr = "";
114
- try {
115
- const child = execa("npx", args, {
116
- stdio: opts.verbose ? "inherit" : "pipe",
117
- env: { ...process.env, FORCE_COLOR: opts.verbose ? "1" : "0" },
118
- reject: true
119
- });
120
- if (!opts.verbose) {
121
- child.stdout?.on("data", (chunk) => {
122
- capturedOut += chunk.toString();
123
- });
124
- child.stderr?.on("data", (chunk) => {
125
- capturedErr += chunk.toString();
126
- });
146
+ let lastDataAt = Date.now();
147
+ let killedReason = null;
148
+ let lastProgressAt = 0;
149
+ const PROGRESS_THROTTLE_MS = 200;
150
+ const child = execa(cmd, args, {
151
+ stdio: ["ignore", "pipe", "pipe"],
152
+ env: {
153
+ ...process.env,
154
+ FORCE_COLOR: "0",
155
+ CI: "1",
156
+ npm_config_yes: "true"
157
+ },
158
+ reject: false,
159
+ windowsHide: true
160
+ });
161
+ const handleChunk = (chunk, target) => {
162
+ const text2 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
163
+ if (target === "out") capturedOut += text2;
164
+ else capturedErr += text2;
165
+ lastDataAt = Date.now();
166
+ if (opts.onProgress) {
167
+ const now = Date.now();
168
+ if (now - lastProgressAt >= PROGRESS_THROTTLE_MS) {
169
+ lastProgressAt = now;
170
+ const lines = text2.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
171
+ const last = lines[lines.length - 1];
172
+ if (last) opts.onProgress(last.slice(0, 60));
173
+ }
127
174
  }
128
- await child;
129
- } catch (err) {
130
- const detail = err?.stderr || err?.stdout || err?.message || String(err);
175
+ if (opts.verbose) {
176
+ process.stderr.write(text2);
177
+ }
178
+ };
179
+ child.stdout?.on("data", (c) => handleChunk(c, "out"));
180
+ child.stderr?.on("data", (c) => handleChunk(c, "err"));
181
+ const idleTimer = setInterval(() => {
182
+ if (Date.now() - lastDataAt > IDLE_TIMEOUT_MS) {
183
+ killedReason = `\u5B50\u8FDB\u7A0B\u5DF2 ${Math.floor(IDLE_TIMEOUT_MS / 1e3)} \u79D2\u65E0\u8F93\u51FA\uFF08\u7591\u4F3C\u5361\u5728\u4EA4\u4E92\u5F0F\u63D0\u95EE\u6216\u7F51\u7EDC\u95EE\u9898\uFF09`;
184
+ child.kill("SIGTERM");
185
+ setTimeout(() => {
186
+ try {
187
+ child.kill("SIGKILL");
188
+ } catch {
189
+ }
190
+ }, 5e3);
191
+ }
192
+ }, 5e3);
193
+ const hardTimer = setTimeout(() => {
194
+ killedReason = `\u5B50\u8FDB\u7A0B\u8D85\u8FC7 ${Math.floor(HARD_TIMEOUT_MS / 1e3)} \u79D2\u672A\u7ED3\u675F\uFF08\u5F3A\u5236\u8D85\u65F6\uFF09`;
195
+ child.kill("SIGTERM");
196
+ setTimeout(() => {
197
+ try {
198
+ child.kill("SIGKILL");
199
+ } catch {
200
+ }
201
+ }, 5e3);
202
+ }, HARD_TIMEOUT_MS);
203
+ const result = await child.catch((e) => e);
204
+ clearInterval(idleTimer);
205
+ clearTimeout(hardTimer);
206
+ const exitCode = result?.exitCode ?? null;
207
+ const signal = result?.signal ?? null;
208
+ if (killedReason) {
131
209
  throw new Error(
132
- `zcf \u6267\u884C\u5931\u8D25 (code-type=${opts.codeType}):
210
+ `zcf \u8C03\u7528\u5931\u8D25\uFF1A${killedReason}
211
+
212
+ \u5E38\u89C1\u539F\u56E0\uFF1A
213
+ \u2022 \u76EE\u6807\u5BA2\u6237\u7AEF\u672A\u5B89\u88C5\uFF08zcf \u8BD5\u56FE\u8BE2\u95EE\u662F\u5426\u5B89\u88C5\uFF09
214
+ \u2022 npm \u955C\u50CF/\u7F51\u7EDC\u6162\uFF0Cnpx \u4E0B\u8F7D zcf \u5305\u5361\u4F4F
215
+ \u2022 \u9632\u706B\u5899\u62E6\u622A\u4E86 registry.npmjs.org
216
+
217
+ \u5EFA\u8BAE\uFF1A\u7528 ${pc.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u67E5\u770B\u8BE6\u7EC6\u65E5\u5FD7`
218
+ );
219
+ }
220
+ if (exitCode !== 0 || signal) {
221
+ const detail = (capturedErr || capturedOut || "").slice(-2e3) || `exit ${exitCode}, signal ${signal}`;
222
+ throw new Error(
223
+ `zcf \u5F02\u5E38\u9000\u51FA\uFF08exit=${exitCode}\uFF09:
133
224
  ${detail}
134
225
 
135
226
  \u{1F4A1} \u7528 ${pc.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
@@ -139,7 +230,7 @@ ${detail}
139
230
  if (/CACError|Unknown option/i.test(combined)) {
140
231
  const match = combined.match(/(CACError:.*?)(?:\n|$)/);
141
232
  throw new Error(
142
- `zcf \u62D2\u7EDD\u4E86\u6211\u4EEC\u4F20\u7684\u53C2\u6570\uFF08code-type=${opts.codeType}\uFF09\uFF1A
233
+ `zcf \u62D2\u7EDD\u4E86\u6211\u4EEC\u4F20\u7684\u53C2\u6570\uFF1A
143
234
  ${match?.[1] || "Unknown option"}
144
235
 
145
236
  \u8FD9\u901A\u5E38\u610F\u5473\u7740 zcf \u5347\u7EA7\u540E flag \u547D\u540D\u53D8\u4E86\u3002\u8BF7\u8054\u7CFB service@easyrouter.io\u3002`
@@ -187,6 +278,10 @@ async function main() {
187
278
  }
188
279
  console.log();
189
280
  intro(pc.bgCyan(pc.black(" EasyRouter Config ")) + pc.dim(" v" + VERSION));
281
+ const verbose = !!args.verbose;
282
+ const selectedHost = await pickAvailableHost(verbose);
283
+ const claudeBaseUrl = selectedHost;
284
+ const codexBaseUrl = selectedHost.replace(/\/+$/, "") + "/v1";
190
285
  let apiKey;
191
286
  if (args.apiKey) {
192
287
  apiKey = normalizeApiKey(args.apiKey);
@@ -233,8 +328,8 @@ async function main() {
233
328
  note(
234
329
  [
235
330
  `${pc.bold("\u8BA1\u8D39\u6A21\u5F0F")} \u6309\u91CF\u4ED8\u8D39`,
236
- configClaude ? `${pc.bold("Claude URL")} ${CLAUDE_BASE_URL}` : null,
237
- configCodex ? `${pc.bold("Codex URL")} ${CODEX_BASE_URL}` : null,
331
+ configClaude ? `${pc.bold("Claude URL")} ${PRIMARY_HOST}` : null,
332
+ configCodex ? `${pc.bold("Codex URL")} ${PRIMARY_HOST}/v1` : null,
238
333
  `${pc.bold("API Key")} ${maskKey(apiKey)}`,
239
334
  `${pc.bold("\u5C06\u914D\u7F6E")} ${[
240
335
  configClaude && "Claude Code",
@@ -250,20 +345,19 @@ async function main() {
250
345
  process.exit(0);
251
346
  }
252
347
  }
253
- const verbose = !!args.verbose;
254
348
  if (configClaude) {
255
- if (verbose) {
256
- log.step("\u914D\u7F6E Claude Code\uFF08\u8BE6\u7EC6\u65E5\u5FD7\uFF09");
257
- }
258
- const s = verbose || !process.stdout.isTTY ? null : spinner();
349
+ const useSpinner = !verbose && process.stdout.isTTY;
350
+ const s = useSpinner ? spinner() : null;
259
351
  s?.start("\u914D\u7F6E Claude Code");
260
- if (!s && !verbose) log.info("\u6B63\u5728\u914D\u7F6E Claude Code\uFF08\u9996\u6B21\u8FD0\u884C\u9700\u4E0B\u8F7D zcf\uFF0C\u7EA6 30~60 \u79D2\uFF09...");
352
+ if (!s) log.info("\u6B63\u5728\u914D\u7F6E Claude Code\uFF08\u9996\u6B21\u8FD0\u884C\u9700\u4E0B\u8F7D zcf\uFF0C\u7EA6 30~60 \u79D2\uFF09...");
261
353
  try {
262
- await runZcf({
263
- codeType: "cc",
264
- baseUrl: CLAUDE_BASE_URL,
354
+ await configureViaZcf({
355
+ target: "cc",
356
+ baseUrl: claudeBaseUrl,
265
357
  apiKey,
266
- verbose
358
+ verbose,
359
+ // 节流的进度回调:spinner 不会高频闪烁
360
+ onProgress: (line) => s?.message(`\u914D\u7F6E Claude Code \xB7 ${pc.dim(line)}`)
267
361
  });
268
362
  s?.stop(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.claude/settings.json"));
269
363
  if (!s) log.success(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 \u2192 ~/.claude/settings.json"));
@@ -274,21 +368,20 @@ async function main() {
274
368
  }
275
369
  }
276
370
  if (configCodex) {
277
- if (verbose) {
278
- log.step("\u914D\u7F6E Codex\uFF08\u8BE6\u7EC6\u65E5\u5FD7\uFF09");
279
- }
280
- const s = verbose || !process.stdout.isTTY ? null : spinner();
371
+ const useSpinner = !verbose && process.stdout.isTTY;
372
+ const s = useSpinner ? spinner() : null;
281
373
  s?.start("\u914D\u7F6E Codex");
282
- if (!s && !verbose) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
374
+ if (!s) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
283
375
  try {
284
- await runZcf({
285
- codeType: "cx",
286
- baseUrl: CODEX_BASE_URL,
376
+ await configureViaZcf({
377
+ target: "codex",
378
+ baseUrl: codexBaseUrl,
287
379
  apiKey,
288
- verbose
380
+ verbose,
381
+ onProgress: (line) => s?.message(`\u914D\u7F6E Codex \xB7 ${pc.dim(line)}`)
289
382
  });
290
- s?.stop(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.codex/config.toml"));
291
- if (!s) log.success(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 \u2192 ~/.codex/config.toml"));
383
+ s?.stop(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.codex/config.toml + auth.json"));
384
+ if (!s) log.success(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 \u2192 ~/.codex/config.toml + auth.json"));
292
385
  } catch (err) {
293
386
  s?.stop(pc.red("\u2717 Codex \u914D\u7F6E\u5931\u8D25"));
294
387
  console.error(err.message);
@@ -296,10 +389,11 @@ async function main() {
296
389
  }
297
390
  }
298
391
  if (!args.skipVerify) {
299
- const s = process.stdout.isTTY ? spinner() : null;
392
+ const useSpinner = process.stdout.isTTY;
393
+ const s = useSpinner ? spinner() : null;
300
394
  s?.start("\u9A8C\u8BC1\u8FDE\u901A\u6027");
301
395
  if (!s) log.info("\u6B63\u5728\u9A8C\u8BC1\u8FDE\u901A\u6027...");
302
- const result = await verifyConnection(CLAUDE_BASE_URL, apiKey);
396
+ const result = await verifyConnection(claudeBaseUrl, apiKey);
303
397
  if (result.ok) {
304
398
  const msg = pc.green("\u2713 \u94FE\u8DEF\u9A8C\u8BC1\u901A\u8FC7");
305
399
  s?.stop(msg) ?? log.success(msg);
@@ -315,7 +409,7 @@ async function main() {
315
409
  if (configCodex) tips.push(pc.cyan("codex") + pc.dim(" # \u542F\u52A8 Codex"));
316
410
  note(tips.join("\n"), "\u{1F389} \u5168\u90E8\u5B8C\u6210\uFF01\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
317
411
  outro(
318
- pc.dim("\u611F\u8C22\u4F7F\u7528 EasyRouter ") + pc.underline(pc.cyan("https://easyrouter.io")) + pc.dim(" \xB7 powered by ") + pc.underline("zcf")
412
+ pc.dim("\u611F\u8C22\u4F7F\u7528 EasyRouter ") + pc.underline(pc.cyan(PRIMARY_HOST)) + pc.dim(" \xB7 Claude Code auto-configuration is powered by ") + pc.underline("UfoMiao/zcf")
319
413
  );
320
414
  }
321
415
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easyrouter-config",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "🚀 一键把 EasyRouter 接入 Claude Code & Codex —— 粘贴 Key 即用",
5
5
  "type": "module",
6
6
  "bin": {