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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "qq",
|
"name": "qq",
|
||||||
"name_zh": "QQ消息",
|
"name_zh": "QQ消息",
|
||||||
"name_en": "qq",
|
"name_en": "qq",
|
||||||
"version": "1.4.3",
|
"version": "1.4.4",
|
||||||
"description": "QQ 消息收发插件,通过 NapCat 协议桥接",
|
"description": "QQ 消息收发插件,通过 NapCat 协议桥接",
|
||||||
"author": "HomeAgent",
|
"author": "HomeAgent",
|
||||||
"entry": "plugin.so",
|
"entry": "plugin.so",
|
||||||
|
|||||||
@ -599,8 +599,16 @@ type 枚举: text(文字)/ voice(语音转文字后发送)/ image(图
|
|||||||
RecallPolicy: "auto",
|
RecallPolicy: "auto",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object", "properties": map[string]interface{}{
|
"type": "object", "properties": map[string]interface{}{
|
||||||
"group_id": map[string]interface{}{"type": "integer", "description": "群号(与user_id二选一)"},
|
// ★ 参数语义必须写死「私聊不要传 group_id」。
|
||||||
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号私聊历史(与group_id二选一)"},
|
//
|
||||||
|
// 现场故障(2026-10-19):模型先调 qq_get_message 取私聊正文,
|
||||||
|
// 而私聊消息的返回体带 "group_id": 0(QQ 协议里私聊群号为 0)。
|
||||||
|
// 它随后把整个返回体的字段原样喂给本工具,于是传入 group_id=0,
|
||||||
|
// 后端走了 get_group_msg_history(群0) → NapCat 回「消息undefined不存在」,
|
||||||
|
// 私聊历史永远拉不到。光在代码里把 0 判成缺失还不够 —— 模型仍需
|
||||||
|
// 知道自己该传什么,否则会反复试错。
|
||||||
|
"group_id": map[string]interface{}{"type": "integer", "description": "群号(仅群聊)。私聊必须省略此字段、只传 user_id;不要传 group_id=0 —— 0 不是合法群号,私聊的 group_id 在消息体里就是 0,回灌进来会查不到任何东西。"},
|
||||||
|
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号(仅私聊历史)。私聊请只传这个,不要同时传 group_id。"},
|
||||||
"count": map[string]interface{}{"type": "integer", "description": "拉取条数,默认10"},
|
"count": map[string]interface{}{"type": "integer", "description": "拉取条数,默认10"},
|
||||||
}, "required": []string{},
|
}, "required": []string{},
|
||||||
},
|
},
|
||||||
@ -1212,6 +1220,24 @@ func argInt64(args map[string]interface{}, key string) (int64, bool) {
|
|||||||
return parsed, err == nil && parsed != 0
|
return parsed, err == nil && parsed != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// argPeerID 解析「会话标识」参数(group_id / user_id),把 0 视为**未提供**。
|
||||||
|
//
|
||||||
|
// ## 为什么不能直接用 convInt64 的 err==nil 当「存在」(2026-10-19 实测故障)
|
||||||
|
//
|
||||||
|
// convInt64 对 0 是成功返回的,于是 `if gerr == nil` 会把「没传」和「传了 0」
|
||||||
|
// 混为一谈。真实后果:模型从 qq_get_message 的返回体里读到私聊消息的
|
||||||
|
// `group_id: 0`,原样回灌给 qq_get_history,代码便认为「调用方要查群 0」,
|
||||||
|
// 打 get_group_msg_history(group_id=0),NapCat 回
|
||||||
|
// `retcode=200 消息undefined不存在`;而真正的私聊端点
|
||||||
|
// get_friend_msg_history **一次都没被调用**(内核日志 13 次失败全部如此)。
|
||||||
|
//
|
||||||
|
// 群号/QQ 号都是正整数,0 从来不是合法会话,所以这里必须把 0 判成缺失。
|
||||||
|
// 权限门用的 argInt64 早有 `parsed != 0` 检查 —— 本函数把同一个判据
|
||||||
|
// 补到业务分支上,避免同一份代码里「权限门严、取数松」的错配。
|
||||||
|
func argPeerID(args map[string]interface{}, key string) (int64, bool) {
|
||||||
|
return argInt64(args, key)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}, auth qqAuthContext) (bool, string) {
|
func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}, auth qqAuthContext) (bool, string) {
|
||||||
if !auth.Active || auth.Owner {
|
if !auth.Active || auth.Owner {
|
||||||
return true, ""
|
return true, ""
|
||||||
@ -2221,16 +2247,28 @@ func msgToGetMsgResult(msg map[string]interface{}) map[string]interface{} {
|
|||||||
if mtType == "group" || gid > 0 {
|
if mtType == "group" || gid > 0 {
|
||||||
loc = "群聊"
|
loc = "群聊"
|
||||||
}
|
}
|
||||||
return map[string]interface{}{
|
// ★ 私聊不要输出 group_id。
|
||||||
|
//
|
||||||
|
// 现场故障(2026-10-19):这里无条件写 "group_id": 0,模型取回私聊正文后
|
||||||
|
// 看到 group_id 字段就以为"这个会话的群号是 0",把它原样回灌给
|
||||||
|
// qq_get_history,于是后者打 get_group_msg_history(group_id=0) 拿到
|
||||||
|
// 「消息undefined不存在」,13 次调用全灭(get_friend_msg_history 0 次)。
|
||||||
|
//
|
||||||
|
// 光在下游把 0 判成缺失只能止血,模型仍会一次次误用。**不在返回体里给它
|
||||||
|
// 这个字段**,才是从源头断掉这条错误链:群聊才带 group_id,私聊只带 user_id。
|
||||||
|
result := map[string]interface{}{
|
||||||
"content": content,
|
"content": content,
|
||||||
"message_id": int64(mid),
|
"message_id": int64(mid),
|
||||||
"user_id": int64(uid),
|
"user_id": int64(uid),
|
||||||
"group_id": int64(gid),
|
|
||||||
"nickname": nickname,
|
"nickname": nickname,
|
||||||
"message_type": mtType,
|
"message_type": mtType,
|
||||||
"type": loc,
|
"type": loc,
|
||||||
"time": time.Unix(int64(mt), 0).Format("2006-01-02 15:04:05"),
|
"time": time.Unix(int64(mt), 0).Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
|
if gid > 0 {
|
||||||
|
result["group_id"] = int64(gid)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, error) {
|
func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, error) {
|
||||||
@ -2611,10 +2649,15 @@ func (p *Plugin) ttsToFile(text string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) handleSendFile(args map[string]interface{}) (interface{}, error) {
|
func (p *Plugin) handleSendFile(args map[string]interface{}) (interface{}, error) {
|
||||||
gid, gerr := convInt64(args["group_id"])
|
// 用 argPeerID(0 视为未提供):旧的 convInt64 会把 group_id=0 当成
|
||||||
uid, uerr := convInt64(args["user_id"])
|
// "发往群 0",于是文件发不出去还报得含糊 —— 同一个零值缺陷,见 argPeerID 注释。
|
||||||
if gerr != nil && uerr != nil {
|
gid, hasGroup := argPeerID(args, "group_id")
|
||||||
return nil, fmt.Errorf("need group_id or user_id")
|
uid, hasUser := argPeerID(args, "user_id")
|
||||||
|
if !hasGroup && !hasUser {
|
||||||
|
return nil, fmt.Errorf("需要 group_id(群)或 user_id(私聊):私聊请只传 user_id,不要传 group_id=0")
|
||||||
|
}
|
||||||
|
if hasGroup && hasUser {
|
||||||
|
return nil, fmt.Errorf("group_id 与 user_id 只能二选一")
|
||||||
}
|
}
|
||||||
filePath, _ := args["file"].(string)
|
filePath, _ := args["file"].(string)
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
@ -2646,7 +2689,7 @@ func (p *Plugin) handleSendFile(args map[string]interface{}) (interface{}, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{"message": cqMsg}
|
params := map[string]interface{}{"message": cqMsg}
|
||||||
if gerr == nil {
|
if hasGroup {
|
||||||
params["group_id"] = gid
|
params["group_id"] = gid
|
||||||
return p.napcat("send_group_msg", params)
|
return p.napcat("send_group_msg", params)
|
||||||
}
|
}
|
||||||
@ -2668,11 +2711,12 @@ func (p *Plugin) handleListChats(args map[string]interface{}) (interface{}, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) handleMarkRead(args map[string]interface{}) (interface{}, error) {
|
func (p *Plugin) handleMarkRead(args map[string]interface{}) (interface{}, error) {
|
||||||
if gid, err := convInt64(args["group_id"]); err == nil {
|
// 0 不是合法会话,必须当"未提供"(否则 markChatRead(0) 静默清不存在的会话)
|
||||||
|
if gid, ok := argPeerID(args, "group_id"); ok {
|
||||||
p.markChatRead(gid)
|
p.markChatRead(gid)
|
||||||
return map[string]interface{}{"status": "ok", "group_id": gid, "unread": 0}, nil
|
return map[string]interface{}{"status": "ok", "group_id": gid, "unread": 0}, nil
|
||||||
}
|
}
|
||||||
if uid, err := convInt64(args["user_id"]); err == nil {
|
if uid, ok := argPeerID(args, "user_id"); ok {
|
||||||
p.markChatRead(uid)
|
p.markChatRead(uid)
|
||||||
return map[string]interface{}{"status": "ok", "user_id": uid, "unread": 0}, nil
|
return map[string]interface{}{"status": "ok", "user_id": uid, "unread": 0}, nil
|
||||||
}
|
}
|
||||||
@ -2680,8 +2724,8 @@ func (p *Plugin) handleMarkRead(args map[string]interface{}) (interface{}, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, error) {
|
func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, error) {
|
||||||
gid, gerr := convInt64(args["group_id"])
|
gid, hasGroup := argPeerID(args, "group_id")
|
||||||
uid, uerr := convInt64(args["user_id"])
|
uid, hasUser := argPeerID(args, "user_id")
|
||||||
count := 10
|
count := 10
|
||||||
if c, err := convInt64(args["count"]); err == nil && c > 0 {
|
if c, err := convInt64(args["count"]); err == nil && c > 0 {
|
||||||
count = int(c)
|
count = int(c)
|
||||||
@ -2689,14 +2733,19 @@ func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, err
|
|||||||
|
|
||||||
var endpoint string
|
var endpoint string
|
||||||
var params map[string]interface{}
|
var params map[string]interface{}
|
||||||
if gerr == nil {
|
switch {
|
||||||
|
case hasGroup && hasUser:
|
||||||
|
// 两个都给 ⇒ 语义不明。旧实现静默走群聊分支,把私聊请求打成群号查询,
|
||||||
|
// 用户看到的是 NapCat 的「消息undefined不存在」,根本看不出是参数用错。
|
||||||
|
return nil, fmt.Errorf("group_id 与 user_id 只能二选一:私聊请只传 user_id,群聊请只传 group_id")
|
||||||
|
case hasGroup:
|
||||||
endpoint = "get_group_msg_history"
|
endpoint = "get_group_msg_history"
|
||||||
params = map[string]interface{}{"group_id": gid, "count": count}
|
params = map[string]interface{}{"group_id": gid, "count": count}
|
||||||
} else if uerr == nil {
|
case hasUser:
|
||||||
endpoint = "get_friend_msg_history"
|
endpoint = "get_friend_msg_history"
|
||||||
params = map[string]interface{}{"user_id": uid, "count": count}
|
params = map[string]interface{}{"user_id": uid, "count": count}
|
||||||
} else {
|
default:
|
||||||
return nil, fmt.Errorf("need group_id or user_id")
|
return nil, fmt.Errorf("需要 group_id(群聊)或 user_id(私聊):私聊请只传 user_id,不要传 group_id=0")
|
||||||
}
|
}
|
||||||
|
|
||||||
rawResp, err := p.napcat(endpoint, params)
|
rawResp, err := p.napcat(endpoint, params)
|
||||||
@ -2818,9 +2867,9 @@ func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, err
|
|||||||
result["files"] = files
|
result["files"] = files
|
||||||
}
|
}
|
||||||
// 拉取过某会话历史即视为已读(与真人客户端一致:看过=已读)
|
// 拉取过某会话历史即视为已读(与真人客户端一致:看过=已读)
|
||||||
if gerr == nil {
|
if hasGroup {
|
||||||
p.markChatRead(gid)
|
p.markChatRead(gid)
|
||||||
} else if uerr == nil {
|
} else if hasUser {
|
||||||
p.markChatRead(uid)
|
p.markChatRead(uid)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
@ -2859,8 +2908,8 @@ func (p *Plugin) handleResolveNickname(args map[string]interface{}) (interface{}
|
|||||||
}
|
}
|
||||||
keyword = strings.ToLower(keyword)
|
keyword = strings.ToLower(keyword)
|
||||||
|
|
||||||
gid, groupErr := convInt64(args["group_id"])
|
gid, hasGroupID := argPeerID(args, "group_id")
|
||||||
if groupErr == nil {
|
if hasGroupID {
|
||||||
v, err := p.napcat("get_group_member_list", map[string]interface{}{"group_id": gid})
|
v, err := p.napcat("get_group_member_list", map[string]interface{}{"group_id": gid})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -22,7 +22,43 @@ QQ 插件在 **HTTP 端口上监听 NapCat 推送**。用户发来的 QQ 消息*
|
|||||||
|
|
||||||
| 工具 | 用途 |
|
| 工具 | 用途 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `qq_send`(及消息类工具) | 主动发消息 |
|
| `qq_get_message` | 按 message_id 取单条正文(消息刚到时先用它) |
|
||||||
|
| `qq_get_history` | 按会话拉最近历史(**群聊传 group_id,私聊传 user_id**) |
|
||||||
|
| `qq_list_chats` | 会话列表,按最新消息排序,带未读数与摘要 |
|
||||||
|
| `qq_mark_read` | 清零某会话未读数 |
|
||||||
|
| `output_send__qq` | 主动发消息(群/私聊由 meta 决定) |
|
||||||
|
|
||||||
|
## ★★ 拉私聊历史:只传 user_id,**不要**传 group_id
|
||||||
|
|
||||||
|
这是本插件最容易踩的坑,已经真实咬过人(2026-10-19 连续 13 次调用全失败):
|
||||||
|
|
||||||
|
```
|
||||||
|
qq_get_history(user_id=2198972886) ✅ 拉私聊
|
||||||
|
qq_get_history(group_id=123456) ✅ 拉群聊
|
||||||
|
qq_get_history(group_id=0, user_id=2198972886) ❌ 报「消息undefined不存在」
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么容易错**:`qq_get_message` 取回一条**私聊**消息时,QQ 协议里群号
|
||||||
|
就是 `0`。如果返回体带了这个字段,把它原样抄进 `qq_get_history` 就会去查
|
||||||
|
"群 0" —— 而群 0 不存在,NapCat 只会回一句含糊的
|
||||||
|
`retcode=200 消息undefined不存在`,看起来像"历史拉不到",其实是参数用错。
|
||||||
|
|
||||||
|
现在的行为(已修):
|
||||||
|
|
||||||
|
- 私聊消息的返回体**不再包含 `group_id`** —— 没这个字段就别去抄它
|
||||||
|
- 后端把 `group_id=0` 判为"未提供",走私聊端点
|
||||||
|
- 两个都传会直接报错并说明"只能二选一",而不是静默选错分支
|
||||||
|
|
||||||
|
**判断该传哪个**:看消息来源。私聊注入会带 `user_id=...`;群聊注入带
|
||||||
|
`group_id=...`。拿不准就先 `qq_list_chats`,它会标明每个会话是群还是私聊。
|
||||||
|
|
||||||
|
## 常见报错怎么读
|
||||||
|
|
||||||
|
| 现象 | 含义 | 怎么办 |
|
||||||
|
|---|---|---|
|
||||||
|
| `消息undefined不存在` | 通常是把私聊当群聊查了(group_id=0 或群号错) | 私聊改传 `user_id`,别传 `group_id` |
|
||||||
|
| `message_id 可能已过期` | NapCat 侧短号有时效(约 7 天) | 改用 `qq_get_history` 按会话拉 |
|
||||||
|
| `只能访问当前 QQ 会话` | 权限门:非 owner 只能读本轮会话 | 不是 bug;需要跨会话请让 owner 操作 |
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
@ -44,5 +80,6 @@ QQ 插件在 **HTTP 端口上监听 NapCat 推送**。用户发来的 QQ 消息*
|
|||||||
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
|
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
|
||||||
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
|
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
|
||||||
|
|
||||||
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
|
(本插件**没有**声明 `auto_start: false`,即按契约默认**自启** —— 这是对的,
|
||||||
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)
|
它必须常驻。所以这里更要小心:`plugin_disable` 会写进持久禁用清单,
|
||||||
|
让重启后的自启也失效,只能再用 `plugin_enable` 解。)
|
||||||
|
|||||||
Reference in New Issue
Block a user