const TASK_ROUTING_HINT_RE = /^【TKMind 路由提示】[\s\S]*?\n\n/; const MEMIND_TASK_ORCHESTRATION_RE = /^【Memind 任务编排】[\s\S]*?(?:用户任务:|用户任务:)\s*/u; const MINDSPACE_CONTEXT_RE = /^\[MindSpace 上下文\][\s\S]*?\n\n/; const USER_IDENTITY_BLOCK_RE = /^\[用户身份\][\s\S]*?\n\n/; const IMAGE_URL_LINES_RE = /\n*\[图片\d+]: [^\n]+/g; const FILE_ATTACHMENT_LINES_RE = /\n*\[文件\d+: [^\]]+\]: [^\n]+/g; const ASSISTANT_DELIVERABLE_RE = /\[[^\]]+\]\(https?:\/\/[^)]+\)|https?:\/\/(?:m\.)?[^/\s]*tkmind\.(?:cn|ai)\/(?:MindSpace|u)\/|\bpublic\/[A-Za-z0-9._-]+\.html\b/i; const INTERNAL_ASSISTANT_MARKERS = [ /data-mindspace-page-tag/i, /mindspace-cover/i, /\bload_skill\b/i, /static-page-publish/i, /\.agents\/skills/i, /SKILL\.md/i, /注意到技能/u, /技能更新/u, /页脚要用.*platform-brand/u, ]; const URL_PATTERN = /https?:\/\/[^\s<>"')\]]+\/(?:MindSpace|temp)\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi; const PUBLICATION_URL_PATTERN = /https?:\/\/[^\s<>"')\]]+\/u\/([a-z0-9._-]+)\/pages\/([^\s<>"')\]]+)/gi; const MARKDOWN_LINK_RE = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g; const COMBINED_LINK_RE = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)|(https?:\/\/[^\s<>"')\]]+)|`?(public\/[A-Za-z0-9._-]+\.html)`?|(\/?MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/(public\/[A-Za-z0-9._-]+\.html))/gi; const RELATIVE_PUBLIC_HTML_RE = /\b(public\/[A-Za-z0-9._-]+\.html)\b/gi; function stripImageUrlLines(text) { return String(text ?? '') .replace(IMAGE_URL_LINES_RE, '') .replace(FILE_ATTACHMENT_LINES_RE, '') .trim(); } function stripUserFacingPrefixes(text) { let next = String(text ?? ''); next = next.replace(USER_IDENTITY_BLOCK_RE, '').trimStart(); next = next.replace(TASK_ROUTING_HINT_RE, '').trimStart(); next = next.replace(MEMIND_TASK_ORCHESTRATION_RE, '').trimStart(); while (MINDSPACE_CONTEXT_RE.test(next)) { next = next.replace(MINDSPACE_CONTEXT_RE, '').trimStart(); } return next.trim(); } function deriveAssistantFacingText(text) { const trimmed = String(text ?? '').trim(); if (!trimmed) return ''; if (ASSISTANT_DELIVERABLE_RE.test(trimmed)) return trimmed; if (INTERNAL_ASSISTANT_MARKERS.some((pattern) => pattern.test(trimmed))) return ''; return trimmed; } function decodeSegment(segment) { try { return decodeURIComponent(segment); } catch { return segment; } } function normalizeWorkspaceRelativePath(relativePath) { const parts = String(relativePath ?? '') .replace(/^\/+/, '') .split('/') .filter((part) => part && part !== '.' && part !== '..'); if (parts.length === 0) return ''; if (parts[0]?.toLowerCase() === 'public') return parts.join('/'); if (parts.length === 1 && parts[0].toLowerCase().endsWith('.html')) return `public/${parts[0]}`; return parts.join('/'); } function resolveWorkspacePublicUrl(relativePath, publishKey, apiBaseUrl) { const normalized = normalizeWorkspaceRelativePath(relativePath); if (!publishKey || !normalized.startsWith('public/') || !/\.html$/i.test(normalized)) { return null; } const base = String(apiBaseUrl || '').replace(/\/$/, ''); if (!base) return null; const encodedPath = normalized.split('/').map((part) => encodeURIComponent(part)).join('/'); return `${base}/MindSpace/${encodeURIComponent(publishKey)}/${encodedPath}`; } function buildPageLinkFromRelativePath(relativePath, publishKey, apiBaseUrl) { const normalized = normalizeWorkspaceRelativePath(relativePath); const publicUrl = resolveWorkspacePublicUrl(normalized, publishKey, apiBaseUrl); if (!publicUrl) return null; const filename = normalized.split('/').pop() || normalized; return { publicUrl, filename, title: filename.replace(/\.html$/i, '').replace(/[-_]+/g, ' '), relativePath: normalized, }; } function extractRelativePublicPageLinks(content, publishKey, apiBaseUrl) { if (!publishKey || !apiBaseUrl) return []; const links = []; const seen = new Set(); for (const match of String(content ?? '').matchAll(RELATIVE_PUBLIC_HTML_RE)) { const link = buildPageLinkFromRelativePath(match[1], publishKey, apiBaseUrl); if (!link || seen.has(link.publicUrl)) continue; seen.add(link.publicUrl); links.push(link); } return links; } function mergePageLinks(...groups) { const links = []; const seen = new Set(); for (const group of groups) { for (const link of group || []) { if (!link?.publicUrl || seen.has(link.publicUrl)) continue; seen.add(link.publicUrl); links.push(link); } } return links; } function normalizeStaticHtmlRelativePath(relativePath) { const parts = String(relativePath ?? '') .replace(/^\/+/, '') .split('/') .filter((part) => part && part !== '.' && part !== '..'); if (parts.length === 0) return ''; if (parts[0]?.toLowerCase() === 'public') return ['public', ...parts.slice(1)].join('/'); if (parts.length === 1 && parts[0].toLowerCase().endsWith('.html')) return `public/${parts[0]}`; return parts.join('/'); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function encodeUrlPath(relativePath) { return relativePath .split('/') .filter(Boolean) .map((part) => encodeURIComponent(part)) .join('/'); } function canonicalizeStaticPageUrl(publicUrl, originalRelativePath) { const canonicalRelativePath = normalizeStaticHtmlRelativePath(originalRelativePath); const originalClean = originalRelativePath.replace(/^\/+/, ''); if (!canonicalRelativePath || canonicalRelativePath === originalClean) return publicUrl; const suffix = escapeRegExp(encodeUrlPath(originalClean)); return publicUrl.replace(new RegExp(`${suffix}$`), encodeUrlPath(canonicalRelativePath)); } function extractStaticPageLinks(content) { const links = []; const seen = new Set(); const source = String(content ?? ''); for (const match of source.matchAll(URL_PATTERN)) { const relativePath = decodeSegment(match[2]); const filename = relativePath.split('/').pop() || relativePath; const publicUrl = canonicalizeStaticPageUrl(match[0], relativePath); if (seen.has(publicUrl)) continue; seen.add(publicUrl); links.push({ publicUrl, filename, title: filename.replace(/\.html$/i, '').replace(/[-_]+/g, ' '), }); } for (const match of source.matchAll(PUBLICATION_URL_PATTERN)) { const slug = decodeSegment(match[2]).replace(/\/$/, ''); const publicUrl = match[0].replace(/\/$/, ''); if (seen.has(publicUrl)) continue; seen.add(publicUrl); const filename = slug.split('/').pop() || slug; links.push({ publicUrl, filename, title: filename.replace(/\.html$/i, '').replace(/[-_]+/g, ' '), }); } return links; } function normalizeUrlForMatch(url) { return String(url ?? '').replace(/\/$/, '').trim(); } function cleanupOrphanMarkdownDecorations(text) { return String(text ?? '') .replace(/(?:\*\*){1,}/g, '') .replace(/^[👉🔗\s\-•*::]+$/gm, '') .replace(/\n{3,}/g, '\n\n') .trim(); } function stripPageDeliverableLinks(text, pageLinks) { if (!pageLinks?.length) return String(text ?? '').trim(); const urlSet = new Set(pageLinks.map((link) => normalizeUrlForMatch(link.publicUrl))); let next = String(text ?? ''); const markdownWithDecorationRe = /[👉🔗\s]*(?:\*\*)?\[([^\]]+)\]\((https?:\/\/[^)]+)\)(?:\*\*)?/g; next = next.replace(markdownWithDecorationRe, (full, _label, url) => urlSet.has(normalizeUrlForMatch(url)) ? '' : full, ); for (const url of urlSet) { next = next.replace(new RegExp(escapeRegExp(url) + '\\/?', 'gi'), ''); } return cleanupOrphanMarkdownDecorations(next); } function parseMessageParts(text, options = {}) { const { publishKey = '', apiBaseUrl = '' } = options; const input = String(text ?? ''); if (!input) return []; const parts = []; let lastIndex = 0; let match; COMBINED_LINK_RE.lastIndex = 0; while ((match = COMBINED_LINK_RE.exec(input)) !== null) { if (match.index > lastIndex) { parts.push({ type: 'text', text: input.slice(lastIndex, match.index) }); } if (match[1] && match[2]) { parts.push({ type: 'link', text: match[1], url: match[2] }); } else if (match[3]) { parts.push({ type: 'link', text: match[3], url: match[3] }); } else if (match[4]) { const relativePath = match[4].replace(/`/g, ''); const link = buildPageLinkFromRelativePath(relativePath, publishKey, apiBaseUrl); parts.push({ type: 'link', text: link?.title || relativePath, url: link?.publicUrl || relativePath, }); } else if (match[5] && match[6]) { const encodedPath = match[5].replace(/^\/+/, ''); const publicUrl = resolveWorkspacePublicUrl( encodedPath.slice(encodedPath.indexOf('public/')), match[6], apiBaseUrl, ) || (apiBaseUrl ? `${String(apiBaseUrl).replace(/\/$/, '')}/${encodedPath}` : match[5]); parts.push({ type: 'link', text: buildPageLinkFromRelativePath( encodedPath.slice(encodedPath.indexOf('public/')), match[6], apiBaseUrl, )?.title || encodedPath.split('/').pop(), url: publicUrl, }); } lastIndex = match.index + match[0].length; } if (lastIndex < input.length) { parts.push({ type: 'text', text: input.slice(lastIndex) }); } return parts.length ? parts : [{ type: 'text', text: input }]; } function getRawMessageText(message) { if (!message) return ''; if (typeof message.text === 'string') return message.text; if (Array.isArray(message.content)) { return message.content .filter((part) => part && part.type === 'text') .map((part) => part.text || '') .join('\n'); } return ''; } function getDisplayText(message) { const rawText = getRawMessageText(message); if (message?.role === 'user') { if (message.metadata?.displayText != null) { return stripImageUrlLines(message.metadata.displayText); } return stripImageUrlLines(stripUserFacingPrefixes(rawText)); } if (message?.metadata?.displayText != null) { return stripImageUrlLines(deriveAssistantFacingText(message.metadata.displayText)); } return stripImageUrlLines(deriveAssistantFacingText(rawText)); } function buildMessageView(message, options = {}) { const { publishKey = '', apiBaseUrl = '' } = options; const rawText = getRawMessageText(message); const displayText = getDisplayText(message); const sourceText = displayText || rawText; const pageLinks = mergePageLinks( extractStaticPageLinks(sourceText), extractRelativePublicPageLinks(sourceText, publishKey, apiBaseUrl), ); let text = displayText; if (!text && pageLinks.length > 0) { text = '页面已生成,可点击下方链接查看。'; } if (!text.trim()) return null; const markdownTitleByUrl = {}; for (const match of String(sourceText).matchAll(MARKDOWN_LINK_RE)) { markdownTitleByUrl[match[2]] = match[1]; } const enrichedPageLinks = pageLinks.map((link) => ({ ...link, title: markdownTitleByUrl[link.publicUrl] || link.title, })); const renderText = stripPageDeliverableLinks(text, enrichedPageLinks); const pageLinkUrls = new Set(enrichedPageLinks.map((link) => normalizeUrlForMatch(link.publicUrl))); const parts = parseMessageParts(renderText, { publishKey, apiBaseUrl }).filter( (part) => part.type !== 'link' || !pageLinkUrls.has(normalizeUrlForMatch(part.url)), ); return { id: message.id || `${message.role}_${message.created || Date.now()}`, role: message.role || 'assistant', text: renderText, copyText: renderText, parts, pageLinks: enrichedPageLinks, created: message.created || 0, }; } module.exports = { buildMessageView, extractStaticPageLinks, extractRelativePublicPageLinks, getDisplayText, parseMessageParts, resolveWorkspacePublicUrl, };