yingclaw 1.7.1 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,8 @@ claw setup
35
35
 
36
36
  工具会根据 Base URL 自动尝试获取模型列表;如果获取失败,则手动输入主模型和快速模型。
37
37
 
38
+ 注意:模型列表能获取不代表一定可用于 Claude Code。自定义接口还必须支持 Anthropic `/v1/messages`,否则 Claude Code 请求会被网关拒绝。
39
+
38
40
  **第三步:以后直接用**
39
41
  ```bash
40
42
  claude
package/bin/cli.js CHANGED
@@ -10,13 +10,13 @@ const {
10
10
  fetchModelsFromBaseUrl,
11
11
  resetConfig,
12
12
  validateConfig,
13
+ normalizeAnthropicBaseUrl,
13
14
  resolveFastModel,
14
15
  buildClaudeEnv,
15
16
  classifyValidationStatus,
16
17
  PROVIDERS,
17
18
  } = require('../lib/config');
18
19
  const { execSync, spawn, spawnSync } = require('child_process');
19
- const https = require('https');
20
20
  const pkg = require('../package.json');
21
21
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
22
22
  const { buildClaudeInstallCommand } = require('../lib/install');
@@ -36,37 +36,39 @@ async function getBanner() {
36
36
  }
37
37
 
38
38
  async function validateKey(config) {
39
- return new Promise((resolve) => {
40
- let url;
41
- try {
42
- url = new URL(config.baseUrl + '/v1/messages');
43
- } catch {
44
- return resolve(null);
45
- }
46
- const body = Buffer.from(JSON.stringify({
47
- model: config.model,
48
- max_tokens: 1,
49
- messages: [{ role: 'user', content: 'hi' }],
50
- }));
51
- const req = https.request({
52
- hostname: url.hostname,
53
- path: url.pathname,
39
+ let url;
40
+ try {
41
+ url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
42
+ new URL(url);
43
+ } catch {
44
+ return null;
45
+ }
46
+
47
+ const controller = new AbortController();
48
+ const timeout = setTimeout(() => controller.abort(), 8000);
49
+
50
+ try {
51
+ const res = await fetch(url, {
54
52
  method: 'POST',
55
- timeout: 8000,
53
+ signal: controller.signal,
56
54
  headers: {
57
55
  'content-type': 'application/json',
56
+ authorization: `Bearer ${config.apiKey}`,
58
57
  'x-api-key': config.apiKey,
59
58
  'anthropic-version': '2023-06-01',
60
- 'content-length': body.length,
61
59
  },
62
- }, (res) => {
63
- resolve(classifyValidationStatus(res.statusCode));
60
+ body: JSON.stringify({
61
+ model: config.model,
62
+ max_tokens: 1,
63
+ messages: [{ role: 'user', content: 'hi' }],
64
+ }),
64
65
  });
65
- req.on('error', () => resolve(null));
66
- req.on('timeout', () => { req.destroy(); resolve(null); });
67
- req.write(body);
68
- req.end();
69
- });
66
+ return classifyValidationStatus(res.status);
67
+ } catch {
68
+ return null;
69
+ } finally {
70
+ clearTimeout(timeout);
71
+ }
70
72
  }
71
73
 
72
74
  function getConfigValidationMessage(config) {
@@ -123,7 +125,7 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
123
125
  message: chalk.cyan('Anthropic Base URL'),
124
126
  default: existingConfig?.provider === 'custom' ? existingConfig.baseUrl : undefined,
125
127
  validate: (v) => v.trim().length > 0 && isValidUrl(v.trim()) ? true : '请输入有效 URL',
126
- }).then(v => v.trim().replace(/\/+$/, ''));
128
+ }).then(v => normalizeAnthropicBaseUrl(v.trim()));
127
129
 
128
130
  let apiKey = existingConfig?.provider === 'custom' ? existingConfig.apiKey : '';
129
131
  if (apiKey) {
package/lib/config.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
- const https = require('https');
5
4
 
6
5
  const CONFIG_FILE = path.join(os.homedir(), '.clawai.json');
7
6
 
@@ -80,9 +79,26 @@ function parseModelIdsResponse(providerKey, data) {
80
79
  return normalizeModelIds(providerKey, ids);
81
80
  }
82
81
 
82
+ function normalizeAnthropicBaseUrl(baseUrl) {
83
+ let url;
84
+ try { url = new URL(baseUrl); } catch { return baseUrl; }
85
+
86
+ const parts = url.pathname.split('/').filter(Boolean);
87
+ if (parts.at(-1) === 'messages' && parts.at(-2) === 'v1') {
88
+ parts.splice(-2, 2);
89
+ } else if (parts.at(-1) === 'v1') {
90
+ parts.pop();
91
+ }
92
+
93
+ url.pathname = parts.length > 0 ? `/${parts.join('/')}` : '/';
94
+ url.search = '';
95
+ url.hash = '';
96
+ return url.toString().replace(/\/+$/, '');
97
+ }
98
+
83
99
  function buildModelUrlCandidates(baseUrl) {
84
100
  let url;
85
- try { url = new URL(baseUrl); } catch { return []; }
101
+ try { url = new URL(normalizeAnthropicBaseUrl(baseUrl)); } catch { return []; }
86
102
 
87
103
  const pathname = url.pathname.replace(/\/+$/, '');
88
104
  const candidates = [];
@@ -118,41 +134,38 @@ async function fetchModelsFromBaseUrl(providerKey, apiKey, baseUrl, fetcher = fe
118
134
  }
119
135
 
120
136
  // 联网拉取厂商支持的模型列表,失败返回 null
121
- function fetchModels(providerKey, apiKey, modelsUrlOverride) {
122
- return new Promise((resolve) => {
123
- const provider = PROVIDERS[providerKey];
124
- const modelsUrl = modelsUrlOverride || provider?.modelsUrl;
125
- if (!modelsUrl) return resolve(null);
126
-
127
- let url;
128
- try { url = new URL(modelsUrl); } catch { return resolve(null); }
129
-
130
- const req = https.request({
131
- hostname: url.hostname,
132
- path: url.pathname + url.search,
137
+ async function fetchModels(providerKey, apiKey, modelsUrlOverride) {
138
+ const provider = PROVIDERS[providerKey];
139
+ const modelsUrl = modelsUrlOverride || provider?.modelsUrl;
140
+ if (!modelsUrl) return null;
141
+
142
+ try {
143
+ new URL(modelsUrl);
144
+ } catch {
145
+ return null;
146
+ }
147
+
148
+ const controller = new AbortController();
149
+ const timeout = setTimeout(() => controller.abort(), 6000);
150
+
151
+ try {
152
+ const res = await fetch(modelsUrl, {
133
153
  method: 'GET',
134
- timeout: 6000,
154
+ signal: controller.signal,
135
155
  headers: {
136
- 'authorization': `Bearer ${apiKey}`,
156
+ authorization: `Bearer ${apiKey}`,
137
157
  'api-key': apiKey, // MiMo 用这个 header
138
158
  },
139
- }, (res) => {
140
- let data = '';
141
- res.on('data', (c) => data += c);
142
- res.on('end', () => {
143
- try {
144
- const ids = parseModelIdsResponse(providerKey, data);
145
- if (ids.length === 0) return resolve(null);
146
- resolve(ids);
147
- } catch {
148
- resolve(null);
149
- }
150
- });
151
159
  });
152
- req.on('error', () => resolve(null));
153
- req.on('timeout', () => { req.destroy(); resolve(null); });
154
- req.end();
155
- });
160
+ if (!res.ok) return null;
161
+
162
+ const ids = parseModelIdsResponse(providerKey, await res.text());
163
+ return ids.length > 0 ? ids : null;
164
+ } catch {
165
+ return null;
166
+ } finally {
167
+ clearTimeout(timeout);
168
+ }
156
169
  }
157
170
 
158
171
  function loadConfig() {
@@ -303,6 +316,7 @@ module.exports = {
303
316
  resetConfig,
304
317
  validateConfig,
305
318
  normalizeModelIds,
319
+ normalizeAnthropicBaseUrl,
306
320
  parseModelIdsResponse,
307
321
  buildModelUrlCandidates,
308
322
  fetchModelsFromBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "1.7.1",
3
+ "version": "1.7.4",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {