让 AI Agent 直接理解并操作运行中的 Android App,而不是隔着屏幕猜测。
AppActuator 是一个面向开发与测试场景的 Android 工具库。你可以把少量业务方法显式开放给 AI Agent,让它通过 adb + Socket + JSON-RPC 2.0 调用方法、读取状态、等待事件。调试包获得完整能力,release 包默认替换为空实现,并在构建时检查是否剥离干净。
text
AI Agent / MCP Client
│
│ JSON-RPC 2.0 over adb forward
▼
┌──────────────── Android App ────────────────┐
│ AppActuator │
│ 方法注册表 · 事件总线 · 生命周期 · 鉴权加密 │
│ │ │
│ ▼ │
│ 你显式开放的业务对象 │
└─────────────────────────────────────────────┘
为什么用它
假设你想让 Agent 测试一局扫雷。只靠截图和坐标点击,它很难可靠地知道「这一格是否已打开」「还剩多少雷」「游戏何时结束」。AppActuator 让 Agent 在你划定的边界内直接调用 game.sweepCell、读取 game.getGameState,并等待 game.gameOver。
| 你通常遇到的问题 | AppActuator 的做法 |
|---|---|
| 坐标点击容易受 UI 改版影响 | 直接调用稳定的业务方法 |
| 截图只能推测内部状态 | 返回结构化 JSON 数据 |
| 轮询又慢又不可靠 | App 主动发事件,Agent 按需等待 |
| 自建调试服务容易遗留在正式包 | release 默认注入 noop,并自动审计依赖、Manifest 和 DEX |
| 暴露范围难以控制 | 只有显式注解且注册的方法可以被调用 |
它适合调试诊断、业务级自动化测试、AI Coding 联调、教学 Demo 和内部工具。它不用于跨 App 系统自动化、代码注入、热更新或逆向分析,也不替代 UIAutomator/Appium;涉及真实用户界面的端到端验证时,两类工具可以配合使用。
5 分钟接入
前置条件:AGP 8.x+、JDK 17+、minSdk 30+。插件和 Android 库已发布到 Gradle Plugin Portal / Maven Central。
1. 配置仓库与插件
宿主工程的 settings.gradle.kts 需要包含:
kotlin
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
然后在应用模块的 build.gradle.kts 中应用插件:
kotlin
plugins {
id("io.gitee.kuangthree.appactuator") version "0.2.0"
}
appActuator {
auth = "disabled" // disabled | optional | required
autoStart = true // App 进程启动时自动初始化
keepAlive = false // 默认不在后台保持信道
requireShellUid = true // 仅允许 adb shell 读取元数据
}
无需手动添加 library-api、library-full 或 library-noop。插件会按构建变体注入正确实现:debug 默认使用 full,release 默认使用 noop。
2. 开放一个业务对象
kotlin
import android.app.Application
import com.universe_st.appactuator.api.ActuatorMethod
import com.universe_st.appactuator.api.ActuatorTarget
import com.universe_st.appactuator.api.AppActuator
import com.universe_st.appactuator.api.ThreadMode
@ActuatorTarget(name = "game", description = "游戏控制器")
class GameController {
private fun gameRunning(): Boolean = true
@ActuatorMethod(
name = "sweepCell",
condition = "gameRunning",
thread = ThreadMode.MAIN,
)
fun sweepCell(x: Int, y: Int): Map<String, Int> {
val result = mapOf("x" to x, "y" to y)
AppActuator.emit("game.boardChanged", result)
return result
}
}
class DemoApplication : Application() {
override fun onCreate() {
super.onCreate()
AppActuator.register(GameController())
}
}
如果项目还没有自定义 Application,请在 AndroidManifest.xml 中声明:
xml
<application android:name=".DemoApplication" ... />
这里有三个重要边界:
- 只有
@ActuatorMethod标记的方法会暴露;普通方法仍然不可见。 condition指向同类中的无参Boolean方法,返回false时调用会被拒绝;条件方法本身不能再标记@ActuatorMethod。ThreadMode.MAIN用于 UI 操作,BACKGROUND用于耗时任务,默认的CALLER适合快速、无 UI 依赖的方法。
3. 构建并确认服务
powershell
.\gradlew.bat :app:assembleDebug
.\gradlew.bat :app:installDebug
adb shell content query --uri content://com.example.app.actuator/metadata
把 com.example.app 换成应用的 applicationId。正常情况下会看到 status=running、动态端口、协议版本和鉴权档位。如果 App 被 force-stop,先显式启动它;如果设备执行过 adb root,默认的 shell UID 校验会拒绝查询,请先 adb unroot。
4. 从 PC 调用
仓库内的 Python 客户端要求 Python 3.10+:
powershell
python -m pip install -e "client"
python
from appactuator import AppActuatorClient
client = AppActuatorClient(package="com.example.app")
try:
client.connect()
client.handshake()
methods = client.list_methods()
result = client.invoke("game", "sweepCell", {"x": 0, "y": 0})
event = client.wait_event("game.gameOver", timeout_ms=60_000)
finally:
client.close()
list_methods() 支持 targets(只看特定对象,如 client.list_methods(targets=["game.1"]),也接受类名前缀 ["game"] 匹配全部实例)与 no_desc(返回最小结构省 token)参数,详见 skill/app-actuator/SKILL.md。
connect() 会自动完成元数据查询和 adb forward,随后由 handshake() 协商协议;close() 会同时清理连接与转发。多台设备同时连接时,请向客户端传入设备序列号。
让 AI Coding 工具直接使用
仓库提供两种 Agent 接入方式:
- Agent Skill:位于仓库根目录的
skill/文件夹,适合能读取操作指引并运行命令的 Agent。 - MCP Server:适合支持本地 stdio MCP 的 AI Coding 工具。安装命令为
python -m pip install -e "client[mcp]";启用鉴权时使用client[all]。
下面是兼容 mcpServers 格式的最小配置。路径应替换为本机仓库的绝对路径;如果客户端已安装到当前 Python 环境,可以移除 PYTHONPATH。
json
{
"mcpServers": {
"appactuator": {
"command": "python",
"args": ["-m", "appactuator.mcp_server"],
"env": {
"PYTHONPATH": "C:/path/to/app-actuator/client",
"APPACTUATOR_PACKAGE": "com.example.app",
"APPACTUATOR_SERIAL": "emulator-5554"
}
}
}
}
MCP Server 暴露固定的 7 个工具,避免宿主方法动态变化导致工具缓存失效:
| 工具 | 用途 |
|---|---|
actuator_connect |
建立连接;重复调用安全 |
actuator_get_status |
查看运行状态与鉴权档位 |
actuator_list_methods |
获取实时方法清单、参数与不可用原因;支持 targets 过滤与 no_desc 最小结构 |
actuator_invoke |
调用一个宿主方法 |
actuator_list_events |
查看已声明或已触发的事件 |
actuator_wait_event |
等待一次事件 |
actuator_close |
关闭连接并清理 adb forward |
Reasonix、OpenCode 等工具的完整配置示例和环境变量说明见 AI Coding MCP 接入文档。
鉴权与正式包安全
内部调试包也建议使用 required 鉴权。先生成密钥对:
powershell
$env:PYTHONPATH = 'client'
python -m appactuator.genkey --out-dir keys
公钥随构建注入 App,私钥只保留在 PC:
powershell
.\gradlew.bat :app:assembleDebug `
"-PappActuator.auth=required" `
"-PappActuator.publicKeyFile=keys/appactuator_public.pem"
鉴权成功后,业务消息使用 AES-256-GCM 加密;握手使用 RSA 公钥体系。required 模式缺少有效公钥时服务不会启动。PowerShell 中的 -PappActuator.<key>=<value> 参数务必整体加引号。
release 安全不是一条使用建议,而是构建机制:
- 插件默认让 release 变体依赖
library-noop,其公开 API 行为固定为空操作。 - 完整实现使用的 Provider、Service 和
INTERNET权限不会由 noop 引入。 - release 构建自动从依赖图、合并后的 Manifest 和 DEX 三个层面审计残留;发现完整实现即构建失败。
请勿用
-PappActuator.enabled=true将完整实现带入生产 release。该开关只应服务于明确隔离的内部构建。
运行模型与重要限制
- 服务只监听设备回环地址
127.0.0.1,PC 必须通过 adb 转发访问。 - 单个 App 同时只接受一个客户端连接;同一连接内可以并发请求。
listMethods和invoke都会实时检查注册状态、生命周期与自定义条件,不依赖旧缓存。waitEvent只等待请求发出之后的事件,不补发历史事件;需要可靠恢复时应同时提供状态查询方法。- Kotlin 默认参数不受支持,调用方必须显式传入全部参数。
- 同一 target 中的暴露方法不能重名;发现重名时整个 target 注册失败。
- 单条消息、并发请求、事件等待与订阅均有资源上限,默认调用超时为 30 秒。
keepAlive=true会引入specialUse前台服务及相应政策成本,只建议用于内部调试构建。library-full声明INTERNET权限以创建本地 Socket;剥离后的 noop 构建不包含该权限。
协议、安全与并发语义的权威定义在 需求规格,实现追踪和端到端验证记录在 合规矩阵。
跑通扫雷 Demo
仓库自带一个 10×10 Compose 扫雷 Demo。它开放 newGame、getGameState、getCell、sweepCell、toggleFlag 和 game.* 事件,是体验完整链路最快的入口。
powershell
.\gradlew.bat :demo:sweeper:assembleDebug
.\gradlew.bat :demo:sweeper:installDebug
adb shell content query --uri content://com.universe_st.appactuator.demo.sweeper.actuator/metadata
$env:PYTHONPATH = 'client'
python client/examples/sweeper_demo.py `
--package com.universe_st.appactuator.demo.sweeper
需要鉴权时:
powershell
$env:PYTHONPATH = 'client'
python -m appactuator.genkey --out-dir keys
.\gradlew.bat :demo:sweeper:assembleDebug "-PappActuator.auth=required" "-PappActuator.publicKeyFile=keys/appactuator_public.pem"
.\gradlew.bat :demo:sweeper:installDebug
python client/examples/sweeper_demo.py --package com.universe_st.appactuator.demo.sweeper --private-key keys/appactuator_private.pem
项目结构
| 路径 | 职责 |
|---|---|
library-api/ |
稳定公开 API:注解、门面、配置、错误码与 noop 兜底 |
library-full/ |
Provider、TCP Server、JSON-RPC、注册表、事件与加密鉴权 |
library-noop/ |
release 剥离构建使用的空 AAR |
library-lifecycle/ |
可选 AndroidX Lifecycle 适配 |
plugin/ |
变体依赖注入、构建期配置与剥离审计 |
client/ |
Python 客户端、CLI、密钥工具与 MCP Server |
demo/sweeper/ |
Compose 扫雷示例 |
skill/ |
Agent 操作指引 |
完整实现中的 core/ 与 crypto/ 不依赖 android.*,因此核心协议和加密逻辑可以直接在 JVM 上测试。公开错误码由 Android 与 Python 两端同步维护。
从源码构建
需要 JDK 17 和 Android SDK,并在 local.properties 中配置 sdk.dir。
powershell
# Debug 构建:注入完整实现
.\gradlew.bat :demo:sweeper:assembleDebug
# Release 构建:注入 noop,并运行剥离审计
.\gradlew.bat :demo:sweeper:assembleRelease
# Android / Gradle 单元测试
.\gradlew.bat :library-full:testDebugUnitTest :library-lifecycle:testDebugUnitTest :demo:sweeper:testDebugUnitTest :plugin:test
# Python 客户端测试
$env:PYTHONPATH = 'client'
python -m unittest discover -s client/tests -v