For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt, and this page is available as Markdown at /zh/guide/genui/openui.md.
这是开发预览网站。请访问正式文档 lynx-stack.dev

@lynx-js/genui/openui

English | 简体中文

@lynx-js/genui/openui 是面向 OpenUI Lang v0.5 的 ReactLynx 客户端运行时。 它会解析声明式 OpenUI 文本、求值响应式状态和数据操作,并通过可信的 ReactLynx 组件 Library 渲染结果。

当 Agent 输出 OpenUI Lang,而你的 Lynx 应用负责传输、工具调用、状态持久化和 宿主 action 时,可以使用这个包。Agent 输出的是数据,而不是可执行 UI 代码;它 只能实例化你交给 renderer 的 Library 中声明过的组件。

如果你第一次接触 OpenUI,可以先这样理解:

  • 在 React 里,是你的代码选择组件并传入 props。
  • 在 OpenUI 里,是 Agent 使用你的 Library 中的组件,逐行写出 assignment。
  • Client 解析这些 assignments,再渲染你注册的真实 ReactLynx 组件。

安装

在 ReactLynx 应用中安装公开的 GenUI 包:

pnpm add @lynx-js/genui @lynx-js/react @lynx-js/lynx-ui @lynx-js/luna-styles

默认 catalog 使用 @lynx-js/lynx-ui 的 headless 行为原语,并使用 @lynx-js/luna-styles 的语义化视觉 tokens。内置的 Button、CheckBox、 Modal、RadioGroup、Slider 和 TextField 使用 lynx-ui 原语。

先引入 Luna,再引入 OpenUI token 适配层,并在 renderer 外应用 Luna 主题 class。Renderer 和各组件的 CSS 会随对应模块自动引入,不需要额外导入 renderer 样式表。需要另一套配色时,也可以使用 Luna 提供的 lunaris-light 和 lunaris-dark。

import '@lynx-js/luna-styles/index.css';
import '@lynx-js/genui/openui/styles/theme.css';

快速开始

创建一个 Library,把原始 OpenUI Lang 传给 <OpenUiRenderer>,并处理需要宿主 应用接管的 actions。

import { createOpenUiLibrary, OpenUiRenderer } from '@lynx-js/genui/openui';
import { useMemo } from '@lynx-js/react';

import '@lynx-js/luna-styles/index.css';
import '@lynx-js/genui/openui/styles/theme.css';

const response = String.raw`
root = Stack([header, card], "column", false, "m", "stretch", "start")
header = Text("Hello OpenUI", "h2")
card = Card([message, actions])
message = TextContent("This UI was described as data.")
actions = Buttons([Button("Continue", Action([@ToAssistant("Continue")]), "primary")])
`.trim();

export function GeneratedView() {
  const library = useMemo(() => createOpenUiLibrary(), []);

  return (
    <view className='luna-light'>
      <OpenUiRenderer
        response={response}
        library={library}
        onAction={(event) => {
          // 把 ContinueConversation/OpenUrl events 转发给宿主。
          console.info(event.humanFriendlyMessage);
        }}
      />
    </view>
  );
}

原始 response 必须是 OpenUI Lang,而不是 Markdown code fence。每一行都是一条 assignment,渲染入口必须命名为 root:

identifier = Component(positional, arguments)
$variable = defaultValue
data = Query("tool_name", { argument: $variable }, { fallback: true })

你需要负责什么

部分负责人作用
@lynx-js/genui/openui这个包OpenUI parser/runtime adapter、ReactLynx renderer、内置 Library、状态/actions 和 prompt helpers。
你的 Agent 服务你的应用使用 OpenUI system prompt 调用模型,并返回原始 OpenUI Lang 文本。
你的 transport adapter你的应用流式追加或设置累计 response 文本,并取消已经过期的请求。
你的 tool provider你的应用实现 Query() 和 Mutation() 引用的工具。
你的 host shell你的应用持久化状态,并处理 renderer 抛出的 assistant/open-URL actions。

