将闲置的安卓手机(或任何带浏览器的设备)变成你 Linux 工作站的自主托管控制台和远程桌面——通过你的私有 Tailscale 网络,无需第三方应用。
它始于“拿旧手机能做什么?”,最终演变为一个 Hyprland 设备的控制平面:窗口/工作区控制、场景宏、带本地 LLM 助手的按键通话语音路由、上下文感知的应用内控制、双向音频+屏幕流式传输、触控远程桌面、虚拟键盘/鼠标、文件传输+手机到电脑共享,以及实时遥测——所有这些都通过一个已安装的 PWA 提供服务,外观为磷光终端风格。
关于可移植性的说明。 这是为特定配置构建的:基于 Arch 的 Linux(Garuda)+ Hyprland + PipeWire + NVIDIA + Tailscale,并使用了 end-4 / illogical-impulse 配置文件。架构是通用的,但许多集成是环境特定的(显示器名称、工作区绑定、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)的应用内键盘控制,且不会抢占焦点。
除了标签页,还有两个功能:
此外:屏幕数字键盘 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 中验证后才执行。即使 Web 应用完全被攻破,爆炸半径也仅限于白名单中的动作,无参数注入风险。套接字权限为 root:<group> 模式 0660。tailscale serve 使用真正的 Let's Encrypt 证书将其暴露在你的 MagicDNS 名称上;它从不绑定到 0.0.0.0。Web 应用可以运行你配置的 shell 命令并以你的用户身份注入输入——它本质上是你机器的远程控制。请将其保留在 tailnet 内,位于 PIN 之后,不要公开暴露。
pactl/PulseAudio 兼容性)的 Linuxhyprctl、pactl / pw-record / pw-play、playerctl、ddcutil、wf-recorder、grim、cpupower、nvidia-smi、kitty(或你的终端)input 组(用于 /dev/uinput)和 组(用于 )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
deckd)首先编辑 systemd/deckd.service — 设置路径,并将 DECK_SOCKET_GROUP 设置为你的用户所属的组(通常是你的主用户组):
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
deckd 运行系统 Python(仅标准库 — 无需虚拟环境)。
直接运行以进行开发:
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):
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
sudo tailscale serve --bg 127.0.0.1:8765
DECK_SECURE_COOKIES=1。在手机上的 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 操作[
{ "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 配置中(将工作区编号和显示器名称调整为你的实际值):
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 绑定):
parec 捕获默认音频接收器的 .monitor → Opus。wf-recorder 捕获显示器 → aiortc,具有质量预设(540p / 720p / 1080p),以在不稳定的链路上保持合理的比特率。hyprctl movecursor)+ uinput 点击。点击 = 单击,拖拽 = 移动,长按 = 右键单击。限制:延迟约 150–300 毫秒;仅在 PWA 前台时可靠(Android 会挂起后台标签页);使用耳机避免回声;一次只能有一个流对端;浏览器无法捕获手机自身的应用音频(仅麦克风)。
tailscale serve 用于 HTTPS/WSS)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、PipeWire、Tailscale、aiortc 和 end-4 配置文件 构建。
i2cddcutilaiortc + PyAV(通过 uv 安装)用于音视频流