Files
PlantAI-dev/wechat_integration.py
T
2026-08-03 18:42:47 +08:00

589 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
"""
WeChat Integration for bbav20311
基于 mcp-wechat-server 的微信集成模块
功能:
- 任务完成通知
- 接收用户指令
- 状态查询
作者: xialab的幸运星
日期: 2026-04-10
"""
import json
import os
import subprocess
import time
import asyncio
from pathlib import Path
from typing import Optional, Dict, Any, Callable
from dataclasses import dataclass
from datetime import datetime
@dataclass
class WeChatConfig:
"""微信配置"""
mcp_server_command: str = "bunx"
mcp_server_args: str = "mcp-wechat-server"
qrcode_path: Path = Path.home() / ".mcp-wechat-server" / "qrcode.txt"
qrcode_png_path: Path = Path.home() / ".mcp-wechat-server" / "qrcode.png"
state_file: Path = Path.home() / ".mcp-wechat-server" / "state.json"
# 任务通知模板
task_complete_template: str = """🎉 任务完成通知
📋 任务ID: {task_id}
🔬 分析类型: {task_type}
⏱️ 耗时: {duration}
📊 状态: {status}
📁 输出文件:
{output_files}
💡 提示: 回复"详情 {task_id}"查看完整结果"""
task_failed_template: str = """❌ 任务失败通知
📋 任务ID: {task_id}
🔬 分析类型: {task_type}
⏱️ 耗时: {duration}
❗ 错误: {error}
请检查输入数据或联系管理员"""
class WeChatIntegration:
"""
微信集成类
使用方法:
1. 初始化: wechat = WeChatIntegration()
2. 登录: await wechat.login()
3. 发送通知: await wechat.notify_task_complete(user_id, task_info)
4. 监听消息: await wechat.start_message_listener(callback)
"""
def __init__(self, config: Optional[WeChatConfig] = None):
self.config = config or WeChatConfig()
self.is_logged_in = False
self.message_cursor = 0
self.use_mock = False # 是否使用模拟模式
async def login(self, timeout: int = 300) -> bool:
"""
登录微信
流程:
1. 调用 login_qrcode 获取二维码
2. 显示二维码供用户扫描
3. 轮询 check_qrcode_status 等待登录
Args:
timeout: 超时时间(秒)
Returns:
bool: 是否登录成功
"""
print("🔐 正在启动微信登录...")
# 步骤1: 获取二维码
print("📱 正在生成登录二维码...")
qrcode_result = await self._call_mcp_tool("login_qrcode", {})
if not qrcode_result:
print("❌ 获取二维码失败")
return False
# 显示二维码
await self._display_qrcode()
# 步骤2: 等待扫码
print("⏳ 请使用微信扫描二维码...")
print(f" 二维码文本: {self.config.qrcode_path}")
print(f" 二维码图片: {self.config.qrcode_png_path}")
# 如果是模拟模式,直接返回成功
if self.use_mock:
print("✅ 模拟登录成功!")
self.is_logged_in = True
return True
start_time = time.time()
while time.time() - start_time < timeout:
status = await self._call_mcp_tool("check_qrcode_status", {})
if status.get("status") == "logged_in":
print("✅ 登录成功!")
self.is_logged_in = True
return True
elif status.get("status") == "expired":
print("⚠️ 二维码已过期,正在重新生成...")
await self._call_mcp_tool("login_qrcode", {})
await self._display_qrcode()
await asyncio.sleep(2)
print("⏰ 登录超时")
return False
async def _display_qrcode(self):
"""显示二维码"""
# 尝试显示文本二维码
if self.config.qrcode_path.exists():
try:
qrcode_text = self.config.qrcode_path.read_text()
print("\n" + "="*40)
print("微信扫码登录:")
print(qrcode_text)
print("="*40 + "\n")
except Exception as e:
print(f" 无法读取文本二维码: {e}")
# 提示图片路径
if self.config.qrcode_png_path.exists():
print(f" 图片二维码: {self.config.qrcode_png_path}")
async def send_text_message(self, user_id: str, message: str) -> bool:
"""
发送文本消息
Args:
user_id: 用户微信ID
message: 消息内容
Returns:
bool: 是否发送成功
"""
if not self.is_logged_in:
print("❌ 未登录,请先调用 login()")
return False
result = await self._call_mcp_tool("send_text_message", {
"user_id": user_id,
"message": message
})
return result.get("success", False)
async def send_typing(self, user_id: str) -> bool:
"""
发送"正在输入..."状态
Args:
user_id: 用户微信ID
Returns:
bool: 是否发送成功
"""
result = await self._call_mcp_tool("send_typing", {
"user_id": user_id
})
return result.get("success", False)
async def notify_task_complete(self, user_id: str, task_info: Dict[str, Any]) -> bool:
"""
发送任务完成通知
Args:
user_id: 用户微信ID
task_info: 任务信息字典
- task_id: 任务ID
- task_type: 任务类型
- duration: 耗时
- status: 状态 (success/failed)
- output_files: 输出文件列表
- error: 错误信息(如果失败)
Returns:
bool: 是否发送成功
"""
if task_info.get("status") == "success":
template = self.config.task_complete_template
else:
template = self.config.task_failed_template
# 格式化输出文件列表
output_files = task_info.get("output_files", [])
if output_files:
files_str = "\n".join([f" • {f}" for f in output_files[:5]]) # 最多显示5个
if len(output_files) > 5:
files_str += f"\n ...还有 {len(output_files) - 5} 个文件"
else:
files_str = " (无输出文件)"
message = template.format(
task_id=task_info.get("task_id", "未知"),
task_type=task_info.get("task_type", "未知"),
duration=task_info.get("duration", "未知"),
status=task_info.get("status", "未知"),
output_files=files_str,
error=task_info.get("error", "")
)
return await self.send_text_message(user_id, message)
async def get_messages(self, timeout: int = 30) -> list:
"""
获取新消息(长轮询)
Args:
timeout: 轮询超时时间(秒)
Returns:
list: 新消息列表
"""
if not self.is_logged_in:
print("❌ 未登录,请先调用 login()")
return []
result = await self._call_mcp_tool("get_messages", {
"cursor": self.message_cursor,
"timeout": timeout
})
messages = result.get("messages", [])
if messages:
# 更新游标
self.message_cursor = result.get("next_cursor", self.message_cursor)
return messages
async def start_message_listener(self, callback: Callable[[Dict], None], poll_interval: int = 5):
"""
启动消息监听循环
Args:
callback: 消息处理回调函数
poll_interval: 轮询间隔(秒)
"""
print("🎧 启动消息监听...")
print(f" 轮询间隔: {poll_interval}秒")
print(" 按 Ctrl+C 停止\n")
try:
while True:
messages = await self.get_messages(timeout=poll_interval)
for msg in messages:
print(f"📨 收到消息: {msg}")
await callback(msg)
await asyncio.sleep(poll_interval)
except KeyboardInterrupt:
print("\n🛑 消息监听已停止")
async def _call_mcp_tool(self, tool_name: str, params: Dict) -> Dict:
"""
调用 MCP 工具
首先尝试使用真实的 mcp-wechat-server,如果失败则回退到模拟模式
"""
try:
# 尝试真实调用
return await self._real_mcp_call(tool_name, params)
except Exception as e:
# 真实调用失败,使用模拟模式
if not self.use_mock:
print(f" ⚠️ MCP 服务器不可用: {e}")
print(" 切换到模拟模式")
self.use_mock = True
return self._mock_call(tool_name, params)
async def _real_mcp_call(self, tool_name: str, params: Dict) -> Dict:
"""真实的 MCP 调用"""
import subprocess
import select
# 构建 MCP 请求 (JSON-RPC 格式)
mcp_request = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": params
},
"id": 1
}
# 调用 mcp-wechat-server
proc = subprocess.Popen(
["bunx", "mcp-wechat-server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 发送请求
request_json = json.dumps(mcp_request) + "\n"
proc.stdin.write(request_json)
proc.stdin.flush()
proc.stdin.close() # 关闭 stdin 表示请求发送完毕
# 读取响应(增加超时到 30 秒)
ready, _, _ = select.select([proc.stdout], [], [], 30)
if ready:
response_line = proc.stdout.readline()
proc.terminate()
try:
response = json.loads(response_line)
if "result" in response:
# 解析 MCP 响应格式
result = response["result"]
if "content" in result and len(result["content"]) > 0:
text_content = result["content"][0].get("text", "")
# 根据工具类型返回不同格式
if tool_name == "login_qrcode":
return {"success": True, "message": text_content}
elif tool_name == "check_qrcode_status":
# 检查是否已登录
if "logged in" in text_content.lower() or "success" in text_content.lower():
return {"status": "logged_in", "message": text_content}
else:
return {"status": "waiting", "message": text_content}
elif tool_name == "send_text_message":
return {"success": "success" in text_content.lower(), "message": text_content}
else:
return {"success": True, "message": text_content}
return result
elif "error" in response:
return {"success": False, "error": response["error"]}
except json.JSONDecodeError:
return {"success": False, "error": "Invalid JSON response"}
else:
proc.terminate()
raise TimeoutError("Timeout waiting for MCP response")
def _mock_call(self, tool_name: str, params: Dict) -> Dict:
"""模拟 MCP 调用(用于测试)"""
if tool_name == "login_qrcode":
# 创建模拟的二维码文件
qrcode_dir = Path.home() / ".mcp-wechat-server"
qrcode_dir.mkdir(exist_ok=True)
# 模拟二维码文本
qrcode_text = "[模拟二维码] 请想象这里有一个微信登录二维码"
(qrcode_dir / "qrcode.txt").write_text(qrcode_text)
return {"success": True, "qrcode_url": "weixin://login/mock"}
elif tool_name == "check_qrcode_status":
# 模拟登录成功
return {"status": "logged_in"}
elif tool_name == "send_text_message":
user_id = params.get('user_id', 'unknown')
message = params.get('message', '')[:50]
print(f" [模拟发送消息给 {user_id}]: {message}...")
return {"success": True}
elif tool_name == "send_typing":
return {"success": True}
elif tool_name == "get_messages":
# 模拟返回消息
return {"messages": [], "next_cursor": self.message_cursor}
return {"success": False, "error": "Unknown tool"}
class WeChatTaskNotifier:
"""
任务通知器 - 与 bbav20311 任务系统集成
使用示例:
notifier = WeChatTaskNotifier(user_wechat_id="wxid_xxx")
await notifier.notify_task_started(task_id="task_001", task_type="identify")
# ... 任务执行 ...
await notifier.notify_task_completed(task_id="task_001", result=...)
"""
def __init__(self, user_wechat_id: str, wechat: Optional[WeChatIntegration] = None):
self.user_wechat_id = user_wechat_id
self.wechat = wechat or WeChatIntegration()
self.task_start_times: Dict[str, datetime] = {}
async def notify_task_started(self, task_id: str, task_type: str, **kwargs):
"""通知任务开始"""
self.task_start_times[task_id] = datetime.now()
message = f"""🚀 任务已启动
📋 任务ID: {task_id}
🔬 分析类型: {task_type}
⏱️ 开始时间: {self.task_start_times[task_id].strftime('%Y-%m-%d %H:%M:%S')}
正在处理中,完成后将通知您..."""
await self.wechat.send_text_message(self.user_wechat_id, message)
async def notify_task_completed(self, task_id: str, output_files: list, **kwargs):
"""通知任务完成"""
start_time = self.task_start_times.get(task_id)
duration = "未知"
if start_time:
delta = datetime.now() - start_time
duration = f"{delta.total_seconds() // 60:.0f}分{delta.total_seconds() % 60:.0f}秒"
task_info = {
"task_id": task_id,
"task_type": kwargs.get("task_type", "未知"),
"duration": duration,
"status": "success",
"output_files": output_files
}
await self.wechat.notify_task_complete(self.user_wechat_id, task_info)
async def notify_task_failed(self, task_id: str, error: str, **kwargs):
"""通知任务失败"""
start_time = self.task_start_times.get(task_id)
duration = "未知"
if start_time:
delta = datetime.now() - start_time
duration = f"{delta.total_seconds() // 60:.0f}分{delta.total_seconds() % 60:.0f}秒"
task_info = {
"task_id": task_id,
"task_type": kwargs.get("task_type", "未知"),
"duration": duration,
"status": "failed",
"output_files": [],
"error": error
}
await self.wechat.notify_task_complete(self.user_wechat_id, task_info)
# ==================== 测试代码 ====================
async def test_wechat_integration():
"""测试微信集成"""
print("="*60)
print("🧪 bbav20311 微信集成测试")
print("="*60)
# 1. 初始化
print("\n📦 步骤1: 初始化微信集成模块")
wechat = WeChatIntegration()
# 2. 登录
print("\n🔐 步骤2: 登录测试")
success = await wechat.login(timeout=10)
if not success:
print("❌ 登录失败,跳过后续测试")
return
# 3. 发送测试消息
print("\n💬 步骤3: 发送测试消息")
test_user_id = "test_user_001"
test_message = "你好!这是来自 bbav20311 的测试消息 🎉"
success = await wechat.send_text_message(test_user_id, test_message)
print(f" 发送结果: {'✅ 成功' if success else '❌ 失败'}")
# 4. 发送任务通知
print("\n📊 步骤4: 发送任务完成通知")
task_info = {
"task_id": "task_20260410_001",
"task_type": "基因家族鉴定 (identify)",
"duration": "5分32秒",
"status": "success",
"output_files": [
"Ft.SPL.identify.gene.list.txt",
"Ft.SPL.identify.protein.fa",
"Ft.SPL.identify.report.pdf"
]
}
success = await wechat.notify_task_complete(test_user_id, task_info)
print(f" 通知结果: {'✅ 成功' if success else '❌ 失败'}")
# 5. 发送失败通知
print("\n❌ 步骤5: 发送任务失败通知")
failed_task_info = {
"task_id": "task_20260410_002",
"task_type": "进化树分析 (phylogeny)",
"duration": "2分15秒",
"status": "failed",
"output_files": [],
"error": "输入文件格式错误: 缺少必需的序列数据"
}
success = await wechat.notify_task_complete(test_user_id, failed_task_info)
print(f" 通知结果: {'✅ 成功' if success else '❌ 失败'}")
# 6. 测试任务通知器
print("\n🤖 步骤6: 测试任务通知器集成")
notifier = WeChatTaskNotifier(user_wechat_id=test_user_id, wechat=wechat)
task_id = "task_20260410_003"
await notifier.notify_task_started(task_id=task_id, task_type="Motif分析")
# 模拟任务执行时间
print(" 模拟任务执行中...")
await asyncio.sleep(2)
await notifier.notify_task_completed(
task_id=task_id,
task_type="Motif分析",
output_files=[
"Ft.SPL.motif.meme.txt",
"Ft.SPL.motif.logo.png"
]
)
print("\n" + "="*60)
print("✅ 所有测试完成!")
print("="*60)
async def test_message_listener():
"""测试消息监听"""
print("\n🎧 测试消息监听(按 Ctrl+C 停止)")
wechat = WeChatIntegration()
await wechat.login(timeout=10)
async def message_handler(msg: Dict):
"""处理收到的消息"""
print(f" 处理消息: {msg}")
# 示例:根据消息内容执行不同操作
content = msg.get("content", "").lower()
if "状态" in content or "status" in content:
# 查询任务状态
reply = "📊 当前系统状态正常,所有服务运行中"
await wechat.send_text_message(msg.get("from"), reply)
elif "帮助" in content or "help" in content:
reply = """🤖 bbav20311 基因家族分析助手
可用命令:
• 状态 - 查询系统状态
• 帮助 - 显示此帮助信息
• 任务列表 - 查看进行中的任务
• 详情 <任务ID> - 查看任务详情
直接发送分析需求即可开始任务!"""
await wechat.send_text_message(msg.get("from"), reply)
await wechat.start_message_listener(callback=message_handler, poll_interval=5)
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "listen":
# 运行消息监听测试
asyncio.run(test_message_listener())
else:
# 运行基础功能测试
asyncio.run(test_wechat_integration())