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) <noreply@anthropic.com>
This commit is contained in:
Pnant
2026-03-31 15:54:59 +08:00
committed by GitHub
parent c5a304dc20
commit 21214fd02a
2 changed files with 71 additions and 34 deletions
+17 -9
View File
@@ -79,14 +79,22 @@ def _load_cookies_from_browser() -> bool:
a local browser keep working. a local browser keep working.
""" """
try: try:
import browser_cookie3 try:
import rookiepy
cookies = list(browser_cookie3.chrome(domain_name=".xueqiu.com")) cookies = rookiepy.chrome([".xueqiu.com"])
if not any(c.name == "xq_a_token" for c in cookies): if not any(c.get("name") == "xq_a_token" for c in cookies):
return False return False
for c in cookies: for c in cookies:
_cookie_jar.set_cookie(c) _cookie_jar.set(c["name"], c["value"], domain=c.get("domain", ".xueqiu.com"))
return True 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: except Exception:
return False return False
@@ -96,7 +104,7 @@ def _ensure_cookies() -> None:
Priority order: Priority order:
1. Saved cookie string in ~/.agent-reach/config.yaml (set by configure --from-browser) 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, 3. Homepage visit fallback (only yields anti-DDoS acw_tc,
not enough for stock APIs) not enough for stock APIs)
""" """
+54 -25
View File
@@ -52,36 +52,66 @@ def extract_all(browser: str = "chrome") -> Dict[str, dict]:
"bilibili": {"SESSDATA": "xxx", "bili_jct": "yyy"}, "bilibili": {"SESSDATA": "xxx", "bili_jct": "yyy"},
} }
""" """
# Try rookiepy first (Rust-based, more stable), fallback to browser_cookie3
use_rookiepy = False
try: try:
import browser_cookie3 import rookiepy
use_rookiepy = True
except ImportError: except ImportError:
raise RuntimeError( try:
"browser_cookie3 not installed. Run: pip install browser-cookie3" import browser_cookie3
) except ImportError:
raise RuntimeError(
# Get browser cookie jar "Cookie extraction requires rookiepy or browser_cookie3.\n"
browser_funcs = { "Install: pip install rookiepy (recommended)\n"
"chrome": browser_cookie3.chrome, " or: pip install browser-cookie3"
"firefox": browser_cookie3.firefox, )
"edge": browser_cookie3.edge,
"brave": browser_cookie3.brave,
"opera": browser_cookie3.opera,
}
browser = browser.lower() browser = browser.lower()
if browser not in browser_funcs: supported = ["chrome", "firefox", "edge", "brave", "opera"]
if browser not in supported:
raise ValueError( raise ValueError(
f"Unsupported browser: {browser}. " f"Unsupported browser: {browser}. Supported: {', '.join(supported)}"
f"Supported: {', '.join(browser_funcs.keys())}"
) )
try: if use_rookiepy:
cookie_jar = browser_funcs[browser]() # rookiepy returns list of dicts with name/value/domain/path keys
except Exception as e: try:
raise RuntimeError( browser_funcs = {
f"Could not read {browser} cookies: {e}\n" "chrome": rookiepy.chrome,
f"Make sure {browser} is closed and you have permission to read its data." "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 = {} 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: if "auth_token" in tc and "ct0" in tc:
config.set("twitter_auth_token", tc["auth_token"]) config.set("twitter_auth_token", tc["auth_token"])
config.set("twitter_ct0", tc["ct0"]) config.set("twitter_ct0", tc["ct0"])
# Sync credentials to bird CLI env and legacy xfetch session.json # Legacy sync (best-effort)
_sync_bird_env(tc["auth_token"], tc["ct0"])
_sync_xfetch_session(tc["auth_token"], tc["ct0"]) _sync_xfetch_session(tc["auth_token"], tc["ct0"])
results_list.append(("Twitter/X", True, "auth_token + ct0")) results_list.append(("Twitter/X", True, "auth_token + ct0"))
else: else: