mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 14:27:30 +00:00
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)。
This commit is contained in:
@ -150,6 +150,36 @@ type InjectOptions struct {
|
||||
// 排队注入(InjectText*/InjectInputSync)没有级别:它们本就是“不需及时处理”
|
||||
// 的那一类,可被任何中断打断。
|
||||
Priority string
|
||||
|
||||
// Payload 是本次注入要额外并入事件 payload 的字段(**不透明**)。
|
||||
//
|
||||
// ## 为什么需要它(2026-10-18)
|
||||
//
|
||||
// QQ 插件的「对方正在输入中」必须只在**自己引起的** before_output 里发,
|
||||
// 而且要知道发给谁。但两条路都断了:
|
||||
//
|
||||
// · `on_input` 的 stageCtx 是局部变量(runStage 完就丢)
|
||||
// · `before_output` 的 stageCtx 是内核**新建**的(emitResponse)
|
||||
// ⇒ 两者**不共享 Extra**,插件拿不到“这轮是谁引起的”
|
||||
//
|
||||
// ## 为什么挂在 InjectOptions 而不是新造一个 Meta 字段
|
||||
//
|
||||
// 第一版我加了 `InjectOptions.Meta` —— 错的。它把同一件事做了两遍:
|
||||
// `InputEvent.Payload` **本来就是通道送入内核的请求体**,身份信息本该
|
||||
// 就在那里。但 `IOInjector` 接口全是 `(source, channel, text, opts)`,
|
||||
// 插件无法直接给 payload 加字段 —— 所以 payload 需要一个扩展口,
|
||||
// 而不是一个平行的新概念。
|
||||
//
|
||||
// 叫 Payload 而不是 Meta 还有一个好处:经内核实测确认,
|
||||
// 它能让 HTTP/CLI/ACP 等**所有注入路径**共用同一套机制。
|
||||
//
|
||||
// ## 内核怎么处理
|
||||
//
|
||||
// 与真 payload 合并(同名键以本字段为准,但**内核保留字不允许覆盖**,
|
||||
// 否则插件能伪造 `context_policy`/`content` 等内核语义),
|
||||
// 然后在 emitResponse 里把**非内核保留键**原样透传到 `stageCtx.Extra`。
|
||||
// 内核不解释其中任何业务字段。
|
||||
Payload map[string]interface{}
|
||||
}
|
||||
|
||||
// 中断优先级取值。
|
||||
|
||||
Reference in New Issue
Block a user