Merge pull request #644 from RockChinQ/feat/online-data-analysis

Feat: v2 数据统计接口
This commit is contained in:
Junyan Qin 2023-12-22 18:33:50 +08:00 committed by GitHub
commit 256bc4dc1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 695 additions and 70 deletions

3
.gitignore vendored
View File

@ -30,4 +30,5 @@ qcapi
claude.json claude.json
bard.json bard.json
/*yaml /*yaml
!/docker-compose.yaml !/docker-compose.yaml
res/instance_id.json

View File

@ -29,7 +29,6 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Linux%E9%83%A8%E7%BD%B2%E8%A7%86%E9%A2%91-208647"> <img alt="Static Badge" src="https://img.shields.io/badge/Linux%E9%83%A8%E7%BD%B2%E8%A7%86%E9%A2%91-208647">
</a> </a>
<a href="https://qchatgpt.rockchin.top">项目主页</a> <a href="https://qchatgpt.rockchin.top">项目主页</a>
<a href="https://qchatgpt.rockchin.top/posts/feature.html">功能介绍</a> <a href="https://qchatgpt.rockchin.top/posts/feature.html">功能介绍</a>
<a href="https://qchatgpt.rockchin.top/posts/deploy/">部署文档</a> <a href="https://qchatgpt.rockchin.top/posts/deploy/">部署文档</a>
@ -38,6 +37,4 @@
<a href="https://github.com/RockChinQ/QChatGPT/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a> <a href="https://github.com/RockChinQ/QChatGPT/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/> <img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/>
</div> </div>

31
main.py
View File

@ -122,7 +122,7 @@ def complete_tips():
non_exist_keys = [] non_exist_keys = []
is_integrity = True is_integrity = True
logging.info("检查tips模块完整性.") logging.debug("检查tips模块完整性.")
tips_template = importlib.import_module('tips-custom-template') tips_template = importlib.import_module('tips-custom-template')
tips = importlib.import_module('tips') tips = importlib.import_module('tips')
for key in dir(tips_template): for key in dir(tips_template):
@ -145,6 +145,10 @@ async def start_process(first_time_init=False):
global known_exception_caught global known_exception_caught
import pkg.utils.context import pkg.utils.context
# 计算host和instance标识符
import pkg.audit.identifier
pkg.audit.identifier.init()
# 加载配置 # 加载配置
cfg_inst: pymodule_cfg.PythonModuleConfigFile = pymodule_cfg.PythonModuleConfigFile( cfg_inst: pymodule_cfg.PythonModuleConfigFile = pymodule_cfg.PythonModuleConfigFile(
'config.py', 'config.py',
@ -158,6 +162,7 @@ async def start_process(first_time_init=False):
complete_tips() complete_tips()
cfg = pkg.utils.context.get_config_manager().data cfg = pkg.utils.context.get_config_manager().data
# 更新openai库到最新版本 # 更新openai库到最新版本
if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']: if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']:
print("正在更新依赖库,请等待...") print("正在更新依赖库,请等待...")
@ -204,6 +209,24 @@ async def start_process(first_time_init=False):
break break
except ValueError: except ValueError:
print("请输入数字") 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.openai.manager
import pkg.database.manager import pkg.database.manager
@ -375,6 +398,12 @@ async def start_process(first_time_init=False):
if len(new_announcement) > 0: if len(new_announcement) > 0:
for announcement in new_announcement: for announcement in new_announcement:
logging.critical("[公告]<{}> {}".format(announcement['time'], announcement['content'])) 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: except Exception as e:
logging.warning("获取公告失败:{}".format(e)) logging.warning("获取公告失败:{}".format(e))

83
pkg/audit/identifier.py Normal file
View File

@ -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)

View File

@ -91,7 +91,7 @@ class DatabaseManager:
`json` text not null `json` text not null
) )
""") """)
print('Database initialized.') # print('Database initialized.')
# session持久化 # session持久化
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int, def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,

View File

@ -6,6 +6,8 @@ from openai.types.chat import chat_completion_message
from .model import RequestBase from .model import RequestBase
from .. import funcmgr from .. import funcmgr
from ...plugin import host
from ...utils import context
class ChatCompletionRequest(RequestBase): class ChatCompletionRequest(RequestBase):
@ -189,6 +191,16 @@ class ChatCompletionRequest(RequestBase):
ret = "error: execute function failed: {}".format(str(e)) ret = "error: execute function failed: {}".format(str(e))
logging.error("函数执行失败: {}".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( self.append_message(
role="function", role="function",
content=json.dumps(ret, ensure_ascii=False), content=json.dumps(ret, ensure_ascii=False),

View File

@ -75,7 +75,7 @@ class KeysManager:
if self.api_key[key_name] not in self.exceeded: if self.api_key[key_name] not in self.exceeded:
self.using_key = self.api_key[key_name] self.using_key = self.api_key[key_name]
logging.info("使用api-key:" + key_name) logging.debug("使用api-key:" + key_name)
# 触发插件事件 # 触发插件事件
args = { args = {

View File

@ -261,6 +261,8 @@ class Session:
pending_res_text = "" pending_res_text = ""
start_time = time.time()
# TODO 对不起,我知道这样非常非常屎山,但我之后会重构的 # TODO 对不起,我知道这样非常非常屎山,但我之后会重构的
for resp in context.get_openai_manager().request_completion(prompts): for resp in context.get_openai_manager().request_completion(prompts):
@ -349,6 +351,26 @@ class Session:
self.just_switched_to_exist_session = False self.just_switched_to_exist_session = False
self.set_ongoing() 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 return res_ans if res_ans[0] != '\n' else res_ans[1:], finish_reason, funcs
# 删除上一回合并返回上一回合的问题 # 删除上一回合并返回上一回合的问题

View File

@ -84,23 +84,34 @@ def iter_plugins_name():
__current_module_path__ = "" __current_module_path__ = ""
def walk_plugin_path(module, prefix='', path_prefix=''): def walk_plugin_path(module, prefix="", path_prefix=""):
global __current_module_path__ global __current_module_path__
"""遍历插件路径""" """遍历插件路径"""
for item in pkgutil.iter_modules(module.__path__): for item in pkgutil.iter_modules(module.__path__):
if item.ispkg: if item.ispkg:
logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name)) logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name))
walk_plugin_path(__import__(module.__name__ + '.' + item.name, fromlist=['']), walk_plugin_path(
prefix + item.name + '.', path_prefix + item.name + '/') __import__(module.__name__ + "." + item.name, fromlist=[""]),
prefix + item.name + ".",
path_prefix + item.name + "/",
)
else: else:
try: try:
logging.debug("扫描插件模块: plugins/{}".format(path_prefix + item.name + '.py')) logging.debug(
__current_module_path__ = "plugins/"+path_prefix + item.name + '.py' "扫描插件模块: plugins/{}".format(path_prefix + item.name + ".py")
)
__current_module_path__ = "plugins/" + path_prefix + item.name + ".py"
importlib.import_module(module.__name__ + '.' + item.name) importlib.import_module(module.__name__ + "." + item.name)
logging.debug('加载模块: plugins/{} 成功'.format(path_prefix + item.name + '.py')) logging.debug(
"加载模块: plugins/{} 成功".format(path_prefix + item.name + ".py")
)
except: 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() traceback.print_exc()
@ -108,7 +119,7 @@ def load_plugins():
"""加载插件""" """加载插件"""
logging.debug("加载插件") logging.debug("加载插件")
PluginHost() PluginHost()
walk_plugin_path(__import__('plugins')) walk_plugin_path(__import__("plugins"))
logging.debug(__plugins__) logging.debug(__plugins__)
@ -132,7 +143,7 @@ def load_plugins():
def initialize_plugins(): def initialize_plugins():
"""初始化插件""" """初始化插件"""
logging.info("初始化插件") logging.debug("初始化插件")
import pkg.plugin.models as models import pkg.plugin.models as models
successfully_initialized_plugins = [] successfully_initialized_plugins = []
@ -141,14 +152,14 @@ def initialize_plugins():
# if not plugin['enabled']: # if not plugin['enabled']:
# continue # continue
try: try:
models.__current_registering_plugin__ = plugin['name'] models.__current_registering_plugin__ = plugin["name"]
plugin['instance'] = plugin["class"](plugin_host=context.get_plugin_host()) plugin["instance"] = plugin["class"](plugin_host=context.get_plugin_host())
# logging.info("插件 {} 已初始化".format(plugin['name'])) # logging.info("插件 {} 已初始化".format(plugin['name']))
successfully_initialized_plugins.append(plugin['name']) successfully_initialized_plugins.append(plugin["name"])
except: except:
logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info())) logging.error("插件{}初始化时发生错误: {}".format(plugin["name"], sys.exc_info()))
logging.debug(traceback.format_exc()) logging.debug(traceback.format_exc())
logging.info("以下插件已初始化: {}".format(", ".join(successfully_initialized_plugins))) 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"""
# 提取 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("/") return repo[0].split("/")
else: else:
return None 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: def download_plugin_source_code(repo_url: str, target_path: str) -> str:
"""下载插件源码""" """下载插件源码"""
# 检查源类型 # 检查源类型
# 提取 username/repo , 正则表达式 # 提取 username/repo , 正则表达式
repo = get_github_plugin_repo_label(repo_url) repo = get_github_plugin_repo_label(repo_url)
target_path += repo[1] target_path += repo[1]
if repo is not None: # github if repo is not None: # github
logging.info("从 GitHub 下载插件源码...") logging.info("从 GitHub 下载插件源码...")
zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD" zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD"
zip_resp = requests.get( zip_resp = requests.get(
url=zipball_url, url=zipball_url, proxies=network.wrapper_proxies(), stream=True
proxies=network.wrapper_proxies(),
stream=True
) )
if zip_resp.status_code != 200: if zip_resp.status_code != 200:
raise Exception("下载源码失败: {}".format(zip_resp.text)) raise Exception("下载源码失败: {}".format(zip_resp.text))
if os.path.exists("temp/"+target_path): if os.path.exists("temp/" + target_path):
shutil.rmtree("temp/"+target_path) shutil.rmtree("temp/" + target_path)
if os.path.exists(target_path): if os.path.exists(target_path):
shutil.rmtree(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): for chunk in zip_resp.iter_content(chunk_size=1024):
if chunk: if chunk:
f.write(chunk) f.write(chunk)
logging.info("下载完成, 解压...") logging.info("下载完成, 解压...")
import zipfile import zipfile
with zipfile.ZipFile("temp/"+target_path+"/source.zip", 'r') as zip_ref:
zip_ref.extractall("temp/"+target_path) with zipfile.ZipFile("temp/" + target_path + "/source.zip", "r") as zip_ref:
os.remove("temp/"+target_path+"/source.zip") zip_ref.extractall("temp/" + target_path)
os.remove("temp/" + target_path + "/source.zip")
# 目标是 username-repo-hash , 用正则表达式提取完整的文件夹名,复制到 plugins/repo # 目标是 username-repo-hash , 用正则表达式提取完整的文件夹名,复制到 plugins/repo
import glob import glob
# 获取解压后的文件夹名 # 获取解压后的文件夹名
unzip_dir = glob.glob("temp/"+target_path+"/*")[0] unzip_dir = glob.glob("temp/" + target_path + "/*")[0]
# 复制到 plugins/repo # 复制到 plugins/repo
shutil.copytree(unzip_dir, target_path+"/") shutil.copytree(unzip_dir, target_path + "/")
# 删除解压后的文件夹 # 删除解压后的文件夹
shutil.rmtree(unzip_dir) shutil.rmtree(unzip_dir)
@ -237,18 +250,20 @@ def download_plugin_source_code(repo_url: str, target_path: str) -> str:
logging.info("解压完成") logging.info("解压完成")
else: else:
raise Exception("暂不支持的源类型,请使用 GitHub 仓库发行插件。") raise Exception("暂不支持的源类型,请使用 GitHub 仓库发行插件。")
return repo[1] return repo[1]
def check_requirements(path: str): def check_requirements(path: str):
# 检查此目录是否包含requirements.txt # 检查此目录是否包含requirements.txt
if os.path.exists(path+"/requirements.txt"): if os.path.exists(path + "/requirements.txt"):
logging.info("检测到requirements.txt正在安装依赖") logging.info("检测到requirements.txt正在安装依赖")
import pkg.utils.pkgmgr 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 import pkg.utils.log as log
log.reset_logging() log.reset_logging()
@ -257,25 +272,43 @@ def install_plugin(repo_url: str):
repo_label = download_plugin_source_code(repo_url, "plugins/") 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") 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: def uninstall_plugin(plugin_name: str) -> str:
"""卸载插件""" """卸载插件"""
if plugin_name not in __plugins__: if plugin_name not in __plugins__:
raise Exception("插件不存在") 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/插件名 # 剪切路径为plugins/插件名
plugin_path = plugin_path.split("plugins/")[1].split("/")[0] plugin_path = plugin_path.split("plugins/")[1].split("/")[0]
# 删除文件夹 # 删除文件夹
shutil.rmtree("plugins/"+plugin_path) shutil.rmtree("plugins/" + plugin_path)
return "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): def update_plugin(plugin_name: str):
@ -287,12 +320,26 @@ def update_plugin(plugin_name: str):
if meta == {}: if meta == {}:
raise Exception("没有此插件元数据信息,无法更新") 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("正在重新安装插件以进行更新...") logging.info("正在重新安装插件以进行更新...")
@ -301,7 +348,7 @@ def update_plugin(plugin_name: str):
def get_plugin_name_by_path_name(plugin_path_name: str) -> str: def get_plugin_name_by_path_name(plugin_path_name: str) -> str:
for k, v in __plugins__.items(): 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 k
return None 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: def get_plugin_path_name_by_plugin_name(plugin_name: str) -> str:
if plugin_name not in __plugins__: if plugin_name not in __plugins__:
return None 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("\\", "/") 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] 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: class EventContext:
"""事件上下文""" """事件上下文"""
eid = 0 eid = 0
"""事件编号""" """事件编号"""
@ -395,6 +463,7 @@ class EventContext:
def emit(event_name: str, **kwargs) -> EventContext: def emit(event_name: str, **kwargs) -> EventContext:
"""触发事件""" """触发事件"""
import pkg.utils.context as context import pkg.utils.context as context
if context.get_plugin_host() is None: if context.get_plugin_host() is None:
return None return None
return context.get_plugin_host().emit(event_name, **kwargs) return context.get_plugin_host().emit(event_name, **kwargs)
@ -443,9 +512,10 @@ class PluginHost:
event_context = EventContext(event_name) event_context = EventContext(event_name)
logging.debug("触发事件: {} ({})".format(event_name, event_context.eid)) logging.debug("触发事件: {} ({})".format(event_name, event_context.eid))
emitted_plugins = []
for plugin in iter_plugins(): for plugin in iter_plugins():
if not plugin["enabled"]:
if not plugin['enabled']:
continue continue
# if plugin['instance'] is None: # if plugin['instance'] is None:
@ -457,9 +527,11 @@ class PluginHost:
# logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info())) # logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
# continue # 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 continue
emitted_plugins.append(plugin['name'])
hooks = [] hooks = []
if event_name in plugin["hooks"]: if event_name in plugin["hooks"]:
hooks = plugin["hooks"][event_name] hooks = plugin["hooks"][event_name]
@ -467,27 +539,44 @@ class PluginHost:
try: try:
already_prevented_default = event_context.is_prevented_default() already_prevented_default = event_context.is_prevented_default()
kwargs['host'] = context.get_plugin_host() kwargs["host"] = context.get_plugin_host()
kwargs['event'] = event_context kwargs["event"] = event_context
hook(plugin['instance'], **kwargs) hook(plugin["instance"], **kwargs)
if event_context.is_prevented_default() and not already_prevented_default: if (
logging.debug("插件 {} 已要求阻止事件 {} 的默认行为".format(plugin['name'], event_name)) event_context.is_prevented_default()
and not already_prevented_default
):
logging.debug(
"插件 {} 已要求阻止事件 {} 的默认行为".format(plugin["name"], event_name)
)
except Exception as e: except Exception as e:
logging.error("插件{}响应事件{}时发生错误".format(plugin['name'], event_name)) logging.error("插件{}响应事件{}时发生错误".format(plugin["name"], event_name))
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
# print("done:{}".format(plugin['name'])) # print("done:{}".format(plugin['name']))
if event_context.is_prevented_postorder(): if event_context.is_prevented_postorder():
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin['name'])) logging.debug("插件 {} 阻止了后序插件的执行".format(plugin["name"]))
break break
logging.debug("事件 {} ({}) 处理完毕,返回值: {}".format(event_name, event_context.eid, logging.debug(
event_context.__return_value__)) "事件 {} ({}) 处理完毕,返回值: {}".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 return event_context
if __name__ == "__main__": if __name__ == "__main__":
pass pass

View File

@ -4,11 +4,10 @@ import pkgutil
import traceback import traceback
import json import json
__command_list__ = {}
import tips as tips_custom import tips as tips_custom
__command_list__ = {}
"""命令树 """命令树
结构 结构

View File

@ -125,6 +125,10 @@ class QQBotManager:
else: else:
self.adapter = context.get_qqbot_manager().adapter self.adapter = context.get_qqbot_manager().adapter
self.bot_account_id = context.get_qqbot_manager().bot_account_id 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) context.set_qqbot_manager(self)

View File

View File

@ -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

View File

View File

@ -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,
}
}
)

View File

@ -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,
}
}
)

View File

@ -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,
}
}
)

35
pkg/utils/center/v2.py Normal file
View File

@ -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)

View File

@ -8,6 +8,7 @@ from ..openai import manager as openai_mgr
from ..qqbot import manager as qqbot_mgr from ..qqbot import manager as qqbot_mgr
from ..config import manager as config_mgr from ..config import manager as config_mgr
from ..plugin import host as plugin_host from ..plugin import host as plugin_host
from .center import v2 as center_v2
context = { context = {
@ -114,3 +115,16 @@ def get_thread_ctl() -> threadctl.ThreadCtl:
t: threadctl.ThreadCtl = context['pool_ctl'] t: threadctl.ThreadCtl = context['pool_ctl']
context_lock.release() context_lock.release()
return t 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

7
pkg/utils/platform.py Normal file
View File

@ -0,0 +1,7 @@
import os
import sys
def get_platform() -> str:
"""获取当前平台"""
return sys.platform

View File

@ -1,11 +1,15 @@
from __future__ import annotations
import datetime import datetime
import logging import logging
import os.path import os.path
import time
import requests import requests
from . import constants from . import constants
from . import network from . import network
from . import context
def check_dulwich_closure(): def check_dulwich_closure():
@ -107,7 +111,10 @@ def compare_version_str(v0: str, v1: str) -> int:
def update_all(cli: bool = False) -> bool: def update_all(cli: bool = False) -> bool:
"""检查更新并下载源码""" """检查更新并下载源码"""
start_time = time.time()
current_tag = get_current_tag() current_tag = get_current_tag()
old_tag = current_tag
rls_list = get_release_list() rls_list = get_release_list()
@ -200,6 +207,13 @@ def update_all(cli: bool = False) -> bool:
with open("current_tag", "w") as f: with open("current_tag", "w") as f:
f.write(current_tag) 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: if not cli:
import pkg.utils.context import pkg.utils.context

View File

@ -11,3 +11,4 @@ nakuru-project-idk
CallingGPT CallingGPT
tiktoken tiktoken
PyYaml PyYaml
aiohttp

View File

@ -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)