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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user