DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

exynos967 /

exynos967/dsh-request-error-dump

Verified

DSH 插件:模型请求报错时,把完整请求体 raw JSON 与上游错误响应 raw JSON 一起落盘 | DSH plugin: dump the exact raw request body and the raw upstream error response when a model request fails

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@a8ac6577

dsh-request-error-dump

DSH 插件:模型请求报错时,把这一次的完整请求体 raw JSON 与上游返回的 raw JSON 一起落盘。

上游 401 / 403 / 429 / 400 到底回了什么,DSH 界面通常只给你一句被归一化过的 "本轮运行失败"。本插件把原始报文留下来,方便事后对照。


它记录什么

每次失败写一个 JSON 文件,包含四块内容:

区块 内容
request 请求 URL、方法、请求头(默认脱敏)、请求体原文 body.raw 与解析后的 body.json
response 状态码、状态文案、响应头、上游响应原文 body.raw 与 body.json
transportError 连接被拒、DNS、TLS、超时等传输层异常(此时 response 为 null)
dsh DSH 侧上下文:turn / step / provider / failure.{message,code,status,requestId} / agent 与会话 id / correlation(本次配对靠哪条信号,见下)

真实样例(截断):

{
  "schema": "dsh-request-error-dump/v1",
  "kind": "http-error",
  "capturedAt": "2026-09-22T05:27:46.919Z",
  "durationMs": 15,
  "request": {
    "url": "https://api.deepseek.com/chat/completions",
    "method": "POST",
    "headers": { "content-type": "application/json", "authorization": "<redacted:30 chars>" },
    "body": {
      "present": true,
      "bytes": 83,
      "truncated": false,
      "raw": "{\"model\":\"deepseek-chat\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"stream\":true}",
      "json": { "model": "deepseek-chat", "messages": [{ "role": "user", "content": "hi" }], "stream": true },
      "source": "string"
    }
  },
  "response": {
    "status": 401,
    "statusText": "Unauthorized",
    "headers": { "content-type": "application/json", "x-request-id": "a1b2c3d4" },
    "body": {
      "present": true,
      "bytes": 130,
      "truncated": false,
      "raw": "{\"error\":{\"message\":\"Authentication Fails, Your api key is invalid\",\"type\":\"authentication_error\",\"code\":\"invalid_request_error\"}}",
      "json": { "error": { "message": "Authentication Fails, Your api key is invalid", "type": "authentication_error", "code": "invalid_request_error" } }
    }
  },
  "transportError": null,
  "dsh": {
    "turn": 4,
    "step": 1,
    "provider": "deepseek-official",
    "agent": { "id": "main", "sessionId": "main-session-abc" },
    "failure": { "message": "Authentication Fails", "code": "AUTH", "status": 401, "requestId": "a1b2c3d4" }
  }
}

body.raw 是逐字节的原文,body.json 只是同一份文本的解析视图,方便直接阅读。 用 bodyFormat: raw 可以只留原文,用 bodyFormat: parsed 可以只留解析结果。


开箱即用:默认只抓模型请求

零配置。装好重启后,只有模型(LLM)请求失败才会 dump 到 ~/.dsh/request-error-dump/。

怎么判定"是模型请求"

两条信号,取并集(宁可多抓,不可漏抓):

  1. 权威信号 —— agent/request-error 认领:DSH 循环在每次模型请求尝试失败时派发该事件。 被认领 = 确定是模型请求,无论 URL 长什么样;
  2. 兜底信号 —— 请求结构:POST 且 JSON body 里带字符串 model / modelId, 或 URL 命中少数"模型写在路径里"的形态(Gemini :generateContent、Bedrock /model/<id>/converse)。这一条用来救不走 agent 循环的模型调用(比如会话标题生成)。

为什么不用 URL 白名单?因为会静默漏抓——自定义网关的路径五花八门,调试工具最怕漏。 所以这里刻意反过来:URL 只列了少数几类,其余全靠 body 里的 model 字段和事件认领。

两个时序细节

  • 不是瞬间落盘:默认延迟 flushDelayMs: 1500(1.5 秒)。这个窗口用来等 agent/request-error 来认领并补上 turn/step/provider/failure;窗口内没被认领、 又不像模型请求的,直接丢弃(这就是过滤噪音的机制)。设 0 则立即写、不等上下文;
  • 只抓走到 HTTP 的失败:密钥没配、路由没有适配器(NO_ADAPTER)这类发请求之前 就失败的错误不会产生 dump。

