fix: reconcile reused wechat agent sessions

This commit is contained in:
john
2026-06-27 18:33:53 +08:00
parent 58d824f5e5
commit f4e8bb51a1
2 changed files with 173 additions and 5 deletions
+22 -5
View File
@@ -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,
+151
View File
@@ -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';