Pular para o conteúdo principal
Q

dsh-config-sync

qcwygm/dsh-config-sync

Tracks the DSH config repo against the copy on this machine (files changed, commits ahead or behind, sessions and skills not captured yet) and generates push, pull and restore scripts that run after the client exits. A blocked pull backs up both sides of every conflicting file so one side can be picked per file; every apply snapshots the profile first, and any snapshot can be restored later. Windows only: the generated sync scripts are PowerShell.

Instalar

dsh plugin --profile web add github:qcwygm/dsh-config-sync

README

dsh-config-sync

中文 | English

DSH 插件:左侧栏加一行「配置同步」,点开是整页体检面板;「设置 → 通用」另留一行摘要。它只做体检 + 备命令,同步动作不在应用内跑(拉起客户端的自动同步除外)。

panel

截图取自 0.3.14,分区结构与现在一致;新版本里每张卡改成「只读 + 修改」,见下文「统一交互」。

面板与宿主返回的文案是双语的:跟随客户端语言(zh-* → 中文,其余 → 英文),没有面板内切换开关。只有生成的 .cmd 脚本内部文字保持中文(它是本机命令行工具)。

安装

插件本体是一份标准 DSH bundle,装到当前 profile 即可:

# 从最新的 release 包安装(无需构建,包内已含宿主/前端/runtime 脚本)
# 版本号换成 Releases 页里最新的那个:https://github.com/QcWyGm/dsh-config-sync/releases
dsh plugin add https://raw.githubusercontent.com/QcWyGm/dsh-config-sync/main/releases/dsh-config-sync-<版本>.tgz

或在 DSH 面板的官方「插件 → 发现 → 索引源」里添加本仓库的索引(做法见下文「作为插件市场的索引源」)。第三方市场插件 dshmarket 不读这份索引,原因见同一节。

装好后需要指向你自己的配置仓库(默认自动探测常见路径),并在「插件更新」卡里按需填写清单地址与令牌——本仓库是公开的,从 GitHub 分发时不填令牌也能更新。

apply.ps1 落地时做三件事:local-plugins\dsh-config-sync-<版本>.tgz 解压到 <AppRoot>\dsh-config-sync-0.1.0;package.json 加一条 link: 依赖与一条 dsh.profile.bundles;cordis.patch.yml 加 config-sync 行。

包含什么

路径说明
index.js宿主半身:/config-sync/status、/prepare、/settings、/auto、/update 路由
client.js浏览器半身:侧栏行、面板、设置摘要行
cordis.patch.ymlbundle 补丁层(id: config-sync)
runtime/退出后执行的六个 PowerShell 脚本 + fix-session-cwd.mjs,随包发布,装好即可生成脚本,不必先克隆配置仓库
tools/自检脚本 selftest.mjs、前端结构检查 clientcheck.mjs
version.json / releases/自更新清单与安装包

入口挂在哪

入口slot说明
左侧栏一行sidebar.panellist(id: config-sync)与「插件 / 自动任务 / 技能市场 / 记忆系统」同一列,图标双向箭头
点开的面板main(key: config-sync)宿主按 id/key 绑定两者,两条 register 必须在嵌套 inject 里成对完成(只 inject main 就向 sidebar.panellist register 无效)
设置摘要行settings.general.item一行四态 chip + 展开(仓库、远端、私钥)

侧栏行的点击切换由宿主处理,契约抄自 dshmarket / dsh-mnemon。

面板显示什么

布局是「一行状态 + 一行指标 + 分区卡片」,读的顺序就是从上到下(0.3.14 起,见 docs/panel.png):

标题行:配置同步 本机 [一致/待推送/待拉取/有问题] 云端 [一致/不一致/待拉取],右端一个「重新检测」。两个徽标同一套配色——绿=一致、橙=待推送/待拉取、红=有问题;左侧只说本机(配置齐不齐、依赖缺不缺、有无未提交/未推送),云端那条全自动:打开面板查一次、之后每 2 分钟复查,没有手动按钮。

元信息行:app 版本 / Electron 版本 · 分支 @ 短 HEAD。原先是标题下的一整段导语,已删掉。

指标条:五格一眼看完——本机 / 契约(版本不一致转橙)、bundle(有解析不到的转红)、依赖(有缺失转红)、备份快照(份数)、上次同步(时间,后面 (+N) 表示之后又有几个文件写入)。表里有事才在下面出现一条「待办」,没事就没有这一块。

分区:概览(指标条 + 待办)→ 本机路径(0.3.30 起:跨机根确认与待确认清单;0.3.34 起只读展示 + 「修改」;0.3.37 起平时整块不显示,只在点标题行「本机路径」按钮时展开(有待处理项时那个按钮变橙并带上数量))→ 同步冲突(只在真有冲突时出现)→ 同步目标(本机仓库 / 云端仓库)→ 自动化与插件更新(自动同步 / 插件更新)→ 退出后执行 → 还原备份 → 同步内容 → 问题反馈(0.3.31 起)。宽屏两列并排,卡片内文字压成"一行值 + 一行灰字说明",长解释不再堆在正文里。

统一交互:「本机路径 / 本机仓库 / 云端仓库 / 自动同步 / 插件更新 / 同步内容」这些卡都是先只读、点「修改」才解锁,改完点「保存」,点「取消」丢弃本次改动。只有"没配过"的卡(没设仓库、没设远端)会强制停在编辑态且不给「取消」——不先填好,推送/拉取/生成脚本都无从下手。本机路径卡没探测到任何根时同样强制编辑态。

「待推送」不只看 git:git status 干净时,新会话/新资料可能还躺在 ~\.dsh 里没 capture。所以 sync.ps1 在 push / pull 成功后写 ~\.dsh\config-sync-state.json(lastSyncAt/mode/head),status 多返回一段 local:stat sessions、skills、.agent-presets、mnemon、profile 五文件、登记的工作区目录,mtime 晚于 lastSyncAt 的计入 local.pending,徽标转橙「待推送」。只看 mtime、不读内容、不跑 capture;上限 20000 文件 / 1.5 秒;复制或另存也会算进来,误报只是多一句提示。

「待拉取」= 本机仓库还是空目录(0.3.26 起):新机器把仓库路径指到一个空文件夹时,这是"等着从云端拉"的正常状态,不报异常——徽标橙色「待拉取」而不是红色「有问题」,待办里那句是「执行「拉取」会从云端把仓库内容拉到这里」。

上次自动同步的结果:~\.dsh\auto-sync-result.json 若是 15 分钟内的,客户端一启动就弹一条 5 秒浮层(apply() 里挂载后 1.5 秒查一次 status,自己 append 到 body、面板没打开也能显示;成功/失败 + 原因 + 失败带日志路径)。面板里那条提示与它共用一个判据和一个 localStorage 键,同一份结果只弹一次。

本机仓库 / 云端仓库(都可配,默认只读,没配就强制配)

  • 仓库判据:目录里同时有 apply.ps1 与 profile\package.json(再有 sync.ps1 才算能推拉)。
  • 优先级:本机设置文件 → 插件配置 repoPath → DSH_CONFIG_REPO → 常见位置自动探测。配置里填的空目录同样算有效答案,不会被自动探测到的另一个仓库顶掉。
  • 四种状态(status 里的 repo.phase):ok 真仓库、empty 空目录/待首次拉取、missing 目录不存在、unusable 目录里有别的东西却不像仓库。repo.pushAllowed / repo.pullAllowed 就按它算,面板按钮和宿主接口共用这一套口径。
  • 空目录 + 云端有内容 → 只允许拉取(0.3.26 起,0.3.35 起加"云端"这一维):路径指向空白文件夹、云端又有东西时,面板只放行「拉取」(+ 还原),推送按钮置灰并写明"先拉取";宿主 prepare(push) 与 auto(push-now) 一律 400,「一键生成」自动降级成「拉取 / 还原」。拉取会把云端仓库整个拉下来:目录不存在就建,再 git init → 补 origin → fetch → checkout -B <分支>(用 init+fetch+checkout 而不是 clone,因为目录里可能已有插件生成的 同步-*.cmd)。
  • 空目录 + 云端也空 → 只允许推送,而且就是「初始化推送」(0.3.35 起;0.3.42 起它真能跑完):面板把按钮改成「初始化推送」,拉取按钮置灰(没有东西可拉)。status 多了 repo.remoteHeads(云端分支数,null=判不了)、repo.remoteEmpty 与三态 repo.mode(ok / await-pull / await-init),宿主与面板都用它。执行时 sync.ps1 push 依次做:git init → 补 origin → 写仓库骨架(把插件 runtime 的 apply.ps1 / capture.ps1 / sync.ps1 / restore.ps1 / post-exit.ps1 / dsh-sync-common.ps1 拷进仓库根,外加 tools\fix-session-cwd.mjs)→ 写 profile\host-contract.json(本机客户端版本)→ capture 抓本机配置 → 提交 → push -u origin main。骨架必须一起带上,否则远端会变成"只有 settings、没有执行体"的半截仓库,另一台 pull 下来跑不起来。
  • 0.3.42 修了两个让初始化推送跑到一半就停的坑:一是 capture.ps1 根外路径体检里漏写了一个 $((v.Replace(...) 被 PowerShell 当成命令 v.Replace 去执行),只在配置里有「自定义根」时才触发,日志里表现为 无法将"v.Replace"项识别为 cmdlet,整次 push 当场失败;二是 sync.ps1 把「仓库目录不是空的」直接当成「路径填错了」,可初始化推送时 capture 刚把 16 项配置写进仓库,于是它被自己的产物挡住、直接退出。现在判据换成「仓库里有本插件的脚本(dsh-sync-common.ps1 / sync.ps1 / capture.ps1 任一)就算这是同步仓库」,只有真填错路径(桌面、下载目录)才拦。
  • 云端有没有内容是 git ls-remote --heads <远端>(15 秒超时)现算的:探不到(离线/认证失败)就按老规矩只允许拉取,不在盲状态下开推送。云端已经有分支、本机却还是空目录时,push 一律拒绝并提示先拉取 —— 硬推会把本机状态写进一份正在使用的历史。
  • 只允许插件自己生成的 同步-*.cmd 与隐藏项(.git 等)算"空";目录里已经有别的文件却不像仓库 → unusable,面板提示重新设路径,保存设置时也会拒。
  • 云端:远端地址优先读 git remote get-url origin,没有才回落设置文件;SSH 私钥只存路径、可留空交给 .ssh\config。
  • 没配过 = 强制设置:卡片停在编辑态、不给「取消」,顶部红字、生成按钮禁用,宿主 prepare 也 400(远端缺失时 还原 不受影响)。
  • 保存顺序:校验(地址只接受 git@host:path / ssh:// / https://;私钥填了就必须存在)→ 写设置 → 有 origin 就 set-url、没有就 remote add → git 失败回滚设置文件,不留"面板显示 A、git 用 B"的错位。
  • 设置里的路径后来失效(被移走/改名):status 给 repo.exists=false、phase='missing',面板回到"必须重设",不会悄悄换成探测到的另一个仓库。
  • 落在 ~\.dsh\dsh-config-sync.json,跟本机走、不进仓库。

支持的云端平台(0.3.27 起写明)

插件本身不挑 git 托管平台:远端地址只校验「写法」,之后的 fetch / push / pull / ls-remote 全部交给 git。Gitee、GitHub、GitLab、自建 Gitea/Forgejo 走的是同一条路。

项说明
地址写法git@host:owner/repo.git、ssh://git@host:port/owner/repo.git、https://host/owner/repo.git 三种
默认分支先试 main,云端没有就取远端第一个分支(老仓库的 master 也行),拉取后自动建立 tracking
认证SSH:填私钥路径(留空则交给 .ssh\config 或 Git 默认选择),生成的脚本用 GIT_SSH_COMMAND + StrictHostKeyChecking=accept-new;HTTPS:用 Git 凭据管理器里的账号/令牌,插件不保存任何密码
报错自检fetch/pull 失败时,提示里的主机名取自 origin 的真实地址(0.3.27 起),不再固定写 git@gitee.com;https 地址给的是 ls-remote 检查,不是 ssh -T
单文件上限100MB(Gitee 免费版、GitHub、GitLab 的默认硬限制都在这个量级),超过的文件不进仓库

私有库要注意两点:①用 SSH 地址最省事;②插件自身的更新清单默认指向作者的公开仓库,如果你把它改成自己的私有库,目前只有 Gitee 的 raw 链接会自动换成 API + token,GitHub/GitLab 私有库的 raw 匿名读会失败——这是已知限制。

工作区与会话也跟着机器走(0.3.44 起补齐 projcache 的 cwd)

工作区和会话跨机能不能用,取决于三处路径是否被换成本机值:

位置里面存的是换机器后怎么办
workspace\workspace-registry.json每个工作区的落点,已写成 {{APP_ROOT}}/xxx 占位符拉取时展开成本机路径,工作区直接开在本机对应目录
conversations\sessions\--ws-<id>--\...\session.v4.jsonl.zstd会话日志第一帧的 cwd拉取时按「本机这个工作区在哪」重写第一帧(只重写首帧,后面的帧一字节不动)
conversations\session_projcache\<sessionId>.jsonrecord.identity.cwd0.3.44 起按登记表的 sessionIds 反查该工作区在本机的位置再改写;以前这一步漏了,所以工作区能列出来、但点开历史会话仍指着对方机器的目录

所以「原机器的存放位置 → 新机器的存放位置」是这样推断的:先看工作区登记表里这个工作区的标题/槽位/相对结构({{APP_ROOT}} 下面第几层、叫什么名字),在本机展开成绝对路径;会话靠 sessionIds 认领同一个工作区,跟着那个落点走,不靠猜盘符。推断不出来(工作区路径是绝对的老盘符、登记表里没有对应项)就不改,宁可留着让面板提示,也不写一个猜出来的路径。

推送侧的报告也分了两组:scope: config 是配置文件里的路径,scope: data 是 workspace\ 与 conversations\ 里还指着对方机器的记录。面板把两组分开列(「配置文件里的路径」/「工作区与会话记录里的路径」),都能逐条采纳。

同步后新会话落在默认工作区(0.3.28 起)

同步完成会顺手把 DSH 拉回来,客户端起来后会自己开一个新会话。这个新会话落在哪个工作区,由 <DshHome>\storages\workspace.json 里的 global.defaultWorkspaceId 决定——而这个值 DSH 只在本机首次初始化时写过一次,之后同步几轮就可能停在某个「上次用的工作区」上。

0.3.28 起,post-exit.ps1 在客户端完全退出之后、重新拉起之前把该值校正到默认工作区。写在此时机是必须的:客户端持有这份登记表并在退出时回写,早写会被盖掉。

项说明
默认行为自动找标题为 default-workspace、或路径以 \deepseek-harness\default-workspace 结尾的工作区,把 defaultWorkspaceId 指过去
指定别的设置文件 ~\.dsh\dsh-config-sync.json 的 defaultWorkspace 填工作区 ID 或绝对路径
关掉同一字段填 off(或 none)
安全阀只在值真的不同时才写;写前原样备份为 workspace.json.bak-<时间戳>;写完回读校验 defaultWorkspaceId 与工作区条数;路径匹配不到工作区就跳过不动,不猜
副作用你在 DSH 里手动切换的「默认工作区」会被下一次同步覆盖回 default-workspace——不想要这个效果就把它设成 off
撤销把 workspace.json.bak-<时间戳> 拷回 workspace.json 即可

一键生成的三份脚本

「一键生成 推送 / 拉取 / 还原」可一次写出三份,也可只生成其中一份;仓库还是空目录时只生成「拉取 / 还原」。文件名、按钮、脚本内文字全中文。

脚本干什么安全阀
同步-推送.cmd退出后 capture 本机配置并推到远端先查 DeepSeek Harness.exe 是否还在跑;本机仓库是空目录时直接拒绝
同步-拉取.cmd退出后从远端拉取并 apply 到本机同上;先写明"本机领先几个提交 / 几项未提交",要输 y —— 它会用仓库版本覆盖本机配置
同步-还原.cmd退出后把 profile 退回某次 apply 前的快照同上;先 -DryRun 打印计划,按 y 才真写
同步-还原-<快照名>.cmd同上,但退回的是指定那一份(面板「还原备份」卡点某行生成)同上;文件名带快照名,多份可以并存互不覆盖

三份都写死本机参数(仓库、AppRoot、.dsh 位置),双击即可。拉取与还原支持 --yes 免确认(给自动化/远控用);没有确认时明说「没有收到确认(y),已退出:本机什么都没改」,不静默退出。

  • 提交说明:生成推送脚本时可填,以 -Message "..." 传进 sync.ps1,时间由脚本执行那一刻自动追加;留空用默认 sync: capture <时间> @ <机器名>。
  • 还原快照怎么选:生成时已有快照就明写进 -BackupDir(并显示时间、共几份);还没有(新机器刚装插件)则不写,交给 restore.ps1 运行时取最新一份——否则脚本会咬住"生成那一刻没有快照"。运行时若最新那份是空快照,会自动往前找第一份含 package.json 的并提示。面板「还原备份」卡点某一行时,走的是同一个生成器,只是把那一份的绝对路径作为 -BackupDir 明写进去、文件名带上快照名。

同步冲突(0.3.17 起,0.3.18 起两份副本都备份)

拉取被本机文件挡住时(同一路径本机也有一份、内容不一样),git 会整批 Aborting。旧做法是"失败就 reset --hard 回滚 + 用最新快照还原配置"——实测这条会把已经快进成功的 HEAD 又打回旧提交,于是永远差几个提交、反复失败。0.3.17 改成不 reset:

  1. sync.ps1 pull 先 git fetch,判定能否快进,再算交集:git diff --name-only HEAD origin/x ∩(本机未入库 ∪ 本机已修改)。命中就逐文件把两边都备份、写清单 ~\.dsh\sync-conflicts.json(每项记 kind、两侧字节数、是否二进制、两侧预览各 1500 字、两份副本是否备份成功),然后 exit 6 停下。仓库与配置都不动,HEAD 不回滚。 备份目录名带中英说明:~\.dsh\backups\conflict-<时间>-拉取冲突-Pull-conflict\,里面是

    • 本机-Local\<相对路径> —— 本机版本(冲突发生时工作区里的原样内容);
    • 云端-Cloud\<相对路径> —— 云端同名版本(git show origin/x:<路径>;走 git cat-file blob + 重定向按字节落盘,.zstd/.tgz 这类二进制不会被转码);
    • 说明-README.txt —— 中英双语:生成时间、仓库目录、同步前 HEAD、云端 ref 与 sha、条目数、两个目录各是什么、两种还原方式;
    • 清单-Conflicts.json —— 本次清单快照,事后能核对当时给了哪些选项、选了哪边。

    任何一侧写出来的字节数与源文件对不上,都会在终端警告、把该侧标记为未备份,面板上对应按钮自动置灰——不留"以为备份了其实没有"的坑。

  2. runtime\post-exit.ps1 见到退出码 6 不走回滚分支,结果写成「拉取待处理」。

  3. 重启客户端后,面板「概览」下方出现「同步冲突」卡(没有清单时不显示):

    • 一行一个文件,点整行展开/收起两侧预览(没有单独的「明细」按钮);
    • 每行右侧「用本机 / 用云端」,底部还有「全部用本机 / 全部用云端」;
    • 展开后两个按钮:「还原本机副本」「还原云端副本」——两份副本都能单独写回工作区,互不覆盖,也不动 git 历史;哪一份没备份成功,对应按钮置灰并写明原因;
    • 「保存选择」写回清单,「放弃清单」删掉清单(下次拉取重新算)。
  4. 选完再点「立即拉取」:sync.ps1 读同一份清单,用云端 的删掉工作区副本、用本机 的也先删(让 git 能快进),快进成功后再把 用本机 的从备份写回工作区——它于是变成一次本地改动,下次推送带上。清单随即清除。

粒度只有「逐项 用本机 / 用云端」+「单项从备份还原」,不做预览内编辑或手工合并:冲突基本都是"另一台机器也改了同名配置",逐项选一边就够,真要合并直接改文件再推送。

接口:GET/POST /config-sync/conflict(GET 读清单,每项带 localSaved/cloudSaved;POST {items:[{path,choice}]} 存选择、{action:'restore',path,side:'local'|'cloud'} 单项还原(默认 local)、{action:'reset'} 放弃清单)。还原写到清单里记录的 repo 路径,所以事后改过 repoPath 也不会写错仓库。status 多返回 conflicts: {exists,total,pending,at,backupDir}。

还原备份(0.3.13 起;0.3.25 起改为「立即还原」+ 时点还原)

面板「还原备份」卡(排在「退出后执行」之后、「同步内容」之前):上面一行是备份目录(默认 ~\.dsh\backups,apply.ps1 每次拉取/落地之前都会往这里打一份快照),下面列出该目录下的所有快照——名称、时间(并带「3 分钟前 / 2 小时前」这样的相对时间)、装了多少插件、能不能还原。

  • 列表不显示体积(0.3.41 起):每份快照都是整份 profile(几千个文件),旧版本每次刷新面板都要把这些快照的文件逐个 stat 数一遍——92 份 / 4.8 万文件实测要 7.7 秒,整次 /config-sync/status 拖到 11 秒,前端一次 fetch 撑不住就显示「读取失败」,要手动点「重新检测」。现在列表只给名称 / 时间 / 插件数 / 能否还原,体积交给资源管理器。同一次改动还做了两件事:远端探测改成「缓存 5 分钟 + 每次最多等 2.5 秒 + 插件装载时预热」,前端 status 请求带 15 秒超时和 3 次自动重试(文案会说「已自动重试 3 次」)。实测 status 从 8.0 秒降到 0.35 秒(中位,20 次)。

  • 0.3.25 起和「立即拉取」是同一条流水线:点某行的「立即还原」→ 面板先向宿主取一份差异预览,逐项列出「将撤走几个快照之后装上的插件」「将移走几个快照之后新增的文件」「将覆盖 / 写回哪些文件」「还原后要重装哪些插件」→ 点「确认还原」才排队 → 客户端正常退出 → post-exit.ps1 弹出可见进度窗口跑 restore.ps1 -RollbackToPoint → 结果写回 auto-sync-result.json → 自动重启客户端。不用再自己退客户端、双击 .cmd、输 y。

  • 时点还原 = 回到那个时间点,不只是把快照里的文件盖回去:快照之后新增的文件移进 backups\manual-<时间戳>\rollback\files;快照之后装上的插件,junction 只删链接(绝不穿进链接删实体目录)、实体目录移进 rollback\plugins;每一项都写进 rollback\rollback-manifest.json——全部可再还原,不做物理删除。动手前还会把当前配置整份另存 manual-<时间戳>。

  • 谁算「快照之后装上的插件」:以还原后 package.json 的 dependencies 为准,node_modules 里不在其中的直接子目录(含 @scope/包)会被撤走;退回去之后由 pnpm install 补齐快照里有、现在没有的插件。

  • 地址可自己改:点「修改」填一个绝对路径(例如 D:\DSH备份)保存后,卡片就改扫那个目录;点「恢复默认」清回 ~\.dsh\backups。配置存在 ~\.dsh\dsh-config-sync.json 的 backupRoot,留空=默认,跟本机走、不进仓库。

  • 只认能用的快照:快照里没有 profile\package.json 的(新机器首次拉取时本机无配置可备份,apply 会留下空快照)在列表里标「没有 profile 文件,不能还原」、「不可还原」置灰;宿主的 restore-backup / restore-preview / restore-now 都走同一套校验,会直接拒掉这类路径。

  • 只在这个目录里选:面板传来的必须是备份根之下真实存在的一份快照(越界、相对路径、含引号换行都直接报错),不会因为改了地址就把范围放宽到任意文件夹。

  • 老路径仍在:「退出后执行」卡里的「只生成还原脚本」照旧写 <仓库>\同步-还原-<快照名>.cmd(含 -DryRun 预览、输 y 确认、manual-<时间戳> 自保),给离线 / 远控 / 手动执行用;restore.ps1 的 -BackupDir 参数没变,新增的 -RollbackToPoint 由面板那条链路自动加上。

插件配置跟得走 + 跨机路径确认(0.3.30 起)

新机器同步完发现「配置没了」,多数不是拉取失败,而是这些配置从来就没进过仓库。0.3.30 把「插件配置」组从 4 个文件扩到 13 项:

仓库路径本机来源说明
settings\our-free-model.json~\our-free-model\settings.json免费模型中转设置(运行时字段已归一,不会每轮都「有改动」)
settings\comfyui-workflows.json~\data\dsh-comfyui\workflows.jsonComfyUI 工作流库
settings\dsh-pet-main-config.jsonc~\dsh-pet\main-config.jsonc桌宠配置
settings\dsh-market-state.json~\.dsh 的 profile 下 .dsh-market\state.json插件市场状态
settings\dsh-pet-memory.json~\dsh-pet\memory.json桌宠记忆
settings\our-free-model-picks.json~\our-free-model\picks.json收藏
settings\dsh-im-interface-language.json~\integrations\dsh-im\interface-language.jsondsh-im 界面语言
settings\dsh-qq-config.json~\integrations\dsh-qq\config.jsonQQ 集成配置
settings\dsh-qq-workspaces.json~\integrations\dsh-qq\workspaces.jsonQQ 集成工作区
settings\redteam-preset-declare.json~\redteam\preset-declare.json红队预设声明
settings\task-board-ledger.json~\task-board\ledger-v2.json任务板台账
settings\task-board-scheduler.json~\task-board\scheduler-v2.json任务板调度
settings\dsh-workspaces.json~\storages\workspace.jsonDSH 工作区登记(新机器「工作区不见了」就是它没同步)

仍然排除:凭据与运行时状态不进仓库 —— channel-pack(含 token-ledger.json)、workbuddy-vault、dsh-purge、stats / feed / catalog / availability、integrations\dsh-qq\bots\<bot>\state.json、会话缓存。

带本机路径的配置:占位符 + 换机确认

两台机器的盘符几乎不可能一样(一台 D:\DeepSeekHarness,另一台放在别处),所以带绝对路径的配置不能原样搬过去:

占位符含义
{{APP_ROOT}}客户端安装目录(DeepSeekHarness 所在的那一层)
{{DSH_HOME}}.dsh 配置目录(一般是 %USERPROFILE%\.dsh)
{{WORKSPACE_HOME}}工作区根目录
{{ROOT:<key>}}用户自选目录(ComfyUI 模型目录、代理程序路径等),键值由用户在面板里定
  • capture.ps1 写仓库前做归一:本机根 → 占位符。同一个路径在原文件里可能是 4 种写法(原样反斜杠 / JSON 双反斜杠 / 正斜杠 / JSON 转义斜杠),都会认;替换按根长度降序(工作区根在安装目录之下,先换长的才不会错位)。
  • 仓库里一律正斜杠({{APP_ROOT}}/mnemon),JSON 里不会出现反斜杠转义问题。
  • apply.ps1 落地前展开:占位符 → 本机路径。

路径确认:拉取后由你选,选过就不再问

  • 首次同步会在 ~\.dsh\config-sync-roots.json 落一份本机根(探测值),本机私有、不进仓库。
  • 某台机器缺某个根(展开不掉)时,apply.ps1 跳过那个文件不写,把它记进 ~\.dsh\config-sync-pending-paths.json(哪个 token、哪个文件、要选什么目录)。
  • 面板「本机路径」卡把这些项列出来,填好本机路径 → 保存 → 重新拉取一次就写进去。
  • 确认过的值优先于自动探测,之后不再问;根都对不上的情况下宁可不写——把 {{ROOT:xxx}} 字面量写进配置,插件读到会当真实路径,那种报错比「没写」难查得多。

两条体检:推送后看根外路径,拉取前看你改没改(0.3.36 起;0.3.43 起根外清单只在接收方提示)

推送之后(capture.ps1 末尾):扫一遍仓库里的配置类文件(settings\*.json、profile\*.json、skills\、presets\、prompt\、顶层 json;conversations\ / workspace\ / mnemon\ / storages-record\ 是会话与运行时数据,换机器本来就不该一致,不参与),把不属于任何已确认根的绝对路径抓出来——比如仓库里还写着 F:\workspace-new(本机根都在 D:\ 下,这条换台机器就是死路径)。抓到就写 ~\.dsh\config-sync-absolute-paths.json(带 machine 与写它时各根的值),控制台也 Warn 一句。不拦提交:路径本来就可能是对的,只是提醒你它跨不了机器。

0.3.43 起这份清单只对「接收方」有意义:清单是推送时写的,本机自己推出去的路径在本机当然都在,把它摊在本机面板上只会让人以为出了问题(实测 50 条里有 40 多条是 conversations\session_projcache 的会话缓存)。现在面板先比清单里的 machine 和本机机器名:

  • 清单来自本机 → 不显示、也不计入告警数(status.localPaths.absolute 归 0)。
  • 清单来自另一台机器(本机是接收方,刚要拉或刚拉完)→ 显示「这份配置是 <机器名> 推送的」,并且逐条给推荐目录:先拿报告里记着的对方根值做前缀映射(F:\DSH\SquadLink → 本机 <APP_ROOT>\SquadLink,保留原大小写),没有根可映射就按最后一级目录名在本机几个根、用户主目录、桌面、各盘根下找同名目录;都没有就明说「本机没有等价目录」。
  • 每行一个「采纳」:点一下就把推荐目录记进本机自选目录({{ROOT:<key>}}),下次推送起这些路径变成占位符,哪台机器拉取都换回自己的值;已采纳的那行显示「已采纳」。原来那句「把目录加成自选目录、再推送一次」要人自己去翻,现在一步到位。
  • 顺带过滤了正则误匹配:s://1v3.apiipu.com/... 以前会被当成 s: 盘符下的路径,现在含 :// 的直接跳过。

拉取之前(apply.ps1 写 settings 前):铺配置是整份 WriteAllText 覆盖,你改完路径没推送就点拉取,改动会被静默盖回去(备份是有的,但没人会去翻 ~\.dsh\backups\config-<时间戳>\settings\)。所以现在覆盖前先比一次:本机现状(归一化成占位符、CRLF 拉平成 LF、our-free-model.json 的 catalogSyncedAt/reloadedAt/reloadCount 一起置 0 对齐 capture 的写法)跟仓库版本不一样,就记进 ~\.dsh\config-sync-overwrite.json,面板提示「有 N 份配置被仓库版本覆盖(本机改动已先备份)」,每条带备份路径。想保住本机改动:先推送,再拉取,别反过来。

「本机路径」卡怎么用(0.3.34 起:只读 + 修改;0.3.37 起:平时不显示)

  • 平时不进页面:这一区(连同「本机路径」标题)整块不渲染——正常机器上路径本来就不用改。入口只有一个:页面标题行的 「本机路径」 按钮,点一下展开,再点变「收起本机路径」。
  • 三类都算告警:待确认路径、仓库里有根外绝对路径、上次拉取覆盖了本机配置——status.localPaths.alert 给三项合计,按钮变橙并显示「本机路径 · N 项告警」,点开就是明细。
  • 默认只读:三行根({{APP_ROOT}} / {{DSH_HOME}} / {{WORKSPACE_HOME}})用等宽字显示当前值,下面一行灰字是它对应的占位符;自选目录({{ROOT:<key>}})同样展示。
  • 点右上「修改」才变成输入框,出现「保存」「取消」;保存成功后自动回到只读态(跟「本机仓库」「同步内容」等卡的操作方式一致)。
  • 编辑态多三件事:每个根右边有「用探测值」(一键填回自动探测到的目录,填错了就靠它退回);每个自选目录右边有「删除」;下面一行「新增目录」填键名 + 路径后点「添加」,用来补插件装在别处的根(键名只接受字母数字 _ . -)。
  • 顶部徽标:有未确认项显示橙色「待确认 N 项」,全齐显示绿色「已确认」;「重新读取」始终可用,重新拉探测值不覆盖已保存的确认值。
  • 强制编辑态:三个根一个都没探测到时停在编辑态,先填再保存。
  • 这一卡的值优先级最高:保存过的根压过脚本命令行探测到的值(Get-SyncRoots 里 roots.json 优先、只补空值)。所以填错不是"没生效",而是真的按错值展开、把配置写成死路径。

问题反馈:走 GitHub Issue(0.3.31 起,0.3.32 起免令牌读)

面板最下面「问题反馈」卡。GitHub Issue 是唯一真源,不再往配置仓库里塞 json:

  1. 填标题 + 详情 → 直接 POST /repos/QcWyGm/dsh-config-sync/issues,label feedback,正文末尾自动附一段环境块(插件版本、机器名、时间、平台、安装目录、配置仓库路径),在 GitHub 上不用再追问;
  2. 列表直接读 Issue:GET /issues?state=all&labels=feedback,open / closed 都在,点标题就是 GitHub 网页;
  3. 点「已解决」→ PATCH issue {state:'closed'},GitHub 上还是同一个线程,想继续讨论就在那边接着聊,想重开点一下又回来;
  4. 面板顶上一行是连接状态,三态:已用 xxx 登录(可提交、可关单) / 令牌不属于所有者(只能读和提交) / 匿名读取(提交走浏览器)。

读不需要令牌(0.3.32 起):这个仓库是公开的,Issue 列表匿名请求就能读(实测 HTTP 200,限额 60 次/小时·每 IP)。所以有令牌先带令牌读(自己的账号、5000 次/小时),令牌失效或压根没有就自动退回匿名读——新机器第一次打开面板就能看到已有反馈,不用先配令牌。

没令牌也能提交(0.3.32 起):面板会把标题、正文和环境块预填进 GitHub 的新建 Issue 页面(https://github.com/<repo>/issues/new?title=&body=&labels=feedback),浏览器里登录着 GitHub 的话点一下 Submit 就成了,全程不需要 token。这条路只在没有令牌时启用;有令牌仍走 API 直发,才能进队列、能补发。

关单只有仓库所有者能点(0.3.32 起):改状态前服务端先用 GET /user 认令牌归属,login 和仓库所有者不一致就直接 403,面板上那个按钮同步灰掉显示「仅所有者」。所以别人的令牌即使填进面板也关不掉单——这条限制在服务端,不只是界面。

令牌不用手填:默认从本机 git 凭据取一次(git credential fill,平时 push 过就已经在里面),存进 <dshHome>\config-sync-gh.json(本机私有、不进仓库)。取不到(比如从没在这台机器 push 过)才需要粘一次,粘完立刻拿 /repos/{repo} 验一遍——验不过就不存。仓库和 label 也能改,跟令牌填在同一行。

断网不丢:建 Issue 失败时这条会进 <dshHome>\config-sync-feedback-outbox.json,面板显示「本机还有 N 条没发出去」,联网后点「补发」逐条建 Issue,成功的自动出队。这不是"另存一份",只是待发队列——发出去了队列就空。

筛选与查询(0.3.33 起):列表头上是 未解决 / 已解决 / 全部 三个按钮(各带实时条数,服务端一次拉全量后算 open/closed/total),右边一个搜索框,打字即筛——编号、标题、作者、正文都参与匹配,正文里那行环境块(机器名、版本)也能搜到。筛选全在本地做,切状态、敲键盘都不打接口,不占匿名限额。灰字实时回显「显示 N / 共 M 条(关键词 xxx)」,当前筛选下没匹配就给提示而不是空白。

卸载残留自动清理(0.3.29 起)

卸载插件是宿主做的:删安装目录、删 package.json 里的依赖、删 cordis.patch.yml 里的模块段。但它不删 profile\node_modules 里那条 junction——实测 dsh-image-search 卸载后目标目录 D:\DeepSeekHarness\dsh-image-search-0.1.0 已经没了、链接还指着它。这类断链 pnpm 也不管(平铺布局下它只按 lockfile 重装,而链接不是它建的),同步同样从不清,于是一直挂到下次重装。

所以 apply.ps1 每次落地都巡检一遍(第 9.6 段),只删三条同时成立的链接:

判据说明
是链接LinkType 为 Junction / SymbolicLink,普通目录不碰
所有 Target 都不存在只要有一个目标还在,就当正常软链留着
不在 package.json 的 dependencies 里在用的 link: 插件一律跳过

删除用 Directory.Delete,只删链接本身、不顺着链接动目标;日志逐条写 已清理孤儿链接(卸载残留):xxx → 目标不存在 yyy。

顺手清插件备份:同步每轮「旧目录先改名备份」会攒出 <目录>.bak-<时间戳>。与在用版本相同的备份是纯冗余(内容就是刚铺上去的那一份),直接删;版本不同的只留最新一份;主目录已不存在的备份保留(可能是唯一回滚点)。

同时修掉一个误判:0.3.29 之前部署判据比的是 mtime,而 git checkout/fetch 会把归档的 mtime 刷成当前时间 → 每轮同步都判「有新版本」,反复改名备份 + 重新解压(dsh-purge-1.1.92 与 .bak-时间戳 并存、每轮多搬一次目录就是这么来的)。现在比归档内 package.json 的 version 与已部署目录的 version,不同才算新版本;读不到版本时退回 mtime,并要求新 10 秒以上。

同步内容勾选(0.3.8 起)

面板「同步内容」卡(挂在面板最下面):把云端同步的十组内容逐组列出来,默认全勾;点一下某一行就展开该组明细,再点一下收起(没有单独的「明细」按钮);组和单项都能取消勾选 —— 取消后推送不再抓、拉取不再还原,重新勾上就继续同步(推送和拉取共用同一份勾选)。

  • 先进只读态:卡片打开时所有复选框是灰的,只展示当前勾选情况;点「修改」才解锁勾选,改完点「保存」才写进文件(点「取消」就丢弃这次改动、回到读到的状态)。避免误点一下就改掉同步范围。
组云端目录明细项(可单独排除)
本机 profileprofile/package.json / lock / cordis 等(整组开关,文件互相咬合,不支持单项)
提示词prompt/AGENTS.md、prompt-inject.md
技能skills/每个技能目录
预设presets/每个预设
技能市场台账storages-record/skill-market-installed.json、skill-market-panel.json
插件配置settings/13 个配置文件(清单见上面「插件配置跟得走」一节),可单独排除
记忆mnemon/整目录(只有一项)
本地插件local-plugins/每个 link 插件
工作区workspace/每个登记的工作区(登记表 workspace-registry.json 也只写勾选项)
会话conversations/每个工作区对应的会话
  • 勾选记在 ~\.dsh\config-sync-scope.json,跟本机走、不进仓库(另一台机器各自勾);文件里只留与默认不同的项,删掉这个文件就等于恢复全勾。
  • 跳过两侧都生效:capture.ps1 不抓(不再写进仓库)、apply.ps1 不还原(不动本机)。-PruneSkills 也不会把"你勾掉的本机技能"当成多出来的项删掉。
  • 卡里另有一个开关「取消勾选时顺手删掉云端副本」,默认关闭:关着时取消勾选只是"这边不再管它",云端那份留着,重新勾上立刻继续同步;打开后取消勾选会真的删掉仓库里的对应目录/子路径(推送时删、提交后另一台机器也拉不到),删除不可恢复。
  • 点不动的明细项(profile 五个文件、mnemon)是刻意做成"整组开关":lock 与 package.json 的 specifier 必须一致,单独排除会装不起来。
  • 本机 profile 这一组是必需内容,不能取消勾选(面板上显示「必需」、复选框是灰的)。它不是"可省的一项",而是 apply.ps1 的前置条件:仓库里没有 profile\package.json 或 profile\cordis.patch.yml 时,拉取会直接 [X] 仓库内容不完整 退出;同一组的 profile\host-contract.json 还是两台机器的版本一致性校验,丢了它版本不一致也照铺,客户端可能起不来。手改 config-sync-scope.json 把这一组写成 false 也无效:capture.ps1 / apply.ps1 与面板都按"开启"处理(Test-SyncGroupEnabled 里钉死)。

自动同步 / 自动推送(两个开关,默认关)

开关行为
启动时自动拉取启动 8 秒后只读 git fetch 比对;落后 > 0 → 系统确认框(是/否)→ 选"是"当场把客户端整族退出(实测 5 秒内进程归零)→ 可见窗口跑拉取 → 拉完自动重启
退出时自动推送三个来源任意命中就排队:本地领先提交、未提交改动、本机资料比上次同步新(新会话等)。启动后每 2 分钟复查,已排队不重复拉起;等 DSH 完全退出后才 push,推完自动重启
  • 动手都由插件拉起 runtime\post-exit.ps1 -Phase wait(隐藏进程)盯客户端退出;真退了才由它用 -WindowStyle Normal 起 -Phase run 的可见窗口干活,标题「DSH 配置同步 · 拉取/推送」。排队期间不弹窗 —— 否则从开机到关机会一直挂着一个挡视线的空窗口。
  • 仓库还没就位(含空目录)时首检直接跳过,且不记成"检查失败"——空目录只是等着拉取。
  • 拉起方式踩过的坑:detached: true 起 powershell 在 Windows 上会当场退出(日志一行都没有),改成短命 powershell 用 Start-Process -PassThru 起独立进程;spawn() 失败是异步的(只在 error 事件报),所以要等 launcher 退出码、拿不到 pid 就如实失败,并写 ~\.dsh\logs\auto-sync-launch.log。
  • 等退出不能只看 pid:插件跑在子进程里,process.pid 不是客户端主进程 —— 按 pid 判断会出现"客户端在跑、等待进程以为它退了",当场在运行期拉取。改成按进程名;另外「点了关闭但进程赖着不走」(实测 7 个进程 hwnd=0 存活 1 分钟以上)也认:活过 60 秒、无可见窗口持续 16 秒 → 结束残留进程再同步(auto-sync-wait.log 有记录)。
  • 一个时刻只跑一次同步:~\.dsh\auto-sync.lock 记 pid。启动首检排的 push 与用户确认的 pull 曾同时开工、并行改配置;现在后来者读锁:同种复用,不同种(手动优先)先结束那个还没开工的等待进程再改排。
  • 退出客户端用整族结束:taskkill /PID <主进程>(不带 /F)对 Electron 主进程无效(This process can only be terminated forcefully),改为 /IM "DeepSeek Harness.exe",3.5 秒后再补 /F。
  • 重启前清环境变量:从插件链路起的客户端连续 3 次"起来就退",干净 shell 跑同一个脚本却成功 —— 差在继承的 ELECTRON_RUN_AS_NODE(还有 ELECTRON_NO_ATTACH_CONSOLE/NODE_OPTIONS/NODE_PATH),清掉后一次成功;启动后还数进程、没活等 8 秒重试,最多 3 次。
  • 拉取失败自动回滚:git reset --hard 回原提交 → 用这次 apply 写盘前刚建的 backups\config-<时间戳> 跑 restore.ps1 → 回滚成不成功都写日志,不完整会提示「请打开日志核对」。
  • 拉取/推送都重启:成功用新配置,失败用回滚后的原配置。重启成功后会刷新 config-sync-state.json 的 lastSyncAt,避免刚 apply 写过的文件被下一次首检误判成"待推送"。
  • 进度窗口收尾:拉取/推送跑完后窗口不自动关闭(原来停 3 秒自己消失,结果一闪就没了,看不到失败原因),改成把「同步已完成 / 日志路径」留在屏幕上,按任意键或点右上角 × 才退出。客户端是在这之前由 explorer.exe 代启的,跟这个窗口没有父子/作业关系,所以窗口留多久、还是被点 × 直接结束,都不会把客户端带走。update(退出后安装那一路)同样保留窗口,横幅写「插件更新已完成」;只有 relaunch(纯重启,窗口里没有可看的结果)仍旧停 3 秒自动关。
  • 退出码:0 完成、2 等待超时、3 缺 sync.ps1、4 重启失败、5 同步失败(pull 已尝试回滚)。
  • 「取消排队」只撤插件里的排队记录;已拉起的外部等待进程仍会等你退出,要停就手动结束它(auto-sync-wait.log / launch 日志有 pid)。

插件自更新(0.2.0 起)

面板「插件更新」卡显示本地版本、云端版本和一行状态;清单与安装包都在这个公开仓库里(version.json + releases/dsh-config-sync-<版本>.tgz,只读几百字节,不拉整包),服务端缓存 30 分钟,挂载时查一次、之后每 30 分钟复查。

状态含义
绿色「已是最新」本地版本 ≥ 清单版本
橙色「有新版本 x.y.z」清单版本更大,附一句新版本说明和「下载更新 x.y.z」按钮
橙色「已下载,待安装」包已经下到 ~\.dsh\update-staging\(插件目录一个字节都没改),点「重启并安装」生效
橙色「已安装,待重启」只出现在老路径上(运行中安装过但还没重启),点「重启 DSH」生效
红色「检查失败」清单读不到(网络 / 仓库没建 / 404),附「检查更新」可强制重查,并逐个列出每个源的失败原因
  • 清单有多个源,按顺序试:内置候选是 gh-proxy.com → ghfast.top → GitHub raw,前两个是实时反代(实测 1 秒内拿到最新清单,tgz 也能从同一前缀拉下来)。国内直连 raw.githubusercontent.com 经常不通,而宿主的 fetch 是 Node 内置实现、不读系统代理,超时一到就被掐断(面板上一句 This operation was aborted 就是这么来的)。下载安装包同样按「读到清单的那个源 → 其余源」依次试,清单通不等于包也能从同一个域名下来。单次 30 秒超时。
  • 别拿 jsDelivr 当清单源:它把 @main 这类分支别名缓存很久,实测发版后仍返回上一版 version.json(purge.jsdelivr.net 也没立刻刷新生效),会让「有新版本」晚几个小时才出现,比读不到更误导。包体是按版本号命名的、不受这个缓存影响,但清单会被拖住。
  • 私有库要填令牌:如果把 dsh-config 私有库当清单源,匿名 raw 一律 403。更新相关的配置都集中在「插件更新」卡里(点「修改配置」进入编辑态):「更新清单」留空即用内置候选,也可指向自建清单(填了就只用你填的那个,不掺镜像);「更新令牌」填 Gitee 私人令牌后,清单和安装包都改走 API raw 端点(/api/v5/repos/qcwygm/dsh-config/raw/<path>?ref=main&access_token=…),分支用 ref 传、不能写进路径。令牌只存本机设置文件,不进仓库;留空保存表示"不改动已存令牌",另给「清除已存令牌」按钮。相对路径的 tgz 会相对实际读到的那份清单解析,换地址不会拼错域名。
  • 更新分两段(0.3.7 起):下载与安装分开。点「下载更新 x.y.z」只把 tgz 下到 ~\.dsh\update-staging\dsh-config-sync-<版本>.tgz 并校验 sha256,宿主不碰插件目录 —— 所以 DSH 不会重新同步插件树,你正在看的「配置同步」页面不会跳走。下载完按钮换成「重启并安装」,点它才退出:spawnWatcher(-Mode update -UpdateTgz <本地包> -UpdateSha256 …) → 关 DSH → 退出后的脚本备份旧目录(~\.dsh\backups\plugin-<旧版本>-<时间>)、覆盖安装、自动拉起新版本(runtime\post-exit.ps1 的 update 段,失败会用备份回滚)。安装发生在退出之后,全程只重启一次。
  • 为什么不再运行中安装:0.3.4–0.3.6 是「DSH 不退出,直接覆盖插件目录」。实测这一步会让宿主把当前面板卸载掉 —— 用户看到的就是"更新到一半页面自己跳走了,得点回来才能按重启"。三步日志能对上:11:41:55 覆盖安装 → 11:42:10 已启动 relaunch,中间可见 15 秒空档。POST {action:'install-live'} 仍保留给 CLI/脚本,界面不再用;POST {action:'apply'}(退出后再下载安装)也保留。

作为插件市场的索引源

本仓库根目录的 market-index.json 就是一份可直接使用的 DSH 市场索引(format: 1,source.type: git + 40 位 commit,安装时从本仓库取包,无需构建)。索引地址:

https://raw.githubusercontent.com/QcWyGm/dsh-config-sync/main/market-index.json

在 DSH 里「插件 → 发现 → 索引源」填入上面的 URL,即可在列表里看到「配置同步」,点安装即按该 commit 拉取本仓库。

  • 前提:这是宿主官方发现页(@deepseek-ai/dsh-plugin-market)的索引源,需要 profile 启用官方 @deepseek-ai/dsh-experimental-plugin-market-bundle 组合包;没启用时「发现」页与索引源入口都不存在。
  • dshmarket 里搜不到,是正常的:第三方市场插件 dshmarket 的列表只来自它自己的精选目录(awesome-dsh-plugin.com/plugins.json,国内走 npm 上的 dsh-plugin-catalog),源码里没有自定义索引源这条路(lib/registry.js 每次只从 region 路由表拉那一份目录,lib/sources.js 只解析该目录的条目),而且它的安装被限定在「该目录里列出的来源」,其它来源一律拒绝。所以自建索引再怎么标准,也不会出现在 dshmarket 的搜索结果里。要出现在那里,只能向 awesome-dsh-plugin 仓库提 PR 收录(它的 README 明确写着:别对 dshmarket 仓库提插件条目)。
  • 国内取索引:raw.githubusercontent.com 时通时不通,可以换 https://gh-proxy.com/ 或 https://ghfast.top/ 前缀;jsDelivr 的 https://cdn.jsdelivr.net/gh/QcWyGm/dsh-config-sync@main/market-index.json 也能用,但它对 @main 这类分支别名有缓存,发布后可能几小时才读到新版本。
  • 只想自己装:不必绕市场,直接 dsh plugin add 那份 tgz 就是最省事的路径(见上面的安装段)。

升级流程:改代码 → pwsh -File tools\pack-plugin.ps1 打包 releases/dsh-config-sync-<版本>.tgz(脚本会按 package.json 的 files 收内容,并自动把 sha256 回填进 version.json)→ 手动改 version.json 的 version(以及 notes)→ 把 market-index.json 里那条 versions 的 version 与 commit 换成新提交 → 推送。索引只引用 commit,所以旧版本也能随时装回。打完包先跑 node tools\selftest.mjs <仓库路径> + node tools\clientcheck.mjs 自检。

接口与路径

接口说明
GET /config-sync/status体检 JSON(含 repo.source / repo.configured / repo.phase / repo.empty / repo.remoteHeads / repo.remoteFiles / repo.pushAllowed / repo.pullAllowed、remote.configured、remote.backupRoot / remote.backupRootCustom、backup.{root,defaultRoot,custom,count,latest,latestFiles,snapshots:[{name,dir,at,files,bytes,usable,plugins,bundles}]}、local.{lastSyncAt,pending,sample}、auto.lastResult)
POST /config-sync/prepare{action:'push'|'pull'|'restore'|'restore-backup'|'all', message?, backupDir?},生成对应 .cmd;all 返回 built: [...](本机仓库为空时按云端文件数分流:都空 → ['push','restore']、云端有内容 → ['pull','restore'],另一个进 skipped);空目录上 push 只在云端 0 文件时放行(那次就是初始化推送),pull 也只在云端有文件时放行;restore-backup 必须给 backupDir(须是备份根之下、含 profile\package.json 的一份快照)
POST /config-sync/settings{repoPath?, remoteUrl?, sshKey?, autoPull?, autoPush?, autoRestart?, backupRoot?, clearBackupRoot?}:校验 → 写设置 →(地址变了才动 git),失败回滚并 400;backupRoot 留空/clearBackupRoot = 回默认 ~\.dsh\backups;空目录可以存,只有"有别的文件又不像仓库"才拒
POST /config-sync/auto{action:'check'|'pull-now'|'push-now'|'restore-preview'|'restore-now'|'cancel', backupDir?, closeNow?, restart?};check 只读 fetch 比对(本机仓库为空时短路返回 pendingPull: true 而不是报错),pull-now/push-now/restore-now 都不带 /F 关闭客户端(走正常退出,配置能存下来);push-now 在空目录上 400;restore-preview 只读算差异(pluginsRemove / pluginsInstall / willRemove / willOverwrite / willWrite),restore-now 才是排队「退出后时点还原」;status 里 auto.pending 带 reason(为什么排队)
GET/POST /config-sync/update{action:'check'|'install-live'|'restart'|'apply'}:check 重新读清单(多源依次试);install-live 运行中下载+校验+覆盖安装(不退出 DSH,回 installed 与 restartRequired);restart 排队 relaunch 后关闭 DSH;apply 是「退出 DSH 后再安装」的兜底
GET/POST /config-sync/scopeGET 回十组同步范围(每组 {id,title,desc,enabled,items:[{id,label,enabled,excludable}]} + deleteCloud + 本机文件 path);POST {deleteCloud?, groups:{id:{enabled?,excluded?}}} 只落与默认不同的项(全勾的组不写文件),不存在的排除项会被清掉

跨站请求 403。客户端只 fetch 这几条,不 import 任何 @deepseek-ai/dsh-client-*,样式只用 --dsw-alias-* token。

路径:仓库按上面的优先级;客户端根 config.appRoot 优先,留空则由插件自身位置推出(插件装在 <AppRoot>\dsh-config-sync-0.1.0),换机不用改;.dsh 为 config.dshHome → %DSH_HOME% → ~\.dsh。同步状态文件 ~\.dsh\config-sync-state.json 由 sync.ps1 写,插件只读。本机私有文件:config-sync-roots.json(已确认的路径根)、config-sync-pending-paths.json(缺根跳过的项)、config-sync-absolute-paths.json(0.3.36:推送后扫出的根外绝对路径)、config-sync-overwrite.json(0.3.36:拉取覆盖了哪些本机配置 + 备份路径)。

边界:capture 要读 cordis.patch.yml,DSH 运行时这个文件正被应用持有,抓到的是半截状态;apply 写完以后,DSH 退出时又会回写覆盖。所以边界固定为:判断该跑什么、把命令写好,退出 DSH 后双击跑。同步机制、目录结构、换机与排障的完整说明在配置仓库根 README.md,本文只讲插件自身。

生成器里已经堵掉的坑

都是在新机器上真跑一遍才暴露的:

坑处理
cmd 的 echo 吃 < > | & ^ %,echo ... config-<时间戳> 被当重定向、那句还不打印动态文案全过 echoSafe():%→%%,特殊符号前加 ^
chcp 65001 后 set /p 读不到重定向输入,自动化喂 y 时 GO 为空、"执行结束"却没做事、退出码还是 0--yes 免确认 + 无确认时明确回执
新机器分支没有 upstream,pull --ff-only 报 There is no tracking information…,PS 5.1 包成 NativeCommandError 后脚本继续走sync.ps1 先探 upstream,没有就 pull --ff-only origin <branch> 再 set-upstream-to;push 带 -u
GIT_SSH_COMMAND 少了 ssh.exe 本体 → /usr/bin/sh: - : invalid option四分支:有 core.sshCommand 照用;找到 ssh.exe 写完整命令;只有私钥没 ssh.exe 就不设变量;无私钥交给 .ssh\config
客户端 asar 打包时按路径找 AppRoot 全落空、bundle 全误报缺失isAppRoot 认 app\package.json 或 app.asar;版本与 bundle 存在性从 asar 里定向取(PS 5.1 ConvertFrom-Json 2 MB 上限,5.7 MB 头只能按 entry 偏移直读),判不了就当"不知道"
插件目录已存在就跳过部署,tgz 更新了本机还是旧代码apply.ps1 比归档内 package.json 的 version 与已部署目录的 version(0.3.29 起;之前比 mtime,被 git checkout 刷时间害得每轮误判),不同才改名 .bak-<时间戳> 备份后重新部署
开了"退出时自动推送"却什么都不发生(无日志、无结果文件)两个叠因:①排队判据只看 ahead > 0,新会话还没 capture 时恒为 0 → 改成三个来源都算 + 每 2 分钟复查;②spawn 假成功(detached 起 powershell 当场退出 / spawn 失败只在 error 事件报)→ 改用 Start-Process 起独立进程 + 等 launcher 退出码 + 写 auto-sync-launch.log
空目录被当成"仓库坏了",面板红字 + 推送按钮可点但必失败0.3.26 起按 repo.phase 分四态:空目录只放行拉取,prepare(push) / push-now / sync.ps1 push 三层都拦住
「云端到底有没有东西可拉」看分支数 → 远端 main 还在、树被删空了仍被判「有内容,只许拉取」,拉下来是空目录、状态不动,初始化推送入口永不出现(死循环,实测 Gitee 上就是这情形:98 个提交、最后 12 个全是"删除文件"、最新树 0 文件)0.3.36 起判据换成分支树里的文件数:ls-remote --heads 先数分支(0 个直接算空),有分支才临时 bare 仓库 fetch --depth 1 --filter=blob:none + ls-tree -r 数文件,探完删目录;remoteFiles===0 一律算「两边都空」→ 只放行推送,且 sync.ps1 用 --force-with-lease 覆盖那个空分支(树是空的,不丢数据)
拉取整份覆盖 settings,改完路径没推送就拉取 → 刚改的值被静默盖回去(只有备份目录,没有提示)0.3.36 起覆盖前比对(归一化成占位符 + CRLF 拉平 + our-free-model.json 三个时间戳字段一起置 0),不同就写 config-sync-overwrite.json 并在面板/控制台说「有 N 份被覆盖、本机改动已先备份」

会话 header cwd、空快照兜底、Get-FileHash 缺失这几类问题由仓库里的 apply.ps1 / restore.ps1 / dsh-sync-common.ps1 处理,细节见根 README.md 第 5、10 节。

自检

  • node tools\selftest.mjs <仓库路径>:假 ctx 拉起宿主半身,打 status、生成脚本、断言 JSON 与落地文件;会先备份仓库根已有的同名 同步-*.cmd,断言完原样还原。必须显式传仓库路径。
  • node tools\clientcheck.mjs:假 __ModuleLoader__ + react 替身跑客户端半身,断言模块能加载、apply() 三处注册成功(侧栏行与 main 的 id/key 成对)、inject 返回回收函数、组件在无数据下渲染不抛。验的是契约与结构,UI 效果仍需重启后目视。
  • pwsh -File tools\pack-plugin.ps1:打包并把 sha256 回填 version.json。必须用 PowerShell 7 跑(脚本是无 BOM 的 UTF-8,5.1 按 GBK 读会把中文认错)。

实测记录(2026-11-11,隔离环境)

假云端裸仓库 + 假客户端根 + 只有 profiles\desktop 的独立 .dsh,从 tgz 装插件,全程不碰主力机目录。

步骤结果
装插件、开面板三道路由都注册;未配置时 repo.path 为空不静默回退,推送被 400 拦
配仓库 / 云端200,路径落进本机 .dsh\dsh-config-sync.json;无 origin → git remote add
生成三份脚本落在新机器仓库根,-AppRoot 指向该机客户端根,带 --yes 与 y/N 守卫
首次拉取首次补 upstream → Already up to date → 备份 11 文件、铺 436 个 → pnpm install 21 个插件 → 25 个 bundle 全部可解析
工作区 / 会话资料落在假客户端根;登记表路径展开成本机路径;空工作区被补建成空目录;会话目录名与本机路径重算的编码逐字一致,header cwd 由 fix-session-cwd.mjs 改写
推送 / 还原推到假云端;还原先 -DryRun 出计划,--yes 后自保 manual-<时间戳> 再写回,测试标记被真抹掉
退出后同步post-exit.ps1 五个退出码(0/2/3/4/5)全部复现
本机待推送真机:git status 干净,~\.dsh\sessions 有两个比上次同步新的会话,徽标转橙「待推送」,local.pending = 15(含 apply 刚重写的 profile 五文件)
确认即退出 + 可见窗口 + 重启真机 {"action":"pull-now"}:DSH 7→0 用 5 秒;+10 秒写着「拉取」的 PowerShell 窗口出现(排队期无窗口);拉完自动 0→4→7
点关闭即推送 + 可见窗口 + 重启真机开 autoPush:排队为隐藏进程 → 点关闭后 7 个进程 hwnd=0 不退 → 16 秒后强制收尾 → 窗口「推送」→ 推送成功 → 自动重启
互斥首检的 push 与手动 pull 同时命中:后者结束前者未开工的等待进程并改排,未再并行
空目录语义(0.3.26)隔离环境实测:repoPhase 八种形态判定全对(含 8.3 短名路径);空目录跑 sync.ps1 push 被拦(退出码 1、未生成 .git);同一目录跑 pull 自动 init → fetch → checkout main,apply.ps1 随包就位

同步边界:工作区资料与会话会跟着走,但默认工作区(default-workspace)不同步(目录名按各机 WorkspaceHome 编码,目标机认不出,只会留下"历史为空"的空工作区);models/、*.whl、*.safetensors 与任何 >100 MB 的文件也不进仓库。细节见根 README.md 第 4、5 节。

上一轮复审发现的 6 条问题(git 身份、跨机会话目录互相改写、写死 origin/main、结束文案、重复逻辑、死代码)已全部整改并验证,对照表在根 README.md 第 12 节。

Plugins relacionados