news 2026/9/27 21:59:57

C语言模拟键盘鼠标事件:TaoToken统一Key接入与config.toml配置骨架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言模拟键盘鼠标事件:TaoToken统一Key接入与config.toml配置骨架

1. 从 keybd_event 到 SendInput:为什么你的自动化脚本需要一个统一通道

如果你写过 Windows 下的 C 语言自动化脚本,大概率见过keybd_event和mouse_event这对老搭档。它们确实能用,几行代码就能让浏览器自己打开、输入关键词、点下回车。但真把它们放进稍微复杂一点的场景,问题就来了:keybd_event是上世纪留下的接口,微软官方文档里明确建议改用SendInput;而当你需要把本地脚本和远端模型能力串起来时,又会撞上第二堵墙——每个模型一个 Key、一套鉴权、一种返回格式,脚本里到处是硬编码的 URL 和密钥。

这篇就聚焦两件事:一是用SendInput把键盘鼠标事件注入写扎实,二是在不引入任何第三方库的前提下,让 C 程序通过一个统一的 Key 通道去调用模型接口。适合正在做本地自动化、RPA 小工具、或者想把脚本接进统一 API 通道的开发者。全程只用 Windows 原生 API 加一个 HTTP 客户端,编译环境用 MSVC 或 MinGW 都行。

我试过把事件注入和网络请求混在一个main里,结果调试时根本分不清是按键没生效还是请求超时。所以下面会拆成清晰的步骤,每一步都能单独验证。

2. TaoToken 前置准备:统一 Key 与 config.toml 骨架

TaoToken 在这里扮演的角色是「统一入口」:你不需要在 C 代码里为不同模型维护不同的地址和密钥,而是通过一个 Key 走同一个 API 端点。官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api 。

先做两件准备工作。

第一,拿到 Key。进入控制台创建 API Key,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建后复制保存。Key 只在创建时完整显示一次,丢了只能重建。

第二,写配置文件。C 语言读 TOML 没有标准库,但 TOML 本身语法简单,我们可以写一个极简解析器,或者干脆用「键=值」逐行读取。下面这份config.toml骨架可以直接复制,字段含义写在注释里:

# config.toml - TaoToken 统一接入配置骨架 [api] # 统一 API 端点,不要带末尾斜杠 base_url = "https://taotoken.net/api" # 从控制台创建的 Key,形如 sk-xxxx api_key = "sk-替换成你自己的Key" # 请求超时,单位毫秒 timeout_ms = 30000 [model] # 对话模型标识,按需替换 name = "claude-sonnet" # 单次最大输出 token max_tokens = 1024 # 采样温度 temperature = 0.7 [input] # 模拟按键之间的间隔,单位毫秒,太快会被目标程序丢弃 key_delay_ms = 120 # 鼠标移动后到点击的等待时间 click_delay_ms = 200

这里有个容易踩的坑:base_url末尾不要加斜杠,否则拼接/v1/messages之类的路径时会出现双斜杠,部分网关会直接返回 404。另外api_key不要提交到 Git,建议在.gitignore里加上config.toml。

如果你后续要做长期编码或 Agent 类任务,可以了解 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它更适合持续性的开发场景;而本篇这种「脚本触发一次请求」的用法,用普通 API Key 就够了。

3. 可复制配置:SendInput 事件注入 + 配置读取

3.1 用 SendInput 替代 keybd_event

SendInput的核心是构造INPUT结构体数组,一次性提交。相比keybd_event,它能更精确地控制扫描码、支持 Unicode 输入、并且返回实际注入的事件数量,方便做错误判断。

先看键盘部分。下面这个函数把「按下并松开一个虚拟键」封装起来:

#include <windows.h> #include <stdio.h> // 模拟一次按键:按下 + 松开 // vk: 虚拟键码,例如 'A'、VK_RETURN static int send_key(WORD vk, DWORD delay_ms) { INPUT inputs[2]; ZeroMemory(inputs, sizeof(inputs)); // 按下 inputs[0].type = INPUT_KEYBOARD; inputs[0].ki.wVk = vk; inputs[0].ki.wScan = 0; inputs[0].ki.dwFlags = 0; // 松开 inputs[1].type = INPUT_KEYBOARD; inputs[1].ki.wVk = vk; inputs[1].ki.wScan = 0; inputs[1].ki.dwFlags = KEYEVENTF_KEYUP; UINT sent = SendInput(2, inputs, sizeof(INPUT)); if (sent != 2) { // 返回实际注入数量,不等于 2 说明被拦截或参数错误 return -1; } Sleep(delay_ms); return 0; }

鼠标部分同理,用INPUT_MOUSE类型。移动用MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE,点击用MOUSEEVENTF_LEFTDOWN和MOUSEEVENTF_LEFTUP:

