From 4852d70d77ef52feeca406cafdc6d62ff3e2e657 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 13 Sep 2026 16:03:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(qq):=20=E8=BE=93=E5=87=BA=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=8F=97"=E5=BD=93=E5=89=8D=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E8=BA=AB=E4=BB=BD"=E9=99=90=E5=88=B6=EF=BC=88?= =?UTF-8?q?=E4=BF=AE=E3=80=8C=E5=8F=AF=E4=BF=A1=20QQ=20=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E4=B8=8D=E5=AE=8C=E6=95=B4=E3=80=8D=E8=AF=AF?= =?UTF-8?q?=E6=8B=92=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现场(用户线上,驻留子联调回执原文): 被**子的中断**唤醒的一轮里,父 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 确认送达。 --- example/qq/plg.json | 2 +- example/qq/plugin.go | 13 +++++++++++++ example/qq/plugin_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/example/qq/plg.json b/example/qq/plg.json index 5141ab3..011b52b 100644 --- a/example/qq/plg.json +++ b/example/qq/plg.json @@ -2,7 +2,7 @@ "name": "qq", "name_zh": "QQ消息", "name_en": "qq", - "version": "1.4.0", + "version": "1.4.1", "description": "QQ 消息收发插件,通过 NapCat 协议桥接", "author": "HomeAgent", "entry": "plugin.so", diff --git a/example/qq/plugin.go b/example/qq/plugin.go index 1dd7144..7e53be8 100644 --- a/example/qq/plugin.go +++ b/example/qq/plugin.go @@ -925,6 +925,19 @@ func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{} if !auth.active || auth.owner { return true, "" } + // 输出工具**不受"当前会话"身份限制**(先于身份判据返回)。 + // + // 为什么:输出是 agent 的**主动调用**,发到哪个会话由它自己给的 meta + // (group_id / user_id)决定 —— handleChannelOutput 会强制要求该字段存在, + // 缺了会得到明确的报错。这里再要求"本轮能精确匹配可信 OneBot 事件"是多余的门, + // 而且会把合法发送一起拒掉:现场(被子的中断唤醒的一轮)父带齐 meta 也发不出去, + // 报「可信 QQ 会话身份不完整」。 + // 「只能访问当前会话」这类限制只对**读取类**工具(get_history / mark_read / + // get_message)成立 —— 那才是真的不能跨会话读。 + if name == "output_send__"+p.name { + return true, "" + } + currentPeer := auth.userID if auth.isGroup { currentPeer = auth.groupID diff --git a/example/qq/plugin_test.go b/example/qq/plugin_test.go index 52f0df6..76e8cde 100644 --- a/example/qq/plugin_test.go +++ b/example/qq/plugin_test.go @@ -202,3 +202,36 @@ func TestZeroLimitsMeanUnlimited(t *testing.T) { } } } + +// 降权(本轮无法精确匹配可信 OneBot 事件 ⇒ auth={active:true}、无 peer、非 owner)时, +// **输出仍必须放行**:发到哪个会话由 agent 自己给的 meta 决定, +// 不该被「当前会话身份」挡住。现场:被子的中断唤醒的一轮里,父带齐 meta 也发不出去 +// (报「可信 QQ 会话身份不完整」)。 +// +// 反之,**读取类**工具在降权时仍受当前会话限制 —— 那才是真的不能跨会话读。 +func TestDowngradedAuthStillAllowsQQOutput(t *testing.T) { + p := newPermissionTestPlugin(t) + p.auth = qqAuthContext{active: true} + p.privateToolAllowlist = []string{"output_send__qq", "qq_get_history"} + p.groupToolAllowlists = map[int64][]string{0: {"output_send__qq", "qq_get_history"}} + + ctx := toolCallContext("output_send__qq", map[string]interface{}{ + "payload": "带齐 meta 的主动发送", + "type": "text", + "meta": `{"user_id":2198972886}`, + }) + if err := p.beforeToolcall(ctx); err != nil { + t.Fatal(err) + } + if ctx.Response != nil { + t.Fatalf("降权时输出被拒: %s", *ctx.Response) + } + + ctx2 := toolCallContext("qq_get_history", map[string]interface{}{"group_id": 1027993713}) + if err := p.beforeToolcall(ctx2); err != nil { + t.Fatal(err) + } + if ctx2.Response == nil || !strings.Contains(*ctx2.Response, "可信 QQ 会话身份不完整") { + t.Fatalf("读取类工具在降权时应被当前会话限制挡住: %#v", ctx2.Response) + } +}