Revert "fix(agent): output_send 缺收件人时从输入事件自动补"

This reverts commit 6b74862.

## 为什么撤

方案本身是错的,三点:

1. **假设 meta 的收件人字段跨通道通用。** 只实现了 qq(user_id/group_id),
   而 wechat、群聊各有各的字段名。逐通道补全会变成一张靠猜的映射表,
   每加一个通道就得重猜一遍。

2. **假设「回复来源 = 收件人」。** 线上实测直接推翻(2026-09-26 19:05):
   一次请求从 webui 会话发起、却要发到 QQ(input source=webui,
   output channel=qq)。收件人与来源根本不是一回事 —— 用户在
   WebUI 里说"发个 QQ 给我",收件人只能来自上下文或用户明说。
   按 channel 名补,等于用错误的假设去覆盖真实场景。

3. **没解决问题,反而引入新风险。** 19:05:46 那次照样报同样的错
   ("meta 中需要 group_id 或 user_id"),补全逻辑对跨通道场景无效。
   而一旦补错,消息会发给错的人 —— 那比报错坏得多。

## 保留的部分

现象描述与排查结论留在这个 revert 的说明里,便于后续重新设计时
不再重复踩:各通道 meta 结构差异极大,要让插件自己声明收件人来源
(qq 插件知道自己的 user_id 从哪来),内核只转发不猜。

不采用"把格式写进工具描述"这类方案:那只缓解症状,
且各通道格式仍需逐个核实。
This commit is contained in:
JianFeeeee
2026-09-26 19:11:51 +08:00
parent 6b74862118
commit 8bd8271f49
2 changed files with 0 additions and 223 deletions

View File

@ -1,7 +1,6 @@
package core
import (
"encoding/json"
"fmt"
"strings"
@ -171,94 +170,3 @@ func (a *Agent) executeOutputListChannels() string {
}
return strings.Join(parts, "\n")
}
// autoFillOutputMeta 在 meta 缺收件人时,从**本轮输入事件**自动补。
//
// 为何需要(线上实测 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 的返回里手抄一遍。抄错就失败,失败才去查 _help。
// 而"回复"这件事的收件人是确定的(= 消息来源),本不该由模型负责。
//
// 边界(都刻意收窄,宁可不补也不能补错):
// - 显式传了 meta ⇒ 原样返回。主动 DM 别人等场景必须保持原行为。
// - meta 里有 group_id/user_id ⇒ 不覆盖。
// - meta 是坏 JSON ⇒ 原样返回。让下游报"格式错",而不是被静默替换
// 成一个模型没要求过的收件人(那比报错更坏:消息会发给错的人)。
// - 非 qq 通道(如 webui)⇒ 不补。webui 走 ResponseCh,不过 output_send。
// - 输入事件里没<E9878C><E6B2A1>件人信息 ⇒ 留空,让下游按原逻辑报"需要 user_id"。
// 宁可报错让模型重试,也不要编一个收件人。
func autoFillOutputMeta(meta, channel string, evt *agentIO.InputEvent) string {
if meta != "" {
// 已有内容:合法且已含收件人就不动;坏 JSON 也原样返回(见函数注释)
var m map[string]interface{}
if err := json.Unmarshal([]byte(meta), &m); err != nil {
return meta
}
if _, ok := m["group_id"]; ok {
return meta
}
if _, ok := m["user_id"]; ok {
return meta
}
// meta 存在但没收件人:补进去
if id := recipientFromEvent(channel, evt); id != nil {
m = mergeMeta(m, id)
if out, err := json.Marshal(m); err == nil {
return string(out)
}
}
return meta
}
// meta 完全没传:能确定收件人才补
if id := recipientFromEvent(channel, evt); id != nil {
if out, err := json.Marshal(map[string]interface{}{
id.key: id.val,
}); err == nil {
return string(out)
}
}
return ""
}
// metaField 是从输入事件里提取出的收件人字段。
type metaField struct {
key string // "user_id" 或 "group_id"
val string
}
// recipientFromEvent 从输入事件推导收件人。
//
// 只处理 qq 通道:它是唯一一个"meta 必填收件人"的异步通道,且收件人
// 与消息来源一一对应。其它通道(wechat 等)不猜 —— 猜错等于发错人。
func recipientFromEvent(channel string, evt *agentIO.InputEvent) *metaField {
if evt == nil || channel != "qq" {
return nil
}
if evt.Payload == nil {
return nil
}
// 群消息优先:群里的 user_id 是**发送者**,用它当收件人会发错人。
// group_id 存在且非 "0" 时才是群聊(QQ 私聊时 group_id 会出现为 0)。
if gid := payloadString(evt.Payload["group_id"]); gid != "" && gid != "0" {
return &metaField{key: "group_id", val: gid}
}
if uid := payloadString(evt.Payload["user_id"]); uid != "" {
return &metaField{key: "user_id", val: uid}
}
return nil
}
func mergeMeta(m map[string]interface{}, f *metaField) map[string]interface{} {
if m == nil {
m = map[string]interface{}{}
}
m[f.key] = f.val
return m
}

View File

@ -1,131 +0,0 @@
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)
}
}