pull/1385/head
SengokuCola 2025-11-25 01:58:35 +08:00
commit 5d9e00c5ea
15 changed files with 620 additions and 447 deletions

13
bot.py
View File

@ -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}")
# 新增:检测外部请求关闭

View File

@ -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"
}

View File

@ -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",
@ -31,10 +35,17 @@
},
{
"type": "action",
"name": "bye_greeting",
"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"
}

View File

@ -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",
@ -31,10 +37,17 @@
},
{
"type": "action",
"name": "bye_greeting",
"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"
}

View File

@ -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):
# """记忆遗忘任务"""

View File

@ -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:

View File

@ -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()

View File

@ -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,
@ -607,6 +611,12 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
for field in required_fields:
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:
# 清理失败的安装
@ -686,20 +696,28 @@ 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():
await update_progress(
stage="error",
progress=0,
message="插件不存在",
operation="uninstall",
plugin_id=request.plugin_id,
error="插件未安装或已被删除",
)
raise HTTPException(status_code=404, detail="插件未安装")
if old_format_path.exists():
plugin_path = old_format_path
else:
await update_progress(
stage="error",
progress=0,
message="插件不存在",
operation="uninstall",
plugin_id=request.plugin_id,
error="插件未安装或已被删除",
)
raise HTTPException(status_code=404, detail="插件未安装")
await update_progress(
stage="loading",
@ -812,25 +830,32 @@ 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():
await update_progress(
stage="error",
progress=0,
message="插件不存在",
operation="update",
plugin_id=request.plugin_id,
error="插件未安装,请先安装",
)
raise HTTPException(status_code=404, detail="插件未安装")
if old_format_path.exists():
plugin_path = old_format_path
else:
await update_progress(
stage="error",
progress=0,
message="插件不存在",
operation="update",
plugin_id=request.plugin_id,
error="插件未安装,请先安装",
)
raise HTTPException(status_code=404, detail="插件未安装")
# 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(
{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,13 +5,13 @@
<link rel="icon" type="image/x-icon" href="/maimai.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MaiBot Dashboard</title>
<script type="module" crossorigin src="/assets/index-Du48JcWB.js"></script>
<script type="module" crossorigin src="/assets/index-0AuPNinr.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dtc2IqVY.js">
<link rel="modulepreload" crossorigin href="/assets/router-SinpzM5S.js">
<link rel="modulepreload" crossorigin href="/assets/charts-BH1Uno6i.js">
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BLBhIcJ8.js">
<link rel="modulepreload" crossorigin href="/assets/icons-COIni9ke.js">
<link rel="stylesheet" crossorigin href="/assets/index-Dq6na-LB.css">
<link rel="modulepreload" crossorigin href="/assets/icons-zVsyMy4K.js">
<link rel="stylesheet" crossorigin href="/assets/index-Dlctrk5R.css">
</head>
<body>
<div id="root"></div>