模板工作流

实验性功能

此功能属于实验性质,将来版本中可能会发生变化。

使用 AI 生成的内容填充结构化的 Tiptap 模板,并将结果插入编辑器中。

查看 GitHub 上的源码

模板工作原理

Tiptap 模板是带有特殊属性的 Tiptap JSON 结构,用于标记动态部分:

  • _templateSlot(字符串键):整个节点将被 AI 生成的 HTML 内容替换。
  • _templateIf(字符串键):节点仅在布尔值为真时才包含。
  • _templateAttributes(由 {key, attribute} 对象组成的数组):将特定节点属性设为 AI 生成的值。

以下是一个 Tiptap JSON 格式的示例模板:

{
  "type": "doc",
  "content": [
    {
      "type": "heading",
      "attrs": { "level": 1 },
      "content": [{ "type": "text", "text": "保密协议" }]
    },
    {
      "type": "paragraph",
      "attrs": { "_templateSlot": "parties" },
      "content": [{ "type": "text", "text": "当事方信息将在此生成。" }]
    },
    {
      "type": "paragraph",
      "attrs": { "_templateIf": "includeArbitration" },
      "content": [{ "type": "text", "text": "仲裁条款内容..." }]
    },
    {
      "type": "heading",
      "attrs": {
        "level": 1,
        "_templateAttributes": [{ "key": "sectionLevel", "attribute": "level" }]
      },
      "content": [{ "type": "text", "text": "适用法律" }]
    }
  ]
}

AI 会生成含有每个键对应值的 JSON 对象:

{
  "parties": "<p>本协议由 <strong>Acme Corp</strong> 与 <strong>Beta LLC</strong> 签订。</p>",
  "includeArbitration": true,
  "sectionLevel": 2
}

技术栈

项目概览

此示例使用 AI 工具包的模板工作流,将 AI 生成内容填充到法律文档模板(保密协议)中。模板包含预定义部分的插槽、条件条款和动态属性。AI 填充动态部分,其余固定的法律模板内容保持不变。

安装

创建一个 Next.js 项目:

npx create-next-app@latest template-workflow

安装核心 Tiptap 包以及用于 OpenAI 的 Vercel AI SDK

npm install @tiptap/react @tiptap/starter-kit ai @ai-sdk/openai zod

安装 Tiptap AI 工具包:

Pro 版本包

AI 工具包为专业版包。安装前请先根据 私有注册表指南 配置对私有 NPM 注册表的访问权限。

npm install @tiptap-pro/ai-toolkit @tiptap-pro/ai-toolkit-tool-definitions

服务端设置

创建一个 API 端点,使用 Vercel AI SDK 调用 OpenAI 模型。

如果你的后端不是 TypeScript,请参阅 非 TypeScript 后端

在 API 端点内部,使用 createTemplateWorkflow 将 HTML 模板转换为工作流配置。该函数会自动提取 HTML 中所有模板键,生成系统提示,并创建输出模式。

// app/api/template-workflow/route.ts
import { openai } from '@ai-sdk/openai'
import { createTemplateWorkflow } from '@tiptap-pro/ai-toolkit-tool-definitions'
import { Output, streamText } from 'ai'

export async function POST(req: Request) {
  const { htmlTemplate, task } = await req.json()

  // 从 HTML 模板创建工作流。
  // 会自动提取模板键,生成提示和输出模式。
  const workflow = createTemplateWorkflow({ htmlTemplate })

  const result = streamText({
    model: openai('gpt-5.4-mini'),
    system: workflow.systemPrompt,
    prompt: JSON.stringify({
      task,
      context: 'Additional background information related to the task',
    }),
    output: Output.object({ schema: workflow.zodOutputSchema }),
  })

  return result.toTextStreamResponse()
}

客户端设置

客户端定义模板为 Tiptap JSON。使用 Vercel AI SDK 的 useObject 钩子来流式接收服务器部分返回的值。传入一个宽松的 Zod schema (z.object({}).passthrough()) 来接受服务器返回的任意属性。

随着部分值的到达,调用 templateWorkflow 并传入 hasFinishedworkflowId 来逐步填充模板。workflowId 允许流式模式,方法会追踪多次调用间的插入范围,迭代替换内容。在调用 API 之前,使用 createHtmlTemplate 方法将模板转换为 HTML,供服务器解析模板键。

