This commit is contained in:
雷雨
2025-12-15 22:05:56 +08:00
commit 8635b84b2d
230 changed files with 53888 additions and 0 deletions

View File

@@ -0,0 +1,373 @@
import json
import re
import os
import datetime
import logging
from typing import Optional, List, Union
import pyautogui
import time
from pywechat import Systemsettings, NotFolderError, Tools, NoChatHistoryError
from pywechat.WechatAuto import Messages
from pywinauto import mouse
class WeChatClient:
"""
微信客户端
负责微信聊天记录获取和消息发送功能
"""
def __init__(self, default_folder_path: Optional[str] = None):
"""
初始化微信客户端
参数:
- default_folder_path: 默认保存聊天记录的文件夹路径
"""
self.default_folder_path = default_folder_path
self.logger = logging.getLogger(__name__)
# 如果指定了默认文件夹路径,确保它存在
if self.default_folder_path:
try:
os.makedirs(self.default_folder_path, exist_ok=True)
except Exception as e:
self.logger.error(f"创建默认文件夹失败: {self.default_folder_path}, 错误: {e}")
def get_chat_history_by_date(self, friend: str, target_date: str, folder_path: str = None,
search_pages: int = 5, wechat_path: str = None, is_maximize: bool = False,
close_wechat: bool = True, scroll_delay: float = 0.01):
"""
获取特定日期的微信聊天记录
参数:
- friend: 好友或群聊备注或昵称
- target_date: 目标日期,格式为"YY/M/D",如"25/3/22"
- folder_path: 保存聊天记录的文件夹路径
- search_pages: 搜索好友时翻页次数
- wechat_path: 微信可执行文件路径
- is_maximize: 是否最大化窗口
- close_wechat: 完成后是否关闭微信
- scroll_delay: 翻页延迟时间(秒)
返回:
- 聊天记录的JSON字符串
"""
if folder_path is None:
folder_path = self.default_folder_path
if folder_path:
folder_path = re.sub(r'(?<!\\)\\(?!\\)', r'\\\\', folder_path)
if not Systemsettings.is_dirctory(folder_path):
try:
os.makedirs(folder_path, exist_ok=True)
except Exception as e:
raise NotFolderError(f'无法创建文件夹: {folder_path}, 错误: {e}')
try:
match = re.match(r'(\d{2})/(\d{1,2})/(\d{1,2})', target_date)
if match:
year, month, day = map(int, match.groups())
target_datetime = datetime.datetime(2000 + year, month, day)
else:
raise ValueError(f'日期格式不正确: {target_date}, 应为"YY/M/D"格式,如"25/3/22"')
except Exception as e:
raise ValueError(f'日期解析错误: {e}')
def parse_date(date_str):
"""解析微信日期格式为datetime对象"""
try:
today = datetime.datetime.now().date()
match = re.match(r'(\d{2})/(\d{1,2})/(\d{1,2})', date_str)
if match:
year, month, day = map(int, match.groups())
return datetime.datetime(2000 + year, month, day)
if "昨天" in date_str:
yesterday = today - datetime.timedelta(days=1)
return datetime.datetime.combine(yesterday, datetime.time())
if "星期" in date_str:
weekday_map = {
"星期一": 0, "星期二": 1, "星期三": 2, "星期四": 3,
"星期五": 4, "星期六": 5, "星期日": 6, "星期天": 6
}
for weekday_str, weekday_num in weekday_map.items():
if weekday_str in date_str:
current_weekday = today.weekday()
days_diff = weekday_num - current_weekday
if days_diff > 0:
days_diff -= 7
target_date = today + datetime.timedelta(days=days_diff)
return datetime.datetime.combine(target_date, datetime.time())
if re.match(r'^\d{1,2}:\d{2}$', date_str):
return datetime.datetime.combine(today, datetime.time())
return None
except Exception as e:
self.logger.error(f"日期解析错误: {date_str}, {e}")
return None
def get_info(contentList):
"""获取当前页面的聊天信息"""
content = []
messages = contentList.children(title='', control_type='ListItem')
who = [message.descendants(control_type='Text')[0].window_text() for message in messages]
time = [message.descendants(control_type='Text')[1].window_text() for message in messages]
for message in messages:
if message.window_text() == '[图片]':
content.append('图片消息')
elif '视频' in message.window_text():
content.append('视频消息')
elif message.window_text() == '[动画表情]':
content.append('动画表情')
elif message.window_text() == '[文件]':
filename = message.descendants(control_type='Text')[2].texts()[0]
content.append(f'文件:{filename}')
elif '[语音]' in message.window_text():
content.append('语音消息')
else:
texts = message.descendants(control_type='Text')
texts = [text.window_text() for text in texts]
if '微信转账' in texts:
index = texts.index('微信转账')
content.append(f'微信转账:{texts[index - 2]}:{texts[index - 1]}')
else:
content.append(texts[2])
chat_history = list(zip(who, time, content))
return chat_history
chat_history_window = Tools.open_chat_history(friend=friend, wechat_path=wechat_path, is_maximize=is_maximize,
close_wechat=close_wechat, search_pages=search_pages)[0]
rec = chat_history_window.rectangle()
mouse.click(coords=(rec.right - 10, rec.bottom - 10))
pyautogui.press('End')
contentList = chat_history_window.child_window(title='全部', control_type='List')
if not contentList.exists():
chat_history_window.close()
raise NoChatHistoryError(f'你还未与{friend}聊天,无法获取聊天记录')
found_target_date = False
search_count = 0
found_earlier_date = False
self.logger.info(f"开始查找日期: {target_date}")
while not found_target_date:
info = get_info(contentList)
if not info:
break
for record in info:
_, time_str, _ = record
msg_date = parse_date(time_str)
if msg_date:
msg_date_obj = msg_date.date() if hasattr(msg_date, 'date') else msg_date
target_date_obj = target_datetime.date() if hasattr(target_datetime, 'date') else target_datetime
if msg_date_obj == target_date_obj:
found_target_date = True
break
elif msg_date_obj < target_date_obj:
found_earlier_date = True
if not found_target_date:
break
if found_target_date or (found_earlier_date and not found_target_date):
break
pyautogui.keyDown('pageup', _pause=False)
if scroll_delay > 0:
time.sleep(scroll_delay)
search_count += 1
if search_count % 10 == 0:
self.logger.warning(f"未找到目标日期,已翻页{search_count}次,继续查找...")
if not found_target_date:
chat_history_window.close()
self.logger.warning(f"未找到{target_date}的聊天记录,共翻页{search_count}")
return json.dumps([], ensure_ascii=False, indent=4)
self.logger.info(f"开始收集{target_date}的聊天记录")
pyautogui.press('End')
collect_count = 0
target_messages = []
first_date_found = False
date_completed = False
while not date_completed:
info = get_info(contentList)
if not info:
break
current_page_has_target = False
page_has_earlier_date = False
for record in info:
_, time_str, _ = record
msg_date = parse_date(time_str)
if msg_date:
msg_date_obj = msg_date.date() if hasattr(msg_date, 'date') else msg_date
target_date_obj = target_datetime.date() if hasattr(target_datetime, 'date') else target_datetime
if msg_date_obj == target_date_obj:
first_date_found = True
current_page_has_target = True
target_messages.append(record)
elif msg_date_obj < target_date_obj and first_date_found:
page_has_earlier_date = True
if not current_page_has_target and page_has_earlier_date and first_date_found:
self.logger.info(f"已收集完{target_date}的所有聊天记录")
break
# 继续向上翻页
pyautogui.keyDown('pageup', _pause=False)
if scroll_delay > 0:
time.sleep(scroll_delay) # 控制翻页速度
collect_count += 1
if collect_count % 10 == 0:
self.logger.info(f"已翻页{collect_count}次,收集到{len(target_messages)}条消息,继续查找...")
pyautogui.press('End')
target_messages.reverse()
formatted_messages = []
for index, record in enumerate(target_messages):
sender, time_str, message = record
formatted_messages.append({
"index": index,
"发送者": sender,
"时间": time_str,
"消息": message
})
chat_history_json = json.dumps(formatted_messages, ensure_ascii=False, indent=4)
if folder_path:
safe_date = target_date.replace('/', '-')
json_path = os.path.abspath(os.path.join(folder_path, f'{friend}{safe_date}聊天记录.json'))
os.makedirs(os.path.dirname(json_path), exist_ok=True)
with open(json_path, 'w', encoding='utf-8') as f:
f.write(chat_history_json)
self.logger.info(f"已保存JSON到: {json_path}")
chat_history_window.close()
if not formatted_messages:
self.logger.warning(f"未找到{target_date}的聊天记录")
else:
self.logger.info(f"共获取到{len(formatted_messages)}{target_date}的聊天记录")
return chat_history_json
def send_message_to_friend(self, friend: str, message: str, delay: float = 1,
search_pages: int = 0):
"""
向单个好友发送单条消息
参数:
- friend: 好友或群聊备注或昵称
- message: 要发送的消息
- delay: 发送延迟时间(秒)
- search_pages: 搜索好友时翻页次数
返回:
- 发送结果
"""
try:
# 使用 send_messages_to_friend 接口,将单条消息包装成列表
Messages.send_messages_to_friend(
friend=friend,
messages=[message],
delay=delay,
search_pages=search_pages
)
return {"status": "success", "message": f"消息已发送给 {friend}"}
except Exception as e:
return {"status": "error", "message": f"发送消息失败: {str(e)}"}
def send_messages_to_friend(self, friend: str, messages: List[str], delay: float = 1,
search_pages: int = 0):
"""
向单个好友发送多条消息
参数:
- friend: 好友或群聊备注或昵称
- messages: 要发送的消息列表
- delay: 发送延迟时间(秒)
- search_pages: 搜索好友时翻页次数
返回:
- 发送结果
"""
try:
Messages.send_messages_to_friend(
friend=friend,
messages=messages,
delay=delay,
search_pages=search_pages
)
return {"status": "success", "message": f"已向 {friend} 发送 {len(messages)} 条消息"}
except Exception as e:
return {"status": "error", "message": f"发送消息失败: {str(e)}"}
def send_message_to_friends(self, friends: List[str], message: Union[str, List[str]], delay: float = 1):
"""
向多个好友发送消息
参数:
- friends: 好友或群聊备注或昵称列表
- message: 要发送的消息或针对每个好友的消息列表
- delay: 发送延迟时间(秒)
返回:
- 发送结果
"""
try:
Messages.send_message_to_friends(
friends=friends,
message=message,
delay=delay
)
return {"status": "success", "message": f"已向 {len(friends)} 位好友发送消息"}
except Exception as e:
return {"status": "error", "message": f"发送消息失败: {str(e)}"}
def send_messages_to_friends(self, friends: List[str], messages: List[List[str]], delay: int = 1):
"""
向多个好友发送多条消息
参数:
- friends: 好友或群聊备注或昵称列表
- messages: 针对每个好友的消息列表的列表
- delay: 发送延迟时间(秒)
返回:
- 发送结果
"""
try:
Messages.send_messages_to_friends(
friends=friends,
messages=messages,
# delay=delay
)
return {"status": "success", "message": f"已向 {len(friends)} 位好友发送消息"}
except Exception as e:
return {"status": "error", "message": f"发送消息失败: {str(e)}"}

