---
title: "将内容流式写入编辑器"
description: "低级 API，用于将内容直接流式写入编辑器内容。详情请参阅我们的文档。"
canonical_url: "https://tiptap.zhcndoc.com/content-ai/capabilities/generation/text-generation/stream"
---

# 将内容流式写入编辑器

低级 API，用于将内容直接流式写入编辑器内容。详情请参阅我们的文档。

- **1. 激活试用或订阅**

  在您的账户中开始[免费试用](https://cloud.tiptap.dev/v2?trial=true)或[订阅 Start 计划](https://cloud.tiptap.dev/v2/billing)。
- **2. 集成 AI 提供商**

  在您的[AI 设置](https://cloud.tiptap.dev/v2/cloud/ai)中配置 OpenAI，或查看
  [自定义 LLM 指南](https://tiptap.zhcndoc.com/content-ai/capabilities/generation/custom-llms.md)。
- **3. 从私有仓库安装**

  要安装前端扩展，请按
  [安装指南](https://tiptap.zhcndoc.com/guides/pro-extensions.md)进行身份验证，使用 Tiptap 的私有 npm 仓库。

`streamContent` 命令是一个低级 API，用于将内容流式写入编辑器。它支持追加内容，也支持替换指定范围内的内容。当你需要将诸如大型语言模型（LLM）响应这样的大量数据流式写入编辑器时，这个命令非常有用。

> **高级集成:**
>
> 该命令适用于需要从 URL 或响应体中向编辑器流式写入内容的高级集成场景。

### 参数

**range**：可以是单个插入位置，也可以是一个对象，指定需替换的范围，包含 `from` 和 `to` 属性。
**callback**：一个异步函数，接收一个写入函数，用于向编辑器流式写入内容。

### `callback` 参数说明

**getWritableStream**：返回一个可写流对象，可以用它来分块写入数据到编辑器。
**write**：一个函数，接收一个对象，包含以下属性：

- `partial`：要插入到编辑器的内容。
- `transform`：一个可选函数，接收一个对象，包含以下属性：
  - `buffer`：流的累积内容。
  - `partial`：当前的部分内容。
  - `editor`：编辑器实例。
  - `defaultTransform`：默认的转换函数。该函数接收累积内容作为输入，并将其插入编辑器。
- `appendToChain`：一个可选函数，用于将命令追加到链中。

## 使用示例

### 使用 `write` API

此示例演示如何从 URL 获取大型数据流，并逐块流式写入编辑器，体现了 `streamContent` 命令的灵活性。

```ts
editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('无法从响应体获取读取器。')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })
    write({ partial: chunk })
  }
})
```

### 使用 `getWritableStream` API

此示例演示另一种使用可写流对象的方式，将数据块写入编辑器。

```ts
editor.commands.streamContent({ from: 0, to: 10 }, async ({ getWritableStream }) => {
  const response = await fetch('https://example.com/stream')
  // 直接将响应体内容传输到编辑器
  await response.body?.pipeTo(getWritableStream())
})
```

### 使用内容变换

你也可以利用 `transform` 函数，在内容流入编辑器前进行转换。此示例演示如何在流式写入编辑器前对内容进行转换。

```ts
editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('无法从响应体获取读取器。')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })

    write({
      partial: transformedChunk,
      transform: ({ buffer, partial, editor, defaultTransform }) => {
        // 使用默认转换函数将整个缓冲区内容转换为大写字母并插入编辑器
        return defaultTransform(buffer.toUpperCase())
      },
    })
  }
})
```

**使用场景:** 从 URL 解析 Markdown 内容并流式写入编辑器。

```ts
import { marked } from 'marked'

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('无法从响应体获取读取器。')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })

    write({
      partial: chunk,
      transform: ({ buffer, partial, editor, defaultTransform }) => {
        // 解析 Markdown 内容为 HTML 字符串，并插入编辑器
        return defaultTransform(marked.parse(buffer))
      },
    })
  }
})
```

### 使用 `appendToChain` 选项

`appendToChain` 函数允许在执行命令链之前追加命令。此示例演示如何在执行之前向命令链添加命令。

```ts
import { selectionToInsertionEnd } from '@tiptap/core'

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  write({
    partial: token,
    appendToChain: (chain) =>
      chain
        // 将选择移动到插入内容的末尾
        .command(({ tr }) => {
          selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
          return true
        })
        // 滚动编辑器视图至插入内容末尾
        .scrollIntoView(),
  })
})
```

### 使用 `streamContent` 的 `respondInline` 选项

默认情况下，`respondInline` 为 `true`。插入块级内容时，有时你可能想将内容作为同级节点插入，而不是直接作为块插入。你可以使用 `respondInline` 选项，将内容插入到与 `from` 位置相同的层级。

```ts
editor.commands.setContent('<p>123</p>')
editor.commands.streamContent(
  4,
  async ({ write }) => {
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 10)
    })
    write({ partial: '<p>hello ' })
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 10)
    })
    write({ partial: 'world</p><p>ok</p>' })
  },
  { respondInline: true },
)
// 输出: <p>123hello world</p><p>ok</p>
// 与 `respondInline` 为 `false` 时的输出不同：<p>123</p><p>hello work</p><p>ok</p>
```

## 技术细节

以下是 `streamContent` 命令的完整 TypeScript 定义：

```ts
declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    streamContent: {
      streamContent: (
        /**
         * 插入内容的位置。
         */
        position: number | Range,
        /**
         * 向编辑器写入内容的回调函数。
         */
        callback: (options: StreamContentAPI) => Promise<any>,
        /**
         * 传递给 `insertContentAt` 命令的选项。
         */
        options?: {
          parseOptions?: NonNullable<
            Parameters<RawCommands['insertContentAt']>['2']
          >['parseOptions']
          /**
           * 这会将内容插入到与 `from` 位置相同的层级。
           * 实际上，这会将内容作为 `from` 位置节点的同级节点插入。
           * @default true
           */
          respondInline?: boolean
        },
      ) => ReturnType
    }
  }
}

type StreamContentAPI = {
  /**
   * 写入内容到编辑器的函数。
   */
  write: (ctx: {
    /**
     * 流的部分内容，用于插入。
     */
    partial: string
    /**
     * 允许在插入编辑器之前转换内容的函数。
     * 必须返回 Prosemirror 的 `Fragment` 或 `Node`。
     */
    transform?: (ctx: {
      /**
       * 累积的流内容。
       */
      buffer: string
      /**
       * 当前部分内容。
       */
      partial: string
      editor: Editor
      /**
       * 默认的转换函数。
       */
      defaultTransform: (
        /**
         * 作为 HTML 字符串插入的内容。
         * @default ctx.buffer
         */
        htmlString?: string,
      ) => Fragment
    }) => Fragment | Node | Node[]
    /**
     * 允许在执行之前将命令追加到命令链。
     */
    appendToChain?: (chain: ChainedCommands) => ChainedCommands
  }) => {
    /**
     * 当前写入的缓冲区。
     */
    buffer: string
    /**
     * 插入内容的起始位置。
     */
    from: number
    /**
     * 插入内容的结束位置。
     */
    to: number
  }
  /**
   * 可写流，用于向编辑器写入内容。
   * @example fetch('https://example.com/stream').then(response => response.body.pipeTo(ctx.getWritableStream()))
   */
  getWritableStream: () => WritableStream
}
```
