9b4a25799f
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>
92 lines
3.2 KiB
TypeScript
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,
|
|
};
|
|
}
|