yingclaw 2.5.20 → 2.5.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -6
- package/bin/cli.js +372 -67
- package/index.js +2 -0
- package/lib/config.js +193 -20
- package/lib/doctor.js +13 -3
- package/lib/gateway.js +137 -6
- package/lib/openai.js +294 -0
- package/lib/panel.js +19 -5
- package/lib/vscode.js +280 -0
- package/package.json +2 -2
package/index.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
|
+
const { normalizeOpenAiBaseUrl, openAiChatCompletionsUrl } = require('./openai');
|
|
5
6
|
|
|
6
7
|
const CONFIG_FILE = path.join(os.homedir(), '.clawai.json');
|
|
7
8
|
const WINDOWS_ENV_LABEL = 'Windows 用户环境变量';
|
|
@@ -15,10 +16,17 @@ const CLAUDE_ENV_KEYS = [
|
|
|
15
16
|
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
16
17
|
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
17
18
|
];
|
|
19
|
+
const VSCODE_CLAUDE_ENV_KEYS = [
|
|
20
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
21
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
|
|
22
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
|
|
23
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES',
|
|
24
|
+
'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY',
|
|
25
|
+
];
|
|
18
26
|
const LEGACY_CLAUDE_ENV_KEYS = [
|
|
19
27
|
'ANTHROPIC_API_KEY',
|
|
20
28
|
];
|
|
21
|
-
const CLEAR_CLAUDE_ENV_KEYS = [...new Set([...CLAUDE_ENV_KEYS, ...LEGACY_CLAUDE_ENV_KEYS])];
|
|
29
|
+
const CLEAR_CLAUDE_ENV_KEYS = [...new Set([...CLAUDE_ENV_KEYS, ...VSCODE_CLAUDE_ENV_KEYS, ...LEGACY_CLAUDE_ENV_KEYS])];
|
|
22
30
|
|
|
23
31
|
const PROVIDERS = {
|
|
24
32
|
deepseek: {
|
|
@@ -33,13 +41,27 @@ const PROVIDERS = {
|
|
|
33
41
|
},
|
|
34
42
|
kimi: {
|
|
35
43
|
name: 'Kimi / Moonshot',
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
protocol: 'openai',
|
|
45
|
+
baseUrl: 'https://api.moonshot.cn/v1',
|
|
46
|
+
modelsUrl: 'https://api.moonshot.cn/v1/models',
|
|
38
47
|
fastModel: 'kimi-k2.5',
|
|
39
48
|
models: [
|
|
40
49
|
{ name: 'Kimi K2.5(代码)', value: 'kimi-k2.5' },
|
|
41
50
|
],
|
|
42
51
|
},
|
|
52
|
+
volcengine: {
|
|
53
|
+
name: '火山方舟 Coding Plan',
|
|
54
|
+
baseUrl: 'https://ark.cn-beijing.volces.com/api/coding',
|
|
55
|
+
modelsUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3/models',
|
|
56
|
+
fastModel: 'ark-code-latest',
|
|
57
|
+
models: [
|
|
58
|
+
{ name: 'Ark Code Latest(控制台选择)', value: 'ark-code-latest' },
|
|
59
|
+
{ name: 'Doubao Seed Code', value: 'doubao-seed-code' },
|
|
60
|
+
{ name: 'GLM 4.7', value: 'glm-4.7' },
|
|
61
|
+
{ name: 'DeepSeek V3.2', value: 'deepseek-v3.2' },
|
|
62
|
+
{ name: 'Kimi K2.5', value: 'kimi-k2.5' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
43
65
|
qwen: {
|
|
44
66
|
name: '阿里云百炼 (Qwen)',
|
|
45
67
|
baseUrl: 'https://dashscope.aliyuncs.com/apps/anthropic',
|
|
@@ -87,9 +109,76 @@ const PROVIDERS = {
|
|
|
87
109
|
{ name: 'MiMo V2 Flash(快速)', value: 'mimo-v2-flash' },
|
|
88
110
|
],
|
|
89
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
|
+
},
|
|
90
172
|
custom: {
|
|
91
173
|
name: '自定义 Anthropic 兼容接口',
|
|
92
174
|
custom: true,
|
|
175
|
+
protocol: 'anthropic',
|
|
176
|
+
models: [],
|
|
177
|
+
},
|
|
178
|
+
custom_openai: {
|
|
179
|
+
name: '自定义 OpenAI 兼容接口',
|
|
180
|
+
custom: true,
|
|
181
|
+
protocol: 'openai',
|
|
93
182
|
models: [],
|
|
94
183
|
},
|
|
95
184
|
};
|
|
@@ -99,6 +188,8 @@ const PREFERRED_MODEL_IDS = {
|
|
|
99
188
|
minimax: ['MiniMax-M2.7', 'MiniMax-M2.7-Turbo', 'MiniMax-M2.5'],
|
|
100
189
|
glm: ['GLM-4.7', 'GLM-5.1', 'GLM-5-Turbo', 'GLM-4.5-Air'],
|
|
101
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'],
|
|
102
193
|
};
|
|
103
194
|
|
|
104
195
|
function sortPreferredModelIds(providerKey, ids) {
|
|
@@ -110,11 +201,28 @@ function sortPreferredModelIds(providerKey, ids) {
|
|
|
110
201
|
];
|
|
111
202
|
}
|
|
112
203
|
|
|
204
|
+
function isChatModelId(model) {
|
|
205
|
+
const value = String(model || '');
|
|
206
|
+
return !/(^|[-_])(tts|voice|voiceclone|voicedesign|speech|audio|image|video|embedding|embed|rerank|moderation)([-_]|$)/i.test(value)
|
|
207
|
+
&& !/(^|[-_])(i2v|t2v|t2i|i2i|v2v|video-generation|image-generation)([-_]|$)/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);
|
|
209
|
+
}
|
|
210
|
+
|
|
113
211
|
function normalizeModelIds(providerKey, ids) {
|
|
114
212
|
if (PREFERRED_MODEL_IDS[providerKey]) {
|
|
115
213
|
return sortPreferredModelIds(providerKey, ids);
|
|
116
214
|
}
|
|
117
215
|
|
|
216
|
+
if (providerKey === 'kimi') {
|
|
217
|
+
const preferred = PROVIDERS.kimi.models.map((item) => item.value);
|
|
218
|
+
const merged = [...new Set([...preferred, ...ids])];
|
|
219
|
+
return [
|
|
220
|
+
...preferred.filter((id) => merged.includes(id)),
|
|
221
|
+
...merged.filter((id) => !preferred.includes(id) && /^kimi-/i.test(id)),
|
|
222
|
+
...merged.filter((id) => !preferred.includes(id) && !/^kimi-/i.test(id)),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
118
226
|
if (providerKey !== 'deepseek') return ids;
|
|
119
227
|
|
|
120
228
|
const mapped = ids.map((id) => {
|
|
@@ -132,7 +240,16 @@ function normalizeModelIds(providerKey, ids) {
|
|
|
132
240
|
function parseModelIdsResponse(providerKey, data) {
|
|
133
241
|
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
134
242
|
const list = parsed.data || parsed.models || [];
|
|
135
|
-
const
|
|
243
|
+
const availableList = providerKey === 'volcengine'
|
|
244
|
+
? list.filter((model) => !/^shutdown$/i.test(String(model.status || '')))
|
|
245
|
+
: list;
|
|
246
|
+
const ids = availableList.map(m => m.id || m.model || m.name).filter(Boolean);
|
|
247
|
+
if (providerKey === 'volcengine') {
|
|
248
|
+
return normalizeModelIds(providerKey, ids.filter(isChatModelId));
|
|
249
|
+
}
|
|
250
|
+
if (providerKey === 'bai') {
|
|
251
|
+
return normalizeModelIds(providerKey, ids.filter(id => !id.includes('/')));
|
|
252
|
+
}
|
|
136
253
|
return normalizeModelIds(providerKey, ids);
|
|
137
254
|
}
|
|
138
255
|
|
|
@@ -155,7 +272,7 @@ function normalizeAnthropicBaseUrl(baseUrl) {
|
|
|
155
272
|
|
|
156
273
|
function buildModelUrlCandidates(baseUrl) {
|
|
157
274
|
let url;
|
|
158
|
-
try { url = new URL(normalizeAnthropicBaseUrl(baseUrl)); } catch { return []; }
|
|
275
|
+
try { url = new URL(normalizeAnthropicBaseUrl(normalizeOpenAiBaseUrl(baseUrl))); } catch { return []; }
|
|
159
276
|
|
|
160
277
|
const pathname = url.pathname.replace(/\/+$/, '');
|
|
161
278
|
const candidates = [];
|
|
@@ -209,10 +326,7 @@ async function fetchModels(providerKey, apiKey, modelsUrlOverride) {
|
|
|
209
326
|
const res = await fetch(modelsUrl, {
|
|
210
327
|
method: 'GET',
|
|
211
328
|
signal: controller.signal,
|
|
212
|
-
headers:
|
|
213
|
-
authorization: `Bearer ${apiKey}`,
|
|
214
|
-
'api-key': apiKey, // MiMo 用这个 header
|
|
215
|
-
},
|
|
329
|
+
headers: buildProviderAuthHeaders(providerKey, apiKey),
|
|
216
330
|
});
|
|
217
331
|
if (!res.ok) return null;
|
|
218
332
|
|
|
@@ -225,9 +339,22 @@ async function fetchModels(providerKey, apiKey, modelsUrlOverride) {
|
|
|
225
339
|
}
|
|
226
340
|
}
|
|
227
341
|
|
|
228
|
-
function
|
|
342
|
+
function migrateConfig(config) {
|
|
343
|
+
if (!config || typeof config !== 'object') return config;
|
|
344
|
+
if (config.provider === 'kimi') {
|
|
345
|
+
return {
|
|
346
|
+
...config,
|
|
347
|
+
protocol: 'openai',
|
|
348
|
+
baseUrl: PROVIDERS.kimi.baseUrl,
|
|
349
|
+
modelsUrl: PROVIDERS.kimi.modelsUrl,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return config;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function loadConfig(options = {}) {
|
|
229
356
|
try {
|
|
230
|
-
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
357
|
+
return migrateConfig(JSON.parse(fs.readFileSync(options.configFile || CONFIG_FILE, 'utf8')));
|
|
231
358
|
} catch {
|
|
232
359
|
return null;
|
|
233
360
|
}
|
|
@@ -259,16 +386,54 @@ function providerKeyFromBaseUrl(baseUrl) {
|
|
|
259
386
|
return Object.entries(PROVIDERS).find(([, provider]) => provider.baseUrl === baseUrl)?.[0];
|
|
260
387
|
}
|
|
261
388
|
|
|
389
|
+
function getProviderProtocol(config = {}) {
|
|
390
|
+
return config.protocol || PROVIDERS[config.provider]?.protocol || 'anthropic';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildProviderAuthHeaders(providerKey, apiKey) {
|
|
394
|
+
const headers = { authorization: `Bearer ${apiKey}` };
|
|
395
|
+
if (getProviderProtocol({ provider: providerKey }) !== 'openai') {
|
|
396
|
+
headers['api-key'] = apiKey; // MiMo 用这个 header
|
|
397
|
+
}
|
|
398
|
+
return headers;
|
|
399
|
+
}
|
|
400
|
+
|
|
262
401
|
function resolveFastModel(provider, model) {
|
|
263
402
|
if (/flash|turbo|haiku|air|lite/i.test(model)) return model;
|
|
264
403
|
return provider?.fastModel || model;
|
|
265
404
|
}
|
|
266
405
|
|
|
267
|
-
function
|
|
406
|
+
function getModelFamily(model) {
|
|
407
|
+
const value = String(model || '').trim().toLowerCase();
|
|
408
|
+
if (!value) return '';
|
|
409
|
+
const normalized = value
|
|
410
|
+
.replace(/^anthropic\//, '')
|
|
411
|
+
.replace(/^\w+\//, '');
|
|
412
|
+
return normalized.split(/[-_.]/).filter(Boolean)[0] || normalized;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function sortRelatedModelIds(ids, mainModel) {
|
|
416
|
+
const unique = [...new Set(ids.filter(Boolean))];
|
|
417
|
+
const family = getModelFamily(mainModel);
|
|
418
|
+
return [
|
|
419
|
+
...unique.filter((id) => id === mainModel),
|
|
420
|
+
...unique.filter((id) => id !== mainModel && getModelFamily(id) === family),
|
|
421
|
+
...unique.filter((id) => id !== mainModel && getModelFamily(id) !== family),
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function buildGatewayBaseUrlForEnv(config) {
|
|
426
|
+
const port = Number.parseInt(config.desktopGatewayPort || 18080, 10);
|
|
427
|
+
return `http://127.0.0.1:${port}/yingclaw`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function buildClaudeEnv(config) {
|
|
431
|
+
const { provider, baseUrl, apiKey, model, fastModel } = config;
|
|
268
432
|
const resolvedFastModel = fastModel || resolveFastModel(PROVIDERS[provider], model);
|
|
433
|
+
const useLocalGateway = getProviderProtocol(config) === 'openai' && config.desktopGatewayKey;
|
|
269
434
|
return {
|
|
270
|
-
ANTHROPIC_BASE_URL: baseUrl,
|
|
271
|
-
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
435
|
+
ANTHROPIC_BASE_URL: useLocalGateway ? buildGatewayBaseUrlForEnv(config) : baseUrl,
|
|
436
|
+
ANTHROPIC_AUTH_TOKEN: useLocalGateway ? config.desktopGatewayKey : apiKey,
|
|
272
437
|
ANTHROPIC_MODEL: model,
|
|
273
438
|
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
274
439
|
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
|
|
@@ -303,15 +468,19 @@ function runWindowsEnvCommands(commands, runner = spawnSync, { ignoreErrors = fa
|
|
|
303
468
|
|
|
304
469
|
function classifyValidationStatus(statusCode) {
|
|
305
470
|
if (statusCode >= 200 && statusCode < 300) return true;
|
|
471
|
+
if (statusCode === 429) return true; // 限流 = Key 有效
|
|
306
472
|
if (statusCode === 401 || statusCode === 403) return false;
|
|
307
473
|
return null;
|
|
308
474
|
}
|
|
309
475
|
|
|
310
476
|
// 发一次最小的 /v1/messages 请求验证 Key(true=有效, false=无效, null=网络/服务异常)
|
|
311
477
|
async function validateKey(config, options = {}) {
|
|
478
|
+
const protocol = getProviderProtocol(config);
|
|
312
479
|
let url;
|
|
313
480
|
try {
|
|
314
|
-
url =
|
|
481
|
+
url = protocol === 'openai'
|
|
482
|
+
? openAiChatCompletionsUrl(config.baseUrl)
|
|
483
|
+
: `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
|
|
315
484
|
new URL(url);
|
|
316
485
|
} catch {
|
|
317
486
|
return null;
|
|
@@ -325,14 +494,12 @@ async function validateKey(config, options = {}) {
|
|
|
325
494
|
signal: controller.signal,
|
|
326
495
|
headers: {
|
|
327
496
|
'content-type': 'application/json',
|
|
328
|
-
|
|
329
|
-
'x-api-key': config.apiKey,
|
|
330
|
-
'api-key': config.apiKey,
|
|
331
|
-
'anthropic-version': '2023-06-01',
|
|
497
|
+
...buildProviderAuthHeaders(config.provider, config.apiKey),
|
|
498
|
+
...(protocol === 'anthropic' ? { 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' } : {}),
|
|
332
499
|
},
|
|
333
500
|
body: JSON.stringify({
|
|
334
501
|
model: config.model,
|
|
335
|
-
max_tokens:
|
|
502
|
+
max_tokens: 16,
|
|
336
503
|
messages: [{ role: 'user', content: 'hi' }],
|
|
337
504
|
}),
|
|
338
505
|
});
|
|
@@ -465,6 +632,11 @@ module.exports = {
|
|
|
465
632
|
buildModelUrlCandidates,
|
|
466
633
|
fetchModelsFromBaseUrl,
|
|
467
634
|
resolveFastModel,
|
|
635
|
+
getModelFamily,
|
|
636
|
+
sortRelatedModelIds,
|
|
637
|
+
isChatModelId,
|
|
638
|
+
buildProviderAuthHeaders,
|
|
639
|
+
getProviderProtocol,
|
|
468
640
|
providerKeyFromBaseUrl,
|
|
469
641
|
buildClaudeEnv,
|
|
470
642
|
buildEnvBlock,
|
|
@@ -473,5 +645,6 @@ module.exports = {
|
|
|
473
645
|
PROVIDERS,
|
|
474
646
|
CONFIG_FILE,
|
|
475
647
|
CLAUDE_ENV_KEYS,
|
|
648
|
+
VSCODE_CLAUDE_ENV_KEYS,
|
|
476
649
|
CLEAR_CLAUDE_ENV_KEYS,
|
|
477
650
|
};
|
package/lib/doctor.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
CLAUDE_ENV_KEYS,
|
|
13
13
|
} = require('./config');
|
|
14
14
|
const { isDesktopConfigured } = require('./desktop');
|
|
15
|
+
const { checkClaudeCodeSettingsEnv } = require('./vscode');
|
|
15
16
|
|
|
16
17
|
const STATUS_OK = 'ok';
|
|
17
18
|
const STATUS_FAIL = 'fail';
|
|
@@ -104,7 +105,16 @@ async function runDoctorChecks(options = {}) {
|
|
|
104
105
|
: (platform === 'win32' ? '重新打开 PowerShell / CMD' : '运行 source ~/.zshrc 或重开终端'),
|
|
105
106
|
});
|
|
106
107
|
|
|
107
|
-
// 6.
|
|
108
|
+
// 6. VS Code / Claude Code 扩展共享设置
|
|
109
|
+
const vscodeSettings = checkClaudeCodeSettingsEnv(config, options);
|
|
110
|
+
checks.push({
|
|
111
|
+
name: 'VS Code Claude Code',
|
|
112
|
+
status: vscodeSettings.configured ? STATUS_OK : STATUS_WARN,
|
|
113
|
+
message: vscodeSettings.configured ? 'VS Code 扩展设置已配置' : `${vscodeSettings.missing.length} 项配置未写入 VS Code 扩展设置`,
|
|
114
|
+
fix: vscodeSettings.configured ? null : '运行 claw vscode,并重启 VS Code',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 7. API Key(顺便确认网络可达)
|
|
108
118
|
const valid = await validateKey(config, { timeoutMs: 6000 });
|
|
109
119
|
if (valid === true) {
|
|
110
120
|
checks.push({ name: 'API Key', status: STATUS_OK, message: '校验通过' });
|
|
@@ -124,7 +134,7 @@ async function runDoctorChecks(options = {}) {
|
|
|
124
134
|
});
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
//
|
|
137
|
+
// 8. Claude 桌面应用接入状态
|
|
128
138
|
const desktopConfigured = isDesktopConfigured();
|
|
129
139
|
checks.push({
|
|
130
140
|
name: 'Claude 桌面应用',
|
|
@@ -132,7 +142,7 @@ async function runDoctorChecks(options = {}) {
|
|
|
132
142
|
message: desktopConfigured ? '已通过 yingclaw 接入' : '未接入(如需运行 claw desktop)',
|
|
133
143
|
});
|
|
134
144
|
|
|
135
|
-
//
|
|
145
|
+
// 9. DeepSeek 旧模型名提醒
|
|
136
146
|
if (config.provider === 'deepseek' && (
|
|
137
147
|
config.model === 'deepseek-v4-pro' ||
|
|
138
148
|
config.model === 'deepseek-v4-flash' ||
|
package/lib/gateway.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const http = require('http');
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
loadConfig: defaultLoadConfig,
|
|
5
|
+
normalizeAnthropicBaseUrl,
|
|
6
|
+
buildProviderAuthHeaders,
|
|
7
|
+
getProviderProtocol,
|
|
8
|
+
isChatModelId,
|
|
9
|
+
} = require('./config');
|
|
10
|
+
const {
|
|
11
|
+
anthropicToOpenAiChatRequest,
|
|
12
|
+
openAiChatCompletionsUrl,
|
|
13
|
+
openAiStreamChunkToAnthropicEvents,
|
|
14
|
+
openAiToAnthropicMessage,
|
|
15
|
+
} = require('./openai');
|
|
4
16
|
|
|
5
17
|
const DEFAULT_DESKTOP_GATEWAY_PORT = 18080;
|
|
6
18
|
const YINGCLAW_GATEWAY_PREFIX = '/yingclaw';
|
|
@@ -59,9 +71,26 @@ function desktopRouteLabel(routeId) {
|
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
function isDesktopChatModel(model) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
return isChatModelId(model);
|
|
75
|
+
}
|
|
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;
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
function buildDesktopGatewayRoutes(config) {
|
|
@@ -93,6 +122,13 @@ function buildDesktopGatewayMappingRows(config) {
|
|
|
93
122
|
|
|
94
123
|
function mapDesktopRouteToUpstream(config, routeId) {
|
|
95
124
|
const requested = stripOneMContextSuffix(routeId);
|
|
125
|
+
const configuredModels = [
|
|
126
|
+
config.model,
|
|
127
|
+
config.fastModel || config.model,
|
|
128
|
+
...(Array.isArray(config.availableModels) ? config.availableModels : []),
|
|
129
|
+
].filter(Boolean);
|
|
130
|
+
if (configuredModels.includes(routeId)) return routeId;
|
|
131
|
+
|
|
96
132
|
const route = buildDesktopGatewayRoutes(config).find((item) => stripOneMContextSuffix(item.id) === requested);
|
|
97
133
|
if (!route) {
|
|
98
134
|
throw new Error(`未配置的 Claude Desktop 模型路由: ${routeId}`);
|
|
@@ -147,6 +183,10 @@ function providerMessagesUrl(config) {
|
|
|
147
183
|
return `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
|
|
148
184
|
}
|
|
149
185
|
|
|
186
|
+
function providerOpenAiChatUrl(config) {
|
|
187
|
+
return openAiChatCompletionsUrl(config.baseUrl);
|
|
188
|
+
}
|
|
189
|
+
|
|
150
190
|
function buildDesktopGatewayUrl(config = {}) {
|
|
151
191
|
const port = Number.parseInt(config.desktopGatewayPort || DEFAULT_DESKTOP_GATEWAY_PORT, 10);
|
|
152
192
|
return {
|
|
@@ -203,14 +243,22 @@ async function proxyMessages(req, res, config) {
|
|
|
203
243
|
return;
|
|
204
244
|
}
|
|
205
245
|
|
|
246
|
+
if (!isNativeClaudeModel(body.model)) {
|
|
247
|
+
body = stripThinkingFromBody(body);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (getProviderProtocol(config) === 'openai') {
|
|
251
|
+
await proxyOpenAiMessages(res, config, body);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
206
255
|
const upstream = await fetch(providerMessagesUrl(config), {
|
|
207
256
|
method: 'POST',
|
|
208
257
|
headers: {
|
|
209
258
|
'content-type': 'application/json',
|
|
210
259
|
accept: body.stream ? 'text/event-stream' : 'application/json',
|
|
211
|
-
|
|
260
|
+
...buildProviderAuthHeaders(config.provider, config.apiKey),
|
|
212
261
|
'x-api-key': config.apiKey,
|
|
213
|
-
'api-key': config.apiKey,
|
|
214
262
|
'anthropic-version': '2023-06-01',
|
|
215
263
|
},
|
|
216
264
|
body: JSON.stringify(body),
|
|
@@ -231,6 +279,89 @@ async function proxyMessages(req, res, config) {
|
|
|
231
279
|
res.end();
|
|
232
280
|
}
|
|
233
281
|
|
|
282
|
+
async function proxyOpenAiMessages(res, config, body) {
|
|
283
|
+
const openAiBody = anthropicToOpenAiChatRequest(body);
|
|
284
|
+
const upstream = await fetch(providerOpenAiChatUrl(config), {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: {
|
|
287
|
+
'content-type': 'application/json',
|
|
288
|
+
accept: body.stream ? 'text/event-stream' : 'application/json',
|
|
289
|
+
...buildProviderAuthHeaders(config.provider, config.apiKey),
|
|
290
|
+
},
|
|
291
|
+
body: JSON.stringify(openAiBody),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!body.stream) {
|
|
295
|
+
const text = await upstream.text();
|
|
296
|
+
if (!upstream.ok) {
|
|
297
|
+
res.writeHead(upstream.status, {
|
|
298
|
+
'content-type': upstream.headers.get('content-type') || 'application/json',
|
|
299
|
+
});
|
|
300
|
+
res.end(text);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let parsed;
|
|
305
|
+
try {
|
|
306
|
+
parsed = text ? JSON.parse(text) : {};
|
|
307
|
+
} catch {
|
|
308
|
+
sendJson(res, 502, { error: { message: 'OpenAI 兼容接口返回了无效 JSON' } });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
sendJson(res, upstream.status, openAiToAnthropicMessage(parsed, body.model));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
res.writeHead(upstream.status, {
|
|
316
|
+
'content-type': upstream.ok ? 'text/event-stream' : (upstream.headers.get('content-type') || 'application/json'),
|
|
317
|
+
'cache-control': 'no-cache',
|
|
318
|
+
});
|
|
319
|
+
if (!upstream.body) {
|
|
320
|
+
res.end();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!upstream.ok) {
|
|
324
|
+
for await (const chunk of upstream.body) res.write(chunk);
|
|
325
|
+
res.end();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const state = { model: body.model, started: false, finished: false };
|
|
330
|
+
let buffer = '';
|
|
331
|
+
const decoder = new TextDecoder();
|
|
332
|
+
|
|
333
|
+
function processFrames(frames) {
|
|
334
|
+
for (const frame of frames) {
|
|
335
|
+
const dataLine = frame.split(/\n/).find((line) => line.startsWith('data:'));
|
|
336
|
+
if (!dataLine) continue;
|
|
337
|
+
const payload = dataLine.slice('data:'.length).trim();
|
|
338
|
+
if (!payload || payload === '[DONE]') continue;
|
|
339
|
+
let parsed;
|
|
340
|
+
try {
|
|
341
|
+
parsed = JSON.parse(payload);
|
|
342
|
+
} catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
res.write(openAiStreamChunkToAnthropicEvents(parsed, state));
|
|
346
|
+
}
|
|
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
|
+
}
|
|
359
|
+
if (!state.finished) {
|
|
360
|
+
res.write(openAiStreamChunkToAnthropicEvents({ model: body.model, choices: [{ delta: {}, finish_reason: 'stop' }] }, state));
|
|
361
|
+
}
|
|
362
|
+
res.end();
|
|
363
|
+
}
|
|
364
|
+
|
|
234
365
|
function createGatewayServer(options = {}) {
|
|
235
366
|
const loadConfig = options.loadConfig || defaultLoadConfig;
|
|
236
367
|
return http.createServer(async (req, res) => {
|