Skip to content

fix(lark): 透传发送失败的飞书错误详情 - #1074

Merged
deepcoldy merged 3 commits into
deepcoldy:masterfrom
le0tan:fix/send-lark-error-details
Aug 29, 2026
Merged

fix(lark): 透传发送失败的飞书错误详情#1074
deepcoldy merged 3 commits into
deepcoldy:masterfrom
le0tan:fix/send-lark-error-details

Conversation

@le0tan

@le0tan le0tan commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

改动

飞书 SDK 在 HTTP 400/4xx 时会抛出包含 response.data.code/msg/log_id 的 AxiosError,但 botmux send 原来只输出 err.message,agent 只能看到通用 HTTP 状态。

本 PR 增加脱敏错误格式化:提取 HTTP 方法、API 路径、状态码、飞书业务错误码、描述和 log_id,不输出 Axios config、请求头或 stack。兼容 SDK logger 传入的嵌套数组,并接入普通文本、语音、文档评论、附件和视频发送失败路径。

对于没有 HTTP response 的超时、断连、DNS 等 transport failure,格式化结果额外保留 transport code/message,例如 POST im/v1/messages → ? ECONNREFUSED "connect ECONNREFUSED 127.0.0.1:1"。已有 HTTP/飞书业务错误输出保持不变。

发送错误描述逻辑收敛为 send-dispatch 导出的 describeSendFailure,CLI 与附件/视频路径共用,并通过行为测试直接验证,不再只依赖源码字符串守卫。

影响面

  • 只改变 Lark 发送失败的 CLI/日志错误展示,不改变成功发送、重试、路由或权限逻辑。
  • botmux send 的 sandbox relay 会原样转发 host CLI 的 stderr,因此隔离会话也能看到相同错误详情。
  • 无 response 时只读取 error 的 code/message;仍不序列化 config、headers 或 stack。
  • 其他 CLI 适配器、后端和会话类型没有行为改动。

验证

  • bun run test -- test/lark-error-format.test.ts test/cli-send-dispatch.test.ts test/cli-send-hook-context.test.ts:3 个文件、70/70 通过
  • 覆盖 HTTP 业务错误、ECONNREFUSED、仅 message 的 timeout、普通 Error fallback、附件/视频失败路径
  • 变异验证:移除 helper 对 formatLarkError 的调用后,直接行为测试及附件业务错误测试均失败
  • bun run build:通过
  • bun run daemon:restart:通过

@le0tan
le0tan requested a review from deepcoldy as a code owner August 28, 2026 20:22
@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个改动 🙏 方向我认为是对的——botmux send 失败时只打印 Request failed with status code 400,对 agent 来说确实等于没有信息,而 formatLarkError 早就具备把飞书业务码压成一行的能力,只是 CLI 侧没接上。

我起了本地假飞书服务器 + 真 SDK 复现,确认了 PR 的两个前提都成立:SDK 的 .catch(e => { logger.error(formatErrors(e)); throw e }) 确实把原始 AxiosError 抛给调用方;LoggerProxy 也确实传的是嵌套数组([[filteredErrorInfo, specificError]]),所以数组解包这一层是必要的。顺带一提,这个解包还修好了一个 PR 描述里没提的收益:master 的 daemon.log 也是坏的——数组进不了 formatLarkError,被 safeStringify 打成一坨含用户消息原文的 JSON blob。

下面两点想请你看一下,都是我实际跑出来的:


1. 🔴 无 HTTP 响应的失败,信息会比改之前更少

formatLarkError 的字段全部来自 response。超时 / 连接被断这类失败没有 response,于是只剩一个空壳。用本分支真实的 formatLarkError(不是重写的副本)跑真实 axios 错误:

              master                              →  本 PR
ECONNREFUSED  connect ECONNREFUSED 127.0.0.1:1    →  POST im/v1/messages → ?
timeout       timeout of 300ms exceeded           →  POST im/v1/messages → ?
DNS           getaddrinfo ENOTFOUND …             →  POST im/v1/messages → ?

这不是构造出来的边角:线上日志里有 350 条这类无响应的 [lark] 失败(Client network socket disconnected 291 条、timeout of 15000ms exceeded 51 条、ECONNRESET 3 条),而 Lark 请求超时正好就是 15s(LARK_REQUEST_TIMEOUT_MS = 15_000)。CLI 与 daemon.log 两条路都会退化。