// app/template-workflow/page.tsx
'use client'

import { experimental_useObject as useObject } from '@ai-sdk/react'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { AiToolkit, getAiToolkit } from '@tiptap-pro/ai-toolkit'
import { useEffect, useState } from 'react'
import { v4 as uuid } from 'uuid'
import { z } from 'zod'

// 定义模板为 Tiptap JSON
const template = {
  type: 'doc',
  content: [
    {
      type: 'heading',
      attrs: { level: 1 },
      content: [{ type: 'text', text: '保密协议' }],
    },
    {
      type: 'paragraph',
      attrs: { _templateSlot: 'parties' },
      content: [{ type: 'text', text: '当事方详情...' }],
    },
    // ... 更多模板内容
  ],
}

// 宽松的 schema,接受服务器返回任意属性
const templateSchema = z.object({}).passthrough()

export default function Page() {
  const editor = useEditor({
    immediatelyRender: false,
    extensions: [StarterKit, AiToolkit],
    content: '<p>点击“生成”以填充模板。</p>',
  })

  const [workflowId, setWorkflowId] = useState('')

  const { submit, isLoading, object } = useObject({
    api: '/api/template-workflow',
    schema: templateSchema,
  })

  // 监听部分结果流式到达
  useEffect(() => {
    if (!editor || !object) return

    const toolkit = getAiToolkit(editor)
    toolkit.templateWorkflow({
      template,
      values: object as Record<string, unknown>,
      position: 'document',
      hasFinished: !isLoading,
      workflowId,
    })
  }, [object, workflowId, editor, isLoading])

  const generate = () => {
    if (!editor) return

    const toolkit = getAiToolkit(editor)
    const htmlTemplate = toolkit.createHtmlTemplate(template)

    setWorkflowId(uuid())
    submit({
      htmlTemplate,
      task: '生成 Acme Corp 和 Beta LLC 之间的保密协议',
    })
  }

  if (!editor) return null

  return (
    <div>
      <EditorContent editor={editor} />
      <button onClick={generate} disabled={isLoading}>
        {isLoading ? '生成中…' : '生成'}
      </button>
    </div>
  )
}

最终效果

通过额外的 CSS 样式,效果是一个精致的模板工作流应用:

查看 GitHub 上的源码

在文档部分插入模板

若要将模板结果插入到特定区域,而不是替换整个文档,请设置 position 选项。它可以是位置或一个 Range

// 在特定范围插入模板结果
toolkit.templateWorkflow({
  template,
  values: object,
  position: { from: 10, to: 50 },
  hasFinished: !isLoading,
  workflowId,
})

构建模板编辑器

首先,在编辑器中注册 TemplateField 扩展。此扩展将四个模板属性(_templateSlot_templateIf_templateAttributes_templateFieldMetadata)作为全局属性添加到编辑器 schema 中的所有节点类型:

import { TemplateField } from '@tiptap-pro/ai-toolkit'

const editor = new Editor({
  extensions: [StarterKit, TemplateField],
})

TemplateField 与 AiToolkit

你可以将 TemplateFieldAiToolkit 扩展一起使用,或者单独使用它,若你只需要模板字段支持而不需要完整的 AI 工具包功能。

TemplateField 扩展提供命令,用于在当前选择的节点上设置和取消模板属性:

// 将选中节点标记为模板插槽
editor.commands.setTemplateSlot('intro')

// 移除模板插槽
editor.commands.unsetTemplateSlot()

// 将选中节点标记为条件包含
editor.commands.setTemplateIf('showDisclaimer')

// 移除条件
editor.commands.unsetTemplateIf()

// 将模板键映射到节点属性
editor.commands.setTemplateAttributes([{ key: 'sectionLevel', attribute: 'level' }])

// 移除属性映射
editor.commands.unsetTemplateAttributes()

当你想要直接在模板中存储关于字段的额外信息时,请使用 _templateFieldMetadata。元数据值是可选的,可以包含任何 JSON 对象,以帮助你的应用程序稍后解释该字段:

