wtfai 1.2.1 → 1.3.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/README.md CHANGED
@@ -169,35 +169,63 @@ session.off('token', onToken)
169
169
  发送消息执行工作流。
170
170
 
171
171
  ```typescript
172
- // 发送纯文本
172
+ // 基础用法:纯文本
173
173
  await session.send({
174
- content: '你好',
174
+ parts: [
175
+ { type: 'text', text: '你好' }
176
+ ]
175
177
  })
176
178
 
177
- // 发送带图片的消息(自动压缩上传)
179
+ // 图片 + 文本(使用 File)
178
180
  await session.send({
179
- content: '请分析这张图片',
180
- images: [imageFile], // File[]
181
+ parts: [
182
+ { type: 'text', text: '请分析下面的图片:' },
183
+ { type: 'image', file: imageFile },
184
+ { type: 'text', text: '说明这张图的内容' }
185
+ ]
181
186
  })
182
187
 
183
- // 发送带文档的消息(自动上传)
188
+ // 图片 + 文本(使用 URL)
184
189
  await session.send({
185
- content: '请总结这份文档',
186
- documents: [pdfFile], // File[]
190
+ parts: [
191
+ { type: 'text', text: '请对比这两张图片:' },
192
+ { type: 'image', url: 'https://example.com/img1.jpg' },
193
+ { type: 'image', url: 'https://example.com/img2.jpg' }
194
+ ]
187
195
  })
188
196
 
189
- // 使用已上传的 URL
197
+ // 完整示例:文本 + 图片 + 文档
190
198
  await session.send({
191
- content: '你好',
192
- imageUrls: ['https://example.com/image.jpg'],
193
- documentInfos: [{
194
- url: 'https://example.com/doc.pdf',
195
- filename: 'document.pdf',
196
- mimeType: 'application/pdf',
197
- }],
199
+ parts: [
200
+ { type: 'text', text: '请对比下面两张图片:' },
201
+ { type: 'image', file: imageFile1 },
202
+ { type: 'image', file: imageFile2 },
203
+ { type: 'text', text: '然后参考这份报告:' },
204
+ {
205
+ type: 'document',
206
+ file: pdfFile,
207
+ filename: 'report.pdf'
208
+ },
209
+ { type: 'text', text: '说明第一张图的问题在哪里' }
210
+ ]
211
+ })
212
+
213
+ // 使用 blob URL
214
+ await session.send({
215
+ parts: [
216
+ { type: 'text', text: '分析这张图:' },
217
+ { type: 'image', url: blobUrl }, // 会自动上传
218
+ ]
198
219
  })
199
220
  ```
200
221
 
222
+ **说明**:
223
+ - `parts` 数组中的元素会按顺序传递给 LLM,使大模型能准确理解位置指代(如"下面的图片"、"第一张图")
224
+ - `image` 和 `document` 类型必须提供 `file` 或 `url` 之一
225
+ - 图片 File 会自动压缩到 1920px 以内,质量 0.8
226
+ - blob URL 会自动上传到服务器
227
+
228
+
201
229
  ##### `restore()`
202
230
 
203
231
  恢复历史会话(需要在创建 session 时传入 threadId)。
@@ -302,16 +330,24 @@ interface SessionState {
302
330
 
303
331
  ```typescript
304
332
  interface SendInput {
305
- content?: string
306
- images?: File[]
307
- imageUrls?: string[]
308
- documents?: File[]
309
- documentInfos?: Array<{
310
- url: string
311
- filename: string
312
- mimeType: string
313
- }>
333
+ /** 内容部件数组,按顺序传递给 LLM */
334
+ parts: ContentPart[]
314
335
  }
336
+
337
+ type ContentPart =
338
+ | { type: 'text'; text: string }
339
+ | {
340
+ type: 'image'
341
+ file?: File // 图片文件(会自动压缩并上传)
342
+ url?: string // 图片 URL(支持 blob URL 和普通 URL)
343
+ }
344
+ | {
345
+ type: 'document'
346
+ file?: File // 文档文件(会自动上传)
347
+ url?: string // 文档 URL(支持 blob URL 和普通 URL)
348
+ filename: string // 文件名
349
+ mimeType?: string // MIME 类型
350
+ }
315
351
  ```
316
352
 
317
353
  ---
@@ -356,7 +392,9 @@ function ChatComponent({ workflowId }: { workflowId: string }) {
356
392
  })
