feat(maizone): 建立多级回退机制以保障空间Cookie获取的健壮性

该提交旨在从根本上解决MaiZone插件因单一Cookie来源不稳定而导致功能中断的问题。通过引入一个具有优先级的多级回退策略,显著提高了服务的可用性和容错能力。

核心变更如下:

- **引入健壮的获取策略**:重构了Cookie的获取逻辑,建立了一条清晰的降级路径。现在系统会优先通过最可靠的Napcat HTTP服务获取,若失败则尝试读取本地文件缓存,最后才调用可能超时的Adapter API。这确保了即使部分服务不可用,插件仍有很大概率正常工作。

- **优化插件生命周期**:修正了插件加载时的初始化流程,将服务注册和后台任务启动合并到单一的`on_plugin_loaded`方法中,消除了潜在的竞态条件,确保了监控和定时任务总能被正确启动。

- **提升操作容忍度**:将Napcat适配器中`get_cookies`动作的超时阈值放宽至40秒,为网络延迟或不稳定的情况提供了更充足的缓冲时间,减少了因过早超时而造成的失败。

- **细化过程日志**:在整个Cookie获取和QZone服务调用链中增加了详细的上下文日志,使得在出现问题时能够快速定位失败环节和具体原因,极大地简化了未来的故障排查工作。
This commit is contained in:
tt-P607
2025-10-01 00:22:09 +08:00
parent 0f31a51097
commit 9dbc108298
4 changed files with 79 additions and 50 deletions

View File

