Merge pull request #2562 from josephier/support_wcferry

feat: add support for WeChat integration via the wcferry protocol
This commit is contained in:
vision
2025-04-09 18:51:01 +08:00
committed by GitHub
6 changed files with 299 additions and 0 deletions

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ tmp
plugins.json
itchat.pkl
*.log
logs/
user_datas.pkl
chatgpt_tool_hub/
plugins/**/

View File

@@ -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()

55
channel/wechat/README.md Normal file
View File

@@ -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. **作者保留权利** >
> - 本工具原作者保留修改、更新、删除该类工具的权利,无需事先通知或承担任何义务。

View File

@@ -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 ''

View File

@@ -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()

View File

@@ -44,3 +44,6 @@ zhipuai>=2.0.1
# tongyi qwen new sdk
dashscope
# wechatferry
wcferry==39.4.2.2