105 lines
3.5 KiB
Python
105 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
kanban-opencode-dispatch.py — opencode 任务调度器(分环节模式)
|
||
=============================================================
|
||
不 spawn 2B agent(实测空转烧 GPU),改为:
|
||
1. 扫描看板 ready 状态的 opencode assignee 任务
|
||
2. 对每个任务跑 kanban-2b-worker.py(分环节:读任务→opencode+2B执行→验证→输出)
|
||
3. 根据结果 kanban complete(带证据)/ block(失败原因)
|
||
4. 并发限制:同时只跑 1 个(2B 单 slot)
|
||
|
||
用法:
|
||
python3 kanban-opencode-dispatch.py # 跑一轮(cron 调用)
|
||
python3 kanban-opencode-dispatch.py --once # 只处理 1 个任务
|
||
"""
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
|
||
HERMES = "/home/muc/bin/hermes"
|
||
WORKER = os.path.expanduser("~/.hermes/scripts/kanban-2b-worker.py")
|
||
|
||
|
||
def sh(cmd, timeout=60, cwd=None):
|
||
try:
|
||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=cwd,
|
||
env=dict(os.environ, PATH="/home/muc/nodejs/node-v24.16.0-linux-x64/bin:" + os.environ.get("PATH", "")))
|
||
return r.returncode, (r.stdout + r.stderr)
|
||
except Exception as e:
|
||
return -1, str(e)[:300]
|
||
|
||
|
||
def list_ready_opencode():
|
||
rc, out = sh([HERMES, "kanban", "list", "--json"])
|
||
if rc != 0:
|
||
print(json.dumps({"ok": False, "error": f"list failed: {out[:200]}"}))
|
||
return []
|
||
try:
|
||
d = json.loads(out)
|
||
tasks = d if isinstance(d, list) else d.get("tasks", [])
|
||
ready = [t for t in tasks
|
||
if t.get("assignee") == "opencode" and t.get("status") == "ready"]
|
||
return ready
|
||
except Exception as e:
|
||
print(json.dumps({"ok": False, "error": f"parse: {e}"}))
|
||
return []
|
||
|
||
|
||
def run_worker(task_id):
|
||
"""跑分环节 worker 脚本"""
|
||
rc, out = sh([sys.executable, WORKER, task_id], timeout=320)
|
||
return rc, out
|
||
|
||
|
||
def finish_task(task_id, result_text, ok):
|
||
if ok:
|
||
rc, out = sh([HERMES, "kanban", "complete", task_id,
|
||
"--summary", result_text[:1500]])
|
||
else:
|
||
rc, out = sh([HERMES, "kanban", "block", task_id, "--reason", result_text[:800]])
|
||
return rc, out
|
||
|
||
|
||
def main():
|
||
once = "--once" in sys.argv
|
||
tasks = list_ready_opencode()
|
||
print(json.dumps({"ok": True, "found_ready": len(tasks),
|
||
"tasks": [t.get("id") for t in tasks]}, ensure_ascii=False))
|
||
if not tasks:
|
||
return
|
||
|
||
batch = tasks[:1] if once else tasks
|
||
for t in batch:
|
||
tid = t["id"]
|
||
title = (t.get("title") or "")[:80]
|
||
print(f"\n=== 处理 {tid}: {title} ===")
|
||
# claim(防并发)
|
||
sh([HERMES, "kanban", "claim", tid], timeout=30)
|
||
rc, out = run_worker(tid)
|
||
# 提取 OPENCODE_RESULT 之后的摘要(worker 输出的结果段)
|
||
summary = ""
|
||
if "=== OPENCODE_RESULT ===" in out:
|
||
summary = out.split("=== OPENCODE_RESULT ===")[-1].strip()[:600]
|
||
else:
|
||
lines = [l for l in out.splitlines() if l.strip()]
|
||
summary = (lines[-3] if lines else "")[:600]
|
||
ok = False
|
||
# 从 worker 输出找 verified(注意 json.dumps 会转义引号,用宽松匹配)
|
||
try:
|
||
import re
|
||
if re.search(r'verified["\']?\s*[:=]\s*true', out, re.IGNORECASE):
|
||
ok = True
|
||
except Exception:
|
||
pass
|
||
print(f"worker exit={rc}, ok={ok}")
|
||
f_rc, f_out = finish_task(tid, summary, ok)
|
||
print(f"finish rc={f_rc}")
|
||
|
||
if once:
|
||
break
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|