easyrouter-config 1.0.3 → 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 +88 -57
  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.1";
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,100 @@ ${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
  }
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
+ }
78
107
  var IDLE_TIMEOUT_MS = 6e4;
79
108
  var HARD_TIMEOUT_MS = 3e5;
80
- async function runZcf(opts) {
109
+ async function configureViaZcf(opts) {
81
110
  const args = [
82
111
  "-y",
83
112
  "zcf",
84
113
  "init",
85
114
  "-s",
86
- // --skip-prompt
87
- "--code-type",
88
- opts.codeType,
89
- "-p",
90
- "custom",
115
+ // --skip-prompt 非交互模式
116
+ "-T",
117
+ opts.target,
118
+ // 'cc' | 'codex'(用文档推荐的简写 -T)
119
+ // EasyRouter 是 OpenAI 兼容网关,统一用 api_key(Bearer Authorization)
91
120
  "--api-type",
92
- "auth_token",
93
- // EasyRouter 走 Bearer
94
- "-u",
95
- opts.baseUrl,
96
- "-k",
121
+ "api_key",
122
+ "--api-key",
97
123
  opts.apiKey,
124
+ "--api-url",
125
+ opts.baseUrl,
98
126
  "-r",
99
127
  "backup",
100
128
  // 已有配置自动备份
101
129
  "--mcp-services",
102
130
  "skip",
103
- // 不装额外 MCP,加速
131
+ // 不装 MCP(即便 zcf 偶尔忽略也无伤)
104
132
  "--workflows",
105
133
  "skip",
106
- // 不装工作流模板
107
134
  "--output-styles",
108
135
  "skip",
109
- // 不装输出样式
110
136
  "--install-cometix-line",
111
137
  "false"
112
- // 不装状态栏
113
138
  ];
139
+ if (opts.target === "codex") {
140
+ args.push("--api-model", CODEX_DEFAULT_MODEL);
141
+ }
114
142
  const isWin = process.platform === "win32";
115
143
  const cmd = isWin ? "npx.cmd" : "npx";
116
144
  let capturedOut = "";
117
145
  let capturedErr = "";
118
146
  let lastDataAt = Date.now();
119
147
  let killedReason = null;
148
+ let lastProgressAt = 0;
149
+ const PROGRESS_THROTTLE_MS = 200;
120
150
  const child = execa(cmd, args, {
121
151
  stdio: ["ignore", "pipe", "pipe"],
122
152
  env: {
123
153
  ...process.env,
124
154
  FORCE_COLOR: "0",
125
- // 强制无颜色,免得 ANSI 干扰我们的检测
126
155
  CI: "1",
127
- // 让某些库自动进入"非交互模式"
128
156
  npm_config_yes: "true"
129
- // npx 自动 yes
130
157
  },
131
158
  reject: false,
132
- // 我们自己处理退出码,避免 throw 时丢失输出
133
159
  windowsHide: true
134
160
  });
135
161
  const handleChunk = (chunk, target) => {
@@ -138,9 +164,13 @@ async function runZcf(opts) {
138
164
  else capturedErr += text2;
139
165
  lastDataAt = Date.now();
140
166
  if (opts.onProgress) {
141
- const lines = text2.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
142
- const last = lines[lines.length - 1];
143
- if (last) opts.onProgress(last.slice(0, 80));
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
+ }
144
174
  }
145
175
  if (opts.verbose) {
146
176
  process.stderr.write(text2);
@@ -180,7 +210,7 @@ async function runZcf(opts) {
180
210
  `zcf \u8C03\u7528\u5931\u8D25\uFF1A${killedReason}
181
211
 
182
212
  \u5E38\u89C1\u539F\u56E0\uFF1A
183
- \u2022 \u6CA1\u88C5\u76EE\u6807\u5BA2\u6237\u7AEF\uFF08zcf \u8BD5\u56FE\u8BE2\u95EE\u662F\u5426\u5B89\u88C5\uFF09
213
+ \u2022 \u76EE\u6807\u5BA2\u6237\u7AEF\u672A\u5B89\u88C5\uFF08zcf \u8BD5\u56FE\u8BE2\u95EE\u662F\u5426\u5B89\u88C5\uFF09
184
214
  \u2022 npm \u955C\u50CF/\u7F51\u7EDC\u6162\uFF0Cnpx \u4E0B\u8F7D zcf \u5305\u5361\u4F4F
185
215
  \u2022 \u9632\u706B\u5899\u62E6\u622A\u4E86 registry.npmjs.org
186
216
 
@@ -190,7 +220,7 @@ async function runZcf(opts) {
190
220
  if (exitCode !== 0 || signal) {
191
221
  const detail = (capturedErr || capturedOut || "").slice(-2e3) || `exit ${exitCode}, signal ${signal}`;
192
222
  throw new Error(
193
- `zcf \u5F02\u5E38\u9000\u51FA\uFF08code-type=${opts.codeType}, exit=${exitCode}\uFF09:
223
+ `zcf \u5F02\u5E38\u9000\u51FA\uFF08exit=${exitCode}\uFF09:
194
224
  ${detail}
195
225
 
196
226
  \u{1F4A1} \u7528 ${pc.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
@@ -200,7 +230,7 @@ ${detail}
200
230
  if (/CACError|Unknown option/i.test(combined)) {
201
231
  const match = combined.match(/(CACError:.*?)(?:\n|$)/);
202
232
  throw new Error(
203
- `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
204
234
  ${match?.[1] || "Unknown option"}
205
235
 
206
236
  \u8FD9\u901A\u5E38\u610F\u5473\u7740 zcf \u5347\u7EA7\u540E flag \u547D\u540D\u53D8\u4E86\u3002\u8BF7\u8054\u7CFB service@easyrouter.io\u3002`
@@ -248,6 +278,10 @@ async function main() {
248
278
  }
249
279
  console.log();
250
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";
251
285
  let apiKey;
252
286
  if (args.apiKey) {
253
287
  apiKey = normalizeApiKey(args.apiKey);
@@ -294,8 +328,8 @@ async function main() {
294
328
  note(
295
329
  [
296
330
  `${pc.bold("\u8BA1\u8D39\u6A21\u5F0F")} \u6309\u91CF\u4ED8\u8D39`,
297
- configClaude ? `${pc.bold("Claude URL")} ${CLAUDE_BASE_URL}` : null,
298
- 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,
299
333
  `${pc.bold("API Key")} ${maskKey(apiKey)}`,
300
334
  `${pc.bold("\u5C06\u914D\u7F6E")} ${[
301
335
  configClaude && "Claude Code",
@@ -311,20 +345,18 @@ async function main() {
311
345
  process.exit(0);
312
346
  }
313
347
  }
314
- const verbose = !!args.verbose;
315
348
  if (configClaude) {
316
- if (verbose) {
317
- log.step("\u914D\u7F6E Claude Code\uFF08\u8BE6\u7EC6\u65E5\u5FD7\uFF09");
318
- }
319
- const s = verbose || !process.stdout.isTTY ? null : spinner();
349
+ const useSpinner = !verbose && process.stdout.isTTY;
350
+ const s = useSpinner ? spinner() : null;
320
351
  s?.start("\u914D\u7F6E Claude Code");
321
- 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...");
322
353
  try {
323
- await runZcf({
324
- codeType: "cc",
325
- baseUrl: CLAUDE_BASE_URL,
354
+ await configureViaZcf({
355
+ target: "cc",
356
+ baseUrl: claudeBaseUrl,
326
357
  apiKey,
327
358
  verbose,
359
+ // 节流的进度回调:spinner 不会高频闪烁
328
360
  onProgress: (line) => s?.message(`\u914D\u7F6E Claude Code \xB7 ${pc.dim(line)}`)
329
361
  });
330
362
  s?.stop(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.claude/settings.json"));
@@ -336,22 +368,20 @@ async function main() {
336
368
  }
337
369
  }
338
370
  if (configCodex) {
339
- if (verbose) {
340
- log.step("\u914D\u7F6E Codex\uFF08\u8BE6\u7EC6\u65E5\u5FD7\uFF09");
341
- }
342
- const s = verbose || !process.stdout.isTTY ? null : spinner();
371
+ const useSpinner = !verbose && process.stdout.isTTY;
372
+ const s = useSpinner ? spinner() : null;
343
373
  s?.start("\u914D\u7F6E Codex");
344
- if (!s && !verbose) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
374
+ if (!s) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
345
375
  try {
346
- await runZcf({
347
- codeType: "cx",
348
- baseUrl: CODEX_BASE_URL,
376
+ await configureViaZcf({
377
+ target: "codex",
378
+ baseUrl: codexBaseUrl,
349
379
  apiKey,
350
380
  verbose,
351
381
  onProgress: (line) => s?.message(`\u914D\u7F6E Codex \xB7 ${pc.dim(line)}`)
352
382
  });
353
- s?.stop(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.codex/config.toml"));
354
- 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"));
355
385
  } catch (err) {
356
386
  s?.stop(pc.red("\u2717 Codex \u914D\u7F6E\u5931\u8D25"));
357
387
  console.error(err.message);
@@ -359,10 +389,11 @@ async function main() {
359
389
  }
360
390
  }
361
391
  if (!args.skipVerify) {
362
- const s = process.stdout.isTTY ? spinner() : null;
392
+ const useSpinner = process.stdout.isTTY;
393
+ const s = useSpinner ? spinner() : null;
363
394
  s?.start("\u9A8C\u8BC1\u8FDE\u901A\u6027");
364
395
  if (!s) log.info("\u6B63\u5728\u9A8C\u8BC1\u8FDE\u901A\u6027...");
365
- const result = await verifyConnection(CLAUDE_BASE_URL, apiKey);
396
+ const result = await verifyConnection(claudeBaseUrl, apiKey);
366
397
  if (result.ok) {
367
398
  const msg = pc.green("\u2713 \u94FE\u8DEF\u9A8C\u8BC1\u901A\u8FC7");
368
399
  s?.stop(msg) ?? log.success(msg);
@@ -378,7 +409,7 @@ async function main() {
378
409
  if (configCodex) tips.push(pc.cyan("codex") + pc.dim(" # \u542F\u52A8 Codex"));
379
410
  note(tips.join("\n"), "\u{1F389} \u5168\u90E8\u5B8C\u6210\uFF01\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
380
411
  outro(
381
- 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")
382
413
  );
383
414
  }
384
415
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easyrouter-config",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "🚀 一键把 EasyRouter 接入 Claude Code & Codex —— 粘贴 Key 即用",
5
5
  "type": "module",
6
6
  "bin": {