yingclaw 2.5.20 → 2.5.26
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 +31 -6
- package/bin/cli.js +372 -67
- package/index.js +2 -0
- package/lib/config.js +193 -20
- package/lib/doctor.js +13 -3
- package/lib/gateway.js +137 -6
- package/lib/openai.js +294 -0
- package/lib/panel.js +19 -5
- package/lib/vscode.js +280 -0
- package/package.json +2 -2
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
|
-
const { select, input, confirm } = require('@inquirer/prompts');
|
|
4
|
+
const { select, input, confirm, search, checkbox } = require('@inquirer/prompts');
|
|
5
5
|
const {
|
|
6
6
|
loadConfig,
|
|
7
7
|
saveConfig,
|
|
@@ -13,7 +13,9 @@ const {
|
|
|
13
13
|
validateConfig,
|
|
14
14
|
validateKey,
|
|
15
15
|
normalizeAnthropicBaseUrl,
|
|
16
|
+
getProviderProtocol,
|
|
16
17
|
resolveFastModel,
|
|
18
|
+
sortRelatedModelIds,
|
|
17
19
|
buildClaudeEnv,
|
|
18
20
|
PROVIDERS,
|
|
19
21
|
CLAUDE_ENV_KEYS,
|
|
@@ -23,6 +25,12 @@ const { execSync, spawn, spawnSync } = require('child_process');
|
|
|
23
25
|
const pkg = require('../package.json');
|
|
24
26
|
const { getGatewayAutostartStatus, installGatewayAutostart, removeGatewayAutostart } = require('../lib/autostart');
|
|
25
27
|
const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
|
|
28
|
+
const {
|
|
29
|
+
buildVsCodeOpenCommand,
|
|
30
|
+
buildVsCodeModelSelectionIds,
|
|
31
|
+
clearVsCodeIntegration,
|
|
32
|
+
writeVsCodeIntegration,
|
|
33
|
+
} = require('../lib/vscode');
|
|
26
34
|
const {
|
|
27
35
|
buildClaudeInstallCommand,
|
|
28
36
|
buildYingclawUpgradeCommand,
|
|
@@ -42,19 +50,23 @@ const {
|
|
|
42
50
|
isDesktopChatModel,
|
|
43
51
|
} = require('../lib/gateway');
|
|
44
52
|
const { runDoctorChecks, summarize, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
|
|
53
|
+
const { normalizeOpenAiBaseUrl } = require('../lib/openai');
|
|
45
54
|
|
|
46
55
|
const program = new Command();
|
|
47
56
|
|
|
57
|
+
let _bannerCache;
|
|
48
58
|
async function getBanner() {
|
|
59
|
+
if (_bannerCache) return _bannerCache;
|
|
49
60
|
const chalk = (await import('chalk')).default;
|
|
50
61
|
const figlet = require('figlet');
|
|
51
62
|
const boxen = (await import('boxen')).default;
|
|
52
63
|
const title = figlet.textSync('yingclaw', { font: 'Small', horizontalLayout: 'fitted' });
|
|
53
64
|
const subtitle = chalk.dim('Claude Code × 国产大模型 一键接入') + ' ' + chalk.cyan(`v${pkg.version}`);
|
|
54
|
-
|
|
65
|
+
_bannerCache = boxen(
|
|
55
66
|
chalk.cyan.bold(title) + '\n' + subtitle,
|
|
56
67
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 0 } }
|
|
57
68
|
);
|
|
69
|
+
return _bannerCache;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
function getConfigValidationMessage(config) {
|
|
@@ -155,15 +167,36 @@ async function offerDesktopSync(chalk, ora, config) {
|
|
|
155
167
|
}
|
|
156
168
|
}
|
|
157
169
|
|
|
170
|
+
const SEARCH_THRESHOLD = 15;
|
|
171
|
+
|
|
158
172
|
async function promptModelFromChoices({ chalk, choices, message, backLabel = '↩ 返回上一步', allowManual = true }) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
const extraChoices = [
|
|
174
|
+
...(allowManual ? [{ name: '手动输入模型名', value: '__MANUAL__' }] : []),
|
|
175
|
+
{ name: chalk.dim(backLabel), value: '__BACK__' },
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
let selected;
|
|
179
|
+
if (choices.length > SEARCH_THRESHOLD) {
|
|
180
|
+
selected = await search({
|
|
181
|
+
message: chalk.cyan(message),
|
|
182
|
+
source: (term) => {
|
|
183
|
+
const filtered = term
|
|
184
|
+
? choices.filter((c) =>
|
|
185
|
+
String(c.name || '').toLowerCase().includes(term.toLowerCase()) ||
|
|
186
|
+
String(c.value || '').toLowerCase().includes(term.toLowerCase())
|
|
187
|
+
)
|
|
188
|
+
: choices;
|
|
189
|
+
return term ? [...filtered, ...extraChoices] : [...extraChoices, ...filtered];
|
|
190
|
+
},
|
|
191
|
+
pageSize: 15,
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
selected = await select({ loop: false,
|
|
195
|
+
message: chalk.cyan(message),
|
|
196
|
+
choices: [...choices, ...extraChoices],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
167
200
|
if (selected === '__BACK__') return '__BACK__';
|
|
168
201
|
if (selected !== '__MANUAL__') return selected;
|
|
169
202
|
|
|
@@ -173,10 +206,121 @@ async function promptModelFromChoices({ chalk, choices, message, backLabel = '
|
|
|
173
206
|
}).then(v => v.trim());
|
|
174
207
|
}
|
|
175
208
|
|
|
209
|
+
const DESKTOP_MODEL_SELECT_THRESHOLD = 10;
|
|
210
|
+
const DESKTOP_MODEL_MAX = 10;
|
|
211
|
+
|
|
212
|
+
async function promptDesktopModels(chalk, config, chatModels) {
|
|
213
|
+
const savedModels = Array.isArray(config.desktopModels)
|
|
214
|
+
? config.desktopModels.filter((m) => chatModels.includes(m))
|
|
215
|
+
: [];
|
|
216
|
+
|
|
217
|
+
// If there is a saved selection, ask whether to keep it or reselect
|
|
218
|
+
if (savedModels.length > 0) {
|
|
219
|
+
console.log(chalk.dim(`上次选择(${savedModels.length} 个):${savedModels.join('、')}`));
|
|
220
|
+
let keep;
|
|
221
|
+
try {
|
|
222
|
+
keep = await confirm({ message: chalk.cyan('保留上次选择?'), default: true });
|
|
223
|
+
} catch (e) {
|
|
224
|
+
if (e?.name === 'ExitPromptError') return null;
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
if (keep) return savedModels;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const selected = savedModels.length > 0
|
|
231
|
+
? [...savedModels]
|
|
232
|
+
: [config.model].filter((m) => m && chatModels.includes(m));
|
|
233
|
+
|
|
234
|
+
const choices = chatModels.map((m) => ({ name: m, value: m }));
|
|
235
|
+
|
|
236
|
+
const DONE = '__DONE__';
|
|
237
|
+
const BACK = '__BACK__';
|
|
238
|
+
|
|
239
|
+
while (true) {
|
|
240
|
+
if (selected.length > 0) {
|
|
241
|
+
console.log(chalk.green(`已选 ${selected.length} 个:${selected.join('、')}`));
|
|
242
|
+
}
|
|
243
|
+
const remaining = choices.filter((c) => !selected.includes(c.value));
|
|
244
|
+
|
|
245
|
+
let picked;
|
|
246
|
+
try {
|
|
247
|
+
picked = await search({
|
|
248
|
+
message: chalk.cyan(selected.length > 0 ? `继续添加桌面模型` : `选择桌面模型`) + chalk.dim(` 共 ${chatModels.length} 个可用`),
|
|
249
|
+
source: (term) => {
|
|
250
|
+
const filtered = term
|
|
251
|
+
? remaining.filter((c) => c.value.toLowerCase().includes(term.toLowerCase()))
|
|
252
|
+
: remaining;
|
|
253
|
+
return [
|
|
254
|
+
...(selected.length > 0 ? [{ name: chalk.green('✔ 完成'), value: DONE, short: '完成' }] : []),
|
|
255
|
+
{ name: chalk.dim('↩ 返回主菜单'), value: BACK, short: '返回主菜单' },
|
|
256
|
+
...filtered,
|
|
257
|
+
];
|
|
258
|
+
},
|
|
259
|
+
pageSize: 15,
|
|
260
|
+
});
|
|
261
|
+
} catch (e) {
|
|
262
|
+
if (e?.name === 'ExitPromptError') return null;
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (picked === BACK) return null;
|
|
267
|
+
if (picked === DONE) return selected;
|
|
268
|
+
|
|
269
|
+
selected.push(picked);
|
|
270
|
+
if (selected.length >= DESKTOP_MODEL_MAX) return selected;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
176
274
|
function getChatModelChoices(models) {
|
|
177
275
|
return models.filter(isDesktopChatModel).map(id => ({ name: id, value: id }));
|
|
178
276
|
}
|
|
179
277
|
|
|
278
|
+
const OPENCODE_GROUP_VALUE = '__opencode__';
|
|
279
|
+
|
|
280
|
+
async function promptProviderKey(chalk, message = '选择 AI 厂商') {
|
|
281
|
+
const buildMainChoices = () => {
|
|
282
|
+
const entries = Object.entries(PROVIDERS)
|
|
283
|
+
.filter(([key]) => key !== 'opencode_zen' && key !== 'opencode_go')
|
|
284
|
+
.map(([value, p]) => ({ name: p.name, value }));
|
|
285
|
+
const openrouterIdx = entries.findIndex((c) => c.value === 'openrouter');
|
|
286
|
+
entries.splice(openrouterIdx, 0, { name: 'OpenCode', value: OPENCODE_GROUP_VALUE });
|
|
287
|
+
return [...entries, { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' }];
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
while (true) {
|
|
291
|
+
let mainKey;
|
|
292
|
+
try {
|
|
293
|
+
mainKey = await select({ loop: false, message: chalk.cyan(message), choices: buildMainChoices() });
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (e?.name === 'ExitPromptError') return null;
|
|
296
|
+
throw e;
|
|
297
|
+
}
|
|
298
|
+
if (mainKey !== OPENCODE_GROUP_VALUE) return mainKey;
|
|
299
|
+
|
|
300
|
+
let sub;
|
|
301
|
+
try {
|
|
302
|
+
sub = await select({ loop: false,
|
|
303
|
+
message: chalk.cyan('选择 OpenCode 服务'),
|
|
304
|
+
choices: [
|
|
305
|
+
{ name: 'Zen(按量计费)', value: 'opencode_zen' },
|
|
306
|
+
{ name: 'Go(订阅制 $10/月)', value: 'opencode_go' },
|
|
307
|
+
{ name: chalk.dim('↩ 返回上一级'), value: '__BACK__' },
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
} catch (e) {
|
|
311
|
+
if (e?.name === 'ExitPromptError') return null;
|
|
312
|
+
throw e;
|
|
313
|
+
}
|
|
314
|
+
if (sub !== '__BACK__') return sub;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function sortModelChoicesByRelatedFamily(choices, mainModel) {
|
|
319
|
+
const order = sortRelatedModelIds(choices.map((choice) => choice.value), mainModel);
|
|
320
|
+
const rank = new Map(order.map((value, index) => [value, index]));
|
|
321
|
+
return [...choices].sort((a, b) => (rank.get(a.value) ?? 9999) - (rank.get(b.value) ?? 9999));
|
|
322
|
+
}
|
|
323
|
+
|
|
180
324
|
function formatFetchedModelCount(chalk, total, chatCount) {
|
|
181
325
|
if (total === chatCount) return chalk.green(`已获取 ${chatCount} 个可用聊天模型`);
|
|
182
326
|
return chalk.green(`已获取 ${total} 个模型,其中 ${chatCount} 个可用于 Claude/桌面聊天`);
|
|
@@ -190,14 +334,35 @@ async function promptManualModel(chalk, message, defaultValue) {
|
|
|
190
334
|
}).then(v => v.trim());
|
|
191
335
|
}
|
|
192
336
|
|
|
193
|
-
|
|
337
|
+
function isOpenAiCompatibleConfig(config) {
|
|
338
|
+
return getProviderProtocol(config) === 'openai';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function ensureGatewayForOpenAiConfig(config) {
|
|
342
|
+
return isOpenAiCompatibleConfig(config) ? ensureDesktopGatewayConfig(config) : config;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function writeClaudeCodeEnv(config) {
|
|
346
|
+
const effectiveConfig = buildClaudeEnv(config);
|
|
347
|
+
return writeEnvToZshrc(
|
|
348
|
+
effectiveConfig.ANTHROPIC_BASE_URL,
|
|
349
|
+
effectiveConfig.ANTHROPIC_AUTH_TOKEN,
|
|
350
|
+
effectiveConfig.ANTHROPIC_MODEL,
|
|
351
|
+
effectiveConfig.CLAUDE_CODE_SUBAGENT_MODEL,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function configureCustomProvider({ chalk, ora, existingConfig, providerKey = 'custom' }) {
|
|
356
|
+
const provider = PROVIDERS[providerKey];
|
|
357
|
+
const protocol = provider.protocol || 'anthropic';
|
|
358
|
+
const sameProvider = existingConfig?.provider === providerKey;
|
|
194
359
|
const baseUrl = await input({
|
|
195
|
-
message: chalk.cyan('Anthropic Base URL'),
|
|
196
|
-
default:
|
|
360
|
+
message: chalk.cyan(protocol === 'openai' ? 'OpenAI Base URL' : 'Anthropic Base URL'),
|
|
361
|
+
default: sameProvider ? existingConfig.baseUrl : undefined,
|
|
197
362
|
validate: (v) => v.trim().length > 0 && isValidUrl(v.trim()) ? true : '请输入有效 URL',
|
|
198
|
-
}).then(v => normalizeAnthropicBaseUrl(v.trim()));
|
|
363
|
+
}).then(v => protocol === 'openai' ? normalizeOpenAiBaseUrl(v.trim()) : normalizeAnthropicBaseUrl(v.trim()));
|
|
199
364
|
|
|
200
|
-
let apiKey =
|
|
365
|
+
let apiKey = sameProvider ? existingConfig.apiKey : '';
|
|
201
366
|
if (apiKey) {
|
|
202
367
|
const keepKey = await confirm({ message: '沿用当前 API Key?', default: true });
|
|
203
368
|
if (!keepKey) apiKey = '';
|
|
@@ -213,7 +378,7 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
|
|
|
213
378
|
let modelChoices = [];
|
|
214
379
|
let modelsUrl;
|
|
215
380
|
const fetchSpinner = ora('正在自动获取可用模型...').start();
|
|
216
|
-
const onlineResult = await fetchModelsFromBaseUrl(
|
|
381
|
+
const onlineResult = await fetchModelsFromBaseUrl(providerKey, apiKey, baseUrl);
|
|
217
382
|
if (onlineResult) {
|
|
218
383
|
modelsUrl = onlineResult.modelsUrl;
|
|
219
384
|
modelChoices = getChatModelChoices(onlineResult.models);
|
|
@@ -231,7 +396,11 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
|
|
|
231
396
|
if (modelChoices.length > 0) {
|
|
232
397
|
model = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择主模型' });
|
|
233
398
|
if (model === '__BACK__') return null;
|
|
234
|
-
fastModel = await promptModelFromChoices({
|
|
399
|
+
fastModel = await promptModelFromChoices({
|
|
400
|
+
chalk,
|
|
401
|
+
choices: sortModelChoicesByRelatedFamily(modelChoices, model),
|
|
402
|
+
message: '选择快速模型 / Subagent 模型',
|
|
403
|
+
});
|
|
235
404
|
if (fastModel === '__BACK__') return null;
|
|
236
405
|
} else {
|
|
237
406
|
model = await promptManualModel(chalk, '输入主模型名');
|
|
@@ -239,8 +408,9 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
|
|
|
239
408
|
}
|
|
240
409
|
|
|
241
410
|
return {
|
|
242
|
-
provider:
|
|
243
|
-
providerName:
|
|
411
|
+
provider: providerKey,
|
|
412
|
+
providerName: provider.name,
|
|
413
|
+
protocol,
|
|
244
414
|
baseUrl,
|
|
245
415
|
modelsUrl: modelsUrl || undefined,
|
|
246
416
|
apiKey,
|
|
@@ -343,17 +513,11 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
|
|
|
343
513
|
|
|
344
514
|
while (true) {
|
|
345
515
|
if (step === 'provider') {
|
|
346
|
-
providerKey = await
|
|
347
|
-
|
|
348
|
-
choices: [
|
|
349
|
-
...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
|
|
350
|
-
{ name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
|
|
351
|
-
],
|
|
352
|
-
});
|
|
353
|
-
if (providerKey === '__BACK__') return;
|
|
516
|
+
providerKey = await promptProviderKey(chalk);
|
|
517
|
+
if (!providerKey || providerKey === '__BACK__') return;
|
|
354
518
|
provider = PROVIDERS[providerKey];
|
|
355
519
|
if (provider.custom) {
|
|
356
|
-
customConfig = await configureCustomProvider({ chalk, ora });
|
|
520
|
+
customConfig = await configureCustomProvider({ chalk, ora, providerKey });
|
|
357
521
|
if (!customConfig) { step = 'provider'; continue; }
|
|
358
522
|
break;
|
|
359
523
|
}
|
|
@@ -388,12 +552,12 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
|
|
|
388
552
|
availableModels = provider.models.map(m => m.value);
|
|
389
553
|
}
|
|
390
554
|
|
|
391
|
-
const m = await
|
|
392
|
-
|
|
393
|
-
choices:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
555
|
+
const m = await promptModelFromChoices({
|
|
556
|
+
chalk,
|
|
557
|
+
choices: modelChoices,
|
|
558
|
+
message: '选择模型',
|
|
559
|
+
backLabel: '↩ 返回上一步(重新输入 Key)',
|
|
560
|
+
allowManual: false,
|
|
397
561
|
});
|
|
398
562
|
if (m === '__BACK__') { step = 'apikey'; continue; }
|
|
399
563
|
model = m;
|
|
@@ -407,9 +571,10 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
|
|
|
407
571
|
try {
|
|
408
572
|
const fastModel = customConfig?.fastModel || resolveFastModel(provider, model);
|
|
409
573
|
cfg = customConfig || { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl, availableModels };
|
|
574
|
+
cfg = ensureGatewayForOpenAiConfig(cfg);
|
|
410
575
|
saveConfig(cfg);
|
|
411
576
|
if (writeCodeEnv) {
|
|
412
|
-
({ file } =
|
|
577
|
+
({ file } = writeClaudeCodeEnv(cfg));
|
|
413
578
|
}
|
|
414
579
|
spinner.succeed(chalk.green(writeCodeEnv ? 'API 连接已保存,Claude Code 终端已接入' : 'API 连接已保存'));
|
|
415
580
|
} catch (e) {
|
|
@@ -432,7 +597,6 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
|
|
|
432
597
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
433
598
|
));
|
|
434
599
|
|
|
435
|
-
await offerDesktopSync(chalk, ora, cfg);
|
|
436
600
|
}
|
|
437
601
|
|
|
438
602
|
program
|
|
@@ -546,7 +710,7 @@ program
|
|
|
546
710
|
|
|
547
711
|
console.log(await getBanner());
|
|
548
712
|
|
|
549
|
-
|
|
713
|
+
let config = loadConfig();
|
|
550
714
|
if (!config) {
|
|
551
715
|
console.log(chalk.red('\n未配置 API 连接,请先运行: claw config\n'));
|
|
552
716
|
return;
|
|
@@ -561,7 +725,9 @@ program
|
|
|
561
725
|
const spinner = ora('写入 Claude Code 终端环境变量...').start();
|
|
562
726
|
let file;
|
|
563
727
|
try {
|
|
564
|
-
|
|
728
|
+
const effectiveConfig = ensureGatewayForOpenAiConfig(config);
|
|
729
|
+
if (effectiveConfig !== config) saveConfig(effectiveConfig);
|
|
730
|
+
({ file } = writeClaudeCodeEnv(effectiveConfig));
|
|
565
731
|
spinner.succeed(chalk.green(`Claude Code 终端已接入 → ${file}`));
|
|
566
732
|
} catch (e) {
|
|
567
733
|
spinner.fail(chalk.red(`写入失败: ${e.message}`));
|
|
@@ -569,10 +735,11 @@ program
|
|
|
569
735
|
}
|
|
570
736
|
|
|
571
737
|
console.log(chalk.dim(getStorageHint(file)));
|
|
738
|
+
const displayedConfig = effectiveConfig;
|
|
572
739
|
console.log(boxen(
|
|
573
740
|
chalk.bold('Claude Code 终端已接入\n\n') +
|
|
574
|
-
chalk.dim('Base URL ') + chalk.cyan(
|
|
575
|
-
chalk.dim('模型 ') + chalk.yellow(
|
|
741
|
+
chalk.dim('Base URL ') + chalk.cyan(buildClaudeEnv(displayedConfig).ANTHROPIC_BASE_URL) + '\n' +
|
|
742
|
+
chalk.dim('模型 ') + chalk.yellow(displayedConfig.model) + '\n\n' +
|
|
576
743
|
chalk.white('需要启动时,在主菜单选择“启动 Claude Code”,或直接输入 ') + chalk.cyan.bold('claude') + '\n' +
|
|
577
744
|
chalk.dim(getActivationHint(file)),
|
|
578
745
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
@@ -620,6 +787,120 @@ program
|
|
|
620
787
|
}
|
|
621
788
|
});
|
|
622
789
|
|
|
790
|
+
program
|
|
791
|
+
.command('vscode')
|
|
792
|
+
.description('接入 VS Code Claude Code 扩展')
|
|
793
|
+
.option('--terminal', '让 Claude Code 扩展默认使用终端模式')
|
|
794
|
+
.action(async (options) => {
|
|
795
|
+
const chalk = (await import('chalk')).default;
|
|
796
|
+
const ora = (await import('ora')).default;
|
|
797
|
+
const boxen = (await import('boxen')).default;
|
|
798
|
+
|
|
799
|
+
console.log(await getBanner());
|
|
800
|
+
|
|
801
|
+
const config = loadConfig();
|
|
802
|
+
if (!config) {
|
|
803
|
+
console.log(chalk.red('\n未配置 API 连接,请先运行: claw config\n'));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const configProblem = getConfigValidationMessage(config);
|
|
807
|
+
if (configProblem) {
|
|
808
|
+
console.log(chalk.red(`\n配置无效:${configProblem}`));
|
|
809
|
+
console.log(chalk.dim('请运行 claw config 重新配置。\n'));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
let effectiveConfig = ensureGatewayForOpenAiConfig(config);
|
|
814
|
+
if (effectiveConfig !== config) saveConfig(effectiveConfig);
|
|
815
|
+
|
|
816
|
+
const vscodeModelChoices = getChatModelChoices(buildVsCodeModelSelectionIds(effectiveConfig));
|
|
817
|
+
const vscodeModelCount = vscodeModelChoices.length;
|
|
818
|
+
const vscodeModel = await promptModelFromChoices({
|
|
819
|
+
chalk,
|
|
820
|
+
choices: vscodeModelChoices,
|
|
821
|
+
message: `选择 VS Code Claude Code 主模型 共 ${vscodeModelCount} 个可用`,
|
|
822
|
+
allowManual: true,
|
|
823
|
+
});
|
|
824
|
+
if (vscodeModel === '__BACK__') return;
|
|
825
|
+
|
|
826
|
+
const vscodeFastModel = await promptModelFromChoices({
|
|
827
|
+
chalk,
|
|
828
|
+
choices: sortModelChoicesByRelatedFamily(vscodeModelChoices, vscodeModel),
|
|
829
|
+
message: `选择 VS Code Claude Code 快速模型 / 备用模型 共 ${vscodeModelCount} 个可用`,
|
|
830
|
+
allowManual: true,
|
|
831
|
+
});
|
|
832
|
+
if (vscodeFastModel === '__BACK__') return;
|
|
833
|
+
|
|
834
|
+
const vscodeConfig = {
|
|
835
|
+
...config,
|
|
836
|
+
...effectiveConfig,
|
|
837
|
+
vscodeModel,
|
|
838
|
+
vscodeFastModel,
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const spinner = ora('写入 VS Code Claude Code 配置...').start();
|
|
842
|
+
let result;
|
|
843
|
+
try {
|
|
844
|
+
result = writeVsCodeIntegration(vscodeConfig, options.terminal ? { useTerminal: true } : {});
|
|
845
|
+
spinner.succeed(chalk.green('VS Code Claude Code 已接入'));
|
|
846
|
+
} catch (e) {
|
|
847
|
+
spinner.fail(chalk.red(`写入失败: ${e.message}`));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
console.log(boxen(
|
|
852
|
+
chalk.bold('VS Code Claude Code 扩展已配置\n\n') +
|
|
853
|
+
chalk.dim('共享设置 ') + chalk.cyan(result.claudeSettings.file) + '\n' +
|
|
854
|
+
chalk.dim('VS Code ') + chalk.cyan(result.vscodeSettings.file) + '\n' +
|
|
855
|
+
chalk.dim('主模型 ') + chalk.yellow(vscodeModel) + '\n' +
|
|
856
|
+
chalk.dim('快速模型 ') + chalk.yellow(vscodeFastModel) + '\n\n' +
|
|
857
|
+
chalk.white('已写入 Claude Code settings.json 的 env,并关闭 VS Code 扩展登录提示。\n') +
|
|
858
|
+
chalk.dim('如果 VS Code 已打开,请执行 Developer: Reload Window 或完全重启 VS Code。'),
|
|
859
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
860
|
+
));
|
|
861
|
+
|
|
862
|
+
const shouldOpen = await confirm({ message: '是否现在打开 Claude Code 的 VS Code 面板?', default: false });
|
|
863
|
+
if (shouldOpen) {
|
|
864
|
+
const openCommand = buildVsCodeOpenCommand(process.platform);
|
|
865
|
+
spawnSync(openCommand.command, openCommand.args, { stdio: 'ignore', windowsHide: true });
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
program
|
|
870
|
+
.command('vscode-reset')
|
|
871
|
+
.description('恢复 VS Code Claude Code 扩展默认配置')
|
|
872
|
+
.action(async () => {
|
|
873
|
+
const chalk = (await import('chalk')).default;
|
|
874
|
+
const ora = (await import('ora')).default;
|
|
875
|
+
const boxen = (await import('boxen')).default;
|
|
876
|
+
|
|
877
|
+
console.log(await getBanner());
|
|
878
|
+
|
|
879
|
+
const yes = await confirm({
|
|
880
|
+
message: chalk.red('确定要恢复 VS Code Claude Code 扩展默认配置吗?API 连接、终端和桌面配置不会被清除'),
|
|
881
|
+
default: false,
|
|
882
|
+
});
|
|
883
|
+
if (!yes) {
|
|
884
|
+
console.log(chalk.dim('已取消'));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const spinner = ora('正在恢复 VS Code Claude Code 默认配置...').start();
|
|
889
|
+
const result = clearVsCodeIntegration();
|
|
890
|
+
|
|
891
|
+
if (result.result === 'updated') {
|
|
892
|
+
spinner.succeed(chalk.green('VS Code Claude Code 已恢复默认'));
|
|
893
|
+
console.log(boxen(
|
|
894
|
+
chalk.bold('已清除以下 VS Code / Claude Code 扩展配置:\n\n') +
|
|
895
|
+
result.files.map(f => chalk.cyan(' • ' + f)).join('\n') +
|
|
896
|
+
'\n\n' + chalk.dim('如果 VS Code 已打开,请执行 Developer: Reload Window 或完全重启 VS Code。'),
|
|
897
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
898
|
+
));
|
|
899
|
+
} else {
|
|
900
|
+
spinner.warn(chalk.yellow('没有找到 VS Code Claude Code 的 yingclaw 配置,无需恢复'));
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
623
904
|
program
|
|
624
905
|
.command('switch')
|
|
625
906
|
.description('快速切换模型(只更新 API 连接)')
|
|
@@ -641,25 +922,19 @@ program
|
|
|
641
922
|
return;
|
|
642
923
|
}
|
|
643
924
|
|
|
644
|
-
const providerKey = await
|
|
645
|
-
|
|
646
|
-
choices: [
|
|
647
|
-
...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
|
|
648
|
-
{ name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
|
|
649
|
-
],
|
|
650
|
-
});
|
|
651
|
-
if (providerKey === '__BACK__') return;
|
|
925
|
+
const providerKey = await promptProviderKey(chalk);
|
|
926
|
+
if (!providerKey || providerKey === '__BACK__') return;
|
|
652
927
|
|
|
653
928
|
const provider = PROVIDERS[providerKey];
|
|
654
929
|
if (provider.custom) {
|
|
655
|
-
const customConfig = await configureCustomProvider({ chalk, ora, existingConfig: config });
|
|
930
|
+
const customConfig = await configureCustomProvider({ chalk, ora, existingConfig: config, providerKey });
|
|
656
931
|
if (!customConfig) return;
|
|
657
932
|
|
|
658
933
|
const spinner = ora('切换中...').start();
|
|
659
|
-
|
|
660
|
-
|
|
934
|
+
const savedConfig = ensureGatewayForOpenAiConfig(customConfig);
|
|
935
|
+
saveConfig(savedConfig);
|
|
936
|
+
spinner.succeed(chalk.green(`API 连接已切换至 ${savedConfig.providerName} · ${savedConfig.model}`));
|
|
661
937
|
console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
|
|
662
|
-
await offerDesktopSync(chalk, ora, customConfig);
|
|
663
938
|
return;
|
|
664
939
|
}
|
|
665
940
|
|
|
@@ -700,12 +975,12 @@ program
|
|
|
700
975
|
availableModels = provider.models.map(m => m.value);
|
|
701
976
|
}
|
|
702
977
|
|
|
703
|
-
const model = await
|
|
704
|
-
|
|
705
|
-
choices:
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
978
|
+
const model = await promptModelFromChoices({
|
|
979
|
+
chalk,
|
|
980
|
+
choices: modelChoices,
|
|
981
|
+
message: '选择模型',
|
|
982
|
+
backLabel: '↩ 返回主菜单',
|
|
983
|
+
allowManual: false,
|
|
709
984
|
});
|
|
710
985
|
if (model === '__BACK__') return;
|
|
711
986
|
|
|
@@ -715,7 +990,6 @@ program
|
|
|
715
990
|
saveConfig(newConfig);
|
|
716
991
|
spinner.succeed(chalk.green(`API 连接已切换至 ${provider.name} · ${model}`));
|
|
717
992
|
console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
|
|
718
|
-
await offerDesktopSync(chalk, ora, newConfig);
|
|
719
993
|
});
|
|
720
994
|
|
|
721
995
|
program
|
|
@@ -793,6 +1067,10 @@ program
|
|
|
793
1067
|
console.log(chalk.yellow('\nClaude 桌面应用 3P 配置目前仅支持 macOS / Windows。\n'));
|
|
794
1068
|
return;
|
|
795
1069
|
}
|
|
1070
|
+
if (options.direct && isOpenAiCompatibleConfig(config)) {
|
|
1071
|
+
console.log(chalk.yellow('\nOpenAI 兼容接口必须通过本机 Gateway 转换协议,不能使用直连模式。\n'));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
796
1074
|
|
|
797
1075
|
let desktopConfig = config;
|
|
798
1076
|
if (!options.direct) {
|
|
@@ -800,6 +1078,16 @@ program
|
|
|
800
1078
|
if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
|
|
801
1079
|
saveConfig(desktopConfig);
|
|
802
1080
|
}
|
|
1081
|
+
|
|
1082
|
+
const chatModels = Array.isArray(desktopConfig.availableModels)
|
|
1083
|
+
? desktopConfig.availableModels.filter(isDesktopChatModel)
|
|
1084
|
+
: [];
|
|
1085
|
+
if (chatModels.length > DESKTOP_MODEL_SELECT_THRESHOLD) {
|
|
1086
|
+
const selectedModels = await promptDesktopModels(chalk, desktopConfig, chatModels);
|
|
1087
|
+
if (!selectedModels) return;
|
|
1088
|
+
saveConfig({ ...desktopConfig, desktopModels: selectedModels });
|
|
1089
|
+
desktopConfig = { ...desktopConfig, availableModels: selectedModels };
|
|
1090
|
+
}
|
|
803
1091
|
}
|
|
804
1092
|
|
|
805
1093
|
const spinner = ora('写入 Claude 桌面应用配置...').start();
|
|
@@ -942,7 +1230,7 @@ program
|
|
|
942
1230
|
|
|
943
1231
|
program
|
|
944
1232
|
.command('reset')
|
|
945
|
-
.description('清除 API
|
|
1233
|
+
.description('清除 API 连接、终端环境变量、VS Code 和桌面配置')
|
|
946
1234
|
.action(async () => {
|
|
947
1235
|
const chalk = (await import('chalk')).default;
|
|
948
1236
|
const ora = (await import('ora')).default;
|
|
@@ -965,6 +1253,10 @@ program
|
|
|
965
1253
|
if (desktopCleared.result === 'updated') {
|
|
966
1254
|
cleared.push(...desktopConfigClearedPaths(desktopCleared));
|
|
967
1255
|
}
|
|
1256
|
+
const vscodeCleared = clearVsCodeIntegration();
|
|
1257
|
+
if (vscodeCleared.result === 'updated') {
|
|
1258
|
+
cleared.push(...vscodeCleared.files);
|
|
1259
|
+
}
|
|
968
1260
|
const autostartCleared = removeGatewayAutostart();
|
|
969
1261
|
if (autostartCleared.result === 'removed' && autostartCleared.file) {
|
|
970
1262
|
cleared.push(autostartCleared.file);
|
|
@@ -1105,15 +1397,18 @@ program
|
|
|
1105
1397
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
1106
1398
|
));
|
|
1107
1399
|
process.exit(0);
|
|
1400
|
+
} else if (result.status === null) {
|
|
1401
|
+
console.log(chalk.red('\nnpm 命令未找到,请确认 Node.js 和 npm 已正确安装后手动运行:'));
|
|
1402
|
+
console.log(chalk.cyan('npm install -g yingclaw@latest'));
|
|
1108
1403
|
} else {
|
|
1109
1404
|
console.log(chalk.red('\n升级失败,请手动运行:'));
|
|
1110
1405
|
console.log(chalk.cyan('npm install -g yingclaw@latest'));
|
|
1111
1406
|
}
|
|
1112
1407
|
});
|
|
1113
1408
|
|
|
1114
|
-
async function renderStatusBar(apiStatus) {
|
|
1409
|
+
async function renderStatusBar(apiStatus, config) {
|
|
1115
1410
|
const chalk = (await import('chalk')).default;
|
|
1116
|
-
|
|
1411
|
+
if (config === undefined) config = loadConfig();
|
|
1117
1412
|
const claudeInstalled = isClaudeInstalled();
|
|
1118
1413
|
|
|
1119
1414
|
const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
|
|
@@ -1131,11 +1426,12 @@ async function renderStatusBar(apiStatus) {
|
|
|
1131
1426
|
const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
|
|
1132
1427
|
cfgPart = statusLines.map((line, index) => {
|
|
1133
1428
|
if (index === 0) return line.replace('API 正常', chalk.green('API 正常')).replace('API Key 无效', chalk.red('API Key 无效')).replace('网络/服务异常', chalk.yellow('网络/服务异常'));
|
|
1134
|
-
if (line.startsWith('
|
|
1429
|
+
if (line.startsWith('终端未生效')) return chalk.yellow(line);
|
|
1135
1430
|
if (line.startsWith('旧模型名')) return chalk.yellow(line);
|
|
1431
|
+
if (line.startsWith('快速模型跨系列')) return chalk.yellow(line);
|
|
1136
1432
|
if (line.startsWith('Desktop Gateway 未运行')) return chalk.yellow(line);
|
|
1137
1433
|
if (line.startsWith('Desktop Gateway 已运行')) return chalk.green(line);
|
|
1138
|
-
if (line.startsWith('
|
|
1434
|
+
if (line.startsWith('模型')) return line.replace(view.mainModel, chalk.yellow(view.mainModel)).replace(view.fastModel, chalk.yellow(view.fastModel));
|
|
1139
1435
|
return line;
|
|
1140
1436
|
}).join('\n ');
|
|
1141
1437
|
} else {
|
|
@@ -1198,6 +1494,7 @@ async function runAdvancedMenu(chalk, hasConfig) {
|
|
|
1198
1494
|
{ name: '🩺 诊断(一键自检并给出修复建议)', value: 'doctor' },
|
|
1199
1495
|
{ name: '🔁 重新检测 API', value: 'recheck', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
|
|
1200
1496
|
{ name: '↩️ 恢复 Claude Code 终端默认', value: 'code-reset' },
|
|
1497
|
+
{ name: '↩️ 恢复 VS Code Claude Code 默认', value: 'vscode-reset' },
|
|
1201
1498
|
{ name: '↩️ 恢复 Claude 桌面默认', value: 'desktop-reset' },
|
|
1202
1499
|
{ name: '🗑 清除所有 yingclaw 配置', value: 'reset' },
|
|
1203
1500
|
{ name: '⬆️ 检查更新', value: 'update' },
|
|
@@ -1239,7 +1536,7 @@ async function runMenu() {
|
|
|
1239
1536
|
console.log(chalk.red(` ● 配置无效:${configProblem}`));
|
|
1240
1537
|
console.log(chalk.dim(' 请先选择"配置 API 连接"重新配置'));
|
|
1241
1538
|
} else {
|
|
1242
|
-
console.log(await renderStatusBar(apiStatus));
|
|
1539
|
+
console.log(await renderStatusBar(apiStatus, config));
|
|
1243
1540
|
}
|
|
1244
1541
|
console.log();
|
|
1245
1542
|
|
|
@@ -1253,6 +1550,7 @@ async function runMenu() {
|
|
|
1253
1550
|
{ name: config ? '🔑 重新配置 API 连接' : '🔑 配置 API 连接', value: 'config' },
|
|
1254
1551
|
{ name: '🔄 切换厂商或模型', value: 'switch', disabled: disabledHint },
|
|
1255
1552
|
{ name: '💻 接入 Claude Code 终端', value: 'code', disabled: disabledHint },
|
|
1553
|
+
{ name: '🧩 接入 VS Code Claude Code 扩展', value: 'vscode', disabled: disabledHint },
|
|
1256
1554
|
{ name: '🖥 接入 Claude 桌面应用', value: 'desktop', disabled: disabledHint },
|
|
1257
1555
|
{ name: '📊 查看当前配置', value: 'status', disabled: !config && '需先配置 API 连接' },
|
|
1258
1556
|
{ name: '🛠 高级 ›', value: 'advanced' },
|
|
@@ -1276,8 +1574,13 @@ async function runMenu() {
|
|
|
1276
1574
|
}
|
|
1277
1575
|
|
|
1278
1576
|
if (resolvedAction === 'launch') {
|
|
1279
|
-
|
|
1577
|
+
let cfg = loadConfig();
|
|
1280
1578
|
if (!cfg || getConfigValidationMessage(cfg)) continue;
|
|
1579
|
+
const effectiveConfig = ensureGatewayForOpenAiConfig(cfg);
|
|
1580
|
+
if (effectiveConfig !== cfg) {
|
|
1581
|
+
saveConfig(effectiveConfig);
|
|
1582
|
+
cfg = effectiveConfig;
|
|
1583
|
+
}
|
|
1281
1584
|
await new Promise((resolve) => {
|
|
1282
1585
|
const child = spawn('claude', [], {
|
|
1283
1586
|
stdio: 'inherit',
|
|
@@ -1297,7 +1600,9 @@ async function runMenu() {
|
|
|
1297
1600
|
install: 'install-claude',
|
|
1298
1601
|
config: 'config',
|
|
1299
1602
|
code: 'code',
|
|
1603
|
+
vscode: 'vscode',
|
|
1300
1604
|
'code-reset': 'code-reset',
|
|
1605
|
+
'vscode-reset': 'vscode-reset',
|
|
1301
1606
|
switch: 'switch',
|
|
1302
1607
|
desktop: 'desktop',
|
|
1303
1608
|
'desktop-reset': 'desktop-reset',
|
|
@@ -1315,7 +1620,7 @@ async function runMenu() {
|
|
|
1315
1620
|
});
|
|
1316
1621
|
|
|
1317
1622
|
// 改 config 的命令需要刷新缓存
|
|
1318
|
-
if (['config', 'switch', 'reset', 'code-reset', 'desktop-reset'].includes(resolvedAction)) {
|
|
1623
|
+
if (['config', 'switch', 'reset', 'code-reset', 'vscode-reset', 'desktop-reset', 'vscode'].includes(resolvedAction)) {
|
|
1319
1624
|
lastCheckResult = undefined;
|
|
1320
1625
|
lastCheckedHash = null;
|
|
1321
1626
|
}
|