@nextclaw/channel-runtime 0.1.29 → 0.1.31

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
@@ -232,6 +232,7 @@ declare class TelegramChannel extends BaseChannel<Config["channels"]["telegram"]
232
232
  private botUserId;
233
233
  private botUsername;
234
234
  private readonly typingController;
235
+ private readonly streamPreview;
235
236
  private transcriber;
236
237
  constructor(config: Config["channels"]["telegram"], bus: MessageBus, groqApiKey?: string, sessionManager?: SessionManager | undefined);
237
238
  start(): Promise<void>;
package/dist/index.js CHANGED
@@ -3010,14 +3010,230 @@ var GroqTranscriptionProvider = class {
3010
3010
  // src/channels/telegram.ts
3011
3011
  import { join as join3 } from "path";
3012
3012
  import { mkdirSync as mkdirSync3 } from "fs";
3013
- import { isTypingStopControlMessage as isTypingStopControlMessage2 } from "@nextclaw/core";
3013
+ import {
3014
+ isAssistantStreamResetControlMessage,
3015
+ isTypingStopControlMessage as isTypingStopControlMessage2,
3016
+ readAssistantStreamDelta
3017
+ } from "@nextclaw/core";
3014
3018
  var TYPING_HEARTBEAT_MS2 = 6e3;
3015
3019
  var TYPING_AUTO_STOP_MS2 = 12e4;
3020
+ var TELEGRAM_TEXT_LIMIT = 4096;
3021
+ var STREAM_PREVIEW_MIN_CHARS = 30;
3022
+ var STREAM_PREVIEW_PARTIAL_MIN_INTERVAL_MS = 700;
3023
+ var STREAM_PREVIEW_BLOCK_MIN_INTERVAL_MS = 1200;
3024
+ var STREAM_PREVIEW_BLOCK_MIN_GROWTH = 120;
3016
3025
  var BOT_COMMANDS = [
3017
3026
  { command: "start", description: "Start the bot" },
3018
3027
  { command: "reset", description: "Reset conversation history" },
3019
3028
  { command: "help", description: "Show available commands" }
3020
3029
  ];
3030
+ var TelegramStreamPreviewController = class {
3031
+ constructor(params) {
3032
+ this.params = params;
3033
+ }
3034
+ states = /* @__PURE__ */ new Map();
3035
+ async handleReset(msg) {
3036
+ const chatId = String(msg.chatId);
3037
+ this.dispose(chatId);
3038
+ if (this.params.resolveMode() === "off") {
3039
+ return;
3040
+ }
3041
+ const replyToMessageId = readReplyToMessageId(msg.metadata);
3042
+ this.states.set(chatId, {
3043
+ chatId,
3044
+ rawText: "",
3045
+ lastRenderedText: "",
3046
+ messageId: void 0,
3047
+ replyToMessageId,
3048
+ silent: msg.metadata?.silent === true,
3049
+ lastSentAt: 0,
3050
+ lastEmittedChars: 0,
3051
+ inFlight: false,
3052
+ pending: false,
3053
+ timer: null
3054
+ });
3055
+ }
3056
+ async handleDelta(msg, delta) {
3057
+ if (!delta) {
3058
+ return;
3059
+ }
3060
+ if (this.params.resolveMode() === "off") {
3061
+ return;
3062
+ }
3063
+ const chatId = String(msg.chatId);
3064
+ const state = this.ensureState(chatId);
3065
+ state.rawText += delta;
3066
+ this.scheduleFlush(state);
3067
+ }
3068
+ async finalizeWithFinalMessage(msg) {
3069
+ if (this.params.resolveMode() === "off") {
3070
+ return false;
3071
+ }
3072
+ const chatId = String(msg.chatId);
3073
+ const state = this.states.get(chatId);
3074
+ if (!state) {
3075
+ return false;
3076
+ }
3077
+ state.rawText = msg.content ?? "";
3078
+ const replyToMessageId = msg.replyTo ? Number(msg.replyTo) : state.replyToMessageId;
3079
+ state.silent = msg.metadata?.silent === true || state.silent;
3080
+ if (!state.rawText.trim()) {
3081
+ this.dispose(chatId);
3082
+ return false;
3083
+ }
3084
+ const handled = await this.flushNow(state, {
3085
+ force: true,
3086
+ allowInitialBelowThreshold: true,
3087
+ replyToMessageId,
3088
+ silent: state.silent
3089
+ });
3090
+ this.dispose(chatId);
3091
+ return handled;
3092
+ }
3093
+ stopAll() {
3094
+ for (const chatId of this.states.keys()) {
3095
+ this.dispose(chatId);
3096
+ }
3097
+ }
3098
+ ensureState(chatId) {
3099
+ const existing = this.states.get(chatId);
3100
+ if (existing) {
3101
+ return existing;
3102
+ }
3103
+ const created = {
3104
+ chatId,
3105
+ rawText: "",
3106
+ lastRenderedText: "",
3107
+ messageId: void 0,
3108
+ replyToMessageId: void 0,
3109
+ silent: false,
3110
+ lastSentAt: 0,
3111
+ lastEmittedChars: 0,
3112
+ inFlight: false,
3113
+ pending: false,
3114
+ timer: null
3115
+ };
3116
+ this.states.set(chatId, created);
3117
+ return created;
3118
+ }
3119
+ scheduleFlush(state) {
3120
+ if (state.timer) {
3121
+ return;
3122
+ }
3123
+ const minInterval = this.params.resolveMode() === "block" ? STREAM_PREVIEW_BLOCK_MIN_INTERVAL_MS : STREAM_PREVIEW_PARTIAL_MIN_INTERVAL_MS;
3124
+ const delay = Math.max(0, minInterval - (Date.now() - state.lastSentAt));
3125
+ state.timer = setTimeout(() => {
3126
+ state.timer = null;
3127
+ void this.flushScheduled(state);
3128
+ }, delay);
3129
+ }
3130
+ async flushScheduled(state) {
3131
+ const current = this.states.get(state.chatId);
3132
+ if (current !== state) {
3133
+ return;
3134
+ }
3135
+ if (state.inFlight) {
3136
+ state.pending = true;
3137
+ return;
3138
+ }
3139
+ state.inFlight = true;
3140
+ try {
3141
+ await this.flushNow(state, {
3142
+ force: false,
3143
+ allowInitialBelowThreshold: false,
3144
+ replyToMessageId: state.replyToMessageId,
3145
+ silent: state.silent
3146
+ });
3147
+ } finally {
3148
+ state.inFlight = false;
3149
+ if (state.pending) {
3150
+ state.pending = false;
3151
+ this.scheduleFlush(state);
3152
+ }
3153
+ }
3154
+ }
3155
+ async flushNow(state, opts) {
3156
+ const bot = this.params.getBot();
3157
+ if (!bot) {
3158
+ return false;
3159
+ }
3160
+ const plainText = state.rawText.trimEnd();
3161
+ if (!plainText) {
3162
+ return false;
3163
+ }
3164
+ const mode = this.params.resolveMode();
3165
+ if (mode === "block" && !opts.force && plainText.length - state.lastEmittedChars < STREAM_PREVIEW_BLOCK_MIN_GROWTH) {
3166
+ return typeof state.messageId === "number";
3167
+ }
3168
+ if (typeof state.messageId !== "number" && !opts.allowInitialBelowThreshold && plainText.length < STREAM_PREVIEW_MIN_CHARS) {
3169
+ return false;
3170
+ }
3171
+ const renderedText = markdownToTelegramHtml(plainText).trimEnd();
3172
+ if (!renderedText) {
3173
+ return false;
3174
+ }
3175
+ const limitedRenderedText = renderedText.slice(0, TELEGRAM_TEXT_LIMIT);
3176
+ if (limitedRenderedText === state.lastRenderedText) {
3177
+ return typeof state.messageId === "number";
3178
+ }
3179
+ if (typeof state.messageId === "number") {
3180
+ try {
3181
+ await bot.editMessageText(limitedRenderedText, {
3182
+ chat_id: Number(state.chatId),
3183
+ message_id: state.messageId,
3184
+ parse_mode: "HTML"
3185
+ });
3186
+ } catch {
3187
+ try {
3188
+ await bot.editMessageText(plainText.slice(0, TELEGRAM_TEXT_LIMIT), {
3189
+ chat_id: Number(state.chatId),
3190
+ message_id: state.messageId
3191
+ });
3192
+ } catch {
3193
+ return false;
3194
+ }
3195
+ }
3196
+ state.lastRenderedText = limitedRenderedText;
3197
+ state.lastSentAt = Date.now();
3198
+ state.lastEmittedChars = plainText.length;
3199
+ return true;
3200
+ }
3201
+ const sendOptions = {
3202
+ parse_mode: "HTML",
3203
+ ...opts.replyToMessageId ? { reply_to_message_id: opts.replyToMessageId } : {},
3204
+ ...opts.silent ? { disable_notification: true } : {}
3205
+ };
3206
+ try {
3207
+ const sent = await bot.sendMessage(Number(state.chatId), limitedRenderedText, sendOptions);
3208
+ if (typeof sent.message_id === "number") {
3209
+ state.messageId = sent.message_id;
3210
+ }
3211
+ } catch {
3212
+ const sent = await bot.sendMessage(Number(state.chatId), plainText.slice(0, TELEGRAM_TEXT_LIMIT), {
3213
+ ...opts.replyToMessageId ? { reply_to_message_id: opts.replyToMessageId } : {},
3214
+ ...opts.silent ? { disable_notification: true } : {}
3215
+ });
3216
+ if (typeof sent.message_id === "number") {
3217
+ state.messageId = sent.message_id;
3218
+ }
3219
+ }
3220
+ state.lastRenderedText = limitedRenderedText;
3221
+ state.lastSentAt = Date.now();
3222
+ state.lastEmittedChars = plainText.length;
3223
+ return true;
3224
+ }
3225
+ dispose(chatId) {
3226
+ const state = this.states.get(chatId);
3227
+ if (!state) {
3228
+ return;
3229
+ }
3230
+ if (state.timer) {
3231
+ clearTimeout(state.timer);
3232
+ state.timer = null;
3233
+ }
3234
+ this.states.delete(chatId);
3235
+ }
3236
+ };
3021
3237
  var TelegramChannel = class extends BaseChannel {
3022
3238
  constructor(config, bus, groqApiKey, sessionManager) {
3023
3239
  super(config, bus);
@@ -3030,12 +3246,17 @@ var TelegramChannel = class extends BaseChannel {
3030
3246
  await this.bot?.sendChatAction(Number(chatId), "typing");
3031
3247
  }
3032
3248
  });
3249
+ this.streamPreview = new TelegramStreamPreviewController({
3250
+ resolveMode: () => resolveTelegramStreamingMode(this.config),
3251
+ getBot: () => this.bot
3252
+ });
3033
3253
  }
