fix(wechat): isolate stale images and page links
Memind CI / Test, build, and release guards (pull_request) Successful in 2m36s

This commit is contained in:
john
2026-07-22 13:30:44 +08:00
parent aea3c1e83e
commit db225b784e
11 changed files with 416 additions and 37 deletions
+79 -25
View File
@@ -271,6 +271,7 @@ async function executeSessionReply(
const decoder = new TextDecoder();
let buffer = '';
let messages = [];
let requestMessages = [];
let hasScopedAssistantUpdate = false;
while (true) {
@@ -301,6 +302,7 @@ async function executeSessionReply(
}
if (event.message.role === 'assistant') hasScopedAssistantUpdate = true;
messages = pushMessage(messages, event.message);
requestMessages = pushMessage(requestMessages, event.message);
} else if (event.type === 'UpdateConversation') {
// Ignore unscoped snapshots until this request has yielded an assistant update.
// Otherwise a stale session snapshot can overwrite the current reply with a
@@ -319,6 +321,10 @@ async function executeSessionReply(
text: messageVisibleText(assistant),
tokenState: event.token_state ?? null,
messages,
// Keep request-scoped stream messages separate from a later full
// UpdateConversation snapshot. Artifact delivery must never inspect
// historical tool calls from the whole dedicated session.
requestMessages,
};
assertWechatAgentReplyIsSendable(reply);
return reply;
@@ -416,6 +422,10 @@ function looksLikeHtmlGenerationIntent(text) {
return isPageGenerateText(text);
}
function replyRequestMessages(reply) {
return reply?.requestMessages ?? reply?.messages ?? [];
}
function looksLikeDocxDownloadIntent(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
@@ -449,8 +459,8 @@ function hasAnyToolRequest(messages = []) {
function isSuspiciousBareCompletionReply(reply, intent) {
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
if (!isBareCompletionText(reply?.text)) return false;
if (extractHtmlWriteTargets(reply?.messages ?? []).length > 0) return false;
return !hasAnyToolRequest(reply?.messages ?? []);
if (extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0) return false;
return !hasAnyToolRequest(replyRequestMessages(reply));
}
function looksLikePublishSuccessClaim(text) {
@@ -485,15 +495,15 @@ async function isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists = d
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
const text = String(reply?.text ?? '').trim();
if (!looksLikePublishSuccessClaim(text)) return false;
if (extractHtmlWriteTargets(reply?.messages ?? []).length > 0) return false;
if (extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0) return false;
return !(await hasAnyValidPublishedHtmlLink(text, linkExists));
}
function isMissingRequiredPublishSkill(reply, intent) {
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
const wroteHtml = extractHtmlWriteTargets(reply?.messages ?? []).length > 0;
const wroteHtml = extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0;
if (!wroteHtml) return false;
return !usedStaticPagePublishSkill(reply?.messages ?? []);
return !usedStaticPagePublishSkill(replyRequestMessages(reply));
}
function buildPublicHtmlUrl(workingDir, relativePath, publicBaseUrl) {
@@ -528,7 +538,7 @@ function ensurePublicHtmlArtifact(htmlPath, workingDir) {
function collectPublishedHtmlArtifacts(reply, { workingDir, publicBaseUrl }) {
const artifacts = [];
const htmlTargets = extractHtmlWriteTargets(reply?.messages ?? []);
const htmlTargets = extractHtmlWriteTargets(replyRequestMessages(reply));
for (const target of htmlTargets) {
const artifact = ensurePublicHtmlArtifact(target, workingDir);
if (!artifact) continue;
@@ -674,7 +684,16 @@ function rewritePublishedHtmlLinks(text, artifacts = []) {
return next;
}
async function maybeAttachPublishedHtmlLink(reply, { workingDir, publicBaseUrl, artifacts: providedArtifacts = null }) {
async function maybeAttachPublishedHtmlLink(
reply,
{
workingDir,
publicBaseUrl,
artifacts: providedArtifacts = null,
allowAttachment = true,
},
) {
if (!allowAttachment) return String(reply?.text ?? '').trim();
const artifacts = Array.isArray(providedArtifacts)
? providedArtifacts
: collectPublishedHtmlArtifacts(reply, { workingDir, publicBaseUrl });
@@ -938,12 +957,15 @@ function resolveHtmlPublishArtifacts({
userId = '',
sessionId = '',
onPageGenerated = null,
allowRecentArtifacts = true,
}) {
const artifactMessages = reply?.requestMessages ?? reply?.messages ?? [];
const artifactReply = { ...reply, messages: artifactMessages };
materializeMissingPublicHtmlWrites({
messages: reply?.messages ?? [],
messages: artifactMessages,
publishDir: workingDir,
});
const publishedArtifacts = collectPublishedHtmlArtifacts(reply, {
const publishedArtifacts = collectPublishedHtmlArtifacts(artifactReply, {
workingDir,
publicBaseUrl,
});
@@ -951,12 +973,14 @@ function resolveHtmlPublishArtifacts({
workingDir,
publicBaseUrl,
});
const recentArtifacts = collectRecentPublishedHtmlArtifacts(intent, {
workingDir,
publicBaseUrl,
replyText: reply?.text,
sinceMs: requestStartedAt,
});
const recentArtifacts = allowRecentArtifacts
? collectRecentPublishedHtmlArtifacts(intent, {
workingDir,
publicBaseUrl,
replyText: reply?.text,
sinceMs: requestStartedAt,
})
: [];
const confirmedArtifacts = allExistingHtmlArtifacts({
publishedArtifacts,
expectedArtifacts,
@@ -968,7 +992,11 @@ function resolveHtmlPublishArtifacts({
recentArtifacts,
replyText: reply?.text,
});
if (typeof onPageGenerated === 'function' && confirmedArtifacts.length > 0) {
if (
typeof onPageGenerated === 'function'
&& confirmedArtifacts.length > 0
&& (allowRecentArtifacts || publishedArtifacts.length > 0)
) {
void onPageGenerated({ userId, sessionId, artifacts: confirmedArtifacts });
}
return {
@@ -1019,7 +1047,7 @@ export function shouldRetryHtmlGenerationReply({
if (isMissingRequiredPublishSkill(reply, intent) || isSuspiciousBareCompletionReply(reply, intent)) {
return true;
}
if (!usedStaticPagePublishSkill(reply?.messages ?? [])) return false;
if (!usedStaticPagePublishSkill(replyRequestMessages(reply))) return false;
return !hasAnyUrl(reply?.text);
}
@@ -1042,11 +1070,22 @@ export function isRecoverableWechatAgentSessionError(message) {
if (/wechat_page_fresh_thumbnail_required:/i.test(normalized)) return true;
if (/403|404|not found|无权访问/i.test(normalized)) return true;
if (/tool_calls|tool_call_id|insufficient tool messages/i.test(normalized)) return true;
if (/historical_image_session_update_unsupported|unknown variant [`']?image_url/i.test(normalized)) {
return true;
}
if (/session already has an active request|active request.*cancel/i.test(normalized)) return true;
if (/wechat_agent_incomplete_reply/i.test(normalized)) return true;
return false;
}
export function shouldDeliverWechatHtmlArtifacts(wechatIntent, intent) {
return (
wechatIntent?.kind === 'page.generate'
|| looksLikeHtmlGenerationIntent(intent?.agentText)
|| isPageDataIntent(intent?.agentText)
);
}
function collectWechatAgentReplyVisibleTexts(reply) {
const texts = [];
const seen = new Set();
@@ -1057,7 +1096,7 @@ function collectWechatAgentReplyVisibleTexts(reply) {
texts.push(normalized);
};
append(reply?.text);
for (const message of reply?.messages ?? []) {
for (const message of replyRequestMessages(reply)) {
if (message?.role !== 'assistant') continue;
append(messageVisibleText(message));
}
@@ -1856,7 +1895,7 @@ export function createWechatMpService({
notifyFailure = true,
}) => {
if (imagePolicy?.pageThumbnailMode !== WECHAT_PAGE_THUMBNAIL_MODE.REQUIRED_FRESH) return;
const images = collectWechatGeneratedImages(reply?.messages ?? []);
const images = collectWechatGeneratedImages(replyRequestMessages(reply));
let verification = verifyFreshWechatPageThumbnails(artifacts, images);
if (!verification.ok) {
logger.warn?.(
@@ -1867,7 +1906,7 @@ export function createWechatMpService({
const repair = repairUnambiguousFreshWechatPageThumbnail({
artifacts,
images,
currentRunHtmlArtifacts: extractPublicHtmlWriteArtifacts(reply?.messages ?? [], {
currentRunHtmlArtifacts: extractPublicHtmlWriteArtifacts(replyRequestMessages(reply), {
publishDir,
}),
verificationReason: verification.reason,
@@ -2293,6 +2332,7 @@ export function createWechatMpService({
// policies. Reusing a conversational route here can make a new request
// inspect/retry unrelated historical pages from that session.
const isPageDataRequest = isPageDataIntent(resetCandidate);
const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent);
const forceNew = shouldForceNewWechatAgentSession(wechatIntent, resetCandidate);
let route = await ensureWechatAgentSession({
userId: user.userId,
@@ -2353,11 +2393,12 @@ export function createWechatMpService({
sessionId,
requestId: replyRequestId,
userMessage,
options: { requireHistoricalImageIsolation: true },
})
: null,
},
);
const generatedImages = collectWechatGeneratedImages(reply?.messages ?? []);
const generatedImages = collectWechatGeneratedImages(replyRequestMessages(reply));
if (imagePolicy.standaloneImageMode === 'required' && generatedImages.length === 0) {
const error = new Error('图片生成没有获得本轮新的有效位图,请稍后重试');
error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED';
@@ -2380,6 +2421,7 @@ export function createWechatMpService({
userId: user.userId,
sessionId,
onPageGenerated,
allowRecentArtifacts: htmlArtifactDeliveryExpected,
});
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
confirmedArtifacts,
@@ -2396,7 +2438,10 @@ export function createWechatMpService({
hasValidLinkInReply,
});
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
let publishArtifacts = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
let publishArtifacts =
htmlArtifactDeliveryExpected || publishedArtifacts.length > 0
? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts })
: [];
if (wechatIntent.kind === 'page.generate') {
const pageOutcome = resolvePageGenerateOutcome({
@@ -2482,6 +2527,7 @@ export function createWechatMpService({
workingDir,
publicBaseUrl: config.publicBaseUrl,
artifacts: publishArtifacts,
allowAttachment: publishArtifacts.length > 0,
});
if (
wechatIntent.kind !== 'page.generate'
@@ -2521,7 +2567,9 @@ export function createWechatMpService({
}
throw markWechatUserNotified(err instanceof Error ? err : new Error(message));
}
const mayBeStaleSession = sessionId && isRecoverableWechatAgentSessionError(message);
const mayBeStaleSession =
sessionId
&& (isRecoverableWechatAgentSessionError(message) || isWechatAgentApiErrorText(message));
if (mayBeStaleSession) {
route = await ensureWechatAgentSession({
userId: user.userId,
@@ -2560,11 +2608,12 @@ export function createWechatMpService({
sessionId,
requestId: replyRequestId,
userMessage,
options: { requireHistoricalImageIsolation: true },
})
: null,
},
);
const generatedImages = collectWechatGeneratedImages(reply?.messages ?? []);
const generatedImages = collectWechatGeneratedImages(replyRequestMessages(reply));
if (imagePolicy.standaloneImageMode === 'required' && generatedImages.length === 0) {
const error = new Error('图片生成没有获得本轮新的有效位图,请稍后重试');
error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED';
@@ -2587,6 +2636,7 @@ export function createWechatMpService({
userId: user.userId,
sessionId,
onPageGenerated,
allowRecentArtifacts: htmlArtifactDeliveryExpected,
});
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
confirmedArtifacts,
@@ -2603,7 +2653,10 @@ export function createWechatMpService({
hasValidLinkInReply,
});
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
let publishArtifacts = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
let publishArtifacts =
htmlArtifactDeliveryExpected || publishedArtifacts.length > 0
? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts })
: [];
if (wechatIntent.kind === 'page.generate') {
const pageOutcome = resolvePageGenerateOutcome({
@@ -2685,6 +2738,7 @@ export function createWechatMpService({
workingDir,
publicBaseUrl: config.publicBaseUrl,
artifacts: publishArtifacts,
allowAttachment: publishArtifacts.length > 0,
});
if (
wechatIntent.kind !== 'page.generate'