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
+36 -4
View File
@@ -1,4 +1,7 @@
import crypto from 'node:crypto';
import {
buildAllowedPageImageEmbedKeys,
} from './chat-image-materialize.mjs';
import path from 'node:path';
import { fetch as undiciFetch } from 'undici';
import { developerToolsFromPolicy } from './capabilities.mjs';
@@ -1121,6 +1124,20 @@ export function resolveWechatRecentMediaPublicUrl(recentMediaEntry) {
return String(recentMediaEntry.items.at(-1)?.media?.publicUrl ?? '').trim();
}
export function collectIntentImageUrls(intent, { mediaAnalysisEnabled = false } = {}) {
if (!mediaAnalysisEnabled) return [];
const mediaPublicUrl = intent?.media?.publicUrl || null;
const mediaItems = Array.isArray(intent?.recentMediaItems) && intent.recentMediaItems.length > 0
? intent.recentMediaItems
: mediaPublicUrl
? [{ media: intent.media, attachment: intent.attachment ?? null }]
: [];
return mediaItems
.filter((item) => !item.attachment)
.map((item) => String(item?.media?.publicUrl ?? '').trim())
.filter((url, index, values) => url && values.indexOf(url) === index);
}
export function prepareWechatIntentForHistoricalImageRetry(intent, { fallbackImageUrl = '' } = {}) {
if (!intent || typeof intent !== 'object') return intent;
const imageUrl = String(intent.media?.publicUrl ?? fallbackImageUrl ?? '').trim();
@@ -2561,16 +2578,13 @@ export function createWechatMpService({
pgRequired = false,
} = {},
) => {
const imageUrls = collectIntentImageUrls(intent, { mediaAnalysisEnabled });
const mediaPublicUrl = intent.media?.publicUrl || null;
const mediaItems = Array.isArray(intent.recentMediaItems) && intent.recentMediaItems.length > 0
? intent.recentMediaItems
: mediaPublicUrl
? [{ media: intent.media, attachment: intent.attachment ?? null }]
: [];
const imageUrls = mediaItems
.filter((item) => !item.attachment)
.map((item) => String(item?.media?.publicUrl ?? '').trim())
.filter((url, index, values) => url && values.indexOf(url) === index);
const fileAttachments = mediaItems
.filter((item) => item.attachment?.filename && item.media?.publicUrl)
.map((item) => ({
@@ -3071,6 +3085,14 @@ export function createWechatMpService({
: [];
if (wechatIntent.kind === 'page.generate') {
const intentImageUrls = collectIntentImageUrls(intent, { mediaAnalysisEnabled });
const allowedImageEmbedKeys = intentImageUrls.length > 0
? buildAllowedPageImageEmbedKeys({
publishDir: userPublishDir,
imageUrls: intentImageUrls,
userId: user.userId,
})
: null;
const pageOutcome = resolvePageGenerateOutcome({
reply,
confirmedArtifacts,
@@ -3084,6 +3106,7 @@ export function createWechatMpService({
publishDir: userPublishDir,
},
wechatCursorChannel: isWechatCursorChannelReply(reply),
allowedImageEmbedKeys,
});
if (pageOutcome.action === 'session_retry') {
if (sessionPageContinuation && pageAttempt < maxImmediateContextPageAttempts) {
@@ -3479,6 +3502,14 @@ export function createWechatMpService({
: [];
if (wechatIntent.kind === 'page.generate') {
const intentImageUrls = collectIntentImageUrls(intent, { mediaAnalysisEnabled });
const allowedImageEmbedKeys = intentImageUrls.length > 0
? buildAllowedPageImageEmbedKeys({
publishDir: userPublishDir,
imageUrls: intentImageUrls,
userId: user.userId,
})
: null;
const pageOutcome = resolvePageGenerateOutcome({
reply,
confirmedArtifacts,
@@ -3492,6 +3523,7 @@ export function createWechatMpService({
publishDir: userPublishDir,
},
wechatCursorChannel: isWechatCursorChannelReply(reply),
allowedImageEmbedKeys,
});
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
const text = pageOutcome.failureText ?? buildPagePublishFailureText();