新增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 自检,确保可直接执行。
This commit is contained in:
219
plugins/maibot_adapter/maibot_maintenance.py
Normal file
219
plugins/maibot_adapter/maibot_maintenance.py
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user