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 -3
View File
@@ -39,6 +39,10 @@ import {
extractCurrentTurnImageUrls,
scrubConversationHistoricalImageAttachments,
} from './chat-image-turn-scope.mjs';
import {
collectEmbedAssetKeys,
materializeWechatPublicImageForEmbed,
} from './chat-image-materialize.mjs';
import { repairConversationToolHistory } from './chat-tool-history-repair.mjs';
import { buildVisionThumbnailBuffer } from './vision-image-thumb.mjs';
import {
@@ -864,6 +868,7 @@ export async function buildVisionPayload({
userMessage,
userId,
publishLayout,
publishDir = null,
localFetchAsset,
llmProviderService,
imgproxySigner = null,
@@ -922,6 +927,14 @@ export async function buildVisionPayload({
const parsed = new URL(rawUrl);
relativePath = parsed.pathname + parsed.search;
} catch { /* keep rawUrl */ }
const materialized = publishDir
? materializeWechatPublicImageForEmbed({
publishDir,
rawUrl,
buffer,
mimeType,
})
: null;
const publicStandardUrl = buildPublicStandardImageUrl(rawUrl, userId, publishLayout);
let visionBuffer = buffer;
let visionMimeType = mimeType;
@@ -940,7 +953,12 @@ export async function buildVisionPayload({
data: visionBuffer.toString('base64'),
relativePath,
rawUrl,
embedUrl: publicStandardUrl ?? relativePath,
embedUrl: materialized?.embedUrl ?? publicStandardUrl ?? relativePath,
allowedEmbedKeys: materialized?.assetKeys ?? collectEmbedAssetKeys([
publicStandardUrl,
relativePath,
rawUrl,
]),
});
} catch (err) {
console.warn('Vision image fetch skipped:', err instanceof Error ? err.message : err);
@@ -1009,6 +1027,11 @@ export async function buildVisionPayload({
const canonicalImageUrls = imageItems
.map((item) => item.embedUrl ?? item.rawUrl)
.filter((url) => typeof url === 'string' && url.trim());
const allowedPageImageEmbedKeys = [
...new Set(
imageItems.flatMap((item) => [...(item.allowedEmbedKeys ?? [])]),
),
];
return {
userMessage: detachCurrentTurnImagesForTextProvider(
@@ -1019,6 +1042,9 @@ export async function buildVisionPayload({
...(userMessage.metadata ?? {}),
...(originalDisplayText ? { displayText: originalDisplayText } : {}),
...(canonicalImageUrls.length ? { imageUrls: canonicalImageUrls } : {}),
...(allowedPageImageEmbedKeys.length
? { allowedPageImageEmbedKeys }
: {}),
},
},
canonicalImageUrls,
@@ -1618,11 +1644,12 @@ export function createTkmindProxy({
// Step 2 — Inject Qwen's text description + server-relative image paths into the
// user_message that goes to DeepSeek via Goose. DeepSeek retains full
// tool-calling capability (write_file, etc.) and creates the page properly.
async function buildVisionBody(userMessage, userId, publishLayout) {
async function buildVisionBody(userMessage, userId, publishLayout, publishDir = null) {
return buildVisionPayload({
userMessage,
userId,
publishLayout,
publishDir,
localFetchAsset,
llmProviderService,
imgproxySigner,
@@ -2039,7 +2066,13 @@ export function createTkmindProxy({
let finalUserMessage = userMessage;
if (llmProviderService && messageHasImages(userMessage) && await llmProviderService.hasVisionKey()) {
const publishLayout = await userAuth.getUserPublishLayout(userId).catch(() => null);
const visionResult = await buildVisionBody(userMessage, userId, publishLayout).catch(() => null);
const publishDir = await userAuth.resolveWorkingDir(userId).catch(() => null);
const visionResult = await buildVisionBody(
userMessage,
userId,
publishLayout,
publishDir,
).catch(() => null);
if (visionResult?.userMessage) {
finalUserMessage = visionResult.userMessage;
}