Initial import of Shirobana bot backend
Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# for_admin_catch_log.py
|
||||
# 管理员日志查看:读取 bot-utility 的 systemd 日志或指定日志文件。
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
LOG_PATH = os.environ.get("BOT_LOG_PATH", "")
|
||||
LOG_UNIT = os.environ.get("BOT_LOG_UNIT", "bot-utility.service")
|
||||
DEFAULT_LINES = 5
|
||||
MAX_LINES = 100
|
||||
|
||||
LOG_ENCODING = os.environ.get("BOT_LOG_ENCODING", "utf-8")
|
||||
|
||||
|
||||
def _admin_list():
|
||||
from channel import admin_list
|
||||
return [x for n in ("qq", "tg") for x in admin_list(n)]
|
||||
|
||||
|
||||
def _tail_log(lines):
|
||||
"""读取最后 lines 行;未指定 BOT_LOG_PATH 时使用 journal,返回 (ok, text)。"""
|
||||
if not LOG_PATH:
|
||||
return _tail_journal(lines)
|
||||
if not os.path.exists(LOG_PATH):
|
||||
return False, f"日志文件不存在: {LOG_PATH}"
|
||||
try:
|
||||
# 从文件尾部倒着读块, 避免一次性载入大文件
|
||||
with open(LOG_PATH, "rb") as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
size = f.tell()
|
||||
block = 8192
|
||||
pos = size
|
||||
buf = b""
|
||||
while pos > 0 and buf.count(b"\n") < lines + 1:
|
||||
read_len = min(block, pos)
|
||||
pos -= read_len
|
||||
f.seek(pos)
|
||||
buf = f.read(read_len) + buf
|
||||
tail_bytes = buf
|
||||
text = tail_bytes.decode(LOG_ENCODING, errors="replace")
|
||||
rows = text.splitlines()
|
||||
if len(rows) > lines:
|
||||
rows = rows[-lines:]
|
||||
body = "\n".join(rows).strip()
|
||||
if not body:
|
||||
return False, "日志为空"
|
||||
return True, body
|
||||
except Exception as e:
|
||||
return False, f"读取日志异常: {str(e)[:200]}"
|
||||
|
||||
|
||||
def _tail_journal(lines):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["journalctl", "--unit", LOG_UNIT, "--lines", str(lines),
|
||||
"--no-pager", "--output=cat", "--quiet"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
timeout=10, check=False,
|
||||
)
|
||||
if result.returncode or "permission" in result.stderr.lower():
|
||||
return False, f"读取 systemd 日志失败: {result.stderr.strip()[:200]}"
|
||||
body = result.stdout.strip()
|
||||
return (True, body) if body else (False, "日志为空")
|
||||
except (OSError, subprocess.TimeoutExpired) as e:
|
||||
return False, f"读取 systemd 日志异常: {str(e)[:200]}"
|
||||
|
||||
|
||||
def _dedupe_merge(text):
|
||||
"""合并连续重复行: 相同行连续出现 N 次时只保留一行并标注 (xN)"""
|
||||
rows = text.splitlines()
|
||||
if not rows:
|
||||
return ""
|
||||
merged = []
|
||||
prev = rows[0]
|
||||
count = 1
|
||||
for row in rows[1:]:
|
||||
if row == prev:
|
||||
count += 1
|
||||
else:
|
||||
merged.append(f"{prev} (x{count})" if count > 1 else prev)
|
||||
prev = row
|
||||
count = 1
|
||||
merged.append(f"{prev} (x{count})" if count > 1 else prev)
|
||||
return "\n".join(merged)
|
||||
|
||||
|
||||
def _clip(text):
|
||||
"""限制输出长度, 避免刷屏 (QQ 单条消息上限约 4000 字符)"""
|
||||
if len(text) > 3800:
|
||||
text = text[-3800:]
|
||||
return text
|
||||
|
||||
|
||||
HELP_TEXT = (
|
||||
"日志查看指令\n"
|
||||
"━━━━━━━━━━━━\n"
|
||||
"• .log help 显示本帮助\n"
|
||||
"• .log <int> 获取最近的 N 条日志 (最大 100, 默认 5)\n"
|
||||
)
|
||||
|
||||
|
||||
async def handle_command(ws, msg_type, target_id, raw_text):
|
||||
from channel import send_message
|
||||
parts = raw_text.split(maxsplit=1)
|
||||
arg = parts[1].strip() if len(parts) >= 2 else ""
|
||||
|
||||
if arg.lower() == "help":
|
||||
await send_message(ws, msg_type, target_id, HELP_TEXT)
|
||||
return
|
||||
|
||||
if arg == "":
|
||||
lines = DEFAULT_LINES
|
||||
else:
|
||||
try:
|
||||
lines = int(arg)
|
||||
except ValueError:
|
||||
await send_message(ws, msg_type, target_id,
|
||||
"参数必须为整数, 例如 .log 10 或 .log help")
|
||||
return
|
||||
if lines < 1:
|
||||
await send_message(ws, msg_type, target_id, "日志条数至少为 1")
|
||||
return
|
||||
if lines > MAX_LINES:
|
||||
lines = MAX_LINES
|
||||
|
||||
ok, text = await asyncio.to_thread(_tail_log, lines)
|
||||
if not ok:
|
||||
await send_message(ws, msg_type, target_id, text)
|
||||
return
|
||||
|
||||
body = _clip(_dedupe_merge(text))
|
||||
await send_message(ws, msg_type, target_id,
|
||||
f"最近 {lines} 条日志:\n━━━━━━━━━━━━\n{body}")
|
||||
Reference in New Issue
Block a user