Skip to content
dsh.fish
Bundle

@lanbaolu/dsh-fail-soft

自动隔离损坏插件,让 DSH 在坏插件面前照常启动:坏插件被禁用、其余插件正常装配,提供隔离管理与一键恢复 UI(deepseek-harness plugin that quarantines broken plugins so the service keeps booting, with status tools, a one-click restore panel and auto patch adaptation)

Source
lanbaolu
stars
9 stars
License
BSD-3-Clause
Updated
Updated 8 hours ago

Readme

# @lanbaolu/dsh-fail-soft

> ✅ **当前状态:核心稳定候选(v0.1.17)**
>
> 仍依赖 DSH 内核补丁:升级 DSH 后请通过 `fail_soft_status` 的 `patch` 字段
> 确认补丁健康状态,若显示 `needs-adaptation` 请先更新 `backup/` 模板再继续使用。

**插件错误自动隔离**:坏插件被禁用、其余插件照常启动,提供隔离管理与恢复 UI。

## 解决的问题

DSH 的插件装配是 fail-loud:bundle 里**任何一个**插件加载/激活失败,整个
`dsh web` 服务就起不来(进程退出,GUI 打不开)。装一个坏插件 = 服务瘫痪,
且报错是一长串内部堆栈。

本插件让服务在坏插件面前**照常启动**:坏插件被自动隔离(写 disabled patch),
其余插件正常装配;隔离列表可查、可一键恢复(工具 + UI 面板)。

## 组成

| 部分 | 文件 | 作用 |
|---|---|---|
| 挂载兜底 | `lib/mount.js` | 被 DSH 内核(`DSH_FAIL_SOFT=1` 时)在 include 树挂载前动态加载:坏插件 → 隔离 → 剔除重试 |
| 运行期服务 | `lib/index.js` | `failSoft` 服务 + `fail_soft_*` 工具 + `/api/fail-soft/*` HTTP API |
| 上下文工具 | `lib/context-utils.js` | `profileDirOf` / 持久化开关读写(零 DSH 依赖) |
| UI 面板 | `lib/client.js` | 设置面板「Fail-soft 隔离」区域:状态 + 一键开关 |
| 第 2 层进程兜底 | `scripts/failsoft-web.mjs`(bin:`dsh-failsoft-web`) | 进程外启动包装器:内核补丁失效/未装配时仍能自己解析崩溃、自己写隔离、退避重拉 |

## 前置条件(一次性)

内核需要"fail-soft 委托插槽"补丁(极小,只做发现与委托,逻辑全在本插件)。

- **npm 安装用户(0.0.9+)**:不需要手动打补丁。npm 包自带 `backup/` 补丁模板,
  插件每次启动时 `heal` 会自动检测;若内核是官方原版会自动打上补丁,
  **首次安装后请重启一次 dsh** 让挂载兜底生效。
- **本地开发**:也可用 `node patch-apply.mjs`(见工作目录 `防止插件错误挂不起服务/`,
  sha256 校验,DSH 更新后重打)。

不装内核补丁时:本插件的运行期服务/UI 仍可用(查隔离、手动隔离、恢复),
但挂载期自动隔离不生效(崩溃发生在任何插件加载之前,纯插件无法拦截)。

## 跟随 DSH 官方更新(内核补丁自愈)

DSH 官方升级 = npx 重新拉包到新的 `~/.npm/_npx/<hash>/` 目录,内核补丁
会被覆盖丢失。本插件每次启动时自动运行 **内核补丁自愈**(`lib/heal.js`):

- **动态定位**实际运行的 DSH 安装(不硬编码 npx hash:从当前进程 argv 推断;
  **0.1.17 起**支持 Desktop App——dsh 内核跑在 Electron 主进程、argv 无内核
  路径时改从 `process.resourcesPath` → `app.asar.unpacked` 定位);
- **检测**补丁状态:`ok`(已打)/ `needs-apply`(丢失,npx 重装同版本)/ `needs-adaptation`(官方改了代码结构);
- **自动重打**:仅当目标与"已知原始版"一致时安全重打(含 `profile-boot-*.js` 文件名 hash 变化的情况);
- **官方改动结构**时报告 `needs-adaptation` 并提示更新 backup/ 模板,**绝不破坏新版代码**。

补丁健康状态可通过 `fail_soft_status` 工具、`/api/fail-soft/status`(`patch` 字段)、
UI 面板(🧩 行)查看。命令行重打:`node patch-apply.mjs`(与插件共用同一套 heal 逻辑)。

