npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-sync-webdav

v0.5.4

Published

DSH WebDAV 配置同步插件:把 ~/.dsh 与应用级设置加密备份到 WebDAV,支持 Windows/Linux/macOS 跨平台互相恢复(profile 自动重映射)

Downloads

415

Readme

dsh-sync · WebDAV 配置同步插件

DSH Plugin License: MIT

把 ~/.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 机

  1. 安装本插件,在设置卡片填好 WebDAV 并保存(会真实测试连接)
  2. 输入口令 → 点「备份」

在 B 机

  1. 先装上本插件(github:ddldxiacan/dsh-sync-webdav,或本地路径),重启 DSH
  2. 在设置卡片填入同一套 WebDAV 配置
  3. 输入同一个口令 → 选备份 → 点「恢复」
  4. 在确认弹窗里看差异预览,并选恢复方式(见下节)
  5. 等卡片提示插件安装完成(备份不含 node_modules,插件代码要在这台电脑重新装)。安装结束前不要重启
  6. 重启 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,不能从那里读本地版本。

两个安全取舍

  1. registry 安装不自动升级。 dsh-sync-plugin 这个包名在 npm 上属于另一位作者的另一个项目(dpskk2/dsh-sync-plugin)。若检测到当前是 registry 安装,插件会拒绝自动升级并提示改用 github: 方式——否则会把别人的项目装到你机器上。(本插件的包名已改为 dsh-sync-webdav 以避开该冲突。)
  2. 升级通道强制走 HTTPS。 pnpm 解析 github: 依赖时,HTTPS 走不通会静默回退到 SSH;在没配 SSH key 的机器上会以难懂的鉴权错误告终。本插件在升级时显式固定 HTTPS 通道。

从旧版本(dsh-sync-plugin)升级

v0.2.0 起包名从 dsh-sync-plugin 改为 dsh-sync-webdav,原因是前者在 npm 上属于别人的项目。改包名意味着它在本机 profile 里是另一个依赖,所以:

  1. 先装新名(github:ddldxiacan/dsh-sync-webdav#v0.2.0)
  2. 再卸掉旧名,否则同一个 id: dsh-sync 会注册两次导致启动报错:
dsh plugin --profile desktop remove dsh-sync-plugin
  1. 重启 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。备份里存的是源机器那一套相对路径,旧版本直接按原路径落地,于是产生两个真实故障:

  1. 配置「恢复了但没生效」:Windows 备份 → Linux 恢复时,配置被写进 profiles/desktop/,而 Linux 实际加载的是 profiles/web,一眼看去「恢复成功 N 个文件」,实际一个都没起作用。
  2. 数据丢失:更严重的是「完全覆盖」模式——它会把目标机上「备份里没有的配置」当作多余项删除,于是 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)

手动安装(不使用插件管理器时)

  1. 把本目录整个复制到 <profile>/node_modules/dsh-sync-webdav/
  2. 在 <profile>/cordis.patch.yml 追加:
- insert:
    - id: dsh-sync
      name: 'dsh-sync-webdav'
      config: {}
  1. 重启 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,永远走不到落盘分支)。

已知限制

  1. 手动触发,不会自动备份——改完配置记得点一次。
  2. 排除项不会被恢复对齐删掉:sessions/、attachments/、聊天派生数据、dsh-runtimes/、node_modules/、日志、缓存和大于 50MB 的文件在恢复/回滚后仍留在本机。配置范围内、备份里没有的文件则会删除。
  3. 聊天记录与聊天文件不随备份迁移:这是刻意的策略选择。需要在机器间搬聊天记录时,请单独复制 ~/.dsh/sessions 与 ~/.dsh/attachments。
  4. Windows 保留设备名(CON/NUL/COM1 等)未被路径校验拦截——它们只会解析到 ~/.dsh 内部,不构成逃逸,属已评估的低危残余。
  5. 插件本身不处理 app.asar 补丁。第三方模型推理档位修复需在新机器上单独重放,见 reasoning-effort-fix/README.md。
  6. 版本检查优先走未认证 GitHub API,限额 60 次/小时(按 IP)。每次成功的 API 检查约花 2~3 个请求,结果在进程内缓存 10 分钟。限额耗尽或 API 连不上时,会改用本机 git ls-remote 取 HEAD sha 和 tag,检查不会因此停摆;这时更新摘要可能为空。备份/恢复不依赖这条通道。
  7. 仓库改名自动跟随:若本仓库日后再次改名,GitHub 会对旧地址返回 301。git/pnpm 会自动跟随(安装不受影响),插件的网络层也已实现 301 跟随(v0.2.1 起),老版本的升级检查不会因此失效。