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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,创建后复制保存。然后确认你要用的模型 ID,比如对话类、编码类各有不同,可以在模型对话页 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,适合需要持续调用、按量计费的场景。
把前置准备好,后面写 C# 代码时就不会被「Key 从哪来」打断思路。下一节直接上可复制的配置和代码。
3. 可复制配置:DPI 感知、坐标换算与统一 Key 片段
这一节全是能直接抄的东西。先解决坐标,再解决 AI 调用。
3.1 开启 DPI 感知,让坐标不再偏移
在Program.cs的Main方法最前面加一行,或者用应用程序清单文件。推荐清单方式,一劳永逸:
<?xml version="1.0" encoding="utf-8"?> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> </windowsSettings> </application> </assembly>PerMonitorV2是关键,它让程序在多显示器不同缩放时也能拿到正确的物理坐标。没有这一步,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" } }读取并构造HttpClient:
using 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/api,Key 填同一把,Model ID 保持一致。这样 C# 脚本、Cline、Codex 三处用的是同一套凭证,改一处即可。
3.4 参数对照表
| 配置项 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 固定,不带查询参数 |
| API Key | sk-... | 控制台创建,全工具复用 |
| Model ID | 按需选择 | 在模型列表页确认 |
| 坐标类型 | 物理像素 | 需开启 PerMonitorV2 |
| 移动 API | SetCursorPos | 返回 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 URL(401/404),再确认坐标(偏移),最后确认权限(False)。大部分问题在前两步就能定位。
6. 把统一 Key 用在长期编码与 Agent 任务上
鼠标移动只是桌面自动化的一环,真正吃配置的是长期运行的编码和 Agent 任务。这类任务的特点是调用频繁、模型可能切换、多个工具并行。如果每个工具各配一套 Key,维护成本会指数上升。
统一到 TaoToken 后,你的配置面收敛成三个值:Base URL、Key、Model ID。C# 脚本读appsettings.json,Cline 读 MCP 配置,Claude Code 读环境变量,Codex 读auth.json,但值都一样。换模型时只改 Model ID,换 Key 时只改一处。
对于需要持续调用的场景,比如让 Agent 自动完成一系列桌面操作,建议用 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。它适合按量计费、长期运行的编码任务,不用每次手动管理额度。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各语言的调用示例和参数说明。API Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,可以创建、吊销、查看用量。
最后给一个实用技巧:把 Base URL 和 Key 放在环境变量里,代码只读环境变量,不写死。这样本地开发和 CI 用同一套代码,只换环境变量。C# 里用Environment.GetEnvironmentVariable("TAOTOKEN_API_KEY"),编码工具里用对应的环境变量配置。一处定义,处处生效,这才是「统一 Key」的完整落地。