WorkBuddy 每日签到(二级降级:UIA → OCR)
用途
在 Windows 上自动完成 WorkBuddy 每日签到/积分领取。完全后台静默——不动用户
真实鼠标、不抢前台焦点、不闪屏、不依赖硬编码屏幕坐标。
WorkBuddy v5.4.7(2026-09-01 实测,当前版本)的签到是三步流程:
> 版本注意:v5.4.5 曾因 Electron 37 / Chromium 138 的无障碍树渲染问题导致 UIA 失效
> (renderer accessibility tree 不渲染,只剩空 Pane);v5.4.7 已修复,UIA 三步恢复走通。
> 但入口按钮文案同步改版:「签到领积分」被移除,改为「Buddy加油站」(详见 v9/v10 段)。
> 当前 UIA(L1 主方案)与 OCR(L2 兜底)两级均可用,UIA 优先。
- 点头像(左下角用户头像)→ 弹出用户菜单;
- 点菜单里的「Buddy 加油站」入口 → 打开加油站卡片;
- 点卡片里的「立即领取 / 今日已领」→ 完成签到。
方案:二级降级(scripts/check_in.py 总调度)
| 层级 | 脚本 | 定位方式 | 适用 |
|---|---|---|---|
| L1 主方案 | uia_checkin.py |
UIA 无障碍树 InvokePattern | 应用暴露 a11y 树(Electron 默认) |
| L2 兜底 | ocr_checkin.py |
截屏 + RapidOCR 文字定位(文字优先,头像用圆检测识别) | UIA 拿不到元素时(游戏/自绘 UI) |
纯坐标版已删除(分辨率/DPI/窗口/改版任一变化即失效)。v15 起 L2 进一步去坐标化:
入口定位一律以「OCR 认出文字」为成功判据,头像靠圆检测识别图标,坐标只在运行时动态计算、
绝不落盘——详见下方 L2 章节。
运行:
# 用本机 WorkBuddy 托管 Python 执行(路径按实际安装位置替换 <USERPROFILE> 与 skill 目录)
& "<USERPROFILE>\.workbuddy\binaries\python\envs\default\Scripts\python.exe" "<skill目录>\workbuddy-check-in\scripts\check_in.py"
check_in.py 自动降级:先跑 UIA,失败(非 0 或异常)自动切 OCR。也可单独指定
--uia / --ocr。
判定输出:[3] 按钮: '立即领取' = 本次签到成功;[3] 按钮: '今日已领' = 当日已签过。
L1:UIA 方案要点(uia_checkin.py)
- 用
pywinauto对每个按钮调用InvokePattern(系统无障碍层点击),不依赖坐标。 ensure_window_state:仅在「隐藏/最小化」时SW_SHOWNOACTIVATE恢复显示(不激活、不抢前台),不再强制最大化。warmup:预热 UIA 树(Electron 无障碍树懒加载,首次访问可能为空,重试)。close_popups:归一化——关残留卡片 + ESC。- 关键词与窗口标题统一从
scripts/config.json读取(avatar_name=头像用户名、
uia.entry_keywords=「签到领积分」入口、uia.claim_keywords/claimed_keywords=
领取按钮),不再硬编码在脚本里。
L2:OCR 方案要点(ocr_checkin.py)
- 截图:
PrintWindow(hwnd, PW_RENDERFULLCONTENT)截窗口客户区内容(后台可截,
即使被游戏全屏遮挡也能截到 Chromium GPU 合成完整内容),ctypes GDIGetDIBits读回
numpy BGR。不依赖窗口最大化/置顶/屏幕位置。 - 坐标系:全部使用「窗口客户区相对坐标」(
GetClientRect取客户区尺寸),运行时动态计算,
不落盘、不记忆任何绝对坐标——skill 会装在很多台各不相同的电脑上(窗口尺寸/DPI/版本各异),
写死的坐标迟早是错的。对窗口位置/大小/状态完全无要求:随便拖、随便缩、被全屏遮挡都能工作。 - 入口定位:三条路依次尝试,成功判据一律是「OCR 识别到入口文字」(不是坐标命中):
1. 文字优先(open_entry第一步):先纯 OCR 在左下角找「Buddy加油站」——
菜单本就可见时零点击直达,彻底绕开坐标;
2. 头像识别(find_avatar,路 2):菜单不可见时,用圆检测HoughCircles在左下角
识别头像圆形图标、点其圆心开菜单。这是「认图」不是「认坐标」,跨 DPI/窗口尺寸自适应;
实测 4 组参数都精确命中同一圆心(与 UIA 实测头像中心仅差 1px);
3. 动态兜底(avatar_candidates,路 3):圆检测失败时,按「固定偏移×DPI 缩放 → 原始像素
→ 比例锚点 → 左下角内缩」算种子点,再以小网格围绕首个有效种子扩散,逐个尝试;
网格永远围绕最可能的点展开,即使配置的偏移完全不对也还有邻近点可试(实测:偏移写错
时自动纠偏到正确位置)。全部候选限制在左下角区域内,避免误点侧栏。 - 按钮定位:在客户区左下角 400×700 子区域 OCR 识别「Buddy加油站」「立即领取」「今日已领」
等文字,返回客户区相对坐标。 - 点击:
PostMessage客户区坐标lp=(y<<16)|x(已经是客户区坐标,不需要ScreenToClient),
后台静默。每点一次头像都会 OCR 复验入口是否出现,未出现就 ESC 关掉浮层再试下一个。 - 单次 OCR 约 1.7s;三步总耗时约 10~20s。
- 关卡片(v16):走完三步后调
close_card(hwnd)收掉加油站卡片——L1 UIA
InvokePattern点「关闭…加油站」按钮(认按钮不认坐标),L2 ESC 兜底。
注:ESC 本身无效(已实测 PostMessage/SendMessage 三种发法全失败,因窗口被
disable_activate置为不可激活时 Chromium 丢弃按键消息),故 L1 才是真正起作用那层。
整个函数 best-effort,失败不影响签到结果。 - 已知局限:OCR 偶发识别偏差(如「已领3天」→「已领3天1」),不影响按钮定位。
- 实测(2026-09-12):
check_in.py --ocr全流程跑通,日志见「头像识别:圆检测命中头像 @ (35,889)
→ 入口 OCR 命中 'Buddy加油站' → 按钮 OCR 命中 '今日已领' → 退出码 0」。
依赖
| 方案 | 依赖 | 是否必需 |
|---|---|---|
| L1 UIA | pywinauto(纯 Python + comtypes,约几 MB) |
必需 |
| L2 OCR | rapidocr-onnxruntime + onnxruntime + numpy(约 100MB) |
可选(仅 OCR 兜底需要) |
rapidocr 在 ocr_checkin.py 内延迟导入——只用 UIA 方案时,OCR 那套约 100MB
的依赖(rapidocr/onnxruntime/opencv)无需安装。安装命令(在托管 venv 内):
# 仅 UIA(最小安装)
& "<USERPROFILE>\.workbuddy\binaries\python\envs\default\Scripts\pip.exe" install pywinauto
# 需要 OCR 兜底时再补装(约 100MB)
& "<USERPROFILE>\.workbuddy\binaries\python\envs\default\Scripts\pip.exe" install rapidocr-onnxruntime "onnxruntime==1.20.1"
> 注意:onnxruntime 需锁 1.20.1——1.28.0 在 Python 3.13 下 DLL 初始化失败。
配置(scripts/config.json)
所有可移植参数集中在 scripts/config.json,移植到新机器/新账号只改配置、不改代码:
| 配置项 | 含义 | 默认值 |
|---|---|---|
window_title |
WorkBuddy 窗口标题 | WorkBuddy |
avatar_name |
头像/账号用户名(UIA 定位头像用) | 必填:填 WorkBuddy 左下角显示的账号名(留空则跳过 UIA、自动走 OCR) |
report_path |
签到成功后的周汇总记录路径(可选,市场用户留空) | ""(空) |
uia.entry_keywords |
加油站入口按钮关键词 | ["Buddy加油站"] |
uia.claim_keywords / claimed_keywords |
领取/已领按钮关键词 | ["立即领取"] / ["今日已领"] |
uia.entry_click_offset.fx/fy |
复合入口按钮点击偏移 | 0.3 / 0.26 |
ocr.entry_keywords 等 |
OCR 定位关键词(入口优先,含「Buddy加油站」「加油站」) | 见 config.json |
ocr.avatar_detect.* |
路2 圆检测:头像识别区域/半径范围(左下角) | 360×160 / r10~26 |
ocr.avatar_anchor.* |
路3 动态兜底种子:offset_x/y(像素)、fx/fy(比例)、box_*(限制区)、grid_step、max_attempts |
94/34、0.10/0.037 等 |
ocr.scan.width/height |
OCR 扫描子区域 | 400 / 700 |
ocr.card_close.close_keywords / require |
关卡片时匹配的按钮名(须同时含 close_keywords 各项 + require) |
["关闭"] / 加油站 |
> 不落盘原则:以上坐标类参数只在运行时参与计算,脚本不记忆、不落盘任何绝对坐标
> (跨机器存坐标本身是隐患)。所以 state 文件里只有 last_checkin_date/time。
加载优先级:环境变量 WB_CHECKIN_CONFIG 指定文件 > 同目录 config.json > 内置默认值。
缺失项自动回退默认值,配置文件损坏只打印提示、不中断签到。
与上游版本的差异(重要——请勿静默回滚)
- v17(2026-09-12 发布 v1.12.0 前的上架整理)——去本机化,让发布包可被任何人直接用:
- 配置默认值去硬编码:
config.py的_DEFAULTS["avatar_name"]由固定的本机账号名
改为空字符串(并在注释与文档中标注「必填」)。留空时 UIA 会跳过头像定位、自动降级
OCR,不会崩;填对自己的账号名则走最快的 UIA 路径。 report_path默认留空:这是「签到成功后追加一条记录到你自己的文件」的可选功能,
属本机自用,发布包中一律留空(不写任何文件);本机自用值只在本地配置里。- 文档去本机化:SKILL.md / README.md / references 中的本机绝对路径、本机 automation
ID、账号名等一律替换为占位符或通用描述(沿用 1.10 发布时确立的同一条标准)。 - 发布包清理:排除
__pycache__等字节码缓存。 -
功能上零行为变更(除"未配置 avatar_name 时降级 OCR"这一更安全的默认),v12–v16
的全部改造随本版本一并首次上架。 -
v16(2026-09-12 用户追问「关窗口有没有同步到 OCR 侧和挂起的那份」)——
OCR 侧关卡片修复:ESC 根本无效,改为「认按钮不认坐标」: - 实测发现(重要):v13 给 OCR 侧加的「发 ESC 关卡片」从未生效。实测卡片开着时,
PostMessage ESC(lParam=0)、PostMessage ESC(带扫描码)、SendMessage ESC(带扫描码)
三种发法全部失败(卡片仍在);只有点真正的「关闭 Buddy 加油站」按钮才关得掉。
根因:脚本全程disable_activate(WS_EX_NOACTIVATE),Chromium 在无焦点时不处理按键消息。
→ 用户此前反馈「每次都要手动关掉」,OCR 兜底路径就是元凶。 - 改造:
close_card(hwnd)分层 best-effort——
L1_close_via_uia():用 UIAInvokePattern(el.invoke())点「关闭…加油站」按钮,
完全不用任何像素坐标;L2close_popups():ESC 兜底(可能无效但无害)。
整个close_card外层包try/except,失败绝不冒泡、绝不影响签到结果判定。 - 刻意不做图像点击:实测用「亮色卡片右上角内缩」推算 ✕ 也准(推算 (232,652) vs 真值
(230,650),仅差 2px,4 个阈值下都稳定),但故意未采用——关卡片只是外观锦上添花,
而图像点击在暗色主题下可能误判亮区、点到侧栏条目(有副作用)。规避 > 补救:宁可不做。 - 回退路径同步受益:
check_in.py只是调度壳,close_popups()/close_card()仅存在于
ocr_checkin.py,故在用版与被挂起的旧 automation 版共用同一处,修一次两边都好。 - 配置新增
ocr.card_close: {close_keywords, require}(默认["关闭"]+加油站)。 -
实测:
check_in.py --ocr从「卡片关闭」起点走完整流程 → 退出码 0 → 卡片已关闭
(UIA 探测「加油站」相关按钮为空);check_in.py默认 UIA 路径无回归。
单元测试 4 项全 PASS(UIA 不可用走 ESC、UIA 可用不调 ESC、两级都抛异常不冒泡、
卡片未开时安全返回 False)。 -
v15(2026-09-12 用户反馈「别的机器上出现坐标不匹配,当时靠看菜单文字才对」)——
OCR 定位去坐标化(认字 + 认图,不认死坐标): - 背景:
ocr_checkin.py原来靠「头像固定像素偏移 (102,42)」点头像开菜单。实测本机真实
头像在 (94,34)(MenuItem 中心)/ 圆检测 (35,889),配置值已偏 8~10px——本机因 MenuItem
够宽(164×44)侥幸命中,换窗口尺寸/DPI 必漏。用户定调:skill 装在很多台各不相同的电脑上,
不要存绝对坐标,要动态、要稳。 - 改造:入口定位改为三路并行,成功判据一律是 OCR 识别到入口文字——
①文字优先:先纯 OCR 找「Buddy加油站」,菜单可见时零点击直达;
②头像识别:find_avatar()用HoughCircles圆检测识别头像圆形图标,点圆心开菜单
(认图不认坐标;实测 4 组参数均精确命中同一圆心);
③动态兜底:avatar_candidates()按「偏移×DPI → 原始像素 → 比例锚点 → 左下角内缩」
算种子,再以网格围绕首个有效种子扩散;即使配置偏移全错也能自动纠偏。 - 不落盘:彻底不做坐标记忆/Persistence(曾短暂实现
ocr_avatar_hit自校准,经用户
指正后移除——跨机器存坐标本身就是隐患)。每次运行全部重新动态计算,state 文件里
只剩last_checkin_date/time。 - 配置新增
ocr.avatar_detect(圆检测区域/半径范围)与ocr.avatar_anchor的
fx/fy/box_width/box_height/grid_step/max_attempts;entry_keywords补「加油站」。 - 实测:
check_in.py --ocr全流程跑通(圆检测命中头像 → 入口文字命中 → 按钮「今日已领」→
退出码 0);候选点单元测试覆盖 4 种窗口尺寸/DPI 与「偏移写错」场景,均在框内且能纠偏。 -
回退路径同步受益:
check_in.py只是 10 行调度壳,坐标逻辑仅存在于ocr_checkin.py,
所以在用版(v2)与被挂起的回退版(旧 automation →check_in.py)共用同一处,修一次两边都好。 -
v14(2026-09-12 用户定调「排障可读性不可牺牲,噪音会磨损判断力」)——日志改造 + 自熄降级:
- 日志契约(硬规则):
- 例行动作不写日志:标志位命中秒退 → 零输出(原先每小时写一条「熄火失败」已消除)。
- 同一类异常每天最多一条(
_log_daily_once),杜绝例行失败刷屏淹没信号。 - 真实事件每次必写:签到闭环(含方案 + 耗时)、签到未完成、周汇总写入、一次性状态变更。
--debug:输出全量细节(含例行动作),排障时随要随有——可读性与可调试性二者兼得。- 硬契约:某天完全没有日志 = 异常信号(任务没跑 / 一直没签成功),应主动排查。
- 日志自动轮转:超过 256 KB 归档为
workbuddy-check-in.log.1,不无限增长。
- 自熄改为「尽力而为 + 一次说明 + 永久停用」:
_silence_today()首次遇权限失败时,把状态
标记为selfextinguish: unsupported并只写一条说明,此后静默返回(不再每次 spawn
schtasks 徒劳尝试、不再刷屏)。降级态 = 「每小时唤醒 + 标志位静默秒退」,用户无感。 - 取舍依据(重要):不为「感知不到的零唤醒」给签到脚本提权。规避 > 监控——不让需要
特权的操作进循环,风险面直接归零;若日后确需真·零唤醒,另择无特权路径(如按用户级启动项- 常驻监督进程 + 每日心跳),不采用提权方案。
-
实测:秒退路径 0 日志(
--debug1 行);自熄失败 1 行说明 + 置 unsupported,二次调用 0 日志
0 次 schtasks 调用;完整流程 2 行高信号日志(签到闭环 + 周汇总)。 -
v13(2026-09-12 用户要求「签到完成后把加油站小窗口也关掉」)——签到后收拾现场:
- 关窗口:
uia_checkin.py在「三步流程跑完」后调用close_popups(connect())关闭加油站
卡片(点「关闭 Buddy 加油站」按钮 + ESC 兜底);ocr_checkin.py走完后 → ⚠️ 原为
close_popups()(仅 ESC),实测该 ESC 从未生效,已于 v16 改为「UIA 按钮优先 + ESC 兜底」
的close_card()。关闭失败吞异常、不影响签到结果判定。实测 UIA 路径卡片成功关闭
(探测无「关闭 / 今日已领 / 立即领取」残留按钮)。 -
⚠️ 发现「自熄闹钟」失效(已于 v14 定案处理):计划任务
WorkBuddyCheckIn的 RunLevel 是
LeastPrivilege(普通权限),脚本在任务上下文里执行schtasks /change改触发时间时报
「请输入 Administrator 的密码」(需管理员权限),故从 2026-09-10 起熄火一直失败,任务
StartBoundary 停在 09-10 06:00,退化为「每小时唤醒 + 标志位秒退」。v14 定案:不做提权
修复(为感知不到的零唤醒付安全代价不划算,且提权方案在 AI 侧无法验证、对市场用户需一次性
管理员操作);改为「尽力而为 + 一次说明 + 永久停用」,并把日志改造成高信噪比台账。 -
v12(2026-09-09 用户明确「skill 上架市场、他人也在用,须通用、不能依赖本机 automation」)——
调度做进程序:Windows 计划任务 + 自举注册 + 成功自熄闹钟(check_in_v2.py升级)。 - 背景:有反馈「automation 改不了」,市场用户装 skill 拿不到本机 automation;且用户要
「签过一次后,当天连唤醒都不跑」。故把每日签到调度从 WorkBuddy automation 移到 skill 程序内。 - 三件套:
- 自举注册:每次运行幂等检查计划任务
WorkBuddyCheckIn是否存在,不存在则用
sys.executable(pythonw.exe)+__file__动态schtasks /create /sc hourly /mo 1注册
——市场用户装哪儿都能用、零手动、重启不失效(任务计划是系统持久配置)。 - 前置标志位门禁:读
~/.workbuddy/state/workbuddy-check-in.json,已签秒退。 - 成功自熄闹钟:签到成功(退出码 0)后
schtasks /change /sd 明天 /st 06:00,把任务
推迟到次日 06:00——当天剩余整点零唤醒、零打扰,次日 06:00 自动恢复每小时唤醒。
- 自举注册:每次运行幂等检查计划任务
- 可选用「周汇总记录」:
report_path(环境变量WB_CHECKIN_REPORT或 config.json 的
report_path字段)非空时,签到成功追加一条 memory.md 兼容记录(含「成功」+「UIA/OCR 降级」),
供周汇总统计;市场用户留空即不落盘。 - 回滚:本机原有的「每天 10:00」旧 automation 继续 PAUSED 作回退;验证期间临时建的
「每小时」automation(本机配置,市场用户拿不到)已删除。 - 踩坑:
schtasks在中文 Windows 输出 GBK,subprocess 不能用text=True(UTF-8 解码报
UnicodeDecodeError、reader 线程抛 traceback)——改为capture_output=True取字节,再
UTF-8→GBK→replace 兜底解码。 -
验证:2026-09-09 注册
WorkBuddyCheckIn(每小时、交互式、已启用),熄火后下次运行
2026-09-10 06:00;--force全流程 UIA 命中「今日已领」+ 报告写入正确。 -
v11(2026-09-09 用户提出「每小时触发 + 标志位」改造)——新增隔离版
check_in_v2.py: - 动机:用户「到公司时间不固定、常早于常规」,单点定时(10:00)会漏签(如 8/30 周末)。
想要「每小时触发 + 签过一次后后续全部跳过」。 - 核心:前置标志位门禁。脚本一进来先读「今日已签」标志位
(~/.workbuddy/state/workbuddy-check-in.json,last_checkin_date)——已签则毫秒级秒退
(实测 0.144s),不 import pywinauto、不碰窗口、不做 UIA/OCR;未签才复用check_in.main()
二级降级,退出码 0 时原子写标志位。支持--force忽略标志位强制重跑(纠正误写用)。 - 隔离 + 灰度:新脚本独立于
check_in.py,二者并存。当时旧调度(每天 10:00,指向
check_in.py)已暂停保留作回退;验证期间的「每小时」触发先小范围灰度,跑稳后再切换。
该灰度已结束——v12 起调度改由程序内自举注册计划任务承担(见下)。 -
调度器限制(重要):
BYHOUR只接受单个整数、不支持逗号分隔多值(BYHOUR=6,10实测报错),
故「5-23 点每整点」表达不了,改用FREQ=HOURLY;INTERVAL=1(每小时,凌晨靠「用户不开机」自然不触发)。 -
v10(2026-09-01 二次排查)——WorkBuddy 5.4.7 修复无障碍树 + 入口文案改版:
- 5.4.7 修复了 5.4.5 的无障碍树渲染 bug:UIA 树从「9 个空 Pane」恢复为丰满
(358 个非空 name 元素,含 Image/Button/Text/MenuItem)。UIA 三步重新走通,不再降级 OCR。 - 入口文案改版:点头像后的用户菜单里,加油站入口按钮名称从
'Buddy 加油站 签到领积分 去邀约 最高得 650 积分/人'(三行)变为
'Buddy加油站 去邀约 最高得 650 积分/人'(两行,去掉了「签到领积分」)。 - 修补:
config.json与config.py的uia.entry_keywords由["签到领积分"]改为
["Buddy加油站"](无空格,精确命中入口且不会误匹配「关闭 Buddy 加油站」)。 - 偏移无需改:新入口按钮 rect (30,863)-(336,951) 306x88,子元素
Text 'Buddy加油站'
位于 y 874~895(相对 fy≈0.24)、x 64~157(相对 fx≈0.26),与现有fx=0.3, fy=0.26吻合。 - v9 的空树快速失败逻辑保留有效:树丰满(>50)时正常走 UIA,树为空时(未来某版本
又回归)快速降级 OCR。两层防御互不冲突。 - 验证:2026-09-01 10:3x 跑通 UIA 三步:
[1] 头像 '<用户名> <用户名>'→[2] 入口 'Buddy加油站 去邀约 最高得 650 积分/人'→[3] 按钮 '今日已领'→=== 三步流程跑完 ===。 - v9(2026-09-01)——WorkBuddy 5.3.12 → 5.4.5 升级后 UIA 事实上不可用,精准快速失败:
- 根因:WorkBuddy 升级到 5.4.5(Electron 37.10.3 / Chromium 138.0.7204.251)。
Chromium 130+ 在 Windows 上改了无障碍行为:没有真实 AT(Narrator/读屏)客户端时,
Chromium 不再渲染 renderer accessibility tree——UIA 树只剩 browser-side 一串空 Pane,
无任何 Button/Image/MenuItem/Text 元素。pywinauto 触发不了(实测:UIA 树只有 9 个空 Pane;
即使SetForegroundWindow强制激活窗口、或发WM_GETOBJECT(OBJID_CLIENT)反复触发,元素数都 < 50)。 - 触发场景:新版本 WorkBuddy 用户。
- 修补:
warmup()的"树丰满"判定阈值从n > 10提到n > 50(老版本 Electron 25/26 丰满时通常
100~300+,50 是安全的"有 vs 无"分界)。main()开头加二次检测:descendants < 50立即 print 明确诊断日志
(FAIL: UIA 树为空(descendants=N < 50),疑似 Electron 37+/Chromium 130+ 默认禁用 renderer 无障碍树;强制降级 OCR)并return 1,让总调度切 OCR 兜底。- 旧 warmup 重试间隔从 0.5s 缩到 0.3s,整体 UIA 失败判定耗时 ≈ 1.5s(原本浪费 2.5s
warmup + close_popups + find avatar 一连串失败才退出)。
- 保留 UIA 方案的代码不动:仅改阈值。如果未来 Electron 修复了无障碍或 WorkBuddy
启动时加--force-renderer-accessibility,UIA 层仍能立即恢复,不需重写。 - 影响:当前签到实质上 = OCR 方案在跑,UIA 是"等老版本回来"的兜底。
总签到时间从 ~12s 缩到 ~9s(UIA 阶段少 3.5s)。 - 验证:2026-09-01 跑通:UIA 1.5s 内 fail + 明确诊断 → OCR 三步全中 → 按钮「今日已领」
判定成功。 - 未来彻底恢复 UIA 的可能路径(未实施):
- 让 WorkBuddy 启动时带
--force-renderer-accessibility(需修改 WorkBuddy 启动快捷方式/脚本,超出本 skill 范围); - 临时
SystemParametersInfo(SPI_SETSCREENREADER, TRUE)让 Chromium 检测到屏幕阅读器
标志(副作用大,不推荐); - 等 Chromium 上游修复无障碍默认行为。
- 让 WorkBuddy 启动时带
- v8(2026-08-19)——彻底消除抢前台(用户反馈"今天没全屏也抢"后实测定位):
1. 抢前台元凶 =el.invoke()(UIAIUIAutomationInvokePattern::Invoke)——
Electron/Chromium 处理无障碍 invoke 时会激活窗口(系统层行为,跟是否
最大化无关)。v6 只删了SW_MAXIMIZE和click_input,漏了这个更隐蔽的源。
2. 点击改用「UIA 拿元素坐标 +PostMessage客户区点击」——
新增click(el, hwnd, fx, fy):拿el.rectangle()屏幕坐标 →ScreenToClient
客户区 →PostMessage(WM_LBUTTONDOWN/UP)。完全不动鼠标、不激活窗口。
Electron 响应 PostMessage 鼠标消息(OCR 方案早就验证过)。
3. 全程加WS_EX_NOACTIVATE防护——Electron 弹浮层(点头像弹用户菜单)
时会自我激活窗口,加这个 EXSTYLE 让 Windows 拒绝激活它。两级方案(UIA/OCR)
都在main()开头disable_activate、finally 里restore_activate。
4.click支持fx/fy偏移——UIA 把三行菜单项「Buddy加油站」「去邀约」
「积分余额」合并报成一个 Button(名称是拼接文字),rect 高 88px 覆盖三行,
几何中心落在「去邀约」行(点中心 = 点去邀约,签到永远 FAIL)。入口
点击改用fx=0.3, fy=0.26(对齐「Buddy加油站」行,与 OCR 命中位置吻合)。
5. 删invoke()——被click()全面替代。close_popups也用click关闭残留卡片。
端到端验证:UIA 三步走通(不再降级 OCR),全程 137 次前台采样 0 次被 WorkBuddy 抢。 - v7(2026-08-18)——OCR 也彻底去掉「最大化」依赖(用户洞察:窗口本身窗口化就能工作):
1. 截图改用PrintWindow截窗口客户区——PW_RENDERFULLCONTENT能截到 Chromium
GPU 合成完整内容,即使被游戏全屏遮挡也能截到(实测窗口化状态 99% 非黑)。
2. 坐标全转「客户区相对」——删除屏幕绝对区域REGION=(0,780,360,660)硬编码和
GetWindowRect锚点;扫描区域改为GetClientRect动态算的左下角 400×700。
3.click_screen→click_client——PostMessage客户区坐标,不需要ScreenToClient。
4.ensure_window简化——只处理隐藏/最小化(SW_SHOWNOACTIVATE),不再
SetWindowPos铺满。OCR 完全不依赖窗口最大化/置顶/屏幕位置。
原理:UIA 读树只需要窗口可见状态(被遮挡也行),不需要最大化;OCR 用PrintWindow截
窗口内容同样不需要窗口最大化/置顶——用户的窗口本身是窗口化就能正常工作。 - v6(2026-08-18)——修复「抢前台」打扰用户(玩游戏/全屏场景):
1. 删SW_MAXIMIZE——ShowWindow(SW_MAXIMIZE)会激活窗口、抢焦点。
UIA 读树根本不需要最大化:ensure_window_state改为仅在「隐藏/最小化」时
SW_SHOWNOACTIVATE恢复显示(显示但不激活、不抢前台)。
2. OCRensure_window铺满改用SetWindowPos + SWP_NOACTIVATE | SWP_SHOWWINDOW,
HWND_NOTOPMOST不置顶遮挡前台应用,SPI_GETWORKAREA取工作区尺寸,
全程不激活窗口。
3. 删除三处click_input()兜底——invoke失败直接返回失败/降级 OCR,
不再真实移动鼠标抢前台。 - v5(2026-08-17):
1.GetCurrentThreadId移到kernel32——v4restore_foreground用
AttachThreadInput + SetForegroundWindow时把GetCurrentThreadId错误挂在
user32(实际属于kernel32.dll),导致模块级restype赋值就抛
AttributeError: function 'GetCurrentThreadId' not found,两级方案全挂。
修:顶部加kernel32 = ctypes.windll.kernel32,.restype与调用都改
kernel32.GetCurrentThreadId。
2.ensure_window_state/ensure_window处理隐藏窗口——v4 只判IsIconic,
不处理IsWindowVisible=False(用户关到系统托盘时常见)。
3. 隐藏窗口先SW_SHOWNOACTIVATE再SW_MAXIMIZE——直接SW_MAXIMIZE从
隐藏态不会触发 Chromium 渲染 UIA 树(只剩 8 个 Pane),必须先SW_SHOW让
窗口可见、Chromium 初始化 UIA 元素,再最大化。 - v4:修复窗口抢前台——
type_keys("{ESC}")(会 set_focus 激活窗口)改
PostMessage发 ESC;keybd_event全局 ESC 改PostMessage;脚本开头记录用户
当前前台窗口(GetForegroundWindow),结束时用AttachThreadInput + SetForegroundWindow
原样归还(try/finally,成功失败都还)。v4 的restore_foreground框架正确,
但GetCurrentThreadId挂错 DLL 由 v5 修正。 - v3:二级降级 UIA → OCR,删除坐标版。
- v2 delta:UIA Invoke 三步(替代坐标点击)。
- v1 delta:
PostMessage替代mouse_event、去前台抢占、加IsIconic、修路径。 - frontmatter /
_meta.json标userModified: true,上游静默覆盖时可识别。
如果上游有新版本,按 open-plugin-vetting 流程选择性合并我们的 delta,不要整包替换。
自动化
- 调度做进程序(v12 起):
check_in_v2.py自举注册 Windows 计划任务
WorkBuddyCheckIn(每小时触发)。签到成功后尽力自熄到次日 06:00;该操作需管理员权限,
不可用时自动永久停用并只说明一次(见 v14),降级为「每小时唤醒 + 标志位静默秒退」
(约 0.3s、无窗口、不写日志、用户无感)。每日签到不依赖 WorkBuddy automation——
automation 是本机配置,市场用户装 skill 时拿不到;计划任务由脚本自己注册,装哪儿都能用。 - 可选的记录文件:
config.json的report_path非空时(或环境变量WB_CHECKIN_REPORT
覆盖),签到成功会向该文件追加一条记录,便于做你自己的签到/汇总统计。留空则不写任何文件。 - 本机自用配置不进发布包:个人记录路径、本机 automation 等只存在于本机配置中。
日志与排障(v14 契约)
日志:~/.workbuddy/state/workbuddy-check-in.log 状态:~/.workbuddy/state/workbuddy-check-in.json
设计原则:日志是判断力的输入,信噪比优先。 例行动作不写日志,只有真实事件才写。
| 情形 | 是否写日志 | 说明 |
|---|---|---|
| 标志位命中、秒退 | 否 | 例行动作;每天可能发生 10+ 次,写就是噪音 |
| 签到闭环成功 | 是(1 行) | 含方案(UIA/OCR 是否降级)+ 耗时 |
| 签到未完成(退出码非 0) | 是 | 真正的信号:按计划每小时重试,持续出现即需排查 |
| 周汇总记录写入 | 是 | 每次成功签到一条 |
| 自熄不可用 | 仅一次 | 说明后置 selfextinguish: unsupported,永久停用 |
| 同上类异常(注册失败等) | 每天最多一条 | _log_daily_once 防刷屏 |
| 需要全量细节 | --debug |
例行动作也会记,排障时随要随有 |
⚠️ 硬契约:某天完全没有日志 = 异常信号。 正常情况下每天至少 1 行(签到闭环)。
若某天空白,说明「任务没跑」或「一直没签成功」,应主动排查,不要当成"没事"。
正常日志长这样(一天 1–2 行):
2026-09-12 11:38:14 [checkin] 签到闭环:方案 UIA(未降级),耗时 21.1s,退出码 0
2026-09-12 11:38:14 [report] 已写记录 → <你的 report_path>
排障常用命令:
# 看最近日志
tail -20 ~/.workbuddy/state/workbuddy-check-in.log
# 强制跑完整流程 + 全量日志(不碰计划任务、报告写临时路径)
WB_CHECKIN_REPORT=/tmp/test.md python check_in_v2.py --force --no-task --debug
# 查计划任务下次唤醒时间(沙箱内 schtasks 可能被拦,可直接读任务 XML)
schtasks /query /tn WorkBuddyCheckIn /fo LIST /v
日志轮转:超过 256 KB 自动归档为 workbuddy-check-in.log.1,不会无限增长。
注意事项
- 运行时保持 WorkBuddy 窗口存在(前台/后台被遮都行,别最小化/关到托盘——
脚本会自动恢复显示,但全程不激活窗口、不抢前台、不动鼠标,可放心在
游戏/全屏场景后台静默运行;若窗口被其他应用完全遮挡、Chromium 不渲染 UIA 树,
仍可能 FAIL)。 - 点击过程中不要手动锁屏。
- 日志是高信噪比台账(v14 契约,见上),正常每天 1–2 行;但要记住:
日志只反映"跑到过什么",签到是否真的到账仍以 WorkBuddy 界面或用户确认为准。
反过来说,某天日志完全空白本身就是异常,必须主动排查。 - WorkBuddy 升级后若元素/文字变化:改
scripts/config.json对应项即可(用户名、
窗口标题、入口/领取按钮关键词、头像锚点偏移),无需改脚本代码。
需要实现细节时再读取:
references/checkin-reference.md