1. 项目概述用手机远程语音控制 Cursor AI 编程如果你和我一样日常重度依赖 Cursor 这类 AI 编程工具并且习惯使用 Wispr Flow 这样的语音输入工具来口述代码和指令那你一定遇到过这个痛点在多个 Cursor 窗口之间频繁切换、点击输入框、按住快捷键说话、再按回车提交……这套流程不仅打断了你的思路还让你的双手无法离开键盘和鼠标去干点别的比如喝口水或者翻翻文档。hand-control这个项目就是为了解决这个“最后一米”的交互问题而生的。它的核心想法极其简单却又非常巧妙把你的手机变成一个纯粹的、带屏幕的“遥控器”。这个遥控器只有一个核心功能——让你能在一堆打开的 Cursor 窗口中快速选择目标然后按住屏幕上的一个大按钮对着你的 AirPods或其他连接 Mac 的麦克风说话松开后语音就会被转录并输入到选中的 Cursor 窗口里。整个过程你的眼睛可以一直盯着 Mac 的大屏幕双手也解放了。这听起来像是一个复杂的系统集成但它的实现却相当优雅完全基于 macOS 的原生能力没有复杂的中间件。手机端只是一个纯粹的、运行在浏览器里的 PWA渐进式 Web 应用界面通过 WebSocket 与 Mac 本地的一个 Python 服务器通信。所有繁重的工作——窗口管理、按键模拟、监听输入状态——都由 Mac 本地的 Python 脚本通过调用 AppleScript、CoreGraphics、Accessibility API 等系统接口来完成。最关键的是所有音频数据都只在你的 Mac 和耳机之间传输手机端只是一个发送“开始录音”和“停止录音”指令的遥控器完全不接触音频流这在隐私和延迟上都做到了最优。接下来我会带你从零开始彻底拆解这个项目的设计思路、实现细节、配置技巧以及我踩过的那些坑。无论你是想直接使用这个工具提升效率还是想学习如何将 macOS 的多种系统 API 组合起来实现自动化这篇文章都会给你带来实实在在的收获。2. 核心设计思路与架构拆解2.1 为什么是“手机遥控器”这个形态在构思任何工具时形态选择决定了用户体验的上限。最初我也考虑过用物理脚踏开关、额外的键盘宏键甚至 Apple Watch。但最终选择手机是基于以下几个硬核的考量屏幕即信息我需要一眼看到所有打开的 Cursor 窗口项目名并能快速切换。物理按钮无法提供这种可视化反馈。手机的屏幕完美解决了“状态可见性”问题。触控即交互按住说话是一个天然的、符合直觉的触屏交互。相比在键盘上找一个不常用的物理键如Fn长按在手机屏幕上按住一个大按钮心理负担和误操作率都更低。零额外硬件几乎人人都有手机无需购买、配置任何新设备。利用现有设备达成专业目的是极客精神的体现。网络隔离的可行性通过 WebSocket 在局域网内通信延迟可以低至毫秒级完全满足“按下-开始录音”的即时性要求。同时由于音频不走网络复杂性和安全性风险骤降。这个设计锚定了整个项目的基调轻前端重后端手机负责显示和触发Mac 负责执行和反馈。2.2 技术栈选型背后的逻辑项目的技术栈非常精简每一项选择都经过了深思熟虑后端FastAPI Uvicorn为什么不是 FlaskFastAPI 对 WebSocket 的原生支持更现代、性能更好异步处理能力可以轻松应对多个并发的连接事件虽然这里通常只有一个手机连接。其自动生成的交互式 API 文档/docs在调试时也非常有用。为什么是 Uvicorn它是 ASGI 服务器的事实标准与 FastAPI 搭配使用简单可靠性能足以应对这种轻量级应用。系统交互PyObjC 原生 API这是项目的灵魂。Python 通过 PyObjC 这个桥梁可以直接调用 macOS 的 Objective-C 框架。ApplicationServices(CoreGraphics)用于模拟键盘事件按下/释放Right Option发送Enter等。这是实现“遥控按键”的基础。Quartz(CGEventTap)用于全局监听键盘事件。这是实现“检测 Wispr 何时停止输入”的关键。通过事件钩子我们可以判断用户停止说话后转录软件是否也停止了输入。AppKit(Accessibility API)用于获取当前聚焦的文本字段内容。这是实现“转录预览”和“无输入框警告”功能的基础。没有这个权限我们就不知道 Wispr 到底把字打到哪里去了。subprocessosascript用于执行 AppleScript来获取和聚焦 Cursor 的窗口列表。AppleScript 是 macOS 上与应用交互的“古老”但极其稳定的方式。前端纯 HTML/CSS/JS (PWA)为什么不用 React/Vue过度设计。这个遥控器界面极其简单几个按钮、一个列表、一个状态显示。引入任何前端框架都会增加构建复杂度背离了“一键运行”的初衷。纯原生三件套足够快也足够实现所需的交互点击、长按、WebSocket 通信。PWA 的价值通过manifest.json和 Service Worker本项目虽未使用但具备条件可以将网页“安装”到手机主屏幕实现全屏、去浏览器地址栏的类原生应用体验。这对于一个遥控器应用来说体验提升是巨大的。通信WebSocket为什么不是 HTTP 轮询遥控器场景是典型的“服务器主动推送”模型。手机按下按钮Mac 开始录音Mac 检测到输入结束需要主动通知手机更新界面显示转录文本、点亮按钮。这种双向、实时的通信WebSocket 是唯一正确的选择比 HTTP 轮询高效、实时得多。这个技术栈组合确保了项目在拥有强大系统级能力的同时保持了极简的依赖和部署复杂度。一个git clone加一个./run.sh就能跑起来这是工具类项目成功的关键。2.3 核心工作流程与数据流理解了“是什么”和“用什么做”之后我们深入到“怎么做”。下面这张序列图清晰地描绘了一次完整的语音指令从触发到执行的完整闭环sequenceDiagram participant Phone as 手机 (PWA) participant Server as Mac 服务器 (FastAPI) participant System as macOS 系统 participant Wispr as Wispr Flow participant Cursor as Cursor App Note over Phone, Cursor: 1. 准备阶段 Phone-Server: WebSocket 连接 Server-System: AppleScript 获取 Cursor 窗口列表 System--Server: 返回窗口信息 Server--Phone: 发送窗口列表用于显示 Note over Phone, Cursor: 2. 用户触发录音 Phone-Server: WebSocket 消息: “开始录音” Server-System: Accessibility API: 检查并记录当前聚焦文本框内容 (基线) alt 无聚焦文本框 Server--Phone: 实时返回警告状态 Phone--User: 显示红色“未聚焦”警告 User-Phone: 松开按钮 (取消操作) else 有聚焦文本框 Server-System: CoreGraphics: 模拟按下 Right Option 键 System-Wispr: 激活 Wispr Flow 录音 Note over User, Wispr: 用户对着 AirPods 说话... Wispr-Cursor: 将语音转录为文本并输入 end Note over Phone, Cursor: 3. 用户结束录音 User-Phone: 松开按钮 Phone-Server: WebSocket 消息: “结束录音” Server-System: CoreGraphics: 模拟释放 Right Option 键 Server-System: CGEventTap: 开始监听键盘空闲事件 loop 每 50ms 检查一次 System--Server: 报告键盘事件 Server--Server: 判断是否空闲超过 400ms end Server-System: Accessibility API: 再次读取聚焦文本框内容 Server--Server: 计算新内容与基线的差值 (即转录文本) Server--Phone: 发送转录文本预览 Phone--User: 显示转录文本和可用的 Submit/Delete 按钮 Note over Phone, Cursor: 4. 用户后置操作 User-Phone: 点击 Submit 或 Delete Phone-Server: WebSocket 消息: “提交”或“删除” alt 点击 Submit Server-System: CoreGraphics: 模拟按下 OptionEnter System-Cursor: 将输入内容提交给 Cursor AI Agent (排队) else 点击 Delete Server-System: CoreGraphics: 模拟按下 CmdZ System-Cursor: 撤销上一步输入 end这个流程中有几个精妙的设计点值得展开“无输入框”的实时警告在按下按钮的瞬间服务器就通过 Accessibility API 检查当前焦点。如果没有可输入的文本框它会立即通知手机显示红色警告。这避免了用户对着空气说了一大段话才发现没打上字的尴尬。这是一个非常重要的用户体验细节它利用了 WebSocket 的双向实时通信能力。“转录完成”的检测机制如何知道 Wispr 什么时候打完了字项目没有采用简单的固定延时而是使用了CGEventTap来全局监听键盘事件。当超过ENTER_IDLE_MS默认 400ms没有检测到新的键盘事件时就认为转录完成了。这种方法比固定延时更精准能适应不同长度、不同速度的转录。同时它还设置了一个安全上限ENTER_MAX_WAIT_S默认 8 秒防止因某些异常导致无限等待。“排队”与“打断”的提交策略这是深度集成 Cursor 工作流的设计。OptionEnter是 Cursor 的“排队”快捷键。如果 AI Agent 正在忙碌你的指令会进入队列等待执行而不是粗暴地打断当前任务。这符合协作式的工作流。当然你也可以在配置中改为普通的Enter来立即发送可能打断当前任务。本地网络与.local域名服务器启动时会打印两个地址IP 地址和.local(Bonjour/mDNS) 地址。强烈建议使用.local地址添加到手机主屏幕。因为你的 Mac 在 DHCP 网络中的 IP 可能会变但它的 Bonjour 主机名如MacBook-Air.local通常是稳定的。这是一条提升长期使用体验的宝贵经验。3. 从零开始的详细配置与实操指南理论讲透了我们上手实操。假设你有一台 Mac安装了 Cursor 和 Wispr Flow并且手机和 Mac 在同一个 Wi-Fi 下。3.1 环境准备与一键启动项目的入门体验做得非常好几乎是一键式的。# 1. 克隆仓库 git clone https://github.com/samwudeliris-sys/hand-control.git cd hand-control # 2. 运行启动脚本 ./run.sh这个run.sh脚本做了以下几件事你可以打开它看看检查 Python 版本需要 3.10。在项目目录下创建一个 Python 虚拟环境.venv/隔离依赖。使用pip安装requirements.txt中的依赖主要是fastapi,uvicorn,pyobjc-framework-Quartz等。启动 Uvicorn 服务器运行server/main.py并绑定到0.0.0.0:8000。第一次运行终端会输出类似下面的信息Hand Control running. Phone URL (stable): http://MacBook-Air.local:8000 Phone URL (by IP): http://192.168.1.42:8000 Bookmark the stable URL on your phone — the .local hostname wont change when your Wi-Fi does. 注意先别急着用手机打开接下来需要配置权限和 Wispr否则功能无法正常工作。3.2 关键配置Wispr Flow 热键这是整个链条的触发开关。Hand Control 默认模拟的是Right Option键键盘右侧的 Alt/Option 键。打开 Wispr Flow 的设置菜单栏图标 - Preferences。找到Activation Hotkey或类似的设置项。将其设置为Right Option。请确保你选择的是“Right Option”而不是普通的“Option”因为代码里模拟的是特定的右侧键码。同时在 Wispr 的音频设置中确保输入设备是你的 AirPods 或 Mac 默认的麦克风。为什么是 Right Option独立性它是一个独立的物理键很少被其他应用程序全局绑定冲突可能性小。可模拟性通过CGEventPost可以稳定、准确地模拟这个键的按下和释放事件。手感位于键盘右侧对于右手操作鼠标的用户来说左手小指或拇指可以轻松找到并长按虽然现在这个动作被手机按钮替代了。3.3 必须授予的 macOS 系统权限由于 Hand Control 需要模拟按键和监听系统事件它要求两个关键的权限。这是很多类似工具卡住的第一步。3.3.1 辅助功能 (Accessibility) 权限这是最重要的一个权限没有它核心功能将失效。打开系统设置 - 隐私与安全性 - 辅助功能。你会看到一个应用列表。你需要找到并勾选你用来运行./run.sh的那个终端应用例如终端 Terminal.app、iTerm2、或是 Cursor 的内置终端。如果列表里没有可以先尝试运行一次./run.shmacOS 可能会自动弹出授权请求。如果还没有重启一下终端应用再运行脚本试试。授权后务必完全退出并重新启动 Hand Control 服务器在终端里按CtrlC然后再次运行./run.sh。这个权限需要重启应用才能生效。这个权限的作用允许 Python 脚本通过 Accessibility API 读取当前聚焦的文本字段内容用于转录预览和警告同时也是CGEventTap监听键盘事件所必需的。3.3.2 自动化 (Automation) 权限这个权限用于控制其他应用这里是 Cursor。第一次运行 Hand Control 并尝试获取 Cursor 窗口列表时macOS 会弹窗询问“终端.app”想要控制“系统事件”。一定要点击“好”或“允许”。如果不小心点了拒绝可以去系统设置 - 隐私与安全性 - 自动化里面修复。在左侧找到你的终端应用如“终端”然后在右侧勾选“系统事件”。这个权限的作用允许 AppleScript 通过System Events来获取和操作 Cursor 的窗口。3.4 手机端配置与使用技巧现在用你的手机浏览器Safari 或 Chrome打开终端里打印的.local地址比如http://MacBook-Air.local:8000。横屏使用页面设计为横屏遥控器布局请将手机横过来以获得最佳体验。添加到主屏幕iOS Safari点击底部的分享按钮向下滚动找到“添加到主屏幕”。你可以重命名比如就叫“Hand Control”然后点击添加。Android Chrome点击右上角菜单选择“添加到主屏幕”或“安装应用”。PWA 优势添加后图标会出现在主屏幕。点击它启动应用会以全屏模式运行没有浏览器地址栏和工具栏看起来和感觉上都像一个真正的原生遥控器 App。界面布局解析顶部窗口列表显示所有打开的 Cursor 窗口点击即可选中并自动将该窗口提到 Mac 前台。预设按钮栏一系列可自定义的快捷短语按钮如“Continue”、“Fix”、“Push”等一键输入常用指令。中央控制区巨大的“HOLD TO TALK”按钮。长按开始录音松开结束。长按时如果 Mac 检测到没有聚焦的输入框这里会显示红色警告。侧边箭头左右箭头可以快速在窗口列表中切换选中项。底部操作区录音结束后这里会显示转录的文本预览并且“DELETE”和“SUBMIT”按钮会亮起供你后续操作。4. 高级配置与个性化定制项目提供了丰富的配置选项让你可以根据自己的习惯进行微调。4.1 调整核心行为参数编辑server/main.py文件找到以下变量ENTER_IDLE_MS 400 # 判定 Wispr 输入结束的静默时间毫秒 ENTER_MAX_WAIT_S 8.0 # 等待输入结束的最大安全时间秒ENTER_IDLE_MS如果你的 Wispr 在说完后“思考”或输入速度较慢可以适当调大这个值比如600或800。ENTER_MAX_WAIT_S这是一个安全网。如果因为某些原因CGEventTap没有正确检测到输入结束超过这个时间后系统会强制进行下一步尝试提交。一般不需要修改。4.2 自定义预设快捷指令预设功能是提升效率的利器。项目自带了一些例子但你可以完全自定义。创建自定义文件cp presets.example.json presets.json编辑presets.json用任何文本编辑器打开这个文件。它是一个 JSON 数组每个对象代表一个按钮。[ { label: 解释代码, text: 请解释一下这段代码的逻辑和作用, submit: send }, { label: 重构, text: 请重构以下代码使其更清晰、可维护, submit: queue }, { label: 写测试, text: 为这个函数/模块编写完整的单元测试。, submit: queue }, { label: 总结变更, text: 总结一下自上次提交以来的所有代码变更。, submit: none } ]label: 显示在按钮上的文字。text: 点击按钮后实际输入到 Cursor 输入框的文字。submit: 输入文字后的行为。queue默认发送OptionEnter排队send发送普通Enter立即执行可能打断none只输入文字不提交方便你继续编辑。重启服务器修改后需要重启./run.sh才能生效。 提示你可以创建多个预设文件并通过环境变量HC_PRESETS_PATH来指定使用哪一个方便在不同项目或场景间切换。4.3 适配其他应用或语音工具虽然项目名为“Hand Control for Cursor”但其架构是通用的。适配其他编辑器/IDE修改server/cursor_windows.py文件。将里面所有出现Cursor字符串的地方替换成目标应用的名字例如Code(VS Code) 或iTerm2。注意应用名必须与它在 macOS 活动监视器里显示的名称完全一致。使用其他语音工具只要你的语音工具支持“按住某个热键开始录音松开停止”的模式就可以使用。你只需要在工具的设置里将激活热键设置为Right Option或你在server/key_control.py里修改后的其他键并确保它能将转录文本输入到当前聚焦的应用程序即可。4.4 修改服务器端口如果 8000 端口被占用启动时会报错。你可以通过环境变量指定新端口PORT8080 ./run.sh服务器启动后会显示新的访问地址。5. 故障排除与实战经验分享在实际使用和调试过程中我遇到了不少问题也总结出一些经验。5.1 常见问题速查表问题现象可能原因解决方案服务器启动失败提示端口占用端口 8000 已被其他程序使用使用PORT8080 ./run.sh换一个端口。或用lsof -i:8000找出占用进程并停止。手机无法打开.local地址网络 mDNS/Bonjour 被阻止常见于公司网络1. 尝试使用终端输出的 IP 地址访问。2. 检查手机和 Mac 是否在同一 Wi-Fi 网络。3. 尝试使用手机热点连接。手机能打开页面但显示“No Cursor windows detected”1. Cursor 没运行。2. 自动化权限未授予。1. 打开 Cursor。2. 去系统设置 - 隐私与安全性 - 自动化确保你的终端应用有权控制“系统事件”。长按按钮手机显示录音但 Mac 没反应Wispr 未启动1. Wispr Flow 未运行。2. Wispr 的热键不是Right Option。3. 辅助功能权限未授予。1. 启动 Wispr Flow。2. 检查并修改 Wispr 的热键设置为Right Option。3. 检查并授予终端辅助功能权限然后重启服务器。录音后Submit/Delete 按钮一直不亮起1.CGEventTap创建失败权限问题。2. Wispr 的输入方式可能不是模拟键盘。1.这是最常见的问题确保辅助功能权限已授予并重启了服务器。查看服务器日志是否有Failed to create event tap错误。2. 确保 Wispr 的输出目标是“模拟键盘输入”。Delete 按钮无法完全撤销输入CmdZ 的撤销粒度与 Wispr 的输入方式不匹配。多次点击 Delete 按钮即发送多次 CmdZ直到完全撤销。或者在 Wispr 设置中尝试调整其输入模式。预设按钮不生效或显示旧内容presets.json格式错误或未加载。1. 检查presets.json的 JSON 格式是否正确可以用在线校验工具。2. 修改后记得重启服务器。3. 访问http://你的Mac.local:8000/presets查看服务器当前加载的预设内容。5.2 核心调试技巧查看服务器日志运行./run.sh的终端窗口是所有信息的来源。注意观察启动时的权限错误提示以及操作过程中的打印信息。测试系统权限如果你怀疑权限问题可以手动测试。打开终端输入python3 -c import Quartz; tap Quartz.CGEventTapCreate(...)需要补全参数看是否会触发权限请求或报错。更简单的方法是运行一个已知需要辅助功能权限的脚本片段。验证 WebSocket 连接在手机的浏览器开发者工具中对于 PWA 较难或者使用一个简单的 WebSocket 客户端工具连接服务器地址ws://MacBook-Air.local:8000/ws可以查看消息往来判断是前端还是后端问题。模拟按键测试你可以临时修改server/key_control.py中的代码写一个简单的测试函数来模拟按下Right Option看看 Wispr 是否会响应从而隔离问题。5.3 安全使用须知项目作者在 README 中已经强调这里我再结合自己的理解重申一下局域网暴露服务器默认绑定0.0.0.0:8000意味着同一 Wi-Fi 下的任何设备都能访问这个遥控器。请仅在可信的网络环境中使用例如家庭网络或个人热点。无认证工具本身没有用户名密码。任何能访问到这个页面的人都可以控制你的 Cursor 输入。建议的加固方案如果你需要在公司等相对复杂的网络中使用强烈建议使用Tailscale或ZeroTier组建一个虚拟局域网。将你的 Mac 和手机加入同一个虚拟网络然后通过虚拟网络 IP 访问。这样既实现了加密通信又相当于增加了一层网络隔离认证。6. 项目源码导读与扩展思路如果你不满足于使用还想学习它的代码实现甚至进行二次开发这里提供一个简单的导读。6.1 核心模块解析server/main.py大脑。FastAPI 应用入口处理 WebSocket 连接协调各个模块工作。它定义了整个状态机空闲、录音中、等待转录、就绪。server/cursor_windows.py窗口管家。通过 AppleScript 与 Cursor 交互获取所有窗口的标题通常包含项目路径和 ID并可以根据 ID 将指定窗口提到前台。server/key_control.py键盘手。利用Quartz.CoreGraphics模拟所有键盘事件包括按下/释放特定键如Right Option、发送组合键CmdZ,OptionEnter以及直接输入字符串用于预设按钮。server/keystroke_watcher.py监听耳。通过CGEventTap创建一个全局键盘事件监听器。它不关心具体按了什么键只关心“是否有键被按下”这个事件用于判断 Wispr 是否停止了输入。server/ax_focus.py透视眼。通过 Accessibility API 获取当前获得焦点的 UI 元素信息。如果能获取到文本内容就记录下来作为“基线”用于后续计算转录差值如果获取不到就报告“无输入框聚焦”。server/presets.py快捷指令库。负责加载和验证presets.json文件。6.2 可能的扩展方向这个项目是一个很好的起点你可以基于它扩展出更多自动化场景多应用支持目前硬编码了 Cursor。可以改造为一个配置项支持 VS Code、Chrome、任何文本编辑器等。甚至可以做一个 UI 让用户选择当前要控制哪个应用。自定义热键与动作预设按钮目前只能输入文本提交。可以扩展为支持执行复杂的 AppleScript 或 Shell 脚本比如“运行测试套件”、“提交 Git”、“部署到服务器”等。状态同步与显示目前手机只显示窗口列表和转录文本。可以让服务器推送更多 Mac 状态到手机比如当前 Git 分支、CPU 使用率、下一个会议时间等把手机变成一个信息仪表盘。语音反馈在手机端增加 TTS文字转语音功能当操作完成或出错时用语音提示实现真正的“盲操作”。离线语音识别虽然 Wispr Flow 很好但依赖网络。可以探索集成本地的语音识别引擎如 macOS 自带的听写功能或 Whisper.cpp实现完全离线的语音编码。这个项目的价值不仅在于它本身解决了问题更在于它清晰地展示了一条路径如何用简单的脚本将成熟的云端 AI 工具Cursor、先进的本地语音工具Wispr Flow和我们口袋里最强大的移动设备手机无缝连接起来创造出一种全新的、更流畅的人机交互体验。它证明了有时候最大的效率提升并非来自某个单一工具的突破而是来自工具间精巧的、自动化的连接。