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 ({
|
||||
appId,
|
||||
openid,
|
||||
@@ -2845,6 +2860,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
upsertWechatAgentRoute,
|
||||
clearWechatAgentRoute,
|
||||
touchWechatAgentRoute,
|
||||
countWechatAgentSessionMessages,
|
||||
recordWechatMpMessage,
|
||||
finishWechatMpMessage,
|
||||
insertWechatMpMessageDetail,
|
||||
|
||||
+99
-45
@@ -26,6 +26,7 @@ const DEFAULT_UNSUPPORTED_TEXT = '当前先支持文字消息,你可以直接
|
||||
const DEFAULT_UNBOUND_TEXT = '先点这里完成绑定,再继续和专属 Agent 对话:';
|
||||
const DEFAULT_PROGRESS_DELAY_MS = 8000;
|
||||
const DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1000;
|
||||
const DEFAULT_SESSION_MESSAGE_ROTATE_COUNT = 200;
|
||||
const PUBLIC_HTML_LINK_PATTERN =
|
||||
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi;
|
||||
const SESSION_WORKSPACE_TOOL_NAMES = new Set([
|
||||
@@ -1268,6 +1269,10 @@ export function loadWechatMpConfig(env = process.env) {
|
||||
0,
|
||||
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,
|
||||
unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_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,
|
||||
progressText: config.progressText ?? DEFAULT_PROGRESS_TEXT,
|
||||
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,
|
||||
mediaPublicBaseUrl: config.mediaPublicBaseUrl || config.publicBaseUrl,
|
||||
maxImageBytes: Math.max(1, Number(config.maxImageBytes ?? 10 * 1024 * 1024)),
|
||||
@@ -1437,6 +1450,7 @@ export function createWechatMpService({
|
||||
token: null,
|
||||
expiresAt: 0,
|
||||
};
|
||||
const rememberedWechatContexts = new Map();
|
||||
let jsapiTicketCache = {
|
||||
ticket: null,
|
||||
expiresAt: 0,
|
||||
@@ -1663,33 +1677,67 @@ export function createWechatMpService({
|
||||
const addressName = resolveWechatAddressName(userContext);
|
||||
const existingRoute = await userAuth.getWechatAgentRoute(config.appId, openid);
|
||||
if (existingRoute?.agentSessionId) {
|
||||
await reconcileAgentSession(
|
||||
(pathname, init) => fetchForSession(existingRoute.agentSessionId, pathname, init),
|
||||
existingRoute.agentSessionId,
|
||||
{
|
||||
workingDir,
|
||||
const now = Date.now();
|
||||
const routeUpdatedAt = Number(existingRoute.updatedAt ?? 0);
|
||||
const routeIsIdle =
|
||||
config.sessionIdleRotateMs > 0 &&
|
||||
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,
|
||||
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,
|
||||
).catch(() => false);
|
||||
if (routeHasTools) {
|
||||
return existingRoute.agentSessionId;
|
||||
).catch(() => false);
|
||||
if (routeHasTools) {
|
||||
if (typeof userAuth.touchWechatAgentRoute === 'function') {
|
||||
await userAuth.touchWechatAgentRoute(config.appId, openid, now).catch((err) => {
|
||||
logger.warn?.('WeChat MP route touch failed:', err);
|
||||
});
|
||||
}
|
||||
return { sessionId: existingRoute.agentSessionId, isNewSession: false };
|
||||
}
|
||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||
rememberedWechatContexts.delete(existingRoute.agentSessionId);
|
||||
}
|
||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||
} else if (existingRoute?.status === 'disabled') {
|
||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||
}
|
||||
@@ -1727,18 +1775,18 @@ export function createWechatMpService({
|
||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||
sessionId,
|
||||
{
|
||||
workingDir,
|
||||
sessionPolicy,
|
||||
sandboxConstraints: publishLayout?.constraints ?? null,
|
||||
userContext: publishLayout
|
||||
? {
|
||||
userId,
|
||||
displayName: addressName || publishLayout.displayName,
|
||||
username: addressName || null,
|
||||
slug: null,
|
||||
}
|
||||
: null,
|
||||
tolerateInvalidWorkingDir: true,
|
||||
workingDir,
|
||||
sessionPolicy,
|
||||
sandboxConstraints: publishLayout?.constraints ?? null,
|
||||
userContext: publishLayout
|
||||
? {
|
||||
userId,
|
||||
displayName: addressName || publishLayout.displayName,
|
||||
username: addressName || null,
|
||||
slug: null,
|
||||
}
|
||||
: null,
|
||||
tolerateInvalidWorkingDir: true,
|
||||
},
|
||||
);
|
||||
await userAuth.upsertWechatAgentRoute({
|
||||
@@ -1747,7 +1795,7 @@ export function createWechatMpService({
|
||||
openid,
|
||||
agentSessionId: sessionId,
|
||||
});
|
||||
return sessionId;
|
||||
return { sessionId, isNewSession: true };
|
||||
};
|
||||
|
||||
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);
|
||||
if (!addressName) return;
|
||||
const cacheKey = `${sessionId}:${addressName}`;
|
||||
const alreadyRemembered = rememberedWechatContexts.get(sessionId) === cacheKey;
|
||||
if (alreadyRemembered && !forceBootstrap) return;
|
||||
try {
|
||||
await readJsonResponse(
|
||||
await fetchForSession(sessionId, '/agent/harness_remember', {
|
||||
@@ -1780,9 +1831,10 @@ export function createWechatMpService({
|
||||
await readJsonResponse(
|
||||
await fetchForSession(sessionId, '/agent/harness_bootstrap', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sessionId, force: true }),
|
||||
body: JSON.stringify({ sessionId, force: Boolean(forceBootstrap) }),
|
||||
}),
|
||||
);
|
||||
rememberedWechatContexts.set(sessionId, cacheKey);
|
||||
} catch (err) {
|
||||
logger.warn?.('WeChat MP user context remember failed:', err);
|
||||
}
|
||||
@@ -1839,15 +1891,16 @@ export function createWechatMpService({
|
||||
const resetCandidate =
|
||||
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
||||
const forceNew = isTopicResetIntent(resetCandidate);
|
||||
let sessionId = await ensureWechatAgentSession({
|
||||
let route = await ensureWechatAgentSession({
|
||||
userId: user.userId,
|
||||
openid: inbound.fromUserName,
|
||||
userContext: user,
|
||||
forceNew,
|
||||
});
|
||||
let sessionId = route.sessionId;
|
||||
const publishLayout = await userAuth.getUserPublishLayout(user.userId);
|
||||
await ensureSessionProvider(sessionId);
|
||||
await rememberWechatUserContext(sessionId, user);
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
let finished = false;
|
||||
let progressTimer = null;
|
||||
if (config.progressDelayMs > 0 && config.progressText) {
|
||||
@@ -1943,14 +1996,15 @@ export function createWechatMpService({
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const mayBeStaleSession = sessionId && isRecoverableWechatAgentSessionError(message);
|
||||
if (mayBeStaleSession) {
|
||||
sessionId = await ensureWechatAgentSession({
|
||||
route = await ensureWechatAgentSession({
|
||||
userId: user.userId,
|
||||
openid: inbound.fromUserName,
|
||||
forceNew: true,
|
||||
userContext: user,
|
||||
});
|
||||
sessionId = route.sessionId;
|
||||
await ensureSessionProvider(sessionId);
|
||||
await rememberWechatUserContext(sessionId, user);
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
const retryId = crypto.randomUUID();
|
||||
const retryStartedAt = Date.now();
|
||||
const reply = await executeSessionReply(
|
||||
@@ -2484,7 +2538,7 @@ export function createWechatMpService({
|
||||
message: '当前账号尚未绑定该服务号',
|
||||
};
|
||||
}
|
||||
const sessionId = await ensureWechatAgentSession({
|
||||
const { sessionId } = await ensureWechatAgentSession({
|
||||
userId,
|
||||
openid,
|
||||
forceNew: true,
|
||||
|
||||
+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