Files
memind/server/portal-mindspace-page-publish-routes.mjs
T

448 lines
13 KiB
JavaScript

import { suggestCoverMetaWithAi } from '../mindspace-cover-ai.mjs';
import { pageInternals } from '../mindspace-pages.mjs';
import { upsertPageDataPolicyIndex } from '../page-data-policy-index.mjs';
import { syncPageDataPolicyAccessMode } from '../page-data-publish-sync.mjs';
function assertRouter(api) {
if (
!api ||
typeof api.get !== 'function' ||
typeof api.post !== 'function'
) {
throw new Error(
'attachPortalMindSpacePagePublishRoutes requires an Express-compatible router',
);
}
}
export function attachPortalMindSpacePagePublishRoutes(
api,
{
env = process.env,
getMindSpacePages = () => null,
getMindSpacePublications = () => null,
getMindSpaceAudit = () => null,
getAuthPool = () => null,
sendData,
handleMindSpaceError,
previewContentSecurityPolicy =
pageInternals.previewContentSecurityPolicy,
suggestCoverMetaWithAiFn = suggestCoverMetaWithAi,
syncPageDataPolicyAccessModeFn =
syncPageDataPolicyAccessMode,
upsertPageDataPolicyIndexFn = upsertPageDataPolicyIndex,
} = {},
) {
assertRouter(api);
if (
typeof sendData !== 'function' ||
typeof handleMindSpaceError !== 'function'
) {
throw new Error(
'attachPortalMindSpacePagePublishRoutes requires response dependencies',
);
}
api.get(
'/mindspace/v1/pages/:pageId/thumbnail',
async (req, res) => {
const pages = getMindSpacePages();
if (!pages) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const svg = await pages.renderThumbnail(
req.currentUser.id,
req.params.pageId,
);
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
res.set('Cache-Control', 'private, max-age=300');
res.setHeader('X-Request-Id', req.requestId);
return res.send(svg);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/pages/:pageId/thumbnail/upload',
async (req, res) => {
const pages = getMindSpacePages();
if (!pages) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const result = await pages.uploadThumbnail(
req.currentUser.id,
req.params.pageId,
{
imageBase64: req.body?.image_base64,
mimeType: req.body?.mime_type,
title: req.body?.title,
summary: req.body?.summary,
html: req.body?.html,
},
);
return sendData(res, req, result);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/pages/:pageId/thumbnail/regenerate',
async (req, res) => {
const pages = getMindSpacePages();
if (!pages) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const useAi = Boolean(req.body?.use_ai);
const result = await pages.regenerateThumbnail(
req.currentUser.id,
req.params.pageId,
{
html: req.body?.html,
title: req.body?.title,
summary: req.body?.summary,
useAi,
instruction: req.body?.instruction,
},
{
suggestCoverMeta: useAi
? (input) => {
const pool = getAuthPool();
if (!pool) {
throw Object.assign(
new Error('LLM 服务未就绪'),
{ code: 'llm_not_configured' },
);
}
return suggestCoverMetaWithAiFn(pool, {
...input,
encryptionKey:
env.H5_SETTINGS_ENCRYPTION_KEY ??
env.TKMIND_SERVER__SECRET_KEY,
});
}
: null,
},
);
return sendData(res, req, result);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/pages/:pageId/rewrite-download-links',
async (req, res) => {
const pages = getMindSpacePages();
if (!pages) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const content = String(req.body?.content ?? '').trim();
if (!content) {
throw Object.assign(
new Error('缺少页面内容'),
{ code: 'invalid_page_input' },
);
}
const html = await pages.rewriteHtmlDownloadLinksForPage(
req.currentUser.id,
req.params.pageId,
content,
);
return sendData(res, req, { html });
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.get(
'/mindspace/v1/pages/:pageId/preview',
async (req, res) => {
const pages = getMindSpacePages();
if (!pages) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const { html, contentFormat } = await pages.renderPreview(
req.currentUser.id,
req.params.pageId,
);
res.set('Content-Type', 'text/html; charset=utf-8');
res.set(
'Content-Security-Policy',
previewContentSecurityPolicy(contentFormat),
);
res.set('Cache-Control', 'private, no-store');
res.set('X-Content-Type-Options', 'nosniff');
return res.send(html);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/pages/:pageId/preview-draft',
async (req, res) => {
const pages = getMindSpacePages();
if (!pages) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const { html, contentFormat } =
await pages.renderDraftPreview(
req.currentUser.id,
req.params.pageId,
{
title: req.body?.title,
summary: req.body?.summary,
content: req.body?.content,
templateId: req.body?.template_id,
},
);
res.set('Content-Type', 'text/html; charset=utf-8');
res.set(
'Content-Security-Policy',
previewContentSecurityPolicy(contentFormat),
);
res.set('Cache-Control', 'private, no-store');
res.set('X-Content-Type-Options', 'nosniff');
return res.send(html);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/pages/:pageId/publish-check',
async (req, res) => {
const publications = getMindSpacePublications();
if (!publications) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const result = await publications.check(
req.currentUser.id,
req.params.pageId,
{
pageVersionId: req.body?.page_version_id,
accessMode: req.body?.access_mode,
urlSlug: req.body?.url_slug,
password: req.body?.password,
expiresAt: req.body?.expires_at,
},
);
return sendData(res, req, result);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
async function transformPage(req, res, {
operation,
auditAction,
}) {
const pages = getMindSpacePages();
if (!pages) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const result = await operation(
req.currentUser.id,
req.params.pageId,
{
pageVersionId: req.body?.page_version_id,
expectedVersion: req.body?.expected_version,
title: req.body?.title,
summary: req.body?.summary,
content: req.body?.content,
},
);
await getMindSpaceAudit()?.write({
userId: req.currentUser.id,
action: auditAction,
objectType: 'page',
objectId: result.page.id,
ip: req.ip,
riskLevel: result.originalScan.riskLevel,
});
return sendData(res, req, result);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
}
api.post('/mindspace/v1/pages/:pageId/redact', (req, res) => {
const pages = getMindSpacePages();
return transformPage(req, res, {
operation: pages?.redactPage.bind(pages),
auditAction: 'page.redact',
});
});
api.post(
'/mindspace/v1/pages/:pageId/publish-fix',
(req, res) => {
const pages = getMindSpacePages();
return transformPage(req, res, {
operation:
pages?.localizePrivateResources.bind(pages),
auditAction: 'page.publish_fix',
});
},
);
api.post(
'/mindspace/v1/pages/:pageId/redacted-copy',
(req, res) => {
const pages = getMindSpacePages();
return transformPage(req, res, {
operation: pages?.redactPage.bind(pages),
auditAction: 'page.redact',
});
},
);
api.post(
'/mindspace/v1/pages/:pageId/publish',
async (req, res) => {
const publications = getMindSpacePublications();
if (!publications) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const publication = await publications.publish(
req.currentUser.id,
req.params.pageId,
{
pageVersionId: req.body?.page_version_id,
accessMode: req.body?.access_mode,
urlSlug: req.body?.url_slug,
password: req.body?.password,
expiresAt: req.body?.expires_at,
acknowledgedFindingIds:
req.body?.acknowledged_finding_ids,
},
);
const workspaceRoot =
req.currentUser.workspaceRoot ?? null;
if (workspaceRoot) {
const syncedPolicy =
syncPageDataPolicyAccessModeFn(
workspaceRoot,
req.params.pageId,
publication.accessMode,
req.currentUser.id,
);
if (syncedPolicy) {
await upsertPageDataPolicyIndexFn(
getAuthPool(),
syncedPolicy,
).catch(() => null);
}
}
await getMindSpaceAudit()?.write({
userId: req.currentUser.id,
action: 'page.publish',
objectType: 'publication',
objectId: publication.id,
ip: req.ip,
});
return sendData(res, req, publication, 201);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/publications/:publicationId/update-status',
async (req, res) => {
const publications = getMindSpacePublications();
if (!publications) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const publication =
await publications.updatePublicationStatus(
req.currentUser.id,
req.params.publicationId,
{
accessMode: req.body?.access_mode,
expiresAt: req.body?.expires_at,
},
);
await getMindSpaceAudit()?.write({
userId: req.currentUser.id,
action: 'publication.status_updated',
objectType: 'publication',
objectId: req.params.publicationId,
ip: req.ip,
});
return sendData(res, req, publication);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/publications/:publicationId/offline',
async (req, res) => {
const publications = getMindSpacePublications();
if (!publications) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const publication = await publications.offline(
req.currentUser.id,
req.params.publicationId,
);
await getMindSpaceAudit()?.write({
userId: req.currentUser.id,
action: 'page.offline',
objectType: 'publication',
objectId: req.params.publicationId,
ip: req.ip,
});
return sendData(res, req, publication);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
api.get(
'/mindspace/v1/publications/:publicationId/stats',
async (req, res) => {
const publications = getMindSpacePublications();
if (!publications) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
return sendData(
res,
req,
await publications.getStats(
req.currentUser.id,
req.params.publicationId,
),
);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
},
);
}