fix wechat mp session rotation and bootstrap reuse
This commit is contained in:
+237
-1
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user