feat(maizone): 建立多级回退机制以保障空间Cookie获取的健壮性
该提交旨在从根本上解决MaiZone插件因单一Cookie来源不稳定而导致功能中断的问题。通过引入一个具有优先级的多级回退策略,显著提高了服务的可用性和容错能力。 核心变更如下: - **引入健壮的获取策略**:重构了Cookie的获取逻辑,建立了一条清晰的降级路径。现在系统会优先通过最可靠的Napcat HTTP服务获取,若失败则尝试读取本地文件缓存,最后才调用可能超时的Adapter API。这确保了即使部分服务不可用,插件仍有很大概率正常工作。 - **优化插件生命周期**:修正了插件加载时的初始化流程,将服务注册和后台任务启动合并到单一的`on_plugin_loaded`方法中,消除了潜在的竞态条件,确保了监控和定时任务总能被正确启动。 - **提升操作容忍度**:将Napcat适配器中`get_cookies`动作的超时阈值放宽至40秒,为网络延迟或不稳定的情况提供了更充足的缓冲时间,减少了因过早超时而造成的失败。 - **细化过程日志**:在整个Cookie获取和QZone服务调用链中增加了详细的上下文日志,使得在出现问题时能够快速定位失败环节和具体原因,极大地简化了未来的故障排查工作。
This commit is contained in:
@@ -88,25 +88,27 @@ class MaiZoneRefactoredPlugin(BasePlugin):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
async def on_plugin_loaded(self):
|
async def on_plugin_loaded(self):
|
||||||
|
"""插件加载完成后的回调,初始化服务并启动后台任务"""
|
||||||
|
# --- 注册权限节点 ---
|
||||||
await permission_api.register_permission_node(
|
await permission_api.register_permission_node(
|
||||||
"plugin.maizone.send_feed", "是否可以使用机器人发送QQ空间说说", "maiZone", False
|
"plugin.maizone.send_feed", "是否可以使用机器人发送QQ空间说说", "maiZone", False
|
||||||
)
|
)
|
||||||
await permission_api.register_permission_node(
|
await permission_api.register_permission_node(
|
||||||
"plugin.maizone.read_feed", "是否可以使用机器人读取QQ空间说说", "maiZone", True
|
"plugin.maizone.read_feed", "是否可以使用机器人读取QQ空间说说", "maiZone", True
|
||||||
)
|
)
|
||||||
# 创建所有服务实例
|
|
||||||
|
# --- 创建并注册所有服务实例 ---
|
||||||
content_service = ContentService(self.get_config)
|
content_service = ContentService(self.get_config)
|
||||||
image_service = ImageService(self.get_config)
|
image_service = ImageService(self.get_config)
|
||||||
cookie_service = CookieService(self.get_config)
|
cookie_service = CookieService(self.get_config)
|
||||||
reply_tracker_service = ReplyTrackerService()
|
reply_tracker_service = ReplyTrackerService()
|
||||||
|
|
||||||
# 使用已创建的 reply_tracker_service 实例
|
|
||||||
qzone_service = QZoneService(
|
qzone_service = QZoneService(
|
||||||
self.get_config,
|
self.get_config,
|
||||||
content_service,
|
content_service,
|
||||||
image_service,
|
image_service,
|
||||||
cookie_service,
|
cookie_service,
|
||||||
reply_tracker_service, # 传入已创建的实例
|
reply_tracker_service,
|
||||||
)
|
)
|
||||||
scheduler_service = SchedulerService(self.get_config, qzone_service)
|
scheduler_service = SchedulerService(self.get_config, qzone_service)
|
||||||
monitor_service = MonitorService(self.get_config, qzone_service)
|
monitor_service = MonitorService(self.get_config, qzone_service)
|
||||||
@@ -115,18 +117,12 @@ class MaiZoneRefactoredPlugin(BasePlugin):
|
|||||||
register_service("reply_tracker", reply_tracker_service)
|
register_service("reply_tracker", reply_tracker_service)
|
||||||
register_service("get_config", self.get_config)
|
register_service("get_config", self.get_config)
|
||||||
|
|
||||||
# 保存服务引用以便后续启动
|
logger.info("MaiZone重构版插件服务已注册。")
|
||||||
self.scheduler_service = scheduler_service
|
|
||||||
self.monitor_service = monitor_service
|
|
||||||
|
|
||||||
logger.info("MaiZone重构版插件已加载,服务已注册。")
|
# --- 启动后台任务 ---
|
||||||
|
asyncio.create_task(scheduler_service.start())
|
||||||
async def on_plugin_loaded(self):
|
asyncio.create_task(monitor_service.start())
|
||||||
"""插件加载完成后的回调,启动异步服务"""
|
logger.info("MaiZone后台监控和定时任务已启动。")
|
||||||
if hasattr(self, "scheduler_service") and hasattr(self, "monitor_service"):
|
|
||||||
asyncio.create_task(self.scheduler_service.start())
|
|
||||||
asyncio.create_task(self.monitor_service.start())
|
|
||||||
logger.info("MaiZone后台任务已启动。")
|
|
||||||
|
|
||||||
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
|
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -113,31 +113,32 @@ class CookieService:
|
|||||||
async def get_cookies(self, qq_account: str, stream_id: Optional[str]) -> Optional[Dict[str, str]]:
|
async def get_cookies(self, qq_account: str, stream_id: Optional[str]) -> Optional[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
获取Cookie,按以下顺序尝试:
|
获取Cookie,按以下顺序尝试:
|
||||||
1. Adapter API
|
1. HTTP备用端点 (更稳定)
|
||||||
2. HTTP备用端点
|
2. 本地文件缓存
|
||||||
3. 本地文件缓存
|
3. Adapter API (作为最后手段)
|
||||||
"""
|
"""
|
||||||
# 1. 尝试从Adapter获取
|
# 1. 尝试从HTTP备用端点获取
|
||||||
cookies = await self._get_cookies_from_adapter(stream_id)
|
logger.info(f"开始尝试从HTTP备用地址获取 {qq_account} 的Cookie...")
|
||||||
if cookies:
|
|
||||||
logger.info("成功从Adapter获取Cookie。")
|
|
||||||
self._save_cookies_to_file(qq_account, cookies)
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
# 2. 尝试从HTTP备用端点获取
|
|
||||||
logger.warning("从Adapter获取Cookie失败,尝试使用HTTP备用地址。")
|
|
||||||
cookies = await self._get_cookies_from_http()
|
cookies = await self._get_cookies_from_http()
|
||||||
if cookies:
|
if cookies:
|
||||||
logger.info("成功从HTTP备用地址获取Cookie。")
|
logger.info(f"成功从HTTP备用地址为 {qq_account} 获取Cookie。")
|
||||||
self._save_cookies_to_file(qq_account, cookies)
|
self._save_cookies_to_file(qq_account, cookies)
|
||||||
return cookies
|
return cookies
|
||||||
|
|
||||||
# 3. 尝试从本地文件加载
|
# 2. 尝试从本地文件加载
|
||||||
logger.warning("从HTTP备用地址获取Cookie失败,尝试加载本地缓存。")
|
logger.warning(f"从HTTP备用地址获取 {qq_account} 的Cookie失败,尝试加载本地缓存。")
|
||||||
cookies = self._load_cookies_from_file(qq_account)
|
cookies = self._load_cookies_from_file(qq_account)
|
||||||
if cookies:
|
if cookies:
|
||||||
logger.info("成功从本地文件加载缓存的Cookie。")
|
logger.info(f"成功从本地文件为 {qq_account} 加载缓存的Cookie。")
|
||||||
return cookies
|
return cookies
|
||||||
|
|
||||||
logger.error("所有Cookie获取方法均失败。")
|
# 3. 尝试从Adapter获取 (作为最后的备用方案)
|
||||||
|
logger.warning(f"从本地缓存加载 {qq_account} 的Cookie失败,最后尝试使用Adapter API。")
|
||||||
|
cookies = await self._get_cookies_from_adapter(stream_id)
|
||||||
|
if cookies:
|
||||||
|
logger.info(f"成功从Adapter API为 {qq_account} 获取Cookie。")
|
||||||
|
self._save_cookies_to_file(qq_account, cookies)
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
logger.error(f"为 {qq_account} 获取Cookie的所有方法均失败。请确保Napcat HTTP服务或Adapter连接至少有一个正常工作,或存在有效的本地Cookie文件。")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -409,8 +409,9 @@ class QZoneService:
|
|||||||
cookie_dir.mkdir(exist_ok=True)
|
cookie_dir.mkdir(exist_ok=True)
|
||||||
cookie_file_path = cookie_dir / f"cookies-{qq_account}.json"
|
cookie_file_path = cookie_dir / f"cookies-{qq_account}.json"
|
||||||
|
|
||||||
|
# 优先尝试通过Napcat HTTP服务获取最新的Cookie
|
||||||
try:
|
try:
|
||||||
# 使用HTTP服务器方式获取Cookie
|
logger.info("尝试通过Napcat HTTP服务获取Cookie...")
|
||||||
host = self.get_config("cookie.http_fallback_host", "172.20.130.55")
|
host = self.get_config("cookie.http_fallback_host", "172.20.130.55")
|
||||||
port = self.get_config("cookie.http_fallback_port", "9999")
|
port = self.get_config("cookie.http_fallback_port", "9999")
|
||||||
napcat_token = self.get_config("cookie.napcat_token", "")
|
napcat_token = self.get_config("cookie.napcat_token", "")
|
||||||
@@ -421,23 +422,43 @@ class QZoneService:
|
|||||||
parsed_cookies = {
|
parsed_cookies = {
|
||||||
k.strip(): v.strip() for k, v in (p.split("=", 1) for p in cookie_str.split("; ") if "=" in p)
|
k.strip(): v.strip() for k, v in (p.split("=", 1) for p in cookie_str.split("; ") if "=" in p)
|
||||||
}
|
}
|
||||||
with open(cookie_file_path, "wb") as f:
|
# 成功获取后,异步写入本地文件作为备份
|
||||||
f.write(orjson.dumps(parsed_cookies))
|
try:
|
||||||
logger.info(f"Cookie已更新并保存至: {cookie_file_path}")
|
with open(cookie_file_path, "wb") as f:
|
||||||
|
f.write(orjson.dumps(parsed_cookies))
|
||||||
|
logger.info(f"通过Napcat服务成功更新Cookie,并已保存至: {cookie_file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"保存Cookie到文件时出错: {e}")
|
||||||
return parsed_cookies
|
return parsed_cookies
|
||||||
|
else:
|
||||||
|
logger.warning("通过Napcat服务未能获取有效Cookie。")
|
||||||
|
|
||||||
# 如果HTTP获取失败,尝试读取本地文件
|
|
||||||
if cookie_file_path.exists():
|
|
||||||
with open(cookie_file_path, "rb") as f:
|
|
||||||
return orjson.loads(f.read())
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"更新或加载Cookie时发生异常: {e}")
|
logger.warning(f"通过Napcat HTTP服务获取Cookie时发生异常: {e}。将尝试从本地文件加载。")
|
||||||
return None
|
|
||||||
|
|
||||||
async def _fetch_cookies_http(self, host: str, port: str, napcat_token: str) -> Optional[Dict]:
|
# 如果通过服务获取失败,则尝试从本地文件加载
|
||||||
|
logger.info("尝试从本地Cookie文件加载...")
|
||||||
|
if cookie_file_path.exists():
|
||||||
|
try:
|
||||||
|
with open(cookie_file_path, "rb") as f:
|
||||||
|
cookies = orjson.loads(f.read())
|
||||||
|
logger.info(f"成功从本地文件加载Cookie: {cookie_file_path}")
|
||||||
|
return cookies
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从本地文件 {cookie_file_path} 读取或解析Cookie失败: {e}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"本地Cookie文件不存在: {cookie_file_path}")
|
||||||
|
|
||||||
|
logger.error("所有获取Cookie的方式均失败。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _fetch_cookies_http(self, host: str, port: int, napcat_token: str) -> Optional[Dict]:
|
||||||
"""通过HTTP服务器获取Cookie"""
|
"""通过HTTP服务器获取Cookie"""
|
||||||
url = f"http://{host}:{port}/get_cookies"
|
# 从配置中读取主机和端口,如果未提供则使用传入的参数
|
||||||
|
final_host = self.get_config("cookie.http_fallback_host", host)
|
||||||
|
final_port = self.get_config("cookie.http_fallback_port", port)
|
||||||
|
url = f"http://{final_host}:{final_port}/get_cookies"
|
||||||
|
|
||||||
max_retries = 5
|
max_retries = 5
|
||||||
retry_delay = 1
|
retry_delay = 1
|
||||||
|
|
||||||
@@ -481,14 +502,19 @@ class QZoneService:
|
|||||||
async def _get_api_client(self, qq_account: str, stream_id: Optional[str]) -> Optional[Dict]:
|
async def _get_api_client(self, qq_account: str, stream_id: Optional[str]) -> Optional[Dict]:
|
||||||
cookies = await self.cookie_service.get_cookies(qq_account, stream_id)
|
cookies = await self.cookie_service.get_cookies(qq_account, stream_id)
|
||||||
if not cookies:
|
if not cookies:
|
||||||
|
logger.error("获取API客户端失败:未能获取到Cookie。请检查Napcat连接是否正常,或是否存在有效的本地Cookie文件。")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
p_skey = cookies.get("p_skey") or cookies.get("p_skey".upper())
|
p_skey = cookies.get("p_skey") or cookies.get("p_skey".upper())
|
||||||
if not p_skey:
|
if not p_skey:
|
||||||
|
logger.error(f"获取API客户端失败:Cookie中缺少关键的 'p_skey'。Cookie内容: {cookies}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
gtk = self._generate_gtk(p_skey)
|
gtk = self._generate_gtk(p_skey)
|
||||||
uin = cookies.get("uin", "").lstrip("o")
|
uin = cookies.get("uin", "").lstrip("o")
|
||||||
|
if not uin:
|
||||||
|
logger.error(f"获取API客户端失败:Cookie中缺少关键的 'uin'。Cookie内容: {cookies}")
|
||||||
|
return None
|
||||||
|
|
||||||
async def _request(method, url, params=None, data=None, headers=None):
|
async def _request(method, url, params=None, data=None, headers=None):
|
||||||
final_headers = {"referer": f"https://user.qzone.qq.com/{uin}", "origin": "https://user.qzone.qq.com"}
|
final_headers = {"referer": f"https://user.qzone.qq.com/{uin}", "origin": "https://user.qzone.qq.com"}
|
||||||
|
|||||||
@@ -185,9 +185,13 @@ class SendHandler:
|
|||||||
|
|
||||||
logger.info(f"执行适配器命令: {action}")
|
logger.info(f"执行适配器命令: {action}")
|
||||||
|
|
||||||
# 直接向Napcat发送命令并获取响应
|
# 根据action决定处理方式
|
||||||
response_task = asyncio.create_task(self.send_message_to_napcat(action, params))
|
if action == "get_cookies":
|
||||||
response = await response_task
|
# 对于get_cookies,我们需要一个更长的超时时间
|
||||||
|
response = await self.send_message_to_napcat(action, params, timeout=40.0)
|
||||||
|
else:
|
||||||
|
# 对于其他命令,使用默认超时
|
||||||
|
response = await self.send_message_to_napcat(action, params)
|
||||||
|
|
||||||
# 发送响应回MaiBot
|
# 发送响应回MaiBot
|
||||||
await self.send_adapter_command_response(raw_message_base, response, request_id)
|
await self.send_adapter_command_response(raw_message_base, response, request_id)
|
||||||
@@ -196,6 +200,8 @@ class SendHandler:
|
|||||||
logger.info(f"适配器命令 {action} 执行成功")
|
logger.info(f"适配器命令 {action} 执行成功")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"适配器命令 {action} 执行失败,napcat返回:{str(response)}")
|
logger.warning(f"适配器命令 {action} 执行失败,napcat返回:{str(response)}")
|
||||||
|
# 无论成功失败,都记录下完整的响应内容以供调试
|
||||||
|
logger.debug(f"适配器命令 {action} 的完整响应: {response}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"处理适配器命令时发生错误: {e}")
|
logger.error(f"处理适配器命令时发生错误: {e}")
|
||||||
@@ -583,7 +589,7 @@ class SendHandler:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_message_to_napcat(self, action: str, params: dict) -> dict:
|
async def send_message_to_napcat(self, action: str, params: dict, timeout: float = 20.0) -> dict:
|
||||||
request_uuid = str(uuid.uuid4())
|
request_uuid = str(uuid.uuid4())
|
||||||
payload = json.dumps({"action": action, "params": params, "echo": request_uuid})
|
payload = json.dumps({"action": action, "params": params, "echo": request_uuid})
|
||||||
|
|
||||||
@@ -595,9 +601,9 @@ class SendHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await connection.send(payload)
|
await connection.send(payload)
|
||||||
response = await get_response(request_uuid)
|
response = await get_response(request_uuid, timeout=timeout) # 使用传入的超时时间
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
logger.error("发送消息超时,未收到响应")
|
logger.error(f"发送消息超时({timeout}秒),未收到响应: action={action}, params={params}")
|
||||||
return {"status": "error", "message": "timeout"}
|
return {"status": "error", "message": "timeout"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"发送消息失败: {e}")
|
logger.error(f"发送消息失败: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user