Merge branch 'feature/wechat-image-followup-poison-fix'
Memind CI / Test, build, and release guards (push) Failing after 3m29s
Memind CI / Test, build, and release guards (push) Failing after 3m29s
Fix WeChat image-then-text report follow-up image_url session poisoning.
This commit is contained in:
+14
-10
@@ -122,11 +122,10 @@ export function detachCurrentTurnImagesForTextProvider(message, canonicalImageUr
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove image attachments from a persisted user message so later turns cannot reuse them.
|
||||
* UI displayText / previewImageUrls are preserved for chat history rendering.
|
||||
* Remove image attachments from a persisted message so later turns cannot reuse them.
|
||||
*/
|
||||
export function scrubUserMessageImageAttachments(message) {
|
||||
if (!message || message.role !== 'user') return { message, changed: false };
|
||||
function scrubPersistedImageAttachments(message) {
|
||||
if (!message) return { message, changed: false };
|
||||
|
||||
const metadata =
|
||||
message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata)
|
||||
@@ -153,15 +152,13 @@ export function scrubUserMessageImageAttachments(message) {
|
||||
return null;
|
||||
}
|
||||
if (item?.type !== 'text' || typeof item.text !== 'string') return item;
|
||||
const nextText = stripAgentImageText(item.text);
|
||||
const nextText = message.role === 'user' ? stripAgentImageText(item.text) : item.text;
|
||||
if (nextText === item.text) return item;
|
||||
contentChanged = true;
|
||||
return nextText ? { ...item, text: nextText } : null;
|
||||
}).filter(Boolean)
|
||||
: message.content;
|
||||
|
||||
const displayText =
|
||||
typeof metadata.displayText === 'string' ? metadata.displayText : null;
|
||||
const changed = hadImageMetadata || contentChanged;
|
||||
if (!changed) return { message, changed: false };
|
||||
|
||||
@@ -170,12 +167,20 @@ export function scrubUserMessageImageAttachments(message) {
|
||||
...message,
|
||||
content,
|
||||
metadata,
|
||||
...(displayText != null ? {} : {}),
|
||||
},
|
||||
changed: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove image attachments from a persisted user message so later turns cannot reuse them.
|
||||
* UI displayText / previewImageUrls are preserved for chat history rendering.
|
||||
*/
|
||||
export function scrubUserMessageImageAttachments(message) {
|
||||
if (!message || message.role !== 'user') return { message, changed: false };
|
||||
return scrubPersistedImageAttachments(message);
|
||||
}
|
||||
|
||||
export function messageContentHasImageUrl(content) {
|
||||
if (!Array.isArray(content)) return false;
|
||||
return content.some((item) => item?.type === 'image_url' && item?.image_url?.url);
|
||||
@@ -203,9 +208,8 @@ export function scrubConversationHistoricalImageAttachments(conversation, active
|
||||
|
||||
let changed = false;
|
||||
const nextConversation = conversation.map((message) => {
|
||||
if (message?.role !== 'user') return message;
|
||||
if (String(message?.id ?? '').trim() === activeId) return message;
|
||||
const scrubbed = scrubUserMessageImageAttachments(message);
|
||||
const scrubbed = scrubPersistedImageAttachments(message);
|
||||
if (scrubbed.changed) changed = true;
|
||||
return scrubbed.message;
|
||||
});
|
||||
|
||||
@@ -103,6 +103,30 @@ test('scrubConversationHistoricalImageAttachments keeps only active turn attachm
|
||||
]);
|
||||
});
|
||||
|
||||
test('scrubConversationHistoricalImageAttachments removes assistant image_url parts', () => {
|
||||
const { conversation, changed } = scrubConversationHistoricalImageAttachments(
|
||||
[
|
||||
{
|
||||
id: 'assistant-old',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'text', text: '已识别报告' },
|
||||
{ type: 'image_url', image_url: { url: 'https://example.com/report.png' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'user-new',
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '解读报告,生成页面' }],
|
||||
},
|
||||
],
|
||||
'user-new',
|
||||
);
|
||||
|
||||
assert.equal(changed, true);
|
||||
assert.equal(conversation[0].content.some((item) => item.type === 'image_url'), false);
|
||||
});
|
||||
|
||||
test('buildCurrentTurnImageScopeNote states one independent topic per upload', () => {
|
||||
const note = buildCurrentTurnImageScopeNote([
|
||||
{
|
||||
|
||||
@@ -2009,6 +2009,17 @@ export function createTkmindProxy({
|
||||
error.code = 'HISTORICAL_IMAGE_SESSION_UPDATE_UNSUPPORTED';
|
||||
throw error;
|
||||
}
|
||||
if (
|
||||
requireHistoricalImageIsolation
|
||||
&& imageIsolation.hasImageUrlContent
|
||||
&& !imageIsolation.updated
|
||||
) {
|
||||
const error = new Error(
|
||||
`historical_image_session_update_unsupported:${imageIsolation.status ?? 'image_url_content_present'}`,
|
||||
);
|
||||
error.code = 'HISTORICAL_IMAGE_SESSION_UPDATE_UNSUPPORTED';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
let finalUserMessage = userMessage;
|
||||
if (llmProviderService && messageHasImages(userMessage) && await llmProviderService.hasVisionKey()) {
|
||||
|
||||
@@ -1569,7 +1569,7 @@ test('submitSessionReplyForUser rotates when assistant history still has image_u
|
||||
},
|
||||
{ requireHistoricalImageIsolation: true },
|
||||
),
|
||||
/historical_image_session_update_unsupported:image_url_content_present/,
|
||||
/historical_image_session_update_unsupported:(405|image_url_content_present)/,
|
||||
);
|
||||
assert.equal(replyBodies.length, 0);
|
||||
}, {
|
||||
|
||||
@@ -1094,6 +1094,25 @@ export function isWechatHistoricalImageSessionError(message) {
|
||||
);
|
||||
}
|
||||
|
||||
export const WECHAT_IMAGE_STORED_ACK_TEXT =
|
||||
'已收到图片。请直接告诉我接下来要做什么,例如:解读报告并生成页面。';
|
||||
|
||||
export function prepareWechatIntentForHistoricalImageRetry(intent) {
|
||||
if (!intent || typeof intent !== 'object') return intent;
|
||||
const imageUrl = String(intent.media?.publicUrl ?? '').trim();
|
||||
const agentText = String(intent.agentText ?? '').trim();
|
||||
if (imageUrl && !agentText.includes(imageUrl)) {
|
||||
intent.agentText = agentText
|
||||
? `${agentText}\n\n[附件图片]: ${imageUrl}`
|
||||
: `[附件图片]: ${imageUrl}`;
|
||||
}
|
||||
delete intent.media;
|
||||
delete intent.attachment;
|
||||
delete intent.recentMediaItems;
|
||||
delete intent.recentMediaBatchId;
|
||||
return intent;
|
||||
}
|
||||
|
||||
export function isRecoverableWechatAgentSessionError(message) {
|
||||
const normalized = String(message ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
@@ -3157,6 +3176,9 @@ export function createWechatMpService({
|
||||
sessionId = route.sessionId;
|
||||
await ensureSessionProvider(sessionId);
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
if (historicalImageError) {
|
||||
prepareWechatIntentForHistoricalImageRetry(intent);
|
||||
}
|
||||
const retryId = crypto.randomUUID();
|
||||
const retryStartedAt = Date.now();
|
||||
const retryPageContinuation = sessionPageContinuation && !historicalImageError;
|
||||
@@ -3683,6 +3705,22 @@ export function createWechatMpService({
|
||||
};
|
||||
intent.agentText = `[图片1]: ${persisted.publicUrl}`;
|
||||
rememberRecentMedia(inbound.fromUserName, intent);
|
||||
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
|
||||
if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') {
|
||||
await userAuth.finishWechatMpMessage({
|
||||
appId: config.appId,
|
||||
openid: inbound.fromUserName,
|
||||
msgId: inbound.msgId,
|
||||
status: 'done',
|
||||
agentSessionId: null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
contentType: 'application/xml; charset=utf-8',
|
||||
body: await buildPassiveReplyBody(WECHAT_IMAGE_STORED_ACK_TEXT),
|
||||
};
|
||||
} catch (error) {
|
||||
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
|
||||
return {
|
||||
|
||||
+59
-116
@@ -14,6 +14,8 @@ import {
|
||||
isRecoverableWechatAgentSessionError,
|
||||
isWechatAgentApiErrorText,
|
||||
isWechatHistoricalImageSessionError,
|
||||
WECHAT_IMAGE_STORED_ACK_TEXT,
|
||||
prepareWechatIntentForHistoricalImageRetry,
|
||||
sanitizeWechatAgentOutboundText,
|
||||
loadWechatMpConfig,
|
||||
maybeAttachPublishedHtmlLink,
|
||||
@@ -3498,6 +3500,19 @@ test('isRecoverableWechatAgentSessionError detects poisoned tool_calls history',
|
||||
assert.equal(isRecoverableWechatAgentSessionError('network timeout'), false);
|
||||
});
|
||||
|
||||
test('prepareWechatIntentForHistoricalImageRetry keeps image url in text only', () => {
|
||||
const intent = prepareWechatIntentForHistoricalImageRetry({
|
||||
agentText: '解读报告,生成页面',
|
||||
media: { publicUrl: 'https://example.com/report.png' },
|
||||
recentMediaItems: [{ media: { publicUrl: 'https://example.com/report.png' } }],
|
||||
recentMediaBatchId: 'batch-1',
|
||||
});
|
||||
assert.match(intent.agentText, /https:\/\/example\.com\/report\.png/);
|
||||
assert.equal(intent.media, undefined);
|
||||
assert.equal(intent.recentMediaItems, undefined);
|
||||
assert.equal(intent.recentMediaBatchId, undefined);
|
||||
});
|
||||
|
||||
test('wechat mp rotates and retries when historical image isolation is unsupported', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
@@ -4917,7 +4932,7 @@ test('wechat mp wildcard media access persists image and routes image url into a
|
||||
const originalRandomUuid = crypto.randomUUID;
|
||||
crypto.randomUUID = () => 'req-image';
|
||||
try {
|
||||
const result = await service.handleInboundMessage(
|
||||
const imageResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'image',
|
||||
content: '',
|
||||
@@ -4929,9 +4944,15 @@ test('wechat mp wildcard media access persists image and routes image url into a
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
},
|
||||
);
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /<Content>/);
|
||||
await result.task;
|
||||
assert.equal(imageResult.status, 200);
|
||||
assert.match(imageResult.body ?? '', /已收到图片/);
|
||||
assert.equal(prompts.length, 0);
|
||||
|
||||
const followupResult = await service.handleInboundMessage(
|
||||
inboundXml({ msgType: 'text', content: '请分析刚才图片' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await followupResult.task;
|
||||
} finally {
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), {
|
||||
@@ -4941,17 +4962,13 @@ test('wechat mp wildcard media access persists image and routes image url into a
|
||||
}
|
||||
|
||||
assert.equal(prompts.length, 1);
|
||||
assert.match(prompts[0], /【微信服务号图片消息】/);
|
||||
assert.match(
|
||||
prompts[0],
|
||||
/\[图片1\]: https:\/\/example\.com\/MindSpace\/test-user-image\/public\/wechat-mp\//,
|
||||
);
|
||||
assert.match(prompts[0], /请分析刚才图片/);
|
||||
assert.equal(metadataCalls.length, 1);
|
||||
assert.equal(metadataCalls[0].source, 'wechat_mp');
|
||||
assert.equal(metadataCalls[0].msgType, 'image');
|
||||
assert.equal(metadataCalls[0].msgType, 'text');
|
||||
assert.equal(metadataCalls[0].imageUrls.length, 1);
|
||||
assert.match(metadataCalls[0].imageUrls[0], /\/public\/wechat-mp\//);
|
||||
assert.equal(detailCalls.length, 1);
|
||||
assert.equal(detailCalls.length, 2);
|
||||
assert.match(detailCalls[0].mediaPublicUrl, /\/wechat-mp\//);
|
||||
});
|
||||
|
||||
@@ -5014,7 +5031,7 @@ test('wechat mp image submission reuses the H5 prepared reply path', async () =>
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await service.handleInboundMessage(
|
||||
const imageResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'image',
|
||||
content: '',
|
||||
@@ -5022,7 +5039,14 @@ test('wechat mp image submission reuses the H5 prepared reply path', async () =>
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await result.task;
|
||||
assert.match(imageResult.body ?? '', /已收到图片/);
|
||||
assert.equal(submitCalls.length, 0);
|
||||
|
||||
const followupResult = await service.handleInboundMessage(
|
||||
inboundXml({ msgType: 'text', content: '请分析刚才图片' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await followupResult.task;
|
||||
} finally {
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), { recursive: true, force: true });
|
||||
}
|
||||
@@ -5030,6 +5054,7 @@ test('wechat mp image submission reuses the H5 prepared reply path', async () =>
|
||||
assert.equal(submitCalls.length, 1);
|
||||
assert.equal(submitCalls[0].userId, testUserId);
|
||||
assert.equal(submitCalls[0].sessionId, 'session-1');
|
||||
assert.equal(submitCalls[0].options?.requireHistoricalImageIsolation, true);
|
||||
assert.equal(submitCalls[0].userMessage.metadata.imageUrls.length, 1);
|
||||
assert.match(submitCalls[0].userMessage.metadata.imageUrls[0], /\/public\/wechat-mp\//);
|
||||
});
|
||||
@@ -5337,8 +5362,6 @@ test('wechat mp serializes image and follow-up text and reattaches recent image'
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-image-followup';
|
||||
const submitCalls = [];
|
||||
let eventCall = 0;
|
||||
let releaseFirst = null;
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
config: {
|
||||
@@ -5351,25 +5374,6 @@ test('wechat mp serializes image and follow-up text and reattaches recent image'
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname) => {
|
||||
if (pathname === '/sessions/session-1/events') {
|
||||
eventCall += 1;
|
||||
if (eventCall === 1) {
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
releaseFirst = () => {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"Message","message":{"id":"assistant-image","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"图片已识别。"}]}}\n\n' +
|
||||
'data: {"type":"Finish"}\n\n',
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
};
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","message":{"id":"assistant-followup","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已结合刚才图片分析主题。"}]}}\n\n',
|
||||
@@ -5419,28 +5423,21 @@ test('wechat mp serializes image and follow-up text and reattaches recent image'
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
while (submitCalls.length === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(submitCalls.length, 0);
|
||||
assert.match(imageResult.body ?? '', new RegExp(WECHAT_IMAGE_STORED_ACK_TEXT.slice(0, 8)));
|
||||
|
||||
const followupResult = await service.handleInboundMessage(
|
||||
inboundXml({ msgType: 'text', content: '请根据刚才图片分析主题' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(submitCalls.length, 1);
|
||||
|
||||
releaseFirst();
|
||||
await imageResult.task;
|
||||
await followupResult.task;
|
||||
} finally {
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
assert.equal(submitCalls.length, 2);
|
||||
assert.deepEqual(
|
||||
submitCalls[1].userMessage.metadata.imageUrls,
|
||||
submitCalls[0].userMessage.metadata.imageUrls,
|
||||
);
|
||||
assert.equal(submitCalls[1].userMessage.metadata.msgType, 'text');
|
||||
assert.equal(submitCalls.length, 1);
|
||||
assert.ok(Array.isArray(submitCalls[0].userMessage.metadata.imageUrls));
|
||||
assert.equal(submitCalls[0].userMessage.metadata.msgType, 'text');
|
||||
});
|
||||
|
||||
test('wechat mp reattaches recent image for report interpretation follow-up', async () => {
|
||||
@@ -5449,8 +5446,6 @@ test('wechat mp reattaches recent image for report interpretation follow-up', as
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-report-followup';
|
||||
const submitCalls = [];
|
||||
let eventCall = 0;
|
||||
let releaseFirst = null;
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
config: {
|
||||
@@ -5463,25 +5458,6 @@ test('wechat mp reattaches recent image for report interpretation follow-up', as
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname) => {
|
||||
if (pathname === '/sessions/session-1/events') {
|
||||
eventCall += 1;
|
||||
if (eventCall === 1) {
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
releaseFirst = () => {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"Message","message":{"id":"assistant-image","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"图片已识别。"}]}}\n\n' +
|
||||
'data: {"type":"Finish"}\n\n',
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
};
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","message":{"id":"assistant-followup","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已解读报告。"}]}}\n\n',
|
||||
@@ -5531,29 +5507,22 @@ test('wechat mp reattaches recent image for report interpretation follow-up', as
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
while (submitCalls.length === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(submitCalls.length, 0);
|
||||
assert.match(imageResult.body ?? '', /已收到图片/);
|
||||
|
||||
const followupResult = await service.handleInboundMessage(
|
||||
inboundXml({ msgType: 'text', content: '解读详细报告,做成页面' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(submitCalls.length, 1);
|
||||
|
||||
releaseFirst();
|
||||
await imageResult.task;
|
||||
await followupResult.task;
|
||||
} finally {
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
assert.equal(submitCalls.length, 2);
|
||||
assert.deepEqual(
|
||||
submitCalls[1].userMessage.metadata.imageUrls,
|
||||
submitCalls[0].userMessage.metadata.imageUrls,
|
||||
);
|
||||
assert.equal(submitCalls[1].userMessage.metadata.msgType, 'text');
|
||||
assert.equal(submitCalls[1].userMessage.metadata.displayText, '解读详细报告,做成页面');
|
||||
assert.equal(submitCalls.length, 1);
|
||||
assert.ok(Array.isArray(submitCalls[0].userMessage.metadata.imageUrls));
|
||||
assert.equal(submitCalls[0].userMessage.metadata.msgType, 'text');
|
||||
assert.equal(submitCalls[0].userMessage.metadata.displayText, '解读详细报告,做成页面');
|
||||
});
|
||||
|
||||
test('wechat mp groups consecutive images for one follow-up and clears the consumed batch', async () => {
|
||||
@@ -5562,8 +5531,6 @@ test('wechat mp groups consecutive images for one follow-up and clears the consu
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-multi-image-followup';
|
||||
const submitCalls = [];
|
||||
let eventCall = 0;
|
||||
let releaseFirst = null;
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
config: {
|
||||
@@ -5576,25 +5543,6 @@ test('wechat mp groups consecutive images for one follow-up and clears the consu
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname) => {
|
||||
if (pathname === '/sessions/session-1/events') {
|
||||
eventCall += 1;
|
||||
if (eventCall === 1) {
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
releaseFirst = () => {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"Message","message":{"id":"assistant-first","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"第一张处理完成。"}]}}\n\n' +
|
||||
'data: {"type":"Finish"}\n\n',
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
};
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","message":{"id":"assistant-ok","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"处理完成。"}]}}\n\n',
|
||||
@@ -5648,7 +5596,8 @@ test('wechat mp groups consecutive images for one follow-up and clears the consu
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
while (submitCalls.length === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(submitCalls.length, 0);
|
||||
assert.match(firstImageResult.body ?? '', /已收到图片/);
|
||||
|
||||
const secondImageResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
@@ -5662,6 +5611,8 @@ test('wechat mp groups consecutive images for one follow-up and clears the consu
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
assert.equal(submitCalls.length, 0);
|
||||
assert.match(secondImageResult.body ?? '', /已收到图片/);
|
||||
|
||||
const followupResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
@@ -5670,12 +5621,6 @@ test('wechat mp groups consecutive images for one follow-up and clears the consu
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(submitCalls.length, 1);
|
||||
|
||||
releaseFirst();
|
||||
await firstImageResult.task;
|
||||
await secondImageResult.task;
|
||||
await followupResult.task;
|
||||
|
||||
const laterResult = await service.handleInboundMessage(
|
||||
@@ -5690,14 +5635,12 @@ test('wechat mp groups consecutive images for one follow-up and clears the consu
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
assert.equal(submitCalls.length, 4);
|
||||
assert.equal(submitCalls[0].userMessage.metadata.imageUrls.length, 1);
|
||||
assert.equal(submitCalls[1].userMessage.metadata.imageUrls.length, 1);
|
||||
assert.equal(submitCalls[2].userMessage.metadata.imageUrls.length, 2);
|
||||
assert.match(submitCalls[2].userMessage.metadata.imageUrls[0], /media-first/);
|
||||
assert.match(submitCalls[2].userMessage.metadata.imageUrls[1], /media-second/);
|
||||
assert.equal(submitCalls[2].userMessage.metadata.msgType, 'text');
|
||||
assert.equal(submitCalls[3].userMessage.metadata.imageUrls, undefined);
|
||||
assert.equal(submitCalls.length, 2);
|
||||
assert.equal(submitCalls[0].userMessage.metadata.imageUrls.length, 2);
|
||||
assert.match(submitCalls[0].userMessage.metadata.imageUrls[0], /media-first/);
|
||||
assert.match(submitCalls[0].userMessage.metadata.imageUrls[1], /media-second/);
|
||||
assert.equal(submitCalls[0].userMessage.metadata.msgType, 'text');
|
||||
assert.equal(submitCalls[1].userMessage.metadata.imageUrls, undefined);
|
||||
});
|
||||
|
||||
test('wechat mp service persists Word and Excel files in user public area and reuses H5 attachment metadata', async () => {
|
||||
|
||||
Reference in New Issue
Block a user