yingclaw 2.5.3 → 2.5.4

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/bin/cli.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  } = require('../lib/config');
21
21
  const { execSync, spawn, spawnSync } = require('child_process');
22
22
  const pkg = require('../package.json');
23
+ const { getGatewayAutostartStatus, installGatewayAutostart } = require('../lib/autostart');
23
24
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
24
25
  const { buildClaudeInstallCommand, checkNodeEnv, getNodeInstallGuide, getInstallFailureHints } = require('../lib/install');
25
26
  const { clearClaudeDesktopConfig, isDesktopConfigured, openClaudeDesktop, writeClaudeDesktopConfig } = require('../lib/desktop');
@@ -241,6 +242,7 @@ async function showStatus() {
241
242
  apiStatus: valid,
242
243
  claudeInstalled: isClaudeInstalled(),
243
244
  desktopGatewayStatus: await checkDesktopGatewayStatus(config),
245
+ desktopGatewayAutostartStatus: getGatewayAutostartStatus(),
244
246
  env: process.env,
245
247
  });
246
248
 
@@ -767,6 +769,18 @@ program
767
769
  }
768
770
 
769
771
  const gatewayUrl = `http://127.0.0.1:${desktopConfig.desktopGatewayPort}${YINGCLAW_GATEWAY_PREFIX}`;
772
+ let autostartLine = '';
773
+ if (!options.direct) {
774
+ try {
775
+ const autostart = installGatewayAutostart();
776
+ autostartLine = autostart.result === 'installed'
777
+ ? chalk.green('Gateway 已设置为登录自动运行')
778
+ : chalk.yellow('当前系统暂不支持自动启动 Gateway,请手动运行: claw gateway');
779
+ } catch (error) {
780
+ autostartLine = chalk.yellow(`Gateway 自动启动设置失败,请手动运行: claw gateway (${error.message})`);
781
+ }
782
+ }
783
+
770
784
  const modeLines = options.direct
771
785
  ? chalk.dim('Base URL ') + chalk.cyan(desktopConfig.baseUrl) + '\n' +
772
786
  chalk.dim('模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
@@ -777,14 +791,14 @@ program
777
791
  chalk.dim('菜单映射 ') + buildDesktopGatewayMappingRows(desktopConfig)
778
792
  .map((row) => `${chalk.white(row.desktopLabel)} ${chalk.dim('→')} ${chalk.yellow(row.upstreamModel)}`)
779
793
  .join('\n' + chalk.dim(' ')) + '\n\n' +
780
- chalk.cyan('使用前请运行: claw gateway');
794
+ autostartLine;
781
795
 
782
796
  console.log(boxen(
783
797
  chalk.bold(options.direct ? 'Claude 桌面应用已配置为直连 Gateway 模式\n\n' : 'Claude 桌面应用已配置为本机 Gateway 模式\n\n') +
784
798
  modeLines + '\n' +
785
799
  chalk.dim('认证方式 ') + chalk.cyan('bearer') + '\n\n' +
786
800
  chalk.yellow(getDesktopOpenHint()) + '\n' +
787
- chalk.dim(options.direct ? '要求:网关必须支持 Anthropic POST /v1/messages,且 Base URL 必须是 HTTPS。' : '要求:保持 claw gateway 运行,Claude Desktop 才能访问第三方模型。'),
801
+ chalk.dim(options.direct ? '要求:网关必须支持 Anthropic POST /v1/messages,且 Base URL 必须是 HTTPS。' : '要求:Gateway 进程保持运行,Claude Desktop 才能访问第三方模型。'),
788
802
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 1 } }
789
803
  ));
790
804
 
@@ -1041,6 +1055,7 @@ async function renderStatusBar(apiStatus) {
1041
1055
  apiStatus,
1042
1056
  claudeInstalled,
1043
1057
  desktopGatewayStatus: await checkDesktopGatewayStatus(config, { timeoutMs: 250 }),
1058
+ desktopGatewayAutostartStatus: getGatewayAutostartStatus(),
1044
1059
  env: process.env,
1045
1060
  });
1046
1061
  const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
