agent-messenger 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/README.md +16 -16
- package/.claude-plugin/marketplace.json +29 -29
- package/.claude-plugin/plugin.json +5 -5
- package/CONTRIBUTING.md +1 -1
- package/README.md +9 -6
- package/bun.lock +89 -105
- package/bunfig.toml +3 -0
- package/dist/package.json +13 -3
- package/dist/src/platforms/discordbot/client.js +2 -2
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.js +2 -1
- package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +2 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +52 -2
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/profile.d.ts +3 -0
- package/dist/src/platforms/kakaotalk/commands/profile.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/commands/profile.js +19 -0
- package/dist/src/platforms/kakaotalk/commands/profile.js.map +1 -0
- package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +1 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +2 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +16 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +8 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/line/commands/auth.js +32 -20
- package/dist/src/platforms/line/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.js +2 -0
- package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +2 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +66 -23
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +4 -0
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +10 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
- package/dist/src/platforms/webex/encryption.js +49 -0
- package/dist/src/platforms/webex/encryption.js.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.js +4 -0
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +6 -5
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/token-extractor.js +92 -43
- package/dist/src/platforms/webex/token-extractor.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +4 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +2 -0
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/dist/src/platforms/wechatbot/cli.d.ts +5 -0
- package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/cli.js +18 -0
- package/dist/src/platforms/wechatbot/cli.js.map +1 -0
- package/dist/src/platforms/wechatbot/client.d.ts +36 -0
- package/dist/src/platforms/wechatbot/client.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/client.js +208 -0
- package/dist/src/platforms/wechatbot/client.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/auth.d.ts +28 -0
- package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/auth.js +164 -0
- package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/index.d.ts +5 -0
- package/dist/src/platforms/wechatbot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/index.js +5 -0
- package/dist/src/platforms/wechatbot/commands/index.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/message.d.ts +18 -0
- package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/message.js +80 -0
- package/dist/src/platforms/wechatbot/commands/message.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/shared.d.ts +9 -0
- package/dist/src/platforms/wechatbot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/shared.js +13 -0
- package/dist/src/platforms/wechatbot/commands/shared.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/template.d.ts +19 -0
- package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/template.js +76 -0
- package/dist/src/platforms/wechatbot/commands/template.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/user.d.ts +20 -0
- package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/user.js +53 -0
- package/dist/src/platforms/wechatbot/commands/user.js.map +1 -0
- package/dist/src/platforms/wechatbot/credential-manager.d.ts +17 -0
- package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/credential-manager.js +121 -0
- package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -0
- package/dist/src/platforms/wechatbot/index.d.ts +5 -0
- package/dist/src/platforms/wechatbot/index.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/index.js +4 -0
- package/dist/src/platforms/wechatbot/index.js.map +1 -0
- package/dist/src/platforms/wechatbot/types.d.ts +94 -0
- package/dist/src/platforms/wechatbot/types.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/types.js +54 -0
- package/dist/src/platforms/wechatbot/types.js.map +1 -0
- package/dist/src/platforms/whatsapp/client.d.ts +1 -0
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +27 -13
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.js +21 -18
- package/dist/src/platforms/whatsapp/commands/auth.js.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.js +14 -8
- package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
- package/docs/content/docs/agent-skills.mdx +4 -4
- package/docs/content/docs/cli/channeltalk.mdx +1 -1
- package/docs/content/docs/cli/channeltalkbot.mdx +1 -1
- package/docs/content/docs/cli/discord.mdx +1 -1
- package/docs/content/docs/cli/discordbot.mdx +1 -1
- package/docs/content/docs/cli/instagram.mdx +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +1 -1
- package/docs/content/docs/cli/line.mdx +1 -1
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/slack.mdx +1 -1
- package/docs/content/docs/cli/slackbot.mdx +1 -1
- package/docs/content/docs/cli/teams.mdx +1 -1
- package/docs/content/docs/cli/webex.mdx +5 -3
- package/docs/content/docs/cli/wechatbot.mdx +179 -0
- package/docs/content/docs/cli/whatsapp.mdx +1 -1
- package/docs/content/docs/cli/whatsappbot.mdx +1 -1
- package/docs/content/docs/sdk/meta.json +1 -1
- package/docs/content/docs/sdk/wechatbot.mdx +282 -0
- package/docs/content/docs/tui.mdx +1 -1
- package/docs/src/app/page.tsx +5 -5
- package/package.json +13 -3
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +24 -1
- package/skills/agent-line/SKILL.md +7 -11
- package/skills/agent-line/references/authentication.md +13 -4
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webex/references/authentication.md +4 -3
- package/skills/agent-webex/references/common-patterns.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +385 -0
- package/skills/agent-whatsapp/SKILL.md +12 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/credential-manager.test.ts +18 -1
- package/src/platforms/discordbot/client.ts +2 -2
- package/src/platforms/instagram/commands/auth.test.ts +216 -0
- package/src/platforms/instagram/commands/chat.test.ts +127 -0
- package/src/platforms/instagram/commands/message.test.ts +178 -0
- package/src/platforms/kakaotalk/cli.ts +2 -1
- package/src/platforms/kakaotalk/client.test.ts +157 -0
- package/src/platforms/kakaotalk/client.ts +57 -3
- package/src/platforms/kakaotalk/commands/auth.test.ts +299 -0
- package/src/platforms/kakaotalk/commands/chat.test.ts +97 -0
- package/src/platforms/kakaotalk/commands/index.ts +1 -0
- package/src/platforms/kakaotalk/commands/message.test.ts +113 -0
- package/src/platforms/kakaotalk/commands/profile.test.ts +84 -0
- package/src/platforms/kakaotalk/commands/profile.ts +21 -0
- package/src/platforms/kakaotalk/index.test.ts +5 -0
- package/src/platforms/kakaotalk/index.ts +2 -0
- package/src/platforms/kakaotalk/protocol/session.ts +2 -0
- package/src/platforms/kakaotalk/types.ts +18 -0
- package/src/platforms/line/commands/auth.test.ts +141 -0
- package/src/platforms/line/commands/auth.ts +28 -19
- package/src/platforms/line/commands/chat.test.ts +110 -0
- package/src/platforms/line/commands/friend.test.ts +98 -0
- package/src/platforms/line/commands/message.test.ts +119 -0
- package/src/platforms/line/commands/profile.test.ts +85 -0
- package/src/platforms/slackbot/commands/channel.test.ts +139 -0
- package/src/platforms/slackbot/commands/message.test.ts +226 -0
- package/src/platforms/slackbot/commands/reaction.test.ts +90 -0
- package/src/platforms/slackbot/commands/user.test.ts +143 -0
- package/src/platforms/teams/commands/reaction.test.ts +45 -61
- package/src/platforms/teams/commands/reaction.ts +2 -0
- package/src/platforms/telegram/commands/chat.test.ts +125 -0
- package/src/platforms/telegram/commands/message.test.ts +92 -0
- package/src/platforms/webex/client.ts +98 -26
- package/src/platforms/webex/commands/auth.ts +4 -0
- package/src/platforms/webex/commands/member.test.ts +65 -58
- package/src/platforms/webex/commands/message.test.ts +78 -121
- package/src/platforms/webex/commands/snapshot.test.ts +59 -46
- package/src/platforms/webex/commands/space.test.ts +49 -48
- package/src/platforms/webex/encryption.ts +53 -0
- package/src/platforms/webex/ensure-auth.ts +4 -0
- package/src/platforms/webex/token-extractor.ts +107 -40
- package/src/platforms/webex/types.ts +4 -0
- package/src/platforms/webex/typings/node-jose.d.ts +27 -0
- package/src/platforms/wechatbot/cli.ts +24 -0
- package/src/platforms/wechatbot/client.test.ts +497 -0
- package/src/platforms/wechatbot/client.ts +268 -0
- package/src/platforms/wechatbot/commands/auth.test.ts +211 -0
- package/src/platforms/wechatbot/commands/auth.ts +203 -0
- package/src/platforms/wechatbot/commands/index.ts +4 -0
- package/src/platforms/wechatbot/commands/message.test.ts +155 -0
- package/src/platforms/wechatbot/commands/message.ts +104 -0
- package/src/platforms/wechatbot/commands/shared.ts +22 -0
- package/src/platforms/wechatbot/commands/template.test.ts +199 -0
- package/src/platforms/wechatbot/commands/template.ts +102 -0
- package/src/platforms/wechatbot/commands/user.test.ts +165 -0
- package/src/platforms/wechatbot/commands/user.ts +75 -0
- package/src/platforms/wechatbot/credential-manager.test.ts +255 -0
- package/src/platforms/wechatbot/credential-manager.ts +148 -0
- package/src/platforms/wechatbot/index.test.ts +49 -0
- package/src/platforms/wechatbot/index.ts +19 -0
- package/src/platforms/wechatbot/types.test.ts +223 -0
- package/src/platforms/wechatbot/types.ts +107 -0
- package/src/platforms/whatsapp/client.ts +24 -13
- package/src/platforms/whatsapp/commands/auth.test.ts +311 -0
- package/src/platforms/whatsapp/commands/auth.ts +21 -17
- package/src/platforms/whatsapp/commands/chat.test.ts +198 -0
- package/src/platforms/whatsapp/commands/message.test.ts +231 -0
- package/src/platforms/whatsapp/credential-manager.test.ts +20 -0
- package/src/platforms/whatsapp/credential-manager.ts +17 -8
- package/src/platforms/whatsappbot/commands/auth.test.ts +217 -0
- package/src/platforms/whatsappbot/commands/message.test.ts +198 -0
- package/src/platforms/whatsappbot/commands/template.test.ts +112 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { WeChatBotClient } from '@/platforms/wechatbot/client'
|
|
4
|
+
import { WeChatBotError } from '@/platforms/wechatbot/types'
|
|
5
|
+
|
|
6
|
+
describe('WeChatBotClient', () => {
|
|
7
|
+
const originalFetch = globalThis.fetch
|
|
8
|
+
let fetchCalls: Array<{ url: string; options?: RequestInit }> = []
|
|
9
|
+
let fetchResponses: Response[] = []
|
|
10
|
+
let fetchIndex = 0
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
fetchCalls = []
|
|
14
|
+
fetchResponses = []
|
|
15
|
+
fetchIndex = 0
|
|
16
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
17
|
+
value: async (url: string | URL | Request, options?: RequestInit): Promise<Response> => {
|
|
18
|
+
fetchCalls.push({ url: url.toString(), options })
|
|
19
|
+
const response = fetchResponses[fetchIndex]
|
|
20
|
+
fetchIndex++
|
|
21
|
+
if (!response) {
|
|
22
|
+
throw new Error('No mock response configured')
|
|
23
|
+
}
|
|
24
|
+
return response
|
|
25
|
+
},
|
|
26
|
+
writable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
33
|
+
value: originalFetch,
|
|
34
|
+
writable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const mockResponse = (body: unknown, status = 200) => {
|
|
40
|
+
fetchResponses.push(
|
|
41
|
+
new Response(JSON.stringify(body), {
|
|
42
|
+
status,
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tokenResponse = () => mockResponse({ access_token: 'test-token', expires_in: 7200 })
|
|
49
|
+
|
|
50
|
+
describe('login', () => {
|
|
51
|
+
test('throws on empty appId', async () => {
|
|
52
|
+
await expect(new WeChatBotClient().login({ appId: '', appSecret: 'secret' })).rejects.toThrow(WeChatBotError)
|
|
53
|
+
await expect(new WeChatBotClient().login({ appId: '', appSecret: 'secret' })).rejects.toThrow('App ID is required')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('throws on empty appSecret', async () => {
|
|
57
|
+
await expect(new WeChatBotClient().login({ appId: 'wx123', appSecret: '' })).rejects.toThrow(WeChatBotError)
|
|
58
|
+
await expect(new WeChatBotClient().login({ appId: 'wx123', appSecret: '' })).rejects.toThrow('App Secret is required')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('accepts valid credentials and returns client', async () => {
|
|
62
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
63
|
+
expect(client).toBeInstanceOf(WeChatBotClient)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('verifyCredentials', () => {
|
|
68
|
+
test('calls token endpoint and returns true on success', async () => {
|
|
69
|
+
tokenResponse()
|
|
70
|
+
|
|
71
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
72
|
+
const result = await client.verifyCredentials()
|
|
73
|
+
|
|
74
|
+
expect(result).toBe(true)
|
|
75
|
+
expect(fetchCalls.length).toBe(1)
|
|
76
|
+
expect(fetchCalls[0].url).toContain('/cgi-bin/token')
|
|
77
|
+
expect(fetchCalls[0].url).toContain('appid=wx123')
|
|
78
|
+
expect(fetchCalls[0].url).toContain('secret=secret123')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('returns false on token error', async () => {
|
|
82
|
+
mockResponse({ errcode: 40125, errmsg: 'invalid appsecret' })
|
|
83
|
+
|
|
84
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'bad-secret' })
|
|
85
|
+
const result = await client.verifyCredentials()
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('returns false on network error', async () => {
|
|
91
|
+
fetchResponses = []
|
|
92
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
93
|
+
value: async (): Promise<Response> => {
|
|
94
|
+
throw new Error('network error')
|
|
95
|
+
},
|
|
96
|
+
writable: true,
|
|
97
|
+
configurable: true,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
101
|
+
const result = await client.verifyCredentials()
|
|
102
|
+
|
|
103
|
+
expect(result).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('sendTextMessage', () => {
|
|
108
|
+
test('sends POST to /cgi-bin/message/custom/send with token in query', async () => {
|
|
109
|
+
tokenResponse()
|
|
110
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
111
|
+
|
|
112
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
113
|
+
await client.sendTextMessage('openid-123', 'Hello world')
|
|
114
|
+
|
|
115
|
+
expect(fetchCalls.length).toBe(2)
|
|
116
|
+
const call = fetchCalls[1]
|
|
117
|
+
expect(call.url).toContain('/cgi-bin/message/custom/send')
|
|
118
|
+
expect(call.url).toContain('access_token=test-token')
|
|
119
|
+
expect(call.options?.method).toBe('POST')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('sends correct body shape', async () => {
|
|
123
|
+
tokenResponse()
|
|
124
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
125
|
+
|
|
126
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
127
|
+
await client.sendTextMessage('openid-123', 'Hello world')
|
|
128
|
+
|
|
129
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
130
|
+
expect(body).toMatchObject({
|
|
131
|
+
touser: 'openid-123',
|
|
132
|
+
msgtype: 'text',
|
|
133
|
+
text: { content: 'Hello world' },
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('sendImageMessage', () => {
|
|
139
|
+
test('sends POST with image payload', async () => {
|
|
140
|
+
tokenResponse()
|
|
141
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
142
|
+
|
|
143
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
144
|
+
await client.sendImageMessage('openid-123', 'media-id-456')
|
|
145
|
+
|
|
146
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
147
|
+
expect(body).toMatchObject({
|
|
148
|
+
touser: 'openid-123',
|
|
149
|
+
msgtype: 'image',
|
|
150
|
+
image: { media_id: 'media-id-456' },
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('sendNewsMessage', () => {
|
|
156
|
+
test('sends POST with news/articles payload', async () => {
|
|
157
|
+
tokenResponse()
|
|
158
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
159
|
+
|
|
160
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
161
|
+
const articles = [
|
|
162
|
+
{
|
|
163
|
+
title: 'Test Article',
|
|
164
|
+
description: 'Test desc',
|
|
165
|
+
url: 'https://example.com',
|
|
166
|
+
picurl: 'https://example.com/pic.jpg',
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
await client.sendNewsMessage('openid-123', articles)
|
|
170
|
+
|
|
171
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
172
|
+
expect(body).toMatchObject({
|
|
173
|
+
touser: 'openid-123',
|
|
174
|
+
msgtype: 'news',
|
|
175
|
+
news: { articles },
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('sendTemplateMessage', () => {
|
|
181
|
+
test('sends POST to /cgi-bin/message/template/send and returns msgid', async () => {
|
|
182
|
+
tokenResponse()
|
|
183
|
+
mockResponse({ errcode: 0, errmsg: 'ok', msgid: 12345 })
|
|
184
|
+
|
|
185
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
186
|
+
const result = await client.sendTemplateMessage(
|
|
187
|
+
'openid-123',
|
|
188
|
+
'template-id-abc',
|
|
189
|
+
{ first: { value: 'Hello' } },
|
|
190
|
+
'https://example.com',
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
expect(result.msgid).toBe(12345)
|
|
194
|
+
expect(fetchCalls[1].url).toContain('/cgi-bin/message/template/send')
|
|
195
|
+
|
|
196
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
197
|
+
expect(body).toMatchObject({
|
|
198
|
+
touser: 'openid-123',
|
|
199
|
+
template_id: 'template-id-abc',
|
|
200
|
+
url: 'https://example.com',
|
|
201
|
+
data: { first: { value: 'Hello' } },
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('listTemplates', () => {
|
|
207
|
+
test('sends GET and unwraps template_list from response', async () => {
|
|
208
|
+
tokenResponse()
|
|
209
|
+
mockResponse({
|
|
210
|
+
errcode: 0,
|
|
211
|
+
errmsg: 'ok',
|
|
212
|
+
template_list: [
|
|
213
|
+
{
|
|
214
|
+
template_id: 'tmpl-001',
|
|
215
|
+
title: 'Order Notification',
|
|
216
|
+
primary_industry: 'IT科技',
|
|
217
|
+
deputy_industry: '互联网|电子商务',
|
|
218
|
+
content: 'ORDER_STATUS {{status.DATA}}',
|
|
219
|
+
example: 'Order shipped',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
225
|
+
const templates = await client.listTemplates()
|
|
226
|
+
|
|
227
|
+
expect(templates).toHaveLength(1)
|
|
228
|
+
expect(templates[0].template_id).toBe('tmpl-001')
|
|
229
|
+
expect(templates[0].title).toBe('Order Notification')
|
|
230
|
+
expect(fetchCalls[1].url).toContain('/cgi-bin/template/get_all_private_template')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe('deleteTemplate', () => {
|
|
235
|
+
test('sends POST with template_id body', async () => {
|
|
236
|
+
tokenResponse()
|
|
237
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
238
|
+
|
|
239
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
240
|
+
await client.deleteTemplate('tmpl-to-delete')
|
|
241
|
+
|
|
242
|
+
expect(fetchCalls[1].url).toContain('/cgi-bin/template/del_private_template')
|
|
243
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
244
|
+
expect(body).toEqual({ template_id: 'tmpl-to-delete' })
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe('getFollowers', () => {
|
|
249
|
+
test('sends GET to /cgi-bin/user/get and returns openids array', async () => {
|
|
250
|
+
tokenResponse()
|
|
251
|
+
mockResponse({
|
|
252
|
+
errcode: 0,
|
|
253
|
+
total: 3,
|
|
254
|
+
count: 2,
|
|
255
|
+
data: { openid: ['openid-1', 'openid-2'] },
|
|
256
|
+
next_openid: 'openid-2',
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
260
|
+
const result = await client.getFollowers()
|
|
261
|
+
|
|
262
|
+
expect(result.total).toBe(3)
|
|
263
|
+
expect(result.count).toBe(2)
|
|
264
|
+
expect(result.openids).toEqual(['openid-1', 'openid-2'])
|
|
265
|
+
expect(result.next_openid).toBe('openid-2')
|
|
266
|
+
expect(fetchCalls[1].url).toContain('/cgi-bin/user/get')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('passes next_openid parameter when provided', async () => {
|
|
270
|
+
tokenResponse()
|
|
271
|
+
mockResponse({
|
|
272
|
+
total: 1,
|
|
273
|
+
count: 1,
|
|
274
|
+
data: { openid: ['openid-3'] },
|
|
275
|
+
next_openid: '',
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
279
|
+
await client.getFollowers('openid-2')
|
|
280
|
+
|
|
281
|
+
expect(fetchCalls[1].url).toContain('next_openid=openid-2')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('returns empty openids when data is missing', async () => {
|
|
285
|
+
tokenResponse()
|
|
286
|
+
mockResponse({
|
|
287
|
+
total: 0,
|
|
288
|
+
count: 0,
|
|
289
|
+
next_openid: '',
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
293
|
+
const result = await client.getFollowers()
|
|
294
|
+
|
|
295
|
+
expect(result.openids).toEqual([])
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('getUserInfo', () => {
|
|
300
|
+
test('sends GET with openid and lang params', async () => {
|
|
301
|
+
tokenResponse()
|
|
302
|
+
mockResponse({
|
|
303
|
+
subscribe: 1,
|
|
304
|
+
openid: 'openid-123',
|
|
305
|
+
language: 'zh_CN',
|
|
306
|
+
subscribe_time: 1609459200,
|
|
307
|
+
remark: '',
|
|
308
|
+
tagid_list: [],
|
|
309
|
+
subscribe_scene: 'ADD_SCENE_QR_CODE',
|
|
310
|
+
qr_scene: 0,
|
|
311
|
+
qr_scene_str: '',
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
315
|
+
const user = await client.getUserInfo('openid-123', 'zh_CN')
|
|
316
|
+
|
|
317
|
+
expect(user.openid).toBe('openid-123')
|
|
318
|
+
expect(user.subscribe).toBe(1)
|
|
319
|
+
expect(fetchCalls[1].url).toContain('/cgi-bin/user/info')
|
|
320
|
+
expect(fetchCalls[1].url).toContain('openid=openid-123')
|
|
321
|
+
expect(fetchCalls[1].url).toContain('lang=zh_CN')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('defaults to zh_CN lang', async () => {
|
|
325
|
+
tokenResponse()
|
|
326
|
+
mockResponse({
|
|
327
|
+
subscribe: 1,
|
|
328
|
+
openid: 'openid-456',
|
|
329
|
+
language: 'zh_CN',
|
|
330
|
+
subscribe_time: 1609459200,
|
|
331
|
+
remark: '',
|
|
332
|
+
tagid_list: [],
|
|
333
|
+
subscribe_scene: 'ADD_SCENE_QR_CODE',
|
|
334
|
+
qr_scene: 0,
|
|
335
|
+
qr_scene_str: '',
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
339
|
+
await client.getUserInfo('openid-456')
|
|
340
|
+
|
|
341
|
+
expect(fetchCalls[1].url).toContain('lang=zh_CN')
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
describe('token caching', () => {
|
|
346
|
+
test('second call does not re-fetch token if not expired', async () => {
|
|
347
|
+
tokenResponse()
|
|
348
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
349
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
350
|
+
|
|
351
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
352
|
+
await client.sendTextMessage('openid-1', 'msg1')
|
|
353
|
+
await client.sendTextMessage('openid-2', 'msg2')
|
|
354
|
+
|
|
355
|
+
expect(fetchCalls.length).toBe(3)
|
|
356
|
+
expect(fetchCalls[0].url).toContain('/cgi-bin/token')
|
|
357
|
+
expect(fetchCalls[1].url).toContain('/cgi-bin/message/custom/send')
|
|
358
|
+
expect(fetchCalls[2].url).toContain('/cgi-bin/message/custom/send')
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe('token auto-refresh on 40001', () => {
|
|
363
|
+
test('fetches new token and retries on errcode 40001', async () => {
|
|
364
|
+
tokenResponse()
|
|
365
|
+
mockResponse({ errcode: 40001, errmsg: 'invalid credential' })
|
|
366
|
+
mockResponse({ access_token: 'new-token', expires_in: 7200 })
|
|
367
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
368
|
+
|
|
369
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
370
|
+
await client.sendTextMessage('openid-123', 'Hello')
|
|
371
|
+
|
|
372
|
+
expect(fetchCalls.length).toBe(4)
|
|
373
|
+
expect(fetchCalls[2].url).toContain('/cgi-bin/token')
|
|
374
|
+
expect(fetchCalls[3].url).toContain('/cgi-bin/message/custom/send')
|
|
375
|
+
expect(fetchCalls[3].url).toContain('access_token=new-token')
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
describe('retry on system busy (errcode -1)', () => {
|
|
380
|
+
test('retries with backoff on errcode -1', async () => {
|
|
381
|
+
tokenResponse()
|
|
382
|
+
mockResponse({ errcode: -1, errmsg: 'system busy' })
|
|
383
|
+
mockResponse({ errcode: -1, errmsg: 'system busy' })
|
|
384
|
+
mockResponse({ errcode: -1, errmsg: 'system busy' })
|
|
385
|
+
mockResponse({ errcode: -1, errmsg: 'system busy' })
|
|
386
|
+
|
|
387
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
await client.sendTextMessage('openid-123', 'Hello')
|
|
391
|
+
expect.unreachable('should have thrown')
|
|
392
|
+
} catch (err) {
|
|
393
|
+
expect(err).toBeInstanceOf(WeChatBotError)
|
|
394
|
+
expect((err as WeChatBotError).message).toBe('WeChat system busy')
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe('network error retry', () => {
|
|
400
|
+
test('GET retries on fetch throw', async () => {
|
|
401
|
+
let callCount = 0
|
|
402
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
403
|
+
value: async (url: string | URL | Request, options?: RequestInit): Promise<Response> => {
|
|
404
|
+
fetchCalls.push({ url: url.toString(), options })
|
|
405
|
+
callCount++
|
|
406
|
+
if (callCount === 1) {
|
|
407
|
+
return new Response(JSON.stringify({ access_token: 'test-token', expires_in: 7200 }), { status: 200 })
|
|
408
|
+
}
|
|
409
|
+
if (callCount <= 3) {
|
|
410
|
+
throw new Error('network error')
|
|
411
|
+
}
|
|
412
|
+
return new Response(
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
total: 0,
|
|
415
|
+
count: 0,
|
|
416
|
+
data: { openid: [] },
|
|
417
|
+
next_openid: '',
|
|
418
|
+
}),
|
|
419
|
+
{ status: 200 },
|
|
420
|
+
)
|
|
421
|
+
},
|
|
422
|
+
writable: true,
|
|
423
|
+
configurable: true,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
427
|
+
const result = await client.getFollowers()
|
|
428
|
+
|
|
429
|
+
expect(result.openids).toEqual([])
|
|
430
|
+
expect(callCount).toBe(4)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
test('POST does not retry on fetch throw', async () => {
|
|
434
|
+
let callCount = 0
|
|
435
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
436
|
+
value: async (url: string | URL | Request, options?: RequestInit): Promise<Response> => {
|
|
437
|
+
fetchCalls.push({ url: url.toString(), options })
|
|
438
|
+
callCount++
|
|
439
|
+
if (callCount === 1) {
|
|
440
|
+
return new Response(JSON.stringify({ access_token: 'test-token', expires_in: 7200 }), { status: 200 })
|
|
441
|
+
}
|
|
442
|
+
throw new Error('network error')
|
|
443
|
+
},
|
|
444
|
+
writable: true,
|
|
445
|
+
configurable: true,
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
449
|
+
await expect(client.sendTextMessage('openid-123', 'Hello')).rejects.toThrow(WeChatBotError)
|
|
450
|
+
expect(callCount).toBe(2)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe('errcode handling', () => {
|
|
455
|
+
test('non-zero errcode throws WeChatBotError with correct code', async () => {
|
|
456
|
+
tokenResponse()
|
|
457
|
+
mockResponse({ errcode: 48001, errmsg: 'api unauthorized' })
|
|
458
|
+
|
|
459
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
await client.sendTextMessage('openid-123', 'Hello')
|
|
463
|
+
expect.unreachable('should have thrown')
|
|
464
|
+
} catch (err) {
|
|
465
|
+
expect(err).toBeInstanceOf(WeChatBotError)
|
|
466
|
+
expect((err as WeChatBotError).code).toBe('48001')
|
|
467
|
+
expect((err as WeChatBotError).message).toBe('api unauthorized')
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
test('errcode 0 does not throw', async () => {
|
|
472
|
+
tokenResponse()
|
|
473
|
+
mockResponse({ errcode: 0, errmsg: 'ok' })
|
|
474
|
+
|
|
475
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
476
|
+
await expect(client.sendTextMessage('openid-123', 'Hello')).resolves.toBeUndefined()
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
describe('rate limit (errcode 45009)', () => {
|
|
481
|
+
test('throws WeChatBotError with code 45009 and appropriate message', async () => {
|
|
482
|
+
tokenResponse()
|
|
483
|
+
mockResponse({ errcode: 45009, errmsg: 'reach max api daily quota limit' })
|
|
484
|
+
|
|
485
|
+
const client = await new WeChatBotClient().login({ appId: 'wx123', appSecret: 'secret123' })
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
await client.sendTextMessage('openid-123', 'Hello')
|
|
489
|
+
expect.unreachable('should have thrown')
|
|
490
|
+
} catch (err) {
|
|
491
|
+
expect(err).toBeInstanceOf(WeChatBotError)
|
|
492
|
+
expect((err as WeChatBotError).code).toBe('45009')
|
|
493
|
+
expect((err as WeChatBotError).message).toContain('frequency limit')
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
})
|