fix(wechat): isolate stale images and page links
Memind CI / Test, build, and release guards (pull_request) Successful in 2m36s
Memind CI / Test, build, and release guards (pull_request) Successful in 2m36s
This commit is contained in:
+79
-25
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user