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 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()` 避免内存泄漏
@@ -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 };
@@ -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,5 @@
1
+ import { Wtfai } from "./client.js";
2
+ import { WorkflowSession } from "./session.js";
3
+ import { UploadService } from "./upload.js";
4
+ const src = Wtfai;
5
+ export { UploadService, WorkflowSession, src as default };
@@ -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
+ }
@@ -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 };
@@ -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
@@ -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
+ }