cang-ying/scripts/memory_sync.py
Zhuyuan Operations ce32073d07 D205 面板功能大升级+本地漫剧管线+经验自动机制+飞书资料归档
- 面板: 会话持久化(oc_sess.json)/余额实时/文件栏(搜索+MD+拖拽)/朗读(edge-tts)/工具过程实时显示/漫剧画布
- 面板: 经验自动检索注入+存经验按钮(experience.py)
- 管线: local_motion本地运镜/IMAGE-FIRST-GUIDE/LOCAL-PIPELINE-V1
- 经验: EED-EXPER-014 面板工程+管线+飞书扒取全记录
- 资料: 飞书《清欢AIGC伪真人短剧全流程》归档
2026-08-03 22:55:02 +08:00

166 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
memory_sync.py — 耳耳蛋记忆库双向自动合并
背景:面板与终端共用同一个 CodeBuddy 项目库,但记忆有两份落点:
① 本地记忆库 ~/.codebuddy/projects/home-ls/memory/ ← 蛋蛋实际读写的
② 仓库副本 ~/cang-ying/memory/eed/ ← 推送到仓库、跨机器带走的
两边都可能被写(终端会话写①、面板会话也写①,但爸爸手动或旧流程可能只更新②),
过去靠手动 cp 同步,一不留神就会覆盖掉另一边刚写的经验。
本脚本做双向合并:
- 只在一边存在的文件 → 补到另一边
- 两边都有且内容不同 → 按 mtime 取新的,旧版备份到 BACKUP_memory_conflict/
- 内容相同 → 跳过
永不静默丢数据:任何被覆盖的版本都先备份。
用法:
python3 memory_sync.py # 执行合并
python3 memory_sync.py --dry-run # 只看会怎么做,不动文件
python3 memory_sync.py --json # 输出 JSON供面板调用
"""
import filecmp
import json
import os
import shutil
import sys
import time
LOCAL_DIR = os.path.expanduser("~/.codebuddy/projects/home-ls/memory")
REPO_DIR = os.path.expanduser("~/cang-ying/memory/eed")
CONFLICT_BACKUP = os.path.expanduser("~/.codebuddy/BACKUP_memory_conflict")
# 仓库侧独有、不该被同步回本地记忆库的文件(仓库自带说明等)
REPO_ONLY = {"README.md"}
def _md_files(d):
if not os.path.isdir(d):
return set()
return {f for f in os.listdir(d) if f.endswith(".md")}
def _backup(path, tag):
"""覆盖前先备份,返回备份路径。"""
os.makedirs(CONFLICT_BACKUP, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
dst = os.path.join(CONFLICT_BACKUP, f"{stamp}_{tag}_{os.path.basename(path)}")
shutil.copy2(path, dst)
return dst
def sync(dry_run=False):
"""执行双向合并,返回结果字典。"""
result = {"ok": True, "copied_to_repo": [], "copied_to_local": [],
"updated_local": [], "updated_repo": [], "backups": [],
"skipped": 0, "error": ""}
if not os.path.isdir(LOCAL_DIR):
result["ok"] = False
result["error"] = f"本地记忆库不存在:{LOCAL_DIR}"
return result
if not os.path.isdir(REPO_DIR):
if dry_run:
result["error"] = f"仓库副本目录不存在(将创建):{REPO_DIR}"
else:
os.makedirs(REPO_DIR, exist_ok=True)
local = _md_files(LOCAL_DIR)
repo = _md_files(REPO_DIR)
# ① 只在本地 → 补到仓库
for name in sorted(local - repo):
if not dry_run:
shutil.copy2(os.path.join(LOCAL_DIR, name), os.path.join(REPO_DIR, name))
result["copied_to_repo"].append(name)
# ② 只在仓库 → 补到本地(跳过仓库自带说明文件)
for name in sorted(repo - local):
if name in REPO_ONLY:
continue
if not dry_run:
shutil.copy2(os.path.join(REPO_DIR, name), os.path.join(LOCAL_DIR, name))
result["copied_to_local"].append(name)
# ③ 两边都有 → 比内容,不同则按 mtime 取新,旧版先备份
for name in sorted(local & repo):
lp = os.path.join(LOCAL_DIR, name)
rp = os.path.join(REPO_DIR, name)
try:
if filecmp.cmp(lp, rp, shallow=False):
result["skipped"] += 1
continue
except OSError as e:
result["error"] += f"[{name} 比对失败: {e}] "
continue
lt, rt = os.path.getmtime(lp), os.path.getmtime(rp)
if lt >= rt:
# 本地更新 → 覆盖仓库,先备份仓库那版
if not dry_run:
result["backups"].append(_backup(rp, "repo"))
shutil.copy2(lp, rp)
result["updated_repo"].append(name)
else:
# 仓库更新 → 覆盖本地,先备份本地那版
if not dry_run:
result["backups"].append(_backup(lp, "local"))
shutil.copy2(rp, lp)
result["updated_local"].append(name)
return result
def summarize(r):
"""把结果压成一句人话,给面板/唤醒卡用。"""
if not r["ok"]:
return "⚠️ 记忆同步失败:" + r["error"]
n = (len(r["copied_to_repo"]) + len(r["copied_to_local"])
+ len(r["updated_local"]) + len(r["updated_repo"]))
if n == 0:
return f"✅ 记忆已同步({r['skipped']} 份一致,无变化)"
parts = []
if r["copied_to_repo"]:
parts.append(f"新增到仓库 {len(r['copied_to_repo'])}")
if r["copied_to_local"]:
parts.append(f"拉回本地 {len(r['copied_to_local'])}")
if r["updated_repo"]:
parts.append(f"本地较新覆盖仓库 {len(r['updated_repo'])}")
if r["updated_local"]:
parts.append(f"仓库较新覆盖本地 {len(r['updated_local'])}")
s = f"✅ 记忆已合并({' · '.join(parts)}"
if r["backups"]:
s += f",旧版已备份 {len(r['backups'])}"
return s
def main():
dry = "--dry-run" in sys.argv
as_json = "--json" in sys.argv
r = sync(dry_run=dry)
if as_json:
print(json.dumps(r, ensure_ascii=False))
return 0 if r["ok"] else 1
print(("【预演,未改动任何文件】" if dry else "【记忆双向合并】"))
print(f" 本地记忆库: {LOCAL_DIR}")
print(f" 仓库副本: {REPO_DIR}")
print()
for key, label in (("copied_to_repo", "→ 补到仓库"),
("copied_to_local", "← 拉回本地"),
("updated_repo", "→ 本地较新,覆盖仓库"),
("updated_local", "← 仓库较新,覆盖本地")):
for name in r[key]:
print(f" {label}: {name}")
if r["backups"]:
print(f"\n 旧版备份 {len(r['backups'])} 份 → {CONFLICT_BACKUP}")
print(f"\n{summarize(r)}")
if r["error"]:
print(f"⚠️ {r['error']}")
return 0 if r["ok"] else 1
if __name__ == "__main__":
sys.exit(main())