fix: inject episodic memory into WeChat agent
Memind CI / Test, build, and release guards (pull_request) Successful in 3m24s

This commit is contained in:
john
2026-07-22 13:41:40 +08:00
parent 881f70f4bf
commit 5242cbf08b
3 changed files with 159 additions and 2 deletions
+1
View File
@@ -881,6 +881,7 @@ async function bootstrapUserAuth() {
scheduleService: process.env.H5_SCHEDULE_ENABLED === '1' ? scheduleService : null, scheduleService: process.env.H5_SCHEDULE_ENABLED === '1' ? scheduleService : null,
wechatScheduleLlmConfigService, wechatScheduleLlmConfigService,
llmProviderService, llmProviderService,
chatIntentRouter,
onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => { onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => {
for (const artifact of artifacts) { for (const artifact of artifacts) {
void sendMindSpaceAnalyticsEvent({ void sendMindSpaceAnalyticsEvent({
+59 -2
View File
@@ -235,7 +235,7 @@ async function executeSessionReply(
requestId, requestId,
prompt, prompt,
metadata = {}, metadata = {},
{ submitReply = null } = {}, { submitReply = null, prepareUserMessage = null } = {},
) { ) {
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, { const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
method: 'GET', method: 'GET',
@@ -246,7 +246,10 @@ async function executeSessionReply(
throw new Error(text || '无法建立公众号消息事件流'); throw new Error(text || '无法建立公众号消息事件流');
} }
const userMessage = createUserMessage(prompt, metadata); let userMessage = createUserMessage(prompt, metadata);
if (prepareUserMessage) {
userMessage = (await prepareUserMessage(userMessage)) ?? userMessage;
}
if (submitReply) { if (submitReply) {
await submitReply({ sessionId, requestId, userMessage }); await submitReply({ sessionId, requestId, userMessage });
} else { } else {
@@ -1504,6 +1507,7 @@ export function createWechatMpService({
scheduleService = null, scheduleService = null,
wechatScheduleLlmConfigService = null, wechatScheduleLlmConfigService = null,
llmProviderService = null, llmProviderService = null,
chatIntentRouter = null,
onPageGenerated = null, onPageGenerated = null,
applySessionLlmProvider = null, applySessionLlmProvider = null,
refreshSessionSnapshot = 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 runIntentMessage = async ({ inbound, intent, user }) => {
const wechatIntent = classifyWechatIntent(intent); const wechatIntent = classifyWechatIntent(intent);
const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers); const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers);
@@ -2294,6 +2341,11 @@ export function createWechatMpService({
agentPrompt, agentPrompt,
buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }), buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }),
{ {
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
userId: user.userId,
sessionId,
userMessage,
}),
submitReply: submitSessionReply submitReply: submitSessionReply
? ({ requestId: replyRequestId, userMessage }) => ? ({ requestId: replyRequestId, userMessage }) =>
submitSessionReply({ submitSessionReply({
@@ -2496,6 +2548,11 @@ export function createWechatMpService({
retryPrompt, retryPrompt,
buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }), buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }),
{ {
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
userId: user.userId,
sessionId,
userMessage,
}),
submitReply: submitSessionReply submitReply: submitSessionReply
? ({ requestId: replyRequestId, userMessage }) => ? ({ requestId: replyRequestId, userMessage }) =>
submitSessionReply({ submitSessionReply({
+99
View File
@@ -24,6 +24,7 @@ import {
decryptWechatMpPayload, decryptWechatMpPayload,
WECHAT_CUSTOMER_TEXT_MAX_BYTES, WECHAT_CUSTOMER_TEXT_MAX_BYTES,
} from './wechat-mp.mjs'; } from './wechat-mp.mjs';
import { createChatIntentRouter } from './chat-intent-router.mjs';
function signatureFor(token, timestamp, nonce) { function signatureFor(token, timestamp, nonce) {
return crypto return crypto
@@ -82,6 +83,7 @@ function createBoundWechatService({
scheduleService = null, scheduleService = null,
applySessionLlmProvider = null, applySessionLlmProvider = null,
submitSessionReply = null, submitSessionReply = null,
chatIntentRouter = null,
}) { }) {
return createWechatMpService({ return createWechatMpService({
config: { config: {
@@ -133,6 +135,7 @@ function createBoundWechatService({
startAgentSession, startAgentSession,
sessionApiFetch, sessionApiFetch,
submitSessionReply, submitSessionReply,
chatIntentRouter,
scheduleService, scheduleService,
applySessionLlmProvider, applySessionLlmProvider,
wechatFetch, 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\//); 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 () => { test('wechat mp serializes image and follow-up text and reattaches recent image', async () => {
const token = 'token'; const token = 'token';
const timestamp = '1710000000'; const timestamp = '1710000000';