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()会从布局、内容、按钮、媒体、浮层和输入组件开始,再应用自定义扩展。 - 配置 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() 会在流结束后执行,并在它依赖的 $variable 变化时重新执行;默认值让界面可以在数据返回前先完成渲染。Mutation() 在 Action 通过 @Run 调用前不会执行。Action step 会按顺序访问,但只有 @Run(mutation) 会等待执行结果,并在失败时停止后续 step。@Run(query) 启动重新获取后便会立即进入下一个 step。
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() 会把自定义定义追加到内置组件之后;同名组件可以覆盖对应名称,但这个公开 factory 不能移除其余默认组件来构造严格 allowlist。可以用 library.toJSONSchema() 检查最终契约。通过 memoization 保证一个活动流的 Library 引用稳定,不要在每次渲染时重新创建。
体验 Playground
Lynx GenUI Playground 提供三种 OpenUI 工作流:
- Create:描述目标界面,查看流式生成的 OpenUI Lang,并在 Lynx Preview 中渲染。
- Examples:编辑和回放布局、响应式状态、
Query()与Mutation()示例。 - Components:浏览内置 Library、位置参数契约、用法片段和实时预览。
单个符号及属性说明请参考 @lynx-js/genui/openui API。实现源码位于 Lynx Stack OpenUI。