npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wtfai

v1.8.8

Published

Agent 工作流 SDK,为第三方 Web 开发者提供简洁的 API 来集成工作流执行、文件上传和会话管理功能。

Readme

wtfai

Agent 工作流 SDK,为第三方 Web 开发者提供简洁的 API 来集成工作流执行、文件上传和会话管理功能。

安装

npm install wtfai
# 或
pnpm add wtfai

使用前准备

  • 前往 https://www.ourteacher.cc/ 联系客服申请开通 API 访问权限,获取 API Key;

  • 您的后端需要代理 https://llm-api.ourschool.cc/workflows/ 开头的接口,在代理过程中需要给请求头添加 x-tenant-secret,值为 API Key;

  • 后续将 baseUrl 改为您的后端代理地址即可;

  • 为方便开发调试,前端可以在创建 Wtfai 实例时传入 tenantSecret,值为 API Key,切记不要在生产环境使用。

快速开始

import Wtfai from 'wtfai'

// 1. 创建客户端
const client = new Wtfai({
  baseUrl: 'https://api.example.com',
})

// 2. 创建会话
const session = client.createSession('workflow-id')

// 3. 发送消息
await session.send({ parts: [{ type: 'text', text: '你好' }] })

API 文档

Wtfai

SDK 主入口类。

构造函数

const client = new Wtfai({
  baseUrl: string,    // 必填,API 服务器地址
  timeout?: number,   // 可选,请求超时时间(毫秒)
  headers?: Record<string, string>,  // 可选,自定义请求头
  tenantSecret?: string, // 可选,租户密钥 (⚠️ 仅用于调试,禁止在生产环境使用)
})

方法

getWorkflows(page?: number, pageSize?: number)

获取工作流列表。

// 分页获取工作流,默认 page=1, pageSize=10
const { items, total } = await client.getWorkflows(1, 20)
console.log(`共 ${total} 个工作流`)
getWorkflow(id: string)

获取工作流详情。

const workflow = await client.getWorkflow('workflow-id')
console.log(workflow.name, workflow.description)
createSession(workflowId: string, options?: SessionOptions)

创建工作流会话。

// 新会话
const session = client.createSession('workflow-id')

// 恢复已有会话
const session = client.createSession('workflow-id', {
  threadId: 'existing-thread-id',
})

WorkflowSession

工作流会话类,管理消息状态和执行流程。

方法

on(event, listener)

监听事件。事件语义与后端 SSE 推送保持一致(见 docs/sse-event-protocol.md)。

事件说明与常见用法

  • start:工作流开始执行。会返回后端确认的 threadIdtitle 仅在新会话时返回(默认为工作流名)。
    • 常见用法:首次拿到 threadId 后持久化,便于恢复会话。
  • nodeStart:某个节点开始执行(排除了 LangGraph 内置的 __start__/__end__)。
    • 常见用法:在 UI 上标记「当前节点」,或显示步骤进度。
  • nodeEnd:某个节点执行结束,可能包含 variables 和/或 messages
    • 常见用法:更新节点级结果或收集中间产物。
    • 注意:后端当前只透传 billing 相关变量;brain/loop 节点的消息已在流式 token 中渲染,这里会省略以避免重复。
  • token:LLM 流式 token。isReasoning 表示推理内容(例如 DeepSeek 的 reasoning_content)。
    • 常见用法:实现打字机效果;区分显示「推理」与「最终输出」。
  • loading:仅在 Loading 节点触发,message 为配置的提示语(纯字符串)。
    • 常见用法:展示阶段性加载提示。
  • title:异步生成的会话标题(仅新会话)。通常在 start 之后稍晚到达。
    • 常见用法:更新会话列表标题/面包屑。
  • complete:本次执行成功结束。
    • 常见用法:停止输入框 loading、允许再次发送。
  • error:执行出错时触发(错误时不会再触发 complete)。
    • 常见用法:toast 提示、错误上报、兜底 UI。
session.on('start', ({ threadId, title }) => {
  // 工作流开始执行,获取到后端确认的 threadId
  // title 只有新会话才会有(默认为工作流名称)
  // 建议持久化 threadId,便于恢复会话
})

session.on('nodeStart', ({ nodeId }) => {
  // 节点开始执行
})

