
1. 从一次“鼠标自己动了”的调试说起SetCursorPos 与 mouse_event 远程控制到底能做什么先说结论Windows 下做鼠标远程控制核心就三件事——坐标换算、事件注入、网络传输。SetCursorPos负责把光标挪到屏幕某个绝对位置mouse_event或更新的SendInput负责模拟按下、抬起、滚轮这些动作ClientToScreen负责把窗口内的逻辑坐标翻译成屏幕坐标。三者串起来再套一层 TCP 或 WebSocket就能在本地双机之间跑通一次点击与拖拽的远程回放。这套东西适合谁适合想理解远程桌面底层原理的 Windows C 开发者、做自动化测试工具的同学、以及想给自己写个“局域网内控制另一台机器鼠标”的小工具的人。它不是什么高深技术但坑不少坐标对不上、DPI 缩放导致偏移、mouse_event在部分场景被 UIPI 拦截、结构体跨机传输字节序和内存对齐问题。我试过在 125% 缩放的屏幕上直接发逻辑坐标接收端光标永远偏一截后来才明白必须统一到屏幕物理坐标。本文不会只给你一段“能跑就行”的代码。我会把链路拆成三段讲清楚给出可复制的config.toml和settings.json配置骨架演示用 TaoToken 统一 Key 打通 API 通道的验证动作最后把常见报错401、local proxy failed、reading choices、OAuth 相关逐个对照排查。目标很明确你在本地两台机器上能跟着把一次点击和一次拖拽完整回放出来。先明确一个边界本文讲的是本地局域网内的技术验证不涉及任何绕过网络管理的手段。所有连接都发生在你自己的设备之间IP 用192.168.x.x这类内网地址。TaoToken 在这里的角色是统一 API Key 管理帮你把模型调用通道收敛到一个入口方便后续接入更复杂的控制逻辑比如用自然语言描述动作让模型生成坐标序列。它不替代你的编辑器也不碰生产数据库就是一个 Key 和通道的聚合层。坐标换算这块最容易混的是三套坐标系屏幕坐标物理像素原点在主显示器左上角、窗口客户区坐标原点在窗口客户区左上角、逻辑坐标受 DPI 缩放影响。发送端如果直接抓OnMouseMove的point那是客户区逻辑坐标接收端如果直接SetCursorPos(point.x, point.y)在缩放不是 100% 的机器上必然偏。正确做法是发送端用ClientToScreen转成屏幕坐标再发或者接收端收到后按自己的窗口做一次转换。excerpt 里那段代码在接收端调了ClientToScreen但发送端发的是客户区坐标两边窗口位置不一致时就会错位——这是第一个要修的坑。事件注入方面mouse_event是老 API参数是dwFlags如MOUSEEVENTF_MOVE、MOUSEEVENTF_LEFTDOWN、MOUSEEVENTF_LEFTUP、dx、dy、dwData、dwExtraInfo。移动用SetCursorPos更直接点击和拖拽用mouse_event补动作。注意MOUSEEVENTF_MOVE配合SetCursorPos时dx/dy一般传 0因为位置已经由SetCursorPos定了。拖拽就是LEFTDOWN→ 若干次移动 →LEFTUP中间每次移动后最好Sleep(10~20)否则目标程序可能来不及响应。网络传输这块结构体直接send有个隐患不同编译器、不同架构下struct的内存对齐可能不一样CPoint里是两个LONG通常没问题但加上int event后整体大小可能是 12 或 16 字节。稳妥做法是手动序列化成固定布局的字节流或者用#pragma pack(1)。另外 TCP 是字节流recv不保证一次收满一个结构体必须循环收够sizeof再解析。excerpt 里直接recv(s_d, (char*)pos, sizeof(point_pos), 0)在局域网小包下大概率没事但严格来说要处理粘包和半包。下面进入实操。我会先给配置骨架再给发送端和接收端的关键代码然后演示用 TaoToken 验证 API 通道最后排错。你不需要一次全懂跟着敲一遍跑通一次点击再跑一次拖拽链路就清楚了。2. TaoToken 前置统一 Key 与 config.toml / settings.json 配置骨架在写鼠标控制代码之前先把 API 通道理顺。为什么这一步放在前面因为后面你想扩展成“模型生成动作序列”时会需要一个稳定的模型调用入口。TaoToken 提供统一 Key把不同模型的接入收敛成一套 Base URL Key Model ID 的组合省得你在多个平台之间来回切。先拿 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。API 基础地址统一用 https://taotoken.net/api 注意这个地址不加 UTM 参数直接写进配置。拿到 Key 后先建一个项目目录比如D:\mouse-remote在里面放两个配置文件。第一个是config.toml用于 C 侧读取网络和模型参数# D:\mouse-remote\config.toml [network] listen_port 8000 target_host 192.168.0.2 target_port 8000 recv_timeout_ms 3000 [coordinate] # 发送端是否把客户区坐标转屏幕坐标后再发 convert_to_screen true # 接收端是否按自身窗口再做一次转换 reconvert_on_receiver false [taotoken] base_url https://taotoken.net/api api_key sk-你的Key model_id claude-sonnet-4-20250514 timeout_ms 30000第二个是settings.json用于 VS Code 或 Cline 这类工具读取方便你在编辑器里直接调模型辅助生成坐标逻辑{ taotoken.baseUrl: https://taotoken.net/api, taotoken.apiKey: sk-你的Key, taotoken.modelId: claude-sonnet-4-20250514, mouseRemote.listenPort: 8000, mouseRemote.targetHost: 192.168.0.2, mouseRemote.targetPort: 8000, mouseRemote.convertToScreen: true }如果你用 Claude Code 做辅助开发配置走 Anthropic 兼容通道Base URL 同样填https://taotoken.net/apiKey 用上面那个Model ID 按你选的填。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的 settings 片段。Coding Plan 适合长期做这类工具开发地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。这里强调三件套Base URL、Key、Model ID缺一不可。很多人只填了 Key 忘了 Base URL结果请求打到默认端点报 401 或 404。TaoToken 的 Base URL 固定是https://taotoken.net/api不要自己拼/v1之类的后缀具体路径由 SDK 或 curl 决定。配置写好后先别急着写鼠标代码用一条 curl 验证通道是否通。打开 PowerShellcurl.exe https://taotoken.net/api/v1/messages -H Content-Type: application/json -H x-api-key: sk-你的Key -H anthropic-version: 2023-06-01 -d {\model\:\claude-sonnet-4-20250514\,\max_tokens\:64,\messages\:[{\role\:\user\,\content\:\回复 OK 两个字母\}]}如果返回里有content字段且文本是OK说明 Key 和通道都正常。这一步很重要因为后面鼠标控制代码里如果嵌了模型调用通道不通会浪费你大量时间在排查业务逻辑上。关于 Key 的安全不要把 Key 硬编码进提交到 Git 的源码里。config.toml和settings.json加进.gitignore或者用环境变量TAOTOKEN_API_KEY读取。C 里可以用getenvNode 里用process.env。这是基本习惯别省。另外TaoToken 的模型对话入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 你可以在网页上先试几条 prompt确认模型能理解“把坐标 (100,200) 转成屏幕坐标”这类指令再写进代码。这样调试时你心里有底知道是代码问题还是模型问题。配置骨架到这里就齐了。下一节进入可复制的 C 配置和代码把发送端、接收端、坐标换算、事件注入串起来。3. 可复制配置SetCursorPos / mouse_event / ClientToScreen 完整代码骨架这一节给可直接编译的代码。我用 Win32 WinsockVS2019 及以上都能编。先建一个共享头文件mouse_packet.h定义数据包结构用#pragma pack(1)避免对齐差异// mouse_packet.h #pragma once #include cstdint #pragma pack(push, 1) struct MousePacket { uint8_t event; // 1move, 2leftdown, 3leftup, 4rightdown, 5rightup, 6wheel int32_t x; // 屏幕坐标 x int32_t y; // 屏幕坐标 y int32_t data; // 滚轮增量等附加数据 }; #pragma pack(pop) constexpr uint8_t EV_MOVE 1; constexpr uint8_t EV_LEFTDOWN 2; constexpr uint8_t EV_LEFTUP 3; constexpr uint8_t EV_RIGHTDOWN 4; constexpr uint8_t EV_RIGHTUP 5; constexpr uint8_t EV_WHEEL 6;发送端核心逻辑抓鼠标移动把客户区坐标用ClientToScreen转屏幕坐标打包发送。注意ClientToScreen需要窗口句柄在 MFC 的OnMouseMove里可以直接用this的m_hWnd。// sender.cpp 关键片段 #include mouse_packet.h #include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) SOCKET g_sock INVALID_SOCKET; bool ConnectToReceiver(const char* host, int port) { WSADATA wsa; if (WSAStartup(MAKEWORD(2,2), wsa) ! 0) return false; g_sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (g_sock INVALID_SOCKET) return false; sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(static_castu_short(port)); inet_pton(AF_INET, host, addr.sin_addr); if (connect(g_sock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { closesocket(g_sock); g_sock INVALID_SOCKET; return false; } return true; } void SendMouseEvent(uint8_t ev, int x, int y, int data 0) { if (g_sock INVALID_SOCKET) return; MousePacket pkt{}; pkt.event ev; pkt.x x; pkt.y y; pkt.data data; send(g_sock, (const char*)pkt, sizeof(pkt), 0); } // 在窗口的 OnMouseMove 里调用 void OnMouseMove(UINT nFlags, CPoint point) { CPoint screenPt point; ClientToScreen(screenPt); // 客户区 - 屏幕坐标 SendMouseEvent(EV_MOVE, screenPt.x, screenPt.y); } // 左键按下 void OnLButtonDown(UINT nFlags, CPoint point) { CPoint screenPt point; ClientToScreen(screenPt); SendMouseEvent(EV_LEFTDOWN, screenPt.x, screenPt.y); } // 左键抬起 void OnLButtonUp(UINT nFlags, CPoint point) { CPoint screenPt point; ClientToScreen(screenPt); SendMouseEvent(EV_LEFTUP, screenPt.x, screenPt.y); }接收端核心逻辑循环收满一个MousePacket按event分派移动用SetCursorPos点击用mouse_event。注意mouse_event的dx/dy在配合SetCursorPos时传 0。// receiver.cpp 关键片段 #include mouse_packet.h #include winsock2.h #include windows.h #pragma comment(lib, ws2_32.lib) bool RecvAll(SOCKET s, char* buf, int len) { int total 0; while (total len) { int n recv(s, buf total, len - total, 0); if (n 0) return false; total n; } return true; } void HandlePacket(const MousePacket pkt) { switch (pkt.event) { case EV_MOVE: SetCursorPos(pkt.x, pkt.y); break; case EV_LEFTDOWN: SetCursorPos(pkt.x, pkt.y); mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); break; case EV_LEFTUP: SetCursorPos(pkt.x, pkt.y); mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); break; case EV_RIGHTDOWN: SetCursorPos(pkt.x, pkt.y); mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0); break; case EV_RIGHTUP: SetCursorPos(pkt.x, pkt.y); mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0); break; case EV_WHEEL: mouse_event(MOUSEEVENTF_WHEEL, 0, 0, (DWORD)pkt.data, 0); break; default: break; } } void ReceiverLoop(SOCKET clientSock) { MousePacket pkt{}; while (true) { if (!RecvAll(clientSock, (char*)pkt, sizeof(pkt))) break; HandlePacket(pkt); } }拖拽怎么实现发送端在OnLButtonDown后进入拖拽状态OnMouseMove时如果处于拖拽就连续发EV_MOVEOnLButtonUp发EV_LEFTUP。接收端收到LEFTDOWN后光标已经按下后续MOVE会带着按下状态移动最后LEFTUP释放。中间每次MOVE之间在发送端加Sleep(15)避免刷太快。如果你用 Cline 或 CC Switch 做辅助开发配置里记得写全三件套。Cline 的 MCP 配置片段{ mcpServers: { taotoken: { command: npx, args: [-y, taotoken/mcp-server], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_MODEL_ID: claude-sonnet-4-20250514 } } } }Codex 的auth.json片段{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }这些配置的作用是让你在写鼠标控制代码时能随时让模型帮你生成坐标换算逻辑或排查 Winsock 错误码。通道统一了切换模型只改model_id一行。编译时注意链接ws2_32.libMFC 项目在stdafx.h里加#include winsock2.h要放在windows.h之前否则会有一堆重定义错误。这是经典坑我踩过不止一次。4. 验证请求与成功结果跑通一次点击和一次拖拽的远程回放代码写完了怎么验证分三步先验证网络连通再验证单次点击最后验证拖拽。第一步网络连通。接收端先跑起来监听 8000 端口。发送端用ConnectToReceiver(192.168.0.2, 8000)返回 true 说明 TCP 握手成功。如果返回 false先用ping 192.168.0.2确认两台机器互通再检查接收端防火墙是否放行 8000 端口。Windows 防火墙默认会拦入站加一条规则New-NetFirewallRule -DisplayName MouseRemote8000 -Direction Inbound -Protocol TCP -LocalPort 8000 -Action Allow第二步单次点击。发送端在窗口里点一下左键接收端应该看到光标先跳到对应屏幕位置然后触发一次左键点击。怎么确认点击生效在接收端打开一个记事本如果点击落在记事本上光标会定位到点击位置。更直观的办法是接收端开一个画图程序点击会留下一个点。这里有个细节SetCursorPos设置的是屏幕物理坐标如果你的接收端显示器缩放是 150%SetCursorPos(100, 100)实际落在物理像素 (100,100)而你在发送端看到的逻辑坐标可能对应物理 (150,150)。所以发送端必须用ClientToScreen转成物理坐标再发。验证方法发送端把鼠标移到窗口客户区左上角打印screenPt.x, screenPt.y接收端SetCursorPos后光标应该落在屏幕左上角附近。如果偏了检查convert_to_screen是否为 true。第三步拖拽。发送端按住左键从 A 点拖到 B 点接收端应该看到光标从 A 对应的屏幕位置按下移动到 B释放。验证拖拽是否成功可以在接收端开一个支持拖拽的程序比如把文件从桌面拖到文件夹看是否触发拖拽行为。如果只移动不拖拽说明LEFTDOWN没发出去或者被拦截。用 TaoToken 验证模型通道在发送端加一个功能按 F1 让模型生成一组坐标序列比如“从 (200,300) 拖到 (600,500)”。调用模型对话接口curl.exe https://taotoken.net/api/v1/messages -H Content-Type: application/json -H x-api-key: sk-你的Key -H anthropic-version: 2023-06-01 -d {\model\:\claude-sonnet-4-20250514\,\max_tokens\:256,\messages\:[{\role\:\user\,\content\:\生成从(200,300)到(600,500)的拖拽坐标序列每步间隔20像素输出JSON数组每项含x和y\}]}返回的 JSON 数组解析后依次发EV_MOVE中间穿插LEFTDOWN和LEFTUP就能实现“模型生成动作代码执行”的闭环。这一步验证成功说明你的 API 通道和鼠标控制链路都通了。成功结果的标志接收端光标跟随发送端移动点击和拖拽都能在目标程序上生效模型返回的坐标序列能被正确解析并回放。如果这些都做到了恭喜你链路跑通了。实测下来局域网内延迟通常在 5~20ms肉眼几乎无感。如果延迟明显检查是不是每次移动都发了包导致刷屏可以加一个最小移动阈值比如坐标变化小于 3 像素就不发。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 对照这一节把常见报错逐个对照。这些错误大多出在 API 通道配置上不是鼠标代码本身的问题但会卡住你很久。401 Unauthorized。最常见的原因是 Key 没填对或 Base URL 写错。检查config.toml里的api_key是否以sk-开头base_url是否是https://taotoken.net/api。如果你用的是环境变量确认TAOTOKEN_API_KEY已经export或set。PowerShell 里临时设置$env:TAOTOKEN_API_KEY sk-你的Key另一个原因是请求头字段名不对。Anthropic 兼容通道用x-api-keyOpenAI 兼容通道用Authorization: Bearer。TaoToken 的文档里写得很清楚按你用的 SDK 选对字段。401 还会在 Key 被删除或过期时出现去控制台确认 Key 状态。local proxy failed。这个错误通常出现在你本地配了代理但代理没启动或端口不对。TaoToken 的请求不需要走本地代理把HTTP_PROXY、HTTPS_PROXY环境变量清掉再试Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue如果你在用 Cline 或 Claude Code检查它们的设置里有没有proxy字段删掉或留空。这个错误和鼠标控制无关纯粹是网络层配置问题。reading choices 相关报错。这个一般出现在 OpenAI 兼容格式的响应解析里报错信息类似cannot read property choices of undefined。原因是返回体不是预期的 OpenAI 格式可能是 Base URL 拼错了路径或者 Model ID 不存在。检查你的请求 URL 是不是https://taotoken.net/api/v1/chat/completionsModel ID 是不是控制台里列出的可用模型。如果返回体是 Anthropic 格式有content字段而你的代码按choices解析就会报这个错。统一用 Anthropic 格式解析或者确认你调的是哪个兼容端点。OAuth 相关报错。如果你用 Claude Code 的 OAuth 登录流程报错可能是OAuth token expired或invalid_grant。TaoToken 的接入推荐直接用 API Key不走 OAuth。在 Claude Code 的配置里把认证方式改成 API KeyBase URL 填https://taotoken.net/apiKey 填sk-开头的那串。OAuth 流程涉及回调地址和 token 刷新调试成本高用 Key 更直接。鼠标代码本身的坑。SetCursorPos返回 false通常是权限问题——接收端程序没有以管理员身份运行而目标窗口是管理员权限的。以管理员身份跑接收端即可。mouse_event无效检查是否被 UIPI 拦截同样用管理员权限。坐标偏移检查 DPI 缩放在接收端程序清单里加dpiAwareapplication xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/dpiAware /windowsSettings /applicationrecv 返回 0 或 SOCKET_ERROR。说明对端关闭了连接。检查发送端是否崩溃退出或者网络是否断开。在RecvAll里加日志打印每次recv的返回值定位是半包还是断连。结构体大小不一致。发送端和接收端如果编译选项不同比如一个 32 位一个 64 位#pragma pack(1)能保证布局一致但int32_t要确认两边都是 4 字节。用static_assert(sizeof(MousePacket) 13, pack error)在编译期检查。排错的核心思路先确认 API 通道通curl 能返回再确认网络通ping 端口最后确认鼠标 API 有权限管理员。三层分开查不要混在一起。6. 继续往下走把统一 Key 接入你的鼠标控制工具链链路跑通之后你可以做几件事让它更实用。第一把坐标序列的生成交给模型你只需要用自然语言描述动作比如“把窗口从左上拖到右下”模型输出坐标数组代码回放。第二加一个录制功能发送端记录一段鼠标操作序列化成 JSON接收端回放用于自动化测试。第三把 TCP 换成 WebSocket方便跨网段或加一层鉴权。TaoToken 在这里的价值是统一 Key。你不需要为每个模型单独配 Key换模型只改model_id。模型对话入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 你可以在网页上先试 prompt确认输出格式再写进代码。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你打算长期做这类工具Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后给一个实用技巧在发送端加一个热键比如 F2切换“录制/回放”模式录制时把MousePacket存进std::vector回放时按时间戳重发。时间戳用GetTickCount64()回放时按差值Sleep。这样你录一次拖拽就能反复回放调试接收端逻辑时特别省事。代码不用一次写完美先跑通移动再加点击再加拖拽每步验证一次。遇到报错按第 5 节对照排查。通道用 curl 先验鼠标用管理员权限跑坐标用ClientToScreen统一。这三条记住基本不会卡住。