- 面板: 会话持久化(oc_sess.json)/余额实时/文件栏(搜索+MD+拖拽)/朗读(edge-tts)/工具过程实时显示/漫剧画布 - 面板: 经验自动检索注入+存经验按钮(experience.py) - 管线: local_motion本地运镜/IMAGE-FIRST-GUIDE/LOCAL-PIPELINE-V1 - 经验: EED-EXPER-014 面板工程+管线+飞书扒取全记录 - 资料: 飞书《清欢AIGC伪真人短剧全流程》归档
166 lines
6.0 KiB
Python
166 lines
6.0 KiB
Python
#!/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())
|