> 版本适配:当官方大幅重构挂载链路、自愈报告 `needs-adaptation` 时,需要更新
> `backup/` 里的 orig/patched 模板(抓官方新版 → 重打 → 存模板),通常一个版本一次。

## 安装(装入 profile)

> ⚠️ **命名必须与包名完全一致**:本插件的包名是 **`@lanbaolu/dsh-fail-soft`**(带 scope)。
> profile 的 dependencies key、`bundles` 条目、node_modules 链接、以及
> `cordis.patch.yml` 的 insert `name` **必须全部使用这个完整包名**。
> **不要**因为仓库/目录名是 `dsh-fail-soft` 或 tgz 文件名是
> `lanbaolu-dsh-fail-soft-*.tgz` 就注册成裸名 `dsh-fail-soft` ——
> 那样 DSH 会按 `@lanbaolu/dsh-fail-soft` 找模块而找不到,启动直接
> `ERR_MODULE_NOT_FOUND` 崩溃(2026-08-19 事故根因)。
>
> 推荐使用 DSH 官方安装(自动按包名注册):
> ```bash
> dsh plugin --profile web add @lanbaolu/dsh-fail-soft@0.1.17
> ```
> ⚠️ **请带上版本号**:pnpm 11 有供应链冷却期(`minimumReleaseAge`)与元数据
> 缓存,**刚发布的版本**用不带版本号的 `add` 可能解析到旧版;显式
> `@版本号` 一定拿到指定版本(本行版本号随每次发布同步,见下方当前状态)。

```bash
# 手动装(务必保留完整包名,以 web profile 为例)
#    ~/.dsh/profiles/web/package.json:
#      "dependencies": { "@lanbaolu/dsh-fail-soft": "link:<本目录>" }
#      "dsh": { "profile": { "bundles": [ ..., "@lanbaolu/dsh-fail-soft" ] } }
# 2. 建 junction:node_modules/@lanbaolu/dsh-fail-soft → 本目录
# 3. 构建时已 link 运行时依赖(@deepseek-ai/dsh-app-boot、@deepseek-ai/dsh-tools
#    进本插件的 node_modules),无需额外安装。
```

## 启用 fail-soft

### 方式一:持久化开关(推荐,终端 / App 通用)

> 插件 **0.0.8+** 提供持久化开关,**App 启动(不加载 shell 环境变量)也能用**:
> 内核补丁启动时读取 `~/.dsh/fail-soft.json`,无需手动设置任何环境变量。
>
> **0.1.14 起首次安装自动启用**:开关文件不存在时,插件激活即写入
> `enabled: true`(装防崩溃插件本身即启用同意),下次重启生效;已存在的
> 开关文件一律尊重(含显式 `false`)。装完后无需任何手动开启步骤。
>
> ⚠️ 唯一例外:若安装时**服务已经因坏插件起不来**,插件没有机会激活写
> 开关——先用环境变量进一次安全模式,隔离坏插件后即恢复:
> `DSH_FAIL_SOFT=1 npx @deepseek-ai/dsh web`(App 用户可手动创建
> `~/.dsh/fail-soft.json`,内容 `{"enabled": true}`)。

- **UI(0.0.10+)**:DSH 设置面板 → 「Fail-soft 隔离」区域,一键开关
  (**0.1.15 起**面板注册改走官方 `slots.inject` 契约:设置 slot 声明未就位
  时等待而非报错——修复部分安装上"装完即 `slot 'settings.section' is not
  declared`"崩溃的反馈;即便注册失败也只降级为无面板,核心隔离能力照常);
- 工具:`fail_soft_set_enabled(true)` / `fail_soft_set_enabled(false)`
- API:`POST /api/fail-soft/set-enabled`,body `{ "enabled": true }`
- 效果:写入 `~/.dsh/fail-soft.json`,**重启 dsh 后生效**;
  `fail_soft_status` 的 `switchEnabled` / `enabled` 字段反映当前开关状态。

### 方式二:环境变量(原有)

```bash
DSH_FAIL_SOFT=1 npx @deepseek-ai/dsh web          # 临时
echo 'export DSH_FAIL_SOFT=1' >> ~/.zshrc          # 永久(仅终端启动生效)
```

`DSH_FAIL_SOFT` 取值:`1|true|yes|on`。可用 `DSH_FAIL_SOFT_MODULE` 覆盖
内核加载的挂载模块(默认按包名解析本插件)。环境变量与持久化开关二者
任一开启即生效。

## 使用

- **自动隔离**:坏插件激活失败 → 诊断打印 + 写
  `- id: <entryId>\n  disabled: true`(带 `# quarantined by @lanbaolu/dsh-fail-soft`
  注释)到 profile 的 `cordis.patch.yml` → 剔除重试挂载 → 服务照常起。
  写入经 `mergePatchBlock` 合并(0.1.2 起):patch 是空数组 `[]`(DSH 默认
  无补丁形态)时用条目块**替换**而非追加,产出始终是单个合法 YAML 数组——隔离器
  自己不会再写坏 patch。
  **0.1.14 起分类隔离**:只隔离"真坏插件"。瞬态环境错误(`EADDRINUSE`
  端口被占等)与官方核心组件(`@deepseek-ai/*`)失败**不写持久隔离**,
  改为抛回原始错误 fail-loud——避免把端口冲突误判成坏插件、把官方
  `webserver` 永久隔离导致"服务活着但 GUI 消失"。
