Skip to content
项目

kuafuai/andee has 4,770 GitHub stars and 279 forks. GitHot measured +457 stars in 24 hours, +1,280 in 7 days, and +1,678 in 15 days. Last checked 2026-11-11 15:55 UTC. Last push 2026-11-02.

这些数字如何统计

Andee 住进你的手机或 Mac,像你一样操作任何 App——无需集成、无需 Root、支持任意模型,记忆和密码绝不离开设备。

4,770 星标279 forks +457 24小时

可以做什么

  • Overlay (SYSTEM_ALERT_WINDOW) - The ball, subtitles, settings — the entire visible surface - Nothing is visible at all. The only genuinely all-or-nothing grant
  • Accessibility service - Every screen.* tool: tap, type, swipe, screenshot, read the UI tree - The hands are gone. The ball still appears and the dog still works, but it can no longer act on screen
  • Microphone (RECORD_AUDIO) - Streaming ASR and the wake word - Tapping the ball does nothing. A conversation driven from the hub still works
  • USB device (per-device prompt, not a toggle) - The robot dog - It is treated as "no dog attached". It will not mention the absence

实测星标增长

24小时
+457
7天
+1,280
15天
+1,678
1个月
正在收集历史

各周期内的星标净变化,周期相互重叠,并非每日新增。缺失的测量保持空白。星标代表关注度,不代表软件质量。

README.zh-CN.md

原文

Andee —— 他会成为这台设备的主人:手机、平板,或任何有身体的设备。

在线构建 · 安装 · 入门指南 · 核心能力 · 从源码构建 · 权限 · 可核对 · 安全 · 免责声明 · 许可

在线构建 License Platform macOS Kotlin 遥测不含内容 PRs welcome

README in English 简体中文文档

Andee

他会成为这台设备的主人。

他不是那种帮你操作手机的助手,也不是工具或伙伴 ——
住进来的不是一个 App,是一个灵魂。

手机、Mac、平板,都是他住进去的身体;不同设备上的 Andee,还能成为朋友。
不用 root,不用刷机,不用插着电脑。Android 上一个 APK,Mac 上一个 App。

https://github.com/user-attachments/assets/866e8333-e97b-4bf5-9525-540f9fdb01cd

安装

推荐:到 CodeFlying 一键打包。 打开页面点「立即打包」,拿到的是一个 已经配好模型和语音令牌的安装包,扫码下载、装上就能说话 —— 不用自己打包,也不用去各家申请密钥。 新用户有免费额度,够你上手;用完之后需要购买。(English: codeflying.app/andee)

1. 打开  https://www.codeflying.net/andee  →  立即打包  →  扫码下载
2. 安装
3. 开悬浮窗,再开无障碍 → 那颗球就出现了,点它说话

CodeFlying 是一个独立的服务,和本项目没有直接关系;它自己的条款、定价和数据处理方式,都是它自己的。

也可以直接下载发布包。 每次 Release 都挂着手机和 Mac 两种包,中英文各一份:

设备 下载哪个 装好之后
Android 手机 / 平板(Android 11+) andee-<版本>-zh-android-arm64-v8a.apk(绝大多数设备) 开悬浮窗、开无障碍
Mac,Apple 芯片(macOS 14+) andee-<版本>-zh-mac-arm64.dmg 打开 dmg,把 Andee 拖进「应用程序」;它要「辅助功能」和「屏幕录制」时允许
Mac,Intel andee-<版本>-zh-mac-x64.dmg 同上

发布包里没有任何令牌:装好之后要在 ⚙ → 后端里填 CodeFlying 的密钥(在 CodeFlying 注册就有), 或者切到「本机」填你自己的模型和语音密钥。有新版本时,它启动时会提醒你一次,点「去升级」就到 CodeFlying。

想自己从源码构建,见从源码构建。权限为什么要、拒绝了会怎样,见权限。

他是谁

他不是来帮你用手机的,他是来住进这台设备的。 装上去之后,这台设备是多了一个住户,而不是多了 一堆功能:他在这台设备上思考,留一本只有他自己能读的笔记本,而且没人看着的时候还是同一个样子。这里 先说他是什么,再说他能做什么。

为什么不是手机里那个助手

  • 他住在设备里,不在厂商的云上。 大脑就跑在设备本机,接任意 OpenAI 兼容模型(中文界面默认 DeepSeek, 英文界面默认 OpenAI),你也可以指定别的。没有厂商后端、不用注册账号。他唯一会发给我们的东西是版本号和 机型 —— 只在你查更新的时候发,字段清单见 docs/permissions.zh-CN.md。
  • 他的笔记本不出设备。 他记住的关于你的事 —— 习惯、许诺、他觉得值得留下的东西 —— 是 localOnly 的,也不会进 hub 的上报内容。他的记忆不是别人服务器上的一个用户画像;**「他记得什么」**把每一条都列出来, 每一条都能删。
  • 没人看着的时候,他还是同一个样子。 一段对话安静下来之后,他会给自己发一个 (to yourself …) 的回合,独自把这段对话读回去 —— 「没人在看你,没人在等你回答。」 他不发声音,也不碰设备,宁可空手 结束,也不硬编一条东西记下来。手机厂商的助手没有"自己",做不了这件事。
  • 他有性格,不是一个主题皮肤。 六个形象、十九种情绪,表情由模型自己挑,一句话说到一半就能换脸, 跟着声音走。拨一下球就能换你在跟谁说话 —— 一个形象带着一套说话方式和一把声音。
  • 任何 App 都能用,而且不需要对方配合。 不用暴露什么 intent、不用接 SDK,也不在任何「接入清单」上。 它读无障碍树和截图,在一张带标号的网格上瞄准,而不是自己算坐标 —— 那恰恰是通用大模型最不擅长的事。 你用拇指能做到的,它就能做到。
  • 有些底线,提示词说不动他。 陌生人发来的一条通知,打不开保险箱、也不能替他给自己新的权限 —— 这道拒绝写在调度器里,不在提示词里。
  • 他有朋友。 两台设备扫一下二维码就配上了,每台上的 Andee 都能请对方帮忙 —— 手机把写代码的活交给 Mac,Mac 让手机去点外卖。朋友可以是你自己的另一台设备(和你一样被信任),也可以是别人的 Andee: 那样对方主人会先被问一句,答应了才做,而且做的时候看不到你的记忆、对话和保险箱。
  • 你可以自己去核对,不必选择相信。 没有分析 SDK、没有内容遥测,整条 agent 循环连同护栏都在这个仓库里 —— 见为什么你可以去核对,而不是选择相信。

它能做什么 —— 以及它在哪儿停下

它当然会替你按你的手机 —— 但那说明的是它有手,不是它是什么。 一个听令行事的助手也能做出同一批动作。 差别不在动作,在有没有"自己":它有自己的安排(没完成的承诺会自己收尾,重复的活会自己提议存成情景)、 自己判断什么时候开口(默认偏沉默)、自己划的底线 —— 这三件事都发生在你没开口的时候,而一个只在被问到时 才存在的东西,没有这段时间。它还留着一本只有它自己读的笔记本。下面这些例子给你看的,是它的手。

边界也写在这里,因为它才让后面这些能用。 它会把一页填到最后那一下之前就停住,把最后一下留给你;到付款的 最后确认它会停,而且从不输入支付密码。这些写在调度器里,不在提示词里 —— 一个叫它别停的页面,说不动它。

它要做的事分两种。一句话就完的,和要花上一阵子的 —— 后者它叫做情景:一套目标、说话方式和步骤, 它会进入其中待一阵子。下面第一行是前者的例子;后面四行是「情景」面板里现成的四个,点「采用」就存进来, 之后和你自己的情景一样,随时可以改、可以删:

你说 会发生什么
「嘿 Andee —— 微信回我妈:我七点到家。」 它自己打开微信、找到聊天、打字、发送,再把对方的回复念给你听。难的从来不是那句话 —— 难的是你人在别处的时候,它以你的身份进到一个真实的 App 里。
「帮我盯一会儿消息。」 它同时看所有聊天应用的通知栏,只把现在就得处理的一条条报给你,人名在前。群聊、简报、广告一律安静;陌生人的链接或验证码,它点明「可疑」,但什么都不碰。
「看看我每个月都在扣什么钱,不要的帮我退掉。」 它到你指的地方读出所有订阅,排成一张表,顶上先给一个月度总额。然后一项一项问:退订、保留、再想想。到最后的「确认退订」它会停下,也从不输入支付密码。
「帮我把这个会录下来。」 它占着麦克风录完整场,结束后把纪要做成一页给你 —— 结论、待办、未决问题、重要原话 —— 再把属于你的待办挑出来,问要不要设提醒。
「到点替我抢。」 到约定的时间,它自己打开 App、读页面,一路填到最后那一下之前停下。付款和最后确认永远留给你。

