wtfai 1.5.7 → 1.6.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
@@ -34,7 +34,7 @@ const client = new Wtfai({
34
34
  const session = client.createSession('workflow-id')
35
35
 
36
36
  // 3. 发送消息
37
- await session.send({ content: '你好' })
37
+ await session.send({ parts: [{ type: 'text', text: '你好' }] })
38
38
  ```
39
39
 
40
40
  ## API 文档
@@ -161,8 +161,64 @@ session.on('error', ({ message }) => {
161
161
  // 发生错误
162
162
  })
163
163
 
164
+ session.on('contentAction', ({ type, payload }) => {
165
+ // 内容区域(如 Markdown)触发的自定义动作
166
+ if (type === 'workflow') {
167
+ const { workflowId, params } = payload
168
+ console.log('跳转工作流:', workflowId, '参数:', params)
169
+ }
170
+ })
164
171
  ```
165
172
 
173
+ **Markdown 交互 (自定义协议)**
174
+
175
+ SDK 的 Markdown 组件支持通过特殊协议触发 `contentAction` 事件,方便在内容中嵌入交互按钮或链接:
176
+
177
+ - **跳转工作流**:使用 `workflow:` 协议。
178
+ 格式:`[文字](workflow:工作流ID?参数1=值1&参数2=值2)`
179
+ 示例:`[查看详情](workflow:cm0p1234?from=chat)`
180
+
181
+ 当用户点击此类链接时,SDK 不会执行页面跳转,而是触发 `contentAction` 事件,由宿主应用决定如何处理(如切换侧边栏、打开弹窗等)。
182
+
183
+ ---
184
+
185
+ ### 结构化内容处理 (WorkflowRegistry)
186
+
187
+ 为了在聊天内容中更美观地展示工作流引用,SDK 提供了 `<workflow>` 自定义标签和全局注册机制。
188
+
189
+ #### 1. 全局注册工作流信息
190
+
191
+ 在应用顶层(或页面容器层)使用 `WorkflowRegistry` 注入工作流的展示名称和图标:
192
+
193
+ ```tsx
194
+ import { WorkflowRegistry } from 'wtfai';
195
+ import { RobotOutlined } from '@ant-design/icons';
196
+
197
+ const MY_WORKFLOWS = {
198
+ 'wf_week_report': {
199
+ name: '自动化周报',
200
+ icon: <RobotOutlined />
201
+ },
202
+ };
203
+
204
+ // 在容器层包裹一次即可
205
+ <WorkflowRegistry mapping={MY_WORKFLOWS}>
206
+ <App />
207
+ </WorkflowRegistry>
208
+ ```
209
+
210
+ #### 2. 在 Markdown 中使用
211
+
212
+ 通过自定义标签 `<workflow id="xxx" />` 引用工作流:
213
+
214
+ - **基本用法**:`<workflow id="wf_week_report" />`
215
+ - **自定义文字**:`<workflow id="wf_week_report">立即查看</workflow>`
216
+
217
+ **渲染效果**:
218
+ SDK 会自动将其渲染为一个美观的“药丸状”卡片按钮,包含图标和名称。如果 `WorkflowRegistry` 中没有匹配到 ID,则会降级显示标签内容或 ID。
219
+
220
+ 点击该卡片会触发 `contentAction` 事件,`type` 为 `'workflow'`。
221
+
166
222
  **最佳实践(推荐组合)**
167
223
 
168
224
  1. **打字机效果**:使用 `token` 拼接流式内容。
@@ -442,6 +498,128 @@ const url = await IframeBridge.uploadFile(file, {
442
498
 
443
499
  ---
444
500
 
501
+ ### Session Data CRUD API
502
+
503
+ 用于会话级数据的持久化存储,与 `saveData/loadData` 不同,这是一套完整的 CRUD 接口,支持集合(Collection)概念,适合存储结构化数据。
504
+
505
+ > **与 saveData/loadData 的区别**:
506
+ > - `saveData/loadData` 存储在工作流 state 中(适合简单 key-value)
507
+ > - Session Data API 存储在独立数据表中(适合结构化数据、列表数据)
508
+
509
+ #### `IframeBridge.createRecord(collection, data)`
510
+
511
+ 在指定集合中创建一条记录。
512
+
513
+ ```javascript
514
+ const id = await IframeBridge.createRecord('notes', {
515
+ title: 'My Note',
516
+ content: 'Hello World',
517
+ createdAt: Date.now()
518
+ });
519
+ console.log('创建成功, ID:', id);
520
+ ```
521
+
522
+ #### `IframeBridge.getRecord(collection, id)`
523
+
524
+ 获取单条记录。
525
+
526
+ ```javascript
527
+ const data = await IframeBridge.getRecord('notes', id);
528
+ if (data) {
529
+ console.log('记录内容:', data.title, data.content);
530
+ } else {
531
+ console.log('记录不存在');
532
+ }
533
+ ```
534
+
535
+ #### `IframeBridge.updateRecord(collection, id, data)`
536
+
537
+ 更新记录(完整替换)。
538
+
539
+ ```javascript
540
+ await IframeBridge.updateRecord('notes', id, {
541
+ title: 'Updated Title',
542
+ content: 'New content',
543
+ updatedAt: Date.now()
544
+ });
545
+ ```
546
+
547
+ #### `IframeBridge.deleteRecord(collection, id)`
548
+
549
+ 删除单条记录。
550
+
551
+ ```javascript
552
+ await IframeBridge.deleteRecord('notes', id);
553
+ ```
554
+
555
+ #### `IframeBridge.listRecords(collection, options?)`
556
+
557
+ 分页查询集合中的记录。
558
+
559
+ ```javascript
560
+ const result = await IframeBridge.listRecords('notes', {
561
+ page: 1,
562
+ pageSize: 10
563
+ });
564
+ console.log(`共 ${result.total} 条记录`);
565
+ result.records.forEach(r => {
566
+ console.log(r.id, r.data.title);
567
+ });
568
+ ```
569
+
570
+ #### `IframeBridge.clearCollection(collection)`
571
+
572
+ 清空集合中的所有记录。
573
+
574
+ ```javascript
575
+ const count = await IframeBridge.clearCollection('notes');
576
+ console.log(`已删除 ${count} 条记录`);
577
+ ```
578
+
579
+ **使用场景**:
580
+ - 待办事项列表
581
+ - 用户收藏/历史记录
582
+ - 表单草稿
583
+ - 游戏存档
584
+
585
+ ---
586
+
587
+ ## HTTP API
588
+
589
+ 以下 API 可直接通过 HTTP 调用,无需使用 SDK。
590
+
591
+ ### 中断工作流执行
592
+
593
+ 中断正在运行的工作流。工作流会返回 `error` 事件携带中断原因后关闭连接。
594
+
595
+ **请求**
596
+
597
+ ```http
598
+ POST /workflows/:workflowId/interrupt
599
+ Content-Type: application/json
600
+
601
+ {
602
+ "threadId": "会话ID",
603
+ "reason": "中断原因"
604
+ }
605
+ ```
606
+
607
+ **响应**
608
+
609
+ ```json
610
+ {
611
+ "success": true,
612
+ "local": true // true=本地实例中断, false=通过集群广播
613
+ }
614
+ ```
615
+
616
+ **说明**:
617
+ - 支持集群模式,会通过 Redis Pub/Sub 广播中断信号到所有实例
618
+ - 工作流被中断后会触发 `error` 事件,`message` 为传入的 `reason`
619
+ - 如果找不到对应的执行会话,返回 404
620
+
621
+ ---
622
+
445
623
  ### UploadService
446
624
 
447
625
  文件上传服务(通过 `client.upload` 访问)。
@@ -97,6 +97,7 @@ class IframeBridgeHost {
97
97
  'executeWorkflow',
98
98
  'uploadFile',
99
99
  'updateSessionTitle',
100
+ 'getSessionId',
100
101
  'createRecord',
101
102
  'getRecord',
102
103
  'updateRecord',
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Wtfai } from './client';
2
2
  export default Wtfai;
3
3
  export { WorkflowSession } from './session';
4
4
  export { UploadService } from './upload';
5
- export type { ClientConfig, SessionOptions, SessionState, SessionEventListeners, SendInput, UploadParams, UploadImageParams, CompressOptions, WorkflowInfo, ContentPart, } from './types';
5
+ export type { ClientConfig, SessionOptions, SessionState, SessionEventListeners, SendInput, UploadParams, UploadImageParams, CompressOptions, WorkflowInfo, ContentPart, ContentAction, } from './types';
6
6
  export type { SimpleMessage, Workflow, WorkflowConfig, } from '@our-llm/shared/types';
7
- export { Markdown, type MarkdownProps } from './ui/markdown';
7
+ export { Markdown, type MarkdownProps, WorkflowRegistry } from './ui/markdown';
8
+ export type { WorkflowInfoMapping } from './ui/context';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Wtfai } from "./client.js";
2
2
  import { WorkflowSession } from "./session.js";
3
3
  import { UploadService } from "./upload.js";
4
- import { Markdown } from "./ui/markdown.js";
4
+ import { Markdown, WorkflowRegistry } from "./ui/markdown.js";
5
5
  const src = Wtfai;
6
- export { Markdown, UploadService, WorkflowSession, src as default };
6
+ export { Markdown, UploadService, WorkflowRegistry, WorkflowSession, src as default };
package/dist/session.d.ts CHANGED
@@ -44,8 +44,13 @@ export declare class WorkflowSession {
44
44
  * 移除事件监听
45
45
  */
46
46
  off<K extends keyof SessionEventListeners>(event: K, listener: SessionEventListeners[K]): this;
47
+ /**
48
+ * 触发内容区域(如 Markdown)产生的交互动作
49
+ */
50
+ emitContentAction(action: import('./types').ContentAction): void;
47
51
  /**
48
52
  * 触发事件
53
+ * @internal
49
54
  */
50
55
  private emit;
51
56
  /**
package/dist/session.js CHANGED
@@ -64,6 +64,9 @@ class WorkflowSession {
64
64
  null == (_this_listeners_get = this.listeners.get(event)) || _this_listeners_get.delete(listener);
65
65
  return this;
66
66
  }
67
+ emitContentAction(action) {
68
+ this.emit('contentAction', action);
69
+ }
67
70
  emit(event, ...args) {
68
71
  if (this.disposed) return;
69
72
  const listeners = this.listeners.get(event);
package/dist/types.d.ts CHANGED
@@ -155,7 +155,21 @@ export interface SessionEventListeners {
155
155
  error: (error: {
156
156
  message: string;
157
157
  }) => void;
158
+ contentAction: (action: ContentAction) => void;
158
159
  }
160
+ /**
161
+ * 内容交互动作联合类型
162
+ */
163
+ export type ContentAction = {
164
+ /** 跳转工作流 */
165
+ type: 'workflow';
166
+ payload: {
167
+ workflowId: string;
168
+ /** 暂时没用上 */
169
+ params?: Record<string, string>;
170
+ extra?: Record<string, any>;
171
+ };
172
+ };
159
173
  /**
160
174
  * 会话状态
161
175
  */
@@ -1,3 +1,11 @@
1
1
  import type { WorkflowSession } from '../session';
2
+ export interface WorkflowInfoMapping {
3
+ name: string;
4
+ icon?: React.ReactNode;
5
+ /** 额外的信息,会在点击的时候透传给调用方,方便使用 */
6
+ extra?: Record<string, any>;
7
+ }
2
8
  export declare const SessionContext: import("react").Context<WorkflowSession | null>;
9
+ export declare const WorkflowRegistryContext: import("react").Context<Record<string, WorkflowInfoMapping>>;
3
10
  export declare const useWorkflowSession: () => WorkflowSession | null;
11
+ export declare const useWorkflowRegistry: () => Record<string, WorkflowInfoMapping>;
@@ -1,4 +1,6 @@
1
1
  import { createContext, useContext } from "react";
2
2
  const SessionContext = createContext(null);
3
+ const WorkflowRegistryContext = createContext({});
3
4
  const useWorkflowSession = ()=>useContext(SessionContext);
4
- export { SessionContext, useWorkflowSession };
5
+ const useWorkflowRegistry = ()=>useContext(WorkflowRegistryContext);
6
+ export { SessionContext, WorkflowRegistryContext, useWorkflowRegistry, useWorkflowSession };
@@ -0,0 +1,14 @@
1
+ .workflow-link {
2
+ cursor: pointer;
3
+ align-items: center;
4
+ display: inline-flex;
5
+ }
6
+
7
+ .workflow-link-icon {
8
+ margin-right: 4px;
9
+ }
10
+
11
+ .workflow-link-icon img {
12
+ margin: 0;
13
+ }
14
+
@@ -1,8 +1,16 @@
1
1
  import { XMarkdownProps } from '@ant-design/x-markdown';
2
2
  import '@ant-design/x-markdown/themes/light.css';
3
+ import './markdown.css';
4
+ import { WorkflowInfoMapping } from './context';
3
5
  import type { WorkflowSession } from '../session';
4
6
  export interface MarkdownProps extends XMarkdownProps {
5
7
  session?: WorkflowSession;
6
8
  }
7
- declare const Markdown: ({ className, streaming, config, components, session, ...props }: MarkdownProps) => import("react/jsx-runtime").JSX.Element;
8
- export { Markdown };
9
+ /**
10
+ * 工作流信息注册表提供者
11
+ */
12
+ export declare const WorkflowRegistry: ({ children, mapping, }: {
13
+ children: React.ReactNode;
14
+ mapping: Record<string, WorkflowInfoMapping>;
15
+ }) => import("react/jsx-runtime").JSX.Element;
16
+ export declare const Markdown: ({ className, streaming, config, components, session, ...props }: MarkdownProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,5 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Children } from "react";
2
3
  import { XMarkdown } from "@ant-design/x-markdown";
3
4
  import "@ant-design/x-markdown/themes/light.css";
4
5
  import Latex from "@ant-design/x-markdown/plugins/Latex";
@@ -6,8 +7,25 @@ import zh_CN from "@ant-design/x/locale/zh_CN";
6
7
  import clsx from "clsx";
7
8
  import { Code } from "./code.js";
8
9
  import { XProvider } from "@ant-design/x";
9
- import { SessionContext } from "./context.js";
10
- const Markdown = ({ className, streaming, config, components, session, ...props })=>/*#__PURE__*/ jsx(XProvider, {
10
+ import "./markdown.css";
11
+ import { SessionContext, WorkflowRegistryContext, useWorkflowRegistry } from "./context.js";
12
+ const WorkflowRegistry = ({ children, mapping })=>{
13
+ const parentRegistry = useWorkflowRegistry();
14
+ const mergedRegistry = {
15
+ ...parentRegistry,
16
+ ...mapping
17
+ };
18
+ return /*#__PURE__*/ jsx(SessionContext.Provider, {
19
+ value: null,
20
+ children: /*#__PURE__*/ jsx(WorkflowRegistryContext.Provider, {
21
+ value: mergedRegistry,
22
+ children: children
23
+ })
24
+ });
25
+ };
26
+ const Markdown = ({ className, streaming, config, components, session, ...props })=>{
27
+ const globalRegistry = useWorkflowRegistry();
28
+ return /*#__PURE__*/ jsx(XProvider, {
11
29
  locale: zh_CN,
12
30
  children: /*#__PURE__*/ jsx(SessionContext.Provider, {
13
31
  value: session || null,
@@ -23,10 +41,37 @@ const Markdown = ({ className, streaming, config, components, session, ...props
23
41
  },
24
42
  components: {
25
43
  ...components,
26
- code: Code
44
+ code: Code,
45
+ workflow: (props)=>{
46
+ const { id, children } = props;
47
+ const info = id ? globalRegistry[id] : void 0;
48
+ const displayContent = (null == info ? void 0 : info.name) || (Children.count(children) > 0 ? children : id);
49
+ return /*#__PURE__*/ jsxs("a", {
50
+ onClick: (e)=>{
51
+ e.preventDefault();
52
+ e.stopPropagation();
53
+ null == session || session.emitContentAction({
54
+ type: 'workflow',
55
+ payload: {
56
+ workflowId: id,
57
+ extra: null == info ? void 0 : info.extra
58
+ }
59
+ });
60
+ },
61
+ className: "workflow-link",
62
+ children: [
63
+ (null == info ? void 0 : info.icon) && /*#__PURE__*/ jsx("span", {
64
+ className: "workflow-link-icon",
65
+ children: info.icon
66
+ }),
67
+ displayContent
68
+ ]
69
+ });
70
+ }
27
71
  },
28
72
  ...props
29
73
  })
30
74
  })
31
75
  });
32
- export { Markdown };
76
+ };
77
+ export { Markdown, WorkflowRegistry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtfai",
3
- "version": "1.5.7",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -13,26 +13,26 @@
13
13
  "dist"
14
14
  ],
15
15
  "devDependencies": {
16
- "@eslint/js": "^9.39.2",
16
+ "@eslint/js": "^9.39.3",
17
17
  "@rsbuild/plugin-react": "^1.4.5",
18
- "@rslib/core": "^0.19.5",
19
- "@types/react": "^19.2.13",
20
- "eslint": "^9.39.2",
18
+ "@rslib/core": "^0.19.6",
19
+ "@types/react": "^19.2.14",
20
+ "eslint": "^9.39.3",
21
21
  "eslint-config-prettier": "^10.1.8",
22
22
  "eslint-plugin-prettier": "^5.5.5",
23
- "globals": "^17.3.0",
23
+ "globals": "^17.4.0",
24
24
  "prettier": "^3.8.1",
25
25
  "react": "^19.2.4",
26
26
  "typescript": "^5.9.3",
27
- "typescript-eslint": "^8.54.0"
27
+ "typescript-eslint": "^8.56.1"
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.2.2",
35
- "@ant-design/x-markdown": "^2.2.2",
34
+ "@ant-design/x": "^2.3.0",
35
+ "@ant-design/x-markdown": "^2.3.0",
36
36
  "@microsoft/fetch-event-source": "^2.0.1",
37
37
  "clsx": "^2.1.1",
38
38
  "compressorjs": "^1.2.1",