241 lines
8.7 KiB
JavaScript
241 lines
8.7 KiB
JavaScript
const PACKAGE_ID_PREFIX = 'cp_';
|
|
const ARTIFACT_ID_PREFIX = 'ca_';
|
|
|
|
export const CONVERSATION_PACKAGE_SCHEMA_VERSION = 1;
|
|
|
|
export const CONVERSATION_ARTIFACT_KINDS = Object.freeze([
|
|
'input_image',
|
|
'input_file',
|
|
'generated_image',
|
|
'generated_file',
|
|
'page',
|
|
'public_html',
|
|
'long_image',
|
|
'thumbnail',
|
|
'docx',
|
|
'pdf',
|
|
]);
|
|
|
|
const MYSQL_SIGNED_INT_MAX = 2147483647;
|
|
const MYSQL_SIGNED_INT_MIN = -2147483648;
|
|
|
|
function requiredString(value, field) {
|
|
const normalized = String(value ?? '').trim();
|
|
if (!normalized) {
|
|
throw Object.assign(new Error(`${field} is required`), {
|
|
code: 'invalid_conversation_package',
|
|
field,
|
|
});
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function optionalString(value) {
|
|
const normalized = String(value ?? '').trim();
|
|
return normalized || null;
|
|
}
|
|
|
|
function optionalNumber(value) {
|
|
if (value == null || value === '') return null;
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|
|
|
|
export function buildConversationDisplayFolderName(title) {
|
|
return optionalString(title) ?? '本次对话文件夹';
|
|
}
|
|
|
|
function safeSortOrder(value) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return 0;
|
|
let normalized = Math.trunc(number);
|
|
if (normalized > MYSQL_SIGNED_INT_MAX && Math.abs(normalized) >= 1_000_000_000_000) {
|
|
normalized = Math.trunc(normalized / 1000);
|
|
}
|
|
if (normalized > MYSQL_SIGNED_INT_MAX) return MYSQL_SIGNED_INT_MAX;
|
|
if (normalized < MYSQL_SIGNED_INT_MIN) return MYSQL_SIGNED_INT_MIN;
|
|
return normalized;
|
|
}
|
|
|
|
function safeStorageSegment(value, field) {
|
|
const normalized = requiredString(value, field);
|
|
const encoded = encodeURIComponent(normalized);
|
|
if (encoded.includes('%2F') || encoded.includes('%5C')) {
|
|
throw Object.assign(new Error(`${field} must not contain path separators`), {
|
|
code: 'invalid_storage_segment',
|
|
field,
|
|
});
|
|
}
|
|
return encoded;
|
|
}
|
|
|
|
export function buildConversationStoragePrefix({ userId, sessionId }) {
|
|
return `users/${safeStorageSegment(userId, 'userId')}/conversations/${safeStorageSegment(
|
|
sessionId,
|
|
'sessionId',
|
|
)}`;
|
|
}
|
|
|
|
export function buildConversationPackageUri({ userId, sessionId }) {
|
|
return `mindspace://users/${safeStorageSegment(userId, 'userId')}/conversations/${safeStorageSegment(
|
|
sessionId,
|
|
'sessionId',
|
|
)}`;
|
|
}
|
|
|
|
export function createConversationPackageRecord(input) {
|
|
const userId = requiredString(input?.userId, 'userId');
|
|
const sessionId = requiredString(input?.sessionId, 'sessionId');
|
|
const now = Number.isFinite(Number(input?.now)) ? Number(input.now) : Date.now();
|
|
const id = optionalString(input?.id) ?? `${PACKAGE_ID_PREFIX}${sessionId}`;
|
|
const storagePrefix =
|
|
optionalString(input?.storagePrefix) ?? buildConversationStoragePrefix({ userId, sessionId });
|
|
|
|
return {
|
|
id,
|
|
userId,
|
|
sessionId,
|
|
title: optionalString(input?.title),
|
|
status: optionalString(input?.status) ?? 'active',
|
|
storagePrefix,
|
|
manifestAssetId: optionalString(input?.manifestAssetId),
|
|
createdAt: Number.isFinite(Number(input?.createdAt)) ? Number(input.createdAt) : now,
|
|
updatedAt: Number.isFinite(Number(input?.updatedAt)) ? Number(input.updatedAt) : now,
|
|
};
|
|
}
|
|
|
|
export function assertConversationArtifactKind(kind) {
|
|
const normalized = requiredString(kind, 'artifactKind');
|
|
if (!CONVERSATION_ARTIFACT_KINDS.includes(normalized)) {
|
|
throw Object.assign(new Error(`unsupported conversation artifact kind: ${normalized}`), {
|
|
code: 'unsupported_conversation_artifact_kind',
|
|
field: 'artifactKind',
|
|
});
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function createConversationArtifact(input) {
|
|
const packageId = requiredString(input?.packageId, 'packageId');
|
|
const artifactKind = assertConversationArtifactKind(input?.artifactKind ?? input?.kind);
|
|
const now = Number.isFinite(Number(input?.now)) ? Number(input.now) : Date.now();
|
|
const id = optionalString(input?.id) ?? `${ARTIFACT_ID_PREFIX}${packageId}_${artifactKind}_${now}`;
|
|
|
|
return {
|
|
id,
|
|
packageId,
|
|
artifactKind,
|
|
role: optionalString(input?.role),
|
|
assetId: optionalString(input?.assetId),
|
|
pageId: optionalString(input?.pageId),
|
|
publicationId: optionalString(input?.publicationId),
|
|
agentRunId: optionalString(input?.agentRunId),
|
|
messageId: optionalString(input?.messageId),
|
|
displayName: optionalString(input?.displayName),
|
|
mimeType: optionalString(input?.mimeType),
|
|
sizeBytes: optionalNumber(input?.sizeBytes),
|
|
storageKey: optionalString(input?.storageKey),
|
|
canonicalUrl: optionalString(input?.canonicalUrl),
|
|
sortOrder: safeSortOrder(input?.sortOrder),
|
|
createdAt: Number.isFinite(Number(input?.createdAt)) ? Number(input.createdAt) : now,
|
|
};
|
|
}
|
|
|
|
function artifactManifestEntry(artifact) {
|
|
return {
|
|
artifactId: artifact.id,
|
|
kind: artifact.artifactKind,
|
|
...(artifact.role ? { role: artifact.role } : {}),
|
|
...(artifact.assetId ? { assetId: artifact.assetId } : {}),
|
|
...(artifact.pageId ? { pageId: artifact.pageId } : {}),
|
|
...(artifact.publicationId ? { publicationId: artifact.publicationId } : {}),
|
|
...(artifact.agentRunId ? { agentRunId: artifact.agentRunId } : {}),
|
|
...(artifact.messageId ? { messageId: artifact.messageId } : {}),
|
|
...(artifact.displayName ? { displayName: artifact.displayName } : {}),
|
|
...(artifact.mimeType ? { mimeType: artifact.mimeType } : {}),
|
|
...(artifact.sizeBytes !== null ? { sizeBytes: artifact.sizeBytes } : {}),
|
|
...(artifact.storageKey ? { storageKey: artifact.storageKey } : {}),
|
|
...(artifact.canonicalUrl ? { canonicalUrl: artifact.canonicalUrl } : {}),
|
|
sortOrder: artifact.sortOrder,
|
|
createdAt: artifact.createdAt,
|
|
};
|
|
}
|
|
|
|
function groupedManifestEntries(artifacts, keyField, idField) {
|
|
const groups = new Map();
|
|
for (const artifact of artifacts) {
|
|
const key = artifact[keyField];
|
|
if (!key) continue;
|
|
const existing = groups.get(key) ?? {
|
|
[idField]: key,
|
|
artifactIds: [],
|
|
createdAt: artifact.createdAt,
|
|
updatedAt: artifact.createdAt,
|
|
};
|
|
existing.artifactIds.push(artifact.id);
|
|
if (!existing.displayName && artifact.displayName) existing.displayName = artifact.displayName;
|
|
if (!existing.canonicalUrl && artifact.canonicalUrl) existing.canonicalUrl = artifact.canonicalUrl;
|
|
existing.createdAt = Math.min(existing.createdAt, artifact.createdAt);
|
|
existing.updatedAt = Math.max(existing.updatedAt, artifact.createdAt);
|
|
groups.set(key, existing);
|
|
}
|
|
return [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a[idField].localeCompare(b[idField]));
|
|
}
|
|
|
|
function provenanceManifestEntries(artifacts, keyField, idField) {
|
|
const groups = new Map();
|
|
for (const artifact of artifacts) {
|
|
const key = artifact[keyField];
|
|
if (!key) continue;
|
|
const existing = groups.get(key) ?? {
|
|
[idField]: key,
|
|
artifactIds: [],
|
|
kinds: [],
|
|
createdAt: artifact.createdAt,
|
|
updatedAt: artifact.createdAt,
|
|
};
|
|
existing.artifactIds.push(artifact.id);
|
|
if (!existing.kinds.includes(artifact.artifactKind)) existing.kinds.push(artifact.artifactKind);
|
|
existing.createdAt = Math.min(existing.createdAt, artifact.createdAt);
|
|
existing.updatedAt = Math.max(existing.updatedAt, artifact.createdAt);
|
|
groups.set(key, existing);
|
|
}
|
|
return [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a[idField].localeCompare(b[idField]));
|
|
}
|
|
|
|
export function buildConversationPackageManifest({ packageRecord, artifacts = [] }) {
|
|
const record = createConversationPackageRecord(packageRecord);
|
|
const normalizedArtifacts = artifacts
|
|
.map((artifact) => createConversationArtifact({ ...artifact, packageId: record.id }))
|
|
.sort((a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt || a.id.localeCompare(b.id));
|
|
|
|
return {
|
|
schemaVersion: CONVERSATION_PACKAGE_SCHEMA_VERSION,
|
|
packageId: record.id,
|
|
userId: record.userId,
|
|
sessionId: record.sessionId,
|
|
uri: buildConversationPackageUri({ userId: record.userId, sessionId: record.sessionId }),
|
|
title: record.title,
|
|
displayFolderName: buildConversationDisplayFolderName(record.title),
|
|
status: record.status,
|
|
storagePrefix: record.storagePrefix,
|
|
manifestAssetId: record.manifestAssetId,
|
|
createdAt: record.createdAt,
|
|
updatedAt: record.updatedAt,
|
|
artifacts: normalizedArtifacts.map(artifactManifestEntry),
|
|
pages: groupedManifestEntries(normalizedArtifacts, 'pageId', 'pageId'),
|
|
publications: groupedManifestEntries(normalizedArtifacts, 'publicationId', 'publicationId'),
|
|
messages: provenanceManifestEntries(normalizedArtifacts, 'messageId', 'messageId'),
|
|
agentRuns: provenanceManifestEntries(normalizedArtifacts, 'agentRunId', 'agentRunId'),
|
|
};
|
|
}
|
|
|
|
export const mindspaceConversationPackageInternals = {
|
|
requiredString,
|
|
optionalNumber,
|
|
optionalString,
|
|
provenanceManifestEntries,
|
|
safeStorageSegment,
|
|
safeSortOrder,
|
|
};
|