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.
- package/dist/index.js +161 -67
- 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.
|
|
20
|
-
var
|
|
21
|
-
var
|
|
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
|
|
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 ${
|
|
69
|
-
Codex \u2192 ${
|
|
70
|
+
Claude Code \u2192 ${PRIMARY_HOST}
|
|
71
|
+
Codex \u2192 ${PRIMARY_HOST}/v1
|
|
70
72
|
|
|
71
73
|
${pc.bold("\u539F\u7406:")}
|
|
72
|
-
\
|
|
73
|
-
\
|
|
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
|
|
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
|
-
"
|
|
86
|
-
opts.
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
// --skip-prompt 非交互模式
|
|
116
|
+
"-T",
|
|
117
|
+
opts.target,
|
|
118
|
+
// 'cc' | 'codex'(用文档推荐的简写 -T)
|
|
119
|
+
// EasyRouter 是 OpenAI 兼容网关,统一用 api_key(Bearer Authorization)
|
|
89
120
|
"--api-type",
|
|
90
|
-
"
|
|
91
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 \
|
|
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\
|
|
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")} ${
|
|
237
|
-
configCodex ? `${pc.bold("Codex URL")} ${
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
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
|
|
263
|
-
|
|
264
|
-
baseUrl:
|
|
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
|
-
|
|
278
|
-
|
|
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
|
|
374
|
+
if (!s) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
|
|
283
375
|
try {
|
|
284
|
-
await
|
|
285
|
-
|
|
286
|
-
baseUrl:
|
|
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
|
|
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(
|
|
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(
|
|
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) => {
|