不用你开口。 你连着耳机、手头不忙、时间也对的时候,它自己挑一首放起来。 你收藏里那几十上百首,它一次读得完;整个曲库,它也点得到。
放歌全程在虚拟副屏上,不占你的屏;你没叫它的时候,还会挑一句歌词递过来,跟你聊那一句 (你耳机里唱到「不然怎么会挑中我呢」的那一刻,它手上就是这一句)。
它的技术底子在两头。 读"你在听什么",它不问平台 —— 直接读系统自己的播放状态
(adb 读 media_session),所以不管你在哪个 App 里放,它都知道放的是哪首、播到了第几秒。
点歌也不走平台接口,而是在自建的虚拟副屏上像人一样去点。
(歌单和歌词是另一条路 —— 走本机的接口服务,见准备工作。)
🎧 实测走 酷狗 · 免 Root 免电脑 · AI 跑在手机 Termux 本地 · 放歌走虚拟副屏不占屏
🔑 先决条件:你的 AI 得先住进手机、再摸到手机
- ① 还没在手机上跑起 Claude Code?→ android-claude-wechat:一条命令装好 Claude Code + 运行环境,免电脑免 Root
- ② 装好了但 AI 摸不到手机?→ android-claude-agent:免 Root 免电脑免 WiFi,给 AI 装上 adb 手脚
- ③ 两个都好了,回到本仓库 —— 让 AI 坐到你旁边,一起听
三个仓库是一条线:住进来 → 摸得到 → 一起听。
🎯 它自己会给你点歌 —— 不用你开口
你连着耳机、手头不忙、时间也对,它自己挑一首放起来 —— 不是定时任务在响,是它真在听。
怎么放的:平台侧的验证码拦着纯接口选歌,所以它走的是"像人一样去点"这条路 —— 在副屏上归位、开搜索、灌歌名、点中结果。结果行的坐标是当场从控件树里读的, 不写死一个像素,换台手机换套主题照样点得中。你收藏里那几十上百首它一次读得完; 整个曲库,它也点得到。你想点也行:"放《桃子》",一句话它就去点。
📻 而且不占你的屏
音乐 App 一启动就抢前台、你正看的被顶走 —— 这事它不干:
歌在虚拟副屏上放(am start --display 丢进自己建的那块屏),声音照常从你耳机出来,
你手里这块屏一动不动。
虚拟副屏的根是别人的开源项目(agent-mobile-use)—— 我们在免 Root 的手机上把它跑通了,走过的弯路和改法写在 TUTORIAL 里,见鸣谢。
💬 你听到哪一句,它知道
不是"知道歌名"那种,是知道你此刻唱到哪个字:把歌词时间轴跟你手机上的播放进度做对齐。
难在 dumpsys media_session 给的是快照 —— 实测跟真实进度差过 39 秒,
得按 /proc/uptime 把这段漂移补回去,它报的那句才跟你耳朵里的一致。
所以它开口,是在对的那一句上开口:
♪ 歌词: 《桃子》唱到「[0:19] 不然怎么会挑中我呢」
[规则: 有意思就接一句聊,没感觉就放着不吭声;一首歌最多两句]
- 想让 AI 陪自己听歌的人 —— 既能点歌,也有旁边那个真在听的人
- 想给 AI 装"主动说话"能力的人 —— 本仓库的轮询机制是通用的,听歌只是它的第一个应用(见第三层)
- 已经在用前两个仓库的人 —— 这是三部曲的第三棒,接得上
| 需要 | 怎么来 |
|---|---|
| Claude Code 跑在手机 Termux 里 | android-claude-wechat |
| adb 回环(AI 的手脚) | android-claude-agent |
| Node.js(跑本地音乐接口服务) | pkg install nodejs,或前两个仓库已带 |
| Python 3 | Termux 自带 |
| 一个音乐 App + 你自己的账号 | 实测走酷狗。换平台要另找对应的本地接口服务(见 第一步) |
bash <(curl -sL https://gitee.com/xvxv663/android-claude-music/raw/master/install.sh)装完你会得到:
~/.claude/skills/kugou-music/ 读收藏 · 自己挑歌放 · 报"此刻唱到哪句"
~/.claude/skills/listen-together/ 陪听规则(写给 AI 看的)
~/.claude/skills/music-control/ 什么时候该放(判断层)
~/.claude/scripts/listen-loop.sh 轮询:此刻唱到哪句 → 递给 AI
~/.claude/scripts/listen-detect.sh 轮询:这会儿该不该自己放一首
~/.claude/scripts/music_moment.sh 放一首(带 40 分钟冷却)
~/.claude/scripts/vd.sh 虚拟副屏(放歌不占屏)
本仓库不内置任何账号。本地音乐接口服务要用你自己的账号登录一次,它才知道"我喜欢"里有什么。 登录态存在你自己手机里,不出手机、不上传。
手机号 + 验证码登录一次即可,步骤见 TUTORIAL.md。
每个人的手机都不一样 —— 概率、频率、时间窗、用哪个 App、屏幕上点哪儿,
全是给你自己调的。下面这些都在文件顶部的变量里(或者 music.env),打开就能改:
| 想改什么 | 改哪儿 | 默认 |
|---|---|---|
| 用哪个音乐 App | music.env → MUSIC_PKG |
com.kugou.android |
| 你的歌单 | music.env → MUSIC_LIKE_ID |
必填,见 TUTORIAL 第四步 |
| 它自己放歌的概率 | listen-detect.sh → CHANCE |
2% |
| 什么时段才放 | listen-detect.sh → HOUR_FROM / HOUR_TO |
8 ~ 23 点 |
| 哪些 App 算"你在忙" | listen-detect.sh → ENT_APPS |
抖音/快手/B站/爱奇艺… |
| 歌词递话的频率 | listen-loop.sh → GAP_MIN / GAP_MAX |
30 ~ 80 秒(随机) |
| 一首歌最多聊几句 | listen-loop.sh → MAX_PER_SONG |
2 |
| 放完一首多久不再放 | music_moment.sh → MUSIC_COOLDOWN_MIN |
40 分钟 |
| 副屏开多大 | vd.sh dstart 宽 高 dpi |
1280x720/240 |
| 副屏上那组坐标 | kugou.sh 顶部 *_X / *_Y |
7 个,装完先量一遍,见下一节 |
| 副屏上点哪儿 | vd.sh tap X Y(每次现给) |
每台机器都不一样 |
# 1. 拿包名(把关键词换成你要找的 App)
adb shell pm list packages | grep -i kugou
# → package:com.kugou.android
# 2. 填进 music.env
MUSIC_PKG=com.kugou.android- 歌词 —— 读歌词走的是本地接口服务(默认那个是酷狗的)。换平台,得找对应的本地接口服务; 没有的话,"一起听"就只剩一半:它知道你听什么,但跟不上唱到哪句。 (好消息是读播放状态和放歌都走系统媒体接口,跟 App 无关,换谁都能用。)
- 坐标 —— 每个 App 副屏上的按钮位置都不一样。
vd.sh tree读控件树的坐标和文字,vd.sh shot截图看着量。别抄别人博客里的坐标,那是别人手机的。
kugou.sh 顶部有一组坐标(7 个),是我按自己的手机量的。换台机器、换个分辨率,
甚至 App 升个小版,它们都可能漂。
漂了不报错——它只是点空:脚本照样报"放上了",实际放的是别的歌。所以装完先量一遍。
怎么量:在副屏上走到那个页面,读控件树,拿元素的框,取中心点。
bash scripts/kugou.sh play # 1. 把 App 丢进副屏(跟平时一样)
bash scripts/vd.sh tree | grep '搜索' # 2. 读树,找你要的那一行
# → 208 FrameLayout "搜索 ..." 478,136,666,186 ...
# └─ 这四个数是 x1,y1,x2,y2
# 3. 中心点 = ((x1+x2)/2, (y1+y2)/2) = ((478+666)/2, (136+186)/2) = (572, 161)
# 填进 kugou.sh 顶部的 SEARCH_X / SEARCH_Y,其余六个同理哪个坐标管哪一步,脚本里每个变量后面都写了注释。改完不用重启什么,下次跑就生效。
七个坐标分别在哪一屏(照着走到那页再量):
| 变量 | 在哪个页面 |
|---|---|
TAB_HOME_X/Y、TAB_MINE_X/Y |
底部导航(任何主页面都有) |
SEARCH_X/Y |
首页顶部搜索框 |
IME_DONE_X/Y |
键盘弹出来时,它右上角的「搜索」键(各输入法位置不同) |
HIT_PLAYALL_X/Y |
搜完出来的结果页,「单曲」那排的「全部播放」 |
LIKE_CARD_X/Y |
「我的」页里「我喜欢」那张卡片(点上半部,下半跟播放条打架) |
FAV_PLAYALL_X/Y |
「我喜欢」歌单页顶部的「播放全部」 |
懒得一个个量:kugou.sh 里有现成的量尺函数,按控件 id 或界面文字直接算中心点——
坐标漂了(比如 App 换了版)用它重新量最快:
id2xy mrs # 按 resource-id 找(搜索框是 mrs)
tree2xy '"我喜欢' # 按界面上的文字找日常放歌不会用它们:读一次控件树要十几秒,每一步都读等于白等。所以平时走上面那组 写死的坐标——快,只在量坐标和兜底的时候才读树。
频率类的东西乘起来看:轮询跑得越勤 × 概率越高 = 放得越频繁。 默认
listen-detect.sh几分钟一轮 × 2%,一天大概也就放个一两次。
三层。前两层是"听得见、听得懂",第三层才是这个仓库真正的骨头 —— 主动。
Android 系统里有个东西叫 media_session,所有正在播放的媒体状态都记在这儿:
adb -s 127.0.0.1:5555 shell dumpsys media_session
# description=桃子 - 吴琳珂Moske
# state=PlaybackState {state=PLAYING(3), position=149000, updated=..., ...}歌名、歌手、播放状态、已经播到第几毫秒 —— 一行全有。这是全部的地基。
⚠️ 这一层要 adb 权限,也就是第二个仓库干的事。 没有它,AI 连"你在听歌"都不知道。
有了"播到第 149 秒",还得知道这 149 秒对应哪句词。三步:
① 拿歌词 —— 本地音乐接口服务按歌名配到这首歌,取回带时间轴的 LRC:
[00:13.00] 指尖触碰我的外壳
[00:19.00] 不然怎么会挑中我呢
② 对齐 —— 找 时间轴 ≤ 当前进度 的最后一句,就是"此刻"那句。
③ 补漂移 —— 这一步最容易翻车。dumpsys 给的是快照,不是实时的:
# 快照里的 updated(系统时钟,毫秒)跟"现在"差了多少,就往进度上补多少
now_ms = /proc/uptime × 1000
drift = now_ms - updated
pos = (position + drift) / 1000实测这个差过 39 秒 —— 不补的话,你耳朵里早唱过去了,它还在报上一句。
再加一个提前量:它打完字递到你眼前,歌已经又走了几秒。默认往前顶 8 秒,报的才是"你会听到的那句"。
到这里它已经听得见、也听得懂了,但还是被动的:你不问,它不说。
要让它主动,先得认清一件事:AI 不是一直醒着的。
Claude Code 这类 Agent 一次只活在"一轮对话"里 —— 你发消息,它醒;回完,它睡。 所以"主动"不是让它一直盯着屏幕,而是给它装一个叫醒它的东西。
这就是轮询:一个常驻的小脚本,隔一阵看一眼世界;有值得说的,就往会话里丢一行。 AI 被这一行叫醒,读到内容,自己决定开口还是闭嘴。
歌词这一路长这样:
# listen-loop.sh 的核心
GAP=$(( RANDOM % 50 + 30 )) # 下次隔 30~80 秒
LP=$(python3 lyric_now.py --auto) # 报"此刻正好唱到的那句"
[ -n "$LP" ] && echo "♪ 歌词: $LP [规则: 有意思就接一句聊,没感觉就放着不吭声]"三个设计要点,全都是踩出来的:
① 时机随机,别用固定间隔。 固定每 60 秒问一次 → 同一首歌听十遍,十遍都在同一句开口,机械得像个闹钟。 改成随机 30~80 秒瞟一眼 → 随的是时机,所以同一首歌每次听,撞上的句子都不同。
② 一首歌最多开口两次。 脚本不知道什么叫"有意思",但它得管住频率 —— 四分钟一首歌,不设上限它能递四十次,把对话淹了。
③ 脚本只递话,判断权留给 AI。
规则跟着事件一起送到 AI 眼前([规则: 有意思就接一句聊…]),接不接是它自己的事。
"这句有没有意思"是主观的 —— 脚本挑不出来,AI 挑得出来。
💡 这套轮询是通用的,不只用来听歌。 你切到别的 App、电量掉到 20%、走了多少步、半夜屏还亮着 —— 任何"值得它主动开口"的事都能挂进同一条循环。歌词只是第一路。 想接进你自己的 AI:把
listen-loop.sh抄进你那个常驻轮询里,或者单独挂一个。
"陪听"还有另一半:它放给你听。这里有个坑 —— 音乐 App 一启动就抢前台, 你的屏当场被顶走,正在聊的、正在看的全断。
解法是虚拟副屏:让系统多开一块看不见的屏,把音乐 App 丢进去跑。 声音照常从耳机出来,你的主屏一动不动。
vd.sh dstart # 开一块副屏
vd.sh open com.kugou.android # 把 App 丢进副屏(不是主屏!)
adb shell input -d <副屏id> keyevent 126 # 播放 / 暂停一句话原理:
am start --display只对自己建的屏放行 —— 系统要看这块屏是不是你建的。 所以settings put global overlay_display_devices开出来的那种系统屏,shell 永远吃SecurityException,得自己建一块。 两个 id 也别搞混:input -d用逻辑 id,screencap -d要 SurfaceFlinger 的 64 位 id,而且每次都变 —— 必须现取现用。
这条路是站在别人肩膀上走的,完整交待在鸣谢和 TUTORIAL 里。
本仓库只用到它"把 App 丢到别处"这一件事;副屏的开关、读树、点击都在 scripts/vd.sh 里,拿得走。
前面讲的都是"它在听"。这一节是"它主动放" —— 没人叫它,它自己挑时候给你放一首。
listen-detect.sh 每跑一轮判五件事:
| 判什么 | 怎么判 | 为什么 |
|---|---|---|
| 屏幕亮着吗 | dumpsys power 读 mWakefulness |
黑着屏放歌没意义 |
| 在时间窗里吗 | 默认 8:00~23:00 | 半夜不吵你 |
| 蓝牙耳机在线吗 | dumpsys audio 数 bt_a2dp |
没耳机等于外放,别放 |
| 正在娱乐 App 里吗 | dumpsys activity 读前台包名 |
你正在刷视频看剧,别插一脚 |
| 概率命中吗 | 默认 2% | 见下 |
前面五关判的是"这会儿能不能放";概率管的是另一件事 —— "放多了会不会烦"。
条件全中的时刻,一天里其实很多(戴着耳机坐着的时候都算)。每次都放,就成了骚扰。 所以再掷一次骰子,大多数时候它按兵不动 —— 一天顶多冒出来一两次,那才叫惊喜。
命中之后它做两件事:放一首(转 music_moment.sh → 走副屏、不占屏,带 40 分钟冷却),
然后把这件事报给 AI —— 这才是"主动说话"的另一半:
♪ 放歌: 桃子 - 吴琳珂Moske [规则: 这一首是你自己放的,不是她点的 ——
①别问她"要不要听歌",想说什么就一句,像随手放了首歌
②她正忙着/刚被吵醒就当没这回事,别硬搭话
③歌名照上面这行念,读不到就别提歌名,别编]
规则跟着事件一起送到 AI 眼前 —— 这一条是整套东西的关键: 脚本不知道"这时候该说什么",AI 知道。所以脚本只把事实和边界递过去(谁放的、别问什么、别编什么), 开口说什么、开不开口,全是 AI 的。
(读法提示:上面那段里 AI 是"你"、你是"她"——因为那是脚本写给 AI 看的话,不是写给你的。)
挂进轮询(跟 listen-loop.sh 并列,隔几分钟一轮就够):
bash ~/.claude/scripts/listen-detect.sh想改成"只报告、放不放由 AI 定":把脚本最后那段
music_moment.sh去掉,只留echo。
读不到歌名 / 报"没在放歌"
- 先确认 adb 在线:
adb -s 127.0.0.1:5555 shell echo ok - 确认真的在放:
adb shell dumpsys media_session | grep -m1 description= - 有些播放器不往
media_session里写状态 —— 换系统自带的或主流音乐 App 试
报的句子总是慢半拍 / 快半拍
- 提前量在
lyric_now.py里,默认8秒:python3 lyric_now.py 12可以临时改 - 觉得一直对不上,先怀疑漂移没补:
/proc/uptime那条读不到就会退化成快照时间
歌词是空的
- 这首歌本地接口没配到歌词。字段还不固定(有的给
lyrics、有的只给 base64 的content),脚本三种都兜了 - 小众歌、纯音乐、翻唱版本容易缺
说"放某首歌"没放成 / 放的不是那首
- 脚本点结果行前会现读控件树找含歌名的那一行;找不到就是搜索没出结果 —— 先
vd.sh tree看看树上有没有那首 - 别走"歌单页右上角搜索"那条:输入法的全屏提取框会盖住上半屏,结果行点不动(点哪儿都被输入法吃掉)。走主界面顶部的搜索
- 全流程约 30 秒(要归位、要搜索、要等页面),慢是正常的
接口服务连不上(4000 端口)
- 服务挂了:进
~/kugou-music-api重新PORT=4000 node index.js - 登录态掉了(隔太久没用):重新登录一次
副屏起不来 / 放歌还是切屏
vd.sh id看两块屏在不在;不在就vd.sh dstart- App 进程还活着的时候,启动请求会被复用到老 task(主屏) —— 所以
vd.sh open内部先force-stop再am start,这一步不能省
Termux 被系统杀后台 —— 关掉 Termux 的电池优化,部分机型还要在最近任务里锁定。
更多问题 → Gitee Issues
- 数据全在本地:播放状态从你自己手机上读,歌单/歌词走本机跑的服务,不上传任何第三方。全开源可审计。
- 权限在你手里:装上之后 AI 能读你的屏幕状态、能操作手机 —— 这份权限随时可以断(关掉 adb 回环、停掉服务即可)。
- 第三方接口:本仓库用到的
KuGouMusicApi是第三方逆向项目,用于读取你自己账号的公开歌单信息。仅供学习交流,不要用于商业用途或传播版权内容,音乐版权归各平台与版权方所有。 - 虚拟副屏吃性能:多开一块屏会让机器发热、耗电,不用的时候收掉(
vd.sh dstop)。 - 本项目仅供学习交流,使用者自行评估风险。
- AcidGr/agent-mobile-use(酸小明,MIT)—— 虚拟副屏的根。我们在没有 root 的手机上把它跑起来了(走 Shizuku + adb 这条路),过程中改的几处代码都在
src/里。本仓库scripts/vd.sh与assets/*.dex源自这个项目(含我们的修改),版权归原作者所有,MIT 许可全文见assets/LICENSE-agent-mobile-use。
项目在持续更新,请以原仓库为准。 - scrcpy(rom1v)——
--new-display那条路,以及FakeContext那个关键解法(Context 上报的包名必须跟调用者 uid 对得上)都是从它那儿学的 - KuGouMusicApi(MakcRe)—— 本机音乐接口服务,读歌单和歌词靠它
- Termux —— Android 上的 Linux 终端
- Shizuku —— 免 Root 系统权限
- claude-code-android 与 Termux 部署 Claude Code 的先驱者们 —— 前两个仓库铺的路
MIT —— 拿走用,署名随意。第三方组件版权归各自作者(见鸣谢)。
- 更多播放器适配:网易云 / QQ 音乐 / 本地播放器的
media_session差异对照 - 歌词源可换:现在绑本地接口,想接 LRCLIB 这类公开歌词库
- 不止歌词:把"听到什么"扩展到播客、有声书、视频 —— 同一套时间轴对齐
- 情绪感知:连着听三首慢歌,它该不该问一句"今天怎么了"
- 选歌走 API 那条路:现在是副屏上搜 + 点(能用但慢,约 30 秒)。 平台侧的滑块验证码挡着纯 API 选歌 —— 等哪天绕得过去,就能一句话秒切
💡 有想法?去 Issues 提。