@mulmobridge/telegram 1.3.0 → 1.3.1
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/router.js +26 -67
- package/dist/stream.d.ts +14 -0
- package/dist/stream.js +50 -0
- package/package.json +1 -1
package/dist/router.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Telegram API, no real MulmoClaude socket, no env reads). The
|
|
4
4
|
// entrypoint in index.ts is then just: read env → wire real deps →
|
|
5
5
|
// drive the polling loop.
|
|
6
|
+
import { createReplyStream } from "./stream.js";
|
|
6
7
|
import { parseDataUrl, replyAfterProcessingIndicator } from "@mulmobridge/client";
|
|
7
8
|
// Telegram caps a single message at 4096 chars. We split long
|
|
8
9
|
// replies naively; pretty formatting (preserve markdown, break on
|
|
@@ -24,52 +25,22 @@ export function createMessageRouter(deps) {
|
|
|
24
25
|
const log = deps.log ?? defaultLog;
|
|
25
26
|
// One denial reply per chat per bridge lifetime — restart clears.
|
|
26
27
|
const deniedAlreadyNotified = new Set();
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
streamDirty = false;
|
|
39
|
-
streamTimer = setInterval(flushStream, STREAM_EDIT_INTERVAL_MS);
|
|
40
|
-
}
|
|
41
|
-
function stopStream() {
|
|
42
|
-
if (streamTimer !== null) {
|
|
43
|
-
clearInterval(streamTimer);
|
|
44
|
-
streamTimer = null;
|
|
28
|
+
// The reply stream of the request in flight. Chunks carry no chat id; the
|
|
29
|
+
// polling loop handles one message at a time, so they belong to this one.
|
|
30
|
+
const inFlight = { stream: null };
|
|
31
|
+
/** Runs `work` while its streamed chunks are mirrored into the chat. The
|
|
32
|
+
* timer is stopped even when `work` throws. */
|
|
33
|
+
async function streamWhile(chatId, work) {
|
|
34
|
+
const stream = createReplyStream(api, chatId, log, STREAM_EDIT_INTERVAL_MS);
|
|
35
|
+
inFlight.stream = stream;
|
|
36
|
+
try {
|
|
37
|
+
const result = await work();
|
|
38
|
+
return { result, streamedMessageId: await stream.finish() };
|
|
45
39
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
function flushStream() {
|
|
52
|
-
if (!streamDirty || streamChatId === null)
|
|
53
|
-
return;
|
|
54
|
-
streamDirty = false;
|
|
55
|
-
const chatId = streamChatId;
|
|
56
|
-
const text = streamAccumulated || "…";
|
|
57
|
-
if (streamMessageId === null) {
|
|
58
|
-
// First flush: send a new message
|
|
59
|
-
api
|
|
60
|
-
.sendMessage(chatId, text)
|
|
61
|
-
.then((msgId) => {
|
|
62
|
-
streamMessageId = msgId;
|
|
63
|
-
})
|
|
64
|
-
.catch((err) => {
|
|
65
|
-
log.error(`[telegram] stream sendMessage failed: ${String(err)}`);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
// Subsequent flushes: edit existing message
|
|
70
|
-
api.editMessageText(chatId, streamMessageId, text).catch((err) => {
|
|
71
|
-
log.error(`[telegram] stream editMessage failed: ${String(err)}`);
|
|
72
|
-
});
|
|
40
|
+
finally {
|
|
41
|
+
stream.stop();
|
|
42
|
+
if (inFlight.stream === stream)
|
|
43
|
+
inFlight.stream = null;
|
|
73
44
|
}
|
|
74
45
|
}
|
|
75
46
|
// Download all media from a message. Telegram messages are usually
|
|
@@ -149,26 +120,17 @@ export function createMessageRouter(deps) {
|
|
|
149
120
|
}
|
|
150
121
|
// Surface the attachment failure so the user knows the file was lost.
|
|
151
122
|
const messageText = resolveMessageText(text, failed);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
startStream(chatId);
|
|
155
|
-
const ack = await sendToMulmo(String(chatId), messageText, attachments.length > 0 ? attachments : undefined);
|
|
156
|
-
// Final flush + capture the message id before stopStream clears it.
|
|
157
|
-
flushStream();
|
|
158
|
-
const sentMsgId = streamMessageId;
|
|
159
|
-
stopStream();
|
|
160
|
-
return () => deliverFinal(chatId, sentMsgId, ack);
|
|
123
|
+
const { result: ack, streamedMessageId } = await streamWhile(chatId, () => sendToMulmo(String(chatId), messageText, attachments.length > 0 ? attachments : undefined));
|
|
124
|
+
return () => deliverFinal(chatId, streamedMessageId, ack);
|
|
161
125
|
}
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
else if (sentMsgId === null) {
|
|
170
|
-
await sendReply(chatId, ack);
|
|
126
|
+
// A streamed reply is completed in place; an error after streaming is sent
|
|
127
|
+
// as its own message so the partial text is not the last word.
|
|
128
|
+
async function deliverFinal(chatId, streamedMessageId, ack) {
|
|
129
|
+
if (streamedMessageId !== null && ack.ok) {
|
|
130
|
+
await api.editMessageText(chatId, streamedMessageId, ack.reply ?? "").catch(() => { });
|
|
131
|
+
return;
|
|
171
132
|
}
|
|
133
|
+
await sendReply(chatId, ack);
|
|
172
134
|
}
|
|
173
135
|
async function handleDenied(msg) {
|
|
174
136
|
const chatId = msg.chat.id;
|
|
@@ -210,10 +172,7 @@ export function createMessageRouter(deps) {
|
|
|
210
172
|
}
|
|
211
173
|
},
|
|
212
174
|
handleTextChunk(chunk) {
|
|
213
|
-
|
|
214
|
-
return;
|
|
215
|
-
streamAccumulated += chunk;
|
|
216
|
-
streamDirty = true;
|
|
175
|
+
inFlight.stream?.append(chunk);
|
|
217
176
|
},
|
|
218
177
|
deniedAlreadyNotified() {
|
|
219
178
|
return deniedAlreadyNotified;
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TelegramApi } from "./api.js";
|
|
2
|
+
interface StreamLog {
|
|
3
|
+
error: (msg: string) => void;
|
|
4
|
+
}
|
|
5
|
+
export interface ReplyStream {
|
|
6
|
+
append(chunk: string): void;
|
|
7
|
+
/** Stops the timer, sends what is pending, and resolves to the id of the
|
|
8
|
+
* streamed message — or null when nothing was streamed. */
|
|
9
|
+
finish(): Promise<number | null>;
|
|
10
|
+
/** Stops the timer without sending anything more. Safe to call twice. */
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createReplyStream(api: TelegramApi, chatId: number, log: StreamLog, interval_ms: number): ReplyStream;
|
|
14
|
+
export {};
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// One streamed reply: chunks accumulate here and a timer mirrors them into a
|
|
2
|
+
// single Telegram message (sent once, then edited). Created per request so a
|
|
3
|
+
// failed request cannot leave its timer or text behind for the next one.
|
|
4
|
+
export function createReplyStream(api, chatId, log, interval_ms) {
|
|
5
|
+
const state = { accumulated: "", dirty: false, messageId: null, flushChain: Promise.resolve(), timer: null };
|
|
6
|
+
async function sendOrEdit(text) {
|
|
7
|
+
if (state.messageId === null) {
|
|
8
|
+
state.messageId = await api.sendMessage(chatId, text);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
await api.editMessageText(chatId, state.messageId, text);
|
|
12
|
+
}
|
|
13
|
+
async function flushPending() {
|
|
14
|
+
if (!state.dirty)
|
|
15
|
+
return;
|
|
16
|
+
state.dirty = false;
|
|
17
|
+
const action = state.messageId === null ? "sendMessage" : "editMessage";
|
|
18
|
+
try {
|
|
19
|
+
await sendOrEdit(state.accumulated || "…");
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
log.error(`[telegram] stream ${action} failed: ${String(err)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function flush() {
|
|
26
|
+
state.flushChain = state.flushChain.then(flushPending);
|
|
27
|
+
return state.flushChain;
|
|
28
|
+
}
|
|
29
|
+
function stop() {
|
|
30
|
+
if (state.timer === null)
|
|
31
|
+
return;
|
|
32
|
+
clearInterval(state.timer);
|
|
33
|
+
state.timer = null;
|
|
34
|
+
}
|
|
35
|
+
state.timer = setInterval(() => void flush(), interval_ms);
|
|
36
|
+
return {
|
|
37
|
+
append(chunk) {
|
|
38
|
+
if (state.timer === null)
|
|
39
|
+
return;
|
|
40
|
+
state.accumulated += chunk;
|
|
41
|
+
state.dirty = true;
|
|
42
|
+
},
|
|
43
|
+
async finish() {
|
|
44
|
+
stop();
|
|
45
|
+
await flush();
|
|
46
|
+
return state.messageId;
|
|
47
|
+
},
|
|
48
|
+
stop,
|
|
49
|
+
};
|
|
50
|
+
}
|