yingclaw 2.5.2 → 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 +30 -4
- package/lib/autostart.js +130 -0
- package/lib/desktop.js +54 -1
- package/lib/gateway.js +81 -4
- package/lib/panel.js +36 -2
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -20,12 +20,15 @@ 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');
|
|
26
27
|
const {
|
|
27
28
|
DEFAULT_DESKTOP_GATEWAY_PORT,
|
|
28
29
|
YINGCLAW_GATEWAY_PREFIX,
|
|
30
|
+
buildDesktopGatewayMappingRows,
|
|
31
|
+
checkDesktopGatewayStatus,
|
|
29
32
|
createGatewayServer,
|
|
30
33
|
ensureDesktopGatewayConfig,
|
|
31
34
|
} = require('../lib/gateway');
|
|
@@ -238,6 +241,8 @@ async function showStatus() {
|
|
|
238
241
|
const view = buildStatusView(config, {
|
|
239
242
|
apiStatus: valid,
|
|
240
243
|
claudeInstalled: isClaudeInstalled(),
|
|
244
|
+
desktopGatewayStatus: await checkDesktopGatewayStatus(config),
|
|
245
|
+
desktopGatewayAutostartStatus: getGatewayAutostartStatus(),
|
|
241
246
|
env: process.env,
|
|
242
247
|
});
|
|
243
248
|
|
|
@@ -248,7 +253,9 @@ async function showStatus() {
|
|
|
248
253
|
? chalk.yellow(value)
|
|
249
254
|
: label === 'Base URL'
|
|
250
255
|
? chalk.cyan(value)
|
|
251
|
-
|
|
256
|
+
: label === 'Desktop Gateway'
|
|
257
|
+
? value.includes('已运行') ? chalk.green(value) : chalk.yellow(value)
|
|
258
|
+
: label === '当前终端' && value === '未生效'
|
|
252
259
|
? chalk.yellow(value)
|
|
253
260
|
: value;
|
|
254
261
|
return `${chalk.dim(label + ':')} ${coloredValue}`;
|
|
@@ -762,21 +769,36 @@ program
|
|
|
762
769
|
}
|
|
763
770
|
|
|
764
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
|
+
|
|
765
784
|
const modeLines = options.direct
|
|
766
785
|
? chalk.dim('Base URL ') + chalk.cyan(desktopConfig.baseUrl) + '\n' +
|
|
767
786
|
chalk.dim('模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
|
|
768
787
|
chalk.yellow('直连模式可能被新版 Claude Desktop 拒绝非 Claude 模型名。')
|
|
769
788
|
: chalk.dim('Gateway ') + chalk.cyan(gatewayUrl) + '\n' +
|
|
770
789
|
chalk.dim('主模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
|
|
771
|
-
chalk.dim('快速模型 ') + chalk.yellow(desktopConfig.fastModel || desktopConfig.model) + '\n
|
|
772
|
-
chalk.
|
|
790
|
+
chalk.dim('快速模型 ') + chalk.yellow(desktopConfig.fastModel || desktopConfig.model) + '\n' +
|
|
791
|
+
chalk.dim('菜单映射 ') + buildDesktopGatewayMappingRows(desktopConfig)
|
|
792
|
+
.map((row) => `${chalk.white(row.desktopLabel)} ${chalk.dim('→')} ${chalk.yellow(row.upstreamModel)}`)
|
|
793
|
+
.join('\n' + chalk.dim(' ')) + '\n\n' +
|
|
794
|
+
autostartLine;
|
|
773
795
|
|
|
774
796
|
console.log(boxen(
|
|
775
797
|
chalk.bold(options.direct ? 'Claude 桌面应用已配置为直连 Gateway 模式\n\n' : 'Claude 桌面应用已配置为本机 Gateway 模式\n\n') +
|
|
776
798
|
modeLines + '\n' +
|
|
777
799
|
chalk.dim('认证方式 ') + chalk.cyan('bearer') + '\n\n' +
|
|
778
800
|
chalk.yellow(getDesktopOpenHint()) + '\n' +
|
|
779
|
-
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 才能访问第三方模型。'),
|
|
780
802
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 1 } }
|
|
781
803
|
));
|
|
782
804
|
|
|
@@ -1032,6 +1054,8 @@ async function renderStatusBar(apiStatus) {
|
|
|
1032
1054
|
const view = buildStatusView(config, {
|
|
1033
1055
|
apiStatus,
|
|
1034
1056
|
claudeInstalled,
|
|
1057
|
+
desktopGatewayStatus: await checkDesktopGatewayStatus(config, { timeoutMs: 250 }),
|
|
1058
|
+
desktopGatewayAutostartStatus: getGatewayAutostartStatus(),
|
|
1035
1059
|
env: process.env,
|
|
1036
1060
|
});
|
|
1037
1061
|
const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
|
|
@@ -1039,6 +1063,8 @@ async function renderStatusBar(apiStatus) {
|
|
|
1039
1063
|
if (index === 0) return line.replace('API 正常', chalk.green('API 正常')).replace('API Key 无效', chalk.red('API Key 无效')).replace('网络/服务异常', chalk.yellow('网络/服务异常'));
|
|
1040
1064
|
if (line.startsWith('环境变量未生效')) return chalk.yellow(line);
|
|
1041
1065
|
if (line.startsWith('旧模型名')) return chalk.yellow(line);
|
|
1066
|
+
if (line.startsWith('Desktop Gateway 未运行')) return chalk.yellow(line);
|
|
1067
|
+
if (line.startsWith('Desktop Gateway 已运行')) return chalk.green(line);
|
|
1042
1068
|
if (line.startsWith('主模型')) return line.replace(view.mainModel, chalk.yellow(view.mainModel)).replace(view.fastModel, chalk.yellow(view.fastModel));
|
|
1043
1069
|
return line;
|
|
1044
1070
|
}).join('\n ');
|
package/lib/autostart.js
ADDED
|
@@ -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, '<')
|
|
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 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/desktop.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
|
|
13
13
|
const CLAUDE_DESKTOP_LABEL = 'Claude 桌面应用配置';
|
|
14
14
|
const YINGCLAW_ENTRY_NAME = 'yingclaw';
|
|
15
|
+
const MAC_POLICY_BUNDLE = 'com.anthropic.claudefordesktop';
|
|
15
16
|
const DESKTOP_GATEWAY_KEYS = [
|
|
16
17
|
'inferenceProvider',
|
|
17
18
|
'inferenceGatewayBaseUrl',
|
|
@@ -22,6 +23,15 @@ const DESKTOP_GATEWAY_KEYS = [
|
|
|
22
23
|
'deploymentOrganizationUuid',
|
|
23
24
|
];
|
|
24
25
|
|
|
26
|
+
function buildRuntimeEnterpriseConfig(entry) {
|
|
27
|
+
return {
|
|
28
|
+
...entry,
|
|
29
|
+
inferenceGatewayHeaders: [],
|
|
30
|
+
isClaudeCodeForDesktopEnabled: true,
|
|
31
|
+
coworkEgressAllowedHosts: ['*'],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
// Claude Desktop 主进程从 Claude-3p/ 目录读取 deploymentMode 和 enterpriseConfig
|
|
26
36
|
function getClaudeDesktopDataDir(options = {}) {
|
|
27
37
|
const platform = options.platform || process.platform;
|
|
@@ -85,6 +95,29 @@ function serializeGatewayModels(config) {
|
|
|
85
95
|
});
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
function buildClaudeDesktopMacDefaultsCommands(entry) {
|
|
99
|
+
const runtime = buildRuntimeEnterpriseConfig(entry);
|
|
100
|
+
return [
|
|
101
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'inferenceProvider', '-string', runtime.inferenceProvider] },
|
|
102
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'inferenceGatewayBaseUrl', '-string', runtime.inferenceGatewayBaseUrl] },
|
|
103
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'inferenceGatewayApiKey', '-string', runtime.inferenceGatewayApiKey] },
|
|
104
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'inferenceGatewayAuthScheme', '-string', runtime.inferenceGatewayAuthScheme] },
|
|
105
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'inferenceGatewayHeaders', '-string', JSON.stringify(runtime.inferenceGatewayHeaders)] },
|
|
106
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'inferenceModels', '-string', JSON.stringify(runtime.inferenceModels)] },
|
|
107
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'isClaudeCodeForDesktopEnabled', '-int', '1'] },
|
|
108
|
+
{ command: 'defaults', args: ['write', MAC_POLICY_BUNDLE, 'coworkEgressAllowedHosts', '-string', JSON.stringify(runtime.coworkEgressAllowedHosts)] },
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeClaudeDesktopMacDefaults(entry) {
|
|
113
|
+
for (const { command, args } of buildClaudeDesktopMacDefaultsCommands(entry)) {
|
|
114
|
+
const result = spawnSync(command, args, { encoding: 'utf8' });
|
|
115
|
+
if (result.status !== 0) {
|
|
116
|
+
throw new Error(`macOS policy 写入失败: ${args[2] || command}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
88
121
|
function buildClaudeDesktopEnterpriseConfig(config, options = {}) {
|
|
89
122
|
const gatewayConfig = ensureDesktopGatewayConfig(config, options);
|
|
90
123
|
const models = serializeGatewayModels(gatewayConfig);
|
|
@@ -196,6 +229,7 @@ function writeClaudeDesktopConfig(config, options = {}) {
|
|
|
196
229
|
disableDeploymentModeChooser: true,
|
|
197
230
|
deploymentOrganizationUuid: uuid,
|
|
198
231
|
};
|
|
232
|
+
const runtimeEntry = buildRuntimeEnterpriseConfig(entry);
|
|
199
233
|
|
|
200
234
|
const otherEntries = existingEntries.filter((entry) => entry && entry.name !== YINGCLAW_ENTRY_NAME);
|
|
201
235
|
const meta = {
|
|
@@ -216,10 +250,28 @@ function writeClaudeDesktopConfig(config, options = {}) {
|
|
|
216
250
|
|
|
217
251
|
const file = options.configFile || path.join(dataDir, 'claude_desktop_config.json');
|
|
218
252
|
const current = readJsonFile(file);
|
|
219
|
-
const
|
|
253
|
+
const currentEnterprise = current.enterpriseConfig && typeof current.enterpriseConfig === 'object'
|
|
254
|
+
? current.enterpriseConfig
|
|
255
|
+
: {};
|
|
256
|
+
const next = {
|
|
257
|
+
...current,
|
|
258
|
+
deploymentMode: '3p',
|
|
259
|
+
enterpriseConfig: {
|
|
260
|
+
...currentEnterprise,
|
|
261
|
+
...runtimeEntry,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
220
264
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
221
265
|
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
222
266
|
|
|
267
|
+
const shouldWritePlatformPolicy = (options.platform || process.platform) === 'darwin'
|
|
268
|
+
&& !options.dataDir
|
|
269
|
+
&& !options.configFile
|
|
270
|
+
&& options.writePlatformPolicy !== false;
|
|
271
|
+
if (shouldWritePlatformPolicy) {
|
|
272
|
+
writeClaudeDesktopMacDefaults(entry);
|
|
273
|
+
}
|
|
274
|
+
|
|
223
275
|
return { result: beforeEntry === entryBody ? 'unchanged' : 'updated', file, config: effectiveConfig };
|
|
224
276
|
}
|
|
225
277
|
|
|
@@ -330,6 +382,7 @@ async function openClaudeDesktop(options = {}) {
|
|
|
330
382
|
module.exports = {
|
|
331
383
|
buildClaudeDesktopEnterpriseConfig,
|
|
332
384
|
buildClaudeDesktopDirectEnterpriseConfig,
|
|
385
|
+
buildClaudeDesktopMacDefaultsCommands,
|
|
333
386
|
buildClaudeDesktopOpenCommands,
|
|
334
387
|
clearClaudeDesktopConfig,
|
|
335
388
|
getClaudeDesktopConfigLibraryDir,
|
package/lib/gateway.js
CHANGED
|
@@ -6,9 +6,9 @@ const DEFAULT_DESKTOP_GATEWAY_PORT = 18080;
|
|
|
6
6
|
const YINGCLAW_GATEWAY_PREFIX = '/yingclaw';
|
|
7
7
|
const DESKTOP_ROUTE_SPECS = [
|
|
8
8
|
{ id: 'claude-sonnet-4-6', displayName: 'Sonnet', upstreamKey: 'model' },
|
|
9
|
-
{ id: 'claude-opus-4-7', displayName: 'Opus', upstreamKey: 'model' },
|
|
10
9
|
{ id: 'claude-haiku-4-5', displayName: 'Haiku', upstreamKey: 'fastModel' },
|
|
11
10
|
];
|
|
11
|
+
const ONE_M_CONTEXT_SUFFIX = ' [1M]';
|
|
12
12
|
|
|
13
13
|
function createGatewayKey() {
|
|
14
14
|
return `ygw-${crypto.randomUUID()}`;
|
|
@@ -27,24 +27,55 @@ function modelSupports1m(model) {
|
|
|
27
27
|
return /\[1m\]/i.test(String(model || ''));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function stripOneMContextSuffix(model) {
|
|
31
|
+
const value = String(model || '').trim();
|
|
32
|
+
return value.replace(/\s*\[1m\]\s*$/i, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function desktopRouteId(routeId, supports1m) {
|
|
36
|
+
const base = stripOneMContextSuffix(routeId);
|
|
37
|
+
return supports1m ? `${base}${ONE_M_CONTEXT_SUFFIX}` : base;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function desktopRouteLabel(routeId) {
|
|
41
|
+
const raw = String(routeId || '').trim();
|
|
42
|
+
const is1m = /\[1m\]\s*$/i.test(raw);
|
|
43
|
+
const base = stripOneMContextSuffix(raw).replace(/^anthropic\//i, '').replace(/^claude-/i, '');
|
|
44
|
+
const parts = base.split('-').filter(Boolean);
|
|
45
|
+
const family = parts.shift() || base;
|
|
46
|
+
const version = parts.join('.');
|
|
47
|
+
const label = `${family.charAt(0).toUpperCase()}${family.slice(1)}${version ? ` ${version}` : ''}`;
|
|
48
|
+
return is1m ? `${label} 1M` : label;
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
function buildDesktopGatewayRoutes(config) {
|
|
31
52
|
const fastModel = config.fastModel || config.model;
|
|
32
53
|
const source = { model: config.model, fastModel };
|
|
33
54
|
return DESKTOP_ROUTE_SPECS
|
|
34
55
|
.map((spec) => {
|
|
35
56
|
const upstreamModel = source[spec.upstreamKey];
|
|
57
|
+
const supports1m = modelSupports1m(upstreamModel);
|
|
36
58
|
return upstreamModel ? {
|
|
37
|
-
id: spec.id,
|
|
59
|
+
id: desktopRouteId(spec.id, supports1m),
|
|
38
60
|
displayName: spec.displayName,
|
|
39
61
|
upstreamModel,
|
|
40
|
-
supports1m
|
|
62
|
+
supports1m,
|
|
41
63
|
} : null;
|
|
42
64
|
})
|
|
43
65
|
.filter(Boolean);
|
|
44
66
|
}
|
|
45
67
|
|
|
68
|
+
function buildDesktopGatewayMappingRows(config) {
|
|
69
|
+
return buildDesktopGatewayRoutes(config).map((route) => ({
|
|
70
|
+
routeId: route.id,
|
|
71
|
+
desktopLabel: desktopRouteLabel(route.id),
|
|
72
|
+
upstreamModel: route.upstreamModel,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
46
76
|
function mapDesktopRouteToUpstream(config, routeId) {
|
|
47
|
-
const
|
|
77
|
+
const requested = stripOneMContextSuffix(routeId);
|
|
78
|
+
const route = buildDesktopGatewayRoutes(config).find((item) => stripOneMContextSuffix(item.id) === requested);
|
|
48
79
|
if (!route) {
|
|
49
80
|
throw new Error(`未配置的 Claude Desktop 模型路由: ${routeId}`);
|
|
50
81
|
}
|
|
@@ -98,6 +129,45 @@ function providerMessagesUrl(config) {
|
|
|
98
129
|
return `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
|
|
99
130
|
}
|
|
100
131
|
|
|
132
|
+
function buildDesktopGatewayUrl(config = {}) {
|
|
133
|
+
const port = Number.parseInt(config.desktopGatewayPort || DEFAULT_DESKTOP_GATEWAY_PORT, 10);
|
|
134
|
+
return {
|
|
135
|
+
port,
|
|
136
|
+
url: `http://127.0.0.1:${port}${YINGCLAW_GATEWAY_PREFIX}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function checkDesktopGatewayStatus(config = {}, options = {}) {
|
|
141
|
+
const { port, url } = buildDesktopGatewayUrl(config);
|
|
142
|
+
const fetchImpl = options.fetch || fetch;
|
|
143
|
+
const configured = !!config.desktopGatewayKey;
|
|
144
|
+
|
|
145
|
+
if (!configured) {
|
|
146
|
+
return { configured: false, running: false, port, url, error: null };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetchImpl(`${url}/health`, {
|
|
151
|
+
signal: AbortSignal.timeout(options.timeoutMs || 800),
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
configured: true,
|
|
155
|
+
running: response.ok,
|
|
156
|
+
port,
|
|
157
|
+
url,
|
|
158
|
+
error: response.ok ? null : `HTTP ${response.status}`,
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
configured: true,
|
|
163
|
+
running: false,
|
|
164
|
+
port,
|
|
165
|
+
url,
|
|
166
|
+
error: error.message,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
101
171
|
async function proxyMessages(req, res, config) {
|
|
102
172
|
const raw = await readRequestBody(req);
|
|
103
173
|
let body;
|
|
@@ -188,11 +258,18 @@ module.exports = {
|
|
|
188
258
|
DEFAULT_DESKTOP_GATEWAY_PORT,
|
|
189
259
|
YINGCLAW_GATEWAY_PREFIX,
|
|
190
260
|
DESKTOP_ROUTE_SPECS,
|
|
261
|
+
ONE_M_CONTEXT_SUFFIX,
|
|
191
262
|
createGatewayKey,
|
|
192
263
|
ensureDesktopGatewayConfig,
|
|
193
264
|
modelSupports1m,
|
|
265
|
+
stripOneMContextSuffix,
|
|
266
|
+
desktopRouteId,
|
|
267
|
+
desktopRouteLabel,
|
|
194
268
|
buildDesktopGatewayRoutes,
|
|
269
|
+
buildDesktopGatewayMappingRows,
|
|
195
270
|
mapDesktopRouteToUpstream,
|
|
196
271
|
buildGatewayModelsResponse,
|
|
272
|
+
buildDesktopGatewayUrl,
|
|
273
|
+
checkDesktopGatewayStatus,
|
|
197
274
|
createGatewayServer,
|
|
198
275
|
};
|
package/lib/panel.js
CHANGED
|
@@ -12,6 +12,21 @@ function isEnvActive(config, env) {
|
|
|
12
12
|
return Object.entries(expected).every(([key, value]) => env[key] === value);
|
|
13
13
|
}
|
|
14
14
|
|
|
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) {
|
|
22
|
+
if (!status) return null;
|
|
23
|
+
const autostartText = desktopGatewayAutostartText(autostartStatus);
|
|
24
|
+
const suffix = autostartText ? ` · ${autostartText}` : '';
|
|
25
|
+
if (!status.configured) return '未配置';
|
|
26
|
+
if (status.running) return `已运行 · 127.0.0.1:${status.port}${suffix}`;
|
|
27
|
+
return `未运行:执行 claw gateway${suffix}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
function buildStatusView(config, options = {}) {
|
|
16
31
|
const provider = PROVIDERS[config.provider];
|
|
17
32
|
const providerName = config.providerName || provider?.name || config.provider;
|
|
@@ -21,6 +36,9 @@ function buildStatusView(config, options = {}) {
|
|
|
21
36
|
const mainModel = expectedEnv.ANTHROPIC_MODEL;
|
|
22
37
|
const fastModel = expectedEnv.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
23
38
|
const envActive = isEnvActive(config, env);
|
|
39
|
+
const desktopGatewayStatus = options.desktopGatewayStatus;
|
|
40
|
+
const desktopGatewayAutostartStatus = options.desktopGatewayAutostartStatus;
|
|
41
|
+
const desktopGatewayText = desktopGatewayStatusText(desktopGatewayStatus, desktopGatewayAutostartStatus);
|
|
24
42
|
const warnings = [];
|
|
25
43
|
|
|
26
44
|
if (config.provider === 'deepseek' && (
|
|
@@ -31,7 +49,7 @@ function buildStatusView(config, options = {}) {
|
|
|
31
49
|
warnings.push('检测到旧 DeepSeek 模型名,建议运行 claw switch 更新到 [1m] 长上下文版本');
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
const view = {
|
|
35
53
|
providerName,
|
|
36
54
|
mainModel,
|
|
37
55
|
fastModel,
|
|
@@ -45,9 +63,17 @@ function buildStatusView(config, options = {}) {
|
|
|
45
63
|
{ label: 'API 状态', value: apiStatusText(options.apiStatus) },
|
|
46
64
|
{ label: 'Claude Code', value: claudeInstalled ? '已安装' : '未检测到' },
|
|
47
65
|
{ label: '当前终端', value: envActive ? '已生效' : '未生效' },
|
|
66
|
+
...(desktopGatewayText ? [{ label: 'Desktop Gateway', value: desktopGatewayText }] : []),
|
|
48
67
|
{ label: 'Base URL', value: config.baseUrl },
|
|
49
68
|
],
|
|
50
69
|
};
|
|
70
|
+
if (desktopGatewayStatus) {
|
|
71
|
+
view.desktopGatewayStatus = desktopGatewayStatus;
|
|
72
|
+
}
|
|
73
|
+
if (desktopGatewayAutostartStatus) {
|
|
74
|
+
view.desktopGatewayAutostartStatus = desktopGatewayAutostartStatus;
|
|
75
|
+
}
|
|
76
|
+
return view;
|
|
51
77
|
}
|
|
52
78
|
|
|
53
79
|
function buildMenuStatusLines(view, options = {}) {
|
|
@@ -66,6 +92,14 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
66
92
|
|
|
67
93
|
lines.push(`主模型 ${view.mainModel} · 快速模型 ${view.fastModel}`);
|
|
68
94
|
|
|
95
|
+
const desktopGatewayText = desktopGatewayStatusText(
|
|
96
|
+
view.desktopGatewayStatus || options.desktopGatewayStatus,
|
|
97
|
+
view.desktopGatewayAutostartStatus || options.desktopGatewayAutostartStatus,
|
|
98
|
+
);
|
|
99
|
+
if (desktopGatewayText && desktopGatewayText !== '未配置') {
|
|
100
|
+
lines.push(`Desktop Gateway ${desktopGatewayText}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
if (view.warnings.some((warning) => warning.includes('旧 DeepSeek 模型名'))) {
|
|
70
104
|
lines.push('旧模型名:选择下方"切换厂商或模型"更新到 [1m]');
|
|
71
105
|
}
|
|
@@ -73,4 +107,4 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
73
107
|
return lines;
|
|
74
108
|
}
|
|
75
109
|
|
|
76
|
-
module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, isEnvActive };
|
|
110
|
+
module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, desktopGatewayAutostartText, desktopGatewayStatusText, isEnvActive };
|