import crypto from 'node:crypto'; import path from 'node:path'; import { fetch as undiciFetch } from 'undici'; import { developerToolsFromPolicy } from './capabilities.mjs'; import { mergeMessageContent } from './message-stream.mjs'; import { reconcileAgentSession } from './session-reconcile.mjs'; import { resolveSessionAccess } from './session-broker.mjs'; import { loadWechatMpConfig } from './wechat-mp-config.mjs'; import { downloadTemporaryMedia, persistWechatAttachment, persistWechatImage, uploadWechatGeneratedImage, } from './wechat-media.mjs'; import { normalizeWechatName, resolveWechatAddressName } from './wechat/user/display-name.mjs'; import { buildAckText } from './wechat/ack/ack-provider.mjs'; import { guardScheduleConfirmationReply } from './wechat/handlers/schedule-guard.mjs'; import { handleWechatScheduleIntent } from './wechat/handlers/schedule.mjs'; import { buildStatusText, resolveSyncReply, shouldHandleSyncReply, } from './wechat/handlers/sync-replies.mjs'; import { classifyWechatIntent } from './wechat/intent/classifier.mjs'; import { isWechatSessionResetAction, resolveWechatSessionAction, } from './wechat/intent/session-action.mjs'; import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs'; import { isPageDataDevIntent, isPageDataIntent, } from './chat-skills.mjs'; import { isWechatImmediateContextPageCreate, isWechatSessionPageContinuation, isWechatContentEditFollowup, } from './wechat/intent/page-continuation.mjs'; import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs'; import { buildWechatImageRunMetadata, resolveWechatImageGenerationPolicy, WECHAT_PAGE_THUMBNAIL_MODE, } from './wechat/image-generation-policy.mjs'; import { buildWechatAgentPrompt } from './wechat/prompts/chat-general.mjs'; import { buildPageGenerateAgentPrompt, buildImmediateContextPageRepairPrompt, buildPagePublishFailureText, } from './wechat/prompts/page-generate.mjs'; import { artifactFileExists, isStubPublicHtmlArtifact, selectSendableHtmlArtifacts, } from './wechat/verify/page-artifact.mjs'; import { collectWechatGeneratedImages, } from './wechat/verify/generated-thumbnail.mjs'; import { resolveBillingTokenState } from './billing-token-state.mjs'; import { buildPageDataCollectFailureText, } from './mindspace-page-data-finish-guard.mjs'; export { buildWechatAgentPrompt }; const DEFAULT_WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token'; 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; const DEFAULT_WECHAT_AGENT_REPLY_TIMEOUT_MS = 15 * 60 * 1000; const WECHAT_SESSION_RESET_ACK_TEXT = '已切换到新会话,请发送你的新需求。'; export { loadWechatMpConfig }; const PUBLIC_HTML_LINK_PATTERN = /https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi; const SESSION_WORKSPACE_TOOL_NAMES = new Set([ 'write', 'edit', 'tree', 'read_file', 'write_file', 'edit_file', 'create_dir', 'list_dir', ]); function escapeRegExp(value) { return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function parseXmlField(xml, field) { const cdataMatch = xml.match(new RegExp(`<${field}><\\/${field}>`)); if (cdataMatch) return cdataMatch[1]; const plainMatch = xml.match(new RegExp(`<${field}>([\\s\\S]*?)<\\/${field}>`)); return plainMatch ? plainMatch[1].trim() : ''; } function parseWechatMessage(xml) { return { toUserName: parseXmlField(xml, 'ToUserName'), fromUserName: parseXmlField(xml, 'FromUserName'), createTime: Number(parseXmlField(xml, 'CreateTime') || 0), msgType: parseXmlField(xml, 'MsgType').toLowerCase(), content: parseXmlField(xml, 'Content'), msgId: parseXmlField(xml, 'MsgId'), mediaId: parseXmlField(xml, 'MediaId'), format: parseXmlField(xml, 'Format'), recognition: parseXmlField(xml, 'Recognition'), picUrl: parseXmlField(xml, 'PicUrl'), locationX: parseXmlField(xml, 'Location_X'), locationY: parseXmlField(xml, 'Location_Y'), scale: parseXmlField(xml, 'Scale'), label: parseXmlField(xml, 'Label'), title: parseXmlField(xml, 'Title'), description: parseXmlField(xml, 'Description'), url: parseXmlField(xml, 'Url'), thumbMediaId: parseXmlField(xml, 'ThumbMediaId'), fileName: parseXmlField(xml, 'FileName') || parseXmlField(xml, 'Filename'), fileSize: parseXmlField(xml, 'FileSize'), event: parseXmlField(xml, 'Event').toLowerCase(), eventKey: parseXmlField(xml, 'EventKey'), latitude: parseXmlField(xml, 'Latitude'), longitude: parseXmlField(xml, 'Longitude'), precision: parseXmlField(xml, 'Precision'), encrypt: parseXmlField(xml, 'Encrypt'), rawXml: xml, }; } function readJsonResponse(response) { return response.text().then((text) => { if (!response.ok) { throw new Error(text || `upstream ${response.status}`); } return text ? JSON.parse(text) : null; }); } async function readJsonBody(response) { const text = await response.text(); if (!text) return { payload: null, text: '' }; try { return { payload: JSON.parse(text), text }; } catch { return { payload: null, text }; } } function sanitizeAsrMessage(message) { if (!message || typeof message !== 'string') return '识别失败,请重试'; const trimmed = message.trim(); if (!trimmed) return '识别失败,请重试'; if (/Failed to load audio|ffmpeg|Invalid data found when processing input|moov atom not found/i.test(trimmed)) { return '音频格式无法识别,请重新录制'; } if (/timeout|timed out/i.test(trimmed)) return '识别超时,请重试'; if (trimmed.length > 160) return `${trimmed.slice(0, 160)}…`; return trimmed; } function guessVoiceMimeType(format = '', contentType = '') { const normalizedContentType = String(contentType ?? '') .split(';')[0] .trim() .toLowerCase(); if (normalizedContentType) return normalizedContentType; const normalizedFormat = String(format ?? '').trim().toLowerCase(); if (normalizedFormat === 'amr') return 'audio/amr'; if (normalizedFormat === 'wav') return 'audio/wav'; if (normalizedFormat === 'mp3') return 'audio/mpeg'; if (normalizedFormat === 'm4a') return 'audio/mp4'; if (normalizedFormat === 'ogg') return 'audio/ogg'; return 'application/octet-stream'; } function deriveWechatEndpointFromUrl(sourceUrl, pathname) { if (!sourceUrl) return ''; try { const url = new URL(sourceUrl); url.pathname = pathname; url.search = ''; return url.toString(); } catch { return ''; } } function createUserMessage(text, metadata = {}) { return { id: crypto.randomUUID(), role: 'user', created: Math.floor(Date.now() / 1000), content: [{ type: 'text', text }], metadata: { userVisible: true, agentVisible: true, ...metadata, }, }; } function messageVisibleText(message) { if (!message?.content) return ''; return message.content .filter((item) => item.type === 'text' && typeof item.text === 'string') .map((item) => item.text) .join(''); } function pushMessage(messages, incoming) { const last = messages[messages.length - 1]; if (last?.id && incoming?.id && last.id === incoming.id) { return [ ...messages.slice(0, -1), { ...last, content: mergeMessageContent(last.content, incoming.content), }, ]; } return [...messages, incoming]; } export async function executeSessionReply( apiFetch, sessionId, requestId, prompt, metadata = {}, { submitReply = null, prepareUserMessage = null, timeoutMs = 0 } = {}, ) { const normalizedTimeoutMs = Math.max(0, Number(timeoutMs) || 0); const abortController = new AbortController(); let timedOut = false; let reader = null; const timeout = normalizedTimeoutMs > 0 ? setTimeout(() => { timedOut = true; abortController.abort(); void reader?.cancel?.().catch?.(() => {}); }, normalizedTimeoutMs) : null; try { const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, { method: 'GET', headers: { Accept: 'text/event-stream' }, signal: abortController.signal, }); if (!eventsResponse.ok || !eventsResponse.body) { const text = await eventsResponse.text().catch(() => ''); throw new Error(text || '无法建立公众号消息事件流'); } let userMessage = createUserMessage(prompt, metadata); if (prepareUserMessage) { userMessage = (await prepareUserMessage(userMessage)) ?? userMessage; } if (submitReply) { await submitReply({ sessionId, requestId, userMessage, signal: abortController.signal }); } else { const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, { method: 'POST', body: JSON.stringify({ request_id: requestId, user_message: userMessage, }), signal: abortController.signal, }); if (!replyResponse.ok) { const text = await replyResponse.text().catch(() => ''); throw new Error(text || 'Agent reply 请求失败'); } replyResponse.body?.cancel().catch?.(() => {}); } reader = eventsResponse.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let messages = []; let requestMessages = []; let hasScopedAssistantUpdate = false; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const frames = buffer.split('\n\n'); buffer = frames.pop() ?? ''; for (const frame of frames) { let data = ''; for (const line of frame.split('\n')) { if (line.startsWith('data:')) data += line.slice(5).trim(); } if (!data) continue; let event; try { event = JSON.parse(data); } catch { continue; } const routingId = event.chat_request_id ?? event.request_id; if (routingId && routingId !== requestId) continue; if (event.type === 'Message' && event.message?.metadata?.userVisible) { const hasActionRequired = event.message.content?.some((item) => item.type === 'actionRequired'); if (hasActionRequired) { throw new Error('当前回复需要人工确认,公众号通道暂不支持'); } 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 // previous page/link from the same WeChat-dedicated session. if (hasScopedAssistantUpdate) { messages = (event.conversation ?? []).filter((item) => item.metadata?.userVisible); } } else if (event.type === 'Error') { throw new Error(event.error || '任务执行失败'); } else if (event.type === 'Finish') { const assistant = [...messages].reverse().find((item) => item.role === 'assistant'); if (!hasScopedAssistantUpdate || !assistant) { throw new Error('本轮未收到可发送的新回复,请稍后重试'); } const reply = { 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; } } } throw new Error('公众号消息事件流提前结束'); } catch (err) { if (timedOut) { const timeoutError = new Error(`公众号消息处理超时(${normalizedTimeoutMs}ms),请稍后重试`); timeoutError.code = 'WECHAT_AGENT_REPLY_TIMEOUT'; timeoutError.retryable = true; throw timeoutError; } throw err; } finally { if (timeout) clearTimeout(timeout); await reader?.cancel?.().catch?.(() => {}); } } const WECHAT_CUSTOMER_TEXT_MAX_BYTES = 2048; function splitWechatText(text, maxBytes = WECHAT_CUSTOMER_TEXT_MAX_BYTES) { const normalized = String(text ?? '').trim(); if (!normalized) return ['我先收到了,但这次没有生成可发送的文本结果。']; const chunks = []; let offset = 0; while (offset < normalized.length) { let byteCount = 0; let end = offset; while (end < normalized.length) { const charBytes = Buffer.byteLength(normalized[end], 'utf8'); if (byteCount + charBytes > maxBytes) break; byteCount += charBytes; end += 1; } if (end === offset) break; chunks.push(normalized.slice(offset, end)); offset = end; } return chunks.length > 0 ? chunks : ['我先收到了,但这次没有生成可发送的文本结果。']; } function appendGeneratedImageFallbackLink(text, generatedImage) { const normalized = String(text ?? '').trim(); const publicUrl = String(generatedImage?.publicUrl ?? '').trim(); if (!publicUrl || normalized.includes(publicUrl)) return normalized; return [normalized, `图片链接:${publicUrl}`].filter(Boolean).join('\n'); } export { splitWechatText, WECHAT_CUSTOMER_TEXT_MAX_BYTES }; function flattenSessionTools(extensions = []) { return new Set( extensions.flatMap((extension) => Array.isArray(extension?.available_tools) ? extension.available_tools : [], ), ); } function needsWorkspaceTools(sessionPolicy) { return developerToolsFromPolicy(sessionPolicy).some((tool) => SESSION_WORKSPACE_TOOL_NAMES.has(tool)); } async function readSessionExtensions(fetchForSession, sessionId) { const payload = await readJsonResponse(await fetchForSession(sessionId, `/sessions/${sessionId}/extensions`)); return payload?.extensions ?? []; } async function sessionHasRequiredTools(fetchForSession, sessionId, sessionPolicy) { if (!needsWorkspaceTools(sessionPolicy)) return true; const desired = developerToolsFromPolicy(sessionPolicy).filter((tool) => SESSION_WORKSPACE_TOOL_NAMES.has(tool)); if (desired.length === 0) return true; const available = flattenSessionTools(await readSessionExtensions(fetchForSession, sessionId)); return desired.some((tool) => available.has(tool)); } function extractHtmlWriteTargets(messages = []) { const targets = new Set(); for (const message of messages) { for (const item of message?.content ?? []) { if (item?.type !== 'toolRequest') continue; const toolCall = item.toolCall?.value; const name = toolCall?.name; const args = toolCall?.arguments ?? {}; const action = String(args.action ?? '').toLowerCase(); const writeLikeDeveloper = name === 'developer' && action === 'write'; const normalizedName = String(name ?? '').trim(); const writeLikeSandbox = (normalizedName === 'write_file' || normalizedName === 'edit_file' || normalizedName.endsWith('__write_file') || normalizedName.endsWith('__edit_file')) && typeof args.path === 'string'; if (!writeLikeDeveloper && !writeLikeSandbox) continue; const candidate = String(args.path ?? '').trim(); if (candidate.toLowerCase().endsWith('.html')) targets.add(candidate); } } return [...targets]; } 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; return /(?:(?:word|docx|\.docx|\.doc|文档).*(?:下载|链接|导出|给我)|(?:下载|导出|提供|给我).*(?:word|docx|\.docx|\.doc|文档))/iu.test(normalized); } function usedStaticPagePublishSkill(messages = []) { return messages.some((message) => message?.content?.some((item) => { if (item?.type !== 'toolRequest') return false; const toolCall = item.toolCall?.value; const name = String(toolCall?.name ?? '').trim(); const args = toolCall?.arguments ?? {}; const skillName = String(args.name ?? '').trim(); return name === 'load_skill' && skillName === 'static-page-publish'; }), ); } function isBareCompletionText(text) { const normalized = String(text ?? '').trim(); return /^(?:已完成|完成了|完成)$/u.test(normalized); } function hasAnyToolRequest(messages = []) { return messages.some((message) => message?.content?.some((item) => item?.type === 'toolRequest'), ); } function isSuspiciousBareCompletionReply(reply, intent) { if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false; if (!isBareCompletionText(reply?.text)) return false; if (extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0) return false; return !hasAnyToolRequest(replyRequestMessages(reply)); } function looksLikePublishSuccessClaim(text) { const normalized = String(text ?? '').trim(); if (!normalized) return false; return /(?:页面|网页|html).*(?:已创建|已生成|已发布|创建完毕|生成完成|发布成功)|(?:成功发布|已经发布|已创建完毕).*(?:页面|网页|html)/iu.test(normalized); } async function hasAnyValidPublishedHtmlLink(text, linkExists, { confirmedArtifacts = [] } = {}) { const value = String(text ?? ''); for (const match of value.matchAll(PUBLIC_HTML_LINK_PATTERN)) { try { if (await linkExists(match[0])) return true; } catch { // treat lookup failures as missing links } const filename = String(match[2] ?? '').split('/').pop()?.trim(); if ( filename && confirmedArtifacts.some((artifact) => { const artifactName = path.posix.basename(String(artifact.relativePath ?? '').replace(/\\/g, '/')); return artifactName === filename && artifactFileExists(artifact); }) ) { return true; } } return false; } async function isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists = defaultPublicHtmlLinkExists } = {}) { if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false; const text = String(reply?.text ?? '').trim(); if (!looksLikePublishSuccessClaim(text)) 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(replyRequestMessages(reply)).length > 0; if (!wroteHtml) return false; return !usedStaticPagePublishSkill(replyRequestMessages(reply)); } function sanitizeFilenameSlug(value) { const normalized = String(value ?? '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); return normalized || 'simple-page'; } function hasAnyUrl(text) { return /https?:\/\/\S+/i.test(String(text ?? '')); } function hasAnyPublicHtmlLink(text) { return [...String(text ?? '').matchAll(PUBLIC_HTML_LINK_PATTERN)].length > 0; } function collectPublicHtmlLinkFilenames(text) { const filenames = new Set(); for (const match of String(text ?? '').matchAll(PUBLIC_HTML_LINK_PATTERN)) { const relativePath = String(match[2] ?? '').trim(); const filename = path.posix.basename(relativePath); if (filename) filenames.add(filename); } return filenames; } function rewritePublishedHtmlLinks(text, artifacts = []) { const value = String(text ?? ''); if (!value || artifacts.length === 0) return value; let next = value; for (const artifact of artifacts) { const canonicalUrl = String(artifact?.url ?? '').trim(); const filename = String(artifact?.relativePath ?? '').replace(/\\/g, '/').split('/').pop(); if (!canonicalUrl || !filename) continue; const wrongPublicLinkPattern = new RegExp( `https?:\\/\\/[^\\s)]+\\/(?:MindSpace\\/[^\\s/]+\\/)?public\\/${escapeRegExp(filename)}\\b`, 'g', ); const wrongTkmindHtmlLinkPattern = new RegExp( `https?:\\/\\/(?:[^\\s./]+\\.)*tkmind\\.cn\\/[^\n\\s)]*${escapeRegExp(filename)}\\b`, 'gi', ); next = next.replace(wrongPublicLinkPattern, canonicalUrl); next = next.replace(wrongTkmindHtmlLinkPattern, canonicalUrl); next = next.replace(PUBLIC_HTML_LINK_PATTERN, (match) => { if (match === canonicalUrl) return match; const matchedFilename = String(match).split('/').pop(); return matchedFilename === filename ? canonicalUrl : match; }); } return next; } async function maybeAttachPublishedHtmlLink( reply, { artifacts: providedArtifacts = [], allowAttachment = true, }, ) { if (!allowAttachment) return String(reply?.text ?? '').trim(); const artifacts = Array.isArray(providedArtifacts) ? providedArtifacts : []; const baseText = rewritePublishedHtmlLinks(String(reply?.text ?? '').trim(), artifacts).trim(); for (const artifact of artifacts) { const url = artifact.url; if (baseText.includes(url)) return baseText; return baseText ? `${baseText}\n\n查看页面:\n${url}` : `查看页面:\n${url}`; } return baseText; } function stripInternalWechatUsername(text) { return String(text ?? '').replace(/^\s*wx_[a-z0-9_]{4,64}\s*[,,、::]\s*/i, ''); } function replaceLeadingInternalWechatUsername(text, user) { const addressName = normalizeWechatName(user?.nickname) || normalizeWechatName(user?.displayName); if (!addressName) return stripInternalWechatUsername(text); return String(text ?? '').replace( /(^|[\s,,、::])wx_[a-z0-9_]{4,64}\s*([,,、::])\s*/gi, (_match, prefix = '', punctuation = ',') => `${prefix}${addressName}${punctuation}`, ); } function convertMarkdownLinks(text) { return String(text ?? '').replace(/\[([^\]\n]{1,160})\]\((https?:\/\/[^\s)]+)\)/g, (_match, label, url) => { const cleanLabel = String(label ?? '').trim(); const cleanUrl = String(url ?? '').trim(); return cleanLabel ? `${cleanLabel}\n${cleanUrl}` : cleanUrl; }); } function splitMarkdownTableRow(line) { const trimmed = String(line ?? '').trim(); if (!trimmed.includes('|')) return null; return trimmed .replace(/^\|/, '') .replace(/\|$/, '') .split('|') .map((cell) => cell.trim()); } function isMarkdownTableSeparator(line) { const cells = splitMarkdownTableRow(line); return Boolean(cells?.length) && cells.every((cell) => /^:?-{3,}:?$/.test(cell)); } function convertMarkdownTables(text) { const lines = String(text ?? '').split('\n'); const output = []; for (let index = 0; index < lines.length; index += 1) { const headers = splitMarkdownTableRow(lines[index]); if (!headers || !isMarkdownTableSeparator(lines[index + 1])) { output.push(lines[index]); continue; } const readableRows = []; index += 2; while (index < lines.length) { const cells = splitMarkdownTableRow(lines[index]); if (!cells) break; const parts = cells .map((cell, cellIndex) => { const header = headers[cellIndex] || `列${cellIndex + 1}`; return cell ? `${header}:${cell}` : ''; }) .filter(Boolean); if (parts.length) readableRows.push(`- ${parts.join(';')}`); index += 1; } index -= 1; output.push(...readableRows); } return output.join('\n'); } function stripMarkdownEmphasis(text) { return String(text ?? '') .replace(/\*\*([^*\n]+)\*\*/g, '$1') .replace(/__([^_\n]+)__/g, '$1') .replace(/`([^`\n]+)`/g, '$1') .replace(/\*/g, '') .replace(/__/g, ''); } function formatWechatOutboundText(text, user = null) { return replaceLeadingInternalWechatUsername( stripMarkdownEmphasis(convertMarkdownTables(convertMarkdownLinks(text))) .replace(/^\s*#{1,6}\s+/gm, '') .replace(/^\s*---+\s*$/gm, '') .replace(/\n{3,}/g, '\n\n'), user, ); } function defaultPublicHtmlLinkExists(urlText) { try { const url = new URL(urlText); return !PUBLIC_HTML_LINK_PATTERN.test( url.toString(), ); } catch { return true; } finally { PUBLIC_HTML_LINK_PATTERN.lastIndex = 0; } } function publicHtmlLinkOwner(urlText) { try { const url = new URL(urlText); const parts = url.pathname .split('/') .filter(Boolean) .map((part) => { try { return decodeURIComponent(part); } catch { return part; } }); if ( parts.length < 4 || parts[0] !== 'MindSpace' || parts[2] !== 'public' ) { return null; } return parts[1] || null; } catch { return null; } } function createPreparedPublicHtmlLinkExists({ userId, prepared, fallback = defaultPublicHtmlLinkExists, }) { const validUrls = new Set([ ...(prepared?.validReplyUrls ?? []), ...(prepared?.confirmedArtifacts ?? []) .flatMap((artifact) => [ artifact?.url, artifact?.canonicalUrl, ]), ].map((value) => String(value ?? '').trim()) .filter(Boolean)); const normalizedUserId = String(userId ?? '').trim(); return async (urlText) => { const normalized = String(urlText ?? '').trim(); if (validUrls.has(normalized)) return true; const owner = publicHtmlLinkOwner(normalized); if (owner && owner === normalizedUserId) { return false; } return fallback(normalized); }; } export async function guardMissingPublicHtmlLinks( text, { linkExists = defaultPublicHtmlLinkExists } = {}, ) { const value = String(text ?? ''); const replacements = []; for (const match of value.matchAll(PUBLIC_HTML_LINK_PATTERN)) { const url = match[0]; let exists = true; try { exists = await linkExists(url); } catch { exists = false; } if (!exists) { const filename = match[2].split('/').pop() ?? '页面'; replacements.push([ url, `(页面生成未完成,已阻止发送失效链接:${filename}。请重新生成页面后再打开。)`, ]); } } return replacements.reduce((next, [from, to]) => next.replaceAll(from, to), value); } function downgradePrematurePublishClaims(text) { const value = String(text ?? ''); if (!value.includes('页面生成未完成')) return value; return value .replace(/(^|\n)([^\n]*页面都已经成功发布了[^\n]*)(?=\n|$)/g, '$1页面暂未生成完成,这次我先不发送失效链接。') .replace(/(^|\n)([^\n]*主题页面已发布[^\n]*)(?=\n|$)/g, '$1🌴 夏日主题页面生成未完成') .replace(/页面已创建完毕/g, '页面生成未完成') .replace(/页面已创建/g, '页面生成未完成') .replace(/发布成功/g, '生成未完成') .replace(/已发布成功/g, '生成未完成'); } export function buildHtmlPublishFailureText() { return buildPagePublishFailureText(); } export function isHtmlPublishFailureMessage(message) { const normalized = String(message ?? '').trim(); return ( normalized.includes('页面技能') || normalized.includes('static-page-publish') || normalized.includes('简版页') ); } export { maybeAttachPublishedHtmlLink }; function uniqueArtifactsByUrl(artifacts = []) { const byUrl = new Map(); for (const artifact of artifacts) { const url = String(artifact?.url ?? '').trim(); if (url) byUrl.set(url, artifact); } return [...byUrl.values()]; } async function resolveHtmlPublishArtifacts({ reply, intent, requestStartedAt, userId = '', sessionId = '', onPageGenerated = null, allowRecentArtifacts = true, htmlDeliveryAuthority = null, }) { const prepareDelivery = htmlDeliveryAuthority ?.prepareWechatHtmlDelivery; const deliveryRequired = allowRecentArtifacts === true || looksLikeHtmlGenerationIntent( intent?.agentText, ) || isWechatPageDataTask(intent?.agentText) || hasAnyPublicHtmlLink(reply?.text) || extractHtmlWriteTargets( replyRequestMessages(reply), ).length > 0; if ( typeof prepareDelivery !== 'function' ) { if (!deliveryRequired) { return { publishedArtifacts: [], expectedArtifacts: [], recentArtifacts: [], confirmedArtifacts: [], verifiedArtifacts: [], validReplyUrls: [], hasValidReplyLink: false, }; } throw Object.assign( new Error( 'MindSpace WeChat HTML delivery authority is unavailable', ), { code: 'MINDSPACE_WECHAT_HTML_AUTHORITY_UNAVAILABLE', }, ); } const prepared = await prepareDelivery({ userId, sessionId, reply: { text: String(reply?.text ?? ''), messages: replyRequestMessages(reply), }, intent: { agentText: String( intent?.agentText ?? '', ), displayText: String( intent?.displayText ?? '', ), }, requestStartedAt, allowRecentArtifacts, }); const publishedArtifacts = Array.isArray( prepared?.publishedArtifacts, ) ? prepared.publishedArtifacts : []; const expectedArtifacts = Array.isArray( prepared?.expectedArtifacts, ) ? prepared.expectedArtifacts : []; const recentArtifacts = Array.isArray( prepared?.recentArtifacts, ) ? prepared.recentArtifacts : []; const confirmedArtifacts = Array.isArray( prepared?.confirmedArtifacts, ) ? prepared.confirmedArtifacts : []; const verifiedArtifacts = Array.isArray( prepared?.verifiedArtifacts, ) ? prepared.verifiedArtifacts : []; if ( typeof onPageGenerated === 'function' && confirmedArtifacts.length > 0 && (allowRecentArtifacts || publishedArtifacts.length > 0) ) { void onPageGenerated({ userId, sessionId, artifacts: confirmedArtifacts }); } return { publishedArtifacts, expectedArtifacts, recentArtifacts, confirmedArtifacts, verifiedArtifacts, validReplyUrls: Array.isArray( prepared?.validReplyUrls, ) ? prepared.validReplyUrls : [], hasValidReplyLink: prepared?.hasValidReplyLink === true, }; } function nonStubHtmlArtifacts(artifacts = []) { return artifacts.filter((artifact) => artifactFileExists(artifact) && !isStubPublicHtmlArtifact(artifact)); } export function shouldRetryHtmlGenerationReply({ reply, intent, confirmedArtifacts = [], hasValidLinkInReply = false, }) { if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false; if (nonStubHtmlArtifacts(confirmedArtifacts).length > 0) return false; const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text); if (replyHasPublicLinks) { if (!hasValidLinkInReply) return true; if (isMissingRequiredPublishSkill(reply, intent)) return true; return false; } if (confirmedArtifacts.length > 0) return false; if (isMissingRequiredPublishSkill(reply, intent) || isSuspiciousBareCompletionReply(reply, intent)) { return true; } if (!usedStaticPagePublishSkill(replyRequestMessages(reply))) return false; return !hasAnyUrl(reply?.text); } function isTopicResetIntent(text) { return isTopicResetText(text); } export function shouldForceNewWechatAgentSession(wechatIntent, resetCandidate) { return ( wechatIntent?.kind === 'session.reset' || isWechatSessionResetAction(wechatIntent?.sessionActionResult) || isTopicResetIntent(resetCandidate) || isPageDataIntent(resetCandidate) ); } export function isWechatImmediateContextFollowup(wechatIntent, text) { return isWechatImmediateContextPageCreate(wechatIntent, text); } export { isWechatSessionPageContinuation }; export function shouldRotateUnlinkedWechatRoute({ routeUpdatedAt, wechatMessageCount, snapshotMessageCount, now = Date.now(), } = {}) { const updatedAt = Number(routeUpdatedAt ?? 0); return ( updatedAt > 0 && Number(now) - updatedAt > 60_000 && Number(wechatMessageCount) === 0 && Number(snapshotMessageCount ?? 0) > 0 ); } export function isWechatPageDataTask(text) { return isPageDataIntent(text) || isPageDataDevIntent(text); } export function isRecoverableWechatAgentSessionError(message) { const normalized = String(message ?? '').trim(); if (!normalized) return false; if (/stale_session_poisoned_completion/i.test(normalized)) return true; 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) { const text = String(intent?.agentText ?? intent?.displayText ?? '').trim(); return ( wechatIntent?.kind === 'page.generate' || looksLikeHtmlGenerationIntent(text) || isPageDataIntent(text) || (wechatIntent?.kind === 'chat.general' && isWechatContentEditFollowup(text)) ); } function collectWechatAgentReplyVisibleTexts(reply) { const texts = []; const seen = new Set(); const append = (text) => { const normalized = String(text ?? '').trim(); if (!normalized || seen.has(normalized)) return; seen.add(normalized); texts.push(normalized); }; append(reply?.text); for (const message of replyRequestMessages(reply)) { if (message?.role !== 'assistant') continue; append(messageVisibleText(message)); } return texts; } export function findRecoverableWechatAgentErrorInReply(reply) { for (const text of collectWechatAgentReplyVisibleTexts(reply)) { if (isRecoverableWechatAgentSessionError(text)) return text; } return null; } export function assertWechatAgentReplyIsSendable(reply) { const recoverable = findRecoverableWechatAgentErrorInReply(reply); if (recoverable) throw new Error(recoverable); const text = String(reply?.text ?? '').trim(); if ( /^(?:let me|i(?:'ll| will))\s+(?:first\s+)?(?:look|check|inspect|analy[sz]e)(?:\s+at)?\s+(?:the\s+)?image(?:\s+first)?[.!]?$/iu.test(text) || /^(?:让我|我先)(?:先)?(?:看|查看|检查|分析)(?:一下)?(?:这张|该张|这个)?图片[。!!]?$/u.test(text) ) { throw new Error('wechat_agent_incomplete_reply'); } } export function isWechatAgentApiErrorText(message) { const normalized = String(message ?? '').trim(); if (!normalized) return false; if (isRecoverableWechatAgentSessionError(normalized)) return true; if (/Ran into this error:/i.test(normalized)) return true; return /Request failed:\s*(Bad request|Internal Server Error)/i.test(normalized); } export function sanitizeWechatAgentOutboundText(text) { const normalized = String(text ?? '').trim(); if (!normalized) return normalized; const stripped = normalized .replace(/Ran into this error:[\s\S]*?(?=\n\n|$)/gi, '') .replace(/Request failed:\s*Bad request \(400\):[\s\S]*?(?=\n\n|$)/gi, '') .replace(/\n{3,}/g, '\n\n') .trim(); if (!stripped || isWechatAgentApiErrorText(stripped)) { return '刚才专属会话状态异常,我已切换到新会话。请再发一次你的需求。'; } return stripped; } function formatWechatAgentFailureMessage(err) { const message = err instanceof Error ? err.message : String(err); if (err?.code === 'WECHAT_IMAGE_GENERATION_REQUIRED') { return '这次图片生成没有获得新的有效位图,所以我没有发送旧图或占位图。请稍后重试。'; } if (isHtmlPublishFailureMessage(message)) return buildHtmlPublishFailureText(); if (isRecoverableWechatAgentSessionError(message) || isWechatAgentApiErrorText(message)) { return '刚才专属会话状态异常,我已切换到新会话。请再发一次你的需求。'; } return `这次转发到专属 Agent 失败了:${message.slice(0, 200)}`; } function markWechatUserNotified(err) { if (err && typeof err === 'object') err.wechatUserNotified = true; return err; } async function enforcePageDataCollectDelivery({ reply, intent, userId, pageDataFinishGuard, publicBaseUrl, requestStartedAt = 0, notifyFailure, }) { const prepareDelivery = pageDataFinishGuard ?.prepareWechatPageDataDelivery; if (typeof prepareDelivery !== 'function') { const agentText = String( intent?.agentText ?? intent?.displayText ?? '', ); if (!isPageDataIntent(agentText)) { return { action: 'skip' }; } const text = buildPageDataCollectFailureText(); if (typeof notifyFailure === 'function') { await notifyFailure(text); } throw markWechatUserNotified(new Error(text)); } const prepared = await prepareDelivery({ userId, reply: { text: String(reply?.text ?? ''), messages: Array.isArray(reply?.messages) ? reply.messages : [], }, intent: { agentText: String(intent?.agentText ?? ''), displayText: String( intent?.displayText ?? '', ), }, publicBaseUrl, requestStartedAt, }); const outcome = prepared?.outcome ?? { action: 'fail', failureText: buildPageDataCollectFailureText(), }; const autoBind = prepared?.autoBind ?? null; if (outcome.action === 'skip') return outcome; if (outcome.action === 'retry') { throw new Error('stale_session_poisoned_completion'); } if (outcome.action === 'fail') { const text = outcome.failureText ?? buildPageDataCollectFailureText(); if (typeof notifyFailure === 'function') { await notifyFailure(text); } throw markWechatUserNotified(new Error(text)); } const deliveryArtifacts = Array.isArray( prepared?.deliveryArtifacts, ) ? prepared.deliveryArtifacts : []; if ( prepared?.deliveryCheck && !prepared.deliveryCheck.ok ) { const text = buildPageDataCollectFailureText(); if (typeof notifyFailure === 'function') { await notifyFailure(text); } throw markWechatUserNotified(new Error(text)); } if ( deliveryArtifacts.length > 0 && reply && typeof prepared?.rewrittenText === 'string' ) { reply.text = prepared.rewrittenText; } return { ...outcome, deliveryArtifacts, autoBind }; } function wasWechatUserNotified(err) { return Boolean(err && typeof err === 'object' && err.wechatUserNotified); } function normalizeNumber(value) { if (value === '' || value === null || value === undefined) return null; const num = Number(value); return Number.isFinite(num) ? num : null; } function isWechatMediaGrayUser(user, configuredUsers = []) { const allowlist = Array.isArray(configuredUsers) ? configuredUsers.map((value) => String(value ?? '').trim().toLowerCase()).filter(Boolean) : []; if (allowlist.length === 0) return false; if (allowlist.includes('*')) return true; const identities = [ user?.userId, user?.username, user?.slug, user?.displayName, user?.nickname, ] .map((value) => String(value ?? '').trim().toLowerCase()) .filter(Boolean); return identities.some((identity) => allowlist.includes(identity)); } function normalizeWechatInboundIntent(inbound) { const msgType = String(inbound?.msgType ?? '').toLowerCase(); const base = { appId: '', openid: String(inbound?.fromUserName ?? '').trim(), msgId: String(inbound?.msgId ?? '').trim(), msgType, displayText: '', agentText: '', raw: { ...inbound }, }; if (msgType === 'text') { const content = String(inbound?.content ?? '').trim(); return { ...base, displayText: content, agentText: content, }; } if (msgType === 'voice') { const recognition = String(inbound?.recognition ?? '').trim(); return { ...base, displayText: recognition ? `语音:${recognition}` : '语音消息', agentText: recognition, media: { mediaId: inbound?.mediaId || '', format: inbound?.format || '', }, }; } if (msgType === 'image') { return { ...base, displayText: '收到图片,请描述你想让我怎么处理', agentText: '', media: { mediaId: inbound?.mediaId || '', picUrl: inbound?.picUrl || '', }, }; } if (msgType === 'file') { const filename = String(inbound?.fileName ?? '').trim(); return { ...base, displayText: filename ? `收到文件:${filename}` : '收到文件', agentText: '', media: { mediaId: inbound?.mediaId || '', }, attachment: { filename, sizeBytes: normalizeNumber(inbound?.fileSize), }, }; } if (msgType === 'location') { const latitude = normalizeNumber(inbound?.locationX); const longitude = normalizeNumber(inbound?.locationY); const scale = normalizeNumber(inbound?.scale); const label = String(inbound?.label ?? '').trim(); const fragments = [ label ? `用户发送了位置:${label}` : '用户发送了位置', latitude !== null ? `纬度:${latitude}` : '', longitude !== null ? `经度:${longitude}` : '', scale !== null ? `缩放:${scale}` : '', ].filter(Boolean); return { ...base, displayText: fragments.join(';'), agentText: fragments.join(';'), location: { latitude, longitude, scale, label, }, }; } if (msgType === 'link') { const title = String(inbound?.title ?? '').trim(); const description = String(inbound?.description ?? '').trim(); const url = String(inbound?.url ?? '').trim(); const fragments = [ '用户分享了链接', title ? `标题:${title}` : '', description ? `描述:${description}` : '', url ? `URL:${url}` : '', ].filter(Boolean); return { ...base, displayText: fragments.join(';'), agentText: fragments.join(';'), link: { title, description, url }, }; } if (msgType === 'video' || msgType === 'shortvideo') { return { ...base, displayText: msgType === 'shortvideo' ? '收到小视频' : '收到视频', agentText: '', media: { mediaId: inbound?.mediaId || '', thumbMediaId: inbound?.thumbMediaId || '', }, }; } if (msgType === 'event') { return { ...base, displayText: inbound?.event ? `事件:${inbound.event}` : '事件消息', agentText: '', location: inbound?.event === 'location' ? { latitude: normalizeNumber(inbound?.latitude), longitude: normalizeNumber(inbound?.longitude), precision: normalizeNumber(inbound?.precision), } : undefined, }; } return base; } function successResponse(task = null) { return { ok: true, status: 200, contentType: 'text/plain; charset=utf-8', body: 'success', ...(task ? { task } : {}), }; } function sha1Hex(parts) { return crypto .createHash('sha1') .update([...parts].sort().join('')) .digest('hex'); } export function verifyWechatMpSignature({ token, timestamp, nonce, signature }) { return sha1Hex([token, timestamp, nonce]) === String(signature ?? ''); } export function verifyWechatMpMsgSignature({ token, timestamp, nonce, echoStr, msgSignature }) { return sha1Hex([token, timestamp, nonce, echoStr]) === String(msgSignature ?? ''); } function decodeWechatMpAesKey(encodingAesKey) { const key = Buffer.from(`${String(encodingAesKey ?? '').trim()}=`, 'base64'); if (key.length !== 32) { throw new Error('invalid encoding aes key'); } return key; } export function decryptWechatMpPayload({ encodingAesKey, appId, encrypted }) { const key = decodeWechatMpAesKey(encodingAesKey); const iv = key.subarray(0, 16); const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); decipher.setAutoPadding(false); const cipherBuf = Buffer.from(String(encrypted ?? ''), 'base64'); let decoded = Buffer.concat([decipher.update(cipherBuf), decipher.final()]); const pad = decoded.at(-1); if (!Number.isInteger(pad) || pad < 1 || pad > 32) { throw new Error('invalid padding'); } decoded = decoded.subarray(0, decoded.length - pad); const content = decoded.subarray(16); const msgLen = content.readUInt32BE(0); const msg = content.subarray(4, 4 + msgLen).toString('utf8'); const receivedAppId = content.subarray(4 + msgLen).toString('utf8'); if (receivedAppId !== String(appId ?? '')) { throw new Error('appid mismatch'); } return msg; } export function verifyWechatMpUrlChallenge(query = {}, config = {}) { const timestamp = String(query.timestamp ?? ''); const nonce = String(query.nonce ?? ''); const echostr = String(query.echostr ?? ''); const encryptType = String(query.encrypt_type ?? '').toLowerCase(); if (encryptType === 'aes') { const msgSignature = String(query.msg_signature ?? ''); if (!config?.token || !config?.encodingAesKey || !config?.appId) { return { ok: false, status: 503, body: 'wechat mp aes mode not configured' }; } if ( !verifyWechatMpMsgSignature({ token: config.token, timestamp, nonce, echoStr: echostr, msgSignature, }) ) { return { ok: false, status: 403, body: 'invalid signature' }; } try { const plain = decryptWechatMpPayload({ encodingAesKey: config.encodingAesKey, appId: config.appId, encrypted: echostr, }); return { ok: true, status: 200, body: plain }; } catch { return { ok: false, status: 403, body: 'invalid echostr' }; } } if ( !verifyWechatMpSignature({ token: config?.token, timestamp, nonce, signature: String(query.signature ?? ''), }) ) { return { ok: false, status: 403, body: 'invalid signature' }; } return { ok: true, status: 200, body: echostr }; } export function buildWechatTextReply({ toUserName, fromUserName, content, createTime = Math.floor(Date.now() / 1000), }) { return [ '', ``, ``, `${Number(createTime) || Math.floor(Date.now() / 1000)}`, '', ``, '', ].join(''); } export function createWechatMpService({ config, userAuth, sessionAccess = null, apiFetch, startAgentSession = null, sessionApiFetch = null, submitSessionReply = null, scheduleService = null, wechatScheduleLlmConfigService = null, llmProviderService = null, chatIntentRouter = null, systemDisclosurePolicyService = null, onPageGenerated = null, applySessionLlmProvider = null, refreshSessionSnapshot = null, sessionIntentClassifier = null, htmlDeliveryAuthority = null, pageDataFinishGuard = null, pageDataDeliveryReviewer = null, wechatFetch = undiciFetch, linkExists = defaultPublicHtmlLinkExists, logger = console, }) { const sessionStore = resolveSessionAccess({ userAuth, sessionAccess }); if (!config?.enabled) { return { enabled: false, }; } const requireFreshPageThumbnail = config.requireFreshPageThumbnail === true; const repairFreshPageThumbnail = config.repairFreshPageThumbnail === true; config = { ...loadWechatMpConfig({}), ...config, tokenUrl: config.tokenUrl || DEFAULT_WECHAT_TOKEN_URL, customerServiceUrl: config.customerServiceUrl || DEFAULT_WECHAT_CUSTOMER_SERVICE_URL, jsapiTicketUrl: config.jsapiTicketUrl || DEFAULT_WECHAT_JSAPI_TICKET_URL, mediaPublicBaseUrl: config.mediaPublicBaseUrl || config.publicBaseUrl, maxImageBytes: Math.max(1, Number(config.maxImageBytes ?? 10 * 1024 * 1024)), maxFileBytes: Math.max(1, Number(config.maxFileBytes ?? 30 * 1024 * 1024)), acceptVoice: config.acceptVoice !== false, acceptImage: config.acceptImage !== false, acceptFile: config.acceptFile !== false, acceptLocation: config.acceptLocation !== false, acceptLink: config.acceptLink !== false, mediaAnalysisGrayUsers: Array.isArray(config.mediaAnalysisGrayUsers) ? config.mediaAnalysisGrayUsers : [], reliabilityGrayUsers: Array.isArray(config.reliabilityGrayUsers) ? config.reliabilityGrayUsers : [], pageDataAiderReviewEnabled: config.pageDataAiderReviewEnabled === true, pageDataAiderReviewUsers: Array.isArray(config.pageDataAiderReviewUsers) ? config.pageDataAiderReviewUsers : [], agentReplyTimeoutMs: Math.max( 0, Number(config.agentReplyTimeoutMs ?? DEFAULT_WECHAT_AGENT_REPLY_TIMEOUT_MS), ), requireFreshPageThumbnail, repairFreshPageThumbnail, asrTarget: config.asrTarget || DEFAULT_ASR_TARGET, }; let accessTokenCache = { token: null, expiresAt: 0, }; const rememberedWechatContexts = new Map(); const confirmedWechatSessionOrigins = new Set(); const messageTasksByOpenid = new Map(); const recentMediaByOpenid = new Map(); let jsapiTicketCache = { ticket: null, expiresAt: 0, }; const fetchForSession = (sessionId, pathname, init) => sessionApiFetch ? sessionApiFetch(sessionId, pathname, init) : apiFetch(pathname, init); const confirmWechatSessionOrigin = async (sessionId) => { if (!sessionId || confirmedWechatSessionOrigins.has(sessionId)) return; if (typeof userAuth.setSessionOrigin === 'function') { try { await userAuth.setSessionOrigin(sessionId, 'wechat'); } catch (err) { logger.warn?.('WeChat MP session origin update failed:', err); return; } } confirmedWechatSessionOrigins.add(sessionId); }; const enqueueMessageTask = (openid, taskFactory) => { const key = String(openid ?? '').trim(); const previous = messageTasksByOpenid.get(key) ?? Promise.resolve(); const task = previous.catch(() => undefined).then(taskFactory); messageTasksByOpenid.set(key, task); void task .finally(() => { if (messageTasksByOpenid.get(key) === task) messageTasksByOpenid.delete(key); }) .catch(() => {}); return task; }; const rememberRecentMedia = (openid, intent) => { const publicUrl = String(intent?.media?.publicUrl ?? '').trim(); if (!publicUrl) return; const key = String(openid ?? '').trim(); const now = Date.now(); const item = { media: { ...(intent.media ?? {}) }, attachment: intent.attachment ? { ...intent.attachment } : null, }; 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, }); }; const attachRecentMediaForFollowup = (openid, intent, mediaAnalysisEnabled) => { if (!mediaAnalysisEnabled || intent?.msgType !== 'text' || intent?.media?.publicUrl) return; const text = String(intent?.agentText ?? ''); if (!/(?:刚才|之前|上一|这张|这份|图片|图像|照片|文件|文档|表格|excel|word)/iu.test(text)) return; 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) => resolveBillingTokenState(tokenState, { sessionId, fetchSession: async (sid) => { const response = await fetchForSession(sid, `/sessions/${encodeURIComponent(sid)}`); if (!response?.ok) return null; return readJsonResponse(response); }, }); const buildBindUrl = () => { if (/^https?:\/\//i.test(config.bindPath)) return config.bindPath; const path = config.bindPath.startsWith('/') ? config.bindPath : `/${config.bindPath}`; return `${config.publicBaseUrl}${path}`; }; const verifyRequest = (query = {}) => verifyWechatMpSignature({ token: config.token, timestamp: String(query.timestamp ?? ''), nonce: String(query.nonce ?? ''), signature: String(query.signature ?? ''), }); const verifyUrlChallenge = (query = {}) => verifyWechatMpUrlChallenge(query, config); const getStableAccessToken = async () => { if (accessTokenCache.token && accessTokenCache.expiresAt > Date.now() + 60_000) { return accessTokenCache.token; } const payload = await readJsonResponse( await wechatFetch(config.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credential', appid: config.appId, secret: config.appSecret, force_refresh: false, }), }), ); if (!payload?.access_token) { throw new Error(payload?.errmsg || '获取微信 access_token 失败'); } const expiresIn = Number(payload.expires_in ?? 7200); accessTokenCache = { token: payload.access_token, expiresAt: Date.now() + expiresIn * 1000, }; return accessTokenCache.token; }; const getJsapiTicket = async () => { if (jsapiTicketCache.ticket && jsapiTicketCache.expiresAt > Date.now() + 60_000) { return jsapiTicketCache.ticket; } const accessToken = await getStableAccessToken(); const url = new URL(config.jsapiTicketUrl); url.searchParams.set('access_token', accessToken); url.searchParams.set('type', 'jsapi'); const payload = await readJsonResponse( await wechatFetch(url.toString(), { method: 'GET', headers: { Accept: 'application/json' }, }), ); if (Number(payload?.errcode ?? 0) !== 0 || !payload?.ticket) { throw new Error(payload?.errmsg || `获取微信 jsapi_ticket 失败 (${payload?.errcode})`); } const expiresIn = Number(payload.expires_in ?? 7200); jsapiTicketCache = { ticket: payload.ticket, expiresAt: Date.now() + expiresIn * 1000, }; return jsapiTicketCache.ticket; }; const createJsSdkSignature = async (pageUrl) => { const normalizedUrl = String(pageUrl ?? '').split('#')[0]; if (!/^https?:\/\//i.test(normalizedUrl)) { throw new Error('签名 URL 必须是完整 http(s) 地址'); } const ticket = await getJsapiTicket(); const nonceStr = crypto.randomBytes(12).toString('hex'); const timestamp = Math.floor(Date.now() / 1000); const signature = crypto .createHash('sha1') .update( [ `jsapi_ticket=${ticket}`, `noncestr=${nonceStr}`, `timestamp=${timestamp}`, `url=${normalizedUrl}`, ].join('&'), ) .digest('hex'); return { appId: config.appId, timestamp, nonceStr, signature, url: normalizedUrl, jsApiList: [ 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'translateVoice', 'updateAppMessageShareData', 'updateTimelineShareData', 'onMenuShareAppMessage', 'onMenuShareTimeline', ], }; }; const sendCustomerServiceText = async ( openid, content, user = null, { verifiedHtmlUrls = [], linkExistsForRequest = linkExists } = {}, ) => { const formatted = formatWechatOutboundText(sanitizeWechatAgentOutboundText(content), user); const verifiedUrlSet = new Set( verifiedHtmlUrls .map((url) => String(url ?? '').trim()) .filter(Boolean), ); const guarded = downgradePrematurePublishClaims( await guardMissingPublicHtmlLinks(formatted, { linkExists: async (url) => { const normalized = String(url ?? '').trim(); if (verifiedUrlSet.has(normalized) && !linkExistsForRequest(normalized)) { verifiedUrlSet.delete(normalized); } return linkExistsForRequest(url); }, }), ); const chunks = splitWechatText(guarded); const accessToken = await getStableAccessToken(); for (const chunk of chunks) { const payload = await readJsonResponse( await wechatFetch( `${config.customerServiceUrl}?access_token=${encodeURIComponent(accessToken)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ touser: openid, msgtype: 'text', text: { content: chunk, }, }), }, ), ); if (Number(payload?.errcode ?? 0) !== 0) { const errcode = Number(payload?.errcode ?? 0); const errmsg = String(payload?.errmsg ?? '').trim() || 'unknown_error'; throw new Error(`微信客服消息发送失败 errcode=${errcode} errmsg=${errmsg}`); } } }; const sendCustomerServiceImage = async (openid, generatedImage) => { const publicUrl = String(generatedImage?.publicUrl ?? '').trim(); if (!publicUrl) throw new Error('生成图片缺少可发送的公网地址'); const accessToken = await getStableAccessToken(); const uploaded = await uploadWechatGeneratedImage(accessToken, publicUrl, { wechatFetch, publicBaseUrl: config.publicBaseUrl, allowedPublicBaseUrls: config.generatedImagePublicBaseUrls, }); const payload = await readJsonResponse( await wechatFetch( `${config.customerServiceUrl}?access_token=${encodeURIComponent(accessToken)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ touser: openid, msgtype: 'image', image: { media_id: uploaded.mediaId }, }), }, ), ); if (Number(payload?.errcode ?? 0) !== 0) { const errcode = Number(payload?.errcode ?? 0); const errmsg = String(payload?.errmsg ?? '').trim() || 'unknown_error'; throw new Error(`微信图片客服消息发送失败 errcode=${errcode} errmsg=${errmsg}`); } return uploaded; }; const sendTextToUser = async (userId, content) => { const openid = await userAuth.getWechatOpenidForUser(userId, config.appId); if (!openid) { throw new Error('用户尚未绑定服务号,无法推送提醒'); } await sendCustomerServiceText(openid, content); }; const enforceFreshPageThumbnailDelivery = async ({ artifacts, reply, openid, user, userId, sessionId, imagePolicy, notifyFailure = true, }) => { if (imagePolicy?.pageThumbnailMode !== WECHAT_PAGE_THUMBNAIL_MODE.REQUIRED_FRESH) { return { ok: true, reason: null, matchRelativePaths: [] }; } const images = collectWechatGeneratedImages(replyRequestMessages(reply)); const ensureFresh = htmlDeliveryAuthority ?.ensureWechatFreshPageThumbnails; let verification; if (typeof ensureFresh !== 'function') { verification = { ok: false, reason: 'mindspace_thumbnail_authority_unavailable', }; } else { try { verification = await ensureFresh({ userId, sessionId, artifacts, images, messages: replyRequestMessages(reply), repairEnabled: config.repairFreshPageThumbnail, }); } catch (error) { verification = { ok: false, reason: `mindspace_thumbnail_authority_failed:${ error instanceof Error ? error.message : String(error) }`, }; } } if (verification?.repair?.ok) { logger.info?.( 'WeChat MP fresh thumbnail repaired by MindSpace:', { relativePath: verification.repair.relativePath, cover: verification.repair.cover, }, ); } if (verification?.ok) { if (verification.reason === 'workspace_thumbnail_fallback') { logger.info?.( 'WeChat MP page delivery accepted workspace thumbnail fallback:', { matchRelativePaths: verification.matchRelativePaths ?? [], }, ); } return verification; } logger.warn?.( 'WeChat MP fresh thumbnail verification failed:', { reason: String( verification?.reason ?? 'unknown', ), artifact: verification?.artifact ?? null, images: images.map((image) => ({ jobId: image.jobId, purpose: image.purpose, })), }, ); const text = buildPagePublishFailureText({ missingFreshThumbnail: true }); if (notifyFailure) { try { await sendCustomerServiceText(openid, text, user); } catch (sendErr) { logger.error?.('WeChat MP fresh thumbnail failure notice failed:', sendErr); } } const error = new Error( `wechat_page_fresh_thumbnail_required:${ verification?.reason ?? 'unknown' }`, ); error.code = 'WECHAT_PAGE_FRESH_THUMBNAIL_REQUIRED'; throw notifyFailure ? markWechatUserNotified(error) : error; }; const ensureSessionProvider = async (sessionId) => { if (!applySessionLlmProvider || !sessionId) return; const applied = await applySessionLlmProvider(sessionId); if (applied && applied.ok === false) { throw new Error(applied.message || '专属 Agent Provider 未配置'); } }; const transcribeWechatVoiceMedia = async (mediaId, format) => { if (!mediaId) return ''; const accessToken = await getStableAccessToken(); const downloaded = await downloadTemporaryMedia(accessToken, mediaId, { wechatFetch }); if (!downloaded.buffer?.length) return ''; const extension = String(format ?? '').trim().toLowerCase() || 'amr'; const form = new FormData(); form.append( 'file', new Blob([downloaded.buffer], { type: guessVoiceMimeType(format, downloaded.contentType), }), `wechat-voice.${extension}`, ); const response = await wechatFetch(`${config.asrTarget.replace(/\/$/, '')}/asr/oneshot`, { method: 'POST', body: form, }); const { payload, text } = await readJsonBody(response); if (!response.ok) { throw new Error(sanitizeAsrMessage(payload?.message ?? text ?? `ASR HTTP ${response.status}`)); } if (Number(payload?.code ?? 200) !== 200) { throw new Error(sanitizeAsrMessage(payload?.message ?? text ?? '识别失败,请重试')); } return String(payload?.data?.text ?? '').trim(); }; const ensureWechatAgentSession = async ({ userId, openid, forceNew = false, 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); const existingRoute = await userAuth.getWechatAgentRoute(config.appId, openid); if (existingRoute?.agentSessionId) { const now = Date.now(); const routeUpdatedAt = Number(existingRoute.updatedAt ?? 0); const routeIsIdle = config.sessionIdleRotateMs > 0 && routeUpdatedAt > 0 && now - routeUpdatedAt > config.sessionIdleRotateMs; let routeWechatMessageCount = null; let routeSnapshotMessageCount = null; let routeMessageCount = 0; if (config.sessionMessageRotateCount > 0) { [routeWechatMessageCount, routeSnapshotMessageCount] = await Promise.all([ typeof userAuth.countWechatAgentSessionMessages === 'function' ? userAuth .countWechatAgentSessionMessages({ appId: config.appId, openid, agentSessionId: existingRoute.agentSessionId, }) .then((value) => Number(value) || 0) .catch((err) => { logger.warn?.('WeChat MP route message count lookup failed:', err); return null; }) : null, typeof userAuth.getWechatAgentSessionSnapshotMessageCount === 'function' ? userAuth .getWechatAgentSessionSnapshotMessageCount(existingRoute.agentSessionId) .then((value) => Number(value) || 0) .catch((err) => { logger.warn?.('WeChat MP snapshot message count lookup failed:', err); return null; }) : null, ]); routeMessageCount = Math.max( 0, Number(routeWechatMessageCount) || 0, Number(routeSnapshotMessageCount) || 0, ); } const routeIsTooLong = config.sessionMessageRotateCount > 0 && Number(routeMessageCount ?? 0) >= config.sessionMessageRotateCount; const routeHasUnlinkedConversation = shouldRotateUnlinkedWechatRoute({ routeUpdatedAt, wechatMessageCount: routeWechatMessageCount, snapshotMessageCount: routeSnapshotMessageCount, now, }); const shouldRotateUnlinkedRoute = routeHasUnlinkedConversation && !retainUnlinkedRoute; if (routeIsIdle || routeIsTooLong || shouldRotateUnlinkedRoute) { if (shouldRotateUnlinkedRoute) { logger.warn?.('WeChat MP orphaned route rotated before reuse:', { agentSessionId: existingRoute.agentSessionId, snapshotMessageCount: routeSnapshotMessageCount, }); } await userAuth.clearWechatAgentRoute(config.appId, openid); rememberedWechatContexts.delete(existingRoute.agentSessionId); } else { sessionPolicy = await userAuth .getAgentSessionPolicy( userId, { sessionId: existingRoute .agentSessionId, packageId: `cp_${existingRoute.agentSessionId}`, }, ); await reconcileAgentSession( (pathname, init) => fetchForSession(existingRoute.agentSessionId, pathname, init), existingRoute.agentSessionId, { workingDir, sessionPolicy, sandboxConstraints: publishLayout?.constraints ?? null, userContext: publishLayout ? { userId, displayName: addressName || publishLayout.displayName, username: addressName || null, slug: null, } : null, tolerateInvalidWorkingDir: true, }, ); const routeHasTools = await sessionHasRequiredTools( fetchForSession, existingRoute.agentSessionId, sessionPolicy, ).catch(() => false); if (routeHasTools) { await confirmWechatSessionOrigin(existingRoute.agentSessionId); if (typeof userAuth.touchWechatAgentRoute === 'function') { await userAuth.touchWechatAgentRoute(config.appId, openid, now).catch((err) => { logger.warn?.('WeChat MP route touch failed:', err); }); } return { sessionId: existingRoute.agentSessionId, isNewSession: false }; } await userAuth.clearWechatAgentRoute(config.appId, openid); rememberedWechatContexts.delete(existingRoute.agentSessionId); } } else if (existingRoute?.status === 'disabled') { await userAuth.clearWechatAgentRoute(config.appId, openid); } const gate = await userAuth.canUseChat(userId); if (!gate.ok) { throw new Error(gate.message || '当前用户无法使用聊天能力'); } const started = startAgentSession ? await startAgentSession({ userId, workingDir, sessionPolicy, origin: 'wechat', }) : await readJsonResponse( await apiFetch('/agent/start', { method: 'POST', body: JSON.stringify({ working_dir: workingDir, enable_context_memory: sessionPolicy.enableContextMemory, ...(sessionPolicy.extensionOverrides ? { extension_overrides: sessionPolicy.extensionOverrides } : {}), }), }), ); const sessionId = started?.id; if (!sessionId) { throw new Error('公众号专属 Agent 会话创建失败'); } sessionPolicy = await userAuth.getAgentSessionPolicy( userId, { sessionId, packageId: `cp_${sessionId}`, }, ); // `/agent/start` already persists the owning user and goosed node via the // portal proxy. Re-registering here without the node can overwrite the // correct mapping back to node 0 in multi-goosed production. if (!startAgentSession) { await sessionStore.registerSession({ userId, sessionId, target: 0, origin: 'wechat', }); } await confirmWechatSessionOrigin(sessionId); await reconcileAgentSession( (pathname, init) => fetchForSession(sessionId, pathname, init), sessionId, { workingDir, sessionPolicy, sandboxConstraints: publishLayout?.constraints ?? null, userContext: publishLayout ? { userId, displayName: addressName || publishLayout.displayName, username: addressName || null, slug: null, } : null, tolerateInvalidWorkingDir: true, }, ); await userAuth.upsertWechatAgentRoute({ userId, appId: config.appId, openid, agentSessionId: sessionId, }); return { sessionId, isNewSession: true }; }; const refreshWechatSessionSnapshot = async (sessionId, userId) => { if (!sessionId || !userId || typeof refreshSessionSnapshot !== 'function') return; try { await refreshSessionSnapshot(sessionId, userId); } catch (err) { logger.warn?.('WeChat MP snapshot refresh failed:', err); } }; const scheduleWechatSessionSnapshotRefresh = (sessionId, userId) => { void refreshWechatSessionSnapshot(sessionId, userId); }; const rememberWechatUserContext = async (sessionId, user, { forceBootstrap = false } = {}) => { const addressName = resolveWechatAddressName(user); if (!addressName) return; const cacheKey = `${sessionId}:${addressName}`; const alreadyRemembered = rememberedWechatContexts.get(sessionId) === cacheKey; if (alreadyRemembered && !forceBootstrap) return; try { await readJsonResponse( await fetchForSession(sessionId, '/agent/harness_remember', { method: 'POST', body: JSON.stringify({ sessionId, title: '微信服务号用户', content: [ `当前服务号用户名称:${addressName}`, '这是通过微信 openid 在 H5 绑定库中查询到的用户名称。', '回复时可以自然使用该名称称呼用户,不要使用 openid 或 wx_ 开头的内部用户名。', ].join('\n'), }), }), ); await readJsonResponse( await fetchForSession(sessionId, '/agent/harness_bootstrap', { method: 'POST', body: JSON.stringify({ sessionId, force: Boolean(forceBootstrap) }), }), ); rememberedWechatContexts.set(sessionId, cacheKey); } catch (err) { logger.warn?.('WeChat MP user context remember failed:', err); } }; const buildIntentMetadata = ( intent, { mediaAnalysisEnabled = false, imagePolicy = null, pgRequired = false, } = {}, ) => { const mediaPublicUrl = intent.media?.publicUrl || 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 && imageUrls.length > 0 ? { imageUrls } : {}), ...(mediaAnalysisEnabled && fileAttachments.length > 0 ? { fileAttachments } : {}), recognition: intent.msgType === 'voice' ? intent.agentText || null : null, location: intent.location || null, link: intent.link || null, memindRun: { ...buildWechatImageRunMetadata(imagePolicy), ...(pgRequired ? { pgRequired: true } : {}), }, }; }; const persistIntentDetail = async ({ intent, userId = null, rawXmlHash = '' }) => { if (typeof userAuth.insertWechatMpMessageDetail !== 'function') return; await userAuth.insertWechatMpMessageDetail({ appId: config.appId, openid: intent.openid, msgId: intent.msgId || null, userId, msgType: intent.msgType, displayText: intent.displayText || '', agentText: intent.agentText || '', mediaId: intent.media?.mediaId || null, mediaUrl: intent.media?.picUrl || null, mediaPublicUrl: intent.media?.publicUrl || null, mediaFormat: intent.media?.format || null, locationLat: intent.location?.latitude ?? null, locationLng: intent.location?.longitude ?? null, locationLabel: intent.location?.label || null, linkUrl: intent.link?.url || null, linkTitle: intent.link?.title || null, rawXmlHash, rawJson: intent.raw, }); }; const resolveGrantedSkills = async (userId) => { try { const row = await userAuth.getUserById(userId); if (!row) return []; const caps = await userAuth.resolveUserCapabilities(row); return Array.isArray(caps?.grantedSkills) ? caps.grantedSkills : []; } catch (err) { logger.warn?.('WeChat MP granted skills lookup failed:', err); return []; } }; const prepareWechatAgentUserMessage = async ({ userId, sessionId, userMessage, preserveAgentPrompt = false, }) => { if ( !chatIntentRouter?.resolveAgentMemoryContext || !chatIntentRouter?.applyAgentOrchestration ) { return userMessage; } const displayText = String( userMessage?.metadata?.displayText ?? messageVisibleText(userMessage), ).trim(); try { const memoryContext = await chatIntentRouter.resolveAgentMemoryContext({ userId, sessionId, text: displayText, forceDeepReasoning: false, }); if ( !memoryContext?.injectionEnabled || !Array.isArray(memoryContext.memories) || memoryContext.memories.length === 0 ) { return userMessage; } const orchestrationMessage = preserveAgentPrompt ? { ...userMessage, metadata: { ...(userMessage?.metadata ?? {}), displayText: messageVisibleText(userMessage), }, } : userMessage; const prepared = chatIntentRouter.applyAgentOrchestration( orchestrationMessage, { route: 'agent_orchestration', reason: '微信服务号消息由 Agent 处理', source: 'wechat_mp', }, { memoryContext }, ); if (!preserveAgentPrompt) return prepared; return { ...prepared, metadata: { ...(prepared?.metadata ?? {}), displayText: displayText || undefined, }, }; } catch (err) { logger.warn?.( 'WeChat MP agent memory resolve skipped:', err instanceof Error ? err.message : err, ); return userMessage; } }; const reviewWechatPageDataArtifacts = async ({ user, requestId, sessionId, sourceMessageId, originalUserText, requestStartedAt, artifacts, forcePageData, }) => { const enabledForUser = config.pageDataAiderReviewEnabled && isWechatMediaGrayUser(user, config.pageDataAiderReviewUsers); if (!enabledForUser) return { action: 'skip', reason: 'disabled' }; if (typeof pageDataDeliveryReviewer?.reviewIfNeeded !== 'function') { const error = new Error('微信 Page Data 强制 Aider 审核服务不可用'); error.code = 'PAGE_DATA_REVIEW_UNAVAILABLE'; error.retryable = false; throw error; } const relativePaths = [ ...new Set( (Array.isArray(artifacts) ? artifacts : []) .map((artifact) => String(artifact?.relativePath ?? '').trim()) .filter(Boolean), ), ]; return pageDataDeliveryReviewer.reviewIfNeeded({ sourceChannel: 'wechat_mp', userId: user.userId, requestId, sessionId, sourceMessageId, originalUserText, relativePaths, requestStartedAt, forcePageData, }); }; const runIntentMessage = async ({ inbound, intent, user }) => { const wechatIntent = await resolveWechatIntent(intent); const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers); const reliabilityEnabled = isWechatMediaGrayUser(user, config.reliabilityGrayUsers); const agentReplyTimeoutMs = reliabilityEnabled ? config.agentReplyTimeoutMs : 0; const resetCandidate = intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : ''; const isPageDataRequest = isWechatPageDataTask(resetCandidate); const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent); const sessionPageContinuation = isWechatSessionPageContinuation(wechatIntent, resetCandidate); const imagePolicy = resolveWechatImageGenerationPolicy({ text: resetCandidate, isPageGenerate: wechatIntent.kind === 'page.generate', requireFreshPageThumbnail: config.requireFreshPageThumbnail && !sessionPageContinuation, }); // Page Data delivery owns persistent files, datasets and two publication // policies. Reusing a conversational route here can make a new request // inspect/retry unrelated historical pages from that session. const forceNew = shouldForceNewWechatAgentSession(wechatIntent, resetCandidate); let route = await ensureWechatAgentSession({ userId: user.userId, openid: inbound.fromUserName, userContext: user, forceNew, retainUnlinkedRoute: sessionPageContinuation, }); let sessionId = route.sessionId; await ensureSessionProvider(sessionId); if (wechatIntent.kind === 'session.reset') { await sendCustomerServiceText( inbound.fromUserName, WECHAT_SESSION_RESET_ACK_TEXT, user, ); return { sessionId }; } await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession }); let finished = false; let progressTimer = null; if (config.progressDelayMs > 0 && config.progressText) { progressTimer = setTimeout(() => { if (finished) return; sendCustomerServiceText(inbound.fromUserName, config.progressText, user).catch((err) => { logger.warn?.('WeChat MP progress reply failed:', err); }); }, config.progressDelayMs); progressTimer.unref?.(); } const requestId = crypto.randomUUID(); const guardScheduleReply = (replyText) => guardScheduleConfirmationReply({ replyText, scheduleService, userId: user.userId, sourceMessageId: intent.msgId, logger, }); try { const requestStartedAt = Date.now(); const agentPrompt = wechatIntent.kind === 'page.generate' ? buildPageGenerateAgentPrompt(intent, { wantsDocx: wechatIntent.wantsDocx, imagePolicy, preferImmediateContext: sessionPageContinuation, }) : buildWechatAgentPrompt(intent, { imagePolicy, preferSessionPageContinuation: sessionPageContinuation, }); const maxImmediateContextPageAttempts = wechatIntent.kind === 'page.generate' && sessionPageContinuation ? 2 : 1; let reply; let publishArtifacts = []; let confirmedArtifacts = []; let verifiedArtifacts = []; let generatedImages = []; let linkExistsForRequest = linkExists; for (let pageAttempt = 1; pageAttempt <= maxImmediateContextPageAttempts; pageAttempt += 1) { const activeAgentPrompt = pageAttempt === 1 ? agentPrompt : buildImmediateContextPageRepairPrompt(intent); const activeRequestId = pageAttempt === 1 ? requestId : crypto.randomUUID(); reply = await executeSessionReply( (pathname, init) => fetchForSession(sessionId, pathname, init), sessionId, activeRequestId, activeAgentPrompt, buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy, pgRequired: isPageDataRequest, }), { prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({ userId: user.userId, sessionId, userMessage, preserveAgentPrompt: sessionPageContinuation, }), submitReply: submitSessionReply ? ({ requestId: replyRequestId, userMessage }) => submitSessionReply({ userId: user.userId, sessionId, requestId: replyRequestId, userMessage, options: { requireHistoricalImageIsolation: true }, }) : null, timeoutMs: agentReplyTimeoutMs, }, ); generatedImages = collectWechatGeneratedImages(replyRequestMessages(reply)); if (imagePolicy.standaloneImageMode === 'required' && generatedImages.length === 0) { const error = new Error('图片生成没有获得本轮新的有效位图,请稍后重试'); error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED'; throw error; } const { publishedArtifacts, expectedArtifacts, recentArtifacts, confirmedArtifacts: resolvedConfirmedArtifacts, verifiedArtifacts: resolvedVerifiedArtifacts, validReplyUrls, hasValidReplyLink, } = await resolveHtmlPublishArtifacts({ reply, intent, requestStartedAt, userId: user.userId, sessionId, onPageGenerated, allowRecentArtifacts: htmlArtifactDeliveryExpected, htmlDeliveryAuthority, }); confirmedArtifacts = resolvedConfirmedArtifacts; verifiedArtifacts = resolvedVerifiedArtifacts; linkExistsForRequest = createPreparedPublicHtmlLinkExists({ userId: user.userId, prepared: { validReplyUrls, confirmedArtifacts, }, fallback: linkExists, }); const hasValidLinkInReply = hasValidReplyLink || await hasAnyValidPublishedHtmlLink( reply?.text, linkExistsForRequest, { confirmedArtifacts }, ); const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text); const suspiciousPublishClaim = expectedArtifacts.length === 0 && recentArtifacts.length === 0 && (await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest })); const htmlGenerationNeedsRetry = shouldRetryHtmlGenerationReply({ reply, intent, confirmedArtifacts, hasValidLinkInReply, }); const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent); publishArtifacts = htmlArtifactDeliveryExpected || publishedArtifacts.length > 0 ? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }) : []; if (wechatIntent.kind === 'page.generate') { const pageOutcome = resolvePageGenerateOutcome({ reply, confirmedArtifacts, verifiedArtifacts, suspiciousPublishClaim, bareCompletionReply, htmlGenerationNeedsRetry, replyHasPublicLinks, }); if (pageOutcome.action === 'session_retry') { throw new Error('stale_session_poisoned_completion'); } if (pageOutcome.action === 'fail') { if ( pageAttempt < maxImmediateContextPageAttempts && pageOutcome.reason === 'skill_or_stub' ) { logger.warn?.('WeChat MP immediate-context page repair retry:', { sessionId, userId: user.userId, pageAttempt, }); continue; } const text = pageOutcome.failureText ?? buildPagePublishFailureText(); try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP page generate failure notice failed:', sendErr); } throw markWechatUserNotified(new Error(text)); } publishArtifacts = pageOutcome.artifacts ?? publishArtifacts; break; } if (looksLikeHtmlGenerationIntent(intent?.agentText)) { if ( suspiciousPublishClaim || bareCompletionReply || (htmlGenerationNeedsRetry && replyHasPublicLinks && confirmedArtifacts.length === 0) ) { throw new Error('stale_session_poisoned_completion'); } if (htmlGenerationNeedsRetry) { const text = buildHtmlPublishFailureText(); try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP html publish failure notice failed:', sendErr); } throw markWechatUserNotified(new Error(text)); } } else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) { throw new Error('stale_session_poisoned_completion'); } break; } await reviewWechatPageDataArtifacts({ user, requestId, sessionId, sourceMessageId: intent.msgId, originalUserText: resetCandidate, requestStartedAt, artifacts: [...confirmedArtifacts, ...publishArtifacts], forcePageData: isPageDataRequest, }); if (wechatIntent.kind === 'page.generate') { const deliveryArtifacts = uniqueArtifactsByUrl([ ...publishArtifacts, ...confirmedArtifacts, ...verifiedArtifacts, ]); await enforceFreshPageThumbnailDelivery({ artifacts: deliveryArtifacts, reply, openid: inbound.fromUserName, user, userId: user.userId, sessionId, imagePolicy, notifyFailure: false, }); if (deliveryArtifacts.length > 0) { publishArtifacts = deliveryArtifacts; } else { const recovered = await resolveHtmlPublishArtifacts({ reply, intent, requestStartedAt, userId: user.userId, sessionId, onPageGenerated, allowRecentArtifacts: true, htmlDeliveryAuthority, }); publishArtifacts = uniqueArtifactsByUrl([ ...selectSendableHtmlArtifacts({ verifiedArtifacts: recovered.verifiedArtifacts, confirmedArtifacts: recovered.confirmedArtifacts, }), ...recovered.confirmedArtifacts, ]); } } const pageDataOutcome = await enforcePageDataCollectDelivery({ reply, intent, userId: user.userId, pageDataFinishGuard, publicBaseUrl: config.publicBaseUrl, requestStartedAt, notifyFailure: async (text) => { try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP page data failure notice failed:', sendErr); } }, }); if (pageDataOutcome?.deliveryArtifacts?.length) { publishArtifacts = uniqueArtifactsByUrl([ ...pageDataOutcome.deliveryArtifacts, ...publishArtifacts, ]); } if (reply.tokenState) { const tokenState = await resolveWechatBillingTokenState(sessionId, reply.tokenState); await userAuth.billSessionUsage(user.userId, sessionId, tokenState, requestId); } let finalizedReply = await maybeAttachPublishedHtmlLink(reply, { artifacts: publishArtifacts, allowAttachment: publishArtifacts.length > 0, }); if ( wechatIntent.kind !== 'page.generate' && imagePolicy.standaloneImageMode !== 'disabled' && generatedImages.length > 0 ) { try { await sendCustomerServiceImage(inbound.fromUserName, generatedImages[0]); } catch (sendErr) { logger.warn?.('WeChat MP generated image native delivery failed, falling back to link:', sendErr); finalizedReply = appendGeneratedImageFallbackLink(finalizedReply, generatedImages[0]); } } scheduleWechatSessionSnapshotRefresh(sessionId, user.userId); await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, { verifiedHtmlUrls: publishArtifacts.map((artifact) => artifact.url), linkExistsForRequest, }); return { sessionId }; } catch (err) { if ( err && typeof err === 'object' && sessionId && !err.wechatAgentSessionId ) { err.wechatAgentSessionId = sessionId; } const message = err instanceof Error ? err.message : String(err); if (err?.code === 'WECHAT_AGENT_REPLY_TIMEOUT') { await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => { logger.warn?.('WeChat MP timed-out session route clear failed:', clearErr); }); } // A Page Data request must fail closed. Retrying a poisoned completion in // another session while the finish guard is also active can turn one // request into repairs against historical pages. Drop only this user's // route so the next explicit request starts cleanly. if ( isPageDataRequest || String(err?.code ?? '').startsWith('PAGE_DATA_REVIEW_') ) { await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => { logger.warn?.('WeChat MP page data route clear failed:', clearErr); }); if (!err?.wechatUserNotified) { const text = buildPageDataCollectFailureText(); try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP page data failure notice failed:', sendErr); } } throw markWechatUserNotified(err instanceof Error ? err : new Error(message)); } const mayBeStaleSession = sessionId && (isRecoverableWechatAgentSessionError(message) || isWechatAgentApiErrorText(message)); if (mayBeStaleSession) { if (sessionPageContinuation) { await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => { logger.warn?.('WeChat MP contextual follow-up route clear failed:', clearErr); }); const text = '我没能可靠确认“做成页面”指的是哪段内容,已经停止沿用旧主题。请把要做成页面的主题或原文再发一次。'; try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP contextual follow-up notice failed:', sendErr); } const contextualError = markWechatUserNotified(new Error(text)); contextualError.wechatAgentSessionId = sessionId; throw contextualError; } route = await ensureWechatAgentSession({ userId: user.userId, openid: inbound.fromUserName, forceNew: true, userContext: user, }); sessionId = route.sessionId; await ensureSessionProvider(sessionId); await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession }); const retryId = crypto.randomUUID(); const retryStartedAt = Date.now(); const retryPrompt = wechatIntent.kind === 'page.generate' ? buildPageGenerateAgentPrompt(intent, { wantsDocx: wechatIntent.wantsDocx, imagePolicy, preferImmediateContext: sessionPageContinuation, }) : buildWechatAgentPrompt(intent, { imagePolicy, preferSessionPageContinuation: sessionPageContinuation, }); const reply = await executeSessionReply( (pathname, init) => fetchForSession(sessionId, pathname, init), sessionId, retryId, retryPrompt, buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy, pgRequired: isPageDataRequest, }), { prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({ userId: user.userId, sessionId, userMessage, preserveAgentPrompt: sessionPageContinuation, }), submitReply: submitSessionReply ? ({ requestId: replyRequestId, userMessage }) => submitSessionReply({ userId: user.userId, sessionId, requestId: replyRequestId, userMessage, options: { requireHistoricalImageIsolation: true }, }) : null, timeoutMs: agentReplyTimeoutMs, }, ); const generatedImages = collectWechatGeneratedImages(replyRequestMessages(reply)); if (imagePolicy.standaloneImageMode === 'required' && generatedImages.length === 0) { const error = new Error('图片生成没有获得本轮新的有效位图,请稍后重试'); error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED'; throw error; } const { publishedArtifacts, verifiedArtifacts, expectedArtifacts, recentArtifacts, confirmedArtifacts, validReplyUrls, hasValidReplyLink, } = await resolveHtmlPublishArtifacts({ reply, intent, requestStartedAt: retryStartedAt, userId: user.userId, sessionId, onPageGenerated, allowRecentArtifacts: htmlArtifactDeliveryExpected, htmlDeliveryAuthority, }); const linkExistsForRequest = createPreparedPublicHtmlLinkExists({ userId: user.userId, prepared: { validReplyUrls, confirmedArtifacts, }, fallback: linkExists, }); const hasValidLinkInReply = hasValidReplyLink || await hasAnyValidPublishedHtmlLink( reply?.text, linkExistsForRequest, { confirmedArtifacts }, ); const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text); const suspiciousPublishClaim = expectedArtifacts.length === 0 && recentArtifacts.length === 0 && (await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest })); const htmlGenerationNeedsRetry = shouldRetryHtmlGenerationReply({ reply, intent, confirmedArtifacts, hasValidLinkInReply, }); const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent); let publishArtifacts = htmlArtifactDeliveryExpected || publishedArtifacts.length > 0 ? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }) : []; if (wechatIntent.kind === 'page.generate') { const pageOutcome = resolvePageGenerateOutcome({ reply, confirmedArtifacts, verifiedArtifacts, suspiciousPublishClaim, bareCompletionReply, htmlGenerationNeedsRetry, replyHasPublicLinks, }); if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') { const text = pageOutcome.failureText ?? buildPagePublishFailureText(); try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP page generate retry failure notice failed:', sendErr); } throw markWechatUserNotified(new Error(text)); } publishArtifacts = pageOutcome.artifacts ?? publishArtifacts; } else if (looksLikeHtmlGenerationIntent(intent?.agentText)) { if ( suspiciousPublishClaim || bareCompletionReply || (htmlGenerationNeedsRetry && replyHasPublicLinks && confirmedArtifacts.length === 0) ) { throw new Error(buildHtmlPublishFailureText()); } if (htmlGenerationNeedsRetry) { const text = buildHtmlPublishFailureText(); try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP html publish failure notice failed:', sendErr); } throw markWechatUserNotified(new Error(text)); } } else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) { throw new Error(buildHtmlPublishFailureText()); } await reviewWechatPageDataArtifacts({ user, requestId: retryId, sessionId, sourceMessageId: intent.msgId, originalUserText: resetCandidate, requestStartedAt: retryStartedAt, artifacts: [...confirmedArtifacts, ...publishArtifacts], forcePageData: isPageDataRequest, }); if (wechatIntent.kind === 'page.generate') { const deliveryArtifacts = uniqueArtifactsByUrl([ ...publishArtifacts, ...confirmedArtifacts, ...verifiedArtifacts, ]); await enforceFreshPageThumbnailDelivery({ artifacts: deliveryArtifacts, reply, openid: inbound.fromUserName, user, userId: user.userId, sessionId, imagePolicy, }); if (deliveryArtifacts.length > 0) { publishArtifacts = deliveryArtifacts; } else { const recovered = await resolveHtmlPublishArtifacts({ reply, intent, requestStartedAt: retryStartedAt, userId: user.userId, sessionId, onPageGenerated, allowRecentArtifacts: true, htmlDeliveryAuthority, }); publishArtifacts = uniqueArtifactsByUrl([ ...selectSendableHtmlArtifacts({ verifiedArtifacts: recovered.verifiedArtifacts, confirmedArtifacts: recovered.confirmedArtifacts, }), ...recovered.confirmedArtifacts, ]); } } const pageDataOutcome = await enforcePageDataCollectDelivery({ reply, intent, userId: user.userId, pageDataFinishGuard, publicBaseUrl: config.publicBaseUrl, requestStartedAt: retryStartedAt, notifyFailure: async (text) => { try { await sendCustomerServiceText(inbound.fromUserName, text, user); } catch (sendErr) { logger.error?.('WeChat MP page data retry failure notice failed:', sendErr); } }, }); if (pageDataOutcome?.deliveryArtifacts?.length) { publishArtifacts = uniqueArtifactsByUrl([ ...pageDataOutcome.deliveryArtifacts, ...publishArtifacts, ]); } if (reply.tokenState) { const tokenState = await resolveWechatBillingTokenState(sessionId, reply.tokenState); await userAuth.billSessionUsage(user.userId, sessionId, tokenState, retryId); } let finalizedReply = await maybeAttachPublishedHtmlLink(reply, { artifacts: publishArtifacts, allowAttachment: publishArtifacts.length > 0, }); if ( wechatIntent.kind !== 'page.generate' && imagePolicy.standaloneImageMode !== 'disabled' && generatedImages.length > 0 ) { try { await sendCustomerServiceImage(inbound.fromUserName, generatedImages[0]); } catch (sendErr) { logger.warn?.('WeChat MP generated image native delivery failed, falling back to link:', sendErr); finalizedReply = appendGeneratedImageFallbackLink(finalizedReply, generatedImages[0]); } } scheduleWechatSessionSnapshotRefresh(sessionId, user.userId); await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, { verifiedHtmlUrls: publishArtifacts.map((artifact) => artifact.url), linkExistsForRequest, }); return { sessionId }; } throw err; } finally { finished = true; if (progressTimer) clearTimeout(progressTimer); } }; const resolveWechatIntent = async (intent) => { const wechatIntent = classifyWechatIntent(intent); if (intent.msgType !== 'text' && intent.msgType !== 'voice') return wechatIntent; const sessionActionResult = await resolveWechatSessionAction(intent.agentText, { semanticClassifier: sessionIntentClassifier, logger, }); if (isWechatSessionResetAction(sessionActionResult)) { intent.sessionAction = sessionActionResult.action; return { ...wechatIntent, kind: 'session.reset', sessionActionResult }; } if (sessionActionResult.action === 'ignore_previous') { intent.sessionAction = sessionActionResult.action; } return { ...wechatIntent, sessionActionResult }; }; const handleInboundMessage = async (bodyText, query = {}) => { if (!verifyRequest(query)) { return { ok: false, status: 403, body: 'invalid signature' }; } const inbound = parseWechatMessage(String(bodyText ?? '')); const rawXmlHash = crypto.createHash('sha1').update(inbound.rawXml || '').digest('hex'); const intent = normalizeWechatInboundIntent(inbound); intent.appId = config.appId; if (!inbound.fromUserName || !inbound.toUserName || !inbound.msgType) { return { ok: false, status: 400, body: 'invalid xml' }; } if (String(query.encrypt_type ?? '').toLowerCase() === 'aes' || inbound.encrypt) { return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '当前公众号回调需使用明文模式接入,请联系管理员切换后再试。', }), }; } if (inbound.msgType === 'event' && inbound.event === 'location') { await persistIntentDetail({ intent, rawXmlHash }); return successResponse(); } if (inbound.msgType === 'event' && inbound.event === 'subscribe') { await persistIntentDetail({ intent, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: `${config.unboundTextPrefix}\n${buildBindUrl()}`, }), }; } if (inbound.msgType === 'event') { await persistIntentDetail({ intent, rawXmlHash }); return successResponse(); } const supportedByConfig = (intent.msgType === 'voice' && config.acceptVoice) || (intent.msgType === 'image' && config.acceptImage) || (intent.msgType === 'file' && config.acceptFile) || (intent.msgType === 'location' && config.acceptLocation) || (intent.msgType === 'link' && config.acceptLink) || intent.msgType === 'text' || intent.msgType === 'video' || intent.msgType === 'shortvideo'; if (!supportedByConfig) { await persistIntentDetail({ intent, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: config.unsupportedText, }), }; } const boundUser = await userAuth.findWechatUserByOpenid(config.appId, inbound.fromUserName); if (!boundUser) { await persistIntentDetail({ intent, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: `${config.unboundTextPrefix}\n${buildBindUrl()}`, }), }; } if (boundUser.status === 'disabled') { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '当前账号不可用,请联系管理员处理。', }), }; } const mediaAnalysisEnabled = isWechatMediaGrayUser( boundUser, config.mediaAnalysisGrayUsers, ); attachRecentMediaForFollowup(inbound.fromUserName, intent, mediaAnalysisEnabled); if (intent.msgType === 'file' && !mediaAnalysisEnabled) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '当前账号尚未开启服务号文件分析灰度,请先通过 H5 上传文件。', }), }; } if (intent.msgType === 'voice' && !intent.agentText.trim() && intent.media?.mediaId) { try { const fallbackText = await transcribeWechatVoiceMedia(intent.media.mediaId, intent.media.format); if (fallbackText) { intent.agentText = fallbackText; intent.displayText = `语音:${fallbackText}`; } } catch (err) { logger.warn?.('WeChat MP voice ASR fallback failed:', err); } } if (intent.msgType === 'text' && !intent.agentText.trim()) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: config.unsupportedText, }), }; } if (intent.msgType === 'voice' && !intent.agentText.trim()) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '未识别到语音文字,请再说一次或输入文字。', }), }; } if (intent.msgType === 'image') { try { const accessToken = await getStableAccessToken(); const persisted = await persistWechatImage( { userId: boundUser.userId, appId: config.appId, openid: inbound.fromUserName, msgId: inbound.msgId, mediaId: inbound.mediaId, picUrl: inbound.picUrl, publicBaseUrl: config.mediaPublicBaseUrl, maxImageBytes: config.maxImageBytes, }, { wechatFetch, accessToken, }, ); intent.media = { ...intent.media, mediaId: inbound.mediaId || intent.media?.mediaId || '', picUrl: inbound.picUrl || intent.media?.picUrl || '', publicUrl: persisted.publicUrl, format: persisted.contentType, source: persisted.source, }; intent.agentText = `[图片1]: ${persisted.publicUrl}`; rememberRecentMedia(inbound.fromUserName, intent); } catch (error) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '图片处理失败,请稍后重试或直接发送文字说明。', }), }; } } if (intent.msgType === 'file') { try { const accessToken = await getStableAccessToken(); const persisted = await persistWechatAttachment( { userId: boundUser.userId, appId: config.appId, openid: inbound.fromUserName, msgId: inbound.msgId, mediaId: inbound.mediaId, filename: inbound.fileName, publicBaseUrl: config.mediaPublicBaseUrl, maxFileBytes: config.maxFileBytes, }, { wechatFetch, accessToken, }, ); intent.media = { ...intent.media, mediaId: inbound.mediaId || intent.media?.mediaId || '', publicUrl: persisted.publicUrl, format: persisted.contentType, source: persisted.source, }; intent.attachment = { ...intent.attachment, filename: persisted.filename, publicUrl: persisted.publicUrl, mimeType: persisted.contentType, sizeBytes: persisted.bytes, }; intent.agentText = `[文件1: ${persisted.filename}]: ${persisted.publicUrl}`; intent.displayText = `文件:${persisted.filename}`; rememberRecentMedia(inbound.fromUserName, intent); } catch (error) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: error instanceof Error ? error.message : '文件处理失败,请稍后重试。', }), }; } } if ( intent.msgType === 'location' && (intent.location?.latitude == null || intent.location?.longitude == null) ) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '这次位置字段不完整,请重新发送位置或直接输入地点。', }), }; } if (intent.msgType === 'link' && !intent.link?.url) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '这次没有拿到完整链接,请重新发送一次。', }), }; } if (intent.msgType === 'video' || intent.msgType === 'shortvideo') { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: '已收到视频,当前先支持文本、语音、图片、定位和链接。', }), }; } if (intent.msgType === 'text' || intent.msgType === 'voice') { const wechatIntent = classifyWechatIntent(intent); if (shouldHandleSyncReply(intent, wechatIntent)) { const syncReply = resolveSyncReply(wechatIntent, boundUser, { statusFallbackText: config.statusText, }); if (syncReply) { await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: syncReply, }), }; } } } if (inbound.msgId && typeof userAuth.recordWechatMpMessage === 'function') { const recorded = await userAuth.recordWechatMpMessage({ appId: config.appId, openid: inbound.fromUserName, msgId: inbound.msgId, }); if (!recorded?.inserted) { return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: buildStatusText(boundUser, config.statusText), }), }; } } await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); let disclosureDecision = null; try { disclosureDecision = systemDisclosurePolicyService?.evaluate?.({ text: intent.agentText, channel: 'wechat_mp', userId: boundUser.userId, sessionId: null, }) ?? null; } catch (err) { logger.warn?.( 'WeChat MP system disclosure policy evaluation failed open:', err instanceof Error ? err.message : err, ); } if (disclosureDecision?.enforced) { if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') { await userAuth.finishWechatMpMessage({ appId: config.appId, openid: inbound.fromUserName, msgId: inbound.msgId, status: 'done', agentSessionId: null, }); } logger.info?.('[system-disclosure-policy] blocked WeChat request', { userId: boundUser.userId, policyVersion: disclosureDecision.policyVersion, reasonCode: disclosureDecision.reasonCode, categories: disclosureDecision.categories, }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: disclosureDecision.responseText, }), }; } const scheduleReply = intent.msgType === 'text' || intent.msgType === 'voice' ? await handleWechatScheduleIntent({ intent, user: boundUser, scheduleService, wechatScheduleLlmConfigService, llmProviderService, logger, }) : null; if (scheduleReply) { 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: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: scheduleReply, }), }; } const task = enqueueMessageTask(inbound.fromUserName, () => runIntentMessage({ inbound, intent, user: boundUser, }), ) .then(async ({ sessionId } = {}) => { settleRecentMediaBatch(inbound.fromUserName, intent, { succeeded: true }); if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') { await userAuth.finishWechatMpMessage({ appId: config.appId, openid: inbound.fromUserName, msgId: inbound.msgId, status: 'done', agentSessionId: sessionId, }); } }) .catch(async (err) => { settleRecentMediaBatch(inbound.fromUserName, intent, { succeeded: false }); if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') { await userAuth.finishWechatMpMessage({ appId: config.appId, openid: inbound.fromUserName, msgId: inbound.msgId, status: 'failed', agentSessionId: err?.wechatAgentSessionId ?? null, }).catch(() => {}); } logger.error?.('WeChat MP background reply failed:', err); if (wasWechatUserNotified(err)) return; try { await sendCustomerServiceText( inbound.fromUserName, formatWechatAgentFailureMessage(err), boundUser, ); } catch (sendErr) { logger.error?.('WeChat MP failure notice failed:', sendErr); } }); return { ok: true, status: 200, contentType: 'application/xml; charset=utf-8', body: buildWechatTextReply({ toUserName: inbound.fromUserName, fromUserName: inbound.toUserName, content: buildAckText({ intent, nickname: resolveWechatAddressName(boundUser), config, fallbackText: config.ackText, }), }), task, }; }; const getRouteStatusForUser = async (userId) => { const openid = await userAuth.getWechatOpenidForUser(userId, config.appId); if (!openid) { return { enabled: true, bound: false, appId: config.appId, openid: null, agentSessionId: null, routeStatus: null, updatedAt: null, }; } const route = await userAuth.getWechatAgentRoute(config.appId, openid); return { enabled: true, bound: true, appId: config.appId, openid, agentSessionId: route?.agentSessionId ?? null, routeStatus: route?.status ?? null, updatedAt: route?.updatedAt ?? null, }; }; const recreateRouteForUser = async (userId) => { const openid = await userAuth.getWechatOpenidForUser(userId, config.appId); if (!openid) { return { ok: false, message: '当前账号尚未绑定该服务号', }; } const { sessionId } = await ensureWechatAgentSession({ userId, openid, forceNew: true, }); const route = await userAuth.getWechatAgentRoute(config.appId, openid); return { ok: true, route: { enabled: true, bound: true, appId: config.appId, openid, agentSessionId: sessionId, routeStatus: route?.status ?? 'active', updatedAt: route?.updatedAt ?? null, }, }; }; return { enabled: true, verifyRequest, verifyUrlChallenge, handleInboundMessage, getRouteStatusForUser, recreateRouteForUser, createJsSdkSignature, sendTextToUser, }; }