Files
memind/mindspace-page-purge.mjs
john ea6c49f046 feat(mindspace): 全部页面管理、级联删除与页面同步去重
支持全部页面分页/多选/删除/分享,删除时可选移除广场帖并清理工作区附件;修复并发同步重复创建页面,并补齐预览下载链接重写与 iframe 下载权限。

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

88 lines
2.7 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import {
isRelativeDownloadReference,
resolveWorkspaceRelativeFilePath,
splitReferenceParts,
} from './mindspace-html-download-links.mjs';
import { workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
const PRIVATE_ASSET_URL_PATTERN =
/(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi;
const URL_ATTR_PATTERN = /(\b(?:href|src)\s*=\s*["'])([^"']+)(["'])/gi;
export function extractAssetIdsFromHtml(html) {
return [
...new Set(
[...String(html ?? '').matchAll(PRIVATE_ASSET_URL_PATTERN)]
.map((match) => match[1])
.filter(Boolean),
),
];
}
export function extractRelativeDownloadRefs(html) {
const refs = new Set();
for (const match of String(html ?? '').matchAll(URL_ATTR_PATTERN)) {
const url = match[2];
if (isRelativeDownloadReference(url)) {
refs.add(splitReferenceParts(url).pathPart);
}
}
return [...refs];
}
export function collectWorkspacePurgeTargets(htmlRelativePath, html) {
const targets = new Set();
const normalizedHtmlPath = String(htmlRelativePath ?? '').replace(/^\/+/, '');
if (normalizedHtmlPath) {
targets.add(normalizedHtmlPath);
if (normalizedHtmlPath.toLowerCase().endsWith('.html')) {
targets.add(workspaceThumbnailRelativePath(normalizedHtmlPath));
const pngThumb = normalizedHtmlPath.replace(/\.html$/i, '.thumbnail.png');
if (pngThumb !== normalizedHtmlPath) targets.add(pngThumb);
}
}
for (const ref of extractRelativeDownloadRefs(html)) {
targets.add(resolveWorkspaceRelativeFilePath(normalizedHtmlPath || 'public/index.html', ref));
}
return [...targets].filter(Boolean);
}
export async function purgeWorkspacePageArtifacts({
publishDir,
htmlRelativePath = null,
html = '',
} = {}) {
if (!publishDir) return { removed: [], skipped: [] };
const resolvedRoot = path.resolve(publishDir);
const removed = [];
const skipped = [];
for (const relativeTarget of collectWorkspacePurgeTargets(htmlRelativePath, html)) {
const absolutePath = path.resolve(publishDir, ...relativeTarget.split('/'));
if (
absolutePath !== resolvedRoot &&
!absolutePath.startsWith(`${resolvedRoot}${path.sep}`)
) {
skipped.push(relativeTarget);
continue;
}
try {
await fs.unlink(absolutePath);
removed.push(relativeTarget);
} catch (error) {
if (error?.code !== 'ENOENT') skipped.push(relativeTarget);
}
}
return { removed, skipped };
}
export const pagePurgeInternals = {
extractAssetIdsFromHtml,
extractRelativeDownloadRefs,
collectWorkspacePurgeTargets,
};