diff --git a/core/weibo/client.py b/core/weibo/client.py index d652736..77d90bf 100644 --- a/core/weibo/client.py +++ b/core/weibo/client.py @@ -15,6 +15,7 @@ import json import secrets import time from dataclasses import dataclass +from html import unescape from html.parser import HTMLParser from typing import Any from urllib.parse import parse_qs, urljoin, urlsplit @@ -321,6 +322,7 @@ class WeiboClient: data: dict[str, Any] | None = None, json: dict[str, Any] | None = None, ) -> dict[str, Any]: + response: requests.Response | None = None try: response = self.session.request( method, @@ -333,10 +335,18 @@ class WeiboClient: ) response.raise_for_status() payload = response.json() + except (requests.exceptions.JSONDecodeError, ValueError) as exc: + text = unescape(response.text if response is not None else "") + if "retcode=6102" in text or "/sso/login.php" in text: + raise WeiboAuthError( + "微博支付/活动域登录态已失效(retcode=6102)," + "请重新登录微博后重新导入 CK" + ) from exc + path = urlsplit(response.url).path if response is not None else "" + suffix = f": {path}" if path else "" + raise WeiboTransportError(f"微博接口返回非 JSON{suffix}") from exc except requests.RequestException as exc: raise WeiboTransportError(f"请求微博失败: {exc}") from exc - except ValueError as exc: - raise WeiboTransportError("微博返回内容不是合法 JSON") from exc if not isinstance(payload, dict): raise WeiboTransportError("微博返回 JSON 结构异常") return payload diff --git a/docs/微博/微博微豆-CK接口与兑换链路确认-2026-11-07.md b/docs/微博/微博微豆-CK接口与兑换链路确认-2026-11-07.md index 1764ae9..b47051e 100644 --- a/docs/微博/微博微豆-CK接口与兑换链路确认-2026-11-07.md +++ b/docs/微博/微博微豆-CK接口与兑换链路确认-2026-11-07.md @@ -8,6 +8,7 @@ generated: status: stable verified: - { by: ai/codex, at: 2026-11-07T19:03:41+08:00 } + - { by: ai/codex, at: 2026-11-08T13:45:00+08:00 } --- # 微博微豆 CK 接口与兑换链路确认 @@ -723,6 +724,37 @@ data.totalCount | `400000` | `user not login`,CK 无效 | | 其他 | 保留服务端 `msg` 和 `trace_id`,不要盲目重试状态变更接口 | +### 10.1 登录页 HTML 与 `retcode=6102` + +部分 V+ / 支付域接口在登录态失效时仍返回 HTTP 200,但 `Content-Type` 为 `text/html`,正文是“新浪通行证”跳转页,并包含: + +```text +retcode=6102 +``` + +2026-11-08 实测到同一账号可以正常请求: + +```text +GET https://vipclub.e.weibo.com/aj/mall/home +-> code=100000 +``` + +但后续请求: + +```text +GET https://vipclub.weibo.com/aj/vmember/gfsupport +GET https://vipclub.weibo.com/aj/paycomponent/userinfo +-> text/html + retcode=6102 +``` + +这说明 CK 已经不能访问 V+ 支付/活动域,不能归类为普通“非 JSON 查询失败”。处理约定: + +- `retcode=6102` 或跳转 `/sso/login.php` 归类为 `WeiboAuthError`。 +- 账号 `ck_check_status` 写为 `invalid`,UI 显示「失效」。 +- 提示用户重新登录微博,并在 V+ 页面可用后重新导出、导入完整 CK。 +- 不要通过反复刷新、重试支付接口来“恢复”状态。 +- 其他未知 HTML 或网关错误保留为 `error`,UI 显示「查询失败」,不要误标成 CK 失效。 + ## 11. 尚未确认 - 是否只需要 `SUB`,还是必须完整 Cookie Jar。 diff --git a/tests/test_weibo_accounts.py b/tests/test_weibo_accounts.py index 05b2c92..0f8a191 100644 --- a/tests/test_weibo_accounts.py +++ b/tests/test_weibo_accounts.py @@ -205,6 +205,22 @@ def test_refresh_marks_payment_access_failure(db, current_user): assert "触发微博账号风控" in account.ck_check_message +def test_refresh_marks_payment_login_expiry_as_invalid(db, current_user): + account_out = import_cookies( + WeiboCookieImport(text="SCF=a; SUB=b", tag=""), + db=db, + current=current_user, + ).accounts[0] + account = db.query(WeiboAccount).filter(WeiboAccount.id == account_out.id).one() + StubWeiboClient.payment_error = WeiboAuthError("支付域 retcode=6102") + + with pytest.raises(WeiboAuthError, match="6102"): + weibo_service.refresh_weibo_account(db, account) + + assert account.ck_check_status == "invalid" + assert "6102" in account.ck_check_message + + def test_list_accounts_returns_parallel_page_shape(db, current_user): assert ( import_cookies( diff --git a/tests/test_weibo_client.py b/tests/test_weibo_client.py index 3ce5ed5..5392d8b 100644 --- a/tests/test_weibo_client.py +++ b/tests/test_weibo_client.py @@ -17,6 +17,7 @@ from core.weibo import ( WeiboExchangeUncertainError, WeiboHomeSnapshot, WeiboRechargeError, + WeiboTransportError, extract_bind_raw_url, normalize_weibo_cookie, ) @@ -50,7 +51,7 @@ class FakeResponse: def json(self): if not isinstance(self.payload, dict): - raise TypeError("not json") + raise requests.exceptions.JSONDecodeError("not json", self.text, 0) return self.payload @@ -155,6 +156,30 @@ def test_fetch_home_keeps_trace_id_for_other_api_error(): client.fetch_home() +def test_fetch_home_treats_sina_login_redirect_as_auth_error(): + session = FakeSession( + [ + ( + "" + ) + ] + ) + client = WeiboClient("SUB=expired", session=session) + + with pytest.raises(WeiboAuthError, match="重新登录微博"): + client.fetch_home() + + +def test_fetch_home_reports_non_json_endpoint_path(): + session = FakeSession(["gateway error"]) + client = WeiboClient("SUB=test", session=session) + + with pytest.raises(WeiboTransportError, match="/aj/mall/home"): + client.fetch_home() + + def test_generate_bind_scheme_posts_account_type_without_weibo_cookie(): session = FakeSession( [