yingclaw 2.5.0 → 2.5.2
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 +101 -10
- package/index.js +1 -0
- package/lib/desktop.js +51 -13
- package/lib/gateway.js +198 -0
- package/lib/install.js +86 -1
- 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
|
@@ -21,8 +21,14 @@ const {
|
|
|
21
21
|
const { execSync, spawn, spawnSync } = require('child_process');
|
|
22
22
|
const pkg = require('../package.json');
|
|
23
23
|
const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
|
|
24
|
-
const { buildClaudeInstallCommand } = require('../lib/install');
|
|
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
|
+
createGatewayServer,
|
|
30
|
+
ensureDesktopGatewayConfig,
|
|
31
|
+
} = require('../lib/gateway');
|
|
26
32
|
const { runDoctorChecks, summarize, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
|
|
27
33
|
|
|
28
34
|
const program = new Command();
|
|
@@ -402,6 +408,31 @@ program
|
|
|
402
408
|
if (!yes) return;
|
|
403
409
|
} catch {}
|
|
404
410
|
|
|
411
|
+
// 检查 npm / Node.js 环境
|
|
412
|
+
const nodeEnv = checkNodeEnv();
|
|
413
|
+
if (!nodeEnv.npmOk) {
|
|
414
|
+
console.log(boxen(
|
|
415
|
+
chalk.bold.red('未检测到 npm,无法安装 Claude Code\n\n') +
|
|
416
|
+
chalk.dim('Claude Code 通过 npm 安装,需要先安装 Node.js(含 npm)。\n\n') +
|
|
417
|
+
chalk.bold('安装方式:\n') +
|
|
418
|
+
getNodeInstallGuide().map(l => chalk.cyan(' ' + l)).join('\n') +
|
|
419
|
+
'\n\n' + chalk.dim('安装完成后重新运行 claw install-claude'),
|
|
420
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'red', margin: { top: 1, bottom: 1 } }
|
|
421
|
+
));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!nodeEnv.nodeOk) {
|
|
425
|
+
console.log(boxen(
|
|
426
|
+
chalk.bold.yellow(`Node.js 版本过低(当前 v${nodeEnv.nodeVersion},需要 ≥18)\n\n`) +
|
|
427
|
+
chalk.dim('Claude Code 要求 Node.js ≥18,当前版本可能导致安装或运行失败。\n\n') +
|
|
428
|
+
chalk.bold('升级方式:\n') +
|
|
429
|
+
getNodeInstallGuide().map(l => chalk.cyan(' ' + l)).join('\n'),
|
|
430
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'yellow', margin: { top: 1, bottom: 1 } }
|
|
431
|
+
));
|
|
432
|
+
const proceed = await confirm({ message: '仍然尝试安装?', default: false });
|
|
433
|
+
if (!proceed) return;
|
|
434
|
+
}
|
|
435
|
+
|
|
405
436
|
const network = await select({ loop: false,
|
|
406
437
|
message: chalk.cyan('你的网络环境'),
|
|
407
438
|
choices: [
|
|
@@ -423,10 +454,9 @@ program
|
|
|
423
454
|
console.log(chalk.green('\n✔ Claude Code 安装成功!'));
|
|
424
455
|
} else {
|
|
425
456
|
console.log(chalk.red('\n✘ 安装失败'));
|
|
457
|
+
const hints = getInstallFailureHints(result, chalk);
|
|
426
458
|
console.log(boxen(
|
|
427
|
-
|
|
428
|
-
chalk.cyan('npm config set registry https://registry.npmmirror.com\n') +
|
|
429
|
-
chalk.cyan('npm install -g @anthropic-ai/claude-code'),
|
|
459
|
+
hints.join(''),
|
|
430
460
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'yellow', margin: { top: 1, bottom: 1 } }
|
|
431
461
|
));
|
|
432
462
|
return;
|
|
@@ -638,10 +668,54 @@ program
|
|
|
638
668
|
.description('查看当前配置和 Key 有效性')
|
|
639
669
|
.action(showStatus);
|
|
640
670
|
|
|
671
|
+
program
|
|
672
|
+
.command('gateway')
|
|
673
|
+
.description('启动 Claude 桌面应用本机 Gateway')
|
|
674
|
+
.option('-p, --port <port>', '监听端口')
|
|
675
|
+
.action(async (options) => {
|
|
676
|
+
const chalk = (await import('chalk')).default;
|
|
677
|
+
const boxen = (await import('boxen')).default;
|
|
678
|
+
|
|
679
|
+
let config = loadConfig();
|
|
680
|
+
if (!config) {
|
|
681
|
+
console.log(chalk.red('\n未配置 API 连接,请先运行: claw config\n'));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const configProblem = getConfigValidationMessage(config);
|
|
685
|
+
if (configProblem) {
|
|
686
|
+
console.log(chalk.red(`\n配置无效:${configProblem}`));
|
|
687
|
+
console.log(chalk.dim('请运行 claw config 重新配置。\n'));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const ensuredConfig = ensureDesktopGatewayConfig(config);
|
|
692
|
+
if (ensuredConfig.desktopGatewayKey !== config.desktopGatewayKey || ensuredConfig.desktopGatewayPort !== config.desktopGatewayPort) {
|
|
693
|
+
saveConfig(ensuredConfig);
|
|
694
|
+
config = ensuredConfig;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const port = Number.parseInt(options.port || config.desktopGatewayPort || DEFAULT_DESKTOP_GATEWAY_PORT, 10);
|
|
698
|
+
const server = createGatewayServer();
|
|
699
|
+
server.listen(port, '127.0.0.1', () => {
|
|
700
|
+
console.log(boxen(
|
|
701
|
+
chalk.bold('yingclaw Desktop Gateway 已启动\n\n') +
|
|
702
|
+
chalk.dim('URL ') + chalk.cyan(`http://127.0.0.1:${port}${YINGCLAW_GATEWAY_PREFIX}`) + '\n' +
|
|
703
|
+
chalk.dim('模型 ') + chalk.yellow(config.model) + '\n\n' +
|
|
704
|
+
chalk.dim('保持此终端打开,Claude Desktop 才能访问第三方模型。'),
|
|
705
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
706
|
+
));
|
|
707
|
+
});
|
|
708
|
+
server.on('error', (error) => {
|
|
709
|
+
console.error(chalk.red(`Gateway 启动失败: ${error.message}`));
|
|
710
|
+
process.exitCode = 1;
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
641
714
|
program
|
|
642
715
|
.command('desktop')
|
|
643
716
|
.description('接入 Claude 桌面应用使用当前模型')
|
|
644
|
-
.
|
|
717
|
+
.option('--direct', '高级:直接写入当前厂商 Gateway URL,不启用本机模型映射')
|
|
718
|
+
.action(async (options) => {
|
|
645
719
|
const chalk = (await import('chalk')).default;
|
|
646
720
|
const ora = (await import('ora')).default;
|
|
647
721
|
const boxen = (await import('boxen')).default;
|
|
@@ -665,10 +739,18 @@ program
|
|
|
665
739
|
return;
|
|
666
740
|
}
|
|
667
741
|
|
|
742
|
+
let desktopConfig = config;
|
|
743
|
+
if (!options.direct) {
|
|
744
|
+
desktopConfig = ensureDesktopGatewayConfig(config);
|
|
745
|
+
if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
|
|
746
|
+
saveConfig(desktopConfig);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
668
750
|
const spinner = ora('写入 Claude 桌面应用配置...').start();
|
|
669
751
|
let result;
|
|
670
752
|
try {
|
|
671
|
-
result = writeClaudeDesktopConfig(
|
|
753
|
+
result = writeClaudeDesktopConfig(desktopConfig, { direct: options.direct });
|
|
672
754
|
if (result.result === 'unsupported') {
|
|
673
755
|
spinner.warn(chalk.yellow('当前系统暂不支持自动配置 Claude 桌面应用'));
|
|
674
756
|
return;
|
|
@@ -679,13 +761,22 @@ program
|
|
|
679
761
|
return;
|
|
680
762
|
}
|
|
681
763
|
|
|
764
|
+
const gatewayUrl = `http://127.0.0.1:${desktopConfig.desktopGatewayPort}${YINGCLAW_GATEWAY_PREFIX}`;
|
|
765
|
+
const modeLines = options.direct
|
|
766
|
+
? chalk.dim('Base URL ') + chalk.cyan(desktopConfig.baseUrl) + '\n' +
|
|
767
|
+
chalk.dim('模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
|
|
768
|
+
chalk.yellow('直连模式可能被新版 Claude Desktop 拒绝非 Claude 模型名。')
|
|
769
|
+
: chalk.dim('Gateway ') + chalk.cyan(gatewayUrl) + '\n' +
|
|
770
|
+
chalk.dim('主模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
|
|
771
|
+
chalk.dim('快速模型 ') + chalk.yellow(desktopConfig.fastModel || desktopConfig.model) + '\n\n' +
|
|
772
|
+
chalk.cyan('使用前请运行: claw gateway');
|
|
773
|
+
|
|
682
774
|
console.log(boxen(
|
|
683
|
-
chalk.bold('Claude
|
|
684
|
-
|
|
685
|
-
chalk.dim('模型 ') + chalk.yellow(config.model) + '\n' +
|
|
775
|
+
chalk.bold(options.direct ? 'Claude 桌面应用已配置为直连 Gateway 模式\n\n' : 'Claude 桌面应用已配置为本机 Gateway 模式\n\n') +
|
|
776
|
+
modeLines + '\n' +
|
|
686
777
|
chalk.dim('认证方式 ') + chalk.cyan('bearer') + '\n\n' +
|
|
687
778
|
chalk.yellow(getDesktopOpenHint()) + '\n' +
|
|
688
|
-
chalk.dim('要求:网关必须支持 Anthropic POST /v1/messages,且 Base URL 必须是 HTTPS。'),
|
|
779
|
+
chalk.dim(options.direct ? '要求:网关必须支持 Anthropic POST /v1/messages,且 Base URL 必须是 HTTPS。' : '要求:保持 claw gateway 运行,Claude Desktop 才能访问第三方模型。'),
|
|
689
780
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 1 } }
|
|
690
781
|
));
|
|
691
782
|
|
package/index.js
CHANGED
package/lib/desktop.js
CHANGED
|
@@ -4,6 +4,11 @@ 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';
|
|
@@ -60,16 +65,43 @@ function toDesktopModelId(model) {
|
|
|
60
65
|
return model.startsWith('claude-') ? model : `claude-${model}`;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
function
|
|
68
|
+
function collectDirectModels(config) {
|
|
64
69
|
const list = Array.isArray(config.availableModels) && config.availableModels.length > 0
|
|
65
70
|
? [config.model, config.fastModel, ...config.availableModels]
|
|
66
71
|
: [config.model, config.fastModel];
|
|
67
72
|
return [...new Set(list.filter(Boolean).map(toDesktopModelId))];
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
function buildGatewayBaseUrl(config) {
|
|
76
|
+
const port = config.desktopGatewayPort || 18080;
|
|
77
|
+
return `http://127.0.0.1:${port}${YINGCLAW_GATEWAY_PREFIX}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function serializeGatewayModels(config) {
|
|
81
|
+
return buildDesktopGatewayRoutes(config).map((route) => {
|
|
82
|
+
const item = { name: route.id, displayName: route.displayName };
|
|
83
|
+
if (route.supports1m) item.supports1m = true;
|
|
84
|
+
return item;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
function buildClaudeDesktopEnterpriseConfig(config, options = {}) {
|
|
72
|
-
const
|
|
89
|
+
const gatewayConfig = ensureDesktopGatewayConfig(config, options);
|
|
90
|
+
const models = serializeGatewayModels(gatewayConfig);
|
|
91
|
+
return {
|
|
92
|
+
inferenceProvider: 'gateway',
|
|
93
|
+
inferenceGatewayBaseUrl: buildGatewayBaseUrl(gatewayConfig),
|
|
94
|
+
inferenceGatewayApiKey: gatewayConfig.desktopGatewayKey,
|
|
95
|
+
inferenceGatewayAuthScheme: options.authScheme || 'bearer',
|
|
96
|
+
inferenceModels: JSON.stringify(models),
|
|
97
|
+
disableDeploymentModeChooser: 'true',
|
|
98
|
+
deploymentOrganizationUuid: options.uuid || crypto.randomUUID(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 兼容旧版直连模式:写入真实 provider Gateway URL 和真实 Key。
|
|
103
|
+
function buildClaudeDesktopDirectEnterpriseConfig(config, options = {}) {
|
|
104
|
+
const models = collectDirectModels(config);
|
|
73
105
|
const baseUrl = normalizeAnthropicBaseUrl(config.baseUrl);
|
|
74
106
|
if (!baseUrl.startsWith('https://')) {
|
|
75
107
|
throw new Error('Claude 桌面应用要求 Gateway Base URL 使用 HTTPS');
|
|
@@ -131,10 +163,9 @@ function clearClaudeDesktopConfigLibrary(options = {}) {
|
|
|
131
163
|
|
|
132
164
|
// 写入 Claude-3p/configLibrary/ 下的 enterprise config 条目(主进程从此处读取)
|
|
133
165
|
function writeClaudeDesktopConfig(config, options = {}) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
166
|
+
const effectiveConfig = options.direct
|
|
167
|
+
? config
|
|
168
|
+
: ensureDesktopGatewayConfig(config, options);
|
|
138
169
|
|
|
139
170
|
const dataDir = options.dataDir || getClaudeDesktopDataDir(options);
|
|
140
171
|
if (!dataDir) {
|
|
@@ -149,13 +180,18 @@ function writeClaudeDesktopConfig(config, options = {}) {
|
|
|
149
180
|
const existingYingclaw = existingEntries.find((entry) => entry && entry.name === YINGCLAW_ENTRY_NAME);
|
|
150
181
|
const uuid = existingYingclaw?.id || options.uuid || crypto.randomUUID();
|
|
151
182
|
|
|
152
|
-
const models =
|
|
183
|
+
const models = options.direct
|
|
184
|
+
? collectDirectModels(effectiveConfig)
|
|
185
|
+
: serializeGatewayModels(effectiveConfig);
|
|
186
|
+
const enterprise = options.direct
|
|
187
|
+
? buildClaudeDesktopDirectEnterpriseConfig(effectiveConfig, { ...options, uuid })
|
|
188
|
+
: buildClaudeDesktopEnterpriseConfig(effectiveConfig, { ...options, uuid });
|
|
153
189
|
|
|
154
190
|
const entry = {
|
|
155
|
-
inferenceProvider:
|
|
156
|
-
inferenceGatewayBaseUrl:
|
|
157
|
-
inferenceGatewayApiKey:
|
|
158
|
-
inferenceGatewayAuthScheme:
|
|
191
|
+
inferenceProvider: enterprise.inferenceProvider,
|
|
192
|
+
inferenceGatewayBaseUrl: enterprise.inferenceGatewayBaseUrl,
|
|
193
|
+
inferenceGatewayApiKey: enterprise.inferenceGatewayApiKey,
|
|
194
|
+
inferenceGatewayAuthScheme: enterprise.inferenceGatewayAuthScheme,
|
|
159
195
|
inferenceModels: models,
|
|
160
196
|
disableDeploymentModeChooser: true,
|
|
161
197
|
deploymentOrganizationUuid: uuid,
|
|
@@ -184,7 +220,7 @@ function writeClaudeDesktopConfig(config, options = {}) {
|
|
|
184
220
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
185
221
|
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
186
222
|
|
|
187
|
-
return { result: beforeEntry === entryBody ? 'unchanged' : 'updated', file };
|
|
223
|
+
return { result: beforeEntry === entryBody ? 'unchanged' : 'updated', file, config: effectiveConfig };
|
|
188
224
|
}
|
|
189
225
|
|
|
190
226
|
function clearClaudeDesktopConfig(options = {}) {
|
|
@@ -293,8 +329,10 @@ async function openClaudeDesktop(options = {}) {
|
|
|
293
329
|
|
|
294
330
|
module.exports = {
|
|
295
331
|
buildClaudeDesktopEnterpriseConfig,
|
|
332
|
+
buildClaudeDesktopDirectEnterpriseConfig,
|
|
296
333
|
buildClaudeDesktopOpenCommands,
|
|
297
334
|
clearClaudeDesktopConfig,
|
|
335
|
+
getClaudeDesktopConfigLibraryDir,
|
|
298
336
|
getClaudeDesktopConfigPath,
|
|
299
337
|
getClaudeDesktopDataDir,
|
|
300
338
|
isDesktopConfigured,
|
package/lib/gateway.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
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-opus-4-7', displayName: 'Opus', upstreamKey: 'model' },
|
|
10
|
+
{ id: 'claude-haiku-4-5', displayName: 'Haiku', upstreamKey: 'fastModel' },
|
|
11
|
+
];
|
|
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 buildDesktopGatewayRoutes(config) {
|
|
31
|
+
const fastModel = config.fastModel || config.model;
|
|
32
|
+
const source = { model: config.model, fastModel };
|
|
33
|
+
return DESKTOP_ROUTE_SPECS
|
|
34
|
+
.map((spec) => {
|
|
35
|
+
const upstreamModel = source[spec.upstreamKey];
|
|
36
|
+
return upstreamModel ? {
|
|
37
|
+
id: spec.id,
|
|
38
|
+
displayName: spec.displayName,
|
|
39
|
+
upstreamModel,
|
|
40
|
+
supports1m: modelSupports1m(upstreamModel),
|
|
41
|
+
} : null;
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mapDesktopRouteToUpstream(config, routeId) {
|
|
47
|
+
const route = buildDesktopGatewayRoutes(config).find((item) => item.id === routeId);
|
|
48
|
+
if (!route) {
|
|
49
|
+
throw new Error(`未配置的 Claude Desktop 模型路由: ${routeId}`);
|
|
50
|
+
}
|
|
51
|
+
return route.upstreamModel;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildGatewayModelsResponse(config) {
|
|
55
|
+
const data = buildDesktopGatewayRoutes(config).map((route) => {
|
|
56
|
+
const item = {
|
|
57
|
+
type: 'model',
|
|
58
|
+
id: route.id,
|
|
59
|
+
display_name: route.displayName,
|
|
60
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
61
|
+
};
|
|
62
|
+
if (route.supports1m) item.supports1m = true;
|
|
63
|
+
return item;
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
data,
|
|
67
|
+
has_more: false,
|
|
68
|
+
first_id: data[0]?.id || null,
|
|
69
|
+
last_id: data.at(-1)?.id || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sendJson(res, status, body) {
|
|
74
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
75
|
+
res.end(JSON.stringify(body));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readRequestBody(req) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
let raw = '';
|
|
81
|
+
req.setEncoding('utf8');
|
|
82
|
+
req.on('data', chunk => { raw += chunk; });
|
|
83
|
+
req.on('end', () => resolve(raw));
|
|
84
|
+
req.on('error', reject);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isAuthorized(req, config) {
|
|
89
|
+
const key = config.desktopGatewayKey;
|
|
90
|
+
if (!key) return false;
|
|
91
|
+
const auth = String(req.headers.authorization || '');
|
|
92
|
+
const bearer = auth.replace(/^bearer\s+/i, '').trim();
|
|
93
|
+
const apiKey = String(req.headers['x-api-key'] || '').trim();
|
|
94
|
+
return bearer === key || apiKey === key;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function providerMessagesUrl(config) {
|
|
98
|
+
return `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function proxyMessages(req, res, config) {
|
|
102
|
+
const raw = await readRequestBody(req);
|
|
103
|
+
let body;
|
|
104
|
+
try {
|
|
105
|
+
body = raw ? JSON.parse(raw) : {};
|
|
106
|
+
} catch {
|
|
107
|
+
sendJson(res, 400, { error: { message: '请求体不是有效 JSON' } });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
body.model = mapDesktopRouteToUpstream(config, body.model);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
sendJson(res, 400, { error: { message: error.message } });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const upstream = await fetch(providerMessagesUrl(config), {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
'content-type': 'application/json',
|
|
122
|
+
accept: body.stream ? 'text/event-stream' : 'application/json',
|
|
123
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
124
|
+
'x-api-key': config.apiKey,
|
|
125
|
+
'anthropic-version': '2023-06-01',
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
res.writeHead(upstream.status, {
|
|
131
|
+
'content-type': upstream.headers.get('content-type') || (body.stream ? 'text/event-stream' : 'application/json'),
|
|
132
|
+
'cache-control': upstream.headers.get('cache-control') || 'no-cache',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!upstream.body) {
|
|
136
|
+
res.end();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for await (const chunk of upstream.body) {
|
|
140
|
+
res.write(chunk);
|
|
141
|
+
}
|
|
142
|
+
res.end();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createGatewayServer(options = {}) {
|
|
146
|
+
const loadConfig = options.loadConfig || defaultLoadConfig;
|
|
147
|
+
return http.createServer(async (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
150
|
+
const config = loadConfig();
|
|
151
|
+
|
|
152
|
+
if (url.pathname === `${YINGCLAW_GATEWAY_PREFIX}/health`) {
|
|
153
|
+
sendJson(res, 200, { status: 'ok' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!config) {
|
|
158
|
+
sendJson(res, 503, { error: { message: 'yingclaw 尚未配置 API 连接' } });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (url.pathname === `${YINGCLAW_GATEWAY_PREFIX}/v1/models` && req.method === 'GET') {
|
|
163
|
+
if (!isAuthorized(req, config)) {
|
|
164
|
+
sendJson(res, 401, { error: { message: 'Invalid gateway API key' } });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
sendJson(res, 200, buildGatewayModelsResponse(config));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (url.pathname === `${YINGCLAW_GATEWAY_PREFIX}/v1/messages` && req.method === 'POST') {
|
|
172
|
+
if (!isAuthorized(req, config)) {
|
|
173
|
+
sendJson(res, 401, { error: { message: 'Invalid gateway API key' } });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
await proxyMessages(req, res, config);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
sendJson(res, 404, { error: { message: 'Not found' } });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
sendJson(res, 500, { error: { message: error.message } });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
DEFAULT_DESKTOP_GATEWAY_PORT,
|
|
189
|
+
YINGCLAW_GATEWAY_PREFIX,
|
|
190
|
+
DESKTOP_ROUTE_SPECS,
|
|
191
|
+
createGatewayKey,
|
|
192
|
+
ensureDesktopGatewayConfig,
|
|
193
|
+
modelSupports1m,
|
|
194
|
+
buildDesktopGatewayRoutes,
|
|
195
|
+
mapDesktopRouteToUpstream,
|
|
196
|
+
buildGatewayModelsResponse,
|
|
197
|
+
createGatewayServer,
|
|
198
|
+
};
|
package/lib/install.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
|
|
1
3
|
function buildClaudeInstallCommand(network) {
|
|
2
4
|
const args = ['install', '-g', '@anthropic-ai/claude-code'];
|
|
3
5
|
if (network === 'cn') {
|
|
@@ -6,4 +8,87 @@ function buildClaudeInstallCommand(network) {
|
|
|
6
8
|
return { command: 'npm', args };
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
function checkNodeEnv() {
|
|
12
|
+
const nodeVersion = process.versions.node;
|
|
13
|
+
const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
|
|
14
|
+
|
|
15
|
+
let npmVersion = null;
|
|
16
|
+
try {
|
|
17
|
+
// Windows 上 npm 是 .cmd 文件,必须加 shell: true 才能找到
|
|
18
|
+
npmVersion = execSync('npm --version', {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
21
|
+
shell: process.platform === 'win32',
|
|
22
|
+
}).trim();
|
|
23
|
+
} catch {}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
nodeVersion,
|
|
27
|
+
nodeMajor,
|
|
28
|
+
nodeOk: nodeMajor >= 18,
|
|
29
|
+
npmVersion,
|
|
30
|
+
npmOk: !!npmVersion,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getNodeInstallGuide() {
|
|
35
|
+
const platform = process.platform;
|
|
36
|
+
if (platform === 'darwin') {
|
|
37
|
+
return [
|
|
38
|
+
'Homebrew: brew install node',
|
|
39
|
+
'官方安装包: https://nodejs.org (选 LTS 版本)',
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
if (platform === 'win32') {
|
|
43
|
+
return [
|
|
44
|
+
'官方安装包: https://nodejs.org (选 LTS 版本)',
|
|
45
|
+
'winget: winget install OpenJS.NodeJS.LTS',
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
'Ubuntu/Debian: sudo apt install nodejs npm',
|
|
50
|
+
'CentOS/RHEL: sudo yum install nodejs npm',
|
|
51
|
+
'nvm(推荐): https://github.com/nvm-sh/nvm',
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 根据 spawnSync 结果判断失败原因,返回对应的提示内容(chalk 字符串数组)。
|
|
57
|
+
* @param {object} result spawnSync 返回值
|
|
58
|
+
* @param {Function} chalk chalk 实例
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
function getInstallFailureHints(result, chalk) {
|
|
62
|
+
// spawnSync 找不到命令(npm 在执行时消失或环境问题)
|
|
63
|
+
if (result.status === null && result.error?.code === 'ENOENT') {
|
|
64
|
+
return [
|
|
65
|
+
chalk.bold.red('npm 命令未找到\n\n'),
|
|
66
|
+
chalk.dim('安装 Node.js 后重试:\n'),
|
|
67
|
+
...getNodeInstallGuide().map(l => chalk.cyan(' ' + l)),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 尝试从 stderr 判断权限错误(stdio inherit 时 stderr 为 null,兜底靠文字提示)
|
|
72
|
+
const stderr = result.stderr ? result.stderr.toString() : '';
|
|
73
|
+
if (stderr.includes('EACCES') || stderr.includes('permission denied') || stderr.includes('Access is denied')) {
|
|
74
|
+
const fixes = process.platform === 'win32'
|
|
75
|
+
? ['以管理员身份运行 PowerShell / CMD,然后重试']
|
|
76
|
+
: [
|
|
77
|
+
'sudo npm install -g @anthropic-ai/claude-code',
|
|
78
|
+
'或修复 npm 全局目录权限:https://docs.npmjs.com/resolving-eacces-permissions-errors',
|
|
79
|
+
];
|
|
80
|
+
return [
|
|
81
|
+
chalk.bold.red('权限不足(EACCES)\n\n'),
|
|
82
|
+
...fixes.map(l => chalk.cyan(' ' + l)),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 通用失败:显示手动安装命令
|
|
87
|
+
return [
|
|
88
|
+
chalk.bold('手动安装:\n\n'),
|
|
89
|
+
chalk.cyan('npm config set registry https://registry.npmmirror.com\n'),
|
|
90
|
+
chalk.cyan('npm install -g @anthropic-ai/claude-code'),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { buildClaudeInstallCommand, checkNodeEnv, getNodeInstallGuide, getInstallFailureHints };
|