yingclaw 2.5.25 → 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 +5 -1
- package/bin/cli.js +175 -47
- package/lib/config.js +75 -15
- package/lib/doctor.js +1 -1
- package/lib/gateway.js +36 -4
- 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,7 +735,7 @@ program
|
|
|
613
735
|
}
|
|
614
736
|
|
|
615
737
|
console.log(chalk.dim(getStorageHint(file)));
|
|
616
|
-
const displayedConfig =
|
|
738
|
+
const displayedConfig = effectiveConfig;
|
|
617
739
|
console.log(boxen(
|
|
618
740
|
chalk.bold('Claude Code 终端已接入\n\n') +
|
|
619
741
|
chalk.dim('Base URL ') + chalk.cyan(buildClaudeEnv(displayedConfig).ANTHROPIC_BASE_URL) + '\n' +
|
|
@@ -692,10 +814,11 @@ program
|
|
|
692
814
|
if (effectiveConfig !== config) saveConfig(effectiveConfig);
|
|
693
815
|
|
|
694
816
|
const vscodeModelChoices = getChatModelChoices(buildVsCodeModelSelectionIds(effectiveConfig));
|
|
817
|
+
const vscodeModelCount = vscodeModelChoices.length;
|
|
695
818
|
const vscodeModel = await promptModelFromChoices({
|
|
696
819
|
chalk,
|
|
697
820
|
choices: vscodeModelChoices,
|
|
698
|
-
message:
|
|
821
|
+
message: `选择 VS Code Claude Code 主模型 共 ${vscodeModelCount} 个可用`,
|
|
699
822
|
allowManual: true,
|
|
700
823
|
});
|
|
701
824
|
if (vscodeModel === '__BACK__') return;
|
|
@@ -703,7 +826,7 @@ program
|
|
|
703
826
|
const vscodeFastModel = await promptModelFromChoices({
|
|
704
827
|
chalk,
|
|
705
828
|
choices: sortModelChoicesByRelatedFamily(vscodeModelChoices, vscodeModel),
|
|
706
|
-
message:
|
|
829
|
+
message: `选择 VS Code Claude Code 快速模型 / 备用模型 共 ${vscodeModelCount} 个可用`,
|
|
707
830
|
allowManual: true,
|
|
708
831
|
});
|
|
709
832
|
if (vscodeFastModel === '__BACK__') return;
|
|
@@ -799,14 +922,8 @@ program
|
|
|
799
922
|
return;
|
|
800
923
|
}
|
|
801
924
|
|
|
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;
|
|
925
|
+
const providerKey = await promptProviderKey(chalk);
|
|
926
|
+
if (!providerKey || providerKey === '__BACK__') return;
|
|
810
927
|
|
|
811
928
|
const provider = PROVIDERS[providerKey];
|
|
812
929
|
if (provider.custom) {
|
|
@@ -818,7 +935,6 @@ program
|
|
|
818
935
|
saveConfig(savedConfig);
|
|
819
936
|
spinner.succeed(chalk.green(`API 连接已切换至 ${savedConfig.providerName} · ${savedConfig.model}`));
|
|
820
937
|
console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
|
|
821
|
-
await offerDesktopSync(chalk, ora, savedConfig);
|
|
822
938
|
return;
|
|
823
939
|
}
|
|
824
940
|
|
|
@@ -859,12 +975,12 @@ program
|
|
|
859
975
|
availableModels = provider.models.map(m => m.value);
|
|
860
976
|
}
|
|
861
977
|
|
|
862
|
-
const model = await
|
|
863
|
-
|
|
864
|
-
choices:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
978
|
+
const model = await promptModelFromChoices({
|
|
979
|
+
chalk,
|
|
980
|
+
choices: modelChoices,
|
|
981
|
+
message: '选择模型',
|
|
982
|
+
backLabel: '↩ 返回主菜单',
|
|
983
|
+
allowManual: false,
|
|
868
984
|
});
|
|
869
985
|
if (model === '__BACK__') return;
|
|
870
986
|
|
|
@@ -874,7 +990,6 @@ program
|
|
|
874
990
|
saveConfig(newConfig);
|
|
875
991
|
spinner.succeed(chalk.green(`API 连接已切换至 ${provider.name} · ${model}`));
|
|
876
992
|
console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
|
|
877
|
-
await offerDesktopSync(chalk, ora, newConfig);
|
|
878
993
|
});
|
|
879
994
|
|
|
880
995
|
program
|
|
@@ -963,6 +1078,16 @@ program
|
|
|
963
1078
|
if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
|
|
964
1079
|
saveConfig(desktopConfig);
|
|
965
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
|
+
}
|
|
966
1091
|
}
|
|
967
1092
|
|
|
968
1093
|
const spinner = ora('写入 Claude 桌面应用配置...').start();
|
|
@@ -1272,15 +1397,18 @@ program
|
|
|
1272
1397
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
1273
1398
|
));
|
|
1274
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'));
|
|
1275
1403
|
} else {
|
|
1276
1404
|
console.log(chalk.red('\n升级失败,请手动运行:'));
|
|
1277
1405
|
console.log(chalk.cyan('npm install -g yingclaw@latest'));
|
|
1278
1406
|
}
|
|
1279
1407
|
});
|
|
1280
1408
|
|
|
1281
|
-
async function renderStatusBar(apiStatus) {
|
|
1409
|
+
async function renderStatusBar(apiStatus, config) {
|
|
1282
1410
|
const chalk = (await import('chalk')).default;
|
|
1283
|
-
|
|
1411
|
+
if (config === undefined) config = loadConfig();
|
|
1284
1412
|
const claudeInstalled = isClaudeInstalled();
|
|
1285
1413
|
|
|
1286
1414
|
const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
|
|
@@ -1408,7 +1536,7 @@ async function runMenu() {
|
|
|
1408
1536
|
console.log(chalk.red(` ● 配置无效:${configProblem}`));
|
|
1409
1537
|
console.log(chalk.dim(' 请先选择"配置 API 连接"重新配置'));
|
|
1410
1538
|
} else {
|
|
1411
|
-
console.log(await renderStatusBar(apiStatus));
|
|
1539
|
+
console.log(await renderStatusBar(apiStatus, config));
|
|
1412
1540
|
}
|
|
1413
1541
|
console.log();
|
|
1414
1542
|
|
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/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,
|