diff --git a/tests/test_huya_elite_query.py b/tests/test_huya_elite_query.py
index 70a1372..6680e0f 100644
--- a/tests/test_huya_elite_query.py
+++ b/tests/test_huya_elite_query.py
@@ -514,9 +514,63 @@ def test_query_all_fails_when_points_center_unreachable(monkeypatch):
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
)
- status, message, _result = marks[-1]
+ status, message, result = marks[-1]
assert status == "failed"
assert "积分中心接口无响应" in message
+ # 两条通道都没回包:CK 失效标记跟着失败结果一起落库(前端整行标红)
+ assert result["ck_expired"] is True
+ assert account.status == "ck_expired"
+
+
+def test_query_all_flags_ck_expired_when_role_lookup_has_no_response(monkeypatch):
+ """角色接口整包没有回包 = 这个 CK 已经不认了(真机:外部导入的过期 CK 就这表现)。
+
+ 真机日志(logs/app-2026-10-15.log,hy_297838517)里
+ ``webActUI.checkUserBindGameAccount`` 的响应只有 ``newdata keys: ['']``(没有 tRsp),
+ ``_resolve_bind_status`` 因此返回 None;积分中心走另一条通道却还能返回 200,
+ 所以不能凭「积分查到了」就当成功,必须打 ck_expired 让页面标红。
+ """
+ shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
+ runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
+ monkeypatch.setattr(
+ runner, "_resolve_bind_status", lambda **kwargs: (None, {}), raising=False
+ )
+ account = HuyaAccount(uid=str(UID), cookie=COOKIE)
+
+ runner._execute_query_all(
+ None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
+ )
+
+ status, message, result = marks[-1]
+ # 宝典/积分查到了,任务本身不算失败(结果照样展示),但 CK 失效标记必须带上
+ assert status == "success"
+ assert result["ck_expired"] is True
+ assert result["ck_expired_reason"] == "角色信息接口无响应"
+ assert account.status == "ck_expired"
+ assert "角色信息接口无响应" in message
+ assert "疑似 CK 已失效" in message
+
+
+def test_query_all_keeps_business_error_out_of_ck_expired(monkeypatch):
+ """接口有回包、只是业务码非 200(例如活动已下线)不算 CK 失效,避免整批误标红。"""
+ shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
+ runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
+ monkeypatch.setattr(
+ runner,
+ "_resolve_bind_status",
+ lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}),
+ raising=False,
+ )
+ account = HuyaAccount(uid=str(UID), cookie=COOKIE)
+
+ runner._execute_query_all(
+ None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
+ )
+
+ _status, message, result = marks[-1]
+ assert result.get("ck_expired") is None
+ assert account.status == "all_queried"
+ assert "活动已下线" in message
def test_shop_client_falls_back_to_http_when_wss_init_fails(monkeypatch):
diff --git a/web/backend/services/huya_runner_elite.py b/web/backend/services/huya_runner_elite.py
index cc66c52..e9bb88b 100644
--- a/web/backend/services/huya_runner_elite.py
+++ b/web/backend/services/huya_runner_elite.py
@@ -10,6 +10,13 @@
* 开通时间 —— ``revenueWebUI.queryUserOrderList`` 里已支付的宝典订单,取最近一次支付时间。
* 有效期 —— 商城订单(orderDetailV5)与积分中心都**不返回**到期时间,所以留空,
界面上「有就显示」。
+* CK 失效 —— 「一键查询」里角色走活动通道(``webActUI.checkUserBindGameAccount``),
+ 真机(2026-10-15,外部导入的过期 CK)表现为**整个请求没有 tRsp 回包**
+ (日志里 ``响应 newdata keys: ['']``),于是 ``_resolve_bind_status`` 返回 None。
+ 这种情况按「CK 已失效」处理:任务 result 打 ``ck_expired`` 标记、账号状态置
+ ``ck_expired``,前端把该行标红提示重新登录/重新导入 Cookie。
+ 积分中心走另一条通道(baseinfo 里的 cookie 校验更松),过期 CK 仍可能返回 200,
+ 所以不能拿积分能不能查来判 CK 有效性。
"""
from __future__ import annotations
@@ -335,6 +342,9 @@ class EliteMixin:
self.payload.get("bind_act_id") or config_info.get("bind_act_id")
)
role_name = ""
+ # 角色查不到 = 这个 CK 已经不认了(模块头里有真机依据),整行要标红提醒
+ ck_expired = False
+ ck_expired_reason = ""
if not b_act_id_int:
failures.append("未配置绑定 bActId")
else:
@@ -346,7 +356,9 @@ class EliteMixin:
b_act_id_int=b_act_id_int,
)
if bind_status is None:
- failures.append("角色信息接口无响应")
+ ck_expired = True
+ ck_expired_reason = "角色信息接口无响应"
+ failures.append(ck_expired_reason)
elif bind_status.status != 200:
failures.append(
bind_status.msg or f"角色信息查询失败: {bind_status.status}"
@@ -372,9 +384,14 @@ class EliteMixin:
message_parts.append("未绑定角色")
account.points = points if points is not None else account.points
- account.status = "all_queried"
+ # CK 失效时账号状态单独标出来(前端「状态」列据此显示红色「CK 失效」)
+ account.status = "ck_expired" if ck_expired else "all_queried"
account.updated_at = datetime.now(UTC)
result["failures"] = failures
+ if ck_expired:
+ result["ck_expired"] = True
+ result["ck_expired_reason"] = ck_expired_reason
+ ck_hint = ";疑似 CK 已失效,请重新登录或重新导入 Cookie" if ck_expired else ""
fetched_anything = points is not None or role_name or opened is not None
if not fetched_anything:
@@ -382,7 +399,7 @@ class EliteMixin:
worker_db,
task,
"failed",
- ";".join(failures) or "一键查询全部失败",
+ (";".join(failures) or "一键查询全部失败") + ck_hint,
result,
)
return
@@ -390,4 +407,4 @@ class EliteMixin:
message = "|".join(message_parts) or "一键查询完成"
if failures:
message += f";部分失败: {';'.join(failures)}"
- self._mark_task(worker_db, task, "success", message, result)
+ self._mark_task(worker_db, task, "success", message + ck_hint, result)
diff --git a/web/frontend/src/pages/HuyaElitePage.tsx b/web/frontend/src/pages/HuyaElitePage.tsx
index 25c8179..0128c5c 100644
--- a/web/frontend/src/pages/HuyaElitePage.tsx
+++ b/web/frontend/src/pages/HuyaElitePage.tsx
@@ -101,6 +101,16 @@ function isLoopbackUrl(url: string): boolean {
return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/i.test(url);
}
+/**
+ * CK 失效:一键查询里角色走活动通道,外部导入的 CK 过期后这个接口直接没有回包,
+ * 后端会打 `result.ck_expired` 标记并把账号状态置为 `ck_expired`(见
+ * backend/services/huya_runner_elite.py 模块头)。这种行必须红色提醒重新登录/重新导入,
+ * 不能因为积分、宝典查到了就显示绿色「成功」。
+ */
+function ckExpired(task: HuyaTaskItem | undefined, row: HuyaAccountItem): boolean {
+ return task?.result?.ck_expired === true || row.status === 'ck_expired';
+}
+
function latestTask(tasks: HuyaTaskItem[], accountId: number): HuyaTaskItem | undefined {
return tasks.filter((task) => task.account_id === accountId).sort((a, b) => b.id - a.id)[0];
}
@@ -329,12 +339,12 @@ export default function HuyaElitePage() {
{ title: '#', width: '4%', align: 'center', render: (_value, _row, index) =>