探索 Tiptap V3 的最新功能

集成自定义 LLM

如果你想使用自己的后端提供对自定义 LLM 的访问,可以在扩展配置中重写下面定义的解析函数。

确保你返回了正确类型的响应,并且正确处理了错误。

注意!

我们强烈建议不要在前端直接调用 OpenAI,因为这可能导致 API 令牌泄露!你应该通过后端代理来保护你的 API 令牌安全。

访问私有注册表

高级 AI 扩展发布在 Tiptap 的私有 npm 注册表中。请按照私有注册表指南集成该扩展。如果你已经验证了你的 Tiptap 账号,可以直接前往#安装

安装

为了使用自定义解析函数,你需要安装我们 Tiptap AI 扩展的高级版本。

npm install @tiptap-pro/extension-ai

同时使用自定义 LLM 和 Tiptap AI 云服务

如果你想在某些情况下依赖我们的云服务,确保你已按照此处说明设置了你的团队。

解析函数

你可以在扩展选项中定义自定义解析函数。请注意它们预期的返回类型如下:

类型方法名返回类型
completionaiCompletionResolverPromise<string | null>
streamingaiStreamResolverPromise<ReadableStream<Uint8Array> | null>
imageaiImageResolverPromise<string | null>

使用 aiCompletionResolver 以非流式方式向编辑器添加文本。

使用 aiStreamResolver 直接将内容流式传输到编辑器,实现打字机效果。

确保流返回 HTML,以便 Tiptap 直接将内容渲染为富文本。此方式免去了 Markdown 解析器的需求,使前端保持轻量。

示例

在完成模式中重写特定命令的解析

本例中我们希望在调用 rephrase 动作/命令时,调用自定义后端。 其他所有情况均由 Tiptap Cloud 默认后端处理。

// ...
import Ai from '@tiptap-pro/extension-ai-advanced'
// ...

Ai.configure({
  appId: 'APP_ID_HERE',
  token: 'TOKEN_HERE',
  // ...
  onError(error, context) {
    // 处理错误
  },
  // 定义完成解析函数(注意:流式和图像需单独定义!)
  aiCompletionResolver: async ({
    editor,
    action,
    text,
    textOptions,
    extensionOptions,
    defaultResolver,
  }) => {
    // 根据 action、text 或其他条件判断
    // 决定是否使用自定义接口
    if (action === 'rephrase') {
      const response = await fetch('https://dummyjson.com/quotes/random')
      const json = await response.json()

      if (!response.ok) {
        throw new Error(`${response.status} ${json?.message}`)
      }

      return json?.quote
    }

    // 其他情况则转发至 Tiptap AI 服务
    return defaultResolver({
      editor,
      action,
      text,
      textOptions,
      extensionOptions,
      defaultResolver,
    })
  },
})

注册新的 AI 命令并调用自定义后端动作

本例中,我们注册了一个名为 aiCustomTextCommand 的编辑器命令,使用 Tiptap 的 runAiTextCommand 函数完成其余工作,并添加自定义命令的解析以调用自定义后端(完成模式)。

// …
import { Ai, runAiTextCommand } from '@tiptap-pro/extension-ai-advanced'
// …

// 如果使用 TypeScript,请声明类型:
//
// declare module '@tiptap/core' {
//   interface Commands<ReturnType> {
//     ai: {
//       aiCustomTextCommand: () => ReturnType,
//     }
//   }
// }

const AiExtended = Ai.extend({
  addCommands() {
    return {
      ...this.parent?.(),

      aiCustomTextCommand:
        (options = {}) =>
        (props) => {
          // 你可以自行实现逻辑,比如获取所选文字并传递给特定命令解析
          return runAiTextCommand(props, 'customCommand', options)
        },
    }
  },
})

// … 这里初始化你的 Tiptap 编辑器实例并注册扩展

const editor = new Editor({
  extensions: [
    /* … 添加其他扩展 */
    AiExtended.configure({
      /* … 添加配置(appId、token 等) */
      onError(error, context) {
        // 处理错误
      },
      aiCompletionResolver: async ({
        action,
        text,
        textOptions,
        extensionOptions,
        defaultResolver,
        editor,
      }) => {
        if (action === 'customCommand') {
          const response = await fetch('https://dummyjson.com/quotes/random')
          const json = await response.json()

          if (!response.ok) {
            throw new Error(`${response.status} ${json?.message}`)
          }

          return json?.quote
        }

        return defaultResolver({
          editor,
          action,
          text,
          textOptions,
          extensionOptions,
          defaultResolver,
        })
      },
    }),
  ],
  content: '',
})

// … 使用以下方式运行新命令:
// editor.chain().focus().aiCustomTextCommand().run()

在流模式下使用你的自定义后端

本例完全依赖自定义后端。

确保 aiStreamResolver 函数返回 ReadableStream<Uint8Array>

请注意:如果你同时想使用流模式和传统的完成模式(非流模式),也需定义 aiCompletionResolver

// ...
import Ai from '@tiptap-pro/extension-ai-advanced'
// ...

Ai.configure({
  appId: 'APP_ID_HERE',
  token: 'TOKEN_HERE',
  // ...
  onError(error, context) {
    // 处理错误
  },
  // 定义流解析函数
  aiStreamResolver: async ({ action, text, textOptions }) => {
    const fetchOptions = {
      method: 'POST',
      headers: {
        accept: 'application/json',
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        ...textOptions,
        text,
      }),
    }

    const response = await fetch(`<YOUR_STREAMED_BACKEND_ENDPOINT>`, fetchOptions)

    if (!response.ok) {
      const json = await response.json()
      throw new Error(`${json?.error} ${json?.message}`)
    }

    return response.body
  },
})