fix: tighten WeChat session rotation and defer snapshot refresh
Use session snapshot message counts alongside WeChat route counts so long-lived dedicated sessions rotate before Goose history bloats, and refresh snapshots asynchronously so customer replies are not blocked. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2274,6 +2274,18 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
return Number(rows?.[0]?.count ?? 0);
|
return Number(rows?.[0]?.count ?? 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getWechatAgentSessionSnapshotMessageCount = async (agentSessionId) => {
|
||||||
|
if (!agentSessionId) return 0;
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT synced_msg_count
|
||||||
|
FROM h5_session_snapshots
|
||||||
|
WHERE agent_session_id = ?
|
||||||
|
LIMIT 1`,
|
||||||
|
[agentSessionId],
|
||||||
|
);
|
||||||
|
return Number(rows?.[0]?.synced_msg_count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
const recordWechatMpMessage = async ({
|
const recordWechatMpMessage = async ({
|
||||||
appId,
|
appId,
|
||||||
openid,
|
openid,
|
||||||
@@ -2861,6 +2873,7 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
clearWechatAgentRoute,
|
clearWechatAgentRoute,
|
||||||
touchWechatAgentRoute,
|
touchWechatAgentRoute,
|
||||||
countWechatAgentSessionMessages,
|
countWechatAgentSessionMessages,
|
||||||
|
getWechatAgentSessionSnapshotMessageCount,
|
||||||
recordWechatMpMessage,
|
recordWechatMpMessage,
|
||||||
finishWechatMpMessage,
|
finishWechatMpMessage,
|
||||||
insertWechatMpMessageDetail,
|
insertWechatMpMessageDetail,
|
||||||
|
|||||||
+30
-8
@@ -1683,10 +1683,12 @@ export function createWechatMpService({
|
|||||||
config.sessionIdleRotateMs > 0 &&
|
config.sessionIdleRotateMs > 0 &&
|
||||||
routeUpdatedAt > 0 &&
|
routeUpdatedAt > 0 &&
|
||||||
now - routeUpdatedAt > config.sessionIdleRotateMs;
|
now - routeUpdatedAt > config.sessionIdleRotateMs;
|
||||||
const routeMessageCount =
|
let routeMessageCount = 0;
|
||||||
config.sessionMessageRotateCount > 0 &&
|
if (config.sessionMessageRotateCount > 0) {
|
||||||
typeof userAuth.countWechatAgentSessionMessages === 'function'
|
const counts = [];
|
||||||
? await userAuth
|
if (typeof userAuth.countWechatAgentSessionMessages === 'function') {
|
||||||
|
counts.push(
|
||||||
|
userAuth
|
||||||
.countWechatAgentSessionMessages({
|
.countWechatAgentSessionMessages({
|
||||||
appId: config.appId,
|
appId: config.appId,
|
||||||
openid,
|
openid,
|
||||||
@@ -1695,8 +1697,24 @@ export function createWechatMpService({
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.warn?.('WeChat MP route message count lookup failed:', err);
|
logger.warn?.('WeChat MP route message count lookup failed:', err);
|
||||||
return 0;
|
return 0;
|
||||||
})
|
}),
|
||||||
: 0;
|
);
|
||||||
|
}
|
||||||
|
if (typeof userAuth.getWechatAgentSessionSnapshotMessageCount === 'function') {
|
||||||
|
counts.push(
|
||||||
|
userAuth
|
||||||
|
.getWechatAgentSessionSnapshotMessageCount(existingRoute.agentSessionId)
|
||||||
|
.catch((err) => {
|
||||||
|
logger.warn?.('WeChat MP snapshot message count lookup failed:', err);
|
||||||
|
return 0;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (counts.length > 0) {
|
||||||
|
const resolved = await Promise.all(counts);
|
||||||
|
routeMessageCount = Math.max(0, ...resolved.map((value) => Number(value) || 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
const routeIsTooLong =
|
const routeIsTooLong =
|
||||||
config.sessionMessageRotateCount > 0 &&
|
config.sessionMessageRotateCount > 0 &&
|
||||||
Number(routeMessageCount ?? 0) >= config.sessionMessageRotateCount;
|
Number(routeMessageCount ?? 0) >= config.sessionMessageRotateCount;
|
||||||
@@ -1807,6 +1825,10 @@ export function createWechatMpService({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scheduleWechatSessionSnapshotRefresh = (sessionId, userId) => {
|
||||||
|
void refreshWechatSessionSnapshot(sessionId, userId);
|
||||||
|
};
|
||||||
|
|
||||||
const rememberWechatUserContext = async (sessionId, user, { forceBootstrap = false } = {}) => {
|
const rememberWechatUserContext = async (sessionId, user, { forceBootstrap = false } = {}) => {
|
||||||
const addressName = resolveWechatAddressName(user);
|
const addressName = resolveWechatAddressName(user);
|
||||||
if (!addressName) return;
|
if (!addressName) return;
|
||||||
@@ -1984,7 +2006,7 @@ export function createWechatMpService({
|
|||||||
publicBaseUrl: config.publicBaseUrl,
|
publicBaseUrl: config.publicBaseUrl,
|
||||||
artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts,
|
artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts,
|
||||||
});
|
});
|
||||||
await refreshWechatSessionSnapshot(sessionId, user.userId);
|
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||||
await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, {
|
await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, {
|
||||||
verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map(
|
verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map(
|
||||||
(artifact) => artifact.url,
|
(artifact) => artifact.url,
|
||||||
@@ -2053,7 +2075,7 @@ export function createWechatMpService({
|
|||||||
publicBaseUrl: config.publicBaseUrl,
|
publicBaseUrl: config.publicBaseUrl,
|
||||||
artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts,
|
artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts,
|
||||||
});
|
});
|
||||||
await refreshWechatSessionSnapshot(sessionId, user.userId);
|
scheduleWechatSessionSnapshotRefresh(sessionId, user.userId);
|
||||||
await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, {
|
await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, {
|
||||||
verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map(
|
verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map(
|
||||||
(artifact) => artifact.url,
|
(artifact) => artifact.url,
|
||||||
|
|||||||
@@ -1424,6 +1424,90 @@ test('wechat mp service rotates a dedicated session after the message-count thre
|
|||||||
assert.equal(routeCleared, true);
|
assert.equal(routeCleared, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('wechat mp service rotates a dedicated session when snapshot message count exceeds threshold', async () => {
|
||||||
|
const token = 'token';
|
||||||
|
const timestamp = '1710000000';
|
||||||
|
const nonce = 'nonce';
|
||||||
|
let routeCleared = false;
|
||||||
|
const service = createBoundWechatService({
|
||||||
|
token,
|
||||||
|
config: {
|
||||||
|
sessionIdleRotateMs: 0,
|
||||||
|
sessionMessageRotateCount: 200,
|
||||||
|
},
|
||||||
|
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-snapshot-rotate","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已换新会话。"}]}}\n\n',
|
||||||
|
'data: {"type":"Finish","request_id":"req-snapshot-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() {
|
||||||
|
return 3;
|
||||||
|
},
|
||||||
|
async getWechatAgentSessionSnapshotMessageCount() {
|
||||||
|
return 616;
|
||||||
|
},
|
||||||
|
async clearWechatAgentRoute() {
|
||||||
|
routeCleared = true;
|
||||||
|
},
|
||||||
|
async upsertWechatAgentRoute() {},
|
||||||
|
async finishWechatMpMessage() {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const originalRandomUuid = crypto.randomUUID;
|
||||||
|
crypto.randomUUID = () => 'req-snapshot-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.equal(routeCleared, true);
|
||||||
|
});
|
||||||
|
|
||||||
test('wechat mp service skips repeated user-context bootstrap for the same session', async () => {
|
test('wechat mp service skips repeated user-context bootstrap for the same session', async () => {
|
||||||
const token = 'token';
|
const token = 'token';
|
||||||
const timestamp = '1710000000';
|
const timestamp = '1710000000';
|
||||||
|
|||||||
Reference in New Issue
Block a user