markpdfdown 0.1.8-beta.6 → 0.2.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/dist/main/index.js +398 -14
- package/dist/preload/index.js +20 -1
- package/dist/renderer/assets/{index-yTU2skrN.css → index-B_JfRqwM.css} +54 -0
- package/dist/renderer/assets/{index-iEK8qT5k.js → index-wHB9i2kW.js} +623 -320
- package/dist/renderer/index.html +2 -2
- package/package.json +10 -3
- package/dist/app/app.js +0 -49
- package/dist/app/controllers/completionController.js +0 -19
- package/dist/app/controllers/modelController.js +0 -53
- package/dist/app/controllers/providerController.js +0 -120
- package/dist/app/dal/modelDal.js +0 -44
- package/dist/app/dal/providerDal.js +0 -78
- package/dist/app/db/index.js +0 -56
- package/dist/app/db/migration.js +0 -157
- package/dist/app/logic/llm/AnthropicClient.js +0 -219
- package/dist/app/logic/llm/AzureOpenAIClient.js +0 -239
- package/dist/app/logic/llm/GeminiClient.js +0 -212
- package/dist/app/logic/llm/LLMClient.js +0 -80
- package/dist/app/logic/llm/OpenAIClient.js +0 -235
- package/dist/app/logic/llm/example-advanced.js +0 -232
- package/dist/app/logic/llm/index.js +0 -14
- package/dist/app/logic/model.js +0 -27
- package/dist/app/middleware/logger.js +0 -23
- package/dist/app/routes/routes.js +0 -16
- package/dist/app/types/Provider.js +0 -1
- package/dist/server/controllers/FileController.js +0 -64
- package/dist/server/controllers/TaskController.js +0 -57
- package/dist/server/controllers/completionController.js +0 -64
- package/dist/server/controllers/modelController.js +0 -74
- package/dist/server/controllers/providerController.js +0 -120
- package/dist/server/dal/TaskDal.js +0 -67
- package/dist/server/dal/modelDal.js +0 -44
- package/dist/server/dal/providerDal.js +0 -83
- package/dist/server/db/index.js +0 -57
- package/dist/server/db/migration.js +0 -157
- package/dist/server/index.js +0 -49
- package/dist/server/logic/File.js +0 -34
- package/dist/server/logic/Task.js +0 -21
- package/dist/server/logic/llm/AnthropicClient.js +0 -220
- package/dist/server/logic/llm/AzureOpenAIClient.js +0 -239
- package/dist/server/logic/llm/GeminiClient.js +0 -213
- package/dist/server/logic/llm/LLMClient.js +0 -83
- package/dist/server/logic/llm/OllamaClient.js +0 -220
- package/dist/server/logic/llm/OpenAIClient.js +0 -235
- package/dist/server/logic/llm/example-advanced.js +0 -231
- package/dist/server/logic/llm/index.js +0 -15
- package/dist/server/logic/model.js +0 -59
- package/dist/server/middleware/logger.js +0 -23
- package/dist/server/routes/routes.js +0 -30
- package/dist/server/types/Provider.js +0 -1
- package/dist/server/types/Task.js +0 -1
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { LLMClient } from './LLMClient.js';
|
|
2
|
-
/**
|
|
3
|
-
* Anthropic客户端实现
|
|
4
|
-
*/
|
|
5
|
-
export class AnthropicClient extends LLMClient {
|
|
6
|
-
constructor(apiKey, baseUrl, apiVersion = '2023-06-01') {
|
|
7
|
-
super(apiKey, baseUrl || 'https://api.anthropic.com/v1');
|
|
8
|
-
this.apiVersion = apiVersion;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* 执行Anthropic文本补全
|
|
12
|
-
*/
|
|
13
|
-
async completion(options) {
|
|
14
|
-
try {
|
|
15
|
-
// 标准化选项,处理向后兼容
|
|
16
|
-
const normalizedOptions = this.normalizeOptions(options);
|
|
17
|
-
const endpoint = `${this.baseUrl}/messages`;
|
|
18
|
-
const modelName = normalizedOptions.model || 'claude-3-opus-20240229';
|
|
19
|
-
// 将消息转换为Anthropic格式
|
|
20
|
-
const anthropicMessages = this.convertMessagesToAnthropicFormat(normalizedOptions.messages);
|
|
21
|
-
const requestBody = {
|
|
22
|
-
model: modelName,
|
|
23
|
-
messages: anthropicMessages,
|
|
24
|
-
temperature: normalizedOptions.temperature ?? 0.7,
|
|
25
|
-
max_tokens: normalizedOptions.maxTokens,
|
|
26
|
-
stream: normalizedOptions.stream || false
|
|
27
|
-
};
|
|
28
|
-
// Anthropic支持System指令,而不是作为一个普通消息
|
|
29
|
-
const systemMessage = normalizedOptions.messages.find(msg => msg.role === 'system');
|
|
30
|
-
if (systemMessage) {
|
|
31
|
-
const systemContent = Array.isArray(systemMessage.content)
|
|
32
|
-
? systemMessage.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
33
|
-
: systemMessage.content.type === 'text' ? systemMessage.content.text : '';
|
|
34
|
-
if (systemContent) {
|
|
35
|
-
requestBody.system = systemContent;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
// Claude没有直接的JSON响应格式,但可以通过系统指令引导
|
|
39
|
-
if (normalizedOptions.response_format?.type === 'json_object' && !requestBody.system) {
|
|
40
|
-
requestBody.system = "请以有效的JSON格式提供响应。";
|
|
41
|
-
}
|
|
42
|
-
else if (normalizedOptions.response_format?.type === 'json_object' && requestBody.system) {
|
|
43
|
-
requestBody.system += "\n\n请以有效的JSON格式提供响应。";
|
|
44
|
-
}
|
|
45
|
-
const response = await fetch(endpoint, {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
headers: {
|
|
48
|
-
'Content-Type': 'application/json',
|
|
49
|
-
'x-api-key': normalizedOptions.apiKey || this.apiKey,
|
|
50
|
-
'anthropic-version': this.apiVersion
|
|
51
|
-
},
|
|
52
|
-
body: JSON.stringify(requestBody)
|
|
53
|
-
});
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
const error = await response.json();
|
|
56
|
-
throw new Error(`Anthropic API错误: ${error.error?.message || response.statusText}`);
|
|
57
|
-
}
|
|
58
|
-
if (normalizedOptions.stream && response.body && normalizedOptions.onUpdate) {
|
|
59
|
-
// 处理流式响应
|
|
60
|
-
const reader = response.body.getReader();
|
|
61
|
-
const decoder = new TextDecoder('utf-8');
|
|
62
|
-
let content = '';
|
|
63
|
-
const processStream = async () => {
|
|
64
|
-
const { done, value } = await reader.read();
|
|
65
|
-
if (done) {
|
|
66
|
-
return {
|
|
67
|
-
content,
|
|
68
|
-
model: modelName,
|
|
69
|
-
finishReason: 'stop',
|
|
70
|
-
responseFormat: normalizedOptions.response_format?.type
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
// 解析流式响应数据
|
|
74
|
-
const chunk = decoder.decode(value);
|
|
75
|
-
const lines = chunk
|
|
76
|
-
.split('\n')
|
|
77
|
-
.filter(line => line.trim() !== '' && line.trim() !== 'data: [DONE]');
|
|
78
|
-
for (const line of lines) {
|
|
79
|
-
if (line.startsWith('data: ')) {
|
|
80
|
-
try {
|
|
81
|
-
const data = JSON.parse(line.slice(6));
|
|
82
|
-
// Claude 3使用content_block_delta类型流式传输内容
|
|
83
|
-
if (data.type === 'content_block_delta' && data.delta?.text) {
|
|
84
|
-
content += data.delta.text;
|
|
85
|
-
if (normalizedOptions.onUpdate) {
|
|
86
|
-
normalizedOptions.onUpdate(content);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// 兼容Claude 2流式响应格式
|
|
90
|
-
if (data.completion) {
|
|
91
|
-
const newContent = data.completion;
|
|
92
|
-
content = newContent; // Claude 2返回完整内容,而不是增量
|
|
93
|
-
if (normalizedOptions.onUpdate) {
|
|
94
|
-
normalizedOptions.onUpdate(content);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
99
|
-
// 忽略解析错误
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return processStream();
|
|
104
|
-
};
|
|
105
|
-
return processStream();
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
// 处理普通响应
|
|
109
|
-
const data = await response.json();
|
|
110
|
-
// Claude API返回格式
|
|
111
|
-
let content = '';
|
|
112
|
-
// Claude 3格式
|
|
113
|
-
if (data.content) {
|
|
114
|
-
// 提取所有文本块
|
|
115
|
-
for (const block of data.content) {
|
|
116
|
-
if (block.type === 'text') {
|
|
117
|
-
content += block.text;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
// 兼容Claude 2格式
|
|
122
|
-
else if (data.completion) {
|
|
123
|
-
content = data.completion;
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
content,
|
|
127
|
-
model: data.model,
|
|
128
|
-
finishReason: data.stop_reason,
|
|
129
|
-
responseFormat: normalizedOptions.response_format?.type,
|
|
130
|
-
rawResponse: data // 保留原始响应以便调试
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
136
|
-
throw new Error(`Anthropic补全请求失败: ${errorMessage}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* 将消息转换为Anthropic API格式
|
|
141
|
-
* Claude API移除系统消息,这将在请求体中单独处理
|
|
142
|
-
*/
|
|
143
|
-
convertMessagesToAnthropicFormat(messages) {
|
|
144
|
-
return messages
|
|
145
|
-
.filter(msg => msg.role !== 'system') // 系统消息会在请求体中单独处理
|
|
146
|
-
.map(message => {
|
|
147
|
-
// Anthropic只支持user和assistant角色
|
|
148
|
-
let role = message.role === 'user' ? 'user' : 'assistant';
|
|
149
|
-
// 将tool角色视为user角色
|
|
150
|
-
if (message.role === 'tool') {
|
|
151
|
-
role = 'user';
|
|
152
|
-
}
|
|
153
|
-
const anthropicMessage = {
|
|
154
|
-
role
|
|
155
|
-
};
|
|
156
|
-
// 处理内容
|
|
157
|
-
if (Array.isArray(message.content)) {
|
|
158
|
-
anthropicMessage.content = this.convertContentArrayToAnthropicFormat(message.content);
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
anthropicMessage.content = this.convertContentToAnthropicFormat(message.content);
|
|
162
|
-
}
|
|
163
|
-
return anthropicMessage;
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* 将内容数组转换为Anthropic内容格式
|
|
168
|
-
*/
|
|
169
|
-
convertContentArrayToAnthropicFormat(contentArray) {
|
|
170
|
-
return contentArray.map(content => this.convertContentToAnthropicFormat(content)).flat();
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* 将单个内容转换为Anthropic内容格式
|
|
174
|
-
*/
|
|
175
|
-
convertContentToAnthropicFormat(content) {
|
|
176
|
-
switch (content.type) {
|
|
177
|
-
case 'text':
|
|
178
|
-
return [{
|
|
179
|
-
type: 'text',
|
|
180
|
-
text: content.text
|
|
181
|
-
}];
|
|
182
|
-
case 'image':
|
|
183
|
-
const imageContent = content;
|
|
184
|
-
let source;
|
|
185
|
-
if (imageContent.image_url.startsWith('data:')) {
|
|
186
|
-
// 处理Base64数据URI
|
|
187
|
-
const parts = imageContent.image_url.split(';base64,');
|
|
188
|
-
if (parts.length === 2) {
|
|
189
|
-
const mediaType = parts[0].replace('data:', '');
|
|
190
|
-
source = {
|
|
191
|
-
type: 'base64',
|
|
192
|
-
media_type: mediaType,
|
|
193
|
-
data: parts[1]
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
// 处理URL
|
|
199
|
-
source = {
|
|
200
|
-
type: 'url',
|
|
201
|
-
url: imageContent.image_url
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return [{
|
|
205
|
-
type: 'image',
|
|
206
|
-
source
|
|
207
|
-
}];
|
|
208
|
-
// Anthropic不支持工具调用和工具结果,将其转换为文本
|
|
209
|
-
case 'tool_call':
|
|
210
|
-
case 'tool_result':
|
|
211
|
-
return [{
|
|
212
|
-
type: 'text',
|
|
213
|
-
text: JSON.stringify(content)
|
|
214
|
-
}];
|
|
215
|
-
default:
|
|
216
|
-
throw new Error(`Anthropic不支持的内容类型: ${content.type}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { LLMClient } from './LLMClient.js';
|
|
2
|
-
/**
|
|
3
|
-
* Azure OpenAI客户端实现
|
|
4
|
-
*/
|
|
5
|
-
export class AzureOpenAIClient extends LLMClient {
|
|
6
|
-
constructor(apiKey, baseUrl, deploymentName = 'gpt-35-turbo', apiVersion = '2023-05-15') {
|
|
7
|
-
// Azure OpenAI需要完整的资源URL
|
|
8
|
-
super(apiKey, baseUrl);
|
|
9
|
-
this.deploymentName = deploymentName;
|
|
10
|
-
this.apiVersion = apiVersion;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* 执行Azure OpenAI文本补全
|
|
14
|
-
*/
|
|
15
|
-
async completion(options) {
|
|
16
|
-
try {
|
|
17
|
-
// 标准化选项,处理向后兼容
|
|
18
|
-
const normalizedOptions = this.normalizeOptions(options);
|
|
19
|
-
// Azure OpenAI端点格式
|
|
20
|
-
const endpoint = `${this.baseUrl}/openai/deployments/${normalizedOptions.model || this.deploymentName}/chat/completions?api-version=${this.apiVersion}`;
|
|
21
|
-
// 转换消息格式为OpenAI格式(Azure OpenAI API兼容OpenAI API)
|
|
22
|
-
const openaiMessages = this.convertMessagesToOpenAIFormat(normalizedOptions.messages);
|
|
23
|
-
const requestBody = {
|
|
24
|
-
messages: openaiMessages,
|
|
25
|
-
temperature: normalizedOptions.temperature ?? 0.7,
|
|
26
|
-
max_tokens: normalizedOptions.maxTokens,
|
|
27
|
-
stream: normalizedOptions.stream || false
|
|
28
|
-
};
|
|
29
|
-
// 添加工具配置(如果有)
|
|
30
|
-
if (normalizedOptions.tools && normalizedOptions.tools.length > 0) {
|
|
31
|
-
requestBody.tools = normalizedOptions.tools;
|
|
32
|
-
if (normalizedOptions.tool_choice) {
|
|
33
|
-
requestBody.tool_choice = normalizedOptions.tool_choice;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// 添加响应格式(如果指定)
|
|
37
|
-
if (normalizedOptions.response_format) {
|
|
38
|
-
requestBody.response_format = normalizedOptions.response_format;
|
|
39
|
-
}
|
|
40
|
-
const response = await fetch(endpoint, {
|
|
41
|
-
method: 'POST',
|
|
42
|
-
headers: {
|
|
43
|
-
'Content-Type': 'application/json',
|
|
44
|
-
'api-key': normalizedOptions.apiKey || this.apiKey
|
|
45
|
-
},
|
|
46
|
-
body: JSON.stringify(requestBody)
|
|
47
|
-
});
|
|
48
|
-
if (!response.ok) {
|
|
49
|
-
const error = await response.json();
|
|
50
|
-
throw new Error(`Azure OpenAI API错误: ${error.error?.message || response.statusText}`);
|
|
51
|
-
}
|
|
52
|
-
if (normalizedOptions.stream && response.body && normalizedOptions.onUpdate) {
|
|
53
|
-
// 处理流式响应
|
|
54
|
-
const reader = response.body.getReader();
|
|
55
|
-
const decoder = new TextDecoder('utf-8');
|
|
56
|
-
let content = '';
|
|
57
|
-
let toolCalls = [];
|
|
58
|
-
const processStream = async () => {
|
|
59
|
-
const { done, value } = await reader.read();
|
|
60
|
-
if (done) {
|
|
61
|
-
return {
|
|
62
|
-
content,
|
|
63
|
-
model: normalizedOptions.model || this.deploymentName,
|
|
64
|
-
finishReason: 'stop',
|
|
65
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
66
|
-
responseFormat: normalizedOptions.response_format?.type
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
// 解析流式响应数据
|
|
70
|
-
const chunk = decoder.decode(value);
|
|
71
|
-
const lines = chunk
|
|
72
|
-
.split('\n')
|
|
73
|
-
.filter(line => line.trim() !== '' && line.trim() !== 'data: [DONE]');
|
|
74
|
-
for (const line of lines) {
|
|
75
|
-
if (line.startsWith('data: ')) {
|
|
76
|
-
try {
|
|
77
|
-
const data = JSON.parse(line.slice(6));
|
|
78
|
-
// 处理常规文本内容
|
|
79
|
-
if (data.choices && data.choices[0]?.delta?.content) {
|
|
80
|
-
const newContent = data.choices[0].delta.content;
|
|
81
|
-
content += newContent;
|
|
82
|
-
if (normalizedOptions.onUpdate) {
|
|
83
|
-
normalizedOptions.onUpdate(content);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// 处理工具调用
|
|
87
|
-
if (data.choices && data.choices[0]?.delta?.tool_calls) {
|
|
88
|
-
const deltaToolCalls = data.choices[0].delta.tool_calls;
|
|
89
|
-
for (const deltaTool of deltaToolCalls) {
|
|
90
|
-
// 查找现有工具调用或创建新的
|
|
91
|
-
let toolCall = toolCalls.find(tc => tc.id === deltaTool.id);
|
|
92
|
-
if (!toolCall && deltaTool.id) {
|
|
93
|
-
toolCall = {
|
|
94
|
-
id: deltaTool.id,
|
|
95
|
-
type: 'function',
|
|
96
|
-
function: {
|
|
97
|
-
name: '',
|
|
98
|
-
arguments: ''
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
toolCalls.push(toolCall);
|
|
102
|
-
}
|
|
103
|
-
if (toolCall && deltaTool.function) {
|
|
104
|
-
if (deltaTool.function.name) {
|
|
105
|
-
toolCall.function.name = deltaTool.function.name;
|
|
106
|
-
}
|
|
107
|
-
if (deltaTool.function.arguments) {
|
|
108
|
-
toolCall.function.arguments += deltaTool.function.arguments;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// 如果有工具调用,同时更新内容
|
|
113
|
-
if (normalizedOptions.onUpdate) {
|
|
114
|
-
normalizedOptions.onUpdate(content);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch (e) {
|
|
119
|
-
// 忽略解析错误
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return processStream();
|
|
124
|
-
};
|
|
125
|
-
return processStream();
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
// 处理普通响应
|
|
129
|
-
const data = await response.json();
|
|
130
|
-
// 提取响应内容
|
|
131
|
-
let responseContent = '';
|
|
132
|
-
const toolCalls = [];
|
|
133
|
-
if (data.choices && data.choices[0]?.message) {
|
|
134
|
-
const message = data.choices[0].message;
|
|
135
|
-
// 提取文本内容
|
|
136
|
-
if (typeof message.content === 'string') {
|
|
137
|
-
responseContent = message.content;
|
|
138
|
-
}
|
|
139
|
-
// 提取工具调用
|
|
140
|
-
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
141
|
-
for (const toolCall of message.tool_calls) {
|
|
142
|
-
toolCalls.push({
|
|
143
|
-
id: toolCall.id,
|
|
144
|
-
type: toolCall.type,
|
|
145
|
-
function: {
|
|
146
|
-
name: toolCall.function.name,
|
|
147
|
-
arguments: toolCall.function.arguments
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
content: responseContent,
|
|
155
|
-
model: data.model || normalizedOptions.model || this.deploymentName,
|
|
156
|
-
finishReason: data.choices[0]?.finish_reason,
|
|
157
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
158
|
-
responseFormat: normalizedOptions.response_format?.type,
|
|
159
|
-
rawResponse: data // 保留原始响应以便调试
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
165
|
-
throw new Error(`Azure OpenAI补全请求失败: ${errorMessage}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* 将消息转换为OpenAI API格式
|
|
170
|
-
*/
|
|
171
|
-
convertMessagesToOpenAIFormat(messages) {
|
|
172
|
-
return messages.map(message => {
|
|
173
|
-
const openaiMessage = {
|
|
174
|
-
role: message.role
|
|
175
|
-
};
|
|
176
|
-
// 处理名称字段(如果存在)
|
|
177
|
-
if (message.name) {
|
|
178
|
-
openaiMessage.name = message.name;
|
|
179
|
-
}
|
|
180
|
-
// 处理内容
|
|
181
|
-
if (Array.isArray(message.content)) {
|
|
182
|
-
// 处理多部分内容
|
|
183
|
-
openaiMessage.content = message.content.map(content => this.convertContentToOpenAIFormat(content));
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
// 处理单一内容
|
|
187
|
-
const content = this.convertContentToOpenAIFormat(message.content);
|
|
188
|
-
// 如果是简单的文本内容,则直接使用字符串
|
|
189
|
-
if (content.type === 'text') {
|
|
190
|
-
openaiMessage.content = content.text;
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
openaiMessage.content = [content];
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return openaiMessage;
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* 将内容对象转换为OpenAI API格式
|
|
201
|
-
*/
|
|
202
|
-
convertContentToOpenAIFormat(content) {
|
|
203
|
-
switch (content.type) {
|
|
204
|
-
case 'text':
|
|
205
|
-
return {
|
|
206
|
-
type: 'text',
|
|
207
|
-
text: content.text
|
|
208
|
-
};
|
|
209
|
-
case 'image':
|
|
210
|
-
const imageContent = content;
|
|
211
|
-
return {
|
|
212
|
-
type: 'image_url',
|
|
213
|
-
image_url: {
|
|
214
|
-
url: imageContent.image_url,
|
|
215
|
-
detail: imageContent.detail || 'auto'
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
case 'tool_call':
|
|
219
|
-
const toolCallContent = content;
|
|
220
|
-
return {
|
|
221
|
-
type: 'tool_call',
|
|
222
|
-
tool_call_id: toolCallContent.tool_call_id,
|
|
223
|
-
function: {
|
|
224
|
-
name: toolCallContent.function.name,
|
|
225
|
-
arguments: toolCallContent.function.arguments
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
case 'tool_result':
|
|
229
|
-
const toolResultContent = content;
|
|
230
|
-
return {
|
|
231
|
-
type: 'tool_result',
|
|
232
|
-
tool_call_id: toolResultContent.tool_call_id,
|
|
233
|
-
content: toolResultContent.content
|
|
234
|
-
};
|
|
235
|
-
default:
|
|
236
|
-
throw new Error(`不支持的内容类型: ${content.type}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { LLMClient } from './LLMClient.js';
|
|
2
|
-
/**
|
|
3
|
-
* Google Gemini客户端实现
|
|
4
|
-
*/
|
|
5
|
-
export class GeminiClient extends LLMClient {
|
|
6
|
-
constructor(apiKey, baseUrl) {
|
|
7
|
-
super(apiKey, baseUrl || 'https://generativelanguage.googleapis.com/v1');
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* 执行Gemini文本补全
|
|
11
|
-
*/
|
|
12
|
-
async completion(options) {
|
|
13
|
-
try {
|
|
14
|
-
// 标准化选项,处理向后兼容
|
|
15
|
-
const normalizedOptions = this.normalizeOptions(options);
|
|
16
|
-
const modelName = normalizedOptions.model || 'gemini-1.5-pro';
|
|
17
|
-
const endpoint = `${this.baseUrl}/models/${modelName}:generateContent?key=${normalizedOptions.apiKey || this.apiKey}`;
|
|
18
|
-
// 将消息转换为Gemini格式
|
|
19
|
-
const geminiContents = this.convertMessagesToGeminiFormat(normalizedOptions.messages);
|
|
20
|
-
const requestBody = {
|
|
21
|
-
contents: geminiContents,
|
|
22
|
-
generationConfig: {
|
|
23
|
-
temperature: normalizedOptions.temperature ?? 0.7,
|
|
24
|
-
maxOutputTokens: normalizedOptions.maxTokens,
|
|
25
|
-
topP: 0.95
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
// 添加响应格式(如果指定)
|
|
29
|
-
if (normalizedOptions.response_format?.type === 'json_object') {
|
|
30
|
-
// Gemini使用不同的键来指定JSON输出
|
|
31
|
-
requestBody.generationConfig.response_mime_type = 'application/json';
|
|
32
|
-
}
|
|
33
|
-
const response = await fetch(endpoint, {
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: {
|
|
36
|
-
'Content-Type': 'application/json'
|
|
37
|
-
},
|
|
38
|
-
body: JSON.stringify(requestBody)
|
|
39
|
-
});
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
const error = await response.json();
|
|
42
|
-
throw new Error(`Gemini API错误: ${error.error?.message || response.statusText}`);
|
|
43
|
-
}
|
|
44
|
-
const data = await response.json();
|
|
45
|
-
// Gemini API返回格式与其他模型不同
|
|
46
|
-
if (data.candidates && data.candidates.length > 0) {
|
|
47
|
-
const candidate = data.candidates[0];
|
|
48
|
-
let content = '';
|
|
49
|
-
if (candidate.content && candidate.content.parts) {
|
|
50
|
-
for (const part of candidate.content.parts) {
|
|
51
|
-
if (part.text) {
|
|
52
|
-
content += part.text;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
content,
|
|
58
|
-
model: modelName,
|
|
59
|
-
finishReason: candidate.finishReason,
|
|
60
|
-
responseFormat: normalizedOptions.response_format?.type,
|
|
61
|
-
rawResponse: data // 保留原始响应以便调试
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
throw new Error('Gemini API返回格式错误');
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
68
|
-
throw new Error(`Gemini补全请求失败: ${errorMessage}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* 将消息转换为Gemini API格式
|
|
73
|
-
*/
|
|
74
|
-
convertMessagesToGeminiFormat(messages) {
|
|
75
|
-
const geminiContents = [];
|
|
76
|
-
let currentRole = null;
|
|
77
|
-
let currentContent = [];
|
|
78
|
-
// Gemini有不同的消息格式,需要相邻的相同角色消息合并
|
|
79
|
-
for (const message of messages) {
|
|
80
|
-
// 跳过System消息,因为Gemini不直接支持,稍后会特殊处理
|
|
81
|
-
if (message.role === 'system') {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
// 如果角色变化,创建新的内容块
|
|
85
|
-
const geminiRole = this.mapRoleToGemini(message.role);
|
|
86
|
-
if (currentRole !== geminiRole && currentContent.length > 0) {
|
|
87
|
-
geminiContents.push({
|
|
88
|
-
role: currentRole,
|
|
89
|
-
parts: currentContent
|
|
90
|
-
});
|
|
91
|
-
currentContent = [];
|
|
92
|
-
}
|
|
93
|
-
currentRole = geminiRole;
|
|
94
|
-
// 处理消息内容
|
|
95
|
-
if (Array.isArray(message.content)) {
|
|
96
|
-
// 处理多部分内容
|
|
97
|
-
for (const content of message.content) {
|
|
98
|
-
currentContent.push(this.convertContentToGeminiFormat(content));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
// 处理单一内容
|
|
103
|
-
currentContent.push(this.convertContentToGeminiFormat(message.content));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// 添加最后一组消息
|
|
107
|
-
if (currentRole && currentContent.length > 0) {
|
|
108
|
-
geminiContents.push({
|
|
109
|
-
role: currentRole,
|
|
110
|
-
parts: currentContent
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
// 特殊处理系统消息:将系统消息添加到第一个用户消息的前面
|
|
114
|
-
const systemMessages = messages.filter(msg => msg.role === 'system');
|
|
115
|
-
if (systemMessages.length > 0 && geminiContents.length > 0 && geminiContents[0].role === 'user') {
|
|
116
|
-
for (const sysMsg of systemMessages) {
|
|
117
|
-
const text = Array.isArray(sysMsg.content)
|
|
118
|
-
? sysMsg.content.map(c => c.type === 'text' ? c.text : '').join('\n')
|
|
119
|
-
: sysMsg.content.type === 'text' ? sysMsg.content.text : '';
|
|
120
|
-
if (text) {
|
|
121
|
-
// 在用户消息前添加系统指令
|
|
122
|
-
if (typeof geminiContents[0].parts[0] === 'object' && geminiContents[0].parts[0].text) {
|
|
123
|
-
geminiContents[0].parts[0].text = `[System Instructions]: ${text}\n\n${geminiContents[0].parts[0].text}`;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return geminiContents;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* 将内容对象转换为Gemini API格式
|
|
132
|
-
*/
|
|
133
|
-
convertContentToGeminiFormat(content) {
|
|
134
|
-
switch (content.type) {
|
|
135
|
-
case 'text':
|
|
136
|
-
return {
|
|
137
|
-
text: content.text
|
|
138
|
-
};
|
|
139
|
-
case 'image':
|
|
140
|
-
const imageContent = content;
|
|
141
|
-
return {
|
|
142
|
-
inline_data: {
|
|
143
|
-
mime_type: this.getMimeTypeFromUrl(imageContent.image_url),
|
|
144
|
-
data: this.extractBase64FromUrl(imageContent.image_url)
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
// Gemini不直接支持工具调用和工具结果
|
|
148
|
-
case 'tool_call':
|
|
149
|
-
case 'tool_result':
|
|
150
|
-
// 将工具相关内容转换为文本
|
|
151
|
-
return {
|
|
152
|
-
text: JSON.stringify(content)
|
|
153
|
-
};
|
|
154
|
-
default:
|
|
155
|
-
throw new Error(`Gemini不支持的内容类型: ${content.type}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* 从URL中提取MIME类型
|
|
160
|
-
*/
|
|
161
|
-
getMimeTypeFromUrl(url) {
|
|
162
|
-
if (url.startsWith('data:')) {
|
|
163
|
-
const mimeMatch = url.match(/^data:([^;]+);/);
|
|
164
|
-
return mimeMatch ? mimeMatch[1] : 'image/jpeg';
|
|
165
|
-
}
|
|
166
|
-
const extension = url.split('.').pop()?.toLowerCase();
|
|
167
|
-
switch (extension) {
|
|
168
|
-
case 'jpg':
|
|
169
|
-
case 'jpeg':
|
|
170
|
-
return 'image/jpeg';
|
|
171
|
-
case 'png':
|
|
172
|
-
return 'image/png';
|
|
173
|
-
case 'gif':
|
|
174
|
-
return 'image/gif';
|
|
175
|
-
case 'webp':
|
|
176
|
-
return 'image/webp';
|
|
177
|
-
default:
|
|
178
|
-
return 'image/jpeg';
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* 从数据URL中提取base64数据
|
|
183
|
-
*/
|
|
184
|
-
extractBase64FromUrl(url) {
|
|
185
|
-
if (url.startsWith('data:')) {
|
|
186
|
-
return url.split(',')[1];
|
|
187
|
-
}
|
|
188
|
-
// 对于非数据URL,需要先获取图像数据
|
|
189
|
-
// 在实际应用中,这里可能需要进行异步请求获取图像并转换为base64
|
|
190
|
-
// 这里简化处理,返回空字符串
|
|
191
|
-
return '';
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* 将角色映射到Gemini支持的角色
|
|
195
|
-
*/
|
|
196
|
-
mapRoleToGemini(role) {
|
|
197
|
-
switch (role) {
|
|
198
|
-
case 'user':
|
|
199
|
-
return 'user';
|
|
200
|
-
case 'assistant':
|
|
201
|
-
return 'model';
|
|
202
|
-
case 'system':
|
|
203
|
-
// Gemini不直接支持系统角色,将在处理中特殊处理
|
|
204
|
-
return 'user';
|
|
205
|
-
case 'tool':
|
|
206
|
-
// Gemini不直接支持工具角色,将其视为用户输入
|
|
207
|
-
return 'user';
|
|
208
|
-
default:
|
|
209
|
-
return 'user';
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|