easyrouter-config 1.0.3 → 1.0.5

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 +150 -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.5" : "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_FALLBACK_MODEL = "gpt-5.5";
22
24
  function parseCliArgs() {
23
25
  try {
24
26
  const { values } = parseArgs({
@@ -60,76 +62,155 @@ ${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
+ }
107
+ var FETCH_MODELS_TIMEOUT_MS = 8e3;
108
+ async function fetchEasyRouterModels(host, verbose) {
109
+ const url = host.replace(/\/+$/, "") + "/api/pricing";
110
+ try {
111
+ const ctrl = new AbortController();
112
+ const timer = setTimeout(() => ctrl.abort(), FETCH_MODELS_TIMEOUT_MS);
113
+ const res = await fetch(url, { signal: ctrl.signal });
114
+ clearTimeout(timer);
115
+ if (!res.ok) {
116
+ if (verbose) log.warn(`\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1AHTTP ${res.status}\uFF08\u5C06\u4F7F\u7528\u515C\u5E95\u6A21\u578B\uFF09`);
117
+ return [];
118
+ }
119
+ const json = await res.json();
120
+ const models = Array.isArray(json?.data) ? json.data : [];
121
+ if (verbose) log.info(`\u2713 \u62C9\u53D6\u5230 ${models.length} \u4E2A EasyRouter \u6A21\u578B`);
122
+ return models;
123
+ } catch (err) {
124
+ if (verbose) log.warn(`\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5F02\u5E38\uFF1A${err?.message || err}\uFF08\u5C06\u4F7F\u7528\u515C\u5E95\u6A21\u578B\uFF09`);
125
+ return [];
126
+ }
127
+ }
128
+ function pickBestCodexModel(models) {
129
+ const isLlm = (m) => (m.tags || "").toUpperCase().includes("LLM");
130
+ const supportsOpenAI = (m) => !m.supported_endpoint_types || m.supported_endpoint_types.includes("openai");
131
+ const isGpt5 = (m) => /^gpt-5(\b|[.\-])/i.test(m.model_name);
132
+ const isSmallTier = (m) => /(^|[\-_.])(mini|nano|small|lite|tiny)([\-_.]|$)/i.test(m.model_name);
133
+ const byNameDesc = (a, b) => b.model_name.localeCompare(a.model_name, "en");
134
+ const gpt5Big = models.filter((m) => isGpt5(m) && isLlm(m) && supportsOpenAI(m) && !isSmallTier(m)).sort(byNameDesc);
135
+ if (gpt5Big.length) return gpt5Big[0].model_name;
136
+ const gpt5Any = models.filter((m) => isGpt5(m) && isLlm(m) && supportsOpenAI(m)).sort(byNameDesc);
137
+ if (gpt5Any.length) return gpt5Any[0].model_name;
138
+ const anyBig = models.filter((m) => isLlm(m) && supportsOpenAI(m) && !isSmallTier(m)).sort(byNameDesc);
139
+ if (anyBig.length) return anyBig[0].model_name;
140
+ return null;
141
+ }
142
+ async function resolveCodexModel(host, interactive, verbose) {
143
+ const models = await fetchEasyRouterModels(host, verbose);
144
+ const auto = pickBestCodexModel(models);
145
+ if (auto) {
146
+ if (verbose) log.info(`\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF1A${pc.cyan(auto)}`);
147
+ return auto;
148
+ }
149
+ if (interactive && models.length) {
150
+ const candidates = models.filter((m) => /^gpt-5|codex/i.test(m.model_name)).sort((a, b) => b.model_name.localeCompare(a.model_name, "en")).slice(0, 8);
151
+ if (candidates.length) {
152
+ const picked = await select({
153
+ message: "\u672A\u81EA\u52A8\u8BC6\u522B Codex \u63A8\u8350\u6A21\u578B\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9",
154
+ options: candidates.map((m) => ({ value: m.model_name, label: m.model_name }))
155
+ });
156
+ if (!isCancel(picked)) return picked;
157
+ }
158
+ }
159
+ if (verbose) log.warn(`\u65E0\u6CD5\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF0C\u4F7F\u7528\u515C\u5E95\uFF1A${CODEX_FALLBACK_MODEL}`);
160
+ return CODEX_FALLBACK_MODEL;
161
+ }
78
162
  var IDLE_TIMEOUT_MS = 6e4;
79
163
  var HARD_TIMEOUT_MS = 3e5;
80
- async function runZcf(opts) {
164
+ async function configureViaZcf(opts) {
81
165
  const args = [
82
166
  "-y",
83
167
  "zcf",
84
168
  "init",
85
169
  "-s",
86
- // --skip-prompt
87
- "--code-type",
88
- opts.codeType,
89
- "-p",
90
- "custom",
170
+ // --skip-prompt 非交互模式
171
+ "-T",
172
+ opts.target,
173
+ // 'cc' | 'codex'(用文档推荐的简写 -T)
174
+ // EasyRouter 是 OpenAI 兼容网关,统一用 api_key(Bearer Authorization)
91
175
  "--api-type",
92
- "auth_token",
93
- // EasyRouter 走 Bearer
94
- "-u",
95
- opts.baseUrl,
96
- "-k",
176
+ "api_key",
177
+ "--api-key",
97
178
  opts.apiKey,
179
+ "--api-url",
180
+ opts.baseUrl,
98
181
  "-r",
99
182
  "backup",
100
183
  // 已有配置自动备份
101
184
  "--mcp-services",
102
185
  "skip",
103
- // 不装额外 MCP,加速
186
+ // 不装 MCP(即便 zcf 偶尔忽略也无伤)
104
187
  "--workflows",
105
188
  "skip",
106
- // 不装工作流模板
107
189
  "--output-styles",
108
190
  "skip",
109
- // 不装输出样式
110
191
  "--install-cometix-line",
111
192
  "false"
112
- // 不装状态栏
113
193
  ];
194
+ if (opts.target === "codex") {
195
+ args.push("--api-model", opts.apiModel || CODEX_FALLBACK_MODEL);
196
+ }
114
197
  const isWin = process.platform === "win32";
115
198
  const cmd = isWin ? "npx.cmd" : "npx";
116
199
  let capturedOut = "";
117
200
  let capturedErr = "";
118
201
  let lastDataAt = Date.now();
119
202
  let killedReason = null;
203
+ let lastProgressAt = 0;
204
+ const PROGRESS_THROTTLE_MS = 200;
120
205
  const child = execa(cmd, args, {
121
206
  stdio: ["ignore", "pipe", "pipe"],
122
207
  env: {
123
208
  ...process.env,
124
209
  FORCE_COLOR: "0",
125
- // 强制无颜色,免得 ANSI 干扰我们的检测
126
210
  CI: "1",
127
- // 让某些库自动进入"非交互模式"
128
211
  npm_config_yes: "true"
129
- // npx 自动 yes
130
212
  },
131
213
  reject: false,
132
- // 我们自己处理退出码,避免 throw 时丢失输出
133
214
  windowsHide: true
134
215
  });
135
216
  const handleChunk = (chunk, target) => {
@@ -138,9 +219,13 @@ async function runZcf(opts) {
138
219
  else capturedErr += text2;
139
220
  lastDataAt = Date.now();
140
221
  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));
222
+ const now = Date.now();
223
+ if (now - lastProgressAt >= PROGRESS_THROTTLE_MS) {
224
+ lastProgressAt = now;
225
+ const lines = text2.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
226
+ const last = lines[lines.length - 1];
227
+ if (last) opts.onProgress(last.slice(0, 60));
228
+ }
144
229
  }
145
230
  if (opts.verbose) {
146
231
  process.stderr.write(text2);
@@ -180,7 +265,7 @@ async function runZcf(opts) {
180
265
  `zcf \u8C03\u7528\u5931\u8D25\uFF1A${killedReason}
181
266
 
182
267
  \u5E38\u89C1\u539F\u56E0\uFF1A
183
- \u2022 \u6CA1\u88C5\u76EE\u6807\u5BA2\u6237\u7AEF\uFF08zcf \u8BD5\u56FE\u8BE2\u95EE\u662F\u5426\u5B89\u88C5\uFF09
268
+ \u2022 \u76EE\u6807\u5BA2\u6237\u7AEF\u672A\u5B89\u88C5\uFF08zcf \u8BD5\u56FE\u8BE2\u95EE\u662F\u5426\u5B89\u88C5\uFF09
184
269
  \u2022 npm \u955C\u50CF/\u7F51\u7EDC\u6162\uFF0Cnpx \u4E0B\u8F7D zcf \u5305\u5361\u4F4F
185
270
  \u2022 \u9632\u706B\u5899\u62E6\u622A\u4E86 registry.npmjs.org
186
271
 
@@ -190,7 +275,7 @@ async function runZcf(opts) {
190
275
  if (exitCode !== 0 || signal) {
191
276
  const detail = (capturedErr || capturedOut || "").slice(-2e3) || `exit ${exitCode}, signal ${signal}`;
192
277
  throw new Error(
193
- `zcf \u5F02\u5E38\u9000\u51FA\uFF08code-type=${opts.codeType}, exit=${exitCode}\uFF09:
278
+ `zcf \u5F02\u5E38\u9000\u51FA\uFF08exit=${exitCode}\uFF09:
194
279
  ${detail}
195
280
 
196
281
  \u{1F4A1} \u7528 ${pc.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
@@ -200,7 +285,7 @@ ${detail}
200
285
  if (/CACError|Unknown option/i.test(combined)) {
201
286
  const match = combined.match(/(CACError:.*?)(?:\n|$)/);
202
287
  throw new Error(
203
- `zcf \u62D2\u7EDD\u4E86\u6211\u4EEC\u4F20\u7684\u53C2\u6570\uFF08code-type=${opts.codeType}\uFF09\uFF1A
288
+ `zcf \u62D2\u7EDD\u4E86\u6211\u4EEC\u4F20\u7684\u53C2\u6570\uFF1A
204
289
  ${match?.[1] || "Unknown option"}
205
290
 
206
291
  \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 +333,10 @@ async function main() {
248
333
  }
249
334
  console.log();
250
335
  intro(pc.bgCyan(pc.black(" EasyRouter Config ")) + pc.dim(" v" + VERSION));
336
+ const verbose = !!args.verbose;
337
+ const selectedHost = await pickAvailableHost(verbose);
338
+ const claudeBaseUrl = selectedHost;
339
+ const codexBaseUrl = selectedHost.replace(/\/+$/, "") + "/v1";
251
340
  let apiKey;
252
341
  if (args.apiKey) {
253
342
  apiKey = normalizeApiKey(args.apiKey);
@@ -294,8 +383,8 @@ async function main() {
294
383
  note(
295
384
  [
296
385
  `${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,
386
+ configClaude ? `${pc.bold("Claude URL")} ${PRIMARY_HOST}` : null,
387
+ configCodex ? `${pc.bold("Codex URL")} ${PRIMARY_HOST}/v1` : null,
299
388
  `${pc.bold("API Key")} ${maskKey(apiKey)}`,
300
389
  `${pc.bold("\u5C06\u914D\u7F6E")} ${[
301
390
  configClaude && "Claude Code",
@@ -311,20 +400,18 @@ async function main() {
311
400
  process.exit(0);
312
401
  }
313
402
  }
314
- const verbose = !!args.verbose;
315
403
  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();
404
+ const useSpinner = !verbose && process.stdout.isTTY;
405
+ const s = useSpinner ? spinner() : null;
320
406
  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...");
407
+ 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
408
  try {
323
- await runZcf({
324
- codeType: "cc",
325
- baseUrl: CLAUDE_BASE_URL,
409
+ await configureViaZcf({
410
+ target: "cc",
411
+ baseUrl: claudeBaseUrl,
326
412
  apiKey,
327
413
  verbose,
414
+ // 节流的进度回调:spinner 不会高频闪烁
328
415
  onProgress: (line) => s?.message(`\u914D\u7F6E Claude Code \xB7 ${pc.dim(line)}`)
329
416
  });
330
417
  s?.stop(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.claude/settings.json"));
@@ -336,22 +423,27 @@ async function main() {
336
423
  }
337
424
  }
338
425
  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();
426
+ const codexModel = await resolveCodexModel(
427
+ selectedHost,
428
+ /* interactive */
429
+ !args.apiKey,
430
+ verbose
431
+ );
432
+ const useSpinner = !verbose && process.stdout.isTTY;
433
+ const s = useSpinner ? spinner() : null;
343
434
  s?.start("\u914D\u7F6E Codex");
344
- if (!s && !verbose) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
435
+ if (!s) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
345
436
  try {
346
- await runZcf({
347
- codeType: "cx",
348
- baseUrl: CODEX_BASE_URL,
437
+ await configureViaZcf({
438
+ target: "codex",
439
+ baseUrl: codexBaseUrl,
349
440
  apiKey,
441
+ apiModel: codexModel,
350
442
  verbose,
351
443
  onProgress: (line) => s?.message(`\u914D\u7F6E Codex \xB7 ${pc.dim(line)}`)
352
444
  });
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"));
445
+ s?.stop(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 ") + pc.dim(`\u2192 model=${codexModel}`));
446
+ if (!s) log.success(pc.green(`\u2713 Codex \u914D\u7F6E\u5B8C\u6210 \u2192 model=${codexModel}`));
355
447
  } catch (err) {
356
448
  s?.stop(pc.red("\u2717 Codex \u914D\u7F6E\u5931\u8D25"));
357
449
  console.error(err.message);
@@ -359,10 +451,11 @@ async function main() {
359
451
  }
360
452
  }
361
453
  if (!args.skipVerify) {
362
- const s = process.stdout.isTTY ? spinner() : null;
454
+ const useSpinner = process.stdout.isTTY;
455
+ const s = useSpinner ? spinner() : null;
363
456
  s?.start("\u9A8C\u8BC1\u8FDE\u901A\u6027");
364
457
  if (!s) log.info("\u6B63\u5728\u9A8C\u8BC1\u8FDE\u901A\u6027...");
365
- const result = await verifyConnection(CLAUDE_BASE_URL, apiKey);
458
+ const result = await verifyConnection(claudeBaseUrl, apiKey);
366
459
  if (result.ok) {
367
460
  const msg = pc.green("\u2713 \u94FE\u8DEF\u9A8C\u8BC1\u901A\u8FC7");
368
461
  s?.stop(msg) ?? log.success(msg);
@@ -378,7 +471,7 @@ async function main() {
378
471
  if (configCodex) tips.push(pc.cyan("codex") + pc.dim(" # \u542F\u52A8 Codex"));
379
472
  note(tips.join("\n"), "\u{1F389} \u5168\u90E8\u5B8C\u6210\uFF01\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
380
473
  outro(
381
- pc.dim("\u611F\u8C22\u4F7F\u7528 EasyRouter ") + pc.underline(pc.cyan("https://easyrouter.io")) + pc.dim(" \xB7 powered by ") + pc.underline("zcf")
474
+ 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
475
  );
383
476
  }
384
477
  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.5",
4
4
  "description": "🚀 一键把 EasyRouter 接入 Claude Code & Codex —— 粘贴 Key 即用",
5
5
  "type": "module",
6
6
  "bin": {