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.
Files changed (229) hide show
  1. package/.claude-plugin/README.md +16 -16
  2. package/.claude-plugin/marketplace.json +29 -29
  3. package/.claude-plugin/plugin.json +5 -5
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +9 -6
  6. package/bun.lock +89 -105
  7. package/bunfig.toml +3 -0
  8. package/dist/package.json +13 -3
  9. package/dist/src/platforms/discordbot/client.js +2 -2
  10. package/dist/src/platforms/discordbot/client.js.map +1 -1
  11. package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
  12. package/dist/src/platforms/kakaotalk/cli.js +2 -1
  13. package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
  14. package/dist/src/platforms/kakaotalk/client.d.ts +2 -1
  15. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  16. package/dist/src/platforms/kakaotalk/client.js +52 -2
  17. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  18. package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
  19. package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
  20. package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
  21. package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
  22. package/dist/src/platforms/kakaotalk/commands/profile.d.ts +3 -0
  23. package/dist/src/platforms/kakaotalk/commands/profile.d.ts.map +1 -0
  24. package/dist/src/platforms/kakaotalk/commands/profile.js +19 -0
  25. package/dist/src/platforms/kakaotalk/commands/profile.js.map +1 -0
  26. package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
  27. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  28. package/dist/src/platforms/kakaotalk/index.js +1 -1
  29. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  30. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  31. package/dist/src/platforms/kakaotalk/protocol/session.js +2 -1
  32. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  33. package/dist/src/platforms/kakaotalk/types.d.ts +16 -0
  34. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  35. package/dist/src/platforms/kakaotalk/types.js +8 -0
  36. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  37. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  38. package/dist/src/platforms/line/commands/auth.js +32 -20
  39. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  40. package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
  41. package/dist/src/platforms/teams/commands/reaction.js +2 -0
  42. package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
  43. package/dist/src/platforms/webex/client.d.ts +2 -0
  44. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  45. package/dist/src/platforms/webex/client.js +66 -23
  46. package/dist/src/platforms/webex/client.js.map +1 -1
  47. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  48. package/dist/src/platforms/webex/commands/auth.js +4 -0
  49. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  50. package/dist/src/platforms/webex/encryption.d.ts +10 -0
  51. package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
  52. package/dist/src/platforms/webex/encryption.js +49 -0
  53. package/dist/src/platforms/webex/encryption.js.map +1 -0
  54. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  55. package/dist/src/platforms/webex/ensure-auth.js +4 -0
  56. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  57. package/dist/src/platforms/webex/token-extractor.d.ts +6 -5
  58. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  59. package/dist/src/platforms/webex/token-extractor.js +92 -43
  60. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  61. package/dist/src/platforms/webex/types.d.ts +4 -0
  62. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  63. package/dist/src/platforms/webex/types.js +2 -0
  64. package/dist/src/platforms/webex/types.js.map +1 -1
  65. package/dist/src/platforms/wechatbot/cli.d.ts +5 -0
  66. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -0
  67. package/dist/src/platforms/wechatbot/cli.js +18 -0
  68. package/dist/src/platforms/wechatbot/cli.js.map +1 -0
  69. package/dist/src/platforms/wechatbot/client.d.ts +36 -0
  70. package/dist/src/platforms/wechatbot/client.d.ts.map +1 -0
  71. package/dist/src/platforms/wechatbot/client.js +208 -0
  72. package/dist/src/platforms/wechatbot/client.js.map +1 -0
  73. package/dist/src/platforms/wechatbot/commands/auth.d.ts +28 -0
  74. package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -0
  75. package/dist/src/platforms/wechatbot/commands/auth.js +164 -0
  76. package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -0
  77. package/dist/src/platforms/wechatbot/commands/index.d.ts +5 -0
  78. package/dist/src/platforms/wechatbot/commands/index.d.ts.map +1 -0
  79. package/dist/src/platforms/wechatbot/commands/index.js +5 -0
  80. package/dist/src/platforms/wechatbot/commands/index.js.map +1 -0
  81. package/dist/src/platforms/wechatbot/commands/message.d.ts +18 -0
  82. package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -0
  83. package/dist/src/platforms/wechatbot/commands/message.js +80 -0
  84. package/dist/src/platforms/wechatbot/commands/message.js.map +1 -0
  85. package/dist/src/platforms/wechatbot/commands/shared.d.ts +9 -0
  86. package/dist/src/platforms/wechatbot/commands/shared.d.ts.map +1 -0
  87. package/dist/src/platforms/wechatbot/commands/shared.js +13 -0
  88. package/dist/src/platforms/wechatbot/commands/shared.js.map +1 -0
  89. package/dist/src/platforms/wechatbot/commands/template.d.ts +19 -0
  90. package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -0
  91. package/dist/src/platforms/wechatbot/commands/template.js +76 -0
  92. package/dist/src/platforms/wechatbot/commands/template.js.map +1 -0
  93. package/dist/src/platforms/wechatbot/commands/user.d.ts +20 -0
  94. package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -0
  95. package/dist/src/platforms/wechatbot/commands/user.js +53 -0
  96. package/dist/src/platforms/wechatbot/commands/user.js.map +1 -0
  97. package/dist/src/platforms/wechatbot/credential-manager.d.ts +17 -0
  98. package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -0
  99. package/dist/src/platforms/wechatbot/credential-manager.js +121 -0
  100. package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -0
  101. package/dist/src/platforms/wechatbot/index.d.ts +5 -0
  102. package/dist/src/platforms/wechatbot/index.d.ts.map +1 -0
  103. package/dist/src/platforms/wechatbot/index.js +4 -0
  104. package/dist/src/platforms/wechatbot/index.js.map +1 -0
  105. package/dist/src/platforms/wechatbot/types.d.ts +94 -0
  106. package/dist/src/platforms/wechatbot/types.d.ts.map +1 -0
  107. package/dist/src/platforms/wechatbot/types.js +54 -0
  108. package/dist/src/platforms/wechatbot/types.js.map +1 -0
  109. package/dist/src/platforms/whatsapp/client.d.ts +1 -0
  110. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  111. package/dist/src/platforms/whatsapp/client.js +27 -13
  112. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  113. package/dist/src/platforms/whatsapp/commands/auth.d.ts.map +1 -1
  114. package/dist/src/platforms/whatsapp/commands/auth.js +21 -18
  115. package/dist/src/platforms/whatsapp/commands/auth.js.map +1 -1
  116. package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
  117. package/dist/src/platforms/whatsapp/credential-manager.js +14 -8
  118. package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
  119. package/docs/content/docs/agent-skills.mdx +4 -4
  120. package/docs/content/docs/cli/channeltalk.mdx +1 -1
  121. package/docs/content/docs/cli/channeltalkbot.mdx +1 -1
  122. package/docs/content/docs/cli/discord.mdx +1 -1
  123. package/docs/content/docs/cli/discordbot.mdx +1 -1
  124. package/docs/content/docs/cli/instagram.mdx +1 -1
  125. package/docs/content/docs/cli/kakaotalk.mdx +1 -1
  126. package/docs/content/docs/cli/line.mdx +1 -1
  127. package/docs/content/docs/cli/meta.json +1 -0
  128. package/docs/content/docs/cli/slack.mdx +1 -1
  129. package/docs/content/docs/cli/slackbot.mdx +1 -1
  130. package/docs/content/docs/cli/teams.mdx +1 -1
  131. package/docs/content/docs/cli/webex.mdx +5 -3
  132. package/docs/content/docs/cli/wechatbot.mdx +179 -0
  133. package/docs/content/docs/cli/whatsapp.mdx +1 -1
  134. package/docs/content/docs/cli/whatsappbot.mdx +1 -1
  135. package/docs/content/docs/sdk/meta.json +1 -1
  136. package/docs/content/docs/sdk/wechatbot.mdx +282 -0
  137. package/docs/content/docs/tui.mdx +1 -1
  138. package/docs/src/app/page.tsx +5 -5
  139. package/package.json +13 -3
  140. package/skills/agent-channeltalk/SKILL.md +1 -1
  141. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  142. package/skills/agent-discord/SKILL.md +1 -1
  143. package/skills/agent-discordbot/SKILL.md +1 -1
  144. package/skills/agent-instagram/SKILL.md +1 -1
  145. package/skills/agent-kakaotalk/SKILL.md +24 -1
  146. package/skills/agent-line/SKILL.md +7 -11
  147. package/skills/agent-line/references/authentication.md +13 -4
  148. package/skills/agent-slack/SKILL.md +1 -1
  149. package/skills/agent-slackbot/SKILL.md +1 -1
  150. package/skills/agent-teams/SKILL.md +1 -1
  151. package/skills/agent-telegram/SKILL.md +1 -1
  152. package/skills/agent-webex/SKILL.md +1 -1
  153. package/skills/agent-webex/references/authentication.md +4 -3
  154. package/skills/agent-webex/references/common-patterns.md +1 -1
  155. package/skills/agent-wechatbot/SKILL.md +385 -0
  156. package/skills/agent-whatsapp/SKILL.md +12 -1
  157. package/skills/agent-whatsappbot/SKILL.md +1 -1
  158. package/src/platforms/discord/credential-manager.test.ts +18 -1
  159. package/src/platforms/discordbot/client.ts +2 -2
  160. package/src/platforms/instagram/commands/auth.test.ts +216 -0
  161. package/src/platforms/instagram/commands/chat.test.ts +127 -0
  162. package/src/platforms/instagram/commands/message.test.ts +178 -0
  163. package/src/platforms/kakaotalk/cli.ts +2 -1
  164. package/src/platforms/kakaotalk/client.test.ts +157 -0
  165. package/src/platforms/kakaotalk/client.ts +57 -3
  166. package/src/platforms/kakaotalk/commands/auth.test.ts +299 -0
  167. package/src/platforms/kakaotalk/commands/chat.test.ts +97 -0
  168. package/src/platforms/kakaotalk/commands/index.ts +1 -0
  169. package/src/platforms/kakaotalk/commands/message.test.ts +113 -0
  170. package/src/platforms/kakaotalk/commands/profile.test.ts +84 -0
  171. package/src/platforms/kakaotalk/commands/profile.ts +21 -0
  172. package/src/platforms/kakaotalk/index.test.ts +5 -0
  173. package/src/platforms/kakaotalk/index.ts +2 -0
  174. package/src/platforms/kakaotalk/protocol/session.ts +2 -0
  175. package/src/platforms/kakaotalk/types.ts +18 -0
  176. package/src/platforms/line/commands/auth.test.ts +141 -0
  177. package/src/platforms/line/commands/auth.ts +28 -19
  178. package/src/platforms/line/commands/chat.test.ts +110 -0
  179. package/src/platforms/line/commands/friend.test.ts +98 -0
  180. package/src/platforms/line/commands/message.test.ts +119 -0
  181. package/src/platforms/line/commands/profile.test.ts +85 -0
  182. package/src/platforms/slackbot/commands/channel.test.ts +139 -0
  183. package/src/platforms/slackbot/commands/message.test.ts +226 -0
  184. package/src/platforms/slackbot/commands/reaction.test.ts +90 -0
  185. package/src/platforms/slackbot/commands/user.test.ts +143 -0
  186. package/src/platforms/teams/commands/reaction.test.ts +45 -61
  187. package/src/platforms/teams/commands/reaction.ts +2 -0
  188. package/src/platforms/telegram/commands/chat.test.ts +125 -0
  189. package/src/platforms/telegram/commands/message.test.ts +92 -0
  190. package/src/platforms/webex/client.ts +98 -26
  191. package/src/platforms/webex/commands/auth.ts +4 -0
  192. package/src/platforms/webex/commands/member.test.ts +65 -58
  193. package/src/platforms/webex/commands/message.test.ts +78 -121
  194. package/src/platforms/webex/commands/snapshot.test.ts +59 -46
  195. package/src/platforms/webex/commands/space.test.ts +49 -48
  196. package/src/platforms/webex/encryption.ts +53 -0
  197. package/src/platforms/webex/ensure-auth.ts +4 -0
  198. package/src/platforms/webex/token-extractor.ts +107 -40
  199. package/src/platforms/webex/types.ts +4 -0
  200. package/src/platforms/webex/typings/node-jose.d.ts +27 -0
  201. package/src/platforms/wechatbot/cli.ts +24 -0
  202. package/src/platforms/wechatbot/client.test.ts +497 -0
  203. package/src/platforms/wechatbot/client.ts +268 -0
  204. package/src/platforms/wechatbot/commands/auth.test.ts +211 -0
  205. package/src/platforms/wechatbot/commands/auth.ts +203 -0
  206. package/src/platforms/wechatbot/commands/index.ts +4 -0
  207. package/src/platforms/wechatbot/commands/message.test.ts +155 -0
  208. package/src/platforms/wechatbot/commands/message.ts +104 -0
  209. package/src/platforms/wechatbot/commands/shared.ts +22 -0
  210. package/src/platforms/wechatbot/commands/template.test.ts +199 -0
  211. package/src/platforms/wechatbot/commands/template.ts +102 -0
  212. package/src/platforms/wechatbot/commands/user.test.ts +165 -0
  213. package/src/platforms/wechatbot/commands/user.ts +75 -0
  214. package/src/platforms/wechatbot/credential-manager.test.ts +255 -0
  215. package/src/platforms/wechatbot/credential-manager.ts +148 -0
  216. package/src/platforms/wechatbot/index.test.ts +49 -0
  217. package/src/platforms/wechatbot/index.ts +19 -0
  218. package/src/platforms/wechatbot/types.test.ts +223 -0
  219. package/src/platforms/wechatbot/types.ts +107 -0
  220. package/src/platforms/whatsapp/client.ts +24 -13
  221. package/src/platforms/whatsapp/commands/auth.test.ts +311 -0
  222. package/src/platforms/whatsapp/commands/auth.ts +21 -17
  223. package/src/platforms/whatsapp/commands/chat.test.ts +198 -0
  224. package/src/platforms/whatsapp/commands/message.test.ts +231 -0
  225. package/src/platforms/whatsapp/credential-manager.test.ts +20 -0
  226. package/src/platforms/whatsapp/credential-manager.ts +17 -8
  227. package/src/platforms/whatsappbot/commands/auth.test.ts +217 -0
  228. package/src/platforms/whatsappbot/commands/message.test.ts +198 -0
  229. package/src/platforms/whatsappbot/commands/template.test.ts +112 -0