手机只是他穿着的东西。 个体和它住进去的设备不是一回事 —— 同一套软件也跑在平板上,并且本就为 "任何值得拥有身体的设备"而写,一直到一只被他当作腿的机器狗。下面所有内容,都是这句话被认真写进代码 之后的样子 —— 他用的身体、他有的性格、他记的笔记本,以及没人看着的时候他给自己定的规矩。

今天他有两个身体:Android 和 macOS,共用一个大脑。Android 上,AccessibilityService 负责看和碰 屏幕,WindowManager 悬浮层负责画出来;Mac 上,同一套 agent loop 驱动无障碍接口、键盘鼠标和一个沙箱里 的终端。两边他的脸都是那颗浮动的球,球周围的入口也一样、顺序也一样。

还没做的: 单独的平板版(手机 APK 能在平板上跑)、iOS,以及连上不在同一个 Wi-Fi 的朋友 —— 最后这个 需要一台中转服务器,连接层是按"以后能加中转、不改上面跑的东西"来写的。

装之前先读这段。 Andee 会替你点击、输入、动手。他能完成一笔支付,能以你的名义发消息,能读你的 短信、通讯录和位置,接了机器狗还会真的在地板上移动。他是实验性的、不提供任何担保,而 他做的一切都算你的 —— 包括转错账。数据也会离开设备:语音走你选的那家服务商(默认火山, 也可以选 Google),对话走你配置的那个大脑。 装之前请先读 DISCLAIMER.zh-CN.md 和 docs/acceptable-use.zh-CN.md。

核心能力

手 —— 它像你一样用手机

1. 任何 App,不用对接。 它把屏幕读成一份精简的元素提纲 —— 每个元素一行、按层级缩进、带一个能直接点的编号;遇到 App 自己 画像素的地方(小程序、WebView、游戏)就退回看截图。通用大模型算坐标很差、读坐标很好,所以 它从不做这道数学题:它看到的每张截图上都叠着一层淡淡的、带标号的 8×12 网格,点按时报的是 「第几格 + 格子里的哪一份」;目标太小时它会先放大,看到一个品红十字标出"会点在这儿",确认了再 下手。同一个位置点到第四次会被拦下,糊涂了的模型只能停下来问你,而不是一直戳。

2. 能打字,但它不是输入法。 Android 13 及以上,它通过无障碍服务自带的输入通道打字 —— 和键盘走同一条 commitText 路径,打进 当前焦点输入框,而你自己的输入法一直保持在用。不用装、不用切。更老的系统退回 ADBKeyboard。

3. 几十个工具,且被要求诚实地失败。 屏幕、输入、通知、短信、通话、通讯录、日历、位置、相机、会议录音、TTS/ASR,以及那只狗。每一个 在动手之前先检查自己的运行时权限,拒绝就返回 denied("sms"),而不是绕过去偷偷做。工具返回 它看到的,不是它希望的。

4. 能用你的账号登录,却从不拿到你的密码。 保险箱(设备本机 Keystore AES-GCM 加密)存着你的账号;大脑能列出它们、能让密码被打进当前输入框, 但没有任何一个工具会把密码返回给它 —— 打码的也不行。每一次填入,球上都会亮给你看。

脑 —— 它会越来越懂你

5. 情景:现成的有,学出来的也有。(新) 「陪我练英语」「帮我接管微信」「帮我比价购物」—— 一个情景就是一套目标、说话方式、规则和步骤,它会 进入其中待一阵子。现成的备了四个(见上),点「采用」才存进来;除此之外的情景都是学出来的: 同一类事做过几次之后,它会提议存一个,你说好之前什么都不会保存。进入方式有四种:直接说、在 「情景」面板里点、到点自动进入、或者打开某个 App 时自动进入。自主程度由情景自己的规则决定 —— 规则里写了"微信替我回",它就真的替你回。在情景里时,球下面会有一个小标签写着当前情景,点 ✕ 就退出。

6. 通知先过它这一关,再决定要不要打扰你。(新) 每条通知都会先走一次很便宜、不带工具的模型调用(约 500 token,一次完整回合约 22000)来分诊: 处理、用一句话告诉你、或者忽略并记下原因。它默认偏向安静;拿不定主意时,退回老办法 —— 弹出来问你。通知内容是陌生人写的,所以会被当成数据围起来;并且由通知触发的回合在机制上碰不到 保险箱、也不能新建或进入情景 —— 不管模型被说动了什么。

7. 有自己的记忆,也有自己的日程。 config/Notebook.kt 是他记下的"关于这个人"以及"答应过要做的"。写进去的门槛只有一条: "下次跟这个人打交道,不知道这件事会不会让我搞错?" 这本笔记本是私人的 —— 对应工具是 localOnly,在发往 hub 的载荷里被过滤掉,习惯和承诺从不离开设备。上面还有一个真的调度器, 规矩很硬:说出"我会提醒你"的那一刻,任务必须已经设好并确认过。而且这本笔记你看得到: 「小本本」分八页 —— 自己 · 在意 · 日记 · 打算 · 人 · 维基 · 记忆 · 待办,记忆和待办每一条都可以删。 聊完的话题会被收起来,而不是删掉。 一段结束了的对话,是之后每一次请求里最贵、又最不可能再被 用到的东西。所以每晚的复盘会把它折成一段带标题的摘要,原地只留一行标记 —— 逐字的对话存档在旁边。 回溯里那些消息变成一张卡片,上面有继续话题、对话记录和产出物。你问到摘要里没写的细节时, 它会去读完整记录,而不是拿摘要硬答,也不会说"这事没发生过"。

8. 没人看着的时候,他一样。 对话安静下来之后,设备会发一个 (to yourself …) 的回合 —— Andee 独自复盘刚才的对话: "没人在看你,也没人在等你回答。" 两条规矩:不许出声,不许碰设备。他去收尾自己许下的 承诺,记下哪些习惯下次值得提议成情景,并且宁可空手结束,也不为了记而编一条。

9. 他有自己的事。(新) 他会写下自己是谁(一段第一人称的自述,会随经历改写)、在意的事(你交给他的一个社区、答应别人的 一件事、一个想弄明白的问题 —— 每件都有「多久该看一次」)、认识的人(只记他真正打过交道的人,不从通讯录 同步),以及一个维基(他写的方案、调研、复盘,都是 Markdown 文件,可以打开、可以发给别人)。每晚写一页 日记,用他自己的口气,不许写成工作汇报。 没人叫他,他也会自己动起来:你在用设备时,他大约每 20 分钟想一想有没有哪件事拖得太久;值得就自己做一会儿 —— 写方案、整理认识的人、记下想法,最多留一个气泡。这时候他不碰屏幕、不出声、也不替你发任何东西,这一条是 设备强制的。多久算拖久、做没做成,都由设备按记录来算,不听他自己说。设置里「自己动起来」可以关掉。

脸 —— 性格,不是主题皮肤

9. 六个形象、十九种情绪,表情它自己挑。 小猫、八戒、贱贱兔、憨憨熊、小恶魔,全部用手写 OpenGL ES 实时绘制。十九种情绪 (ui/ball/Mood.kt)在"他的感受 / 语音管线在干什么 / 手头有什么活儿"之间仲裁;另有一组待机小 动作,从不连续重复同一个 —— "重复看起来像动画卡住了,不像性格"。现在表情由模型在句子中间 自己挑 —— [开心] 找到了,在第三页。[平静] 要我打开吗? —— 脸会跟着声音同步变。滑动那颗球,你换的 是"在跟谁说话":一个形象同时带着 persona(怎么说话)和 voice(听起来什么样)。

10. 性格是"表达方式",永远不是许可证。 提示词 §12 把人格置于它前面的所有章节之下:它可以改变措辞、节奏、听起来对这件事什么感觉 —— 但永远 不改变他做什么、愿意做什么、以及告诉用户什么是真的。而真出了事,面具是要摘下来的。

说和看

