wtfai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +376 -0
- package/dist/client.d.ts +55 -0
- package/dist/client.js +43 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/session.d.ts +48 -0
- package/dist/session.js +245 -0
- package/dist/sse.d.ts +19 -0
- package/dist/sse.js +60 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +0 -0
- package/dist/upload.d.ts +25 -0
- package/dist/upload.js +103 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# wtfai
|
|
2
|
+
|
|
3
|
+
Agent 工作流 SDK,为第三方 Web 开发者提供简洁的 API 来集成工作流执行、文件上传和会话管理功能。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install wtfai
|
|
9
|
+
# 或
|
|
10
|
+
pnpm add wtfai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 快速开始
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import Wtfai from 'wtfai'
|
|
17
|
+
|
|
18
|
+
// 1. 创建客户端
|
|
19
|
+
const client = new Wtfai({
|
|
20
|
+
baseUrl: 'https://api.example.com',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// 2. 创建会话
|
|
24
|
+
const session = client.createSession('workflow-id')
|
|
25
|
+
|
|
26
|
+
// 3. 监听状态变化
|
|
27
|
+
session.on('stateChange', (state) => {
|
|
28
|
+
console.log('消息列表:', state.messages)
|
|
29
|
+
console.log('是否执行中:', state.isExecuting)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// 4. 发送消息
|
|
33
|
+
await session.send({ content: '你好' })
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API 文档
|
|
37
|
+
|
|
38
|
+
### Wtfai
|
|
39
|
+
|
|
40
|
+
SDK 主入口类。
|
|
41
|
+
|
|
42
|
+
#### 构造函数
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const client = new Wtfai({
|
|
46
|
+
baseUrl: string, // 必填,API 服务器地址
|
|
47
|
+
timeout?: number, // 可选,请求超时时间(毫秒)
|
|
48
|
+
headers?: Record<string, string>, // 可选,自定义请求头
|
|
49
|
+
tenantSecret?: string, // 可选,租户密钥 (⚠️ 仅用于调试,禁止在生产环境使用)
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### 方法
|
|
54
|
+
|
|
55
|
+
##### `getWorkflows(page?: number, pageSize?: number)`
|
|
56
|
+
|
|
57
|
+
获取工作流列表。
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// 分页获取工作流,默认 page=1, pageSize=10
|
|
61
|
+
const { items, total } = await client.getWorkflows(1, 20)
|
|
62
|
+
console.log(`共 ${total} 个工作流`)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
##### `getWorkflow(id: string)`
|
|
66
|
+
|
|
67
|
+
获取工作流详情。
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const workflow = await client.getWorkflow('workflow-id')
|
|
71
|
+
console.log(workflow.name, workflow.description)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
##### `createSession(workflowId: string, options?: SessionOptions)`
|
|
75
|
+
|
|
76
|
+
创建工作流会话。
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// 新会话
|
|
80
|
+
const session = client.createSession('workflow-id')
|
|
81
|
+
|
|
82
|
+
// 恢复已有会话
|
|
83
|
+
const session = client.createSession('workflow-id', {
|
|
84
|
+
threadId: 'existing-thread-id',
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### WorkflowSession
|
|
91
|
+
|
|
92
|
+
工作流会话类,管理消息状态和执行流程。
|
|
93
|
+
|
|
94
|
+
#### 方法
|
|
95
|
+
|
|
96
|
+
##### `on(event, listener)`
|
|
97
|
+
|
|
98
|
+
监听事件。
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
session.on('start', ({ threadId }) => {
|
|
102
|
+
// 工作流开始执行,获取到 threadId
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
session.on('nodeStart', ({ nodeId }) => {
|
|
106
|
+
// 节点开始执行
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
session.on('nodeEnd', ({ nodeId, variables, messages }) => {
|
|
110
|
+
// 节点执行结束
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
session.on('token', ({ nodeId, content, isReasoning }) => {
|
|
114
|
+
// 收到流式 token(用于打字机效果)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
session.on('loading', ({ nodeId, message }) => {
|
|
118
|
+
// 收到 loading 事件,用于更新加载状态
|
|
119
|
+
console.log('Loading:', message)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
session.on('complete', () => {
|
|
123
|
+
// 工作流执行完成
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
session.on('error', ({ message }) => {
|
|
127
|
+
// 发生错误
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
session.on('stateChange', (state) => {
|
|
131
|
+
// 状态变化(推荐使用此事件更新 UI)
|
|
132
|
+
// state.messages: SimpleMessage[]
|
|
133
|
+
// state.isExecuting: boolean
|
|
134
|
+
// state.threadId?: string
|
|
135
|
+
// state.currentNodeId?: string
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
##### `off(event, listener)`
|
|
140
|
+
|
|
141
|
+
移除事件监听。
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const onToken = (data) => console.log(data)
|
|
145
|
+
session.on('token', onToken)
|
|
146
|
+
|
|
147
|
+
// 不需要时移除监听
|
|
148
|
+
session.off('token', onToken)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
##### `send(input: SendInput)`
|
|
152
|
+
|
|
153
|
+
发送消息执行工作流。
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// 发送纯文本
|
|
157
|
+
await session.send({
|
|
158
|
+
content: '你好',
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// 发送带图片的消息(自动压缩上传)
|
|
162
|
+
await session.send({
|
|
163
|
+
content: '请分析这张图片',
|
|
164
|
+
images: [imageFile], // File[]
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// 发送带文档的消息(自动上传)
|
|
168
|
+
await session.send({
|
|
169
|
+
content: '请总结这份文档',
|
|
170
|
+
documents: [pdfFile], // File[]
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// 使用已上传的 URL
|
|
174
|
+
await session.send({
|
|
175
|
+
content: '你好',
|
|
176
|
+
imageUrls: ['https://example.com/image.jpg'],
|
|
177
|
+
documentInfos: [{
|
|
178
|
+
url: 'https://example.com/doc.pdf',
|
|
179
|
+
filename: 'document.pdf',
|
|
180
|
+
mimeType: 'application/pdf',
|
|
181
|
+
}],
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
##### `restore()`
|
|
186
|
+
|
|
187
|
+
恢复历史会话(需要在创建 session 时传入 threadId)。
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const session = client.createSession('workflow-id', {
|
|
191
|
+
threadId: 'existing-thread-id',
|
|
192
|
+
})
|
|
193
|
+
await session.restore()
|
|
194
|
+
// 会触发 stateChange 事件,包含历史消息
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
##### `getState()`
|
|
198
|
+
|
|
199
|
+
获取当前状态快照。
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const state = session.getState()
|
|
203
|
+
console.log(state.messages)
|
|
204
|
+
console.log(state.isExecuting)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
##### `abort()`
|
|
208
|
+
|
|
209
|
+
中止当前执行。
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
session.abort()
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### UploadService
|
|
218
|
+
|
|
219
|
+
文件上传服务(通过 `client.upload` 访问)。
|
|
220
|
+
|
|
221
|
+
##### `uploadFile(params)`
|
|
222
|
+
|
|
223
|
+
上传文件。
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const url = await client.upload.uploadFile({
|
|
227
|
+
file: file,
|
|
228
|
+
resourceType: 'conversation', // 'conversation' | 'workflow' | 'knowledge-base'
|
|
229
|
+
onProgress: (percent) => {
|
|
230
|
+
console.log(`上传进度: ${percent * 100}%`)
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
##### `uploadImage(file, resourceType, onProgress?)`
|
|
236
|
+
|
|
237
|
+
上传图片(自动压缩)。
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const url = await client.upload.uploadImage(
|
|
241
|
+
imageFile,
|
|
242
|
+
'conversation',
|
|
243
|
+
(percent) => console.log(`${percent * 100}%`),
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 类型定义
|
|
250
|
+
|
|
251
|
+
### SimpleMessage
|
|
252
|
+
|
|
253
|
+
消息类型(与后端保持一致)。
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
interface SimpleMessage {
|
|
257
|
+
type: 'human' | 'ai' | 'system'
|
|
258
|
+
content: string | Array<{ type: string; [key: string]: any }>
|
|
259
|
+
id?: string
|
|
260
|
+
images?: string[]
|
|
261
|
+
videos?: string[]
|
|
262
|
+
documents?: Array<{ filename: string; url: string }>
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### SessionState
|
|
267
|
+
|
|
268
|
+
会话状态类型。
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
interface SessionState {
|
|
272
|
+
threadId?: string
|
|
273
|
+
messages: SimpleMessage[]
|
|
274
|
+
isExecuting: boolean
|
|
275
|
+
currentNodeId?: string
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### SendInput
|
|
280
|
+
|
|
281
|
+
发送消息输入类型。
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
interface SendInput {
|
|
285
|
+
content?: string
|
|
286
|
+
images?: File[]
|
|
287
|
+
imageUrls?: string[]
|
|
288
|
+
documents?: File[]
|
|
289
|
+
documentInfos?: Array<{
|
|
290
|
+
url: string
|
|
291
|
+
filename: string
|
|
292
|
+
mimeType: string
|
|
293
|
+
}>
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 完整示例
|
|
300
|
+
|
|
301
|
+
### React 聊天组件
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { useState, useEffect, useRef } from 'react'
|
|
305
|
+
import Wtfai, { type SimpleMessage, type WorkflowSession } from 'wtfai'
|
|
306
|
+
|
|
307
|
+
const client = new W't'fa'i({
|
|
308
|
+
baseUrl: 'https://api.example.com',
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
function ChatComponent({ workflowId }: { workflowId: string }) {
|
|
312
|
+
const [messages, setMessages] = useState<SimpleMessage[]>([])
|
|
313
|
+
const [isExecuting, setIsExecuting] = useState(false)
|
|
314
|
+
const [input, setInput] = useState('')
|
|
315
|
+
const sessionRef = useRef<WorkflowSession | null>(null)
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
return () => {
|
|
319
|
+
sessionRef.current?.abort()
|
|
320
|
+
}
|
|
321
|
+
}, [])
|
|
322
|
+
|
|
323
|
+
const handleSend = async () => {
|
|
324
|
+
if (!input.trim() || isExecuting) return
|
|
325
|
+
|
|
326
|
+
const session = client.createSession(workflowId)
|
|
327
|
+
sessionRef.current = session
|
|
328
|
+
|
|
329
|
+
session.on('stateChange', (state) => {
|
|
330
|
+
setMessages([...state.messages])
|
|
331
|
+
setIsExecuting(state.isExecuting)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
session.on('error', ({ message }) => {
|
|
335
|
+
alert(`错误: ${message}`)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
await session.send({ content: input })
|
|
340
|
+
setInput('')
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error(error)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div>
|
|
348
|
+
<div className="messages">
|
|
349
|
+
{messages.map((msg, i) => (
|
|
350
|
+
<div key={i} className={msg.type}>
|
|
351
|
+
{typeof msg.content === 'string' ? msg.content : '...'}
|
|
352
|
+
</div>
|
|
353
|
+
))}
|
|
354
|
+
{isExecuting && <div>思考中...</div>}
|
|
355
|
+
</div>
|
|
356
|
+
<input
|
|
357
|
+
value={input}
|
|
358
|
+
onChange={(e) => setInput(e.target.value)}
|
|
359
|
+
disabled={isExecuting}
|
|
360
|
+
/>
|
|
361
|
+
<button onClick={handleSend} disabled={isExecuting}>
|
|
362
|
+
发送
|
|
363
|
+
</button>
|
|
364
|
+
</div>
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 注意事项
|
|
372
|
+
|
|
373
|
+
1. **图片自动压缩**:通过 `session.send({ images: [...] })` 上传的图片会自动压缩到 1920px 以内,质量 0.8
|
|
374
|
+
2. **会话管理**:每次调用 `send()` 都会更新 `threadId`,如需保存会话请在 `start` 事件中获取
|
|
375
|
+
3. **错误处理**:建议始终监听 `error` 事件处理异常情况
|
|
376
|
+
4. **资源清理**:组件卸载时调用 `session.abort()` 避免内存泄漏
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Workflow, WorkflowListResponse } from '@our-llm/shared/workflow.types';
|
|
2
|
+
import type { OurLLMClientConfig, SessionOptions } from './types';
|
|
3
|
+
import { WorkflowSession } from './session';
|
|
4
|
+
import { UploadService } from './upload';
|
|
5
|
+
/**
|
|
6
|
+
* Wtfai 客户端
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import Wtfai from 'wtfai'
|
|
11
|
+
*
|
|
12
|
+
* const client = new Wtfai({
|
|
13
|
+
* baseUrl: 'https://api.example.com',
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* // 获取工作流
|
|
17
|
+
* const workflow = await client.getWorkflow('workflow-id')
|
|
18
|
+
*
|
|
19
|
+
* // 获取工作流列表
|
|
20
|
+
* const workflows = await client.getWorkflows(1, 10)
|
|
21
|
+
*
|
|
22
|
+
* // 创建会话
|
|
23
|
+
* const session = client.createSession('workflow-id')
|
|
24
|
+
*
|
|
25
|
+
* // 监听状态变化
|
|
26
|
+
* session.on('stateChange', (state) => {
|
|
27
|
+
* console.log('Messages:', state.messages)
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* // 发送消息
|
|
31
|
+
* await session.send({ content: '你好' })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class Wtfai {
|
|
35
|
+
private readonly baseUrl;
|
|
36
|
+
private readonly uploadService;
|
|
37
|
+
/**
|
|
38
|
+
* 文件上传服务
|
|
39
|
+
*/
|
|
40
|
+
readonly upload: UploadService;
|
|
41
|
+
private readonly headers;
|
|
42
|
+
constructor(config: OurLLMClientConfig);
|
|
43
|
+
/**
|
|
44
|
+
* 获取工作流列表
|
|
45
|
+
*/
|
|
46
|
+
getWorkflows(page?: number, pageSize?: number): Promise<WorkflowListResponse>;
|
|
47
|
+
/**
|
|
48
|
+
* 获取工作流详情
|
|
49
|
+
*/
|
|
50
|
+
getWorkflow(id: string): Promise<Workflow>;
|
|
51
|
+
/**
|
|
52
|
+
* 创建工作流会话
|
|
53
|
+
*/
|
|
54
|
+
createSession(workflowId: string, options?: SessionOptions): WorkflowSession;
|
|
55
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { WorkflowSession } from "./session.js";
|
|
2
|
+
import { UploadService } from "./upload.js";
|
|
3
|
+
class Wtfai {
|
|
4
|
+
baseUrl;
|
|
5
|
+
uploadService;
|
|
6
|
+
upload;
|
|
7
|
+
headers;
|
|
8
|
+
constructor(config){
|
|
9
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
10
|
+
const headers = {
|
|
11
|
+
...config.headers
|
|
12
|
+
};
|
|
13
|
+
if (config.tenantSecret) {
|
|
14
|
+
console.warn('%c[OurLLM SDK] ⚠️ Warning: tenantSecret is provided. This should ONLY be used for debugging purposes. DO NOT use in production.', 'color: red; font-weight: bold; font-size: 14px;');
|
|
15
|
+
headers['x-tenant-secret'] = config.tenantSecret;
|
|
16
|
+
}
|
|
17
|
+
this.headers = headers;
|
|
18
|
+
this.uploadService = new UploadService(this.baseUrl, this.headers);
|
|
19
|
+
this.upload = this.uploadService;
|
|
20
|
+
}
|
|
21
|
+
async getWorkflows(page = 1, pageSize = 10) {
|
|
22
|
+
const params = new URLSearchParams({
|
|
23
|
+
current: String(page),
|
|
24
|
+
pageSize: String(pageSize)
|
|
25
|
+
});
|
|
26
|
+
const response = await fetch(`${this.baseUrl}/workflows?${params}`, {
|
|
27
|
+
headers: this.headers
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) throw new Error('获取工作流列表失败');
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
async getWorkflow(id) {
|
|
33
|
+
const response = await fetch(`${this.baseUrl}/workflows/${id}`, {
|
|
34
|
+
headers: this.headers
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) throw new Error('获取工作流详情失败');
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
createSession(workflowId, options) {
|
|
40
|
+
return new WorkflowSession(workflowId, this.baseUrl, this.uploadService, this.headers, options);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export { Wtfai };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Wtfai } from './client';
|
|
2
|
+
export default Wtfai;
|
|
3
|
+
export { WorkflowSession } from './session';
|
|
4
|
+
export { UploadService } from './upload';
|
|
5
|
+
export type { OurLLMClientConfig, SessionOptions, SessionState, SessionEventListeners, SendInput, UploadParams, CompressOptions, WorkflowInfo, } from './types';
|
|
6
|
+
export type { SimpleMessage } from '@our-llm/shared/workflow-events';
|
|
7
|
+
export type { Workflow, WorkflowConfig } from '@our-llm/shared/workflow.types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SessionOptions, SessionState, SessionEventListeners, SendInput } from './types';
|
|
2
|
+
import { UploadService } from './upload';
|
|
3
|
+
/**
|
|
4
|
+
* 工作流会话
|
|
5
|
+
* 管理单个工作流的执行状态和消息
|
|
6
|
+
*/
|
|
7
|
+
export declare class WorkflowSession {
|
|
8
|
+
private readonly workflowId;
|
|
9
|
+
private readonly baseUrl;
|
|
10
|
+
private readonly uploadService;
|
|
11
|
+
private readonly headers;
|
|
12
|
+
private state;
|
|
13
|
+
private listeners;
|
|
14
|
+
private abortController?;
|
|
15
|
+
constructor(workflowId: string, baseUrl: string, uploadService: UploadService, headers?: Record<string, string>, options?: SessionOptions);
|
|
16
|
+
/**
|
|
17
|
+
* 监听事件
|
|
18
|
+
*/
|
|
19
|
+
on<K extends keyof SessionEventListeners>(event: K, listener: SessionEventListeners[K]): this;
|
|
20
|
+
/**
|
|
21
|
+
* 移除事件监听
|
|
22
|
+
*/
|
|
23
|
+
off<K extends keyof SessionEventListeners>(event: K, listener: SessionEventListeners[K]): this;
|
|
24
|
+
/**
|
|
25
|
+
* 触发事件
|
|
26
|
+
*/
|
|
27
|
+
private emit;
|
|
28
|
+
/**
|
|
29
|
+
* 更新状态并触发 stateChange 事件
|
|
30
|
+
*/
|
|
31
|
+
private updateState;
|
|
32
|
+
/**
|
|
33
|
+
* 获取当前状态快照
|
|
34
|
+
*/
|
|
35
|
+
getState(): SessionState;
|
|
36
|
+
/**
|
|
37
|
+
* 恢复历史会话
|
|
38
|
+
*/
|
|
39
|
+
restore(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* 发送消息执行工作流
|
|
42
|
+
*/
|
|
43
|
+
send(input: SendInput): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* 中止执行
|
|
46
|
+
*/
|
|
47
|
+
abort(): void;
|
|
48
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { executeWorkflowSSE } from "./sse.js";
|
|
2
|
+
import { v7 } from "uuid";
|
|
3
|
+
class WorkflowSession {
|
|
4
|
+
workflowId;
|
|
5
|
+
baseUrl;
|
|
6
|
+
uploadService;
|
|
7
|
+
headers;
|
|
8
|
+
state = {
|
|
9
|
+
messages: [],
|
|
10
|
+
isExecuting: false
|
|
11
|
+
};
|
|
12
|
+
listeners = new Map();
|
|
13
|
+
abortController;
|
|
14
|
+
constructor(workflowId, baseUrl, uploadService, headers = {}, options = {}){
|
|
15
|
+
this.workflowId = workflowId;
|
|
16
|
+
this.baseUrl = baseUrl;
|
|
17
|
+
this.uploadService = uploadService;
|
|
18
|
+
this.headers = headers;
|
|
19
|
+
if (options.threadId) this.state.threadId = options.threadId;
|
|
20
|
+
else this.state.threadId = v7();
|
|
21
|
+
}
|
|
22
|
+
on(event, listener) {
|
|
23
|
+
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
|
|
24
|
+
this.listeners.get(event).add(listener);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
off(event, listener) {
|
|
28
|
+
this.listeners.get(event)?.delete(listener);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
emit(event, ...args) {
|
|
32
|
+
const listeners = this.listeners.get(event);
|
|
33
|
+
if (listeners) Array.from(listeners).forEach((listener)=>{
|
|
34
|
+
listener(...args);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
updateState(updates) {
|
|
38
|
+
this.state = {
|
|
39
|
+
...this.state,
|
|
40
|
+
...updates
|
|
41
|
+
};
|
|
42
|
+
this.emit('stateChange', {
|
|
43
|
+
...this.state
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
getState() {
|
|
47
|
+
return {
|
|
48
|
+
...this.state
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async restore() {
|
|
52
|
+
if (!this.state.threadId) throw new Error('无法恢复会话:未提供 threadId');
|
|
53
|
+
const response = await fetch(`${this.baseUrl}/workflows/${this.workflowId}/state?threadId=${this.state.threadId}`, {
|
|
54
|
+
headers: this.headers
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) throw new Error('获取工作流状态失败');
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
this.updateState({
|
|
59
|
+
messages: data.messages
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async send(input) {
|
|
63
|
+
if (this.state.isExecuting) throw new Error('工作流正在执行中');
|
|
64
|
+
this.updateState({
|
|
65
|
+
isExecuting: true,
|
|
66
|
+
currentNodeId: void 0
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const contentParts = [];
|
|
70
|
+
if (input.content) contentParts.push({
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: input.content
|
|
73
|
+
});
|
|
74
|
+
if (input.images && input.images.length > 0) {
|
|
75
|
+
const imageUrls = await Promise.all(input.images.map((file)=>this.uploadService.uploadImage(file, 'conversation')));
|
|
76
|
+
for (const url of imageUrls)contentParts.push({
|
|
77
|
+
type: 'image',
|
|
78
|
+
url
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (input.imageUrls) for (const url of input.imageUrls)contentParts.push({
|
|
82
|
+
type: 'image',
|
|
83
|
+
url
|
|
84
|
+
});
|
|
85
|
+
if (input.documents && input.documents.length > 0) {
|
|
86
|
+
const docInfos = await Promise.all(input.documents.map(async (file)=>{
|
|
87
|
+
const url = await this.uploadService.uploadFile({
|
|
88
|
+
file,
|
|
89
|
+
resourceType: 'conversation'
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
url,
|
|
93
|
+
filename: file.name,
|
|
94
|
+
mimeType: file.type
|
|
95
|
+
};
|
|
96
|
+
}));
|
|
97
|
+
for (const doc of docInfos)contentParts.push({
|
|
98
|
+
type: 'document',
|
|
99
|
+
url: doc.url,
|
|
100
|
+
filename: doc.filename,
|
|
101
|
+
mimeType: doc.mimeType
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (input.documentInfos) for (const doc of input.documentInfos)contentParts.push({
|
|
105
|
+
type: 'document',
|
|
106
|
+
url: doc.url,
|
|
107
|
+
filename: doc.filename,
|
|
108
|
+
mimeType: doc.mimeType
|
|
109
|
+
});
|
|
110
|
+
const executionInput = {
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
content: contentParts
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
const uploadedImages = contentParts.filter((p)=>'image' === p.type && 'url' in p).map((p)=>p.url);
|
|
118
|
+
const uploadedDocs = contentParts.filter((p)=>'document' === p.type).map((p)=>({
|
|
119
|
+
filename: p.filename,
|
|
120
|
+
url: p.url || '#'
|
|
121
|
+
}));
|
|
122
|
+
const userMessage = {
|
|
123
|
+
type: 'human',
|
|
124
|
+
content: input.content || '',
|
|
125
|
+
images: uploadedImages.length > 0 ? uploadedImages : void 0,
|
|
126
|
+
documents: uploadedDocs.length > 0 ? uploadedDocs : void 0
|
|
127
|
+
};
|
|
128
|
+
this.updateState({
|
|
129
|
+
messages: [
|
|
130
|
+
...this.state.messages,
|
|
131
|
+
userMessage
|
|
132
|
+
]
|
|
133
|
+
});
|
|
134
|
+
let currentAiMessage = null;
|
|
135
|
+
let currentAiMessageIndex = -1;
|
|
136
|
+
this.abortController = await executeWorkflowSSE(`${this.baseUrl}/workflows/${this.workflowId}/execute`, {
|
|
137
|
+
input: executionInput,
|
|
138
|
+
threadId: this.state.threadId
|
|
139
|
+
}, {
|
|
140
|
+
onWorkflowStart: (data)=>{
|
|
141
|
+
this.updateState({
|
|
142
|
+
threadId: data.t
|
|
143
|
+
});
|
|
144
|
+
this.emit('start', {
|
|
145
|
+
threadId: data.t
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
onNodeStart: (data)=>{
|
|
149
|
+
this.updateState({
|
|
150
|
+
currentNodeId: data.n
|
|
151
|
+
});
|
|
152
|
+
this.emit('nodeStart', {
|
|
153
|
+
nodeId: data.n
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
onNodeEnd: (data)=>{
|
|
157
|
+
if (data.m && data.m.length > 0) {
|
|
158
|
+
const newMessages = [
|
|
159
|
+
...this.state.messages
|
|
160
|
+
];
|
|
161
|
+
for (const msg of data.m)if ('human' !== msg.type) newMessages.push(msg);
|
|
162
|
+
this.updateState({
|
|
163
|
+
messages: newMessages
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
this.emit('nodeEnd', {
|
|
167
|
+
nodeId: data.n,
|
|
168
|
+
variables: data.v,
|
|
169
|
+
messages: data.m
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
onToken: (data)=>{
|
|
173
|
+
if (!currentAiMessage) {
|
|
174
|
+
currentAiMessage = {
|
|
175
|
+
type: 'ai',
|
|
176
|
+
content: ''
|
|
177
|
+
};
|
|
178
|
+
const newMessages = [
|
|
179
|
+
...this.state.messages,
|
|
180
|
+
currentAiMessage
|
|
181
|
+
];
|
|
182
|
+
currentAiMessageIndex = newMessages.length - 1;
|
|
183
|
+
this.updateState({
|
|
184
|
+
messages: newMessages
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
currentAiMessage.content = currentAiMessage.content + data.c;
|
|
188
|
+
const updatedMessages = [
|
|
189
|
+
...this.state.messages
|
|
190
|
+
];
|
|
191
|
+
updatedMessages[currentAiMessageIndex] = {
|
|
192
|
+
...currentAiMessage
|
|
193
|
+
};
|
|
194
|
+
this.updateState({
|
|
195
|
+
messages: updatedMessages
|
|
196
|
+
});
|
|
197
|
+
this.emit('token', {
|
|
198
|
+
nodeId: data.n,
|
|
199
|
+
content: data.c,
|
|
200
|
+
isReasoning: 1 === data.r
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
onLoading: (data)=>{
|
|
204
|
+
this.emit('loading', {
|
|
205
|
+
nodeId: data.n,
|
|
206
|
+
message: data.m
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
onComplete: ()=>{
|
|
210
|
+
this.updateState({
|
|
211
|
+
isExecuting: false,
|
|
212
|
+
currentNodeId: void 0
|
|
213
|
+
});
|
|
214
|
+
this.emit('complete');
|
|
215
|
+
},
|
|
216
|
+
onError: (error)=>{
|
|
217
|
+
this.updateState({
|
|
218
|
+
isExecuting: false,
|
|
219
|
+
currentNodeId: void 0
|
|
220
|
+
});
|
|
221
|
+
this.emit('error', error);
|
|
222
|
+
}
|
|
223
|
+
}, this.headers);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.updateState({
|
|
226
|
+
isExecuting: false,
|
|
227
|
+
currentNodeId: void 0
|
|
228
|
+
});
|
|
229
|
+
this.emit('error', {
|
|
230
|
+
message: String(error)
|
|
231
|
+
});
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
abort() {
|
|
236
|
+
if (this.abortController) {
|
|
237
|
+
this.abortController.abort();
|
|
238
|
+
this.updateState({
|
|
239
|
+
isExecuting: false,
|
|
240
|
+
currentNodeId: void 0
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export { WorkflowSession };
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WorkflowStartData, NodeStartData, NodeEndData, TokenData, LoadingData } from '@our-llm/shared/workflow-events';
|
|
2
|
+
/**
|
|
3
|
+
* SSE 事件回调
|
|
4
|
+
*/
|
|
5
|
+
export interface SSECallbacks {
|
|
6
|
+
onWorkflowStart?: (data: WorkflowStartData) => void;
|
|
7
|
+
onNodeStart?: (data: NodeStartData) => void;
|
|
8
|
+
onNodeEnd?: (data: NodeEndData) => void;
|
|
9
|
+
onToken?: (data: TokenData) => void;
|
|
10
|
+
onLoading?: (data: LoadingData) => void;
|
|
11
|
+
onComplete?: () => void;
|
|
12
|
+
onError?: (error: {
|
|
13
|
+
message: string;
|
|
14
|
+
}) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 执行工作流的 SSE 连接
|
|
18
|
+
*/
|
|
19
|
+
export declare function executeWorkflowSSE(url: string, body: unknown, callbacks: SSECallbacks, headers?: Record<string, string>): Promise<AbortController>;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
|
2
|
+
async function executeWorkflowSSE(url, body, callbacks, headers = {}) {
|
|
3
|
+
const ctrl = new AbortController();
|
|
4
|
+
await fetchEventSource(url, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
...headers
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify(body),
|
|
11
|
+
signal: ctrl.signal,
|
|
12
|
+
onmessage (msg) {
|
|
13
|
+
const eventType = msg.event;
|
|
14
|
+
if ('ok' === eventType) {
|
|
15
|
+
callbacks.onComplete?.();
|
|
16
|
+
ctrl.abort();
|
|
17
|
+
} else if ('err' === eventType) {
|
|
18
|
+
const data = JSON.parse(msg.data);
|
|
19
|
+
callbacks.onError?.({
|
|
20
|
+
message: data.m
|
|
21
|
+
});
|
|
22
|
+
ctrl.abort();
|
|
23
|
+
} else try {
|
|
24
|
+
const data = JSON.parse(msg.data);
|
|
25
|
+
switch(eventType){
|
|
26
|
+
case 'ws':
|
|
27
|
+
callbacks.onWorkflowStart?.(data);
|
|
28
|
+
break;
|
|
29
|
+
case 'ns':
|
|
30
|
+
callbacks.onNodeStart?.(data);
|
|
31
|
+
break;
|
|
32
|
+
case 'ne':
|
|
33
|
+
callbacks.onNodeEnd?.(data);
|
|
34
|
+
break;
|
|
35
|
+
case 'tk':
|
|
36
|
+
callbacks.onToken?.(data);
|
|
37
|
+
break;
|
|
38
|
+
case 'l':
|
|
39
|
+
callbacks.onLoading?.(data);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error('[SDK] Failed to parse SSE data', e);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
onerror (err) {
|
|
47
|
+
callbacks.onError?.({
|
|
48
|
+
message: String(err)
|
|
49
|
+
});
|
|
50
|
+
ctrl.abort();
|
|
51
|
+
throw err;
|
|
52
|
+
},
|
|
53
|
+
onclose () {
|
|
54
|
+
callbacks.onComplete?.();
|
|
55
|
+
ctrl.abort();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return ctrl;
|
|
59
|
+
}
|
|
60
|
+
export { executeWorkflowSSE };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK 配置类型
|
|
3
|
+
*/
|
|
4
|
+
export interface OurLLMClientConfig {
|
|
5
|
+
/** API 服务器地址 */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/** 请求超时时间(毫秒) */
|
|
8
|
+
timeout?: number;
|
|
9
|
+
/** 自定义请求头 */
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
/** 租户密钥 (仅用于调试,禁止在生产环境使用) */
|
|
12
|
+
tenantSecret?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 会话选项
|
|
16
|
+
*/
|
|
17
|
+
export interface SessionOptions {
|
|
18
|
+
/** 已有的线程 ID,用于恢复历史会话 */
|
|
19
|
+
threadId?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 上传参数
|
|
23
|
+
*/
|
|
24
|
+
export interface UploadParams {
|
|
25
|
+
/** 要上传的文件 */
|
|
26
|
+
file: File;
|
|
27
|
+
/** 资源类型 */
|
|
28
|
+
resourceType: 'conversation' | 'workflow' | 'knowledge-base';
|
|
29
|
+
/** 上传进度回调 */
|
|
30
|
+
onProgress?: (percent: number) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 图片压缩选项
|
|
34
|
+
*/
|
|
35
|
+
export interface CompressOptions {
|
|
36
|
+
/** 压缩质量 0-1,默认 0.8 */
|
|
37
|
+
quality?: number;
|
|
38
|
+
/** 最大宽度,默认 1920 */
|
|
39
|
+
maxWidth?: number;
|
|
40
|
+
/** 最大高度,默认 1920 */
|
|
41
|
+
maxHeight?: number;
|
|
42
|
+
/** 超过此大小(字节)的图片会被转换为 JPEG,默认 1MB */
|
|
43
|
+
convertSize?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 发送消息的输入
|
|
47
|
+
*/
|
|
48
|
+
export interface SendInput {
|
|
49
|
+
/** 文本内容 */
|
|
50
|
+
content?: string;
|
|
51
|
+
/** 图片文件列表(会自动压缩并上传) */
|
|
52
|
+
images?: File[];
|
|
53
|
+
/** 已上传的图片 URL 列表 */
|
|
54
|
+
imageUrls?: string[];
|
|
55
|
+
/** 文档文件列表(会自动上传) */
|
|
56
|
+
documents?: File[];
|
|
57
|
+
/** 已上传的文档信息列表 */
|
|
58
|
+
documentInfos?: Array<{
|
|
59
|
+
url: string;
|
|
60
|
+
filename: string;
|
|
61
|
+
mimeType: string;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 上传临时凭证
|
|
66
|
+
*/
|
|
67
|
+
export interface UploadCredentials {
|
|
68
|
+
credentials: {
|
|
69
|
+
tmpSecretId: string;
|
|
70
|
+
tmpSecretKey: string;
|
|
71
|
+
sessionToken: string;
|
|
72
|
+
};
|
|
73
|
+
startTime: number;
|
|
74
|
+
expiredTime: number;
|
|
75
|
+
bucket: string;
|
|
76
|
+
region: string;
|
|
77
|
+
key: string;
|
|
78
|
+
cdnDomain?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 会话事件类型
|
|
82
|
+
*/
|
|
83
|
+
export type SessionEventType = 'start' | 'nodeStart' | 'nodeEnd' | 'token' | 'loading' | 'complete' | 'error' | 'stateChange';
|
|
84
|
+
/**
|
|
85
|
+
* 会话事件监听器类型映射
|
|
86
|
+
*/
|
|
87
|
+
export interface SessionEventListeners {
|
|
88
|
+
start: (data: {
|
|
89
|
+
threadId: string;
|
|
90
|
+
}) => void;
|
|
91
|
+
nodeStart: (data: {
|
|
92
|
+
nodeId: string;
|
|
93
|
+
}) => void;
|
|
94
|
+
nodeEnd: (data: {
|
|
95
|
+
nodeId: string;
|
|
96
|
+
variables?: Record<string, unknown>;
|
|
97
|
+
messages?: import('@our-llm/shared/workflow-events').SimpleMessage[];
|
|
98
|
+
}) => void;
|
|
99
|
+
token: (data: {
|
|
100
|
+
nodeId: string;
|
|
101
|
+
content: string;
|
|
102
|
+
isReasoning?: boolean;
|
|
103
|
+
}) => void;
|
|
104
|
+
loading: (data: {
|
|
105
|
+
nodeId: string;
|
|
106
|
+
message: string;
|
|
107
|
+
}) => void;
|
|
108
|
+
complete: () => void;
|
|
109
|
+
error: (error: {
|
|
110
|
+
message: string;
|
|
111
|
+
}) => void;
|
|
112
|
+
stateChange: (state: SessionState) => void;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 会话状态
|
|
116
|
+
*/
|
|
117
|
+
export interface SessionState {
|
|
118
|
+
/** 线程 ID */
|
|
119
|
+
threadId?: string;
|
|
120
|
+
/** 消息列表 */
|
|
121
|
+
messages: import('@our-llm/shared/workflow-events').SimpleMessage[];
|
|
122
|
+
/** 是否正在执行 */
|
|
123
|
+
isExecuting: boolean;
|
|
124
|
+
/** 当前执行节点 ID */
|
|
125
|
+
currentNodeId?: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 工作流基本信息
|
|
129
|
+
*/
|
|
130
|
+
export interface WorkflowInfo {
|
|
131
|
+
id: string;
|
|
132
|
+
name: string;
|
|
133
|
+
description: string;
|
|
134
|
+
}
|
package/dist/types.js
ADDED
|
File without changes
|
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UploadParams } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 文件上传服务
|
|
4
|
+
*/
|
|
5
|
+
export declare class UploadService {
|
|
6
|
+
private readonly baseUrl;
|
|
7
|
+
private readonly headers;
|
|
8
|
+
constructor(baseUrl: string, headers?: Record<string, string>);
|
|
9
|
+
/**
|
|
10
|
+
* 压缩图片
|
|
11
|
+
*/
|
|
12
|
+
private compressImage;
|
|
13
|
+
/**
|
|
14
|
+
* 获取 COS 实例
|
|
15
|
+
*/
|
|
16
|
+
private getCosInstance;
|
|
17
|
+
/**
|
|
18
|
+
* 上传文件
|
|
19
|
+
*/
|
|
20
|
+
uploadFile(params: UploadParams): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* 上传图片(自动压缩)
|
|
23
|
+
*/
|
|
24
|
+
uploadImage(file: File, resourceType: 'conversation' | 'workflow' | 'knowledge-base', onProgress?: (percent: number) => void): Promise<string>;
|
|
25
|
+
}
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import cos_js_sdk_v5 from "cos-js-sdk-v5";
|
|
2
|
+
import compressorjs from "compressorjs";
|
|
3
|
+
class UploadService {
|
|
4
|
+
baseUrl;
|
|
5
|
+
headers;
|
|
6
|
+
constructor(baseUrl, headers = {}){
|
|
7
|
+
this.baseUrl = baseUrl;
|
|
8
|
+
this.headers = headers;
|
|
9
|
+
}
|
|
10
|
+
compressImage(file, options = {}) {
|
|
11
|
+
const { quality = 0.8, maxWidth = 1920, maxHeight = 1920, convertSize = 1000000 } = options;
|
|
12
|
+
return new Promise((resolve)=>{
|
|
13
|
+
new compressorjs(file, {
|
|
14
|
+
quality,
|
|
15
|
+
maxWidth,
|
|
16
|
+
maxHeight,
|
|
17
|
+
convertSize,
|
|
18
|
+
success: (result)=>{
|
|
19
|
+
let filename = file.name;
|
|
20
|
+
if (result.type !== file.type) {
|
|
21
|
+
const ext = 'image/jpeg' === result.type ? '.jpg' : '.webp';
|
|
22
|
+
filename = file.name.replace(/\.[^.]+$/, ext);
|
|
23
|
+
}
|
|
24
|
+
const compressedFile = new File([
|
|
25
|
+
result
|
|
26
|
+
], filename, {
|
|
27
|
+
type: result.type,
|
|
28
|
+
lastModified: Date.now()
|
|
29
|
+
});
|
|
30
|
+
console.log(`[SDK] 图片压缩: ${file.name} ${(file.size / 1024).toFixed(1)}KB -> ${(compressedFile.size / 1024).toFixed(1)}KB`);
|
|
31
|
+
resolve(compressedFile);
|
|
32
|
+
},
|
|
33
|
+
error: (err)=>{
|
|
34
|
+
console.warn(`[SDK] 图片压缩失败,使用原图: ${file.name}`, err);
|
|
35
|
+
resolve(file);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async getCosInstance(params) {
|
|
41
|
+
const response = await fetch(`${this.baseUrl}/workflows/upload/credentials`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
...this.headers
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify(params)
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) throw new Error('获取上传凭证失败');
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
const cos = new cos_js_sdk_v5({
|
|
52
|
+
getAuthorization: (_options, callback)=>{
|
|
53
|
+
callback({
|
|
54
|
+
TmpSecretId: data.credentials.tmpSecretId,
|
|
55
|
+
TmpSecretKey: data.credentials.tmpSecretKey,
|
|
56
|
+
SecurityToken: data.credentials.sessionToken,
|
|
57
|
+
StartTime: data.startTime,
|
|
58
|
+
ExpiredTime: data.expiredTime
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
cos,
|
|
64
|
+
key: data.key,
|
|
65
|
+
bucket: data.bucket,
|
|
66
|
+
region: data.region,
|
|
67
|
+
cdnDomain: data.cdnDomain
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async uploadFile(params) {
|
|
71
|
+
const { file, resourceType, onProgress } = params;
|
|
72
|
+
const { cos, key, bucket, region, cdnDomain } = await this.getCosInstance({
|
|
73
|
+
resourceType,
|
|
74
|
+
filename: file.name
|
|
75
|
+
});
|
|
76
|
+
return new Promise((resolve, reject)=>{
|
|
77
|
+
cos.putObject({
|
|
78
|
+
Bucket: bucket,
|
|
79
|
+
Region: region,
|
|
80
|
+
Key: key,
|
|
81
|
+
Body: file,
|
|
82
|
+
onProgress: (progressData)=>{
|
|
83
|
+
if (onProgress) onProgress(progressData.percent);
|
|
84
|
+
}
|
|
85
|
+
}, (err, _data)=>{
|
|
86
|
+
if (err) reject(err);
|
|
87
|
+
else {
|
|
88
|
+
const url = cdnDomain ? `https://${cdnDomain}/${key}` : `https://${bucket}.cos.${region}.myqcloud.com/${key}`;
|
|
89
|
+
resolve(url);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async uploadImage(file, resourceType, onProgress) {
|
|
95
|
+
const compressedFile = await this.compressImage(file);
|
|
96
|
+
return this.uploadFile({
|
|
97
|
+
file: compressedFile,
|
|
98
|
+
resourceType,
|
|
99
|
+
onProgress
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export { UploadService };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wtfai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@eslint/js": "^9.39.2",
|
|
17
|
+
"@rsbuild/plugin-react": "^1.4.2",
|
|
18
|
+
"@rslib/core": "^0.19.1",
|
|
19
|
+
"@types/react": "^19.2.7",
|
|
20
|
+
"eslint": "^9.39.2",
|
|
21
|
+
"eslint-config-prettier": "^10.1.8",
|
|
22
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
23
|
+
"globals": "^17.0.0",
|
|
24
|
+
"prettier": "^3.7.4",
|
|
25
|
+
"react": "^19.2.3",
|
|
26
|
+
"typescript": "^5.9.3",
|
|
27
|
+
"typescript-eslint": "^8.51.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=16.9.0",
|
|
31
|
+
"react-dom": ">=16.9.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@microsoft/fetch-event-source": "^2.0.1",
|
|
35
|
+
"compressorjs": "^1.2.1",
|
|
36
|
+
"cos-js-sdk-v5": "^1.10.1",
|
|
37
|
+
"uuid": "^13.0.0",
|
|
38
|
+
"@our-llm/shared": "2.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "rslib build",
|
|
42
|
+
"dev": "rslib build --watch",
|
|
43
|
+
"format": "prettier --write .",
|
|
44
|
+
"lint": "eslint ."
|
|
45
|
+
}
|
|
46
|
+
}
|