fix(wechat): enforce current-turn image paths in page delivery
Memind CI / Test, build, and release guards (push) Successful in 6m58s
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:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user