🎁 100 free AI Toolkit licenses – apply by August 15.Learn more

AI 代理聊天机器人

构建一个简单的 AI 代理聊天机器人,它可以读取和编辑 Tiptap 文档。

查看 GitHub 上的源码

技术栈

  • 一个客户端 React 应用程序
  • 一个服务器应用程序
  • 一个 AI 代理框架,例如 Vercel AI SDK
  • OpenAI 模型
  • Server AI Toolkit

Server AI Toolkit 可与任何 AI 代理框架和 AI 模型配合使用。可将其用于任何前端框架和后端编程语言。本指南使用一个简单的 Node.js 服务器端点,它接收一个 Request 对象并返回一个 Response

在继续之前,请按照 安装指南 配置 Server AI Toolkit。

设置演示应用

npx create-next-app@latest server-ai-agent-chatbot

安装核心 Tiptap 包、协作扩展,以及用于 OpenAI 的 Vercel AI SDK

npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-collaboration @tiptap-pro/provider ai @ai-sdk/react @ai-sdk/openai zod uuid yjs jose

安装 Server AI Toolkit 包:

npm install @tiptap/ai-toolkit

Server API 端点

创建一个 API 端点,使用 Vercel AI SDK 调用 OpenAI 模型。从 Server AI Toolkit API 获取工具定义,并通过 API 执行工具。

安装依赖

在你的服务器应用中安装这些依赖:

npm install ai @ai-sdk/openai jose zod

环境变量

先设置授权

在创建辅助函数之前,请先按照 授权指南 配置你的环境变量和身份验证辅助函数 (createJwtTokengetAuthHeaders)。

你还需要一个 OpenAI API 密钥;如果使用 Tiptap Cloud 协作,还需要提供包含你的 Document Server ID 的 TIPTAP_CLOUD_DOCUMENT_SERVER_ID。将它们添加到你的 .env 文件中,并与 授权指南 中的变量一起配置:

# .env(除了 Server AI Toolkit 变量之外)
TIPTAP_CLOUD_DOCUMENT_SERVER_ID=document-server-id
OPENAI_API_KEY=your-openai-key # AI SDK 会自动获取它

辅助函数

创建几个辅助函数以支持你的端点并与 Server AI Toolkit 交互:

获取工具定义

此函数会从 Server AI Toolkit API 获取提示词和可用的工具定义。

// lib/server-ai-toolkit/get-tool-definitions.ts
import type z from 'zod'
import { getAuthHeaders } from './get-auth-headers'

/**
 * 从 Server AI Toolkit API 获取工具定义
 */
export async function getToolDefinitions(editorContext: unknown): Promise<{
  prompt: string
  tools: {
    name: string
    description: string
    inputSchema: z.core.JSONSchema.JSONSchema
  }[]
}> {
  const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev'

  const response = await fetch(`${apiBaseUrl}/v4/ai/toolkit/fetch-tools`, {
    method: 'POST',
    headers: await getAuthHeaders(),
    body: JSON.stringify({
      editorContext,
      tools: {
        tiptapRead: true,
        tiptapEdit: true,
      },
    }),
  })

  if (!response.ok) {
    throw new Error(`获取工具失败:${response.statusText}`)
  }
  const responseData = await response.json()

  return {
    prompt: responseData.systemPrompt,
    tools: responseData.tools,
  }
}

使用自定义节点?

如果你的编辑器包含自定义节点或标记,请使用 addJsonSchemaAwareness 进行配置。请参见 自定义节点指南

执行工具

此函数通过 Server AI Toolkit API 执行工具。它会将工具名称、输入参数、文档 ID 和编辑器上下文数据发送到 API。当 JWT 包含该文档的 Documents:Write 权限时,服务器会自动获取并保存 Tiptap Cloud 文档。

// lib/server-ai-toolkit/execute-tool.ts
import { getAuthHeaders } from './get-auth-headers'

/**
 * 通过 Server AI Toolkit API 执行工具
 */
export async function executeTool(
  toolName: string,
  input: unknown,
  documentId: string,
  editorContext: unknown,
): Promise<{
  tool: { name: string; output: unknown }
  docChanged: boolean
  document: object | null
}> {
  const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev'

  const response = await fetch(`${apiBaseUrl}/v4/ai/toolkit/execute-tool`, {
    method: 'POST',
    headers: await getAuthHeaders(documentId),
    body: JSON.stringify({
      editorContext,
      document: {
        type: 'cloud',
        id: documentId,
      },
      user: 'ai-assistant',
      tool: {
        name: toolName,
        input,
      },
    }),
  })

  if (!response.ok) {
    throw new Error(`工具执行失败:${response.statusText}`)
  }

  return response.json()
}

创建 POST /api/server-ai-agent-chatbot 端点:

// server/server-ai-agent-chatbot.ts
import { openai } from '@ai-sdk/openai'
import { createAgentUIStreamResponse, ToolLoopAgent, tool } from 'ai'
import z from 'zod'
import { executeTool } from '@/lib/server-ai-toolkit/execute-tool'
import { getToolDefinitions } from '@/lib/server-ai-toolkit/get-tool-definitions'

export async function handleChatRequest(request: Request): Promise<Response> {
  const {
    messages,
    editorContext,
    documentId,
  }: {
    messages: unknown[]
    editorContext: unknown
    documentId: string
  } = await request.json()

  // 从 Server AI Toolkit API 获取提示词和工具定义
  const { prompt, tools: toolDefinitions } = await getToolDefinitions(editorContext)

  // 将 API 工具定义转换为 AI SDK 工具格式
  const tools = Object.fromEntries(
    toolDefinitions.map((toolDef) => [
      toolDef.name,
      tool({
        description: toolDef.description,
        inputSchema: z.fromJSONSchema(toolDef.inputSchema),
        execute: async (input) => {
          try {
            const result = await executeTool(toolDef.name, input, documentId, editorContext)
            return result.tool.output
          } catch (error) {
            console.error(`执行工具 ${toolDef.name} 失败:`, error)
            return {
              error: error instanceof Error ? error.message : '未知错误',
            }
          }
        },
      }),
    ]),
  )

  const agent = new ToolLoopAgent({
    model: openai('gpt-5.6-luna'),
    instructions: `You are an assistant that can edit rich text documents.
In your responses, be concise and to the point. However, the content you generate in the document does not need to be concise and to the point: instead, it should follow the user's request as closely as possible.
Before calling any tools, summarize you're going to do (in a sentence or less), as a high-level view of the task, as a human writer would describe it.
Rule: In your responses, do not provide any details of the tool calls.
Rule: In your responses, do not provide any details of the HTML content of the document.

${prompt}`,
    tools,
  })

  return createAgentUIStreamResponse({
    agent,
    uiMessages: messages,
  })
}

客户端设置

安装依赖

在你的客户端应用中安装这些依赖:

npm install @ai-sdk/react @tiptap/extension-collaboration @tiptap/react @tiptap/starter-kit @tiptap-pro/provider @tiptap/ai-toolkit ai uuid yjs

创建一个客户端 React 组件,用于渲染支持协作的 Tiptap 编辑器以及一个简单的聊天界面。该组件利用 Vercel AI SDK 中的 useChat hook 来调用 API 端点并管理聊天对话。

首先,创建一个用于生成 Tiptap Cloud 协作 JWT 的 API 端点。此端点会授权客户端访问 Tiptap Cloud 协作服务中的特定文档。

创建 POST /api/collaboration-token 端点。

// server/collaboration-token.ts
import { SignJWT, importPKCS8 } from 'jose'

const TIPTAP_PRIVATE_KEY = process.env.TIPTAP_PRIVATE_KEY
const TIPTAP_ENVIRONMENT_ID = process.env.TIPTAP_ENVIRONMENT_ID
const TIPTAP_CLOUD_DOCUMENT_SERVER_ID = process.env.TIPTAP_CLOUD_DOCUMENT_SERVER_ID

export async function handleCollaborationTokenRequest(request: Request): Promise<Response> {
  const { documentId, userId } = await request.json()
  const privateKey = await importPKCS8(TIPTAP_PRIVATE_KEY, 'ES256')

  const token = await new SignJWT({
    permissions: [{ action: 'Documents:Write', resource: documentId }],
  })
    .setProtectedHeader({ alg: 'ES256' })
    .setIssuer(TIPTAP_ENVIRONMENT_ID)
    .setSubject(userId)
    .setAudience(['Documents'])
    .setExpirationTime('1h')
    .sign(privateKey)

  return Response.json({ token, appId: TIPTAP_CLOUD_DOCUMENT_SERVER_ID })
}

