# phone-deck
将闲置的安卓手机(或任何带浏览器的设备)变成你 Linux 工作站的自主托管控制台和远程桌面——通过你的私有 Tailscale 网络,无需第三方应用。
它始于“拿旧手机能做什么?”,最终演变为一个 Hyprland 设备的控制平面:窗口/工作区控制、场景宏、带本地 LLM 助手的按键通话语音路由、上下文感知的应用内控制、双向音频+屏幕流式传输、触控远程桌面、虚拟键盘/鼠标、文件传输+手机到电脑共享,以及实时遥测——所有这些都通过一个已安装的 PWA 提供服务,外观为磷光终端风格。
> **关于可移植性的说明。** 这是为特定配置构建的:**基于 Arch 的 Linux(Garuda)+ Hyprland + PipeWire + NVIDIA + Tailscale**,并使用了 [end-4 / illogical-impulse](https://github.com/end-4/dots-hyprland) 配置文件。架构是通用的,但许多集成是环境特定的(显示器名称、工作区绑定、matugen 主题路径、`cpupower`/`nvidia-smi`)。请将其视为一个可适配的工作参考,而非即开即用的软件包。
---
## 功能特性
该界面是一个带标签页的深色横屏 Web 应用(主题与你的桌面同步):
| 标签页 | 功能 |
|---|---|
| **Workspaces** | 按显示器显示实时工作区网格(点击切换)、实时窗口列表(点击聚焦)、各显示器 DPMS 开关 + `ddcutil` 亮度控制 |
| **Remote** | 屏幕触控板(拖拽/点击/两指) + 带修饰键和组合键的虚拟键盘,通过内核 `uinput` 设备实现 |
| **Modes** | 一键场景宏——跨显示器启动/关闭/排列整个应用布局(例如“工作”/“休闲”模式)|
| **Voice** | 按键通话语音路由(faster-whisper,CPU):引导词通道——`macro`(运行模式/命令)、`type`(听写)、`input`(短语→按键组合)——具有**执行前确认**功能,外加 **"friday"**,一个只读本地 LLM 问答器(Ollama + Qwen),支持自托管网络搜索和实时系统状态感知 |
| **Audio** | 麦克风/扬声器静音、音量、输出**和**输入设备选择器、`playerctl` 传输控制 + 封面艺术(追踪活动的 MPRIS 播放器)|
| **Stream** | 双向 **WebRTC 音频**(电脑↔手机,手机作为麦克风和仅手机输出)**+ 屏幕视频**,以及**点击控制流式传输的屏幕** = 真正的远程桌面 |
| **System** | 性能模式切换、实时**主题**切换、Tailscale 状态、最占用资源的进程(点击杀死)、锁定、NetworkManager 重启、挂起/重启/关机 |
| **Files** | 截取显示器截图→在手机上查看/下载;从手机拖放文件到电脑 |
| **Config** | 在手机上编辑 `commands.json` / `modes.json`(以及上下文/语音)配置文件(JSON 验证)|
始终显示在标签页上方——一个**上下文条**,实时呈现当前发生了什么:活动通话(静音/切换到它)、正在播放的媒体+传输控制,以及焦点应用(YouTube / Brave / Teams)的应用内键盘控制,且*不会抢占焦点*。
除了标签页,还有两个功能:
- **发送到电脑** — PWA 注册为 Android **分享目标**:从手机任何应用分享图片、文本或链接,它会传送到电脑——图片保存到投递目录*并*放入剪贴板作为可粘贴的 PNG,文本放入剪贴板,裸链接直接打开。
- **环境思考器** — 闲置几分钟后,控制台变为磷光仪表盘:带峰值保持的指针拨盘遥测(“我不在时它是否爆表?”)、时钟、正在播放。
此外:屏幕数字键盘 PIN 登录、屏幕唤醒锁定、全屏横屏 PWA,以及登录时的暴力破解锁定。
---
## 架构
```
手机/笔记本电脑(浏览器 PWA)
│ HTTPS + WSS(仅 Tailscale)
▼
tailscale serve ──► FastAPI web app ── Unix socket ──► deckd
(真实 TLS 证书) (以你的用户身份运行) (动作名称) (以 root 身份运行)
│ │
hyprctl · pactl · ddcutil · grim · 固定白名单:
wf-recorder · uinput · WebRTC 特权命令
(cpupower, nvidia-smi,
systemctl, …)
```
两个进程:
- **`app/` — Web 应用** 以你的普通用户身份运行。它处理所有不需要 root 的操作:Hyprland 控制、音频、亮度、屏幕截图、虚拟输入、WebRTC 流媒体、文件传输。
- **`deckd/` — 一个微小的 root 助手**(仅标准库,无依赖),用于少数特权操作。Web 应用从不向其发送 shell 字符串——仅发送来自固定白名单的**动作名称**(`governor_performance`、`gpu_power_limit`、`suspend` 等),在 [`deckd/actions.py`](https://github.com/smit-shah-gg/phone-deck/blob/main/deckd/actions.py) 中验证后才执行。即使 Web 应用完全被攻破,爆炸半径也仅限于白名单中的动作,无参数注入风险。套接字权限为 `root:<group>` 模式 `0660`。
### 安全模型
- **仅在你的 tailnet 内可访问** — `tailscale serve` 使用真正的 Let's Encrypt 证书将其暴露在你的 MagicDNS 名称上;它从不绑定到 `0.0.0.0`。
- **JWT** 登录(PIN → 签名 cookie),反复失败后具有指数退避锁定。
- 特权操作隔离在白名单辅助守护进程之后。
- 上传路径经过基本名清理(不能逃离投递目录)。
> Web 应用*可以*运行你配置的 shell 命令并以你的用户身份注入输入——它本质上是你机器的远程控制。请将其保留在 tailnet 内,位于 PIN 之后,不要公开暴露。
---
## 要求
- 安装了 **Hyprland**(wlroots)、**PipeWire**(带 `pactl`/PulseAudio 兼容性)的 Linux
- **Python ≥ 3.11** 和 **[uv](https://docs.astral.sh/uv/)**
- **Tailscale**(为你的 tailnet 启用 HTTPS 证书)
- 各功能使用的 CLI 工具(根据需要安装):`hyprctl`、`pactl` / `pw-record` / `pw-play`、`playerctl`、`ddcutil`、`wf-recorder`、`grim`、`cpupower`、`nvidia-smi`、`kitty`(或你的终端)
- 你的用户需要属于 **`input`** 组(用于 `/dev/uinput`)和 **`i2c`** 组(用于 `ddcutil`)
- `aiortc` + PyAV(通过 `uv` 安装)用于音视频流
---
## 安装
```bash
git clone <your-repo-url> phone-deck
cd phone-deck
uv sync # creates .venv and installs dependencies
uv run python -m app.set_pin # set your unlock PIN
```
### 1. root 助手(`deckd`)
首先编辑 `systemd/deckd.service` — 设置路径,并将 `DECK_SOCKET_GROUP` 设置为你的用户所属的组(通常是你的主用户组):
```bash
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
```
`deckd` 运行系统 Python(仅标准库 — 无需虚拟环境)。
### 2. Web 应用
直接运行以进行开发:
```bash
uv run uvicorn app.main:app --host 127.0.0.1 --port 8765
```
…或作为用户服务运行。先编辑 `systemd/phone-deck.service` 中的路径,然后安装它。
**重要提示:** 在 Hyprland 并非通过 uwsm 启动的配置中,systemd 的 `graphical-session.target` 永远不会激活,因此用户单元不会在登录时自动启动。可靠的解决方法是导入会话环境后从 Hyprland 启动它——添加到你的 Hyprland 自启动配置中(例如 end-4 的 `~/.config/hypr/custom/execs.conf`):
```bash
exec-once = systemctl --user import-environment WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS XDG_CURRENT_DESKTOP && systemctl --user start phone-deck
```
### 3. 通过 Tailscale 实现 HTTPS
1. 启用证书:Tailscale 管理控制台 → **DNS → Enable HTTPS Certificates**。
2. 使用真实证书为应用提供前端:
```bash
sudo tailscale serve --bg 127.0.0.1:8765
```
3. 启用 HTTPS 后,在 Web 应用环境中设置 `DECK_SECURE_COOKIES=1`。
### 4. 在手机上安装
在手机上的 Chrome 中打开 `https://<your-host>.<your-tailnet>.ts.net/`(手机必须连接你的 tailnet),登录,然后 **⋮ → 安装应用**。它以全屏、横屏锁定模式启动,并保持屏幕常亮。需要 HTTPS 以支持 PWA 的 service worker + 唤醒锁定。
> 适用于任何 tailnet 设备——包括笔记本电脑(触控表面使用 Pointer Events,因此鼠标也能用)。WebRTC 流是单对端的(一次一个查看者)。
---
## 配置
所有运行时配置位于 `~/.config/phone-deck/` 中,并可在 **Config** 标签页中编辑(JSON 验证,保留 `.bak` 备份)。更改在下次操作时生效——无需重启。
### `commands.json` — 自定义 shell 操作
```json
[
{ "id": "reload_wm", "label": "Reload WM", "run": "hyprctl reload" },
{ "id": "deploy", "label": "Deploy site", "run": "cd /srv/app && ./deploy.sh",
"confirm": true, "timeout": 120 }
]
```
`run` 可以是字符串(通过 `bash -lc` 执行)或列表(argv,无 shell)。这些命令以你的用户身份运行——这是本地受信任的文件。
### `modes.json` — 场景宏
每个模式是通过 `hyprctl` 运行的一系列有序步骤:
- `launch {cmd, workspace, match?, once?}` — 在工作区上打开应用。使用 `match`(窗口类)+ `once` 可避免重复,如果窗口落到了错误的工作区,它会重新定位(对于浏览器 PWA 这类窗口来自现有进程的应用是必要的)。
- `close {match, workspaces?}` — 关闭指定类的窗口,可选地仅在列出的工作区上关闭。
- `move {match, workspace}` — 将匹配的窗口移动到指定工作区。
- `focus {workspace}` — 将工作区切换到视野中(例如将每个显示器重置到其主工作区)。
### Hyprland 工作区 → 显示器绑定
仅当工作区固定到显示器时,模式放置才是确定性的。将绑定添加到你的 Hyprland 配置中(将工作区编号和显示器名称调整为你的实际值):
```
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
```
### 主题
控制台采用**磷光终端**皮肤,具有四种可实时切换的颜色方案——**绿色**(默认)、**琥珀色**、**冰蓝色**和**自动**(从你的 matugen 壁纸调色板 `~/.local/state/quickshell/user/generated/colors.json` 中推导磷光色调)。在 **System** 标签页中切换;选择结果持久保存在 `~/.config/phone-deck/theme.json` 中。`app/theme.py` 将其作为 `/theme.css` 提供。环境思考器的空闲超时和诗文行也可在此配置。
---
## 流式传输工作原理
通过 **aiortc + PyAV** 实现双向 WebRTC,并通过子进程管道桥接到 PipeWire/Hyprland(避免脆弱的设备 I/O 绑定):
- **PC → 手机音频:** 使用 `parec` 捕获默认音频接收器的 `.monitor` → Opus。
- **手机 → PC 麦克风:** 接收手机麦克风 → 一个 PipeWire 空接收器,其监视器应用将其选为输入("Monitor of PhoneDeckMic")。
- **仅手机输出:** 将 PC 播放路由到虚拟接收器,以便在手机上收听时扬声器静音;停止后恢复。
- **PC → 手机视频:** `wf-recorder` 捕获显示器 → aiortc,具有质量预设(540p / 720p / 1080p),以在不稳定的链路上保持合理的比特率。
- **触控远程桌面:** 在流式视频上点击/拖拽 → 绝对光标定位(`hyprctl movecursor`)+ `uinput` 点击。点击 = 单击,拖拽 = 移动,长按 = 右键单击。
限制:延迟约 150–300 毫秒;仅在 PWA 前台时可靠(Android 会挂起后台标签页);使用耳机避免回声;一次只能有一个流对端;浏览器无法捕获手机*自身*的应用音频(仅麦克风)。
---
## 技术栈
- **后端:** FastAPI + Uvicorn、WebSockets、aiortc/PyAV、python-evdev、psutil、PyJWT
- **前端:** HTML + Tailwind(CDN)+ 原生 JS、PWA(manifest + service worker + wake-lock)
- **辅助守护进程:** 仅 Python 标准库
- **传输层:** Tailscale(`tailscale serve` 用于 HTTPS/WSS)
## 测试
```bash
uv run pytest
```
覆盖 `deckd` 白名单、JWT + 登录锁定、PIN 哈希、配置编辑器验证、模式解析、流媒体质量/命令逻辑以及输入键映射。实时 WebRTC/PipeWire/Hyprland 路径在开发过程中通过环回验证。
## 项目结构
```
app/ FastAPI web app
main.py routes + WebSockets
hypr.py Hyprland (snapshot, dispatch, event socket, cursor)
audio.py PipeWire device control + playerctl
audio_rtc.py WebRTC audio + screen video bridge
hid.py virtual keyboard/mouse via uinput
modes.py scene-macro engine
brightness.py ddcutil
grab.py screenshot + file upload
theme.py matugen → CSS
...
deckd/ root helper (stdlib only) + action allowlist
systemd/ service units
tests/ pytest suite
```
## 许可协议
MIT — 参见 `LICENSE`(如果希望使用其他许可协议,请在发布前添加)。
## 致谢
基于 [Hyprland](https://hyprland.org/)、[PipeWire](https://pipewire.org/)、[Tailscale](https://tailscale.com/)、[aiortc](https://github.com/aiortc/aiortc) 和 [end-4 配置文件](https://github.com/end-4/dots-hyprland) 构建。