|
|
cca6fdaa6b
|
fix(qq): 打字指示器挂到正确路径 + 状态限流改为补发而非丢弃
用户反馈两处仍然不对:
「qq插件的通过在线状态展示agent工作状态还是不正确」
「正在输入中显示也不正确」
## 缺陷 1:打字指示器一次都没触发过
诊断日志给出了决定性证据(加临时探针跑真实私聊):
[qq-dbg] before_output: input_source="" qq_uid=<nil>(has=false) extra_keys=0
`extra_keys=0` —— 说明触发的是**另一条路径的 before_output**。
`before_output` 被两个语义共用:
emitResponse(eventloop.go) = 一轮的回复,turn 边界
output.go = 单次 output_send__* 的工具输出
**真正把消息发出去的是后者**,而它构造 stageCtx 时**连 Extra 都没初始化**
(nil)。我把打字指示器挂在它上面,读了永远不存在的 qq_uid。
修法:内核 output.go 的 stageCtx.Extra 补三个键
output_channel 目标通道
output_meta 本次发送的 meta 原文(user_id / group_id)
output_type text / image / file
插件据此在**消息发出前** startTyping,发出后 stopTyping。
群聊不发(QQ 的「正在输入」只在私聊标题栏有意义)。
## 缺陷 2:output.go 路径没有 after_output
它才是"消息真的送达"的时刻,却只发了 before_output。没有配对的收尾,
指示器只能等 30 秒超时自己灭。补上 after_output。
★ 但 after_output 现在被两条路径共用,必须区分:
单次发送 → stopTypingAfterOutput(配对的收尾)
turn 边界 → stopTypingForTurn + auth 清理 + **状态归位「在线」**
状态归位**只能由 turn 边界触发**:一轮可能发多条消息,若每条发完都归位
「在线」,状态会在干活期间反复跳回空闲 —— 那正是"用状态展示工作进度"
失效的原因之一。
## 缺陷 3:限流把期望状态丢掉了
`applyStatusMaybe` 命中 2 秒限流时直接 `return`,**期望状态被丢弃**。
而 stage 之间常常不到 2 秒:
pre_action(学习中) --0.5s--> before_toolcall(搬砖中)
⇒ "搬砖中" 被丢
状态是「当前工作阶段」的**投影**,不是事件流 —— 投影必须最终收敛到最新值。
改为:限流只限制发送频率,被推迟的记进 `pending`,窗口结束后补发**最新**值
(不是补发被拒的那个:窗口内可能连来多个,中间值没有独立意义)。
`restoreStatus` 停机时先取消待补发,否则定时器会在连接拆除后醒来。
## 实测(真实私聊 webhook)
17:58:15 对方正在输入中 → 开始(uid=2198972886,消息发出前)
17:58:15 对方正在输入中 → 结束(uid=2198972886,消息已发出)
状态序列体现出工作阶段:
17:58:05 学习中(限流后补发)
17:58:07 搬砖中
17:58:09 学习中(限流后补发)
17:58:15 搬砖中
17:58:19 在线(turn 边界,任务结束)
## 判据
新增 TestOnlineResetOnlyOnTurnBoundary(变异咬:单次发送也归位 → 报错)。
★ 修了判据自身的假绿:typing_test 的 bodyOf 用"找行首 }"截函数体,
stageAfterOutput 新增 if 分支后,第一个行首 } 是**内层块**的结尾
⇒ 判据只看到前半段 ⇒ 后半段的调用"看不见"、删掉也不咬。
改用括号计数的 funcBody。
|
2026-10-18 18:01:00 +08:00 |
|
|
|
c6c39aa2a8
|
feat(qq): Qzone 说说、群发硬拦截、@全体不算@bot、状态语义修正
## 1. Qzone 说说(send_qzone_msg / delete_qzone_msg)
一次真实的误判,值得记下来:我曾断言「生产 NapCat 不支持发说说」,
依据是探测返回「不支持的Api SendQzoneMsg」——**是我的调用名错了**。
router.ts 里声明的是下划线式 `send_qzone_msg`,我用的是源码**类名**
`SendQzoneMsg`。换对名字后一次成功。
教训:「API 不存在」这个结论必须用真实 action 名验证;源码里的
类名/文件名/方法名都不是调用名。判据 TestQzoneActionNamesAreSnakeCase
把这条钉住了。
本地校验 ugc_right 白名单(1/4/16/64/128)与 16/128 必带 target_uins,
省一次跨进程往返、给模型更直接的反馈。
## 2. 内部委派不得向群聊发送(现场事故)
offload 子 agent 的「分诊汇报」把本该私聊给用户的进度播报**发到了群里**:
17:15:41 executing tool: output_send__qq
17:15:41 handleChannelOutput meta={"group_id": 979911915}
17:15:42 群友质问「又没@你,你就这么确定是找你的?」
根因:子 agent 被授予全部输出通道(AllowedOutputs=nil,用户明确要求
"持有全部输出通道"),于是它能自己选目标——而它只从上下文里看到一个
群号,就选了群。
修法:`beforeToolcall` 在 internal 放行前调用 outputSendGroupTarget,
识别出群目标就写 denial。为什么不放在 handleChannelOutput:ToolHandler
只收 args,拿不到帧上的 comefrom(SDK 契约如此),只有 stage 帧能看到。
## 3. @全体成员不算 @bot
原实现 `if v == botIDStr || v == "all" { return true }` —— 任何人
@全体都会拉起 bot。那不是"在叫 bot",是"在叫所有人":一个是针对性请求,
一个是广播,语义完全不同。
群里误响应的代价不是"多回一句",而是**在未被邀请的场合发言**。
宁可漏响应(用户会再 @ 一次),不能误响应。
## 4. 状态语义按用户定义重排
学习中 = 等待 LLM 搬砖中 = 执行 toolcall 在线 = 空闲
pre_action → 学习中(stepLLM 之前,唯一能提前标记"要等模型"处)
before_toolcall → 搬砖中
after_toolcall → 学习中(工具回来了,又要等 LLM)
before_output → 不改(LLM 已回、即将输出,不是等模型)
after_output → 在线(force,绕防抖)
原先「学习中」挂在 before_output 是错的:那个时刻 LLM 已经返回。
## 5. 「对方正在输入中」按轮驱动
起点从 HTTP 入口(消息刚到)改到 before_output(每轮只触发一次),
终点从 output_send(任何发送都停,含群聊/发给别人)改到 after_output。
身份由 InjectOptions.Payload 携带 `qq_uid`,内核透传到 stageCtx.Extra。
不用全局 p.auth:并发私聊会串号(A 的指示打到 B 身上)。
## 6. 启动日志不再硬编码工具数
`log.Printf("... (%d tools)", p.name, 15)` 硬编码了 15,工具加到 24 后
仍报 15 —— 排查"qzone 装上没"时被误导了好几轮。改成真实计数 p.toolCount。
## 判据(新增 13 条,变异 8/8 咬)
Qzone 5 条、群发拦截 5 条、@门 4 条、状态映射 7 条、打字 7 条。
★ 两个判据自身缺陷被变异抓出并修掉:
· @门判据用字符串找 `}\n\t}` 截分支,而 joined 已 strip 缩进 ⇒ 找不到
⇒ 分支延伸到文件末尾 ⇒ "文件里任何别处的 return"都让它通过。
改用括号计数 ifBlockAt。
· 状态判据只查常量名,`_ = qqStatusStudy` 就蒙混过关。改查真实调用
applyStatus(qqStatusStudy)。
|
2026-10-18 17:39:40 +08:00 |
|
|
|
4a9705ca41
|
feat(hmapdev): 构建期告警「同一 stage 被重复注册」+ qq 合并写法
## 用户指出的关键:告警要给**开发者**看,所以必须在构建期
我第一版把告警放进了工具链生成的 proc_main 模板(插件**运行期**打日志)。
用户直接指出:「本身工具链这个编译器警告就是给开发者看的」——运行期日志
是给运维/用户看的,开发者看不到。已撤销,改为构建期静态检查。
## 缺陷(真实事故,2026-10-18)
插件侧把 stage handler 存在 `map[string]sdk.StageHandler`(stage 名为键)⇒
**同一 stage 注册两次,后者静默覆盖前者**。
QQ 插件当时写成:
RegisterStage(BeforeToolcall, 权限门) ← 被下一条覆盖
RegisterStage(BeforeToolcall, 切状态) ← 实际生效
后果不只是状态不切换 —— **权限门被静默关掉了**(那几版部署里任何人
发消息都能调任何工具)。编译器不报、运行时不报错、内核日志还显示
`2 handlers [qq qq]`(看着完全正常)。只有静态检查能发现。
## 实现
· `stage_dup_check.go`:扫构建目录下的 .go(排除 _test.go 与 z_proc*),
统计每个 stage 常量被 RegisterStage 引用几次,>1 就告警
· 接入 `buildTarget` 开头 ⇒ 所有 target(含 lua)都检查
· **只告警不中断**:存量插件里重复注册可能是本意,直接 fail 会让它们
升 SDK 后无法构建
· `example/qq`:三个重复 stage 合并成单 handler(权限门 + 状态切换)
## 判据(4 条)
· 同 stage 两次 ⇒ 报出来
· 不同 stage 各一次 ⇒ 不误报
· **注释里的示例注册不计入**
· 测试文件 / 工具链生成文件不计入
★ 「注释」这条是这个检查器**第一版自己栽的坑**:直接对全文跑正则,
把插件里「当初写错了、留在注释里当教训」的那几行也统计进去,于是
修好之后照样报「注册了 3 次」(实测)。现在逐行跳 `//` 与跨行 `/* */`。
本项目已多次绊倒在「注释里有、代码里没有」上,这次轮到检查器自己。
## 实测
qq 修好后构建: packaged qq_linux_amd64.hmap(无告警)
故意加一条重复: ⚠️ 警告: stage StageBeforeOutput 注册了 2 次
建议:把多个功能合并进同一个 handler
|
2026-10-18 15:38:52 +08:00 |
|
|
|
61a871cf88
|
feat(qq): 在线状态随内核阶段自动切换 + 手动设置工具
## 用户要的两件事
1. 「在不同内核工作阶段自动切换 QQ 状态,比如 toolcall 阶段切忙碌中、
生成文字阶段切思考中」
2. 失败不阻塞、不重试、只写日志(完全非致命错误)
## 取值以 NapCat 源码为准(不是我猜的)
我最初拿 `status=11` 试探,NapCat 返回 retcode=0 **但那是无效状态**
(它不校验)。拉了源码 `SetOnlineStatus.ts` 的 actionDescription 才拿到
权威取值表:
在线 status=10 ext=0 忙碌 status=50 ext=0
离开 status=30 ext=0 请勿打扰 status=70 ext=0
隐身 status=40 ext=0 搬砖中 status=10 ext=2023
学习中 status=10 ext=1018 听歌中 status=10 ext=1028
## 阶段映射
StageBeforeToolcall → 搬砖中(status=10, ext=2023)
StageBeforeOutput → 学习中(status=10, ext=1018)★ QQ 无"思考中"预置项
StageAfterOutput → 在线 (status=10, ext=0)
`after_toolcall` 不切:工具刚结束还要继续,频繁切换只会闪。
## 失败处理(用户明确要求的设计)
三个钩子**永远 return nil**,不重试:
· 不返回 error ⇒ 内核不会因状态失败而中断工具调用或改写回复
· 不重试 ⇒ NapCat 掉线时不会每轮打 HTTP + 打日志
· 失败只在 applyStatus 记日志(首次 + 每 10 次),不刷屏
## 其它
· 默认**关闭**(`status_auto`):改 QQ 状态对外可见,属于"内核替用户
做决定",与 offload 同类的显式特权。
· 手动工具两个:`qq_set_status`(预置状态,收中英文名)、
`qq_set_diy_status`(自定义 face_id + wording)。
· 防抖:同状态跳过、最短间隔 2s —— stage 每轮都触发,不防抖会打满 NapCat。
· `Stop()` 里恢复在线:否则停机时停在"搬砖中",用户看到一个永久错位
且不知道是谁改的状态。★ 恢复判据用**实际应用过什么**(applied),
不能用 status_auto —— 手动工具设的状态在开关关闭时也要能恢复。
## 判据(5 条,变异 2/2 咬)
· 指向必拒绝的地址时三个钩子仍返回 nil(不阻塞)
· 开关关闭时完全不发请求
· 状态名归一化(含中文别名)
· 阶段映射用**权威取值**(忙碌必须是 50,不是 11)
· 未知状态名被拒且不发请求
## 实机验证(生产 NapCat 192.168.2.106:25570)
qq_set_status busy → map[ext_status:0 status:忙碌 status_num:50]
qq_set_status working → map[ext_status:2023 status:搬砖中 status_num:10]
qq_set_status online → map[ext_status:0 status:在线 status_num:10]
qq_set_status 乱写的 → 未知状态名: 乱写的(拒绝,未发请求)
注:`get_status` 只返回 online/good,不回显具体状态 —— 那是该接口的
特性,不是设置失败。
|
2026-10-18 14:34:17 +08:00 |
|