fix wechat mp session rotation and bootstrap reuse

This commit is contained in:
john
2026-07-04 12:10:22 +08:00
parent 3139395b58
commit bef72b2768
3 changed files with 352 additions and 46 deletions
+237 -1
View File
@@ -61,6 +61,7 @@ function createBoundWechatService({
token = 'token',
wechatFetch,
sessionApiFetch,
startAgentSession = null,
userAuth = {},
config = {},
scheduleService = null,
@@ -112,6 +113,7 @@ function createBoundWechatService({
async insertWechatMpMessageDetail() {},
...userAuth,
},
startAgentSession,
sessionApiFetch,
scheduleService,
applySessionLlmProvider,
@@ -1016,7 +1018,7 @@ test('wechat mp service routes text to dedicated session and sends customer serv
if (pathname === '/agent/harness_bootstrap') {
const body = JSON.parse(init.body);
assert.equal(body.sessionId, 'session-1');
assert.equal(body.force, true);
assert.equal(body.force, false);
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
throw new Error(`unexpected api path: ${pathname}`);
@@ -1252,6 +1254,240 @@ test('wechat mp service applies provider before forwarding to dedicated session'
assert.deepEqual(appliedSessions, ['session-1']);
});
test('wechat mp service rotates an idle dedicated session before replying', async () => {
const token = 'token';
const timestamp = '1710000000';
const nonce = 'nonce';
const clearedRoutes = [];
const upsertedSessions = [];
const sessionApiCalls = [];
let routeCleared = false;
const service = createBoundWechatService({
token,
config: {
sessionIdleRotateMs: 1000,
},
startAgentSession: async () => ({ id: 'session-2' }),
sessionApiFetch: async (sessionId, pathname, init = {}) => {
sessionApiCalls.push([sessionId, pathname, init.body ?? null]);
assert.equal(sessionId, 'session-2');
if (pathname === '/sessions/session-2/events') {
return new Response(
[
'data: {"type":"Message","request_id":"req-rotate","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到,新的会话已接上。"}]}}\n\n',
'data: {"type":"Finish","request_id":"req-rotate","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
].join(''),
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
);
}
if (pathname === '/sessions/session-2/reply') {
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
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}`);
},
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}`);
},
userAuth: {
async getWechatAgentRoute() {
if (routeCleared) return null;
return {
agentSessionId: 'session-1',
status: 'active',
updatedAt: Date.now() - 60_000,
};
},
async clearWechatAgentRoute(appId, openid) {
routeCleared = true;
clearedRoutes.push([appId, openid]);
},
async upsertWechatAgentRoute({ agentSessionId }) {
upsertedSessions.push(agentSessionId);
},
async finishWechatMpMessage() {},
},
});
const originalRandomUuid = crypto.randomUUID;
crypto.randomUUID = () => 'req-rotate';
try {
const result = await service.handleInboundMessage(inboundXml({ content: '继续' }), {
timestamp,
nonce,
signature: signatureFor(token, timestamp, nonce),
});
assert.equal(result.status, 200);
await result.task;
} finally {
crypto.randomUUID = originalRandomUuid;
}
assert.deepEqual(clearedRoutes, [['wx123', 'openid-1']]);
assert.deepEqual(upsertedSessions, ['session-2']);
assert.ok(sessionApiCalls.some(([sessionId, pathname]) => sessionId === 'session-2' && pathname === '/sessions/session-2/reply'));
});
test('wechat mp service rotates a dedicated session after the message-count threshold', async () => {
const token = 'token';
const timestamp = '1710000000';
const nonce = 'nonce';
let routeCleared = false;
const countedSessions = [];
const service = createBoundWechatService({
token,
config: {
sessionIdleRotateMs: 0,
sessionMessageRotateCount: 2,
},
startAgentSession: async () => ({ id: 'session-2' }),
sessionApiFetch: async (sessionId, pathname) => {
assert.equal(sessionId, 'session-2');
if (pathname === '/sessions/session-2/events') {
return new Response(
[
'data: {"type":"Message","request_id":"req-count-rotate","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已换新会话。"}]}}\n\n',
'data: {"type":"Finish","request_id":"req-count-rotate","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
].join(''),
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
);
}
if (pathname === '/sessions/session-2/reply') {
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
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}`);
},
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}`);
},
userAuth: {
async getWechatAgentRoute() {
if (routeCleared) return null;
return {
agentSessionId: 'session-1',
status: 'active',
updatedAt: Date.now(),
};
},
async countWechatAgentSessionMessages({ agentSessionId }) {
countedSessions.push(agentSessionId);
return 2;
},
async clearWechatAgentRoute() {
routeCleared = true;
},
async upsertWechatAgentRoute() {},
async finishWechatMpMessage() {},
},
});
const originalRandomUuid = crypto.randomUUID;
crypto.randomUUID = () => 'req-count-rotate';
try {
const result = await service.handleInboundMessage(inboundXml({ content: '继续' }), {
timestamp,
nonce,
signature: signatureFor(token, timestamp, nonce),
});
assert.equal(result.status, 200);
await result.task;
} finally {
crypto.randomUUID = originalRandomUuid;
}
assert.deepEqual(countedSessions, ['session-1']);
assert.equal(routeCleared, true);
});
test('wechat mp service skips repeated user-context bootstrap for the same session', async () => {
const token = 'token';
const timestamp = '1710000000';
const nonce = 'nonce';
const bootstrapBodies = [];
const service = createBoundWechatService({
token,
sessionApiFetch: async (sessionId, pathname, init = {}) => {
assert.equal(sessionId, 'session-1');
if (pathname === '/sessions/session-1/events') {
return new Response(
[
'data: {"type":"Message","request_id":"req-bootstrap","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到。"}]}}\n\n',
'data: {"type":"Finish","request_id":"req-bootstrap","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
].join(''),
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
);
}
if (pathname === '/sessions/session-1/reply') {
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
if (pathname === '/agent/harness_remember') {
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
if (pathname === '/agent/harness_bootstrap') {
bootstrapBodies.push(JSON.parse(String(init.body ?? '{}')));
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
}
throw new Error(`unexpected api path: ${pathname}`);
},
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 originalRandomUuid = crypto.randomUUID;
crypto.randomUUID = () => 'req-bootstrap';
try {
for (const content of ['第一条', '第二条']) {
const result = await service.handleInboundMessage(inboundXml({ content }), {
timestamp,
nonce,
signature: signatureFor(token, timestamp, nonce),
});
assert.equal(result.status, 200);
await result.task;
}
} finally {
crypto.randomUUID = originalRandomUuid;
}
assert.deepEqual(bootstrapBodies, [{ sessionId: 'session-1', force: false }]);
});
test('wechat mp service injects static page publish instructions for html generation requests', async () => {
const token = 'token';
const timestamp = '1710000000';