diff --git a/.gitignore b/.gitignore index 690818a8..7ee4d84d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ botpy.log data.db configs/session configs/config.yaml +**/.DS_Store +temp diff --git a/addons/dashboard/server.py b/addons/dashboard/server.py index 555ad750..645e3a78 100644 --- a/addons/dashboard/server.py +++ b/addons/dashboard/server.py @@ -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() diff --git a/app.py b/app.py deleted file mode 100644 index dc0f19c2..00000000 --- a/app.py +++ /dev/null @@ -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 - -# 当前 IP:117.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} \ No newline at end of file diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index 09227339..a76a2073 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -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: diff --git a/model/command/command.py b/model/command/command.py index 0d70f8ce..b167690e 100644 --- a/model/command/command.py +++ b/model/command/command.py @@ -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: 存储机器人的昵称 diff --git a/util/func_call.py b/util/function_calling/func_call.py similarity index 100% rename from util/func_call.py rename to util/function_calling/func_call.py diff --git a/util/gplugin.py b/util/function_calling/gplugin.py similarity index 99% rename from util/gplugin.py rename to util/function_calling/gplugin.py index 5795179f..6faff1ff 100644 --- a/util/gplugin.py +++ b/util/function_calling/gplugin.py @@ -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 diff --git a/util/general_utils.py b/util/general_utils.py index db8185f9..de68f278 100644 --- a/util/general_utils.py +++ b/util/general_utils.py @@ -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]) \ No newline at end of file + 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 \ No newline at end of file diff --git a/util/plugin_util.py b/util/plugin_util.py index b20e15d2..ef631710 100644 --- a/util/plugin_util.py +++ b/util/plugin_util.py @@ -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 - \ No newline at end of file + +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 \ No newline at end of file