easyrouter-config 1.0.5 → 1.0.7
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 +798 -234
- 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.7" : "0.0.0-dev";
|
|
20
24
|
var PRIMARY_HOST = "https://easyrouter.io";
|
|
21
25
|
var FALLBACK_HOSTS = ["https://ezr.sh"];
|
|
22
|
-
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;
|
|
23
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,64 +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";
|
|
110
151
|
try {
|
|
111
152
|
const ctrl = new AbortController();
|
|
112
|
-
const timer = setTimeout(() => ctrl.abort(),
|
|
113
|
-
const res = await fetch(
|
|
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
|
+
});
|
|
114
162
|
clearTimeout(timer);
|
|
115
|
-
|
|
116
|
-
if (verbose) log.warn(`\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1AHTTP ${res.status}\uFF08\u5C06\u4F7F\u7528\u515C\u5E95\u6A21\u578B\uFF09`);
|
|
117
|
-
return [];
|
|
118
|
-
}
|
|
119
|
-
const json = await res.json();
|
|
120
|
-
const models = Array.isArray(json?.data) ? json.data : [];
|
|
121
|
-
if (verbose) log.info(`\u2713 \u62C9\u53D6\u5230 ${models.length} \u4E2A EasyRouter \u6A21\u578B`);
|
|
122
|
-
return models;
|
|
163
|
+
return { ok: res.ok, status: res.status };
|
|
123
164
|
} catch (err) {
|
|
124
|
-
|
|
125
|
-
return [];
|
|
165
|
+
return { ok: false, error: err?.message || String(err) };
|
|
126
166
|
}
|
|
127
167
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const models = await fetchEasyRouterModels(host, verbose);
|
|
144
|
-
const auto = pickBestCodexModel(models);
|
|
145
|
-
if (auto) {
|
|
146
|
-
if (verbose) log.info(`\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF1A${pc.cyan(auto)}`);
|
|
147
|
-
return auto;
|
|
148
|
-
}
|
|
149
|
-
if (interactive && models.length) {
|
|
150
|
-
const candidates = models.filter((m) => /^gpt-5|codex/i.test(m.model_name)).sort((a, b) => b.model_name.localeCompare(a.model_name, "en")).slice(0, 8);
|
|
151
|
-
if (candidates.length) {
|
|
152
|
-
const picked = await select({
|
|
153
|
-
message: "\u672A\u81EA\u52A8\u8BC6\u522B Codex \u63A8\u8350\u6A21\u578B\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9",
|
|
154
|
-
options: candidates.map((m) => ({ value: m.model_name, label: m.model_name }))
|
|
155
|
-
});
|
|
156
|
-
if (!isCancel(picked)) return picked;
|
|
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 };
|
|
157
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 };
|
|
158
197
|
}
|
|
159
|
-
if (verbose) log.warn(`\u65E0\u6CD5\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF0C\u4F7F\u7528\u515C\u5E95\uFF1A${CODEX_FALLBACK_MODEL}`);
|
|
160
|
-
return CODEX_FALLBACK_MODEL;
|
|
161
198
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
199
|
+
|
|
200
|
+
// src/clients/zcf-runner.ts
|
|
201
|
+
import { execa as execa2 } from "execa";
|
|
202
|
+
import pc3 from "picocolors";
|
|
203
|
+
async function runZcf(opts) {
|
|
165
204
|
const args = [
|
|
166
205
|
"-y",
|
|
167
206
|
"zcf",
|
|
@@ -170,7 +209,7 @@ async function configureViaZcf(opts) {
|
|
|
170
209
|
// --skip-prompt 非交互模式
|
|
171
210
|
"-T",
|
|
172
211
|
opts.target,
|
|
173
|
-
// 'cc' | 'codex'
|
|
212
|
+
// 'cc' | 'codex'
|
|
174
213
|
// EasyRouter 是 OpenAI 兼容网关,统一用 api_key(Bearer Authorization)
|
|
175
214
|
"--api-type",
|
|
176
215
|
"api_key",
|
|
@@ -180,10 +219,8 @@ async function configureViaZcf(opts) {
|
|
|
180
219
|
opts.baseUrl,
|
|
181
220
|
"-r",
|
|
182
221
|
"backup",
|
|
183
|
-
// 已有配置自动备份
|
|
184
222
|
"--mcp-services",
|
|
185
223
|
"skip",
|
|
186
|
-
// 不装 MCP(即便 zcf 偶尔忽略也无伤)
|
|
187
224
|
"--workflows",
|
|
188
225
|
"skip",
|
|
189
226
|
"--output-styles",
|
|
@@ -201,8 +238,7 @@ async function configureViaZcf(opts) {
|
|
|
201
238
|
let lastDataAt = Date.now();
|
|
202
239
|
let killedReason = null;
|
|
203
240
|
let lastProgressAt = 0;
|
|
204
|
-
const
|
|
205
|
-
const child = execa(cmd, args, {
|
|
241
|
+
const child = execa2(cmd, args, {
|
|
206
242
|
stdio: ["ignore", "pipe", "pipe"],
|
|
207
243
|
env: {
|
|
208
244
|
...process.env,
|
|
@@ -234,8 +270,10 @@ async function configureViaZcf(opts) {
|
|
|
234
270
|
child.stdout?.on("data", (c) => handleChunk(c, "out"));
|
|
235
271
|
child.stderr?.on("data", (c) => handleChunk(c, "err"));
|
|
236
272
|
const idleTimer = setInterval(() => {
|
|
237
|
-
if (Date.now() - lastDataAt >
|
|
238
|
-
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`;
|
|
239
277
|
child.kill("SIGTERM");
|
|
240
278
|
setTimeout(() => {
|
|
241
279
|
try {
|
|
@@ -246,7 +284,9 @@ async function configureViaZcf(opts) {
|
|
|
246
284
|
}
|
|
247
285
|
}, 5e3);
|
|
248
286
|
const hardTimer = setTimeout(() => {
|
|
249
|
-
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`;
|
|
250
290
|
child.kill("SIGTERM");
|
|
251
291
|
setTimeout(() => {
|
|
252
292
|
try {
|
|
@@ -254,7 +294,7 @@ async function configureViaZcf(opts) {
|
|
|
254
294
|
} catch {
|
|
255
295
|
}
|
|
256
296
|
}, 5e3);
|
|
257
|
-
},
|
|
297
|
+
}, SUBPROCESS_HARD_TIMEOUT_MS);
|
|
258
298
|
const result = await child.catch((e) => e);
|
|
259
299
|
clearInterval(idleTimer);
|
|
260
300
|
clearTimeout(hardTimer);
|
|
@@ -269,7 +309,7 @@ async function configureViaZcf(opts) {
|
|
|
269
309
|
\u2022 npm \u955C\u50CF/\u7F51\u7EDC\u6162\uFF0Cnpx \u4E0B\u8F7D zcf \u5305\u5361\u4F4F
|
|
270
310
|
\u2022 \u9632\u706B\u5899\u62E6\u622A\u4E86 registry.npmjs.org
|
|
271
311
|
|
|
272
|
-
\u5EFA\u8BAE\uFF1A\u7528 ${
|
|
312
|
+
\u5EFA\u8BAE\uFF1A\u7528 ${pc3.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u67E5\u770B\u8BE6\u7EC6\u65E5\u5FD7`
|
|
273
313
|
);
|
|
274
314
|
}
|
|
275
315
|
if (exitCode !== 0 || signal) {
|
|
@@ -278,7 +318,7 @@ async function configureViaZcf(opts) {
|
|
|
278
318
|
`zcf \u5F02\u5E38\u9000\u51FA\uFF08exit=${exitCode}\uFF09:
|
|
279
319
|
${detail}
|
|
280
320
|
|
|
281
|
-
\u{1F4A1} \u7528 ${
|
|
321
|
+
\u{1F4A1} \u7528 ${pc3.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
|
|
282
322
|
);
|
|
283
323
|
}
|
|
284
324
|
const combined = capturedOut + "\n" + capturedErr;
|
|
@@ -292,35 +332,544 @@ ${detail}
|
|
|
292
332
|
);
|
|
293
333
|
}
|
|
294
334
|
}
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
function toOpenClawCost(m) {
|
|
366
|
+
const ratio = m.model_ratio ?? 0;
|
|
367
|
+
const compRatio = m.completion_ratio ?? 1;
|
|
368
|
+
const cacheRatio = m.cache_ratio ?? 0;
|
|
369
|
+
const input = round6(ratio * 2e-3);
|
|
370
|
+
const output = round6(ratio * compRatio * 2e-3);
|
|
371
|
+
const cacheRead = round6(ratio * cacheRatio * 2e-3);
|
|
372
|
+
const cacheWrite = round6(input * 1.25);
|
|
373
|
+
const tiered = m.tiered_pricing;
|
|
374
|
+
const contextWindow = tiered?.input?.threshold ?? tiered?.output?.threshold ?? tiered?.cache_read?.threshold ?? 131072;
|
|
375
|
+
const maxTokens = Math.min(32768, Math.max(4096, Math.floor(contextWindow / 4)));
|
|
376
|
+
return {
|
|
377
|
+
cost: { input, output, cacheRead, cacheWrite },
|
|
378
|
+
contextWindow,
|
|
379
|
+
maxTokens
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function round6(n) {
|
|
383
|
+
return Math.round(n * 1e6) / 1e6;
|
|
384
|
+
}
|
|
385
|
+
async function fetchEasyRouterModels(host, verbose) {
|
|
386
|
+
const url = host.replace(/\/+$/, "") + "/api/pricing";
|
|
297
387
|
try {
|
|
298
388
|
const ctrl = new AbortController();
|
|
299
|
-
const timer = setTimeout(() => ctrl.abort(),
|
|
300
|
-
const res = await fetch(
|
|
301
|
-
method: "GET",
|
|
302
|
-
headers: {
|
|
303
|
-
Authorization: `Bearer ${apiKey}`,
|
|
304
|
-
"anthropic-version": "2023-06-01"
|
|
305
|
-
},
|
|
306
|
-
signal: ctrl.signal
|
|
307
|
-
});
|
|
389
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_MODELS_TIMEOUT_MS);
|
|
390
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
308
391
|
clearTimeout(timer);
|
|
309
|
-
|
|
392
|
+
if (!res.ok) {
|
|
393
|
+
if (verbose)
|
|
394
|
+
log2.warn(`\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1AHTTP ${res.status}\uFF08\u5C06\u4F7F\u7528\u515C\u5E95\u6A21\u578B\uFF09`);
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
const json = await res.json();
|
|
398
|
+
const models = Array.isArray(json?.data) ? json.data : [];
|
|
399
|
+
if (verbose) log2.info(`\u2713 \u62C9\u53D6\u5230 ${models.length} \u4E2A EasyRouter \u6A21\u578B`);
|
|
400
|
+
return models;
|
|
310
401
|
} catch (err) {
|
|
311
|
-
|
|
402
|
+
if (verbose)
|
|
403
|
+
log2.warn(`\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5F02\u5E38\uFF1A${err?.message || err}\uFF08\u5C06\u4F7F\u7528\u515C\u5E95\u6A21\u578B\uFF09`);
|
|
404
|
+
return [];
|
|
312
405
|
}
|
|
313
406
|
}
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
407
|
+
function filterOpenAICompatibleLLMs(models) {
|
|
408
|
+
return models.filter((m) => {
|
|
409
|
+
const isLlm = (m.tags || "").toUpperCase().includes("LLM");
|
|
410
|
+
const supportsOpenAI = !m.supported_endpoint_types || m.supported_endpoint_types.length === 0 || m.supported_endpoint_types.includes("openai");
|
|
411
|
+
return isLlm && supportsOpenAI;
|
|
412
|
+
});
|
|
319
413
|
}
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
414
|
+
function pickBestCodexModel(models) {
|
|
415
|
+
const isLlm = (m) => (m.tags || "").toUpperCase().includes("LLM");
|
|
416
|
+
const supportsOpenAI = (m) => !m.supported_endpoint_types || m.supported_endpoint_types.includes("openai");
|
|
417
|
+
const isGpt5 = (m) => /^gpt-5(\b|[.\-])/i.test(m.model_name);
|
|
418
|
+
const isSmallTier = (m) => /(^|[\-_.])(mini|nano|small|lite|tiny)([\-_.]|$)/i.test(m.model_name);
|
|
419
|
+
const extractVersion = (name) => {
|
|
420
|
+
const m = name.match(/^gpt-(\d+(?:\.\d+)*)/i);
|
|
421
|
+
return m ? m[1] : "0";
|
|
422
|
+
};
|
|
423
|
+
const compareVersion = (a, b) => {
|
|
424
|
+
const pa = a.split(".").map(Number);
|
|
425
|
+
const pb = b.split(".").map(Number);
|
|
426
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
427
|
+
const x = pa[i] || 0;
|
|
428
|
+
const y = pb[i] || 0;
|
|
429
|
+
if (x !== y) return x - y;
|
|
430
|
+
}
|
|
431
|
+
return 0;
|
|
432
|
+
};
|
|
433
|
+
const pickFromPool = (pool) => {
|
|
434
|
+
if (!pool.length) return null;
|
|
435
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const m of pool) {
|
|
437
|
+
const v = extractVersion(m.model_name);
|
|
438
|
+
if (!byVersion.has(v)) byVersion.set(v, []);
|
|
439
|
+
byVersion.get(v).push(m);
|
|
440
|
+
}
|
|
441
|
+
const maxVersion = Array.from(byVersion.keys()).sort(compareVersion).pop();
|
|
442
|
+
const sameVersion = byVersion.get(maxVersion);
|
|
443
|
+
const withCodex = sameVersion.filter((m) => /codex/i.test(m.model_name));
|
|
444
|
+
if (withCodex.length) {
|
|
445
|
+
return withCodex.sort(
|
|
446
|
+
(a, b) => a.model_name.localeCompare(b.model_name)
|
|
447
|
+
)[0].model_name;
|
|
448
|
+
}
|
|
449
|
+
return sameVersion.sort(
|
|
450
|
+
(a, b) => a.model_name.localeCompare(b.model_name)
|
|
451
|
+
)[0].model_name;
|
|
452
|
+
};
|
|
453
|
+
const tier1 = pickFromPool(
|
|
454
|
+
models.filter(
|
|
455
|
+
(m) => isGpt5(m) && isLlm(m) && supportsOpenAI(m) && !isSmallTier(m)
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
if (tier1) return tier1;
|
|
459
|
+
const tier2 = pickFromPool(
|
|
460
|
+
models.filter((m) => isGpt5(m) && isLlm(m) && supportsOpenAI(m))
|
|
461
|
+
);
|
|
462
|
+
if (tier2) return tier2;
|
|
463
|
+
const tier3 = pickFromPool(
|
|
464
|
+
models.filter((m) => isLlm(m) && supportsOpenAI(m) && !isSmallTier(m))
|
|
465
|
+
);
|
|
466
|
+
if (tier3) return tier3;
|
|
467
|
+
return null;
|
|
323
468
|
}
|
|
469
|
+
async function resolveCodexModel(host, interactive, verbose) {
|
|
470
|
+
const models = await fetchEasyRouterModels(host, verbose);
|
|
471
|
+
const auto = pickBestCodexModel(models);
|
|
472
|
+
if (auto) {
|
|
473
|
+
if (verbose) log2.info(`\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF1A${pc4.cyan(auto)}`);
|
|
474
|
+
return auto;
|
|
475
|
+
}
|
|
476
|
+
if (interactive && models.length) {
|
|
477
|
+
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);
|
|
478
|
+
if (candidates.length) {
|
|
479
|
+
const picked = await select({
|
|
480
|
+
message: "\u672A\u81EA\u52A8\u8BC6\u522B Codex \u63A8\u8350\u6A21\u578B\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9",
|
|
481
|
+
options: candidates.map((m) => ({
|
|
482
|
+
value: m.model_name,
|
|
483
|
+
label: m.model_name
|
|
484
|
+
}))
|
|
485
|
+
});
|
|
486
|
+
if (!isCancel(picked)) return picked;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (verbose)
|
|
490
|
+
log2.warn(`\u65E0\u6CD5\u81EA\u52A8\u9009\u62E9 Codex \u6A21\u578B\uFF0C\u4F7F\u7528\u515C\u5E95\uFF1A${CODEX_FALLBACK_MODEL}`);
|
|
491
|
+
return CODEX_FALLBACK_MODEL;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/clients/codex.ts
|
|
495
|
+
var codexClient = {
|
|
496
|
+
id: "codex",
|
|
497
|
+
label: "Codex",
|
|
498
|
+
hint: "~/.codex/config.toml + auth.json",
|
|
499
|
+
launchCommand: "codex",
|
|
500
|
+
async detect() {
|
|
501
|
+
const r = await detectCommand({ command: "codex" });
|
|
502
|
+
return {
|
|
503
|
+
installed: r.installed,
|
|
504
|
+
version: r.version,
|
|
505
|
+
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"
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
async configure(ctx) {
|
|
509
|
+
const interactive = typeof process !== "undefined" && !!process.stdout.isTTY;
|
|
510
|
+
const model = await resolveCodexModel(ctx.host, interactive, ctx.verbose);
|
|
511
|
+
const codexBaseUrl = ctx.host.replace(/\/+$/, "") + "/v1";
|
|
512
|
+
await runZcf({
|
|
513
|
+
target: "codex",
|
|
514
|
+
baseUrl: codexBaseUrl,
|
|
515
|
+
apiKey: ctx.apiKey,
|
|
516
|
+
apiModel: model,
|
|
517
|
+
verbose: ctx.verbose,
|
|
518
|
+
onProgress: ctx.onProgress
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
configPaths: ["~/.codex/config.toml", "~/.codex/auth.json"],
|
|
522
|
+
model
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/clients/openclaw.ts
|
|
528
|
+
import { execa as execa3 } from "execa";
|
|
529
|
+
import { promises as fs } from "fs";
|
|
530
|
+
import { homedir } from "os";
|
|
531
|
+
import { join, dirname } from "path";
|
|
532
|
+
import pc5 from "picocolors";
|
|
533
|
+
var OPENCLAW_PROVIDER_ID = "custom-easyrouter-io";
|
|
534
|
+
var openClawClient = {
|
|
535
|
+
id: "openclaw",
|
|
536
|
+
label: "OpenClaw",
|
|
537
|
+
hint: "~/.openclaw/openclaw.json",
|
|
538
|
+
launchCommand: "openclaw",
|
|
539
|
+
async detect() {
|
|
540
|
+
const r = await detectCommand({ command: "openclaw" });
|
|
541
|
+
return {
|
|
542
|
+
installed: r.installed,
|
|
543
|
+
version: r.version,
|
|
544
|
+
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"
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
async configure(ctx) {
|
|
548
|
+
const interactive = typeof process !== "undefined" && !!process.stdout.isTTY;
|
|
549
|
+
const allModels = await fetchEasyRouterModels(ctx.host, ctx.verbose);
|
|
550
|
+
const llms = filterOpenAICompatibleLLMs(allModels);
|
|
551
|
+
const defaultModel = await resolveCodexModel(
|
|
552
|
+
ctx.host,
|
|
553
|
+
interactive,
|
|
554
|
+
ctx.verbose
|
|
555
|
+
);
|
|
556
|
+
const baseUrl = ctx.host.replace(/\/+$/, "") + "/v1";
|
|
557
|
+
const args = [
|
|
558
|
+
"onboard",
|
|
559
|
+
"--non-interactive",
|
|
560
|
+
"--auth-choice",
|
|
561
|
+
"custom-api-key",
|
|
562
|
+
"--custom-base-url",
|
|
563
|
+
baseUrl,
|
|
564
|
+
"--custom-model-id",
|
|
565
|
+
defaultModel,
|
|
566
|
+
"--custom-api-key",
|
|
567
|
+
ctx.apiKey,
|
|
568
|
+
"--custom-compatibility",
|
|
569
|
+
"openai",
|
|
570
|
+
"--secret-input-mode",
|
|
571
|
+
"plaintext",
|
|
572
|
+
"--accept-risk",
|
|
573
|
+
// 自定义 provider 必填
|
|
574
|
+
"--skip-health"
|
|
575
|
+
// 不要求 gateway daemon 启动
|
|
576
|
+
];
|
|
577
|
+
await runOpenClawSubprocess(args, ctx);
|
|
578
|
+
let registered = 0;
|
|
579
|
+
if (llms.length > 1) {
|
|
580
|
+
registered = await patchOpenClawJson(llms, defaultModel, ctx);
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
configPaths: ["~/.openclaw/openclaw.json"],
|
|
584
|
+
model: registered > 1 ? `${defaultModel} (+${registered - 1} more)` : defaultModel
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
async function patchOpenClawJson(llms, defaultModel, ctx) {
|
|
589
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
590
|
+
let json;
|
|
591
|
+
try {
|
|
592
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
593
|
+
json = JSON.parse(raw);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
if (ctx.verbose) {
|
|
596
|
+
console.warn(
|
|
597
|
+
`[openclaw] \u8DF3\u8FC7 patch\uFF1A\u8BFB ${configPath} \u5931\u8D25\uFF08${err?.code || err?.message}\uFF09`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return 1;
|
|
601
|
+
}
|
|
602
|
+
const providers = json?.models?.providers;
|
|
603
|
+
if (!providers || typeof providers !== "object") {
|
|
604
|
+
if (ctx.verbose) {
|
|
605
|
+
console.warn(`[openclaw] \u8DF3\u8FC7 patch\uFF1Amodels.providers \u6BB5\u4E0D\u5B58\u5728`);
|
|
606
|
+
}
|
|
607
|
+
return 1;
|
|
608
|
+
}
|
|
609
|
+
const provider = providers[OPENCLAW_PROVIDER_ID];
|
|
610
|
+
if (!provider || !Array.isArray(provider.models)) {
|
|
611
|
+
if (ctx.verbose) {
|
|
612
|
+
console.warn(
|
|
613
|
+
`[openclaw] \u8DF3\u8FC7 patch\uFF1Amodels.providers.${OPENCLAW_PROVIDER_ID}.models \u4E0D\u662F\u6570\u7EC4`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return 1;
|
|
617
|
+
}
|
|
618
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
619
|
+
provider.models.forEach((m, i) => {
|
|
620
|
+
existingById.set(m.id, i);
|
|
621
|
+
});
|
|
622
|
+
const before = provider.models.length;
|
|
623
|
+
let updated = 0;
|
|
624
|
+
const agentsModels = json?.agents?.defaults?.models ?? null;
|
|
625
|
+
for (const m of llms) {
|
|
626
|
+
const { cost, contextWindow, maxTokens } = toOpenClawCost(m);
|
|
627
|
+
const idx = existingById.get(m.model_name);
|
|
628
|
+
if (idx !== void 0) {
|
|
629
|
+
const existing = provider.models[idx];
|
|
630
|
+
if (existing.cost?.input === 0 && existing.cost?.output === 0) {
|
|
631
|
+
existing.cost = cost;
|
|
632
|
+
existing.contextWindow = contextWindow;
|
|
633
|
+
existing.maxTokens = maxTokens;
|
|
634
|
+
updated++;
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
provider.models.push({
|
|
638
|
+
id: m.model_name,
|
|
639
|
+
name: m.model_name + " (EasyRouter)",
|
|
640
|
+
contextWindow,
|
|
641
|
+
maxTokens,
|
|
642
|
+
input: ["text"],
|
|
643
|
+
cost,
|
|
644
|
+
reasoning: false
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
if (agentsModels !== null) {
|
|
648
|
+
const agentKey = `${OPENCLAW_PROVIDER_ID}/${m.model_name}`;
|
|
649
|
+
const existing = agentsModels[agentKey];
|
|
650
|
+
if (!existing || Object.keys(existing).length === 0) {
|
|
651
|
+
agentsModels[agentKey] = { alias: agentKey };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const added = provider.models.length - before;
|
|
656
|
+
if (added === 0 && updated === 0) {
|
|
657
|
+
if (ctx.verbose) {
|
|
658
|
+
console.warn(
|
|
659
|
+
`[openclaw] \u6240\u6709 ${llms.length} \u4E2A\u6A21\u578B\u5747\u5DF2\u662F\u6700\u65B0\uFF0C\u8DF3\u8FC7\u5199\u5165`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
return provider.models.length;
|
|
663
|
+
}
|
|
664
|
+
await fs.copyFile(configPath, configPath + ".bak." + Date.now());
|
|
665
|
+
await atomicWriteJson(configPath, json);
|
|
666
|
+
if (ctx.verbose) {
|
|
667
|
+
const parts = [];
|
|
668
|
+
if (added > 0) parts.push(`\u65B0\u589E ${added} \u4E2A`);
|
|
669
|
+
if (updated > 0) parts.push(`\u4FEE\u6B63 cost/ctx ${updated} \u4E2A`);
|
|
670
|
+
console.warn(
|
|
671
|
+
`[openclaw] \u2713 ${parts.join("\uFF0C")}\uFF0C\u5171 ${provider.models.length} \u4E2A\uFF08\u9ED8\u8BA4\uFF1A${defaultModel}\uFF09`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
return provider.models.length;
|
|
675
|
+
}
|
|
676
|
+
async function atomicWriteJson(path, obj) {
|
|
677
|
+
const tmp = path + ".tmp." + process.pid;
|
|
678
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
679
|
+
await fs.writeFile(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
680
|
+
await fs.rename(tmp, path);
|
|
681
|
+
}
|
|
682
|
+
async function runOpenClawSubprocess(args, ctx) {
|
|
683
|
+
let capturedOut = "";
|
|
684
|
+
let capturedErr = "";
|
|
685
|
+
let lastDataAt = Date.now();
|
|
686
|
+
let killedReason = null;
|
|
687
|
+
let lastProgressAt = 0;
|
|
688
|
+
const child = execa3("openclaw", args, {
|
|
689
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
690
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
691
|
+
reject: false,
|
|
692
|
+
windowsHide: true
|
|
693
|
+
});
|
|
694
|
+
const handleChunk = (chunk, target) => {
|
|
695
|
+
const text2 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
696
|
+
if (target === "out") capturedOut += text2;
|
|
697
|
+
else capturedErr += text2;
|
|
698
|
+
lastDataAt = Date.now();
|
|
699
|
+
if (ctx.onProgress) {
|
|
700
|
+
const now = Date.now();
|
|
701
|
+
if (now - lastProgressAt >= PROGRESS_THROTTLE_MS) {
|
|
702
|
+
lastProgressAt = now;
|
|
703
|
+
const lines = text2.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
704
|
+
const last = lines[lines.length - 1];
|
|
705
|
+
if (last) ctx.onProgress(last.slice(0, 60));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (ctx.verbose) process.stderr.write(text2);
|
|
709
|
+
};
|
|
710
|
+
child.stdout?.on("data", (c) => handleChunk(c, "out"));
|
|
711
|
+
child.stderr?.on("data", (c) => handleChunk(c, "err"));
|
|
712
|
+
const idleTimer = setInterval(() => {
|
|
713
|
+
if (Date.now() - lastDataAt > SUBPROCESS_IDLE_TIMEOUT_MS) {
|
|
714
|
+
killedReason = `openclaw \u5DF2 ${Math.floor(
|
|
715
|
+
SUBPROCESS_IDLE_TIMEOUT_MS / 1e3
|
|
716
|
+
)} \u79D2\u65E0\u8F93\u51FA\uFF08\u7591\u4F3C\u5361\u4F4F\uFF09`;
|
|
717
|
+
child.kill("SIGTERM");
|
|
718
|
+
setTimeout(() => {
|
|
719
|
+
try {
|
|
720
|
+
child.kill("SIGKILL");
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}, 5e3);
|
|
724
|
+
}
|
|
725
|
+
}, 5e3);
|
|
726
|
+
const hardTimer = setTimeout(() => {
|
|
727
|
+
killedReason = `openclaw \u8D85\u8FC7 ${Math.floor(
|
|
728
|
+
SUBPROCESS_HARD_TIMEOUT_MS / 1e3
|
|
729
|
+
)} \u79D2\u672A\u7ED3\u675F\uFF08\u5F3A\u5236\u8D85\u65F6\uFF09`;
|
|
730
|
+
child.kill("SIGTERM");
|
|
731
|
+
setTimeout(() => {
|
|
732
|
+
try {
|
|
733
|
+
child.kill("SIGKILL");
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
}, 5e3);
|
|
737
|
+
}, SUBPROCESS_HARD_TIMEOUT_MS);
|
|
738
|
+
const result = await child.catch((e) => e);
|
|
739
|
+
clearInterval(idleTimer);
|
|
740
|
+
clearTimeout(hardTimer);
|
|
741
|
+
const exitCode = result?.exitCode ?? null;
|
|
742
|
+
if (killedReason) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
`openclaw \u8C03\u7528\u5931\u8D25\uFF1A${killedReason}
|
|
745
|
+
|
|
746
|
+
\u5EFA\u8BAE\uFF1A\u7528 ${pc5.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u67E5\u770B\u8BE6\u7EC6\u65E5\u5FD7`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (exitCode !== 0) {
|
|
750
|
+
const detail = (capturedErr || capturedOut || "").slice(-2e3) || `exit ${exitCode}`;
|
|
751
|
+
throw new Error(
|
|
752
|
+
`openclaw \u5F02\u5E38\u9000\u51FA\uFF08exit=${exitCode}\uFF09:
|
|
753
|
+
${detail}
|
|
754
|
+
|
|
755
|
+
\u{1F4A1} \u7528 ${pc5.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/clients/hermes.ts
|
|
761
|
+
import { execa as execa4 } from "execa";
|
|
762
|
+
import { promises as fs2 } from "fs";
|
|
763
|
+
import { homedir as homedir2 } from "os";
|
|
764
|
+
import { join as join2 } from "path";
|
|
765
|
+
var HERMES_PROVIDER_NAME = "easyrouter";
|
|
766
|
+
var HERMES_KEY_ENV = "EASYROUTER_API_KEY";
|
|
767
|
+
var hermesClient = {
|
|
768
|
+
id: "hermes",
|
|
769
|
+
label: "Hermes",
|
|
770
|
+
hint: "~/.hermes/config.yaml + .env",
|
|
771
|
+
launchCommand: "hermes",
|
|
772
|
+
async detect() {
|
|
773
|
+
const r = await detectCommand({ command: "hermes" });
|
|
774
|
+
return {
|
|
775
|
+
installed: r.installed,
|
|
776
|
+
version: r.version,
|
|
777
|
+
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"
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
async configure(ctx) {
|
|
781
|
+
const interactive = typeof process !== "undefined" && !!process.stdout.isTTY;
|
|
782
|
+
const defaultModel = await resolveCodexModel(
|
|
783
|
+
ctx.host,
|
|
784
|
+
interactive,
|
|
785
|
+
ctx.verbose
|
|
786
|
+
);
|
|
787
|
+
const baseUrl = ctx.host.replace(/\/+$/, "") + "/v1";
|
|
788
|
+
await runHermesConfigSet(
|
|
789
|
+
`providers.${HERMES_PROVIDER_NAME}.base_url`,
|
|
790
|
+
baseUrl,
|
|
791
|
+
ctx
|
|
792
|
+
);
|
|
793
|
+
if (ctx.onProgress) ctx.onProgress(`provider \u6CE8\u518C`);
|
|
794
|
+
await runHermesConfigSet(
|
|
795
|
+
`providers.${HERMES_PROVIDER_NAME}.key_env`,
|
|
796
|
+
HERMES_KEY_ENV,
|
|
797
|
+
ctx
|
|
798
|
+
);
|
|
799
|
+
await runHermesConfigSet(
|
|
800
|
+
`providers.${HERMES_PROVIDER_NAME}.kind`,
|
|
801
|
+
"openai",
|
|
802
|
+
ctx
|
|
803
|
+
);
|
|
804
|
+
const fullDefaultId = `${HERMES_PROVIDER_NAME}/${defaultModel}`;
|
|
805
|
+
await runHermesConfigSet("model.default", fullDefaultId, ctx);
|
|
806
|
+
await runHermesConfigSet("model.provider", HERMES_PROVIDER_NAME, ctx);
|
|
807
|
+
await runHermesConfigSet("model.base_url", baseUrl, ctx);
|
|
808
|
+
if (ctx.onProgress) ctx.onProgress(`\u9ED8\u8BA4 model = ${defaultModel}`);
|
|
809
|
+
const envPath = join2(homedir2(), ".hermes", ".env");
|
|
810
|
+
await fs2.mkdir(join2(homedir2(), ".hermes"), { recursive: true });
|
|
811
|
+
let envContent = "";
|
|
812
|
+
try {
|
|
813
|
+
envContent = await fs2.readFile(envPath, "utf8");
|
|
814
|
+
} catch (err) {
|
|
815
|
+
if (err.code !== "ENOENT") throw err;
|
|
816
|
+
}
|
|
817
|
+
envContent = upsertEnvVar(envContent, HERMES_KEY_ENV, ctx.apiKey);
|
|
818
|
+
await fs2.writeFile(envPath, envContent, { encoding: "utf8", mode: 384 });
|
|
819
|
+
if (ctx.onProgress) ctx.onProgress("\u5199\u5165 ~/.hermes/.env");
|
|
820
|
+
return {
|
|
821
|
+
configPaths: ["~/.hermes/config.yaml", "~/.hermes/.env"],
|
|
822
|
+
model: defaultModel
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
async function runHermesConfigSet(key, value, ctx) {
|
|
827
|
+
const result = await execa4("hermes", ["config", "set", key, value], {
|
|
828
|
+
reject: false,
|
|
829
|
+
timeout: 3e4,
|
|
830
|
+
windowsHide: true,
|
|
831
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
832
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
833
|
+
});
|
|
834
|
+
if (ctx.verbose && (result.stdout || result.stderr)) {
|
|
835
|
+
process.stderr.write(
|
|
836
|
+
`[hermes config set ${key}]
|
|
837
|
+
${result.stdout}
|
|
838
|
+
${result.stderr}
|
|
839
|
+
`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
if (result.exitCode !== 0) {
|
|
843
|
+
const detail = (result.stderr || result.stdout || "").slice(-1e3);
|
|
844
|
+
throw new Error(
|
|
845
|
+
`hermes config set ${key} \u5931\u8D25\uFF08exit=${result.exitCode}\uFF09:
|
|
846
|
+
${detail}`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function upsertEnvVar(content, key, value) {
|
|
851
|
+
const escaped = value.replace(/(["\\])/g, "\\$1");
|
|
852
|
+
const line = `${key}="${escaped}"`;
|
|
853
|
+
const re = new RegExp(`^${key}=.*$`, "m");
|
|
854
|
+
if (re.test(content)) {
|
|
855
|
+
return content.replace(re, line);
|
|
856
|
+
}
|
|
857
|
+
const trailing = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
858
|
+
return content + trailing + line + "\n";
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/clients/registry.ts
|
|
862
|
+
var CLIENT_REGISTRY = {
|
|
863
|
+
claude: claudeClient,
|
|
864
|
+
codex: codexClient,
|
|
865
|
+
openclaw: openClawClient,
|
|
866
|
+
hermes: hermesClient
|
|
867
|
+
};
|
|
868
|
+
function listClients() {
|
|
869
|
+
return Object.values(CLIENT_REGISTRY);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/index.ts
|
|
324
873
|
async function main() {
|
|
325
874
|
const args = parseCliArgs();
|
|
326
875
|
if (args.help) {
|
|
@@ -332,15 +881,16 @@ async function main() {
|
|
|
332
881
|
return;
|
|
333
882
|
}
|
|
334
883
|
console.log();
|
|
335
|
-
intro(
|
|
884
|
+
intro(
|
|
885
|
+
pc6.bgCyan(pc6.black(" EasyRouter Config ")) + pc6.dim(" v" + VERSION)
|
|
886
|
+
);
|
|
336
887
|
const verbose = !!args.verbose;
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
const codexBaseUrl = selectedHost.replace(/\/+$/, "") + "/v1";
|
|
888
|
+
const interactive = !args.apiKey;
|
|
889
|
+
const host = await pickAvailableHost(verbose);
|
|
340
890
|
let apiKey;
|
|
341
891
|
if (args.apiKey) {
|
|
342
892
|
apiKey = normalizeApiKey(args.apiKey);
|
|
343
|
-
|
|
893
|
+
log3.info(`API Key: ${pc6.dim(maskKey(apiKey))}\uFF08\u6765\u81EA\u547D\u4EE4\u884C\u53C2\u6570\uFF09`);
|
|
344
894
|
} else {
|
|
345
895
|
const input = await text({
|
|
346
896
|
message: "\u8BF7\u8F93\u5165\u4F60\u7684 EasyRouter API Key",
|
|
@@ -351,134 +901,148 @@ async function main() {
|
|
|
351
901
|
return void 0;
|
|
352
902
|
}
|
|
353
903
|
});
|
|
354
|
-
if (
|
|
904
|
+
if (isCancel2(input)) {
|
|
355
905
|
cancel("\u5DF2\u53D6\u6D88");
|
|
356
906
|
process.exit(0);
|
|
357
907
|
}
|
|
358
908
|
apiKey = normalizeApiKey(input);
|
|
359
909
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
],
|
|
374
|
-
initialValue: "both"
|
|
375
|
-
});
|
|
376
|
-
if (isCancel(target)) {
|
|
377
|
-
cancel("\u5DF2\u53D6\u6D88");
|
|
378
|
-
process.exit(0);
|
|
379
|
-
}
|
|
380
|
-
if (target === "claude") configCodex = false;
|
|
381
|
-
else if (target === "codex") configClaude = false;
|
|
382
|
-
}
|
|
383
|
-
note(
|
|
384
|
-
[
|
|
385
|
-
`${pc.bold("\u8BA1\u8D39\u6A21\u5F0F")} \u6309\u91CF\u4ED8\u8D39`,
|
|
386
|
-
configClaude ? `${pc.bold("Claude URL")} ${PRIMARY_HOST}` : null,
|
|
387
|
-
configCodex ? `${pc.bold("Codex URL")} ${PRIMARY_HOST}/v1` : null,
|
|
388
|
-
`${pc.bold("API Key")} ${maskKey(apiKey)}`,
|
|
389
|
-
`${pc.bold("\u5C06\u914D\u7F6E")} ${[
|
|
390
|
-
configClaude && "Claude Code",
|
|
391
|
-
configCodex && "Codex"
|
|
392
|
-
].filter(Boolean).join(" + ")}`
|
|
393
|
-
].filter(Boolean).join("\n"),
|
|
394
|
-
"\u914D\u7F6E\u9884\u89C8"
|
|
395
|
-
);
|
|
396
|
-
if (!args.apiKey) {
|
|
910
|
+
const selected = await pickClients(args.only, interactive);
|
|
911
|
+
if (selected.length === 0) {
|
|
912
|
+
cancel("\u672A\u9009\u62E9\u4EFB\u4F55\u5BA2\u6237\u7AEF");
|
|
913
|
+
process.exit(0);
|
|
914
|
+
}
|
|
915
|
+
const detection = await detectAll(selected, verbose);
|
|
916
|
+
const missing = detection.filter((d) => !d.availability.installed);
|
|
917
|
+
if (missing.length > 0) {
|
|
918
|
+
printMissingHints(missing);
|
|
919
|
+
process.exit(1);
|
|
920
|
+
}
|
|
921
|
+
printSummary({ apiKey, selected });
|
|
922
|
+
if (interactive) {
|
|
397
923
|
const ok = await confirm({ message: "\u7EE7\u7EED\uFF1F", initialValue: true });
|
|
398
|
-
if (
|
|
924
|
+
if (isCancel2(ok) || !ok) {
|
|
399
925
|
cancel("\u5DF2\u53D6\u6D88");
|
|
400
926
|
process.exit(0);
|
|
401
927
|
}
|
|
402
928
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const s = useSpinner ? spinner() : null;
|
|
406
|
-
s?.start("\u914D\u7F6E Claude Code");
|
|
407
|
-
if (!s) log.info("\u6B63\u5728\u914D\u7F6E Claude Code\uFF08\u9996\u6B21\u8FD0\u884C\u9700\u4E0B\u8F7D zcf\uFF0C\u7EA6 30~60 \u79D2\uFF09...");
|
|
408
|
-
try {
|
|
409
|
-
await configureViaZcf({
|
|
410
|
-
target: "cc",
|
|
411
|
-
baseUrl: claudeBaseUrl,
|
|
412
|
-
apiKey,
|
|
413
|
-
verbose,
|
|
414
|
-
// 节流的进度回调:spinner 不会高频闪烁
|
|
415
|
-
onProgress: (line) => s?.message(`\u914D\u7F6E Claude Code \xB7 ${pc.dim(line)}`)
|
|
416
|
-
});
|
|
417
|
-
s?.stop(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.claude/settings.json"));
|
|
418
|
-
if (!s) log.success(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 \u2192 ~/.claude/settings.json"));
|
|
419
|
-
} catch (err) {
|
|
420
|
-
s?.stop(pc.red("\u2717 Claude Code \u914D\u7F6E\u5931\u8D25"));
|
|
421
|
-
console.error(err.message);
|
|
422
|
-
process.exit(1);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (configCodex) {
|
|
426
|
-
const codexModel = await resolveCodexModel(
|
|
427
|
-
selectedHost,
|
|
428
|
-
/* interactive */
|
|
429
|
-
!args.apiKey,
|
|
430
|
-
verbose
|
|
431
|
-
);
|
|
432
|
-
const useSpinner = !verbose && process.stdout.isTTY;
|
|
433
|
-
const s = useSpinner ? spinner() : null;
|
|
434
|
-
s?.start("\u914D\u7F6E Codex");
|
|
435
|
-
if (!s) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
|
|
436
|
-
try {
|
|
437
|
-
await configureViaZcf({
|
|
438
|
-
target: "codex",
|
|
439
|
-
baseUrl: codexBaseUrl,
|
|
440
|
-
apiKey,
|
|
441
|
-
apiModel: codexModel,
|
|
442
|
-
verbose,
|
|
443
|
-
onProgress: (line) => s?.message(`\u914D\u7F6E Codex \xB7 ${pc.dim(line)}`)
|
|
444
|
-
});
|
|
445
|
-
s?.stop(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 ") + pc.dim(`\u2192 model=${codexModel}`));
|
|
446
|
-
if (!s) log.success(pc.green(`\u2713 Codex \u914D\u7F6E\u5B8C\u6210 \u2192 model=${codexModel}`));
|
|
447
|
-
} catch (err) {
|
|
448
|
-
s?.stop(pc.red("\u2717 Codex \u914D\u7F6E\u5931\u8D25"));
|
|
449
|
-
console.error(err.message);
|
|
450
|
-
process.exit(1);
|
|
451
|
-
}
|
|
929
|
+
for (const client of selected) {
|
|
930
|
+
await configureOne(client, { host, apiKey, verbose });
|
|
452
931
|
}
|
|
453
932
|
if (!args.skipVerify) {
|
|
454
933
|
const useSpinner = process.stdout.isTTY;
|
|
455
934
|
const s = useSpinner ? spinner() : null;
|
|
456
935
|
s?.start("\u9A8C\u8BC1\u8FDE\u901A\u6027");
|
|
457
|
-
if (!s)
|
|
458
|
-
const result = await verifyConnection(
|
|
936
|
+
if (!s) log3.info("\u6B63\u5728\u9A8C\u8BC1\u8FDE\u901A\u6027...");
|
|
937
|
+
const result = await verifyConnection(host, apiKey);
|
|
459
938
|
if (result.ok) {
|
|
460
|
-
const msg =
|
|
461
|
-
s?.stop(msg) ??
|
|
939
|
+
const msg = pc6.green("\u2713 \u94FE\u8DEF\u9A8C\u8BC1\u901A\u8FC7");
|
|
940
|
+
s?.stop(msg) ?? log3.success(msg);
|
|
462
941
|
} else {
|
|
463
|
-
const msg =
|
|
942
|
+
const msg = pc6.yellow(
|
|
464
943
|
`\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`
|
|
465
944
|
);
|
|
466
|
-
s?.stop(msg) ??
|
|
945
|
+
s?.stop(msg) ?? log3.warn(msg);
|
|
467
946
|
}
|
|
468
947
|
}
|
|
469
|
-
const tips =
|
|
470
|
-
|
|
471
|
-
|
|
948
|
+
const tips = selected.map(
|
|
949
|
+
(c) => pc6.cyan(c.launchCommand) + pc6.dim(` # \u542F\u52A8 ${c.label}`)
|
|
950
|
+
);
|
|
472
951
|
note(tips.join("\n"), "\u{1F389} \u5168\u90E8\u5B8C\u6210\uFF01\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
|
|
473
952
|
outro(
|
|
474
|
-
|
|
953
|
+
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")
|
|
475
954
|
);
|
|
476
955
|
}
|
|
956
|
+
async function pickClients(only, interactive) {
|
|
957
|
+
if (only && only.length) {
|
|
958
|
+
return only.map((id) => CLIENT_REGISTRY[id]);
|
|
959
|
+
}
|
|
960
|
+
if (!interactive) {
|
|
961
|
+
return [CLIENT_REGISTRY.claude, CLIENT_REGISTRY.codex];
|
|
962
|
+
}
|
|
963
|
+
const all = listClients();
|
|
964
|
+
const picked = await multiselect({
|
|
965
|
+
message: "\u8BF7\u9009\u62E9\u8981\u914D\u7F6E\u7684\u5BA2\u6237\u7AEF\uFF08\u7A7A\u683C\u5207\u6362\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09",
|
|
966
|
+
options: all.map((c) => ({
|
|
967
|
+
value: c.id,
|
|
968
|
+
label: c.label,
|
|
969
|
+
hint: c.hint
|
|
970
|
+
})),
|
|
971
|
+
initialValues: [],
|
|
972
|
+
required: false
|
|
973
|
+
});
|
|
974
|
+
if (isCancel2(picked)) {
|
|
975
|
+
cancel("\u5DF2\u53D6\u6D88");
|
|
976
|
+
process.exit(0);
|
|
977
|
+
}
|
|
978
|
+
return picked.map((id) => CLIENT_REGISTRY[id]);
|
|
979
|
+
}
|
|
980
|
+
async function detectAll(clients, verbose) {
|
|
981
|
+
const results = [];
|
|
982
|
+
for (const c of clients) {
|
|
983
|
+
const availability = await c.detect();
|
|
984
|
+
results.push({ client: c, availability });
|
|
985
|
+
if (verbose) {
|
|
986
|
+
if (availability.installed) {
|
|
987
|
+
log3.info(
|
|
988
|
+
`\u2713 ${c.label}${availability.version ? ` v${availability.version}` : ""} \u5DF2\u5B89\u88C5`
|
|
989
|
+
);
|
|
990
|
+
} else {
|
|
991
|
+
log3.warn(`\u2717 ${c.label} \u672A\u68C0\u6D4B\u5230`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return results;
|
|
996
|
+
}
|
|
997
|
+
function printMissingHints(missing) {
|
|
998
|
+
console.error();
|
|
999
|
+
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"));
|
|
1000
|
+
console.error();
|
|
1001
|
+
for (const { client, availability } of missing) {
|
|
1002
|
+
const hint = availability.installHint || "\u8BF7\u67E5\u770B\u5B98\u65B9\u6587\u6863";
|
|
1003
|
+
console.error(` ${pc6.bold(client.label)}\uFF1A`);
|
|
1004
|
+
hint.split("\n").forEach((line) => {
|
|
1005
|
+
console.error(` ${pc6.dim(line)}`);
|
|
1006
|
+
});
|
|
1007
|
+
console.error();
|
|
1008
|
+
}
|
|
1009
|
+
console.error(pc6.dim("\u5B89\u88C5\u5B8C\u6210\u540E\u91CD\u65B0\u8FD0\u884C\u672C\u547D\u4EE4\u5373\u53EF\u7EE7\u7EED\u914D\u7F6E\u3002"));
|
|
1010
|
+
}
|
|
1011
|
+
function printSummary(opts) {
|
|
1012
|
+
const lines = [
|
|
1013
|
+
`${pc6.bold("\u8BA1\u8D39\u6A21\u5F0F")} \u6309\u91CF\u4ED8\u8D39`,
|
|
1014
|
+
`${pc6.bold("BaseURL")} ${PRIMARY_HOST}`,
|
|
1015
|
+
`${pc6.bold("API Key")} ${maskKey(opts.apiKey)}`,
|
|
1016
|
+
`${pc6.bold("\u5C06\u914D\u7F6E")} ${opts.selected.map((c) => c.label).join(" + ")}`
|
|
1017
|
+
];
|
|
1018
|
+
note(lines.join("\n"), "\u914D\u7F6E\u9884\u89C8");
|
|
1019
|
+
}
|
|
1020
|
+
async function configureOne(client, ctx) {
|
|
1021
|
+
const useSpinner = !ctx.verbose && process.stdout.isTTY;
|
|
1022
|
+
const s = useSpinner ? spinner() : null;
|
|
1023
|
+
s?.start(`\u914D\u7F6E ${client.label}`);
|
|
1024
|
+
if (!s) log3.info(`\u6B63\u5728\u914D\u7F6E ${client.label}...`);
|
|
1025
|
+
try {
|
|
1026
|
+
const result = await client.configure({
|
|
1027
|
+
host: ctx.host,
|
|
1028
|
+
apiKey: ctx.apiKey,
|
|
1029
|
+
verbose: ctx.verbose,
|
|
1030
|
+
onProgress: (line) => s?.message(`\u914D\u7F6E ${client.label} \xB7 ${pc6.dim(line)}`)
|
|
1031
|
+
});
|
|
1032
|
+
const summary = `\u2192 ${result.configPaths.join(" + ")}` + (result.model ? ` model=${result.model}` : "");
|
|
1033
|
+
s?.stop(pc6.green(`\u2713 ${client.label} \u914D\u7F6E\u5B8C\u6210 `) + pc6.dim(summary));
|
|
1034
|
+
if (!s) log3.success(pc6.green(`\u2713 ${client.label} \u914D\u7F6E\u5B8C\u6210 ${summary}`));
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
s?.stop(pc6.red(`\u2717 ${client.label} \u914D\u7F6E\u5931\u8D25`));
|
|
1037
|
+
console.error(err.message);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
477
1041
|
main().catch((err) => {
|
|
478
1042
|
console.error();
|
|
479
|
-
console.error(
|
|
1043
|
+
console.error(pc6.red("\u2717 \u81F4\u547D\u9519\u8BEF\uFF1A"), err?.message || err);
|
|
480
1044
|
if (err?.stack && process.env.DEBUG) {
|
|
481
|
-
console.error(
|
|
1045
|
+
console.error(pc6.dim(err.stack));
|
|
482
1046
|
}
|
|
483
1047
|
process.exit(1);
|
|
484
1048
|
});
|