mr-sliy 1.0.0
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/.env.example +145 -0
- package/database/schema.sql +187 -0
- package/package.json +74 -0
- package/scripts/download-tree-sitter.js +171 -0
- package/scripts/postinstall.js +134 -0
- package/src/agent/agent.js +563 -0
- package/src/agent.js +87 -0
- package/src/cli/index.js +1643 -0
- package/src/config/index.js +232 -0
- package/src/engine/dualModeEngine.js +486 -0
- package/src/index.js +165 -0
- package/src/middlewares/errorHandler.js +166 -0
- package/src/middlewares/index.js +23 -0
- package/src/routes/aiRoutes.js +117 -0
- package/src/routes/configRoutes.js +31 -0
- package/src/routes/index.js +75 -0
- package/src/routes/issueRoutes.js +195 -0
- package/src/routes/projectRoutes.js +46 -0
- package/src/routes/reportRoutes.js +40 -0
- package/src/routes/scanRoutes.js +245 -0
- package/src/routes/userRoutes.js +47 -0
- package/src/services/ast/parser.js +503 -0
- package/src/services/detection/detector.js +934 -0
- package/src/services/llm/providers.js +1107 -0
- package/src/services/rag/agent.js +375 -0
- package/src/services/vector/knowledgeBase.js +863 -0
- package/src/skills/Skill.js +38 -0
- package/src/skills/code-analysis/index.js +272 -0
- package/src/skills/code-detection/index.js +166 -0
- package/src/skills/code-detection/rules/console-log.js +45 -0
- package/src/skills/code-detection/rules/deep-nesting.js +76 -0
- package/src/skills/code-detection/rules/duplicate-code.js +57 -0
- package/src/skills/code-detection/rules/high-complexity.js +109 -0
- package/src/skills/code-detection/rules/index.js +59 -0
- package/src/skills/code-detection/rules/long-functions.js +54 -0
- package/src/skills/code-detection/rules/magic-numbers.js +48 -0
- package/src/skills/code-detection/rules/missing-comment.js +64 -0
- package/src/skills/code-detection/rules/null-check.js +71 -0
- package/src/skills/code-detection/rules/unnecessary-else.js +46 -0
- package/src/skills/code-detection/rules/unused-functions.js +57 -0
- package/src/skills/code-detection/rules/unused-imports.js +57 -0
- package/src/skills/code-detection/rules/unused-variables.js +54 -0
- package/src/skills/code-optimization/index.js +319 -0
- package/src/skills/index.js +152 -0
- package/src/utils/crypto.js +212 -0
- package/src/utils/database.js +125 -0
- package/src/utils/helpers.js +226 -0
- package/src/utils/logger.js +202 -0
- package/src/utils/mysql.js +198 -0
- package/src/utils/response.js +124 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 多LLM提供商管理模块
|
|
3
|
+
* 支持OpenAI、Claude、Ollama、Azure OpenAI等云端大模型
|
|
4
|
+
* 用户可自由切换和配置
|
|
5
|
+
* 支持配置持久化,记住用户的AI模型选择
|
|
6
|
+
* 配置文件使用AES-256-GCM加密存储
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { logger } = require('../../utils/logger');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const { encryptToFile, decryptFromFile, hash } = require('../../utils/crypto');
|
|
14
|
+
|
|
15
|
+
// 本地配置持久化(加密存储)
|
|
16
|
+
const CONFIG_FILE = path.join(__dirname, '../../../data/provider-config.enc');
|
|
17
|
+
|
|
18
|
+
// 获取机器唯一标识(用于区分不同电脑)
|
|
19
|
+
function getMachineId() {
|
|
20
|
+
const hostname = os.hostname();
|
|
21
|
+
const username = os.userInfo().username;
|
|
22
|
+
const platform = os.platform();
|
|
23
|
+
return hash(`${hostname}-${username}-${platform}`).substring(0, 8);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 加载保存的配置(从加密文件读取)
|
|
27
|
+
function loadSavedConfig() {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
30
|
+
const config = decryptFromFile(CONFIG_FILE);
|
|
31
|
+
const currentMachineId = getMachineId();
|
|
32
|
+
|
|
33
|
+
if (config && config.machineId === currentMachineId) {
|
|
34
|
+
logger.debug(`加载保存的提供商配置: ${config.activeProvider || '无'}`);
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.debug(`加载提供商配置失败: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 保存配置(加密存储)
|
|
45
|
+
function saveConfig(activeProvider, providerConfigs = {}) {
|
|
46
|
+
try {
|
|
47
|
+
const configDir = path.dirname(CONFIG_FILE);
|
|
48
|
+
if (!fs.existsSync(configDir)) {
|
|
49
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const config = {
|
|
53
|
+
machineId: getMachineId(),
|
|
54
|
+
activeProvider,
|
|
55
|
+
providerConfigs,
|
|
56
|
+
savedAt: new Date().toISOString()
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
encryptToFile(CONFIG_FILE, config);
|
|
60
|
+
logger.debug(`保存提供商配置: ${activeProvider}`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.debug(`保存提供商配置失败: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* LLM提供商基类
|
|
68
|
+
*/
|
|
69
|
+
class LLMProvider {
|
|
70
|
+
constructor(name, config) {
|
|
71
|
+
this.name = name;
|
|
72
|
+
this.config = config;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async chat(messages, options = {}) {
|
|
76
|
+
throw new Error('子类必须实现chat方法');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async optimizeCode(codeSnippet, context, options = {}) {
|
|
80
|
+
const prompt = this.buildOptimizationPrompt(codeSnippet, context);
|
|
81
|
+
const messages = [
|
|
82
|
+
{ role: 'system', content: '你是一个专业的代码优化专家,擅长代码重构、性能优化和最佳实践建议。' },
|
|
83
|
+
{ role: 'user', content: prompt }
|
|
84
|
+
];
|
|
85
|
+
return this.chat(messages, options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
buildOptimizationPrompt(codeSnippet, context) {
|
|
89
|
+
return `请分析以下代码片段并提供优化建议。
|
|
90
|
+
|
|
91
|
+
代码语言: ${context.language || '未知'}
|
|
92
|
+
问题类型: ${context.issueType || '一般优化'}
|
|
93
|
+
问题描述: ${context.message || ''}
|
|
94
|
+
|
|
95
|
+
原始代码:
|
|
96
|
+
\`\`\`${context.language || ''}
|
|
97
|
+
${codeSnippet}
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
请提供以下内容:
|
|
101
|
+
1. 优化后的代码(完整可运行的代码)
|
|
102
|
+
2. 优化说明(为什么这样优化,解决了什么问题)
|
|
103
|
+
3. 最佳实践建议(通用的编码建议)
|
|
104
|
+
|
|
105
|
+
请以JSON格式返回:
|
|
106
|
+
{
|
|
107
|
+
"optimizedCode": "优化后的完整代码",
|
|
108
|
+
"explanation": "优化说明",
|
|
109
|
+
"suggestions": ["建议1", "建议2"]
|
|
110
|
+
}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isAvailable() {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* OpenAI提供商
|
|
120
|
+
*/
|
|
121
|
+
class OpenAIProvider extends LLMProvider {
|
|
122
|
+
constructor(config) {
|
|
123
|
+
super('openai', config);
|
|
124
|
+
this.baseURL = config.baseURL || 'https://api.openai.com/v1';
|
|
125
|
+
this.apiKey = config.apiKey;
|
|
126
|
+
this.model = config.model || 'gpt-4';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isAvailable() {
|
|
130
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async chat(messages, options = {}) {
|
|
134
|
+
if (!this.isAvailable()) {
|
|
135
|
+
throw new Error('OpenAI API Key未配置');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const url = `${this.baseURL}/chat/completions`;
|
|
139
|
+
const body = {
|
|
140
|
+
model: options.model || this.model,
|
|
141
|
+
messages,
|
|
142
|
+
temperature: options.temperature ?? 0.7,
|
|
143
|
+
max_tokens: options.maxTokens || 2000,
|
|
144
|
+
top_p: options.topP || 1,
|
|
145
|
+
stream: options.stream || false
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(url, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify(body)
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const errorData = await response.json().catch(() => ({}));
|
|
160
|
+
throw new Error(`OpenAI API错误: ${errorData.error?.message || response.statusText}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const data = await response.json();
|
|
164
|
+
const content = data.choices[0]?.message?.content || '';
|
|
165
|
+
|
|
166
|
+
// 尝试解析JSON
|
|
167
|
+
let parsed = null;
|
|
168
|
+
try {
|
|
169
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
170
|
+
if (jsonMatch) {
|
|
171
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// JSON解析失败,返回原始内容
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
content: parsed || content,
|
|
179
|
+
rawContent: content,
|
|
180
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
181
|
+
model: data.model
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.error('OpenAI调用失败:', error);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Claude (Anthropic) 提供商
|
|
192
|
+
*/
|
|
193
|
+
class ClaudeProvider extends LLMProvider {
|
|
194
|
+
constructor(config) {
|
|
195
|
+
super('claude', config);
|
|
196
|
+
this.baseURL = config.baseURL || 'https://api.anthropic.com/v1';
|
|
197
|
+
this.apiKey = config.apiKey;
|
|
198
|
+
this.model = config.model || 'claude-3-sonnet-20240229';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
isAvailable() {
|
|
202
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async chat(messages, options = {}) {
|
|
206
|
+
if (!this.isAvailable()) {
|
|
207
|
+
throw new Error('Claude API Key未配置');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 转换消息格式
|
|
211
|
+
const systemMessage = messages.find(m => m.role === 'system')?.content || '';
|
|
212
|
+
const userMessages = messages.filter(m => m.role !== 'system');
|
|
213
|
+
|
|
214
|
+
const url = `${this.baseURL}/messages`;
|
|
215
|
+
const body = {
|
|
216
|
+
model: options.model || this.model,
|
|
217
|
+
max_tokens: options.maxTokens || 2000,
|
|
218
|
+
temperature: options.temperature ?? 0.7,
|
|
219
|
+
system: systemMessage,
|
|
220
|
+
messages: userMessages.map(m => ({
|
|
221
|
+
role: m.role === 'user' ? 'user' : 'assistant',
|
|
222
|
+
content: m.content
|
|
223
|
+
}))
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch(url, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
'x-api-key': this.apiKey,
|
|
232
|
+
'anthropic-version': '2023-06-01'
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(body)
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const errorData = await response.json().catch(() => ({}));
|
|
239
|
+
throw new Error(`Claude API错误: ${errorData.error?.message || response.statusText}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const data = await response.json();
|
|
243
|
+
const content = data.content?.[0]?.text || '';
|
|
244
|
+
|
|
245
|
+
let parsed = null;
|
|
246
|
+
try {
|
|
247
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
248
|
+
if (jsonMatch) {
|
|
249
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
content: parsed || content,
|
|
255
|
+
rawContent: content,
|
|
256
|
+
tokensUsed: data.usage?.input_tokens + data.usage?.output_tokens || 0,
|
|
257
|
+
model: data.model
|
|
258
|
+
};
|
|
259
|
+
} catch (error) {
|
|
260
|
+
logger.error('Claude调用失败:', error);
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Ollama (本地大模型) 提供商
|
|
268
|
+
*/
|
|
269
|
+
class OllamaProvider extends LLMProvider {
|
|
270
|
+
constructor(config) {
|
|
271
|
+
super('ollama', config);
|
|
272
|
+
this.baseURL = config.baseURL || 'http://localhost:11434';
|
|
273
|
+
this.model = config.model || 'codellama';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
isAvailable() {
|
|
277
|
+
// Ollama不需要API Key,但需要本地服务运行
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async chat(messages, options = {}) {
|
|
282
|
+
const url = `${this.baseURL}/api/chat`;
|
|
283
|
+
const body = {
|
|
284
|
+
model: options.model || this.model,
|
|
285
|
+
messages,
|
|
286
|
+
stream: false,
|
|
287
|
+
options: {
|
|
288
|
+
temperature: options.temperature ?? 0.7,
|
|
289
|
+
num_predict: options.maxTokens || 2000
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetch(url, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify(body)
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
throw new Error(`Ollama服务错误: ${response.statusText}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const data = await response.json();
|
|
305
|
+
const content = data.message?.content || '';
|
|
306
|
+
|
|
307
|
+
let parsed = null;
|
|
308
|
+
try {
|
|
309
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
310
|
+
if (jsonMatch) {
|
|
311
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
content: parsed || content,
|
|
317
|
+
rawContent: content,
|
|
318
|
+
tokensUsed: data.eval_count || 0,
|
|
319
|
+
model: data.model
|
|
320
|
+
};
|
|
321
|
+
} catch (error) {
|
|
322
|
+
logger.error('Ollama调用失败:', error);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Google Gemini 提供商
|
|
330
|
+
*/
|
|
331
|
+
class GeminiProvider extends LLMProvider {
|
|
332
|
+
constructor(config) {
|
|
333
|
+
super('gemini', config);
|
|
334
|
+
this.baseURL = config.baseURL || 'https://generativelanguage.googleapis.com/v1beta';
|
|
335
|
+
this.apiKey = config.apiKey;
|
|
336
|
+
this.model = config.model || 'gemini-1.5-pro';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
isAvailable() {
|
|
340
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async chat(messages, options = {}) {
|
|
344
|
+
if (!this.isAvailable()) {
|
|
345
|
+
throw new Error('Gemini API Key未配置');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const url = `${this.baseURL}/models/${options.model || this.model}:generateContent`;
|
|
349
|
+
|
|
350
|
+
const contents = messages.map(m => ({
|
|
351
|
+
role: m.role === 'assistant' ? 'model' : m.role,
|
|
352
|
+
parts: [{ text: m.content }]
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
const body = {
|
|
356
|
+
contents,
|
|
357
|
+
generationConfig: {
|
|
358
|
+
temperature: options.temperature ?? 0.7,
|
|
359
|
+
maxOutputTokens: options.maxTokens || 2000,
|
|
360
|
+
topP: options.topP || 1
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const response = await fetch(`${url}?key=${this.apiKey}`, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: { 'Content-Type': 'application/json' },
|
|
368
|
+
body: JSON.stringify(body)
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
const errorData = await response.json().catch(() => ({}));
|
|
373
|
+
throw new Error(`Gemini API错误: ${errorData.error?.message || response.statusText}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const data = await response.json();
|
|
377
|
+
const content = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
378
|
+
|
|
379
|
+
let parsed = null;
|
|
380
|
+
try {
|
|
381
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
382
|
+
if (jsonMatch) {
|
|
383
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
384
|
+
}
|
|
385
|
+
} catch (e) {}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
content: parsed || content,
|
|
389
|
+
rawContent: content,
|
|
390
|
+
tokensUsed: data.usageMetadata?.totalTokenCount || 0,
|
|
391
|
+
model: data.model
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
logger.error('Gemini调用失败:', error);
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 阿里通义千问 提供商
|
|
402
|
+
*/
|
|
403
|
+
class TongyiProvider extends LLMProvider {
|
|
404
|
+
constructor(config) {
|
|
405
|
+
super('tongyi', config);
|
|
406
|
+
this.baseURL = config.baseURL || 'https://dashscope.aliyuncs.com/api/v1';
|
|
407
|
+
this.apiKey = config.apiKey;
|
|
408
|
+
this.model = config.model || 'qwen-plus';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
isAvailable() {
|
|
412
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async chat(messages, options = {}) {
|
|
416
|
+
if (!this.isAvailable()) {
|
|
417
|
+
throw new Error('通义千问 API Key未配置');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const url = `${this.baseURL}/services/aigc/text-generation/generation`;
|
|
421
|
+
const body = {
|
|
422
|
+
model: options.model || this.model,
|
|
423
|
+
input: {
|
|
424
|
+
messages
|
|
425
|
+
},
|
|
426
|
+
parameters: {
|
|
427
|
+
temperature: options.temperature ?? 0.7,
|
|
428
|
+
max_tokens: options.maxTokens || 2000
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const response = await fetch(url, {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: {
|
|
436
|
+
'Content-Type': 'application/json',
|
|
437
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
438
|
+
},
|
|
439
|
+
body: JSON.stringify(body)
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
const errorData = await response.json().catch(() => ({}));
|
|
444
|
+
throw new Error(`通义千问错误: ${errorData.message || response.statusText}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const data = await response.json();
|
|
448
|
+
const content = data.output?.text || '';
|
|
449
|
+
|
|
450
|
+
let parsed = null;
|
|
451
|
+
try {
|
|
452
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
453
|
+
if (jsonMatch) {
|
|
454
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
455
|
+
}
|
|
456
|
+
} catch (e) {}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
content: parsed || content,
|
|
460
|
+
rawContent: content,
|
|
461
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
462
|
+
model: data.model
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
logger.error('通义千问调用失败:', error);
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 字节豆包 提供商
|
|
473
|
+
*/
|
|
474
|
+
class DoubaoProvider extends LLMProvider {
|
|
475
|
+
constructor(config) {
|
|
476
|
+
super('doubao', config);
|
|
477
|
+
this.baseURL = config.baseURL || 'https://api.doubao.com/v1';
|
|
478
|
+
this.apiKey = config.apiKey;
|
|
479
|
+
this.model = config.model || 'Doubao-7B';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
isAvailable() {
|
|
483
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async chat(messages, options = {}) {
|
|
487
|
+
if (!this.isAvailable()) {
|
|
488
|
+
throw new Error('豆包 API Key未配置');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const url = `${this.baseURL}/chat/completions`;
|
|
492
|
+
const body = {
|
|
493
|
+
model: options.model || this.model,
|
|
494
|
+
messages,
|
|
495
|
+
temperature: options.temperature ?? 0.7,
|
|
496
|
+
max_tokens: options.maxTokens || 2000
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const response = await fetch(url, {
|
|
501
|
+
method: 'POST',
|
|
502
|
+
headers: {
|
|
503
|
+
'Content-Type': 'application/json',
|
|
504
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
505
|
+
},
|
|
506
|
+
body: JSON.stringify(body)
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (!response.ok) {
|
|
510
|
+
const errorData = await response.json().catch(() => ({}));
|
|
511
|
+
throw new Error(`豆包 API错误: ${errorData.error?.message || response.statusText}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const data = await response.json();
|
|
515
|
+
const content = data.choices[0]?.message?.content || '';
|
|
516
|
+
|
|
517
|
+
let parsed = null;
|
|
518
|
+
try {
|
|
519
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
520
|
+
if (jsonMatch) {
|
|
521
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
522
|
+
}
|
|
523
|
+
} catch (e) {}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
content: parsed || content,
|
|
527
|
+
rawContent: content,
|
|
528
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
529
|
+
model: data.model
|
|
530
|
+
};
|
|
531
|
+
} catch (error) {
|
|
532
|
+
logger.error('豆包调用失败:', error);
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* 百度文心一言 提供商
|
|
540
|
+
*/
|
|
541
|
+
class WenxinProvider extends LLMProvider {
|
|
542
|
+
constructor(config) {
|
|
543
|
+
super('wenxin', config);
|
|
544
|
+
this.baseURL = config.baseURL || 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat';
|
|
545
|
+
this.apiKey = config.apiKey;
|
|
546
|
+
this.secretKey = config.secretKey;
|
|
547
|
+
this.accessToken = null;
|
|
548
|
+
this.tokenExpireTime = 0;
|
|
549
|
+
this.model = config.model || 'ernie-3.5';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
isAvailable() {
|
|
553
|
+
return !!this.apiKey && !!this.secretKey;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async getAccessToken() {
|
|
557
|
+
if (this.accessToken && Date.now() < this.tokenExpireTime) {
|
|
558
|
+
return this.accessToken;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${this.apiKey}&client_secret=${this.secretKey}`;
|
|
562
|
+
const response = await fetch(url);
|
|
563
|
+
const data = await response.json();
|
|
564
|
+
|
|
565
|
+
this.accessToken = data.access_token;
|
|
566
|
+
this.tokenExpireTime = Date.now() + (data.expires_in - 60) * 1000;
|
|
567
|
+
|
|
568
|
+
return this.accessToken;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async chat(messages, options = {}) {
|
|
572
|
+
if (!this.isAvailable()) {
|
|
573
|
+
throw new Error('文心一言 API Key或Secret Key未配置');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const accessToken = await this.getAccessToken();
|
|
577
|
+
const url = `${this.baseURL}/${options.model || this.model}?access_token=${accessToken}`;
|
|
578
|
+
|
|
579
|
+
const body = {
|
|
580
|
+
messages,
|
|
581
|
+
temperature: options.temperature ?? 0.7,
|
|
582
|
+
max_tokens: options.maxTokens || 2000
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const response = await fetch(url, {
|
|
587
|
+
method: 'POST',
|
|
588
|
+
headers: { 'Content-Type': 'application/json' },
|
|
589
|
+
body: JSON.stringify(body)
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (!response.ok) {
|
|
593
|
+
const errorData = await response.json().catch(() => ({}));
|
|
594
|
+
throw new Error(`文心一言错误: ${errorData.error_msg || response.statusText}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const data = await response.json();
|
|
598
|
+
const content = data.result || '';
|
|
599
|
+
|
|
600
|
+
let parsed = null;
|
|
601
|
+
try {
|
|
602
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
603
|
+
if (jsonMatch) {
|
|
604
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
605
|
+
}
|
|
606
|
+
} catch (e) {}
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
content: parsed || content,
|
|
610
|
+
rawContent: content,
|
|
611
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
612
|
+
model: options.model || this.model
|
|
613
|
+
};
|
|
614
|
+
} catch (error) {
|
|
615
|
+
logger.error('文心一言调用失败:', error);
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Azure OpenAI 提供商
|
|
623
|
+
*/
|
|
624
|
+
class AzureOpenAIProvider extends LLMProvider {
|
|
625
|
+
constructor(config) {
|
|
626
|
+
super('azure', config);
|
|
627
|
+
this.endpoint = config.endpoint;
|
|
628
|
+
this.apiKey = config.apiKey;
|
|
629
|
+
this.deploymentName = config.deploymentName || 'gpt-4';
|
|
630
|
+
this.apiVersion = config.apiVersion || '2024-02-01';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
isAvailable() {
|
|
634
|
+
return !!this.apiKey && !!this.endpoint;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async chat(messages, options = {}) {
|
|
638
|
+
if (!this.isAvailable()) {
|
|
639
|
+
throw new Error('Azure OpenAI配置不完整');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const url = `${this.endpoint}/openai/deployments/${this.deploymentName}/chat/completions?api-version=${this.apiVersion}`;
|
|
643
|
+
const body = {
|
|
644
|
+
messages,
|
|
645
|
+
temperature: options.temperature ?? 0.7,
|
|
646
|
+
max_tokens: options.maxTokens || 2000
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const response = await fetch(url, {
|
|
651
|
+
method: 'POST',
|
|
652
|
+
headers: {
|
|
653
|
+
'Content-Type': 'application/json',
|
|
654
|
+
'api-key': this.apiKey
|
|
655
|
+
},
|
|
656
|
+
body: JSON.stringify(body)
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (!response.ok) {
|
|
660
|
+
const errorData = await response.json().catch(() => ({}));
|
|
661
|
+
throw new Error(`Azure OpenAI错误: ${errorData.error?.message || response.statusText}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const data = await response.json();
|
|
665
|
+
const content = data.choices[0]?.message?.content || '';
|
|
666
|
+
|
|
667
|
+
let parsed = null;
|
|
668
|
+
try {
|
|
669
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*\}/);
|
|
670
|
+
if (jsonMatch) {
|
|
671
|
+
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
672
|
+
}
|
|
673
|
+
} catch (e) {}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
content: parsed || content,
|
|
677
|
+
rawContent: content,
|
|
678
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
679
|
+
model: data.model
|
|
680
|
+
};
|
|
681
|
+
} catch (error) {
|
|
682
|
+
logger.error('Azure OpenAI调用失败:', error);
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* DeepSeek 提供商
|
|
690
|
+
*/
|
|
691
|
+
class DeepSeekProvider extends LLMProvider {
|
|
692
|
+
constructor(config) {
|
|
693
|
+
super('deepseek', config);
|
|
694
|
+
this.baseURL = config.baseURL || 'https://api.deepseek.com/v1';
|
|
695
|
+
this.apiKey = config.apiKey;
|
|
696
|
+
this.model = config.model || 'deepseek-chat';
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
isAvailable() {
|
|
700
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async chat(messages, options = {}) {
|
|
704
|
+
if (!this.isAvailable()) {
|
|
705
|
+
throw new Error('DeepSeek API Key未配置');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const url = `${this.baseURL}/chat/completions`;
|
|
709
|
+
const body = {
|
|
710
|
+
model: options.model || this.model,
|
|
711
|
+
messages,
|
|
712
|
+
temperature: options.temperature ?? 0.7,
|
|
713
|
+
max_tokens: options.maxTokens || 2000,
|
|
714
|
+
top_p: options.topP || 1,
|
|
715
|
+
stream: options.stream || false
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
const response = await fetch(url, {
|
|
720
|
+
method: 'POST',
|
|
721
|
+
headers: {
|
|
722
|
+
'Content-Type': 'application/json',
|
|
723
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
724
|
+
},
|
|
725
|
+
body: JSON.stringify(body)
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
const errorData = await response.json().catch(() => ({}));
|
|
730
|
+
throw new Error(`DeepSeek错误: ${errorData.error?.message || response.statusText}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const data = await response.json();
|
|
734
|
+
const content = data.choices[0]?.message?.content || '';
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
content,
|
|
738
|
+
rawContent: content,
|
|
739
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
740
|
+
model: data.model
|
|
741
|
+
};
|
|
742
|
+
} catch (error) {
|
|
743
|
+
logger.error('DeepSeek调用失败:', error);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* 智谱AI (ChatGLM) 提供商
|
|
751
|
+
*/
|
|
752
|
+
class ZhipuProvider extends LLMProvider {
|
|
753
|
+
constructor(config) {
|
|
754
|
+
super('zhipu', config);
|
|
755
|
+
this.baseURL = config.baseURL || 'https://open.bigmodel.cn/api/paas/v4';
|
|
756
|
+
this.apiKey = config.apiKey;
|
|
757
|
+
this.model = config.model || 'glm-4';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
isAvailable() {
|
|
761
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async chat(messages, options = {}) {
|
|
765
|
+
if (!this.isAvailable()) {
|
|
766
|
+
throw new Error('智谱AI API Key未配置');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const url = `${this.baseURL}/chat/completions`;
|
|
770
|
+
const body = {
|
|
771
|
+
model: options.model || this.model,
|
|
772
|
+
messages,
|
|
773
|
+
temperature: options.temperature ?? 0.7,
|
|
774
|
+
max_tokens: options.maxTokens || 2000,
|
|
775
|
+
top_p: options.topP || 1,
|
|
776
|
+
stream: options.stream || false
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const response = await fetch(url, {
|
|
781
|
+
method: 'POST',
|
|
782
|
+
headers: {
|
|
783
|
+
'Content-Type': 'application/json',
|
|
784
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
785
|
+
},
|
|
786
|
+
body: JSON.stringify(body)
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
if (!response.ok) {
|
|
790
|
+
const errorData = await response.json().catch(() => ({}));
|
|
791
|
+
throw new Error(`智谱AI错误: ${errorData.error?.message || response.statusText}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const data = await response.json();
|
|
795
|
+
const content = data.choices[0]?.message?.content || '';
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
content,
|
|
799
|
+
rawContent: content,
|
|
800
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
801
|
+
model: data.model
|
|
802
|
+
};
|
|
803
|
+
} catch (error) {
|
|
804
|
+
logger.error('智谱AI调用失败:', error);
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Moonshot AI (Kimi) 提供商
|
|
812
|
+
*/
|
|
813
|
+
class MoonshotProvider extends LLMProvider {
|
|
814
|
+
constructor(config) {
|
|
815
|
+
super('moonshot', config);
|
|
816
|
+
this.baseURL = config.baseURL || 'https://api.moonshot.cn/v1';
|
|
817
|
+
this.apiKey = config.apiKey;
|
|
818
|
+
this.model = config.model || 'moonshot-v1-8k';
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
isAvailable() {
|
|
822
|
+
return !!this.apiKey && this.apiKey.length > 0;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async chat(messages, options = {}) {
|
|
826
|
+
if (!this.isAvailable()) {
|
|
827
|
+
throw new Error('Moonshot API Key未配置');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const url = `${this.baseURL}/chat/completions`;
|
|
831
|
+
const body = {
|
|
832
|
+
model: options.model || this.model,
|
|
833
|
+
messages,
|
|
834
|
+
temperature: options.temperature ?? 0.7,
|
|
835
|
+
max_tokens: options.maxTokens || 2000,
|
|
836
|
+
top_p: options.topP || 1,
|
|
837
|
+
stream: options.stream || false
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
try {
|
|
841
|
+
const response = await fetch(url, {
|
|
842
|
+
method: 'POST',
|
|
843
|
+
headers: {
|
|
844
|
+
'Content-Type': 'application/json',
|
|
845
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
846
|
+
},
|
|
847
|
+
body: JSON.stringify(body)
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
if (!response.ok) {
|
|
851
|
+
const errorData = await response.json().catch(() => ({}));
|
|
852
|
+
throw new Error(`Moonshot错误: ${errorData.error?.message || response.statusText}`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const data = await response.json();
|
|
856
|
+
const content = data.choices[0]?.message?.content || '';
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
content,
|
|
860
|
+
rawContent: content,
|
|
861
|
+
tokensUsed: data.usage?.total_tokens || 0,
|
|
862
|
+
model: data.model
|
|
863
|
+
};
|
|
864
|
+
} catch (error) {
|
|
865
|
+
logger.error('Moonshot调用失败:', error);
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* LLM提供商管理器
|
|
873
|
+
*/
|
|
874
|
+
class LLMProviderManager {
|
|
875
|
+
constructor() {
|
|
876
|
+
this.providers = new Map();
|
|
877
|
+
this.activeProvider = null;
|
|
878
|
+
|
|
879
|
+
// 尝试加载保存的配置
|
|
880
|
+
this.loadSavedConfig();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* 加载保存的配置
|
|
885
|
+
*/
|
|
886
|
+
loadSavedConfig() {
|
|
887
|
+
const savedConfig = loadSavedConfig();
|
|
888
|
+
if (savedConfig && savedConfig.activeProvider) {
|
|
889
|
+
// 延迟应用配置,确保提供商已注册
|
|
890
|
+
setTimeout(() => {
|
|
891
|
+
this.applySavedConfig(savedConfig);
|
|
892
|
+
}, 0);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* 应用保存的配置
|
|
898
|
+
*/
|
|
899
|
+
applySavedConfig(savedConfig) {
|
|
900
|
+
// 更新提供商配置
|
|
901
|
+
if (savedConfig.providerConfigs) {
|
|
902
|
+
Object.entries(savedConfig.providerConfigs).forEach(([name, config]) => {
|
|
903
|
+
this.updateProviderConfig(name, config);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// 尝试切换到保存的提供商
|
|
908
|
+
if (savedConfig.activeProvider) {
|
|
909
|
+
try {
|
|
910
|
+
const provider = this.providers.get(savedConfig.activeProvider.toLowerCase());
|
|
911
|
+
if (provider && provider.isAvailable()) {
|
|
912
|
+
this.activeProvider = provider;
|
|
913
|
+
logger.info(`已恢复上次的提供商: ${savedConfig.activeProvider}`);
|
|
914
|
+
}
|
|
915
|
+
} catch (error) {
|
|
916
|
+
logger.debug(`恢复提供商失败: ${error.message}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* 保存当前配置
|
|
923
|
+
*/
|
|
924
|
+
saveCurrentConfig() {
|
|
925
|
+
const providerConfigs = {};
|
|
926
|
+
this.providers.forEach((provider, name) => {
|
|
927
|
+
const config = {};
|
|
928
|
+
if (provider.apiKey) config.apiKey = provider.apiKey;
|
|
929
|
+
if (provider.baseURL) config.baseURL = provider.baseURL;
|
|
930
|
+
if (provider.model) config.model = provider.model;
|
|
931
|
+
if (provider.endpoint) config.endpoint = provider.endpoint;
|
|
932
|
+
if (provider.deploymentName) config.deploymentName = provider.deploymentName;
|
|
933
|
+
if (provider.secretKey) config.secretKey = provider.secretKey;
|
|
934
|
+
if (provider.apiVersion) config.apiVersion = provider.apiVersion;
|
|
935
|
+
providerConfigs[name] = config;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
saveConfig(
|
|
939
|
+
this.activeProvider?.name || null,
|
|
940
|
+
providerConfigs
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* 注册提供商
|
|
946
|
+
*/
|
|
947
|
+
register(name, config) {
|
|
948
|
+
let provider;
|
|
949
|
+
switch (name.toLowerCase()) {
|
|
950
|
+
case 'openai':
|
|
951
|
+
provider = new OpenAIProvider(config);
|
|
952
|
+
break;
|
|
953
|
+
case 'claude':
|
|
954
|
+
case 'anthropic':
|
|
955
|
+
provider = new ClaudeProvider(config);
|
|
956
|
+
break;
|
|
957
|
+
case 'ollama':
|
|
958
|
+
provider = new OllamaProvider(config);
|
|
959
|
+
break;
|
|
960
|
+
case 'azure':
|
|
961
|
+
case 'azureopenai':
|
|
962
|
+
provider = new AzureOpenAIProvider(config);
|
|
963
|
+
break;
|
|
964
|
+
case 'gemini':
|
|
965
|
+
case 'google':
|
|
966
|
+
provider = new GeminiProvider(config);
|
|
967
|
+
break;
|
|
968
|
+
case 'tongyi':
|
|
969
|
+
case 'qwen':
|
|
970
|
+
provider = new TongyiProvider(config);
|
|
971
|
+
break;
|
|
972
|
+
case 'doubao':
|
|
973
|
+
case 'bytedance':
|
|
974
|
+
provider = new DoubaoProvider(config);
|
|
975
|
+
break;
|
|
976
|
+
case 'wenxin':
|
|
977
|
+
case 'ernie':
|
|
978
|
+
case 'baidu':
|
|
979
|
+
provider = new WenxinProvider(config);
|
|
980
|
+
break;
|
|
981
|
+
case 'deepseek':
|
|
982
|
+
provider = new DeepSeekProvider(config);
|
|
983
|
+
break;
|
|
984
|
+
case 'zhipu':
|
|
985
|
+
case 'chatglm':
|
|
986
|
+
case 'glm':
|
|
987
|
+
provider = new ZhipuProvider(config);
|
|
988
|
+
break;
|
|
989
|
+
case 'moonshot':
|
|
990
|
+
case 'kimi':
|
|
991
|
+
provider = new MoonshotProvider(config);
|
|
992
|
+
break;
|
|
993
|
+
default:
|
|
994
|
+
throw new Error(`不支持的提供商: ${name}`);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
this.providers.set(name.toLowerCase(), provider);
|
|
998
|
+
logger.info(`注册LLM提供商: ${name}`);
|
|
999
|
+
return provider;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* 设置活跃提供商
|
|
1004
|
+
*/
|
|
1005
|
+
setActiveProvider(name) {
|
|
1006
|
+
const provider = this.providers.get(name.toLowerCase());
|
|
1007
|
+
if (!provider) {
|
|
1008
|
+
throw new Error(`未找到提供商: ${name}`);
|
|
1009
|
+
}
|
|
1010
|
+
if (!provider.isAvailable()) {
|
|
1011
|
+
throw new Error(`提供商 ${name} 不可用,请检查配置`);
|
|
1012
|
+
}
|
|
1013
|
+
this.activeProvider = provider;
|
|
1014
|
+
logger.info(`切换活跃LLM提供商: ${name}`);
|
|
1015
|
+
|
|
1016
|
+
// 保存当前配置
|
|
1017
|
+
this.saveCurrentConfig();
|
|
1018
|
+
|
|
1019
|
+
return provider;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* 获取活跃提供商
|
|
1024
|
+
*/
|
|
1025
|
+
getActiveProvider() {
|
|
1026
|
+
return this.activeProvider;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* 获取所有可用提供商
|
|
1031
|
+
*/
|
|
1032
|
+
getAvailableProviders() {
|
|
1033
|
+
const available = [];
|
|
1034
|
+
this.providers.forEach((provider, name) => {
|
|
1035
|
+
available.push({
|
|
1036
|
+
name,
|
|
1037
|
+
available: provider.isAvailable(),
|
|
1038
|
+
model: provider.model || provider.deploymentName
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
return available;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* 获取所有已注册提供商
|
|
1046
|
+
*/
|
|
1047
|
+
getAllProviders() {
|
|
1048
|
+
return Array.from(this.providers.keys());
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* 使用活跃提供商发送请求
|
|
1053
|
+
*/
|
|
1054
|
+
async chat(messages, options = {}) {
|
|
1055
|
+
if (!this.activeProvider) {
|
|
1056
|
+
throw new Error('未设置活跃LLM提供商');
|
|
1057
|
+
}
|
|
1058
|
+
return this.activeProvider.chat(messages, options);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* 使用活跃提供商优化代码
|
|
1063
|
+
*/
|
|
1064
|
+
async optimizeCode(codeSnippet, context, options = {}) {
|
|
1065
|
+
if (!this.activeProvider) {
|
|
1066
|
+
throw new Error('未设置活跃LLM提供商');
|
|
1067
|
+
}
|
|
1068
|
+
return this.activeProvider.optimizeCode(codeSnippet, context, options);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* 更新提供商配置
|
|
1073
|
+
*/
|
|
1074
|
+
updateProviderConfig(name, config) {
|
|
1075
|
+
const provider = this.providers.get(name.toLowerCase());
|
|
1076
|
+
if (provider) {
|
|
1077
|
+
Object.assign(provider.config, config);
|
|
1078
|
+
if (config.apiKey) provider.apiKey = config.apiKey;
|
|
1079
|
+
if (config.baseURL) provider.baseURL = config.baseURL;
|
|
1080
|
+
if (config.model) provider.model = config.model;
|
|
1081
|
+
if (config.endpoint) provider.endpoint = config.endpoint;
|
|
1082
|
+
if (config.deploymentName) provider.deploymentName = config.deploymentName;
|
|
1083
|
+
if (config.secretKey) provider.secretKey = config.secretKey;
|
|
1084
|
+
if (config.apiVersion) provider.apiVersion = config.apiVersion;
|
|
1085
|
+
logger.info(`更新提供商配置: ${name}`);
|
|
1086
|
+
|
|
1087
|
+
// 保存当前配置
|
|
1088
|
+
this.saveCurrentConfig();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// 单例实例
|
|
1094
|
+
const providerManager = new LLMProviderManager();
|
|
1095
|
+
|
|
1096
|
+
module.exports = {
|
|
1097
|
+
LLMProviderManager,
|
|
1098
|
+
OpenAIProvider,
|
|
1099
|
+
ClaudeProvider,
|
|
1100
|
+
OllamaProvider,
|
|
1101
|
+
AzureOpenAIProvider,
|
|
1102
|
+
GeminiProvider,
|
|
1103
|
+
TongyiProvider,
|
|
1104
|
+
DoubaoProvider,
|
|
1105
|
+
WenxinProvider,
|
|
1106
|
+
providerManager
|
|
1107
|
+
};
|