激活
activateGenUiSessionAgent runtime 响应里的 gen_ui_runtime 交给 activateGenUiSession。切会话后只处理当前 key 的事件。
await activateGenUiSession(sessionKey, runtime, { registryBaseUrl: "https://www.djzctest3.gm/gen-ui-registry", });
模型调用平台工具 present_ui,用自然语言说明「要一块什么样的界面」;平台按已发布的组件目录生成界面规格,对话客户端再把组件从 Registry 拉下来画出来。页面不落在模型的上下文里,而是落在你们的组件库里。
怎么用这一章 这张表是全文的入口。同一件事可能横跨两节——比如「发布 Pack」既要看第 IV 章的 CLI,也要看第 VII 章的提交模型。
读法 业务项目一般不直接依赖合同包;真正要动手的只有 CLI、SDK 和领域 Pack 三块。Kernel 与 Registry 是平台侧的地基。
调用 present_ui,给出 description 和 data。不手写 Spec。
向 Registry 要 Catalog 和 prompt,生成 Spec 并送去校验。合格后把流式事件直接推给客户端。
存元数据与构建产物,提供 Catalog、prompt 和校验。不调用模型,也不推界面。
从 Registry 加载 Kernel 与领域 Pack,吃 present_ui 的事件,画出界面。
@agents-platform/gen-ui-contract
Pack 元数据、Catalog、校验的共同形状。
业务项目一般不直接依赖它——它是其余各块之间的共同语言,改它等于改所有人的接口。
gen-ui/registry
存 Pack 元数据与构建产物,提供 Catalog 与校验。
它不调用模型。可以理解成一个「知道所有组件长什么样」的服务,被平台和客户端同时查询。
gen-ui/registry-cms
给人看的管理页:清单、Schema、预览、导入导出、运行时配置。
打开它需要 Registry 配了 publish token 时输入同一个
token;内测环境通常不设 token,打开即可。
@agents-platform/gen-ui-sdk
对话客户端用来激活会话、吃流式事件、把界面挂到消息流或侧栏。
不打包 React,也不依赖
@json-render/shadcn。平台自带的 web-ui
已经接好。
@agents-platform/gen-ui-cli · 命令名
gen-ui
在业务仓库里创建 Pack、同步合同、构建发布、打 zip。
需要 Node.js 22+。四个子命令各自都可以加
--help。
kernel-shadcn
平台自带的基础组件(按钮、输入、卡片、表格等)和主题变量。
它是布局底座。关掉「使用 Kernel 组件」时主题仍然在,只是
Catalog 里不再列出 Kernel 组件。
业务仓库里用 CLI 创建
你们自己的界面,例如请假单、订单卡片。
一个用户能单独指认的界面,一个目录:src/components/<kebab-case>/。
src/agents_platform/agent/gen_ui · Manage ·
web-ui
按 Agent 配置解析 Catalog、生成 Spec、把事件推给客户端。
产物走对象存储,元数据走 PostgreSQL 的
gen_ui schema。
关键判断点 第 5 步的校验决定了模型能不能「蒙混过关」。GEN_UI_VALIDATE_RESOLVED_STATE 默认开启,会把能静态求值的状态表达式代入后再校验。
管理员在 Manage 里给 Agent 打开生成式 UI,选好 Kernel 和允许使用的领域组件。
Manage用户跟这个 Agent 说话。平台把该 Agent 的 gen_ui_runtime 交给客户端,SDK 按里面的地址去 Registry 注册 Remote。
SDK模型调用内部工具 present_ui,用自然语言 description 说明要什么界面,用 data 带上原始业务数据。
Model平台按 Catalog 生成 json-render Spec,送到 Registry 校验。不合格就让模型重试。
Registry合格后通过流式事件把补丁和状态推给客户端。SDK 加载组件,画出 PresentationView。
Client「画在哪」和「等不等用户」是两个独立维度,组合出四种形态:横轴是 surface(inline / artifact),纵轴是等待用户。
侧栏里的表单就是 artifact 加 interrupt(等待用户)——两个维度都取到,才有「Agent 停住、等你在侧栏填完」这套交互。
理解这四条之后,第 X 章的 SDK 挂载规则和第 XIV 章的 Manage 开关就都能推导出来了——那四个开关正是按这张表切的。
setGenUiAutoOpenArtifact(false)。
等待用户
inline 表单未提交时同样画 PresentationView。提交后有
submittedSummary 只显示这一句,否则 Host 用
useInterruptSubmitted 展开字段。
侧栏表单Agent
停住,等用户提交后再继续。这是提问式交互的主形态。
前置条件 安装 @agents-platform/gen-ui-cli 后即可使用命令 gen-ui,需要 Node.js 22+。
gen-ui create在当前目录下生成 Pack 项目。写在当前工作目录,不写进平台仓库。
gen-ui create leave-request --css tailwind --no-demo gen-ui create leave-request --css stylex --bridge --react 17 --storybook
gen-ui sync用各组件的 meta.ts 生成 pack.json 与 Module Federation exposes。
gen-ui sync gen-ui sync ./leave-request
gen-ui publish把 dist 或 zip 发到 Registry。
gen-ui publish ./leave-request --build \ --registry https://www.djzctest3.gm/gen-ui-registry
gen-ui zip只打 Pack Bundle zip,不访问 Registry。换环境、或先给别人审包时用。
gen-ui zip ./leave-request --build \ --out ./leave-request-v1.zip
这三条在 create 时就该定下来,改起来牵连 Registry 上的存量。
包 id leave-request
本地组件名 Card
────────────────────────────────
typeName LeaveRequestCard
↑ 包 id 的 Pascal 前缀 + 本地名
package.json "version": "1"
Registry 记成 v1
| 选项 | 含义 |
|---|---|
--css tailwind|stylex |
样式方案,默认 Tailwind。前缀都是 gui,颜色跟
Host 的主题变量走。
|
--bridge |
组件自带 React,挂在 ShadowRoot 里,与 Host 的 React 隔离。 |
--react 17|19 |
只在 --bridge 下有效,默认
17,方便旧项目迁入。不加 --bridge 时与 Host
共享 React 19。
|
--storybook |
同时生成 Storybook。 |
--demo / --no-demo |
是否生成示例 card/。默认生成,而且会进发布产物。正式包用 --no-demo。
|
| 选项 | 含义 |
|---|---|
--registry |
Registry 根地址。没传则用环境变量
GEN_UI_REGISTRY_URL。
|
--token |
发布凭证。没传则用
GEN_UI_REGISTRY_PUBLISH_TOKEN。Registry 没配
token 时可以不传。
|
--mode overwrite|new-major |
覆盖当前主线,或新开一条主线。交互终端会问;非交互默认
overwrite。
|
--major vN |
覆盖指定主线,默认用 package.json 的主线数字。
|
--build |
发布前先 pnpm run build。不加就上传现有
dist。
|
这一步是可选的 上一章的四个子命令,你自己敲也完全能走完。装 skill 的意义是让 Agent 知道 meta.ts 该怎么写、typeName 该怎么起名、哪些约束越界会被 Registry 拒收——把这份文档里的规则变成它手上的工具。
npx skills add https://gitlab.gm/GovProEleTrade/resource-data/data-resource-and-intelligent-procurement/agents-platform/-/tree/master/gen-ui/skills/gen-ui
一条命令,装完即用。skills add 接受的是仓库里那个目录的 URL,不是包名——所以升级要重跑这条命令,它不会自动跟着平台版本走。
gitlab.gm/GovProEleTrade/…/agents-platform 下的 gen-ui/skills/gen-ui 目录。换分支就换 URL 里的 master。
装在你写代码的那台机器的 Agent 里,和 Registry、Kernel 无关。团队成员各装各的。
心智模型 meta.ts 不是给人看的文档,而是模型的选择依据。description 写得好不好,直接决定模型会不会在正确的场景挑中你的组件。
组件目录的唯一真相。扫描路径 src/components/*/meta.ts。改这个文件,不要改产物。
component 模式与 Host 共享 React 19;bridge 模式自带 React,挂在 ShadowRoot 里。
// 入口是同目录的 LeaveForm.tsx // 具名组件再 export default // Module Federation 靠默认导出装载 export function LeaveForm(props: LeaveFormProps) { … } export default LeaveForm;
// Remote 在影子树里自己创建 React root export function mount(shadowRoot: ShadowRoot, props: Props) { return { update?, unmount }; }
为什么重要 这条边界决定了生成器会不会给你多造一条提交链。选错会让界面上出现两个提交入口,或者一个都没有。
Spec 负责绑定状态和提交控件,通常是 Kernel 的输入框和按钮。
你只描述数据形状,提交壳由平台生成。大多数表单用这个。
提交权在组件里。Catalog 会标 [self submit],生成器不再为它造另一条 Input/Button 提交链。
Pack 与 Host 各带一份 SDK,两边的 Context 互相看不见。所以在 Pack 里再调一次,拿到的是空能力。Pack 只使用注入进来的 genUi;引入 SDK 时只作类型导入(import type),不要进运行时依赖。
import type { GenUiCapability } from "@agents-platform/gen-ui-sdk"; type LeaveState = { reason: string; days: number }; <button type="button" disabled={!genUi?.canSubmit || !reason.trim()} onClick={() => void genUi!.submit({ reason, days }, { summary: `请假 ${days} 天` }) } > {genUi?.submitting ? "提交中…" : "提交"} </button>
等待用户的块走 Host 的 resume:表单数据仍是快照盖上显式 state;非空 summary 放进保留信封 $genUi,平台剥成 submittedSummary,不进 submittedData。inline / artifact 走 submitMessage,summary 就是那条气泡。
Host 没接桥、已经提交过、或这一笔还在途,它都是 false。不需要自己维护这套状态。
一条规则 Pack 不定义 --gui-* 变量,组件里不写死颜色。这套变量由 Kernel 在运行时注入。
类名经 src/cn.ts 的 cn() 处理,自动补上前缀。
cn("border-border")
// → "gui:border-border"
用 src/theme.stylex.ts 的令牌,不写裸值。
t.border t.radiusLg
只用脚手架接好的 --gui-*,作用域是 .gui-scope。暗色和换肤会跟着 Host。
本地预览页的 .gui-scope 在 src/preview-theme.css,只服务 pnpm dev 和 Storybook。真正上线后,主题由 Kernel 注入。
StyleX 项目保留 rsbuild 的 treeshakeCompensation: true,以及 Storybook 的 lazyCompilation: false,否则开发预览会没有样式。
PresentationView 在 Kernel remote 可用时自己注入 --gui-*。暗色跟祖先或自身上的 dark 走(.dark .gui-scope 或 .gui-scope.dark)。
适用范围 平台自带的 web-ui 已经接好。这一章讲的是把另一套对话客户端接上时要做什么。客户端需要能拿到本平台的 Agent runtime、LangGraph 形状的消息,以及流式 custom。
| 包 | requiredVersion |
|---|---|
react、react/jsx-runtime、react/jsx-dev-runtime
|
^19.1.0 |
react-dom、react-dom/client |
^19.1.0 |
@json-render/core、@json-render/react
|
^0.20.0,并写 version: "0.20.0"
|
bridge 模式的 Pack 自带 React,不走这张表。同一会话可以并存多条主线,Remote 名里带主线(id__vN)。
Agent runtime 响应里的 gen_ui_runtime 交给 activateGenUiSession。切会话后只处理当前 key 的事件。
await activateGenUiSession(sessionKey, runtime, { registryBaseUrl: "https://www.djzctest3.gm/gen-ui-registry", });
custom 里 type 为 gen-ui-runtime、gen-ui-patch、gen-ui-state 的整条事件交给 dispatchGenUiEvent(sessionKey, event)。
runtime 绑定完成后再水合,避免块先到、Remote 还没注册。
await hydrateGenUiRuntimes( sessionKey, resolvedRuntimesFromMessages(messages), ); for (const presentation of presentationsFromMessages(messages)) { await hydrateGenUiPresentation(sessionKey, presentation); }
这是 Pack 里 genUi.submit 的出口。切会话或卸载时传 null。
setGenUiHostBridge({ resume: (payload) => resumeInterrupt(payload), submitMessage: async (input) => { if (streaming) return; await sendMessage({ text: input.text }); }, });
边距与对齐 留在 Host 的 className。只有要换成自己的提交壳时才传 onInterruptSubmit 和 renderSubmit,提交前 markInterruptSubmitted。
const snapshots = usePresentations(); const target = resolvePresentUiTarget( call, laterMessages, messages, snapshots, );
target 给出 presentationId、surface、interrupt(是否等待用户)。同一 id 后一次成功调用盖过旧位置。工具结果 status === "error" 时不认领这块,错误文案由 Host 自己画。
消息流里 <PresentationView presentationId={id} className="..." />。
若同时等待用户:未提交时同样画 PresentationView。提交后由 Host 画只读卡片:工具结果或 useInterruptSubmittedSummary(id) 有 submittedSummary 就只显示这一句,否则用 useInterruptSubmitted(id) 展开字段。
消息流入口用 ArtifactRef(className / children 画卡片)。点击会 selectArtifact。
侧栏用 useSelectedArtifact() 的 id 再画 PresentationView。新 artifact 默认自动选中;不抢焦点就 setGenUiAutoOpenArtifact(false);收起用 closeArtifact()。
凭证 Registry 配置了 GEN_UI_REGISTRY_PUBLISH_TOKEN 时,打开 CMS 要输入同一个 token。它写入浏览器,供导出、导入、改运行时配置使用。内测环境通常不设 token,打开即可。
| 环境 | CMS 地址 |
|---|---|
| test3 |
https://www.djzctest3.gm/gen-ui-registry/cms/
|
| show1 |
https://www.djzcshow1.gm/gen-ui-registry/cms/
|
| 生产 |
只在内网
http://<内网IP>:30033/gen-ui-registry/cms/,不对公网开放
|
| 环境 | Runtime Config 地址 |
|---|---|
| test3 |
https://www.djzctest3.gm/gen-ui-registry/cms/runtime-config
|
| show1 |
https://www.djzcshow1.gm/gen-ui-registry/cms/runtime-config
|
| 生产 |
只在内网
http://<内网IP>:30033/gen-ui-registry/cms/runtime-config
|
左侧是包列表,可按 Kernel / 领域包筛选和搜索。点进一个包,有四个页签。
| 页签 | 看什么 |
|---|---|
| Exposes | 暴露了哪些组件、typeName、说明 |
| Schemas | 每个组件的 props 合同 |
| Majors | 已发布的主线 |
| Playground | 用 Storybook 预览(发布时带了 Storybook 才会有) |
顶栏 API 文档 打开这份 Registry 的 OpenAPI。Manage 里 Agent 的「生成式 UI」页也有「打开 Registry CMS」。
浏览器可以读到的键值,例如领域组件要用的 API 根地址。平台把它放进 gen_ui_runtime.runtime_config,Host 激活会话时注入 SDK,Pack 用 getRuntimeConfig() 读取。
页面分两块:上面是键值表(键名 / 值 / 说明三列),右上角 新增配置项 加一行;下面是 SDK 使用方式,把注入和读取两条路写成代码给你抄。空配置时表里显示「暂无配置项」——不报错,就是读不到。
// 平台按 Agent 解析配置,随会话下发 gen_ui_runtime.runtime_config = { "leave.apiBase": "https://www.djzctest3.gm/leave-api", "leave.env": "test3", };
await activateGenUiSession(sessionKey, runtime, { registryBaseUrl: "https://www.djzctest3.gm/gen-ui-registry", // 想覆盖 Registry 下发的值,在这里合并后再传 runtimeConfig: runtime.runtime_config ?? {}, });
const { apiBase, env } = getRuntimeConfig(); // 配置缺失时给一个能跑通的兜底,别让组件白屏 const base = apiBase ?? "/fallback-api";
把根换成上面的 Registry 根即可。types 是领域组件白名单——不传则该 Pack 的组件都在。
Kernel 组件默认保留,因为它是布局底座;关掉「使用 Kernel 组件」时主题仍然在,只是 Catalog 里不再列出 Kernel 组件。
GET /catalog?kernel=kernel-shadcn@v1&packRefs=leave-request@latest GET /catalog/prompt?kernel=kernel-shadcn@v1&packRefs=leave-request@v1&types=LeaveRequestForm GET /health
前提 test3、show1、生产各自有一份 Registry:元数据在各自的库,产物在各自的桶 agents-platform-gui。没有自动复制。
推荐。有可视化勾选,能看到写入/跳过结果。
勾选要搬走的包和主线(可按 Kernel / 领域、关键字筛选)。默认会选中当前列表里的主线。
源 CMS包详情里的「导出此包」只导出当前这一个包。目标 Registry 若要求 token,导入导出都会带上你登录 CMS 时填的凭证。
源 CMS看结果:写入几条、跳过几条。内容 digest 与目标上同一主线相同的槽会跳过;内容不同则写入(覆盖该主线槽)。
目标 CMS适合自动化或源环境不可达 CMS 时。
gen-ui zip . --build --out ./leave-request-v1.zip gen-ui publish ./leave-request-v1.zip \ --registry https://www.djzcshow1.gm/gen-ui-registry \ --token "$GEN_UI_REGISTRY_PUBLISH_TOKEN"
三类变量,三种用途 API 用集群内地址,Manage / web-ui 用构建期变量,对象桶单独一套。混用会拼出两层前缀。
| 变量 | 含义 |
|---|---|
GEN_UI_REGISTRY_URL |
这是集群内的根,不带浏览器前缀 /gen-ui-registry。例如测试环境容器内是
http://agents-platform-gen-ui-registry-service:30033。没配这个变量,Catalog 和校验都不会工作。
|
GEN_UI_VALIDATE_RESOLVED_STATE |
默认开启。校验前把 Spec 里能静态求值的
$state / $bindState 换成 State
的实际值,这样「路径对、取值不符合
props」也会被打回让模型改。设为
false 则这些表达式整棵跳过取值校验。
|
GEN_UI_MINIO_BUCKET |
对象桶用 agents-platform-gui,不要复用平台自己的 MINIO_BUCKET。
|
Manage 与 web-ui 在构建期用 VITE_GEN_UI_REGISTRY_URL。部署到平台域名时写成 /gen-ui-registry,浏览器会把它拼到当前页面的 origin。
编辑 Agent,页签「生成式 UI」,按序做完这四步。
kernel-shadcn@latest;日常可钉成
kernel-shadcn@v1 这类明确主线。
<id>@vN 或
<id>@latest,并至少勾选一个 typeName。
为什么单列一章 第 XII 章的稳定性和第 XIII 章的权限控制,全都建立在这三个词上面。混用它们是最容易出线上事故的地方。
形如 leave-request@v1 或 leave-request@latest。
@latest 解析该包当前最新主线;钉死 v1 则不受后续新主线影响。
来自 Pack 项目 package.json 的 version。写成 "1",Registry 记成 v1。
同一主线可以覆盖发布;要保留旧产物就开新主线。
模型在 Spec 里使用的组件名。gen-ui sync 会给本地组件名加上包 id 的 Pascal 前缀。
包 leave-request、本地 Card,全名是 LeaveRequestCard。Manage 里勾选的就是这个全名。
怎么用 左边一栏偏「配置与集成」,右边一栏偏「Pack 与产物」。上线前对着扫一遍,比事后查日志便宜。
/gen-ui-registry
该路径只适用于挂在平台域名下的 web-ui / Manage。独立站点要写 test3 或 show1 的绝对地址,以 / 开头的值会被拼到当前页面的 origin 上。
@latest 上了线
随后有人覆盖或新开主线,线上界面跟着变。要稳定就钉
vN。
SDK 注册 Remote 直接失败。SDK 不会自己 createInstance,宿主必须先建好全局 runtime。
这份配置会下发到浏览器。值会进
gen_ui_runtime.runtime_config,谁都能读。
create 时留着示例 card/发布后 Catalog 里多出一个不该给模型用的组件。它进了发布产物,模型就可能挑中它。
忘了在目标环境补 Runtime Config,组件里
getRuntimeConfig() 为空。Runtime Config 不在 Pack Bundle zip 里。
pack.json 或
src/generated/exposes.ts
下次 sync 会被覆盖。改 meta.ts。那两份是产物,不是来源。
改了 treeshakeCompensation 或 Storybook 的
lazyCompilation: false,本地预览没有样式。
领域组件的 typeName 必须以包 id 的 Pascal 前缀开头,否则 Registry 拒收。
Registry 会拒绝「self 组件在场时还出现
submitInterrupt」以及「self 组件 props 里出现
$bindState」。只读 $state 仍然合法。
GEN_UI_REGISTRY_URL 却写成了浏览器前缀
API 侧要的是集群内根,不带
/gen-ui-registry。没配对,Catalog
和校验都不会工作。
用户没有提交入口,Agent 停在那里等一个永远不来的提交。