3034
3254
  name = "telegram";
3035
3255
  bot = null;
3036
3256
  botUserId = null;
3037
3257
  botUsername = null;
3038
3258
  typingController;
3259
+ streamPreview;
3039
3260
  transcriber;
3040
3261
  async start() {
3041
3262
  if (!this.config.token) {
@@ -3129,17 +3350,27 @@ Just send me a text message to chat!`;
3129
3350
  async stop() {
3130
3351
  this.running = false;
3131
3352
  this.typingController.stopAll();
3353
+ this.streamPreview.stopAll();
3132
3354
  if (this.bot) {
3133
3355
  await this.bot.stopPolling();
3134
3356
  this.bot = null;
3135
3357
  }
3136
3358
  }
3137
3359
  async handleControlMessage(msg) {
3138
- if (!isTypingStopControlMessage2(msg)) {
3139
- return false;
3360
+ if (isTypingStopControlMessage2(msg)) {
3361
+ this.stopTyping(msg.chatId);
3362
+ return true;
3140
3363
  }
3141
- this.stopTyping(msg.chatId);
3142
- return true;
3364
+ if (isAssistantStreamResetControlMessage(msg)) {
3365
+ await this.streamPreview.handleReset(msg);
3366
+ return true;
3367
+ }
3368
+ const delta = readAssistantStreamDelta(msg);
3369
+ if (delta !== null) {
3370
+ await this.streamPreview.handleDelta(msg, delta);
3371
+ return true;
3372
+ }
3373
+ return false;
3143
3374
  }
3144
3375
  async send(msg) {
3145
3376
  if (isTypingStopControlMessage2(msg)) {
@@ -3150,6 +3381,9 @@ Just send me a text message to chat!`;
3150
3381
  return;
3151
3382
  }
3152
3383
  this.stopTyping(msg.chatId);
3384
+ if (await this.streamPreview.finalizeWithFinalMessage(msg)) {
3385
+ return;
3386
+ }
3153
3387
  const htmlContent = markdownToTelegramHtml(msg.content ?? "");
3154
3388
  const silent = msg.metadata?.silent === true;
3155
3389
  const replyTo = msg.replyTo ? Number(msg.replyTo) : void 0;
@@ -3438,6 +3672,35 @@ function shouldSendAckReaction(params) {
3438
3672
  }
3439
3673
  return false;
3440
3674
  }
