diff --git a/src/plugins/built_in/maizone_refactored/plugin.py b/src/plugins/built_in/maizone_refactored/plugin.py index ca9a8c72c..6507c1c92 100644 --- a/src/plugins/built_in/maizone_refactored/plugin.py +++ b/src/plugins/built_in/maizone_refactored/plugin.py @@ -24,6 +24,7 @@ from .services.qzone_service import QZoneService from .services.scheduler_service import SchedulerService from .services.monitor_service import MonitorService from .services.cookie_service import CookieService +from .services.reply_tracker_service import ReplyTrackerService from .services.manager import register_service logger = get_logger("MaiZone.Plugin") @@ -99,11 +100,13 @@ class MaiZoneRefactoredPlugin(BasePlugin): 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) scheduler_service = SchedulerService(self.get_config, qzone_service) monitor_service = MonitorService(self.get_config, qzone_service) register_service("qzone", qzone_service) + register_service("reply_tracker", reply_tracker_service) register_service("get_config", self.get_config) # 保存服务引用以便后续启动 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 758755800..ea422b7e5 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -27,6 +27,7 @@ from src.chat.utils.chat_message_builder import ( from .content_service import ContentService from .image_service import ImageService from .cookie_service import CookieService +from .reply_tracker_service import ReplyTrackerService logger = get_logger("MaiZone.QZoneService") @@ -55,6 +56,7 @@ class QZoneService: self.content_service = content_service self.image_service = image_service self.cookie_service = cookie_service + self.reply_tracker = ReplyTrackerService() # --- Public Methods (High-Level Business Logic) --- @@ -249,43 +251,83 @@ class QZoneService: content = feed.get("content", "") fid = feed.get("tid", "") - if not comments: - return - - # 筛选出未被自己回复过的评论 - if not comments: + if not comments or not fid: return # 1. 将评论分为用户评论和自己的回复 user_comments = [c for c in comments if str(c.get('qq_account')) != str(qq_account)] my_replies = [c for c in comments if str(c.get('qq_account')) == str(qq_account)] + + if not user_comments: + return - # 2. 获取所有已经被我回复过的评论的ID - replied_comment_ids = {reply.get('parent_tid') for reply in my_replies if reply.get('parent_tid')} + # 2. 验证已记录的回复是否仍然存在,清理已删除的回复记录 + await self._validate_and_cleanup_reply_records(fid, my_replies) - # 3. 找出所有尚未被回复过的用户评论 - comments_to_reply = [ - comment for comment in user_comments - if comment.get('comment_tid') not in replied_comment_ids - ] + # 3. 使用验证后的持久化记录来筛选未回复的评论 + comments_to_reply = [] + for comment in user_comments: + comment_tid = comment.get('comment_tid') + if not comment_tid: + continue + + # 检查是否已经在持久化记录中标记为已回复 + if not self.reply_tracker.has_replied(fid, comment_tid): + comments_to_reply.append(comment) if not comments_to_reply: + logger.debug(f"说说 {fid} 下的所有评论都已回复过") return logger.info(f"发现自己说说下的 {len(comments_to_reply)} 条新评论,准备回复...") for comment in comments_to_reply: - reply_content = await self.content_service.generate_comment_reply( - content, comment.get("content", ""), comment.get("nickname", "") - ) - if reply_content: - success = await api_client["reply"]( - fid, qq_account, comment.get("nickname", ""), reply_content, comment.get("comment_tid") + comment_tid = comment.get("comment_tid") + nickname = comment.get("nickname", "") + comment_content = comment.get("content", "") + + try: + reply_content = await self.content_service.generate_comment_reply( + content, comment_content, nickname ) - if success: - logger.info(f"成功回复'{comment.get('nickname', '')}'的评论: '{reply_content}'") + if reply_content: + success = await api_client["reply"]( + fid, qq_account, nickname, reply_content, comment_tid + ) + if success: + # 标记为已回复 + self.reply_tracker.mark_as_replied(fid, comment_tid) + logger.info(f"成功回复'{nickname}'的评论: '{reply_content}'") + else: + logger.error(f"回复'{nickname}'的评论失败") + await asyncio.sleep(random.uniform(10, 20)) else: - logger.error(f"回复'{comment.get('nickname', '')}'的评论失败") - await asyncio.sleep(random.uniform(10, 20)) + logger.warning(f"生成回复内容失败,跳过回复'{nickname}'的评论") + except Exception as e: + logger.error(f"回复'{nickname}'的评论时发生异常: {e}", exc_info=True) + + async def _validate_and_cleanup_reply_records(self, fid: str, my_replies: List[Dict]): + """验证并清理已删除的回复记录""" + # 获取当前记录中该说说的所有已回复评论ID + recorded_replied_comments = self.reply_tracker.get_replied_comments(fid) + + if not recorded_replied_comments: + return + + # 从API返回的我的回复中提取parent_tid(即被回复的评论ID) + current_replied_comments = set() + for reply in my_replies: + parent_tid = reply.get('parent_tid') + if parent_tid: + current_replied_comments.add(parent_tid) + + # 找出记录中有但实际已不存在的回复 + deleted_replies = recorded_replied_comments - current_replied_comments + + if deleted_replies: + logger.info(f"检测到 {len(deleted_replies)} 个回复已被删除,清理记录...") + for comment_tid in deleted_replies: + self.reply_tracker.remove_reply_record(fid, comment_tid) + logger.debug(f"已清理删除的回复记录: feed_id={fid}, comment_id={comment_tid}") async def _process_single_feed(self, feed: Dict, api_client: Dict, target_qq: str, target_name: str): """处理单条说说,决定是否评论和点赞""" 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 new file mode 100644 index 000000000..a90c88d9f --- /dev/null +++ b/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +""" +评论回复跟踪服务 +负责记录和管理已回复过的评论ID,避免重复回复 +""" + +import json +import time +from pathlib import Path +from typing import Set, Dict, Any +from src.common.logger import get_logger + +logger = get_logger("MaiZone.ReplyTrackerService") + + +class ReplyTrackerService: + """ + 评论回复跟踪服务 + 使用本地JSON文件持久化存储已回复的评论ID + """ + + def __init__(self): + # 数据存储路径 + self.data_dir = Path(__file__).resolve().parent.parent / "data" + self.data_dir.mkdir(exist_ok=True) + self.reply_record_file = self.data_dir / "replied_comments.json" + + # 内存中的已回复评论记录 + # 格式: {feed_id: {comment_id: timestamp, ...}, ...} + self.replied_comments: Dict[str, Dict[str, float]] = {} + + # 数据清理配置 + self.max_record_days = 30 # 保留30天的记录 + + # 加载已有数据 + self._load_data() + + 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)} 条说说的回复记录") + else: + logger.info("未找到回复记录文件,将创建新的记录") + except Exception as e: + logger.error(f"加载回复记录失败: {e}") + self.replied_comments = {} + + def _save_data(self): + """保存已回复评论数据到文件""" + try: + # 清理过期数据 + self._cleanup_old_records() + + with open(self.reply_record_file, 'w', encoding='utf-8') as f: + json.dump(self.replied_comments, f, ensure_ascii=False, indent=2) + logger.debug("回复记录已保存") + except Exception as e: + logger.error(f"保存回复记录失败: {e}") + + def _cleanup_old_records(self): + """清理超过保留期限的记录""" + current_time = time.time() + cutoff_time = current_time - (self.max_record_days * 24 * 60 * 60) + + feeds_to_remove = [] + total_removed = 0 + + 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) + + # 移除过期的评论记录 + for comment_id in comments_to_remove: + del comments[comment_id] + total_removed += 1 + + # 如果该说说下没有任何记录了,标记删除整个说说记录 + if not comments: + feeds_to_remove.append(feed_id) + + # 移除空的说说记录 + for feed_id in feeds_to_remove: + del self.replied_comments[feed_id] + + if total_removed > 0: + logger.info(f"清理了 {total_removed} 条过期的回复记录") + + def has_replied(self, feed_id: str, comment_id: str) -> bool: + """ + 检查是否已经回复过指定的评论 + + Args: + feed_id: 说说ID + comment_id: 评论ID + + Returns: + bool: 如果已回复过返回True,否则返回False + """ + if not feed_id or not comment_id: + return False + + return (feed_id in self.replied_comments and + comment_id in self.replied_comments[feed_id]) + + def mark_as_replied(self, feed_id: str, comment_id: str): + """ + 标记指定评论为已回复 + + Args: + feed_id: 说说ID + comment_id: 评论ID + """ + if not feed_id or not comment_id: + logger.warning("feed_id 或 comment_id 为空,无法标记为已回复") + return + + current_time = time.time() + + if feed_id not in self.replied_comments: + self.replied_comments[feed_id] = {} + + self.replied_comments[feed_id][comment_id] = current_time + + # 保存到文件 + self._save_data() + + logger.info(f"已标记评论为已回复: feed_id={feed_id}, comment_id={comment_id}") + + def get_replied_comments(self, feed_id: str) -> Set[str]: + """ + 获取指定说说下所有已回复的评论ID + + Args: + feed_id: 说说ID + + Returns: + Set[str]: 已回复的评论ID集合 + """ + if feed_id in self.replied_comments: + return set(self.replied_comments[feed_id].keys()) + return set() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取回复记录统计信息 + + Returns: + Dict: 包含统计信息的字典 + """ + total_feeds = len(self.replied_comments) + total_replies = sum(len(comments) for comments in self.replied_comments.values()) + + return { + "total_feeds_with_replies": total_feeds, + "total_replied_comments": total_replies, + "data_file": str(self.reply_record_file), + "max_record_days": self.max_record_days + } + + def remove_reply_record(self, feed_id: str, comment_id: str): + """ + 移除指定评论的回复记录 + + Args: + feed_id: 说说ID + comment_id: 评论ID + """ + if feed_id in self.replied_comments and comment_id in self.replied_comments[feed_id]: + del self.replied_comments[feed_id][comment_id] + + # 如果该说说下没有任何回复记录了,删除整个说说记录 + if not self.replied_comments[feed_id]: + del self.replied_comments[feed_id] + + self._save_data() + logger.debug(f"已移除回复记录: feed_id={feed_id}, comment_id={comment_id}") + + def remove_feed_records(self, feed_id: str): + """ + 移除指定说说的所有回复记录 + + Args: + feed_id: 说说ID + """ + if feed_id in self.replied_comments: + del self.replied_comments[feed_id] + self._save_data() + logger.info(f"已移除说说 {feed_id} 的所有回复记录") \ No newline at end of file