yingclaw 2.5.31 → 2.5.38
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 +96 -2
- package/bin/cli.js +118 -14
- package/index.js +1 -0
- package/lib/config.js +44 -11
- package/lib/desktop.js +235 -3
- package/lib/doctor.js +200 -2
- package/lib/gateway.js +313 -10
- package/lib/google.js +138 -0
- package/lib/panel.js +18 -1
- package/package.json +1 -1
package/lib/gateway.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const http = require('http');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
3
4
|
const {
|
|
4
5
|
loadConfig: defaultLoadConfig,
|
|
5
6
|
normalizeAnthropicBaseUrl,
|
|
@@ -13,9 +14,16 @@ const {
|
|
|
13
14
|
openAiStreamChunkToAnthropicEvents,
|
|
14
15
|
openAiToAnthropicMessage,
|
|
15
16
|
} = require('./openai');
|
|
17
|
+
const {
|
|
18
|
+
googleGenerateContentUrl,
|
|
19
|
+
anthropicToGeminiRequest,
|
|
20
|
+
geminiToAnthropicMessage,
|
|
21
|
+
geminiStreamChunkToAnthropicEvents,
|
|
22
|
+
} = require('./google');
|
|
16
23
|
|
|
17
24
|
const DEFAULT_DESKTOP_GATEWAY_PORT = 18080;
|
|
18
25
|
const YINGCLAW_GATEWAY_PREFIX = '/yingclaw';
|
|
26
|
+
const DESKTOP_GATEWAY_MIN_MAX_TOKENS = 4096;
|
|
19
27
|
const DESKTOP_ROUTE_SPECS = [
|
|
20
28
|
{ id: 'claude-sonnet-4-6', displayName: 'Sonnet', upstreamKey: 'model' },
|
|
21
29
|
{ id: 'claude-haiku-4-5', displayName: 'Haiku', upstreamKey: 'fastModel' },
|
|
@@ -93,6 +101,15 @@ function stripThinkingFromBody(body) {
|
|
|
93
101
|
return cleaned;
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
function ensureDesktopGatewayMaxTokens(body) {
|
|
105
|
+
const next = { ...body };
|
|
106
|
+
const current = Number.parseInt(next.max_tokens, 10);
|
|
107
|
+
if (!Number.isFinite(current) || current < DESKTOP_GATEWAY_MIN_MAX_TOKENS) {
|
|
108
|
+
next.max_tokens = DESKTOP_GATEWAY_MIN_MAX_TOKENS;
|
|
109
|
+
}
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
function buildDesktopGatewayRoutes(config) {
|
|
97
114
|
const fastModel = config.fastModel || config.model;
|
|
98
115
|
const configuredModels = [config.model, fastModel];
|
|
@@ -160,6 +177,118 @@ function sendJson(res, status, body) {
|
|
|
160
177
|
res.end(JSON.stringify(body));
|
|
161
178
|
}
|
|
162
179
|
|
|
180
|
+
function extractErrorText(bodyText) {
|
|
181
|
+
const raw = String(bodyText || '');
|
|
182
|
+
try {
|
|
183
|
+
const parsed = JSON.parse(raw);
|
|
184
|
+
return [
|
|
185
|
+
parsed?.error?.message,
|
|
186
|
+
parsed?.error?.code,
|
|
187
|
+
parsed?.message,
|
|
188
|
+
parsed?.code,
|
|
189
|
+
raw,
|
|
190
|
+
].filter(Boolean).join(' ');
|
|
191
|
+
} catch {
|
|
192
|
+
return raw;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function translateUpstreamError(status, bodyText, config = {}) {
|
|
197
|
+
const text = extractErrorText(bodyText);
|
|
198
|
+
const lower = text.toLowerCase();
|
|
199
|
+
const provider = config.providerName || config.provider || '当前厂商';
|
|
200
|
+
let category = 'upstream';
|
|
201
|
+
let message = `${provider} 返回 HTTP ${status}`;
|
|
202
|
+
let suggestion = '请检查模型名、Base URL、API Key 和服务状态。';
|
|
203
|
+
|
|
204
|
+
if (status === 401 || status === 403 || /invalid api key|unauthorized|forbidden|credential|permission|鉴权|认证|权限/i.test(text)) {
|
|
205
|
+
category = 'auth';
|
|
206
|
+
message = 'API Key 无效、过期,或当前 Key 没有访问该模型的权限。';
|
|
207
|
+
suggestion = '运行 claw config 重新输入 Key,并确认 Key 属于当前选择的厂商。';
|
|
208
|
+
} else if (status === 404 || /model not found|not found|unknown model|model.*不存在|模型.*不存在/i.test(text)) {
|
|
209
|
+
category = 'model';
|
|
210
|
+
message = '模型不存在,或该账号不可见这个模型。';
|
|
211
|
+
suggestion = '运行 claw switch 重新获取模型列表,或手动选择控制台里实际可用的模型名。';
|
|
212
|
+
} else if (status === 408 || status === 504 || /timeout|timed out|etimedout|aborted/i.test(lower)) {
|
|
213
|
+
category = 'timeout';
|
|
214
|
+
message = '上游服务响应超时。';
|
|
215
|
+
suggestion = '检查网络/VPN,或换用更快模型后重试。';
|
|
216
|
+
} else if (/subscription|codingplan|coding plan|quota|余额|套餐|订阅/i.test(text)) {
|
|
217
|
+
category = 'subscription';
|
|
218
|
+
message = '账号套餐、余额或 Coding Plan 权限不足。';
|
|
219
|
+
suggestion = '请到厂商控制台确认套餐/余额/模型权限;火山方舟需要开通 Coding Plan 后才能使用相关模型。';
|
|
220
|
+
} else if (status === 429 || /rate limit|too many requests|限流/i.test(text)) {
|
|
221
|
+
category = 'rate_limit';
|
|
222
|
+
message = '请求被限流。';
|
|
223
|
+
suggestion = '稍后重试,或切换到限额更高的 Key/模型。';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
category,
|
|
228
|
+
message,
|
|
229
|
+
suggestion,
|
|
230
|
+
httpStatus: status,
|
|
231
|
+
upstreamMessage: text.slice(0, 1000),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function sendTranslatedUpstreamError(res, status, bodyText, config) {
|
|
236
|
+
const translated = translateUpstreamError(status, bodyText, config);
|
|
237
|
+
sendJson(res, status, { error: translated });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function writeSse(res, event) {
|
|
241
|
+
res.write(`event: ${event.type}\n`);
|
|
242
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function textBlocksFromAnthropicContent(content) {
|
|
246
|
+
const blocks = Array.isArray(content)
|
|
247
|
+
? content.filter((block) => block && block.type === 'text' && typeof block.text === 'string')
|
|
248
|
+
: [];
|
|
249
|
+
return blocks.length > 0 ? blocks : [{ type: 'text', text: '' }];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function sendAnthropicMessageAsStream(res, message, fallbackModel) {
|
|
253
|
+
const usage = message.usage || { input_tokens: 0, output_tokens: 0 };
|
|
254
|
+
const blocks = textBlocksFromAnthropicContent(message.content);
|
|
255
|
+
|
|
256
|
+
res.writeHead(200, {
|
|
257
|
+
'content-type': 'text/event-stream',
|
|
258
|
+
'cache-control': 'no-cache',
|
|
259
|
+
});
|
|
260
|
+
writeSse(res, {
|
|
261
|
+
type: 'message_start',
|
|
262
|
+
message: {
|
|
263
|
+
id: message.id || `msg_${crypto.randomUUID().replace(/-/g, '')}`,
|
|
264
|
+
type: 'message',
|
|
265
|
+
role: 'assistant',
|
|
266
|
+
content: [],
|
|
267
|
+
model: message.model || fallbackModel,
|
|
268
|
+
stop_reason: null,
|
|
269
|
+
stop_sequence: null,
|
|
270
|
+
usage,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
blocks.forEach((block, index) => {
|
|
274
|
+
writeSse(res, { type: 'content_block_start', index, content_block: { type: 'text', text: '' } });
|
|
275
|
+
if (block.text) {
|
|
276
|
+
writeSse(res, { type: 'content_block_delta', index, delta: { type: 'text_delta', text: block.text } });
|
|
277
|
+
}
|
|
278
|
+
writeSse(res, { type: 'content_block_stop', index });
|
|
279
|
+
});
|
|
280
|
+
writeSse(res, {
|
|
281
|
+
type: 'message_delta',
|
|
282
|
+
delta: {
|
|
283
|
+
stop_reason: message.stop_reason || 'end_turn',
|
|
284
|
+
stop_sequence: message.stop_sequence || null,
|
|
285
|
+
},
|
|
286
|
+
usage,
|
|
287
|
+
});
|
|
288
|
+
writeSse(res, { type: 'message_stop' });
|
|
289
|
+
res.end();
|
|
290
|
+
}
|
|
291
|
+
|
|
163
292
|
function readRequestBody(req) {
|
|
164
293
|
return new Promise((resolve, reject) => {
|
|
165
294
|
let raw = '';
|
|
@@ -195,6 +324,68 @@ function buildDesktopGatewayUrl(config = {}) {
|
|
|
195
324
|
};
|
|
196
325
|
}
|
|
197
326
|
|
|
327
|
+
function parseLsofPortOwner(stdout, port) {
|
|
328
|
+
const lines = String(stdout || '').split(/\r?\n/);
|
|
329
|
+
let pid = null;
|
|
330
|
+
let processName = null;
|
|
331
|
+
for (const line of lines) {
|
|
332
|
+
if (line.startsWith('p')) pid = Number.parseInt(line.slice(1), 10);
|
|
333
|
+
if (line.startsWith('c')) processName = line.slice(1) || null;
|
|
334
|
+
}
|
|
335
|
+
return Number.isFinite(pid)
|
|
336
|
+
? { listening: true, port, pid, processName, path: null }
|
|
337
|
+
: { listening: false, port, pid: null, processName: null, path: null };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function checkGatewayPortOwner(port = DEFAULT_DESKTOP_GATEWAY_PORT, options = {}) {
|
|
341
|
+
const runner = options.runner || spawnSync;
|
|
342
|
+
const platform = options.platform || process.platform;
|
|
343
|
+
const portNumber = Number.parseInt(port, 10);
|
|
344
|
+
|
|
345
|
+
if (platform === 'win32') {
|
|
346
|
+
const result = runner('powershell.exe', [
|
|
347
|
+
'-NoProfile',
|
|
348
|
+
'-Command',
|
|
349
|
+
`$c = Get-NetTCPConnection -LocalAddress 127.0.0.1 -LocalPort ${portNumber} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1; if ($c) { $p = Get-Process -Id $c.OwningProcess -ErrorAction SilentlyContinue; [pscustomobject]@{pid=$c.OwningProcess;processName=$p.ProcessName;path=$p.Path} | ConvertTo-Json -Compress }`,
|
|
350
|
+
], { encoding: 'utf8', stdio: 'pipe', windowsHide: true });
|
|
351
|
+
if (result.status !== 0 || !String(result.stdout || '').trim()) {
|
|
352
|
+
return { listening: false, port: portNumber, pid: null, processName: null, path: null };
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(String(result.stdout).trim());
|
|
356
|
+
return {
|
|
357
|
+
listening: true,
|
|
358
|
+
port: portNumber,
|
|
359
|
+
pid: Number(parsed.pid) || null,
|
|
360
|
+
processName: parsed.processName || null,
|
|
361
|
+
path: parsed.path || null,
|
|
362
|
+
};
|
|
363
|
+
} catch {
|
|
364
|
+
return { listening: true, port: portNumber, pid: null, processName: null, path: null };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const result = runner('lsof', ['-nP', `-iTCP:${portNumber}`, '-sTCP:LISTEN', '-Fp', '-Fc'], {
|
|
369
|
+
encoding: 'utf8',
|
|
370
|
+
stdio: 'pipe',
|
|
371
|
+
});
|
|
372
|
+
if (result.status !== 0 || !String(result.stdout || '').trim()) {
|
|
373
|
+
return { listening: false, port: portNumber, pid: null, processName: null, path: null };
|
|
374
|
+
}
|
|
375
|
+
return parseLsofPortOwner(result.stdout, portNumber);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function explainGatewayListenError(error, owner = null) {
|
|
379
|
+
if (error && error.code === 'EADDRINUSE') {
|
|
380
|
+
const port = owner?.port || DEFAULT_DESKTOP_GATEWAY_PORT;
|
|
381
|
+
const processText = owner?.processName
|
|
382
|
+
? `${owner.processName}${owner.pid ? ` (PID ${owner.pid})` : ''}`
|
|
383
|
+
: '其他进程';
|
|
384
|
+
return `Gateway 启动失败:端口 ${port} 已被 ${processText} 占用。请关闭旧 gateway,或使用 claw gateway --port <新端口> 后重新运行 claw desktop。`;
|
|
385
|
+
}
|
|
386
|
+
return `Gateway 启动失败: ${error?.message || error}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
198
389
|
async function checkDesktopGatewayStatus(config = {}, options = {}) {
|
|
199
390
|
const { port, url } = buildDesktopGatewayUrl(config);
|
|
200
391
|
const fetchImpl = options.fetch || fetch;
|
|
@@ -243,27 +434,60 @@ async function proxyMessages(req, res, config) {
|
|
|
243
434
|
return;
|
|
244
435
|
}
|
|
245
436
|
|
|
246
|
-
|
|
437
|
+
const protocol = getProviderProtocol(config);
|
|
438
|
+
if (protocol === 'anthropic' && !isNativeClaudeModel(body.model)) {
|
|
439
|
+
body = ensureDesktopGatewayMaxTokens(stripThinkingFromBody(body));
|
|
440
|
+
} else if (!isNativeClaudeModel(body.model)) {
|
|
247
441
|
body = stripThinkingFromBody(body);
|
|
248
442
|
}
|
|
249
443
|
|
|
250
|
-
if (
|
|
444
|
+
if (protocol === 'openai') {
|
|
251
445
|
await proxyOpenAiMessages(res, config, body);
|
|
252
446
|
return;
|
|
253
447
|
}
|
|
448
|
+
if (protocol === 'google') {
|
|
449
|
+
await proxyGoogleMessages(res, config, body);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
254
452
|
|
|
453
|
+
const upstreamBody = body.stream && !isNativeClaudeModel(body.model)
|
|
454
|
+
? { ...body, stream: false }
|
|
455
|
+
: body;
|
|
255
456
|
const upstream = await fetch(providerMessagesUrl(config), {
|
|
256
457
|
method: 'POST',
|
|
257
458
|
headers: {
|
|
258
459
|
'content-type': 'application/json',
|
|
259
|
-
accept:
|
|
460
|
+
accept: upstreamBody.stream ? 'text/event-stream' : 'application/json',
|
|
260
461
|
...buildProviderAuthHeaders(config.provider, config.apiKey),
|
|
261
462
|
'x-api-key': config.apiKey,
|
|
262
463
|
'anthropic-version': '2023-06-01',
|
|
263
464
|
},
|
|
264
|
-
body: JSON.stringify(
|
|
465
|
+
body: JSON.stringify(upstreamBody),
|
|
265
466
|
});
|
|
266
467
|
|
|
468
|
+
if (body.stream && !upstreamBody.stream) {
|
|
469
|
+
const text = await upstream.text();
|
|
470
|
+
if (!upstream.ok) {
|
|
471
|
+
sendTranslatedUpstreamError(res, upstream.status, text, config);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
let parsed;
|
|
475
|
+
try {
|
|
476
|
+
parsed = text ? JSON.parse(text) : {};
|
|
477
|
+
} catch {
|
|
478
|
+
sendJson(res, 502, { error: { message: 'Anthropic 兼容接口返回了无效 JSON' } });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
sendAnthropicMessageAsStream(res, parsed, body.model);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!upstream.ok) {
|
|
486
|
+
const text = await streamBodyToText(upstream.body);
|
|
487
|
+
sendTranslatedUpstreamError(res, upstream.status, text, config);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
267
491
|
res.writeHead(upstream.status, {
|
|
268
492
|
'content-type': upstream.headers.get('content-type') || (body.stream ? 'text/event-stream' : 'application/json'),
|
|
269
493
|
'cache-control': upstream.headers.get('cache-control') || 'no-cache',
|
|
@@ -294,10 +518,7 @@ async function proxyOpenAiMessages(res, config, body) {
|
|
|
294
518
|
if (!body.stream) {
|
|
295
519
|
const text = await upstream.text();
|
|
296
520
|
if (!upstream.ok) {
|
|
297
|
-
res
|
|
298
|
-
'content-type': upstream.headers.get('content-type') || 'application/json',
|
|
299
|
-
});
|
|
300
|
-
res.end(text);
|
|
521
|
+
sendTranslatedUpstreamError(res, upstream.status, text, config);
|
|
301
522
|
return;
|
|
302
523
|
}
|
|
303
524
|
|
|
@@ -321,8 +542,8 @@ async function proxyOpenAiMessages(res, config, body) {
|
|
|
321
542
|
return;
|
|
322
543
|
}
|
|
323
544
|
if (!upstream.ok) {
|
|
324
|
-
|
|
325
|
-
res.
|
|
545
|
+
const text = await streamBodyToText(upstream.body);
|
|
546
|
+
sendTranslatedUpstreamError(res, upstream.status, text, config);
|
|
326
547
|
return;
|
|
327
548
|
}
|
|
328
549
|
|
|
@@ -362,6 +583,84 @@ async function proxyOpenAiMessages(res, config, body) {
|
|
|
362
583
|
res.end();
|
|
363
584
|
}
|
|
364
585
|
|
|
586
|
+
async function streamBodyToText(body) {
|
|
587
|
+
if (!body) return '';
|
|
588
|
+
const chunks = [];
|
|
589
|
+
const decoder = new TextDecoder();
|
|
590
|
+
for await (const chunk of body) {
|
|
591
|
+
chunks.push(typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }));
|
|
592
|
+
}
|
|
593
|
+
chunks.push(decoder.decode());
|
|
594
|
+
return chunks.join('');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function proxyGoogleMessages(res, config, body) {
|
|
598
|
+
const stream = !!body.stream;
|
|
599
|
+
const geminiBody = anthropicToGeminiRequest(body);
|
|
600
|
+
const upstream = await fetch(googleGenerateContentUrl(config.baseUrl, body.model, stream), {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: {
|
|
603
|
+
'content-type': 'application/json',
|
|
604
|
+
...buildProviderAuthHeaders(config.provider, config.apiKey),
|
|
605
|
+
},
|
|
606
|
+
body: JSON.stringify(geminiBody),
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
if (!stream) {
|
|
610
|
+
const text = await upstream.text();
|
|
611
|
+
if (!upstream.ok) {
|
|
612
|
+
sendTranslatedUpstreamError(res, upstream.status, text, config);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
let parsed;
|
|
616
|
+
try { parsed = text ? JSON.parse(text) : {}; } catch {
|
|
617
|
+
sendJson(res, 502, { error: { message: 'Google 兼容接口返回了无效 JSON' } });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
sendJson(res, upstream.status, geminiToAnthropicMessage(parsed, body.model));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
res.writeHead(upstream.status, {
|
|
625
|
+
'content-type': upstream.ok ? 'text/event-stream' : (upstream.headers.get('content-type') || 'application/json'),
|
|
626
|
+
'cache-control': 'no-cache',
|
|
627
|
+
});
|
|
628
|
+
if (!upstream.body) { res.end(); return; }
|
|
629
|
+
if (!upstream.ok) {
|
|
630
|
+
const text = await streamBodyToText(upstream.body);
|
|
631
|
+
sendTranslatedUpstreamError(res, upstream.status, text, config);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const state = { model: body.model, started: false, finished: false };
|
|
636
|
+
let buffer = '';
|
|
637
|
+
const decoder = new TextDecoder();
|
|
638
|
+
|
|
639
|
+
function processFrames(frames) {
|
|
640
|
+
for (const frame of frames) {
|
|
641
|
+
const dataLine = frame.split(/\n/).find((line) => line.startsWith('data:'));
|
|
642
|
+
if (!dataLine) continue;
|
|
643
|
+
const payload = dataLine.slice('data:'.length).trim();
|
|
644
|
+
if (!payload || payload === '[DONE]') continue;
|
|
645
|
+
let parsed;
|
|
646
|
+
try { parsed = JSON.parse(payload); } catch { continue; }
|
|
647
|
+
res.write(geminiStreamChunkToAnthropicEvents(parsed, state));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
for await (const chunk of upstream.body) {
|
|
652
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
653
|
+
const frames = buffer.split(/\n\n/);
|
|
654
|
+
buffer = frames.pop() || '';
|
|
655
|
+
processFrames(frames);
|
|
656
|
+
}
|
|
657
|
+
if (buffer.trim()) processFrames([buffer]);
|
|
658
|
+
if (!state.finished) {
|
|
659
|
+
res.write(geminiStreamChunkToAnthropicEvents({ candidates: [{ finishReason: 'STOP' }] }, state));
|
|
660
|
+
}
|
|
661
|
+
res.end();
|
|
662
|
+
}
|
|
663
|
+
|
|
365
664
|
function createGatewayServer(options = {}) {
|
|
366
665
|
const loadConfig = options.loadConfig || defaultLoadConfig;
|
|
367
666
|
return http.createServer(async (req, res) => {
|
|
@@ -406,6 +705,7 @@ function createGatewayServer(options = {}) {
|
|
|
406
705
|
|
|
407
706
|
module.exports = {
|
|
408
707
|
DEFAULT_DESKTOP_GATEWAY_PORT,
|
|
708
|
+
DESKTOP_GATEWAY_MIN_MAX_TOKENS,
|
|
409
709
|
YINGCLAW_GATEWAY_PREFIX,
|
|
410
710
|
DESKTOP_ROUTE_SPECS,
|
|
411
711
|
ONE_M_CONTEXT_SUFFIX,
|
|
@@ -421,6 +721,9 @@ module.exports = {
|
|
|
421
721
|
mapDesktopRouteToUpstream,
|
|
422
722
|
buildGatewayModelsResponse,
|
|
423
723
|
buildDesktopGatewayUrl,
|
|
724
|
+
checkGatewayPortOwner,
|
|
424
725
|
checkDesktopGatewayStatus,
|
|
726
|
+
explainGatewayListenError,
|
|
727
|
+
translateUpstreamError,
|
|
425
728
|
createGatewayServer,
|
|
426
729
|
};
|
package/lib/google.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
function normalizeGoogleBaseUrl(baseUrl) {
|
|
2
|
+
let url;
|
|
3
|
+
try { url = new URL(baseUrl); } catch { return baseUrl; }
|
|
4
|
+
let pathname = url.pathname.replace(/\/+$/, '');
|
|
5
|
+
// strip trailing /models/something:action or /models
|
|
6
|
+
pathname = pathname.replace(/\/models\/[^/]+(:[^?]*)?(\?.*)?$/, '');
|
|
7
|
+
pathname = pathname.replace(/\/models$/, '');
|
|
8
|
+
url.pathname = pathname || '/';
|
|
9
|
+
url.search = '';
|
|
10
|
+
url.hash = '';
|
|
11
|
+
return url.toString().replace(/\/+$/, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function googleGenerateContentUrl(baseUrl, model, stream) {
|
|
15
|
+
const base = normalizeGoogleBaseUrl(baseUrl);
|
|
16
|
+
const action = stream ? 'streamGenerateContent?alt=sse' : 'generateContent';
|
|
17
|
+
return `${base}/models/${encodeURIComponent(model)}:${action}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function anthropicContentToText(content) {
|
|
21
|
+
if (typeof content === 'string') return content;
|
|
22
|
+
if (!Array.isArray(content)) return '';
|
|
23
|
+
return content.filter((p) => p?.type === 'text').map((p) => p.text || '').join('');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function anthropicToGeminiRequest(body) {
|
|
27
|
+
const contents = (body.messages || []).map((msg) => ({
|
|
28
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
29
|
+
parts: [{ text: anthropicContentToText(msg.content) }],
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const request = { contents };
|
|
33
|
+
|
|
34
|
+
const systemText = typeof body.system === 'string'
|
|
35
|
+
? body.system
|
|
36
|
+
: Array.isArray(body.system)
|
|
37
|
+
? body.system.filter((p) => p?.type === 'text').map((p) => p.text || '').join('')
|
|
38
|
+
: '';
|
|
39
|
+
if (systemText) request.systemInstruction = { parts: [{ text: systemText }] };
|
|
40
|
+
|
|
41
|
+
const genConfig = {};
|
|
42
|
+
if (body.max_tokens !== undefined) genConfig.maxOutputTokens = body.max_tokens;
|
|
43
|
+
if (body.temperature !== undefined) genConfig.temperature = body.temperature;
|
|
44
|
+
if (body.top_p !== undefined) genConfig.topP = body.top_p;
|
|
45
|
+
if (Array.isArray(body.stop_sequences) && body.stop_sequences.length > 0) {
|
|
46
|
+
genConfig.stopSequences = body.stop_sequences;
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(genConfig).length > 0) request.generationConfig = genConfig;
|
|
49
|
+
|
|
50
|
+
return request;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function geminiFinishReasonToAnthropic(reason) {
|
|
54
|
+
if (reason === 'MAX_TOKENS') return 'max_tokens';
|
|
55
|
+
return 'end_turn';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function geminiToAnthropicMessage(body, model) {
|
|
59
|
+
const candidate = body?.candidates?.[0] || {};
|
|
60
|
+
const parts = candidate.content?.parts || [];
|
|
61
|
+
const text = parts.map((p) => p.text || '').join('');
|
|
62
|
+
return {
|
|
63
|
+
id: `msg_${Date.now()}`,
|
|
64
|
+
type: 'message',
|
|
65
|
+
role: 'assistant',
|
|
66
|
+
model,
|
|
67
|
+
content: [{ type: 'text', text }],
|
|
68
|
+
stop_reason: geminiFinishReasonToAnthropic(candidate.finishReason),
|
|
69
|
+
stop_sequence: null,
|
|
70
|
+
usage: {
|
|
71
|
+
input_tokens: body?.usageMetadata?.promptTokenCount ?? 0,
|
|
72
|
+
output_tokens: body?.usageMetadata?.candidatesTokenCount ?? 0,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function anthropicSse(event, data) {
|
|
78
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function geminiStreamChunkToAnthropicEvents(chunk, state) {
|
|
82
|
+
const candidate = chunk?.candidates?.[0] || {};
|
|
83
|
+
const parts = candidate.content?.parts || [];
|
|
84
|
+
const text = parts.map((p) => p.text || '').join('');
|
|
85
|
+
const events = [];
|
|
86
|
+
|
|
87
|
+
if (!state.started) {
|
|
88
|
+
state.started = true;
|
|
89
|
+
events.push(anthropicSse('message_start', {
|
|
90
|
+
type: 'message_start',
|
|
91
|
+
message: {
|
|
92
|
+
id: `msg_${Date.now()}`,
|
|
93
|
+
type: 'message',
|
|
94
|
+
role: 'assistant',
|
|
95
|
+
model: state.model,
|
|
96
|
+
content: [],
|
|
97
|
+
stop_reason: null,
|
|
98
|
+
stop_sequence: null,
|
|
99
|
+
usage: { input_tokens: chunk?.usageMetadata?.promptTokenCount ?? 0, output_tokens: 0 },
|
|
100
|
+
},
|
|
101
|
+
}));
|
|
102
|
+
events.push(anthropicSse('content_block_start', {
|
|
103
|
+
type: 'content_block_start',
|
|
104
|
+
index: 0,
|
|
105
|
+
content_block: { type: 'text', text: '' },
|
|
106
|
+
}));
|
|
107
|
+
events.push(anthropicSse('ping', { type: 'ping' }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (text) {
|
|
111
|
+
events.push(anthropicSse('content_block_delta', {
|
|
112
|
+
type: 'content_block_delta',
|
|
113
|
+
index: 0,
|
|
114
|
+
delta: { type: 'text_delta', text },
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (candidate.finishReason) {
|
|
119
|
+
state.finished = true;
|
|
120
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: 0 }));
|
|
121
|
+
events.push(anthropicSse('message_delta', {
|
|
122
|
+
type: 'message_delta',
|
|
123
|
+
delta: { stop_reason: geminiFinishReasonToAnthropic(candidate.finishReason), stop_sequence: null },
|
|
124
|
+
usage: { output_tokens: chunk?.usageMetadata?.candidatesTokenCount ?? 0 },
|
|
125
|
+
}));
|
|
126
|
+
events.push(anthropicSse('message_stop', { type: 'message_stop' }));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return events.join('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
normalizeGoogleBaseUrl,
|
|
134
|
+
googleGenerateContentUrl,
|
|
135
|
+
anthropicToGeminiRequest,
|
|
136
|
+
geminiToAnthropicMessage,
|
|
137
|
+
geminiStreamChunkToAnthropicEvents,
|
|
138
|
+
};
|
package/lib/panel.js
CHANGED
|
@@ -27,6 +27,13 @@ function desktopGatewayStatusText(status, autostartStatus = null) {
|
|
|
27
27
|
return `未运行:执行 claw gateway${suffix}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function desktopCodeStatusText(status) {
|
|
31
|
+
if (!status) return null;
|
|
32
|
+
if (status.configured) return status.simpleEnabled ? '已配置 · simple' : '已配置';
|
|
33
|
+
const count = (status.missing?.length || 0) + (status.mismatched?.length || 0);
|
|
34
|
+
return count > 0 ? `未配置:${count} 项异常` : '未配置';
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
function buildStatusView(config, options = {}) {
|
|
31
38
|
const provider = PROVIDERS[config.provider];
|
|
32
39
|
const providerName = config.providerName || provider?.name || config.provider;
|
|
@@ -39,6 +46,8 @@ function buildStatusView(config, options = {}) {
|
|
|
39
46
|
const desktopGatewayStatus = options.desktopGatewayStatus;
|
|
40
47
|
const desktopGatewayAutostartStatus = options.desktopGatewayAutostartStatus;
|
|
41
48
|
const desktopGatewayText = desktopGatewayStatusText(desktopGatewayStatus, desktopGatewayAutostartStatus);
|
|
49
|
+
const desktopCodeStatus = options.desktopCodeStatus;
|
|
50
|
+
const desktopCodeText = desktopCodeStatusText(desktopCodeStatus);
|
|
42
51
|
const availableModelCount = Array.isArray(config.availableModels) ? config.availableModels.length : 0;
|
|
43
52
|
const mainFamily = getModelFamily(mainModel);
|
|
44
53
|
const fastFamily = getModelFamily(fastModel);
|
|
@@ -71,6 +80,7 @@ function buildStatusView(config, options = {}) {
|
|
|
71
80
|
{ label: 'Claude Code', value: claudeInstalled ? '已安装' : '未检测到' },
|
|
72
81
|
{ label: '当前终端', value: envActive ? '已生效' : '未生效' },
|
|
73
82
|
...(desktopGatewayText ? [{ label: 'Desktop Gateway', value: desktopGatewayText }] : []),
|
|
83
|
+
...(desktopCodeText ? [{ label: 'Desktop Code', value: desktopCodeText }] : []),
|
|
74
84
|
{ label: 'Base URL', value: config.baseUrl },
|
|
75
85
|
],
|
|
76
86
|
};
|
|
@@ -80,6 +90,9 @@ function buildStatusView(config, options = {}) {
|
|
|
80
90
|
if (desktopGatewayAutostartStatus) {
|
|
81
91
|
view.desktopGatewayAutostartStatus = desktopGatewayAutostartStatus;
|
|
82
92
|
}
|
|
93
|
+
if (desktopCodeStatus) {
|
|
94
|
+
view.desktopCodeStatus = desktopCodeStatus;
|
|
95
|
+
}
|
|
83
96
|
return view;
|
|
84
97
|
}
|
|
85
98
|
|
|
@@ -109,6 +122,10 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
109
122
|
if (desktopGatewayText && desktopGatewayText !== '未配置') {
|
|
110
123
|
lines.push(`Desktop Gateway ${desktopGatewayText}`);
|
|
111
124
|
}
|
|
125
|
+
const desktopCodeStatus = view.desktopCodeStatus || options.desktopCodeStatus;
|
|
126
|
+
if (desktopCodeStatus && desktopCodeStatus.hasYingclawEnv && !desktopCodeStatus.configured) {
|
|
127
|
+
lines.push('Desktop Code 配置不完整:需要时运行 claw desktop --with-code 后重启 Claude');
|
|
128
|
+
}
|
|
112
129
|
|
|
113
130
|
if (view.warnings.some((warning) => warning.includes('旧 DeepSeek 模型名'))) {
|
|
114
131
|
lines.push('旧模型名:选择下方"切换厂商或模型"更新到 [1m]');
|
|
@@ -121,4 +138,4 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
121
138
|
return lines;
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, desktopGatewayAutostartText, desktopGatewayStatusText, isEnvActive };
|
|
141
|
+
module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, desktopCodeStatusText, desktopGatewayAutostartText, desktopGatewayStatusText, isEnvActive };
|