# -*- coding: utf-8 -*- """deploy_code —— 把**程序文件**复制到某个工作区,让那个工作区**跑自己的一份**。 🔴 为什么需要它(用户 2026-10-03 定案): 「**跨工作区使用会话协作技能,除了看板共用,其余都是独立的,包括程序和相关文件**」 ⇒ 改口径之前,**所有工作区跑的都是技能目录里同一份 `collabd.py`**(靠 `COLLABD_CONFIG` 区分工作区)。 ⚠️ 那种形态下:改一份代码 ⇒ **所有工作区同时变了**;某个区的代码坏了 ⇒ 别的区一起坏。 ⇒ 现在改成:**技能目录=源(唯一真身)**,每个工作区 `.workbuddy/collab/` 下有**自己的副本**。 ✅ 判据(能不能证明"真的独立"): ① 副本文件在位且 **md5 与源一致**(本脚本打印); ② 心跳里的 **`argv0`** 指向**该工作区的副本路径**(由 `collabd.py` 写进心跳)—— ⛔ 只看"文件在"不够:进程可能还在跑旧的那份(改完不重启=没生效,P0-17 同族)。 用法: python deploy_code.py --ws E:/ProgramData/AIProject/<工作区> # 部署默认清单 python deploy_code.py --ws <工作区> --dry-run # 只报要做什么 python deploy_code.py --ws <工作区> --only collabd.py # 只部一个 ⚠️ **改完代码要重跑本命令**(各区副本不会自动跟着变)—— 这是"独立"的代价:⛔ 一处改动要分发 N 处 ⇒ 改完请顺手跑一遍,别等某个区行为不对再来查。 """ from __future__ import annotations import argparse import hashlib import os import shutil import sys from pathlib import Path HERE = Path(__file__).resolve().parent # 🔴 各工作区默认需要的**独立程序**(⛔ 不含 `board.py` —— 看板是**共用一份**的) # 🔴🔴 2026-10-05 补:**管理入口也必须进清单** —— 原先只有 `collabd.py`/`goalctl.py`, # 结果 `collabctl.py`(用户双击 `.bat` 调的那个)**从不被分发** ⇒ 各区副本停留在旧版 # ⇒ 「看着改了、其实各区没吃到」= P0-57 同族再次发生(本轮实测:新副本 23176 B/旧副本 16982 B)。 # ⛔ 判据:**凡是被计划任务、`.bat`、hook 直接执行的本包脚本,都在此清单内**。 DEFAULT_FILES = [ "collabd.py", # 常驻本体(各区独立跑) "goalctl.py", # 目标控制 "collabctl.py", # 🔴 管理入口(一键开关 .bat 调它) "guard.py", # 守护判据 "session-rules-check.py", # 命名/角色一致性闸门(hook 调) "init_workspace.py", # 工作区初始化 "supervise-launch.py", # 🔴 常驻启动器(计划任务载体指向它 ⇒ 进程内设 CODEBUDDY_CONFIG_DIR) ] def md5(p: Path) -> str: try: return hashlib.md5(p.read_bytes()).hexdigest() except Exception: return "?" def main() -> int: try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass ap = argparse.ArgumentParser() ap.add_argument("--ws", required=True, help="工作区绝对路径") ap.add_argument("--only", default="", help="只部这一个文件名(可多次用逗号分隔)") ap.add_argument("--dry-run", action="store_true") a = ap.parse_args() ws = Path(a.ws).resolve() if not ws.is_dir(): print("✗ 工作区不存在:%s" % ws) return 1 dst_dir = ws / ".workbuddy" / "collab" files = [x.strip() for x in (a.only.split(",") if a.only else DEFAULT_FILES) if x.strip()] print("工作区:%s" % ws) print("副本目录:%s" % dst_dir) bad = 0 for name in files: src = HERE / name if not src.is_file(): print(" ✗ 源里没有 %s" % name) bad += 1 continue dst = dst_dir / name if a.dry_run: print(" · [试运行] %s → %s(源 md5=%s)" % (name, dst, md5(src)[:8])) continue dst_dir.mkdir(parents=True, exist_ok=True) # 🔴 覆盖前先留一份带时间戳的备份(⛔ 免得新版在这台机器上跑不通时没退路) if dst.is_file() and md5(dst) != md5(src): bak = dst.with_suffix(dst.suffix + ".bak-%s" % time_stamp()) try: shutil.copy2(str(dst), str(bak)) print(" · 旧副本已备份:%s" % bak.name) except Exception as e: print(" ⚠ 备份失败(继续):%r" % e) shutil.copy2(str(src), str(dst)) same = md5(src) == md5(dst) print(" %s %s md5=%s%s" % ("✓" if same else "✗", name, md5(dst)[:8], "" if same else "(⚠ 与源不一致)")) if not same: bad += 1 if not a.dry_run: print("\n%s 下一步:让该工作区的常驻程序**重启**才会用上副本" "(⛔ 改文件不重启=没生效)——判据=心跳里的 `argv0` 指向副本路径。" % ws.name) return 0 if bad == 0 else 1 def time_stamp() -> str: import time return time.strftime("%Y%m%d-%H%M%S") if __name__ == "__main__": sys.exit(main())