session.on('nodeEnd', ({ nodeId, variables, messages }) => {
  // 节点执行结束
  // variables: 目前只包含 billing 相关变量
  // messages: brain/loop 节点通常已在 token 中渲染,可能为空
})

session.on('token', ({ nodeId, content, isReasoning }) => {
  // 收到流式 token(用于打字机效果)
  // isReasoning 为 true 时表示推理内容
})

session.on('loading', ({ nodeId, message }) => {
  // Loading 节点的提示语(纯字符串)
  console.log('Loading:', message)
})

session.on('title', ({ title }) => {
  // 收到标题事件,用于更新会话标题
  console.log('会话标题:', title)
})

session.on('complete', () => {
  // 工作流执行完成(成功)
})

session.on('error', ({ message }) => {
  // 发生错误
})

session.on('contentAction', ({ type, payload }) => {
  // 内容区域(如 Markdown)触发的自定义动作
  if (type === 'workflow') {
    const { workflowId, params } = payload
    console.log('跳转工作流:', workflowId, '参数:', params)
  }
})

Markdown 交互 (自定义协议)

SDK 的 Markdown 组件支持通过特殊协议触发 contentAction 事件,方便在内容中嵌入交互按钮或链接:

  • 跳转工作流:使用 workflow: 协议。 格式:[文字](workflow:工作流ID?参数1=值1&参数2=值2) 示例:[查看详情](workflow:cm0p1234?from=chat)

当用户点击此类链接时,SDK 不会执行页面跳转,而是触发 contentAction 事件,由宿主应用决定如何处理(如切换侧边栏、打开弹窗等)。


结构化内容处理 (WorkflowRegistry)

为了在聊天内容中更美观地展示工作流引用,SDK 提供了 <workflow> 自定义标签和全局注册机制。

1. 全局注册工作流信息

在应用顶层(或页面容器层)使用 WorkflowRegistry 注入工作流的展示名称和图标:

import { WorkflowRegistry } from 'wtfai';
import { RobotOutlined } from '@ant-design/icons';

const MY_WORKFLOWS = {
  'wf_week_report': { 
    name: '自动化周报', 
    icon: <RobotOutlined /> 
  },
};

// 在容器层包裹一次即可
<WorkflowRegistry mapping={MY_WORKFLOWS}>
   <App />
</WorkflowRegistry>

2. 在 Markdown 中使用

通过自定义标签 <workflow id="xxx" /> 引用工作流:

  • 基本用法<workflow id="wf_week_report" />
  • 自定义文字<workflow id="wf_week_report">立即查看</workflow>

渲染效果: SDK 会自动将其渲染为一个美观的“药丸状”卡片按钮,包含图标和名称。如果 WorkflowRegistry 中没有匹配到 ID,则会降级显示标签内容或 ID。

点击该卡片会触发 contentAction 事件,type'workflow'

3. 内容包装器 (contentWrapper)

WorkflowRegistry 支持 contentWrapper 属性,允许开发者在渲染 Markdown 内容前对其进行包装或增强。目前支持对 code 块进行包装。

配置类型

export type ContentWrapperConfig = {
  code?: (
    props: ComponentProps, // props 包含 lang, children, streamStatus 等
  ) => ((children: ReactNode) => ReactNode) | null | undefined | false
}

使用示例

比如,你想给所有的 html:run 类型的代码预览块增加一个自定义的消息提示:

import { WorkflowRegistry } from 'wtfai';

<WorkflowRegistry
  contentWrapper={{
    code: (props) => {
      // 只有特定语言才进行包装
      if (props.lang === 'html:run') {
        return (children) => (
          <div className="my-custom-wrapper">
            <div className="wrapper-header">这是我自定义加的内容</div>
            {children}
          </div>
        )
      }
      return false // 返回 false 表示不包装,由 SDK 按默认方式渲染
    },
  }}
>
  <ChatPage />
</WorkflowRegistry>

这种机制非常适合在不修改 SDK 源码的前提下,为特定类型的内容增加业务相关的 UI 装饰(如操作按钮、免责声明、权限校验提示等)。

4. 消息导出

SDK 提供 exportMessages 用于导出当前会话消息,并会把 Mermaid 等图表内容导出为图片数据。导出结果不会自动下载文件;如需生成文件内容,可使用 createExportedMessagesBlob 得到 Blob,后续上传、保存或下载由宿主应用自行决定。

配置导出上下文

导出需要知道消息数据和对应的 Markdown 容器 DOM。通常在渲染消息列表的外层传入 exportConfig

import { useRef } from 'react'
import {
  WorkflowRegistry,
  exportMessages,
  createExportedMessagesBlob,
  type WorkflowExportConfig,
} from 'wtfai'

const messageListRef = useRef<HTMLDivElement>(null)

const exportConfig: WorkflowExportConfig = {
  containerRef: messageListRef,
  messages,
}

async function handleExport() {
  const payload = await exportMessages(exportConfig)
  const blob = createExportedMessagesBlob(payload)

  // SDK 只返回数据,不假设业务如何消费。
  // 下面仅演示浏览器下载,实际也可以上传到服务器或写入 IndexedDB。
  const url = URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = `chat-export-${Date.now()}.json`
  link.click()
  URL.revokeObjectURL(url)
}

return (
  <div ref={messageListRef}>
    <WorkflowRegistry exportConfig={exportConfig}>
      <MessageList messages={messages} />
    </WorkflowRegistry>
  </div>
)

导出结果结构

每条消息会保留原始内容,同时提供一个普通 Markdown 渲染器可直接展示的内容字段:

type ExportedMessage = {
  id: string
  role: 'user' | 'assistant'
  content: string
  renderableContent: string
  attachments?: unknown
  chartImages: Array<{
    kind: string
    format: 'png'
    dataUrl: string
  }>
}
  • content:原始消息内容,不做修改。
  • renderableContent:可直接渲染的 Markdown 内容。SDK 会把已成功导出的 Mermaid 等代码块替换为 Markdown 图片标记,图片地址为 base64 data URL。
  • chartImages:结构化的图表图片数据,便于业务侧单独存储、上传或二次处理。

最佳实践(推荐组合)

  1. 打字机效果:使用 token 拼接流式内容。
  2. 消息列表:优先由 token 推进 AI 回复;nodeEnd 仅用于补齐非 brain/loop 节点的消息。
  3. 加载状态loading 展示阶段性提示,complete/error 关闭 loading 并解锁输入。
  4. 会话管理:在 start 保存 threadIdtitle 更新会话标题。
off(event, listener)

移除事件监听。

const onToken = (data) => console.log(data)
session.on('token', onToken)

// 不需要时移除监听
session.off('token', onToken)
send(input: SendInput)

发送消息执行工作流。

// 基础用法:纯文本
await session.send({
  parts: [
    { type: 'text', text: '你好' }
  ]
})

// 图片 + 文本(使用 File)
await session.send({
  parts: [
    { type: 'text', text: '请分析下面的图片:' },
    { type: 'image', file: imageFile },
    { type: 'text', text: '说明这张图的内容' }
  ]
})

// 图片 + 文本(使用 URL)
await session.send({
  parts: [
    { type: 'text', text: '请对比这两张图片:' },
    { type: 'image', url: 'https://example.com/img1.jpg' },
    { type: 'image', url: 'https://example.com/img2.jpg' }
  ]
})

// 完整示例:文本 + 图片 + 文档
await session.send({
  parts: [
    { type: 'text', text: '请对比下面两张图片:' },
    { type: 'image', file: imageFile1 },
    { type: 'image', file: imageFile2 },
    { type: 'text', text: '然后参考这份报告:' },
    { 
      type: 'document',
      file: pdfFile,
      filename: 'report.pdf'
    },
    { type: 'text', text: '说明第一张图的问题在哪里' }
  ]
})

// 使用 blob URL
await session.send({
  parts: [
    { type: 'text', text: '分析这张图:' },
    { type: 'image', url: blobUrl },  // 会自动上传
  ]
})

说明

  • parts 数组中的元素会按顺序传递给 LLM,使大模型能准确理解位置指代(如"下面的图片"、"第一张图")
  • imagedocument 类型必须提供 fileurl 之一
  • 图片 File 会自动压缩到 1920px 以内,质量 0.8
  • blob URL 会自动上传到服务器
restore()

恢复历史会话(需要在创建 session 时传入 threadId)。

const session = client.createSession('workflow-id', {
  threadId: 'existing-thread-id',
})
await session.restore()
// 会将历史消息写入 state,可通过 getState 获取
updateSessionTitle(title: string)

更新会话标题(手动修改)。

