
1. 从一次坐标偏移说起C# 鼠标移动到底难在哪很多人第一次写 C# 桌面自动化都会觉得「移动鼠标」是最简单的一步调个SetCursorPos不就完了我最早也是这么想的直到在一个双屏 4K 缩放 150% 的机器上脚本把鼠标移到了「看起来对、实际偏了 300 像素」的位置点击全部落空。问题不在 API而在坐标系、DPI 缩放和屏幕边界这三件事上。先把概念说清楚。C# 操作鼠标移动到指定的屏幕位置本质是调用 Windows 的user32.dll里的SetCursorPos(int x, int y)它接收的是物理屏幕坐标原点在左上角x 向右增大y 向下增大。而System.Windows.Forms.Cursor.Position用的是逻辑坐标在开启 DPI 感知后两者才会一致。如果你用 WinForms 的Screen.PrimaryScreen.Bounds拿到的是逻辑尺寸直接喂给SetCursorPos在缩放屏上就会错位。这套东西适合谁三类人一是做 RPA/自动化测试的需要精确点击某个控件二是做游戏辅助或批量操作的注意合规边界三是把桌面脚本接上大模型让 AI 决定「点哪里」的开发者。第三类正是本文的重点——脚本本身不难难的是脚本要调用外部 AI 服务时endpoint 和 Key 散落在各个工具里改一次配置要动五六个文件。我试过把 OpenAI、Claude、本地模型的 Key 分别写在appsettings.json、环境变量、还有某个硬编码的常量里结果换一台机器就崩。后来统一改成 TaoToken 的单一 Key 单一 Base URL所有调用方只认这一套配置量直接砍掉一大半。下面就从坐标换算讲到统一 Key 的落地。这一节先给结论移动鼠标用SetCursorPos坐标换算用 DPI 感知 物理像素AI 调用统一走 TaoToken。三件事拆开都不复杂合在一起才是能跑的自动化脚本。2. TaoToken 前置把散落的 endpoint 和 Key 收成一套在写代码之前先把「外部 AI 服务」这条链路理清。桌面自动化脚本经常需要 AI 参与决策比如截图后让模型判断「下一步点哪个按钮」或者把自然语言指令翻译成坐标。这时候脚本里就会出现 HTTP 请求而请求需要 Base URL 和 API Key。传统做法是每个工具各配一套C# 脚本里写一份Python 辅助脚本写一份Cline 或 Claude Code 这类编码工具再配一份。问题很明显——Key 轮换时要改多处模型换版本时要同步多处团队协作时新人根本不知道哪份配置是生效的。TaoToken 的思路是提供一个统一的 API 入口Base URL 固定为https://taotoken.net/api所有模型调用都走这个地址Key 也只用一把。你可以在官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 了解整体能力实际接入时只需要记住两个东西Base URL 和 API Key。对 C# 脚本来说这意味着HttpClient的BaseAddress只设一次Authorization头只填一把 Key。对编码工具来说无论是 Cline 的 MCP 配置、Claude Code 的环境变量还是 Codex 的auth.json都指向同一个地址。这就是「统一 Key」的价值一处配置多处复用。具体到操作你需要先拿到 Key。进入控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建后复制保存。然后确认你要用的模型 ID比如对话类、编码类各有不同可以在模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 查看当前可用的模型列表。这里有个容易踩的坑很多人把 Base URL 写成https://taotoken.net少了/api结果请求 404。记住API 调用一律带/api后缀且这个地址不加任何查询参数。Key 放在请求头Authorization: Bearer 你的Key里不要拼在 URL 上。如果你是用 Claude Code 做长期编码或者用 Coding Plan 跑 Agent 任务配置方式略有不同但核心还是那两样Base URL 和 Key。Coding Plan 的入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 适合需要持续调用、按量计费的场景。把前置准备好后面写 C# 代码时就不会被「Key 从哪来」打断思路。下一节直接上可复制的配置和代码。3. 可复制配置DPI 感知、坐标换算与统一 Key 片段这一节全是能直接抄的东西。先解决坐标再解决 AI 调用。3.1 开启 DPI 感知让坐标不再偏移在Program.cs的Main方法最前面加一行或者用应用程序清单文件。推荐清单方式一劳永逸?xml version1.0 encodingutf-8? assembly manifestVersion1.0 xmlnsurn:schemas-microsoft-com:asm.v1 application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettingsPerMonitorV2/dpiAwareness /windowsSettings /application /assemblyPerMonitorV2是关键它让程序在多显示器不同缩放时也能拿到正确的物理坐标。没有这一步SetCursorPos在 150% 缩放的屏幕上会偏。3.2 鼠标移动的核心代码using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; public static class MouseMover { [DllImport(user32.dll, SetLastError true)] private static extern bool SetCursorPos(int x, int y); [DllImport(user32.dll)] private static extern bool GetCursorPos(out POINT lpPoint); [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; } /// summary /// 移动鼠标到物理屏幕坐标 /// /summary public static bool MoveTo(int x, int y) { return SetCursorPos(x, y); } /// summary /// 回读当前鼠标坐标用于验证 /// /summary public static Point GetPosition() { GetCursorPos(out POINT p); return new Point(p.X, p.Y); } /// summary /// 移动到主屏中心物理像素 /// /summary public static void MoveToPrimaryCenter() { var bounds Screen.PrimaryScreen.Bounds; MoveTo(bounds.Width / 2, bounds.Height / 2); } }注意Screen.PrimaryScreen.Bounds在 DPI 感知开启后返回的是物理像素可以直接用。如果你没开 DPI 感知这里拿到的是逻辑像素需要乘以缩放比。3.3 统一 Key 的配置片段C# 侧建议用appsettings.json存配置避免硬编码{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-你的Key, ModelId: 你的模型ID } }读取并构造HttpClientusing System.Net.Http; using System.Net.Http.Headers; using Microsoft.Extensions.Configuration; var config new ConfigurationBuilder() .AddJsonFile(appsettings.json) .Build(); var baseUrl config[TaoToken:BaseUrl]; var apiKey config[TaoToken:ApiKey]; var client new HttpClient { BaseAddress new Uri(baseUrl) }; client.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, apiKey);如果你同时用 Cline 的 MCP配置里也要写全三件套。以 MCP 的 JSON 为例{ mcpServers: { taotoken: { url: https://taotoken.net/api, headers: { Authorization: Bearer sk-你的Key }, model: 你的模型ID } } }Codex 的auth.json同理Base URL 填https://taotoken.net/apiKey 填同一把Model ID 保持一致。这样 C# 脚本、Cline、Codex 三处用的是同一套凭证改一处即可。3.4 参数对照表配置项值说明Base URLhttps://taotoken.net/api固定不带查询参数API Keysk-...控制台创建全工具复用Model ID按需选择在模型列表页确认坐标类型物理像素需开启 PerMonitorV2移动 APISetCursorPos返回 bool失败查 GetLastError配置齐了下一节验证。4. 验证请求移动到位并回读坐标写完代码不验证等于没写。这一节给一个完整的验证流程包含鼠标移动和 AI 调用两部分。4.1 鼠标移动验证class Program { static void Main() { // 目标坐标主屏 (800, 600) int targetX 800, targetY 600; bool ok MouseMover.MoveTo(targetX, targetY); Console.WriteLine($SetCursorPos 返回: {ok}); // 回读 var pos MouseMover.GetPosition(); Console.WriteLine($当前坐标: ({pos.X}, {pos.Y})); if (pos.X targetX pos.Y targetY) Console.WriteLine(移动成功坐标一致); else Console.WriteLine($坐标不一致偏差 ({pos.X - targetX}, {pos.Y - targetY})); } }运行后你应该看到SetCursorPos 返回: True 当前坐标: (800, 600) 移动成功坐标一致如果返回False用Marshal.GetLastWin32Error()拿错误码。常见的是坐标超出屏幕范围或者程序没有桌面会话权限比如跑在服务里。4.2 AI 调用验证用同一个HttpClient发一次对话请求确认 Key 和 Base URL 生效var payload new { model config[TaoToken:ModelId], messages new[] { new { role user, content 回复 OK 两个字母即可 } } }; var json System.Text.Json.JsonSerializer.Serialize(payload); var content new StringContent(json, System.Text.Encoding.UTF8, application/json); var resp await client.PostAsync(/v1/chat/completions, content); var body await resp.Content.ReadAsStringAsync(); Console.WriteLine($状态码: {resp.StatusCode}); Console.WriteLine(body);成功时状态码 200返回体里能看到模型回复。如果 401说明 Key 不对或没带Bearer如果 404检查 Base URL 是否漏了/api。4.3 把两者串起来真实场景是截图 → 发给模型 → 模型返回坐标 → 移动鼠标。验证时可以先跳过截图直接让模型返回一个固定坐标// 假设模型返回 {x: 800, y: 600} MouseMover.MoveTo(800, 600); var pos MouseMover.GetPosition(); Console.WriteLine($AI 指定坐标执行结果: ({pos.X}, {pos.Y}));这一步跑通说明「AI 决策 鼠标执行」的闭环成立。剩下的就是业务逻辑。5. 常见报错排查401、local proxy failed 与坐标偏移这一节按真实报错来遇到哪个查哪个。401 Unauthorized。最常见。原因有三Key 写错、没加Bearer前缀、Key 已失效。检查Authorization头的格式必须是Bearer sk-xxx中间一个空格。如果用的是环境变量确认变量名拼写正确且程序读到了。TaoToken 的 Key 在控制台可重新生成旧 Key 作废后所有调用方都要更新。local proxy failed。这个报错通常出现在编码工具里比如 Cline 或 Claude Code 配置了本地代理但代理没启动。如果你没有用本地代理检查配置里是否残留了http://127.0.0.1:xxxx之类的地址。统一走 TaoToken 后Base URL 应该是https://taotoken.net/api不需要本地代理。把配置里的代理项删掉即可。reading choices 报错。这多半是响应体解析失败常见于模型返回格式和代码预期不一致。先打印原始响应体确认结构。如果是流式返回注意choices在 SSE 的每个data:块里不是一次性返回。用非流式请求先验证再切流式。OAuth 相关报错。Claude Code 这类工具可能走 OAuth 流程如果你用 API Key 方式接入需要在配置里明确指定 Key 而不是走 OAuth。检查配置文件里是否有oauth字段删掉或改为 Key 认证。Codex 的auth.json里如果同时有 OAuth token 和 API Key可能冲突保留 Key 即可。坐标偏移。前面提过根因是 DPI。验证方法移动鼠标到 (0,0)看是否真的到左上角。如果到了但 (800,600) 偏了说明缩放比没处理。开启 PerMonitorV2 后重启程序。另一个可能是多显示器SetCursorPos用的是虚拟屏幕坐标副屏在主屏左边时 x 可能为负这是正常的。SetCursorPos 返回 False。用Marshal.GetLastWin32Error()拿错误码。常见 5拒绝访问说明程序权限不够比如以服务方式运行。改成用户会话下运行或以管理员身份启动。移动成功但点击无效。鼠标移动和点击是两回事。移动到位后点击需要mouse_event或SendInput。如果目标窗口没激活点击可能落到别的窗口。先SetForegroundWindow再点击。排查顺序建议先确认 Key 和 Base URL401/404再确认坐标偏移最后确认权限False。大部分问题在前两步就能定位。6. 把统一 Key 用在长期编码与 Agent 任务上鼠标移动只是桌面自动化的一环真正吃配置的是长期运行的编码和 Agent 任务。这类任务的特点是调用频繁、模型可能切换、多个工具并行。如果每个工具各配一套 Key维护成本会指数上升。统一到 TaoToken 后你的配置面收敛成三个值Base URL、Key、Model ID。C# 脚本读appsettings.jsonCline 读 MCP 配置Claude Code 读环境变量Codex 读auth.json但值都一样。换模型时只改 Model ID换 Key 时只改一处。对于需要持续调用的场景比如让 Agent 自动完成一系列桌面操作建议用 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。它适合按量计费、长期运行的编码任务不用每次手动管理额度。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各语言的调用示例和参数说明。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 可以创建、吊销、查看用量。最后给一个实用技巧把 Base URL 和 Key 放在环境变量里代码只读环境变量不写死。这样本地开发和 CI 用同一套代码只换环境变量。C# 里用Environment.GetEnvironmentVariable(TAOTOKEN_API_KEY)编码工具里用对应的环境变量配置。一处定义处处生效这才是「统一 Key」的完整落地。