feat:初版
This commit is contained in:
690
plugins/MessageLogger/main.py
Normal file
690
plugins/MessageLogger/main.py
Normal file
@@ -0,0 +1,690 @@
|
||||
"""
|
||||
消息记录插件
|
||||
|
||||
将所有消息存储到MySQL数据库
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
from utils.plugin_base import PluginBase
|
||||
from utils.decorators import (
|
||||
on_text_message,
|
||||
on_image_message,
|
||||
on_voice_message,
|
||||
on_video_message,
|
||||
on_file_message,
|
||||
on_emoji_message
|
||||
)
|
||||
import pymysql
|
||||
from WechatHook import WechatHookClient
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
import uuid
|
||||
import aiohttp
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class MessageLogger(PluginBase):
|
||||
"""消息记录插件"""
|
||||
|
||||
description = "消息记录插件 - 将消息存储到MySQL"
|
||||
author = "ShiHao"
|
||||
version = "1.0.0"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config = None
|
||||
self.db_config = None
|
||||
|
||||
# 创建独立的日志记录器
|
||||
self._setup_logger()
|
||||
|
||||
def _setup_logger(self):
|
||||
"""设置独立的日志记录器"""
|
||||
# 创建日志目录
|
||||
log_dir = Path(__file__).parent.parent.parent / "logs"
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 添加独立的日志文件处理器(使用 filter 来过滤)
|
||||
log_file = log_dir / "message_logger.log"
|
||||
|
||||
# 为这个插件添加一个独立的日志处理器
|
||||
self.logger_id = logger.add(
|
||||
log_file,
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
|
||||
encoding="utf-8",
|
||||
filter=lambda record: record["extra"].get("name") == "MessageLogger"
|
||||
)
|
||||
|
||||
# 创建带标记的 logger
|
||||
self.log = logger.bind(name="MessageLogger")
|
||||
|
||||
async def async_init(self):
|
||||
"""异步初始化"""
|
||||
try:
|
||||
self.log.info("=" * 50)
|
||||
self.log.info("MessageLogger 插件开始初始化...")
|
||||
self.log.info("=" * 50)
|
||||
|
||||
config_path = Path(__file__).parent / "config.toml"
|
||||
if not config_path.exists():
|
||||
self.log.error(f"MessageLogger 配置文件不存在: {config_path}")
|
||||
return
|
||||
|
||||
with open(config_path, "rb") as f:
|
||||
self.config = tomllib.load(f)
|
||||
|
||||
self.db_config = self.config["database"]
|
||||
|
||||
# 初始化 MinIO 客户端
|
||||
self.minio_client = Minio(
|
||||
"101.201.65.129:19000",
|
||||
access_key="admin",
|
||||
secret_key="80012029Lz",
|
||||
secure=False
|
||||
)
|
||||
self.minio_bucket = "wechat"
|
||||
|
||||
# 设置全局实例,供其他地方调用
|
||||
MessageLogger._instance = self
|
||||
self.log.info(f"MessageLogger 全局实例已设置: {self}")
|
||||
|
||||
# 测试数据库连接
|
||||
try:
|
||||
with self.get_db_connection() as conn:
|
||||
self.log.info("MessageLogger 数据库连接测试成功")
|
||||
except Exception as e:
|
||||
self.log.error(f"MessageLogger 数据库连接测试失败: {e}")
|
||||
|
||||
self.log.success("=" * 50)
|
||||
self.log.success("MessageLogger 插件初始化完成!")
|
||||
self.log.success("=" * 50)
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"MessageLogger 插件初始化失败: {e}")
|
||||
import traceback
|
||||
self.log.error(f"详细错误: {traceback.format_exc()}")
|
||||
self.config = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""获取MessageLogger实例"""
|
||||
instance = getattr(cls, '_instance', None)
|
||||
if instance is None:
|
||||
logger.warning("MessageLogger 全局实例为空,可能插件未正确初始化")
|
||||
return instance
|
||||
|
||||
|
||||
def get_db_connection(self):
|
||||
"""获取数据库连接"""
|
||||
return pymysql.connect(
|
||||
host=self.db_config["host"],
|
||||
port=self.db_config["port"],
|
||||
user=self.db_config["user"],
|
||||
password=self.db_config["password"],
|
||||
database=self.db_config["database"],
|
||||
charset=self.db_config["charset"],
|
||||
autocommit=True
|
||||
)
|
||||
|
||||
def extract_image_info(self, raw_msg: str) -> tuple:
|
||||
"""从图片消息中提取 CDN URL 和 AES Key"""
|
||||
try:
|
||||
root = ET.fromstring(raw_msg)
|
||||
img = root.find(".//img")
|
||||
if img is not None:
|
||||
cdnurl = img.get("cdnbigimgurl", "") or img.get("cdnmidimgurl", "")
|
||||
aeskey = img.get("aeskey", "")
|
||||
return (cdnurl, aeskey)
|
||||
except Exception as e:
|
||||
self.log.error(f"提取图片信息失败: {e}")
|
||||
return ("", "")
|
||||
|
||||
def extract_video_info(self, raw_msg: str) -> tuple:
|
||||
"""从视频消息中提取 CDN URL 和 AES Key"""
|
||||
try:
|
||||
root = ET.fromstring(raw_msg)
|
||||
video = root.find(".//videomsg")
|
||||
if video is not None:
|
||||
cdnurl = video.get("cdnvideourl", "")
|
||||
aeskey = video.get("aeskey", "")
|
||||
|
||||
# 如果主要的CDN信息为空,尝试获取原始视频信息
|
||||
if not cdnurl or not aeskey:
|
||||
cdnrawvideourl = video.get("cdnrawvideourl", "")
|
||||
cdnrawvideoaeskey = video.get("cdnrawvideoaeskey", "")
|
||||
if cdnrawvideourl and cdnrawvideoaeskey:
|
||||
self.log.info(f"使用原始视频CDN信息: url={cdnrawvideourl[:50]}..., aeskey={cdnrawvideoaeskey[:20]}...")
|
||||
return (cdnrawvideourl, cdnrawvideoaeskey)
|
||||
|
||||
if cdnurl and aeskey:
|
||||
self.log.info(f"提取视频CDN信息成功: url={cdnurl[:50]}..., aeskey={aeskey[:20]}...")
|
||||
return (cdnurl, aeskey)
|
||||
else:
|
||||
self.log.warning(f"视频CDN信息不完整: cdnurl={'有' if cdnurl else '无'}, aeskey={'有' if aeskey else '无'}")
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"提取视频信息失败: {e}")
|
||||
return ("", "")
|
||||
|
||||
def extract_cdn_url(self, raw_msg: str) -> str:
|
||||
"""从消息中提取 CDN URL(表情包等)"""
|
||||
try:
|
||||
match = re.search(r'cdnurl="([^"]+)"', raw_msg)
|
||||
if match:
|
||||
url = match.group(1).replace("&", "&")
|
||||
return url
|
||||
except Exception as e:
|
||||
self.log.error(f"提取 CDN URL 失败: {e}")
|
||||
return ""
|
||||
|
||||
def extract_file_info(self, raw_msg: str) -> tuple:
|
||||
"""从文件消息中提取文件信息"""
|
||||
try:
|
||||
root = ET.fromstring(raw_msg)
|
||||
appmsg = root.find(".//appmsg")
|
||||
if appmsg is not None:
|
||||
# 提取文件基本信息
|
||||
title = appmsg.find("title")
|
||||
filename = title.text if title is not None else ""
|
||||
|
||||
# 提取文件附件信息
|
||||
appattach = appmsg.find("appattach")
|
||||
if appattach is not None:
|
||||
cdnattachurl = appattach.find("cdnattachurl")
|
||||
aeskey = appattach.find("aeskey")
|
||||
fileext = appattach.find("fileext")
|
||||
totallen = appattach.find("totallen")
|
||||
|
||||
cdn_url = cdnattachurl.text if cdnattachurl is not None else ""
|
||||
aes_key = aeskey.text if aeskey is not None else ""
|
||||
file_ext = fileext.text if fileext is not None else ""
|
||||
file_size = totallen.text if totallen is not None else "0"
|
||||
|
||||
self.log.info(f"提取文件信息: 文件名={filename}, 大小={file_size}字节, 扩展名={file_ext}")
|
||||
return (filename, cdn_url, aes_key, file_ext, file_size)
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"提取文件信息失败: {e}")
|
||||
return ("", "", "", "", "0")
|
||||
|
||||
async def download_image_and_upload(self, bot, cdnurl: str, aeskey: str) -> str:
|
||||
"""下载图片并上传到 MinIO"""
|
||||
try:
|
||||
temp_file = Path(__file__).parent / f"temp_{uuid.uuid4().hex}.jpg"
|
||||
success = await bot.cdn_download(cdnurl, aeskey, str(temp_file), file_type=2)
|
||||
if not success:
|
||||
success = await bot.cdn_download(cdnurl, aeskey, str(temp_file), file_type=1)
|
||||
|
||||
# 等待文件下载完成
|
||||
import asyncio
|
||||
for _ in range(50):
|
||||
if temp_file.exists() and temp_file.stat().st_size > 0:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
if temp_file.exists() and temp_file.stat().st_size > 0:
|
||||
media_url = await self.upload_file_to_minio(str(temp_file), "images")
|
||||
temp_file.unlink()
|
||||
return media_url
|
||||
else:
|
||||
self.log.error("图片下载超时或失败")
|
||||
return ""
|
||||
except Exception as e:
|
||||
self.log.error(f"下载图片并上传失败: {e}")
|
||||
return ""
|
||||
|
||||
async def download_video_and_upload(self, bot, cdnurl: str, aeskey: str) -> str:
|
||||
"""下载视频并上传到 MinIO"""
|
||||
try:
|
||||
temp_file = Path(__file__).parent / f"temp_{uuid.uuid4().hex}.mp4"
|
||||
# file_type=4 是视频
|
||||
success = await bot.cdn_download(cdnurl, aeskey, str(temp_file), file_type=4)
|
||||
|
||||
# 等待文件下载完成(视频较大,等待时间更长)
|
||||
import asyncio
|
||||
for _ in range(100):
|
||||
if temp_file.exists() and temp_file.stat().st_size > 0:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
if temp_file.exists() and temp_file.stat().st_size > 0:
|
||||
media_url = await self.upload_file_to_minio(str(temp_file), "videos")
|
||||
temp_file.unlink()
|
||||
return media_url
|
||||
else:
|
||||
self.log.error("视频下载超时或失败")
|
||||
return ""
|
||||
except Exception as e:
|
||||
self.log.error(f"下载视频并上传失败: {e}")
|
||||
return ""
|
||||
|
||||
async def download_file_and_upload(self, bot, cdnurl: str, aeskey: str, filename: str, file_ext: str) -> str:
|
||||
"""下载文件并上传到 MinIO"""
|
||||
try:
|
||||
# 生成临时文件名,保持原始扩展名
|
||||
if not filename.endswith(f".{file_ext}") and file_ext:
|
||||
temp_filename = f"temp_{uuid.uuid4().hex}.{file_ext}"
|
||||
else:
|
||||
temp_filename = f"temp_{uuid.uuid4().hex}_{filename}"
|
||||
|
||||
temp_file = Path(__file__).parent / temp_filename
|
||||
|
||||
# file_type=5 是文件
|
||||
self.log.info(f"开始下载文件: {filename}")
|
||||
success = await bot.cdn_download(cdnurl, aeskey, str(temp_file), file_type=5)
|
||||
|
||||
# 等待文件下载完成
|
||||
import asyncio
|
||||
downloaded_file = None
|
||||
|
||||
# 等待更长时间,并检查可能的文件路径
|
||||
for i in range(100): # 增加等待时间到10秒
|
||||
# 检查原始路径
|
||||
if temp_file.exists() and temp_file.stat().st_size > 0:
|
||||
downloaded_file = temp_file
|
||||
break
|
||||
|
||||
# 检查可能的其他路径(CDN下载可能会修改文件名)
|
||||
parent_dir = temp_file.parent
|
||||
temp_pattern = temp_file.stem
|
||||
for possible_file in parent_dir.glob(f"*{temp_pattern}*"):
|
||||
if possible_file.exists() and possible_file.stat().st_size > 0:
|
||||
downloaded_file = possible_file
|
||||
self.log.info(f"找到下载的文件: {possible_file}")
|
||||
break
|
||||
|
||||
if downloaded_file:
|
||||
break
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
if downloaded_file and downloaded_file.exists() and downloaded_file.stat().st_size > 0:
|
||||
self.log.info(f"文件下载成功,开始上传到MinIO: {downloaded_file}")
|
||||
media_url = await self.upload_file_to_minio(str(downloaded_file), "files", filename)
|
||||
downloaded_file.unlink()
|
||||
return media_url
|
||||
else:
|
||||
self.log.error(f"文件下载超时或失败: {filename}")
|
||||
# 清理可能存在的临时文件
|
||||
parent_dir = temp_file.parent
|
||||
temp_pattern = temp_file.stem
|
||||
for possible_file in parent_dir.glob(f"*{temp_pattern}*"):
|
||||
if possible_file.exists():
|
||||
self.log.info(f"清理临时文件: {possible_file}")
|
||||
possible_file.unlink()
|
||||
return ""
|
||||
except Exception as e:
|
||||
self.log.error(f"下载文件并上传失败: {e}")
|
||||
return ""
|
||||
|
||||
async def download_and_upload(self, url: str, file_type: str, ext: str) -> str:
|
||||
"""下载文件并上传到 MinIO"""
|
||||
try:
|
||||
# 下载文件
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.read()
|
||||
# 保存到临时文件
|
||||
temp_file = Path(__file__).parent / f"temp_{uuid.uuid4().hex}{ext}"
|
||||
temp_file.write_bytes(data)
|
||||
# 上传
|
||||
media_url = await self.upload_file_to_minio(str(temp_file), file_type)
|
||||
# 删除临时文件
|
||||
temp_file.unlink()
|
||||
return media_url
|
||||
except Exception as e:
|
||||
self.log.error(f"下载并上传文件失败: {e}")
|
||||
return ""
|
||||
|
||||
async def upload_file_to_minio(self, local_file: str, file_type: str, original_filename: str = "") -> str:
|
||||
"""上传文件到 MinIO"""
|
||||
try:
|
||||
# 生成唯一文件名
|
||||
file_ext = Path(local_file).suffix
|
||||
unique_id = uuid.uuid4().hex
|
||||
|
||||
if original_filename:
|
||||
# 使用原始文件名(去掉扩展名)+ 唯一ID + 扩展名
|
||||
original_name = Path(original_filename).stem
|
||||
# 清理文件名中的特殊字符
|
||||
import re
|
||||
original_name = re.sub(r'[^\w\-_\.]', '_', original_name)
|
||||
filename = f"{original_name}_{unique_id}{file_ext}"
|
||||
else:
|
||||
filename = f"{unique_id}{file_ext}"
|
||||
|
||||
object_name = f"{file_type}/{datetime.now().strftime('%Y%m%d')}/{filename}"
|
||||
|
||||
# 上传文件
|
||||
import asyncio
|
||||
await asyncio.to_thread(
|
||||
self.minio_client.fput_object,
|
||||
self.minio_bucket,
|
||||
object_name,
|
||||
local_file
|
||||
)
|
||||
|
||||
# 返回访问 URL
|
||||
url = f"http://101.201.65.129:19000/{self.minio_bucket}/{object_name}"
|
||||
self.log.debug(f"文件上传成功: {url}")
|
||||
return url
|
||||
|
||||
except S3Error as e:
|
||||
self.log.error(f"上传文件到 MinIO 失败: {e}")
|
||||
return ""
|
||||
|
||||
async def save_message(self, message: dict, msg_type: str, bot: WechatHookClient):
|
||||
"""保存消息到数据库"""
|
||||
if not self.config or not self.config["behavior"]["enabled"]:
|
||||
return
|
||||
|
||||
try:
|
||||
sender_wxid = message.get("SenderWxid", "")
|
||||
from_wxid = message.get("FromWxid", "")
|
||||
is_group = message.get("IsGroup", False)
|
||||
content = message.get("Content", "")
|
||||
create_time = message.get("CreateTime", 0)
|
||||
|
||||
# 转换时间戳
|
||||
if create_time:
|
||||
msg_time = datetime.fromtimestamp(create_time)
|
||||
else:
|
||||
msg_time = datetime.now()
|
||||
|
||||
# 获取昵称和头像
|
||||
nickname = ""
|
||||
avatar_url = ""
|
||||
|
||||
if is_group and self.config["behavior"]["fetch_avatar"]:
|
||||
try:
|
||||
self.log.info(f"尝试获取用户信息: from_wxid={from_wxid}, sender_wxid={sender_wxid}")
|
||||
user_info = await bot.get_user_info_in_chatroom(from_wxid, sender_wxid)
|
||||
self.log.info(f"获取到用户信息: {user_info}")
|
||||
|
||||
if user_info:
|
||||
# 处理不同的数据结构
|
||||
if isinstance(user_info.get("nickName"), dict):
|
||||
nickname = user_info.get("nickName", {}).get("string", "")
|
||||
else:
|
||||
nickname = user_info.get("nickName", "")
|
||||
|
||||
avatar_url = user_info.get("bigHeadImgUrl", "")
|
||||
self.log.info(f"解析用户信息: nickname={nickname}, avatar_url={avatar_url[:50]}...")
|
||||
else:
|
||||
self.log.warning(f"用户信息为空: {sender_wxid}")
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"获取用户信息失败: {e}")
|
||||
import traceback
|
||||
self.log.error(f"详细错误: {traceback.format_exc()}")
|
||||
|
||||
# 如果获取失败,从历史记录中查找
|
||||
if not nickname or not avatar_url:
|
||||
self.log.info(f"尝试从历史记录获取用户信息: {sender_wxid}")
|
||||
try:
|
||||
with self.get_db_connection() as conn:
|
||||
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
sql = """
|
||||
SELECT nickname, avatar_url
|
||||
FROM messages
|
||||
WHERE sender_wxid = %s AND nickname != '' AND avatar_url != ''
|
||||
ORDER BY create_time DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(sql, (sender_wxid,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
if not nickname:
|
||||
nickname = result.get("nickname", "")
|
||||
if not avatar_url:
|
||||
avatar_url = result.get("avatar_url", "")
|
||||
self.log.success(f"从历史记录获取成功: nickname={nickname}, avatar_url={avatar_url[:50] if avatar_url else '(空)'}...")
|
||||
except Exception as e:
|
||||
self.log.error(f"从历史记录获取用户信息失败: {e}")
|
||||
elif not is_group and self.config["behavior"]["fetch_avatar"]:
|
||||
# 私聊消息,尝试获取联系人信息
|
||||
try:
|
||||
self.log.info(f"尝试获取私聊用户信息: {sender_wxid}")
|
||||
# 这里可以添加获取私聊用户信息的逻辑
|
||||
# user_info = await bot.get_contact_info(sender_wxid)
|
||||
except Exception as e:
|
||||
self.log.error(f"获取私聊用户信息失败: {e}")
|
||||
|
||||
# 群组ID(如果是群聊)
|
||||
group_id = from_wxid if is_group else None
|
||||
|
||||
# 处理媒体文件上传
|
||||
media_url = ""
|
||||
|
||||
# 表情包消息 - 从 CDN 下载
|
||||
if msg_type == "emoji":
|
||||
cdn_url = self.extract_cdn_url(content)
|
||||
if cdn_url and cdn_url.startswith("http"):
|
||||
media_url = await self.download_and_upload(cdn_url, "emojis", ".gif")
|
||||
|
||||
# 图片消息 - 使用 CDN 下载 API
|
||||
elif msg_type == "image":
|
||||
cdnurl, aeskey = self.extract_image_info(content)
|
||||
if cdnurl and aeskey:
|
||||
media_url = await self.download_image_and_upload(bot, cdnurl, aeskey)
|
||||
|
||||
# 视频消息 - 使用 CDN 下载 API
|
||||
elif msg_type == "video":
|
||||
self.log.info(f"处理视频消息: from={from_wxid}, sender={sender_wxid}")
|
||||
cdnurl, aeskey = self.extract_video_info(content)
|
||||
if cdnurl and aeskey:
|
||||
self.log.info(f"开始下载并上传视频: {cdnurl[:50]}...")
|
||||
media_url = await self.download_video_and_upload(bot, cdnurl, aeskey)
|
||||
if media_url:
|
||||
self.log.success(f"视频上传成功: {media_url}")
|
||||
else:
|
||||
self.log.error("视频上传失败")
|
||||
elif message.get("Video"):
|
||||
self.log.info("使用消息中的视频数据")
|
||||
video_data = message["Video"]
|
||||
temp_file = Path(__file__).parent / f"temp_{uuid.uuid4().hex}.mp4"
|
||||
temp_file.write_bytes(video_data)
|
||||
media_url = await self.upload_file_to_minio(str(temp_file), "videos")
|
||||
temp_file.unlink()
|
||||
else:
|
||||
self.log.warning("视频消息中没有找到可用的CDN信息或视频数据")
|
||||
|
||||
# 语音消息
|
||||
elif msg_type == "voice":
|
||||
voice_data = message.get("ImgBuf", {}).get("buffer")
|
||||
if voice_data:
|
||||
temp_file = Path(__file__).parent / f"temp_{uuid.uuid4().hex}.silk"
|
||||
temp_file.write_bytes(voice_data)
|
||||
media_url = await self.upload_file_to_minio(str(temp_file), "voices")
|
||||
temp_file.unlink()
|
||||
|
||||
# 文件消息 - 使用 CDN 下载 API
|
||||
elif msg_type == "file":
|
||||
self.log.info(f"处理文件消息: from={from_wxid}, sender={sender_wxid}")
|
||||
filename, cdnurl, aeskey, file_ext, file_size = self.extract_file_info(content)
|
||||
if cdnurl and aeskey and filename:
|
||||
self.log.info(f"开始下载并上传文件: {filename} ({file_size}字节)")
|
||||
media_url = await self.download_file_and_upload(bot, cdnurl, aeskey, filename, file_ext)
|
||||
if media_url:
|
||||
self.log.success(f"文件上传成功: {media_url}")
|
||||
else:
|
||||
self.log.error("文件上传失败")
|
||||
elif message.get("File"):
|
||||
self.log.info("使用消息中的文件数据")
|
||||
file_data = message["File"]
|
||||
filename = message.get("Filename", "file")
|
||||
temp_file = Path(__file__).parent / f"temp_{filename}"
|
||||
temp_file.write_bytes(file_data)
|
||||
media_url = await self.upload_file_to_minio(str(temp_file), "files", filename)
|
||||
temp_file.unlink()
|
||||
else:
|
||||
self.log.warning("文件消息中没有找到可用的CDN信息或文件数据")
|
||||
|
||||
# 保存到数据库
|
||||
with self.get_db_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
sql = """
|
||||
INSERT INTO messages
|
||||
(sender_wxid, nickname, avatar_url, content, msg_type,
|
||||
is_group, group_id, media_url, create_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(sql, (
|
||||
sender_wxid,
|
||||
nickname,
|
||||
avatar_url,
|
||||
content,
|
||||
msg_type,
|
||||
int(is_group),
|
||||
group_id,
|
||||
media_url,
|
||||
msg_time
|
||||
))
|
||||
|
||||
self.log.debug(f"消息已保存: {sender_wxid} - {content[:20]}...")
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"保存消息失败: {e}")
|
||||
|
||||
async def save_bot_message(self, to_wxid: str, content: str, msg_type: str = "text", media_url: str = ""):
|
||||
"""保存机器人自身发送的消息"""
|
||||
if not self.config or not self.config["behavior"]["enabled"]:
|
||||
return
|
||||
|
||||
if not self.config["behavior"].get("log_bot_messages", True):
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取机器人信息
|
||||
import tomllib
|
||||
with open("main_config.toml", "rb") as f:
|
||||
main_config = tomllib.load(f)
|
||||
|
||||
bot_config = main_config.get("Bot", {})
|
||||
bot_wxid = bot_config.get("wxid", "bot")
|
||||
bot_nickname = bot_config.get("nickname", "机器人")
|
||||
bot_avatar_url = ""
|
||||
|
||||
# 判断是否是群聊(需要先定义,后面会用到)
|
||||
is_group = to_wxid.endswith("@chatroom")
|
||||
group_id = to_wxid if is_group else None
|
||||
|
||||
# 获取机器人头像(如果启用了头像获取功能)
|
||||
if self.config["behavior"]["fetch_avatar"]:
|
||||
try:
|
||||
self.log.info(f"尝试获取机器人头像: {bot_wxid}")
|
||||
|
||||
# 检查是否有缓存的机器人头像
|
||||
if not hasattr(self, '_bot_avatar_cache'):
|
||||
self._bot_avatar_cache = {}
|
||||
|
||||
if bot_wxid in self._bot_avatar_cache:
|
||||
bot_avatar_url = self._bot_avatar_cache[bot_wxid]
|
||||
self.log.info(f"使用缓存的机器人头像: {bot_avatar_url[:50] if bot_avatar_url else '(空)'}...")
|
||||
else:
|
||||
# 尝试自动获取机器人头像
|
||||
bot_avatar_url = ""
|
||||
|
||||
# 方法1: 优先使用配置中的头像URL(如果有的话)
|
||||
config_avatar = self.config["behavior"].get("bot_avatar_url", "")
|
||||
if config_avatar:
|
||||
bot_avatar_url = config_avatar
|
||||
self.log.info(f"使用配置的机器人头像: {bot_avatar_url}")
|
||||
else:
|
||||
# 方法2: 由于API限制,机器人无法通过get_user_info_in_chatroom获取自己的头像
|
||||
# 我们需要使用其他方法
|
||||
self.log.info("API无法获取机器人自己的头像,建议在配置中设置bot_avatar_url")
|
||||
|
||||
# 可以尝试从主配置获取
|
||||
main_avatar = bot_config.get("avatar_url", "")
|
||||
if main_avatar:
|
||||
bot_avatar_url = main_avatar
|
||||
self.log.info(f"从主配置获取机器人头像: {bot_avatar_url}")
|
||||
|
||||
# 缓存头像URL(即使为空也缓存,避免重复尝试)
|
||||
self._bot_avatar_cache[bot_wxid] = bot_avatar_url
|
||||
self.log.info(f"最终机器人头像URL: {bot_avatar_url if bot_avatar_url else '(空)'}")
|
||||
|
||||
except Exception as e:
|
||||
self.log.warning(f"获取机器人头像失败: {e}")
|
||||
bot_avatar_url = ""
|
||||
|
||||
# 保存到数据库
|
||||
with self.get_db_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
sql = """
|
||||
INSERT INTO messages
|
||||
(sender_wxid, nickname, avatar_url, content, msg_type,
|
||||
is_group, group_id, media_url, create_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(sql, (
|
||||
bot_wxid,
|
||||
bot_nickname,
|
||||
bot_avatar_url, # 使用获取到的机器人头像URL
|
||||
content,
|
||||
msg_type,
|
||||
int(is_group),
|
||||
group_id,
|
||||
media_url,
|
||||
datetime.now()
|
||||
))
|
||||
|
||||
self.log.debug(f"机器人消息已保存: {bot_wxid} -> {to_wxid} - {content[:20]}...")
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"保存机器人消息失败: {e}")
|
||||
|
||||
@on_text_message(priority=10)
|
||||
async def handle_text(self, bot: WechatHookClient, message: dict):
|
||||
"""处理文本消息"""
|
||||
self.log.info(f"[MessageLogger] 收到文本消息: {message.get('Content', '')[:20]}")
|
||||
if self.config and self.config["behavior"]["log_text"]:
|
||||
asyncio.create_task(self.save_message(message, "text", bot))
|
||||
return True
|
||||
|
||||
@on_image_message(priority=10)
|
||||
async def handle_image(self, bot: WechatHookClient, message: dict):
|
||||
"""处理图片消息"""
|
||||
self.log.info(f"[MessageLogger] 收到图片消息: {message.get('FromWxid')}")
|
||||
if self.config and self.config["behavior"]["log_image"]:
|
||||
asyncio.create_task(self.save_message(message, "image", bot))
|
||||
return True
|
||||
|
||||
@on_voice_message(priority=10)
|
||||
async def handle_voice(self, bot: WechatHookClient, message: dict):
|
||||
"""处理语音消息"""
|
||||
if self.config and self.config["behavior"]["log_voice"]:
|
||||
asyncio.create_task(self.save_message(message, "voice", bot))
|
||||
return True
|
||||
|
||||
@on_video_message(priority=10)
|
||||
async def handle_video(self, bot: WechatHookClient, message: dict):
|
||||
"""处理视频消息"""
|
||||
self.log.info(f"MessageLogger 收到视频消息: {message.get('FromWxid')}")
|
||||
if self.config and self.config["behavior"]["log_video"]:
|
||||
asyncio.create_task(self.save_message(message, "video", bot))
|
||||
return True
|
||||
|
||||
@on_file_message(priority=10)
|
||||
async def handle_file(self, bot: WechatHookClient, message: dict):
|
||||
"""处理文件消息"""
|
||||
if self.config and self.config["behavior"]["log_file"]:
|
||||
asyncio.create_task(self.save_message(message, "file", bot))
|
||||
return True
|
||||
|
||||
@on_emoji_message(priority=10)
|
||||
async def handle_emoji(self, bot: WechatHookClient, message: dict):
|
||||
"""处理表情包消息"""
|
||||
if self.config and self.config["behavior"]["log_emoji"]:
|
||||
asyncio.create_task(self.save_message(message, "emoji", bot))
|
||||
return True
|
||||
Reference in New Issue
Block a user