dsh-sync-webdav
Verifieddsh-sync-webdav · v0.5.4 · MIT · Web UI
DSH WebDAV 配置同步插件:把 ~/.dsh 与应用级设置加密备份到 WebDAV,支持 Windows/Linux/macOS 跨平台互相恢复(profile 自动重映射)
Install
dsh plugin add dsh-sync-webdav Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-sync · WebDAV 配置同步插件
把 ~/.dsh 的整体配置打包加密后备份到 WebDAV,在新机器上一键恢复——替代「手工搬运配置文件」的做法。
适用于 DeepSeek Harness 的双半区插件(Host + Client),零 npm 依赖。
版本 0.5.0。要求 DSH >= 0.2.1-rc.2(端点栅栏用到
connection.requestRejection,profile 判定用到ctx.profileContext);已在 0.2.1-rc.2 / Web 版实测。下限写
0.2.1-rc.2而不是0.2.1是有意的:按 semver 规范,预发布版本 不满足>=0.2.1这种不含预发布标识的范围, 而下限若写>=0.2.1,正在跑0.2.1-rc.2的机器会被宿主的engines.dsh校验判为incompatible-version、直接装不上。
为什么需要它
换机器或换账号时,DSH 的配置散落在 ~/.dsh 里:模型路由、凭据、任务板、插件设置……手工搬运容易漏、容易错,而且这些文件里有 API key,直接扔网盘不安全。
dsh-sync 把这件事变成两个按钮:备份(加密上传)和恢复(解密还原,且自动先做本地快照)。
功能
在设置页新增一张「配置同步(WebDAV)」卡片(与 general / account / models / plugins 并列,全新 id sync,不替换任何现有页面)。已保存的服务器地址、用户名和远程目录会在下次打开时回填到输入框,并在卡片上写明「已保存:…」,避免只看到占位符、分不清有没有配置过。密码不再回显(只显示「已填写」):留空保存表示沿用已保存的那份。
| 功能 | 说明 |
|---|---|
| 服务器配置 | WebDAV URL、用户名、密码、远程目录(默认 dsh-sync,留空=网盘根目录);「保存并测试连接」会真实发起 PROPFIND/MKCOL 验证 |
| 状态区 | 远端可达性、备份条目(名称/大小/时间)、最近一次本地快照 |
| 备份 | 输入口令 → 打包 ~/.dsh → AES-256-GCM 加密 → PUT 上传 |
| 恢复 | 选一个备份(或「恢复最新」)→ 看差异预览并选恢复方式(只补缺失 / 覆盖同名 / 完全覆盖)→ 确认 → 自动先做本地快照 → 解密后按所选方式落地 → 改写源机器路径 → 自动安装缺失插件 |
| 删除 | 远端备份列表里每一份都可以单独删除 |
| 回滚 | 从本地快照列表还原(始终是对齐到快照,不提供「只补缺失」——快照是同一台机器的历史状态) |
| 插件重装 | 恢复后检测到插件依赖缺失 → 点「重装插件」→ 自动改写 link: 依赖并执行 pnpm install |
| 版本升级 | 打开设置页自动检查新版本;有新版时一键升级本插件自身(升级前自动备份,失败可回滚) |
触发方式:纯手动。 插件不做任何定时或后台自动同步。
换机器迁移(A 机 → B 机)完整流程
备份不含 node_modules/,所以恢复后只有插件的配置、没有插件的代码。DSH 启动时对缺失的 bundle 只是警告跳过、不会崩溃,也不会自动重装——结果是所有插件静默消失。因此迁移分两步:
在 A 机
- 安装本插件,在设置卡片填好 WebDAV 并保存(会真实测试连接)
- 输入口令 → 点「备份」
在 B 机
- 先装上本插件(
github:ddldxiacan/dsh-sync-webdav,或本地路径),重启 DSH - 在设置卡片填入同一套 WebDAV 配置
- 输入同一个口令 → 选备份 → 点「恢复」
- 在确认弹窗里看差异预览,并选恢复方式(见下节)
- 等卡片提示插件安装完成(备份不含
node_modules,插件代码要在这台电脑重新装)。安装结束前不要重启 - 重启 DSH
B 机已经配好皮肤/插件,恢复会覆盖掉吗?
不会——默认就不会。 恢复确认弹窗会先比对本机与这份备份,显示「本机缺少 N 个 / 内容相同 N 个 / 内容不同 N 个 / 本机独有 N 个」,并点出具体是哪些文件,然后让你选:
| 恢复方式 | 备份里的文件 | 本机已有的同名配置 | 本机独有的配置(备份里没有) |
|---|---|---|---|
| 只补缺失(默认) | 本机没有的才写入 | 原样保留(内容不同的列进结果里「已保留」) | 保留 |
| 覆盖同名配置 | 全部覆盖写入 | 被备份覆盖 | 保留 |
| 完全覆盖 | 全部覆盖写入 | 被备份覆盖 | 删除,与源机器一模一样 |
想「B 机和 A 机完全一致」选完全覆盖;想「保留我在 B 机改的皮肤和设置、只把缺的补上」保持默认的只补缺失即可。内容是全新机器(~/.dsh 里没有多余配置)时,两者结果完全一样。
被保留的冲突文件会在恢复结果里逐个列出,不会静默放过:看到「按你的选择保留了本机 N 个改过的配置」就说明这些文件没有被备份覆盖——想用备份版本,改选「覆盖同名配置」或「完全覆盖」再恢复一次即可(恢复前仍会自动快照,可回滚)。
第 5 步会自动做两件事,否则另一台电脑上的插件会全部显示「包不可用」:
- 把只在 A 机成立的路径改到这台电脑。包括
file:C:/Users/某用户/.dsh/...这种本地包(改成相对路径),以及pnpm-workspace.yaml里写死的storeDir(别人的 pnpm 存储目录会让整次pnpm install失败,于是每个插件都报cannot resolve profile bundle) - 发现有插件依赖没装好时,自动执行
pnpm install。装不好时卡片上仍留着「重装插件」按钮
link: / file: 指向 A 机独有目录、而这台电脑上没有对应文件时,会改写成备份里记录的仓库地址;推导不出仓库的条目会从依赖里去掉,避免一条坏依赖拖垮全部插件。
第 5 步会自动处理 A 机的本地路径依赖:A 机若用 link:E:\…\dsh-sync-webdav 这种写法装了本插件,这个绝对路径在 B 机并不存在,直接 pnpm install 会失败。插件在备份时就把这类依赖的仓库地址采集进备份包,恢复时自动改写为 github:ddldxiacan/dsh-sync-webdav 之类的可移植 spec 再安装。
恢复流程不会自动跑 pnpm install——重装由你点按钮触发,因为安装可能耗时数分钟且会改动
package.json。
B 机前置条件:Git 必须在
PATH中(link:依赖改写后的github:spec 需要 git 解析),且能访问 registry。实测 7 个插件冷安装约 2 分钟。
恢复为什么“看起来没反应”?逐项自查
恢复是写文件,不会自动刷新界面或重启,恢复成功后必须重启 DeepSeek Harness 才生效。若点了「恢复」后什么都没发生,按顺序检查:
| 现象 | 最可能的原因 | 处理 |
|---|---|---|
| 设置页里根本没有「配置同步(WebDAV)」卡片 | 本机没装本插件;或装的版本与备份不匹配(v0.1.0 只注册 3 个端点,无插件重装卡片) | 装与 A 机同版本的插件,重启 DSH |
| 卡片显示「插件服务未就绪:请确认本机已安装 dsh-sync-webdav…」 | 插件 Host 半区未加载或端点未注册 | 同上;确认 cordis.patch.yml 里 ui-settings 未被禁用 |
| 点了「恢复」出现「口令错误」类红字 | passphrase 与备份时不一致 | 用回备份时的口令 |
| 恢复提示「有 N 个条目被跳过」 | 备份内容异常(含不安全路径),或 AppData 路径在新机无法解析 | 反馈该提示给插件作者;被跳过的文件未落地 |
| 提示「恢复成功」但界面没有变化 | 正常现象——配置已写入,但 DSH 未重启 | 重启 DeepSeek Harness |
| 远端列表为空 / 「远端没有备份」 | WebDAV 配置与 A 机不一致,或远程目录填错 | 核对 url/username/password/remoteDir |
v0.3.1 起:恢复成功后提示会明确报告「已恢复 N 个文件(逐文件校验通过)+ 快照名 + 需重启」,被跳过/校验失败的文件会逐个列出,不再静默。
更新说明
v0.5.0 — 多 profile 备份优先迁移「源机器在用的那份」(重要)
上一版的重映射在备份里同时存在 desktop 和 web 两个 profile 时会选错。
实测事故(2026-10-20):Windows 机器同时有 desktop(22 个插件,日常在用)和 web(4 个插件,备用)两个 profile,备份里两个都在。旧逻辑看到「备份里有 web」就直接取恒等映射,结果:Windows 的 web 覆盖了 Linux 的 web,而真正在用的 desktop 被写到 Linux 的 profiles/desktop —— dsh web 根本不加载它。用户看着「恢复成功」,实际拿到的仍是那份 4 插件的空壳配置。
修复:重映射优先采用备份 meta.profile 记录的「源机器备份时正在使用的 profile」,它才是这份备份真正代表的那套配置。
meta.profile=desktop、本机web→ 把desktop映射到web,不再看备份里是否有同名web。- 备份里与目标同名的那个 profile(本例的
web)跳过不落地:两份写进同一目录会互相覆盖,跳过并在结果里逐条回报,不静默丢。 - 旧备份没有
meta时退化为原有规则(含目标名即恒等),行为与旧版一致,不影响老备份。 - 同平台(
desktop→desktop、web→web)仍是恒等,零行为变化。
v0.4.9 — 自测不再污染真实快照目录
createTestHarness 在没指定 snapshotRoot 时会退回 ~/.dsh-sync-snapshots(与真实运行一致)。但自测会反复 makeSnapshot / rollback,于是每跑一次 node tools-selftest.mjs,就往用户真实的快照目录写几份测试快照,并按「保留 5 份」把用户自己的回滚点顶掉。
实测事故(2026-10-20):连跑几次自测后,用户恢复前的快照被挤掉,回滚点丢失,且界面里混进了 url: 127.0.0.1:<mock> 的测试快照。
现在只要没显式指定 snapshotRoot,自测一律落在测试 root 内部(<root>/.dsh-sync-snapshots),绝不碰真实 home。并新增断言锁死:未指定时 snapshotRoot 必须以测试 root 开头。
如果你的机器上已经出现
url为127.0.0.1:<端口>的快照,可直接删除~/.dsh-sync-snapshots/下对应目录——那是测试残留,不是你的配置。
v0.4.8 — 恢复不再把插件自己删掉(重要)
修掉一个会让插件把自己卸载掉的缺陷,一旦触发就只能手工重装才能救回来。
现象:恢复完成后插件消失 —— 设置页的「配置同步」卡片不见了,重启也回不来。
根因:恢复会把备份里的 profiles/<name>/package.json 整个覆盖过来。如果那份备份来自一台没装本插件(或插件装在别的 profile)的机器,覆盖后目标机的 profile 里就没有本插件了。接着设置页自动触发 pnpm install(按新的 package.json 收敛 node_modules),于是插件代码被删除;DSH 一重启就加载不到这个 bundle —— 而这时用户连「回滚」按钮都点不到,因为插件已经没了。
实测事故(2026-10-20):Windows 机器备份里的 profiles/web 恰好没装本插件,在 Linux 上用「完全覆盖」恢复后,依赖声明、bundle 注册、node_modules 三者全被清掉。
修复:恢复完成后自动确保本插件仍在目标 profile 的 dependencies 与 dsh.profile.bundles 里,用的值是恢复前本机的原声明(不瞎编版本),结果里明确提示「已自动把它补回 profile,无需手动重装」。只补自己这一个包名,其它依赖与字段一律不动。
v0.4.7 — 远程目录默认 dsh-sync
以前远程目录是空值 + placeholder 提示(「建议 dsh-sync」),多数人会直接跳过,结果备份全堆在网盘根目录里——跟网盘里其它文件混在一起,清理和核对都不方便。
现在改成默认预填 dsh-sync:
- 新装 / 没配过:设置页里远程目录已填好
dsh-sync,保存后备份落在<你的 WebDAV>/dsh-sync/子目录。 - 脚本 / 老客户端不传
remoteDir:Host 端补默认值,同样落到dsh-sync。 - 显式留空仍然是「网盘根目录」:这是一条有意义的语义,且已有用户的配置里存的就是空串。若把空串当「没填」补成默认值,他们下次备份会写到新目录,而自己以为备份还在原处——看起来就像备份全丢了。所以只在字段完全缺失时才补默认。
- 备份列表始终同时搜索「当前目录 / 根目录 /
dsh-sync」三处,老备份不会因为默认值变化而找不到。
v0.4.6 — 跨平台恢复不再丢配置(重要)
修掉一个数据丢失级缺陷,凡是换过平台(Windows ↔ Linux/macOS)用过「完全覆盖」的人都可能踩到:
- profile 名自动重映射:Windows 桌面版固定
profiles/desktop、dsh web固定profiles/web。旧版把备份里的路径原样落地,导致跨平台恢复时配置写进目标机不加载的 profile(看着成功、实际没生效)。现在恢复时自动把profiles/<源>/…换成本机正在跑的 profile,预览和结果里都会告知。 - 修掉「完全覆盖」删掉目标机 profile:旧版会把目标机上「备份里没有的配置」当多余项删除,于是 Linux 自己的
profiles/web/*(插件依赖、cordis.patch.yml、pnpm-workspace.yaml)被整棵删掉。现在profiles/web保住了,mirror只删真正多余的配置。 - 同平台恢复零行为变化:
desktop→desktop、web→web是恒等映射,与旧版逐字节一致。 - 自定义 profile 名不猜测,不做映射(避免把配置写进没打算用的 profile)。
- 顺手修掉 4 个自测自身的平台假设缺陷(用
127.0.0.1:1断言落盘、Linux 上仍读APPDATA),自测从386 passed / 4 failed变成 406 passed / 0 failed。
升级后必须重启 DeepSeek Harness 生效。已经用旧版跨平台恢复过的机器,建议重启后确认一次配置是否落在当前 profile 下。
版本升级
插件自带升级模块,安装过的用户不必重装即可跟进新版本。
打开设置页时自动检查一次(结果在宿主内缓存 10 分钟),卡片上直接显示当前版本与是否有新版。也可随时点「检查更新」强制刷新。
| 当前安装方式 | 升级行为 |
|---|---|
github: / registry:(受管安装) |
显示「发现新版本」+ 更新摘要 + 「立即升级」按钮 |
link: / file:(开发态) |
显示「开发态安装,不受版本升级管理;更新请在该目录执行 git pull」,不提供升级按钮 |
点「立即升级」后:先把 package.json 备份为 package.json.bak-dsh-sync-<时间戳>,再把依赖声明改写为带 tag 的确定性 spec(github:<owner>/<repo>#vX.Y.Z),然后执行 pnpm install 并重启 DSH 生效。升级过程只改写这一个依赖键,profile 里其它插件依赖与 dsh.profile.bundles 原样不动。
判定「有没有新版本」用 commit sha,不是版本号
这一点与直觉相反,但对本插件是必须的:发新提交可以不改 package.json 的 version(本仓库的 4da85ef 与 b503783 两个提交版本号都是 0.1.0)。实测若只比 semver,判定会 100% 失效。
因此:
- 本地已装 sha:从 profile 的
pnpm-lock.yaml中提取(用「任意 40 位 hex」匹配——同一条github:specifier 会产出 codeload /git+ssh/git+https三种锁文件形态,字段名不统一) - 远端最新 sha:优先 GitHub API
commits/main;API 限流或连不上时回退到git ls-remote <repo> HEAD(tag 同样从refs/tags/*解析)。changelog 仍只走 compare API,限流时摘要可以为空,但「有没有更新」不再被锁死 - 两者不等即为「有更新」;
latest字段优先取最高的v*tag,无 tag 时退化为 sha 前 7 位
注意:
node_modules/<包名>/里的文件不含 sha,不能从那里读本地版本。
两个安全取舍
- registry 安装不自动升级。
dsh-sync-plugin这个包名在 npm 上属于另一位作者的另一个项目(dpskk2/dsh-sync-plugin)。若检测到当前是 registry 安装,插件会拒绝自动升级并提示改用github:方式——否则会把别人的项目装到你机器上。(本插件的包名已改为dsh-sync-webdav以避开该冲突。) - 升级通道强制走 HTTPS。 pnpm 解析
github:依赖时,HTTPS 走不通会静默回退到 SSH;在没配 SSH key 的机器上会以难懂的鉴权错误告终。本插件在升级时显式固定 HTTPS 通道。
从旧版本(dsh-sync-plugin)升级
v0.2.0 起包名从 dsh-sync-plugin 改为 dsh-sync-webdav,原因是前者在 npm 上属于别人的项目。改包名意味着它在本机 profile 里是另一个依赖,所以:
- 先装新名(
github:ddldxiacan/dsh-sync-webdav#v0.2.0) - 再卸掉旧名,否则同一个
id: dsh-sync会注册两次导致启动报错:
dsh plugin --profile desktop remove dsh-sync-plugin
- 重启 DSH
升级到 v0.2.0 之后,后续版本就都能在设置页里一键升级了。
发版流程(维护者)
# 1. 改完代码,跑自测(必须全绿)
node tools-selftest.mjs
# 2. bump 版本号(package.json 的 version)
# 3. 提交并打标签 —— tag 是升级模块的版本锚点,必须打
git add -A
git commit -m "feat: ..."
git tag -a v0.2.0 -m "v0.2.0: ..."
git push origin main --tags
打 tag 是发版的必要步骤:升级功能以 v* tag 作为「最新版本」的来源,并把它作为安装锚点(github:<owner>/<repo>#vX.Y.Z)。只提交不打 tag,已安装用户的「检查更新」会退化为按 commit sha 比较——仍能发现更新,但看不到版本号。
若要让安装路径也解析出具体提交,pnpm 会把 #vX.Y.Z 钉到该 tag 指向的 sha 并记进锁文件(实测确认)。
同步范围与排除规则
策略:~/.dsh 下除「工作区聊天数据」和「可重建产物」之外,全部同步;home 根下承载模型配置的几个目录按白名单一并同步(__home__/ 挂载点)。
| 类别 | 内容 | 实测体积 |
|---|---|---|
| ❌ 聊天记录 | sessions/(会话正文 session.v4.jsonl.zstd) |
51.2 MB |
| ❌ 聊天文件 | attachments/(聊天里上传的文件对象) |
10.1 MB |
| ❌ 聊天派生 | storages/session_projcache/(会话标题与首条输入原文)、storages/workspace.json(含 archivedSessionIds) |
1.9 MB |
| ❌ 可重建 | dsh-runtimes/(DSH 运行时,架构相关)、node_modules/(插件依赖) |
257.2 MB + 991.7 MB |
| ❌ 日志缓存 | logs/、npm-cache/、tmp*/、cache*/、单个 >50MB 的文件 |
— |
| ✅ 同步 | 其余一切:凭据、插件配置、profile、agent-presets、task-board、skins、local-packages、用量、会话归档… | 约 12.7 MB |
| ✅ 例外 | cache/dsh-claude-style/(模型分时费率表)—— cache*/ 规则会误伤配置,故按前缀白名单放行 |
19 KB |
排除清单是白名单式的显式四类:新出现的目录默认进备份,不会被静默漏掉。
为什么不做字面全量:全量是 1.33GB,而当前实现把整个 payload 读进内存再加密——实测 350MB 就会 OOM。 排除的两块都是可重建产物(重启装运行时、点「重装插件」装依赖),不是用户数据。
远端目录只保留最近 5 份 .bundle,更旧的在备份成功后自动 DELETE。
恢复/回滚时的对齐删除只作用于「配置范围」,被排除的 sessions/、attachments/ 等一律不动——
换机恢复不会误删目标机上已有的聊天记录。
v0.5.4 起额外同步 home 级模型配置:自定义大模型配置并不总在 ~/.dsh 下。
模型目录、价表与 provider 切换器散落在 home 根,旧实现只采 ~/.dsh + __appdata__,
这些文件既不进备份、恢复时也补不回来。现在按显式白名单采集,同样以 __home__/ 前缀并入同一 payload,恢复/回滚时写回 home 原位:
| 挂载点 | 采集内容 | 不收 |
|---|---|---|
~/.dsh-freetap/ |
全部(模型价表、adapter 状态) | — |
~/.dsh-opencode-free/ |
全部 | — |
~/.opencode2dsh/ |
全部 | — |
~/.dsh-wallpaper-engine/ |
config.json、build-stamp.json、fontsets/、mascot/ |
diag/、cache/、thumbs/、uploads/、avatars/、ffmpeg/ |
~/.cc-switch/ |
settings.json、cc-switch.db、live-state.json、model-pricing.json、codex-*、skills/ |
backups/、logs/、crash.log |
~/.claude/ |
settings.json、settings.local.json、skills/、cc-haha/、plugins/known_marketplaces.json |
.runtime/、cache/、projects/、sessions/、.git/ |
任何挂载点内的
node_modules/、.git/、.runtime/、cache*/、logs/、tmp*/与 >8MB 的文件一律不收 —— 白名单只覆盖「配置」,绝不把可重建产物拖进备份。
与
__appdata__/一样是 fail-safe:旧备份若完全没有__home__/条目、也没有meta.homeIncluded,恢复时不会据此清空本机 home 配置(否则用一份旧备份「完全覆盖」就会把本机的模型配置全删掉)。
自测隔离:
createTestHarness默认把 home 指向测试 root 内部,绝不读/删用户真实的~/.cc-switch、~/.claude。 v0.3.0 起额外同步 Electron 应用级设置:%APPDATA%\@deepseek-ai\dsh-desktop\desktop\settings.json等用户配置文件,以__appdata__/前缀并入同一加密备份,恢复/回滚时自动写回原位(Chromium 缓存类目录与 >2MB 文件仍排除,不撑大备份)。
设置页的「备份」卡片里有一行「本次同步范围」,展开即可看到会同步哪些顶层项、排除了哪些以及排除原因;
/status 的 coverage 字段、备份结果的 coverage 字段给出同样信息(机器可读)。
/status的coverage不统计被排除目录的体积(递归量 5.8 万个文件要 3.6 秒,会拖慢每次轮询), 只报excludedCount(排除项个数);备份结果里的coverage才带excludedFiles/excludedBytes。
备份包含
~/.dsh/dsh-sync.json自身(里面存着 WebDAV 的 url/username/password),这属有意设计——配置整体迁移。备份内容受口令加密保护。
安全模型
- 加密:
scrypt(passphrase, salt, 32)派生密钥 +AES-256-GCM;带 SHA-256 内容校验,篡改或口令错误都会明确报错 - 口令:只在内存中存在,绝不落盘、绝不写入日志(勾选「在本机记住」时才写进
dsh-sync.json) - 本机文件权限:
~/.dsh/dsh-sync.json以0600落盘(里面有 WebDAV 密码与记住的口令),旧文件保存时同步收紧。 该权限在 POSIX 上直接生效;Windows 上由 NTFS 用户目录 ACL 兜底(chmod基本无效,属平台限制) - 端点鉴权:所有
/dsh-sync/*走宿主的 Host/Origin 栅栏 + 浏览器 cookie 认证,与/api同一套;connection缺失时只放行 loopback - 不回传明文密码:
/status只回hasPassword,设置页也不再回显密码(留空=沿用已保存的) - 恢复方式(0.4.2 起由用户选):确认弹窗会先比对本机与备份,列出「本机缺少 / 内容相同 / 内容不同 / 本机独有」各有多少,再由你选一种方式:只补缺失(默认)——本机已有的配置一律不动,只补本机没有的文件;覆盖同名配置——备份里的文件全部覆盖写入,但本机多出来的配置保留;完全覆盖——覆盖写入并删掉备份里没有的配置,与源机器一模一样。被保留的冲突文件会在结果里逐个回报。全新机器上「只补缺失」与「完全覆盖」结果相同。配置范围就是打包规则会收进去的路径。聊天记录(
sessions/)、聊天文件(attachments/)、聊天派生数据、dsh-runtimes/、node_modules/、日志、缓存和大于 50MB 的文件不在备份里,恢复时也不删除——否则会删掉运行时导致 DSH 起不来,或丢掉聊天记录。旧备份若完全没有应用级设置(没有__appdata__/条目,也没有meta.appDataIncluded),不会据此清空本机 AppData 配置。
跨平台(Linux / macOS / Web 版)
插件本身是纯 Node ESM,没有原生依赖,Windows、Linux、macOS 都能跑。dsh web(纯 CLI,无 Electron)同样支持,只是少了桌面版内置运行时,pnpm 必须自己在 PATH 里(见下节)。
四处平台差异已经处理:
| 事项 | Windows | Linux | macOS |
|---|---|---|---|
| Electron 应用级设置根目录 | %APPDATA%(…\AppData\Roaming) |
$XDG_CONFIG_HOME 或 ~/.config |
~/Library/Application Support |
| profile 名 | 桌面版固定 profiles/desktop |
dsh web 固定 profiles/web |
同 Linux(dsh web) |
| 路径比较是否区分大小写 | 不区分 | 区分(/Home ≠ /home) |
不区分(默认卷) |
| pnpm 调用形态 | .CMD 外壳经 cmd.exe |
脚本用 node、原生 ELF 直接执行 |
同 Linux |
符号链接会被跟随(0.4.5 起):Linux/macOS 上把 skins/ 或整个 ~/.dsh 软链到别处很常见(dotfiles 管理、独立仓库)。旧版本用 readdir 的 dirent 判断,软链既不算文件也不算目录,会被静默跳过 —— 备份看着成功,其实少了整棵配置。现在会跟随链接(含断链跳过、成环检测),并且「完全覆盖」只删链接自身、绝不删它指向的真实文件。
0.4.4 修复:旧版本在 Linux/macOS 上会无条件拼出
~/AppData/Roaming。那是 Windows 才有的路径,在 Linux 上并不存在 —— 结果是恢复一份 Windows 备份时会在你的 home 下凭空创建~/AppData/Roaming/@deepseek-ai/…这棵假目录树,而且恢复预览里还会把它报成「本机缺少的配置」。现在按平台解析真实位置,解析不到就跳过,绝不虚构路径。
跨平台恢复会自动重映射 profile(0.4.6 起)
profile 名是跟平台绑定的:Windows 桌面版固定用 profiles/desktop,Linux/macOS 的 dsh web 固定用 profiles/web。备份里存的是源机器那一套相对路径,旧版本直接按原路径落地,于是产生两个真实故障:
- 配置「恢复了但没生效」:Windows 备份 → Linux 恢复时,配置被写进
profiles/desktop/,而 Linux 实际加载的是profiles/web,一眼看去「恢复成功 N 个文件」,实际一个都没起作用。 - 数据丢失:更严重的是「完全覆盖」模式——它会把目标机上「备份里没有的配置」当作多余项删除,于是 Linux 自己的
profiles/web/*(插件依赖、cordis.patch.yml、pnpm-workspace.yaml)被整棵删掉,重启后插件全部消失。
现在恢复时会把备份里的 profiles/<源 profile>/… 换成本机正在使用的 profile,预览与结果里都会明确告知:
跨平台恢复:配置会换到本机的 profile
这份备份来自 desktop profile,本机使用的是 web profile,恢复时会自动把配置写到本机 profile 下。
规则是「宁可不动,也绝不猜错」:
| 备份里的 profile | 本机 profile | 行为 |
|---|---|---|
desktop |
web |
重映射 desktop → web |
web |
desktop |
重映射 web → desktop |
desktop |
desktop |
恒等,与旧版行为完全一致 |
自定义名(如 alpha) |
beta |
不映射——备份里没有任何信息能断定这是同一套配置改了名,强行改名会把配置写进你没打算用的 profile |
只重映射 profiles/<name>/ 这一段:顶层文件(settings.yaml.imported、.credentials.yaml 等)和 __appdata__/ 挂载点都原样保留。四种组合(Win→Win、Win→Linux、Linux→Win、Linux→Linux)都有回归断言锁住。
备份本身是平台无关的:内部一律用 posix 相对路径作为键,__appdata__/ 前缀只是「挂载点」标记,换平台只切换根目录,不改备份格式。所以可以 A 机 Windows 备份、B 机 Linux 恢复。
安装
Web 版 / CLI 安装的前置条件:pnpm 必须在 PATH 里
桌面版自带运行时(resources/runtime/primary-runtime/dependencies/pnpm/),插件直接用内置的那份;
Web 版(dsh --profile web)是纯 npm 安装的 CLI,没有内置运行时,卡片上的「重装插件」「恢复后自动安装」都依赖系统里的 pnpm:
npm i -g pnpm # 或 corepack enable pnpm
pnpm --version # 必须能在 PATH 里被找到
插件的查找顺序是:宿主给的 packageManager → 桌面版 resources/ 布局 → PATH 上的 pnpm。按找到的形态决定怎么调用:
PATH 上是 |
怎么跑 | 为什么 |
|---|---|---|
…/pnpm/bin/pnpm.mjs 脚本 |
node --expose-internals <script> |
脚本入口才认 --expose-internals |
| 原生启动器(pnpm ≥ 10 的 ELF/Mach-O/PE) | 直接执行 | 交给 node 会 SyntaxError: Invalid or unexpected token |
Windows 的 pnpm.CMD 外壳(corepack/npm 装法) |
cmd.exe /d /s /c ""<path>" <args>" + windowsVerbatimArguments |
spawn 直接跑 .CMD 是 EINVAL,带空格的路径还会被截成 'C:\Program' |
方式一:从 npm 安装(推荐)
已发布到 npm,在 DSH 的插件管理界面填入包名即可:
dsh-sync-webdav
或使用 CLI:
dsh plugin --profile <你的profile> add dsh-sync-webdav # Web 版通常是 --profile web
本插件零 npm 依赖(只装它自己一个包)、不含需要构建脚本的依赖,所以一条命令即装好,
不需要任何额外参数,也不需要 Git。DSH 会读取 package.json 里的 dsh.bundle.patch,
自动把 dsh-sync 注册进 profile 的 cordis.patch.yml,无需手工改配置。
方式一·备选:从 GitHub 仓库安装
不想走 npm 时可改用仓库地址:
github:ddldxiacan/dsh-sync-webdav
或:
dsh plugin --profile <你的profile> add github:ddldxiacan/dsh-sync-webdav # Web 版通常是 --profile web
DSH 会通过 pnpm 拉取本仓库,读取 package.json 里的 dsh.bundle.patch,自动把 dsh-sync 注册进 profile 的 cordis.patch.yml,无需手工改配置。
⚠️ 前置条件:本机必须已安装 Git 且在
PATH中。 pnpm 安装github:形式的 spec 时会调用git ls-remote探测远程仓库;找不到 git 会直接报Command failed with exit code 1: git ls-remote …。新版 Windows 的 Git for Windows 默认加入PATH,无需额外配置。仓库为 Public,因此无需任何凭据即可安装。(若你 fork 后改为 Private,则需确保本机 git 已能访问该仓库:HTTPS 走 Personal Access Token,或配置 SSH key。)
方式二:本地目录安装(开发调试 / 二次开发)
dsh plugin --profile <你的profile> add "E:\完整路径\dsh-sync-webdav"
插件管理器同样支持绝对路径安装,适合改完代码立刻验证。
安装后
重启 DeepSeek Harness,然后在 设置 页找到「配置同步(WebDAV)」卡片。
仓库结构
dsh-sync-webdav/
├── package.json # name: dsh-sync-webdav,声明 dsh.bundle.patch 与 dsh.client
├── cordis.patch.yml # 注册 dsh-sync 服务(name 与 package.json 一致)
├── index.js # Host 半区:打包/加密/WebDAV/恢复/回滚/插件重装 + 12 个 HTTP 端点
├── client.js # Client 半区:设置页卡片
└── tools-selftest.mjs # 自测(含 mock WebDAV 服务器与 B 机恢复 E2E)
手动安装(不使用插件管理器时)
- 把本目录整个复制到
<profile>/node_modules/dsh-sync-webdav/ - 在
<profile>/cordis.patch.yml追加:
- insert:
- id: dsh-sync
name: 'dsh-sync-webdav'
config: {}
- 重启 DSH
注意:不要同时存在手动安装与插件管理器安装两份——同一个
id: dsh-sync注册两次会导致启动报错。
HTTP 端点(供脚本/自动化调用)
业务结果返回 HTTP 200,以 ok 字段判断成败;错误统一 { ok: false, error }。
传输层状态码与宿主 /api 对齐:401(未登录)/ 403(不是从 dsh 页面发起的请求,被 Host/Origin 栅栏拦下)、
405 方法不允许(带 Allow 头)、400 请求体不是合法 JSON、500 内部异常。
鉴权:所有
/dsh-sync/*端点都套与宿主/api同一套栅栏(connection.requestRejection)—— Host/Origin 校验 + 浏览器 cookie 认证。少了这层,任意网页里的fetch()(text/plain是 CORS 简单请求, 不触发预检但副作用照样发生)就能改写 WebDAV 指向、种入攻击者已知的口令,并把~/.dsh打包外传。connection服务取不到时 fail-closed:只有本机 loopback 直连放行。
| 方法与路径 | 请求体 | 成功响应 |
|---|---|---|
GET /dsh-sync/status |
— | { configured, config: { url, username, password: '', remoteDir, hasPassword, hasPassphrase } | null, coverage: { included[], excluded[], totals }, remote: { reachable, backups: [{name,size,modified}], latest? }, lastLocalSnapshot? }(不含明文密码) |
POST /dsh-sync/config |
{ url, username, password, remoteDir } |
{ ok: true }(先做连通性测试,失败不保存;密码留空=沿用已保存的) |
POST /dsh-sync/backup |
{ passphrase } |
{ ok: true, name, size, fileCount, homeCount, scope, coverage, pruned }(coverage 给出已同步/已排除的顶层清单,scope.homeMounts 给出 __home__/ 各挂载点计数) |
POST /dsh-sync/restore |
{ name?, passphrase, mode? } |
{ ok: true, restored, verified, removedCount, snapshot, portability?, meta?, profileRemap?, remapped? }(profileRemap 形如 { from:'desktop', to:'web', count },跨平台恢复才有) |
POST /dsh-sync/delete |
{ name } 或 { names: [] } |
{ ok: true, deleted }(只接受 dsh-sync-YYYYMMDD-HHMMSS.bundle) |
POST /dsh-sync/rollback |
{ snapshot } |
{ ok: true, restored, removedCount } |
GET /dsh-sync/plugins |
— | { ok: true, profile, deps: [{name, spec, kind, installed, rewrite?}], missing, needsInstall, mapped, pnpm: {available, path?, via?}, lastRestore? } |
POST /dsh-sync/plugins/install |
{} 或 { approve: ['pkg'] } |
{ ok: true, installed, exitCode, durationMs, rewritten, removed, lockSynced, restored, log };失败时还带 pendingBuilds(被 pnpm 拦下的构建脚本包名) |
approve 是「允许并重试」用的:先把包名写进 profile 的 pnpm-workspace.yaml 的 allowBuilds,再跑一次安装。
安装失败时 restored: true 表示 package.json 与 pnpm-lock.yaml 已还原到本次安装前。
备份文件名:dsh-sync-<yyyymmdd-HHMMSS>.bundle
备份格式
[8 字节 magic "DSHSYNC1"][4 字节 headerLen(LE)][header JSON][AES-256-GCM 密文]
header: { version, salt(b64), iv(b64), tag(b64), payloadSha256 }
明文 payload: { files: [{ path, sha256, data(b64) }], meta?: { dshVersion, profile, createdAt, linkMap, unmapped } }
meta 是可选的:旧备份没有它,恢复时按「无插件映射信息」处理(不会报错,只是重装插件时无法自动改写 link: 依赖)。
自测
node tools-selftest.mjs
预期自测全绿(0 failed)。覆盖:排除规则(含 node_modules/sessions 回归断言)、加解密往返、错误口令与篡改拒绝、打包/解包、备份 meta 与旧备份向后兼容、快照与回滚(含「dsh-runtimes/ 必须在回滚后存活」的回归断言)、路径穿越防护、link: → 仓库 spec 改写、B 机恢复 E2E(A 机备份 → B 机恢复 → 重装插件依赖)、异步 runner 不阻塞事件循环(含「源码中不得出现 spawnSync(」防退化护栏),一个内置 mock WebDAV 服务器上的 Basic Auth / MKCOL / PUT / GET / PROPFIND 全链路,以及 v0.4.0 新增的回归断言:PATH 兜底的各条分支(原生启动器直接执行 / 旁有 bin/pnpm.mjs 时用脚本 / Windows .CMD 外壳经 cmd.exe / 完全没有 pnpm 时给出「npm i -g pnpm」提示)、resolveProfile 优先采信 profileContext.name、requestRejection 返回 401/403 时 12 个端点全部不进入业务逻辑、connection 缺失时 fail-closed、安装失败还原 package.json、parseIgnoredBuildScripts 与 allowBuilds 写入,以及 v0.4.2 新增的恢复方式回归断言:merge 只写本机缺少的文件且保留本机改过的同名配置、overwrite 覆盖同名但保留本机独有配置、mirror 覆盖并删除本机独有配置、三种模式都不删聊天记录与运行时、planPayloadApply 预览不写盘且把「新增/相同/冲突/本机独有」分开报、未知 mode 回落默认 merge,以及 v0.5.0 新增的多 profile 优先级回归断言(meta.profile 优先于同名判断、与目标同名的 profile 被跳过并回报、同平台仍恒等、无 meta 的旧备份退化行为不变),以及 v0.4.9 新增的自测隔离回归断言(未指定 snapshotRoot 时必须落在测试 root 内、dshRoot 不得是真实 ~/.dsh),以及 v0.4.8 新增的恢复自保回归断言(备份不含本插件时依赖与 bundle 注册都会被补回、已存在时幂等、只缺 bundle 时不动已有依赖版本、profile 缺失时 fail-soft、拿不到原声明时不瞎编依赖值),以及 v0.4.7 新增的远程目录默认值回归断言(缺字段补 dsh-sync、显式空串仍为根目录、自定义目录原样保留、null/undefined 不抛错、与 configForDisk 对接后规范化正确、backupSearchDirs 仍覆盖根目录以保证老备份找得到),以及 v0.4.6 新增的跨平台 profile 重映射回归断言:planProfileRemap 只在 desktop↔web 之间映射、同平台恒等、自定义 profile 名不被乱猜、只改 profiles/<name>/ 一段而不动顶层文件与 __appdata__、Win→Linux 与 Linux→Win 两个方向都落到本机 profile、mirror 不得把目标机自己的 profile 整棵删掉(v0.4.5 的数据丢失点)、以及「可达 WebDAV 时配置真的落盘」(旧用例用必然拒绝连接的 127.0.0.1:1,永远走不到落盘分支)。
已知限制
- 手动触发,不会自动备份——改完配置记得点一次。
- 排除项不会被恢复对齐删掉:
sessions/、attachments/、聊天派生数据、dsh-runtimes/、node_modules/、日志、缓存和大于 50MB 的文件在恢复/回滚后仍留在本机。配置范围内、备份里没有的文件则会删除。 - 聊天记录与聊天文件不随备份迁移:这是刻意的策略选择。需要在机器间搬聊天记录时,请单独复制
~/.dsh/sessions与~/.dsh/attachments。 - Windows 保留设备名(
CON/NUL/COM1等)未被路径校验拦截——它们只会解析到~/.dsh内部,不构成逃逸,属已评估的低危残余。 - 插件本身不处理
app.asar补丁。第三方模型推理档位修复需在新机器上单独重放,见reasoning-effort-fix/README.md。 - 版本检查优先走未认证 GitHub API,限额 60 次/小时(按 IP)。每次成功的 API 检查约花 2~3 个请求,结果在进程内缓存 10 分钟。限额耗尽或 API 连不上时,会改用本机
git ls-remote取 HEAD sha 和 tag,检查不会因此停摆;这时更新摘要可能为空。备份/恢复不依赖这条通道。 - 仓库改名自动跟随:若本仓库日后再次改名,GitHub 会对旧地址返回 301。
git/pnpm会自动跟随(安装不受影响),插件的网络层也已实现 301 跟随(v0.2.1 起),老版本的升级检查不会因此失效。