await session.updateSessionTitle('新的会话标题')
// 会更新服务端变量,并触发 title 事件
getState()

获取当前状态快照。

const state = session.getState()
console.log(state.messages)
console.log(state.isExecuting)
abort()

中止当前执行。

session.abort()
dispose()

释放会话资源:移除所有事件监听并中止执行,同时清空会话内部状态。被释放的 session 不可再使用。

session.dispose()

说明

  • abort() 仅用于中断当前执行(比如用户点击停止生成)
  • dispose() 用于彻底清理会话(组件卸载、切换会话时),调用后不可再复用该 session
setWorkflowVariables(values)

更新服务端会话变量。

await session.setWorkflowVariables({
  key: 'value',
  userPreference: { theme: 'dark' }
})
getWorkflowVariables()

获取服务端会话变量。

const vars = await session.getWorkflowVariables()
console.log(vars.userPreference)
registerIframeMethods(methods)

注册可供 Iframe (Guest) 调用的方法。

session.registerIframeMethods({
  // 定义一个获取用户信息的方法
  getUserInfo: async () => {
    return { name: 'Alice', id: 123 }
  },
  
  // 定义一个打开弹窗的方法
  openModal: (type) => {
    setModalVisible(type)
  }
})

Iframe SDK (Guest Side)

我们在 packages/sdk/iframe-sdk.js 提供了轻量级的 SDK,供嵌入的 HTML 工具使用。

该 SDK 不需要构建工具,直接通过 <script> 标签引入即可。

引入 SDK

<script src="/iframe-sdk.js"></script>

使用方法

SDK 会在全局挂载 IframeBridge 对象。

IframeBridge.call(method, params?)

调用宿主 (Host) 方法。

// 调用自定义方法
const userInfo = await IframeBridge.call('getUserInfo');

// 调用宿主方法并传参
await IframeBridge.call('log', ['Hello from iframe']);

IframeBridge.hasMethod(methodName)

检查宿主是否支持某个方法。

const supportsLog = await IframeBridge.hasMethod('log');
if (supportsLog) {
  // ...
}

IframeBridge.saveData(key, value)

持久化存储数据(存储在当前工作流会话的 variables 中)。

await IframeBridge.saveData('draft', { content: '...' });

IframeBridge.loadData(key)

读取持久化数据。

const draft = await IframeBridge.loadData('draft');

IframeBridge.executeWorkflow(workflowId, input)

临时/嵌套执行另一个工作流。

const result = await IframeBridge.executeWorkflow('target-workflow-id', {
  parts: [
    { type: 'image', url: 'https://...' }
  ]
});
console.log(result); // 执行产生的所有消息

IframeBridge.updateSessionTitle(title)

更新当前会话的标题。

await IframeBridge.updateSessionTitle('新的标题');

IframeBridge.uploadFile(file, options?)

上传文件到云存储,返回 CDN URL。

// 从 file input 上传
const fileInput = document.querySelector('input[type="file"]');
const url = await IframeBridge.uploadFile(fileInput.files[0]);
console.log('文件 URL:', url);

// 从 canvas 上传截图
canvas.toBlob(async (blob) => {
  const file = new File([blob], 'screenshot.png', { type: 'image/png' });
  const url = await IframeBridge.uploadFile(file);
  console.log('截图 URL:', url);
});

// 指定资源类型
const url = await IframeBridge.uploadFile(file, {
  resourceType: 'conversation'  // 'conversation' | 'workflow' | 'knowledge-base'
});

说明

  • 图片类型会自动压缩到 1920px 以内
  • 音视频文件会自动获取时长信息
  • 返回的是可直接访问的 CDN URL

Session Data CRUD API

用于会话级数据的持久化存储,与 saveData/loadData 不同,这是一套完整的 CRUD 接口,支持集合(Collection)概念,适合存储结构化数据。

与 saveData/loadData 的区别

  • saveData/loadData 存储在工作流 state 中(适合简单 key-value)
  • Session Data API 存储在独立数据表中(适合结构化数据、列表数据)

IframeBridge.createRecord(collection, data, options?)

在指定集合中创建一条记录。可以通过 options.entityId 字段进行二级分类归属(如老师 ID、学生 ID),后续查询时利用此字段能大幅提升检索性能。

