722b18326f
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
7.7 KiB
JavaScript
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,
|
|
};
|