fix(wechat): repair share preview on disk for sanitized delivery artifacts
Memind CI / Test, build, and release guards (push) Failing after 6s

Production WeChat delivery strips localPath from artifacts, so share-preview
auto-repair never wrote back to HTML. Resolve paths via publishDir +
relativePath, repair at materialize and delivery prep, and pass h5Root from
Portal so page.generate can patch missing meta before sending links.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-02 08:47:51 +08:00
parent 0a71370bf4
commit 1faba62f9b
11 changed files with 238 additions and 75 deletions
+3 -1
View File
@@ -4,6 +4,7 @@ import path from 'node:path';
import { extractStaticPageLinks, materializePrivateAssetsInPublicHtmlFiles } from './mindspace-chat-save.mjs'; import { extractStaticPageLinks, materializePrivateAssetsInPublicHtmlFiles } from './mindspace-chat-save.mjs';
import { DOWNLOADABLE_FILE_PATTERN } from './mindspace-html-download-links.mjs'; import { DOWNLOADABLE_FILE_PATTERN } from './mindspace-html-download-links.mjs';
import { scheduleWorkspaceHtmlThumbnailSidecars } from './mindspace-workspace-thumbnails.mjs'; import { scheduleWorkspaceHtmlThumbnailSidecars } from './mindspace-workspace-thumbnails.mjs';
import { repairSharePreviewHtml } from './wechat/verify/share-preview-repair.mjs';
/** /**
* Public HTML finish-sync invariants (regression guard — do not simplify away). * Public HTML finish-sync invariants (regression guard — do not simplify away).
@@ -397,7 +398,8 @@ export function materializeMissingPublicHtmlWrites({ messages, publishDir }) {
} }
try { try {
fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(destination, artifact.content, 'utf8'); const preparedContent = repairSharePreviewHtml(String(artifact.content ?? ''), {});
fs.writeFileSync(destination, preparedContent, 'utf8');
materialized.push(artifact.relativePath); materialized.push(artifact.relativePath);
scheduleWorkspaceHtmlThumbnailSidecars(root, artifact.relativePath); scheduleWorkspaceHtmlThumbnailSidecars(root, artifact.relativePath);
} catch { } catch {
+25 -5
View File
@@ -17,6 +17,9 @@ import {
import { import {
verifySharePreviewMeta, verifySharePreviewMeta,
} from './wechat/verify/share-preview.mjs'; } from './wechat/verify/share-preview.mjs';
import {
ensureArtifactsSharePreviewReady,
} from './wechat/verify/share-preview-repair.mjs';
const PUBLIC_HTML_LINK_PATTERN = const PUBLIC_HTML_LINK_PATTERN =
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi; /https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
@@ -397,11 +400,26 @@ export function prepareWechatHtmlDeliveryAtWorkspace({
requestStartedAt, requestStartedAt,
}) })
: []; : [];
const confirmedArtifacts = uniqueArtifacts([ const confirmedArtifacts = ensureArtifactsSharePreviewReady(
...publishedArtifacts, uniqueArtifacts([
...expectedArtifacts, ...publishedArtifacts,
...recentArtifacts, ...expectedArtifacts,
]); ...recentArtifacts,
]),
{
publishDir,
topic: String(
intent?.agentText ?? intent?.displayText ?? '',
).trim(),
},
).map(
(artifact) =>
readArtifactDescriptor(artifact.relativePath, {
publishDir,
buildCanonicalUrl,
copyIntoPublic: false,
}) ?? artifact,
);
const linkedFilenames = const linkedFilenames =
collectLinkedHtmlFilenames(reply?.text); collectLinkedHtmlFilenames(reply?.text);
const matchedArtifacts = const matchedArtifacts =
@@ -610,6 +628,7 @@ export async function ensureWechatFreshPageThumbnailsAtWorkspace({
verifyFreshWechatPageThumbnails( verifyFreshWechatPageThumbnails(
workspaceArtifacts, workspaceArtifacts,
images, images,
{ publishDir },
); );
let repair = null; let repair = null;
if ( if (
@@ -638,6 +657,7 @@ export async function ensureWechatFreshPageThumbnailsAtWorkspace({
verifyFreshWechatPageThumbnails( verifyFreshWechatPageThumbnails(
workspaceArtifacts, workspaceArtifacts,
images, images,
{ publishDir },
); );
} }
} }
+11 -12
View File
@@ -81,17 +81,16 @@ test('MindSpace prepares WeChat HTML delivery with logical artifacts only', (t)
}, },
}); });
assert.equal( const written = fs.readFileSync(
fs.readFileSync( path.join(
path.join( publishDir,
publishDir, 'public',
'public', 'hello.html',
'hello.html',
),
'utf8',
), ),
content, 'utf8',
); );
assert.match(written, /name="description"/);
assert.match(written, /data-mindspace-page-tag="platform-brand"/);
assert.equal( assert.equal(
result.confirmedArtifacts.length, result.confirmedArtifacts.length,
1, 1,
@@ -107,12 +106,12 @@ test('MindSpace prepares WeChat HTML delivery with logical artifacts only', (t)
isStub: false, isStub: false,
isHtmlDocument: true, isHtmlDocument: true,
sharePreview: { sharePreview: {
ok: false, ok: true,
reason: 'missing_description', reason: null,
}, },
auxiliary: false, auxiliary: false,
sizeBytes: sizeBytes:
Buffer.byteLength(content), Buffer.byteLength(written),
}, },
); );
assert.equal( assert.equal(
@@ -105,6 +105,7 @@ export async function bootstrapPortalIntegrationServices({
config: wechatMpConfig, config: wechatMpConfig,
userAuth, userAuth,
sessionAccess, sessionAccess,
h5Root,
htmlDeliveryAuthority: htmlDeliveryAuthority:
typeof mindSpacePublicFinish typeof mindSpacePublicFinish
?.prepareWechatHtmlDelivery === ?.prepareWechatHtmlDelivery ===
+13
View File
@@ -61,6 +61,7 @@ import {
collectWechatGeneratedImages, collectWechatGeneratedImages,
} from './wechat/verify/generated-thumbnail.mjs'; } from './wechat/verify/generated-thumbnail.mjs';
import { resolveBillingTokenState } from './billing-token-state.mjs'; import { resolveBillingTokenState } from './billing-token-state.mjs';
import { resolveMindSpaceUserPublishDir } from './mindspace-runtime-config.mjs';
import { import {
buildPageDataCollectFailureText, buildPageDataCollectFailureText,
} from './mindspace-page-data-finish-guard.mjs'; } from './mindspace-page-data-finish-guard.mjs';
@@ -1503,6 +1504,7 @@ export function createWechatMpService({
pageDataDeliveryReviewer = null, pageDataDeliveryReviewer = null,
wechatFetch = undiciFetch, wechatFetch = undiciFetch,
linkExists = defaultPublicHtmlLinkExists, linkExists = defaultPublicHtmlLinkExists,
h5Root = '',
logger = console, logger = console,
}) { }) {
const sessionStore = resolveSessionAccess({ userAuth, sessionAccess }); const sessionStore = resolveSessionAccess({ userAuth, sessionAccess });
@@ -1513,6 +1515,11 @@ export function createWechatMpService({
} }
const requireFreshPageThumbnail = config.requireFreshPageThumbnail === true; const requireFreshPageThumbnail = config.requireFreshPageThumbnail === true;
const repairFreshPageThumbnail = config.repairFreshPageThumbnail === true; const repairFreshPageThumbnail = config.repairFreshPageThumbnail === true;
const resolveUserPublishDir = (userId) => {
const normalizedUserId = String(userId ?? '').trim();
if (!normalizedUserId || !String(h5Root ?? '').trim()) return '';
return resolveMindSpaceUserPublishDir(h5Root, { id: normalizedUserId });
};
config = { config = {
...loadWechatMpConfig({}), ...loadWechatMpConfig({}),
...config, ...config,
@@ -2650,6 +2657,9 @@ export function createWechatMpService({
htmlGenerationNeedsRetry, htmlGenerationNeedsRetry,
replyHasPublicLinks, replyHasPublicLinks,
topic: wechatIntent?.topic ?? intent?.agentText ?? '', topic: wechatIntent?.topic ?? intent?.agentText ?? '',
repairContext: {
publishDir: resolveUserPublishDir(user.userId),
},
}); });
if (pageOutcome.action === 'session_retry') { if (pageOutcome.action === 'session_retry') {
throw new Error('stale_session_poisoned_completion'); throw new Error('stale_session_poisoned_completion');
@@ -2969,6 +2979,9 @@ export function createWechatMpService({
htmlGenerationNeedsRetry, htmlGenerationNeedsRetry,
replyHasPublicLinks, replyHasPublicLinks,
topic: wechatIntent?.topic ?? intent?.agentText ?? '', topic: wechatIntent?.topic ?? intent?.agentText ?? '',
repairContext: {
publishDir: resolveUserPublishDir(user.userId),
},
}); });
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') { if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
const text = pageOutcome.failureText ?? buildPagePublishFailureText(); const text = pageOutcome.failureText ?? buildPagePublishFailureText();
+27 -7
View File
@@ -8,17 +8,25 @@ export function evaluatePageGenerateSendableArtifacts({
confirmedArtifacts = [], confirmedArtifacts = [],
repairContext = {}, repairContext = {},
} = {}) { } = {}) {
const sendable = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }); const publishDir = String(repairContext.publishDir ?? '').trim();
const verified = sendable.filter((artifact) => verifyPageArtifactContent(artifact).ok); const previewOptions = { publishDir };
const sendable = selectSendableHtmlArtifacts({
verifiedArtifacts,
confirmedArtifacts,
publishDir,
});
const verified = sendable.filter((artifact) =>
verifyPageArtifactContent(artifact, previewOptions).ok,
);
const candidates = verified.length > 0 ? verified : sendable; const candidates = verified.length > 0 ? verified : sendable;
const ready = filterSharePreviewReadyArtifacts(candidates); const ready = filterSharePreviewReadyArtifacts(candidates, previewOptions);
if (ready.length > 0) return ready; if (ready.length > 0) return ready;
const repaired = []; const repaired = [];
for (const artifact of candidates) { for (const artifact of candidates) {
if (!verifyPageArtifactContent(artifact).ok) continue; if (!verifyPageArtifactContent(artifact, previewOptions).ok) continue;
const result = repairArtifactSharePreview(artifact, repairContext); const result = repairArtifactSharePreview(artifact, repairContext);
if (result.ok && verifyArtifactSharePreview(artifact).ok) { if (result.ok && verifyArtifactSharePreview(artifact, previewOptions).ok) {
repaired.push(artifact); repaired.push(artifact);
} }
} }
@@ -42,8 +50,10 @@ export function resolvePageGenerateOutcome({
}) { }) {
const context = { const context = {
topic: String(topic || repairContext.topic || '').trim(), topic: String(topic || repairContext.topic || '').trim(),
publishDir: String(repairContext.publishDir ?? '').trim(),
...repairContext, ...repairContext,
}; };
const previewOptions = { publishDir: context.publishDir };
const sendable = evaluatePageGenerateSendableArtifacts({ const sendable = evaluatePageGenerateSendableArtifacts({
verifiedArtifacts, verifiedArtifacts,
confirmedArtifacts, confirmedArtifacts,
@@ -71,11 +81,21 @@ export function resolvePageGenerateOutcome({
} }
const previewCandidates = filterSharePreviewReadyArtifacts( const previewCandidates = filterSharePreviewReadyArtifacts(
selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }), selectSendableHtmlArtifacts({
verifiedArtifacts,
confirmedArtifacts,
publishDir: context.publishDir,
}),
previewOptions,
); );
if (previewCandidates.length === 0 && confirmedArtifacts.length > 0) { if (previewCandidates.length === 0 && confirmedArtifacts.length > 0) {
const previewIssue = verifyArtifactSharePreview( const previewIssue = verifyArtifactSharePreview(
selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts })[0] ?? confirmedArtifacts[0], selectSendableHtmlArtifacts({
verifiedArtifacts,
confirmedArtifacts,
publishDir: context.publishDir,
})[0] ?? confirmedArtifacts[0],
previewOptions,
); );
return { return {
action: 'fail', action: 'fail',
+7 -2
View File
@@ -5,6 +5,7 @@ import {
parseMindspaceCoverMeta, parseMindspaceCoverMeta,
upsertMindspaceCoverMeta, upsertMindspaceCoverMeta,
} from '../../mindspace-cover-meta.mjs'; } from '../../mindspace-cover-meta.mjs';
import { resolveArtifactLocalPath } from './page-artifact.mjs';
const RASTER_MIME_PATTERN = /^image\/(?:png|jpeg|webp)$/i; const RASTER_MIME_PATTERN = /^image\/(?:png|jpeg|webp)$/i;
@@ -121,7 +122,11 @@ export function isWechatAuxiliaryPageArtifact(artifact) {
|| /<[^>]+data-mindspace-page-role=["'](?:admin|auxiliary)["']/i.test(html); || /<[^>]+data-mindspace-page-role=["'](?:admin|auxiliary)["']/i.test(html);
} }
export function verifyFreshWechatPageThumbnails(artifacts = [], images = []) { export function verifyFreshWechatPageThumbnails(
artifacts = [],
images = [],
{ publishDir = '' } = {},
) {
if (!Array.isArray(artifacts) || artifacts.length === 0) { if (!Array.isArray(artifacts) || artifacts.length === 0) {
return { ok: false, reason: 'missing_page_artifact', matches: [] }; return { ok: false, reason: 'missing_page_artifact', matches: [] };
} }
@@ -136,7 +141,7 @@ export function verifyFreshWechatPageThumbnails(artifacts = [], images = []) {
const usedJobs = new Set(); const usedJobs = new Set();
const matches = []; const matches = [];
for (const artifact of eligibleArtifacts) { for (const artifact of eligibleArtifacts) {
const localPath = String(artifact?.localPath ?? '').trim(); const localPath = resolveArtifactLocalPath(artifact, publishDir);
if (!localPath || !fs.existsSync(localPath)) { if (!localPath || !fs.existsSync(localPath)) {
return { ok: false, reason: 'missing_page_artifact', artifact, matches, skippedArtifacts }; return { ok: false, reason: 'missing_page_artifact', artifact, matches, skippedArtifacts };
} }
+52 -24
View File
@@ -1,30 +1,50 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path';
const STUB_MARKERS = ['临时补出', '服务号兜底', '服务号自动补出简版页面']; const STUB_MARKERS = ['临时补出', '服务号兜底', '服务号自动补出简版页面'];
export function resolveArtifactLocalPath(artifact, publishDir = '') {
const localPath = String(artifact?.localPath ?? '').trim();
if (localPath) return localPath;
const relativePath = String(artifact?.relativePath ?? '').trim();
const root = String(publishDir ?? '').trim();
if (!relativePath || !root) return '';
const resolved = path.resolve(root, relativePath);
const normalizedRoot = path.resolve(root);
if (
resolved !== normalizedRoot &&
!resolved.startsWith(`${normalizedRoot}${path.sep}`)
) {
return '';
}
return resolved;
}
export function isStubPublicHtmlContent(content) { export function isStubPublicHtmlContent(content) {
const value = String(content ?? ''); const value = String(content ?? '');
return STUB_MARKERS.some((marker) => value.includes(marker)); return STUB_MARKERS.some((marker) => value.includes(marker));
} }
export function artifactFileExists(artifact) { export function artifactFileExists(artifact, { publishDir = '' } = {}) {
const localPath = resolveArtifactLocalPath(artifact, publishDir);
if (localPath) {
try {
return fs.existsSync(localPath) && fs.statSync(localPath).isFile();
} catch {
return false;
}
}
if (typeof artifact?.exists === 'boolean') { if (typeof artifact?.exists === 'boolean') {
return artifact.exists; return artifact.exists;
} }
const localPath = String(artifact?.localPath ?? '').trim(); return false;
if (!localPath) return false;
try {
return fs.existsSync(localPath) && fs.statSync(localPath).isFile();
} catch {
return false;
}
} }
export function isStubPublicHtmlArtifact(artifact) { export function isStubPublicHtmlArtifact(artifact, { publishDir = '' } = {}) {
if (typeof artifact?.isStub === 'boolean') { if (typeof artifact?.isStub === 'boolean') {
return artifact.isStub; return artifact.isStub;
} }
const localPath = String(artifact?.localPath ?? '').trim(); const localPath = resolveArtifactLocalPath(artifact, publishDir);
if (!localPath) return false; if (!localPath) return false;
try { try {
return isStubPublicHtmlContent(fs.readFileSync(localPath, 'utf8')); return isStubPublicHtmlContent(fs.readFileSync(localPath, 'utf8'));
@@ -34,27 +54,35 @@ export function isStubPublicHtmlArtifact(artifact) {
} }
/** Real HTML artifacts only — never fall back to stub placeholders. */ /** Real HTML artifacts only — never fall back to stub placeholders. */
export function filterSendableHtmlArtifacts(artifacts = []) { export function filterSendableHtmlArtifacts(artifacts = [], options = {}) {
return artifacts.filter((artifact) => artifactFileExists(artifact) && !isStubPublicHtmlArtifact(artifact)); return artifacts.filter(
(artifact) =>
artifactFileExists(artifact, options) &&
!isStubPublicHtmlArtifact(artifact, options),
);
} }
export function selectSendableHtmlArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) { export function selectSendableHtmlArtifacts({
verifiedArtifacts = [],
confirmedArtifacts = [],
publishDir = '',
} = {}) {
const options = { publishDir };
const candidates = verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts; const candidates = verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts;
return filterSendableHtmlArtifacts(candidates); return filterSendableHtmlArtifacts(candidates, options);
} }
export function verifyPageArtifactContent(artifact, { minBytes = 512 } = {}) { export function verifyPageArtifactContent(artifact, { minBytes = 512, publishDir = '' } = {}) {
if (!artifactFileExists(artifact)) { const localPath = resolveArtifactLocalPath(artifact, publishDir);
const resolvedArtifact = localPath ? { ...artifact, localPath } : artifact;
if (!artifactFileExists(resolvedArtifact, { publishDir })) {
return { ok: false, reason: 'missing_file' }; return { ok: false, reason: 'missing_file' };
} }
if (isStubPublicHtmlArtifact(artifact)) { if (isStubPublicHtmlArtifact(resolvedArtifact, { publishDir })) {
return { ok: false, reason: 'stub_placeholder' }; return { ok: false, reason: 'stub_placeholder' };
} }
if (!String(artifact?.localPath ?? '').trim()) { if (!localPath) {
if ( if (Number(artifact?.sizeBytes ?? 0) < minBytes) {
Number(artifact?.sizeBytes ?? 0) <
minBytes
) {
return { return {
ok: false, ok: false,
reason: 'too_small', reason: 'too_small',
@@ -68,11 +96,11 @@ export function verifyPageArtifactContent(artifact, { minBytes = 512 } = {}) {
} }
return { ok: true, reason: null }; return { ok: true, reason: null };
} }
const size = fs.statSync(artifact.localPath).size; const size = fs.statSync(localPath).size;
if (size < minBytes) { if (size < minBytes) {
return { ok: false, reason: 'too_small' }; return { ok: false, reason: 'too_small' };
} }
const content = fs.readFileSync(artifact.localPath, 'utf8'); const content = fs.readFileSync(localPath, 'utf8');
if (!/<(?:html|body|main|article)\b/i.test(content)) { if (!/<(?:html|body|main|article)\b/i.test(content)) {
return { ok: false, reason: 'not_html_document' }; return { ok: false, reason: 'not_html_document' };
} }
+50 -6
View File
@@ -1,8 +1,13 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path';
import { injectBeforeDocumentClosingHead } from '../../html-document-injection.mjs'; import { injectBeforeDocumentClosingHead } from '../../html-document-injection.mjs';
import { normalizeCoverMetaSuggestion, upsertMindspaceCoverMeta } from '../../mindspace-cover-meta.mjs'; import { normalizeCoverMetaSuggestion, upsertMindspaceCoverMeta } from '../../mindspace-cover-meta.mjs';
import { preparePublishedPlatformBrand } from '../../mindspace-page-tag.mjs'; import { preparePublishedPlatformBrand } from '../../mindspace-page-tag.mjs';
import { artifactFileExists, verifyPageArtifactContent } from './page-artifact.mjs'; import {
artifactFileExists,
resolveArtifactLocalPath,
verifyPageArtifactContent,
} from './page-artifact.mjs';
import { import {
hasMindspaceCoverMeta, hasMindspaceCoverMeta,
hasPlatformBrandMarker, hasPlatformBrandMarker,
@@ -18,6 +23,8 @@ function escapeMetaAttribute(value) {
.replaceAll('<', '&lt;'); .replaceAll('<', '&lt;');
} }
export { resolveArtifactLocalPath } from './page-artifact.mjs';
export function extractPageTitle(html) { export function extractPageTitle(html) {
const match = String(html ?? '').match(/<title[^>]*>([\s\S]*?)<\/title>/i); const match = String(html ?? '').match(/<title[^>]*>([\s\S]*?)<\/title>/i);
return match?.[1]?.replace(/<[^>]+>/g, '').trim() ?? ''; return match?.[1]?.replace(/<[^>]+>/g, '').trim() ?? '';
@@ -70,20 +77,35 @@ export function repairSharePreviewHtml(html, { title = '', topic = '' } = {}) {
return next; return next;
} }
export function repairArtifactSharePreview(artifact, { topic = '' } = {}) { function syncArtifactSharePreview(artifact, localPath) {
if (!artifactFileExists(artifact)) { if (!artifact || !localPath) return artifact;
try {
const content = fs.readFileSync(localPath, 'utf8');
artifact.sharePreview = verifySharePreviewMeta(content);
artifact.exists = true;
artifact.isHtmlDocument = /<(?:html|body|main|article)\b/i.test(content);
} catch {
// Keep the caller's existing artifact metadata when disk read fails.
}
return artifact;
}
export function repairArtifactSharePreview(artifact, { topic = '', publishDir = '' } = {}) {
const localPath = resolveArtifactLocalPath(artifact, publishDir);
const resolvedArtifact = localPath ? { ...artifact, localPath } : artifact;
if (!artifactFileExists(resolvedArtifact)) {
return { ok: false, reason: 'missing_file', changes: [] }; return { ok: false, reason: 'missing_file', changes: [] };
} }
if (!verifyPageArtifactContent(artifact).ok) { if (!verifyPageArtifactContent(resolvedArtifact).ok) {
return { ok: false, reason: 'not_repairable_artifact', changes: [] }; return { ok: false, reason: 'not_repairable_artifact', changes: [] };
} }
const localPath = String(artifact.localPath ?? '').trim();
if (!localPath) { if (!localPath) {
return { ok: false, reason: 'missing_local_path', changes: [] }; return { ok: false, reason: 'missing_local_path', changes: [] };
} }
const before = fs.readFileSync(localPath, 'utf8'); const before = fs.readFileSync(localPath, 'utf8');
if (verifySharePreviewMeta(before).ok) { if (verifySharePreviewMeta(before).ok) {
syncArtifactSharePreview(artifact, localPath);
return { ok: true, reason: null, changes: [], alreadyReady: true }; return { ok: true, reason: null, changes: [], alreadyReady: true };
} }
@@ -99,9 +121,31 @@ export function repairArtifactSharePreview(artifact, { topic = '' } = {}) {
if (!hasPlatformBrandMarker(before) && hasPlatformBrandMarker(after)) changes.push('platform_brand'); if (!hasPlatformBrandMarker(before) && hasPlatformBrandMarker(after)) changes.push('platform_brand');
fs.writeFileSync(localPath, after, 'utf8'); fs.writeFileSync(localPath, after, 'utf8');
const verified = verifyArtifactSharePreview(artifact); syncArtifactSharePreview(artifact, localPath);
const verified = verifyArtifactSharePreview(artifact, { publishDir });
if (!verified.ok) { if (!verified.ok) {
return { ok: false, reason: verified.reason ?? 'repair_incomplete', changes }; return { ok: false, reason: verified.reason ?? 'repair_incomplete', changes };
} }
return { ok: true, reason: null, changes }; return { ok: true, reason: null, changes };
} }
export function ensureArtifactsSharePreviewReady(
artifacts = [],
{ topic = '', publishDir = '' } = {},
) {
const next = [];
for (const artifact of artifacts) {
if (!artifact) continue;
if (artifact.auxiliary || artifact.isStub) {
next.push(artifact);
continue;
}
if (artifact.sharePreview?.ok === true) {
next.push(artifact);
continue;
}
repairArtifactSharePreview(artifact, { topic, publishDir });
next.push(artifact);
}
return next;
}
+23 -18
View File
@@ -1,5 +1,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import { artifactFileExists } from './page-artifact.mjs'; import { artifactFileExists } from './page-artifact.mjs';
import { resolveArtifactLocalPath } from './page-artifact.mjs';
import { hasPlatformBrandMarker } from '../../mindspace-page-tag.mjs'; import { hasPlatformBrandMarker } from '../../mindspace-page-tag.mjs';
export { hasPlatformBrandMarker }; export { hasPlatformBrandMarker };
@@ -57,26 +58,30 @@ export function verifySharePreviewMeta(html) {
return { ok: true, reason: null }; return { ok: true, reason: null };
} }
export function verifyArtifactSharePreview(artifact) { export function verifyArtifactSharePreview(artifact, { publishDir = '' } = {}) {
if (!artifactFileExists(artifact)) { const localPath = resolveArtifactLocalPath(artifact, publishDir);
return { ok: false, reason: 'missing_file' }; if (localPath) {
} try {
if (!String(artifact?.localPath ?? '').trim()) { if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
if ( const content = fs.readFileSync(localPath, 'utf8');
typeof artifact?.sharePreview?.ok === return verifySharePreviewMeta(content);
'boolean' }
) { } catch {
return artifact.sharePreview; // Fall back to cached sharePreview metadata below.
} }
return {
ok: false,
reason: 'missing_share_preview_evaluation',
};
} }
const content = fs.readFileSync(artifact.localPath, 'utf8'); if (
return verifySharePreviewMeta(content); typeof artifact?.sharePreview?.ok ===
'boolean'
) {
return artifact.sharePreview;
}
return {
ok: false,
reason: 'missing_file',
};
} }
export function filterSharePreviewReadyArtifacts(artifacts = []) { export function filterSharePreviewReadyArtifacts(artifacts = [], options = {}) {
return artifacts.filter((artifact) => verifyArtifactSharePreview(artifact).ok); return artifacts.filter((artifact) => verifyArtifactSharePreview(artifact, options).ok);
} }
+26
View File
@@ -215,6 +215,32 @@ test('repairSharePreviewHtml injects missing description, cover, and platform br
assert.match(repaired, /data-mindspace-page-tag="platform-brand"/); assert.match(repaired, /data-mindspace-page-tag="platform-brand"/);
}); });
test('resolvePageGenerateOutcome repairs sanitized artifact using publishDir + relativePath', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-sanitized-'));
const file = path.join(publishDir, 'public', 'page.html');
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(
file,
`<!doctype html><html><head><title>安吉攻略</title></head><body><main>${'x'.repeat(600)}</main></body></html>`,
'utf8',
);
const artifact = {
relativePath: 'public/page.html',
url: 'https://m.tkmind.cn/MindSpace/u/public/page.html',
exists: true,
isHtmlDocument: true,
sharePreview: { ok: false, reason: 'missing_mindspace_cover' },
};
const outcome = resolvePageGenerateOutcome({
reply: { text: '页面已生成' },
confirmedArtifacts: [artifact],
topic: '帮我把这个安吉攻略生成页面',
repairContext: { publishDir },
});
assert.equal(outcome.action, 'send');
assert.equal(verifyArtifactSharePreview(artifact, { publishDir }).ok, true);
});
test('resolvePageGenerateOutcome repairs and sends when html lacks share preview meta', () => { test('resolvePageGenerateOutcome repairs and sends when html lacks share preview meta', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-')); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-'));
const file = path.join(dir, 'page.html'); const file = path.join(dir, 'page.html');