Skip to content

Commit c3127f7

Browse files
authored
Merge pull request #2562 from josephier/support_wcferry
feat: add support for WeChat integration via the wcferry protocol
2 parents 40b62e9 + e8bc173 commit c3127f7

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ tmp
1414
plugins.json
1515
itchat.pkl
1616
*.log
17+
logs/
1718
user_datas.pkl
1819
chatgpt_tool_hub/
1920
plugins/**/

channel/channel_factory.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ def create_channel(channel_type) -> Channel:
1818
elif channel_type == "wxy":
1919
from channel.wechat.wechaty_channel import WechatyChannel
2020
ch = WechatyChannel()
21+
elif channel_type == "wcf":
22+
from channel.wechat.wcf_channel import WechatfChannel
23+
ch = WechatfChannel()
2124
elif channel_type == "terminal":
2225
from channel.terminal.terminal_channel import TerminalChannel
2326
ch = TerminalChannel()

channel/wechat/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
**本项目接入微信目前有`itchat``wechaty``wechatferry`三种协议,其中前两个协议目前(2025年3月)已经无法使用,遂新增 [WechatFerry](https://github.com/lich0821/WeChatFerry) 协议。**
2+
3+
## WechatFerry 协议
4+
### 准备工作
5+
6+
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)
7+
8+
下载后安装并登录,**关闭系统自动更新** (wx客户端版本降级不影响历史聊天数据)
9+
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)
10+
11+
安装时候记得勾选 `add to path`
12+
13+
### 克隆项目
14+
```
15+
git clone https://github.com/zhayujie/chatgpt-on-wechat
16+
```
17+
如果克隆失败或者无法克隆,可以下载压缩包到本地解压
18+
19+
### 安装依赖
20+
切换到项目更目录,执行下面的命令:
21+
```
22+
pip3 install -r requirements.txt
23+
pip3 install -r requirements-optional.txt
24+
```
25+
### 配置
26+
配置文件的模板在根目录的 `config-template.json` 中,需复制该模板创建最终生效的 `config.json` 文件,执行下面的命令或者手动复制并重命名
27+
```
28+
copy config-template.json config.json
29+
```
30+
设置启动通道:`"channel_type": "wcf"`, 其他配置参考项目[配置说明](https://docs.link-ai.tech/cow/quick-start/config)
31+
32+
### 启动
33+
直接在项目根目录下执行:
34+
```
35+
python3 app.py
36+
```
37+
执行后,正常应会提示”微信登录成功,当前用户xxxx“。
38+
39+
如果执行后无反应,说明python解释器的系统变量不是`python3`, 可以尝试`py app.py`等;如果有报错,请检查版本是否正确,以及自行咨询AI尝试解决。
40+
41+
42+
### ⚠ 免责声明
43+
>1. **本工具为开源项目,仅提供基础功能,供用户进行合法的学习、研究和非商业用途**
44+
禁止将本工具用于任何违法违规行为。
45+
>2. **二次开发者的责任**
46+
> - 任何基于本工具进行的二次开发、修改或衍生产品,其行为及后果由二次开发者独立承担,与本工具原作者无关。
47+
> - **禁止** 使用贡献者的姓名、项目名称或相关信息作为二次开发产品的营销或推广手段。
48+
> - 建议二次开发者在其衍生产品中添加自己的责任声明,明确责任归属。
49+
>3. **用户责任**
50+
> - 使用本工具或其衍生产品的所有后果由用户自行承担,原作者不对因直接或间接使用本工具而导致的任何损失、责任或争议负责。
51+
>4. **法律法规**
52+
> - 用户和二次开发者须遵守《中华人民共和国网络安全法》、《中华人民共和国数据安全法》等相关法律法规。
53+
> - 本工具涉及的所有第三方商标或产品名称,其权利归权利人所有,作者与第三方无任何直接或间接关系。
54+
>5. **作者保留权利** >
55+
> - 本工具原作者保留修改、更新、删除该类工具的权利,无需事先通知或承担任何义务。

channel/wechat/wcf_channel.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# encoding:utf-8
2+
3+
"""
4+
wechat channel
5+
"""
6+
7+
import io
8+
import json
9+
import os
10+
import threading
11+
import time
12+
from queue import Empty
13+
from typing import Any
14+
15+
from bridge.context import *
16+
from bridge.reply import *
17+
from channel.chat_channel import ChatChannel
18+
from channel.wechat.wcf_message import WechatfMessage
19+
from common.log import logger
20+
from common.singleton import singleton
21+
from common.utils import *
22+
from config import conf, get_appdata_dir
23+
from wcferry import Wcf, WxMsg
24+
25+
26+
@singleton
27+
class WechatfChannel(ChatChannel):
28+
NOT_SUPPORT_REPLYTYPE = []
29+
30+
def __init__(self):
31+
super().__init__()
32+
self.NOT_SUPPORT_REPLYTYPE = []
33+
# 使用字典存储最近消息,用于去重
34+
self.received_msgs = {}
35+
# 初始化wcferry客户端
36+
self.wcf = Wcf()
37+
self.wxid = None # 登录后会被设置为当前登录用户的wxid
38+
39+
def startup(self):
40+
"""
41+
启动通道
42+
"""
43+
try:
44+
# wcferry会自动唤起微信并登录
45+
self.wxid = self.wcf.get_self_wxid()
46+
self.name = self.wcf.get_user_info().get("name")
47+
logger.info(f"微信登录成功,当前用户ID: {self.wxid}, 用户名:{self.name}")
48+
self.contact_cache = ContactCache(self.wcf)
49+
self.contact_cache.update()
50+
# 启动消息接收
51+
self.wcf.enable_receiving_msg()
52+
# 创建消息处理线程
53+
t = threading.Thread(target=self._process_messages, name="WeChatThread", daemon=True)
54+
t.start()
55+
56+
57+
except Exception as e:
58+
logger.error(f"微信通道启动失败: {e}")
59+
raise e
60+
61+
def _process_messages(self):
62+
"""
63+
处理消息队列
64+
"""
65+
while True:
66+
try:
67+
msg = self.wcf.get_msg()
68+
if msg:
69+
self._handle_message(msg)
70+
except Empty:
71+
continue
72+
except Exception as e:
73+
logger.error(f"处理消息失败: {e}")
74+
continue
75+
76+
def _handle_message(self, msg: WxMsg):
77+
"""
78+
处理单条消息
79+
"""
80+
try:
81+
# 构造消息对象
82+
cmsg = WechatfMessage(self, msg)
83+
# 消息去重
84+
if cmsg.msg_id in self.received_msgs:
85+
return
86+
self.received_msgs[cmsg.msg_id] = time.time()
87+
# 清理过期消息ID
88+
self._clean_expired_msgs()
89+
90+
logger.debug(f"收到消息: {msg}")
91+
context = self._compose_context(cmsg.ctype, cmsg.content,
92+
isgroup=cmsg.is_group,
93+
msg=cmsg)
94+
if context:
95+
self.produce(context)
96+
except Exception as e:
97+
logger.error(f"处理消息失败: {e}")
98+
99+
def _clean_expired_msgs(self, expire_time: float = 60):
100+
"""
101+
清理过期的消息ID
102+
"""
103+
now = time.time()
104+
for msg_id in list(self.received_msgs.keys()):
105+
if now - self.received_msgs[msg_id] > expire_time:
106+
del self.received_msgs[msg_id]
107+
108+
def send(self, reply: Reply, context: Context):
109+
"""
110+
发送消息
111+
"""
112+
receiver = context["receiver"]
113+
if not receiver:
114+
logger.error("receiver is empty")
115+
return
116+
117+
try:
118+
if reply.type == ReplyType.TEXT:
119+
# 处理@信息
120+
at_list = []
121+
if context.get("isgroup"):
122+
if context["msg"].actual_user_id:
123+
at_list = [context["msg"].actual_user_id]
124+
at_str = ",".join(at_list) if at_list else ""
125+
self.wcf.send_text(reply.content, receiver, at_str)
126+
127+
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
128+
self.wcf.send_text(reply.content, receiver)
129+
else:
130+
logger.error(f"暂不支持的消息类型: {reply.type}")
131+
132+
except Exception as e:
133+
logger.error(f"发送消息失败: {e}")
134+
135+
def close(self):
136+
"""
137+
关闭通道
138+
"""
139+
try:
140+
self.wcf.cleanup()
141+
except Exception as e:
142+
logger.error(f"关闭通道失败: {e}")
143+
144+
145+
class ContactCache:
146+
def __init__(self, wcf):
147+
"""
148+
wcf: 一个 wcfferry.client.Wcf 实例
149+
"""
150+
self.wcf = wcf
151+
self._contact_map = {} # 形如 {wxid: {完整联系人信息}}
152+
153+
def update(self):
154+
"""
155+
更新缓存:调用 get_contacts(),
156+
再把 wcf.contacts 构建成 {wxid: {完整信息}} 的字典
157+
"""
158+
self.wcf.get_contacts()
159+
self._contact_map.clear()
160+
for item in self.wcf.contacts:
161+
wxid = item.get('wxid')
162+
if wxid: # 确保有 wxid 字段
163+
self._contact_map[wxid] = item
164+
165+
def get_contact(self, wxid: str) -> dict:
166+
"""
167+
返回该 wxid 对应的完整联系人 dict,
168+
如果没找到就返回 None
169+
"""
170+
return self._contact_map.get(wxid)
171+
172+
def get_name_by_wxid(self, wxid: str) -> str:
173+
"""
174+
通过wxid,获取成员/群名称
175+
"""
176+
contact = self.get_contact(wxid)
177+
if contact:
178+
return contact.get('name', '')
179+
return ''

channel/wechat/wcf_message.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# encoding:utf-8
2+
3+
"""
4+
wechat channel message
5+
"""
6+
7+
from bridge.context import ContextType
8+
from channel.chat_message import ChatMessage
9+
from common.log import logger
10+
from wcferry import WxMsg
11+
12+
13+
class WechatfMessage(ChatMessage):
14+
"""
15+
微信消息封装类
16+
"""
17+
18+
def __init__(self, channel, wcf_msg: WxMsg, is_group=False):
19+
"""
20+
初始化消息对象
21+
:param wcf_msg: wcferry消息对象
22+
:param is_group: 是否是群消息
23+
"""
24+
super().__init__(wcf_msg)
25+
self.msg_id = wcf_msg.id
26+
self.create_time = wcf_msg.ts # 使用消息时间戳
27+
self.is_group = is_group or wcf_msg._is_group
28+
self.wxid = channel.wxid
29+
self.name = channel.name
30+
31+
# 解析消息类型
32+
if wcf_msg.is_text():
33+
self.ctype = ContextType.TEXT
34+
self.content = wcf_msg.content
35+
else:
36+
raise NotImplementedError(f"Unsupported message type: {wcf_msg.type}")
37+
38+
# 设置发送者和接收者信息
39+
self.from_user_id = self.wxid if wcf_msg.sender == self.wxid else wcf_msg.sender
40+
self.from_user_nickname = self.name if wcf_msg.sender == self.wxid else channel.contact_cache.get_name_by_wxid(wcf_msg.sender)
41+
self.to_user_id = self.wxid
42+
self.to_user_nickname = self.name
43+
self.other_user_id = wcf_msg.sender
44+
self.other_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.sender)
45+
46+
# 群消息特殊处理
47+
if self.is_group:
48+
self.other_user_id = wcf_msg.roomid
49+
self.other_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.roomid)
50+
self.actual_user_id = wcf_msg.sender
51+
self.actual_user_nickname = channel.wcf.get_alias_in_chatroom(wcf_msg.sender, wcf_msg.roomid)
52+
if not self.actual_user_nickname: # 群聊获取不到企微号成员昵称,这里尝试从联系人缓存去获取
53+
self.actual_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.sender)
54+
self.room_id = wcf_msg.roomid
55+
self.is_at = wcf_msg.is_at(self.wxid) # 是否被@当前登录用户
56+
57+
# 判断是否是自己发送的消息
58+
self.my_msg = wcf_msg.from_self()

requirements-optional.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@ zhipuai>=2.0.1
4444

4545
# tongyi qwen new sdk
4646
dashscope
47+
48+
# wechatferry
49+
wcferry==39.4.2.2

0 commit comments

Comments
 (0)