Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox_Bot into dev
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -191,6 +191,7 @@ celerybeat.pid
|
|||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.venv
|
.venv
|
||||||
|
.venv311/
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
|
|||||||
187
bot.py
187
bot.py
@@ -215,6 +215,16 @@ class ShutdownManager:
|
|||||||
logger.info("正在优雅关闭麦麦...")
|
logger.info("正在优雅关闭麦麦...")
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 停止 WebUI 开发服务(如果在运行)
|
||||||
|
try:
|
||||||
|
# WebUIManager 可能在后文定义,这里只在运行阶段引用
|
||||||
|
await WebUIManager.stop_dev_server() # type: ignore[name-defined]
|
||||||
|
except NameError:
|
||||||
|
# 若未定义(例如异常提前退出),忽略
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"停止WebUI开发服务时出错: {e}")
|
||||||
|
|
||||||
# 停止异步任务
|
# 停止异步任务
|
||||||
tasks_stopped = await TaskManager.stop_async_tasks()
|
tasks_stopped = await TaskManager.stop_async_tasks()
|
||||||
|
|
||||||
@@ -279,7 +289,7 @@ class DatabaseManager:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# 使用线程执行器运行潜在的阻塞操作
|
# 使用线程执行器运行潜在的阻塞操作
|
||||||
await asyncio.to_thread(initialize_sql_database, global_config.database)
|
await initialize_sql_database( global_config.database)
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
logger.info(
|
logger.info(
|
||||||
f"数据库连接初始化成功,使用 {global_config.database.database_type} 数据库,耗时: {elapsed_time:.2f}秒"
|
f"数据库连接初始化成功,使用 {global_config.database.database_type} 数据库,耗时: {elapsed_time:.2f}秒"
|
||||||
@@ -355,6 +365,174 @@ class EasterEgg:
|
|||||||
logger.info(rainbow_text)
|
logger.info(rainbow_text)
|
||||||
|
|
||||||
|
|
||||||
|
class WebUIManager:
|
||||||
|
"""WebUI 开发服务器管理"""
|
||||||
|
|
||||||
|
_process = None
|
||||||
|
_drain_task = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_webui_dir() -> Path | None:
|
||||||
|
"""解析 webui 目录,优先使用同级目录 MoFox_Bot/webui,其次回退到上级目录 ../webui。
|
||||||
|
|
||||||
|
也支持通过环境变量 WEBUI_DIR/WEBUI_PATH 指定绝对或相对路径。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
env_dir = os.getenv("WEBUI_DIR") or os.getenv("WEBUI_PATH")
|
||||||
|
if env_dir:
|
||||||
|
p = Path(env_dir).expanduser()
|
||||||
|
if not p.is_absolute():
|
||||||
|
p = (Path(__file__).resolve().parent / p).resolve()
|
||||||
|
if p.exists():
|
||||||
|
return p
|
||||||
|
script_dir = Path(__file__).resolve().parent
|
||||||
|
candidates = [
|
||||||
|
script_dir / "webui", # MoFox_Bot/webui(优先)
|
||||||
|
script_dir.parent / "webui", # 上级目录/webui(兼容最初需求)
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if c.exists():
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def start_dev_server(timeout: float = 60.0) -> bool:
|
||||||
|
"""启动 `npm run dev` 并在超时内检测是否成功。
|
||||||
|
|
||||||
|
返回 True 表示检测到成功信号;False 表示失败/超时/进程退出。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
webui_dir = WebUIManager._resolve_webui_dir()
|
||||||
|
if not webui_dir:
|
||||||
|
logger.error("未找到 webui 目录(可在 .env 使用 WEBUI_DIR 指定路径)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if WebUIManager._process and WebUIManager._process.returncode is None:
|
||||||
|
logger.info("WebUI 开发服务器已在运行,跳过重复启动")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info(f"正在启动 WebUI 开发服务器: npm run dev (cwd={webui_dir})")
|
||||||
|
npm_exe = "npm.cmd" if platform.system().lower() == "windows" else "npm"
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
npm_exe,
|
||||||
|
"run",
|
||||||
|
"dev",
|
||||||
|
cwd=str(webui_dir),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
WebUIManager._process = proc
|
||||||
|
|
||||||
|
success_keywords = [
|
||||||
|
"compiled successfully",
|
||||||
|
"ready in",
|
||||||
|
"local:",
|
||||||
|
"listening on",
|
||||||
|
"running at:",
|
||||||
|
"started server",
|
||||||
|
"app running at:",
|
||||||
|
"ready - started server",
|
||||||
|
"vite v", # Vite 一般会输出版本与 ready in
|
||||||
|
]
|
||||||
|
failure_keywords = [
|
||||||
|
"err!",
|
||||||
|
"error",
|
||||||
|
"eaddrinuse",
|
||||||
|
"address already in use",
|
||||||
|
"syntaxerror",
|
||||||
|
"fatal",
|
||||||
|
"npm ERR!",
|
||||||
|
]
|
||||||
|
|
||||||
|
start_ts = time.time()
|
||||||
|
detected_success = False
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if proc.returncode is not None:
|
||||||
|
if proc.returncode != 0:
|
||||||
|
logger.error(f"WebUI 进程提前退出,退出码: {proc.returncode}")
|
||||||
|
else:
|
||||||
|
logger.warning("WebUI 进程已退出")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
line = await asyncio.wait_for(proc.stdout.readline(), timeout=1.0) # type: ignore[arg-type]
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
line = b""
|
||||||
|
|
||||||
|
if line:
|
||||||
|
text = line.decode(errors="ignore").rstrip()
|
||||||
|
logger.info(f"[webui] {text}")
|
||||||
|
low = text.lower()
|
||||||
|
if any(k in low for k in success_keywords):
|
||||||
|
detected_success = True
|
||||||
|
break
|
||||||
|
if any(k in low for k in failure_keywords):
|
||||||
|
detected_success = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if time.time() - start_ts > timeout:
|
||||||
|
logger.warning("WebUI 启动检测超时")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 后台继续读取剩余输出,避免缓冲区阻塞
|
||||||
|
async def _drain_rest():
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = await proc.stdout.readline() # type: ignore[union-attr]
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
text = line.decode(errors="ignore").rstrip()
|
||||||
|
logger.info(f"[webui] {text}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"webui 日志读取停止: {e}")
|
||||||
|
|
||||||
|
WebUIManager._drain_task = asyncio.create_task(_drain_rest())
|
||||||
|
return bool(detected_success)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("未找到 npm,请确认已安装 Node.js 并将 npm 加入 PATH")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动 WebUI 开发服务器失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def stop_dev_server(timeout: float = 5.0) -> bool:
|
||||||
|
"""停止 WebUI 开发服务器(如果在运行)。"""
|
||||||
|
proc = WebUIManager._process
|
||||||
|
if not proc:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
if proc.returncode is None:
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"发送终止信号失败: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(proc.wait(), timeout=timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if WebUIManager._drain_task and not WebUIManager._drain_task.done():
|
||||||
|
WebUIManager._drain_task.cancel()
|
||||||
|
try:
|
||||||
|
await WebUIManager._drain_task
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logger.info("WebUI 开发服务器已停止")
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
WebUIManager._process = None
|
||||||
|
WebUIManager._drain_task = None
|
||||||
|
|
||||||
class MaiBotMain:
|
class MaiBotMain:
|
||||||
"""麦麦机器人主程序类"""
|
"""麦麦机器人主程序类"""
|
||||||
|
|
||||||
@@ -455,6 +633,13 @@ async def main_async():
|
|||||||
# 确保环境文件存在
|
# 确保环境文件存在
|
||||||
ConfigManager.ensure_env_file()
|
ConfigManager.ensure_env_file()
|
||||||
|
|
||||||
|
# 启动 WebUI 开发服务器(成功/失败都继续后续步骤)
|
||||||
|
webui_ok = await WebUIManager.start_dev_server(timeout=60)
|
||||||
|
if webui_ok:
|
||||||
|
logger.info("WebUI 启动成功,继续下一步骤")
|
||||||
|
else:
|
||||||
|
logger.error("WebUI 启动失败,继续下一步骤")
|
||||||
|
|
||||||
# 创建主程序实例并执行初始化
|
# 创建主程序实例并执行初始化
|
||||||
maibot = MaiBotMain()
|
maibot = MaiBotMain()
|
||||||
main_system = await maibot.run_sync_init()
|
main_system = await maibot.run_sync_init()
|
||||||
|
|||||||
349045
depends-data/dict.txt
Normal file
349045
depends-data/dict.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,9 @@ class ChineseTypoGenerator:
|
|||||||
|
|
||||||
# 使用内置的词频文件
|
# 使用内置的词频文件
|
||||||
char_freq = defaultdict(int)
|
char_freq = defaultdict(int)
|
||||||
dict_path = os.path.join(os.path.dirname(rjieba.__file__), "dict.txt")
|
# 从当前文件向上返回三级目录到项目根目录,然后拼接路径
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||||
|
dict_path = os.path.join(base_dir, "depends-data", "dict.txt")
|
||||||
|
|
||||||
# 读取rjieba的词典文件
|
# 读取rjieba的词典文件
|
||||||
with open(dict_path, encoding="utf-8") as f:
|
with open(dict_path, encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -48,35 +48,6 @@ class DatabaseProxy:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyTransaction:
|
|
||||||
"""SQLAlchemy 异步事务上下文管理器 (兼容旧代码示例,推荐直接使用 get_db_session)。"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._ctx = None
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
# get_db_session 是一个 async contextmanager
|
|
||||||
self._ctx = get_db_session()
|
|
||||||
self.session = await self._ctx.__aenter__()
|
|
||||||
return self.session
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
try:
|
|
||||||
if self.session:
|
|
||||||
if exc_type is None:
|
|
||||||
try:
|
|
||||||
await self.session.commit()
|
|
||||||
except Exception:
|
|
||||||
await self.session.rollback()
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
await self.session.rollback()
|
|
||||||
finally:
|
|
||||||
if self._ctx:
|
|
||||||
await self._ctx.__aexit__(exc_type, exc_val, exc_tb)
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局数据库代理实例
|
# 创建全局数据库代理实例
|
||||||
db = DatabaseProxy()
|
db = DatabaseProxy()
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class IPermissionManager(ABC):
|
|||||||
async def check_permission(self, user: UserInfo, permission_node: str) -> bool: ...
|
async def check_permission(self, user: UserInfo, permission_node: str) -> bool: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_master(self, user: UserInfo) -> bool: ... # 同步快速判断
|
async def is_master(self, user: UserInfo) -> bool: ... # 同步快速判断
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def register_permission_node(self, node: PermissionNode) -> bool: ...
|
async def register_permission_node(self, node: PermissionNode) -> bool: ...
|
||||||
@@ -82,9 +82,9 @@ class PermissionAPI:
|
|||||||
self._ensure_manager()
|
self._ensure_manager()
|
||||||
return await self._permission_manager.check_permission(UserInfo(platform, user_id), permission_node)
|
return await self._permission_manager.check_permission(UserInfo(platform, user_id), permission_node)
|
||||||
|
|
||||||
def is_master(self, platform: str, user_id: str) -> bool:
|
async def is_master(self, platform: str, user_id: str) -> bool:
|
||||||
self._ensure_manager()
|
self._ensure_manager()
|
||||||
return self._permission_manager.is_master(UserInfo(platform, user_id))
|
return await self._permission_manager.is_master(UserInfo(platform, user_id))
|
||||||
|
|
||||||
async def register_permission_node(
|
async def register_permission_node(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ class PermissionCommand(PlusCommand):
|
|||||||
target_user_id = chat_stream.user_info.user_id
|
target_user_id = chat_stream.user_info.user_id
|
||||||
|
|
||||||
# 检查是否为Master用户
|
# 检查是否为Master用户
|
||||||
is_master = permission_api.is_master(chat_stream.platform, target_user_id)
|
is_master = await permission_api.is_master(chat_stream.platform, target_user_id)
|
||||||
|
|
||||||
# 获取用户权限
|
# 获取用户权限
|
||||||
permissions = await permission_api.get_user_permissions(chat_stream.platform, target_user_id)
|
permissions = await permission_api.get_user_permissions(chat_stream.platform, target_user_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user