fix(wechat): enforce current-turn image paths in page delivery
Memind CI / Test, build, and release guards (push) Successful in 6m58s

Materialize wechat-mp uploads into dated public/images paths during vision
preprocessing and fail closed when delivered HTML references stale workspace
images instead of the current turn allowlist.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-27 12:54:46 +08:00
parent aecde46ff6
commit 760a1760ae
7 changed files with 442 additions and 9 deletions
+32 -2
View File
@@ -1,5 +1,5 @@
import { buildPagePublishFailureText } from '../prompts/page-generate.mjs';
import { selectSendableHtmlArtifacts, verifyPageArtifactContent } from '../verify/page-artifact.mjs';
import { selectSendableHtmlArtifacts, verifyPageArtifactContent, verifyPageArtifactImageSources } from '../verify/page-artifact.mjs';
import { repairArtifactSharePreview } from '../verify/share-preview-repair.mjs';
import { filterSharePreviewReadyArtifacts, verifyArtifactSharePreview } from '../verify/share-preview.mjs';
@@ -7,6 +7,7 @@ export function evaluatePageGenerateSendableArtifacts({
verifiedArtifacts = [],
confirmedArtifacts = [],
repairContext = {},
allowedImageEmbedKeys = null,
} = {}) {
const publishDir = String(repairContext.publishDir ?? '').trim();
const previewOptions = { publishDir };
@@ -18,7 +19,13 @@ export function evaluatePageGenerateSendableArtifacts({
const verified = sendable.filter((artifact) =>
verifyPageArtifactContent(artifact, previewOptions).ok,
);
const candidates = verified.length > 0 ? verified : sendable;
const imageVerified = (verified.length > 0 ? verified : sendable).filter((artifact) =>
verifyPageArtifactImageSources(artifact, {
publishDir,
allowedImageEmbedKeys,
}).ok,
);
const candidates = imageVerified.length > 0 ? imageVerified : [];
const ready = filterSharePreviewReadyArtifacts(candidates, previewOptions);
if (ready.length > 0) return ready;
@@ -48,6 +55,7 @@ export function resolvePageGenerateOutcome({
topic = '',
repairContext = {},
wechatCursorChannel = false,
allowedImageEmbedKeys = null,
}) {
const context = {
topic: String(topic || repairContext.topic || '').trim(),
@@ -59,8 +67,30 @@ export function resolvePageGenerateOutcome({
verifiedArtifacts,
confirmedArtifacts,
repairContext: context,
allowedImageEmbedKeys,
});
if (sendable.length === 0 && allowedImageEmbedKeys instanceof Set && allowedImageEmbedKeys.size > 0) {
const staleCandidate = selectSendableHtmlArtifacts({
verifiedArtifacts,
confirmedArtifacts,
publishDir: context.publishDir,
}).find((artifact) =>
verifyPageArtifactContent(artifact, previewOptions).ok
&& !verifyPageArtifactImageSources(artifact, {
publishDir: context.publishDir,
allowedImageEmbedKeys,
}).ok,
);
if (staleCandidate) {
return {
action: 'fail',
failureText: buildPagePublishFailureText(),
reason: 'stale_image_source',
};
}
}
if (sendable.length > 0) {
return { action: 'send', artifacts: sendable };
}
+34
View File
@@ -1,5 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import { verifyHtmlImageSourcesAllowed } from '../../chat-image-materialize.mjs';
const STUB_MARKERS = ['临时补出', '服务号兜底', '服务号自动补出简版页面'];
@@ -106,3 +107,36 @@ export function verifyPageArtifactContent(artifact, { minBytes = 512, publishDir
}
return { ok: true, reason: null };
}
export function verifyPageArtifactImageSources(
artifact,
{
publishDir = '',
allowedImageEmbedKeys = null,
} = {},
) {
const allowed = allowedImageEmbedKeys instanceof Set
? allowedImageEmbedKeys
: Array.isArray(allowedImageEmbedKeys)
? new Set(allowedImageEmbedKeys)
: null;
if (!allowed || allowed.size === 0) {
return { ok: true, reason: null, offendingKeys: [] };
}
const localPath = resolveArtifactLocalPath(artifact, publishDir);
if (!localPath) {
return { ok: false, reason: 'missing_file', offendingKeys: [] };
}
let content = '';
try {
content = fs.readFileSync(localPath, 'utf8');
} catch {
return { ok: false, reason: 'missing_file', offendingKeys: [] };
}
const verification = verifyHtmlImageSourcesAllowed(content, allowed);
return {
ok: verification.ok,
reason: verification.reason,
offendingKeys: verification.offendingKeys ?? [],
};
}
+28
View File
@@ -13,6 +13,7 @@ import {
} from './verify/page-artifact.mjs';
import { guardScheduleConfirmationReply, looksLikeScheduleConfirmation } from './handlers/schedule-guard.mjs';
import { resolvePageGenerateOutcome } from './handlers/page-generate.mjs';
import { buildAllowedPageImageEmbedKeys } from '../chat-image-materialize.mjs';
import { buildGreetingText, resolveSyncReply } from './handlers/sync-replies.mjs';
import { buildWechatAgentPrompt } from './prompts/chat-general.mjs';
import {
@@ -167,6 +168,33 @@ test('schedule confirmation guard rewrites agent pseudo-confirm when ITL enabled
assert.doesNotMatch(guarded, /不能算设置成功/);
});
test('resolvePageGenerateOutcome fails closed when html reuses stale image paths', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-stale-image-'));
const wechatMp = path.join(publishDir, 'public/wechat-mp/new.jpg');
fs.mkdirSync(path.dirname(wechatMp), { recursive: true });
fs.writeFileSync(wechatMp, Buffer.from('fresh-image'));
const htmlPath = path.join(publishDir, 'public/page.html');
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(
htmlPath,
`<!doctype html><html><head><meta name="description" content="摘要"><meta name="mindspace-cover" content='{"tag":"页面","cover":"images/2026-08-22/old.jpg"}'><p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p></head><body><main>${'x'.repeat(600)}<img src="images/2026-08-22/old.jpg"></main></body></html>`,
'utf8',
);
const allowedImageEmbedKeys = buildAllowedPageImageEmbedKeys({
publishDir,
imageUrls: ['https://m.tkmind.cn/MindSpace/user-1/public/wechat-mp/new.jpg'],
});
const outcome = resolvePageGenerateOutcome({
reply: { text: '页面完成' },
confirmedArtifacts: [{ localPath: htmlPath, relativePath: 'public/page.html' }],
repairContext: { publishDir },
allowedImageEmbedKeys,
});
assert.equal(outcome.action, 'fail');
assert.equal(outcome.reason, 'stale_image_source');
fs.rmSync(publishDir, { recursive: true, force: true });
});
test('resolvePageGenerateOutcome fails when only stub artifacts exist', () => {
const outcome = resolvePageGenerateOutcome({
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/tang-poem.html' },