开源地址: github.com/Quor-a/Zorv…
一、整体架构概览
ZorvAI 的可视化体系并非单一模块,而是一套分层协作的交互框架:
┌─────────────────────────────────────────────────────────────┐
│ 用户界面层 (Jetpack Compose) │
│ ChatScreen · VisualPopupDialog · VisualQuestionDialog │
│ MiniAppCardView · MermaidCardView · 60+ 组件渲染器 │
├─────────────────────────────────────────────────────────────┤
│ 工具调度层 (QuroToolRegistry) │
│ visual_popup · visual_question · ui_widget · aci_call ... │
├─────────────────────────────────────────────────────────────┤
│ 能力执行层 │
│ VisualPopupQueue · VisualQuestionQueue · QuroChatCardStore │
│ MiniAppBridgeInterface · McpAciBridge · QuroAidlAciManager │
├─────────────────────────────────────────────────────────────┤
│ 运行时 / 引擎层 │
│ QuickJS · Brython · Mermaid.js · WebView · AIDL Binder │
└─────────────────────────────────────────────────────────────┘
所有可视化能力统一以 QuroTool 接口暴露给 LLM,AI 通过 ReAct 循环自动发现并调用。核心设计原则:
- Tool-first:一切能力皆工具,LLM 只看注册表就能发现并使用
- 事件驱动:弹窗/询问通过
Channel+Flow异步通知 UI 层 - 线程同步:工具调用线程通过
CountDownLatch阻塞等待用户操作结果 - 气泡融合:可视化组件直接嵌入聊天气泡,而非浮层弹窗
二、可视化弹窗系统
ZorvAI 提供两种弹窗模式,覆盖「固定组件」和「完全自由」两种场景。
2.1 固定 UI 弹窗(visual_popup)
AI 调用 visual_popup 工具,传入结构化参数,系统渲染包含按钮、输入框、图片的弹窗。
数据模型:
// 弹窗按钮
data class PopupButton(
val text: String,
val value: String,
val style: String = "primary" // primary / secondary / danger / success
)
// 弹窗输入框
data class PopupInput(
val id: String,
val label: String,
val placeholder: String = "",
val defaultValue: String = "",
val type: String = "text" // text / number / password / email
)
// 待处理的弹窗数据
data class VisualPopupData(
val id: String,
val title: String,
val content: String, // 支持 Markdown / HTML / 纯文本
val buttons: List<PopupButton>,
val inputs: List<PopupInput>,
val imageUrl: String?,
val width: Int?,
val height: Int?,
val cancelable: Boolean = true,
val timeout: Int = 60,
val latch: CountDownLatch, // 阻塞等待用户操作
val result: AtomicReference<PopupResult?>,
var status: PopupStatus = PopupStatus.PENDING
)
enum class PopupStatus { PENDING, ACTIVE, COMPLETED, CANCELLED }
data class PopupResult(
val buttonValue: String?,
val inputValues: Map<String, String>,
val cancelled: Boolean = false
)
AI 调用示例:
{
"title": "天气查询确认",
"content": "即将查询 **北京** 的天气信息,是否继续?",
"buttons": [
{"text": "确认查询", "value": "confirm", "style": "primary"},
{"text": "取消", "value": "cancel", "style": "danger"}
],
"inputs": [
{"id": "city", "label": "城市", "placeholder": "输入城市名", "type": "text"}
],
"image_url": "https://example.com/weather-icon.png",
"cancelable": true,
"timeout": 60
}
返回结果:
{
"button": "confirm",
"inputs": {"city": "北京"},
"cancelled": false
}
核心执行流程:
class VisualPopupTool : QuroTool {
override val name = "visual_popup"
override fun run(context: Context, arguments: String): String {
val args = JSONObject(arguments)
val latch = CountDownLatch(1)
val result = AtomicReference<PopupResult?>(null)
// 构造弹窗数据
val popup = VisualPopupData(
id = UUID.randomUUID().toString(),
title = args.optString("title", "提示"),
content = args.optString("content", ""),
buttons = parseButtons(args.optJSONArray("buttons")),
inputs = parseInputs(args.optJSONArray("inputs")),
imageUrl = args.optString("image_url", null),
width = args.optInt("width", 400),
height = args.optInt("height", 300),
cancelable = args.optBoolean("cancelable", true),
timeout = args.optInt("timeout", 60),
latch = latch,
result = result
)
// 加入队列,通知 UI 层
VisualPopupQueue.addPopup(popup)
// 阻塞等待用户操作(带超时)
val completed = latch.await(popup.timeout.toLong(), TimeUnit.SECONDS)
return if (completed && result.get() != null) {
result.get().toJson().toString()
} else {
"""{"cancelled": true, "reason": "timeout"}"""
}
}
}
2.2 AI 自写 UI 弹窗(visual_custom_popup)
当固定组件无法满足需求时,AI 可以完全自写 HTML/CSS/JS,渲染为自由形态的弹窗。
数据模型:
data class VisualCustomPopupData(
val id: String,
val title: String,
val htmlContent: String, // AI 完全自写的 HTML/CSS/JS
val cardTitle: String,
val cardDescription: String,
val width: Int?,
val height: Int?,
val cancelable: Boolean = true,
val timeout: Int = 120,
val latch: CountDownLatch,
val result: AtomicReference<String?> // 返回 AI 自定义的结果
)
AI 调用示例:
{
"title": "数据可视化",
"html": "<!DOCTYPE html><html><head><style>body{font-family:sans-serif;padding:20px;}</style></head><body><h2>销售数据</h2><canvas id='chart' width='300' height='200'></canvas><script>const ctx=document.getElementById('chart').getContext('2d');ctx.fillStyle='#4CAF50';ctx.fillRect(10,150,60,50);ctx.fillStyle='#2196F3';ctx.fillRect(80,100,60,100);ctx.fillStyle='#FF9800';ctx.fillRect(150,50,60,150);window.parent.postMessage(JSON.stringify({action:'submit',data:{selected:'chart_viewed'}}),'*</script></body></html>",
"card_title": "数据看板",
"card_description": "点击查看销售数据可视化",
"width": 400,
"height": 350,
"overlay": false
}
HTML 与原生通信协议:
// JS → Native:提交结果
window.parent.postMessage(
JSON.stringify({action: 'submit', data: {key: 'value'}}),
'*'
);
// JS → Native:关闭弹窗
window.parent.postMessage(
JSON.stringify({action: 'close'}),
'*'
);
系统自动包装: AI 提交的 HTML 会被 generateCustomPopupHtml() 自动注入 submitResult() 和 closePopup() 函数,无需 AI 手动处理通信细节。
2.3 悬浮窗模式
visual_custom_popup 支持 overlay: true 参数,通过 VisualPopupOverlayService(前台 Service)在 App 外以系统级悬浮窗显示:
// AndroidManifest.xml 声明
<service
android:name=".service.VisualPopupOverlayService"
android:foregroundServiceType="dataSync"
android:exported="false" />
// 需要权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
悬浮窗使用 WindowManager + ComposeView 渲染,支持拖动、关闭。
三、可视化询问机制
AI 在执行任务时,遇到模糊命令、缺少信息或需要确认,必须调用可视化工具询问用户,禁止猜测、禁止假设、禁止跳过。
3.1 选择题询问(visual_question)
data class VisualPendingQuestion(
val question: String,
val options: List<String>,
val allowCustom: Boolean,
val title: String,
val latch: CountDownLatch,
val result: AtomicReference<String?>
)
AI 调用示例:
{
"question": "你想要哪种天气信息?",
"options": ["当前天气", "未来7天预报", "空气质量"],
"allow_custom": true,
"title": "天气查询",
"timeout": 30
}
弹窗行为: 不允许关闭(dismissOnBackPress=false, dismissOnClickOutside=false),必须回答。
3.2 操作确认(visual_action)
data class VisualPendingAction(
val title: String,
val message: String,
val buttons: List<VisualButtonConfig>,
val latch: CountDownLatch,
val result: AtomicReference<String?>
)
data class VisualButtonConfig(
val text: String,
val value: String,
val style: String = "primary"
)
AI 调用示例:
{
"title": "文件操作确认",
"message": "确定要删除 /sdcard/Download/test.txt 吗?此操作不可恢复。",
"buttons": [
{"text": "确认删除", "value": "delete", "style": "danger"},
{"text": "取消", "value": "cancel", "style": "secondary"}
],
"timeout": 30
}
3.3 事件驱动架构
所有可视化询问统一使用事件驱动模型:
// 全局队列(单例)
object VisualQuestionQueue {
private val _pendingQuestions = mutableListOf<VisualPendingQuestion>()
fun addQuestion(question: VisualPendingQuestion) {
synchronized(_pendingQuestions) {
_pendingQuestions.add(question)
}
// 通过 Channel 通知 UI 层
scope.launch { _eventFlow.emit(VisualEvent.QuestionAdded) }
}
fun submitAnswer(index: Int, answer: String) {
synchronized(_pendingQuestions) {
if (index in _pendingQuestions.indices) {
_pendingQuestions[index].result.set(answer)
_pendingQuestions[index].latch.countDown()
_pendingQuestions.removeAt(index)
}
}
}
}
四、可视化编程(Mermaid)
Mermaid 图表作为 ui_widget 工具的 mermaid 类型组件实现,支持流程图、时序图、状态机、类图、思维导图等。
4.1 数据模型
data class MermaidCard(
override val id: String,
override val title: String,
val source: String, // AI 生成的 Mermaid 源码
val theme: String = "", // default | dark | forest | neutral | base
) : QuroChatCard
4.2 两种触发方式
方式一:AI 调用 ui_widget 工具
{
"type": "mermaid",
"title": "系统架构图",
"source": "graph TD\n A[用户] --> B[AI引擎]\n B --> C[工具集]\n C --> D[ACI控制端]\n D --> E[受控端App]",
"theme": "default"
}
方式二:用户或 AI 直接写围栏代码块
```mermaid
sequenceDiagram
participant U as 用户
participant AI as AI引擎
participant T as 工具集
participant A as ACI受控端
U->>AI: 发送指令
AI->>T: 调用工具
T->>A: ACI调用
A-->>T: 返回结果
T-->>AI: 工具结果
AI-->>U: 回复消息
```
对话框自动识别 ```mermaid 围栏并渲染为矢量图。
4.3 离线渲染实现
// 资源文件
// app/src/main/assets/www/mermaid_render.html
// app/src/main/assets/libs/mermaid.min.js (内联打包,不依赖 CDN)
class MermaidWebView(context: Context) : WebView(context) {
fun renderMermaid(source: String, theme: String = "default") {
// 加载离线渲染页面
loadUrl("file:///android_asset/www/mermaid_render.html")
// 注入 Mermaid 源码
evaluateJavascript("""
mermaid.initialize({startOnLoad:false, theme:'$theme'});
mermaid.render('graph', `$source`).then(({svg}) => {
document.getElementById('container').innerHTML = svg;
});
""".trimIndent(), null)
}
}
支持的图表类型:
| 类型 | 语法 | 用途 |
|---|---|---|
flowchart | graph TD / flowchart LR | 流程图、架构图 |
sequenceDiagram | sequenceDiagram | 时序图 |
stateDiagram-v2 | stateDiagram-v2 | 状态机 |
classDiagram | classDiagram | 类图 |
mindmap | mindmap | 思维导图 |
gitGraph | gitGraph | Git 提交图 |
pie | pie | 饼图 |
timeline | timeline | 时间线 |
五、自研小程序框架(MiniApp)
ZorvAI 自研了一套轻量小程序框架,AI 生成完整 HTML+JS+CSS 代码,在对话框内实时渲染为可交互的小程序页面。
5.1 架构设计
┌─────────────────────────────────────────┐
│ MiniApp WebView │
│ ┌─────────────────────────────────┐ │
│ │ AI 生成的 HTML/JS/CSS │ │
│ │ Page({data:{...}, methods:{...}})│ │
│ └───────────────┬─────────────────┘ │
│ │ window.NativeBridge │
├──────────────────┼──────────────────────┤
│ JSBridge │ │
│ MiniAppBridgeInterface │
│ @JavascriptInterface │
│ fun invoke(json: String) │
├──────────────────┴──────────────────────┤
│ 原生能力模块 │
│ Storage · Device · UI · Network · Router│
└─────────────────────────────────────────┘
5.2 JSBridge 接口
class MiniAppBridgeInterface(
private val context: Context,
private val webView: WebView
) {
private val modules = mutableMapOf<String, MiniAppBridgeModule>()
@JavascriptInterface
fun invoke(json: String) {
val request = JSONObject(json)
val module = request.getString("module")
val method = request.getString("method")
val params = request.getJSONObject("params")
val id = request.getString("id")
modules[module]?.invoke(method, params) { code, data, message ->
sendResponse(id, code, data, message)
}
}
fun sendResponse(id: String, code: Int, data: Any?, message: String?) {
val response = JSONObject().apply {
put("id", id)
put("type", "response")
put("code", code)
put("data", data)
put("message", message ?: "")
}
webView.post {
webView.evaluateJavascript(
"window.onNativeResponse('${response}')", null
)
}
}
}
interface MiniAppBridgeModule {
val name: String
fun invoke(method: String, params: JSONObject, callback: (Int, Any?, String?) -> Unit)
}
5.3 内置模块
| 模块 | 方法 | 功能 |
|---|---|---|
storage | setItem / getItem / removeItem / clear | 本地存储(SharedPreferences) |
device | getSystemInfo / vibrate | 设备信息、震动 |
ui | toast / setNavigationBarTitle | Toast 提示、导航栏标题 |
network | request | HTTP 请求(GET/POST) |
router | navigateTo / navigateBack | 页面导航 |
5.4 AI 调用示例
{
"type": "miniapp",
"title": "待办清单",
"html": "<div id='app'><h3>待办清单</h3><input id='input' placeholder='添加待办'><button onclick='addTodo()'>添加</button><ul id='list'></ul></div><script>Page({data:{todos:[]},addTodo(){const input=document.getElementById('input');const text=input.value;if(text){this.data.todos.push(text);this.renderList();input.value='';}},renderList(){const list=document.getElementById('list');list.innerHTML=this.data.todos.map((t,i)=>'<li>'+t+' <button onclick=\"Page.current.removeTodo('+i+')\">删除</button></li>').join('');},removeTodo(i){this.data.todos.splice(i,1);this.renderList();}});</script>"
}
5.5 双后端运行时
- QuickJS 后端:Native 线程执行,带内存上限 16MB + 超时中断 2s + 关闭
eval - WebView 后端:零 NDK 依赖,通过 DOM 渲染
六、可视化组件引擎(ui_widget)
ui_widget 工具支持 60+ 种可交互组件,直接融进聊天气泡。
6.1 工具入口
class UiWidgetTool : QuroTool {
override val name = "ui_widget"
override val parametersJson = """
{"type":"object","properties":{"spec":{"type":"string"}},"required":["spec"]}
""".trimIndent()
override fun run(context: Context, arguments: String): String {
val jo = JSONObject(arguments)
val spec = jo.optString("spec", "").ifBlank { arguments }
val card = parseComponentSpec(spec) ?: return "未知组件类型"
// 挂进聊天气泡
val bridge = QuroUiActionBridge.onCard
if (bridge != null) bridge(card)
else Snapshot.withMutableSnapshot { QuroChatCardStore.add(card) }
return """{"ok":true,"id":"${card.id}","title":"${card.title}"}"""
}
}
6.2 组件分类一览
| 归类 | 组件类型 | 用途 |
|---|---|---|
| Input | button, toggle, slider, form, chips, quickreply, quickaction | 用户输入交互 |
| Data | stat, progress, gauge, counter, rating, pie, chart, heatmap, radar, compare | 数据展示 |
| Media | media, mediaplay, image, video | 多媒体播放 |
| Layout | tabs, expandable, carousel, kanban, steps, timeline | 布局组织 |
| Action | actions, toolcall, timer | 动作触发 |
| Navigation | breadcrumb, segmented, list | 导航跳转 |
| Decoration | alert, badge, avatargroup, tagcloud, color, note, info | 装饰辅助 |
| Special | mermaid, miniapp, htmlpreview | 可视化编程/小程序/HTML预览 |
6.3 卡片仓库(QuroChatCardStore)
object QuroChatCardStore {
val cards: SnapshotStateList<QuroChatCard>
fun add(card: QuroChatCard)
fun remove(id: String)
// 交互状态更新
fun toggleTodo(cardId: String, itemIndex: Int)
fun setToggle(cardId: String, checked: Boolean)
fun setSlider(cardId: String, value: Float)
fun setCounter(cardId: String, value: Int)
fun setSegmented(cardId: String, index: Int)
fun setRating(cardId: String, value: Int)
fun setChips(cardId: String, selected: List<String>)
fun setTabs(cardId: String, index: Int)
fun setExpandable(cardId: String, expanded: Boolean)
fun setFormField(cardId: String, key: String, value: String)
// 工具调用状态
fun updateToolCall(cardId: String, status: String?, progress: Float?, message: String?)
fun appendStreamLine(cardId: String, line: String)
}
6.4 组件调用示例
进度条:
{
"type": "progress",
"value": 75,
"title": "任务完成度"
}
待办清单:
{
"type": "todo",
"title": "今日任务",
"items": [
{"text": "完成报告", "checked": true},
{"text": "回复邮件", "checked": false},
{"text": "代码审查", "checked": false}
]
}
图表:
{
"type": "chart",
"title": "月度销售",
"data": {
"labels": ["1月", "2月", "3月", "4月"],
"values": [120, 200, 150, 180]
}
}
七、ACI 跨应用能力接口
ACI(Agent Capability Interface)是一套同设备、无 Root、基于 AIDL Binder 的本地跨应用调用框架。
7.1 架构设计
┌──────────────────────────┐ AIDL Binder ┌──────────────────────────┐
│ 控制端(Zorv AI) │ ─── call / callAsync ───> │ 受控端(你的 App) │
│ QuroAidlAciManager │ <── ACIResponse ─────── │ BaseACIService 子类 │
│ - discover() 发现 │ │ - onCreateCapabilities │
│ - bind() 绑定 │ ─── ACTION_WAKE 广播 ──> │ - onCall() 处理 │
│ - getCapabilities() 取清单│ (唤醒 stopped 进程) │ - onCheckPermission() │
└──────────────────────────┘ └──────────────────────────┘
7.2 控制端管理器
class QuroAidlAciManager private constructor(private val appContext: Context) {
companion object {
const val ACI_ACTION = "ai.aci.core.ACTION_BIND"
const val ACI_WAKE_ACTION = "ai.aci.core.ACTION_WAKE"
fun init(context: Context)
fun getInstance(): QuroAidlAciManager
}
// ① 服务发现:扫描已安装 App 中声明了 ACI Intent Filter 的服务
fun discover(): List<DiscoveredApp>
// ② 同步调用:通过 AIDL Binder 调用受控端能力
fun call(pkg: String, cap: String, args: Bundle): AidlAciResponse
// ③ 生成能力清单给 LLM
fun getCapabilityPrompt(): String
// ④ 能力索引(按包名分组)
fun getCapabilityIndex(): Map<String, List<Capability>>
}
7.3 AI 工具接口
// 列出已发现的 ACI 受控端 App 及其能力
class QuroAidlAciListTool : QuroTool {
override val name = "aci_list"
override val description = "列出所有已连接的ACI受控端App及其暴露的能力"
}
// 调用受控端能力
class QuroAidlAciCallTool : QuroTool {
override val name = "aci_call"
override val description = "调用指定受控端App的ACI能力"
}
调用参数格式:
{
"target_package": "com.example.weather",
"capability": "weather_now",
"args": {"city": "北京"},
"confirm": true
}
返回结果:
{
"success": true,
"result": {
"temperature": "25°C",
"weather": "晴",
"humidity": "45%"
}
}
7.4 受控端最小接入示例
class MyAciService : BaseAidlAciService() {
override fun onCreateCapabilities(caps: MutableList<Capability>) {
caps.add(Capability.create("open_url", "在浏览器打开指定网址")
.addParam("url", "string", true, "目标网址")
.addFlag(Capability.FLAG_BACKGROUND))
}
override fun onCall(req: AidlAciRequest): AidlAciResponse {
return when (req.capability) {
"open_url" -> {
val url = req.args.getString("url")
// 执行打开网址操作
AidlAciResponse.success().putResult("ok", true)
}
else -> AidlAciResponse.error("未知能力: ${req.capability}")
}
}
}
八、工具注册与调度
所有可视化工具在 QuroBuiltInTools.kt 中统一注册:
fun buildQuroRegistry(context: Context? = null): QuroToolRegistry {
val r = QuroToolRegistry()
// 可视化问答和操作弹窗工具
r.register(VisualQuestionTool()) // visual_question
r.register(VisualActionTool()) // visual_action
r.register(VisualPopupTool()) // visual_popup
r.register(VisualCustomPopupTool()) // visual_custom_popup
// 对话框内联 UI 组件工具
r.register(UiWidgetTool()) // ui_widget
// ACI(Agent Capability Interface)
r.register(QuroAidlAciListTool()) // aci_list
r.register(QuroAidlAciCallTool()) // aci_call
// ... 其他 100+ 工具 ...
return r
}
LLM 调度流程:
用户输入 → LLM 推理 → 选择工具 → ReAct 循环
↓
visual_popup / ui_widget / aci_call ...
↓
工具执行(阻塞等待用户操作)
↓
结果返回 LLM → 生成回复
九、技术栈总结
| 层级 | 技术 | 用途 |
|---|---|---|
| UI 渲染 | Jetpack Compose + WebView | 弹窗、组件、小程序渲染 |
| 事件驱动 | Kotlin Channel + Flow | 弹窗/询问异步通知 |
| 线程同步 | CountDownLatch + AtomicReference | 工具调用阻塞等待 |
| 跨应用 | AIDL Binder | ACI 控制端 ↔ 受控端通信 |
| 小程序运行时 | QuickJS (Native) / WebView (DOM) | 小程序逻辑执行 |
| 图表渲染 | Mermaid.js (离线内联) | 流程图/时序图/类图等 |
| 组件系统 | SnapshotStateList + Compose | 60+ 可交互组件状态管理 |
| 工具注册 | QuroToolRegistry | 120+ 工具统一注册表 |
附录:关键文件索引
| 功能 | 文件路径 |
|---|---|
| 可视化弹窗 | core/tools/VisualPopupTool.kt |
| AI自写弹窗 | core/tools/VisualCustomPopupTool.kt |
| 选择题询问 | core/tools/VisualQuestionTool.kt |
| 操作确认 | core/tools/VisualActionTool.kt |
| 小程序桥接 | core/miniapp/MiniAppBridgeInterface.kt |
| UI组件工具 | core/tools/QuroToolsUiWidget.kt |
| 组件数据模型 | core/cards/QuroChatCard.kt |
| 工具注册 | core/tools/QuroBuiltInTools.kt |
| ACI管理器 | core/aidlaci/QuroAidlAciManager.kt |
| ACI工具 | core/aidlaci/QuroAidlAciTools.kt |
| 弹窗UI | ui/VisualPopupDialog.kt |
| 询问UI | ui/VisualQuestionDialog.kt |
ZorvAI — 让 AI 真正成为能替你操作设备的「智能体」,而不只是会聊天的模型。