默认排除的端点

excludeUrls 默认含 ['/user/balance']:DSH 桌面版的余额组件每分钟轮询一次它, 密钥失效时一分钟一个 401,50 个保留位约 50 分钟就被占满。模型门禁本来就会挡掉它, 这里再列一次是双保险——万一你设了 modelRequestsOnly: false,它仍然不会被记录。

- id: request-error-dump
  config:
    excludeUrls: ['/user/balance']   # 默认值,可按需增删

为什么监听器要 prepend

agent/request-error 是 waterfall:不调用 next() 的监听器会否决整条链。 dsh-llm-retry 在接管重试时正是返回 { kind: 'retry' } 而不调 next()。 如果本插件按默认顺序注册在它之后,被重试的失败就永远轮不到我们记录。 所以监听器用 { prepend: true } 注册在最前,自己再 return next() 把控制权交回去—— 既不会漏,也不会干扰别人的恢复逻辑(已用真实 Cordis 跑过否决场景验证)。

为什么必须在 wire 层做

DSH 的适配器抛出的错误会被 normalizeLlmFailure 归一化,只保留 message / code / status / providerRetryAfterMs / requestId —— 上游响应原文在这一步就 被丢掉了。所以任何只挂在 agent/request-error 或 llm/stream 上的观察者都拿不到 raw 报文。

真正还同时存在"请求体原文"和"上游响应原文"的地方,只有适配器发起 HTTP 请求的那一刻。 本插件因此包装进程全局的 fetch:

  • 成功路径完全透明:2xx 直接原样返回,响应体一个字都不读,零开销;
  • 失败路径:非 2xx 时先 response.clone() 再读取,调用方自己的响应体仍然可读; 传输层抛错时记录异常并原样重抛;
  • 捕获回调里的任何异常都会被吞掉——记录失败绝不等于请求失败。

安装

本插件零运行时依赖,可直接从目录安装。

# 从 GitHub 安装(推荐)
dsh plugin --profile desktop add github:exynos967/dsh-request-error-dump

# 本地目录(开发用)
dsh plugin --profile desktop add link:D:/dsh-workdir/test/dsh-request-error-dump

装完需要重启 DSH(dsh.profile.bundles 在启动时读取)。

版本提醒:本插件已在 Cordis 4.0.2(DSH 桌面版内置的 @deepseek-ai/cordis)上 完成挂载验证。它只使用 ctx.on、ctx.effect 与进程全局 fetch,不导入任何 @deepseek-ai/* 包,因此不挑 profile,也不受 DSH 小版本升级影响。


配置

所有配置项都有可用默认值,不配也能跑。写在 profile 的 cordis.patch.yml 里覆盖该行:

- id: request-error-dump
  config:
    outDir: ~/.dsh/request-error-dump
    maxFiles: 50
    maxCaptureBytes: 0
    bodyFormat: both
    redactHeaders: ['authorization', 'api-key', 'cookie']
    flushDelayMs: 1500
    ignoreAborted: true
    modelRequestsOnly: true
    excludeUrls: ['/user/balance']
    log: true
选项 默认值 说明
outDir $DSH_HOME/request-error-dump(即 ~/.dsh/request-error-dump) 落盘目录,支持 ~
maxFiles 50 保留最新的 N 个 dump,更早的自动清理;0 表示不清理
maxCaptureBytes 0 单个体积上限(字节);默认 0 = 不截断,保持完整。截断时会标记 truncated: true 并跳过 json 视图
bodyFormat both raw 只留原文 / parsed 只留解析结果 / both 都留
redactHeaders authorization、proxy-authorization、api-key、x-api-key、x-auth-token、x-goog-api-key、cookie、set-cookie 要脱敏的请求头/响应头名;设为 [] 即完全原样
flushDelayMs 1500 延迟落盘窗口,用来等 agent/request-error 补上 DSH 上下文;0 表示立即写、不等上下文
modelRequestsOnly true 只记录模型请求。true 时:被 agent/request-error 认领的,或请求结构像模型调用的(POST + body 带 model/modelId,或 URL 命中 Gemini/Bedrock 形态)才写;其余在窗口结束时丢弃。设 false 恢复"抓所有失败的 HTTP"
excludeUrls ['/user/balance'] 正则列表,对 "METHOD URL" 求值,命中即不记录(在模型门禁之前生效)。设为 [] 取消默认排除
ignoreAborted true 用户主动取消(AbortError)不记录
log true 每次落盘在 host 日志里打一行提示

落盘与保留

  • 文件名形如 2026-09-22T05-27-46-919Z-0001.json,时间戳前缀保证字典序即时间序;
  • 先写 .tmp 再 rename,不会出现半截文件;
  • 超过 maxFiles 后按时间从旧到新清理;并发写入的清理串行化,不会误删新文件;
  • 插件卸载/重载时会把还在等待窗口里的记录刷完,不会丢最后一次报错。

⚠️ 隐私边界

请求体里就是你的完整上下文:系统提示词、源码、工具结果、对话历史,可能还有你贴进去的 密钥。插件默认只脱敏请求头里的凭据,正文一律原样保存。

  • 别在不可信机器上长期开启,也别把 outDir 指向同步盘;
  • 分享 dump 给他人或提交 issue 前请人工检查;
  • 想连请求头也原样保留,设 redactHeaders: [](危险,默认不这么做)。

它不做什么(诚实边界)

  • "模型请求"的判定不是 100% 精确:包装层是进程全局的,判定靠事件认领 + 请求结构。 极端情况下(自定义网关路径 + body 里没有 model 字段 + 该调用又不走 agent 循环) 可能漏抓,此时把 modelRequestsOnly 设为 false 可退化为全抓;
  • 不抓 provider 原生 HTTP 报文的全貌:抓到的是适配器真正 fetch 出去的那一份 body 与真正回来的那一份响应体,但不含 DNS/TLS 层信息、重定向链、以及适配器内部的 重试次数;
  • 不抓流中途失败:HTTP 200 之后 SSE 流中断/解析失败不会触发落盘(那需要缓冲整条 流,代价太高);
  • 不抓 FormData / Blob / ReadableStream 请求体:这些 body 一旦读取就被消费,dump 里 会标记 unavailable 说明原因;
  • dsh 区块的关联是启发式的:按 requestId → 状态码+模型形态 → 模型形态 → 状态码 → 最近一条 的顺序配对,并把实际命中的那条信号写进 dsh.correlation (requestId / status+model / model / status / newest),不假装确定。 命中 newest 时说明只是兜底猜测;dump 里的 URL 与时间戳始终是准的;
  • 不改动任何请求:纯观察者,不重试、不修改、不注入。

工作原理

监听器注册在插件自己的上下文上(无 scope 标记)。核对过 @deepseek-ai/dsh-scope 的 scopeTarget 过滤器:scopeOf(ctx) === undefined 时一律放行,因此根级监听器能收到 每个 agent 的 agent/request-error,不需要 inject 任何服务。同时 agent/request-error 是 waterfall —— Cordis 明确规定"不调用 next() 的监听器会否决整条链",所以本插件 永远 return next(),绝不干扰 dsh-llm-retry 这类拥有恢复权的监听器。

适配器 fetch(init.body = JSON.stringify(payload))
        │
        ▼
globalThis.fetch 包装层          ← 只在此处两段原文同时存在
        │  2xx → 原样返回(不读 body)
        │  非 2xx / 抛错 → 克隆响应、组装 record
        ▼
dump-store 待写队列(flushDelayMs 窗口)
        │  agent/request-error 到达 → 按 requestId/status 认领并补上 dsh 区块
        ▼
原子写入 $DSH_HOME/request-error-dump/*.json → 按 maxFiles 清理

模块划分(每个文件一个职责):

文件 职责
index.js Cordis 插件入口:配置合并、装配、卸载
src/paths.js DSH_HOME / ~ 路径解析
src/record.js 请求头归一化与脱敏、body 读取/截断/解析、record 组装
src/dump-store.js 待写队列、关联认领、原子写入、保留清理
src/fetch-capture.js globalThis.fetch 包装层(可重复安装、可还原)
src/annotate.js agent/request-error 监听器(纯观察者,始终 return next())

测试

npm test                      # 32 个单元/端到端用例,零依赖
node test/cordis-integration.mjs   # 在真实 @deepseek-ai/cordis 上挂载验证

cordis-integration.mjs 会在本机找到 DSH 安装目录时,用真实 Cordis 运行时加载插件、 触发一次真实 waterfall 派发并校验 dump 内容;找不到时自动跳过。可用 DSH_APP_ROOT 指定安装路径。


License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit a8ac6577b897

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout