fix: reconcile reused wechat agent sessions
This commit is contained in:
+22
-5
@@ -1050,8 +1050,30 @@ export function createWechatMpService({
|
||||
if (forceNew) {
|
||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
||||
}
|
||||
const workingDir = await userAuth.resolveWorkingDir(userId);
|
||||
const sessionPolicy = await userAuth.getAgentSessionPolicy(userId);
|
||||
const publishLayout = await userAuth.getUserPublishLayout(userId);
|
||||
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,
|
||||
sessionPolicy,
|
||||
sandboxConstraints: publishLayout?.constraints ?? null,
|
||||
userContext: publishLayout
|
||||
? {
|
||||
userId,
|
||||
displayName: addressName || publishLayout.displayName,
|
||||
username: addressName || null,
|
||||
slug: null,
|
||||
}
|
||||
: null,
|
||||
tolerateInvalidWorkingDir: true,
|
||||
},
|
||||
);
|
||||
return existingRoute.agentSessionId;
|
||||
}
|
||||
|
||||
@@ -1059,10 +1081,6 @@ export function createWechatMpService({
|
||||
if (!gate.ok) {
|
||||
throw new Error(gate.message || '当前用户无法使用聊天能力');
|
||||
}
|
||||
|
||||
const workingDir = await userAuth.resolveWorkingDir(userId);
|
||||
const sessionPolicy = await userAuth.getAgentSessionPolicy(userId);
|
||||
const publishLayout = await userAuth.getUserPublishLayout(userId);
|
||||
const started = await readJsonResponse(
|
||||
await apiFetch('/agent/start', {
|
||||
method: 'POST',
|
||||
@@ -1083,7 +1101,6 @@ export function createWechatMpService({
|
||||
// `/agent/start` already persists the owning user and goosed node via the
|
||||
// portal proxy. Re-registering here without the node can overwrite the
|
||||
// correct mapping back to node 0 in multi-goosed production.
|
||||
const addressName = resolveWechatAddressName(userContext);
|
||||
await reconcileAgentSession(
|
||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||
sessionId,
|
||||
|
||||
@@ -937,6 +937,157 @@ test('wechat mp service injects schedule skill instructions for reminder-like re
|
||||
}
|
||||
});
|
||||
|
||||
test('wechat mp service reconciles existing dedicated session before reply', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const calls = [];
|
||||
const workingDir = '/Users/john/Project/Memind/MindSpace/user-1';
|
||||
const extensionConfig = {
|
||||
type: 'stdio',
|
||||
name: 'sandbox-fs',
|
||||
cmd: '/usr/local/bin/node',
|
||||
args: ['/Users/john/Project/Memind/mindspace-sandbox-mcp.mjs', workingDir],
|
||||
envs: { SANDBOX_ROOT: workingDir },
|
||||
available_tools: ['list_dir'],
|
||||
};
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
enabled: true,
|
||||
appId: 'wx123',
|
||||
appSecret: 'secret',
|
||||
token,
|
||||
publicBaseUrl: 'https://example.com',
|
||||
bindPath: '/auth/wechat/authorize?intent=login',
|
||||
ackText: 'ack',
|
||||
unsupportedText: 'unsupported',
|
||||
unboundTextPrefix: '请先绑定',
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||||
},
|
||||
async getWechatAgentRoute() {
|
||||
return { agentSessionId: 'session-1' };
|
||||
},
|
||||
async clearWechatAgentRoute() {},
|
||||
async canUseChat() {
|
||||
return { ok: true };
|
||||
},
|
||||
async resolveWorkingDir() {
|
||||
return workingDir;
|
||||
},
|
||||
async getAgentSessionPolicy() {
|
||||
return {
|
||||
enableContextMemory: false,
|
||||
extensionOverrides: [extensionConfig],
|
||||
unrestricted: false,
|
||||
gooseMode: 'auto',
|
||||
};
|
||||
},
|
||||
async getUserPublishLayout() {
|
||||
return { displayName: 'John', username: 'john', slug: 'john', constraints: 'base' };
|
||||
},
|
||||
async registerAgentSession() {},
|
||||
async upsertWechatAgentRoute() {},
|
||||
async billSessionUsage() {},
|
||||
},
|
||||
apiFetch: async (pathname, init = {}) => {
|
||||
calls.push(['fallback', pathname, init.method ?? 'GET']);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
},
|
||||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||||
assert.equal(sessionId, 'session-1');
|
||||
calls.push([pathname, init.method ?? 'GET']);
|
||||
if (pathname === '/sessions/session-1') {
|
||||
return new Response(JSON.stringify({ working_dir: '/root/tkmind_go/ui/h5/MindSpace/user-1' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (pathname === '/agent/update_working_dir') {
|
||||
const body = JSON.parse(init.body);
|
||||
assert.equal(body.session_id, 'session-1');
|
||||
assert.equal(body.working_dir, workingDir);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/agent/update_session') {
|
||||
const body = JSON.parse(init.body);
|
||||
assert.equal(body.session_id, 'session-1');
|
||||
assert.equal(body.goose_mode, 'auto');
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/sessions/session-1/extensions') {
|
||||
return new Response(JSON.stringify({ extensions: [] }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (pathname === '/agent/add_extension') {
|
||||
const body = JSON.parse(init.body);
|
||||
assert.equal(body.session_id, 'session-1');
|
||||
assert.equal(body.config.name, 'sandbox-fs');
|
||||
assert.equal(body.config.args[1], workingDir);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/agent/restart') {
|
||||
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') {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/sessions/session-1/events') {
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","request_id":"req-1","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-1","token_state":{"inputTokens":1,"outputTokens":1}}\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' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${pathname}`);
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
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-1';
|
||||
try {
|
||||
const result = await service.handleInboundMessage(inboundXml({ content: '继续' }), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
await result.task;
|
||||
assert.equal(calls.some(([pathname]) => pathname === '/agent/update_working_dir'), true);
|
||||
assert.equal(calls.some(([pathname]) => pathname === '/agent/add_extension'), true);
|
||||
assert.equal(calls.some(([pathname]) => pathname === '/sessions/session-1/reply'), true);
|
||||
} finally {
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
}
|
||||
});
|
||||
|
||||
test('wechat mp service blocks schedule confirmation when no schedule item was written', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
|
||||
Reference in New Issue
Block a user