- init.js: DISABLE_AUTO_LOGIN=true 时不自动 bot.start(), 导出 start/stop/isRunning - 新增 route/bot.js: POST /api/bot/login|logout, GET /api/bot/status (按需触发登录) - login.js: bot 未运行时不展示残留二维码, 显示开始登录按钮 - wechat4u wechat.js: checkLogin 忙循环加 3s 间隔+指数退避 (原来毫秒级轰炸 login.wx.qq.com) - puppet-wechat4u: logout 后不自动 start (原来登出即自动重登) - 补丁脚本: deploy/linux/wxbot_on_demand_login_patch.py (可重放, 容器重建后一键恢复) - 文档: docs/WXBOT_ON_DEMAND_LOGIN.md - agents_health_check/self_todo_executor: 禁用 wxBotWebhook 自动重启 (重启=重登触发风控)
149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
self_todo_executor.py — TODO 自修复执行器(Linux/246 部署版)
|
|
=============================================================
|
|
每 10 分钟触发。轮询 health_todos.jsonl 中 pending 条目,执行修复,标记结果。
|
|
|
|
部署:Linux crontab: */10 * * * * cd /home/hmo/AgentsMeeting/gateway/scripts && /usr/bin/python3 self_todo_executor.py >> ../logs/todo_executor.log 2>&1
|
|
"""
|
|
import json, os, sys, subprocess
|
|
from datetime import datetime
|
|
|
|
try:
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
except Exception:
|
|
pass
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
GATEWAY_DIR = os.path.dirname(SCRIPT_DIR)
|
|
TEMP = os.path.join(GATEWAY_DIR, "temp")
|
|
LOGS = os.path.join(GATEWAY_DIR, "logs")
|
|
os.makedirs(LOGS, exist_ok=True)
|
|
|
|
TODO_FILE = os.path.join(TEMP, "health_todos.jsonl")
|
|
EXECUTOR_LOG = os.path.join(LOGS, "todo_executor.log")
|
|
|
|
# ── 服务修复命令映射(key = service name)─────────────────
|
|
FIX_MAP = {
|
|
"dashboard": ["sudo", "systemctl", "restart", "agentsmeeting-dashboard"],
|
|
"hermes_gateway_mohe": ["sudo", "systemctl", "restart", "hermes-gateway@mohe"],
|
|
"hermes_gateway_zhiwei": ["sudo", "systemctl", "restart", "hermes-gateway@zhiwei"],
|
|
"wechat_bridge": None, # 禁用自动重启: 重启=重登触发风控, 人工处理
|
|
# 远程服务无本地修复命令
|
|
"xmpp_bot_xxm": None,
|
|
"article_processor": None,
|
|
}
|
|
|
|
|
|
def log(msg):
|
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
line = f"{ts} [executor] {msg}"
|
|
print(line, flush=True)
|
|
with open(EXECUTOR_LOG, "a", encoding="utf-8") as f:
|
|
f.write(line + "\n")
|
|
|
|
|
|
def read_pending_todos():
|
|
todos = []
|
|
if not os.path.exists(TODO_FILE):
|
|
return todos
|
|
try:
|
|
with open(TODO_FILE, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
entry = json.loads(line)
|
|
if entry.get("status") == "pending":
|
|
todos.append(entry)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
except Exception as e:
|
|
log(f"读取 TODO 失败: {e}")
|
|
return todos
|
|
|
|
|
|
def mark_todo(entry, status, result=""):
|
|
if not os.path.exists(TODO_FILE):
|
|
return
|
|
entry["status"] = status
|
|
entry["resolved_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
if result:
|
|
entry["result"] = result
|
|
|
|
lines = []
|
|
try:
|
|
with open(TODO_FILE, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
existing = json.loads(line)
|
|
if (existing.get("created") == entry.get("created")
|
|
and existing.get("service") == entry.get("service")):
|
|
lines.append(json.dumps(entry, ensure_ascii=False))
|
|
else:
|
|
lines.append(line)
|
|
except json.JSONDecodeError:
|
|
lines.append(line)
|
|
except Exception as e:
|
|
log(f"读取 TODO 文件失败: {e}")
|
|
return
|
|
|
|
try:
|
|
with open(TODO_FILE, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(lines) + "\n")
|
|
except Exception as e:
|
|
log(f"写入 TODO 文件失败: {e}")
|
|
|
|
|
|
def execute_fix(service_name):
|
|
"""执行修复命令。返回 (success, detail)。"""
|
|
cmd = FIX_MAP.get(service_name)
|
|
if not cmd:
|
|
return (False, "无本地修复命令(远程服务)")
|
|
|
|
log(f"执行修复: {service_name} → {' '.join(cmd)}")
|
|
try:
|
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
if r.returncode == 0:
|
|
return (True, "exit=0")
|
|
else:
|
|
detail = r.stderr[:200] if r.stderr else r.stdout[:200]
|
|
return (False, f"exit={r.returncode}: {detail}")
|
|
except subprocess.TimeoutExpired:
|
|
return (False, "timeout (30s)")
|
|
except Exception as e:
|
|
return (False, str(e)[:100])
|
|
|
|
|
|
def main():
|
|
log("=== TODO Executor 启动 ===")
|
|
todos = read_pending_todos()
|
|
log(f"待处理 TODO: {len(todos)} 条")
|
|
|
|
for entry in todos:
|
|
service = entry.get("service", "?")
|
|
issue = entry.get("issue", "?")
|
|
log(f"处理: {service} — {issue}")
|
|
|
|
success, detail = execute_fix(service)
|
|
if not success and "another instance" in detail.lower():
|
|
mark_todo(entry, "completed", "already_running")
|
|
log(f" ✅ {service}: 已在运行")
|
|
elif success:
|
|
mark_todo(entry, "completed", detail)
|
|
log(f" ✅ {service}: 修复成功")
|
|
else:
|
|
mark_todo(entry, "failed", detail)
|
|
log(f" ❌ {service}: 修复失败 ({detail})")
|
|
|
|
log(f"=== TODO Executor 完成 ({len(todos)} 条处理) ===")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|