用 Python 实现 WorkBuddy 每日自动领积分,踩了这些坑才搞定
本文记录了一次真实的 Windows 桌面自动化实战:用 Python ctypes 控制鼠标,自动完成 WorkBuddy 软件的每日签到领积分操作,并配置 Windows 任务计划程序实现每天定时自动执行。
起因
WorkBuddy 每天登录可以领取积分,但每天手动点几下感觉很麻烦。
能不能写个脚本,让电脑自动完成这几次点击?
答案是可以,但过程并没有想象中简单。
整体思路
签到流程只有两步:
看起来很简单,实际上踩了不少坑。
环境准备
- 系统
- 分辨率
- Python:3.13(WorkBuddy 内置 Python 版本)
第一步:找到按钮的屏幕坐标
自动点击的前提是知道要点哪里。
写了一个坐标追踪工具,将鼠标移到目标位置,按 F9 记录坐标:
python📋 长按下文选中复制
import ctypes, ctypes.wintypes, timeuser32 = ctypes.windll.user32kernel32 = ctypes.windll.kernel32# 设置 DPI 感知,确保坐标是物理像素ctypes.windll.shcore.SetProcessDpiAwareness(2)# 按 F9 记录当前鼠标坐标,按 F10 退出print("移动鼠标到目标位置,按 F9 记录,按 F10 退出")coords = []while True: if kernel32.GetAsyncKeyState(0x79) & 0x8000: # F10 退出 break if kernel32.GetAsyncKeyState(0x78) & 0x8000: # F9 记录 pt = ctypes.wintypes.POINT() user32.GetCursorPos(ctypes.byref(pt)) coords.append((pt.x, pt.y)) print(f"记录坐标: ({pt.x}, {pt.y})") time.sleep(0.5) time.sleep(0.05)print("所有坐标:", coords)实测采集到的坐标:
第二步:写点击脚本
有了坐标,开始写自动点击。
⚠ 首次踩坑
最初尝试用 `SendInput`(Windows 官方推荐的模拟输入 API),结果在 DPI 200% 下坐标归一化不准,点偏了。
最终稳定下来的方案是:SetCursorPos 移动光标 + mouse_event 执行点击。
python📋 长按下文选中复制
import ctypes, ctypes.wintypes, timeuser32 = ctypes.windll.user32ctypes.windll.shcore.SetProcessDpiAwareness(2)COORD_USER_MENU = (86, 1293) # 用户菜单按钮坐标COORD_CHECKIN_BTN = (234, 637) # 立即领取按钮坐标def click(x, y): user32.SetCursorPos(x, y) # 移动光标到目标位置 time.sleep(0.3) # 等待光标稳定 user32.mouse_event(0x0002, 0, 0, 0, 0) # 鼠标左键按下 time.sleep(0.08) user32.mouse_event(0x0004, 0, 0, 0, 0) # 鼠标左键释放# 激活 WorkBuddy 窗口hwnd = user32.FindWindowW(None, "WorkBuddy")user32.SetForegroundWindow(hwnd)user32.BringWindowToTop(hwnd)time.sleep(0.6)# 点击用户菜单click(*COORD_USER_MENU)time.sleep(2.5) # 等待菜单展开# 点击领取积分click(*COORD_CHECKIN_BTN)print("签到完成!")第三步:踩到最大的坑——UIPI 权限隔离
脚本写好了,鼠标能移动,却怎么也点不进去。WorkBuddy 完全没反应。
找了很久,查了 Windows 官方文档,才发现根本原因:
**Windows UIPI(用户界面特权隔离)**:从 Vista 开始,低权限进程无法向高权限进程发送模拟鼠标/键盘事件。
WorkBuddy 以管理员权限运行,而普通 CMD 里的 Python 脚本是普通权限——两者之间有一道隔离墙,mouse_event 发出的点击事件直接被系统底层丢弃,目标程序根本没收到。
💡 解决方法
**以管理员身份运行 Python 脚本。**右键开始菜单 → **Windows PowerShell(管理员)** → 运行脚本。
改完立刻成功,菜单展开,积分到账。
第四步:配置 Windows 定时任务
手动运行还不够,要实现每天 8:00 自动签到。
在管理员 PowerShell 中运行以下命令(一次性配置,永久生效):
powershell📋 长按下文选中复制
$action = New-ScheduledTaskAction ` -Execute "C:\Users\xyf\.workbuddy\binaries\python\versions\3.13.12\python.exe" ` -Argument "C:\Users\xyf\WorkBuddy\checkin\test_move.py"$trigger = New-ScheduledTaskTrigger -Daily -At "08:00"$principal = New-ScheduledTaskPrincipal ` -UserId "$env:USERDOMAIN\$env:USERNAME" ` -LogonType Interactive ` -RunLevel Highest # 以最高权限(管理员)运行,解决 UIPI 问题$settings = New-ScheduledTaskSettingsSet ` -StartWhenAvailable # 如果 8:00 时锁屏,解锁后自动补跑Register-ScheduledTask ` -TaskName "WorkBuddy每日签到" ` -Action $action ` -Trigger $trigger ` -Principal $principal ` -Settings $settings ` -Force
关键参数说明
定时任务优化建议
创建任务后,打开任务计划程序(Win + R → taskschd.msc),找到 "WorkBuddy每日签到" 任务,右键 → 属性:
- 常规:勾选「使用最高权限运行」,保证脚本中点击功能生效
- 触发器:增加一条「解锁时触发」,确保锁屏状态下也能自动执行
- 操作:确认 Python 路径和 `test_move.py` 路径正确
踩坑汇总
| | | |
|---|
| | | 改用 `mouse_event`(无 ABSOLUTE 标志) |
| | | |
| | | 使用 `kernel32.GetConsoleWindow()` |
| | | |
| | | |
总结
这次自动化开发的核心收获:
- Windows 桌面自动化首选 `SetCursorPos + mouse_event`,`SendInput` 在高 DPI 环境下坐标换算容易出问题
- UIPI 权限隔离是最常见的点击失效原因,遇到鼠标能移动但点击无反应,第一个想到的就是权限问题
- 坐标必须在目标机器上实时采集
整个开发过程大概花了 2 个小时,其中 80% 的时间都在排查 UIPI 这一个问题。希望这篇文章能帮你少走弯路。
💡 小贴士
如果你觉得配置过程比较复杂,可以把文档发给 WorkBuddy,让它学习一遍后帮助你一起完成。后续 WorkBuddy 软件更新导致按钮位置发生变化,可以重新定位一次坐标信息。
附录:完整 test_move.py 脚本
python📋 长按下文选中复制
# test_move.py - 鼠标移动诊断import ctypes, ctypes.wintypes, timeuser32 = ctypes.windll.user32kernel32 = ctypes.windll.kernel32# 尝试各种 DPI 感知设置print("=== DPI 感知测试 ===")try: ctypes.windll.shcore.SetProcessDpiAwareness(2) print("[OK] SetProcessDpiAwareness(2) 成功")except Exception as e: print(f"[失败] SetProcessDpiAwareness: {e}") try: user32.SetProcessDPIAware() print("[OK] SetProcessDPIAware() 成功") except Exception as e2: print(f"[失败] SetProcessDPIAware: {e2}")# 获取 DPI 值try: from ctypes import wintypes class POINT(ctypes.Structure): _fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)] # 获取主显示器 DPI hdc = user32.GetDC(0) dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88) # LOGPIXELSX print(f"当前 DPI: {dpi}") ctypes.windll.gdi32.ReleaseDC(0, hdc)except Exception as e: print(f"获取 DPI 失败: {e}")# 测试1:移动到屏幕中心sw = user32.GetSystemMetrics(0) # SM_CXSCREENsh = user32.GetSystemMetrics(1) # SM_CYSCREENprint(f"\n屏幕分辨率(GetSystemMetrics): {sw} x {sh}")cx = sw // 2cy = sh // 2print(f"尝试移动到屏幕中心: ({cx}, {cy})")ret = user32.SetCursorPos(cx, cy)print(f"SetCursorPos 返回值: {ret} (0=失败, 非0=成功)")time.sleep(2)# 验证实际位置pt = wintypes.POINT()user32.GetCursorPos(ctypes.byref(pt))print(f"GetCursorPos 实际位置: ({pt.x}, {pt.y})")if pt.x == cx and pt.y == cy: print("\n✅ SetCursorPos 工作正常!")else: print(f"\n❌ 坐标不匹配!目标({cx},{cy}) 实际({pt.x},{pt.y})") print(" 可能原因:DPI 缩放导致坐标被系统自动换算")# 测试2:移动到 (100, 1300) 附近print(f"\n尝试移动到 (100, 1300)...")user32.SetCursorPos(100, 1300)time.sleep(1)user32.GetCursorPos(ctypes.byref(pt))print(f"实际位置: ({pt.x}, {pt.y})")# ============================================================# 签到功能(对齐 debug_step3c.py 的成功方案:mouse_event 点击)# ============================================================COORD_USER_MENU = (86, 1293)COORD_CHECKIN_BTN = (234, 637)MOUSEEVENTF_LEFTDOWN = 0x0002MOUSEEVENTF_LEFTUP = 0x0004def send_click(x, y, label=""): """SetCursorPos 定位 + mouse_event 点击(对齐 debug_step3c.py 的成功代码)""" ret = user32.SetCursorPos(x, y) time.sleep(0.3) pt = wintypes.POINT() user32.GetCursorPos(ctypes.byref(pt)) print(f" [{label}] 目标({x},{y}) 实际({pt.x},{pt.y}) ret={ret}") user32.mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) time.sleep(0.08) user32.mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) print(f" [{label}] 点击已发送") time.sleep(0.15)# --- 查找并激活 WorkBuddy 窗口(对齐 debug_step3c.py)---print("\n=== 签到开始 ===")hwnd = user32.FindWindowW(None, "WorkBuddy")print(f" FindWindowW 返回值: {hwnd} (0=未找到)")if hwnd == 0: print("[错误] 找不到 WorkBuddy 窗口,请确认程序正在运行") exit(1)print(f" [OK] 找到窗口,句柄: 0x{hwnd:08X}")# 激活窗口(与 debug_step3c.py 完全相同的方式)print(" 激活窗口...")try: class WP(ctypes.Structure): _fields_ = [ ("length", ctypes.c_uint), ("flags", ctypes.c_uint), ("showCmd", ctypes.c_uint), ("ptMinPosition", ctypes.wintypes.POINT), ("ptMaxPosition", ctypes.wintypes.POINT), ("rcNormalPosition", ctypes.wintypes.RECT), ] wp = WP() wp.length = ctypes.sizeof(wp) user32.GetWindowPlacement(hwnd, ctypes.byref(wp)) print(f" 窗口状态: {wp.showCmd} (2=最小化, 1=正常, 3=最大化)") if wp.showCmd == 2: user32.ShowWindow(hwnd, 9) print(" 已恢复最小化窗口") time.sleep(0.8) user32.SetForegroundWindow(hwnd) user32.BringWindowToTop(hwnd) print(" [OK] 窗口已激活") time.sleep(0.6)except Exception as e: print(f" [ERROR] 激活窗口失败: {e}")# --- 点击用户菜单 ---print(f"\n[1/2] 点击用户菜单 ({COORD_USER_MENU[0]}, {COORD_USER_MENU[1]})")send_click(*COORD_USER_MENU, label="用户菜单")print(" -> 等待菜单展开 (1s)...")time.sleep(1)# --- 点击签到按钮 ---print(f"\n[2/2] 点击签到按钮 ({COORD_CHECKIN_BTN[0]}, {COORD_CHECKIN_BTN[1]})")send_click(*COORD_CHECKIN_BTN, label="立即领取")time.sleep(0.5)print("\n[完成] 签到点击序列执行完毕。")如果你也在用 WorkBuddy,欢迎留言交流
或者分享你遇到的其他自动化踩坑经验 🙌