@@ -88,25 +88,27 @@ class MaiZoneRefactoredPlugin(BasePlugin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
async def on_plugin_loaded(self):
"""插件加载完成后的回调,初始化服务并启动后台任务"""
# --- 注册权限节点 ---
await permission_api.register_permission_node(
"plugin.maizone.send_feed", "是否可以使用机器人发送QQ空间说说", "maiZone", False
)
await permission_api.register_permission_node(
"plugin.maizone.read_feed", "是否可以使用机器人读取QQ空间说说", "maiZone", True
)
# 创建所有服务实例
# --- 创建并注册所有服务实例 ---
content_service = ContentService(self.get_config)
image_service = ImageService(self.get_config)
cookie_service = CookieService(self.get_config)
reply_tracker_service = ReplyTrackerService()
# 使用已创建的 reply_tracker_service 实例
qzone_service = QZoneService(
self.get_config,
content_service,
image_service,
cookie_service,
reply_tracker_service, # 传入已创建的实例
reply_tracker_service,
)
scheduler_service = SchedulerService(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("get_config", self.get_config)
# 保存服务引用以便后续启动
self.scheduler_service = scheduler_service
self.monitor_service = monitor_service
logger.info("MaiZone重构版插件服务已注册。")
logger.info("MaiZone重构版插件已加载服务已注册。")
async def on_plugin_loaded(self):
"""插件加载完成后的回调,启动异步服务"""
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后台任务已启动。")
# --- 启动后台任务 ---
asyncio.create_task(scheduler_service.start())
asyncio.create_task(monitor_service.start())
logger.info("MaiZone后台监控和定时任务已启动。")
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [

View File

@@ -113,31 +113,32 @@ class CookieService:
async def get_cookies(self, qq_account: str, stream_id: Optional[str]) -> Optional[Dict[str, str]]:
"""
获取Cookie按以下顺序尝试
1. Adapter API
2. HTTP备用端点
3. 本地文件缓存
1. HTTP备用端点 (更稳定)
2. 本地文件缓存
3. Adapter API (作为最后手段)
"""
# 1. 尝试从Adapter获取
cookies = await self._get_cookies_from_adapter(stream_id)
if cookies:
logger.info("成功从Adapter获取Cookie。")
self._save_cookies_to_file(qq_account, cookies)
return cookies
# 2. 尝试从HTTP备用端点获取
logger.warning("从Adapter获取Cookie失败尝试使用HTTP备用地址。")
# 1. 尝试从HTTP备用端点获取
logger.info(f"开始尝试从HTTP备用地址获取 {qq_account} 的Cookie...")
cookies = await self._get_cookies_from_http()
if cookies:
logger.info("成功从HTTP备用地址获取Cookie。")
logger.info(f"成功从HTTP备用地址{qq_account} 获取Cookie。")
self._save_cookies_to_file(qq_account, cookies)
return cookies
# 3. 尝试从本地文件加载
logger.warning("从HTTP备用地址获取Cookie失败尝试加载本地缓存。")
# 2. 尝试从本地文件加载
logger.warning(f"从HTTP备用地址获取 {qq_account}Cookie失败尝试加载本地缓存。")
cookies = self._load_cookies_from_file(qq_account)
if cookies:
logger.info("成功从本地文件加载缓存的Cookie。")
logger.info(f"成功从本地文件{qq_account} 加载缓存的Cookie。")
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

View File

@@ -409,8 +409,9 @@ class QZoneService:
cookie_dir.mkdir(exist_ok=True)
cookie_file_path = cookie_dir / f"cookies-{qq_account}.json"
# 优先尝试通过Napcat HTTP服务获取最新的Cookie
try:
# 使用HTTP服务器方式获取Cookie
logger.info("尝试通过Napcat HTTP服务获取Cookie...")
host = self.get_config("cookie.http_fallback_host", "172.20.130.55")
port = self.get_config("cookie.http_fallback_port", "9999")
napcat_token = self.get_config("cookie.napcat_token", "")
@@ -421,23 +422,43 @@ class QZoneService:
parsed_cookies = {
k.strip(): v.strip() for k, v in (p.split("=", 1) for p in cookie_str.split("; ") if "=" in p)
}
# 成功获取后,异步写入本地文件作为备份
try:
with open(cookie_file_path, "wb") as f:
f.write(orjson.dumps(parsed_cookies))
logger.info(f"Cookie已更新并保存至: {cookie_file_path}")
return parsed_cookies
# 如果HTTP获取失败尝试读取本地文件
if cookie_file_path.exists():
with open(cookie_file_path, "rb") as f:
return orjson.loads(f.read())
return None
logger.info(f"通过Napcat服务成功更新Cookie,并已保存至: {cookie_file_path}")
except Exception as e:
logger.error(f"更新或加载Cookie时发生异常: {e}")
logger.warning(f"保存Cookie到文件时出错: {e}")
return parsed_cookies
else:
logger.warning("通过Napcat服务未能获取有效Cookie。")
except Exception as e:
logger.warning(f"通过Napcat HTTP服务获取Cookie时发生异常: {e}。将尝试从本地文件加载。")
# 如果通过服务获取失败,则尝试从本地文件加载
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: str, napcat_token: str) -> Optional[Dict]:
async def _fetch_cookies_http(self, host: str, port: int, napcat_token: str) -> Optional[Dict]:
"""通过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
retry_delay = 1
@@ -481,14 +502,19 @@ class QZoneService:
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)
if not cookies:
logger.error("获取API客户端失败未能获取到Cookie。请检查Napcat连接是否正常或是否存在有效的本地Cookie文件。")
return None
p_skey = cookies.get("p_skey") or cookies.get("p_skey".upper())
if not p_skey:
logger.error(f"获取API客户端失败Cookie中缺少关键的 'p_skey'。Cookie内容: {cookies}")
return None
gtk = self._generate_gtk(p_skey)
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):
final_headers = {"referer": f"https://user.qzone.qq.com/{uin}", "origin": "https://user.qzone.qq.com"}

View File

@@ -185,9 +185,13 @@ class SendHandler:
logger.info(f"执行适配器命令: {action}")
# 直接向Napcat发送命令并获取响应
response_task = asyncio.create_task(self.send_message_to_napcat(action, params))
response = await response_task
# 根据action决定处理方式
if action == "get_cookies":
# 对于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
await self.send_adapter_command_response(raw_message_base, response, request_id)
@@ -196,6 +200,8 @@ class SendHandler:
logger.info(f"适配器命令 {action} 执行成功")
else:
logger.warning(f"适配器命令 {action} 执行失败napcat返回{str(response)}")
# 无论成功失败,都记录下完整的响应内容以供调试
logger.debug(f"适配器命令 {action} 的完整响应: {response}")
except Exception as 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())
payload = json.dumps({"action": action, "params": params, "echo": request_uuid})
@@ -595,9 +601,9 @@ class SendHandler:
try:
await connection.send(payload)
response = await get_response(request_uuid)
response = await get_response(request_uuid, timeout=timeout) # 使用传入的超时时间
except TimeoutError:
logger.error("发送消息超时,未收到响应")
logger.error(f"发送消息超时{timeout}秒),未收到响应: action={action}, params={params}")
return {"status": "error", "message": "timeout"}
except Exception as e:
logger.error(f"发送消息失败: {e}")