Files
memind/mindspace-public-finish-service.mjs
2026-07-27 15:34:35 +08:00

558 lines
14 KiB
JavaScript

import {
collectOwnPublicHtmlArtifactRefs,
materializePublicHtmlWritesFromSessionEvent,
syncPublicHtmlAfterFinish,
} from './mindspace-public-finish-sync.mjs';
import {
buildMindSpacePublicUrlForUser,
resolveMindSpaceUserPublishDir,
} from './mindspace-runtime-config.mjs';
import {
evaluateH5HtmlFinishGuard,
} from './mindspace-h5-html-finish-guard.mjs';
import {
buildPageDataDeliveryArtifactsFromBindResult,
ensurePageDataDeliveryReady,
maybeAutoBindPageDataHtmlPages,
preparePageDataAfterFinish,
resolvePageDataCollectOutcomeAsync,
rewritePageDataDeliveryLinks,
} from './mindspace-page-data-finish-guard.mjs';
import {
createPageService,
} from './mindspace-pages.mjs';
import {
ensureWechatFreshPageThumbnailsAtWorkspace,
prepareWechatHtmlDeliveryAtWorkspace,
} from './mindspace-wechat-html-delivery.mjs';
function eventMessages(event, recentCount) {
if (event?.type === 'Message' && event.message) {
return [event.message];
}
if (
event?.type === 'UpdateConversation' &&
Array.isArray(event.conversation)
) {
return event.conversation.slice(
-Math.max(1, recentCount),
);
}
return [];
}
function sanitizePageDataPreparation(value) {
if (!value) return null;
return JSON.parse(
JSON.stringify(value, (key, item) => {
if (
[
'absolutePath',
'content',
'localPath',
].includes(key)
) {
return undefined;
}
if (item instanceof Map) {
return Object.fromEntries(item);
}
return item;
}),
);
}
export function createMindSpacePublicFinishService({
pool,
h5Root,
storageRoot,
env = process.env,
syncWorkspaceAssets = null,
conversationArtifactService,
resolveMindSpaceUserPublishDirFn =
resolveMindSpaceUserPublishDir,
buildMindSpacePublicUrlForUserFn =
buildMindSpacePublicUrlForUser,
materializeSessionEventFn =
materializePublicHtmlWritesFromSessionEvent,
collectArtifactRefsFn =
collectOwnPublicHtmlArtifactRefs,
syncAfterFinishFn = syncPublicHtmlAfterFinish,
evaluateH5HtmlFinishGuardFn =
evaluateH5HtmlFinishGuard,
preparePageDataAfterFinishFn =
preparePageDataAfterFinish,
resolvePageDataCollectOutcomeAsyncFn =
resolvePageDataCollectOutcomeAsync,
maybeAutoBindPageDataHtmlPagesFn =
maybeAutoBindPageDataHtmlPages,
buildPageDataDeliveryArtifactsFromBindResultFn =
buildPageDataDeliveryArtifactsFromBindResult,
ensurePageDataDeliveryReadyFn =
ensurePageDataDeliveryReady,
rewritePageDataDeliveryLinksFn =
rewritePageDataDeliveryLinks,
createPageServiceFn = createPageService,
prepareWechatHtmlDeliveryAtWorkspaceFn =
prepareWechatHtmlDeliveryAtWorkspace,
ensureWechatFreshPageThumbnailsAtWorkspaceFn =
ensureWechatFreshPageThumbnailsAtWorkspace,
} = {}) {
if (
!pool ||
typeof pool.query !== 'function' ||
!h5Root ||
!storageRoot
) {
throw new Error(
'createMindSpacePublicFinishService requires pool, h5Root, and storageRoot',
);
}
if (
!conversationArtifactService ||
typeof conversationArtifactService
.registerPublicHtmlArtifacts !== 'function'
) {
throw new Error(
'createMindSpacePublicFinishService requires conversationArtifactService',
);
}
const resolveContext = (userId) => {
const normalizedUserId = String(userId ?? '').trim();
if (!normalizedUserId) {
throw Object.assign(
new Error('MindSpace public finish requires userId'),
{ code: 'invalid_input' },
);
}
const currentUser = { id: normalizedUserId };
return {
currentUser,
publishDir: resolveMindSpaceUserPublishDirFn(
h5Root,
currentUser,
),
};
};
const attachCanonicalUrls = (result, currentUser) => {
const artifactRefs = Array.isArray(
result?.publicHtmlArtifactRefs,
)
? result.publicHtmlArtifactRefs
: [];
return {
...result,
publicHtmlArtifacts: artifactRefs.map((ref) => ({
...ref,
canonicalUrl: buildMindSpacePublicUrlForUserFn({
h5Root,
env,
user: currentUser,
relativePath: ref.relativePath,
}),
})),
};
};
const buildCanonicalUrl = (
currentUser,
relativePath,
) =>
buildMindSpacePublicUrlForUserFn({
h5Root,
env,
user: currentUser,
relativePath,
});
const syncAndRegisterWechatArtifacts = async ({
currentUser,
sessionId,
relativePaths,
}) => {
const normalizedRelativePaths = [
...new Set(
relativePaths
.map((value) =>
String(value ?? '').trim(),
)
.filter(Boolean),
),
];
if (normalizedRelativePaths.length === 0) {
return;
}
if (
typeof syncWorkspaceAssets === 'function'
) {
await syncWorkspaceAssets(currentUser.id, {
categoryCode: 'public',
sourceSessionId:
String(sessionId ?? '').trim() ||
null,
onlyRelativePaths:
normalizedRelativePaths,
});
}
if (String(sessionId ?? '').trim()) {
await conversationArtifactService
.registerPublicHtmlArtifacts({
userId: currentUser.id,
sessionId,
relativePaths:
normalizedRelativePaths.filter(
(relativePath) =>
relativePath
.toLowerCase()
.endsWith('.html'),
),
});
}
};
const preparePageDataForUser = async ({
userId,
messages,
userText = '',
} = {}) => {
const { currentUser, publishDir } =
resolveContext(userId);
return sanitizePageDataPreparation(
await preparePageDataAfterFinishFn({
userId: currentUser.id,
publishDir,
messages,
pool,
h5Root,
storageRoot,
userText,
}),
);
};
const prepareWechatPageDataForUser = async ({
userId,
reply = null,
intent = null,
publicBaseUrl = null,
requestStartedAt = 0,
} = {}) => {
const { currentUser, publishDir } =
resolveContext(userId);
const normalizedReply = {
text: String(reply?.text ?? ''),
messages: Array.isArray(reply?.messages)
? reply.messages
: [],
};
const normalizedIntent = {
agentText: String(intent?.agentText ?? ''),
displayText: String(
intent?.displayText ?? '',
),
};
let outcome =
await resolvePageDataCollectOutcomeAsyncFn({
reply: normalizedReply,
intent: normalizedIntent,
publishDir,
requestStartedAt,
pool,
userId: currentUser.id,
findPageByRelativePath: null,
apiBase: publicBaseUrl,
});
let autoBind = null;
if (outcome?.action === 'skip') {
return sanitizePageDataPreparation({
outcome,
autoBind,
deliveryArtifacts: [],
deliveryCheck: null,
rewrittenText: normalizedReply.text,
});
}
const pageService = createPageServiceFn(pool, {
h5Root,
storageRoot,
});
const findPageByRelativePath =
pageService.findPageByRelativePath.bind(
pageService,
);
autoBind =
await maybeAutoBindPageDataHtmlPagesFn({
pool,
userId: currentUser.id,
publishDir,
h5Root,
storageRoot,
findPageByRelativePath,
});
outcome =
await resolvePageDataCollectOutcomeAsyncFn({
reply: normalizedReply,
intent: normalizedIntent,
publishDir,
requestStartedAt,
pool,
userId: currentUser.id,
findPageByRelativePath,
apiBase: publicBaseUrl,
});
let deliveryArtifacts = [];
let deliveryCheck = null;
let rewrittenText = normalizedReply.text;
if (outcome?.action === 'send') {
deliveryArtifacts =
buildPageDataDeliveryArtifactsFromBindResultFn(
autoBind,
publishDir,
{ publicBaseUrl },
);
if (deliveryArtifacts.length > 0) {
deliveryCheck =
await ensurePageDataDeliveryReadyFn({
publishDir,
userId: currentUser.id,
pool,
h5Root,
storageRoot,
apiBase: publicBaseUrl,
artifacts: deliveryArtifacts,
});
rewrittenText =
rewritePageDataDeliveryLinksFn(
normalizedReply.text,
deliveryArtifacts,
);
}
}
return sanitizePageDataPreparation({
outcome,
autoBind,
deliveryArtifacts,
deliveryCheck,
rewrittenText,
});
};
const prepareWechatHtmlForUser = async ({
userId,
sessionId = null,
reply = null,
intent = null,
requestStartedAt = 0,
allowRecentArtifacts = true,
} = {}) => {
const { currentUser, publishDir } =
resolveContext(userId);
const normalizedReply = {
text: String(reply?.text ?? ''),
messages: Array.isArray(reply?.messages)
? reply.messages
: [],
};
const normalizedIntent = {
agentText: String(
intent?.agentText ?? '',
),
displayText: String(
intent?.displayText ?? '',
),
};
const prepared =
prepareWechatHtmlDeliveryAtWorkspaceFn({
reply: normalizedReply,
intent: normalizedIntent,
publishDir,
requestStartedAt,
allowRecentArtifacts:
allowRecentArtifacts === true,
buildCanonicalUrl: (relativePath) =>
buildCanonicalUrl(
currentUser,
relativePath,
),
});
await syncAfterFinishFn({
messages: normalizedReply.messages,
currentUser,
publishDir,
sessionId,
pool,
storageRoot,
h5Root,
syncWorkspaceAssets,
registerPublicHtmlArtifacts:
(_registeredUserId, options) =>
conversationArtifactService
.registerPublicHtmlArtifacts({
userId: currentUser.id,
sessionId: options?.sessionId,
relativePaths:
options?.relativePaths,
artifactRefs:
options?.artifactRefs,
}),
});
const confirmedRelativePaths =
prepared.confirmedArtifacts.map(
(artifact) => artifact.relativePath,
);
await syncAndRegisterWechatArtifacts({
currentUser,
sessionId,
relativePaths: confirmedRelativePaths,
});
return prepared;
};
const ensureWechatFreshPageThumbnailsForUser =
async ({
userId,
sessionId = null,
artifacts = [],
images = [],
messages = [],
repairEnabled = false,
} = {}) => {
const { currentUser, publishDir } =
resolveContext(userId);
const result =
await ensureWechatFreshPageThumbnailsAtWorkspaceFn(
{
artifacts,
images,
messages: Array.isArray(messages)
? messages
: [],
publishDir,
repairEnabled:
repairEnabled === true,
buildCanonicalUrl: (relativePath) =>
buildCanonicalUrl(
currentUser,
relativePath,
),
},
);
if (result.ok) {
await syncAndRegisterWechatArtifacts({
currentUser,
sessionId,
relativePaths: [
...result.matchRelativePaths,
...result.thumbnailRelativePaths,
],
});
}
return result;
};
return {
async materializeSessionEvent({
userId,
event,
recentCount = 20,
} = {}) {
const { currentUser, publishDir } =
resolveContext(userId);
const result = materializeSessionEventFn(event, {
publishDir,
recentCount,
});
const publicHtmlArtifactRefs = collectArtifactRefsFn({
messages: eventMessages(event, recentCount),
currentUser,
publishDir,
materialized: result?.materialized,
skipped: result?.skipped,
});
return attachCanonicalUrls(
{
...result,
publicHtmlRelativePaths:
publicHtmlArtifactRefs.map(
(ref) => ref.relativePath,
),
publicHtmlArtifactRefs,
},
currentUser,
);
},
async syncAfterFinish({
userId,
sessionId,
messages,
currentUser: suppliedCurrentUser = null,
} = {}) {
const { currentUser, publishDir } =
resolveContext(userId);
const result = await syncAfterFinishFn({
messages,
currentUser,
publishDir,
sessionId,
pool,
storageRoot,
h5Root,
syncWorkspaceAssets,
registerPublicHtmlArtifacts:
(_registeredUserId, options) =>
conversationArtifactService
.registerPublicHtmlArtifacts({
userId: currentUser.id,
sessionId: options?.sessionId,
relativePaths: options?.relativePaths,
artifactRefs: options?.artifactRefs,
}),
});
const evaluated =
evaluateH5HtmlFinishGuardFn({
messages,
currentUser: {
...suppliedCurrentUser,
id: currentUser.id,
},
publishDir,
syncResult: result,
});
const {
linkExists: _linkExists,
...deliveryEvaluation
} = evaluated ?? {};
return attachCanonicalUrls(
{
...result,
deliveryEvaluation,
},
currentUser,
);
},
preparePageDataAfterFinish:
preparePageDataForUser,
prepareWechatPageDataDelivery:
prepareWechatPageDataForUser,
prepareWechatHtmlDelivery:
prepareWechatHtmlForUser,
ensureWechatFreshPageThumbnails:
ensureWechatFreshPageThumbnailsForUser,
};
}
export const mindSpacePublicFinishServiceInternals = {
eventMessages,
sanitizePageDataPreparation,
};