yingclaw 2.5.20 → 2.5.26
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 +31 -6
- package/bin/cli.js +372 -67
- package/index.js +2 -0
- package/lib/config.js +193 -20
- package/lib/doctor.js +13 -3
- package/lib/gateway.js +137 -6
- package/lib/openai.js +294 -0
- package/lib/panel.js +19 -5
- package/lib/vscode.js +280 -0
- package/package.json +2 -2
package/lib/openai.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
function normalizeOpenAiBaseUrl(baseUrl) {
|
|
2
|
+
let url;
|
|
3
|
+
try { url = new URL(baseUrl); } catch { return baseUrl; }
|
|
4
|
+
|
|
5
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
6
|
+
if (parts.at(-1) === 'completions' && parts.at(-2) === 'chat' && parts.at(-3) === 'v1') {
|
|
7
|
+
parts.splice(-3, 3);
|
|
8
|
+
} else if (parts.at(-1) === 'models' && parts.at(-2) === 'v1') {
|
|
9
|
+
parts.splice(-2, 2);
|
|
10
|
+
} else if (parts.at(-1) === 'v1') {
|
|
11
|
+
parts.pop();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
url.pathname = parts.length > 0 ? `/${parts.join('/')}` : '/';
|
|
15
|
+
url.search = '';
|
|
16
|
+
url.hash = '';
|
|
17
|
+
return url.toString().replace(/\/+$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function openAiChatCompletionsUrl(baseUrl) {
|
|
21
|
+
return `${normalizeOpenAiBaseUrl(baseUrl)}/v1/chat/completions`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function anthropicTextFromContent(content) {
|
|
25
|
+
if (typeof content === 'string') return content;
|
|
26
|
+
if (!Array.isArray(content)) return '';
|
|
27
|
+
return content
|
|
28
|
+
.map((part) => {
|
|
29
|
+
if (typeof part === 'string') return part;
|
|
30
|
+
if (part?.type === 'text') return part.text || '';
|
|
31
|
+
return '';
|
|
32
|
+
})
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeSystemMessages(system) {
|
|
38
|
+
if (!system) return [];
|
|
39
|
+
if (typeof system === 'string') return [{ role: 'system', content: system }];
|
|
40
|
+
if (Array.isArray(system)) {
|
|
41
|
+
const text = anthropicTextFromContent(system);
|
|
42
|
+
return text ? [{ role: 'system', content: text }] : [];
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function anthropicToolUseToOpenAi(block) {
|
|
48
|
+
return {
|
|
49
|
+
id: block.id,
|
|
50
|
+
type: 'function',
|
|
51
|
+
function: {
|
|
52
|
+
name: block.name,
|
|
53
|
+
arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {}),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function anthropicToolResultContentToText(content) {
|
|
59
|
+
if (typeof content === 'string') return content;
|
|
60
|
+
if (Array.isArray(content)) {
|
|
61
|
+
return content.filter(p => p?.type === 'text').map(p => p.text || '').join('\n');
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function anthropicMessageToOpenAi(message) {
|
|
67
|
+
const content = message.content;
|
|
68
|
+
if (message.role === 'assistant') {
|
|
69
|
+
const textParts = Array.isArray(content)
|
|
70
|
+
? content.filter(p => p?.type === 'text').map(p => p.text || '').join('')
|
|
71
|
+
: typeof content === 'string' ? content : '';
|
|
72
|
+
const toolUses = Array.isArray(content) ? content.filter(p => p?.type === 'tool_use') : [];
|
|
73
|
+
const msg = { role: 'assistant', content: textParts || null };
|
|
74
|
+
if (toolUses.length > 0) {
|
|
75
|
+
msg.tool_calls = toolUses.map(anthropicToolUseToOpenAi);
|
|
76
|
+
}
|
|
77
|
+
if (msg.content === null && !msg.tool_calls) msg.content = '';
|
|
78
|
+
return [msg];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// user role: split tool_result blocks into separate 'tool' messages
|
|
82
|
+
if (!Array.isArray(content)) {
|
|
83
|
+
return [{ role: 'user', content: anthropicTextFromContent(content) }];
|
|
84
|
+
}
|
|
85
|
+
const toolResults = content.filter(p => p?.type === 'tool_result');
|
|
86
|
+
const other = content.filter(p => p?.type !== 'tool_result');
|
|
87
|
+
const messages = toolResults.map(tr => ({
|
|
88
|
+
role: 'tool',
|
|
89
|
+
tool_call_id: tr.tool_use_id,
|
|
90
|
+
content: anthropicToolResultContentToText(tr.content),
|
|
91
|
+
}));
|
|
92
|
+
const text = anthropicTextFromContent(other);
|
|
93
|
+
if (text) messages.push({ role: 'user', content: text });
|
|
94
|
+
if (messages.length === 0) messages.push({ role: 'user', content: '' });
|
|
95
|
+
return messages;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function anthropicToolsToOpenAi(tools) {
|
|
99
|
+
if (!Array.isArray(tools)) return undefined;
|
|
100
|
+
const converted = tools.map(tool => ({
|
|
101
|
+
type: 'function',
|
|
102
|
+
function: {
|
|
103
|
+
name: tool.name,
|
|
104
|
+
description: tool.description || '',
|
|
105
|
+
parameters: tool.input_schema || { type: 'object', properties: {} },
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
return converted.length > 0 ? converted : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function anthropicToOpenAiChatRequest(body) {
|
|
112
|
+
const messages = [
|
|
113
|
+
...normalizeSystemMessages(body.system),
|
|
114
|
+
...(Array.isArray(body.messages) ? body.messages : []).flatMap(anthropicMessageToOpenAi),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const request = {
|
|
118
|
+
model: body.model,
|
|
119
|
+
messages,
|
|
120
|
+
stream: !!body.stream,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (body.max_tokens !== undefined) request.max_tokens = body.max_tokens;
|
|
124
|
+
if (body.temperature !== undefined) request.temperature = body.temperature;
|
|
125
|
+
if (body.top_p !== undefined) request.top_p = body.top_p;
|
|
126
|
+
if (body.stop_sequences !== undefined) request.stop = body.stop_sequences;
|
|
127
|
+
const tools = anthropicToolsToOpenAi(body.tools);
|
|
128
|
+
if (tools) request.tools = tools;
|
|
129
|
+
return request;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function openAiFinishReasonToAnthropic(reason) {
|
|
133
|
+
if (reason === 'length') return 'max_tokens';
|
|
134
|
+
if (reason === 'tool_calls' || reason === 'function_call') return 'tool_use';
|
|
135
|
+
if (reason === 'stop') return 'end_turn';
|
|
136
|
+
return reason || 'end_turn';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function openAiToolCallToAnthropic(toolCall) {
|
|
140
|
+
let input = {};
|
|
141
|
+
try { input = JSON.parse(toolCall.function?.arguments || '{}'); } catch {}
|
|
142
|
+
return {
|
|
143
|
+
type: 'tool_use',
|
|
144
|
+
id: toolCall.id || `tool_${Date.now()}`,
|
|
145
|
+
name: toolCall.function?.name || '',
|
|
146
|
+
input,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function openAiToAnthropicMessage(body, fallbackModel) {
|
|
151
|
+
const choice = body?.choices?.[0] || {};
|
|
152
|
+
const message = choice.message || {};
|
|
153
|
+
const rawContent = message.content ?? choice.text ?? '';
|
|
154
|
+
const text = Array.isArray(rawContent)
|
|
155
|
+
? rawContent.map((part) => part?.text || '').join('')
|
|
156
|
+
: String(rawContent || '');
|
|
157
|
+
|
|
158
|
+
const contentBlocks = [];
|
|
159
|
+
if (text) contentBlocks.push({ type: 'text', text });
|
|
160
|
+
if (Array.isArray(message.tool_calls)) {
|
|
161
|
+
for (const tc of message.tool_calls) contentBlocks.push(openAiToolCallToAnthropic(tc));
|
|
162
|
+
}
|
|
163
|
+
if (contentBlocks.length === 0) contentBlocks.push({ type: 'text', text: '' });
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
id: body?.id || `msg_${Date.now()}`,
|
|
167
|
+
type: 'message',
|
|
168
|
+
role: 'assistant',
|
|
169
|
+
model: body?.model || fallbackModel,
|
|
170
|
+
content: contentBlocks,
|
|
171
|
+
stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason),
|
|
172
|
+
stop_sequence: null,
|
|
173
|
+
usage: {
|
|
174
|
+
input_tokens: body?.usage?.prompt_tokens ?? body?.usage?.input_tokens ?? 0,
|
|
175
|
+
output_tokens: body?.usage?.completion_tokens ?? body?.usage?.output_tokens ?? 0,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function anthropicSse(event, data) {
|
|
181
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function openAiStreamChunkToAnthropicEvents(chunk, state) {
|
|
185
|
+
const choice = chunk?.choices?.[0] || {};
|
|
186
|
+
const delta = choice.delta || {};
|
|
187
|
+
const events = [];
|
|
188
|
+
|
|
189
|
+
if (!state.started) {
|
|
190
|
+
state.started = true;
|
|
191
|
+
// index 0 is always the text block
|
|
192
|
+
state.textBlockOpen = false;
|
|
193
|
+
// map tool_call index → anthropic content block index
|
|
194
|
+
state.toolIndexMap = {};
|
|
195
|
+
state.nextBlockIndex = 0;
|
|
196
|
+
events.push(anthropicSse('message_start', {
|
|
197
|
+
type: 'message_start',
|
|
198
|
+
message: {
|
|
199
|
+
id: chunk?.id || `msg_${Date.now()}`,
|
|
200
|
+
type: 'message',
|
|
201
|
+
role: 'assistant',
|
|
202
|
+
model: chunk?.model || state.model,
|
|
203
|
+
content: [],
|
|
204
|
+
stop_reason: null,
|
|
205
|
+
stop_sequence: null,
|
|
206
|
+
usage: { input_tokens: chunk?.usage?.prompt_tokens ?? 0, output_tokens: 0 },
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (delta.content) {
|
|
212
|
+
if (!state.textBlockOpen) {
|
|
213
|
+
state.textBlockOpen = true;
|
|
214
|
+
state.textBlockIndex = state.nextBlockIndex++;
|
|
215
|
+
events.push(anthropicSse('content_block_start', {
|
|
216
|
+
type: 'content_block_start',
|
|
217
|
+
index: state.textBlockIndex,
|
|
218
|
+
content_block: { type: 'text', text: '' },
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
events.push(anthropicSse('content_block_delta', {
|
|
222
|
+
type: 'content_block_delta',
|
|
223
|
+
index: state.textBlockIndex,
|
|
224
|
+
delta: { type: 'text_delta', text: delta.content },
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
229
|
+
for (const tc of delta.tool_calls) {
|
|
230
|
+
const tcIdx = tc.index ?? 0;
|
|
231
|
+
if (!(tcIdx in state.toolIndexMap)) {
|
|
232
|
+
// close text block first if open
|
|
233
|
+
if (state.textBlockOpen && !state.textBlockClosed) {
|
|
234
|
+
state.textBlockClosed = true;
|
|
235
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: state.textBlockIndex }));
|
|
236
|
+
}
|
|
237
|
+
const blockIndex = state.nextBlockIndex++;
|
|
238
|
+
state.toolIndexMap[tcIdx] = blockIndex;
|
|
239
|
+
let input = {};
|
|
240
|
+
try { if (tc.function?.arguments) input = JSON.parse(tc.function.arguments); } catch {}
|
|
241
|
+
events.push(anthropicSse('content_block_start', {
|
|
242
|
+
type: 'content_block_start',
|
|
243
|
+
index: blockIndex,
|
|
244
|
+
content_block: {
|
|
245
|
+
type: 'tool_use',
|
|
246
|
+
id: tc.id || `tool_${Date.now()}_${tcIdx}`,
|
|
247
|
+
name: tc.function?.name || '',
|
|
248
|
+
input,
|
|
249
|
+
},
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
if (tc.function?.arguments) {
|
|
253
|
+
events.push(anthropicSse('content_block_delta', {
|
|
254
|
+
type: 'content_block_delta',
|
|
255
|
+
index: state.toolIndexMap[tcIdx],
|
|
256
|
+
delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (choice.finish_reason) {
|
|
263
|
+
state.finished = true;
|
|
264
|
+
// close any open blocks
|
|
265
|
+
if (state.textBlockOpen && !state.textBlockClosed) {
|
|
266
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: state.textBlockIndex }));
|
|
267
|
+
}
|
|
268
|
+
for (const blockIndex of Object.values(state.toolIndexMap)) {
|
|
269
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: blockIndex }));
|
|
270
|
+
}
|
|
271
|
+
// if nothing was opened at all, emit an empty text block
|
|
272
|
+
if (!state.textBlockOpen && Object.keys(state.toolIndexMap).length === 0) {
|
|
273
|
+
events.push(anthropicSse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));
|
|
274
|
+
events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: 0 }));
|
|
275
|
+
}
|
|
276
|
+
const outputTokens = chunk?.usage?.completion_tokens ?? chunk?.usage?.output_tokens ?? 0;
|
|
277
|
+
events.push(anthropicSse('message_delta', {
|
|
278
|
+
type: 'message_delta',
|
|
279
|
+
delta: { stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason), stop_sequence: null },
|
|
280
|
+
usage: { output_tokens: outputTokens },
|
|
281
|
+
}));
|
|
282
|
+
events.push(anthropicSse('message_stop', { type: 'message_stop' }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return events.join('');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
anthropicToOpenAiChatRequest,
|
|
290
|
+
normalizeOpenAiBaseUrl,
|
|
291
|
+
openAiChatCompletionsUrl,
|
|
292
|
+
openAiStreamChunkToAnthropicEvents,
|
|
293
|
+
openAiToAnthropicMessage,
|
|
294
|
+
};
|
package/lib/panel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { buildClaudeEnv, PROVIDERS } = require('./config');
|
|
1
|
+
const { buildClaudeEnv, getModelFamily, PROVIDERS } = require('./config');
|
|
2
2
|
|
|
3
3
|
function apiStatusText(apiStatus) {
|
|
4
4
|
if (apiStatus === true) return 'API 正常';
|
|
@@ -39,6 +39,9 @@ function buildStatusView(config, options = {}) {
|
|
|
39
39
|
const desktopGatewayStatus = options.desktopGatewayStatus;
|
|
40
40
|
const desktopGatewayAutostartStatus = options.desktopGatewayAutostartStatus;
|
|
41
41
|
const desktopGatewayText = desktopGatewayStatusText(desktopGatewayStatus, desktopGatewayAutostartStatus);
|
|
42
|
+
const availableModelCount = Array.isArray(config.availableModels) ? config.availableModels.length : 0;
|
|
43
|
+
const mainFamily = getModelFamily(mainModel);
|
|
44
|
+
const fastFamily = getModelFamily(fastModel);
|
|
42
45
|
const warnings = [];
|
|
43
46
|
|
|
44
47
|
if (config.provider === 'deepseek' && (
|
|
@@ -48,11 +51,15 @@ function buildStatusView(config, options = {}) {
|
|
|
48
51
|
)) {
|
|
49
52
|
warnings.push('检测到旧 DeepSeek 模型名,建议运行 claw switch 更新到 [1m] 长上下文版本');
|
|
50
53
|
}
|
|
54
|
+
if (config.provider === 'custom' && mainFamily && fastFamily && mainFamily !== fastFamily) {
|
|
55
|
+
warnings.push(`快速模型跨系列:${mainFamily} → ${fastFamily}`);
|
|
56
|
+
}
|
|
51
57
|
|
|
52
58
|
const view = {
|
|
53
59
|
providerName,
|
|
54
60
|
mainModel,
|
|
55
61
|
fastModel,
|
|
62
|
+
availableModelCount,
|
|
56
63
|
envActive,
|
|
57
64
|
warnings,
|
|
58
65
|
lines: [
|
|
@@ -83,14 +90,17 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
83
90
|
];
|
|
84
91
|
|
|
85
92
|
if (view.envActive) {
|
|
86
|
-
lines.push('
|
|
93
|
+
lines.push('终端已生效');
|
|
87
94
|
} else if (options.platform === 'win32') {
|
|
88
|
-
lines.push('
|
|
95
|
+
lines.push('终端未生效:重新打开 PowerShell / CMD');
|
|
89
96
|
} else {
|
|
90
|
-
|
|
97
|
+
const shell = options.shell || process.env.SHELL || '';
|
|
98
|
+
const rcFile = shell.includes('bash') ? '~/.bashrc' : '~/.zshrc';
|
|
99
|
+
lines.push(`终端未生效:source ${rcFile} 或重开终端`);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
|
|
102
|
+
const modelCountText = view.availableModelCount ? ` · 可用 ${view.availableModelCount}` : '';
|
|
103
|
+
lines.push(`模型 ${view.mainModel} · 快速 ${view.fastModel}${modelCountText}`);
|
|
94
104
|
|
|
95
105
|
const desktopGatewayText = desktopGatewayStatusText(
|
|
96
106
|
view.desktopGatewayStatus || options.desktopGatewayStatus,
|
|
@@ -103,6 +113,10 @@ function buildMenuStatusLines(view, options = {}) {
|
|
|
103
113
|
if (view.warnings.some((warning) => warning.includes('旧 DeepSeek 模型名'))) {
|
|
104
114
|
lines.push('旧模型名:选择下方"切换厂商或模型"更新到 [1m]');
|
|
105
115
|
}
|
|
116
|
+
const crossFamilyWarning = view.warnings.find((warning) => warning.startsWith('快速模型跨系列'));
|
|
117
|
+
if (crossFamilyWarning) {
|
|
118
|
+
lines.push(crossFamilyWarning);
|
|
119
|
+
}
|
|
106
120
|
|
|
107
121
|
return lines;
|
|
108
122
|
}
|
package/lib/vscode.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { buildClaudeEnv, resolveFastModel, PROVIDERS, CLEAR_CLAUDE_ENV_KEYS } = require('./config');
|
|
5
|
+
const { isDesktopChatModel, buildDesktopGatewayRoutes, buildDesktopGatewayUrl } = require('./gateway');
|
|
6
|
+
|
|
7
|
+
const CLAUDE_CODE_SETTINGS_SCHEMA = 'https://json.schemastore.org/claude-code-settings.json';
|
|
8
|
+
const VSCODE_CLAUDE_OPEN_URI = 'vscode://anthropic.claude-code/open';
|
|
9
|
+
const VSCODE_MODEL_SETTINGS_KEYS = ['model', 'availableModels'];
|
|
10
|
+
|
|
11
|
+
function readJsonFile(file) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
14
|
+
} catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeJsonFile(file, value) {
|
|
20
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getClaudeCodeSettingsPath(options = {}) {
|
|
25
|
+
const homeDir = options.homeDir || os.homedir();
|
|
26
|
+
return options.settingsFile || path.join(homeDir, '.claude', 'settings.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getVsCodeUserSettingsPath(options = {}) {
|
|
30
|
+
if (options.vsCodeSettingsFile) return options.vsCodeSettingsFile;
|
|
31
|
+
|
|
32
|
+
const platform = options.platform || process.platform;
|
|
33
|
+
const homeDir = options.homeDir || os.homedir();
|
|
34
|
+
|
|
35
|
+
if (platform === 'darwin') {
|
|
36
|
+
return path.join(homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json');
|
|
37
|
+
}
|
|
38
|
+
if (platform === 'win32') {
|
|
39
|
+
const appData = options.appData || process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
40
|
+
return path.win32.join(appData, 'Code', 'User', 'settings.json');
|
|
41
|
+
}
|
|
42
|
+
return path.join(homeDir, '.config', 'Code', 'User', 'settings.json');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildClaudeCodeSettingsPatch(config) {
|
|
46
|
+
const availableModels = buildVsCodeAvailableModels(config);
|
|
47
|
+
return {
|
|
48
|
+
$schema: CLAUDE_CODE_SETTINGS_SCHEMA,
|
|
49
|
+
availableModels,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildVsCodeClaudeEnv(config) {
|
|
54
|
+
const { provider, model, fastModel, vscodeModel, vscodeFastModel } = config;
|
|
55
|
+
const selectedModel = vscodeModel || model;
|
|
56
|
+
const resolvedFastModel = vscodeFastModel || fastModel || resolveFastModel(PROVIDERS[provider], selectedModel);
|
|
57
|
+
const claudeEnv = buildClaudeEnv({ ...config, model: selectedModel, fastModel: resolvedFastModel });
|
|
58
|
+
const env = {
|
|
59
|
+
ANTHROPIC_BASE_URL: claudeEnv.ANTHROPIC_BASE_URL,
|
|
60
|
+
ANTHROPIC_AUTH_TOKEN: claudeEnv.ANTHROPIC_AUTH_TOKEN,
|
|
61
|
+
ANTHROPIC_MODEL: claudeEnv.ANTHROPIC_MODEL,
|
|
62
|
+
CLAUDE_CODE_SUBAGENT_MODEL: resolvedFastModel,
|
|
63
|
+
CLAUDE_CODE_EFFORT_LEVEL: 'max',
|
|
64
|
+
};
|
|
65
|
+
if (resolvedFastModel && resolvedFastModel !== selectedModel) {
|
|
66
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION = resolvedFastModel;
|
|
67
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME = resolvedFastModel;
|
|
68
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION = 'Custom fast model';
|
|
69
|
+
}
|
|
70
|
+
return env;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildVsCodeAvailableModels({ model, fastModel, availableModels, vscodeModel, vscodeFastModel }) {
|
|
74
|
+
const providerModels = Array.isArray(availableModels) && availableModels.length > 0
|
|
75
|
+
? availableModels
|
|
76
|
+
: [];
|
|
77
|
+
const models = [vscodeModel || model, vscodeFastModel || fastModel, model, fastModel, ...providerModels].filter(Boolean);
|
|
78
|
+
const chatModels = models.filter(isDesktopChatModel);
|
|
79
|
+
return [...new Set(chatModels.length > 0 ? chatModels : [model, fastModel].filter(Boolean))];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildVsCodeModelSelectionIds(config) {
|
|
83
|
+
return buildVsCodeAvailableModels(config);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeClaudeCodeSettings(config, options = {}) {
|
|
87
|
+
const file = getClaudeCodeSettingsPath(options);
|
|
88
|
+
const current = readJsonFile(file);
|
|
89
|
+
const patch = buildClaudeCodeSettingsPatch(config);
|
|
90
|
+
|
|
91
|
+
// Clean up any yingclaw env vars previously written to settings.json
|
|
92
|
+
const nextEnv = { ...(current.env && typeof current.env === 'object' ? current.env : {}) };
|
|
93
|
+
for (const key of CLEAR_CLAUDE_ENV_KEYS) {
|
|
94
|
+
delete nextEnv[key];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const next = {
|
|
98
|
+
...current,
|
|
99
|
+
$schema: current.$schema || patch.$schema,
|
|
100
|
+
availableModels: patch.availableModels,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Remove model from settings.json — it would also affect the terminal
|
|
104
|
+
delete next.model;
|
|
105
|
+
|
|
106
|
+
if (Object.keys(nextEnv).length > 0) {
|
|
107
|
+
next.env = nextEnv;
|
|
108
|
+
} else {
|
|
109
|
+
delete next.env;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
writeJsonFile(file, next);
|
|
113
|
+
try {
|
|
114
|
+
fs.chmodSync(file, 0o600);
|
|
115
|
+
} catch {}
|
|
116
|
+
return { result: 'updated', file };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildVsCodeExtensionEnvVars(config) {
|
|
120
|
+
const env = buildVsCodeClaudeEnv(config);
|
|
121
|
+
return Object.entries(env).map(([name, value]) => ({ name, value: String(value) }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeVsCodeExtensionSettings(config, options = {}) {
|
|
125
|
+
const file = getVsCodeUserSettingsPath(options);
|
|
126
|
+
const current = readJsonFile(file);
|
|
127
|
+
const next = {
|
|
128
|
+
...current,
|
|
129
|
+
'claudeCode.disableLoginPrompt': true,
|
|
130
|
+
'claudeCode.environmentVariables': buildVsCodeExtensionEnvVars(config),
|
|
131
|
+
};
|
|
132
|
+
if (options.useTerminal === true || options.useTerminal === false) {
|
|
133
|
+
next['claudeCode.useTerminal'] = options.useTerminal;
|
|
134
|
+
}
|
|
135
|
+
writeJsonFile(file, next);
|
|
136
|
+
return { result: 'updated', file };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function writeVsCodeIntegration(config, options = {}) {
|
|
140
|
+
const claudeSettings = writeClaudeCodeSettings(config, options);
|
|
141
|
+
const vscodeSettings = writeVsCodeExtensionSettings(config, options);
|
|
142
|
+
return {
|
|
143
|
+
result: 'updated',
|
|
144
|
+
files: [claudeSettings.file, vscodeSettings.file],
|
|
145
|
+
claudeSettings,
|
|
146
|
+
vscodeSettings,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function clearClaudeCodeSettingsEnv(options = {}) {
|
|
151
|
+
const file = getClaudeCodeSettingsPath(options);
|
|
152
|
+
if (!fs.existsSync(file)) return { result: 'missing', file };
|
|
153
|
+
|
|
154
|
+
const current = readJsonFile(file);
|
|
155
|
+
const nextEnv = { ...(current.env && typeof current.env === 'object' ? current.env : {}) };
|
|
156
|
+
let changed = false;
|
|
157
|
+
|
|
158
|
+
for (const key of CLEAR_CLAUDE_ENV_KEYS) {
|
|
159
|
+
if (Object.prototype.hasOwnProperty.call(nextEnv, key)) {
|
|
160
|
+
delete nextEnv[key];
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const key of VSCODE_MODEL_SETTINGS_KEYS) {
|
|
165
|
+
if (Object.prototype.hasOwnProperty.call(current, key)) {
|
|
166
|
+
changed = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!changed) return { result: 'missing', file };
|
|
171
|
+
|
|
172
|
+
const next = { ...current };
|
|
173
|
+
for (const key of VSCODE_MODEL_SETTINGS_KEYS) {
|
|
174
|
+
delete next[key];
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(nextEnv).length > 0) {
|
|
177
|
+
next.env = nextEnv;
|
|
178
|
+
} else {
|
|
179
|
+
delete next.env;
|
|
180
|
+
}
|
|
181
|
+
writeJsonFile(file, next);
|
|
182
|
+
return { result: 'updated', file };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function clearVsCodeExtensionSettings(options = {}) {
|
|
186
|
+
const file = getVsCodeUserSettingsPath(options);
|
|
187
|
+
if (!fs.existsSync(file)) return { result: 'missing', file };
|
|
188
|
+
|
|
189
|
+
const current = readJsonFile(file);
|
|
190
|
+
const next = { ...current };
|
|
191
|
+
let changed = false;
|
|
192
|
+
for (const key of ['claudeCode.disableLoginPrompt', 'claudeCode.useTerminal', 'claudeCode.environmentVariables']) {
|
|
193
|
+
if (Object.prototype.hasOwnProperty.call(next, key)) {
|
|
194
|
+
delete next[key];
|
|
195
|
+
changed = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!changed) return { result: 'missing', file };
|
|
199
|
+
|
|
200
|
+
writeJsonFile(file, next);
|
|
201
|
+
return { result: 'updated', file };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function clearVsCodeIntegration(options = {}) {
|
|
205
|
+
const claudeSettings = clearClaudeCodeSettingsEnv(options);
|
|
206
|
+
const vscodeSettings = clearVsCodeExtensionSettings(options);
|
|
207
|
+
const files = [claudeSettings, vscodeSettings]
|
|
208
|
+
.filter(result => result.result === 'updated')
|
|
209
|
+
.map(result => result.file);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
result: files.length > 0 ? 'updated' : 'missing',
|
|
213
|
+
files,
|
|
214
|
+
claudeSettings,
|
|
215
|
+
vscodeSettings,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function checkClaudeCodeSettingsEnv(config, options = {}) {
|
|
220
|
+
const settingsFile = getClaudeCodeSettingsPath(options);
|
|
221
|
+
const vsCodeFile = getVsCodeUserSettingsPath(options);
|
|
222
|
+
const settings = readJsonFile(settingsFile);
|
|
223
|
+
const vsCodeSettings = readJsonFile(vsCodeFile);
|
|
224
|
+
const missing = [];
|
|
225
|
+
|
|
226
|
+
// Check availableModels in settings.json
|
|
227
|
+
const expectedModels = buildVsCodeAvailableModels(config);
|
|
228
|
+
if (!Array.isArray(settings.availableModels) || settings.availableModels.join(',') !== expectedModels.join(',')) {
|
|
229
|
+
missing.push('availableModels');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check claudeCode.environmentVariables in VS Code extension settings
|
|
233
|
+
const expectedEnv = buildVsCodeClaudeEnv(config);
|
|
234
|
+
const actualEnvArray = vsCodeSettings['claudeCode.environmentVariables'];
|
|
235
|
+
if (!Array.isArray(actualEnvArray)) {
|
|
236
|
+
missing.push('claudeCode.environmentVariables');
|
|
237
|
+
} else {
|
|
238
|
+
const actualEnvMap = {};
|
|
239
|
+
for (const { name, value } of actualEnvArray) actualEnvMap[name] = value;
|
|
240
|
+
for (const [key, value] of Object.entries(expectedEnv)) {
|
|
241
|
+
if (actualEnvMap[key] !== String(value)) missing.push(key);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
file: settingsFile,
|
|
247
|
+
configured: missing.length === 0,
|
|
248
|
+
missing,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildVsCodeOpenCommand(platform = process.platform) {
|
|
253
|
+
if (platform === 'darwin') {
|
|
254
|
+
return { command: 'open', args: [VSCODE_CLAUDE_OPEN_URI] };
|
|
255
|
+
}
|
|
256
|
+
if (platform === 'win32') {
|
|
257
|
+
return { command: 'cmd', args: ['/c', 'start', '', VSCODE_CLAUDE_OPEN_URI] };
|
|
258
|
+
}
|
|
259
|
+
return { command: 'xdg-open', args: [VSCODE_CLAUDE_OPEN_URI] };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
CLAUDE_CODE_SETTINGS_SCHEMA,
|
|
264
|
+
VSCODE_CLAUDE_OPEN_URI,
|
|
265
|
+
buildClaudeCodeSettingsPatch,
|
|
266
|
+
buildVsCodeAvailableModels,
|
|
267
|
+
buildVsCodeClaudeEnv,
|
|
268
|
+
buildVsCodeExtensionEnvVars,
|
|
269
|
+
buildVsCodeModelSelectionIds,
|
|
270
|
+
buildVsCodeOpenCommand,
|
|
271
|
+
checkClaudeCodeSettingsEnv,
|
|
272
|
+
clearClaudeCodeSettingsEnv,
|
|
273
|
+
clearVsCodeExtensionSettings,
|
|
274
|
+
clearVsCodeIntegration,
|
|
275
|
+
getClaudeCodeSettingsPath,
|
|
276
|
+
getVsCodeUserSettingsPath,
|
|
277
|
+
writeClaudeCodeSettings,
|
|
278
|
+
writeVsCodeExtensionSettings,
|
|
279
|
+
writeVsCodeIntegration,
|
|
280
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yingclaw",
|
|
3
|
-
"version": "2.5.
|
|
4
|
-
"description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi
|
|
3
|
+
"version": "2.5.26",
|
|
4
|
+
"description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、火山方舟、Qwen、MiniMax、GLM、MiMo、自定义接口",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claw": "bin/cli.js"
|