|
|
65d2bd9e61
|
fix(qq): NapCat 确认超时提示「不要盲目重发」,版本 1.4.2 → 1.4.3
生产实测(2026-10-13):一次回复发出 4 条相同消息,日志中 3 次
output_send__qq 的调用 id 各不相同 —— 是模型看到「发送失败」后自己重试。
根因(在 NapCat 运行代码 /app/napcat/napcat.mjs 中确认):
sendMsg 把消息交给 QQ 内核后,等 onMsgInfoListUpdate 事件匹配
guildId + KSEND_STATUS_SUCCESS,内部固定等待 10s(附件更长)。
主机 IO 饱和(iowait 94–96%)时主线程被 10–30s 长任务阻塞,回调排不上队,
于是 NapCat 抛 "Timeout: NTEvent ... sendMsg" → 非零 retcode(实测 200)。
但消息**已被 QQ 送达**:同一窗口内确有 16 条「发送 -> 私聊」成功记录。
这与已修的 HTTP 客户端超时是两条不同路径:
- HTTP 超时:client.Post 返回 err(bd26c2a 已告警)
- NapCat 超时:HTTP 200 + 响应体带 NTEvent 字样(本次补)
只报「失败」不说「可能已送出」,模型必重发。
改动:
- napcat() 对发送类动作(napcatSendActions)的 NTEvent 超时包装
errNapcatTimeout,并附「很可能已送出、不要盲目重发」的告警;
查询类动作保持普通失败(可安全重试)。
- 不按 retcode 判定:该超时来自 callNormalEventV2 的普通 Error,
retcode 本身不携带「超时」语义。
- 不以 serviceAndMethod == sendMsg 为条件:调用方已限定发送动作,
该收紧既无生产证据支持,又会把同类超时退回普通失败重新诱发重发。
测试(全部穿过 napcat() 真实调用链):
- TestNapcatSendTimeoutWarnsAgainstBlindResend(生产原文 retcode=200)
- TestNapcatQueryTimeoutIsNotSendTimeout
- TestNapcatSendActionTimeoutRegardlessOfServiceName
变异验证(3 项,均按预期失败后退回):
① 删告警分支 → 发送用例失败 ② 加回 sendMsg 限定 → 不限服务名用例失败
③ 去掉发送动作门禁 → 查询用例失败
|
2026-10-13 12:52:55 +08:00 |
|
|
|
bd26c2a402
|
fix(qq): 发送类端点用长超时客户端 + 校验 OneBot retcode,消除「已发出却报失败」
## 真实故障(2026-10-13,生产日志实证)
从 `<data>/log/week_2026-W41.tar.gz` 解出的历史日志 + `memory/context.json`
拿到完整错误文本:
napcat send_private_msg: Post "http://192.168.2.106:25570/send_private_msg":
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
对应日志时序(~30s 恰好撞上客户端超时):
17:50:07 executing tool: output_send__qq
17:50:47 result: 通过 [qq] 通道发送失败: ... Post "...": ... ← 40s
21:47:15 → 21:47:47 ← 32s
08:17:39 → 08:18:09 ← 30s
**消息其实发出去了**,证据有二:
① agent 自己在上下文里总结(memory/context.json 原文):
「其余几次 send 是 napcat 的 25570 接口超时重试,用户只收到一份,不会刷屏」
「napcat 这会儿接口有点不稳(好几次 context deadline exceeded),
不过关键的那一发是成功的」
② 同一内容重试后用户只收到一份 —— 那是 qq 循环保险拦住的,属侥幸而非正确行为。
根因:`httpClient.Timeout = 30s` 被**查询与发送共用**。而 QQ 发送要经
NapCat 的 `NodeIKernelMsgService/sendMsg`,慢时响应头 30s 回不来 ⇒
把已送达的消息报成失败 ⇒ 模型重试。
## 另一个独立缺陷:不校验 OneBot retcode
`napcat()` 原来只看 HTTP 通不通、JSON 能不能解,**从不看响应体里的
成败字段**。而 OneBot v11 的成败在 body 里:
成功 {"status":"ok","retcode":0,"data":{"message_id":...}}
失败 {"status":"failed","retcode":100,"message":"..."}
或 {"data":null,"message":"Timeout: NTEvent ...sendMsg","status":"failed"}
历史日志里实测到 NapCat 返回 `data:null` + `sendMsg Timeout`(真失败)
却被当作发送成功。两个方向都错,必须靠 retcode 判定。
## 改动
- 新增 `sendClient`(120s + Transport 显式配 `IdleConnTimeout`/`ResponseHeaderTimeout`)。
发送类端点(send_private_msg / send_group_msg / send_msg)走它;查询类
保持 30s(幂等,快速失败更利于排查)。
- `Transport.IdleConnTimeout=30s`:默认 Transport 的空闲连接无上限等待,
复用对端已单向关闭的 keep-alive 连接会得到"请求已写入/对端已处理,
本地读响应 EOF"——又一种「已发送却报失败」。
- `napcat()` 校验 OneBot `retcode`/`status`,失败转 error。
- 超时单独包 `errNapcatTimeout`,错误文案明写「**可能已送达并已发出,
不要盲目重发**」——只说"失败"会诱发模型重发 → 重复消息。
## 判据(4 条,两条经变异测试修正)
- `TestOneBotError_FailedResponses`:7 组 retcode/status 组合的判定
- `TestNapcatConvertsOneBotFailureToError`:**端到端**——napcat() 必须把
OneBot 失败转 error。★ 变异(删掉 napcat() 里的 oneBotError 调用)
实测发现:只测 oneBotError 的判据照样全绿(判据没穿过调用链)
⇒ 补这条才咬住。
- `TestNapcatSendUsesSendClient`:**真的验证路由**(用 Transport 重写 host
观测哪个 client 实际发了请求)。★ 变异(删掉路由分支)实测发现:
只断言两个 client 的 Timeout 字段值照样全绿 ⇒ 重写为观测实际请求。
- `TestNapcatTimeoutErrorIsActionable`:超时错误必须含「可能已送达」
与「不要盲目重发」
27 条判据全绿(含既有 23 条)。
|
2026-10-13 08:02:45 +08:00 |
|
|
|
777c3a0ad3
|
fix(proc,qq)!: shm 模板 v2 —— fExtraBag 透传插件自建 Extra 键;qq 身份可序列化绑帧
内核 shm 桥 v2 前(见主仓同步提交)只透传 4 个具名 Extra 键,插件自建键
(qq 的 qq_auth 绑帧身份)在 stage 之间静默丢失 → before_toolcall 永远回落
可变全局身份 → 权限门绑帧在 proc 插件下整体失效(owner 被误拒/非 owner
可能被误放,2026-10-12 生产实证)。
模板(proc_main.go.tmpl):
- shmStageFieldCount 18→19、shmVersion 1→2(与内核镜像同步)
- fExtraBag:具名 4 键之外的自建键整体 JSON 装袋,read/snapshot/dirty 全链接入
- ReadInto:bag 先打平、具名键后覆盖(具名键永远赢,防御袋污染)
qq 插件:
- qqAuthContext 字段导出 + json tag:非导出字段过 JSON 桥会静默丢空
(原根因链一环)
- authOnFrame 双形态:值形态(进程内/单测)+ map 形态(proc 桥往返,
数字 float64);半截身份拒绝 —— 宁回落全局也不猜(owner 误判即提权)
- authFromMap 按 json tag 重建,逐字段校验类型
判据(plugin_test.go):往返无损/降权保持/半截拒绝(逐键)/帧身份优先于
全局(含 owner 被误拒的原始故障场景反向)。变异 Q1-Q4 全杀。
|
2026-10-12 08:28:41 +08:00 |
|
|
|
b6a6168e65
|
chore(module)!: module 路径随托管地迁移 gitcode → github
SDK 仓已迁到 GitHub 作为权威源,go module 路径同步:
`gitcode.com/JianFeeeee/homeagent-sdk` → `github.com/JianFeeeee/homeagentsdk`
## 范围
74 个文件:71 个 Go 源码 + go.mod/go.sum + hmapdev 模板 + CI 与打包脚本。
主仓(HomeAgent)侧对应改动是 6d38a164。本仓这些文件当时**不在工作树上**
(见下),所以那次的批量替换扫不到它们——这里是补偿。
## 为何这批文件当时不在工作树上(事故与恢复)
10-08 07:58 主仓做 checkout/cherry-pick 序列时,把本仓工作树的 20 个文件
误清理了(`example/*/plugin.go` + `plg.json`,10 个示例)。
根因是**结构误判**:本仓是「一目录两仓库」结构下主仓 `third_party/` 里的
**独立 git 仓库**,而主仓 .gitignore 忽略了 `example/`,于是它在主仓
`git status` 里不可见 —— 看着像「不受跟踪的构建产物」,实际是本仓受跟踪的源码。
已全部恢复(`git checkout -- <D 文件>`,只恢复删除、不动迁移改动),
20 个文件与 HEAD 逐字节一致。
**破坏的连锁后果**:恢复出来的 `plugin.go` 仍写着旧 module 路径,
因为迁移时它们已不在工作树上。所以这次补偿是必须的,不是可选优化。
## 保留 gitcode 引用
以下位置的 `gitcode` **有意保留**,不要"顺手清掉":
- README.md / docs 里「二进制实际分发地址目前是 gitcode」的说明
- mkdocs.yml 的 gitcode 镜像链接(国内直连更快)
- `tools/hmapdev/templates.go` 里「SDK 已从 gitcode 迁到 GitHub」的注释
gitcode 仍是**只读镜像**(源码同步;release 附件暂时仍在那边)。
## 验证
- 本仓根 module:`go build ./...` + `go test ./...` 全通过
- 全部 20 个 example:`go vet` 通过(补偿前 10 个因旧 module 路径失败)
- example/qq:`go test` 通过,且能从源码 build 出可用 plugin 产物
|
2026-10-09 07:25:25 +08:00 |
|
|
|
cfa72df3e9
|
fix(qq): 权限身份改为绑帧,修中断抢占/运行中到达导致的串权与失效
问题(都是插件全局 p.auth 一份状态引起):
- 中断抢占当前轮并把现场压栈,中断轮收尾 afterOutput 清空全局身份;外层
恢复(resumeTask 复用同帧、不重跑 StageOnInput)后 auth.active=false,
beforeToolcall 在 !active 处直接返回 —— 该轮剩余工具调用**完全不受门**。
- 运行中到达的新消息会调 activateAuthContext 改写全局身份,把正在跑的那一轮
换成另一方的身份:换高即越权,换低即误拒。
改法:身份在 StageOnInput 绑定到本帧的 StageContext.Extra 上,beforeToolcall
以帧上身份为准(无绑定时才回退插件全局,兼容单测)。帧随中断栈一起压栈/恢复,
身份自然跟着走。
顺带:合并中断正文里的整批 message_id 现在全部消费(原来只清第一个,其余要等
generation 回收),新增 qqMessageIDsRe 支持 message_id=100,101,102 连写。
新增 4 条测试覆盖:中断恢复、运行中到达、整批 id 消费、非 QQ 轮不受门。
|
2026-09-14 16:45:09 +08:00 |
|
|
|
fe1c4cdb09
|
feat(qq): Bot 所有者消息升到 L2;普通消息保持 L1
jianf:所有者的话不该被普通人的消息打断/挤到队尾。
- 新增 interruptLevel(owner):owner → PriorityL2(一般提醒),其余 → PriorityL1(后台)。
一批里只要有一条来自 owner,整批按 L2 投递。
- 为什么不是 L3:L3 是时钟/终端那类"实时",QQ 是异步消息,抬到 L3 会打断真正实时的工作。
- 新增测试 TestOwnerMessagesGetHigherInterruptLevel 钉住 owner=L2 / 普通人=L1。
|
2026-09-14 16:31:27 +08:00 |
|
|
|
a01fe21ab1
|
feat(qq): 同一会话连续消息合并为一次中断 + 示例 SDK 指回仓库源码
需求(jianf):同一个人连发的数条消息应打包成一次中断,别逐条唤醒 Agent。
- debounce 合并:同一会话 + 同一发送者(群聊按 群号+QQ、私聊按 QQ)在
batch_window_ms(默认 1500)内的连续消息合成一批,每来一条重置计时;
整批不超过 batch_max_ms(默认 30000),避免对方持续刷屏时一直不投。
- n>1 时中断说明「短时间连续发来 N 条」并列出 message_id,建议一次
get_history 拿全上下文;n==1 沿用原文,行为与合并前逐字一致。
- 可配置 batch_window_ms / batch_max_ms,0 = 关闭合并(逐条投递)。
- Stop 时 flush 未到点批次,别把对方消息吞掉。
- 新增 4 条测试(同发送者合并 / 不同发送者不合并 / 窗口 0 逐条 / 单条沿用原文)。
顺带:deepsearch / vikunja 的 go.mod 与 plg.json 此前指向本地安装的 SDK 1.2.0,
导致无法用当前 SDK 重编(缺 InjectOptions.Priority)。改回 ../../ 仓库源码,
与其余示例一致。
|
2026-09-14 16:09:45 +08:00 |
|
|
|
4852d70d77
|
fix(qq): 输出工具不再受"当前会话身份"限制(修「可信 QQ 会话身份不完整」误拒)
现场(用户线上,驻留子联调回执原文):
被**子的中断**唤醒的一轮里,父 agent 调用 output_send__qq(meta 带齐 user_id)被拒:
「QQ 权限策略拒绝工具 output_send__qq:可信 QQ 会话身份不完整;请不要改用其他会话 ID 重试」
根因:`sessionToolArgsAllowed` 对**所有**工具都先要求"本轮能精确匹配可信 OneBot 事件"。
`onInputAuthContext` 在来源是 QQ 但匹配不到可信事件时会降权成
`auth = qqAuthContext{active: true}`(无 peer、非 owner)⇒ `currentPeer == 0`
⇒ 连**输出**也一并被拒 ✗。
但输出是 agent 的**主动调用**:发到哪个会话由它自己给的 meta(group_id / user_id)决定,
`handleChannelOutput` 已经强制要求该字段存在(缺了给明确报错)。再要求"当前会话身份"
是多余的门,而且会把合法发送一起挡掉 —— 设计上收到输入后可以往任意(已授权)通道
发任意多次。
改法:`output_send__qq` 在身份判据**之前**直接放行;「只能访问当前会话」这类限制
保留给**读取类**工具(get_history / mark_read / get_message)—— 那才真的不能跨会话读。
判据 `TestDowngradedAuthStillAllowsQQOutput`:
降权态下输出放行、读取类仍被当前会话限制挡住。
扰动验证:去掉放行分支 ⇒ 该判据报出与现场**一字不差**的那句拒绝。
线上实测:CLI 发起的轮次里 output_send__qq 返回 ok,插件日志 handleChannelOutput 确认送达。
|
2026-09-13 16:03:24 +08:00 |
|
|
|
fd5a291df1
|
feat(qq): 权限门 + 单轮循环保险 + 极简发送回执 (v1.4.0)
回应 problem.md:核心把「发送成功」的富回执喂给模型,模型读成
「这步成功,继续下一步」而重复调用 output_send__qq。
- handleChannelOutput 成功只返回 "ok",不回传 NapCat 原始响应(含 message_id)
- 全局权限门 on_input/before_toolcall/post_action:工具白名单、私人资源边界、
高风险命令 confirm 校验;before_toolcall 的 Response 只用于拒绝单个工具,
post_action 负责清除以免被内核误当作结束推理的最终响应
- 单轮循环保险:max_qq_tool_calls=200 / max_qq_output_calls=20 /
max_duplicate_qq_send=1,只拦参数完全相同的重复调用,不误杀必需的多次调用
- plg.json 1.2.0 → 1.4.0
|
2026-09-10 20:36:43 +08:00 |
|