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)
|
loop.run_until_complete(main_tasks)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
# loop.run_until_complete(get_global_api().stop())
|
|
||||||
logger.warning("收到中断信号,正在优雅关闭...")
|
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():
|
if loop and not loop.is_closed():
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(graceful_shutdown())
|
loop.run_until_complete(graceful_shutdown())
|
||||||
except Exception as ge: # 捕捉优雅关闭时可能发生的错误
|
except Exception as ge:
|
||||||
logger.error(f"优雅关闭时发生错误: {ge}")
|
logger.error(f"优雅关闭时发生错误: {ge}")
|
||||||
# 新增:检测外部请求关闭
|
# 新增:检测外部请求关闭
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,25 @@
|
||||||
"url": "https://github.com/MaiM-with-u"
|
"url": "https://github.com/MaiM-with-u"
|
||||||
},
|
},
|
||||||
"license": "GPL-v3.0-or-later",
|
"license": "GPL-v3.0-or-later",
|
||||||
|
|
||||||
"host_application": {
|
"host_application": {
|
||||||
"min_version": "0.10.3"
|
"min_version": "0.10.3"
|
||||||
},
|
},
|
||||||
"homepage_url": "https://github.com/SengokuCola/BetterFrequency",
|
"homepage_url": "https://github.com/SengokuCola/BetterFrequency",
|
||||||
"repository_url": "https://github.com/SengokuCola/BetterFrequency",
|
"repository_url": "https://github.com/SengokuCola/BetterFrequency",
|
||||||
"keywords": ["frequency", "control", "talk_frequency", "plugin", "shortcut"],
|
"keywords": [
|
||||||
"categories": ["Chat", "Frequency", "Control"],
|
"frequency",
|
||||||
|
"control",
|
||||||
|
"talk_frequency",
|
||||||
|
"plugin",
|
||||||
|
"shortcut"
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"Chat",
|
||||||
|
"Frequency",
|
||||||
|
"Control"
|
||||||
|
],
|
||||||
"default_locale": "zh-CN",
|
"default_locale": "zh-CN",
|
||||||
"locales_path": "_locales",
|
"locales_path": "_locales",
|
||||||
|
|
||||||
"plugin_info": {
|
"plugin_info": {
|
||||||
"is_built_in": false,
|
"is_built_in": false,
|
||||||
"plugin_type": "frequency",
|
"plugin_type": "frequency",
|
||||||
|
|
@ -46,5 +53,6 @@
|
||||||
"支持完整命令和简化命令",
|
"支持完整命令和简化命令",
|
||||||
"快速操作支持"
|
"快速操作支持"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"id": "SengokuCola.BetterFrequency"
|
||||||
}
|
}
|
||||||
|
|
@ -8,18 +8,22 @@
|
||||||
"url": "https://github.com/SengokuCola"
|
"url": "https://github.com/SengokuCola"
|
||||||
},
|
},
|
||||||
"license": "GPL-v3.0-or-later",
|
"license": "GPL-v3.0-or-later",
|
||||||
|
|
||||||
"host_application": {
|
"host_application": {
|
||||||
"min_version": "0.10.4"
|
"min_version": "0.10.4"
|
||||||
},
|
},
|
||||||
"homepage_url": "https://github.com/SengokuCola/BetterEmoji",
|
"homepage_url": "https://github.com/SengokuCola/BetterEmoji",
|
||||||
"repository_url": "https://github.com/SengokuCola/BetterEmoji",
|
"repository_url": "https://github.com/SengokuCola/BetterEmoji",
|
||||||
"keywords": ["emoji", "manage", "plugin"],
|
"keywords": [
|
||||||
"categories": ["Examples", "Tutorial"],
|
"emoji",
|
||||||
|
"manage",
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"Examples",
|
||||||
|
"Tutorial"
|
||||||
|
],
|
||||||
"default_locale": "zh-CN",
|
"default_locale": "zh-CN",
|
||||||
"locales_path": "_locales",
|
"locales_path": "_locales",
|
||||||
|
|
||||||
"plugin_info": {
|
"plugin_info": {
|
||||||
"is_built_in": false,
|
"is_built_in": false,
|
||||||
"plugin_type": "emoji_manage",
|
"plugin_type": "emoji_manage",
|
||||||
|
|
@ -33,8 +37,15 @@
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"name": "bye_greeting",
|
"name": "bye_greeting",
|
||||||
"description": "向用户发送告别消息",
|
"description": "向用户发送告别消息",
|
||||||
"activation_modes": ["keyword"],
|
"activation_modes": [
|
||||||
"keywords": ["再见", "bye", "88", "拜拜"]
|
"keyword"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"再见",
|
||||||
|
"bye",
|
||||||
|
"88",
|
||||||
|
"拜拜"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
|
|
@ -49,5 +60,6 @@
|
||||||
"配置文件示例",
|
"配置文件示例",
|
||||||
"新手教程代码"
|
"新手教程代码"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"id": "SengokuCola.BetterEmoji"
|
||||||
}
|
}
|
||||||
|
|
@ -8,18 +8,24 @@
|
||||||
"url": "https://github.com/MaiM-with-u"
|
"url": "https://github.com/MaiM-with-u"
|
||||||
},
|
},
|
||||||
"license": "GPL-v3.0-or-later",
|
"license": "GPL-v3.0-or-later",
|
||||||
|
|
||||||
"host_application": {
|
"host_application": {
|
||||||
"min_version": "0.8.0"
|
"min_version": "0.8.0"
|
||||||
},
|
},
|
||||||
"homepage_url": "https://github.com/MaiM-with-u/maibot",
|
"homepage_url": "https://github.com/MaiM-with-u/maibot",
|
||||||
"repository_url": "https://github.com/MaiM-with-u/maibot",
|
"repository_url": "https://github.com/MaiM-with-u/maibot",
|
||||||
"keywords": ["demo", "example", "hello", "greeting", "tutorial"],
|
"keywords": [
|
||||||
"categories": ["Examples", "Tutorial"],
|
"demo",
|
||||||
|
"example",
|
||||||
|
"hello",
|
||||||
|
"greeting",
|
||||||
|
"tutorial"
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"Examples",
|
||||||
|
"Tutorial"
|
||||||
|
],
|
||||||
"default_locale": "zh-CN",
|
"default_locale": "zh-CN",
|
||||||
"locales_path": "_locales",
|
"locales_path": "_locales",
|
||||||
|
|
||||||
"plugin_info": {
|
"plugin_info": {
|
||||||
"is_built_in": false,
|
"is_built_in": false,
|
||||||
"plugin_type": "example",
|
"plugin_type": "example",
|
||||||
|
|
@ -33,8 +39,15 @@
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"name": "bye_greeting",
|
"name": "bye_greeting",
|
||||||
"description": "向用户发送告别消息",
|
"description": "向用户发送告别消息",
|
||||||
"activation_modes": ["keyword"],
|
"activation_modes": [
|
||||||
"keywords": ["再见", "bye", "88", "拜拜"]
|
"keyword"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"再见",
|
||||||
|
"bye",
|
||||||
|
"88",
|
||||||
|
"拜拜"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
|
|
@ -49,5 +62,6 @@
|
||||||
"配置文件示例",
|
"配置文件示例",
|
||||||
"新手教程代码"
|
"新手教程代码"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"id": "MaiBot开发团队.maibot"
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +156,7 @@ class MainSystem:
|
||||||
|
|
||||||
async def schedule_tasks(self):
|
async def schedule_tasks(self):
|
||||||
"""调度定时任务"""
|
"""调度定时任务"""
|
||||||
while True:
|
try:
|
||||||
tasks = [
|
tasks = [
|
||||||
get_emoji_manager().start_periodic_check_register(),
|
get_emoji_manager().start_periodic_check_register(),
|
||||||
self.app.run(),
|
self.app.run(),
|
||||||
|
|
@ -168,6 +168,9 @@ class MainSystem:
|
||||||
tasks.append(self.webui_server.start())
|
tasks.append(self.webui_server.start())
|
||||||
|
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info("调度任务已取消")
|
||||||
|
raise
|
||||||
|
|
||||||
# async def forget_memory_task(self):
|
# async def forget_memory_task(self):
|
||||||
# """记忆遗忘任务"""
|
# """记忆遗忘任务"""
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Body
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from src.common.logger import get_logger
|
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 (
|
from src.config.official_configs import (
|
||||||
BotConfig,
|
BotConfig,
|
||||||
PersonalityConfig,
|
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")
|
@router.get("/adapter-config/path")
|
||||||
async def 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:
|
if not adapter_config_path:
|
||||||
return {"success": True, "path": None}
|
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
|
import datetime
|
||||||
mtime = os.path.getmtime(adapter_config_path)
|
mtime = os.path.getmtime(abs_path)
|
||||||
last_modified = datetime.datetime.fromtimestamp(mtime).isoformat()
|
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:
|
else:
|
||||||
|
# 文件不存在,返回原路径
|
||||||
return {"success": True, "path": adapter_config_path, "lastModified": None}
|
return {"success": True, "path": adapter_config_path, "lastModified": None}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -471,15 +509,21 @@ async def save_adapter_config_path(data: dict[str, str] = Body(...)):
|
||||||
else:
|
else:
|
||||||
webui_data = {}
|
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)
|
os.makedirs("data", exist_ok=True)
|
||||||
with open(webui_data_path, "w", encoding="utf-8") as f:
|
with open(webui_data_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(webui_data, f, ensure_ascii=False, indent=2)
|
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": "路径已保存"}
|
return {"success": True, "message": "路径已保存"}
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
@ -496,19 +540,22 @@ async def get_adapter_config(path: str):
|
||||||
if not path:
|
if not path:
|
||||||
raise HTTPException(status_code=400, detail="路径参数不能为空")
|
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}")
|
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 格式的配置文件")
|
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()
|
content = f.read()
|
||||||
|
|
||||||
logger.info(f"已读取适配器配置: {path}")
|
logger.info(f"已读取适配器配置: {path} (绝对路径: {abs_path})")
|
||||||
return {"success": True, "content": content}
|
return {"success": True, "content": content}
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
@ -530,8 +577,11 @@ async def save_adapter_config(data: dict[str, str] = Body(...)):
|
||||||
if content is None:
|
if content is None:
|
||||||
raise HTTPException(status_code=400, detail="配置内容不能为空")
|
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 格式的配置文件")
|
raise HTTPException(status_code=400, detail="只支持 .toml 格式的配置文件")
|
||||||
|
|
||||||
# 验证 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)}")
|
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)
|
f.write(content)
|
||||||
|
|
||||||
logger.info(f"适配器配置已保存: {path}")
|
logger.info(f"适配器配置已保存: {path} (绝对路径: {abs_path})")
|
||||||
return {"success": True, "message": "配置已保存"}
|
return {"success": True, "message": "配置已保存"}
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
|
||||||
|
|
@ -394,11 +394,9 @@ async def register_emoji(emoji_id: int, authorization: Optional[str] = Header(No
|
||||||
if emoji.is_registered:
|
if emoji.is_registered:
|
||||||
raise HTTPException(status_code=400, detail="该表情包已经注册")
|
raise HTTPException(status_code=400, detail="该表情包已经注册")
|
||||||
|
|
||||||
if emoji.is_banned:
|
# 注册表情包(如果已封禁,自动解除封禁)
|
||||||
raise HTTPException(status_code=400, detail="该表情包已被禁用,无法注册")
|
|
||||||
|
|
||||||
# 注册表情包
|
|
||||||
emoji.is_registered = True
|
emoji.is_registered = True
|
||||||
|
emoji.is_banned = False # 注册时自动解除封禁
|
||||||
emoji.register_time = time.time()
|
emoji.register_time = time.time()
|
||||||
emoji.save()
|
emoji.save()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -516,10 +516,14 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
plugins_dir = Path("plugins")
|
plugins_dir = Path("plugins")
|
||||||
plugins_dir.mkdir(exist_ok=True)
|
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(
|
await update_progress(
|
||||||
stage="error",
|
stage="error",
|
||||||
progress=0,
|
progress=0,
|
||||||
|
|
@ -608,6 +612,12 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
if field not in manifest:
|
if field not in manifest:
|
||||||
raise ValueError(f"缺少必需字段: {field}")
|
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:
|
except Exception as e:
|
||||||
# 清理失败的安装
|
# 清理失败的安装
|
||||||
import shutil
|
import shutil
|
||||||
|
|
@ -686,11 +696,19 @@ async def uninstall_plugin(
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. 检查插件是否存在
|
# 1. 检查插件是否存在(支持新旧两种格式)
|
||||||
plugins_dir = Path("plugins")
|
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 not plugin_path.exists():
|
||||||
|
if old_format_path.exists():
|
||||||
|
plugin_path = old_format_path
|
||||||
|
else:
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="error",
|
stage="error",
|
||||||
progress=0,
|
progress=0,
|
||||||
|
|
@ -812,11 +830,19 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. 检查插件是否已安装
|
# 1. 检查插件是否已安装(支持新旧两种格式)
|
||||||
plugins_dir = Path("plugins")
|
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 not plugin_path.exists():
|
||||||
|
if old_format_path.exists():
|
||||||
|
plugin_path = old_format_path
|
||||||
|
else:
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="error",
|
stage="error",
|
||||||
progress=0,
|
progress=0,
|
||||||
|
|
@ -830,7 +856,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
# 2. 读取旧版本信息
|
# 2. 读取旧版本信息
|
||||||
manifest_path = plugin_path / "_manifest.json"
|
manifest_path = plugin_path / "_manifest.json"
|
||||||
old_version = "unknown"
|
old_version = "unknown"
|
||||||
plugin_name = request.plugin_id
|
|
||||||
|
|
||||||
if manifest_path.exists():
|
if manifest_path.exists():
|
||||||
try:
|
try:
|
||||||
|
|
@ -839,7 +864,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||||
manifest = json_module.load(f)
|
manifest = json_module.load(f)
|
||||||
old_version = manifest.get("version", "unknown")
|
old_version = manifest.get("version", "unknown")
|
||||||
_plugin_name = manifest.get("name", request.plugin_id)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -1032,18 +1056,18 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
||||||
if not plugin_path.is_dir():
|
if not plugin_path.is_dir():
|
||||||
continue
|
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
|
continue
|
||||||
|
|
||||||
# 读取 _manifest.json
|
# 读取 _manifest.json
|
||||||
manifest_path = plugin_path / "_manifest.json"
|
manifest_path = plugin_path / "_manifest.json"
|
||||||
|
|
||||||
if not manifest_path.exists():
|
if not manifest_path.exists():
|
||||||
logger.warning(f"插件 {plugin_id} 缺少 _manifest.json,跳过")
|
logger.warning(f"插件文件夹 {folder_name} 缺少 _manifest.json,跳过")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
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:
|
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
|
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 信息)
|
# 添加到已安装列表(返回完整的 manifest 信息)
|
||||||
installed_plugins.append(
|
installed_plugins.append(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue