Skip to content

生成器发布模板协议

目标

生成器发布模板协议 (Generator Runtime Contract) 用于约束生成器(runtime)的发布模板后,模板的运行方式,让同一个生成器既可以作为完整页面运行,也可以作为模板页中的嵌入模块运行。

如果你需要的是 generator-workbench 和 生成器(runtime) 之间的实际通信说明,包括 routeMode、生成器(runtime)事件和反向命令,请同时阅读 应用壳与生成器通信

它解决的是:

  • 同一个生成器同时支持 fullembed 两种模式
  • 模板页只渲染中间画布和右侧部分参数
  • 生成器状态可以被宿主读取、设置、恢复和导出
  • 参数面板可以通过统一 schema 和过滤规则裁剪

它不解决的是:

  • 登录、分享、云保存、积分、计费、导出上传等平台能力实现
  • CMS 配置、发布编排、审批流
  • 顶部导航、登录弹窗、分享弹窗等具体业务 UI 形态

这些仍然分别由 generator-sdkgenerator-control-plane 负责。

与重构交付口径的关系

在旧生成器改造场景里,需要把下面两种口径分开理解:

  • 兼容性重构:以不回归和最小改动为优先,允许先补一部分 runtime 或 adapter
  • 标准化重构:最终要把旧生成器收敛成标准 generator 结构

Generator Runtime Contract标准化重构必要条件,但不是充分条件

也就是说,下面这些情况都不能单独等同于“标准化重构完成”:

  • 只补了 window.__GENERATOR_RUNTIME__
  • 只补了 getState() / setState() / patchState()
  • 只做了 SDK 接入和轻量 bridge 包装
  • 只做了 PanelSchema,但还没有 full / embed

要宣称“标准化重构完成”,至少还应同时具备:

  1. generator-sdk 已接入本次要求的平台能力
  2. full / embed 双入口可运行
  3. window.__GENERATOR_RUNTIME__ 已暴露
  4. getPanelSchema() + PanelFilter 可被宿主消费
  5. 涉及模板场景时,使用统一模板协议
  6. 兼容性、迁移或 CMS 说明已补齐

边界

generator-sdk

负责平台能力:

  • 登录
  • 云保存 / 恢复
  • 历史记录
  • 积分
  • 统一计费
  • 导出到本地 / 打开到 Studio

Generator Runtime Contract

负责宿主与生成器之间的运行时协议:

  • 画布挂载
  • 参数面板挂载
  • 状态获取与设置
  • 参数 schema 暴露
  • 参数过滤
  • 导出数据提供
  • 运行时事件订阅

核心原则

  1. 状态单一事实源 生成器必须由一份可序列化的 state 驱动。模板页和完整页使用同一份业务状态模型。

  2. 壳层与核心解耦 顶部导航、分享、登录、业务浮层属于宿主壳层;画布、参数、导出 provider 属于生成器运行时。

  3. 参数面板来源于 schema 模板页不应硬编码某个生成器的表单逻辑,而应消费运行时暴露的 PanelSchema

  4. 嵌入可预测 同一份 statefullembed 模式下的核心画布结果必须一致。

  5. 渐进兼容 允许旧 bridge 通过 adapter 映射到本协议,但新生成器必须直接实现本协议。

  6. 完成宣称受门禁约束 即使 runtime 已部分落地,只要没有满足完整标准化条件,也只能表述为阶段性交付,不能误报为标准 generator 已完成。

模式定义

full

完整编辑器模式,通常包含:

  • 顶部导航
  • 左侧工具栏
  • 中间画布
  • 右侧完整参数面板

embed

嵌入模式,由宿主页面提供业务壳层,生成器只暴露:

  • 画布
  • 参数面板或其子集
  • 状态同步能力
  • 导出能力

最小接口

V1 要求生成器至少暴露以下接口:

ts
type RuntimeMode = 'full' | 'embed'
type MountTarget = 'full' | 'canvas' | 'panel'

interface GeneratorRuntime {
  version: string
  generatorId: string
  capabilities: RuntimeCapabilities