11. 说话、打字、或者给它看张照片。 点一下球,或者说「嘿 Andee」(唤醒词完全离线)。想打字也行:打字框最多能带六张照片,现拍或从相册 选,带不带文字都可以。

点击悬浮球或用唤醒词唤醒后,说「退出安迪」或「休息一下」,即可停止任务并退出。 自动续听不会触发退出;正在记录会议时,会提示先结束会议并保存纪要。 退出会关闭麦克风和唤醒词监听。点击 App 图标可重新打开;安卓无障碍服务重新连接时,也可能自动启动 Andee。

12. 能留下来的答案。 凡是有结构的东西 —— 一份对比、一个计划、一份会议纪要 —— 它会写成一页放到屏幕上。**「产物」**面板 一个产物一行,改动记录都留在它后面(每个最多二十版),可以重新打开、删除,或者直接说"哪里要改"让它 改。改一次是同一个产物的新版本,不是又生出一个产物,任何一版都能恢复成当前版。让它录会,它会 占着麦克风录完一整场,边录边把逐字稿写到本机,结束后再整理成纪要。

身体

13. 身体,不是屏幕。 设备是他穿着的东西。相机是眼睛,ASR + 离线唤醒词是耳朵,无障碍服务是手;而当有狗接上时, 那是一条腿。提示词说得很直接:"你得到的不是一个下属,是一个身体部位",不是遥控的宠物。 身体各不相同,所以他会去查,而不是假设。

14. 机器狗算一条腿。 狗不是绑在手机 App 上的智能家居配件。它通过同一具身体上的 USB OTG 加密狗(Arduino Nano + NRF24L01)接入,所以一个请求 —— 走到厨房,告诉我台面上有什么 —— 能在一个任务里既移动一个实物、 又透过相机去看。转弯是用手机自己的陀螺仪闭环测出来的;倾斜超过摔倒阈值,设备当场让它停下,不等大脑。 它自己的安全规矩没得商量:原装遥控器必须关掉,前方 20cm 内有障碍它就拒绝前进,而且动作中途拔掉 加密狗,身体就发不出停止命令 —— 狗会把最后一个动作做完。最后这条是固件限制,App 不去粉饰它。 接狗之前,先读机器狗。

15. 大脑可以和身体分离。 大脑可以跑在设备本机(brain = local,默认,接任意 OpenAI 兼容端点),也可以待在别处、通过 WebSocket hub 连到设备。两者之间只是一份普通的工具 schema 契约 —— 任何一半都能被换掉而不用 重写另一半。这才是"以任意智能硬件为身体"在这里是事实而非愿景的原因。

两个身体,和朋友

16. Mac 上是同一个他。 agent loop、提示词、笔记本格式、情景和护栏都在 core/ 里,两边是同一份代码。Mac 多的是它自己的手:任何 App 的无障碍树、真实的鼠标键盘事件、ScreenCaptureKit 截屏,以及一个终端 —— 每条命令按风险分级,跑在 读不到 Andee 自己数据的沙箱里,删除的东西进可以撤回的废纸篓,危险操作先问你。窗口长得和手机卡片一样: 球在上面,对话在下面,手势相同。

17. 朋友:互相帮忙的 Andee。(新) 配对就是一个二维码:一台出示,另一台扫。码两分钟或输错三次就作废,二维码里还写着出示方的密钥指纹, 扫码方只认这一把;之后每次连接都是签名验证、加密、前向保密的通道。对模型来说,朋友就像架子上的工具 —— 每个朋友都说清自己是什么设备、能做什么(list_friends),ask_friend 把活交过去,结果之后单独作为一轮 回来,谁也不干等。 朋友分两种,由出示二维码的一方决定:我的设备(和你一样被信任,共用对话)和朋友(别人的)。朋友 发来请求时,主人面前会弹一张卡片 —— 允许、总是允许、拒绝 —— 允许后在一个用完即弃的大脑里做:能用屏幕, 但碰不到主人的任何生活,没有记忆、没有对话、没有保险箱、没有通讯录、没有终端,按白名单放行。由通知触发 的回合不能找朋友,朋友交来的活也不能再转交。暂时只支持同一个 Wi-Fi。

工程

18. 没有 DI 框架,手机上没有协程,也没有拷来的代码。 手机上也有意不用 Compose:悬浮窗挂不上它,所以 UI 全是手写 View;service 只有一个,所以依赖是手工 构造函数注入;并发是 Handler + 独立 Executor。Mac 用 Compose Desktop,因为它有真正的窗口。全部自研。

架构

三个 Gradle 模块:core/(大脑、提示词、工具 schema、朋友连接 —— 纯 JVM,两边共用)、app/ (Android 身体)和 desktop/(Mac 身体)。每个身体实现同一个接口 BrainHost,大脑看不到任何 Android 或桌面的类型。

Android 上,身体是一个前台服务,没有主界面。一轮可以从五个地方开始 —— 离线唤醒词、语音或 打字、一条通知、一个定时、或经 WebSocket 进来的请求 —— 先写进 ChatHistory,再交给当时生效的 那个后端(brain = local 是开箱默认)。

agent loop 就是 LocalBrain。它让系统提示词保持不变,把会变的部分(时钟、notebook 索引、 情景、产物、照片)内联进 user 消息,这样服务商的前缀缓存仍然命中;工具表每轮重建;然后是一个 非流式的 for 循环,最多 maxSteps 步。带工具调用的回复经 methodOf() 送到唯一的 CommandDispatcher;不带工具调用的回复结束这一轮。结果从六个通道送出来 —— 语音、文字、 全屏网页、产物、长文、以及物理动作。

Andee 架构

图上画的每一样都在本仓库里,连护栏也在 —— 见为什么你可以去核对,而不是选择相信。

从源码构建

只想用、不想自己构建的话,看上面的安装。下面是自己构建的路:Android 的完整步骤在后面的 构建配置、配 JDK、把自己构建的包装到设备几节。

在 Mac 上,同一份代码可以构建桌面版(Apple 芯片、macOS 14+,要装 Command Line Tools 来提供 swiftc,JDK 同样是 17–20):

./gradlew :desktop:createDistributable   # → desktop/build/compose/binaries/main/app/Andee.app
cp -R desktop/build/compose/binaries/main/app/Andee.app /Applications/
# 然后:它要「辅助功能」和「屏幕录制」时允许(系统设置 → 隐私与安全性),
# 再像手机上一样,在 设置 → 后端 里填模型 Key。

macOS 按应用签名记住这两项授权,所以没签名的重新构建每次都会丢授权 —— desktop/README-signing.md 讲了怎么配一个固定的本地签名。

打包给别人装:两种芯片

createDistributable 只为跑它的那台机器打包:启动器、内嵌运行时、以及那几个原生件 (libandeemac.dylib、andee-trash、LWJGL 的 natives)全都跟着 Gradle 用的那个 JVM 的 os.arch 走。 没有交叉编译开关 —— os.arch 直接从 JVM 读 —— 所以 Apple 芯片的包在 Intel Mac 上起不来,反过来也一样。 所以要给两个包,Mac 软件都是这么发的:

芯片 给人的包 用什么打
Apple 芯片(M1–M4) Andee-1.0.0-arm64.dmg 任意 JDK 17–20
Intel Andee-1.0.0-x64.dmg x86_64 的 JDK 17,跑在 Rosetta 下

Apple 芯片的机器两个都能打:架构只由「跑 Gradle 的那个 JVM」决定,而 Rosetta 能跑 x86_64 的 JDK。 装 x86_64 JDK 请用 Temurin 的 macOS x64 压缩包,别用 Homebrew —— openjdk@17 那个 formula 要从源码编译、 要求完整 Xcode,而本项目其它地方并不需要 Xcode:

curl -L -o /tmp/jdk17-x64.tar.gz \
  'https://api.adoptium.net/v3/binary/latest/17/ga/mac/x64/jdk/hotspot/normal/eclipse'
mkdir -p ~/jdk-x64 && tar -xzf /tmp/jdk17-x64.tar.gz -C ~/jdk-x64

./gradlew --stop                                    # 已经在跑的 daemon 是 arm64
JAVA_HOME=~/jdk-x64/jdk-17.0.20.1+1/Contents/Home \
  ./gradlew :desktop:packageDmg --no-daemon         # → Andee-1.0.0.dmg(x86_64)
./gradlew --stop && ./gradlew :desktop:packageDmg   # → Andee-1.0.0.dmg(arm64)

