refactor: unify approval forwarding and rendering

This commit is contained in:
Peter Steinberger
2026-03-30 08:27:37 +09:00
parent 8720070fe0
commit 15c3aa82bf
7 changed files with 518 additions and 326 deletions

View File

@@ -240,4 +240,50 @@ describe("discordOutbound", () => {
channelId: "ch-1",
});
});
it("neutralizes approval mentions only for approval payloads", async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456",
text: "",
payload: {
text: "Approval @everyone <@123> <#456>",
channelData: {
execApproval: {
approvalId: "req-1",
approvalSlug: "req-1",
},
},
},
accountId: "default",
});
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456",
"Approval @\u200beveryone <@\u200b123> <#\u200b456>",
expect.objectContaining({
accountId: "default",
}),
);
});
it("leaves non-approval mentions unchanged", async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456",
text: "",
payload: {
text: "Hello @everyone",
},
accountId: "default",
});
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456",
"Hello @everyone",
expect.objectContaining({
accountId: "default",
}),
);
});
});

View File

@@ -26,6 +26,33 @@ import { buildDiscordInteractiveComponents } from "./shared-interactive.js";
export const DISCORD_TEXT_CHUNK_LIMIT = 2000;
function hasApprovalChannelData(payload: { channelData?: unknown }): boolean {
const channelData = payload.channelData;
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
return false;
}
return Boolean((channelData as { execApproval?: unknown }).execApproval);
}
function neutralizeDiscordApprovalMentions(value: string): string {
return value
.replace(/@everyone/gi, "@\u200beveryone")
.replace(/@here/gi, "@\u200bhere")
.replace(/<@/g, "<@\u200b")
.replace(/<#/g, "<#\u200b");
}
function normalizeDiscordApprovalPayload<T extends { text?: string; channelData?: unknown }>(
payload: T,
): T {
return hasApprovalChannelData(payload) && payload.text
? {
...payload,
text: neutralizeDiscordApprovalMentions(payload.text),
}
: payload;
}
function resolveDiscordOutboundTarget(params: {
to: string;
threadId?: string | number | null;
@@ -96,12 +123,13 @@ export const discordOutbound: ChannelOutboundAdapter = {
chunker: null,
textChunkLimit: DISCORD_TEXT_CHUNK_LIMIT,
pollMaxOptions: 10,
normalizePayload: ({ payload }) => normalizeDiscordApprovalPayload(payload),
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
sendPayload: async (ctx) => {
const payload = {
const payload = normalizeDiscordApprovalPayload({
...ctx.payload,
text: ctx.payload.text ?? "",
};
});
const discordData = payload.channelData?.discord as
| { components?: DiscordComponentMessageSpec }
| undefined;