yingclaw 2.5.18 → 2.5.20

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 CHANGED
@@ -47,11 +47,10 @@ claw code
47
47
  接入 Claude 桌面应用:
48
48
  ```bash
49
49
  claw desktop
50
- claw gateway
51
50
  ```
52
51
  `claw desktop` 会把 Claude Desktop 指向 yingclaw 本机 Gateway,并只暴露 `claude-sonnet-4-6`、`claude-opus-4-7`、`claude-haiku-4-5` 这类 Claude Desktop 可接受的模型名。真实第三方模型仍按 `claw config` 保存的主模型和快速模型转发。
53
52
 
54
- 使用 Claude Desktop 时需要保持 `claw gateway` 运行。
53
+ `claw desktop` 会设置 Gateway 登录自动运行;如果自动启动失败,可以手动运行 `claw gateway`。
55
54
 
56
55
  ## 支持的厂商
57
56
 
@@ -95,7 +94,7 @@ claw setup # 兼容旧命令:config + code
95
94
  |------|---------|---------|
96
95
  | macOS | ✅ 写入 `~/.zshrc` | ✅ 自动启动本机 Gateway 并重启 Claude Desktop |
97
96
  | Linux / WSL | ✅ 写入 `~/.zshrc` / `~/.bashrc` | — |
98
- | Windows | ✅ 写入用户级环境变量(需重开终端) | ✅ 写入 `%APPDATA%\Claude-3p\`,并写入登录启动脚本 |
97
+ | Windows | ✅ 写入用户级环境变量(需重开终端) | ✅ 同步写入 `%APPDATA%\Claude-3p\` 和 `%LOCALAPPDATA%\Claude-3p\`,并写入登录启动脚本 |
99
98
 
100
99
  ## 原理
101
100
 
@@ -118,6 +117,7 @@ CLAUDE_CODE_EFFORT_LEVEL
118
117
  - Claude Desktop 访问 `http://127.0.0.1:18080/yingclaw`
119
118
  - Gateway 再转发到当前保存的 Anthropic 兼容接口
120
119
  - macOS 使用 LaunchAgent 登录启动 Gateway,Windows 使用 Startup 目录脚本登录启动 Gateway
120
+ - Windows 会同时写入 Roaming / Local 两个 Claude-3p 位置,以兼容不同 Claude Desktop 安装方式
121
121
  - 终端接入仍直接使用 `ANTHROPIC_*` 环境变量,不受桌面 Gateway 影响
122
122
 
123
123
  使用 `inferenceProvider=gateway`、`inferenceGatewayAuthScheme=bearer`,将 Gateway Base URL 指向 yingclaw 本机 Gateway。高级用户可用 `claw desktop --direct` 保留旧式直连写入,但新版 Claude Desktop 可能拒绝非 Claude 模型名。
package/bin/cli.js CHANGED
@@ -21,9 +21,16 @@ const {
21
21
  } = require('../lib/config');
22
22
  const { execSync, spawn, spawnSync } = require('child_process');
23
23
  const pkg = require('../package.json');
24
- const { getGatewayAutostartStatus, installGatewayAutostart } = require('../lib/autostart');
24
+ const { getGatewayAutostartStatus, installGatewayAutostart, removeGatewayAutostart } = require('../lib/autostart');
25
25
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
26
- const { buildClaudeInstallCommand, checkNodeEnv, getNodeInstallGuide, getInstallFailureHints } = require('../lib/install');
26
+ const {
27
+ buildClaudeInstallCommand,
28
+ buildYingclawUpgradeCommand,
29
+ buildWindowsYingclawUpgradeScript,
30
+ checkNodeEnv,
31
+ getNodeInstallGuide,
32
+ getInstallFailureHints,
33
+ } = require('../lib/install');
27
34
  const { clearClaudeDesktopConfig, isDesktopConfigured, openClaudeDesktop, writeClaudeDesktopConfig } = require('../lib/desktop');
