fix: aggregate WeChat multi-image batches

This commit is contained in:
john
2026-07-18 19:53:09 +08:00
parent 341cf5ae89
commit 711a7d2061
3 changed files with 257 additions and 18 deletions
+77 -18
View File
@@ -50,6 +50,8 @@ const DEFAULT_WECHAT_CUSTOMER_SERVICE_URL =
'https://api.weixin.qq.com/cgi-bin/message/custom/send';
const DEFAULT_WECHAT_JSAPI_TICKET_URL = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket';
const DEFAULT_ASR_TARGET = process.env.H5_ASR_TARGET ?? 'https://asr.tkmind.cn';
const WECHAT_RECENT_MEDIA_TTL_MS = 15 * 60 * 1000;
const WECHAT_RECENT_IMAGE_MAX_COUNT = 10;
export { loadWechatMpConfig };
const PUBLIC_HTML_LINK_PATTERN =
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
@@ -1538,10 +1540,31 @@ export function createWechatMpService({
const rememberRecentMedia = (openid, intent) => {
const publicUrl = String(intent?.media?.publicUrl ?? '').trim();
if (!publicUrl) return;
recentMediaByOpenid.set(String(openid ?? '').trim(), {
const key = String(openid ?? '').trim();
const now = Date.now();
const item = {
media: { ...(intent.media ?? {}) },
attachment: intent.attachment ? { ...intent.attachment } : null,
rememberedAt: Date.now(),
};
const current = recentMediaByOpenid.get(key);
const canAppendImage =
intent?.msgType === 'image' &&
current &&
!current.claimed &&
now - current.rememberedAt <= WECHAT_RECENT_MEDIA_TTL_MS &&
current.items.every((recentItem) => !recentItem.attachment);
const candidates = canAppendImage ? [...current.items, item] : [item];
const deduped = candidates.filter(
(candidate, index, values) =>
values.findIndex(
(value) => String(value?.media?.publicUrl ?? '') === String(candidate?.media?.publicUrl ?? ''),
) === index,
);
recentMediaByOpenid.set(key, {
items: deduped.slice(-WECHAT_RECENT_IMAGE_MAX_COUNT),
rememberedAt: now,
batchId: crypto.randomUUID(),
claimed: false,
});
};
@@ -1549,10 +1572,36 @@ export function createWechatMpService({
if (!mediaAnalysisEnabled || intent?.msgType !== 'text' || intent?.media?.publicUrl) return;
const text = String(intent?.agentText ?? '');
if (!/(?:刚才|之前|上一|这张|这份|图片|图像|照片|文件|文档|表格|excel|word)/iu.test(text)) return;
const recent = recentMediaByOpenid.get(String(openid ?? '').trim());
if (!recent || Date.now() - recent.rememberedAt > 15 * 60 * 1000) return;
intent.media = { ...recent.media, source: 'wechat_recent_media' };
if (recent.attachment) intent.attachment = { ...recent.attachment };
const key = String(openid ?? '').trim();
const recent = recentMediaByOpenid.get(key);
if (
!recent ||
recent.claimed ||
Date.now() - recent.rememberedAt > WECHAT_RECENT_MEDIA_TTL_MS ||
recent.items.length === 0
) {
return;
}
const items = recent.items.map((item) => ({
media: { ...(item.media ?? {}), source: 'wechat_recent_media' },
attachment: item.attachment ? { ...item.attachment } : null,
}));
const primary = items.at(-1);
intent.media = { ...(primary?.media ?? {}) };
if (primary?.attachment) intent.attachment = { ...primary.attachment };
intent.recentMediaItems = items;
intent.recentMediaBatchId = recent.batchId;
recent.claimed = true;
};
const settleRecentMediaBatch = (openid, intent, { succeeded }) => {
const batchId = String(intent?.recentMediaBatchId ?? '').trim();
if (!batchId) return;
const key = String(openid ?? '').trim();
const recent = recentMediaByOpenid.get(key);
if (!recent || recent.batchId !== batchId) return;
if (succeeded) recentMediaByOpenid.delete(key);
else recent.claimed = false;
};
const resolveWechatBillingTokenState = (sessionId, tokenState) =>
@@ -1975,25 +2024,33 @@ export function createWechatMpService({
const buildIntentMetadata = (intent, { mediaAnalysisEnabled = false } = {}) => {
const mediaPublicUrl = intent.media?.publicUrl || null;
const fileAttachment =
mediaPublicUrl && intent.attachment?.filename
? {
assetId: '',
downloadUrl: mediaPublicUrl,
filename: intent.attachment.filename,
mimeType: intent.attachment.mimeType || 'application/octet-stream',
}
: null;
const mediaItems = Array.isArray(intent.recentMediaItems) && intent.recentMediaItems.length > 0
? intent.recentMediaItems
: mediaPublicUrl
? [{ media: intent.media, attachment: intent.attachment ?? null }]
: [];
const imageUrls = mediaItems
.filter((item) => !item.attachment)
.map((item) => String(item?.media?.publicUrl ?? '').trim())
.filter((url, index, values) => url && values.indexOf(url) === index);
const fileAttachments = mediaItems
.filter((item) => item.attachment?.filename && item.media?.publicUrl)
.map((item) => ({
assetId: '',
downloadUrl: item.media.publicUrl,
filename: item.attachment.filename,
mimeType: item.attachment.mimeType || 'application/octet-stream',
}));
return {
source: 'wechat_mp',
msgType: intent.msgType,
originalMsgId: intent.msgId || null,
displayText: intent.displayText || '',
mediaPublicUrl,
...(mediaAnalysisEnabled && mediaPublicUrl && !fileAttachment
? { imageUrls: [mediaPublicUrl] }
...(mediaAnalysisEnabled && imageUrls.length > 0
? { imageUrls }
: {}),
...(mediaAnalysisEnabled && fileAttachment ? { fileAttachments: [fileAttachment] } : {}),
...(mediaAnalysisEnabled && fileAttachments.length > 0 ? { fileAttachments } : {}),
recognition: intent.msgType === 'voice' ? intent.agentText || null : null,
location: intent.location || null,
link: intent.link || null,
@@ -2776,6 +2833,7 @@ export function createWechatMpService({
}),
)
.then(async ({ sessionId } = {}) => {
settleRecentMediaBatch(inbound.fromUserName, intent, { succeeded: true });
if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') {
await userAuth.finishWechatMpMessage({
appId: config.appId,
@@ -2787,6 +2845,7 @@ export function createWechatMpService({
}
})
.catch(async (err) => {
settleRecentMediaBatch(inbound.fromUserName, intent, { succeeded: false });
if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') {
await userAuth.finishWechatMpMessage({
appId: config.appId,