Files
memind/src/utils/messageSave.ts
T
john 9b4a25799f Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks
response templates by message type and intent (translate, summary,
rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync,
zero I/O, auto-falls back to config.ackText on any error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 15:19:03 +08:00

92 lines
3.2 KiB
TypeScript

const URL_PATTERN =
/https?:\/\/[^\s<>"')\]]+\/(?:MindSpace|temp)\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi;
export type MessageSaveKind = 'page' | 'article';
export type MessageSaveOwner = {
userId?: string | null;
username?: string | null;
};
export type MessageSaveActions = {
kind: MessageSaveKind;
previewUrl: string | null;
links: Array<{ publicUrl: string; filename: string }>;
};
function decodeSegment(segment: string) {
try {
return decodeURIComponent(segment);
} catch {
return segment;
}
}
function normalizeOwnerOptions(owner?: string | MessageSaveOwner): MessageSaveOwner {
if (typeof owner === 'string') return { username: owner };
return owner ?? {};
}
function normalizeStaticHtmlRelativePath(relativePath: string) {
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: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function encodeUrlPath(relativePath: string) {
return relativePath
.split('/')
.filter(Boolean)
.map((part) => encodeURIComponent(part))
.join('/');
}
function canonicalizeStaticPageUrl(publicUrl: string, originalRelativePath: string) {
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));
}
export function extractStaticPageLinks(content: string, owner?: string | MessageSaveOwner) {
const { userId, username } = normalizeOwnerOptions(owner);
const normalizedUserId = userId?.trim().toLowerCase();
const normalizedUsername = username?.trim().toLowerCase();
const links: Array<{ publicUrl: string; filename: string }> = [];
const seen = new Set<string>();
for (const match of content.matchAll(URL_PATTERN)) {
const linkOwner = decodeSegment(match[1]).toLowerCase();
const relativePath = decodeSegment(match[2]);
const filename = relativePath.split('/').pop() ?? relativePath;
if (normalizedUserId) {
if (linkOwner !== normalizedUserId && linkOwner !== normalizedUsername) continue;
} else if (normalizedUsername && linkOwner !== normalizedUsername) {
continue;
}
const publicUrl = canonicalizeStaticPageUrl(match[0], relativePath);
if (seen.has(publicUrl)) continue;
seen.add(publicUrl);
links.push({ publicUrl, filename });
}
return links;
}
export function getMessageSaveActions(content: string, owner?: string | MessageSaveOwner): MessageSaveActions {
const links = extractStaticPageLinks(content, owner);
return {
kind: links.length > 0 ? 'page' : 'article',
previewUrl: links[0]?.publicUrl ?? null,
links,
};
}