feat(weixin): show typing indicator during turns

This commit is contained in:
xmanrui 2026-08-25 13:43:03 +08:00
parent ad3c08ace3
commit cb34e3f8de
10 changed files with 948 additions and 200 deletions

View file

@ -208,6 +208,103 @@ test('sendText emits the iLink message envelope without reflecting the token in
assert.doesNotMatch(calls[0].init.body, /host-only-token/);
});
test('getConfig requests a typing ticket for the current Weixin conversation', async () => {
const calls = [];
const api = createWeixinApi({
fetchImpl: async (url, init) => {
calls.push({ url: url.toString(), init });
return jsonResponse({ ret: 0, typing_ticket: 'typing-ticket' });
},
});
const config = await api.getConfig({
baseUrl: 'https://ilinkai.weixin.qq.com',
token: 'host-only-token',
toUserId: 'wx-user',
contextToken: 'message-context',
});
assert.deepEqual(config, { typingTicket: 'typing-ticket' });
assert.match(calls[0].url, /\/ilink\/bot\/getconfig$/);
assert.equal(calls[0].init.headers.Authorization, 'Bearer host-only-token');
const body = JSON.parse(calls[0].init.body);
assert.equal(body.ilink_user_id, 'wx-user');
assert.equal(body.context_token, 'message-context');
assert.equal(body.base_info.channel_version, '2.4.6');
assert.doesNotMatch(calls[0].init.body, /host-only-token/);
});
test('sendTyping sends start and cancel states with the cached ticket', async () => {
const calls = [];
const api = createWeixinApi({
fetchImpl: async (url, init) => {
calls.push({ url: url.toString(), init });
return jsonResponse({ ret: 0 });
},
});
for (const status of [1, 2]) {
await api.sendTyping({
baseUrl: 'https://ilinkai.weixin.qq.com',
token: 'host-only-token',
toUserId: 'wx-user',
typingTicket: 'typing-ticket',
status,
});
}
assert.deepEqual(calls.map(({ url, init }) => {
const body = JSON.parse(init.body);
return {
path: new URL(url).pathname,
user: body.ilink_user_id,
ticket: body.typing_ticket,
status: body.status,
tokenInBody: init.body.includes('host-only-token'),
};
}), [
{
path: '/ilink/bot/sendtyping',
user: 'wx-user',
ticket: 'typing-ticket',
status: 1,
tokenInBody: false,
},
{
path: '/ilink/bot/sendtyping',
user: 'wx-user',
ticket: 'typing-ticket',
status: 2,
tokenInBody: false,
},
]);
});
test('typing endpoints reject non-zero iLink return codes', async () => {
const api = createWeixinApi({
fetchImpl: async () => jsonResponse({ ret: -2, errmsg: 'rejected' }),
});
await assert.rejects(
api.getConfig({
baseUrl: 'https://ilinkai.weixin.qq.com',
token: 'host-only-token',
toUserId: 'wx-user',
}),
(error) => error instanceof WeixinApiError && error.code === 'config-rejected',
);
await assert.rejects(
api.sendTyping({
baseUrl: 'https://ilinkai.weixin.qq.com',
token: 'host-only-token',
toUserId: 'wx-user',
typingTicket: 'typing-ticket',
status: 1,
}),
(error) => error instanceof WeixinApiError && error.code === 'typing-rejected',
);
});
test('sendFile uses the iLink 2.4.6 encrypted CDN flow and sends a native file item', async () => {
const calls = [];
const api = createWeixinApi({