agent-messenger 2.27.0 → 2.27.2
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/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/listener.d.ts +9 -1
- package/dist/src/platforms/discord/listener.d.ts.map +1 -1
- package/dist/src/platforms/discord/listener.js +79 -3
- package/dist/src/platforms/discord/listener.js.map +1 -1
- package/dist/src/platforms/discordbot/listener.d.ts +6 -0
- package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/listener.js +78 -2
- package/dist/src/platforms/discordbot/listener.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +4 -4
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/package.json +1 -1
- 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 +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- 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-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/listener.test.ts +175 -143
- package/src/platforms/discord/listener.ts +87 -3
- package/src/platforms/discordbot/listener.test.ts +186 -184
- package/src/platforms/discordbot/listener.ts +83 -2
- package/src/platforms/kakaotalk/client.test.ts +40 -0
- package/src/platforms/kakaotalk/client.ts +6 -4
|
@@ -13,6 +13,11 @@ const RECONNECT_MAX_DELAY = 30_000
|
|
|
13
13
|
const NON_RECOVERABLE_CLOSE_CODES = [4004, 4010, 4011, 4012, 4013, 4014]
|
|
14
14
|
const SESSION_RESET_CLOSE_CODES = [4007, 4009]
|
|
15
15
|
|
|
16
|
+
// Hello arrives within milliseconds and READY within a few seconds on a healthy gateway;
|
|
17
|
+
// 15s is generous for a successful handshake while still failing fast on bad tokens,
|
|
18
|
+
// network stalls, or a gateway that accepts the socket but never delivers READY.
|
|
19
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 15_000
|
|
20
|
+
|
|
16
21
|
const DEFAULT_INTENTS =
|
|
17
22
|
DiscordIntent.Guilds |
|
|
18
23
|
DiscordIntent.GuildMessages |
|
|
@@ -26,11 +31,13 @@ type EventKey = keyof DiscordBotListenerEventMap
|
|
|
26
31
|
|
|
27
32
|
export interface DiscordBotListenerOptions {
|
|
28
33
|
intents?: number
|
|
34
|
+
connectTimeoutMs?: number
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
export class DiscordBotListener {
|
|
32
38
|
private client: DiscordBotClient
|
|
33
39
|
private intents: number
|
|
40
|
+
private connectTimeoutMs: number
|
|
34
41
|
private running = false
|
|
35
42
|
private ws: WebSocket | null = null
|
|
36
43
|
private emitter = new EventEmitter()
|
|
@@ -39,6 +46,7 @@ export class DiscordBotListener {
|
|
|
39
46
|
private heartbeatJitterTimer: ReturnType<typeof setTimeout> | null = null
|
|
40
47
|
private invalidSessionTimer: ReturnType<typeof setTimeout> | null = null
|
|
41
48
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
49
|
+
private connectTimeoutTimer: ReturnType<typeof setTimeout> | null = null
|
|
42
50
|
private reconnectAttempts = 0
|
|
43
51
|
private sequence: number | null = null
|
|
44
52
|
private sessionId: string | null = null
|
|
@@ -46,24 +54,91 @@ export class DiscordBotListener {
|
|
|
46
54
|
private token: string | null = null
|
|
47
55
|
private cachedUser: { id: string; username: string } | null = null
|
|
48
56
|
private generation = 0
|
|
57
|
+
private startPromise: Promise<void> | null = null
|
|
58
|
+
private pendingStartReject: ((error: Error) => void) | null = null
|
|
49
59
|
|
|
50
60
|
constructor(client: DiscordBotClient, options?: DiscordBotListenerOptions) {
|
|
51
61
|
this.client = client
|
|
52
62
|
this.intents = options?.intents ?? DEFAULT_INTENTS
|
|
63
|
+
this.connectTimeoutMs = options?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
async start(): Promise<void> {
|
|
67
|
+
if (this.startPromise) return this.startPromise
|
|
56
68
|
if (this.running) return
|
|
69
|
+
|
|
57
70
|
this.running = true
|
|
58
71
|
this.reconnectAttempts = 0
|
|
59
|
-
this.generation
|
|
60
|
-
|
|
72
|
+
const generation = ++this.generation
|
|
73
|
+
|
|
74
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
75
|
+
let settled = false
|
|
76
|
+
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
this.emitter.off('connected', onConnected)
|
|
79
|
+
this.emitter.off('error', onError)
|
|
80
|
+
this.pendingStartReject = null
|
|
81
|
+
if (this.connectTimeoutTimer) {
|
|
82
|
+
clearTimeout(this.connectTimeoutTimer)
|
|
83
|
+
this.connectTimeoutTimer = null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onConnected = () => {
|
|
88
|
+
if (settled || !this.isCurrent(generation)) return
|
|
89
|
+
settled = true
|
|
90
|
+
cleanup()
|
|
91
|
+
resolve()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const onError = (error: Error) => {
|
|
95
|
+
if (settled || !this.isCurrent(generation)) return
|
|
96
|
+
settled = true
|
|
97
|
+
cleanup()
|
|
98
|
+
this.teardownFailedStart(generation)
|
|
99
|
+
reject(error)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.emitter.once('connected', onConnected)
|
|
103
|
+
this.emitter.once('error', onError)
|
|
104
|
+
|
|
105
|
+
// Generation-agnostic on purpose: stop() invokes this to reject an in-flight start
|
|
106
|
+
// (after bumping generation) without leaking the once() handlers.
|
|
107
|
+
this.pendingStartReject = (error: Error) => {
|
|
108
|
+
if (settled) return
|
|
109
|
+
settled = true
|
|
110
|
+
cleanup()
|
|
111
|
+
reject(error)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.connectTimeoutTimer = setTimeout(() => {
|
|
115
|
+
onError(new Error(`Discord gateway did not become ready within ${this.connectTimeoutMs}ms`))
|
|
116
|
+
}, this.connectTimeoutMs)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const run = async (): Promise<void> => {
|
|
120
|
+
try {
|
|
121
|
+
await Promise.all([this.connect(generation), ready])
|
|
122
|
+
} finally {
|
|
123
|
+
if (this.generation === generation) this.startPromise = null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const startPromise = run()
|
|
127
|
+
this.startPromise = startPromise
|
|
128
|
+
return startPromise
|
|
61
129
|
}
|
|
62
130
|
|
|
63
131
|
stop(): void {
|
|
64
132
|
this.running = false
|
|
65
133
|
this.generation++
|
|
66
134
|
this.clearTimers()
|
|
135
|
+
if (this.connectTimeoutTimer) {
|
|
136
|
+
clearTimeout(this.connectTimeoutTimer)
|
|
137
|
+
this.connectTimeoutTimer = null
|
|
138
|
+
}
|
|
139
|
+
const rejectStart = this.pendingStartReject
|
|
140
|
+
this.pendingStartReject = null
|
|
141
|
+
this.startPromise = null
|
|
67
142
|
if (this.ws) {
|
|
68
143
|
this.ws.close()
|
|
69
144
|
this.ws = null
|
|
@@ -73,6 +148,12 @@ export class DiscordBotListener {
|
|
|
73
148
|
this.resumeGatewayUrl = null
|
|
74
149
|
this.token = null
|
|
75
150
|
this.cachedUser = null
|
|
151
|
+
rejectStart?.(new Error('Discord gateway start was stopped before becoming ready'))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private teardownFailedStart(generation: number): void {
|
|
155
|
+
if (!this.isCurrent(generation)) return
|
|
156
|
+
this.stop()
|
|
76
157
|
}
|
|
77
158
|
|
|
78
159
|
on<K extends EventKey>(event: K, listener: (...args: DiscordBotListenerEventMap[K]) => void): this {
|
|
@@ -540,6 +540,46 @@ describe('KakaoTalkClient', () => {
|
|
|
540
540
|
client.close()
|
|
541
541
|
})
|
|
542
542
|
|
|
543
|
+
it('normalizes Long-like chat ids from LCHATLIST pages', async () => {
|
|
544
|
+
const loginResult = {
|
|
545
|
+
...DEFAULT_LOGIN_RESULT,
|
|
546
|
+
eof: false,
|
|
547
|
+
}
|
|
548
|
+
mockLogin.mockResolvedValue(loginResult)
|
|
549
|
+
|
|
550
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
551
|
+
body: {
|
|
552
|
+
chatDatas: [
|
|
553
|
+
{
|
|
554
|
+
c: makeLong(300),
|
|
555
|
+
t: 1,
|
|
556
|
+
k: ['Dave'],
|
|
557
|
+
i: [4],
|
|
558
|
+
a: 1,
|
|
559
|
+
n: 0,
|
|
560
|
+
o: 1698000000,
|
|
561
|
+
l: { authorId: 4, message: 'from paginated chat', sendAt: 1698000000 },
|
|
562
|
+
ll: makeLong(100),
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
lastTokenId: makeLong(1),
|
|
566
|
+
lastChatId: makeLong(300),
|
|
567
|
+
eof: true,
|
|
568
|
+
},
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
572
|
+
const chats = await client.getChats({ all: true })
|
|
573
|
+
|
|
574
|
+
const paginatedChat = chats.find((chat) => chat.chat_id === '300')
|
|
575
|
+
expect(paginatedChat?.chat_id).toBe('300')
|
|
576
|
+
expect(paginatedChat?.display_name).toBe('Dave')
|
|
577
|
+
expect(paginatedChat?.last_message?.author_name).toBe('Dave')
|
|
578
|
+
expect(chats.some((chat) => chat.chat_id === '[object Object]')).toBe(false)
|
|
579
|
+
|
|
580
|
+
client.close()
|
|
581
|
+
})
|
|
582
|
+
|
|
543
583
|
it('wraps errors as KakaoTalkError', async () => {
|
|
544
584
|
mockLogin.mockRejectedValue(new Error('Connection refused'))
|
|
545
585
|
|
|
@@ -62,7 +62,7 @@ class MemberNameCache {
|
|
|
62
62
|
const names = chat.k as string[] | undefined
|
|
63
63
|
if (!Array.isArray(ids) || !Array.isArray(names)) continue
|
|
64
64
|
|
|
65
|
-
const chatId =
|
|
65
|
+
const chatId = longToString(chat.c)
|
|
66
66
|
let map = this.byChatId.get(chatId)
|
|
67
67
|
if (!map) {
|
|
68
68
|
map = new Map()
|
|
@@ -176,7 +176,7 @@ function formatChat(chat: ChatData, title: string | null, nameCache: MemberNameC
|
|
|
176
176
|
const memberNames = (chat.k ?? []) as string[]
|
|
177
177
|
const lastLog = chat.l as Record<string, unknown> | null
|
|
178
178
|
const displayName = memberNames.join(', ') || null
|
|
179
|
-
const chatId =
|
|
179
|
+
const chatId = longToString(chat.c)
|
|
180
180
|
|
|
181
181
|
return {
|
|
182
182
|
chat_id: chatId,
|
|
@@ -256,7 +256,7 @@ function findMaxLogId(logs: Array<Record<string, unknown>>, field: string): Long
|
|
|
256
256
|
|
|
257
257
|
function collectChats(chatDatas: ChatData[], into: ChatData[], seen: Set<string>): void {
|
|
258
258
|
for (const chat of chatDatas) {
|
|
259
|
-
const id =
|
|
259
|
+
const id = longToString(chat.c)
|
|
260
260
|
if (!seen.has(id)) {
|
|
261
261
|
seen.add(id)
|
|
262
262
|
into.push(chat)
|
|
@@ -771,7 +771,9 @@ export class KakaoTalkClient {
|
|
|
771
771
|
}
|
|
772
772
|
|
|
773
773
|
const titles = options?.resolveTitles
|
|
774
|
-
? await Promise.all(
|
|
774
|
+
? await Promise.all(
|
|
775
|
+
results.map((chat) => this.fetchChatTitle(session, parseLong(longToString(chat.c)), chat)),
|
|
776
|
+
)
|
|
775
777
|
: null
|
|
776
778
|
|
|
777
779
|
return results.map((chat, i) => formatChat(chat, titles ? titles[i] : null, this.nameCache))
|