464 lines
18 KiB
Python
464 lines
18 KiB
Python
import traceback
|
|
import aiohttp
|
|
import os
|
|
|
|
import ssl
|
|
import certifi
|
|
|
|
from .route import Route, Response, RouteContext
|
|
from astrbot.core import logger
|
|
from quart import request
|
|
from astrbot.core.star.star_manager import PluginManager
|
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
from astrbot.core.star.star_handler import star_handlers_registry
|
|
from astrbot.core.star.filter.command import CommandFilter
|
|
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
|
from astrbot.core.star.filter.permission import PermissionTypeFilter
|
|
from astrbot.core.star.filter.regex import RegexFilter
|
|
from astrbot.core.star.star_handler import EventType
|
|
from astrbot.core import DEMO_MODE
|
|
|
|
try:
|
|
import nh3
|
|
except ImportError:
|
|
logger.warning("未安装 nh3 库,无法清理插件 README.md 中的 HTML 标签。")
|
|
nh3 = None
|
|
|
|
|
|
class PluginRoute(Route):
|
|
def __init__(
|
|
self,
|
|
context: RouteContext,
|
|
core_lifecycle: AstrBotCoreLifecycle,
|
|
plugin_manager: PluginManager,
|
|
) -> None:
|
|
super().__init__(context)
|
|
self.routes = {
|
|
"/plugin/get": ("GET", self.get_plugins),
|
|
"/plugin/install": ("POST", self.install_plugin),
|
|
"/plugin/install-upload": ("POST", self.install_plugin_upload),
|
|
"/plugin/update": ("POST", self.update_plugin),
|
|
"/plugin/uninstall": ("POST", self.uninstall_plugin),
|
|
"/plugin/market_list": ("GET", self.get_online_plugins),
|
|
"/plugin/off": ("POST", self.off_plugin),
|
|
"/plugin/on": ("POST", self.on_plugin),
|
|
"/plugin/reload": ("POST", self.reload_plugins),
|
|
"/plugin/readme": ("GET", self.get_plugin_readme),
|
|
"/plugin/platform_enable/get": ("GET", self.get_plugin_platform_enable),
|
|
"/plugin/platform_enable/set": ("POST", self.set_plugin_platform_enable),
|
|
}
|
|
self.core_lifecycle = core_lifecycle
|
|
self.plugin_manager = plugin_manager
|
|
self.register_routes()
|
|
|
|
self.translated_event_type = {
|
|
EventType.AdapterMessageEvent: "平台消息下发时",
|
|
EventType.OnLLMRequestEvent: "LLM 请求时",
|
|
EventType.OnLLMResponseEvent: "LLM 响应后",
|
|
EventType.OnDecoratingResultEvent: "回复消息前",
|
|
EventType.OnCallingFuncToolEvent: "函数工具",
|
|
EventType.OnAfterMessageSentEvent: "发送消息后",
|
|
}
|
|
|
|
async def reload_plugins(self):
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
data = await request.json
|
|
plugin_name = data.get("name", None)
|
|
try:
|
|
success, message = await self.plugin_manager.reload(plugin_name)
|
|
if not success:
|
|
return Response().error(message).__dict__
|
|
return Response().ok(None, "重载成功。").__dict__
|
|
except Exception as e:
|
|
logger.error(f"/api/plugin/reload: {traceback.format_exc()}")
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def get_online_plugins(self):
|
|
custom = request.args.get("custom_registry")
|
|
|
|
if custom:
|
|
urls = [custom]
|
|
else:
|
|
urls = ["https://api.soulter.top/astrbot/plugins"]
|
|
|
|
# 新增:创建 SSL 上下文,使用 certifi 提供的根证书
|
|
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
|
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
|
for url in urls:
|
|
try:
|
|
async with aiohttp.ClientSession(
|
|
trust_env=True, connector=connector
|
|
) as session:
|
|
async with session.get(url) as response:
|
|
if response.status == 200:
|
|
result = await response.json()
|
|
return Response().ok(result).__dict__
|
|
else:
|
|
logger.error(f"请求 {url} 失败,状态码:{response.status}")
|
|
except Exception as e:
|
|
logger.error(f"请求 {url} 失败,错误:{e}")
|
|
|
|
return Response().error("获取插件列表失败").__dict__
|
|
|
|
async def get_plugins(self):
|
|
_plugin_resp = []
|
|
plugin_name = request.args.get("name")
|
|
for plugin in self.plugin_manager.context.get_all_stars():
|
|
if plugin_name and plugin.name != plugin_name:
|
|
continue
|
|
_t = {
|
|
"name": plugin.name,
|
|
"repo": "" if plugin.repo is None else plugin.repo,
|
|
"author": plugin.author,
|
|
"desc": plugin.desc,
|
|
"version": plugin.version,
|
|
"reserved": plugin.reserved,
|
|
"activated": plugin.activated,
|
|
"online_vesion": "",
|
|
"handlers": await self.get_plugin_handlers_info(
|
|
plugin.star_handler_full_names
|
|
),
|
|
}
|
|
_plugin_resp.append(_t)
|
|
return (
|
|
Response()
|
|
.ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info)
|
|
.__dict__
|
|
)
|
|
|
|
async def get_plugin_handlers_info(self, handler_full_names: list[str]):
|
|
"""解析插件行为"""
|
|
handlers = []
|
|
|
|
for handler_full_name in handler_full_names:
|
|
info = {}
|
|
handler = star_handlers_registry.star_handlers_map.get(
|
|
handler_full_name, None
|
|
)
|
|
if handler is None:
|
|
continue
|
|
info["event_type"] = handler.event_type.name
|
|
info["event_type_h"] = self.translated_event_type.get(
|
|
handler.event_type, handler.event_type.name
|
|
)
|
|
info["handler_full_name"] = handler.handler_full_name
|
|
info["desc"] = handler.desc
|
|
info["handler_name"] = handler.handler_name
|
|
|
|
if handler.event_type == EventType.AdapterMessageEvent:
|
|
# 处理平台适配器消息事件
|
|
has_admin = False
|
|
for filter in (
|
|
handler.event_filters
|
|
): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高
|
|
if isinstance(filter, CommandFilter):
|
|
info["type"] = "指令"
|
|
info["cmd"] = (
|
|
f"{filter.parent_command_names[0]} {filter.command_name}"
|
|
)
|
|
info["cmd"] = info["cmd"].strip()
|
|
if (
|
|
self.core_lifecycle.astrbot_config["wake_prefix"]
|
|
and len(self.core_lifecycle.astrbot_config["wake_prefix"])
|
|
> 0
|
|
):
|
|
info["cmd"] = (
|
|
f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}"
|
|
)
|
|
elif isinstance(filter, CommandGroupFilter):
|
|
info["type"] = "指令组"
|
|
info["cmd"] = filter.get_complete_command_names()[0]
|
|
info["cmd"] = info["cmd"].strip()
|
|
info["sub_command"] = filter.print_cmd_tree(
|
|
filter.sub_command_filters
|
|
)
|
|
if (
|
|
self.core_lifecycle.astrbot_config["wake_prefix"]
|
|
and len(self.core_lifecycle.astrbot_config["wake_prefix"])
|
|
> 0
|
|
):
|
|
info["cmd"] = (
|
|
f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}"
|
|
)
|
|
elif isinstance(filter, RegexFilter):
|
|
info["type"] = "正则匹配"
|
|
info["cmd"] = filter.regex_str
|
|
elif isinstance(filter, PermissionTypeFilter):
|
|
has_admin = True
|
|
info["has_admin"] = has_admin
|
|
if "cmd" not in info:
|
|
info["cmd"] = "未知"
|
|
if "type" not in info:
|
|
info["type"] = "事件监听器"
|
|
else:
|
|
info["cmd"] = "自动触发"
|
|
info["type"] = "无"
|
|
|
|
if not info["desc"]:
|
|
info["desc"] = "无描述"
|
|
|
|
handlers.append(info)
|
|
|
|
return handlers
|
|
|
|
async def install_plugin(self):
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
post_data = await request.json
|
|
repo_url = post_data["url"]
|
|
|
|
proxy: str = post_data.get("proxy", None)
|
|
if proxy:
|
|
proxy = proxy.removesuffix("/")
|
|
|
|
try:
|
|
logger.info(f"正在安装插件 {repo_url}")
|
|
plugin_info = await self.plugin_manager.install_plugin(repo_url, proxy)
|
|
# self.core_lifecycle.restart()
|
|
logger.info(f"安装插件 {repo_url} 成功。")
|
|
return Response().ok(plugin_info, "安装成功。").__dict__
|
|
except Exception as e:
|
|
logger.error(traceback.format_exc())
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def install_plugin_upload(self):
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
try:
|
|
file = await request.files
|
|
file = file["file"]
|
|
logger.info(f"正在安装用户上传的插件 {file.filename}")
|
|
file_path = f"data/temp/{file.filename}"
|
|
await file.save(file_path)
|
|
plugin_info = await self.plugin_manager.install_plugin_from_file(file_path)
|
|
# self.core_lifecycle.restart()
|
|
logger.info(f"安装插件 {file.filename} 成功")
|
|
return Response().ok(plugin_info, "安装成功。").__dict__
|
|
except Exception as e:
|
|
logger.error(traceback.format_exc())
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def uninstall_plugin(self):
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
post_data = await request.json
|
|
plugin_name = post_data["name"]
|
|
try:
|
|
logger.info(f"正在卸载插件 {plugin_name}")
|
|
await self.plugin_manager.uninstall_plugin(plugin_name)
|
|
logger.info(f"卸载插件 {plugin_name} 成功")
|
|
return Response().ok(None, "卸载成功").__dict__
|
|
except Exception as e:
|
|
logger.error(traceback.format_exc())
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def update_plugin(self):
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
post_data = await request.json
|
|
plugin_name = post_data["name"]
|
|
proxy: str = post_data.get("proxy", None)
|
|
try:
|
|
logger.info(f"正在更新插件 {plugin_name}")
|
|
await self.plugin_manager.update_plugin(plugin_name, proxy)
|
|
# self.core_lifecycle.restart()
|
|
await self.plugin_manager.reload(plugin_name)
|
|
logger.info(f"更新插件 {plugin_name} 成功。")
|
|
return Response().ok(None, "更新成功。").__dict__
|
|
except Exception as e:
|
|
logger.error(f"/api/plugin/update: {traceback.format_exc()}")
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def off_plugin(self):
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
post_data = await request.json
|
|
plugin_name = post_data["name"]
|
|
try:
|
|
await self.plugin_manager.turn_off_plugin(plugin_name)
|
|
logger.info(f"停用插件 {plugin_name} 。")
|
|
return Response().ok(None, "停用成功。").__dict__
|
|
except Exception as e:
|
|
logger.error(f"/api/plugin/off: {traceback.format_exc()}")
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def on_plugin(self):
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
post_data = await request.json
|
|
plugin_name = post_data["name"]
|
|
try:
|
|
await self.plugin_manager.turn_on_plugin(plugin_name)
|
|
logger.info(f"启用插件 {plugin_name} 。")
|
|
return Response().ok(None, "启用成功。").__dict__
|
|
except Exception as e:
|
|
logger.error(f"/api/plugin/on: {traceback.format_exc()}")
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def get_plugin_readme(self):
|
|
if not nh3:
|
|
return Response().error("未安装 nh3 库").__dict__
|
|
|
|
plugin_name = request.args.get("name")
|
|
logger.debug(f"正在获取插件 {plugin_name} 的README文件内容")
|
|
|
|
if not plugin_name:
|
|
logger.warning("插件名称为空")
|
|
return Response().error("插件名称不能为空").__dict__
|
|
|
|
plugin_obj = None
|
|
for plugin in self.plugin_manager.context.get_all_stars():
|
|
if plugin.name == plugin_name:
|
|
plugin_obj = plugin
|
|
break
|
|
|
|
if not plugin_obj:
|
|
logger.warning(f"插件 {plugin_name} 不存在")
|
|
return Response().error(f"插件 {plugin_name} 不存在").__dict__
|
|
|
|
plugin_dir = os.path.join(
|
|
self.plugin_manager.plugin_store_path, plugin_obj.root_dir_name
|
|
)
|
|
|
|
if not os.path.isdir(plugin_dir):
|
|
logger.warning(f"无法找到插件目录: {plugin_dir}")
|
|
return Response().error(f"无法找到插件 {plugin_name} 的目录").__dict__
|
|
|
|
readme_path = os.path.join(plugin_dir, "README.md")
|
|
|
|
if not os.path.isfile(readme_path):
|
|
logger.warning(f"插件 {plugin_name} 没有README文件")
|
|
return Response().error(f"插件 {plugin_name} 没有README文件").__dict__
|
|
|
|
try:
|
|
with open(readme_path, "r", encoding="utf-8") as f:
|
|
readme_content = f.read()
|
|
|
|
cleaned_content = nh3.clean(readme_content)
|
|
|
|
return (
|
|
Response()
|
|
.ok({"content": cleaned_content}, "成功获取README内容")
|
|
.__dict__
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"/api/plugin/readme: {traceback.format_exc()}")
|
|
return Response().error(f"读取README文件失败: {str(e)}").__dict__
|
|
|
|
async def get_plugin_platform_enable(self):
|
|
"""获取插件在各平台的可用性配置"""
|
|
try:
|
|
platform_enable = self.core_lifecycle.astrbot_config.get(
|
|
"platform_settings", {}
|
|
).get("plugin_enable", {})
|
|
|
|
# 获取所有可用平台
|
|
platforms = []
|
|
|
|
for platform in self.core_lifecycle.astrbot_config.get("platform", []):
|
|
platform_type = platform.get("type", "")
|
|
platform_id = platform.get("id", "")
|
|
|
|
platforms.append({
|
|
"name": platform_id, # 使用type作为name,这是系统内部使用的平台名称
|
|
"id": platform_id, # 保留id字段以便前端可以显示
|
|
"type": platform_type,
|
|
"display_name": f"{platform_type}({platform_id})",
|
|
})
|
|
|
|
adjusted_platform_enable = {}
|
|
for platform_id, plugins in platform_enable.items():
|
|
adjusted_platform_enable[platform_id] = plugins
|
|
|
|
# 获取所有插件,包括系统内部插件
|
|
plugins = []
|
|
for plugin in self.plugin_manager.context.get_all_stars():
|
|
plugins.append({
|
|
"name": plugin.name,
|
|
"desc": plugin.desc,
|
|
"reserved": plugin.reserved, # 添加reserved标志
|
|
})
|
|
|
|
logger.debug(
|
|
f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
|
|
)
|
|
|
|
return (
|
|
Response()
|
|
.ok({
|
|
"platforms": platforms,
|
|
"plugins": plugins,
|
|
"platform_enable": adjusted_platform_enable,
|
|
})
|
|
.__dict__
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"/api/plugin/platform_enable/get: {traceback.format_exc()}")
|
|
return Response().error(str(e)).__dict__
|
|
|
|
async def set_plugin_platform_enable(self):
|
|
"""设置插件在各平台的可用性配置"""
|
|
if DEMO_MODE:
|
|
return (
|
|
Response()
|
|
.error("You are not permitted to do this operation in demo mode")
|
|
.__dict__
|
|
)
|
|
|
|
try:
|
|
data = await request.json
|
|
platform_enable = data.get("platform_enable", {})
|
|
|
|
# 更新配置
|
|
config = self.core_lifecycle.astrbot_config
|
|
platform_settings = config.get("platform_settings", {})
|
|
platform_settings["plugin_enable"] = platform_enable
|
|
config["platform_settings"] = platform_settings
|
|
config.save_config()
|
|
|
|
# 更新插件的平台兼容性缓存
|
|
await self.plugin_manager.update_all_platform_compatibility()
|
|
|
|
logger.info(f"插件平台可用性配置已更新: {platform_enable}")
|
|
|
|
return Response().ok(None, "插件平台可用性配置已更新").__dict__
|
|
except Exception as e:
|
|
logger.error(f"/api/plugin/platform_enable/set: {traceback.format_exc()}")
|
|
return Response().error(str(e)).__dict__
|