markpdfdown 0.1.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/LICENSE +201 -0
- package/README.md +128 -0
- package/bin/cli.js +130 -0
- package/dist/main/AnthropicClient-CTbHYiqm.js +193 -0
- package/dist/main/GeminiClient-CrtYbwaF.js +196 -0
- package/dist/main/OllamaClient-DKJsnvIt.js +197 -0
- package/dist/main/OpenAIClient-gyy2nFkw.js +214 -0
- package/dist/main/OpenAIResponsesClient-DETYz2nL.js +297 -0
- package/dist/main/index.js +3523 -0
- package/dist/preload/index.js +102 -0
- package/dist/renderer/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/renderer/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/renderer/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/renderer/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/renderer/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/renderer/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/renderer/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/renderer/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/renderer/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/renderer/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/renderer/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/renderer/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/renderer/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/renderer/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/renderer/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/renderer/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/renderer/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/renderer/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/renderer/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/renderer/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/renderer/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/renderer/assets/MarkPDFdown-C6Sb1T4M.png +0 -0
- package/dist/renderer/assets/index-CbMlWqbh.css +327 -0
- package/dist/renderer/assets/index-DeDe7lry.js +123956 -0
- package/dist/renderer/index.html +14 -0
- package/package.json +156 -0
- package/src/core/infrastructure/db/migrations/20250414154412_/migration.sql +24 -0
- package/src/core/infrastructure/db/migrations/20250419090345_/migration.sql +29 -0
- package/src/core/infrastructure/db/migrations/20250419104636_/migration.sql +47 -0
- package/src/core/infrastructure/db/migrations/20260121154536_add_worker_fields/migration.sql +50 -0
- package/src/core/infrastructure/db/migrations/20260124014806_/migration.sql +55 -0
- package/src/core/infrastructure/db/migrations/migration_lock.toml +3 -0
- package/src/core/infrastructure/db/schema.prisma +104 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { L as LLMClient } from "./index.js";
|
|
2
|
+
class GeminiClient extends LLMClient {
|
|
3
|
+
constructor(apiKey, baseUrl) {
|
|
4
|
+
super(apiKey, baseUrl || "https://generativelanguage.googleapis.com/v1beta/models");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 执行Gemini文本补全
|
|
8
|
+
*/
|
|
9
|
+
async completion(options) {
|
|
10
|
+
try {
|
|
11
|
+
const normalizedOptions = this.normalizeOptions(options);
|
|
12
|
+
const modelName = normalizedOptions.model || "gemini-1.5-pro";
|
|
13
|
+
const endpoint = `${this.baseUrl}/${modelName}:generateContent`;
|
|
14
|
+
const geminiContents = this.convertMessagesToGeminiFormat(normalizedOptions.messages);
|
|
15
|
+
const requestBody = {
|
|
16
|
+
contents: geminiContents,
|
|
17
|
+
generationConfig: {
|
|
18
|
+
temperature: normalizedOptions.temperature ?? 0.7,
|
|
19
|
+
maxOutputTokens: normalizedOptions.maxTokens,
|
|
20
|
+
topP: 0.95
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
if (normalizedOptions.response_format?.type === "json_object") {
|
|
24
|
+
requestBody.generationConfig.response_mime_type = "application/json";
|
|
25
|
+
}
|
|
26
|
+
const response = await fetch(endpoint, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"x-goog-api-key": normalizedOptions.apiKey || this.apiKey,
|
|
31
|
+
"X-Title": "MarkPDFdown",
|
|
32
|
+
"HTTP-Referer": "https://github.com/MarkPDFdown"
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(requestBody)
|
|
35
|
+
});
|
|
36
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] POST ${endpoint} (model: ${modelName}) ${response.status} - ${response.statusText}`);
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = await response.json();
|
|
39
|
+
throw new Error(`Gemini API错误: ${error.error?.message || response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
if (data.candidates && data.candidates.length > 0) {
|
|
43
|
+
const candidate = data.candidates[0];
|
|
44
|
+
let content = "";
|
|
45
|
+
if (candidate.content && candidate.content.parts) {
|
|
46
|
+
for (const part of candidate.content.parts) {
|
|
47
|
+
if (part.text) {
|
|
48
|
+
content += part.text;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
content,
|
|
54
|
+
model: modelName,
|
|
55
|
+
finishReason: candidate.finishReason,
|
|
56
|
+
responseFormat: normalizedOptions.response_format?.type,
|
|
57
|
+
rawResponse: data
|
|
58
|
+
// 保留原始响应以便调试
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
throw new Error("Gemini API返回格式错误");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
+
throw new Error(`Gemini补全请求失败: ${errorMessage}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 将消息转换为Gemini API格式
|
|
69
|
+
*/
|
|
70
|
+
convertMessagesToGeminiFormat(messages) {
|
|
71
|
+
const geminiContents = [];
|
|
72
|
+
let currentRole = null;
|
|
73
|
+
let currentContent = [];
|
|
74
|
+
for (const message of messages) {
|
|
75
|
+
if (message.role === "system") {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const geminiRole = this.mapRoleToGemini(message.role);
|
|
79
|
+
if (currentRole !== geminiRole && currentContent.length > 0) {
|
|
80
|
+
geminiContents.push({
|
|
81
|
+
role: currentRole,
|
|
82
|
+
parts: currentContent
|
|
83
|
+
});
|
|
84
|
+
currentContent = [];
|
|
85
|
+
}
|
|
86
|
+
currentRole = geminiRole;
|
|
87
|
+
if (Array.isArray(message.content)) {
|
|
88
|
+
for (const content of message.content) {
|
|
89
|
+
currentContent.push(this.convertContentToGeminiFormat(content));
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
currentContent.push(this.convertContentToGeminiFormat(message.content));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (currentRole && currentContent.length > 0) {
|
|
96
|
+
geminiContents.push({
|
|
97
|
+
role: currentRole,
|
|
98
|
+
parts: currentContent
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const systemMessages = messages.filter((msg) => msg.role === "system");
|
|
102
|
+
if (systemMessages.length > 0 && geminiContents.length > 0 && geminiContents[0].role === "user") {
|
|
103
|
+
for (const sysMsg of systemMessages) {
|
|
104
|
+
const text = Array.isArray(sysMsg.content) ? sysMsg.content.map((c) => c.type === "text" ? c.text : "").join("\n") : sysMsg.content.type === "text" ? sysMsg.content.text : "";
|
|
105
|
+
if (text) {
|
|
106
|
+
if (typeof geminiContents[0].parts[0] === "object" && geminiContents[0].parts[0].text) {
|
|
107
|
+
geminiContents[0].parts[0].text = `[System Instructions]: ${text}
|
|
108
|
+
|
|
109
|
+
${geminiContents[0].parts[0].text}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return geminiContents;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 将内容对象转换为Gemini API格式
|
|
118
|
+
*/
|
|
119
|
+
convertContentToGeminiFormat(content) {
|
|
120
|
+
switch (content.type) {
|
|
121
|
+
case "text":
|
|
122
|
+
return {
|
|
123
|
+
text: content.text
|
|
124
|
+
};
|
|
125
|
+
case "image_url": {
|
|
126
|
+
const imageContent = content;
|
|
127
|
+
return {
|
|
128
|
+
inline_data: {
|
|
129
|
+
mime_type: this.getMimeTypeFromUrl(imageContent.image_url.url),
|
|
130
|
+
data: this.extractBase64FromUrl(imageContent.image_url.url)
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Gemini不直接支持工具调用和工具结果
|
|
135
|
+
case "tool_call":
|
|
136
|
+
case "tool_result":
|
|
137
|
+
return {
|
|
138
|
+
text: JSON.stringify(content)
|
|
139
|
+
};
|
|
140
|
+
default:
|
|
141
|
+
throw new Error(`Gemini不支持的内容类型: ${content.type}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 从URL中提取MIME类型
|
|
146
|
+
*/
|
|
147
|
+
getMimeTypeFromUrl(url) {
|
|
148
|
+
if (url.startsWith("data:")) {
|
|
149
|
+
const mimeMatch = url.match(/^data:([^;]+);/);
|
|
150
|
+
return mimeMatch ? mimeMatch[1] : "image/jpeg";
|
|
151
|
+
}
|
|
152
|
+
const extension = url.split(".").pop()?.toLowerCase();
|
|
153
|
+
switch (extension) {
|
|
154
|
+
case "jpg":
|
|
155
|
+
case "jpeg":
|
|
156
|
+
return "image/jpeg";
|
|
157
|
+
case "png":
|
|
158
|
+
return "image/png";
|
|
159
|
+
case "gif":
|
|
160
|
+
return "image/gif";
|
|
161
|
+
case "webp":
|
|
162
|
+
return "image/webp";
|
|
163
|
+
default:
|
|
164
|
+
return "image/jpeg";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 从数据URL中提取base64数据
|
|
169
|
+
*/
|
|
170
|
+
extractBase64FromUrl(url) {
|
|
171
|
+
if (url.startsWith("data:")) {
|
|
172
|
+
return url.split(",")[1];
|
|
173
|
+
}
|
|
174
|
+
return "";
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 将角色映射到Gemini支持的角色
|
|
178
|
+
*/
|
|
179
|
+
mapRoleToGemini(role) {
|
|
180
|
+
switch (role) {
|
|
181
|
+
case "user":
|
|
182
|
+
return "user";
|
|
183
|
+
case "assistant":
|
|
184
|
+
return "model";
|
|
185
|
+
case "system":
|
|
186
|
+
return "user";
|
|
187
|
+
case "tool":
|
|
188
|
+
return "user";
|
|
189
|
+
default:
|
|
190
|
+
return "user";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export {
|
|
195
|
+
GeminiClient
|
|
196
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { L as LLMClient } from "./index.js";
|
|
2
|
+
class OllamaClient extends LLMClient {
|
|
3
|
+
constructor(apiKey, baseUrl) {
|
|
4
|
+
super(apiKey, baseUrl || "http://localhost:11434/api");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 执行Ollama文本补全
|
|
8
|
+
*/
|
|
9
|
+
async completion(options) {
|
|
10
|
+
try {
|
|
11
|
+
const normalizedOptions = this.normalizeOptions(options);
|
|
12
|
+
const ollamaMessages = this.convertMessagesToOllamaFormat(normalizedOptions.messages);
|
|
13
|
+
const requestBody = {
|
|
14
|
+
model: normalizedOptions.model || "llama3",
|
|
15
|
+
messages: ollamaMessages,
|
|
16
|
+
stream: normalizedOptions.stream !== false,
|
|
17
|
+
// 默认为流式响应
|
|
18
|
+
options: {}
|
|
19
|
+
};
|
|
20
|
+
if (normalizedOptions.temperature !== void 0) {
|
|
21
|
+
requestBody.options.temperature = normalizedOptions.temperature;
|
|
22
|
+
}
|
|
23
|
+
if (normalizedOptions.maxTokens !== void 0) {
|
|
24
|
+
requestBody.options.num_predict = normalizedOptions.maxTokens;
|
|
25
|
+
}
|
|
26
|
+
if (normalizedOptions.tools && normalizedOptions.tools.length > 0) {
|
|
27
|
+
requestBody.tools = normalizedOptions.tools;
|
|
28
|
+
}
|
|
29
|
+
if (normalizedOptions.response_format?.type === "json_object") {
|
|
30
|
+
requestBody.format = "json";
|
|
31
|
+
}
|
|
32
|
+
const response = await fetch(`${this.baseUrl}`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"X-Title": "MarkPDFdown",
|
|
37
|
+
"HTTP-Referer": "https://github.com/MarkPDFdown"
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(requestBody)
|
|
40
|
+
});
|
|
41
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] POST ${this.baseUrl} (model: ${requestBody.model}) ${response.status} - ${response.statusText}`);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const error = await response.json();
|
|
44
|
+
throw new Error(`Ollama API错误: ${error.error || response.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
if (normalizedOptions.stream && response.body && normalizedOptions.onUpdate) {
|
|
47
|
+
const reader = response.body.getReader();
|
|
48
|
+
const decoder = new TextDecoder("utf-8");
|
|
49
|
+
let content = "";
|
|
50
|
+
let model = "";
|
|
51
|
+
const processStream = async () => {
|
|
52
|
+
const { done, value } = await reader.read();
|
|
53
|
+
if (done) {
|
|
54
|
+
return {
|
|
55
|
+
content,
|
|
56
|
+
model: model || normalizedOptions.model || "llama3",
|
|
57
|
+
finishReason: "stop",
|
|
58
|
+
responseFormat: normalizedOptions.response_format?.type
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const chunk = decoder.decode(value);
|
|
62
|
+
const lines = chunk.split("\n").filter((line) => line.trim() !== "");
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(line);
|
|
66
|
+
if (data.model && !model) {
|
|
67
|
+
model = data.model;
|
|
68
|
+
}
|
|
69
|
+
if (data.message && data.message.content) {
|
|
70
|
+
if (!data.done) {
|
|
71
|
+
content += data.message.content;
|
|
72
|
+
if (normalizedOptions.onUpdate) {
|
|
73
|
+
normalizedOptions.onUpdate(content);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (data.done === true) {
|
|
78
|
+
return {
|
|
79
|
+
content,
|
|
80
|
+
model: data.model || normalizedOptions.model || "llama3",
|
|
81
|
+
finishReason: "stop",
|
|
82
|
+
responseFormat: normalizedOptions.response_format?.type,
|
|
83
|
+
rawResponse: data
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return processStream();
|
|
90
|
+
};
|
|
91
|
+
return processStream();
|
|
92
|
+
} else {
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
return {
|
|
95
|
+
content: data.message?.content || "",
|
|
96
|
+
model: data.model,
|
|
97
|
+
finishReason: data.done ? "stop" : void 0,
|
|
98
|
+
responseFormat: normalizedOptions.response_format?.type,
|
|
99
|
+
rawResponse: data
|
|
100
|
+
// 保留原始响应以便调试
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
105
|
+
throw new Error(`Ollama补全请求失败: ${errorMessage}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 将消息转换为Ollama API格式
|
|
110
|
+
*/
|
|
111
|
+
convertMessagesToOllamaFormat(messages) {
|
|
112
|
+
return messages.map((message) => {
|
|
113
|
+
const ollamaMessage = {
|
|
114
|
+
role: message.role
|
|
115
|
+
};
|
|
116
|
+
if (Array.isArray(message.content)) {
|
|
117
|
+
ollamaMessage.content = this.convertContentArrayToOllamaFormat(message.content);
|
|
118
|
+
} else {
|
|
119
|
+
ollamaMessage.content = this.convertContentToOllamaFormat(message.content);
|
|
120
|
+
}
|
|
121
|
+
const images = this.extractImages(message.content);
|
|
122
|
+
if (images.length > 0) {
|
|
123
|
+
ollamaMessage.images = images;
|
|
124
|
+
}
|
|
125
|
+
return ollamaMessage;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 提取消息中的图片
|
|
130
|
+
*/
|
|
131
|
+
extractImages(content) {
|
|
132
|
+
const images = [];
|
|
133
|
+
if (Array.isArray(content)) {
|
|
134
|
+
for (const item of content) {
|
|
135
|
+
if (item.type === "image_url") {
|
|
136
|
+
const imageContent = item;
|
|
137
|
+
images.push(this.processImageUrl(imageContent.image_url.url));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else if (content.type === "image_url") {
|
|
141
|
+
const imageContent = content;
|
|
142
|
+
images.push(this.processImageUrl(imageContent.image_url.url));
|
|
143
|
+
}
|
|
144
|
+
return images;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 处理图片 URL,去掉 data URI 前缀
|
|
148
|
+
*/
|
|
149
|
+
processImageUrl(url) {
|
|
150
|
+
if (url.startsWith("data:")) {
|
|
151
|
+
const parts = url.split(";base64,");
|
|
152
|
+
if (parts.length === 2) {
|
|
153
|
+
return parts[1];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return url;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 将内容数组转换为Ollama内容格式
|
|
160
|
+
*/
|
|
161
|
+
convertContentArrayToOllamaFormat(contentArray) {
|
|
162
|
+
return contentArray.map((content) => this.convertContentToOllamaFormat(content)).join("\n");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 将单个内容转换为Ollama内容格式
|
|
166
|
+
*/
|
|
167
|
+
convertContentToOllamaFormat(content) {
|
|
168
|
+
switch (content.type) {
|
|
169
|
+
case "text":
|
|
170
|
+
return content.text;
|
|
171
|
+
case "image_url":
|
|
172
|
+
return "";
|
|
173
|
+
case "tool_call": {
|
|
174
|
+
const toolCall = content;
|
|
175
|
+
return JSON.stringify({
|
|
176
|
+
tool_call_id: toolCall.tool_call_id,
|
|
177
|
+
function: {
|
|
178
|
+
name: toolCall.function.name,
|
|
179
|
+
arguments: toolCall.function.arguments
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
case "tool_result": {
|
|
184
|
+
const toolResult = content;
|
|
185
|
+
return JSON.stringify({
|
|
186
|
+
tool_call_id: toolResult.tool_call_id,
|
|
187
|
+
content: toolResult.content
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
OllamaClient
|
|
197
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { L as LLMClient } from "./index.js";
|
|
2
|
+
class OpenAIClient extends LLMClient {
|
|
3
|
+
constructor(apiKey, baseUrl) {
|
|
4
|
+
super(apiKey, baseUrl || "https://api.openai.com/v1/chat/completions");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 执行OpenAI文本补全
|
|
8
|
+
*/
|
|
9
|
+
async completion(options) {
|
|
10
|
+
try {
|
|
11
|
+
const normalizedOptions = this.normalizeOptions(options);
|
|
12
|
+
const openaiMessages = this.convertMessagesToOpenAIFormat(normalizedOptions.messages);
|
|
13
|
+
const requestBody = {
|
|
14
|
+
model: normalizedOptions.model || "gpt-3.5-turbo",
|
|
15
|
+
messages: openaiMessages,
|
|
16
|
+
temperature: normalizedOptions.temperature ?? 0.7,
|
|
17
|
+
max_tokens: normalizedOptions.maxTokens,
|
|
18
|
+
stream: normalizedOptions.stream || false
|
|
19
|
+
};
|
|
20
|
+
if (normalizedOptions.tools && normalizedOptions.tools.length > 0) {
|
|
21
|
+
requestBody.tools = normalizedOptions.tools;
|
|
22
|
+
if (normalizedOptions.tool_choice) {
|
|
23
|
+
requestBody.tool_choice = normalizedOptions.tool_choice;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (normalizedOptions.response_format) {
|
|
27
|
+
requestBody.response_format = normalizedOptions.response_format;
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(`${this.baseUrl}`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"Authorization": `Bearer ${normalizedOptions.apiKey || this.apiKey}`,
|
|
34
|
+
"X-Title": "MarkPDFdown",
|
|
35
|
+
"HTTP-Referer": "https://github.com/MarkPDFdown"
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(requestBody)
|
|
38
|
+
});
|
|
39
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] POST ${this.baseUrl} (model: ${requestBody.model}) ${response.status} - ${response.statusText}`);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const error = await response.json();
|
|
42
|
+
throw new Error(`OpenAI API错误: ${error.error?.message || response.statusText}`);
|
|
43
|
+
}
|
|
44
|
+
if (normalizedOptions.stream && response.body && normalizedOptions.onUpdate) {
|
|
45
|
+
const reader = response.body.getReader();
|
|
46
|
+
const decoder = new TextDecoder("utf-8");
|
|
47
|
+
let content = "";
|
|
48
|
+
const toolCalls = [];
|
|
49
|
+
const processStream = async () => {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done) {
|
|
52
|
+
return {
|
|
53
|
+
content,
|
|
54
|
+
model: normalizedOptions.model || "gpt-3.5-turbo",
|
|
55
|
+
finishReason: "stop",
|
|
56
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
57
|
+
responseFormat: normalizedOptions.response_format?.type
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const chunk = decoder.decode(value);
|
|
61
|
+
const lines = chunk.split("\n").filter((line) => line.trim() !== "" && line.trim() !== "data: [DONE]");
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (line.startsWith("data: ")) {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(line.slice(6));
|
|
66
|
+
if (data.choices && data.choices[0]?.delta?.content) {
|
|
67
|
+
const newContent = data.choices[0].delta.content;
|
|
68
|
+
content += newContent;
|
|
69
|
+
if (normalizedOptions.onUpdate) {
|
|
70
|
+
normalizedOptions.onUpdate(content);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (data.choices && data.choices[0]?.delta?.tool_calls) {
|
|
74
|
+
const deltaToolCalls = data.choices[0].delta.tool_calls;
|
|
75
|
+
for (const deltaTool of deltaToolCalls) {
|
|
76
|
+
let toolCall = toolCalls.find((tc) => tc.id === deltaTool.id);
|
|
77
|
+
if (!toolCall && deltaTool.id) {
|
|
78
|
+
toolCall = {
|
|
79
|
+
id: deltaTool.id,
|
|
80
|
+
type: "function",
|
|
81
|
+
function: {
|
|
82
|
+
name: "",
|
|
83
|
+
arguments: ""
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
toolCalls.push(toolCall);
|
|
87
|
+
}
|
|
88
|
+
if (toolCall && deltaTool.function) {
|
|
89
|
+
if (deltaTool.function.name) {
|
|
90
|
+
toolCall.function.name = deltaTool.function.name;
|
|
91
|
+
}
|
|
92
|
+
if (deltaTool.function.arguments) {
|
|
93
|
+
toolCall.function.arguments += deltaTool.function.arguments;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (normalizedOptions.onUpdate) {
|
|
98
|
+
normalizedOptions.onUpdate(content);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return processStream();
|
|
106
|
+
};
|
|
107
|
+
return processStream();
|
|
108
|
+
} else {
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
let responseContent = "";
|
|
111
|
+
const toolCalls = [];
|
|
112
|
+
if (data.choices && data.choices[0]?.message) {
|
|
113
|
+
const message = data.choices[0].message;
|
|
114
|
+
if (typeof message.content === "string") {
|
|
115
|
+
responseContent = message.content;
|
|
116
|
+
}
|
|
117
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
118
|
+
for (const toolCall of message.tool_calls) {
|
|
119
|
+
toolCalls.push({
|
|
120
|
+
id: toolCall.id,
|
|
121
|
+
type: toolCall.type,
|
|
122
|
+
function: {
|
|
123
|
+
name: toolCall.function.name,
|
|
124
|
+
arguments: toolCall.function.arguments
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
content: responseContent,
|
|
132
|
+
model: data.model,
|
|
133
|
+
finishReason: data.choices[0]?.finish_reason,
|
|
134
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
135
|
+
responseFormat: normalizedOptions.response_format?.type,
|
|
136
|
+
rawResponse: data
|
|
137
|
+
// 保留原始响应以便调试
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
142
|
+
throw new Error(`OpenAI补全请求失败: ${errorMessage}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 将消息转换为OpenAI API格式
|
|
147
|
+
*/
|
|
148
|
+
convertMessagesToOpenAIFormat(messages) {
|
|
149
|
+
return messages.map((message) => {
|
|
150
|
+
const openaiMessage = {
|
|
151
|
+
role: message.role
|
|
152
|
+
};
|
|
153
|
+
if (message.name) {
|
|
154
|
+
openaiMessage.name = message.name;
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(message.content)) {
|
|
157
|
+
openaiMessage.content = message.content.map((content) => this.convertContentToOpenAIFormat(content));
|
|
158
|
+
} else {
|
|
159
|
+
const content = this.convertContentToOpenAIFormat(message.content);
|
|
160
|
+
if (content.type === "text") {
|
|
161
|
+
openaiMessage.content = content.text;
|
|
162
|
+
} else {
|
|
163
|
+
openaiMessage.content = [content];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return openaiMessage;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 将内容对象转换为OpenAI API格式
|
|
171
|
+
*/
|
|
172
|
+
convertContentToOpenAIFormat(content) {
|
|
173
|
+
switch (content.type) {
|
|
174
|
+
case "text":
|
|
175
|
+
return {
|
|
176
|
+
type: "text",
|
|
177
|
+
text: content.text
|
|
178
|
+
};
|
|
179
|
+
case "image_url": {
|
|
180
|
+
const imageContent = content;
|
|
181
|
+
return {
|
|
182
|
+
type: "image_url",
|
|
183
|
+
image_url: {
|
|
184
|
+
url: imageContent.image_url.url
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
case "tool_call": {
|
|
189
|
+
const toolCallContent = content;
|
|
190
|
+
return {
|
|
191
|
+
type: "tool_call",
|
|
192
|
+
tool_call_id: toolCallContent.tool_call_id,
|
|
193
|
+
function: {
|
|
194
|
+
name: toolCallContent.function.name,
|
|
195
|
+
arguments: toolCallContent.function.arguments
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
case "tool_result": {
|
|
200
|
+
const toolResultContent = content;
|
|
201
|
+
return {
|
|
202
|
+
type: "tool_result",
|
|
203
|
+
tool_call_id: toolResultContent.tool_call_id,
|
|
204
|
+
content: toolResultContent.content
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
default:
|
|
208
|
+
throw new Error(`不支持的内容类型: ${content.type}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export {
|
|
213
|
+
OpenAIClient
|
|
214
|
+
};
|