easyrouter-config 1.0.4 → 1.0.6

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 +765 -183
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,98 +5,126 @@ import {
5
5
  intro,
6
6
  outro,
7
7
  text,
8
- select,
8
+ multiselect,
9
9
  confirm,
10
10
  spinner,
11
- isCancel,
11
+ isCancel as isCancel2,
12
12
  cancel,
13
- log,
13
+ log as log3,
14
14
  note
15
15
  } from "@clack/prompts";
16
- import { execa } from "execa";
17
- import pc from "picocolors";
16
+ import pc6 from "picocolors";
17
+
18
+ // src/cli/args.ts
18
19
  import { parseArgs } from "util";
19
- var VERSION = true ? "1.0.4" : "0.0.0-dev";
20
+ import pc from "picocolors";
21
+
22
+ // src/config/constants.ts
23
+ var VERSION = true ? "1.0.6" : "0.0.0-dev";
20
24
  var PRIMARY_HOST = "https://easyrouter.io";
21
25
  var FALLBACK_HOSTS = ["https://ezr.sh"];
22
- var PROBE_TIMEOUT_MS = 5e3;
23
- var CODEX_DEFAULT_MODEL = "gpt-5-codex";
26
+ var HOST_PROBE_TIMEOUT_MS = 5e3;
27
+ var FETCH_MODELS_TIMEOUT_MS = 8e3;
28
+ var VERIFY_TIMEOUT_MS = 1e4;
29
+ var SUBPROCESS_IDLE_TIMEOUT_MS = 6e4;
30
+ var SUBPROCESS_HARD_TIMEOUT_MS = 3e5;
31
+ var PROGRESS_THROTTLE_MS = 200;
32
+ var CODEX_FALLBACK_MODEL = "gpt-5.5";
33
+
34
+ // src/clients/types.ts
35
+ var ALL_CLIENT_IDS = ["claude", "codex", "openclaw", "hermes"];
36
+
37
+ // src/cli/args.ts
24
38
  function parseCliArgs() {
25
- try {
26
- const { values } = parseArgs({
27
- args: process.argv.slice(2),
28
- options: {
29
- "api-key": { type: "string", short: "k" },
30
- only: { type: "string" },
31
- "skip-verify": { type: "boolean" },
32
- verbose: { type: "boolean", short: "v" },
33
- help: { type: "boolean", short: "h" },
34
- version: { type: "boolean", short: "V" }
35
- },
36
- allowPositionals: false,
37
- strict: false
38
- });
39
- return {
40
- apiKey: values["api-key"],
41
- only: values.only,
42
- skipVerify: values["skip-verify"],
43
- verbose: values.verbose,
44
- help: values.help,
45
- version: values.version
46
- };
47
- } catch {
48
- return {};
39
+ const { values } = parseArgs({
40
+ args: process.argv.slice(2),
41
+ options: {
42
+ "api-key": { type: "string", short: "k" },
43
+ only: { type: "string" },
44
+ "skip-verify": { type: "boolean" },
45
+ verbose: { type: "boolean", short: "v" },
46
+ help: { type: "boolean", short: "h" },
47
+ version: { type: "boolean", short: "V" }
48
+ },
49
+ allowPositionals: false,
50
+ strict: false
51
+ });
52
+ return {
53
+ apiKey: values["api-key"],
54
+ only: parseOnlyValue(values.only),
55
+ skipVerify: values["skip-verify"],
56
+ verbose: values.verbose,
57
+ help: values.help,
58
+ version: values.version
59
+ };
60
+ }
61
+ function parseOnlyValue(raw) {
62
+ if (!raw) return void 0;
63
+ if (raw === "all") return [...ALL_CLIENT_IDS];
64
+ const tokens = raw.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
65
+ for (const t of tokens) {
66
+ if (!ALL_CLIENT_IDS.includes(t)) {
67
+ throw new Error(
68
+ `--only \u5305\u542B\u672A\u77E5\u5BA2\u6237\u7AEF "${t}"\u3002
69
+ \u53EF\u9009\u503C\uFF1A${ALL_CLIENT_IDS.join(", ")} \u6216 all`
70
+ );
71
+ }
49
72
  }
73
+ return tokens;
50
74
  }
51
75
  function printHelp() {
52
76
  console.log(`
53
77
  ${pc.bold(pc.cyan("easyrouter-config"))} ${pc.dim("v" + VERSION)}
54
- \u4E00\u952E\u628A EasyRouter \u63A5\u5165 Claude Code & Codex\uFF08\u6309\u91CF\u4ED8\u8D39\uFF09
78
+ \u4E00\u952E\u628A EasyRouter \u63A5\u5165 Claude Code / Codex / OpenClaw / Hermes\uFF08\u6309\u91CF\u4ED8\u8D39\uFF09
55
79
 
56
80
  ${pc.bold("\u7528\u6CD5:")}
57
- ${pc.green("npx -y easyrouter-config")} \u4EA4\u4E92\u5F0F\uFF08\u63A8\u8350\uFF09
58
- ${pc.green("npx -y easyrouter-config -k sk-xxx")} \u4E00\u884C\u547D\u4EE4\u914D\u7F6E
59
- ${pc.green("npx -y easyrouter-config -k sk-xxx --only claude")} \u53EA\u914D Claude Code
81
+ ${pc.green("npx -y easyrouter-config")} \u4EA4\u4E92\u5F0F\uFF08\u63A8\u8350 \u2014 \u9009\u4F60\u60F3\u914D\u7684\u5BA2\u6237\u7AEF\uFF09
82
+ ${pc.green("npx -y easyrouter-config -k sk-xxx")} \u975E\u4EA4\u4E92\uFF1A\u9ED8\u8BA4\u914D Claude Code + Codex
83
+ ${pc.green("npx -y easyrouter-config -k sk-xxx --only claude")} \u53EA\u914D Claude Code
84
+ ${pc.green("npx -y easyrouter-config -k sk-xxx --only claude,codex,openclaw")} \u914D\u591A\u4E2A
85
+ ${pc.green("npx -y easyrouter-config -k sk-xxx --only all")} \u914D\u5168\u90E8 4 \u4E2A\u5BA2\u6237\u7AEF
60
86
 
61
87
  ${pc.bold("\u53C2\u6570:")}
62
88
  -k, --api-key <key> EasyRouter API Key\uFF08\u53EF\u7701\u7565 sk- \u524D\u7F00\uFF09
63
- --only <tool> claude | codex \uFF08\u53EA\u914D\u5176\u4E00\uFF09
89
+ --only <list> \u9017\u53F7\u5206\u9694\u7684\u5BA2\u6237\u7AEF\uFF1A${ALL_CLIENT_IDS.join("|")}|all
64
90
  --skip-verify \u8DF3\u8FC7\u8FDE\u901A\u6027\u9A8C\u8BC1
65
- -v, --verbose \u663E\u793A\u5E95\u5C42\u7EC6\u8282\uFF08\u5305\u62EC BaseURL \u63A2\u6D4B\u8FC7\u7A0B\uFF09
91
+ -v, --verbose \u663E\u793A\u5E95\u5C42\u7EC6\u8282\uFF08\u542B BaseURL \u63A2\u6D4B\u3001\u6A21\u578B\u6311\u9009\u7B49\uFF09
66
92
  -h, --help \u663E\u793A\u5E2E\u52A9
67
93
  -V, --version \u663E\u793A\u7248\u672C
68
94
 
69
95
  ${pc.bold("\u5185\u7F6E BaseURL:")}
70
- Claude Code \u2192 ${PRIMARY_HOST}
71
- Codex \u2192 ${PRIMARY_HOST}/v1
96
+ EasyRouter \u2192 ${PRIMARY_HOST}
97
+ \u8DEF\u5F84\u4E0E\u8BA4\u8BC1\u7531\u5404\u5BA2\u6237\u7AEF\u81EA\u884C\u5904\u7406\uFF08OpenAI \u517C\u5BB9\u7528 /v1\uFF0CClaude \u76F4\u8FDE\u6839\u57DF\u540D\uFF09
72
98
 
73
99
  ${pc.bold("\u539F\u7406:")}
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
78
- \u2022 Claude Code \u2192 ~/.claude/settings.json
79
- \u2022 Codex \u2192 ~/.codex/config.toml + ~/.codex/auth.json
100
+ Claude Code & Codex \u2192 \u8C03\u7528 ${pc.underline("zcf")} (UfoMiao/zcf) \u5B50\u8FDB\u7A0B
101
+ OpenClaw \u2192 \u8C03\u7528\u672C\u5730 ${pc.underline("openclaw onboard")} \u5B50\u547D\u4EE4\uFF08\u9700\u5148\u88C5 openclaw\uFF09
102
+ Hermes \u2192 \u8C03\u7528\u672C\u5730 ${pc.underline("hermes config set")} \u5B50\u547D\u4EE4\uFF08\u9700\u5148\u88C5 hermes\uFF09
80
103
  `);
81
104
  }
105
+
106
+ // src/config/host.ts
107
+ import { log } from "@clack/prompts";
108
+ import pc2 from "picocolors";
82
109
  async function pickAvailableHost(verbose) {
83
110
  const candidates = [PRIMARY_HOST, ...FALLBACK_HOSTS];
84
111
  for (const host of candidates) {
85
112
  const ok = await probeHost(host);
86
113
  if (verbose) {
87
- const tag = ok ? pc.green("\u2713") : pc.red("\u2717");
114
+ const tag = ok ? pc2.green("\u2713") : pc2.red("\u2717");
88
115
  log.info(`${tag} \u63A2\u6D4B ${host} \u2192 ${ok ? "\u53EF\u8FBE" : "\u4E0D\u53EF\u8FBE"}`);
89
116
  }
90
117
  if (ok) return host;
91
118
  }
92
- if (verbose) log.warn(`\u6240\u6709\u5019\u9009\u57DF\u540D\u5747\u4E0D\u53EF\u8FBE\uFF0C\u4F7F\u7528\u4E3B\u57DF\u540D ${PRIMARY_HOST}`);
119
+ if (verbose)
120
+ log.warn(`\u6240\u6709\u5019\u9009\u57DF\u540D\u5747\u4E0D\u53EF\u8FBE\uFF0C\u4F7F\u7528\u4E3B\u57DF\u540D ${PRIMARY_HOST}`);
93
121
  return PRIMARY_HOST;
94
122
  }
95
123
  async function probeHost(host) {
96
124
  const url = host.replace(/\/+$/, "") + "/v1/models";
97
125
  try {
98
126
  const ctrl = new AbortController();
99
- const timer = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT_MS);
127
+ const timer = setTimeout(() => ctrl.abort(), HOST_PROBE_TIMEOUT_MS);
100
128
  const res = await fetch(url, { method: "HEAD", signal: ctrl.signal });
101
129
  clearTimeout(timer);
102
130
  return res.status > 0;
@@ -104,9 +132,75 @@ async function probeHost(host) {
104
132
  return false;
105
133
  }
106
134
  }
107
- var IDLE_TIMEOUT_MS = 6e4;
108
- var HARD_TIMEOUT_MS = 3e5;
109
- async function configureViaZcf(opts) {
135
+
136
+ // src/config/apiKey.ts
137
+ function normalizeApiKey(raw) {
138
+ const trimmed = raw.trim();
139
+ if (!trimmed) return trimmed;
140
+ if (trimmed.startsWith("sk-")) return trimmed;
141
+ return "sk-" + trimmed;
142
+ }
143
+ function maskKey(key) {
144
+ if (key.length <= 8) return "*".repeat(key.length);
145
+ return key.slice(0, 6) + "*".repeat(Math.max(4, key.length - 10)) + key.slice(-4);
146
+ }
147
+
148
+ // src/utils/verify.ts
149
+ async function verifyConnection(baseUrl, apiKey) {
150
+ const probeUrl = baseUrl.replace(/\/+$/, "") + "/v1/models";
151
+ try {
152
+ const ctrl = new AbortController();
153
+ const timer = setTimeout(() => ctrl.abort(), VERIFY_TIMEOUT_MS);
154
+ const res = await fetch(probeUrl, {
155
+ method: "GET",
156
+ headers: {
157
+ Authorization: `Bearer ${apiKey}`,
158
+ "anthropic-version": "2023-06-01"
159
+ },
160
+ signal: ctrl.signal
161
+ });
162
+ clearTimeout(timer);
163
+ return { ok: res.ok, status: res.status };
164
+ } catch (err) {
165
+ return { ok: false, error: err?.message || String(err) };
166
+ }
167
+ }
168
+
169
+ // src/clients/detect.ts
170
+ import { execa } from "execa";
171
+ async function detectCommand(opts) {
172
+ const args = opts.args ?? ["--version"];
173
+ const timeout = opts.timeoutMs ?? 5e3;
174
+ try {
175
+ const result = await execa(opts.command, args, {
176
+ reject: false,
177
+ timeout,
178
+ windowsHide: true,
179
+ stdio: ["ignore", "pipe", "pipe"]
180
+ });
181
+ if (result?.errno === "ENOENT" || result?.code === "ENOENT") {
182
+ return { installed: false };
183
+ }
184
+ if (result?.failed && /\bENOENT\b|not found|command not found/i.test(
185
+ String(result?.shortMessage || result?.stderr || "")
186
+ )) {
187
+ return { installed: false };
188
+ }
189
+ const out = ((result?.stdout || "") + "\n" + (result?.stderr || "")).split(/\r?\n/)[0]?.trim();
190
+ const v = out?.match(/v?(\d+\.\d+(?:\.\d+)?(?:[\-+][\w.]+)?)/)?.[1];
191
+ return { installed: true, version: v };
192
+ } catch (err) {
193
+ if (err?.code === "ENOENT" || /ENOENT/.test(String(err?.message))) {
194
+ return { installed: false };
195
+ }
196
+ return { installed: false };
197
+ }
198
+ }
199
+
200
+ // src/clients/zcf-runner.ts
201
+ import { execa as execa2 } from "execa";
202
+ import pc3 from "picocolors";
203
+ async function runZcf(opts) {
110
204
  const args = [
111
205
  "-y",
112
206
  "zcf",
@@ -115,7 +209,7 @@ async function configureViaZcf(opts) {
115
209
  // --skip-prompt 非交互模式
116
210
  "-T",
117
211
  opts.target,
118
- // 'cc' | 'codex'(用文档推荐的简写 -T)
212
+ // 'cc' | 'codex'
119
213
  // EasyRouter 是 OpenAI 兼容网关,统一用 api_key(Bearer Authorization)
120
214
  "--api-type",
121
215
  "api_key",
@@ -125,10 +219,8 @@ async function configureViaZcf(opts) {
125
219
  opts.baseUrl,
126
220
  "-r",
127
221
  "backup",
128
- // 已有配置自动备份
129
222
  "--mcp-services",
130
223
  "skip",
131
- // 不装 MCP(即便 zcf 偶尔忽略也无伤)
132
224
  "--workflows",
133
225
  "skip",
134
226
  "--output-styles",
@@ -137,7 +229,7 @@ async function configureViaZcf(opts) {
137
229
  "false"
138
230
  ];
139
231
  if (opts.target === "codex") {
140
- args.push("--api-model", CODEX_DEFAULT_MODEL);
232
+ args.push("--api-model", opts.apiModel || CODEX_FALLBACK_MODEL);
141
233
  }
142
234
  const isWin = process.platform === "win32";
143
235
  const cmd = isWin ? "npx.cmd" : "npx";
@@ -146,8 +238,7 @@ async function configureViaZcf(opts) {
146
238
  let lastDataAt = Date.now();
147
239
  let killedReason = null;
148
240
  let lastProgressAt = 0;
149
- const PROGRESS_THROTTLE_MS = 200;
150
- const child = execa(cmd, args, {
241
+ const child = execa2(cmd, args, {
151
242
  stdio: ["ignore", "pipe", "pipe"],
152
243
  env: {
153
244
  ...process.env,
@@ -179,8 +270,10 @@ async function configureViaZcf(opts) {
179
270
  child.stdout?.on("data", (c) => handleChunk(c, "out"));
180
271
  child.stderr?.on("data", (c) => handleChunk(c, "err"));
181
272
  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`;
273
+ if (Date.now() - lastDataAt > SUBPROCESS_IDLE_TIMEOUT_MS) {
274
+ killedReason = `\u5B50\u8FDB\u7A0B\u5DF2 ${Math.floor(
275
+ SUBPROCESS_IDLE_TIMEOUT_MS / 1e3
276
+ )} \u79D2\u65E0\u8F93\u51FA\uFF08\u7591\u4F3C\u5361\u5728\u4EA4\u4E92\u5F0F\u63D0\u95EE\u6216\u7F51\u7EDC\u95EE\u9898\uFF09`;
184
277
  child.kill("SIGTERM");
185
278
  setTimeout(() => {
186
279
  try {
@@ -191,7 +284,9 @@ async function configureViaZcf(opts) {
191
284
  }
192
285
  }, 5e3);
193
286
  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`;
287
+ killedReason = `\u5B50\u8FDB\u7A0B\u8D85\u8FC7 ${Math.floor(
288
+ SUBPROCESS_HARD_TIMEOUT_MS / 1e3
289
+ )} \u79D2\u672A\u7ED3\u675F\uFF08\u5F3A\u5236\u8D85\u65F6\uFF09`;
195
290
  child.kill("SIGTERM");
196
291
  setTimeout(() => {
197
292
  try {
@@ -199,7 +294,7 @@ async function configureViaZcf(opts) {
199
294
  } catch {
200
295
  }
201
296
  }, 5e3);
202
- }, HARD_TIMEOUT_MS);
297
+ }, SUBPROCESS_HARD_TIMEOUT_MS);
203
298
  const result = await child.catch((e) => e);
204
299
  clearInterval(idleTimer);
205
300
  clearTimeout(hardTimer);
@@ -214,7 +309,7 @@ async function configureViaZcf(opts) {
214
309
  \u2022 npm \u955C\u50CF/\u7F51\u7EDC\u6162\uFF0Cnpx \u4E0B\u8F7D zcf \u5305\u5361\u4F4F
215
310
  \u2022 \u9632\u706B\u5899\u62E6\u622A\u4E86 registry.npmjs.org
216
311
 
217
- \u5EFA\u8BAE\uFF1A\u7528 ${pc.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u67E5\u770B\u8BE6\u7EC6\u65E5\u5FD7`
312
+ \u5EFA\u8BAE\uFF1A\u7528 ${pc3.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u67E5\u770B\u8BE6\u7EC6\u65E5\u5FD7`
218
313
  );
219
314
  }
220
315
  if (exitCode !== 0 || signal) {
@@ -223,7 +318,7 @@ async function configureViaZcf(opts) {
223
318
  `zcf \u5F02\u5E38\u9000\u51FA\uFF08exit=${exitCode}\uFF09:
224
319
  ${detail}
225
320
 
226
- \u{1F4A1} \u7528 ${pc.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
321
+ \u{1F4A1} \u7528 ${pc3.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
227
322
  );
228
323
  }
229
324
  const combined = capturedOut + "\n" + capturedErr;
@@ -237,35 +332,500 @@ ${detail}
237
332
  );
238
333
  }
239
334
  }
240
- async function verifyConnection(baseUrl, apiKey) {
241
- const probeUrl = baseUrl.replace(/\/+$/, "") + "/v1/models";
335
+
336
+ // src/clients/claude.ts
337
+ var claudeClient = {
338
+ id: "claude",
339
+ label: "Claude Code",
340
+ hint: "~/.claude/settings.json",
341
+ launchCommand: "claude",
342
+ async detect() {
343
+ const r = await detectCommand({ command: "claude" });
344
+ return {
345
+ installed: r.installed,
346
+ version: r.version,
347
+ installHint: "\u524D\u5F80 https://claude.com/claude-code \u5B89\u88C5\uFF0C\u6216\u8FD0\u884C\uFF1A\n npm install -g @anthropic-ai/claude-code"
348
+ };
349
+ },
350
+ async configure(ctx) {
351
+ await runZcf({
352
+ target: "cc",
353
+ baseUrl: ctx.host,
354
+ apiKey: ctx.apiKey,
355
+ verbose: ctx.verbose,
356
+ onProgress: ctx.onProgress
357
+ });
358
+ return { configPaths: ["~/.claude/settings.json"] };
359
+ }
360
+ };
361
+
362
+ // src/config/models.ts
363
+ import { log as log2, select, isCancel } from "@clack/prompts";
364
+ import pc4 from "picocolors";
365
+ async function fetchEasyRouterModels(host, verbose) {
366
+ const url = host.replace(/\/+$/, "") + "/api/pricing";
242
367
  try {
243
368
  const ctrl = new AbortController();
244
- const timer = setTimeout(() => ctrl.abort(), 1e4);
245
- const res = await fetch(probeUrl, {
246
- method: "GET",
247
- headers: {
248
- Authorization: `Bearer ${apiKey}`,
249
- "anthropic-version": "2023-06-01"
250
- },
251
- signal: ctrl.signal
252
- });
369
+ const timer = setTimeout(() => ctrl.abort(), FETCH_MODELS_TIMEOUT_MS);
370
+ const res = await fetch(url, { signal: ctrl.signal });
253
371
  clearTimeout(timer);
254
- return { ok: res.ok, status: res.status };
372
+ if (!res.ok) {
373
+ if (verbose)
374
+ log2.warn(`\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1AHTTP ${res.status}\uFF08\u5C06\u4F7F\u7528\u515C\u5E95\u6A21\u578B\uFF09`);
375
+ return [];
376
+ }
377
+ const json = await res.json();
378
+ const models = Array.isArray(json?.data) ? json.data : [];
379
+ if (verbose) log2.info(`\u2713 \u62C9\u53D6\u5230 ${models.length} \u4E2A EasyRouter \u6A21\u578B`);
380
+ return models;
255
381
  } catch (err) {
256
- return { ok: false, error: err?.message || String(err) };
382
+ if (verbose)
383
+ log2.warn(`\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5F02\u5E38\uFF1A${err?.message || err}\uFF08\u5C06\u4F7F\u7528\u515C\u5E95\u6A21\u578B\uFF09`);
384
+ return [];
257
385
  }
258
386
  }
259
- function normalizeApiKey(raw) {
260
- const trimmed = raw.trim();
261
- if (!trimmed) return trimmed;
262
- if (trimmed.startsWith("sk-")) return trimmed;
263
- return "sk-" + trimmed;
387
+ function filterOpenAICompatibleLLMs(models) {
388
+ return models.filter((m) => {
389
+ const isLlm = (m.tags || "").toUpperCase().includes("LLM");
390
+ const supportsOpenAI = !m.supported_endpoint_types || m.supported_endpoint_types.length === 0 || m.supported_endpoint_types.includes("openai");
391
+ return isLlm && supportsOpenAI;
392
+ });
264
393
  }
265
- function maskKey(key) {
266
- if (key.length <= 8) return "*".repeat(key.length);
267
- return key.slice(0, 6) + "*".repeat(Math.max(4, key.length - 10)) + key.slice(-4);
394
+ function pickBestCodexModel(models) {
395
+ const isLlm = (m) => (m.tags || "").toUpperCase().includes("LLM");
396
+ const supportsOpenAI = (m) => !m.supported_endpoint_types || m.supported_endpoint_types.includes("openai");
397
+ const isGpt5 = (m) => /^gpt-5(\b|[.\-])/i.test(m.model_name);
398
+ const isSmallTier = (m) => /(^|[\-_.])(mini|nano|small|lite|tiny)([\-_.]|$)/i.test(m.model_name);
399
+ const extractVersion = (name) => {
400
+ const m = name.match(/^gpt-(\d+(?:\.\d+)*)/i);
401
+ return m ? m[1] : "0";
402
+ };
403
+ const compareVersion = (a, b) => {
404
+ const pa = a.split(".").map(Number);
405
+ const pb = b.split(".").map(Number);
406
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
407
+ const x = pa[i] || 0;
408
+ const y = pb[i] || 0;
409
+ if (x !== y) return x - y;
410
+ }
411
+ return 0;
412
+ };
413
+ const pickFromPool = (pool) => {
414
+ if (!pool.length) return null;
415
+ const byVersion = /* @__PURE__ */ new Map();
416
+ for (const m of pool) {
417
+ const v = extractVersion(m.model_name);
418
+ if (!byVersion.has(v)) byVersion.set(v, []);
419
+ byVersion.get(v).push(m);
420
+ }
421
+ const maxVersion = Array.from(byVersion.keys()).sort(compareVersion).pop();
422
+ const sameVersion = byVersion.get(maxVersion);
423
+ const withCodex = sameVersion.filter((m) => /codex/i.test(m.model_name));
424
+ if (withCodex.length) {
425
+ return withCodex.sort(
426
+ (a, b) => a.model_name.localeCompare(b.model_name)
427
+ )[0].model_name;
428
+ }
429
+ return sameVersion.sort(
430
+ (a, b) => a.model_name.localeCompare(b.model_name)
431
+ )[0].model_name;
432
+ };
433
+ const tier1 = pickFromPool(
434
+ models.filter(
435
+ (m) => isGpt5(m) && isLlm(m) && supportsOpenAI(m) && !isSmallTier(m)
436
+ )
437
+ );
438
+ if (tier1) return tier1;
439
+ const tier2 = pickFromPool(
440
+ models.filter((m) => isGpt5(m) && isLlm(m) && supportsOpenAI(m))
441
+ );
442
+ if (tier2) return tier2;
443
+ const tier3 = pickFromPool(
444
+ models.filter((m) => isLlm(m) && supportsOpenAI(m) && !isSmallTier(m))
445
+ );
446
+ if (tier3) return tier3;
447
+ return null;
448
+ }
449
+ async function resolveCodexModel(host, interactive, verbose) {
450
+ const models = await fetchEasyRouterModels(host, verbose);
451
+ const auto = pickBestCodexModel(models);
452
+ if (auto) {
453
+ if (verbose) log2.info(`\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF1A${pc4.cyan(auto)}`);
454
+ return auto;
455
+ }
456
+ if (interactive && models.length) {
457
+ 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);
458
+ if (candidates.length) {
459
+ const picked = await select({
460
+ message: "\u672A\u81EA\u52A8\u8BC6\u522B Codex \u63A8\u8350\u6A21\u578B\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9",
461
+ options: candidates.map((m) => ({
462
+ value: m.model_name,
463
+ label: m.model_name
464
+ }))
465
+ });
466
+ if (!isCancel(picked)) return picked;
467
+ }
468
+ }
469
+ if (verbose)
470
+ log2.warn(`\u65E0\u6CD5\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF0C\u4F7F\u7528\u515C\u5E95\uFF1A${CODEX_FALLBACK_MODEL}`);
471
+ return CODEX_FALLBACK_MODEL;
268
472
  }
473
+
474
+ // src/clients/codex.ts
475
+ var codexClient = {
476
+ id: "codex",
477
+ label: "Codex",
478
+ hint: "~/.codex/config.toml + auth.json",
479
+ launchCommand: "codex",
480
+ async detect() {
481
+ const r = await detectCommand({ command: "codex" });
482
+ return {
483
+ installed: r.installed,
484
+ version: r.version,
485
+ installHint: "\u524D\u5F80 https://github.com/openai/codex \u5B89\u88C5\uFF0C\u6216\u8FD0\u884C\uFF1A\n npm install -g @openai/codex\n # \u6216 macOS: brew install --cask codex"
486
+ };
487
+ },
488
+ async configure(ctx) {
489
+ const interactive = typeof process !== "undefined" && !!process.stdout.isTTY;
490
+ const model = await resolveCodexModel(ctx.host, interactive, ctx.verbose);
491
+ const codexBaseUrl = ctx.host.replace(/\/+$/, "") + "/v1";
492
+ await runZcf({
493
+ target: "codex",
494
+ baseUrl: codexBaseUrl,
495
+ apiKey: ctx.apiKey,
496
+ apiModel: model,
497
+ verbose: ctx.verbose,
498
+ onProgress: ctx.onProgress
499
+ });
500
+ return {
501
+ configPaths: ["~/.codex/config.toml", "~/.codex/auth.json"],
502
+ model
503
+ };
504
+ }
505
+ };
506
+
507
+ // src/clients/openclaw.ts
508
+ import { execa as execa3 } from "execa";
509
+ import { promises as fs } from "fs";
510
+ import { homedir } from "os";
511
+ import { join, dirname } from "path";
512
+ import pc5 from "picocolors";
513
+ var OPENCLAW_PROVIDER_ID = "custom-easyrouter-io";
514
+ var openClawClient = {
515
+ id: "openclaw",
516
+ label: "OpenClaw",
517
+ hint: "~/.openclaw/openclaw.json",
518
+ launchCommand: "openclaw",
519
+ async detect() {
520
+ const r = await detectCommand({ command: "openclaw" });
521
+ return {
522
+ installed: r.installed,
523
+ version: r.version,
524
+ installHint: "\u524D\u5F80 https://openclaw.ai \u5B89\u88C5\uFF0C\u6216\u8FD0\u884C\uFF1A\n curl -fsSL https://openclaw.ai/install.sh | bash\n # \u6216 npm install -g openclaw\n # \u6216 macOS: brew install openclaw-cli"
525
+ };
526
+ },
527
+ async configure(ctx) {
528
+ const interactive = typeof process !== "undefined" && !!process.stdout.isTTY;
529
+ const allModels = await fetchEasyRouterModels(ctx.host, ctx.verbose);
530
+ const llms = filterOpenAICompatibleLLMs(allModels);
531
+ const defaultModel = await resolveCodexModel(
532
+ ctx.host,
533
+ interactive,
534
+ ctx.verbose
535
+ );
536
+ const baseUrl = ctx.host.replace(/\/+$/, "") + "/v1";
537
+ const args = [
538
+ "onboard",
539
+ "--non-interactive",
540
+ "--auth-choice",
541
+ "custom-api-key",
542
+ "--custom-base-url",
543
+ baseUrl,
544
+ "--custom-model-id",
545
+ defaultModel,
546
+ "--custom-api-key",
547
+ ctx.apiKey,
548
+ "--custom-compatibility",
549
+ "openai",
550
+ "--secret-input-mode",
551
+ "plaintext",
552
+ "--accept-risk",
553
+ // 自定义 provider 必填
554
+ "--skip-health"
555
+ // 不要求 gateway daemon 启动
556
+ ];
557
+ await runOpenClawSubprocess(args, ctx);
558
+ let registered = 0;
559
+ if (llms.length > 1) {
560
+ registered = await patchOpenClawJson(llms, defaultModel, ctx);
561
+ }
562
+ return {
563
+ configPaths: ["~/.openclaw/openclaw.json"],
564
+ model: registered > 1 ? `${defaultModel} (+${registered - 1} more)` : defaultModel
565
+ };
566
+ }
567
+ };
568
+ async function patchOpenClawJson(llms, defaultModel, ctx) {
569
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
570
+ let json;
571
+ try {
572
+ const raw = await fs.readFile(configPath, "utf8");
573
+ json = JSON.parse(raw);
574
+ } catch (err) {
575
+ if (ctx.verbose) {
576
+ console.warn(
577
+ `[openclaw] \u8DF3\u8FC7 patch\uFF1A\u8BFB ${configPath} \u5931\u8D25\uFF08${err?.code || err?.message}\uFF09`
578
+ );
579
+ }
580
+ return 1;
581
+ }
582
+ const providers = json?.models?.providers;
583
+ if (!providers || typeof providers !== "object") {
584
+ if (ctx.verbose) {
585
+ console.warn(`[openclaw] \u8DF3\u8FC7 patch\uFF1Amodels.providers \u6BB5\u4E0D\u5B58\u5728`);
586
+ }
587
+ return 1;
588
+ }
589
+ const provider = providers[OPENCLAW_PROVIDER_ID];
590
+ if (!provider || !Array.isArray(provider.models)) {
591
+ if (ctx.verbose) {
592
+ console.warn(
593
+ `[openclaw] \u8DF3\u8FC7 patch\uFF1Amodels.providers.${OPENCLAW_PROVIDER_ID}.models \u4E0D\u662F\u6570\u7EC4`
594
+ );
595
+ }
596
+ return 1;
597
+ }
598
+ const existingIds = new Set(
599
+ provider.models.map((m) => m.id)
600
+ );
601
+ const before = provider.models.length;
602
+ for (const m of llms) {
603
+ if (existingIds.has(m.model_name)) continue;
604
+ provider.models.push({
605
+ id: m.model_name,
606
+ name: m.model_name + " (EasyRouter)",
607
+ contextWindow: 16e3,
608
+ // OpenClaw 默认值;用户可自行编辑
609
+ maxTokens: 4096,
610
+ input: ["text"],
611
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
612
+ reasoning: false
613
+ });
614
+ }
615
+ if (provider.models.length === before) {
616
+ if (ctx.verbose) {
617
+ console.warn(
618
+ `[openclaw] \u5DF2\u6CE8\u518C\u8FC7\u6240\u6709 ${llms.length} \u4E2A\u6A21\u578B\uFF0C\u8DF3\u8FC7 patch`
619
+ );
620
+ }
621
+ return provider.models.length;
622
+ }
623
+ await fs.copyFile(configPath, configPath + ".bak." + Date.now());
624
+ await atomicWriteJson(configPath, json);
625
+ if (ctx.verbose) {
626
+ console.warn(
627
+ `[openclaw] \u2713 \u6CE8\u518C ${provider.models.length - before} \u4E2A\u65B0\u6A21\u578B\uFF0C\u5171 ${provider.models.length} \u4E2A\uFF08\u9ED8\u8BA4\uFF1A${defaultModel}\uFF09`
628
+ );
629
+ }
630
+ return provider.models.length;
631
+ }
632
+ async function atomicWriteJson(path, obj) {
633
+ const tmp = path + ".tmp." + process.pid;
634
+ await fs.mkdir(dirname(path), { recursive: true });
635
+ await fs.writeFile(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
636
+ await fs.rename(tmp, path);
637
+ }
638
+ async function runOpenClawSubprocess(args, ctx) {
639
+ let capturedOut = "";
640
+ let capturedErr = "";
641
+ let lastDataAt = Date.now();
642
+ let killedReason = null;
643
+ let lastProgressAt = 0;
644
+ const child = execa3("openclaw", args, {
645
+ stdio: ["ignore", "pipe", "pipe"],
646
+ env: { ...process.env, FORCE_COLOR: "0" },
647
+ reject: false,
648
+ windowsHide: true
649
+ });
650
+ const handleChunk = (chunk, target) => {
651
+ const text2 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
652
+ if (target === "out") capturedOut += text2;
653
+ else capturedErr += text2;
654
+ lastDataAt = Date.now();
655
+ if (ctx.onProgress) {
656
+ const now = Date.now();
657
+ if (now - lastProgressAt >= PROGRESS_THROTTLE_MS) {
658
+ lastProgressAt = now;
659
+ const lines = text2.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
660
+ const last = lines[lines.length - 1];
661
+ if (last) ctx.onProgress(last.slice(0, 60));
662
+ }
663
+ }
664
+ if (ctx.verbose) process.stderr.write(text2);
665
+ };
666
+ child.stdout?.on("data", (c) => handleChunk(c, "out"));
667
+ child.stderr?.on("data", (c) => handleChunk(c, "err"));
668
+ const idleTimer = setInterval(() => {
669
+ if (Date.now() - lastDataAt > SUBPROCESS_IDLE_TIMEOUT_MS) {
670
+ killedReason = `openclaw \u5DF2 ${Math.floor(
671
+ SUBPROCESS_IDLE_TIMEOUT_MS / 1e3
672
+ )} \u79D2\u65E0\u8F93\u51FA\uFF08\u7591\u4F3C\u5361\u4F4F\uFF09`;
673
+ child.kill("SIGTERM");
674
+ setTimeout(() => {
675
+ try {
676
+ child.kill("SIGKILL");
677
+ } catch {
678
+ }
679
+ }, 5e3);
680
+ }
681
+ }, 5e3);
682
+ const hardTimer = setTimeout(() => {
683
+ killedReason = `openclaw \u8D85\u8FC7 ${Math.floor(
684
+ SUBPROCESS_HARD_TIMEOUT_MS / 1e3
685
+ )} \u79D2\u672A\u7ED3\u675F\uFF08\u5F3A\u5236\u8D85\u65F6\uFF09`;
686
+ child.kill("SIGTERM");
687
+ setTimeout(() => {
688
+ try {
689
+ child.kill("SIGKILL");
690
+ } catch {
691
+ }
692
+ }, 5e3);
693
+ }, SUBPROCESS_HARD_TIMEOUT_MS);
694
+ const result = await child.catch((e) => e);
695
+ clearInterval(idleTimer);
696
+ clearTimeout(hardTimer);
697
+ const exitCode = result?.exitCode ?? null;
698
+ if (killedReason) {
699
+ throw new Error(
700
+ `openclaw \u8C03\u7528\u5931\u8D25\uFF1A${killedReason}
701
+
702
+ \u5EFA\u8BAE\uFF1A\u7528 ${pc5.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u67E5\u770B\u8BE6\u7EC6\u65E5\u5FD7`
703
+ );
704
+ }
705
+ if (exitCode !== 0) {
706
+ const detail = (capturedErr || capturedOut || "").slice(-2e3) || `exit ${exitCode}`;
707
+ throw new Error(
708
+ `openclaw \u5F02\u5E38\u9000\u51FA\uFF08exit=${exitCode}\uFF09:
709
+ ${detail}
710
+
711
+ \u{1F4A1} \u7528 ${pc5.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
712
+ );
713
+ }
714
+ }
715
+
716
+ // src/clients/hermes.ts
717
+ import { execa as execa4 } from "execa";
718
+ import { promises as fs2 } from "fs";
719
+ import { homedir as homedir2 } from "os";
720
+ import { join as join2 } from "path";
721
+ var HERMES_PROVIDER_NAME = "easyrouter";
722
+ var HERMES_KEY_ENV = "EASYROUTER_API_KEY";
723
+ var hermesClient = {
724
+ id: "hermes",
725
+ label: "Hermes",
726
+ hint: "~/.hermes/config.yaml + .env",
727
+ launchCommand: "hermes",
728
+ async detect() {
729
+ const r = await detectCommand({ command: "hermes" });
730
+ return {
731
+ installed: r.installed,
732
+ version: r.version,
733
+ installHint: "\u524D\u5F80 https://hermes.nousresearch.com \u5B89\u88C5\uFF0C\u6216\u8FD0\u884C\uFF1A\n curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash"
734
+ };
735
+ },
736
+ async configure(ctx) {
737
+ const interactive = typeof process !== "undefined" && !!process.stdout.isTTY;
738
+ const defaultModel = await resolveCodexModel(
739
+ ctx.host,
740
+ interactive,
741
+ ctx.verbose
742
+ );
743
+ const baseUrl = ctx.host.replace(/\/+$/, "") + "/v1";
744
+ await runHermesConfigSet(
745
+ `providers.${HERMES_PROVIDER_NAME}.base_url`,
746
+ baseUrl,
747
+ ctx
748
+ );
749
+ if (ctx.onProgress) ctx.onProgress(`provider \u6CE8\u518C`);
750
+ await runHermesConfigSet(
751
+ `providers.${HERMES_PROVIDER_NAME}.key_env`,
752
+ HERMES_KEY_ENV,
753
+ ctx
754
+ );
755
+ await runHermesConfigSet(
756
+ `providers.${HERMES_PROVIDER_NAME}.kind`,
757
+ "openai",
758
+ ctx
759
+ );
760
+ const fullDefaultId = `${HERMES_PROVIDER_NAME}/${defaultModel}`;
761
+ await runHermesConfigSet("model.default", fullDefaultId, ctx);
762
+ await runHermesConfigSet("model.provider", HERMES_PROVIDER_NAME, ctx);
763
+ await runHermesConfigSet("model.base_url", baseUrl, ctx);
764
+ if (ctx.onProgress) ctx.onProgress(`\u9ED8\u8BA4 model = ${defaultModel}`);
765
+ const envPath = join2(homedir2(), ".hermes", ".env");
766
+ await fs2.mkdir(join2(homedir2(), ".hermes"), { recursive: true });
767
+ let envContent = "";
768
+ try {
769
+ envContent = await fs2.readFile(envPath, "utf8");
770
+ } catch (err) {
771
+ if (err.code !== "ENOENT") throw err;
772
+ }
773
+ envContent = upsertEnvVar(envContent, HERMES_KEY_ENV, ctx.apiKey);
774
+ await fs2.writeFile(envPath, envContent, { encoding: "utf8", mode: 384 });
775
+ if (ctx.onProgress) ctx.onProgress("\u5199\u5165 ~/.hermes/.env");
776
+ return {
777
+ configPaths: ["~/.hermes/config.yaml", "~/.hermes/.env"],
778
+ model: defaultModel
779
+ };
780
+ }
781
+ };
782
+ async function runHermesConfigSet(key, value, ctx) {
783
+ const result = await execa4("hermes", ["config", "set", key, value], {
784
+ reject: false,
785
+ timeout: 3e4,
786
+ windowsHide: true,
787
+ env: { ...process.env, FORCE_COLOR: "0" },
788
+ stdio: ["ignore", "pipe", "pipe"]
789
+ });
790
+ if (ctx.verbose && (result.stdout || result.stderr)) {
791
+ process.stderr.write(
792
+ `[hermes config set ${key}]
793
+ ${result.stdout}
794
+ ${result.stderr}
795
+ `
796
+ );
797
+ }
798
+ if (result.exitCode !== 0) {
799
+ const detail = (result.stderr || result.stdout || "").slice(-1e3);
800
+ throw new Error(
801
+ `hermes config set ${key} \u5931\u8D25\uFF08exit=${result.exitCode}\uFF09:
802
+ ${detail}`
803
+ );
804
+ }
805
+ }
806
+ function upsertEnvVar(content, key, value) {
807
+ const escaped = value.replace(/(["\\])/g, "\\$1");
808
+ const line = `${key}="${escaped}"`;
809
+ const re = new RegExp(`^${key}=.*$`, "m");
810
+ if (re.test(content)) {
811
+ return content.replace(re, line);
812
+ }
813
+ const trailing = content.length === 0 || content.endsWith("\n") ? "" : "\n";
814
+ return content + trailing + line + "\n";
815
+ }
816
+
817
+ // src/clients/registry.ts
818
+ var CLIENT_REGISTRY = {
819
+ claude: claudeClient,
820
+ codex: codexClient,
821
+ openclaw: openClawClient,
822
+ hermes: hermesClient
823
+ };
824
+ function listClients() {
825
+ return Object.values(CLIENT_REGISTRY);
826
+ }
827
+
828
+ // src/index.ts
269
829
  async function main() {
270
830
  const args = parseCliArgs();
271
831
  if (args.help) {
@@ -277,15 +837,16 @@ async function main() {
277
837
  return;
278
838
  }
279
839
  console.log();
280
- intro(pc.bgCyan(pc.black(" EasyRouter Config ")) + pc.dim(" v" + VERSION));
840
+ intro(
841
+ pc6.bgCyan(pc6.black(" EasyRouter Config ")) + pc6.dim(" v" + VERSION)
842
+ );
281
843
  const verbose = !!args.verbose;
282
- const selectedHost = await pickAvailableHost(verbose);
283
- const claudeBaseUrl = selectedHost;
284
- const codexBaseUrl = selectedHost.replace(/\/+$/, "") + "/v1";
844
+ const interactive = !args.apiKey;
845
+ const host = await pickAvailableHost(verbose);
285
846
  let apiKey;
286
847
  if (args.apiKey) {
287
848
  apiKey = normalizeApiKey(args.apiKey);
288
- log.info(`API Key: ${pc.dim(maskKey(apiKey))}\uFF08\u6765\u81EA\u547D\u4EE4\u884C\u53C2\u6570\uFF09`);
849
+ log3.info(`API Key: ${pc6.dim(maskKey(apiKey))}\uFF08\u6765\u81EA\u547D\u4EE4\u884C\u53C2\u6570\uFF09`);
289
850
  } else {
290
851
  const input = await text({
291
852
  message: "\u8BF7\u8F93\u5165\u4F60\u7684 EasyRouter API Key",
@@ -296,127 +857,148 @@ async function main() {
296
857
  return void 0;
297
858
  }
298
859
  });
299
- if (isCancel(input)) {
860
+ if (isCancel2(input)) {
300
861
  cancel("\u5DF2\u53D6\u6D88");
301
862
  process.exit(0);
302
863
  }
303
864
  apiKey = normalizeApiKey(input);
304
865
  }
305
- let configClaude = true;
306
- let configCodex = true;
307
- if (args.only === "claude") {
308
- configCodex = false;
309
- } else if (args.only === "codex") {
310
- configClaude = false;
311
- } else if (!args.apiKey) {
312
- const target = await select({
313
- message: "\u8BF7\u9009\u62E9\u8981\u914D\u7F6E\u7684\u5BA2\u6237\u7AEF",
314
- options: [
315
- { value: "both", label: "Claude Code + Codex", hint: "\u4E24\u4E2A\u90FD\u914D\uFF08\u63A8\u8350\uFF09" },
316
- { value: "claude", label: "\u4EC5 Claude Code", hint: "\u53EA\u914D ~/.claude/settings.json" },
317
- { value: "codex", label: "\u4EC5 Codex", hint: "\u53EA\u914D ~/.codex/config.toml" }
318
- ],
319
- initialValue: "both"
320
- });
321
- if (isCancel(target)) {
322
- cancel("\u5DF2\u53D6\u6D88");
323
- process.exit(0);
324
- }
325
- if (target === "claude") configCodex = false;
326
- else if (target === "codex") configClaude = false;
327
- }
328
- note(
329
- [
330
- `${pc.bold("\u8BA1\u8D39\u6A21\u5F0F")} \u6309\u91CF\u4ED8\u8D39`,
331
- configClaude ? `${pc.bold("Claude URL")} ${PRIMARY_HOST}` : null,
332
- configCodex ? `${pc.bold("Codex URL")} ${PRIMARY_HOST}/v1` : null,
333
- `${pc.bold("API Key")} ${maskKey(apiKey)}`,
334
- `${pc.bold("\u5C06\u914D\u7F6E")} ${[
335
- configClaude && "Claude Code",
336
- configCodex && "Codex"
337
- ].filter(Boolean).join(" + ")}`
338
- ].filter(Boolean).join("\n"),
339
- "\u914D\u7F6E\u9884\u89C8"
340
- );
341
- if (!args.apiKey) {
866
+ const selected = await pickClients(args.only, interactive);
867
+ if (selected.length === 0) {
868
+ cancel("\u672A\u9009\u62E9\u4EFB\u4F55\u5BA2\u6237\u7AEF");
869
+ process.exit(0);
870
+ }
871
+ const detection = await detectAll(selected, verbose);
872
+ const missing = detection.filter((d) => !d.availability.installed);
873
+ if (missing.length > 0) {
874
+ printMissingHints(missing);
875
+ process.exit(1);
876
+ }
877
+ printSummary({ apiKey, selected });
878
+ if (interactive) {
342
879
  const ok = await confirm({ message: "\u7EE7\u7EED\uFF1F", initialValue: true });
343
- if (isCancel(ok) || !ok) {
880
+ if (isCancel2(ok) || !ok) {
344
881
  cancel("\u5DF2\u53D6\u6D88");
345
882
  process.exit(0);
346
883
  }
347
884
  }
348
- if (configClaude) {
349
- const useSpinner = !verbose && process.stdout.isTTY;
350
- const s = useSpinner ? spinner() : null;
351
- s?.start("\u914D\u7F6E Claude Code");
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...");
353
- try {
354
- await configureViaZcf({
355
- target: "cc",
356
- baseUrl: claudeBaseUrl,
357
- apiKey,
358
- verbose,
359
- // 节流的进度回调:spinner 不会高频闪烁
360
- onProgress: (line) => s?.message(`\u914D\u7F6E Claude Code \xB7 ${pc.dim(line)}`)
361
- });
362
- s?.stop(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.claude/settings.json"));
363
- if (!s) log.success(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 \u2192 ~/.claude/settings.json"));
364
- } catch (err) {
365
- s?.stop(pc.red("\u2717 Claude Code \u914D\u7F6E\u5931\u8D25"));
366
- console.error(err.message);
367
- process.exit(1);
368
- }
369
- }
370
- if (configCodex) {
371
- const useSpinner = !verbose && process.stdout.isTTY;
372
- const s = useSpinner ? spinner() : null;
373
- s?.start("\u914D\u7F6E Codex");
374
- if (!s) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
375
- try {
376
- await configureViaZcf({
377
- target: "codex",
378
- baseUrl: codexBaseUrl,
379
- apiKey,
380
- verbose,
381
- onProgress: (line) => s?.message(`\u914D\u7F6E Codex \xB7 ${pc.dim(line)}`)
382
- });
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"));
385
- } catch (err) {
386
- s?.stop(pc.red("\u2717 Codex \u914D\u7F6E\u5931\u8D25"));
387
- console.error(err.message);
388
- process.exit(1);
389
- }
885
+ for (const client of selected) {
886
+ await configureOne(client, { host, apiKey, verbose });
390
887
  }
391
888
  if (!args.skipVerify) {
392
889
  const useSpinner = process.stdout.isTTY;
393
890
  const s = useSpinner ? spinner() : null;
394
891
  s?.start("\u9A8C\u8BC1\u8FDE\u901A\u6027");
395
- if (!s) log.info("\u6B63\u5728\u9A8C\u8BC1\u8FDE\u901A\u6027...");
396
- const result = await verifyConnection(claudeBaseUrl, apiKey);
892
+ if (!s) log3.info("\u6B63\u5728\u9A8C\u8BC1\u8FDE\u901A\u6027...");
893
+ const result = await verifyConnection(host, apiKey);
397
894
  if (result.ok) {
398
- const msg = pc.green("\u2713 \u94FE\u8DEF\u9A8C\u8BC1\u901A\u8FC7");
399
- s?.stop(msg) ?? log.success(msg);
895
+ const msg = pc6.green("\u2713 \u94FE\u8DEF\u9A8C\u8BC1\u901A\u8FC7");
896
+ s?.stop(msg) ?? log3.success(msg);
400
897
  } else {
401
- const msg = pc.yellow(
898
+ const msg = pc6.yellow(
402
899
  `\u26A0 \u9A8C\u8BC1\u672A\u901A\u8FC7${result.status ? ` (HTTP ${result.status})` : ""}` + (result.error ? `: ${result.error}` : "") + ` \u2014\u2014 \u914D\u7F6E\u5DF2\u5199\u5165\uFF0C\u53EF\u624B\u52A8\u6D4B\u8BD5`
403
900
  );
404
- s?.stop(msg) ?? log.warn(msg);
901
+ s?.stop(msg) ?? log3.warn(msg);
405
902
  }
406
903
  }
407
- const tips = [];
408
- if (configClaude) tips.push(pc.cyan("claude") + pc.dim(" # \u542F\u52A8 Claude Code"));
409
- if (configCodex) tips.push(pc.cyan("codex") + pc.dim(" # \u542F\u52A8 Codex"));
904
+ const tips = selected.map(
905
+ (c) => pc6.cyan(c.launchCommand) + pc6.dim(` # \u542F\u52A8 ${c.label}`)
906
+ );
410
907
  note(tips.join("\n"), "\u{1F389} \u5168\u90E8\u5B8C\u6210\uFF01\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
411
908
  outro(
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")
909
+ pc6.dim("\u611F\u8C22\u4F7F\u7528 EasyRouter ") + pc6.underline(pc6.cyan(PRIMARY_HOST)) + pc6.dim(" \xB7 Claude Code auto-configuration is powered by ") + pc6.underline("UfoMiao/zcf")
413
910
  );
414
911
  }
912
+ async function pickClients(only, interactive) {
913
+ if (only && only.length) {
914
+ return only.map((id) => CLIENT_REGISTRY[id]);
915
+ }
916
+ if (!interactive) {
917
+ return [CLIENT_REGISTRY.claude, CLIENT_REGISTRY.codex];
918
+ }
919
+ const all = listClients();
920
+ const picked = await multiselect({
921
+ message: "\u8BF7\u9009\u62E9\u8981\u914D\u7F6E\u7684\u5BA2\u6237\u7AEF\uFF08\u7A7A\u683C\u5207\u6362\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09",
922
+ options: all.map((c) => ({
923
+ value: c.id,
924
+ label: c.label,
925
+ hint: c.hint
926
+ })),
927
+ initialValues: [],
928
+ required: false
929
+ });
930
+ if (isCancel2(picked)) {
931
+ cancel("\u5DF2\u53D6\u6D88");
932
+ process.exit(0);
933
+ }
934
+ return picked.map((id) => CLIENT_REGISTRY[id]);
935
+ }
936
+ async function detectAll(clients, verbose) {
937
+ const results = [];
938
+ for (const c of clients) {
939
+ const availability = await c.detect();
940
+ results.push({ client: c, availability });
941
+ if (verbose) {
942
+ if (availability.installed) {
943
+ log3.info(
944
+ `\u2713 ${c.label}${availability.version ? ` v${availability.version}` : ""} \u5DF2\u5B89\u88C5`
945
+ );
946
+ } else {
947
+ log3.warn(`\u2717 ${c.label} \u672A\u68C0\u6D4B\u5230`);
948
+ }
949
+ }
950
+ }
951
+ return results;
952
+ }
953
+ function printMissingHints(missing) {
954
+ console.error();
955
+ console.error(pc6.red("\u2717 \u4EE5\u4E0B\u5BA2\u6237\u7AEF\u5C1A\u672A\u5B89\u88C5\uFF0C\u8BF7\u5148\u5B89\u88C5\u518D\u8FD0\u884C\u672C\u547D\u4EE4\uFF1A"));
956
+ console.error();
957
+ for (const { client, availability } of missing) {
958
+ const hint = availability.installHint || "\u8BF7\u67E5\u770B\u5B98\u65B9\u6587\u6863";
959
+ console.error(` ${pc6.bold(client.label)}\uFF1A`);
960
+ hint.split("\n").forEach((line) => {
961
+ console.error(` ${pc6.dim(line)}`);
962
+ });
963
+ console.error();
964
+ }
965
+ console.error(pc6.dim("\u5B89\u88C5\u5B8C\u6210\u540E\u91CD\u65B0\u8FD0\u884C\u672C\u547D\u4EE4\u5373\u53EF\u7EE7\u7EED\u914D\u7F6E\u3002"));
966
+ }
967
+ function printSummary(opts) {
968
+ const lines = [
969
+ `${pc6.bold("\u8BA1\u8D39\u6A21\u5F0F")} \u6309\u91CF\u4ED8\u8D39`,
970
+ `${pc6.bold("BaseURL")} ${PRIMARY_HOST}`,
971
+ `${pc6.bold("API Key")} ${maskKey(opts.apiKey)}`,
972
+ `${pc6.bold("\u5C06\u914D\u7F6E")} ${opts.selected.map((c) => c.label).join(" + ")}`
973
+ ];
974
+ note(lines.join("\n"), "\u914D\u7F6E\u9884\u89C8");
975
+ }
976
+ async function configureOne(client, ctx) {
977
+ const useSpinner = !ctx.verbose && process.stdout.isTTY;
978
+ const s = useSpinner ? spinner() : null;
979
+ s?.start(`\u914D\u7F6E ${client.label}`);
980
+ if (!s) log3.info(`\u6B63\u5728\u914D\u7F6E ${client.label}...`);
981
+ try {
982
+ const result = await client.configure({
983
+ host: ctx.host,
984
+ apiKey: ctx.apiKey,
985
+ verbose: ctx.verbose,
986
+ onProgress: (line) => s?.message(`\u914D\u7F6E ${client.label} \xB7 ${pc6.dim(line)}`)
987
+ });
988
+ const summary = `\u2192 ${result.configPaths.join(" + ")}` + (result.model ? ` model=${result.model}` : "");
989
+ s?.stop(pc6.green(`\u2713 ${client.label} \u914D\u7F6E\u5B8C\u6210 `) + pc6.dim(summary));
990
+ if (!s) log3.success(pc6.green(`\u2713 ${client.label} \u914D\u7F6E\u5B8C\u6210 ${summary}`));
991
+ } catch (err) {
992
+ s?.stop(pc6.red(`\u2717 ${client.label} \u914D\u7F6E\u5931\u8D25`));
993
+ console.error(err.message);
994
+ process.exit(1);
995
+ }
996
+ }
415
997
  main().catch((err) => {
416
998
  console.error();
417
- console.error(pc.red("\u2717 \u81F4\u547D\u9519\u8BEF\uFF1A"), err?.message || err);
999
+ console.error(pc6.red("\u2717 \u81F4\u547D\u9519\u8BEF\uFF1A"), err?.message || err);
418
1000
  if (err?.stack && process.env.DEBUG) {
419
- console.error(pc.dim(err.stack));
1001
+ console.error(pc6.dim(err.stack));
420
1002
  }
421
1003
  process.exit(1);
422
1004
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easyrouter-config",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "🚀 一键把 EasyRouter 接入 Claude Code & Codex —— 粘贴 Key 即用",
5
5
  "type": "module",
6
6
  "bin": {