fix(qqbot): preserve framework command source

This commit is contained in:
brokemac79
2026-05-08 19:41:54 +01:00
committed by Peter Steinberger
parent 2a6239084f
commit 877eae9b58
4 changed files with 60 additions and 1 deletions

View File

@@ -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.

View File

@@ -98,6 +98,7 @@ function makeInbound(overrides: Partial<InboundContext> = {}): InboundContext {
function makeRuntime(params: {
onFinalize?: (ctx: Record<string, unknown>) => 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<string, unknown> | 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",
});
});
});

View File

@@ -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,

View File

@@ -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<unknown>;
resolveEffectiveMessagesConfig: (