fix(auto-reply): keep docking in direct chats

This commit is contained in:
Vincent Koc
2026-05-01 02:34:39 -07:00
parent 5230b09ca9
commit 4f02a57f65
3 changed files with 33 additions and 0 deletions

View File

@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
- Slack/slash commands: send block-only slash command replies instead of dropping Slack block payloads with no plain-text fallback. Thanks @vincentkoc.
- Telegram/messages: derive fallback text from interactive button/select labels before sending button-only payloads, so Telegram replies are not rejected as empty messages. Thanks @vincentkoc.
- LINE/messages: send quick-reply-only payloads with fallback option text instead of accepting the payload and returning an empty delivery. Thanks @vincentkoc.
- Auto-reply/docking: require `/dock-*` route switches to start from direct chats, so group or channel participants cannot reroute a shared session's future replies into a linked DM. Thanks @vincentkoc.
- Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc.
- Setup/import: honor non-interactive `--import-from` onboarding flags by running the migration import path instead of silently completing normal setup without importing anything. Thanks @vincentkoc.
- Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram.

View File

@@ -55,6 +55,7 @@ function buildDockParams(commandBody: string, ctxOverrides?: Partial<MsgContext>
Provider: "telegram",
Surface: "telegram",
OriginatingChannel: "telegram",
ChatType: "direct",
SenderId: "42",
From: "42",
...ctxOverrides,
@@ -121,6 +122,25 @@ describe("handleDockCommand", () => {
expect(params.sessionEntry?.lastChannel).toBe("telegram");
});
it("rejects group-session docking before it can reroute replies to a linked DM", async () => {
const params = buildDockParams("/dock-discord", {
ChatType: "group",
From: "telegram:group:-100123",
To: "telegram:-100123",
OriginatingTo: "telegram:-100123",
SenderId: "42",
});
const result = await handleDockCommand(params, true);
expect(result).toEqual({
shouldContinue: false,
reply: { text: "Cannot dock to discord: docking is only available from direct chats." },
});
expect(params.sessionEntry?.lastChannel).toBe("telegram");
expect(params.sessionEntry?.lastTo).toBe("42");
});
it("fails closed when no session entry can be persisted", async () => {
const params = buildDockParams("/dock-discord");
params.sessionEntry = undefined;

View File

@@ -38,6 +38,10 @@ function resolveTargetChannelAccountId(
return normalizeOptionalString(plugin?.config.defaultAccountId?.(params.cfg)) || "default";
}
function isDirectDockSource(params: HandleCommandsParams): boolean {
return normalizeLowercaseStringOrEmpty(params.ctx.ChatType) === "direct";
}
function collectSourcePeerCandidates(params: HandleCommandsParams): string[] {
return [
params.ctx.NativeDirectUserId,
@@ -126,6 +130,14 @@ export const handleDockCommand: CommandHandler = async (params, allowTextCommand
reply: { text: `Already docked to ${targetChannel}.` },
};
}
if (!isDirectDockSource(params)) {
return {
shouldContinue: false,
reply: {
text: `Cannot dock to ${targetChannel}: docking is only available from direct chats.`,
},
};
}
const sourceCandidates = buildSourceIdentityCandidates(params, sourceChannel);
if (sourceCandidates.size === 0) {