Files
memind/mindspace-public-finish-service.mjs
john 5ead1abf7f
Memind CI / Test, build, and release guards (push) Failing after 3s
fix(page-data): scope WeChat survey delivery when tool calls are hidden
Collect request-scoped tool messages in the WeChat SSE stream, infer this
turn Page Data HTML via mtime when messages lack writes, and limit bind and
delivery smoke to those paths so historical already_bound pages cannot block new surveys.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 21:41:00 +08:00

585 lines
15 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,
resolvePageDataRequestRelativePaths,
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 ?? '',
),
};
const scopedRelativePaths = resolvePageDataRequestRelativePaths({
publishDir,
messages: normalizedReply.messages,
requestStartedAt,
});
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,
);
const relevantRelativePaths = [
...new Set([
...scopedRelativePaths,
...(outcome?.evaluation?.relevantFiles ?? [])
.map((file) => file?.relativePath)
.filter(Boolean),
]),
];
autoBind =
await maybeAutoBindPageDataHtmlPagesFn({
pool,
userId: currentUser.id,
publishDir,
h5Root,
storageRoot,
findPageByRelativePath,
onlyRelativePaths:
relevantRelativePaths.length > 0
? relevantRelativePaths
: null,
});
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') {
const deliveryRelativePaths = [
...new Set([
...scopedRelativePaths,
...(outcome?.evaluation?.relevantFiles ?? [])
.map((file) => file?.relativePath)
.filter(Boolean),
]),
];
deliveryArtifacts =
buildPageDataDeliveryArtifactsFromBindResultFn(
autoBind,
publishDir,
{ publicBaseUrl },
deliveryRelativePaths,
);
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,
};