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/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
- if (!isNativeClaudeModel(body.model)) {
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 (getProviderProtocol(config) === 'openai') {
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: body.stream ? 'text/event-stream' : 'application/json',
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(body),
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.writeHead(upstream.status, {
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
- for await (const chunk of upstream.body) res.write(chunk);
325
- res.end();
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "2.5.31",
3
+ "version": "2.5.38",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、火山方舟、Qwen、MiniMax、GLM、MiMo、自定义接口",
5
5
  "main": "index.js",
6
6
  "bin": {