☰
Halo 编辑器运行期元数据(Runtime Metadata)全解析:为 AI Agent 构建可读的富文本组件清单
2026/10/8 18:59:33 网站建设 项目流程

Halo 编辑器运行期元数据(Runtime Metadata)全解析:为 AI Agent 构建可读的富文本组件清单

【免费下载链接】haloHalo 是一款强大易用的开源建站工具,从个人博客、知识库,到企业官网、在线商城,Halo 都能助您轻松实现,一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/halo

导读

@halo-dev/richtext-editor是 Halo 建站系统内置的富文本编辑器包(源码位于 ui/packages/editor),它基于 Tiptap/ProseMirror 构建,并额外提供了一套运行期组件元数据(Runtime Metadata)机制:允许编辑器扩展(Node、Mark、Extension)声明描述自身 schema、属性、结构关系与 AI 使用方式的元数据,最终由生成器汇聚成一份稳定的运行期 Manifest 快照。本文以 runtime-metadata.md 为主线,结合editor-metadata模块源码与测试用例,讲解如何为自定义组件声明元数据、如何扩展现有组件、如何贡献全局属性说明、如何读取 Manifest,以及完整的数据校验与容量约束。

一、什么是运行期元数据:设计定位与核心概念

Halo 富文本编辑器中的每个 Node、Mark 和 Extension,本质上是对最终 ProseMirror schema 的一段描述。运行期元数据机制把这些描述"翻译"成结构化的、面向 AI 与外部消费者的组件清单(Manifest)。

元数据回答四类问题:

  • schema 是什么:该组件的 kind(node/mark)、name、content 表达式、group、属性及默认值;
  • 组件怎么用:它适合什么场景(useWhen)、应避免什么场景(avoidWhen)、属性取值建议(attributeGuidance);
  • 组件之间什么关系:允许的父级、每个父级下出现的最小/最大次数(structure);
  • 如何生成:是否允许 AI 直接产出 HTML(generation.mode),以及是否需要外部能力(requiredCapabilities)。

从源码看,这些信息在 Editor 实例创建后,由 createHaloEditorManifest 从editor.extensionManager.extensions和editor.schema中同步解析生成。文档明确指出:元数据本身不会改变或约束组件行为,它是对运行期能力的纯描述;AI Agent 是当前的主要消费者,其他插件、工具也可以读取它来了解当前编辑器实际注册的组件。

二、声明新组件:addHaloEditorMetadata钩子

扩展作者通过 Tiptap 生命周期钩子addHaloEditorMetadata()声明元数据。文档给出的数学公式节点示例完整展示了声明结构:

