757 lines
21 KiB
TypeScript
757 lines
21 KiB
TypeScript
import type { ChatMessagePart } from './types';
|
|
|
|
const LINK_CARD_LINE_PATTERN = /^\s*\[\[link-card:(.+?)\]\]\s*$/i;
|
|
const PROMPT_LINE_PATTERN = /^\s*\[\[prompt:(.+?)\]\]\s*$/i;
|
|
const STANDALONE_MARKDOWN_LINK_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?\[([^\]]+)\]\((.+)\)\s*$/;
|
|
const STANDALONE_URL_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?((?:https?:\/\/|\/)[^\s<>)\]]+)\s*$/i;
|
|
const PROMPT_BLOCK_START_PATTERN = /^\s*\[\[prompt:\s*$/i;
|
|
const PROMPT_BLOCK_END_PATTERN = /^\s*\]\]\s*$/;
|
|
const PROMPT_CODE_BLOCK_START_PATTERN = /^\s*```(?:json|prompt)(?:\s+prompt)?\s*$/i;
|
|
const CODE_BLOCK_END_PATTERN = /^\s*```\s*$/;
|
|
const CODE_FENCE_TOGGLE_PATTERN = /^\s*```/;
|
|
const ATTACHMENT_SECTION_TITLE_PATTERN = /^\s*첨부\s*파일\s*:?\s*$/i;
|
|
const ATTACHMENT_ENTRY_PATTERN = /^\s*-\s+(.+?)\s*:\s*(.+?)\s*$/;
|
|
const RESOURCE_PATH_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
|
|
const CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
|
|
const CHAT_DOT_CODEX_MARKER = '/.codex_chat/';
|
|
const CHAT_PUBLIC_DOT_CODEX_MARKER = '/public/.codex_chat/';
|
|
const RESOURCE_MANAGER_PREVIEW_MARKER = '/api/resource-manager/preview/';
|
|
const RESOURCE_MANAGER_ROOT_MARKER = 'resource/';
|
|
type PromptPart = Extract<ChatMessagePart, { type: 'prompt' }>;
|
|
type PromptOption = PromptPart['options'][number];
|
|
type PromptPreview = NonNullable<PromptOption['preview']>;
|
|
type PromptStep = NonNullable<PromptPart['steps']>[number];
|
|
|
|
function normalizeText(value: unknown) {
|
|
return String(value ?? '').trim();
|
|
}
|
|
|
|
function unwrapMarkdownLinkTarget(value: string) {
|
|
const normalized = normalizeText(value);
|
|
|
|
if (!normalized) {
|
|
return '';
|
|
}
|
|
|
|
const matched = normalized.match(/^<([\s\S]+)>$/);
|
|
return matched?.[1]?.trim() ?? normalized;
|
|
}
|
|
|
|
function normalizeResourceManagerPathSegment(segment: string) {
|
|
const normalized = normalizeText(segment);
|
|
|
|
if (!normalized) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
return encodeURIComponent(decodeURIComponent(normalized));
|
|
} catch {
|
|
return encodeURIComponent(normalized);
|
|
}
|
|
}
|
|
|
|
function buildResourceManagerPreviewUrl(value: string) {
|
|
const normalized = normalizeText(value).replace(/\\/g, '/');
|
|
const matchedResourcePath = normalized.match(/(?:^|\/)(resource\/.+)$/i)?.[1];
|
|
const resourcePath = normalizeText(matchedResourcePath).replace(/^\/+/, '');
|
|
|
|
if (!resourcePath) {
|
|
return '';
|
|
}
|
|
|
|
const relativePath = resourcePath.slice(RESOURCE_MANAGER_ROOT_MARKER.length).replace(/^\/+/, '');
|
|
|
|
if (!relativePath) {
|
|
return '';
|
|
}
|
|
|
|
const encodedPath = relativePath
|
|
.split('/')
|
|
.filter(Boolean)
|
|
.map((segment) => normalizeResourceManagerPathSegment(segment))
|
|
.join('/');
|
|
|
|
return encodedPath ? `${RESOURCE_MANAGER_PREVIEW_MARKER}${encodedPath}` : '';
|
|
}
|
|
|
|
function extractKnownPreviewPath(value: string) {
|
|
const normalized = normalizeText(value);
|
|
|
|
if (!normalized) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(normalized);
|
|
const pathname = `${parsed.pathname || ''}${parsed.search || ''}${parsed.hash || ''}`;
|
|
|
|
if (pathname.startsWith(CHAT_API_RESOURCE_MARKER) || pathname.startsWith(RESOURCE_MANAGER_PREVIEW_MARKER)) {
|
|
return pathname;
|
|
}
|
|
|
|
return normalized;
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function normalizeUrl(value: string) {
|
|
const normalized = unwrapMarkdownLinkTarget(value);
|
|
|
|
if (!normalized) {
|
|
return '';
|
|
}
|
|
|
|
const knownPreviewPath = extractKnownPreviewPath(normalized);
|
|
|
|
if (knownPreviewPath) {
|
|
return knownPreviewPath;
|
|
}
|
|
|
|
const malformedResourceMatch = normalized.match(/^https?:\/(api\/chat\/resources\/.+)$/i);
|
|
if (malformedResourceMatch?.[1]) {
|
|
return `/${malformedResourceMatch[1]}`;
|
|
}
|
|
|
|
const apiMarkerIndex = normalized.lastIndexOf(CHAT_API_RESOURCE_MARKER);
|
|
if (apiMarkerIndex >= 0) {
|
|
const apiPath = normalized.slice(apiMarkerIndex);
|
|
const dotCodexIndex = apiPath.indexOf(CHAT_DOT_CODEX_MARKER);
|
|
return dotCodexIndex >= 0
|
|
? `${CHAT_API_RESOURCE_MARKER}${apiPath.slice(dotCodexIndex + 1)}`
|
|
: apiPath;
|
|
}
|
|
|
|
const publicDotCodexIndex = normalized.lastIndexOf(CHAT_PUBLIC_DOT_CODEX_MARKER);
|
|
if (publicDotCodexIndex >= 0) {
|
|
return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(publicDotCodexIndex + 8)}`;
|
|
}
|
|
|
|
const dotCodexIndex = normalized.lastIndexOf(CHAT_DOT_CODEX_MARKER);
|
|
if (dotCodexIndex >= 0) {
|
|
return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(dotCodexIndex + 1)}`;
|
|
}
|
|
|
|
if (normalized === 'resource' || normalized === '/resource' || normalized.startsWith('resource/') || normalized.includes('/resource/')) {
|
|
const resourceManagerPreviewUrl = buildResourceManagerPreviewUrl(normalized);
|
|
|
|
if (resourceManagerPreviewUrl) {
|
|
return resourceManagerPreviewUrl;
|
|
}
|
|
}
|
|
|
|
if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
|
|
return normalized;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function hasKnownFileExtension(url: string) {
|
|
const pathname = url.split('?')[0] ?? '';
|
|
return /\.[a-z0-9]{1,8}$/i.test(pathname);
|
|
}
|
|
|
|
function isStructuredLinkCardCandidate(url: string) {
|
|
const normalized = normalizeUrl(url);
|
|
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
|
|
if (isInternalResourceUrl(normalized)) {
|
|
return false;
|
|
}
|
|
|
|
return /^https?:\/\//i.test(normalized) && !hasKnownFileExtension(normalized);
|
|
}
|
|
|
|
function buildFallbackLinkTitle(url: string) {
|
|
try {
|
|
const parsed = new URL(url, 'https://local.invalid');
|
|
const lastSegment = parsed.pathname.split('/').filter(Boolean).at(-1)?.trim();
|
|
return lastSegment || parsed.hostname || normalizeText(url);
|
|
} catch {
|
|
return normalizeText(url);
|
|
}
|
|
}
|
|
|
|
function normalizeStandaloneTitle(value: string) {
|
|
return value
|
|
.replace(/^\s*(?:[-*+]\s+|\d+\.\s+)?/, '')
|
|
.replace(/[`'"]+/g, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function resolveStandaloneLinkTitle(keptLines: string[], url: string) {
|
|
for (let index = keptLines.length - 1; index >= 0; index -= 1) {
|
|
const candidate = normalizeStandaloneTitle(keptLines[index] ?? '');
|
|
|
|
if (candidate) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return buildFallbackLinkTitle(url);
|
|
}
|
|
|
|
function normalizePromptPreview(value: unknown): PromptPreview | null {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
|
|
const record = value as Record<string, unknown>;
|
|
const type =
|
|
record.type === 'image' || record.type === 'markdown' || record.type === 'html' || record.type === 'resource'
|
|
? record.type
|
|
: null;
|
|
const url = normalizeUrl(normalizeText(record.url));
|
|
const content = String(record.content ?? '').trim() || null;
|
|
const alt = normalizeText(record.alt) || null;
|
|
const title = normalizeText(record.title) || null;
|
|
|
|
if (!type) {
|
|
return null;
|
|
}
|
|
|
|
if (type === 'image' || type === 'resource') {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
} else if (!content && !url) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
type,
|
|
url: url || null,
|
|
content,
|
|
alt,
|
|
title,
|
|
};
|
|
}
|
|
|
|
function isPromptOption(value: PromptOption | null): value is PromptOption {
|
|
return value != null;
|
|
}
|
|
|
|
function normalizePromptOption(value: unknown): PromptOption | null {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
|
|
const record = value as Record<string, unknown>;
|
|
const optionValue = normalizeText(record.value);
|
|
const label = normalizeText(record.label);
|
|
|
|
if (!optionValue || !label) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
value: optionValue,
|
|
label,
|
|
description: normalizeText(record.description) || null,
|
|
preview: normalizePromptPreview(record.preview),
|
|
};
|
|
}
|
|
|
|
function normalizePromptSelectedValues(value: unknown) {
|
|
return [
|
|
...(Array.isArray(value) ? value : []),
|
|
]
|
|
.map((item) => normalizeText(item))
|
|
.filter(Boolean)
|
|
.filter((item, index, array) => array.indexOf(item) === index);
|
|
}
|
|
|
|
function normalizePromptAttachment(value: unknown) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
|
|
const record = value as Record<string, unknown>;
|
|
const id = normalizeText(record.id);
|
|
const name = normalizeText(record.name);
|
|
const path = normalizeText(record.path);
|
|
const publicUrl = normalizeText(record.publicUrl);
|
|
const size = Number(record.size);
|
|
const mimeType = normalizeText(record.mimeType);
|
|
|
|
if (!id || !name || !path || !publicUrl || !Number.isFinite(size) || size < 0 || !mimeType) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id,
|
|
name,
|
|
path,
|
|
publicUrl,
|
|
size,
|
|
mimeType,
|
|
};
|
|
}
|
|
|
|
function normalizePromptAttachments(value: unknown) {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
|
|
const seen = new Set<string>();
|
|
|
|
return value
|
|
.map((item) => normalizePromptAttachment(item))
|
|
.filter((item): item is NonNullable<PromptPart['attachments']>[number] => Boolean(item))
|
|
.filter((item) => {
|
|
if (seen.has(item.id)) {
|
|
return false;
|
|
}
|
|
|
|
seen.add(item.id);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function normalizePromptSteps(value: unknown): PromptStep[] {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
|
|
return value.flatMap((item, index) => {
|
|
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
return [];
|
|
}
|
|
|
|
const record = item as Record<string, unknown>;
|
|
const key = normalizeText(record.key) || `step-${index + 1}`;
|
|
const title = normalizeText(record.title);
|
|
const options = Array.isArray(record.options)
|
|
? record.options.map((option) => normalizePromptOption(option)).filter(isPromptOption)
|
|
: [];
|
|
|
|
if (!title || options.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{
|
|
key,
|
|
title,
|
|
description: normalizeText(record.description) || null,
|
|
submitLabel: normalizeText(record.submitLabel) || null,
|
|
mode: record.mode === 'direct' || record.mode === 'queue' ? record.mode : null,
|
|
multiple: record.multiple === true,
|
|
optional: record.optional === true,
|
|
responseTemplate: normalizeText(record.responseTemplate) || null,
|
|
freeTextLabel: normalizeText(record.freeTextLabel) || null,
|
|
freeTextPlaceholder: normalizeText(record.freeTextPlaceholder) || null,
|
|
selectedValues: normalizePromptSelectedValues(record.selectedValues),
|
|
options,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
function decodeUrlComponentSafely(value: string) {
|
|
try {
|
|
return decodeURIComponent(value);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function resolveLinkCardUrlAndActionLabel(rawUrl: string, rawActionLabel?: string) {
|
|
let resolvedUrl = normalizeText(rawUrl);
|
|
let resolvedActionLabel = normalizeText(rawActionLabel);
|
|
|
|
if (!resolvedActionLabel) {
|
|
const decodedUrl = decodeUrlComponentSafely(resolvedUrl);
|
|
const dividerIndex = decodedUrl.lastIndexOf('|');
|
|
|
|
if (dividerIndex > 0 && dividerIndex < decodedUrl.length - 1) {
|
|
resolvedUrl = decodedUrl.slice(0, dividerIndex).trim();
|
|
resolvedActionLabel = decodedUrl.slice(dividerIndex + 1).trim();
|
|
}
|
|
}
|
|
|
|
return {
|
|
url: normalizeUrl(resolvedUrl),
|
|
actionLabel: resolvedActionLabel || null,
|
|
};
|
|
}
|
|
|
|
function isInternalResourceUrl(url: string) {
|
|
return RESOURCE_PATH_PREFIXES.some((prefix) => url.startsWith(prefix));
|
|
}
|
|
|
|
function buildLinkCardPart(rawBody: string): ChatMessagePart | null {
|
|
const segments = rawBody
|
|
.split('|')
|
|
.map((segment) => segment.trim())
|
|
.filter(Boolean);
|
|
|
|
if (segments.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
const [rawTitle, rawUrl, rawActionLabel] = segments;
|
|
const title = normalizeText(rawTitle);
|
|
const { url, actionLabel } = resolveLinkCardUrlAndActionLabel(rawUrl, rawActionLabel);
|
|
|
|
if (!title || !url) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
type: 'link_card',
|
|
title,
|
|
url,
|
|
actionLabel,
|
|
};
|
|
}
|
|
|
|
function buildPromptPart(rawBody: string): ChatMessagePart | null {
|
|
let parsed: unknown;
|
|
|
|
try {
|
|
parsed = JSON.parse(rawBody);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
return null;
|
|
}
|
|
|
|
const record = parsed as Record<string, unknown>;
|
|
const title = normalizeText(record.title);
|
|
const options = Array.isArray(record.options)
|
|
? record.options.map((item) => normalizePromptOption(item)).filter(isPromptOption)
|
|
: [];
|
|
const steps = normalizePromptSteps(record.steps);
|
|
|
|
if (!title || (options.length === 0 && steps.length === 0)) {
|
|
return null;
|
|
}
|
|
|
|
const mode = record.mode === 'direct' || record.mode === 'queue' ? record.mode : null;
|
|
const selectedValues = [
|
|
...normalizePromptSelectedValues(record.selectedValues),
|
|
...(record.selectedValue != null ? [record.selectedValue] : []),
|
|
...steps.flatMap((step) => step.selectedValues ?? []),
|
|
]
|
|
.map((item) => normalizeText(item))
|
|
.filter(Boolean)
|
|
.filter((value, index, values) => values.indexOf(value) === index);
|
|
const resolvedBy =
|
|
record.resolvedBy === 'user' || record.resolvedBy === 'timeout' || record.resolvedBy === 'system'
|
|
? record.resolvedBy
|
|
: null;
|
|
|
|
return {
|
|
type: 'prompt',
|
|
title,
|
|
description: normalizeText(record.description) || null,
|
|
submitLabel: normalizeText(record.submitLabel) || null,
|
|
mode,
|
|
multiple: record.multiple === true,
|
|
responseTemplate: normalizeText(record.responseTemplate) || null,
|
|
freeTextLabel: normalizeText(record.freeTextLabel) || null,
|
|
freeTextPlaceholder: normalizeText(record.freeTextPlaceholder) || null,
|
|
currentStepKey: normalizeText(record.currentStepKey) || null,
|
|
steps: steps.length > 0 ? steps : undefined,
|
|
readOnly: record.readOnly === true || resolvedBy != null,
|
|
selectedValues,
|
|
resolvedBy,
|
|
resolvedAt: normalizeText(record.resolvedAt) || null,
|
|
resultText: normalizeText(record.resultText) || null,
|
|
attachments: normalizePromptAttachments(record.attachments),
|
|
options,
|
|
};
|
|
}
|
|
|
|
function buildPromptPartFromBlock(rawBody: string) {
|
|
const trimmed = rawBody.trim();
|
|
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
const promptWrapperMatched = trimmed.match(/^\[\[prompt:\s*([\s\S]*?)\s*\]\]$/i);
|
|
return buildPromptPart(promptWrapperMatched?.[1] ?? trimmed);
|
|
}
|
|
|
|
function extractAttachmentEntryUrl(rawLine: string) {
|
|
const matched = rawLine.match(ATTACHMENT_ENTRY_PATTERN);
|
|
|
|
if (!matched) {
|
|
return '';
|
|
}
|
|
|
|
const resolvedUrl = normalizeUrl(matched[2] ?? '');
|
|
return resolvedUrl || '';
|
|
}
|
|
|
|
export function extractAttachmentPreviewUrls(text: string) {
|
|
const lines = String(text ?? '').split('\n');
|
|
const keptLines: string[] = [];
|
|
const urls: string[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const line = lines[index] ?? '';
|
|
|
|
if (!ATTACHMENT_SECTION_TITLE_PATTERN.test(line)) {
|
|
keptLines.push(line);
|
|
continue;
|
|
}
|
|
|
|
const attachmentUrls: string[] = [];
|
|
let cursor = index + 1;
|
|
|
|
while (cursor < lines.length) {
|
|
const nextLine = lines[cursor] ?? '';
|
|
|
|
if (!nextLine.trim()) {
|
|
cursor += 1;
|
|
continue;
|
|
}
|
|
|
|
const attachmentUrl = extractAttachmentEntryUrl(nextLine);
|
|
|
|
if (!attachmentUrl) {
|
|
break;
|
|
}
|
|
|
|
attachmentUrls.push(attachmentUrl);
|
|
cursor += 1;
|
|
}
|
|
|
|
if (attachmentUrls.length === 0) {
|
|
keptLines.push(line);
|
|
continue;
|
|
}
|
|
|
|
attachmentUrls.forEach((url) => {
|
|
if (seen.has(url)) {
|
|
return;
|
|
}
|
|
|
|
seen.add(url);
|
|
urls.push(url);
|
|
});
|
|
|
|
index = cursor - 1;
|
|
}
|
|
|
|
return {
|
|
strippedText: keptLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
|
|
urls,
|
|
};
|
|
}
|
|
|
|
export function extractChatMessageParts(text: string) {
|
|
const attachmentExtraction = extractAttachmentPreviewUrls(text);
|
|
const lines = String(attachmentExtraction.strippedText ?? '').split('\n');
|
|
const keptLines: string[] = [];
|
|
const parts: ChatMessagePart[] = [];
|
|
const seenLinkKeys = new Set<string>();
|
|
let isInsideCodeFence = false;
|
|
const pushPart = (nextPart: ChatMessagePart | null) => {
|
|
if (!nextPart) {
|
|
return false;
|
|
}
|
|
|
|
const dedupeKey =
|
|
nextPart.type === 'link_card'
|
|
? `${nextPart.type}:${nextPart.title}:${nextPart.url}:${nextPart.actionLabel ?? ''}`
|
|
: [
|
|
nextPart.type,
|
|
nextPart.title,
|
|
nextPart.options
|
|
.map((option) =>
|
|
[
|
|
option.value,
|
|
option.label,
|
|
option.preview?.type ?? '',
|
|
option.preview?.url ?? '',
|
|
option.preview?.content ?? '',
|
|
option.preview?.title ?? '',
|
|
].join('|'),
|
|
)
|
|
.join(','),
|
|
(nextPart.steps ?? [])
|
|
.map((step) =>
|
|
[
|
|
step.key,
|
|
step.title,
|
|
step.options.map((option) => `${option.value}:${option.label}`).join(','),
|
|
].join('|'),
|
|
)
|
|
.join(','),
|
|
nextPart.selectedValues?.join(',') ?? '',
|
|
nextPart.resolvedBy ?? '',
|
|
nextPart.resultText ?? '',
|
|
nextPart.readOnly === true ? 'readonly' : '',
|
|
].join(':');
|
|
|
|
if (seenLinkKeys.has(dedupeKey)) {
|
|
return true;
|
|
}
|
|
|
|
seenLinkKeys.add(dedupeKey);
|
|
parts.push(nextPart);
|
|
return true;
|
|
};
|
|
|
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
const line = lines[lineIndex] ?? '';
|
|
|
|
if (PROMPT_CODE_BLOCK_START_PATTERN.test(line)) {
|
|
const fencedLines = [line];
|
|
const jsonBodyLines: string[] = [];
|
|
let cursor = lineIndex + 1;
|
|
let foundFenceEnd = false;
|
|
|
|
for (; cursor < lines.length; cursor += 1) {
|
|
const nextLine = lines[cursor] ?? '';
|
|
fencedLines.push(nextLine);
|
|
|
|
if (CODE_BLOCK_END_PATTERN.test(nextLine)) {
|
|
foundFenceEnd = true;
|
|
break;
|
|
}
|
|
|
|
jsonBodyLines.push(nextLine);
|
|
}
|
|
|
|
if (foundFenceEnd && pushPart(buildPromptPartFromBlock(jsonBodyLines.join('\n')))) {
|
|
lineIndex = cursor;
|
|
continue;
|
|
}
|
|
|
|
keptLines.push(...fencedLines);
|
|
lineIndex = foundFenceEnd ? cursor : lines.length;
|
|
continue;
|
|
}
|
|
|
|
if (CODE_FENCE_TOGGLE_PATTERN.test(line)) {
|
|
keptLines.push(line);
|
|
isInsideCodeFence = !isInsideCodeFence;
|
|
continue;
|
|
}
|
|
|
|
if (isInsideCodeFence) {
|
|
keptLines.push(line);
|
|
continue;
|
|
}
|
|
|
|
const promptMatched = line.match(PROMPT_LINE_PATTERN);
|
|
|
|
if (promptMatched) {
|
|
if (!pushPart(buildPromptPart(promptMatched[1] ?? ''))) {
|
|
keptLines.push(line);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (PROMPT_BLOCK_START_PATTERN.test(line)) {
|
|
const wrappedLines = [line];
|
|
const promptBodyLines: string[] = [];
|
|
let cursor = lineIndex + 1;
|
|
let foundBlockEnd = false;
|
|
|
|
for (; cursor < lines.length; cursor += 1) {
|
|
const nextLine = lines[cursor] ?? '';
|
|
wrappedLines.push(nextLine);
|
|
|
|
if (PROMPT_BLOCK_END_PATTERN.test(nextLine)) {
|
|
foundBlockEnd = true;
|
|
break;
|
|
}
|
|
|
|
promptBodyLines.push(nextLine);
|
|
}
|
|
|
|
if (foundBlockEnd && pushPart(buildPromptPartFromBlock(promptBodyLines.join('\n')))) {
|
|
lineIndex = cursor;
|
|
continue;
|
|
}
|
|
|
|
keptLines.push(...wrappedLines);
|
|
lineIndex = foundBlockEnd ? cursor : lines.length;
|
|
continue;
|
|
}
|
|
|
|
const matched = line.match(LINK_CARD_LINE_PATTERN);
|
|
|
|
if (!matched) {
|
|
const markdownLinkMatch = line.match(STANDALONE_MARKDOWN_LINK_LINE_PATTERN);
|
|
if (markdownLinkMatch) {
|
|
const [, rawTitle, rawUrl] = markdownLinkMatch;
|
|
if (isStructuredLinkCardCandidate(rawUrl ?? '')) {
|
|
if (pushPart(buildLinkCardPart(`${rawTitle}|${rawUrl}`))) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
const standaloneUrlMatch = line.match(STANDALONE_URL_LINE_PATTERN);
|
|
if (standaloneUrlMatch) {
|
|
const rawUrl = standaloneUrlMatch[1] ?? '';
|
|
if (isStructuredLinkCardCandidate(rawUrl)) {
|
|
if (pushPart(buildLinkCardPart(`${resolveStandaloneLinkTitle(keptLines, rawUrl)}|${rawUrl}`))) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
keptLines.push(line);
|
|
continue;
|
|
}
|
|
|
|
if (!pushPart(buildLinkCardPart(matched[1] ?? ''))) {
|
|
keptLines.push(line);
|
|
continue;
|
|
}
|
|
|
|
const latestPart = parts.at(-1);
|
|
if (latestPart?.type === 'link_card' && isInternalResourceUrl(latestPart.url)) {
|
|
parts.pop();
|
|
seenLinkKeys.delete(`${latestPart.type}:${latestPart.title}:${latestPart.url}:${latestPart.actionLabel ?? ''}`);
|
|
keptLines.push(latestPart.url);
|
|
}
|
|
}
|
|
|
|
const strippedWithEmbeddedPrompts = (() => {
|
|
const nextLines: string[] = [];
|
|
let isInsideCodeFence = false;
|
|
|
|
keptLines.forEach((line) => {
|
|
if (CODE_FENCE_TOGGLE_PATTERN.test(line)) {
|
|
nextLines.push(line);
|
|
isInsideCodeFence = !isInsideCodeFence;
|
|
return;
|
|
}
|
|
|
|
if (isInsideCodeFence) {
|
|
nextLines.push(line);
|
|
return;
|
|
}
|
|
|
|
nextLines.push(
|
|
line.replace(/\[\[prompt:(.+?)\]\]/gi, (fullMatch, rawBody) => (pushPart(buildPromptPart(rawBody)) ? '' : fullMatch)),
|
|
);
|
|
});
|
|
|
|
return nextLines.join('\n');
|
|
})();
|
|
|
|
return {
|
|
strippedText: strippedWithEmbeddedPrompts.replace(/\n{3,}/g, '\n\n').trim(),
|
|
parts,
|
|
};
|
|
}
|