yingclaw 2.5.19 → 2.5.25

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/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
- baseUrl: 'https://api.moonshot.ai/anthropic',
37
- modelsUrl: 'https://api.moonshot.ai/v1/models',
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',
@@ -90,6 +112,13 @@ const PROVIDERS = {
90
112
  custom: {
91
113
  name: '自定义 Anthropic 兼容接口',
92
114
  custom: true,
115
+ protocol: 'anthropic',
116
+ models: [],
117
+ },
118
+ custom_openai: {
119
+ name: '自定义 OpenAI 兼容接口',
120
+ custom: true,
121
+ protocol: 'openai',
93
122
  models: [],
94
123
  },
95
124
  };
@@ -110,11 +139,28 @@ function sortPreferredModelIds(providerKey, ids) {
110
139
  ];
111
140
  }
112
141
 
142
+ function isChatModelId(model) {
143
+ const value = String(model || '');
144
+ return !/(^|[-_])(tts|voice|voiceclone|voicedesign|speech|audio|image|video|embedding|embed|rerank|moderation)([-_]|$)/i.test(value)
145
+ && !/(^|[-_])(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);
147
+ }
148
+
113
149
  function normalizeModelIds(providerKey, ids) {
114
150
  if (PREFERRED_MODEL_IDS[providerKey]) {
115
151
  return sortPreferredModelIds(providerKey, ids);
116
152
  }
117
153
 
154
+ if (providerKey === 'kimi') {
155
+ const preferred = PROVIDERS.kimi.models.map((item) => item.value);
156
+ const merged = [...new Set([...preferred, ...ids])];
157
+ return [
158
+ ...preferred.filter((id) => merged.includes(id)),
159
+ ...merged.filter((id) => !preferred.includes(id) && /^kimi-/i.test(id)),
160
+ ...merged.filter((id) => !preferred.includes(id) && !/^kimi-/i.test(id)),
161
+ ];
162
+ }
163
+
118
164
  if (providerKey !== 'deepseek') return ids;
119
165
 
120
166
  const mapped = ids.map((id) => {
@@ -132,7 +178,13 @@ function normalizeModelIds(providerKey, ids) {
132
178
  function parseModelIdsResponse(providerKey, data) {
133
179
  const parsed = typeof data === 'string' ? JSON.parse(data) : data;
134
180
  const list = parsed.data || parsed.models || [];
135
- const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
181
+ const availableList = providerKey === 'volcengine'
182
+ ? list.filter((model) => !/^shutdown$/i.test(String(model.status || '')))
183
+ : list;
184
+ const ids = availableList.map(m => m.id || m.model || m.name).filter(Boolean);
185
+ if (providerKey === 'volcengine') {
186
+ return normalizeModelIds(providerKey, ids.filter(isChatModelId));
187
+ }
136
188
  return normalizeModelIds(providerKey, ids);
137
189
  }
138
190
 
@@ -155,7 +207,7 @@ function normalizeAnthropicBaseUrl(baseUrl) {
155
207
 
156
208
  function buildModelUrlCandidates(baseUrl) {
157
209
  let url;
158
- try { url = new URL(normalizeAnthropicBaseUrl(baseUrl)); } catch { return []; }
210
+ try { url = new URL(normalizeAnthropicBaseUrl(normalizeOpenAiBaseUrl(baseUrl))); } catch { return []; }
159
211
 
160
212
  const pathname = url.pathname.replace(/\/+$/, '');
161
213
  const candidates = [];
@@ -209,10 +261,7 @@ async function fetchModels(providerKey, apiKey, modelsUrlOverride) {
209
261
  const res = await fetch(modelsUrl, {
210
262
  method: 'GET',
211
263
  signal: controller.signal,
212
- headers: {
213
- authorization: `Bearer ${apiKey}`,
214
- 'api-key': apiKey, // MiMo 用这个 header
215
- },
264
+ headers: buildProviderAuthHeaders(providerKey, apiKey),
216
265
  });
217
266
  if (!res.ok) return null;
218
267
 
@@ -225,9 +274,22 @@ async function fetchModels(providerKey, apiKey, modelsUrlOverride) {
225
274
  }
226
275
  }
227
276
 
228
- function loadConfig() {
277
+ function migrateConfig(config) {
278
+ if (!config || typeof config !== 'object') return config;
279
+ if (config.provider === 'kimi') {
280
+ return {
281
+ ...config,
282
+ protocol: 'openai',
283
+ baseUrl: PROVIDERS.kimi.baseUrl,
284
+ modelsUrl: PROVIDERS.kimi.modelsUrl,
285
+ };
286
+ }
287
+ return config;
288
+ }
289
+
290
+ function loadConfig(options = {}) {
229
291
  try {
230
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
292
+ return migrateConfig(JSON.parse(fs.readFileSync(options.configFile || CONFIG_FILE, 'utf8')));
231
293
  } catch {
232
294
  return null;
233
295
  }
@@ -259,16 +321,54 @@ function providerKeyFromBaseUrl(baseUrl) {
259
321
  return Object.entries(PROVIDERS).find(([, provider]) => provider.baseUrl === baseUrl)?.[0];
260
322
  }
261
323
 
324
+ function getProviderProtocol(config = {}) {
325
+ return config.protocol || PROVIDERS[config.provider]?.protocol || 'anthropic';
326
+ }
327
+
328
+ function buildProviderAuthHeaders(providerKey, apiKey) {
329
+ const headers = { authorization: `Bearer ${apiKey}` };
330
+ if (getProviderProtocol({ provider: providerKey }) !== 'openai') {
331
+ headers['api-key'] = apiKey; // MiMo 用这个 header
332
+ }
333
+ return headers;
334
+ }
335
+
262
336
  function resolveFastModel(provider, model) {
263
337
  if (/flash|turbo|haiku|air|lite/i.test(model)) return model;
264
338
  return provider?.fastModel || model;
265
339
  }
266
340
 
267
- function buildClaudeEnv({ provider, baseUrl, apiKey, model, fastModel }) {
341
+ function getModelFamily(model) {
342
+ const value = String(model || '').trim().toLowerCase();
343
+ if (!value) return '';
344
+ const normalized = value
345
+ .replace(/^anthropic\//, '')
346
+ .replace(/^\w+\//, '');
347
+ return normalized.split(/[-_.]/).filter(Boolean)[0] || normalized;
348
+ }
349
+
350
+ function sortRelatedModelIds(ids, mainModel) {
351
+ const unique = [...new Set(ids.filter(Boolean))];
352
+ const family = getModelFamily(mainModel);
353
+ return [
354
+ ...unique.filter((id) => id === mainModel),
355
+ ...unique.filter((id) => id !== mainModel && getModelFamily(id) === family),
356
+ ...unique.filter((id) => id !== mainModel && getModelFamily(id) !== family),
357
+ ];
358
+ }
359
+
360
+ function buildGatewayBaseUrlForEnv(config) {
361
+ const port = Number.parseInt(config.desktopGatewayPort || 18080, 10);
362
+ return `http://127.0.0.1:${port}/yingclaw`;
363
+ }
364
+
365
+ function buildClaudeEnv(config) {
366
+ const { provider, baseUrl, apiKey, model, fastModel } = config;
268
367
  const resolvedFastModel = fastModel || resolveFastModel(PROVIDERS[provider], model);
368
+ const useLocalGateway = getProviderProtocol(config) === 'openai' && config.desktopGatewayKey;
269
369
  return {
270
- ANTHROPIC_BASE_URL: baseUrl,
271
- ANTHROPIC_AUTH_TOKEN: apiKey,
370
+ ANTHROPIC_BASE_URL: useLocalGateway ? buildGatewayBaseUrlForEnv(config) : baseUrl,
371
+ ANTHROPIC_AUTH_TOKEN: useLocalGateway ? config.desktopGatewayKey : apiKey,
272
372
  ANTHROPIC_MODEL: model,
273
373
  ANTHROPIC_DEFAULT_OPUS_MODEL: model,
274
374
  ANTHROPIC_DEFAULT_SONNET_MODEL: model,
@@ -311,7 +411,9 @@ function classifyValidationStatus(statusCode) {
311
411
  async function validateKey(config, options = {}) {
312
412
  let url;
313
413
  try {
314
- url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
414
+ url = getProviderProtocol(config) === 'openai'
415
+ ? openAiChatCompletionsUrl(config.baseUrl)
416
+ : `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
315
417
  new URL(url);
316
418
  } catch {
317
419
  return null;
@@ -325,16 +427,21 @@ async function validateKey(config, options = {}) {
325
427
  signal: controller.signal,
326
428
  headers: {
327
429
  'content-type': 'application/json',
328
- authorization: `Bearer ${config.apiKey}`,
329
- 'x-api-key': config.apiKey,
330
- 'api-key': config.apiKey,
331
- 'anthropic-version': '2023-06-01',
430
+ ...buildProviderAuthHeaders(config.provider, config.apiKey),
431
+ ...(getProviderProtocol(config) === 'anthropic' ? { 'x-api-key': config.apiKey } : {}),
432
+ ...(getProviderProtocol(config) === 'anthropic' ? { 'anthropic-version': '2023-06-01' } : {}),
332
433
  },
333
- body: JSON.stringify({
334
- model: config.model,
335
- max_tokens: 1,
336
- messages: [{ role: 'user', content: 'hi' }],
337
- }),
434
+ body: JSON.stringify(getProviderProtocol(config) === 'openai'
435
+ ? {
436
+ model: config.model,
437
+ max_tokens: 1,
438
+ messages: [{ role: 'user', content: 'hi' }],
439
+ }
440
+ : {
441
+ model: config.model,
442
+ max_tokens: 1,
443
+ messages: [{ role: 'user', content: 'hi' }],
444
+ }),
338
445
  });
339
446
  return classifyValidationStatus(res.status);
340
447
  } catch {
@@ -465,6 +572,11 @@ module.exports = {
465
572
  buildModelUrlCandidates,
466
573
  fetchModelsFromBaseUrl,
467
574
  resolveFastModel,
575
+ getModelFamily,
576
+ sortRelatedModelIds,
577
+ isChatModelId,
578
+ buildProviderAuthHeaders,
579
+ getProviderProtocol,
468
580
  providerKeyFromBaseUrl,
469
581
  buildClaudeEnv,
470
582
  buildEnvBlock,
@@ -473,5 +585,6 @@ module.exports = {
473
585
  PROVIDERS,
474
586
  CONFIG_FILE,
475
587
  CLAUDE_ENV_KEYS,
588
+ VSCODE_CLAUDE_ENV_KEYS,
476
589
  CLEAR_CLAUDE_ENV_KEYS,
477
590
  };
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. API Key(顺便确认网络可达)
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 ? '共享 settings.json 已写入' : `${vscodeSettings.missing.length} 个变量未写入共享设置`,
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
- // 7. Claude 桌面应用接入状态
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
- // 8. DeepSeek 旧模型名提醒
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 { loadConfig: defaultLoadConfig, normalizeAnthropicBaseUrl } = require('./config');
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,7 @@ function desktopRouteLabel(routeId) {
59
71
  }
60
72
 
61
73
  function isDesktopChatModel(model) {
62
- const value = String(model || '');
63
- return !/(^|[-_])(tts|voice|voiceclone|voicedesign|speech|audio|image|video|embedding|embed|rerank|moderation)([-_]|$)/i.test(value)
64
- && !/(cogview|cogvideo|wanx|flux|sdxl|stable-diffusion)/i.test(value);
74
+ return isChatModelId(model);
65
75
  }
66
76
 
67
77
  function buildDesktopGatewayRoutes(config) {
@@ -93,6 +103,13 @@ function buildDesktopGatewayMappingRows(config) {
93
103
 
94
104
  function mapDesktopRouteToUpstream(config, routeId) {
95
105
  const requested = stripOneMContextSuffix(routeId);
106
+ const configuredModels = [
107
+ config.model,
108
+ config.fastModel || config.model,
109
+ ...(Array.isArray(config.availableModels) ? config.availableModels : []),
110
+ ].filter(Boolean);
111
+ if (configuredModels.includes(routeId)) return routeId;
112
+
96
113
  const route = buildDesktopGatewayRoutes(config).find((item) => stripOneMContextSuffix(item.id) === requested);
97
114
  if (!route) {
98
115
  throw new Error(`未配置的 Claude Desktop 模型路由: ${routeId}`);
@@ -147,6 +164,10 @@ function providerMessagesUrl(config) {
147
164
  return `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
148
165
  }
149
166
 
167
+ function providerOpenAiChatUrl(config) {
168
+ return openAiChatCompletionsUrl(config.baseUrl);
169
+ }
170
+
150
171
  function buildDesktopGatewayUrl(config = {}) {
151
172
  const port = Number.parseInt(config.desktopGatewayPort || DEFAULT_DESKTOP_GATEWAY_PORT, 10);
152
173
  return {
@@ -203,14 +224,18 @@ async function proxyMessages(req, res, config) {
203
224
  return;
204
225
  }
205
226
 
227
+ if (getProviderProtocol(config) === 'openai') {
228
+ await proxyOpenAiMessages(res, config, body);
229
+ return;
230
+ }
231
+
206
232
  const upstream = await fetch(providerMessagesUrl(config), {
207
233
  method: 'POST',
208
234
  headers: {
209
235
  'content-type': 'application/json',
210
236
  accept: body.stream ? 'text/event-stream' : 'application/json',
211
- authorization: `Bearer ${config.apiKey}`,
237
+ ...buildProviderAuthHeaders(config.provider, config.apiKey),
212
238
  'x-api-key': config.apiKey,
213
- 'api-key': config.apiKey,
214
239
  'anthropic-version': '2023-06-01',
215
240
  },
216
241
  body: JSON.stringify(body),
@@ -231,6 +256,80 @@ async function proxyMessages(req, res, config) {
231
256
  res.end();
232
257
  }
233
258
 
259
+ async function proxyOpenAiMessages(res, config, body) {
260
+ const openAiBody = anthropicToOpenAiChatRequest(body);
261
+ const upstream = await fetch(providerOpenAiChatUrl(config), {
262
+ method: 'POST',
263
+ headers: {
264
+ 'content-type': 'application/json',
265
+ accept: body.stream ? 'text/event-stream' : 'application/json',
266
+ ...buildProviderAuthHeaders(config.provider, config.apiKey),
267
+ },
268
+ body: JSON.stringify(openAiBody),
269
+ });
270
+
271
+ if (!body.stream) {
272
+ const text = await upstream.text();
273
+ if (!upstream.ok) {
274
+ res.writeHead(upstream.status, {
275
+ 'content-type': upstream.headers.get('content-type') || 'application/json',
276
+ });
277
+ res.end(text);
278
+ return;
279
+ }
280
+
281
+ let parsed;
282
+ try {
283
+ parsed = text ? JSON.parse(text) : {};
284
+ } catch {
285
+ sendJson(res, 502, { error: { message: 'OpenAI 兼容接口返回了无效 JSON' } });
286
+ return;
287
+ }
288
+ sendJson(res, upstream.status, openAiToAnthropicMessage(parsed, body.model));
289
+ return;
290
+ }
291
+
292
+ res.writeHead(upstream.status, {
293
+ 'content-type': upstream.ok ? 'text/event-stream' : (upstream.headers.get('content-type') || 'application/json'),
294
+ 'cache-control': 'no-cache',
295
+ });
296
+ if (!upstream.body) {
297
+ res.end();
298
+ return;
299
+ }
300
+ if (!upstream.ok) {
301
+ for await (const chunk of upstream.body) res.write(chunk);
302
+ res.end();
303
+ return;
304
+ }
305
+
306
+ const state = { model: body.model, started: false, finished: false };
307
+ let buffer = '';
308
+ const decoder = new TextDecoder();
309
+ for await (const chunk of upstream.body) {
310
+ buffer += decoder.decode(chunk, { stream: true });
311
+ const frames = buffer.split(/\n\n/);
312
+ buffer = frames.pop() || '';
313
+ for (const frame of frames) {
314
+ const dataLine = frame.split(/\n/).find((line) => line.startsWith('data:'));
315
+ if (!dataLine) continue;
316
+ const payload = dataLine.slice('data:'.length).trim();
317
+ if (!payload || payload === '[DONE]') continue;
318
+ let parsed;
319
+ try {
320
+ parsed = JSON.parse(payload);
321
+ } catch {
322
+ continue;
323
+ }
324
+ res.write(openAiStreamChunkToAnthropicEvents(parsed, state));
325
+ }
326
+ }
327
+ if (!state.finished) {
328
+ res.write(openAiStreamChunkToAnthropicEvents({ model: body.model, choices: [{ delta: {}, finish_reason: 'stop' }] }, state));
329
+ }
330
+ res.end();
331
+ }
332
+
234
333
  function createGatewayServer(options = {}) {
235
334
  const loadConfig = options.loadConfig || defaultLoadConfig;
236
335
  return http.createServer(async (req, res) => {
package/lib/install.js CHANGED
@@ -8,6 +8,44 @@ function buildClaudeInstallCommand(network) {
8
8
  return { command: 'npm', args };
9
9
  }
10
10
 
11
+ function buildYingclawUpgradeCommand(network) {
12
+ const args = ['install', '-g', 'yingclaw@latest'];
13
+ if (network === 'cn') {
14
+ args.push('--registry=https://registry.npmmirror.com');
15
+ }
16
+ return { command: 'npm', args };
17
+ }
18
+
19
+ function quoteWindowsBatchArg(value) {
20
+ return `"${String(value).replace(/"/g, '""')}"`;
21
+ }
22
+
23
+ function buildWindowsYingclawUpgradeScript(network) {
24
+ const { command, args } = buildYingclawUpgradeCommand(network);
25
+ const commandLine = [command, ...args].map(quoteWindowsBatchArg).join(' ');
26
+ return [
27
+ '@echo off',
28
+ 'chcp 65001 >nul',
29
+ 'echo yingclaw Windows updater',
30
+ 'echo.',
31
+ 'echo 当前 claw 进程退出后开始升级,避免 Windows 文件锁 EBUSY...',
32
+ 'timeout /t 2 /nobreak >nul',
33
+ commandLine,
34
+ 'set "code=%ERRORLEVEL%"',
35
+ 'echo.',
36
+ 'if "%code%"=="0" (',
37
+ ' echo yingclaw 更新完成,请重新运行 claw',
38
+ ') else (',
39
+ ' echo yingclaw 更新失败,错误码 %code%',
40
+ ` echo 请手动运行: ${commandLine}`,
41
+ ')',
42
+ 'echo.',
43
+ 'pause',
44
+ 'del "%~f0" >nul 2>nul',
45
+ '',
46
+ ].join('\r\n');
47
+ }
48
+
11
49
  function checkNodeEnv() {
12
50
  const nodeVersion = process.versions.node;
13
51
  const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
@@ -91,4 +129,11 @@ function getInstallFailureHints(result, chalk) {
91
129
  ];
92
130
  }
93
131
 
94
- module.exports = { buildClaudeInstallCommand, checkNodeEnv, getNodeInstallGuide, getInstallFailureHints };
132
+ module.exports = {
133
+ buildClaudeInstallCommand,
134
+ buildYingclawUpgradeCommand,
135
+ buildWindowsYingclawUpgradeScript,
136
+ checkNodeEnv,
137
+ getNodeInstallGuide,
138
+ getInstallFailureHints,
139
+ };
package/lib/openai.js ADDED
@@ -0,0 +1,152 @@
1
+ function normalizeOpenAiBaseUrl(baseUrl) {
2
+ let url;
3
+ try { url = new URL(baseUrl); } catch { return baseUrl; }
4
+
5
+ const parts = url.pathname.split('/').filter(Boolean);
6
+ if (parts.at(-1) === 'completions' && parts.at(-2) === 'chat' && parts.at(-3) === 'v1') {
7
+ parts.splice(-3, 3);
8
+ } else if (parts.at(-1) === 'models' && parts.at(-2) === 'v1') {
9
+ parts.splice(-2, 2);
10
+ } else if (parts.at(-1) === 'v1') {
11
+ parts.pop();
12
+ }
13
+
14
+ url.pathname = parts.length > 0 ? `/${parts.join('/')}` : '/';
15
+ url.search = '';
16
+ url.hash = '';
17
+ return url.toString().replace(/\/+$/, '');
18
+ }
19
+
20
+ function openAiChatCompletionsUrl(baseUrl) {
21
+ return `${normalizeOpenAiBaseUrl(baseUrl)}/v1/chat/completions`;
22
+ }
23
+
24
+ function anthropicTextFromContent(content) {
25
+ if (typeof content === 'string') return content;
26
+ if (!Array.isArray(content)) return '';
27
+ return content
28
+ .map((part) => {
29
+ if (typeof part === 'string') return part;
30
+ if (part?.type === 'text') return part.text || '';
31
+ return '';
32
+ })
33
+ .filter(Boolean)
34
+ .join('\n');
35
+ }
36
+
37
+ function normalizeSystemMessages(system) {
38
+ if (!system) return [];
39
+ if (typeof system === 'string') return [{ role: 'system', content: system }];
40
+ if (Array.isArray(system)) {
41
+ const text = anthropicTextFromContent(system);
42
+ return text ? [{ role: 'system', content: text }] : [];
43
+ }
44
+ return [];
45
+ }
46
+
47
+ function anthropicToOpenAiChatRequest(body) {
48
+ const messages = [
49
+ ...normalizeSystemMessages(body.system),
50
+ ...(Array.isArray(body.messages) ? body.messages : []).map((message) => ({
51
+ role: message.role === 'assistant' ? 'assistant' : 'user',
52
+ content: anthropicTextFromContent(message.content),
53
+ })),
54
+ ];
55
+
56
+ const request = {
57
+ model: body.model,
58
+ messages,
59
+ stream: !!body.stream,
60
+ };
61
+
62
+ if (body.max_tokens !== undefined) request.max_tokens = body.max_tokens;
63
+ if (body.temperature !== undefined) request.temperature = body.temperature;
64
+ if (body.top_p !== undefined) request.top_p = body.top_p;
65
+ if (body.stop_sequences !== undefined) request.stop = body.stop_sequences;
66
+ return request;
67
+ }
68
+
69
+ function openAiFinishReasonToAnthropic(reason) {
70
+ if (reason === 'length') return 'max_tokens';
71
+ if (reason === 'tool_calls' || reason === 'function_call') return 'tool_use';
72
+ if (reason === 'stop') return 'end_turn';
73
+ return reason || 'end_turn';
74
+ }
75
+
76
+ function openAiToAnthropicMessage(body, fallbackModel) {
77
+ const choice = body?.choices?.[0] || {};
78
+ const content = choice.message?.content ?? choice.text ?? '';
79
+ const text = Array.isArray(content)
80
+ ? content.map((part) => part?.text || '').join('')
81
+ : String(content || '');
82
+ return {
83
+ id: body?.id || `msg_${Date.now()}`,
84
+ type: 'message',
85
+ role: 'assistant',
86
+ model: body?.model || fallbackModel,
87
+ content: [{ type: 'text', text }],
88
+ stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason),
89
+ stop_sequence: null,
90
+ usage: {
91
+ input_tokens: body?.usage?.prompt_tokens ?? body?.usage?.input_tokens ?? 0,
92
+ output_tokens: body?.usage?.completion_tokens ?? body?.usage?.output_tokens ?? 0,
93
+ },
94
+ };
95
+ }
96
+
97
+ function anthropicSse(event, data) {
98
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
99
+ }
100
+
101
+ function openAiStreamChunkToAnthropicEvents(chunk, state) {
102
+ const choice = chunk?.choices?.[0] || {};
103
+ const delta = choice.delta || {};
104
+ const events = [];
105
+ if (!state.started) {
106
+ state.started = true;
107
+ events.push(anthropicSse('message_start', {
108
+ type: 'message_start',
109
+ message: {
110
+ id: chunk?.id || `msg_${Date.now()}`,
111
+ type: 'message',
112
+ role: 'assistant',
113
+ model: chunk?.model || state.model,
114
+ content: [],
115
+ stop_reason: null,
116
+ stop_sequence: null,
117
+ usage: { input_tokens: 0, output_tokens: 0 },
118
+ },
119
+ }));
120
+ events.push(anthropicSse('content_block_start', {
121
+ type: 'content_block_start',
122
+ index: 0,
123
+ content_block: { type: 'text', text: '' },
124
+ }));
125
+ }
126
+ if (delta.content) {
127
+ events.push(anthropicSse('content_block_delta', {
128
+ type: 'content_block_delta',
129
+ index: 0,
130
+ delta: { type: 'text_delta', text: delta.content },
131
+ }));
132
+ }
133
+ if (choice.finish_reason) {
134
+ state.finished = true;
135
+ events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: 0 }));
136
+ events.push(anthropicSse('message_delta', {
137
+ type: 'message_delta',
138
+ delta: { stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason), stop_sequence: null },
139
+ usage: { output_tokens: 0 },
140
+ }));
141
+ events.push(anthropicSse('message_stop', { type: 'message_stop' }));
142
+ }
143
+ return events.join('');
144
+ }
145
+
146
+ module.exports = {
147
+ anthropicToOpenAiChatRequest,
148
+ normalizeOpenAiBaseUrl,
149
+ openAiChatCompletionsUrl,
150
+ openAiStreamChunkToAnthropicEvents,
151
+ openAiToAnthropicMessage,
152
+ };