perf: 优化插件代码结构。

fix: 修复卸载插件之后,线程无限自旋的问题。
This commit is contained in:
Soulter
2023-12-21 14:00:29 +08:00
parent 1b1e558a3b
commit 57bde33bfe
9 changed files with 178 additions and 299 deletions

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ botpy.log
data.db
configs/session
configs/config.yaml
**/.DS_Store
temp

View File

@@ -155,6 +155,14 @@ class AstrBotDashBoard():
message="",
data=_plugin_resp
).__dict__
@self.dashboard_be.post("/api/extensions/install")
def install_plugin():
pass
@self.dashboard_be.post("/api/extensions/uninstall")
def uninstall_plugin():
pass
def register(self, name: str):
def decorator(func):
@@ -163,7 +171,9 @@ class AstrBotDashBoard():
return decorator
def run(self):
gu.log(f"\n\n==================\n您可以访问:\n\thttp://localhost:6185/\n来登录可视化面板。\n==================\n\n", tag="可视化面板")
ip_address = gu.get_local_ip_addresses()
ip_str = f"http://{ip_address}:6185\n\thttp://localhost:6185"
gu.log(f"\n\n==================\n您可以访问:\n\n\t{ip_str}\n\n来登录可视化面板。\n注意: 所有配置项现已全量迁移至 cmd_config.json 文件下。您可以登录可视化面板在线修改配置。\n==================\n\n", tag="可视化面板")
# self.dashboard_be.run(host="0.0.0.0", port=6185)
http_server = make_server('0.0.0.0', 6185, self.dashboard_be)
http_server.serve_forever()

159
app.py
View File

@@ -1,159 +0,0 @@
# fastapi
from typing import Union
from fastapi import FastAPI, Request
import pymysql
import re
import requests
from pydantic import BaseModel
import os
import threading
import time
import json
app = FastAPI()
db = pymysql.connect(
host='localhost',
port=3306,
user='sorbot',
password='sorbot123.,.',
database='sorbot'
)
cnt_10m = 0
cnt_ips_10m = 0
url = 'http://localhost:9091/metrics/job/sorbot'
ip_ls = []
class Tick(BaseModel):
version: str
count: int
ip: str
others: str
def get_from_db(ip: str):
db.ping()
cursor = db.cursor()
sql = f"SELECT * FROM bases WHERE ip = '{ip}'"
print(sql)
cursor.execute(sql)
result = cursor.fetchone()
cursor.close()
return result
# 当前 IP117.133.151.135 来自于:中国 北京 北京 移动
# def filter_ip(ori: str):
# # 正则表达式得到ip
# ip = re.findall(r'\b(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b', ori)[0]
# return ip
def thread_total_cnt():
global cnt_10m, cnt_ips_10m
while True:
try:
metric_data = 'qqc_total ' + str(cnt_10m)
cmd = "echo " + metric_data + " | curl --data-binary @- " + url
print(cmd)
res = os.system('echo \'' + metric_data + '\' | curl --data-binary @- ' + url)
cnt_10m = 0
except Exception as e:
print(e)
try:
metric_data = 'qqc_ips_total ' + str(cnt_ips_10m)
cmd = "echo " + metric_data + " | curl --data-binary @- " + url
print(cmd)
res = os.system('echo \'' + metric_data + '\' | curl --data-binary @- ' + url)
cnt_ips_10m = 0
ip_ls.clear()
except Exception as e:
print(e)
print('thread_total_cnt sleep 5 min')
time.sleep(610)
# 启动线程
t = threading.Thread(target=thread_total_cnt, daemon=True)
t.start()
def push_to_gateway(tick: Tick):
# push to gateway
ip = tick.ip
others = json.loads(tick.others)
admin = others['admin']
if admin is None:
admin = "unknown"
if tick.version is None:
tick.version = "unknown"
metric_data = 'qqc_ips{version="' + tick.version + '",ip="' + ip + '",admin="' + admin + '"} ' + str(tick.count)
print(metric_data)
# r = requests.post(url, data=metric_data.encode('utf-8'), )
# print(r.text)
cmd = "echo " + metric_data + " | curl --data-binary @- " + url
print(cmd)
res = os.system('echo \'' + metric_data + '\' | curl --data-binary @- ' + url)
@app.post("/upload")
def upload(tick: Tick, request: Request):
global cnt_ips_10m, cnt_10m
# ip = filter_ip(tick.ip)
ip = request.client.host
print("IP", ip, "|", "TICK", tick.__dict__)
tick.ip = ip
result = get_from_db(ip)
try:
if result is None:
# 插入数据库
db.ping()
cursor = db.cursor()
sql = f"INSERT INTO bases (ip, count, version, others) VALUES ('{ip}', {tick.count}, '{tick.version}', '{tick.others}')"
cursor.execute(sql)
db.commit()
cursor.close()
else:
# 更新数据库
db.ping()
cursor = db.cursor()
cnt = result[1] + tick.count
sql = f"UPDATE bases SET count = {cnt}, version = '{tick.version}', others = '{tick.others}' WHERE ip = '{ip}'"
cursor.execute(sql)
db.commit()
cursor.close()
# print(result)
except Exception as e:
print(e)
raise e
try:
# insert 到 clicks 表。结构是total_cnt, ip, timestamp, version
db.ping()
cursor = db.cursor()
sql = f"INSERT INTO clicks (total_cnt, ip, timestamp, version) VALUES ({tick.count}, '{ip}', {int(time.time())}, '{tick.version}')"
cursor.execute(sql)
db.commit()
cursor.close()
except Exception as e:
print(e)
raise e
cnt_10m += tick.count
if ip not in ip_ls:
cnt_ips_10m += 1
ip_ls.append(ip)
# push to gateway
push_to_gateway(tick)
return {
"code": 200,
"status": "ok",
}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}

