yingclaw 2.5.3 → 2.5.5
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 +17 -2
- package/lib/autostart.js +168 -0
- package/lib/panel.js +21 -6
- package/package.json +1 -1
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
|
-
|
|
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。' : '
|
|
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 });
|
package/lib/autostart.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
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, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
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 sleepSync(ms) {
|
|
67
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getLaunchAgentRuntimeStatus(runner, service) {
|
|
71
|
+
const result = runner('launchctl', ['print', service], { encoding: 'utf8', stdio: 'pipe' });
|
|
72
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
73
|
+
return {
|
|
74
|
+
loaded: result.status === 0,
|
|
75
|
+
running: result.status === 0 && /state\s*=\s*running/i.test(output),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function waitForLaunchAgentUnload(runner, service, options = {}) {
|
|
80
|
+
const timeoutMs = options.timeoutMs == null ? 1500 : options.timeoutMs;
|
|
81
|
+
const intervalMs = options.intervalMs == null ? 100 : options.intervalMs;
|
|
82
|
+
const deadline = Date.now() + timeoutMs;
|
|
83
|
+
|
|
84
|
+
do {
|
|
85
|
+
if (!getLaunchAgentRuntimeStatus(runner, service).loaded) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (Date.now() >= deadline) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
sleepSync(intervalMs);
|
|
92
|
+
} while (true);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function installGatewayAutostart(options = {}) {
|
|
96
|
+
const platform = options.platform || process.platform;
|
|
97
|
+
if (platform !== 'darwin') {
|
|
98
|
+
return { result: 'unsupported', file: null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const homeDir = options.homeDir || os.homedir();
|
|
102
|
+
const file = options.file || getMacLaunchAgentPath({ homeDir });
|
|
103
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
104
|
+
fs.mkdirSync(path.join(homeDir, 'Library', 'Logs'), { recursive: true });
|
|
105
|
+
fs.writeFileSync(file, buildMacLaunchAgentPlist({ ...options, homeDir }), 'utf8');
|
|
106
|
+
|
|
107
|
+
const uid = options.uid || (typeof process.getuid === 'function' ? process.getuid() : null);
|
|
108
|
+
if (uid == null) {
|
|
109
|
+
return { result: 'installed', file };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const runner = options.runner || spawnSync;
|
|
113
|
+
const domain = `gui/${uid}`;
|
|
114
|
+
const service = `${domain}/${GATEWAY_LAUNCH_AGENT_LABEL}`;
|
|
115
|
+
|
|
116
|
+
const bootout = runLaunchctl(runner, ['bootout', service], { optional: true });
|
|
117
|
+
if (bootout.status === 0) {
|
|
118
|
+
waitForLaunchAgentUnload(runner, service, {
|
|
119
|
+
timeoutMs: options.unloadTimeoutMs,
|
|
120
|
+
intervalMs: options.unloadPollIntervalMs,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const bootstrap = runLaunchctl(runner, ['bootstrap', domain, file], { optional: true });
|
|
124
|
+
if (bootstrap.status !== 0 && !getLaunchAgentRuntimeStatus(runner, service).loaded) {
|
|
125
|
+
const stderr = String(bootstrap.stderr || '').trim();
|
|
126
|
+
throw new Error(`launchctl bootstrap ${domain} ${file} 失败${stderr ? `: ${stderr}` : ''}`);
|
|
127
|
+
}
|
|
128
|
+
runLaunchctl(runner, ['enable', service]);
|
|
129
|
+
runLaunchctl(runner, ['kickstart', '-k', service]);
|
|
130
|
+
|
|
131
|
+
return { result: 'installed', file };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getGatewayAutostartStatus(options = {}) {
|
|
135
|
+
const platform = options.platform || process.platform;
|
|
136
|
+
if (platform !== 'darwin') {
|
|
137
|
+
return { supported: false, installed: false, running: false, file: null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const homeDir = options.homeDir || os.homedir();
|
|
141
|
+
const file = options.file || getMacLaunchAgentPath({ homeDir });
|
|
142
|
+
const installed = fs.existsSync(file);
|
|
143
|
+
if (!installed) {
|
|
144
|
+
return { supported: true, installed: false, running: false, file };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const uid = options.uid || (typeof process.getuid === 'function' ? process.getuid() : null);
|
|
148
|
+
if (uid == null) {
|
|
149
|
+
return { supported: true, installed: true, running: false, file };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const runner = options.runner || spawnSync;
|
|
153
|
+
const runtimeStatus = getLaunchAgentRuntimeStatus(runner, `gui/${uid}/${GATEWAY_LAUNCH_AGENT_LABEL}`);
|
|
154
|
+
return {
|
|
155
|
+
supported: true,
|
|
156
|
+
installed: true,
|
|
157
|
+
running: runtimeStatus.running,
|
|
158
|
+
file,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
GATEWAY_LAUNCH_AGENT_LABEL,
|
|
164
|
+
buildMacLaunchAgentPlist,
|
|
165
|
+
getGatewayAutostartStatus,
|
|
166
|
+
getMacLaunchAgentPath,
|
|
167
|
+
installGatewayAutostart,
|
|
168
|
+
};
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 };
|