# dsh-dot/phone · 她的手机

An Android phone the plugin installs and runs for the character, exposed to the model as `mcp__phone__*`. No Android Studio, no Java: the plugin fetches three archives from Google's repository, writes the AVD itself, boots the emulator and mounts [mobile-mcp](https://github.com/mobile-next/mobile-mcp) over adb.

## 它做什么

1. **安装**（第一次）：接受 [Android SDK 许可](https://developer.android.com/studio/terms)后（`/phone accept`，或配置 `acceptLicense: true`，或 `~/.dsh/dsh-phone.json` 里 `{"acceptLicense": true}`），从 `dl.google.com/android/repository` 读索引，下载 stable 通道的 emulator、platform-tools 和 `system-images;android-<api>;google_apis;<abi>`（约 1.7 GB），解压到 `~/.dsh/phone/`。`google_apis` 镜像没有 Play 商店——检测面最小，App 用 APK 装。
2. **AVD**：`~/.dsh/phone/avd/dot.avd/config.ini` 由插件写（Pixel 6 尺寸、1080×2400、3 GB 内存、8 GB 数据分区、不带 Play）。想改尺寸或内存，改配置后删掉这个目录重建。
3. **开机**：`emulator -avd dot -port 5554 -no-boot-anim -gpu auto`，等 `sys.boot_completed`，然后常亮屏幕、关动画、解锁。上一次 dsh 留下的还在跑的模拟器会被直接接管，不会开两个。模拟器自己退出会在 `restartDelayMs` 后重开。
4. **MCP**：开机后以 stdio 挂一个 mobile-mcp（随插件一起安装，`ANDROID_HOME` 指向 `~/.dsh/phone`，`ANDROID_SERIAL=emulator-5554`），工具名 `mcp__phone__mobile_*`：列设备、截图、列屏幕元素（带坐标）、点、滑、长按、输入、按键、打开/关闭 App、装卸 App、开 URL。
5. **提示**：手机就绪后系统提示里多一段 `phone:device`，告诉模型设备号和用法（先看元素再点；验证码、人脸停下来叫用户）。
6. **闸门**：`mobile_install_app` / `mobile_uninstall_app` 弹审批；`mobile_type_keys` 的内容像密码 / 支付 / 验证码时弹审批（`sensitivePatterns`）。幕间的观察轮只放行只读工具（看、滑、点进去、开 App、返回），其余直接拒绝、不审批。
7. **通知叫醒她**：插件盯着系统的事件日志（`adb logcat -b events notification_enqueue`），每有 App 发通知就立刻读一次通知栏（`dumpsys notification`），另外每 `notifyPollMs` 兜底轮询一次——QQ 这类 App 在聊天窗口开着时会立刻撤回通知，光靠轮询会漏，新出现的通知（启动时已经在栏里的不算；系统包不算；`notifyApps` 可以只挑几个 App）交给 dot，dot 往她最近的会话里追加一条 `[手机通知] QQ · 宽：……`，她自己决定去 App 里回还是只回 `DOT_SILENT`。这条回复不会转发到任何 IM。所以她不需要定闹钟去看 QQ——有人找她，手机会响。**任何 App 都算**，包括她以后自己装的：新 App 默认叫醒；像手机自己的通知设置一样可以按 App 静音——`/phone notify mute <包名>` / `wake <包名>`（持久化在 `dsh-phone.json`），`/phone notify` 看每个 App 发过多少条和当前策略。她在 Android 设置里关掉某个 App 的通知也一样生效（通知栏里没有就不会叫）。几秒内连来的多条合成一次叫醒（dot 的 `notifyBatchMs`）；dot 的 `notifyHours` 可以设一个静音时段（默认不设）。`/phone notifications` 看最近 20 条。
8. **实时画面**：两种。一是模拟器窗口本身（`window: true`）；二是插件的网页，挂在 **dsh 自己的 web server** 上：`<dsh 地址>/dsh-dot/phone/`——dsh 右侧栏的「手机」标签页就是它：屏幕每秒刷新几次（PNG 流），能直接点、滑、按返回/主页、输入文字，登录 App 就在这里做。**它不单独监听端口**：页面、画面流和每一次点击都先过 dsh 的 `connection` 服务（dsh 自己的登录态 + Host/Origin 围栏，转发头一概不认），只有登录了这个 dsh 的人能看能点。想在别的设备上看，把 dsh 本身暴露出去（如 `tailscale serve` dsh 的端口，或 dsh 的远程访问），再把那个地址写进 `publicUrl`，审批提醒里的链接就是 `<publicUrl>/dsh-dot/phone/`。无头跑（服务器）时 `window: false` 只用网页。网页里的文字输入走 `adb shell input text`，只支持 ASCII（账号、验证码够用）；中文由她自己的工具输。

## 配置

| 配置项 | 默认 | 作用 |
|---|---|---|
| `home` | `~/.dsh/phone` | SDK 三件套和 AVD 所在 |
| `api` / `tag` / `abi` | `35` / `google_apis` / 跟随宿主 CPU | 系统镜像；Apple silicon 用 `arm64-v8a`，x86 用 `x86_64` |
| `avd` / `device` | `dot` / `pixel_6` | AVD 名和机型 |
| `ramMb` / `dataGb` | `3072` / `8` | 内存、数据分区 |
| `port` | `5554` | 控制台端口；adb 序列号 `emulator-<port>` |
| `window` / `DSH_DOT_PHONE_WINDOW=off` | `true` | 显示模拟器窗口 |
| `liveView` | `true` | 在 dsh 的 web server 上挂实时画面页（`/dsh-dot/phone/`）；`false` 关闭 |
| `publicUrl` | 无 | 这个 dsh 对外的地址（你自己的暴露方式）；实时画面链接 = `<publicUrl>/dsh-dot/phone/`，写进审批提醒 |
| `gpu` | `auto` | 模拟器 `-gpu` |
| `keepAppsAlive` / `keepAliveEveryMinutes` | `true` / `10` | 关掉 doze、standby、cached-apps freezer，给所有第三方 App（含她以后装的）后台运行许可，否则 QQ 退后台几分钟就被冻结、收不到消息（真机实测：发出到弹通知差 3.5 分钟） |
| `locale` / `timezone` | `zh-CN` / `Asia/Shanghai` | 手机的语言和时区 |
| `installers` | 浏览器、文件管理器 | 开机时被允许安装未知应用的包；`[]` 则她得自己去设置里开 |
| `acceptLicense` / `DSH_DOT_PHONE_ACCEPT_LICENSE=yes` | `false` | SDK 许可 |
| `channel` | `stable` | 模拟器通道 |
| `bootTimeoutMs` / `restartDelayMs` | `300000` / `10000` | 开机等待、崩溃重开间隔 |
| `notifications` | `true` | 盯不盯通知栏 |
| `notifyPollMs` / `notifyApps` / `notifyMuted` | `4000` / `[]` / `[]` | 兜底快照间隔（事件到了会立刻读，`0` = 只靠事件）；只关心哪些包（空 = 所有非系统 App，含以后装的）；静音的包（`/phone notify mute` 也写这里） |
| `mcp` / `toolCallTimeoutMs` | `true` / `60000` | 挂 mobile-mcp |
| `askTools` / `sensitivePatterns` | 装卸 App / 密码支付验证码 | 审批规则 |
| `DSH_DOT_PHONE=off` | | 整个组件不加载 |

桌面版没有 shell 环境变量时，用 `~/.dsh/dsh-phone.json` 写同名字段。

## 装 App 和登录

- **她自己装。** 手机没有应用商店，和一个人拿到新手机一样：她用手机上的浏览器打开官网，下载安装包，点开安装。插件开机时已允许浏览器和文件管理器安装未知应用（`installers`），所以不用进设置。这是故意的——养一个会自己折腾手机的角色，而不是替她把一切装好。`adb install` 当然也能用，但那是你的事，不是她的。
- 在模拟器窗口里亲手登录。验证码、人脸、短信验证都由你过；她遇到会停下来告诉你。
- **用小号**：各平台条款都不允许自动化，模拟器也可能被检测。登录态在 AVD 里，重启不丢；`~/.dsh/phone/avd/` 整个目录就是这部手机，可以备份。

## 平台

| | 可用 | 备注 |
|---|---|---|
| macOS Apple silicon | ✅ | arm64 镜像原生速度；第一次启动系统会问是否允许模拟器用虚拟化 |
| macOS Intel | ✅ | x86_64 镜像，依赖 Hypervisor.framework |
| Linux x86_64 | ✅ | 需要 KVM（`/dev/kvm` 可写）；无头跑用 `window: false` |
| Linux arm64 | ❌ | Google 不发 Linux arm64 模拟器 |
| Windows x64 | ✅ | 需要 WHPX 或 Hyper-V；解压用 PowerShell |
| 另一台 Linux 虚拟机里 | ❌ | 嵌套虚拟化。手机和电脑是两台设备，不是一台装另一台 |

## 已知限制

- App 开在前台时不会发通知（QQ 的聊天窗口开着就收不到那个人的消息通知）。提示里让她用完 App 按 HOME 回桌面。

- 第一次下载约 2 GB，取决于到 `dl.google.com` 的速度。
- 没有 Play 商店和 Google 服务；需要 GMS 的 App 不能用。
- 模拟器检测：部分 App 会限制功能或拒绝登录。退路是一台真安卓机接 adb——`ANDROID_SERIAL` 换成真机序列号即可，组件其余不变（待做：`serial` 配置项）。
- mobile-mcp 的工具每次调用都要带 `device`，模型偶尔会忘；提示段里给了设备号。
- 多台手机未做。
