@nextclaw/channel-runtime 0.1.28 → 0.1.30
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 +1 -0
- package/dist/index.js +268 -5
- package/package.json +2 -2
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 {
|
|
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 (
|
|
3139
|
-
|
|
3360
|
+
if (isTypingStopControlMessage2(msg)) {
|
|
3361
|
+
this.stopTyping(msg.chatId);
|
|
3362
|
+
return true;
|
|
3140
3363
|
}
|
|
3141
|
-
|
|
3142
|
-
|
|
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 "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/channel-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
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.
|
|
18
|
+
"@nextclaw/core": "^0.7.3",
|
|
19
19
|
"@slack/socket-mode": "^1.3.3",
|
|
20
20
|
"@slack/web-api": "^7.6.0",
|
|
21
21
|
"dingtalk-stream": "^2.1.4",
|