From 21214fd02ab2a0b58130357f39de583ec68f5cf1 Mon Sep 17 00:00:00 2001 From: Pnant <73925474+Panniantong@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:54:59 +0800 Subject: [PATCH] fix(xueqiu): add rookiepy as primary cookie backend, keep browser_cookie3 as fallback (#232) browser_cookie3 has been unmaintained for 15+ months and breaks with newer browser versions. rookiepy (Rust-based, 345 stars, updated 2026-01) is now the preferred cookie extraction backend. - xueqiu.py: try rookiepy first, fallback to browser_cookie3 - cookie_extract.py: same priority order, compatible API wrapper - Remove bird CLI credential sync (bird repo deleted) - 104 tests passing Co-authored-by: Claude Opus 4.6 (1M context) --- agent_reach/channels/xueqiu.py | 26 +++++++---- agent_reach/cookie_extract.py | 79 +++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 34 deletions(-) diff --git a/agent_reach/channels/xueqiu.py b/agent_reach/channels/xueqiu.py index 6b62b06..4bef961 100644 --- a/agent_reach/channels/xueqiu.py +++ b/agent_reach/channels/xueqiu.py @@ -79,14 +79,22 @@ def _load_cookies_from_browser() -> bool: a local browser keep working. """ try: - import browser_cookie3 - - cookies = list(browser_cookie3.chrome(domain_name=".xueqiu.com")) - if not any(c.name == "xq_a_token" for c in cookies): - return False - for c in cookies: - _cookie_jar.set_cookie(c) - return True + try: + import rookiepy + cookies = rookiepy.chrome([".xueqiu.com"]) + if not any(c.get("name") == "xq_a_token" for c in cookies): + return False + for c in cookies: + _cookie_jar.set(c["name"], c["value"], domain=c.get("domain", ".xueqiu.com")) + return True + except ImportError: + import browser_cookie3 + cookies = list(browser_cookie3.chrome(domain_name=".xueqiu.com")) + if not any(c.name == "xq_a_token" for c in cookies): + return False + for c in cookies: + _cookie_jar.set_cookie(c) + return True except Exception: return False @@ -96,7 +104,7 @@ def _ensure_cookies() -> None: Priority order: 1. Saved cookie string in ~/.agent-reach/config.yaml (set by configure --from-browser) - 2. Live Chrome browser cookies via browser_cookie3 (if installed + logged in) + 2. Live Chrome browser cookies via rookiepy/browser_cookie3 (if installed + logged in) 3. Homepage visit fallback (only yields anti-DDoS acw_tc, not enough for stock APIs) """ diff --git a/agent_reach/cookie_extract.py b/agent_reach/cookie_extract.py index 0ba3417..727a441 100644 --- a/agent_reach/cookie_extract.py +++ b/agent_reach/cookie_extract.py @@ -52,36 +52,66 @@ def extract_all(browser: str = "chrome") -> Dict[str, dict]: "bilibili": {"SESSDATA": "xxx", "bili_jct": "yyy"}, } """ + # Try rookiepy first (Rust-based, more stable), fallback to browser_cookie3 + use_rookiepy = False try: - import browser_cookie3 + import rookiepy + use_rookiepy = True except ImportError: - raise RuntimeError( - "browser_cookie3 not installed. Run: pip install browser-cookie3" - ) - - # Get browser cookie jar - browser_funcs = { - "chrome": browser_cookie3.chrome, - "firefox": browser_cookie3.firefox, - "edge": browser_cookie3.edge, - "brave": browser_cookie3.brave, - "opera": browser_cookie3.opera, - } + try: + import browser_cookie3 + except ImportError: + raise RuntimeError( + "Cookie extraction requires rookiepy or browser_cookie3.\n" + "Install: pip install rookiepy (recommended)\n" + " or: pip install browser-cookie3" + ) browser = browser.lower() - if browser not in browser_funcs: + supported = ["chrome", "firefox", "edge", "brave", "opera"] + if browser not in supported: raise ValueError( - f"Unsupported browser: {browser}. " - f"Supported: {', '.join(browser_funcs.keys())}" + f"Unsupported browser: {browser}. Supported: {', '.join(supported)}" ) - try: - cookie_jar = browser_funcs[browser]() - except Exception as e: - raise RuntimeError( - f"Could not read {browser} cookies: {e}\n" - f"Make sure {browser} is closed and you have permission to read its data." - ) + if use_rookiepy: + # rookiepy returns list of dicts with name/value/domain/path keys + try: + browser_funcs = { + "chrome": rookiepy.chrome, + "firefox": rookiepy.firefox, + "edge": rookiepy.edge, + "brave": rookiepy.brave, + "opera": rookiepy.opera, + } + raw_cookies = browser_funcs[browser]() + # Wrap into objects with .name, .value, .domain for compatibility + class _Cookie: + def __init__(self, d): + self.name = d.get("name", "") + self.value = d.get("value", "") + self.domain = d.get("domain", "") + cookie_jar = [_Cookie(c) for c in raw_cookies] + except Exception as e: + raise RuntimeError( + f"Could not read {browser} cookies via rookiepy: {e}\n" + f"Make sure {browser} is closed and you have permission." + ) + else: + browser_funcs = { + "chrome": browser_cookie3.chrome, + "firefox": browser_cookie3.firefox, + "edge": browser_cookie3.edge, + "brave": browser_cookie3.brave, + "opera": browser_cookie3.opera, + } + try: + cookie_jar = browser_funcs[browser]() + except Exception as e: + raise RuntimeError( + f"Could not read {browser} cookies: {e}\n" + f"Make sure {browser} is closed and you have permission." + ) results = {} @@ -193,8 +223,7 @@ def configure_from_browser(browser: str, config) -> List[Tuple[str, bool, str]]: if "auth_token" in tc and "ct0" in tc: config.set("twitter_auth_token", tc["auth_token"]) config.set("twitter_ct0", tc["ct0"]) - # Sync credentials to bird CLI env and legacy xfetch session.json - _sync_bird_env(tc["auth_token"], tc["ct0"]) + # Legacy sync (best-effort) _sync_xfetch_session(tc["auth_token"], tc["ct0"]) results_list.append(("Twitter/X", True, "auth_token + ct0")) else: