fix(discord): honor agent media roots in replies
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Docs/tool-loop detection config keys: align `docs/tools/loop-detection.md` examples and field names with the current `tools.loopDetection` schema to prevent copy-paste validation failures from outdated keys. (#33182) Thanks @Mylszd.
|
||||
- Discord/inbound debouncer: skip bot-own MESSAGE_CREATE events before they reach the debounce queue to avoid self-triggered slowdowns in busy servers. Thanks @thewilloftheshadow.
|
||||
- Discord/Agent-scoped media roots: pass `mediaLocalRoots` through Discord monitor reply delivery (message + component interaction paths) so local media attachments honor per-agent workspace roots instead of falling back to default global roots. Thanks @thewilloftheshadow.
|
||||
- Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow.
|
||||
- Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. Thanks @thewilloftheshadow.
|
||||
- Discord/voice messages: request upload slots with JSON fetch calls so voice message uploads no longer fail with content-type errors. Thanks @thewilloftheshadow.
|
||||
|
||||
@@ -34,6 +34,7 @@ import type { DiscordAccountConfig } from "../../config/types.discord.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { logDebug, logError } from "../../logger.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
@@ -976,6 +977,7 @@ async function dispatchDiscordComponentEvent(params: {
|
||||
fallbackLimit: 2000,
|
||||
});
|
||||
const token = ctx.token ?? "";
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(ctx.cfg, agentId);
|
||||
const replyToMode =
|
||||
ctx.discordConfig?.replyToMode ?? ctx.cfg.channels?.discord?.replyToMode ?? "off";
|
||||
const replyReference = createReplyReferencePlanner({
|
||||
@@ -1005,6 +1007,7 @@ async function dispatchDiscordComponentEvent(params: {
|
||||
maxLinesPerMessage: ctx.discordConfig?.maxLinesPerMessage,
|
||||
tableMode,
|
||||
chunkMode: resolveChunkMode(ctx.cfg, "discord", accountId),
|
||||
mediaLocalRoots,
|
||||
});
|
||||
replyReference.markSent();
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
|
||||
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||
import { convertMarkdownTables } from "../../markdown/tables.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { buildAgentSessionKey } from "../../routing/resolve-route.js";
|
||||
import { resolveThreadSessionKeys } from "../../routing/session-key.js";
|
||||
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
||||
@@ -128,6 +129,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
accountId,
|
||||
});
|
||||
const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
|
||||
const shouldAckReaction = () =>
|
||||
Boolean(
|
||||
ackReaction &&
|
||||
@@ -668,6 +670,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
chunkMode,
|
||||
sessionKey: ctxPayload.SessionKey,
|
||||
threadBindings,
|
||||
mediaLocalRoots,
|
||||
});
|
||||
replyReference.markSent();
|
||||
},
|
||||
|
||||
@@ -135,6 +135,45 @@ describe("deliverDiscordReply", () => {
|
||||
expect(sendMessageDiscordMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes mediaLocalRoots through media sends", async () => {
|
||||
const mediaLocalRoots = ["/tmp/workspace-agent"] as const;
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "Media reply",
|
||||
mediaUrls: ["https://example.com/first.png", "https://example.com/second.png"],
|
||||
},
|
||||
],
|
||||
target: "channel:654",
|
||||
token: "token",
|
||||
runtime,
|
||||
textLimit: 2000,
|
||||
mediaLocalRoots,
|
||||
});
|
||||
|
||||
expect(sendMessageDiscordMock).toHaveBeenCalledTimes(2);
|
||||
expect(sendMessageDiscordMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"channel:654",
|
||||
"Media reply",
|
||||
expect.objectContaining({
|
||||
token: "token",
|
||||
mediaUrl: "https://example.com/first.png",
|
||||
mediaLocalRoots,
|
||||
}),
|
||||
);
|
||||
expect(sendMessageDiscordMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"channel:654",
|
||||
"",
|
||||
expect.objectContaining({
|
||||
token: "token",
|
||||
mediaUrl: "https://example.com/second.png",
|
||||
mediaLocalRoots,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses replyToId only for the first chunk when replyToMode is first", async () => {
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
|
||||
@@ -192,6 +192,7 @@ async function sendAdditionalDiscordMedia(params: {
|
||||
rest?: RequestClient;
|
||||
accountId?: string;
|
||||
mediaUrls: string[];
|
||||
mediaLocalRoots?: readonly string[];
|
||||
resolveReplyTo: () => string | undefined;
|
||||
retryConfig: ResolvedRetryConfig;
|
||||
}) {
|
||||
@@ -204,6 +205,7 @@ async function sendAdditionalDiscordMedia(params: {
|
||||
rest: params.rest,
|
||||
mediaUrl,
|
||||
accountId: params.accountId,
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
replyTo,
|
||||
}),
|
||||
params.retryConfig,
|
||||
@@ -226,6 +228,7 @@ export async function deliverDiscordReply(params: {
|
||||
chunkMode?: ChunkMode;
|
||||
sessionKey?: string;
|
||||
threadBindings?: DiscordThreadBindingLookup;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
}) {
|
||||
const chunkLimit = Math.min(params.textLimit, 2000);
|
||||
const replyTo = params.replyToId?.trim() || undefined;
|
||||
@@ -341,6 +344,7 @@ export async function deliverDiscordReply(params: {
|
||||
rest: params.rest,
|
||||
accountId: params.accountId,
|
||||
mediaUrls: mediaList.slice(1),
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
resolveReplyTo,
|
||||
retryConfig,
|
||||
});
|
||||
@@ -353,6 +357,7 @@ export async function deliverDiscordReply(params: {
|
||||
rest: params.rest,
|
||||
mediaUrl: firstMedia,
|
||||
accountId: params.accountId,
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
replyTo,
|
||||
});
|
||||
deliveredAny = true;
|
||||
@@ -362,6 +367,7 @@ export async function deliverDiscordReply(params: {
|
||||
rest: params.rest,
|
||||
accountId: params.accountId,
|
||||
mediaUrls: mediaList.slice(1),
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
resolveReplyTo,
|
||||
retryConfig,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user