OpenUI
OpenUI 是一套生成式 UI 框架。它面向模型的输出格式 OpenUI Lang 是一种紧凑、按行组织、为流式传输设计的语言。Agent 不需要生成可执行的界面代码,而是从应用控制的组件库中选择 Card(...)、Button(...) 等组件表达式。
@lynx-js/genui/openui 是 OpenUI Lang 在 ReactLynx 上的渲染实现。它会增量解析模型输出,把可识别的表达式映射为已注册的 ReactLynx 组件,再通过 Lynx 的原生渲染管线呈现界面。
组件实现、视觉设计、可用工具、导航能力和 Agent 连接仍由应用负责。OpenUI Library 定义组件词汇;toolProvider 与 onAction 则分别控制工具和宿主侧副作用。
在线示例
下面的示例会把一段模拟的 OpenUI Lang 响应流式传给 Renderer。随着数据块逐步到达,Renderer 会渐进构建一份上海两日行程。你可以点击询问替代方案或使用此计划来查看交给宿主处理的 Action,也可以点击重新播放重启流式响应。
OpenUI 如何工作
一套 OpenUI 接入包含五个部分:
- 定义 Library:创建组件定义及其 Zod Schema。
createOpenUiLibrary()默认包含布局、内容、按钮、媒体、浮层和输入组件,再应用自定义扩展;也可以只使用显式选择的 definitions 构建 Library。 - 配置 Agent:使用同一份组件契约生成 OpenUI system prompt,并提供给模型。
- 流式传输 OpenUI Lang:把模型返回的每个增量片段追加到响应文本中。语句采用函数式记法,例如
title = TextContent("Hello")。 - 解析并渲染:
<OpenUiRenderer>增量解析已经完整的语句、解析前向引用,并渲染已注册的 ReactLynx 组件。 - 处理运行时副作用:Renderer 管理本地状态与工具调用;需要宿主处理的 Action 则通过
onAction交给应用。
这让生成组件始终处于明确词汇范围内:未注册的组件名不会被渲染,Agent 也不会向应用注入任意 ReactLynx 代码。Query()、Mutation() 等语言内置能力属于运行时能力,其实际权限需要通过 toolProvider 与 onAction 约束。
快速开始
安装 Renderer 及其 ReactLynx peer dependencies:
在应用样式中引入 OpenUI 样式:
已发布的 @lynx-js/genui@0.0.6 使用 renderer.css。当前 lynx-stack/main 源码已把宿主侧主题变量移到 @lynx-js/genui/openui/styles/theme.css;直接基于 main 开发,或使用明确导出该入口的版本时,请改用该路径。
创建一个引用稳定的 Library,再把 OpenUI Lang 源码传给 Renderer:
OpenUI Lang 使用位置参数,参数顺序由组件 Schema 决定。例如,内置 Stack 的签名是 Stack(children, direction?, wrap?, gap?, align?, justify?)。请在 Components Playground 中查看当前签名,不要凭组件名称猜测参数位置。
生成 Agent 指令
Agent 必须了解与 Renderer 一致的组件及语言契约。在没有 ReactLynx 运行时依赖的服务端生成内置 system prompt:
把 systemPrompt 作为模型的 system instruction。添加自定义组件时,需要同步维护服务端 prompt 定义和客户端 Library 定义,避免 Agent 输出 Renderer 无法识别的组件或参数。
流式传输模型输出
response 表示当前已经收到的完整文本,而不是最新的单个增量片段。按顺序追加 chunk,并让 isStreaming 与真实网络生命周期保持一致:
当 isStreaming 为 true 时,运行时会延后 Query() 和 Mutation() 相关工作,并抑制流式解析过程中的临时错误。只在流真正结束后把它设为 false;否则可能提前执行不完整的工具调用,或让 Query 一直无法启动。
接入状态、工具与 Action
OpenUI Lang v0.5 可以描述响应式状态、读写操作和有序 Action:
通过 toolProvider 把 Query() 和 Mutation() 中的名称连接到应用能力:
流结束且 Query statement 完整后,它的 default 或预取结果会参与第一次同步渲染。传入 toolProvider 时,Query() 会在 commit 后重新校验,并在它依赖的 $variable 变化时重新执行。Mutation() 在 Action 通过 @Run 调用前不会执行。Action step 会按顺序访问,但只有 @Run(mutation) 会等待执行结果,并在失败时停止后续 step。@Run(query) 启动重新获取后便会立即进入下一个 step。
预取 Query 结果
如果 Query 数据在 ReactLynx 首屏前已经可用,可以通过 initialQueryResults 传入。每个 key 都是 Query assignment name(statement ID),而不是 tool name,因此调用同一个工具但参数不同的 Queries 也不会混淆:
预取值(包括 null、false 或 0)会在第一次同步渲染时优先于 Query default。工具仍不会在 render 阶段执行:传入 toolProvider 时,runtime 会在 commit 后重新校验,并在请求成功后替换 fallback。重新校验失败时会保留预取 fallback,并进入正常的 Query 错误处理流程。如果预取结果是无需重新校验的不可变 snapshot,可以不传 toolProvider。
Query 数据只属于当前的原始 response。替换 response 时,即使新的 response 复用了相同的 assignment name,也应传入与它匹配的 initialQueryResults;runtime 不会暴露上一个 response 遗留的数据。
onStateUpdate 快照会直接保存 $variable 的值,而表单控件可能使用 { value, componentType } 结构。持久化逻辑需要兼容这两种形态,并能安全处理重复快照。
运行时和宿主对 Action step 的分工如下:
toolProvider 也可以是实现了兼容 callTool({ name, arguments }) 方法的 MCP client。请把所有工具调用视为不可信输入:校验参数、在后端执行鉴权,并且只暴露生成界面真正需要的能力。
扩展组件 Library
使用 defineComponent 把 ReactLynx Renderer 与 Zod Schema 组合起来,再追加到默认 Library:
定义组件时,应把 zod 声明为应用的直接依赖。Schema 决定组件参数顺序,以及生成 prompt 和 JSON Schema 所需的元数据。当前 parser 会检查组件名、必填参数和多余参数等结构约束,但不会对每个值执行完整的 zod.parse();组件与工具仍需自行校验安全敏感值。组件回调收到的是 { props, renderNode, statementId },而不是直接把组件 props 作为顶层参数传入。
默认情况下,createOpenUiLibrary() 会把自定义 definitions 追加到 26 个内置组件和 6 个 component groups 之后;同名自定义组件会覆盖对应的内置组件。设置 includeDefaultComponents: false 会同时移除内置 definitions 及其 groups,此时 components 和 componentGroups 描述的是完整的 Library vocabulary。可以用 library.toJSONSchema() 检查最终契约。通过 memoization 保证一个活动流的 Library 引用稳定,不要在每次渲染时重新创建。
显式选择组件
从逐组件子路径导入所需的内置组件。如果未选择的组件也不能进入静态依赖图,请使用不依赖 catalog 的 openui/explicit 入口:
关闭 defaults 不会改变默认 root 名称 Stack。请像上面的示例一样包含 Stack,或把 root 设为另一个由调用方提供的组件;如果缺少 root,创建 Library 时会报错。省略 componentGroups 时,Library 不包含任何 groups。Agent prompt 和客户端 Library 必须限制为相同的组件名称。
即使从 @lynx-js/genui/openui 导入 factory,includeDefaultComponents 也能控制运行时 vocabulary;但主入口仍会静态引用完整的默认 catalog。要建立静态依赖边界,所有 OpenUI runtime API(包括 OpenUiRenderer、defineComponent 和 hooks)都应从 openui/explicit 导入,所选内置组件则从逐组件 catalog/<Component> 路径导入;不要再同时导入 openui 主入口。
体验 Playground
Lynx GenUI Playground 提供三种 OpenUI 工作流:
- Create:描述目标界面,查看流式生成的 OpenUI Lang,并在 Lynx Preview 中渲染。
- Examples:编辑和回放布局、响应式状态、
Query()与Mutation()示例。 - Components:浏览内置 Library、位置参数契约、用法片段和实时预览。
单个符号及属性说明请参考 @lynx-js/genui/openui API。实现源码位于 Lynx Stack OpenUI。