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.5.5

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({ content: '你好' })

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 }) => {
  // 发生错误
})

最佳实践(推荐组合)

  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

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() 避免内存泄漏