- 多进程看门狗: xmpp_watchdog.py (守护 bot + article_processor) - Tier 1 快速健康检查: agents_health_check.py (5min) - Tier 2 全面健康检查: agents_daily_health.py (每日08:00) - TODO 自修复执行器: self_todo_executor.py (10min) - 共享服务注册表: service_registry.py - xmpp_agent_core: /health 免认证 + /send 端点 - Dashboard: SSH检测修复 + kanban版块 + 路径跨平台兼容 - 修复kanban-api-reference.md误导内容 - 修复R02: frp隧道heartbeat保活
181 lines
5.8 KiB
Python
181 lines
5.8 KiB
Python
"""
|
|
self_todo_executor.py — TODO 自修复执行器
|
|
- 轮询 health_todos.jsonl 中 status=pending 的条目
|
|
- 执行 fix_action(启动脚本)
|
|
- 成功→标记 completed
|
|
- 失败→ escalation 到 LLM(通过 xmpp_bot 通知)
|
|
|
|
MoFin self_todo_executor.py 的 AgentsMeeting 适配版。
|
|
"""
|
|
import json, os, sys, time, subprocess, shlex, urllib.request
|
|
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)
|
|
BASE = os.path.dirname(GATEWAY_DIR)
|
|
TEMP = os.path.join(BASE, "gateway", "temp")
|
|
LOGS = os.path.join(BASE, "gateway", "logs")
|
|
PYTHON = r"C:\Users\hmo\AppData\Local\Programs\Python\Python310\python.exe"
|
|
|
|
TODO_FILE = os.path.join(TEMP, "health_todos.jsonl")
|
|
EXECUTOR_LOG = os.path.join(LOGS, "todo_executor.log")
|
|
|
|
|
|
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():
|
|
"""读取 health_todos.jsonl 中所有 status=pending 的条目。"""
|
|
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=""):
|
|
"""在 JSONL 中将条目标记为 completed 或 failed。"""
|
|
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)
|
|
# 按 created + service 匹配
|
|
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(entry):
|
|
"""执行修复操作。返回 (success, detail)。"""
|
|
script = entry.get("fix_script", "")
|
|
args = entry.get("fix_args", [])
|
|
cwd = entry.get("fix_cwd", None)
|
|
service = entry.get("service", "unknown")
|
|
|
|
if not script or not os.path.exists(script):
|
|
return (False, f"修复脚本不存在: {script}")
|
|
|
|
cmd = [PYTHON, script] + list(args)
|
|
log(f"执行修复: {service} → {' '.join(cmd[-3:])}")
|
|
|
|
try:
|
|
r = subprocess.run(
|
|
cmd,
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
)
|
|
if r.returncode == 0:
|
|
return (True, f"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 escalate_to_xmpp(entry, result):
|
|
"""通过 xmpp_bot 的 HTTP 桥发送告警到群聊(失败 escalation)。"""
|
|
service = entry.get("service", "?")
|
|
issue = entry.get("issue", "?")
|
|
detail = result[:200]
|
|
payload = json.dumps({
|
|
"message": f"[executor] 修复失败: {service}\n问题: {issue}\n结果: {detail}\n需要人工介入"
|
|
}).encode("utf-8")
|
|
try:
|
|
req = urllib.request.Request(
|
|
"http://127.0.0.1:5802/send",
|
|
data=payload,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Api-Key": "xxm_bridge_8f3a2c",
|
|
},
|
|
)
|
|
urllib.request.urlopen(req, timeout=5)
|
|
log(f"已通过 XMPP 发送 escalation: {service}")
|
|
except Exception as e:
|
|
log(f"XMPP escalation 失败: {e}")
|
|
|
|
|
|
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(entry)
|
|
if success:
|
|
mark_todo(entry, "completed", detail)
|
|
log(f" ✅ {service}: 修复成功 ({detail})")
|
|
else:
|
|
mark_todo(entry, "failed", detail)
|
|
log(f" ❌ {service}: 修复失败 ({detail}) → escalation")
|
|
try:
|
|
escalate_to_xmpp(entry, detail)
|
|
except Exception as e:
|
|
log(f" escalation 异常: {e}")
|
|
|
|
log(f"=== TODO Executor 完成 ({len(todos)} 条处理) ===")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|