yingclaw 2.5.1 → 2.5.3
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 +12 -5
- package/bin/cli.js +85 -7
- package/index.js +1 -0
- package/lib/desktop.js +105 -14
- package/lib/gateway.js +275 -0
- package/lib/panel.js +21 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,8 +47,11 @@ claw code
|
|
|
47
47
|
接入 Claude 桌面应用:
|
|
48
48
|
```bash
|
|
49
49
|
claw desktop
|
|
50
|
+
claw gateway
|
|
50
51
|
```
|
|
51
|
-
|
|
52
|
+
`claw desktop` 会把 Claude Desktop 指向 yingclaw 本机 Gateway,并只暴露 `claude-sonnet-4-6`、`claude-opus-4-7`、`claude-haiku-4-5` 这类 Claude Desktop 可接受的模型名。真实第三方模型仍按 `claw config` 保存的主模型和快速模型转发。
|
|
53
|
+
|
|
54
|
+
使用 Claude Desktop 时需要保持 `claw gateway` 运行。
|
|
52
55
|
|
|
53
56
|
## 支持的厂商
|
|
54
57
|
|
|
@@ -71,6 +74,8 @@ claw # 交互菜单(无参数时自动进入)
|
|
|
71
74
|
claw config # 配置 API 连接
|
|
72
75
|
claw code # 接入 Claude Code 终端
|
|
73
76
|
claw desktop # 接入 Claude 桌面应用
|
|
77
|
+
claw gateway # 启动 Claude 桌面应用本机 Gateway
|
|
78
|
+
claw desktop --direct # 高级:直连写入厂商 Gateway URL
|
|
74
79
|
claw switch # 快速切换厂商或模型
|
|
75
80
|
claw status # 查看当前配置,验证 Key 是否有效
|
|
76
81
|
claw update # 检查并升级到最新版本
|
|
@@ -105,12 +110,14 @@ CLAUDE_CODE_SUBAGENT_MODEL
|
|
|
105
110
|
CLAUDE_CODE_EFFORT_LEVEL
|
|
106
111
|
```
|
|
107
112
|
|
|
108
|
-
**桌面接入**(`claw desktop
|
|
113
|
+
**桌面接入**(`claw desktop`)默认写入本机 Gateway:
|
|
109
114
|
|
|
110
|
-
- macOS
|
|
111
|
-
-
|
|
115
|
+
- macOS / Windows:写入 `Claude-3p/configLibrary/` 中的 yingclaw entry
|
|
116
|
+
- Claude Desktop 访问 `http://127.0.0.1:18080/yingclaw`
|
|
117
|
+
- Gateway 再转发到当前保存的 Anthropic 兼容接口
|
|
118
|
+
- 终端接入仍直接使用 `ANTHROPIC_*` 环境变量,不受桌面 Gateway 影响
|
|
112
119
|
|
|
113
|
-
使用 `inferenceProvider=gateway`、`inferenceGatewayAuthScheme=bearer`,将 Gateway Base URL
|
|
120
|
+
使用 `inferenceProvider=gateway`、`inferenceGatewayAuthScheme=bearer`,将 Gateway Base URL 指向 yingclaw 本机 Gateway。高级用户可用 `claw desktop --direct` 保留旧式直连写入,但新版 Claude Desktop 可能拒绝非 Claude 模型名。
|
|
114
121
|
|
|
115
122
|
**自定义接口**需支持 Anthropic `/v1/messages` 格式;工具会根据 Base URL 自动尝试获取模型列表,失败则手动输入。
|
|
116
123
|
|
package/bin/cli.js
CHANGED
|
@@ -23,6 +23,14 @@ const pkg = require('../package.json');
|
|
|
23
23
|
const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
|
|
24
24
|
const { buildClaudeInstallCommand, checkNodeEnv, getNodeInstallGuide, getInstallFailureHints } = require('../lib/install');
|
|
25
25
|
const { clearClaudeDesktopConfig, isDesktopConfigured, openClaudeDesktop, writeClaudeDesktopConfig } = require('../lib/desktop');
|
|
26
|
+
const {
|
|
27
|
+
DEFAULT_DESKTOP_GATEWAY_PORT,
|
|
28
|
+
YINGCLAW_GATEWAY_PREFIX,
|
|
29
|
+
buildDesktopGatewayMappingRows,
|
|
30
|
+
checkDesktopGatewayStatus,
|
|
31
|
+
createGatewayServer,
|
|
32
|
+
ensureDesktopGatewayConfig,
|
|
33
|
+
} = require('../lib/gateway');
|
|
26
34
|
const { runDoctorChecks, summarize, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
|
|
27
35
|
|
|
28
36
|
const program = new Command();
|
|
@@ -232,6 +240,7 @@ async function showStatus() {
|
|
|
232
240
|
const view = buildStatusView(config, {
|
|
233
241
|
apiStatus: valid,
|
|
234
242
|
claudeInstalled: isClaudeInstalled(),
|
|
243
|
+
desktopGatewayStatus: await checkDesktopGatewayStatus(config),
|
|
235
244
|
env: process.env,
|
|
236
245
|
});
|
|
237
246
|
|
|
@@ -242,7 +251,9 @@ async function showStatus() {
|
|
|
242
251
|
? chalk.yellow(value)
|
|
243
252
|
: label === 'Base URL'
|
|
244
253
|
? chalk.cyan(value)
|
|
245
|
-
|
|
254
|
+
: label === 'Desktop Gateway'
|
|
255
|
+
? value.includes('已运行') ? chalk.green(value) : chalk.yellow(value)
|
|
256
|
+
: label === '当前终端' && value === '未生效'
|
|
246
257
|
? chalk.yellow(value)
|
|
247
258
|
: value;
|
|
248
259
|
return `${chalk.dim(label + ':')} ${coloredValue}`;
|
|
@@ -662,10 +673,54 @@ program
|
|
|
662
673
|
.description('查看当前配置和 Key 有效性')
|
|
663
674
|
.action(showStatus);
|
|
664
675
|
|
|
676
|
+
program
|
|
677
|
+
.command('gateway')
|
|
678
|
+
.description('启动 Claude 桌面应用本机 Gateway')
|
|
679
|
+
.option('-p, --port <port>', '监听端口')
|
|
680
|
+
.action(async (options) => {
|
|
681
|
+
const chalk = (await import('chalk')).default;
|
|
682
|
+
const boxen = (await import('boxen')).default;
|
|
683
|
+
|
|
684
|
+
let config = loadConfig();
|
|
685
|
+
if (!config) {
|
|
686
|
+
console.log(chalk.red('\n未配置 API 连接,请先运行: claw config\n'));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const configProblem = getConfigValidationMessage(config);
|
|
690
|
+
if (configProblem) {
|
|
691
|
+
console.log(chalk.red(`\n配置无效:${configProblem}`));
|
|
692
|
+
console.log(chalk.dim('请运行 claw config 重新配置。\n'));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const ensuredConfig = ensureDesktopGatewayConfig(config);
|
|
697
|
+
if (ensuredConfig.desktopGatewayKey !== config.desktopGatewayKey || ensuredConfig.desktopGatewayPort !== config.desktopGatewayPort) {
|
|
698
|
+
saveConfig(ensuredConfig);
|
|
699
|
+
config = ensuredConfig;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const port = Number.parseInt(options.port || config.desktopGatewayPort || DEFAULT_DESKTOP_GATEWAY_PORT, 10);
|
|
703
|
+
const server = createGatewayServer();
|
|
704
|
+
server.listen(port, '127.0.0.1', () => {
|
|
705
|
+
console.log(boxen(
|
|
706
|
+
chalk.bold('yingclaw Desktop Gateway 已启动\n\n') +
|
|
707
|
+
chalk.dim('URL ') + chalk.cyan(`http://127.0.0.1:${port}${YINGCLAW_GATEWAY_PREFIX}`) + '\n' +
|
|
708
|
+
chalk.dim('模型 ') + chalk.yellow(config.model) + '\n\n' +
|
|
709
|
+
chalk.dim('保持此终端打开,Claude Desktop 才能访问第三方模型。'),
|
|
710
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
711
|
+
));
|
|
712
|
+
});
|
|
713
|
+
server.on('error', (error) => {
|
|
714
|
+
console.error(chalk.red(`Gateway 启动失败: ${error.message}`));
|
|
715
|
+
process.exitCode = 1;
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
|
|
665
719
|
program
|
|
666
720
|
.command('desktop')
|
|
667
721
|
.description('接入 Claude 桌面应用使用当前模型')
|
|
668
|
-
.
|
|
722
|
+
.option('--direct', '高级:直接写入当前厂商 Gateway URL,不启用本机模型映射')
|
|
723
|
+
.action(async (options) => {
|
|
669
724
|
const chalk = (await import('chalk')).default;
|
|
670
725
|
const ora = (await import('ora')).default;
|
|
671
726
|
const boxen = (await import('boxen')).default;
|
|
@@ -689,10 +744,18 @@ program
|
|
|
689
744
|
return;
|
|
690
745
|
}
|
|
691
746
|
|
|
747
|
+
let desktopConfig = config;
|
|
748
|
+
if (!options.direct) {
|
|
749
|
+
desktopConfig = ensureDesktopGatewayConfig(config);
|
|
750
|
+
if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
|
|
751
|
+
saveConfig(desktopConfig);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
692
755
|
const spinner = ora('写入 Claude 桌面应用配置...').start();
|
|
693
756
|
let result;
|
|
694
757
|
try {
|
|
695
|
-
result = writeClaudeDesktopConfig(
|
|
758
|
+
result = writeClaudeDesktopConfig(desktopConfig, { direct: options.direct });
|
|
696
759
|
if (result.result === 'unsupported') {
|
|
697
760
|
spinner.warn(chalk.yellow('当前系统暂不支持自动配置 Claude 桌面应用'));
|
|
698
761
|
return;
|
|
@@ -703,13 +766,25 @@ program
|
|
|
703
766
|
return;
|
|
704
767
|
}
|
|
705
768
|
|
|
769
|
+
const gatewayUrl = `http://127.0.0.1:${desktopConfig.desktopGatewayPort}${YINGCLAW_GATEWAY_PREFIX}`;
|
|
770
|
+
const modeLines = options.direct
|
|
771
|
+
? chalk.dim('Base URL ') + chalk.cyan(desktopConfig.baseUrl) + '\n' +
|
|
772
|
+
chalk.dim('模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
|
|
773
|
+
chalk.yellow('直连模式可能被新版 Claude Desktop 拒绝非 Claude 模型名。')
|
|
774
|
+
: chalk.dim('Gateway ') + chalk.cyan(gatewayUrl) + '\n' +
|
|
775
|
+
chalk.dim('主模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
|
|
776
|
+
chalk.dim('快速模型 ') + chalk.yellow(desktopConfig.fastModel || desktopConfig.model) + '\n' +
|
|
777
|
+
chalk.dim('菜单映射 ') + buildDesktopGatewayMappingRows(desktopConfig)
|
|
778
|
+
.map((row) => `${chalk.white(row.desktopLabel)} ${chalk.dim('→')} ${chalk.yellow(row.upstreamModel)}`)
|
|
779
|
+
.join('\n' + chalk.dim(' ')) + '\n\n' +
|
|
780
|
+
chalk.cyan('使用前请运行: claw gateway');
|
|
781
|
+
|
|
706
782
|
console.log(boxen(
|
|
707
|
-
chalk.bold('Claude
|
|
708
|
-
|
|
709
|
-
chalk.dim('模型 ') + chalk.yellow(config.model) + '\n' +
|
|
783
|
+
chalk.bold(options.direct ? 'Claude 桌面应用已配置为直连 Gateway 模式\n\n' : 'Claude 桌面应用已配置为本机 Gateway 模式\n\n') +
|
|
784
|
+
modeLines + '\n' +
|
|
710
785
|
chalk.dim('认证方式 ') + chalk.cyan('bearer') + '\n\n' +
|
|
711
786
|
chalk.yellow(getDesktopOpenHint()) + '\n' +
|
|
712
|
-
chalk.dim('要求:网关必须支持 Anthropic POST /v1/messages,且 Base URL 必须是 HTTPS。'),
|
|
787
|
+
chalk.dim(options.direct ? '要求:网关必须支持 Anthropic POST /v1/messages,且 Base URL 必须是 HTTPS。' : '要求:保持 claw gateway 运行,Claude Desktop 才能访问第三方模型。'),
|
|
713
788
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 1 } }
|
|
714
789
|
));
|
|
715
790
|
|
|
@@ -965,6 +1040,7 @@ async function renderStatusBar(apiStatus) {
|
|
|
965
1040
|
const view = buildStatusView(config, {
|
|
966
1041
|
apiStatus,
|
|
967
1042
|
claudeInstalled,
|
|
1043
|
+
desktopGatewayStatus: await checkDesktopGatewayStatus(config, { timeoutMs: 250 }),
|
|
968
1044
|
env: process.env,
|
|
969
1045
|
});
|
|
970
1046
|
const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
|
|
@@ -972,6 +1048,8 @@ async function renderStatusBar(apiStatus) {
|
|
|
972
1048
|
if (index === 0) return line.replace('API 正常', chalk.green('API 正常')).replace('API Key 无效', chalk.red('API Key 无效')).replace('网络/服务异常', chalk.yellow('网络/服务异常'));
|
|
973
1049
|
if (line.startsWith('环境变量未生效')) return chalk.yellow(line);
|
|
974
1050
|
if (line.startsWith('旧模型名')) return chalk.yellow(line);
|
|
1051
|
+
if (line.startsWith('Desktop Gateway 未运行')) return chalk.yellow(line);
|
|
1052
|
+
if (line.startsWith('Desktop Gateway 已运行')) return chalk.green(line);
|
|
975
1053
|
if (line.startsWith('主模型')) return line.replace(view.mainModel, chalk.yellow(view.mainModel)).replace(view.fastModel, chalk.yellow(view.fastModel));
|
|
976
1054
|
return line;
|
|
977
1055
|
}).join('\n ');
|
package/index.js
CHANGED
package/lib/desktop.js
CHANGED
|
@@ -4,9 +4,15 @@ const os = require('os');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
6
|
const { normalizeAnthropicBaseUrl } = require('./config');
|
|
7
|
+
const {
|
|
8
|
+
YINGCLAW_GATEWAY_PREFIX,
|
|
9
|
+
buildDesktopGatewayRoutes,
|
|
10
|
+
ensureDesktopGatewayConfig,
|
|
11
|
+
} = require('./gateway');
|
|
7
12
|
|
|
8
13
|
const CLAUDE_DESKTOP_LABEL = 'Claude 桌面应用配置';
|
|
9
14
|
const YINGCLAW_ENTRY_NAME = 'yingclaw';
|
|
15
|
+
const MAC_POLICY_BUNDLE = 'com.anthropic.claudefordesktop';
|
|
10
16
|
const DESKTOP_GATEWAY_KEYS = [
|
|
11
17
|
'inferenceProvider',
|
|
12
18
|
'inferenceGatewayBaseUrl',
|
|
@@ -17,6 +23,15 @@ const DESKTOP_GATEWAY_KEYS = [
|
|
|
17
23
|
'deploymentOrganizationUuid',
|
|
18
24
|
];
|
|
19
25
|
|
|
26
|
+
function buildRuntimeEnterpriseConfig(entry) {
|
|
27
|
+
return {
|
|
28
|
+
...entry,
|
|
29
|
+
inferenceGatewayHeaders: [],
|
|
30
|
+
isClaudeCodeForDesktopEnabled: true,
|
|
31
|
+
coworkEgressAllowedHosts: ['*'],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
// Claude Desktop 主进程从 Claude-3p/ 目录读取 deploymentMode 和 enterpriseConfig
|
|
21
36
|
function getClaudeDesktopDataDir(options = {}) {
|
|
22
37
|
const platform = options.platform || process.platform;
|
|
@@ -60,16 +75,66 @@ function toDesktopModelId(model) {
|
|
|
60
75
|
return model.startsWith('claude-') ? model : `claude-${model}`;
|
|
61
76
|
}
|
|
62
77
|
|
|
63
|
-
function
|
|
78
|
+
function collectDirectModels(config) {
|
|
64
79
|
const list = Array.isArray(config.availableModels) && config.availableModels.length > 0
|
|
65
80
|
? [config.model, config.fastModel, ...config.availableModels]
|
|
66
81
|
: [config.model, config.fastModel];
|
|
67
82
|
return [...new Set(list.filter(Boolean).map(toDesktopModelId))];
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
function buildGatewayBaseUrl(config) {
|
|
86
|
+
const port = config.desktopGatewayPort || 18080;
|
|
87
|
+
return `http://127.0.0.1:${port}${YINGCLAW_GATEWAY_PREFIX}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function serializeGatewayModels(config) {
|
|
91
|
+
return buildDesktopGatewayRoutes(config).map((route) => {
|
|
92
|
+
const item = { name: route.id, displayName: route.displayName };
|
|
93
|
+
if (route.supports1m) item.supports1m = true;
|
|
94
|
+
return item;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
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
|
+
|
|
71
121
|
function buildClaudeDesktopEnterpriseConfig(config, options = {}) {
|
|
72
|
-
const
|
|
122
|
+
const gatewayConfig = ensureDesktopGatewayConfig(config, options);
|
|
123
|
+
const models = serializeGatewayModels(gatewayConfig);
|
|
124
|
+
return {
|
|
125
|
+
inferenceProvider: 'gateway',
|
|
126
|
+
inferenceGatewayBaseUrl: buildGatewayBaseUrl(gatewayConfig),
|
|
127
|
+
inferenceGatewayApiKey: gatewayConfig.desktopGatewayKey,
|
|
128
|
+
inferenceGatewayAuthScheme: options.authScheme || 'bearer',
|
|
129
|
+
inferenceModels: JSON.stringify(models),
|
|
130
|
+
disableDeploymentModeChooser: 'true',
|
|
131
|
+
deploymentOrganizationUuid: options.uuid || crypto.randomUUID(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 兼容旧版直连模式:写入真实 provider Gateway URL 和真实 Key。
|
|
136
|
+
function buildClaudeDesktopDirectEnterpriseConfig(config, options = {}) {
|
|
137
|
+
const models = collectDirectModels(config);
|
|
73
138
|
const baseUrl = normalizeAnthropicBaseUrl(config.baseUrl);
|
|
74
139
|
if (!baseUrl.startsWith('https://')) {
|
|
75
140
|
throw new Error('Claude 桌面应用要求 Gateway Base URL 使用 HTTPS');
|
|
@@ -131,10 +196,9 @@ function clearClaudeDesktopConfigLibrary(options = {}) {
|
|
|
131
196
|
|
|
132
197
|
// 写入 Claude-3p/configLibrary/ 下的 enterprise config 条目(主进程从此处读取)
|
|
133
198
|
function writeClaudeDesktopConfig(config, options = {}) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
199
|
+
const effectiveConfig = options.direct
|
|
200
|
+
? config
|
|
201
|
+
: ensureDesktopGatewayConfig(config, options);
|
|
138
202
|
|
|
139
203
|
const dataDir = options.dataDir || getClaudeDesktopDataDir(options);
|
|
140
204
|
if (!dataDir) {
|
|
@@ -149,17 +213,23 @@ function writeClaudeDesktopConfig(config, options = {}) {
|
|
|
149
213
|
const existingYingclaw = existingEntries.find((entry) => entry && entry.name === YINGCLAW_ENTRY_NAME);
|
|
150
214
|
const uuid = existingYingclaw?.id || options.uuid || crypto.randomUUID();
|
|
151
215
|
|
|
152
|
-
const models =
|
|
216
|
+
const models = options.direct
|
|
217
|
+
? collectDirectModels(effectiveConfig)
|
|
218
|
+
: serializeGatewayModels(effectiveConfig);
|
|
219
|
+
const enterprise = options.direct
|
|
220
|
+
? buildClaudeDesktopDirectEnterpriseConfig(effectiveConfig, { ...options, uuid })
|
|
221
|
+
: buildClaudeDesktopEnterpriseConfig(effectiveConfig, { ...options, uuid });
|
|
153
222
|
|
|
154
223
|
const entry = {
|
|
155
|
-
inferenceProvider:
|
|
156
|
-
inferenceGatewayBaseUrl:
|
|
157
|
-
inferenceGatewayApiKey:
|
|
158
|
-
inferenceGatewayAuthScheme:
|
|
224
|
+
inferenceProvider: enterprise.inferenceProvider,
|
|
225
|
+
inferenceGatewayBaseUrl: enterprise.inferenceGatewayBaseUrl,
|
|
226
|
+
inferenceGatewayApiKey: enterprise.inferenceGatewayApiKey,
|
|
227
|
+
inferenceGatewayAuthScheme: enterprise.inferenceGatewayAuthScheme,
|
|
159
228
|
inferenceModels: models,
|
|
160
229
|
disableDeploymentModeChooser: true,
|
|
161
230
|
deploymentOrganizationUuid: uuid,
|
|
162
231
|
};
|
|
232
|
+
const runtimeEntry = buildRuntimeEnterpriseConfig(entry);
|
|
163
233
|
|
|
164
234
|
const otherEntries = existingEntries.filter((entry) => entry && entry.name !== YINGCLAW_ENTRY_NAME);
|
|
165
235
|
const meta = {
|
|
@@ -180,11 +250,29 @@ function writeClaudeDesktopConfig(config, options = {}) {
|
|
|
180
250
|
|
|
181
251
|
const file = options.configFile || path.join(dataDir, 'claude_desktop_config.json');
|
|
182
252
|
const current = readJsonFile(file);
|
|
183
|
-
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
|
+
};
|
|
184
264
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
185
265
|
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
186
266
|
|
|
187
|
-
|
|
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
|
+
|
|
275
|
+
return { result: beforeEntry === entryBody ? 'unchanged' : 'updated', file, config: effectiveConfig };
|
|
188
276
|
}
|
|
189
277
|
|
|
190
278
|
function clearClaudeDesktopConfig(options = {}) {
|
|
@@ -293,8 +381,11 @@ async function openClaudeDesktop(options = {}) {
|
|
|
293
381
|
|
|
294
382
|
module.exports = {
|
|
295
383
|
buildClaudeDesktopEnterpriseConfig,
|
|
384
|
+
buildClaudeDesktopDirectEnterpriseConfig,
|
|
385
|
+
buildClaudeDesktopMacDefaultsCommands,
|
|
296
386
|
buildClaudeDesktopOpenCommands,
|
|
297
387
|
clearClaudeDesktopConfig,
|
|
388
|
+
getClaudeDesktopConfigLibraryDir,
|
|
298
389
|
getClaudeDesktopConfigPath,
|
|
299
390
|
getClaudeDesktopDataDir,
|
|
300
391
|
isDesktopConfigured,
|
package/lib/gateway.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { loadConfig: defaultLoadConfig, normalizeAnthropicBaseUrl } = require('./config');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DESKTOP_GATEWAY_PORT = 18080;
|
|
6
|
+
const YINGCLAW_GATEWAY_PREFIX = '/yingclaw';
|
|
7
|
+
const DESKTOP_ROUTE_SPECS = [
|
|
8
|
+
{ id: 'claude-sonnet-4-6', displayName: 'Sonnet', upstreamKey: 'model' },
|
|
9
|
+
{ id: 'claude-haiku-4-5', displayName: 'Haiku', upstreamKey: 'fastModel' },
|
|
10
|
+
];
|
|
11
|
+
const ONE_M_CONTEXT_SUFFIX = ' [1M]';
|
|
12
|
+
|
|
13
|
+
function createGatewayKey() {
|
|
14
|
+
return `ygw-${crypto.randomUUID()}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDesktopGatewayConfig(config, options = {}) {
|
|
18
|
+
const keyFactory = options.keyFactory || createGatewayKey;
|
|
19
|
+
return {
|
|
20
|
+
...config,
|
|
21
|
+
desktopGatewayPort: config.desktopGatewayPort || DEFAULT_DESKTOP_GATEWAY_PORT,
|
|
22
|
+
desktopGatewayKey: config.desktopGatewayKey || keyFactory(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function modelSupports1m(model) {
|
|
27
|
+
return /\[1m\]/i.test(String(model || ''));
|
|
28
|
+
}
|
|
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
|
+
|
|
51
|
+
function buildDesktopGatewayRoutes(config) {
|
|
52
|
+
const fastModel = config.fastModel || config.model;
|
|
53
|
+
const source = { model: config.model, fastModel };
|
|
54
|
+
return DESKTOP_ROUTE_SPECS
|
|
55
|
+
.map((spec) => {
|
|
56
|
+
const upstreamModel = source[spec.upstreamKey];
|
|
57
|
+
const supports1m = modelSupports1m(upstreamModel);
|
|
58
|
+
return upstreamModel ? {
|
|
59
|
+
id: desktopRouteId(spec.id, supports1m),
|
|
60
|
+
displayName: spec.displayName,
|
|
61
|
+
upstreamModel,
|
|
62
|
+
supports1m,
|
|
63
|
+
} : null;
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
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
|
+
|
|
76
|
+
function mapDesktopRouteToUpstream(config, routeId) {
|
|
77
|
+
const requested = stripOneMContextSuffix(routeId);
|
|
78
|
+
const route = buildDesktopGatewayRoutes(config).find((item) => stripOneMContextSuffix(item.id) === requested);
|
|
79
|
+
if (!route) {
|
|
80
|
+
throw new Error(`未配置的 Claude Desktop 模型路由: ${routeId}`);
|
|
81
|
+
}
|
|
82
|
+
return route.upstreamModel;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildGatewayModelsResponse(config) {
|
|
86
|
+
const data = buildDesktopGatewayRoutes(config).map((route) => {
|
|
87
|
+
const item = {
|
|
88
|
+
type: 'model',
|
|
89
|
+
id: route.id,
|
|
90
|
+
display_name: route.displayName,
|
|
91
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
92
|
+
};
|
|
93
|
+
if (route.supports1m) item.supports1m = true;
|
|
94
|
+
return item;
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
data,
|
|
98
|
+
has_more: false,
|
|
99
|
+
first_id: data[0]?.id || null,
|
|
100
|
+
last_id: data.at(-1)?.id || null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sendJson(res, status, body) {
|
|
105
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
106
|
+
res.end(JSON.stringify(body));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readRequestBody(req) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
let raw = '';
|
|
112
|
+
req.setEncoding('utf8');
|
|
113
|
+
req.on('data', chunk => { raw += chunk; });
|
|
114
|
+
req.on('end', () => resolve(raw));
|
|
115
|
+
req.on('error', reject);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isAuthorized(req, config) {
|
|
120
|
+
const key = config.desktopGatewayKey;
|
|
121
|
+
if (!key) return false;
|
|
122
|
+
const auth = String(req.headers.authorization || '');
|
|
123
|
+
const bearer = auth.replace(/^bearer\s+/i, '').trim();
|
|
124
|
+
const apiKey = String(req.headers['x-api-key'] || '').trim();
|
|
125
|
+
return bearer === key || apiKey === key;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function providerMessagesUrl(config) {
|
|
129
|
+
return `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
|
|
130
|
+
}
|
|
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
|
+
|
|
171
|
+
async function proxyMessages(req, res, config) {
|
|
172
|
+
const raw = await readRequestBody(req);
|
|
173
|
+
let body;
|
|
174
|
+
try {
|
|
175
|
+
body = raw ? JSON.parse(raw) : {};
|
|
176
|
+
} catch {
|
|
177
|
+
sendJson(res, 400, { error: { message: '请求体不是有效 JSON' } });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
body.model = mapDesktopRouteToUpstream(config, body.model);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
sendJson(res, 400, { error: { message: error.message } });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const upstream = await fetch(providerMessagesUrl(config), {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: {
|
|
191
|
+
'content-type': 'application/json',
|
|
192
|
+
accept: body.stream ? 'text/event-stream' : 'application/json',
|
|
193
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
194
|
+
'x-api-key': config.apiKey,
|
|
195
|
+
'anthropic-version': '2023-06-01',
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
res.writeHead(upstream.status, {
|
|
201
|
+
'content-type': upstream.headers.get('content-type') || (body.stream ? 'text/event-stream' : 'application/json'),
|
|
202
|
+
'cache-control': upstream.headers.get('cache-control') || 'no-cache',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!upstream.body) {
|
|
206
|
+
res.end();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for await (const chunk of upstream.body) {
|
|
210
|
+
res.write(chunk);
|
|
211
|
+
}
|
|
212
|
+
res.end();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createGatewayServer(options = {}) {
|
|
216
|
+
const loadConfig = options.loadConfig || defaultLoadConfig;
|
|
217
|
+
return http.createServer(async (req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
220
|
+
const config = loadConfig();
|
|
221
|
+
|
|
222
|
+
if (url.pathname === `${YINGCLAW_GATEWAY_PREFIX}/health`) {
|
|
223
|
+
sendJson(res, 200, { status: 'ok' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!config) {
|
|
228
|
+
sendJson(res, 503, { error: { message: 'yingclaw 尚未配置 API 连接' } });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (url.pathname === `${YINGCLAW_GATEWAY_PREFIX}/v1/models` && req.method === 'GET') {
|
|
233
|
+
if (!isAuthorized(req, config)) {
|
|
234
|
+
sendJson(res, 401, { error: { message: 'Invalid gateway API key' } });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
sendJson(res, 200, buildGatewayModelsResponse(config));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (url.pathname === `${YINGCLAW_GATEWAY_PREFIX}/v1/messages` && req.method === 'POST') {
|
|
242
|
+
if (!isAuthorized(req, config)) {
|
|
243
|
+
sendJson(res, 401, { error: { message: 'Invalid gateway API key' } });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await proxyMessages(req, res, config);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
sendJson(res, 404, { error: { message: 'Not found' } });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
sendJson(res, 500, { error: { message: error.message } });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
DEFAULT_DESKTOP_GATEWAY_PORT,
|
|
259
|
+
YINGCLAW_GATEWAY_PREFIX,
|
|
260
|
+
DESKTOP_ROUTE_SPECS,
|
|
261
|
+
ONE_M_CONTEXT_SUFFIX,
|
|
262
|
+
createGatewayKey,
|
|
263
|
+
ensureDesktopGatewayConfig,
|
|
264
|
+
modelSupports1m,
|
|
265
|
+
stripOneMContextSuffix,
|
|
266
|
+
desktopRouteId,
|
|
267
|
+
desktopRouteLabel,
|
|
268
|
+
buildDesktopGatewayRoutes,
|
|
269
|
+
buildDesktopGatewayMappingRows,
|
|
270
|
+
mapDesktopRouteToUpstream,
|
|
271
|
+
buildGatewayModelsResponse,
|
|
272
|
+
buildDesktopGatewayUrl,
|
|
273
|
+
checkDesktopGatewayStatus,
|
|
274
|
+
createGatewayServer,
|
|
275
|
+
};
|
package/lib/panel.js
CHANGED
|
@@ -12,6 +12,13 @@ function isEnvActive(config, env) {
|
|
|
12
12
|
return Object.entries(expected).every(([key, value]) => env[key] === value);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function desktopGatewayStatusText(status) {
|
|
16
|
+
if (!status) return null;
|
|
17
|
+
if (!status.configured) return '未配置';
|
|
18
|
+
if (status.running) return `已运行 · 127.0.0.1:${status.port}`;
|
|
19
|
+
return '未运行:执行 claw gateway';
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
function buildStatusView(config, options = {}) {
|
|
16
23
|
const provider = PROVIDERS[config.provider];
|
|
17
24
|
const providerName = config.providerName || provider?.name || config.provider;
|
|
@@ -21,6 +28,8 @@ function buildStatusView(config, options = {}) {
|
|
|
21
28
|
const mainModel = expectedEnv.ANTHROPIC_MODEL;
|
|
22
29
|
const fastModel = expectedEnv.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
23
30
|
const envActive = isEnvActive(config, env);
|
|
31
|
+
const desktopGatewayStatus = options.desktopGatewayStatus;
|
|
32
|
+
const desktopGatewayText = desktopGatewayStatusText(desktopGatewayStatus);
|
|
24
33
|
const warnings = [];
|
|
25
34
|
|
|
26
35
|
if (config.provider === 'deepseek' && (
|
|
@@ -31,7 +40,7 @@ function buildStatusView(config, options = {}) {
|
|
|
31
40
|
warnings.push('检测到旧 DeepSeek 模型名,建议运行 claw switch 更新到 [1m] 长上下文版本');
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
const view = {
|
|
35
44
|
providerName,
|
|
36
45
|
mainModel,
|
|
37
46
|
fastModel,
|
|
@@ -45,9 +54,14 @@ function buildStatusView(config, options = {}) {
|
|
|
45
54
|
{ label: 'API 状态', value: apiStatusText(options.apiStatus) },
|
|
46
55
|
{ label: 'Claude Code', value: claudeInstalled ? '已安装' : '未检测到' },
|
|
47
56
|
{ label: '当前终端', value: envActive ? '已生效' : '未生效' },
|
|
57
|
+
...(desktopGatewayText ? [{ label: 'Desktop Gateway', value: desktopGatewayText }] : []),
|
|
48
58
|
{ label: 'Base URL', value: config.baseUrl },
|
|
49
59
|
],
|
|
50
60
|
};
|
|
61
|
+
if (desktopGatewayStatus) {
|
|
62
|
+
view.desktopGatewayStatus = desktopGatewayStatus;
|
|
63
|
+
}
|
|
64
|
+
return view;
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
function buildMenuStatusLines(view, options = {}) {
|
|
@@ -66,6 +80,11 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
66
80
|
|
|
67
81
|
lines.push(`主模型 ${view.mainModel} · 快速模型 ${view.fastModel}`);
|
|
68
82
|
|
|
83
|
+
const desktopGatewayText = desktopGatewayStatusText(view.desktopGatewayStatus || options.desktopGatewayStatus);
|
|
84
|
+
if (desktopGatewayText && desktopGatewayText !== '未配置') {
|
|
85
|
+
lines.push(`Desktop Gateway ${desktopGatewayText}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
if (view.warnings.some((warning) => warning.includes('旧 DeepSeek 模型名'))) {
|
|
70
89
|
lines.push('旧模型名:选择下方"切换厂商或模型"更新到 [1m]');
|
|
71
90
|
}
|
|
@@ -73,4 +92,4 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
73
92
|
return lines;
|
|
74
93
|
}
|
|
75
94
|
|
|
76
|
-
module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, isEnvActive };
|
|
95
|
+
module.exports = { buildMenuStatusLines, buildStatusView, apiStatusText, desktopGatewayStatusText, isEnvActive };
|