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.
- package/dist/index.js +765 -183
- 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
|
-
|
|
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
|
|
17
|
-
|
|
16
|
+
import pc6 from "picocolors";
|
|
17
|
+
|
|
18
|
+
// src/cli/args.ts
|
|
18
19
|
import { parseArgs } from "util";
|
|
19
|
-
|
|
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
|
|
23
|
-
var
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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")}
|
|
58
|
-
${pc.green("npx -y easyrouter-config -k sk-xxx")}
|
|
59
|
-
${pc.green("npx -y easyrouter-config -k sk-xxx --only claude")}
|
|
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 <
|
|
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\
|
|
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
|
-
|
|
71
|
-
|
|
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 \
|
|
75
|
-
|
|
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 ?
|
|
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)
|
|
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(),
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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'
|
|
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",
|
|
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
|
|
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 >
|
|
183
|
-
killedReason = `\u5B50\u8FDB\u7A0B\u5DF2 ${Math.floor(
|
|
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(
|
|
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
|
-
},
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
241
|
-
|
|
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(),
|
|
245
|
-
const res = await fetch(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
840
|
+
intro(
|
|
841
|
+
pc6.bgCyan(pc6.black(" EasyRouter Config ")) + pc6.dim(" v" + VERSION)
|
|
842
|
+
);
|
|
281
843
|
const verbose = !!args.verbose;
|
|
282
|
-
const
|
|
283
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 (
|
|
880
|
+
if (isCancel2(ok) || !ok) {
|
|
344
881
|
cancel("\u5DF2\u53D6\u6D88");
|
|
345
882
|
process.exit(0);
|
|
346
883
|
}
|
|
347
884
|
}
|
|
348
|
-
|
|
349
|
-
|
|
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)
|
|
396
|
-
const result = await verifyConnection(
|
|
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 =
|
|
399
|
-
s?.stop(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 =
|
|
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) ??
|
|
901
|
+
s?.stop(msg) ?? log3.warn(msg);
|
|
405
902
|
}
|
|
406
903
|
}
|
|
407
|
-
const tips =
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1001
|
+
console.error(pc6.dim(err.stack));
|
|
420
1002
|
}
|
|
421
1003
|
process.exit(1);
|
|
422
1004
|
});
|