cdp_exact_launch.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. """Correct game launch: use the KNOWN enterGameClient HTTP path from our SDK
  2. (same as the real client's 启动 button) targeted at gameId=206923 + a real
  3. 玖扬 serverid, PLUS CDP-click verification. But first: find the true 玖扬
  4. game detail page via the search flow (click suggestion properly this time),
  5. because EnterGameClient only records the launch — cq.exe is spawned by the
  6. Electron side via the game page's IPC.
  7. Robust plan:
  8. 1. CDP: focus search input, select-all, type 玖扬复古176 with REAL key events
  9. 2. Wait for suggestion dropdown; click the suggestion whose text EXACTLY
  10. starts with 玖扬 (verify its gameId in its attributes/parent)
  11. 3. On the detail page: verify the game name shows 玖扬复古176 before clicking 启动
  12. 4. Background capture running the whole time
  13. """
  14. import asyncio
  15. import base64
  16. import json
  17. import subprocess
  18. import sys
  19. import time
  20. import httpx
  21. import websockets
  22. CDP = "http://127.0.0.1:9222"
  23. GAME = "玖扬复古176"
  24. async def get_targets():
  25. async with httpx.AsyncClient(timeout=5) as c:
  26. r = await c.get(f"{CDP}/json/list")
  27. return r.json()
  28. class Page:
  29. def __init__(self, ws):
  30. self.ws = ws
  31. self.mid = 0
  32. async def call(self, method, params=None):
  33. self.mid += 1
  34. await self.ws.send(json.dumps({"id": self.mid, "method": method, "params": params or {}}))
  35. while True:
  36. m = json.loads(await self.ws.recv())
  37. if m.get("id") == self.mid:
  38. return m.get("result", {})
  39. async def eval(self, expr):
  40. r = await self.call("Runtime.evaluate", {"expression": expr, "returnByValue": True})
  41. return r.get("result", {}).get("value")
  42. async def click(self, x, y):
  43. await self.call("Input.dispatchMouseEvent", {"type": "mousePressed", "x": x, "y": y, "button": "left", "clickCount": 1, "buttons": 1})
  44. await asyncio.sleep(0.06)
  45. await self.call("Input.dispatchMouseEvent", {"type": "mouseReleased", "x": x, "y": y, "button": "left", "clickCount": 1, "buttons": 0})
  46. async def key(self, key, code, vk, text=None):
  47. p = {"type": "keyDown", "key": key, "code": code, "windowsVirtualKeyCode": vk}
  48. if text:
  49. p["text"] = text
  50. await self.call("Input.dispatchKeyEvent", p)
  51. await asyncio.sleep(0.03)
  52. p["type"] = "keyUp"
  53. await self.call("Input.dispatchKeyEvent", p)
  54. async def shot(self, name):
  55. r = await self.call("Page.captureScreenshot", {"format": "png"})
  56. if r.get("data"):
  57. with open(name, "wb") as f:
  58. f.write(base64.b64decode(r["data"]))
  59. def poll():
  60. try:
  61. out = subprocess.run(
  62. ["powershell", "-NoProfile", "-Command",
  63. "$p = Get-Process | Where-Object {$_.Name -match '^cq'} | ForEach-Object { $_.Name + ':' + $_.Id };"
  64. "$g = (netstat -ano | Select-String ':12000' | Measure-Object).Count;"
  65. "$c = (Get-Content 'D:\\传奇\\cq2_sdk\\real_login.jsonl' -ErrorAction SilentlyContinue | Measure-Object).Count;"
  66. "ConvertTo-Json @{procs=$p; gw=$g; cap=$c}"],
  67. capture_output=True, text=True, timeout=8)
  68. return json.loads(out.stdout or "{}")
  69. except Exception:
  70. return {}
  71. async def main():
  72. targets = await get_targets()
  73. home = next((t for t in targets if "homePage" in t.get("url", "")), None)
  74. if not home:
  75. print("no home page")
  76. return 1
  77. async with websockets.connect(home["webSocketDebuggerUrl"], max_size=10**7) as ws:
  78. page = Page(ws)
  79. # ---- 1. search flow ----
  80. inp = await page.eval("""
  81. (function() {
  82. const inps = document.querySelectorAll('input');
  83. for (const i of inps) {
  84. const r = i.getBoundingClientRect();
  85. if (r.width > 100 && r.y < 150 && i.offsetParent) {
  86. return JSON.stringify({x: r.x + 30, y: r.y + r.height/2});
  87. }
  88. }
  89. return null;
  90. })()
  91. """)
  92. if not inp:
  93. print("no search input")
  94. return 1
  95. pos = json.loads(inp)
  96. print(f"search input @ ({pos['x']:.0f},{pos['y']:.0f})")
  97. # focus + select all + backspace (clear old text)
  98. await page.click(pos["x"], pos["y"])
  99. await asyncio.sleep(0.3)
  100. await page.key("a", "KeyA", 65)
  101. await asyncio.sleep(0.1)
  102. await page.key("Backspace", "Backspace", 8)
  103. await asyncio.sleep(0.3)
  104. # type via IME-commit style: set value + input event per char
  105. # (CDP can't do real IME, but the page's input handler reads .value)
  106. for ch in GAME:
  107. await page.eval(f"""
  108. (function() {{
  109. const i = document.activeElement;
  110. if (i && i.tagName === 'INPUT') {{
  111. const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
  112. set.call(i, i.value + '{ch}');
  113. i.dispatchEvent(new Event('input', {{bubbles: true}}));
  114. i.dispatchEvent(new KeyboardEvent('keyup', {{key: '{ch}', bubbles: true}}));
  115. }}
  116. }})()
  117. """)
  118. await asyncio.sleep(0.12)
  119. val = await page.eval("document.activeElement && document.activeElement.value")
  120. print(f"typed: {val}")
  121. # press Enter to commit search
  122. await page.key("Enter", "Enter", 13, text="\r")
  123. await asyncio.sleep(4)
  124. url = await page.eval("location.href")
  125. print(f"url after Enter: {url[:80]}")
  126. await page.shot("search_commit.png")
  127. # ---- 2. find the EXACT game card ----
  128. card = await page.eval(r"""
  129. (function() {
  130. // search-results page cards; match text EXACTLY on 玖扬复古176
  131. const els = document.querySelectorAll('*');
  132. let best = null;
  133. for (const el of els) {
  134. if (el.children.length <= 4 && el.offsetParent) {
  135. const t = (el.innerText || '').trim();
  136. if (t.replace(/\s/g, '') === '玖扬复古176') {
  137. const r = el.getBoundingClientRect();
  138. if (r.width > 60 && r.height > 30 && (!best || r.width > best.w)) {
  139. best = {x: r.x + r.width/2, y: r.y + r.height/2, w: r.width, h: r.height};
  140. }
  141. }
  142. }
  143. }
  144. return best ? JSON.stringify(best) : null;
  145. })()
  146. """)
  147. if not card:
  148. # maybe still on home; check any exact-match anywhere
  149. print("no exact card; screenshot for diagnosis")
  150. body = await page.eval("document.body.innerText.slice(0, 400)")
  151. print(f"body head: {body}")
  152. return 1
  153. c = json.loads(card)
  154. print(f"*** EXACT 玖扬复古176 card @ ({c['x']:.0f},{c['y']:.0f}) ***")
  155. await page.shot("target_card.png")
  156. # ---- 3. click card -> detail page ----
  157. await page.click(c["x"], c["y"])
  158. await asyncio.sleep(4)
  159. url2 = await page.eval("location.href")
  160. title = await page.eval("document.title")
  161. print(f"after card click: url={url2[:80]} title={title}")
  162. # verify we're on 玖扬's page
  163. verify = await page.eval("""
  164. document.body.innerText.includes('玖扬复古176') ? 'YES' : document.body.innerText.slice(0,200)
  165. """)
  166. print(f"page contains 玖扬复古176: {verify if verify == 'YES' else str(verify)[:150]}")
  167. if verify != "YES":
  168. print("WRONG PAGE — aborting to avoid another misfire")
  169. await page.shot("wrong_page.png")
  170. return 1
  171. # ---- 4. find + click 启动 on THE GAME'S OWN PAGE ----
  172. for attempt in range(4):
  173. btn = await page.eval(r"""
  174. (function() {
  175. const btns = document.querySelectorAll('button, [class*=btn]');
  176. for (const b of btns) {
  177. const t = (b.innerText || '').trim();
  178. if (/^(启动|开始游戏|进入游戏)$/.test(t) && b.offsetParent) {
  179. const r = b.getBoundingClientRect();
  180. return JSON.stringify({text: t, x: r.x + r.width/2, y: r.y + r.height/2});
  181. }
  182. }
  183. return null;
  184. })()
  185. """)
  186. if btn:
  187. b = json.loads(btn)
  188. print(f"启动 button: '{b['text']}' @ ({b['x']:.0f},{b['y']:.0f})")
  189. await page.shot("before_launch_target.png")
  190. await page.click(b["x"], b["y"])
  191. print("*** 启动 CLICKED (on 玖扬复古176's page) ***")
  192. break
  193. await asyncio.sleep(2.5)
  194. else:
  195. print("no 启动 button on detail page")
  196. await page.shot("no_launch_on_detail.png")
  197. return 1
  198. # ---- 5. monitor ----
  199. print("\n=== monitoring 240s ===")
  200. t0 = time.time()
  201. ok = False
  202. while time.time() - t0 < 240:
  203. await asyncio.sleep(5)
  204. el = int(time.time() - t0)
  205. st = poll()
  206. procs = st.get("procs", [])
  207. gw = st.get("gw", 0)
  208. cap = st.get("cap", 0)
  209. flags = []
  210. if procs:
  211. flags.append("procs=" + ",".join(p.split(":")[0] for p in (procs if isinstance(procs, list) else [procs])))
  212. if gw:
  213. flags.append(f"gw={gw}")
  214. if cap:
  215. flags.append(f"cap={cap}")
  216. print(f" t+{el:>3}s {' '.join(flags) or '...'}")
  217. if cap >= 2:
  218. ok = True
  219. print("\n*** CAPTURED REAL 玖扬 LOGIN TRAFFIC ***")
  220. await asyncio.sleep(20)
  221. break
  222. if procs and gw and el > 60:
  223. ok = True
  224. print("\n*** 玖扬 game connected to gateway ***")
  225. await asyncio.sleep(30)
  226. break
  227. return 0 if ok else 1
  228. sys.exit(asyncio.run(main()))