fix(discord): escape prose markdown without clobbering structured output

This commit is contained in:
claude-bot
2026-06-01 20:44:03 +09:00
parent e9dd09a036
commit 7197099db5
2 changed files with 16 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
import path from 'path'; import path from 'path';
import { normalizeAgentOutput } from '../agent-protocol.js'; import { normalizeAgentOutput } from '../agent-protocol.js';
import { neutralizeStrayMarkdown, sanitizeForOutbound } from '../router.js';
import type { OutboundAttachment } from '../types.js'; import type { OutboundAttachment } from '../types.js';
const LOCAL_MARKDOWN_LINK_RE = /\[[^\]\n]*\]\((\/[^)\n]+)\)/g; const LOCAL_MARKDOWN_LINK_RE = /\[[^\]\n]*\]\((\/[^)\n]+)\)/g;
@@ -46,7 +47,17 @@ export function prepareDiscordOutbound(
const structuredOutput = const structuredOutput =
normalized.output?.visibility === 'public' ? normalized.output : null; normalized.output?.visibility === 'public' ? normalized.output : null;
const outboundText = structuredOutput?.text ?? normalized.result ?? text; const outboundText = structuredOutput?.text ?? normalized.result ?? text;
const cleanText = sanitizeLocalMarkdownLinks(outboundText); // Redact/strip before escaping so secret patterns (which contain `_`) still
// match, then escape stray Discord markdown in free-form agent prose so
// emphasis markers (**bold**, ~~strike~~, …) render literally. Author-
// controlled structured envelopes are intentional, so their text is left
// untouched. Escaping runs before sanitizeLocalMarkdownLinks so the inline
// code it deliberately produces is preserved.
const safeText = sanitizeForOutbound(outboundText);
const renderedText = structuredOutput
? safeText
: neutralizeStrayMarkdown(safeText);
const cleanText = sanitizeLocalMarkdownLinks(renderedText);
const attachments = const attachments =
optionAttachments && optionAttachments.length > 0 optionAttachments && optionAttachments.length > 0
? optionAttachments ? optionAttachments

View File

@@ -15,7 +15,7 @@ import { CACHE_DIR } from '../config.js';
import { getEnv } from '../env.js'; import { getEnv } from '../env.js';
import { logger } from '../logger.js'; import { logger } from '../logger.js';
import { validateOutboundAttachments } from '../outbound-attachments.js'; import { validateOutboundAttachments } from '../outbound-attachments.js';
import { formatOutbound } from '../router.js'; import { sanitizeForOutbound } from '../router.js';
import { hasReviewerLease } from '../service-routing.js'; import { hasReviewerLease } from '../service-routing.js';
import type { import type {
DeleteRecentMessagesByContentOptions, DeleteRecentMessagesByContentOptions,
@@ -528,7 +528,9 @@ export class DiscordChannel implements Channel {
for (const [name, id] of Object.entries(mentionMap)) { for (const [name, id] of Object.entries(mentionMap)) {
cleaned = cleaned.replace(new RegExp(`@${name}`, 'g'), `<@${id}>`); cleaned = cleaned.replace(new RegExp(`@${name}`, 'g'), `<@${id}>`);
} }
cleaned = formatOutbound(cleaned); // Markdown escaping already happened in prepareDiscordOutbound (before
// the deliberate link→inline-code step). Here we only strip/redact.
cleaned = sanitizeForOutbound(cleaned);
// Discord has a 2000 character limit per message and 10 attachments per message // Discord has a 2000 character limit per message and 10 attachments per message
const MAX_LENGTH = 2000; const MAX_LENGTH = 2000;