# dsh-web-ui

> Add conversation recovery to dsh web GUI: edit last user message to regenerate, auto-retry failed rounds on recoverable errors (up to 5 with exponential backoff), all paths use fork, original session unchanged.

## Metadata

- Author: [@zhu1090093659](https://github.com/zhu1090093659)
- Repo: <https://github.com/zhu1090093659/dsh-web-ui.git>
- GitHub: [zhu1090093659/dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui)
- Stars: 5,126
- Language: TypeScript
- License: [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
- Homepage: <https://gallery.dsh-market.com>
- Topics: `deepseek-harness`, `dsh`, `dsh-plugin`, `web-ui`
- Forks: 311
- Open Issues: 49
- Last push: 2026-08-20T14:37:38.000Z
- Added: 2026-08-20T00:00:00.000Z

## Install

```bash
dsh plugin --profile web add github:zhu1090093659/dsh-web-ui/packages/dsh-chat-recovery
```

## Wiki

## 一句话定位
为 dsh web GUI 加对话恢复能力：编辑最近一条已结束的用户消息让它从原位置之前重新生成；对失败轮次自动按可恢复错误最多 5 次指数退避重试（或给出手动重试按钮）。两条路径都走 fork，原会话始终不被改动。

## 核心能力
- 在最近一条已结束用户消息的尾部出现「编辑」按钮：点击进入行内编辑器（预填原文本），保存即从该消息之前的历史前缀切分支、打开子分支并重新发送编辑后文本；原会话完整保留
- 自动重试失败轮次：仅对可恢复的模型/API 错误（超时、网络、5xx、429 限流、空响应等）且轮次不含工具/命令时启动，最多额外 5 次，节奏 1s/2s/4s/8s/16s
- 手动重试入口：不可恢复错误、含工具/命令的轮次、用户主动停止、输出 token 上限一律只显示「重试」按钮，每次点击重放一次，由用户自行决定是否接受工具副作用
- composer 上方 dock 状态行：显示当前等待次数/进行中、剩余退避秒数、最终失败原因，附「立即重试」（跳过剩余等待）与「取消重试」按钮
- 与 host 自身 llm/retry 链协同：当 host 已对同轮次发起 model-retry 时，本插件主动让位，避免双轨重试

## 技术实现
- **语言**: TypeScript（host 半区）+ TypeScript + React 18 + CSS Modules（client 半区）
- **关键依赖**: `react` ^18.2.0（peer，宿主运行时注入）、`@deepseek-ai/dsh-client-ui-conversation`（提供 SlotMap 与 turnTail/input.dock 槽位）、`@deepseek-ai/dsh-client-ui-slots`（slot 注册与翻译运行时）、`@deepseek-ai/dsh-client-runtime`（sessions/workspaces/snapshot 服务）
- **架构模式**: 双半区 cordis bundle。`src/index.ts` 是 host 半区（apply 为 no-op，纯注册用占位让 bundle 可见）；`src/client/index.ts` 是 browser 半区（apply 时先做 globalThis 装载守护，再注册 locale 与两个 slot：`conversation.chat.turnTail` 放编辑+手动重试按钮、`conversation.input.dock` 放监督器状态行）。跨半区共享的纯逻辑放在 `src/core/`（`transcript` / `retry-policy` / `retry-supervisor`），框架无关便于单测
- **入口文件**: `packages/dsh-chat-recovery/src/index.ts`（host 入口，apply 为 no-op）、`packages/dsh-chat-recovery/src/client/index.ts`（client 入口，apply 注册槽位与监督器）；bundle 声明在 `packages/dsh-chat-recovery/cordis.patch.yml:5-7`（插入 id `ui-chat-recovery`），浏览器依赖在 `packages/dsh-chat-recovery/package.json:24-37`（注入 4 个官方 `@deepseek-ai/dsh-client-*` 模块 + `platform: web`）

## 适用场景
- 发完消息后发现措辞不对/想换个问法的普通用户：点尾部「编辑」改一行直接重生成，比手动复制粘贴再发一遍省事；
- 跑大批量 agent 任务、偶尔遇到网络抖动/限流的用户：模型/API 抽风时不必自己守在屏幕前，1s/2s/4s/8s/16s 自动重试 5 次，工具调用这种"重放有副作用"的轮次则只给手动按钮，避免偷偷再调一次工具；
- 想在原会话基础上尝试不同提问方向、又不想污染主线历史的用户：编辑/重试都从原位置之前切子分支，主线永远是干净的，事后可以对比多个分支版本。

## 前置依赖与兼容性
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| DSH 宿主 | `0.1.0-rc.6+` 推荐 | package.json 未在 dsh.engines 声明；devDependencies 统一锁到 `@deepseek-ai/dsh-* ^0.1.0-rc.8` |
| Node.js | `^22.19.0` 或 `>=24.0.0` | package.json `engines.node` |
| 平台 | 跨平台 | host 半区 apply 为 no-op；client 半区跑在浏览器，无 OS 限制 |
| 原生模块 | 无 | 全 TypeScript / React，不引入 native 绑定 |
| React | `^18.2.0` | peerDependency，由宿主注入 |

## 安装方式
```bash
dsh plugin --profile web add github:zhu1090093659/dsh-web-ui/packages/dsh-chat-recovery
```

## 配置项
本插件无需用户配置（无 dsh settings 卡、无环境变量、无 Schema 声明）。

源码内仅 `src/core/retry-policy.ts` 暴露两个运行时常量，调整需改源码后重新构建：

| 内部常量 | 位置 | 默认值 | 作用 |
|---|---|---|---|
| `MAX_EXTRA_RETRIES` | core/retry-policy.ts:18 | 5 | 自动重试在原失败之外的额外尝试上限 |
| `BACKOFF_DELAYS_MS` | core/retry-policy.ts:21 | `[1_000, 2_000, 4_000, 8_000, 16_000]` ms | 第 1..5 次自动重试前的退避延迟 |

## 常见问题

**Q: 安装之后按钮没出现怎么办？**

A: 三步排查。第一，确认 `dsh web` 重启过，bundle 激活是启动时一次性做的，刷新浏览器不足以触发；第二，看一眼浏览器控制台是否有 chat-recovery 相关的报错；第三，编辑按钮只在"最近一条已结束用户消息"的尾部出现——会话正在跑、或最近一条消息含附件/图片，会让编辑按钮不渲染。turnTail 槽位的选择器只能拿到 turn/seq/openFile 三个字段，无法读会话快照，所以入口会在每个已完成轮次上都尝试挂载，由组件内部用快照决定是否显示。证据：`packages/dsh-chat-recovery/src/client/TurnActionsView.tsx:11-15、56-60`、`packages/dsh-chat-recovery/README.zh.md:64-66`。

**Q: 编辑了首轮消息，结果会怎样？**

A: 首轮之前没有可切的 turn/end 前缀，分叉退化为在同一工作区下新建一个空白会话，并把编辑后的文本发过去——原会话不受任何影响。如果当时没有匹配的工作区，会先创建再 connect。代码：`packages/dsh-chat-recovery/src/client/wiring.ts:20-31`（connectBlank 工作区回退）+ `packages/dsh-chat-recovery/src/client/wiring.ts:82-94`（forkAtSeq 为 null 时走空白分支）。

**Q: 自动重试和手动重试怎么触发？**

A: 自动重试仅在满足四个条件时启动：错误码/消息命中可恢复正则（timeout/network/5xx/429/rate-limit/empty/no-response 等）、轮次不含 tool-result/command 节点、不是用户主动停止、不是输出 token 上限；并且 host 端没有同轮次 model-retry 在调度。一旦命中，监督器在 dock 上显示「自动重试 1/5，约 1s 后」并按指数退避自动发起 fork+prompt。其它任何失败——包括不可恢复错误（401/403/402/422/quota/auth/key/permission/invalid/cancel 等）、含工具/命令的轮次、用户停止、max-tokens——都不会自动重试，只在尾部放一个「重试」按钮，每次点击重放一次。判定：`packages/dsh-chat-recovery/src/core/retry-policy.ts:114-122、154-164、172-182`。

**Q: 自动重试正在跑的时候我想取消，怎么做？**

A: dock 上的「取消重试」按钮随时可点，点了之后不再发起后续尝试；同一会话若再次出现新失败才会重新进入自动流程。auto 等待阶段还有「立即重试」按钮，跳过剩余退避直接发起下一次。取消后界面停在失败轮次，不会被强制跳到子分支。代码：`packages/dsh-chat-recovery/src/client/RetryDock.tsx:36-58`、`packages/dsh-chat-recovery/src/core/retry-supervisor.ts:215-219、222-226`。

**Q: 5 次自动重试用完了会怎样？**

A: 监督器进入 exhausted 状态（"已重试 5 次仍失败：原因"），停在原失败轮次上，dock 给出「手动重试」按钮；点一次只会跑一次 fork+prompt（手动模式不走指数退避，也不自动重试）。同时若有自动重试期间的子分支，会自动 open 回原会话，避免用户停留在一个无意义的中间分支上。代码：`packages/dsh-chat-recovery/src/core/retry-supervisor.ts:160-167、305-313`、`packages/dsh-chat-recovery/src/client/RetryDock.tsx:75-90`。

**Q: 会不会在原会话里堆出重复消息？**

A: 不会。每次重试都从失败轮次"之前"的 turn/end 前缀切一个全新子分支（首轮失败退化为同工作区空白会话），仅在该子分支里把原始文本重发一次。原会话保持不变，失败轮次的流式片段也不会进入下一次模型请求。同时如果用户在重试子分支里又自己发了新消息，监督器会立即让位（cancel），避免覆盖用户意图。代码：`packages/dsh-chat-recovery/src/core/retry-supervisor.ts:263-303`、`packages/dsh-chat-recovery/src/core/retry-supervisor.ts:146-149`。

**Q: 关闭浏览器标签页会取消自动重试吗？**

A: 会。自动重试依赖浏览器侧的 setTimeout 调度（与任务看板的 cron 同模型），关闭/刷新标签页即丢失本次监督；下次重新打开会话只能看到手动重试按钮，不会自动继续尝试。原会话和失败轮次的历史由 host 持久化保留，不会丢失。说明：`packages/dsh-chat-recovery/README.zh.md:66-68`。

**Q: 含图片/附件的消息能编辑吗？**

A: 不能。判定逻辑要求 user 消息的所有内容块都是文本块；只要有一条非文本块（图片/附件/文件等），就返回 null 不给编辑。这是出于"无法安全复制进重新提交的提示词"的考量——图片二进制不能简单重发。代码：`packages/dsh-chat-recovery/src/core/transcript.ts:44-52、61-94`。

**Q: 主机自身已经在重试，本插件会重复发起吗？**

A: 不会。turn 上若已有 model-retry 节点且处于 scheduled/started，verdictFor 直接返回 `{action:'none'}` 让本插件静默；只有 host 让出之后，本插件才会接管后续判断。代码：`packages/dsh-chat-recovery/src/core/transcript.ts:168-172`、`packages/dsh-chat-recovery/src/core/retry-policy.ts:176`。

**Q: 卸载插件会不会留下残留？**

A: 不会。本插件不在 host 上写任何持久文件，编辑/重试都通过 fork 创建新会话，原会话不变，失败历史由 host 自己保留。卸载 `dsh plugin --profile web remove @linxin666/dsh-chat-recovery` 后无插件自留状态。说明：`packages/dsh-chat-recovery/README.zh.md:29-39`。

## 上手难度
入门 — 一行命令安装，重启 `dsh web` 即可见尾部「编辑」按钮与 dock 状态行；无需任何配置，原会话始终不受影响。

## 已知问题与限制
- 客户端 turnTail chain 槽位的选择器拿不到会话快照，只能匹配每个已结束轮次；按钮是否显示由组件内部用快照过滤；选择器本身的覆盖范围无法缩小（`packages/dsh-chat-recovery/src/client/TurnActionsView.tsx:11-15`）
- 仅纯文本用户消息可编辑；含图片/附件的消息一律不显示编辑按钮，避免把二进制块复制进重发的提示词（`packages/dsh-chat-recovery/src/core/transcript.ts:44-52`）
- 自动重试依赖浏览器侧的 setTimeout 调度，关闭/刷新标签页即取消监督；下次打开只会看到手动重试按钮，不会自动继续（`packages/dsh-chat-recovery/src/client/index.ts:80-82`、`packages/dsh-chat-recovery/README.zh.md:66-68`）
- 任何含 tool-result/command 节点的轮次只走手动重试，不自动重试；工具副作用只在用户显式点击时重放（`packages/dsh-chat-recovery/src/core/retry-policy.ts:154-164`）
- max-tokens 与用户主动停止的 interrupted 一律只走手动，不自动重试（`packages/dsh-chat-recovery/src/core/retry-policy.ts:158-162`）
- 自动重试到 MAX_EXTRA_RETRIES=5 后停在 failed/exhausted 状态；不再自动尝试，需要用户手动重试或编辑消息（`packages/dsh-chat-recovery/src/core/retry-supervisor.ts:160-167`）
- 编辑首轮消息退化为同工作区空白会话；fork 路径不可用时不再回退到原会话内修改（`packages/dsh-chat-recovery/src/client/wiring.ts:82-94`）
- 客户端 apply 用 globalThis 上的 `__dshChatRecoveryApplied` 做单实例守护；hot-reload 时由 fiber cleanup 释放，但页面级 reload 之前只能有一个生效工厂（`packages/dsh-chat-recovery/src/client/apply-guard.ts:21-30`）

---

This document is auto-generated by [deepseek-plugin.org](https://deepseek-plugin.org). HTML page: [dsh-web-ui](https://deepseek-plugin.org/plugins/zhu1090093659/dsh-web-ui/packages/dsh-chat-recovery)
Wiki generated by AI (model: `MiniMax-M3`)
