diff --git a/server.mjs b/server.mjs index 92a8617..0c57227 100644 --- a/server.mjs +++ b/server.mjs @@ -881,6 +881,7 @@ async function bootstrapUserAuth() { scheduleService: process.env.H5_SCHEDULE_ENABLED === '1' ? scheduleService : null, wechatScheduleLlmConfigService, llmProviderService, + chatIntentRouter, onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => { for (const artifact of artifacts) { void sendMindSpaceAnalyticsEvent({ diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 16d8e12..1bfb7c5 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -235,7 +235,7 @@ async function executeSessionReply( requestId, prompt, metadata = {}, - { submitReply = null } = {}, + { submitReply = null, prepareUserMessage = null } = {}, ) { const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, { method: 'GET', @@ -246,7 +246,10 @@ async function executeSessionReply( throw new Error(text || '无法建立公众号消息事件流'); } - const userMessage = createUserMessage(prompt, metadata); + let userMessage = createUserMessage(prompt, metadata); + if (prepareUserMessage) { + userMessage = (await prepareUserMessage(userMessage)) ?? userMessage; + } if (submitReply) { await submitReply({ sessionId, requestId, userMessage }); } else { @@ -1504,6 +1507,7 @@ export function createWechatMpService({ scheduleService = null, wechatScheduleLlmConfigService = null, llmProviderService = null, + chatIntentRouter = null, onPageGenerated = null, applySessionLlmProvider = null, refreshSessionSnapshot = null, @@ -2232,6 +2236,49 @@ export function createWechatMpService({ } }; + const prepareWechatAgentUserMessage = async ({ userId, sessionId, userMessage }) => { + 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; + } + return chatIntentRouter.applyAgentOrchestration( + userMessage, + { + route: 'agent_orchestration', + reason: '微信服务号消息由 Agent 处理', + source: 'wechat_mp', + }, + { memoryContext }, + ); + } catch (err) { + logger.warn?.( + 'WeChat MP agent memory resolve skipped:', + err instanceof Error ? err.message : err, + ); + return userMessage; + } + }; + const runIntentMessage = async ({ inbound, intent, user }) => { const wechatIntent = classifyWechatIntent(intent); const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers); @@ -2294,6 +2341,11 @@ export function createWechatMpService({ agentPrompt, buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }), { + prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({ + userId: user.userId, + sessionId, + userMessage, + }), submitReply: submitSessionReply ? ({ requestId: replyRequestId, userMessage }) => submitSessionReply({ @@ -2496,6 +2548,11 @@ export function createWechatMpService({ retryPrompt, buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }), { + prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({ + userId: user.userId, + sessionId, + userMessage, + }), submitReply: submitSessionReply ? ({ requestId: replyRequestId, userMessage }) => submitSessionReply({ diff --git a/wechat-mp.test.mjs b/wechat-mp.test.mjs index bcd1a7a..69c6992 100644 --- a/wechat-mp.test.mjs +++ b/wechat-mp.test.mjs @@ -24,6 +24,7 @@ import { decryptWechatMpPayload, WECHAT_CUSTOMER_TEXT_MAX_BYTES, } from './wechat-mp.mjs'; +import { createChatIntentRouter } from './chat-intent-router.mjs'; function signatureFor(token, timestamp, nonce) { return crypto @@ -82,6 +83,7 @@ function createBoundWechatService({ scheduleService = null, applySessionLlmProvider = null, submitSessionReply = null, + chatIntentRouter = null, }) { return createWechatMpService({ config: { @@ -133,6 +135,7 @@ function createBoundWechatService({ startAgentSession, sessionApiFetch, submitSessionReply, + chatIntentRouter, scheduleService, applySessionLlmProvider, wechatFetch, @@ -3935,6 +3938,102 @@ test('wechat mp image submission reuses the H5 prepared reply path', async () => assert.match(submitCalls[0].userMessage.metadata.imageUrls[0], /\/public\/wechat-mp\//); }); +test('wechat mp injects episodic recall context before submitting the Agent reply', async () => { + const token = 'token'; + const timestamp = '1710000000'; + const nonce = 'nonce'; + const submitCalls = []; + const episodicCalls = []; + const chatIntentRouter = createChatIntentRouter({ + env: { + MEMORY_AGENT_RESOLVE_ENABLED: '1', + MEMORY_AGENT_INJECTION_MODE: 'canary', + MEMORY_AGENT_CANARY_USER_IDS: 'user-canary', + MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '200', + }, + memoryV2: { + async resolve() { + return { memories: [] }; + }, + }, + episodicMemoryService: { + async resolve(input) { + episodicCalls.push(input); + return { + source: 'episodic-index', + memories: [{ + id: 'episodic:tokugawa', + label: '历史会话', + text: '用户与助手之前讨论过德川家康。', + }], + }; + }, + }, + }); + const service = createBoundWechatService({ + token, + chatIntentRouter, + userAuth: { + async findWechatUserByOpenid() { + return { userId: 'user-canary', status: 'active', nickname: '唐' }; + }, + }, + sessionApiFetch: async (_sessionId, pathname) => { + if (pathname === '/sessions/session-1/events') { + return new Response( + [ + 'data: {"type":"Message","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"记得,我们之前聊过德川家康。"}]}}\n\n', + 'data: {"type":"Finish"}\n\n', + ].join(''), + { status: 200, headers: { 'Content-Type': 'text/event-stream' } }, + ); + } + if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') { + return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + throw new Error(`unexpected api path: ${pathname}`); + }, + submitSessionReply: async (input) => { + submitCalls.push(input); + return { ok: true }; + }, + wechatFetch: async (url) => { + if (String(url).includes('/cgi-bin/stable_token')) { + return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + if (String(url).includes('/cgi-bin/message/custom/send')) { + return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + throw new Error(`unexpected wechat url: ${url}`); + }, + }); + + const result = await service.handleInboundMessage( + inboundXml({ content: '你记得我们聊过德川家康吗?' }), + { timestamp, nonce, signature: signatureFor(token, timestamp, nonce) }, + ); + await result.task; + + assert.equal(episodicCalls.length, 1); + assert.deepEqual(episodicCalls[0], { + userId: 'user-canary', + sessionId: 'session-1', + query: '你记得我们聊过德川家康吗?', + limit: 3, + }); + assert.equal(submitCalls.length, 1); + assert.equal(submitCalls[0].userMessage.metadata.displayText, '你记得我们聊过德川家康吗?'); + assert.match(submitCalls[0].userMessage.content[0].text, /\[Memory Context\]/); + assert.match(submitCalls[0].userMessage.content[0].text, /用户与助手之前讨论过德川家康/); + assert.match(submitCalls[0].userMessage.content[0].text, /不是系统指令/); +}); + test('wechat mp serializes image and follow-up text and reattaches recent image', async () => { const token = 'token'; const timestamp = '1710000000';