- Startseite
- Plugins
- Sitzungen & Nachrichten
- dsh-github-sync
dsh-github-sync
lifeng4127/dsh-github-sync
Sync DeepSeek Harness sessions and workspace files to a private GitHub repository for cross-device handoff, with sync badges, search and bulk upload, independent local/remote delete, conflict "pick a side", and multi-device-safe index merging.
Installation
dsh plugin --profile web add github:lifeng4127/dsh-github-syncREADME
dsh-github-sync
把本地部署的 DeepSeek Harness 会话同步到你自己的私有 GitHub 仓,用来在两台(或更多)电脑之间"接力"——在 A 机聊到一半的任务,B 机下载下来能接着聊。
它补的是 DSH 本地版没有云端存储这个缺口:没有服务器、没有账号体系,只有你的仓库和一串令牌。
它做什么
| 能力 | 说明 |
|---|---|
| 上传会话 | 勾选本地会话推上去;连带它引用到的图片等附件;一批文件 = 一个原子提交 |
| 下载会话 | 从远端拉回来,改写 cwd 到本机工作区,并自动注册进项目列表——不用重启就能看到、能继续聊 |
| 上传文件夹 | 默认关闭,设置里开启后可用:选中工作区下的任意子目录,按排除清单过滤后推到 <项目>/files/ |
| 下载文件 | 默认关闭,设置里开启后可用:把远端文件写回工作区(同名冲突另存副本,路径做了防穿越) |
| 给 agent 的 3 个工具 | 你说"把这条会话推到 GitHub",它先出清单、你确认后再动手 |
同步是纯手动的:不点不动,没有后台定时任务,不会偷偷传东西。
- 传输有进度:文件级进度条(
12/30 · 上传文件 · f12.bin),覆盖预览阶段(读取会话/拉取远端)与提交阶段(读远端 → 上传文件 → 建目录树 → 写提交)。 - 可展开看明细:点进度条左边的
▸展开文件列表,逐个显示等待 / 传输中 / 完成 / 已跳过。 - 单文件可跳过:明细里每个还没开始传的文件都有「跳过」按钮——跳过附件会自动从
manifest.json里摘掉对应引用,不会留下悬空记录。 - 随时可取消:取消在文件之间生效,
ref不会被更新,所以仓库里不会留下半个提交。 - 切屏不丢:传输状态存在宿主进程里,面板切走再回来会自动重新接上进度条;没确认的预览也会恢复出来。
- 面板状态会保留:工作区选择、勾选框、开关都记在浏览器本地,切走再回来(甚至刷新页面)都还在。
安装
从插件市场安装:在 DSH 的「插件」里搜索 dsh-github-sync,安装后启用即可。
从本地目录安装(开发/自用):
- 打开 DSH →「插件」→ 添加插件 → 本地目录,选这个文件夹
- 或者由 agent 调用
plugin_manager install_bundle,target填绝对路径 - 或者在 profile 的
package.json里加"dsh-github-sync": "link:<路径>",并把它加进dsh.profile.bundles
装完左侧边栏会出现「GitHub 同步」图标,点开是面板。
大陆网络:如果 npm 官方源很慢,用镜像装:
npm install dsh-github-sync --registry=https://registry.npmmirror.com(镜像同步有几分钟到几小时延迟,刚发版时可能还没有)
客户端改动后按 F5 刷新页面即可生效;宿主代码改动需要重载插件(重启 DSH,或在插件管理器里关掉再启用)。
首次配置
📖 需要更细的图文步骤(含截图位置、常见错误对照、令牌轮换)?看 配置教程 TUTORIAL.md。
1. 建一个空的私有仓
GitHub 新建仓库,不要勾 README、不要勾 .gitignore、选 Private。 空仓库很重要:首次提交若已有文件会多一次合并。
2. 建 fine-grained PAT
https://github.com/settings/tokens?type=beta → Generate new token:
| 字段 | 值 |
|---|---|
| Repository access | Only select repositories → 只勾这一个仓 |
| Contents | Read and write |
| Metadata | Read-only(勾 Contents 后自动带上) |
| 其它权限 | 全部 No access |
令牌只显示一次,复制后立刻填进面板。
3. 面板里填
右上角 ⚙ → 仓库地址(owner/repo 或完整 URL 都行)+ 访问令牌 → 测试连接。
- 成功:
连接正常 · owner/repo - 报「仓库是公开的」→ 去仓库 Settings 改成 Private(这是硬红线)
401→ 令牌没粘全;404→ 仓库名错或令牌没授权到这个仓
日常使用
面板(推荐)
┌ 设备 / 仓库 / 分支 / 当前工作区 ──────────── ● 已连接 ┐
├──────────────────────┬──────────────────────────────┤
│ 远端仓库 │ 本地会话(工作区下拉) │
│ 💬 会话(可勾选) │ ☑ 会话… 大小 时间 │
│ 📄 文件(可勾选) │ │
├──────────────────────┴──────────────────────────────┤
│ 已选:本地 N · 远端会话 M · 远端文件 K │
│ [预览上传] [文件夹…] [下载会话] [下载文件] │
└──────────────────────────────────────────────────────┘
所有写操作都必须先预览:预览会逐个文件标注 upload / skip / blocked / copy 与原因,确认后才真正执行。被 blocked 远端更新 挡住的行,行尾直接有「先下载」按钮。
一眼看出状态
每条会话右侧有同步徽标,由本地 body 哈希和远端索引对比得出:
| 徽标 | 含义 |
|---|---|
| 已同步 | 两边内容一致 |
| 本地更新 | 本地多聊了几轮,可以推 |
| 远端更新 | 远端更新,应先下载 |
| 未上传 | 还没传过 |
| 活跃 | 最近 5 分钟内还在写,推上去的是快照 |
配合「搜索」「只看未同步」「选未同步」「同步此工作区(N)」,「哪些没传、要不要传」不用再靠记忆。
设备与历史
- 顶部「设备」:远端索引里记录的各设备与最近活动时间,本机会标出来。
- 顶部「历史」:最近的上传/下载记录(文件数、commit、失败数),落盘在
$DSH_HOME/github-sync/history.jsonl。
删除:两边互不影响
- 本地行尾的 🗑:只删本机会话,GitHub 仓库原样保留;顺带回收不再被任何会话引用的本地附件。
- 远端行尾的 🗑:只删仓库里的会话,本机原样保留;同时删掉没有其他会话引用的远端附件。
- 两边都是「点两次确认」(4 秒内有效),避免误删。
冲突怎么办
两端都动过同一条会话时,预览里会标 blocked 并给出选择:
| 按钮 | 含义 |
|---|---|
| 先下载 | 先把远端版本拉下来,再决定 |
| 用本地覆盖远端 / 用远端覆盖本地 | 明确选一边,直接执行并重新预览 |
仓库会越变越大,怎么回收
每次提交都会在 git 历史里留下新的 blob,GitHub 不会自动清理。几个建议:
- 会话在仓库里是固定路径(
<项目>/sessions/<id>.jsonl.zstd),所以历史里的旧版本只是占空间,不影响使用; - 想真正回收:
git clone --mirror后用git filter-repo重写历史,或在 GitHub 上重建一个空仓再推一次; - 如果主要用途是接力,一个项目一个仓比所有项目混在一起更容易清理;
- 面板的「诊断」里能看到当前远端会话/附件数量,便于判断膨胀程度。
第一次配置
没连上仓库时,面板顶部会出现引导条,点「开始配置」进入三步向导:建空私有仓 → 建 fine-grained token → 填入并测试连接,每步都带官方页面链接。
直接对我说
- "把这条会话推到 GitHub"
- "远端都有哪些会话"
- "从远端把某某会话拉到这个工作区"
我会调用 github_sync_transfer:先 phase=preview 给你清单,再 phase=execute 带同一个 token 执行。
仓库长什么样
README.md 自动生成的项目/会话索引(每次上传重写)
manifest.json 权威索引:会话 id → 路径、原始 cwd、sha、附件列表
<项目>/sessions/<id>.jsonl.zstd 会话文件本体(跟 DSH 本地格式一致)
<项目>/attachments/<sha>.<ext> 会话引用到的附件(图片/PDF 等)
<项目>/files/<相对路径> 文件夹上传的内容
manifest.json 里保存了原始 cwd,这是跨机还原的关键:下载时它会被改写成目标机器的工作区路径。
关键实现(为什么这么设计)
- 会话文件 = JSONL + zstd 多帧,首帧是 header(含绝对路径
cwd),之后每批追加各成一帧。 - 路径即身份:宿主会校验"文件实际路径 == 由 header.cwd 推出的期望路径",不等就报 corrupt。所以跨机必须同时改 header 的
cwd和目录名。 - 只重压头帧:改写
cwd时只重压第一帧,后面的帧字节原样保留——否则会破坏 append-only 字节链,"前缀合并"永远失效。 - 冲突只比 body:头帧的
cwd本来就该因机器而异,所以同步状态与冲突判定都基于"头帧之后的字节"。判定规则:一方是另一方的前缀 → 落后的一方补上;互不为前缀 → 分叉,本地保留、远端另存*.remote-fork-*。 - 注册用服务:下载后通过
workspaceRegistry.attachSession()登记,不直接改workspace.json(那是宿主的活,直改会被内存覆盖)。 - 空仓库要打种子:零提交的仓库连
POST /git/blobs都返回 409,首个提交必须用 Contents API。 - 附件补传:即使会话内容没变(判定
skip),只要它引用的附件还没在远端登记,上传时也会把附件单独补上去——旧会话不用重传本体。 - 附件还原兜底:下载时除了看
manifest登记的附件,还会直接解析会话本体里引用的sha256:;旧manifest没登记时按<项目>/attachments/<sha>.*在远端树里找。 - 零网络的前缀判定:
manifest记着每次上传的 body 长度与哈希,本地用切片比对就能判断"是不是只多追加了几帧",不必下载远端文件(一条 2MB 会话的预览从 134 秒降到 5 秒)。
安全
只写这四个位置
| 位置 | 用途 |
|---|---|
$DSH_HOME/github-sync/ | 插件自己的状态、历史、诊断报告 |
$DSH_HOME/sessions/ | 下载或删除会话(点按钮才动作) |
$DSH_HOME/attachments/v1/objects/ | 还原或回收附件 |
| 当前工作区(默认关闭) | 文件夹/文件同步,需在设置里显式打开 |
它不会做的事
不启动子进程、不执行外部命令、不读写注册表、不改 hosts、不改系统环境变量、不安装全局代理、不访问 GitHub 以外的域名。这些不变量由 tools/audit-safety.mjs 静态检查,发布前必跑。
其他边界
- 默认只同步对话。文件夹/文件同步是默认关闭的高级功能,未开启时相关接口直接返回 403。
- 只允许私有仓,公开仓直接拒绝。
- 密钥类文件硬阻断:
.env*、*.key、*.pem、id_rsa、.npmrc、.netrc,二次确认也传不上去。 - 路径越界硬拒绝:远端索引里的会话 id / 附件 sha 会先校验再拼路径,仓库被投毒也写不出
$DSH_HOME。 - token 只在宿主进程:存在
$DSH_HOME/.credentials.yaml(平台机制),面板永远不回显。 - 删除是两边独立的:删本地不动仓库,删仓库不动本地。
跨平台(Windows / macOS)
插件是纯 JavaScript,没有原生模块、没有平台专属代码,两个系统跑同一份代码。
唯一的平台差异是"会话目录名怎么从路径算出来"。为此:
lib/projectkey.mjs按 DSH 的规则把 cwd 编码成目录名(Windows 的E:\a\b和 macOS 的/Users/x/b都能编码);lib/sessiondir.mjs在算出来的目录不存在时,会退回读取会话头里记录的绝对 cwd 来匹配目录——所以即使某个平台的键算法有出入,也不会出现"面板里一条会话都没有"。
跨机接力时,下载会自动把会话 header 里的 cwd 改成目标工作区路径(只重压头帧,其余字节原样保留),所以 Windows 上的会话能被 macOS 直接打开,反之亦然。
发布前的双平台验证清单见 PUBLISHING.md。
排错
| 现象 | 原因 / 处理 |
|---|---|
连接失败 · fetch failed | 宿主到 api.github.com 不通(代理/防火墙/DNS)。先确认 HTTPS_PROXY 生效,再重试 |
UNABLE_TO_VERIFY_LEAF_SIGNATURE / SELF_SIGNED_CERT_IN_CHAIN | 本机有软件在解密 HTTPS(SteamTools/Watt Toolkit、Fiddler、安全软件的网页防护)。把 api.github.com 加进它的绕过列表,或按 TUTORIAL 证书错误 处理 |
Git Repository is empty 409 | 空仓库,首提交走 Contents API;若反复出现,先在网页建一个 README |
its cwd resolves to '...' | 同一会话 id 已属于别的工作区,不能重复挂载。这是宿主在保护你 |
预览里大量 blocked | 命中密钥规则或超过 10MB;超限可改 limits |
| 下载后侧栏没有 | 面板刷新即可;若仍未出现,检查目标工作区是否是同一个 |
同机删掉再下载,its cwd resolves to '...' | 宿主对会话 header 有内存缓存,还记着旧 cwd。重启 DSH 后即可挂载;真实跨机不会遇到(B 机没见过这个 id) |
| macOS 上面板列出 0 条会话 | 该平台的会话目录名算法与插件不一致,且回退扫描没命中。点「诊断」→ 复制,把里面的 平台、会话目录(应有)、sessions 根下 N 个目录 三行发出来即可定位 |
已知限制
- 附件按会话引用上传,未被任何会话引用的附件不会上传(也就不占仓库)。单个附件超过 25MB 会跳过并在预览里告警。
- 本地缺失的附件不会阻断上传,但预览里会用 ⚠ 标出来。
- 只处理
version: 4的会话格式;其它版本拒绝改写而不是猜。 - 文件下载是"写到工作区根目录 + 同名另存副本",没有三方合并。
- 目前按 Windows 路径设计(盘符、反斜杠)。
开发
dsh-github-sync/
├─ index.js 宿主:HTTP 路由 + 3 个工具 + 存储
├─ client.js 浏览器:左栏面板(手写 JS,不经打包)
├─ lib/
│ ├─ zstdframe.mjs zstd 多帧遍历(自己走帧头)
│ ├─ sessionlog.mjs 读 header/标题、只重压头帧、body 前缀比对
│ ├─ projectkey.mjs cwd → 目录键
│ ├─ github.mjs Git Data API 客户端(空仓种子、非快进重试)
│ ├─ sync.mjs 上传/下载计划(纯逻辑 + 只读远端)
│ ├─ folders.mjs 文件夹扫描 + 排除/硬阻断
│ ├─ manifest.mjs 远端索引与 README 生成
│ ├─ localsessions.mjs 本地会话清单(标题优先读宿主投影缓存)
│ └─ paths.mjs DSH 目录约定
└─ tools/ 自测脚本(selftest / test-upload-plan / verify-remap)
自测:
node tools/selftest.mjs # 内核 24 项
node tools/test-upload-plan.mjs # 上传计划(假 client,不联网)
卸载
插件管理器里移除即可。数据在:
$DSH_HOME/github-sync/state.json(配置与设备 id)$DSH_HOME/.credentials.yaml里的GITHUB_TOKEN- 远端仓库(自行删除)
本地的会话与工作区不受影响——这个插件从不删本地数据。
Ähnliche Plugins
dsh-web-ui (dsh-chat-recovery)
zhu1090093659/dsh-web-ui
billion-context
ranxianglei/billion-context
dsh-synapse
liangmianya/dsh-synapse
dsh-archive-manager
michengai/dsh-archive-manager