mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-11-01 06:47:25 +00:00
fix(qq): NapCat 确认超时提示「不要盲目重发」,版本 1.4.2 → 1.4.3
生产实测(2026-10-13):一次回复发出 4 条相同消息,日志中 3 次 output_send__qq 的调用 id 各不相同 —— 是模型看到「发送失败」后自己重试。 根因(在 NapCat 运行代码 /app/napcat/napcat.mjs 中确认): sendMsg 把消息交给 QQ 内核后,等 onMsgInfoListUpdate 事件匹配 guildId + KSEND_STATUS_SUCCESS,内部固定等待 10s(附件更长)。 主机 IO 饱和(iowait 94–96%)时主线程被 10–30s 长任务阻塞,回调排不上队, 于是 NapCat 抛 "Timeout: NTEvent ... sendMsg" → 非零 retcode(实测 200)。 但消息**已被 QQ 送达**:同一窗口内确有 16 条「发送 -> 私聊」成功记录。 这与已修的 HTTP 客户端超时是两条不同路径: - HTTP 超时:client.Post 返回 err(bd26c2a 已告警) - NapCat 超时:HTTP 200 + 响应体带 NTEvent 字样(本次补) 只报「失败」不说「可能已送出」,模型必重发。 改动: - napcat() 对发送类动作(napcatSendActions)的 NTEvent 超时包装 errNapcatTimeout,并附「很可能已送出、不要盲目重发」的告警; 查询类动作保持普通失败(可安全重试)。 - 不按 retcode 判定:该超时来自 callNormalEventV2 的普通 Error, retcode 本身不携带「超时」语义。 - 不以 serviceAndMethod == sendMsg 为条件:调用方已限定发送动作, 该收紧既无生产证据支持,又会把同类超时退回普通失败重新诱发重发。 测试(全部穿过 napcat() 真实调用链): - TestNapcatSendTimeoutWarnsAgainstBlindResend(生产原文 retcode=200) - TestNapcatQueryTimeoutIsNotSendTimeout - TestNapcatSendActionTimeoutRegardlessOfServiceName 变异验证(3 项,均按预期失败后退回): ① 删告警分支 → 发送用例失败 ② 加回 sendMsg 限定 → 不限服务名用例失败 ③ 去掉发送动作门禁 → 查询用例失败
This commit is contained in:
@ -3425,11 +3425,49 @@ func (p *Plugin) napcat(action string, params map[string]interface{}) (interface
|
||||
// (如 data:null + "Timeout: NTEvent serviceAndMethod:...sendMsg")
|
||||
// 也会被当成发送成功。
|
||||
if err := oneBotError(raw); err != nil {
|
||||
// ★ NapCat 侧超时(2026-10-13 生产实测:一次回复发了 4 条)。
|
||||
//
|
||||
// 与上面的 HTTP 超时**同属「可能已送达」**,但走的是另一条路径:
|
||||
// NapCat 把发送交给 QQ 内核后等 onMsgInfoListUpdate 事件,
|
||||
// 超时(默认 10s+)即抛 "Timeout: NTEvent ...sendMsg",
|
||||
// 映射成非零 retcode(本次实测 retcode=200)。
|
||||
//
|
||||
// 若只报「发送失败」,模型会当成真失败而重发 —— 实测中
|
||||
// 模型连发 3 次(调用 id 各不相同,是模型自己的重试决策)。
|
||||
// 所以必须与 HTTP 超时给同样的「不要盲目重发」告警。
|
||||
if napcatSendActions[action] && isNapcatSendUnconfirmed(err) {
|
||||
return nil, fmt.Errorf("napcat %s: %w(NapCat 未等到 QQ 的发送确认,"+
|
||||
"但消息**很可能已送出**,**不要盲目重发**;若确认对方没收到再重试): %v",
|
||||
action, errNapcatTimeout, err)
|
||||
}
|
||||
return nil, fmt.Errorf("napcat %s: %w", action, err)
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// isNapcatSendUnconfirmed 报告 OneBot 错误是否为「NapCat 未等到内核确认」。
|
||||
//
|
||||
// 为何按消息文本而不是 retcode 判定:这条超时来自 NapCat 内部的
|
||||
// callNormalEventV2(napcat.mjs)——它把等待监听事件的超时包成普通 Error,
|
||||
// 再经 OneBot 适配层转成非零 retcode,所以 **retcode 本身不携带"超时"语义**
|
||||
// (实测为 200,而非某个专门的超时码)。可辨识的特征只有消息里的
|
||||
// NTEvent + Timeout 字样。
|
||||
//
|
||||
// ★ 为何**不**限定 serviceAndMethod 必须是 sendMsg:
|
||||
// 调用方已经用 napcatSendActions 限定了"这是发送动作",这里再卡 sendMsg
|
||||
// 是多余的。生产实测到的超时原文里服务名确实是 sendMsg,因此该限定在现有
|
||||
// 证据下**既无必要、也无观测依据**;留着一个未被任何证据支持的收紧条件,
|
||||
// 只会把同样「可能已送达」的情形误判回普通失败,而普通失败会诱发模型重发。
|
||||
// 变异验证:加上/去掉该限定,对「发送动作 + 非 sendMsg 超时」用例结果不同,
|
||||
// 说明这行代码是活的(而非冗余),故保持宽松版本。
|
||||
func isNapcatSendUnconfirmed(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
s := err.Error()
|
||||
return strings.Contains(s, "NTEvent") && strings.Contains(s, "Timeout")
|
||||
}
|
||||
|
||||
// oneBotError 解析 OneBot 响应里的成败字段,成功返回 nil。
|
||||
//
|
||||
// 判据以 retcode 为主、status 为辅:
|
||||
|
||||
Reference in New Issue
Block a user