View File

@@ -0,0 +1,311 @@
import json
from typing import Any, Sequence, Dict, List, Optional
from mcp.server import Server
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource, ErrorData, TextResourceContents, \
BlobResourceContents
from mcp.shared.exceptions import McpError
from .WechatClient import WeChatClient
class WeChatServer:
"""
微信服务器
提供微信聊天记录获取和消息发送功能的API接口
"""
def __init__(self, default_folder_path: Optional[str] = None, host: str = "0.0.0.0", port: int = 3000):
"""
初始化微信服务器
参数:
- default_folder_path: 默认保存聊天记录的文件夹路径
- host: SSE服务器监听地址
- port: SSE服务器监听端口
"""
self.wechat_client = WeChatClient(default_folder_path=default_folder_path)
self.host = host
self.port = port
self.sse = SseServerTransport("/messages")
async def serve(self):
"""启动微信服务器SSE模式"""
server = Server("WeChatServer")
@server.list_resources()
async def handle_list_resources():
"""列出可用的微信资源"""
return [
{
"uri": "wechat://chats/history",
"name": "微信聊天记录",
"description": "获取微信聊天记录",
"mimeType": "application/json",
}
]
@server.read_resource()
async def handle_read_resource(uri: str) -> List[TextResourceContents | BlobResourceContents]:
"""读取指定的微信资源"""
if uri.startswith("wechat://"):
return [
TextResourceContents(
uri=uri,
mimeType="application/json",
text=json.dumps({"message": "请使用工具接口获取微信聊天记录"}, ensure_ascii=False)
)
]
raise ValueError(f"不支持的URI: {uri}")
@server.list_tools()
async def list_tools() -> List[Tool]:
"""列出可用的微信工具"""
return [
Tool(
name="wechat_get_chat_history",
description="获取特定日期的微信聊天记录",
inputSchema={
"type": "object",
"properties": {
"to_user": {
"type": "string",
"description": "好友或群聊备注或昵称",
},
"target_date": {
"type": "string",
"description": "目标日期格式为YY/M/D如25/3/22",
},
},
"required": ["to_user", "target_date"],
}
),
Tool(
name="wechat_send_message",
description="向单个微信好友发送单条消息",
inputSchema={
"type": "object",
"properties": {
"to_user": {
"type": "string",
"description": "好友或群聊备注或昵称",
},
"message": {
"type": "string",
"description": "要发送的消息",
}
},
"required": ["to_user", "message"],
}
),
Tool(
name="wechat_send_multiple_messages",
description="向单个微信好友发送多条消息",
inputSchema={
"type": "object",
"properties": {
"to_user": {
"type": "string",
"description": "好友或群聊备注或昵称",
},
"messages": {
"type": "array",
"items": {"type": "string"},
"description": "要发送的消息列表 (用英文逗号分隔的字符串输入)",
}
},
"required": ["to_user", "messages"],
}
),
Tool(
name="wechat_send_to_multiple_friends",
description="向多个微信好友发送单条或者多条消息",
inputSchema={
"type": "object",
"properties": {
"to_user": {
"type": "array",
"items": {"type": "string"},
"description": "好友或群聊备注或昵称列表 (用英文逗号分隔的字符串输入)",
},
"message": {
"type": "string",
"description": "要发送的消息 (单条消息xxx会发给所有好友多条消息xxx,xxx,xxx用英文逗号分隔且数量与好友数相同时将分别发送给对应好友)",
}
},
"required": ["to_user", "message"],
}
)
]
@server.call_tool()
async def call_tool(
name: str, arguments: Dict[str, Any]
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
try:
if name == "wechat_get_chat_history":
friend = arguments.get("to_user")
target_date = arguments.get("target_date")
if not friend or not target_date:
raise ValueError("缺少必要参数: to_user 或 target_date")
folder_path = arguments.get("folder_path")
search_pages = arguments.get("search_pages", 5)
scroll_delay = arguments.get("scroll_delay", 0.01)
chat_history = self.wechat_client.get_chat_history_by_date(
friend=friend,
target_date=target_date,
folder_path=folder_path,
search_pages=search_pages,
scroll_delay=scroll_delay
)
records = json.loads(chat_history)
output = f"获取到 {len(records)} 条与 {friend}{target_date} 的聊天记录\n\n"
for record in records:
output += f"发送者: {record['发送者']}\n"
output += f"时间: {record['时间']}\n"
output += f"消息: {record['消息']}\n"
output += "-" * 30 + "\n"
return [TextContent(type="text", text=output)]
elif name == "wechat_send_message":
friend = arguments.get("to_user")
message = arguments.get("message")
if not friend or not message:
raise ValueError("缺少必要参数: to_user 或 message")
delay = arguments.get("delay", 1.0)
search_pages = arguments.get("search_pages", 0)
result = self.wechat_client.send_message_to_friend(
friend=friend,
message=message,
delay=delay,
search_pages=search_pages
)
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
elif name == "wechat_send_multiple_messages":
friend = arguments.get("to_user")
messages = arguments.get("messages")
if not friend or not messages:
raise ValueError("缺少必要参数: to_user 或 messages")
if isinstance(messages, str):
try:
messages = json.loads(messages)
except json.JSONDecodeError:
for separator in ['', '', ';', '\n']:
messages = messages.replace(separator, ',')
messages = [msg.strip() for msg in messages.split(',')]
messages = [msg for msg in messages if msg]
if not isinstance(messages, list):
messages = [messages]
delay = arguments.get("delay", 1.0)
search_pages = arguments.get("search_pages", 0)
result = self.wechat_client.send_messages_to_friend(
friend=friend,
messages=messages,
delay=delay,
search_pages=search_pages
)
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
elif name == "wechat_send_to_multiple_friends":
friends = arguments.get("to_user")
message = arguments.get("message")
if not friends or not message:
raise ValueError("缺少必要参数: to_user 或 message")
if isinstance(friends, str):
try:
friends = json.loads(friends)
except json.JSONDecodeError:
friends = [f.strip() for f in friends.split(',')]
if not isinstance(friends, list):
friends = [friends]
if isinstance(message, str):
if message.count('","') > 0 and message.count('","') == (len(friends) - 1):
try:
parsed_messages = json.loads(f'[{message}]')
messages = parsed_messages
except json.JSONDecodeError:
messages = []
msg_parts = message.split('","')
for i, part in enumerate(msg_parts):
if i == 0 and part.startswith('"'):
part = part[1:]
if i == len(msg_parts) - 1 and part.endswith('"'):
part = part[:-1]
messages.append(part)
else:
messages = [message] * len(friends)
elif isinstance(message, list):
messages = message
else:
messages = [str(message)] * len(friends)
if len(messages) < len(friends):
last_message = messages[-1] if messages else ""
messages.extend([last_message] * (len(friends) - len(messages)))
elif len(messages) > len(friends):
messages = messages[:len(friends)]
delay = arguments.get("delay", 1.0)
result = self.wechat_client.send_message_to_friends(
friends=friends,
message=messages,
delay=delay
)
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
return [TextContent(type="text", text=f"不支持的工具: {name}")]
except Exception as e:
print(f"工具调用出错: {str(e)}")
error = ErrorData(message=f"微信服务错误: {str(e)}", code=-32603)
raise McpError(error)
# 创建 Starlette 应用
async def handle_sse(request):
async with self.sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
async def handle_messages(request):
await self.sse.handle_post_message(request.scope, request.receive, request._send)
app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse),
Route("/messages", endpoint=handle_messages, methods=["POST"]),
],
)
# 运行服务器
import uvicorn
print(f"MCP WeChat SSE Server running on http://{self.host}:{self.port}")
print(f"SSE endpoint: http://{self.host}:{self.port}/sse")
await uvicorn.Server(
uvicorn.Config(app, host=self.host, port=self.port, log_level="info")
).serve()

View File

@@ -0,0 +1,31 @@
from mcp_server_wechat_sse.WechatServer import WeChatServer
async def serve(default_folder_path=None, host="0.0.0.0", port=3000):
"""启动微信MCP服务器SSE模式"""
server = WeChatServer(default_folder_path=default_folder_path, host=host, port=port)
await server.serve()
def main():
"""提供微信交互功能的MCP服务器"""
import argparse
import asyncio
parser = argparse.ArgumentParser(
description="给模型提供微信聊天记录获取和消息发送功能的MCP服务器SSE模式"
)
parser.add_argument("--folder-path", default="C://Users//Administrator//Documents//mcp_wechat_history",
help="默认保存聊天记录的文件夹路径")
parser.add_argument("--host", default="0.0.0.0",
help="SSE服务器监听地址默认0.0.0.0")
parser.add_argument("--port", type=int, default=3000,
help="SSE服务器监听端口默认3000")
args = parser.parse_args()
asyncio.run(serve(
default_folder_path=args.folder_path,
host=args.host,
port=args.port
))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,3 @@
from mcp_server_wechat_sse import main
main()