生成器发布模板协议
目标
生成器发布模板协议 (Generator Runtime Contract) 用于约束生成器(runtime)的发布模板后,模板的运行方式,让同一个生成器既可以作为完整页面运行,也可以作为模板页中的嵌入模块运行。
如果你需要的是 generator-workbench 和 生成器(runtime) 之间的实际通信说明,包括 routeMode、生成器(runtime)事件和反向命令,请同时阅读 应用壳与生成器通信。
它解决的是:
- 同一个生成器同时支持
full和embed两种模式 - 模板页只渲染中间画布和右侧部分参数
- 生成器状态可以被宿主读取、设置、恢复和导出
- 参数面板可以通过统一 schema 和过滤规则裁剪
它不解决的是:
- 登录、分享、云保存、积分、计费、导出上传等平台能力实现
- CMS 配置、发布编排、审批流
- 顶部导航、登录弹窗、分享弹窗等具体业务 UI 形态
这些仍然分别由 generator-sdk 和 generator-control-plane 负责。
与重构交付口径的关系
在旧生成器改造场景里,需要把下面两种口径分开理解:
兼容性重构:以不回归和最小改动为优先,允许先补一部分 runtime 或 adapter标准化重构:最终要把旧生成器收敛成标准 generator 结构
Generator Runtime Contract 是 标准化重构 的必要条件,但不是充分条件。
也就是说,下面这些情况都不能单独等同于“标准化重构完成”:
- 只补了
window.__GENERATOR_RUNTIME__ - 只补了
getState()/setState()/patchState() - 只做了 SDK 接入和轻量 bridge 包装
- 只做了
PanelSchema,但还没有full/embed
要宣称“标准化重构完成”,至少还应同时具备:
generator-sdk已接入本次要求的平台能力full/embed双入口可运行window.__GENERATOR_RUNTIME__已暴露getPanelSchema()+PanelFilter可被宿主消费- 涉及模板场景时,使用统一模板协议
- 兼容性、迁移或 CMS 说明已补齐
边界
generator-sdk
负责平台能力:
- 登录
- 云保存 / 恢复
- 历史记录
- 积分
- 统一计费
- 导出到本地 / 打开到 Studio
Generator Runtime Contract
负责宿主与生成器之间的运行时协议:
- 画布挂载
- 参数面板挂载
- 状态获取与设置
- 参数 schema 暴露
- 参数过滤
- 导出数据提供
- 运行时事件订阅
核心原则
状态单一事实源 生成器必须由一份可序列化的
state驱动。模板页和完整页使用同一份业务状态模型。壳层与核心解耦 顶部导航、分享、登录、业务浮层属于宿主壳层;画布、参数、导出 provider 属于生成器运行时。
参数面板来源于 schema 模板页不应硬编码某个生成器的表单逻辑,而应消费运行时暴露的
PanelSchema。嵌入可预测 同一份
state在full与embed模式下的核心画布结果必须一致。渐进兼容 允许旧 bridge 通过 adapter 映射到本协议,但新生成器必须直接实现本协议。
完成宣称受门禁约束 即使 runtime 已部分落地,只要没有满足完整标准化条件,也只能表述为阶段性交付,不能误报为标准 generator 已完成。
模式定义
full
完整编辑器模式,通常包含:
- 顶部导航
- 左侧工具栏
- 中间画布
- 右侧完整参数面板
embed
嵌入模式,由宿主页面提供业务壳层,生成器只暴露:
- 画布
- 参数面板或其子集
- 状态同步能力
- 导出能力
最小接口
V1 要求生成器至少暴露以下接口:
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()
负责把生成器挂载到宿主提供的容器中。
interface MountOptions {
mode: RuntimeMode
routeMode?: RuntimeMode
target: MountTarget
container: HTMLElement
state?: GeneratorState
panelFilter?: PanelFilter
readonly?: boolean
hostContext?: HostContext
}要求:
mode: 'full'时允许渲染完整编辑器mode: 'embed'时必须支持canvas或panel挂载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 的命令,例如壳层动作、路由同步或未来宿主编排请求。
interface WorkbenchCommand {
type: string
data?: unknown
}这个方法是可选的,但如果你希望 generator-workbench 或其他宿主以公开、可扩展的方式实现 workbench -> runtime 通信,推荐统一使用它作为标准入口。
建议的浏览器注册方式
为了兼容纯 HTML 和 iframe / script 场景,推荐约定:
declare global {
interface Window {
__GENERATOR_RUNTIME__?: GeneratorRuntime
__registerGeneratorRuntime__?: (runtime: GeneratorRuntime) => void
}
}状态模型建议
不要求所有生成器共享同一套渲染引擎,但要求共享统一的协议形状。
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>>
}建议约束
meta和document应稳定,可用于云保存、恢复、模板页渲染params用于参数面板字段绑定selection和view允许是编辑态数据,不要求模板页完整消费state必须可 JSON 序列化
PanelSchema
模板页不应该为每个生成器单独写一份右侧面板,而应消费生成器返回的参数 schema。
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 推荐优先支持这些基础字段:
textnumbersliderselecttogglecolorimagecustom
如果某个生成器存在复杂自定义控件,可使用 custom,但仍应通过统一 schema 暴露其占位与绑定信息。
PanelFilter
模板页只渲染部分参数时,必须通过 PanelFilter 裁剪,而不是重新硬编码逻辑。
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.pathreadonlyFields只改变交互权限,不改变字段可见性- 宿主不得直接修改生成器内部字段定义,只能通过 filter 裁剪
Template Definition Protocol
当生成器需要支持“发布模板 / 导入模板”时,推荐统一使用 generator-sdk 的 template 模块,而不是为每个生成器单独设计 JSON 结构。
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>
}约定:
defaults是模板默认状态快照panelFilter是模板消费侧唯一标准,宿主应直接消费,不要重新推导一套字段规则adjustableFields只用于模板作者工具展示,不替代panelFilter- 导入模板后,宿主应通过
runtime.setState()/runtime.patchState()+panelFilter共同恢复模板能力
如果运行时已经接入模板协议,可选暴露能力标记:
capabilities: {
supportsTemplates?: true
}embed 模式约束
新生成器在 embed 模式下必须遵守以下规则:
- 不渲染顶部导航
- 不渲染登录、分享、积分等平台壳层入口
- 不自行扣费,由宿主结合
generator-sdk触发 - 不依赖全局
body布局决定核心渲染 - 不写死页面宽高,必须适应容器尺寸
- 必须支持只读模式
- 同一份
state在full/embed模式下核心画布结果一致
如果旧生成器只能在 embed 模式下局部运行、但还没有独立 full 入口,仍然不应宣称“已完成标准化重构”。
事件协议
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_changeselect_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
推荐写法:
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 -> workbenchdispatchWorkbenchCommand(command)负责workbench -> runtime
示例:
runtime.dispatchWorkbenchCommand = async (command) => {
if (command.type === 'open-login') {
openLoginPanel(command.data)
}
}当这套契约运行在 generator-workbench 里时,宿主侧看到的方法名是 workbench.dispatchRuntimeCommand(...)。它只是同一条方向的适配封装,本质上仍然是 workbench -> runtime。
导出协议
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、结合计费- 宿主决定何时触发导出
宿主接入示例
模板页左侧画布 + 右侧部分参数的典型接法:
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 和模板页共同消费的最小协议。
推荐工程结构
新生成器建议默认按以下结构组织:
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 很重要,但它只是标准化重构的一部分,不应替代最终完成态门禁。