3675
+ function readReplyToMessageId(metadata) {
3676
+ const raw = metadata.message_id;
3677
+ if (typeof raw === "number" && Number.isFinite(raw)) {
3678
+ return Math.trunc(raw);
3679
+ }
3680
+ if (typeof raw === "string") {
3681
+ const parsed = Number(raw);
3682
+ if (Number.isFinite(parsed)) {
3683
+ return Math.trunc(parsed);
3684
+ }
3685
+ }
3686
+ return void 0;
3687
+ }
3688
+ function resolveTelegramStreamingMode(config) {
3689
+ const raw = config.streaming;
3690
+ if (raw === true) {
3691
+ return "partial";
3692
+ }
3693
+ if (raw === false || raw === void 0 || raw === null) {
3694
+ return "off";
3695
+ }
3696
+ if (raw === "progress") {
3697
+ return "partial";
3698
+ }
3699
+ if (raw === "partial" || raw === "block" || raw === "off") {
3700
+ return raw;
3701
+ }
3702
+ return "off";
3703
+ }
3441
3704
  function markdownToTelegramHtml(text) {
3442
3705
  if (!text) {
3443
3706
  return "";
@@ -3857,12 +4120,16 @@ var BUILTIN_CHANNEL_RUNTIMES = {
3857
4120
  telegram: {
3858
4121
  id: "telegram",
3859
4122
  isEnabled: (config) => config.channels.telegram.enabled,
3860
- createChannel: (context) => new TelegramChannel(
3861
- context.config.channels.telegram,
3862
- context.bus,
3863
- context.config.providers.groq.apiKey,
3864
- context.sessionManager
3865
- )
4123
+ createChannel: (context) => {
4124
+ const providers = context.config.providers;
4125
+ const groqApiKey = providers.groq?.apiKey;
4126
+ return new TelegramChannel(
4127
+ context.config.channels.telegram,
4128
+ context.bus,
4129
+ groqApiKey,
4130
+ context.sessionManager
4131
+ );
4132
+ }
3866
4133
  },
3867
4134
  whatsapp: {
3868
4135
  id: "whatsapp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "private": false,
5
5
  "description": "Runtime implementations for NextClaw builtin channel plugins.",
6
6
  "type": "module",
@@ -15,7 +15,6 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@larksuiteoapi/node-sdk": "^1.58.0",
18
- "@nextclaw/core": "^0.7.0",
19
18
  "@slack/socket-mode": "^1.3.3",
20
19
  "@slack/web-api": "^7.6.0",
21
20
  "dingtalk-stream": "^2.1.4",
@@ -28,7 +27,8 @@
28
27
  "socket.io-client": "^4.7.5",
29
28
  "undici": "^6.21.0",
30
29
  "ws": "^8.18.0",
31
- "socket.io-msgpack-parser": "^3.0.2"
30
+ "socket.io-msgpack-parser": "^3.0.2",
31
+ "@nextclaw/core": "0.7.3"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/mailparser": "^3.4.6",