065b1588b7
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>
692 lines
16 KiB
JavaScript
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,
|
|
};
|