yingclaw 2.5.25 → 2.5.27
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 +5 -1
- package/bin/cli.js +187 -50
- package/lib/config.js +75 -15
- package/lib/doctor.js +1 -1
- package/lib/gateway.js +36 -4
- package/lib/install.js +7 -3
- package/lib/openai.js +160 -18
- package/lib/panel.js +3 -1
- package/lib/vscode.js +48 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Claude Code × 国产大模型,一键接入。
|
|
4
4
|
|
|
5
|
-
支持 DeepSeek、Kimi、火山方舟 Coding Plan、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo,也支持自定义 Anthropic 兼容接口和自定义 OpenAI 兼容接口,无需梯子即可使用 Claude Code。
|
|
5
|
+
支持 DeepSeek、Kimi、火山方舟 Coding Plan、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo、B.AI、OpenCode Zen/Go,也支持自定义 Anthropic 兼容接口和自定义 OpenAI 兼容接口,无需梯子即可使用 Claude Code。
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -69,6 +69,10 @@ claw desktop
|
|
|
69
69
|
| MiniMax | MiniMax-M2.7 | MiniMax-M2.7-Turbo |
|
|
70
70
|
| 智谱 GLM | GLM-4.7 | GLM-5-Turbo |
|
|
71
71
|
| 小米 MiMo | mimo-v2.5-pro | mimo-v2.5 |
|
|
72
|
+
| B.AI | claude-sonnet-4.6 | claude-haiku-4.5 |
|
|
73
|
+
| OpenCode Zen(按量) | claude-sonnet-4-6 | deepseek-v4-flash-free |
|
|
74
|
+
| OpenCode Go(订阅制) | glm-5.1 | deepseek-v4-flash |
|
|
75
|
+
| OpenRouter | deepseek/deepseek-r1-0528 | deepseek/deepseek-chat-v3-0324 |
|
|
72
76
|
| 自定义 Anthropic 兼容接口 | 自动获取或手动输入 | 手动选择 |
|
|
73
77
|
| 自定义 OpenAI 兼容接口 | 自动获取或手动输入 | 手动选择 |
|
|
74
78
|
|
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,
|
|
@@ -54,16 +54,19 @@ const { normalizeOpenAiBaseUrl } = require('../lib/openai');
|
|
|
54
54
|
|
|
55
55
|
const program = new Command();
|
|
56
56
|
|
|
57
|
+
let _bannerCache;
|
|
57
58
|
async function getBanner() {
|
|
59
|
+
if (_bannerCache) return _bannerCache;
|
|
58
60
|
const chalk = (await import('chalk')).default;
|
|
59
61
|
const figlet = require('figlet');
|
|
60
62
|
const boxen = (await import('boxen')).default;
|
|
61
63
|
const title = figlet.textSync('yingclaw', { font: 'Small', horizontalLayout: 'fitted' });
|
|
62
64
|
const subtitle = chalk.dim('Claude Code × 国产大模型 一键接入') + ' ' + chalk.cyan(`v${pkg.version}`);
|
|
63
|
-
|
|
65
|
+
_bannerCache = boxen(
|
|
64
66
|
chalk.cyan.bold(title) + '\n' + subtitle,
|
|
65
67
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 0 } }
|
|
66
68
|
);
|
|
69
|
+
return _bannerCache;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
function getConfigValidationMessage(config) {
|
|
@@ -164,15 +167,36 @@ async function offerDesktopSync(chalk, ora, config) {
|
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
|
|
170
|
+
const SEARCH_THRESHOLD = 15;
|
|
171
|
+
|
|
167
172
|
async function promptModelFromChoices({ chalk, choices, message, backLabel = '↩ 返回上一步', allowManual = true }) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
|
|
176
200
|
if (selected === '__BACK__') return '__BACK__';
|
|
177
201
|
if (selected !== '__MANUAL__') return selected;
|
|
178
202
|
|
|
@@ -182,10 +206,115 @@ async function promptModelFromChoices({ chalk, choices, message, backLabel = '
|
|
|
182
206
|
}).then(v => v.trim());
|
|
183
207
|
}
|
|
184
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
|
+
|
|
185
274
|
function getChatModelChoices(models) {
|
|
186
275
|
return models.filter(isDesktopChatModel).map(id => ({ name: id, value: id }));
|
|
187
276
|
}
|
|
188
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
|
+
|
|
189
318
|
function sortModelChoicesByRelatedFamily(choices, mainModel) {
|
|
190
319
|
const order = sortRelatedModelIds(choices.map((choice) => choice.value), mainModel);
|
|
191
320
|
const rank = new Map(order.map((value, index) => [value, index]));
|
|
@@ -384,14 +513,8 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
|
|
|
384
513
|
|
|
385
514
|
while (true) {
|
|
386
515
|
if (step === 'provider') {
|
|
387
|
-
providerKey = await
|
|
388
|
-
|
|
389
|
-
choices: [
|
|
390
|
-
...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
|
|
391
|
-
{ name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
|
|
392
|
-
],
|
|
393
|
-
});
|
|
394
|
-
if (providerKey === '__BACK__') return;
|
|
516
|
+
providerKey = await promptProviderKey(chalk);
|
|
517
|
+
if (!providerKey || providerKey === '__BACK__') return;
|
|
395
518
|
provider = PROVIDERS[providerKey];
|
|
396
519
|
if (provider.custom) {
|
|
397
520
|
customConfig = await configureCustomProvider({ chalk, ora, providerKey });
|
|
@@ -429,12 +552,12 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
|
|
|
429
552
|
availableModels = provider.models.map(m => m.value);
|
|
430
553
|
}
|
|
431
554
|
|
|
432
|
-
const m = await
|
|
433
|
-
|
|
434
|
-
choices:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
555
|
+
const m = await promptModelFromChoices({
|
|
556
|
+
chalk,
|
|
557
|
+
choices: modelChoices,
|
|
558
|
+
message: '选择模型',
|
|
559
|
+
backLabel: '↩ 返回上一步(重新输入 Key)',
|
|
560
|
+
allowManual: false,
|
|
438
561
|
});
|
|
439
562
|
if (m === '__BACK__') { step = 'apikey'; continue; }
|
|
440
563
|
model = m;
|
|
@@ -474,7 +597,6 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
|
|
|
474
597
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
475
598
|
));
|
|
476
599
|
|
|
477
|
-
await offerDesktopSync(chalk, ora, cfg);
|
|
478
600
|
}
|
|
479
601
|
|
|
480
602
|
program
|
|
@@ -613,12 +735,17 @@ program
|
|
|
613
735
|
}
|
|
614
736
|
|
|
615
737
|
console.log(chalk.dim(getStorageHint(file)));
|
|
616
|
-
const displayedConfig =
|
|
738
|
+
const displayedConfig = effectiveConfig;
|
|
739
|
+
const needsGatewayHint = process.platform !== 'darwin' && process.platform !== 'win32'
|
|
740
|
+
&& isOpenAiCompatibleConfig(effectiveConfig);
|
|
617
741
|
console.log(boxen(
|
|
618
742
|
chalk.bold('Claude Code 终端已接入\n\n') +
|
|
619
743
|
chalk.dim('Base URL ') + chalk.cyan(buildClaudeEnv(displayedConfig).ANTHROPIC_BASE_URL) + '\n' +
|
|
620
744
|
chalk.dim('模型 ') + chalk.yellow(displayedConfig.model) + '\n\n' +
|
|
621
|
-
|
|
745
|
+
(needsGatewayHint
|
|
746
|
+
? chalk.yellow('注:OpenAI 兼容接口需要本机 Gateway,请在新终端运行 ') + chalk.cyan.bold('claw gateway') + chalk.yellow(' 并保持运行。\n\n')
|
|
747
|
+
: '') +
|
|
748
|
+
chalk.white('需要启动时,在主菜单选择”启动 Claude Code”,或直接输入 ') + chalk.cyan.bold('claude') + '\n' +
|
|
622
749
|
chalk.dim(getActivationHint(file)),
|
|
623
750
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
624
751
|
));
|
|
@@ -692,10 +819,11 @@ program
|
|
|
692
819
|
if (effectiveConfig !== config) saveConfig(effectiveConfig);
|
|
693
820
|
|
|
694
821
|
const vscodeModelChoices = getChatModelChoices(buildVsCodeModelSelectionIds(effectiveConfig));
|
|
822
|
+
const vscodeModelCount = vscodeModelChoices.length;
|
|
695
823
|
const vscodeModel = await promptModelFromChoices({
|
|
696
824
|
chalk,
|
|
697
825
|
choices: vscodeModelChoices,
|
|
698
|
-
message:
|
|
826
|
+
message: `选择 VS Code Claude Code 主模型 共 ${vscodeModelCount} 个可用`,
|
|
699
827
|
allowManual: true,
|
|
700
828
|
});
|
|
701
829
|
if (vscodeModel === '__BACK__') return;
|
|
@@ -703,7 +831,7 @@ program
|
|
|
703
831
|
const vscodeFastModel = await promptModelFromChoices({
|
|
704
832
|
chalk,
|
|
705
833
|
choices: sortModelChoicesByRelatedFamily(vscodeModelChoices, vscodeModel),
|
|
706
|
-
message:
|
|
834
|
+
message: `选择 VS Code Claude Code 快速模型 / 备用模型 共 ${vscodeModelCount} 个可用`,
|
|
707
835
|
allowManual: true,
|
|
708
836
|
});
|
|
709
837
|
if (vscodeFastModel === '__BACK__') return;
|
|
@@ -725,12 +853,17 @@ program
|
|
|
725
853
|
return;
|
|
726
854
|
}
|
|
727
855
|
|
|
856
|
+
const vscodeNeedsGatewayHint = process.platform !== 'darwin' && process.platform !== 'win32'
|
|
857
|
+
&& isOpenAiCompatibleConfig(effectiveConfig);
|
|
728
858
|
console.log(boxen(
|
|
729
859
|
chalk.bold('VS Code Claude Code 扩展已配置\n\n') +
|
|
730
860
|
chalk.dim('共享设置 ') + chalk.cyan(result.claudeSettings.file) + '\n' +
|
|
731
861
|
chalk.dim('VS Code ') + chalk.cyan(result.vscodeSettings.file) + '\n' +
|
|
732
862
|
chalk.dim('主模型 ') + chalk.yellow(vscodeModel) + '\n' +
|
|
733
863
|
chalk.dim('快速模型 ') + chalk.yellow(vscodeFastModel) + '\n\n' +
|
|
864
|
+
(vscodeNeedsGatewayHint
|
|
865
|
+
? chalk.yellow('注:OpenAI 兼容接口需要本机 Gateway,请在新终端运行 ') + chalk.cyan.bold('claw gateway') + chalk.yellow(' 并保持运行。\n\n')
|
|
866
|
+
: '') +
|
|
734
867
|
chalk.white('已写入 Claude Code settings.json 的 env,并关闭 VS Code 扩展登录提示。\n') +
|
|
735
868
|
chalk.dim('如果 VS Code 已打开,请执行 Developer: Reload Window 或完全重启 VS Code。'),
|
|
736
869
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
@@ -799,14 +932,8 @@ program
|
|
|
799
932
|
return;
|
|
800
933
|
}
|
|
801
934
|
|
|
802
|
-
const providerKey = await
|
|
803
|
-
|
|
804
|
-
choices: [
|
|
805
|
-
...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
|
|
806
|
-
{ name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
|
|
807
|
-
],
|
|
808
|
-
});
|
|
809
|
-
if (providerKey === '__BACK__') return;
|
|
935
|
+
const providerKey = await promptProviderKey(chalk);
|
|
936
|
+
if (!providerKey || providerKey === '__BACK__') return;
|
|
810
937
|
|
|
811
938
|
const provider = PROVIDERS[providerKey];
|
|
812
939
|
if (provider.custom) {
|
|
@@ -818,7 +945,6 @@ program
|
|
|
818
945
|
saveConfig(savedConfig);
|
|
819
946
|
spinner.succeed(chalk.green(`API 连接已切换至 ${savedConfig.providerName} · ${savedConfig.model}`));
|
|
820
947
|
console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
|
|
821
|
-
await offerDesktopSync(chalk, ora, savedConfig);
|
|
822
948
|
return;
|
|
823
949
|
}
|
|
824
950
|
|
|
@@ -859,12 +985,12 @@ program
|
|
|
859
985
|
availableModels = provider.models.map(m => m.value);
|
|
860
986
|
}
|
|
861
987
|
|
|
862
|
-
const model = await
|
|
863
|
-
|
|
864
|
-
choices:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
988
|
+
const model = await promptModelFromChoices({
|
|
989
|
+
chalk,
|
|
990
|
+
choices: modelChoices,
|
|
991
|
+
message: '选择模型',
|
|
992
|
+
backLabel: '↩ 返回主菜单',
|
|
993
|
+
allowManual: false,
|
|
868
994
|
});
|
|
869
995
|
if (model === '__BACK__') return;
|
|
870
996
|
|
|
@@ -874,7 +1000,6 @@ program
|
|
|
874
1000
|
saveConfig(newConfig);
|
|
875
1001
|
spinner.succeed(chalk.green(`API 连接已切换至 ${provider.name} · ${model}`));
|
|
876
1002
|
console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
|
|
877
|
-
await offerDesktopSync(chalk, ora, newConfig);
|
|
878
1003
|
});
|
|
879
1004
|
|
|
880
1005
|
program
|
|
@@ -963,6 +1088,16 @@ program
|
|
|
963
1088
|
if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
|
|
964
1089
|
saveConfig(desktopConfig);
|
|
965
1090
|
}
|
|
1091
|
+
|
|
1092
|
+
const chatModels = Array.isArray(desktopConfig.availableModels)
|
|
1093
|
+
? desktopConfig.availableModels.filter(isDesktopChatModel)
|
|
1094
|
+
: [];
|
|
1095
|
+
if (chatModels.length > DESKTOP_MODEL_SELECT_THRESHOLD) {
|
|
1096
|
+
const selectedModels = await promptDesktopModels(chalk, desktopConfig, chatModels);
|
|
1097
|
+
if (!selectedModels) return;
|
|
1098
|
+
saveConfig({ ...desktopConfig, desktopModels: selectedModels });
|
|
1099
|
+
desktopConfig = { ...desktopConfig, availableModels: selectedModels };
|
|
1100
|
+
}
|
|
966
1101
|
}
|
|
967
1102
|
|
|
968
1103
|
const spinner = ora('写入 Claude 桌面应用配置...').start();
|
|
@@ -1273,14 +1408,16 @@ program
|
|
|
1273
1408
|
));
|
|
1274
1409
|
process.exit(0);
|
|
1275
1410
|
} else {
|
|
1276
|
-
|
|
1277
|
-
console.log(chalk.
|
|
1411
|
+
const hints = getInstallFailureHints(result, chalk);
|
|
1412
|
+
console.log(chalk.red('\n升级失败'));
|
|
1413
|
+
if (hints.length > 0) console.log(...hints);
|
|
1414
|
+
console.log(chalk.dim('\n手动运行:') + chalk.cyan('npm install -g yingclaw@latest'));
|
|
1278
1415
|
}
|
|
1279
1416
|
});
|
|
1280
1417
|
|
|
1281
|
-
async function renderStatusBar(apiStatus) {
|
|
1418
|
+
async function renderStatusBar(apiStatus, config) {
|
|
1282
1419
|
const chalk = (await import('chalk')).default;
|
|
1283
|
-
|
|
1420
|
+
if (config === undefined) config = loadConfig();
|
|
1284
1421
|
const claudeInstalled = isClaudeInstalled();
|
|
1285
1422
|
|
|
1286
1423
|
const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
|
|
@@ -1408,7 +1545,7 @@ async function runMenu() {
|
|
|
1408
1545
|
console.log(chalk.red(` ● 配置无效:${configProblem}`));
|
|
1409
1546
|
console.log(chalk.dim(' 请先选择"配置 API 连接"重新配置'));
|
|
1410
1547
|
} else {
|
|
1411
|
-
console.log(await renderStatusBar(apiStatus));
|
|
1548
|
+
console.log(await renderStatusBar(apiStatus, config));
|
|
1412
1549
|
}
|
|
1413
1550
|
console.log();
|
|
1414
1551
|
|
package/lib/config.js
CHANGED
|
@@ -109,6 +109,66 @@ const PROVIDERS = {
|
|
|
109
109
|
{ name: 'MiMo V2 Flash(快速)', value: 'mimo-v2-flash' },
|
|
110
110
|
],
|
|
111
111
|
},
|
|
112
|
+
bai: {
|
|
113
|
+
name: 'B.AI',
|
|
114
|
+
baseUrl: 'https://api.b.ai',
|
|
115
|
+
modelsUrl: 'https://api.b.ai/v1/models',
|
|
116
|
+
fastModel: 'claude-haiku-4.5',
|
|
117
|
+
models: [
|
|
118
|
+
{ name: 'Claude Sonnet 4.6(推荐)', value: 'claude-sonnet-4.6' },
|
|
119
|
+
{ name: 'Claude Opus 4.7(强力)', value: 'claude-opus-4.7' },
|
|
120
|
+
{ name: 'Claude Haiku 4.5(快速)', value: 'claude-haiku-4.5' },
|
|
121
|
+
{ name: 'DeepSeek V4 Pro', value: 'deepseek-v4-pro' },
|
|
122
|
+
{ name: 'GLM-5.1', value: 'glm-5.1' },
|
|
123
|
+
{ name: 'MiniMax M2.7', value: 'minimax-m2.7' },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
opencode_zen: {
|
|
127
|
+
name: 'OpenCode Zen(按量)',
|
|
128
|
+
protocol: 'openai',
|
|
129
|
+
baseUrl: 'https://opencode.ai/zen',
|
|
130
|
+
modelsUrl: 'https://opencode.ai/zen/v1/models',
|
|
131
|
+
fastModel: 'deepseek-v4-flash-free',
|
|
132
|
+
models: [
|
|
133
|
+
{ name: 'Claude Sonnet 4.6(推荐)', value: 'claude-sonnet-4-6' },
|
|
134
|
+
{ name: 'Claude Opus 4.7(强力)', value: 'claude-opus-4-7' },
|
|
135
|
+
{ name: 'Claude Haiku 4.5(快速)', value: 'claude-haiku-4-5' },
|
|
136
|
+
{ name: 'GLM 5.1', value: 'glm-5.1' },
|
|
137
|
+
{ name: 'MiniMax M2.7', value: 'minimax-m2.7' },
|
|
138
|
+
{ name: 'Kimi K2.5', value: 'kimi-k2.5' },
|
|
139
|
+
{ name: 'DeepSeek V4 Flash(免费)', value: 'deepseek-v4-flash-free' },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
opencode_go: {
|
|
143
|
+
name: 'OpenCode Go(订阅制)',
|
|
144
|
+
protocol: 'openai',
|
|
145
|
+
baseUrl: 'https://opencode.ai/zen/go',
|
|
146
|
+
modelsUrl: 'https://opencode.ai/zen/go/v1/models',
|
|
147
|
+
fastModel: 'deepseek-v4-flash',
|
|
148
|
+
models: [
|
|
149
|
+
{ name: 'GLM-5.1', value: 'glm-5.1' },
|
|
150
|
+
{ name: 'Kimi K2.5', value: 'kimi-k2.5' },
|
|
151
|
+
{ name: 'DeepSeek V4 Pro(强力)', value: 'deepseek-v4-pro' },
|
|
152
|
+
{ name: 'DeepSeek V4 Flash(快速)', value: 'deepseek-v4-flash' },
|
|
153
|
+
{ name: 'MiniMax M2.7', value: 'minimax-m2.7' },
|
|
154
|
+
{ name: 'MiniMax M2.5', value: 'minimax-m2.5' },
|
|
155
|
+
{ name: 'MiMo V2.5 Pro', value: 'mimo-v2.5-pro' },
|
|
156
|
+
{ name: 'Qwen3.6 Plus', value: 'qwen3.6-plus' },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
openrouter: {
|
|
160
|
+
name: 'OpenRouter',
|
|
161
|
+
protocol: 'openai',
|
|
162
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
163
|
+
modelsUrl: 'https://openrouter.ai/api/v1/models',
|
|
164
|
+
fastModel: 'deepseek/deepseek-chat-v3-0324',
|
|
165
|
+
models: [
|
|
166
|
+
{ name: 'DeepSeek R1 0528(推理)', value: 'deepseek/deepseek-r1-0528' },
|
|
167
|
+
{ name: 'DeepSeek Chat V3(快速)', value: 'deepseek/deepseek-chat-v3-0324' },
|
|
168
|
+
{ name: 'Gemini 2.5 Pro(Google)', value: 'google/gemini-2.5-pro' },
|
|
169
|
+
{ name: 'Claude Sonnet 4.5(Anthropic)', value: 'anthropic/claude-sonnet-4-5' },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
112
172
|
custom: {
|
|
113
173
|
name: '自定义 Anthropic 兼容接口',
|
|
114
174
|
custom: true,
|
|
@@ -128,6 +188,8 @@ const PREFERRED_MODEL_IDS = {
|
|
|
128
188
|
minimax: ['MiniMax-M2.7', 'MiniMax-M2.7-Turbo', 'MiniMax-M2.5'],
|
|
129
189
|
glm: ['GLM-4.7', 'GLM-5.1', 'GLM-5-Turbo', 'GLM-4.5-Air'],
|
|
130
190
|
mimo: ['mimo-v2.5-pro', 'mimo-v2.5', 'mimo-v2-flash'],
|
|
191
|
+
opencode_zen: ['claude-sonnet-4-6', 'claude-opus-4-7', 'claude-haiku-4-5', 'deepseek-v4-flash-free'],
|
|
192
|
+
opencode_go: ['glm-5.1', 'deepseek-v4-pro', 'deepseek-v4-flash'],
|
|
131
193
|
};
|
|
132
194
|
|
|
133
195
|
function sortPreferredModelIds(providerKey, ids) {
|
|
@@ -143,7 +205,7 @@ function isChatModelId(model) {
|
|
|
143
205
|
const value = String(model || '');
|
|
144
206
|
return !/(^|[-_])(tts|voice|voiceclone|voicedesign|speech|audio|image|video|embedding|embed|rerank|moderation)([-_]|$)/i.test(value)
|
|
145
207
|
&& !/(^|[-_])(i2v|t2v|t2i|i2i|v2v|video-generation|image-generation)([-_]|$)/i.test(value)
|
|
146
|
-
&& !/(cogview|cogvideo|wanx|^wan\d|seedance|seedream|seededit|vision|hyper3d|hitem3d|seed3d|flux|sdxl|stable-diffusion)/i.test(value);
|
|
208
|
+
&& !/(cogview|cogvideo|wanx|^wan\d|seedance|seedream|seededit|vision|hyper3d|hitem3d|seed3d|flux|sdxl|stable-diffusion|dall-e|whisper)/i.test(value);
|
|
147
209
|
}
|
|
148
210
|
|
|
149
211
|
function normalizeModelIds(providerKey, ids) {
|
|
@@ -185,6 +247,9 @@ function parseModelIdsResponse(providerKey, data) {
|
|
|
185
247
|
if (providerKey === 'volcengine') {
|
|
186
248
|
return normalizeModelIds(providerKey, ids.filter(isChatModelId));
|
|
187
249
|
}
|
|
250
|
+
if (providerKey === 'bai') {
|
|
251
|
+
return normalizeModelIds(providerKey, ids.filter(id => !id.includes('/')));
|
|
252
|
+
}
|
|
188
253
|
return normalizeModelIds(providerKey, ids);
|
|
189
254
|
}
|
|
190
255
|
|
|
@@ -403,15 +468,17 @@ function runWindowsEnvCommands(commands, runner = spawnSync, { ignoreErrors = fa
|
|
|
403
468
|
|
|
404
469
|
function classifyValidationStatus(statusCode) {
|
|
405
470
|
if (statusCode >= 200 && statusCode < 300) return true;
|
|
471
|
+
if (statusCode === 429) return true; // 限流 = Key 有效
|
|
406
472
|
if (statusCode === 401 || statusCode === 403) return false;
|
|
407
473
|
return null;
|
|
408
474
|
}
|
|
409
475
|
|
|
410
476
|
// 发一次最小的 /v1/messages 请求验证 Key(true=有效, false=无效, null=网络/服务异常)
|
|
411
477
|
async function validateKey(config, options = {}) {
|
|
478
|
+
const protocol = getProviderProtocol(config);
|
|
412
479
|
let url;
|
|
413
480
|
try {
|
|
414
|
-
url =
|
|
481
|
+
url = protocol === 'openai'
|
|
415
482
|
? openAiChatCompletionsUrl(config.baseUrl)
|
|
416
483
|
: `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
|
|
417
484
|
new URL(url);
|
|
@@ -428,20 +495,13 @@ async function validateKey(config, options = {}) {
|
|
|
428
495
|
headers: {
|
|
429
496
|
'content-type': 'application/json',
|
|
430
497
|
...buildProviderAuthHeaders(config.provider, config.apiKey),
|
|
431
|
-
...(
|
|
432
|
-
...(getProviderProtocol(config) === 'anthropic' ? { 'anthropic-version': '2023-06-01' } : {}),
|
|
498
|
+
...(protocol === 'anthropic' ? { 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' } : {}),
|
|
433
499
|
},
|
|
434
|
-
body: JSON.stringify(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
: {
|
|
441
|
-
model: config.model,
|
|
442
|
-
max_tokens: 1,
|
|
443
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
444
|
-
}),
|
|
500
|
+
body: JSON.stringify({
|
|
501
|
+
model: config.model,
|
|
502
|
+
max_tokens: 16,
|
|
503
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
504
|
+
}),
|
|
445
505
|
});
|
|
446
506
|
return classifyValidationStatus(res.status);
|
|
447
507
|
} catch {
|
package/lib/doctor.js
CHANGED
|
@@ -110,7 +110,7 @@ async function runDoctorChecks(options = {}) {
|
|
|
110
110
|
checks.push({
|
|
111
111
|
name: 'VS Code Claude Code',
|
|
112
112
|
status: vscodeSettings.configured ? STATUS_OK : STATUS_WARN,
|
|
113
|
-
message: vscodeSettings.configured ? '
|
|
113
|
+
message: vscodeSettings.configured ? 'VS Code 扩展设置已配置' : `${vscodeSettings.missing.length} 项配置未写入 VS Code 扩展设置`,
|
|
114
114
|
fix: vscodeSettings.configured ? null : '运行 claw vscode,并重启 VS Code',
|
|
115
115
|
});
|
|
116
116
|
|
package/lib/gateway.js
CHANGED
|
@@ -74,6 +74,25 @@ function isDesktopChatModel(model) {
|
|
|
74
74
|
return isChatModelId(model);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function isNativeClaudeModel(modelId) {
|
|
78
|
+
return /^claude-/i.test(String(modelId || ''));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stripThinkingFromBody(body) {
|
|
82
|
+
const cleaned = { ...body };
|
|
83
|
+
delete cleaned.thinking;
|
|
84
|
+
if (Array.isArray(cleaned.messages)) {
|
|
85
|
+
cleaned.messages = cleaned.messages.map((msg) => {
|
|
86
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
87
|
+
const content = msg.content.filter(
|
|
88
|
+
(block) => block.type !== 'thinking' && block.type !== 'redacted_thinking',
|
|
89
|
+
);
|
|
90
|
+
return { ...msg, content: content.length > 0 ? content : [{ type: 'text', text: '' }] };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return cleaned;
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
function buildDesktopGatewayRoutes(config) {
|
|
78
97
|
const fastModel = config.fastModel || config.model;
|
|
79
98
|
const configuredModels = [config.model, fastModel];
|
|
@@ -224,6 +243,10 @@ async function proxyMessages(req, res, config) {
|
|
|
224
243
|
return;
|
|
225
244
|
}
|
|
226
245
|
|
|
246
|
+
if (!isNativeClaudeModel(body.model)) {
|
|
247
|
+
body = stripThinkingFromBody(body);
|
|
248
|
+
}
|
|
249
|
+
|
|
227
250
|
if (getProviderProtocol(config) === 'openai') {
|
|
228
251
|
await proxyOpenAiMessages(res, config, body);
|
|
229
252
|
return;
|
|
@@ -306,10 +329,8 @@ async function proxyOpenAiMessages(res, config, body) {
|
|
|
306
329
|
const state = { model: body.model, started: false, finished: false };
|
|
307
330
|
let buffer = '';
|
|
308
331
|
const decoder = new TextDecoder();
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const frames = buffer.split(/\n\n/);
|
|
312
|
-
buffer = frames.pop() || '';
|
|
332
|
+
|
|
333
|
+
function processFrames(frames) {
|
|
313
334
|
for (const frame of frames) {
|
|
314
335
|
const dataLine = frame.split(/\n/).find((line) => line.startsWith('data:'));
|
|
315
336
|
if (!dataLine) continue;
|
|
@@ -324,6 +345,17 @@ async function proxyOpenAiMessages(res, config, body) {
|
|
|
324
345
|
res.write(openAiStreamChunkToAnthropicEvents(parsed, state));
|
|
325
346
|
}
|
|
326
347
|
}
|
|
348
|
+
|
|
349
|
+
for await (const chunk of upstream.body) {
|
|
350
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
351
|
+
const frames = buffer.split(/\n\n/);
|
|
352
|
+
buffer = frames.pop() || '';
|
|
353
|
+
processFrames(frames);
|
|
354
|
+
}
|
|
355
|
+
// flush any remaining buffer content not terminated by \n\n
|
|
356
|
+
if (buffer.trim()) {
|
|
357
|
+
processFrames([buffer]);
|
|
358
|
+
}
|
|
327
359
|
if (!state.finished) {
|
|
328
360
|
res.write(openAiStreamChunkToAnthropicEvents({ model: body.model, choices: [{ delta: {}, finish_reason: 'stop' }] }, state));
|
|
329
361
|
}
|
package/lib/install.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const path = require('path');
|
|
1
2
|
const { execSync } = require('child_process');
|
|
2
3
|
|
|
3
4
|
function buildClaudeInstallCommand(network) {
|
|
@@ -20,9 +21,12 @@ function quoteWindowsBatchArg(value) {
|
|
|
20
21
|
return `"${String(value).replace(/"/g, '""')}"`;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function buildWindowsYingclawUpgradeScript(network) {
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
function buildWindowsYingclawUpgradeScript(network, options = {}) {
|
|
25
|
+
const nodePath = options.nodePath || process.execPath;
|
|
26
|
+
// 用绝对路径避免 PATH 里的残留 npm 被优先找到(System32 等位置)
|
|
27
|
+
const npmCmd = options.npmCmd || path.win32.join(path.win32.dirname(nodePath), 'npm.cmd');
|
|
28
|
+
const { args } = buildYingclawUpgradeCommand(network);
|
|
29
|
+
const commandLine = `call ${quoteWindowsBatchArg(npmCmd)} ${args.map(quoteWindowsBatchArg).join(' ')}`;
|
|
26
30
|
return [
|
|
27
31
|
'@echo off',
|
|
28
32
|
'chcp 65001 >nul',
|
package/lib/openai.js
CHANGED
|
@@ -44,13 +44,74 @@ function normalizeSystemMessages(system) {
|
|
|
44
44
|
return [];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function anthropicToolUseToOpenAi(block) {
|
|
48
|
+
return {
|
|
49
|
+
id: block.id,
|
|
50
|
+
type: 'function',
|
|
51
|
+
function: {
|
|
52
|
+
name: block.name,
|
|
53
|
+
arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {}),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function anthropicToolResultContentToText(content) {
|
|
59
|
+
if (typeof content === 'string') return content;
|
|
60
|
+
if (Array.isArray(content)) {
|
|
61
|
+
return content.filter(p => p?.type === 'text').map(p => p.text || '').join('\n');
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function anthropicMessageToOpenAi(message) {
|
|
67
|
+
const content = message.content;
|
|
68
|
+
if (message.role === 'assistant') {
|
|
69
|
+
const textParts = Array.isArray(content)
|
|
70
|
+
? content.filter(p => p?.type === 'text').map(p => p.text || '').join('')
|
|
71
|
+
: typeof content === 'string' ? content : '';
|
|
72
|
+
const toolUses = Array.isArray(content) ? content.filter(p => p?.type === 'tool_use') : [];
|
|
73
|
+
const msg = { role: 'assistant', content: textParts || null };
|
|
74
|
+
if (toolUses.length > 0) {
|
|
75
|
+
msg.tool_calls = toolUses.map(anthropicToolUseToOpenAi);
|
|
76
|
+
}
|
|
77
|
+
if (msg.content === null && !msg.tool_calls) msg.content = '';
|
|
78
|
+
return [msg];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// user role: split tool_result blocks into separate 'tool' messages
|
|
82
|
+
if (!Array.isArray(content)) {
|
|
83
|
+
return [{ role: 'user', content: anthropicTextFromContent(content) }];
|
|
84
|
+
}
|
|
85
|
+
const toolResults = content.filter(p => p?.type === 'tool_result');
|
|
86
|
+
const other = content.filter(p => p?.type !== 'tool_result');
|
|
87
|
+
const messages = toolResults.map(tr => ({
|
|
88
|
+
role: 'tool',
|
|
89
|
+
tool_call_id: tr.tool_use_id,
|
|
90
|
+
content: anthropicToolResultContentToText(tr.content),
|
|
91
|
+
}));
|
|
92
|
+
const text = anthropicTextFromContent(other);
|
|
93
|
+
if (text) messages.push({ role: 'user', content: text });
|
|
94
|
+
if (messages.length === 0) messages.push({ role: 'user', content: '' });
|
|
95
|
+
return messages;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function anthropicToolsToOpenAi(tools) {
|
|
99
|
+
if (!Array.isArray(tools)) return undefined;
|
|
100
|
+
const converted = tools.map(tool => ({
|
|
101
|
+
type: 'function',
|
|
102
|
+
function: {
|
|
103
|
+
name: tool.name,
|
|
104
|
+
description: tool.description || '',
|
|
105
|
+
parameters: tool.input_schema || { type: 'object', properties: {} },
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
return converted.length > 0 ? converted : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
47
111
|
function anthropicToOpenAiChatRequest(body) {
|
|
48
112
|
const messages = [
|
|
49
113
|
...normalizeSystemMessages(body.system),
|
|
50
|
-
...(Array.isArray(body.messages) ? body.messages : []).
|
|
51
|
-
role: message.role === 'assistant' ? 'assistant' : 'user',
|
|
52
|
-
content: anthropicTextFromContent(message.content),
|
|
53
|
-
})),
|
|
114
|
+
...(Array.isArray(body.messages) ? body.messages : []).flatMap(anthropicMessageToOpenAi),
|
|
54
115
|
];
|
|
55
116
|
|
|
56
117
|
const request = {
|
|
@@ -63,6 +124,8 @@ function anthropicToOpenAiChatRequest(body) {
|
|
|
63
124
|
if (body.temperature !== undefined) request.temperature = body.temperature;
|
|
64
125
|
if (body.top_p !== undefined) request.top_p = body.top_p;
|
|
65
126
|
if (body.stop_sequences !== undefined) request.stop = body.stop_sequences;
|
|
127
|
+
const tools = anthropicToolsToOpenAi(body.tools);
|
|
128
|
+
if (tools) request.tools = tools;
|
|
66
129
|
return request;
|
|
67
130
|
}
|
|
68
131
|
|
|
@@ -73,18 +136,38 @@ function openAiFinishReasonToAnthropic(reason) {
|
|
|
73
136
|
return reason || 'end_turn';
|
|
74
137
|
}
|
|
75
138
|
|
|
139
|
+
function openAiToolCallToAnthropic(toolCall) {
|
|
140
|
+
let input = {};
|
|
141
|
+
try { input = JSON.parse(toolCall.function?.arguments || '{}'); } catch {}
|
|
142
|
+
return {
|
|
143
|
+
type: 'tool_use',
|
|
144
|
+
id: toolCall.id || `tool_${Date.now()}`,
|
|
145
|
+
name: toolCall.function?.name || '',
|
|
146
|
+
input,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
76
150
|
function openAiToAnthropicMessage(body, fallbackModel) {
|
|
77
151
|
const choice = body?.choices?.[0] || {};
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
152
|
+
const message = choice.message || {};
|
|
153
|
+
const rawContent = message.content ?? choice.text ?? '';
|
|
154
|
+
const text = Array.isArray(rawContent)
|
|
155
|
+
? rawContent.map((part) => part?.text || '').join('')
|
|
156
|
+
: String(rawContent || '');
|
|
157
|
+
|
|
158
|
+
const contentBlocks = [];
|
|
159
|
+
if (text) contentBlocks.push({ type: 'text', text });
|
|
160
|
+
if (Array.isArray(message.tool_calls)) {
|
|
161
|
+
for (const tc of message.tool_calls) contentBlocks.push(openAiToolCallToAnthropic(tc));
|
|
162
|
+
}
|
|
163
|
+
if (contentBlocks.length === 0) contentBlocks.push({ type: 'text', text: '' });
|
|
164
|
+
|
|
82
165
|
return {
|
|
83
166
|
id: body?.id || `msg_${Date.now()}`,
|
|
84
167
|
type: 'message',
|
|
85
168
|
role: 'assistant',
|
|
86
169
|
model: body?.model || fallbackModel,
|
|
87
|
-
content:
|
|
170
|
+
content: contentBlocks,
|
|
88
171
|
stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason),
|
|
89
172
|
stop_sequence: null,
|
|
90
173
|
usage: {
|
|
@@ -102,8 +185,14 @@ function openAiStreamChunkToAnthropicEvents(chunk, state) {
|
|
|
102
185
|
const choice = chunk?.choices?.[0] || {};
|
|
103
186
|
const delta = choice.delta || {};
|
|
104
187
|
const events = [];
|
|
188
|
+
|
|
105
189
|
if (!state.started) {
|
|
106
190
|
state.started = true;
|
|
191
|
+
// index 0 is always the text block
|
|
192
|
+
state.textBlockOpen = false;
|
|
193
|
+
// map tool_call index → anthropic content block index
|
|
194
|
+
state.toolIndexMap = {};
|
|
195
|
+
state.nextBlockIndex = 0;
|
|
107
196
|
events.push(anthropicSse('message_start', {
|
|
108
197
|
type: 'message_start',
|
|
109
198
|
message: {
|
|
@@ -114,32 +203,85 @@ function openAiStreamChunkToAnthropicEvents(chunk, state) {
|
|
|
114
203
|
content: [],
|
|
115
204
|
stop_reason: null,
|
|
116
205
|
stop_sequence: null,
|
|
117
|
-
usage: { input_tokens: 0, output_tokens: 0 },
|
|
206
|
+
usage: { input_tokens: chunk?.usage?.prompt_tokens ?? 0, output_tokens: 0 },
|
|
118
207
|
},
|
|
119
208
|
}));
|
|
120
|
-
events.push(anthropicSse('content_block_start', {
|
|
121
|
-
type: 'content_block_start',
|
|
122
|
-
index: 0,
|
|
123
|
-
content_block: { type: 'text', text: '' },
|
|
124
|
-
}));
|
|
125
209
|
}
|
|
210
|
+
|
|
126
211
|
if (delta.content) {
|
|
212
|
+
if (!state.textBlockOpen) {
|
|
213
|
+
state.textBlockOpen = true;
|
|
214
|
+
state.textBlockIndex = state.nextBlockIndex++;
|
|
215
|
+
events.push(anthropicSse('content_block_start', {
|
|
216
|
+
type: 'content_block_start',
|
|
217
|
+
index: state.textBlockIndex,
|
|
218
|
+
content_block: { type: 'text', text: '' },
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
127
221
|
events.push(anthropicSse('content_block_delta', {
|
|
128
222
|
type: 'content_block_delta',
|
|
129
|
-
index:
|
|
223
|
+
index: state.textBlockIndex,
|
|
130
224
|
delta: { type: 'text_delta', text: delta.content },
|
|
131
225
|
}));
|
|
132
226
|
}
|
|
227
|
+
|
|
228
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
229
|
+
for (const tc of delta.tool_calls) {
|
|
230
|
+
const tcIdx = tc.index ?? 0;
|
|
231
|
+
if (!(tcIdx in state.toolIndexMap)) {
|
|
232
|
+
// close text block first if open
|
|
233
|
+
if (state.textBlockOpen && !state.textBlockClosed) {
|
|
234
|
+
state.textBlockClosed = true;
|
|
235
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: state.textBlockIndex }));
|
|
236
|
+
}
|
|
237
|
+
const blockIndex = state.nextBlockIndex++;
|
|
238
|
+
state.toolIndexMap[tcIdx] = blockIndex;
|
|
239
|
+
let input = {};
|
|
240
|
+
try { if (tc.function?.arguments) input = JSON.parse(tc.function.arguments); } catch {}
|
|
241
|
+
events.push(anthropicSse('content_block_start', {
|
|
242
|
+
type: 'content_block_start',
|
|
243
|
+
index: blockIndex,
|
|
244
|
+
content_block: {
|
|
245
|
+
type: 'tool_use',
|
|
246
|
+
id: tc.id || `tool_${Date.now()}_${tcIdx}`,
|
|
247
|
+
name: tc.function?.name || '',
|
|
248
|
+
input,
|
|
249
|
+
},
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
if (tc.function?.arguments) {
|
|
253
|
+
events.push(anthropicSse('content_block_delta', {
|
|
254
|
+
type: 'content_block_delta',
|
|
255
|
+
index: state.toolIndexMap[tcIdx],
|
|
256
|
+
delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
133
262
|
if (choice.finish_reason) {
|
|
134
263
|
state.finished = true;
|
|
135
|
-
|
|
264
|
+
// close any open blocks
|
|
265
|
+
if (state.textBlockOpen && !state.textBlockClosed) {
|
|
266
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: state.textBlockIndex }));
|
|
267
|
+
}
|
|
268
|
+
for (const blockIndex of Object.values(state.toolIndexMap)) {
|
|
269
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: blockIndex }));
|
|
270
|
+
}
|
|
271
|
+
// if nothing was opened at all, emit an empty text block
|
|
272
|
+
if (!state.textBlockOpen && Object.keys(state.toolIndexMap).length === 0) {
|
|
273
|
+
events.push(anthropicSse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));
|
|
274
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: 0 }));
|
|
275
|
+
}
|
|
276
|
+
const outputTokens = chunk?.usage?.completion_tokens ?? chunk?.usage?.output_tokens ?? 0;
|
|
136
277
|
events.push(anthropicSse('message_delta', {
|
|
137
278
|
type: 'message_delta',
|
|
138
279
|
delta: { stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason), stop_sequence: null },
|
|
139
|
-
usage: { output_tokens:
|
|
280
|
+
usage: { output_tokens: outputTokens },
|
|
140
281
|
}));
|
|
141
282
|
events.push(anthropicSse('message_stop', { type: 'message_stop' }));
|
|
142
283
|
}
|
|
284
|
+
|
|
143
285
|
return events.join('');
|
|
144
286
|
}
|
|
145
287
|
|
package/lib/panel.js
CHANGED
|
@@ -94,7 +94,9 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
94
94
|
} else if (options.platform === 'win32') {
|
|
95
95
|
lines.push('终端未生效:重新打开 PowerShell / CMD');
|
|
96
96
|
} else {
|
|
97
|
-
|
|
97
|
+
const shell = options.shell || process.env.SHELL || '';
|
|
98
|
+
const rcFile = shell.includes('bash') ? '~/.bashrc' : '~/.zshrc';
|
|
99
|
+
lines.push(`终端未生效:source ${rcFile} 或重开终端`);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
const modelCountText = view.availableModelCount ? ` · 可用 ${view.availableModelCount}` : '';
|
package/lib/vscode.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { buildClaudeEnv, resolveFastModel, PROVIDERS, CLEAR_CLAUDE_ENV_KEYS } = require('./config');
|
|
5
|
-
const { isDesktopChatModel } = require('./gateway');
|
|
5
|
+
const { isDesktopChatModel, buildDesktopGatewayRoutes, buildDesktopGatewayUrl } = require('./gateway');
|
|
6
6
|
|
|
7
7
|
const CLAUDE_CODE_SETTINGS_SCHEMA = 'https://json.schemastore.org/claude-code-settings.json';
|
|
8
8
|
const VSCODE_CLAUDE_OPEN_URI = 'vscode://anthropic.claude-code/open';
|
|
@@ -43,14 +43,10 @@ function getVsCodeUserSettingsPath(options = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function buildClaudeCodeSettingsPatch(config) {
|
|
46
|
-
const env = buildVsCodeClaudeEnv(config);
|
|
47
46
|
const availableModels = buildVsCodeAvailableModels(config);
|
|
48
|
-
const model = config.vscodeModel || config.model;
|
|
49
47
|
return {
|
|
50
48
|
$schema: CLAUDE_CODE_SETTINGS_SCHEMA,
|
|
51
|
-
model: availableModels.includes(model) ? model : availableModels[0],
|
|
52
49
|
availableModels,
|
|
53
|
-
env,
|
|
54
50
|
};
|
|
55
51
|
}
|
|
56
52
|
|
|
@@ -62,7 +58,7 @@ function buildVsCodeClaudeEnv(config) {
|
|
|
62
58
|
const env = {
|
|
63
59
|
ANTHROPIC_BASE_URL: claudeEnv.ANTHROPIC_BASE_URL,
|
|
64
60
|
ANTHROPIC_AUTH_TOKEN: claudeEnv.ANTHROPIC_AUTH_TOKEN,
|
|
65
|
-
|
|
61
|
+
ANTHROPIC_MODEL: claudeEnv.ANTHROPIC_MODEL,
|
|
66
62
|
CLAUDE_CODE_SUBAGENT_MODEL: resolvedFastModel,
|
|
67
63
|
CLAUDE_CODE_EFFORT_LEVEL: 'max',
|
|
68
64
|
};
|
|
@@ -91,32 +87,47 @@ function writeClaudeCodeSettings(config, options = {}) {
|
|
|
91
87
|
const file = getClaudeCodeSettingsPath(options);
|
|
92
88
|
const current = readJsonFile(file);
|
|
93
89
|
const patch = buildClaudeCodeSettingsPatch(config);
|
|
90
|
+
|
|
91
|
+
// Clean up any yingclaw env vars previously written to settings.json
|
|
94
92
|
const nextEnv = { ...(current.env && typeof current.env === 'object' ? current.env : {}) };
|
|
95
93
|
for (const key of CLEAR_CLAUDE_ENV_KEYS) {
|
|
96
94
|
delete nextEnv[key];
|
|
97
95
|
}
|
|
98
|
-
Object.assign(nextEnv, patch.env);
|
|
99
96
|
|
|
100
97
|
const next = {
|
|
101
98
|
...current,
|
|
102
99
|
$schema: current.$schema || patch.$schema,
|
|
103
|
-
model: patch.model,
|
|
104
100
|
availableModels: patch.availableModels,
|
|
105
|
-
env: nextEnv,
|
|
106
101
|
};
|
|
102
|
+
|
|
103
|
+
// Remove model from settings.json — it would also affect the terminal
|
|
104
|
+
delete next.model;
|
|
105
|
+
|
|
106
|
+
if (Object.keys(nextEnv).length > 0) {
|
|
107
|
+
next.env = nextEnv;
|
|
108
|
+
} else {
|
|
109
|
+
delete next.env;
|
|
110
|
+
}
|
|
111
|
+
|
|
107
112
|
writeJsonFile(file, next);
|
|
108
113
|
try {
|
|
109
114
|
fs.chmodSync(file, 0o600);
|
|
110
115
|
} catch {}
|
|
111
|
-
return { result: 'updated', file
|
|
116
|
+
return { result: 'updated', file };
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
function
|
|
119
|
+
function buildVsCodeExtensionEnvVars(config) {
|
|
120
|
+
const env = buildVsCodeClaudeEnv(config);
|
|
121
|
+
return Object.entries(env).map(([name, value]) => ({ name, value: String(value) }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeVsCodeExtensionSettings(config, options = {}) {
|
|
115
125
|
const file = getVsCodeUserSettingsPath(options);
|
|
116
126
|
const current = readJsonFile(file);
|
|
117
127
|
const next = {
|
|
118
128
|
...current,
|
|
119
129
|
'claudeCode.disableLoginPrompt': true,
|
|
130
|
+
'claudeCode.environmentVariables': buildVsCodeExtensionEnvVars(config),
|
|
120
131
|
};
|
|
121
132
|
if (options.useTerminal === true || options.useTerminal === false) {
|
|
122
133
|
next['claudeCode.useTerminal'] = options.useTerminal;
|
|
@@ -127,7 +138,7 @@ function writeVsCodeExtensionSettings(options = {}) {
|
|
|
127
138
|
|
|
128
139
|
function writeVsCodeIntegration(config, options = {}) {
|
|
129
140
|
const claudeSettings = writeClaudeCodeSettings(config, options);
|
|
130
|
-
const vscodeSettings = writeVsCodeExtensionSettings(options);
|
|
141
|
+
const vscodeSettings = writeVsCodeExtensionSettings(config, options);
|
|
131
142
|
return {
|
|
132
143
|
result: 'updated',
|
|
133
144
|
files: [claudeSettings.file, vscodeSettings.file],
|
|
@@ -178,7 +189,7 @@ function clearVsCodeExtensionSettings(options = {}) {
|
|
|
178
189
|
const current = readJsonFile(file);
|
|
179
190
|
const next = { ...current };
|
|
180
191
|
let changed = false;
|
|
181
|
-
for (const key of ['claudeCode.disableLoginPrompt', 'claudeCode.useTerminal']) {
|
|
192
|
+
for (const key of ['claudeCode.disableLoginPrompt', 'claudeCode.useTerminal', 'claudeCode.environmentVariables']) {
|
|
182
193
|
if (Object.prototype.hasOwnProperty.call(next, key)) {
|
|
183
194
|
delete next[key];
|
|
184
195
|
changed = true;
|
|
@@ -206,21 +217,33 @@ function clearVsCodeIntegration(options = {}) {
|
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
function checkClaudeCodeSettingsEnv(config, options = {}) {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const missing =
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
const settingsFile = getClaudeCodeSettingsPath(options);
|
|
221
|
+
const vsCodeFile = getVsCodeUserSettingsPath(options);
|
|
222
|
+
const settings = readJsonFile(settingsFile);
|
|
223
|
+
const vsCodeSettings = readJsonFile(vsCodeFile);
|
|
224
|
+
const missing = [];
|
|
225
|
+
|
|
226
|
+
// Check availableModels in settings.json
|
|
216
227
|
const expectedModels = buildVsCodeAvailableModels(config);
|
|
217
|
-
|
|
218
|
-
if (current.model !== expectedModel) missing.push('model');
|
|
219
|
-
if (!Array.isArray(current.availableModels) || current.availableModels.join(',') !== expectedModels.join(',')) {
|
|
228
|
+
if (!Array.isArray(settings.availableModels) || settings.availableModels.join(',') !== expectedModels.join(',')) {
|
|
220
229
|
missing.push('availableModels');
|
|
221
230
|
}
|
|
231
|
+
|
|
232
|
+
// Check claudeCode.environmentVariables in VS Code extension settings
|
|
233
|
+
const expectedEnv = buildVsCodeClaudeEnv(config);
|
|
234
|
+
const actualEnvArray = vsCodeSettings['claudeCode.environmentVariables'];
|
|
235
|
+
if (!Array.isArray(actualEnvArray)) {
|
|
236
|
+
missing.push('claudeCode.environmentVariables');
|
|
237
|
+
} else {
|
|
238
|
+
const actualEnvMap = {};
|
|
239
|
+
for (const { name, value } of actualEnvArray) actualEnvMap[name] = value;
|
|
240
|
+
for (const [key, value] of Object.entries(expectedEnv)) {
|
|
241
|
+
if (actualEnvMap[key] !== String(value)) missing.push(key);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
222
245
|
return {
|
|
223
|
-
file,
|
|
246
|
+
file: settingsFile,
|
|
224
247
|
configured: missing.length === 0,
|
|
225
248
|
missing,
|
|
226
249
|
};
|
|
@@ -242,6 +265,7 @@ module.exports = {
|
|
|
242
265
|
buildClaudeCodeSettingsPatch,
|
|
243
266
|
buildVsCodeAvailableModels,
|
|
244
267
|
buildVsCodeClaudeEnv,
|
|
268
|
+
buildVsCodeExtensionEnvVars,
|
|
245
269
|
buildVsCodeModelSelectionIds,
|
|
246
270
|
buildVsCodeOpenCommand,
|
|
247
271
|
checkClaudeCodeSettingsEnv,
|