dsh-git-sync 设计文档

August 16, 2026 · View on GitHub

用 git 实现 DSH 多端同步:工作区对话/文件双向同步、预设与插件清单同步。 复用系统 git 凭据(SSH key / Windows 凭据管理器 / PAT),不存储任何密码

1. 目标与场景

工作区类型例子同步方向同步内容
本机强依赖(IAR 编译等)<WORKSPACE_B>单向(本机 push,他机查看)仅对话(mode: conversations
纯开发(插件/预设生产)<WORKSPACE_A>双向续接全部文件(mode: all

核心事实(已从 DSH rc.6 源码验证):

  • 一个会话 = ~/.dsh/sessions/<projectKey(cwd)>/session-<uuid>/session.jsonl.zstd(zstd 多帧)
  • 会话按工作区(header 里的 cwd 绝对路径)分目录;目录名 projectKey(cwd) = 分隔符(\/://)合并为 -、非安全字符转 ~XXXX-- 包裹
  • DSH 启动时扫描 sessions 目录自动发现/收养未注册会话(dsh-workspace.bootstrap),无需改 workspace.json
  • workspace.json / session_projcache.json 是派生索引,不必同步
  • 会话 header 硬编码 cwd,跨设备同会话续接要求路径一致;路径不同则旧会话以"虚拟工作区"只读可查

2. 插件架构

dsh-git-sync/
├── package.json        # dsh-plugin 元数据(bundles 里注册)
├── docs/design.md      # 本文档
├── lib/index.js        # host 半部:git 服务 + 配置 + /dsh-git-sync/* 路由
├── lib/client.js       # client 半部:settings.plugin.item 卡片(+ 阶段2 快捷入口)
└── README.md

2.1 配置存储

全局配置文件 ~/.dsh/.git-sync.yaml(与 .credentials.yaml 同级,host 读写;不含密钥):

# 绑定(中央配置仓库:预设/插件清单,阶段3 启用)
bind:
  centralRepo: ""        # 例 git@github.com:<ACCOUNT>/dsh-dotfiles.git
  centralBranch: main
# 每个工作区(key = 工作区绝对路径;跨设备由逻辑名 `name` 关联镜像目录)
workspaces:
  "<WORKSPACE_A_ABS_PATH>":
    enabled: true
    mode: all            # conversations | all
    remote: git@github.com:<ACCOUNT>/<WORKSPACE_A_REPO>.git
    branch: main
  "<WORKSPACE_B_ABS_PATH>":
    enabled: true
    mode: conversations
    remote: git@github.com:<ACCOUNT>/<WORKSPACE_B_REPO>.git
    branch: main

配置项说明:

  • mode: conversations:只同步 ~/.dsh/sessions/<projectKey(cwd)>/ 到仓库 .dsh-sync/不碰工作区文件
  • mode: all:同步工作区全部文件(git add -A
  • remote 为空 = 未绑定,卡片显示"未配置"
  • 凭据:系统 git 凭据(git push/pull 自动走),可选 PAT 配到系统凭据管理器,插件不存

2.2 仓库内约定目录

<workspace>/
├── .dsh-sync/                      # 仅 conversations 模式使用
│   └── <projectKey(cwd)>/          # 目录名 = 会话归属(跨设备原样拷贝自洽)
│       └── session-<uuid>/session.jsonl.zstd
├── (工作区文件 …)
└── .git/
  • .dsh-sync/ 进 git(.gitignore 不得排除)
  • 会话文件是 zstd 压缩的追加日志,增量提交即可

3. host 半部(lib/index.js)

3.1 依赖注入

inject = ['webServer', 'subprocess', 'workspaceRegistry', 'sessionPersistence']
  • ctx.webServer.register({kind:'prefix', path:'/dsh-git-sync', handler})
  • ctx.subprocess.spawn(spec) 跑 git(复用 layout 插件的 subprocessRunner 模式)
  • ctx.workspaceRegistry.list() → 工作区记录(path/title/sessionIds
  • ctx.sessionPersistence.list() → 所有持久化会话 header(id/cwd

3.2 安全边界(gate)

复用 layout 插件的 workspace gate:/dsh-git-sync/*{path} 必须 realpath 后落在注册工作区内;remote/branch 字符串做白名单校验(禁 - 开头、空白、..)。配置读写路由允许任意工作区 path(从 workspaceRegistry 校验存在)。

3.3 路由

路由入参返回说明
GET 不支持;全 POST与 layout 一致
/dsh-git-sync/config{}完整配置~/.dsh/.git-sync.yaml
/dsh-git-sync/config{workspacePath, patch}更新后的配置写单个工作区方案或 bind
/dsh-git-sync/workspaces{}[{path,title,mode,remote,enabled, gitRoot, branch, localChanged, remoteAhead}]工作区列表 + 同步状态
/dsh-git-sync/status{path}{gitRoot,branch,changed,sessions:[{id,size,updatedAt}],remoteAhead,remoteBehind}本地/远程差异
/dsh-git-sync/push{path, message?}{pushed, sessions, files}上传(见 3.4)
/dsh-git-sync/pull{path}{pulled, sessions, files, requiresRestart}拉取(见 3.5)

3.4 push 流程

  1. gate → canonical(工作区根,realpath)
  2. 读配置:mode/remote/branchremote 空 → 报"未配置"
  3. 确保 canonical 本身是 git 仓库(无 .gitgit init严格本层,不向上找)
  4. mode === 'conversations'
    • ctx.sessionPersistence.list() → 按 header.cwd 过滤出 cwd === canonical 的会话 id
    • 复制 ~/.dsh/sessions/<projectKey(cwd)>/session-<id>/session.jsonl.zstd<canonical>/.dsh-sync/<projectKey(cwd)>/session-<id>/(缺失才写)
    • git add .dsh-sync + git commit -m "dsh-sync: N sessions" + git push
  5. mode === 'all'git add -A + commit + push
  6. 返回统计

3.5 pull 流程

  1. gate + 配置
  2. git pull(未绑定 upstream 时用 git pull <remote> <branch>
  3. mode === 'conversations'
    • <canonical>/.dsh-sync/<projectKey(cwd)>/ 枚举会话
    • 复制缺失的~/.dsh/sessions/<projectKey(cwd)>/不覆盖已存在文件——防覆盖本机正在写的会话)
    • requiresRestart: true(DSH 需重启才扫描发现新会话)
  4. mode === 'all':直接 git pull 合入工作区文件
  5. 返回统计

3.6 编码工具

projectKey(cwd) 从 DSH 源码复制(行为必须一致):

function projectKey(cwd) {
  let readable = "";
  let separatorRun = false;
  for (let i = 0; i < cwd.length; i++) {
    const code = cwd.charCodeAt(i);
    const ch = String.fromCharCode(code);
    if (ch === "/" || ch === "\\" || ch === ":") {
      if (!separatorRun) readable += "-";
      separatorRun = true;
    } else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
      readable += ch; separatorRun = false;
    } else {
      readable += "~" + code.toString(16).toUpperCase().padStart(4, "0"); separatorRun = false;
    }
  }
  return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
}

sessionsRoot = join(DSH_HOME, 'sessions')DSH_HOME = ~/.dsh,兼容 $DSH_HOME)。

4. client 半部(lib/client.js)

4.1 设置卡片(MVP 主入口)

注册 settings.plugin.itemkind:'list'scope:'root'apply 顶层注册,与 layout 插件一致):

┌─ Git 同步 ────────────────────────────┐
│ 绑定中央仓库: [URL] [分支] (阶段3)      │
│ 工作区                    状态    模式 │
│ <WORKSPACE_A>     ✓同步    [全部文件▼] │
│   [推送] [拉取] [状态]                │
│ <WORKSPACE_B>     ⚠远程新  [仅对话▼]   │
│   [推送] [拉取] [状态]                │
└──────────────────────────────────────┘
  • 工作区列表来自 /dsh-git-sync/workspaces
  • 每个工作区:mode 下拉(仅对话/全部文件)+ remote 输入 + 推送/拉取按钮 + 状态徽标
  • 状态徽标:未配置(无 remote)/ ✓ 同步 / ↑ 本地有新 / ↓ 远程有新 / ⚠ 冲突或错误
  • 操作结果用 Toast 提示;pull 后提示"重启 DSH 后新会话出现"
  • 配置保存走 /dsh-git-sync/config

4.2 快捷入口(阶段2,可选)

sidebar.footer.actionkind:'list')注册图标按钮;有远程更新时亮徽标,点击打开设置卡片同款面板(用官方 Modal)。

5. 中央配置仓库(阶段3)

bind.centralRepo 指向私有仓库,内容为声明文件(非安装产物):

  • presets.yml[{id, repo, revision}] → 各设备 git pull 预设仓库
  • plugins.yml[{id, source: github:...}] → 各设备 dsh plugin add 清单
  • 启动时 git pull 中央仓库 → 对比 revision → 提示"预设/插件有更新"

禁止同步 ~/.dsh/profiles/(pnpm 安装产物)与 .credentials.yaml

6. 风险与约束

  1. 私有仓库:对话含敏感内容,必须私有
  2. 并发:同一会话两端同时写会 git 冲突;conversations 模式靠"只补缺失"规避大部分;同一时刻仅一台续接
  3. 路径一致性:同会话续接要求设备间工作区路径一致;否则新设备=新起点、旧会话只读
  4. 二进制进 git:会话文件无 diff,但 zstd 压缩后很小,增量提交可接受
  5. 版本脆弱性projectKey 编码/sessions 目录布局随 DSH 升级可能变(rc 阶段),升级后需回归验证

7. 分阶段计划

阶段内容
MVP(本轮)host 全路由 + 设置卡片 + conversations/all 模式 push/pull/status + 配置持久化
阶段2sidebar.footer.action 快捷入口 + 远程更新徽标 + 启动时检查
阶段3中央配置仓库 + 预设/插件清单自动更新提示