|
| 1 | +# checklist_helper.py — CL(folder) 一站式任务清单(支持 checklist/mapreduce 两种模式) |
| 2 | +import json, time, subprocess, socket, sys |
| 3 | +from pathlib import Path |
| 4 | +_R = Path(__file__).resolve().parent.parent |
| 5 | +_BBS, _MAIN = _R/"assets/agent_bbs.py", _R/"agentmain.py" |
| 6 | +_W_RE, _M_RE = _R/"reflect/agent_team_worker.py", _R/"reflect/checklist_master.py" |
| 7 | +_PK = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} |
| 8 | +if sys.platform == "win32": _PK["creationflags"] = 0x200 |
| 9 | + |
| 10 | +class CL: |
| 11 | + def __init__(self, folder, goal="", workers=0): |
| 12 | + """ |
| 13 | + workers=0: checklist模式,master自己逐个执行,不启动BBS |
| 14 | + workers>0: mapreduce模式,启动BBS+N个worker并行 |
| 15 | + """ |
| 16 | + self.folder = Path(folder); self.folder.mkdir(parents=True, exist_ok=True) |
| 17 | + self.path = self.folder / "state.json" |
| 18 | + self.workers = workers |
| 19 | + if self.path.exists(): self._d = json.loads(self.path.read_text("utf-8")) |
| 20 | + else: |
| 21 | + self._d = {"closed": False, "goal": goal, "bbs": None, "tasks": []} |
| 22 | + self._save() |
| 23 | + if workers > 0: |
| 24 | + self._ensure_bbs() |
| 25 | + self.start_worker(workers) |
| 26 | + |
| 27 | + @property |
| 28 | + def tasks(self): return self._d["tasks"] |
| 29 | + @property |
| 30 | + def closed(self): return self._d.get("closed", False) |
| 31 | + @property |
| 32 | + def has_open(self): return any(t["result"] is None for t in self.tasks) |
| 33 | + @property |
| 34 | + def bbs_url(self): return self._d["bbs"]["url"] if self._d["bbs"] else None |
| 35 | + @property |
| 36 | + def bbs_key(self): return self._d["bbs"]["key"] if self._d["bbs"] else None |
| 37 | + @property |
| 38 | + def mode(self): return "mapreduce" if self._d["bbs"] else "checklist" |
| 39 | + def _save(self): self.path.write_text(json.dumps(self._d, ensure_ascii=False, indent=1), "utf-8") |
| 40 | + |
| 41 | + def _ensure_bbs(self): |
| 42 | + if self._d["bbs"]: return |
| 43 | + with socket.socket() as s: s.bind(('',0)); port = s.getsockname()[1] |
| 44 | + key = f"cl_{int(time.time())%1000}" |
| 45 | + (self.folder/"bbs").mkdir(exist_ok=True) |
| 46 | + subprocess.Popen(["python", str(_BBS), "--cwd", str(self.folder/"bbs"), |
| 47 | + "--port", str(port), "--key", key], **_PK) |
| 48 | + time.sleep(1) |
| 49 | + self._d["bbs"] = {"url": f"http://127.0.0.1:{port}", "key": key} |
| 50 | + self._save() |
| 51 | + |
| 52 | + def add(self, texts): |
| 53 | + nid = max((t["id"] for t in self.tasks), default=0) + 1 |
| 54 | + ids = [] |
| 55 | + for t in texts: |
| 56 | + self.tasks.append({"id": nid, "text": t, "result": None, "ts": int(time.time())}) |
| 57 | + ids.append(nid); nid += 1 |
| 58 | + self._save(); return ids |
| 59 | + |
| 60 | + def mark(self, tid, result): |
| 61 | + for t in self.tasks: |
| 62 | + if t["id"] == tid: t["result"] = result; t["ts"] = int(time.time()); break |
| 63 | + self._save() |
| 64 | + |
| 65 | + def look(self): |
| 66 | + done = sum(1 for t in self.tasks if t["result"] is not None) |
| 67 | + lines = [f"[{done}/{len(self.tasks)}] mode={self.mode}"] |
| 68 | + for t in self.tasks: |
| 69 | + l = f'{"✓" if t["result"] else "○"} #{t["id"]} {t["text"][:60]}' |
| 70 | + if t["result"]: l += f' → {t["result"][:60]}' |
| 71 | + lines.append(l) |
| 72 | + return "\n".join(lines) |
| 73 | + |
| 74 | + def close(self): |
| 75 | + assert not self.has_open, "has open tasks" |
| 76 | + self._d["closed"] = True; self._save() |
| 77 | + |
| 78 | + def start_worker(self, n=None): |
| 79 | + n = n or self.workers or 1 |
| 80 | + if n <= 0: return |
| 81 | + for i in range(n): |
| 82 | + subprocess.Popen(["python", str(_MAIN), "--reflect", str(_W_RE), |
| 83 | + "--base_url", self.bbs_url, "--board_key", self.bbs_key, "--name", f"w{i+1}"], **_PK) |
| 84 | + if i < n - 1: time.sleep(5) |
| 85 | + |
| 86 | + def start_master(self): |
| 87 | + subprocess.Popen(["python", str(_MAIN), "--reflect", str(_M_RE), |
| 88 | + "--mr_folder", str(self.folder.resolve())], **_PK) |
0 commit comments