103 lines
4.2 KiB
Python
103 lines
4.2 KiB
Python
# checklist_helper.py — CL(folder) 一站式任务清单(支持 checklist/mapreduce 两种模式)
|
|
import json, time, subprocess, socket, sys
|
|
from pathlib import Path
|
|
_R = Path(__file__).resolve().parent.parent
|
|
_BBS, _MAIN = _R/"assets/agent_bbs.py", _R/"agentmain.py"
|
|
_W_RE, _M_RE = _R/"reflect/agent_team_worker.py", _R/"reflect/checklist_master.py"
|
|
_PK = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}
|
|
if sys.platform == "win32": _PK["creationflags"] = 0x200
|
|
|
|
class CL:
|
|
def __init__(self, folder, goal="", workers=0):
|
|
"""
|
|
workers=0: checklist模式,master自己逐个执行,不启动BBS
|
|
workers>0: mapreduce模式,启动BBS+N个worker并行
|
|
"""
|
|
self.folder = Path(folder); self.folder.mkdir(parents=True, exist_ok=True)
|
|
self.path = self.folder / "state.json"
|
|
self.workers = workers
|
|
if self.path.exists(): self._d = json.loads(self.path.read_text("utf-8"))
|
|
else:
|
|
self._d = {"closed": False, "goal": goal, "bbs": None, "tasks": []}
|
|
self._save()
|
|
if workers > 0:
|
|
self._ensure_bbs()
|
|
self.start_worker(workers)
|
|
|
|
@property
|
|
def tasks(self): return self._d["tasks"]
|
|
@property
|
|
def closed(self): return self._d.get("closed", False)
|
|
@property
|
|
def has_open(self): return any(t["result"] is None for t in self.tasks)
|
|
@property
|
|
def bbs_url(self): return self._d["bbs"]["url"] if self._d["bbs"] else None
|
|
@property
|
|
def bbs_key(self): return self._d["bbs"]["key"] if self._d["bbs"] else None
|
|
@property
|
|
def mode(self): return "mapreduce" if self._d["bbs"] else "checklist"
|
|
def _save(self): self.path.write_text(json.dumps(self._d, ensure_ascii=False, indent=1), "utf-8")
|
|
|
|
def _ensure_bbs(self):
|
|
if self._d["bbs"]: return
|
|
with socket.socket() as s: s.bind(('',0)); port = s.getsockname()[1]
|
|
key = f"cl_{int(time.time())%1000}"
|
|
(self.folder/"bbs").mkdir(exist_ok=True)
|
|
subprocess.Popen(["python", str(_BBS), "--cwd", str(self.folder/"bbs"),
|
|
"--port", str(port), "--key", key], **_PK)
|
|
time.sleep(1)
|
|
self._d["bbs"] = {"url": f"http://127.0.0.1:{port}", "key": key}
|
|
self._save()
|
|
|
|
def add(self, texts):
|
|
nid = max((t["id"] for t in self.tasks), default=0) + 1
|
|
ids = []
|
|
for t in texts:
|
|
self.tasks.append({"id": nid, "text": t, "result": None, "ts": int(time.time())})
|
|
ids.append(nid); nid += 1
|
|
self._save();
|
|
print('task added, must reread checklist SOP before start executing ...');
|
|
return ids
|
|
|
|
def mark(self, tid, result):
|
|
for t in self.tasks:
|
|
if t["id"] == tid: t["result"] = result; t["ts"] = int(time.time()); break
|
|
self._save()
|
|
|
|
def look(self):
|
|
done = sum(1 for t in self.tasks if t["result"] is not None)
|
|
lines = [f"[{done}/{len(self.tasks)}] mode={self.mode}"]
|
|
for t in self.tasks:
|
|
l = f'{"✓" if t["result"] else "○"} #{t["id"]} {t["text"][:60]}'
|
|
if t["result"]: l += f' → {t["result"][:60]}'
|
|
lines.append(l)
|
|
return "\n".join(lines)
|
|
|
|
def close(self):
|
|
assert not self.has_open, "has open tasks"
|
|
self._d["closed"] = True; self._save()
|
|
|
|
def start_worker(self, n=None):
|
|
n = n or self.workers or 1
|
|
if n <= 0: return
|
|
for i in range(n):
|
|
subprocess.Popen(["python", str(_MAIN), "--reflect", str(_W_RE),
|
|
"--base_url", self.bbs_url, "--board_key", self.bbs_key, "--name", f"w{i+1}"], **_PK)
|
|
if i < n - 1: time.sleep(5)
|
|
|
|
def _pid_alive(self, pid):
|
|
if not pid: return False
|
|
try:
|
|
r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}"], capture_output=True, text=True)
|
|
return str(pid) in r.stdout
|
|
except Exception: return False
|
|
|
|
def start_master(self):
|
|
old_pid = self._d.get("master_pid")
|
|
if old_pid and self._pid_alive(old_pid):
|
|
print(f"[CL] master already running (PID {old_pid}), skip")
|
|
return
|
|
p = subprocess.Popen(["python", str(_MAIN), "--reflect", str(_M_RE),
|
|
"--mr_folder", str(self.folder.resolve())], **_PK)
|
|
self._d["master_pid"] = p.pid; self._save()
|