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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。先做两件准备工作。第一拿到 Key。进入控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建后复制保存。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_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合持续性的开发场景而本篇这种「脚本触发一次请求」的用法用普通 API Key 就够了。3. 可复制配置SendInput 事件注入 配置读取3.1 用 SendInput 替代 keybd_eventSendInput的核心是构造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(LC-Auto/1.0, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) goto cleanup; hConnect WinHttpConnect(hSession, Ltaotoken.net, INTERNET_DEFAULT_HTTPS_PORT, 0); if (!hConnect) goto cleanup; hRequest WinHttpOpenRequest(hConnect, LPOST, 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, LContent-Type: application/json\r\n LAuthorization: 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 返回 401Authorization头格式是Bearer sk-xxxx注意Bearer和 Key 之间有一个空格。如果 Key 是从控制台复制的确认没有把首尾空格带进去。另外检查config.toml里api_key的引号是否被正确去掉——上面的解析器会去掉成对引号但如果你手写时漏了引号值里可能混入注释。5.4 HTTP 返回 404大概率是base_url末尾多了斜杠或者请求路径拼错。本篇用的路径是/api/v1/messages如果你的端点不同以接入文档为准。文档地址是 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的路径和参数说明。5.5 编译报错找不到 winhttp.hMinGW 需要安装mingw-w64的完整包MSVC 则默认自带。链接时别忘了-lwinhttp。如果用的是旧版 MinGW可能还需要-lole32。6. 把事件注入和统一通道拆开维护走到这里你已经有了一个能跑的最小闭环SendInput负责本地事件config.toml负责配置WinHTTP 负责通道调用。实际项目里我建议把这三块拆成独立文件——input.c只管按键鼠标config.c只管读配置api.c只管网络请求。这样调试时你可以单独替换某一块而不用在几百行main里找问题。如果你接下来要验证不同模型的返回差异可以直接用模型对话页面快速对比地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 不用每次都改 C 代码重新编译。而当你需要管理多个 Key、查看调用量时控制台里的 API Keys 页面会更方便https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。最后提醒一句SendInput注入的事件会进入真实的系统输入队列调试时把key_delay_ms调大一点给自己留出按CtrlC的时间否则脚本可能会在你没准备好的窗口里乱点。
企业数字化 ERP 产品动态
相关推荐
XXAgent 开发 Koog 选型与使用指南 本文面向 XXAgent 开发,覆盖两部分:① 为什么选 Koog(技术选型),② Koog 是什么、怎么用、在本项目怎么落地,并附实测踩坑。
适用版本:ai.koog:koog-agents 1.2.0(见 gradle/libs.v… · 2026/9/27 21:59:51
深度解析 Vibe Coding:用 TaoToken 统一 Key 打通 AI 原生 IDE 的编程新范式 /* 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:59:51
Cursor 登录按钮失效?从日志揪出“真凶”——Ubuntu 22.04 排障全记录 /* 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:59:51
大学生做课程项目用什么AI编程软件?最新权威8款AI编程工具全解析(含TaoToken统一Key配置) /* 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 22:35:18
Claude Code 使用指南:从 CLAUDE.md 到 Skill 子代理的协作配置 /* 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 22:35:18
python如何做接口自动化测试 进行接口自动化测试的步骤主要包括这些方面呢, 第一个阶段是选择合适的测试框架, 第二个阶段是要编写测试脚本, 第三个阶段是需要设置测试环境, 第四个阶段是执行测试并生成报告。在这里, 我们会把如何使用进行接口自动化测试的过程详细去描述一下的。一、去挑选一个恰当的测试… · 2026/9/27 22:35:18
做服装在哪个网站找新手入门 不会代码做服装站?3步图解步骤教你低成本起步 你是不是也卡在这一步:手里有货,脑子里有图,但就是不会代码,看着那些后台配置头大? 别急, 做服装在哪个网站找 货源只是第一步,怎么把展示出来才是关键。 今天不聊虚的,直接上 图解步骤… · 2026/9/27 22:35:12
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01