diff --git a/src/plugins/built_in/maizone_refactored/plugin.py b/src/plugins/built_in/maizone_refactored/plugin.py index 345401bdd..a82db7511 100644 --- a/src/plugins/built_in/maizone_refactored/plugin.py +++ b/src/plugins/built_in/maizone_refactored/plugin.py @@ -90,11 +90,20 @@ class MaiZoneRefactoredPlugin(BasePlugin): 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() - qzone_service = QZoneService(self.get_config, content_service, image_service, cookie_service) + + # 使用已创建的 reply_tracker_service 实例 + qzone_service = QZoneService( + self.get_config, + content_service, + image_service, + cookie_service, + reply_tracker_service # 传入已创建的实例 + ) scheduler_service = SchedulerService(self.get_config, qzone_service) monitor_service = MonitorService(self.get_config, qzone_service) diff --git a/src/plugins/built_in/maizone_refactored/services/qzone_service.py b/src/plugins/built_in/maizone_refactored/services/qzone_service.py index 46a80ac30..004b7fa4f 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -51,12 +51,14 @@ class QZoneService: content_service: ContentService, image_service: ImageService, cookie_service: CookieService, + reply_tracker: ReplyTrackerService = None, ): self.get_config = get_config self.content_service = content_service self.image_service = image_service self.cookie_service = cookie_service - self.reply_tracker = ReplyTrackerService() + # 如果没有提供 reply_tracker 实例,则创建一个新的 + self.reply_tracker = reply_tracker if reply_tracker is not None else ReplyTrackerService() # --- Public Methods (High-Level Business Logic) --- @@ -260,10 +262,7 @@ class QZoneService: if not user_comments: return - # 2. 验证已记录的回复是否仍然存在,清理已删除的回复记录 - await self._validate_and_cleanup_reply_records(fid, my_replies) - - # 3. 使用验证后的持久化记录来筛选未回复的评论 + # 直接检查评论是否已回复,不做验证清理 comments_to_reply = [] for comment in user_comments: comment_tid = comment.get("comment_tid") @@ -272,10 +271,13 @@ class QZoneService: # 检查是否已经在持久化记录中标记为已回复 if not self.reply_tracker.has_replied(fid, comment_tid): + # 记录日志以便追踪 + logger.debug(f"发现新评论需要回复 - 说说ID: {fid}, 评论ID: {comment_tid}, " + f"评论人: {comment.get('nickname', '')}, 内容: {comment.get('content', '')}") comments_to_reply.append(comment) if not comments_to_reply: - logger.debug(f"说说 {fid} 下的所有评论都已回复过") + logger.debug(f"说说 {fid} 下的所有评论都已回复过或无需回复") return logger.info(f"发现自己说说下的 {len(comments_to_reply)} 条新评论,准备回复...") @@ -801,6 +803,10 @@ class QZoneService: "richval": "", "paramstr": f"@{target_name} {content}", } + + # 记录详细的请求参数用于调试 + logger.info(f"子回复请求参数: topicId={data['topicId']}, parent_tid={data['parent_tid']}, content='{content[:50]}...'") + await _request("POST", self.REPLY_URL, params={"g_tk": gtk}, data=data) return True except Exception as e: diff --git a/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py b/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py index 655bf3330..9df67d26a 100644 --- a/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py +++ b/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py @@ -7,7 +7,7 @@ import json import time from pathlib import Path -from typing import Set, Dict, Any +from typing import Set, Dict, Any, Union from src.common.logger import get_logger logger = get_logger("MaiZone.ReplyTrackerService") @@ -22,7 +22,7 @@ class ReplyTrackerService: def __init__(self): # 数据存储路径 self.data_dir = Path(__file__).resolve().parent.parent / "data" - self.data_dir.mkdir(exist_ok=True) + self.data_dir.mkdir(exist_ok=True, parents=True) self.reply_record_file = self.data_dir / "replied_comments.json" # 内存中的已回复评论记录 @@ -34,32 +34,109 @@ class ReplyTrackerService: # 加载已有数据 self._load_data() + logger.debug(f"ReplyTrackerService initialized with data file: {self.reply_record_file}") + + def _validate_data(self, data: Any) -> bool: + """验证加载的数据格式是否正确""" + if not isinstance(data, dict): + logger.error("加载的数据不是字典格式") + return False + + for feed_id, comments in data.items(): + if not isinstance(feed_id, str): + logger.error(f"无效的说说ID格式: {feed_id}") + return False + if not isinstance(comments, dict): + logger.error(f"说说 {feed_id} 的评论数据不是字典格式") + return False + for comment_id, timestamp in comments.items(): + # 确保comment_id是字符串格式,如果是数字则转换为字符串 + if not isinstance(comment_id, (str, int)): + logger.error(f"无效的评论ID格式: {comment_id}") + return False + if not isinstance(timestamp, (int, float)): + logger.error(f"无效的时间戳格式: {timestamp}") + return False + return True def _load_data(self): """从文件加载已回复评论数据""" try: if self.reply_record_file.exists(): - with open(self.reply_record_file, "r", encoding="utf-8") as f: - data = json.load(f) - self.replied_comments = data - logger.info(f"已加载 {len(self.replied_comments)} 条说说的回复记录") + try: + with open(self.reply_record_file, "r", encoding="utf-8") as f: + file_content = f.read().strip() + if not file_content: # 文件为空 + logger.warning("回复记录文件为空,将创建新的记录") + self.replied_comments = {} + return + + data = json.loads(file_content) + if self._validate_data(data): + self.replied_comments = data + logger.info(f"已加载 {len(self.replied_comments)} 条说说的回复记录," + f"总计 {sum(len(comments) for comments in self.replied_comments.values())} 条评论") + else: + logger.error("加载的数据格式无效,将创建新的记录") + self.replied_comments = {} + except json.JSONDecodeError as e: + logger.error(f"解析回复记录文件失败: {e}") + self._backup_corrupted_file() + self.replied_comments = {} else: logger.info("未找到回复记录文件,将创建新的记录") + self.replied_comments = {} except Exception as e: - logger.error(f"加载回复记录失败: {e}") + logger.error(f"加载回复记录失败: {e}", exc_info=True) self.replied_comments = {} + def _backup_corrupted_file(self): + """备份损坏的数据文件""" + try: + if self.reply_record_file.exists(): + backup_file = self.reply_record_file.with_suffix(f".json.bak.{int(time.time())}") + self.reply_record_file.rename(backup_file) + logger.warning(f"已将损坏的数据文件备份为: {backup_file}") + except Exception as e: + logger.error(f"备份损坏的数据文件失败: {e}") + def _save_data(self): """保存已回复评论数据到文件""" try: + # 验证数据格式 + if not self._validate_data(self.replied_comments): + logger.error("当前数据格式无效,取消保存") + return + # 清理过期数据 self._cleanup_old_records() - with open(self.reply_record_file, "w", encoding="utf-8") as f: + # 创建临时文件 + temp_file = self.reply_record_file.with_suffix('.tmp') + + # 先写入临时文件 + with open(temp_file, "w", encoding="utf-8") as f: json.dump(self.replied_comments, f, ensure_ascii=False, indent=2) - logger.debug("回复记录已保存") + + # 如果写入成功,重命名为正式文件 + if temp_file.stat().st_size > 0: # 确保写入成功 + # 在Windows上,如果目标文件已存在,需要先删除它 + if self.reply_record_file.exists(): + self.reply_record_file.unlink() + temp_file.rename(self.reply_record_file) + logger.debug(f"回复记录已保存,包含 {len(self.replied_comments)} 条说说的记录") + else: + logger.error("临时文件写入失败,文件大小为0") + temp_file.unlink() # 删除空的临时文件 + except Exception as e: - logger.error(f"保存回复记录失败: {e}") + logger.error(f"保存回复记录失败: {e}", exc_info=True) + # 尝试删除可能存在的临时文件 + try: + if temp_file.exists(): + temp_file.unlink() + except: + pass def _cleanup_old_records(self): """清理超过保留期限的记录""" @@ -69,9 +146,11 @@ class ReplyTrackerService: feeds_to_remove = [] total_removed = 0 + # 仅清理超过保留期限的记录,不根据API返回结果清理 for feed_id, comments in self.replied_comments.items(): comments_to_remove = [] + # 仅清理超过指定天数的记录 for comment_id, timestamp in comments.items(): if timestamp < cutoff_time: comments_to_remove.append(comment_id) @@ -90,47 +169,53 @@ class ReplyTrackerService: del self.replied_comments[feed_id] if total_removed > 0: - logger.info(f"清理了 {total_removed} 条过期的回复记录") + logger.info(f"清理了 {total_removed} 条超过{self.max_record_days}天的过期回复记录") - def has_replied(self, feed_id: str, comment_id: str) -> bool: + def has_replied(self, feed_id: str, comment_id: Union[str, int]) -> bool: """ 检查是否已经回复过指定的评论 Args: feed_id: 说说ID - comment_id: 评论ID + comment_id: 评论ID (可以是字符串或数字) Returns: bool: 如果已回复过返回True,否则返回False """ - if not feed_id or not comment_id: + if not feed_id or comment_id is None: return False - return feed_id in self.replied_comments and comment_id in self.replied_comments[feed_id] + comment_id_str = str(comment_id) + return feed_id in self.replied_comments and comment_id_str in self.replied_comments[feed_id] - def mark_as_replied(self, feed_id: str, comment_id: str): + def mark_as_replied(self, feed_id: str, comment_id: Union[str, int]): """ 标记指定评论为已回复 Args: feed_id: 说说ID - comment_id: 评论ID + comment_id: 评论ID (可以是字符串或数字) """ - if not feed_id or not comment_id: + if not feed_id or comment_id is None: logger.warning("feed_id 或 comment_id 为空,无法标记为已回复") return current_time = time.time() + # 确保将comment_id转换为字符串格式 + comment_id_str = str(comment_id) + if feed_id not in self.replied_comments: self.replied_comments[feed_id] = {} - self.replied_comments[feed_id][comment_id] = current_time + self.replied_comments[feed_id][comment_id_str] = current_time - # 保存到文件 - self._save_data() - - logger.info(f"已标记评论为已回复: feed_id={feed_id}, comment_id={comment_id}") + # 验证数据并保存到文件 + if self._validate_data(self.replied_comments): + self._save_data() + logger.info(f"已标记评论为已回复: feed_id={feed_id}, comment_id={comment_id}") + else: + logger.error(f"标记评论时数据验证失败: feed_id={feed_id}, comment_id={comment_id}") def get_replied_comments(self, feed_id: str) -> Set[str]: """ @@ -143,7 +228,8 @@ class ReplyTrackerService: Set[str]: 已回复的评论ID集合 """ if feed_id in self.replied_comments: - return set(self.replied_comments[feed_id].keys()) + # 确保所有评论ID都是字符串格式 + return {str(comment_id) for comment_id in self.replied_comments[feed_id].keys()} return set() def get_stats(self) -> Dict[str, Any]: