Files
memind/mindspace-wechat-html-delivery.mjs
john 065b1588b7 fix: deliver WeChat pages when image_make fails
Fall back to workspace share thumbnails so already-published HTML can be
sent instead of blocking on REQUIRED_FRESH. Also allow an explicit
ALLOW_DIRECT_STABLE_RELEASE path matching the pre-2026-07-23 direct
stable packaging flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 18:19:29 +08:00

692 lines
16 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import {
extractPublicHtmlWriteArtifacts,
isStubPublicHtmlContent,
materializeMissingPublicHtmlWrites,
normalizePublicHtmlRelativePath,
} from './mindspace-public-finish-sync.mjs';
import {
repairUnambiguousFreshWechatPageThumbnail,
verifyFreshWechatPageThumbnails,
} from './wechat/verify/generated-thumbnail.mjs';
import {
ensureWorkspaceHtmlThumbnail,
workspaceThumbnailRelativePath,
} from './mindspace-workspace-thumbnails.mjs';
import {
verifySharePreviewMeta,
} from './wechat/verify/share-preview.mjs';
const PUBLIC_HTML_LINK_PATTERN =
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
const AUXILIARY_PAGE_PATH_PATTERN =
/(?:^|[-_.\/])(?:admin|manage|management|backend|password|redirect|download|error)(?:[-_.\/]|$)/i;
function replyMessages(reply) {
return Array.isArray(reply?.messages)
? reply.messages
: [];
}
function extractHtmlWriteTargets(messages = []) {
const targets = new Set();
for (const message of messages) {
for (const item of message?.content ?? []) {
if (item?.type !== 'toolRequest') continue;
const toolCall = item.toolCall?.value;
const args = toolCall?.arguments ?? {};
const name = String(toolCall?.name ?? '').trim();
const action = String(args.action ?? '').trim().toLowerCase();
const writeLike =
(name === 'developer' &&
(action === 'write' || action === 'edit')) ||
name === 'write' ||
name.endsWith('__write') ||
name === 'write_file' ||
name === 'edit_file' ||
name.endsWith('__write_file') ||
name.endsWith('__edit_file');
if (!writeLike || typeof args.path !== 'string') {
continue;
}
const candidate = String(args.path).trim();
if (candidate.toLowerCase().endsWith('.html')) {
targets.add(candidate);
}
}
}
return [...targets];
}
function extractRequestedHtmlTarget(text) {
const value = String(text ?? '');
if (!value) return '';
const explicitPublicMatch =
value.match(
/(?:^|[\s`'"])(public\/[a-z0-9._-]+\.html)\b/i,
);
if (explicitPublicMatch?.[1]) {
return explicitPublicMatch[1];
}
const bareMatch =
value.match(
/(?:^|[\s`'"])([a-z0-9._-]+\.html)\b/i,
);
return bareMatch?.[1]
? `public/${bareMatch[1]}`
: '';
}
function collectLinkedHtmlFilenames(text) {
const filenames = new Set();
for (
const match of String(text ?? '')
.matchAll(PUBLIC_HTML_LINK_PATTERN)
) {
const filename =
path.posix.basename(
String(match[2] ?? '').trim(),
);
if (filename) filenames.add(filename);
}
return filenames;
}
function readArtifactDescriptor(
target,
{
publishDir,
buildCanonicalUrl,
copyIntoPublic = true,
},
) {
const workspaceRoot =
path.resolve(String(publishDir ?? ''));
if (!workspaceRoot) return null;
const candidate = String(target ?? '').trim();
if (!candidate) return null;
const source = path.isAbsolute(candidate)
? path.resolve(candidate)
: path.resolve(workspaceRoot, candidate);
if (
source !== workspaceRoot &&
!source.startsWith(
`${workspaceRoot}${path.sep}`,
)
) {
return null;
}
try {
if (
!fs.existsSync(source) ||
!fs.statSync(source).isFile()
) {
return null;
}
} catch {
return null;
}
const publicRoot =
path.join(workspaceRoot, 'public');
let publishedPath = source;
if (
copyIntoPublic &&
source !== publicRoot &&
!source.startsWith(`${publicRoot}${path.sep}`)
) {
fs.mkdirSync(publicRoot, { recursive: true });
publishedPath = path.join(
publicRoot,
path.basename(source),
);
if (publishedPath !== source) {
fs.copyFileSync(source, publishedPath);
}
}
const relativePath = path
.relative(workspaceRoot, publishedPath)
.replace(/\\/g, '/');
if (
!relativePath ||
relativePath.startsWith('..') ||
path.isAbsolute(relativePath)
) {
return null;
}
const normalizedRelativePath =
normalizePublicHtmlRelativePath(relativePath);
if (
!normalizedRelativePath
.toLowerCase()
.endsWith('.html')
) {
return null;
}
let content = '';
let stat;
try {
content = fs.readFileSync(
publishedPath,
'utf8',
);
stat = fs.statSync(publishedPath);
} catch {
return null;
}
const canonicalUrl =
String(
buildCanonicalUrl?.(
normalizedRelativePath,
) ?? '',
).trim();
const auxiliary =
AUXILIARY_PAGE_PATH_PATTERN.test(
normalizedRelativePath,
) ||
/<meta[^>]*name=["']mindspace-page-role["'][^>]*content=["'](?:admin|auxiliary)["']/i.test(
content,
) ||
/<[^>]+data-mindspace-page-role=["'](?:admin|auxiliary)["']/i.test(
content,
);
const sharePreview =
verifySharePreviewMeta(content);
return {
localPath: publishedPath,
relativePath: normalizedRelativePath,
url: canonicalUrl,
canonicalUrl,
exists: true,
isStub: isStubPublicHtmlContent(content),
isHtmlDocument:
/<(?:html|body|main|article)\b/i.test(
content,
),
sharePreview,
auxiliary,
sizeBytes: Number(stat.size ?? 0),
mtimeMs: Number(stat.mtimeMs ?? 0),
};
}
function collectRecentArtifacts({
publishDir,
buildCanonicalUrl,
replyText = '',
requestStartedAt = 0,
limit = 20,
}) {
const publicRoot = path.join(
path.resolve(publishDir),
'public',
);
try {
if (
!fs.existsSync(publicRoot) ||
!fs.statSync(publicRoot).isDirectory()
) {
return [];
}
} catch {
return [];
}
const linkedFilenames =
collectLinkedHtmlFilenames(replyText);
const artifacts = [];
const stack = [publicRoot];
while (
stack.length > 0 &&
artifacts.length < limit
) {
const current = stack.pop();
let entries;
try {
entries = fs.readdirSync(current, {
withFileTypes: true,
});
} catch {
continue;
}
for (const entry of entries) {
const absolutePath =
path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(absolutePath);
continue;
}
if (
!entry.isFile() ||
!entry.name.toLowerCase().endsWith('.html')
) {
continue;
}
const artifact = readArtifactDescriptor(
absolutePath,
{
publishDir,
buildCanonicalUrl,
copyIntoPublic: false,
},
);
if (!artifact) continue;
if (
requestStartedAt > 0 &&
artifact.mtimeMs + 1 < requestStartedAt
) {
continue;
}
const filename = path.posix.basename(
artifact.relativePath,
);
if (
linkedFilenames.size > 0 &&
!linkedFilenames.has(filename)
) {
continue;
}
artifacts.push(artifact);
if (artifacts.length >= limit) break;
}
}
return artifacts.sort(
(left, right) =>
right.mtimeMs - left.mtimeMs,
);
}
function uniqueArtifacts(artifacts = []) {
const byRelativePath = new Map();
for (const artifact of artifacts) {
const relativePath = String(
artifact?.relativePath ?? '',
).trim();
if (relativePath) {
byRelativePath.set(relativePath, artifact);
}
}
return [...byRelativePath.values()];
}
function sanitizeArtifact(artifact) {
if (!artifact) return null;
const {
localPath: _localPath,
mtimeMs: _mtimeMs,
...safeArtifact
} = artifact;
return safeArtifact;
}
function sanitizeArtifacts(artifacts = []) {
return artifacts
.map(sanitizeArtifact)
.filter(Boolean);
}
function buildValidReplyUrls(
replyText,
confirmedArtifacts,
) {
const artifactsByFilename = new Map(
confirmedArtifacts.map((artifact) => [
path.posix.basename(artifact.relativePath),
artifact,
]),
);
const urls = [];
for (
const match of String(replyText ?? '')
.matchAll(PUBLIC_HTML_LINK_PATTERN)
) {
const filename = path.posix.basename(
String(match[2] ?? '').trim(),
);
if (artifactsByFilename.has(filename)) {
urls.push(match[0]);
}
}
return [...new Set(urls)];
}
export function prepareWechatHtmlDeliveryAtWorkspace({
reply = null,
intent = null,
publishDir,
buildCanonicalUrl,
requestStartedAt = 0,
allowRecentArtifacts = true,
} = {}) {
const messages = replyMessages(reply);
const materialization =
materializeMissingPublicHtmlWrites({
messages,
publishDir,
});
const publishedArtifacts = uniqueArtifacts(
extractHtmlWriteTargets(messages)
.map((target) =>
readArtifactDescriptor(target, {
publishDir,
buildCanonicalUrl,
}),
)
.filter(Boolean),
);
const expectedTarget =
extractRequestedHtmlTarget(
intent?.agentText ??
intent?.displayText ??
'',
);
const expectedArtifacts = expectedTarget
? [
readArtifactDescriptor(expectedTarget, {
publishDir,
buildCanonicalUrl,
}),
].filter(Boolean)
: [];
const recentArtifacts = allowRecentArtifacts
? collectRecentArtifacts({
publishDir,
buildCanonicalUrl,
replyText: reply?.text,
requestStartedAt,
})
: [];
const confirmedArtifacts = uniqueArtifacts([
...publishedArtifacts,
...expectedArtifacts,
...recentArtifacts,
]);
const linkedFilenames =
collectLinkedHtmlFilenames(reply?.text);
const matchedArtifacts =
linkedFilenames.size > 0
? confirmedArtifacts.filter((artifact) =>
linkedFilenames.has(
path.posix.basename(
artifact.relativePath,
),
),
)
: confirmedArtifacts;
const validReplyUrls = buildValidReplyUrls(
reply?.text,
confirmedArtifacts,
);
return {
materialization,
publishedArtifacts:
sanitizeArtifacts(publishedArtifacts),
expectedArtifacts:
sanitizeArtifacts(expectedArtifacts),
recentArtifacts:
sanitizeArtifacts(recentArtifacts),
confirmedArtifacts:
sanitizeArtifacts(confirmedArtifacts),
verifiedArtifacts:
sanitizeArtifacts(matchedArtifacts),
validReplyUrls,
hasValidReplyLink:
validReplyUrls.length > 0,
};
}
function restoreWorkspaceArtifacts({
artifacts,
publishDir,
buildCanonicalUrl,
}) {
return uniqueArtifacts(
artifacts
.map((artifact) =>
readArtifactDescriptor(
artifact?.relativePath,
{
publishDir,
buildCanonicalUrl,
copyIntoPublic: false,
},
),
)
.filter(Boolean),
);
}
function sanitizeFreshVerification(
verification,
repair = null,
) {
return {
ok: verification?.ok === true,
reason:
verification?.reason == null
? null
: String(verification.reason),
artifact: sanitizeArtifact(
verification?.artifact,
),
cover:
String(verification?.cover ?? '').trim() ||
null,
matchRelativePaths: Array.isArray(
verification?.matches,
)
? verification.matches
.map((match) =>
String(
match?.artifact?.relativePath ?? '',
).trim(),
)
.filter(Boolean)
: [],
skippedRelativePaths: Array.isArray(
verification?.skippedArtifacts,
)
? verification.skippedArtifacts
.map((artifact) =>
String(
artifact?.relativePath ?? '',
).trim(),
)
.filter(Boolean)
: [],
repair: repair
? {
ok: repair.ok === true,
reason:
repair.reason == null
? null
: String(repair.reason),
relativePath:
String(
repair?.artifact?.relativePath ?? '',
).trim() || null,
cover:
String(repair?.cover ?? '').trim() ||
null,
}
: null,
};
}
function collectDeliverablePageArtifacts({
artifacts = [],
messages = [],
publishDir,
buildCanonicalUrl,
} = {}) {
const restored = restoreWorkspaceArtifacts({
artifacts,
publishDir,
buildCanonicalUrl,
});
if (restored.length > 0) return restored;
const writeArtifacts = extractPublicHtmlWriteArtifacts(
messages,
{ publishDir },
);
return restoreWorkspaceArtifacts({
artifacts: writeArtifacts.map((artifact) => ({
relativePath: artifact.relativePath,
})),
publishDir,
buildCanonicalUrl,
});
}
async function acceptWorkspaceThumbnailFallback({
artifacts = [],
publishDir,
} = {}) {
const eligible = artifacts.filter((artifact) => {
const localPath = String(artifact?.localPath ?? '').trim();
return localPath && fs.existsSync(localPath) && artifact?.auxiliary !== true;
});
if (eligible.length === 0) {
return { ok: false, reason: 'missing_page_artifact', matches: [] };
}
await Promise.all(
eligible.map((artifact) =>
ensureWorkspaceHtmlThumbnail(
publishDir,
artifact.relativePath,
),
),
);
const withThumbnails = eligible.filter((artifact) =>
fs.existsSync(
path.join(
publishDir,
workspaceThumbnailRelativePath(artifact.relativePath),
),
),
);
if (withThumbnails.length !== eligible.length) {
return {
ok: false,
reason: 'workspace_thumbnail_fallback_failed',
matches: [],
artifact: eligible.find((artifact) =>
!fs.existsSync(
path.join(
publishDir,
workspaceThumbnailRelativePath(artifact.relativePath),
),
),
),
};
}
return {
ok: true,
reason: 'workspace_thumbnail_fallback',
matches: withThumbnails.map((artifact) => ({
artifact,
image: null,
cover: null,
})),
};
}
export async function ensureWechatFreshPageThumbnailsAtWorkspace({
artifacts = [],
images = [],
messages = [],
publishDir,
buildCanonicalUrl,
repairEnabled = false,
} = {}) {
let workspaceArtifacts = collectDeliverablePageArtifacts({
artifacts,
messages,
publishDir,
buildCanonicalUrl,
});
let verification =
verifyFreshWechatPageThumbnails(
workspaceArtifacts,
images,
);
let repair = null;
if (
!verification.ok &&
repairEnabled
) {
repair =
repairUnambiguousFreshWechatPageThumbnail({
artifacts: workspaceArtifacts,
images,
currentRunHtmlArtifacts:
extractPublicHtmlWriteArtifacts(
messages,
{ publishDir },
),
verificationReason:
verification.reason,
});
if (repair.ok) {
workspaceArtifacts = restoreWorkspaceArtifacts({
artifacts: workspaceArtifacts,
publishDir,
buildCanonicalUrl,
});
verification =
verifyFreshWechatPageThumbnails(
workspaceArtifacts,
images,
);
}
}
// When image_make failed or returned nothing, still deliver pages that already
// landed on disk by generating the workspace share thumbnail sidecars.
if (
!verification.ok
&& ['fresh_image_not_generated', 'missing_generated_cover', 'cover_not_from_current_run'].includes(
String(verification.reason ?? ''),
)
&& workspaceArtifacts.length > 0
) {
verification = await acceptWorkspaceThumbnailFallback({
artifacts: workspaceArtifacts,
publishDir,
});
}
if (verification.ok) {
await Promise.all(
(verification.matches ?? []).map((match) =>
ensureWorkspaceHtmlThumbnail(
publishDir,
match.artifact.relativePath,
),
),
);
}
return {
...sanitizeFreshVerification(
verification,
repair,
),
thumbnailRelativePaths:
verification.ok
? (verification.matches ?? []).map((match) =>
workspaceThumbnailRelativePath(
match.artifact.relativePath,
),
)
: [],
};
}
export const mindSpaceWechatHtmlDeliveryInternals = {
buildValidReplyUrls,
collectLinkedHtmlFilenames,
extractHtmlWriteTargets,
extractRequestedHtmlTarget,
readArtifactDescriptor,
sanitizeArtifact,
};