feat: improve mindspace asset handling and local runtime paths
This commit is contained in:
+118
-10
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user