mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:40:44 +00:00
fix(discord): pin text dm main route owner
This commit is contained in:
@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Discord: keep text-DM main-session route updates pinned to the configured DM owner, matching component interactions so another direct-message sender cannot redirect future main-session replies. 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.
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveEnvelopeFormatOptions,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
import { buildPendingHistoryContextFromMap } from "openclaw/plugin-sdk/reply-history";
|
||||
@@ -13,7 +14,7 @@ import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/sess
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveDiscordConversationIdentity } from "../conversation-identity.js";
|
||||
import { ChannelType } from "../internal/discord.js";
|
||||
import { normalizeDiscordSlug } from "./allow-list.js";
|
||||
import { normalizeDiscordAllowList, normalizeDiscordSlug } from "./allow-list.js";
|
||||
import { resolveTimestampMs } from "./format.js";
|
||||
import {
|
||||
buildDiscordInboundAccessContext,
|
||||
@@ -28,6 +29,12 @@ import {
|
||||
import { buildDirectLabel, buildGuildLabel, resolveReplyContext } from "./reply-context.js";
|
||||
import { resolveDiscordAutoThreadReplyPlan, resolveDiscordThreadStarter } from "./threading.js";
|
||||
|
||||
function normalizeDiscordDmOwnerEntry(entry: string): string | undefined {
|
||||
const normalized = normalizeDiscordAllowList([entry], ["discord:", "user:", "pk:"]);
|
||||
const candidate = normalized?.ids.values().next().value;
|
||||
return typeof candidate === "string" && /^\d+$/.test(candidate) ? candidate : undefined;
|
||||
}
|
||||
|
||||
export async function buildDiscordMessageProcessContext(params: {
|
||||
ctx: DiscordMessagePreflightContext;
|
||||
text: string;
|
||||
@@ -104,6 +111,13 @@ export async function buildDiscordMessageProcessContext(params: {
|
||||
channelTopic: channelInfo?.topic,
|
||||
messageBody: text,
|
||||
});
|
||||
const pinnedMainDmOwner = isDirectMessage
|
||||
? resolvePinnedMainDmOwnerFromAllowlist({
|
||||
dmScope: cfg.session?.dmScope,
|
||||
allowFrom: channelConfig?.users ?? guildInfo?.users,
|
||||
normalizeEntry: normalizeDiscordDmOwnerEntry,
|
||||
})
|
||||
: null;
|
||||
const contextVisibilityMode = resolveChannelContextVisibilityMode({
|
||||
cfg,
|
||||
channel: "discord",
|
||||
@@ -347,6 +361,24 @@ export async function buildDiscordMessageProcessContext(params: {
|
||||
channel: "discord",
|
||||
to: lastRouteTo,
|
||||
accountId: route.accountId,
|
||||
mainDmOwnerPin:
|
||||
isDirectMessage && persistedSessionKey === route.mainSessionKey && pinnedMainDmOwner
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: author.id,
|
||||
onSkip: ({
|
||||
ownerRecipient,
|
||||
senderRecipient,
|
||||
}: {
|
||||
ownerRecipient: string;
|
||||
senderRecipient: string;
|
||||
}) => {
|
||||
logVerbose(
|
||||
`discord: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
onRecordError: (err: unknown) => {
|
||||
logVerbose(`discord: failed updating session meta: ${String(err)}`);
|
||||
|
||||
@@ -198,6 +198,27 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
|
||||
recordInboundSession: (...args: unknown[]) => recordInboundSession(...args),
|
||||
resolvePinnedMainDmOwnerFromAllowlist: (params: {
|
||||
dmScope?: string | null;
|
||||
allowFrom?: Array<string | number> | null;
|
||||
normalizeEntry: (entry: string) => string | undefined;
|
||||
}) => {
|
||||
if ((params.dmScope ?? "main") !== "main") {
|
||||
return null;
|
||||
}
|
||||
const allowFrom = Array.isArray(params.allowFrom) ? params.allowFrom : [];
|
||||
if (allowFrom.some((entry) => String(entry).trim() === "*")) {
|
||||
return null;
|
||||
}
|
||||
const owners = Array.from(
|
||||
new Set(
|
||||
allowFrom
|
||||
.map((entry) => params.normalizeEntry(String(entry)))
|
||||
.filter((entry): entry is string => Boolean(entry)),
|
||||
),
|
||||
);
|
||||
return owners.length === 1 ? owners[0] : null;
|
||||
},
|
||||
registerSessionBindingAdapter: vi.fn(),
|
||||
unregisterSessionBindingAdapter: vi.fn(),
|
||||
resolveThreadBindingConversationIdFromBindingId: (bindingId: string) =>
|
||||
@@ -306,7 +327,13 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
function getLastRouteUpdate():
|
||||
| { sessionKey?: string; channel?: string; to?: string; accountId?: string }
|
||||
| {
|
||||
sessionKey?: string;
|
||||
channel?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
mainDmOwnerPin?: { ownerRecipient?: string; senderRecipient?: string };
|
||||
}
|
||||
| undefined {
|
||||
const callArgs = recordInboundSession.mock.calls.at(-1) as unknown[] | undefined;
|
||||
const params = callArgs?.[0] as
|
||||
@@ -316,6 +343,7 @@ function getLastRouteUpdate():
|
||||
channel?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
mainDmOwnerPin?: { ownerRecipient?: string; senderRecipient?: string };
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
@@ -782,6 +810,48 @@ describe("processDiscordMessage session routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("pins Discord text DM main-route updates to the single configured DM owner", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
...createDirectMessageContextOverrides(),
|
||||
cfg: {
|
||||
messages: { ackReaction: "👀" },
|
||||
session: {
|
||||
store: "/tmp/openclaw-discord-process-test-sessions.json",
|
||||
dmScope: "main",
|
||||
},
|
||||
},
|
||||
channelConfig: { users: ["user:111"] },
|
||||
baseSessionKey: "agent:main:main",
|
||||
author: {
|
||||
id: "222",
|
||||
username: "bob",
|
||||
discriminator: "0",
|
||||
globalName: "Bob",
|
||||
},
|
||||
sender: { id: "222", label: "bob" },
|
||||
route: {
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:main",
|
||||
mainSessionKey: "agent:main:main",
|
||||
},
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastRouteUpdate()).toMatchObject({
|
||||
sessionKey: "agent:main:main",
|
||||
channel: "discord",
|
||||
to: "user:222",
|
||||
accountId: "default",
|
||||
mainDmOwnerPin: {
|
||||
ownerRecipient: "111",
|
||||
senderRecipient: "222",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("stores group lastRoute with channel target", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
baseSessionKey: "agent:main:discord:channel:c1",
|
||||
|
||||
Reference in New Issue
Block a user