更新
This commit is contained in:
@@ -103,6 +103,12 @@ _LOOP_STATES: "weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, _LoopWsState
|
||||
)
|
||||
|
||||
|
||||
# frontier 按 device_id 寻址推送:两个托管账号共用同一个设备号时,两条长连接会
|
||||
# 订阅到同一个地址并互相收到对方的私信。真正的拦截在 service 的会话归属校验里,
|
||||
# 这里只负责把「为什么会串号」明确告诉用户。持弱引用,账号停管后自动失效。
|
||||
_FRONTIER_DEVICE_OWNERS: "dict[str, weakref.ref[DouyinImWsClient]]" = {}
|
||||
|
||||
|
||||
def _get_loop_state() -> _LoopWsState:
|
||||
loop = asyncio.get_running_loop()
|
||||
state = _LOOP_STATES.get(loop)
|
||||
@@ -155,6 +161,8 @@ class DouyinImWsClient:
|
||||
self._dispatcher_task: Optional[asyncio.Task] = None
|
||||
self._received_frame_count = 0
|
||||
self._heartbeat_ack_logged = False
|
||||
self._frontier_device_id = ""
|
||||
self._blocked_device_owner_id: Optional[int] = None
|
||||
|
||||
async def start(self):
|
||||
if self._task and not self._task.done():
|
||||
@@ -210,6 +218,7 @@ class DouyinImWsClient:
|
||||
if self._task is task:
|
||||
self._task = None
|
||||
self._connection = None
|
||||
self._release_frontier_device()
|
||||
await self._stop_dispatcher()
|
||||
|
||||
def _record_connection_system_event(
|
||||
@@ -247,6 +256,7 @@ class DouyinImWsClient:
|
||||
account_key = int(self.account_id or 0)
|
||||
state.system_log_last_at.pop((account_key, "connected"), None)
|
||||
state.system_log_last_at.pop((account_key, "retry"), None)
|
||||
state.system_log_last_at.pop((account_key, "device_taken"), None)
|
||||
|
||||
def _ensure_dispatcher(self) -> None:
|
||||
if self._dispatcher_task and not self._dispatcher_task.done():
|
||||
@@ -311,8 +321,15 @@ class DouyinImWsClient:
|
||||
first_attempt = False
|
||||
if not connect_url:
|
||||
raise RuntimeError("frontier WebSocket URL is unavailable")
|
||||
logger.info("Connecting IM WebSocket: %s...", connect_url[:100])
|
||||
await self._run_connection(connect_url)
|
||||
if self._claim_frontier_device(connect_url):
|
||||
logger.info("Connecting IM WebSocket: %s...", connect_url[:100])
|
||||
await self._run_connection(connect_url)
|
||||
else:
|
||||
# 设备号已被另一个在跑的账号占用:绝不并连同一个推送地址,
|
||||
# 本账号本轮退回 HTTP 轮询兜底(connected 保持 False,
|
||||
# service 会自动切到更快的会话对账节奏),并在退避后重试,
|
||||
# 等占用方停管时自动接管。
|
||||
self._report_frontier_device_taken(connect_url)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as exc:
|
||||
@@ -348,6 +365,76 @@ class DouyinImWsClient:
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
self._release_frontier_device()
|
||||
|
||||
def _frontier_device_owner(self, device_id: str) -> "Optional[DouyinImWsClient]":
|
||||
"""当前仍活着的设备号占用方(run 循环任务还在跑才算数)。"""
|
||||
reference = _FRONTIER_DEVICE_OWNERS.get(device_id)
|
||||
owner = reference() if reference is not None else None
|
||||
if owner is None or owner is self:
|
||||
return None
|
||||
task = owner._task
|
||||
if not owner._running or task is None or task.done():
|
||||
return None
|
||||
return owner
|
||||
|
||||
def _claim_frontier_device(self, url: str) -> bool:
|
||||
"""独占本账号的 frontier 设备地址;已被别的账号占用时返回 False。
|
||||
|
||||
frontier 按 device_id 寻址推送。两个账号共用同一个设备号时,同时建连
|
||||
会让两条连接互相收到对方的私信(串号的根因),且抖音也可能只保留最后
|
||||
一条连接、把先连上的那个账号踢成「连着但收不到」。所以同一个设备地址
|
||||
永远只允许一个账号建连,另一个账号走 HTTP 轮询兜底。
|
||||
"""
|
||||
from .frontier import ws_device_id
|
||||
|
||||
device_id = ws_device_id(url)
|
||||
if not device_id:
|
||||
# 判不出设备号(自建地址/异常格式)时不阻断连接,交给会话归属校验兜底。
|
||||
return True
|
||||
owner = self._frontier_device_owner(device_id)
|
||||
if owner is not None and int(owner.account_id or 0) != int(self.account_id or 0):
|
||||
self._blocked_device_owner_id = owner.account_id
|
||||
return False
|
||||
_FRONTIER_DEVICE_OWNERS[device_id] = weakref.ref(self)
|
||||
self._frontier_device_id = device_id
|
||||
self._blocked_device_owner_id = None
|
||||
return True
|
||||
|
||||
def _report_frontier_device_taken(self, url: str) -> None:
|
||||
from .frontier import ws_device_id
|
||||
|
||||
device_id = ws_device_id(url)
|
||||
owner_id = self._blocked_device_owner_id
|
||||
logger.error(
|
||||
"Account %s cannot open frontier device_id %s: already held by "
|
||||
"account %s; falling back to HTTP polling this round",
|
||||
self.account_id,
|
||||
device_id,
|
||||
owner_id,
|
||||
)
|
||||
self._record_connection_system_event(
|
||||
"device_taken",
|
||||
"实时接收已让出:与另一个账号共用长连接设备号",
|
||||
detail=(
|
||||
f"本账号与账号 {owner_id} 的 frontier 设备号相同(device_id={device_id})。"
|
||||
"同一个设备地址只允许一个账号建立长连接,否则两个账号会互相收到对方的"
|
||||
"私信。本账号本轮不建连,改由 HTTP 会话轮询接收(有几十秒级延迟),"
|
||||
"并在对方停止托管后自动接管。"
|
||||
"根治办法:为每个账号在独立的浏览器配置/设备上重新采集凭证。"
|
||||
),
|
||||
level="error",
|
||||
)
|
||||
|
||||
def _release_frontier_device(self) -> None:
|
||||
device_id = self._frontier_device_id
|
||||
self._frontier_device_id = ""
|
||||
if not device_id:
|
||||
return
|
||||
reference = _FRONTIER_DEVICE_OWNERS.get(device_id)
|
||||
if reference is not None and reference() is self:
|
||||
_FRONTIER_DEVICE_OWNERS.pop(device_id, None)
|
||||
|
||||
def _connection_headers(self) -> list[tuple[str, str]]:
|
||||
headers = [
|
||||
("Pragma", "no-cache"),
|
||||
|
||||
Reference in New Issue
Block a user