首次接入要知道

  • OpenUI v0.5 应优先使用 <OpenUiRenderer response={...}>。旧版 result={parseResult} 路径可以渲染预解析的静态树,但不拥有 v0.5 的 query、 mutation 或响应式状态运行时。
  • 模型流式输出期间,把累计 response 和 isStreaming 一起传入。增量 parser 会持续保留已完成且可渲染的 statements;内置交互在流结束前保持禁用。
  • 完整 response 到达后,Query defaults 和 initialQueryResults 会进入第一次同步 渲染。预取结果必须用 Query assignment name(而不是 tool name)作为 key。如果同时 传入 toolProvider,Query 会在 commit 后重新校验,并在响应式参数变化时重新执行。 Mutation() 只会通过 action 中的 @Run(...) 触发。
  • onAction 接收 @ToAssistant(...)、@OpenUrl(...) 等宿主 actions;状态 steps 和工具 steps 会先在 runtime 内执行。
  • onError 会返回结构化的 parser、runtime、render 和 tool errors,适合接入 Agent correction loop。
  • 默认情况下,createOpenUiLibrary() 内置 26 个组件。额外 definitions 会追加在默认组件后; 如果名称相同,后加入的组件会替换默认实现。
  • includeDefaultComponents: false 会把 Library vocabulary 限制为调用方提供的 definitions,但这个 flag 不会移除主入口对默认 catalog 的静态依赖。如果未选择的 内置组件不应进入依赖图,请从 @lynx-js/genui/openui/explicit 导入 createOpenUiLibrary,并通过逐组件子路径导入保留的内置组件,例如 @lynx-js/genui/openui/catalog/Stack。
  • @lynx-js/genui/openui/catalog/agent 导出使用 null renderer 的内置组件定义, 用于 Node 服务端生成 schema 和 prompt。Prompt library 和 ReactLynx catalog 共享同一套 schema、描述和 ref。 可通过 catalog/agent/<Component> 按需导入单个组件定义。
  • OpenUI Chat 支持通过 @lynx-js/genui/openui/chat/<Component> 按组件导入, 服务端对应使用 openui/chat/agent/<Component>。从 openui/explicit 创建 Library,设置 root: 'Card'、includeDefaultComponents: false,并传入所选组件。 每个组件自动引入自身和必要的共享样式;createOpenUiChatLibrary() 仍然注册整套 Chat 组件。完整示例见 Chat 按需导入。

流式模型输出

把累计模型文本作为 response,把生成状态作为 isStreaming 传给 renderer。 如果业务已经有 React 状态或会话状态,直接复用即可,无需另外创建 parser。

  • response 是截至当前收到的完整文本,不是最新一个 delta。收到 delta 时追加; transport 下发累计快照时,直接替换文本。
  • 整个生成期间保持 isStreaming={true}。UI 会逐步展示,Query 执行、mutation 注册和内置交互会等待生成完成。
  • 成功结束时,把最终权威文本和 isStreaming={false} 一起更新。该标记默认是 false,展示静态或已完成的历史 response 时可以省略。

接入 transport

把文本和生成状态放在同一个 state 对象中,确保一起更新。下面展示 transport 的 回调接法;根据服务返回的是 delta 还是累计快照,选择对应的文本回调:

import { createOpenUiLibrary, OpenUiRenderer } from '@lynx-js/genui/openui';
import { useMemo, useState } from '@lynx-js/react';

export function GeneratedView() {
  const library = useMemo(() => createOpenUiLibrary(), []);
  const [output, setOutput] = useState({
    response: null as string | null,
    isStreaming: false,
  });

  function onStart() {
    setOutput({ response: '', isStreaming: true });
  }

  function onDelta(delta: string) {
    setOutput((current) => ({
      response: (current.response ?? '') + delta,
      isStreaming: true,
    }));
  }

  function onSnapshot(fullText: string) {
    setOutput({ response: fullText, isStreaming: true });
  }

  function onDone(finalText: string) {
    setOutput({ response: finalText, isStreaming: false });
  }

  function onFailureOrCancel() {
    setOutput({ response: null, isStreaming: false });
  }

  // 把这些回调接到已有 transport。
  return <OpenUiRenderer library={library} {...output} />;
}

如果完成事件没有最终文本,保留累计 response,只更新标记: setOutput(current => ({ ...current, isStreaming: false }))。 即使最终文本和最后一次增量相同,也必须切换标记,才能启用 Query、完成后的错误报告 和交互。自定义交互组件也应遵循 useIsStreaming()。

增量解析是怎么工作的

只要 library 对象不变,renderer 就会复用同一个 streaming parser。 虽然每次传入累计全文,parser 会识别追加的文本、缓存已完成的语句,并重新解析 尚未完成的尾部以预览部分输出。Forward references 在目标声明到达后变为可渲染状态。 改写或缩短之前收到的文本会重置解析缓存;用 useMemo 或模块常量保持 Library 稳定。

这是语句级增量解析:每次文本更新仍会做预处理和结果重建,一条很长但尚未完成的语句 也可能被反复解析。它没有提供组件级 patch 协议。文本不变、只切换 isStreaming 时,不会重新解析。

完成、取消与新会话

使用 SSE 时,只有成功的 done 事件才代表完成,并应以其中的最终文本为准。 连接关闭本身不代表成功。

异常或取消时,用 { response: null, isStreaming: false } 清空未完成文本, 避免为半成品启用 Query 或交互。更新状态不会取消网络请求;transport 负责取消和错误 报告。开始下一次生成前,停止旧请求、拒绝其迟到事件,再重置文本。 清空 response 不会重置运行时表单状态;新会话需要重置这些状态时,为 renderer 使用新的 key。

更多文档