feat(mindspace): 全部页面管理、级联删除与页面同步去重

支持全部页面分页/多选/删除/分享,删除时可选移除广场帖并清理工作区附件;修复并发同步重复创建页面,并补齐预览下载链接重写与 iframe 下载权限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 01:22:38 +08:00
parent 4e7ef3d853
commit 66ca0b3bec
30 changed files with 2562 additions and 351 deletions
+54 -17
View File
@@ -300,6 +300,7 @@ async function bootstrapUserAuth() {
},
});
mindSpacePublications = createPublicationService(pool, {
h5Root: __dirname,
storageRoot:
process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5),
@@ -2664,21 +2665,35 @@ async function resolveOwnedAssistantMessage(userId, sessionId, messageId) {
const SAVE_TARGET_CATEGORIES = new Set(['draft', 'oa', 'public']);
const pageSyncInFlight = new Map();
async function syncUserGeneratedPages(userId) {
if (!mindSpacePages || !authPool || !userId) return;
await syncGeneratedPagesFromPublicAssets({
pool: authPool,
pageService: mindSpacePages,
assetService: mindSpaceAssets,
userId,
publishDir: resolvePublishDir(__dirname, { id: userId }),
syncWorkspaceAssets:
mindSpaceAssets && WORKSPACE_MAINTENANCE_ENABLED
? (targetUserId, options) => mindSpaceAssets.syncWorkspaceAssets(targetUserId, options)
: null,
}).catch((error) => {
console.warn('[MindSpace] page sync failed:', error?.message ?? error);
});
let inFlight = pageSyncInFlight.get(userId);
if (!inFlight) {
inFlight = syncGeneratedPagesFromPublicAssets({
pool: authPool,
pageService: mindSpacePages,
assetService: mindSpaceAssets,
userId,
publishDir: resolvePublishDir(__dirname, { id: userId }),
syncWorkspaceAssets:
mindSpaceAssets && WORKSPACE_MAINTENANCE_ENABLED
? (targetUserId, options) => mindSpaceAssets.syncWorkspaceAssets(targetUserId, options)
: null,
})
.catch((error) => {
console.warn('[MindSpace] page sync failed:', error?.message ?? error);
})
.finally(() => {
if (pageSyncInFlight.get(userId) === inFlight) {
pageSyncInFlight.delete(userId);
}
});
pageSyncInFlight.set(userId, inFlight);
}
await inFlight;
}
async function resolveChatSaveBundle(user, h5Root, input = {}) {
@@ -3083,10 +3098,24 @@ api.get('/mindspace/v1/pages', async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
try {
await syncUserGeneratedPages(req.currentUser.id);
const pages = await mindSpacePages.listPages(req.currentUser.id, {
const limit = Number.parseInt(String(req.query.limit ?? ''), 10);
const offset = Number.parseInt(String(req.query.offset ?? ''), 10);
const result = await mindSpacePages.listPages(req.currentUser.id, {
status: typeof req.query.status === 'string' ? req.query.status : undefined,
categoryCode: typeof req.query.category_code === 'string' ? req.query.category_code : undefined,
...(Number.isFinite(limit) ? { limit } : {}),
...(Number.isFinite(offset) ? { offset } : {}),
});
return res.json({
data: result.items,
page: {
total: result.total,
limit: result.limit,
offset: result.offset,
has_more: result.hasMore,
next_cursor: null,
},
});
return res.json({ data: pages, page: { next_cursor: null, has_more: false } });
} catch (error) {
return mindSpaceError(res, req, error);
}
@@ -3109,7 +3138,14 @@ api.get('/mindspace/v1/pages/:pageId', async (req, res) => {
api.delete('/mindspace/v1/pages/:pageId', async (req, res) => {
if (!mindSpacePages || !ensureMindSpaceEnabled(res, req)) return;
try {
const result = await mindSpacePages.deletePage(req.currentUser.id, req.params.pageId);
const removeFromPlaza =
String(req.query?.remove_from_plaza ?? req.body?.remove_from_plaza ?? '').toLowerCase() ===
'true' ||
req.query?.remove_from_plaza === '1' ||
req.body?.remove_from_plaza === true;
const result = await mindSpacePages.deletePage(req.currentUser.id, req.params.pageId, {
removeFromPlaza,
});
const quota = await mindSpace.getQuota(req.currentUser.id);
await mindSpaceAudit?.write({
userId: req.currentUser.id,
@@ -3119,6 +3155,7 @@ api.delete('/mindspace/v1/pages/:pageId', async (req, res) => {
ip: req.ip,
detail: {
offlinedPublicationCount: result.offlinedPublicationCount,
hiddenPlazaPostCount: result.hiddenPlazaPostCount,
deletedAssetCount: result.deletedAssetCount,
freedBytes: result.freedBytes,
},
@@ -4434,7 +4471,7 @@ function publishedPageShellHtml({ iframeUrl, shareUrl, title }) {
<body>
<div class="publication-shell">
<!-- Game-like publications need script execution, but not same-origin sandbox escape. -->
<iframe class="publication-frame" title="${safeTitle}" src="${iframeSrc}" sandbox="allow-scripts"></iframe>
<iframe class="publication-frame" title="${safeTitle}" src="${iframeSrc}" sandbox="allow-scripts allow-downloads"></iframe>
</div>
<button type="button" class="publication-share-fab" id="publication-share-fab">分享</button>
<div class="publication-share-sheet" id="publication-share-sheet" hidden>