
先说个真实经历我把 Claude Computer Use 接到自己电脑上跑自动化结果被卡得头皮发麻。截图发给模型模型分析得头头是道然后调用鼠标点击目标软件的窗口纹丝不动。换 pyautogui、换 SendInput、换各种模拟库全都被“吞”了。最后一个多月的老问题被一块十几块钱的 CH9329 硬件 HID 模块解决掉。这篇文章就把完整方案写出来包括 SendInput 为什么会被拦截、CH9329 的协议帧怎么拼、怎么把 Claude Computer Use 的 computer 动作翻译成真实键鼠事件以及我踩过的各种坑。适合正在做桌面端 AI 智能体、自动化测试、或者需要物理级输入注入的朋友参考。1. 先搞清楚SendInput 为什么总被“吃掉”1.1 SendInput 的工作原理和软肋SendInput 是 Windows 提供的 Win32 API从 Windows XP 时代就是标准键鼠模拟手段。它的调用链路大概是这样应用调用 SendInput系统把输入事件写入输入队列然后经过 raw input 处理、消息分发等环节最终送到目标窗口。表面看很正规但它本质上是“软件层注入”无论换了多少层封装都逃不出用户态的范围。问题恰恰出在这里。只要输入事件还在软件链路里就有一堆环节可以拦截全局钩子。SetWindowsHookEx 可以挂 WH_KEYBOARD_LL 和 WH_MOUSE_LL这类钩子在底层键盘鼠标驱动之上、应用窗口之下任何模拟输入经过这里都会被拿到钩子函数直接返回 1 就能吞掉事件。安全软件和反作弊。不少安全产品会 hook 掉 SendInput 对应的内核入口或者对窗口消息做过滤专门防自动化工具。UIPI用户界面特权隔离。如果目标窗口是以管理员权限运行的普通权限进程调用 SendInput 去发键盘鼠标事件事件会被 UIPI 拦截目标窗口根本收不到。输入法、远程桌面、虚拟机窗口的输入重定向也经常吃掉模拟输入。理解这一点之后你就会明白一个残酷的事实任何基于 SendInput 的方案都建立在“系统愿意接收软件模拟输入”的假设之上。一旦目标软件有意识防自动化整个方案就报销了。这也解释了为什么 Claude Computer Use 这类 AI 智能体在真实桌面环境下经常“失灵”——不是模型不够聪明而是底层输入通道先天不足。1.2 如何判断输入是被“吞”了如果你也遇到类似情况先别急着换模型或者调 prompt先确认是不是输入事件根本没送达。我总结了一套快速排查法在记事本里测。用一段最简单的 SendInput 代码向记事本发送文本如果记事本能收到说明系统层面的输入链路是通的。再在目标软件里测。同样代码发一遍如果目标软件没反应基本可以确定问题出在目标软件自身或它绑定的安全组件上。用系统自带的事件监控。Windows 可以通过启用“键盘/鼠标事件日志”观察事件是否到达系统层或者写一个小程序调用 GetAsyncKeyState 轮询按键状态如果轮询能读到按键、但目标窗口收不到消息说明事件被钩子或 UIPI 拦截在窗口分发阶段。还有一个很常见的现象是“部分拦截”比如鼠标能移动但点击无效或者键盘普通字母能用但组合键 CtrlC 无效。这种通常是被钩子针对性地过滤了。总之只要目标软件“看不见”输入的窗口而系统层又能感知到输入那大概率就是软件层拦截。这时候就该考虑跳过软件注入直接从硬件层面“伪造”一个真实的 USB 键鼠设备出来。2. CH9329 方案选型为什么是它2.1 方案对比市面上能实现硬件键鼠注入的方案不少我实际对比过几条路线最后选了 CH9329。方案成本开发量可靠性适用场景SendInput 等软件模拟零成本低容易被钩子和安全组件拦截无法绕过拦截的场景Arduino Leonardo/MicroATmega32U4几十到上百中高需要自己写 USB HID 代码或固件物理层输出可靠想顺便做原型开发CH9329 模块十元左右低串口发字节即可物理层输出可靠快速落地接入现有系统AC6328A2 等蓝牙 SoC十几到几十高需要自己刷 HID 固件还要分析报告描述符物理层输出还要适配蓝牙协议需要无线键鼠的场景CH9329 最吸引我的地方是它是沁恒出的“串口转 HID 键鼠芯片”出厂固件已经把 USB HID 协议栈固化好了。你不需要管设备描述符、报告描述符、端点配置这些东西它插上 USB 就是一个标准的 USB 键盘加鼠标复合设备。外面那些“hid 报告描述符分析工具 v1.7”“ac6328a2 hid 自拍”之类的折腾在 CH9329 这里统统可以跳过因为它连“hid 固件”都不用你自己刷出厂就是能用的。相比之下Arduino Leonardo 也能实现同样的功能但需要自己维护 HID 描述符写代码处理组合键、鼠标位移拆分开发量比 CH9329 大得多。AC6328A2 这类方案更麻烦它本质上是一颗蓝牙音频/SOC 芯片做 HID 设备要自己写固件、调报告描述符适合做嵌入式产品不适合快速接一个 Claude Computer Use 这种应用层系统。2.2 硬件链路与材料清单CH9329 的架构很简单控制端通过 UART 串口发送指令帧芯片解析后通过 USB HID 协议把键鼠事件输出到目标电脑。整体链路长这样控制端跑 Claude Computer Use 的进程 - USB-TTL 串口模块 - CH9329 模块 - USB 线插入目标电脑目标电脑看到的是一把标准 USB 键盘和一个标准 USB 鼠标完全不知道背后是 AI 在控制。这也是它能绕过软件层拦截的根本原因输入事件是在 USB 协议层面产生的应用层那些钩子根本看不到入口。材料清单比较简单CH9329 模块一个十几块钱注意买带 USB 公头或者可以引出 USB 口的版本。USB-TTL 串口模块一个CP2102 或 CH340 都行几块钱到十几块钱。杜邦线四根VCC、GND、TXD、RXD。目标电脑和控制端。控制端可以是另一台电脑也可以就是目标电脑本身。接线方式要注意CH9329 的 VCC 接 USB-TTL 的 5VGND 接 GNDCH9329 的 TX 接 USB-TTL 的 RXCH9329 的 RX 接 USB-TTL 的 TX交叉相接。CH9329 一般建议 5V 供电如果 USB-TTL 模块有 5V 档就用 5V用 3.3V 供电在一些批次的模块上会不稳定。这里有个小细节控制端和目标电脑可以是同一台。也就是说目标电脑上跑一个 Claude Computer Use 进程同时插着一个 USB-TTL 连 CH9329CH9329 又插回同一台电脑的另一个 USB 口。这样就形成了“自己控制自己”的闭环。好处是不用准备两台机器坏处是如果目标软件检测到串口设备可能会暴露自动化痕迹但大多数普通应用不会管串口。3. CH9329 协议帧逐字节拆解与驱动封装3.1 帧格式CH9329 的协议帧格式不同厂家模块和不同固件版本会有差异我手里这块模块的通信格式如下你们用的时候一定要拿规格书对一下尤其是长度字段和校验和的范围翻车概率最高的就是这里。常见的帧结构是帧头0x57 0xAB长度两个字节小端序通常表示“命令码 数据区”的字节数命令码不同命令的类型数据区与命令相关校验和前面所有字节累加和取低 8 位以键盘事件为例我们这边的命令码是 0x02数据区是 8 个字节结构依次是修饰键、保留字节、按键码 1 到按键码 6。修饰键的低四位分别对应 Ctrl、Shift、Alt、Win普通按键用 HID Usage ID 表示字母 a 是 0x04回车是 0x28音量加是 0x80音量减是 0x81。也就是说CH9329 不仅能发普通按键也能发媒体键和音量键这就是标题热词里“hid 键盘发送音量修改和普通按键”的由来——HID 协议本身就是一套标准的 Usage 表音量键只是几个特殊的 Usage ID 而已。鼠标事件的命令码是 0x05数据区是 4 个字节鼠标按钮状态、X 位移、Y 位移、滚轮。X 和 Y 是有符号数取值范围是 -127 到 127超出这个范围必须拆分成多帧陆续发送这是 HID 鼠标标准里 movement 字段的硬限制不是 CH9329 故意为难你。3.2 Python 驱动封装串口发送用 pyserial 就够了。我封装了一个最小可用的 CH9329 类核心思路是每个操作都转成完整的协议帧写进串口import serial import time class CH9329: def __init__(self, port, baudrate115200): self.ser serial.Serial(port, baudrate, timeout0.1) self.button_state 0x00 def _send(self, cmd, data): length len(data) 1 frame bytearray([0x57, 0xAB, length 0xFF, (length 8) 0xFF, cmd]) frame.extend(data) frame.append(sum(frame) 0xFF) self.ser.write(frame) time.sleep(0.005) def key_down(self, usage_id, modifiers0x00): data bytes([modifiers, 0x00, usage_id, 0x00, 0x00, 0x00, 0x00, 0x00]) self._send(0x02, data) def key_up(self): self._send(0x02, bytes(8)) def press_key(self, usage_id, modifiers0x00): self.key_down(usage_id, modifiers) time.sleep(0.03) self.key_up() def mouse_move(self, dx, dy): while dx or dy: step_x max(-127, min(127, dx)) step_y max(-127, min(127, dy)) self._send( 0x05, bytes([self.button_state, step_x 0xFF, step_y 0xFF, 0x00]), ) dx - step_x dy - step_y time.sleep(0.01) def mouse_button(self, mask, pressTrue): if press: self.button_state | mask else: self.button_state ~mask self._send(0x05, bytes([self.button_state, 0x00, 0x00, 0x00])) time.sleep(0.02) def click(self, mask0x01): self.mouse_button(mask, True) time.sleep(0.03) self.mouse_button(mask, False)这段代码有几个细节值得说每帧之间加了 sleep是因为 CH9329 内部 MCU 处理帧需要时间。实测 5ms 间隔比较稳如果调成 0 或太快偶尔会丢帧。鼠标按钮状态、位移是分别发送的移动时带上当前 button_state这样在按住拖拽时移动鼠标目标电脑才能正确识别为“拖动”。鼠标位移用循环拆分到单步不超过 127这是 HID 协议硬限制拆帧时保持方向一致通常一次大位移拆成十几个小位移看起来和连续移动一样。3.3 把绝对坐标折算成相对位移Claude Computer Use 的 computer 工具返回的鼠标动作通常是屏幕绝对坐标比如 coordinate [640, 480]。但 CH9329 只认相对位移所以代理层必须维护一个“鼠标当前位置”的状态。做法很直接每次移动前计算目标坐标和当前坐标的差值然后调用 mouse_move 去发相对帧。同时把“当前位置”更新为目标坐标保证下一次移动计算准确。要注意如果有人手动动了鼠标或者系统因为显示设置缩放导致坐标漂移这个状态就失效了需要重新同步一次当前鼠标实际位置。DPI 缩放是最容易踩的坑。Windows 如果开了 125% 或 150% 缩放截图坐标和 SendInput 坐标体系会不一致CH9329 因为是相对位移问题不大但 Claude Computer Use 基于截图给的坐标是缩放后的逻辑坐标两者对不上就会导致“模型想点左上角实际点到右下角”。我的建议是目标电脑把系统缩放强制设成 100%分辨率固定下来截图和鼠标坐标就一一对应了。4. 对接 Claude Computer Use完整落地实现4.1 把 tool_use 动作翻译成硬件输入Claude Computer Use 通过 computer 工具让模型可以操作电脑常见的 action 类型有 key、type、mouse_move、left_click、left_click_drag、right_click、middle_click、double_click、screenshot。这一步的核心工作是写一个 dispatcher把这些逻辑动作映射成 CH9329 帧序列。我这里列一个最小实现def handle_tool_use(tool_use, ch9329): if tool_use.name ! computer: return inp tool_use.input action inp.get(action) if action key: keys inp.get(keys, []) for key in keys: usage, modifier key_to_hid(key) ch9329.press_key(usage, modifier) elif action type: text inp.get(text, ) for ch in text: usage, modifier char_to_hid(ch) ch9329.press_key(usage, modifier) elif action mouse_move: x, y inp.get(coordinate) ch9329.move_to(x, y) elif action left_click: ch9329.click(0x01) elif action left_click_drag: x, y inp.get(coordinate) ch9329.mouse_button(0x01, True) ch9329.move_to(x, y) ch9329.mouse_button(0x01, False) elif action right_click: ch9329.click(0x02) elif action double_click: ch9329.click(0x01) time.sleep(0.05) ch9329.click(0x01)这里需要准备两个映射表key_to_hid 把 Claude 返回的按键名比如“ctrl c”“enter”“tab”转成 HID usage ID 和修饰键char_to_hid 把字符转成 HID 键盘码。字符映射看起来麻烦实际上就是一套 ASCII 到 HID Usage 的对照表网上很好找。要注意大小写小写字母是不带 Shift 的大写字母要带 Shift 修饰键特殊字符也一样比如感叹号在美式键盘上是 Shift1。4.2 主循环与全链路联调Claude Computer Use 的完整流程是循环式的截图、发给模型、模型返回 tool_use、执行动作、再截图。接入 CH9329 之后主循环只需要在 tool_use 处理阶段调用上面的 dispatcher 即可。伪代码如下while not done: screenshot capture_screen() response client.messages.create( ..., tools[computer_tool], messages[...], ) for block in response.content: if block.type tool_use: handle_tool_use(block, ch9329) send_tool_result(block.id, done) elif block.type text and is_finished(block.text): done True联调的时候我建议按下面这个顺序逐步上难度先单独测 CH9329 类。写一个脚本发一个回车键、移动一下鼠标确认目标机能正常响应。再测 dispatcher。不接 API直接构造几个 tool_use 块比如 mouse_move 到 (100, 100)、left_click、type 一段文字观察目标机行为是否符合预期。最后再接 Claude Computer Use。跑一个简单任务比如“打开记事本输入 Hello World”确认全链路能跑通。我第一次直接把全套接上结果模型疯狂点击但目标机没反应浪费了小半天排查。后来发现只是串口波特率设置成了 9600CH9329 模块被我之前配置成了 115200两边各说各话。所以顺序真的很重要。4.3 可靠性设计硬件注入方案最怕的就是状态卡死。比如程序在按下 Ctrl 之后异常退出没有发送释放帧目标机就会一直处于 Ctrl 被按住的状态桌面上一顿乱操作。为了避免这类问题我做了三个措施第一主程序用 try/finally 包住动作执行区finally 里强制发送键盘全 0 帧和鼠标按键状态重置。这样就算异常退出目标机也能恢复正常输入状态。第二加上一个低频率的心跳同步。空闲状态下每 500ms 发一次键盘空帧把修饰键和按键码全部清零。这样即使某帧因为串口干扰丢了下一个心跳帧也会把状态拉回来。第三所有动作都打印日志。包括发的是什么帧、对应的逻辑动作是什么、耗时多少。出了问题看日志就能定位是模型决策问题、坐标问题还是驱动问题不用瞎猜。性能方面实测下来 CH9329 在 115200 波特率下单帧传输不到 1ms加上代码里的 sleep一次点击大概 60ms一次跨屏移动需要 40~80ms。对 Claude Computer Use 这种“截图-思考-动作”的循环来说输入延迟完全不是瓶颈瓶颈还是在 API 响应时间和截图速度上。5. 常见问题与排查技巧实录5.1 串口帧错误与波特率问题这是最容易踩的坑。表现是目标机完全无动作控制端程序也不报错。排查思路是这样的确认目标机是否识别到了 CH9329。插上 USB 后设备管理器里应该出现“USB 输入设备”或者 HID keyboard/mouse。确认控制端能打开串口。Linux 下一般是 /dev/ttyUSB0Windows 下是 COM3 之类的打开失败就检查驱动。确认波特率一致。CH9329 出厂默认通常是 9600如果之前改过配置可能变成 115200 或其他值。串口工具发一帧再用逻辑分析仪或者串口助手看返回能快速定位。确认接线没有接反。CH9329 的 TX 接 USB-TTL 的 RXCH9329 的 RX 接 USB-TTL 的 TXGND 必须共地。有个小技巧串口助手手动发一帧“键盘全 0 释放帧”如果目标机那边没有任何反应但串口助手也不报错那大概率是波特率或者接线问题。如果目标是 Windows 系统可以打开记事本发一个字符键看是否被输入进去。5.2 按键卡住、鼠标乱飘按键卡住是硬件注入方案的典型副作用。最常见的原因是程序异常退出导致释放帧没有发出去。另一个容易忽略的场景是手动移动鼠标导致的坐标漂移这时候当前坐标状态就不准了后续 move_to 会把鼠标移动到错误的位置。我处理这个问题的方式是双保险程序异常时 finally 里强制重置同时定期发空帧同步状态。坐标漂移则是每次执行 mouse_move 前通过系统 API 读取当前真实鼠标位置重新校准 current_x/current_y。虽然要多调一次系统接口但可靠性提升很大。5.3 坐标/截图不同步模型根据截图决定点哪里CH9329 把坐标转成位移如果两个坐标系不一致动作就会乱套。常见原因有DPI 缩放不一致。Windows 设置里的缩放到 150%截图是物理像素坐标是逻辑像素两者差 1.5 倍。多显示器。Claude Computer Use 默认截的主屏幕如果鼠标在副屏幕两者坐标系完全不匹配。窗口位置动态变化。模型看到某个按钮在某个位置但实际执行时窗口被弹窗遮挡或者移动了。解决办法很粗暴目标机固定为单显示器、固定分辨率、100% 缩放。这样截图坐标和物理鼠标坐标严格一致模型也很少出现“点偏”的问题。如果你必须在多显示器环境跑至少把截图范围限定在主屏幕并保证鼠标起始位置在主屏幕内。5.4 常见问题速查表现象可能原因解决方式目标机完全无响应波特率不一致、接线错误、CH9329 未识别检查设备管理器、用串口助手发帧、确认波特率一致按键按下后弹不起来异常退出没发释放帧、丢帧finally 里重置、定时发空帧鼠标点击位置不准DPI 缩放、多显示器、状态漂移100% 缩放、单显示器、执行前校准鼠标位置串口能发但目标机偶发漏动作帧间隔太短、未加保护间隔每帧之间至少 5ms按键按下到释放至少 30ms模型反复截图但不动作tool_use 解析失败、动作映射缺失检查 dispatcher、打印 tool_use 原始内容这套方案跑通之后我又把它接到了内部报表系统的自动化测试流程里。目标软件的开发方给客户端加了安全控件传统的 pyautogui 和 SendInput 全被吞换成 CH9329 之后一次通过。对我来说这个方案的真正价值不是“绕过拦截”这个动作本身而是让 AI 智能体能真正落到物理世界——它不再是一个只能操作窗口消息的虚拟角色而是一个能像真实用户一样操作键盘鼠标的实体。最后分享一个细节CH9329 模块拿回来后最好先用热缩管或者电工胶布把排针区域包一下杜邦线插拔几次容易接触不良表现就是串口偶尔报错、目标机输入时断时续。我前期排查过很多次“协议错误”最后发现只是线松了。硬件方案看似简单但物理连接稳固才是稳定性的底线。