diff --git a/.gitignore b/.gitignore index 560e615..dd4fdeb 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ tmp plugins.json itchat.pkl *.log +logs/ user_datas.pkl chatgpt_tool_hub/ plugins/**/ diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 43a8be3..50e1756 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -18,6 +18,9 @@ def create_channel(channel_type) -> Channel: elif channel_type == "wxy": from channel.wechat.wechaty_channel import WechatyChannel ch = WechatyChannel() + elif channel_type == "wcf": + from channel.wechat.wcf_channel import WechatfChannel + ch = WechatfChannel() elif channel_type == "terminal": from channel.terminal.terminal_channel import TerminalChannel ch = TerminalChannel() diff --git a/channel/wechat/README.md b/channel/wechat/README.md new file mode 100644 index 0000000..11c4082 --- /dev/null +++ b/channel/wechat/README.md @@ -0,0 +1,55 @@ +**本项目接入微信目前有`itchat`、`wechaty`、`wechatferry`三种协议,其中前两个协议目前(2025年3月)已经无法使用,遂新增 [WechatFerry](https://github.com/lich0821/WeChatFerry) 协议。** + +## WechatFerry 协议 +### 准备工作 + +1. 使用该协议接入微信,需要使用特定版本的`windows`客户端,具体因协议的版本而异,目前使用的是`wcferry == 39.4.1.0`,对应的wx客户端版本为`3.9.12.17`,[下载链接](https://github.com/lich0821/WeChatFerry/releases/download/v39.4.1/WeChatSetup-3.9.12.17.exe) + + 下载后安装并登录,**关闭系统自动更新** (wx客户端版本降级不影响历史聊天数据) +2. python版本:`Python>=3.9`,建议3.9或3.10即可,[3.10.10下载链接](https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe) , + + 安装时候记得勾选 `add to path`。 + +### 克隆项目 +``` +git clone https://github.com/zhayujie/chatgpt-on-wechat +``` +如果克隆失败或者无法克隆,可以下载压缩包到本地解压 + +### 安装依赖 +切换到项目更目录,执行下面的命令: +``` +pip3 install -r requirements.txt +pip3 install -r requirements-optional.txt +``` +### 配置 +配置文件的模板在根目录的 `config-template.json` 中,需复制该模板创建最终生效的 `config.json` 文件,执行下面的命令或者手动复制并重命名 +``` +copy config-template.json config.json +``` +设置启动通道:`"channel_type": "wcf"`, 其他配置参考项目[配置说明](https://docs.link-ai.tech/cow/quick-start/config) + +### 启动 +直接在项目根目录下执行: +``` +python3 app.py +``` +执行后,正常应会提示”微信登录成功,当前用户xxxx“。 + +如果执行后无反应,说明python解释器的系统变量不是`python3`, 可以尝试`py app.py`等;如果有报错,请检查版本是否正确,以及自行咨询AI尝试解决。 + + +### ⚠ 免责声明 +>1. **本工具为开源项目,仅提供基础功能,供用户进行合法的学习、研究和非商业用途**。 + 禁止将本工具用于任何违法违规行为。 +>2. **二次开发者的责任** +> - 任何基于本工具进行的二次开发、修改或衍生产品,其行为及后果由二次开发者独立承担,与本工具原作者无关。 +> - **禁止** 使用贡献者的姓名、项目名称或相关信息作为二次开发产品的营销或推广手段。 +> - 建议二次开发者在其衍生产品中添加自己的责任声明,明确责任归属。 +>3. **用户责任** +> - 使用本工具或其衍生产品的所有后果由用户自行承担,原作者不对因直接或间接使用本工具而导致的任何损失、责任或争议负责。 +>4. **法律法规** +> - 用户和二次开发者须遵守《中华人民共和国网络安全法》、《中华人民共和国数据安全法》等相关法律法规。 +> - 本工具涉及的所有第三方商标或产品名称,其权利归权利人所有,作者与第三方无任何直接或间接关系。 +>5. **作者保留权利** > +> - 本工具原作者保留修改、更新、删除该类工具的权利,无需事先通知或承担任何义务。 diff --git a/channel/wechat/wcf_channel.py b/channel/wechat/wcf_channel.py new file mode 100644 index 0000000..af5468b --- /dev/null +++ b/channel/wechat/wcf_channel.py @@ -0,0 +1,179 @@ +# encoding:utf-8 + +""" +wechat channel +""" + +import io +import json +import os +import threading +import time +from queue import Empty +from typing import Any + +from bridge.context import * +from bridge.reply import * +from channel.chat_channel import ChatChannel +from channel.wechat.wcf_message import WechatfMessage +from common.log import logger +from common.singleton import singleton +from common.utils import * +from config import conf, get_appdata_dir +from wcferry import Wcf, WxMsg + + +@singleton +class WechatfChannel(ChatChannel): + NOT_SUPPORT_REPLYTYPE = [] + + def __init__(self): + super().__init__() + self.NOT_SUPPORT_REPLYTYPE = [] + # 使用字典存储最近消息,用于去重 + self.received_msgs = {} + # 初始化wcferry客户端 + self.wcf = Wcf() + self.wxid = None # 登录后会被设置为当前登录用户的wxid + + def startup(self): + """ + 启动通道 + """ + try: + # wcferry会自动唤起微信并登录 + self.wxid = self.wcf.get_self_wxid() + self.name = self.wcf.get_user_info().get("name") + logger.info(f"微信登录成功,当前用户ID: {self.wxid}, 用户名:{self.name}") + self.contact_cache = ContactCache(self.wcf) + self.contact_cache.update() + # 启动消息接收 + self.wcf.enable_receiving_msg() + # 创建消息处理线程 + t = threading.Thread(target=self._process_messages, name="WeChatThread", daemon=True) + t.start() + + + except Exception as e: + logger.error(f"微信通道启动失败: {e}") + raise e + + def _process_messages(self): + """ + 处理消息队列 + """ + while True: + try: + msg = self.wcf.get_msg() + if msg: + self._handle_message(msg) + except Empty: + continue + except Exception as e: + logger.error(f"处理消息失败: {e}") + continue + + def _handle_message(self, msg: WxMsg): + """ + 处理单条消息 + """ + try: + # 构造消息对象 + cmsg = WechatfMessage(self, msg) + # 消息去重 + if cmsg.msg_id in self.received_msgs: + return + self.received_msgs[cmsg.msg_id] = time.time() + # 清理过期消息ID + self._clean_expired_msgs() + + logger.debug(f"收到消息: {msg}") + context = self._compose_context(cmsg.ctype, cmsg.content, + isgroup=cmsg.is_group, + msg=cmsg) + if context: + self.produce(context) + except Exception as e: + logger.error(f"处理消息失败: {e}") + + def _clean_expired_msgs(self, expire_time: float = 60): + """ + 清理过期的消息ID + """ + now = time.time() + for msg_id in list(self.received_msgs.keys()): + if now - self.received_msgs[msg_id] > expire_time: + del self.received_msgs[msg_id] + + def send(self, reply: Reply, context: Context): + """ + 发送消息 + """ + receiver = context["receiver"] + if not receiver: + logger.error("receiver is empty") + return + + try: + if reply.type == ReplyType.TEXT: + # 处理@信息 + at_list = [] + if context.get("isgroup"): + if context["msg"].actual_user_id: + at_list = [context["msg"].actual_user_id] + at_str = ",".join(at_list) if at_list else "" + self.wcf.send_text(reply.content, receiver, at_str) + + elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: + self.wcf.send_text(reply.content, receiver) + else: + logger.error(f"暂不支持的消息类型: {reply.type}") + + except Exception as e: + logger.error(f"发送消息失败: {e}") + + def close(self): + """ + 关闭通道 + """ + try: + self.wcf.cleanup() + except Exception as e: + logger.error(f"关闭通道失败: {e}") + + +class ContactCache: + def __init__(self, wcf): + """ + wcf: 一个 wcfferry.client.Wcf 实例 + """ + self.wcf = wcf + self._contact_map = {} # 形如 {wxid: {完整联系人信息}} + + def update(self): + """ + 更新缓存:调用 get_contacts(), + 再把 wcf.contacts 构建成 {wxid: {完整信息}} 的字典 + """ + self.wcf.get_contacts() + self._contact_map.clear() + for item in self.wcf.contacts: + wxid = item.get('wxid') + if wxid: # 确保有 wxid 字段 + self._contact_map[wxid] = item + + def get_contact(self, wxid: str) -> dict: + """ + 返回该 wxid 对应的完整联系人 dict, + 如果没找到就返回 None + """ + return self._contact_map.get(wxid) + + def get_name_by_wxid(self, wxid: str) -> str: + """ + 通过wxid,获取成员/群名称 + """ + contact = self.get_contact(wxid) + if contact: + return contact.get('name', '') + return '' \ No newline at end of file diff --git a/channel/wechat/wcf_message.py b/channel/wechat/wcf_message.py new file mode 100644 index 0000000..827a578 --- /dev/null +++ b/channel/wechat/wcf_message.py @@ -0,0 +1,58 @@ +# encoding:utf-8 + +""" +wechat channel message +""" + +from bridge.context import ContextType +from channel.chat_message import ChatMessage +from common.log import logger +from wcferry import WxMsg + + +class WechatfMessage(ChatMessage): + """ + 微信消息封装类 + """ + + def __init__(self, channel, wcf_msg: WxMsg, is_group=False): + """ + 初始化消息对象 + :param wcf_msg: wcferry消息对象 + :param is_group: 是否是群消息 + """ + super().__init__(wcf_msg) + self.msg_id = wcf_msg.id + self.create_time = wcf_msg.ts # 使用消息时间戳 + self.is_group = is_group or wcf_msg._is_group + self.wxid = channel.wxid + self.name = channel.name + + # 解析消息类型 + if wcf_msg.is_text(): + self.ctype = ContextType.TEXT + self.content = wcf_msg.content + else: + raise NotImplementedError(f"Unsupported message type: {wcf_msg.type}") + + # 设置发送者和接收者信息 + self.from_user_id = self.wxid if wcf_msg.sender == self.wxid else wcf_msg.sender + self.from_user_nickname = self.name if wcf_msg.sender == self.wxid else channel.contact_cache.get_name_by_wxid(wcf_msg.sender) + self.to_user_id = self.wxid + self.to_user_nickname = self.name + self.other_user_id = wcf_msg.sender + self.other_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.sender) + + # 群消息特殊处理 + if self.is_group: + self.other_user_id = wcf_msg.roomid + self.other_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.roomid) + self.actual_user_id = wcf_msg.sender + self.actual_user_nickname = channel.wcf.get_alias_in_chatroom(wcf_msg.sender, wcf_msg.roomid) + if not self.actual_user_nickname: # 群聊获取不到企微号成员昵称,这里尝试从联系人缓存去获取 + self.actual_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.sender) + self.room_id = wcf_msg.roomid + self.is_at = wcf_msg.is_at(self.wxid) # 是否被@当前登录用户 + + # 判断是否是自己发送的消息 + self.my_msg = wcf_msg.from_self() diff --git a/requirements-optional.txt b/requirements-optional.txt index f158d33..7a823b5 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -44,3 +44,6 @@ zhipuai>=2.0.1 # tongyi qwen new sdk dashscope + +# wechatferry +wcferry==39.4.2.2