  mount(options: MountOptions): MountedInstance | Promise<MountedInstance>
  getState(): GeneratorState
  setState(nextState: GeneratorState, options?: SetStateOptions): void | Promise<void>
  patchState(patch: StatePatch, options?: SetStateOptions): void | Promise<void>
  getPanelSchema(options?: GetPanelSchemaOptions): PanelSchema
  export(action: ExportAction, options?: ExportOptions): Promise<ExportResult | null>
  subscribe(listener: RuntimeEventListener): () => void
  dispatchWorkbenchCommand?(command: WorkbenchCommand): void | Promise<void>
}

这组接口定义的是 runtime 的最小协议面,不等于完整交付门槛。对旧生成器来说,它常常对应“阶段 2:补齐 runtime 接口”,而不是全部改造工作的终点。

mount()

负责把生成器挂载到宿主提供的容器中。

ts
interface MountOptions {
  mode: RuntimeMode
  routeMode?: RuntimeMode
  target: MountTarget
  container: HTMLElement
  state?: GeneratorState
  panelFilter?: PanelFilter
  readonly?: boolean
  hostContext?: HostContext
}

要求:

  • mode: 'full' 时允许渲染完整编辑器
  • mode: 'embed' 时必须支持 canvaspanel 挂载
  • routeMode 表示当前页面路由里的 ?mode=,用于区分“runtime 挂载模式”和“页面能力模式”
  • generator-workbench 运行在 shell 模式下时,runtime 仍可能收到 mode: 'full',但同时收到 routeMode: 'embed'
  • 必须支持重复挂载和卸载
  • 必须响应容器尺寸变化

getState() / setState() / patchState()

负责宿主和生成器之间的状态同步。

  • getState() 返回当前完整业务状态
  • setState() 用于整份状态替换
  • patchState() 用于局部更新,适合模板页控制右侧少量参数

getPanelSchema()

返回参数面板描述协议,供宿主渲染全部或部分参数。

export()

由生成器提供导出结果,宿主或 generator-sdk 决定如何下载、打开 Studio、计费。

subscribe()

订阅运行时事件,如状态变化、参数 schema 变化和错误。

dispatchWorkbenchCommand()

接收 workbench 侧发给 runtime 的命令,例如壳层动作、路由同步或未来宿主编排请求。

ts
interface WorkbenchCommand {
  type: string
  data?: unknown
}

这个方法是可选的,但如果你希望 generator-workbench 或其他宿主以公开、可扩展的方式实现 workbench -> runtime 通信,推荐统一使用它作为标准入口。

建议的浏览器注册方式

为了兼容纯 HTML 和 iframe / script 场景,推荐约定:

ts
declare global {
  interface Window {
    __GENERATOR_RUNTIME__?: GeneratorRuntime
    __registerGeneratorRuntime__?: (runtime: GeneratorRuntime) => void
  }
}

状态模型建议

不要求所有生成器共享同一套渲染引擎,但要求共享统一的协议形状。

ts
interface GeneratorState {
  meta: {
    schemaVersion: string
    generatorId: string
    title?: string
    updatedAt?: string
  }
  document: {
    width?: number
    height?: number
    unit?: 'mm' | 'px' | 'in'
    layers?: Array<Record<string, unknown>>
  }
  params: Record<string, unknown>
  selection?: {
    activeLayerId?: string | null
    activeTool?: string | null
  }
  view?: {
    zoom?: number
    panX?: number
    panY?: number
    panelTab?: string
    expandedGroups?: string[]
  }
  assets?: Array<Record<string, unknown>>
}

建议约束

  • metadocument 应稳定,可用于云保存、恢复、模板页渲染
  • params 用于参数面板字段绑定
  • selectionview 允许是编辑态数据,不要求模板页完整消费
  • state 必须可 JSON 序列化

PanelSchema

模板页不应该为每个生成器单独写一份右侧面板,而应消费生成器返回的参数 schema。

ts
interface PanelSchema {
  version: string
  generatorId: string
  groups: PanelGroup[]
}

interface PanelGroup {
  id: string
  title: string
  description?: string
  order?: number
  collapsible?: boolean
  defaultExpanded?: boolean
  fields: PanelField[]
}

interface BaseField {
  id: string
  label: string
  type: string
  bind: {
    path: string
  }
  helpText?: string
  readonly?: boolean
}