// options = { entityId?: string }
const id = await IframeBridge.createRecord('notes', {
  title: 'My Note',
  content: 'Hello World',
  createdAt: Date.now()
}, { entityId: 'user_123' }); // 可选的 entityId
console.log('创建成功, ID:', id);

IframeBridge.createRecords(collection, dataList, options?)

批量在指定集合中创建多条记录。支持传入可选的配置项如 entityId

// options = { entityId?: string }
const ids = await IframeBridge.createRecords('logs', [
  { level: 'info', message: 'Task started' },
  { level: 'success', message: 'Task finished' }
], { entityId: 'user_123' });
console.log('创建成功, IDs:', ids);

IframeBridge.getRecord(collection, id)

获取单条记录。

const data = await IframeBridge.getRecord('notes', id);
if (data) {
  console.log('记录内容:', data.title, data.content);
} else {
  console.log('记录不存在');
}

IframeBridge.updateRecord(collection, id, data)

更新记录(完整替换)。

await IframeBridge.updateRecord('notes', id, {
  title: 'Updated Title',
  content: 'New content',
  updatedAt: Date.now()
});

IframeBridge.deleteRecord(collection, id)

删除单条记录。

await IframeBridge.deleteRecord('notes', id);

IframeBridge.listRecords(collection, options?)

分页查询集合中的记录。可以通过传入 entityId 来极速筛选归属于特定实体的数据。

// options = { page?: number, pageSize?: number, order?: 'ASC' | 'DESC', entityId?: string }
const result = await IframeBridge.listRecords('notes', {
  page: 1,
  pageSize: 10,
  order: 'DESC',
  entityId: 'user_123' // 可选,利用组合索引极速过滤
});
console.log(`共 ${result.total} 条记录`);
result.records.forEach(r => {
  console.log(r.id, r.data.title);
});

IframeBridge.clearCollection(collection)

清空集合中的所有记录。

const count = await IframeBridge.clearCollection('notes');
console.log(`已删除 ${count} 条记录`);

使用场景

  • 待办事项列表
  • 用户收藏/历史记录
  • 表单草稿
  • 游戏存档

HTTP API

以下 API 可直接通过 HTTP 调用,无需使用 SDK。

中断工作流执行

中断正在运行的工作流。工作流会返回 error 事件携带中断原因后关闭连接。

请求

POST /workflows/:workflowId/interrupt
Content-Type: application/json

{
  "threadId": "会话ID",
  "reason": "中断原因"
}

响应

{
  "success": true,
  "local": true  // true=本地实例中断, false=通过集群广播
}

说明

  • 支持集群模式,会通过 Redis Pub/Sub 广播中断信号到所有实例
  • 工作流被中断后会触发 error 事件,message 为传入的 reason
  • 如果找不到对应的执行会话,返回 404


RealtimeService

提供跨端的实时消息总线功能(通过 client.realtime 访问)。

subscribe(topic: string, onEvent: (event: RealtimeEvent) => void)

订阅实时消息流。适合大屏端监听由其他端发送的指令或通知。

  • 自动管理:方法内部会自动计算并附加当前浏览器的设备指纹(deviceId),激活在线状态追踪。
  • 返回值:返回一个对象,包含 unsubscribe() 方法用于断开连接。
const sub = await client.realtime.subscribe('class:101', (event) => {
  console.log('收到实时更新:', event.data);
  // event.event 为事件名称,event.data 为业务数据
});

// 当页面销毁或不需要监听时,请务必手动释放:
// sub.unsubscribe();
push(topic: string, event: string, data: any)

推送实时消息。适合手机端(或其他客户端)发送指令。

await client.realtime.push('class:101', 'msgA', {
  foo: 'bar'
});
getPresence(topic: string)

获取指定主题下的在线状态详情(如当前有多少个大屏在线)。

const { onlineCount, clients } = await client.realtime.getPresence('class:101');
console.log(`当前在线设备数: ${onlineCount}`);

UploadService

文件上传服务(通过 client.upload 访问)。

uploadFile(params)

上传文件。

const url = await client.upload.uploadFile({
  file: file,
  resourceType: 'conversation',  // 'conversation' | 'workflow' | 'knowledge-base'
  onProgress: (percent) => {
    console.log(`上传进度: ${percent * 100}%`)
  },
})
uploadImage(params)

上传图片(自动压缩)。

