Files
EJClaw/src/channels/discord-outbound.ts
Eyejoker 6197e90c8c Fix structured attachment rendering (#69)
* review: harden readonly git checks and lint verification

* test: satisfy readonly reviewer sandbox typing

* test: fix readonly reviewer sandbox assertions

* test: remove impossible readonly sandbox guards

* test: relax readonly sandbox assertions

* test: cast readonly sandbox expectation shape

* test: cast readonly sandbox expectation through unknown

* Phase 0 — STEP_DONE/TASK_DONE split + owner-follow-up integration smoke

* Add STEP_DONE guards, verdict storage, and stale delivery suppression

* style: format paired stepdone telemetry files

* Route STEP_DONE through reviewer

* Add structured Discord attachments

* style: format structured attachment test

* Fix room thread bot output parity

* Remove duplicate work item attachment migration from owner branch

* Remove legacy dashboard rooms renderer

* Extract dashboard parsed body renderer

* Extract dashboard redaction helpers

* Extract dashboard RoomCardV2 component

* Include RoomCardV2 test in vitest suite

* Extract dashboard RoomBoardV2 component

* Extract dashboard EmptyState component

* Extract dashboard InboxPanel component

* Split dashboard InboxPanel card renderer

* Extract dashboard TaskPanel component

* Extract dashboard UsagePanel component

* Fix dashboard room thread chunk rendering

* Extract dashboard ServicePanel component

* Render dashboard live progress markdown

* Extract dashboard SettingsPanel component

* Fix structured attachment rendering
2026-04-28 17:36:11 +09:00

102 lines
3.0 KiB
TypeScript

import path from 'path';
import {
extractImageTagPaths,
normalizeEjclawStructuredOutput,
} from '../agent-protocol.js';
import type { OutboundAttachment } from '../types.js';
const IMAGE_EXTS = /\.(png|jpe?g|gif|webp|bmp)$/i;
const MD_LINK_RE = /\[[^\]]*\]\((\/[^)]+)\)/g;
export interface PreparedDiscordOutbound {
text: string;
cleanText: string;
attachments: OutboundAttachment[];
attachmentSource: 'structured' | 'md-link' | 'image-tag' | 'none';
silent: boolean;
}
function extractMarkdownImageAttachments(text: string): {
cleanText: string;
attachments: OutboundAttachment[];
} {
const attachments: OutboundAttachment[] = [];
const seen = new Set<string>();
const cleanText = text.replace(MD_LINK_RE, (_full, rawPath: string) => {
const trimmed = rawPath.trim();
if (IMAGE_EXTS.test(trimmed)) {
if (!seen.has(trimmed)) {
attachments.push({
path: trimmed,
name: path.basename(trimmed),
});
seen.add(trimmed);
}
return '';
}
const basename = path.basename(trimmed.replace(/#.*$/, ''));
const lineMatch = trimmed.match(/#L(\d+)/);
return lineMatch ? `\`${basename}:${lineMatch[1]}\`` : `\`${basename}\``;
});
return { cleanText, attachments };
}
function imageTagPathsToAttachments(paths: string[]): OutboundAttachment[] {
return paths
.filter((filePath) => IMAGE_EXTS.test(filePath))
.map((filePath) => ({
path: filePath,
name: path.basename(filePath),
}));
}
export function prepareDiscordOutbound(
text: string,
optionAttachments: OutboundAttachment[] | undefined,
): PreparedDiscordOutbound {
const normalized = normalizeEjclawStructuredOutput(text);
if (normalized.output?.visibility === 'silent') {
return {
text: '',
cleanText: '',
attachments: [],
attachmentSource: 'none',
silent: true,
};
}
const structuredOutput =
normalized.output?.visibility === 'public' ? normalized.output : null;
const outboundText = structuredOutput?.text ?? normalized.result ?? text;
const structuredAttachments =
optionAttachments && optionAttachments.length > 0
? optionAttachments
: (structuredOutput?.attachments ?? []);
const hasStructuredAttachments = structuredAttachments.length > 0;
const markdownExtracted = extractMarkdownImageAttachments(outboundText);
const imageTagExtracted = extractImageTagPaths(markdownExtracted.cleanText);
const legacyImageTagAttachments = imageTagPathsToAttachments(
imageTagExtracted.imagePaths,
);
return {
text: outboundText,
cleanText: imageTagExtracted.cleanText,
attachments: hasStructuredAttachments
? structuredAttachments
: [...markdownExtracted.attachments, ...legacyImageTagAttachments],
attachmentSource: hasStructuredAttachments
? 'structured'
: markdownExtracted.attachments.length > 0
? 'md-link'
: legacyImageTagAttachments.length > 0
? 'image-tag'
: 'none',
silent: false,
};
}