diff --git a/.gitignore b/.gitignore index 9d4595d..d671df3 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ qcapi claude.json bard.json /*yaml -!/docker-compose.yaml \ No newline at end of file +!/docker-compose.yaml +res/instance_id.json \ No newline at end of file diff --git a/README.md b/README.md index 688a21e..430f01a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ Static Badge - 项目主页功能介绍部署文档 | @@ -38,6 +37,4 @@ 提交插件 回复效果(带有联网插件) - - diff --git a/main.py b/main.py index 043500e..5ceb67f 100644 --- a/main.py +++ b/main.py @@ -122,7 +122,7 @@ def complete_tips(): non_exist_keys = [] is_integrity = True - logging.info("检查tips模块完整性.") + logging.debug("检查tips模块完整性.") tips_template = importlib.import_module('tips-custom-template') tips = importlib.import_module('tips') for key in dir(tips_template): @@ -145,6 +145,10 @@ async def start_process(first_time_init=False): global known_exception_caught import pkg.utils.context + # 计算host和instance标识符 + import pkg.audit.identifier + pkg.audit.identifier.init() + # 加载配置 cfg_inst: pymodule_cfg.PythonModuleConfigFile = pymodule_cfg.PythonModuleConfigFile( 'config.py', @@ -158,6 +162,7 @@ async def start_process(first_time_init=False): complete_tips() cfg = pkg.utils.context.get_config_manager().data + # 更新openai库到最新版本 if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']: print("正在更新依赖库,请等待...") @@ -204,6 +209,24 @@ async def start_process(first_time_init=False): break except ValueError: print("请输入数字") + + # 初始化中央服务器 API 交互实例 + from pkg.utils.center import apigroup + from pkg.utils.center import v2 as center_v2 + + center_v2_api = center_v2.V2CenterAPI( + basic_info={ + "host_id": pkg.audit.identifier.identifier['host_id'], + "instance_id": pkg.audit.identifier.identifier['instance_id'], + "semantic_version": pkg.utils.updater.get_current_tag(), + "platform": sys.platform, + }, + runtime_info={ + "admin_id": "{}".format(cfg['admin_qq']), + "msg_source": cfg['msg_source_adapter'], + } + ) + pkg.utils.context.set_center_v2_api(center_v2_api) import pkg.openai.manager import pkg.database.manager @@ -375,6 +398,12 @@ async def start_process(first_time_init=False): if len(new_announcement) > 0: for announcement in new_announcement: logging.critical("[公告]<{}> {}".format(announcement['time'], announcement['content'])) + + # 发送统计数据 + pkg.utils.context.get_center_v2_api().main.post_announcement_showed( + [announcement['id'] for announcement in new_announcement] + ) + except Exception as e: logging.warning("获取公告失败:{}".format(e)) diff --git a/pkg/audit/identifier.py b/pkg/audit/identifier.py new file mode 100644 index 0000000..334b7e3 --- /dev/null +++ b/pkg/audit/identifier.py @@ -0,0 +1,83 @@ +import os +import uuid +import json +import time + + +identifier = { + 'host_id': '', + 'instance_id': '', + 'host_create_ts': 0, + 'instance_create_ts': 0, +} + +HOST_ID_FILE = os.path.expanduser('~/.qchatgpt/host_id.json') +INSTANCE_ID_FILE = 'res/instance_id.json' + +def init(): + global identifier + + if not os.path.exists(os.path.expanduser('~/.qchatgpt')): + os.mkdir(os.path.expanduser('~/.qchatgpt')) + + if not os.path.exists(HOST_ID_FILE): + new_host_id = 'host_'+str(uuid.uuid4()) + new_host_create_ts = int(time.time()) + + with open(HOST_ID_FILE, 'w') as f: + json.dump({ + 'host_id': new_host_id, + 'host_create_ts': new_host_create_ts + }, f) + + identifier['host_id'] = new_host_id + identifier['host_create_ts'] = new_host_create_ts + else: + loaded_host_id = '' + loaded_host_create_ts = 0 + + with open(HOST_ID_FILE, 'r') as f: + file_content = json.load(f) + loaded_host_id = file_content['host_id'] + loaded_host_create_ts = file_content['host_create_ts'] + + identifier['host_id'] = loaded_host_id + identifier['host_create_ts'] = loaded_host_create_ts + + # 检查实例 id + if os.path.exists(INSTANCE_ID_FILE): + instance_id = {} + with open(INSTANCE_ID_FILE, 'r') as f: + instance_id = json.load(f) + + if instance_id['host_id'] != identifier['host_id']: # 如果实例 id 不是当前主机的,删除 + os.remove(INSTANCE_ID_FILE) + + if not os.path.exists(INSTANCE_ID_FILE): + new_instance_id = 'instance_'+str(uuid.uuid4()) + new_instance_create_ts = int(time.time()) + + with open(INSTANCE_ID_FILE, 'w') as f: + json.dump({ + 'host_id': identifier['host_id'], + 'instance_id': new_instance_id, + 'instance_create_ts': new_instance_create_ts + }, f) + + identifier['instance_id'] = new_instance_id + identifier['instance_create_ts'] = new_instance_create_ts + else: + loaded_instance_id = '' + loaded_instance_create_ts = 0 + + with open(INSTANCE_ID_FILE, 'r') as f: + file_content = json.load(f) + loaded_instance_id = file_content['instance_id'] + loaded_instance_create_ts = file_content['instance_create_ts'] + + identifier['instance_id'] = loaded_instance_id + identifier['instance_create_ts'] = loaded_instance_create_ts + +def print_out(): + global identifier + print(identifier) diff --git a/pkg/database/manager.py b/pkg/database/manager.py index f410b41..ad44d51 100644 --- a/pkg/database/manager.py +++ b/pkg/database/manager.py @@ -91,7 +91,7 @@ class DatabaseManager: `json` text not null ) """) - print('Database initialized.') + # print('Database initialized.') # session持久化 def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int, diff --git a/pkg/openai/api/chat_completion.py b/pkg/openai/api/chat_completion.py index e308f17..1e0e1bc 100644 --- a/pkg/openai/api/chat_completion.py +++ b/pkg/openai/api/chat_completion.py @@ -6,6 +6,8 @@ from openai.types.chat import chat_completion_message from .model import RequestBase from .. import funcmgr +from ...plugin import host +from ...utils import context class ChatCompletionRequest(RequestBase): @@ -189,6 +191,16 @@ class ChatCompletionRequest(RequestBase): ret = "error: execute function failed: {}".format(str(e)) logging.error("函数执行失败: {}".format(str(e))) + # 上报数据 + plugin_info = host.get_plugin_info_for_audit(func_name.split('-')[0]) + audit_func_name = func_name.split('-')[1] + audit_func_desc = funcmgr.get_func_schema(func_name)['description'] + context.get_center_v2_api().usage.post_function_record( + plugin=plugin_info, + function_name=audit_func_name, + function_description=audit_func_desc, + ) + self.append_message( role="function", content=json.dumps(ret, ensure_ascii=False), diff --git a/pkg/openai/keymgr.py b/pkg/openai/keymgr.py index ea9c292..be6d728 100644 --- a/pkg/openai/keymgr.py +++ b/pkg/openai/keymgr.py @@ -75,7 +75,7 @@ class KeysManager: if self.api_key[key_name] not in self.exceeded: self.using_key = self.api_key[key_name] - logging.info("使用api-key:" + key_name) + logging.debug("使用api-key:" + key_name) # 触发插件事件 args = { diff --git a/pkg/openai/session.py b/pkg/openai/session.py index 197ccfd..1f9c76d 100644 --- a/pkg/openai/session.py +++ b/pkg/openai/session.py @@ -261,6 +261,8 @@ class Session: pending_res_text = "" + start_time = time.time() + # TODO 对不起,我知道这样非常非常屎山,但我之后会重构的 for resp in context.get_openai_manager().request_completion(prompts): @@ -349,6 +351,26 @@ class Session: self.just_switched_to_exist_session = False self.set_ongoing() + # 上报使用量数据 + session_type = session_name_spt[0] + session_id = session_name_spt[1] + + ability_provider = "QChatGPT.Text" + usage = total_tokens + model_name = context.get_config_manager().data['completion_api_params']['model'] + response_seconds = int(time.time() - start_time) + retry_times = -1 # 暂不记录 + + context.get_center_v2_api().usage.post_query_record( + session_type=session_type, + session_id=session_id, + query_ability_provider=ability_provider, + usage=usage, + model_name=model_name, + response_seconds=response_seconds, + retry_times=retry_times + ) + return res_ans if res_ans[0] != '\n' else res_ans[1:], finish_reason, funcs # 删除上一回合并返回上一回合的问题 diff --git a/pkg/plugin/host.py b/pkg/plugin/host.py index 2780684..50af1ec 100644 --- a/pkg/plugin/host.py +++ b/pkg/plugin/host.py @@ -84,23 +84,34 @@ def iter_plugins_name(): __current_module_path__ = "" -def walk_plugin_path(module, prefix='', path_prefix=''): +def walk_plugin_path(module, prefix="", path_prefix=""): global __current_module_path__ """遍历插件路径""" for item in pkgutil.iter_modules(module.__path__): if item.ispkg: logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name)) - walk_plugin_path(__import__(module.__name__ + '.' + item.name, fromlist=['']), - prefix + item.name + '.', path_prefix + item.name + '/') + walk_plugin_path( + __import__(module.__name__ + "." + item.name, fromlist=[""]), + prefix + item.name + ".", + path_prefix + item.name + "/", + ) else: try: - logging.debug("扫描插件模块: plugins/{}".format(path_prefix + item.name + '.py')) - __current_module_path__ = "plugins/"+path_prefix + item.name + '.py' + logging.debug( + "扫描插件模块: plugins/{}".format(path_prefix + item.name + ".py") + ) + __current_module_path__ = "plugins/" + path_prefix + item.name + ".py" - importlib.import_module(module.__name__ + '.' + item.name) - logging.debug('加载模块: plugins/{} 成功'.format(path_prefix + item.name + '.py')) + importlib.import_module(module.__name__ + "." + item.name) + logging.debug( + "加载模块: plugins/{} 成功".format(path_prefix + item.name + ".py") + ) except: - logging.error('加载模块: plugins/{} 失败: {}'.format(path_prefix + item.name + '.py', sys.exc_info())) + logging.error( + "加载模块: plugins/{} 失败: {}".format( + path_prefix + item.name + ".py", sys.exc_info() + ) + ) traceback.print_exc() @@ -108,7 +119,7 @@ def load_plugins(): """加载插件""" logging.debug("加载插件") PluginHost() - walk_plugin_path(__import__('plugins')) + walk_plugin_path(__import__("plugins")) logging.debug(__plugins__) @@ -132,7 +143,7 @@ def load_plugins(): def initialize_plugins(): """初始化插件""" - logging.info("初始化插件") + logging.debug("初始化插件") import pkg.plugin.models as models successfully_initialized_plugins = [] @@ -141,14 +152,14 @@ def initialize_plugins(): # if not plugin['enabled']: # continue try: - models.__current_registering_plugin__ = plugin['name'] - plugin['instance'] = plugin["class"](plugin_host=context.get_plugin_host()) + models.__current_registering_plugin__ = plugin["name"] + plugin["instance"] = plugin["class"](plugin_host=context.get_plugin_host()) # logging.info("插件 {} 已初始化".format(plugin['name'])) - successfully_initialized_plugins.append(plugin['name']) + successfully_initialized_plugins.append(plugin["name"]) except: - logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info())) + logging.error("插件{}初始化时发生错误: {}".format(plugin["name"], sys.exc_info())) logging.debug(traceback.format_exc()) - + logging.info("以下插件已初始化: {}".format(", ".join(successfully_initialized_plugins))) @@ -172,9 +183,12 @@ def get_github_plugin_repo_label(repo_url: str) -> list[str]: """获取username, repo""" # 提取 username/repo , 正则表达式 - repo = re.findall(r'(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)', repo_url) + repo = re.findall( + r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)", + repo_url, + ) - if len(repo) > 0: # github + if len(repo) > 0: # github return repo[0].split("/") else: return None @@ -183,53 +197,52 @@ def get_github_plugin_repo_label(repo_url: str) -> list[str]: def download_plugin_source_code(repo_url: str, target_path: str) -> str: """下载插件源码""" # 检查源类型 - + # 提取 username/repo , 正则表达式 - repo = get_github_plugin_repo_label(repo_url) + repo = get_github_plugin_repo_label(repo_url) target_path += repo[1] - if repo is not None: # github + if repo is not None: # github logging.info("从 GitHub 下载插件源码...") zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD" zip_resp = requests.get( - url=zipball_url, - proxies=network.wrapper_proxies(), - stream=True + url=zipball_url, proxies=network.wrapper_proxies(), stream=True ) if zip_resp.status_code != 200: raise Exception("下载源码失败: {}".format(zip_resp.text)) - - if os.path.exists("temp/"+target_path): - shutil.rmtree("temp/"+target_path) + + if os.path.exists("temp/" + target_path): + shutil.rmtree("temp/" + target_path) if os.path.exists(target_path): shutil.rmtree(target_path) - os.makedirs("temp/"+target_path) + os.makedirs("temp/" + target_path) - with open("temp/"+target_path+"/source.zip", "wb") as f: + with open("temp/" + target_path + "/source.zip", "wb") as f: for chunk in zip_resp.iter_content(chunk_size=1024): if chunk: f.write(chunk) logging.info("下载完成, 解压...") import zipfile - with zipfile.ZipFile("temp/"+target_path+"/source.zip", 'r') as zip_ref: - zip_ref.extractall("temp/"+target_path) - os.remove("temp/"+target_path+"/source.zip") + + with zipfile.ZipFile("temp/" + target_path + "/source.zip", "r") as zip_ref: + zip_ref.extractall("temp/" + target_path) + os.remove("temp/" + target_path + "/source.zip") # 目标是 username-repo-hash , 用正则表达式提取完整的文件夹名,复制到 plugins/repo import glob # 获取解压后的文件夹名 - unzip_dir = glob.glob("temp/"+target_path+"/*")[0] + unzip_dir = glob.glob("temp/" + target_path + "/*")[0] # 复制到 plugins/repo - shutil.copytree(unzip_dir, target_path+"/") + shutil.copytree(unzip_dir, target_path + "/") # 删除解压后的文件夹 shutil.rmtree(unzip_dir) @@ -237,18 +250,20 @@ def download_plugin_source_code(repo_url: str, target_path: str) -> str: logging.info("解压完成") else: raise Exception("暂不支持的源类型,请使用 GitHub 仓库发行插件。") - + return repo[1] def check_requirements(path: str): # 检查此目录是否包含requirements.txt - if os.path.exists(path+"/requirements.txt"): + if os.path.exists(path + "/requirements.txt"): logging.info("检测到requirements.txt,正在安装依赖") import pkg.utils.pkgmgr - pkg.utils.pkgmgr.install_requirements(path+"/requirements.txt") + + pkg.utils.pkgmgr.install_requirements(path + "/requirements.txt") import pkg.utils.log as log + log.reset_logging() @@ -257,25 +272,43 @@ def install_plugin(repo_url: str): repo_label = download_plugin_source_code(repo_url, "plugins/") - check_requirements("plugins/"+repo_label) + check_requirements("plugins/" + repo_label) metadata.set_plugin_metadata(repo_label, repo_url, int(time.time()), "HEAD") + # 上报安装记录 + context.get_center_v2_api().plugin.post_install_record( + plugin={ + "name": "unknown", + "remote": repo_url, + "author": "unknown", + "version": "HEAD", + } + ) + def uninstall_plugin(plugin_name: str) -> str: """卸载插件""" if plugin_name not in __plugins__: raise Exception("插件不存在") + plugin_info = get_plugin_info_for_audit(plugin_name) + # 获取文件夹路径 - plugin_path = __plugins__[plugin_name]['path'].replace("\\", "/") + plugin_path = __plugins__[plugin_name]["path"].replace("\\", "/") # 剪切路径为plugins/插件名 plugin_path = plugin_path.split("plugins/")[1].split("/")[0] # 删除文件夹 - shutil.rmtree("plugins/"+plugin_path) - return "plugins/"+plugin_path + shutil.rmtree("plugins/" + plugin_path) + + # 上报卸载记录 + context.get_center_v2_api().plugin.post_remove_record( + plugin=plugin_info + ) + + return "plugins/" + plugin_path def update_plugin(plugin_name: str): @@ -287,12 +320,26 @@ def update_plugin(plugin_name: str): if meta == {}: raise Exception("没有此插件元数据信息,无法更新") - - remote_url = meta['source'] - if remote_url == "https://github.com/RockChinQ/QChatGPT" or remote_url == "https://gitee.com/RockChin/QChatGPT" \ - or remote_url == "" or remote_url is None or remote_url == "http://github.com/RockChinQ/QChatGPT" or remote_url == "http://gitee.com/RockChin/QChatGPT": - raise Exception("插件没有远程地址记录,无法更新") + old_plugin_info = get_plugin_info_for_audit(plugin_name) + + context.get_center_v2_api().plugin.post_update_record( + plugin=old_plugin_info, + old_version=old_plugin_info['version'], + new_version='HEAD', + ) + + remote_url = meta["source"] + if ( + remote_url == "https://github.com/RockChinQ/QChatGPT" + or remote_url == "https://gitee.com/RockChin/QChatGPT" + or remote_url == "" + or remote_url is None + or remote_url == "http://github.com/RockChinQ/QChatGPT" + or remote_url == "http://gitee.com/RockChin/QChatGPT" + ): + raise Exception("插件没有远程地址记录,无法更新") + # 重新安装插件 logging.info("正在重新安装插件以进行更新...") @@ -301,7 +348,7 @@ def update_plugin(plugin_name: str): def get_plugin_name_by_path_name(plugin_path_name: str) -> str: for k, v in __plugins__.items(): - if v['path'] == "plugins/"+plugin_path_name+"/main.py": + if v["path"] == "plugins/" + plugin_path_name + "/main.py": return k return None @@ -309,8 +356,8 @@ def get_plugin_name_by_path_name(plugin_path_name: str) -> str: def get_plugin_path_name_by_plugin_name(plugin_name: str) -> str: if plugin_name not in __plugins__: return None - - plugin_main_module_path = __plugins__[plugin_name]['path'] + + plugin_main_module_path = __plugins__[plugin_name]["path"] plugin_main_module_path = plugin_main_module_path.replace("\\", "/") @@ -319,8 +366,29 @@ def get_plugin_path_name_by_plugin_name(plugin_name: str) -> str: return spt[1] +def get_plugin_info_for_audit(plugin_name: str) -> dict: + """获取插件信息""" + if plugin_name not in __plugins__: + return {} + plugin = __plugins__[plugin_name] + + name = plugin["name"] + meta = metadata.get_plugin_metadata(get_plugin_path_name_by_plugin_name(name)) + remote = meta["source"] if meta != {} else "" + author = plugin["author"] + version = plugin["version"] + + return { + "name": name, + "remote": remote, + "author": author, + "version": version, + } + + class EventContext: """事件上下文""" + eid = 0 """事件编号""" @@ -395,6 +463,7 @@ class EventContext: def emit(event_name: str, **kwargs) -> EventContext: """触发事件""" import pkg.utils.context as context + if context.get_plugin_host() is None: return None return context.get_plugin_host().emit(event_name, **kwargs) @@ -443,9 +512,10 @@ class PluginHost: event_context = EventContext(event_name) logging.debug("触发事件: {} ({})".format(event_name, event_context.eid)) + + emitted_plugins = [] for plugin in iter_plugins(): - - if not plugin['enabled']: + if not plugin["enabled"]: continue # if plugin['instance'] is None: @@ -457,9 +527,11 @@ class PluginHost: # logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info())) # continue - if 'hooks' not in plugin or event_name not in plugin['hooks']: + if "hooks" not in plugin or event_name not in plugin["hooks"]: continue + emitted_plugins.append(plugin['name']) + hooks = [] if event_name in plugin["hooks"]: hooks = plugin["hooks"][event_name] @@ -467,27 +539,44 @@ class PluginHost: try: already_prevented_default = event_context.is_prevented_default() - kwargs['host'] = context.get_plugin_host() - kwargs['event'] = event_context + kwargs["host"] = context.get_plugin_host() + kwargs["event"] = event_context - hook(plugin['instance'], **kwargs) + hook(plugin["instance"], **kwargs) - if event_context.is_prevented_default() and not already_prevented_default: - logging.debug("插件 {} 已要求阻止事件 {} 的默认行为".format(plugin['name'], event_name)) + if ( + event_context.is_prevented_default() + and not already_prevented_default + ): + logging.debug( + "插件 {} 已要求阻止事件 {} 的默认行为".format(plugin["name"], event_name) + ) except Exception as e: - logging.error("插件{}响应事件{}时发生错误".format(plugin['name'], event_name)) + logging.error("插件{}响应事件{}时发生错误".format(plugin["name"], event_name)) logging.error(traceback.format_exc()) # print("done:{}".format(plugin['name'])) if event_context.is_prevented_postorder(): - logging.debug("插件 {} 阻止了后序插件的执行".format(plugin['name'])) + logging.debug("插件 {} 阻止了后序插件的执行".format(plugin["name"])) break - logging.debug("事件 {} ({}) 处理完毕,返回值: {}".format(event_name, event_context.eid, - event_context.__return_value__)) + logging.debug( + "事件 {} ({}) 处理完毕,返回值: {}".format( + event_name, event_context.eid, event_context.__return_value__ + ) + ) + + if len(emitted_plugins) > 0: + plugins_info = [get_plugin_info_for_audit(p) for p in emitted_plugins] + + context.get_center_v2_api().usage.post_event_record( + plugins=plugins_info, + event_name=event_name, + ) return event_context + if __name__ == "__main__": pass diff --git a/pkg/qqbot/cmds/aamgr.py b/pkg/qqbot/cmds/aamgr.py index 27596c4..f761063 100644 --- a/pkg/qqbot/cmds/aamgr.py +++ b/pkg/qqbot/cmds/aamgr.py @@ -4,11 +4,10 @@ import pkgutil import traceback import json - -__command_list__ = {} - import tips as tips_custom + +__command_list__ = {} """命令树 结构: diff --git a/pkg/qqbot/manager.py b/pkg/qqbot/manager.py index 588d662..8cd663f 100644 --- a/pkg/qqbot/manager.py +++ b/pkg/qqbot/manager.py @@ -125,6 +125,10 @@ class QQBotManager: else: self.adapter = context.get_qqbot_manager().adapter self.bot_account_id = context.get_qqbot_manager().bot_account_id + + # 保存 account_id 到审计模块 + from ..utils.center import apigroup + apigroup.APIGroup._runtime_info['account_id'] = "{}".format(self.bot_account_id) context.set_qqbot_manager(self) diff --git a/pkg/utils/center/__init__.py b/pkg/utils/center/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pkg/utils/center/apigroup.py b/pkg/utils/center/apigroup.py new file mode 100644 index 0000000..94812d5 --- /dev/null +++ b/pkg/utils/center/apigroup.py @@ -0,0 +1,88 @@ +import abc +import uuid +import json +import logging +import threading + +import requests + + +class APIGroup(metaclass=abc.ABCMeta): + """API 组抽象类""" + _basic_info: dict = None + _runtime_info: dict = None + + prefix = None + + def __init__(self, prefix: str): + self.prefix = prefix + + def do( + self, + method: str, + path: str, + data: dict = None, + params: dict = None, + headers: dict = {}, + **kwargs + ): + """执行一个请求""" + def thr_wrapper( + self, + method: str, + path: str, + data: dict = None, + params: dict = None, + headers: dict = {}, + **kwargs + ): + try: + url = self.prefix + path + data = json.dumps(data) + headers['Content-Type'] = 'application/json' + + ret = requests.request( + method, + url, + data=data, + params=params, + headers=headers, + **kwargs + ) + + logging.debug("data: %s", data) + + logging.debug("ret: %s", ret.json()) + except Exception as e: + logging.debug("上报数据失败: %s", e) + + thr = threading.Thread(target=thr_wrapper, args=( + self, + method, + path, + data, + params, + headers, + ), kwargs=kwargs) + thr.start() + + + def gen_rid( + self + ): + """生成一个请求 ID""" + return str(uuid.uuid4()) + + def basic_info( + self + ): + """获取基本信息""" + basic_info = APIGroup._basic_info.copy() + basic_info['rid'] = self.gen_rid() + return basic_info + + def runtime_info( + self + ): + """获取运行时信息""" + return APIGroup._runtime_info diff --git a/pkg/utils/center/groups/__init__.py b/pkg/utils/center/groups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pkg/utils/center/groups/main.py b/pkg/utils/center/groups/main.py new file mode 100644 index 0000000..f158cd7 --- /dev/null +++ b/pkg/utils/center/groups/main.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from .. import apigroup + + +class V2MainDataAPI(apigroup.APIGroup): + """主程序相关 数据API""" + + def __init__(self, prefix: str): + super().__init__(prefix+"/main") + + def post_update_record( + self, + spent_seconds: int, + infer_reason: str, + old_version: str, + new_version: str, + ): + """提交更新记录""" + return self.do( + "POST", + "/update", + data={ + "basic": self.basic_info(), + "update_info": { + "spent_seconds": spent_seconds, + "infer_reason": infer_reason, + "old_version": old_version, + "new_version": new_version, + } + } + ) + + def post_announcement_showed( + self, + ids: list[int], + ): + """提交公告已阅""" + return self.do( + "POST", + "/announcement", + data={ + "basic": self.basic_info(), + "announcement_info": { + "ids": ids, + } + } + ) diff --git a/pkg/utils/center/groups/plugin.py b/pkg/utils/center/groups/plugin.py new file mode 100644 index 0000000..b3ac423 --- /dev/null +++ b/pkg/utils/center/groups/plugin.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from .. import apigroup + + +class V2PluginDataAPI(apigroup.APIGroup): + """插件数据相关 API""" + + def __init__(self, prefix: str): + super().__init__(prefix+"/plugin") + + def post_install_record( + self, + plugin: dict + ): + """提交插件安装记录""" + return self.do( + "POST", + "/install", + data={ + "basic": self.basic_info(), + "plugin": plugin, + } + ) + + def post_remove_record( + self, + plugin: dict + ): + """提交插件卸载记录""" + return self.do( + "POST", + "/remove", + data={ + "basic": self.basic_info(), + "plugin": plugin, + } + ) + + def post_update_record( + self, + plugin: dict, + old_version: str, + new_version: str, + ): + """提交插件更新记录""" + return self.do( + "POST", + "/update", + data={ + "basic": self.basic_info(), + "plugin": plugin, + "update_info": { + "old_version": old_version, + "new_version": new_version, + } + } + ) diff --git a/pkg/utils/center/groups/usage.py b/pkg/utils/center/groups/usage.py new file mode 100644 index 0000000..6e383a3 --- /dev/null +++ b/pkg/utils/center/groups/usage.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from .. import apigroup + + +class V2UsageDataAPI(apigroup.APIGroup): + """使用量数据相关 API""" + + def __init__(self, prefix: str): + super().__init__(prefix+"/usage") + + def post_query_record( + self, + session_type: str, + session_id: str, + query_ability_provider: str, + usage: int, + model_name: str, + response_seconds: int, + retry_times: int, + ): + """提交请求记录""" + return self.do( + "POST", + "/query", + data={ + "basic": self.basic_info(), + "runtime": self.runtime_info(), + "session_info": { + "type": session_type, + "id": session_id, + }, + "query_info": { + "ability_provider": query_ability_provider, + "usage": usage, + "model_name": model_name, + "response_seconds": response_seconds, + "retry_times": retry_times, + } + } + ) + + def post_event_record( + self, + plugins: list[dict], + event_name: str, + ): + """提交事件触发记录""" + return self.do( + "POST", + "/event", + data={ + "basic": self.basic_info(), + "runtime": self.runtime_info(), + "plugins": plugins, + "event_info": { + "name": event_name, + } + } + ) + + def post_function_record( + self, + plugin: dict, + function_name: str, + function_description: str, + ): + """提交内容函数使用记录""" + return self.do( + "POST", + "/function", + data={ + "basic": self.basic_info(), + "plugin": plugin, + "function_info": { + "name": function_name, + "description": function_description, + } + } + ) + diff --git a/pkg/utils/center/v2.py b/pkg/utils/center/v2.py new file mode 100644 index 0000000..b1c0a3e --- /dev/null +++ b/pkg/utils/center/v2.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import logging + +from . import apigroup +from .groups import main +from .groups import usage +from .groups import plugin + + +BACKEND_URL = "https://api.qchatgpt.rockchin.top/api/v2" + +class V2CenterAPI: + """中央服务器 v2 API 交互类""" + + main: main.V2MainDataAPI = None + """主 API 组""" + + usage: usage.V2UsageDataAPI = None + """使用量 API 组""" + + plugin: plugin.V2PluginDataAPI = None + """插件 API 组""" + + def __init__(self, basic_info: dict = None, runtime_info: dict = None): + """初始化""" + + logging.debug("basic_info: %s, runtime_info: %s", basic_info, runtime_info) + + apigroup.APIGroup._basic_info = basic_info + apigroup.APIGroup._runtime_info = runtime_info + + self.main = main.V2MainDataAPI(BACKEND_URL) + self.usage = usage.V2UsageDataAPI(BACKEND_URL) + self.plugin = plugin.V2PluginDataAPI(BACKEND_URL) diff --git a/pkg/utils/context.py b/pkg/utils/context.py index e26c702..e6a2734 100644 --- a/pkg/utils/context.py +++ b/pkg/utils/context.py @@ -8,6 +8,7 @@ from ..openai import manager as openai_mgr from ..qqbot import manager as qqbot_mgr from ..config import manager as config_mgr from ..plugin import host as plugin_host +from .center import v2 as center_v2 context = { @@ -114,3 +115,16 @@ def get_thread_ctl() -> threadctl.ThreadCtl: t: threadctl.ThreadCtl = context['pool_ctl'] context_lock.release() return t + + +def set_center_v2_api(inst: center_v2.V2CenterAPI): + context_lock.acquire() + context['center_v2_api'] = inst + context_lock.release() + + +def get_center_v2_api() -> center_v2.V2CenterAPI: + context_lock.acquire() + t: center_v2.V2CenterAPI = context['center_v2_api'] + context_lock.release() + return t \ No newline at end of file diff --git a/pkg/utils/platform.py b/pkg/utils/platform.py new file mode 100644 index 0000000..b280b07 --- /dev/null +++ b/pkg/utils/platform.py @@ -0,0 +1,7 @@ +import os +import sys + + +def get_platform() -> str: + """获取当前平台""" + return sys.platform diff --git a/pkg/utils/updater.py b/pkg/utils/updater.py index 767f362..3881298 100644 --- a/pkg/utils/updater.py +++ b/pkg/utils/updater.py @@ -1,11 +1,15 @@ +from __future__ import annotations + import datetime import logging import os.path +import time import requests from . import constants from . import network +from . import context def check_dulwich_closure(): @@ -107,7 +111,10 @@ def compare_version_str(v0: str, v1: str) -> int: def update_all(cli: bool = False) -> bool: """检查更新并下载源码""" + start_time = time.time() + current_tag = get_current_tag() + old_tag = current_tag rls_list = get_release_list() @@ -200,6 +207,13 @@ def update_all(cli: bool = False) -> bool: with open("current_tag", "w") as f: f.write(current_tag) + context.get_center_v2_api().main.post_update_record( + spent_seconds=int(time.time()-start_time), + infer_reason="update", + old_version=old_tag, + new_version=current_tag, + ) + # 通知管理员 if not cli: import pkg.utils.context diff --git a/requirements.txt b/requirements.txt index 7047d58..c3e2940 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ nakuru-project-idk CallingGPT tiktoken PyYaml +aiohttp \ No newline at end of file diff --git a/tests/identifier_test/host_identifier.py b/tests/identifier_test/host_identifier.py new file mode 100644 index 0000000..6483493 --- /dev/null +++ b/tests/identifier_test/host_identifier.py @@ -0,0 +1,43 @@ +import os +import uuid +import json + +# 向 ~/.qchatgpt 写入一个 标识符 + +if not os.path.exists(os.path.expanduser('~/.qchatgpt')): + os.mkdir(os.path.expanduser('~/.qchatgpt')) + +identifier = { + "host_id": "host_"+str(uuid.uuid4()), +} + +if not os.path.exists(os.path.expanduser('~/.qchatgpt/host.json')): + print('create ~/.qchatgpt/host.json') + with open(os.path.expanduser('~/.qchatgpt/host.json'), 'w') as f: + json.dump(identifier, f) +else: + print('load ~/.qchatgpt/host.json') + with open(os.path.expanduser('~/.qchatgpt/host.json'), 'r') as f: + identifier = json.load(f) + +print(identifier) + +instance_id = { + "host_id": identifier['host_id'], + "instance_id": "instance_"+str(uuid.uuid4()), +} + +# 实例 id +if os.path.exists("res/instance_id.json"): + with open("res/instance_id.json", 'r') as f: + instance_id = json.load(f) + + if instance_id['host_id'] != identifier['host_id']: + os.remove("res/instance_id.json") + +if not os.path.exists("res/instance_id.json"): + print('create res/instance_id.json') + with open("res/instance_id.json", 'w') as f: + json.dump(instance_id, f) + +print(instance_id) \ No newline at end of file