refactor(maizone): 优化回复跟踪服务和实现子回复

对 `maizone_refactored` 插件进行多项重构和功能改进:

1.  **依赖注入 `ReplyTrackerService`**:
    -   在 `plugin.py` 中创建 `ReplyTrackerService` 的单例,并将其注入到 `QZoneService` 中。
    -   这确保了整个插件共享同一个回复记录实例,避免了之前在 `QZoneService` 内部创建实例导致的状态不一致问题。

2.  **增强 `ReplyTrackerService` 的健壮性**:
    -   增加了对 `replied_comments.json` 文件加载时的数据验证和错误处理,包括处理空文件和JSON解析错误。
    -   实现了损坏文件的自动备份机制。
    -   采用原子化写入操作(先写临时文件再重命名),防止在保存过程中因意外中断导致数据文件损坏。
    -   改进了日志记录,提供了更清晰的加载、保存和清理过程信息。

3.  **实现真正的子回复(盖楼)功能**:
    -   修改了 `QZoneService` 中 `_reply` 方法的请求参数,特别是 `topicId` 和 `paramstr`,并添加了 `parent_tid`,以实现对特定评论的直接回复,而不是简单地在说说下发表新评论。

4.  **优化评论处理逻辑**:
    -   移除了在处理新评论前对已记录回复进行验证的步骤,简化了逻辑,直接检查评论是否已被回复。
This commit is contained in:
tt-P607
2025-09-01 15:07:56 +08:00
committed by Windpicker-owo
parent 6cf59deef3
commit 2f1dcee7cb
3 changed files with 132 additions and 31 deletions

View File

@@ -90,11 +90,20 @@ class MaiZoneRefactoredPlugin(BasePlugin):
permission_api.register_permission_node( 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()
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) scheduler_service = SchedulerService(self.get_config, qzone_service)
monitor_service = MonitorService(self.get_config, qzone_service) monitor_service = MonitorService(self.get_config, qzone_service)

View File

@@ -51,12 +51,14 @@ class QZoneService:
content_service: ContentService, content_service: ContentService,
image_service: ImageService, image_service: ImageService,
cookie_service: CookieService, cookie_service: CookieService,
reply_tracker: ReplyTrackerService = None,
): ):
self.get_config = get_config self.get_config = get_config
self.content_service = content_service self.content_service = content_service
self.image_service = image_service self.image_service = image_service
self.cookie_service = cookie_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) --- # --- Public Methods (High-Level Business Logic) ---
@@ -260,10 +262,7 @@ class QZoneService:
if not user_comments: if not user_comments:
return return
# 2. 验证已记录的回复是否仍然存在,清理已删除的回复记录 # 直接检查评论是否已回复,不做验证清理
await self._validate_and_cleanup_reply_records(fid, my_replies)
# 3. 使用验证后的持久化记录来筛选未回复的评论
comments_to_reply = [] comments_to_reply = []
for comment in user_comments: for comment in user_comments:
comment_tid = comment.get("comment_tid") comment_tid = comment.get("comment_tid")
@@ -272,10 +271,13 @@ class QZoneService:
# 检查是否已经在持久化记录中标记为已回复 # 检查是否已经在持久化记录中标记为已回复
if not self.reply_tracker.has_replied(fid, comment_tid): 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) comments_to_reply.append(comment)
if not comments_to_reply: if not comments_to_reply:
logger.debug(f"说说 {fid} 下的所有评论都已回复过") logger.debug(f"说说 {fid} 下的所有评论都已回复过或无需回复")
return return
logger.info(f"发现自己说说下的 {len(comments_to_reply)} 条新评论,准备回复...") logger.info(f"发现自己说说下的 {len(comments_to_reply)} 条新评论,准备回复...")
@@ -801,6 +803,10 @@ class QZoneService:
"richval": "", "richval": "",
"paramstr": f"@{target_name} {content}", "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) await _request("POST", self.REPLY_URL, params={"g_tk": gtk}, data=data)
return True return True
except Exception as e: except Exception as e:

View File

