98 lines
3.7 KiB
Python
98 lines
3.7 KiB
Python
#!/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())
|