mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 14:27:30 +00:00
fix(qq): 私聊历史拉不到 —— group_id=0 被当成"查群 0"
现场(2026-10-19 内核日志,13 次调用全灭):
用户反复说"gethistory 拉不到私聊消息",纠正多次仍不改。
查日志:qq_get_history 失败 13 次,错误全部是
napcat get_group_msg_history: retcode=200: 消息undefined不存在
而 get_friend_msg_history 一次都没被调用过。
## 根因
handleGetHistory 用 `convInt64(args["group_id"])` 的 **err==nil** 判"调用方给了群号",
而 convInt64 对 0 也成功返回。于是:
模型 qq_get_message 取私聊正文
→ 返回体无条件带 "group_id": 0(QQ 协议里私聊群号就是 0)
→ 模型把字段原样抄进 qq_get_history
→ 代码认为"要查群 0" → get_group_msg_history(group_id=0)
→ NapCat 回「消息undefined不存在」,看起来像历史拉不到
实机验证:直接 POST NapCat
get_group_msg_history {"group_id":0} → retcode=200 消息undefined不存在
get_friend_msg_history {"user_id":…} → retcode=0 正常
## 改动
1. 新增 argPeerID:把 0 判为"未提供"(群号/QQ号都是正整数,0 从不是合法会话)。
权限门用的 argInt64 早有 parsed!=0 检查 —— 本仓此前是「权限门严、取数松」。
2. handleGetHistory 改用它,并区分三种情况:只给 user_id 走私聊、只给 group_id
走群聊、两个都给直接报错(旧实现静默选群分支,把私聊打成群查询)。
3. 同一零值缺陷一并修:handleSendFile(group_id=0 会发往不存在的群)、
handleMarkRead(markChatRead(0) 静默清空不存在的会话)、
handleResolveNickname(group_id=0 打群成员端点)。
4. **msgToGetMsgResult 私聊不再输出 group_id** —— 这是根因的上游。只改下游判据
只能止血,模型仍会一次次把 0 抄回来。
5. schema 写死参数语义:「私聊必须省略 group_id、只传 user_id;不要传 0」。
模型需要知道自己该传什么,否则会反复试错。
6. skill 补「拉私聊历史」整节 + 常见报错对照表;顺带修正一句错述
(原文称"本插件声明 auto_start: false",实际三方都未声明,按契约默认自启)。
7. 新增 history_endpoint_test.go:5 条判据钉住端点选择与返回体契约,
用 httptest 打桩 NapCat,断言真实命中的 action。
## 版本
1.4.3 → 1.4.4。已构建 hmap 并经 plugin_install 装到本机生产实例,
增量重载后 qq 子进程换新、工具全部重注册、skill 移交 skillmgr。
This commit is contained in:
181
example/qq/history_endpoint_test.go
Normal file
181
example/qq/history_endpoint_test.go
Normal file
@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ★ 回归:qq_get_history 的端点选择不能把 group_id=0 当成"查群 0"。
|
||||
//
|
||||
// 现场故障(2026-10-19,内核日志 13 次调用全灭):
|
||||
//
|
||||
// 模型先 qq_get_message 拿到私聊正文,返回体当时无条件带 "group_id": 0
|
||||
// (QQ 协议里私聊的群号就是 0)。它把字段原样抄进 qq_get_history,
|
||||
// 于是后端走 get_group_msg_history(group_id=0),NapCat 回
|
||||
// 「retcode=200 消息undefined不存在」;真正的 get_friend_msg_history
|
||||
// 一次都没被调用过。用户看到的现象是"私聊历史拉不到"。
|
||||
//
|
||||
// 本测试钉住两条:① 0 视为未提供,私聊走私聊端点;② 私聊返回体不再吐 group_id。
|
||||
|
||||
// fakeNapCat 记录被命中的 action,并对历史端点返回一条固定消息。
|
||||
type fakeNapCat struct {
|
||||
mu sync.Mutex
|
||||
actions []string
|
||||
srv *httptest.Server
|
||||
}
|
||||
|
||||
func newFakeNapCat(t *testing.T) *fakeNapCat {
|
||||
t.Helper()
|
||||
f := &fakeNapCat{}
|
||||
f.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
action := strings.TrimPrefix(r.URL.Path, "/")
|
||||
f.mu.Lock()
|
||||
f.actions = append(f.actions, action)
|
||||
f.mu.Unlock()
|
||||
|
||||
// 一条私聊消息:group_id 缺省/为 0,与真实 NapCat 的返回一致。
|
||||
body := map[string]interface{}{
|
||||
"status": "ok",
|
||||
"retcode": 0,
|
||||
"data": map[string]interface{}{
|
||||
"messages": []interface{}{
|
||||
map[string]interface{}{
|
||||
"message_id": 123456,
|
||||
"user_id": 2198972886,
|
||||
"group_id": 0,
|
||||
"message_type": "private",
|
||||
"time": 1792407859,
|
||||
"raw_message": "hello",
|
||||
"sender": map[string]interface{}{"nickname": "—/"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}))
|
||||
t.Cleanup(f.srv.Close)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *fakeNapCat) hits() []string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return append([]string(nil), f.actions...)
|
||||
}
|
||||
|
||||
func newHistoryPlugin(t *testing.T) (*Plugin, *fakeNapCat) {
|
||||
t.Helper()
|
||||
f := newFakeNapCat(t)
|
||||
instance, err := NewPluginFactory("qq", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := instance.(*Plugin)
|
||||
// httpClient 在 Start() 里才建;不经 Start 的单测得自己给,否则 napcat() 空指针。
|
||||
p.httpClient = &http.Client{Timeout: 5 * time.Second}
|
||||
p.napcatURL = f.srv.URL
|
||||
return p, f
|
||||
}
|
||||
|
||||
// group_id=0(私聊消息体里抄来的)必须走私聊端点,而不是群端点。
|
||||
func TestGetHistoryTreatsZeroGroupIDAsAbsent(t *testing.T) {
|
||||
p, f := newHistoryPlugin(t)
|
||||
|
||||
// 模型最容易犯的错:把 get_message 返回体里的 group_id:0 连同 user_id 一起传。
|
||||
if _, err := p.handleGetHistory(map[string]interface{}{
|
||||
"group_id": float64(0),
|
||||
"user_id": float64(2198972886),
|
||||
}); err != nil {
|
||||
t.Fatalf("私聊历史不该报错: %v", err)
|
||||
}
|
||||
|
||||
hits := f.hits()
|
||||
if len(hits) != 1 {
|
||||
t.Fatalf("应只调用 1 次 NapCat,实际 %d 次: %v", len(hits), hits)
|
||||
}
|
||||
if hits[0] != "get_friend_msg_history" {
|
||||
t.Fatalf("私聊必须走 get_friend_msg_history,实际走了 %s", hits[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 只传 group_id=0(没有任何有效会话)必须明确报错,而不是去查"群 0"。
|
||||
func TestGetHistoryRejectsOnlyZeroGroupID(t *testing.T) {
|
||||
p, f := newHistoryPlugin(t)
|
||||
|
||||
_, err := p.handleGetHistory(map[string]interface{}{"group_id": float64(0)})
|
||||
if err == nil {
|
||||
t.Fatal("只传 group_id=0 应报错(0 不是合法会话)")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "user_id") {
|
||||
t.Fatalf("错误信息应引导用 user_id,实际: %v", err)
|
||||
}
|
||||
if hits := f.hits(); len(hits) != 0 {
|
||||
t.Fatalf("参数非法时不该请求 NapCat,实际: %v", hits)
|
||||
}
|
||||
}
|
||||
|
||||
// 群聊传 group_id 仍走群端点(别把正常路径改坏)。
|
||||
func TestGetHistoryGroupStillUsesGroupEndpoint(t *testing.T) {
|
||||
p, f := newHistoryPlugin(t)
|
||||
|
||||
if _, err := p.handleGetHistory(map[string]interface{}{"group_id": float64(123456)}); err != nil {
|
||||
t.Fatalf("群聊历史不该报错: %v", err)
|
||||
}
|
||||
hits := f.hits()
|
||||
if len(hits) != 1 || hits[0] != "get_group_msg_history" {
|
||||
t.Fatalf("群聊应走 get_group_msg_history,实际: %v", hits)
|
||||
}
|
||||
}
|
||||
|
||||
// 两个都给 ⇒ 语义不明,必须报错而不是静默选一个分支。
|
||||
func TestGetHistoryRejectsBothPeers(t *testing.T) {
|
||||
p, f := newHistoryPlugin(t)
|
||||
|
||||
_, err := p.handleGetHistory(map[string]interface{}{
|
||||
"group_id": float64(123456),
|
||||
"user_id": float64(2198972886),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("同时传 group_id 与 user_id 应报错")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "二选一") {
|
||||
t.Fatalf("错误信息应说明二选一,实际: %v", err)
|
||||
}
|
||||
if hits := f.hits(); len(hits) != 0 {
|
||||
t.Fatalf("参数非法时不该请求 NapCat,实际: %v", hits)
|
||||
}
|
||||
}
|
||||
|
||||
// msgToGetMsgResult:私聊不吐 group_id,群聊才吐。防止模型再从返回体里抄 0。
|
||||
func TestMsgToGetMsgResultOmitsGroupIDForPrivate(t *testing.T) {
|
||||
privateMsg := map[string]interface{}{
|
||||
"message_id": float64(1),
|
||||
"user_id": float64(2198972886),
|
||||
"group_id": float64(0),
|
||||
"message_type": "private",
|
||||
"time": float64(1792407859),
|
||||
"raw_message": "hi",
|
||||
}
|
||||
got := msgToGetMsgResult(privateMsg)
|
||||
if _, exists := got["group_id"]; exists {
|
||||
t.Fatalf("私聊返回体不该包含 group_id(会诱导模型回灌 0),实际: %v", got)
|
||||
}
|
||||
|
||||
groupMsg := map[string]interface{}{
|
||||
"message_id": float64(2),
|
||||
"user_id": float64(2198972886),
|
||||
"group_id": float64(123456),
|
||||
"message_type": "group",
|
||||
"time": float64(1792407859),
|
||||
"raw_message": "hi all",
|
||||
}
|
||||
gotGroup := msgToGetMsgResult(groupMsg)
|
||||
if gid, ok := gotGroup["group_id"]; !ok || gid != int64(123456) {
|
||||
t.Fatalf("群聊返回体应包含真实 group_id,实际: %v", gotGroup)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user