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
+99
View File
@@ -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';