Files
memind/mindspace-html-download-links.mjs
T
john 722b18326f feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:08:06 +08:00

228 lines
7.7 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { buildPublicUrl } from './user-publish.mjs';
export const DOWNLOADABLE_FILE_PATTERN = /\.(?:docx?|pdf|xlsx?|pptx?|zip|csv|txt|md)$/i;
const URL_ATTR_PATTERN = /(\b(?:href|src)\s*=\s*["'])([^"']+)(["'])/gi;
export function isRelativeDownloadReference(value) {
const trimmed = String(value ?? '').trim();
if (!trimmed) return false;
const pathPart = trimmed.split('#')[0].split('?')[0];
if (/^([a-z][a-z0-9+.-]*:|\/|#|data:|mailto:|javascript:)/i.test(pathPart)) return false;
return DOWNLOADABLE_FILE_PATTERN.test(pathPart);
}
export function splitReferenceParts(relativeRef) {
const value = String(relativeRef ?? '');
const match = value.match(/^([^#?]+)(.*)$/);
return {
pathPart: match?.[1] ?? value,
suffix: match?.[2] ?? '',
};
}
export function resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef) {
const { pathPart } = splitReferenceParts(relativeRef);
const ref = String(pathPart ?? '').replace(/^\/+/, '');
if (!ref) return ref;
if (/^(?:public|oa|private)\//.test(ref)) return ref;
const htmlPath = String(htmlRelativePath ?? 'public/index.html').replace(/^\/+/, '');
const htmlDir = path.posix.dirname(htmlPath);
if (!htmlDir || htmlDir === '.') return `public/${ref}`;
return path.posix.join(htmlDir, ref).replace(/\\/g, '/');
}
export function resolveSiblingDocxPath(htmlRelativePath) {
const htmlPath = String(htmlRelativePath ?? '').replace(/^\/+/, '');
if (!htmlPath.toLowerCase().endsWith('.html')) return null;
return htmlPath.replace(/\.html$/i, '.docx');
}
function resolveExistingSiblingDocxPath(htmlRelativePath, publishDir) {
const sibling = resolveSiblingDocxPath(htmlRelativePath);
if (!sibling || !publishDir) return null;
return workspaceFileExists(publishDir, sibling) ? sibling : null;
}
export function inferWorkspaceHtmlRelativePath({
snapshotRelativePath = null,
pageTitle = null,
htmlContent = null,
publishDir = null,
} = {}) {
const fromSnapshot = String(snapshotRelativePath ?? '').trim();
if (fromSnapshot) {
const normalized = fromSnapshot.replace(/^\/+/, '');
if (normalized.toLowerCase().startsWith('public/')) return normalized;
if (normalized.toLowerCase().endsWith('.html')) return `public/${path.posix.basename(normalized)}`;
return normalized;
}
const publicDir = publishDir ? path.join(publishDir, 'public') : null;
if (!publicDir || !fs.existsSync(publicDir) || !htmlContent) {
return 'public/index.html';
}
const normalizedContent = String(htmlContent);
const title = String(pageTitle ?? '').trim();
for (const name of fs.readdirSync(publicDir)) {
if (!name.toLowerCase().endsWith('.html')) continue;
const absolutePath = path.join(publicDir, name);
try {
const diskContent = fs.readFileSync(absolutePath, 'utf8');
if (diskContent === normalizedContent) return `public/${name}`;
if (title) {
const titleMatch = diskContent.match(/<title[^>]*>([^<]+)<\/title>/i);
if (titleMatch?.[1]?.trim() === title) return `public/${name}`;
}
} catch {
// ignore unreadable files
}
}
return 'public/index.html';
}
export function buildWorkspaceDownloadLinkIndex(assets = []) {
const byBasename = new Map();
const byPath = new Map();
for (const asset of assets) {
const id = asset.id;
const filename = String(asset.original_filename ?? asset.originalFilename ?? '').replace(/\\/g, '/');
if (!id || !filename) continue;
byPath.set(filename, id);
byBasename.set(path.posix.basename(filename), id);
if (filename.startsWith('public/')) {
byPath.set(filename.slice('public/'.length), id);
} else if (!filename.includes('/')) {
byPath.set(`public/${filename}`, id);
}
}
return { byBasename, byPath };
}
export function lookupAssetIdForWorkspacePath(index, workspaceRelativePath) {
const normalized = String(workspaceRelativePath ?? '').replace(/\\/g, '/');
if (!normalized || !index) return null;
return (
index.byPath.get(normalized)
?? index.byPath.get(`public/${normalized}`)
?? index.byBasename.get(path.posix.basename(normalized))
?? null
);
}
export function buildAssetDownloadUrl(assetId) {
return `/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download`;
}
export function buildPublicWorkspaceFileUrl(publicBaseUrl, publishKey, workspaceRelativePath) {
const clean = String(workspaceRelativePath ?? '').replace(/^\/+/, '');
return buildPublicUrl(publicBaseUrl, publishKey, clean);
}
function workspaceFileExists(publishDir, workspaceRelativePath) {
if (!publishDir || !workspaceRelativePath) return false;
const absolutePath = path.join(publishDir, ...String(workspaceRelativePath).split('/'));
try {
return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
} catch {
return false;
}
}
function collectDownloadWorkspaceCandidates(htmlRelativePath, relativeRef, publishDir) {
const workspacePath = resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef);
const candidates = new Set([
workspacePath,
path.posix.join('public', path.posix.basename(workspacePath)),
]);
const siblingDocx = resolveSiblingDocxPath(htmlRelativePath);
if (siblingDocx) {
candidates.add(siblingDocx);
candidates.add(path.posix.join('public', path.posix.basename(siblingDocx)));
}
return [...candidates];
}
export function resolveDownloadTargetUrl({
relativeRef,
htmlRelativePath = 'public/index.html',
publishDir = null,
linkIndex = null,
publicBaseUrl = '',
publishKey = null,
preferAssetDownload = true,
}) {
const { suffix } = splitReferenceParts(relativeRef);
const candidates = collectDownloadWorkspaceCandidates(htmlRelativePath, relativeRef, publishDir);
for (const candidate of candidates) {
const assetId = lookupAssetIdForWorkspacePath(linkIndex, candidate);
if (preferAssetDownload && assetId) {
return `${buildAssetDownloadUrl(assetId)}${suffix}`;
}
}
if (publishDir && publishKey) {
for (const candidate of candidates) {
if (workspaceFileExists(publishDir, candidate)) {
return `${buildPublicWorkspaceFileUrl(publicBaseUrl, publishKey, candidate)}${suffix}`;
}
}
}
for (const candidate of candidates) {
const assetId = lookupAssetIdForWorkspacePath(linkIndex, candidate);
if (assetId) {
return `${buildAssetDownloadUrl(assetId)}${suffix}`;
}
}
return null;
}
export function rewriteRelativeDownloadLinks(html, options = {}) {
const source = String(html ?? '');
if (!source) return { html: source, count: 0 };
let count = 0;
const rewritten = source.replace(URL_ATTR_PATTERN, (full, prefix, url, suffix) => {
if (!isRelativeDownloadReference(url)) return full;
const resolved = resolveDownloadTargetUrl({ ...options, relativeRef: url });
if (!resolved) return full;
count += 1;
return `${prefix}${resolved}${suffix}`;
});
return { html: rewritten, count };
}
export async function loadWorkspaceDownloadLinkIndex(pool, userId) {
const [rows] = await pool.query(
`SELECT id, original_filename
FROM h5_assets
WHERE user_id = ? AND status <> 'deleted'`,
[userId],
);
return buildWorkspaceDownloadLinkIndex(rows);
}
export async function prepareHtmlDownloadLinks(pool, userId, html, options = {}) {
const linkIndex = options.linkIndex ?? (await loadWorkspaceDownloadLinkIndex(pool, userId));
return rewriteRelativeDownloadLinks(html, { ...options, linkIndex });
}
export const downloadLinkInternals = {
isRelativeDownloadReference,
resolveWorkspaceRelativeFilePath,
resolveSiblingDocxPath,
inferWorkspaceHtmlRelativePath,
buildWorkspaceDownloadLinkIndex,
lookupAssetIdForWorkspacePath,
resolveDownloadTargetUrl,
rewriteRelativeDownloadLinks,
};