This commit is contained in:
minecraft1024a
2025-10-06 20:45:20 +08:00
3 changed files with 194 additions and 9 deletions

1
.gitignore vendored
View File

@@ -191,6 +191,7 @@ celerybeat.pid
# Environments # Environments
.venv .venv
.venv311/
env/ env/
venv/ venv/
ENV/ ENV/

185
bot.py
View File

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

View File

@@ -332,14 +332,13 @@ class MessageManager:
async def _check_and_handle_interruption(self, chat_stream: ChatStream | None = None): async def _check_and_handle_interruption(self, chat_stream: ChatStream | None = None):
"""检查并处理消息打断""" """检查并处理消息打断"""
if not global_config.chat.interruption_enabled: if not global_config.chat.interruption_enabled or not chat_stream:
return return
# 检查是否有正在进行的处理任务 # 从 chatter_manager 检查是否有正在进行的处理任务
if ( processing_task = self.chatter_manager.get_processing_task(chat_stream.stream_id)
chat_stream.context_manager.context.processing_task
and not chat_stream.context_manager.context.processing_task.done() if processing_task and not processing_task.done():
):
# 计算打断概率 # 计算打断概率
interruption_probability = chat_stream.context_manager.context.calculate_interruption_probability( interruption_probability = chat_stream.context_manager.context.calculate_interruption_probability(
global_config.chat.interruption_max_limit, global_config.chat.interruption_probability_factor global_config.chat.interruption_max_limit, global_config.chat.interruption_probability_factor
@@ -357,11 +356,11 @@ class MessageManager:
logger.info(f"聊天流 {chat_stream.stream_id} 触发消息打断,打断概率: {interruption_probability:.2f}") logger.info(f"聊天流 {chat_stream.stream_id} 触发消息打断,打断概率: {interruption_probability:.2f}")
# 取消现有任务 # 取消现有任务
chat_stream.context_manager.context.processing_task.cancel() processing_task.cancel()
try: try:
await chat_stream.context_manager.context.processing_task await processing_task
except asyncio.CancelledError: except asyncio.CancelledError:
pass logger.debug(f"消息打断成功取消任务: {chat_stream.stream_id}")
# 增加打断计数并应用afc阈值降低 # 增加打断计数并应用afc阈值降低
await chat_stream.context_manager.context.increment_interruption_count() await chat_stream.context_manager.context.increment_interruption_count()