diff --git a/pkg/boot/__init__.py b/pkg/boot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg/boot/app.py b/pkg/boot/app.py new file mode 100644 index 00000000..843db869 --- /dev/null +++ b/pkg/boot/app.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import logging + +from ..qqbot import manager as qqbot_mgr +from ..openai import manager as openai_mgr +from ..config import manager as config_mgr +from ..database import manager as database_mgr +from ..utils.center import v2 as center_mgr + + +class Application: + im_mgr: qqbot_mgr.QQBotManager = None + + llm_mgr: openai_mgr.OpenAIInteract = None + + cfg_mgr: config_mgr.ConfigManager = None + + tips_mgr: config_mgr.ConfigManager = None + + db_mgr: database_mgr.DatabaseManager = None + + ctr_mgr: center_mgr.V2CenterAPI = None + + logger: logging.Logger = None + + def __init__(self): + pass + + async def run(self): + pass diff --git a/pkg/boot/boot.py b/pkg/boot/boot.py new file mode 100644 index 00000000..2d640904 --- /dev/null +++ b/pkg/boot/boot.py @@ -0,0 +1,124 @@ +from __future__ import print_function + +import os +import sys + +from . import files +from . import deps +from . import log +from . import config + +from . import app +from ..audit import identifier +from ..database import manager as db_mgr +from ..openai import manager as llm_mgr +from ..openai import session as llm_session +from ..openai import dprompt as llm_dprompt +from ..qqbot import manager as im_mgr +from ..qqbot.cmds import aamgr as im_cmd_aamgr +from ..plugin import host as plugin_host +from ..utils.center import v2 as center_v2 +from ..utils import updater +from ..utils import context + +use_override = False + + +async def make_app() -> app.Application: + global use_override + + generated_files = await files.generate_files() + + if generated_files: + print("以下文件不存在,已自动生成,请修改配置文件后重启:") + for file in generated_files: + print("-", file) + + sys.exit(0) + + missing_deps = await deps.check_deps() + + if missing_deps: + print("以下依赖包未安装,将自动安装,请完成后重启程序:") + for dep in missing_deps: + print("-", dep) + await deps.install_deps(missing_deps) + sys.exit(0) + + qcg_logger = await log.init_logging() + + # 生成标识符 + identifier.init() + + cfg_mgr = await config.load_config() + context.set_config_manager(cfg_mgr) + cfg = cfg_mgr.data + + # 检查是否携带了 --override 或 -r 参数 + if '--override' in sys.argv or '-r' in sys.argv: + use_override = True + + if use_override: + overrided = await config.override_config_manager(cfg_mgr) + if overrided: + qcg_logger.info("以下配置项已使用 override.json 覆盖:" + ",".join(overrided)) + + tips_mgr = await config.load_tips() + + # 初始化文字转图片 + from pkg.utils import text2img + # TODO make it async + text2img.initialize() + + # 检查管理员QQ号 + if cfg_mgr.data['admin_qq'] == 0: + qcg_logger.warning("未设置管理员QQ号,将无法使用管理员命令,请在 config.py 中修改 admin_qq") + + # TODO make it async + llm_dprompt.register_all() + im_cmd_aamgr.register_all() + im_cmd_aamgr.apply_privileges() + + # 构建组建实例 + ap = app.Application() + ap.logger = qcg_logger + ap.cfg_mgr = cfg_mgr + ap.tips_mgr = tips_mgr + + center_v2_api = center_v2.V2CenterAPI( + basic_info={ + "host_id": identifier.identifier['host_id'], + "instance_id": identifier.identifier['instance_id'], + "semantic_version": updater.get_current_tag(), + "platform": sys.platform, + }, + runtime_info={ + "admin_id": "{}".format(cfg['admin_qq']), + "msg_source": cfg['msg_source_adapter'], + } + ) + ap.ctr_mgr = center_v2_api + + db_mgr_inst = db_mgr.DatabaseManager(ap) + # TODO make it async + db_mgr_inst.initialize_database() + ap.db_mgr = db_mgr_inst + + llm_mgr_inst = llm_mgr.OpenAIInteract(ap) + ap.llm_mgr = llm_mgr_inst + # TODO make it async + llm_session.load_sessions() + + im_mgr_inst = im_mgr.QQBotManager(first_time_init=True, ap=ap) + ap.im_mgr = im_mgr_inst + + # TODO make it async + plugin_host.load_plugins() + # plugin_host.initialize_plugins() + + return ap + + +async def main(): + app_inst = await make_app() + await app_inst.run() diff --git a/pkg/boot/config.py b/pkg/boot/config.py new file mode 100644 index 00000000..f18ed2c3 --- /dev/null +++ b/pkg/boot/config.py @@ -0,0 +1,43 @@ +import json + +from ..config import manager as config_mgr +from ..config.impls import pymodule + + +async def load_config() -> config_mgr.ConfigManager: + """加载配置文件""" + cfg_inst = pymodule.PythonModuleConfigFile( + "config.py", + "config-template.py" + ) + + cfg_mgr = config_mgr.ConfigManager(cfg_inst) + await cfg_mgr.load_config() + + return cfg_mgr + + +async def load_tips() -> config_mgr.ConfigManager: + """加载提示文件""" + tips_inst = pymodule.PythonModuleConfigFile( + "tips.py", + "tips-custom-template.py" + ) + + tips_mgr = config_mgr.ConfigManager(tips_inst) + await tips_mgr.load_config() + + return tips_mgr + + +async def override_config_manager(cfg_mgr: config_mgr.ConfigManager) -> list[str]: + override_json = json.load(open("override.json", "r", encoding="utf-8")) + overrided = [] + + config = cfg_mgr.data + for key in override_json: + if key in config: + config[key] = override_json[key] + overrided.append(key) + + return overrided diff --git a/pkg/boot/deps.py b/pkg/boot/deps.py new file mode 100644 index 00000000..db4672cf --- /dev/null +++ b/pkg/boot/deps.py @@ -0,0 +1,34 @@ +import pip + +required_deps = { + "requests": "requests", + "openai": "openai", + "dulwich": "dulwich", + "colorlog": "colorlog", + "mirai": "yiri-mirai-rc", + "func_timeout": "func_timeout", + "PIL": "pillow", + "nakuru": "nakuru-project-idk", + "CallingGPT": "CallingGPT", + "tiktoken": "tiktoken", + "yaml": "pyyaml", + "aiohttp": "aiohttp", +} + + +async def check_deps() -> list[str]: + global required_deps + + missing_deps = [] + for dep in required_deps: + try: + __import__(dep) + except ImportError: + missing_deps.append(dep) + return missing_deps + +async def install_deps(deps: list[str]): + global required_deps + + for dep in deps: + pip.main(["install", required_deps[dep]]) diff --git a/pkg/boot/files.py b/pkg/boot/files.py new file mode 100644 index 00000000..eaa4ffb0 --- /dev/null +++ b/pkg/boot/files.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import os +import shutil +import sys + + +required_files = { + "config.py": "config-template.py", + "banlist.py": "res/templates/banlist-template.py", + "tips.py": "tips-custom-template.py", + "sensitive.json": "res/templates/sensitive-template.json", + "scenario/default.json": "scenario/default-template.json", + "cmdpriv.json": "res/templates/cmdpriv-template.json", +} + +required_paths = [ + "plugins", + "prompts", + "temp", + "logs" +] + +async def generate_files() -> list[str]: + global required_files, required_paths + + for required_paths in required_paths: + if not os.path.exists(required_paths): + os.mkdir(required_paths) + + generated_files = [] + for file in required_files: + if not os.path.exists(file): + shutil.copyfile(required_files[file], file) + generated_files.append(file) + + return generated_files diff --git a/pkg/boot/log.py b/pkg/boot/log.py new file mode 100644 index 00000000..1f3e75fa --- /dev/null +++ b/pkg/boot/log.py @@ -0,0 +1,47 @@ +import logging +import os +import sys +import time + +import colorlog + + +log_colors_config = { + 'DEBUG': 'green', # cyan white + 'INFO': 'white', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'cyan', +} + + +async def init_logging() -> logging.Logger: + + level = logging.INFO + + if 'DEBUG' in os.environ and os.environ['DEBUG'] in ['true', '1']: + level = logging.DEBUG + + log_file_name = "logs/qcg-%s.log" % time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + + qcg_logger = logging.getLogger("qcg") + + qcg_logger.setLevel(level) + + log_handlers: logging.Handler = [ + logging.StreamHandler(sys.stdout), + logging.FileHandler(log_file_name) + ] + + for handler in log_handlers: + handler.setLevel(level) + handler.setFormatter( + colorlog.ColoredFormatter( + fmt="[%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + log_colors=log_colors_config + ) + ) + qcg_logger.addHandler(handler) + + return qcg_logger \ No newline at end of file diff --git a/pkg/boot/misc.py b/pkg/boot/misc.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg/config/impls/__init__.py b/pkg/config/impls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg/config/manager.py b/pkg/config/manager.py index 53a6b099..5893ff0b 100644 --- a/pkg/config/manager.py +++ b/pkg/config/manager.py @@ -14,7 +14,6 @@ class ConfigManager: def __init__(self, cfg_file: file_model.ConfigFile) -> None: self.file = cfg_file self.data = {} - context.set_config_manager(self) async def load_config(self): self.data = await self.file.load() diff --git a/pkg/database/manager.py b/pkg/database/manager.py index ad44d512..c1153e8f 100644 --- a/pkg/database/manager.py +++ b/pkg/database/manager.py @@ -17,7 +17,7 @@ class DatabaseManager: conn = None cursor = None - def __init__(self): + def __init__(self, *args, **kwargs): self.reconnect() diff --git a/pkg/openai/manager.py b/pkg/openai/manager.py index 8b2b6145..3fd53be6 100644 --- a/pkg/openai/manager.py +++ b/pkg/openai/manager.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import openai @@ -8,6 +10,7 @@ from ..utils import context from ..audit import gatherer from ..openai import modelmgr from ..openai.api import model as api_model +from ..boot import app class OpenAIInteract: @@ -26,12 +29,27 @@ class OpenAIInteract: client: openai.Client = None - def __init__(self, api_key: str): + def __init__(self, ap: app.Application): + + cfg= ap.cfg_mgr.data + api_key = cfg['openai_config']['api_key'] self.key_mgr = keymgr.KeysManager(api_key) self.audit_mgr = gatherer.DataGatherer() - # logging.info("文字总使用量:%d", self.audit_mgr.get_total_text_length()) + # 配置OpenAI proxy + openai.proxies = None # 先重置,因为重载后可能需要清除proxy + if "http_proxy" in cfg['openai_config'] and cfg['openai_config']["http_proxy"] is not None: + openai.proxies = { + "http": cfg['openai_config']["http_proxy"], + "https": cfg['openai_config']["http_proxy"] + } + + # 配置openai api_base + if "reverse_proxy" in cfg['openai_config'] and cfg['openai_config']["reverse_proxy"] is not None: + logging.debug("设置反向代理: "+cfg['openai_config']['reverse_proxy']) + openai.base_url = cfg['openai_config']["reverse_proxy"] + self.client = openai.Client( api_key=self.key_mgr.get_using_key(), diff --git a/pkg/qqbot/manager.py b/pkg/qqbot/manager.py index 8cd663ff..eddd9b15 100644 --- a/pkg/qqbot/manager.py +++ b/pkg/qqbot/manager.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import logging @@ -16,6 +18,8 @@ from ..plugin import models as plugin_models import tips as tips_custom from ..qqbot import adapter as msadapter +from ..boot import app + # 检查消息是否符合泛响应匹配机制 def check_response_rule(group_id:int, text: str): @@ -101,7 +105,7 @@ class QQBotManager: ban_person = [] ban_group = [] - def __init__(self, first_time_init=True): + def __init__(self, first_time_init=True, ap: app.Application = None): config = context.get_config_manager().data self.timeout = config['process_message_timeout'] diff --git a/pkg/utils/center/v2.py b/pkg/utils/center/v2.py index b1c0a3e6..53594b51 100644 --- a/pkg/utils/center/v2.py +++ b/pkg/utils/center/v2.py @@ -6,6 +6,7 @@ from . import apigroup from .groups import main from .groups import usage from .groups import plugin +from ...utils import context BACKEND_URL = "https://api.qchatgpt.rockchin.top/api/v2" @@ -33,3 +34,5 @@ class V2CenterAPI: self.main = main.V2MainDataAPI(BACKEND_URL) self.usage = usage.V2UsageDataAPI(BACKEND_URL) self.plugin = plugin.V2PluginDataAPI(BACKEND_URL) + + context.set_center_v2_api(self)