Files
memind/server/portal-mindspace-chat-share-routes.mjs
T

333 lines
9.3 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import { inlinePrivateAssetsInHtml } from '../mindspace-pages.mjs';
import {
getQuickPlazaFromPublicHtmlStatus,
quickPlazaFromChat,
quickPlazaFromPublicHtml,
} from '../mindspace-chat-plaza.mjs';
import { rewriteWorkspacePublicAssetReferences } from '../mindspace-publications.mjs';
import {
buildMindSpacePublicUrlForUser,
resolveMindSpaceRuntimeConfig,
resolveMindSpaceUserPublishDir,
} from '../mindspace-runtime-config.mjs';
import { PUBLIC_ZONE_DIR } from '../user-publish.mjs';
function assertRouter(api) {
if (
!api ||
typeof api.get !== 'function' ||
typeof api.post !== 'function'
) {
throw new Error(
'attachPortalMindSpaceChatShareRoutes requires an Express-compatible router',
);
}
}
export function attachPortalMindSpaceChatShareRoutes(
api,
{
h5Root = '',
env = process.env,
getMindSpacePages = () => null,
getMindSpacePublications = () => null,
getPlazaPosts = () => null,
getAuthPool = () => null,
resolveChatSaveBundle,
resolvePlazaPostUrl,
mapPlazaError = () => 500,
sendData = (res, _req, data, status = 200) =>
res.status(status).json({ data }),
sendError = (res, _req, status, code, message) =>
res.status(status).json({ error: { code, message } }),
handlePlazaError = (_res, _req, error) => {
throw error;
},
handleMindSpaceError = (_res, _req, error) => {
throw error;
},
resolveMindSpaceRuntimeConfigFn = resolveMindSpaceRuntimeConfig,
inlinePrivateAssetsInHtmlFn = inlinePrivateAssetsInHtml,
resolveMindSpaceUserPublishDirFn = resolveMindSpaceUserPublishDir,
rewriteWorkspacePublicAssetReferencesFn =
rewriteWorkspacePublicAssetReferences,
buildMindSpacePublicUrlForUserFn =
buildMindSpacePublicUrlForUser,
quickPlazaFromChatFn = quickPlazaFromChat,
getQuickPlazaFromPublicHtmlStatusFn =
getQuickPlazaFromPublicHtmlStatus,
quickPlazaFromPublicHtmlFn = quickPlazaFromPublicHtml,
mkdir = fs.mkdir,
writeFile = fs.writeFile,
randomUUID = crypto.randomUUID,
now = Date.now,
logger = console,
} = {},
) {
assertRouter(api);
if (
typeof resolveChatSaveBundle !== 'function' ||
typeof resolvePlazaPostUrl !== 'function'
) {
throw new Error(
'attachPortalMindSpaceChatShareRoutes requires chat and Plaza resolvers',
);
}
api.post('/mindspace/v1/pages/quick-share-from-chat', async (req, res) => {
if (!getMindSpacePages()) {
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
const bundle = await resolveChatSaveBundle(
req.currentUser,
h5Root,
req.body,
);
logger.log(
'[quick-share] bundle resolved, hasHtml:',
Boolean(bundle.resolvedHtml),
);
if (!bundle.resolvedHtml) {
throw Object.assign(new Error('无法读取链接页面内容'), {
code: 'static_page_not_found',
});
}
const { storageRoot } = resolveMindSpaceRuntimeConfigFn(
h5Root,
env,
);
const { html: localizedHtml } =
await inlinePrivateAssetsInHtmlFn(
getAuthPool(),
storageRoot,
req.currentUser.id,
bundle.resolvedHtml.content,
);
const publishDir = resolveMindSpaceUserPublishDirFn(
h5Root,
req.currentUser,
);
const sharedDir = path.join(
publishDir,
PUBLIC_ZONE_DIR,
'shared',
);
await mkdir(sharedDir, { recursive: true });
const basename = bundle.resolvedHtml.filename.replace(
/\.html$/i,
'',
);
const filename =
`${basename}-${randomUUID().slice(0, 8)}.html`;
const sharedRelativePath =
`${PUBLIC_ZONE_DIR}/shared/${filename}`;
const sharedHtml =
rewriteWorkspacePublicAssetReferencesFn(
localizedHtml,
sharedRelativePath,
);
const destPath = path.join(sharedDir, filename);
await writeFile(destPath, sharedHtml, 'utf8');
const publicUrl = buildMindSpacePublicUrlForUserFn({
h5Root,
env,
user: req.currentUser.id,
relativePath: sharedRelativePath,
});
return res.status(201).json({
data: {
publicUrl,
filename,
},
});
} catch (error) {
logger.error('[quick-share] error:', error);
return handleMindSpaceError(res, req, error);
}
});
api.post('/mindspace/v1/pages/quick-plaza-from-chat', async (req, res) => {
const pages = getMindSpacePages();
const publications = getMindSpacePublications();
const plazaPosts = getPlazaPosts();
if (!pages || !publications || !plazaPosts) {
return sendError(
res,
req,
503,
'plaza_unavailable',
'Plaza 或 MindSpace 未启用',
);
}
const startedAt = now();
try {
const bundle = await resolveChatSaveBundle(
req.currentUser,
h5Root,
req.body,
);
const publishDir = resolveMindSpaceUserPublishDirFn(
h5Root,
req.currentUser,
);
const result = await quickPlazaFromChatFn({
user: req.currentUser,
h5Root,
bundle,
body: req.body ?? {},
mindSpacePages: pages,
mindSpacePublications: publications,
plazaPosts,
publishDir,
});
logger.info('[quick-plaza] ok', {
ms: now() - startedAt,
pageId: result.pageId,
publicationId: result.publicationId,
postId: result.post?.id,
});
return sendData(res, req, result, 201);
} catch (error) {
logger.error('[quick-plaza] error:', {
ms: now() - startedAt,
error,
});
if (error?.code && mapPlazaError(error) !== 500) {
return handlePlazaError(res, req, error);
}
return handleMindSpaceError(res, req, error);
}
});
api.get(
'/mindspace/v1/pages/quick-plaza-from-public-html/status',
async (req, res) => {
const pages = getMindSpacePages();
const publications = getMindSpacePublications();
const plazaPosts = getPlazaPosts();
if (!pages || !publications || !plazaPosts) {
return sendError(
res,
req,
503,
'plaza_unavailable',
'Plaza 或 MindSpace 未启用',
);
}
try {
const relativePath = String(
req.query?.relative_path ??
req.query?.relativePath ??
'',
).trim();
const result =
await getQuickPlazaFromPublicHtmlStatusFn({
user: req.currentUser,
relativePath,
mindSpacePages: pages,
mindSpacePublications: publications,
plazaPosts,
});
const postId = result.post?.id;
return sendData(res, req, {
...result,
plaza_url: postId
? resolvePlazaPostUrl(postId, req)
: null,
});
} catch (error) {
if (error?.code && mapPlazaError(error) !== 500) {
return handlePlazaError(res, req, error);
}
return handleMindSpaceError(res, req, error);
}
},
);
api.post(
'/mindspace/v1/pages/quick-plaza-from-public-html',
async (req, res) => {
const pages = getMindSpacePages();
const publications = getMindSpacePublications();
const plazaPosts = getPlazaPosts();
if (!pages || !publications || !plazaPosts) {
return sendError(
res,
req,
503,
'plaza_unavailable',
'Plaza 或 MindSpace 未启用',
);
}
const startedAt = now();
try {
const relativePath = String(
req.body?.relative_path ??
req.body?.relativePath ??
'',
).trim();
const publishDir = resolveMindSpaceUserPublishDirFn(
h5Root,
req.currentUser,
);
const result = await quickPlazaFromPublicHtmlFn({
user: req.currentUser,
relativePath,
mindSpacePages: pages,
mindSpacePublications: publications,
plazaPosts,
publishDir,
});
const plazaUrl = resolvePlazaPostUrl(
result.post.id,
req,
);
logger.info('[quick-plaza-public-html] ok', {
ms: now() - startedAt,
pageId: result.pageId,
publicationId: result.publicationId,
postId: result.post?.id,
relativePath,
});
return sendData(
res,
req,
{
...result,
plaza_url: plazaUrl,
},
201,
);
} catch (error) {
logger.error('[quick-plaza-public-html] error:', {
ms: now() - startedAt,
error,
});
if (error?.code === 'ALREADY_PUBLISHED') {
const postId =
error?.details?.post_id ?? error?.details?.postId;
if (postId) {
error.details = {
...(error.details ?? {}),
plaza_url: resolvePlazaPostUrl(postId, req),
};
}
}
if (error?.code && mapPlazaError(error) !== 500) {
return handlePlazaError(res, req, error);
}
return handleMindSpaceError(res, req, error);
}
},
);
}