march-control-cli 0.1.3
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/README.md +220 -0
- package/core/apply.js +152 -0
- package/core/backup.js +53 -0
- package/core/constants.js +55 -0
- package/core/desktop-service.js +219 -0
- package/core/desktop-state.js +511 -0
- package/core/index.js +1293 -0
- package/core/paths.js +71 -0
- package/core/presets.js +171 -0
- package/core/probe.js +70 -0
- package/core/store.js +218 -0
- package/core/utils.js +178 -0
- package/core/writers/codex.js +102 -0
- package/core/writers/index.js +16 -0
- package/core/writers/openclaw.js +93 -0
- package/core/writers/opencode.js +91 -0
- package/desktop/assets/march-mark.svg +21 -0
- package/desktop/main.js +192 -0
- package/desktop/preload.js +49 -0
- package/desktop/renderer/app.js +327 -0
- package/desktop/renderer/index.html +130 -0
- package/desktop/renderer/styles.css +413 -0
- package/package.json +106 -0
- package/scripts/desktop-dev.mjs +90 -0
- package/scripts/postinstall.mjs +28 -0
- package/scripts/serve-site.mjs +51 -0
- package/site/app.js +10 -0
- package/site/assets/march-mark.svg +22 -0
- package/site/index.html +286 -0
- package/site/styles.css +566 -0
- package/src/App.tsx +1186 -0
- package/src/components/layout/app-sidebar.tsx +103 -0
- package/src/components/layout/top-toolbar.tsx +44 -0
- package/src/components/layout/workspace-tabs.tsx +32 -0
- package/src/components/providers/inspector-panel.tsx +84 -0
- package/src/components/providers/metric-strip.tsx +26 -0
- package/src/components/providers/provider-editor.tsx +87 -0
- package/src/components/providers/provider-table.tsx +85 -0
- package/src/components/ui/logo-mark.tsx +16 -0
- package/src/features/mcp/mcp-view.tsx +45 -0
- package/src/features/prompts/prompts-view.tsx +40 -0
- package/src/features/providers/providers-view.tsx +40 -0
- package/src/features/providers/types.ts +8 -0
- package/src/features/skills/skills-view.tsx +44 -0
- package/src/hooks/use-control-workspace.ts +184 -0
- package/src/index.css +22 -0
- package/src/lib/client.ts +944 -0
- package/src/lib/query-client.ts +3 -0
- package/src/lib/workspace-sections.ts +34 -0
- package/src/main.tsx +14 -0
- package/src/types.ts +76 -0
- package/src/vite-env.d.ts +56 -0
- package/src-tauri/README.md +11 -0
package/core/index.js
ADDED
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import {
|
|
6
|
+
applyClonedProvider,
|
|
7
|
+
applyEditedProvider,
|
|
8
|
+
applyProvider,
|
|
9
|
+
applyRemovedProvider,
|
|
10
|
+
applyStoredProvider
|
|
11
|
+
} from "./apply.js";
|
|
12
|
+
import {
|
|
13
|
+
APP_NAME,
|
|
14
|
+
DEFAULT_BASE_URL,
|
|
15
|
+
DEFAULT_CANDIDATE_BASE_URLS,
|
|
16
|
+
DEFAULT_OPENCLAW_BASE_URL,
|
|
17
|
+
DEFAULT_PLATFORM_SELECTION,
|
|
18
|
+
DEFAULT_PRIMARY_MODEL,
|
|
19
|
+
DEFAULT_PROVIDER_NAME,
|
|
20
|
+
PLATFORM_META,
|
|
21
|
+
SUPPORTED_PLATFORMS
|
|
22
|
+
} from "./constants.js";
|
|
23
|
+
import { getCurrentProvider, listProviders } from "./store.js";
|
|
24
|
+
import {
|
|
25
|
+
buildOpenClawBaseUrl,
|
|
26
|
+
dedupeStrings,
|
|
27
|
+
formatLatency,
|
|
28
|
+
maskApiKey,
|
|
29
|
+
normalizeBaseUrl,
|
|
30
|
+
parseCommaList
|
|
31
|
+
} from "./utils.js";
|
|
32
|
+
import { getBestProbeResult, probeBaseUrls } from "./probe.js";
|
|
33
|
+
import { getPreset, listPresets, removePreset, upsertPreset } from "./presets.js";
|
|
34
|
+
|
|
35
|
+
function collect(value, previous) {
|
|
36
|
+
previous.push(value);
|
|
37
|
+
return previous;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printBrandBanner() {
|
|
41
|
+
const logo = [
|
|
42
|
+
" __ __ _ ____ ____ _ _ ",
|
|
43
|
+
"| \\/ | / \\ | _ \\ / ___| | | |",
|
|
44
|
+
"| |\\/| | / _ \\ | |_) | | | |_| |",
|
|
45
|
+
"| | | |/ ___ \\| _ <| |___| _ |",
|
|
46
|
+
"|_| |_/_/ \\_\\_| \\_\\\\____|_| |_|"
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log(chalk.gray("┌────────────────────────────────────────────────────────────┐"));
|
|
51
|
+
for (const line of logo) {
|
|
52
|
+
console.log(chalk.cyanBright(`│ ${line.padEnd(58, " ")}│`));
|
|
53
|
+
}
|
|
54
|
+
console.log(chalk.yellow("│ March 配置管理工具 │"));
|
|
55
|
+
console.log(chalk.gray("│ Codex / OpenCode / OpenClaw 一键管理 │"));
|
|
56
|
+
console.log(chalk.gray("└────────────────────────────────────────────────────────────┘"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printSuccessBanner() {
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(chalk.black.bgGreenBright(" 配置完成 "));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printHeader(title) {
|
|
65
|
+
console.log(chalk.cyan(`\n${title}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printSetupStep(step, title, detail = "") {
|
|
69
|
+
console.log(chalk.cyan(`\n[${step}/4] ${title}`));
|
|
70
|
+
if (detail) {
|
|
71
|
+
console.log(chalk.gray(detail));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getPlatformLabelList(platforms) {
|
|
76
|
+
return platforms.map((platform) => PLATFORM_META[platform].label).join(" / ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printSetupPlan({
|
|
80
|
+
modeLabel,
|
|
81
|
+
presetName,
|
|
82
|
+
providerName,
|
|
83
|
+
model,
|
|
84
|
+
platforms,
|
|
85
|
+
commonBaseUrl,
|
|
86
|
+
openclawBaseUrl,
|
|
87
|
+
probeEnabled,
|
|
88
|
+
backupEnabled
|
|
89
|
+
}) {
|
|
90
|
+
printHeader("安装摘要");
|
|
91
|
+
console.log(`模式: ${modeLabel}`);
|
|
92
|
+
console.log(`平台: ${getPlatformLabelList(platforms)}`);
|
|
93
|
+
console.log(`配置名称: ${providerName}`);
|
|
94
|
+
console.log(`模型: ${model || DEFAULT_PRIMARY_MODEL}`);
|
|
95
|
+
console.log(`Codex/OpenCode 地址: ${commonBaseUrl}`);
|
|
96
|
+
if (platforms.includes("openclaw")) {
|
|
97
|
+
console.log(`OpenClaw 地址: ${openclawBaseUrl}`);
|
|
98
|
+
}
|
|
99
|
+
console.log(`测速: ${probeEnabled ? "开启" : "关闭"}`);
|
|
100
|
+
console.log(`备份: ${backupEnabled ? "开启" : "关闭"}`);
|
|
101
|
+
if (presetName) {
|
|
102
|
+
console.log(`预设: ${presetName}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function printNextActions() {
|
|
107
|
+
printHeader("下一步建议");
|
|
108
|
+
console.log("1) march menu");
|
|
109
|
+
console.log("2) march cx list");
|
|
110
|
+
console.log("3) march preset list");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printProviderSummary(platform, provider) {
|
|
114
|
+
const meta = PLATFORM_META[platform];
|
|
115
|
+
console.log(chalk.green(`\n${meta.label} 当前启用配置`));
|
|
116
|
+
console.log(`名称: ${provider.name}`);
|
|
117
|
+
console.log(`ID: ${provider.id}`);
|
|
118
|
+
console.log(`地址: ${provider.baseUrl}`);
|
|
119
|
+
console.log(`模型: ${provider.model || DEFAULT_PRIMARY_MODEL}`);
|
|
120
|
+
console.log(`API Key: ${maskApiKey(provider.apiKey)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printProviderList(platform) {
|
|
124
|
+
const meta = PLATFORM_META[platform];
|
|
125
|
+
const providers = listProviders(platform);
|
|
126
|
+
const current = getCurrentProvider(platform);
|
|
127
|
+
|
|
128
|
+
printHeader(`${meta.label} 已保存配置`);
|
|
129
|
+
|
|
130
|
+
if (providers.length === 0) {
|
|
131
|
+
console.log(chalk.gray("还没有保存任何配置。"));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const provider of providers) {
|
|
136
|
+
const marker = current?.id === provider.id ? chalk.green("[当前]") : "";
|
|
137
|
+
console.log(`${provider.name} ${marker}`.trim());
|
|
138
|
+
console.log(` ${provider.baseUrl}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printPresetList() {
|
|
143
|
+
const presets = listPresets();
|
|
144
|
+
printHeader("可用预设");
|
|
145
|
+
|
|
146
|
+
if (presets.length === 0) {
|
|
147
|
+
console.log(chalk.gray("暂无预设。"));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const preset of presets) {
|
|
152
|
+
const sourceLabel = preset.readonly ? chalk.gray("内置") : chalk.green("自定义");
|
|
153
|
+
console.log(`${preset.name} (${sourceLabel})`);
|
|
154
|
+
console.log(` 提供方: ${preset.providerName}`);
|
|
155
|
+
console.log(` 通用地址: ${preset.commonBaseUrl}`);
|
|
156
|
+
console.log(` OpenClaw 地址: ${preset.openclawBaseUrl}`);
|
|
157
|
+
console.log(` 模型: ${preset.model || DEFAULT_PRIMARY_MODEL}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function printPresetSummary(title, preset) {
|
|
162
|
+
printHeader(title);
|
|
163
|
+
console.log(`名称: ${preset.name}`);
|
|
164
|
+
console.log(`提供方: ${preset.providerName}`);
|
|
165
|
+
console.log(`通用地址: ${preset.commonBaseUrl}`);
|
|
166
|
+
console.log(`OpenClaw 地址: ${preset.openclawBaseUrl}`);
|
|
167
|
+
console.log(`模型: ${preset.model || DEFAULT_PRIMARY_MODEL}`);
|
|
168
|
+
console.log(`来源: ${preset.source || (preset.readonly ? "内置" : "自定义")}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function printProbeResults(label, results) {
|
|
172
|
+
printHeader(`${label} 地址测速结果`);
|
|
173
|
+
|
|
174
|
+
for (const result of results) {
|
|
175
|
+
const status = result.ok ? chalk.green("可用") : chalk.red("失败");
|
|
176
|
+
console.log(`${status} ${result.baseUrl} (${formatLatency(result)})`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parsePlatforms(platformsArg) {
|
|
181
|
+
if (!platformsArg) {
|
|
182
|
+
return [...DEFAULT_PLATFORM_SELECTION];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const values = parseCommaList(platformsArg).map((value) => value.toLowerCase());
|
|
186
|
+
const aliasMap = {
|
|
187
|
+
cx: "codex",
|
|
188
|
+
oc: "opencode",
|
|
189
|
+
ow: "openclaw"
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const resolved = dedupeStrings(
|
|
193
|
+
values.map((value) => aliasMap[value] || value).filter((value) => SUPPORTED_PLATFORMS.includes(value))
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (resolved.length === 0) {
|
|
197
|
+
throw new Error(`不支持的平台参数: ${platformsArg}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function resolveBaseUrl(label, rawOptions) {
|
|
204
|
+
const shouldProbe = rawOptions.probe !== false;
|
|
205
|
+
const confirmChoice = rawOptions.confirmChoice === true;
|
|
206
|
+
const candidates = dedupeStrings(rawOptions.candidates || []);
|
|
207
|
+
|
|
208
|
+
if (candidates.length === 0) {
|
|
209
|
+
throw new Error(`没有可用候选地址: ${label}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!shouldProbe) {
|
|
213
|
+
return normalizeBaseUrl(candidates[0]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const results = await probeBaseUrls(candidates, { timeoutMs: 5000 });
|
|
217
|
+
printProbeResults(label, results);
|
|
218
|
+
const best = getBestProbeResult(results);
|
|
219
|
+
|
|
220
|
+
if (!best) {
|
|
221
|
+
throw new Error(`无法确定可用地址: ${label}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!rawOptions.interactive || results.length === 1 || !confirmChoice) {
|
|
225
|
+
return normalizeBaseUrl(best.baseUrl);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const answer = await inquirer.prompt([
|
|
229
|
+
{
|
|
230
|
+
type: "list",
|
|
231
|
+
name: "baseUrl",
|
|
232
|
+
message: `请选择 ${label} 使用地址`,
|
|
233
|
+
choices: results.map((result) => ({
|
|
234
|
+
name: `${result.baseUrl} (${formatLatency(result)})`,
|
|
235
|
+
value: result.baseUrl
|
|
236
|
+
})),
|
|
237
|
+
default: best.baseUrl
|
|
238
|
+
}
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
return normalizeBaseUrl(answer.baseUrl);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function promptForApiKey(initialValue) {
|
|
245
|
+
if (initialValue?.trim()) {
|
|
246
|
+
return initialValue.trim();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const answers = await inquirer.prompt([
|
|
250
|
+
{
|
|
251
|
+
type: "password",
|
|
252
|
+
name: "apiKey",
|
|
253
|
+
message: "请输入 API Key",
|
|
254
|
+
mask: "*",
|
|
255
|
+
validate(value) {
|
|
256
|
+
return value.trim() ? true : "API Key 不能为空";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
return answers.apiKey.trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function promptForSetupDefaults(rawOptions) {
|
|
265
|
+
if (!rawOptions.interactive || rawOptions.advanced !== true) {
|
|
266
|
+
return {
|
|
267
|
+
providerName: rawOptions.providerName || DEFAULT_PROVIDER_NAME,
|
|
268
|
+
commonBaseUrl: rawOptions.baseUrl || DEFAULT_BASE_URL,
|
|
269
|
+
openclawBaseUrl: rawOptions.openclawBaseUrl || buildOpenClawBaseUrl(rawOptions.baseUrl || DEFAULT_BASE_URL)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const answers = await inquirer.prompt([
|
|
274
|
+
{
|
|
275
|
+
type: "input",
|
|
276
|
+
name: "providerName",
|
|
277
|
+
message: "配置名称",
|
|
278
|
+
default: rawOptions.providerName || DEFAULT_PROVIDER_NAME
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
type: "input",
|
|
282
|
+
name: "commonBaseUrl",
|
|
283
|
+
message: "Codex / OpenCode 地址",
|
|
284
|
+
default: rawOptions.baseUrl || DEFAULT_BASE_URL
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: "input",
|
|
288
|
+
name: "openclawBaseUrl",
|
|
289
|
+
message: "OpenClaw 地址",
|
|
290
|
+
default:
|
|
291
|
+
rawOptions.openclawBaseUrl ||
|
|
292
|
+
buildOpenClawBaseUrl(rawOptions.baseUrl || DEFAULT_BASE_URL)
|
|
293
|
+
}
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
providerName: answers.providerName.trim(),
|
|
298
|
+
commonBaseUrl: answers.commonBaseUrl.trim(),
|
|
299
|
+
openclawBaseUrl: answers.openclawBaseUrl.trim()
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function applyPresetToSetupOptions(rawOptions = {}) {
|
|
303
|
+
const presetName = `${rawOptions.preset || ""}`.trim();
|
|
304
|
+
if (!presetName) {
|
|
305
|
+
return {
|
|
306
|
+
...rawOptions,
|
|
307
|
+
_resolvedPreset: null
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const preset = getPreset(presetName);
|
|
312
|
+
if (!preset) {
|
|
313
|
+
throw new Error(`未找到预设: ${presetName}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const commonBaseUrl = rawOptions.baseUrl || preset.commonBaseUrl;
|
|
317
|
+
return {
|
|
318
|
+
...rawOptions,
|
|
319
|
+
providerName: rawOptions.providerName || preset.providerName || preset.name || DEFAULT_PROVIDER_NAME,
|
|
320
|
+
baseUrl: commonBaseUrl,
|
|
321
|
+
openclawBaseUrl:
|
|
322
|
+
rawOptions.openclawBaseUrl || preset.openclawBaseUrl || buildOpenClawBaseUrl(commonBaseUrl),
|
|
323
|
+
model: rawOptions.model || preset.model || DEFAULT_PRIMARY_MODEL,
|
|
324
|
+
_resolvedPreset: preset
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function runSetup(rawOptions = {}) {
|
|
329
|
+
const options = applyPresetToSetupOptions(rawOptions);
|
|
330
|
+
const interactive = options.interactive !== false;
|
|
331
|
+
const advanced = options.advanced === true;
|
|
332
|
+
const modeLabel = interactive ? (advanced ? "交互-自定义" : "交互-快速") : "命令行";
|
|
333
|
+
|
|
334
|
+
if (interactive) {
|
|
335
|
+
printBrandBanner();
|
|
336
|
+
printHeader(advanced ? "自定义安装" : "快速安装");
|
|
337
|
+
if (!advanced) {
|
|
338
|
+
console.log(chalk.gray("默认使用预设并自动测速选择最佳地址。"));
|
|
339
|
+
}
|
|
340
|
+
if (options._resolvedPreset) {
|
|
341
|
+
console.log(chalk.gray(`使用预设: ${options._resolvedPreset.name}`));
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
printHeader("March 命令行安装");
|
|
345
|
+
if (options._resolvedPreset) {
|
|
346
|
+
console.log(chalk.gray(`使用预设: ${options._resolvedPreset.name}`));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
printSetupStep(1, "收集安装参数");
|
|
350
|
+
|
|
351
|
+
const providerMeta = await promptForSetupDefaults({
|
|
352
|
+
...options,
|
|
353
|
+
interactive,
|
|
354
|
+
advanced
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const platforms = interactive && !options.platforms
|
|
358
|
+
? (
|
|
359
|
+
await inquirer.prompt([
|
|
360
|
+
{
|
|
361
|
+
type: "checkbox",
|
|
362
|
+
name: "platforms",
|
|
363
|
+
message: "请选择要安装的平台",
|
|
364
|
+
choices: SUPPORTED_PLATFORMS.map((platform) => ({
|
|
365
|
+
name: `${PLATFORM_META[platform].label}${platform === "codex" ? "(推荐)" : ""}`,
|
|
366
|
+
value: platform,
|
|
367
|
+
checked: true
|
|
368
|
+
})),
|
|
369
|
+
validate(value) {
|
|
370
|
+
return value.length > 0 ? true : "请至少选择一个平台";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
])
|
|
374
|
+
).platforms
|
|
375
|
+
: parsePlatforms(options.platforms);
|
|
376
|
+
|
|
377
|
+
const apiKey = await promptForApiKey(options.apiKey);
|
|
378
|
+
printSetupStep(
|
|
379
|
+
2,
|
|
380
|
+
"检测可用地址",
|
|
381
|
+
options.probe === false ? "已跳过测速,将使用提供的首个地址。" : "正在测速并选择最佳可用地址。"
|
|
382
|
+
);
|
|
383
|
+
const commonBaseUrl = await resolveBaseUrl("Codex/OpenCode", {
|
|
384
|
+
interactive,
|
|
385
|
+
confirmChoice: advanced,
|
|
386
|
+
probe: options.probe,
|
|
387
|
+
candidates: [providerMeta.commonBaseUrl, ...DEFAULT_CANDIDATE_BASE_URLS, ...(options.candidateBaseUrls || [])]
|
|
388
|
+
});
|
|
389
|
+
const openclawBaseUrl = platforms.includes("openclaw")
|
|
390
|
+
? await resolveBaseUrl("OpenClaw", {
|
|
391
|
+
interactive,
|
|
392
|
+
confirmChoice: advanced,
|
|
393
|
+
probe: options.probe,
|
|
394
|
+
candidates: [
|
|
395
|
+
providerMeta.openclawBaseUrl || buildOpenClawBaseUrl(commonBaseUrl),
|
|
396
|
+
...(options.openclawCandidateBaseUrls || [])
|
|
397
|
+
]
|
|
398
|
+
})
|
|
399
|
+
: null;
|
|
400
|
+
|
|
401
|
+
const providerModel = options.model || DEFAULT_PRIMARY_MODEL;
|
|
402
|
+
const results = [];
|
|
403
|
+
const backupEnabled = options.backup !== false;
|
|
404
|
+
|
|
405
|
+
printSetupPlan({
|
|
406
|
+
modeLabel,
|
|
407
|
+
presetName: options._resolvedPreset?.name || "",
|
|
408
|
+
providerName: providerMeta.providerName,
|
|
409
|
+
model: providerModel,
|
|
410
|
+
platforms,
|
|
411
|
+
commonBaseUrl,
|
|
412
|
+
openclawBaseUrl: openclawBaseUrl || "",
|
|
413
|
+
probeEnabled: options.probe !== false,
|
|
414
|
+
backupEnabled
|
|
415
|
+
});
|
|
416
|
+
printSetupStep(3, "写入配置文件");
|
|
417
|
+
|
|
418
|
+
for (const platform of platforms) {
|
|
419
|
+
const baseUrl = platform === "openclaw" ? openclawBaseUrl : commonBaseUrl;
|
|
420
|
+
console.log(chalk.gray(`→ 正在配置 ${PLATFORM_META[platform].label} ...`));
|
|
421
|
+
const result = applyProvider(
|
|
422
|
+
platform,
|
|
423
|
+
{
|
|
424
|
+
name: providerMeta.providerName,
|
|
425
|
+
baseUrl,
|
|
426
|
+
apiKey,
|
|
427
|
+
model: providerModel
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
overwrite: options.overwrite,
|
|
431
|
+
backup: backupEnabled,
|
|
432
|
+
activate: true
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
results.push({
|
|
437
|
+
platform,
|
|
438
|
+
...result
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
printSetupStep(4, "安装完成");
|
|
443
|
+
printSuccessBanner();
|
|
444
|
+
printHeader("安装完成");
|
|
445
|
+
for (const item of results) {
|
|
446
|
+
console.log(chalk.green(`[OK] ${PLATFORM_META[item.platform].label} 已完成配置`));
|
|
447
|
+
console.log(` 地址: ${item.provider.baseUrl}`);
|
|
448
|
+
console.log(` 备份: ${item.backupDir || "(disabled)"}`);
|
|
449
|
+
}
|
|
450
|
+
printNextActions();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function runAdd(platform, rawOptions = {}) {
|
|
454
|
+
const interactive = rawOptions.interactive !== false;
|
|
455
|
+
const meta = PLATFORM_META[platform];
|
|
456
|
+
|
|
457
|
+
const answers = interactive
|
|
458
|
+
? await inquirer.prompt([
|
|
459
|
+
{
|
|
460
|
+
type: "input",
|
|
461
|
+
name: "providerName",
|
|
462
|
+
message: `${meta.label} 配置名称`,
|
|
463
|
+
default: rawOptions.providerName || DEFAULT_PROVIDER_NAME
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
type: "input",
|
|
467
|
+
name: "baseUrl",
|
|
468
|
+
message: `${meta.label} 地址`,
|
|
469
|
+
default: rawOptions.baseUrl || meta.defaultBaseUrl
|
|
470
|
+
}
|
|
471
|
+
])
|
|
472
|
+
: {
|
|
473
|
+
providerName: rawOptions.providerName || DEFAULT_PROVIDER_NAME,
|
|
474
|
+
baseUrl: rawOptions.baseUrl || meta.defaultBaseUrl
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const apiKey = await promptForApiKey(rawOptions.apiKey);
|
|
478
|
+
const baseUrl = await resolveBaseUrl(meta.label, {
|
|
479
|
+
interactive,
|
|
480
|
+
confirmChoice: interactive,
|
|
481
|
+
probe: rawOptions.probe,
|
|
482
|
+
candidates: [answers.baseUrl, ...(rawOptions.candidateBaseUrls || [])]
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const result = applyProvider(
|
|
486
|
+
platform,
|
|
487
|
+
{
|
|
488
|
+
name: answers.providerName.trim(),
|
|
489
|
+
baseUrl,
|
|
490
|
+
apiKey,
|
|
491
|
+
model: rawOptions.model || DEFAULT_PRIMARY_MODEL
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
overwrite: rawOptions.overwrite,
|
|
495
|
+
backup: rawOptions.backup !== false,
|
|
496
|
+
activate: true
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
printProviderSummary(platform, result.provider);
|
|
501
|
+
console.log(`备份: ${result.backupDir || "(disabled)"}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function selectProviderId(platform, message) {
|
|
505
|
+
const providers = listProviders(platform);
|
|
506
|
+
if (providers.length === 0) {
|
|
507
|
+
throw new Error("没有找到已保存的配置");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const answer = await inquirer.prompt([
|
|
511
|
+
{
|
|
512
|
+
type: "list",
|
|
513
|
+
name: "providerId",
|
|
514
|
+
message,
|
|
515
|
+
choices: providers.map((provider) => ({
|
|
516
|
+
name: `${provider.name} (${provider.baseUrl})`,
|
|
517
|
+
value: provider.id
|
|
518
|
+
}))
|
|
519
|
+
}
|
|
520
|
+
]);
|
|
521
|
+
|
|
522
|
+
return answer.providerId;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function resolveProviderByIdOrName(platform, nameOrId) {
|
|
526
|
+
const providers = listProviders(platform);
|
|
527
|
+
const lowered = `${nameOrId || ""}`.trim().toLowerCase();
|
|
528
|
+
return (
|
|
529
|
+
providers.find((provider) => provider.id === nameOrId) ||
|
|
530
|
+
providers.find((provider) => provider.name.trim().toLowerCase() === lowered) ||
|
|
531
|
+
null
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function runEdit(platform, nameOrId, rawOptions = {}) {
|
|
536
|
+
const interactive = rawOptions.interactive !== false;
|
|
537
|
+
const targetNameOrId =
|
|
538
|
+
nameOrId || (interactive ? await selectProviderId(platform, "请选择要编辑的配置") : null);
|
|
539
|
+
|
|
540
|
+
if (!targetNameOrId) {
|
|
541
|
+
throw new Error("缺少配置名称或 ID");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const target = resolveProviderByIdOrName(platform, targetNameOrId);
|
|
545
|
+
if (!target) {
|
|
546
|
+
throw new Error(`未找到配置: ${targetNameOrId}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const meta = PLATFORM_META[platform];
|
|
550
|
+
const answers = interactive
|
|
551
|
+
? await inquirer.prompt([
|
|
552
|
+
{
|
|
553
|
+
type: "input",
|
|
554
|
+
name: "providerName",
|
|
555
|
+
message: `${meta.label} 配置名称`,
|
|
556
|
+
default: rawOptions.providerName || target.name
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
type: "input",
|
|
560
|
+
name: "baseUrl",
|
|
561
|
+
message: `${meta.label} 地址`,
|
|
562
|
+
default: rawOptions.baseUrl || target.baseUrl
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
type: "input",
|
|
566
|
+
name: "model",
|
|
567
|
+
message: `${meta.label} 模型`,
|
|
568
|
+
default: rawOptions.model || target.model || DEFAULT_PRIMARY_MODEL
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
type: "password",
|
|
572
|
+
name: "apiKey",
|
|
573
|
+
message: "API Key(留空则保持不变)",
|
|
574
|
+
mask: "*"
|
|
575
|
+
}
|
|
576
|
+
])
|
|
577
|
+
: {
|
|
578
|
+
providerName: rawOptions.providerName || target.name,
|
|
579
|
+
baseUrl: rawOptions.baseUrl || target.baseUrl,
|
|
580
|
+
model: rawOptions.model || target.model || DEFAULT_PRIMARY_MODEL,
|
|
581
|
+
apiKey: rawOptions.apiKey || ""
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const resolvedBaseUrl = await resolveBaseUrl(meta.label, {
|
|
585
|
+
interactive: false,
|
|
586
|
+
confirmChoice: false,
|
|
587
|
+
probe: rawOptions.probe,
|
|
588
|
+
candidates: [answers.baseUrl || target.baseUrl, ...(rawOptions.candidateBaseUrls || [])]
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const updates = {
|
|
592
|
+
name: `${answers.providerName || target.name}`.trim(),
|
|
593
|
+
baseUrl: resolvedBaseUrl,
|
|
594
|
+
model: `${answers.model || target.model || DEFAULT_PRIMARY_MODEL}`.trim() || DEFAULT_PRIMARY_MODEL
|
|
595
|
+
};
|
|
596
|
+
if (`${answers.apiKey || ""}`.trim()) {
|
|
597
|
+
updates.apiKey = `${answers.apiKey}`.trim();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const result = applyEditedProvider(platform, targetNameOrId, updates, {
|
|
601
|
+
backup: rawOptions.backup !== false,
|
|
602
|
+
overwrite: rawOptions.overwrite === true,
|
|
603
|
+
activate: rawOptions.activate === true
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
printProviderSummary(platform, result.provider);
|
|
607
|
+
console.log(`当前启用: ${result.activeProvider ? result.activeProvider.name : "无"}`);
|
|
608
|
+
console.log(`备份: ${result.backupDir || "(disabled)"}`);
|
|
609
|
+
}
|
|
610
|
+
async function runClone(platform, sourceNameOrId, rawOptions = {}) {
|
|
611
|
+
const interactive = rawOptions.interactive !== false;
|
|
612
|
+
const sourceId =
|
|
613
|
+
sourceNameOrId || (interactive ? await selectProviderId(platform, "请选择要克隆的配置") : null);
|
|
614
|
+
|
|
615
|
+
if (!sourceId) {
|
|
616
|
+
throw new Error("缺少来源配置名称或 ID");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const source = resolveProviderByIdOrName(platform, sourceId);
|
|
620
|
+
if (!source) {
|
|
621
|
+
throw new Error(`未找到配置: ${sourceId}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const meta = PLATFORM_META[platform];
|
|
625
|
+
const answers = interactive
|
|
626
|
+
? await inquirer.prompt([
|
|
627
|
+
{
|
|
628
|
+
type: "input",
|
|
629
|
+
name: "providerName",
|
|
630
|
+
message: "新配置名称",
|
|
631
|
+
default: rawOptions.providerName || `${source.name}-copy`
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
type: "input",
|
|
635
|
+
name: "baseUrl",
|
|
636
|
+
message: `${meta.label} 地址`,
|
|
637
|
+
default: rawOptions.baseUrl || source.baseUrl
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
type: "input",
|
|
641
|
+
name: "model",
|
|
642
|
+
message: `${meta.label} 模型`,
|
|
643
|
+
default: rawOptions.model || source.model || DEFAULT_PRIMARY_MODEL
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
type: "password",
|
|
647
|
+
name: "apiKey",
|
|
648
|
+
message: "API Key(留空则复用来源配置)",
|
|
649
|
+
mask: "*"
|
|
650
|
+
}
|
|
651
|
+
])
|
|
652
|
+
: {
|
|
653
|
+
providerName: rawOptions.providerName,
|
|
654
|
+
baseUrl: rawOptions.baseUrl || source.baseUrl,
|
|
655
|
+
model: rawOptions.model || source.model || DEFAULT_PRIMARY_MODEL,
|
|
656
|
+
apiKey: rawOptions.apiKey || ""
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const providerName = `${answers.providerName || ""}`.trim();
|
|
660
|
+
if (!providerName) {
|
|
661
|
+
throw new Error("新配置名称不能为空");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const resolvedBaseUrl = await resolveBaseUrl(meta.label, {
|
|
665
|
+
interactive: false,
|
|
666
|
+
confirmChoice: false,
|
|
667
|
+
probe: rawOptions.probe,
|
|
668
|
+
candidates: [answers.baseUrl || source.baseUrl, ...(rawOptions.candidateBaseUrls || [])]
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const result = applyClonedProvider(
|
|
672
|
+
platform,
|
|
673
|
+
sourceId,
|
|
674
|
+
{
|
|
675
|
+
name: providerName,
|
|
676
|
+
baseUrl: resolvedBaseUrl,
|
|
677
|
+
model: `${answers.model || source.model || DEFAULT_PRIMARY_MODEL}`.trim() || DEFAULT_PRIMARY_MODEL,
|
|
678
|
+
apiKey: `${answers.apiKey || ""}`.trim() || source.apiKey
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
backup: rawOptions.backup !== false,
|
|
682
|
+
overwrite: rawOptions.overwrite === true,
|
|
683
|
+
activate: rawOptions.activate === true
|
|
684
|
+
}
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
printProviderSummary(platform, result.provider);
|
|
688
|
+
console.log(`当前启用: ${result.activeProvider ? result.activeProvider.name : "无"}`);
|
|
689
|
+
console.log(`备份: ${result.backupDir || "(disabled)"}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function runRemove(platform, nameOrId, rawOptions = {}) {
|
|
693
|
+
const interactive = rawOptions.interactive !== false;
|
|
694
|
+
const targetNameOrId =
|
|
695
|
+
nameOrId || (interactive ? await selectProviderId(platform, "请选择要删除的配置") : null);
|
|
696
|
+
|
|
697
|
+
if (!targetNameOrId) {
|
|
698
|
+
throw new Error("缺少配置名称或 ID");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (interactive && rawOptions.force !== true) {
|
|
702
|
+
const answer = await inquirer.prompt([
|
|
703
|
+
{
|
|
704
|
+
type: "confirm",
|
|
705
|
+
name: "confirmed",
|
|
706
|
+
message: `确认删除配置 "${targetNameOrId}" 吗?`,
|
|
707
|
+
default: false
|
|
708
|
+
}
|
|
709
|
+
]);
|
|
710
|
+
|
|
711
|
+
if (!answer.confirmed) {
|
|
712
|
+
console.log(chalk.yellow("已取消。"));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const result = applyRemovedProvider(platform, targetNameOrId, {
|
|
718
|
+
backup: rawOptions.backup !== false,
|
|
719
|
+
overwrite: rawOptions.overwrite === true,
|
|
720
|
+
activateFallback: rawOptions.activateFallback !== false
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
console.log(chalk.green(`已删除配置: ${result.removedProvider.name}`));
|
|
724
|
+
console.log(`回退启用: ${result.activeProvider ? result.activeProvider.name : "无"}`);
|
|
725
|
+
console.log(`备份: ${result.backupDir || "(disabled)"}`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function runCurrent(platform) {
|
|
729
|
+
const provider = getCurrentProvider(platform);
|
|
730
|
+
if (!provider) {
|
|
731
|
+
console.log(chalk.yellow("当前没有启用配置。"));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
printProviderSummary(platform, provider);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function runUse(platform, nameOrId, rawOptions = {}) {
|
|
739
|
+
const result = applyStoredProvider(platform, nameOrId, {
|
|
740
|
+
overwrite: rawOptions.overwrite,
|
|
741
|
+
backup: rawOptions.backup !== false
|
|
742
|
+
});
|
|
743
|
+
printProviderSummary(platform, result.provider);
|
|
744
|
+
console.log(`备份: ${result.backupDir || "(disabled)"}`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function runProbeCommand(rawOptions = {}) {
|
|
748
|
+
const urls = dedupeStrings(rawOptions.urls || []);
|
|
749
|
+
|
|
750
|
+
if (urls.length === 0) {
|
|
751
|
+
throw new Error("请至少提供一个 --url");
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const results = await probeBaseUrls(urls, { timeoutMs: 5000 });
|
|
755
|
+
printProbeResults("Probe", results);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function runPresetList(rawOptions = {}) {
|
|
759
|
+
const presets = listPresets();
|
|
760
|
+
if (rawOptions.json === true) {
|
|
761
|
+
console.log(JSON.stringify(presets, null, 2));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
printPresetList();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function runPresetAdd(rawOptions = {}) {
|
|
768
|
+
const interactive = rawOptions.interactive !== false;
|
|
769
|
+
const answers = interactive
|
|
770
|
+
? await inquirer.prompt([
|
|
771
|
+
{
|
|
772
|
+
type: "input",
|
|
773
|
+
name: "name",
|
|
774
|
+
message: "预设名称",
|
|
775
|
+
default: rawOptions.name || ""
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
type: "input",
|
|
779
|
+
name: "providerName",
|
|
780
|
+
message: "默认配置名称",
|
|
781
|
+
default: rawOptions.providerName || rawOptions.name || DEFAULT_PROVIDER_NAME
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
type: "input",
|
|
785
|
+
name: "commonBaseUrl",
|
|
786
|
+
message: "Codex/OpenCode 地址",
|
|
787
|
+
default: rawOptions.commonBaseUrl || rawOptions.baseUrl || DEFAULT_BASE_URL
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
type: "input",
|
|
791
|
+
name: "openclawBaseUrl",
|
|
792
|
+
message: "OpenClaw 地址",
|
|
793
|
+
default:
|
|
794
|
+
rawOptions.openclawBaseUrl ||
|
|
795
|
+
buildOpenClawBaseUrl(rawOptions.commonBaseUrl || rawOptions.baseUrl || DEFAULT_BASE_URL)
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
type: "input",
|
|
799
|
+
name: "model",
|
|
800
|
+
message: "默认模型",
|
|
801
|
+
default: rawOptions.model || DEFAULT_PRIMARY_MODEL
|
|
802
|
+
}
|
|
803
|
+
])
|
|
804
|
+
: {
|
|
805
|
+
name: rawOptions.name,
|
|
806
|
+
providerName: rawOptions.providerName || rawOptions.name || DEFAULT_PROVIDER_NAME,
|
|
807
|
+
commonBaseUrl: rawOptions.commonBaseUrl || rawOptions.baseUrl || DEFAULT_BASE_URL,
|
|
808
|
+
openclawBaseUrl:
|
|
809
|
+
rawOptions.openclawBaseUrl ||
|
|
810
|
+
buildOpenClawBaseUrl(rawOptions.commonBaseUrl || rawOptions.baseUrl || DEFAULT_BASE_URL),
|
|
811
|
+
model: rawOptions.model || DEFAULT_PRIMARY_MODEL
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const preset = upsertPreset({
|
|
815
|
+
name: answers.name,
|
|
816
|
+
providerName: answers.providerName,
|
|
817
|
+
commonBaseUrl: answers.commonBaseUrl,
|
|
818
|
+
openclawBaseUrl: answers.openclawBaseUrl,
|
|
819
|
+
model: answers.model
|
|
820
|
+
});
|
|
821
|
+
printPresetSummary("预设已保存", preset);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async function runPresetUse(name, rawOptions = {}) {
|
|
825
|
+
await runSetup({
|
|
826
|
+
...rawOptions,
|
|
827
|
+
preset: name,
|
|
828
|
+
interactive: false
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async function runPresetRemove(name, rawOptions = {}) {
|
|
833
|
+
const interactive = rawOptions.interactive !== false;
|
|
834
|
+
if (interactive && rawOptions.force !== true) {
|
|
835
|
+
const answer = await inquirer.prompt([
|
|
836
|
+
{
|
|
837
|
+
type: "confirm",
|
|
838
|
+
name: "confirmed",
|
|
839
|
+
message: `确认删除预设 "${name}" 吗?`,
|
|
840
|
+
default: false
|
|
841
|
+
}
|
|
842
|
+
]);
|
|
843
|
+
|
|
844
|
+
if (!answer.confirmed) {
|
|
845
|
+
console.log(chalk.yellow("已取消。"));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const removed = removePreset(name);
|
|
851
|
+
printPresetSummary("预设已删除", removed);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function selectPresetName(message, options = {}) {
|
|
855
|
+
const presets = listPresets().filter((preset) =>
|
|
856
|
+
options.customOnly === true ? preset.readonly !== true : true
|
|
857
|
+
);
|
|
858
|
+
if (presets.length === 0) {
|
|
859
|
+
throw new Error(options.customOnly ? "没有找到自定义预设" : "没有找到预设");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const answer = await inquirer.prompt([
|
|
863
|
+
{
|
|
864
|
+
type: "list",
|
|
865
|
+
name: "presetName",
|
|
866
|
+
message,
|
|
867
|
+
choices: presets.map((preset) => ({
|
|
868
|
+
name: `${preset.name} (${preset.source || (preset.readonly ? "内置" : "自定义")})`,
|
|
869
|
+
value: preset.name
|
|
870
|
+
}))
|
|
871
|
+
}
|
|
872
|
+
]);
|
|
873
|
+
|
|
874
|
+
return answer.presetName;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function runPresetMenu() {
|
|
878
|
+
let shouldExit = false;
|
|
879
|
+
|
|
880
|
+
while (!shouldExit) {
|
|
881
|
+
const answer = await inquirer.prompt([
|
|
882
|
+
{
|
|
883
|
+
type: "list",
|
|
884
|
+
name: "action",
|
|
885
|
+
message: "预设管理",
|
|
886
|
+
choices: [
|
|
887
|
+
{ name: "查看预设", value: "list" },
|
|
888
|
+
{ name: "新增或更新预设", value: "add" },
|
|
889
|
+
{ name: "立即使用预设", value: "use" },
|
|
890
|
+
{ name: "删除自定义预设", value: "remove" },
|
|
891
|
+
{ name: "返回", value: "back" }
|
|
892
|
+
]
|
|
893
|
+
}
|
|
894
|
+
]);
|
|
895
|
+
|
|
896
|
+
switch (answer.action) {
|
|
897
|
+
case "list":
|
|
898
|
+
runPresetList();
|
|
899
|
+
break;
|
|
900
|
+
case "add":
|
|
901
|
+
await runPresetAdd({ interactive: true });
|
|
902
|
+
break;
|
|
903
|
+
case "use": {
|
|
904
|
+
const presetName = await selectPresetName("请选择预设");
|
|
905
|
+
await runPresetUse(presetName, {});
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
case "remove": {
|
|
909
|
+
const presetName = await selectPresetName("请选择要删除的自定义预设", { customOnly: true });
|
|
910
|
+
await runPresetRemove(presetName, { interactive: true });
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case "back":
|
|
914
|
+
shouldExit = true;
|
|
915
|
+
break;
|
|
916
|
+
default:
|
|
917
|
+
shouldExit = true;
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async function runPlatformMenu(platform) {
|
|
924
|
+
const meta = PLATFORM_META[platform];
|
|
925
|
+
let shouldExit = false;
|
|
926
|
+
|
|
927
|
+
while (!shouldExit) {
|
|
928
|
+
const answer = await inquirer.prompt([
|
|
929
|
+
{
|
|
930
|
+
type: "list",
|
|
931
|
+
name: "action",
|
|
932
|
+
message: `${meta.label} 配置管理`,
|
|
933
|
+
choices: [
|
|
934
|
+
{ name: "查看已保存配置", value: "list" },
|
|
935
|
+
{ name: "查看当前启用配置", value: "current" },
|
|
936
|
+
{ name: "新增或更新配置", value: "add" },
|
|
937
|
+
{ name: "切换配置", value: "use" },
|
|
938
|
+
{ name: "编辑配置", value: "edit" },
|
|
939
|
+
{ name: "克隆配置", value: "clone" },
|
|
940
|
+
{ name: "删除配置", value: "remove" },
|
|
941
|
+
{ name: "返回", value: "back" }
|
|
942
|
+
]
|
|
943
|
+
}
|
|
944
|
+
]);
|
|
945
|
+
|
|
946
|
+
switch (answer.action) {
|
|
947
|
+
case "list":
|
|
948
|
+
printProviderList(platform);
|
|
949
|
+
break;
|
|
950
|
+
case "current":
|
|
951
|
+
runCurrent(platform);
|
|
952
|
+
break;
|
|
953
|
+
case "add":
|
|
954
|
+
await runAdd(platform, { interactive: true, probe: true, backup: true });
|
|
955
|
+
break;
|
|
956
|
+
case "use": {
|
|
957
|
+
const providerId = await selectProviderId(platform, "请选择要启用的配置");
|
|
958
|
+
runUse(platform, providerId, { backup: true });
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
case "edit": {
|
|
962
|
+
const providerId = await selectProviderId(platform, "请选择要编辑的配置");
|
|
963
|
+
await runEdit(platform, providerId, { interactive: true, probe: true, backup: true });
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
case "clone": {
|
|
967
|
+
const providerId = await selectProviderId(platform, "请选择要克隆的配置");
|
|
968
|
+
await runClone(platform, providerId, { interactive: true, probe: true, backup: true });
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
case "remove": {
|
|
972
|
+
const providerId = await selectProviderId(platform, "请选择要删除的配置");
|
|
973
|
+
await runRemove(platform, providerId, { interactive: true, backup: true });
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
case "back":
|
|
977
|
+
shouldExit = true;
|
|
978
|
+
break;
|
|
979
|
+
default:
|
|
980
|
+
shouldExit = true;
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function runMainMenu() {
|
|
987
|
+
let shouldExit = false;
|
|
988
|
+
|
|
989
|
+
printBrandBanner();
|
|
990
|
+
|
|
991
|
+
while (!shouldExit) {
|
|
992
|
+
const answer = await inquirer.prompt([
|
|
993
|
+
{
|
|
994
|
+
type: "list",
|
|
995
|
+
name: "action",
|
|
996
|
+
message: "March 主菜单",
|
|
997
|
+
choices: [
|
|
998
|
+
{ name: "快速安装", value: "setup" },
|
|
999
|
+
{ name: "自定义安装", value: "advanced-setup" },
|
|
1000
|
+
{ name: "管理预设", value: "presets" },
|
|
1001
|
+
{ name: "管理 Codex", value: "codex" },
|
|
1002
|
+
{ name: "管理 OpenCode", value: "opencode" },
|
|
1003
|
+
{ name: "管理 OpenClaw", value: "openclaw" },
|
|
1004
|
+
{ name: "退出", value: "exit" }
|
|
1005
|
+
]
|
|
1006
|
+
}
|
|
1007
|
+
]);
|
|
1008
|
+
|
|
1009
|
+
switch (answer.action) {
|
|
1010
|
+
case "setup":
|
|
1011
|
+
await runSetup({ interactive: true, probe: true, backup: true, advanced: false });
|
|
1012
|
+
break;
|
|
1013
|
+
case "advanced-setup":
|
|
1014
|
+
await runSetup({ interactive: true, probe: true, backup: true, advanced: true });
|
|
1015
|
+
break;
|
|
1016
|
+
case "presets":
|
|
1017
|
+
await runPresetMenu();
|
|
1018
|
+
break;
|
|
1019
|
+
case "codex":
|
|
1020
|
+
case "opencode":
|
|
1021
|
+
case "openclaw":
|
|
1022
|
+
await runPlatformMenu(answer.action);
|
|
1023
|
+
break;
|
|
1024
|
+
case "exit":
|
|
1025
|
+
shouldExit = true;
|
|
1026
|
+
break;
|
|
1027
|
+
default:
|
|
1028
|
+
shouldExit = true;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const program = new Command();
|
|
1034
|
+
program
|
|
1035
|
+
.name(APP_NAME)
|
|
1036
|
+
.description("March 服务商配置管理工具")
|
|
1037
|
+
.version("0.1.2");
|
|
1038
|
+
|
|
1039
|
+
program
|
|
1040
|
+
.command("menu")
|
|
1041
|
+
.description("打开交互菜单")
|
|
1042
|
+
.action(async () => {
|
|
1043
|
+
await runMainMenu();
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
program
|
|
1047
|
+
.command("setup")
|
|
1048
|
+
.description("一键配置 Codex / OpenCode / OpenClaw")
|
|
1049
|
+
.option("-k, --api-key <apiKey>", "API Key")
|
|
1050
|
+
.option("--preset <name>", "预设名称")
|
|
1051
|
+
.option("-p, --platforms <platforms>", "平台列表,逗号分隔:codex,opencode,openclaw")
|
|
1052
|
+
.option("-n, --provider-name <name>", "配置名称")
|
|
1053
|
+
.option("-b, --base-url <url>", "Codex / OpenCode 地址")
|
|
1054
|
+
.option("--openclaw-base-url <url>", "OpenClaw 地址")
|
|
1055
|
+
.option("--model <model>", "模型")
|
|
1056
|
+
.option("--candidate-base-url <url>", "额外 Codex / OpenCode 备选地址", collect, [])
|
|
1057
|
+
.option("--openclaw-candidate-base-url <url>", "额外 OpenClaw 备选地址", collect, [])
|
|
1058
|
+
.option("--no-probe", "跳过地址测速")
|
|
1059
|
+
.option("--no-backup", "跳过自动备份")
|
|
1060
|
+
.option("--overwrite", "覆盖写入目标配置(不合并)")
|
|
1061
|
+
.action(async (options) => {
|
|
1062
|
+
await runSetup({
|
|
1063
|
+
interactive: false,
|
|
1064
|
+
apiKey: options.apiKey,
|
|
1065
|
+
preset: options.preset,
|
|
1066
|
+
platforms: options.platforms,
|
|
1067
|
+
providerName: options.providerName,
|
|
1068
|
+
baseUrl: options.baseUrl,
|
|
1069
|
+
openclawBaseUrl: options.openclawBaseUrl,
|
|
1070
|
+
model: options.model,
|
|
1071
|
+
candidateBaseUrls: options.candidateBaseUrl,
|
|
1072
|
+
openclawCandidateBaseUrls: options.openclawCandidateBaseUrl,
|
|
1073
|
+
probe: options.probe,
|
|
1074
|
+
backup: options.backup,
|
|
1075
|
+
overwrite: options.overwrite
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
program
|
|
1080
|
+
.command("probe")
|
|
1081
|
+
.description("测速一个或多个地址")
|
|
1082
|
+
.requiredOption("-u, --url <url>", "待测速地址", collect, [])
|
|
1083
|
+
.action(async (options) => {
|
|
1084
|
+
await runProbeCommand({
|
|
1085
|
+
urls: options.url
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const presetCommand = program.command("preset").description("管理安装预设");
|
|
1090
|
+
|
|
1091
|
+
presetCommand
|
|
1092
|
+
.command("list")
|
|
1093
|
+
.description("查看可用预设")
|
|
1094
|
+
.option("--json", "输出 JSON")
|
|
1095
|
+
.action((options) => {
|
|
1096
|
+
runPresetList({
|
|
1097
|
+
json: options.json === true
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
presetCommand
|
|
1102
|
+
.command("add")
|
|
1103
|
+
.description("新增或更新自定义预设")
|
|
1104
|
+
.requiredOption("-n, --name <name>", "预设名称")
|
|
1105
|
+
.option("--provider-name <name>", "默认配置名称")
|
|
1106
|
+
.option("--base-url <url>", "Codex/OpenCode 默认地址")
|
|
1107
|
+
.option("--common-base-url <url>", "Codex/OpenCode 默认地址")
|
|
1108
|
+
.option("--openclaw-base-url <url>", "OpenClaw 默认地址")
|
|
1109
|
+
.option("--model <model>", "默认模型", DEFAULT_PRIMARY_MODEL)
|
|
1110
|
+
.action(async (options) => {
|
|
1111
|
+
await runPresetAdd({
|
|
1112
|
+
interactive: false,
|
|
1113
|
+
name: options.name,
|
|
1114
|
+
providerName: options.providerName,
|
|
1115
|
+
baseUrl: options.baseUrl,
|
|
1116
|
+
commonBaseUrl: options.commonBaseUrl,
|
|
1117
|
+
openclawBaseUrl: options.openclawBaseUrl,
|
|
1118
|
+
model: options.model
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
presetCommand
|
|
1123
|
+
.command("use <name>")
|
|
1124
|
+
.description("使用预设执行安装")
|
|
1125
|
+
.option("-k, --api-key <apiKey>", "API Key")
|
|
1126
|
+
.option("-p, --platforms <platforms>", "平台列表,逗号分隔:codex,opencode,openclaw")
|
|
1127
|
+
.option("-n, --provider-name <name>", "覆盖配置名称")
|
|
1128
|
+
.option("--model <model>", "覆盖模型")
|
|
1129
|
+
.option("--candidate-base-url <url>", "额外 Codex / OpenCode 备选地址", collect, [])
|
|
1130
|
+
.option("--openclaw-candidate-base-url <url>", "额外 OpenClaw 备选地址", collect, [])
|
|
1131
|
+
.option("--no-probe", "跳过地址测速")
|
|
1132
|
+
.option("--no-backup", "跳过自动备份")
|
|
1133
|
+
.option("--overwrite", "覆盖写入目标配置(不合并)")
|
|
1134
|
+
.action(async (name, options) => {
|
|
1135
|
+
await runPresetUse(name, {
|
|
1136
|
+
apiKey: options.apiKey,
|
|
1137
|
+
platforms: options.platforms,
|
|
1138
|
+
providerName: options.providerName,
|
|
1139
|
+
model: options.model,
|
|
1140
|
+
candidateBaseUrls: options.candidateBaseUrl,
|
|
1141
|
+
openclawCandidateBaseUrls: options.openclawCandidateBaseUrl,
|
|
1142
|
+
probe: options.probe,
|
|
1143
|
+
backup: options.backup,
|
|
1144
|
+
overwrite: options.overwrite
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
presetCommand
|
|
1149
|
+
.command("remove <name>")
|
|
1150
|
+
.description("删除自定义预设")
|
|
1151
|
+
.option("-f, --force", "跳过确认")
|
|
1152
|
+
.action(async (name, options) => {
|
|
1153
|
+
await runPresetRemove(name, {
|
|
1154
|
+
interactive: false,
|
|
1155
|
+
force: options.force
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
function registerPlatformCommand(platform) {
|
|
1160
|
+
const meta = PLATFORM_META[platform];
|
|
1161
|
+
const command = program.command(meta.command).description(`管理 ${meta.label} 配置`);
|
|
1162
|
+
|
|
1163
|
+
command.command("list").description(`查看 ${meta.label} 已保存配置`).action(() => printProviderList(platform));
|
|
1164
|
+
|
|
1165
|
+
command.command("current").description(`查看当前启用的 ${meta.label} 配置`).action(() => runCurrent(platform));
|
|
1166
|
+
|
|
1167
|
+
command
|
|
1168
|
+
.command("add")
|
|
1169
|
+
.description(`新增或更新 ${meta.label} 配置`)
|
|
1170
|
+
.option("-n, --provider-name <name>", "配置名称", DEFAULT_PROVIDER_NAME)
|
|
1171
|
+
.option("-u, --base-url <url>", "地址", meta.defaultBaseUrl)
|
|
1172
|
+
.option("-k, --api-key <apiKey>", "API Key")
|
|
1173
|
+
.option("--candidate-base-url <url>", "额外备选地址", collect, [])
|
|
1174
|
+
.option("--model <model>", "模型", DEFAULT_PRIMARY_MODEL)
|
|
1175
|
+
.option("--no-probe", "跳过地址测速")
|
|
1176
|
+
.option("--no-backup", "跳过自动备份")
|
|
1177
|
+
.option("--overwrite", "覆盖写入目标配置(不合并)")
|
|
1178
|
+
.action(async (options) => {
|
|
1179
|
+
await runAdd(platform, {
|
|
1180
|
+
interactive: false,
|
|
1181
|
+
providerName: options.providerName,
|
|
1182
|
+
baseUrl: options.baseUrl,
|
|
1183
|
+
candidateBaseUrls: options.candidateBaseUrl,
|
|
1184
|
+
apiKey: options.apiKey,
|
|
1185
|
+
model: options.model,
|
|
1186
|
+
probe: options.probe,
|
|
1187
|
+
backup: options.backup,
|
|
1188
|
+
overwrite: options.overwrite
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
command
|
|
1193
|
+
.command("use <nameOrId>")
|
|
1194
|
+
.description(`切换到已保存的 ${meta.label} 配置`)
|
|
1195
|
+
.option("--no-backup", "跳过自动备份")
|
|
1196
|
+
.option("--overwrite", "覆盖写入目标配置(不合并)")
|
|
1197
|
+
.action((nameOrId, options) => {
|
|
1198
|
+
runUse(platform, nameOrId, {
|
|
1199
|
+
backup: options.backup,
|
|
1200
|
+
overwrite: options.overwrite
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
command
|
|
1205
|
+
.command("edit <nameOrId>")
|
|
1206
|
+
.description(`编辑已有 ${meta.label} 配置`)
|
|
1207
|
+
.option("-n, --provider-name <name>", "配置名称")
|
|
1208
|
+
.option("-u, --base-url <url>", "地址")
|
|
1209
|
+
.option("-k, --api-key <apiKey>", "API Key")
|
|
1210
|
+
.option("--candidate-base-url <url>", "额外备选地址", collect, [])
|
|
1211
|
+
.option("--model <model>", "模型")
|
|
1212
|
+
.option("--activate", "编辑后立即启用")
|
|
1213
|
+
.option("--no-probe", "跳过地址测速")
|
|
1214
|
+
.option("--no-backup", "跳过自动备份")
|
|
1215
|
+
.option("--overwrite", "覆盖写入目标配置(不合并)")
|
|
1216
|
+
.action(async (nameOrId, options) => {
|
|
1217
|
+
await runEdit(platform, nameOrId, {
|
|
1218
|
+
interactive: false,
|
|
1219
|
+
providerName: options.providerName,
|
|
1220
|
+
baseUrl: options.baseUrl,
|
|
1221
|
+
candidateBaseUrls: options.candidateBaseUrl,
|
|
1222
|
+
apiKey: options.apiKey,
|
|
1223
|
+
model: options.model,
|
|
1224
|
+
activate: options.activate,
|
|
1225
|
+
probe: options.probe,
|
|
1226
|
+
backup: options.backup,
|
|
1227
|
+
overwrite: options.overwrite
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
command
|
|
1232
|
+
.command("clone <nameOrId>")
|
|
1233
|
+
.description(`克隆已有 ${meta.label} 配置`)
|
|
1234
|
+
.requiredOption("-n, --provider-name <name>", "新配置名称")
|
|
1235
|
+
.option("-u, --base-url <url>", "地址")
|
|
1236
|
+
.option("-k, --api-key <apiKey>", "API Key")
|
|
1237
|
+
.option("--candidate-base-url <url>", "额外备选地址", collect, [])
|
|
1238
|
+
.option("--model <model>", "模型")
|
|
1239
|
+
.option("--activate", "克隆后立即启用")
|
|
1240
|
+
.option("--no-probe", "跳过地址测速")
|
|
1241
|
+
.option("--no-backup", "跳过自动备份")
|
|
1242
|
+
.option("--overwrite", "覆盖写入目标配置(不合并)")
|
|
1243
|
+
.action(async (nameOrId, options) => {
|
|
1244
|
+
await runClone(platform, nameOrId, {
|
|
1245
|
+
interactive: false,
|
|
1246
|
+
providerName: options.providerName,
|
|
1247
|
+
baseUrl: options.baseUrl,
|
|
1248
|
+
candidateBaseUrls: options.candidateBaseUrl,
|
|
1249
|
+
apiKey: options.apiKey,
|
|
1250
|
+
model: options.model,
|
|
1251
|
+
activate: options.activate,
|
|
1252
|
+
probe: options.probe,
|
|
1253
|
+
backup: options.backup,
|
|
1254
|
+
overwrite: options.overwrite
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
command
|
|
1259
|
+
.command("remove <nameOrId>")
|
|
1260
|
+
.description(`删除已有 ${meta.label} 配置`)
|
|
1261
|
+
.option("--no-backup", "跳过自动备份")
|
|
1262
|
+
.option("--no-activate-fallback", "删除当前配置时不自动切换")
|
|
1263
|
+
.option("--overwrite", "覆盖写入目标配置(不合并)")
|
|
1264
|
+
.option("-f, --force", "交互模式下跳过确认")
|
|
1265
|
+
.action(async (nameOrId, options) => {
|
|
1266
|
+
await runRemove(platform, nameOrId, {
|
|
1267
|
+
interactive: false,
|
|
1268
|
+
backup: options.backup,
|
|
1269
|
+
activateFallback: options.activateFallback,
|
|
1270
|
+
overwrite: options.overwrite,
|
|
1271
|
+
force: options.force
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
command.action(async () => {
|
|
1276
|
+
await runPlatformMenu(platform);
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
for (const platform of SUPPORTED_PLATFORMS) {
|
|
1281
|
+
registerPlatformCommand(platform);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
if (process.argv.length <= 2) {
|
|
1286
|
+
await runSetup({ interactive: true, probe: true, backup: true, advanced: false });
|
|
1287
|
+
} else {
|
|
1288
|
+
await program.parseAsync(process.argv);
|
|
1289
|
+
}
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
console.error(chalk.red(`错误: ${error.message}`));
|
|
1292
|
+
process.exitCode = 1;
|
|
1293
|
+
}
|