From 5082da78ab49737b2d65f1606bb524b117c74696 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 6 Apr 2026 10:16:00 +0800 Subject: [PATCH] chore: auto release commit (2026-04-06 10:16:00) --- scripts/am_login_child_runner.py | 223 ++++++++++++++++++++++++++ scripts/am_open_child_runner.py | 34 ++++ scripts/main.py | 259 +------------------------------ 3 files changed, 263 insertions(+), 253 deletions(-) create mode 100644 scripts/am_login_child_runner.py create mode 100644 scripts/am_open_child_runner.py diff --git a/scripts/am_login_child_runner.py b/scripts/am_login_child_runner.py new file mode 100644 index 0000000..87ddf05 --- /dev/null +++ b/scripts/am_login_child_runner.py @@ -0,0 +1,223 @@ +"""Playwright 登录检测子进程:由 main.cmd_login 以独立解释器启动,argv[1] 为 JSON 配置路径。""" +import json +import logging +import sys +import time +from urllib.parse import urlparse + +from playwright.sync_api import sync_playwright + + +def page_location_href(p): + # Prefer location.href over page.url for SPA (e.g. Sohu shell path lag). + try: + href = p.evaluate("() => location.href") + if href and str(href).strip(): + return str(href).strip() + except Exception: + pass + try: + return (p.url or "").strip() + except Exception: + return "" + + +def host_matches_anchor(netloc, anchor): + h = (netloc or "").lower().strip() + a = (anchor or "").lower().strip() + if not a: + return True + if not h: + return False + return h == a or h.endswith("." + a) or a.endswith("." + h) + + +def loc_first_visible(pg, selectors): + for sel in selectors: + s = str(sel).strip() + if not s: + continue + try: + loc = pg.locator(s).first + if loc.is_visible(timeout=500): + return True, s + except Exception: + pass + return False, None + + +def list_pages_on_anchor(ctx, main_page, bundle): + anchor = (bundle.get("anchor_host") or "").strip().lower() + seen_ids = set() + pages = [] + for pg in list(ctx.pages or []): + try: + i = id(pg) + if i not in seen_ids: + seen_ids.add(i) + pages.append(pg) + except Exception: + pages.append(pg) + if main_page is not None: + try: + i = id(main_page) + if i not in seen_ids: + seen_ids.add(i) + pages.append(main_page) + except Exception: + pages.append(main_page) + out = [] + for pg in pages: + try: + href = page_location_href(pg) + except Exception: + continue + if not href or str(href).lower().startswith("about:"): + continue + try: + host = urlparse(href).netloc.lower() + except Exception: + continue + if anchor and not host_matches_anchor(host, anchor): + continue + out.append(pg) + return out + + +def evaluate_logged_out_dom(ctx, main_page, bundle): + selectors = [ + str(x).strip() + for x in (bundle.get("logged_out_dom_selectors") or []) + if x is not None and str(x).strip() + ] + if not selectors: + return True, "no_logged_out_dom_selectors_configured" + for pg in list_pages_on_anchor(ctx, main_page, bundle): + try: + href = page_location_href(pg) + except Exception: + href = "" + hit, which = loc_first_visible(pg, selectors) + if hit: + return True, "logged_out_dom=%r url=%r" % (which, href) + return False, "" + + +def main(): + with open(sys.argv[1], encoding="utf-8") as f: + c = json.load(f) + bundle = c.get("login_detect_bundle") or {} + poll = float(c.get("poll_interval", 1.5)) + try: + dom_grace = float(c.get("dom_grace_sec", 4.0)) + except (TypeError, ValueError): + dom_grace = 4.0 + if dom_grace < 0: + dom_grace = 0.0 + t0 = time.time() + deadline = t0 + float(c["timeout_sec"]) + interactive_ok = False + result_path = c.get("result_path") or "" + log_path = (c.get("log_file") or "").strip() + lg = logging.getLogger("openclaw.skill.account_manager.login_child") + if log_path: + lg.handlers.clear() + lg.setLevel(logging.DEBUG) + _fh = logging.FileHandler(log_path, encoding="utf-8") + _fh.setFormatter( + logging.Formatter( + "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + ) + ) + lg.addHandler(_fh) + lg.propagate = False + last_eval_detail = "" + with sync_playwright() as p: + ctx = p.chromium.launch_persistent_context( + user_data_dir=c["profile_dir"], + headless=False, + channel=c["channel"], + no_viewport=True, + args=["--start-maximized"], + ) + try: + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) + if dom_grace > 0: + time.sleep(dom_grace) + if lg.handlers: + lg.info( + "goto_done initial_pages=%s poll_interval_sec=%s dom_grace_sec=%s", + len(ctx.pages or []), + poll, + dom_grace, + ) + while time.time() < deadline: + iter_start = time.time() + try: + still_logged_out, detail = evaluate_logged_out_dom(ctx, page, bundle) + ok = not still_logged_out + last_eval_detail = detail + if lg.handlers: + parts = [] + for tab in list(ctx.pages or []): + try: + href = page_location_href(tab) + pu = (tab.url or "").strip() + parts.append("href=%r playwright_url=%r" % (href, pu)) + except Exception: + parts.append("(tab_read_error)") + lg.debug( + "poll iter_start_elapsed=%.1fs deadline_in=%.1fs tabs=%s poll_interval_sec=%s logged_out=%s ok=%s detail=%s | %s", + iter_start - t0, + deadline - iter_start, + len(ctx.pages or []), + poll, + still_logged_out, + ok, + detail, + " ; ".join(parts) if parts else "no_tabs", + ) + if ok: + interactive_ok = True + if lg.handlers: + lg.info("login_detected_ok %s", detail) + break + except Exception as ex: + if lg.handlers: + lg.warning("poll_exception err=%s", ex, exc_info=True) + spent = time.time() - iter_start + time.sleep(max(0.0, poll - spent)) + if not interactive_ok: + if lg.handlers: + lg.warning("login_poll_exhausted detail=%s", last_eval_detail) + rem = max(0.0, deadline - time.time()) + if rem > 0: + try: + ctx.wait_for_event("close", timeout=rem * 1000) + except Exception: + pass + else: + if lg.handlers: + lg.info("login_success_closing_browser_immediately") + except Exception as e: + if lg.handlers: + lg.exception("login_runner_fatal err=%s", e) + print(e, file=sys.stderr) + raise + finally: + if result_path: + try: + with open(result_path, "w", encoding="utf-8") as rf: + json.dump({"interactive_ok": interactive_ok}, rf, ensure_ascii=False) + except Exception: + pass + try: + ctx.close() + except Exception: + pass + + +if __name__ == "__main__": + main() diff --git a/scripts/am_open_child_runner.py b/scripts/am_open_child_runner.py new file mode 100644 index 0000000..6e0df5f --- /dev/null +++ b/scripts/am_open_child_runner.py @@ -0,0 +1,34 @@ +"""Playwright 仅打开浏览器子进程:由 main.cmd_open 启动,argv[1] 为 JSON 配置路径。""" +import json +import sys + +from playwright.sync_api import sync_playwright + + +def main(): + with open(sys.argv[1], encoding="utf-8") as f: + c = json.load(f) + with sync_playwright() as p: + ctx = p.chromium.launch_persistent_context( + user_data_dir=c["profile_dir"], + headless=False, + channel=c["channel"], + no_viewport=True, + args=["--start-maximized"], + ) + try: + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) + ctx.wait_for_event("close", timeout=86400000) + except Exception as e: + print(e, file=sys.stderr) + raise + finally: + try: + ctx.close() + except Exception: + pass + + +if __name__ == "__main__": + main() diff --git a/scripts/main.py b/scripts/main.py index 6f2809f..74e1b04 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -1074,214 +1074,8 @@ def cmd_login(account_id): url, log_file, ) - runner = r"""import json, logging, sys, time -from urllib.parse import urlparse -from playwright.sync_api import sync_playwright - -def page_location_href(p): - # Prefer location.href over page.url for SPA (e.g. Sohu shell path lag). - try: - href = p.evaluate("() => location.href") - if href and str(href).strip(): - return str(href).strip() - except Exception: - pass - try: - return (p.url or "").strip() - except Exception: - return "" - -def host_matches_anchor(netloc, anchor): - h = (netloc or "").lower().strip() - a = (anchor or "").lower().strip() - if not a: - return True - if not h: - return False - return h == a or h.endswith("." + a) or a.endswith("." + h) - -def loc_first_visible(pg, selectors): - for sel in selectors: - s = str(sel).strip() - if not s: - continue - try: - loc = pg.locator(s).first - if loc.is_visible(timeout=500): - return True, s - except Exception: - pass - return False, None - -def list_pages_on_anchor(ctx, main_page, bundle): - anchor = (bundle.get("anchor_host") or "").strip().lower() - seen_ids = set() - pages = [] - for pg in list(ctx.pages or []): - try: - i = id(pg) - if i not in seen_ids: - seen_ids.add(i) - pages.append(pg) - except Exception: - pages.append(pg) - if main_page is not None: - try: - i = id(main_page) - if i not in seen_ids: - seen_ids.add(i) - pages.append(main_page) - except Exception: - pages.append(main_page) - out = [] - for pg in pages: - try: - href = page_location_href(pg) - except Exception: - continue - if not href or str(href).lower().startswith("about:"): - continue - try: - host = urlparse(href).netloc.lower() - except Exception: - continue - if anchor and not host_matches_anchor(host, anchor): - continue - out.append(pg) - return out - -def evaluate_logged_out_dom(ctx, main_page, bundle): - selectors = [ - str(x).strip() - for x in (bundle.get("logged_out_dom_selectors") or []) - if x is not None and str(x).strip() - ] - if not selectors: - return True, "no_logged_out_dom_selectors_configured" - for pg in list_pages_on_anchor(ctx, main_page, bundle): - try: - href = page_location_href(pg) - except Exception: - href = "" - hit, which = loc_first_visible(pg, selectors) - if hit: - return True, "logged_out_dom=%r url=%r" % (which, href) - return False, "" - -with open(sys.argv[1], encoding="utf-8") as f: - c = json.load(f) -bundle = c.get("login_detect_bundle") or {} -poll = float(c.get("poll_interval", 1.5)) -try: - dom_grace = float(c.get("dom_grace_sec", 4.0)) -except (TypeError, ValueError): - dom_grace = 4.0 -if dom_grace < 0: - dom_grace = 0.0 -t0 = time.time() -deadline = t0 + float(c["timeout_sec"]) -interactive_ok = False -result_path = c.get("result_path") or "" -log_path = (c.get("log_file") or "").strip() -lg = logging.getLogger("openclaw.skill.account_manager.login_child") -if log_path: - lg.handlers.clear() - lg.setLevel(logging.DEBUG) - _fh = logging.FileHandler(log_path, encoding="utf-8") - _fh.setFormatter( - logging.Formatter( - "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", - datefmt="%Y-%m-%dT%H:%M:%S", - ) - ) - lg.addHandler(_fh) - lg.propagate = False -last_eval_detail = "" -with sync_playwright() as p: - ctx = p.chromium.launch_persistent_context( - user_data_dir=c["profile_dir"], - headless=False, - channel=c["channel"], - no_viewport=True, - args=["--start-maximized"], - ) - try: - page = ctx.pages[0] if ctx.pages else ctx.new_page() - page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) - if dom_grace > 0: - time.sleep(dom_grace) - if lg.handlers: - lg.info( - "goto_done initial_pages=%s poll_interval_sec=%s dom_grace_sec=%s", - len(ctx.pages or []), - poll, - dom_grace, - ) - while time.time() < deadline: - iter_start = time.time() - try: - still_logged_out, detail = evaluate_logged_out_dom(ctx, page, bundle) - ok = not still_logged_out - last_eval_detail = detail - if lg.handlers: - parts = [] - for tab in list(ctx.pages or []): - try: - href = page_location_href(tab) - pu = (tab.url or "").strip() - parts.append("href=%r playwright_url=%r" % (href, pu)) - except Exception: - parts.append("(tab_read_error)") - lg.debug( - "poll iter_start_elapsed=%.1fs deadline_in=%.1fs tabs=%s poll_interval_sec=%s logged_out=%s ok=%s detail=%s | %s", - iter_start - t0, - deadline - iter_start, - len(ctx.pages or []), - poll, - still_logged_out, - ok, - detail, - " ; ".join(parts) if parts else "no_tabs", - ) - if ok: - interactive_ok = True - if lg.handlers: - lg.info("login_detected_ok %s", detail) - break - except Exception as ex: - if lg.handlers: - lg.warning("poll_exception err=%s", ex, exc_info=True) - spent = time.time() - iter_start - time.sleep(max(0.0, poll - spent)) - if not interactive_ok: - if lg.handlers: - lg.warning("login_poll_exhausted detail=%s", last_eval_detail) - rem = max(0.0, deadline - time.time()) - if rem > 0: - try: - ctx.wait_for_event("close", timeout=rem * 1000) - except Exception: - pass - else: - if lg.handlers: - lg.info("login_success_closing_browser_immediately") - except Exception as e: - if lg.handlers: - lg.exception("login_runner_fatal err=%s", e) - print(e, file=sys.stderr) - raise - finally: - if result_path: - try: - with open(result_path, "w", encoding="utf-8") as rf: - json.dump({"interactive_ok": interactive_ok}, rf, ensure_ascii=False) - except Exception: - pass - try: - ctx.close() - except Exception: - pass -""" + _scripts_dir = os.path.dirname(os.path.abspath(__file__)) + login_runner_path = os.path.join(_scripts_dir, "am_login_child_runner.py") cfg = { "channel": channel, "profile_dir": profile_dir, @@ -1302,15 +1096,10 @@ with sync_playwright() as p: ) as jf: json.dump(cfg, jf, ensure_ascii=False) cfg_path = jf.name - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False, encoding="utf-8" - ) as pf: - pf.write(runner) - py_path = pf.name proc_rc = None try: r = subprocess.run( - [sys.executable, py_path, cfg_path], + [sys.executable, login_runner_path, cfg_path], timeout=timeout_sec + 180, ) proc_rc = r.returncode @@ -1325,10 +1114,6 @@ with sync_playwright() as p: os.unlink(cfg_path) except OSError: pass - try: - os.unlink(py_path) - except OSError: - pass interactive_ok = False try: @@ -1398,53 +1183,21 @@ def cmd_open(account_id): print("请在本窗口中自行确认登录态。关闭浏览器后命令结束。") print("需要自动检测并写入数据库时,请执行:python main.py login ") - runner_open = r"""import json, sys -from playwright.sync_api import sync_playwright -with open(sys.argv[1], encoding="utf-8") as f: - c = json.load(f) -with sync_playwright() as p: - ctx = p.chromium.launch_persistent_context( - user_data_dir=c["profile_dir"], - headless=False, - channel=c["channel"], - no_viewport=True, - args=["--start-maximized"], - ) - try: - page = ctx.pages[0] if ctx.pages else ctx.new_page() - page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) - ctx.wait_for_event("close", timeout=86400000) - except Exception as e: - print(e, file=sys.stderr) - raise - finally: - try: - ctx.close() - except Exception: - pass -""" + _scripts_dir = os.path.dirname(os.path.abspath(__file__)) + open_runner_path = os.path.join(_scripts_dir, "am_open_child_runner.py") cfg = {"channel": channel, "profile_dir": profile_dir, "url": url} with tempfile.NamedTemporaryFile( mode="w", suffix=".json", delete=False, encoding="utf-8" ) as jf: json.dump(cfg, jf, ensure_ascii=False) cfg_path = jf.name - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False, encoding="utf-8" - ) as pf: - pf.write(runner_open) - py_path = pf.name try: - subprocess.run([sys.executable, py_path, cfg_path]) + subprocess.run([sys.executable, open_runner_path, cfg_path]) finally: try: os.unlink(cfg_path) except OSError: pass - try: - os.unlink(py_path) - except OSError: - pass def _mark_login_status(account_id, success: bool):