Record published companion artifacts

This commit is contained in:
john
2026-07-02 21:47:10 +08:00
parent b38dd5961f
commit c86a3fd15b
2 changed files with 178 additions and 0 deletions
+105
View File
@@ -7,7 +7,9 @@ import { pageInternals } from './mindspace-pages.mjs';
import { loadMindSpaceConfig } from './mindspace-config.mjs';
import { buildPublicUrl, resolvePublicBaseUrl, resolvePublishDir } from './user-publish.mjs';
import {
isRelativeDownloadReference,
loadWorkspaceDownloadLinkIndex,
resolveWorkspaceRelativeFilePath,
rewriteRelativeDownloadLinks,
inferWorkspaceHtmlRelativePath,
} from './mindspace-html-download-links.mjs';
@@ -16,6 +18,7 @@ import { createImgproxySigner } from './imgproxy-signer.mjs';
const SCANNER_VERSION = 'mindspace-content-v1';
const PRIVATE_ASSET_DOWNLOAD_URL_PATTERN =
/(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi;
const URL_ATTR_PATTERN = /(\b(?:href|src)\s*=\s*["'])([^"']+)(["'])/gi;
const ACCESS_MODES = new Set([
'public',
'password',
@@ -165,6 +168,71 @@ function buildPublicationThumbnailFallback(ownerSlug, urlSlug) {
return `/u/${encodeURIComponent(ownerSlug)}/pages/${encodeURIComponent(urlSlug)}.thumbnail.png`;
}
function companionKindForWorkspacePath(workspacePath) {
const normalized = String(workspacePath ?? '').toLowerCase();
if (normalized.endsWith('.long.png')) return 'long_image';
if (normalized.endsWith('.docx') || normalized.endsWith('.doc')) return 'docx';
if (normalized.endsWith('.pdf')) return 'pdf';
return 'generated_file';
}
function companionMimeForWorkspacePath(workspacePath) {
const normalized = String(workspacePath ?? '').toLowerCase();
if (normalized.endsWith('.long.png') || normalized.endsWith('.png')) return 'image/png';
if (normalized.endsWith('.docx')) {
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
}
if (normalized.endsWith('.doc')) return 'application/msword';
if (normalized.endsWith('.pdf')) return 'application/pdf';
if (normalized.endsWith('.csv')) return 'text/csv';
if (normalized.endsWith('.md')) return 'text/markdown';
if (normalized.endsWith('.txt')) return 'text/plain';
return 'application/octet-stream';
}
async function statWorkspaceFile(publishDir, workspacePath) {
if (!publishDir || !workspacePath) return null;
const normalized = String(workspacePath).replace(/^\/+/, '').replace(/\\/g, '/');
const root = path.resolve(publishDir);
const absolutePath = path.resolve(root, ...normalized.split('/'));
if (absolutePath !== root && !absolutePath.startsWith(`${root}${path.sep}`)) return null;
try {
const stat = await fs.stat(absolutePath);
if (!stat.isFile()) return null;
return { workspacePath: normalized, sizeBytes: stat.size };
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
return null;
}
}
async function collectPublicationCompanionArtifacts({
publishDir,
html = '',
htmlRelativePath = null,
}) {
if (!publishDir || !htmlRelativePath) return [];
const candidates = new Set();
const normalizedHtmlPath = String(htmlRelativePath).replace(/^\/+/, '').replace(/\\/g, '/');
if (normalizedHtmlPath.toLowerCase().endsWith('.html')) {
candidates.add(normalizedHtmlPath.replace(/\.html$/i, '.docx'));
candidates.add(normalizedHtmlPath.replace(/\.html$/i, '.long.png'));
}
let match;
while ((match = URL_ATTR_PATTERN.exec(String(html ?? ''))) !== null) {
const ref = match[2];
if (isRelativeDownloadReference(ref)) {
candidates.add(resolveWorkspaceRelativeFilePath(normalizedHtmlPath, ref));
}
}
const artifacts = [];
for (const candidate of candidates) {
const stat = await statWorkspaceFile(publishDir, candidate);
if (stat) artifacts.push(stat);
}
return artifacts;
}
const PUBLIC_IMAGE_STORAGE_KEY_PATTERN = /^users\/([^/]+)\/images\/(\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)$/i;
function storageKeyToPublicStandardImageUrl(storageKey, publicBaseUrl) {
@@ -755,6 +823,8 @@ export function createPublicationService(pool, options = {}) {
bundleAssetId,
storageKey,
htmlBytes,
html: page.content,
publishDir: h5Root ? resolvePublishDir(h5Root, { id: userId }) : null,
now,
});
return publication;
@@ -1067,6 +1137,8 @@ async function registerPublicationArtifactForConversation({
bundleAssetId,
storageKey,
htmlBytes,
html,
publishDir,
now,
}) {
const sessionId = String(page?.source_session_id ?? '').trim();
@@ -1095,6 +1167,38 @@ async function registerPublicationArtifactForConversation({
sortOrder: Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0,
now,
});
const htmlRelativePath = page.source_relative_path ?? null;
const companionArtifacts = await collectPublicationCompanionArtifacts({
publishDir,
html,
htmlRelativePath,
});
await Promise.all(
companionArtifacts.map((companion, index) =>
registry.recordArtifact({
id: `ca_${publication.id}_${crypto
.createHash('sha256')
.update(companion.workspacePath)
.digest('hex')
.slice(0, 12)}`,
packageId: packageRecord.id,
artifactKind: companionKindForWorkspacePath(companion.workspacePath),
role: 'assistant',
pageId: page.page_id,
publicationId: publication.id,
messageId: page.source_message_id ?? null,
displayName: path.posix.basename(companion.workspacePath),
mimeType: companionMimeForWorkspacePath(companion.workspacePath),
sizeBytes: companion.sizeBytes,
canonicalUrl: buildPublicUrl(resolvePublicBaseUrl(), userId, companion.workspacePath),
sortOrder:
(Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0) +
index +
1,
now,
}),
),
);
await registry.writeManifestForSession({ userId, sessionId });
return artifact;
} catch (error) {
@@ -1121,6 +1225,7 @@ export const publicationInternals = {
workspacePublicAssetRelativeUrl,
rewriteWorkspacePublicAssetReferences,
localizePrivateImageReferences,
collectPublicationCompanionArtifacts,
prepareHtmlPublishContent,
registerPublicationArtifactForConversation,
scanContent, // re-exported from mindspace-content-scan.mjs