diff --git a/.gitignore b/.gitignore index d04bfb5..1781a73 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ logs/ *.log.* config.yaml + +*.db \ No newline at end of file diff --git a/README.MD b/README.MD index 5ac4573..8f7b7c1 100644 --- a/README.MD +++ b/README.MD @@ -80,6 +80,7 @@ python main.py -c 2 >> 2. chatgpt 模型 >> 3. 讯飞星火模型 >> 4. chatglm 模型 +>> 7. Azure 模型 6. 停止 @@ -103,12 +104,32 @@ groups: enable: [] # 允许响应的群 roomId,大概长这样:2xxxxxxxxx3@chatroom, 多个群用 `,` 分隔 ``` +#### 管理用户(好友)的相应权限 +第一次运行前设置,所有通讯录中的好友都将初次设置为此权限。以后每次打开会更新好友列表,但是不会更新权限。如果需要重新配置默认权限,请删除**你的微信id_permission.db**文件,再重新运行程序。 +```yaml +permission: + default: 2 # 默认权限 + # 0:群聊和私聊都不响应; 1:群聊不响应,私聊响应; 2:群聊响应,私聊不响应; 3:群聊和私聊都响应 +``` +如果要更新某用户(好友)的权限,可以在浏览器打开 http://127.0.0.1:5000 查看用户(好友)的微信id,然后在点击Update 或者访问 http://127.0.0.1:5000/update 填入新的权限信息,然后进行更新。 + +如果某用户不是好友,它第一次在群里中@你时,他的用户id会被记录到数据库中,默认权限为2,默认权限有效期为2099年。 + +不在数据库中的用户无法使用Web更新权限,但可以使用别的方法向数据库中添加记录。 + #### 配置 AI 模型 为了使用 AI 模型,需要对相应模型并进行配置。 使用 ChatGLM 见注意事项 [README.MD](base/chatglm/README.MD) ```yaml +azure: + key: # 填写你 Azure OpenAI API key + endpoint: https://XXXXXX.openai.azure.com/ + deployment_name: XXXXXX + api_version: 2024-02-15-preview + prompt: 你是智能聊天机器人,你叫 wcferry # 根据需要对角色进行设定 + chatgpt: # -----chatgpt配置这行不填----- key: # 填写你 ChatGPT 的 key api: https://api.openai.com/v1 # 如果你不知道这是干嘛的,就不要改 diff --git a/base/func_azure.py b/base/func_azure.py new file mode 100644 index 0000000..2303370 --- /dev/null +++ b/base/func_azure.py @@ -0,0 +1,98 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +from datetime import datetime + +import httpx +from openai import APIConnectionError, APIError, AuthenticationError, AzureOpenAI + + +class Azure(): + def __init__(self, conf: dict) -> None: + key = conf.get("key") + endpoint = conf.get("endpoint") + prompt = conf.get("prompt") + api_version = conf.get("api_version") + self.model = conf.get("deployment_name") + self.LOG = logging.getLogger("Azure") + self.client = AzureOpenAI(api_key=key, azure_endpoint=endpoint, api_version=api_version) + self.conversation_list = {} + self.system_content_msg = {"role": "system", "content": prompt} + + def __repr__(self): + return 'Azure' + + @staticmethod + def value_check(conf: dict) -> bool: + if conf: + if conf.get("key") and conf.get("endpoint") and conf.get("prompt") and conf.get("deployment_name") and conf.get("api_version"): + return True + return False + + def get_answer(self, question: str, wxid: str) -> str: + # wxid或者roomid,个人时为微信id,群消息时为群id + self.updateMessage(wxid, question, "user") + rsp = "" + try: + ret = self.client.chat.completions.create(model=self.model, + messages=self.conversation_list[wxid], + temperature=0.5) + rsp = ret.choices[0].message.content + rsp = rsp[2:] if rsp.startswith("\n\n") else rsp + rsp = rsp.replace("\n\n", "\n") + self.updateMessage(wxid, rsp, "assistant") + except AuthenticationError: + self.LOG.error("Azure API 认证失败,请检查 API 密钥是否正确") + except APIConnectionError: + self.LOG.error("无法连接到 Azure API,请检查网络连接") + except APIError as e1: + self.LOG.error(f"Azure API 返回了错误:{str(e1)}") + except Exception as e0: + self.LOG.error(f"发生未知错误:{str(e0)}") + + return rsp + + def updateMessage(self, wxid: str, question: str, role: str) -> None: + time_mk = "-" + # 初始化聊天记录,组装系统信息 + if wxid not in self.conversation_list.keys(): + question_ = [ + self.system_content_msg, + {"role": "system", "content": "" + time_mk} + ] + self.conversation_list[wxid] = question_ + + # 当前问题 + content_question_ = {"role": role, "content": question} + self.conversation_list[wxid].append(content_question_) + # for cont in self.conversation_list[wxid]: + # if cont["role"] != "system": + # continue + # if cont["content"].startswith(time_mk): + # cont["content"] = time_mk + # 只存储10条记录,超过滚动清除 + i = len(self.conversation_list[wxid]) + if i > 10: + print("滚动清除微信记录:" + wxid) + # 删除多余的记录,倒着删,且跳过第一个的系统消息 + del self.conversation_list[wxid][1] + + +if __name__ == "__main__": + from configuration import Config + config = Config().AZURE + if not config: + exit(0) + + chat = Azure(config) + + while True: + q = input(">>> ") + try: + time_start = datetime.now() # 记录开始时间 + print(chat.get_answer(q, "wxid")) + time_end = datetime.now() # 记录结束时间 + print(f"{round((time_end - time_start).total_seconds(), 2)}s") # 计算的时间差为程序的执行时间,单位为秒/s + except Exception as e: + print(e) diff --git a/config.yaml.template b/config.yaml.template index 5fcc8ea..47ea93f 100644 --- a/config.yaml.template +++ b/config.yaml.template @@ -41,12 +41,23 @@ logging: groups: enable: [] # 允许响应的群 roomId,大概长这样:2xxxxxxxxx3@chatroom +permission: + default: 2 # 默认权限 + # 0:群聊和私聊都不响应; 1:群聊不响应,私聊响应; 2:群聊响应,私聊不响应; 3:群聊和私聊都响应 + news: receivers: [] # 定时新闻接收人(roomid 或者 wxid) report_reminder: receivers: [] # 定时日报周报月报提醒(roomid 或者 wxid) +azure: + key: # 填写你 Azure OpenAI API key + endpoint: https://XXXXXX.openai.azure.com/ + deployment_name: XXXXXX + api_version: 2024-02-15-preview + prompt: 你是智能聊天机器人,你叫 wcferry # 根据需要对角色进行设定 + chatgpt: # -----chatgpt配置这行不填----- key: # 填写你 ChatGPT 的 key api: https://api.openai.com/v1 # 如果你不知道这是干嘛的,就不要改 diff --git a/configuration.py b/configuration.py index 611a526..9f8c097 100644 --- a/configuration.py +++ b/configuration.py @@ -28,9 +28,11 @@ def reload(self) -> None: yconfig = self._load_config() logging.config.dictConfig(yconfig["logging"]) self.GROUPS = yconfig["groups"]["enable"] + self.PERMISSION = yconfig["permission"]["default"] self.NEWS = yconfig["news"]["receivers"] self.REPORT_REMINDERS = yconfig["report_reminder"]["receivers"] + self.AZURE = yconfig.get("azure", {}) self.CHATGPT = yconfig.get("chatgpt", {}) self.TIGERBOT = yconfig.get("tigerbot", {}) self.XINGHUO_WEB = yconfig.get("xinghuo_web", {}) diff --git a/constants.py b/constants.py index a1f87f1..1757696 100644 --- a/constants.py +++ b/constants.py @@ -10,6 +10,7 @@ class ChatType(IntEnum): CHATGLM = 4 # ChatGLM BardAssistant = 5 # Google Bard ZhiPu = 6 # ZhiPu + Azure = 7 # Azure @staticmethod def is_in_chat_types(chat_type: int) -> bool: diff --git a/main.py b/main.py index 43de74d..06a7d4d 100644 --- a/main.py +++ b/main.py @@ -2,13 +2,16 @@ # -*- coding: utf-8 -*- import signal +import threading from argparse import ArgumentParser +from wcferry import Wcf + from base.func_report_reminder import ReportReminder from configuration import Config from constants import ChatType +from permission import init_permission, update_permission from robot import Robot, __version__ -from wcferry import Wcf def weather_report(robot: Robot) -> None: @@ -42,6 +45,16 @@ def handler(sig, frame): # 机器人启动发送测试消息 robot.sendTextMsg("机器人启动成功!", "filehelper") + # 更新机器人回复用户(好友)响应权限 + init_permission(wcf, config) + user_id = wcf.get_user_info() + user_id = user_id['wxid'] + database_file = "{}_permission.db".format(user_id) + + # add a threading to run update_permission + t = threading.Thread(target=update_permission, args=(database_file,)) + t.start() + # 接收消息 # robot.enableRecvMsg() # 可能会丢消息? robot.enableReceivingMsg() # 加队列 diff --git a/permission.py b/permission.py new file mode 100644 index 0000000..01dedb1 --- /dev/null +++ b/permission.py @@ -0,0 +1,106 @@ +import datetime +import os +import signal +import sqlite3 +import threading + +from flask import Flask, redirect, render_template, request, url_for +from wcferry import Wcf, WxMsg + +from configuration import Config + + +def init_permission(wcf: Wcf, config: Config): + # permission: + # 0->群聊和私聊都不响应 + # 1->群聊不响应,私聊响应 + # 2->群聊响应,私聊不响应 + # 3->群聊和私聊都响应 + user_id = wcf.get_user_info() + user_id = user_id['wxid'] + + database_file = "{}_permission.db".format(user_id) + # 检查数据库是否存在,不存在以user_id为名创建,存在则打开数据库 + if not os.path.exists(database_file): + conn = sqlite3.connect(database_file) + cursor = conn.cursor() + cursor.execute('''CREATE TABLE permission + (wxid text, code text, remark text, name text, country text, province text,city text, gender text, permission int, permission_end_time timestamp)''') + conn.commit() + # 将微信通讯录导入数据库,permission_end_time为2099-12-31 23:59:59 + default_permission = config.PERMISSION + default_permission_end_time = '2099-12-31 23:59:59' + wcf.get_contacts() + contact_list = wcf.contacts + for contact in contact_list: + cursor.execute("INSERT INTO permission VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",( + contact['wxid'], contact['code'], contact['remark'], contact['name'], contact['country'], contact['province'], contact['city'], contact['gender'],default_permission, default_permission_end_time)) + conn.commit() + # 关闭数据库 + conn.close() + else: + conn = sqlite3.connect(database_file) + cursor = conn.cursor() + # 将更新后的微信通讯录存入数据库 + wcf.get_contacts() + contact_list = wcf.contacts + for contact in contact_list: + # 查询数据库中是否存在该联系人 + cursor.execute("SELECT * FROM permission WHERE wxid = ?", (contact['wxid'],)) + result = cursor.fetchone() + if result is None: + cursor.execute("INSERT INTO permission VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",( + contact['wxid'], contact['code'], contact['remark'], contact['name'], contact['country'], contact['province'], contact['city'], contact['gender'], config.PERMISSION, '2099-12-31 23:59:59')) + else: + cursor.execute("UPDATE permission SET code = ?, remark = ?, name = ?, country = ?,province = ?, city = ?, gender = ? WHERE wxid = ?", (contact['code'], contact['remark'], contact['name'], contact['country'], contact['province'], contact['city'], contact['gender'], contact['wxid'])) + conn.commit() + # 关闭数据库 + conn.close() + +def update_permission(DATABASE_URI): + app = Flask(__name__) + + @app.route('/') + def index(): + conn = sqlite3.connect(DATABASE_URI) + cursor = conn.cursor() + cursor.execute("SELECT * FROM permission") + rows = cursor.fetchall() + conn.close() + return render_template('index.html', users=rows) + + @app.route('/update', methods=['POST', 'GET']) + def update(): + if request.method == 'POST': + wxid = request.form['wxid_field'] + permission = int(request.form['permission_field']) + permission_end_time = request.form['permission_end_time_field'] + permission_end_time = permission_end_time.replace('T', ' ') + ":00" + conn = sqlite3.connect(DATABASE_URI) + cursor = conn.cursor() + cursor.execute("UPDATE permission SET permission = ?, permission_end_time = ? WHERE wxid = ?", (permission, permission_end_time, wxid)) + conn.commit() + conn.close() + return redirect(url_for('index')) + else: + return render_template('update.html') + + app.run(debug=False) + +if __name__ == "__main__": + config = Config() + wcf = Wcf(debug=True) + def handler(sig, frame): + wcf.cleanup() # 退出前清理环境 + exit(0) + + signal.signal(signal.SIGINT, handler) + + init_permission(wcf, config) + user_id = wcf.get_user_info() + user_id = user_id['wxid'] + database_file = "{}_permission.db".format(user_id) + + # add a threading to run update_permission + t = threading.Thread(target=update_permission, args=(database_file,)) + t.start() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f417589..25f4347 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ zhdate ipykernel google-generativeai zhipuai +Flask \ No newline at end of file diff --git a/robot.py b/robot.py index f9c8712..54e81c7 100644 --- a/robot.py +++ b/robot.py @@ -1,22 +1,25 @@ # -*- coding: utf-8 -*- +import datetime import logging import re +import sqlite3 import time import xml.etree.ElementTree as ET from queue import Empty from threading import Thread -from base.func_zhipu import ZhiPu from wcferry import Wcf, WxMsg from base.func_bard import BardAssistant from base.func_chatglm import ChatGLM from base.func_chatgpt import ChatGPT +from base.func_azure import Azure from base.func_chengyu import cy from base.func_news import News from base.func_tigerbot import TigerBot from base.func_xinghuo_web import XinghuoWeb +from base.func_zhipu import ZhiPu from configuration import Config from constants import ChatType from job_mgmt import Job @@ -40,6 +43,8 @@ def __init__(self, config: Config, wcf: Wcf, chat_type: int) -> None: self.chat = TigerBot(self.config.TIGERBOT) elif chat_type == ChatType.CHATGPT.value and ChatGPT.value_check(self.config.CHATGPT): self.chat = ChatGPT(self.config.CHATGPT) + elif chat_type == ChatType.Azure.value and Azure.value_check(self.config.AZURE): + self.chat = Azure(self.config.AZURE) elif chat_type == ChatType.XINGHUO_WEB.value and XinghuoWeb.value_check(self.config.XINGHUO_WEB): self.chat = XinghuoWeb(self.config.XINGHUO_WEB) elif chat_type == ChatType.CHATGLM.value and ChatGLM.value_check(self.config.CHATGLM): @@ -56,6 +61,8 @@ def __init__(self, config: Config, wcf: Wcf, chat_type: int) -> None: self.chat = TigerBot(self.config.TIGERBOT) elif ChatGPT.value_check(self.config.CHATGPT): self.chat = ChatGPT(self.config.CHATGPT) + elif Azure.value_check(self.config.AZURE): + self.chat = Azure(self.config.AZURE) elif XinghuoWeb.value_check(self.config.XINGHUO_WEB): self.chat = XinghuoWeb(self.config.XINGHUO_WEB) elif ChatGLM.value_check(self.config.CHATGLM): @@ -116,19 +123,64 @@ def toChitchat(self, msg: WxMsg) -> bool: if not self.chat: # 没接 ChatGPT,固定回复 rsp = "你@我干嘛?" else: # 接了 ChatGPT,智能回复 - q = re.sub(r"@.*?[\u2005|\s]", "", msg.content).replace(" ", "") - rsp = self.chat.get_answer(q, (msg.roomid if msg.from_group() else msg.sender)) - - if rsp: - if msg.from_group(): - self.sendTextMsg(rsp, msg.roomid, msg.sender) - else: - self.sendTextMsg(rsp, msg.sender) - - return True - else: - self.LOG.error(f"无法从 ChatGPT 获得答案") - return False + # 检查信息发送者是否在权限数据库中 + user_id = self.wcf.get_user_info() + user_id = user_id['wxid'] + database_file = "{}_permission.db".format(user_id) + conn = sqlite3.connect(database_file) + cursor = conn.cursor() + cursor.execute("SELECT * FROM permission WHERE wxid = ?", (msg.sender,)) + result = cursor.fetchone() + conn.close() + if result: + permission = result[-2] + permission_end_time = datetime.datetime.strptime(result[-1], "%Y-%m-%d %H:%M:%S") + now = datetime.datetime.now() + if msg.from_group(): # 来自群聊 + if permission >= 2 and now < permission_end_time: + q = re.sub(r"@.*?[\u2005|\s]", "", msg.content).replace(" ", "") + rsp = self.chat.get_answer(q, msg.roomid) + if rsp: + self.sendTextMsg(rsp, msg.roomid, msg.sender) + return True + else: + self.LOG.error(f"无法从 ChatGPT 获得答案") + return False + else: + self.LOG.error("{}没有被授予权限或在限制期内。".format(msg.sender)) + return False + else: # 来自私聊 + if permission%2 == 1 and now < permission_end_time: + q = re.sub(r"@.*?[\u2005|\s]", "", msg.content).replace(" ", "") + rsp = self.chat.get_answer(q, msg.sender) + if rsp: + self.sendTextMsg(rsp, msg.sender) + return True + else: + self.LOG.error(f"无法从 ChatGPT 获得答案") + return False + else: + self.LOG.error("{}没有被授予权限或在限制期内。".format(msg.sender)) + return False + else: # 不在权限数据库中,肯定不是好友,且此消息来自群聊 + default_permission = 2 # 第一次来自群聊响应,权限设置为2 + default_permission_end_time = '2099-12-31 23:59:59' + remark = "用户来自群聊" + msg.roomid + # 在数据库中插入记录 + conn = sqlite3.connect(database_file) + cursor = conn.cursor() + sql = "INSERT INTO permission (wxid, remark, permission, permission_end_time) VALUES (?, ?, ?, ?)" + conn.execute(sql, (msg.sender, remark, default_permission, default_permission_end_time)) + conn.commit() + conn.close() + q = re.sub(r"@.*?[\u2005|\s]", "", msg.content).replace(" ", "") + rsp = self.chat.get_answer(q, msg.roomid) + if rsp: + self.sendTextMsg(rsp, msg.roomid, msg.sender) + return True + else: + self.LOG.error(f"无法从 ChatGPT 获得答案") + return False def processMsg(self, msg: WxMsg) -> None: """当接收到消息的时候,会调用本方法。如果不实现本方法,则打印原始消息。 @@ -155,7 +207,8 @@ def processMsg(self, msg: WxMsg) -> None: # 非群聊信息,按消息类型进行处理 if msg.type == 37: # 好友请求 - self.autoAcceptFriendRequest(msg) + # self.autoAcceptFriendRequest(msg) + pass elif msg.type == 10000: # 系统信息 self.sayHiToNewFriend(msg) @@ -249,11 +302,23 @@ def autoAcceptFriendRequest(self, msg: WxMsg) -> None: self.LOG.error(f"同意好友出错:{e}") def sayHiToNewFriend(self, msg: WxMsg) -> None: - nickName = re.findall(r"你已添加了(.*),现在可以开始聊天了。", msg.content) + # nickName = re.findall(r"你已添加了(.*),现在可以开始聊天了。", msg.content) + nickName = re.findall(r"You have added (.*) as your Weixin contact. Start chatting!", msg.content) if nickName: # 添加了好友,更新好友列表 self.allContacts[msg.sender] = nickName[0] - self.sendTextMsg(f"Hi {nickName[0]},我自动通过了你的好友请求。", msg.sender) + # self.sendTextMsg(f"Hi {nickName[0]},我自动通过了你的好友请求。", msg.sender) + + # 更新机器人响应好友权限 + user_id = self.wcf.get_user_info() + user_id = user_id['wxid'] + database_file = "{}_permission.db".format(user_id) + conn = sqlite3.connect(database_file) + cursor = conn.cursor() + cursor.execute("INSERT INTO permission (wxid, permission, permission_end_time timestamp) VALUES (?, ?, ?)", (msg.sender, self.config.PERMISSION, '2099-12-31 23:59:59')) + conn.commit() + cursor.close() + conn.close() def newsReport(self) -> None: receivers = self.config.NEWS diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..fe8ff23 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,38 @@ +input[type="text"], +input[type="email"], +input[type="datetime-local"], +select { + border: 2px solid #000; + margin: 0 0 1em 0; + padding: 8px; + width: 14%; + font-size: 20px; +} + +input[type="submit"] { + border: 3px solid #333; + background-color: #e7dcdc; + border-radius: 5px; + padding: 10px 2em; + font-weight: bold; + color: #333; + font-size: 20px; + } + +input[type="submit"]:hover, input[type="submit"]:focus { + background-color: #333; +} + +label { + font-family: Arial, sans-serif; + font-size: 24px; + color: #333; + margin-bottom: 5px; +} + +a { + font-family: Arial, sans-serif; + font-size: 16px; + color: #333; + margin-bottom: 5px; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..27789a5 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,43 @@ + + + + + + Table Example + + +

Update

+

Contacts

+ + + + + + + + + + + + + + + + {% for item in users %} + + + + + + + + + + + + + {% endfor %} + +
wxidcoderemarknamecountryprovincecitygenderpermissionpermission_end_time
{{item[0]}}{{item[1]}}{{item[2]}}{{item[3]}}{{item[4]}}{{item[5]}}{{item[6]}}{{item[7]}}{{item[8]}}{{item[9]}}
+ + \ No newline at end of file diff --git a/templates/update.html b/templates/update.html new file mode 100644 index 0000000..86f0162 --- /dev/null +++ b/templates/update.html @@ -0,0 +1,38 @@ + + + + + + + Table Example + + +
+

Update

+
+ + +
+ + +
+ + +
+ + +
+
+

权限说明

+ 0:群聊和私聊都不回应
+ 1:群聊不回应,私聊回应
+ 2:群聊回应,私聊不回应
+ 3:群聊和私聊都回应
+
+ + \ No newline at end of file