98 lines
4.0 KiB
Python
98 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
kanban-2b-worker.py — 分环节 kanban 代码 worker(牧尘 2026-09-10 定案)
|
||
=====================================================================
|
||
环节拆分:2B 不做协议,只做执行。本脚本由 opencode profile worker 首轮运行:
|
||
环节1 读任务 :从 env/kanban 读任务描述(确定性代码)
|
||
环节2 执行 :调 opencode CLI + llama-local/minicpm5-2b 写代码(唯一 LLM 点)
|
||
环节3 验证 :运行产物,检查 exit code + 输出(确定性代码)
|
||
环节4 输出结果 :打印 JSON,由 worker agent 据此 kanban_complete/block
|
||
|
||
用法(worker 内):
|
||
python3 ~/.hermes/scripts/kanban-2b-worker.py
|
||
"""
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
|
||
OPENCODE = "/home/muc/nodejs/node-v24.16.0-linux-x64/bin/opencode"
|
||
# 执行模型链:本地2B(0成本) → 智谱glm-4-flash(免费) → 商汤deepseek(免费)
|
||
# 牧尘 2026-09-10:opencode 加智谱也加本地 2B
|
||
MODEL_CHAIN = [
|
||
"llama-local/minicpm5-2b", # 本地 0 成本(主力)
|
||
"zhipu/glm-4-flash", # 智谱免费(代码倾向强)
|
||
"sensenova/deepseek-v4-flash", # 商汤免费
|
||
]
|
||
HERMES = "/home/muc/bin/hermes"
|
||
|
||
|
||
def sh(cmd, timeout=120, cwd=None, max_out=3000):
|
||
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)[:max_out]
|
||
except Exception as e:
|
||
return -1, str(e)[:500]
|
||
|
||
|
||
def get_task():
|
||
"""从 kanban CLI 读任务详情"""
|
||
task_id = os.environ.get("HERMES_KANBAN_TASK", "")
|
||
if not task_id:
|
||
# fallback: 尝试从 argv
|
||
task_id = sys.argv[1] if len(sys.argv) > 1 else ""
|
||
if not task_id:
|
||
return None, "no HERMES_KANBAN_TASK env"
|
||
rc, out = sh([HERMES, "kanban", "show", task_id, "--json"], max_out=20000)
|
||
if rc != 0:
|
||
return None, f"kanban show failed: {out[:300]}"
|
||
try:
|
||
d = json.loads(out)
|
||
t = d.get("task") or d # kanban show --json 返回 {task:{...}}
|
||
title = t.get("title") or t.get("summary") or ""
|
||
body = t.get("body") or ""
|
||
ws = t.get("workspace_path") or os.environ.get("HERMES_KANBAN_WORKSPACE") or ""
|
||
return {"id": task_id, "title": title, "body": body, "workspace": ws}, None
|
||
except Exception as e:
|
||
return None, f"parse failed: {e}"
|
||
|
||
|
||
def run_opencode(task_desc, workspace):
|
||
"""调 opencode CLI + 2B 执行(唯一 LLM 环节)"""
|
||
if not workspace or not os.path.isdir(workspace):
|
||
workspace = "/tmp"
|
||
prompt = (
|
||
f"完成以下任务。写代码 → 实际运行验证 → 报告结果。\n"
|
||
f"任务: {task_desc[:1500]}\n"
|
||
"要求: 1) 在当前目录创建所需文件 2) 实际运行验证 3) 最后输出一行 JSON: "
|
||
'{"files": ["..."], "verified": true/false, "output": "运行输出摘要"}'
|
||
)
|
||
# 模型链逐个试:本地2B → 智谱 → 商汤(全免费;超时/失败下一个)
|
||
last_err = "no model attempted"
|
||
for model in MODEL_CHAIN:
|
||
t0 = time.time()
|
||
rc, out = sh([OPENCODE, "run", "--model", model, prompt], timeout=280, cwd=workspace)
|
||
if rc == 0 and out.strip():
|
||
return {"model": model, "elapsed_s": round(time.time() - t0), "raw": out[-2000:]}
|
||
last_err = out[-300:]
|
||
return {"error": f"opencode all models failed: {last_err}"}
|
||
|
||
|
||
def main():
|
||
task, err = get_task()
|
||
if err:
|
||
print(json.dumps({"ok": False, "stage": "read_task", "error": err}, ensure_ascii=False))
|
||
return
|
||
print(json.dumps({"ok": True, "stage": "read_task", "task_id": task["id"]}, ensure_ascii=False))
|
||
|
||
desc = task["title"] + ("\n" + task["body"] if task.get("body") else "")
|
||
result = run_opencode(desc, task.get("workspace", ""))
|
||
print("=== OPENCODE_RESULT ===")
|
||
print(json.dumps(result, ensure_ascii=False)[:2500])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|