mirror of
https://github.com/RockChinQ/QChatGPT.git
synced 2024-11-16 19:57:04 +08:00
commit
d51a0a644a
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -4,4 +4,6 @@ __pycache__/
|
|||
database.db
|
||||
qchatgpt.log
|
||||
config.py
|
||||
banlist.py
|
||||
banlist.py
|
||||
plugins/
|
||||
!plugins/__init__.py
|
15
README.md
15
README.md
|
@ -99,13 +99,26 @@ python3 main.py
|
|||
- 如提示安装`uvicorn`或`hypercorn`请*不要*安装,这两个不是必需的,目前存在未知原因bug
|
||||
- 如报错`TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary`, 请参考 [此处](https://github.com/RockChinQ/QChatGPT/issues/5)
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## 🚀使用
|
||||
|
||||
查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
|
||||
|
||||
## 🧩插件生态
|
||||
|
||||
现已支持自行开发插件对功能进行扩展或自定义程序行为
|
||||
详见[Wiki插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
|
||||
开发教程见[Wiki插件开发页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91)
|
||||
|
||||
### 示例插件
|
||||
|
||||
在`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
|
||||
|
||||
- `cmdcn` - 主程序指令中文形式
|
||||
- `hello_plugin` - 在收到消息`hello`时回复相应消息
|
||||
- `urlikethisijustsix` - 收到冒犯性消息时回复相应消息
|
||||
|
||||
## 👍赞赏
|
||||
|
||||
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/>
|
70
main.py
70
main.py
|
@ -39,6 +39,38 @@ def init_db():
|
|||
known_exception_caught = False
|
||||
|
||||
|
||||
def reset_logging():
|
||||
assert os.path.exists('config.py')
|
||||
|
||||
config = importlib.import_module('config')
|
||||
|
||||
import pkg.utils.context
|
||||
|
||||
if pkg.utils.context.context['logger_handler'] is not None:
|
||||
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
|
||||
|
||||
for handler in logging.getLogger().handlers:
|
||||
logging.getLogger().removeHandler(handler)
|
||||
|
||||
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
|
||||
filename='qchatgpt.log', # log日志输出的文件位置和文件名
|
||||
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
|
||||
# 日志输出的格式
|
||||
# -8表示占位符,让输出左对齐,输出长度都为8位
|
||||
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
|
||||
)
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(config.logging_level)
|
||||
sh.setFormatter(colorlog.ColoredFormatter(
|
||||
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
|
||||
"%(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
log_colors=log_colors_config
|
||||
))
|
||||
logging.getLogger().addHandler(sh)
|
||||
return sh
|
||||
|
||||
|
||||
def main(first_time_init=False):
|
||||
global known_exception_caught
|
||||
|
||||
|
@ -52,25 +84,7 @@ def main(first_time_init=False):
|
|||
import pkg.utils.context
|
||||
pkg.utils.context.set_config(config)
|
||||
|
||||
if pkg.utils.context.context['logger_handler'] is not None:
|
||||
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
|
||||
|
||||
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
|
||||
filename='qchatgpt.log', # log日志输出的文件位置和文件名
|
||||
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
|
||||
# 日志输出的格式
|
||||
# -8表示占位符,让输出左对齐,输出长度都为8位
|
||||
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
|
||||
)
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(config.logging_level)
|
||||
sh.setFormatter(colorlog.ColoredFormatter(
|
||||
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
|
||||
"%(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
log_colors=log_colors_config
|
||||
))
|
||||
logging.getLogger().addHandler(sh)
|
||||
sh = reset_logging()
|
||||
|
||||
# 检查是否设置了管理员
|
||||
if not (hasattr(config, 'admin_qq') and config.admin_qq != 0):
|
||||
|
@ -117,9 +131,16 @@ def main(first_time_init=False):
|
|||
timeout=config.process_message_timeout, retry=config.retry_times,
|
||||
first_time_init=first_time_init)
|
||||
|
||||
# 加载插件
|
||||
import pkg.plugin.host
|
||||
pkg.plugin.host.load_plugins()
|
||||
|
||||
pkg.plugin.host.initialize_plugins()
|
||||
|
||||
if first_time_init: # 不是热重载之后的启动,则不启动新的bot线程
|
||||
|
||||
import mirai.exceptions
|
||||
|
||||
def run_bot_wrapper():
|
||||
global known_exception_caught
|
||||
try:
|
||||
|
@ -155,7 +176,7 @@ def main(first_time_init=False):
|
|||
known_exception_caught = True
|
||||
else:
|
||||
logging.error(
|
||||
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/issues 查找或提issue".format(e))
|
||||
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/QChatGPT/issues 查找或提issue".format(e))
|
||||
known_exception_caught = True
|
||||
raise e
|
||||
|
||||
|
@ -201,6 +222,9 @@ def stop():
|
|||
import pkg.qqbot.manager
|
||||
import pkg.openai.session
|
||||
try:
|
||||
import pkg.plugin.host
|
||||
pkg.plugin.host.unload_plugins()
|
||||
|
||||
qqbot_inst = pkg.utils.context.get_qqbot_manager()
|
||||
assert isinstance(qqbot_inst, pkg.qqbot.manager.QQBotManager)
|
||||
|
||||
|
@ -230,6 +254,12 @@ if __name__ == '__main__':
|
|||
|
||||
elif len(sys.argv) > 1 and sys.argv[1] == 'update':
|
||||
try:
|
||||
try:
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.ensure_dulwich()
|
||||
except:
|
||||
pass
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
repo = porcelain.open_repo('.')
|
||||
|
|
|
@ -40,6 +40,9 @@ class DataGatherer:
|
|||
except:
|
||||
return
|
||||
|
||||
def get_usage(self, key_md5):
|
||||
return self.usage[key_md5] if key_md5 in self.usage else {}
|
||||
|
||||
def report_text_model_usage(self, model, text):
|
||||
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
|
||||
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
import hashlib
|
||||
import logging
|
||||
|
||||
import pkg.database.manager
|
||||
import pkg.qqbot.manager
|
||||
import pkg.utils.context
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
|
||||
class KeysManager:
|
||||
api_key = {}
|
||||
|
@ -39,13 +37,10 @@ class KeysManager:
|
|||
elif type(api_key) is list:
|
||||
for i in range(len(api_key)):
|
||||
self.api_key[str(i)] = api_key[i]
|
||||
|
||||
self.auto_switch()
|
||||
# 从usage中删除未加载的api-key的记录
|
||||
# 不删了,也许会运行时添加曾经有记录的api-key
|
||||
|
||||
if 'exceeded_keys' in pkg.utils.context.context and pkg.utils.context.context['exceeded_keys'] is not None:
|
||||
self.exceeded = pkg.utils.context.context['exceeded_keys']
|
||||
self.auto_switch()
|
||||
|
||||
# 根据tested自动切换到可用的api-key
|
||||
# 返回是否切换成功, 切换后的api-key的别名
|
||||
|
@ -53,7 +48,16 @@ class KeysManager:
|
|||
for key_name in self.api_key:
|
||||
if self.api_key[key_name] not in self.exceeded:
|
||||
self.using_key = self.api_key[key_name]
|
||||
|
||||
logging.info("使用api-key:" + key_name)
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
"key_name": key_name,
|
||||
"key_list": self.api_key.keys()
|
||||
}
|
||||
_ = plugin_host.emit(plugin_models.KeySwitched, **args)
|
||||
|
||||
return True, key_name
|
||||
|
||||
self.using_key = list(self.api_key.values())[0]
|
||||
|
|
|
@ -6,6 +6,9 @@ import pkg.openai.manager
|
|||
import pkg.database.manager
|
||||
import pkg.utils.context
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
|
||||
# 运行时保存的所有session
|
||||
sessions = {}
|
||||
|
||||
|
@ -120,6 +123,17 @@ class Session:
|
|||
config = pkg.utils.context.get_config()
|
||||
if int(time.time()) - self.last_interact_timestamp > config.session_expire_time:
|
||||
logging.info('session {} 已过期'.format(self.name))
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
'session_name': self.name,
|
||||
'session': self,
|
||||
'session_expire_time': config.session_expire_time
|
||||
}
|
||||
event = pkg.plugin.host.emit(plugin_models.SessionExpired, **args)
|
||||
if event.is_prevented_default():
|
||||
return
|
||||
|
||||
self.reset(expired=True, schedule_new=False)
|
||||
|
||||
# 删除此session
|
||||
|
@ -131,6 +145,18 @@ class Session:
|
|||
def append(self, text: str) -> str:
|
||||
self.last_interact_timestamp = int(time.time())
|
||||
|
||||
# 触发插件事件
|
||||
if self.prompt == self.get_default_prompt():
|
||||
args = {
|
||||
'session_name': self.name,
|
||||
'session': self,
|
||||
'default_prompt': self.prompt,
|
||||
}
|
||||
|
||||
event = pkg.plugin.host.emit(plugin_models.SessionFirstMessageReceived, **args)
|
||||
if event.is_prevented_default():
|
||||
return None
|
||||
|
||||
# max_rounds = config.prompt_submit_round_amount if hasattr(config, 'prompt_submit_round_amount') else 7
|
||||
config = pkg.utils.context.get_config()
|
||||
max_rounds = 1000 # 不再限制回合数
|
||||
|
@ -220,6 +246,15 @@ class Session:
|
|||
if self.prompt != self.get_default_prompt():
|
||||
self.persistence()
|
||||
if explicit:
|
||||
# 触发插件事件
|
||||
args = {
|
||||
'session_name': self.name,
|
||||
'session': self
|
||||
}
|
||||
|
||||
# 此事件不支持阻止默认行为
|
||||
_ = pkg.plugin.host.emit(plugin_models.SessionExplicitReset, **args)
|
||||
|
||||
pkg.utils.context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
|
||||
|
||||
if expired:
|
||||
|
|
0
pkg/plugin/__init__.py
Normal file
0
pkg/plugin/__init__.py
Normal file
273
pkg/plugin/host.py
Normal file
273
pkg/plugin/host.py
Normal file
|
@ -0,0 +1,273 @@
|
|||
# 插件管理模块
|
||||
import asyncio
|
||||
import logging
|
||||
import importlib
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pkg.utils.context as context
|
||||
import pkg.plugin.switch as switch
|
||||
|
||||
from mirai import Mirai
|
||||
|
||||
__plugins__ = {}
|
||||
"""
|
||||
插件列表
|
||||
|
||||
示例:
|
||||
{
|
||||
"example": {
|
||||
"path": "plugins/example/main.py",
|
||||
"enabled: True,
|
||||
"name": "example",
|
||||
"description": "example",
|
||||
"version": "0.0.1",
|
||||
"author": "RockChinQ",
|
||||
"class": <class 'plugins.example.ExamplePlugin'>,
|
||||
"hooks": {
|
||||
"person_message": [
|
||||
<function ExamplePlugin.person_message at 0x0000020E1D1B8D38>
|
||||
]
|
||||
},
|
||||
"instance": None
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
__current_module_path__ = ""
|
||||
|
||||
|
||||
def walk_plugin_path(module, prefix='', path_prefix=''):
|
||||
global __current_module_path__
|
||||
"""遍历插件路径"""
|
||||
for item in pkgutil.iter_modules(module.__path__):
|
||||
if item.ispkg:
|
||||
logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name))
|
||||
walk_plugin_path(__import__(module.__name__ + '.' + item.name, fromlist=['']),
|
||||
prefix + item.name + '.', path_prefix + item.name + '/')
|
||||
else:
|
||||
logging.debug("扫描插件模块: plugins/{}".format(path_prefix + item.name + '.py'))
|
||||
logging.info('加载模块: plugins/{}'.format(path_prefix + item.name + '.py'))
|
||||
__current_module_path__ = "plugins/"+path_prefix + item.name + '.py'
|
||||
|
||||
importlib.import_module(module.__name__ + '.' + item.name)
|
||||
|
||||
|
||||
def load_plugins():
|
||||
""" 加载插件 """
|
||||
logging.info("加载插件")
|
||||
PluginHost()
|
||||
walk_plugin_path(__import__('plugins'))
|
||||
|
||||
logging.debug(__plugins__)
|
||||
|
||||
# 加载开关数据
|
||||
switch.load_switch()
|
||||
|
||||
|
||||
def initialize_plugins():
|
||||
""" 初始化插件 """
|
||||
logging.info("初始化插件")
|
||||
for plugin in __plugins__.values():
|
||||
if not plugin['enabled']:
|
||||
continue
|
||||
try:
|
||||
plugin['instance'] = plugin["class"](plugin_host=context.get_plugin_host())
|
||||
except:
|
||||
logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
|
||||
|
||||
|
||||
def unload_plugins():
|
||||
""" 卸载插件 """
|
||||
for plugin in __plugins__.values():
|
||||
if plugin['enabled'] and plugin['instance'] is not None:
|
||||
if not hasattr(plugin['instance'], '__del__'):
|
||||
logging.warning("插件{}没有定义析构函数".format(plugin['name']))
|
||||
else:
|
||||
try:
|
||||
plugin['instance'].__del__()
|
||||
logging.info("卸载插件: {}".format(plugin['name']))
|
||||
except:
|
||||
logging.error("插件{}卸载时发生错误: {}".format(plugin['name'], sys.exc_info()))
|
||||
|
||||
|
||||
def install_plugin(repo_url: str):
|
||||
""" 安装插件,从git储存库获取并解决依赖 """
|
||||
try:
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.ensure_dulwich()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
logging.info("克隆插件储存库: {}".format(repo_url))
|
||||
repo = porcelain.clone(repo_url, "plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/", checkout=True)
|
||||
|
||||
# 检查此目录是否包含requirements.txt
|
||||
if os.path.exists("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt"):
|
||||
logging.info("检测到requirements.txt,正在安装依赖")
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.install_requirements("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt")
|
||||
|
||||
import main
|
||||
main.reset_logging()
|
||||
|
||||
|
||||
class EventContext:
|
||||
""" 事件上下文 """
|
||||
eid = 0
|
||||
"""事件编号"""
|
||||
|
||||
name = ""
|
||||
|
||||
__prevent_default__ = False
|
||||
""" 是否阻止默认行为 """
|
||||
|
||||
__prevent_postorder__ = False
|
||||
""" 是否阻止后续插件的执行 """
|
||||
|
||||
__return_value__ = {}
|
||||
""" 返回值
|
||||
示例:
|
||||
{
|
||||
"example": [
|
||||
'value1',
|
||||
'value2',
|
||||
3,
|
||||
4,
|
||||
{
|
||||
'key1': 'value1',
|
||||
},
|
||||
['value1', 'value2']
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
def add_return(self, key: str, ret):
|
||||
"""添加返回值"""
|
||||
if key not in self.__return_value__:
|
||||
self.__return_value__[key] = []
|
||||
self.__return_value__[key].append(ret)
|
||||
|
||||
def get_return(self, key: str):
|
||||
"""获取key的所有返回值"""
|
||||
if key in self.__return_value__:
|
||||
return self.__return_value__[key]
|
||||
return None
|
||||
|
||||
def get_return_value(self, key: str):
|
||||
"""获取key的首个返回值"""
|
||||
if key in self.__return_value__:
|
||||
return self.__return_value__[key][0]
|
||||
return None
|
||||
|
||||
def prevent_default(self):
|
||||
"""阻止默认行为"""
|
||||
self.__prevent_default__ = True
|
||||
|
||||
def prevent_postorder(self):
|
||||
"""阻止后续插件执行"""
|
||||
self.__prevent_postorder__ = True
|
||||
|
||||
def is_prevented_default(self):
|
||||
"""是否阻止默认行为"""
|
||||
return self.__prevent_default__
|
||||
|
||||
def is_prevented_postorder(self):
|
||||
"""是否阻止后序插件执行"""
|
||||
return self.__prevent_postorder__
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.eid = EventContext.eid
|
||||
self.__prevent_default__ = False
|
||||
self.__prevent_postorder__ = False
|
||||
self.__return_value__ = {}
|
||||
EventContext.eid += 1
|
||||
|
||||
|
||||
def emit(event_name: str, **kwargs) -> EventContext:
|
||||
""" 触发事件 """
|
||||
import pkg.utils.context as context
|
||||
if context.get_plugin_host() is None:
|
||||
return None
|
||||
return context.get_plugin_host().emit(event_name, **kwargs)
|
||||
|
||||
|
||||
class PluginHost:
|
||||
"""插件宿主"""
|
||||
|
||||
def __init__(self):
|
||||
context.set_plugin_host(self)
|
||||
|
||||
def get_runtime_context(self) -> context:
|
||||
"""获取运行时上下文(pkg.utils.context模块的对象)
|
||||
|
||||
此上下文用于和主程序其他模块交互(数据库、QQ机器人、OpenAI接口等)
|
||||
详见pkg.utils.context模块
|
||||
其中的context变量保存了其他重要模块的类对象,可以使用这些对象进行交互
|
||||
"""
|
||||
return context
|
||||
|
||||
def get_bot(self) -> Mirai:
|
||||
"""获取机器人对象"""
|
||||
return context.get_qqbot_manager().bot
|
||||
|
||||
def send_person_message(self, person, message):
|
||||
"""发送私聊消息"""
|
||||
asyncio.run(self.get_bot().send_friend_message(person, message))
|
||||
|
||||
def send_group_message(self, group, message):
|
||||
"""发送群消息"""
|
||||
asyncio.run(self.get_bot().send_group_message(group, message))
|
||||
|
||||
def notify_admin(self, message):
|
||||
"""通知管理员"""
|
||||
context.get_qqbot_manager().notify_admin(message)
|
||||
|
||||
def emit(self, event_name: str, **kwargs) -> EventContext:
|
||||
""" 触发事件 """
|
||||
event_context = EventContext(event_name)
|
||||
logging.debug("触发事件: {} ({})".format(event_name, event_context.eid))
|
||||
for plugin in __plugins__.values():
|
||||
|
||||
if not plugin['enabled']:
|
||||
continue
|
||||
|
||||
if plugin['instance'] is None:
|
||||
# 从关闭状态切到开启状态之后,重新加载插件
|
||||
try:
|
||||
plugin['instance'] = plugin["class"]()
|
||||
except:
|
||||
logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
|
||||
continue
|
||||
|
||||
for hook in plugin['hooks'].get(event_name, []):
|
||||
try:
|
||||
already_prevented_default = event_context.is_prevented_default()
|
||||
|
||||
kwargs['host'] = context.get_plugin_host()
|
||||
kwargs['event'] = event_context
|
||||
|
||||
hook(plugin['instance'], **kwargs)
|
||||
|
||||
if event_context.is_prevented_default() and not already_prevented_default:
|
||||
logging.debug("插件 {} 已要求阻止事件 {} 的默认行为".format(plugin['name'], event_name))
|
||||
|
||||
if event_context.is_prevented_postorder():
|
||||
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin['name']))
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logging.error("插件{}触发事件{}时发生错误".format(plugin['name'], event_name))
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
return event_context
|
219
pkg/plugin/models.py
Normal file
219
pkg/plugin/models.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
import logging
|
||||
|
||||
import pkg.plugin.host as host
|
||||
import pkg.utils.context
|
||||
|
||||
PersonMessageReceived = "person_message_received"
|
||||
"""收到私聊消息时,在判断是否应该响应前触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
message_chain: mirai.models.message.MessageChain 消息链
|
||||
"""
|
||||
|
||||
GroupMessageReceived = "group_message_received"
|
||||
"""收到群聊消息时,在判断是否应该响应前触发(所有群消息)
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
message_chain: mirai.models.message.MessageChain 消息链
|
||||
"""
|
||||
|
||||
PersonNormalMessageReceived = "person_normal_message_received"
|
||||
"""判断为应该处理的私聊普通消息时触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
text_message: str 消息文本
|
||||
|
||||
returns (optional):
|
||||
alter: str 修改后的消息文本
|
||||
reply: list 回复消息组件列表
|
||||
"""
|
||||
|
||||
PersonCommandSent = "person_command_sent"
|
||||
"""判断为应该处理的私聊指令时触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
command: str 指令
|
||||
params: list[str] 参数列表
|
||||
text_message: str 完整指令文本
|
||||
is_admin: bool 是否为管理员
|
||||
|
||||
returns (optional):
|
||||
alter: str 修改后的完整指令文本
|
||||
reply: list 回复消息组件列表
|
||||
"""
|
||||
|
||||
GroupNormalMessageReceived = "group_normal_message_received"
|
||||
"""判断为应该处理的群聊普通消息时触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
text_message: str 消息文本
|
||||
|
||||
returns (optional):
|
||||
alter: str 修改后的消息文本
|
||||
reply: list 回复消息组件列表
|
||||
"""
|
||||
|
||||
GroupCommandSent = "group_command_sent"
|
||||
"""判断为应该处理的群聊指令时触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
command: str 指令
|
||||
params: list[str] 参数列表
|
||||
text_message: str 完整指令文本
|
||||
is_admin: bool 是否为管理员
|
||||
|
||||
returns (optional):
|
||||
alter: str 修改后的完整指令文本
|
||||
reply: list 回复消息组件列表
|
||||
"""
|
||||
|
||||
NormalMessageResponded = "normal_message_responded"
|
||||
"""获取到对普通消息的文字响应时触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
session: pkg.openai.session.Session 会话对象
|
||||
prefix: str 回复文字消息的前缀
|
||||
response_text: str 响应文本
|
||||
|
||||
returns (optional):
|
||||
prefix: str 修改后的回复文字消息的前缀
|
||||
reply: list 替换回复消息组件列表
|
||||
"""
|
||||
|
||||
SessionFirstMessageReceived = "session_first_message_received"
|
||||
"""会话被第一次交互时触发
|
||||
kwargs:
|
||||
session_name: str 会话名称(<launcher_type>_<launcher_id>)
|
||||
session: pkg.openai.session.Session 会话对象
|
||||
default_prompt: str 预设值
|
||||
"""
|
||||
|
||||
SessionExplicitReset = "session_reset"
|
||||
"""会话被用户手动重置时触发,此事件不支持阻止默认行为
|
||||
kwargs:
|
||||
session_name: str 会话名称(<launcher_type>_<launcher_id>)
|
||||
session: pkg.openai.session.Session 会话对象
|
||||
"""
|
||||
|
||||
SessionExpired = "session_expired"
|
||||
"""会话过期时触发
|
||||
kwargs:
|
||||
session_name: str 会话名称(<launcher_type>_<launcher_id>)
|
||||
session: pkg.openai.session.Session 会话对象
|
||||
session_expire_time: int 已设置的会话过期时间(秒)
|
||||
"""
|
||||
|
||||
KeyExceeded = "key_exceeded"
|
||||
"""api-key超额时触发
|
||||
kwargs:
|
||||
key_name: str 超额的api-key名称
|
||||
usage: dict 超额的api-key使用情况
|
||||
exceeded_keys: list[str] 超额的api-key列表
|
||||
"""
|
||||
|
||||
KeySwitched = "key_switched"
|
||||
"""api-key超额切换成功时触发,此事件不支持阻止默认行为
|
||||
kwargs:
|
||||
key_name: str 切换成功的api-key名称
|
||||
key_list: list[str] api-key列表
|
||||
"""
|
||||
|
||||
|
||||
def on(event: str):
|
||||
"""注册事件监听器
|
||||
:param
|
||||
event: str 事件名称
|
||||
"""
|
||||
return Plugin.on(event)
|
||||
|
||||
|
||||
__current_registering_plugin__ = ""
|
||||
|
||||
|
||||
class Plugin:
|
||||
|
||||
host: host.PluginHost
|
||||
"""插件宿主,提供插件的一些基础功能"""
|
||||
|
||||
@classmethod
|
||||
def on(cls, event):
|
||||
"""事件处理器装饰器
|
||||
|
||||
:param
|
||||
event: 事件类型
|
||||
:return:
|
||||
None
|
||||
"""
|
||||
|
||||
def wrapper(func):
|
||||
plugin_hooks = host.__plugins__[__current_registering_plugin__]["hooks"]
|
||||
|
||||
if event not in plugin_hooks:
|
||||
plugin_hooks[event] = []
|
||||
plugin_hooks[event].append(func)
|
||||
|
||||
host.__plugins__[__current_registering_plugin__]["hooks"] = plugin_hooks
|
||||
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def register(name: str, description: str, version: str, author: str):
|
||||
"""注册插件, 此函数作为装饰器使用
|
||||
|
||||
Args:
|
||||
name (str): 插件名称
|
||||
description (str): 插件描述
|
||||
version (str): 插件版本
|
||||
author (str): 插件作者
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
global __current_registering_plugin__
|
||||
|
||||
__current_registering_plugin__ = name
|
||||
|
||||
host.__plugins__[name] = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"version": version,
|
||||
"author": author,
|
||||
"hooks": {},
|
||||
"path": host.__current_module_path__,
|
||||
"enabled": True,
|
||||
"instance": None,
|
||||
}
|
||||
|
||||
def wrapper(cls: Plugin):
|
||||
cls.name = name
|
||||
cls.description = description
|
||||
cls.version = version
|
||||
cls.author = author
|
||||
cls.host = pkg.utils.context.get_plugin_host()
|
||||
cls.enabled = True
|
||||
cls.path = host.__current_module_path__
|
||||
|
||||
# 存到插件列表
|
||||
host.__plugins__[name]["class"] = cls
|
||||
|
||||
logging.info("插件注册完成: n='{}', d='{}', v={}, a='{}' ({})".format(name, description, version, author, cls))
|
||||
|
||||
return cls
|
||||
|
||||
return wrapper
|
87
pkg/plugin/switch.py
Normal file
87
pkg/plugin/switch.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
# 控制插件的开关
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pkg.plugin.host as host
|
||||
|
||||
|
||||
def wrapper_dict_from_plugin_list() -> dict:
|
||||
""" 将插件列表转换为开关json """
|
||||
switch = {}
|
||||
|
||||
for plugin_name in host.__plugins__:
|
||||
plugin = host.__plugins__[plugin_name]
|
||||
|
||||
switch[plugin_name] = {
|
||||
"path": plugin["path"],
|
||||
"enabled": plugin["enabled"],
|
||||
}
|
||||
|
||||
return switch
|
||||
|
||||
|
||||
def apply_switch(switch: dict):
|
||||
"""将开关数据应用到插件列表中"""
|
||||
for plugin_name in switch:
|
||||
host.__plugins__[plugin_name]["enabled"] = switch[plugin_name]["enabled"]
|
||||
|
||||
|
||||
def dump_switch():
|
||||
""" 保存开关数据 """
|
||||
logging.debug("保存开关数据")
|
||||
# 将开关数据写入plugins/switch.json
|
||||
|
||||
switch = wrapper_dict_from_plugin_list()
|
||||
|
||||
with open("plugins/switch.json", "w", encoding="utf-8") as f:
|
||||
json.dump(switch, f, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def load_switch():
|
||||
""" 加载开关数据 """
|
||||
logging.debug("加载开关数据")
|
||||
# 读取plugins/switch.json
|
||||
|
||||
switch = {}
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists("plugins/switch.json"):
|
||||
# 不存在则创建
|
||||
with open("plugins/switch.json", "w", encoding="utf-8") as f:
|
||||
json.dump(switch, f, indent=4, ensure_ascii=False)
|
||||
|
||||
with open("plugins/switch.json", "r", encoding="utf-8") as f:
|
||||
switch = json.load(f)
|
||||
|
||||
if switch is None:
|
||||
switch = {}
|
||||
|
||||
switch_modified = False
|
||||
|
||||
switch_copy = switch.copy()
|
||||
# 检查switch中多余的和path不相符的
|
||||
for plugin_name in switch_copy:
|
||||
if plugin_name not in host.__plugins__:
|
||||
del switch[plugin_name]
|
||||
switch_modified = True
|
||||
elif switch[plugin_name]["path"] != host.__plugins__[plugin_name]["path"]:
|
||||
# 删除此不相符的
|
||||
del switch[plugin_name]
|
||||
switch_modified = True
|
||||
|
||||
# 检查plugin中多余的
|
||||
for plugin_name in host.__plugins__:
|
||||
if plugin_name not in switch:
|
||||
switch[plugin_name] = {
|
||||
"path": host.__plugins__[plugin_name]["path"],
|
||||
"enabled": host.__plugins__[plugin_name]["enabled"],
|
||||
}
|
||||
switch_modified = True
|
||||
|
||||
# 如果switch有修改,保存
|
||||
if switch_modified:
|
||||
dump_switch()
|
||||
|
||||
# 应用开关数据
|
||||
apply_switch(switch)
|
313
pkg/qqbot/command.py
Normal file
313
pkg/qqbot/command.py
Normal file
|
@ -0,0 +1,313 @@
|
|||
# 指令处理模块
|
||||
import logging
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
import threading
|
||||
|
||||
import pkg.openai.session
|
||||
import pkg.openai.manager
|
||||
import pkg.utils.reloader
|
||||
import pkg.utils.updater
|
||||
import pkg.utils.context
|
||||
import pkg.qqbot.message
|
||||
|
||||
from mirai import Image
|
||||
|
||||
|
||||
def config_operation(cmd, params):
|
||||
reply = []
|
||||
config = pkg.utils.context.get_config()
|
||||
reply_str = ""
|
||||
if len(params) == 0:
|
||||
reply = ["[bot]err:请输入配置项"]
|
||||
else:
|
||||
cfg_name = params[0]
|
||||
if cfg_name == 'all':
|
||||
reply_str = "[bot]所有配置项:\n\n"
|
||||
for cfg in dir(config):
|
||||
if not cfg.startswith('__') and not cfg == 'logging':
|
||||
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
|
||||
if isinstance(getattr(config, cfg), str):
|
||||
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
|
||||
elif isinstance(getattr(config, cfg), dict):
|
||||
# 不进行unicode转义,并格式化
|
||||
reply_str += "{}: {}\n".format(cfg,
|
||||
json.dumps(getattr(config, cfg),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
|
||||
reply = [reply_str]
|
||||
elif cfg_name in dir(config):
|
||||
if len(params) == 1:
|
||||
# 按照配置项类型进行格式化
|
||||
if isinstance(getattr(config, cfg_name), str):
|
||||
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
|
||||
elif isinstance(getattr(config, cfg_name), dict):
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
|
||||
json.dumps(getattr(config, cfg_name),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
|
||||
reply = [reply_str]
|
||||
else:
|
||||
cfg_value = " ".join(params[1:])
|
||||
# 类型转换,如果是json则转换为字典
|
||||
if cfg_value == 'true':
|
||||
cfg_value = True
|
||||
elif cfg_value == 'false':
|
||||
cfg_value = False
|
||||
elif cfg_value.isdigit():
|
||||
cfg_value = int(cfg_value)
|
||||
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
|
||||
cfg_value = json.loads(cfg_value)
|
||||
else:
|
||||
try:
|
||||
cfg_value = float(cfg_value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 检查类型是否匹配
|
||||
if isinstance(getattr(config, cfg_name), type(cfg_value)):
|
||||
setattr(config, cfg_name, cfg_value)
|
||||
pkg.utils.context.set_config(config)
|
||||
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
|
||||
else:
|
||||
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
|
||||
|
||||
else:
|
||||
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
def plugin_operation(cmd, params, is_admin):
|
||||
reply = []
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.utils.updater as updater
|
||||
|
||||
plugin_list = plugin_host.__plugins__
|
||||
|
||||
if len(params) == 0:
|
||||
reply_str = "[bot]所有插件({}):\n\n".format(len(plugin_list))
|
||||
idx = 0
|
||||
for key in plugin_list:
|
||||
plugin = plugin_list[key]
|
||||
reply_str += "#{} {}:\n{}\nv{}\n作者: {}\n".format((idx+1), plugin['name'], plugin['description'],
|
||||
plugin['version'], plugin['author'])
|
||||
|
||||
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
|
||||
reply_str += "源码: "+updater.get_remote_url("/".join(plugin['path'].split('/')[:-1]))+"\n"
|
||||
|
||||
reply_str += "\n"
|
||||
|
||||
idx += 1
|
||||
|
||||
reply = [reply_str]
|
||||
elif params[0] == 'update':
|
||||
# 更新所有插件
|
||||
if is_admin:
|
||||
def closure():
|
||||
import pkg.utils.context
|
||||
updated = []
|
||||
for key in plugin_list:
|
||||
plugin = plugin_list[key]
|
||||
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
|
||||
success = updater.pull_latest("/".join(plugin['path'].split('/')[:-1]))
|
||||
if success:
|
||||
updated.append(plugin['name'])
|
||||
|
||||
# 检查是否有requirements.txt
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("正在安装依赖...")
|
||||
for key in plugin_list:
|
||||
plugin = plugin_list[key]
|
||||
if os.path.exists("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt"):
|
||||
logging.info("{}检测到requirements.txt,安装依赖".format(plugin['name']))
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.install_requirements("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt")
|
||||
|
||||
import main
|
||||
main.reset_logging()
|
||||
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("[bot]已更新插件: {}".format(", ".join(updated)))
|
||||
|
||||
threading.Thread(target=closure).start()
|
||||
reply = ["[bot]正在更新所有插件,请勿重复发起..."]
|
||||
else:
|
||||
reply = ["[bot]err:权限不足"]
|
||||
elif params[0].startswith("http"):
|
||||
if is_admin:
|
||||
|
||||
def closure():
|
||||
try:
|
||||
plugin_host.install_plugin(params[0])
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 指令重载插件")
|
||||
except Exception as e:
|
||||
logging.error("插件安装失败:{}".format(e))
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
|
||||
|
||||
threading.Thread(target=closure, args=()).start()
|
||||
reply = ["[bot]正在安装插件..."]
|
||||
else:
|
||||
reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
|
||||
return reply
|
||||
|
||||
|
||||
def process_command(session_name: str, text_message: str, mgr, config,
|
||||
launcher_type: str, launcher_id: int, sender_id: int) -> list:
|
||||
reply = []
|
||||
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 == 'resend':
|
||||
session = pkg.openai.session.get_session(session_name)
|
||||
to_send = session.undo()
|
||||
|
||||
reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config,
|
||||
launcher_type, launcher_id, sender_id)
|
||||
elif cmd == 'usage':
|
||||
reply_str = "[bot]各api-key使用情况:\n\n"
|
||||
|
||||
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
|
||||
for key_name in api_keys:
|
||||
text_length = pkg.utils.context.get_openai_manager().audit_mgr \
|
||||
.get_text_length_of_key(api_keys[key_name])
|
||||
image_count = pkg.utils.context.get_openai_manager().audit_mgr \
|
||||
.get_image_count_of_key(api_keys[key_name])
|
||||
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
|
||||
int(image_count))
|
||||
|
||||
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'])]
|
||||
if not (hasattr(config, 'include_image_description')
|
||||
and not config.include_image_description):
|
||||
reply.append(" ".join(params))
|
||||
elif cmd == 'version':
|
||||
reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
|
||||
try:
|
||||
if pkg.utils.updater.is_new_version_available():
|
||||
reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
|
||||
except:
|
||||
pass
|
||||
|
||||
reply = [reply_str]
|
||||
|
||||
elif cmd == 'plugin':
|
||||
reply = plugin_operation(cmd, params, True
|
||||
if (launcher_type == 'person' and launcher_id == config.admin_qq)
|
||||
else False)
|
||||
elif cmd == 'reload' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
def reload_task():
|
||||
pkg.utils.reloader.reload_all()
|
||||
|
||||
threading.Thread(target=reload_task, daemon=True).start()
|
||||
elif cmd == 'update' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
def update_task():
|
||||
try:
|
||||
if pkg.utils.updater.update_all():
|
||||
pkg.utils.reloader.reload_all(notify=False)
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
|
||||
else:
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
|
||||
except Exception as e0:
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
|
||||
return
|
||||
|
||||
threading.Thread(target=update_task, daemon=True).start()
|
||||
|
||||
reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
|
||||
elif cmd == 'cfg' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
reply = config_operation(cmd, params)
|
||||
else:
|
||||
if cmd.startswith("~") and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
config_item = cmd[1:]
|
||||
params = [config_item] + params
|
||||
reply = config_operation("cfg", params)
|
||||
else:
|
||||
reply = ["[bot]err:未知的指令或权限不足: " + cmd]
|
||||
except Exception as e:
|
||||
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
|
||||
logging.exception(e)
|
||||
reply = ["[bot]err:{}".format(e)]
|
||||
|
||||
return reply
|
|
@ -16,6 +16,9 @@ import pkg.qqbot.filter
|
|||
import pkg.qqbot.process as processor
|
||||
import pkg.utils.context
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
|
||||
|
||||
# 并行运行
|
||||
def go(func, args=()):
|
||||
|
@ -51,7 +54,7 @@ def check_response_rule(text: str) -> (bool, str):
|
|||
class QQBotManager:
|
||||
retry = 3
|
||||
|
||||
bot = None
|
||||
bot: Mirai = None
|
||||
|
||||
reply_filter = None
|
||||
|
||||
|
@ -95,15 +98,64 @@ class QQBotManager:
|
|||
# Caution: 注册新的事件处理器之后,请务必在unsubscribe_all中编写相应的取消订阅代码
|
||||
@self.bot.on(FriendMessage)
|
||||
async def on_friend_message(event: FriendMessage):
|
||||
go(self.on_person_message, (event,))
|
||||
|
||||
def friend_message_handler(event: FriendMessage):
|
||||
|
||||
# 触发事件
|
||||
args = {
|
||||
"launcher_type": "person",
|
||||
"launcher_id": event.sender.id,
|
||||
"sender_id": event.sender.id,
|
||||
"message_chain": event.message_chain,
|
||||
}
|
||||
plugin_event = plugin_host.emit(plugin_models.PersonMessageReceived, **args)
|
||||
|
||||
if plugin_event.is_prevented_default():
|
||||
return
|
||||
|
||||
self.on_person_message(event)
|
||||
|
||||
go(friend_message_handler, (event,))
|
||||
|
||||
@self.bot.on(StrangerMessage)
|
||||
async def on_stranger_message(event: StrangerMessage):
|
||||
go(self.on_person_message, (event,))
|
||||
|
||||
def stranger_message_handler(event: StrangerMessage):
|
||||
# 触发事件
|
||||
args = {
|
||||
"launcher_type": "person",
|
||||
"launcher_id": event.sender.id,
|
||||
"sender_id": event.sender.id,
|
||||
"message_chain": event.message_chain,
|
||||
}
|
||||
plugin_event = plugin_host.emit(plugin_models.PersonMessageReceived, **args)
|
||||
|
||||
if plugin_event.is_prevented_default():
|
||||
return
|
||||
|
||||
self.on_person_message(event)
|
||||
|
||||
go(stranger_message_handler, (event,))
|
||||
|
||||
@self.bot.on(GroupMessage)
|
||||
async def on_group_message(event: GroupMessage):
|
||||
go(self.on_group_message, (event,))
|
||||
|
||||
def group_message_handler(event: GroupMessage):
|
||||
# 触发事件
|
||||
args = {
|
||||
"launcher_type": "group",
|
||||
"launcher_id": event.group.id,
|
||||
"sender_id": event.sender.id,
|
||||
"message_chain": event.message_chain,
|
||||
}
|
||||
plugin_event = plugin_host.emit(plugin_models.GroupMessageReceived, **args)
|
||||
|
||||
if plugin_event.is_prevented_default():
|
||||
return
|
||||
|
||||
self.on_group_message(event)
|
||||
|
||||
go(group_message_handler, (event,))
|
||||
|
||||
def unsubscribe_all():
|
||||
"""取消所有订阅
|
||||
|
@ -155,6 +207,7 @@ class QQBotManager:
|
|||
|
||||
# 私聊消息处理
|
||||
def on_person_message(self, event: MessageEvent):
|
||||
|
||||
reply = ''
|
||||
|
||||
if event.sender.id == self.bot.qq:
|
||||
|
@ -189,6 +242,7 @@ class QQBotManager:
|
|||
|
||||
# 群消息处理
|
||||
def on_group_message(self, event: GroupMessage):
|
||||
|
||||
reply = ''
|
||||
|
||||
def process(text=None) -> str:
|
||||
|
|
92
pkg/qqbot/message.py
Normal file
92
pkg/qqbot/message.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
# 普通消息处理模块
|
||||
import logging
|
||||
import openai
|
||||
import pkg.utils.context
|
||||
import pkg.openai.session
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
|
||||
|
||||
def process_normal_message(text_message: str, mgr, config, launcher_type: str,
|
||||
launcher_id: int, sender_id: int) -> list:
|
||||
session_name = f"{launcher_type}_{launcher_id}"
|
||||
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)
|
||||
|
||||
reply = []
|
||||
while True:
|
||||
try:
|
||||
prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else ""
|
||||
|
||||
text = session.append(text_message)
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
"launcher_type": launcher_type,
|
||||
"launcher_id": launcher_id,
|
||||
"sender_id": sender_id,
|
||||
"session": session,
|
||||
"prefix": prefix,
|
||||
"response_text": text
|
||||
}
|
||||
|
||||
event = pkg.plugin.host.emit(plugin_models.NormalMessageResponded, **args)
|
||||
|
||||
if event.get_return_value("prefix") is not None:
|
||||
prefix = event.get_return_value("prefix")
|
||||
|
||||
if event.get_return_value("reply") is not None:
|
||||
reply = event.get_return_value("reply")
|
||||
|
||||
if not event.is_prevented_default():
|
||||
reply = [prefix + text]
|
||||
except openai.error.APIConnectionError as e:
|
||||
mgr.notify_admin("{}会话调用API失败:{}".format(session_name, e))
|
||||
reply = ["[bot]err:调用API失败,请重试或联系作者,或等待修复"]
|
||||
except openai.error.RateLimitError as e:
|
||||
logging.debug(type(e))
|
||||
# 尝试切换api-key
|
||||
current_key_name = pkg.utils.context.get_openai_manager().key_mgr.get_key_name(
|
||||
pkg.utils.context.get_openai_manager().key_mgr.using_key
|
||||
)
|
||||
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded()
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
'key_name': current_key_name,
|
||||
'usage': pkg.utils.context.get_openai_manager().audit_mgr
|
||||
.get_usage(pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()),
|
||||
'exceeded_keys': pkg.utils.context.get_openai_manager().key_mgr.exceeded,
|
||||
}
|
||||
event = plugin_host.emit(plugin_models.KeyExceeded, **args)
|
||||
|
||||
if not event.is_prevented_default():
|
||||
switched, name = pkg.utils.context.get_openai_manager().key_mgr.auto_switch()
|
||||
|
||||
if not switched:
|
||||
mgr.notify_admin(
|
||||
"api-key调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key".format(
|
||||
current_key_name))
|
||||
reply = ["[bot]err:API调用额度超额,请联系作者,或等待修复"]
|
||||
else:
|
||||
openai.api_key = pkg.utils.context.get_openai_manager().key_mgr.get_using_key()
|
||||
mgr.notify_admin("api-key调用额度超限({}),接口报错,已切换到{}".format(current_key_name, name))
|
||||
reply = ["[bot]err:API调用额度超额,已自动切换,请重新发送消息"]
|
||||
continue
|
||||
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)]
|
||||
break
|
||||
|
||||
return reply
|
|
@ -1,14 +1,10 @@
|
|||
# 此模块提供了消息处理的具体逻辑的接口
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
|
||||
from func_timeout import func_set_timeout
|
||||
import logging
|
||||
import openai
|
||||
|
||||
from mirai import Image, MessageChain, Plain
|
||||
from mirai import MessageChain, Plain
|
||||
|
||||
# 这里不使用动态引入config
|
||||
# 因为在这里动态引入会卡死程序
|
||||
|
@ -20,76 +16,15 @@ import pkg.openai.manager
|
|||
import pkg.utils.reloader
|
||||
import pkg.utils.updater
|
||||
import pkg.utils.context
|
||||
import pkg.qqbot.message
|
||||
import pkg.qqbot.command
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
|
||||
processing = []
|
||||
|
||||
|
||||
def config_operation(cmd, params):
|
||||
reply = []
|
||||
config = pkg.utils.context.get_config()
|
||||
reply_str = ""
|
||||
if len(params) == 0:
|
||||
reply = ["[bot]err:请输入配置项"]
|
||||
else:
|
||||
cfg_name = params[0]
|
||||
if cfg_name == 'all':
|
||||
reply_str = "[bot]所有配置项:\n\n"
|
||||
for cfg in dir(config):
|
||||
if not cfg.startswith('__') and not cfg == 'logging':
|
||||
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
|
||||
if isinstance(getattr(config, cfg), str):
|
||||
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
|
||||
elif isinstance(getattr(config, cfg), dict):
|
||||
# 不进行unicode转义,并格式化
|
||||
reply_str += "{}: {}\n".format(cfg,
|
||||
json.dumps(getattr(config, cfg),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
|
||||
reply = [reply_str]
|
||||
elif cfg_name in dir(config):
|
||||
if len(params) == 1:
|
||||
# 按照配置项类型进行格式化
|
||||
if isinstance(getattr(config, cfg_name), str):
|
||||
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
|
||||
elif isinstance(getattr(config, cfg_name), dict):
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
|
||||
json.dumps(getattr(config, cfg_name),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
|
||||
reply = [reply_str]
|
||||
else:
|
||||
cfg_value = " ".join(params[1:])
|
||||
# 类型转换,如果是json则转换为字典
|
||||
if cfg_value == 'true':
|
||||
cfg_value = True
|
||||
elif cfg_value == 'false':
|
||||
cfg_value = False
|
||||
elif cfg_value.isdigit():
|
||||
cfg_value = int(cfg_value)
|
||||
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
|
||||
cfg_value = json.loads(cfg_value)
|
||||
else:
|
||||
try:
|
||||
cfg_value = float(cfg_value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 检查类型是否匹配
|
||||
if isinstance(getattr(config, cfg_name), type(cfg_value)):
|
||||
setattr(config, cfg_name, cfg_value)
|
||||
pkg.utils.context.set_config(config)
|
||||
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
|
||||
else:
|
||||
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
|
||||
|
||||
else:
|
||||
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
@func_set_timeout(config_init_import.process_message_timeout)
|
||||
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: MessageChain,
|
||||
sender_id: int) -> MessageChain:
|
||||
|
@ -120,210 +55,64 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
|
|||
|
||||
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 MessageChain([Plain("[bot]err:正在处理中,请稍后再试")])
|
||||
|
||||
processing.append(session_name)
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
|
||||
is_message = True
|
||||
processing.append(session_name)
|
||||
try:
|
||||
|
||||
if text_message.startswith('!') or text_message.startswith("!"): # 指令
|
||||
is_message = False
|
||||
try:
|
||||
logging.info(
|
||||
"[{}]发起指令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
|
||||
"..." if len(text_message) > 20 else "")))
|
||||
# 触发插件事件
|
||||
args = {
|
||||
'launcher_type': launcher_type,
|
||||
'launcher_id': launcher_id,
|
||||
'sender_id': sender_id,
|
||||
'command': text_message[1:].strip().split(' ')[0],
|
||||
'params': text_message[1:].strip().split(' ')[1:],
|
||||
'text_message': text_message,
|
||||
'is_admin': sender_id is config.admin_qq,
|
||||
}
|
||||
event = plugin_host.emit(plugin_models.PersonCommandSent
|
||||
if launcher_type == 'person'
|
||||
else plugin_models.GroupCommandSent, **args)
|
||||
|
||||
cmd = text_message[1:].strip().split(' ')[0]
|
||||
if event.get_return_value("alter") is not None:
|
||||
text_message = event.get_return_value("alter")
|
||||
|
||||
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
|
||||
# 取出插件提交的返回值赋值给reply
|
||||
if event.get_return_value("reply") is not None:
|
||||
reply = event.get_return_value("reply")
|
||||
|
||||
if len(params) > 0:
|
||||
try:
|
||||
page = int(params[0])
|
||||
except ValueError:
|
||||
pass
|
||||
if not event.is_prevented_default():
|
||||
reply = pkg.qqbot.command.process_command(session_name, text_message,
|
||||
mgr, config, launcher_type, launcher_id, sender_id)
|
||||
|
||||
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
|
||||
else: # 消息
|
||||
# 触发插件事件
|
||||
args = {
|
||||
"launcher_type": launcher_type,
|
||||
"launcher_id": launcher_id,
|
||||
"sender_id": sender_id,
|
||||
"text_message": text_message,
|
||||
}
|
||||
event = plugin_host.emit(plugin_models.PersonNormalMessageReceived
|
||||
if launcher_type == 'person'
|
||||
else plugin_models.GroupNormalMessageReceived, **args)
|
||||
|
||||
reply_str += "\n以上信息倒序排列"
|
||||
if current != -1:
|
||||
reply_str += ",当前会话是 #{}\n".format(current)
|
||||
else:
|
||||
reply_str += ",当前处于全新会话或不在此页"
|
||||
if event.get_return_value("alter") is not None:
|
||||
text_message = event.get_return_value("alter")
|
||||
|
||||
reply = [reply_str]
|
||||
elif cmd == 'resend':
|
||||
session = pkg.openai.session.get_session(session_name)
|
||||
to_send = session.undo()
|
||||
text_message = to_send
|
||||
is_message = True
|
||||
elif cmd == 'usage':
|
||||
reply_str = "[bot]各api-key使用情况:\n\n"
|
||||
# 取出插件提交的返回值赋值给reply
|
||||
if event.get_return_value("reply") is not None:
|
||||
reply = event.get_return_value("reply")
|
||||
|
||||
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
|
||||
for key_name in api_keys:
|
||||
text_length = pkg.utils.context.get_openai_manager().audit_mgr \
|
||||
.get_text_length_of_key(api_keys[key_name])
|
||||
image_count = pkg.utils.context.get_openai_manager().audit_mgr \
|
||||
.get_image_count_of_key(api_keys[key_name])
|
||||
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
|
||||
int(image_count))
|
||||
|
||||
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'])]
|
||||
if not (hasattr(config, 'include_image_description')
|
||||
and not config.include_image_description):
|
||||
reply.append(" ".join(params))
|
||||
elif cmd == 'version':
|
||||
reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
|
||||
try:
|
||||
if pkg.utils.updater.is_new_version_available():
|
||||
reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
|
||||
except:
|
||||
pass
|
||||
|
||||
reply = [reply_str]
|
||||
elif cmd == 'reload' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
def reload_task():
|
||||
pkg.utils.reloader.reload_all()
|
||||
|
||||
threading.Thread(target=reload_task, daemon=True).start()
|
||||
elif cmd == 'update' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
def update_task():
|
||||
try:
|
||||
if pkg.utils.updater.update_all():
|
||||
pkg.utils.reloader.reload_all(notify=False)
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
|
||||
else:
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
|
||||
except Exception as e0:
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
|
||||
return
|
||||
|
||||
threading.Thread(target=update_task, daemon=True).start()
|
||||
|
||||
reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
|
||||
elif cmd == 'cfg' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
reply = config_operation(cmd, params)
|
||||
else:
|
||||
if cmd.startswith("~") and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
config_item = cmd[1:]
|
||||
params = [config_item] + params
|
||||
reply = config_operation("cfg", params)
|
||||
else:
|
||||
reply = ["[bot]err:未知的指令或权限不足: " + cmd]
|
||||
except Exception as e:
|
||||
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
|
||||
logging.exception(e)
|
||||
reply = ["[bot]err:{}".format(e)]
|
||||
|
||||
if is_message: # 消息
|
||||
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)
|
||||
|
||||
while True:
|
||||
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:
|
||||
logging.debug(type(e))
|
||||
# 尝试切换api-key
|
||||
current_key_name = pkg.utils.context.get_openai_manager().key_mgr.get_key_name(
|
||||
pkg.utils.context.get_openai_manager().key_mgr.using_key
|
||||
)
|
||||
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded()
|
||||
switched, name = pkg.utils.context.get_openai_manager().key_mgr.auto_switch()
|
||||
|
||||
if not switched:
|
||||
mgr.notify_admin("api-key调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key".format(
|
||||
current_key_name))
|
||||
reply = ["[bot]err:API调用额度超额,请联系作者,或等待修复"]
|
||||
else:
|
||||
openai.api_key = pkg.utils.context.get_openai_manager().key_mgr.get_using_key()
|
||||
mgr.notify_admin("api-key调用额度超限({}),接口报错,已切换到{}".format(current_key_name, name))
|
||||
reply = ["[bot]err:API调用额度超额,已自动切换,请重新发送消息"]
|
||||
continue
|
||||
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)]
|
||||
break
|
||||
if not event.is_prevented_default():
|
||||
reply = pkg.qqbot.message.process_normal_message(text_message,
|
||||
mgr, config, launcher_type, launcher_id, sender_id)
|
||||
|
||||
if reply is not None and type(reply[0]) == str:
|
||||
logging.info(
|
||||
|
|
|
@ -6,6 +6,7 @@ context = {
|
|||
},
|
||||
'logger_handler': None,
|
||||
'config': None,
|
||||
'plugin_host': None,
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,4 +39,12 @@ def set_qqbot_manager(inst):
|
|||
|
||||
|
||||
def get_qqbot_manager():
|
||||
return context['inst']['qqbot.manager.QQBotManager']
|
||||
return context['inst']['qqbot.manager.QQBotManager']
|
||||
|
||||
|
||||
def set_plugin_host(inst):
|
||||
context['plugin_host'] = inst
|
||||
|
||||
|
||||
def get_plugin_host():
|
||||
return context['plugin_host']
|
||||
|
|
31
pkg/utils/pkgmgr.py
Normal file
31
pkg/utils/pkgmgr.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from pip._internal import main as pipmain
|
||||
|
||||
|
||||
def install(package):
|
||||
pipmain(['install', package])
|
||||
|
||||
|
||||
def install_requirements(file):
|
||||
pipmain(['install', '-r', file])
|
||||
|
||||
|
||||
def ensure_dulwich():
|
||||
# 尝试三次
|
||||
for i in range(3):
|
||||
try:
|
||||
import dulwich
|
||||
return
|
||||
except ImportError:
|
||||
install('dulwich')
|
||||
|
||||
raise ImportError("无法自动安装dulwich库")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
install("openai11")
|
||||
except Exception as e:
|
||||
print(111)
|
||||
print(e)
|
||||
|
||||
print(222)
|
|
@ -4,6 +4,7 @@ import threading
|
|||
import importlib
|
||||
import pkgutil
|
||||
import pkg.utils.context
|
||||
import pkg.plugin.host
|
||||
|
||||
|
||||
def walk(module, prefix=''):
|
||||
|
@ -34,6 +35,10 @@ def reload_all(notify=True):
|
|||
importlib.reload(__import__('banlist'))
|
||||
pkg.utils.context.context = context
|
||||
|
||||
# 重载插件
|
||||
import plugins
|
||||
walk(plugins)
|
||||
|
||||
# 执行启动流程
|
||||
logging.info("执行程序启动流程")
|
||||
threading.Thread(target=main.main, args=(False,), daemon=False).start()
|
||||
|
|
|
@ -3,12 +3,35 @@ import datetime
|
|||
import pkg.utils.context
|
||||
|
||||
|
||||
def update_all() -> bool:
|
||||
"""使用dulwich更新源码"""
|
||||
def check_dulwich_closure():
|
||||
try:
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.ensure_dulwich()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
|
||||
|
||||
def pull_latest(repo_path: str) -> bool:
|
||||
"""拉取最新代码"""
|
||||
check_dulwich_closure()
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
repo = porcelain.open_repo(repo_path)
|
||||
porcelain.pull(repo)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_all() -> bool:
|
||||
"""使用dulwich更新源码"""
|
||||
check_dulwich_closure()
|
||||
import dulwich
|
||||
try:
|
||||
before_commit_id = get_current_commit_id()
|
||||
from dulwich import porcelain
|
||||
|
@ -35,12 +58,30 @@ def update_all() -> bool:
|
|||
raise Exception("分支不一致,自动更新仅支持master分支,请手动更新(https://github.com/RockChinQ/QChatGPT/issues/76)")
|
||||
|
||||
|
||||
def is_repo(path: str) -> bool:
|
||||
"""检查是否是git仓库"""
|
||||
check_dulwich_closure()
|
||||
|
||||
from dulwich import porcelain
|
||||
try:
|
||||
porcelain.open_repo(path)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def get_remote_url(repo_path: str) -> str:
|
||||
"""获取远程仓库地址"""
|
||||
check_dulwich_closure()
|
||||
|
||||
from dulwich import porcelain
|
||||
repo = porcelain.open_repo(repo_path)
|
||||
return str(porcelain.get_remote_repo(repo, "origin")[1])
|
||||
|
||||
|
||||
def get_current_version_info() -> str:
|
||||
"""获取当前版本信息"""
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
check_dulwich_closure()
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
|
@ -62,10 +103,7 @@ def get_current_version_info() -> str:
|
|||
|
||||
def get_commit_id_and_time_and_msg() -> str:
|
||||
"""获取当前提交id和时间和提交信息"""
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
check_dulwich_closure()
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
|
@ -79,10 +117,7 @@ def get_commit_id_and_time_and_msg() -> str:
|
|||
|
||||
def get_current_commit_id() -> str:
|
||||
"""检查是否有新版本"""
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
check_dulwich_closure()
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
|
@ -97,10 +132,7 @@ def get_current_commit_id() -> str:
|
|||
|
||||
def is_new_version_available() -> bool:
|
||||
"""检查是否有新版本"""
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
check_dulwich_closure()
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
|
|
14
plugins/__init__.py
Normal file
14
plugins/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
# 在此处填写的插件仓库将会被自动下载并加载
|
||||
# 支持gitee和github仓库
|
||||
# 这种加载插件的方式是推荐的,便于插件的获取和更新
|
||||
#
|
||||
# 示例:
|
||||
# plugin_repos = [
|
||||
# 'https://github.com/SampleUser/SampleRepo',
|
||||
# 'https://gitee.com/SampleUser/SampleRepo'
|
||||
# ]
|
||||
|
||||
|
||||
remote_repos = [
|
||||
|
||||
]
|
BIN
res/plugin_hello_group.jpg
Normal file
BIN
res/plugin_hello_group.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
res/plugin_hello_person.png
Normal file
BIN
res/plugin_hello_person.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
3
tests/plugin_examples/__init__.py
Normal file
3
tests/plugin_examples/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# 插件示例
|
||||
# 将此目录下的目录放入plugins目录即可使用
|
||||
# 每个示例插件的功能请查看其包内的__init__.py或README.md
|
0
tests/plugin_examples/cmdcn/__init__.py
Normal file
0
tests/plugin_examples/cmdcn/__init__.py
Normal file
51
tests/plugin_examples/cmdcn/cmdcn.py
Normal file
51
tests/plugin_examples/cmdcn/cmdcn.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from pkg.plugin.models import *
|
||||
from pkg.plugin.host import EventContext, PluginHost
|
||||
|
||||
"""
|
||||
基本命令的中文形式支持
|
||||
"""
|
||||
|
||||
|
||||
__mapping__ = {
|
||||
"帮助": "help",
|
||||
"重置": "reset",
|
||||
"前一次": "last",
|
||||
"后一次": "next",
|
||||
"会话内容": "prompt",
|
||||
"列出会话": "list",
|
||||
"重新回答": "resend",
|
||||
"使用量": "usage",
|
||||
"绘画": "draw",
|
||||
"版本": "version",
|
||||
"热重载": "reload",
|
||||
"热更新": "update",
|
||||
"配置": "cfg",
|
||||
}
|
||||
|
||||
|
||||
@register(name="CmdCN", description="命令中文支持", version="0.1", author="RockChinQ")
|
||||
class CmdCnPlugin(Plugin):
|
||||
|
||||
def __init__(self, plugin_host: PluginHost):
|
||||
pass
|
||||
|
||||
# 私聊发送指令
|
||||
@on(PersonCommandSent)
|
||||
def person_command_sent(self, event: EventContext, **kwargs):
|
||||
cmd = kwargs['command']
|
||||
if cmd in __mapping__:
|
||||
|
||||
# 返回替换后的指令
|
||||
event.add_return("alter", "!"+__mapping__[cmd]+" "+" ".join(kwargs['params']))
|
||||
|
||||
# 群聊发送指令
|
||||
@on(GroupCommandSent)
|
||||
def group_command_sent(self, event: EventContext, **kwargs):
|
||||
cmd = kwargs['command']
|
||||
if cmd in __mapping__:
|
||||
|
||||
# 返回替换后的指令
|
||||
event.add_return("alter", "!"+__mapping__[cmd]+" "+" ".join(kwargs['params']))
|
||||
|
||||
def __del__(self):
|
||||
pass
|
0
tests/plugin_examples/hello_plugin/__init__.py
Normal file
0
tests/plugin_examples/hello_plugin/__init__.py
Normal file
50
tests/plugin_examples/hello_plugin/main.py
Normal file
50
tests/plugin_examples/hello_plugin/main.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from pkg.plugin.models import *
|
||||
from pkg.plugin.host import EventContext, PluginHost
|
||||
|
||||
"""
|
||||
在收到私聊或群聊消息"hello"时,回复"hello, <发送者id>!"或"hello, everyone!"
|
||||
"""
|
||||
|
||||
|
||||
# 注册插件
|
||||
@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
|
||||
class HelloPlugin(Plugin):
|
||||
|
||||
# 插件加载时触发
|
||||
# plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
|
||||
def __init__(self, plugin_host: PluginHost):
|
||||
pass
|
||||
|
||||
# 当收到个人消息时触发
|
||||
@on(PersonNormalMessageReceived)
|
||||
def person_normal_message_received(self, event: EventContext, **kwargs):
|
||||
msg = kwargs['text_message']
|
||||
if msg == "hello": # 如果消息为hello
|
||||
|
||||
# 输出调试信息
|
||||
logging.debug("hello, {}".format(kwargs['sender_id']))
|
||||
|
||||
# 回复消息 "hello, <发送者id>!"
|
||||
event.add_return("reply", ["hello, {}!".format(kwargs['sender_id'])])
|
||||
|
||||
# 阻止该事件默认行为(向接口获取回复)
|
||||
event.prevent_default()
|
||||
|
||||
# 当收到群消息时触发
|
||||
@on(GroupNormalMessageReceived)
|
||||
def group_normal_message_received(self, event: EventContext, **kwargs):
|
||||
msg = kwargs['text_message']
|
||||
if msg == "hello": # 如果消息为hello
|
||||
|
||||
# 输出调试信息
|
||||
logging.debug("hello, {}".format(kwargs['sender_id']))
|
||||
|
||||
# 回复消息 "hello, everyone!"
|
||||
event.add_return("reply", ["hello, everyone!"])
|
||||
|
||||
# 阻止该事件默认行为(向接口获取回复)
|
||||
event.prevent_default()
|
||||
|
||||
# 插件卸载时触发
|
||||
def __del__(self):
|
||||
pass
|
43
tests/plugin_examples/urlikethisijustsix/urlt.py
Normal file
43
tests/plugin_examples/urlikethisijustsix/urlt.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import random
|
||||
|
||||
from mirai import Plain
|
||||
|
||||
from pkg.plugin.models import *
|
||||
from pkg.plugin.host import EventContext
|
||||
|
||||
"""
|
||||
私聊或群聊消息为以下列出的一些冒犯性词语时,自动回复__random_reply__中的一句话
|
||||
"""
|
||||
|
||||
|
||||
__words__ = ['sb', "傻逼", "dinner", "操你妈", "cnm", "fuck you", "fuckyou",
|
||||
"f*ck you", "弱智", "若智", "答辩", "依托答辩", "低能儿", "nt", "脑瘫", "闹谈", "老坛"]
|
||||
|
||||
__random_reply__ = ['好好好', "啊对对对", "好好好好", "你说得对", "谢谢夸奖"]
|
||||
|
||||
|
||||
@register(name="啊对对对", description="你都这样了,我就顺从你吧", version="0.1", author="RockChinQ")
|
||||
class AdddPlugin(Plugin):
|
||||
|
||||
def __init__(self, plugin_host: PluginHost):
|
||||
pass
|
||||
|
||||
# 绑定私聊消息事件和群消息事件
|
||||
@on(PersonNormalMessageReceived)
|
||||
@on(GroupNormalMessageReceived)
|
||||
def normal_message_received(self, event: EventContext, **kwargs):
|
||||
msg = kwargs['text_message']
|
||||
|
||||
# 如果消息中包含关键词
|
||||
if msg in __words__:
|
||||
# 随机一个回复
|
||||
idx = random.randint(0, len(__random_reply__)-1)
|
||||
|
||||
# 返回回复的消息
|
||||
event.add_return("reply", [Plain(__random_reply__[idx])])
|
||||
|
||||
# 阻止向接口获取回复
|
||||
event.prevent_default()
|
||||
|
||||
def __del__(self):
|
||||
pass
|
Loading…
Reference in New Issue
Block a user