基于WebSocket与macOS原生API的远程语音控制方案实现
1. 项目概述用手机远程语音控制 Cursor AI 的完整方案如果你和我一样日常重度依赖 Cursor 这样的 AI 编程助手并且习惯使用 Wispr Flow 这类“按住说话”的语音输入工具来提升编码效率那你一定遇到过这个痛点当屏幕上同时开着三四个 Cursor 窗口分别对应不同的项目或功能模块时想要快速地在它们之间切换并输入指令就变成了一场手忙脚乱的“舞蹈”。你需要先用鼠标点击激活目标窗口然后按住Fn键或你设置的快捷键开始说话说完松开再按Enter发送接着再去找下一个窗口……整个过程不仅打断了你的思路也让语音输入本该带来的流畅感大打折扣。这个名为Hand Control的项目就是为了彻底解决这个效率瓶颈而生的。它的核心思路极其巧妙把你的手机变成一个纯粹的、专为 Cursor 设计的“语音指令遥控器”。你不再需要触碰 Mac 的键盘或鼠标只需在手机屏幕上按住一个大按钮对着 AirPods 说话指令就会精准地发送到你选定的 Cursor 窗口中。整个过程中音频完全在 Mac 本地处理手机只负责传递控制信号没有任何隐私泄露的风险。这听起来像是一个复杂的系统但得益于 macOS 强大的原生 API 和清晰的架构设计实现起来却相当优雅。接下来我将为你深入拆解这个项目的每一个技术细节、实现原理并分享我在部署和使用过程中积累的实战经验。2. 核心架构与工作原理深度解析Hand Control 的架构清晰地划分了客户端手机和服务端Mac两者通过 WebSocket 进行实时、双向的通信。这种设计确保了极低的延迟和高效的指令传递。整个系统的运作可以看作是一次精心编排的“人机协作演出”手机是遥控器Mac 是执行引擎而 macOS 的各种原生 API 则是连接它们的桥梁。2.1 系统组件交互流程图解让我们先通过一个更技术化的视角来看清数据是如何流动的用户操作手机界面 │ ▼ 手机通过 WebSocket 发送指令如开始录音、选择窗口 │ ▼ Mac 服务端 (FastAPI) 接收指令 │ ├───▶ 调用 AppleScript获取并聚焦指定 Cursor 窗口 │ ├───▶ 调用 Accessibility API检查并记录当前聚焦文本框的初始状态 │ └───▶ 调用 CoreGraphics模拟按下“右 Option”键激活 Wispr Flow │ ▼ 用户通过 AirPods 对 Mac 说话Wispr Flow 开始转录并输入 │ ▼ CGEventTap 全局键盘事件监听器启动 │ 用户松开手机按钮 │ ▼ CoreGraphics 模拟释放“右 Option”键 │ ▼ CGEventTap 检测到 Wispr 停止输入如400ms 无新按键 │ ▼ Accessibility API 再次读取聚焦文本框与初始状态对比得出“转录增量文本” │ ▼ 服务端将“转录文本”通过 WebSocket 发回手机显示 │ ▼ 手机界面显示文本预览并亮起“提交”和“删除”按钮 │ ▼ 用户点击按钮触发对应的键盘模拟OptionEnter 或 CmdZ这个流程的每一个环节都至关重要并且充分利用了 macOS 的特性。例如使用AppleScript来操控 GUI 应用是 macOS 上历史悠久且稳定的方案它比一些基于坐标的点击模拟要可靠得多。而CoreGraphics框架下的CGEventPost函数是系统级模拟键盘事件的标准方式兼容性最好。Accessibility API则让我们能够以编程方式“看到”屏幕上哪个文本框获得了焦点以及其中的内容这是实现“转录预览”和“无输入框警告”功能的基础。2.2 关键技术选型背后的考量为什么是这些技术这里有一些你可能没想过的深层原因FastAPI WebSocket选择 FastAPI 而非 Flask 或 Django主要是看中其现代、高性能的特性以及对 WebSocket 的原生、简洁支持通过websockets库。对于这种需要实时双向通信的遥控器应用WebSocket 是比轮询Polling或长轮询Long-Polling高效得多的选择。服务端可以主动向手机推送状态更新如转录完成手机也能即时发送控制指令。纯前端 PWA手机端界面是一个纯粹的、单文件的 HTML 页面并配置了 PWA渐进式 Web 应用清单。这样做的好处是零构建依赖不需要 React、Vue 等框架启动和运行极其简单。原生应用体验添加到手机主屏幕后可以全屏、横屏锁定运行隐藏浏览器地址栏体验接近原生 App。跨平台iOS 的 Safari 和 Android 的 Chrome 都良好支持 PWA一份代码即可覆盖两大平台。右 Option 键作为激活键这是一个非常精妙的设计。首先它是一个独立的物理按键非常适合“按住”这个动作。其次在大多数开发环境和日常使用中“右 Option”键很少被绑定重要功能冲突概率极低。最后也是最重要的一点Fn键在系统层面的处理比较特殊通过程序模拟有时会不稳定或产生副作用而模拟一个标准的修饰键如 Option则要可靠得多。注意权限是这一切的基础。Accessibility辅助功能权限允许应用模拟按键和读取界面内容Automation自动化权限下的System Events权限则允许 AppleScript 控制其他应用如 Cursor。首次运行时务必按照提示授权否则核心功能将无法工作。3. 从零开始的详细部署与配置指南理解了原理我们开始动手。部署 Hand Control 的过程非常顺畅但有几个细节决定了最终的体验。我会带你走一遍我实际操作的完整流程并指出所有可能踩坑的地方。3.1 环境准备与初次运行首先确保你的环境符合要求macOSIntel 或 Apple Silicon 均可、Python 3.10、Cursor 和 Wispr Flow 已安装。然后打开终端执行克隆和运行git clone https://github.com/samwudeliris-sys/hand-control.git cd hand-control ./run.sh这个run.sh脚本做了三件事1) 在项目目录下创建 Python 虚拟环境.venv2) 使用pip安装requirements.txt中的依赖主要是 FastAPI、uvicorn 和 pyobjc3) 启动开发服务器。一切顺利的话你会看到类似下面的输出Hand Control running. Phone URL (stable): http://MacBook-Pro.local:8000 Phone URL (by IP): http://192.168.1.100:8000 Bookmark the stable URL on your phone — the .local hostname wont change when your Wi-Fi does.关键一步请务必使用那个以.local结尾的“稳定 URL”。这是你 Mac 的 BonjourmDNS主机名只要你的 Mac 和手机在同一个局域网内这个地址就是稳定的不会因为 IP 变化而失效。相比之下IP 地址可能会随着 DHCP 租约到期而改变。3.2 权限配置一次搞定一劳永逸首次运行后macOS 会弹出权限请求。这是最关键的一步如果处理不当后续功能会全部失效。辅助功能 (Accessibility) 权限这是第一个也是最重要的弹窗。它通常会说“终端”或“iTerm2”想要控制你的电脑。你必须点击“打开系统设置”然后在“隐私与安全性” - “辅助功能”列表中找到你正在使用的终端应用如 Terminal.app, iTerm.app, 或 Cursor 的内置终端并勾选它。常见坑点如果你在授权后功能依然不正常请完全退出终端应用并重新打开再运行./run.sh。有时系统需要重启应用才能加载新的权限。自动化 (Automation) 权限当服务端第一次尝试通过 AppleScript 获取 Cursor 窗口列表时会弹出第二个请求“终端”想要控制“系统事件”。同样点击“打开系统设置”在“隐私与安全性” - “自动化”中找到你的终端应用确保其下的“系统事件”被勾选。补救措施如果不小心点了“不允许”可以手动进入上述路径勾选。勾选后通常需要重启 Hand Control 服务端在终端按CtrlC停止再运行./run.sh。3.3 Wispr Flow 配置同步为了让 Hand Control 能激活 Wispr Flow两者的热键必须匹配。打开 Wispr Flow 的设置界面找到“激活热键”或类似的设置项。将其修改为Right Option键盘右侧的 Option/Alt 键。同时检查“输入设备”是否已设置为你的 AirPods 或系统默认麦克风如果 AirPods 已是默认设备。这个配置只需要做一次。之后当你在手机上按住按钮时Hand Control 服务端就会模拟按下右 Option 键从而触发 Wispr Flow 开始聆听。3.4 手机端配置与优化体验在手机的浏览器Safari 或 Chrome中输入刚才记下的.local地址例如http://MacBook-Pro.local:8000。页面加载后将手机横屏放置界面会自动适配为遥控器布局。为了获得最佳体验我强烈建议将其“添加到主屏幕”在 Safari (iOS) 中点击底部的分享按钮在弹出菜单中找到“添加到主屏幕”选项。你可以将名称修改为“Hand Control”。在 Chrome (Android) 中点击右上角菜单选择“添加到主屏幕”或“安装应用”。完成之后你的手机主屏幕上就会出现一个 Hand Control 的图标。点击它应用会以全屏、横屏锁定的模式打开没有任何浏览器界面元素体验和原生 App 几乎无异。4. 手机遥控器界面详解与高效使用技巧现在你的手机已经变成了一个功能完备的 Cursor 语音遥控器。让我们仔细剖析一下这个界面的每一个部分并分享一些提升效率的使用技巧。4.1 界面布局与核心功能区横屏状态下的界面逻辑清晰分为几个主要区域顶部窗口选择条这里以“药丸”或“卡片”的形式动态显示你当前 Mac 上所有打开的 Cursor 窗口通常以窗口标题如项目名来区分。轻点任意一个窗口卡片即可将其选中为当前指令接收窗口。选中后该窗口会在你的 Mac 上被自动提到前台raise让你直观地确认目标。预设指令按钮区位于窗口选择条下方是一排可自定义的快捷按钮如“Push”、“Continue”、“Fix”等。这是提升效率的利器后面会详细讲如何定制。中央控制区这是最重要的区域。中央巨大的“HOLD TO TALK”按钮就是你的录音键。长按它直到你说完话再松开。在长按期间如果系统检测到当前选中的 Cursor 窗口内没有获得焦点的文本框比如你点错了窗口或者光标在代码编辑器里按钮上方会出现一个红色的“No text field focused”提示条这时你应该松开手先去 Mac 上点击一下 Cursor 的聊天输入框再重新操作。侧边导航按钮屏幕左右边缘的“◀”和“▶”按钮可以让你快速在窗口列表中循环切换比在顶部条上精确点击有时更快。底部操作区在你松开“HOLD TO TALK”按钮并且 Wispr Flow 完成输入后这个区域会激活。左侧的“删除 (✕)”按钮发送CmdZ撤销刚才的语音输入右侧的“提交 (↵)”按钮发送OptionEnter将输入的内容提交给 Cursor AI 处理。在等待 Wispr 输入完成时这两个按钮会呈现呼吸脉冲效果一旦就绪会变为实心可点击状态。4.2 预设指令打造你的专属快捷命令库预设指令Presets是我认为这个工具设计最出彩的地方之一。它解决了语音输入中的一个常见问题有些短句、指令你每天要重复几十遍比如“继续”、“运行测试”、“提交代码”。每次都口述它们既浪费时间也容易口误。Hand Control 允许你完全自定义这排按钮。项目根目录下有一个presets.example.json文件复制一份并重命名为presets.json该文件已被.gitignore忽略不会影响版本控制cp presets.example.json presets.json然后编辑presets.json。它的结构是一个 JSON 数组每个对象代表一个按钮[ { label: 重构, text: 请重构这个函数使其更简洁并添加注释, submit: queue }, { label: 解释, text: 请用中文解释一下这段代码的逻辑, submit: send }, { label: Commit, text: commit with message: fix: resolve null pointer issue, submit: none } ]每个预设有三个关键属性label: 显示在按钮上的文字要简短。text: 点击按钮后实际输入到 Cursor 输入框中的文本。submit: 输入文本后的行为。queue默认会接着按OptionEnter将指令加入队列send会按普通的Enter立即发送可能中断 AI 当前任务none则只输入文本不发送方便你进一步编辑。我的实战经验我会为不同的项目类型创建不同的预设文件并通过环境变量HC_PRESETS_PATH来指定。例如在做前端项目时我的预设多是关于 UI 组件和样式而在后端项目时则是数据库查询和 API 逻辑。灵活切换预设集能让这个遥控器更加趁手。5. 高级配置与深度定制Hand Control 的默认配置已经足够好用但它的代码结构非常清晰也为我们留下了充足的定制空间。你可以根据个人工作流进行微调。5.1 调整语音输入结束检测灵敏度这是影响体验的一个重要参数。在server/main.py文件中找到这两个变量ENTER_IDLE_MS 400 # 判定 Wispr 输入结束的静默时长毫秒 ENTER_MAX_WAIT_S 8.0 # 安全上限超过此时长则强制结束等待ENTER_IDLE_MS: 默认 400ms。意思是当系统检测到超过 400 毫秒没有新的键盘事件时就认为 Wispr 已经输入完毕。如果你的网络或 Wispr 处理稍慢可以适当调高这个值比如设为600或800避免按钮过早亮起此时转录可能还未完成。反之如果你觉得松开按钮到按钮亮起的延迟太长可以调低。ENTER_MAX_WAIT_S: 默认 8 秒。这是一个安全机制防止因为某些异常如 Wispr 卡住、权限问题导致事件监听失效而无限期等待。一般不需要修改。5.2 修改目标应用与激活热键虽然项目叫“Hand Control for Cursor”但其核心机制并不局限于 Cursor。更换目标应用如果你想控制 VS Code、iTerm2 或其他任何应用只需修改server/cursor_windows.py文件。找到里面使用Cursor字符串的 AppleScript 语句将其替换为目标应用的名字例如CodeVS Code或iTerm2。需要注意的是新应用的窗口获取和聚焦逻辑可能需要微调。更换激活热键如果你使用的不是 Wispr Flow或者其他语音工具使用了不同的热键你需要修改两处在你的语音工具设置中将激活方式改为一个特定的修饰键例如Right Control右 Control 键。在server/key_control.py文件中找到KEYCODE_RIGHT_OPTION这个常量。你需要将其值改为目标键的 macOS 键码。可以在网上搜索 “macOS key codes” 找到对应列表。例如右 Control 键的键码可能是62不同键盘布局可能不同需确认。5.3 网络与端口配置默认服务运行在 8000 端口。如果该端口被占用启动时会报错。你可以通过环境变量指定其他端口PORT8080 ./run.sh服务启动后它会绑定到0.0.0.0这意味着你局域网内的任何设备都能访问它。这是一个重要的安全提示请仅在可信的私人网络中使用。如果你需要在公司网络或咖啡馆等环境使用强烈建议结合Tailscale或ZeroTier这类虚拟组网工具它们能为你创建一个加密的虚拟局域网并自带身份验证可以安全地从外部网络访问你家中的 Mac。6. 故障排除与实战经验分享即使按照步骤操作也可能会遇到一些问题。下面是我在长期使用中遇到的一些典型情况及其解决方法。6.1 权限问题导致功能异常这是最常见的问题根源。症状手机能连接但点击窗口没反应或者按住说话后 Mac 没任何动静服务器日志可能有Failed to create event tap错误。排查打开“系统设置” - “隐私与安全性” - “辅助功能”确认你使用的终端应用如 Terminal, iTerm2, Cursor Terminal已被勾选。勾选后必须完全退出该终端应用并重新打开然后再次运行./run.sh。在同一设置页面检查“自动化”权限确保终端应用下的“系统事件”已被勾选。如果问题依旧可以尝试重启 Mac。有时系统权限服务的缓存会导致问题。6.2 网络连接问题症状手机浏览器无法打开.local地址。排查确认在同一网络确保手机和 Mac 连接的是同一个 Wi-Fi。有些公共或企业网络会启用“客户端隔离”阻止设备间互访。可以尝试用手机开热点让 Mac 连接或者使用 Tailscale。尝试 IP 地址使用服务器启动时打印的 IP 地址 URL如http://192.168.1.100:8000进行访问。检查防火墙在 Mac 的“系统设置” - “网络” - “防火墙”中确保没有阻止 Python 或终端应用的入站连接。初次运行时系统可能会询问是否允许请点击允许。6.3 语音输入相关的问题症状按住手机按钮后Wispr Flow 没有启动录音看不到录音指示。排查确认 Wispr Flow 正在运行菜单栏有图标。核对 Wispr Flow 的激活热键是否严格设置为Right Option。在 Mac 上打开一个文本编辑器如备忘录尝试用 Hand Control 操作看是否能正常模拟按键并输入。这可以排除 Cursor 特定问题。症状“提交”或“删除”按钮反应不灵或者转录文本显示不全。排查调整ENTER_IDLE_MS参数。如果 Wispr 转录较长的内容时输入速度慢可以适当调大这个值。检查 Cursor 的聊天输入框是否确实获得了焦点。有时窗口聚焦了但光标可能不在输入框内。确保在开始前手动点击一下输入框。6.4 我的独家使用心得双设备工作流我通常将 Mac 屏幕用于全屏代码编辑而将 Cursor 聊天窗口放在 iPad通过 Sidecar 随航或另一台显示器上。这样Hand Control 手机遥控器 分离的聊天窗口让我能完全专注于主屏幕的代码视线无需来回切换语音指令的体验无缝衔接。预设分组管理如前所述我为不同语言和框架创建了不同的presets.json文件。我会写一个简单的 shell 脚本根据当前项目目录快速切换预设集让快捷指令始终贴合上下文。结合快捷键Hand Control 解决了窗口切换和语音触发的痛点但 Cursor 内部的一些常用操作如CmdK快速新建聊天我仍然用键盘快捷键。两者结合形成了“手机遥控宏观流程键盘控制微观操作”的高效模式。备用方案虽然.local地址很稳定但在某些网络环境下我会将 Mac 的局域网 IP 设为静态然后在手机主屏幕书签里也保存一份 IP 地址的链接作为备用。这个项目的优雅之处在于它没有尝试做一个大而全的语音控制平台而是精准地解决了一个特定场景下的高频痛点。通过组合 macOS 上稳定、成熟的底层 API它构建了一个轻量、响应迅速且极其实用的工具。从克隆项目到手机遥控器开始工作整个过程可能不超过 10 分钟但带来的效率提升却是持续的。如果你也厌倦了在多个 AI 聊天窗口间手动切换的繁琐强烈建议你花一点时间部署体验一下它很可能成为你开发工具链中一个令人惊喜的“效率倍增器”。