- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件) - 附 .gitignore(产物 + 本机凭据) - 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库) - 本提交为孤儿提交(父提交为空),历史自此重新开始
114 lines
5.2 KiB
Python
114 lines
5.2 KiB
Python
# -*- 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())
|