🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

会话回退 yamingmou/dsh-retrace

DeepSeek Harnessv0.4.135
yamingmou6 天前有提交已提供中文文档扫描:含敏感能力

撤回、编辑重发和重新生成 DeepSeek Harness 会话消息,Web 与桌面端插件

39.2/100
综合分
6
GitHub 星标
739
npm 周下载
—/5
用户评分

数据截至 2026/10/29 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-retrace
npm 源 · dsh-retrace 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · chat
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 6 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-retrace @ 0.4.135
✓Node 引擎要求 >=20.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 19:57:15

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 66 处,展示前 57 条)
  • 读取本地文件
    lib/index.js:61
    badgeTable = JSON.parse(readFileSync(resolveBadgeTablePath(), 'utf8')).codes ?? null
  • 读取本地文件
    lib/index.js:103
    badgeTable = JSON.parse(readFileSync(resolveBadgeTablePath(), 'utf8')).codes ?? null
  • 读取本地文件
    lib/index.js:531
    if (existsSync(candidate)) { root = candidate; break }
  • 读取本地文件
    lib/index.js:559
    const row = JSON.parse(readFileSync(fp, 'utf8'))
  • 发起网络请求
    lib/client.bundle.js:218
    `;var zh={"action.edit":"\u91CD\u53D1","action.editAria":"\u91CD\u53D1\u8FD9\u6761\u6D88\u606F","action.recall":"\u64A4\u56DE","action.recallAssistant
  • 发起网络请求
    lib/dynamic-client.js:224
    `;var zh={"action.edit":"\u91CD\u53D1","action.editAria":"\u91CD\u53D1\u8FD9\u6761\u6D88\u606F","action.recall":"\u64A4\u56DE","action.recallAssistant
  • 执行外部命令固定命令
    lib/rollback.js:669
    const out = execFileSync('lsof', ['-nP', '-t', file], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
  • 读取本地文件
    lib/rollback.js:734
    if (!existsSync(file)) throw editorError('session-file-not-found', `会话文件不存在:${file}`, { file })
  • 读取本地文件
    lib/rollback.js:735
    const before = readFileSync(file)
  • 写入本地文件
    lib/rollback.js:876
    mkdirSync(backupDir, { recursive: true })
  • 写入本地文件
    lib/rollback.js:878
    copyFileSync(file, backupPath)
  • 读取本地文件
    lib/rollback.js:879
    const backupSize = statSync(backupPath).size
  • 写入本地文件
    lib/rollback.js:886
    writeFileSync(tmpPath, next)
  • 写入本地文件
    lib/rollback.js:887
    renameSync(tmpPath, file)
  • 读取本地文件
    lib/rollback.js:889
    try { if (existsSync(tmpPath)) unlinkSync(tmpPath) } catch { }
  • 删除本地文件
    lib/rollback.js:889
    try { if (existsSync(tmpPath)) unlinkSync(tmpPath) } catch { }
  • 写入本地文件
    lib/rollback.js:897
    copyFileSync(backupPath, file)
  • 读取本地文件
    lib/rollback.js:898
    rolledBack = Buffer.compare(readFileSync(file), before) === 0
  • 读取本地文件
    lib/rollback.js:922
    disk = readFileSync(file)
  • 读取本地文件
    lib/platform/session-paths.js:136
    try { accessSync(r); return r } catch { }
  • 读取本地文件
    lib/platform/session-paths.js:170
    try { accessSync(root); return dirname(root) } catch { }
  • 读取本地文件
    lib/platform/session-paths.js:175
    try { accessSync(base); return base } catch { }
  • 读取本地文件
    lib/platform/session-paths.js:187
    const m = statSync(p).mtimeMs
  • 读取本地文件
    lib/platform/session-paths.js:203
    try { workspaces = readdirSync(root) } catch { continue }
  • 读取本地文件
    lib/platform/session-paths.js:223
    try { workspaces = readdirSync(root) } catch { continue }
  • 读取本地文件
    lib/identity/shortcode.js:170
    const head = readFileSync(file)
  • 执行外部命令固定命令
    lib/identity/shortcode.js:175
    const out = execFileSync('zstdcat', [file], { maxBuffer: 1 << 28 }).toString('utf8')
  • 读取本地文件
    lib/identity/shortcode.js:203
    if (!existsSync(path)) return edges
  • 读取本地文件
    lib/identity/shortcode.js:205
    try { pairs = JSON.parse(readFileSync(path, 'utf8')).pairs ?? [] } catch { return edges }
  • 读取本地文件
    lib/identity/shortcode.js:296
    if (!existsSync(root)) return { rows, errors: [{ root, error: 'root-missing' }] }
  • 读取本地文件
    lib/identity/shortcode.js:514
    if (!existsSync(row.path)) return { openable: false, reason: 'file-missing', layer: LAYERS.RUNTIME }
  • 读取本地文件
    lib/identity/shortcode.js:535
    try { return statSync(path).size } catch { return null }
  • 写入本地文件
    lib/identity/shortcode.js:644
    appendFileSync(path, JSON.stringify(record) + '\n')
  • 读取本地文件
    lib/identity/code-assign.js:115
    if (existsSync(tablePath)) {
  • 读取本地文件
    lib/identity/code-assign.js:116
    try { raw = readFileSync(tablePath, 'utf8') } catch (error) { return { ok: false, error: `table-unreadable:${msgOf(error)}` } }
  • 写入本地文件
    lib/identity/code-assign.js:136
    mkdirSync(dirname(tablePath), { recursive: true })
  • 写入本地文件
    lib/identity/code-assign.js:139
    writeFileSync(tmp, JSON.stringify(table, null, 2) + '\n')
  • 读取本地文件
    lib/identity/code-assign.js:142
    try { currentRaw = readFileSync(tablePath, 'utf8') } catch (error) { currentRaw = error && error.code === 'ENOENT' ? null : undefined }
  • 删除本地文件
    lib/identity/code-assign.js:145
    try { rmSync(tmp, { force: true }) } catch { }
  • 写入本地文件
    lib/identity/code-assign.js:149
    renameSync(tmp, tablePath)
  • 读取本地文件
    lib/identity/code-assign.js:152
    try { if (existsSync(tmp)) rmSync(tmp, { force: true }) } catch { }
  • 删除本地文件
    lib/identity/code-assign.js:152
    try { if (existsSync(tmp)) rmSync(tmp, { force: true }) } catch { }
  • 写入本地文件
    lib/summary-store.js:72
    await mkdir(dirname(path), { recursive: true, mode: 0o700 })
  • 写入本地文件
    lib/summary-store.js:76
    await appendFile(path, `\n${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 })
  • 读取本地文件
    lib/summary-store.js:92
    text = await readFile(path, 'utf8')
  • 写入本地文件
    lib/artifact-store.js:101
    await mkdir(target, { recursive: true, mode: 0o700 })
  • 写入本地文件
    lib/artifact-store.js:102
    await chmod(target, 0o700)
  • 读取本地文件
    lib/artifact-store.js:169
    const existing = new Uint8Array(await readFile(target))
  • 删除本地文件
    lib/artifact-store.js:176
    await unlink(temporary)
  • 删除本地文件
    lib/artifact-store.js:181
    await unlink(temporary).catch(() => {})
  • 读取本地文件
    lib/artifact-store.js:188
    const data = new Uint8Array(await readFile(objectPath(root, sha256)))
  • 删除本地文件
    lib/artifact-store.js:198
    await unlink(objectPath(root, sha256))
  • 读取本地文件
    lib/artifact-store.js:209
    buckets = await readdir(objectsRoot, { withFileTypes: true })
  • 读取本地文件
    lib/artifact-store.js:217
    for (const entry of await readdir(join(objectsRoot, bucket.name))) {
  • 发起网络请求
    lib/client.js:237
    return fetch(`${ROUTE_BASE}/${op}`, {
  • 发起网络请求
    lib/client.js:2693
    return fetch(`${ROUTE_BASE}${path}`, { headers: retraceConfigHeaders() }).then((res) => {
  • 发起网络请求
    lib/client.js:4323
    return fetch(`${ROUTE_BASE}/runningState`, {
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-ui-conversation@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-session@deepseek-ai/dsh-storage-domain@deepseek-ai/dsh-token-meter
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 撤回消息
  • 编辑重发
  • 重新生成

README

由 DeepSeek 最新模型翻译生成
🧭 dsh-retrace 撤回 · 重发(从这条重开)· 重新生成,加上写安全的会话版本化 —— DeepSeek Harness 的 Agent 业务层(生产级保证) 实现。 简体中文 · English 撤回 / 重发 / 重新生成 —— 每个会话都该有的三个操作。但回退不只是「撤掉一条消息」:DeepSeek Harness 把对话存在 append-only 事件日志里,撤回只回退上下文,改过的 产物文件不会自动还原。dsh-retrace 把对话和它的产物一起版本化,并保证 每一次新的回退都合法——不会弄脏日志,新写入的 marker 不产生 token-meter 配对债 (两段原子对按构造即通过)。 ⚠️ 诚实的边界(与配套契约自己的说明一致):旧版本留下的单段 marker 是已知设计债。压缩前请用配套 check 体检并 fix --remove-markers 清理,否则宿主的压缩前自检会挡住 /compact。新的回退不会增加这笔债。 🛡️ 写安全 · 🔍 深层体检 · 🔄 检测→修复→守护 —— 详见下方「生产级保证」。 版本索引:VERSIONS.md 0.4.135 发布前,安装/固定版本请用 0.4.134(当前 npm latest)。
展开完整文档(README 剩余 1 节)
🧭 dsh-retrace

撤回 · 重发(从这条重开)· 重新生成,加上写安全的会话版本化 —— DeepSeek Harness 的 Agent 业务层(生产级保证) 实现。

简体中文 · English

撤回 / 重发 / 重新生成 —— 每个会话都该有的三个操作。但回退不只是「撤掉一条消息」:DeepSeek Harness 把对话存在 append-only 事件日志里,撤回只回退上下文,改过的 产物文件不会自动还原。dsh-retrace 把对话和它的产物一起版本化,并保证 每一次新的回退都合法——不会弄脏日志,新写入的 marker 不产生 token-meter 配对债 (两段原子对按构造即通过)。

⚠️ 诚实的边界(与配套契约自己的说明一致):旧版本留下的单段 marker 是已知设计债。压缩前请用配套 check 体检并 fix --remove-markers 清理,否则宿主的压缩前自检会挡住 /compact。新的回退不会增加这笔债。

🛡️ 写安全 · 🔍 深层体检 · 🔄 检测→修复→守护 —— 详见下方「生产级保证」。

版本索引:VERSIONS.md

0.4.135 发布前,安装/固定版本请用 0.4.134(当前 npm latest)。

⚡ 一分钟安装

需要带 dsh CLI 的 DeepSeek Harness;装完重启 DSH 生效(运行中的应用不会热加载)。

dsh plugin --profile desktop add dsh-retrace # DSH 桌面版
或 Web 部署:dsh plugin --profile web add dsh-retrace 或从解压后的检出 / ZIP:
dsh plugin --profile desktop add ~/plugins/dsh-retrace

没有命令行? 先装一次社区插件市场,再在 设置 → Plugin Market 搜 dsh-retrace 一键安装:

dsh plugin --profile desktop add dshmarket # 只需一次

重启后,悬停任意助手回复 → ↩ / ↻;任意用户消息 → ✎。详细步骤见 📦 安装。

🛡️ 生产级保证(0.4.x 全部已上线)

| | 能力 | 说明 |
| 🛡️ | 写安全 | 每次回退过三层写前契约校验;运行中的 agent 自动停止(官方 cancel/whenIdle,带等待上限);轮次间 marker 用临时 step 包裹 —— 新的回退不会弄脏日志,新 marker 不产生 token-meter 配对债;旧单段 marker 是已知设计债(压缩前先 check + fix --remove-markers) |
| 🔍 | 深层体检 | 配套 dsh-log-contract 30+ 条契约规则(token-meter 配对 / 跨 step 引用 / 物理序 / inbox 重放),用真实损坏会话当测试集 —— 能找出让 /compact 永久失效的那类问题 |
| 🔄 | 检测→修复→守护 | 看门狗在并发写入第一时间快照日志;离线 fix 原地中和问题 marker、裁剪跨 step 引用;写前校验在坏事件落盘前拦住 |

✨ 功能

| 操作 | 入口 | 效果 |
| ↩ 撤回 | 悬停任意助手回复;或用户消息下方的操作行 | 先弹二次确认(破坏性动作:本条及其后的对话将离开会话)。确认后,若本轮仍在运行,插件先通过官方停止入口暂停在途轮次并等待(有上限);暂停失败/超时也照常撤回,并给出可见提示。随后整轮(该条输入及其输出、工具行)从模型上下文与对话视图中一并移除,输入原文回显到输入框方便立即修改后重发;一条短暂提示标记回退点,你继续输入后自动消失。 |
| ✎ 重发(从这条重开) | 用户消息下方的操作行 | 主语义是从这条重开:本条及其后被回退,并用新文本重发,对话进入新的时间线分支。若该范围被守卫拒绝(过大、无法原地改写),错误提示里给出兜底动作 —— 「仅改文本(保留后续)」。新消息下方有一个折叠的「原输入」对照,点击展开、可配置关闭。 |
| ↻ 重新生成 | 悬停任意助手回复 | 回退并隐藏该回复及其后内容,重新发送原提问,让智能体重新作答。 |

撤销一次撤回

| 操作 | 作用面 | 效果 |
| ↺ 恢复显示 | 只改界面 | 把该标记隐藏的行重新显示回对话视图。日志只增不减、模型上下文不变,撤回痕迹保留。入口在该标记行上。 |
| ⏪ 真正恢复 | 改日志 | 原地中和这两段标记(不改 seq 编号),让被撤回区间回到模型面,该区间的重发 / 撤回入口复活。要求会话空闲、且不是你当前正在查看的会话;成功后需切走并重开该会话(或刷新页面)才看到恢复后的内容。被拒时给出可执行的下一步(会话运行中 / 文件被其他进程持有 / 已被更晚的标记重新遮蔽 / 当前宿主未装配该能力)。 |

读档点与回档(0.4.x 已上线) —— 每次回退都会被记录为一个读档点:

| | 能力 | 说明 |
| 🕘 | 读档点 | 对话视图里的「读档点」Tab:每个改动(类型/时间/消息数/文件变更徽标),经 session/projection 推送帧实时更新(零轮询),大列表窗口化;嵌套历史按大纲逐层展开 |
| ↩️ | 产物回退 | 仅对话 / 仅产物 / 两者,先干跑预览再执行;git 优先 + 内容寻址快照兜底;回退本身是新读档点(restore)。预览在会话运行中直接拒绝(不会出现「预览说可以、确认时才被拒」),预览/回档失败都会在界面上显示,不静默 |
| 🧬 | 谱系标识 | 新建会话自动分配稳定谱系标识;侧栏每个会话只显示一个标识(刷新不重复) |
| 🧭 | 跳转对话 | 从读档点一键跳转到对应位置(自动翻页加载更早历史 + 锚点高亮);该点超出自动加载预算时,给出可诊断原因,而不是点了没反应 |
| 🧹 | 存储有界 | 快照只保留最近 N 个读档点(默认 50);节流后台扫掠回收被截断的旧档 |

损坏日志上的回档不静默:宿主的严格重放是权威,但一条畸形事件(例如第三方写入的畸形替换行)就能毒化整次重放。这种情况会退到镜像重放,日志同时记录「已降级」与原始错误,并在 wire 上把结果标为 degraded;连镜像都跑不了时返回结构化的 replay-failed 错误,而不是笼统的 internal。

为什么与众不同(交互层差异——上面的保证是存储层):

- 🎯 整轮撤回 —— 移除输入 和 它的输出(含工具行),而不只是单条气泡。
- 🖥️ Web + Desktop 双端 —— 同一插件覆盖 DeepSeek Harness 两种界面。
- 🧠 视图 ⇄ 上下文同步 —— 对话视图永远反映智能体真正看到的内容。
- ⚡ 30 秒上手 —— 动态插件形式无需重建即可在当前会话试用。

📦 安装

1. Profile bundle(推荐)

包声明了 dsh.bundle 清单,可通过官方插件路径安装到任意 profile:

dsh plugin --profile <name> add dsh-retrace

⚠️ 安装后需要重启。 安装会写入新文件并重新生成 profile 组合,但运行中的应用不会热加载 bundle —— 请退出并重新打开 DSH Desktop(独立 Web 部署则重启
dsh 进程)来加载插件。卸载:dsh plugin --profile <name> remove
dsh-retrace(卸载后同样需要重启)。

2. 手动安装(不依赖 dsh CLI)

用纯文件编辑 + pnpm 装进同一个 profile —— 也就是 dsh plugin add 帮你做的那些步骤:

下载了 ZIP? 解压到固定位置(如 ~/plugins/dsh-retrace),然后执行 dsh plugin --profile desktop add ~/plugins/dsh-retrace;或按下面步骤,把依赖行指向该文件夹:"dsh-retrace": "file:~/plugins/dsh-retrace"。

1. 打开 profile 清单(默认位置:DSH Desktop 为 <插件数据家>/profiles/desktop,独立 Web 为 <插件数据家>/profiles/web —— 插件数据家在设了 $DSH_HOME 时就是它,否则跟随活动会话基座(基座来自 $DSH_HOME 与 $DSH_SESSION_BASES 配置的额外基座,最后兜底 ~/.dsh);~/.dsh/profiles 只是迁移前的兜底),同时加入依赖和 bundle 层条目:

{
"dependencies": {
"dsh-retrace": "^0.4.135"
},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-retrace"
]
}
}
}

(保留 profile 原有条目,只需新增 dsh-retrace 这两处。)

2. 在 profile 目录里安装:

cd "$DSH_HOME/profiles/<name>" && pnpm install # 或你实际使用的基座

3. 重启 DSH Desktop / dsh 进程(见上文)。

本地开发时,可以把依赖指向本地检出目录而不是注册表: "dsh-retrace": "file:/路径/to/dsh-retrace" —— 或者交给 dsh: dsh plugin --profile <name> add /路径/to/dsh-retrace。
想要最新提交而不等发版时,用本包 repository 字段里的仓库地址加 git 依赖(pnpm 写法 "dsh-retrace": "github:<owner>/dsh-retrace"),放在同一个 dependencies 块里,再 pnpm install。

3. npm 包 + 组合文件(经典方式)

npm i dsh-retrace

在所使用的应用/部署的 cordis.yml 组合文件中加入一行普通插件条目:

- name: 'dsh-retrace'

Client 半区会依据包内 dsh.client 元数据被自动打包进 Web 客户端(组合变化时会自动重建客户端模块);Host 半区为浏览器 UI 注册同源 HTTP 路由 /api/plugins/retrace/。

4. 动态插件(当前会话,免安装、免重建)

包内提供了两个自包含的动态入口:

1. 打开插件编辑界面,用 lib/dynamic-host.js(Host 半区)和 lib/dynamic-client.js(Client 半区)新建插件;
2. 批准并运行 Client 半区;
3. 完成 —— 悬停任意助手回复或用户消息,即可使用 ↩ / ✎ / ↻。

动态 Host 通过 harness.handle 注册同一组操作(retrace.recall / retrace.editAndResend / retrace.regenerate)。

5. 依赖与权限

- DeepSeek Harness 需提供 package.json 里声明的 peer 包(@deepseek-ai/dsh-session、@deepseek-ai/dsh-client-、cordis、react ——当前构建面向 0.1.7-rc.2 这一代内核)。
- dsh-log-contract >= 0.3.18(作为依赖自动安装)。
- 插件通过 inject 声明所需宿主能力(sessions、agents、webServer、fs、subprocess、sandboxPolicy、jobs)。
缺某个能力只会让对应功能降级(例如没有 subprocess ⇒ 回档只用内置快照),不影响启动。

🔒 关闭守卫(防误关丢进度)

退出/重载前先看清还有什么在跑:

| | 是什么 | |
| 🛡️ | 运行中检测 | 逐会话扫描运行中工作:agent 正在跑 / inbox 排队 / 后台 jobs / 未闭合轮 |
| 📋 | 运行中横幅 | 有运行中工作的会话显示页面常驻横幅(会话谱系标识 + 原因),退出前可见 |
| ⚠️ | 退出提示 | 插件 dispose(应用退出/重载)时中文提示列出每个运行中会话与原因——只提示,绝不代你取消 agent |
| 🔒 | 页面关闭阻止 | 桌面端一律不武装宿主原生确认框;改用页面自绘确认门:先画框并同步校验可见才拦(preventDefault),画不出 / 不可见 / 页面不可见 ⇒ 当场放行;确认框一直等你选择(Esc = 取消);看门狗(Web Worker 计时器,不受后台节流)只作最后一道。关掉设置项「退出确认(关闭守卫)」即退回官方形态(不用重启) |
| 🔎 | 查询面 | retrace.runningState(host RPC)+ GET\|POST /api/plugins/retrace/runningState(HTTP),两入口同形状;全会话形状另带宿主判定的承载面(surface / quitVeto) |

关闭确认的行为(用户可见口径):是否弹宿主原生关闭确认由宿主与页面共同决定。
桌面端不弹宿主原生框,改用页面内自绘确认(Esc = 取消)。框画不出来 / 框不可见 / 页面不可见 ⇒ 当场放行。把设置里的「退出确认(关闭守卫)」关掉即退回宿主默认形态(不用重启)。

已知限制(照实写):某些桌面壳的退出口径根本不经过页面(如托盘项直接销毁窗口,或点 X 只是隐藏窗口)⇒ 那类壳上「退出时弹确认」插件侧做不到,需要壳提供 seam (will-prevent-unload 处理,或退出前的询问钩子)。桌面端退不掉时,把设置里的「退出确认(关闭守卫)」关掉即退回官方形态(不用重启)。

⚙️ 设置 → 通用

| 设置项 | 默认 | 说明 |
| 编辑后显示原提问对照 | 开 | 重发消息下方的折叠「原输入」引用,显示最近一次被替换的原文(仅作对照,不会进入模型上下文)。 |
| 读档点与产物快照 | 开 | 开:每次撤回/编辑自动存一档(消息与触碰文件),提供读档点列表与产物回退;关:仅回退上下文,不存档、不追踪产物(最省资源)。 |
| 给旧内容生成 AI 摘要 | 关 | 开:撤回 / 编辑时多花 1 次小模型调用,为被丢弃的旧内容生成摘要;依赖宿主 llm 服务(官方 @deepseek-ai/dsh-llm),缺失时自动降级为只显示逐字原文。关:零 LLM 调用;逐字原文(零 token 成本)始终产出。每次操作至多 1 次小调用(输入文本上限 500 字符、输出 ≤200 token、5 秒超时),模型与凭据沿用会话自身的默认选择,插件不新增任何配置面。 |
| 启用 git 集成 | 开 | 开:工作区是 git 仓库时用 git 记录与回退(不自动提交、不动你的分支),非仓库可在读档点视图里一键启用;关:一律用内置快照(存于插件数据家),不触碰工作区 git 状态,功能等价。 |
| 读档点保留上限 | 50 | 文件快照只保留最近 N 个读档点,超出自动清理最旧的;读档点记录与审计痕迹始终保留。 |
| 退出确认(关闭守卫) | 关 | 开:有运行中任务或未完成对话时,页面关闭前由本插件自绘确认框(Esc 等同取消)。默认关,因为这道门也会拦住应用内重载 —— 桌面端口径与自救方式见 🔒 关闭守卫。 |

设置里原有的「按标记隐藏被编辑/撤回的消息」与「编辑后从新对话开始」两个开关已撤除:撤回/重发/重新生成恒隐藏被替换的内容(单个 marker 要隐藏超过 40% 的对话时降级为只显示提示,历史永不静默消失),重发恒为「从这条重开」语义,兜底动作如上文所述。

🧠 工作原理

持久化日志(只追加) 模型上下文与视图 ┌────────────────────────────────┐ ┌────────────────────┐ │ … 目标消息 │ │ … 目标消息 │ │ ↓ 阴影区间 │ │ ↓ 回退 │
│ [目标 … 最后一个表面节点] │ ──▶ │ (空 replace │
│ ↳ 追加一条替换型 │ │ = 上下文截断) │ │ assistant/message(空) │ agent.followup(新提示) │ ↳ 可选「原提问」对照 │ → 下一轮基于回退后的历史重建请求 └────────────────────────────────┘ └────────────────────┘

1. Host 核心(lib/host-core.js,零运行时依赖):在会话的活跃表面中定位目标消息,计算阴影区间 [消息 … 最后一个表面节点],追加一条空内容的替换型 assistant/message —— 空助手消息是合法表面节点,但派生不出任何模型消息,因此 LLM 上下文直接回退。
2. 重发 / 重新生成:额外调用 agent.followup(...) 发送(新的)提示文本,智能体的下一轮请求基于回退后的 session.deriveMessages() 构建。
3. Client(lib/client.js)注册:
- 每条用户消息下的 user-actions 对话节点(重发/撤回行 + 内联编辑器);撤回后把原文回显到输入框,
- recall-marker 节点渲染器:提示行 + 注入 CSS 把被阴影化的消息行从对话流中隐藏(视图与模型上下文保持同步),并可显示「原提问」对照块、「恢复显示」按钮 与「真正恢复」动作,
- conversation.chat.assistant-actions 中的 retrace 入口(撤回/重新生成),
- 「读档点」视图与 设置 → 通用 中的偏好开关。

这里有两个不同层面:持久化日志(只追加;旧事件从不被改写或删除)与模型可见表面(由追加的替换事件回退)。因此旧事件作为审计痕迹留在记录中——但它们会被同步地从模型上下文和可见对话中清除,界面始终反映智能体真正看到的内容。
因为插件只追加合法、带类型的会话事件,持久化、投影与记录保持一致。(真正恢复是唯一会改写日志的操作,且是原地改写、不改 seq 编号。)

🔺 兼容性与升级须知

dsh-retrace 是 bundle 型插件:它插进哪套宿主,就依赖那套宿主暴露的面。因此宿主 移除一个包或一个客户端服务时,旧版插件即使一行没改也会坏 —— 而且症状通常是「起不来」,不是「某个功能看起来不对」。

本节的目的:让你能分清 宿主侧破坏性变更 与 插件侧缺陷。提 issue 前请先看这节。

宿主侧变更须知

宿主升级偶尔会移除插件依赖的 API —— 这些是宿主侧变化,不是插件缺陷,一行一条:

- 升级后开机循环/恢复界面 → 切到干净会话并重启 DSH(插件降级而非阻断)。
- 版本/检查点面板为空 → dsh plugin --profile add dsh-retrace 刷新 bundle。
- 标题/重命名行为变化 → 自动标题路径由宿主守卫,手动重命名不受影响。
- 依赖下限:dsh-log-contract >= 0.3.18。

历史细节与原始报错见 CHANGELOG.md;兼容矩阵见 docs/COMPAT.md。

升级

dsh plugin --profile desktop add dsh-retrace@0.4.135
然后重启 DSH —— 插件不会热重载

0.4.27 与 0.4.31 已撤回(短暂发布后撤回、npm 上标记 deprecated);它们的替代版本包含其全部修复。

不需要数据迁移。 会话格式未变(v3),不改写任何会话,也不需要重建索引:升级、重启即可。
若宿主仍提供旧成员,兼容访问器会让那些路径继续可用 —— 本构建不丢弃老宿主。

如果升级后 App 起不来:一个插件失败就能拖垮整棵树,所以先恢复、再排查:

1. 把 dsh-retrace 从 profile 的 dsh.profile.bundles 和 dependencies 里删掉,重启,确认能先进得来;
2. 读宿主日志 —— macOS:~/Library/Application Support/DSH Desktop/logs/host/dsh-<日期>.error.log;
3. plugin tree failed to load 是宿主侧;renderer boot failed 是客户端侧。
两者都会点名出问题的插件/包 —— 从那里查起。

版本固定建议

请固定到确切版本(dsh-retrace@0.4.135),并让 dsh-log-contract 解析到 >=0.3.18。
不要在跨宿主升级时依赖 ^0.4 这种范围:这里的兼容性由宿主的面决定,光看 semver 不够。

⚠️ 要求与限制

- 只有用户消息可以重发;撤回同时适用于用户与助手消息。工具结果会随区间一并被阴影化,但不能单独作为撤回目标。
- 撤回若指向正在运行的一轮,会先暂停它(官方停止入口、有等待上限)。暂停失败/超时也照常撤回,并用提示告知。真正恢复与回档预览要求会话空闲,否则被拒。
- 撤回/重发作用于活跃模型表面:已被压缩或此前已撤回的消息会被拒绝(target-shadowed);写入尚未 flush 的消息会如实报 message-pending(「消息生成中」),不再被误判成「已不在活跃对话」。
- 重新生成只重发原提示的文本部分;携带图片的提示会退化为仅文本重发。
- 真正恢复会原地改写会话日志(中和两段标记,seq 编号不动)。会话是你当前正在查看的、正在运行的,或文件被其他进程持有时,它会拒绝执行;成功后需重开该会话(或刷新)才看到结果。

🗺️ 路线图

详细下一步与方向:docs/PLAN.md | 能力路线:docs/ROADMAP.md

当前已具备(0.4.x):

- 撤回 / 重发 / 重新生成——每次回退都过三层写前校验与安全编辑路径(自动停 agent、临时 step 包裹 marker),新的回退不会损坏日志、不新增 /compact 债;旧单段 marker 需要压缩前清理(配套 check + fix --remove-markers)。
- 撤销:恢复显示(界面)与真正恢复(日志层面、原地)。
- 单会话读档点时间线 + 产物回退(git 优先 + 快照兜底、干跑预览、跳转对话)。
- 关闭守卫(运行中横幅、退出提示、页面自绘确认门)。
- 实时看门狗——并发写入第一时间快照日志。
- 配套 dsh-log-contract:30+ 条离线契约规则 + 原地修复(fix --neutralize / --clip-crossstep),能处理会让 /compact 永久失败的会话。
- 供集成使用的只读宿主面:retrace/versions、retrace/forkmap 投影, GET /api/plugins/retrace/{versions,forkmap,lineage,event,surface,doctor,snapshot}, POST /api/plugins/retrace/{rollback/preview,rollback,git/init},以及谱系标识操作(sessionBadge / setBadgeTitle / initBadgeTitles / badgeMap)与 retrace CLI(见文末)。

未来计划——agent 业务层规划(运行时守护、中断治理、生态开放接口)尚未发布,此节是计划而非已上线能力。本 README 描述的是开发线(main),可能领先于 npm 上最新发布版。

🛠️ 开发

目录结构 lib/host-core.js # 传输无关的 Host 逻辑(无 import) lib/index.js # 发布版 Host:harness RPC + HTTP 路由 lib/client.js # Client 源码(import React;传输层可插拔) lib/client.bundle.js # 构建产物 —— 自注册 loader entry (window.__ModuleLoader__.load),由 client-modules 提供 lib/dynamic-host.js # 生成的动态 Host 半区(源自 lib/host-core.js) lib/dynamic-client.js # 生成的动态 Client 半区(源自 lib/client.js) scripts/build-client.mjs # 打包 lib/client.js → lib/client.bundle.js scripts/generate-dynamic.mjs # 从权威源生成两个动态入口 scripts/check-dynamic.mjs # 语法检查动态入口(函数体形态) test/ # vitest 套件:host-core 操作 + 生成产物冒烟测试 .github/workflows/ # CI(语法 + 构建同步 + 测试)与 npm 发布(v* tag) cordis.patch.yml # dsh.bundle profile patch 层

pnpm install # 安装开发依赖(vitest、esbuild);需要 Node >= 22.13
pnpm check # 语法检查源码与生成的动态入口
pnpm build # 重新生成 lib/dynamic-.js 与 lib/client.bundle.js
pnpm test # 运行单元测试
npm pack --dry-run # 校验发布文件清单

⚠️ 生成文件。 lib/dynamic-host.js、lib/dynamic-client.js 与 lib/client.bundle.js 是由 lib/host-core.js 和 lib/client.js 生成的构建产物 —— 请勿手改。CI 会在构建产物与源码不同步时失败(git diff --exit-code),因此提交前记得执行 pnpm build。动态 Client 与发布版共用同一份 client 源码,仅通过 __setMessageEditorWire 切换传输层(host.call vs HTTP 路由)。

欢迎提交 PR 与 issue —— 问题追踪入口见本包 repository/bugs 元数据里的链接。

📚 生态

收录于 dsh-plugin topic。

Agent 业务层(生产级保证) 的一部分——即 dsh-retrace 在 DeepSeek Harness 上实现的那层框架无关的业务层定义。配套组件:

- dsh-log-contract —— 业务层的「医生」:30+ 条离线契约规则 + 原地修复(fix --neutralize / --clip-crossstep)。
作为依赖自动安装,也独立发布供直接使用。

直接从 git 安装(无需 npm registry —— 适合把本仓库链接丢给 AI,或想装最新提交):

dsh plugin --profile desktop add github:<owner>/dsh-retrace
或直接用 pnpm 装进 profile:
cd "$DSH_HOME/profiles/desktop" && pnpm add github:<owner>/dsh-retrace

<owner> 取本包 repository 字段里的 owner。然后照常重启 DSH Desktop, dsh-log-contract 依赖会自动带上。

DeepSeek Harness 插件生态的精选总览见 awesome-dsh-plugin (第三方收录,使用前请自行确认可用性)。

👥 团队

由 OfferKuai 团队开发——一款 AI 求职助手,使命是「用户要的是结果,而不是反复的对话」。本插件以开源形式发布,回馈 DeepSeek Harness 社区。

📄 License

MIT

🧭 会话日志考古(retrace CLI)

DSH 会话日志持久化了每次工具调用的完整输入输出——数据资产与审计资产。
retrace CLI 提供只读考古能力(复用 dsh-log-contract 的契约与提取):

retrace index <session> # 工具调用索引 retrace query <session> --cmd "seed-scale" # 按命令正则查输出 retrace extract <session> --pattern "seed-scale" --out ./found # 导出输出 retrace file-history <session> <path> # 文件 write/edit 历史版本 retrace file-diff <session> <path> 0 5 # 两版本行级 diff retrace lineage <session> # 会话 parent 链谱系

<session> 为完整日志路径或 sessionId(自动在活动会话基座查找:$DSH_HOME/sessions,否则 $DSH_SESSION_BASES 各基座,最后才是 ~/.dsh/sessions)。全部只读。

会话谱系:GET /api/plugins/retrace/lineage?sessionId= 沿当前会话的 parentSession 接续链遍历(只读、带环保护),与 CLI retrace lineage 同一语义 ——任何客户端都能查到「这个会话是从哪个会话接着干/分叉出来的」,也是分叉拓扑的元数据源。

兼容性与运行契约(Compatibility & runtime contract)

| 项 | 声明 |
| Node.js | >=20.0.0(见 package.json#engines) |
| DSH | >=0.1.5 <0.3.0(见 package.json#dsh.compatibility;桌面与 Web 两种模式均支持) |
| 运行所需文件 | 运行时所需文件全部随仓库提交并在 package.json#files 声明(lib/、cordis.patch.yml、README 等) |
| 单文件体积 | 每个分发文件 < 256 KiB(最大约 249 KB) |
| 构建产物 | 运行所需构建产物随包提交(lib/client.bundle.js、lib/dynamic-client.js、lib/dynamic-host.js);仓库内可复现:pnpm i && pnpm build(生成器 scripts/generate-dynamic.mjs、scripts/build-client.mjs) |
| 运行时依赖 | dsh-log-contract(日志契约校验)、zod(参数校验);不联网、不下载额外代码 |
| 宿主依赖(peer) | @deepseek-ai/cordis 与若干 @deepseek-ai/dsh- 客户端包(由 DSH 提供,不由本插件分发) |
| 权限 / 外部服务 | 只读写本机会话数据目录与会话日志;不访问外部服务、不上报数据;不改动 DSH 安装目录之外的系统路径 |
| 失败边界 | 所有写入(撤回/编辑/恢复/回档)均在目标文件之外先备份、写后做帧级校验与折叠重放;失败即中止并保留原文件;不可判定时按"拒绝 + 可操作提示"处理,不静默降级 |
| 可回退 | 每个操作都有可逆路径;本地管理脚本可一键回退到上一快照 |

- 稳定标识由会话 id 确定性导出(无随机后缀);对同一会话重复读取得到同一个标识。
预览分享卡片长什么样 ↓yamingmou/dsh-retrace
分享 · DeepSeek Harness Hub
yamingmou/dsh-retrace

撤回、编辑重发和重新生成 DeepSeek Harness 会话消息,Web 与桌面端插件

回到顶部