Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,682 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildWechatTextReply,
|
||||
createWechatMpService,
|
||||
loadWechatMpConfig,
|
||||
splitWechatText,
|
||||
verifyWechatMpSignature,
|
||||
WECHAT_CUSTOMER_TEXT_MAX_BYTES,
|
||||
} from './wechat-mp.mjs';
|
||||
|
||||
function signatureFor(token, timestamp, nonce) {
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
.update([token, timestamp, nonce].sort().join(''))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function inboundXml({
|
||||
toUserName = 'gh_test',
|
||||
fromUserName = 'openid-1',
|
||||
msgType = 'text',
|
||||
content = 'hello',
|
||||
event = '',
|
||||
} = {}) {
|
||||
return [
|
||||
'<xml>',
|
||||
`<ToUserName><![CDATA[${toUserName}]]></ToUserName>`,
|
||||
`<FromUserName><![CDATA[${fromUserName}]]></FromUserName>`,
|
||||
'<CreateTime>1710000000</CreateTime>',
|
||||
`<MsgType><![CDATA[${msgType}]]></MsgType>`,
|
||||
content ? `<Content><![CDATA[${content}]]></Content>` : '',
|
||||
event ? `<Event><![CDATA[${event}]]></Event>` : '',
|
||||
'<MsgId>10001</MsgId>',
|
||||
'</xml>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
test('splitWechatText respects WeChat 2048-byte customer service limit', () => {
|
||||
const longChinese = '中'.repeat(900);
|
||||
const chunks = splitWechatText(longChinese);
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(Buffer.byteLength(chunk, 'utf8') <= WECHAT_CUSTOMER_TEXT_MAX_BYTES);
|
||||
}
|
||||
assert.equal(chunks.join(''), longChinese);
|
||||
});
|
||||
|
||||
test('wechat mp service splits long agent replies into multiple customer messages', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const longReply = '答'.repeat(900);
|
||||
const wechatCalls = [];
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
enabled: true,
|
||||
appId: 'wx123',
|
||||
appSecret: 'secret',
|
||||
token,
|
||||
publicBaseUrl: 'https://example.com',
|
||||
bindPath: '/auth/wechat/authorize?intent=login',
|
||||
ackText: 'ack',
|
||||
progressDelayMs: 0,
|
||||
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 '/tmp/user-1';
|
||||
},
|
||||
async getAgentSessionPolicy() {
|
||||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||||
},
|
||||
async getUserPublishLayout() {
|
||||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||||
},
|
||||
async registerAgentSession() {},
|
||||
async upsertWechatAgentRoute() {},
|
||||
async billSessionUsage() {},
|
||||
},
|
||||
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-long","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":${JSON.stringify(longReply)}}]}}\n\n`,
|
||||
'data: {"type":"Finish","request_id":"req-long","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' || pathname === '/agent/harness_bootstrap') {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${pathname}`);
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
wechatCalls.push([url, init.method ?? 'GET', init.body ?? null]);
|
||||
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')) {
|
||||
const payload = JSON.parse(init.body);
|
||||
assert.ok(Buffer.byteLength(payload.text.content, 'utf8') <= WECHAT_CUSTOMER_TEXT_MAX_BYTES);
|
||||
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-long';
|
||||
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;
|
||||
}
|
||||
|
||||
const sendCalls = wechatCalls.filter(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||||
assert.ok(sendCalls.length > 1);
|
||||
const combined = sendCalls
|
||||
.map(([, , body]) => JSON.parse(body).text.content)
|
||||
.join('');
|
||||
assert.equal(combined, longReply);
|
||||
});
|
||||
|
||||
test('loadWechatMpConfig requires full config and enable flag', () => {
|
||||
const config = loadWechatMpConfig({
|
||||
H5_WECHAT_MP_ENABLED: '1',
|
||||
H5_WECHAT_MP_APP_ID: 'wx123',
|
||||
H5_WECHAT_MP_APP_SECRET: 'secret',
|
||||
H5_WECHAT_MP_TOKEN: 'token',
|
||||
H5_PUBLIC_BASE_URL: 'https://example.com',
|
||||
});
|
||||
assert.equal(config.enabled, true);
|
||||
assert.equal(config.bindPath, '/auth/wechat/authorize?intent=login');
|
||||
});
|
||||
|
||||
test('verifyWechatMpSignature accepts valid signature', () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
assert.equal(
|
||||
verifyWechatMpSignature({
|
||||
token,
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('buildWechatTextReply includes both parties and content', () => {
|
||||
const xml = buildWechatTextReply({
|
||||
toUserName: 'openid-1',
|
||||
fromUserName: 'gh_test',
|
||||
content: '收到',
|
||||
createTime: 123,
|
||||
});
|
||||
assert.match(xml, /<ToUserName><!\[CDATA\[openid-1\]\]><\/ToUserName>/);
|
||||
assert.match(xml, /<Content><!\[CDATA\[收到\]\]><\/Content>/);
|
||||
});
|
||||
|
||||
test('wechat mp service prompts binding for unknown openid', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
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 null;
|
||||
},
|
||||
},
|
||||
apiFetch: async () => new Response('{}', { status: 200 }),
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(inboundXml(), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /请先绑定/);
|
||||
assert.match(result.body, /https:\/\/example.com\/auth\/wechat\/authorize\?intent=login/);
|
||||
});
|
||||
|
||||
test('wechat mp service routes text to dedicated session and sends customer service reply', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const apiCalls = [];
|
||||
const wechatCalls = [];
|
||||
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 '/tmp/user-1';
|
||||
},
|
||||
async getAgentSessionPolicy() {
|
||||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||||
},
|
||||
async getUserPublishLayout() {
|
||||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||||
},
|
||||
async registerAgentSession() {},
|
||||
async upsertWechatAgentRoute() {},
|
||||
async billSessionUsage(userId, sessionId, tokenState, requestId) {
|
||||
apiCalls.push(['bill', userId, sessionId, tokenState, requestId]);
|
||||
},
|
||||
},
|
||||
apiFetch: async (pathname, init = {}) => {
|
||||
apiCalls.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');
|
||||
apiCalls.push([pathname, init.method ?? 'GET']);
|
||||
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":"wx_abcd1234,页面已创建完毕!\\n[标题](https://example.com/page.html)\\n\\n| 项 | 值 |\\n| --- | --- |\\n| A | B |"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-1","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname === '/sessions/session-1/reply') {
|
||||
const body = JSON.parse(init.body);
|
||||
assert.equal(body.user_message.content[0].text, '【微信服务号新消息】请只回答下面这条用户消息,不要主动延续无关的历史话题。\n若用户只是在测试连通性,请一句话确认收到即可,不要展开旧任务。\n\n用户消息:帮我做个页面');
|
||||
body.request_id && (apiCalls[apiCalls.length - 1][2] = body.request_id);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/agent/harness_remember') {
|
||||
const body = JSON.parse(init.body);
|
||||
assert.equal(body.sessionId, 'session-1');
|
||||
assert.match(body.content, /当前服务号用户名称/);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/agent/harness_bootstrap') {
|
||||
const body = JSON.parse(init.body);
|
||||
assert.equal(body.sessionId, 'session-1');
|
||||
assert.equal(body.force, true);
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${pathname}`);
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
wechatCalls.push([url, init.method ?? 'GET', init.body ?? null]);
|
||||
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')) {
|
||||
const payload = JSON.parse(init.body);
|
||||
assert.equal(payload.touser, 'openid-1');
|
||||
assert.match(payload.text.content, /页面已创建完毕/);
|
||||
assert.doesNotMatch(payload.text.content, /^wx_/);
|
||||
assert.doesNotMatch(payload.text.content, /\[标题\]\(/);
|
||||
assert.match(payload.text.content, /标题\nhttps:\/\/example.com\/page.html/);
|
||||
assert.match(payload.text.content, /项:A;值:B/);
|
||||
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;
|
||||
let uuidCalls = 0;
|
||||
crypto.randomUUID = () => {
|
||||
uuidCalls += 1;
|
||||
return uuidCalls === 1 ? 'req-1' : `uuid-${uuidCalls}`;
|
||||
};
|
||||
try {
|
||||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我做个页面' }), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /ack/);
|
||||
await result.task;
|
||||
} finally {
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
apiCalls.some(([pathname]) => pathname === '/sessions/session-1/reply'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
apiCalls.some(([kind]) => kind === 'fallback'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
wechatCalls.some(([url]) => String(url).includes('/cgi-bin/message/custom/send')),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('wechat mp service creates daily todo digest without routing to agent', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const apiCalls = [];
|
||||
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 recordWechatMpMessage() {
|
||||
return { inserted: true };
|
||||
},
|
||||
async finishWechatMpMessage(input) {
|
||||
apiCalls.push(['finish', input.status, input.agentSessionId]);
|
||||
},
|
||||
},
|
||||
scheduleService: {
|
||||
async createDailyTodoDigest(input) {
|
||||
apiCalls.push(['digest', input.userId, input.hour, input.minute, input.channel]);
|
||||
return {
|
||||
id: 'sub-1',
|
||||
userId: input.userId,
|
||||
hour: input.hour,
|
||||
minute: input.minute,
|
||||
timezone: input.timezone,
|
||||
channel: input.channel,
|
||||
};
|
||||
},
|
||||
},
|
||||
apiFetch: async (pathname) => {
|
||||
apiCalls.push(['agent', pathname]);
|
||||
return new Response('{}', { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ content: '每天早上 7 点给我发一天的待办记录' }),
|
||||
{
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /每天早上 7点/);
|
||||
assert.deepEqual(apiCalls, [
|
||||
['digest', 'user-1', 7, 0, 'wechat'],
|
||||
['finish', 'done', null],
|
||||
]);
|
||||
});
|
||||
|
||||
test('wechat mp service returns status text for question mark probes', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
enabled: true,
|
||||
appId: 'wx123',
|
||||
appSecret: 'secret',
|
||||
token,
|
||||
publicBaseUrl: 'https://example.com',
|
||||
bindPath: '/auth/wechat/authorize?intent=login',
|
||||
ackText: 'ack',
|
||||
statusText: '我在这边',
|
||||
unsupportedText: 'unsupported',
|
||||
unboundTextPrefix: '请先绑定',
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||||
},
|
||||
},
|
||||
apiFetch: async () => new Response('{}', { status: 200 }),
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(inboundXml({ content: '?' }), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /我在这边/);
|
||||
assert.match(result.body, /毕升/);
|
||||
});
|
||||
|
||||
test('wechat mp service greets bound users with stored wechat nickname', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
let replyCalled = false;
|
||||
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: '毕升' };
|
||||
},
|
||||
},
|
||||
apiFetch: async (pathname) => {
|
||||
if (pathname === '/sessions/session-1/reply') replyCalled = true;
|
||||
return new Response('{}', { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(inboundXml({ content: '你好' }), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /你好,毕升/);
|
||||
assert.equal(replyCalled, false);
|
||||
});
|
||||
|
||||
test('wechat mp service replies to connectivity test without agent', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
let replyCalled = false;
|
||||
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: '毕升' };
|
||||
},
|
||||
},
|
||||
apiFetch: async (pathname) => {
|
||||
if (pathname.includes('/reply')) replyCalled = true;
|
||||
return new Response('{}', { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(inboundXml({ content: '测试999' }), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /公众号消息通道正常/);
|
||||
assert.equal(replyCalled, false);
|
||||
});
|
||||
|
||||
test('wechat mp service ignores duplicated message ids', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
let replyCalled = false;
|
||||
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' };
|
||||
},
|
||||
async recordWechatMpMessage() {
|
||||
return { inserted: false };
|
||||
},
|
||||
},
|
||||
apiFetch: async (pathname) => {
|
||||
if (pathname === '/sessions/session-1/reply') replyCalled = true;
|
||||
return new Response('{}', { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我继续处理' }), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /上一条如果还没完成/);
|
||||
assert.equal(replyCalled, false);
|
||||
});
|
||||
|
||||
test('wechat mp service exposes route status and can recreate route for bound user', async () => {
|
||||
const calls = [];
|
||||
let routeCleared = false;
|
||||
let routeUpserted = false;
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
enabled: true,
|
||||
appId: 'wx123',
|
||||
appSecret: 'secret',
|
||||
token: 'token',
|
||||
publicBaseUrl: 'https://example.com',
|
||||
bindPath: '/auth/wechat/authorize?intent=login',
|
||||
ackText: 'ack',
|
||||
unsupportedText: 'unsupported',
|
||||
unboundTextPrefix: '请先绑定',
|
||||
},
|
||||
userAuth: {
|
||||
async getWechatOpenidForUser() {
|
||||
return 'openid-1';
|
||||
},
|
||||
async getWechatAgentRoute(_appId, _openid) {
|
||||
if (routeCleared && !routeUpserted) return null;
|
||||
return {
|
||||
agentSessionId: routeUpserted ? 'session-new' : 'session-old',
|
||||
status: 'active',
|
||||
updatedAt: 1710000000000,
|
||||
};
|
||||
},
|
||||
async clearWechatAgentRoute() {
|
||||
routeCleared = true;
|
||||
calls.push('cleared');
|
||||
},
|
||||
async canUseChat() {
|
||||
return { ok: true };
|
||||
},
|
||||
async resolveWorkingDir() {
|
||||
return '/tmp/user-1';
|
||||
},
|
||||
async getAgentSessionPolicy() {
|
||||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||||
},
|
||||
async getUserPublishLayout() {
|
||||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||||
},
|
||||
async registerAgentSession(_userId, sessionId) {
|
||||
calls.push(`register:${sessionId}`);
|
||||
},
|
||||
async upsertWechatAgentRoute() {
|
||||
routeUpserted = true;
|
||||
calls.push('upserted');
|
||||
},
|
||||
},
|
||||
apiFetch: async (pathname) => {
|
||||
if (pathname === '/agent/start') {
|
||||
return new Response(JSON.stringify({ id: 'session-new' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fallback api path: ${pathname}`);
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname) => {
|
||||
if (pathname === '/sessions/session-new') {
|
||||
return new Response(JSON.stringify({ working_dir: '/tmp/user-1' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (pathname === '/sessions/session-new/extensions') {
|
||||
return new Response(JSON.stringify({ extensions: [] }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (
|
||||
pathname === '/agent/restart'
|
||||
) {
|
||||
return new Response('{}', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected api path: ${pathname}`);
|
||||
},
|
||||
});
|
||||
|
||||
const status = await service.getRouteStatusForUser('user-1');
|
||||
assert.deepEqual(status, {
|
||||
enabled: true,
|
||||
bound: true,
|
||||
appId: 'wx123',
|
||||
openid: 'openid-1',
|
||||
agentSessionId: 'session-old',
|
||||
routeStatus: 'active',
|
||||
updatedAt: 1710000000000,
|
||||
});
|
||||
|
||||
const recreated = await service.recreateRouteForUser('user-1');
|
||||
assert.equal(recreated.ok, true);
|
||||
assert.equal(recreated.route.agentSessionId, 'session-new');
|
||||
assert.equal(calls.includes('cleared'), true);
|
||||
assert.equal(calls.includes('upserted'), true);
|
||||
});
|
||||
Reference in New Issue
Block a user