This commit is contained in:
Your Name
2026-08-27 18:32:03 +08:00
parent 4ac6990efe
commit 1f3addcf79
50 changed files with 9145 additions and 1760 deletions
+62 -5
View File
@@ -320,6 +320,7 @@ class DouyinImService:
reply_cooldown_seconds: Optional[int] = None,
cooldown_resolver: Optional[Callable[[], Awaitable[int]]] = None,
refresh_credentials: Optional[Callable[[], Awaitable[bool]]] = None,
send_fallback: Optional[Callable[[str, str], Awaitable[tuple[bool, str]]]] = None,
follow_tick: Optional[Callable[[], Awaitable[None]]] = None,
on_session_invalid: Optional[Callable[[str], Awaitable[None]]] = None,
on_ready: Optional[ReadyFn] = None,
@@ -354,6 +355,11 @@ class DouyinImService:
self._cooldown_resolver = cooldown_resolver
# 由 worker 注入:触发后台重新采集 web_protect/keys(刷新 ts_sign),返回是否刷新成功
self.refresh_credentials = refresh_credentials
# 由 worker 注入的第二套发送方案:当 HTTP 签名发送被安全网关拒绝
# decision=KICK / 7911 / INVALID_REQUEST)时,用浏览器页面上下文
# 重新发送(真实 JS 生成 a_bogus/bd-ticket-guard,可自愈被踢的会话)。
# 签名: async (conversation_id, content) -> (ok, detail)
self.send_fallback = send_fallback
self._running = False
self._replied_keys: set[str] = set()
self._logged_keys: set[str] = set()
@@ -363,6 +369,9 @@ class DouyinImService:
self._conv_previews: dict[str, str] = {}
self._conv_names: dict[str, str] = {} # uid/conv_id -> nickname
self._conv_meta: dict[str, dict] = {} # conversation_id -> meta
# 抖音判定会话列表请求本身不合法时置位:这轮托管不再重复轮询该接口,
# 实时长连接成为唯一接收通道(已在系统日志里说明)。
self._conversation_list_unsupported = False
self._ws_client: Optional[DouyinImWsClient] = None
self.last_error: str = ""
@@ -1037,6 +1046,11 @@ class DouyinImService:
initial: bool = False,
defer_handlers: bool = False,
) -> list[dict]:
if self._conversation_list_unsupported:
# 抖音已明确拒绝过这个请求本身;重复调用只会每轮浪费一次请求,
# 并把同一条错误反复写进日志。原因已在首次拒绝时记录。
return []
controller = get_traffic_controller()
async with controller.background_slot(
self.account_id,
@@ -1053,6 +1067,14 @@ class DouyinImService:
account_id=self.account_id,
) as http:
conversations = await http.get_conversations(enrich_profiles=False)
if http.conversation_list_unsupported:
self._conversation_list_unsupported = True
logger.warning(
"Account %s disabled conversation reconciliation; "
"the realtime WebSocket is now the only receive path",
self.account_id,
)
return []
# Capture the previous preview before _index_conversations overwrites
# _conv_meta. A conversation-list preview is not inherently a new
@@ -1445,6 +1467,40 @@ class DouyinImService:
if refreshed:
continue
break
# 第二套发送方案(浏览器页面内发送):
# HTTP 签名发送被安全网关拒绝(KICK/7911/INVALID_REQUEST)时,交给 worker
# 用浏览器页面上下文重发——由抖音页面自带的 security-sdk 在真实环境生成
# a_bogus/bd-ticket-guard,绕开我们 Node execjs 的签名模拟,可自愈被踢会话。
upper_err = (self.last_error or "").upper()
if self.send_fallback and (
"DECISION=KICK" in upper_err
or "STATUS_CODE=7911" in upper_err
or "INVALID_REQUEST" in upper_err
):
try:
fb_ok, fb_detail = await self.send_fallback(conversation_id, content)
except Exception as exc:
logger.warning(f"send_fallback raised for {conversation_id}: {exc}")
fb_ok, fb_detail = False, f"浏览器兜底发送异常:{exc}"
if fb_ok:
self._session_invalid_strikes = 0
self._session_invalid_fired = False # 兜底成功说明登录仍有效,撤销自动下线
system_logger.record(
"浏览器兜底发送成功",
detail=f"会话 {conversation_id}{fb_detail}",
level="success",
category="send",
account_id=self.account_id,
)
return True, None
system_logger.record(
"浏览器兜底发送失败",
detail=f"会话 {conversation_id}{fb_detail}",
level="error",
category="send",
account_id=self.account_id,
)
await self._note_session_invalid(self.last_error)
return False, None
@@ -1479,7 +1535,7 @@ class DouyinImService:
system_logger.record(
"IM 登录失效,自动下线",
detail=f"{reason}{failure_detail})。"
"请停止托管后用浏览器模式重新登录并打开私信页,再重新启动托管",
"系统正在自动重登录,请留意账号卡片上的登录二维码并扫码",
level="error",
category="auth",
account_id=self.account_id,
@@ -1495,17 +1551,18 @@ class DouyinImService:
"""手动发送私信"""
from .conv_util import normalize_conversation_id
from .auth import DouyinAuth
from .dy_util import DEFAULT_USER_AGENT
auth = DouyinAuth()
auth.perepare_auth(
self.session.cookie_header(),
self.session.web_protect_str,
self.session.keys_str,
user_agent=self.session.user_agent or DEFAULT_USER_AGENT,
)
if getattr(self.session, "uid_verified", False) and self.session.my_uid:
my_uid = self.session.my_uid
else:
my_uid = await asyncio.to_thread(lambda: auth.get_uid()) or self.session.my_uid
my_uid = self.session.my_uid
if not my_uid:
my_uid = await asyncio.to_thread(lambda: auth.get_uid()) or 0
if my_uid:
conversation_id = normalize_conversation_id(conversation_id, my_uid)