wtfai 1.4.4 → 1.5.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
@@ -256,6 +256,143 @@ console.log(state.isExecuting)
256
256
  session.abort()
257
257
  ```
258
258
 
259
+ ##### `setWorkflowVariables(values)`
260
+
261
+ 更新服务端会话变量。
262
+
263
+ ```typescript
264
+ await session.setWorkflowVariables({
265
+ key: 'value',
266
+ userPreference: { theme: 'dark' }
267
+ })
268
+ ```
269
+
270
+ ##### `getWorkflowVariables()`
271
+
272
+ 获取服务端会话变量。
273
+
274
+ ```typescript
275
+ const vars = await session.getWorkflowVariables()
276
+ console.log(vars.userPreference)
277
+ ```
278
+
279
+ ##### `registerIframeMethods(methods)`
280
+
281
+ 注册可供 Iframe (Guest) 调用的方法。
282
+
283
+ ```typescript
284
+ session.registerIframeMethods({
285
+ // 定义一个获取用户信息的方法
286
+ getUserInfo: async () => {
287
+ return { name: 'Alice', id: 123 }
288
+ },
289
+
290
+ // 定义一个打开弹窗的方法
291
+ openModal: (type) => {
292
+ setModalVisible(type)
293
+ }
294
+ })
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Iframe SDK (Guest Side)
300
+
301
+ 我们在 `packages/sdk/iframe-sdk.js` 提供了轻量级的 SDK,供嵌入的 HTML 工具使用。
302
+
303
+ 该 SDK 不需要构建工具,直接通过 `<script>` 标签引入即可。
304
+
305
+ ### 引入 SDK
306
+
307
+ ```html
308
+ <script src="/iframe-sdk.js"></script>
309
+ ```
310
+
311
+ ### 使用方法
312
+
313
+ SDK 会在全局挂载 `IframeBridge` 对象。
314
+
315
+ #### `IframeBridge.call(method, params?)`
316
+
317
+ 调用宿主 (Host) 方法。
318
+
319
+ ```javascript
320
+ // 调用自定义方法
321
+ const userInfo = await IframeBridge.call('getUserInfo');
322
+
323
+ // 调用宿主方法并传参
324
+ await IframeBridge.call('log', ['Hello from iframe']);
325
+ ```
326
+
327
+ #### `IframeBridge.hasMethod(methodName)`
328
+
329
+ 检查宿主是否支持某个方法。
330
+
331
+ ```javascript
332
+ const supportsLog = await IframeBridge.hasMethod('log');
333
+ if (supportsLog) {
334
+ // ...
335
+ }
336
+ ```
337
+
338
+
339
+ #### `IframeBridge.saveData(key, value)`
340
+
341
+ 持久化存储数据(存储在当前工作流会话的 variables 中)。
342
+
343
+ ```javascript
344
+ await IframeBridge.saveData('draft', { content: '...' });
345
+ ```
346
+
347
+ #### `IframeBridge.loadData(key)`
348
+
349
+ 读取持久化数据。
350
+
351
+ ```javascript
352
+ const draft = await IframeBridge.loadData('draft');
353
+ ```
354
+
355
+ #### `IframeBridge.executeWorkflow(workflowId, input)`
356
+
357
+ 临时/嵌套执行另一个工作流。
358
+
359
+ ```javascript
360
+ const result = await IframeBridge.executeWorkflow('target-workflow-id', {
361
+ parts: [
362
+ { type: 'image', url: 'https://...' }
363
+ ]
364
+ });
365
+ console.log(result); // 执行产生的所有消息
366
+ ```
367
+
368
+ #### `IframeBridge.uploadFile(file, options?)`
369
+
370
+ 上传文件到云存储,返回 CDN URL。
371
+
372
+ ```javascript
373
+ // 从 file input 上传
374
+ const fileInput = document.querySelector('input[type="file"]');
375
+ const url = await IframeBridge.uploadFile(fileInput.files[0]);
376
+ console.log('文件 URL:', url);
377
+
378
+ // 从 canvas 上传截图
379
+ canvas.toBlob(async (blob) => {
380
+ const file = new File([blob], 'screenshot.png', { type: 'image/png' });
381
+ const url = await IframeBridge.uploadFile(file);
382
+ console.log('截图 URL:', url);
383
+ });
384
+
385
+ // 指定资源类型
386
+ const url = await IframeBridge.uploadFile(file, {
387
+ resourceType: 'conversation' // 'conversation' | 'workflow' | 'knowledge-base'
388
+ });
389
+ ```
390
+
391
+ **说明**:
392
+ - 图片类型会自动压缩到 1920px 以内
393
+ - 音视频文件会自动获取时长信息
394
+ - 返回的是可直接访问的 CDN URL
395
+
259
396
  ---
