
1. 从轮询到钩子Windows 鼠标行为捕获到底难在哪如果你写过桌面行为记录、自动化测试回放、或者给内部工具加一个「操作轨迹」功能大概率绕不开一个需求把用户在 Windows 上的鼠标点击和移动抓下来。最直觉的做法就是开一个死循环每隔几十毫秒调一次GetCursorPos读坐标再用GetAsyncKeyState判断左右键有没有按下。这个思路能跑但真放到实际场景里问题会一个接一个冒出来。先说轮询方案的天生缺陷。Sleep(100)意味着你每 100 毫秒才采样一次鼠标快速划过屏幕时中间一大段轨迹直接丢了回放出来就是「瞬移」。而且GetAsyncKeyState的最低有效位表示「自上次调用以来是否按过」Windows 是多任务抢占式的别的程序也可能调用这个函数把「刚按下」的状态抢走所以你不能依赖它做精确的按下/抬起判定。更麻烦的是轮询是主动查询CPU 一直被你占着长时间跑下来风扇都转起来了。真正靠谱的做法是装钩子Hook。SetWindowsHookEx配合WH_MOUSE_LL低级鼠标钩子能让系统在鼠标事件发生时主动回调你的函数坐标、按键、滚轮、时间戳一次性给全精度和实时性都不是轮询能比的。代价是你要理解 Windows 的消息循环机制钩子回调必须挂在有消息循环的线程上否则收不到事件。这篇文章就按「先跑通轮询版 → 再升级到钩子版 → 最后把采集到的数据通过统一 API 通道上报」这条线走。中间会给出可以直接复制的 Win32 代码、编译命令、以及一个容易踩的坑钩子回调里不能做耗时操作否则系统会把你的钩子踢掉。至于数据上报那一环我会用 TaoToken 的统一 Key/API 通道来演示这样你不用为每个模型或服务单独维护一套鉴权逻辑。适合谁看有 C 语言基础、想在 Windows 上做行为采集的开发者做自动化测试需要录制鼠标操作的测试工程师以及想理解 Win32 钩子机制的学生。下面所有代码都在 Windows 10/11 MinGW-w64g环境下实测过MSVC 也能编译差异我会标注。2. TaoToken 前置准备统一 Key 与 API 通道接入在写钩子代码之前先把数据上报的通道准备好。你采集到的鼠标事件最终要落到某个地方——可能是本地日志也可能是发给一个模型做行为分析或者存进后端。如果每个目标都单独配一套 Key 和 Base URL代码里会散落一堆硬编码换环境就崩。TaoToken 的思路是给你一个统一的入口Key 和 API 地址固定模型或服务通过参数切换。先注册并拿到 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 完成账号注册后进入控制台。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在左侧菜单找到「API Keys」点新建复制那串以sk-开头的字符串。这个 Key 只显示一次丢了就得重建所以先存到环境变量里别写死在源码。API 的基础地址是 https://taotoken.net/api 注意这个地址不带任何查询参数是纯粹的接口根路径。所有请求都往这个根路径拼比如对话接口就是/v1/chat/completions。如果你用的是 OpenAI 兼容的 SDK把base_url设成https://taotoken.net/api就行。这里有个细节值得说清楚TaoToken 不是让你绕过什么它就是一个正常的 API 聚合入口把不同模型的调用统一成一套鉴权和计费。你在代码里只需要维护一个Authorization: Bearer sk-xxx头换模型只改请求体里的model字段。对于鼠标行为采集这种场景你可能想把事件流批量发给一个模型做「异常操作检测」或者只是简单地上报到自己的服务统一通道能省掉大量配置工作。配置建议用环境变量Windows 下可以这样设PowerShell$env:TAOTOKEN_API_KEY sk-你的实际Key $env:TAOTOKEN_BASE_URL https://taotoken.net/api设完之后新开的终端都能读到。如果你要在 C 代码里读取用getenv(TAOTOKEN_API_KEY)即可注意返回值可能是 NULL要做判空。这样做的另一个好处是源码提交到 Git 时不会泄露 Key团队协作时每人本地配自己的。模型 ID 怎么选如果你只是做事件上报用任意一个便宜的对话模型都行如果要做行为语义分析选一个上下文长一点的。具体可用列表在文档里查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。文档里会列出当前支持的模型 ID 和对应的计费方式照着填进请求体就行。前置准备就这些一个 Key、一个 Base URL、一个模型 ID。三件套齐了后面代码里直接引用。别小看这一步很多人卡在 401 就是因为 Key 复制时带了空格或者环境变量没生效。下一节开始写真正的捕获代码。3. 可复制配置轮询版与钩子版鼠标捕获代码先给一个能立刻跑起来的轮询版理解基本 API再升级到钩子。轮询版的核心就是GetCursorPos和GetAsyncKeyState两个函数头文件只需要windows.h。#include windows.h #include stdio.h int main(void) { POINT pt; while (1) { Sleep(50); if (GetCursorPos(pt)) { printf(move x%ld y%ld\n, pt.x, pt.y); } if (GetAsyncKeyState(VK_LBUTTON) 0x8000) { printf(left down at x%ld y%ld\n, pt.x, pt.y); } if (GetAsyncKeyState(VK_RBUTTON) 0x8000) { printf(right down at x%ld y%ld\n, pt.x, pt.y); } if (GetAsyncKeyState(VK_MBUTTON) 0x8000) { printf(middle down at x%ld y%ld\n, pt.x, pt.y); } } return 0; }编译命令MinGW-w64g mouse_poll.cpp -o mouse_poll.exe -lgdi32 -luser32跑起来你会看到坐标刷屏按键按下时打印一行。但正如第一节说的这个版本有两个硬伤采样间隔丢轨迹以及GetAsyncKeyState的按下判定不可靠按住不放会一直打印。所以它只适合快速验证环境不适合生产。钩子版才是正解。低级鼠标钩子用WH_MOUSE_LL回调函数签名是LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)。lParam指向一个MSLLHOOKSTRUCT里面有坐标、时间戳、鼠标数据。关键点钩子必须装在带消息循环的线程上主线程跑一个GetMessage循环即可。#include windows.h #include stdio.h static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode HC_ACTION) { MSLLHOOKSTRUCT *p (MSLLHOOKSTRUCT *)lParam; switch (wParam) { case WM_MOUSEMOVE: printf(move x%ld y%ld t%lu\n, p-pt.x, p-pt.y, p-time); break; case WM_LBUTTONDOWN: printf(left down x%ld y%ld\n, p-pt.x, p-pt.y); break; case WM_LBUTTONUP: printf(left up x%ld y%ld\n, p-pt.x, p-pt.y); break; case WM_RBUTTONDOWN: printf(right down x%ld y%ld\n, p-pt.x, p-pt.y); break; case WM_MOUSEWHEEL: printf(wheel delta%d\n, (short)HIWORD(p-mouseData)); break; } } return CallNextHookEx(NULL, nCode, wParam, lParam); } int main(void) { HHOOK hHook SetWindowsHookEx(WH_MOUSE_LL, MouseProc, GetModuleHandle(NULL), 0); if (!hHook) { printf(hook failed: %lu\n, GetLastError()); return 1; } printf(hook installed, press CtrlC to exit\n); MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } UnhookWindowsHookEx(hHook); return 0; }编译g mouse_hook.cpp -o mouse_hook.exe -luser32注意SetWindowsHookEx的第三个参数对于WH_MOUSE_LL这种全局低级钩子传GetModuleHandle(NULL)即可不需要 DLL 注入。第四个参数传 0 表示全局。如果你只想钩当前进程传GetCurrentThreadId()。这里有个必须强调的坑回调函数里绝对不能做耗时操作。printf在控制台其实已经算慢的了高频移动时可能拖慢系统。生产环境应该把事件写进一个环形缓冲区由另一个线程消费。另外回调里不要调用会触发消息循环的函数否则可能死锁。如果你要把事件上报到 TaoToken建议在回调里只做「入队」上报逻辑放到独立线程。下面是一个简化的上报函数用 WinHTTP 发 POST#include windows.h #include winhttp.h #include stdio.h void report_event(const char *json) { HINTERNET hSession WinHttpOpen(LMouseReporter/1.0, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); HINTERNET hConnect WinHttpConnect(hSession, Ltaotoken.net, INTERNET_DEFAULT_HTTPS_PORT, 0); HINTERNET hRequest WinHttpOpenRequest(hConnect, LPOST, L/api/v1/chat/completions, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); const wchar_t *headers LContent-Type: application/json\r\nAuthorization: Bearer sk-你的Key\r\n; WinHttpSendRequest(hRequest, headers, -1L, (LPVOID)json, (DWORD)strlen(json), (DWORD)strlen(json), 0); WinHttpReceiveResponse(hRequest, NULL); WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); }编译时加-lwinhttp。注意这里 Key 是硬编码演示实际请从环境变量读。上报的 JSON 结构按文档要求拼模型 ID 填你选的那个。这样一套下来捕获和上报就打通了。4. 验证请求与成功结果实际点击移动动作测试代码写完得验证它真的在工作。分两步先验证钩子捕获再验证上报通道。验证钩子编译mouse_hook.exe后直接运行会看到hook installed提示。然后在桌面上慢慢移动鼠标控制台应该实时打印move x... y...。点一下左键打印left down和left up。滚一下滚轮打印wheel delta120向前或-120向后。如果这些都有说明钩子装成功了。我实测时发现一个现象移动事件非常密集一秒钟能刷几百行控制台滚动太快根本看不清。这时候可以把WM_MOUSEMOVE的打印改成「每 10 个事件打印一次」或者加个计数器。生产环境肯定要限流不然日志文件瞬间几个 G。验证上报把上报函数接进回调的队列消费线程发一条测试事件。你可以先用一个固定的 JSON 字符串手动调report_event看返回。更直观的方式是用 curl 先测通道通不通curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d {model:你的模型ID,messages:[{role:user,content:ping}]}如果返回里有choices字段和内容说明 Key 和通道都没问题。这一步能排除掉大部分配置错误。返回 401 就是 Key 错了返回 404 就是路径拼错了返回超时就是网络问题。把 curl 测通之后再跑 C 程序的上报。建议在report_event里加日志把 HTTP 状态码打出来。WinHttpQueryHeaders可以拿到状态码200 就是成功。如果一直拿不到响应检查WinHttpSendRequest的返回值以及是不是忘了WinHttpReceiveResponse。一个完整的验证流程是这样的运行钩子程序 → 在屏幕上画个圈 → 点几下左右键 → 滚轮上下滚 → 观察控制台输出是否覆盖了 move/down/up/wheel 四类事件 → 同时看上报线程的日志有没有 200。全部通过说明捕获和上报链路都通了。如果你想让验证更直观可以把捕获到的坐标写进一个 CSV然后用脚本画轨迹图。不过那是另一个话题了。这里的关键是先确保事件类型齐全再确保上报成功最后才考虑数据怎么用。5. 本篇常见错误排查401、钩子失效与编译报错这一节把最容易卡住人的几个报错列出来对照着查。401 Unauthorized。这是上报环节最常见的。原因通常有三个Key 复制时带了首尾空格环境变量没生效比如在旧终端里跑请求头拼写错误比如Authorization写成了Authorisation或者漏了Bearer前缀。排查方法先用 curl 测curl 通了再查 C 代码。如果 curl 也 401那就是 Key 本身的问题去控制台重新生成一个。hook failed: 1428。SetWindowsHookEx返回 NULLGetLastError是 1428意思是「不能设置非全局钩子而没有模块句柄」。这通常发生在你用了WH_MOUSE_LL但第三个参数传了 NULL。改成GetModuleHandle(NULL)即可。另一个可能是你在没有消息循环的线程里装钩子低级钩子要求线程有消息队列。钩子装上了但收不到事件。检查你的消息循环是不是GetMessage而不是PeekMessage空转。GetMessage会阻塞等待消息钩子回调才能被派发。如果你用while(1)空转回调永远不会触发。另外如果程序没有以管理员权限运行某些高权限窗口比如任务管理器上的鼠标事件可能收不到这是 UIPI 机制限制不是代码问题。编译报错 undefined reference toSetWindowsHookEx。链接时漏了库。MinGW 下加-luser32MSVC 下在项目属性里链接User32.lib。GetCursorPos也在 user32 里。WinHttp相关函数要加-lwinhttp。回调里 printf 导致系统卡顿。前面提过回调必须快。如果你发现鼠标移动变卡八成是回调里做了同步 IO。解决办法回调只往无锁队列里塞数据另开线程消费。队列可以用简单的环形缓冲区加临界区实现。上报时WinHttpSendRequest返回 FALSE。用GetLastError看具体码。常见的是 12007域名解析失败或 12029连接失败。检查网络以及 URL 是不是写成了http://而不是https://。TaoToken 的 API 走 HTTPS端口 443。OAuth 相关报错。如果你用的是某些需要 OAuth 流程的客户端比如 Claude Code 这类工具报错信息里可能出现 OAuth 字样。这种情况通常是客户端配置里的鉴权方式选错了。对于直接调 API 的场景用 Bearer Token 就行不需要 OAuth。如果你在用 Claude Code 接入参考文档里的配置说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面会讲清楚 Base URL、Key、Model ID 三件套怎么填。reading choices 报错。这通常出现在解析响应时说明返回的 JSON 里没有choices字段。原因可能是模型 ID 填错了或者请求体格式不对。先用 curl 看原始返回确认结构再改解析代码。把这几类错误过一遍基本能覆盖 90% 的卡点。剩下的就是网络环境和权限问题了。6. 把捕获数据用起来接入与后续方向钩子跑通、上报验证成功之后你手里就有了一条实时的鼠标事件流。接下来怎么用取决于你的场景。做自动化测试的可以把事件流存成脚本回放时用SendInput模拟做行为分析的可以把事件批量发给模型让它判断「这段操作是否异常」做内部工具的可以做个热力图看用户在哪块区域点击最频繁。如果你要把事件流接到模型做分析用 TaoToken 的对话接口就行。把一批事件拼成 JSON 数组塞进messages让模型输出结构化结果。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 可以先在网页上试提示词调好了再写进代码。如果你要长期跑编码或 Agent 类任务Coding Plan 更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 可以建多个 Key 分别给不同程序用方便排查和吊销。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 遇到参数问题先查文档。最后给一个实用建议钩子程序长时间运行一定要做异常保护。用SetUnhandledExceptionFilter兜底崩溃时自动卸载钩子不然钩子残留会导致整个系统的鼠标响应变慢只能重启解决。这个坑我踩过排查了半天才发现是上次调试时程序异常退出没清理钩子。