- **工具**(模型可直接调用):`fail_soft_status` / `fail_soft_list` /
  `fail_soft_restore` / `fail_soft_quarantine` / `fail_soft_repair`。
- **HTTP API**:`GET /api/fail-soft/status`、`GET /api/fail-soft/list`、
  `POST /api/fail-soft/restore {id}`、`POST /api/fail-soft/quarantine {id,name,reason}`、
  `POST /api/fail-soft/repair`。
- **修复引擎(0.1.8+)**:`fail_soft_repair` 一键修复——补丁丢失自动重打;
  官方改结构自动回滚到官方原版(挂载兜底不生效但服务能起)并给适配指引;
  同时去重 profile patch 里重复的 entry id(集成 dsh-fix / dev_fix_patch 能力)。
- **UI 面板**:web 会话侧栏 conversation.view 显示隔离列表与恢复按钮。
- **恢复**:修复插件后删除 patch 文件里对应条目(或用 restore 工具/UI)。

## 第 2 层:进程级兜底(v0.1.16+)

> 场景:内核补丁被官方大改回滚、或 fail-soft 插件自身没起来(Desktop App 内嵌
> 内核不在 heal 扫描范围)时,挂载兜底(第 1 层)兜不住。第 2 层把兜底搬到
> **进程外**:外层进程与一切插件解耦,捕获 dsh 崩溃输出 → 解析坏插件 →
> **自己写隔离** → 带退避重拉。不依赖内核补丁,也不依赖插件被装配。

```bash
# 拉起 dsh web(进程外兜底,退出码透传)
dsh-failsoft-web
# 透传参数给 dsh web / 指定 profile / 指定 dsh 入口
dsh-failsoft-web -- --port 3080
dsh-failsoft-web --dsh /Applications/DSH\ Desktop.app/.../dsh/lib/bin.js
DSH_PROFILE=desktop dsh-failsoft-web --dsh <desktop bin>   # Desktop 场景
# 调参
dsh-failsoft-web --max-restarts 3 --backoff-ms 2000
```

行为:dsh 正常退出(0) 透传退出;崩溃 + 解析出可隔离插件 → 写 disabled patch
后自动重拉;崩溃 + 无可隔离对象 → 跑 doctor 诊断,最多再给一次机会。

## 已知边界

- 只兜"插件加载/激活失败"。profile 的 `cordis.patch.yml` 用户手写坏(YAML
  语法错):fail-soft 下会**给出诊断并自动从写前备份恢复**(0.1.6 起,
  写前备份 `.bak.<ISO>` + 解析失败自动恢复);非 fail-soft 仍 fail-loud,
  但会提示如何恢复。本插件自己的隔离写入由 `mergePatchBlock` 保证合法
  (0.1.2 起,空数组 `[]` 替换而非追加)。
- 每轮最多隔离一批失败插件并重试,5 轮后放弃(服务以降级树启动,不崩)。
- 挂载期自动隔离需要内核补丁 + `DSH_FAIL_SOFT=1`(崩溃在插件加载前)。
- 运行期工具/API 采用**延迟注册**:bundle 装配可能早于 `tools`/`webServer`
  服务就绪,插件会监听服务注册事件自动补注册;因此重启后 `fail_soft_*`
  工具和 `/api/fail-soft/*` 会随服务就绪自动出现,**不需要手动热重载**。

## 开发者

构建 / 发布(TP 自动上传 + preflight 门禁)/ 回归测试等维护流程见 **`RELEASE.md`**。

Install

dsh plugin --profile web add github:lanbaolu/dsh-fail-soft

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source