Files
memind/miniapp/utils/message-display.js
john c842119929 fix(miniapp): add missing uuid and message-display utils
Commit the utility modules required by api.js, messages.js, and chat page
so WeChat DevTools can resolve require('./uuid') at runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 13:01:05 +08:00

337 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};