mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
feat: enhance iLink polling logic to support multiple payload formats and improve success determination
This commit is contained in:
@@ -1238,23 +1238,32 @@ class ILinkClient:
|
|||||||
def _extract_updates(
|
def _extract_updates(
|
||||||
self, payload: Dict[str, Any]
|
self, payload: Dict[str, Any]
|
||||||
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
|
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
|
||||||
"""按官方 getupdates 协议提取顶层 msgs 与 get_updates_buf。"""
|
"""
|
||||||
sync_buf = self._pick_present_value(payload, ["get_updates_buf"])
|
提取轮询结果中的消息列表与游标。
|
||||||
|
|
||||||
|
线上存在两种等价字段命名:较新的实现返回 `get_updates_buf`,
|
||||||
|
部分实例仍然返回 `sync_buf`。两者都表示下一轮轮询应携带的游标。
|
||||||
|
"""
|
||||||
|
sync_buf = self._pick_present_value(
|
||||||
|
payload, ["get_updates_buf", "sync_buf", "syncBuf"]
|
||||||
|
)
|
||||||
items = payload.get("msgs")
|
items = payload.get("msgs")
|
||||||
if isinstance(items, list):
|
if isinstance(items, list):
|
||||||
return items, sync_buf
|
return items, sync_buf
|
||||||
return [], sync_buf
|
return [], sync_buf
|
||||||
|
|
||||||
def _has_canonical_poll_shape(self, payload: Dict[str, Any]) -> bool:
|
@staticmethod
|
||||||
|
def _has_canonical_poll_shape(payload: Dict[str, Any]) -> bool:
|
||||||
"""官方响应至少应包含顶层 msgs 列表。"""
|
"""官方响应至少应包含顶层 msgs 列表。"""
|
||||||
return isinstance(payload.get("msgs"), list)
|
return isinstance(payload.get("msgs"), list)
|
||||||
|
|
||||||
def _is_poll_success(self, payload: Dict[str, Any]) -> bool:
|
def _resolve_poll_success(self, payload: Dict[str, Any]) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
判断 getupdates 是否明确成功。
|
判断 getupdates 是否给出了明确的成功/失败信号。
|
||||||
|
|
||||||
轮询接口不能沿用“只要没有明显报错就算成功”的宽松策略,否则服务端返回旧消息列表、
|
轮询接口不能沿用“只要没有明显报错就算成功”的宽松策略,否则服务端返回旧消息列表、
|
||||||
但状态码其实失败时,会被误判为可消费响应,导致旧消息再次进入业务链路。
|
但状态码其实失败时,会被误判为可消费响应,导致旧消息再次进入业务链路。
|
||||||
|
返回 `None` 表示响应里没有显式状态,需要交给协议结构继续判断。
|
||||||
"""
|
"""
|
||||||
if not payload:
|
if not payload:
|
||||||
return False
|
return False
|
||||||
@@ -1286,7 +1295,7 @@ class ILinkClient:
|
|||||||
return True
|
return True
|
||||||
if lowered in {"failed", "fail", "error", "denied", "blocked"}:
|
if lowered in {"failed", "fail", "error", "denied", "blocked"}:
|
||||||
return False
|
return False
|
||||||
return False
|
return None
|
||||||
|
|
||||||
def _build_poll_result(
|
def _build_poll_result(
|
||||||
self,
|
self,
|
||||||
@@ -1320,16 +1329,22 @@ class ILinkClient:
|
|||||||
if not self.bot_token:
|
if not self.bot_token:
|
||||||
return [], self.sync_buf, {"success": False, "message": "bot token 未配置"}
|
return [], self.sync_buf, {"success": False, "message": "bot token 未配置"}
|
||||||
url = f"{self.base_url}/ilink/bot/getupdates"
|
url = f"{self.base_url}/ilink/bot/getupdates"
|
||||||
payload = {}
|
|
||||||
request_body = self._with_base_info({"get_updates_buf": self.sync_buf or ""})
|
request_body = self._with_base_info({"get_updates_buf": self.sync_buf or ""})
|
||||||
resp = RequestUtils(
|
resp = RequestUtils(
|
||||||
headers=self._headers(auth_required=True),
|
headers=self._headers(auth_required=True),
|
||||||
timeout=timeout_seconds + 10,
|
timeout=timeout_seconds + 10,
|
||||||
).post(url, json=request_body)
|
).post(url, json=request_body)
|
||||||
payload = self._json(resp)
|
payload = self._json(resp)
|
||||||
success = bool(payload and self._is_poll_success(payload))
|
explicit_success = self._resolve_poll_success(payload)
|
||||||
|
has_canonical_shape = self._has_canonical_poll_shape(payload)
|
||||||
|
# 某些 iLink 部署不会返回 ret/success,但顶层 msgs + sync_buf 已经足够表明
|
||||||
|
# 这是一次有效的轮询响应;只有出现显式失败信号时才应拒绝消费。
|
||||||
|
success = bool(payload) and (
|
||||||
|
explicit_success is True
|
||||||
|
or (explicit_success is None and has_canonical_shape)
|
||||||
|
)
|
||||||
last_message = None
|
last_message = None
|
||||||
if payload and not success:
|
if payload and explicit_success is False:
|
||||||
last_message = self._find_first_value(
|
last_message = self._find_first_value(
|
||||||
payload, ["errmsg", "message", "error", "error_msg", "detail"]
|
payload, ["errmsg", "message", "error", "error_msg", "detail"]
|
||||||
) or self._short_text(payload)
|
) or self._short_text(payload)
|
||||||
@@ -1344,7 +1359,7 @@ class ILinkClient:
|
|||||||
payload=payload,
|
payload=payload,
|
||||||
message=last_message or "轮询响应未明确成功",
|
message=last_message or "轮询响应未明确成功",
|
||||||
)
|
)
|
||||||
if not self._has_canonical_poll_shape(payload):
|
if not has_canonical_shape:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"getupdates 返回非官方结构,已拒绝消费: %s",
|
"getupdates 返回非官方结构,已拒绝消费: %s",
|
||||||
self._short_text(payload),
|
self._short_text(payload),
|
||||||
@@ -1592,7 +1607,8 @@ class WechatClawBot:
|
|||||||
return "image/gif"
|
return "image/gif"
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
|
|
||||||
def _load_remote_image(self, image: str) -> Optional[bytes]:
|
@staticmethod
|
||||||
|
def _load_remote_image(image: str) -> Optional[bytes]:
|
||||||
image_url = str(image or "").strip()
|
image_url = str(image or "").strip()
|
||||||
if not image_url:
|
if not image_url:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -108,6 +108,33 @@ class WechatClawBotTest(unittest.TestCase):
|
|||||||
self.assertEqual(client.sync_buf, "")
|
self.assertEqual(client.sync_buf, "")
|
||||||
self.assertEqual(len(messages), 1)
|
self.assertEqual(len(messages), 1)
|
||||||
|
|
||||||
|
def test_ilink_poll_updates_accepts_canonical_payload_without_explicit_success_flag(self):
|
||||||
|
client = ILinkClient(
|
||||||
|
base_url="https://ilinkai.weixin.qq.com",
|
||||||
|
bot_token="token",
|
||||||
|
sync_buf="cursor-old",
|
||||||
|
)
|
||||||
|
response = MagicMock()
|
||||||
|
response.json.return_value = {
|
||||||
|
"sync_buf": "cursor-new",
|
||||||
|
"msgs": [
|
||||||
|
{
|
||||||
|
"message_id": "msg-1002",
|
||||||
|
"from_user_id": "wxid_user_2",
|
||||||
|
"item_list": [{"type": 1, "text_item": {"text": "收到"}}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("app.modules.wechatclawbot.wechatclawbot.RequestUtils.post", return_value=response):
|
||||||
|
messages, sync_buf, result = client.poll_updates()
|
||||||
|
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertEqual(sync_buf, "cursor-new")
|
||||||
|
self.assertEqual(client.sync_buf, "cursor-new")
|
||||||
|
self.assertEqual(len(messages), 1)
|
||||||
|
self.assertEqual(messages[0].text, "收到")
|
||||||
|
|
||||||
def test_ilink_poll_updates_rejects_noncanonical_nested_success_payload(self):
|
def test_ilink_poll_updates_rejects_noncanonical_nested_success_payload(self):
|
||||||
client = ILinkClient(
|
client = ILinkClient(
|
||||||
base_url="https://ilinkai.weixin.qq.com",
|
base_url="https://ilinkai.weixin.qq.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user