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(
"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)

View File

@@ -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:

View File

@@ -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():
try:
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
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:
logger.info("未找到回复记录文件,将创建新的记录")
except Exception as e:
logger.error(f"加载回复记录失败: {e}")
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):
"""保存已回复评论数据到文件"""
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
# 保存到文件
# 验证数据并保存到文件
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]: