From c853bba4baa668921e584fa73978dde2c9fc025a Mon Sep 17 00:00:00 2001 From: RockChinQ <1010553892@qq.com> Date: Tue, 6 Feb 2024 21:26:03 +0800 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=9D=87=E6=94=B9=E4=B8=BAjson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + pkg/audit/center/groups/main.py | 3 +- pkg/audit/center/groups/plugin.py | 3 +- pkg/audit/center/groups/usage.py | 3 +- pkg/command/cmdmgr.py | 8 +- pkg/command/operator.py | 1 + pkg/command/operators/cfg.py | 98 ------------------- pkg/command/operators/help.py | 2 +- pkg/config/manager.py | 3 + pkg/core/app.py | 10 +- pkg/core/boot.py | 40 +++----- pkg/core/bootutils/files.py | 24 +++-- pkg/core/bootutils/log.py | 2 +- pkg/core/controller.py | 10 +- pkg/pipeline/bansess/bansess.py | 62 ++++-------- pkg/pipeline/cntfilter/cntfilter.py | 6 +- .../cntfilter/filters/baiduexamine.py | 8 +- pkg/pipeline/cntfilter/filters/banwords.py | 6 +- pkg/pipeline/cntfilter/filters/cntignore.py | 8 +- pkg/pipeline/longtext/longtext.py | 20 ++-- pkg/pipeline/longtext/strategies/image.py | 2 +- pkg/pipeline/preproc/preproc.py | 4 +- pkg/pipeline/process/handlers/chat.py | 8 +- pkg/pipeline/process/handlers/command.py | 4 +- pkg/pipeline/ratelimit/algos/fixedwin.py | 10 +- pkg/pipeline/ratelimit/ratelimit.py | 2 +- pkg/pipeline/respback/respback.py | 2 +- pkg/pipeline/resprule/resprule.py | 4 +- pkg/pipeline/resprule/rules/prefix.py | 1 + pkg/pipeline/resprule/rules/random.py | 2 +- pkg/pipeline/wrapper/wrapper.py | 2 +- pkg/platform/manager.py | 49 ++++------ pkg/provider/requester/apis/chatcmpl.py | 6 +- pkg/provider/requester/modelmgr.py | 2 +- pkg/provider/session/sessionmgr.py | 9 +- pkg/provider/sysprompt/loaders/scenario.py | 4 +- pkg/provider/sysprompt/loaders/single.py | 6 +- pkg/provider/sysprompt/sysprompt.py | 2 +- pkg/utils/proxy.py | 23 ++--- templates/__init__.py | 0 templates/command.json | 3 + templates/pipeline.json | 36 +++++++ templates/platform.json | 20 ++++ templates/plugin-settings.json | 3 + templates/provider.json | 17 ++++ templates/scenario-template.json | 12 +++ templates/sensitive-words.json | 78 +++++++++++++++ templates/system.json | 11 +++ 48 files changed, 355 insertions(+), 285 deletions(-) delete mode 100644 pkg/command/operators/cfg.py create mode 100644 templates/__init__.py create mode 100644 templates/command.json create mode 100644 templates/pipeline.json create mode 100644 templates/platform.json create mode 100644 templates/plugin-settings.json create mode 100644 templates/provider.json create mode 100644 templates/scenario-template.json create mode 100644 templates/sensitive-words.json create mode 100644 templates/system.json diff --git a/.gitignore b/.gitignore index b9069a7f..7b2a70a9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ bard.json !/docker-compose.yaml res/instance_id.json .DS_Store +/data \ No newline at end of file diff --git a/pkg/audit/center/groups/main.py b/pkg/audit/center/groups/main.py index 24e5287b..3a31a65b 100644 --- a/pkg/audit/center/groups/main.py +++ b/pkg/audit/center/groups/main.py @@ -12,8 +12,7 @@ class V2MainDataAPI(apigroup.APIGroup): super().__init__(prefix+"/main", ap) async def do(self, *args, **kwargs): - config = self.ap.cfg_mgr.data - if not config['report_usage']: + if not self.ap.system_cfg.data['report-usage']: return None return await super().do(*args, **kwargs) diff --git a/pkg/audit/center/groups/plugin.py b/pkg/audit/center/groups/plugin.py index 312d5e86..627b116c 100644 --- a/pkg/audit/center/groups/plugin.py +++ b/pkg/audit/center/groups/plugin.py @@ -12,8 +12,7 @@ class V2PluginDataAPI(apigroup.APIGroup): super().__init__(prefix+"/plugin", ap) async def do(self, *args, **kwargs): - config = self.ap.cfg_mgr.data - if not config['report_usage']: + if not self.ap.system_cfg.data['report-usage']: return None return await super().do(*args, **kwargs) diff --git a/pkg/audit/center/groups/usage.py b/pkg/audit/center/groups/usage.py index 02d89387..8a8bdf04 100644 --- a/pkg/audit/center/groups/usage.py +++ b/pkg/audit/center/groups/usage.py @@ -12,8 +12,7 @@ class V2UsageDataAPI(apigroup.APIGroup): super().__init__(prefix+"/usage", ap) async def do(self, *args, **kwargs): - config = self.ap.cfg_mgr.data - if not config['report_usage']: + if not self.ap.system_cfg.data['report-usage']: return None return await super().do(*args, **kwargs) diff --git a/pkg/command/cmdmgr.py b/pkg/command/cmdmgr.py index cf2dbc88..b8521751 100644 --- a/pkg/command/cmdmgr.py +++ b/pkg/command/cmdmgr.py @@ -6,7 +6,7 @@ from ..core import app, entities as core_entities from ..provider import entities as llm_entities from . import entities, operator, errors -from .operators import func, plugin, default, reset, list as list_cmd, last, next, delc, resend, prompt, cfg, cmd, help, version, update +from .operators import func, plugin, default, reset, list as list_cmd, last, next, delc, resend, prompt, cmd, help, version, update class CommandManager: @@ -85,9 +85,11 @@ class CommandManager: """ privilege = 1 - if query.sender_id == self.ap.cfg_mgr.data['admin_qq'] \ - or query.sender_id in self.ap.cfg_mgr['admin_qq']: + + if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.system_cfg.data['admin-sessions']: privilege = 2 + + print(f'privilege: {privilege}') ctx = entities.ExecuteContext( query=query, diff --git a/pkg/command/operator.py b/pkg/command/operator.py index 1b29e6c0..d0b731a9 100644 --- a/pkg/command/operator.py +++ b/pkg/command/operator.py @@ -24,6 +24,7 @@ def operator_class( cls.help = help cls.usage = usage cls.parent_class = parent_class + cls.lowest_privilege = privilege preregistered_operators.append(cls) diff --git a/pkg/command/operators/cfg.py b/pkg/command/operators/cfg.py deleted file mode 100644 index b67ff3e6..00000000 --- a/pkg/command/operators/cfg.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -import typing -import json - -from .. import operator, entities, cmdmgr, errors - - -@operator.operator_class( - name="cfg", - help="配置项管理", - usage='!cfg <配置项> [配置值]\n!cfg all', - privilege=2 -) -class CfgOperator(operator.CommandOperator): - - async def execute( - self, - context: entities.ExecuteContext - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - """执行 - """ - reply = '' - - params = context.crt_params - cfg_mgr = self.ap.cfg_mgr - - false = False - true = True - - reply_str = "" - if len(params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供配置项名称')) - else: - cfg_name = params[0] - if cfg_name == 'all': - reply_str = "[bot]所有配置项:\n\n" - for cfg in cfg_mgr.data.keys(): - if not cfg.startswith('__') and not cfg == 'logging': - # 根据配置项类型进行格式化,如果是字典则转换为json并格式化 - if isinstance(cfg_mgr.data[cfg], str): - reply_str += "{}: \"{}\"\n".format(cfg, cfg_mgr.data[cfg]) - elif isinstance(cfg_mgr.data[cfg], dict): - # 不进行unicode转义,并格式化 - reply_str += "{}: {}\n".format(cfg, - json.dumps(cfg_mgr.data[cfg], - ensure_ascii=False, indent=4)) - else: - reply_str += "{}: {}\n".format(cfg, cfg_mgr.data[cfg]) - yield entities.CommandReturn(text=reply_str) - else: - cfg_entry_path = cfg_name.split('.') - - try: - if len(params) == 1: # 未指定配置值,返回配置项值 - cfg_entry = cfg_mgr.data[cfg_entry_path[0]] - if len(cfg_entry_path) > 1: - for i in range(1, len(cfg_entry_path)): - cfg_entry = cfg_entry[cfg_entry_path[i]] - - if isinstance(cfg_entry, str): - reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, cfg_entry) - elif isinstance(cfg_entry, dict): - reply_str = "[bot]配置项{}: {}\n".format(cfg_name, - json.dumps(cfg_entry, - ensure_ascii=False, indent=4)) - else: - reply_str = "[bot]配置项{}: {}\n".format(cfg_name, cfg_entry) - - yield entities.CommandReturn(text=reply_str) - else: - cfg_value = " ".join(params[1:]) - - cfg_value = eval(cfg_value) - - cfg_entry = cfg_mgr.data[cfg_entry_path[0]] - if len(cfg_entry_path) > 1: - for i in range(1, len(cfg_entry_path) - 1): - cfg_entry = cfg_entry[cfg_entry_path[i]] - if isinstance(cfg_entry[cfg_entry_path[-1]], type(cfg_value)): - cfg_entry[cfg_entry_path[-1]] = cfg_value - yield entities.CommandReturn(text="配置项{}修改成功".format(cfg_name)) - else: - # reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)] - yield entities.CommandReturn(error=errors.CommandOperationError("配置项{}类型不匹配".format(cfg_name))) - else: - cfg_mgr.data[cfg_entry_path[0]] = cfg_value - # reply = ["[bot]配置项{}修改成功".format(cfg_name)] - yield entities.CommandReturn(text="配置项{}修改成功".format(cfg_name)) - except KeyError: - # reply = ["[bot]err:未找到配置项 {}".format(cfg_name)] - yield entities.CommandReturn(error=errors.CommandOperationError("未找到配置项 {}".format(cfg_name))) - except NameError: - # reply = ["[bot]err:值{}不合法(字符串需要使用双引号包裹)".format(cfg_value)] - yield entities.CommandReturn(error=errors.CommandOperationError("值{}不合法(字符串需要使用双引号包裹)".format(cfg_value))) - except ValueError: - # reply = ["[bot]err:未找到配置项 {}".format(cfg_name)] - yield entities.CommandReturn(error=errors.CommandOperationError("未找到配置项 {}".format(cfg_name))) diff --git a/pkg/command/operators/help.py b/pkg/command/operators/help.py index c99c2948..570e103c 100644 --- a/pkg/command/operators/help.py +++ b/pkg/command/operators/help.py @@ -16,7 +16,7 @@ class HelpOperator(operator.CommandOperator): self, context: entities.ExecuteContext ) -> typing.AsyncGenerator[entities.CommandReturn, None]: - help = self.ap.tips_mgr.data['help_message'] + help = self.ap.system_cfg.data['help-message'] help += '\n发送命令 !cmd 可查看命令列表' diff --git a/pkg/config/manager.py b/pkg/config/manager.py index 8377a51a..b75f0202 100644 --- a/pkg/config/manager.py +++ b/pkg/config/manager.py @@ -4,6 +4,9 @@ from . import model as file_model from .impls import pymodule, json as json_file +managers: ConfigManager = [] + + class ConfigManager: """配置文件管理器""" diff --git a/pkg/core/app.py b/pkg/core/app.py index cd9607b0..f91d8986 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -31,9 +31,15 @@ class Application: tool_mgr: llm_tool_mgr.ToolManager = None - cfg_mgr: config_mgr.ConfigManager = None + command_cfg: config_mgr.ConfigManager = None - tips_mgr: config_mgr.ConfigManager = None + pipeline_cfg: config_mgr.ConfigManager = None + + platform_cfg: config_mgr.ConfigManager = None + + provider_cfg: config_mgr.ConfigManager = None + + system_cfg: config_mgr.ConfigManager = None ctr_mgr: center_mgr.V2CenterAPI = None diff --git a/pkg/core/boot.py b/pkg/core/boot.py index c251624d..ef5fe2ec 100644 --- a/pkg/core/boot.py +++ b/pkg/core/boot.py @@ -32,7 +32,7 @@ async def make_app() -> app.Application: generated_files = await files.generate_files() if generated_files: - print("以下文件不存在,已自动生成,请修改配置文件后重启:") + print("以下文件不存在,已自动生成,请按需修改配置文件后重启:") for file in generated_files: print("-", file) @@ -52,31 +52,23 @@ async def make_app() -> app.Application: # 生成标识符 identifier.init() - cfg_mgr = await config.load_python_module_config("config.py", "config-template.py") - cfg = cfg_mgr.data + # ========== 加载配置文件 ========== - # 检查是否携带了 --override 或 -r 参数 - if "--override" in sys.argv or "-r" in sys.argv: - use_override = True + command_cfg = await config.load_json_config("data/config/command.json", "templates/command.json") + pipeline_cfg = await config.load_json_config("data/config/pipeline.json", "templates/pipeline.json") + platform_cfg = await config.load_json_config("data/config/platform.json", "templates/platform.json") + provider_cfg = await config.load_json_config("data/config/provider.json", "templates/provider.json") + system_cfg = await config.load_json_config("data/config/system.json", "templates/system.json") - 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_python_module_config( - "tips.py", "tips-custom-template.py" - ) - - # 检查管理员QQ号 - if cfg_mgr.data["admin_qq"] == 0: - qcg_logger.warning("未设置管理员QQ号,将无法使用管理员命令,请在 config.py 中修改 admin_qq") - - # 构建组建实例 + # ========== 构建应用实例 ========== ap = app.Application() ap.logger = qcg_logger - ap.cfg_mgr = cfg_mgr - ap.tips_mgr = tips_mgr + + ap.command_cfg = command_cfg + ap.pipeline_cfg = pipeline_cfg + ap.platform_cfg = platform_cfg + ap.provider_cfg = provider_cfg + ap.system_cfg = system_cfg proxy_mgr = proxy.ProxyManager(ap) await proxy_mgr.initialize() @@ -95,8 +87,8 @@ async def make_app() -> app.Application: "platform": sys.platform, }, runtime_info={ - "admin_id": "{}".format(cfg["admin_qq"]), - "msg_source": cfg["msg_source_adapter"], + "admin_id": "{}".format(system_cfg.data["admin-sessions"]), + "msg_source": platform_cfg.data["platform-adapter"], }, ) ap.ctr_mgr = center_v2_api diff --git a/pkg/core/bootutils/files.py b/pkg/core/bootutils/files.py index 25a8ba5b..975e3aad 100644 --- a/pkg/core/bootutils/files.py +++ b/pkg/core/bootutils/files.py @@ -6,19 +6,25 @@ import sys required_files = { - "config.py": "config-template.py", - "banlist.py": "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", + "plugins/__init__.py": "templates/__init__.py", + "plugins/plugins.json": "templates/plugin-settings.json", + "data/config/command.json": "templates/command.json", + "data/config/pipeline.json": "templates/pipeline.json", + "data/config/platform.json": "templates/platform.json", + "data/config/provider.json": "templates/provider.json", + "data/config/system.json": "templates/system.json", + "data/config/sensitive-words.json": "templates/sensitive-words.json", + "data/scenario/default.json": "templates/scenario-template.json", } required_paths = [ - "plugins", - "prompts", "temp", - "logs" + "data", + "data/prompts", + "data/scenario", + "data/logs", + "data/config", + "plugins" ] async def generate_files() -> list[str]: diff --git a/pkg/core/bootutils/log.py b/pkg/core/bootutils/log.py index 4bc0e4de..a63b7c92 100644 --- a/pkg/core/bootutils/log.py +++ b/pkg/core/bootutils/log.py @@ -21,7 +21,7 @@ async def init_logging() -> logging.Logger: if "DEBUG" in os.environ and os.environ["DEBUG"] in ["true", "1"]: level = logging.DEBUG - log_file_name = "logs/qcg-%s.log" % time.strftime( + log_file_name = "data/logs/qcg-%s.log" % time.strftime( "%Y-%m-%d-%H-%M-%S", time.localtime() ) diff --git a/pkg/core/controller.py b/pkg/core/controller.py index 9cf4727a..12403aff 100644 --- a/pkg/core/controller.py +++ b/pkg/core/controller.py @@ -8,8 +8,6 @@ from . import app, entities from ..pipeline import entities as pipeline_entities from ..plugin import events -DEFAULT_QUERY_CONCURRENCY = 10 - class Controller: """总控制器 @@ -21,7 +19,7 @@ class Controller: def __init__(self, ap: app.Application): self.ap = ap - self.semaphore = asyncio.Semaphore(DEFAULT_QUERY_CONCURRENCY) + self.semaphore = asyncio.Semaphore(self.ap.system_cfg.data['pipeline-concurrency']) async def consumer(self): """事件处理循环 @@ -150,9 +148,9 @@ class Controller: try: await self._execute_from_stage(0, query) except Exception as e: - self.ap.logger.error(f"处理请求时出错 {query}: {e}") - # self.ap.logger.debug(f"处理请求时出错 {query}: {e}", exc_info=True) - traceback.print_exc() + self.ap.logger.error(f"处理请求时出错 query_id={query.query_id}: {e}") + self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") + # traceback.print_exc() finally: self.ap.logger.debug(f"Query {query} processed") diff --git a/pkg/pipeline/bansess/bansess.py b/pkg/pipeline/bansess/bansess.py index a0f63c36..60d23584 100644 --- a/pkg/pipeline/bansess/bansess.py +++ b/pkg/pipeline/bansess/bansess.py @@ -23,54 +23,28 @@ class BanSessionCheckStage(stage.PipelineStage): stage_inst_name: str ) -> entities.StageProcessResult: - if not self.banlist_mgr.data['enable']: - return entities.StageProcessResult( - result_type=entities.ResultType.CONTINUE, - new_query=query - ) - + found = False + + mode = self.ap.pipeline_cfg.data['access-control']['mode'] + + sess_list = self.ap.pipeline_cfg.data['access-control'][mode] + + if (query.launcher_type == 'group' and 'group_*' in sess_list) \ + or (query.launcher_type == 'person' and 'person_*' in sess_list): + found = True + else: + for sess in sess_list: + if sess == f"{query.launcher_type}_{query.launcher_id}": + found = True + break + result = False - if query.launcher_type == 'group': - if not self.banlist_mgr.data['enable_group']: # 未启用群聊响应 - result = True - # 检查是否显式声明发起人QQ要被person忽略 - elif query.sender_id in self.banlist_mgr.data['person']: - result = True - else: - for group_rule in self.banlist_mgr.data['group']: - if type(group_rule) == int: - if group_rule == query.launcher_id: - result = True - elif type(group_rule) == str: - if group_rule.startswith('!'): - reg_str = group_rule[1:] - if re.match(reg_str, str(query.launcher_id)): - result = False - break - else: - if re.match(group_rule, str(query.launcher_id)): - result = True - elif query.launcher_type == 'person': - if not self.banlist_mgr.data['enable_private']: - result = True - else: - for person_rule in self.banlist_mgr.data['person']: - if type(person_rule) == int: - if person_rule == query.launcher_id: - result = True - elif type(person_rule) == str: - if person_rule.startswith('!'): - reg_str = person_rule[1:] - if re.match(reg_str, str(query.launcher_id)): - result = False - break - else: - if re.match(person_rule, str(query.launcher_id)): - result = True + if mode == 'blacklist': + result = found return entities.StageProcessResult( result_type=entities.ResultType.CONTINUE if not result else entities.ResultType.INTERRUPT, new_query=query, - debug_notice=f'根据禁用列表忽略消息: {query.launcher_type}_{query.launcher_id}' if result else '' + debug_notice=f'根据访问控制忽略消息: {query.launcher_type}_{query.launcher_id}' if result else '' ) diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index 78412458..9982a51e 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -24,10 +24,10 @@ class ContentFilterStage(stage.PipelineStage): async def initialize(self): self.filter_chain.append(cntignore.ContentIgnore(self.ap)) - if self.ap.cfg_mgr.data['sensitive_word_filter']: + if self.ap.pipeline_cfg.data['check-sensitive-words']: self.filter_chain.append(banwords.BanWordFilter(self.ap)) - if self.ap.cfg_mgr.data['baidu_check']: + if self.ap.pipeline_cfg.data['baidu-cloud-examine']['enable']: self.filter_chain.append(baiduexamine.BaiduCloudExamine(self.ap)) for filter in self.filter_chain: @@ -41,7 +41,7 @@ class ContentFilterStage(stage.PipelineStage): """请求llm前处理消息 只要有一个不通过就不放行,只放行 PASS 的消息 """ - if not self.ap.cfg_mgr.data['income_msg_check']: + if not self.ap.pipeline_cfg.data['income-msg-check']: return entities.StageProcessResult( result_type=entities.ResultType.CONTINUE, new_query=query diff --git a/pkg/pipeline/cntfilter/filters/baiduexamine.py b/pkg/pipeline/cntfilter/filters/baiduexamine.py index a658897b..f72fe960 100644 --- a/pkg/pipeline/cntfilter/filters/baiduexamine.py +++ b/pkg/pipeline/cntfilter/filters/baiduexamine.py @@ -19,8 +19,8 @@ class BaiduCloudExamine(filter_model.ContentFilter): BAIDU_EXAMINE_TOKEN_URL, params={ "grant_type": "client_credentials", - "client_id": self.ap.cfg_mgr.data['baidu_api_key'], - "client_secret": self.ap.cfg_mgr.data['baidu_secret_key'] + "client_id": self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-key'], + "client_secret": self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-secret'] } ) as resp: return (await resp.json())['access_token'] @@ -56,6 +56,6 @@ class BaiduCloudExamine(filter_model.ContentFilter): return entities.FilterResult( level=entities.ResultLevel.BLOCK, replacement=message, - user_notice=self.ap.cfg_mgr.data['inappropriate_message_tips'], + user_notice="消息中存在不合适的内容, 请修改", console_notice=f"百度云判定结果:{conclusion}" - ) \ No newline at end of file + ) diff --git a/pkg/pipeline/cntfilter/filters/banwords.py b/pkg/pipeline/cntfilter/filters/banwords.py index 9451c7b8..587f81c3 100644 --- a/pkg/pipeline/cntfilter/filters/banwords.py +++ b/pkg/pipeline/cntfilter/filters/banwords.py @@ -13,8 +13,8 @@ class BanWordFilter(filter_model.ContentFilter): async def initialize(self): self.sensitive = await cfg_mgr.load_json_config( - "sensitive.json", - "res/templates/sensitive-template.json" + "data/config/sensitive-words.json", + "templates/sensitive-words.json" ) async def process(self, message: str) -> entities.FilterResult: @@ -39,6 +39,6 @@ class BanWordFilter(filter_model.ContentFilter): return entities.FilterResult( level=entities.ResultLevel.MASKED if found else entities.ResultLevel.PASS, replacement=message, - user_notice='[bot] 消息中存在不合适的内容, 请更换措辞' if found else '', + user_notice='消息中存在不合适的内容, 请修改' if found else '', console_notice='' ) \ No newline at end of file diff --git a/pkg/pipeline/cntfilter/filters/cntignore.py b/pkg/pipeline/cntfilter/filters/cntignore.py index 81408868..92fe94e8 100644 --- a/pkg/pipeline/cntfilter/filters/cntignore.py +++ b/pkg/pipeline/cntfilter/filters/cntignore.py @@ -15,8 +15,8 @@ class ContentIgnore(filter_model.ContentFilter): ] async def process(self, message: str) -> entities.FilterResult: - if 'prefix' in self.ap.cfg_mgr.data['ignore_rules']: - for rule in self.ap.cfg_mgr.data['ignore_rules']['prefix']: + if 'prefix' in self.ap.pipeline_cfg.data['ignore-rules']: + for rule in self.ap.pipeline_cfg.data['ignore-rules']['prefix']: if message.startswith(rule): return entities.FilterResult( level=entities.ResultLevel.BLOCK, @@ -25,8 +25,8 @@ class ContentIgnore(filter_model.ContentFilter): console_notice='根据 ignore_rules 中的 prefix 规则,忽略消息' ) - if 'regexp' in self.ap.cfg_mgr.data['ignore_rules']: - for rule in self.ap.cfg_mgr.data['ignore_rules']['regexp']: + if 'regexp' in self.ap.pipeline_cfg.data['ignore-rules']: + for rule in self.ap.pipeline_cfg.data['ignore-rules']['regexp']: if re.search(rule, message): return entities.FilterResult( level=entities.ResultLevel.BLOCK, diff --git a/pkg/pipeline/longtext/longtext.py b/pkg/pipeline/longtext/longtext.py index 72c36cdf..85de47c1 100644 --- a/pkg/pipeline/longtext/longtext.py +++ b/pkg/pipeline/longtext/longtext.py @@ -19,9 +19,9 @@ class LongTextProcessStage(stage.PipelineStage): strategy_impl: strategy.LongTextStrategy async def initialize(self): - config = self.ap.cfg_mgr.data - if self.ap.cfg_mgr.data['blob_message_strategy'] == 'image': - use_font = config['font_path'] + config = self.ap.platform_cfg.data['long-text-process'] + if config['strategy'] == 'image': + use_font = config['font-path'] try: # 检查是否存在 if not os.path.exists(use_font): @@ -33,23 +33,25 @@ class LongTextProcessStage(stage.PipelineStage): config['blob_message_strategy'] = "forward" else: self.ap.logger.info("使用Windows自带字体:" + use_font) - self.ap.cfg_mgr.data['font_path'] = use_font + config['font-path'] = use_font else: self.ap.logger.warn("未找到字体文件,且无法使用系统自带字体,更换为转发消息组件以发送长消息,您可以在config.py中调整相关设置。") - self.ap.cfg_mgr.data['blob_message_strategy'] = "forward" + + self.ap.platform_cfg.data['long-text-process']['strategy'] = "forward" except: traceback.print_exc() self.ap.logger.error("加载字体文件失败({}),更换为转发消息组件以发送长消息,您可以在config.py中调整相关设置。".format(use_font)) - self.ap.cfg_mgr.data['blob_message_strategy'] = "forward" + + self.ap.platform_cfg.data['long-text-process']['strategy'] = "forward" - if self.ap.cfg_mgr.data['blob_message_strategy'] == 'image': + if config['strategy'] == 'image': self.strategy_impl = image.Text2ImageStrategy(self.ap) - elif self.ap.cfg_mgr.data['blob_message_strategy'] == 'forward': + elif config['strategy'] == 'forward': self.strategy_impl = forward.ForwardComponentStrategy(self.ap) await self.strategy_impl.initialize() async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: - if len(str(query.resp_message_chain)) > self.ap.cfg_mgr.data['blob_message_threshold']: + if len(str(query.resp_message_chain)) > self.ap.platform_cfg.data['long-text-process']['threshold']: query.resp_message_chain = MessageChain(await self.strategy_impl.process(str(query.resp_message_chain))) return entities.StageProcessResult( result_type=entities.ResultType.CONTINUE, diff --git a/pkg/pipeline/longtext/strategies/image.py b/pkg/pipeline/longtext/strategies/image.py index 4f789098..1d350f60 100644 --- a/pkg/pipeline/longtext/strategies/image.py +++ b/pkg/pipeline/longtext/strategies/image.py @@ -19,7 +19,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): text_render_font: ImageFont.FreeTypeFont async def initialize(self): - self.text_render_font = ImageFont.truetype(self.ap.cfg_mgr.data['font_path'], 32, encoding="utf-8") + self.text_render_font = ImageFont.truetype(self.ap.platform_cfg.data['long-text-process']['font-path'], 32, encoding="utf-8") async def process(self, message: str) -> list[MessageComponent]: img_path = self.text_to_image( diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index 1df5daf9..ad0d7ff6 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -52,7 +52,7 @@ class PreProcessor(stage.PipelineStage): query.messages = event_ctx.event.prompt # 根据模型max_tokens剪裁 - max_tokens = min(query.use_model.max_tokens, self.ap.cfg_mgr.data['prompt_submit_length']) + max_tokens = min(query.use_model.max_tokens, self.ap.pipeline_cfg.data['submit-messages-tokens']) test_messages = query.prompt.messages + query.messages + [query.user_message] @@ -63,7 +63,7 @@ class PreProcessor(stage.PipelineStage): result_type=entities.ResultType.INTERRUPT, new_query=query, user_notice='输入内容过长,请减少情景预设或者输入内容长度', - console_notice='输入内容过长,请减少情景预设或者输入内容长度,或者增大配置文件中的 prompt_submit_length 项(但不能超过所用模型最大tokens数)' + console_notice='输入内容过长,请减少情景预设或者输入内容长度,或者增大配置文件中的 submit-messages-tokens 项(但不能超过所用模型最大tokens数)' ) query.messages.pop(0) # pop第一个肯定是role=user的 diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index c34f2298..26c99a2e 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -54,6 +54,12 @@ class ChatMessageHandler(handler.MessageHandler): ) else: + if not self.ap.provider_cfg.data['enable-chat']: + yield entities.StageProcessResult( + result_type=entities.ResultType.INTERRUPT, + new_query=query, + ) + if event_ctx.event.alter is not None: query.message_chain = mirai.MessageChain([ mirai.Plain(event_ctx.event.alter) @@ -83,7 +89,7 @@ class ChatMessageHandler(handler.MessageHandler): yield entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, new_query=query, - user_notice=self.ap.tips_mgr.data['alter_tip_message'] if self.ap.cfg_mgr.data['hide_exce_info_to_user'] else f'{e}', + user_notice='请求失败' if self.ap.platform_cfg.data['hide-exception-info'] else f'{e}', error_notice=f'{e}', debug_notice=traceback.format_exc() ) diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index 50600a36..d5873e38 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -23,8 +23,8 @@ class CommandHandler(handler.MessageHandler): privilege = 1 - if query.sender_id == self.ap.cfg_mgr.data['admin_qq'] \ - or query.sender_id in self.ap.cfg_mgr['admin_qq']: + + if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.system_cfg.data['admin-sessions']: privilege = 2 spt = str(query.message_chain).strip().split(' ') diff --git a/pkg/pipeline/ratelimit/algos/fixedwin.py b/pkg/pipeline/ratelimit/algos/fixedwin.py index 4996fbaa..bb69b0dd 100644 --- a/pkg/pipeline/ratelimit/algos/fixedwin.py +++ b/pkg/pipeline/ratelimit/algos/fixedwin.py @@ -55,16 +55,16 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): # 获取当前分钟的访问次数 count = container.records.get(now, 0) - limitation = self.ap.cfg_mgr.data['rate_limitation']['default'] + limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default'] - if session_name in self.ap.cfg_mgr.data['rate_limitation']: - limitation = self.ap.cfg_mgr.data['rate_limitation'][session_name] + if session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']: + limitation = self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name] # 如果访问次数超过了限制 if count >= limitation: - if self.ap.cfg_mgr.data['rate_limit_strategy'] == 'drop': + if self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'drop': return False - elif self.ap.cfg_mgr.data['rate_limit_strategy'] == 'wait': + elif self.ap.pipeline_cfg.data['rate-limit']['strategy'] == 'wait': # 等待下一分钟 await asyncio.sleep(60 - time.time() % 60) diff --git a/pkg/pipeline/ratelimit/ratelimit.py b/pkg/pipeline/ratelimit/ratelimit.py index 2e3c6706..cc8e4ac1 100644 --- a/pkg/pipeline/ratelimit/ratelimit.py +++ b/pkg/pipeline/ratelimit/ratelimit.py @@ -42,7 +42,7 @@ class RateLimit(stage.PipelineStage): result_type=entities.ResultType.INTERRUPT, new_query=query, console_notice=f"根据限速规则忽略 {query.launcher_type.value}:{query.launcher_id} 消息", - user_notice=self.ap.tips_mgr.data['rate_limit_drop_tip'] + user_notice=f"请求数超过限速器设定值,已丢弃本消息。" ) elif stage_inst_name == "ReleaseRateLimitOccupancy": await self.algo.release_access( diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 0a60a793..52b23ab6 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -20,7 +20,7 @@ class SendResponseBackStage(stage.PipelineStage): async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: """处理 """ - random_delay = random.uniform(*self.ap.cfg_mgr.data['force_delay_range']) + random_delay = random.uniform(*self.ap.platform_cfg.data['force-delay']) self.ap.logger.debug( "根据规则强制延迟回复: %s s", diff --git a/pkg/pipeline/resprule/resprule.py b/pkg/pipeline/resprule/resprule.py index 6335a7d4..894bbce1 100644 --- a/pkg/pipeline/resprule/resprule.py +++ b/pkg/pipeline/resprule/resprule.py @@ -33,13 +33,13 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: - if query.launcher_type != 'group': + if query.launcher_type.value != 'group': return entities.StageProcessResult( result_type=entities.ResultType.CONTINUE, new_query=query ) - rules = self.ap.cfg_mgr.data['response_rules'] + rules = self.ap.pipeline_cfg.data['respond-rules'] use_rule = rules['default'] diff --git a/pkg/pipeline/resprule/rules/prefix.py b/pkg/pipeline/resprule/rules/prefix.py index 31ead5ab..b23f5f8f 100644 --- a/pkg/pipeline/resprule/rules/prefix.py +++ b/pkg/pipeline/resprule/rules/prefix.py @@ -16,6 +16,7 @@ class PrefixRule(rule_model.GroupRespondRule): for prefix in prefixes: if message_text.startswith(prefix): + return entities.RuleJudgeResult( matching=True, replacement=mirai.MessageChain([ diff --git a/pkg/pipeline/resprule/rules/random.py b/pkg/pipeline/resprule/rules/random.py index 1e8354b5..932d00be 100644 --- a/pkg/pipeline/resprule/rules/random.py +++ b/pkg/pipeline/resprule/rules/random.py @@ -14,7 +14,7 @@ class RandomRespRule(rule_model.GroupRespondRule): message_chain: mirai.MessageChain, rule_dict: dict ) -> entities.RuleJudgeResult: - random_rate = rule_dict['random_rate'] + random_rate = rule_dict['random'] return entities.RuleJudgeResult( matching=random.random() < random_rate, diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index 0625a3d9..0278b603 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -85,7 +85,7 @@ class ResponseWrapper(stage.PipelineStage): query.resp_message_chain = mirai.MessageChain([mirai.Plain(reply_text)]) - if self.ap.cfg_mgr.data['trace_function_calls']: + if self.ap.platform_cfg.data['track-function-calls']: event_ctx = await self.ap.plugin_mgr.emit_event( event=events.NormalMessageResponded( diff --git a/pkg/platform/manager.py b/pkg/platform/manager.py index 277d8ff5..931c5902 100644 --- a/pkg/platform/manager.py +++ b/pkg/platform/manager.py @@ -31,19 +31,16 @@ class PlatformManager: async def initialize(self): - config = self.ap.cfg_mgr.data - - logging.debug("Use adapter:" + config['msg_source_adapter']) - if config['msg_source_adapter'] == 'yirimirai': + if self.ap.platform_cfg.data['platform-adapter'] == 'yiri-mirai': from pkg.platform.sources.yirimirai import YiriMiraiAdapter - mirai_http_api_config = config['mirai_http_api_config'] - self.bot_account_id = config['mirai_http_api_config']['qq'] + mirai_http_api_config = self.ap.platform_cfg.data['yiri-mirai-config'] + self.bot_account_id = mirai_http_api_config['qq'] self.adapter = YiriMiraiAdapter(mirai_http_api_config) - elif config['msg_source_adapter'] == 'nakuru': - from pkg.platform.sources.nakuru import NakuruProjectAdapter - self.adapter = NakuruProjectAdapter(config['nakuru_config']) - self.bot_account_id = self.adapter.bot_account_id + # elif config['msg_source_adapter'] == 'nakuru': + # from pkg.platform.sources.nakuru import NakuruProjectAdapter + # self.adapter = NakuruProjectAdapter(config['nakuru_config']) + # self.bot_account_id = self.adapter.bot_account_id # 保存 account_id 到审计模块 from ..audit.center import apigroup @@ -99,7 +96,7 @@ class PlatformManager: ) # nakuru不区分好友和陌生人,故仅为yirimirai注册陌生人事件 - if config['msg_source_adapter'] == 'yirimirai': + if self.ap.platform_cfg.data['platform-adapter'] == 'yiri-mirai': self.adapter.register_listener( StrangerMessage, on_stranger_message @@ -133,27 +130,26 @@ class PlatformManager: ) async def send(self, event, msg, check_quote=True, check_at_sender=True): - config = self.ap.cfg_mgr.data - if check_at_sender and config['at_sender']: + if check_at_sender and self.ap.platform_cfg.data['at-sender']: msg.insert( 0, Plain(" \n") ) # 当回复的正文中包含换行时,quote可能会自带at,此时就不再单独添加at,只添加换行 - if "\n" not in str(msg[1]) or config['msg_source_adapter'] == 'nakuru': - msg.insert( - 0, - At( - event.sender.id - ) + # if "\n" not in str(msg[1]) or self.ap.platform_cfg.data['platform-adapter'] == 'nakuru': + msg.insert( + 0, + At( + event.sender.id ) + ) await self.adapter.reply_message( event, msg, - quote_origin=True if config['quote_origin'] and check_quote else False + quote_origin=True if self.ap.platform_cfg.data['quote-origin'] and check_quote else False ) # 通知系统管理员 @@ -161,19 +157,16 @@ class PlatformManager: await self.notify_admin_message_chain(MessageChain([Plain("[bot]{}".format(message))])) async def notify_admin_message_chain(self, message: mirai.MessageChain): - config = self.ap.cfg_mgr.data - if config['admin_qq'] != 0 and config['admin_qq'] != []: - logging.info("通知管理员:{}".format(message)) + if self.ap.system_cfg.data['admin-sessions'] != []: admin_list = [] - - if type(config['admin_qq']) == int: - admin_list.append(config['admin_qq']) + for admin in self.ap.system_cfg.data['admin-sessions']: + admin_list.append(admin) for adm in admin_list: self.adapter.send_message( - "person", - adm, + adm.split("_")[0], + adm.split("_")[1], message ) diff --git a/pkg/provider/requester/apis/chatcmpl.py b/pkg/provider/requester/apis/chatcmpl.py index d82152bd..8630d4e3 100644 --- a/pkg/provider/requester/apis/chatcmpl.py +++ b/pkg/provider/requester/apis/chatcmpl.py @@ -22,8 +22,8 @@ class OpenAIChatCompletion(api.LLMAPIRequester): async def initialize(self): self.client = openai.AsyncClient( api_key="", - base_url=self.ap.cfg_mgr.data["openai_config"]["reverse_proxy"], - timeout=self.ap.cfg_mgr.data["process_message_timeout"], + base_url=self.ap.provider_cfg.data['openai-config']['base_url'], + timeout=self.ap.provider_cfg.data['openai-config']['request-timeout'], ) async def _req( @@ -51,7 +51,7 @@ class OpenAIChatCompletion(api.LLMAPIRequester): ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = self.ap.cfg_mgr.data["completion_api_params"].copy() + args = self.ap.provider_cfg.data['openai-config']['chat-completions-params'].copy() args["model"] = use_model.name if use_model.model_name is None else use_model.model_name if use_model.tool_call_supported: diff --git a/pkg/provider/requester/modelmgr.py b/pkg/provider/requester/modelmgr.py index ab5ff9e4..dd942970 100644 --- a/pkg/provider/requester/modelmgr.py +++ b/pkg/provider/requester/modelmgr.py @@ -29,7 +29,7 @@ class ModelManager: async def initialize(self): openai_chat_completion = chatcmpl.OpenAIChatCompletion(self.ap) await openai_chat_completion.initialize() - openai_token_mgr = token.TokenManager(self.ap, list(self.ap.cfg_mgr.data['openai_config']['api_key'].values())) + openai_token_mgr = token.TokenManager(self.ap, list(self.ap.provider_cfg.data['openai-config']['api-keys'])) tiktoken_tokenizer = tiktoken.Tiktoken(self.ap) diff --git a/pkg/provider/session/sessionmgr.py b/pkg/provider/session/sessionmgr.py index a20e2b52..a7812504 100644 --- a/pkg/provider/session/sessionmgr.py +++ b/pkg/provider/session/sessionmgr.py @@ -25,10 +25,15 @@ class SessionManager: if query.launcher_type == session.launcher_type and query.launcher_id == session.launcher_id: return session + session_concurrency = self.ap.system_cfg.data['session-concurrency']['default'] + + if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.system_cfg.data['session-concurrency']: + session_concurrency = self.ap.system_cfg.data['session-concurrency'][f'{query.launcher_type.value}_{query.launcher_id}'] + session = core_entities.Session( launcher_type=query.launcher_type, launcher_id=query.launcher_id, - semaphore=asyncio.Semaphore(1) if self.ap.cfg_mgr.data['wait_last_done'] else asyncio.Semaphore(10000), + semaphore=asyncio.Semaphore(session_concurrency), ) self.session_list.append(session) return session @@ -41,7 +46,7 @@ class SessionManager: conversation = core_entities.Conversation( prompt=await self.ap.prompt_mgr.get_prompt(session.use_prompt_name), messages=[], - use_model=await self.ap.model_mgr.get_model_by_name(self.ap.cfg_mgr.data['completion_api_params']['model']), + use_model=await self.ap.model_mgr.get_model_by_name(self.ap.provider_cfg.data['openai-config']['chat-completions-params']['model']), use_funcs=await self.ap.tool_mgr.get_all_functions(), ) session.conversations.append(conversation) diff --git a/pkg/provider/sysprompt/loaders/scenario.py b/pkg/provider/sysprompt/loaders/scenario.py index 917de486..a559ff73 100644 --- a/pkg/provider/sysprompt/loaders/scenario.py +++ b/pkg/provider/sysprompt/loaders/scenario.py @@ -14,8 +14,8 @@ class ScenarioPromptLoader(loader.PromptLoader): async def load(self): """加载Prompt """ - for file in os.listdir("scenarios"): - with open("scenarios/{}".format(file), "r", encoding="utf-8") as f: + for file in os.listdir("data/scenarios"): + with open("data/scenarios/{}".format(file), "r", encoding="utf-8") as f: file_str = f.read() file_name = file.split(".")[0] file_json = json.loads(file_str) diff --git a/pkg/provider/sysprompt/loaders/single.py b/pkg/provider/sysprompt/loaders/single.py index 0b109630..57e06ed2 100644 --- a/pkg/provider/sysprompt/loaders/single.py +++ b/pkg/provider/sysprompt/loaders/single.py @@ -14,7 +14,7 @@ class SingleSystemPromptLoader(loader.PromptLoader): """加载Prompt """ - for name, cnt in self.ap.cfg_mgr.data['default_prompt'].items(): + for name, cnt in self.ap.provider_cfg.data['prompt'].items(): prompt = entities.Prompt( name=name, messages=[ @@ -26,8 +26,8 @@ class SingleSystemPromptLoader(loader.PromptLoader): ) self.prompts.append(prompt) - for file in os.listdir("prompts"): - with open("prompts/{}".format(file), "r", encoding="utf-8") as f: + for file in os.listdir("data/prompts"): + with open("data/prompts/{}".format(file), "r", encoding="utf-8") as f: file_str = f.read() file_name = file.split(".")[0] prompt = entities.Prompt( diff --git a/pkg/provider/sysprompt/sysprompt.py b/pkg/provider/sysprompt/sysprompt.py index 5df28ee7..5500bb10 100644 --- a/pkg/provider/sysprompt/sysprompt.py +++ b/pkg/provider/sysprompt/sysprompt.py @@ -23,7 +23,7 @@ class PromptManager: "full_scenario": scenario.ScenarioPromptLoader } - loader_cls = loader_map[self.ap.cfg_mgr.data['preset_mode']] + loader_cls = loader_map[self.ap.provider_cfg.data['prompt-mode']] self.loader_inst: loader.PromptLoader = loader_cls(self.ap) diff --git a/pkg/utils/proxy.py b/pkg/utils/proxy.py index 1c5ee18c..74228b86 100644 --- a/pkg/utils/proxy.py +++ b/pkg/utils/proxy.py @@ -1,5 +1,8 @@ from __future__ import annotations +import os +import sys + from ..core import app @@ -14,17 +17,15 @@ class ProxyManager: self.forward_proxies = {} async def initialize(self): - config = self.ap.cfg_mgr.data + self.forward_proxies = { + "http": os.getenv("HTTP_PROXY") or os.getenv("http_proxy"), + "https": os.getenv("HTTPS_PROXY") or os.getenv("https_proxy"), + } - return ( - { - "http": config["openai_config"]["proxy"], - "https": config["openai_config"]["proxy"], - } - if "proxy" in config["openai_config"] - and (config["openai_config"]["proxy"] is not None) - else None - ) + if 'http' in self.ap.system_cfg.data['network-proxies']: + self.forward_proxies['http'] = self.ap.system_cfg.data['network-proxies']['http'] + if 'https' in self.ap.system_cfg.data['network-proxies']: + self.forward_proxies['https'] = self.ap.system_cfg.data['network-proxies']['https'] - def get_forward_proxies(self) -> str: + def get_forward_proxies(self) -> dict: return self.forward_proxies diff --git a/templates/__init__.py b/templates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/templates/command.json b/templates/command.json new file mode 100644 index 00000000..55360fc6 --- /dev/null +++ b/templates/command.json @@ -0,0 +1,3 @@ +{ + "privilege": {} +} \ No newline at end of file diff --git a/templates/pipeline.json b/templates/pipeline.json new file mode 100644 index 00000000..7284fdaf --- /dev/null +++ b/templates/pipeline.json @@ -0,0 +1,36 @@ +{ + "access-control":{ + "mode": "blacklist", + "blacklist": [], + "whitelist": [] + }, + "respond-rules": { + "default": { + "at": true, + "prefix": [ + "/ai", "!ai", "!ai", "ai" + ], + "regexp": [], + "random": 0.0 + } + }, + "income-msg-check": true, + "ignore-rules": { + "prefix": ["/"], + "regexp": [] + }, + "check-sensitive-words": true, + "baidu-cloud-examine": { + "enable": false, + "api-key": "", + "api-secret": "" + }, + "submit-messages-tokens": 3072, + "rate-limit": { + "strategy": "drop", + "algo": "fixwin", + "fixwin": { + "default": 60 + } + } +} \ No newline at end of file diff --git a/templates/platform.json b/templates/platform.json new file mode 100644 index 00000000..a25ba3e5 --- /dev/null +++ b/templates/platform.json @@ -0,0 +1,20 @@ +{ + "platform-adapter": "yiri-mirai", + "yiri-mirai-config": { + "adapter": "WebSocketAdapter", + "host": "localhost", + "port": 8080, + "verifyKey": "yirimirai", + "qq": 123456789 + }, + "track-function-calls": true, + "quote-origin": false, + "at-sender": false, + "force-delay": [0, 0], + "long-text-process": { + "threshold": 256, + "strategy": "forward", + "font-path": "" + }, + "hide-exception-info": true +} \ No newline at end of file diff --git a/templates/plugin-settings.json b/templates/plugin-settings.json new file mode 100644 index 00000000..1d807ed1 --- /dev/null +++ b/templates/plugin-settings.json @@ -0,0 +1,3 @@ +{ + "plugins": [] +} \ No newline at end of file diff --git a/templates/provider.json b/templates/provider.json new file mode 100644 index 00000000..23360277 --- /dev/null +++ b/templates/provider.json @@ -0,0 +1,17 @@ +{ + "enable-chat": true, + "openai-config": { + "api-keys": [ + "sk-1234567890" + ], + "base_url": "https://api.openai.com/v1", + "chat-completions-params": { + "model": "gpt-3.5-turbo" + }, + "request-timeout": 120 + }, + "prompt-mode": "normal", + "prompt": { + "default": "如果用户之后想获取帮助,请你说”输入!help获取帮助“。" + } +} \ No newline at end of file diff --git a/templates/scenario-template.json b/templates/scenario-template.json new file mode 100644 index 00000000..d9b7267a --- /dev/null +++ b/templates/scenario-template.json @@ -0,0 +1,12 @@ +{ + "prompt": [ + { + "role": "system", + "content": "You are a helpful assistant. 如果我需要帮助,你要说“输入!help获得帮助”" + }, + { + "role": "assistant", + "content": "好的,我是一个能干的AI助手。 如果你需要帮助,我会说“输入!help获得帮助”" + } + ] +} \ No newline at end of file diff --git a/templates/sensitive-words.json b/templates/sensitive-words.json new file mode 100644 index 00000000..61d15ff9 --- /dev/null +++ b/templates/sensitive-words.json @@ -0,0 +1,78 @@ +{ + "说明": "mask将替换敏感词中的每一个字,若mask_word值不为空,则将敏感词整个替换为mask_word的值", + "mask": "*", + "mask_word": "", + "words": [ + "习近平", + "胡锦涛", + "江泽民", + "温家宝", + "李克强", + "李长春", + "毛泽东", + "邓小平", + "周恩来", + "马克思", + "社会主义", + "共产党", + "共产主义", + "大陆官方", + "北京政权", + "中华帝国", + "中国政府", + "共狗", + "六四事件", + "天安门", + "六四", + "政治局常委", + "两会", + "共青团", + "学潮", + "八九", + "二十大", + "民进党", + "台独", + "台湾独立", + "台湾国", + "国民党", + "台湾民国", + "中华民国", + "pornhub", + "Pornhub", + "[Yy]ou[Pp]orn", + "porn", + "Porn", + "[Xx][Vv]ideos", + "[Rr]ed[Tt]ube", + "[Xx][Hh]amster", + "[Ss]pank[Ww]ire", + "[Ss]pank[Bb]ang", + "[Tt]ube8", + "[Yy]ou[Jj]izz", + "[Bb]razzers", + "[Nn]aughty[ ]?[Aa]merica", + "作爱", + "做爱", + "性交", + "性爱", + "自慰", + "阴茎", + "淫妇", + "肛交", + "交配", + "性关系", + "性活动", + "色情", + "色图", + "涩图", + "裸体", + "小穴", + "淫荡", + "性爱", + "翻墙", + "VPN", + "科学上网", + "挂梯子", + "GFW" + ] +} \ No newline at end of file diff --git a/templates/system.json b/templates/system.json new file mode 100644 index 00000000..c7b0118e --- /dev/null +++ b/templates/system.json @@ -0,0 +1,11 @@ +{ + "admin-sessions": [], + "network-proxies": {}, + "report-usage": true, + "logging-level": "info", + "session-concurrency": { + "default": 1 + }, + "pipeline-concurrency": 20, + "help-message": "QChatGPT - 😎高稳定性、🧩支持插件、🌏实时联网的 ChatGPT QQ 机器人🤖\n链接:https://q.rkcn.top" +} \ No newline at end of file From c1fed3410be05442fca1ee6e1f789eeae7609311 Mon Sep 17 00:00:00 2001 From: RockChinQ <1010553892@qq.com> Date: Tue, 6 Feb 2024 21:27:14 +0800 Subject: [PATCH 2/7] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E8=BF=87?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- banlist-template.py | 30 -- config-template.py | 372 --------------------- override-all.json | 90 ----- res/templates/cmdpriv-template.json | 30 -- res/templates/plugin-setting-template.json | 3 - res/templates/sensitive-template.json | 78 ----- tips-custom-template.py | 37 -- 7 files changed, 640 deletions(-) delete mode 100644 banlist-template.py delete mode 100644 config-template.py delete mode 100644 override-all.json delete mode 100644 res/templates/cmdpriv-template.json delete mode 100644 res/templates/plugin-setting-template.json delete mode 100644 res/templates/sensitive-template.json delete mode 100644 tips-custom-template.py diff --git a/banlist-template.py b/banlist-template.py deleted file mode 100644 index dcaf375e..00000000 --- a/banlist-template.py +++ /dev/null @@ -1,30 +0,0 @@ -# 是否处理群聊消息 -# 为False时忽略所有群聊消息 -# 优先级高于下方禁用列表 -enable_group = True - -# 是否处理私聊消息 -# 为False时忽略所有私聊消息 -# 优先级高于下方禁用列表 -enable_private = True - -# 是否启用禁用列表 -enable = True - -# 禁用规则(黑名单) -# person为个人,其中的QQ号会被禁止与机器人进行私聊或群聊交互 -# 示例: person = [2854196310, 1234567890, 9876543210] -# group为群组,其中的群号会被禁止与机器人进行交互 -# 示例: group = [123456789, 987654321, 1234567890] -# -# 支持正则表达式,字符串都将被识别为正则表达式,例如: -# person = [12345678, 87654321, "2854.*"] -# group = [123456789, 987654321, "1234.*"] -# 若要排除某个QQ号或群号(即允许使用),可以在前面加上"!",例如: -# person = ["!1234567890"] -# group = ["!987654321"] -# 排除规则优先级高于包含规则,即如果同时存在包含规则和排除规则,排除规则将生效,例如: -# person = ["1234.*", "!1234567890"] -# 那么1234567890将不会被禁用,而其他以1234开头的QQ号都会被禁用 -person = [2854196310] # 2854196310是Q群管家机器人的QQ号,默认屏蔽以免出现循环 -group = [204785790, 691226829] # 本项目交流群的群号,默认屏蔽,避免在交流群测试机器人 diff --git a/config-template.py b/config-template.py deleted file mode 100644 index 6808bb8a..00000000 --- a/config-template.py +++ /dev/null @@ -1,372 +0,0 @@ -# 配置文件: 注释里标[必需]的参数必须修改, 其他参数根据需要修改, 但请勿删除 -import logging - -# 消息处理协议适配器 -# 目前支持以下适配器: -# - "yirimirai": mirai的通信框架,YiriMirai框架适配器, 请同时填写下方mirai_http_api_config -# - "nakuru": go-cqhttp通信框架,请同时填写下方nakuru_config -msg_source_adapter = "yirimirai" - -# [必需(与nakuru二选一,取决于msg_source_adapter)] Mirai的配置 -# 请到配置mirai的步骤中的教程查看每个字段的信息 -# adapter: 选择适配器,目前支持HTTPAdapter和WebSocketAdapter -# host: 运行mirai的主机地址 -# port: 运行mirai的主机端口 -# verifyKey: mirai-api-http的verifyKey -# qq: 机器人的QQ号 -# -# 注意: QQ机器人配置不支持热重载及热更新 -mirai_http_api_config = { - "adapter": "WebSocketAdapter", - "host": "localhost", - "port": 8080, - "verifyKey": "yirimirai", - "qq": 1234567890 -} - -# [必需(与mirai二选一,取决于msg_source_adapter)] -# 使用nakuru-project框架连接go-cqhttp的配置 -nakuru_config = { - "host": "localhost", # go-cqhttp的地址 - "port": 6700, # go-cqhttp的正向websocket端口 - "http_port": 5700, # go-cqhttp的正向http端口 - "token": "" # 若在go-cqhttp的config.yml设置了access_token, 则填写此处 -} - -# [必需] OpenAI的配置 -# api_key: OpenAI的API Key -# http_proxy: 请求OpenAI时使用的代理,None为不使用,https和socks5暂不能使用 -# 若只有一个api-key,请直接修改以下内容中的"openai_api_key"为你的api-key -# -# 如准备了多个api-key,可以以字典的形式填写,程序会自动选择可用的api-key -# 例如 -# openai_config = { -# "api_key": { -# "default": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -# "key1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -# "key2": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -# }, -# "http_proxy": "http://127.0.0.1:12345" -# } -# -# 现已支持反向代理,可以添加reverse_proxy字段以使用反向代理 -# 使用反向代理可以在国内使用OpenAI的API,反向代理的配置请参考 -# https://github.com/Ice-Hazymoon/openai-scf-proxy -# -# 反向代理填写示例: -# openai_config = { -# "api_key": { -# "default": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -# "key1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -# "key2": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -# }, -# "reverse_proxy": "http://example.com:12345/v1" -# } -# -# 作者开设公用反向代理地址: https://api.openai.rockchin.top/v1 -# 随时可能关闭,仅供测试使用,有条件建议使用正向代理或者自建反向代理 -openai_config = { - "api_key": { - "default": "openai_api_key" - }, - "http_proxy": None, - "reverse_proxy": None -} - -# api-key切换策略 -# active:每次请求时都会切换api-key -# passive:仅当api-key超额时才会切换api-key -switch_strategy = "active" - -# [必需] 管理员QQ号,用于接收报错等通知及执行管理员级别命令 -# 支持多个管理员,可以使用list形式设置,例如: -# admin_qq = [12345678, 87654321] -admin_qq = 0 - -# 情景预设(机器人人格) -# 每个会话的预设信息,影响所有会话,无视命令重置 -# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令 -# 例如: -# default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”" -# 这样用户在不知所措的时候机器人就会提示其输入!help获取帮助 -# 可参考 https://github.com/PlexPt/awesome-chatgpt-prompts-zh -# -# 如果需要多个情景预设,并在运行期间方便切换,请使用字典的形式填写,例如 -# default_prompt = { -# "default": "如果我之后想获取帮助,请你说“输入!help获取帮助”", -# "linux-terminal": "我想让你充当 Linux 终端。我将输入命令,您将回复终端应显示的内容。", -# "en-dict": "我想让你充当英英词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈。", -# } -# -# 在使用期间即可通过命令: -# !reset [名称] -# 来使用指定的情景预设重置会话 -# 例如: -# !reset linux-terminal -# 若不指定名称,则使用默认情景预设 -# -# 也可以使用命令: -# !default <名称> -# 将指定的情景预设设置为默认情景预设 -# 例如: -# !default linux-terminal -# 之后的会话重置时若不指定名称,则使用linux-terminal情景预设 -# -# 还可以加载文件中的预设文字,使用方法请查看:https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E9%A2%84%E8%AE%BE%E6%96%87%E5%AD%97 -default_prompt = { - "default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。", -} - -# 情景预设格式 -# 参考值:默认方式:normal | 完整情景:full_scenario -# 默认方式 的格式为上述default_prompt中的内容,或prompts目录下的文件名 -# 完整情景方式 的格式为JSON,在scenario目录下的JSON文件中列出对话的每个回合,编写方法见scenario/default-template.json -# 编写方法请查看:https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E9%A2%84%E8%AE%BE%E6%96%87%E5%AD%97full_scenario%E6%A8%A1%E5%BC%8F -preset_mode = "normal" - -# 群内响应规则 -# 符合此消息的群内消息即使不包含at机器人也会响应 -# 支持消息前缀匹配及正则表达式匹配 -# 支持设置是否响应at消息、随机响应概率 -# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式(regexp)匹配的消息不会删除匹配的部分 -# 前缀匹配优先级高于正则表达式匹配 -# 正则表达式简明教程:https://www.runoob.com/regexp/regexp-tutorial.html -# -# 支持针对不同群设置不同的响应规则,例如: -# response_rules = { -# "default": { -# "at": True, -# "prefix": ["/ai", "!ai", "!ai", "ai"], -# "regexp": [], -# "random_rate": 0.0, -# }, -# "12345678": { -# "at": False, -# "prefix": ["/ai", "!ai", "!ai", "ai"], -# "regexp": [], -# "random_rate": 0.0, -# }, -# } -# -# 以上设置将会在群号为12345678的群中关闭at响应 -# 未单独设置的群将使用default规则 -response_rules = { - "default": { - "at": True, # 是否响应at机器人的消息 - "prefix": ["/ai", "!ai", "!ai", "ai"], - "regexp": [], # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办" - "random_rate": 0.0, # 随机响应概率,0.0-1.0,0.0为不随机响应,1.0为响应所有消息, 仅在前几项判断不通过时生效 - }, -} - - -# 消息忽略规则 -# 适用于私聊及群聊 -# 符合此规则的消息将不会被响应 -# 支持消息前缀匹配及正则表达式匹配 -# 此设置优先级高于response_rules -# 用以过滤mirai等其他层级的命令 -# @see https://github.com/RockChinQ/QChatGPT/issues/165 -# -# *需要同时开启下方 income_msg_check 才会生效 -ignore_rules = { - "prefix": ["/"], - "regexp": [] -} - -# 是否检查收到的消息中是否包含敏感词 -# 若收到的消息无法通过下方指定的敏感词检查策略,则发送提示信息 -income_msg_check = False - -# 敏感词过滤开关,以同样数量的*代替敏感词回复 -# 请在sensitive.json中添加敏感词 -sensitive_word_filter = True - -# 是否启用百度云内容安全审核 -# 注册方式查看 https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy -baidu_check = False - -# 百度云API_KEY 24位英文数字字符串 -baidu_api_key = "" - -# 百度云SECRET_KEY 32位的英文数字字符串 -baidu_secret_key = "" - -# 不合规消息自定义返回 -inappropriate_message_tips = "[百度云]请珍惜机器人,当前返回内容不合规" - -# 启动时是否发送赞赏码 -# 仅当使用量已经超过2048字时发送 -encourage_sponsor_at_start = True - -# 每次向OpenAI接口发送对话记录上下文的字符数 -# 最大不超过(4096 - max_tokens)个字符,max_tokens为下方completion_api_params中的max_tokens -# 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快 -prompt_submit_length = 3072 - -# 是否在token超限报错时自动重置会话 -# 可在tips.py中编辑提示语 -auto_reset = True - -# OpenAI补全API的参数 -# 请在下方填写模型,程序自动选择接口 -# 模型文档:https://platform.openai.com/docs/models -# 现已支持的模型有: -# -# ChatCompletions 接口: -# # GPT 4 系列 -# "gpt-4-1106-preview", -# "gpt-4-vision-preview", -# "gpt-4", -# "gpt-4-32k", -# "gpt-4-0613", -# "gpt-4-32k-0613", -# "gpt-4-0314", # legacy -# "gpt-4-32k-0314", # legacy -# # GPT 3.5 系列 -# "gpt-3.5-turbo-1106", -# "gpt-3.5-turbo", -# "gpt-3.5-turbo-16k", -# "gpt-3.5-turbo-0613", # legacy -# "gpt-3.5-turbo-16k-0613", # legacy -# "gpt-3.5-turbo-0301", # legacy -# -# Completions接口: -# "gpt-3.5-turbo-instruct", -# -# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create -# 请将内容修改到config.py中,请勿修改config-template.py -# -# 支持通过 One API 接入多种模型,请在上方的openai_config中设置One API的代理地址, -# 并在此填写您要使用的模型名称,详细请参考:https://github.com/songquanpeng/one-api -# -# 支持的 One API 模型: -# "SparkDesk", -# "chatglm_pro", -# "chatglm_std", -# "chatglm_lite", -# "qwen-v1", -# "qwen-plus-v1", -# "ERNIE-Bot", -# "ERNIE-Bot-turbo", -# "gemini-pro", -completion_api_params = { - "model": "gpt-3.5-turbo", - "temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1] -} - -# OpenAI的Image API的参数 -# 具体请查看OpenAI的文档: https://platform.openai.com/docs/api-reference/images/create -image_api_params = { - "model": "dall-e-2", # 默认使用 dall-e-2 模型,也可以改为 dall-e-3 - # 图片尺寸 - # dall-e-2 模型支持 256x256, 512x512, 1024x1024 - # dall-e-3 模型支持 1024x1024, 1792x1024, 1024x1792 - "size": "256x256", -} - -# 跟踪函数调用 -# 为True时,在每次GPT进行Function Calling时都会输出发送一条回复给用户 -# 同时,一次提问内所有的Function Calling和普通回复消息都会单独发送给用户 -trace_function_calls = True - -# 群内回复消息时是否引用原消息 -quote_origin = False - -# 群内回复消息时是否at发送者 -at_sender = False - -# 回复绘图时是否包含图片描述 -include_image_description = True - -# 消息处理的超时时间,单位为秒 -process_message_timeout = 120 - -# 回复消息时是否显示[GPT]前缀 -show_prefix = False - -# 回复前的强制延迟时间,降低机器人被腾讯风控概率 -# *此机制对命令和消息、私聊及群聊均生效 -# 每次处理时从以下的范围取一个随机秒数, -# 当此次消息处理时间低于此秒数时,将会强制延迟至此秒数 -# 例如:[1.5, 3],则每次处理时会随机取一个1.5-3秒的随机数,若处理时间低于此随机数,则强制延迟至此随机秒数 -# 若您不需要此功能,请将force_delay_range设置为[0, 0] -force_delay_range = [0, 0] - -# 应用长消息处理策略的阈值 -# 当回复消息长度超过此值时,将使用长消息处理策略 -blob_message_threshold = 256 - -# 长消息处理策略 -# - "image": 将长消息转换为图片发送 -# - "forward": 将长消息转换为转发消息组件发送 -blob_message_strategy = "forward" - -# 允许等待 -# 同一会话内,是否等待上一条消息处理完成后再处理下一条消息 -# 若设置为False,若上一条未处理完时收到了新消息,将会丢弃新消息 -# 丢弃消息时的提示信息可以在tips.py中修改 -wait_last_done = True - -# 文字转图片时使用的字体文件路径 -# 当策略为"image"时生效 -# 若在Windows系统下,程序会自动使用Windows自带的微软雅黑字体 -# 若未填写或不存在且不是Windows,将禁用文字转图片功能,改为使用转发消息组件 -font_path = "" - -# 消息处理超时重试次数 -retry_times = 3 - -# 消息处理出错时是否向用户隐藏错误详细信息 -# 设置为True时,仅向管理员发送错误详细信息 -# 设置为False时,向用户及管理员发送错误详细信息 -hide_exce_info_to_user = False - -# 每个会话的过期时间,单位为秒 -# 默认值20分钟 -session_expire_time = 1200 - -# 会话限速 -# 单会话内每分钟可进行的对话次数 -# 若不需要限速,可以设置为一个很大的值 -# 默认值60次,基本上不会触发限速 -# -# 若要设置针对某特定群的限速,请使用如下格式: -# { -# "group_<群号>": 60, -# "default": 60, -# } -# 若要设置针对某特定用户私聊的限速,请使用如下格式: -# { -# "person_<用户QQ>": 60, -# "default": 60, -# } -# 同时设置多个群和私聊的限速,示例: -# { -# "group_12345678": 60, -# "group_87654321": 60, -# "person_234567890": 60, -# "person_345678901": 60, -# "default": 60, -# } -# -# 注意: 未指定的都使用default的限速值,default不可删除 -rate_limitation = { - "default": 60, -} - -# 会话限速策略 -# - "wait": 每次对话获取到回复时,等待一定时间再发送回复,保证其不会超过限速均值 -# - "drop": 此分钟内,若对话次数超过限速次数,则丢弃之后的对话,每自然分钟重置 -rate_limit_strategy = "drop" - -# 是否在启动时进行依赖库更新 -upgrade_dependencies = False - -# 是否上报统计信息 -# 用于统计机器人的使用情况,数据不公开,不会收集任何敏感信息。 -# 仅实例识别UUID、上报时间、字数使用量、绘图使用量、插件使用情况、用户信息,其他信息不会上报 -report_usage = True - -# 日志级别 -logging_level = logging.INFO diff --git a/override-all.json b/override-all.json deleted file mode 100644 index d790edb7..00000000 --- a/override-all.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "comment": "这是override.json支持的字段全集, 关于override.json机制, 请查看https://github.com/RockChinQ/QChatGPT/pull/271", - "msg_source_adapter": "yirimirai", - "mirai_http_api_config": { - "adapter": "WebSocketAdapter", - "host": "localhost", - "port": 8080, - "verifyKey": "yirimirai", - "qq": 1234567890 - }, - "nakuru_config": { - "host": "localhost", - "port": 6700, - "http_port": 5700, - "token": "" - }, - "openai_config": { - "api_key": { - "default": "openai_api_key" - }, - "http_proxy": null, - "reverse_proxy": null - }, - "switch_strategy": "active", - "admin_qq": 0, - "default_prompt": { - "default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。" - }, - "preset_mode": "normal", - "response_rules": { - "default": { - "at": true, - "prefix": [ - "/ai", - "!ai", - "!ai", - "ai" - ], - "regexp": [], - "random_rate": 0.0 - } - }, - "ignore_rules": { - "prefix": [ - "/" - ], - "regexp": [] - }, - "income_msg_check": false, - "sensitive_word_filter": true, - "baidu_check": false, - "baidu_api_key": "", - "baidu_secret_key": "", - "inappropriate_message_tips": "[百度云]请珍惜机器人,当前返回内容不合规", - "encourage_sponsor_at_start": true, - "prompt_submit_length": 3072, - "auto_reset": true, - "completion_api_params": { - "model": "gpt-3.5-turbo", - "temperature": 0.9 - }, - "image_api_params": { - "model": "dall-e-2", - "size": "256x256" - }, - "trace_function_calls": true, - "quote_origin": false, - "at_sender": false, - "include_image_description": true, - "process_message_timeout": 120, - "show_prefix": false, - "force_delay_range": [ - 0, - 0 - ], - "blob_message_threshold": 256, - "blob_message_strategy": "forward", - "wait_last_done": true, - "font_path": "", - "retry_times": 3, - "hide_exce_info_to_user": false, - "session_expire_time": 1200, - "rate_limitation": { - "default": 60 - }, - "rate_limit_strategy": "drop", - "upgrade_dependencies": false, - "report_usage": true, - "logging_level": 20 -} \ No newline at end of file diff --git a/res/templates/cmdpriv-template.json b/res/templates/cmdpriv-template.json deleted file mode 100644 index 33603878..00000000 --- a/res/templates/cmdpriv-template.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "comment": "以下为命令权限,请设置到cmdpriv.json中。关于此功能的说明,请查看:https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%91%BD%E4%BB%A4%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6", - "draw": 1, - "func": 1, - "plugin": 1, - "plugin.get": 2, - "plugin.update": 2, - "plugin.del": 2, - "plugin.off": 2, - "plugin.on": 2, - "default": 1, - "default.set": 2, - "del": 1, - "del.all": 1, - "delhst": 2, - "delhst.all": 2, - "last": 1, - "list": 1, - "next": 1, - "prompt": 1, - "resend": 1, - "reset": 1, - "cfg": 2, - "cmd": 1, - "help": 1, - "reload": 2, - "update": 2, - "usage": 1, - "version": 1 -} \ No newline at end of file diff --git a/res/templates/plugin-setting-template.json b/res/templates/plugin-setting-template.json deleted file mode 100644 index 1d807ed1..00000000 --- a/res/templates/plugin-setting-template.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": [] -} \ No newline at end of file diff --git a/res/templates/sensitive-template.json b/res/templates/sensitive-template.json deleted file mode 100644 index 61d15ff9..00000000 --- a/res/templates/sensitive-template.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "说明": "mask将替换敏感词中的每一个字,若mask_word值不为空,则将敏感词整个替换为mask_word的值", - "mask": "*", - "mask_word": "", - "words": [ - "习近平", - "胡锦涛", - "江泽民", - "温家宝", - "李克强", - "李长春", - "毛泽东", - "邓小平", - "周恩来", - "马克思", - "社会主义", - "共产党", - "共产主义", - "大陆官方", - "北京政权", - "中华帝国", - "中国政府", - "共狗", - "六四事件", - "天安门", - "六四", - "政治局常委", - "两会", - "共青团", - "学潮", - "八九", - "二十大", - "民进党", - "台独", - "台湾独立", - "台湾国", - "国民党", - "台湾民国", - "中华民国", - "pornhub", - "Pornhub", - "[Yy]ou[Pp]orn", - "porn", - "Porn", - "[Xx][Vv]ideos", - "[Rr]ed[Tt]ube", - "[Xx][Hh]amster", - "[Ss]pank[Ww]ire", - "[Ss]pank[Bb]ang", - "[Tt]ube8", - "[Yy]ou[Jj]izz", - "[Bb]razzers", - "[Nn]aughty[ ]?[Aa]merica", - "作爱", - "做爱", - "性交", - "性爱", - "自慰", - "阴茎", - "淫妇", - "肛交", - "交配", - "性关系", - "性活动", - "色情", - "色图", - "涩图", - "裸体", - "小穴", - "淫荡", - "性爱", - "翻墙", - "VPN", - "科学上网", - "挂梯子", - "GFW" - ] -} \ No newline at end of file diff --git a/tips-custom-template.py b/tips-custom-template.py deleted file mode 100644 index 129f957f..00000000 --- a/tips-custom-template.py +++ /dev/null @@ -1,37 +0,0 @@ -import config -# ---------------------------------------------自定义提示语--------------------------------------------- - -# 消息处理出错时向用户发送的提示信息,仅当config.py中hide_exce_info_to_user为True时生效 -# 设置为空字符串时,不发送提示信息 -alter_tip_message = '[bot]err:出错了,请稍后再试' - -# drop策略时,超过限速均值时,丢弃的对话的提示信息,仅当config.py中rate_limitation_strategy为"drop"时生效 -# 若设置为空字符串,则不发送提示信息 -rate_limit_drop_tip = "本分钟对话次数超过限速次数,此对话被丢弃" - -# 只允许同时处理一条消息时,新消息被丢弃时的提示信息 -# 当config.py中的wait_last_done为False时生效 -# 若设置为空字符串,则不发送提示信息 -message_drop_tip = "[bot]当前有一条消息正在处理,请等待处理完成" - -# 命令 !help帮助消息 -help_message = """此机器人通过调用大型语言模型生成回复,不具有情感。 -你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。 -欢迎到github.com/RockChinQ/QChatGPT 给个star""" - -# 私聊消息超时提示 -reply_message = "[bot]err:请求超时" -# 群聊消息超时提示 -replys_message = "[bot]err:请求超时" - -# 命令权限不足提示 -command_admin_message = "[bot]err:权限不足: " -# 命令无效提示 -command_err_message = "[bot]err:命令不存在:" - -# 会话重置提示 -command_reset_message = "[bot]会话已重置" -command_reset_name_message = "[bot]会话已重置,使用场景预设:" - -# 会话自动重置时的提示 -session_auto_reset_message = "[bot]会话token超限,已自动重置,请重新发送消息" From 26912ef9760c1c6dd4e833ea24e76f0efcef7a08 Mon Sep 17 00:00:00 2001 From: RockChinQ <1010553892@qq.com> Date: Tue, 6 Feb 2024 21:28:01 +0800 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test.html | 10 ---------- 未命名.txt | Bin 1038 -> 0 bytes 2 files changed, 10 deletions(-) delete mode 100644 test.html delete mode 100644 未命名.txt diff --git a/test.html b/test.html deleted file mode 100644 index bdb9b804..00000000 --- a/test.html +++ /dev/null @@ -1,10 +0,0 @@ - -
-Test
-This is a test for QChatGPT.
- - \ No newline at end of file diff --git a/未命名.txt b/未命名.txt deleted file mode 100644 index ad3969353d5172dd95b249145d6b2d6751bcaecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1038 zcmah|O=uHA82xgoxQWn$6g)`l#TKD$Qh#cDvU-q~y6YxuQd!xg*-Um**CaH_&>w24 z1WiaoTB8UB#exTqJ$dlr4~Pc|s0TfWC&@+VO{&eoINwgvlp>V5%)alNH*eniw*FU! zOJKqjq~S7L1qHfcKzZ&Rz2%%2KmZT)Gvu{dH4wcJhDZ0vwwR@pYHy$?M72#)(*zjY z?RqTviv!VNLPU03QJP@EC1c2FL=S55~D& zBE3G=uZI{Dn*cWc9`OT^^=*K9zXf!cf$u2qTmbrpW3mfiZU;#yi=8@@_Z0AB9B>Zc zc=G_9H!wzU71^F=#0gO1_*MZ(V@U#Tfxi7vJ_NWOfI6Q9JO}`}#BG4P0atZUhVo>< z#el5<0RWE0BLJ+w5%4>ZX@C;-fIRevxE62+;I9B2!}9?L0-gilT>BZ|Isn$61*iis z1|b~e9DoB1 z0Wj7F0mcD$0Wgoy?-T%H)Q;7DP~upl?gs!UV=(~EC&YXJ1=tnf1z=kSurC1P>;UWz z!0~Vc&IaIkpzaD_DPUWG2Y~%zfG8jdKtIv{{ti$dVgX=BKmxEQAOt`gZ2$p)vQ)q! z0QB#2KpcQ`1> 4WX!}k8ocB0qFiyyizV8FTc%eS*i!s62*8zS2)}b69;Cz4tz!+j13u8VN z$`1h|0Aq#n(Pk{S2jHC98gMJ%3ILAn{(xc!Xjd1Q$KCj1%zp&HF-E^I7KZ_r08RmL z0F2+kfHA-$034IM0fzz}02~QGJ23`GqhH8-6aZtU0iYkw{r24Z9h5j ;8fPL=;+ywXy;8DP-fEPi&1SQUk0$>Dy zF}e%z1mHx#3INK#8Gv@6Jve6L0EmyFAMy_L!LHdK4Vz3r|0Kx8`1E}}bSM*ebYoD? z1N>kE>;T6A%xT>@KobBQSB%+e0LE<^lsGpqZix8+j2*@U0e#zZ0%M2u5Yx}%1~#|j zb1an002sIH0k;5fY#G4L0F2Gk0G#`A0FL*ofX4x!f(>RUF9o13L>_?czn?(+TzekS zXb;*5cH_AWc918|iO7rfg8-EIF5oW!%$36dIOi?^JrG|!*AeFc?$SR5>!tMp>nvPr zbph8m23U`JbbG_PECBbaaecKua9#CHKzX3!vo#v7(-7CU=Xn|I*av{~qy_K*giaZR z@ ZVa67{|aK%3E@ z-2iyKg7VOIl(Q`WWn+mppqfQ S(C|LpUf&>%ayJLy z`!>qKW30!#MBO_AmIB;>tpLco89)UfAIin^GV-AwEKv^Hg6IE(0BAFwUv>e^0iX<| z(N_f8iM&{cyvTzk>Nf(=-z@;hhd!e|tiyBt0AMBn->>EZupZyPkQePk`Lh5h2j!vd z7%S9)_M%R-8T+CP)H4h~IST C&t$XK=}wff1_TEAq&9v{?0M#MO)BU z^v?l68_*}T0sG(>Vm$B|^`f3GupVV&ISqjE*d2f}P!Glf?Zx<^4%CG{;J6?U`h;yL z7kxmzXcNlBcp(qQ4s~N+9IvilC 5&Jnaf2|yWW3(7?r`LQp^(V-3+fO@;JLmt$D z$KAM~t>`!UgYt&}*v C_-f&2)Zqi8?I68W(m>(M5RGYi1J`vNcy zsKXAxdYnsWb5}pwh;di~=#Kj$pwU+xKlB6bLm&_8umUhn7$3|>92bns3_u=$vBNRO zoIp9~n-hSxAdR*oKhhXi^c(Hy#=0AK^a1@ton2lEXv}@|1NC4Z)MEo65B5QuQCD}Y zQ3uvzJH`U*am *^L(I3={H1aJ4pib0 `Os#J4aN#>LSD25+tE(619jpt+J(G(0gxAUV~O!YnMfmlFCYX!T__7 y9PbnFFA%?wBBraYvhx#@IRlXgk^_1JJ%00Og>cc#M7`FVd(RV~28v0XTns z0OUveu|(i_VSgTg^9SumKV}244P_$Tm5VlFyio@FiTyCH=p*u>z33mt3vERpFU} }SN3qW3M!{a3Yl#BM^c%e?LL!I4ui(}HA zJ4j `Gaj37vw{`PzS~n`Op`fL#PkuANIpIqt6%v exc3X_+p9nU_JVV{HO=#H;xI?=m*v#aI6r! z191E>Hpq)S7_06$q7KxFc4NLS0ia)K8@8cNEYbgNJ=%{njuEz@eDndw6J? -0P=Tv(NC0X1Rx*A5aWhEqHpL2>O|Yo zPP7l(&^F{nTZaJXM|a#=peaB%7Tr9+erO-+#Cd@2I1jty*p-L6&=#ymoAKO$ebHu& zT?~Le;FuyG%0m0G4sGntX`FMY8};G1p&X1c+Q|d34d+ughUf#@gFd4S8i4X}zTq*( zodcj9U7r>Mjq=e}tncO(%Ee;~0Q(?6%E9(-9I! Odzo-*+y8%l9*oHFDZtREg z>;mmUc^DJaf&HcdupaXg?M7cv7utmKQ6Bc+1Az0TJBQIe?B@VrJH{Aeh<2f03;_K^ zdB}_L+qF~9gcAEu0F;URXe*8xjuHBV?OokyC)zX{fbx(Bf%>o?+JW_`4{gR0`O!!8 z3Fj{QjX;@rj4{G~C>!I9`jAFjaE$E$^cCx{4S_mPCbnaIunpse=R4Gi^A_7t5BiJs zSho=Xbs|67gEnA49CNJ0u|r-wM!RrMVLWj>Q4Y#OU$GsJyZ$4M@k;?P?${UQ;kf{1 zpe@q@SdaQKzNiQFW86@l1%Uiro0kBMGP?3`EO7iVE?qycFZMxSupP%0 o=I(gLYRtY7pk z@yUaR9xR=6_St*wwf8>X|GK17)ofO^`(E79L$`XC0-ES?cN)k;mm0+AliAQBr@eJD zXb<_xzaEIAQ(k*BwBnV6cE0Z2+0&zL=fYff1F?E6j?T_XPJMb9*k1(n;Pd^_pvQdh zLu1#n$43wQ(}6g(nJ+&&dXHb9_ UyH5ghz;Qb4&A?oIgLviJ4`@`Yb7w&h zKU{oqS>4N_-@g&C%>}gCtMOXUxtkkaT;8p>?@YH<|Mb&Y%xu7RHbCu-@0;+3K~C>Q ztlIE+SM1)>R3M%{aMymDJ>XHlJq~kK1CE&h=TC#*3iO1Je0apPcjl^B^19cb-9Uae z+W9o+FNVE5;P*j~Xym7FW{2O5@OXcG o@Q&R3c9sJ^c)T6+!N;ag-X1<@b zaQ1F+%cmxqP6B-R@ofic!%HVkdi|RLjoy%6;Zmnq`Q e@Wa_&uVl|hESnj%U-apjw?mWO( 0 z?5m3}A1fcTpjR*H;b*p=1$v>ce7!Ng^6Nc4VrcR9 Km}f zs}H{Qt$^Qd;I~8%_Jg?qj~r^JRek!k9^lYN`f0+;r*F%ct3jq zZngJshumi24v&2J>7(7u=%z J@t+bgZ4%&p4P=r&Fp%^juQtz@IKW_ z-)_M7;jnIHrBjUh)Fswg{yTv_&}(k) }_xwTs zto+n1zdg>?z?+ocH*W^VfgIkHe&D+ixYxhC!FjM3@Hq(dTd&;9<&A9za;Z@-< khYk^3W)!**ME-kJDa_xb;K4H;&^@Fco|^&~y3J;v4df&?yIByk_nmj~>|T>0v;( zcysJ;nY?t1oew%++Po3>a^d4s4!x4w%(nt{;8Z6qdM+oM7~DAc(M^-O)#*+?^|_}9 z$6BB^x#ZptjshC>+{zB(t_Qyi#P^;ab<{^c 5#NqI!^w^#c z(1}Yw^cRP|u+cFQh^OBwx7z0dJ(1tc*~O_@?(=}(bRds!!5Q`fb1`FmW0RYoyjJ~j z?+&MXP!Ahz^3ZQD&O4_|57c-!U>Cm|sLjl1HW#aY(5|+f;50CI9QZ#Ba9#vB }CUi8c`D{`Z58K%Czcz0)UJ^bn8y>UJlm{&Y^hb-vDgy*K{+W&`y-2+Bv# zX`lx<`O^!2lk|)a@TnGa(L3v2;5!j3k9yRi26^ZZPrttCBYjpis(m`pN4-(ke!#9z z@_rhWLk!J_fxUjW=W4(!KOVEwXJ?%FeiGouM+eQ~+r#$uWVmzY(^`DfY7wtC`Sb}V zA8*0?`64(D)U+GOK`-bVf8VcF%qM|(T5+ +J zPak#ywa_n*n%S+m_|fKG{>{LfSqtQ4=ZoLGo$;-)3@xJtg z9`n=--++7lqmA7<8@R7t{o+fz-xa!Oq5b1PZ{@&GD c_D$ZK};+UqZPdw^e$&0gMeiq+TspncLe zvDIQvuQTq)0bRK0z)zF@@KK{2w2Jdi#oY;TsabyYc`tbA(^Gorz<(S(2;}e{7l*xm z%4hY)aN*P=JSTzN-p*0L2RDDUnlWyg;5;xhKJ?4&t+Z#p6}_WF4nCW~qd>enwX6s3 z*8=(V)O*M04bg&gA@DoUcV$(NI`oT=Ug|5py+9Aup+D`bTGS+;-so+6ypz2-(&tTd zp581y^qY}d4g%Wjz4yKYK746WpBcz64%Dqi{NDunP7^= 5lz;SZSqC z-KT+i@asjPNBW>|@Il~CeKc+de8e^%&T7?LZ%uu2&jorW)}C(q^ryAq^G(s^ZL~L? zu~_{uPr39+FL#Gkyf}R_Cwav XAqPy(4~f z>+$D-H{#y=aJL!gFD-cW#(Vl`IJ4=G+T`8~*wlk-Kaf}b-f{c?tL%3IbC4egJ-FG- zg`IwN& j|2RAr3aIN9`L6{{j}qdSN-&QYwDZ{ %sj%AKDXlw4DXsTzfK+{pA2BfA7g&jX34h_ume_3~=M%N54LO6C4M6t9H56NwfLl z!^7XXcWmm@i!qjSR)>Dzq(we?#XGA{&&*JbxM;yAR$uhTck9hv5A@7x-u(G_d-CC= z_hujmt#s>?`sLx<-m&2VGhwHL4{p#0c6oj4W_=}4)3w06_%P5rx*MlI+X0>8_}jNv zY>R`9U7z%V7CGsG*1@h1=BXyN-3|5v^HM)PI`oy@JjAR8^63v<_X0c{fqHP$<(rxg z)M~F6`p=gZ`t(wb@;jTC9;lD5&XEqj`++{oua9b_S3a}i_gll>+}LoaMb7s9G<*D8 z0bZJDTMzWB@80ZuYreY=0=4;l)JH2`u%}1t&amo_Gp%OZw@5QS^}3Toy|mFv6MnT? z`RF@;y;298H|&h>F9vLQPXaxJ=1}iNa3h!sd^h6t&Rpfy2YdCZQ%}Xo!IwsJvRALX zwBYn!
)mv{yJ^0jYRV!`mbm)WH+#LkorMS*c9H?C^(AD0s%a2DLbctsNJT&SR zZY!;H_8u2ry`aNcZE6rNA5OEhR-^vv4IX WWpB#%1(c3`G*$|E0b_WbR|(=5iGO|EK_A1}Qh2mOxZp^x3%R|DGg=lwuW zyf?kQI-KQD%Vw}1sG&2FTm5wFfq99|Te$SkvOW&}QSe8B-srV>d|w9c UcxsnlZh58yG4ip^1nlCc0($Mc z;*gJ~`mwoZm&>Xz;wJ<7aENg)ml&|;XHSQFymYibw2QYwwKtboHgk3_UhR0r$-!5T z?7dt5@&SEf&C%Jc*8;zLX5(AZduMgx6^~Co>q_A5ECzDQ;Z1A>`0 F$mw@uegI!*Yi-`8{X3)8y|>Kn8y$17^-e9DL3#PH0YB@lV4RaCHuv>m!&yK5 zDo1nES3Ysxb@SzX>3`;|uV(|^&w3g>7;rv1-^t#Y?{RP#Tm)=j?@k~6R(Rj?`yDU` z`upbfXFuTQy*XbV_7hq8nT4AAwytDvuJlg^H0YuE@dI{w%vQZ>m*4y6M~AltZwKDo zMu1zK{`9>)&Ys540>7opf&6&r#_Rpzky}3R(4EyCJ}VvIEUv3KJ>nxTyZFY@seU!n z$`3cYz4xnkP!2u8g?nu8UMwxXHNe4NZRN0n829e%_0iiGzZ%fu&a8Y_V&q>R&g^QT z-@DL5{kk^5J?M%1ej{?KSMB8!$Jh7H7x-DdAvMtB%uoGRze(=AbLWCyT+V%`wCayL zwOVP$$&QC6ch=6K{p*~>=`HT=^-a&jva5$)dv)`ZM?Ssgk3&zzu&KrG86WlNjrh*P z8^)zB{OX~T4<0#y-u8( 3-_&0yu9rL?PBEj#%WLo zA2E7i=2myl25R6>JHCD^c<}0fHQ*|T@4xl-{kqey_UPJhrd=$kAD{g8{HKOhf85dP zjs`Kc(bIR!N4>R8W-pHzvHawB&!z{@1nr^z*n3ZU&rdJtF&}r%bef&L82)Vf!nfbK zJ7;!#Z$b>vC8qK6i7N-YJs-L4d$ziFN29YoS@qir)oQQ*e0`tvveARb?}oW;2Ksj% zTm-wpz2RJ&mEF7aHVy~uWCibLKX@GQgY951I0*QNk@G>|&ffyx7e0RC*`4{Bw>W$E ze2;^pfUi5gVx0L~dnZOt>sG)|OgXLYX%&N?CjF?dyMDVk#PG4Yt1hv0vC*VYc-&h- z9p&JsFY>rk7tZp^!QZ|7&i3AJ{mLgtYi4tgv$^!8zWlsFdfivQy*s(+Itlow1E1XO zyFE_5I~`X1{MBLK@0%p3kz!!w=-dW0lvP74Ye$^|fK8MGw5Uz7uQlV)nS))21#f zyb!z;_=dj; XF@|L+H{1V0F#4d} g{U(Mj+p- zBRgMrZ1Pr%b8U3p2 T_?!#RhV8kF)-K z+3B(R?H9+V+VzM|XS}quKQxMAZ?Em?;#Yq@IJ?SM9{lQ|MXm1mu}=r(5O42}zkB;a zZ0A~=RSaKubgRR@dFw0J)!`0*^){w8<8$BI?Qys-msQWqtl!jlJ%}CHy7s=`SHG*S zeh=z1te3tM+oyrI>s@)f{_b1Z!0g@mn`VXEfjQG>4)$*bt(8yTjea@1xA(2}Z=}DU z<|ZEwZ&DmT+*W-kKR+?<*yv*uBbL3n yE{eKaeY zTEAEt mh;6Oz z#P&`t?!|YGeC3jt272a#&EOz-9N<+mUwrBlLsREr1v&YRVJ0-_w^%c=@)^hIm73`6 z>fMg_`t04#2WCJsA9lSFORqh+Yk!^jtF< (h4CpYbV4{BxaS$);d=8g_r z_EsFAMk|~At=S4{GV7k}+dI0%;l~Z$4EsjV`FjJntoT4LLA)N~@b{I?_haR!hQ^r( zJ70aacQ+Z}aPK$B?*h=m2QNQ9Y>jm%fAh&@&(EEIH^^n*-xxXCFPyk>;^E`pA9Ulf zmkU~l-ZX|*vErb4 1Q&+yZ*5Nrf`EY{igic9{9KrH^VfL?Kn!Ac+ped1szpwGR$cE_3oe>A_ MzO{bL?` z@C`s?^j7WeaJ$okD?#mSIIU&{-f+*jXu(xKckQV;(r4Ajg+R@6nu++v*{=os^wR1Z zQ?I;u)GU`A_U(aKz2~d1_P!M>z2-@ScS0L`{q_I7fDV1Y&xV_={%q! pqg zSMBtw!8gh7+&93V9k(2PFLLW==cO(-b+u1?+jAP}QnUA|r)s%9tm hO;{{-hjNj!FI3{;IMWM%h|*2z@3>nd*9~tao`S@KAIbt#~9uxsLvZJ z%!i$CS9|wQ#y{!PGhL0fc27t5`d@$FLAm+3Ge0(Z)X}|o?~l!!a8|pV;+mTs#EbQA zta#-szqiS*KK}O5ywxI?w~F5$#PmCEZuerVqdNJijeU&jY#bi8)+7(D?B(t6ADwDp z=VPs(mCc*)H`Kbz 4DY0uTm4tUu4sF!Uz7{{C8csBfLV6VSbZPxl*@pN_XT)*mT zOyk(`gFF3kXZ3eQKlEik(D&`2y= rxa_SX-zxD2% z&46$3-Su6%Gj}?xS#I}zH}p&deEWvQIMX8*ryTa+{&Ju``n(ly%i3?y-FpH4_EryQ zrbW-ZkG@HJzI4eqA9#;?CuS+o7rEWrv(ZR5u+0TD$uFLLA&|$}o0f+bcXZGx&zRTV zJ%6>+AeT5jWS7sI_l6b&{CdDoeEV@LdpXNTw?5$<$I!xtzi&nkxxh@s$*Deh#<}pj zYplEWPkwsjfb!b&!EfIl(9ECRT7UWZv>*2R&90B)_^t+Kf!iG(tJ-L>m(L2cI#-YP zAg{Iec }i%8Xr|qJa^}-|@v!r0Pu;Ppv3+&ty{e6kzZu~I zy^vQNAGLTt`qDR`20rbn{`A|nsu{P{nV&OGx!7x?rSN9y<0FSyJ+Xfh`1tenmhJgk z)vYf1=(qY!wz^jr&3eoBtH7I9dw*MT_y*SkzhCRaUN7~7HvLjd^~=SMV+^@&1Ztpn zBk)_X709s|_||;~AP3OYnV2_UGcb$(Hqb{8Za#QvtxZk1*!Khc>UU3zwezCio!RQ2 zy&kc%b*0NaZER+tU;2;J*&NN7k261gl1o4MwQv0F`Qfm~?`&T7bchk_-YQPN-Ko(% z+s}gPme<}q@R@^|i`Osr*8+L;R)6$lIS{9>H1SjSdZ6bt9Ry~gcKuxm%!N(gw*oeN zY+naB +_`cC)N06#lku&<34eXIr?_Ud!zcTp}jT<+QN^T9U}Ob6xETXXR~`^`9;gZ^5* z2R7&bU*O_96{tZk<@9e4E9hHiL7$nJp?uzxzB_lX9yYUNb0-fjvr(VA)hnmHwR`vS zG^bvfh2FE{VsqBF&0sg!9^ehi-S2ue|Gn7X%$eRV^Z&D#UR^rNULC=A{>``lUvAq^ zz4P6 g;`qHwWLBe&Uf&O;-Xu-g4g) zANt(S2hR31+zG1hD0{!z`03C$di5yt__l7f^Yixf21je7N34FxFJ4|fHD~^2L+g5= z25%PB -r+I_u{2rh>z%3XdY~pX;L{7Sa?_4q9DccJ zkw<-f2RPN`PH*JpPshVR-D2>59;g-c_)ee?bhfvCYw_`SM;HD2uWs`ZCk6+&?<~}y z$9%j6wdgG_@6lQQ!+;&9zPXq8Zh+g|)x(!Ib8_Z~pD(R)%R{Fae0tH@@T*;~nv;)Q z`Xs*`c Ae%k zgO3ig)_;Ed@m9<2?8Ta^Ro!Y5tJk!ui+^)i)umtV_1;_A58Tn9ck05WPwLY{Z-+K_ zYH=@*y*c3Pd$V`1_QQY<`qbo3J$Ua0=3JfTq)u}(3) U@f5ORv@pwx>sM{pR+t_sXj64b!S$6AKsez&> gP|F`td&s?gyRQdiHv5#(ZeP z>mBKr8r6In$fFnh=yvZ+GadQ@N5ND;f8SVtzvS8u`UYCJKB<$vZ_OQ>UeF`gNuc(f zU_VeB`(&V2b{zc0*_(-)Xa;ug1-E#6eN&fu*~F~`I2VS!cg8NxEY sk~y=nE}r$ztuOq^A3^^R7))+>QGu4iK96N}@cfChFt<(7*U{?`M2p#ztDIq1Vl zx7^PB?H>j>aH<)HGY&Oa#k!vi@ZmlT9t^AeH0r6G-Ysr^IPkf{&(4 pv7&-`)U z4BV;zUSNK5)1p_n_^?k0X74?(24kZUh!{oX!ab#(@EsBt;yoaDF~s70(ecbkFl(iy)!-Tc9OFcbF^fj+5iGSFk% zyfMD=(`MZd)F+>NdgxN09?C7oy;^b6OPjf pKni12dB6VIU9vdO^o_ zKwszBI&rEAr#U;DhupivS>57r%8!eFestRNr(19ET?8~A1#;og3%+{FSG{^+X6o+D zX%=%FtOs(N2RmPL*L!;L*z2J_IjcoHe|LIB7rVLR!%GW3pjFILpoeOpZ6jC>@VmPk z(C JBGOX5CukG&8)uG2Hg%q%NB6 ztI^7)2K~mf6zJP-fJ2-d?(yl1w CU3;N?D^2=8^Ngt-v?h>aGSTYm5nBT8v%`AzaKmb z)Uy_N%l7n)*X_Y(Jq)b+$`4Qd`+LFXIM@r+t7rUi9tHf^>}e?PX7;`lT74haf)@hy zo74Dv(KmLphs(ju;KhJ_HsB-XN^m3K_jJ&E{_fq$$A3D&VRf#Z7M#{ z%+`JqykQ^;!!(>Rw#^*v5M^)H8F}53@I4I=vP3^iJ=*0cUai^=@i7yH~G! zv9#l}w%6?Z*t`qx2bUN&?~biGt^C>AFYg=lQ9gR*F#{ac(L8F=Uv=WQ)+XL8tT@HH z$LE`n*ITFGs$SsZPCwLSEx#PVP8-x_y2HtXAgJCLur{PyTO8$a)%edxTL+k3uz^b!ZVx6G!`=D i$R=xI{Iv1=D`-QCL#cy#~`S=agFE!D?r}6dM zkBx?%0N+8dHmu%^IPb+OryO*N!DoJSuLN`hPI+h-t4Y*wjr6f2(+_@64)xF}0b$oHVJ09(nX%OlM(!e4J_EV;+aWac~l}x88-f z S`dSyQH0exb;Cq8OtceYoDRn7vBdpYFCUk*Ov#Z`;5dhzI+ ze)jE`OYY_^FRgm%Y+rq1YFpPhgQCFiFSwT%H}DmwR;x39 z`n?T4?6?|>lU*P9mq%~(j~2ep-Q#NwV(f9+^R=hX`^9HZ2d#Zi`YUH^6(e8YkG(gk zhrSJdJHcAO$6N9p%4hXm+l#?{8sL!M_n>|;-c`SMdT7)yc6$0A)uLB8>W^10HhXpB zwO1dF`qcNs&SvGe6^O4d9`??1K6^C)Euh!V`qpn$-uC)-_P}>J&?~xe`EJxRH|*8v z?dyrY>bcsieMj>7P4bTU@O7rU-xgnS J?`=C2Xed@s1?S0XT5tE=#_Ug6VTKd%Y$E^t@7E+u^P}|PVU&u z73g7euhzbMcC(cS&tkv_)Y-k-`Laz6dv|Q*FE6c)qkS?ErzSb%630jG@~B0x-OJ&P zG^hUSm6_;KXKj^FY~MBp zVI%OC#N+%dSRc;bnfJ6kobC1NUeFrM)O*6c63EYoe)ZEVC!JP3kk=i4I?UD{SNeD9 z9|wB*N5S6@{$22C;62On4}<>@pMMf;2LB?^Cq2&HrQgYlm)^5rE^uf6uY=!<&%et0 zcZ0ti{GH&F;HlXEF!*o7nmt?bXSFx(Pg^hj@rd8v9O(Yf|JVMf#s9zVzVl6ZmIjP! z5qD`S_K$;K%m2=!_mc0#|0d-n^Z0|{_k;f$EROIOvNt=w3EqyrdS~;)c_r&YAn#g` z-d{4q^!XAlZ~A8T=5-!i3cefsui(D~KM(#S;QLX)uewL)OaD9^za3Z$`u}P0H-oQ& z)b;ItI6w8@!}T9yyOvdd|5NZUga0v{e>dy@4#M};KL{TF8R5Iz3iJRLf`eiIYF4#) z|L+9rf&0ASZ|!=e&liEddXswltAMUog419%@K#gnx4%n%oBnO^D4@5qdOYm&eSG`x z#@DjqF=sPB3~mSb%+ed7%O1xwf!~eVUd&$3TY+zTJAE`a^@ybvmwcJ|w{dv93%?KY zTAvHl=gwQeZ|{43AyEI-0Ec@vkkdP}r-eQ}6|V=hdbjN0Z99vj`+6`FTnnBJ+=;mo z+zk3#XAg3S0lu}@S59_ZIPAX{$lW}0$)OH*{ia`xJ2v~CXR{Yu)CUJ&IeVu*+VHv4 z*Y<@*evQL38FY2moH!=}F*v3JzPQ=cg SDSi13O)_^e-YsGcWX6R3w{##PCpK4kV6dngW#*+ z7r{Hj+3)UIuo!Fv$HRUntA7ux@gVqdKnrevqvUk%UTriv+nWj9W^casa^d91N8RrR z?)6uF;B21! s{m88j zU{lA#0H0XCV%vwK?CEL`t^J+RC!DnD1ugVfmmJ=K_aTpa`Mn4E^yDm WllnXS(!XFB`L-J*{5{D}j5p(jrz4yzKfVb}x{J4&3goc-Xyv{$kau56 z$(~ +5g7%bV{qQE$>kQS1i$*pv z>ae
*VA&iw+Fh#yfmC~;Z~>E z=L7Y76Ju^Yq6OquhnzUA?)l?VgLA)4GpEPz0vkUu@>RQh&hGWWym8 E91}O;f)|XYp)&>40Ag z+|fxdZhIX1LYK3=?)w``W3{M7f8Gqr%TJu&J~nyP aX@4oeC5A4X HbH9|m&pWuwoTZs@)}k&i}xVw;~$yu98k9i5l_cyKh=Z1!UH z3y(XoeA@?{^|#`8$4895vt1A9!DsgBbe12dSasXG8*}nAJ2v C51X^z zS({59XDdyeLG!7>ULEe` 5X_dcyx{-Z*EpIi^?@f;Vap7#NUhs2n z4)J{0TkGBI_XBgF+i#D)m}P$({ARdkKMQDdM?=2@{;i`u)C+Z)uX*1L@S6d<)w{;) z8^Far?vXe?blZzj$1{PN%~U>GtZe*n;1XvpY6f@o=q-J0-c^0v(WyQ)>z{wO;O8r* zI&kZwJ9*uy0f&{H4;{4CzLh;L-=5zf_0&f_`Y1O|VxTqJ%OhWbiyaT`YLmZO%7IV+ z^^UJP=ojy-5Aw)Q_sp & 7F7R#m|orhTbX3aK!yJGm# zp x8^^u2O{`T&1;Io(0UE}LV8;GT;yzX(+27L6^Jr1*BbH`U5R(;i< z-N2jcY}n;^HrNjMx)U#t+VmLM%~?$KwH|Y|m%n|s=dVV5=AstoYUZmSojV_S+2DJD zSbpxjIrsE*{=Q-MYV3U5TXUnAzZ$&j`qJ#a{Zc>OFdcZ`^68N{D|n0T8!piz4eJ{9tWAy8f)mssZ8=pDQtRDC6t+Sqa8~AbR9c_5&z(X59 z_PszKP6PU#%>x%*_THfW$*mW1+UvLe$){)XdGGc#;SlHhumU}`>8-bb!&}4Yz2P@o z9Ju%aKmB8~inSMq)9QQ_JP7F1WBT>uZeR}k!+v)-f1FiM-JJz`@i@@qpATof61NlB z gBc2BJ@ZTHGW@#3Ehq(38`bHprBfux$*MYYso{xLH-jz38eJ9!f zEbwmZ+4<0EPlMQ30`a)qd$aD?_|agc-`?tOJupw)<|)=2Waker27Kkf&-QA-uRh+B zy*DK9C&9fyEv?Oa^PPCF?5_`dx%7fx`Ec0F*Bb0;TpMhy<^7y-h}VDj`lS{e?E2wt zx8~=EJw5jWar}JyV$9O_<=dzx`Rm8_+5mgC{CXg#@80@Wpe{L`y-$7Ce=&5HUq5^c z>Z8e-W;v$<@xFUKyc38w4|e|EqrJX)cWQNp=K}q})xL<4%bniQBEPx*MxY+PF9rJJ z&FYPM^u{-Vk49Rb56pp&vtEkFYyV~NY=GxxpeLsR4PqV+XEUWqezEFdyA`NO|9wX| z<&lGr8v4D_rT6Y}xD&_D24*xFn1|m0e$9syKaE!Se!x#``{gVrh_R}rziD~|dSh14 z4R|&yu)i402IlIXE^B?B$=*AZ-=5t&aAq&WgL|B{Tk+2X;%Xnqn+G4c>({eY3?KL2 zVr}d+sRJJ$@ier*&g!Ab%Gdj$m7dpv#em)0@~+3g`+l$y==a0mgJ6BYT2}L44Hg3P z6u%jGLu}rTGhZCu(R?r$u+xLX+1*m$9=sWGG`a_Sv-ZaL7V??5d}`%`?|Ps<-<&?^ zkGgzY`lN4q!B1ash}T;_(*e8v)nRo%8}RjxoyFtw*7(cGANQ4DB9PbF9DNhwoNJpI zY;xfTy2a5g#-29$*vybkZESqh<#(=e_-NuUmledYn}Kt==@Re0J!?Li#K 6(b&fY_7W7kV79j(jXT6Sl=?UPt_ zidTEV$6kM|-V-0ZV*Pu<`*5bKe2r^v`S{8szy9g3zuSC$d*bms+gH1@`kd*|V>Y?G z4Y4%I(N!+~dLs{wVmcQ*V%hcGEZwo=1^)EWIv1!#kHqMOl`g(&5^F7w8q|x&$}WaK zZS4sTc6C%E8-Mk*FMQZQJPqC&UDfQZsY^WIYrpZdhK20$G|pbEKH#>u)}NnTIH33S zvHIOnr+(3Gmg2lqf2U~?Yft-fK%+Ow&R2eY hK4QH^I`qSyc3Q1`^+h~iXMJ%$=5dc(eRSa0yU9SD zT %n%g9Qe(#>V>?%32zKHU1F>tmp%gRdS~AFoPj=|!<_N21m=!g z57=qzD$e)ko ?$ocwYI7E^2kHc~!cPx<;;hYsOMhwAtH#PH*8AblhZbCX#e388+k5Xc%E_N5 z9PYdYF>KCa=&{Pr&t49`)#;m*1E)K&G_X6Hq1e_?4gA>oG*&(~@tsY3sMmVu%%7h= zSo>WzpBnj^1s;0!q`Jj9^HB%8_h;6)<%iB}yyq{LpSbpk4!PPpXL~mJaOtlzoj7qh zyQ5`Xi@WNOgAZSQWLKlNp!f37(%$HYGrr~$?<}5vYxCRVlSd!$`;C^*>WqgEUi;3> z+pBIiHM$qu??((xX4t*oE;iia 0e^K& z27K^}r$L-K^JO!u{$9{<7~nb! b)LTD}MRR7HH5Py69~` z%dJjtYAImb3a$;n34G)OXFOKlTy?fzy;m &;3oCoIB^W(v`I@nLM zZU>J7{&xd5SP8`0oBKs@5b$H?yB?T {n!%j`{O$$pCqX%XJ$pKxak|69e=pb! z+`9vBseb#}%VqTzj)uKH%l*^A#!mZupbv6*BjViA&tE {@x2cG^_>cYcJl<-dj_@8r5vyvwUg;HgQ&VdtCI` 4o6Uz<1=^(nnls;%m QlL(jdm!+~fqkP@5U^ zQ%|+XX)i{eeqab+)>fgOA?RA`TCJesbzBA7B>?{BX7gYjdfCFYt3V7x&KcL2E9DvtFsqp50y# z+EXhn)qs1fuX*Kir(f#8h0`ANj4!)=--$T0@12!RZ}`gVjH`XCPP}5xz4`dkXXS65 z58Si!#m%lac;%P~@Ts3Kc%z*epS}e#_PEN2TMUlA2di^?f&;|2ht<^DX!0FZyR*M{ zR eO{B+@JJ@)cD^Y^~Qw!WUlb%y<| zXb!x6KlI7jTIiu`Y!k=sdy*fodmQZ#I~$0Xzi+=WY~G18(5#l$FHieo&j%OS(_odu zeSg=+e)RZ0`}@I1ZF2E52Ylk2-@P}6Uw+@(<$x}CIQUxmc$;d`JDmE#mo7fl?%tgh zue;8UeL27@hcix~u|1K`xpncwueW^rw-rC0&O^L>e(zQS=k`zi_G} 0D^{_tUI|K#NF8`3k}`s^N>OFq8=&T`s|<71{cPXadkOTl44gP85$ zy}<8 !Dc|Kp7}c}RuB2;A^+RKaWEI?&w3y)Ki_h{aq() ~C=BY;S)O(Nz=+STY{O!fcXU2HVRh*jC@4i0X8}Q@fP1w^)KmYMr zZrt*KoVer{-~Nc_D|UQejkJi@8~1#{SuF7F-0a=S(RIAXAy;*&ou9Yk9pU36FF$&n zaqy{LXZKzCm hJEoszJmT=$ Km2A znclv~=BS-MwTh*|D!0`er={O08($n&Fo*t)agP@#-K~Rvd+rXuSS$ao;`@8u`ta~6 zuO76|Y L5rYf(nh8GK_VSCX zjh|lP$Ely `e(`sS?X^}(+$9JuWJ&C#j`G1Wka82P guQSl04`8nkozHsqd~mD|t9txf&>XxmI{2$ePTcZ$cC_+!7VCZ?;Cm8? z<0D7)>7jf6{;l2r{6HIjTzva3*jhh3Kk@c{Q~BKrXw^sl{@pGwe*NJu5BRsV9PKCn z_HQS9{GEwAZ;?%me?Q{I*S_+tCiis7s}FRm*UZJRn}dA%CJyNJR`|45_6Gs}&dE%0 zsny;c9ei=i<=x0Zr{1vf=chmPiCYZZ*|Y1%QXmeWoK~Q#?@qk`EG`G$&WRp=&V9SM zI#1kk`p-|a$|>F|jvr3{4wFM(+8R%f-|hASw|G7v25{kTKj?99mSVgaJ#J6>`!CKt zU;D21-VGgkXwOf-^iNK{TLE7+IQ!3yddAl`#Kzwl4__-C_UbZQ{gA8m Q+{`9ZQlCe2>SE 2;OQ#rX_23sTC;#@mdU|L6t%F~4v=+H>u(LUj^ULFo zk3NFgSmmg0dm8wwRg60}`K@Z;M `>6KHj2H}j1^ z&+(oH@=XS82SMM4oN}m3AH2n_Vb5Pr_5 ;+Q!&%M` zgO3CHXv47`&`jrA@KxZBPPzH)1ZM#rZ(NVR3629b?*)1z=bsC3(uqUg QCk3HXUoO8L|_pZKH_v1J-kxveqta9*Umv )&lS ztBtRB=3R+puTH+@^hVv`P^* Z{Cc}-TSit zA~+AsdpBVB2E8Tk61+jbC;a(%BW%{KVZWYL%u>LYpWJMHv+Dd|;I~Ara{(LOi-GUd zo2JEH4ea#1Hk{S4GQhjU 2p^u^*Fyi*l6iI?`7Y3 z{y5n9;N)ko?hk^ugVuqUosYWk*z=)_X7*~ukBhC^#LMIN#oj$k4y$_WV;;EpzZLLv zCl^iXejHQ}n>Xhy2A4B`8t7y99=$zw?+&lI@o~?-JnV7k4<2{&mq)&hKt6uBfFGL} z_xSa$wRx|;yVnB0AMkA8?Y%z0+juGPF4 }sUb3i#;4Bi5N8U2@{3jV^xmrArU^ 1MK4T$tpKqob6X lFLa#lZ1wZ9YCgKtsq $O%lt84nBPC{hg?v9*p}%BObB#YN>7 Up|e&0nJ&vJp9M+ z-gia^=$pQ{*E@0ker*ru&8)YA`N6iE6=>nJKAiPxEqE{B>mH{$@$>yG1bmKyxnMb1 z3RVL;%~riQK@9kP(SP^8OLt<}tlr@5;fxRH5bJI$Xsq?!C(iWp)n|2xv6r_TY-#}< z?x25X_ ASPFHfOo HD>L$)u;B#0&f@LWQGIga&|CSO z@q)fNi>(%Zv^djEi<+I?Ra5P_`CF^oyJDk_7CGp_gM%HPSvZ?3-qy?)+7oZPn!O|b zdLfp^-r=R8I`Q`{(B!`T;ODHDa(zE&T=VmR_QSn+YiscLhnD)vqc*)&yPDnUv3E|Z zZ;Bs#--bPZ^|jY@*jw8}ytr`Cq3`bSszDF^4peJv;fJffa>>b$PwSzFjc#@0 M$5&40zE|(GabnDr4&OO0XT9VDG~f}>&)-)1?DeL7 z$L0Ow5YyEhn%_61mi81!=PmzMfKRLyydiOVD90zk{Q!?VINbTSrN5D%2P=UY{504O z%oPuR_Vqvy)p8j4HgNmyb^>o;J~$5A&( >Uea2h*2b4j@AQ*C-1Y0)ZvhXzW4PnzPQC8*$y(j`-RtvC&>E|ouO7Pp z-N0P&elO5ZerBki=>QkFgOflmebjsY?)8gJU)iPt+ %m;G7^qjBz9atc1~jV&2Y<0Ef&Q6;d#l{^vU_W|*zMKoJ5rZD zKX*9mD^860xp&tXHhY@+$%zXGK6Wv{er;IGUrx2@%|@^^K!4r|@Up4#$#prl{a`hq z!@WN8)hl`QWj&CekDU5v4s^<+$Ni4j#Q!+ZPkid4#XQs`kKCY7IIHii?9GTB?|Z?m zK#e&0vf%*!a+;GI{G8q41X|62p3dK%4^H~@Pd;_C z1DtwT9rCDQEznnYZ1nTRs~>8jQH?aoUtMz6u9wZrhFk44c?0^1 zR}QnS4sXR (U#sG^ pJk=uD^H=ovqK=Ii%DAM{6!eG6jPdndP8Z-=I51AUfX99!RodhKzG!7U$~vpzYi zPwlw$gD!oN!^(Co@Ll(v(q%6eUtz}jtKMo7uQ#*-PJQTX*l4xt1%GFdlbt50AKh}% zO@~_Wy4PR*QzJg{(BB+;wO$YOQ=RJ5AM?dUzuav6 |*uLo&KrsW}prn>aw;Ew9+7kO^iO?7}oNsjh@EQss?fNv?o^;^GiH&)-| z*8{wK#nGsKTJWl6CQ$cOz=lV>I`mJBIq`AV?^I4YJ41Qc+_&%B*~?2m=no&9mjn9w zw??yU@A!4Eb~DDot`F|i0dljchaS50)@(XU9Q>XMZUp+KNBF!C_0nQ4`quA2PCa6m zgZ8!HYJgil8sw%Qml$U~lha!R^^2hm_=x34pS 3#T5LoqTGbdkot3$Q;?t zd~$&E^}xM1qINpe*xs@Ajj2=a %>m#OJ LDzU))x@I~V%+u(RO-dwJ!@ z*S{Ig;;njTy7;edqG4mqwc9H!rJxodh_gjy-hFu=|t@Oy{&Ka* -0tbsTXA|WH?TXa zT@HKkYR6Ao!Dc@jh;v^qXYa!q^aFou(NDas*_jq=^V;iKH8{&fzgTvAaq2b?dv|oT zUVie3?|kj~xU;% 5+3>N8ceaXQ<0p1L*a_x>{!XrBug`ujK!3n{ z0d=x_cb9|30Jq @jHiCO*H7|_X1q{ zO1t{?*UFbBXZ(DudjWfA)%^VRO CZzaVb5T$42qe3vh_`C)^cdDj)EYiAyL zaLpE3w&{t(n3^~G?OAh2romCv>iVMzAdD6#4-Bljg!&yH0BvZcGGL9RYRcCyWp=%6(E!px}aaNuN z<04C!4ShPU<(HmV%@v9t8x_Mu`emE0JXpKR6^>#g!=`n^&%E)$9I|Wgu)}`kl01H7 z!8}{!jn^D`{>_&Ue$_5L^SJSAo(*RT;8k^R9LVEIw(_ZU4bCOk@}*T*tAnvXojX6q zgSCNqeR}5gwQ?$leB-5t?0GhE#veDef)8HKEb@cki_aPpGheRp)YiO~4dZ;chLU4T zEY3r%v(LP7GOmsDp---4<&tf+I2w?nH`Vve>x t>sPVxM~|(l1#QKKQ^j^VeZ1w3O?l^+E}v}j%YLnmd|R)PKyLUq zr)7)(ivgb2pDi}zL;j5MuT@7NFRp Z z+WEdU2ga-KKIERmul=t+=-2qk^qq0mQJ?OKfIoi8SrfYKuq%FYay-bBjPc^ju4}en z&@Ue1V^3V>D_(NtOY52~cIg@8OYFvtxu$Q-I_Z;VTfaEaEiU2|9~pMY;cMOX*#-8< zR7++dI1|tt4XoR8ASZO>)0&f2KgP*ehl0H7t0ntDpAT|$YD^84oZ8Y?hh*3&dH#&C zhre-tONK666^D7U^28p0=IFRKj(^2r-T4+<`6gRF&6i(3?Tymmhi$UP*r!|k_+T3c zyv74I$QN+0dqCxYZ27T=%C?$O3%3F>oDS$)FZ|iZi4Q&(gN6RO_9Y+J0=1=9$Q!fX zX9G4yg8Km;>gGZ~&N#p40yV~`Ikws2*Lt{SQ{LEP!#Oq 1qLQzW8NN9!myqeKk~^)E0Z{(>R^V55N4lW|Iuik;{sSj=owoPw(xy zHij>I@>6lS#+8raCI&Ke*p-j+FaF{#@5adw!hEeOyY%tG(|Gw2H{W!{&L7~!ZsngY zo0Yex(>Dg>*p*wpDp!26TXM$bgdKhLj|V y-`XS{qm zOK>(P7J7KHOU_vN!4((tbZ{i&S}SJj%YUuCYjg5dxxlGx8DppB**8`kDt2|jr)%7b z6Iu2P=GY_8hjHfsU9pqL4==J>a&pQiU1OCmaj0ea0dlU%uvyn^8`H|Ee%Y^e!3BT7 zfe&&8_Spn}aW!WgFM52l$1gc^?D8wG=8Vx}zhZSyGe(acw!t-Bw&X!;jLo{{)B51V zc5!2$yu9h-XdX(34SwjDCs%Rtv8KN^X3dQ85A0U_)w0bdJA6D9kTotYtvUW&SIl(S zt32Ut4y=>B)wP_N$7OKs%p2!JUt9H3IkN`(#`!F6Wt)E0qdtg(tUf(Dc;d^4+~Z`t z?7J2ty|PV*t=a>|D+ao9yec5GHo$`qKIMunzSv>kHB`N^#fEF+xKs?<;&Ci}&k}fO z-52%YZg3;G9o*^LTm6`EeK1%3d+C#ReKR08XrpAW_cG*4$5`p$cQr5u*LqZ+kCLxB z@_ZQ|)aAoijh7s~D?N;r9deh0!Ir;E{WyDaQ}%FnT_DF68~Esp@ws3nD7(+3Pu4XV zcIC(Qa$t^4z8?l;Dxde$XWtkZc8ecfINyU^vh17-*e}orb7aIq#uynk*_NjoW7jws zUmED+XM9(%BbW>93-zxa)w4Nuye?3m&fxuhUp=4buj@?Mn`<>o?r3m4Fh`DW_6`Mj z?+X?Ka{9R90PN7EcOkGZ$k+qg0|7f)HjUYHe1P#&0pIoqo3&2rRZRRX1oHuX&?m?C znc#4LEkF2Z?Rk3i@ghqXSADH eR67URbZcrjXiPUA$MZ8kMJdrgE;vlhtHZouH>zJ zRLpp?EoV5fr5(i6xrWsN|8=cjaTwzt_$ZmdR$Q*d!ym5BH(X&XkOz5TM_+D@%ZomK z^vH I6 z8Z$?> +)N=e2~|g$6sBTuNaN-U-PcT!lqVT0Nbvufquna zGBu`U3vYJu#$RpX!MAaGT0F?|!FRRJ6W7+Y tTJ^AS1Wpql;JRiUA*UP%)J~y6m{F{FDu{{E5$etxMT7CXZS+ z=;6jcz4B$;9xwl8Ut9U4ryiVLr7tFS_@Gaw#*E|62j8yEYuUxsT#Xrr zzAr95jNz_-B2Yi>8TRxbjEUiJKdx?^fzDTQTB!SzG4pUF5ThEv1qWlNdbmCo zs4;rx psvl4ku#8A zz|;8U-v!oR&X)rI z-1&hoUGr4~ zz=yW-R&~G*)Vy)r=*lUbL2hKtk*jes_$>v+MUAmZuEyw SW4@MVk)yXwSv#mYZl#>i;tYs Ec^?wbu44oyt|MKfi3^%Gcm| zi!+~U7sN)6E?KtZR{rS8t1&uaZ2~nZhw_0BUHqy(Do6O@aXzr7Y~U zP$4kzS1^68gjI9aS+aHKiKE%m>)fH}R;x2X^ z*c87pc8&(@Tneg|_-Dr)o_ye7J@6Ad9w2{b0y$upOvNQX=FbKAnztVKUJdBrLk>TB zYQ#KF_~637aa`n9t%%LKk;PSP^u?it@ JX9>M`NeBh-*-(`I|z9&PL~bW`r3B}PY2c%_`|Q( zSS!EAYMd>)1@VeiUW}Qq*tPZqyXK*6%dvHm6Y*%(hnV!`%-XQ0&xRb6)v|jsz>A!i z)Qt5%9b5=51@bu`(6Jsj1M9yWSbx5(pS7sH$&R^W0efH%;jkDOC%X`c!8{+X#YO*U z-`5(aqYreo{OXGVH*w-7X8w%JFCStA`_=mLV;}G(4_da=nHF%8PksK_ JNi{mKzJeCZj(bt
=Dj>&)bzSLOYbh6EF^;$O zV@nRKnROJKTv)qz1 y7k3_r$Laz1YdV^$VRG z{r3O0@KQJ4{ 84^= Zw>TojQm@FvO_yY7vA%4^o+mtL%A;hv4g{q z{6}u+9l;Ou^6yKYKbd~o7SE>*mv(F@efVq;pUrUeUPw#UI9YwM;IIFv$J_f)X8hg3 zulKF_($5;VeJvSt F7;&o&>7+ &2%(&5B(ouek3!;n(NIE zH`mfX-7IE)bN=3#IZ`KMV $c8`G}N-&6T}yd9a& z-1_uZM}Bp@PUU){^){XUx?m)+Pvj47qs`ikjkf;Qw|Y~}QyCcvyS16$p1D=AJkgGh zWqe)6C(^EJo^F|}?d@&Is2C m01tXCeNjniM+moNIk??0vX7@BV63g~v=1_9F zv3VxhIvu{-lFe)RvnQD!Ppxdp+_u=*61%gJ-I2eyMr$tG+u~^=*wHLUepfPoEIFP? zmF>;dXliF`#y5t;rquYk*qg`-FJx_HcU$vhBqp;K2Qo7f-y5=`v&r4IM7cG5^ftuS ziR5oCQdVta<|Z>T7B0J*HSxWrxfm@~x4F5H`H4ig811vMF&E40;%%ah^~QPEB7; zP2I>Jao$c1-bv(#;%k5NxcRQ;eDfbAZ|qptb!|