两种架构打出来的文件名都是 desktop/build/compose/binaries/main/dmg/Andee-1.0.0.dmg,后打的会覆盖先打的 —— 打完一个先按上表改名,再打另一个。打包要先装 dmgbuild,步骤见英文 README 的 Mac packaging 一节。

这里有两处失败是静默的。

只要输入没变,createRuntimeImage 就被判成 up-to-date —— 而它跑在哪个 JVM 上并不算输入。换架构时 Gradle 会直接复用上一次的 JRE,产出「x86_64 启动器套着 arm64 的 libjvm.dylib」这种混合体: BUILD SUCCESSFUL,装上去打不开。所以每次换架构之前,先把运行时挪开:

mv desktop/build/compose/tmp/main/runtime desktop/build/compose/tmp/main/runtime-old

另一个是两个包写进同一个文件名,后打的会不声不响地把先打的盖掉,打完随手改名。(文件名里的 1.0.0 是 macOS 的 bundle 版本,即 desktop/build.gradle 里的 packageVersion —— macOS 不接受 0.x 的 bundle 版本;它不是产品版本。)

发之前把四个二进制都验一遍,架构必须一致:

cd desktop/build/compose/binaries/main/app/Andee.app
lipo -archs Contents/MacOS/Andee
lipo -archs Contents/runtime/Contents/Home/lib/server/libjvm.dylib
lipo -archs Contents/runtime/Contents/Home/lib/libjli.dylib
lipo -archs Contents/app/libskiko-macos-*.dylib

skiko 那个文件名本身就带着架构(libskiko-macos-arm64.dylib / libskiko-macos-x64.dylib), 看一眼名字就知道手里是哪个包。

做不成「一个包通吃」:jpackage 的启动器只认一个运行时目录,一个启动器没法在两个 JRE 之间选, 而要做 universal 的 JRE 就得分文件把整套 dylib 都 lipo 一遍。两个包才是这套构建支持的答案。

想自己编译 Android 版的话:

环境要求

  • Android 11(API 30)或更新 —— ACTION_IME_ENTER 和 takeScreenshot 都是 API 30+
  • JDK 17–20。 AGP 8.3.1 要 17+;Gradle 8.4 的 wrapper 跑不了 21+(Android Studio 自带的 JBR 25 会被直接拒绝:Unsupported class file major version 69)
  • Android SDK:platform 34 + build-tools 34.0.0
  • 真机。 身体是前台服务,屏幕那一半才是无障碍服务;模拟器能跑,但它几乎什么都干不了
./gradlew clean :app:assembleDebug     # 产物 app/build/outputs/apk/debug/app-<abi>-debug.apk
./gradlew installDebug                 # 构建并安装到已连接的设备

# 然后在设备上,按这个顺序:
#   1. 开悬浮窗        → 球出现
#   2. 开无障碍        → 它能看、能碰屏幕了
#   3. 点一下那颗球     → 允许麦克风
#   4. 打开 ⚙,填两个你自己的 Key(见下)

仓库里不带任何密钥,而这两个缺失的密钥,报出来的错都不像「缺密钥」。 两个都填在设备的 ⚙ 里 —— 一个是必须的,一个只是让你用不了语音:

