diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 74f05a4..529b409 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -1984,6 +1984,7 @@ export function createWechatMpService({ userId, openid, forceNew = false, + retainUnlinkedRoute = false, userContext = null, }) => { if (forceNew) { @@ -2045,8 +2046,10 @@ export function createWechatMpService({ snapshotMessageCount: routeSnapshotMessageCount, now, }); - if (routeIsIdle || routeIsTooLong || routeHasUnlinkedConversation) { - if (routeHasUnlinkedConversation) { + const shouldRotateUnlinkedRoute = + routeHasUnlinkedConversation && !retainUnlinkedRoute; + if (routeIsIdle || routeIsTooLong || shouldRotateUnlinkedRoute) { + if (shouldRotateUnlinkedRoute) { logger.warn?.('WeChat MP orphaned route rotated before reuse:', { agentSessionId: existingRoute.agentSessionId, snapshotMessageCount: routeSnapshotMessageCount, @@ -2439,6 +2442,7 @@ export function createWechatMpService({ openid: inbound.fromUserName, userContext: user, forceNew, + retainUnlinkedRoute: immediateContextFollowup, }); let sessionId = route.sessionId; await ensureSessionProvider(sessionId); diff --git a/wechat-mp.test.mjs b/wechat-mp.test.mjs index 61511b3..c6278df 100644 --- a/wechat-mp.test.mjs +++ b/wechat-mp.test.mjs @@ -358,6 +358,73 @@ test('WeChat immediate-context page guard does not match complete page requests' assert.doesNotMatch(ordinaryPrompt, /即时上下文优先/); }); +test('WeChat immediate-context page retains an unlinked route with adjacent snapshot', async () => { + const clearedRoutes = []; + const startedSessions = []; + let activeRoute = { + agentSessionId: 'session-poem', + updatedAt: Date.now() - 120_000, + }; + const service = createBoundWechatService({ + config: { + sessionMessageRotateCount: 0, + sessionIdleRotateMs: 0, + }, + userAuth: { + async getWechatAgentRoute() { + return activeRoute; + }, + async clearWechatAgentRoute(_appId, openid) { + clearedRoutes.push(openid); + activeRoute = null; + }, + async countWechatAgentSessionMessages() { + return 0; + }, + async getWechatAgentSessionSnapshotMessageCount() { + return 5; + }, + async touchWechatAgentRoute(_appId, _openid, updatedAt) { + activeRoute = { ...activeRoute, updatedAt }; + }, + }, + startAgentSession: async (sessionId) => { + startedSessions.push(sessionId); + return sessionId; + }, + sessionApiFetch: async (pathname) => { + if (String(pathname).includes('/tools')) { + return new Response(JSON.stringify({ tools: [] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + return new Response(JSON.stringify({ conversation: [] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }, + submitSessionReply: async () => ({ + ok: true, + conversation: [{ role: 'assistant', content: [{ type: 'text', text: 'ok' }] }], + }), + }); + + const timestamp = '1710000000'; + const nonce = 'nonce-immediate-context-route'; + await service.handleInboundMessage( + inboundXml({ content: '把刚才的诗做成页面' }), + { + timestamp, + nonce, + signature: signatureFor('token', timestamp, nonce), + }, + ); + + assert.equal(clearedRoutes.length, 0); + assert.equal(startedSessions.length, 0); +}); + test('WeChat only rotates an aged route with conversation but no linked inbound message', () => { const now = 200_000; assert.equal(