需要说明的是归因→ ? 这个形状是 master 里 formatLarkError 既有的行为,不是这个 PR 写坏的;但这个 PR 把 CLI 的错误新接进了这条路,所以从用户视角看是本 PR 引入的退化。

一个已验证可行的最小修法——在 formatLarkError 末尾,当既没有 HTTP 状态码、也没有业务码时,保留 transport message:

// 既无 HTTP 状态也无业务码 ⇒ transport message 是唯一的信号,必须留下
if (httpStatus == null && typeof code !== 'number') {
  if (typeof v.code === 'string') parts.push(v.code);          // ECONNRESET / ETIMEDOUT
  if (typeof v.message === 'string' && v.message) parts.push(`"${v.message}"`);
}

实测结果(也已验证脱敏不受影响,仍不碰 config/headers/stack):

  • POST im/v1/messages → ? "timeout of 15000ms exceeded"
  • 本 PR 的目标用例逐字不变POST im/v1/messages → 400 code=230022 "content contains sensitive information" log_id=LOGREAL123
  • 非 axios 值仍返回 null,调用方照旧回退

2. 🟠 cli.ts 那条测试目前没有牙

test/cli-send-hook-context.test.ts 断言的是源码字符串,只能看到调用点的写法,看不到函数体的行为。我把 describeSendFailure 里的 formatLarkError(err) ?? 删掉(等于三条 CLI 路径的收益全部作废),4 个文件 70/70 依然全绿

它不是一个空变异:删掉后输出真的会退回 Request failed with status code 400。建议补一条直接测 helper 行为的断言(喂一个 axios 形状的错误,断言输出里有 code= / log_id=),字符串守卫可以保留作为形态防线。

其余三处变异都是真红的,方向没问题:删数组解包 ❌红、还原 send-dispatch 两处 ❌红、还原主发送出口 ❌红。


另外两点供参考(非阻断)

  • 覆盖是完整的:我把 cmdSend 内所有 console.error 过了一遍,3 条飞书发送出口都接上了;剩下的(7646 权限拒绝、8225 VC 索引、9309 feedback 索引)不属于飞书发送失败,保持原样是对的。附件侧只有 file/video 两个 sender,都改到了。
  • 关于「本机 2 个测试失败与本改动无关」:结论成立,但原因可能和你判断的不同——这个分支落后 master 3 个 commit,其中 517cbff37 fix(test): 隔离 Bun 测试用户目录 正是修这个的(Bun 会缓存 os.homedir(),没有它测试会读到真实的 ~/.botmux)。我建了合并树(本 PR + 最新 master)实测,那些失败全部消失,全量只剩 2 个既有 flaky(干净 master 单跑反而失败 5 个)。所以不是你的改动造成的,但合并前建议 rebase 一下。

脱敏、CI(3/3 绿)、编译产物我都核过了,没有问题。


本条为自动评审的初步意见,仅供参考,以维护者审阅结论为准

le0tan added 3 commits August 29, 2026 16:18
sendFileAttachments / sendVideoAttachments 原来把 AxiosError 拍平成
err.message,丢掉了 code/log_id。改为先走 formatLarkError(脱敏,
不含 config/headers/stack)再回退 message,使附件与视频的失败行
(⚠️ 附件/视频未发送 …)与其它发送路径一致携带飞书业务错误码。
@le0tan
le0tan force-pushed the fix/send-lark-error-details branch from 2df6756 to 74cb09c Compare August 29, 2026 08:20
@le0tan

le0tan commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

已按两点建议处理,更新在 74cb09c1(分支也已 rebase 到最新 master):

  • formatLarkError 在既无 HTTP status、也无飞书业务码时保留 transport code/message。ECONNREFUSED 输出包含错误码和连接信息,仅有 message 的 timeout 也会保留;已有 HTTP/业务错误输出逐字不变,仍不触碰 config、headers、stack。
  • describeSendFailure 已收敛到 send-dispatch 并导出,CLI、附件、视频共用。新增直接行为测试,断言业务 code/msg/log_id,不再只靠 cli.ts 源码字符串。
  • 做了变异验证:移除 helper 对 formatLarkError 的调用后,直接行为测试和附件错误测试都会失败。

验证:相关 3 个测试文件 70/70 通过,bun run buildbun run daemon:restart 通过。PR 描述已同步更新。

@deepcoldy
deepcoldy merged commit 2d5c812 into deepcoldy:master Aug 29, 2026
3 checks passed
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.18.7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants