Files
homeagent-sdk/example/qq/skill/qq-tools/SKILL.md
JianFeeeee bea410f891 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。
2026-10-19 20:27:23 +08:00

86 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: qq-tools
description: QQ 消息收发(NapCat 协议桥接)。本插件**常驻自启**:它在监听外部消息,不能按需加载。
---
# QQ 消息
## 本插件为什么常驻
QQ 插件在 **HTTP 端口上监听 NapCat 推送**。用户发来的 QQ 消息**随时可能到达**,
到达后它立刻注入给 agent。
如果把本插件改成按需加载:
- 内核启动时它不会被拉起
- **没有人会去拉起它** —— 因为没有人知道有消息在等着
- 结果:QQ 消息**静默丢失**,用户以为 agent 不理他
所以 `auto_start` 必须保持默认(true)。这不是性能取舍,是**功能正确性**。
## 工具
| 工具 | 用途 |
|---|---|
| `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 操作 |
## 注意
- 依赖 **NapCat** 服务在运行且已登录。
- 消息可能过期(NapCat 侧有时效),发送失败时会明确报错。
- **不要在群里 @全体成员**、不要连续刷屏。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件**没有**声明 `auto_start: false`,即按契约默认**自启** —— 这是对的,
它必须常驻。所以这里更要小心:`plugin_disable` 会写进持久禁用清单,
让重启后的自启也失效,只能再用 `plugin_enable` 解。)