dsh-sync-client
v0.1.7
Published
DSH configuration sync client: host-plane plugin row that syncs agent presets and settings namespaces with dsh-sync-server.
Maintainers
Readme
dsh-sync-client
DSH 配置同步系统的客户端:一个 host 平面的 Cordis 插件包(bundle),把用户自建的
agent preset($DSH_HOME/.agent-presets/)、用户 skills($DSH_HOME/skills/)与
白名单 settings 命名空间,通过内容寻址方式同步到自托管服务端
dsh-sync-server。零运行时依赖(核心引擎纯 Node
标准库);DSH 接入层遵循本部署的 @deepseek-ai/* 包约定。
组成
src/
├── index.js # Cordis 插件(对象式导出):工具注册 + 周期/事件触发 + HTTP 桥接路由
├── client.js # 浏览器半部(clientModules):状态徽标 + 冲突面板
├── bridge.js # host→浏览器 JSON 桥(/dsh-sync/* 路由的纯逻辑,可测试)
├── engine.js # 3-way 同步引擎(扫描→差异→push/pull/冲突)+ 根适配器
├── scanner.js # 预设/技能树扫描与忽略规则(node_modules/.git/超限文件)
├── manifest.js # 本地持久状态(设备 ID、base 清单、上次同步结果)
├── transport.js # dsh-sync-server /v1 HTTP 客户端(重试/退避/超时)
├── credentials.js # 状态目录凭据文件(0600,CLI 与降级路径)
├── settings-adapter.js# settings 命名空间根适配器(JSON 快照 + replace 回写)
├── paths.js # DSH_HOME / 预设根 / 技能根 / 状态目录解析
└── cli.js # 独立 CLI:register / status / now
test/ # 64 个用例:单元 + fake server e2e + bridge + UI
cordis.patch.yml # bundle 补丁:向组合插入本插件行Web UI(M3,经 clientModules 交付)
package.json 声明 dsh.client = { platform: "web", inject: [...] },
exports["./client"] 指向 src/client.js;dsh-client-modules 服务扫描后将其作为
/plugins/dsh-sync-client/client.js 注入页面。
| 位置 | 内容 |
| --- | --- |
| sidebar.footer.action | 状态徽标:状态点(绿=已同步 / 橙=有冲突 / 红=错误 / 灰=未配置)+ 冲突数量角标;提供“同步”(双向)和“获取”(仅拉取)两个按钮 |
| settings.section(“数据同步”页) | 状态/设备/上次同步/最近结果/错误 + “配置”按钮(打开弹窗填写服务端地址和管理密钥,点击“保存”注册或切换连接)+ “同步”和“获取”按钮 + 冲突裁决 |
数据通道:浏览器同源 fetch → host 半部 ctx.webServer 注册的 JSON 路由:
| 路由 | 方法 | 说明 |
| --- | --- | --- |
| /dsh-sync/status | GET | 状态 + 冲突列表 + 动态配置能力 |
| /dsh-sync/setup | POST | 运行时注册并切换连接(body: { serverUrl, adminToken, deviceName? }) |
| /dsh-sync/sync | POST | 立即同步(body: { direction }) |
| /dsh-sync/conflicts | GET | 冲突列表 |
| /dsh-sync/resolve | POST | 裁决(body: { path, choice }) |
路由逻辑在 bridge.js(纯函数,返回 { ok, ... } 信封),由测试直接覆盖;UI 本身
只做渲染与轮询(30s)。同源信任模型与参考插件 dsh-plugin-deepseek-usage 一致
(仅本机 Web 界面可达)。
安装(M2 形态,M4 会做成正式发布流程)
打包:
npm pack(生成dsh-sync-client-0.1.7.tgz)。安装进
webprofile(把本包作为 bundle 插入组合,行内name走 node_modules 解析):dsh plugin --profile web add ./dsh-sync-client-0.1.7.tgz等价手工方式:把包放入
$DSH_HOME/profiles/web/node_modules/,并在$DSH_HOME/profiles/web/package.json的dependencies与dsh.profile.bundles中登记(桌面端还会同步.dsh-desktop-plugin-sync.json)。重启 DSH,行
dsh-sync-client生效:启动即跑一轮初始同步,之后按syncIntervalMs周期同步,并在settings/updated与预设目录下的fs/observed事件时防抖触发。
注册设备(三选一)
方式 A — 独立 CLI(推荐,无需把管理令牌交给模型):
# 在要同步的机器上执行一次:
node src/cli.js register --server https://sync.example.com --admin-token <管理员令牌> --name laptop-a
node src/cli.js now # 立即跑一轮同步
node src/cli.js status # 查看状态CLI 只同步
presets/与skills/文件根(无 DSH 运行时);settings 命名空间由 已装行的插件在进程内同步。CLI 注册或切换服务端时会自动重置 3-way base,与插件 行为一致。
方式 B — “数据同步”设置页(弹窗配置):点击“配置”按钮打开弹窗,填写服务端地址和
管理密钥,点击“保存”即可在运行时注册或切换服务端,不需要重启。管理密钥只用于本次注册
请求且保存后随即从表单清空;本地仅保存服务端签发的设备 token。弹窗支持“取消”、点击
遮罩或按 Esc 关闭。
方式 C — 会话内工具 dsh_sync_setup:传 serverUrl + adminToken(仅首次)注册
并存储凭据;之后所有 dsh_sync_* 工具可用。设备凭据同时写入 DSH credentials 服务的
grant record(dsh-sync/device)与状态目录文件(0600)。
行配置(cordis.patch.yml 或组合行 config)
| 键 | 默认 | 说明 |
| --- | --- | --- |
| serverUrl | '' | 尚未动态配置时使用的初始服务端地址 |
| settingsNamespaces | agent-default-model, ui-theme, agent-presets, llm-pi-ai | 非敏感 settings 命名空间白名单;内联 secret 会整项跳过 |
| presetRoot | 自动 | 覆盖预设根(默认取 agentPresets.roots 的 user 根或 $DSH_HOME/.agent-presets) |
| skillsRoot | $DSH_HOME/skills | 覆盖用户技能根 |
| syncIntervalMs | 300000 | 周期同步间隔(≥10s) |
| maxFileBytes | 1048576 | 单文件同步上限(与服务端一致) |
| syncCredentials | false | 选择开启:把凭据文档 $DSH_HOME/.credentials.yaml 一并同步(见下节;服务端明文存储) |
| disabled | false | 关闭插件 |
插件只挂载上表中的明确白名单根,不会递归扫描整个 dshdesktop。.credentials.yaml
默认不同步(可用 syncCredentials 开启,见下节);.dsh-sync/、sessions/、
storages/、profiles/、runtime/、npm-cache/、npm-cli/ 以及 dsh-desktop.db*
始终不进入同步;permission 设置可显式加入白名单,但默认不跨设备复制安全策略。
凭据同步(可选,默认关闭)
开启 syncCredentials: true 后,凭据文档 $DSH_HOME/.credentials.yaml
(credentials 服务托管 refs: 环境密钥与 records: 授权记录的文件)作为
credentials/credentials.yaml 参与同一套 3-way 同步。要点:
- 明文上传:服务端 blob 内容寻址、不加密。只在自建、TLS、自己控制的服务端上 开启(E2E 加密是 M5 计划,在此之前服务器能看到全部密钥)。
- 热生效:本地
credentials服务 watcher 会在文件被 pull 覆盖后自动热重载, 另一台机器的 API key 无需重启即可用;反之,本地通过模型页等改密钥会触发credentials/reference-updated/credentials/record-updated事件,防抖推送。 - 设备 token 隔离:开启后本插件的设备凭据只存
.dsh-sync/credentials.json(绝不同步),不再写入 credentials 服务 record——被同步的文档永远不会替换掉本机 用来认证的 token;旧 record 会被一次性迁移到状态目录文件。 - 冲突:文档是单文件整体对比,双边都改 → 冲突副本物化为
.credentials.yaml.conflict-<sha8>(同目录普通文件,credentials 服务的 watcher 只盯原文件名,不会误加载),人工合并后裁决 keep-local / keep-remote。 - CLI:
dsh-sync now --credentials在独立 CLI 轮次中包含该文件根。
工具(注册给 Agent)
| 工具 | 作用 |
| --- | --- |
| dsh_sync_status | 服务端/设备/最近同步/冲突/错误总览 |
| dsh_sync_now | 立即一轮同步(full/push/pull) |
| dsh_sync_conflicts | 列出当前冲突 |
| dsh_sync_resolve | 裁决:keep-local / keep-remote / keep-both |
| dsh_sync_setup | 配置服务端与设备凭据 |
同步语义(与 DESIGN.md §4/§7 一致)
- 3-way 合并:本地 / 远端 / 上次同步点(base);差异以内容哈希判定,与时钟无关。
- push:先上传 blob(内容寻址、幂等),再原子提交清单(含墓碑删除)。
- pull:按远端写回文件(临时文件 + 原子改名),并更新 base。
- 冲突(双边改动且内容不同):保守策略——本地保留,远端版本物化为确定性的
<path>.conflict-<sha8>副本(会作为新文件同步到所有机器),并向服务端日志上报conflict;base 不动,冲突持续到用dsh_sync_resolve裁决。keep-local:本地版本推送为远端(删除冗余副本)。keep-remote:远端覆盖本地,并清理conflict-*副本。keep-both:本地另存<path>.local-<sha8>,远端占位原路径。
- settings 命名空间:以
settings/<ns>伪文件参与同一套 3-way;写回走settings.replace(保留服务权威性并触发其自身的变更事件);不支持远端删除命名空间。 扫描和写回都会拒绝含内联apiKey/token/secret/password等字段的命名空间;apiKeyEnv这类环境变量引用可以同步。命名空间冲突不会物化.conflict-*副本 (伪文件无处安放,只上报冲突等待裁决),keep-both对 settings 根不可用,会返回 明确错误——请用keep-local/keep-remote裁决。 - 状态文件容错:
state.json损坏或版本不符时不会让插件行装载失败——旧文件被 隔离为state.json.corrupt-<ts>,base 重置为空(后续轮次保守收敛),错误写入 状态供 UI/doctor 展示。 - 离线/失败:捕获错误写入状态(不破坏 base),指数退避自动重试(30s→30min)。
测试
node --test # 64 个用例:scanner/manifest/transport/credentials/settings 单元 +
# engine 全场景(push/pull/tombstone/冲突三类解析/settings 冲突/凭据文档/忽略/离线)+
# bridge(状态/同步/冲突/裁决信封/请求体上限)+
# e2e 双设备全流程(fake server 实现 Go 端 /v1 契约)+
# client-ui(设置页渲染/弹窗/裁决错误展示)与 M1 服务端的端到端验证
# 终端 1:起真实 Go 服务端
ADMIN_TOKEN=$(openssl rand -hex 32) DSH_SYNC_DATA_DIR=./data PORT=8080 go run ../server/cmd/api
# 终端 2:注册并同步
export DSH_HOME=$(mktemp -d) # 隔离的 DSH 家目录
node src/cli.js register --server http://127.0.0.1:8080 --admin-token <token> --name laptop-a
mkdir -p "$DSH_HOME/.agent-presets/my-preset"
echo 'name: my-preset' > "$DSH_HOME/.agent-presets/my-preset/agent.cordis.yml"
node src/cli.js now