yingclaw 2.5.1 → 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 +73 -6
- package/index.js +1 -0
- package/lib/desktop.js +51 -13
- package/lib/gateway.js +198 -0
- 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,12 @@ 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
|
+
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();
|
|
@@ -662,10 +668,54 @@ program
|
|
|
662
668
|
.description('查看当前配置和 Key 有效性')
|
|
663
669
|
.action(showStatus);
|
|
664
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
|
+
|
|
665
714
|
program
|
|
666
715
|
.command('desktop')
|
|
667
716
|
.description('接入 Claude 桌面应用使用当前模型')
|
|
668
|
-
.
|
|
717
|
+
.option('--direct', '高级:直接写入当前厂商 Gateway URL,不启用本机模型映射')
|
|
718
|
+
.action(async (options) => {
|
|
669
719
|
const chalk = (await import('chalk')).default;
|
|
670
720
|
const ora = (await import('ora')).default;
|
|
671
721
|
const boxen = (await import('boxen')).default;
|
|
@@ -689,10 +739,18 @@ program
|
|
|
689
739
|
return;
|
|
690
740
|
}
|
|
691
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
|
+
|
|
692
750
|
const spinner = ora('写入 Claude 桌面应用配置...').start();
|
|
693
751
|
let result;
|
|
694
752
|
try {
|
|
695
|
-
result = writeClaudeDesktopConfig(
|
|
753
|
+
result = writeClaudeDesktopConfig(desktopConfig, { direct: options.direct });
|
|
696
754
|
if (result.result === 'unsupported') {
|
|
697
755
|
spinner.warn(chalk.yellow('当前系统暂不支持自动配置 Claude 桌面应用'));
|
|
698
756
|
return;
|
|
@@ -703,13 +761,22 @@ program
|
|
|
703
761
|
return;
|
|
704
762
|
}
|
|
705
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
|
+
|
|
706
774
|
console.log(boxen(
|
|
707
|
-
chalk.bold('Claude
|
|
708
|
-
|
|
709
|
-
chalk.dim('模型 ') + chalk.yellow(config.model) + '\n' +
|
|
775
|
+
chalk.bold(options.direct ? 'Claude 桌面应用已配置为直连 Gateway 模式\n\n' : 'Claude 桌面应用已配置为本机 Gateway 模式\n\n') +
|
|
776
|
+
modeLines + '\n' +
|
|
710
777
|
chalk.dim('认证方式 ') + chalk.cyan('bearer') + '\n\n' +
|
|
711
778
|
chalk.yellow(getDesktopOpenHint()) + '\n' +
|
|
712
|
-
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 才能访问第三方模型。'),
|
|
713
780
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 1 } }
|
|
714
781
|
));
|
|
715
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
|
+
};
|