#!/usr/bin/env python3 # -*- coding: utf-8 -*- """同步 Windows ↔ 207:/opt/workbuddy(程序 + md + html 完全同步)。 约定: - scripts_cursor / scripts_bash_cursor / patches_cursor 按**文档名**分子目录 - 共用程序可存在于多个文档子目录 - 改配置 / 打补丁:在 207 上执行 `/usr/bin/python3 /opt/workbuddy/scripts_cursor/<文档名>/xxx.py` - Windows 与 207 的程序和文档保持完全同步 用法(Windows): python scripts_cursor/00_部署_workbuddy到207.py python scripts_cursor/00_部署_workbuddy到207.py --docs-only """ from __future__ import annotations import os import stat import sys from pathlib import Path, PurePosixPath import paramiko # 00_部署 在 scripts_cursor 根:可直接导入根上的 _bootstrap/_config _HERE = Path(__file__).resolve().parent if str(_HERE) not in sys.path: sys.path.insert(0, str(_HERE)) import _bootstrap # noqa: F401 import _config as C HOST = "192.168.110.207" SSH_USER = "root" SSH_PASS = "123456" REMOTE_ROOT = C.WORKBUDDY_ROOT ROOT = Path(__file__).resolve().parents[1] SYNC_DIRS = ( "scripts_cursor", "scripts_bash_cursor", "patches_cursor", "docs/md_cursor", "docs/html_cursor", ) SYNC_FILES = ("dify_api.py",) SKIP_DIR_NAMES = {"__pycache__", ".git", ".cursor"} SKIP_FILE_SUFFIXES = (".pyc",) def should_skip(rel: str) -> bool: name = PurePosixPath(rel).name if name.endswith(SKIP_FILE_SUFFIXES): return True for p in PurePosixPath(rel).parts: if p in SKIP_DIR_NAMES: return True return False def collect_files() -> list[tuple[Path, str]]: out: list[tuple[Path, str]] = [] for fn in SYNC_FILES: p = ROOT / fn if p.is_file() and not should_skip(fn): out.append((p, fn.replace("\\", "/"))) for dname in SYNC_DIRS: base = ROOT / dname if not base.is_dir(): continue for p in base.rglob("*"): if not p.is_file(): continue rel = p.relative_to(ROOT).as_posix() if should_skip(rel): continue out.append((p, rel)) return sorted(out, key=lambda x: x[1]) def sftp_mkdirs(sftp: paramiko.SFTPClient, remote_dir: str) -> None: remote_dir = remote_dir.replace("\\", "/") if not remote_dir or remote_dir == "/": return parts = remote_dir.split("/") cur = "" for part in parts: if not part: continue cur = cur + "/" + part try: sftp.stat(cur) except IOError: sftp.mkdir(cur) def main() -> int: docs_only = "--docs-only" in sys.argv files = collect_files() if docs_only: files = [ (p, rel) for p, rel in files if rel.startswith("docs/md_cursor/") or rel.startswith("docs/html_cursor/") ] print("sync files:", len(files), "->", REMOTE_ROOT, "docs_only=", docs_only) c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(HOST, username=SSH_USER, password=SSH_PASS, timeout=20) sftp = c.open_sftp() try: c.exec_command("mkdir -p %s" % REMOTE_ROOT) for local, rel in files: remote = (REMOTE_ROOT + "/" + rel).replace("\\", "/") sftp_mkdirs(sftp, os.path.dirname(remote)) sftp.put(str(local), remote) if rel.endswith(".sh"): sftp.chmod( remote, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IROTH, ) marker = REMOTE_ROOT + "/.installed" with sftp.open(marker, "w") as f: f.write("ok\n") print("upload done") cmd = ( "cd %s/scripts_cursor && %s -c \"import _bootstrap; import dify_api; import _config as C; " "print('local_mode', dify_api.LOCAL_MODE); print('root', C.WORKBUDDY_ROOT)\"" % (REMOTE_ROOT, C.PYTHON3) ) stdin, stdout, stderr = c.exec_command(cmd, timeout=60) out = stdout.read().decode(errors="replace") err = stderr.read().decode(errors="replace") print(out.strip() or err.strip()) if "local_mode True" not in out: print("[WARN] local_mode 未检测到 True") return 1 print("[PASS] 207 /opt/workbuddy 同步与 import 自检通过") return 0 finally: sftp.close() c.close() if __name__ == "__main__": sys.exit(main())