Files
memind/mindspace-conversation-package-artifact-service.mjs
2026-07-27 15:34:35 +08:00

343 lines
9.0 KiB
JavaScript

import crypto from 'node:crypto';
import path from 'node:path';
import { registerChatDocxArtifactForConversation } from './mindspace-chat-docx-package.mjs';
import { registerPublicHtmlArtifactsForConversationPackage } from './mindspace-conversation-package-public-html.mjs';
import { resolveMindSpaceUserPublishDir } from './mindspace-runtime-config.mjs';
function normalizeBuffer(value) {
if (Buffer.isBuffer(value)) return value;
if (typeof value === 'string') {
return Buffer.from(value, 'utf8');
}
if (ArrayBuffer.isView(value)) {
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
}
return null;
}
function generatedArtifactMetadata(
relativePath,
) {
const extension = path.posix
.extname(relativePath)
.toLowerCase();
const byExtension = {
'.csv': [
'generated_file',
'text/csv',
],
'.doc': [
'docx',
'application/msword',
],
'.docx': [
'docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
],
'.gif': ['generated_image', 'image/gif'],
'.jpeg': [
'generated_image',
'image/jpeg',
],
'.jpg': [
'generated_image',
'image/jpeg',
],
'.json': [
'generated_file',
'application/json',
],
'.md': [
'generated_file',
'text/markdown',
],
'.pdf': ['pdf', 'application/pdf'],
'.png': [
'generated_image',
'image/png',
],
'.svg': [
'generated_image',
'image/svg+xml',
],
'.txt': [
'generated_file',
'text/plain',
],
'.webp': [
'generated_image',
'image/webp',
],
'.xlsx': [
'generated_file',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
};
const [artifactKind, mimeType] =
byExtension[extension] ?? [
'generated_file',
'application/octet-stream',
];
return { artifactKind, mimeType };
}
export function createMindSpaceConversationArtifactService({
conversationPackageRegistry,
h5Root,
env = process.env,
resolveMindSpaceUserPublishDirFn =
resolveMindSpaceUserPublishDir,
registerPublicHtmlArtifactsFn =
registerPublicHtmlArtifactsForConversationPackage,
registerChatDocxArtifactFn =
registerChatDocxArtifactForConversation,
} = {}) {
if (!conversationPackageRegistry) {
throw new Error(
'createMindSpaceConversationArtifactService requires a conversation package registry',
);
}
if (!h5Root) {
throw new Error(
'createMindSpaceConversationArtifactService requires h5Root',
);
}
return {
async registerPublicHtmlArtifacts({
userId,
sessionId,
title = null,
relativePaths = [],
artifactRefs = [],
} = {}) {
const normalizedUserId = String(userId ?? '').trim();
if (
!normalizedUserId ||
!String(sessionId ?? '').trim()
) {
return [];
}
const user = { id: normalizedUserId };
const publishDir = resolveMindSpaceUserPublishDirFn(
h5Root,
user,
);
return registerPublicHtmlArtifactsFn({
conversationPackageRegistry,
h5Root,
env,
user,
publishDir,
sessionId,
title,
relativePaths,
artifactRefs,
});
},
async registerChatDocxArtifact({
userId,
sessionId,
messageId,
selectedLinkIndex = 0,
title = null,
filename,
body,
} = {}) {
const buffer = normalizeBuffer(body);
if (
!String(userId ?? '').trim() ||
!String(sessionId ?? '').trim() ||
!String(messageId ?? '').trim() ||
!String(filename ?? '').trim() ||
!buffer
) {
return null;
}
return registerChatDocxArtifactFn({
registry: conversationPackageRegistry,
user: { id: String(userId).trim() },
bundle: {
source: {
session: {
name: String(title ?? '').trim() || null,
},
},
},
requestBody: {
session_id: String(sessionId).trim(),
message_id: String(messageId).trim(),
selected_link_index: selectedLinkIndex,
},
filename: String(filename).trim(),
buffer,
});
},
async registerPublishedLongImageArtifact({
ownerId,
sessionId,
messageId = null,
publicationId,
pageId = null,
title = null,
filename = null,
body,
canonicalUrl = null,
} = {}) {
const normalizedOwnerId = String(ownerId ?? '').trim();
const normalizedSessionId = String(sessionId ?? '').trim();
const normalizedPublicationId =
String(publicationId ?? '').trim();
const buffer = normalizeBuffer(body);
if (
!normalizedOwnerId ||
!normalizedSessionId ||
!normalizedPublicationId ||
!buffer
) {
return null;
}
const hash = crypto
.createHash('sha256')
.update(
`${normalizedOwnerId}:${normalizedSessionId}:${normalizedPublicationId}:long-image`,
)
.digest('hex')
.slice(0, 16);
const artifactId = `ca_long_image_${hash}`;
const displayName =
String(filename ?? '').trim() ||
`${normalizedPublicationId}.long.png`;
const relativePath =
`artifacts/${artifactId}/${path.posix.basename(displayName)}`;
const now = Date.now();
const { packageRecord, writeResult } =
await conversationPackageRegistry.putObjectForSession({
userId: normalizedOwnerId,
sessionId: normalizedSessionId,
title: String(title ?? '').trim() || null,
relativePath,
body: buffer,
});
const artifact =
await conversationPackageRegistry.recordArtifact({
id: artifactId,
packageId: packageRecord.id,
artifactKind: 'long_image',
role: 'assistant',
pageId: String(pageId ?? '').trim() || null,
publicationId: normalizedPublicationId,
messageId: String(messageId ?? '').trim() || null,
displayName: path.posix.basename(displayName),
mimeType: 'image/png',
sizeBytes: buffer.length,
storageKey: writeResult.key,
canonicalUrl:
String(canonicalUrl ?? '').trim() || null,
sortOrder: now,
now,
});
await conversationPackageRegistry.writeManifestForSession({
userId: normalizedOwnerId,
sessionId: normalizedSessionId,
});
return artifact;
},
async registerWorkspaceFileArtifact({
userId,
sessionId,
messageId = null,
title = null,
relativePath,
body,
} = {}) {
const normalizedUserId = String(
userId ?? '',
).trim();
const normalizedSessionId = String(
sessionId ?? '',
).trim();
const normalizedRelativePath = String(
relativePath ?? '',
)
.trim()
.replace(/\\/g, '/');
const buffer = normalizeBuffer(body);
if (
!normalizedUserId ||
!normalizedSessionId ||
!normalizedRelativePath ||
!buffer
) {
return null;
}
const hash = crypto
.createHash('sha256')
.update(
`${normalizedUserId}:${normalizedSessionId}:${normalizedRelativePath}`,
)
.digest('hex')
.slice(0, 20);
const artifactId =
`ca_workspace_write_${hash}`;
const displayName =
path.posix.basename(
normalizedRelativePath,
);
const packageRelativePath =
`artifacts/${artifactId}/${displayName}`;
const metadata =
generatedArtifactMetadata(
normalizedRelativePath,
);
const now = Date.now();
const { packageRecord, writeResult } =
await conversationPackageRegistry
.putObjectForSession({
userId: normalizedUserId,
sessionId: normalizedSessionId,
title:
String(title ?? '').trim() ||
null,
relativePath:
packageRelativePath,
body: buffer,
});
const artifact =
await conversationPackageRegistry
.recordArtifact({
id: artifactId,
packageId: packageRecord.id,
artifactKind:
metadata.artifactKind,
role: 'assistant',
messageId:
String(messageId ?? '').trim() ||
null,
displayName,
mimeType: metadata.mimeType,
sizeBytes: buffer.length,
storageKey: writeResult.key,
canonicalUrl: null,
sortOrder: now,
now,
});
await conversationPackageRegistry
.writeManifestForSession({
userId: normalizedUserId,
sessionId: normalizedSessionId,
});
return artifact;
},
};
}
export const mindSpaceConversationArtifactServiceInternals = {
normalizeBuffer,
generatedArtifactMetadata,
};