Files
sanguo_vnpy_v2/scripts/qmt_relogin/ui_act.py
T
claude_dev 2b2ae3c4d8
CI/CD / test (push) Successful in 28s
CI/CD / nas-deploy (push) Successful in 3s
CI/CD / nas-verify (push) Successful in 11s
feat(ops): 大QMT哨兵+09:15盘前探针三层防御上线——30min巡检+必不成交单即撤+gate留痕 [vps]
- qmt_sentinel.py: sanguo-qmt-sentinel 每30min(进程+桥RPC ping判红,账户类仅记录);
  红->schtasks /run sanguo-qmt-relogin->轮询桥恢复<=10min->复检留痕;无退避(拍板#4)
- qmt_probe_0915.py: sanguo-qmt-probe0915 周一至五09:15; 510050.SH买100@0.01
  FIX_PRICE必不成交单(09-09 09:40实证参数形状)+即撤; status57废单=会话陈旧
  ->触发relogin赶开盘; 窗口硬卡09:15:00-09:19:30(09:20-09:25不可撤)
- qmt_gate_common.py: redis/ping/tasklist/触发/轮询/gate留痕共享库
- 留痕: C:\sanguo_bigqmt\gate\qmt_gate_YYYYMMDD.log + qmt_gate_latest.json
- ui_act.py 修复: activate仅contains时KeyError; credtype LocalFree类型错; 新增text op
  (typewrite连打丢数字/shift字符,验证码须key/hotkey逐字符打)
- 实测: 首夜14跳全绿cash零漂移; 下单RPC管线ok=True; SKIP/退出码/桥死分支全验
- schtask XML x2(SYSTEM+HighestAvailable,照sanguo-xt-daily模式)
2026-09-10 07:24:45 +08:00

189 lines
7.0 KiB
Python

# -*- coding: utf-8 -*-
"""Execute UI actions from ui_actions.json (list of ops), screenshot after each
marked step. Runs in console session via schtask. ASCII output only.
Ops: click(x,y,clicks,button) move(x,y) key(k) hotkey(keys[]) scroll(dy,x,y)
wait(sec) shot(tag)
"""
import json
import time
import pyautogui
pyautogui.FAILSAFE = True
PLAN = r"C:\sanguo_bigqmt\ui_actions.json"
SHOT = r"C:\sanguo_bigqmt\shot_%02d.png"
plan = json.load(open(PLAN, encoding="utf-8")) # utf-8 regardless of GBK console
n = 0
def shot(tag=""):
global n
n += 1
pyautogui.screenshot().save(SHOT % n)
print("SHOT_%02d %s" % (n, tag))
for a in plan:
op = a["op"]
if op == "closewin":
import ctypes
user32 = ctypes.windll.user32
WM_CLOSE = 0x0010
if a.get("hwnd"):
user32.PostMessageW(a["hwnd"], WM_CLOSE, 0, 0)
print("CLOSEWIN hwnd=%s" % a["hwnd"])
else:
Ws2 = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
hits = []
@Ws2
def cb2(h2, _):
buf = ctypes.create_unicode_buffer(256)
user32.GetWindowTextW(h2, buf, 256)
if a["contains"] in buf.value:
hits.append(h2)
return True
user32.EnumWindows(cb2, 0)
for h2 in hits:
user32.PostMessageW(h2, WM_CLOSE, 0, 0)
print("CLOSEWIN %s hwnd=%s" % (a["contains"], h2))
if op == "exec":
import subprocess
r = subprocess.run([r"C:\Python310\python.exe", a["script"]],
capture_output=True, text=True, timeout=60)
for ln in (r.stdout or "").splitlines():
print("X| " + ln)
for ln in (r.stderr or "").splitlines()[:10]:
print("E| " + ln)
if op == "activate":
import ctypes
user32 = ctypes.windll.user32
title = a.get("title")
h = user32.FindWindowW(None, title) if title else 0
if not h and a.get("contains"):
Ws = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
found = []
@Ws
def cb(h2, _):
buf = ctypes.create_unicode_buffer(256)
user32.GetWindowTextW(h2, buf, 256)
if a["contains"] in buf.value and user32.IsWindowVisible(h2):
found.append(h2)
return True
user32.EnumWindows(cb, 0)
h = found[0] if found else 0
if h:
user32.ShowWindow(h, 9) # SW_RESTORE
user32.SetForegroundWindow(h)
time.sleep(0.8)
print("ACTIVATE ok hwnd=%s title=%s" % (h, a.get("title") or a.get("contains")))
else:
print("ACTIVATE_FAIL not_found")
if op == "launch":
import subprocess
cwd = a.get("cwd") or None
subprocess.Popen([a["exe"]], cwd=cwd)
print("LAUNCH %s" % a["exe"].encode("unicode_escape").decode("ascii"))
if op == "minimize":
import ctypes
user32 = ctypes.windll.user32
if a.get("hwnd"):
user32.ShowWindow(a["hwnd"], 6) # SW_MINIMIZE
print("MINIMIZE hwnd=%s" % a["hwnd"])
elif a.get("contains"):
Ws3 = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
hits = []
@Ws3
def cb3(h2, _):
buf = ctypes.create_unicode_buffer(256)
user32.GetWindowTextW(h2, buf, 256)
if a["contains"] in buf.value and user32.IsWindowVisible(h2):
hits.append(h2)
return True
user32.EnumWindows(cb3, 0)
for h2 in hits[:1]:
user32.ShowWindow(h2, 6)
print("MINIMIZE %s hwnd=%s" % (a["contains"], h2))
if op == "credtype":
# decrypt DPAPI secret in-process and type it into field (x,y).
# never printed, never written to disk.
import base64
import ctypes
class DATA_BLOB(ctypes.Structure):
_fields_ = [("cbData", ctypes.c_ulong),
("pbData", ctypes.POINTER(ctypes.c_char))]
enc_hex = open(r"C:\sanguo_bigqmt\creds\qmt_login.enc",
encoding="ascii").read().strip()
raw = bytes.fromhex(enc_hex)
bin_ = DATA_BLOB(len(raw), ctypes.cast(
ctypes.create_string_buffer(raw, len(raw)),
ctypes.POINTER(ctypes.c_char)))
bout = DATA_BLOB()
ok = ctypes.windll.crypt32.CryptUnprotectData(
ctypes.byref(bin_), None, None, None, None, 0, ctypes.byref(bout))
if not ok:
print("CREDTYPE decrypt_failed")
else:
plain = ctypes.string_at(bout.pbData, bout.cbData).decode(
"utf-16-le", errors="replace").rstrip("\x00")
# cred json: {"user": "...", "password": "..."}
import json as _j
pw = _j.loads(plain)["password"]
pyautogui.click(a["x"], a["y"])
time.sleep(0.4)
pyautogui.typewrite(pw, interval=0.06)
print("CREDTYPE typed len=%d" % len(pw))
ctypes.windll.kernel32.LocalFree(bout.pbData)
if op == "text":
# type arbitrary (ASCII) text at x,y — used for captcha fields
pyautogui.click(a["x"], a["y"])
time.sleep(0.3)
pyautogui.typewrite(a["s"], interval=0.08)
print("TEXT typed len=%d" % len(a["s"]))
if op == "info":
im = pyautogui.screenshot()
print("SIZE_cursor=%s SIZE_bitmap=%s" % (pyautogui.size(), im.size))
if op == "shot":
shot(a.get("tag", ""))
elif op == "region":
n += 1
im = pyautogui.screenshot()
im.crop((a["x"], a["y"], a["x"] + a["w"], a["y"] + a["h"])).save(
SHOT % n)
print("REGION_%02d %s" % (n, a.get("tag", "")))
elif op == "zoom":
n += 1
s = a.get("scale", 3)
im = pyautogui.screenshot().crop(
(a["x"], a["y"], a["x"] + a["w"], a["y"] + a["h"]))
im = im.resize((im.width * s, im.height * s))
im.save(SHOT % n)
print("ZOOM_%02d %s" % (n, a.get("tag", "")))
elif op == "click":
pyautogui.click(a["x"], a["y"], clicks=a.get("clicks", 1),
interval=0.35, button=a.get("button", "left"))
print("CLICK %s %s x%s" % (a["x"], a["y"], a.get("clicks", 1)))
elif op == "move":
pyautogui.moveTo(a["x"], a["y"], duration=0.25)
print("MOVE %s %s" % (a["x"], a["y"]))
elif op == "key":
pyautogui.press(a["key"])
print("KEY %s" % a["key"])
elif op == "hotkey":
pyautogui.hotkey(*a["keys"])
print("HOTKEY %s" % "+".join(a["keys"]))
elif op == "scroll":
pyautogui.scroll(a.get("dy", -300), x=a.get("x"), y=a.get("y"))
print("SCROLL %s" % a.get("dy", -300))
elif op == "wait":
time.sleep(a["sec"])
print("WAIT %s" % a["sec"])
time.sleep(a.get("after", 0.6))
shot("end")
print("UI_ACT_DONE nshots=%d" % n)