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 @@
-
项目主页 |
功能介绍 |
部署文档 |
@@ -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