欢迎光临
我们一直在努力

[特殊字符] ZorvAI 动态 UI 组件:让 AI 的回答「看得见、用得上」

🌟 项目简介

ZorvAI 动态 UI(quro-ui) 是一套基于 Jetpack Compose 原生构建的可交互界面渲染框架。它让 AI 不再局限于「文字 + 代码块」的输出形态,而是能够主动地生成卡片、表单、列表、播放器、浏览器、富媒体等完整的交互式界面——就像一位熟练的前端工程师,根据用户意图即时绘制出最合适的 UI。

🔗 开源地址:https://github.com/Quor-a/ZorvAI

✨ 核心理念

理念内涵
原生即正义 直接用 Compose 渲染,不依赖 WebView/HTML(除了白名单的 HTML 节点)
DSL = 结构 用 JSON 描述界面,模型输出友好、解析稳定、可版本控制
稳定可重现 每个节点生成稳定 ID,重渲染后状态不丢、回调不串
密度自适应 以 360 dp 设计宽度为基线,按当前真实宽度动态缩放(手机/折叠屏/平板)
暗色优先 16 阶灰度 + 语义色板,深色场景默认开启,亮色按需切换
必备输出 v1.0.82 起,AI 把动态 UI 作为默认呈现方式,不再需要用户要求

🏛 架构总览

┌────────────────────────────────────────────────────────────────────────┐
│ QuroAssistant 主对话流水线 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌────────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ 用户提问 │ → │ System │ → │ LLM │ → │ 文本/quro-ui │ │
│ │ + 上下文 │ │ Prompt │ │ 决策 │ │ JSON 混合输出 │ │
│ └─────────┘ │ 「动态 UI │ │ 工具调用│ └─────────────────┘ │
│ │ 必备输出」 │ └─────────┘ │ │
│ └────────────┘ ▼ │
│ ┌─────────────────┐ │
│ │ A2uiEnvelope │ │
│ │ (a2ui 协议信封) │ │
│ └─────────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌────────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ QuroUiDslParser│ → │ QuroUiCatalog│ → │ QuroUiNode │ │
│ │ 净化/解析/纠错 │ │ 调色板与图标 │ │ AST │ │
│ │ │ │ 字面量校验 │ │ │ │
│ └────────────────┘ └──────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ SurfaceHost │ │
│ │ 挂载 Compose 容器 │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ QuroUiRenderer │ │
│ │ 17+ 原生组件渲染 │ │
│ └──────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘


📦 模块拆解 · 10 个文件各司其职

core/ui/dynamicui/
├── QuroUiNode.kt # AST 节点类型定义
├── QuroUiDslParser.kt # quro-ui 字符串净化 + JSON 解析
├── QuroUiCatalog.kt # 调色板 / 图标库 / 校验器
├── QuroUiColor.kt # 16 阶灰度 + 语义色映射
├── QuroUiIcons.kt # Lucide 图标库(camelCase → snake_case)
├── QuroUiPointer.kt # 路径解析 + 数据更新指针([a-z0-9_./-]+)
├── QuroUiRenderer.kt # ⭐ 核心:JSON AST → Compose 组件
├── SurfaceHost.kt # 无限尺寸 / maxWidth 崩溃修复 + 渲染挂载
├── A2uiEnvelope.kt # a2ui 信封协议(deepMerge / updateDataModel)
├── A2uiInterpreter.kt # 信封嗅探(lowercase 开头自动识别)
└── QuroDynamicUiTool.kt # ⭐ ui_dsl_spec / ui_validate 工具

🔧 各文件职责一览

文件行数(估)关键能力
QuroUiNode.kt ~350 QuroUiRootNode/QuroUiContainerNode/QuroUiLeafNode 三层 AST;QuroListNode 支持 {{item.field}};QuroHtmlNode 透明 WebView
QuroUiDslParser.kt ~280 sanitizeJson() 抹平误置闭合符;fixOutsideStrings() 修复字符串外的脏括号;normalizeQuotes() 跟踪 inSgl/inDbl 状态
QuroUiCatalog.kt ~220 颜色通过 QuroUiColor.parse() 校验;图标白名单 + 未知图标回退默认;LeafNode 合法性守卫
QuroUiColor.kt ~120 gray.0-gray.15 灰度;primary/secondary/success/warning/danger 语义色;自动判别 light/dark
QuroUiIcons.kt ~200 Lucide 图标集;camelCase → snake_case → 小写归一;缺失自动回退 circle_help
QuroUiPointer.kt ~150 路径字段名正则 [\\w\\-./];解决 JSON 路径解析时的中划线/下划线混用
QuroUiRenderer.kt ~1400 核心:RenderColumn/RenderRow/RenderBox/RenderCard/RenderList/RenderTabs/RenderSlider/RenderText/RenderImage/RenderIcon/RenderBadge/RenderProgress/RenderButton/RenderTextInput/RenderSelect/RenderMarkdown/RenderHtml/RenderVideo/RenderAudio/RenderBrowser/RenderCode/RenderDivider/RenderSpacer 等
SurfaceHost.kt ~180 修复 Infinity 触发的 Compose 崩溃;包一层 BoxWithConstraints 提供真实可用宽度
A2uiEnvelope.kt ~200 { "version": …, "a2ui": … } 信封;deepMerge() 支持增量数据模型合并
A2uiInterpreter.kt ~120 嗅探 lowercase 键名头({"kind":"a2ui", …})自动剥信封;纯 quro-ui JSON 不受影响
QuroDynamicUiTool.kt ~300 ui_dsl_spec 拉取动态 UI 规格(提示词);ui_validate 模型自检输出可解析性