@@ -0,0 +1,155 @@
1
+ import { afterAll, describe, expect, mock, test } from 'bun:test'
2
+ import { rmSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ import { WeChatBotCredentialManager } from '@/platforms/wechatbot/credential-manager'
6
+
7
+ const sendTextMessageMock = mock(() => Promise.resolve())
8
+ const sendImageMessageMock = mock(() => Promise.resolve())
9
+ const sendNewsMessageMock = mock(() => Promise.resolve())
10
+
11
+ mock.module('../client', () => ({
12
+ WeChatBotClient: class MockWeChatBotClient {
13
+ async login() {
14
+ return this
15
+ }
16
+ sendTextMessage = sendTextMessageMock
17
+ sendImageMessage = sendImageMessageMock
18
+ sendNewsMessage = sendNewsMessageMock
19
+ },
20
+ }))
21
+
22
+ const { sendAction, sendNewsAction } = await import('@/platforms/wechatbot/commands/message')
23
+
24
+ const testDirs: string[] = []
25
+
26
+ function makeCredManager(): WeChatBotCredentialManager {
27
+ const dir = join(
28
+ import.meta.dir,
29
+ `.test-message-config-${Date.now()}-${Math.random().toString(36).slice(2)}`,
30
+ )
31
+ testDirs.push(dir)
32
+ const manager = new WeChatBotCredentialManager(dir)
33
+ return manager
34
+ }
35
+
36
+ async function makeCredManagerWithCreds(): Promise<WeChatBotCredentialManager> {
37
+ const manager = makeCredManager()
38
+ await manager.setCredentials({ app_id: 'wx123', app_secret: 'secret123', account_name: 'My Account' })
39
+ return manager
40
+ }
41
+
42
+ afterAll(() => {
43
+ for (const dir of testDirs) {
44
+ rmSync(dir, { recursive: true, force: true })
45
+ }
46
+ })
47
+
48
+ describe('sendAction', () => {
49
+ test('sends text message and returns success', async () => {
50
+ const credManager = await makeCredManagerWithCreds()
51
+ const result = await sendAction('openid-123', 'Hello world', { _credManager: credManager })
52
+
53
+ expect(result.success).toBe(true)
54
+ expect(sendTextMessageMock).toHaveBeenCalledWith('openid-123', 'Hello world')
55
+ })
56
+
57
+ test('returns error when client throws', async () => {
58
+ mock.module('../client', () => ({
59
+ WeChatBotClient: class MockWeChatBotClient {
60
+ async login() {
61
+ return this
62
+ }
63
+ sendTextMessage = mock(() => Promise.reject(new Error('API error')))
64
+ },
65
+ }))
66
+
67
+ const { sendAction: sendActionFresh } = await import('@/platforms/wechatbot/commands/message')
68
+ const credManager = await makeCredManagerWithCreds()
69
+ const result = await sendActionFresh('openid-123', 'Hello', { _credManager: credManager })
70
+
71
+ expect(result.error).toBe('API error')
72
+ expect(result.success).toBeUndefined()
73
+ })
74
+ })
75
+
76
+ describe('sendImageAction', () => {
77
+ test('sends image message and returns success', async () => {
78
+ mock.module('../client', () => ({
79
+ WeChatBotClient: class MockWeChatBotClient {
80
+ async login() {
81
+ return this
82
+ }
83
+ sendImageMessage = mock(() => Promise.resolve())
84
+ },
85
+ }))
86
+
87
+ const { sendImageAction: sendImageActionFresh } = await import('@/platforms/wechatbot/commands/message')
88
+ const credManager = await makeCredManagerWithCreds()
89
+ const result = await sendImageActionFresh('openid-123', 'media-id-456', { _credManager: credManager })
90
+
91
+ expect(result.success).toBe(true)
92
+ })
93
+
94
+ test('returns error when client throws', async () => {
95
+ mock.module('../client', () => ({
96
+ WeChatBotClient: class MockWeChatBotClient {
97
+ async login() {
98
+ return this
99
+ }
100
+ sendImageMessage = mock(() => Promise.reject(new Error('image upload failed')))
101
+ },
102
+ }))
103
+
104
+ const { sendImageAction: sendImageActionFresh } = await import('@/platforms/wechatbot/commands/message')
105
+ const credManager = await makeCredManagerWithCreds()
106
+ const result = await sendImageActionFresh('openid-123', 'bad-media-id', { _credManager: credManager })
107
+
108
+ expect(result.error).toBe('image upload failed')
109
+ })
110
+ })
111
+
112
+ describe('sendNewsAction', () => {
113
+ test('returns error when required options are missing', async () => {
114
+ const credManager = await makeCredManagerWithCreds()
115
+ const result = await sendNewsAction('openid-123', { _credManager: credManager })
116
+
117
+ expect(result.error).toContain('--title')
118
+ expect(result.success).toBeUndefined()
119
+ })
120
+
121
+ test('returns error when title is missing', async () => {
122
+ const credManager = await makeCredManagerWithCreds()
123
+ const result = await sendNewsAction('openid-123', {
124
+ description: 'Test desc',
125
+ url: 'https://example.com',
126
+ picurl: 'https://example.com/pic.jpg',
127
+ _credManager: credManager,
128
+ })
129
+
130
+ expect(result.error).toContain('--title')
131
+ })
132
+
133
+ test('sends news message with all required options', async () => {
134
+ mock.module('../client', () => ({
135
+ WeChatBotClient: class MockWeChatBotClient {
136
+ async login() {
137
+ return this
138
+ }
139
+ sendNewsMessage = mock(() => Promise.resolve())
140
+ },
141
+ }))
142
+
143
+ const { sendNewsAction: sendNewsActionFresh } = await import('@/platforms/wechatbot/commands/message')
144
+ const credManager = await makeCredManagerWithCreds()
145
+ const result = await sendNewsActionFresh('openid-123', {
146
+ title: 'Test Article',
147
+ description: 'Test description',
148
+ url: 'https://example.com',
149
+ picurl: 'https://example.com/pic.jpg',
150
+ _credManager: credManager,
151
+ })
152
+
153
+ expect(result.success).toBe(true)
154
+ })
155
+ })
@@ -0,0 +1,104 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { formatOutput } from '@/shared/utils/output'
4
+
5
+ import type { WeChatBotNewsArticle } from '../types'
6
+ import type { AccountOption } from './shared'
7
+ import { getClient } from './shared'
8
+
9
+ interface MessageResult {
10
+ success?: boolean
11
+ error?: string
12
+ }
13
+
14
+ type MessageOptions = AccountOption & {
15
+ title?: string
16
+ description?: string
17
+ url?: string
18
+ picurl?: string
19
+ }
20
+
21
+ export async function sendAction(openId: string, text: string, options: MessageOptions): Promise<MessageResult> {
22
+ try {
23
+ const client = await getClient(options)
24
+ await client.sendTextMessage(openId, text)
25
+ return { success: true }
26
+ } catch (error) {
27
+ return { error: (error as Error).message }
28
+ }
29
+ }
30
+
31
+ export async function sendImageAction(openId: string, mediaId: string, options: MessageOptions): Promise<MessageResult> {
32
+ try {
33
+ const client = await getClient(options)
34
+ await client.sendImageMessage(openId, mediaId)
35
+ return { success: true }
36
+ } catch (error) {
37
+ return { error: (error as Error).message }
38
+ }
39
+ }
40
+
41
+ export async function sendNewsAction(openId: string, options: MessageOptions): Promise<MessageResult> {
42
+ try {
43
+ if (!options.title || !options.description || !options.url || !options.picurl) {
44
+ return { error: '--title, --description, --url, and --picurl are required' }
45
+ }
46
+
47
+ const article: WeChatBotNewsArticle = {
48
+ title: options.title,
49
+ description: options.description,
50
+ url: options.url,
51
+ picurl: options.picurl,
52
+ }
53
+
54
+ const client = await getClient(options)
55
+ await client.sendNewsMessage(openId, [article])
56
+ return { success: true }
57
+ } catch (error) {
58
+ return { error: (error as Error).message }
59
+ }
60
+ }
61
+
62
+ function cliOutput(result: MessageResult, pretty?: boolean): void {
63
+ console.log(formatOutput(result, pretty))
64
+ if (result.error) process.exit(1)
65
+ }
66
+
67
+ export const messageCommand = new Command('message')
68
+ .description('Message commands')
69
+ .addCommand(
70
+ new Command('send')
71
+ .description('Send a text message (customer service)')
72
+ .argument('<open-id>', 'Recipient OpenID')
73
+ .argument('<text>', 'Message text')
74
+ .option('--account <id>', 'Account ID')
75
+ .option('--pretty', 'Pretty print JSON output')
76
+ .action(async (openId: string, text: string, opts: MessageOptions) => {
77
+ cliOutput(await sendAction(openId, text, opts), opts.pretty)
78
+ }),
79
+ )
80
+ .addCommand(
81
+ new Command('send-image')
82
+ .description('Send an image message (customer service)')
83
+ .argument('<open-id>', 'Recipient OpenID')
84
+ .argument('<media-id>', 'Media ID of the image')
85
+ .option('--account <id>', 'Account ID')
86
+ .option('--pretty', 'Pretty print JSON output')
87
+ .action(async (openId: string, mediaId: string, opts: MessageOptions) => {
88
+ cliOutput(await sendImageAction(openId, mediaId, opts), opts.pretty)
89
+ }),
90
+ )
91
+ .addCommand(
92
+ new Command('send-news')
93
+ .description('Send a news/article message (customer service)')
94
+ .argument('<open-id>', 'Recipient OpenID')
95
+ .requiredOption('--title <title>', 'Article title')
96
+ .requiredOption('--description <description>', 'Article description')
97
+ .requiredOption('--url <url>', 'Article URL')
98
+ .requiredOption('--picurl <picurl>', 'Article picture URL')
99
+ .option('--account <id>', 'Account ID')
100
+ .option('--pretty', 'Pretty print JSON output')
101
+ .action(async (openId: string, opts: MessageOptions) => {
102
+ cliOutput(await sendNewsAction(openId, opts), opts.pretty)
103
+ }),
104
+ )
@@ -0,0 +1,22 @@
1
+ import { formatOutput } from '@/shared/utils/output'
2
+
3
+ import { WeChatBotClient } from '../client'
4
+ import { WeChatBotCredentialManager } from '../credential-manager'
5
+
6
+ export interface AccountOption {
7
+ account?: string
8
+ pretty?: boolean
9
+ _credManager?: WeChatBotCredentialManager
10
+ }
11
+
12
+ export async function getClient(options: AccountOption): Promise<WeChatBotClient> {
13
+ const credManager = options._credManager ?? new WeChatBotCredentialManager()
14
+ const creds = await credManager.getCredentials(options.account)
15
+
16
+ if (!creds) {
17
+ console.log(formatOutput({ error: 'No credentials. Run "auth set <app-id> <app-secret>" first.' }, options.pretty))
18
+ process.exit(1)
19
+ }
20
+
21
+ return await new WeChatBotClient().login({ appId: creds.app_id, appSecret: creds.app_secret })
22
+ }
@@ -0,0 +1,199 @@
1
+ import { afterAll, describe, expect, mock, test } from 'bun:test'
2
+ import { rmSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ import { WeChatBotCredentialManager } from '@/platforms/wechatbot/credential-manager'
6
+
7
+ const mockTemplates = [
8
+ {
9
+ template_id: 'tmpl-001',
10
+ title: 'Order Notification',
11
+ primary_industry: 'IT科技',
12
+ deputy_industry: '互联网|电子商务',
13
+ content: 'ORDER_STATUS {{status.DATA}}',
14
+ example: 'Order shipped',
15
+ },
16
+ ]
17
+
18
+ mock.module('../client', () => ({
19
+ WeChatBotClient: class MockWeChatBotClient {
20
+ async login() {
21
+ return this
22
+ }
23
+ listTemplates = mock(() => Promise.resolve(mockTemplates))
24
+ sendTemplateMessage = mock(() => Promise.resolve({ msgid: 12345 }))
25
+ deleteTemplate = mock(() => Promise.resolve())
26
+ },
27
+ }))
28
+
29
+ const { listAction } = await import('@/platforms/wechatbot/commands/template')
30
+
31
+ const testDirs: string[] = []
32
+
33
+ function makeCredManager(): WeChatBotCredentialManager {
34
+ const dir = join(
35
+ import.meta.dir,
36
+ `.test-template-config-${Date.now()}-${Math.random().toString(36).slice(2)}`,
37
+ )
38
+ testDirs.push(dir)
39
+ return new WeChatBotCredentialManager(dir)
40
+ }
41
+
42
+ async function makeCredManagerWithCreds(): Promise<WeChatBotCredentialManager> {
43
+ const manager = makeCredManager()
44
+ await manager.setCredentials({ app_id: 'wx123', app_secret: 'secret123', account_name: 'My Account' })
45
+ return manager
46
+ }
47
+
48
+ afterAll(() => {
49
+ for (const dir of testDirs) {
50
+ rmSync(dir, { recursive: true, force: true })
51
+ }
52
+ })
53
+
54
+ describe('listAction', () => {
55
+ test('returns templates list', async () => {
56
+ const credManager = await makeCredManagerWithCreds()
57
+ const result = await listAction({ _credManager: credManager })
58
+
59
+ expect(result.templates).toHaveLength(1)
60
+ expect(result.templates?.[0].template_id).toBe('tmpl-001')
61
+ expect(result.templates?.[0].title).toBe('Order Notification')
62
+ })
63
+
64
+ test('returns error when client throws', async () => {
65
+ mock.module('../client', () => ({
66
+ WeChatBotClient: class MockWeChatBotClient {
67
+ async login() {
68
+ return this
69
+ }
70
+ listTemplates = mock(() => Promise.reject(new Error('API error')))
71
+ },
72
+ }))
73
+
74
+ const { listAction: listActionFresh } = await import('@/platforms/wechatbot/commands/template')
75
+ const credManager = await makeCredManagerWithCreds()
76
+ const result = await listActionFresh({ _credManager: credManager })
77
+
78
+ expect(result.error).toBe('API error')
79
+ expect(result.templates).toBeUndefined()
80
+ })
81
+ })
82
+
83
+ describe('sendAction (template)', () => {
84
+ test('sends template message and returns msgid', async () => {
85
+ mock.module('../client', () => ({
86
+ WeChatBotClient: class MockWeChatBotClient {
87
+ async login() {
88
+ return this
89
+ }
90
+ sendTemplateMessage = mock(() => Promise.resolve({ msgid: 99999 }))
91
+ },
92
+ }))
93
+
94
+ const { sendAction: sendActionFresh } = await import('@/platforms/wechatbot/commands/template')
95
+ const credManager = await makeCredManagerWithCreds()
96
+ const result = await sendActionFresh('openid-123', 'tmpl-001', {
97
+ data: JSON.stringify({ first: { value: 'Hello' } }),
98
+ url: 'https://example.com',
99
+ _credManager: credManager,
100
+ })
101
+
102
+ expect(result.msgid).toBe(99999)
103
+ expect(result.error).toBeUndefined()
104
+ })
105
+
106
+ test('returns error when data is invalid JSON', async () => {
107
+ mock.module('../client', () => ({
108
+ WeChatBotClient: class MockWeChatBotClient {
109
+ async login() {
110
+ return this
111
+ }
112
+ sendTemplateMessage = mock(() => Promise.resolve({ msgid: 1 }))
113
+ },
114
+ }))
115
+
116
+ const { sendAction: sendActionFresh } = await import('@/platforms/wechatbot/commands/template')
117
+ const credManager = await makeCredManagerWithCreds()
118
+ const result = await sendActionFresh('openid-123', 'tmpl-001', {
119
+ data: '{invalid json}',
120
+ _credManager: credManager,
121
+ })
122
+
123
+ expect(result.error).toContain('Invalid --data JSON')
124
+ expect(result.msgid).toBeUndefined()
125
+ })
126
+
127
+ test('sends without data when not provided', async () => {
128
+ const sendTemplateMsg = mock(() => Promise.resolve({ msgid: 777 }))
129
+ mock.module('../client', () => ({
130
+ WeChatBotClient: class MockWeChatBotClient {
131
+ async login() {
132
+ return this
133
+ }
134
+ sendTemplateMessage = sendTemplateMsg
135
+ },
136
+ }))
137
+
138
+ const { sendAction: sendActionFresh } = await import('@/platforms/wechatbot/commands/template')
139
+ const credManager = await makeCredManagerWithCreds()
140
+ const result = await sendActionFresh('openid-123', 'tmpl-001', { _credManager: credManager })
141
+
142
+ expect(result.msgid).toBe(777)
143
+ })
144
+
145
+ test('returns error when client throws', async () => {
146
+ mock.module('../client', () => ({
147
+ WeChatBotClient: class MockWeChatBotClient {
148
+ async login() {
149
+ return this
150
+ }
151
+ sendTemplateMessage = mock(() => Promise.reject(new Error('template send failed')))
152
+ },
153
+ }))
154
+
155
+ const { sendAction: sendActionFresh } = await import('@/platforms/wechatbot/commands/template')
156
+ const credManager = await makeCredManagerWithCreds()
157
+ const result = await sendActionFresh('openid-123', 'tmpl-001', { _credManager: credManager })
158
+
159
+ expect(result.error).toBe('template send failed')
160
+ })
161
+ })
162
+
163
+ describe('deleteAction', () => {
164
+ test('deletes template and returns success', async () => {
165
+ mock.module('../client', () => ({
166
+ WeChatBotClient: class MockWeChatBotClient {
167
+ async login() {
168
+ return this
169
+ }
170
+ deleteTemplate = mock(() => Promise.resolve())
171
+ },
172
+ }))
173
+
174
+ const { deleteAction: deleteActionFresh } = await import('@/platforms/wechatbot/commands/template')
175
+ const credManager = await makeCredManagerWithCreds()
176
+ const result = await deleteActionFresh('tmpl-to-delete', { _credManager: credManager })
177
+
178
+ expect(result.success).toBe(true)
179
+ expect(result.error).toBeUndefined()
180
+ })
181
+
182
+ test('returns error when client throws', async () => {
183
+ mock.module('../client', () => ({
184
+ WeChatBotClient: class MockWeChatBotClient {
185
+ async login() {
186
+ return this
187
+ }
188
+ deleteTemplate = mock(() => Promise.reject(new Error('template not found')))
189
+ },
190
+ }))
191
+
192
+ const { deleteAction: deleteActionFresh } = await import('@/platforms/wechatbot/commands/template')
193
+ const credManager = await makeCredManagerWithCreds()
194
+ const result = await deleteActionFresh('nonexistent-tmpl', { _credManager: credManager })
195
+
196
+ expect(result.error).toBe('template not found')
197
+ expect(result.success).toBeUndefined()
198
+ })
199
+ })
@@ -0,0 +1,102 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { formatOutput } from '@/shared/utils/output'
4
+
5
+ import type { WeChatBotTemplate } from '../types'
6
+ import type { AccountOption } from './shared'
7
+ import { getClient } from './shared'
8
+
9
+ interface TemplateResult {
10
+ templates?: WeChatBotTemplate[]
11
+ msgid?: number
12
+ success?: boolean
13
+ error?: string
14
+ }
15
+
16
+ type TemplateOptions = AccountOption & {
17
+ data?: string
18
+ url?: string
19
+ }
20
+
21
+ export async function listAction(options: TemplateOptions): Promise<TemplateResult> {
22
+ try {
23
+ const client = await getClient(options)
24
+ const templates = await client.listTemplates()
25
+ return { templates }
26
+ } catch (error) {
27
+ return { error: (error as Error).message }
28
+ }
29
+ }
30
+
31
+ export async function sendAction(
32
+ openId: string,
33
+ templateId: string,
34
+ options: TemplateOptions,
35
+ ): Promise<TemplateResult> {
36
+ try {
37
+ let data: Record<string, { value: string }> = {}
38
+ if (options.data) {
39
+ try {
40
+ data = JSON.parse(options.data) as Record<string, { value: string }>
41
+ } catch {
42
+ return { error: 'Invalid --data JSON' }
43
+ }
44
+ }
45
+
46
+ const client = await getClient(options)
47
+ const result = await client.sendTemplateMessage(openId, templateId, data, options.url)
48
+ return { msgid: result.msgid }
49
+ } catch (error) {
50
+ return { error: (error as Error).message }
51
+ }
52
+ }
53
+
54
+ export async function deleteAction(templateId: string, options: TemplateOptions): Promise<TemplateResult> {
55
+ try {
56
+ const client = await getClient(options)
57
+ await client.deleteTemplate(templateId)
58
+ return { success: true }
59
+ } catch (error) {
60
+ return { error: (error as Error).message }
61
+ }
62
+ }
63
+
64
+ function cliOutput(result: TemplateResult, pretty?: boolean): void {
65
+ console.log(formatOutput(result, pretty))
66
+ if (result.error) process.exit(1)
67
+ }
68
+
69
+ export const templateCommand = new Command('template')
70
+ .description('Template message commands')
71
+ .addCommand(
72
+ new Command('list')
73
+ .description('List all private templates')
74
+ .option('--account <id>', 'Account ID')
75
+ .option('--pretty', 'Pretty print JSON output')
76
+ .action(async (opts: TemplateOptions) => {
77
+ cliOutput(await listAction(opts), opts.pretty)
78
+ }),
79
+ )
80
+ .addCommand(
81
+ new Command('send')
82
+ .description('Send a template message')
83
+ .argument('<open-id>', 'Recipient OpenID')
84
+ .argument('<template-id>', 'Template ID')
85
+ .option('--data <json>', 'Template data as JSON object')
86
+ .option('--url <url>', 'URL to redirect when message is clicked')
87
+ .option('--account <id>', 'Account ID')
88
+ .option('--pretty', 'Pretty print JSON output')
89
+ .action(async (openId: string, templateId: string, opts: TemplateOptions) => {
90
+ cliOutput(await sendAction(openId, templateId, opts), opts.pretty)
91
+ }),
92
+ )
93
+ .addCommand(
94
+ new Command('delete')
95
+ .description('Delete a private template')
96
+ .argument('<template-id>', 'Template ID to delete')
97
+ .option('--account <id>', 'Account ID')
98
+ .option('--pretty', 'Pretty print JSON output')
99
+ .action(async (templateId: string, opts: TemplateOptions) => {
100
+ cliOutput(await deleteAction(templateId, opts), opts.pretty)
101
+ }),
102
+ )