wtfai 1.5.1 → 1.5.3

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
@@ -33,13 +33,7 @@ const client = new Wtfai({
33
33
  // 2. 创建会话
34
34
  const session = client.createSession('workflow-id')
35
35
 
36
- // 3. 监听状态变化
37
- session.on('stateChange', (state) => {
38
- console.log('消息列表:', state.messages)
39
- console.log('是否执行中:', state.isExecuting)
40
- })
41
-
42
- // 4. 发送消息
36
+ // 3. 发送消息
43
37
  await session.send({ content: '你好' })
44
38
  ```
45
39
 
@@ -105,12 +99,33 @@ const session = client.createSession('workflow-id', {
105
99
 
106
100
  ##### `on(event, listener)`
107
101
 
108
- 监听事件。
102
+ 监听事件。事件语义与后端 SSE 推送保持一致(见 `docs/sse-event-protocol.md`)。
103
+
104
+ **事件说明与常见用法**
105
+
106
+ - `start`:工作流开始执行。会返回后端确认的 `threadId`;`title` 仅在新会话时返回(默认为工作流名)。
107
+ - 常见用法:首次拿到 `threadId` 后持久化,便于恢复会话。
108
+ - `nodeStart`:某个节点开始执行(排除了 LangGraph 内置的 `__start__/__end__`)。
109
+ - 常见用法:在 UI 上标记「当前节点」,或显示步骤进度。
110
+ - `nodeEnd`:某个节点执行结束,可能包含 `variables` 和/或 `messages`。
111
+ - 常见用法:更新节点级结果或收集中间产物。
112
+ - 注意:后端当前只透传 billing 相关变量;`brain/loop` 节点的消息已在流式 token 中渲染,这里会省略以避免重复。
113
+ - `token`:LLM 流式 token。`isReasoning` 表示推理内容(例如 DeepSeek 的 reasoning_content)。
114
+ - 常见用法:实现打字机效果;区分显示「推理」与「最终输出」。
115
+ - `loading`:仅在 Loading 节点触发,`message` 为配置的提示语(纯字符串)。
116
+ - 常见用法:展示阶段性加载提示。
117
+ - `title`:异步生成的会话标题(仅新会话)。通常在 `start` 之后稍晚到达。
118
+ - 常见用法:更新会话列表标题/面包屑。
119
+ - `complete`:本次执行成功结束。
120
+ - 常见用法:停止输入框 loading、允许再次发送。
121
+ - `error`:执行出错时触发(错误时不会再触发 `complete`)。
122
+ - 常见用法:toast 提示、错误上报、兜底 UI。
109
123
 
110
124
  ```typescript
111
125
  session.on('start', ({ threadId, title }) => {
112
- // 工作流开始执行,获取到 threadId
113
- // title 只有新会话才会有(默认为工作流名称),追问会话不会返回
126
+ // 工作流开始执行,获取到后端确认的 threadId
127
+ // title 只有新会话才会有(默认为工作流名称)
128
+ // 建议持久化 threadId,便于恢复会话
114
129
  })
115
130
 
116
131
  session.on('nodeStart', ({ nodeId }) => {
@@ -119,14 +134,17 @@ session.on('nodeStart', ({ nodeId }) => {
119
134
 
120
135
  session.on('nodeEnd', ({ nodeId, variables, messages }) => {
121
136
  // 节点执行结束
137
+ // variables: 目前只包含 billing 相关变量
138
+ // messages: brain/loop 节点通常已在 token 中渲染,可能为空
122
139
  })
123
140
 
124
141
  session.on('token', ({ nodeId, content, isReasoning }) => {
125
142
  // 收到流式 token(用于打字机效果)
143
+ // isReasoning 为 true 时表示推理内容
126
144
  })
127
145
 
128
146
  session.on('loading', ({ nodeId, message }) => {
129
- // 收到 loading 事件,用于更新加载状态
147
+ // Loading 节点的提示语(纯字符串)
130
148
  console.log('Loading:', message)
131
149
  })
132
150
 
@@ -136,22 +154,22 @@ session.on('title', ({ title }) => {
136
154
  })
137
155
 
138
156
  session.on('complete', () => {
139
- // 工作流执行完成
157
+ // 工作流执行完成(成功)
140
158
  })
141
159
 
142
160
  session.on('error', ({ message }) => {
143
161
  // 发生错误
144
162
  })
145
163
 
146
- session.on('stateChange', (state) => {
147
- // 状态变化(推荐使用此事件更新 UI)
148
- // state.messages: SimpleMessage[]
149
- // state.isExecuting: boolean
150
- // state.threadId?: string
151
- // state.currentNodeId?: string
152
- })
153
164
  ```
154
165
 
166
+ **最佳实践(推荐组合)**
167
+
168
+ 1. **打字机效果**:使用 `token` 拼接流式内容。
169
+ 2. **消息列表**:优先由 `token` 推进 AI 回复;`nodeEnd` 仅用于补齐非 brain/loop 节点的消息。
170
+ 3. **加载状态**:`loading` 展示阶段性提示,`complete/error` 关闭 loading 并解锁输入。
171
+ 4. **会话管理**:在 `start` 保存 `threadId`,`title` 更新会话标题。
172
+
155
173
  ##### `off(event, listener)`
156
174
 
157
175
  移除事件监听。
@@ -235,7 +253,16 @@ const session = client.createSession('workflow-id', {
235
253
  threadId: 'existing-thread-id',
236
254
  })
237
255
  await session.restore()
238
- // 会触发 stateChange 事件,包含历史消息
256
+ // 会将历史消息写入 state,可通过 getState 获取
257
+ ```
258
+
259
+ ##### `updateSessionTitle(title: string)`
260
+
261
+ 更新会话标题(手动修改)。
262
+
263
+ ```typescript
264
+ await session.updateSessionTitle('新的会话标题')
265
+ // 会更新服务端变量,并触发 title 事件
239
266
  ```
240
267
 
241
268
  ##### `getState()`
@@ -256,6 +283,18 @@ console.log(state.isExecuting)
256
283
  session.abort()
257
284
  ```
258
285
 
286
+ ##### `dispose()`
287
+
288
+ 释放会话资源:移除所有事件监听并中止执行,同时清空会话内部状态。被释放的 session 不可再使用。
289
+
290
+ ```typescript
291
+ session.dispose()
292
+ ```
293
+
294
+ **说明**:
295
+ - `abort()` 仅用于中断当前执行(比如用户点击停止生成)
296
+ - `dispose()` 用于彻底清理会话(组件卸载、切换会话时),调用后不可再复用该 session
297
+
259
298
  ##### `setWorkflowVariables(values)`
260
299
 
261
300
  更新服务端会话变量。
@@ -511,6 +550,7 @@ function ChatComponent({ workflowId }: { workflowId: string }) {
511
550
  const [isExecuting, setIsExecuting] = useState(false)
512
551
  const [input, setInput] = useState('')
513
552
  const sessionRef = useRef<WorkflowSession | null>(null)
553
+ const streamingIndexRef = useRef<number | null>(null)
514
554
 
515
555
  useEffect(() => {
516
556
  return () => {
@@ -524,22 +564,54 @@ function ChatComponent({ workflowId }: { workflowId: string }) {
524
564
  const session = client.createSession(workflowId)
525
565
  sessionRef.current = session
526
566
 
527
- session.on('stateChange', (state) => {
528
- setMessages([...state.messages])
529
- setIsExecuting(state.isExecuting)
567
+ session.on('token', ({ content }) => {
568
+ setMessages((prev) => {
569
+ const next = [...prev]
570
+ if (streamingIndexRef.current === null) {
571
+ streamingIndexRef.current = next.length
572
+ next.push({
573
+ type: 'ai',
574
+ parts: [{ type: 'text', text: content }],
575
+ })
576
+ return next
577
+ }
578
+
579
+ const index = streamingIndexRef.current
580
+ const msg = next[index]
581
+ const firstPart = msg?.parts?.[0]
582
+ if (msg && firstPart?.type === 'text') {
583
+ const updatedParts = [...msg.parts]
584
+ updatedParts[0] = {
585
+ ...firstPart,
586
+ text: firstPart.text + content,
587
+ }
588
+ next[index] = { ...msg, parts: updatedParts }
589
+ }
590
+ return next
591
+ })
592
+ })
593
+
594
+ session.on('complete', () => {
595
+ setIsExecuting(false)
596
+ streamingIndexRef.current = null
530
597
  })
531
598
 
532
599
  session.on('error', ({ message }) => {
533
600
  alert(`错误: ${message}`)
601
+ setIsExecuting(false)
602
+ streamingIndexRef.current = null
534
603
  })
535
604
 
536
605
  try {
606
+ setIsExecuting(true)
537
607
  await session.send({
538
608
  parts: [{ type: 'text', text: input }]
539
609
  })
540
610
  setInput('')
541
611
  } catch (error) {
542
612
  console.error(error)
613
+ setIsExecuting(false)
614
+ streamingIndexRef.current = null
543
615
  }
544
616
  }
545
617
 
@@ -548,7 +620,7 @@ function ChatComponent({ workflowId }: { workflowId: string }) {
548
620
  <div className="messages">
549
621
  {messages.map((msg, i) => (
550
622
  <div key={i} className={msg.type}>
551
- {typeof msg.content === 'string' ? msg.content : '...'}
623
+ {msg.parts.find((part) => part.type === 'text')?.text || '...'}
552
624
  </div>
553
625
  ))}
554
626
  {isExecuting && <div>思考中...</div>}
package/dist/client.d.ts CHANGED
@@ -22,11 +22,6 @@ import { UploadService } from './upload';
22
22
  * // 创建会话
23
23
  * const session = client.createSession('workflow-id')
24
24
  *
25
- * // 监听状态变化
26
- * session.on('stateChange', (state) => {
27
- * console.log('Messages:', state.messages)
28
- * })
29
- *
30
25
  * // 发送消息
31
26
  * await session.send({ parts: [{ type: 'text', text: '你好' }] })
32
27
  * ```
package/dist/session.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class WorkflowSession {
13
13
  private state;
14
14
  private listeners;
15
15
  private abortController?;
16
+ private disposed;
16
17
  constructor(workflowId: string, baseUrl: string, uploadService: UploadService, headers?: Record<string, string>, options?: SessionOptions);
17
18
  private iframeMethods;
18
19
  /**
@@ -47,7 +48,7 @@ export declare class WorkflowSession {
47
48
  */
48
49
  private emit;
49
50
  /**
50
- * 更新状态并触发 stateChange 事件
51
+ * 更新会话状态
51
52
  */
52
53
  private updateState;
53
54
  /**
@@ -58,15 +59,17 @@ export declare class WorkflowSession {
58
59
  * 恢复历史会话
59
60
  */
60
61
  restore(): Promise<void>;
61
- /**
62
- * 获取工作流变量
63
- */
64
62
  getWorkflowVariables(): Promise<Record<string, any>>;
65
63
  /**
66
64
  * 更新会话状态(服务端变量)
67
65
  * @param values 需要更新的变量键值对
68
66
  */
69
67
  setWorkflowVariables(values: Record<string, unknown>): Promise<void>;
68
+ /**
69
+ * 更新会话标题
70
+ * @param title 新的标题
71
+ */
72
+ updateSessionTitle(title: string): Promise<void>;
70
73
  /**
71
74
  * 执行指定的工作流(临时/嵌套执行)
72
75
  * 不会影响当前会话的状态(messages 等)
@@ -98,4 +101,12 @@ export declare class WorkflowSession {
98
101
  * 中止执行
99
102
  */
100
103
  abort(): void;
104
+ /**
105
+ * 释放会话资源(移除监听并中止执行)
106
+ */
107
+ dispose(): void;
108
+ /**
109
+ * 确保会话未被释放
110
+ */
111
+ private assertNotDisposed;
101
112
  }
package/dist/session.js CHANGED
@@ -12,6 +12,7 @@ function _define_property(obj, key, value) {
12
12
  }
13
13
  class WorkflowSession {
14
14
  registerIframeMethods(methods) {
15
+ this.assertNotDisposed();
15
16
  this.iframeMethods = {
16
17
  ...this.iframeMethods,
17
18
  ...methods
@@ -19,9 +20,11 @@ class WorkflowSession {
19
20
  return this;
20
21
  }
21
22
  getIframeMethods() {
23
+ this.assertNotDisposed();
22
24
  return this.iframeMethods;
23
25
  }
24
26
  async uploadFileFromIframe(options) {
27
+ this.assertNotDisposed();
25
28
  const { base64, filename, mimeType, resourceType = 'conversation' } = options;
26
29
  const byteString = atob(base64);
27
30
  const arrayBuffer = new ArrayBuffer(byteString.length);
@@ -47,16 +50,19 @@ class WorkflowSession {
47
50
  });
48
51
  }
49
52
  on(event, listener) {
53
+ this.assertNotDisposed();
50
54
  if (!this.listeners.has(event)) this.listeners.set(event, new Set());
51
55
  this.listeners.get(event).add(listener);
52
56
  return this;
53
57
  }
54
58
  off(event, listener) {
55
59
  var _this_listeners_get;
60
+ this.assertNotDisposed();
56
61
  null == (_this_listeners_get = this.listeners.get(event)) || _this_listeners_get.delete(listener);
57
62
  return this;
58
63
  }
59
64
  emit(event, ...args) {
65
+ if (this.disposed) return;
60
66
  const listeners = this.listeners.get(event);
61
67
  if (listeners) Array.from(listeners).forEach((listener)=>{
62
68
  listener(...args);
@@ -67,27 +73,29 @@ class WorkflowSession {
67
73
  ...this.state,
68
74
  ...updates
69
75
  };
70
- this.emit('stateChange', {
71
- ...this.state
72
- });
73
76
  }
74
77
  getState() {
78
+ this.assertNotDisposed();
75
79
  return {
76
80
  ...this.state
77
81
  };
78
82
  }
79
83
  async restore() {
84
+ this.assertNotDisposed();
80
85
  if (!this.state.threadId) throw new Error('无法恢复会话:未提供 threadId');
81
86
  const response = await fetch(`${this.baseUrl}/workflows/${this.workflowId}/state?threadId=${this.state.threadId}`, {
82
87
  headers: this.headers
83
88
  });
84
89
  if (!response.ok) throw new Error('获取工作流状态失败');
85
90
  const data = await response.json();
91
+ const variables = data.variables || {};
86
92
  this.updateState({
87
- messages: data.messages
93
+ messages: data.messages,
94
+ title: variables.conversationTitle
88
95
  });
89
96
  }
90
97
  async getWorkflowVariables() {
98
+ this.assertNotDisposed();
91
99
  if (!this.state.threadId) throw new Error('No active session (missing threadId)');
92
100
  const response = await fetch(`${this.baseUrl}/workflows/${this.workflowId}/state?threadId=${this.state.threadId}`, {
93
101
  headers: this.headers
@@ -97,7 +105,8 @@ class WorkflowSession {
97
105
  return data.variables || {};
98
106
  }
99
107
  async setWorkflowVariables(values) {
100
- if (!this.state.threadId || this.state.threadId.startsWith('tmp_')) throw new Error('No active session (missing valid threadId)');
108
+ this.assertNotDisposed();
109
+ if (!this.state.threadId) throw new Error('No active session (missing valid threadId)');
101
110
  const response = await fetch(`${this.baseUrl}/workflows/${this.workflowId}/variables`, {
102
111
  method: 'POST',
103
112
  headers: {
@@ -111,7 +120,43 @@ class WorkflowSession {
111
120
  });
112
121
  if (!response.ok) throw new Error('Failed to update state');
113
122
  }
123
+ async updateSessionTitle(title) {
124
+ this.assertNotDisposed();
125
+ if (!this.state.threadId) throw new Error('No active session (missing valid threadId)');
126
+ const executionInput = {
127
+ messages: [],
128
+ variables: {
129
+ conversationTitle: title
130
+ }
131
+ };
132
+ return new Promise((resolve, reject)=>{
133
+ executeWorkflowSSE(`${this.baseUrl}/workflows/${this.workflowId}/execute`, {
134
+ input: executionInput,
135
+ threadId: this.state.threadId,
136
+ mode: 'title_update'
137
+ }, {
138
+ onTitle: (data)=>{
139
+ this.updateState({
140
+ title: data.ti
141
+ });
142
+ this.emit('title', {
143
+ title: data.ti
144
+ });
145
+ },
146
+ onComplete: ()=>{
147
+ this.updateState({
148
+ title
149
+ });
150
+ resolve();
151
+ },
152
+ onError: (err)=>{
153
+ reject(new Error(err.message));
154
+ }
155
+ }, this.headers).catch(reject);
156
+ });
157
+ }
114
158
  async executeWorkflow(workflowId, input) {
159
+ this.assertNotDisposed();
115
160
  const { parts: inputParts, ...rest } = input;
116
161
  const parts = [];
117
162
  for (const part of inputParts)switch(part.type){
@@ -190,6 +235,7 @@ class WorkflowSession {
190
235
  });
191
236
  }
192
237
  async send({ parts: inputParts, ...rest }) {
238
+ this.assertNotDisposed();
193
239
  if (this.state.isExecuting) throw new Error('工作流正在执行中');
194
240
  this.updateState({
195
241
  isExecuting: true,
@@ -271,6 +317,9 @@ class WorkflowSession {
271
317
  this.updateState({
272
318
  threadId: data.t
273
319
  });
320
+ if (data.ti) this.updateState({
321
+ title: data.ti
322
+ });
274
323
  this.emit('start', {
275
324
  threadId: data.t,
276
325
  ...data.ti && {
@@ -346,6 +395,9 @@ class WorkflowSession {
346
395
  });
347
396
  },
348
397
  onTitle: (data)=>{
398
+ this.updateState({
399
+ title: data.ti
400
+ });
349
401
  this.emit('title', {
350
402
  title: data.ti
351
403
  });
@@ -431,6 +483,7 @@ class WorkflowSession {
431
483
  throw new Error('Document part must have either file or url');
432
484
  }
433
485
  abort() {
486
+ if (this.disposed) return;
434
487
  if (this.abortController) {
435
488
  this.abortController.abort();
436
489
  this.updateState({
@@ -439,6 +492,21 @@ class WorkflowSession {
439
492
  });
440
493
  }
441
494
  }
495
+ dispose() {
496
+ if (this.disposed) return;
497
+ this.disposed = true;
498
+ this.abort();
499
+ this.listeners.clear();
500
+ this.iframeMethods = {};
501
+ this.abortController = void 0;
502
+ this.state = {
503
+ messages: [],
504
+ isExecuting: false
505
+ };
506
+ }
507
+ assertNotDisposed() {
508
+ if (this.disposed) throw new Error('Session has been disposed');
509
+ }
442
510
  constructor(workflowId, baseUrl, uploadService, headers = {}, options = {}){
443
511
  _define_property(this, "workflowId", void 0);
444
512
  _define_property(this, "baseUrl", void 0);
@@ -447,6 +515,7 @@ class WorkflowSession {
447
515
  _define_property(this, "state", void 0);
448
516
  _define_property(this, "listeners", void 0);
449
517
  _define_property(this, "abortController", void 0);
518
+ _define_property(this, "disposed", void 0);
450
519
  _define_property(this, "iframeMethods", void 0);
451
520
  this.workflowId = workflowId;
452
521
  this.baseUrl = baseUrl;
@@ -457,6 +526,7 @@ class WorkflowSession {
457
526
  isExecuting: false
458
527
  };
459
528
  this.listeners = new Map();
529
+ this.disposed = false;
460
530
  this.iframeMethods = {};
461
531
  if (options.threadId) this.state.threadId = options.threadId;
462
532
  else this.state.threadId = v7();
package/dist/types.d.ts CHANGED
@@ -122,7 +122,7 @@ export interface UploadCredentials {
122
122
  /**
123
123
  * 会话事件类型
124
124
  */
125
- export type SessionEventType = 'start' | 'nodeStart' | 'nodeEnd' | 'token' | 'loading' | 'complete' | 'error' | 'stateChange';
125
+ export type SessionEventType = 'start' | 'nodeStart' | 'nodeEnd' | 'token' | 'loading' | 'complete' | 'error';
126
126
  /**
127
127
  * 会话事件监听器类型映射
128
128
  */
@@ -155,7 +155,6 @@ export interface SessionEventListeners {
155
155
  error: (error: {
156
156
  message: string;
157
157
  }) => void;
158
- stateChange: (state: SessionState) => void;
159
158
  }
160
159
  /**
161
160
  * 会话状态
@@ -169,6 +168,8 @@ export interface SessionState {
169
168
  isExecuting: boolean;
170
169
  /** 当前执行节点 ID */
171
170
  currentNodeId?: string;
171
+ /** 会话标题 */
172
+ title?: string;
172
173
  }
173
174
  /**
174
175
  * 工作流基本信息
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtfai",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -16,23 +16,23 @@
16
16
  "@eslint/js": "^9.39.2",
17
17
  "@rsbuild/plugin-react": "^1.4.3",
18
18
  "@rslib/core": "^0.19.3",
19
- "@types/react": "^19.2.9",
19
+ "@types/react": "^19.2.10",
20
20
  "eslint": "^9.39.2",
21
21
  "eslint-config-prettier": "^10.1.8",
22
22
  "eslint-plugin-prettier": "^5.5.5",
23
- "globals": "^17.1.0",
23
+ "globals": "^17.2.0",
24
24
  "prettier": "^3.8.1",
25
- "react": "^19.2.3",
25
+ "react": "^19.2.4",
26
26
  "typescript": "^5.9.3",
27
- "typescript-eslint": "^8.53.1"
27
+ "typescript-eslint": "^8.54.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "react": ">=16.9.0",
31
31
  "react-dom": ">=16.9.0"
32
32
  },
33
33
  "dependencies": {
34
- "@ant-design/x": "^2.1.3",
35
- "@ant-design/x-markdown": "^2.1.3",
34
+ "@ant-design/x": "^2.2.0",
35
+ "@ant-design/x-markdown": "^2.2.0",
36
36
  "@microsoft/fetch-event-source": "^2.0.1",
37
37
  "clsx": "^2.1.1",
38
38
  "compressorjs": "^1.2.1",