View File

@@ -33,7 +33,8 @@ from model.provider.provider import Provider
from model.command.command import Command
from util import general_utils as gu
from util.cmd_config import CmdConfig as cc
import util.gplugin as gplugin
import util.function_calling.gplugin as gplugin
import util.plugin_util as putil
from PIL import Image as PILImage
import io
import traceback
@@ -357,7 +358,7 @@ def initBot(cfg):
gu.log("--------加载插件--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
# 加载插件
_command = Command(None, _global_object)
ok, err = _command.plugin_reload(_global_object.cached_plugins)
ok, err = putil.plugin_reload(_global_object.cached_plugins)
if ok:
gu.log("加载插件完成", gu.LEVEL_INFO)
else:

View File

@@ -118,46 +118,6 @@ class Command:
return None
except BaseException as e:
raise e
def plugin_reload(self, cached_plugins: dict, target: str = None, all: bool = False):
plugins = self.get_plugin_modules()
if plugins is None:
return False, "未找到任何插件模块"
fail_rec = ""
for plugin in plugins:
try:
p = plugin['module']
root_dir_name = plugin['pname']
if p not in cached_plugins or p == target or all:
module = __import__("addons.plugins." + root_dir_name + "." + p, fromlist=[p])
if p in cached_plugins:
module = importlib.reload(module)
cls = putil.get_classes(p, module)
obj = getattr(module, cls[0])()
try:
info = obj.info()
if 'name' not in info or 'desc' not in info or 'version' not in info or 'author' not in info:
fail_rec += f"载入插件{p}失败,原因: 插件信息不完整\n"
continue
if isinstance(info, dict) == False:
fail_rec += f"载入插件{p}失败,原因: 插件信息格式不正确\n"
continue
except BaseException as e:
fail_rec += f"调用插件{p} info失败, 原因: {str(e)}\n"
continue
cached_plugins[info['name']] = {
"module": module,
"clsobj": obj,
"info": info,
"name": info['name'],
"root_dir_name": root_dir_name,
}
except BaseException as e:
fail_rec += f"加载{p}插件出现问题,原因 {str(e)}\n"
if fail_rec == "":
return True, None
else:
return False, fail_rec
'''
插件指令
@@ -170,81 +130,28 @@ class Command:
p = gu.create_text_image("【插件指令面板】", "安装插件: \nplugin i 插件Github地址\n卸载插件: \nplugin d 插件名 \n重载插件: \nplugin reload\n查看插件列表:\nplugin l\n更新插件: plugin u 插件名\n")
return True, [Image.fromFileSystem(p)], "plugin"
else:
ppath = ""
if os.path.exists("addons/plugins"):
ppath = "addons/plugins"
elif os.path.exists("QQChannelChatGPT/addons/plugins"):
ppath = "QQChannelChatGPT/addons/plugins"
else:
return False, "未找到插件目录", "plugin"
if l[1] == "i":
if role != "admin":
return False, f"你的身份组{role}没有权限安装插件", "plugin"
try:
# 删除末尾的/
if l[2].endswith("/"):
l[2] = l[2][:-1]
# 得到url的最后一段
d = l[2].split("/")[-1]
# 转换非法字符:-
d = d.replace("-", "_")
# 创建文件夹
plugin_path = os.path.join(ppath, d)
if os.path.exists(plugin_path):
shutil.rmtree(plugin_path)
os.mkdir(plugin_path)
Repo.clone_from(l[2],to_path=plugin_path,branch='master')
# 读取插件的requirements.txt
if os.path.exists(os.path.join(plugin_path, "requirements.txt")):
mm = pipmain(['install', '-r', os.path.join(plugin_path, "requirements.txt")])
if mm != 0:
return False, "插件依赖安装失败需要您手动pip安装对应插件的依赖。", "plugin"
# 加载没缓存的插件
ok, err = self.plugin_reload(cached_plugins, target=d)
if ok:
return True, "插件拉取并载入成功~", "plugin"
else:
# if os.path.exists(plugin_path):
# shutil.rmtree(plugin_path)
return False, f"插件拉取载入失败。\n跟踪: \n{err}", "plugin"
putil.install_plugin(l[2], cached_plugins)
return True, "插件拉取并载入成功~", "plugin"
except BaseException as e:
return False, f"拉取插件失败,原因: {str(e)}", "plugin"
elif l[1] == "d":
if role != "admin":
return False, f"你的身份组{role}没有权限删除插件", "plugin"
if l[2] not in cached_plugins:
return False, "未找到该插件", "plugin"
try:
root_dir_name = cached_plugins[l[2]]["root_dir_name"]
self.remove_dir(os.path.join(ppath, root_dir_name))
del cached_plugins[l[2]]
putil.uninstall_plugin(l[2], cached_plugins)
return True, "插件卸载成功~", "plugin"
except BaseException as e:
return False, f"卸载插件失败,原因: {str(e)}", "plugin"
elif l[1] == "u":
if l[2] not in cached_plugins:
return False, "未找到该插件", "plugin"
root_dir_name = cached_plugins[l[2]]["root_dir_name"]
plugin_path = os.path.join(ppath, root_dir_name)
try:
repo = Repo(path = plugin_path)
repo.remotes.origin.pull()
# 读取插件的requirements.txt
if os.path.exists(os.path.join(plugin_path, "requirements.txt")):
mm = pipmain(['install', '-r', os.path.join(plugin_path, "requirements.txt")])
if mm != 0:
return False, "插件依赖安装失败需要您手动pip安装对应插件的依赖。", "plugin"
ok, err = self.plugin_reload(cached_plugins, target=l[2])
if ok:
return True, "\n更新插件成功!!", "plugin"
else:
return False, "更新插件成功,但是重载插件失败。\n问题跟踪: \n"+err, "plugin"
putil.update_plugin(l[2], cached_plugins)
return True, "\n更新插件成功!!", "plugin"
except BaseException as e:
return False, "更新插件失败, 请使用plugin i指令覆盖安装", "plugin"
return False, f"更新插件失败,原因: {str(e)}\n建议: 使用 plugin i 指令进行覆盖安装(插件数据可能会丢失)", "plugin"
elif l[1] == "l":
try:
plugin_list_info = "\n".join([f"{k}: \n名称: {v['info']['name']}\n简介: {v['info']['desc']}\n版本: {v['info']['version']}\n作者: {v['info']['author']}\n" for k, v in cached_plugins.items()])
@@ -262,45 +169,34 @@ class Command:
return False, "未找到该插件", "plugin"
except BaseException as e:
return False, f"获取插件信息失败,原因: {str(e)}", "plugin"
elif l[1] == "reload":
if role != "admin":
return False, f"你的身份组{role}没有权限重载插件", "plugin"
for plugin in cached_plugins:
try:
print(f"更新插件 {plugin} 依赖...")
plugin_path = os.path.join(ppath, cached_plugins[plugin]["root_dir_name"])
if os.path.exists(os.path.join(plugin_path, "requirements.txt")):
mm = pipmain(['install', '-r', os.path.join(plugin_path, "requirements.txt"), "--quiet"])
if mm != 0:
return False, "插件依赖安装失败需要您手动pip安装对应插件的依赖。", "plugin"
except BaseException as e:
print(f"插件{plugin}依赖安装失败,原因: {str(e)}")
try:
ok, err = self.plugin_reload(cached_plugins, all = True)
if ok:
return True, "\n重载插件成功~", "plugin"
else:
# if os.path.exists(plugin_path):
# shutil.rmtree(plugin_path)
return False, f"插件重载失败。\n跟踪: \n{err}", "plugin"
except BaseException as e:
return False, f"插件重载失败,原因: {str(e)}", "plugin"
# elif l[1] == "reload":
# if role != "admin":
# return False, f"你的身份组{role}没有权限重载插件", "plugin"
# for plugin in cached_plugins:
# try:
# print(f"更新插件 {plugin} 依赖...")
# plugin_path = os.path.join(ppath, cached_plugins[plugin]["root_dir_name"])
# if os.path.exists(os.path.join(plugin_path, "requirements.txt")):
# mm = pipmain(['install', '-r', os.path.join(plugin_path, "requirements.txt"), "--quiet"])
# if mm != 0:
# return False, "插件依赖安装失败需要您手动pip安装对应插件的依赖。", "plugin"
# except BaseException as e:
# print(f"插件{plugin}依赖安装失败,原因: {str(e)}")
# try:
# ok, err = self.plugin_reload(cached_plugins, all = True)
# if ok:
# return True, "\n重载插件成功~", "plugin"
# else:
# # if os.path.exists(plugin_path):
# # shutil.rmtree(plugin_path)
# return False, f"插件重载失败。\n跟踪: \n{err}", "plugin"
# except BaseException as e:
# return False, f"插件重载失败,原因: {str(e)}", "plugin"
elif l[1] == "dev":
if role != "admin":
return False, f"你的身份组{role}没有权限开发者模式", "plugin"
return True, "cached_plugins: \n" + str(cached_plugins), "plugin"
def remove_dir(self, file_path):
while 1:
if not os.path.exists(file_path):
break
try:
shutil.rmtree(file_path)
except PermissionError as e:
err_file_path = str(e).split("\'", 2)[1]
if os.path.exists(err_file_path):
os.chmod(err_file_path, stat.S_IWUSR)
'''
nick: 存储机器人的昵称

View File

@@ -2,7 +2,7 @@ import requests
import util.general_utils as gu
from bs4 import BeautifulSoup
import time
from util.func_call import (
from util.function_calling.func_call import (
FuncCall,
FuncCallJsonFormatError,
FuncNotFoundError

View File

@@ -6,6 +6,7 @@ import os
import re
import requests
from util.cmd_config import CmdConfig
import socket
PLATFORM_GOCQ = 'gocq'
PLATFORM_QQCHAN = 'qqchan'
@@ -520,4 +521,14 @@ def try_migrate_config(old_config: dict):
if cc.get("qqbot", None) is None:
# 未迁移过
for k in old_config:
cc.put(k, old_config[k])
cc.put(k, old_config[k])
def get_local_ip_addresses():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
finally:
s.close()
return ip

View File

@@ -3,6 +3,16 @@
'''
import os
import inspect
try:
import git.exc
from git.repo import Repo
except ImportError:
pass
import shutil
from pip._internal import main as pipmain
import importlib
import stat
# 找出模块里所有的类名
def get_classes(p_name, arg):
@@ -31,4 +41,112 @@ def get_modules(path):
"module": file[:-3],
})
return modules
def get_plugin_store_path():
if os.path.exists("addons/plugins"):
return "addons/plugins"
elif os.path.exists("QQChannelChatGPT/addons/plugins"):
return "QQChannelChatGPT/addons/plugins"
elif os.path.exists("AstrBot/addons/plugins"):
return "AstrBot/addons/plugins"
else:
raise FileNotFoundError("插件文件夹不存在。")
def plugin_reload(self, cached_plugins: dict, target: str = None, all: bool = False):
plugins = self.get_plugin_modules()
if plugins is None:
return False, "未找到任何插件模块"
fail_rec = ""
for plugin in plugins:
try:
p = plugin['module']
root_dir_name = plugin['pname']
if p not in cached_plugins or p == target or all:
module = __import__("addons.plugins." + root_dir_name + "." + p, fromlist=[p])
if p in cached_plugins:
module = importlib.reload(module)
cls = get_classes(p, module)
obj = getattr(module, cls[0])()
try:
info = obj.info()
if 'name' not in info or 'desc' not in info or 'version' not in info or 'author' not in info:
fail_rec += f"载入插件{p}失败,原因: 插件信息不完整\n"
continue
if isinstance(info, dict) == False:
fail_rec += f"载入插件{p}失败,原因: 插件信息格式不正确\n"
continue
except BaseException as e:
fail_rec += f"调用插件{p} info失败, 原因: {str(e)}\n"
continue
cached_plugins[info['name']] = {
"module": module,
"clsobj": obj,
"info": info,
"name": info['name'],
"root_dir_name": root_dir_name,
}
except BaseException as e:
fail_rec += f"加载{p}插件出现问题,原因 {str(e)}\n"
if fail_rec == "":
return True, None
else:
return False, fail_rec
def install_plugin(repo_url: str, cached_plugins: dict):
ppath = get_plugin_store_path()
# 删除末尾的 /
if repo_url.endswith("/"):
repo_url = repo_url[:-1]
# 得到 url 的最后一段
d = repo_url.split("/")[-1]
# 转换非法字符:-
d = d.replace("-", "_")
# 创建文件夹
plugin_path = os.path.join(ppath, d)
if os.path.exists(plugin_path):
shutil.rmtree(plugin_path)
Repo.clone_from(repo_url, to_path=plugin_path, branch='master')
# 读取插件的requirements.txt
if os.path.exists(os.path.join(plugin_path, "requirements.txt")):
if pipmain(['install', '-r', os.path.join(plugin_path, "requirements.txt")]) != 0:
raise Exception("插件的依赖安装失败, 需要您手动 pip 安装对应插件的依赖。")
ok, err = plugin_reload(cached_plugins, target=d)
if not ok: raise Exception(err)
def uninstall_plugin(plugin_name: str, cached_plugins: dict):
if plugin_name not in cached_plugins:
raise Exception("插件不存在。")
root_dir_name = cached_plugins[plugin_name]["root_dir_name"]
ppath = get_plugin_store_path()
del cached_plugins[plugin_name]
if not remove_dir(os.path.join(ppath, root_dir_name)):
raise Exception("移除插件成功,但是删除插件文件夹失败。您可以手动删除该文件夹,位于 addons/plugins/ 下。")
def update_plugin(plugin_name: str, cached_plugins: dict):
if plugin_name not in cached_plugins:
raise Exception("插件不存在。")
ppath = get_plugin_store_path()
root_dir_name = cached_plugins[plugin_name]["root_dir_name"]
plugin_path = os.path.join(ppath, root_dir_name)
repo = Repo(path = plugin_path)
repo.remotes.origin.pull()
# 读取插件的requirements.txt
if os.path.exists(os.path.join(plugin_path, "requirements.txt")):
if pipmain(['install', '-r', os.path.join(plugin_path, "requirements.txt")]) != 0:
raise Exception("插件依赖安装失败, 需要您手动pip安装对应插件的依赖。")
ok, err = plugin_reload(cached_plugins, target=plugin_name)
if not ok: raise Exception(err)
def remove_dir(self, file_path) -> bool:
try_cnt = 50
while try_cnt > 0:
if not os.path.exists(file_path):
return False
try:
shutil.rmtree(file_path)
return True
except PermissionError as e:
err_file_path = str(e).split("\'", 2)[1]
if os.path.exists(err_file_path):
os.chmod(err_file_path, stat.S_IWUSR)
try_cnt -= 1