260
397
 
261
398
  ### UploadService
@@ -0,0 +1,33 @@
1
+ import type { WorkflowSession } from './session';
2
+ /**
3
+ * Host-side Iframe Bridge
4
+ * Handles communication between the SDK (Host) and the embedded Iframe (Guest).
5
+ */
6
+ export declare class IframeBridgeHost {
7
+ private readonly session;
8
+ private iframeWindow;
9
+ private messageHandler;
10
+ constructor(session: WorkflowSession);
11
+ /**
12
+ * Attach the bridge to an iframe element.
13
+ * Starts listening for messages from the iframe.
14
+ */
15
+ attach(iframe: HTMLIFrameElement): void;
16
+ /**
17
+ * Detach the bridge.
18
+ * Stops listening for messages.
19
+ */
20
+ detach(): void;
21
+ /**
22
+ * Handle an incoming call message from the iframe.
23
+ */
24
+ private handleCall;
25
+ /**
26
+ * Check if a method is available (built-in or user-defined).
27
+ */
28
+ private hasMethod;
29
+ /**
30
+ * Send a response message back to the iframe.
31
+ */
32
+ private sendResponse;
33
+ }
@@ -0,0 +1,94 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) Object.defineProperty(obj, key, {
3
+ value: value,
4
+ enumerable: true,
5
+ configurable: true,
6
+ writable: true
7
+ });
8
+ else obj[key] = value;
9
+ return obj;
10
+ }
11
+ class IframeBridgeHost {
12
+ attach(iframe) {
13
+ if (this.messageHandler) this.detach();
14
+ this.iframeWindow = iframe.contentWindow;
15
+ this.messageHandler = async (event)=>{
16
+ const data = event.data;
17
+ if (!data || 'iframe-bridge-call' !== data.type) return;
18
+ await this.handleCall(data);
19
+ };
20
+ window.addEventListener('message', this.messageHandler);
21
+ }
22
+ detach() {
23
+ if (this.messageHandler) {
24
+ window.removeEventListener('message', this.messageHandler);
25
+ this.messageHandler = null;
26
+ }
27
+ this.iframeWindow = null;
28
+ }
29
+ async handleCall(message) {
30
+ const { callId, method, params = [] } = message;
31
+ let result;
32
+ let error;
33
+ try {
34
+ if ('hasMethod' === method) {
35
+ const [targetMethod] = params;
36
+ result = this.hasMethod(targetMethod);
37
+ } else if ('saveData' === method) {
38
+ const [key, value] = params;
39
+ const safeKey = `iframe_data_${key}`;
40
+ await this.session.setWorkflowVariables({
41
+ [safeKey]: value
42
+ });
43
+ result = true;
44
+ } else if ('loadData' === method) {
45
+ const [key] = params;
46
+ const variables = await this.session.getWorkflowVariables();
47
+ result = variables[`iframe_data_${key}`];
48
+ } else if ('executeWorkflow' === method) {
49
+ const [workflowId, input] = params;
50
+ result = await this.session.executeWorkflow(workflowId, input);
51
+ } else if ('uploadFile' === method) {
52
+ const [options] = params;
53
+ result = await this.session.uploadFileFromIframe(options);
54
+ } else {
55
+ const userMethods = this.session.getIframeMethods();
56
+ const fn = userMethods[method];
57
+ if ('function' == typeof fn) result = await fn(...params);
58
+ else throw new Error(`Method "${method}" not found or not registered on host.`);
59
+ }
60
+ } catch (e) {
61
+ error = e instanceof Error ? e.message : String(e);
62
+ }
63
+ this.sendResponse({
64
+ type: 'iframe-bridge-response',
65
+ callId,
66
+ result,
67
+ error
68
+ });
69
+ }
70
+ hasMethod(methodName) {
71
+ const builtIns = [
72
+ 'saveData',
73
+ 'loadData',
74
+ 'hasMethod',
75
+ 'executeWorkflow',
76
+ 'uploadFile'
77
+ ];
78
+ if (builtIns.includes(methodName)) return true;
79
+ const userMethods = this.session.getIframeMethods();
80
+ return 'function' == typeof userMethods[methodName];
81
+ }
82
+ sendResponse(message) {
83
+ if (this.iframeWindow) this.iframeWindow.postMessage(message, '*');
84
+ }
85
+ constructor(session){
86
+ _define_property(this, "session", void 0);
87
+ _define_property(this, "iframeWindow", void 0);
88
+ _define_property(this, "messageHandler", void 0);
89
+ this.session = session;
90
+ this.iframeWindow = null;
91
+ this.messageHandler = null;
92
+ }
93
+ }
94
+ export { IframeBridgeHost };
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ export { WorkflowSession } from './session';
4
4
  export { UploadService } from './upload';
