Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b58cdc2c8 | |||
| 9f7633be24 | |||
| fbd68a36e9 | |||
| be8ce867ca | |||
| 67b9478155 | |||
| 4c2bc95462 |
+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([
|
||||
{
|
||||
|
||||
@@ -129,22 +129,16 @@ test('production release rejects --skip-tests before repository or network prefl
|
||||
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /103 只读预检/);
|
||||
});
|
||||
|
||||
test('fast production release allows --skip-tests but still requires check-release-ready', () => {
|
||||
const result = spawnSync(
|
||||
'bash',
|
||||
[
|
||||
path.join(ROOT, 'scripts', 'release-portal-runtime-prod.sh'),
|
||||
'--mode',
|
||||
'fast',
|
||||
'--skip-tests',
|
||||
'--skip-build',
|
||||
'--dry-run',
|
||||
],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
test('fast production release allows --skip-tests but still requires check-release-ready', async () => {
|
||||
const source = await fs.readFile(
|
||||
path.join(ROOT, 'scripts', 'release-portal-runtime-prod.sh'),
|
||||
'utf8',
|
||||
);
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.doesNotMatch(result.stderr, /标准发布禁止 --skip-tests/);
|
||||
assert.match(`${result.stdout}\n${result.stderr}`, /快速发布:跳过额外本地测试/);
|
||||
assert.match(source, /ALLOW_MAIN_RELEASE=1 bash "\$\{ROOT\}\/scripts\/check-release-ready\.sh"/);
|
||||
assert.match(source, /快速发布:跳过额外本地测试/);
|
||||
const skipTestsBlockEnd = source.indexOf('fi\n\nif [[ "${DRY_RUN}" -eq 0 ]]; then');
|
||||
const checkReleaseIdx = source.indexOf('check-release-ready.sh');
|
||||
assert.ok(skipTestsBlockEnd >= 0 && checkReleaseIdx > skipTestsBlockEnd);
|
||||
});
|
||||
|
||||
test('fast release wrapper delegates to runtime prod script with --mode fast', async () => {
|
||||
|
||||
@@ -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);
|
||||
}, {
|
||||
|
||||
+126
-3
@@ -85,6 +85,7 @@ import { resolveMindSpaceUserPublishDir } from './mindspace-runtime-config.mjs';
|
||||
import {
|
||||
buildPageDataCollectFailureText,
|
||||
} from './mindspace-page-data-finish-guard.mjs';
|
||||
import { conversationHasImageUrlContent } from './chat-image-turn-scope.mjs';
|
||||
|
||||
export { buildWechatAgentPrompt };
|
||||
|
||||
@@ -1094,6 +1095,30 @@ export function isWechatHistoricalImageSessionError(message) {
|
||||
);
|
||||
}
|
||||
|
||||
export const WECHAT_IMAGE_STORED_ACK_TEXT =
|
||||
'已收到图片。请直接告诉我接下来要做什么,例如:解读报告并生成页面。';
|
||||
|
||||
export function resolveWechatRecentMediaPublicUrl(recentMediaEntry) {
|
||||
if (!recentMediaEntry?.items?.length) return '';
|
||||
return String(recentMediaEntry.items.at(-1)?.media?.publicUrl ?? '').trim();
|
||||
}
|
||||
|
||||
export function prepareWechatIntentForHistoricalImageRetry(intent, { fallbackImageUrl = '' } = {}) {
|
||||
if (!intent || typeof intent !== 'object') return intent;
|
||||
const imageUrl = String(intent.media?.publicUrl ?? fallbackImageUrl ?? '').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;
|
||||
@@ -2166,15 +2191,23 @@ export function createWechatMpService({
|
||||
retainUnlinkedRoute = false,
|
||||
userContext = null,
|
||||
}) => {
|
||||
if (forceNew) {
|
||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||
}
|
||||
const workingDir = await userAuth.resolveWorkingDir(userId);
|
||||
let sessionPolicy =
|
||||
await userAuth.getAgentSessionPolicy(userId);
|
||||
const publishLayout = await userAuth.getUserPublishLayout(userId);
|
||||
const addressName = resolveWechatAddressName(userContext);
|
||||
let carriedSessionContentForNewSession = '';
|
||||
if (forceNew) {
|
||||
const routeBeforeForceNew = await userAuth.getWechatAgentRoute(config.appId, openid);
|
||||
if (routeBeforeForceNew?.agentSessionId) {
|
||||
carriedSessionContentForNewSession = await fetchLastSubstantiveAssistantFromSession(
|
||||
fetchForSession,
|
||||
routeBeforeForceNew.agentSessionId,
|
||||
);
|
||||
rememberedWechatContexts.delete(routeBeforeForceNew.agentSessionId);
|
||||
}
|
||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||
}
|
||||
const existingRoute = await userAuth.getWechatAgentRoute(config.appId, openid);
|
||||
if (existingRoute?.agentSessionId) {
|
||||
const now = Date.now();
|
||||
@@ -2386,6 +2419,53 @@ export function createWechatMpService({
|
||||
};
|
||||
};
|
||||
|
||||
const rotateWechatSessionIfImagePolluted = async ({
|
||||
userId,
|
||||
openid,
|
||||
sessionId,
|
||||
user,
|
||||
carriedSessionContent = '',
|
||||
}) => {
|
||||
if (!sessionId) {
|
||||
return { sessionId, carriedSessionContent, rotated: false };
|
||||
}
|
||||
try {
|
||||
const response = await fetchForSession(
|
||||
sessionId,
|
||||
`/sessions/${encodeURIComponent(sessionId)}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
return { sessionId, carriedSessionContent, rotated: false };
|
||||
}
|
||||
const payload = await readJsonResponse(response);
|
||||
const conversation = Array.isArray(payload?.conversation) ? payload.conversation : [];
|
||||
if (!conversationHasImageUrlContent(conversation)) {
|
||||
return { sessionId, carriedSessionContent, rotated: false };
|
||||
}
|
||||
logger.warn?.('WeChat MP rotating image-polluted agent session before reply:', {
|
||||
agentSessionId: sessionId,
|
||||
conversationLength: conversation.length,
|
||||
});
|
||||
const nextRoute = await ensureWechatAgentSession({
|
||||
userId,
|
||||
openid,
|
||||
forceNew: true,
|
||||
userContext: user,
|
||||
});
|
||||
const nextCarried = String(carriedSessionContent ?? '').trim()
|
||||
|| String(nextRoute.carriedSessionContent ?? '').trim();
|
||||
return {
|
||||
sessionId: nextRoute.sessionId,
|
||||
carriedSessionContent: nextCarried,
|
||||
rotated: true,
|
||||
isNewSession: nextRoute.isNewSession,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.warn?.('WeChat MP image-polluted session rotate skipped:', err);
|
||||
return { sessionId, carriedSessionContent, rotated: false };
|
||||
}
|
||||
};
|
||||
|
||||
const refreshWechatSessionSnapshot = async (sessionId, userId) => {
|
||||
if (!sessionId || !userId || typeof refreshSessionSnapshot !== 'function') return;
|
||||
try {
|
||||
@@ -2693,6 +2773,26 @@ export function createWechatMpService({
|
||||
return { sessionId };
|
||||
}
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
const pollutionRotation = await rotateWechatSessionIfImagePolluted({
|
||||
userId: user.userId,
|
||||
openid: inbound.fromUserName,
|
||||
sessionId,
|
||||
user,
|
||||
carriedSessionContent,
|
||||
});
|
||||
if (pollutionRotation.rotated) {
|
||||
sessionId = pollutionRotation.sessionId;
|
||||
route = {
|
||||
...route,
|
||||
sessionId,
|
||||
isNewSession: pollutionRotation.isNewSession ?? true,
|
||||
};
|
||||
if (pollutionRotation.carriedSessionContent) {
|
||||
carriedSessionContent = pollutionRotation.carriedSessionContent;
|
||||
}
|
||||
await ensureSessionProvider(sessionId);
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: true });
|
||||
}
|
||||
if (
|
||||
wechatIntent.kind === 'page.generate'
|
||||
&& sessionPageContinuation
|
||||
@@ -3157,6 +3257,13 @@ export function createWechatMpService({
|
||||
sessionId = route.sessionId;
|
||||
await ensureSessionProvider(sessionId);
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
if (historicalImageError) {
|
||||
prepareWechatIntentForHistoricalImageRetry(intent, {
|
||||
fallbackImageUrl: resolveWechatRecentMediaPublicUrl(
|
||||
recentMediaByOpenid.get(String(inbound.fromUserName ?? '').trim()),
|
||||
),
|
||||
});
|
||||
}
|
||||
const retryId = crypto.randomUUID();
|
||||
const retryStartedAt = Date.now();
|
||||
const retryPageContinuation = sessionPageContinuation && !historicalImageError;
|
||||
@@ -3683,6 +3790,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 {
|
||||
|
||||
+369
-115
@@ -14,6 +14,9 @@ import {
|
||||
isRecoverableWechatAgentSessionError,
|
||||
isWechatAgentApiErrorText,
|
||||
isWechatHistoricalImageSessionError,
|
||||
WECHAT_IMAGE_STORED_ACK_TEXT,
|
||||
prepareWechatIntentForHistoricalImageRetry,
|
||||
resolveWechatRecentMediaPublicUrl,
|
||||
sanitizeWechatAgentOutboundText,
|
||||
loadWechatMpConfig,
|
||||
maybeAttachPublishedHtmlLink,
|
||||
@@ -3498,6 +3501,40 @@ 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('prepareWechatIntentForHistoricalImageRetry can recover image url from recent media cache', () => {
|
||||
const intent = prepareWechatIntentForHistoricalImageRetry(
|
||||
{ agentText: '帮我把报告再详细点' },
|
||||
{ fallbackImageUrl: 'https://example.com/report.png' },
|
||||
);
|
||||
assert.match(intent.agentText, /https:\/\/example\.com\/report\.png/);
|
||||
assert.equal(intent.media, undefined);
|
||||
});
|
||||
|
||||
test('resolveWechatRecentMediaPublicUrl reads the latest remembered image', () => {
|
||||
assert.equal(
|
||||
resolveWechatRecentMediaPublicUrl({
|
||||
items: [
|
||||
{ media: { publicUrl: 'https://example.com/first.png' } },
|
||||
{ media: { publicUrl: 'https://example.com/second.png' } },
|
||||
],
|
||||
}),
|
||||
'https://example.com/second.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('wechat mp rotates and retries when historical image isolation is unsupported', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
@@ -4917,7 +4954,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 +4966,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 +4984,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 +5053,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 +5061,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 +5076,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 +5384,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 +5396,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 +5445,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 +5468,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 +5480,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 +5529,311 @@ 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.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 rotates polluted image session before a content-edit follow-up', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-image-followup-rotate';
|
||||
const submitCalls = [];
|
||||
let activeSessionId = 'session-1';
|
||||
let nextSessionId = 2;
|
||||
let routeCleared = false;
|
||||
const pollutedConversation = [
|
||||
{
|
||||
id: 'assistant-report',
|
||||
role: 'assistant',
|
||||
metadata: { userVisible: true },
|
||||
content: [{ type: 'text', text: '报告页面已生成。' }],
|
||||
},
|
||||
{
|
||||
id: 'assistant-image',
|
||||
role: 'assistant',
|
||||
metadata: { userVisible: false },
|
||||
content: [{ type: 'image_url', image_url: { url: 'https://example.com/report.png' } }],
|
||||
},
|
||||
];
|
||||
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
config: {
|
||||
mediaAnalysisGrayUsers: [testUserId],
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: testUserId, status: 'active', nickname: '唐' };
|
||||
},
|
||||
async getWechatAgentRoute() {
|
||||
if (routeCleared || !activeSessionId) return null;
|
||||
return { agentSessionId: activeSessionId, status: 'active', updatedAt: Date.now() };
|
||||
},
|
||||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||||
activeSessionId = agentSessionId;
|
||||
routeCleared = false;
|
||||
},
|
||||
async clearWechatAgentRoute() {
|
||||
routeCleared = true;
|
||||
},
|
||||
},
|
||||
startAgentSession: async () => ({ id: `session-${nextSessionId++}` }),
|
||||
sessionApiFetch: async (sessionId, pathname) => {
|
||||
if (pathname === `/sessions/${sessionId}`) {
|
||||
const body = sessionId === 'session-1'
|
||||
? { conversation: pollutedConversation }
|
||||
: { conversation: [] };
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (pathname === `/sessions/${sessionId}/events`) {
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","request_id":"req-followup","message":{"id":"assistant-2","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已按你的要求补充报告细节。"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-followup","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${sessionId} ${pathname}`);
|
||||
},
|
||||
submitSessionReply: async (input) => {
|
||||
submitCalls.push(input);
|
||||
return { ok: true };
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
if (String(url).includes('/cgi-bin/stable_token')) {
|
||||
return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
||||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
const originalRandomUuid = crypto.randomUUID;
|
||||
crypto.randomUUID = (() => {
|
||||
const ids = ['req-followup'];
|
||||
return () => ids.shift() ?? 'req-followup';
|
||||
})();
|
||||
try {
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ msgType: 'text', content: '帮我把报告再详细一点' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await result.task;
|
||||
} finally {
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
}
|
||||
|
||||
assert.equal(submitCalls.length, 1);
|
||||
assert.notEqual(submitCalls[0].sessionId, 'session-1');
|
||||
assert.equal(activeSessionId, submitCalls[0].sessionId);
|
||||
});
|
||||
|
||||
test('wechat mp simulates tang image report page and desensitized follow-up', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const testUserId = 'test-user-tang-report-followup';
|
||||
const submitCalls = [];
|
||||
const customerReplies = [];
|
||||
let activeSessionId = 'session-1';
|
||||
let nextSessionId = 2;
|
||||
let routeCleared = false;
|
||||
let reportPageCompleted = false;
|
||||
let reportSessionId = null;
|
||||
const pollutedConversation = [
|
||||
{
|
||||
id: 'assistant-report',
|
||||
role: 'assistant',
|
||||
metadata: { userVisible: true },
|
||||
content: [{ type: 'text', text: '已解读化验报告并生成页面链接。' }],
|
||||
},
|
||||
{
|
||||
id: 'assistant-image',
|
||||
role: 'assistant',
|
||||
metadata: { userVisible: false },
|
||||
content: [{ type: 'image_url', image_url: { url: 'https://example.com/report.png' } }],
|
||||
},
|
||||
];
|
||||
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
config: {
|
||||
mediaAnalysisGrayUsers: [testUserId],
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: testUserId, status: 'active', nickname: '唐' };
|
||||
},
|
||||
async getWechatAgentRoute() {
|
||||
if (routeCleared || !activeSessionId) return null;
|
||||
return { agentSessionId: activeSessionId, status: 'active', updatedAt: Date.now() };
|
||||
},
|
||||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||||
activeSessionId = agentSessionId;
|
||||
routeCleared = false;
|
||||
},
|
||||
async clearWechatAgentRoute() {
|
||||
routeCleared = true;
|
||||
},
|
||||
},
|
||||
startAgentSession: async () => ({ id: `session-${nextSessionId++}` }),
|
||||
sessionApiFetch: async (sessionId, pathname) => {
|
||||
if (pathname === `/sessions/${sessionId}`) {
|
||||
const conversation =
|
||||
reportSessionId && sessionId === reportSessionId && reportPageCompleted
|
||||
? pollutedConversation
|
||||
: [];
|
||||
return new Response(JSON.stringify({ conversation }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (pathname === `/sessions/${sessionId}/events`) {
|
||||
const isReportStream = submitCalls.length === 0;
|
||||
const text = isReportStream
|
||||
? '报告页面已生成:https://m.tkmind.cn/MindSpace/test/public/report.html'
|
||||
: '已按你的要求脱敏并补充报告细节。';
|
||||
return new Response(
|
||||
[
|
||||
`data: {"type":"Message","message":{"id":"assistant-${isReportStream ? 'report' : 'edit'}","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"${text}"}]}}\n\n`,
|
||||
'data: {"type":"Finish","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${sessionId} ${pathname}`);
|
||||
},
|
||||
submitSessionReply: async (input) => {
|
||||
submitCalls.push(input);
|
||||
if (!reportSessionId) {
|
||||
reportSessionId = input.sessionId;
|
||||
reportPageCompleted = true;
|
||||
}
|
||||
assert.equal(input.options?.requireHistoricalImageIsolation, true);
|
||||
return { ok: true };
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
if (String(url).includes('/cgi-bin/stable_token')) {
|
||||
return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/media/get')) {
|
||||
return new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
||||
customerReplies.push(JSON.parse(init.body ?? '{}'));
|
||||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const resetResult = await service.handleInboundMessage(
|
||||
inboundXml({ msgType: 'text', content: '换新会话', extraFields: { MsgId: 'tang-1' } }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
if (resetResult.task) await resetResult.task;
|
||||
assert.equal(submitCalls.length, 0);
|
||||
|
||||
const imageResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'image',
|
||||
content: '',
|
||||
extraFields: {
|
||||
MsgId: 'tang-2',
|
||||
MediaId: 'media-report',
|
||||
PicUrl: 'https://wx.example.com/report.png',
|
||||
},
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
assert.equal(submitCalls.length, 0);
|
||||
assert.match(imageResult.body ?? '', /已收到图片/);
|
||||
|
||||
const reportResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'text',
|
||||
content: '解读报告并生成页面',
|
||||
extraFields: { MsgId: 'tang-3' },
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await reportResult.task;
|
||||
|
||||
const editResult = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'text',
|
||||
content: '帮我把真名脱敏,报告再详细点',
|
||||
extraFields: { MsgId: 'tang-4' },
|
||||
}),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await editResult.task;
|
||||
} finally {
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
assert.ok(submitCalls.length >= 2, `submitCalls=${submitCalls.length}`);
|
||||
const firstReportSubmit = submitCalls.find(
|
||||
(call) => Array.isArray(call.userMessage.metadata?.imageUrls) && call.userMessage.metadata.imageUrls.length > 0,
|
||||
);
|
||||
assert.ok(firstReportSubmit, 'missing report submit with imageUrls');
|
||||
assert.equal(firstReportSubmit.sessionId, reportSessionId);
|
||||
assert.notEqual(submitCalls.at(-1).sessionId, reportSessionId);
|
||||
assert.ok(customerReplies.length >= 2);
|
||||
const replyTexts = customerReplies.map((payload) => String(payload?.text?.content ?? ''));
|
||||
for (const text of replyTexts) {
|
||||
assert.doesNotMatch(text, /unknown variant [`']?image_url/i);
|
||||
assert.doesNotMatch(text, /Ran into this error:/i);
|
||||
}
|
||||
assert.ok(replyTexts.some((text) => /脱敏/.test(text)), replyTexts.join(' | '));
|
||||
assert.ok(
|
||||
replyTexts.some((text) => /report\.html/.test(text))
|
||||
|| submitCalls.some((call) => Array.isArray(call.userMessage.metadata?.imageUrls)),
|
||||
replyTexts.join(' | '),
|
||||
);
|
||||
assert.equal(submitCalls[1].userMessage.metadata.msgType, 'text');
|
||||
assert.equal(submitCalls[1].userMessage.metadata.displayText, '解读详细报告,做成页面');
|
||||
});
|
||||
|
||||
test('wechat mp groups consecutive images for one follow-up and clears the consumed batch', async () => {
|
||||
@@ -5562,8 +5842,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 +5854,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 +5907,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 +5922,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 +5932,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 +5946,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