现在创建主页面组件:

// components/AiChatbot.tsx
import { useChat } from '@ai-sdk/react'
import { Collaboration } from '@tiptap/extension-collaboration'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { TiptapCollabProvider } from '@tiptap-pro/provider'
import { ServerAiToolkit, getEditorContext } from '@tiptap/ai-toolkit'
import { DefaultChatTransport } from 'ai'
import { useEffect, useState } from 'react'
import { v4 as uuid } from 'uuid'
import * as Y from 'yjs'

export function Page() {
  const [doc] = useState(() => new Y.Doc())
  const [documentId] = useState(() => `server-ai-agent-chatbot/${uuid()}`)

  const editor = useEditor({
    immediatelyRender: false,
    extensions: [StarterKit, Collaboration.configure({ document: doc }), ServerAiToolkit],
  })

  useEffect(() => {
    let provider: TiptapCollabProvider | undefined

    const connect = async () => {
      const response = await fetch('/api/collaboration-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ documentId, userId }),
      })

      if (!response.ok) {
        throw new Error('获取协作凭证失败')
      }

      const { appId, token } = await response.json()
      provider = new TiptapCollabProvider({
        appId,
        name: documentId,
        token,
        document: doc,
        user: 'user-1',
      })
    }

    void connect().catch(console.error)

    return () => provider?.destroy()
  }, [doc, documentId, userId])

  const editorContext = editor ? getEditorContext(editor) : null

  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/server-ai-agent-chatbot',
      body: { documentId },
    }),
  })

  const [input, setInput] = useState('替换最后一段为关于 Tiptap 的短篇故事')

  if (!editor) return null

  return (
    <div>
      <EditorContent editor={editor} />
      {messages?.map((message) => (
        <div key={message.id} style={{ whiteSpace: 'pre-wrap' }}>
          <strong>{message.role}</strong>
          <br />
          <div className="mt-2 whitespace-pre-wrap">
            {messages?.map((message) =>
              message.parts
                .filter((p) => p.type === 'text')
                .map((p) => p.text)
                .join('\n') || '加载中...'
            )}
          </div>
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault()
          if (input.trim()) {
            sendMessage({ text: input }, { body: { editorContext } })
            setInput('')
          }
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} />
        <button type="submit">发送</button>
      </form>
    </div>
  )
}

文档状态管理

此实现传入了 document: { type: 'cloud', id: documentId },因此 Server AI Toolkit 会自动从 Tiptap Cloud 获取并保存文档。客户端使用 Tiptap Collaboration 实时同步更改。

最终结果

配合额外的 CSS 样式,效果是一个简单但精致的 AI 聊天机器人应用,使用 Server AI Toolkit 进行文档编辑:

查看 GitHub 上的源码

替代方案:直接提供文档

不使用 Tiptap Cloud 文档时,你可以通过在 execute-tool 请求体中传入内联的 document 字段,自己提供并管理文档。

采用这种方式时,你需要在每次工具执行前获取文档,并在 docChangedtrue 时将更新后的文档保存回去。

import { loadDocument, saveDocument } from './db'

const document = await loadDocument()
const response = await fetch(`${apiBaseUrl}/v4/ai/toolkit/execute-tool`, {
  method: 'POST',
  headers: await getAuthHeaders(),
  body: JSON.stringify({
    editorContext,
    document: {
      type: 'inline',
      content: document,
    },
    tool: {
      name: toolName,
      input,
    },
  }),
})

if (!response.ok) {
  throw new Error(`工具执行失败:${response.statusText}`)
}

const body = await response.json()

// 如果工具执行修改了文档,则存储该文档
if (body.docChanged && body.document) {
  await saveDocument(body.document)
}

tiptapRead 工具可以修改文档

tiptapRead 工具可以对文档进行编辑,以便为后续的读取操作做准备。 因此,如果 docChanged 属性为 true,你应该在执行 tiptapRead 工具后更新文档。

document 字段中以内联方式提供文档有几个限制。首先,不支持 Tiptap 评论,因为它们存储在 Tiptap Cloud 文档中。此外,变更无法关联到某个特定用户,也没有与 版本历史 的内置集成。

有关 document 字段的更多信息,请参阅 REST API 参考

下一步