@nextclaw/channel-runtime 0.1.1 → 0.1.3

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/dist/index.d.ts CHANGED
@@ -57,7 +57,7 @@ declare class DingTalkChannel extends BaseChannel<Config["channels"]["dingtalk"]
57
57
  declare class DiscordChannel extends BaseChannel<Config["channels"]["discord"]> {
58
58
  name: string;
59
59
  private client;
60
- private typingTasks;
60
+ private readonly typingController;
61
61
  constructor(config: Config["channels"]["discord"], bus: MessageBus);
62
62
  start(): Promise<void>;
63
63
  stop(): Promise<void>;
@@ -192,7 +192,7 @@ declare class TelegramChannel extends BaseChannel<Config["channels"]["telegram"]
192
192
  private sessionManager?;
193
193
  name: string;
194
194
  private bot;
195
- private typingTasks;
195
+ private readonly typingController;
196
196
  private transcriber;
197
197
  constructor(config: Config["channels"]["telegram"], bus: MessageBus, groqApiKey?: string, sessionManager?: SessionManager | undefined);
198
198
  start(): Promise<void>;
package/dist/index.js CHANGED
@@ -182,15 +182,73 @@ import { mkdirSync, writeFileSync } from "fs";
182
182
  // src/utils/helpers.ts
183
183
  import { getDataPath } from "@nextclaw/core";
184
184
 
185
+ // src/channels/typing-controller.ts
186
+ var ChannelTypingController = class {
187
+ heartbeatMs;
188
+ autoStopMs;
189
+ sendTyping;
190
+ tasks = /* @__PURE__ */ new Map();
191
+ constructor(options) {
192
+ this.heartbeatMs = Math.max(1e3, Math.floor(options.heartbeatMs));
193
+ this.autoStopMs = Math.max(this.heartbeatMs, Math.floor(options.autoStopMs));
194
+ this.sendTyping = options.sendTyping;
195
+ }
196
+ start(targetId) {
197
+ this.stop(targetId);
198
+ void this.sendTyping(targetId);
199
+ const heartbeat = setInterval(() => {
200
+ void this.sendTyping(targetId);
201
+ }, this.heartbeatMs);
202
+ const autoStop = setTimeout(() => {
203
+ this.stop(targetId);
204
+ }, this.autoStopMs);
205
+ this.tasks.set(targetId, {
206
+ heartbeat,
207
+ autoStop
208
+ });
209
+ }
210
+ stop(targetId) {
211
+ const task = this.tasks.get(targetId);
212
+ if (!task) {
213
+ return;
214
+ }
215
+ clearInterval(task.heartbeat);
216
+ clearTimeout(task.autoStop);
217
+ this.tasks.delete(targetId);
218
+ }
219
+ stopAll() {
220
+ for (const targetId of this.tasks.keys()) {
221
+ this.stop(targetId);
222
+ }
223
+ }
224
+ };
225
+
185
226
  // src/channels/discord.ts
186
227
  var DEFAULT_MEDIA_MAX_MB = 8;
187
228
  var MEDIA_FETCH_TIMEOUT_MS = 15e3;
229
+ var TYPING_HEARTBEAT_MS = 8e3;
230
+ var TYPING_AUTO_STOP_MS = 45e3;
188
231
  var DiscordChannel = class extends BaseChannel {
189
232
  name = "discord";
190
233
  client = null;
191
- typingTasks = /* @__PURE__ */ new Map();
234
+ typingController;
192
235
  constructor(config, bus) {
193
236
  super(config, bus);
237
+ this.typingController = new ChannelTypingController({
238
+ heartbeatMs: TYPING_HEARTBEAT_MS,
239
+ autoStopMs: TYPING_AUTO_STOP_MS,
240
+ sendTyping: async (channelId) => {
241
+ if (!this.client) {
242
+ return;
243
+ }
244
+ const channel = this.client.channels.cache.get(channelId);
245
+ if (!channel || !channel.isTextBased()) {
246
+ return;
247
+ }
248
+ const textChannel = channel;
249
+ await textChannel.sendTyping();
250
+ }
251
+ });
194
252
  }
195
253
  async start() {
196
254
  if (!this.config.token) {
@@ -211,10 +269,7 @@ var DiscordChannel = class extends BaseChannel {
211
269
  }
212
270
  async stop() {
213
271
  this.running = false;
214
- for (const task of this.typingTasks.values()) {
215
- clearInterval(task);
216
- }
217
- this.typingTasks.clear();
272
+ this.typingController.stopAll();
218
273
  if (this.client) {
219
274
  await this.client.destroy();
220
275
  this.client = null;
@@ -285,18 +340,23 @@ var DiscordChannel = class extends BaseChannel {
285
340
  }
286
341
  const replyTo = message.reference?.messageId ?? null;
287
342
  this.startTyping(channelId);
288
- await this.handleMessage({
289
- senderId,
290
- chatId: channelId,
291
- content: contentParts.length ? contentParts.join("\n") : "[empty message]",
292
- attachments,
293
- metadata: {
294
- message_id: message.id,
295
- guild_id: message.guildId,
296
- reply_to: replyTo,
297
- ...attachmentIssues.length ? { attachment_issues: attachmentIssues } : {}
298
- }
299
- });
343
+ try {
344
+ await this.handleMessage({
345
+ senderId,
346
+ chatId: channelId,
347
+ content: contentParts.length ? contentParts.join("\n") : "[empty message]",
348
+ attachments,
349
+ metadata: {
350
+ message_id: message.id,
351
+ guild_id: message.guildId,
352
+ reply_to: replyTo,
353
+ ...attachmentIssues.length ? { attachment_issues: attachmentIssues } : {}
354
+ }
355
+ });
356
+ } catch (err) {
357
+ this.stopTyping(channelId);
358
+ throw err;
359
+ }
300
360
  }
301
361
  resolveProxyAgent() {
302
362
  const proxy = this.config.proxy?.trim();
@@ -437,26 +497,10 @@ var DiscordChannel = class extends BaseChannel {
437
497
  }
438
498
  }
439
499
  startTyping(channelId) {
440
- this.stopTyping(channelId);
441
- if (!this.client) {
442
- return;
443
- }
444
- const channel = this.client.channels.cache.get(channelId);
445
- if (!channel || !channel.isTextBased()) {
446
- return;
447
- }
448
- const textChannel = channel;
449
- const task = setInterval(() => {
450
- void textChannel.sendTyping();
451
- }, 8e3);
452
- this.typingTasks.set(channelId, task);
500
+ this.typingController.start(channelId);
453
501
  }
454
502
  stopTyping(channelId) {
455
- const task = this.typingTasks.get(channelId);
456
- if (task) {
457
- clearInterval(task);
458
- this.typingTasks.delete(channelId);
459
- }
503
+ this.typingController.stop(channelId);
460
504
  }
461
505
  };
462
506
  function sanitizeAttachmentName(name) {
@@ -2167,6 +2211,8 @@ var GroqTranscriptionProvider = class {
2167
2211
  // src/channels/telegram.ts
2168
2212
  import { join as join3 } from "path";
2169
2213
  import { mkdirSync as mkdirSync3 } from "fs";
2214
+ var TYPING_HEARTBEAT_MS2 = 4e3;
2215
+ var TYPING_AUTO_STOP_MS2 = 45e3;
2170
2216
  var BOT_COMMANDS = [
2171
2217
  { command: "start", description: "Start the bot" },
2172
2218
  { command: "reset", description: "Reset conversation history" },
@@ -2177,10 +2223,17 @@ var TelegramChannel = class extends BaseChannel {
2177
2223
  super(config, bus);
2178
2224
  this.sessionManager = sessionManager;
2179
2225
  this.transcriber = new GroqTranscriptionProvider(groqApiKey ?? null);
2226
+ this.typingController = new ChannelTypingController({
2227
+ heartbeatMs: TYPING_HEARTBEAT_MS2,
2228
+ autoStopMs: TYPING_AUTO_STOP_MS2,
2229
+ sendTyping: async (chatId) => {
2230
+ await this.bot?.sendChatAction(Number(chatId), "typing");
2231
+ }
2232
+ });
2180
2233
  }
2181
2234
  name = "telegram";
2182
2235
  bot = null;
2183
- typingTasks = /* @__PURE__ */ new Map();
2236
+ typingController;
2184
2237
  transcriber;
2185
2238
  async start() {
2186
2239
  if (!this.config.token) {
@@ -2246,10 +2299,7 @@ Just send me a text message to chat!`;
2246
2299
  }
2247
2300
  async stop() {
2248
2301
  this.running = false;
2249
- for (const task of this.typingTasks.values()) {
2250
- clearInterval(task);
2251
- }
2252
- this.typingTasks.clear();
2302
+ this.typingController.stopAll();
2253
2303
  if (this.bot) {
2254
2304
  await this.bot.stopPolling();
2255
2305
  this.bot = null;
@@ -2326,35 +2376,29 @@ Just send me a text message to chat!`;
2326
2376
  }
2327
2377
  const content = contentParts.length ? contentParts.join("\n") : "[empty message]";
2328
2378
  this.startTyping(chatId);
2329
- await this.dispatchToBus(senderId, chatId, content, attachments, {
2330
- message_id: message.message_id,
2331
- user_id: sender.id,
2332
- username: sender.username,
2333
- first_name: sender.firstName,
2334
- sender_type: sender.type,
2335
- is_bot: sender.isBot,
2336
- is_group: message.chat.type !== "private"
2337
- });
2379
+ try {
2380
+ await this.dispatchToBus(senderId, chatId, content, attachments, {
2381
+ message_id: message.message_id,
2382
+ user_id: sender.id,
2383
+ username: sender.username,
2384
+ first_name: sender.firstName,
2385
+ sender_type: sender.type,
2386
+ is_bot: sender.isBot,
2387
+ is_group: message.chat.type !== "private"
2388
+ });
2389
+ } catch (err) {
2390
+ this.stopTyping(chatId);
2391
+ throw err;
2392
+ }
2338
2393
  }
2339
2394
  async dispatchToBus(senderId, chatId, content, attachments, metadata) {
2340
2395
  await this.handleMessage({ senderId, chatId, content, attachments, metadata });
2341
2396
  }
2342
2397
  startTyping(chatId) {
2343
- this.stopTyping(chatId);
2344
- if (!this.bot) {
2345
- return;
2346
- }
2347
- const task = setInterval(() => {
2348
- void this.bot?.sendChatAction(Number(chatId), "typing");
2349
- }, 4e3);
2350
- this.typingTasks.set(chatId, task);
2398
+ this.typingController.start(chatId);
2351
2399
  }
2352
2400
  stopTyping(chatId) {
2353
- const task = this.typingTasks.get(chatId);
2354
- if (task) {
2355
- clearInterval(task);
2356
- this.typingTasks.delete(chatId);
2357
- }
2401
+ this.typingController.stop(chatId);
2358
2402
  }
2359
2403
  };
2360
2404
  function resolveSender(message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Runtime implementations for NextClaw builtin channel plugins.",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@larksuiteoapi/node-sdk": "^1.58.0",
18
- "@nextclaw/core": "^0.6.17",
18
+ "@nextclaw/core": "^0.6.18",
19
19
  "@slack/socket-mode": "^1.3.3",
20
20
  "@slack/web-api": "^7.6.0",
21
21
  "dingtalk-stream": "^2.1.4",