ARTICLE · 1021767
ZorvAI APK 插件框架 · 技术架构与功能介绍
ZorvAI APK 插件框架 · 技术架构与功能介绍
文档类型:技术架构说明 | 架构介绍 | 功能介绍适用宿主:Zorv AI(
com.ai.assistance.quro)≥ 1.0.89框架版本:v1.0.90代码位置::plugin-contract(契约层)·:plugin-engine(引擎层)·app/core/plugin+app/core/tools+app/ui(宿主接入层)示例插件::plugin-devkit:plugin-express:plugin-todo:plugin-units:plugin-sysinfo:plugin-zorvweb
开源地址
| GitHub | github.com/Quor-a/ZorvAI |
| Gitee | gitee.com/ZorvAI/ZorvAI |
| GitLab | jihulab.com/quor-a-group/ZorvAI |
| github.com/Quor-a/ZorvAI/releases | |
| github.com/Quor-a/ZorvAI/issues | |
apk-plugin-arch 分支) | docs/APK_PLUGIN_DEVELOPMENT_MANUAL.md |
git clone https://github.com/Quor-a/ZorvAI |
本项目完全开源(Apache-2.0),多平台托管;插件框架的源码就在上面的仓库里—— 契约层
plugin-contract/、引擎层plugin-engine/、6 个示例插件plugin-*/可直接编译运行。
目录
| 一 | |
| 二 | |
| 三 | |
| 四 | |
| 五 | |
| 六 | |
| 七 | |
| 八 | |
| 九 | |
| 十 | |
| 十一 | |
| 十二 | |
| 十三 |
一、为什么需要 APK 插件框架
ZorvAI 是一个能力密度很高的 AI 助理:终端、浏览器、ACI、TTS、可视化和 14 类扩展能力都长在宿主里。 传统做法下,每加一个能力都要动宿主源码——改工具注册表、改系统提示、改设置页、重新发版。 这条路径有三个绕不开的成本:
| 发布耦合 | |
| 源码耦合 | |
| 能力上限 = 团队上限 |
APK 插件的目标:把「给 AI 加能力」从「改宿主」变成「装一个 APK」。
设计目标四条:
- 宿主零改动
—— 宿主不认识任何具体插件,只认识「扩展点」这一个抽象; - 能力可分发
—— 插件是标准 APK,可签名、可版本管理、可从任意渠道分发; - AI 原生
—— 插件注册的工具自动进入 LLM 工具集,装完立刻可用,无需重启; - 可控安全
—— 插件与宿主同进程、同权限,因此必须有明确的信任边界(同签名)。
二、核心思想(一句话)
宿主不认识任何具体插件,只认识「扩展点」这一个抽象。 插件往槽里放实现,宿主负责调度。
// 插件作者写的全部代码,本质上就是「往槽里放东西」class HelloEntry : PluginEntry {override funonCreate(ctx: PluginContext) {plugin(ctx) {aiTool("hello_greet", "向某人打招呼。当用户说「跟某人打招呼」时调用。") {param("who", ParamType.STRING, "要打招呼的对象名字")execute { args -> ToolResult.text("你好,${args.string("who")}!") }}}}}
这段代码不进宿主、不改宿主、不重新编译宿主。装进宿主后,LLM 的工具集里立刻多出一个 hello_greet。
这就是「神经-代码一体化」在宿主侧的具体形态:宿主提供稳定的骨架(扩展点 + 调度), 插件提供可替换的血肉(实现),两者通过签名互相担保。
三、分层架构
框架是严格的四层结构,依赖方向单向向下,上层不知道下层的具体实现:
┌──────────────── 宿主 App(:app)· 调度层 ────────────────────────────┐│ ││ QuroApplication.onCreate() ││ └── QuroPluginHost.attach(this) ← 宿主唯一一次性接线点 ││ ├── QuroPluginEngine.init(app, HOST_CAPS) ││ ├── AciBridge 双向注入 ││ └── QuroPluginEngine.loadAllInstalled() ││ ││ AI 工具链 ││ QuroToolRegistry.coreSpecs() ││ └── pluginHostToolSpecs() ││ ├── QuroPluginTools → 单入口工具 apk_plugin ││ └── QuroPluginHost.toolSpecs() → 插件贡献的 AI 工具 ││ ││ QuroToolEngine.execute() ││ ├── 命中插件工具 → QuroPluginHost.executePluginTool()(suspend) ││ └── 否则走内置注册表派发 ││ ││ 界面 ││ PluginManagerScreen ← 启动器式插件桌面(网格/搜索/长按菜单/Dock)││ PluginSurfaceActivity ← 通用界面承载 Activity │└──────────────────────────────────────────────────────────────────────┘▲HostToolBridge / AciBridge(宿主取用桥)│┌────────────── :plugin-engine(引擎层)· 能力层 ───────────────────────┐│ QuroPluginEngine 加载 / 卸载 / 热重载(DexClassLoader) ││ PluginInstaller 清单解析 · 同签名校验 · 原子替换 · .so 提取 ││ PluginClassLoader 隔离类加载器(宿主 ClassLoader 作父) ││ PluginContextImpl 插件运行时上下文(存储 / 日志 / 互调) ││ ExtensionRegistry 14 个扩展点「收纳槽」 ││ HostToolBridge 扩展点 → 宿主工具规格 ││ AciBridge ACI 能力双向桥 │└──────────────────────────────────────────────────────────────────────┘▲┌──────────── :plugin-contract(契约层|compileOnly)· 约定层 ──────────┐│ PluginEntry PluginContext ExtensionType PluginDsl ToolSpec… │└──────────────────────────────────────────────────────────────────────┘▲┌──────────────── 你的插件 APK(独立签名 APK)· 实现层 ─────────────────┐│ YourEntry : PluginEntry → onCreate 里 plugin(ctx) { ... } │└──────────────────────────────────────────────────────────────────────┘
各层职责
| 契约层 | :plugin-contract | compileOnly、引擎 implementation | |
| 引擎层 | :plugin-engine | implementation | |
| 调度层 | :app | ||
| 实现层 |
为什么契约层必须是
compileOnly? 若用implementation把契约层打进插件 APK,插件与宿主会各持有一份PluginEntry类的副本。 宿主用自己的 ClassLoader 加载插件时,ClassCastException会立刻出现—— 这是本框架第一大坑,也是新人最常踩的坑。
四、四个关键机制
4.1 扩展点收纳槽(ExtensionRegistry)
框架的灵魂是一个只有 14 个格子的注册表。每个格子对应一类可插入的能力:
AI_TOOL | HostToolBridgeQuroToolRegistry | ||
ACI_CAPABILITY | AciBridge | ||
UI_SURFACE | PluginSurfaceActivity | ||
CHAT_CARD | |||
UI_WIDGET | |||
MODEL_PROVIDER | |||
RAG_SOURCE | |||
COMMAND | /斜杠指令 | ||
SETTING | |||
SCHEDULE_TASK | |||
CHANNEL | |||
FILE_HANDLER | |||
CODE_RUNTIME | |||
SPEECH |
收纳槽的索引键是 (ExtensionType, id):
同名同类的扩展后注册者覆盖先注册者 —— 所以工具名必须加插件前缀(如 express_query、web_open);插件卸载时 unregisterAll()一次性清空该插件的全部格子,不留悬挂引用。
4.2 类加载与资源挂载
插件 APK 不是系统安装的应用,它用 DexClassLoader 在宿主进程内加载:
PluginClassLoader(dexPath = <host>/files/plugins/<id>/active/base.apk,optimizedDir = <host>/files/plugins/.odex/<id>,nativeLibraryDir = <host>/files/plugins/<id>/active/lib, ← 按设备 ABI 提取的 .soparent = 宿主 ClassLoader ← 关键)
两个设计细节:
- 父加载器 = 宿主 ClassLoader
:插件因此能拿到宿主的 Class,也能被宿主反射实例化; - 资源挂载 = 宿主资源路径 + 插件资源路径
:新建 Android.Resources时把两边都挂上, 插件里的R.string.xxx能用,同时插件的 View 也能继承宿主主题(不会出现样式割裂)。
因为跑在宿主进程内,插件不能有自己的 Activity / Service / BroadcastReceiver—— 要界面请注册 UI_SURFACE,由宿主用通用 Activity 承载。
4.3 双向桥(HostToolBridge / AciBridge)
桥的作用是把插件的能力翻译成宿主认识的形态,两个方向:
方向一(插件 → 宿主)AiToolExtension → HostToolBridge.collectToolSpecs()→ ToolParamSpec → OpenAI function-callingJSONSchema→ QuroToolRegistry.coreSpecs() → LLM tools 字段方向二(ACI 双向)AciCapabilityExtension → AciBridge.capabilitySink→ QuroPluginAciRegistry.publish() → 对外能力清单AciBridge.externalCapabilities + aciInvoker→ refreshAciMirror()(宿主定期刷新)→ 外部 ACI 能力镜像成 AI 工具 aci_list / aci_call
结果:插件既是能力的提供方,也是能力的消费方,且两条链路都收敛到「AI 工具」这一个出口。
4.4 单入口工具 apk_plugin
框架只暴露 1 个 AI 工具给 LLM,而不是十几个零散管理工具。这样做的原因:
工具集里工具越少,LLM 选错工具的概率越低; 管理类动作(装/卸/查)不该进入日常工具集,只在需要时由同一入口分发。
status | engine_ready 等) | |
list | ||
info | plugin_id | |
tools | ||
surfaces | ||
open | surface_id | |
install | pathskip_signature_check) | |
install_builtin | ||
uninstall | plugin_id | |
reload | plugin_id | |
call | nameargs) |
call 是关键的兜底通道:工具集被裁剪、或插件刚装完还没轮到下一轮时, AI 仍可用 apk_plugin(action="call", name="...", args="{...}") 调用插件工具。 宿主侧等价于直接调 QuroPluginHost.executePluginTool,不经过工具集—— 所以「插件装了但 AI 用不了」这个死角不存在。
历史说明:早期版本曾用 6 个独立工具(
plugin_list/plugin_info/plugin_install/plugin_uninstall/plugin_reload/plugin_surface_open)实现同样的功能, 现已全部删除,统一收敛为apk_plugin。
五、加载全流程
5.1 安装(install(apk))
install(apk)├─ 1. PackageManager 读清单 → 校验 meta-data quro.plugin.entry,缺失即拒├─ 2. 校验 APK 签名 SHA-256 == 宿主签名,不一致即拒├─ 3. 取 pluginId / versionCode / versionName / label(桌面显示名)├─ 4. 原子替换写入 /data/data/<host>/files/plugins/<pluginId>/active/base.apk├─ 5. 从 APK 提取 lib/<abi>/*.so → active/lib/├─ 6. 写 SharedPreferences 记录(quro_plugins/record_<pluginId>)└─ 7. load(pluginId)├─ 新建 PluginClassLoader(宿主 ClassLoader 作父)├─ Android.Resources 挂「宿主资源路径 + 插件资源路径」├─ 反射 newInstance() → YourEntry└─ YourEntry.onCreate(ctx) ← 你在这里注册扩展点
七步里前两步是信任闸门,任何一步不过,安装直接失败并给出明确原因(见手册「调试与排错」)。
5.2 启动(宿主冷启动)
QuroApplication.onCreate()└── QuroPluginHost.attach(app)├── QuroPluginEngine.init(app, HOST_CAPS)│ HOST_CAPS = {llm, memory, tts, stt, aci, terminal, file, web, screen, shell}├── AciBridge 双向注入├── QuroPluginEngine.loadAllInstalled() ← 已装插件的 onCreate 在此跑├── refreshAciMirror() ← 外部 ACI 能力镜像成 AI 工具└── QuroPluginAciRegistry.publish(...) ← 插件能力对外发布
性能约束:
onCreate在Application.onCreate的loadAllInstalled()里同步执行, 所以插件必须保持轻量——耗时的初始化放到首次execute时惰性执行,或自己开后台线程。
六、AI 工具链(让 LLM 自己找到插件)
这是整个框架最核心的价值链路。插件注册完,AI 不用被通知就知道新工具存在:
YourEntry.onCreate└─ ctx.register(AiToolExtension)└─ ExtensionRegistry(收纳槽)└─ HostToolBridge.collectToolSpecs()└─ QuroPluginHost.toolSpecs() ← ToolParamSpec → JSON Schema└─ pluginHostToolSpecs() → QuroToolRegistry.coreSpecs()└─ LLM tools 字段(下一轮 functioncalling 即可见)执行:QuroToolEngine.execute(call)└─ QuroPluginHost.executePluginTool(name, args) ← suspend 桥接└─ HostToolBridge.executeTool → YourExecutor.execute(ToolArgs)
三个设计要点:
- 自动进工具集
:不需要重启宿主,不需要手动刷新注册表; - 契约是 JSON Schema
: ToolParamSpec(含enum、default、required)被宿主翻译成 标准 function-calling schema,LLM 侧零特殊处理; - 执行体是
suspend:插件可以直接 withContext(Dispatchers.IO)发网络请求、读数据库, 不会阻塞主线程(这是 Android 侧最容易被忽略的一条)。
决定调用率的是 description
宿主的系统提示里有一整章讲插件框架,但真正决定 LLM 会不会调用你的是工具描述:
"快递查询工具" | |
"根据快递单号查询物流轨迹。当用户询问快递到哪了、物流状态、包裹进度时调用。" | |
param("no", STRING, "单号") | |
param("no", STRING, "快递单号,如 SF1234567890") | |
enum 约束有限取值集合 |
七、界面体系
7.1 为什么插件不能写 Activity
插件 APK 没有真实的系统安装记录,startActivity 一个插件里的 Activity 会直接失败。 宿主提供通用承载 Activity解决这个问题:
PluginManagerScreen(插件桌面)│ 单击图标 / 长按「打开界面」/ 详情「打开界面」│ AI: apk_plugin(action="open", surface_id="...")▼PluginSurfaceActivity(宿主通用承载)│ 调用插件的 UiSurfaceExtension.build(activityContext, host)▼插件返回的 View 树(由插件负责构建,可用 WebView 做完整前端)
7.2 交给插件的三个回调
interface SurfaceHost {val pluginId: Stringfunclose() // 关闭当前界面funtoast(message: String) // 宿主 ToastfunrunOnUi(block: () -> Unit) // 切主线程}
build(actCtx, host) | WebView(actCtx)) | |
SurfaceBackHandler.onSurfaceBack() | truefalse = 交宿主关闭 | |
onRelease() | 释放资源 |
7.3 插件桌面(PluginManagerScreen)
启动器形态的插件管理界面,是插件能力的可视化入口:
网格布局 + 搜索 + 长按菜单(打开界面 / 重载 / 卸载 / 详情) 底部 Dock →「导入 APK」直接装 图标取插件 APK 的 launcher 图标(没配则显示首字母色块) 显示 v1.0.0 (1)形式的版本信息
已废弃:早期 JS 演示壳
PluginsScreen已更名LegacyJsPluginRuntimeScreen,打开即空白,不要把它当成插件入口。现役插件桌面就是PluginManagerScreen。
八、安全模型
插件跑在宿主进程内、拥有与宿主完全相同的权限——这本质上等同于「代码注入」。 因此框架的安全边界不是「限制插件能做什么」,而是「保证插件是我信任的代码」:
| 同签名 | ||
| 只放私有目录 | /data/data/<host>/files/plugins/ | |
| 必须是声明了 entry 的 APK | quro.plugin.entry | |
| 原子替换 | .tmp.bak → active,失败回滚 | |
| 卸载即注销 | unregisterAll() |
宿主 Release 证书 SHA-256(插件必须使用同一份 keystore):
D9:5B:1B:EC:57:B9:D5:EE:88:96:05:9C:0F:3C:B5:09:E5:E9:CE:7C:CD:AE:DB:9C:6B:2E:98:49:BA:10:C7:99调试期可用 apk_plugin(action="install", path=..., skip_signature_check=true) 跳过校验,正式分发绝不能跳过。
九、可靠性设计
| 原子替换安装 | .tmp → 旧版改名 .bak → .tmp 改名 active → 删 .bak;任一步失败回滚 | |
| 热重载 | apk_plugin(action="reload", plugin_id=...)unload + load | |
| 卸载即清理 | unloadonDestroy + unregisterAll)→ 删 plugins/<id>/ → 清记录 | |
| 失败降级 | ||
| 原生库自动适配 | lib/<abi>/*.so 到 active/lib/,作为 librarySearchPath 传入 |
热重载会丢状态:
reload=unload+load,插件实例重建。 需要持久化的东西放ctx.putString()或ctx.getFilesDir()。
十、功能介绍
10.1 面向用户:装了插件之后,AI 多了什么能力
| 给 AI 加工具 | AI_TOOL 扩展点 | |
| 完整的插件界面 | UI_SURFACE 扩展点 | |
| 斜杠指令 | /express SF1234567890 | COMMAND 扩展点 |
| 对话卡片 | CHAT_CARD 扩展点 | |
| 跨 App 能力开放 | ACI_CAPABILITY 扩展点 | |
| 新模型 / 新知识源 | MODEL_PROVIDERRAG_SOURCE | |
| 新文件类型支持 | .xyz 文件由插件预览 | FILE_HANDLER |
| 新语音 / 脚本引擎 | SPEECHCODE_RUNTIME |
关键点:这些能力全部由插件提供,宿主代码零改动。
10.2 面向开发者:你能往哪些槽里放东西
AI_TOOL | ||
UI_SURFACE | ||
/斜杠指令 | COMMAND | |
ACI_CAPABILITY | ||
SETTINGSCHEDULE_TASK / FILE_HANDLER | ||
MODEL_PROVIDERRAG_SOURCE / CHANNEL / SPEECH |
10.3 框架给开发者提供的便利
| 声明式 DSL | plugin(ctx) { aiTool(...) { param(...); execute {...} } } |
| 参数宽松解析 | args.int() / args.number() 兜底转换) |
| 宿主能力探测 | ctx.hasHostCapability("llm") |
| 安全的键值存储 | ctx.getString / putString / getBool / putBool |
| 私有文件目录 | ctx.getFilesDir()<host>/files/plugins/<pluginId>/ |
| 日志归集 | ctx.log(tag, msg)QuroPlugin/[<pluginId>],一眼分清是哪个插件 |
| 插件互调 | ctx.callCapability(id, args) |
| AI 兜底调用 | apk_plugin(action="call") |
十一、内置示例插件
仓库内置 6 个可直接编译、可直接安装的示例插件,本身就是框架能力的规格说明:
:plugin-express | AI_TOOL + 1 ACI_CAPABILITY + 1 COMMAND | 最小完整范式 | |
:plugin-devkit | AI_TOOL + 1 ACI_CAPABILITY | dev_base64dev_hashdev_jsondev_regexdev_urldev_uuid) | |
:plugin-zorvweb | AI_TOOL + 1 UI_SURFACE | 旗舰示例 | |
:plugin-todo | AI_TOOL | ||
:plugin-units | AI_TOOL | enum 参数约束) | |
:plugin-sysinfo | AI_TOOL | sys_batterysys_memorysys_storagesys_report) |
旗舰示例:plugin-zorvweb 的 15 个工具
web_open web_nav web_read web_query web_findweb_elements web_console web_script web_wait web_tabsweb_media web_http web_crawl web_info web_search_page
外加 1 个 uiSurface(id = 浏览器,标题「ZorvWeb 浏览器」), 插件内部用 BrowserSurfaceView 构建界面,onRelease 里做 WebView 资源释放——这一个插件就同时演示了 AI 工具族、界面承载、生命周期管理三件事。
十二、方案对比
| ZorvAI APK 插件 | |||
|---|---|---|---|
| 能,且自动进工具集 | |||
| 能(同进程同权限) | |||
| 能(宿主通用 Activity 承载 + 继承宿主主题) | |||
| 签名 APK,可版本管理与热重载 | |||
| 同进程原生,无跨进程开销 | |||
| Kotlin / Java(全生态可用) | |||
| 同签名 + 私有目录 + 清单校验 | |||
| 不需要 |
取舍说明:APK 插件用「同签名」换来了最强的能力与性能, 代价是第三方无法自由发布插件——这是有意的设计选择:插件跑在宿主进程内, 放开签名等于允许任意代码注入。需要开放生态时,正确路径是 ACI_CAPABILITY(走 Binder 语义、跨进程隔离)。
十三、设计原则与边界
13.1 五条设计原则
| 宿主只认识抽象 | ExtensionType |
| 依赖单向向下 | |
| 契约层零实现 | :plugin-contractcompileOnly 语义成立 |
| 能力收敛到单一出口 | |
| 默认安全 |
13.2 边界与限制(明确不做的事)
Activity / Service / Receiver | UI_SURFACESCHEDULE_TASK(周期任务) | |
ACI_CAPABILITY | ||
onCreate 里做重活 | Application.onCreate 同步链路里 | |
reloadunload + load | ctx.putString / getFilesDir() |
13.3 术语表
| 宿主(Host) | :app),负责调度与承载 |
| 契约层(Contract) | :plugin-contract |
| 扩展点(Extension Point) | |
| 收纳槽(Registry) | ExtensionRegistry(ExtensionType, id) 索引的注册表 |
| 表面(Surface) | |
| 桥(Bridge) | HostToolBridgeAciBridge,把插件能力翻译成宿主认识的形态 |
| ACI |
相关文档与源码位置
apk-plugin-arch 分支 docs | ||
| github.com/Quor-a/ZorvAI | ||
plugin-contract/.../extension/ExtensionPoints.kt | ExtensionType + 数据类 | |
plugin-contract/.../dsl/PluginDsl.kt | plugin(ctx) { ... } | |
plugin-engine/ | ||
plugin-zorvwebplugin-devkit · plugin-express |
仓库地址
GitHub:github.com/Quor-a/ZorvAI(主仓库)Gitee:gitee.com/ZorvAI/ZorvAIGitLab:jihulab.com/quor-a-group/ZorvAI下载:Releases | 反馈:Issues
本文档描述 ZorvAI APK 插件框架 v1.0.90 的架构与能力,随代码演进维护。项目开源地址:github.com/Quor-a/ZorvAI