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 CHANGED
@@ -47,8 +47,11 @@ claw code
47
47
  接入 Claude 桌面应用:
48
48
  ```bash
49
49
  claw desktop
50
+ claw gateway
50
51
  ```
51
- 将配置写入 Claude Desktop 第三方推理本地配置。macOS 会自动重启 Claude Desktop;Windows 需手动重新打开。
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`)写入 Claude Desktop 第三方推理配置:
113
+ **桌面接入**(`claw desktop`)默认写入本机 Gateway:
109
114
 
110
- - macOS:`~/Library/Application Support/Claude-3p/configLibrary/`
111
- - Windows:`%APPDATA%\Claude-3p\configLibrary\`
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 指向对应厂商的 Anthropic 兼容接口。桌面接入要求 Base URL 使用 HTTPS。
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
- : label === '当前终端' && value === '未生效'
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
- .action(async () => {
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(config);
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 桌面应用已配置为 Gateway 模式\n\n') +
708
- chalk.dim('Base URL ') + chalk.cyan(config.baseUrl) + '\n' +
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
@@ -1,6 +1,7 @@
1
1
  module.exports = {
2
2
  ...require('./lib/config'),
3
3
  ...require('./lib/desktop'),
4
+ ...require('./lib/gateway'),
4
5
  ...require('./lib/install'),
5
6
  ...require('./lib/panel'),
6
7
  };
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 collectModels(config) {
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
- // 按官方 schema:所有值必须是字符串(包括布尔、数组都序列化)
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 models = collectModels(config);
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 baseUrl = normalizeAnthropicBaseUrl(config.baseUrl);
135
- if (!baseUrl.startsWith('https://')) {
136
- throw new Error('Claude 桌面应用要求 Gateway Base URL 使用 HTTPS');
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 = collectModels(config);
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: 'gateway',
156
- inferenceGatewayBaseUrl: baseUrl,
157
- inferenceGatewayApiKey: config.apiKey,
158
- inferenceGatewayAuthScheme: options.authScheme || 'bearer',
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 next = { ...current, deploymentMode: '3p' };
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
- return { result: beforeEntry === entryBody ? 'unchanged' : 'updated', file };
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
- return {
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {