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.
Files changed (81) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +128 -0
  3. package/bin/cli.js +130 -0
  4. package/dist/main/AnthropicClient-CTbHYiqm.js +193 -0
  5. package/dist/main/GeminiClient-CrtYbwaF.js +196 -0
  6. package/dist/main/OllamaClient-DKJsnvIt.js +197 -0
  7. package/dist/main/OpenAIClient-gyy2nFkw.js +214 -0
  8. package/dist/main/OpenAIResponsesClient-DETYz2nL.js +297 -0
  9. package/dist/main/index.js +3523 -0
  10. package/dist/preload/index.js +102 -0
  11. package/dist/renderer/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  12. package/dist/renderer/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  13. package/dist/renderer/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  14. package/dist/renderer/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  15. package/dist/renderer/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  16. package/dist/renderer/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  17. package/dist/renderer/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  18. package/dist/renderer/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  19. package/dist/renderer/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  20. package/dist/renderer/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  21. package/dist/renderer/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  22. package/dist/renderer/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  23. package/dist/renderer/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  24. package/dist/renderer/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  25. package/dist/renderer/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  26. package/dist/renderer/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  27. package/dist/renderer/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  28. package/dist/renderer/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  29. package/dist/renderer/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  30. package/dist/renderer/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  31. package/dist/renderer/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  32. package/dist/renderer/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  33. package/dist/renderer/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  34. package/dist/renderer/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  35. package/dist/renderer/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  36. package/dist/renderer/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  37. package/dist/renderer/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  38. package/dist/renderer/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  39. package/dist/renderer/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  40. package/dist/renderer/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  41. package/dist/renderer/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  42. package/dist/renderer/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  43. package/dist/renderer/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  44. package/dist/renderer/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  45. package/dist/renderer/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  46. package/dist/renderer/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  47. package/dist/renderer/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  48. package/dist/renderer/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  49. package/dist/renderer/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  50. package/dist/renderer/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  51. package/dist/renderer/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  52. package/dist/renderer/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  53. package/dist/renderer/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  54. package/dist/renderer/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  55. package/dist/renderer/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  56. package/dist/renderer/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  57. package/dist/renderer/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  58. package/dist/renderer/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  59. package/dist/renderer/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  60. package/dist/renderer/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  61. package/dist/renderer/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  62. package/dist/renderer/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  63. package/dist/renderer/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  64. package/dist/renderer/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  65. package/dist/renderer/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  66. package/dist/renderer/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  67. package/dist/renderer/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  68. package/dist/renderer/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  69. package/dist/renderer/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  70. package/dist/renderer/assets/MarkPDFdown-C6Sb1T4M.png +0 -0
  71. package/dist/renderer/assets/index-CbMlWqbh.css +327 -0
  72. package/dist/renderer/assets/index-DeDe7lry.js +123956 -0
  73. package/dist/renderer/index.html +14 -0
  74. package/package.json +156 -0
  75. package/src/core/infrastructure/db/migrations/20250414154412_/migration.sql +24 -0
  76. package/src/core/infrastructure/db/migrations/20250419090345_/migration.sql +29 -0
  77. package/src/core/infrastructure/db/migrations/20250419104636_/migration.sql +47 -0
  78. package/src/core/infrastructure/db/migrations/20260121154536_add_worker_fields/migration.sql +50 -0
  79. package/src/core/infrastructure/db/migrations/20260124014806_/migration.sql +55 -0
  80. package/src/core/infrastructure/db/migrations/migration_lock.toml +3 -0
  81. 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
+ };