fix: recover WeChat page thumbnail delivery
Memind CI / Test, build, and release guards (pull_request) Successful in 5m1s

This commit is contained in:
john
2026-07-22 11:51:15 +08:00
parent 57d2c14fca
commit cb62b36cd5
13 changed files with 366 additions and 26 deletions
+107
View File
@@ -1,5 +1,10 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import {
parseMindspaceCoverMeta,
upsertMindspaceCoverMeta,
} from '../../mindspace-cover-meta.mjs';
const RASTER_MIME_PATTERN = /^image\/(?:png|jpeg|webp)$/i;
@@ -29,6 +34,7 @@ function normalizeGeneratedImage(result) {
if (!htmlSrc && !publicUrl && !workspaceRelativePath) return null;
return {
jobId: String(result.jobId),
purpose: String(result.purpose ?? '').trim() || null,
mimeType,
assetId: String(asset.id ?? '').trim() || null,
htmlSrc: htmlSrc || null,
@@ -141,6 +147,7 @@ export function verifyFreshWechatPageThumbnails(artifacts = [], images = []) {
ok: false,
reason: cover ? 'cover_not_from_current_run' : 'missing_generated_cover',
artifact,
cover: cover || null,
matches,
skippedArtifacts,
};
@@ -150,3 +157,103 @@ export function verifyFreshWechatPageThumbnails(artifacts = [], images = []) {
}
return { ok: true, reason: null, matches, skippedArtifacts };
}
function uniqueArtifactsByLocalPath(artifacts = []) {
const unique = new Map();
for (const artifact of artifacts) {
const localPath = String(artifact?.localPath ?? '').trim();
if (localPath) unique.set(localPath, artifact);
}
return [...unique.values()];
}
function atomicWriteHtml(localPath, content) {
const tempPath = path.join(
path.dirname(localPath),
`.${path.basename(localPath)}.wechat-thumbnail-${process.pid}-${crypto.randomUUID()}.tmp`,
);
try {
fs.writeFileSync(tempPath, content, 'utf8');
fs.renameSync(tempPath, localPath);
} finally {
try {
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
} catch {
// Best-effort cleanup only; the destination write already decided the result.
}
}
}
export function repairUnambiguousFreshWechatPageThumbnail({
artifacts = [],
images = [],
currentRunHtmlArtifacts = [],
verificationReason = '',
} = {}) {
if (!['missing_generated_cover', 'cover_not_from_current_run'].includes(verificationReason)) {
return { ok: false, reason: 'verification_not_repairable' };
}
const eligibleArtifacts = uniqueArtifactsByLocalPath(
artifacts.filter((artifact) => !isWechatAuxiliaryPageArtifact(artifact)),
);
const pageImages = images.filter((image) => image?.purpose === 'hero' || !image?.purpose);
if (eligibleArtifacts.length !== 1 || pageImages.length !== 1) {
return {
ok: false,
reason: 'ambiguous_page_image_mapping',
artifactCount: eligibleArtifacts.length,
imageCount: pageImages.length,
};
}
const artifact = eligibleArtifacts[0];
const image = pageImages[0];
const cover = String(image?.htmlSrc ?? '').trim();
const localPath = String(artifact?.localPath ?? '').trim();
if (!cover || !localPath || !fs.existsSync(localPath)) {
return { ok: false, reason: 'repair_source_unavailable', artifact, image };
}
const normalizedRelativePath = String(artifact.relativePath ?? '').trim().replace(/\\/g, '/');
const currentRunArtifact = currentRunHtmlArtifacts.find(
(candidate) => String(candidate?.relativePath ?? '').trim().replace(/\\/g, '/') === normalizedRelativePath,
);
const sourceHtml = typeof currentRunArtifact?.content === 'string'
? currentRunArtifact.content
: fs.readFileSync(localPath, 'utf8');
const coverMeta = parseMindspaceCoverMeta(sourceHtml);
if (!coverMeta) {
return { ok: false, reason: 'valid_cover_meta_required', artifact, image };
}
const repairedHtml = upsertMindspaceCoverMeta(sourceHtml, { cover });
atomicWriteHtml(localPath, repairedHtml);
return {
ok: true,
reason: null,
artifact,
image,
cover,
restoredCurrentRunHtml: Boolean(currentRunArtifact),
};
}
export function summarizeFreshWechatThumbnailVerification(verification, images = []) {
return {
reason: String(verification?.reason ?? 'unknown'),
artifact: verification?.artifact
? {
relativePath: String(verification.artifact.relativePath ?? ''),
localPath: path.basename(String(verification.artifact.localPath ?? '')),
}
: null,
cover: String(verification?.cover ?? ''),
matches: Array.isArray(verification?.matches) ? verification.matches.length : 0,
images: images.map((image) => ({
jobId: String(image?.jobId ?? ''),
purpose: String(image?.purpose ?? ''),
htmlSrc: String(image?.htmlSrc ?? ''),
workspaceRelativePath: String(image?.workspaceRelativePath ?? ''),
})),
};
}
@@ -6,12 +6,14 @@ import test from 'node:test';
import {
collectWechatGeneratedImages,
extractMindspaceCoverPath,
repairUnambiguousFreshWechatPageThumbnail,
verifyFreshWechatPageThumbnails,
} from './generated-thumbnail.mjs';
function generatedImageMessages({ jobId = 'job-1', htmlSrc = 'images/fresh.webp' } = {}) {
const result = {
ok: true,
purpose: 'hero',
jobId,
source: { mimeType: 'image/webp' },
asset: {
@@ -115,3 +117,60 @@ test('auxiliary admin pages do not require a separate generated thumbnail', () =
fs.rmSync(root, { recursive: true, force: true });
}
});
test('one current-run hero repairs one page from the current write snapshot and re-verifies', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-repair-cover-'));
try {
const htmlPath = path.join(root, 'page.html');
const diskHtml = '<html><head><meta name="mindspace-cover" content=\'{"tag":"页面","subtitle":"磁盘旧副本"}\'></head><body>disk copy</body></html>';
const currentRunHtml = '<html><head><meta name="mindspace-cover" content=\'{"tag":"页面","subtitle":"本轮副本"}\'></head><body>current run copy</body></html>';
fs.writeFileSync(htmlPath, diskHtml, 'utf8');
const artifacts = [{ localPath: htmlPath, relativePath: 'public/page.html' }];
const images = collectWechatGeneratedImages(generatedImageMessages());
const failed = verifyFreshWechatPageThumbnails(artifacts, images);
assert.equal(failed.reason, 'missing_generated_cover');
const repaired = repairUnambiguousFreshWechatPageThumbnail({
artifacts,
images,
currentRunHtmlArtifacts: [{ relativePath: 'public/page.html', content: currentRunHtml }],
verificationReason: failed.reason,
});
assert.equal(repaired.ok, true);
assert.equal(repaired.restoredCurrentRunHtml, true);
const written = fs.readFileSync(htmlPath, 'utf8');
assert.match(written, /current run copy/);
assert.doesNotMatch(written, /disk copy/);
assert.equal(extractMindspaceCoverPath(written), 'images/fresh.webp');
assert.equal(verifyFreshWechatPageThumbnails(artifacts, images).ok, true);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('thumbnail repair refuses an ambiguous page-to-image mapping without changing the page', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-ambiguous-repair-'));
try {
const htmlPath = path.join(root, 'page.html');
const original = '<meta name="mindspace-cover" content=\'{"tag":"页面"}\'>';
fs.writeFileSync(htmlPath, original, 'utf8');
const artifacts = [{ localPath: htmlPath, relativePath: 'public/page.html' }];
const images = collectWechatGeneratedImages([
...generatedImageMessages({ jobId: 'job-1', htmlSrc: 'images/one.webp' }),
...generatedImageMessages({ jobId: 'job-2', htmlSrc: 'images/two.webp' }),
]);
const repaired = repairUnambiguousFreshWechatPageThumbnail({
artifacts,
images,
verificationReason: 'missing_generated_cover',
});
assert.equal(repaired.ok, false);
assert.equal(repaired.reason, 'ambiguous_page_image_mapping');
assert.equal(fs.readFileSync(htmlPath, 'utf8'), original);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});