字段类型建议

V1 推荐优先支持这些基础字段:

  • text
  • number
  • slider
  • select
  • toggle
  • color
  • image
  • custom

如果某个生成器存在复杂自定义控件,可使用 custom,但仍应通过统一 schema 暴露其占位与绑定信息。

PanelFilter

模板页只渲染部分参数时,必须通过 PanelFilter 裁剪,而不是重新硬编码逻辑。

ts
interface PanelFilter {
  includeGroups?: string[]
  excludeGroups?: string[]
  includeFields?: string[]
  excludeFields?: string[]
  readonlyFields?: string[]
  hiddenFields?: string[]
  orderOverrides?: Array<{ id: string; order: number }>
}

过滤规则

  • 如果某个 group 被排除,则其下字段全部不可见
  • 如果某个 group 过滤后无可见字段,则该 group 自动隐藏
  • includeFields / excludeFields / readonlyFields 的标准匹配值应优先使用 field.bind.path
  • readonlyFields 只改变交互权限,不改变字段可见性
  • 宿主不得直接修改生成器内部字段定义,只能通过 filter 裁剪

Template Definition Protocol

当生成器需要支持“发布模板 / 导入模板”时,推荐统一使用 generator-sdktemplate 模块,而不是为每个生成器单独设计 JSON 结构。

ts
interface GeneratorTemplateDefinition {
  type: 'generator-template'
  version: '1.0.0'
  generatorId: string
  appKey?: string
  templateMeta?: Record<string, unknown>
  defaults: Record<string, unknown>
  panelFilter: PanelFilter
  adjustableFields: Array<{
    groupId: string
    groupTitle?: string
    fieldId: string
    fieldLabel?: string
    path: string
  }>
  metadata?: Record<string, unknown>
}

约定:

  1. defaults 是模板默认状态快照
  2. panelFilter 是模板消费侧唯一标准,宿主应直接消费,不要重新推导一套字段规则
  3. adjustableFields 只用于模板作者工具展示,不替代 panelFilter
  4. 导入模板后,宿主应通过 runtime.setState() / runtime.patchState() + panelFilter 共同恢复模板能力

如果运行时已经接入模板协议,可选暴露能力标记:

ts
capabilities: {
  supportsTemplates?: true
}

embed 模式约束

新生成器在 embed 模式下必须遵守以下规则:

  1. 不渲染顶部导航
  2. 不渲染登录、分享、积分等平台壳层入口
  3. 不自行扣费,由宿主结合 generator-sdk 触发
  4. 不依赖全局 body 布局决定核心渲染
  5. 不写死页面宽高,必须适应容器尺寸
  6. 必须支持只读模式
  7. 同一份 statefull / embed 模式下核心画布结果一致

如果旧生成器只能在 embed 模式下局部运行、但还没有独立 full 入口,仍然不应宣称“已完成标准化重构”。

事件协议

ts
type RuntimeEvent =
  | { type: 'ready' }
  | { type: 'state-change'; state: GeneratorState; patch?: StatePatch; source?: string }
  | { type: 'params_change'; data: { field: string; value: unknown; params?: Record<string, unknown> } }
  | { type: 'panel-schema-change'; schema: PanelSchema }
  | { type: 'select_template'; data: { name: string; category?: string } }
  | { type: 'warning'; code: string; message: string }
  | { type: 'error'; code: string; message: string }

V1 最少要求:

  • 首次挂载后触发一次 ready
  • 参数修改后触发 state-change
  • 如果 runtime 需要提供字段级参数反馈,则在参数面板字段修改后触发 params_change
  • 当 schema 可见性随状态变化时触发 panel-schema-change

如果 runtime 需要通知宿主更多业务侧交互,可以额外发送:

  • params_change
  • select_template

约定:

  • params_change.data.field 必填,表示被修改的参数字段名
  • params_change.data.value 必填,表示该字段的新值
  • params_change.data.params 选填,可用于携带当前完整参数对象
  • data.name 必填,表示选中的模板名称
  • data.category 选填,表示模板分类
  • 当 runtime 被 generator-workbench 承载且页面路由为 ?mode=embed 时,workbench 会把这个 runtime 事件转发给父页面 bridge,事件名为 generator_toSelectTemplate

推荐写法:

ts
const listeners = new Set<(event: RuntimeEvent) => void>()

function emit(event: RuntimeEvent) {
  listeners.forEach((listener) => listener(event))
}

function onTemplateCardClick(template: { name: string; category?: string }) {
  emit({
    type: 'select_template',
    data: {
      name: template.name,
      category: template.category,
    },
  })
}

function onPanelFieldChange(field: string, value: unknown, params: Record<string, unknown>) {
  emit({
    type: 'params_change',
    data: {
      field,
      value,
      params,
    },
  })
}

Workbench -> Runtime 命令约定

如果需要双向通信,推荐按这个方向拆分:

  • subscribe(listener) 负责 runtime -> workbench
  • dispatchWorkbenchCommand(command) 负责 workbench -> runtime

示例:

ts
runtime.dispatchWorkbenchCommand = async (command) => {
  if (command.type === 'open-login') {
    openLoginPanel(command.data)
  }
}

当这套契约运行在 generator-workbench 里时,宿主侧看到的方法名是 workbench.dispatchRuntimeCommand(...)。它只是同一条方向的适配封装,本质上仍然是 workbench -> runtime

导出协议

ts
type ExportAction =
  | 'cover'
  | 'download-svg'
  | 'download-png'
  | 'open-in-studio'

interface ExportResult {
  dataUrl: string
  mimeType: string
  ext: string
  width?: number
  height?: number
  metadata?: Record<string, unknown>
}

职责划分:

  • Runtime 负责生成导出数据
  • generator-sdk 负责下载到本地、打开到 Studio、结合计费
  • 宿主决定何时触发导出

宿主接入示例

模板页左侧画布 + 右侧部分参数的典型接法:

ts
const runtime = window.__GENERATOR_RUNTIME__
const sdk = GeneratorSDK.init({ appKey: 'your_app_key', env: 'prod' })
const importedTemplate = sdk.template.parse(templateText)
const snapshot = sdk.template.toRuntimeSnapshot(importedTemplate)

await runtime.mount({
  mode: 'embed',
  target: 'canvas',
  container: canvasContainer,
  state: snapshot.state,
})

const panelSchema = runtime.getPanelSchema({
  panelFilter: {
    ...snapshot.panelFilter,
  },
})

await runtime.mount({
  mode: 'embed',
  target: 'panel',
  container: panelContainer,
  state: snapshot.state,
  panelFilter: snapshot.panelFilter,
})

与旧 bridge 的关系

当前旧生成器普遍使用 window.__GENERATOR_BRIDGE__。V1 允许通过 adapter 做兼容,但不建议新生成器继续只实现 bridge。

推荐映射:

  • getSnapshot() -> getState()
  • applySnapshot() -> setState()
  • getExportData() -> export()
  • _sync() -> subscribe() + state-change

如果当前仅完成了 bridge 到 runtime 的 adapter 映射,建议在对外汇报中明确写成:

  • 已完成兼容性重构
  • 或已完成阶段 1/2

不要直接写成“已完成标准 generator 改造”。

V1 非目标

以下内容不属于 V1:

  • 通用 JSON 画布渲染引擎
  • 强制所有生成器共享同一套 layer schema
  • 所有参数控件统一由平台渲染
  • 旧生成器一次性全部迁移

V1 只定义一个可被 Skill、MCP、Starter 和模板页共同消费的最小协议。

推荐工程结构

新生成器建议默认按以下结构组织:

txt
src/
  core/
    state.ts
    renderer.ts
    panel-schema.ts
    export.ts
  runtime/
    index.ts
    mount-canvas.ts
    mount-panel.ts
  pages/
    full.ts
    embed.ts
  platform/
    sdk.ts

一句话结论

Generator Runtime Contract 的本质是:

把“生成器如何嵌入到不同宿主中运行”标准化,而不是把所有生成器都改造成同一种渲染引擎。

补一句面向旧生成器重构的判断标准:

runtime contract 很重要,但它只是标准化重构的一部分,不应替代最终完成态门禁。

MIT Licensed