5
5
  export type { ClientConfig, SessionOptions, SessionState, SessionEventListeners, SendInput, UploadParams, UploadImageParams, CompressOptions, WorkflowInfo, ContentPart, } from './types';
6
6
  export type { SimpleMessage, Workflow, WorkflowConfig, } from '@our-llm/shared/types';
7
- export { Markdown } from './ui/markdown';
7
+ export { Markdown, type MarkdownProps } from './ui/markdown';
package/dist/session.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { SimpleMessage } from '@our-llm/shared/types';
1
2
  import type { SessionOptions, SessionState, SessionEventListeners, SendInput } from './types';
2
3
  import { UploadService } from './upload';
3
4
  /**
@@ -13,6 +14,26 @@ export declare class WorkflowSession {
13
14
  private listeners;
14
15
  private abortController?;
15
16
  constructor(workflowId: string, baseUrl: string, uploadService: UploadService, headers?: Record<string, string>, options?: SessionOptions);
17
+ private iframeMethods;
18
+ /**
19
+ * 注册可供 iframe 调用的自定义方法
20
+ */
21
+ registerIframeMethods(methods: Record<string, (...args: any[]) => any>): this;
22
+ /**
23
+ * 获取已注册的 iframe 方法(供内部使用)
24
+ */
25
+ getIframeMethods(): Record<string, (...args: any[]) => any>;
26
+ /**
27
+ * 上传文件(供 iframe-bridge 使用)
28
+ * @param options 上传选项
29
+ * @returns 文件的 CDN URL
30
+ */
31
+ uploadFileFromIframe(options: {
32
+ base64: string;
33
+ filename: string;
34
+ mimeType: string;
35
+ resourceType?: 'conversation' | 'workflow' | 'knowledge-base';
36
+ }): Promise<string>;
16
37
  /**
17
38
  * 监听事件
18
39
  */
