fix wechat mp session rotation and bootstrap reuse
This commit is contained in:
@@ -2259,6 +2259,21 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const countWechatAgentSessionMessages = async ({
|
||||||
|
appId,
|
||||||
|
openid,
|
||||||
|
agentSessionId,
|
||||||
|
}) => {
|
||||||
|
if (!appId || !openid || !agentSessionId) return 0;
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT COUNT(*) AS count
|
||||||
|
FROM h5_wechat_mp_messages
|
||||||
|
WHERE app_id = ? AND openid = ? AND agent_session_id = ?`,
|
||||||
|
[appId, openid, agentSessionId],
|
||||||
|
);
|
||||||
|
return Number(rows?.[0]?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
const recordWechatMpMessage = async ({
|
const recordWechatMpMessage = async ({
|
||||||
appId,
|
appId,
|
||||||
openid,
|
openid,
|
||||||
@@ -2845,6 +2860,7 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
upsertWechatAgentRoute,
|
upsertWechatAgentRoute,
|
||||||
clearWechatAgentRoute,
|
clearWechatAgentRoute,
|
||||||
touchWechatAgentRoute,
|
touchWechatAgentRoute,
|
||||||
|
countWechatAgentSessionMessages,
|
||||||
recordWechatMpMessage,
|
recordWechatMpMessage,
|
||||||
finishWechatMpMessage,
|
finishWechatMpMessage,
|
||||||
insertWechatMpMessageDetail,
|
insertWechatMpMessageDetail,
|
||||||
|
|||||||
+99
-45
@@ -26,6 +26,7 @@ const DEFAULT_UNSUPPORTED_TEXT = '当前先支持文字消息,你可以直接
|
|||||||
const DEFAULT_UNBOUND_TEXT = '先点这里完成绑定,再继续和专属 Agent 对话:';
|
const DEFAULT_UNBOUND_TEXT = '先点这里完成绑定,再继续和专属 Agent 对话:';
|
||||||
const DEFAULT_PROGRESS_DELAY_MS = 8000;
|
const DEFAULT_PROGRESS_DELAY_MS = 8000;
|
||||||
const DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1000;
|
const DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1000;
|
||||||
|
const DEFAULT_SESSION_MESSAGE_ROTATE_COUNT = 200;
|
||||||
const PUBLIC_HTML_LINK_PATTERN =
|
const PUBLIC_HTML_LINK_PATTERN =
|
||||||
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
|
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
|
||||||
const SESSION_WORKSPACE_TOOL_NAMES = new Set([
|
const SESSION_WORKSPACE_TOOL_NAMES = new Set([
|
||||||
@@ -1268,6 +1269,10 @@ export function loadWechatMpConfig(env = process.env) {
|
|||||||
0,
|
0,
|
||||||
Number(env.H5_WECHAT_MP_SESSION_IDLE_ROTATE_MS ?? DEFAULT_SESSION_IDLE_ROTATE_MS),
|
Number(env.H5_WECHAT_MP_SESSION_IDLE_ROTATE_MS ?? DEFAULT_SESSION_IDLE_ROTATE_MS),
|
||||||
),
|
),
|
||||||
|
sessionMessageRotateCount: Math.max(
|
||||||
|
0,
|
||||||
|
Number(env.H5_WECHAT_MP_SESSION_MESSAGE_ROTATE_COUNT ?? DEFAULT_SESSION_MESSAGE_ROTATE_COUNT),
|
||||||
|
),
|
||||||
statusText: env.H5_WECHAT_MP_STATUS_TEXT?.trim() || DEFAULT_STATUS_TEXT,
|
statusText: env.H5_WECHAT_MP_STATUS_TEXT?.trim() || DEFAULT_STATUS_TEXT,
|
||||||
unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_TEXT,
|
unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_TEXT,
|
||||||
unboundTextPrefix: env.H5_WECHAT_MP_UNBOUND_TEXT_PREFIX?.trim() || DEFAULT_UNBOUND_TEXT,
|
unboundTextPrefix: env.H5_WECHAT_MP_UNBOUND_TEXT_PREFIX?.trim() || DEFAULT_UNBOUND_TEXT,
|
||||||
@@ -1423,6 +1428,14 @@ export function createWechatMpService({
|
|||||||
jsapiTicketUrl: config.jsapiTicketUrl || DEFAULT_WECHAT_JSAPI_TICKET_URL,
|
jsapiTicketUrl: config.jsapiTicketUrl || DEFAULT_WECHAT_JSAPI_TICKET_URL,
|
||||||
progressText: config.progressText ?? DEFAULT_PROGRESS_TEXT,
|
progressText: config.progressText ?? DEFAULT_PROGRESS_TEXT,
|
||||||
progressDelayMs: Math.max(0, Number(config.progressDelayMs ?? DEFAULT_PROGRESS_DELAY_MS)),
|
progressDelayMs: Math.max(0, Number(config.progressDelayMs ?? DEFAULT_PROGRESS_DELAY_MS)),
|
||||||
|
sessionIdleRotateMs: Math.max(
|
||||||
|
0,
|
||||||
|
Number(config.sessionIdleRotateMs ?? DEFAULT_SESSION_IDLE_ROTATE_MS),
|
||||||
|
),
|
||||||
|
sessionMessageRotateCount: Math.max(
|
||||||
|
0,
|
||||||
|
Number(config.sessionMessageRotateCount ?? DEFAULT_SESSION_MESSAGE_ROTATE_COUNT),
|
||||||
|
),
|
||||||
statusText: config.statusText || DEFAULT_STATUS_TEXT,
|
statusText: config.statusText || DEFAULT_STATUS_TEXT,
|
||||||
mediaPublicBaseUrl: config.mediaPublicBaseUrl || config.publicBaseUrl,
|
mediaPublicBaseUrl: config.mediaPublicBaseUrl || config.publicBaseUrl,
|
||||||
maxImageBytes: Math.max(1, Number(config.maxImageBytes ?? 10 * 1024 * 1024)),
|
maxImageBytes: Math.max(1, Number(config.maxImageBytes ?? 10 * 1024 * 1024)),
|
||||||
@@ -1437,6 +1450,7 @@ export function createWechatMpService({
|
|||||||
token: null,
|
token: null,
|
||||||
expiresAt: 0,
|
expiresAt: 0,
|
||||||
};
|
};
|
||||||
|
const rememberedWechatContexts = new Map();
|
||||||
let jsapiTicketCache = {
|
let jsapiTicketCache = {
|
||||||
ticket: null,
|
ticket: null,
|
||||||
expiresAt: 0,
|
expiresAt: 0,
|
||||||
@@ -1663,33 +1677,67 @@ export function createWechatMpService({
|
|||||||
const addressName = resolveWechatAddressName(userContext);
|
const addressName = resolveWechatAddressName(userContext);
|
||||||
const existingRoute = await userAuth.getWechatAgentRoute(config.appId, openid);
|
const existingRoute = await userAuth.getWechatAgentRoute(config.appId, openid);
|
||||||
if (existingRoute?.agentSessionId) {
|
if (existingRoute?.agentSessionId) {
|
||||||
await reconcileAgentSession(
|
const now = Date.now();
|
||||||
(pathname, init) => fetchForSession(existingRoute.agentSessionId, pathname, init),
|
const routeUpdatedAt = Number(existingRoute.updatedAt ?? 0);
|
||||||
existingRoute.agentSessionId,
|
const routeIsIdle =
|
||||||
{
|
config.sessionIdleRotateMs > 0 &&
|
||||||
workingDir,
|
routeUpdatedAt > 0 &&
|
||||||
|
now - routeUpdatedAt > config.sessionIdleRotateMs;
|
||||||
|
const routeMessageCount =
|
||||||
|
config.sessionMessageRotateCount > 0 &&
|
||||||
|
typeof userAuth.countWechatAgentSessionMessages === 'function'
|
||||||
|
? await userAuth
|
||||||
|
.countWechatAgentSessionMessages({
|
||||||
|
appId: config.appId,
|
||||||
|
openid,
|
||||||
|
agentSessionId: existingRoute.agentSessionId,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.warn?.('WeChat MP route message count lookup failed:', err);
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
: 0;
|
||||||
|
const routeIsTooLong =
|
||||||
|
config.sessionMessageRotateCount > 0 &&
|
||||||
|
Number(routeMessageCount ?? 0) >= config.sessionMessageRotateCount;
|
||||||
|
if (routeIsIdle || routeIsTooLong) {
|
||||||
|
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||||
|
rememberedWechatContexts.delete(existingRoute.agentSessionId);
|
||||||
|
} else {
|
||||||
|
await reconcileAgentSession(
|
||||||
|
(pathname, init) => fetchForSession(existingRoute.agentSessionId, pathname, init),
|
||||||
|
existingRoute.agentSessionId,
|
||||||
|
{
|
||||||
|
workingDir,
|
||||||
|
sessionPolicy,
|
||||||
|
sandboxConstraints: publishLayout?.constraints ?? null,
|
||||||
|
userContext: publishLayout
|
||||||
|
? {
|
||||||
|
userId,
|
||||||
|
displayName: addressName || publishLayout.displayName,
|
||||||
|
username: addressName || null,
|
||||||
|
slug: null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
tolerateInvalidWorkingDir: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const routeHasTools = await sessionHasRequiredTools(
|
||||||
|
fetchForSession,
|
||||||
|
existingRoute.agentSessionId,
|
||||||
sessionPolicy,
|
sessionPolicy,
|
||||||
sandboxConstraints: publishLayout?.constraints ?? null,
|
).catch(() => false);
|
||||||
userContext: publishLayout
|
if (routeHasTools) {
|
||||||
? {
|
if (typeof userAuth.touchWechatAgentRoute === 'function') {
|
||||||
userId,
|
await userAuth.touchWechatAgentRoute(config.appId, openid, now).catch((err) => {
|
||||||
displayName: addressName || publishLayout.displayName,
|
logger.warn?.('WeChat MP route touch failed:', err);
|
||||||
username: addressName || null,
|
});
|
||||||
slug: null,
|
}
|
||||||
}
|
return { sessionId: existingRoute.agentSessionId, isNewSession: false };
|
||||||
: null,
|
}
|
||||||
tolerateInvalidWorkingDir: true,
|
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||||
},
|
rememberedWechatContexts.delete(existingRoute.agentSessionId);
|
||||||
);
|
|
||||||
const routeHasTools = await sessionHasRequiredTools(
|
|
||||||
fetchForSession,
|
|
||||||
existingRoute.agentSessionId,
|
|
||||||
sessionPolicy,
|
|
||||||
).catch(() => false);
|
|
||||||
if (routeHasTools) {
|
|
||||||
return existingRoute.agentSessionId;
|
|
||||||
}
|
}
|
||||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
|
||||||
} else if (existingRoute?.status === 'disabled') {
|
} else if (existingRoute?.status === 'disabled') {
|
||||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||||
}
|
}
|
||||||
@@ -1727,18 +1775,18 @@ export function createWechatMpService({
|
|||||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||||
sessionId,
|
sessionId,
|
||||||
{
|
{
|
||||||
workingDir,
|
workingDir,
|
||||||
sessionPolicy,
|
sessionPolicy,
|
||||||
sandboxConstraints: publishLayout?.constraints ?? null,
|
sandboxConstraints: publishLayout?.constraints ?? null,
|
||||||
userContext: publishLayout
|
userContext: publishLayout
|
||||||
? {
|
? {
|
||||||
userId,
|
userId,
|
||||||
displayName: addressName || publishLayout.displayName,
|
displayName: addressName || publishLayout.displayName,
|
||||||
username: addressName || null,
|
username: addressName || null,
|
||||||
slug: null,
|
slug: null,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
tolerateInvalidWorkingDir: true,
|
tolerateInvalidWorkingDir: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await userAuth.upsertWechatAgentRoute({
|
await userAuth.upsertWechatAgentRoute({
|
||||||
@@ -1747,7 +1795,7 @@ export function createWechatMpService({
|
|||||||
openid,
|
openid,
|
||||||
agentSessionId: sessionId,
|
agentSessionId: sessionId,
|
||||||
});
|
});
|
||||||
return sessionId;
|
return { sessionId, isNewSession: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshWechatSessionSnapshot = async (sessionId, userId) => {
|
const refreshWechatSessionSnapshot = async (sessionId, userId) => {
|
||||||
@@ -1759,9 +1807,12 @@ export function createWechatMpService({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rememberWechatUserContext = async (sessionId, user) => {
|
const rememberWechatUserContext = async (sessionId, user, { forceBootstrap = false } = {}) => {
|
||||||
const addressName = resolveWechatAddressName(user);
|
const addressName = resolveWechatAddressName(user);
|
||||||
if (!addressName) return;
|
if (!addressName) return;
|
||||||
|
const cacheKey = `${sessionId}:${addressName}`;
|
||||||
|
const alreadyRemembered = rememberedWechatContexts.get(sessionId) === cacheKey;
|
||||||
|
if (alreadyRemembered && !forceBootstrap) return;
|
||||||
try {
|
try {
|
||||||
await readJsonResponse(
|
await readJsonResponse(
|
||||||
await fetchForSession(sessionId, '/agent/harness_remember', {
|
await fetchForSession(sessionId, '/agent/harness_remember', {
|
||||||
@@ -1780,9 +1831,10 @@ export function createWechatMpService({
|
|||||||
await readJsonResponse(
|
await readJsonResponse(
|
||||||
await fetchForSession(sessionId, '/agent/harness_bootstrap', {
|
await fetchForSession(sessionId, '/agent/harness_bootstrap', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ sessionId, force: true }),
|
body: JSON.stringify({ sessionId, force: Boolean(forceBootstrap) }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
rememberedWechatContexts.set(sessionId, cacheKey);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn?.('WeChat MP user context remember failed:', err);
|
logger.warn?.('WeChat MP user context remember failed:', err);
|
||||||
}
|
}
|
||||||
@@ -1839,15 +1891,16 @@ export function createWechatMpService({
|
|||||||
const resetCandidate =
|
const resetCandidate =
|
||||||
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
||||||
const forceNew = isTopicResetIntent(resetCandidate);
|
const forceNew = isTopicResetIntent(resetCandidate);
|
||||||
let sessionId = await ensureWechatAgentSession({
|
let route = await ensureWechatAgentSession({
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
openid: inbound.fromUserName,
|
openid: inbound.fromUserName,
|
||||||
userContext: user,
|
userContext: user,
|
||||||
forceNew,
|
forceNew,
|
||||||
});
|
});
|
||||||
|
let sessionId = route.sessionId;
|
||||||
const publishLayout = await userAuth.getUserPublishLayout(user.userId);
|
const publishLayout = await userAuth.getUserPublishLayout(user.userId);
|
||||||
await ensureSessionProvider(sessionId);
|
await ensureSessionProvider(sessionId);
|
||||||
await rememberWechatUserContext(sessionId, user);
|
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||||
let finished = false;
|
let finished = false;
|
||||||
let progressTimer = null;
|
let progressTimer = null;
|
||||||
if (config.progressDelayMs > 0 && config.progressText) {
|
if (config.progressDelayMs > 0 && config.progressText) {
|
||||||
@@ -1943,14 +1996,15 @@ export function createWechatMpService({
|
|||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
const mayBeStaleSession = sessionId && isRecoverableWechatAgentSessionError(message);
|
const mayBeStaleSession = sessionId && isRecoverableWechatAgentSessionError(message);
|
||||||
if (mayBeStaleSession) {
|
if (mayBeStaleSession) {
|
||||||
sessionId = await ensureWechatAgentSession({
|
route = await ensureWechatAgentSession({
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
openid: inbound.fromUserName,
|
openid: inbound.fromUserName,
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
userContext: user,
|
userContext: user,
|
||||||
});
|
});
|
||||||
|
sessionId = route.sessionId;
|
||||||
await ensureSessionProvider(sessionId);
|
await ensureSessionProvider(sessionId);
|
||||||
await rememberWechatUserContext(sessionId, user);
|
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||||
const retryId = crypto.randomUUID();
|
const retryId = crypto.randomUUID();
|
||||||
const retryStartedAt = Date.now();
|
const retryStartedAt = Date.now();
|
||||||
const reply = await executeSessionReply(
|
const reply = await executeSessionReply(
|
||||||
@@ -2484,7 +2538,7 @@ export function createWechatMpService({
|
|||||||
message: '当前账号尚未绑定该服务号',
|
message: '当前账号尚未绑定该服务号',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const sessionId = await ensureWechatAgentSession({
|
const { sessionId } = await ensureWechatAgentSession({
|
||||||
userId,
|
userId,
|
||||||
openid,
|
openid,
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
|
|||||||
+237
-1
@@ -61,6 +61,7 @@ function createBoundWechatService({
|
|||||||
token = 'token',
|
token = 'token',
|
||||||
wechatFetch,
|
wechatFetch,
|
||||||
sessionApiFetch,
|
sessionApiFetch,
|
||||||
|
startAgentSession = null,
|
||||||
userAuth = {},
|
userAuth = {},
|
||||||
config = {},
|
config = {},
|
||||||
scheduleService = null,
|
scheduleService = null,
|
||||||
@@ -112,6 +113,7 @@ function createBoundWechatService({
|
|||||||
async insertWechatMpMessageDetail() {},
|
async insertWechatMpMessageDetail() {},
|
||||||
...userAuth,
|
...userAuth,
|
||||||
},
|
},
|
||||||
|
startAgentSession,
|
||||||
sessionApiFetch,
|
sessionApiFetch,
|
||||||
scheduleService,
|
scheduleService,
|
||||||
applySessionLlmProvider,
|
applySessionLlmProvider,
|
||||||
@@ -1016,7 +1018,7 @@ test('wechat mp service routes text to dedicated session and sends customer serv
|
|||||||
if (pathname === '/agent/harness_bootstrap') {
|
if (pathname === '/agent/harness_bootstrap') {
|
||||||
const body = JSON.parse(init.body);
|
const body = JSON.parse(init.body);
|
||||||
assert.equal(body.sessionId, 'session-1');
|
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' } });
|
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||||
}
|
}
|
||||||
throw new Error(`unexpected api path: ${pathname}`);
|
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']);
|
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 () => {
|
test('wechat mp service injects static page publish instructions for html generation requests', async () => {
|
||||||
const token = 'token';
|
const token = 'token';
|
||||||
const timestamp = '1710000000';
|
const timestamp = '1710000000';
|
||||||
|
|||||||
Reference in New Issue
Block a user