feat: improve mindspace asset handling and local runtime paths

This commit is contained in:
john
2026-06-28 12:18:26 +08:00
parent 4a9bc710f1
commit ea19ffb5fa
36 changed files with 867 additions and 100 deletions
+118 -10
View File
@@ -71,6 +71,9 @@ import {
buildWorkspaceThumbnailUrl,
injectHtmlBaseHref,
resolveClosestHtmlRelativePath,
createAssetDataUriResolver,
materializePrivateAssetsInWorkspaceHtml,
repairMissingHtmlAssetReferences,
resolveChatSaveAnalysis,
resolveStaticHtmlContent,
} from './mindspace-chat-save.mjs';
@@ -1559,6 +1562,9 @@ api.use(async (req, res, next) => {
if (req.path.startsWith('/internal/agent/')) return next();
if (req.path === '/agent/mindspace_page_patch') return next();
if (req.path === '/config/blocked-words') return next();
if (req.method === 'GET' && /^\/mindspace\/v1\/assets\/[^/]+\/download$/.test(req.path)) {
return next();
}
const plazaPublic = isPlazaPublicRead(req.path, req.method);
@@ -2189,17 +2195,65 @@ api.get('/mindspace/v1/authorize-image', async (req, res) => {
api.get('/mindspace/v1/assets/:assetId/download', async (req, res) => {
if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return;
try {
const { asset, path: assetPath } = await mindSpaceAssets.readAsset(
req.currentUser.id,
req.params.assetId,
);
const assetId = req.params.assetId;
const currentUserId = req.currentUser?.id ?? null;
let allowedByPublication = false;
let publicationAccessMode = null;
if (!currentUserId) {
const referrer = String(req.get('referer') ?? req.get('referrer') ?? '');
const isPublicPageReferrer = (() => {
if (!referrer) return false;
try {
return new URL(referrer, resolveRequestOrigin(req) || 'http://localhost').pathname.includes('/public/');
} catch {
return referrer.includes('/public/');
}
})();
const [refs] = await authPool.query(
`SELECT pr.access_mode, pr.expires_at
FROM h5_publication_asset_refs refs
JOIN h5_publish_records pr ON refs.publication_id = pr.id
WHERE refs.asset_id = ? AND pr.status = 'online'
ORDER BY CASE
WHEN pr.access_mode = 'public' THEN 0
WHEN pr.access_mode = 'time_limited' THEN 1
ELSE 2
END,
pr.expires_at DESC
LIMIT 1`,
[assetId],
);
const publication = refs[0] ?? null;
if (publication?.access_mode === 'public') {
allowedByPublication = true;
publicationAccessMode = 'public';
} else if (
publication?.access_mode === 'time_limited' &&
publication.expires_at &&
Number(publication.expires_at) > Date.now()
) {
allowedByPublication = true;
publicationAccessMode = 'time_limited';
}
if (!allowedByPublication && isPublicPageReferrer) {
allowedByPublication = true;
publicationAccessMode = 'public-page-referrer';
}
if (!allowedByPublication) {
return res.status(401).json({ message: '未授权,请重新登录' });
}
}
const { asset, path: assetPath } = currentUserId
? await mindSpaceAssets.readAsset(currentUserId, assetId)
: await mindSpaceAssets.readPublicAsset(assetId);
await mindSpaceAudit?.write({
userId: req.currentUser.id,
userId: req.currentUser?.id ?? null,
action: 'asset.download',
objectType: 'asset',
objectId: req.params.assetId,
objectId: assetId,
ip: req.ip,
riskLevel: asset.riskLevel,
detail: allowedByPublication ? { accessMode: publicationAccessMode } : undefined,
});
res.type(asset.mimeType);
const inline =
@@ -2207,7 +2261,7 @@ api.get('/mindspace/v1/assets/:assetId/download', async (req, res) => {
req.query.disposition === 'inline' ||
req.get('sec-fetch-dest') === 'iframe';
if (inline && asset.mimeType.startsWith('image/') && wantsInlineImageViewer(req)) {
const downloadUrl = `/api/mindspace/v1/assets/${encodeURIComponent(req.params.assetId)}/download?inline=1`;
const downloadUrl = `/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download?inline=1`;
const html = renderImageAssetViewerHtml({ asset, downloadUrl });
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Cache-Control', 'private, no-store');
@@ -2224,7 +2278,7 @@ api.get('/mindspace/v1/assets/:assetId/download', async (req, res) => {
return res.sendFile(assetPath);
} catch (error) {
await mindSpaceAudit?.write({
userId: req.currentUser.id,
userId: req.currentUser?.id ?? null,
action: 'asset.download',
objectType: 'asset',
objectId: req.params.assetId,
@@ -2384,13 +2438,53 @@ async function resolveChatSaveBundle(user, h5Root, input = {}) {
const previewSummary = String(input.previewSummary ?? input.preview_summary ?? '').trim();
const source = await resolveOwnedAssistantMessage(user.id, sessionId, messageId);
const { analysis, resolvedHtml } = await resolveChatSaveAnalysis({
let { analysis, resolvedHtml } = await resolveChatSaveAnalysis({
content: source.content,
userId: user.id,
username: user.username,
h5Root,
selectedLinkIndex,
});
if (resolvedHtml?.content && resolvedHtml.relativePath && authPool) {
const storageRoot =
process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace');
let htmlContent = resolvedHtml.content;
const repaired = await repairMissingHtmlAssetReferences({
pool: authPool,
userId: user.id,
sourceContent: source.content,
html: htmlContent,
}).catch(() => ({ html: htmlContent, changed: false }));
if (repaired.changed) {
htmlContent = repaired.html;
}
const materialized = await materializePrivateAssetsInWorkspaceHtml({
pool: authPool,
storageRoot,
h5Root,
userId: user.id,
html: htmlContent,
htmlRelativePath: resolvedHtml.relativePath,
writeBack: false,
}).catch(() => ({ html: htmlContent, count: 0, changed: false }));
if (materialized.changed) {
htmlContent = materialized.html;
}
if (htmlContent !== resolvedHtml.content) {
const publishDir = resolvePublishDir(h5Root, { id: user.id });
await fsPromises.writeFile(
path.join(publishDir, resolvedHtml.relativePath),
htmlContent,
'utf8',
);
resolvedHtml = { ...resolvedHtml, content: htmlContent };
}
}
return {
source,
analysis,
@@ -2464,10 +2558,18 @@ api.get('/mindspace/v1/pages/chat-save-thumbnail', async (req, res) => {
bundle.resolvedHtml.content,
{ title, subtitle, force: true },
).catch(() => {});
const storageRoot =
process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace');
const resolveAssetDataUri = createAssetDataUriResolver(
authPool,
storageRoot,
req.currentUser.id,
);
const svg = await generateHtmlThumbnail(publishDir, thumbRel, bundle.resolvedHtml.content, {
title,
subtitle,
contentBaseDir: path.dirname(bundle.resolvedHtml.absolute),
resolveAssetDataUri,
force: true,
});
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
@@ -2482,6 +2584,8 @@ api.get('/mindspace/v1/pages/chat-save-thumbnail', async (req, res) => {
api.post('/mindspace/v1/pages/analyze-chat-save', async (req, res) => {
if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
try {
const sessionId = req.body?.session_id;
const messageId = req.body?.message_id;
const bundle = await resolveChatSaveBundle(req.currentUser, __dirname, req.body);
const {
source,
@@ -2687,7 +2791,11 @@ api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
: null;
let page;
if (replacePageId) {
page = await mindSpacePages.updatePage(req.currentUser.id, replacePageId, pageInput);
const existingPage = await mindSpacePages.getPage(req.currentUser.id, replacePageId);
page = await mindSpacePages.updatePage(req.currentUser.id, replacePageId, {
...pageInput,
expectedVersion: existingPage.versionNo,
});
} else {
page = await mindSpacePages.createFromChat(req.currentUser.id, pageInput, {
sessionId: req.body.session_id,