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(主仓库 · Release / Issues) | github.com/Quor-a/Zorv… |
| Gitee | gitee.com/ZorvAI/Zorv… |
| GitLab(极狐) | jihulab.com/quor-a-grou… |
| 常用入口 | 链接 |
|---|---|
| 最新 Release(免登录下载 APK) | github.com/Quor-a/Zorv… |
| 问题反馈 / 需求建议 | github.com/Quor-a/Zorv… |
开发手册(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 插件框架 |
| 二 | 核心思想(一句话) |
| 三 | 分层架构 |
| 四 | 四个关键机制 |
| 五 | 加载全流程 |
| 六 | AI 工具链 |
| 七 | 界面体系 |
| 八 | 安全模型 |
| 九 | 可靠性设计 |
| 十 | 功能介绍 |
| 十一 | 内置示例插件 |
| 十二 | 方案对比 |
| 十三 | 设计原则与边界 |
一、为什么需要 APK 插件框架
ZorvAI 是一个能力密度很高的 AI 助理:终端、浏览器、ACI、TTS、可视化和 14 类扩展能力都长在宿主里。 传统做法下,每加一个能力都要动宿主源码——改工具注册表、改系统提示、改设置页、重新发版。 这条路径有三个绕不开的成本:
| 成本 | 具体表现 |
|---|---|
| 发布耦合 | 一个小工具也要走完整 App 发版流程(构建 383MB APK、上传、用户下载安装) |
| 源码耦合 | 第三方想在 ZorvAI 里加能力,必须拿到宿主源码并编译整个工程 |
| 能力上限 = 团队上限 | 宿主团队写多少,AI 就有多少能力,无法外部扩展 |
APK 插件的目标:把「给 AI 加能力」从「改宿主」变成「装一个 APK」。
设计目标四条:
- 宿主零改动 —— 宿主不认识任何具体插件,只认识「扩展点」这一个抽象;
- 能力可分发 —— 插件是标准 APK,可签名、可版本管理、可从任意渠道分发;
- AI 原生 —— 插件注册的工具自动进入 LLM 工具集,装完立刻可用,无需重启;
- 可控安全 —— 插件与宿主同进程、同权限,因此必须有明确的信任边界(同签名)。
二、核心思想(一句话)
宿主不认识任何具体插件,只认识「扩展点」这一个抽象。 插件往槽里放实现,宿主负责调度。
// 插件作者写的全部代码,本质上就是「往槽里放东西」
class HelloEntry : PluginEntry {
override fun onCreate(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 | 宿主本体 | 决定「什么时候、用哪个扩展点、给谁看」,接进 AI 工具链与 UI |
| 实现层 | 你的插件 APK | 无 | 只依赖契约层,提供扩展点实现 |
为什么契约层必须是
compileOnly? 若用implementation把契约层打进插件 APK,插件与宿主会各持有一份PluginEntry类的副本。 宿主用自己的 ClassLoader 加载插件时,ClassCastException会立刻出现—— 这是本框架第一大坑,也是新人最常踩的坑。
四、四个关键机制
4.1 扩展点收纳槽(ExtensionRegistry)
框架的灵魂是一个只有 14 个格子的注册表。每个格子对应一类可插入的能力:
| # | ExtensionType | 用途 | 宿主消费方 |
|---|---|---|---|
| 1 | AI_TOOL | 给 LLM 加一个可调用工具 ★ 最常用 | HostToolBridge → QuroToolRegistry |
| 2 | ACI_CAPABILITY | 把能力暴露给其他 App / Agent | AciBridge → ACI 服务端清单 |
| 3 | UI_SURFACE | 插件自带的完整界面(View 树) | PluginSurfaceActivity |
| 4 | CHAT_CARD | 聊天气泡内自定义结构化卡片 | 聊天渲染层 |
| 5 | UI_WIDGET | 气泡内可交互内联组件 | 内联组件渲染 |
| 6 | MODEL_PROVIDER | 新增一种大模型接入 | 模型配置层 |
| 7 | RAG_SOURCE | 新增一种知识库来源 | 知识库层 |
| 8 | COMMAND | 输入框 /斜杠指令 | 输入框解析 |
| 9 | SETTING | 设置页新增配置项 | 设置页 |
| 10 | SCHEDULE_TASK | 新增周期任务执行体 | 定时任务调度 |
| 11 | CHANNEL | 新增一个 IM 接入 | 消息渠道层 |
| 12 | FILE_HANDLER | 新增文件类型的打开/预览 | 文件打开分发 |
| 13 | CODE_RUNTIME | 新增脚本语言引擎 | 脚本执行层 |
| 14 | SPEECH | 新增 TTS / STT 引擎 | 语音层 |
收纳槽的索引键是 (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 提取的 .so
parent = 宿主 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-calling JSON Schema
→ 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 选错工具的概率越低;
- 管理类动作(装/卸/查)不该进入日常工具集,只在需要时由同一入口分发。
| action | 必填参数 | 作用 |
|---|---|---|
status | — | 框架状态(engine_ready 等) |
list | — | 已装插件清单 |
info | plugin_id | 插件明细(含扩展点构成) |
tools | — | 插件贡献的全部 AI 工具 |
surfaces | — | 插件界面清单 |
open | surface_id | 打开插件界面 |
install | path(可选 skip_signature_check) | 从 APK 安装 |
install_builtin | — | 装宿主内置示例插件 |
uninstall | plugin_id | 卸载 |
reload | plugin_id(不传=全部) | 热重载 |
call | name(可选 args) | ★ 直接调用插件 AI 工具 |
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 字段(下一轮 function calling 即可见)
执行:
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 会不会调用你的是工具描述:
| 做法 | 效果 |
|---|---|
❌ "快递查询工具" | AI 不知道什么时候该用 |
✅ "根据快递单号查询物流轨迹。当用户询问快递到哪了、物流状态、包裹进度时调用。" | AI 能把口语映射过来 |
❌ param("no", STRING, "单号") | AI 可能传错格式 |
✅ param("no", STRING, "快递单号,如 SF1234567890") | 有示例,命中率高 |
✅ 用 enum 约束有限取值集合 | 避免 AI 自由发挥传错值 |
七、界面体系
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: String
fun close() // 关闭当前界面
fun toast(message: String) // 宿主 Toast
fun runOnUi(block: () -> Unit) // 切主线程
}
| 回调 | 时机 | 插件该做什么 |
|---|---|---|
build(actCtx, host) | 界面打开 | 返回 View 树(拿到的是 Activity Context,可直接 WebView(actCtx)) |
SurfaceBackHandler.onSurfaceBack() | 返回键/手势 | true = 自己消费(如先退一层内部页面);false = 交宿主关闭 |
onRelease() | 界面关闭 | 释放资源:detach WebView、注销监听、停计时器 |
7.3 插件桌面(PluginManagerScreen)
启动器形态的插件管理界面,是插件能力的可视化入口:
- 网格布局 + 搜索 + 长按菜单(打开界面 / 重载 / 卸载 / 详情)
- 底部 Dock →「导入 APK」直接装
- 图标取插件 APK 的 launcher 图标(没配则显示首字母色块)
- 显示
v1.0.0 (1)形式的版本信息
已废弃:早期 JS 演示壳
PluginsScreen已更名LegacyJsPluginRuntimeScreen,打开即空白, 不要把它当成插件入口。现役插件桌面就是PluginManagerScreen。
八、安全模型
插件跑在宿主进程内、拥有与宿主完全相同的权限——这本质上等同于「代码注入」。 因此框架的安全边界不是「限制插件能做什么」,而是「保证插件是我信任的代码」:
| 边界 | 机制 | 为什么必须 |
|---|---|---|
| 同签名 | 宿主比对 APK 证书 SHA-256 | 插件与宿主同进程同权限;不同签名 = 不可信代码 |
| 只放私有目录 | /data/data/<host>/files/plugins/ | 外置存储可被其他 App 篡改 |
| 必须是声明了 entry 的 APK | 清单解析 quro.plugin.entry | 防止误装普通 APK |
| 原子替换 | .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 | 改完插件立刻生效,不需要重启宿主 App |
| 卸载即清理 | unload(onDestroy + unregisterAll)→ 删 plugins/<id>/ → 清记录 | 不留垃圾、不留悬挂扩展点 |
| 失败降级 | 安装失败给出明确原因(缺 entry / 签名不符 / 写入失败…) | 排错不靠猜 |
| 原生库自动适配 | 安装时按设备 ABI 提取 lib/<abi>/*.so 到 active/lib/,作为 librarySearchPath 传入 | 一个 APK 适配多 ABI 设备 |
热重载会丢状态:
reload=unload+load,插件实例重建。 需要持久化的东西放ctx.putString()或ctx.getFilesDir()。
十、功能介绍
10.1 面向用户:装了插件之后,AI 多了什么能力
| 能力域 | 用户可说的话 | 背后是谁在干活 |
|---|---|---|
| 给 AI 加工具 | 「帮我算一下 base64 解码」「查一下我的快递」 | 插件的 AI_TOOL 扩展点 |
| 完整的插件界面 | 插件桌面点图标打开「ZorvWeb 浏览器」 | 插件的 UI_SURFACE 扩展点 |
| 斜杠指令 | /express SF1234567890 | 插件的 COMMAND 扩展点 |
| 对话卡片 | 聊天里出现插件渲染的结构化卡片 | 插件的 CHAT_CARD 扩展点 |
| 跨 App 能力开放 | 其他 App / 其他 Agent 通过 ACI 调我的能力 | 插件的 ACI_CAPABILITY 扩展点 |
| 新模型 / 新知识源 | 在模型列表里多一个自建服务商 | MODEL_PROVIDER / RAG_SOURCE |
| 新文件类型支持 | 点开 .xyz 文件由插件预览 | FILE_HANDLER |
| 新语音 / 脚本引擎 | 换一个 TTS,或多一种脚本语言 | SPEECH / CODE_RUNTIME |
关键点:这些能力全部由插件提供,宿主代码零改动。
10.2 面向开发者:你能往哪些槽里放东西
| 你想做的 | 用哪个扩展点 | 难度 |
|---|---|---|
| 给 AI 加一个可调用工具 | AI_TOOL | ★ 最常用,10 行代码 |
| 做一个插件自己的完整界面 | UI_SURFACE | ★★ 会写 View / WebView 即可 |
加一条 /斜杠指令 | COMMAND | ★ |
| 把能力暴露给别的 App | ACI_CAPABILITY | ★★ |
| 加配置项 / 定时任务 / 文件处理 | SETTING / SCHEDULE_TASK / FILE_HANDLER | ★★ |
| 接新模型 / 新知识库 / 新渠道 / 新语音 | MODEL_PROVIDER / RAG_SOURCE / CHANNEL / SPEECH | ★★★ |
10.3 框架给开发者提供的便利
| 便利 | 具体体现 |
|---|---|
| 声明式 DSL | plugin(ctx) { aiTool(...) { param(...); execute {...} } },不用手写注册样板 |
| 参数宽松解析 | LLM 把数字传成字符串也能取到(args.int() / args.number() 兜底转换) |
| 宿主能力探测 | ctx.hasHostCapability("llm") 做可选依赖判断,有则增强、无则降级 |
| 安全的键值存储 | ctx.getString / putString / getBool / putBool,底层按插件隔离 |
| 私有文件目录 | ctx.getFilesDir() → <host>/files/plugins/<pluginId>/ |
| 日志归集 | ctx.log(tag, msg) → logcat TAG 为 QuroPlugin/[<pluginId>],一眼分清是哪个插件 |
| 插件互调 | ctx.callCapability(id, args),先找插件能力、再找外部 ACI 能力 |
| AI 兜底调用 | apk_plugin(action="call") 绕过工具集裁剪 |
十一、内置示例插件
仓库内置 6 个可直接编译、可直接安装的示例插件,本身就是框架能力的规格说明:
| 插件 | 显示名 | 注册了哪些扩展点 | 演示了什么 |
|---|---|---|---|
:plugin-express | 快递查询插件 | 1 AI_TOOL + 1 ACI_CAPABILITY + 1 COMMAND | 最小完整范式:一个插件同时贡献 AI 工具、对外能力、斜杠指令 |
:plugin-devkit | 开发工具箱 | 6 AI_TOOL + 1 ACI_CAPABILITY | 多功能插件组织方式(dev_base64 dev_hash dev_json dev_regex dev_url dev_uuid) |
:plugin-zorvweb | ZorvWeb 网页引擎 | 15 AI_TOOL + 1 UI_SURFACE | 旗舰示例:AI 工具 + 完整界面共存,用 WebView 做插件自带前端 |
:plugin-todo | 待办清单 | 5 AI_TOOL | 有状态插件 + 持久化 |
:plugin-units | 单位换算 | 1 AI_TOOL | 极简单工具插件(含 enum 参数约束) |
:plugin-sysinfo | 设备体检 | 4 AI_TOOL | 借宿主 Context 读系统信息(sys_battery sys_memory sys_storage sys_report) |
旗舰示例:plugin-zorvweb 的 15 个工具
web_open web_nav web_read web_query web_find
web_elements web_console web_script web_wait web_tabs
web_media web_http web_crawl web_info web_search_page
外加 1 个 uiSurface(id = 浏览器,标题「ZorvWeb 浏览器」),
插件内部用 BrowserSurfaceView 构建界面,onRelease 里做 WebView 资源释放——
这一个插件就同时演示了 AI 工具族、界面承载、生命周期管理三件事。
十二、方案对比
| 维度 | 系统安装的独立 App | 纯 JS / 脚本插件 | ZorvAI APK 插件 |
|---|---|---|---|
| 能给 AI 加工具 | 不能(除非走 ACI 绕行) | 能 | 能,且自动进工具集 |
| 能访问宿主上下文 | 不能 | 受限 | 能(同进程同权限) |
| 能渲染完整界面 | 自己开 Activity(体验割裂) | 受沙箱限制 | 能(宿主通用 Activity 承载 + 继承宿主主题) |
| 分发形式 | 应用商店 / APK | 脚本文件 | 签名 APK,可版本管理与热重载 |
| 性能 | 跨进程开销 | 解释执行开销 | 同进程原生,无跨进程开销 |
| 语言 | Kotlin / Java | JS | Kotlin / Java(全生态可用) |
| 安全边界 | 系统沙箱 | 沙箱 | 同签名 + 私有目录 + 清单校验 |
| 改宿主源码 | 不需要 | 不需要 | 不需要 |
取舍说明:APK 插件用「同签名」换来了最强的能力与性能,
代价是第三方无法自由发布插件——这是有意的设计选择:插件跑在宿主进程内,
放开签名等于允许任意代码注入。需要开放生态时,正确路径是 ACI_CAPABILITY(走 Binder 语义、跨进程隔离)。
十三、设计原则与边界
13.1 五条设计原则
| 原则 | 落地方式 |
|---|---|
| 宿主只认识抽象 | 宿主不 import 任何插件类,只消费 ExtensionType |
| 依赖单向向下 | 实现层 → 契约层;引擎层 → 契约层;调度层 → 引擎层。禁止反向 |
| 契约层零实现 | :plugin-contract 只有接口与数据类,保证 compileOnly 语义成立 |
| 能力收敛到单一出口 | 14 类能力最终都翻译成「AI 工具」或「ACI 能力」两种形态 |
| 默认安全 | 同签名 + 私有目录 + 清单校验,三条闸门都不提供「宽松模式」(调试开关除外) |
13.2 边界与限制(明确不做的事)
| 限制 | 原因 | 替代方案 |
|---|---|---|
不能写 Activity / Service / Receiver | 插件无系统安装记录 | UI_SURFACE(界面)、SCHEDULE_TASK(周期任务) |
| 不能直接改宿主源码或布局 | 会破坏「宿主零改动」前提 | 只能通过扩展点接入 |
| 不能自由分发(必须同签名) | 同进程 = 代码注入 | 开放生态走 ACI_CAPABILITY |
不能在 onCreate 里做重活 | 在 Application.onCreate 同步链路里 | 惰性执行 / 自己开后台线程 |
| 热重载不保状态 | reload = unload + load | 持久化到 ctx.putString / getFilesDir() |
13.3 术语表
| 术语 | 含义 |
|---|---|
| 宿主(Host) | ZorvAI 主程序(:app),负责调度与承载 |
| 契约层(Contract) | :plugin-contract,插件与宿主之间的唯一接口约定 |
| 扩展点(Extension Point) | 14 种可插入能力槽位之一 |
| 收纳槽(Registry) | ExtensionRegistry,按 (ExtensionType, id) 索引的注册表 |
| 表面(Surface) | 插件提供的完整界面,由宿主通用 Activity 承载 |
| 桥(Bridge) | HostToolBridge / AciBridge,把插件能力翻译成宿主认识的形态 |
| ACI | 跨进程能力调用体系;插件能力可对外发布,外部能力也可镜像成 AI 工具 |
相关文档与源码位置
| 文档 / 源码 | 链接 | 内容 |
|---|---|---|
| 《ZorvAI APK 插件开发手册》 | 本机桌面 / apk-plugin-arch 分支 docs | 从 0 到 1 写插件:DSL 完整参考、工程配置、排错速查、完整示例 |
| 仓库 README | github.com/Quor-a/Zorv… →「APK 级插件框架」 | 面向使用者的能力索引 |
| 扩展点权威定义 | plugin-contract/.../extension/ExtensionPoints.kt | 14 种 ExtensionType + 数据类 |
| DSL 权威定义 | plugin-contract/.../dsl/PluginDsl.kt | plugin(ctx) { ... } 全部 builder |
| 引擎层实现 | plugin-engine/ | 加载 / 安装 / 收纳槽 / 双向桥 |
| 示例插件 | plugin-zorvweb · plugin-devkit · plugin-express | 可直接编译运行的参考实现 |
仓库地址
GitHub:github.com/Quor-a/Zorv…(主仓库) Gitee:gitee.com/ZorvAI/Zorv… GitLab:jihulab.com/quor-a-grou… 下载:Releases | 反馈:Issues
本文档描述 ZorvAI APK 插件框架 v1.0.90 的架构与能力,随代码演进维护。 项目开源地址:github.com/Quor-a/Zorv…