# dsh-tool-normalizer (中文文档)

[![dsh-plugin](https://img.shields.io/badge/dsh--plugin-tools-blue)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![npm version](https://img.shields.io/npm/v/dsh-tool-normalizer.svg)](https://www.npmjs.com/package/dsh-tool-normalizer)

> 模型工具调用自愈层：把过去每次都要完整重试一轮的调用失败静默修好，并给你看修复的证据。

[English Documentation (README.md)](./README.md)

![工具自愈看板：自愈成功率 97.7%、自愈 837 次、节省 Token 153.3M](assets/dashboard-hero.png)

## 为什么开发这个插件

每次工具调用失败都要完整重试一轮：报错返回、模型把整段对话上下文重读一遍、再试一次。在大工作区里，一次重试就要重传约 18 万 input token —— 约 5% 的工具失败率，会让相关轮次的 token 开销悄悄翻倍还多，Agent 每隔几分钟就 visible 地卡一下。

本插件挂在 `tools/execute` 流水线上，在失败到达模型之前把它修好：缺 `description` 就补上，忘读文件就代读一次并重试编辑，相对路径就解析，修不好的错误就追加一条可操作的提示。在 192 个真实会话（15,460 次工具调用）中实测：

- 表面错误率从 **7.95% 降到 2.20%**（可见失败减少约 **72%**）。
- 本会发生的错误中约 **82%** 被自动修复（837 次自愈 vs 182 次残留）。
- 每次自愈省掉一次全量上下文重传（中位数约 16 万 input token），累计节省 **约 153M token** —— 约为同期模型实际消耗（133M）的 **115%**，即没有自愈的话 token 开销约为现在的 2.2 倍（估算口径：token-meter 实测请求压力 × 跳过回环数）。

你得到的是一个不再被自己的工具调用绊倒的 Agent —— 外加一个能证明这一点的看板：

![一次文件编辑自愈的前后参数对比](assets/dashboard-trace.png)

## 它为你做什么

1. **在失败前修好调用** —— 参数错位（`command` → `code`、缺 `description`、Markdown 围栏）、Code-Mode 内层调用描述、相对路径与视图范围。
2. **只重试该重试的** —— 文件门禁失败做一次作用域内的读取后重试，范围越界做一次有界重试；锚点丢失类错误绝不盲目重试。
3. **一切可见** —— KPI 卡片、工具/类别排行、可筛选的前后对比流水，在设置面板一键直达：

| 根因排行 | 自愈规则 |
| --- | --- |
| ![工具与类别排行](assets/dashboard-analytics.png) | ![六条自愈规则全部生效中](assets/dashboard-rules.png) |

---

## 📖 前因后果与数据驱动背景

在 DeepSeek Harness 的自主 Agent 循环中，工具调用的鲁棒性直接决定了任务的成功率与流畅度。通过对 **111 个真实持久化会话**（共包含 **11,176 次工具调用**）进行全量统计与诊断，发现共有 **543 次 Tool Call 错误**（整体错误率 **4.86%**，影响了 **53.2%** 的会话）。

深层归因分析揭示了 **四大结构性错误根因**：

1. **`INVALID_ARGS` 参数契约错位 (13.6%, 74 次)**：
   - 模型在调用 `run_code` 时常套用 bash 惯性，传递 `{"command": "..."}` 而非 `{"description": "...", "code": "..."}`。
   - 模型常漏传 `description` 必填字段。
2. **`UNKNOWN_TOOL` Code-Mode 认知惯性冲突 (12.3%, 67 次)**：
   - 当配置启用 Code-Mode 时，系统仅向模型暴露 `run_code` 单一入口。但模型常习惯性直接发起 `read`、`bash`、`write`、`grep` 等独立工具调用，导致直接报 `UNKNOWN_TOOL` 失败。
3. **`CODE_RUN_FAILED` 沙箱内执行异常 (46.8%, 254 次)**：
   - 模型在 JS 模板字符串中嵌套生成复杂多行 shell 脚本或 Python 脚本时，因反引号未转义或换行破裂导致 JS 语法解析失败。
4. **文件系统安全策略门禁 (5.5%, 30 次)**：
   - 未遵循先读后改策略（`FS_NOT_OBSERVED`）、编辑器 `view_range` 行号超界、或者使用了相对路径。

### 线上实际效果（v0.4.0 · 192 会话 / 15,460 次调用）

插件首次生效前（7,182 次调用，错误率 **7.95%**）与生效后（8,289 次调用，错误率 **2.20%**）对比：

- `INVALID_ARGS` 缺描述失败从 74 降到 2（外层 `RUN_CODE_DESC` + 前置 `INNER_DESC` 修复）。
- 以 `CODE_RUN_FAILED` 形式暴露的内层缺 `description` 从 45 降到 0（插件日志中 598 次前置 `INNER_DESC` 成功）。
- `FS_NOT_OBSERVED` 从 10 降到 0（169 次观察后重试成功）。
- `UNKNOWN_TOOL` 减半（71 → 35）但未消除：PTC 折叠调用在 waterfall 之前即被拒绝，任何插件都观测不到；v0.4.0 改为向这类错误追加可直接改写的提示，而不是静默丢弃。
- 残留 `CODE_RUN_FAILED` 语法失败多为安全改写无法猜测的语义错误（把 Python 当 JS 粘贴、调错 API）；v0.4.0 对其追加解析失败提示。

反事实上界：若没有 837 次成功自愈，生效后窗口错误率约为 12.3% 而非 2.20%。节省 Token 累计的是实测避免的重传（token-meter 压力量 × 跳过回环数），非硬编码常量。v0.4.0 起插件自身的嵌套恢复调用不再计入拦截总数，分母仅为面向用户的调用。

---

## 🎯 `dsh-tool-normalizer` 想解决的问题

本插件基于 Cordis 的 `tools/execute` 瀑布流扩展点构建，作为低开销、确定性的前置中间件对模型生成的工具调用进行自动纠偏与自愈，并配套提供 Web UI 诊断与统计看板：

```
       模型发起的 Tool Call
              │
              ▼
   ┌────────────────────────────────────────────────────────┐
   │             dsh-tool-normalizer 插件                   │
   │                                                        │
   │  1. run_code 自动归一化 (command ➔ code, 补全描述)       │
   │  2. 直接调用安全恢复 (保留上下文的嵌套派发)              │
   │  3. 编辑器路径与范围修正 (相对路径、倒置/越界范围)       │
   │  4. 动态精简提示词注入 (按需挂载, 零冗余 Token)          │
   │  5. 实时运行遥测与统计分析追踪器 (Tracker)              │
   └────────────────────────────────────────────────────────┘
              │
              ▼
       尽量恢复可修复错误
              │
              ▼
    [Web UI] 设置面板 ➔ 工具自愈与统计 (实时图表与日志)
```

### 核心功能

- 🛠️ **`run_code` 参数智能自愈**：
  - 自动识别并转换 `{"command": "git status"}` / `{"cmd": "..."}` 为标准的 `run_code` JavaScript 调用。空或非字符串 command 不再自愈为空程序（会静默成功），而是留给宿主大声拒绝。
  - 自动补全缺失或为空的 `description` 字段。
  - 自动剥离误包含的 Markdown 代码块标记（如 ````typescript ... ````）。
  - 仅当内层目标工具的当前 schema 将 `description` 标记为必填时才补全；`read`、`glob`、`grep` 等开放参数工具保持原始参数不变。
- 🌉 **Code-Mode 透明工具桥接**：
  - 当 `UNKNOWN_TOOL` 已经进入 `tools/execute` 且目标工具在当前 Agent 作用域中可见时，插件通过宿主的 `tools.execute()` 重新以嵌套调用派发，保留 Agent、会话、取消信号、上下文和终结状态。可桥接名单覆盖 `bash/read/write/grep/edit/glob/str_replace_editor/job_output/job_kill` 及 `web_fetch/web_search/todo_write/skill/ask_user_question`。
  - 适用范围说明：在 PTC（`code`）折叠模式下，宿主在任何监听器之前拒绝直调，这条路径插件无法仅靠自身拦截；v0.4.0 对这类错误在保留原文的基础上追加一条可直接粘贴的 `run_code` 改写提示。插件也不会直接调用工具定义的 `execute()` 方法。
- 💡 **失败即时提示**（`errorHints`，默认开启）：
  - 对无法恢复的 PTC 直调错误与不可修复的 `run_code` 解析失败，在保留原报错文本的前提下追加一条可操作提示，模型当轮即可纠正。设为 `false` 可保持宿主报错逐字节不变。
- 📐 **编辑器参数与边界纠偏**：
  - 仅对 `str_replace_editor`（拒收相对路径）将相对路径转换为当前会话工作目录下的绝对路径；`edit`/`read`/`write` 族自己会解析，插件不碰。
  - 先做结构性范围修正；当 `str_replace_editor` 返回包含文件行数的越界错误时，按真实行数嵌套重试，并保留 `-1` 到文件末尾的语义。
- 🩹 **文件观察后重试**：
  - 在编辑/写入返回 `FS_NOT_OBSERVED` 或 `FS_STALE_VERSION` 后读取目标文件，再通过宿主标准派发重试一次；锚点丢失类错误（`FS_EDIT_NOT_FOUND`、`FS_AMBIGUOUS_EDIT`）绝不盲目重试，仅预读刷新观察态以便模型下次重试不再被额外拦截；正常调用不会预先增加一次读取。
- 📊 **可视化运行与诊断面板 (Web UI)**：
  - 无缝挂载至 DSH 的 **设置面板（`settings.section`）**。
  - 实时呈现核心 KPI 指标：拦截总数、成功纠正数、纠正尝试成功率 %、未恢复错误数。
  - 工具维度与问题类别的可视化分布进度条。
  - 支持按状态（全部 / 仅看纠偏 / 仅看失败）筛选的实时运行流水明细表，直观对比纠偏前后的输入差异。
  - v0.4.1 界面修复：深色主题下选中的筛选 pill 与页签不再是黑底黑字（改为 tint 底 + 品牌色文字）；未修改的正常放行行改用中性色而非成功绿；规则页新增第 6 张卡片说明失败提示。本文截图均取自真实运行的部署（首图深色、明细浅色）。

---

## 🧭 UI 页面放置位置与设计考量

**挂载位置**：DeepSeek Harness 设置导航页（`settings.section`，ID 为 `tool-normalizer`，序号 `25`）。

### 选址考量：
1. **符合 DSH 官方架构规范**：在 DeepSeek Harness 的 Web UI 规范中，所有系统监控、用量统计（如 `dsh-usage-atlas`）、模型配置与插件管理均统一收纳于设置抽屉（Settings Panel）内。
2. **保持主对话界面纯净**：将诊断与统计收纳于设置页，既不干扰 Agent 主对话流与工作区画布，又可通过侧边栏左下角齿轮图标一键直达。
3. **运维与排障一体化**：开发者可在同一设置视窗内完成模型切换、插件开关以及工具自愈率观察。

---

## 🚀 安装与快速上手

在 DeepSeek Harness 中，插件是按 **组合 Profile**（如 `web`, `headless`, `tui` 等）进行隔离与依赖管理的。

### 第一步：安装插件至目标 Profile

使用全局 `dsh` 命令（或在源码仓库下使用 `pnpm dsh`）：

```sh
# 1. 安装至 Web UI 模式（含设置面板可视化看板）
dsh plugin --profile web add dsh-tool-normalizer
# （若在 deepseek-harness 源码仓库下开发调试）
pnpm dsh plugin --profile web add dsh-tool-normalizer

# 2. 安装至 Headless 自动化模式
dsh plugin --profile headless add dsh-tool-normalizer

# 3. 安装至 TUI 终端交互模式
dsh plugin --profile tui add dsh-tool-normalizer
```

#### 本地开发模式链接（可选）
如果你正在本地修改或测试插件源码：
```sh
pnpm dsh plugin --profile web add ./plugins/dsh-tool-normalizer
```

### 第二步：启动并查看效果

```sh
# 启动 Web 界面
dsh web
# （或源码启动）
pnpm dsh web
```

打开浏览器进入 Harness 界面，点击左下角 **设置 (⚙️)** ➔ **「工具自愈与统计」**，即可实时查看所有工具调用拦截流水、纠偏统计与成功率图表！

---

## ⚙️ 配置项说明

你可以在工作区的 `cordis.patch.yml` 中自定义插件的运行参数：

```yaml
- insert:
    - id: tool-normalizer
      name: dsh-tool-normalizer
      config:
        autoWrapRunCode: true
        autoBridgeDirectTools: true
        autoObserveFiles: true
        autoClampRanges: true
        injectPrompt: true
        errorHints: true
        persistPassthrough: false
```

| 配置字段 | 类型 | 默认值 | 作用说明 |
| :--- | :---: | :---: | :--- |
| `autoWrapRunCode` | `boolean` | `true` | 自动转换 `command` 属性为 `code`，自动补全描述，剥离 Markdown 标记 |
| `autoBridgeDirectTools` | `boolean` | `true` | 仅对已进入 `tools/execute` 的 `UNKNOWN_TOOL` 结果尝试安全嵌套恢复；宿主提前拒绝的调用插件无法拦截 |
| `autoObserveFiles` | `boolean` | `true` | 仅在收到 `FS_NOT_OBSERVED` 后读取目标并重试一次编辑/写入 |
| `autoClampRanges` | `boolean` | `true` | 修正编辑器范围；仅对 `str_replace_editor` 解析相对路径 |
| `injectPrompt` | `boolean` | `true` | 动态向 `systemPrompt` 注册极简工具最佳实践提示词（静态文本，不影响前缀缓存命中） |
| `errorHints` | `boolean` | `true` | 对不可恢复的 PTC/语法错误追加一条可操作提示，原报错文本完整保留 |
| `persistPassthrough` | `boolean` | `false` | 是否将未修改且成功的正常放行调用逐条写入 JSONL；默认仅保留聚合计数，失败和自愈事件仍保留明细 |

成功率只计算实际发生修复/恢复尝试的调用：`healedSuccess / (healedSuccess + healedFailed)`。前置规范化改对、但终错属于另一失败类别时，记为无关的未修复失败而非修复失败，成功率才反映真实修复能力。预检/守卫拒绝等未修复失败计入总数但不进入成功率分母。正常成功放行不会进入详细 JSONL，以避免日志被高频健康调用淹没；其计数写入同目录的 `tool-normalizer-summary.json`（最多每秒落盘一次）。明细日志超 2 MB 自动轮转（保留最新约 1 MB），debug 日志仅记录失败与自愈。

"预估节省 Token"KPI 为每次成功自愈累计的**实测**输入 token：每次修复记为「跳过的模型回环数 × token-meter 请求压力量」（即再多一次请求需重新提交的整段提示词）。它依赖组合中的 `@deepseek-ai/dsh-token-meter`；未挂载时该统计保持为 `0`，不再使用硬编码的单次重试成本。

流水明细对长参数只保留首尾预览，并额外显示实际修改的字段或恢复路径，避免新增字段位于截断区域时看起来没有变化。

---

## 📦 发版与发布指南 (Release & Publishing)

### 方式一：基于 GitHub Actions 自动化发版（推荐）

1. 在 GitHub 仓库设置中配置 npm Token：
   - 进入 **GitHub 仓库 Settings ➔ Secrets and variables ➔ Actions ➔ New repository secret**。
   - Secret 名称：`NPM_TOKEN`，值为开启了 2FA Bypass 权限的 npm Token。
2. 升级版本号并推送 Tag：
   ```sh
   # 升级小版本（patch / minor / major）
   npm version patch

   # 推送分支与 Tags 到 GitHub
   git push origin main --tags
   ```
3. 在 GitHub 页面基于新推送的 Tag 发布 Release，GitHub Actions 流程（`.github/workflows/publish.yml`）将自动运行全套测试、打包并将新版本发布至 npm 官方镜像源！

### 方式二：本地手动发布到 npm

```sh
# 1. 执行全量测试与打包编译检查
npm run check

# 2. 登录 npm 账号（若未登录）
npm login

# 3. 执行发布
npm publish --access public
```

---

## 已知局限

- **不对 `read`/`write` 做路径归一化。** `read`/`write`/`edit` 工具族自己就会按会话工作目录解析相对路径，只有 `str_replace_editor` 会拒收。给前者"治病"会把本来就能成功的调用计入自愈，污染成功率，因此插件有意不碰。

---

## 🧪 单元测试与验证

```sh
# 运行单元测试
pnpm test

# 运行测试并打包产物
pnpm run check
```

---

## 📄 开源许可

MIT © [merenguesL](https://github.com/merenguesL)
