WorkBuddy 缓存目录跨盘迁移
概述 / Overview
把 WorkBuddy 的数据目录(默认 %USERPROFILE%\.workbuddy,通常 2~10 GB)从系统盘搬到
另一个本地磁盘,再把原路径替换成一个 NTFS junction(目录联接)。
结果是:WorkBuddy 完全感知不到变化 —— 它仍然读写 C:\Users\<你>\.workbuddy,
而真实数据已在别的盘上。历史对话、记忆、技能、连接器、自定义模型全部照常显示,
系统盘空间一次性释放。
整条链路是:准备(全量复制/增量合并 + 数据库处理)→ 内容一致性闸门 →
让应用让位 → 改名备份 → 建 junction → 功能性校验 → 把应用拉回来。
任何一步失败都会自动回滚或保持原状,全程零数据丢失。
> 仅支持 Windows(junction 是 NTFS 特性)。macOS/Linux 请改用符号链接方案。
三条必须提前告知用户的事实
- 源盘可用空间不会立刻变大。 旧数据会完整保留在
<源目录>.pre-portal备份里,
必须跑完cleanup_backup.py才真正释放。否则用户会看着没变的可用空间误判失败。 - 迁移会短暂关闭 WorkBuddy(约几秒)。 零点击 / 一键模式都会自动关掉它、迁完再自动打开,
使用者无需手动退出应用;但请先保存正在编辑的内容。 - 耗时主要在复制:4 GB 数据实测约 3 分钟(取决于磁盘速度)。
何时使用 / When to use
- 用户想释放系统盘空间,且大头是 WorkBuddy 的缓存/数据目录。
- 用户想把
.workbuddy迁到 D 盘、E 盘或其他本地磁盘。 - 用户反馈「历史对话 / 技能 / 连接器 / 自定义模型看不见了」——
常见原因是数据被迁到别处、但原路径没做成 junction,应用读的是空壳目录。 - 触发词:迁移缓存目录 / 数据目录搬家 / 改存储位置 / junction / 软链接 / C 盘满了。
不适用:只想清理普通缓存文件(那是清理工具的活);macOS/Linux 环境。
工作流 / Workflow(Agent 按这个顺序做)
步骤 0 · 先诊断,不要直接动手
python scripts/verify_portal.py # 0=OK 已迁移 / 1=WARN 未迁移 / 2=BROKEN 数据分裂
python scripts/migrate_portal.py --rehearse # 彩排:临时假数据跑完整流程,不碰真数据
彩排 exit 0 说明本机 junction 能力正常。先彩排再动真格,成本为零。
步骤 1 · 问清目标盘(或让脚本自动挑)
脚本默认自动挑选「非系统盘中可用空间最大」的盘。用户有指定就用 --drive。
步骤 2 · 执行迁移 —— 三选一
路径 A · 一键自动(推荐给绝大多数用户)
python scripts/make_launchers.py --drive E # 生成 5 个可双击 bat(含 一键迁移.bat)
然后把 一键迁移.bat 的位置告诉用户,让他双击。这个 bat 会自动:
确认提示 → 结束 WorkBuddy 体系进程 → 迁移 → 重新打开 WorkBuddy → 写结果文件
结束后目标盘会出现 迁移结果.md(人话版结论),用户重启后一眼可见。
> 为什么用 bat 而不是让 Agent 直接跑?因为迁移必须结束 WorkBuddy 进程,
> 而 Agent 自己就跑在 WorkBuddy 里 —— 直接跑等于自杀。双击 bat 让迁移
> 运行在独立于 WorkBuddy 的进程上下文里,才能活到最后。
路径 B · Agent 零点击(推荐:用户说「我自己不参与」时)
python scripts/auto_migrate.py --drive E --detach
脚本借 explorer.exe 转手,起一个活在 WorkBuddy 的 Job 对象之外的 worker 进程,
随即返回。worker 随后自行完成:准备 → 结束 WorkBuddy → 迁移 → 重新打开 WorkBuddy → 写结果文件。
使用者不需要做任何事 —— 既不用自己退出 WorkBuddy,也不用双击 bat。
为什么必须「借 explorer 转手」而不是普通脱壳?
Chromium/Electron 会给自己的整棵进程树套一个 Job 对象,并常设 KILL_ON_JOB_CLOSE:
一旦 WorkBuddy 被关闭,Job 内的所有子进程会被一并清掉。而由 WorkBuddy 内部直接
Popen 出来的进程都会继承这个 Job,所以「自己脱壳」是脱不出去的
(CREATE_BREAKAWAY_FROM_JOB 在本机实测被拒:WinError 5,且 IsProcessInJob = True)。
explorer.exe 是常驻的系统进程、不属于该 Job,由它拉起的脚本天然落在 Job 之外
(实测:经 explorer 转手拉起的进程 IsProcessInJob = False)。
三级降级(每一级以「worker 握手」为准,wbc-auto-handshake.json 会回报 in_job 字段):
| 级别 | 通道 | 是否脱离 Job |
|---|---|---|
| 1 | explorer 转手(首选) | ✅ 是(实测) |
| 2 | CREATE_BREAKAWAY_FROM_JOB |
仅当 Job 允许 breakaway |
| 3 | 仅 DETACHED_PROCESS |
❌ 否(有连坐风险,明确告警) |
worker 一起来就先写握手文件自证 in_job;发起方若 25 秒内收不到握手,自动降到下一级 ——
杜绝「以为起了、其实没起」的静默失败。
⚠️ 无论走哪条通道,执行后当前对话都会被中断(WorkBuddy 被关闭并重启)。
请先向用户说明「接下来 WorkBuddy 会自动关闭并重新打开」,再执行。
迁移完成后,目标盘会出现 迁移结果.md(人话版结论),用户重启后一眼可见。
路径 C · 纯手动(用户在命令行里自己跑)
python scripts/auto_migrate.py --prepare # ① 准备(可在应用运行时做)
# 用户完全退出 WorkBuddy(托盘右键退出 + 任务管理器确认无残留)
python scripts/auto_migrate.py --skip-prepare # ② 收尾(改名 + 建链 + 校验 + 重启)
或更窄的用法:python scripts/migrate_portal.py --drive E(迁移器本体,要求应用已退出)。
三条通道怎么选
| 通道 | 需要用户做什么 | 可靠性 |
|---|---|---|
| A 一键 bat | 双击一次 bat | 最高(bat 由 explorer 启动,天然在 Job 外) |
B --detach 零点击 |
什么都不用做 | 高(explorer 转手实测脱离 Job;失败自动降级) |
| C 纯手动 | 自己退出应用 + 敲两条命令 | 高(但最费事) |
步骤 3 · 验证
python scripts/verify_portal.py # 应为 OK(0)
python scripts/migrate_portal.py --rehearse # 环境自检仍应通过
别只看迁移器自己的日志,独立复核这几项:
fsutil reparsepoint query <源目录>→ 替代名称是否指向目标盘;- 穿透写入测试:经源路径写一个文件,确认它真的落在目标盘;
- 经源路径看到的
projects工作区清单 == 目标盘清单; - 数据目录下无残留
.portal-state一致性标记。
步骤 4 · 清理旧备份(释放空间的最后一步)
# 零点击(Agent 用,推荐):自动关应用 → 清理 → 重新拉起应用 → 写结果文件
python scripts/auto_cleanup.py
# 手动:先预演看要删什么,确认无误后真正删除(需完全退出 WorkBuddy)
python scripts/cleanup_backup.py --dry-run
python scripts/cleanup_backup.py
> cleanup_backup.py 内部已是「robocopy 批量删 + ACL 复位 + 逐文件扫尾」三段式,
> 能自动处理「DENY 删除 ACL」这类隐性阻拦;若仍有文件被后台守护进程占用,
> 会明确列出并提示「重启后再跑一次即可」。
> 建议先让用户正常用一两天(打开应用、翻会话历史、跑技能)再删备份 ——
> 这是不可逆操作,验证期还没过。
步骤 5 · 长期维护清理(数据目录不再次长胖)
迁移不是一劳永逸——日志 / 缓存 / 崩溃报告 / 文件历史快照会随使用持续膨胀。
maintain_portal.py 提供分级安全清理(融合社区最佳实践 + 实测风险图谱):
python scripts/maintain_portal.py --status # ① 先扫描:分级报告谁占了多少
python scripts/maintain_portal.py --yes # ② 清低风险项(可再生缓存)
python scripts/maintain_portal.py --yes --include-medium # ③ 连同中风险项(清前自动备份)
python scripts/maintain_portal.py --days 30 --yes # 自定义日志保留天数(默认 14)
python scripts/maintain_portal.py --yes --quick # 应急模式:跳过扫描报告直接清(省时间)
python scripts/maintain_portal.py --yes --config extra.json # 自定义清理项(追加低风险全清目录)
python scripts/maintain_portal.py --yes --via-explorer # Agent 沙箱内运行:借 explorer 转手到沙箱外
--config 的 JSON 格式:{"extra_purge_dirs": ["相对路径", ...]}(字符串数组),
追加项与内置低风险项同等对待(全清、保留目录本体),适合接入各机器特有的缓存目录。
风险分级(判定不确定一律归「永不碰」):
| 级别 | 内容 | 处理 |
|---|---|---|
| 低 | app/Crashpad、traces/、app/session 的 Chromium 缓存子目录、logs/ 超期日志、遥测队列、临时残留 |
默认可清(可再生) |
| 中 | file-history/、changes-detail/、changes-index/、file-tree-manifests/ |
默认只列清单;--include-medium 时先备份到 cache_archive/ 再清 |
| 高(永不碰) | projects/(对话全文)、sessions/、workspace/(恢复备份)、memory/、skills/、plugins/、workbuddy.db*、全部配置文件与用户产物 |
脚本不枚举、不进入 |
执行特性:robocopy /MIR 原生批量删 + icacls /reset 摘 DENY ACL;
purge 保留目录本体;被运行中应用锁定的文件(如 app/session 缓存)跳过并明确列出
——退出 WorkBuddy 后再跑一次即可全清。报告落盘 <数据目录>/maintain-report.md。
持续化:想每周自动维护,用 WorkBuddy 自动化建一个每周任务
(prompt 示例:「运行 python <技能目录>/scripts/maintain_portal.py --yes 并把
maintain-report.md 摘要告诉我」),即可零操心长期保持数据目录苗条。
步骤 6 · 已删除对话残留彻底清理(隐私 + 空间)
界面点「删除」只是给 workbuddy.db 的 sessions.deleted_at 打软删除标记 ——
对话正文仍以明文留在硬盘(含工具调用完整出入参)。purge_conversations.py
把整条残留链一次清干净,覆盖 10 处(含同类技能普遍漏掉的孤儿数据):
# ① 先出清单(绝不删)
python scripts/purge_conversations.py --soft-deleted --list
python scripts/purge_conversations.py --orphans --list # 孤儿残留
# ② 预演
python scripts/purge_conversations.py --soft-deleted --dry-run
# ③ 备份后真删(文件 + 产物索引 + 恢复备份)
python scripts/purge_conversations.py --soft-deleted --yes
# ④ 连数据库记录 / 云同步索引一起清
python scripts/purge_conversations.py --soft-deleted --purge-db --yes
# ⑤ 隐私模式(覆写后再删)+ 零点击(自动关/开应用)
python scripts/purge_conversations.py --soft-deleted --shred --yes --via-explorer
# ⑥ 把重要会话加入永久保护清单(之后任何清理档位都跳过)
python scripts/purge_conversations.py --protect "<会话id或标题关键词>"
# 还原
python scripts/purge_conversations.py --restore "<备份目录>"
筛选档位:--soft-deleted(界面已删) / --orphans(磁盘有DB无) /
--older-than 30d|6m|1y / --all / --session ID / --top N / --include-cache。
安全阀(借鉴竞品「防误删」设计并加强):
- 活跃保护:
--all自动排除最近 3 天有活动的会话(防误删正在用的对话);
确要连活跃一起清需显式--force。 - 保护清单:
--protect <会话id或标题关键词>把会话写入
purge-protected.json(可累积多条),之后任何档位的清理都永久跳过这些会话。 - 分批删除:每批 20 项、批批校验;整批失败立即停止并保留现场,不盲删到底。
- 真实释放量:报告同时给出「按项计算」与
disk_usage磁盘实测两个数字,
不夸大战绩。
覆盖范围(10 处残留链):
| # | 残留 | 位置 |
|---|---|---|
| 1 | 对话正文(明文) | projects/*/<sid>.jsonl |
| 2 | 文件回滚快照 | projects/*/<sid>*.ndjson |
| 3 | 会话元信息 | projects/*/<sid>.meta.json |
| 4 | 产物面板索引 | artifact-index/<sid>.json |
| 5 | 会话索引文件(孤儿高发) | sessions/<sid>.json |
| 6 | 重启恢复备份 | workspace/sessions/<sid>/modify_backup/ |
| 7 | 数据库记录与用量 | workbuddy.db → sessions / session_usage |
| 8 | 云同步/搜索索引 | edge-sync-mapping*.db → edge_sync_mapping / edge_sync_image_mapping |
| 9 | 附件与缓存 | blobs/ file-history/ local_storage/ shell-snapshots/ buddy-snapshots/ traces/ |
| 10 | 沙箱工作副本 | editor-sdk-sandbox-ws/ |
触发时机与执行流程:
1. 触发:用户说「彻底清理对话 / 对话还在硬盘上 / 隐私清理 / 释放空间」,
或作为长期维护的一环定期跑(可挂自动化:每周清一次 --soft-deleted)。
2. 流程:--list 出清单 → 与用户确认范围 → 进程守卫(未退出则要求退出或
--via-explorer 零点击自动关/开)→ 默认先备份到目标盘
<数据目录父目录>/WB-Purge-Backup/<时间戳>/(含 _manifest.json)→
数据库整库备份 → 删文件(--shred 时覆写后删)→ --purge-db 时清
sessions/session_usage/同步索引(只清文件确实删净的会话)→
写 purge-report.md 与审计日志。
3. 红线:绝不跳过清单直接删;--yes 不等于授权(授权来自用户在对话里的确认);
文件被占用时跳过并报告,不强删;不改 ~/.workbuddy 之外的任何数据。
步骤 7 · 需要时回滚
python scripts/undo_portal.py
撤销 junction,从备份还原成迁移前的真实目录。目标盘数据不受影响。
脚本清单 / Scripts
| 脚本 | 作用 |
|---|---|
scripts/auto_migrate.py |
全自动迁移编排(推荐入口):准备 → 让位 → 迁移 → 重启应用 → 写结果 |
scripts/migrate_portal.py |
迁移器本体:复制 + 合并 + 建 junction + 校验 + 失败自动回滚 |
scripts/undo_portal.py |
回滚:撤销 junction,还原真实目录 |
scripts/auto_cleanup.py |
零点击清理旧备份(Agent 用):借 explorer 转手起 Job 外 worker,自动关/开应用 |
scripts/cleanup_backup.py |
安全删除旧备份(摘链接 → ACL 复位 → robocopy 批量删 → 逐文件扫尾) |
scripts/maintain_portal.py |
长期维护清理:分级安全清理(低/中/高),status 扫描、dry-run 预演、--quick 应急直清、--config 自定义清理项、中风险先备份 |
scripts/purge_conversations.py |
已删除对话残留彻底清理:10 处残留链、6 种筛选档位、活跃会话保护(--force 才可越过)、--protect 永久保护清单、分批删除批批校验、真实磁盘释放量、备份还原、--shred 隐私覆写、--purge-db |
scripts/verify_portal.py |
状态巡检(OK / WARN / BROKEN) |
scripts/make_launchers.py |
生成可双击 .bat(含一键迁移) |
所有脚本均为纯 Python 标准库,无需 pip install。建议 Python 3.10+。
退出码 / Exit codes
auto_migrate.py 与 migrate_portal.py 保持一致:
| 码 | 含义 |
|---|---|
| 0 | 成功(或已是 junction,无需重复迁移) |
| 1 | 前置条件不满足(源不存在、目标同盘、空间不足、已存在备份) |
| 2 | 准备阶段失败 / 检测到 WorkBuddy 进程存活 |
| 3 | 改名失败(仍有进程或句柄占用源目录)→ 源目录原状未动 |
| 4 | 建链接失败(已自动回滚) |
| 5 | 校验失败(已自动回滚) |
| 6 | 内容一致性闸门未过(已中止,源目录未动) |
| 7 | 环境自检未通过(零改动) |
verify_portal.py:0=OK / 1=WARN / 2=BROKEN。
cleanup_backup.py:0=成功(或无备份可删)/ 1=前置条件不满足 / 2=进程存活 / 3=目标盘校验失败 / 4=备份内仍有链接 / 5=仍有文件未删净(重启后再跑即可)。
maintain_portal.py:0=成功 / 1=参数或前置问题 / 2=有文件被占用未删净(明细见 maintain-report.md,重启后再跑即可)。
purge_conversations.py:0=成功(或无命中)/ 1=前置问题 / 2=WorkBuddy 仍在运行(需退出或用 --via-explorer)/ 3=备份失败(拒绝删除)/ 4=有文件被占用未删净。
关键铁律 / Critical rules
这些是血泪换来的,改脚本前务必先读 references/TROUBLESHOOTING.md。
- 绝不覆盖合并。 源/目标两边的同名会话文件常是「互补的两段」,覆盖会丢整段对话。
只做「目标不存在才复制」,并断言合并后目标文件数不减少。 - 绝不使用
robocopy /MOVE。 它「先搬数据、再判成败」,失败会让源目录变空壳。 - Windows junction 检测禁用
os.path.islink。mklink /J建出的 junction,
os.path.islink()与stat.S_ISLNK()都返回 False!必须用os.path.isjunction()
或lstat().st_file_attributes & 0x400。 - 删除含链接的目录前,先用不带
/S的rmdir摘除链接(备份里可能嵌套 junction)。 - 校验用「功能性证明」:以「经原路径看到的工作区清单 == 目标盘清单」为准,
而不是只看链接对象存不存在。 - sqlite 句柄必须
try/finally关闭,否则库损坏时连接泄漏、句柄占住文件。 - 进程识别要「名字 OR 路径」双规则。 只按进程名会漏掉
sandbox-center.exe/sandbox-cli.exe/editor_sdk.exe这类名字不含 workbuddy 的
WorkBuddy 子进程,它们同样持有.workbuddy下的句柄。 - 改名失败(code=3)不要靠重试。
MoveFileW遇占用是立即返回失败,
属结构性问题(CWD 在树内、或树内文件被独占打开)。正确形态是
「清理占用者 → 再改名」的循环。 - 只结束「WB 体系」进程,绝不碰 explorer 等系统进程。
且必须排除自身 PID —— 否则脚本会杀掉自己。 - 只要关过 WorkBuddy,无论成败都必须把它拉回来。 用全局标记 +
atexit兜底,并让「调用即消费标记」避免启动出两个实例。 - 改名与建链之间要有一致性标记(
.portal-state)。进程恰好在这 1 秒内被杀时,
下次运行才能自动补完,否则会留下「源目录既不是真目录、也不是链接」的半成品。 - 演练要能真正地「不碰真数据」:用硬开关(
_REHEARSE_LOCK)锁死一切杀进程动作,
不要依赖调用方记得传参。 - 「零点击」的关键是越过 Job 对象,而不是「随便脱壳」。
Electron/Chromium 的 Job 常带KILL_ON_JOB_CLOSE,Job 内子进程会随应用一同被杀。
要点:① 由不属于该 Job 的进程(如explorer.exe)转手拉起 worker;
② 用握手文件自证in_job状态,绝不确定假设成功。 - worker 的解释器路径绝不能含 workbuddy/codebuddy。 进程守卫按
「名字 OR exe 路径」双规则识别 WB 体系 —— 用.workbuddy\binaries\下的
内置 python 起 worker,守卫会把 worker(甚至清理脚本自己)误判成 WB 体系
而拒绝执行(exit 2)。起飞前先挑一个路径干净的解释器;守卫本身也要排除自身 PID。 - 「删不掉」不一定是进程占用,先查 ACL。 实测备份里个别目录(如
logs\<日期>)
会被安全策略附加一条显式 DENY 删除 (D,DC) ACE:文件独占打开正常(FREE),
但删除一律 WinError 5。修法:icacls <备份根> /reset /T /C /Q恢复继承 ACL,
cleanup_backup.py 已内置这一步。 - 大批量删除用 robocopy,别用 python 逐文件。
robocopy <空目录> <备份> /MIR /R:1 /W:1
是原生批量删,比 pythonos.remove逐文件快数十倍(1.7 万个文件几分钟 vs 数小时)。
用完再逐文件扫尾,锁定文件明确列出、提示重启后重跑,绝不静默吞错。 - Agent 沙箱内的删除会被 safe-delete 拦截。 在 Agent 会话里直接
os.remove/Remove-Item用户目录文件,可能被转成「回收站」操作而失败;
原生工具(robocopy)不受影响。删备份的正确姿势:auto_cleanup.py
(explorer 转手到沙箱外)或让用户手动跑cleanup_backup.py。 - 「回收站失败」多半就是「文件被占用」。 safe-delete 把删除路由到回收站,
回收站对被进程占用的文件操作失败 —— 实测无占用文件照常删除成功。
所以维护清理里app/session缓存被运行中的应用锁定是正常现象:
跳过并明确列出,退出应用后重跑即可全清,不要为了删它去杀应用进程。 - 维护清理分级执行,中风险一律先备份。 可再生缓存(低)默认可清;
会丢痕迹的(中)先copytree到cache_archive/再清,备份失败就跳过该项;
用户数据(高)连枚举都不进。维护报告必须落盘,锁文件列明细,绝不静默。 - 清对话残留必须「先清单、后确认、再备份」。 界面删除 = 软删除(打标记),
明文正文还在盘上;顺序铁律是「先在界面删(打标记),再跑脚本清文件」。
--yes只是省掉二次输入,授权必须来自用户的明确同意;
--purge-db只清「文件确实删净」的会话记录,避免留下无正文的空壳。
能力边界(诚实说明)
- 已实现:进程识别(名字 OR 路径)、改名前的占用清理(
--takeover)、
一致性标记与开机自愈、内容一致性闸门、finally兜底重启应用、零副作用彩排、
借explorer.exe转手越过 Job 的真·零点击(含握手自证 + 三级降级)、
零点击备份清理(auto_cleanup.py,自动关/开应用)、
清理三段式(icacls ACL 复位 → robocopy 批量删 → 逐文件扫尾并报告锁定文件)、
数据目录长期维护清理(maintain_portal.py,低/中/高风险分级、中风险先备份、
锁定文件明确报告、支持--via-explorer沙箱外执行)。 - 未实现(有意为之):句柄级强拆(
DuplicateHandle+DUPLICATE_CLOSE_SOURCE)。
原因:需要精确识别 File 类型句柄索引、风险与收益不成比例,且无法充分验证。
当前替代方案是「结束 WB 体系进程 + 下次运行自愈」,已能覆盖绝大多数的占用场景。
极端情况(如explorer.exe持有目录本体句柄)脚本会如实报告占用者,
并提示「关闭该目录窗口」或「重启后立即运行」。 - Job 越狱通道:
explorer.exe转手(已实测脱离 Job)。这是「零点击」成立的关键 ——
详见路径 B 与references/TROUBLESHOOTING.md的「Job 对象专章」。 - 不需要:任务计划程序(
schtasks)/ WMI 调度。它们同样能脱离 Job,
但需额外权限、会留下需要清理的调度残留;explorer 转手同样有效且零残留。
资源 / Resources
scripts/—— 9 个可直接运行的 Python 脚本(纯标准库)。references/TROUBLESHOOTING.md—— 事故复盘、逐条铁律、故障速查表。
其中「改名失败专章」是核心知识,值得单独通读。