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/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('环境变量未生效:重新打开 PowerShell / CMD');
95
+ lines.push('终端未生效:重新打开 PowerShell / CMD');
89
96
  } else {
90
- lines.push('环境变量未生效:运行 source ~/.zshrc 或重开终端');
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
- lines.push(`主模型 ${view.mainModel} · 快速模型 ${view.fastModel}`);
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.20",
4
- "description": "Claude Code × 国产大模型一键接入:DeepSeek、KimiQwen、MiniMax、GLM、MiMo",
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"