xiaowei-system/scripts/kanban-watch.py

98 lines
3.7 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 python3
"""kanban-watch.py — 团队任务巡检2026-09-05
每 30 分钟由 cron 触发。状态变化/异常才输出watchdog 静默模式):
- blocked 任务crash/protocol_violation→ 告警
- running 超时(>25min 未完成)→ 提示可能卡死/加戏
- 新完成 → 简报
- 异常任务连续存在 → 只报一次(防刷屏)
"""
import json, os, subprocess, sys, datetime
def get_tasks():
r = subprocess.run(["hermes", "kanban", "list", "--json"],
capture_output=True, timeout=30)
return json.loads(r.stdout.decode("utf-8", errors="replace"))
def main():
state_file = "/tmp/kanban-watch-state.json"
try:
tasks = get_tasks()
except Exception as e:
print(f"❌ kanban 巡检失败: {e}")
return 1
active = [t for t in tasks if t.get("status") in ("ready", "running", "blocked")]
done_ids = {t["id"] for t in tasks if t.get("status") == "done"}
# 加载上次状态
prev = {}
if os.path.exists(state_file):
try:
prev = json.load(open(state_file))
except Exception:
prev = {}
prev_blocked = set(prev.get("blocked", []))
prev_done = set(prev.get("done", []))
prev_running = set(prev.get("running", []))
now_blocked = {t["id"]: t.get("title", "?")[:60] for t in tasks if t.get("status") == "blocked"}
now_running = {}
for t in tasks:
if t.get("status") == "running":
now_running[t["id"]] = t.get("title", "?")[:60]
new_blocked = set(now_blocked) - prev_blocked
new_done = done_ids - prev_done
still_blocked = set(now_blocked) & prev_blocked
new_running = set(now_running) - prev_running
lines = []
# 新 blocked → 告警
if new_blocked:
lines.append("🔴 任务异常(新 blocked:")
for tid in sorted(new_blocked):
lines.append(f"{tid[:12]} [{now_blocked[tid]}]")
# 查 reason
try:
r = subprocess.run(["hermes", "kanban", "show", tid], capture_output=True, text=True, timeout=20)
for ln in r.stdout.split("\n"):
if "error" in ln.lower() or "crash" in ln.lower():
lines.append(f" {ln.strip()[:120]}")
break
except Exception:
pass
# running 超时(>25min
for t in tasks:
if t.get("status") == "running" and t.get("started_at"):
try:
st = datetime.datetime.fromisoformat(t["started_at"].replace("Z", "+00:00"))
if datetime.datetime.now(datetime.timezone.utc) - st > datetime.timedelta(minutes=25):
lines.append(f"⏱️ running 超时 >25min: {t['id'][:12]} [{t.get('title','?')[:50]}]")
except Exception:
pass
# 新 done → 简报
if new_done:
lines.append("✅ 任务完成:")
for tid in sorted(new_done):
title = next((t.get("title", "?") for t in tasks if t["id"] == tid), "?")
who = next((t.get("assignee", "?") for t in tasks if t["id"] == tid), "?")
lines.append(f"{tid[:12]} [{who}] {title[:50]}")
# 存状态
json.dump({"blocked": list(now_blocked), "done": list(done_ids),
"running": list(now_running)}, open(state_file, "w"))
if lines:
print("📋 团队任务巡检 " + datetime.datetime.now().strftime("%H:%M"))
print("\n".join(lines))
# 附活跃任务全貌
if active:
print(f"\n活跃: {len(active)} | running={len(now_running)} blocked={len(now_blocked)}")
else:
# 静默(状态无变化)
pass
return 0
if __name__ == "__main__":
sys.exit(main())