mirror of https://github.com/Mai-with-u/MaiBot.git
feat: 优化任务调度和插件管理,支持路径规范化及插件 ID 自动生成
parent
a1dd26d578
commit
513182067d
13
bot.py
13
bot.py
|
|
@ -235,12 +235,21 @@ if __name__ == "__main__":
|
|||
loop.run_until_complete(main_tasks)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# loop.run_until_complete(get_global_api().stop())
|
||||
logger.warning("收到中断信号,正在优雅关闭...")
|
||||
|
||||
# 取消主任务
|
||||
if 'main_tasks' in locals() and main_tasks and not main_tasks.done():
|
||||
main_tasks.cancel()
|
||||
try:
|
||||
loop.run_until_complete(main_tasks)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# 执行优雅关闭
|
||||
if loop and not loop.is_closed():
|
||||
try:
|
||||
loop.run_until_complete(graceful_shutdown())
|
||||
except Exception as ge: # 捕捉优雅关闭时可能发生的错误
|
||||
except Exception as ge:
|
||||
logger.error(f"优雅关闭时发生错误: {ge}")
|
||||
# 新增:检测外部请求关闭
|
||||
|
||||
|
|
|
|||
|
|
@ -8,18 +8,25 @@
|
|||
"url": "https://github.com/MaiM-with-u"
|
||||
},
|
||||
"license": "GPL-v3.0-or-later",
|
||||
|
||||
"host_application": {
|
||||
"min_version": "0.10.3"
|
||||
},
|
||||
"homepage_url": "https://github.com/SengokuCola/BetterFrequency",
|
||||
"repository_url": "https://github.com/SengokuCola/BetterFrequency",
|
||||
"keywords": ["frequency", "control", "talk_frequency", "plugin", "shortcut"],
|
||||
"categories": ["Chat", "Frequency", "Control"],
|
||||
|
||||
"keywords": [
|
||||
"frequency",
|
||||
"control",
|
||||
"talk_frequency",
|
||||
"plugin",
|
||||
"shortcut"
|
||||
],
|
||||
"categories": [
|
||||
"Chat",
|
||||
"Frequency",
|
||||
"Control"
|
||||
],
|
||||
"default_locale": "zh-CN",
|
||||
"locales_path": "_locales",
|
||||
|
||||
"plugin_info": {
|
||||
"is_built_in": false,
|
||||
"plugin_type": "frequency",
|
||||
|
|
@ -46,5 +53,6 @@
|
|||
"支持完整命令和简化命令",
|
||||
"快速操作支持"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "SengokuCola.BetterFrequency"
|
||||
}
|
||||
|
|
@ -8,18 +8,22 @@
|
|||
"url": "https://github.com/SengokuCola"
|
||||
},
|
||||
"license": "GPL-v3.0-or-later",
|
||||
|
||||
"host_application": {
|
||||
"min_version": "0.10.4"
|
||||
},
|
||||
"homepage_url": "https://github.com/SengokuCola/BetterEmoji",
|
||||
"repository_url": "https://github.com/SengokuCola/BetterEmoji",
|
||||
"keywords": ["emoji", "manage", "plugin"],
|
||||
"categories": ["Examples", "Tutorial"],
|
||||
|
||||
"keywords": [
|
||||
"emoji",
|
||||
"manage",
|
||||
"plugin"
|
||||
],
|
||||
"categories": [
|
||||
"Examples",
|
||||
"Tutorial"
|
||||
],
|
||||
"default_locale": "zh-CN",
|
||||
"locales_path": "_locales",
|
||||
|
||||
"plugin_info": {
|
||||
"is_built_in": false,
|
||||
"plugin_type": "emoji_manage",
|
||||
|
|
@ -33,8 +37,15 @@
|
|||
"type": "action",
|
||||
"name": "bye_greeting",
|
||||
"description": "向用户发送告别消息",
|
||||
"activation_modes": ["keyword"],
|
||||
"keywords": ["再见", "bye", "88", "拜拜"]
|
||||
"activation_modes": [
|
||||
"keyword"
|
||||
],
|
||||
"keywords": [
|
||||
"再见",
|
||||
"bye",
|
||||
"88",
|
||||
"拜拜"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
|
|
@ -49,5 +60,6 @@
|
|||
"配置文件示例",
|
||||
"新手教程代码"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "SengokuCola.BetterEmoji"
|
||||
}
|
||||
|
|
@ -8,18 +8,24 @@
|
|||
"url": "https://github.com/MaiM-with-u"
|
||||
},
|
||||
"license": "GPL-v3.0-or-later",
|
||||
|
||||
"host_application": {
|
||||
"min_version": "0.8.0"
|
||||
},
|
||||
"homepage_url": "https://github.com/MaiM-with-u/maibot",
|
||||
"repository_url": "https://github.com/MaiM-with-u/maibot",
|
||||
"keywords": ["demo", "example", "hello", "greeting", "tutorial"],
|
||||
"categories": ["Examples", "Tutorial"],
|
||||
|
||||
"keywords": [
|
||||
"demo",
|
||||
"example",
|
||||
"hello",
|
||||
"greeting",
|
||||
"tutorial"
|
||||
],
|
||||
"categories": [
|
||||
"Examples",
|
||||
"Tutorial"
|
||||
],
|
||||
"default_locale": "zh-CN",
|
||||
"locales_path": "_locales",
|
||||
|
||||
"plugin_info": {
|
||||
"is_built_in": false,
|
||||
"plugin_type": "example",
|
||||
|
|
@ -33,8 +39,15 @@
|
|||
"type": "action",
|
||||
"name": "bye_greeting",
|
||||
"description": "向用户发送告别消息",
|
||||
"activation_modes": ["keyword"],
|
||||
"keywords": ["再见", "bye", "88", "拜拜"]
|
||||
"activation_modes": [
|
||||
"keyword"
|
||||
],
|
||||
"keywords": [
|
||||
"再见",
|
||||
"bye",
|
||||
"88",
|
||||
"拜拜"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
|
|
@ -49,5 +62,6 @@
|
|||
"配置文件示例",
|
||||
"新手教程代码"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "MaiBot开发团队.maibot"
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ class MainSystem:
|
|||
|
||||
async def schedule_tasks(self):
|
||||
"""调度定时任务"""
|
||||
while True:
|
||||
try:
|
||||
tasks = [
|
||||
get_emoji_manager().start_periodic_check_register(),
|
||||
self.app.run(),
|
||||
|
|
@ -168,6 +168,9 @@ class MainSystem:
|
|||
tasks.append(self.webui_server.start())
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("调度任务已取消")
|
||||
raise
|
||||
|
||||
# async def forget_memory_task(self):
|
||||
# """记忆遗忘任务"""
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Body
|
|||
from typing import Any
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import Config, APIAdapterConfig, CONFIG_DIR
|
||||
from src.config.config import Config, APIAdapterConfig, CONFIG_DIR, PROJECT_ROOT
|
||||
from src.config.official_configs import (
|
||||
BotConfig,
|
||||
PersonalityConfig,
|
||||
|
|
@ -421,6 +421,38 @@ async def update_model_config_section(section_name: str, section_data: Any = Bod
|
|||
# ===== 适配器配置管理接口 =====
|
||||
|
||||
|
||||
def _normalize_adapter_path(path: str) -> str:
|
||||
"""将路径转换为绝对路径(如果是相对路径,则相对于项目根目录)"""
|
||||
if not path:
|
||||
return path
|
||||
|
||||
# 如果已经是绝对路径,直接返回
|
||||
if os.path.isabs(path):
|
||||
return path
|
||||
|
||||
# 相对路径,转换为相对于项目根目录的绝对路径
|
||||
return os.path.normpath(os.path.join(PROJECT_ROOT, path))
|
||||
|
||||
|
||||
def _to_relative_path(path: str) -> str:
|
||||
"""尝试将绝对路径转换为相对于项目根目录的相对路径,如果无法转换则返回原路径"""
|
||||
if not path or not os.path.isabs(path):
|
||||
return path
|
||||
|
||||
try:
|
||||
# 尝试获取相对路径
|
||||
rel_path = os.path.relpath(path, PROJECT_ROOT)
|
||||
# 如果相对路径不是以 .. 开头(说明文件在项目目录内),则返回相对路径
|
||||
if not rel_path.startswith('..'):
|
||||
return rel_path
|
||||
except (ValueError, TypeError):
|
||||
# 在 Windows 上,如果路径在不同驱动器,relpath 会抛出 ValueError
|
||||
pass
|
||||
|
||||
# 无法转换为相对路径,返回绝对路径
|
||||
return path
|
||||
|
||||
|
||||
@router.get("/adapter-config/path")
|
||||
async def get_adapter_config_path():
|
||||
"""获取保存的适配器配置文件路径"""
|
||||
|
|
@ -438,13 +470,19 @@ async def get_adapter_config_path():
|
|||
if not adapter_config_path:
|
||||
return {"success": True, "path": None}
|
||||
|
||||
# 将路径规范化为绝对路径
|
||||
abs_path = _normalize_adapter_path(adapter_config_path)
|
||||
|
||||
# 检查文件是否存在并返回最后修改时间
|
||||
if os.path.exists(adapter_config_path):
|
||||
if os.path.exists(abs_path):
|
||||
import datetime
|
||||
mtime = os.path.getmtime(adapter_config_path)
|
||||
mtime = os.path.getmtime(abs_path)
|
||||
last_modified = datetime.datetime.fromtimestamp(mtime).isoformat()
|
||||
return {"success": True, "path": adapter_config_path, "lastModified": last_modified}
|
||||
# 返回相对路径(如果可能)
|
||||
display_path = _to_relative_path(abs_path)
|
||||
return {"success": True, "path": display_path, "lastModified": last_modified}
|
||||
else:
|
||||
# 文件不存在,返回原路径
|
||||
return {"success": True, "path": adapter_config_path, "lastModified": None}
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -471,15 +509,21 @@ async def save_adapter_config_path(data: dict[str, str] = Body(...)):
|
|||
else:
|
||||
webui_data = {}
|
||||
|
||||
# 将路径规范化为绝对路径
|
||||
abs_path = _normalize_adapter_path(path)
|
||||
|
||||
# 尝试转换为相对路径保存(如果文件在项目目录内)
|
||||
save_path = _to_relative_path(abs_path)
|
||||
|
||||
# 更新路径
|
||||
webui_data["adapter_config_path"] = path
|
||||
webui_data["adapter_config_path"] = save_path
|
||||
|
||||
# 保存
|
||||
os.makedirs("data", exist_ok=True)
|
||||
with open(webui_data_path, "w", encoding="utf-8") as f:
|
||||
json.dump(webui_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"适配器配置路径已保存: {path}")
|
||||
logger.info(f"适配器配置路径已保存: {save_path}(绝对路径: {abs_path})")
|
||||
return {"success": True, "message": "路径已保存"}
|
||||
|
||||
except HTTPException:
|
||||
|
|
@ -496,19 +540,22 @@ async def get_adapter_config(path: str):
|
|||
if not path:
|
||||
raise HTTPException(status_code=400, detail="路径参数不能为空")
|
||||
|
||||
# 将路径规范化为绝对路径
|
||||
abs_path = _normalize_adapter_path(path)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(path):
|
||||
if not os.path.exists(abs_path):
|
||||
raise HTTPException(status_code=404, detail=f"配置文件不存在: {path}")
|
||||
|
||||
# 检查文件扩展名
|
||||
if not path.endswith(".toml"):
|
||||
if not abs_path.endswith(".toml"):
|
||||
raise HTTPException(status_code=400, detail="只支持 .toml 格式的配置文件")
|
||||
|
||||
# 读取文件内容
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
with open(abs_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
logger.info(f"已读取适配器配置: {path}")
|
||||
logger.info(f"已读取适配器配置: {path} (绝对路径: {abs_path})")
|
||||
return {"success": True, "content": content}
|
||||
|
||||
except HTTPException:
|
||||
|
|
@ -530,8 +577,11 @@ async def save_adapter_config(data: dict[str, str] = Body(...)):
|
|||
if content is None:
|
||||
raise HTTPException(status_code=400, detail="配置内容不能为空")
|
||||
|
||||
# 将路径规范化为绝对路径
|
||||
abs_path = _normalize_adapter_path(path)
|
||||
|
||||
# 检查文件扩展名
|
||||
if not path.endswith(".toml"):
|
||||
if not abs_path.endswith(".toml"):
|
||||
raise HTTPException(status_code=400, detail="只支持 .toml 格式的配置文件")
|
||||
|
||||
# 验证 TOML 格式
|
||||
|
|
@ -542,13 +592,15 @@ async def save_adapter_config(data: dict[str, str] = Body(...)):
|
|||
raise HTTPException(status_code=400, detail=f"TOML 格式错误: {str(e)}")
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(path) if os.path.dirname(path) else ".", exist_ok=True)
|
||||
dir_path = os.path.dirname(abs_path)
|
||||
if dir_path:
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
# 保存文件
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
with open(abs_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
logger.info(f"适配器配置已保存: {path}")
|
||||
logger.info(f"适配器配置已保存: {path} (绝对路径: {abs_path})")
|
||||
return {"success": True, "message": "配置已保存"}
|
||||
|
||||
except HTTPException:
|
||||
|
|
|
|||
|
|
@ -394,11 +394,9 @@ async def register_emoji(emoji_id: int, authorization: Optional[str] = Header(No
|
|||
if emoji.is_registered:
|
||||
raise HTTPException(status_code=400, detail="该表情包已经注册")
|
||||
|
||||
if emoji.is_banned:
|
||||
raise HTTPException(status_code=400, detail="该表情包已被禁用,无法注册")
|
||||
|
||||
# 注册表情包
|
||||
# 注册表情包(如果已封禁,自动解除封禁)
|
||||
emoji.is_registered = True
|
||||
emoji.is_banned = False # 注册时自动解除封禁
|
||||
emoji.register_time = time.time()
|
||||
emoji.save()
|
||||
|
||||
|
|
|
|||
|
|
@ -516,10 +516,14 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
plugins_dir = Path("plugins")
|
||||
plugins_dir.mkdir(exist_ok=True)
|
||||
|
||||
target_path = plugins_dir / request.plugin_id
|
||||
# 将插件 ID 中的点替换为下划线作为文件夹名称(避免文件系统问题)
|
||||
# 例如: SengokuCola.Mute-Plugin -> SengokuCola_Mute-Plugin
|
||||
folder_name = request.plugin_id.replace(".", "_")
|
||||
target_path = plugins_dir / folder_name
|
||||
|
||||
# 检查插件是否已安装
|
||||
if target_path.exists():
|
||||
# 检查插件是否已安装(需要检查两种格式:新格式下划线和旧格式点)
|
||||
old_format_path = plugins_dir / request.plugin_id
|
||||
if target_path.exists() or old_format_path.exists():
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
|
|
@ -608,6 +612,12 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
if field not in manifest:
|
||||
raise ValueError(f"缺少必需字段: {field}")
|
||||
|
||||
# 将插件 ID 写入 manifest(用于后续准确识别)
|
||||
# 这样即使文件夹名称改变,也能通过 manifest 准确识别插件
|
||||
manifest["id"] = request.plugin_id
|
||||
with open(manifest_path, "w", encoding="utf-8") as f:
|
||||
json_module.dump(manifest, f, ensure_ascii=False, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
# 清理失败的安装
|
||||
import shutil
|
||||
|
|
@ -686,11 +696,19 @@ async def uninstall_plugin(
|
|||
plugin_id=request.plugin_id,
|
||||
)
|
||||
|
||||
# 1. 检查插件是否存在
|
||||
# 1. 检查插件是否存在(支持新旧两种格式)
|
||||
plugins_dir = Path("plugins")
|
||||
plugin_path = plugins_dir / request.plugin_id
|
||||
# 新格式:下划线
|
||||
folder_name = request.plugin_id.replace(".", "_")
|
||||
plugin_path = plugins_dir / folder_name
|
||||
# 旧格式:点
|
||||
old_format_path = plugins_dir / request.plugin_id
|
||||
|
||||
# 优先使用新格式,如果不存在则尝试旧格式
|
||||
if not plugin_path.exists():
|
||||
if old_format_path.exists():
|
||||
plugin_path = old_format_path
|
||||
else:
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
|
|
@ -812,11 +830,19 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
plugin_id=request.plugin_id,
|
||||
)
|
||||
|
||||
# 1. 检查插件是否已安装
|
||||
# 1. 检查插件是否已安装(支持新旧两种格式)
|
||||
plugins_dir = Path("plugins")
|
||||
plugin_path = plugins_dir / request.plugin_id
|
||||
# 新格式:下划线
|
||||
folder_name = request.plugin_id.replace(".", "_")
|
||||
plugin_path = plugins_dir / folder_name
|
||||
# 旧格式:点
|
||||
old_format_path = plugins_dir / request.plugin_id
|
||||
|
||||
# 优先使用新格式,如果不存在则尝试旧格式
|
||||
if not plugin_path.exists():
|
||||
if old_format_path.exists():
|
||||
plugin_path = old_format_path
|
||||
else:
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
|
|
@ -830,7 +856,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
# 2. 读取旧版本信息
|
||||
manifest_path = plugin_path / "_manifest.json"
|
||||
old_version = "unknown"
|
||||
plugin_name = request.plugin_id
|
||||
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
|
|
@ -839,7 +864,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||
manifest = json_module.load(f)
|
||||
old_version = manifest.get("version", "unknown")
|
||||
_plugin_name = manifest.get("name", request.plugin_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -1032,18 +1056,18 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
|||
if not plugin_path.is_dir():
|
||||
continue
|
||||
|
||||
# 目录名即为插件 ID
|
||||
plugin_id = plugin_path.name
|
||||
# 目录名(可能是下划线格式、点格式或其他格式)
|
||||
folder_name = plugin_path.name
|
||||
|
||||
# 跳过隐藏目录和特殊目录
|
||||
if plugin_id.startswith(".") or plugin_id.startswith("__"):
|
||||
if folder_name.startswith(".") or folder_name.startswith("__"):
|
||||
continue
|
||||
|
||||
# 读取 _manifest.json
|
||||
manifest_path = plugin_path / "_manifest.json"
|
||||
|
||||
if not manifest_path.exists():
|
||||
logger.warning(f"插件 {plugin_id} 缺少 _manifest.json,跳过")
|
||||
logger.warning(f"插件文件夹 {folder_name} 缺少 _manifest.json,跳过")
|
||||
continue
|
||||
|
||||
try:
|
||||
|
|
@ -1054,9 +1078,58 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
|||
|
||||
# 基本验证
|
||||
if "name" not in manifest or "version" not in manifest:
|
||||
logger.warning(f"插件 {plugin_id} 的 _manifest.json 格式无效,跳过")
|
||||
logger.warning(f"插件文件夹 {folder_name} 的 _manifest.json 格式无效,跳过")
|
||||
continue
|
||||
|
||||
# 获取插件 ID(优先从 manifest,否则从文件夹名推断)
|
||||
if "id" in manifest:
|
||||
# 优先使用 manifest 中的 id(最准确)
|
||||
plugin_id = manifest["id"]
|
||||
else:
|
||||
# 从 manifest 信息构建 ID
|
||||
# 尝试从 author.name 和 repository_url 构建标准 ID
|
||||
author_name = None
|
||||
repo_name = None
|
||||
|
||||
# 获取作者名
|
||||
if "author" in manifest:
|
||||
if isinstance(manifest["author"], dict) and "name" in manifest["author"]:
|
||||
author_name = manifest["author"]["name"]
|
||||
elif isinstance(manifest["author"], str):
|
||||
author_name = manifest["author"]
|
||||
|
||||
# 从 repository_url 获取仓库名
|
||||
if "repository_url" in manifest:
|
||||
repo_url = manifest["repository_url"].rstrip("/")
|
||||
if repo_url.endswith(".git"):
|
||||
repo_url = repo_url[:-4]
|
||||
repo_name = repo_url.split("/")[-1]
|
||||
|
||||
# 构建 ID
|
||||
if author_name and repo_name:
|
||||
# 标准格式: Author.RepoName
|
||||
plugin_id = f"{author_name}.{repo_name}"
|
||||
elif author_name:
|
||||
# 如果只有作者,使用 Author.FolderName
|
||||
plugin_id = f"{author_name}.{folder_name}"
|
||||
else:
|
||||
# 从文件夹名推断
|
||||
if "_" in folder_name and "." not in folder_name:
|
||||
# 假设格式为 Author_PluginName,转换为 Author.PluginName
|
||||
plugin_id = folder_name.replace("_", ".", 1)
|
||||
else:
|
||||
# 直接使用文件夹名
|
||||
plugin_id = folder_name
|
||||
|
||||
# 将推断的 ID 写入 manifest(方便下次识别)
|
||||
logger.info(f"为插件 {folder_name} 自动生成 ID: {plugin_id}")
|
||||
manifest["id"] = plugin_id
|
||||
try:
|
||||
with open(manifest_path, "w", encoding="utf-8") as f:
|
||||
json_module.dump(manifest, f, ensure_ascii=False, indent=2)
|
||||
except Exception as write_error:
|
||||
logger.warning(f"无法写入 ID 到 manifest: {write_error}")
|
||||
|
||||
# 添加到已安装列表(返回完整的 manifest 信息)
|
||||
installed_plugins.append(
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue