mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
## 现象(线上 2026-09-26 17:57)
用户从 QQ 私聊发来消息,agent 生成了回复也调了 output_send__qq,
但**没填 meta**:
17:57:45 qq_get_message → {user_id: 2198972886, message_type: private}
17:57:46 output_send__qq → 失败:meta 中需要 group_id 或 user_id 字段
17:58:14 output_send__qq_help → 查格式
17:58:14 output_send__qq → ok ← 靠重试成功,整轮耗时 74s
信息内核**本来就有**(输入事件里带着 user_id/group_id),却要模型从
qq_get_message 的返回里手抄进 meta。抄错就失败,失败才去查 _help。
而"回复"这件事的收件人是确定的(= 消息来源),本不该由模型负责。
运气差就不重试:同日 17:15 / 17:21 两次 `tools=[]` —— 模型压根没调
output_send,回复生成了但没发出去,日志连一行告警都没有。
## 改法
meta 缺收件人时,内核从**本轮输入事件**推导后补上。模型只需给内容。
## 边界(都刻意收窄:宁可不补,也不能补错)
- 显式传了 meta ⇒ 原样返回。主动 DM 别人等场景必须保持原行为。
- meta 里已有 group_id/user_id ⇒ 不覆盖。
- meta 是坏 JSON ⇒ 原样返回。让下游报"格式错",而不是被静默替换成
一个模型没要求过的收件人 —— 那比报错更坏:消息会发给错的人。
- 非 qq 通道(如 webui)⇒ 不补。webui 走 ResponseCh,不过 output_send。
其余异步通道(wechat 等)不猜:猜错等于发错人。
- 输入事件里没有收件人信息 ⇒ 留空,让下游按原逻辑报"需要 user_id"。
宁可报错让模型重试,也不要编一个收件人。
- 群消息里 user_id 是**发送者**不是收件人 ⇒ group_id 非 "0" 时优先用
group_id,否则会把消息发回给群成员本人。
数字型 user_id 要按整数格式化:JSON 反序列化成 float64 时
fmt.Sprint 会得到 "2.198972886e+09"。
## 判据:7 条
私聊补 user_id / 群聊补 group_id 且不补 user_id / 显式 meta 不改写 /
已有收件人不覆盖 / 坏 JSON 不静默替换 / webui 不补 / 无信息留空(含 nil evt)。
★ 实现时我先自己写了个 payloadString,编译报错才发现包内已有更完整的
版本(memorypass.go:176,含 float64/int64/int/json.Number 分支),
直接复用 —— 不重复造轮子。
全量 42 包绿。
132 lines
4.8 KiB
Go
132 lines
4.8 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
)
|
||
|
||
// ===== output_send 自动补收件人 =====
|
||
//
|
||
// 线上现象(2026-09-26 17:57):用户从 QQ 私聊发来消息,agent 生成了回复、
|
||
// 也调了 output_send__qq,但**没填 meta**:
|
||
//
|
||
// 17:57:45 qq_get_message → {user_id: 2198972886, message_type: private, ...}
|
||
// 17:57:46 output_send__qq → 失败:meta 中需要 group_id 或 user_id 字段
|
||
// 17:58:14 output_send__qq_help → 查格式
|
||
// 17:58:14 output_send__qq → ok ← 28 秒后靠重试成功
|
||
//
|
||
// 即:信息内核**本来就有**(输入事件里带着 user_id),却要模型从
|
||
// qq_get_message 的返回里手抄进 meta。抄错就失败,失败才去查 _help,
|
||
// 一次本该 5 秒的回复花了 74 秒;运气差就不重试(17:15 / 17:21 那两次
|
||
// tools=[] 压根没调发送,回复静默丢失)。
|
||
//
|
||
// 修法:meta 缺收件人时,内核从**本轮输入事件**自动补。显式传 meta 时
|
||
// 不干预(主动 DM 别人等场景行为不变)。
|
||
|
||
// ---- 判据:meta 补全逻辑 ----
|
||
|
||
// 私聊:补 user_id。
|
||
func TestOutputSendAutoFillsUserIDForPrivateChat(t *testing.T) {
|
||
evt := &agentIO.InputEvent{
|
||
Source: "qq/qq",
|
||
Payload: map[string]interface{}{"user_id": "2198972886", "message_type": "private"},
|
||
}
|
||
meta := autoFillOutputMeta("", "qq", evt)
|
||
if !strings.Contains(meta, "2198972886") {
|
||
t.Errorf("私聊应自动补 user_id,实际 meta=%q", meta)
|
||
}
|
||
// 必须是真的 JSON,不能是拼字符串
|
||
var m map[string]interface{}
|
||
if err := json.Unmarshal([]byte(meta), &m); err != nil {
|
||
t.Fatalf("补出的 meta 不是合法 JSON: %v (meta=%q)", err, meta)
|
||
}
|
||
if m["user_id"] != "2198972886" {
|
||
t.Errorf("user_id 不对: %v", m["user_id"])
|
||
}
|
||
}
|
||
|
||
// 群聊:补 group_id(且不该同时补 user_id —— 群消息有 user_id 是发送者,
|
||
// 不是收件人)。
|
||
func TestOutputSendAutoFillsGroupIDForGroupChat(t *testing.T) {
|
||
evt := &agentIO.InputEvent{
|
||
Source: "qq/qq",
|
||
Payload: map[string]interface{}{"user_id": "111", "group_id": "999", "message_type": "group"},
|
||
}
|
||
meta := autoFillOutputMeta("", "qq", evt)
|
||
var m map[string]interface{}
|
||
if err := json.Unmarshal([]byte(meta), &m); err != nil {
|
||
t.Fatalf("不是合法 JSON: %v (meta=%q)", err, meta)
|
||
}
|
||
if m["group_id"] != "999" {
|
||
t.Errorf("群聊应补 group_id,实际 %v (meta=%q)", m["group_id"], meta)
|
||
}
|
||
if _, ok := m["user_id"]; ok {
|
||
t.Errorf("群聊不该补 user_id(那是发送者不是收件人): %q", meta)
|
||
}
|
||
}
|
||
|
||
// 显式传了 meta 就不动它(主动 DM 等场景必须保持原行为)。
|
||
func TestOutputSendKeepsExplicitMeta(t *testing.T) {
|
||
evt := &agentIO.InputEvent{
|
||
Source: "qq/qq",
|
||
Payload: map[string]interface{}{"user_id": "2198972886"},
|
||
}
|
||
explicit := `{"user_id":"someone_else"}`
|
||
if got := autoFillOutputMeta(explicit, "qq", evt); got != explicit {
|
||
t.Errorf("显式 meta 不该被改写: got=%q want=%q", got, explicit)
|
||
}
|
||
}
|
||
|
||
// meta 里已有 group_id(群聊)时也不该覆盖。
|
||
func TestOutputSendDoesNotOverwriteExistingRecipient(t *testing.T) {
|
||
evt := &agentIO.InputEvent{
|
||
Source: "qq/qq",
|
||
Payload: map[string]interface{}{"user_id": "111", "group_id": "999"},
|
||
}
|
||
explicit := `{"group_id":"777"}`
|
||
if got := autoFillOutputMeta(explicit, "qq", evt); got != explicit {
|
||
t.Errorf("已有收件人时不该覆盖: got=%q want=%q", got, explicit)
|
||
}
|
||
}
|
||
|
||
// meta 是坏 JSON(模型写了错的)时不该崩,也不该当成"没传"而静默覆盖。
|
||
func TestOutputSendBadJSONMetaIsNotSilentlyReplaced(t *testing.T) {
|
||
evt := &agentIO.InputEvent{
|
||
Source: "qq/qq",
|
||
Payload: map[string]interface{}{"user_id": "2198972886"},
|
||
}
|
||
broken := `{"user_id": ` // 截断的 JSON
|
||
got := autoFillOutputMeta(broken, "qq", evt)
|
||
if got != broken {
|
||
t.Errorf("坏 JSON 应原样保留(让下游报格式错,而不是被静默替换成别的东西): got=%q", got)
|
||
}
|
||
}
|
||
|
||
// 非 qq 通道(如 webui)不该被补 —— webui 回复走 ResponseCh,
|
||
// 不经过 output_send 的 meta。
|
||
func TestOutputSendNoAutoFillForWebUI(t *testing.T) {
|
||
evt := &agentIO.InputEvent{
|
||
Source: "webui",
|
||
Payload: map[string]interface{}{"user_id": "123"},
|
||
}
|
||
if got := autoFillOutputMeta("", "webui", evt); got != "" {
|
||
t.Errorf("webui 通道不该补 meta: %q", got)
|
||
}
|
||
}
|
||
|
||
// 输入事件里没有收件人信息时,保持空(让下游按原逻辑报"需要 user_id",
|
||
// 而不是编一个错的收件人)。
|
||
func TestOutputSendNoRecipientInfoStaysEmpty(t *testing.T) {
|
||
evt := &agentIO.InputEvent{Source: "qq/qq", Payload: map[string]interface{}{}}
|
||
if got := autoFillOutputMeta("", "qq", evt); got != "" {
|
||
t.Errorf("无收件人信息时应留空让下游报错,不能编: %q", got)
|
||
}
|
||
// evt 为 nil 也不能崩
|
||
if got := autoFillOutputMeta("", "qq", nil); got != "" {
|
||
t.Errorf("nil evt 应留空: %q", got)
|
||
}
|
||
}
|