ZorvAI 可视化技术架构详解:从弹窗到跨应用调用的完整指南

9 阅读7分钟

开源地址: 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)
    }
}

支持的图表类型:

类型语法用途
flowchartgraph TD / flowchart LR流程图、架构图
sequenceDiagramsequenceDiagram时序图
stateDiagram-v2stateDiagram-v2状态机
classDiagramclassDiagram类图
mindmapmindmap思维导图
gitGraphgitGraphGit 提交图
piepie饼图
timelinetimeline时间线

五、自研小程序框架(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 内置模块

模块方法功能
storagesetItem / getItem / removeItem / clear本地存储(SharedPreferences)
devicegetSystemInfo / vibrate设备信息、震动
uitoast / setNavigationBarTitleToast 提示、导航栏标题
networkrequestHTTP 请求(GET/POST)
routernavigateTo / 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 组件分类一览

归类组件类型用途
Inputbutton, toggle, slider, form, chips, quickreply, quickaction用户输入交互
Datastat, progress, gauge, counter, rating, pie, chart, heatmap, radar, compare数据展示
Mediamedia, mediaplay, image, video多媒体播放
Layouttabs, expandable, carousel, kanban, steps, timeline布局组织
Actionactions, toolcall, timer动作触发
Navigationbreadcrumb, segmented, list导航跳转
Decorationalert, badge, avatargroup, tagcloud, color, note, info装饰辅助
Specialmermaid, 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 BinderACI 控制端 ↔ 受控端通信
小程序运行时QuickJS (Native) / WebView (DOM)小程序逻辑执行
图表渲染Mermaid.js (离线内联)流程图/时序图/类图等
组件系统SnapshotStateList + Compose60+ 可交互组件状态管理
工具注册QuroToolRegistry120+ 工具统一注册表

附录:关键文件索引

功能文件路径
可视化弹窗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
弹窗UIui/VisualPopupDialog.kt
询问UIui/VisualQuestionDialog.kt

ZorvAI — 让 AI 真正成为能替你操作设备的「智能体」,而不只是会聊天的模型。