将内容流式写入编辑器
Available in Start plan
streamContent 命令是一个低级 API,用于将内容流式写入编辑器。它支持追加内容,也支持替换指定范围内的内容。当你需要将诸如大型语言模型(LLM)响应这样的大量数据流式写入编辑器时,这个命令非常有用。
高级集成
该命令适用于需要从 URL 或响应体中向编辑器流式写入内容的高级集成场景。
参数
range:可以是单个插入位置,也可以是一个对象,指定需替换的范围,包含 from 和 to 属性。
callback:一个异步函数,接收一个写入函数,用于向编辑器流式写入内容。
callback 参数说明
getWritableStream:返回一个可写流对象,可以用它来分块写入数据到编辑器。
write:一个函数,接收一个对象,包含以下属性:
partial:要插入到编辑器的内容。transform:一个可选函数,接收一个对象,包含以下属性:buffer:流的累积内容。partial:当前的部分内容。editor:编辑器实例。defaultTransform:默认的转换函数。该函数接收累积内容作为输入,并将其插入编辑器。
appendToChain:一个可选函数,用于将命令追加到链中。
使用示例
使用 write API
此示例演示如何从 URL 获取大型数据流,并逐块流式写入编辑器,体现了 streamContent 命令的灵活性。
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
此示例演示另一种使用可写流对象的方式,将数据块写入编辑器。
editor.commands.streamContent({ from: 0, to: 10 }, async ({ getWritableStream }) => {
const response = await fetch('https://example.com/stream')
// 直接将响应体内容传输到编辑器
await response.body?.pipeTo(getWritableStream())
})使用内容变换
你也可以利用 transform 函数,在内容流入编辑器前进行转换。此示例演示如何在流式写入编辑器前对内容进行转换。
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 内容并流式写入编辑器。
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 函数允许在执行命令链之前追加命令。此示例演示如何在执行之前向命令链添加命令。
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 位置相同的层级。
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 定义:
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
}