Files
abot/plugins/maibot_adapter/maibot_maintenance.py
liuwei c63c9cf73f 新增MaiBot远端维护脚本并放置到maibot_adapter目录
变更项:\n1. 新增 plugins/maibot_adapter/maibot_maintenance.py 维护脚本,支持 status/logs/restart/health/config 五类操作。\n2. 脚本默认维护 192.168.2.240 的 maibot-core-lite 容器,可通过参数覆盖主机、账号、容器名和端口。\n3. 增加关键日志过滤能力,聚焦 Timing Gate、no_reply、SendService、platform_map 等排障核心字段。\n4. 增加中文详细注释与编码兜底输出,避免日志中 emoji/特殊字符导致脚本中断。\n5. 已通过 py_compile 和 --help 自检,确保可直接执行。
2026-04-29 11:55:58 +08:00

220 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
MaiBot 维护脚本(放在 maibot_adapter 插件目录下)。
用途:
1. 一键查看远端 MaiBot 容器状态;
2. 一键拉取关键日志(重点看 timing_gate / no_reply / 发送失败);
3. 一键重启 MaiBot 容器;
4. 一键查看远端核心配置片段;
5. 一键检查 WebUI 健康接口。
示例:
python plugins/maibot_adapter/maibot_maintenance.py status
python plugins/maibot_adapter/maibot_maintenance.py logs --since 20m
python plugins/maibot_adapter/maibot_maintenance.py restart
python plugins/maibot_adapter/maibot_maintenance.py health
python plugins/maibot_adapter/maibot_maintenance.py config
"""
from __future__ import annotations
import argparse
import sys
from typing import Iterable
import paramiko
def _run_ssh_cmd(
host: str,
username: str,
password: str,
command: str,
timeout: int = 60,
) -> tuple[int, str, str]:
"""
在远端通过 SSH 执行命令并返回退出码、stdout、stderr。
说明:
1. 统一封装 SSH 调用,方便所有子命令复用;
2. 使用 utf-8 ignore 解码,避免日志中出现异常字符时脚本直接崩溃;
3. 返回退出码用于调用方判断是否成功并决定是否继续后续步骤。
"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(hostname=host, username=username, password=password, timeout=20)
stdin, stdout, stderr = client.exec_command(command, timeout=timeout)
out_text = stdout.read().decode("utf-8", "ignore")
err_text = stderr.read().decode("utf-8", "ignore")
exit_status = stdout.channel.recv_exit_status()
return exit_status, out_text, err_text
finally:
client.close()
def _print_header(title: str) -> None:
"""输出分隔标题,方便终端快速定位每段结果。"""
print(f"\n===== {title} =====")
def _safe_print_lines(lines: Iterable[str]) -> None:
"""
安全逐行输出,兼容 Windows 控制台编码。
说明:
1. 某些日志包含 emoji 或控制字符,直接 print 可能触发编码异常;
2. 这里做 gbk/backslashreplace 兜底,确保维护脚本不会因打印失败中断。
"""
for line in lines:
try:
print(line)
except UnicodeEncodeError:
print(line.encode("gbk", "backslashreplace").decode("gbk", "ignore"))
def cmd_status(args: argparse.Namespace) -> int:
"""查看远端容器运行状态。"""
command = (
"docker ps --format "
"'table {{.Names}}\\t{{.Image}}\\t{{.Status}}' "
f"| (head -n 1; grep '{args.container}' || true)"
)
code, out_text, err_text = _run_ssh_cmd(args.host, args.user, args.password, command)
_print_header("MaiBot 容器状态")
print(out_text.strip() or "(无输出)")
if err_text.strip():
_print_header("stderr")
print(err_text.strip())
return code
def cmd_logs(args: argparse.Namespace) -> int:
"""查看远端关键日志,默认聚焦回复决策与发送链路。"""
command = f"docker logs --since {args.since} {args.container} 2>&1"
code, out_text, err_text = _run_ssh_cmd(args.host, args.user, args.password, command, timeout=120)
text = f"{out_text}\n{err_text}".strip()
if not text:
_print_header("关键日志")
print("(无输出)")
return code
# 这里聚焦你当前排障最常用的关键词,尽量减少噪声。
keywords = (
"Timing Gate",
"no_reply",
"continue",
"wait",
"reply",
"回复",
"SendService",
"无法发送",
"发送成功",
"发送失败",
"Updated platform_map",
"Bridge received",
)
_print_header(f"关键日志since={args.since}")
filtered = [line for line in text.splitlines() if any(k in line for k in keywords)]
if not filtered:
print("(未匹配到关键日志,可尝试增大 --since)")
return code
_safe_print_lines(filtered)
return code
def cmd_restart(args: argparse.Namespace) -> int:
"""重启远端容器并回显最新状态。"""
_print_header("执行重启")
code, out_text, err_text = _run_ssh_cmd(
args.host,
args.user,
args.password,
f"docker restart {args.container}",
timeout=120,
)
print(out_text.strip() or "(无输出)")
if err_text.strip():
_print_header("stderr")
print(err_text.strip())
# 重启后再补一段状态,避免用户还要再跑一次 status。
cmd_status(args)
return code
def cmd_health(args: argparse.Namespace) -> int:
"""检查远端 WebUI 健康接口。"""
url = f"http://{args.host}:{args.webui_port}/api/webui/health"
command = f"curl -sS --max-time 8 '{url}'"
code, out_text, err_text = _run_ssh_cmd(args.host, args.user, args.password, command, timeout=20)
_print_header(f"WebUI 健康检查: {url}")
print(out_text.strip() or "(无输出)")
if err_text.strip():
_print_header("stderr")
print(err_text.strip())
return code
def cmd_config(args: argparse.Namespace) -> int:
"""查看远端核心配置片段,便于快速确认关键参数。"""
command = (
"docker exec {container} sh -lc "
"\"echo '[bot_config.toml]'; "
"sed -n '1,220p' /MaiMBot/config/bot_config.toml; "
"echo; "
"echo '[model_config.toml]'; "
"sed -n '1,220p' /MaiMBot/config/model_config.toml\""
).format(container=args.container)
code, out_text, err_text = _run_ssh_cmd(args.host, args.user, args.password, command, timeout=120)
_print_header("远端配置快照")
_safe_print_lines(out_text.splitlines())
if err_text.strip():
_print_header("stderr")
_safe_print_lines(err_text.splitlines())
return code
def build_parser() -> argparse.ArgumentParser:
"""构建命令行参数解析器。"""
parser = argparse.ArgumentParser(description="MaiBot 远端维护脚本")
parser.add_argument("--host", default="192.168.2.240", help="远端服务器地址")
parser.add_argument("--user", default="root", help="SSH 用户名")
parser.add_argument("--password", default="lw123456", help="SSH 密码")
parser.add_argument("--container", default="maibot-core-lite", help="MaiBot 容器名")
parser.add_argument("--webui-port", type=int, default=18001, help="WebUI 对外端口")
subparsers = parser.add_subparsers(dest="action", required=True)
status_parser = subparsers.add_parser("status", help="查看容器状态")
status_parser.set_defaults(func=cmd_status)
logs_parser = subparsers.add_parser("logs", help="查看关键日志")
logs_parser.add_argument("--since", default="20m", help="日志时间窗口,例如 10m / 1h")
logs_parser.set_defaults(func=cmd_logs)
restart_parser = subparsers.add_parser("restart", help="重启容器")
restart_parser.set_defaults(func=cmd_restart)
health_parser = subparsers.add_parser("health", help="检查 WebUI 健康接口")
health_parser.set_defaults(func=cmd_health)
config_parser = subparsers.add_parser("config", help="查看远端核心配置")
config_parser.set_defaults(func=cmd_config)
return parser
def main() -> int:
"""脚本入口。"""
parser = build_parser()
args = parser.parse_args()
return int(args.func(args))
if __name__ == "__main__":
sys.exit(main())