@@ -0,0 +1,130 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+
6
+ const GATEWAY_LAUNCH_AGENT_LABEL = 'com.yingclaw.gateway';
7
+
8
+ function xmlEscape(value) {
9
+ return String(value)
10
+ .replace(/&/g, '&')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&apos;');
15
+ }
16
+
17
+ function getMacLaunchAgentPath(options = {}) {
18
+ const homeDir = options.homeDir || os.homedir();
19
+ return path.join(homeDir, 'Library', 'LaunchAgents', `${GATEWAY_LAUNCH_AGENT_LABEL}.plist`);
20
+ }
21
+
22
+ function buildMacLaunchAgentPlist(options = {}) {
23
+ const nodePath = options.nodePath || process.execPath;
24
+ const cliPath = options.cliPath || path.join(__dirname, '..', 'bin', 'cli.js');
25
+ const workingDirectory = options.workingDirectory || path.join(__dirname, '..');
26
+ const homeDir = options.homeDir || os.homedir();
27
+ const logPath = options.logPath || path.join(homeDir, 'Library', 'Logs', 'yingclaw-gateway.log');
28
+ const errorLogPath = options.errorLogPath || path.join(homeDir, 'Library', 'Logs', 'yingclaw-gateway.err.log');
29
+
30
+ return `<?xml version="1.0" encoding="UTF-8"?>
31
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
32
+ <plist version="1.0">
33
+ <dict>
34
+ <key>Label</key>
35
+ <string>${GATEWAY_LAUNCH_AGENT_LABEL}</string>
36
+ <key>ProgramArguments</key>
37
+ <array>
38
+ <string>${xmlEscape(nodePath)}</string>
39
+ <string>${xmlEscape(cliPath)}</string>
40
+ <string>gateway</string>
41
+ </array>
42
+ <key>WorkingDirectory</key>
43
+ <string>${xmlEscape(workingDirectory)}</string>
44
+ <key>RunAtLoad</key>
45
+ <true/>
46
+ <key>KeepAlive</key>
47
+ <false/>
48
+ <key>StandardOutPath</key>
49
+ <string>${xmlEscape(logPath)}</string>
50
+ <key>StandardErrorPath</key>
51
+ <string>${xmlEscape(errorLogPath)}</string>
52
+ </dict>
53
+ </plist>
54
+ `;
55
+ }
56
+
57
+ function runLaunchctl(runner, args, options = {}) {
58
+ const result = runner('launchctl', args, { encoding: 'utf8', stdio: 'pipe' });
59
+ if (result.status !== 0 && !options.optional) {
60
+ const stderr = String(result.stderr || '').trim();
61
+ throw new Error(`launchctl ${args.join(' ')} 失败${stderr ? `: ${stderr}` : ''}`);
62
+ }
63
+ return result;
64
+ }
65
+
66
+ function installGatewayAutostart(options = {}) {
67
+ const platform = options.platform || process.platform;
68
+ if (platform !== 'darwin') {
69
+ return { result: 'unsupported', file: null };
70
+ }
71
+
72
+ const homeDir = options.homeDir || os.homedir();
73
+ const file = options.file || getMacLaunchAgentPath({ homeDir });
74
+ fs.mkdirSync(path.dirname(file), { recursive: true });
75
+ fs.mkdirSync(path.join(homeDir, 'Library', 'Logs'), { recursive: true });
76
+ fs.writeFileSync(file, buildMacLaunchAgentPlist({ ...options, homeDir }), 'utf8');
77
+
78
+ const uid = options.uid || (typeof process.getuid === 'function' ? process.getuid() : null);
79
+ if (uid == null) {
80
+ return { result: 'installed', file };
81
+ }
82
+
83
+ const runner = options.runner || spawnSync;
84
+ const domain = `gui/${uid}`;
85
+ const service = `${domain}/${GATEWAY_LAUNCH_AGENT_LABEL}`;
86
+
87
+ runLaunchctl(runner, ['bootout', domain, GATEWAY_LAUNCH_AGENT_LABEL], { optional: true });
88
+ runLaunchctl(runner, ['bootstrap', domain, file]);
89
+ runLaunchctl(runner, ['enable', service]);
90
+ runLaunchctl(runner, ['kickstart', '-k', service]);
91
+
92
+ return { result: 'installed', file };
93
+ }
94
+
95
+ function getGatewayAutostartStatus(options = {}) {
96
+ const platform = options.platform || process.platform;
97
+ if (platform !== 'darwin') {
98
+ return { supported: false, installed: false, running: false, file: null };
99
+ }
100
+
101
+ const homeDir = options.homeDir || os.homedir();
102
+ const file = options.file || getMacLaunchAgentPath({ homeDir });
103
+ const installed = fs.existsSync(file);
104
+ if (!installed) {
105
+ return { supported: true, installed: false, running: false, file };
106
+ }
107
+
108
+ const uid = options.uid || (typeof process.getuid === 'function' ? process.getuid() : null);
109
+ if (uid == null) {
110
+ return { supported: true, installed: true, running: false, file };
111
+ }
112
+
113
+ const runner = options.runner || spawnSync;
114
+ const result = runner('launchctl', ['print', `gui/${uid}/${GATEWAY_LAUNCH_AGENT_LABEL}`], { encoding: 'utf8', stdio: 'pipe' });
115
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
116
+ return {
117
+ supported: true,
118
+ installed: true,
119
+ running: result.status === 0 && /state\s*=\s*running/i.test(output),
120
+ file,
121
+ };
122
+ }
123
+
124
+ module.exports = {
125
+ GATEWAY_LAUNCH_AGENT_LABEL,
126
+ buildMacLaunchAgentPlist,
127
+ getGatewayAutostartStatus,
128
+ getMacLaunchAgentPath,
129
+ installGatewayAutostart,
130
+ };
package/lib/panel.js CHANGED
@@ -12,11 +12,19 @@ function isEnvActive(config, env) {
12
12
  return Object.entries(expected).every(([key, value]) => env[key] === value);
13
13
  }
14
14
 
15
- function desktopGatewayStatusText(status) {
15
+ function desktopGatewayAutostartText(status) {
16
+ if (!status || !status.supported) return null;
17
+ if (status.running || status.installed) return '登录自动运行';
18
+ return '未设置自动运行';
19
+ }
20
+
21
+ function desktopGatewayStatusText(status, autostartStatus = null) {
16
22
  if (!status) return null;
23
+ const autostartText = desktopGatewayAutostartText(autostartStatus);
24
+ const suffix = autostartText ? ` · ${autostartText}` : '';
17
25
  if (!status.configured) return '未配置';
18
- if (status.running) return `已运行 · 127.0.0.1:${status.port}`;
19
- return '未运行:执行 claw gateway';
26
+ if (status.running) return `已运行 · 127.0.0.1:${status.port}${suffix}`;
27
+ return `未运行:执行 claw gateway${suffix}`;
20
28
  }
21
29
 
22
30
  function buildStatusView(config, options = {}) {
@@ -29,7 +37,8 @@ function buildStatusView(config, options = {}) {
29
37
  const fastModel = expectedEnv.CLAUDE_CODE_SUBAGENT_MODEL;
30
38
  const envActive = isEnvActive(config, env);
31
39
  const desktopGatewayStatus = options.desktopGatewayStatus;
32
- const desktopGatewayText = desktopGatewayStatusText(desktopGatewayStatus);
40
+ const desktopGatewayAutostartStatus = options.desktopGatewayAutostartStatus;
41
+ const desktopGatewayText = desktopGatewayStatusText(desktopGatewayStatus, desktopGatewayAutostartStatus);
33
42
  const warnings = [];
34
43
 
35
44
  if (config.provider === 'deepseek' && (
@@ -61,6 +70,9 @@ function buildStatusView(config, options = {}) {
61
70
  if (desktopGatewayStatus) {
62
71
  view.desktopGatewayStatus = desktopGatewayStatus;
63
72
  }
73
+ if (desktopGatewayAutostartStatus) {
74
+ view.desktopGatewayAutostartStatus = desktopGatewayAutostartStatus;
75
+ }
64
76
  return view;
65
77
  }
66
78
 
@@ -80,7 +92,10 @@ function buildMenuStatusLines(view, options = {}) {
80
92
 
81
93
  lines.push(`主模型 ${view.mainModel} · 快速模型 ${view.fastModel}`);
82
94
 
83
- const desktopGatewayText = desktopGatewayStatusText(view.desktopGatewayStatus || options.desktopGatewayStatus);
95
+ const desktopGatewayText = desktopGatewayStatusText(
96
+ view.desktopGatewayStatus || options.desktopGatewayStatus,
97
+ view.desktopGatewayAutostartStatus || options.desktopGatewayAutostartStatus,
98
+ );
84
99
  if (desktopGatewayText && desktopGatewayText !== '未配置') {
85
100
  lines.push(`Desktop Gateway ${desktopGatewayText}`);
86
101
  }
@@ -92,4 +107,4 @@ function buildMenuStatusLines(view, options = {}) {
92
107
  return lines;
93
108
  }
94
109
 
95
- module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, desktopGatewayStatusText, isEnvActive };
110
+ module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, desktopGatewayAutostartText, desktopGatewayStatusText, isEnvActive };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {