deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
一个 100% 开源的 DeepSeek Harness(dsh)插件 bundle,借鉴 opencode / dsh-turn-rewind 的 OpenCode 式架构:为非 git 场景实现内容寻址的文件快照台账,并通过 DSH 官方 sessions.fork 重开一个新会话来回退对话——恢复文件 + 从选中的那一轮输入重新续起。
sessions.create / sessions.fork 新建一个会话(从选中的这一轮输入之后继续),并把这条输入的原文填回新会话的输入框,方便你改完重新发送。原会话会自动归档(从会话列表消失;DSH 没有"删除单个会话"的 RPC,归档是官方机制——数据与历史仍在磁盘上,新会话继续可回退)。$DSH_HOME/rewind-ledger 台账目录(内容寻址的 blob + per-turn manifest 都放在这里;rewind 的第一版原型数据与之无关,已废弃);回退时把工作区恢复到那个 turn 的快照。支持新增/删除/修改/权限/文本与二进制。为什么是"重开新会话"而不是"原地折叠":DSH 的会话日志是只读追加、不可物理截断;而且 DSH 浏览器端不会根据第三方插件追加的普通
user/message+surfaceOp.replace折叠对话。所以采取与 turn-rewind 同款的做法——恢复文件后 fork 一个新会话(原会话归档、从列表隐藏)。浏览器打开新会话时天然只显示到选中的那一轮为止,可靠且不需要 DSH 内部支持第三方 surface 折叠。
要求 dsh 与 pnpm 在 PATH 上。
REM 说明:%USERPROFILE% 是当前用户的 home(等价于 PowerShell 的 $env:USERPROFILE)。
REM 以下命令在 cmd 与 PowerShell 中均可运行(PowerShell 请把 %USERPROFILE% 换成 $env:USERPROFILE)。
# 1) 克隆
cd %USERPROFILE%
git clone https://github.com/2501136589/dsh-rewind.git
cd dsh-rewind
# 2) 安装进 web profile
# 推荐(无空格路径):
dsh plugin --profile web add .
# 若路径含空格导致相对锚定解析出错,则进 profile 目录直接 pnpm:
cd %USERPROFILE%\.dsh\profiles\web
pnpm add "link:<dsh-rewind 的绝对路径>"
无论哪种方式,最后都要让 @xiaoxin/dsh-rewind 出现在 web profile 的 package.json 里:
"dsh": { "profile": { "bundles": [ "@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "@xiaoxin/dsh-rewind" ] } },
"dependencies": { "@xiaoxin/dsh-rewind": "link:<路径>" }
注意:bundle 行必须用
- insert:包裹在 bundle 的cordis.patch.yml里(本仓库已配好),否则 loader 会把它当“按 id 覆盖已有 entry”而静默忽略,导致按钮不出现。
然后完全停掉再启动 dsh web(装载器与浏览器客户端 bundle 在新进程里才注册),再 Ctrl+F5 强刷。成功后,每条已完成的助手消息旁会出现一个纯图标回退按钮。
发布到 npm 后,可用 dsh plugin --profile web add @xiaoxin/dsh-rewind 直接安装 package 名。
declares no dsh.bundle in its package.json:dsh plugin 没把这个包识别为 bundle。确认 package.json 里有 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }。client bundle not found:确认 exports["./client"] 指向真实存在的 lib/client.js。dsh.profile.bundles 里有 @xiaoxin/dsh-rewind;再确认 cordis.patch.yml 用的是 - insert: 包裹 loader 行(本仓库已配好)。然后完全停掉再启动 dsh web(不是只刷新页面),再 Ctrl+F5。dsh-rewind/
├─ package.json # bundle 清单:dsh.bundle.patch + dsh.client(web)
├─ cordis.patch.yml # 装载 host 插件的 loader 行(必须用 - insert:)
├─ lib/
│ ├─ index.js # host 侧入口:装配快照采集、/rewind 命令与 HTTP 路由
│ ├─ snapshot.js # 非 git 工作区遍历 + sha256 内容寻址快照/差异
│ ├─ ledger.js # 内容寻址 blob/manifest 台账($DSH_HOME/rewind-ledger)
│ ├─ host-rewind.js # turn 采集、消息→fork 边界解析、恢复、HTTP handler
│ ├─ client.js # 浏览器侧:用户消息回退按钮 + 重开会话
│ └─ types/*.d.ts # 类型声明
├─ test/ # smoke 测试(node 直跑,无外部依赖)
├─ LICENSE # MIT
└─ README.md
一个包同时承载 host 逻辑(dsh.bundle.patch → cordis.patch.yml)和浏览器 UI(dsh.client.platform:"web" + exports["./client"])。dsh-client-modules 会扫描 loader 行里的包,发现 dsh.client.* 声明后把 lib/client.js 喂给浏览器。host 侧不依赖 git:快照靠 snapshot.js 的目录遍历 + ledger.js 的内容寻址存储。
sessions.fork/create 新建一个会话(从选中那一轮之后继续),并把这条输入的原文填回新会话输入框。cordis.patch.yml 里设 archiveOriginalSession: false 保留原会话)。/rewind <messageSeq|messageId> [rewind|list-files|dry-run]
messageSeq —— 目标用户消息的 seq;也可传 messageId(会按 user/message 的 data.message.id 解析)。必须是某轮的开场用户消息。rewind(恢复文件 + fork 新会话);list-files / dry-run 只预览不执行。/rewind list(或直接 /rewind):列出本会话所有可回退的用户消息 —— 每条显示 seq | turn | 是否有快照 | 消息预览,照着抄 seq 即可。user/message、turn/start、tool/call 等所有事件共用一条递增计数器,所以用户消息的 seq 往往不连续(例如 1, 47, 92…)。/rewind list 会直接给出可用值;浏览器按钮弹窗里显示的也是这套 seq。sessions.fork/create 重开新会话。两者各自可靠,不需要 DSH 内部支持第三方表面折叠。apiProxy.sessions.fork({ sessionId, atSeq: 选中轮之前的 turn/end })(首轮则 sessions.create({ cwd }))新建一个会话,其历史天然只到选中的那一轮为止;然后调用 apiProxy.workspace.archiveSession 归档原会话(默认,可在 cordis.patch.yml 关掉)——DSH 没有"删除单个会话"的 RPC,归档会把原会话从侧栏列表和搜索中排除,但它的数据与台账快照仍在磁盘上,分叉后的新会话继续回退不受影响。浏览器打开新会话即看到折叠后的效果。agent/pre-step 且 step===1)前,对工作区做一次快照(哈希成 sha256 内容寻址树存进台账)。这是 opencode 式"step 前打 baseline",而不是每条用户消息/每次写入。$DSH_HOME/rewind-ledger/ 下按工作区 sha256 分目录。支持新增/删除/修改/权限/文本与二进制;超大或超总量会明确失败,不静默漏。header.seedLength。在这些"拷贝来的消息"上回退时,插件沿 header.parentSession 血缘链向上找到真正录制这些快照的会话,用它的台账(checkpointOwnerFor)。所以 A→回退→A2,在 A2 里继续回退 A 的消息、甚至回退 A2 自己续写的新轮次,都成立;消息在血缘链上每一层的 seedLength 之内,就继续向上找。write/edit 一族,tool/call)∪ 该会话连续快照之间的漂移(覆盖 shell 副作用)。所以 B 会话回退时,A 会话后来改的文件不会出现在预览里、也不会被还原。会话事件完全取不到时才回退到"全量差异"(scope=workspace,旧行为)。lib/index.js + host-rewind.js + snapshot.js + ledger.js)TurnCheckpointCoordinator 用PREPENDED 的 agent/pre-step 监听器,在 step===1 时对 session.header.cwd 做一次快照(walkWorkspace 递归遍历,跳过 .git/node_modules/台账目录;snapshot.js 哈希成 sha256 树),并把内容寻址 blob + per-turn manifest 写入 ledger.js 的 LedgerStore($DSH_HOME/rewind-ledger)。绝不阻塞/否决 agent。/rewind <messageSeq|messageId> 命令 + 同名 /rewind HTTP 路由。messageTargetFor 把目标用户消息解析为 { turn, turnStartSeq, previousTurnEndSeq }(必须是该轮的开场用户消息);diffTrees(快照entries, 当前树) 得到改动文件预览;engine.applyRestoreFiles 把工作区恢复到快照(删新增/恢复修改/找回删除,路径做越界防护);createConversationRestart 调 apiProxy.sessions.fork(有 previousTurnEndSeq 时)或 sessions.create(首轮;先经 workspace.list 解析源会话工作区、把 workspaceId 带进载荷,避免生成"无工作区"的新会话)得到新会话 id。lib/client.js)apply(ctx) 注入 conversation.session.header.actions 槽,用一个 session 作用域组件经过 React portal 把回退按钮放进每条用户消息的 action 行(data-chat-flow-kind="user" + data-chat-anchor-key + data-time-hover-root)。GET /rewind?sessionId=..&messageSeq=.. 预览文件改动 → 确认后 POST /rewind 恢复文件并 fork 新会话。openSessionWithDraft: ctx.sessions.open(新会话id) + 在该会话 scope 上 ctx.conversation.input.for(scope).setDraft(原文),把这条输入原文填回新会话输入框。sessions.fork + 同源 HTTP,而不是 session.command + 表面替换之前版本想走 session.command + surfaceOp.replace 原地折叠,但 DSH 浏览器端不渲染第三方 surface-replace,聊天永远不动。改用官方的 sessions.fork(新建会话)是平台原生支持、必定在浏览器端正确显示历史截断的方式;同源 HTTP 路由让浏览器能预览恢复前的文件改动,无需第三方 Remote 命名空间。
archiveOriginalSession: false 关闭)。DSH 目前没有"删除单个会话"的 RPC,也没有在 UI 里取消归档的入口——归档后原会话从侧栏/搜索消失,但其数据仍在磁盘上(~/.dsh/sessions/...)。需要"物理删除"时,那要 DSH 平台支持第三方删除会话,超出本插件(也是一切第三方 bundle 的边界)。sessions.create。首轮回退会先通过 workspace.list 找到源会话所在的工作区,把新会话挂进同一工作区——不会出现"没有工作区"的新会话。walkWorkspace 跳过 .git、node_modules 和台账目录,不回退这些内容;超大文件/超总量会明确失败而不是静默漏。dsh-turn-rewind 补齐这些护栏(它已含完整的 Change Ledger)。scripts/repair-ledger.mjs)内容寻址台账是只增不减的:采集绝不删除 blob(避免"UI 还列着某轮、blob 已被 GC"的 BLOB_NOT_FOUND)。需要瘦身时手动跑一次修复:
node scripts/repair-ledger.mjs # 默认修 $DSH_HOME/rewind-ledger(无 DSH_HOME 则 ~/.dsh/rewind-ledger)
node scripts/repair-ledger.mjs <其他目录> # 指定台账目录
它会删除:① 包含 node_modules/.git 路径的旧时代污染 manifest;② 引用了缺失/损坏 blob 的 manifest;然后 collectGarbage 清掉不再被任何 manifest 引用的 blob。
cd dsh-rewind
node test/client-smoke.mjs # 客户端:槽注册 + openSessionWithDraft 回填 helper
node test/render-smoke.mjs # 组件渲染:纯图标按钮 + Modal
node test/host-smoke.mjs # host:快照采集/差异/恢复 + messageTargetFor + fork
node test/host-undo-smoke.mjs # 端到端:恢复新增/删除/修改 + blob 哈希校验 + fork
node test/host-fork-lineage-smoke.mjs # fork 血缘:A2/A3 里回退父会话消息的台账解析
node test/host-scoped-rewind-smoke.mjs # 跨会话隔离:B 回退不碰 A 改过的文件
# 或一次跑全部:
npm test # = 上面 6 个脚本依次执行
测试在临时工作区 + 临时 DSH_HOME 下直跑,不触网、不读真实 DSH 环境,node 即可。
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。