const url = await client.upload.uploadImage({
  file: imageFile,
  resourceType: 'conversation',
  onProgress: (percent) => console.log(`${percent * 100}%`),
  compressOptions: {         // 可选
    quality: 0.8,
    maxWidth: 1920,
  },
})

类型定义

SimpleMessage

消息类型(与后端保持一致)。

/** 消息内容部件 */
type WorkflowMessagePart =
  | { type: 'text'; text: string }
  | { type: 'image'; url: string }
  | { type: 'video'; url: string }
  | { type: 'audio'; url: string }
  | { type: 'document'; filename: string; url: string }

interface SimpleMessage {
  type: 'human' | 'ai' | 'system' | 'tool'
  parts: WorkflowMessagePart[]
  id?: string
}

SessionState

会话状态类型。

interface SessionState {
  threadId?: string
  messages: SimpleMessage[]
  isExecuting: boolean
  currentNodeId?: string
}

SendInput

发送消息输入类型。

interface SendInput {
  /** 内容部件数组,按顺序传递给 LLM */
  parts: ContentPart[]
}

type ContentPart =
  | { type: 'text'; text: string }
  | {
      type: 'image'
      file?: File      // 图片文件(会自动压缩并上传)
      url?: string     // 图片 URL(支持 blob URL 和普通 URL)
    }
  | {
      type: 'document'
      file?: File      // 文档文件(会自动上传)
      url?: string     // 文档 URL(支持 blob URL 和普通 URL)
      filename: string // 文件名
      mimeType?: string // MIME 类型
    }

完整示例

React 聊天组件

import { useState, useEffect, useRef } from 'react'
import Wtfai, { type SimpleMessage, type WorkflowSession } from 'wtfai'

const client = new Wtfai({
  baseUrl: 'https://api.example.com',
})

function ChatComponent({ workflowId }: { workflowId: string }) {
  const [messages, setMessages] = useState<SimpleMessage[]>([])
  const [isExecuting, setIsExecuting] = useState(false)
  const [input, setInput] = useState('')
  const sessionRef = useRef<WorkflowSession | null>(null)
  const streamingIndexRef = useRef<number | null>(null)

  useEffect(() => {
    return () => {
      sessionRef.current?.abort()
    }
  }, [])

  const handleSend = async () => {
    if (!input.trim() || isExecuting) return

    const session = client.createSession(workflowId)
    sessionRef.current = session

    session.on('token', ({ content }) => {
      setMessages((prev) => {
        const next = [...prev]
        if (streamingIndexRef.current === null) {
          streamingIndexRef.current = next.length
          next.push({
            type: 'ai',
            parts: [{ type: 'text', text: content }],
          })
          return next
        }

        const index = streamingIndexRef.current
        const msg = next[index]
        const firstPart = msg?.parts?.[0]
        if (msg && firstPart?.type === 'text') {
          const updatedParts = [...msg.parts]
          updatedParts[0] = {
            ...firstPart,
            text: firstPart.text + content,
          }
          next[index] = { ...msg, parts: updatedParts }
        }
        return next
      })
    })

    session.on('complete', () => {
      setIsExecuting(false)
      streamingIndexRef.current = null
    })

    session.on('error', ({ message }) => {
      alert(`错误: ${message}`)
      setIsExecuting(false)
      streamingIndexRef.current = null
    })

    try {
      setIsExecuting(true)
      await session.send({
        parts: [{ type: 'text', text: input }]
      })
      setInput('')
    } catch (error) {
      console.error(error)
      setIsExecuting(false)
      streamingIndexRef.current = null
    }
  }

  return (
    <div>
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={msg.type}>
            {msg.parts.find((part) => part.type === 'text')?.text || '...'}
          </div>
        ))}
        {isExecuting && <div>思考中...</div>}
      </div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        disabled={isExecuting}
      />
      <button onClick={handleSend} disabled={isExecuting}>
        发送
      </button>
    </div>
  )
}

注意事项

  1. 顺序保持parts 数组中的元素会按顺序传递给 LLM,确保大模型能理解位置指代
  2. 图片自动压缩:通过 File 上传的图片会自动压缩到 1920px 以内,质量 0.8
  3. 会话管理:每次调用 send() 都会更新 threadId,如需保存会话请在 start 事件中获取
  4. 错误处理:建议始终监听 error 事件处理异常情况
  5. 资源清理:组件卸载时调用 session.abort() 避免内存泄漏