[特殊字符] ZorvAI 动态 UI 组件:让 AI 的回答「看得见、用得上」 项目简介ZorvAI 动态 UIquro-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~350QuroUiRootNode/QuroUiContainerNode/QuroUiLeafNode三层 ASTQuroListNode支持{{item.field}}QuroHtmlNode透明 WebViewQuroUiDslParser.kt~280sanitizeJson()抹平误置闭合符fixOutsideStrings()修复字符串外的脏括号normalizeQuotes()跟踪inSgl/inDbl状态QuroUiCatalog.kt~220颜色通过QuroUiColor.parse()校验图标白名单 未知图标回退默认LeafNode 合法性守卫QuroUiColor.kt~120gray.0-gray.15灰度primary/secondary/success/warning/danger语义色自动判别 light/darkQuroUiIcons.kt~200Lucide 图标集camelCase → snake_case → 小写归一缺失自动回退circle_helpQuroUiPointer.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~300ui_dsl_spec拉取动态 UI 规格提示词ui_validate模型自检输出可解析性 DSL 解析管线quro-ui 的输入是模型输出在 fenced code block 里的 JSON。我们永远不假设模型一定写出干净 JSON所以解析管线有四层防护 1️⃣sanitizeJson(raw: String)目标抹平「误置闭合符」最常见的 AI 病。funsanitizeJson(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}— 引号未关。funfixOutsideStrings(s:String):String{valoutStringBuilder()varinSglfalse;varinDblfalsefor(cins){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 设计宽度ComposablefunrememberDensityScale():Float{valconfigLocalConfiguration.currentvaldesignWidthDp360fvalactualWidthDpconfig.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: {...} }。点击后前端把namepayload回传给宿主由业务层更新数据模型并重渲染。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 / scrollrow容器gap / padding / align / wrapbox容器padding / aligncard容器padding / radius / elevation / backgroundtabs容器tabs[]activeIndex状态list容器datatemplate占位符{{item}}/{{item.field}}/{{index}}text叶子text / size / weight / color / align / maxLinesimage叶子src / fit / radius / placeholdericon叶子name (Lucide)/size / colorbadge叶子text / variant (success/warning/danger/...)progress叶子value / max / variantdivider叶子color / thicknessspacer叶子height / widthbutton叶子label / action / varianttext_input叶子placeholder / value / onSubmitcheckbox叶子label / checked / onChangeswitch叶子label / checked / onChangeselect叶子options[] / value / onChangeslider叶子min / max / value / onChangemarkdown叶子content实时渲染 Markdownhtml叶子content透明 WebView 容器v1.0.82 深度修复video叶子src / controls / autoplayaudio叶子src / controlsbrowser叶子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: 使用 Lets 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 被前端报错更稳健。 主题与样式 调色板QuroUiColorToken 类示例说明gray.0–gray.15gray.0 #FFFFFFgray.15 #0A0A0A16 阶中性灰primary主品牌色v1.0.82靛蓝 #5046E4主操作secondary次操作色次按钮successwarningdangerinfo绿/橙/红/蓝状态徽章surfaceonSurface卡片背景/前景自动暗色反转✏️ 图标QuroUiIcons内置Lucide图标集约 1000 个常用图标camelCase → snake_case → 小写归一未知图标回退circle_help绝不渲染空白方块 v1.0.82 必备输出设计 · 系统提示词这一节是让「动态 UI」真正成为默认行为的关键。### 动态 UIquro-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/imageQuroToolRouter.categorize()已加入DYNAMIC_UI映射早于ui_*规则避免被通用 UI 工具误判。 8 轮 Bug 修复亮点轮次模块症状修复Round 2QuroUiRenderer占位符{{item.emoji}}显示原文不替换ListNode 取值模板改用 key 路径item.emoji不再依赖整段item字符串化Round 2QuroUiDslParser字符串内含未转义引号导致 parse 崩溃normalizeQuotes引入inSgl跟踪未关闭时强制补双引号Round 3SurfaceHost父容器传Infinity触发 Compose 测量崩溃外层裹BoxWithConstraints把可用宽度收紧到maxWidth - paddingRound 4QuroUiCatalog颜色字面量大小写不一致Primary/PRIMARYQuroUiColor.parse()单点入口统一归一Round 5QuroUiRenderertext_input在card内只能点一次聚焦拆Modifier.focusRequesterremember(root)防止重渲染拿错引用Round 6QuroUiRenderer暗色下文字看不清用了浅色 token渲染时根据当前isSystemInDarkTheme()二次反转Round 7QuroUiNodeQuroHtmlNode透明背景露原生控件色WebViewsetBackgroundColor(Color.TRANSPARENT) 容器同步graphicsLayer 0fRound 8QuroUiRendererQuroUI 区块与普通消息块视觉混淆无边框、间距过近给quro-ui段落加 12 dp 顶部间距 卡片化外框淡化正文连续感 设计哲学❓ 为什么选 Compose 原生而不是 WebView维度Compose 原生WebView HTML性能与系统同帧率零额外进程独立进程重绘制、内存抖动暗色一致性跟随主题零额外样式需要在 HTML 里镜像一套 token滚动/手势LazyColumn、NestedScroll 原生开箱即用手势与宿主 Activity 冲突、需手写桥接体积代码约 40 KB解析渲染离线 HTML 模板 50 KB 运行时桥接调试Layout Inspector / Preview 直接看远程 Chrome DevToolsAI 输出适配JSON 描述简单、字段扁平HTML/CSS 结构脆弱、标签嵌套深➡结论可枚举的非媒体场景一律 Compose 原生只有真正需要浏览器内核的如打开任意 URL才走 WebView 容器节点browser。❓ 为什么 JSON DSL 而不是 JSON Schema 或 ProtobufJSON 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子节点也能