# -*- coding: utf-8 -*- """🔴 保活机制收敛(2026-10-05 用户定案)——「计划任务起个程序」+「一键能关掉」。 用户口径(逐字): · 「**常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?**」 · 「**我总要以一键能关掉这些东西吧,我关不掉这些东西,我还要投诉你呢**」 ⇒ 正解=**计划任务直接起 `collabd.py --supervise`**(**常驻本体**,自带内部循环 10~30 s 一轮), 动作**必须是 `pythonw.exe`**(GUI 子系统 ⇒ 不分配控制台 ⇒ 不弹窗)。 🔴🔴 **2026-10-05 实测推翻了两条旧结论(都在本机跑出来的,不是推的)**: ① **`IsProcessInJob` 在本机没有鉴别力** —— 四组对照(任务 Highest/Interactive、 任务 Limited/S4U、任务 Highest/S4U、subprocess 直起)**全部返回 IN-JOB** ⇒ 本沙箱给谁都套作业对象 ⇒ **不能用它判断"会不会被会话树带走"**。 ✅ 真判据=**父链根在哪**:会话树里的进程父链一路是 `bash → sandbox-cli → WorkBuddy.exe → explorer`; 任务起的进程**父进程已退出、链断在自己身上**(实测 30560/60596/70228 三件皆如此)。 ② **常驻不需要网关口令** —— `--supervise` 唯一职责=**建检查会话排期** (`maybe_spawn_check_agent` ⇒ `create_check_schedule` ⇒ **直连 SQLite 写 `automations` 表**), **全程不走网关**。口令只在 `--ensure` 的 **spawn 分支**才是前置门(`ensure_supervise` 里那条)。 ✅ **隔离区实测**:**不含口令**的计划任务起 `--supervise` ⇒ 15 秒内写出心跳(pid 3944 · round=2)。 ⇒ 所以**保活必须用 `--supervise`**,⛔ 不是 `--ensure`(那在任务环境下会因无口令而"起了就退")。 **为什么不是 `powershell.exe`**:它是控制台程序,启动必然分配控制台 ⇒ 闪窗(10-05 弹窗事故的真因)。 **为什么不是 `start-supervise.ps1` 那个看守循环**:常驻**自带内部循环**(`--supervise` 本身不退) ⇒ 外层再套"永不退出的看守"纯属多余,那层看守才是当初每 60 秒创建进程的来源。 **为什么是 5 分钟**:`--supervise` 是**长驻**的,**不需要周期叫醒**; 这个 5 分钟只是**兜底**——万一常驻真被杀,最多 5 分钟内被重新拉起(一次性判活,判到就走)。 ⚠️ 判活由 `--supervise` 自己**进场即判**(`supervise_alive()`:pid 活 + 心跳新 + 身份核验), **已在 ⇒ 新起的立刻让位退出**(幂等,⛔ 不会"叠加成两条")。 **一键关掉(新增能力)**:本脚本支持 `off` ⇒ 一个动作做完四件事: ① 关两区开关 ② 禁用所有相关计划任务 ③ 杀掉所有相关进程 ④ 打印复核结果 ⇒ ⛔ 不再需要"禁用任务 + 杀进程"分开记(10-05 就是因为只禁任务没杀进程,以为停了其实没停)。 用法: pythonw setup_keepalive.py status # 只读盘点(开关/存活/任务/进程) pythonw setup_keepalive.py on --apply # 建立保活(写计划任务)+ 起一次 pythonw setup_keepalive.py off --apply # 🔴 一键全关(幂等,可反复跑) ⛔ 不带 --apply 一律干跑,只打印动作。 """ import os import subprocess import sys PYW = r"E:\ProgramData\.workbuddy\binaries\python\versions\3.13.12\pythonw.exe" AIROOT = r"E:\ProgramData\AIProject" # 🔴 登记制:工作区 → 保活任务名(⛔ 只在这里写一份) WS = { "ai1net-dsh-server": "collabd-keepalive-ai1net-dsh-server", "vibe-product": "collabd-keepalive-vibe-product", } # 看板:全局只一条 BOARD_PY = r"E:\ProgramData\.workbuddy\skills\session-mechanism\scripts\board.py" BOARD_TASK = "dsh-board-keepalive" BOARD_PORT = 20099 # 旧任务(10-05 弹窗事故的那批)⇒ 一律禁用,⛔ 不删除(可逆) OLD_TASKS = [ "collabd-supervise-ai1net-dsh-server", "collabd-supervise-vibe-product", "collabd-supervise-selftest", "dsh-board-20099", ] HIDE = 0x08000000 # NO_WINDOW:本脚本自己起 powershell 时也不许闪窗 INTERVAL_MIN = 5 APPLY = "--apply" in sys.argv CMD = next((a for a in sys.argv[1:] if not a.startswith("--")), "status") def ps(script, timeout=90): try: r = subprocess.run(["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script], capture_output=True, timeout=timeout, check=False, creationflags=HIDE) return (r.stdout or b"").decode("utf-8", "replace") except Exception as e: return "⛔ ps 失败:%r" % (e,) def switch_path(ws): return os.path.join(AIROOT, ws, ".workbuddy", "collab", "supervise.switch") def set_switch(ws, val): p = switch_path(ws) try: os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "w", encoding="utf-8", newline="\n") as f: f.write(val) return True except Exception as e: print(" ⛔ 写开关失败 %s:%r" % (p, e)) return False def switch_of(ws): try: with open(switch_path(ws), encoding="utf-8", errors="replace") as f: return "on" if f.read().strip().lower() == "on" else "off" except Exception: return "off" def hb_of(ws): import json import time p = os.path.join(AIROOT, ws, ".workbuddy", "collab", "logs", "supervise-heartbeat.json") try: with open(p, encoding="utf-8-sig") as f: d = json.load(f) ts = float(d.get("ts") or 0) return int(d.get("pid") or 0), (time.time() - ts if ts else None) except Exception: return 0, None def pid_alive(pid): import ctypes try: h = ctypes.windll.kernel32.OpenProcess(0x1000, False, int(pid)) if not h: return False ctypes.windll.kernel32.CloseHandle(h) return True except Exception: return False def alive_of(ws): pid, age = hb_of(ws) if not pid or age is None: return False, "无心跳" if age > 90: return False, "心跳停了 %.0f 分钟" % (age / 60.0) if not pid_alive(pid): return False, "心跳新但进程已死" return True, "pid %d · 心跳 %.0f 秒前" % (pid, age) def task_state(name): st = ps("(Get-ScheduledTask | Where-Object { $_.TaskName -eq '%s' }).State" % name).strip() return st or "未知" def make_task(task, exe, args, workdir): """🔴 建保活任务:动作=**pythonw.exe**(⛔ 不是 powershell/cmd)。 🔴🔴 2026-10-05 实测补丁:`workdir` **必须是「工作区根」**,⛔ **不是脚本所在目录**。 病根:`collabd.load_cfg()` 按 `/.workbuddy/collab/collabd.config.json` 找配置。 若把 cwd 设成**脚本目录**(`/.workbuddy/collab`)⇒ 它去找 `/.workbuddy/collab/.workbuddy/collab/collabd.config.json` ⇒ **必然找不到** ⇒ `CFG_MISSING` ⇒ **`--supervise` 直接拒跑**(实测 rc=2;任务侧记 `LastResult=1`)。 ✅ 改成**工作区根** ⇒ `State=Running`、`LastResult=267009`、心跳正常(实测)。 """ script = ( "$a = New-ScheduledTaskAction -Execute '%s' -Argument '%s' -WorkingDirectory '%s';" "$t = New-ScheduledTaskTrigger -Once -At (Get-Date) " "-RepetitionInterval (New-TimeSpan -Minutes %d);" "$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries " "-StartWhenAvailable -MultipleInstances IgnoreNew " "-ExecutionTimeLimit (New-TimeSpan -Minutes 2) -Hidden;" "$p = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest;" "Register-ScheduledTask -TaskName '%s' -Action $a -Trigger $t -Settings $s -Principal $p " "-Force | Out-Null; 'ok %s'" ) % (exe, args, workdir, INTERVAL_MIN, task, task) return ps(script).strip() def kill_all(): """🔴 杀干净:**先把心跳里记的 pid 杀掉**(不依赖 PowerShell 读进程 —— 那条路读不到会漏杀)。""" n = 0 for ws in WS: pid, _ = hb_of(ws) if pid and pid_alive(pid): subprocess.run(["taskkill", "/F", "/PID", str(pid)], capture_output=True, timeout=20, check=False, creationflags=HIDE) n += 1 out = ps("Get-CimInstance Win32_Process | Where-Object { " "$_.CommandLine -like '*collabd.py*' -or $_.CommandLine -like '*start-supervise*' " "-or $_.CommandLine -like '*board.py*' -or $_.CommandLine -like '*board-keeper*' } | " "ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }") n += len([x for x in out.split() if x.strip().isdigit()]) return n def cmd_status(): print("=" * 62) for ws in WS: ok, why = alive_of(ws) print(" %-22s 开关=%-3s %s %s" % (ws, switch_of(ws), "✅活" if ok else "⛔停", why)) print("") print(" 计划任务:") for ws, t in WS.items(): print(" %-38s = %s" % (t, task_state(t))) print(" %-38s = %s" % (BOARD_TASK, task_state(BOARD_TASK))) for t in OLD_TASKS: print(" %-38s = %s(旧,应保持禁用)" % (t, task_state(t))) print("=" * 62) def cmd_on(): print("── 建立保活:计划任务 → pythonw collabd.py --supervise(每 %d 分钟兜底判活)──" % INTERVAL_MIN) for ws, task in WS.items(): script = os.path.join(AIROOT, ws, ".workbuddy", "collab", "collabd.py") if not os.path.isfile(script): print(" ⚠️ 跳过(副本不存在):%s" % script) continue print(" " + make_task(task, PYW, '"%s" --supervise' % script, os.path.join(AIROOT, ws))) set_switch(ws, "on") print(" 开关 on:%s" % ws) if os.path.isfile(BOARD_PY): print(" " + make_task(BOARD_TASK, PYW, '"%s" --serve %d' % (BOARD_PY, BOARD_PORT), os.path.dirname(BOARD_PY))) for t in OLD_TASKS: ps("Disable-ScheduledTask -TaskName '%s' -ErrorAction SilentlyContinue | Out-Null" % t) print(" 旧任务已禁用:%d 个" % len(OLD_TASKS)) # 立刻起一次,不用等 5 分钟 for t in list(WS.values()) + [BOARD_TASK]: ps("Start-ScheduledTask -TaskName '%s' -ErrorAction SilentlyContinue | Out-Null" % t) print(" 已触发一次(立即生效,不必等 5 分钟)") def cmd_off(): """🔴 **一键全关**(用户要的那个按钮):开关+任务+进程,一次做完,并复核。""" print("── 一键全关 ──") for ws in WS: set_switch(ws, "off") print(" ① 开关 off:%s" % ws) for t in list(WS.values()) + [BOARD_TASK] + OLD_TASKS: ps("Disable-ScheduledTask -TaskName '%s' -ErrorAction SilentlyContinue | Out-Null" % t) print(" ② 计划任务已禁用:%d 个" % (len(WS) + 1 + len(OLD_TASKS))) n = kill_all() print(" ③ 已杀进程:%d 个" % n) print("") print("── 复核(⛔ 不许只说『已停』就完事)──") cmd_status() def main(): if not APPLY and CMD in ("on", "off"): print("(干跑)将要执行:%s —— 加 --apply 真做" % CMD) if CMD == "off": print(" ① 关两区开关 ② 禁用 %d 个计划任务 ③ 杀相关进程 ④ 复核" % (len(WS) + 1 + len(OLD_TASKS))) else: for ws, t in WS.items(): print(" 建任务 %s → 每 %d 分钟 pythonw collabd.py --supervise" % (t, INTERVAL_MIN)) print(" 建任务 %s → pythonw board.py --serve %d" % (BOARD_TASK, BOARD_PORT)) print(" 禁用旧任务 %d 个;两区开关置 on" % len(OLD_TASKS)) return 0 if CMD == "status": cmd_status() elif CMD == "on": cmd_on() elif CMD == "off": cmd_off() else: print(__doc__) return 1 return 0 if __name__ == "__main__": sys.exit(main())