Files

113 lines
5.2 KiB
Python
Raw Permalink Normal View History

# -*- 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())