fix(wechat): repair share preview on disk for sanitized delivery artifacts
Memind CI / Test, build, and release guards (push) Failing after 6s
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:
@@ -4,6 +4,7 @@ import path from 'node:path';
|
||||
import { extractStaticPageLinks, materializePrivateAssetsInPublicHtmlFiles } from './mindspace-chat-save.mjs';
|
||||
import { DOWNLOADABLE_FILE_PATTERN } from './mindspace-html-download-links.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).
|
||||
@@ -397,7 +398,8 @@ export function materializeMissingPublicHtmlWrites({ messages, publishDir }) {
|
||||
}
|
||||
try {
|
||||
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);
|
||||
scheduleWorkspaceHtmlThumbnailSidecars(root, artifact.relativePath);
|
||||
} catch {
|
||||
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
import {
|
||||
verifySharePreviewMeta,
|
||||
} from './wechat/verify/share-preview.mjs';
|
||||
import {
|
||||
ensureArtifactsSharePreviewReady,
|
||||
} from './wechat/verify/share-preview-repair.mjs';
|
||||
|
||||
const PUBLIC_HTML_LINK_PATTERN =
|
||||
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
|
||||
@@ -397,11 +400,26 @@ export function prepareWechatHtmlDeliveryAtWorkspace({
|
||||
requestStartedAt,
|
||||
})
|
||||
: [];
|
||||
const confirmedArtifacts = uniqueArtifacts([
|
||||
...publishedArtifacts,
|
||||
...expectedArtifacts,
|
||||
...recentArtifacts,
|
||||
]);
|
||||
const confirmedArtifacts = ensureArtifactsSharePreviewReady(
|
||||
uniqueArtifacts([
|
||||
...publishedArtifacts,
|
||||
...expectedArtifacts,
|
||||
...recentArtifacts,
|
||||
]),
|
||||
{
|
||||
publishDir,
|
||||
topic: String(
|
||||
intent?.agentText ?? intent?.displayText ?? '',
|
||||
).trim(),
|
||||
},
|
||||
).map(
|
||||
(artifact) =>
|
||||
readArtifactDescriptor(artifact.relativePath, {
|
||||
publishDir,
|
||||
buildCanonicalUrl,
|
||||
copyIntoPublic: false,
|
||||
}) ?? artifact,
|
||||
);
|
||||
const linkedFilenames =
|
||||
collectLinkedHtmlFilenames(reply?.text);
|
||||
const matchedArtifacts =
|
||||
@@ -610,6 +628,7 @@ export async function ensureWechatFreshPageThumbnailsAtWorkspace({
|
||||
verifyFreshWechatPageThumbnails(
|
||||
workspaceArtifacts,
|
||||
images,
|
||||
{ publishDir },
|
||||
);
|
||||
let repair = null;
|
||||
if (
|
||||
@@ -638,6 +657,7 @@ export async function ensureWechatFreshPageThumbnailsAtWorkspace({
|
||||
verifyFreshWechatPageThumbnails(
|
||||
workspaceArtifacts,
|
||||
images,
|
||||
{ publishDir },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,17 +81,16 @@ test('MindSpace prepares WeChat HTML delivery with logical artifacts only', (t)
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
publishDir,
|
||||
'public',
|
||||
'hello.html',
|
||||
),
|
||||
'utf8',
|
||||
const written = fs.readFileSync(
|
||||
path.join(
|
||||
publishDir,
|
||||
'public',
|
||||
'hello.html',
|
||||
),
|
||||
content,
|
||||
'utf8',
|
||||
);
|
||||
assert.match(written, /name="description"/);
|
||||
assert.match(written, /data-mindspace-page-tag="platform-brand"/);
|
||||
assert.equal(
|
||||
result.confirmedArtifacts.length,
|
||||
1,
|
||||
@@ -107,12 +106,12 @@ test('MindSpace prepares WeChat HTML delivery with logical artifacts only', (t)
|
||||
isStub: false,
|
||||
isHtmlDocument: true,
|
||||
sharePreview: {
|
||||
ok: false,
|
||||
reason: 'missing_description',
|
||||
ok: true,
|
||||
reason: null,
|
||||
},
|
||||
auxiliary: false,
|
||||
sizeBytes:
|
||||
Buffer.byteLength(content),
|
||||
Buffer.byteLength(written),
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
|
||||
@@ -105,6 +105,7 @@ export async function bootstrapPortalIntegrationServices({
|
||||
config: wechatMpConfig,
|
||||
userAuth,
|
||||
sessionAccess,
|
||||
h5Root,
|
||||
htmlDeliveryAuthority:
|
||||
typeof mindSpacePublicFinish
|
||||
?.prepareWechatHtmlDelivery ===
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
collectWechatGeneratedImages,
|
||||
} from './wechat/verify/generated-thumbnail.mjs';
|
||||
import { resolveBillingTokenState } from './billing-token-state.mjs';
|
||||
import { resolveMindSpaceUserPublishDir } from './mindspace-runtime-config.mjs';
|
||||
import {
|
||||
buildPageDataCollectFailureText,
|
||||
} from './mindspace-page-data-finish-guard.mjs';
|
||||
@@ -1503,6 +1504,7 @@ export function createWechatMpService({
|
||||
pageDataDeliveryReviewer = null,
|
||||
wechatFetch = undiciFetch,
|
||||
linkExists = defaultPublicHtmlLinkExists,
|
||||
h5Root = '',
|
||||
logger = console,
|
||||
}) {
|
||||
const sessionStore = resolveSessionAccess({ userAuth, sessionAccess });
|
||||
@@ -1513,6 +1515,11 @@ export function createWechatMpService({
|
||||
}
|
||||
const requireFreshPageThumbnail = config.requireFreshPageThumbnail === 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 = {
|
||||
...loadWechatMpConfig({}),
|
||||
...config,
|
||||
@@ -2650,6 +2657,9 @@ export function createWechatMpService({
|
||||
htmlGenerationNeedsRetry,
|
||||
replyHasPublicLinks,
|
||||
topic: wechatIntent?.topic ?? intent?.agentText ?? '',
|
||||
repairContext: {
|
||||
publishDir: resolveUserPublishDir(user.userId),
|
||||
},
|
||||
});
|
||||
if (pageOutcome.action === 'session_retry') {
|
||||
throw new Error('stale_session_poisoned_completion');
|
||||
@@ -2969,6 +2979,9 @@ export function createWechatMpService({
|
||||
htmlGenerationNeedsRetry,
|
||||
replyHasPublicLinks,
|
||||
topic: wechatIntent?.topic ?? intent?.agentText ?? '',
|
||||
repairContext: {
|
||||
publishDir: resolveUserPublishDir(user.userId),
|
||||
},
|
||||
});
|
||||
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
|
||||
const text = pageOutcome.failureText ?? buildPagePublishFailureText();
|
||||
|
||||
@@ -8,17 +8,25 @@ export function evaluatePageGenerateSendableArtifacts({
|
||||
confirmedArtifacts = [],
|
||||
repairContext = {},
|
||||
} = {}) {
|
||||
const sendable = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
const verified = sendable.filter((artifact) => verifyPageArtifactContent(artifact).ok);
|
||||
const publishDir = String(repairContext.publishDir ?? '').trim();
|
||||
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 ready = filterSharePreviewReadyArtifacts(candidates);
|
||||
const ready = filterSharePreviewReadyArtifacts(candidates, previewOptions);
|
||||
if (ready.length > 0) return ready;
|
||||
|
||||
const repaired = [];
|
||||
for (const artifact of candidates) {
|
||||
if (!verifyPageArtifactContent(artifact).ok) continue;
|
||||
if (!verifyPageArtifactContent(artifact, previewOptions).ok) continue;
|
||||
const result = repairArtifactSharePreview(artifact, repairContext);
|
||||
if (result.ok && verifyArtifactSharePreview(artifact).ok) {
|
||||
if (result.ok && verifyArtifactSharePreview(artifact, previewOptions).ok) {
|
||||
repaired.push(artifact);
|
||||
}
|
||||
}
|
||||
@@ -42,8 +50,10 @@ export function resolvePageGenerateOutcome({
|
||||
}) {
|
||||
const context = {
|
||||
topic: String(topic || repairContext.topic || '').trim(),
|
||||
publishDir: String(repairContext.publishDir ?? '').trim(),
|
||||
...repairContext,
|
||||
};
|
||||
const previewOptions = { publishDir: context.publishDir };
|
||||
const sendable = evaluatePageGenerateSendableArtifacts({
|
||||
verifiedArtifacts,
|
||||
confirmedArtifacts,
|
||||
@@ -71,11 +81,21 @@ export function resolvePageGenerateOutcome({
|
||||
}
|
||||
|
||||
const previewCandidates = filterSharePreviewReadyArtifacts(
|
||||
selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }),
|
||||
selectSendableHtmlArtifacts({
|
||||
verifiedArtifacts,
|
||||
confirmedArtifacts,
|
||||
publishDir: context.publishDir,
|
||||
}),
|
||||
previewOptions,
|
||||
);
|
||||
if (previewCandidates.length === 0 && confirmedArtifacts.length > 0) {
|
||||
const previewIssue = verifyArtifactSharePreview(
|
||||
selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts })[0] ?? confirmedArtifacts[0],
|
||||
selectSendableHtmlArtifacts({
|
||||
verifiedArtifacts,
|
||||
confirmedArtifacts,
|
||||
publishDir: context.publishDir,
|
||||
})[0] ?? confirmedArtifacts[0],
|
||||
previewOptions,
|
||||
);
|
||||
return {
|
||||
action: 'fail',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseMindspaceCoverMeta,
|
||||
upsertMindspaceCoverMeta,
|
||||
} from '../../mindspace-cover-meta.mjs';
|
||||
import { resolveArtifactLocalPath } from './page-artifact.mjs';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function verifyFreshWechatPageThumbnails(artifacts = [], images = []) {
|
||||
export function verifyFreshWechatPageThumbnails(
|
||||
artifacts = [],
|
||||
images = [],
|
||||
{ publishDir = '' } = {},
|
||||
) {
|
||||
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
||||
return { ok: false, reason: 'missing_page_artifact', matches: [] };
|
||||
}
|
||||
@@ -136,7 +141,7 @@ export function verifyFreshWechatPageThumbnails(artifacts = [], images = []) {
|
||||
const usedJobs = new Set();
|
||||
const matches = [];
|
||||
for (const artifact of eligibleArtifacts) {
|
||||
const localPath = String(artifact?.localPath ?? '').trim();
|
||||
const localPath = resolveArtifactLocalPath(artifact, publishDir);
|
||||
if (!localPath || !fs.existsSync(localPath)) {
|
||||
return { ok: false, reason: 'missing_page_artifact', artifact, matches, skippedArtifacts };
|
||||
}
|
||||
|
||||
@@ -1,30 +1,50 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
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) {
|
||||
const value = String(content ?? '');
|
||||
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') {
|
||||
return artifact.exists;
|
||||
}
|
||||
const localPath = String(artifact?.localPath ?? '').trim();
|
||||
if (!localPath) return false;
|
||||
try {
|
||||
return fs.existsSync(localPath) && fs.statSync(localPath).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isStubPublicHtmlArtifact(artifact) {
|
||||
export function isStubPublicHtmlArtifact(artifact, { publishDir = '' } = {}) {
|
||||
if (typeof artifact?.isStub === 'boolean') {
|
||||
return artifact.isStub;
|
||||
}
|
||||
const localPath = String(artifact?.localPath ?? '').trim();
|
||||
const localPath = resolveArtifactLocalPath(artifact, publishDir);
|
||||
if (!localPath) return false;
|
||||
try {
|
||||
return isStubPublicHtmlContent(fs.readFileSync(localPath, 'utf8'));
|
||||
@@ -34,27 +54,35 @@ export function isStubPublicHtmlArtifact(artifact) {
|
||||
}
|
||||
|
||||
/** Real HTML artifacts only — never fall back to stub placeholders. */
|
||||
export function filterSendableHtmlArtifacts(artifacts = []) {
|
||||
return artifacts.filter((artifact) => artifactFileExists(artifact) && !isStubPublicHtmlArtifact(artifact));
|
||||
export function filterSendableHtmlArtifacts(artifacts = [], options = {}) {
|
||||
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;
|
||||
return filterSendableHtmlArtifacts(candidates);
|
||||
return filterSendableHtmlArtifacts(candidates, options);
|
||||
}
|
||||
|
||||
export function verifyPageArtifactContent(artifact, { minBytes = 512 } = {}) {
|
||||
if (!artifactFileExists(artifact)) {
|
||||
export function verifyPageArtifactContent(artifact, { minBytes = 512, publishDir = '' } = {}) {
|
||||
const localPath = resolveArtifactLocalPath(artifact, publishDir);
|
||||
const resolvedArtifact = localPath ? { ...artifact, localPath } : artifact;
|
||||
if (!artifactFileExists(resolvedArtifact, { publishDir })) {
|
||||
return { ok: false, reason: 'missing_file' };
|
||||
}
|
||||
if (isStubPublicHtmlArtifact(artifact)) {
|
||||
if (isStubPublicHtmlArtifact(resolvedArtifact, { publishDir })) {
|
||||
return { ok: false, reason: 'stub_placeholder' };
|
||||
}
|
||||
if (!String(artifact?.localPath ?? '').trim()) {
|
||||
if (
|
||||
Number(artifact?.sizeBytes ?? 0) <
|
||||
minBytes
|
||||
) {
|
||||
if (!localPath) {
|
||||
if (Number(artifact?.sizeBytes ?? 0) < minBytes) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'too_small',
|
||||
@@ -68,11 +96,11 @@ export function verifyPageArtifactContent(artifact, { minBytes = 512 } = {}) {
|
||||
}
|
||||
return { ok: true, reason: null };
|
||||
}
|
||||
const size = fs.statSync(artifact.localPath).size;
|
||||
const size = fs.statSync(localPath).size;
|
||||
if (size < minBytes) {
|
||||
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)) {
|
||||
return { ok: false, reason: 'not_html_document' };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { injectBeforeDocumentClosingHead } from '../../html-document-injection.mjs';
|
||||
import { normalizeCoverMetaSuggestion, upsertMindspaceCoverMeta } from '../../mindspace-cover-meta.mjs';
|
||||
import { preparePublishedPlatformBrand } from '../../mindspace-page-tag.mjs';
|
||||
import { artifactFileExists, verifyPageArtifactContent } from './page-artifact.mjs';
|
||||
import {
|
||||
artifactFileExists,
|
||||
resolveArtifactLocalPath,
|
||||
verifyPageArtifactContent,
|
||||
} from './page-artifact.mjs';
|
||||
import {
|
||||
hasMindspaceCoverMeta,
|
||||
hasPlatformBrandMarker,
|
||||
@@ -18,6 +23,8 @@ function escapeMetaAttribute(value) {
|
||||
.replaceAll('<', '<');
|
||||
}
|
||||
|
||||
export { resolveArtifactLocalPath } from './page-artifact.mjs';
|
||||
|
||||
export function extractPageTitle(html) {
|
||||
const match = String(html ?? '').match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
||||
return match?.[1]?.replace(/<[^>]+>/g, '').trim() ?? '';
|
||||
@@ -70,20 +77,35 @@ export function repairSharePreviewHtml(html, { title = '', topic = '' } = {}) {
|
||||
return next;
|
||||
}
|
||||
|
||||
export function repairArtifactSharePreview(artifact, { topic = '' } = {}) {
|
||||
if (!artifactFileExists(artifact)) {
|
||||
function syncArtifactSharePreview(artifact, localPath) {
|
||||
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: [] };
|
||||
}
|
||||
if (!verifyPageArtifactContent(artifact).ok) {
|
||||
if (!verifyPageArtifactContent(resolvedArtifact).ok) {
|
||||
return { ok: false, reason: 'not_repairable_artifact', changes: [] };
|
||||
}
|
||||
const localPath = String(artifact.localPath ?? '').trim();
|
||||
if (!localPath) {
|
||||
return { ok: false, reason: 'missing_local_path', changes: [] };
|
||||
}
|
||||
|
||||
const before = fs.readFileSync(localPath, 'utf8');
|
||||
if (verifySharePreviewMeta(before).ok) {
|
||||
syncArtifactSharePreview(artifact, localPath);
|
||||
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');
|
||||
|
||||
fs.writeFileSync(localPath, after, 'utf8');
|
||||
const verified = verifyArtifactSharePreview(artifact);
|
||||
syncArtifactSharePreview(artifact, localPath);
|
||||
const verified = verifyArtifactSharePreview(artifact, { publishDir });
|
||||
if (!verified.ok) {
|
||||
return { ok: false, reason: verified.reason ?? 'repair_incomplete', 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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'node:fs';
|
||||
import { artifactFileExists } from './page-artifact.mjs';
|
||||
import { resolveArtifactLocalPath } from './page-artifact.mjs';
|
||||
import { hasPlatformBrandMarker } from '../../mindspace-page-tag.mjs';
|
||||
|
||||
export { hasPlatformBrandMarker };
|
||||
@@ -57,26 +58,30 @@ export function verifySharePreviewMeta(html) {
|
||||
return { ok: true, reason: null };
|
||||
}
|
||||
|
||||
export function verifyArtifactSharePreview(artifact) {
|
||||
if (!artifactFileExists(artifact)) {
|
||||
return { ok: false, reason: 'missing_file' };
|
||||
}
|
||||
if (!String(artifact?.localPath ?? '').trim()) {
|
||||
if (
|
||||
typeof artifact?.sharePreview?.ok ===
|
||||
'boolean'
|
||||
) {
|
||||
return artifact.sharePreview;
|
||||
export function verifyArtifactSharePreview(artifact, { publishDir = '' } = {}) {
|
||||
const localPath = resolveArtifactLocalPath(artifact, publishDir);
|
||||
if (localPath) {
|
||||
try {
|
||||
if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
|
||||
const content = fs.readFileSync(localPath, 'utf8');
|
||||
return verifySharePreviewMeta(content);
|
||||
}
|
||||
} catch {
|
||||
// Fall back to cached sharePreview metadata below.
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'missing_share_preview_evaluation',
|
||||
};
|
||||
}
|
||||
const content = fs.readFileSync(artifact.localPath, 'utf8');
|
||||
return verifySharePreviewMeta(content);
|
||||
if (
|
||||
typeof artifact?.sharePreview?.ok ===
|
||||
'boolean'
|
||||
) {
|
||||
return artifact.sharePreview;
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'missing_file',
|
||||
};
|
||||
}
|
||||
|
||||
export function filterSharePreviewReadyArtifacts(artifacts = []) {
|
||||
return artifacts.filter((artifact) => verifyArtifactSharePreview(artifact).ok);
|
||||
export function filterSharePreviewReadyArtifacts(artifacts = [], options = {}) {
|
||||
return artifacts.filter((artifact) => verifyArtifactSharePreview(artifact, options).ok);
|
||||
}
|
||||
|
||||
@@ -215,6 +215,32 @@ test('repairSharePreviewHtml injects missing description, cover, and platform br
|
||||
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', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-'));
|
||||
const file = path.join(dir, 'page.html');
|
||||
|
||||
Reference in New Issue
Block a user