diff --git a/.github/ISSUE_TEMPLATE/需求建议.md b/.github/ISSUE_TEMPLATE/需求建议.md index c16fb51a..20b91445 100644 --- a/.github/ISSUE_TEMPLATE/需求建议.md +++ b/.github/ISSUE_TEMPLATE/需求建议.md @@ -7,4 +7,4 @@ assignees: '' --- -想写啥都行 +不是需求建议请勿填写此模板!!!! diff --git a/config-template.py b/config-template.py index bee7a561..adc92ca5 100644 --- a/config-template.py +++ b/config-template.py @@ -30,13 +30,7 @@ openai_config = { }, } -# [可选] 机器人的配置 -#user_name: 管理员(主人)的名字 -#bot_name: 机器人的名字 -user_name = 'You' -bot_name = 'Bot' - -# [可选] 情景预设(机器人人格) +# 情景预设(机器人人格) # 每个会话的预设信息,影响所有会话,无视指令重置 # 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令 # 例如: 如果我之后想获取帮助,请你说“输入!help获取帮助”, @@ -58,10 +52,11 @@ response_rules = { "regexp": [] # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办" } -# 单个api-key的使用量警告阈值 -# 当使用此api-key进行请求的文字量达到此阈值时,会在控制台输出警告并通知管理员 +# 单个api-key的费用警告阈值 +# 当使用此api-key进行请求所消耗的费用估算达到此阈值时,会在控制台输出警告并通知管理员 # 若之后还有未使用超过此值的api-key,则会切换到新的api-key进行请求 -api_key_usage_threshold = 900000 +# 单位:美元 +api_key_fee_threshold = 18.0 # 敏感词过滤开关,以同样数量的*代替敏感词回复 # 请在sensitive.json中添加敏感词 @@ -83,9 +78,21 @@ completion_api_params = { "presence_penalty": 1.0, } +# OpenAI的Image API的参数 +# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/images/create +image_api_params = { + "size": "256x256", # 图片尺寸,支持256x256, 512x512, 1024x1024 +} + # 消息处理的超时时间,单位为秒 process_message_timeout = 15 +# 机器人的配置 +# user_name: 管理员(主人)的名字 +# bot_name: 机器人的名字 +user_name = 'You' +bot_name = 'Bot' + # 回复消息时是否显示[GPT]前缀 show_prefix = False diff --git a/main.py b/main.py index 988626fc..8c19f5df 100644 --- a/main.py +++ b/main.py @@ -59,7 +59,7 @@ def main(): database.initialize_database() - openai_interact = pkg.openai.manager.OpenAIInteract(config.openai_config['api_key'], config.completion_api_params) + openai_interact = pkg.openai.manager.OpenAIInteract(config.openai_config['api_key']) # 加载所有未超时的session pkg.openai.session.load_sessions() @@ -78,7 +78,7 @@ def main(): time.sleep(86400) except KeyboardInterrupt: try: - pkg.openai.manager.get_inst().key_mgr.dump_usage() + pkg.openai.manager.get_inst().key_mgr.dump_fee() for session in pkg.openai.session.sessions: logging.info('持久化session: %s', session) pkg.openai.session.sessions[session].persistence() diff --git a/pkg/database/manager.py b/pkg/database/manager.py index ecd38002..cbe243e9 100644 --- a/pkg/database/manager.py +++ b/pkg/database/manager.py @@ -58,6 +58,15 @@ class DatabaseManager: `usage` bigint not null ) """) + + self.execute(""" + create table if not exists `account_fee`( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `key_md5` varchar(255) not null, + `timestamp` bigint not null, + `fee` DECIMAL(12,6) not null + ) + """) print('Database initialized.') # session持久化 @@ -264,6 +273,45 @@ class DatabaseManager: usage[key_md5] = usage_count return usage + def dump_api_key_fee(self, api_keys: dict, fee: dict): + logging.debug("dumping api key fee...") + logging.debug(api_keys) + logging.debug(fee) + for api_key in api_keys: + # 计算key的md5值 + key_md5 = hashlib.md5(api_keys[api_key].encode('utf-8')).hexdigest() + # 获取使用量 + fee_count = 0 + if key_md5 in fee: + fee_count = fee[key_md5] + # 将使用量存进数据库 + # 先检查是否已存在 + self.execute(""" + select count(*) from `account_fee` where `key_md5` = '{}'""".format(key_md5)) + result = self.cursor.fetchone() + if result[0] == 0: + # 不存在则插入 + self.execute(""" + insert into `account_fee` (`key_md5`, `fee`,`timestamp`) values ('{}', {}, {}) + """.format(key_md5, fee_count, int(time.time()))) + else: + # 存在则更新,timestamp设置为当前 + self.execute(""" + update `account_fee` set `fee` = {}, `timestamp` = {} where `key_md5` = '{}' + """.format(fee_count, int(time.time()), key_md5)) + + def load_api_key_fee(self): + self.execute(""" + select `key_md5`, `fee` from `account_fee` + """) + results = self.cursor.fetchall() + fee = {} + for result in results: + key_md5 = result[0] + fee_count = result[1] + fee[key_md5] = fee_count + return fee + def get_inst() -> DatabaseManager: global inst return inst diff --git a/pkg/openai/keymgr.py b/pkg/openai/keymgr.py index 3f878d90..a0fea80d 100644 --- a/pkg/openai/keymgr.py +++ b/pkg/openai/keymgr.py @@ -12,9 +12,9 @@ class KeysManager: # api-key的使用量 # 其中键为api-key的md5值,值为使用量 - usage = {} + fee = {} - api_key_usage_threshold = 900000 + api_key_fee_threshold = 18.0 using_key = "" @@ -24,9 +24,11 @@ class KeysManager: return self.using_key def __init__(self, api_key): - if hasattr(config, 'api_key_usage_threshold'): - self.api_key_usage_threshold = config.api_key_usage_threshold - self.load_usage() + # if hasattr(config, 'api_key_usage_threshold'): + # self.api_key_usage_threshold = config.api_key_usage_threshold + if hasattr(config, 'api_key_fee_threshold'): + self.api_key_fee_threshold = config.api_key_fee_threshold + self.load_fee() if type(api_key) is dict: self.api_key = api_key @@ -45,42 +47,88 @@ class KeysManager: # 根据使用量自动切换到可用的api-key # 返回是否切换成功, 切换后的api-key的别名 def auto_switch(self) -> (bool, str): - self.dump_usage() + self.dump_fee() for key_name in self.api_key: - if self.get_usage(self.api_key[key_name]) < self.api_key_usage_threshold: + if self.get_fee(self.api_key[key_name]) < self.api_key_fee_threshold: self.using_key = self.api_key[key_name] logging.info("使用api-key:" + key_name) return True, key_name self.using_key = list(self.api_key.values())[0] - logging.info("使用api-key:" + self.using_key) + logging.info("使用api-key:" + self.api_key.keys()[0]) return False, "" - def get_usage(self, api_key): - md5 = hashlib.md5(api_key.encode('utf-8')).hexdigest() - if md5 not in self.usage: - self.usage[md5] = 0 - return self.usage[md5] - def add(self, key_name, key): self.api_key[key_name] = key + # def get_usage(self, api_key): + # md5 = hashlib.md5(api_key.encode('utf-8')).hexdigest() + # if md5 not in self.usage: + # self.usage[md5] = 0 + # return self.usage[md5] + # 报告使用 # 返回是否需要将openai的api-key切换 - def report_usage(self, new_content: str) -> bool: + # def report_usage(self, new_content: str) -> bool: + # md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest() + # if md5 not in self.usage: + # self.usage[md5] = 0 + # + # # 经测算得出的理论与实际的偏差比例 + # salt_rate = 0.91 + # + # self.usage[md5] += ( (len(new_content.encode('utf-8')) - len(new_content)) / 2 + len(new_content) )*salt_rate + # + # self.usage[md5] = int(self.usage[md5]) + # + # if self.usage[md5] >= self.api_key_usage_threshold: + # switch_result, key_name = self.auto_switch() + # + # # 检查是否切换到新的 + # if switch_result: + # if key_name not in self.alerted: + # # 通知管理员 + # pkg.qqbot.manager.get_inst().notify_admin("api-key已切换到:" + key_name) + # self.alerted.append(key_name) + # return True + # else: + # if key_name not in self.alerted: + # # 通知管理员 + # pkg.qqbot.manager.get_inst().notify_admin("api-key已用完,无未使用的api-key可供切换") + # self.alerted.append(key_name) + # return False + + # 设置当前使用的api-key使用量超限 + # 这是在尝试调用api时发生超限异常时调用的 + def set_current_exceeded(self): md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest() - if md5 not in self.usage: - self.usage[md5] = 0 + # self.usage[md5] = self.api_key_usage_threshold + self.fee[md5] = self.api_key_fee_threshold + self.dump_fee() - # 经测算得出的理论与实际的偏差比例 - salt_rate = 0.91 + # def dump_usage(self): + # pkg.database.manager.get_inst().dump_api_key_usage(api_keys=self.api_key, usage=self.usage) - self.usage[md5] += ( (len(new_content.encode('utf-8')) - len(new_content)) / 2 + len(new_content) )*salt_rate + # def load_usage(self): + # self.usage = pkg.database.manager.get_inst().load_api_key_usage() + # logging.debug("load usage:" + str(self.usage)) + # print("load usage:" + str(self.usage)) - self.usage[md5] = int(self.usage[md5]) + def get_fee(self, api_key): + md5 = hashlib.md5(api_key.encode('utf-8')).hexdigest() + if md5 not in self.fee: + self.fee[md5] = 0 + return self.fee[md5] - if self.usage[md5] >= self.api_key_usage_threshold: + def report_fee(self, fee: float) -> bool: + md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest() + if md5 not in self.fee: + self.fee[md5] = 0 + + self.fee[md5] += fee + + if self.fee[md5] >= self.api_key_fee_threshold: switch_result, key_name = self.auto_switch() # 检查是否切换到新的 @@ -97,17 +145,9 @@ class KeysManager: self.alerted.append(key_name) return False - # 设置当前使用的api-key使用量超限 - # 这是在尝试调用api时发生超限异常时调用的 - def set_current_exceeded(self): - md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest() - self.usage[md5] = self.api_key_usage_threshold - self.dump_usage() + def dump_fee(self): + pkg.database.manager.get_inst().dump_api_key_fee(api_keys=self.api_key, fee=self.fee) - def dump_usage(self): - pkg.database.manager.get_inst().dump_api_key_usage(api_keys=self.api_key, usage=self.usage) - - def load_usage(self): - self.usage = pkg.database.manager.get_inst().load_api_key_usage() - logging.debug("load usage:" + str(self.usage)) - print("load usage:" + str(self.usage)) + def load_fee(self): + self.fee = pkg.database.manager.get_inst().load_api_key_fee() + logging.info("load fee:" + str(self.fee)) \ No newline at end of file diff --git a/pkg/openai/manager.py b/pkg/openai/manager.py index e753953f..d65f0e54 100644 --- a/pkg/openai/manager.py +++ b/pkg/openai/manager.py @@ -5,6 +5,7 @@ import openai import config import pkg.openai.keymgr +import pkg.openai.pricing as pricing inst = None @@ -15,9 +16,12 @@ class OpenAIInteract: key_mgr = None - def __init__(self, api_key: str, api_params: dict): + default_image_api_params = { + "size": "256x256", + } + + def __init__(self, api_key: str): # self.api_key = api_key - self.api_params = api_params self.key_mgr = pkg.openai.keymgr.KeysManager(api_key) @@ -28,14 +32,33 @@ class OpenAIInteract: # 请求OpenAI Completion def request_completion(self, prompt, stop): - logging.debug("请求OpenAI Completion, key:"+openai.api_key) response = openai.Completion.create( prompt=prompt, stop=stop, timeout=config.process_message_timeout, - **self.api_params + **config.completion_api_params ) - switched = self.key_mgr.report_usage(prompt + response['choices'][0]['text']) + + switched = self.key_mgr.report_fee(pricing.language_base_price(config.completion_api_params['model'], + prompt + response['choices'][0]['text'])) + + if switched: + openai.api_key = self.key_mgr.get_using_key() + + return response + + def request_image(self, prompt): + + params = config.image_api_params if hasattr(config, "image_api_params") else self.default_image_api_params + + response = openai.Image.create( + prompt=prompt, + n=1, + **params + ) + + switched = self.key_mgr.report_fee(pricing.image_price(params['size'])) + if switched: openai.api_key = self.key_mgr.get_using_key() diff --git a/pkg/openai/pricing.py b/pkg/openai/pricing.py new file mode 100644 index 00000000..1040a04c --- /dev/null +++ b/pkg/openai/pricing.py @@ -0,0 +1,21 @@ +pricing = { + "base": { # 文字模型单位是1000字符 + "text-davinci-003": 0.02, + }, + "image": { + "256x256": 0.016, + "512x512": 0.018, + "1024x1024": 0.02, + } +} + + +def language_base_price(model, text): + salt_rate = 0.93 + length = ((len(text.encode('utf-8')) - len(text)) / 2 + len(text)) * salt_rate + + return pricing["base"][model] * length / 1000 + + +def image_price(size): + return pricing["image"][size] diff --git a/pkg/openai/session.py b/pkg/openai/session.py index 2d28f38a..9bc9e9e1 100644 --- a/pkg/openai/session.py +++ b/pkg/openai/session.py @@ -268,3 +268,6 @@ class Session: def list_history(self, capacity: int = 10, page: int = 0): return pkg.database.manager.get_inst().list_history(self.name, capacity, page, get_default_prompt()) + + def draw_image(self, prompt: str): + return pkg.openai.manager.get_inst().request_image(prompt) diff --git a/pkg/qqbot/manager.py b/pkg/qqbot/manager.py index b1fdcd14..af58babf 100644 --- a/pkg/qqbot/manager.py +++ b/pkg/qqbot/manager.py @@ -10,18 +10,14 @@ from mirai import At, GroupMessage, MessageEvent, Mirai, Plain, StrangerMessage, import config import pkg.openai.session import pkg.openai.manager -from func_timeout import func_set_timeout, FunctionTimedOut -import datetime +from func_timeout import FunctionTimedOut import logging import pkg.qqbot.filter - -help_text = config.help_message +import pkg.qqbot.process as processor inst = None -processing = [] - # 并行运行 def go(func, args=()): @@ -54,7 +50,6 @@ def check_response_rule(text: str) -> (bool, str): # 控制QQ消息输入输出的类 class QQBotManager: - timeout = 60 retry = 3 bot = None @@ -93,6 +88,9 @@ class QQBotManager: ) ) + else: + raise Exception("未知的适配器类型") + @bot.on(FriendMessage) async def on_friend_message(event: FriendMessage): go(self.on_person_message, (event,)) @@ -110,169 +108,11 @@ class QQBotManager: global inst inst = self - # 统一的消息处理函数 - @func_set_timeout(timeout) - def process_message(self, launcher_type: str, launcher_id: int, text_message: str) -> str: - global processing - reply = '' - session_name = "{}_{}".format(launcher_type, launcher_id) - - pkg.openai.session.get_session(session_name).acquire_response_lock() - - try: - if session_name in processing: - pkg.openai.session.get_session(session_name).release_response_lock() - return "[bot]err:正在处理中,请稍后再试" - - processing.append(session_name) - - try: - - if text_message.startswith('!') or text_message.startswith("!"): # 指令 - try: - logging.info( - "[{}]发起指令:{}".format(session_name, text_message[:min(20, len(text_message))] + ( - "..." if len(text_message) > 20 else ""))) - - cmd = text_message[1:].strip().split(' ')[0] - - params = text_message[1:].strip().split(' ')[1:] - if cmd == 'help': - reply = "[bot]" + help_text - elif cmd == 'reset': - pkg.openai.session.get_session(session_name).reset(explicit=True) - reply = "[bot]会话已重置" - elif cmd == 'last': - result = pkg.openai.session.get_session(session_name).last_session() - if result is None: - reply = "[bot]没有前一次的对话" - else: - datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime( - '%Y-%m-%d %H:%M:%S') - reply = "[bot]已切换到前一次的对话:\n创建时间:{}\n".format( - datetime_str) + result.prompt[ - :min(100, - len(result.prompt))] + \ - ("..." if len(result.prompt) > 100 else "#END#") - elif cmd == 'next': - result = pkg.openai.session.get_session(session_name).next_session() - if result is None: - reply = "[bot]没有后一次的对话" - else: - datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime( - '%Y-%m-%d %H:%M:%S') - reply = "[bot]已切换到后一次的对话:\n创建时间:{}\n".format( - datetime_str) + result.prompt[ - :min(100, - len(result.prompt))] + \ - ("..." if len(result.prompt) > 100 else "#END#") - elif cmd == 'prompt': - reply = "[bot]当前对话所有内容:\n" + pkg.openai.session.get_session(session_name).prompt - elif cmd == 'list': - pkg.openai.session.get_session(session_name).persistence() - page = 0 - - if len(params) > 0: - try: - page = int(params[0]) - except ValueError: - pass - - results = pkg.openai.session.get_session(session_name).list_history(page=page) - if len(results) == 0: - reply = "[bot]第{}页没有历史会话".format(page) - else: - reply = "[bot]历史会话 第{}页:\n".format(page) - current = -1 - for i in range(len(results)): - # 时间(使用create_timestamp转换) 序号 部分内容 - datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp']) - reply += "#{} 创建:{} {}\n".format(i + page * 10, - datetime_obj.strftime("%Y-%m-%d %H:%M:%S"), - results[i]['prompt'][ - :min(20, len(results[i]['prompt']))]) - if results[i]['create_timestamp'] == pkg.openai.session.get_session( - session_name).create_timestamp: - current = i + page * 10 - - reply += "\n以上信息倒序排列" - if current != -1: - reply += ",当前会话是 #{}\n".format(current) - else: - reply += ",当前处于全新会话或不在此页" - elif cmd == 'usage': - api_keys = pkg.openai.manager.get_inst().key_mgr.api_key - reply = "[bot]api-key使用情况:(阈值:{})\n\n".format( - pkg.openai.manager.get_inst().key_mgr.api_key_usage_threshold) - - using_key_name = "" - for api_key in api_keys: - reply += "{}:\n - {}字 {}%\n".format(api_key, - pkg.openai.manager.get_inst().key_mgr.get_usage( - api_keys[api_key]), - round( - pkg.openai.manager.get_inst().key_mgr.get_usage( - api_keys[ - api_key]) / pkg.openai.manager.get_inst().key_mgr.api_key_usage_threshold * 100, - 3)) - if api_keys[api_key] == pkg.openai.manager.get_inst().key_mgr.using_key: - using_key_name = api_key - reply += "\n当前使用:{}".format(using_key_name) - except Exception as e: - self.notify_admin("{}指令执行失败:{}".format(session_name, e)) - logging.exception(e) - reply = "[bot]err:{}".format(e) - else: # 消息 - logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + ( - "..." if len(text_message) > 20 else ""))) - - session = pkg.openai.session.get_session(session_name) - try: - prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else "" - reply = prefix + session.append(text_message) - except openai.error.APIConnectionError as e: - self.notify_admin("{}会话调用API失败:{}".format(session_name, e)) - reply = "[bot]err:调用API失败,请重试或联系作者,或等待修复" - except openai.error.RateLimitError as e: - # 尝试切换api-key - current_tokens_amt = pkg.openai.manager.get_inst().key_mgr.get_usage(pkg.openai.manager.get_inst().key_mgr.get_using_key()) - pkg.openai.manager.get_inst().key_mgr.set_current_exceeded() - switched, name = pkg.openai.manager.get_inst().key_mgr.auto_switch() - - if not switched: - self.notify_admin("API调用额度超限({}),请向OpenAI账户充值或在config.py中更换api_key".format(current_tokens_amt)) - reply = "[bot]err:API调用额度超额,请联系作者,或等待修复" - else: - openai.api_key = pkg.openai.manager.get_inst().key_mgr.get_using_key() - self.notify_admin("API调用额度超限({}),已切换到{}".format(current_tokens_amt, name)) - reply = "[bot]err:API调用额度超额,已自动切换,请重新发送消息" - except openai.error.InvalidRequestError as e: - self.notify_admin("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或" - "completion_api_params中的max_tokens参数数值过大导致的,请尝试将其降低".format( - session_name, e)) - reply = "[bot]err:API调用参数错误,请联系作者,或等待修复" - except Exception as e: - logging.exception(e) - reply = "[bot]err:{}".format(e) - - logging.info( - "回复[{}]消息:{}".format(session_name, - reply[:min(100, len(reply))] + ("..." if len(reply) > 100 else ""))) - reply = self.reply_filter.process(reply) - - finally: - processing.remove(session_name) - finally: - pkg.openai.session.get_session(session_name).release_response_lock() - - return reply - def send(self, event, msg): asyncio.run(self.bot.send(event, msg)) # 私聊消息处理 def on_person_message(self, event: MessageEvent): - global processing reply = '' @@ -286,7 +126,7 @@ class QQBotManager: failed = 0 for i in range(self.retry): try: - reply = self.process_message('person', event.sender.id, str(event.message_chain)) + reply = processor.process_message('person', event.sender.id, str(event.message_chain)) break except FunctionTimedOut: pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock() @@ -296,14 +136,13 @@ class QQBotManager: if failed == self.retry: pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock() self.notify_admin("{} 请求超时".format("person_{}".format(event.sender.id))) - reply = "[bot]err:请求超时" + reply = ["[bot]err:请求超时"] - if reply != '': + if reply: return self.send(event, reply) # 群消息处理 def on_group_message(self, event: GroupMessage): - global processing reply = '' @@ -312,13 +151,11 @@ class QQBotManager: if At(self.bot.qq) in event.message_chain: event.message_chain.remove(At(self.bot.qq)) - processing.append("group_{}".format(event.sender.id)) - # 超时则重试,重试超过次数则放弃 failed = 0 for i in range(self.retry): try: - replys = self.process_message('group', event.group.id, + replys = processor.process_message('group', event.group.id, str(event.message_chain).strip() if text is None else text) break except FunctionTimedOut: @@ -327,7 +164,7 @@ class QQBotManager: if failed == self.retry: self.notify_admin("{} 请求超时".format("group_{}".format(event.sender.id))) - replys = "[bot]err:请求超时" + replys = ["[bot]err:请求超时"] return replys @@ -342,7 +179,7 @@ class QQBotManager: # 直接调用 reply = process() - if reply != '': + if reply: return self.send(event, reply) # 通知系统管理员 diff --git a/pkg/qqbot/process.py b/pkg/qqbot/process.py new file mode 100644 index 00000000..30351514 --- /dev/null +++ b/pkg/qqbot/process.py @@ -0,0 +1,199 @@ +# 此模块提供了消息处理的具体逻辑的接口 +import datetime + +import pkg.qqbot.manager as manager +from func_timeout import func_set_timeout +import logging +import openai + +from mirai import Image + +import config + +import pkg.openai.session +import pkg.openai.manager + +processing = [] + + +@func_set_timeout(config.process_message_timeout) +def process_message(launcher_type: str, launcher_id: int, text_message: str) -> []: + global processing + + mgr = pkg.qqbot.manager.get_inst() + + reply = [] + session_name = "{}_{}".format(launcher_type, launcher_id) + + pkg.openai.session.get_session(session_name).acquire_response_lock() + + try: + if session_name in processing: + pkg.openai.session.get_session(session_name).release_response_lock() + return ["[bot]err:正在处理中,请稍后再试"] + + processing.append(session_name) + + try: + + if text_message.startswith('!') or text_message.startswith("!"): # 指令 + try: + logging.info( + "[{}]发起指令:{}".format(session_name, text_message[:min(20, len(text_message))] + ( + "..." if len(text_message) > 20 else ""))) + + cmd = text_message[1:].strip().split(' ')[0] + + params = text_message[1:].strip().split(' ')[1:] + if cmd == 'help': + reply = ["[bot]" + config.help_message] + elif cmd == 'reset': + pkg.openai.session.get_session(session_name).reset(explicit=True) + reply = ["[bot]会话已重置"] + elif cmd == 'last': + result = pkg.openai.session.get_session(session_name).last_session() + if result is None: + reply = ["[bot]没有前一次的对话"] + else: + datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime( + '%Y-%m-%d %H:%M:%S') + reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format( + datetime_str) + result.prompt[ + :min(100, + len(result.prompt))] + \ + ("..." if len(result.prompt) > 100 else "#END#")] + elif cmd == 'next': + result = pkg.openai.session.get_session(session_name).next_session() + if result is None: + reply = ["[bot]没有后一次的对话"] + else: + datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime( + '%Y-%m-%d %H:%M:%S') + reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format( + datetime_str) + result.prompt[ + :min(100, + len(result.prompt))] + \ + ("..." if len(result.prompt) > 100 else "#END#")] + elif cmd == 'prompt': + reply = ["[bot]当前对话所有内容:\n" + pkg.openai.session.get_session(session_name).prompt] + elif cmd == 'list': + pkg.openai.session.get_session(session_name).persistence() + page = 0 + + if len(params) > 0: + try: + page = int(params[0]) + except ValueError: + pass + + results = pkg.openai.session.get_session(session_name).list_history(page=page) + if len(results) == 0: + reply = ["[bot]第{}页没有历史会话".format(page)] + else: + reply_str = "[bot]历史会话 第{}页:\n".format(page) + current = -1 + for i in range(len(results)): + # 时间(使用create_timestamp转换) 序号 部分内容 + datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp']) + reply_str += "#{} 创建:{} {}\n".format(i + page * 10, + datetime_obj.strftime("%Y-%m-%d %H:%M:%S"), + results[i]['prompt'][ + :min(20, len(results[i]['prompt']))]) + if results[i]['create_timestamp'] == pkg.openai.session.get_session( + session_name).create_timestamp: + current = i + page * 10 + + reply_str += "\n以上信息倒序排列" + if current != -1: + reply_str += ",当前会话是 #{}\n".format(current) + else: + reply_str += ",当前处于全新会话或不在此页" + + reply = [reply_str] + elif cmd == 'usage': + api_keys = pkg.openai.manager.get_inst().key_mgr.api_key + reply_str = "[bot]api-key使用情况:(阈值:{})\n\n".format( + pkg.openai.manager.get_inst().key_mgr.api_key_fee_threshold) + + using_key_name = "" + for api_key in api_keys: + reply_str += "{}:\n - {}美元 {}%\n".format(api_key, + round(pkg.openai.manager.get_inst().key_mgr.get_fee( + api_keys[api_key]), 6), + round( + pkg.openai.manager.get_inst().key_mgr.get_fee( + api_keys[ + api_key]) / pkg.openai.manager.get_inst().key_mgr.api_key_fee_threshold * 100, + 3)) + if api_keys[api_key] == pkg.openai.manager.get_inst().key_mgr.using_key: + using_key_name = api_key + reply_str += "\n当前使用:{}".format(using_key_name) + + reply = [reply_str] + + elif cmd == 'draw': + if len(params) == 0: + reply = ["[bot]err:请输入图片描述文字"] + else: + session = pkg.openai.session.get_session(session_name) + + res = session.draw_image(" ".join(params)) + + logging.debug("draw_image result:{}".format(res)) + reply = [Image(url=res['data'][0]['url'])] + except Exception as e: + mgr.notify_admin("{}指令执行失败:{}".format(session_name, e)) + logging.exception(e) + reply = ["[bot]err:{}".format(e)] + else: # 消息 + logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + ( + "..." if len(text_message) > 20 else ""))) + + session = pkg.openai.session.get_session(session_name) + try: + prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else "" + reply = [prefix + session.append(text_message)] + except openai.error.APIConnectionError as e: + mgr.notify_admin("{}会话调用API失败:{}".format(session_name, e)) + reply = ["[bot]err:调用API失败,请重试或联系作者,或等待修复"] + except openai.error.RateLimitError as e: + # 尝试切换api-key + current_tokens_amt = pkg.openai.manager.get_inst().key_mgr.get_fee( + pkg.openai.manager.get_inst().key_mgr.get_using_key()) + pkg.openai.manager.get_inst().key_mgr.set_current_exceeded() + switched, name = pkg.openai.manager.get_inst().key_mgr.auto_switch() + + if not switched: + mgr.notify_admin("API调用额度超限({}),请向OpenAI账户充值或在config.py中更换api_key".format( + current_tokens_amt)) + reply = ["[bot]err:API调用额度超额,请联系作者,或等待修复"] + else: + openai.api_key = pkg.openai.manager.get_inst().key_mgr.get_using_key() + mgr.notify_admin("API调用额度超限({}),已切换到{}".format(current_tokens_amt, name)) + reply = ["[bot]err:API调用额度超额,已自动切换,请重新发送消息"] + except openai.error.InvalidRequestError as e: + mgr.notify_admin("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或" + "completion_api_params中的max_tokens参数数值过大导致的,请尝试将其降低".format( + session_name, e)) + reply = ["[bot]err:API调用参数错误,请联系作者,或等待修复"] + except openai.error.ServiceUnavailableError as e: + # mgr.notify_admin("{}API调用服务不可用:{}".format(session_name, e)) + reply = ["[bot]err:API调用服务暂不可用,请尝试重试"] + except Exception as e: + logging.exception(e) + reply = ["[bot]err:{}".format(e)] + + if reply is not None and type(reply[0]) == str: + logging.info( + "回复[{}]文字消息:{}".format(session_name, + reply[0][:min(100, len(reply[0]))] + ("..." if len(reply[0]) > 100 else ""))) + reply = [mgr.reply_filter.process(reply[0])] + else: + logging.info("回复[{}]图片消息:{}".format(session_name, reply)) + + finally: + processing.remove(session_name) + finally: + pkg.openai.session.get_session(session_name).release_response_lock() + + return reply