@@ -37,6 +58,23 @@ export declare class WorkflowSession {
37
58
  * 恢复历史会话
38
59
  */
39
60
  restore(): Promise<void>;
61
+ /**
62
+ * 获取工作流变量
63
+ */
64
+ getWorkflowVariables(): Promise<Record<string, any>>;
65
+ /**
66
+ * 更新会话状态(服务端变量)
67
+ * @param values 需要更新的变量键值对
68
+ */
69
+ setWorkflowVariables(values: Record<string, unknown>): Promise<void>;
70
+ /**
71
+ * 执行指定的工作流(临时/嵌套执行)
72
+ * 不会影响当前会话的状态(messages 等)
73
+ * @param workflowId 目标工作流 ID
74
+ * @param input 输入数据(与 send 方法一致)
75
+ * @returns 执行结果的所有消息
76
+ */
77
+ executeWorkflow(workflowId: string, input: SendInput): Promise<SimpleMessage[]>;
40
78
  /**
41
79
  * 发送消息执行工作流
42
80
  */
package/dist/session.js CHANGED
@@ -11,6 +11,41 @@ function _define_property(obj, key, value) {
11
11
  return obj;
12
12
  }
13
13
  class WorkflowSession {
14
+ registerIframeMethods(methods) {
15
+ this.iframeMethods = {
16
+ ...this.iframeMethods,
17
+ ...methods
18
+ };
19
+ return this;
20
+ }
21
+ getIframeMethods() {
22
+ return this.iframeMethods;
23
+ }
24
+ async uploadFileFromIframe(options) {
25
+ const { base64, filename, mimeType, resourceType = 'conversation' } = options;
26
+ const byteString = atob(base64);
27
+ const arrayBuffer = new ArrayBuffer(byteString.length);
28
+ const uint8Array = new Uint8Array(arrayBuffer);
29
+ for(let i = 0; i < byteString.length; i++)uint8Array[i] = byteString.charCodeAt(i);
30
+ const blob = new Blob([
31
+ uint8Array
32
+ ], {
33
+ type: mimeType
34
+ });
35
+ const file = new File([
36
+ blob
37
+ ], filename, {
38
+ type: mimeType
39
+ });
40
+ if (mimeType.startsWith('image/')) return this.uploadService.uploadImage({
41
+ file,
42
+ resourceType
43
+ });
44
+ return this.uploadService.uploadFile({
45
+ file,
46
+ resourceType
47
+ });
48
+ }
14
49
  on(event, listener) {
15
50
  if (!this.listeners.has(event)) this.listeners.set(event, new Set());
16
51
  this.listeners.get(event).add(listener);
@@ -52,6 +87,108 @@ class WorkflowSession {
52
87
  messages: data.messages
53
88
  });
54
89
  }
90
+ async getWorkflowVariables() {
91
+ if (!this.state.threadId) throw new Error('No active session (missing threadId)');
92
+ const response = await fetch(`${this.baseUrl}/workflows/${this.workflowId}/state?threadId=${this.state.threadId}`, {
93
+ headers: this.headers
94
+ });
95
+ if (!response.ok) throw new Error('Failed to get workflow variables');
96
+ const data = await response.json();
97
+ return data.variables || {};
98
+ }
99
+ async setWorkflowVariables(values) {
100
+ if (!this.state.threadId || this.state.threadId.startsWith('tmp_')) throw new Error('No active session (missing valid threadId)');
101
+ const response = await fetch(`${this.baseUrl}/workflows/${this.workflowId}/variables`, {
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ ...this.headers
106
+ },
107
+ body: JSON.stringify({
108
+ threadId: this.state.threadId,
109
+ values
110
+ })
111
+ });
112
+ if (!response.ok) throw new Error('Failed to update state');
113
+ }
114
+ async executeWorkflow(workflowId, input) {
115
+ const { parts: inputParts, ...rest } = input;
116
+ const parts = [];
117
+ for (const part of inputParts)switch(part.type){
118
+ case 'text':
119
+ parts.push({
120
+ type: 'text',
121
+ text: part.text
122
+ });
123
+ break;
124
+ case 'image':
125
+ {
126
+ const imageUrl = await this.resolveMediaUrl(part, 'image');
127
+ parts.push({
128
+ type: 'image',
129
+ url: imageUrl
130
+ });
131
+ break;
132
+ }
133
+ case 'document':
134
+ {
135
+ const docInfo = await this.resolveDocumentInfo(part);
136
+ parts.push({
137
+ type: 'document',
138
+ url: docInfo.url,
139
+ filename: docInfo.filename,
140
+ mimeType: docInfo.mimeType
141
+ });
142
+ break;
143
+ }
144
+ case 'video':
145
+ {
146
+ const videoUrl = await this.resolveMediaUrl(part, 'video');
147
+ parts.push({
148
+ type: 'video',
149
+ url: videoUrl
150
+ });
151
+ break;
152
+ }
153
+ case 'audio':
154
+ {
155
+ const audioUrl = await this.resolveMediaUrl(part, 'audio');
156
+ parts.push({
157
+ type: 'audio',
158
+ url: audioUrl
159
+ });
160
+ break;
161
+ }
162
+ }
163
+ const executionInput = {
164
+ messages: [
165
+ {
166
+ content: parts
167
+ }
168
+ ]
169
+ };
170
+ const resultMessages = [];
171
+ const tempThreadId = v7();
172
+ return new Promise((resolve, reject)=>{
173
+ executeWorkflowSSE(`${this.baseUrl}/workflows/${workflowId}/execute`, {
174
+ input: executionInput,
175
+ threadId: tempThreadId,
176
+ ...rest
177
+ }, {
178
+ onNodeEnd: (data)=>{
179
+ if (data.m && data.m.length > 0) {
180
+ for (const msg of data.m)if ('human' !== msg.type) resultMessages.push(msg);
181
+ }
182
+ },
183
+ onComplete: ()=>{
184
+ resolve(resultMessages);
185
+ },
186
+ onError: (err)=>{
187
+ reject(new Error(err.message));
188
+ }
189
+ }, this.headers).catch(reject);
190
+ });
191
+ }
55
192
  async send({ parts: inputParts, ...rest }) {
56
193
  if (this.state.isExecuting) throw new Error('工作流正在执行中');
57
194
  this.updateState({
@@ -310,6 +447,7 @@ class WorkflowSession {
310
447
  _define_property(this, "state", void 0);
311
448
  _define_property(this, "listeners", void 0);
312
449
  _define_property(this, "abortController", void 0);
450
+ _define_property(this, "iframeMethods", void 0);
313
451
  this.workflowId = workflowId;
314
452
  this.baseUrl = baseUrl;
315
453
  this.uploadService = uploadService;
@@ -319,6 +457,7 @@ class WorkflowSession {
319
457
  isExecuting: false
320
458
  };
321
459
  this.listeners = new Map();
460
+ this.iframeMethods = {};
322
461
  if (options.threadId) this.state.threadId = options.threadId;
323
462
  else this.state.threadId = v7();
324
463
  }
package/dist/ui/code.js CHANGED
@@ -1,18 +1,36 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
2
3
  import { CodeHighlighter, Mermaid } from "@ant-design/x";
3
4
  import { jsonrepair } from "jsonrepair";
5
+ import { useWorkflowSession } from "./context.js";
6
+ import { IframeBridgeHost } from "../iframe-bridge.js";
4
7
  const Code = (props)=>{
5
8
  var _className_match;
6
9
  const { className, children } = props;
7
10
  const lang = (null == className ? void 0 : null == (_className_match = className.match(/language-(\w+)/)) ? void 0 : _className_match[1]) || '';
11
+ const session = useWorkflowSession();
12
+ const iframeRef = useRef(null);
13
+ const bridgeRef = useRef(null);
14
+ useEffect(()=>{
15
+ if (session && iframeRef.current) {
16
+ if (!bridgeRef.current) bridgeRef.current = new IframeBridgeHost(session);
17
+ bridgeRef.current.attach(iframeRef.current);
18
+ }
19
+ return ()=>{
20
+ var _bridgeRef_current;
21
+ null == (_bridgeRef_current = bridgeRef.current) || _bridgeRef_current.detach();
22
+ };
23
+ }, [
24
+ session
25
+ ]);
8
26
  if ('string' != typeof children) return null;
9
27
  if ('mermaid' === lang) return /*#__PURE__*/ jsx(Mermaid, {
10
28
  children: children
11
29
  });
12
- console.log(lang);
13
- if ('tmpl' === lang) {
30
+ if ('tmpl' === lang) try {
14
31
  const json = JSON.parse(jsonrepair(children));
15
32
  if ('iframe' === json.type) return /*#__PURE__*/ jsx("iframe", {
33
+ ref: iframeRef,
16
34
  src: json.data.src,
17
35
  width: "100%",
18
36
  allow: "clipboard-read; clipboard-write; camera; microphone",
@@ -23,7 +41,7 @@ const Code = (props)=>{
23
41
  display: 'block'
24
42
  }
25
43
  });
26
- }
44
+ } catch {}
27
45
  return /*#__PURE__*/ jsx(CodeHighlighter, {
28
46
  lang: lang,
29
47
  children: children
@@ -0,0 +1,3 @@
1
+ import type { WorkflowSession } from '../session';
2
+ export declare const SessionContext: import("react").Context<WorkflowSession | null>;
3
+ export declare const useWorkflowSession: () => WorkflowSession | null;
@@ -0,0 +1,4 @@
1
+ import { createContext, useContext } from "react";
2
+ const SessionContext = createContext(null);
3
+ const useWorkflowSession = ()=>useContext(SessionContext);
4
+ export { SessionContext, useWorkflowSession };
@@ -1,4 +1,8 @@
1
1
  import { XMarkdownProps } from '@ant-design/x-markdown';
2
2
  import '@ant-design/x-markdown/themes/light.css';
3
- declare const Markdown: ({ className, streaming, config, components, ...props }: XMarkdownProps) => import("react/jsx-runtime").JSX.Element;
3
+ import type { WorkflowSession } from '../session';
4
+ export interface MarkdownProps extends XMarkdownProps {
5
+ session?: WorkflowSession;
6
+ }
7
+ declare const Markdown: ({ className, streaming, config, components, session, ...props }: MarkdownProps) => import("react/jsx-runtime").JSX.Element;
4
8
  export { Markdown };
@@ -6,23 +6,27 @@ import zh_CN from "@ant-design/x/locale/zh_CN";
6
6
  import clsx from "clsx";
7
7
  import { Code } from "./code.js";
8
8
  import { XProvider } from "@ant-design/x";
9
- const Markdown = ({ className, streaming, config, components, ...props })=>/*#__PURE__*/ jsx(XProvider, {
9
+ import { SessionContext } from "./context.js";
10
+ const Markdown = ({ className, streaming, config, components, session, ...props })=>/*#__PURE__*/ jsx(XProvider, {
10
11
  locale: zh_CN,
11
- children: /*#__PURE__*/ jsx(XMarkdown, {
12
- className: clsx(className, 'x-markdown-light'),
13
- streaming: {
14
- ...streaming,
15
- enableAnimation: true
16
- },
17
- config: {
18
- ...config,
19
- extensions: Latex()
20
- },
21
- components: {
22
- ...components,
23
- code: Code
24
- },
25
- ...props
12
+ children: /*#__PURE__*/ jsx(SessionContext.Provider, {
13
+ value: session || null,
14
+ children: /*#__PURE__*/ jsx(XMarkdown, {
15
+ className: clsx(className, 'x-markdown-light'),
16
+ streaming: {
17
+ ...streaming,
18
+ enableAnimation: true
19
+ },
20
+ config: {
21
+ ...config,
22
+ extensions: Latex()
23
+ },
24
+ components: {
25
+ ...components,
26
+ code: Code
27
+ },
28
+ ...props
29
+ })
26
30
  })
27
31
  });
28
32
  export { Markdown };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtfai",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -15,16 +15,16 @@
15
15
  "devDependencies": {
16
16
  "@eslint/js": "^9.39.2",
17
17
  "@rsbuild/plugin-react": "^1.4.3",
18
- "@rslib/core": "^0.19.2",
19
- "@types/react": "^19.2.8",
18
+ "@rslib/core": "^0.19.3",
19
+ "@types/react": "^19.2.9",
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.0.0",
24
- "prettier": "^3.8.0",
23
+ "globals": "^17.1.0",
24
+ "prettier": "^3.8.1",
25
25
  "react": "^19.2.3",
26
26
  "typescript": "^5.9.3",
27
- "typescript-eslint": "^8.53.0"
27
+ "typescript-eslint": "^8.53.1"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "react": ">=16.9.0",