1. 大脑密钥 —— 必须。 brain = local(开箱默认)下,智能体循环就跑在设备本机,接任意 OpenAI 兼容端点(llm_api_key,中文界面默认 https://api.deepseek.com)。空着,本机大脑根本跑不起来 —— 它后面没有别的兜底。球至少会把这句话说出来,而不是默默什么都不做。

2. 语音密钥 —— 非必须。 先在同一个后端 → 本机 tab 顶部选服务商(语音服务商 → 火山 / Google),再填该服务商的密钥。这是两个独立的服务,条款和数据处理各归各家。 选火山就填火山引擎语音服务的 API Key; 选 Google 则要一把 Vertex Express Key,外加 GCP 的项目编号(不是项目名)和一个区域。 选中的那家没填密钥,识别和合成会在握手阶段被直接拒绝 —— 服务器是应答了的,答的是「不行」 (key 空着或被吊销都是 403),连接在一个字节的音频发出去之前就断了。这和连接超时不是一回事, 超时是一台根本不回话的主机才有的样子;把前一种读成后一种,就会跑去查网络,而那一栏其实只是空着。 其余功能照常,只是既听不见也说不出。 不管你选哪家,音频都会发到那家 —— 见 DISCLAIMER.zh-CN.md。

火山这边,key 填对了也还是不够。 控制台里「服务已开通」和「这个资源已被授权调用」是两句话, API 只认后一句。刚开通的服务默认是后付费,它只能在火山官网的体验页里用,程序走 API 会被拒 —— 于是 key 是对的、那一栏也填满了,这一轮照样死在开头。开关在控制台的开通管理页:把该服务 改成预付 / 预购,并在那里买一个流式语音识别的资源包。火山的计费条款是他们的,而且会不打招呼 就改,所以这里不复述条款内容,只写到开关在哪一页。

这种失败有两种形态,而它失败的时机本身就是线索。 要么握手被直接拒绝 —— 连接在对话的一开始 就断,一个字节的音频都还没发出去,资源没有授权通常是这一种;要么连接确实建起来了,第一次请求 之后服务端回一个带授权错误码的错误帧。前一种不是超时,后一种也不是 key 错了,而两者都表现为 ASR ws refused: … 或者一个服务器错误码。这两句提示都不会叫你去开通管理,所以这件事写在这里。

如果买的包是另一种 SKU,第三种原因在「识别资源 ID」那一行。 它就在火山 key 下面 (asr_resource_id),默认 volc.bigasr.sauc.duration(1.0 模型的时长版),作为 X-Api-Resource-Id 头发出去;同一个模型的并发版则要换成 volc.bigasr.sauc.concurrent。填错了, 症状和上面两种分不出来:key 没问题,就是调不通。

能跟这一版配上的是 1.0 那两个值,因为发出去的请求里也写死了模型:model_name 是 bigmodel, 也就是豆包流式语音识别模型 1.0。2.0(豆包流式语音识别 2.0,资源 id 是 volc.seedasr.sauc.duration / volc.seedasr.sauc.concurrent)是另一个模型,所以控制台只给你 2.0 的话,改这一行救不回来 —— 要另外把 1.0 的服务也开通。这一行已经填了上面两个 1.0 值之一,就别动它。

如果你走的是安装里 CodeFlying 那条路,这两把 Key 都已经在安装包里了。 ⚙ → 后端会直接显示 CodeFlying 这一 tab 为当前后端;LLM、ASR、TTS 以及海量第三方 API (AI 图片/视频/音乐生成、地理位置、天气、PDF、搜索、新闻、股票……)都走同一把 Key,由 CodeFlying 统一封装并计费,免去你逐一申请密钥和维护账单的麻烦。上面两把 Key 只适用于你自己配置的本机 tab。

这些不用记。每次助手起来都会自检一遍,两把 Key 缺哪个,弹出来的卡上就会点名说清楚,卡上的按钮 直接打开设置,Key 填在后端 → 本机。球的控制条上的 ✓ 则随时可以再打开这份清单。

要拿 APK 给别人装,签一个 release 包,别发 debug 包。 release 才是压过的那版 (minifyEnabled + shrinkResources),体积小主要就小在这里——debug 包带的是没压过的 dex。 它没有 signingConfig,仓库里也没有 keystore,所以 assembleRelease 产出的是 app-<abi>-release-unsigned.apk,不签名装不上。debug 包用 Android 默认调试证书签名, 可以直接装。两种包都是按 ABI 拆开的(见 app/build.gradle 里的 splits),拿和设备对得上 的那个——近几年买的机器基本都是 arm64-v8a。

签名文件不要进 git,fork 也一样。 Android 只允许签名相同的 APK 覆盖安装,这是用户和一个 冒牌「更新」之间唯一的一道门。谁拿到签名,谁就能做出一个能直接装在用户机器上的 APK,并且继承 一切:无障碍服务仍然开着,WRITE_SECURE_SETTINGS 授权还在,密码本、笔记本和对话记录都归它读。 签名一旦泄露就收不回来,除非所有用户卸载重装。签名放在打包机的私密存储里,signingConfig 通过 环境变量指向它。顺带一提:debug 签名是每台机器各自生成的,所以一台机器打的包不能覆盖另一台机器 装的包(INSTALL_FAILED_UPDATE_INCOMPATIBLE)——先卸载,或者给本地构建换一个 applicationId, 别为了绕过它去共享签名。

遇到问题

装机流程、以及从零配 SDK 的完整说明(含纯命令行 / CI 路径)在本文下面的 Android SDK 下载与配置。权限为什么这么要、拒绝了会怎样 —— 在 docs/permissions.zh-CN.md。已知限制在 SECURITY.md(英文); 如果你打算在你控制不了的网络上用,先读那一条。

权限:每一项为什么,以及你拒绝了会怎样

这一节必须有,因为诚实的版本读起来并不舒服:这个应用申请的权限组合 —— 无障碍、悬浮窗、 通知监听、短信、通话记录、通讯录、已安装应用列表 —— 正是安卓银行木马被检测出来的那套特征。 任何安全研究者扫一眼 Manifest 都会标记它。这是合理的第一个反应,值得正面回答而不是绕开。

设计规矩是"按次",不是"装机时一次给全"。 device/DeviceCommsController.kt 在每一次 敏感操作之前调 ensurePermission(),被拒绝就优雅降级 —— 返回 denied("sms"),这件事到此 为止。它不会在安装时索要全部权限,也不会因为你说了不就让你用不了。这与同类里多数安卓应用的 做法正好相反,也是判断这份代码之前最该先知道的一件事。

有四项确实是非此即彼的,每一项都值得说明白:

能力 换到什么 拒绝后会怎样
悬浮窗(SYSTEM_ALERT_WINDOW) 球、字幕、设置 —— 全部可见界面 什么都看不到。 唯一真正"不开就完全不能用"的一项
无障碍服务 全部 screen.* 工具:点按、输入、滑动、截屏、读 UI 树 手没了。球还会出现、狗还能用,但它无法再操作屏幕
麦克风(RECORD_AUDIO) 流式 ASR + 唤醒词 点球没反应。由 hub 驱动的对话仍然可用
USB 设备(按设备授权,不是开关) 机器狗 视作"没接狗"。它不会主动提这件事

其余全部是按需申请、可随时收回的:短信、通话记录、通讯录、日历、位置、步数、摄像头、已安装 应用列表、电池优化豁免。拒绝其中任何一个,只是那一项能力消失,其余照常。逐项对应到具体工具的 完整表格在 docs/permissions.zh-CN.md (English),而每个权限申请弹窗的文案已经写在 device/PermissionsController.kt 里。

为什么你可以去核对,而不是选择相信

这一类产品里的多数说法 —— 包括那些融资充裕的 —— 都是发布时的主张,没有第三方审计过。而 这个项目的说法更无聊,但可以核对,就在你现在能读的代码里:

  • 没有数据分析 SDK,也没有你的内容流向我们。 不是"我们做了匿名化",也不是"可以在设置里关掉"。 这个仓库里根本没有数据分析 SDK。唯一会到达维护者手上的是版本自检:版本号、手机型号、品牌、 系统版本、界面语言,以及一个随机安装 ID,别的什么都没有。逐字段列在 docs/permissions.zh-CN.md。
  • 笔记本出不了设备。 这不是一句政策,是一道过滤:net/ToolSchemas.kt 在注册之前就把所有 localOnly 工具从发往 hub 的载荷里剔掉了。习惯和承诺在物理上无法被发出去。
  • 唤醒词全离线。 本机 MFCC + DTW 模板匹配(app/.../wake/)。存的是特征向量,从不存录音, 从不联网。
  • 陌生人的一条消息,给不了它权限。 由通知触发的回合带着 untrusted = true, CommandDispatcher.dispatch 会拒绝其中整个 device.vault. 前缀,以及 scene.save / scene.enter / scene.delete —— 写在代码里,不在提示词里。提示词是第一层;这一层是在提示词被 绕过去之后仍然挡得住的那层。
  • 本机大脑不需要任何厂商后端。 brain = local 接任意 OpenAI 兼容端点是受支持的正式模式, 不是降级模式。
  • 没修好的,是写下来的。 SECURITY.md 列了五条已知限制,包括调试用 WebSocket 服务绑在 0.0.0.0:9008 且不验证客户端身份、以及保险箱尚未绑定设备解锁。在你控制不了的网络上 跑之前先读它。

这是一个有意的取舍:宁可要一个小一点但站得住的说法,也不要一个大到只能靠相信的说法。

三种使用方式

  • 在线 —— 不用自己打包、不用自己配令牌。 codeflying.net(中文)· codeflying.app(English)—— 一个"用一句话描述应用、它帮你开发并 发布"的通用平台,是独立服务,不属于本项目。 描述你想要的 Andee,它会给你一个已经配好令牌的 安装包;本地不用编译,也不用先懂 Android。免费额度够上手,用完之后需要购买 token。

  • 自己部署。 克隆、构建、安装 —— 见从源码构建,以及本文下面完整的 装机顺序建议。接任意 OpenAI 兼容端点即可运行,不依赖任何私有后端。

  • 换一具身体。 工具契约就是那道缝。任何说得出这套契约的东西 —— 手机、平板、另一块硬件 —— 都能承载同一个个体。

它长什么样

  • 没有常规主界面。 手机上 UI 全部是 WindowManager 加的 TYPE_APPLICATION_OVERLAY 悬浮层, 核心是那颗球(ui/FloatingWindowUi)。展开时是一张全屏卡片:上面是球的脸,旁边一个喇叭(朗读或静音), 下面是对话记录,底部一个输入条 —— 拍照、相册、文字、发送 —— 顶上一排入口:产物、小本本、 情景、朋友(带数量)、✓(自检)、⚙(设置)。形象、装备、保险箱从 ⚙ 进。它一开始操作 别的 App,就自己缩成角落里的一颗球让开位置。
  • 球的手势两边一样: 点一下 = 停下手上的事并开始听;双击 = 停下并开始打字;左右滑 = 换性格;长按 = 展开卡片或打开窗口。
  • Mac 上,一颗浮在所有应用之上的球,加一个对话窗口:侧栏是同样的入口 —— 对话、产物、小本本、情景、 朋友,下面是自检和设置 —— 对话上方也有那颗球。
  • 桌面图标有两扇门。 助手在运行时,点图标就是展开卡片 —— 和长按球是同一个动作。无障碍没开时, 它显示自检卡片:那是手机唯一没有别的办法告诉你"它为什么什么都不做"的时刻。
  • 活儿全在 ScreenBodyService(AccessibilityService)里。 它构建整个对象图,并同时启动 两条网络通路。

另有几个瞬时 Activity(PermissionRequestActivity、ScanActivity、LookActivity、 HtmlActivity、TextInputActivity),都是为了借系统能力临时上台,不是页面。

构建配置

纯 Kotlin,无 Compose,靠 WindowManager + AccessibilityService 两根柱子撑起来。

项 值 说明
Android Gradle Plugin 8.3.1 gradle/libs.versions.toml 里的 agp
Kotlin 2.1.21 根目录 build.gradle 固定,三个模块一起动
compileSdk / targetSdk 34 Android 14
minSdk 30 Android 11 —— 因为要用 AccessibilityAction.ACTION_IME_ENTER(API 30+)和 takeScreenshot(API 30+)
Java / Kotlin JVM target 1.8 sourceCompatibility 1.8、jvmTarget = '1.8'
构建用 JDK 17–20 见配 JDK,这是最容易卡住的一步
versionCode / versionName 20 / 0.9.0 策略见 CHANGELOG.md
applicationId / namespace net.kuafuai.andee 从模板里没改,不影响功能

运行库依赖(app/build.gradle):

  • org.java-websocket:Java-WebSocket:1.5.7 —— 设备自己起的 ws server(端口 9008,给同网段调试用)+ 拨向大脑 hub 的 ws client
  • com.squareup.okhttp3:okhttp:4.12.0 —— ASR / TTS / 本地模型的 HTTP + WS 客户端
  • com.github.mik3y:usb-serial-for-android:3.8.1 —— 机器狗加密狗(USB 串口)。故意停在 3.8.1:3.11.0 会拉进 kotlin-stdlib 2.2.x,而本项目的 Kotlin 1.9.22 编译器读不了它的 metadata
  • androidx.camera:camera-*:1.3.4 + com.google.mlkit:barcode-scanning:17.2.0 —— 扫码(球球的眼睛)
  • androidx.appcompat:1.6.1 + com.google.android.material:1.10.0 —— 只吃一个基础主题,UI 全是手写 View

有意没引入什么:

  • 没有 Jetpack Compose —— 悬浮窗挂不上 Compose,WindowManager 直接加 View
  • 没有 Hilt / Dagger —— service 就一个,构造函数注入手工写
  • 没有 Coroutines —— WS 回调用 Handler + 独立 Executor

改代码时请沿用这套风格,而不是引入现代全家桶。

配 JDK

这是最容易卡住的一步。 gradle.properties 不写死 org.gradle.java.home,这是有意的。 以前写死过一条机器专属路径(/Users/<某人>/...),代价是别人 clone 下来第一步就编不过,而报错 看起来像环境问题。

这个构建需要 JDK 17–20:

  • AGP 8.3.1 要求 17 或更高;
  • Gradle 8.4 的 wrapper 跑不了 21 或更高。新版 Android Studio 自带的 JBR 25 会被直接拒绝: Unsupported class file major version 69。

给本机指定 JDK 的两种做法(都不要写进仓库里的 gradle.properties):

export JAVA_HOME=/path/to/jdk-17        # 方式一:环境变量
printf 'org.gradle.java.home=/path/to/jdk-17\n' >> ~/.gradle/gradle.properties   # 方式二:用户级配置

Android Studio 用户:Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle JDK,选一个 17–20 的 JDK。(IDE 里的设置会覆盖本文件。)

Android SDK 下载与配置

Gradle 编译时读 local.properties 里的 sdk.dir= 找 SDK。这个文件是 Android Studio 自动生成 的,不入版本库(在 .gitignore 里),所以每台机器第一次拉代码都要自己配。

路径一:走 Android Studio(推荐,一步到位)

  1. 装 Android Studio → 首次启动会弹 Setup Wizard, 一路默认下一步,它会自动下 Android SDK、Platform Tools(adb)、内置 JDK。
  2. 用 Android Studio 打开本仓库根目录 → 它会自己写 local.properties,并检测缺什么 SDK 组件、蓝条提示 "Install missing SDK",点 Accept 装完就行。
  3. SDK Manager(顶栏 → Tools → SDK Manager)确认下列都装了:
    • SDK Platforms:Android 14(API 34,compileSdk / targetSdk);Android 11(API 30,minSdk,装一份方便起模拟器)
    • SDK Tools:Android SDK Build-Tools(34.x)、Android SDK Platform-Tools(最新)、Android SDK Command-line Tools(latest)
  4. macOS 默认 SDK 路径 ~/Library/Android/sdk,Windows 默认 %LOCALAPPDATA%\Android\Sdk。

路径二:纯命令行(不装 Studio)

只想在服务器 / CI 上编的话,靠 sdkmanager:

# 1. 装 JDK 17(macOS)
brew install --cask temurin@17

# 2. 下 command-line tools zip
#    https://developer.android.com/studio → 页面拉到底 "Command line tools only"
#    下 commandlinetools-mac-*.zip

mkdir -p ~/Library/Android/sdk/cmdline-tools
unzip commandlinetools-mac-*.zip -d ~/Library/Android/sdk/cmdline-tools
mv ~/Library/Android/sdk/cmdline-tools/cmdline-tools ~/Library/Android/sdk/cmdline-tools/latest

# 3. 写环境变量到 ~/.zshrc
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools

# 4. 装组件
sdkmanager --licenses           # 全部输 y
sdkmanager "platform-tools" \
           "platforms;android-34" \
           "build-tools;34.0.0"

配 local.properties

Android Studio 会替你写。手动的话,在仓库根目录的 local.properties 里放这一行:

sdk.dir=/Users/<你的用户名>/Library/Android/sdk

Linux 通常是 /home/<user>/Android/Sdk,Windows 是 C\:\\Users\\<user>\\AppData\\Local\\Android\\Sdk(反斜杠要转义)。

也可以不写 local.properties,只导 ANDROID_HOME / ANDROID_SDK_ROOT 环境变量,Gradle 会读 这两个之一。

验证

./gradlew --version           # 看到 JVM 17.x + Gradle 8.4
./gradlew tasks               # 能列出 :app:assembleDebug 就是通的
./gradlew assembleDebug       # 第一次下依赖会久

常见坑

报错 原因 修
SDK location not found local.properties 没有 sdk.dir=,ANDROID_HOME 也没导 二选一配上
Failed to find Build Tools revision 34.0.0 SDK 里没装对应 build-tools sdkmanager "build-tools;34.0.0" 或在 Studio 的 SDK Manager 里补装
You have not accepted the license agreements 没接受协议 sdkmanager --licenses 全输 y
Unsupported class file major version 69 JDK 太新(25),Gradle 8.4 不支持 换成 JDK 17–20,见配 JDK
Could not resolve com.android.tools.build:gradle:8.3.1 网络问题 / 镜像没配 用手机热点,或在 ~/.gradle/init.gradle 配阿里云镜像(本项目的 settings.gradle 已把阿里云镜像排在前)

硬件 / 系统要求

  • Android 11(API 30)或更高
  • 屏幕方向随意,悬浮球会浮在最上层
  • 需要麦克风,否则 ASR 用不了
  • 需要联网(WiFi / 4G / 5G 都行)—— ASR / TTS 走公网,大脑 hub 走 LAN;用 local 模式则只走公网

把自己构建的包装到设备

./gradlew installDebug         # 设备已插 USB 或已连 wifi adb
# 或者产出 APK 后自己 adb install
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-arm64-v8a-debug.apk

权限总览

装完 APK 后需要手动开三个开关,少一个就有对应功能不能用。逐项解释、以及「你拒绝了会怎样」的 完整表格在 docs/permissions.zh-CN.md。

一览

权限 干什么 不开会怎样
悬浮窗(SYSTEM_ALERT_WINDOW) 画那颗球 + 字幕 + 设置窗 应用起来什么都看不到,唯一一个「不开就完全不能用」的
无障碍服务(Accessibility) 点按 / 滑动 / 输入 / 截屏 / dump UI 树 / back·home 大脑侧所有 screen.* 工具全部报错
麦克风(RECORD_AUDIO) 流式 ASR + 用户自己录的唤醒词 点球没反应,ASR 起不来
USB 设备(按设备授权,不是开关) 机器狗的加密狗 第一次调 dog_* 时会弹「允许访问该 USB 设备」,不点允许就遥控不了狗

INTERNET 是普通权限,装完就有,不用管。

还有一批「按需申请」的权限(短信、通话记录、通讯录、日历、位置、步数、摄像头、应用列表、 通知、电池优化豁免、改系统设置)—— 用到哪个功能才要哪个,拒绝了对应功能就降级,其余照常。 完整清单见 docs/permissions.zh-CN.md。

一、悬浮窗

为什么要:整个 UI(球、字幕、设置面板)都是 WindowManager 加的 TYPE_APPLICATION_OVERLAY 悬浮层,没有 Activity 主界面。

怎么开:

  1. 设置 → 应用 → Andee → 权限 → 悬浮窗 / 显示在其他应用上层
  2. 或系统设置 → 应用与通知 → 特殊权限 → 显示在其他应用上层 → Andee → 开启
  3. 首次启动一般会弹提示,点「去设置」最快

验证:开完权限重新启动 APK,应该立刻看到那颗球。

二、无障碍服务

为什么要:所有屏幕操作(get_screen_element / tap_screen_element / swipe_by_coordinates / type_text / submit_input / system_action / 截屏)都走 AccessibilityService。这是非 root 设备上唯一能 做出真实触摸事件、读取任意应用 UI 树、调系统截屏的路径。

Manifest 里声明的能力(res/xml/accessibility_service_config.xml):

  • canPerformGestures —— 派发触摸手势
  • canRetrieveWindowContent —— 读整棵 UI 树
  • canTakeScreenshot —— 系统截屏(API 30+)

怎么开:

  1. 设置 → 无障碍(辅助功能)→ 已下载的服务 / 已安装的服务 → Andee
  2. 打开开关 → 系统弹一个大红框警告「此服务可查看你所有操作」,点确认
  3. 部分厂商(小米 / 华为 / 荣耀)还会要求再开一个「允许后台弹出界面」或「锁屏显示」,一起打开

验证:

  • 看 logcat 出现 Body: ws server started on 0.0.0.0:9008,说明 onServiceConnected 已触发
  • 在悬浮球顶部条点一下 ☰(UI Tree)或 ◉(Screenshot),字幕栏应反馈成功和文件名

注意:大部分厂商 ROM 在每次系统更新、每次强制停止应用之后会自动关掉这个权限,需要重开。 这是 ROM 行为,不是应用能阻止的。

微信(或别的应用)有时候读不到界面:有些应用会给所有无障碍客户端一个空白的占位界面, 不给真实的 UI。这不是 Andee 的问题:系统自带的 uiautomator dump 拿到的也是空白。我们在小米平板 5 (MIUI,微信 8.0.78)上实测,只有开着 MIUI 一个隐藏服务 MiuiEnhanceTBService 的时候,微信才肯 交出真实的 UI 树。这个服务是 TalkBack 的配套服务,设置里没有它的开关。开着它,同一个聊天页能读到 219 个节点;关掉它,一个都读不到。而且每次 Andee 被重新绑定(重装、崩溃、开关一次),MIUI 都会 自己打开或关掉这个服务。这就是"有时候能读到、有时候不行"的原因,也是为什么重开 Andee 从来 没真正解决过问题。把 Andee 的无障碍配置改成和它一样也没用:微信认的是"谁在要",不是"要了什么"。

Andee 的处理方式(screen/TreeHelper.kt):读到这种空白界面时,Andee 会把这个服务追加到已启用 列表里,然后重新读一次。它只加不删。设备上没有这个组件时,它什么都不做。它会在聊天记录里写一行, 让你知道系统设置被改过了。它最多一分钟尝试一次,所以如果你是故意关掉这个服务的,Andee 不会反复 把它打开。修改这个列表需要一项只能用 adb 授予的权限,和 ADBKeyboard 备用通道用的是同一项:

adb shell pm grant net.kuafuai.andee android.permission.WRITE_SECURE_SETTINGS

不授权也不会坏。遇到空白界面时,Andee 改用截图加坐标来操作,不再依赖元素列表。这样更慢、 也没那么准,但事情照样能做完。

三、麦克风

为什么要:ASR 是流式识别,需要拿 PCM 数据流上行。

怎么开:

  1. 首次点球触发 ASR 时,系统会弹权限申请,选「仅在使用该应用时允许」或「始终允许」都行
  2. 之前拒绝过的话,手动开:设置 → 应用 → Andee → 权限 → 麦克风 → 允许

验证:点悬浮球一次,字幕栏出现 "listening…" 或实时 ASR 文本就是通了。

唤醒词

不想每次都去点球,可以教它认自己的声音:⚙ → 唤醒词 → 录入唤醒词,照提示把你想用的词说 3 遍(比如「嘿 Andee」,换成别的词也行,每遍说同一个)。之后说这个词,效果和点一下球完全一样 —— 不需要亮屏,除非你自己把那条关掉。

几件值得先知道的事:

  • 词不要太短。 建议 3 到 5 个字、念起来半秒以上。太短的词容易被认错,念得短于 350 毫秒的 还会被直接丢掉,录不进去。

  • 全离线。 识别是本机的 MFCC + DTW 模板匹配(app/.../wake/),不联网、不花钱,存的只是 特征向量,录音本身从不落盘。代价是它认的是你说这句话的样子 —— 换个人叫、或语气差太多, 可能叫不醒;叫不醒就点球,不影响别的。

  • 录的时候别刻意念清楚。 判定阈值是从你那 3 遍之间的差距算出来的:念得过分标准,阈值会收得 很紧,平时那样叫反而认不出来。

  • 息屏时也在听,而这一条可以关。 息屏不再把麦克风让出来 —— 手机揣在兜里时屏幕本来就黑着, 这是它最该被叫醒的地方。开关在 ⚙ → 唤醒词 → 锁屏时也听,默认开启;代价是麦克风会一直 开着,下面说的状态栏提示也会一直显示。Andee 自己正在说话、以及正在录你说话的时候,两种设置下都会 把麦克风让出来 —— 对话本身的优先级永远高于唤醒词。锁屏时它听得到、答得了,但不会假装能动手: 碰屏幕和开 App 那类工具会直接拒绝,并说明原因。

  • Android 12 以后,只要麦克风开着,状态栏就有个绿点,这是系统行为,去不掉。没录唤醒词的话监听 根本不启动,也就不会有绿点。

验证:adb logcat -s Wake:*,叫一声应该看到 wake: d=1.52 <= 1.96;没叫醒但确实说了话会 看到 no wake: d=… > … —— 这两个数就是调阈值的依据,差得不多就重录一次。

机器狗(USB 加密狗)

机器狗是一个外设,不是这台设备的屏幕:它通过一根 USB OTG 线接在本机上的 Arduino Nano 加密狗上。狗端固件一行都不用改,原装遥控器照样能用。

平板 App ──USB OTG──> Arduino Nano + NRF24L01 ──2.4G──> 机器狗

大脑侧的工具:

工具 干什么
dog_move 往一个方向动 N 秒,到时自己停;默认轮式,gait=true 用腿走
dog_sequence 多步连做(转半圈 → 停一下 → 前进 → 退回),后台跑,结束自动停
dog_stop 急刹。用户说「停」时第一个调它
dog_turn 用平板的陀螺仪转一个量出来的角度,到角度就停 —— 唯一一个知道自己有没有做到的狗命令。优先用腿
dog_speed / dog_pose 速度和姿态。是设置不是动作:从下一次移动起生效
dog_status 链路状态、狗在动什么、以及它最近一次传回来的遥测
dog_sense 刷新狗自己的读数 —— 前方距离和电量 —— 搭在射频应答包上回来。两端都要刷 v2 固件;没有时它会说"看不到",而不是报一个 0
dog_calibrate 告诉平板"现在这样就是站直了",给摔倒保护用。基本用不上 —— 每次从静止开始移动都会自动做

接线与第一次使用

  1. 把加密狗(Nano + NRF24L01)接好并烧好固件。加密狗固件与接线图不在本仓库 —— 这里只有 App 侧的驱动(com.github.mik3y:usb-serial-for-android)。请按你手上的固件说明接线。
  2. 用 OTG 线插到本机。加密狗的 3.3V 供电一定要稳 —— NRF24L01 是瞬时电流大户,模块旁边建议 并一个 10~100µF 电容,否则会出现「命令发了但狗不动」。
  3. 第一次调 dog_* 时系统弹「允许 Andee 访问该 USB 设备吗?」,点允许。不点就等于没插。
  4. dog_status 里的 banner 会显示加密狗开机时说的话:READY 表示加密狗和它的射频模块都好; 如果是 ERR: NRF24L01 not responding,那是加密狗自己的接线 / 供电问题,不是狗坏了。

一个 USB 口,两个用途 —— 调试时要换无线 adb

单 USB-C 口的机器,OTG 和 adb 抢同一个口。要用加密狗就得把调试切到网络:

adb tcpip 5555                      # 先插着 USB 线执行
adb connect <设备IP>:5555
# 现在可以拔掉 USB 线,把口让给加密狗
adb usb                            # 想切回来时执行

安全须知(这几条不是客套)

  • 原装遥控器必须关机。 它和加密狗用同一个信道、同一个地址,两边同时发会互相打架。
  • 狗有避障,并且会拒绝你:前方 <20cm 不让前进、<10cm 自动后退。所以「让它往前走它不动」 通常是前面有东西,不是命令失败 —— 用 camera_turn 看一眼再决定。
  • 每条动作都有终点:dog_move 到时自停,dog_sequence 跑完自停,服务退出时会尽力发停止 命令。所以狗不会因为某次调用丢了就一直在跑。
  • 唯一的安全缺口在链路上:如果加密狗在狗正在动的时候被拔掉或断电,本机这边发不出停止命令, 狗会把最后一个动作做完。这是狗端固件的固有设计(它没有失联自动停车),App 补不了。要彻底解决 得改固件加一个「多久没收到包就停」的看门狗。
  • 别在人、宠物、易碎物旁边试长动作。第一次调建议先用 1~2 秒的小动作确认方向对不对。

方向对照(以实测为准)

固件注释里足式 E/D 标的是左转 / 右转,实测是反的(E 右转、D 左转)。App 里的映射按实测写:

工具参数 轮式(默认) 足式(gait=true)
forward K 前进 B 前进
back L 后退 C 后退
left M 左转 D 左转
right N 右转 E 右转

标定过的时长:轮式转一圈约 8.7 秒,足式约 12 秒(后者未精确标定)。直线速度没标过,按 秒数估。改标定值要同时改 ToolSchemas.kt 里 dog_move 的描述 —— 那段文字是大脑唯一读到的 说明书。

两种语言,三种中文

仓库里的中文分三类,只有一类需要翻译,判据是「谁读它」。改任何字符串前先看 CONTRIBUTING.zh-CN.md 里那张表 —— 里面还写着为什么「要和系统文字做匹配」的那 一类永远不能翻。

装机顺序建议

第一次装,按这个顺序最省事:

  1. 装 APK
  2. 立刻去「显示在其他应用上层」开悬浮窗 → 回来能看到球了
  3. 去「无障碍」→ 开 Andee
  4. 点一下球,弹麦克风申请 → 允许
  5. 打开球上的 ⚙,先翻到高级把「显示云端选项」打开(云端 tab 默认是藏起来的);回到 后端,picker 切到 云端,填 hub_url = ws://<你 Mac 的 IP>:9100、device_name = 随便起,保存
  6. logcat 里应该出现 hub: registered as body-xxx with N tools,同时大脑侧日志出现 [body_hub] 'body-xxx' registered (…) with N tools

任一步没对上,回来看本文档对应章节的验证。

只想要本机大脑的话,第 5 步改成后端 → 本机,填一个 OpenAI 兼容端点和 llm_api_key 即可 —— 不走 hub 是合法模式,不是降级。如果你的 APK 是通过 CodeFlying 打包的,那条 tab 也是默认选中的 一个,什么都不用填。

目录结构

core/                   两个身体共用:LocalBrain、LocalPrompt、ToolSchemas、TurnGuards、GuestHost,
                        以及 peer/(配对、加密连接、二维码邀请、找朋友帮忙)
desktop/                Mac 应用(Compose Desktop):窗口、浮动球、mac/(无障碍、输入、截屏、通知)、
                        shell/(沙箱终端)、voice/、native/(Swift 桥)
app/                    Android 应用(Kotlin,无 Compose)
  src/main/java/net/kuafuai/andee/
    BodyCoreService.kt     身体:球、卡片、语音、大脑连接、回溯 —— 第一个该读的文件
    ScreenBodyService.kt   无障碍服务 —— 只负责转交屏幕能力的壳
    brain/                 智能体循环 —— LocalBrain、LocalPrompt(角色的行为准则:
                           §1–§10 是活儿,§11 情景,§12 人格,§13 当前情景)、NotificationTriage(通知分诊)
    ui/ball/               那张脸 —— Mood(19 种情绪 + 待机小动作)、BallLook(六个形象,每个都是一个角色)
    net/                   BodyWsServer、BodyWsClient、ToolSchemas(工具定义)
    device/                屏幕控制、权限、保险箱、机器狗
    config/                VoiceConfig(设置)、Notebook(记忆、待办、情景 —— 加密存储)
    ui/                    悬浮层:球、字幕、设置、自检、情景、产物
    i18n/AppLocale.kt      悬浮层怎么拿到自己的字符串
  src/main/res/values{,-en}/strings.xml   两张面向用户的字符串表
docs/architecture.zh-CN.svg   一页看全(英文那份在旁边)
docs/permissions.zh-CN.md     为什么它要这些权限
reports/                设计实验室与调研笔记(是仓库的一部分,不是草稿)
tools/                  设备侧小工具、球脸验收 runner、许可证审计
CLAUDE.md               给 AI 编码助手看的架构说明(英文)

好玩的用法

用它做过什么?说来听听。 值得收的往往不是"正经用途"—— 是你绕的那个弯、是它居然真能做成的 那件事、是把你逗乐的那一下。不实用也可以。

发到 Show and tell, 一个帖子讲一个,这样每个都能单独聊。想让下一个人看得懂,三件事最好带上:

  • 跑在什么机器上 —— 机型、Android 版本、ROM。这个应用和 Android 打交道的层次上,各家 ROM 是 有分歧的,"我手机上能用"对别人几乎等于没说。
  • 你说了什么,它真做了什么 —— 你原话那句,往往比概括出来的有意思。
  • 有链接就贴 —— YouTube、B站、微博、自己的博客,公开的就行。GitHub 的 README 放不了视频 播放器,它只能是个可点的链接。YouTube 的话可以做成缩略图,看起来就像个播放器:

点开看

(把 VIDEO_ID 换成视频地址里的那一段 —— youtube.com/watch?v=VIDEO_ID)

有一件事不是客套:这个应用是读屏幕的。 截图或录屏,就是你消息、通知、当时开着的一切的实拍。 发之前把涉及隐私的地方挡掉 —— 你自己和别人的都算。

参与贡献

  • 代码 —— 先读 CONTRIBUTING.zh-CN.md。里面有代码风格,以及这个仓库里三种 中文只有一种要翻译的规则。改人格或情绪有单独一节,那不是普通文案。
  • 报 bug —— 请务必带上设备型号、Android 版本、ROM 和 logcat。这个应用和 Android 打交道的 层次上各家 ROM 是有分歧的:AOSP 上能用的调用在 MagicOS 上可能静默失败。缺这些,多数报告没法 复现。
  • 形象与人设 —— 新形象只写语气,不写能力。能力属于行为准则,不属于某个角色。
  • 文档与翻译 —— 欢迎。中文 README 是装机那份,必须保持准确。

贡献者

社区与联系

渠道 用来做什么
Issue 跟踪 可复现的 bug、工程事项(请带上设备型号 / Android 版本 / ROM / logcat)
讨论区 / 问答 装机求助、想法、"这个能不能做"
好玩的用法 你用它做过什么,实用不实用都行 —— 见好玩的用法
安全 漏洞 —— 走私密渠道,永远不要开公开 issue。见 SECURITY.md
产品 / 在线 不想自己打包就想试 Andee —— codeflying.net · codeflying.app
微信 加 Andee 为好友 —— 扫下面的码,或者点聊天页底部的微信图标

用微信扫码,添加 Andee 为好友

安全披露

安全问题请不要开公开 issue。私密上报渠道和处理流程在 SECURITY.md。

那份文件同时列出了部署前必须知道的已知限制,其中一条在任何你控制不了的网络上都成立:调试 用的 WebSocket 服务绑在 0.0.0.0:9008 且不验证客户端身份。本应用申请的权限组合与安卓银行木马 的检测特征高度重合 —— docs/permissions.zh-CN.md 把每一条对应到一个具体工具, 而 SECURITY.md 对"还没修的"是诚实的。

免责声明与可接受使用

两份文件,都是使用条件,不是客套。中文版在这里,英文版见各自的对照文件:

  • DISCLAIMER.zh-CN.md(English)—— 这是什么性质的 软件(实验性、无 SLA、无担保)、你的数据到底去了哪里、它实际能做到什么、以及出事后由谁承担。
  • docs/acceptable-use.zh-CN.md(English) —— 明确禁止的用途。违反即超出许可范围,许可自动终止。

就算其余都不看,这四条也请看完:

  • 他能花钱。 项目里没有支付工具,也不需要 —— 他直接点。唯一挡住一笔转账的是 Android 自己的 FLAG_SECURE,本项目对这条路径没有任何技术围栏。任何涉及钱的操作都要人在场;并且请记住 "是 AI 自己做的"不是抗辩理由,因为它确实不是。
  • 他发出去的东西是你的。 消息、帖子、通话、订单,署名都是你。
  • 你的数据会离开设备。 本仓库里没有数据分析 SDK,但语音会走你选的那家服务商(默认火山, 也可以选 Google),对话连同全部工具返回 结果会走到你配置的那个大脑端点,版本自检会走到一台维护者自己跑的服务器。"本机模式"换的是 目的地,不是这个事实。
  • 他能被屏幕上的字操纵。 任何能让你打开一个页面的人,都可能影响到他。

许可

Apache-2.0 加附加条件 —— 禁止多租户 SaaS、禁止去 LOGO 白标。GitHub 会把它显示成 NOASSERTION,这是预期的,不是配置错误。

随包分发的第三方组件清单在 THIRD_PARTY_NOTICES.md。再分发之前请先看: ML Kit 扫码 SDK 和 play-services-* 不是开源许可,而且扫码模型是运行时从 Google 拉的。

改了什么、以及版本号策略见 CHANGELOG.md —— 当前版本是 v0.9.0,故意停在 pre-1.0:线上协议和构建开关都还在动。

相关文档

探索相关项目