-
Notifications
You must be signed in to change notification settings - Fork 918
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
init: add wechat and wechat_mp channel
- Loading branch information
0 parents
commit b13627a
Showing
18 changed files
with
664 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.DS_Store | ||
.idea | ||
__pycache__/ | ||
venv* | ||
*.pyc | ||
config.json | ||
QR.png |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Copyright (c) 2023 zhayujie | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# 简介 | ||
|
||
将 **AI模型** 接入各类 **消息应用**,开发者通过轻量配置即可在二者之间选择一条连线,运行起一个智能对话机器人,在一个项目中轻松完成多条链路的切换。该架构扩展性强,每接入一个应用可复用已有的算法能力,同样每接入一个模型也可作用于所有应用之上。 | ||
|
||
**模型:** | ||
|
||
- [x] ChatGPT | ||
- ... | ||
|
||
**应用:** | ||
|
||
- [ ] 终端 | ||
- [ ] Web | ||
- [x] 个人微信 | ||
- [x] 公众号 | ||
- [ ] 企业微信 | ||
- [ ] Telegram | ||
- [ ] 钉钉 | ||
- ... | ||
|
||
|
||
|
||
# 快速开始 | ||
|
||
## 一、准备 | ||
|
||
### 1.运行环境 | ||
|
||
支持 Linux、MacOS、Windows 系统(Linux服务器上可长期运行)。同时需安装 Python,建议Python版本在 3.7.1~3.10 之间。 | ||
|
||
项目代码克隆: | ||
|
||
```bash | ||
git clone https://github.com/zhayujie/bot-on-anything | ||
cd bot-on-anything/ | ||
``` | ||
> 或在 Realase 直接手动下载源码。 | ||
### 2.配置说明 | ||
|
||
核心配置文件为 `config.json`, | ||
|
||
|
||
|
||
## 二、选择模型 | ||
|
||
### 1.ChatGPT | ||
|
||
|
||
## 三、选择应用 | ||
|
||
### 1.微信 | ||
|
||
|
||
### 2.公众号 | ||
|
||
|
||
|
||
|
||
## 四、运行 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# encoding:utf-8 | ||
|
||
import config | ||
from channel import channel_factory | ||
from common.log import logger | ||
|
||
|
||
if __name__ == '__main__': | ||
try: | ||
# load config | ||
config.load_config() | ||
logger.info("[INIT] load config: {}".format(config.conf())) | ||
|
||
# create channel | ||
channel = channel_factory.create_channel(config.conf().get("channel")) | ||
|
||
# startup channel | ||
channel.startup() | ||
except Exception as e: | ||
logger.error("App startup failed!") | ||
logger.exception(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from model import model_factory | ||
import config | ||
|
||
class Bridge(object): | ||
def __init__(self): | ||
pass | ||
|
||
def fetch_reply_content(self, query, context): | ||
return model_factory.create_bot(config.conf().get("model")).reply(query, context) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
""" | ||
Message sending channel abstract class | ||
""" | ||
|
||
from bridge.bridge import Bridge | ||
|
||
class Channel(object): | ||
def startup(self): | ||
""" | ||
init channel | ||
""" | ||
raise NotImplementedError | ||
|
||
def handle(self, msg): | ||
""" | ||
process received msg | ||
:param msg: message object | ||
""" | ||
raise NotImplementedError | ||
|
||
def send(self, msg, receiver): | ||
""" | ||
send message to user | ||
:param msg: message content | ||
:param receiver: receiver channel account | ||
:return: | ||
""" | ||
raise NotImplementedError | ||
|
||
def build_reply_content(self, query, context=None): | ||
return Bridge().fetch_reply_content(query, context) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
""" | ||
channel factory | ||
""" | ||
from common import const | ||
|
||
def create_channel(channel_type): | ||
""" | ||
create a channel instance | ||
:param channel_type: channel type code | ||
:return: channel instance | ||
""" | ||
if channel_type == const.WECHAT: | ||
from channel.wechat.wechat_channel import WechatChannel | ||
return WechatChannel() | ||
|
||
elif channel_type == const.WECHAT_MP: | ||
from channel.wechat.wechat_mp_channel import WechatPublicAccount | ||
return WechatPublicAccount() | ||
|
||
else: | ||
raise RuntimeError |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
# encoding:utf-8 | ||
|
||
""" | ||
wechat channel | ||
""" | ||
import itchat | ||
import json | ||
from itchat.content import * | ||
from channel.channel import Channel | ||
from concurrent.futures import ThreadPoolExecutor | ||
from common.log import logger | ||
from config import conf | ||
import requests | ||
import io | ||
|
||
thread_pool = ThreadPoolExecutor(max_workers=8) | ||
|
||
|
||
@itchat.msg_register(TEXT) | ||
def handler_single_msg(msg): | ||
WechatChannel().handle(msg) | ||
return None | ||
|
||
|
||
@itchat.msg_register(TEXT, isGroupChat=True) | ||
def handler_group_msg(msg): | ||
WechatChannel().handle_group(msg) | ||
return None | ||
|
||
|
||
class WechatChannel(Channel): | ||
def __init__(self): | ||
pass | ||
|
||
def startup(self): | ||
# login by scan QRCode | ||
itchat.auto_login(enableCmdQR=2) | ||
|
||
# start message listener | ||
itchat.run() | ||
|
||
def handle(self, msg): | ||
logger.debug("[WX]receive msg: " + json.dumps(msg, ensure_ascii=False)) | ||
from_user_id = msg['FromUserName'] | ||
to_user_id = msg['ToUserName'] # 接收人id | ||
other_user_id = msg['User']['UserName'] # 对手方id | ||
content = msg['Text'] | ||
match_prefix = self.check_prefix(content, conf().get('single_chat_prefix')) | ||
if from_user_id == other_user_id and match_prefix is not None: | ||
# 好友向自己发送消息 | ||
if match_prefix != '': | ||
str_list = content.split(match_prefix, 1) | ||
if len(str_list) == 2: | ||
content = str_list[1].strip() | ||
|
||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix')) | ||
if img_match_prefix: | ||
content = content.split(img_match_prefix, 1)[1].strip() | ||
thread_pool.submit(self._do_send_img, content, from_user_id) | ||
else: | ||
thread_pool.submit(self._do_send, content, from_user_id) | ||
|
||
elif to_user_id == other_user_id and match_prefix: | ||
# 自己给好友发送消息 | ||
str_list = content.split(match_prefix, 1) | ||
if len(str_list) == 2: | ||
content = str_list[1].strip() | ||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix')) | ||
if img_match_prefix: | ||
content = content.split(img_match_prefix, 1)[1].strip() | ||
thread_pool.submit(self._do_send_img, content, to_user_id) | ||
else: | ||
thread_pool.submit(self._do_send, content, to_user_id) | ||
|
||
|
||
def handle_group(self, msg): | ||
logger.debug("[WX]receive group msg: " + json.dumps(msg, ensure_ascii=False)) | ||
group_name = msg['User'].get('NickName', None) | ||
group_id = msg['User'].get('UserName', None) | ||
if not group_name: | ||
return "" | ||
origin_content = msg['Content'] | ||
content = msg['Content'] | ||
content_list = content.split(' ', 1) | ||
context_special_list = content.split('\u2005', 1) | ||
if len(context_special_list) == 2: | ||
content = context_special_list[1] | ||
elif len(content_list) == 2: | ||
content = content_list[1] | ||
|
||
config = conf() | ||
match_prefix = (msg['IsAt'] and not config.get("group_at_off", False)) or self.check_prefix(origin_content, config.get('group_chat_prefix')) \ | ||
or self.check_contain(origin_content, config.get('group_chat_keyword')) | ||
if ('ALL_GROUP' in config.get('group_name_white_list') or group_name in config.get('group_name_white_list') or self.check_contain(group_name, config.get('group_name_keyword_white_list'))) and match_prefix: | ||
img_match_prefix = self.check_prefix(content, conf().get('image_create_prefix')) | ||
if img_match_prefix: | ||
content = content.split(img_match_prefix, 1)[1].strip() | ||
thread_pool.submit(self._do_send_img, content, group_id) | ||
else: | ||
thread_pool.submit(self._do_send_group, content, msg) | ||
|
||
def send(self, msg, receiver): | ||
logger.info('[WX] sendMsg={}, receiver={}'.format(msg, receiver)) | ||
itchat.send(msg, toUserName=receiver) | ||
|
||
def _do_send(self, query, reply_user_id): | ||
try: | ||
if not query: | ||
return | ||
context = dict() | ||
context['from_user_id'] = reply_user_id | ||
reply_text = super().build_reply_content(query, context) | ||
if reply_text: | ||
self.send(conf().get("single_chat_reply_prefix") + reply_text, reply_user_id) | ||
except Exception as e: | ||
logger.exception(e) | ||
|
||
def _do_send_img(self, query, reply_user_id): | ||
try: | ||
if not query: | ||
return | ||
context = dict() | ||
context['type'] = 'IMAGE_CREATE' | ||
img_url = super().build_reply_content(query, context) | ||
if not img_url: | ||
return | ||
|
||
# 图片下载 | ||
pic_res = requests.get(img_url, stream=True) | ||
image_storage = io.BytesIO() | ||
for block in pic_res.iter_content(1024): | ||
image_storage.write(block) | ||
image_storage.seek(0) | ||
|
||
# 图片发送 | ||
logger.info('[WX] sendImage, receiver={}'.format(reply_user_id)) | ||
itchat.send_image(image_storage, reply_user_id) | ||
except Exception as e: | ||
logger.exception(e) | ||
|
||
def _do_send_group(self, query, msg): | ||
if not query: | ||
return | ||
context = dict() | ||
context['from_user_id'] = msg['ActualUserName'] | ||
reply_text = super().build_reply_content(query, context) | ||
if reply_text: | ||
reply_text = '@' + msg['ActualNickName'] + ' ' + reply_text.strip() | ||
self.send(conf().get("group_chat_reply_prefix", "") + reply_text, msg['User']['UserName']) | ||
|
||
|
||
def check_prefix(self, content, prefix_list): | ||
for prefix in prefix_list: | ||
if content.startswith(prefix): | ||
return prefix | ||
return None | ||
|
||
|
||
def check_contain(self, content, keyword_list): | ||
if not keyword_list: | ||
return None | ||
for ky in keyword_list: | ||
if content.find(ky) != -1: | ||
return True | ||
return None |
Oops, something went wrong.