{
  "type": "paragraph",
  "attrs": {
    "_templateSlot": "intro",
    "_templateFieldMetadata": {
      "label": "Introduction",
      "group": "summary",
      "analyticsKey": "template.intro"
    }
  },
  "content": [{ "type": "text", "text": "Placeholder" }]
}

你可以将元数据附加到插槽字段、条件字段和属性字段。元数据不会改变模板的填充方式。它仅用于你自己的应用程序逻辑。

使用 [_templateslot] CSS 属性选择器为模板插槽设置样式。TemplateField 扩展在 DOM 中输出小写的模板属性:

[_templateslot] {
  border: 2px dashed #6a00f5;
  border-radius: 4px;
  padding: 4px 8px;
  background-color: rgba(106, 0, 245, 0.05);
  position: relative;
}

[_templateslot]::after {
  content: attr(_templateslot);
  position: absolute;
  top: -10px;
  right: 4px;
  font-size: 10px;
  background: #6a00f5;
  color: white;
  padding: 0 4px;
  border-radius: 2px;
}

填充后保留模板字段

默认情况下,templateWorkflow 方法填充模板后会移除所有 _templateSlot 属性。设置 preserveSlotAttrtrue,可在填充节点上保留这些属性:

toolkit.templateWorkflow({
  template,
  values: object,
  position: 'document',
  hasFinished: !isLoading,
  workflowId,
  preserveSlotAttr: true,
})

当启用 preserveSlotAttr 时,每个填充节点都会保留其 _templateSlot 属性。这允许你:

  • 识别 AI 填充的内容:检查文档的哪些部分是由 AI 生成的。
  • 重新填充模板字段:将填充后的文档用作新模板以重新生成特定部分。
  • 保持字段元数据可用:稍后从填充后的文档中读取 _templateFieldMetadata

重新填充模板字段

preserveSlotAttrtrue 时,填充后的文档仍包含 _templateSlot 属性。你可以提取文档 JSON,并传回 templateWorkflow 来重新填充特定字段:

// 将当前文档提取为模板
const currentDoc = editor.getJSON()

// 使用新值重新填充
const toolkit = getAiToolkit(editor)
toolkit.templateWorkflow({
  template: currentDoc,
  values: newValues,
  position: 'document',
  preserveSlotAttr: true,
})

这支持迭代式编辑流程,用户可请求 AI 重新生成文档的单个部分,而不会影响其他内容。

读取字段内容和元数据

AI 工具包还导出 getTemplateFieldMatches(),这是一个辅助函数,用于扫描 Tiptap JSON 文档中的字段名,并按文档顺序返回所有匹配的字段。

当你想要以下操作时,这很有用:

  • 读取模板字段的当前内容
  • 访问存储在该字段上的 _templateFieldMetadata
  • 支持重新生成、分析或自定义 UI 标签等工作流
import { getTemplateFieldMatches } from '@tiptap-pro/ai-toolkit'

const matches = getTemplateFieldMatches({
  document: editor.getJSON(),
  fieldName: 'intro',
})

for (const match of matches) {
  console.log(match.fieldType)
  console.log(match.metadata)
  console.log(match.content)
}

每个结果包括:

  • fieldType:匹配是否来自 _templateSlot_templateIf_templateAttributes
  • metadata_templateFieldMetadata 对象,或 null
  • content:匹配节点的內容(Tiptap JSON 格式),空节点则为 null

控制哪些模板字段是必填的

默认情况下,所有模板字段都是必填的——AI 必须填写每个字段。要将特定字段设为可选,请在服务器端使用 requiredSlotsrequiredConditionsrequiredAttributes。仅列表中的字段是必填,未列出的字段则为可选:

// 服务器端:仅 "intro" 是必填,其他插槽为可选
const workflow = createTemplateWorkflow({
  htmlTemplate,
  requiredSlots: ['intro'],
})

AI 会始终填充必填插槽,也可能根据任务上下文选择填充其他插槽。

要将某类字段全部设为可选,请传入空数组:

// 所有插槽可选,但特定条件和属性为必填
const workflow = createTemplateWorkflow({
  htmlTemplate,
  requiredSlots: [],
  requiredConditions: ['includeDisclaimer'],
  requiredAttributes: ['headingLevel'],
})

相关指南