// 绝对坐标移动 + 左键单击 // x, y 为屏幕绝对坐标(0~65535 归一化前需换算) static int send_click(int x, int y, DWORD delay_ms) { // 屏幕尺寸换算成 0~65535 int screen_w = GetSystemMetrics(SM_CXSCREEN); int screen_h = GetSystemMetrics(SM_CYSCREEN); LONG nx = (LONG)((double)x * 65535 / (screen_w - 1)); LONG ny = (LONG)((double)y * 65535 / (screen_h - 1)); INPUT inputs[3]; ZeroMemory(inputs, sizeof(inputs)); inputs[0].type = INPUT_MOUSE; inputs[0].mi.dx = nx; inputs[0].mi.dy = ny; inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; inputs[1].type = INPUT_MOUSE; inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTDOWN; inputs[2].type = INPUT_MOUSE; inputs[2].mi.dwFlags = MOUSEEVENTF_LEFTUP; UINT sent = SendInput(3, inputs, sizeof(INPUT)); if (sent != 3) { return -1; } Sleep(delay_ms); return 0; }

注意坐标归一化:SendInput的绝对坐标是 0 到 65535 的映射值,不是像素。如果你直接传像素值,鼠标会跑到屏幕角落。上面用GetSystemMetrics做了换算,这是最稳的写法。

3.2 读取 config.toml 的极简实现

不引入第三方库,就按行读、按=切分。下面这个函数把配置读进一个结构体:

typedef struct { char base_url[256]; char api_key[256]; char model[64]; int timeout_ms; int key_delay_ms; int click_delay_ms; } AppConfig; // 去掉首尾空白 static void trim(char *s) { char *p = s; while (*p == ' ' || *p == '\t') p++; if (p != s) memmove(s, p, strlen(p) + 1); size_t len = strlen(s); while (len > 0 && (s[len-1] == ' ' || s[len-1] == '\t' || s[len-1] == '\r' || s[len-1] == '\n')) { s[--len] = '\0'; } } static int load_config(const char *path, AppConfig *cfg) { FILE *fp = fopen(path, "r"); if (!fp) return -1; char line[512]; while (fgets(line, sizeof(line), fp)) { trim(line); if (line[0] == '#' || line[0] == '[' || line[0] == '\0') continue; char *eq = strchr(line, '='); if (!eq) continue; *eq = '\0'; char *key = line; char *val = eq + 1; trim(key); trim(val); // 去掉值两端的引号 size_t vlen = strlen(val); if (vlen >= 2 && val[0] == '"' && val[vlen-1] == '"') { val[vlen-1] = '\0'; val++; } if (strcmp(key, "base_url") == 0) strncpy(cfg->base_url, val, 255); else if (strcmp(key, "api_key") == 0) strncpy(cfg->api_key, val, 255); else if (strcmp(key, "model") == 0) strncpy(cfg->model, val, 63); else if (strcmp(key, "timeout_ms") == 0) cfg->timeout_ms = atoi(val); else if (strcmp(key, "key_delay_ms") == 0) cfg->key_delay_ms = atoi(val); else if (strcmp(key, "click_delay_ms") == 0) cfg->click_delay_ms = atoi(val); } fclose(fp); return 0; }

这个解析器不处理多行数组、内联表等复杂 TOML 语法,但对上面那份骨架完全够用。如果你以后要扩展,建议换成tomlc99这类单文件库,仍然不违反「不依赖重型第三方库」的原则。

4. 验证请求:一次模拟按键触发 + 返回码检查

现在把事件注入和通道调用串起来。思路是:脚本先模拟按键打开一个输入框并输入内容,然后把这个内容作为 prompt 发给统一 API,最后检查 HTTP 返回码和响应体。

4.1 用 WinHTTP 发请求

Windows 自带 WinHTTP,不需要额外链接库(链接时加-lwinhttp)。下面是一个最小 POST 封装:

#include <winhttp.h> #pragma comment(lib, "winhttp.lib") // 返回 HTTP 状态码,响应体写入 resp_buf static int http_post_json(const AppConfig *cfg, const char *body, char *resp_buf, size_t resp_size) { int status = -1; HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL; // 解析主机名和路径(这里假设 base_url 为 https://taotoken.net/api) hSession = WinHttpOpen(L"C-Auto/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) goto cleanup; hConnect = WinHttpConnect(hSession, L"taotoken.net", INTERNET_DEFAULT_HTTPS_PORT, 0); if (!hConnect) goto cleanup; hRequest = WinHttpOpenRequest(hConnect, L"POST", L"/api/v1/messages", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); if (!hRequest) goto cleanup; // 组装请求头 wchar_t headers[512]; swprintf(headers, 512, L"Content-Type: application/json\r\n" L"Authorization: Bearer %hs\r\n", cfg->api_key); if (!WinHttpSendRequest(hRequest, headers, -1L, (LPVOID)body, (DWORD)strlen(body), (DWORD)strlen(body), 0)) { goto cleanup; } if (!WinHttpReceiveResponse(hRequest, NULL)) goto cleanup; DWORD code = 0, len = sizeof(code); WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &code, &len, WINHTTP_NO_HEADER_INDEX); status = (int)code; // 读响应体 DWORD read = 0, total = 0; while (WinHttpReadData(hRequest, resp_buf + total, (DWORD)(resp_size - total - 1), &read) && read > 0) { total += read; if (total >= resp_size - 1) break; } resp_buf[total] = '\0'; cleanup: if (hRequest) WinHttpCloseHandle(hRequest); if (hConnect) WinHttpCloseHandle(hConnect); if (hSession) WinHttpCloseHandle(hSession); return status; }

4.2 主流程:按键触发 + 请求 + 返回码验证

int main(void) { AppConfig cfg; memset(&cfg, 0, sizeof(cfg)); if (load_config("config.toml", &cfg) != 0) { printf("config.toml 读取失败\n"); return 1; } // 1. 模拟按键:输入 "hello" 并回车 const WORD keys[] = { 'H', 'E', 'L', 'L', 'O', VK_RETURN }; for (int i = 0; i < 6; i++) { if (send_key(keys[i], cfg.key_delay_ms) != 0) { printf("按键注入失败,索引 %d\n", i); return 2; } } // 2. 模拟鼠标点击 (400, 300) if (send_click(400, 300, cfg.click_delay_ms) != 0) { printf("鼠标注入失败\n"); return 3; } // 3. 调用统一 API const char *body = "{\"model\":\"claude-sonnet\"," "\"max_tokens\":64," "\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}"; char resp[4096]; int status = http_post_json(&cfg, body, resp, sizeof(resp)); printf("HTTP 状态码: %d\n", status); printf("响应片段: %.200s\n", resp); if (status == 200) { printf("通道调用成功\n"); } else { printf("通道调用失败,请检查 Key 与端点\n"); } return 0; }

编译命令(MinGW 为例):

gcc main.c -o auto.exe -lwinhttp -luser32

运行后你应该看到类似输出:

HTTP 状态码: 200 响应片段: {"id":"msg_...","content":[{"type":"text","text":"pong"}]} 通道调用成功

如果状态码是 200,说明事件注入和统一通道都跑通了。如果按键没生效但状态码正常,问题在事件注入侧;如果状态码是 401 或 404,问题在配置侧。

5. 本篇常见错排查

5.1 SendInput 返回 0 或小于预期

最常见的原因是权限。如果你的目标程序以管理员身份运行,而你的脚本是普通权限,SendInput会被 UIPI(用户界面特权隔离)拦截,返回 0。解决办法是让脚本和目标程序权限一致,或者用管理员权限运行脚本。

另一个原因是INPUT结构体没有ZeroMemory,残留的dwFlags导致事件类型错乱。上面代码里每次都清零,就是为了避开这个坑。

5.2 鼠标点击位置偏移

如果你传的是像素坐标而没做 0~65535 归一化,鼠标会跑到屏幕边缘。检查send_click里的换算公式,确认用了GetSystemMetrics(SM_CXSCREEN)。另外多显示器环境下,SM_CXSCREEN只返回主屏宽度,跨屏点击需要改用MOUSEEVENTF_VIRTUALDESK。

5.3 HTTP 返回 401

Authorization头格式是Bearer sk-xxxx,注意Bearer和 Key 之间有一个空格。如果 Key 是从控制台复制的,确认没有把首尾空格带进去。另外检查config.toml里api_key的引号是否被正确去掉——上面的解析器会去掉成对引号,但如果你手写时漏了引号,值里可能混入注释。

5.4 HTTP 返回 404

大概率是base_url末尾多了斜杠,或者请求路径拼错。本篇用的路径是/api/v1/messages,如果你的端点不同,以接入文档为准。文档地址是 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的路径和参数说明。

5.5 编译报错找不到 winhttp.h

MinGW 需要安装mingw-w64的完整包,MSVC 则默认自带。链接时别忘了-lwinhttp。如果用的是旧版 MinGW,可能还需要-lole32。

6. 把事件注入和统一通道拆开维护

走到这里,你已经有了一个能跑的最小闭环:SendInput负责本地事件,config.toml负责配置,WinHTTP 负责通道调用。实际项目里,我建议把这三块拆成独立文件——input.c只管按键鼠标,config.c只管读配置,api.c只管网络请求。这样调试时你可以单独替换某一块,而不用在几百行main里找问题。

如果你接下来要验证不同模型的返回差异,可以直接用模型对话页面快速对比,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,不用每次都改 C 代码重新编译。而当你需要管理多个 Key、查看调用量时,控制台里的 API Keys 页面会更方便:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。

最后提醒一句:SendInput注入的事件会进入真实的系统输入队列,调试时把key_delay_ms调大一点,给自己留出按Ctrl+C的时间,否则脚本可能会在你没准备好的窗口里乱点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 21:56:35

Oracle pipelined函数实战:用PLSQL管道函数实现高性能大数据处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 21:55:13

Claude 接入 MiniMax 模型报 400 invalid params:system(2013) 配置排查与修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华