28
35
  const {
29
36
  DEFAULT_DESKTOP_GATEWAY_PORT,
@@ -102,6 +109,20 @@ function getSavedConfigHint() {
102
109
  return '⚠ API Key 以明文存储在 ~/.clawai.json';
103
110
  }
104
111
 
112
+ function clearCurrentProcessClaudeEnv() {
113
+ for (const key of CLEAR_CLAUDE_ENV_KEYS) {
114
+ delete process.env[key];
115
+ }
116
+ }
117
+
118
+ function desktopConfigClearedPaths(result) {
119
+ const path = require('path');
120
+ return [
121
+ ...(Array.isArray(result.dataDirs) ? result.dataDirs.map(dir => path.join(dir, 'configLibrary/')) : []),
122
+ ...(Array.isArray(result.files) ? result.files : [result.file]),
123
+ ].filter(Boolean);
124
+ }
125
+
105
126
  async function offerDesktopSync(chalk, ora, config) {
106
127
  if (!isDesktopConfigured()) return;
107
128
  const syncDesktop = await confirm({ message: 'Claude 桌面应用已配置,是否同步新模型?', default: true });
@@ -789,7 +810,10 @@ program
789
810
  spinner.warn(chalk.yellow('当前系统暂不支持自动配置 Claude 桌面应用'));
790
811
  return;
791
812
  }
792
- spinner.succeed(chalk.green(`Claude 桌面应用配置已写入 → ${result.file}`));
813
+ const extraFiles = Array.isArray(result.files) && result.files.length > 1
814
+ ? chalk.dim(`(已同步 ${result.files.length} 个配置位置)`)
815
+ : '';
816
+ spinner.succeed(chalk.green(`Claude 桌面应用配置已写入 → ${result.file}`) + extraFiles);
793
817
  } catch (e) {
794
818
  spinner.fail(chalk.red(`写入失败: ${e.message}`));
795
819
  return;
@@ -882,10 +906,7 @@ program
882
906
 
883
907
  if (result.result === 'updated') {
884
908
  spinner.succeed(chalk.green('Claude 桌面应用已恢复默认'));
885
- const cleared = [
886
- result.dataDir ? require('path').join(result.dataDir, 'configLibrary/') : null,
887
- result.file,
888
- ].filter(Boolean);
909
+ const cleared = desktopConfigClearedPaths(result);
889
910
  console.log(boxen(
890
911
  chalk.bold('已清除 Claude 桌面应用第三方推理配置:\n\n') +
891
912
  cleared.map(f => chalk.cyan(' • ' + f)).join('\n'),
@@ -942,8 +963,11 @@ program
942
963
  const cleared = resetConfig();
943
964
  const desktopCleared = clearClaudeDesktopConfig();
944
965
  if (desktopCleared.result === 'updated') {
945
- if (desktopCleared.dataDir) cleared.push(require('path').join(desktopCleared.dataDir, 'configLibrary/'));
946
- if (desktopCleared.file) cleared.push(desktopCleared.file);
966
+ cleared.push(...desktopConfigClearedPaths(desktopCleared));
967
+ }
968
+ const autostartCleared = removeGatewayAutostart();
969
+ if (autostartCleared.result === 'removed' && autostartCleared.file) {
970
+ cleared.push(autostartCleared.file);
947
971
  }
948
972
 
949
973
  if (cleared.length === 0) {
@@ -1048,12 +1072,31 @@ program
1048
1072
  ],
1049
1073
  });
1050
1074
 
1051
- const upgradeArgs = ['install', '-g', 'yingclaw@latest'];
1052
- if (network === 'cn') upgradeArgs.push('--registry=https://registry.npmmirror.com');
1053
- const upgradeCmd = { command: 'npm', args: upgradeArgs };
1075
+ const upgradeCmd = buildYingclawUpgradeCommand(network);
1076
+
1077
+ if (process.platform === 'win32') {
1078
+ const fs = require('fs');
1079
+ const os = require('os');
1080
+ const path = require('path');
1081
+ const scriptFile = path.join(os.tmpdir(), `yingclaw-upgrade-${Date.now()}.cmd`);
1082
+ fs.writeFileSync(scriptFile, buildWindowsYingclawUpgradeScript(network), 'utf8');
1083
+ const child = spawn('cmd', ['/c', 'start', '', scriptFile], {
1084
+ detached: true,
1085
+ stdio: 'ignore',
1086
+ windowsHide: false,
1087
+ });
1088
+ if (child && typeof child.unref === 'function') child.unref();
1089
+ console.log(boxen(
1090
+ chalk.bold(`已打开独立更新窗口,准备升级到 v${latest}\n\n`) +
1091
+ chalk.dim('Windows 会锁定当前正在运行的包目录,所以更新会在当前 claw 退出后执行。\n') +
1092
+ chalk.dim('更新完成后重新运行 ') + chalk.cyan('claw'),
1093
+ { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
1094
+ ));
1095
+ process.exit(0);
1096
+ }
1054
1097
 
1055
1098
  console.log(chalk.dim('\n升级中...\n'));
1056
- const result = spawnSync(upgradeCmd.command, upgradeCmd.args, { stdio: 'inherit', shell: process.platform === 'win32' });
1099
+ const result = spawnSync(upgradeCmd.command, upgradeCmd.args, { stdio: 'inherit' });
1057
1100
 
1058
1101
  if (result.status === 0) {
1059
1102
  console.log(boxen(
@@ -1272,10 +1315,13 @@ async function runMenu() {
1272
1315
  });
1273
1316
 
1274
1317
  // 改 config 的命令需要刷新缓存
1275
- if (['config', 'switch', 'reset', 'code-reset'].includes(resolvedAction)) {
1318
+ if (['config', 'switch', 'reset', 'code-reset', 'desktop-reset'].includes(resolvedAction)) {
1276
1319
  lastCheckResult = undefined;
1277
1320
  lastCheckedHash = null;
1278
1321
  }
1322
+ if (process.platform === 'win32' && ['reset', 'code-reset'].includes(resolvedAction)) {
1323
+ clearCurrentProcessClaudeEnv();
1324
+ }
1279
1325
  // 安装 Claude 后刷新检测缓存
1280
1326
  if (resolvedAction === 'install') {
1281
1327
  invalidateClaudeInstalledCache();
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  ...require('./lib/config'),
3
+ ...require('./lib/autostart'),
3
4
  ...require('./lib/desktop'),
4
5
  ...require('./lib/gateway'),
5
6
  ...require('./lib/install'),
package/lib/autostart.js CHANGED
@@ -188,6 +188,36 @@ function installGatewayAutostart(options = {}) {
188
188
  return { result: 'installed', file };
189
189
  }
190
190
 
191
+ function removeGatewayAutostart(options = {}) {
192
+ const platform = options.platform || process.platform;
193
+ if (platform === 'win32') {
194
+ const file = options.file || getWindowsStartupScriptPath(options);
195
+ if (!fs.existsSync(file)) {
196
+ return { result: 'missing', file };
197
+ }
198
+ fs.unlinkSync(file);
199
+ return { result: 'removed', file };
200
+ }
201
+
202
+ if (platform !== 'darwin') {
203
+ return { result: 'unsupported', file: null };
204
+ }
205
+
206
+ const homeDir = options.homeDir || os.homedir();
207
+ const file = options.file || getMacLaunchAgentPath({ homeDir });
208
+ const uid = options.uid || (typeof process.getuid === 'function' ? process.getuid() : null);
209
+ const runner = options.runner || spawnSync;
210
+
211
+ if (uid != null) {
212
+ runLaunchctl(runner, ['bootout', `gui/${uid}/${GATEWAY_LAUNCH_AGENT_LABEL}`], { optional: true });
213
+ }
214
+ if (!fs.existsSync(file)) {
215
+ return { result: 'missing', file };
216
+ }
217
+ fs.unlinkSync(file);
218
+ return { result: 'removed', file };
219
+ }
220
+
191
221
  function getGatewayAutostartStatus(options = {}) {
192
222
  const platform = options.platform || process.platform;
193
223
  if (platform === 'win32') {
@@ -237,4 +267,5 @@ module.exports = {
237
267
  getMacLaunchAgentPath,
238
268
  getWindowsStartupScriptPath,
239
269
  installGatewayAutostart,
270
+ removeGatewayAutostart,
240
271
  };
package/lib/config.js CHANGED
@@ -407,6 +407,9 @@ function clearClaudeCodeEnv(options = {}) {
407
407
 
408
408
  if (platform === 'win32') {
409
409
  runWindowsEnvCommands(buildWindowsClearEnvCommands(), options.runner || spawnSync, { ignoreErrors: true });
410
+ for (const key of CLEAR_CLAUDE_ENV_KEYS) {
411
+ delete process.env[key];
412
+ }
410
413
  cleared.push(WINDOWS_ENV_LABEL);
411
414
  return cleared;
412
415
  }
package/lib/desktop.js CHANGED
@@ -55,6 +55,25 @@ function getClaudeDesktopDataDir(options = {}) {
55
55
  return null;
56
56
  }
57
57
 
58
+ function getClaudeDesktopDataDirs(options = {}) {
59
+ if (options.dataDir) return [options.dataDir];
60
+
61
+ const platform = options.platform || process.platform;
62
+ const homeDir = options.homeDir || os.homedir();
63
+
64
+ if (platform === 'win32') {
65
+ const roamingAppData = options.appData || process.env.APPDATA || [homeDir, 'AppData', 'Roaming'].join('\\');
66
+ const localAppData = options.localAppData || process.env.LOCALAPPDATA || [homeDir, 'AppData', 'Local'].join('\\');
67
+ return [...new Set([
68
+ roamingAppData + '\\Claude-3p',
69
+ localAppData + '\\Claude-3p',
70
+ ])];
71
+ }
72
+
73
+ const dataDir = getClaudeDesktopDataDir(options);
74
+ return dataDir ? [dataDir] : [];
75
+ }
76
+
58
77
  function getClaudeDesktopConfigPath(options = {}) {
59
78
  const platform = options.platform || process.platform;
60
79
  const dataDir = options.dataDir || getClaudeDesktopDataDir(options);
@@ -172,7 +191,8 @@ function buildClaudeDesktopDirectEnterpriseConfig(config, options = {}) {
172
191
 
173
192
  // 只清除 yingclaw 写入的那条 entry,保留用户其它 3P 配置
174
193
  function clearClaudeDesktopConfigLibrary(options = {}) {
175
- const dir = options.configLibraryDir || getClaudeDesktopConfigLibraryDir(options);
194
+ const dataDir = options.dataDir || getClaudeDesktopDataDir(options);
195
+ const dir = options.configLibraryDir || (dataDir ? path.join(dataDir, 'configLibrary') : null);
176
196
  if (!dir || !fs.existsSync(dir)) {
177
197
  return { result: 'missing', dir };
178
198
  }
@@ -214,17 +234,11 @@ function clearClaudeDesktopConfigLibrary(options = {}) {
214
234
  return { result: 'updated', dir };
215
235
  }
216
236
 
217
- // 写入 Claude-3p/configLibrary/ 下的 enterprise config 条目(主进程从此处读取)
218
- function writeClaudeDesktopConfig(config, options = {}) {
237
+ function writeClaudeDesktopConfigToDir(config, options, dataDir) {
219
238
  const effectiveConfig = options.direct
220
239
  ? config
221
240
  : ensureDesktopGatewayConfig(config, options);
222
241
 
223
- const dataDir = options.dataDir || getClaudeDesktopDataDir(options);
224
- if (!dataDir) {
225
- return { result: 'unsupported', file: null };
226
- }
227
-
228
242
  const configLibraryDir = path.join(dataDir, 'configLibrary');
229
243
  const existingMetaFile = path.join(configLibraryDir, '_meta.json');
230
244
  const existingMeta = readJsonFile(existingMetaFile);
@@ -257,7 +271,7 @@ function writeClaudeDesktopConfig(config, options = {}) {
257
271
  appliedId: uuid,
258
272
  entries: [...otherEntries, { id: uuid, name: YINGCLAW_ENTRY_NAME }],
259
273
  isManaged: typeof existingMeta.isManaged === 'boolean' ? existingMeta.isManaged : false,
260
- platform: existingMeta.platform || process.platform,
274
+ platform: existingMeta.platform || options.platform || process.platform,
261
275
  };
262
276
 
263
277
  fs.mkdirSync(configLibraryDir, { recursive: true });
@@ -295,9 +309,32 @@ function writeClaudeDesktopConfig(config, options = {}) {
295
309
  return { result: beforeEntry === entryBody ? 'unchanged' : 'updated', file, config: effectiveConfig };
296
310
  }
297
311
 
298
- function clearClaudeDesktopConfig(options = {}) {
312
+ // 写入 Claude-3p/configLibrary/ 下的 enterprise config 条目(主进程从此处读取)
313
+ function writeClaudeDesktopConfig(config, options = {}) {
314
+ const dataDirs = options.configFile
315
+ ? [options.dataDir || path.dirname(options.configFile)]
316
+ : getClaudeDesktopDataDirs(options);
317
+ if (dataDirs.length === 0) {
318
+ return { result: 'unsupported', file: null, files: [] };
319
+ }
320
+
321
+ const results = dataDirs.map((dataDir, index) => {
322
+ const writeOptions = index === 0
323
+ ? options
324
+ : { ...options, configFile: null, dataDir };
325
+ return writeClaudeDesktopConfigToDir(config, writeOptions, dataDir);
326
+ });
327
+ const primary = results[0];
328
+ return {
329
+ ...primary,
330
+ result: results.some((item) => item.result === 'updated') ? 'updated' : primary.result,
331
+ files: results.map((item) => item.file).filter(Boolean),
332
+ dataDirs,
333
+ };
334
+ }
335
+
336
+ function clearClaudeDesktopConfigAtDir(options = {}, dataDir) {
299
337
  // Derive dataDir only when configFile is not explicitly overridden (to avoid touching real system dirs in tests)
300
- const dataDir = options.dataDir || (options.configFile ? null : getClaudeDesktopDataDir(options));
301
338
  const file = options.configFile || (dataDir ? path.join(dataDir, 'claude_desktop_config.json') : null);
302
339
 
303
340
  // Clear configLibrary regardless of whether the main config file exists
@@ -320,7 +357,7 @@ function clearClaudeDesktopConfig(options = {}) {
320
357
  // Remove legacy enterpriseConfig gateway keys
321
358
  if (next.enterpriseConfig && typeof next.enterpriseConfig === 'object') {
322
359
  const enterpriseConfig = { ...next.enterpriseConfig };
323
- for (const key of DESKTOP_GATEWAY_KEYS) {
360
+ for (const key of MAC_POLICY_KEYS) {
324
361
  delete enterpriseConfig[key];
325
362
  }
326
363
  if (Object.keys(enterpriseConfig).length > 0) {
@@ -338,12 +375,36 @@ function clearClaudeDesktopConfig(options = {}) {
338
375
  return { result: 'updated', file, dataDir };
339
376
  }
340
377
 
378
+ function clearClaudeDesktopConfig(options = {}) {
379
+ const dataDirs = options.configFile
380
+ ? [options.dataDir || path.dirname(options.configFile)]
381
+ : getClaudeDesktopDataDirs(options);
382
+
383
+ if (dataDirs.length === 0) {
384
+ return { result: 'missing', file: null, files: [], dataDir: null, dataDirs: [] };
385
+ }
386
+
387
+ const results = dataDirs.map((dataDir, index) => {
388
+ const clearOptions = index === 0
389
+ ? options
390
+ : { ...options, configFile: null, dataDir };
391
+ return clearClaudeDesktopConfigAtDir(clearOptions, dataDir);
392
+ });
393
+ const primary = results[0];
394
+ return {
395
+ ...primary,
396
+ result: results.some((item) => item.result === 'updated') ? 'updated' : primary.result,
397
+ files: results.map((item) => item.file).filter(Boolean),
398
+ dataDirs,
399
+ };
400
+ }
401
+
341
402
  function isDesktopConfigured(options = {}) {
342
- const dir = options.dataDir || getClaudeDesktopDataDir(options);
343
- if (!dir) return false;
344
- const meta = readJsonFile(path.join(dir, 'configLibrary', '_meta.json'));
345
- if (!Array.isArray(meta.entries)) return false;
346
- return meta.entries.some((entry) => entry && entry.name === YINGCLAW_ENTRY_NAME);
403
+ return getClaudeDesktopDataDirs(options).some((dir) => {
404
+ const meta = readJsonFile(path.join(dir, 'configLibrary', '_meta.json'));
405
+ if (!Array.isArray(meta.entries)) return false;
406
+ return meta.entries.some((entry) => entry && entry.name === YINGCLAW_ENTRY_NAME);
407
+ });
347
408
  }
348
409
 
349
410
  function buildClaudeDesktopOpenCommands(platform = process.platform, options = {}) {
@@ -432,6 +493,7 @@ module.exports = {
432
493
  getClaudeDesktopConfigLibraryDir,
433
494
  getClaudeDesktopConfigPath,
434
495
  getClaudeDesktopDataDir,
496
+ getClaudeDesktopDataDirs,
435
497
  isDesktopConfigured,
436
498
  openClaudeDesktop,
437
499
  writeClaudeDesktopConfig,
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "2.5.18",
3
+ "version": "2.5.20",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {