"""Verified offer-claim workflow, with an explicit already-claimed branch.""" import time from .engine import Desktop, Region, WorkflowError from .permissions import mouse_permission_granted class MacGameDesktop(Desktop): def window_bounds(self): import Quartz windows = Quartz.CGWindowListCopyWindowInfo( Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) matches = [w for w in windows if w.get('kCGWindowName') == '永远的蔚蓝星球' and w.get('kCGWindowOwnerName') == '微信'] if len(matches) != 1: raise WorkflowError('请在微信中正常打开一个游戏窗口,不要直接启动 WeApp。') return dict(matches[0]['kCGWindowBounds']) def region_for(self, screenshot): b = self.window_bounds() width, height = self.api.size() sx, sy = screenshot.width / width, screenshot.height / height # This observed WeChat header is 44 logical points high. Expanded # sidebars change aspect ratio and are rejected by Region.validate. region = Region(round(b['X'] * sx), round((b['Y'] + 44) * sy), round(b['Width'] * sx), round((b['Height'] - 44) * sy)) region.validate(screenshot.size) return region def activate(self): if not mouse_permission_granted(): raise WorkflowError('请先为运行宿主授予 macOS 辅助功能权限。') b = self.window_bounds() self.region_for(self.screenshot()) # Focus with a click in the observed empty part of the header, so # the first game-button click is not consumed by window activation. self.api.moveTo(b['X'] + 80, b['Y'] + 22, duration=0.2) self.api.mouseDown() try: time.sleep(0.2) finally: saved = self.api.FAILSAFE self.api.FAILSAFE = False try: self.api.mouseUp() finally: self.api.FAILSAFE = saved time.sleep(0.3) class OfferRunner: def __init__(self, desktop, matcher, flow, region_provider, log=print, timeout=12): self.desktop = desktop self.matcher = matcher self.flow = flow self.region_provider = region_provider self.log = log self.timeout = timeout def wait(self, *names): deadline = time.monotonic() + self.timeout previous, count = None, 0 while time.monotonic() < deadline: screenshot = self.desktop.screenshot() region = self.region_provider(screenshot) frame = self.matcher.frame(screenshot, region) detected = next((name for name in names if self.matcher.matches(frame, self.flow['states'][name])), None) count = count + 1 if detected and detected == previous else 1 if detected else 0 previous = detected if count >= 2: return detected, screenshot, region time.sleep(0.25) raise WorkflowError('未识别到预期页面 ' + '/'.join(names) + ',已停止,不重复领取。') def click(self, action, observation): _, screenshot, region = observation # Stop on movement after the recognition frame instead of clicking # stale coordinates. A future run can re-locate the moved window. if self.region_provider(screenshot) != region: raise WorkflowError('识别后游戏窗口移动,已停止,请重新运行。') self.desktop.click(self.flow['clicks'][action], region, screenshot.size) self.log('已执行:' + action) def run(self, dry_run=False): home = self.wait('home') if dry_run: self.log('首页识别通过。计划:特惠 → 一键领取 → 关闭奖励 → 验证按钮变灰 → 返回首页。未点击。') return 'preview' self.click('open', home) offer = self.wait('available', 'claimed', 'menu') if offer[0] == 'menu': self.click('battlepass', offer) offer = self.wait('available', 'claimed') if offer[0] == 'claimed': self.log('一键领取已变灰,当前没有可领取奖励;跳过领取。') self.click('back', offer) self.wait('home') self.log('已确认返回首页。') return 'already_claimed' self.click('claim', offer) reward = self.wait('reward') self.log('已确认“恭喜获得”奖励弹框。') self.click('dismiss', reward) completed = self.wait('claimed') self.log('已确认一键领取按钮变灰。') self.click('back', completed) self.wait('home') self.log('领取完成,已确认返回首页。') return 'claimed'