mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
修复 QQbot渠道依旧会重复发送消息问题
This commit is contained in:
@@ -19,6 +19,7 @@ class QQBotModule(_ModuleBase, _MessageBase[QQBot]):
|
|||||||
"""QQ Bot 通知模块"""
|
"""QQ Bot 通知模块"""
|
||||||
|
|
||||||
def init_module(self) -> None:
|
def init_module(self) -> None:
|
||||||
|
self.stop()
|
||||||
super().init_service(service_name=QQBot.__name__.lower(), service_type=QQBot)
|
super().init_service(service_name=QQBot.__name__.lower(), service_type=QQBot)
|
||||||
self._channel = MessageChannel.QQ
|
self._channel = MessageChannel.QQ
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ QQ Bot Gateway WebSocket 客户端
|
|||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Optional
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ def run_gateway(
|
|||||||
get_gateway_url_fn: Callable[[str], str],
|
get_gateway_url_fn: Callable[[str], str],
|
||||||
on_message_fn: Callable[[dict], None],
|
on_message_fn: Callable[[dict], None],
|
||||||
stop_event: threading.Event,
|
stop_event: threading.Event,
|
||||||
|
ws_holder: List,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
在后台线程中运行 Gateway WebSocket 连接
|
在后台线程中运行 Gateway WebSocket 连接
|
||||||
@@ -34,20 +35,20 @@ def run_gateway(
|
|||||||
:param get_gateway_url_fn: 获取 gateway URL 的函数 (token) -> url
|
:param get_gateway_url_fn: 获取 gateway URL 的函数 (token) -> url
|
||||||
:param on_message_fn: 收到消息时的回调 (payload_dict) -> None
|
:param on_message_fn: 收到消息时的回调 (payload_dict) -> None
|
||||||
:param stop_event: 停止事件,set 时退出循环
|
:param stop_event: 停止事件,set 时退出循环
|
||||||
|
:param ws_holder: 调用方持有的单元素列表,存放当前 WebSocketApp,供 stop() 时 close 以打断 run_forever
|
||||||
"""
|
"""
|
||||||
last_seq: Optional[int] = None
|
last_seq: Optional[int] = None
|
||||||
heartbeat_interval_ms: Optional[int] = None
|
heartbeat_interval_ms: Optional[int] = None
|
||||||
heartbeat_timer: Optional[threading.Timer] = None
|
heartbeat_timer: Optional[threading.Timer] = None
|
||||||
ws_ref: list = [] # 用于在闭包中保持 ws 引用
|
|
||||||
|
|
||||||
def send_heartbeat():
|
def send_heartbeat():
|
||||||
nonlocal heartbeat_timer
|
nonlocal heartbeat_timer
|
||||||
if stop_event.is_set():
|
if stop_event.is_set():
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
if ws_ref and ws_ref[0]:
|
if ws_holder and ws_holder[0]:
|
||||||
payload = {"op": 1, "d": last_seq}
|
payload = {"op": 1, "d": last_seq}
|
||||||
ws_ref[0].send(json.dumps(payload))
|
ws_holder[0].send(json.dumps(payload))
|
||||||
logger.debug(f"[QQ Gateway:{config_name}] Heartbeat sent, seq={last_seq}")
|
logger.debug(f"[QQ Gateway:{config_name}] Heartbeat sent, seq={last_seq}")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.debug(f"[QQ Gateway:{config_name}] Heartbeat error: {err}")
|
logger.debug(f"[QQ Gateway:{config_name}] Heartbeat error: {err}")
|
||||||
@@ -87,7 +88,7 @@ def run_gateway(
|
|||||||
"shard": [0, 1],
|
"shard": [0, 1],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ws_ref[0].send(json.dumps(identify))
|
ws_holder[0].send(json.dumps(identify))
|
||||||
logger.info(f"[QQ Gateway:{config_name}] Identify sent")
|
logger.info(f"[QQ Gateway:{config_name}] Identify sent")
|
||||||
|
|
||||||
# 启动心跳
|
# 启动心跳
|
||||||
@@ -139,8 +140,8 @@ def run_gateway(
|
|||||||
|
|
||||||
elif op == 9: # Invalid Session
|
elif op == 9: # Invalid Session
|
||||||
logger.warning(f"[QQ Gateway:{config_name}] Invalid session")
|
logger.warning(f"[QQ Gateway:{config_name}] Invalid session")
|
||||||
if ws_ref and ws_ref[0]:
|
if ws_holder and ws_holder[0]:
|
||||||
ws_ref[0].close()
|
ws_holder[0].close()
|
||||||
|
|
||||||
def on_ws_error(_, error):
|
def on_ws_error(_, error):
|
||||||
logger.error(f"[QQ Gateway:{config_name}] WebSocket error: {error}")
|
logger.error(f"[QQ Gateway:{config_name}] WebSocket error: {error}")
|
||||||
@@ -149,6 +150,7 @@ def run_gateway(
|
|||||||
logger.info(f"[QQ Gateway:{config_name}] WebSocket closed: {close_status_code} {close_msg}")
|
logger.info(f"[QQ Gateway:{config_name}] WebSocket closed: {close_status_code} {close_msg}")
|
||||||
if heartbeat_timer:
|
if heartbeat_timer:
|
||||||
heartbeat_timer.cancel()
|
heartbeat_timer.cancel()
|
||||||
|
ws_holder.clear()
|
||||||
|
|
||||||
reconnect_delays = [1, 2, 5, 10, 30, 60]
|
reconnect_delays = [1, 2, 5, 10, 30, 60]
|
||||||
attempt = 0
|
attempt = 0
|
||||||
@@ -165,8 +167,8 @@ def run_gateway(
|
|||||||
on_error=on_ws_error,
|
on_error=on_ws_error,
|
||||||
on_close=on_ws_close,
|
on_close=on_ws_close,
|
||||||
)
|
)
|
||||||
ws_ref.clear()
|
ws_holder.clear()
|
||||||
ws_ref.append(ws)
|
ws_holder.append(ws)
|
||||||
|
|
||||||
# run_forever 会阻塞,需要传入 stop_event 的检查
|
# run_forever 会阻塞,需要传入 stop_event 的检查
|
||||||
# websocket-client 的 run_forever 支持 ping_interval, ping_timeout
|
# websocket-client 的 run_forever 支持 ping_interval, ping_timeout
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ class QQBot:
|
|||||||
:param QQ_GROUP_OPENID: 默认群组 openid(群聊,与 QQ_OPENID 二选一)
|
:param QQ_GROUP_OPENID: 默认群组 openid(群聊,与 QQ_OPENID 二选一)
|
||||||
:param name: 配置名称,用于消息来源标识和 Gateway 接收
|
:param name: 配置名称,用于消息来源标识和 Gateway 接收
|
||||||
"""
|
"""
|
||||||
|
self._gateway_stop = None
|
||||||
|
self._gateway_thread = None
|
||||||
|
self._gateway_ws_holder: list = []
|
||||||
if not QQ_APP_ID or not QQ_APP_SECRET:
|
if not QQ_APP_ID or not QQ_APP_SECRET:
|
||||||
logger.error("QQ Bot 配置不完整:缺少 AppID 或 AppSecret")
|
logger.error("QQ Bot 配置不完整:缺少 AppID 或 AppSecret")
|
||||||
self._ready = False
|
self._ready = False
|
||||||
@@ -151,6 +154,7 @@ class QQBot:
|
|||||||
"get_gateway_url_fn": get_gateway_url,
|
"get_gateway_url_fn": get_gateway_url,
|
||||||
"on_message_fn": self._on_gateway_message,
|
"on_message_fn": self._on_gateway_message,
|
||||||
"stop_event": self._gateway_stop,
|
"stop_event": self._gateway_stop,
|
||||||
|
"ws_holder": self._gateway_ws_holder,
|
||||||
},
|
},
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
@@ -161,10 +165,19 @@ class QQBot:
|
|||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""停止 Gateway 连接"""
|
"""停止 Gateway 连接"""
|
||||||
if self._gateway_stop:
|
if self._gateway_stop is not None:
|
||||||
self._gateway_stop.set()
|
self._gateway_stop.set()
|
||||||
if self._gateway_thread and self._gateway_thread.is_alive():
|
try:
|
||||||
self._gateway_thread.join(timeout=5)
|
if self._gateway_ws_holder:
|
||||||
|
self._gateway_ws_holder[0].close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"QQ Bot Gateway WebSocket close: {e}")
|
||||||
|
if self._gateway_thread is not None and self._gateway_thread.is_alive():
|
||||||
|
self._gateway_thread.join(timeout=20)
|
||||||
|
if self._gateway_thread.is_alive():
|
||||||
|
logger.warning(
|
||||||
|
"QQ Bot Gateway 线程在 stop 后仍未退出,可能存在重复收消息,请重启进程"
|
||||||
|
)
|
||||||
|
|
||||||
def get_state(self) -> bool:
|
def get_state(self) -> bool:
|
||||||
"""获取就绪状态"""
|
"""获取就绪状态"""
|
||||||
|
|||||||
Reference in New Issue
Block a user