多端同步管家
解决 WorkBuddy 换电脑后看不到历史会话的问题:会话正文、自定义技能、长期记忆在多台机器之间实时互通。
为什么需要它
WorkBuddy 的会话正文存在 ~/.workbuddy/projects/<工作区>/<会话ID>.jsonl,但侧边栏读的是
~/.workbuddy/workbuddy.db 里的 sessions 表。直接同步整个目录会出两种事故:
workbuddy.db是 SQLite 二进制,两端同时写会互相覆盖,表现为会话列表突然清空sessions/*.json是运行时心跳(含 pid、端口),同步过去会让 A 机去连 B 机的端口
所以本技能的设计是:只同步文本,索引各自本地重建。
workbuddy.db 永远不进同步范围,index_sync.py 从 jsonl 反推索引补进本地库。
快速使用
所有脚本都在 scripts/ 下,不依赖第三方包(纯标准库)。
| 场景 | 命令 |
|---|---|
| 接入 / 修复(零配置,默认路径) | python scripts/setup_pc.py |
| 检查健康状态 | python scripts/doctor.py |
| 只诊断不修改 | python scripts/doctor.py --dry-run |
| 立即修复 | python scripts/doctor.py(默认就修) |
| 看守护最近结果 | python scripts/guardian.py --status |
| 注册/卸载自启 | python scripts/register_autostart.py [--remove] |
| 只想看看环境 | python scripts/setup_pc.py --check |
在 WorkBuddy 里直接说「同步挂了帮我看看」,就会走 doctor.py;
说「把多端同步弄好」,就直接跑 python scripts/setup_pc.py,不传任何参数。
零配置是怎么回事
scripts/defaults.json 里已经内置了中转服务器的地址、端口、Device ID 和注册端口:
{
"hub": { "host": "112.126.66.6", "device_id": "YDURNF7-...", "port": 22000 },
"join_port": 8099,
"open_register": true
}
所以新机器上只需要这一条命令:
python scripts/setup_pc.py
它会自动完成:下载 Syncthing → 生成配置 → 生成同步规则 → 配好中转 → 向服务器自助注册
(租户名按本机 Device ID 派生,如 wb-d1a2b3c4)→ 建独立目录 → 注册开机自启 → 自检。
用户不需要知道 IP、Device ID、租户名,也不需要管理员在服务器上做任何操作。
> 给 Agent 的执行规则:用户表达「接入同步 / 换电脑了 / 同步弄好」时,直接执行
> python scripts/setup_pc.py,不要追问服务器地址或 Device ID——都已内置。
> 只有用户明确说「用我自己的服务器」时,才走下面的 --join 或 --hub-host。
换一台中转服务器
内置的是技能作者的中转机。想用自己的,两种方式(都会覆盖内置值,只对本机生效):
# 1) 自己部署中转机后手动指定
python scripts/setup_pc.py --hub-host <你的IP> --hub-id <你的Device ID>
# 2) 对方给你加入码
python scripts/setup_pc.py --join WBSYNC1-...
覆盖结果写进 scripts/config.local.json,优先级高于 defaults.json,重跑不会丢。
想回到内置服务器:删掉 config.local.json 里的 hub 和 tenant 字段即可。
三种加入方式
| 方式 | 谁要动手 | 安全性 | 适用 |
|---|---|---|---|
| 零配置自助注册(默认) | 谁也不动手 | 中(知道地址就能加,靠名额上限兜底) | 公开分发、陌生人 |
一次性加入码 --join |
管理员每人发一次码 | 最高(30 分钟、用过即废) | 少数熟人 |
手动 --hub-host/--hub-id |
用户自己填 | 最高 | 自建中转机 |
自助注册模式下,租户名由服务器按 Device ID 派生(d + 设备 ID 前 8 位),
客户端传上来的 tenant 一律忽略——否则谁都能写别人的名字挤进别人的 folder。
同一台设备重复报到是幂等的,始终回到同一个 folder。
已经配过一次的机器(config.local.json 里有 tenant)重跑时会沿用原租户,不会被改成派生名;
如果是更早的单租户配置(有 config.local.json 但没有 tenant),则沿用原配置不切换,
避免把已同步的历史切断。
一键加入
管理员给一个加入码,用户粘进对话就完事:
python scripts/setup_pc.py --join WBSYNC1-eyJoIjoi...
这一条会依次做完:下载 Syncthing → 生成配置 → 建文件夹 → 向服务器报到申请独立目录
→ 注册开机自启 → 跑自检。中间不需要填服务器地址、Device ID 或端口。
> 触发规则:用户消息里出现 WBSYNC1- 开头的字符串,直接当作 --join 的参数执行,
> 不要追问服务器信息——都在码里。码是一次性的,默认 30 分钟过期。
多人共用一台中转服务器
一台 hub 可以服务多个人,每人一份互相看不见的独立目录:
| 概念 | 值 |
|---|---|
| 租户 | zhangsan |
| folder id | wb-zhangsan |
| 服务器路径 | /var/lib/syncthing/tenants/zhangsan |
隔离靠 hub 端的 folder.devices 白名单:每个 folder 只共享给「该用户的设备 + hub 自身」。
客户端改不了服务器配置,所以即使有人硬写 --tenant lisi,也只是「连上但没共享」,
同步 0 个文件,碰不到别人的数据。前提是 hub 的 autoAcceptFolders 保持关闭。
服务器侧操作
python3 provision.py list # 看所有租户
python3 provision.py add --tenant zhangsan --device <ID> # 手动开通
python3 provision.py invite --tenant zhangsan --host <公网IP> # 生成加入码
python3 provision.py serve --port 8099 # 只接受带码的报到
python3 provision.py serve --port 8099 --open --max-tenants 20 # 允许自助注册
python3 provision.py remove --tenant zhangsan --purge # 删除(含数据)
invite 生成的码里含:hub 地址、Device ID、租户名、一次性令牌、注册端口。
用户拿码执行 setup_pc.py --join,服务器自动建目录并共享,管理员不用再动手。
注册服务要常驻
serve 必须长期运行,用 systemd 托管(裸 nohup 会随会话结束被杀):
# /etc/systemd/system/workbuddy-join.service
[Unit]
Description=WorkBuddy Sync Join Service
After=network.target syncthing-workbuddy.service
[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 /opt/workbuddy-sync/provision.py serve --port 8099
Restart=always
RestartSec=5
StandardOutput=append:/var/log/wb-join.log
StandardError=append:/var/log/wb-join.log
[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now workbuddy-join
curl -s --noproxy '*' http://127.0.0.1:8099/health # {"ok": true, ...}
注册端口需要放行
serve 监听 8099,要在云安全组放行 TCP 8099。不放行也能用,只是退化成
「管理员手动 add + 用户带 --tenant 接入」的两步流程。
> 安全提示:serve 是明文 HTTP。加入码含一次性令牌(30 分钟失效、用过即废),
> 接口本身也有失败限流,但授权对象尽量填固定出口 IP 段而不是 0.0.0.0/0。
> 每个人只加一次,加完可以把规则收紧。
开放注册(--open)
要让「装了技能的人自己就能加、管理员完全不介入」,只能放弃邀请码:
ExecStart=/usr/bin/python3 /opt/workbuddy-sync/provision.py serve --port 8099 --open --max-tenants 20
内置三道闸:
- 租户名由 Device ID 派生,客户端传的名字被忽略 —— 无法越权进别人的 folder
--max-tenants总名额(默认 20),满了返回 503- 单 IP 24 小时最多 3 个新租户,挡批量脚本
代价必须清楚:谁拿到 hub 地址就能占用约 300 MB 空间,且流量走你的服务器。
发布到公开市场时,这个地址等于对所有使用者公开,撤不回来(历史版本可下载)。
> 本技能从 v1.4.0 起就是这样分发的:defaults.json 内置了 hub 地址以便零配置。
> 也就是说任何装了本技能的人都能直接注册。要收回,只能按下面的步骤关掉 --open,
> 再发一个删掉 defaults.json 里 hub 字段的新版本——已经装过的旧版本仍指向你的服务器。
真要长期对陌生人开放,建议先算好磁盘与流量成本,或改用「每人自备中转机」。
随时关掉:把 --open 从 ExecStart 里删掉再 systemctl daemon-reload && systemctl restart workbuddy-join,
已注册的租户不受影响,只是新人加不进来。
完整加入流程(已验证)
# 服务器:生成码
python3 provision.py invite --tenant zhangsan --host <公网IP>
# → WBSYNC1-eyJoIjoi...
# 用户机器:一条命令
python scripts/setup_pc.py --join WBSYNC1-eyJoIjoi...
# 装 Syncthing → 配对 hub → 建 folder → 报到申请独立目录 → 注册自启 → 自检
说清楚边界
- 数据仍然物理存在 hub 的磁盘上,hub 管理员技术上能读到所有租户的内容。
- 每个租户是一份完整副本(约 300 MB 起),容量按租户数线性增长。
- hub 是单点,它挂了所有租户都停止同步(数据不丢,只是不同步)。
装完就不用管了
setup_pc.py 会调 register_autostart.py,用 Windows 计划任务(Task Scheduler)
把 Syncthing 注册成受系统托管的常驻任务:
| 任务名 | 作用 |
|---|---|
WorkBuddySync-Syncthing |
登录自动启动;崩溃后 1 分钟重试,最多 3 次 |
WorkBuddySync-Guard |
每 10 分钟体检一次,发现问题就地修 |
为什么必须是计划任务:计划任务由 svchost 拉起进程,父级不在终端 / IDE 的
进程树里。而直接从命令行或 subprocess.Popen 启动的 Syncthing,会在那条命令
结束时被连带清理(即使加了 DETACHED_PROCESS / BREAKAWAY_FROM_JOB 也没用)。
所以「后台启动」必须走计划任务,这是能否长期在线的分水岭。
注册时会顺手 Run 一次,装完立即生效,不用重启、不用手动双击。
schtasks.exe 在不少环境被列为黑名单,因此实现走 COM(Schedule.Service),
schtasks 和启动目录只是降级兜底。
工作方式
Windows PC A ─┐
├─ Syncthing 直连 22000 ─→ 阿里云(中继 + 30 天版本备份)
Windows PC B ─┘
↓
guardian 每 10 分钟:doctor → 发现问题就地修
↓
projects/*.jsonl 落地后,index_sync 补本地 sessions 表 → 侧边栏可见
自愈能力
doctor.py 会依次检查 8 项,默认发现就修:
| 检查 | 发现问题时的动作 |
|---|---|
| 安装 | 没装则 setup_pc.py 自动下载便携版 |
| 进程 | 没运行则后台拉起并等待就绪 |
| 共享文件夹 | 被暂停/路径丢失则恢复 |
| 同步范围 | 目录结构变了(升级后多出顶层目录)则重新生成 .stignore |
| 连接 | 全部离线则触发重置重连 |
| 同步状态 | 有待传文件则强制重扫 |
| 会话索引 | jsonl 比数据库多则自动补索引 |
| 冲突文件 | 有 .sync-conflict-* 则按消息 id 去重合并 |
关键约束(改动前必读)
-
.stignore必须用显式排除,不能用*+!xxx白名单。
Syncthing 2.x 实测!反向引用失效,白名单写法扫描结果是 0 个文件。
gen_stignore.py按目录真实内容枚举生成,升级 WorkBuddy 后记得重跑。 -
写
.stignore前要停 Syncthing。
运行中会独占文件,Windows 上open(...,'w')抛 PermissionError。
gen_stignore.py已内置r+截断 → 临时文件替换的降级链。 -
两台机器同时开也不会丢数据,只会产生
.sync-conflict-*.jsonl,
守护进程会自动合并(按每行消息id去重 + 按时间戳排序)。 -
Agent 会话里拉起的 Syncthing 撑不过命令结束。
在 WorkBuddy / IDE 的 Bash-PowerShell 工具中启动的syncthing.exe,
会在该条命令返回时被连带清理(TerminateProcess,日志无任何退出记录)。
已实测无效:cmd /c start、explorer.exe <exe>、
Popen(DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_BREAKAWAY_FROM_JOB)。
只要启动它的脚本持续前台持有,进程就能活;脚本一退出立刻消失。
→ 正确做法是注册计划任务再 Run(见上文「装完就不用管了」),
由 Task Scheduler 托管即可常驻。doctor.py的「自动拉起」只适合临时救急,
若看到「体检刚全绿、几分钟后又掉线」,说明还是走了会话进程树,改走计划任务。 -
Windows 自启优先用 COM 计划任务,不要依赖
schtasks或启动文件夹。
reg.exe/schtasks在此环境被安全策略拦截,用winreg或
win32com.client.Dispatch("Schedule.Service")直接操作最稳。
注意win32com依赖 pywin32,目标机没有时要能降级,不能直接崩。 -
不上同步的东西(刻意取舍):定时任务(存在 db 里)、连接器/MCP 配置、
插件版本、各机器专属状态文件(device-id、settings.json)。
同步范围越小,故障面越小。
配置
- 通用默认值:
scripts/defaults.json(随包发布,含内置中转服务器地址,见「零配置是怎么回事」) - 本机私有覆盖:
scripts/config.local.json(自建中转、安装路径等本机差异;可选,不写就用内置值) - 本机运行时:
~/.workbuddy-sync/config.json(自动生成,不在同步范围内) - 守护日志:
~/.workbuddy-sync/guardian.log - 最近一次体检结果:
~/.workbuddy-sync/last_report.json
配置目录刻意放在 ~/.workbuddy 之外,避免各机器的 Device ID 互相污染。
排障
| 现象 | 原因 | 处理 |
|---|---|---|
| 侧边栏看不到别的机器的会话 | 索引没补 | python scripts/doctor.py |
| 同步卡住、磁盘暴涨 | 规则失效,日志被纳入 | 停进程后 python scripts/gen_stignore.py |
| 诊断显示 0 个文件 | 用了 * + ! 白名单写法 |
改用 gen_stignore.py |
.stignore 写不进去 |
Syncthing 独占 或 安全软件限制 | 先 python scripts/doctor.py(会自己停/启) |
| 守护没在跑 | 启动项失效 | python scripts/register_autostart.py |