import { Node } from "@halo-dev/richtext-editor"; export const MathBlock = Node.create({ name: "mathBlock", group: "block", atom: true, addAttributes() { return { formula: { default: "", }, }; }, parseHTML() { return [{ tag: 'div[data-type="math-block"]' }]; }, renderHTML({ HTMLAttributes }) { return ["div", { ...HTMLAttributes, "data-type": "math-block" }]; }, // 声明运行期元数据中的 AI 使用说明 addHaloEditorMetadata() { return { ai: { description: "A display mathematical formula.", exposure: "available", useWhen: ["Presenting a standalone mathematical expression."], attributeGuidance: { formula: { description: "Formula source written in LaTeX.", format: "LaTeX", examples: ["E = mc^2"], }, }, generation: { mode: "requires-capability", requiredCapabilities: ["math-to-html"], }, examples: [ '<div>import { ExtensionCodeBlock } from "@halo-dev/richtext-editor"; export const HighlightedCodeBlock = ExtensionCodeBlock.extend({ addAttributes() { return { ...this.parent?.(), highlightTheme: { default: null, }, }; }, addHaloEditorMetadata() { return { ai: { attributeGuidance: { highlightTheme: { description: "Syntax-highlighting theme.", allowedValues: ["github-light", "github-dark"], omitWhen: ["The editor default theme should be used."], }, }, }, }; }, });

合并规则(有源码与测试双重印证)

  • 自动合并,无需手动调this.parent():resolveDeclaration会遍历从基类到子类的完整继承链(extensionChain),收集链上所有addHaloEditorMetadata钩子的返回值并逐层合并。测试用例"composes parent hooks once and lets child arrays replace parent arrays"验证了这一点:子类只声明description与aliases,基类的useWhen依然保留在最终元数据中。
  • 数组采用"后者替换"策略:mergeMetadataPatch使用es-toolkit的mergeWith,配合replaceArrays回调——子类声明的数组直接替换父类数组,而非拼接。
  • 最终 Manifest 同时包含原组件描述与新属性:文档明确"最终 Manifest 会同时包含原 code block 的描述和highlightTheme"。

仓库中 code-block.ts 正是这一模式的生产级实例:它给 Tiptap 的 code block 补充了collapsed、theme两个属性,并为language、collapsed、theme提供完整的attributeGuidance,其中allowedValues会动态来自this.options.languages/this.options.themes——说明元数据声明函数内可以访问运行期 options,从而让 Manifest 反映真实配置。

属性级指导(attributeGuidance)的完整字段

字段作用约束
description属性含义说明必填,最长 1,000 字符
format属性值格式(如LaTeX、URL)可选
allowedValues允许取值白名单最多 32 项,限 string/number/boolean/null
examples取值示例最多 32 项
useWhen/omitWhen/guidelines何时使用/何时省略/使用准则数组最多 10 项

attributeGuidance既可以是对象,也可以简写为字符串({ tone: "Configured tone" }等价于{ tone: { description: "Configured tone" } }),这一点在HaloEditorAIAttributeGuidanceDeclaration联合类型中定义,并有测试覆盖。

四、为全局属性贡献说明:Plain Extension 的contributions机制

普通 Extension 不会成为 Manifest 组件,但它可以通过contributions向明确命名的 Node 或 Mark注入元数据——这非常适合addGlobalAttributes()场景,例如给paragraph和heading同时补充tone属性说明:

import { Extension } from "@halo-dev/richtext-editor"; export const Tone = Extension.create({ name: "tone", addGlobalAttributes() { return [ { types: ["paragraph", "heading"], attributes: { tone: { default: null, }, }, }, ]; }, addHaloEditorMetadata() { return { contributions: [ { targets: [ { kind: "node", name: "paragraph" }, { kind: "node", name: "heading" }, ], metadata: { ai: { attributeGuidance: { tone: { description: "Writing tone for this block.", allowedValues: ["neutral", "friendly", "formal"], }, }, }, }, }, ], }; }, });

贡献的落地规则

从 manifest.ts 的实现看:

  1. 只作用于最终 schema 中真实存在的目标:componentExists会检查editor.schema.nodes[name]/editor.schema.marks[name],目标不存在则忽略并告警Ignored metadata contribution for missing node "xxx"。
  2. 冲突按 priority 决胜负:所有贡献先按priority(未配置时默认DEFAULT_PRIORITY = 100)、再按注册顺序、最后按声明顺序排序,后序覆盖前序。测试用例"merges directed contributions by priority and registration order"验证:priority 同为 200 的两个贡献,后注册的high-last胜出。
  3. targets 自动去重:uniqueTargets以kind:name为键去重。

五、组件自身的结构说明:structure元数据

组件只声明自己的父级和数量关系,不跨组件声明子节点(子节点关系由 schema 的content表达式与各组件的structure各自描述):

addHaloEditorMetadata() { return { ai: { description: "An optional caption belonging to a figure.", }, structure: { allowedParents: ["figure"], minPerParent: 0, maxPerParent: 1, }, }; }

这表示:当前组件只能位于figure下,在每个figure中可省略且最多出现一次。

源码中的校验与归一化

normalizeStructure会检查:

  • allowedParents非空,且每个父级必须真实存在于schema.nodes;
  • minPerParent/maxPerParent必须是>= 0的整数(validCount);
  • minPerParent > maxPerParent时整体丢弃并告警。

仓库内置的 figure/index.ts 是structure与ai组合的完整范例:figure组件声明exposure: "recommended"、generation.mode: "direct-html",并给出三种媒体(image/video/audio)的完整 HTML 示例;而figureCaption则声明allowedParents: ["figure"]、minPerParent: 0、maxPerParent: 1。测试用例"normalizes figureCaption structure and produces stable signatures"断言了该结构。

六、禁用与兼容性:失败软着陆设计

ai: false明确禁用 AI 主动使用

return { ai: false };
  • ai: false表示建议 AI 不主动使用该组件;
  • 但组件的 schema 信息(kind、name、content、attributes、htmlParseRules 等)仍然完整出现在 Manifest 中供 AI 理解上下文;
  • 测试用例中doc(文档根节点)即使用{ ai: false },仍出现在组件列表中。

失败软着陆(Fail Soft)

文档列出的三类异常处理,全部有源码实现支撑:

异常场景处理方式源码位置
声明钩子抛错忽略该扩展的元数据,保留其他有效数据,开发环境告警resolveDeclaration中的 try/catch
字段无效(类型错误、超长、引用不存在的属性/父节点)丢弃无效字段,保留有效字段sanitize*系列函数 +normalizeAI对缺失属性的attributeGuidance告警
超出容量限制逐项截断或整体丢弃该组件 AI 元数据,schema 组件本身保留assignStringArray、sanitizeExamples、applyMetadata、enforceManifestMetadataLimit

关键设计意图:生产环境不会因元数据问题阻止编辑器运行——告警仅通过import.meta.env.DEV判断,只在开发环境输出到 console(见warn函数)。

安全边界

  • 未知字段会被丢弃:测试用例传入systemPrompt: "ignored"和unknown: true,最终 Manifest 中均不存在;
  • 禁止放入敏感内容:不要把 system prompt、可执行回调或敏感信息放入元数据——因为它们会被序列化进 Manifest 并可能被分发给 AI 等消费者。

完整容量限制一览

限制项上限源码常量
每段文本(description、guidelines 等)1,000 字符MAX_TEXT_LENGTH
说明数组与 aliases10 项MAX_GUIDANCE_ITEMS(aliases 单项最长 100)
allowedValues与属性示例32 项MAX_ATTRIBUTE_VALUES
组件 HTML 示例3 个、每个 ≤ 4 KiBMAX_HTML_EXAMPLES/MAX_HTML_EXAMPLE_BYTES
单组件 AI 元数据16 KiBMAX_COMPONENT_AI_BYTES
单个 Manifest 的 AI 元数据总量128 KiBMAX_MANIFEST_AI_BYTES

enforceManifestMetadataLimit按组件排序依次累加 AI 元数据体积,超出 128 KiB 后丢弃后续组件的 AI 信息(schema 组件仍保留)。测试用例"keeps schema components when component or Manifest AI limits are exceeded"验证了这一行为。

七、读取运行期 Manifest:AI 插件如何接入

Editor 创建完成后即可同步生成最终快照:

import { createHaloEditorManifest, type HaloEditorManifest, type VueEditor, } from "@halo-dev/richtext-editor"; function editorManifest(editor: VueEditor): HaloEditorManifest { return createHaloEditorManifest(editor); }

Manifest 的结构

HaloEditorManifest(types.ts)包含三部分:

interface HaloEditorManifest { version: 1; // 固定版本号 signature: string; // 稳定的内容签名 components: HaloEditorComponent[]; // 全部 Node 与 Mark 的规范化描述 }

组件描述的内容

nodeComponent/markComponent从最终 schema 中提取(见 manifest.ts):

  • Node 组件:content表达式、group、inline、atom、leaf、code、whitespace、selectable、draggable、defining、isolating;
  • Mark 组件:excludes、inclusive、spanning;
  • 公共部分:attributes(名称、是否必填、默认值,按名称排序)、htmlParseRules(tag/style/priority)、以及可选的ai与structure。

关于signature的稳定性

签名通过object-hash对{ version, components }生成。测试用例"changes the signature only when normalized schema or metadata changes"验证了关键性质:

  • 无关的纯 Extension 注册顺序变化,不影响 signature(因为最终组件只取 schema 中真实存在的 node/mark);
  • 组件描述或元数据变化,signature 必然变化;
  • 同一配置重复生成,signature 完全一致("normalizes figureCaption structure and produces stable signatures"用例)。

这意味着 AI 插件可以缓存 Manifest 并基于 signature 做增量判断,知道"编辑器能力是否发生变化"。

消费者的职责边界

文档结尾明确指出使用原则:

  • AI 插件可以把 Manifest 加入模型上下文(context);
  • 其他消费者可以用它了解当前编辑器实际注册的组件;
  • 消费者自行决定是否根据其中的建议进行额外校验——元数据是"建议"而非"强制约束"。

八、从测试看行为保证:manifest.spec.ts的关键断言

仓库在 manifest.spec.ts 中对上述机制做了系统验证,值得在接入时参考:

测试用例验证点
使用最终配置的 schema 并包含嵌套组件addExtensions嵌套注册的组件会进入 Manifest;options 配置(如tone: "warm")会反映到属性的defaultValue与元数据描述
父钩子只执行一次、子数组替换父数组继承链合并语义
按 priority 与注册顺序合并贡献冲突裁决规则
只保留最终重复身份同一名称多个扩展时,最后生效的扩展胜出
失败软着陆未知字段、超长数组、缺失属性、无效父节点、抛错钩子均被优雅处理
内置组件全覆盖默认ExtensionsKit下每个组件都有显式 AI 声明且无告警
内置组件可生成代表性变体figure的 img/video/audio、heading的 h1-h3、columns的 cols=2/3 等示例均包含在元数据中

最后两条用例对插件作者尤为重要:Halo 内置的所有默认组件都有完整的 AI 元数据声明,插件扩展这些组件时会自动继承其描述,无需重复编写。

九、实战接入清单

  1. 自定义 Node/Mark:在Node.create/Mark.create中实现addHaloEditorMetadata(),至少提供ai.description;如需 AI 生成,补充generation与examples。
  2. 扩展内置组件:.extend()后只写局部补丁,继承链自动合并;数组字段记得用"替换"语义设计。
  3. 全局属性说明:用 Plain Extension +contributions向目标组件注入attributeGuidance,注意目标必须真实存在。
  4. 结构约束:为有明确父子关系的组件声明structure.allowedParents+minPerParent/maxPerParent。
  5. 敏感组件:返回{ ai: false }阻止 AI 主动生成,schema 信息仍保留。
  6. 读取与消费:Editor 就绪后调用createHaloEditorManifest(editor),把signature与components交给 AI 上下文或能力探测逻辑。
  7. 容量自检:遵循第六节限制表,避免元数据被截断或丢弃;利用开发环境 console 告警(前缀[halo-editor-metadata])定位无效声明。

结语

Halo 编辑器运行期元数据机制,本质上是把"编辑器最终长什么样、每个组件怎么用"变成一份结构化、可校验、带签名、AI 可消费的声明式契约。通过addHaloEditorMetadata钩子与createHaloEditorManifest生成器,插件作者可以低成本地为 AI 提供高质量的组件使用指导,而 Halo 内置组件与完整的测试套件则保证了这份契约的稳定性与兼容性。对于希望在 Halo 生态中构建 AI 写作、内容生成等能力的开发者,掌握本文所述的声明语法、合并规则、贡献机制与容量约束,是让 AI 正确理解并使用编辑器组件的第一步。

【免费下载链接】haloHalo 是一款强大易用的开源建站工具,从个人博客、知识库,到企业官网、在线商城,Halo 都能助您轻松实现,一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/halo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询