diff --git a/.gitignore b/.gitignore index d5178c6..efdf602 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ nohup.out tmp plugins.json itchat.pkl +user_datas.pkl \ No newline at end of file diff --git a/app.py b/app.py index a2e6b9c..4e51ab9 100644 --- a/app.py +++ b/app.py @@ -4,13 +4,22 @@ import os from config import conf, load_config from channel import channel_factory from common.log import logger - from plugins import * +import signal +import sys + +def sigterm_handler(_signo, _stack_frame): + conf().save_user_datas() + sys.exit(0) def run(): try: # load config load_config() + # ctrl + c + signal.signal(signal.SIGINT, sigterm_handler) + # kill signal + signal.signal(signal.SIGTERM, sigterm_handler) # create channel channel_name=conf().get('channel_type', 'wx') diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/chat_gpt_bot.py index e4507b3..27c5b2a 100644 --- a/bot/chatgpt/chat_gpt_bot.py +++ b/bot/chatgpt/chat_gpt_bot.py @@ -13,7 +13,6 @@ from common.expired_dict import ExpiredDict import openai import openai.error import time -import redis # OpenAI对话模型API (可用) class ChatGPTBot(Bot,OpenAIImage): diff --git a/channel/wechatmp/README.md b/channel/wechatmp/README.md index 31069b5..cf04e2d 100644 --- a/channel/wechatmp/README.md +++ b/channel/wechatmp/README.md @@ -8,18 +8,15 @@ 在开始部署前,你需要一个拥有公网IP的服务器,以提供微信服务器和我们自己服务器的连接。或者你需要进行内网穿透,否则微信服务器无法将消息发送给我们的服务器。 -此外,需要在我们的服务器上安装额外的依赖web.py和redis,其中redis用来储存用户私有的配置信息。 +此外,需要在我们的服务器上安装python的web框架web.py。 以ubuntu为例(在ubuntu 22.04上测试): ``` -sudo apt-get install redis -sudo systemctl start redis -pip3 install redis pip3 install web.py ``` 然后在[微信公众平台](mp.weixin.qq.com)注册一个自己的公众号,类型选择订阅号,主体为个人即可。 -然后根据[接入指南](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html)的说明,在[微信公众平台](mp.weixin.qq.com)的“设置与开发”-“基本配置”-“服务器配置”中填写服务器地址(URL)和令牌(Token)。这个Token是你自己编的一个特定的令牌。消息加解密方式目前选择的是明文模式。相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要将本项目根目录的`app.py`中channel_name改成"mp",将上述的Token填写在本项目根目录的`config.json`中,例如`"wechatmp_token": "Your Token",` 然后运行`python3 app.py`启动web服务器,然后在刚才的“服务器配置”中点击`提交`即可验证你的服务器。 +然后根据[接入指南](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html)的说明,在[微信公众平台](mp.weixin.qq.com)的“设置与开发”-“基本配置”-“服务器配置”中填写服务器地址(URL)和令牌(Token)。这个Token是你自己编的一个特定的令牌。消息加解密方式目前选择的是明文模式。相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要在本项目根目录的`config.json`中添加`"channel_type": "wechatmp", "wechatmp_token": "your Token", ` 然后运行`python3 app.py`启动web服务器,然后在刚才的“服务器配置”中点击`提交`即可验证你的服务器。 随后在[微信公众平台](mp.weixin.qq.com)启用服务器,关闭手动填写规则的自动回复,即可实现ChatGPT的自动回复。 @@ -29,7 +26,7 @@ pip3 install web.py 另外,由于微信官方的限制,自动回复有长度限制。因此这里将ChatGPT的回答拆分,分成每段600字回复(限制大约在700字)。 ## 私有api_key -公共api有访问频率限制(免费账号每分钟最多20次ChatGPT的API调用),这在服务多人的时候会遇到问题。因此这里多加了一个设置私有api_key的功能,私有的api_key将储存在redis中。另外后续计划利用redis储存更多的用户个人配置。目前通过godcmd插件的命令来设置私有api_key。 +公共api有访问频率限制(免费账号每分钟最多20次ChatGPT的API调用),这在服务多人的时候会遇到问题。因此这里多加了一个设置私有api_key的功能。目前通过godcmd插件的命令来设置私有api_key。 ## 命令优化 之前plugin中#和$符号混用,且$这个符号在微信中和中文会有较大间隔,体验实在不好。这里我将所有命令更改成了以#开头。添加了一个叫finish的plugin来最后处理所有#结尾的命令,防止未知命令变成ChatGPT的query。 diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index aa81048..efef6cd 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -13,7 +13,6 @@ from bridge.reply import * from bridge.context import * from plugins import * import traceback -import redis # If using SSL, uncomment the following lines, and modify the certificate path. # from cheroot.server import HTTPServer @@ -181,12 +180,8 @@ class WechatMPChannel(Channel): context = Context() context.kwargs = {'isgroup': False, 'receiver': fromUser, 'session_id': fromUser} - R = redis.Redis(host='localhost', port=6379, db=0) - user_openai_api_key = "openai_api_key_" + fromUser - api_key = R.get(user_openai_api_key) - if api_key != None: - api_key = api_key.decode("utf-8") - context['openai_api_key'] = api_key # None or user openai_api_key + user_data = conf().get_user_data(fromUser) + context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key img_match_prefix = check_prefix(message, conf().get('image_create_prefix')) if img_match_prefix: diff --git a/config.py b/config.py index 7009c00..ce70106 100644 --- a/config.py +++ b/config.py @@ -3,6 +3,7 @@ import json import os from common.log import logger +import pickle # 将所有可用的配置项写在字典里, 请使用小写字母 available_setting = { @@ -88,6 +89,11 @@ available_setting = { class Config(dict): + def __init__(self, d:dict={}): + super().__init__(d) + # user_datas: 用户数据,key为用户名,value为用户数据,也是dict + self.user_datas = {} + def __getitem__(self, key): if key not in available_setting: raise Exception("key {} not in available_setting".format(key)) @@ -106,6 +112,30 @@ class Config(dict): except Exception as e: raise e + # Make sure to return a dictionary to ensure atomic + def get_user_data(self, user) -> dict: + if self.user_datas.get(user) is None: + self.user_datas[user] = {} + return self.user_datas[user] + + def load_user_datas(self): + try: + with open('user_datas.pkl', 'rb') as f: + self.user_datas = pickle.load(f) + logger.info("[Config] User datas loaded.") + except FileNotFoundError as e: + logger.info("[Config] User datas file not found, ignore.") + except Exception as e: + logger.info("[Config] User datas error: {}".format(e)) + self.user_datas = {} + + def save_user_datas(self): + try: + with open('user_datas.pkl', 'wb') as f: + pickle.dump(self.user_datas, f) + logger.info("[Config] User datas saved.") + except Exception as e: + logger.info("[Config] User datas error: {}".format(e)) config = Config() @@ -142,6 +172,7 @@ def load_config(): logger.info("[INIT] load config: {}".format(config)) + config.load_user_datas() def get_root(): return os.path.dirname(os.path.abspath(__file__)) diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index 33e72a3..c60f937 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -7,11 +7,12 @@ from typing import Tuple from bridge.bridge import Bridge from bridge.context import ContextType from bridge.reply import Reply, ReplyType -from config import load_config +from config import conf, load_config import plugins from plugins import * from common import const from common.log import logger +import pickle # 定义指令集 COMMANDS = { @@ -195,20 +196,18 @@ class Godcmd(Plugin): ok, result = False, "unknown args" elif cmd == "set_openai_api_key": if len(args) == 1: - import redis - R = redis.Redis(host='localhost', port=6379, db=0) - user_openai_api_key = "openai_api_key_" + user - R.set(user_openai_api_key, args[0]) - # R.sadd("openai_api_key", args[0]) + user_data = conf().get_user_data(user) + user_data['openai_api_key'] = args[0] ok, result = True, "你的OpenAI私有api_key已设置为" + args[0] else: ok, result = False, "请提供一个api_key" elif cmd == "reset_openai_api_key": - import redis - R = redis.Redis(host='localhost', port=6379, db=0) - user_openai_api_key = "openai_api_key_" + user - R.delete(user_openai_api_key) - ok, result = True, "OpenAI的api_key已重置" + try: + user_data = conf().get_user_data(user) + user_data.pop('openai_api_key') + except Exception as e: + ok, result = False, "你没有设置私有api_key" + ok, result = True, "你的OpenAI私有api_key已清除" # elif cmd == "helpp": # if len(args) != 1: # ok, result = False, "请提供插件名"