fix(xueqiu): fix 400 errors by using browser cookies and correct headers
The Xueqiu stock API requires a login session token (xq_a_token) that is generated by Xueqiu's frontend JavaScript and cannot be obtained by simply visiting the homepage. This caused persistent HTTP 400 (error code 400016) for all users. Changes: - _ensure_cookies(): add three-level priority — config file (saved by --from-browser) → live Chrome cookies via browser_cookie3 → homepage fallback. The homepage-only approach only ever got acw_tc (anti-DDoS token), never xq_a_token. - _get_json(): switch User-Agent from "agent-reach/1.0" to a real Chrome UA, and add Referer: https://xueqiu.com/ to all API requests. - get_hot_posts(): replace the defunct /statuses/hot/listV3.json endpoint (returns empty body) with the v4 public timeline endpoint; correctly parse item.data as a JSON string to extract author, text, and likes. - cookie_extract.py: add Xueqiu to PLATFORM_SPECS and configure_from_browser so that `agent-reach configure --from-browser chrome` now also saves Xueqiu cookies (only when xq_a_token is present). - check(): improve error message to direct users to --from-browser instead of suggesting a proxy. - Fix urllib.parse.quote usage (was using urllib.request.quote). - Update tier and backends description to reflect cookie requirement. - Add 2 new tests: cookie loading from config, Referer/UA header verification. - Update docs: README, install guide, troubleshooting, SKILL.md, CHANGELOG.
This commit is contained in:
+92
-39
@@ -473,31 +473,23 @@ class TestXueqiuChannel:
|
||||
|
||||
monkeypatch.setattr(xueqiu_mod, "_cookies_initialized", True)
|
||||
|
||||
fake_data = {
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"original_status": {
|
||||
"id": 111,
|
||||
"title": "市场分析",
|
||||
"text": "<p>今天大盘走势&分析</p>",
|
||||
"user": {"screen_name": "投资者A"},
|
||||
"like_count": 42,
|
||||
"target": "/1234567890/111",
|
||||
}
|
||||
},
|
||||
{
|
||||
"original_status": {
|
||||
"id": 222,
|
||||
"title": "",
|
||||
"text": "短评",
|
||||
"user": {"screen_name": "投资者B"},
|
||||
"like_count": 10,
|
||||
"target": "/9876543210/222",
|
||||
}
|
||||
},
|
||||
]
|
||||
# v4 timeline: each item has a JSON-encoded `data` field
|
||||
def make_item(id_, title, text, author, likes, target):
|
||||
post = {
|
||||
"id": id_,
|
||||
"title": title,
|
||||
"text": text,
|
||||
"user": {"screen_name": author},
|
||||
"like_count": likes,
|
||||
"target": target,
|
||||
}
|
||||
return {"data": json.dumps(post), "original_status": None}
|
||||
|
||||
fake_data = {
|
||||
"list": [
|
||||
make_item(111, "市场分析", "<p>今天大盘走势&分析</p>", "投资者A", 42, "/1234567890/111"),
|
||||
make_item(222, "", "短评", "投资者B", 10, "/9876543210/222"),
|
||||
]
|
||||
}
|
||||
|
||||
class FakeResponse:
|
||||
@@ -526,21 +518,20 @@ class TestXueqiuChannel:
|
||||
monkeypatch.setattr(xueqiu_mod, "_cookies_initialized", True)
|
||||
|
||||
fake_data = {
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"original_status": {
|
||||
"id": i,
|
||||
"title": f"Post {i}",
|
||||
"text": f"Content {i}",
|
||||
"user": {"screen_name": f"User {i}"},
|
||||
"like_count": i,
|
||||
"target": f"/user/{i}",
|
||||
}
|
||||
}
|
||||
for i in range(10)
|
||||
]
|
||||
}
|
||||
"list": [
|
||||
{
|
||||
"data": json.dumps({
|
||||
"id": i,
|
||||
"title": f"Post {i}",
|
||||
"text": f"Content {i}",
|
||||
"user": {"screen_name": f"User {i}"},
|
||||
"like_count": i,
|
||||
"target": f"/user/{i}",
|
||||
}),
|
||||
"original_status": None,
|
||||
}
|
||||
for i in range(10)
|
||||
]
|
||||
}
|
||||
|
||||
class FakeResponse:
|
||||
@@ -594,6 +585,68 @@ class TestXueqiuChannel:
|
||||
assert stocks[1]["percent"] == -0.8
|
||||
assert stocks[2]["rank"] == 3
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Cookie loading
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_ensure_cookies_loads_from_config(self, monkeypatch, tmp_path):
|
||||
"""_ensure_cookies() should inject cookies from the config file."""
|
||||
import agent_reach.channels.xueqiu as xueqiu_mod
|
||||
|
||||
monkeypatch.setattr(xueqiu_mod, "_cookies_initialized", False)
|
||||
|
||||
# Provide a fake Config that returns a cookie string with xq_a_token
|
||||
class FakeConfig:
|
||||
def get(self, key, default=None):
|
||||
if key == "xueqiu_cookie":
|
||||
return "xq_a_token=TESTTOKEN; xq_is_login=1"
|
||||
return default
|
||||
|
||||
import agent_reach.channels.xueqiu as xq_mod
|
||||
monkeypatch.setattr(
|
||||
xq_mod,
|
||||
"_load_cookies_from_config",
|
||||
lambda: (xq_mod._inject_cookie_string("xq_a_token=TESTTOKEN; xq_is_login=1") or True),
|
||||
)
|
||||
monkeypatch.setattr(xq_mod, "_load_cookies_from_browser", lambda: False)
|
||||
|
||||
# Patch opener so no real HTTP call is made
|
||||
class FakeResp:
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *_): pass
|
||||
def read(self): return b'{"data":{"items":[]}}'
|
||||
|
||||
monkeypatch.setattr(xq_mod._opener, "open", lambda req, timeout=None: FakeResp())
|
||||
|
||||
xq_mod._ensure_cookies()
|
||||
assert xq_mod._cookies_initialized is True
|
||||
cookie_names = {c.name for c in xq_mod._cookie_jar}
|
||||
assert "xq_a_token" in cookie_names
|
||||
|
||||
def test_get_json_sends_referer_and_browser_ua(self, monkeypatch):
|
||||
"""_get_json() must send Referer and a browser-like User-Agent."""
|
||||
import agent_reach.channels.xueqiu as xueqiu_mod
|
||||
|
||||
monkeypatch.setattr(xueqiu_mod, "_cookies_initialized", True)
|
||||
captured = {}
|
||||
|
||||
class FakeResp:
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *_): pass
|
||||
def read(self): return b'{"data":{"items":[]}}'
|
||||
|
||||
def fake_open(req, timeout=None):
|
||||
captured["ua"] = req.get_header("User-agent")
|
||||
captured["referer"] = req.get_header("Referer")
|
||||
return FakeResp()
|
||||
|
||||
monkeypatch.setattr(xueqiu_mod._opener, "open", fake_open)
|
||||
xueqiu_mod._get_json("https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=SH000001")
|
||||
|
||||
assert captured["referer"] == "https://xueqiu.com/"
|
||||
assert "Mozilla" in captured["ua"]
|
||||
assert "agent-reach" not in captured["ua"]
|
||||
|
||||
|
||||
class TestXiaoHongShuChannel:
|
||||
def test_reports_ok_when_server_health_is_ok(self, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user