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 ''
0 commit comments