🧬 DSL 解析管线

quro-ui 的输入是模型输出在 fenced code block 里的 JSON。我们永远不假设模型一定写出干净 JSON,所以解析管线有四层防护:

🛡 1️⃣ sanitizeJson(raw: String)

目标:抹平「误置闭合符」(最常见的 AI 病)。

fun sanitizeJson(raw: String): String {
// 1) 找到第一个 '[' 或 '{' 作为起点
// 2) 跟踪 (字符串内/外) + (反斜杠转义) 状态机
// 3) 在字符串外允许的成对字符 [ ] { } :
// – 若遇到孤立的 ']' 或 '}',先看上层栈;不平衡则补一个同向(保守)补齐
// 4) 丢弃顶层其余杂质(多余反引号、注释尾巴)
}

🔧 典型拯救:

// 模型输出:
[
{ "type": "text", "text": "你好" }
{ "type": "button", "label": "确定" } // ← 漏了 ,
]

// sanitizeJson 后:
[
{ "type": "text", "text": "你好" },
{ "type": "button", "label": "确定" }
]

🛡 2️⃣ fixOutsideStrings(s: String)

目标:修字符串外的脏括号({ type: "foo} — 引号未关)。

fun fixOutsideStrings(s: String): String {
val out = StringBuilder()
var inSgl = false; var inDbl = false
for (c in s) {
when {
c == '\\\\' && (inSgl || inDbl) -> { out.append(c); /* 跳过下个 */ }
c == '"' && !inSgl -> inDbl = !inDbl
c == '\\'' && !inDbl -> inSgl = !inSgl
...
}
out.append(c)
}
}

🛡 3️⃣ normalizeQuotes(s: String)

目标:统一单/双引号 → JSON 标准双引号。在字符串外为 inSgl = false 时安全替换。

🛡 4️⃣ QuroUiCatalog + QuroUiColor.parse() 校验

目标:颜色字面量必须是已知 token,否则归一为 gray.7(中灰);图标名必须存在于白名单。


🎨 渲染管线

JSON AST (QuroUiNode)


┌─────────────────────────────┐
│ QuroUiRenderer.render(root) │
└─────────────────────────────┘

├─ 容器节点 → RenderColumn/RenderRow/RenderBox/RenderCard
│ │
│ └─ forEach child → 递归调用 renderChild()

└─ 叶子节点 → RenderText/RenderImage/RenderButton/RenderHtml/…

└─ stableId(prefix, json) → 用于 Compose Key

📐 密度自适应(360 dp 设计宽度)

@Composable
fun rememberDensityScale(): Float {
val config = LocalConfiguration.current
val designWidthDp = 360f
val actualWidthDp = config.screenWidthDp.toFloat()
return (actualWidthDp / designWidthDp).coerceIn(0.85f, 2.0f)
}

文本、间距、内边距、圆角、卡片宽度都按 densityScale 缩放;图标按矢量自


🛠 实战:构建一个待办清单

理论讲完,来点能直接跑的东西。下面用 quro-ui 构建一个完整的「待办清单」:顶部一个输入框,中间是 list 渲染的待办项(每项带 checkbox 勾选),底部一个「清空已完成」按钮。

下面是你的待办清单,试试勾选或新增:

```quro-ui
{
"type": "column",
"gap": 12,
"padding": 14,
"children": [
{
"type": "text",
"text": "📝 今日待办",
"weight": "bold",
"size": 18
},
{
"type": "row",
"gap": 8,
"children": [
{
"type": "text_input",
"placeholder": "输入新任务,回车添加",
"value": "{{input}}",
"onSubmit": {
"type": "callback",
"name": "todo_add",
"payload": { "text": "{{input}}" }
}
},
{
"type": "button",
"label": "添加",
"variant": "primary",
"action": {
"type": "callback",
"name": "todo_add",
"payload": { "text": "{{input}}" }
}
}
]
},
{
"type": "list",
"gap": 8,
"data": [
{ "id": "t1", "title": "写周报", "done": false },
{ "id": "t2", "title": "回复邮件", "done": true },
{ "id": "t3", "title": "预约会议室", "done": false }
],
"template": {
"type": "row",
"gap": 10,
"align": "spaceBetween",
"children": [
{
"type": "checkbox",
"label": "{{item.title}}",
"checked": "{{item.done}}",
"onChange": {
"type": "toggle",
"stateKey": "todo.{{item.id}}.done"
}
},
{
"type": "button",
"label": "删除",
"variant": "secondary",
"action": {
"type": "callback",
"name": "todo_remove",
"payload": { "id": "{{item.id}}" }
}
}
]
}
},
{
"type": "button",
"label": "🗑 清空已完成",
"variant": "danger",
"action": {
"type": "callback",
"name": "todo_clear_done",
"payload": {}
}
}
]
}
```

🧩 节点渲染效果拆解

节点渲染效果
column 垂直容器,gap: 12 让标题、输入行、列表、清空按钮之间保持 12 dp 间距
text 顶部加粗标题「📝 今日待办」,size: 18 突出层级
row 水平排列「输入框 + 添加按钮」,gap: 8 让两者紧贴不粘连
text_input 占位提示「输入新任务,回车添加」,value 绑定 {{input}} 保持受控
button 「添加」用 primary 主色;「删除」用 secondary 次色;「清空」用 danger 红色
list 遍历 data 数组,每行按 template 渲染,{{item.title}} 取当前行标题
checkbox 左侧勾选框 + 右侧标签,checked 绑定 {{item.done}} 回显完成状态

⚡ 交互动作如何绑定

  • callback(新增 / 删除 / 清空):按钮或输入框的 action / onSubmit 里声明 { "type": "callback", "name": "todo_add", "payload": {…} }。点击后前端把 name + payload 回传给宿主,由业务层更新数据模型并重渲染。
  • toggle(勾选完成):checkbox 的 onChange 用 { "type": "toggle", "stateKey": "todo.{{item.id}}.done" }。它不经过业务回调,直接翻转 stateKey 指向的布尔状态,实现「本地即时勾选」——配合 {{item.done}} 回显,勾选后整行状态立刻同步。
  • 占位符联动:{{item.id}} / {{item.title}} / {{item.done}} 在 list 内逐行求值,让每个 checkbox 和删除按钮都拿到自己那一行的数据,互不串扰。

💡 要点:callback 适合「需要宿主处理」的动作(增删、持久化),toggle 适合「纯本地状态翻转」(勾选、开关)。两者组合,就能在纯 JSON 里搭出可交互的完整界面。

适应不缩放。


🧩 节点类型完整清单 · 17+ 组件

类型类别关键属性
column 容器 gap / padding / align / scroll
row 容器 gap / padding / align / wrap
box 容器 padding / align
card 容器 padding / radius / elevation / background
tabs 容器 tabs[] + activeIndex 状态
list 容器 data + template 占位符 {{item}}/{{item.field}}/{{index}}
text 叶子 text / size / weight / color / align / maxLines
image 叶子 src / fit / radius / placeholder
icon 叶子 name (Lucide) / size / color
badge 叶子 text / variant (success/warning/danger/…)
progress 叶子 value / max / variant
divider 叶子 color / thickness
spacer 叶子 height / width
button 叶子 label / action / variant
text_input 叶子 placeholder / value / onSubmit
checkbox 叶子 label / checked / onChange
switch 叶子 label / checked / onChange
select 叶子 options[] / value / onChange
slider 叶子 min / max / value / onChange
markdown 叶子 content 实时渲染 Markdown
html 叶子 content 透明 WebView 容器(v1.0.82 深度修复)
video 叶子 src / controls / autoplay
audio 叶子 src / controls
browser 叶子 url / height / cookies / ua(内嵌 WebView 容器,v1.0.82 已稳定)
code 叶子 code / lang / theme

📝 实战示例:AI 输出

下面是配置服务器的一键操作清单:

```quro-ui
{
"type": "list",
"padding": 12,
"gap": 8,
"data": [
{ "emoji": "🛠", "title": "安装 Nginx", "desc": "通过 apt/yum 安装最新稳定版" },
{ "emoji": "🔒", "title": "配置 HTTPS", "desc": "使用 Let's Encrypt 自动签发" },
{ "emoji": "📦", "title": "部署静态站点", "desc": "/var/www/html 权限设置" }
],
"template": {
"type": "row",
"gap": 12,
"children": [
{ "type": "text", "text": "{{item.emoji}}", "size": 20 },
{
"type": "column",
"children": [
{ "type": "text", "text": "{{item.title}}", "weight": "bold" },
{ "type": "text", "text": "{{item.desc}}", "size": 12, "color": "gray.10" }
]
}
]
}
}
```

渲染效果:每行 = 表情 + 加粗标题 + 灰色描述,自适应宽度。


⚡ 动作类型 · 8+ 种交互

动作参数v1.0.82 增强
callback { name, payload }
tool_call { name, args }
skill { name, args }
open_url { url } 支持深链 zorvai://…
copy { text }
open_app { packageName }
toggle { stateKey }
open_screen 🆕 { screen, args } 直达应用内屏(设置/插件/会话)
render_html 🆕 { html } 服务端/Skill 主动渲染 HTML 节点
render_vispro 🆕 { spec } 触发可视化处理管线(图表/流程图)
visual_popup 🆕 { payload } 系统级浮层提示
visual_ask 🆕 { question, options[] } 阻塞式可视化提问,等待用户选择

🔁 占位符与数据流

占位符适用场景示例
{{index}} list 节点里返回当前序号 第 {{index}} 项
{{item}} list 节点里整行数据(字符串字段时)
{{item.field}} list 节点里按字段取数据 {{item.title}} / {{ite> **同源更新**:List 内部如嵌套 tabs/card,子节点也能取到外层的 {{item.xxx}}`,渲染时整树连坐求值。

🔧 工具支持 · 模型自检

🧰 ui_dsl_spec

// 模型调用:
{ "tool": "ui_dsl_spec", "args": { "section": "all" } }

// 返回:quro-ui 节点清单 + 示例 JSON + 注意事项

🧰 ui_validate

// 模型自检:把刚才输出的 quro-ui JSON 喂回工具,立即返回可解析性评分
{ "tool": "ui_validate", "args": { "dsl": "<JSON>" } }

// 返回: { "ok": true, "warnings": […], "fix_suggestions": […] }

✅ 典型用法:模型自检一轮后再发出,比直接发送错误 JSON 被前端报错更稳健。


🎨 主题与样式

🌑 调色板(QuroUiColor)

Token 类示例说明
gray.0–gray.15 gray.0 = #FFFFFF gray.15 = #0A0A0A 16 阶中性灰
primary 主品牌色(v1.0.82:靛蓝 #5046E4) 主操作
secondary 次操作色 次按钮
success warning danger info 绿/橙/红/蓝 状态徽章
surface onSurface 卡片背景/前景 自动暗色反转

✏️ 图标(QuroUiIcons)

  • 内置 Lucide 图标集(约 1000 个常用图标)
  • camelCase → snake_case → 小写归一
  • 未知图标回退 circle_help,绝不渲染空白方块

🧠 v1.0.82 必备输出设计 · 系统提示词

这一节是让「动态 UI」真正成为默认行为的关键。

### 动态 UI(quro-ui 原生组件 · 必备输出)

**何时用:** 始终默认使用。任何需要呈现「操作清单 / 选项 / 表单 /
播放器 / 浏览器 / 富媒体」的回答,都优先用 quro-ui 渲染,而不是
纯文本。即使只生成一张卡片也要用它。

**输出规范:**
1. 单条 quro-ui JSON 必须被 ```quro-ui … ```围栏包裹;
2. 复杂的可拆为多条 ```quro-ui 块;
3. 关键结论、解释、对话照常用正文;UI 只是更强的呈现通道。

**自检:** 发送前调用 ui_validate 工具。

🧭 在工具分类中的位置

🧠 ToolCapabilityDirectory.DYNAMIC_UI
├─ IntentMatcher: "原生交互界面 / 动态UI"
│ └─ 命中工具: [ui_dsl_spec, ui_validate]
├─ IntentMatcher: "卡片 / 列表 / 表单 / 播放器 / 浏览器界面"
│ └─ 命中工具: [ui_dsl_spec]
└─ 优先级: 5(高于普通 text/image)

QuroToolRouter.categorize() 已加入 DYNAMIC_UI 映射,早于 ui_* 规则,避免被通用 UI 工具误判。


🐞 8 轮 Bug 修复亮点

轮次模块症状修复
Round 2 QuroUiRenderer 占位符 {{item.emoji}} 显示原文不替换 ListNode 取值模板改用 key 路径 item.emoji,不再依赖整段 item 字符串化
Round 2 QuroUiDslParser 字符串内含未转义引号导致 parse 崩溃 normalizeQuotes 引入 inSgl 跟踪,未关闭时强制补双引号
Round 3 SurfaceHost 父容器传 Infinity 触发 Compose 测量崩溃 外层裹 BoxWithConstraints,把可用宽度收紧到 maxWidth – padding
Round 4 QuroUiCatalog 颜色字面量大小写不一致(Primary/PRIMARY) QuroUiColor.parse() 单点入口,统一归一
Round 5 QuroUiRenderer text_input 在 card 内只能点一次聚焦 拆 Modifier.focusRequester,remember(root) 防止重渲染拿错引用
Round 6 QuroUiRenderer 暗色下文字看不清(用了浅色 token) 渲染时根据当前 isSystemInDarkTheme() 二次反转
Round 7 QuroUiNode QuroHtmlNode 透明背景露原生控件色 WebView setBackgroundColor(Color.TRANSPARENT) + 容器同步 graphicsLayer = 0f
Round 8 QuroUiRenderer QuroUI 区块与普通消息块视觉混淆(无边框、间距过近) 给 quro-ui 段落加 12 dp 顶部间距 + 卡片化外框,淡化正文连续感

🎯 设计哲学

❓ 为什么选 Compose 原生而不是 WebView?

维度Compose 原生WebView + HTML
性能 与系统同帧率,零额外进程 独立进程,重绘制、内存抖动
暗色一致性 跟随主题,零额外样式 需要在 HTML 里镜像一套 token
滚动/手势 LazyColumn、NestedScroll 原生开箱即用 手势与宿主 Activity 冲突、需手写桥接
体积 代码约 40 KB(解析+渲染) 离线 HTML 模板 + 50 KB+ 运行时桥接
调试 Layout Inspector / Preview 直接看 远程 Chrome DevTools
AI 输出适配 JSON 描述简单、字段扁平 HTML/CSS 结构脆弱、标签嵌套深

➡ 结论:可枚举的非媒体场景一律 Compose 原生;只有真正需要浏览器内核的(如打开任意 URL)才走 WebView 容器节点 browser。

❓ 为什么 JSON DSL 而不是 JSON Schema 或 Protobuf?

  • JSON Schema 太啰嗦,模型不爱输出;结构校验可以靠 catalog 完成
  • Protobuf / TypeScript 模型往往拼错大小写或忘了枚举值;JSON 字面量最稳
  • YAML 缩进依赖坑惨过模型
  • JSON 是当下 LLM 输出文本的最稳定格式(token 训练量最大)

🚀 未来扩展方向

  • 🧩 可视化处理(render_vispro):流程图、时序图、思维导图渲染器
  • 🎞 Timeline / Carousel 节点:横向滑动 + 自动播放
  • 🪟 visual_ask 增强:多选、可填空、附件上传
  • 🧠 state.io 持久化:节点状态写入数据模型,跨消息保持
  • 🌐 a2ui envelope 互通:与外部 a2ui 协议完全双向兼容
  • 📱 桌面 / 折叠屏断点:除 360 dp 外,新增 ≥ 600 dp / ≥ 840 dp 的多断点布局

📚 参考示例 · 完整卡片输出

下面为你列出 3 款适合远程开发的笔记本,按性价比排序:

```quro-ui
{
"type": "card",
"padding": 14,
"gap": 10,
"background": "surface",
"radius": 14,
"children": [
{
"type": "row",
"align": "spaceBetween",
"children": [
{ "type": "text", "text": "🏆 性价比首选", "weight": "bold", "size": 16 },
{ "type": "badge", "text": "TOP1", "variant": "success" }
]
},
{
"type": "text",
"text": "MacBook Air M2 · 16 GB / 512 GB",
"size": 14
},
{
"type": "row",
"gap": 8,
"children": [
{
"type": "button",
"label": "查看配置",
"variant": "primary",
"action": { "type": "open_url", "url": "https://example.com/mac-air" }
},
{
"type": "button",
"label": "加入对比",
"variant": "secondary",
"action": { "type": "tool_call", "name": "add_to_compare", "args": { "id": "mac-air-m2" } }
}
]
}
]
}
```

如果你需要开发 Android 原生,建议再考虑内存升级到 24 GB 的型号。


🌟 动态 UI,让 AI 的回答「看得见、用得上」🌟

ZorvAI · v1.0.82 · 2026-09-05

如嵌套 `tabs`/`card`,子节点也能

赞(0)
未经允许不得转载:171主机测评 » [特殊字符] ZorvAI 动态 UI 组件:让 AI 的回答「看得见、用得上」
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址