@@ -7,7 +7,7 @@
import json import json
import time import time
from pathlib import Path from pathlib import Path
from typing import Set, Dict, Any from typing import Set, Dict, Any, Union
from src.common.logger import get_logger from src.common.logger import get_logger
logger = get_logger("MaiZone.ReplyTrackerService") logger = get_logger("MaiZone.ReplyTrackerService")
@@ -22,7 +22,7 @@ class ReplyTrackerService:
def __init__(self): def __init__(self):
# 数据存储路径 # 数据存储路径
self.data_dir = Path(__file__).resolve().parent.parent / "data" 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" self.reply_record_file = self.data_dir / "replied_comments.json"
# 内存中的已回复评论记录 # 内存中的已回复评论记录
@@ -34,32 +34,109 @@ class ReplyTrackerService:
# 加载已有数据 # 加载已有数据
self._load_data() 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): def _load_data(self):
"""从文件加载已回复评论数据""" """从文件加载已回复评论数据"""
try: try:
if self.reply_record_file.exists(): if self.reply_record_file.exists():
try:
with open(self.reply_record_file, "r", encoding="utf-8") as f: with open(self.reply_record_file, "r", encoding="utf-8") as f:
data = json.load(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 self.replied_comments = data
logger.info(f"已加载 {len(self.replied_comments)} 条说说的回复记录") 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: else:
logger.info("未找到回复记录文件,将创建新的记录") logger.info("未找到回复记录文件,将创建新的记录")
except Exception as e:
logger.error(f"加载回复记录失败: {e}")
self.replied_comments = {} self.replied_comments = {}
except Exception as 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): def _save_data(self):
"""保存已回复评论数据到文件""" """保存已回复评论数据到文件"""
try: try:
# 验证数据格式
if not self._validate_data(self.replied_comments):
logger.error("当前数据格式无效,取消保存")
return
# 清理过期数据 # 清理过期数据
self._cleanup_old_records() 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) 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: 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): def _cleanup_old_records(self):
"""清理超过保留期限的记录""" """清理超过保留期限的记录"""
@@ -69,9 +146,11 @@ class ReplyTrackerService:
feeds_to_remove = [] feeds_to_remove = []
total_removed = 0 total_removed = 0
# 仅清理超过保留期限的记录不根据API返回结果清理
for feed_id, comments in self.replied_comments.items(): for feed_id, comments in self.replied_comments.items():
comments_to_remove = [] comments_to_remove = []
# 仅清理超过指定天数的记录
for comment_id, timestamp in comments.items(): for comment_id, timestamp in comments.items():
if timestamp < cutoff_time: if timestamp < cutoff_time:
comments_to_remove.append(comment_id) comments_to_remove.append(comment_id)
@@ -90,47 +169,53 @@ class ReplyTrackerService:
del self.replied_comments[feed_id] del self.replied_comments[feed_id]
if total_removed > 0: 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: Args:
feed_id: 说说ID feed_id: 说说ID
comment_id: 评论ID comment_id: 评论ID (可以是字符串或数字)
Returns: Returns:
bool: 如果已回复过返回True否则返回False bool: 如果已回复过返回True否则返回False
""" """
if not feed_id or not comment_id: if not feed_id or comment_id is None:
return False 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: Args:
feed_id: 说说ID 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 为空,无法标记为已回复") logger.warning("feed_id 或 comment_id 为空,无法标记为已回复")
return return
current_time = time.time() current_time = time.time()
# 确保将comment_id转换为字符串格式
comment_id_str = str(comment_id)
if feed_id not in self.replied_comments: if feed_id not in self.replied_comments:
self.replied_comments[feed_id] = {} self.replied_comments[feed_id] = {}
self.replied_comments[feed_id][comment_id] = current_time self.replied_comments[feed_id][comment_id_str] = current_time
# 保存到文件 # 验证数据并保存到文件
if self._validate_data(self.replied_comments):
self._save_data() self._save_data()
logger.info(f"已标记评论为已回复: feed_id={feed_id}, comment_id={comment_id}") 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]: def get_replied_comments(self, feed_id: str) -> Set[str]:
""" """
@@ -143,7 +228,8 @@ class ReplyTrackerService:
Set[str]: 已回复的评论ID集合 Set[str]: 已回复的评论ID集合
""" """
if feed_id in self.replied_comments: 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() return set()
def get_stats(self) -> Dict[str, Any]: def get_stats(self) -> Dict[str, Any]: