fix: reconcile reused wechat agent sessions
This commit is contained in:
+22
-5
@@ -1050,8 +1050,30 @@ export function createWechatMpService({
|
|||||||
if (forceNew) {
|
if (forceNew) {
|
||||||
await userAuth.clearWechatAgentRoute(config.appId, openid);
|
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);
|
const existingRoute = await userAuth.getWechatAgentRoute(config.appId, openid);
|
||||||
if (existingRoute?.agentSessionId) {
|
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;
|
return existingRoute.agentSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1059,10 +1081,6 @@ export function createWechatMpService({
|
|||||||
if (!gate.ok) {
|
if (!gate.ok) {
|
||||||
throw new Error(gate.message || '当前用户无法使用聊天能力');
|
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(
|
const started = await readJsonResponse(
|
||||||
await apiFetch('/agent/start', {
|
await apiFetch('/agent/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1083,7 +1101,6 @@ export function createWechatMpService({
|
|||||||
// `/agent/start` already persists the owning user and goosed node via the
|
// `/agent/start` already persists the owning user and goosed node via the
|
||||||
// portal proxy. Re-registering here without the node can overwrite the
|
// portal proxy. Re-registering here without the node can overwrite the
|
||||||
// correct mapping back to node 0 in multi-goosed production.
|
// correct mapping back to node 0 in multi-goosed production.
|
||||||
const addressName = resolveWechatAddressName(userContext);
|
|
||||||
await reconcileAgentSession(
|
await reconcileAgentSession(
|
||||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||||
sessionId,
|
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 () => {
|
test('wechat mp service blocks schedule confirmation when no schedule item was written', async () => {
|
||||||
const token = 'token';
|
const token = 'token';
|
||||||
const timestamp = '1710000000';
|
const timestamp = '1710000000';
|
||||||
|
|||||||
Reference in New Issue
Block a user