From 877eae9b589ff0c23a94e98d743053f2b6fd0ccb Mon Sep 17 00:00:00 2001 From: brokemac79 Date: Fri, 8 May 2026 19:41:54 +0100 Subject: [PATCH] fix(qqbot): preserve framework command source --- CHANGELOG.md | 1 + .../engine/gateway/outbound-dispatch.test.ts | 40 +++++++++++++++++++ .../src/engine/gateway/outbound-dispatch.ts | 17 +++++++- extensions/qqbot/src/engine/gateway/types.ts | 3 ++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d85afa2f8a..84fe6fa56eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai - Active Memory: support concrete `plugins.entries.active-memory.config.toolsAllow` recall tool names for custom memory plugins while keeping the built-in memory-core default on `memory_search`/`memory_get` and preserving `memory_recall` automatically for `plugins.slots.memory: "memory-lancedb"`. - Telegram: share the grammY API throttler across polling and ad hoc send clients for the same bot token, so visible draft previews and CLI sends use one quota gate. Thanks @anagnorisis2peripeteia. - Telegram/Feishu: honor configured per-agent and global `reasoningDefault` values when deciding whether channel reasoning previews should stream or stay hidden, addressing the preview-default part of #73182. Thanks @anagnorisis2peripeteia. +- QQBot: mark recognized framework slash commands as text-command turns before reply dispatch so `/models`, `/status`, and `/new` responses stay visible in QQ Bot C2C conversations. Fixes #79310. Thanks @rollingshmily. - Docker: run the runtime image under `tini` so long-lived containers reap orphaned child processes and forward signals correctly. (#77885) Thanks @VintageAyu. - Logging/redaction: redact quoted HTTP client secret fields and auth/cookie headers in shared log and formatted error output. Related #71211 and #65623. (#75033) Thanks @liaoandi. - Gateway/SDK: document and stabilize the task ledger RPC surface for `tasks.list`, `tasks.get`, and `tasks.cancel`, including generated Swift model typing for optional task summaries. Thanks @BunsDev. diff --git a/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts b/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts index b7d85c29501..46841edb8d4 100644 --- a/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts +++ b/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts @@ -98,6 +98,7 @@ function makeInbound(overrides: Partial = {}): InboundContext { function makeRuntime(params: { onFinalize?: (ctx: Record) => void; + isControlCommandMessage?: (text?: string, cfg?: unknown) => boolean; onDeliver?: ( deliver: ( payload: { text?: string; audioAsVoice?: boolean }, @@ -164,6 +165,9 @@ function makeRuntime(params: { text: { chunkMarkdownText: (text: string) => [text], }, + commands: { + isControlCommandMessage: params.isControlCommandMessage ?? (() => false), + }, }, tts: { textToSpeech: vi.fn(async () => ({ @@ -228,4 +232,40 @@ describe("dispatchOutbound", () => { ); expect(sendTextMock).not.toHaveBeenCalled(); }); + + it("marks recognized C2C framework slash commands as text commands", async () => { + let finalized: Record | undefined; + const runtime = makeRuntime({ + isControlCommandMessage: (text) => text === "/models", + onFinalize: (ctx) => (finalized = ctx), + }); + + await dispatchOutbound( + makeInbound({ + event: { + type: "c2c", + senderId: "user-openid", + messageId: "msg-models", + content: "/models", + timestamp: "2026-04-25T00:00:00.000Z", + }, + parsedContent: "/models", + userContent: "/models", + userMessage: "/models", + agentBody: "/models", + body: "/models", + commandAuthorized: true, + }), + { runtime, cfg: { commands: { text: true } }, account }, + ); + + expect(finalized).toMatchObject({ + CommandBody: "/models", + CommandAuthorized: true, + CommandSource: "text", + Provider: "qqbot", + Surface: "qqbot", + ChatType: "direct", + }); + }); }); diff --git a/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts b/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts index 771eb7e0b74..5da05f86af8 100644 --- a/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts +++ b/extensions/qqbot/src/engine/gateway/outbound-dispatch.ts @@ -94,7 +94,7 @@ export async function dispatchOutbound( const sendErrorMessage = (errorText: string) => sendErrorToTarget(replyCtx, errorText); // ---- Build ctxPayload ---- - const ctxPayload = buildCtxPayload(inbound, runtime); + const ctxPayload = buildCtxPayload(inbound, runtime, cfg); // ---- Deliver state ---- let hasResponse = false; @@ -512,11 +512,25 @@ export async function dispatchOutbound( // ============ ctxPayload builder ============ +function resolveCommandSource( + inbound: InboundContext, + runtime: GatewayPluginRuntime, + cfg: unknown, +): "text" | undefined { + const commandBody = inbound.event.content; + if (!runtime.channel.commands?.isControlCommandMessage?.(commandBody, cfg)) { + return undefined; + } + return "text"; +} + function buildCtxPayload( inbound: InboundContext, runtime: GatewayPluginRuntime, + cfg: unknown, ): FinalizedMsgContext { const { event } = inbound; + const commandSource = resolveCommandSource(inbound, runtime, cfg); return runtime.channel.reply.finalizeInboundContext({ Body: inbound.body, BodyForAgent: inbound.agentBody, @@ -546,6 +560,7 @@ function buildCtxPayload( QQVoiceAsrReferTexts: inbound.uniqueVoiceAsrReferTexts, QQVoiceInputStrategy: "prefer_audio_stt_then_asr_fallback", CommandAuthorized: inbound.commandAuthorized, + ...(commandSource ? { CommandSource: commandSource } : {}), ...(inbound.voiceMediaTypes.length > 0 ? { MediaTypes: inbound.voiceMediaTypes, diff --git a/extensions/qqbot/src/engine/gateway/types.ts b/extensions/qqbot/src/engine/gateway/types.ts index a2902e8eb7d..68b9aff90e0 100644 --- a/extensions/qqbot/src/engine/gateway/types.ts +++ b/extensions/qqbot/src/engine/gateway/types.ts @@ -42,6 +42,9 @@ export interface GatewayPluginRuntime { peer: { kind: "group" | "direct"; id: string }; }) => { sessionKey: string; accountId: string; agentId?: string }; }; + commands?: { + isControlCommandMessage?: (text?: string, cfg?: unknown) => boolean; + }; reply: { dispatchReplyWithBufferedBlockDispatcher: (params: unknown) => Promise; resolveEffectiveMessagesConfig: (