357
393
 
358
394
  try {
359
- await session.send({ content: input })
395
+ await session.send({
396
+ parts: [{ type: 'text', text: input }]
397
+ })
360
398
  setInput('')
361
399
  } catch (error) {
362
400
  console.error(error)
@@ -390,7 +428,8 @@ function ChatComponent({ workflowId }: { workflowId: string }) {
390
428
 
391
429
  ## 注意事项
392
430
 
393
- 1. **图片自动压缩**:通过 `session.send({ images: [...] })` 上传的图片会自动压缩到 1920px 以内,质量 0.8
394
- 2. **会话管理**:每次调用 `send()` 都会更新 `threadId`,如需保存会话请在 `start` 事件中获取
395
- 3. **错误处理**:建议始终监听 `error` 事件处理异常情况
396
- 4. **资源清理**:组件卸载时调用 `session.abort()` 避免内存泄漏
431
+ 1. **顺序保持**:`parts` 数组中的元素会按顺序传递给 LLM,确保大模型能理解位置指代
432
+ 2. **图片自动压缩**:通过 File 上传的图片会自动压缩到 1920px 以内,质量 0.8
433
+ 3. **会话管理**:每次调用 `send()` 都会更新 `threadId`,如需保存会话请在 `start` 事件中获取
434
+ 4. **错误处理**:建议始终监听 `error` 事件处理异常情况
435
+ 5. **资源清理**:组件卸载时调用 `session.abort()` 避免内存泄漏
package/dist/session.d.ts CHANGED
@@ -41,6 +41,14 @@ export declare class WorkflowSession {
41
41
  * 发送消息执行工作流
42
42
  */
43
43
  send(input: SendInput): Promise<void>;
44
+ /**
45
+ * 解析图片 URL(支持 File 和 URL)
46
+ */
47
+ private resolveImageUrl;
48
+ /**
49
+ * 解析文档信息(支持 File 和 URL)
50
+ */
51
+ private resolveDocumentInfo;
44
52
  /**
45
53
  * 中止执行
46
54
  */
package/dist/session.js CHANGED
@@ -60,89 +60,33 @@ class WorkflowSession {
60
60
  });
61
61
  try {
62
62
  const contentParts = [];
63
- if (input.content) contentParts.push({
64
- type: 'text',
65
- text: input.content
66
- });
67
- if (input.images && input.images.length > 0) {
68
- const imageUrls = await Promise.all(input.images.map((file)=>this.uploadService.uploadImage({
69
- file,
70
- resourceType: 'conversation'
71
- })));
72
- for (const url of imageUrls)contentParts.push({
73
- type: 'image',
74
- url
75
- });
76
- }
77
- if (input.imageUrls && input.imageUrls.length > 0) {
78
- const resolvedUrls = await Promise.all(input.imageUrls.map(async (url)=>{
79
- if (url.startsWith('blob:')) {
80
- const response = await fetch(url);
81
- const blob = await response.blob();
82
- const filename = `image_${Date.now()}.${blob.type.split('/')[1] || 'png'}`;
83
- const file = new File([
84
- blob
85
- ], filename, {
86
- type: blob.type
87
- });
88
- return this.uploadService.uploadImage({
89
- file,
90
- resourceType: 'conversation'
91
- });
92
- }
93
- return url;
94
- }));
95
- for (const url of resolvedUrls)contentParts.push({
96
- type: 'image',
97
- url
98
- });
99
- }
100
- if (input.documents && input.documents.length > 0) {
101
- const docInfos = await Promise.all(input.documents.map(async (file)=>{
102
- const url = await this.uploadService.uploadFile({
103
- file,
104
- resourceType: 'conversation'
63
+ for (const part of input.parts)switch(part.type){
64
+ case 'text':
65
+ contentParts.push({
66
+ type: 'text',
67
+ text: part.text
105
68
  });
106
- return {
107
- url,
108
- filename: file.name,
109
- mimeType: file.type
110
- };
111
- }));
112
- for (const doc of docInfos)contentParts.push({
113
- type: 'document',
114
- url: doc.url,
115
- filename: doc.filename,
116
- mimeType: doc.mimeType
117
- });
118
- }
119
- if (input.documentInfos && input.documentInfos.length > 0) {
120
- const resolvedDocs = await Promise.all(input.documentInfos.map(async (doc)=>{
121
- if (doc.url.startsWith('blob:')) {
122
- const response = await fetch(doc.url);
123
- const blob = await response.blob();
124
- const file = new File([
125
- blob
126
- ], doc.filename, {
127
- type: doc.mimeType || blob.type
69
+ break;
70
+ case 'image':
71
+ {
72
+ const imageUrl = await this.resolveImageUrl(part);
73
+ contentParts.push({
74
+ type: 'image',
75
+ url: imageUrl
128
76
  });
129
- const uploadedUrl = await this.uploadService.uploadFile({
130
- file,
131
- resourceType: 'conversation'
77
+ break;
78
+ }
79
+ case 'document':
80
+ {
81
+ const docInfo = await this.resolveDocumentInfo(part);
82
+ contentParts.push({
83
+ type: 'document',
84
+ url: docInfo.url,
85
+ filename: docInfo.filename,
86
+ mimeType: docInfo.mimeType
132
87
  });
133
- return {
134
- ...doc,
135
- url: uploadedUrl
136
- };
88
+ break;
137
89
  }
138
- return doc;
139
- }));
140
- for (const doc of resolvedDocs)contentParts.push({
141
- type: 'document',
142
- url: doc.url,
143
- filename: doc.filename,
144
- mimeType: doc.mimeType
145
- });
146
90
  }
147
91
  const executionInput = {
148
92
  messages: [
@@ -151,16 +95,31 @@ class WorkflowSession {
151
95
  }
152
96
  ]
153
97
  };
154
- const uploadedImages = contentParts.filter((p)=>'image' === p.type && 'url' in p).map((p)=>p.url);
155
- const uploadedDocs = contentParts.filter((p)=>'document' === p.type).map((p)=>({
156
- filename: p.filename,
157
- url: p.url || '#'
158
- }));
98
+ const parts = contentParts.map((part)=>{
99
+ switch(part.type){
100
+ case 'text':
101
+ return {
102
+ type: 'text',
103
+ text: part.text
104
+ };
105
+ case 'image':
106
+ return {
107
+ type: 'image',
108
+ url: part.url
109
+ };
110
+ case 'document':
111
+ return {
112
+ type: 'document',
113
+ filename: part.filename,
114
+ url: part.url
115
+ };
116
+ default:
117
+ throw new Error(`Unknown part type: ${part.type}`);
118
+ }
119
+ });
159
120
  const userMessage = {
160
121
  type: 'human',
161
- content: input.content || '',
162
- images: uploadedImages.length > 0 ? uploadedImages : void 0,
163
- documents: uploadedDocs.length > 0 ? uploadedDocs : void 0
122
+ parts
164
123
  };
165
124
  this.updateState({
166
125
  messages: [
@@ -213,7 +172,12 @@ class WorkflowSession {
213
172
  if (!currentAiMessage) {
214
173
  currentAiMessage = {
215
174
  type: 'ai',
216
- content: ''
175
+ parts: [
176
+ {
177
+ type: 'text',
178
+ text: ''
179
+ }
180
+ ]
217
181
  };
218
182
  const newMessages = [
219
183
  ...this.state.messages,
@@ -224,7 +188,8 @@ class WorkflowSession {
224
188
  messages: newMessages
225
189
  });
226
190
  }
227
- currentAiMessage.content = currentAiMessage.content + data.c;
191
+ const textPart = currentAiMessage.parts.find((p)=>'text' === p.type);
192
+ if (textPart && 'text' === textPart.type) textPart.text += data.c;
228
193
  const updatedMessages = [
229
194
  ...this.state.messages
230
195
  ];
@@ -277,6 +242,69 @@ class WorkflowSession {
277
242
  throw error;
278
243
  }
279
244
  }
245
+ async resolveImageUrl(part) {
246
+ if (part.file) return this.uploadService.uploadImage({
247
+ file: part.file,
248
+ resourceType: 'conversation'
249
+ });
250
+ if (part.url) {
251
+ if (part.url.startsWith('blob:')) {
252
+ const response = await fetch(part.url);
253
+ const blob = await response.blob();
254
+ const filename = `image_${Date.now()}.${blob.type.split('/')[1] || 'png'}`;
255
+ const file = new File([
256
+ blob
257
+ ], filename, {
258
+ type: blob.type
259
+ });
260
+ return this.uploadService.uploadImage({
261
+ file,
262
+ resourceType: 'conversation'
263
+ });
264
+ }
265
+ return part.url;
266
+ }
267
+ throw new Error('Image part must have either file or url');
268
+ }
269
+ async resolveDocumentInfo(part) {
270
+ if (part.file) {
271
+ const url = await this.uploadService.uploadFile({
272
+ file: part.file,
273
+ resourceType: 'conversation'
274
+ });
275
+ return {
276
+ url,
277
+ filename: part.filename,
278
+ mimeType: part.mimeType || part.file.type
279
+ };
280
+ }
281
+ if (part.url) {
282
+ if (part.url.startsWith('blob:')) {
283
+ const response = await fetch(part.url);
284
+ const blob = await response.blob();
285
+ const file = new File([
286
+ blob
287
+ ], part.filename, {
288
+ type: part.mimeType || blob.type
289
+ });
290
+ const url = await this.uploadService.uploadFile({
291
+ file,
292
+ resourceType: 'conversation'
293
+ });
294
+ return {
295
+ url,
296
+ filename: part.filename,
297
+ mimeType: part.mimeType || blob.type
298
+ };
299
+ }
300
+ return {
301
+ url: part.url,
302
+ filename: part.filename,
303
+ mimeType: part.mimeType || 'application/octet-stream'
304
+ };
305
+ }
306
+ throw new Error('Document part must have either file or url');
307
+ }
280
308
  abort() {
281
309
  if (this.abortController) {
282
310
  this.abortController.abort();
package/dist/types.d.ts CHANGED
@@ -49,24 +49,43 @@ export interface CompressOptions {
49
49
  /** 超过此大小(字节)的图片会被转换为 JPEG,默认 1MB */
50
50
  convertSize?: number;
51
51
  }
52
+ /**
53
+ * 内容部件类型
54
+ */
55
+ export type ContentPart = {
56
+ type: 'text';
57
+ text: string;
58
+ } | {
59
+ type: 'image';
60
+ /** 图片文件(会自动压缩并上传) */
61
+ file: File;
62
+ } | {
63
+ type: 'image';
64
+ /** 图片 URL(支持 blob URL 和普通 URL) */
65
+ url: string;
66
+ } | {
67
+ type: 'document';
68
+ /** 文档 URL(支持 blob URL 和普通 URL) */
69
+ url: string;
70
+ /** 文件名 */
71
+ filename: string;
72
+ /** MIME 类型 */
73
+ mimeType: string;
74
+ } | {
75
+ type: 'document';
76
+ /** 文档文件(会自动上传) */
77
+ file: File;
78
+ /** 文件名 */
79
+ filename: string;
80
+ /** MIME 类型 */
81
+ mimeType: string;
82
+ };
52
83
  /**
53
84
  * 发送消息的输入
54
85
  */
55
86
  export interface SendInput {
56
- /** 文本内容 */
57
- content?: string;
58
- /** 图片文件列表(会自动压缩并上传) */
59
- images?: File[];
60
- /** 已上传的图片 URL 列表 */
61
- imageUrls?: string[];
62
- /** 文档文件列表(会自动上传) */
63
- documents?: File[];
64
- /** 已上传的文档信息列表 */
65
- documentInfos?: Array<{
66
- url: string;
67
- filename: string;
68
- mimeType: string;
69
- }>;
87
+ /** 内容部件数组,按顺序传递给 LLM */
88
+ parts: ContentPart[];
70
89
  }
71
90
  /**
72
91
  * 上传临时凭证
@@ -0,0 +1,2 @@
1
+ import { type ComponentProps } from '@ant-design/x-markdown';
2
+ export declare const Code: React.FC<ComponentProps>;
@@ -0,0 +1,16 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { CodeHighlighter, Mermaid } from "@ant-design/x";
3
+ const Code = (props)=>{
4
+ var _className_match;
5
+ const { className, children } = props;
6
+ const lang = (null == className ? void 0 : null == (_className_match = className.match(/language-(\w+)/)) ? void 0 : _className_match[1]) || '';
7
+ if ('string' != typeof children) return null;
8
+ if ('mermaid' === lang) return /*#__PURE__*/ jsx(Mermaid, {
9
+ children: children
10
+ });
11
+ return /*#__PURE__*/ jsx(CodeHighlighter, {
12
+ lang: lang,
13
+ children: children
14
+ });
15
+ };
16
+ export { Code };
@@ -1,4 +1,4 @@
1
1
  import { XMarkdownProps } from '@ant-design/x-markdown';
2
2
  import '@ant-design/x-markdown/themes/light.css';
3
- declare const Markdown: ({ className, streaming, ...props }: XMarkdownProps) => import("react/jsx-runtime").JSX.Element;
3
+ declare const Markdown: ({ className, streaming, config, components, ...props }: XMarkdownProps) => import("react/jsx-runtime").JSX.Element;
4
4
  export { Markdown };
@@ -1,13 +1,28 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { XMarkdown } from "@ant-design/x-markdown";
3
3
  import "@ant-design/x-markdown/themes/light.css";
4
+ import Latex from "@ant-design/x-markdown/plugins/Latex";
5
+ import zh_CN from "@ant-design/x/locale/zh_CN";
4
6
  import clsx from "clsx";
5
- const Markdown = ({ className, streaming, ...props })=>/*#__PURE__*/ jsx(XMarkdown, {
6
- className: clsx(className, 'x-markdown-light'),
7
- streaming: {
8
- ...streaming,
9
- enableAnimation: true
10
- },
11
- ...props
7
+ import { Code } from "./code.js";
8
+ import { XProvider } from "@ant-design/x";
9
+ const Markdown = ({ className, streaming, config, components, ...props })=>/*#__PURE__*/ jsx(XProvider, {
10
+ locale: zh_CN,
11
+ children: /*#__PURE__*/ jsx(XMarkdown, {
12
+ className: clsx(className, 'x-markdown-light'),
13
+ streaming: {
14
+ ...streaming,
15
+ enableAnimation: true
16
+ },
17
+ config: {
18
+ ...config,
19
+ extensions: Latex()
20
+ },
21
+ components: {
22
+ ...components,
23
+ code: Code
24
+ },
25
+ ...props
26
+ })
12
27
  });
13
28
  export { Markdown };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtfai",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {