Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ tmp
plugins.json
itchat.pkl
*.log
logs/
user_datas.pkl
chatgpt_tool_hub/
plugins/**/
Expand Down
3 changes: 3 additions & 0 deletions channel/channel_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
55 changes: 55 additions & 0 deletions channel/wechat/README.md
Original file line number Diff line number Diff line change
@@ -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. **作者保留权利** >
> - 本工具原作者保留修改、更新、删除该类工具的权利,无需事先通知或承担任何义务。
179 changes: 179 additions & 0 deletions channel/wechat/wcf_channel.py
Original file line number Diff line number Diff line change
@@ -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 ''
58 changes: 58 additions & 0 deletions channel/wechat/wcf_message.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 3 additions & 0 deletions requirements-optional.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ zhipuai>=2.0.1

# tongyi qwen new sdk
dashscope

# wechatferry
wcferry==39.4.2.2