ccnew 0.1.10

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