wangzan101/Mika:2.7k stars 的安卓本地 AI 助理,把后台虚拟屏玩成了主力
上周我在通勤地铁上,突然想起来一件事:冰箱里那盒牛奶该在周五前吃完。
我打开手机想设个闹钟提醒自己。结果我在手机上点了整整四分钟。 闹钟 App 的图标藏在一个要横向滑动才能看到的第二屏里,滑到之后是一个要滚动才能看到的列表,列表里的「+」是个只有 24dp 的圆形图标,我点了三次没点中,第四次终于按对了 —— 然后弹出一个时间选择器,那玩意儿是个滚轮。
我把手机锁屏,越想越气。
那天晚上我逛 GitHub,撞见一个 2026 年 10 月 29 日才建仓的项目,两天半冲到 2,706 stars。
它叫 Mika。
我点进去看 README 第一句话:
你手机上的 AI 助理:说一句话,它替你操作 App、上网,把事办成。
然后是这一句:
它在一块看不见的后台屏上干活,你照样刷你的手机。
我盯着这句话看了大概十秒钟。
因为那正是我刚才那四分钟里最想要的东西 —— 别占我的屏幕。
一、一个具体的痛点:手机 GUI Agent 都在跟「我的屏幕」抢地盘
先说清楚我想解决什么,因为这是所有手机 Agent 项目绕不开的起点。
大模型现在能看图(vision)也能点屏幕(GUI grounding),所以「让 AI 操作手机」这件事在原理上已经不难了。难的是工程落地,而工程落地的第一道坎不是模型,是物理约束。
你让一个 Agent 去操作手机,只有两种物理现实:
第一种,它就操作你正在用的那块屏。
结果就是:Agent 打开微信找联系人,你的屏幕上微信自己动起来了;Agent 滚列表,你的手指刚要划拉就被抢走;Agent 卡在某个登录框上等你处理,你发现你的手机已经被导航到登录页了,干不了别的。
这个体验是没法接受的。你的手机在 Agent 手里就变成了一个专机。
第二种,它用某种远程控制协议(Appium、uiautomator2 那套)连着一台真机或模拟器。
这是学界和工业界的主流做法,效果确实好 —— 阿里 Open-AutoGLM、字节 UI-TARS、各家的 MobileAgent,全是这个路子。但它有另一道墙:你得有一台专门的手机或一台模拟器。
于是「AI 操作我的手机」这件事,通常发生在别人的设备或者第二台设备上。
Mika 的解法是第三种:它把 Android 的 VirtualDisplay 真正用起来了。
在 Android 上,App 可以创建一个虚拟显示(VirtualDisplay),把它挂到系统里,让另一个 App 的窗口渲染上去。这个能力本来是给投屏、录屏、无障碍测试用的。安卓 14 开始,虚拟屏还有自己的焦点(focus)—— 这意味着虚拟屏上有个输入框正等着打字,你主屏上照样能打字,两边互不干扰。
Mika 就建了这么一块屏:
在一块单独的虚拟屏上打开 App、点、打字、翻页,你照常用手机;想看就点开小画面。
屏幕上有一个可以拖动的小窗(PiP)实时显示 Agent 在干什么,你也可以完全不管它。它在这块屏上装微信、翻聊天记录、找联系人、点发送 —— 你的主屏上一秒钟都没被占过。
这一条就足以让我把整个仓库翻了一遍。
然后我发现它远不止这一招。
二、它是什么:一句话定义 + 几个必须记下来的数字
先把事实摆出来:
| 项目 | 值 |
|---|---|
| 仓库 | wangzan101/Mika |
| Stars | 2,706 |
| Forks | 436 |
| License | Apache-2.0 |
| 主语言 | Kotlin(8.78 MB,1,171 个 .kt 文件) |
| 建仓 | 2026-10-29 |
| 最后推送 | 2026-10-31 |
| 提交数 | 226 |
| 贡献者 | 19 |
| PR | 55 |
| 最新版本 | v0.1.27(2026-10-31 发布,APK 88.7 MB,285 次下载) |
| 话题标签 | accessibility / ai-agent / android / automation / jetpack-compose / kotlin / llm / mobile-agent |
一句话定义:
Mika 是一个完全跑在你自己手机上的 AI 助理。它用自己实现的 ADB 协议驱动一个以 shell 身份运行的「内置手机助手」,在一块后台虚拟屏上用无障碍服务读屏和操作 App,用内置 WebView 浏览器处理网页,用 PRoot 沙箱跑 Python/Node/shell 处理文件;所有会改变外部世界的动作,都必须经过一个唯一的执行入口。
几个数字值得单独强调:
- 代码规模 8.78 MB 的 Kotlin,1,171 个 .kt 文件。这个体量在「两天半涨到 2.7k star」的项目里非常不寻常。它不是几周写出来的 demo,是有历史的工程 —— 只是历史被压缩在了两天半里。
- 两个 Gradle 模块:
core是纯 Kotlin/JVM,不依赖 Android,主循环、工具、上下文、记忆、知识、触发器全在这里,可以在电脑上跑单测;app是安卓宿主,负责界面、前台服务、虚拟屏、无障碍、WebView、沙箱。 - 128 道真机评测题(
eval/tasks/),分成read/system、read/content、read/shopping、write、irreversible、impossible、barrier、web等类别,还有skills、memory专项题集。 - 292 个 core 测试文件 + 248 个 app 测试文件(
core/src/test/与app/src/test/、app/src/androidTest/)。
我做了去重检查:wangzan101、Mika、mika 在已有的 123 篇文章里都没有命中,可以放心写。
三、核心机制之一:内置手机助手,和自己实现的 ADB 协议
先讲清楚一件容易被忽略的事:Android 从来不给你「以 shell 身份执行命令」这个能力给普通 App。
adb shell 是系统级工具,普通 App 拿不到 INTERACT_ACROSS_USERS_FULL 这种权限,也没有 adbd 协议的服务端。
Mika 的做法非常直接:它自己实现了一套 ADB 协议(core/adb/),然后借助安卓系统自带的那个「无线调试」功能 ——
安卓原版的无线调试(Wireless debugging)正是把 adbd 暴露到局域网上,配对一次之后就能连。Mika 借用的是这个已经存在的、官方设计给开发者用的口子。
配对码还能自动读取:Mika 用无障碍服务从系统设置页把配对码读出来,发一条带 RemoteInput 的高优先级通知直接填进去。整套流程在向导页上是这样收尾的:
「无线调试自己关掉了?安卓在换 Wi-Fi、重启后会关掉它。Mika 会在你允许的前提下自己打开;连不上就回到「设置手机操作」重新配对。」
这句「在允许的前提下自己打开」对应的是 WirelessDebuggingAutoOn:它直接往系统设置里写 adb_wifi_enabled = 1,只开不关,不碰 USB 调试和开发者选项。
助手为什么用 shell 身份
因为 shell 身份能拿到普通 App 拿不到的东西:
- 跨用户启动 Activity(
INTERACT_ACROSS_USERS_FULL)—— 这是它能打开「分身 / 工作资料」里那个 App 的前提; - 往虚拟屏上启动 Activity(
am start --display D); - 注入定向按键到指定窗口(
injectTo,只投给焦点窗口); - 读取任务栈(
ITaskStackListener)判断「用户主屏上是不是弹出了什么」。
Mika 还做了接口握手:交接时核对 shell 身份、APK、Binder 所在进程、一次性口令和签名指纹。口令用完即作废,24 小时过期,磁盘上不留能用的口令。
USB 兜底路径
无线调试在很多国产 ROM 上会被阉割。Mika 为此准备了一条完整的备用链路:电脑经 USB 跑一条命令就能启动同一个助手。
1 | adb shell sh /sdcard/Android/data/com.wangzan101.mika/files/usb-start.sh |
这个脚本是 App 自己写到 getExternalFilesDir(null) 里的(App 自己写不用权限,shell 读得到,别的 App 读不到)。脚本里的助手用 setsid 另起会话、放进后台子 shell,父 shell 退出后归 init—— 所以你拔线、adb 会话结束,助手照样活着。
脚本还贴心地做了两件事:只在 id -u 是 2000(shell)时才启动;启动后等 2 秒用 pidof 确认进程活着,再告诉你「正在启动,打开 Mika 确认」。
助手还能把 Mika 拉回来
Mika 被系统杀了怎么办?助手不死(它是 shell 进程),它知道 Mika 的死亡通知(HelperProcess.onAppDied),会等 8 秒(GRACE_MS)—— 如果 Mika 这期间自己回来了就不动手。
真没回来,助手按 Mika 最后交来的「心愿」决定要不要拉起它:
拉的时候先把 Mika 的无障碍服务追加回列表、打开
accessibility_enabled(别的服务一个不动),再照常重新交接。
10 分钟里至多拉 3 次,一拉起来又死的就停手。
这一整套东西合起来的意思是:Mika 不是「装了个 App 就完事」,而是在你的手机上给自己搭了一套常驻的、带权限的运行环境。
四、核心机制之二:唯一执行入口,ActionExecutor
这是我读完整个 architecture.md 之后,觉得最值得抄的一条设计。
Mika 有一条铁律:
所有改变外部世界的动作都经
ActionExecutor(core/action/ActionExecutor.kt)。ToolDispatcher.execute要一个ActionPass(core/tool/ActionPass.kt),它的构造函数是 internal,只有ActionExecutor能造,从编译层面杜绝绕过。
不是靠 code review 保证的,不是靠「大家记得调用」,是靠 Kotlin 的 internal 可见性在编译期堵死的。
然后这个入口里有一个固定顺序,每个来源都一样:
执行顺序固定:守卫 → 锁屏检查 → 不可逆动作检查与同意 → 执行 → 动作后核对 → 记录。
我把 aimed() 的骨架摘出来(源码 core/action/ActionExecutor.kt,下面代码为节选,注释里带「//」的是我加的说明,源码原本没有):
1 | // 之后的关卡、判定都按这次调用的那块屏(到这次调用结束) |
1 | // 操作守卫:重复同一动作、没有进展、反复读、失败太多 |
注意这个 when 分支的顺序:Required(需要用户同意)→ Mismatch(申报的对象对不上)→ Denied(没人能问)→ Unresolved(目标没解析出来)→ OwnApp(要操作 Mika 自己的界面)。
每一个都是明确的失败码,不是「静默跳过」。这个设计的好处是:任何一次「Agent 明明该做但没做」,在运行记录里都有一个可查的原因。
只有花钱才问
审批策略非常克制:
别的一律直接做:发送、发帖、提交、删除、联系别人、改账号或安全设置、往密码框里打字、看图找到却看不出是什么的点、读不出目标的点击,都不问。
只有一件事要问:付款。
而付款本身也不交给 Agent:
付款本身 (判定是 pay) 不经同意,交给用户亲手付 (
hand_to_user,reason=payment)。
这个取舍很聪明。大多数 Agent 框架的做法是「所有不可逆操作都要确认」,结果就是 Agent 每点一下发消息都要弹窗,用户烦到关掉。Mika 只在真正花钱的那一步拦一次,其他全放行。
而且它还区分了「有人在场」和「无人在场」:
渠道、定时、MCP 发起的运行、子任务和无人在场的运行问不到人,一律拒绝 (
approval_unavailable)。
定时任务在询问模式下遇到要花钱的一步,不暂停等人,直接不做,模型照实说哪一步没做成。
全局开关,但不影响正在跑的任务
开发者选项里有一个「自主模式」。开了之后连付款也不问。
设计上有个细节挺讲究:
换开关管之后的所有任务;正在做的任务从询问换成自主,下一步起付款也不再问。
也就是切换立刻生效于下一步,不用等任务结束。但工具和提示词在一个任务内不变。
不过这里有个必须诚实说明的坑,Mika 自己的 roadmap 里写着:
自主时
ConfirmationGate不存在,ConfirmDecision.judge里自主的提前返回排在「不对 Mika 自己动手」(ownApp) 检查之前,所以这层保护在自主模式下也不生效。
也就是说,自主模式下 Agent 可以去操作 Mika 自己的界面。作者的解释是「Agent 操作 Mika 自己的界面也可能是在帮用户办事,以后再定」。这在我看是个真实的、目前没解的风险。
五、核心机制之三:把「像人一样操作」做成可测试的纯逻辑
这是我读源码时最震惊的一段。
Mika 对「操作节奏」这件事做了极其精细的工程化。它不是简单地 sleep 一下,而是把每一个参数都变成独立的纯逻辑类,可以单测、可以用固定随机种子复现。
手势的节奏(core/cap/phone/GesturePacing.kt)
每次落指前停 30–120 毫秒;点按按住 50–120 毫秒,双击每下 40–90、两下之间 80–140 (在系统认双击的时限里); 点元素时落点在中心附近偏一点 (不超过宽高的五分之一、至多 12 像素,窄于 24 像素的方向不偏,总在元素里), 按坐标点至多偏 3 像素。
滑动走弧线:
两点之间走一小段弧线 (偏离不超过距离的 6%、至多 30 像素), 时长在给定值上下浮动 8%, 起步慢:短于 800 毫秒的快滑松手时的速度和以前匀速滑动相当 (甩出去的远近不变), 不短于 800 毫秒的慢拖收尾停稳再抬手、不甩。
「甩出去的远近不变」这种描述,说明作者是在用真人操作录像反复对比。
打字节奏(core/cap/phone/TypingPacing.kt)
拉丁字母每个字 60–170 毫秒,中日韩文字每个字 160–380 毫秒、连着的 1–3 个一起上屏 (像输入法选词), 标点、空格之后偶尔多停 0.3–0.9 秒;字多时几个字一段、至多 40 段,总时长至多 6 秒。
而且它分段落写入:
AndroidScreen.typeText找到输入框后先按节奏一段一段写进越来越长的前缀 (ACTION_SET_TEXT, 对任何原生输入框都管用,中日韩文字照样), 最后一段全文照旧交给typeAndVerify整段写入、读回核对、重试。
写第一段后立刻读回:框不接受半截字就停下改用整段写入。
这解释了我开头那个闹钟滚轮为什么能过 —— 它甚至有专门处理滚轮的工具 phone_set_wheel:
按控件能做什么依次试:滚轮里有输入框 (原版的数字滚轮有) 就把值写进去… 能无障碍「往前 / 往后滚」(原版数字滚轮一下一格) 就读出当前值,按
WheelSteps一批至多 5 下地滚、每批读回重算… 都不行 (App 自己画的滚轮) 就读这一列看得到的几项… 按格数慢拖 (不短于 1 秒,按节奏走时末尾停稳再抬手、不甩,免得转过头)
「慢拖、停稳再抬手、不甩,免得转过头」—— 这是我今天看到的最有人味的一句代码注释。
停留节奏(AppDwell / PageDwell)
同一个网站连着换页不会秒开:
这个网站在最近 10 分钟里的第一页不停留,第二页起停留 4–7 秒,之后每多一页上下限各加 0.6 / 1 秒,最长 12 秒。
限制操作快慢的网站(pace: careful)停得更久:10–16 秒起,最长 30 秒。
对外动作的间隔(core/action/OutwardPacing.kt)
这个类的注释写得非常清楚,而且完全是可以直接抄走的代码:
1 | /** |
逻辑是:
一个网站 (App) 里的第一次不等;之后每次和上一次至少隔 8–20 秒随机取的一段,这个窗口里做得越多,上下限各加一截,都不超过 30 秒;间隔从上一次做完算起,中间读页面、想下一步花掉的时间都算在内,只补不够的部分。
「间隔从上一次做完算起,中间读页面、想下一步的时间都算在内,只补不够的部分」 —— 这句话说明作者想清楚了:Agent 思考再慢也算它自己的节奏的一部分,不需要额外补偿。这个细节一般人是想不到的。
为什么要做这些?
答案写在 knowledge-format.md 里:
pace是这个网站的一条事实,不是给模型的规定:只能写careful, 表示这个网站限制一个访客浏览的快慢 (翻页、点开详情太快或太多会被拦、要验证)。
风控识别的是「机器行为特征」。100 毫秒点 50 次、每一页停留时间一模一样,这在一堆真实用户里非常显眼。Mika 的做法是把节奏做成可配置的策略表,让 Agent 的行为分布更接近真人。
我认为这是这个项目最有价值、也最少被同类项目认真做的部分。
节奏本身也分档
设置里能选「快 / 标准 / 像人」三档:
「快」(
FAST: 动作之间不另停、打字一次写入、不设屏数上限,即以前的做法)、「标准」(STANDARD, 默认)、「像人」(HUMAN: 所有 App 都按慢的那一套停留和打字)。
而且还有屏数上限:
限制操作快慢的 App 一个任务里换到新的一屏至多 24 次;到了之后在 App 里换屏的动作不做,报
page_limit。
网站那边是普通 20 页、careful 12 页。
这个设计我认为值得单独表扬。 很多 Agent 是「能做就做」,Mika 是「知道自己该适可而止,并且明明白白告诉模型「到此为止了,你去问用户」**。这比默默失败或者无限重试高级得多。
六、核心机制之四:读屏的双通道,和「文字识别补读」
一个 GUI Agent 要能操作,前提是看得懂。
Mika 的读屏是双通道:
- 无障碍控件树(
app/a11y/MikaAccessibilityService.kt→ScreenAccessibility.kt)—— 按屏读控件树、在指定屏上做手势。 - OCR 文字识别(
app/cap/phone/a11y/PhoneOcr.kt,ML Kit)—— 控件树读不出来时补。
关键在什么时候决定用 OCR:
每次读屏先按控件树判它够不够用 (
core/uistate/TreeQuality.kt): 有名字的元素够多、控件大多有名字、App 窗口里没有一大块一个字也没有的区域,才算明显够用,不识别文字;拿不准时用随 APK 自带的文字识别读一次,树里没有的字补出三行以上就算缺了一块 (partial); 树缺了一块或读不到 (unusable) 时一定识别文字。
这个判定标准很实在:不是「树里没字就 OCR」,而是「树里有一大片空白才 OCR」。因为 OCR 慢且不准,能用树就用树。
OCR 的结果还要清洗:
补上的字 (
core/uistate/VisibleText.kt) 先去掉认错的行 (置信度低、全是符号、以中文为主的屏上零碎的西文数字串), 每行带 0–999 的位置和点击中心。
「以中文为主的屏上零碎的西文数字串」—— 这是真实的 OCR 误报模式,作者显然踩过。
输入框的可靠性
往输入框打字是这个领域最难的部分之一,因为你不知道打进去了没有。Mika 的做法是打完立刻读回核对:
输入、清空也接受一个点 (坐标,或看图找到的
@vN)… 之后比打字前后屏上识别到的文字核对 (core/uistate/UnseenField.kt): 多出现一次为一致,前后完全没变为没打进去,其余为核实不了;弹着输入法时键盘上的字不算。
「弹着输入法时键盘上的字不算」—— 这个细节太实用了。中文输入法在候选框里显示的候选字,会让「前后对比」失效。
而当焦点根本不在框上时,它选择不猜:
屏上什么都没拿着输入焦点、也没弹输入法时根本不粘,直接报
no_focus… 因为粘了界面别处一动反而会被当成「可能打进去了」。
以及一个很实在的判断:
没弹输入法而前后完全没变,多半是焦点不在框上,报
no_focus请模型先点输入框,不再盲目重粘。
防截屏页面
付款、输密码这类页面禁止截屏,Mika 会请你接管。
遇到这种页面,它明确不尝试,而不是瞎点。
可视化作为补充
配了「看图模型」时,它还能看图找按钮。但作者对看图的成本有清醒认识:
描述界面一次要读整屏、写一大段,比定位慢好几倍:提示词里只让它在看不懂是什么页面时用。
而且看图定位的结果会被标注可信度:
定位准度没验证过的模型要有佐证,但这一屏的树不能用、或缺了一块而要找的不在树里时没东西可对照,框照样交出去,标
unverified, 点完由效果说话。
「点完由效果说话」—— 它不看模型说得准不准,看点下去之后界面变没变。
动作效果的判定
手机上点、手势看不出变化而这一屏的树读不出那一处 —— 树不能用,或缺了一块而点不在任何元素上 —— 种类照旧是
no_effect, 另标treeBlind, 提示改说「看不出变化不等于没点上」, 不说多半没点上。
这个区分太重要了。普通 Agent 会把「我没读出来」直接当成「我点失败了」,然后重试 —— 结果就是在一个已经成功的界面上再点一遍(再点一遍「发送」)。
七、核心机制之五:知识与技能 —— 纯文本的「App 使用手册」
Mika 有一个我觉得非常聪明的设计:它把「怎么用某个 App」写成纯文本 Markdown,放进 APK 里。
仓库里现在有 23 个 App 知识(knowledge/apps/<id>/APP.md)和 18 个网站知识(knowledge/sites/<域名>/SITE.md)。
微信那个文件长这样(core/src/main/resources/knowledge/apps/wechat/APP.md):
1 | --- |
注意 pace: careful 那一行 —— 微信被标成了「限制操作快慢」。这直接决定了 Agent 在微信里的停留时间和打字节奏。
知识的三层划分
| 文件 | 管什么 | 不管什么 |
|---|---|---|
knowledge/apps/<id>/APP.md |
这个 App 是什么、界面怎么组织、有哪些功能 | 行为规定、做事方法 |
knowledge/sites/<域名>/SITE.md |
这个网站是什么、页面和网址怎么组织 | 行为规定、做事方法 |
skills/<id>/SKILL.md |
一类事怎么做(可跨 App / 网站) | 某个行业、某个品类的专门做法 |
而且规定不许写进知识文件:
只写事实。授权、确认、停止条件、「不要编造」「交给用户」这类规定归通用准则和审批层,知识里不写。
讲界面时说「界面」「页面」「入口」「按钮」, 不说「看屏幕」。
现象写成现象。「首页网格里的『外卖』和底部标签里的『外卖』是两个入口,前者进外卖频道」, 而不是「勿点底部标签」。
这种「只描述现象,不下指令」的写法很专业。它把「App 的知识」和「Agent 的行为准则」解耦了 —— 前者是数据,后者是代码里的审批层和守卫。
知识怎么被用到
开局按任务文字匹配;进入某个 App (读过它的界面) 或某个网站 (第一次成功打开或读取) 时补上它的知识正文。每份只给一次,每个任务最多 3 份。
不多给,也不不给。模型自己去查:
| 工具 | 查什么 |
|---|---|
knowledge_search |
App 与网站知识,最多 10 条 |
knowledge_read |
读一份 App 或网站知识,或它的附件 |
skill_search |
技能(内置的 + 用户的个人技能) |
skill_read |
读技能全文,或它的附件 |
还能自己学
App 知识自学… 值得学的有三种:办成了、这个 App 里生效的动作够多;办成了但磕绊 (出错或界面没变的动作两次以上,或返回两次以上); 没办成或只办成一部分… 要回补充改完以后的全文:只有「- 」开头的事实行,至多 12 条;没有值得补的回 NONE。
用户个人技能也有类似流程:起草 → 用户在「记忆与技能」里收下 → 试用 → 办成一次转正式 → 正式技能近 3 次里有 2 次不好也停用。
这个「学 → 试 → 转正 → 退役」的闭环,是我见过的对「AI 越用越懂你」这个承诺最认真的实现。
八、上下文、记忆和「少而精」
对长任务来说,上下文管理比模型能力更决定成败。
Mika 的上下文分四个区(core/agent/ContextBuilder.kt):
区 0 是系统提示词和工具定义,一个任务内只在展开一组工具时变;区 1 是开局上下文,写一次不改;区 2 是历史,只追加;区 3 是本步的结果、画面和运行时状态。
压缩策略:
估算的输入量超过模型上下文的 0.6 (扣掉固定部分) 时… 压缩后降到触发线的一半以下,有回差避免连着压,但不会拖过上下文的 0.9。
怎么压 (不调模型): 先缩短最近 12 轮之前的旧结果… 再把旧轮次折叠成「Earlier steps (compressed)」; 不够再逐档收紧。
压缩不调模型,纯规则。这在成本和延迟上是巨大优势。
还有一条我觉得是整个项目里最值得抄的原则:
永不裁剪: 用户说的话 (任务原话、插话、回答) 从不裁剪;追问、确认、接管那几轮不折叠。
用户在任务进行中插话(「其实我要的是那个蓝色的」),这句话永远不会被压缩掉。
记忆分三类
core/memory/ 里:
三类
MemoryKind—— 事实、偏好、做法。事实和偏好记下即用、可撤销,做法要用户批准。
而且:
只从用户自己说的话里记 (
UserUtterances, 带着屏幕、浏览器页的页面说明时只收用户打的字), 不从屏幕和网页内容里记;密码、验证码、卡号、证件号等由SensitiveFilter挡掉。
记忆加密存在手机本地(app/memory/EncryptedMemoryFile.kt,走 Android Keystore)。
云同步和导出明确不做:
记忆不做云同步与导出;以后要做,需要单独设计加密导出。
这个「知道自己不做什么」的态度,比什么都稀缺。
工具也是分组的
另一个减少 token 的手段 —— 工具两层折叠:
手机、网页、沙箱各是一组,开局只给入口工具,调用后展开它那一组;展开后下一次请求起给整组全文,入口换回全文。
而且展开是有顺序的:一直都在的 → screens → 手机 → 网页 → 沙箱。
「开局只给入口工具」这个设计,直接把初始 prompt 的工具定义量砍掉了大半。
九、内置浏览器、Linux 沙箱,和从电脑派活
内置浏览器(WebView + DevTools 协议)
WebView 跑在 App 自己建的私有虚拟显示上,不占手机屏幕,也不需要内置助手。
操作方式是走 DevTools 协议,真实输入事件:
通过 WebView 的 DevTools 协议操作页面,点击和输入是真实输入事件;连不上时退回注入的页面脚本。
有一个细节我很在意:
从不打开 Runtime 这一组 (
Runtime.enable): 开着它时页面察觉得到挂着调试器,真机上有的网站因此一直重载。
也就是说它为了不被网站检测,宁可放弃一整套调试能力。读脚本只用隔离世界和求值。
登录状态是共享的:
用户自己用的浏览器… 和 Agent 的标签同一个网页配置 (Cookie、网页存储), 在这里登录了 Mika 办事时也是登录的。
还能在网页上直接问它:
网页 (不是起始页) 底下多一条「✦ 问问 Mika,或让它在这页做点什么…」
而且它会借用户的标签页,而不是自己开一个新的:
借到了就当场建好网页这一套、把这个标签借进任务的标签池;它成为任务的当前标签… 但不挂到任务的网页显示上 (它在浏览器页里)、不算每任务和全局上限。
用户一摸页面,Agent 立刻暂停:
手指按下 →
UserBrowser.userTouched→ 此刻正在干活、借来的页面是当前标签… 当场显示暂停… 下一个会改动屏幕的工具之前停在关卡。
Linux 沙箱(PRoot + Alpine 3.22)
每个对话一个工作区,跑 Python、Node、shell。默认断网。
沙箱的三个原生程序随 APK 走(tools/sandbox/README.md):
| 文件 | 是什么 | 许可证 |
|---|---|---|
libproot.so |
PRoot 本体,静态链接 talloc | GPL-2.0-or-later / LGPL-3.0-or-later |
libproot_loader.so |
PRoot 载入器 | GPL-2.0-or-later |
libmikarun.so |
沙箱启动器(超时、断网、清场) | MIT |
Mika 只以子进程方式运行它们,不链接它们 —— 所以 GPL 不会传染到主工程。
而且作者给 PRoot 打了 4 个补丁,修的都是真问题:
proot-0002-seccomp-trap-enosys.patch: 安卓给 App 装的 seccomp 策略对新系统调用 (io_uring、clone3、faccessat2、epoll_pwait2、rseq 等) 发 SIGSYS…PRoot 在这种 SIGSYS 的信号停止处把返回值改成-ENOSYS并取消信号。
proot-0003-arm64-new-path-syscalls.patch:PRoot 5.4.0 的 arm64 调用号表只到 statx (291), 之后新增的调用 PRoot 认不出,平台放行时它们带着没转换的路径直达内核,按宿主路径执行。
这已经不是「做个 demo」的水平了,是真的在 Android 上跑 Linux 用户态。
从电脑派活:CLI 和 MCP
Mika 可以作为一个 MCP 服务器,让电脑上的 AI 工具把活派给手机。
USB 连接(推荐):
1 | adb shell sh /sdcard/Android/data/com.wangzan101.mika/files/connect-computer.sh |
输出:
1 | { |
MCP 工具:
| 工具 | 做什么 |
|---|---|
mika_run_task |
把一件事交给 Mika,返回 task_id |
mika_task_status |
任务状态、最近进度、在等什么 |
mika_wait_task |
等到状态变了或到时间 |
mika_answer |
回答 Mika 的问题 |
mika_steer_task |
向 running 任务追加要求(最多 4000 字符) |
mika_cancel_task |
停下这个任务 |
mika_list_tasks |
电脑交来的最近几个任务 |
CLI 是并列的另一个入口,走普通 HTTP:
1 | mika devices --json |
安全设计的几条硬规矩:
只接受私有网络: 两端都必须是局域网、私有 VPN 网络或本机的地址,公网地址来的连接当场断开。
不接受浏览器的请求: 带
Origin请求头的一律拒绝。
花钱和确认只能在手机上: 电脑交来的任务里,付款、需要你同意的操作永远不会执行,不管「自主模式」开没开。
只碰电脑自己交来的任务: 电脑看不到、也停不了你在手机上发起的任务。
令牌是 32 字节随机数,常量时间比较,同一地址一分钟内错 5 次封一分钟。
还有远程亮屏解锁(默认关闭),凭据加密存在本机,只定向输入到系统锁屏界面,失败一次即暂停。
十、快速上手
手机上(主要路径)
第 1 步:从 Releases 下载 APK(安卓 11+)。
第 2 步:设置 › 模型,选一家服务商填 API Key,或直接用 ChatGPT /xAI/ OpenRouter 账号登录。
内置预设:DeepSeek、通义千问、Kimi、智谱、硅基流动、火山方舟、MiniMax、阶跃星辰、腾讯混元、百度千帆、OpenRouter、OpenCode Go、Anthropic、Gemini、OpenAI、xAI、Mistral、Groq、Ollama、LM Studio。
第 3 步(只有要操作手机 App 才需要):设置 › 设置手机操作,按向导配对一次无线调试。六步:
- 打开开发者选项(连点「版本号」7 次)
- 打开 USB 调试
- 打开 Mika 的无障碍(建议跳过,配对后它会自己开)
- 打开无线调试
- 配对(配对码自动读取,直接填进通知)
- 连上
向导的设计有个好细节:
读到开着就自动打勾、往下走;读到关着就停在那一步,但读开关的三步都有「我已经打开了,下一步」… 所以哪一步都不会卡死。
电脑上
1 | mika connect --device SERIAL --json |
或者用 MCP:
1 | { |
跑评测
1 | adb -s <设备> shell am instrument -w \ |
开发
1 | ./gradlew :core:test :core:cliTest :app:testDebugUnitTest :app:assembleDebug |
沙箱运行环境可复现校验:
1 | python3 tools/sandbox/build-runtime.py --check |
十一、和同类项目放在一起看
| 项目 | 形态 | 和 Mika 的差别 |
|---|---|---|
| Appium / uiautomator2 | PC 端连真机 / 模拟器的自动化框架 | 它们是给人写脚本的;Mika 是给模型用的,且跑在手机本地,不需要 PC 常驻 |
| 阿里 Open-AutoGLM / 字节 UI-TARS | 手机 GUI grounding 模型 + 控制系统 | 它们提供的是能力(能不能点对);Mika 提供的是工程骨架(点了之后谁来记、谁来审、谁来兜底) |
| Claude Computer Use / 各类 CUA | 云端模型 + 远程桌面 | 那是桌面;Mika 是手机,且利用了 Android 特有的 VirtualDisplay 和无障碍这两个能力 |
| Open-AutoGLM 桌面版 / 各类 RPA | 桌面 RPA | 同上,加上 Android 的 shell 权限和虚拟屏是手机独有的 |
| Claude Code / Codex CLI 等终端 Agent | 命令行 | 它们在你的终端里;Mika 在你的手机里,能碰你的 App、你的登录态、你的相册 |
最接近的其实是 Anthropic 自己的 computer use + Android 模拟器方案,但那需要一台模拟器。
Mika 的差异化很清楚:它假设你只有一部手机,你想让它操作的就是这部手机,而且你还得能同时用这部手机。
十二、真实局限
这个项目很新(两天半,226 个提交),作者自己在 roadmap 里列了一堆。我把关键的几条摘出来,加上我自己看到的:
1. 最终核查被关掉了。
开发阶段关掉了最终核查 (用户决定:开发阶段保持关闭)。
TaskRun里reviewFinalAnswer = false: 回答一律记为「未核实」。
这意味着你现在跑出来的所有回答,Mika 自己都不确定是不是对的。学习流程看的是结局判定(Done / Partial / NotPossible),不是核查。
2. 自主模式下,ownApp 保护失效。 前面说过了,Agent 可能去操作 Mika 自己的界面。
3. 中断不续跑。
从不从中间续跑: 屏幕状态回不去。
进程被杀,任务就断了,只能「中断 → 告知 → 用户重新发起」。
4. 真机验证还不充分。
内置助手的各项系统操作、助手被杀或无线调试关闭后的自动恢复、重启后的自动恢复、长时间不用后授权失效的引导、网页渲染进程崩溃、Android 13–15 上精确闹钟授权与撤权、锁屏期间的定时任务,目前主要由单测和设备测试覆盖,还需要在真机上逐项跑过。
5. 剪贴板会漏。
网页类编辑区靠粘贴输入,安卓 12 起主屏会短暂提示「正在访问剪贴板」; 验证码、证件号这类敏感文字粘贴时会在剪贴板里停留约一秒。
6. 安卓版本要求不低。 操作 App 要安卓 12+,推荐安卓 14+(因为虚拟屏要有自己的焦点)。安卓 12/13 上「操作后台屏可能影响主屏焦点」。
7. 沙箱只有 arm64。 非 arm64 设备装得上,但沙箱那一整组工具直接不可用。
8. APK 88.7 MB。 因为内置了 Alpine 根文件系统、PRoot 三个原生库、ML Kit 中文文字识别模型。
9. 迭代快到有点吓人。 从 v0.1.18 到 v0.1.27,10 个版本集中在 2026-10-31 一天内发布。功能确实猛,但也意味着你今天装的版本和明天的可能行为不同。README 里写着「Beta 说明… 升级时可能有不兼容的改动」。
10. 没有云端,纯本地 = 没有服务端兜底。 这是优点也是缺点:没有账号体系,没有跨设备同步,没有远程诊断。你只能靠自己。
十三、适合谁,不适合谁
适合:
- 重度 App 用户,尤其是那些「手机上的操作比电脑还重要」的人 —— 每天在微信、淘宝、小红书、飞书里办几十件事的人。
- 想在自己手机上跑 Agent 又不想被 Agent 霸占手机的人。后台虚拟屏这个点解决的是真痛点。
- 愿意折腾开发者选项的人。Mika 需要无线调试 + USB 调试 + 无障碍,这些在国产 ROM 上都不算无门槛。
- 在意数据不出手机的人。没有 Mika 服务器,API Key 走你自己的服务商,记忆加密存本地。
- 想做手机 Agent 工程的人。那份 338 行的
architecture.md和 1,171 个 Kotlin 文件,是目前公开可见的、把手机 GUI Agent 工程化做得最细的项目之一。
不适合:
- 只有 iPhone 的人。整个项目是 Android 独占(用了 VirtualDisplay、无障碍、shell 权限、PRoot 这些 Android 特有的东西),iOS 上没有对应物,短期内也不会有。
- 要「完全自动、绝不打扰」的人。默认询问模式会在付款时停下来问你;而且真机验证还不充分,复杂任务翻车概率不低。
- 需要开箱即用稳定生产环境的人。226 个提交、两天的迭代节奏、关掉的最终核查、Beta 声明 —— 这是个高速迭代中的工程项目,不是产品。
- 有合规顾虑的企业环境。用自动化操作第三方 App 可能违反其用户协议,作者自己在免责声明里写了「有被限制功能、封号的风险」。这个风险是真实的。
最后
回到我开头那个牛奶。
如果那天晚上我就装上 Mika,我大概会说一句:「明天下班前提醒我冰箱里的牛奶还没喝」。
它会建一块后台屏,打开时钟 App,找到那个藏起来的「+」,用 ACTION_SET_TEXT 往输入框里分段写入 9 点 40 分,读回核对,然后设好。我的手机全程在我手里。
这听起来没什么。但如果你知道过去两年所有 GUI Agent 演示里,没有一个能在 Agent 操作时让你继续用手机,你就会明白这件事有多难得。
Mika 里我最欣赏的不是那个虚拟屏(虽然它是核心),而是它在每一个环节都把「判断」写成了纯逻辑:
- 间隔 8–20 秒是
OutwardPacing里一个LongRange; - 打字 60–170 毫秒是
TypingPacing里一个随机区间; - 「点没点中」是
UnseenField里的一个when; - 「该不该问用户」是
ConfirmDecision.judge里一个when分支。
这些全是可以在电脑上跑单测的纯函数。作者甚至给节奏类做了固定随机种子的构造器。
AI Agent 的可靠性从来不来自更聪明的模型,而来自更刻意的工程。 Mika 用 8.78 MB 的 Kotlin 把这句话写在了屏幕上。
如果你也想看,去 github.com/wangzan101/Mika。记得先看 docs/dev/architecture.md—— 那 338 行,比 README 好看。
本文所有数据来自 2026-11-01 抓取的 GitHub REST API 与仓库源码(wangzan101/Mika,main 分支)。Star 数会变,其他数字以仓库为准。