This commit is contained in:
Your Name
2026-09-01 15:31:05 +08:00
parent 1f3addcf79
commit 2fc864cc00
20 changed files with 1381 additions and 45 deletions
+50 -5
View File
@@ -14,7 +14,12 @@ from rpa_engine.egress_channels import (
resolve_fixed_channel,
resolve_send_channels,
)
from .conv_util import build_conversation_id, normalize_conversation_id, resolve_peer_uid
from .conv_util import (
build_conversation_id,
conversation_belongs_to,
normalize_conversation_id,
resolve_peer_uid,
)
from .message_content import format_im_message, serialize_message_content
from .peer_profile import enrich_conversation_item, fetch_peer_profile, is_generic_peer_name
from .protocol import normalize_im_payload_from_bytes, _pick_avatar_url
@@ -1144,8 +1149,17 @@ class DouyinImHttpClient:
self.session.my_uid,
uid,
)
previous = int(self.session.my_uid or 0)
self.session.my_uid = uid
self.session.uid_verified = True
# 托管注册表按 UID 记录「本系统正在托管谁」。纠正后必须迁移,否则回环
# 防护会认错人:旧 UID 永远留在表里,真实 UID 从未登记。只迁移确实已登记
# 的托管身份,避免 API 侧的临时客户端把自己也登记进去。
from . import hosted_registry
if hosted_registry.is_hosted(previous):
hosted_registry.unregister(previous)
hosted_registry.register(uid)
async def get_conversations(
self,
@@ -1383,6 +1397,26 @@ class DouyinImHttpClient:
self._set_error("无法获取当前账号 UID")
self._log_send_failure(conversation_id, "无法获取当前账号 UID(Cookie 可能已失效)")
return False
# 跨账号写入闸门:normalize_conversation_id 会把任何会话 ID 改写成
# 0:1:{本账号}:{末段 UID},所以一条属于别的账号的会话流到这里会被
# 静默改写并发给对方的好友。发送前先确认本账号确实是该会话的参与方。
if not conversation_belongs_to(conversation_id, my_uid):
detail = (
f"会话 {conversation_id} 的参与方都不是本账号(uid={my_uid}),"
"拒绝发送:这条会话属于另一个账号,继续发送会把消息发给别人的好友。"
)
self._set_error(detail)
self.last_send_channel_retryable = False
self._log_send_failure(conversation_id, detail)
logger.error(
"Account %s refused cross-account send to %s (my_uid=%s)",
self.account_id,
conversation_id,
my_uid,
)
return False
if not auth.is_sign_ready():
self._set_error("缺少 IM 签名密钥,请用浏览器登录补全 localStorage")
self._log_send_failure(
@@ -1510,10 +1544,14 @@ class DouyinImHttpClient:
decision = str(result.get("decision") or "").strip().upper()
if decision == "KICK":
self.last_send_channel_retryable = True
# KICK is a terminal, account-session decision. Retrying the
# same authenticated write from another source address cannot
# repair the session and only adds another high-risk request.
self.last_send_needs_refresh = False
self.last_send_channel_retryable = False
detail = (
"抖音安全网关返回 decision=KICK,当前登录/安全会话已被服务端踢下线;"
"系统正在自动重登录,请留意账号卡片上的二维码并扫码"
"已停止本次发送及公网通道重试,系统正在自动重登录,请留意账号卡片上的二维码并扫码"
)
elif decision:
detail = f"抖音安全网关拒绝发送 decision={decision}"
@@ -1527,7 +1565,11 @@ class DouyinImHttpClient:
hint = _BUSINESS_REJECT_FALLBACK
# 7911 属于“签名凭证失效/安全校验未过”,标记为可刷新后重试
self.last_send_needs_refresh = status_code in _CREDENTIAL_EXPIRED_CODES
self.last_send_channel_retryable = self.last_send_needs_refresh
# 7911 is a credential/signature problem. It may be retried
# once only after refreshing the credentials on the same
# session; switching egress mid-session makes the fingerprint
# less consistent and must not be used as the recovery path.
self.last_send_channel_retryable = False
detail = f"抖音拒绝投递 status_code={status_code}"
if status_reason:
detail += f";抖音提示:{status_reason}"
@@ -1550,7 +1592,10 @@ class DouyinImHttpClient:
detail = "".join(reason_bits) or "接口返回但未确认投递(无 server_message_id"
if "INVALID_REQUEST" in detail.upper():
self.last_send_channel_retryable = True
# INVALID_REQUEST is a protocol/session rejection, not a
# transport failure. A second public IP sends the same invalid
# request and can invalidate an otherwise recoverable login.
self.last_send_channel_retryable = False
full_detail = f"{detail}{target}resp[{result.get('summary')}]"
if self.last_request_debug:
full_detail += f"\n--- 请求详情 ---\n{self.last_request_debug}"