@johpaz/hive-core 1.0.6 → 1.0.7
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/package.json +5 -2
- package/src/agent/index.ts +145 -17
- package/src/agent/providers/index.ts +20 -1
- package/src/agent/workspace.ts +111 -0
- package/src/channels/base.ts +28 -2
- package/src/channels/discord.ts +58 -10
- package/src/channels/manager.ts +114 -3
- package/src/channels/slack.ts +38 -4
- package/src/channels/telegram.ts +263 -43
- package/src/channels/webchat.ts +22 -0
- package/src/channels/whatsapp.ts +51 -3
- package/src/config/loader.ts +47 -8
- package/src/gateway/server.ts +612 -240
- package/src/gateway/session.ts +2 -1
- package/src/gateway/slash-commands.ts +7 -14
- package/src/memory/notes.ts +28 -130
- package/src/multi-agent/manager.ts +28 -0
- package/src/storage/sqlite.ts +230 -0
- package/src/tools/workspace.ts +171 -0
package/src/channels/manager.ts
CHANGED
|
@@ -25,7 +25,8 @@ export class ChannelManager {
|
|
|
25
25
|
const channelConfigs = this.config.channels ?? {};
|
|
26
26
|
|
|
27
27
|
for (const [channelName, channelConfig] of Object.entries(channelConfigs)) {
|
|
28
|
-
|
|
28
|
+
// If enabled is explicitly false, skip
|
|
29
|
+
if (channelConfig.enabled === false) {
|
|
29
30
|
this.log.debug(`Channel ${channelName} is disabled`);
|
|
30
31
|
continue;
|
|
31
32
|
}
|
|
@@ -61,7 +62,7 @@ export class ChannelManager {
|
|
|
61
62
|
channel = createTelegramChannel(accountId, {
|
|
62
63
|
enabled: true,
|
|
63
64
|
botToken: config.botToken as string,
|
|
64
|
-
dmPolicy: (config.dmPolicy as "open" | "pairing" | "allowlist") ?? "
|
|
65
|
+
dmPolicy: (config.dmPolicy as "open" | "pairing" | "allowlist") ?? "open",
|
|
65
66
|
allowFrom: (config.allowFrom as string[]) ?? [],
|
|
66
67
|
groups: (config.groups as boolean) ?? false,
|
|
67
68
|
} as TelegramConfig);
|
|
@@ -173,6 +174,95 @@ export class ChannelManager {
|
|
|
173
174
|
return undefined;
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
async removeChannel(channelName: string, accountId: string): Promise<void> {
|
|
178
|
+
const key = `${channelName}:${accountId}`;
|
|
179
|
+
await this.stopChannel(channelName, accountId);
|
|
180
|
+
this.channels.delete(key);
|
|
181
|
+
this.log.info(`Removed channel: ${key}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getAccountConfig(channelName: string, accountId: string): any {
|
|
185
|
+
const channelConfigs = (this.config.channels ?? {}) as Record<string, any>;
|
|
186
|
+
const channelConfig = channelConfigs[channelName];
|
|
187
|
+
if (!channelConfig) return null;
|
|
188
|
+
|
|
189
|
+
const accounts = channelConfig.accounts ?? { default: channelConfig };
|
|
190
|
+
return accounts[accountId] || null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async startChannel(channelName: string, accountId: string): Promise<void> {
|
|
194
|
+
const key = `${channelName}:${accountId}`;
|
|
195
|
+
let channel = this.channels.get(key);
|
|
196
|
+
|
|
197
|
+
if (!channel) {
|
|
198
|
+
const channelConfigs = (this.config.channels ?? {}) as Record<string, any>;
|
|
199
|
+
const channelConfig = channelConfigs[channelName];
|
|
200
|
+
if (!channelConfig) {
|
|
201
|
+
throw new Error(`Channel configuration not found: ${channelName}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const accounts = channelConfig.accounts ?? { default: channelConfig };
|
|
205
|
+
const accountConfig = accounts[accountId];
|
|
206
|
+
if (!accountConfig && accountId !== "default") {
|
|
207
|
+
throw new Error(`Account configuration not found: ${accountId} for channel ${channelName}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const fullConfig = { ...channelConfig, ...(accountConfig ?? {}) };
|
|
211
|
+
await this.createChannel(channelName, accountId, fullConfig as any);
|
|
212
|
+
channel = this.channels.get(key);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!channel) {
|
|
216
|
+
throw new Error(`Failed to instantiate channel: ${key}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (channel.isRunning()) {
|
|
220
|
+
this.log.info(`Channel ${key} is already running`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await channel.start();
|
|
225
|
+
this.log.info(`Started channel: ${key}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async stopChannel(channelName: string, accountId: string): Promise<void> {
|
|
229
|
+
const key = `${channelName}:${accountId}`;
|
|
230
|
+
const channel = this.channels.get(key);
|
|
231
|
+
|
|
232
|
+
if (!channel) {
|
|
233
|
+
this.log.warn(`Channel ${key} not found or not instantiated`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!channel.isRunning()) {
|
|
238
|
+
this.log.info(`Channel ${key} is not running`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await channel.stop();
|
|
243
|
+
this.log.info(`Stopped channel: ${key}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
listAllAvailableChannels(): Array<{ name: string; accountId: string; running: boolean; enabled: boolean }> {
|
|
247
|
+
const available: Array<{ name: string; accountId: string; running: boolean; enabled: boolean }> = [];
|
|
248
|
+
const channelConfigs = (this.config.channels ?? {}) as Record<string, any>;
|
|
249
|
+
|
|
250
|
+
for (const [channelName, channelConfig] of Object.entries(channelConfigs)) {
|
|
251
|
+
const accounts = channelConfig.accounts ?? { default: channelConfig };
|
|
252
|
+
for (const accountId of Object.keys(accounts)) {
|
|
253
|
+
const key = `${channelName}:${accountId}`;
|
|
254
|
+
const channel = this.channels.get(key);
|
|
255
|
+
available.push({
|
|
256
|
+
name: channelName,
|
|
257
|
+
accountId: accountId,
|
|
258
|
+
running: channel ? channel.isRunning() : false,
|
|
259
|
+
enabled: channelConfig.enabled !== false,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return available;
|
|
264
|
+
}
|
|
265
|
+
|
|
176
266
|
listChannels(): Array<{ name: string; accountId: string; running: boolean }> {
|
|
177
267
|
return Array.from(this.channels.entries()).map(([key, channel]) => {
|
|
178
268
|
const [name, accountId] = key.split(":");
|
|
@@ -190,13 +280,34 @@ export class ChannelManager {
|
|
|
190
280
|
message: unknown
|
|
191
281
|
): Promise<void> {
|
|
192
282
|
const channel = this.getChannel(channelName);
|
|
193
|
-
|
|
283
|
+
|
|
194
284
|
if (!channel) {
|
|
195
285
|
throw new Error(`Channel not found: ${channelName}`);
|
|
196
286
|
}
|
|
197
287
|
|
|
198
288
|
await channel.send(sessionId, message as any);
|
|
199
289
|
}
|
|
290
|
+
|
|
291
|
+
async startTyping(channelName: string, sessionId: string): Promise<void> {
|
|
292
|
+
const channel = this.getChannel(channelName);
|
|
293
|
+
if (channel?.startTyping) {
|
|
294
|
+
await channel.startTyping(sessionId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async stopTyping(channelName: string, sessionId: string): Promise<void> {
|
|
299
|
+
const channel = this.getChannel(channelName);
|
|
300
|
+
if (channel?.stopTyping) {
|
|
301
|
+
await channel.stopTyping(sessionId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async markAsRead(channelName: string, sessionId: string, messageId?: string): Promise<void> {
|
|
306
|
+
const channel = this.getChannel(channelName);
|
|
307
|
+
if (channel?.markAsRead) {
|
|
308
|
+
await channel.markAsRead(sessionId, messageId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
200
311
|
}
|
|
201
312
|
|
|
202
313
|
export function createChannelManager(config: Config): ChannelManager {
|
package/src/channels/slack.ts
CHANGED
|
@@ -25,6 +25,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
25
25
|
status: "disconnected",
|
|
26
26
|
};
|
|
27
27
|
private log = logger.child("slack");
|
|
28
|
+
private pendingMessages: Map<string, { ts: string; channel: string }> = new Map();
|
|
28
29
|
|
|
29
30
|
constructor(config: SlackConfig) {
|
|
30
31
|
super();
|
|
@@ -171,6 +172,29 @@ export class SlackChannel extends BaseChannel {
|
|
|
171
172
|
await this.handleMessage(incoming);
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
async startTyping(sessionId: string): Promise<void> {
|
|
176
|
+
if (!this.app) return;
|
|
177
|
+
|
|
178
|
+
const peerId = this.extractPeerId(sessionId);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const result = await this.app.client.chat.postMessage({
|
|
182
|
+
channel: peerId,
|
|
183
|
+
text: "⏳ Procesando...",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (result.ts && result.channel) {
|
|
187
|
+
this.pendingMessages.set(sessionId, { ts: result.ts as string, channel: result.channel as string });
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.log.debug(`Could not send typing placeholder: ${(error as Error).message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async stopTyping(_sessionId: string): Promise<void> {
|
|
195
|
+
// No-op for Slack - we edit the message in send()
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
175
199
|
if (!this.app) {
|
|
176
200
|
throw new Error("Slack not connected");
|
|
@@ -180,12 +204,22 @@ export class SlackChannel extends BaseChannel {
|
|
|
180
204
|
if (!text) return;
|
|
181
205
|
|
|
182
206
|
const peerId = this.extractPeerId(sessionId);
|
|
207
|
+
const pending = this.pendingMessages.get(sessionId);
|
|
183
208
|
|
|
184
209
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
if (pending) {
|
|
211
|
+
await this.app.client.chat.update({
|
|
212
|
+
channel: pending.channel,
|
|
213
|
+
ts: pending.ts,
|
|
214
|
+
text,
|
|
215
|
+
});
|
|
216
|
+
this.pendingMessages.delete(sessionId);
|
|
217
|
+
} else {
|
|
218
|
+
await this.app.client.chat.postMessage({
|
|
219
|
+
channel: peerId,
|
|
220
|
+
text,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
189
223
|
|
|
190
224
|
this.log.debug(`Sent message to ${peerId}`);
|
|
191
225
|
} catch (error) {
|
package/src/channels/telegram.ts
CHANGED
|
@@ -11,14 +11,21 @@ export class TelegramChannel extends BaseChannel {
|
|
|
11
11
|
name = "telegram";
|
|
12
12
|
accountId: string;
|
|
13
13
|
config: TelegramConfig;
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
private bot?: Bot;
|
|
16
16
|
private log = logger.child("telegram");
|
|
17
|
+
private chatIdCache: Map<string, number> = new Map();
|
|
18
|
+
private messageIdCache: Map<string, number> = new Map();
|
|
17
19
|
|
|
18
20
|
constructor(accountId: string, config: TelegramConfig) {
|
|
19
21
|
super();
|
|
20
22
|
this.accountId = accountId;
|
|
21
|
-
this.config =
|
|
23
|
+
this.config = {
|
|
24
|
+
...config,
|
|
25
|
+
dmPolicy: config.dmPolicy ?? "open",
|
|
26
|
+
allowFrom: config.allowFrom ?? [],
|
|
27
|
+
enabled: config.enabled ?? true,
|
|
28
|
+
};
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
async start(): Promise<void> {
|
|
@@ -28,121 +35,331 @@ export class TelegramChannel extends BaseChannel {
|
|
|
28
35
|
|
|
29
36
|
this.bot = new Bot(this.config.botToken);
|
|
30
37
|
|
|
31
|
-
this.bot.on("message
|
|
38
|
+
this.bot.on("message", async (ctx: Context) => {
|
|
32
39
|
await this.handleTelegramMessage(ctx);
|
|
33
40
|
});
|
|
34
41
|
|
|
35
|
-
this.bot.on("edited_message
|
|
42
|
+
this.bot.on("edited_message", async (ctx: Context) => {
|
|
36
43
|
await this.handleTelegramMessage(ctx);
|
|
37
44
|
});
|
|
38
45
|
|
|
39
|
-
this.bot.catch((err) => {
|
|
46
|
+
this.bot.catch((err: Error) => {
|
|
40
47
|
this.log.error(`Telegram error: ${err.message}`);
|
|
41
48
|
});
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} catch (error) {
|
|
53
|
-
this.log.error(`Failed to start Telegram bot: ${(error as Error).message}`);
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
50
|
+
this.bot.start({
|
|
51
|
+
onStart: () => {
|
|
52
|
+
this.running = true;
|
|
53
|
+
this.log.info(`Telegram bot started: @${this.bot?.botInfo?.username ?? "unknown"}`);
|
|
54
|
+
},
|
|
55
|
+
}).catch((error: Error) => {
|
|
56
|
+
this.log.error(`Telegram bot error: ${error.message}`);
|
|
57
|
+
this.running = false;
|
|
58
|
+
});
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
private async handleTelegramMessage(ctx: Context): Promise<void> {
|
|
59
62
|
const message = ctx.message ?? ctx.editedMessage;
|
|
60
|
-
if (!message
|
|
63
|
+
if (!message) return;
|
|
61
64
|
|
|
62
65
|
const chatId = message.chat.id.toString();
|
|
66
|
+
const userId = message.from?.id?.toString() ?? "unknown";
|
|
63
67
|
const isGroup = message.chat.type === "group" || message.chat.type === "supergroup";
|
|
64
68
|
const kind = isGroup ? "group" : "direct";
|
|
65
|
-
const peerId = isGroup
|
|
69
|
+
const peerId = isGroup
|
|
66
70
|
? `${message.chat.id}:${message.from?.id ?? "unknown"}`
|
|
67
71
|
: chatId;
|
|
72
|
+
const messageId = message.message_id;
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if (message.from?.is_bot) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const text = message.text;
|
|
80
|
+
const isCommand = text?.startsWith("/") ?? false;
|
|
81
|
+
|
|
82
|
+
if (text === "/myid" || text?.startsWith("/myid@")) {
|
|
83
|
+
await ctx.reply(
|
|
84
|
+
`🆔 Tu Telegram ID es: <code>${userId}</code>\n\n` +
|
|
85
|
+
`Para autorizarte, añade esto a ~/.hive/hive.yaml:\n` +
|
|
86
|
+
`<pre>channels:\n telegram:\n accounts:\n default:\n allowFrom:\n - "tg:${userId}"</pre>\n\n` +
|
|
87
|
+
`O ejecuta:\n<code>hive config set channels.telegram.accounts.default.allowFrom.+ "tg:${userId}"</code>`,
|
|
88
|
+
{ parse_mode: "HTML" }
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (text === "/start" || text?.startsWith("/start@")) {
|
|
94
|
+
const agentName = "Bee";
|
|
95
|
+
await ctx.reply(
|
|
96
|
+
`¡Hola! Soy ${agentName}, tu asistente personal.\n\n` +
|
|
97
|
+
`Tu Telegram ID: <code>${userId}</code>\n\n` +
|
|
98
|
+
`Para empezar a usar el bot, asegúrate de estar autorizado.`,
|
|
99
|
+
{ parse_mode: "HTML" }
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (text === "/help" || text?.startsWith("/help@")) {
|
|
105
|
+
await ctx.reply(this.getHelpMessage(userId), { parse_mode: "HTML" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (text === "/stop" || text?.startsWith("/stop@")) {
|
|
110
|
+
await ctx.reply("⏹ Detención actual cancelada.", { parse_mode: "HTML" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (text === "/new" || text?.startsWith("/new@")) {
|
|
115
|
+
await ctx.reply("🔄 Sesión reiniciada.", { parse_mode: "HTML" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
68
118
|
|
|
69
119
|
if (!isGroup && !this.isUserAllowed(chatId)) {
|
|
70
120
|
this.log.debug(`Message from unauthorized user: ${chatId}`);
|
|
71
|
-
|
|
121
|
+
const rejectMsg = this.config.dmPolicy === "allowlist"
|
|
122
|
+
? `⛔ No estás autorizado.\n\n` +
|
|
123
|
+
`Tu Telegram ID: <code>${userId}</code>\n\n` +
|
|
124
|
+
`Para autorizarte:\n` +
|
|
125
|
+
`1. Ejecuta en el servidor: <code>hive config edit</code>\n` +
|
|
126
|
+
`2. Añade bajo channels.telegram.accounts.default.allowFrom:\n` +
|
|
127
|
+
`<pre> - "tg:${userId}"</pre>\n` +
|
|
128
|
+
`3. Ejecuta: <code>hive reload</code>`
|
|
129
|
+
: `⛔ No estás autorizado para usar este bot.\n\n` +
|
|
130
|
+
`Tu Telegram ID: <code>${userId}</code>`;
|
|
131
|
+
await ctx.reply(rejectMsg, { parse_mode: "HTML" });
|
|
72
132
|
return;
|
|
73
133
|
}
|
|
74
134
|
|
|
75
|
-
if (isGroup && !this.config.groups) {
|
|
135
|
+
if (isGroup && !(this.config.groups ?? false)) {
|
|
76
136
|
return;
|
|
77
137
|
}
|
|
78
138
|
|
|
139
|
+
let content = text;
|
|
140
|
+
let contentType = "text";
|
|
141
|
+
|
|
142
|
+
if (message.photo && !text) {
|
|
143
|
+
const caption = message.caption ?? "";
|
|
144
|
+
if (caption) {
|
|
145
|
+
content = `[📷 Foto] ${caption}`;
|
|
146
|
+
contentType = "photo";
|
|
147
|
+
} else {
|
|
148
|
+
await ctx.reply(
|
|
149
|
+
"📷 Recibí tu foto. Por favor, añade texto descriptivo para que pueda procesarla.",
|
|
150
|
+
{ parse_mode: "HTML" }
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (message.voice) {
|
|
157
|
+
await ctx.reply(
|
|
158
|
+
"🎤 Recibí tu mensaje de voz. El procesamiento de audio no está disponible todavía.",
|
|
159
|
+
{ parse_mode: "HTML" }
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (message.sticker) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (message.document && !text) {
|
|
169
|
+
const docName = (message.document as any).file_name ?? "documento";
|
|
170
|
+
const caption = message.caption ?? "";
|
|
171
|
+
if (caption) {
|
|
172
|
+
content = `[📎 ${docName}] ${caption}`;
|
|
173
|
+
contentType = "document";
|
|
174
|
+
} else {
|
|
175
|
+
await ctx.reply(
|
|
176
|
+
"📎 Recibí tu documento. Por favor, añade texto descriptivo para que pueda procesarlo.",
|
|
177
|
+
{ parse_mode: "HTML" }
|
|
178
|
+
);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sessionId = this.formatSessionId(peerId, kind);
|
|
184
|
+
this.chatIdCache.set(sessionId, message.chat.id);
|
|
185
|
+
this.messageIdCache.set(sessionId, messageId);
|
|
186
|
+
|
|
79
187
|
const incomingMessage: IncomingMessage = {
|
|
80
|
-
sessionId
|
|
188
|
+
sessionId,
|
|
81
189
|
channel: "telegram",
|
|
82
190
|
accountId: this.accountId,
|
|
83
191
|
peerId,
|
|
84
192
|
peerKind: kind,
|
|
85
|
-
content:
|
|
193
|
+
content: content ?? "",
|
|
86
194
|
metadata: {
|
|
87
195
|
telegram: {
|
|
88
196
|
chatId: message.chat.id,
|
|
89
197
|
userId: message.from?.id,
|
|
90
198
|
username: message.from?.username,
|
|
91
|
-
messageId
|
|
199
|
+
messageId,
|
|
92
200
|
chatType: message.chat.type,
|
|
201
|
+
contentType,
|
|
93
202
|
},
|
|
94
203
|
},
|
|
95
|
-
replyToId: message.reply_to_message
|
|
96
|
-
? `tg:${message.reply_to_message.message_id}`
|
|
204
|
+
replyToId: message.reply_to_message
|
|
205
|
+
? `tg:${message.reply_to_message.message_id}`
|
|
97
206
|
: undefined,
|
|
98
207
|
};
|
|
99
208
|
|
|
100
209
|
await this.handleMessage(incomingMessage);
|
|
101
210
|
}
|
|
102
211
|
|
|
212
|
+
private getHelpMessage(_userId: string): string {
|
|
213
|
+
return `📚 <b>Comandos disponibles:</b>
|
|
214
|
+
|
|
215
|
+
<code>/myid</code> - Muestra tu Telegram ID
|
|
216
|
+
<code>/start</code> - Iniciar conversación
|
|
217
|
+
<code>/help</code> - Mostrar esta ayuda
|
|
218
|
+
<code>/stop</code> - Detener tarea actual
|
|
219
|
+
<code>/new</code> - Reiniciar sesión
|
|
220
|
+
|
|
221
|
+
💡 <i>Envía un mensaje para comenzar.</i>`;
|
|
222
|
+
}
|
|
223
|
+
|
|
103
224
|
async stop(): Promise<void> {
|
|
104
225
|
if (this.bot) {
|
|
105
|
-
this.bot.stop();
|
|
226
|
+
await this.bot.stop();
|
|
106
227
|
this.running = false;
|
|
107
228
|
this.log.info("Telegram bot stopped");
|
|
108
229
|
}
|
|
109
230
|
}
|
|
110
231
|
|
|
232
|
+
private getChatIdFromSession(sessionId: string): number {
|
|
233
|
+
const cached = this.chatIdCache.get(sessionId);
|
|
234
|
+
if (cached) return cached;
|
|
235
|
+
|
|
236
|
+
const parts = sessionId.split(":");
|
|
237
|
+
const peerId = parts.slice(4).join(":");
|
|
238
|
+
const chatIdStr = peerId.split(":")[0];
|
|
239
|
+
return Number(chatIdStr);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private getMessageIdFromSession(sessionId: string): number | undefined {
|
|
243
|
+
return this.messageIdCache.get(sessionId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async startTyping(sessionId: string): Promise<void> {
|
|
247
|
+
if (!this.bot) return;
|
|
248
|
+
|
|
249
|
+
const chatId = this.getChatIdFromSession(sessionId);
|
|
250
|
+
if (isNaN(chatId)) return;
|
|
251
|
+
|
|
252
|
+
await this.bot.api.sendChatAction(chatId, "typing");
|
|
253
|
+
|
|
254
|
+
const interval = setInterval(async () => {
|
|
255
|
+
try {
|
|
256
|
+
await this.bot!.api.sendChatAction(chatId, "typing");
|
|
257
|
+
} catch {
|
|
258
|
+
this.stopTyping(sessionId);
|
|
259
|
+
}
|
|
260
|
+
}, 4000);
|
|
261
|
+
|
|
262
|
+
this.typingIntervals.set(sessionId, interval);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async stopTyping(sessionId: string): Promise<void> {
|
|
266
|
+
const interval = this.typingIntervals.get(sessionId);
|
|
267
|
+
if (interval) {
|
|
268
|
+
clearInterval(interval);
|
|
269
|
+
this.typingIntervals.delete(sessionId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
111
273
|
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
112
274
|
if (!this.bot) {
|
|
113
275
|
throw new Error("Telegram bot not started");
|
|
114
276
|
}
|
|
115
277
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
const chatId = Number(chatIdStr);
|
|
278
|
+
await this.stopTyping(sessionId);
|
|
279
|
+
|
|
280
|
+
const chatId = this.getChatIdFromSession(sessionId);
|
|
120
281
|
|
|
121
282
|
if (isNaN(chatId)) {
|
|
122
283
|
throw new Error(`Invalid chat ID from session: ${sessionId}`);
|
|
123
284
|
}
|
|
124
285
|
|
|
125
286
|
const content = message.content ?? "";
|
|
287
|
+
|
|
288
|
+
if (!content || content.trim().length === 0) {
|
|
289
|
+
this.log.warn(`Empty response from agent, skipping send`, { sessionId, chatId });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const replyToId = this.getMessageIdFromSession(sessionId);
|
|
126
294
|
const maxLength = 4096;
|
|
127
295
|
|
|
128
296
|
try {
|
|
129
297
|
if (content.length <= maxLength) {
|
|
130
|
-
await this.
|
|
298
|
+
await this.sendWithRetry(chatId, content, replyToId);
|
|
131
299
|
} else {
|
|
132
300
|
const chunks = this.chunkMessage(content, maxLength);
|
|
133
|
-
for (
|
|
134
|
-
|
|
301
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
302
|
+
await this.sendWithRetry(chatId, chunks[i]!, i === 0 ? replyToId : undefined);
|
|
303
|
+
if (i < chunks.length - 1) {
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
135
305
|
}
|
|
136
306
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
307
|
+
}
|
|
308
|
+
} catch (error: unknown) {
|
|
309
|
+
if (error instanceof GrammyError) {
|
|
310
|
+
this.log.error(`Telegram API error: ${error.description}`);
|
|
311
|
+
|
|
312
|
+
if (error.error_code === 403) {
|
|
313
|
+
this.log.warn(`Bot was blocked by user: ${chatId}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
} else if (error instanceof Error) {
|
|
317
|
+
this.log.error(`Telegram send error: ${error.message}`);
|
|
318
|
+
} else {
|
|
319
|
+
this.log.error(`Telegram send error: ${String(error)}`);
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async sendWithRetry(
|
|
326
|
+
chatId: number,
|
|
327
|
+
text: string,
|
|
328
|
+
replyToId?: number
|
|
329
|
+
): Promise<void> {
|
|
330
|
+
const maxRetries = 3;
|
|
331
|
+
const backoffMs = [1000, 2000, 4000];
|
|
332
|
+
|
|
333
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
334
|
+
try {
|
|
335
|
+
const options: any = { parse_mode: "Markdown" };
|
|
336
|
+
if (replyToId) {
|
|
337
|
+
options.reply_parameters = { message_id: replyToId };
|
|
338
|
+
}
|
|
339
|
+
await this.bot!.api.sendMessage(chatId, text, options);
|
|
340
|
+
return;
|
|
341
|
+
} catch (error: unknown) {
|
|
342
|
+
const err = error as Error & { error_code?: number; parameters?: { retry_after?: number } };
|
|
343
|
+
|
|
344
|
+
if (err.error_code === 400) {
|
|
345
|
+
this.log.error(`Bad Request: ${err.message}`);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (err.error_code === 429) {
|
|
350
|
+
const retryAfter = err.parameters?.retry_after ?? 1;
|
|
351
|
+
this.log.warn(`Rate limited, waiting ${retryAfter}s`);
|
|
352
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (attempt < maxRetries - 1) {
|
|
357
|
+
this.log.warn(`Send failed, retrying in ${backoffMs[attempt]}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs[attempt]));
|
|
359
|
+
} else {
|
|
360
|
+
throw error;
|
|
143
361
|
}
|
|
144
362
|
}
|
|
145
|
-
throw error;
|
|
146
363
|
}
|
|
147
364
|
}
|
|
148
365
|
|
|
@@ -156,7 +373,10 @@ export class TelegramChannel extends BaseChannel {
|
|
|
156
373
|
break;
|
|
157
374
|
}
|
|
158
375
|
|
|
159
|
-
let splitPoint = remaining.lastIndexOf("\n", maxLength);
|
|
376
|
+
let splitPoint = remaining.lastIndexOf("\n\n", maxLength);
|
|
377
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
378
|
+
splitPoint = remaining.lastIndexOf("\n", maxLength);
|
|
379
|
+
}
|
|
160
380
|
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
161
381
|
splitPoint = remaining.lastIndexOf(" ", maxLength);
|
|
162
382
|
}
|
package/src/channels/webchat.ts
CHANGED
|
@@ -47,6 +47,28 @@ export class WebChatChannel extends BaseChannel {
|
|
|
47
47
|
this.log.debug(`WebChat connection unregistered: ${sessionId}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
async startTyping(sessionId: string): Promise<void> {
|
|
51
|
+
const ws = this.connections.get(sessionId);
|
|
52
|
+
if (!ws) return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: true }));
|
|
56
|
+
} catch {
|
|
57
|
+
// Connection closed
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async stopTyping(sessionId: string): Promise<void> {
|
|
62
|
+
const ws = this.connections.get(sessionId);
|
|
63
|
+
if (!ws) return;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false }));
|
|
67
|
+
} catch {
|
|
68
|
+
// Connection closed
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
51
73
|
const ws = this.connections.get(sessionId);
|
|
52
74
|
|