@krischoichoi/channel-telegram 0.6.0
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/README.md +129 -0
- package/lib/adapter.d.ts +94 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +417 -0
- package/lib/adapter.js.map +1 -0
- package/lib/api-error.d.ts +74 -0
- package/lib/api-error.d.ts.map +1 -0
- package/lib/api-error.js +103 -0
- package/lib/api-error.js.map +1 -0
- package/lib/config.d.ts +115 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +67 -0
- package/lib/config.js.map +1 -0
- package/lib/definition.d.ts +59 -0
- package/lib/definition.d.ts.map +1 -0
- package/lib/definition.js +158 -0
- package/lib/definition.js.map +1 -0
- package/lib/inbound.d.ts +61 -0
- package/lib/inbound.d.ts.map +1 -0
- package/lib/inbound.js +125 -0
- package/lib/inbound.js.map +1 -0
- package/lib/index.d.ts +52 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +114 -0
- package/lib/index.js.map +1 -0
- package/lib/manifest.d.ts +16 -0
- package/lib/manifest.d.ts.map +1 -0
- package/lib/manifest.js +26 -0
- package/lib/manifest.js.map +1 -0
- package/lib/mapper.d.ts +51 -0
- package/lib/mapper.d.ts.map +1 -0
- package/lib/mapper.js +314 -0
- package/lib/mapper.js.map +1 -0
- package/lib/media-hydrator.d.ts +46 -0
- package/lib/media-hydrator.d.ts.map +1 -0
- package/lib/media-hydrator.js +85 -0
- package/lib/media-hydrator.js.map +1 -0
- package/lib/outbound.d.ts +42 -0
- package/lib/outbound.d.ts.map +1 -0
- package/lib/outbound.js +215 -0
- package/lib/outbound.js.map +1 -0
- package/lib/render/html.d.ts +13 -0
- package/lib/render/html.d.ts.map +1 -0
- package/lib/render/html.js +110 -0
- package/lib/render/html.js.map +1 -0
- package/lib/render/index.d.ts +64 -0
- package/lib/render/index.d.ts.map +1 -0
- package/lib/render/index.js +76 -0
- package/lib/render/index.js.map +1 -0
- package/lib/render/markdown.d.ts +38 -0
- package/lib/render/markdown.d.ts.map +1 -0
- package/lib/render/markdown.js +187 -0
- package/lib/render/markdown.js.map +1 -0
- package/lib/render/plain.d.ts +43 -0
- package/lib/render/plain.d.ts.map +1 -0
- package/lib/render/plain.js +97 -0
- package/lib/render/plain.js.map +1 -0
- package/lib/render/segment.d.ts +39 -0
- package/lib/render/segment.d.ts.map +1 -0
- package/lib/render/segment.js +117 -0
- package/lib/render/segment.js.map +1 -0
- package/lib/rich-message.d.ts +75 -0
- package/lib/rich-message.d.ts.map +1 -0
- package/lib/rich-message.js +11 -0
- package/lib/rich-message.js.map +1 -0
- package/lib/rich-streaming-reply.d.ts +69 -0
- package/lib/rich-streaming-reply.d.ts.map +1 -0
- package/lib/rich-streaming-reply.js +207 -0
- package/lib/rich-streaming-reply.js.map +1 -0
- package/lib/streaming-reply.d.ts +98 -0
- package/lib/streaming-reply.d.ts.map +1 -0
- package/lib/streaming-reply.js +274 -0
- package/lib/streaming-reply.js.map +1 -0
- package/lib/transport.d.ts +38 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +91 -0
- package/lib/transport.js.map +1 -0
- package/lib/upstream.d.ts +174 -0
- package/lib/upstream.d.ts.map +1 -0
- package/lib/upstream.js +452 -0
- package/lib/upstream.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TelegramRichStreamingReply` — DSH `ReplyHandle` over Bot API 10.1 rich
|
|
3
|
+
* drafts (plan §6.1 / §20.4).
|
|
4
|
+
*
|
|
5
|
+
* For private DMs with rich output active, the plan replaces per-delta
|
|
6
|
+
* `editMessageText` with the native rich draft flow:
|
|
7
|
+
*
|
|
8
|
+
* sendRichMessageDraft(draft_id) → 30s temporary preview, updated in place
|
|
9
|
+
* … (throttled by ReplyRouter) → same draft_id re-sent with the growing
|
|
10
|
+
* partial Markdown
|
|
11
|
+
* sendRichMessage(final) → persists the final result; the draft
|
|
12
|
+
* preview is superseded
|
|
13
|
+
*
|
|
14
|
+
* The draft is inherently *partial-safe*: the server renders whatever has been
|
|
15
|
+
* sent so far, so an unfinished code fence / table / link never breaks the
|
|
16
|
+
* pipeline (plan §20.4 Partial Markdown Gate). Only the final `sendRichMessage`
|
|
17
|
+
* renders the *complete* Markdown; on a format failure the final falls back to
|
|
18
|
+
* plain text once (never an infinite rich→plain→rich loop).
|
|
19
|
+
*
|
|
20
|
+
* The handle does NOT throttle internally — ReplyRouter owns throttling via
|
|
21
|
+
* config `reply.updateIntervalMs`. Operations are serialized so concurrent
|
|
22
|
+
* router flushes cannot interleave.
|
|
23
|
+
*/
|
|
24
|
+
import type { ChannelTarget, OutboundMessage, ReplyHandle } from '@krischoichoi/channel-core';
|
|
25
|
+
import type { TelegramUpstream } from './upstream.js';
|
|
26
|
+
import { type RenderOptions } from './render/index.js';
|
|
27
|
+
export interface TelegramRichStreamingReplyOptions {
|
|
28
|
+
/** Output formatting policy (mode/fallback) for the final rich send. */
|
|
29
|
+
formatting?: Partial<RenderOptions>;
|
|
30
|
+
}
|
|
31
|
+
export declare class TelegramRichStreamingReply implements ReplyHandle {
|
|
32
|
+
private readonly upstream;
|
|
33
|
+
private readonly target;
|
|
34
|
+
private readonly placeholder;
|
|
35
|
+
private readonly options;
|
|
36
|
+
private text;
|
|
37
|
+
private draftId;
|
|
38
|
+
private started;
|
|
39
|
+
private finalized;
|
|
40
|
+
private draftPending;
|
|
41
|
+
private finalPending;
|
|
42
|
+
private cooldownUntil;
|
|
43
|
+
private cooldownTimer;
|
|
44
|
+
private queue;
|
|
45
|
+
constructor(upstream: TelegramUpstream, target: ChannelTarget, placeholder?: string, options?: TelegramRichStreamingReplyOptions, draftId?: number);
|
|
46
|
+
get activeDraftId(): number;
|
|
47
|
+
/** Open the temporary draft preview. */
|
|
48
|
+
start(): Promise<void>;
|
|
49
|
+
append(delta: string): Promise<void>;
|
|
50
|
+
replace(message: OutboundMessage): Promise<void>;
|
|
51
|
+
finish(message?: OutboundMessage): Promise<void>;
|
|
52
|
+
fail(error: unknown): Promise<void>;
|
|
53
|
+
/** Re-send the same draft id with the newest partial Markdown preview. */
|
|
54
|
+
private flushDraft;
|
|
55
|
+
/**
|
|
56
|
+
* Persist the final result. For DM rich mode the final is one (or, when
|
|
57
|
+
* overflowing, several) `sendRichMessage` calls carrying the complete
|
|
58
|
+
* Markdown; a format failure falls back to plain exactly once.
|
|
59
|
+
*/
|
|
60
|
+
private flushFinal;
|
|
61
|
+
private sendFinal;
|
|
62
|
+
private enqueue;
|
|
63
|
+
private inCooldown;
|
|
64
|
+
/** Keep only the current draft/final state while Telegram asks us to wait. */
|
|
65
|
+
private enterCooldown;
|
|
66
|
+
private scheduleCooldown;
|
|
67
|
+
private clearCooldown;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=rich-streaming-reply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-streaming-reply.d.ts","sourceRoot":"","sources":["../src/rich-streaming-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQzE,MAAM,WAAW,iCAAiC;IAChD,wEAAwE;IACxE,UAAU,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CACrC;AAED,qBAAa,0BAA2B,YAAW,WAAW;IAY1D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAd1B,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAA4C;IACjE,OAAO,CAAC,KAAK,CAAoC;gBAG9B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,aAAa,EACrB,WAAW,SAAM,EACjB,OAAO,GAAE,iCAAsC,EAChE,OAAO,CAAC,EAAE,MAAM;IAKlB,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,wCAAwC;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpC,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhD,MAAM,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhD,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnC,0EAA0E;YAC5D,UAAU;IAkBxB;;;;OAIG;YACW,UAAU;YAaV,SAAS;IAyBvB,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,UAAU;IAMlB,8EAA8E;IAC9E,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,aAAa;CAOtB"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { TelegramApiError } from './api-error.js';
|
|
2
|
+
import { RICH_MESSAGE_MAX_UTF8 } from './rich-message.js';
|
|
3
|
+
import { truncateGraphemes } from './render/segment.js';
|
|
4
|
+
import { sendWithFallback } from './render/index.js';
|
|
5
|
+
/** A non-zero integer that identifies this reply stream across draft updates. */
|
|
6
|
+
function newDraftId() {
|
|
7
|
+
const value = globalThis.crypto.getRandomValues(new Uint32Array(1))[0] ?? 0;
|
|
8
|
+
return value === 0 ? 1 : value;
|
|
9
|
+
}
|
|
10
|
+
export class TelegramRichStreamingReply {
|
|
11
|
+
upstream;
|
|
12
|
+
target;
|
|
13
|
+
placeholder;
|
|
14
|
+
options;
|
|
15
|
+
text = '';
|
|
16
|
+
draftId;
|
|
17
|
+
started = false;
|
|
18
|
+
finalized = false;
|
|
19
|
+
draftPending = false;
|
|
20
|
+
finalPending = false;
|
|
21
|
+
cooldownUntil = 0;
|
|
22
|
+
cooldownTimer;
|
|
23
|
+
queue = Promise.resolve();
|
|
24
|
+
constructor(upstream, target, placeholder = '…', options = {}, draftId) {
|
|
25
|
+
this.upstream = upstream;
|
|
26
|
+
this.target = target;
|
|
27
|
+
this.placeholder = placeholder;
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.draftId = draftId ?? newDraftId();
|
|
30
|
+
}
|
|
31
|
+
get activeDraftId() {
|
|
32
|
+
return this.draftId;
|
|
33
|
+
}
|
|
34
|
+
/** Open the temporary draft preview. */
|
|
35
|
+
async start() {
|
|
36
|
+
if (this.started)
|
|
37
|
+
return;
|
|
38
|
+
this.started = true;
|
|
39
|
+
this.draftPending = true;
|
|
40
|
+
await this.flushDraft(this.placeholder);
|
|
41
|
+
}
|
|
42
|
+
append(delta) {
|
|
43
|
+
return this.replace({ text: this.text + delta });
|
|
44
|
+
}
|
|
45
|
+
replace(message) {
|
|
46
|
+
return this.enqueue(async () => {
|
|
47
|
+
if (this.finalized)
|
|
48
|
+
return;
|
|
49
|
+
if (message.text === undefined)
|
|
50
|
+
return;
|
|
51
|
+
this.text = message.text;
|
|
52
|
+
this.draftPending = true;
|
|
53
|
+
await this.flushDraft();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
finish(message) {
|
|
57
|
+
return this.enqueue(async () => {
|
|
58
|
+
if (this.finalized)
|
|
59
|
+
return;
|
|
60
|
+
if (message?.text !== undefined)
|
|
61
|
+
this.text = message.text;
|
|
62
|
+
this.draftPending = false;
|
|
63
|
+
this.finalPending = true;
|
|
64
|
+
await this.flushFinal();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
fail(error) {
|
|
68
|
+
return this.enqueue(async () => {
|
|
69
|
+
if (this.finalized)
|
|
70
|
+
return;
|
|
71
|
+
if (isRateLimit(error))
|
|
72
|
+
return;
|
|
73
|
+
this.finalized = true;
|
|
74
|
+
this.clearCooldown();
|
|
75
|
+
const msg = `Warning: ${messageOf(error)}`;
|
|
76
|
+
try {
|
|
77
|
+
await sendWithFallback(msg, { mode: 'plain' }, (plan) => {
|
|
78
|
+
const text = plan.kind === 'rich' ? (plan.texts[0] ?? '') : (plan.chunks[0] ?? '');
|
|
79
|
+
return this.upstream.sendMessage(this.target.conversationId, text);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Marking failure must not mask the original error.
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Re-send the same draft id with the newest partial Markdown preview. */
|
|
88
|
+
async flushDraft(placeholder) {
|
|
89
|
+
if (!this.started || !this.draftPending || this.inCooldown())
|
|
90
|
+
return;
|
|
91
|
+
const source = this.text || placeholder;
|
|
92
|
+
if (!source)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
await this.upstream.sendRichMessageDraft(this.target.conversationId, this.draftId, { markdown: truncateGraphemes(source, RICH_MESSAGE_MAX_UTF8) }, targetOptions(this.target));
|
|
96
|
+
this.draftPending = false;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!isRateLimit(error))
|
|
100
|
+
throw error;
|
|
101
|
+
this.enterCooldown(error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Persist the final result. For DM rich mode the final is one (or, when
|
|
106
|
+
* overflowing, several) `sendRichMessage` calls carrying the complete
|
|
107
|
+
* Markdown; a format failure falls back to plain exactly once.
|
|
108
|
+
*/
|
|
109
|
+
async flushFinal() {
|
|
110
|
+
if (!this.finalPending || this.inCooldown())
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
await this.sendFinal();
|
|
114
|
+
this.finalPending = false;
|
|
115
|
+
this.finalized = true;
|
|
116
|
+
this.clearCooldown();
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!isRateLimit(error))
|
|
120
|
+
throw error;
|
|
121
|
+
this.enterCooldown(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async sendFinal() {
|
|
125
|
+
const options = {
|
|
126
|
+
mode: this.options.formatting?.mode ?? 'auto',
|
|
127
|
+
};
|
|
128
|
+
const source = this.text;
|
|
129
|
+
if (!source)
|
|
130
|
+
return;
|
|
131
|
+
await sendWithFallback(source, options, async (plan) => {
|
|
132
|
+
if (plan.kind === 'rich') {
|
|
133
|
+
for (const text of plan.texts) {
|
|
134
|
+
await this.upstream.sendRichMessage(this.target.conversationId, { markdown: text }, targetOptions(this.target));
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// plain fallback path.
|
|
139
|
+
for (const chunk of plan.chunks) {
|
|
140
|
+
await this.upstream.sendMessage(this.target.conversationId, chunk, targetOptions(this.target), plan.parseMode ? { parseMode: plan.parseMode } : undefined);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
enqueue(task) {
|
|
145
|
+
const next = this.queue.then(task, task);
|
|
146
|
+
this.queue = next.then(() => undefined, () => undefined);
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
inCooldown() {
|
|
150
|
+
if (Date.now() >= this.cooldownUntil)
|
|
151
|
+
return false;
|
|
152
|
+
this.scheduleCooldown();
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
/** Keep only the current draft/final state while Telegram asks us to wait. */
|
|
156
|
+
enterCooldown(error) {
|
|
157
|
+
const seconds = error.parameters?.retryAfter;
|
|
158
|
+
const delay = typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0
|
|
159
|
+
? Math.ceil(seconds * 1000)
|
|
160
|
+
: 1_000;
|
|
161
|
+
this.cooldownUntil = Math.max(this.cooldownUntil, Date.now() + delay);
|
|
162
|
+
this.scheduleCooldown();
|
|
163
|
+
}
|
|
164
|
+
scheduleCooldown() {
|
|
165
|
+
if (this.cooldownTimer || this.finalized)
|
|
166
|
+
return;
|
|
167
|
+
const delay = Math.max(0, this.cooldownUntil - Date.now());
|
|
168
|
+
this.cooldownTimer = setTimeout(() => {
|
|
169
|
+
this.cooldownTimer = undefined;
|
|
170
|
+
void this.enqueue(async () => {
|
|
171
|
+
if (this.finalized)
|
|
172
|
+
return;
|
|
173
|
+
if (this.finalPending) {
|
|
174
|
+
await this.flushFinal();
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
await this.flushDraft();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}, delay);
|
|
181
|
+
}
|
|
182
|
+
clearCooldown() {
|
|
183
|
+
this.cooldownUntil = 0;
|
|
184
|
+
if (this.cooldownTimer) {
|
|
185
|
+
clearTimeout(this.cooldownTimer);
|
|
186
|
+
this.cooldownTimer = undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function isRateLimit(error) {
|
|
191
|
+
return error instanceof TelegramApiError || (typeof error === 'object'
|
|
192
|
+
&& error !== null
|
|
193
|
+
&& 'kind' in error
|
|
194
|
+
&& error.kind === 'rate-limit');
|
|
195
|
+
}
|
|
196
|
+
function messageOf(error) {
|
|
197
|
+
if (error instanceof Error)
|
|
198
|
+
return error.message;
|
|
199
|
+
return String(error);
|
|
200
|
+
}
|
|
201
|
+
function targetOptions(target) {
|
|
202
|
+
return {
|
|
203
|
+
replyToMessageId: target.replyToMessageId,
|
|
204
|
+
messageThreadId: target.threadId,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=rich-streaming-reply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-streaming-reply.js","sourceRoot":"","sources":["../src/rich-streaming-reply.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TelegramStreamingReply` — DSH `ReplyHandle` over Telegram edit streaming.
|
|
3
|
+
*
|
|
4
|
+
* Telegram streaming is modeled as: send one message, then edit it in place
|
|
5
|
+
* with `editMessageText` as full-text previews arrive. This maps onto the
|
|
6
|
+
* reply contract used by ReplyRouter for `streaming: 'edit'`:
|
|
7
|
+
* - `replace(message)` -> create the message on first content, then edit
|
|
8
|
+
* - `append(delta)` -> accumulate and edit full text (kept for symmetry)
|
|
9
|
+
* - `finish(message?)` -> render + send the final text and close
|
|
10
|
+
* - `fail(error)` -> edit a short failure note when a message exists
|
|
11
|
+
*
|
|
12
|
+
* ## Preview-freeze fix (plan §3.2 / §6.3 / §20.5)
|
|
13
|
+
*
|
|
14
|
+
* The old implementation always edited `chunks[0]`, so once the model passed
|
|
15
|
+
* 4096 chars the first chunk stopped changing and the preview appeared frozen.
|
|
16
|
+
* This version shows a *rolling preview*: up to 4096 visible chars it shows the
|
|
17
|
+
* whole text; beyond that it shows `…` + the newest `4095` graphemes, so the
|
|
18
|
+
* preview keeps visibly moving while the model streams. The final message is
|
|
19
|
+
* always sent with complete, stable segmentation.
|
|
20
|
+
*
|
|
21
|
+
* ## Group rich final (plan §6.2)
|
|
22
|
+
*
|
|
23
|
+
* When configured for rich output on a non-DM target, `finish` upgrades the
|
|
24
|
+
* same message to a Rich Message with `editMessageRich` (first segment) plus
|
|
25
|
+
* `sendRichMessage` for any overflow. A formatting error falls back to plain
|
|
26
|
+
* once; the preview phase always uses plain edits so partial Markdown never
|
|
27
|
+
* triggers a rich parse (plan §20.5).
|
|
28
|
+
*
|
|
29
|
+
* ReplyRouter owns normal throttling via config `reply.updateIntervalMs`.
|
|
30
|
+
* This handle additionally honors Telegram 429 cooldowns by retaining only
|
|
31
|
+
* the newest desired preview until the retry window expires.
|
|
32
|
+
*/
|
|
33
|
+
import type { ChannelTarget, OutboundMessage, ReplyHandle } from '@krischoichoi/channel-core';
|
|
34
|
+
import type { TelegramUpstream } from './upstream.js';
|
|
35
|
+
import { type RenderOptions } from './render/index.js';
|
|
36
|
+
export interface TelegramStreamingReplyOptions {
|
|
37
|
+
/** Output formatting policy for the final message (mode/fallback). */
|
|
38
|
+
formatting?: Partial<RenderOptions>;
|
|
39
|
+
/**
|
|
40
|
+
* When true and rich formatting is active, `finish` edits the SAME message
|
|
41
|
+
* into a Rich Message (group path); otherwise the final is plain text.
|
|
42
|
+
*/
|
|
43
|
+
richFinal?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare class TelegramStreamingReply implements ReplyHandle {
|
|
46
|
+
private readonly upstream;
|
|
47
|
+
private readonly target;
|
|
48
|
+
private readonly placeholder;
|
|
49
|
+
private readonly options;
|
|
50
|
+
private text;
|
|
51
|
+
private sentPreview;
|
|
52
|
+
private messageId;
|
|
53
|
+
private finalized;
|
|
54
|
+
private queue;
|
|
55
|
+
/** Latest desired preview; deliberately not an edit queue. */
|
|
56
|
+
private pendingPreview;
|
|
57
|
+
private cooldownUntil;
|
|
58
|
+
private cooldownTimer;
|
|
59
|
+
private finishing;
|
|
60
|
+
constructor(upstream: TelegramUpstream, target: ChannelTarget, placeholder?: string, options?: TelegramStreamingReplyOptions);
|
|
61
|
+
/** Send the initial placeholder message (called once by the adapter). */
|
|
62
|
+
start(): Promise<void>;
|
|
63
|
+
append(delta: string): Promise<void>;
|
|
64
|
+
replace(message: OutboundMessage): Promise<void>;
|
|
65
|
+
finish(message?: OutboundMessage): Promise<void>;
|
|
66
|
+
fail(error: unknown): Promise<void>;
|
|
67
|
+
/** Edit the rolling preview now; on `final` render + send the complete result. */
|
|
68
|
+
private sync;
|
|
69
|
+
/**
|
|
70
|
+
* Send the final message. Plain mode: first chunk edits the existing message,
|
|
71
|
+
* overflow chunks are sent as new messages. Rich mode (richFinal + rich
|
|
72
|
+
* formatting): upgrade the same message to a Rich Message; fall back to plain
|
|
73
|
+
* once on a format error.
|
|
74
|
+
*/
|
|
75
|
+
private sendFinal;
|
|
76
|
+
/** Upgrade the in-place message to a Rich Message with plain-once fallback. */
|
|
77
|
+
private sendRichFinal;
|
|
78
|
+
/** Retry only the in-place final edit after a Telegram-requested cooldown. */
|
|
79
|
+
private editFinalText;
|
|
80
|
+
/** The rich final path uses Telegram's edit endpoint too, so it shares 429 recovery. */
|
|
81
|
+
private editFinalRich;
|
|
82
|
+
private isCoolingDown;
|
|
83
|
+
/** Honor the server's retry hint, extending (never shortening) cooldown. */
|
|
84
|
+
private startCooldown;
|
|
85
|
+
/** One timer, one latest-value flush. It never replays a sequence of edits. */
|
|
86
|
+
private scheduleCooldownFlush;
|
|
87
|
+
private clearCooldownTimer;
|
|
88
|
+
private waitForCooldown;
|
|
89
|
+
private enqueue;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a rolling preview for an in-progress stream: show the whole text up to
|
|
93
|
+
* `limit`, otherwise `…` + the newest `limit - 1` graphemes. This keeps the
|
|
94
|
+
* preview visibly changing once the model exceeds the single-message limit
|
|
95
|
+
* (plan §6.3), fixing the frozen `chunks[0]` behaviour.
|
|
96
|
+
*/
|
|
97
|
+
export declare function makePreview(text: string, limit: number): string;
|
|
98
|
+
//# sourceMappingURL=streaming-reply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-reply.d.ts","sourceRoot":"","sources":["../src/streaming-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACZ,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,sEAAsE;IACtE,UAAU,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,qBAAa,sBAAuB,YAAW,WAAW;IAatD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAf1B,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAoC;IACjD,8DAA8D;IAC9D,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAA4C;IACjE,OAAO,CAAC,SAAS,CAAS;gBAGP,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,aAAa,EACrB,WAAW,SAAM,EACjB,OAAO,GAAE,6BAAkC;IAG9D,yEAAyE;IACnE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpC,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhD,MAAM,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAehD,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAenC,kFAAkF;YACpE,IAAI;IA2BlB;;;;;OAKG;YACW,SAAS;IAyBvB,+EAA+E;YACjE,aAAa;IAkC3B,8EAA8E;YAChE,aAAa;IAa3B,wFAAwF;YAC1E,aAAa;IAa3B,OAAO,CAAC,aAAa;IAIrB,4EAA4E;IAC5E,OAAO,CAAC,aAAa;IASrB,+EAA+E;IAC/E,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,kBAAkB;YAKZ,eAAe;IAQ7B,OAAO,CAAC,OAAO;CAQhB;AAkBD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/D"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { TelegramApiError } from './api-error.js';
|
|
2
|
+
import { REGULAR_MESSAGE_MAX } from './rich-message.js';
|
|
3
|
+
import { truncateGraphemes, tailGraphemes, visibleLength, splitByGraphemes } from './render/segment.js';
|
|
4
|
+
import { sendWithFallback } from './render/index.js';
|
|
5
|
+
export class TelegramStreamingReply {
|
|
6
|
+
upstream;
|
|
7
|
+
target;
|
|
8
|
+
placeholder;
|
|
9
|
+
options;
|
|
10
|
+
text = '';
|
|
11
|
+
sentPreview;
|
|
12
|
+
messageId;
|
|
13
|
+
finalized = false;
|
|
14
|
+
queue = Promise.resolve();
|
|
15
|
+
/** Latest desired preview; deliberately not an edit queue. */
|
|
16
|
+
pendingPreview;
|
|
17
|
+
cooldownUntil = 0;
|
|
18
|
+
cooldownTimer;
|
|
19
|
+
finishing = false;
|
|
20
|
+
constructor(upstream, target, placeholder = '…', options = {}) {
|
|
21
|
+
this.upstream = upstream;
|
|
22
|
+
this.target = target;
|
|
23
|
+
this.placeholder = placeholder;
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
/** Send the initial placeholder message (called once by the adapter). */
|
|
27
|
+
async start() {
|
|
28
|
+
if (this.messageId)
|
|
29
|
+
return;
|
|
30
|
+
const preview = makePreview(this.placeholder, REGULAR_MESSAGE_MAX);
|
|
31
|
+
const sent = await this.upstream.sendMessage(this.target.conversationId, preview, targetOptions(this.target));
|
|
32
|
+
this.messageId = sent.messageId;
|
|
33
|
+
this.sentPreview = preview;
|
|
34
|
+
}
|
|
35
|
+
append(delta) {
|
|
36
|
+
return this.replace({ text: this.text + delta });
|
|
37
|
+
}
|
|
38
|
+
replace(message) {
|
|
39
|
+
return this.enqueue(async () => {
|
|
40
|
+
if (this.finalized || this.finishing)
|
|
41
|
+
return;
|
|
42
|
+
if (message.text === undefined)
|
|
43
|
+
return;
|
|
44
|
+
this.text = message.text;
|
|
45
|
+
this.pendingPreview = makePreview(this.text, REGULAR_MESSAGE_MAX);
|
|
46
|
+
await this.sync(false);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
finish(message) {
|
|
50
|
+
// Suppress a deferred preview immediately. `finish` itself may wait in the
|
|
51
|
+
// operation queue behind one in-flight edit, but no newer preview should
|
|
52
|
+
// be sent once the turn has ended.
|
|
53
|
+
this.finishing = true;
|
|
54
|
+
this.clearCooldownTimer();
|
|
55
|
+
return this.enqueue(async () => {
|
|
56
|
+
if (this.finalized)
|
|
57
|
+
return;
|
|
58
|
+
if (message?.text !== undefined)
|
|
59
|
+
this.text = message.text;
|
|
60
|
+
this.pendingPreview = undefined;
|
|
61
|
+
await this.sync(true);
|
|
62
|
+
this.finalized = true;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
fail(error) {
|
|
66
|
+
return this.enqueue(async () => {
|
|
67
|
+
if (this.finalized)
|
|
68
|
+
return;
|
|
69
|
+
if (isRateLimitError(error))
|
|
70
|
+
return;
|
|
71
|
+
this.finalized = true;
|
|
72
|
+
if (!this.messageId)
|
|
73
|
+
return;
|
|
74
|
+
const text = truncateGraphemes(`Warning: ${messageOf(error)}`, REGULAR_MESSAGE_MAX);
|
|
75
|
+
try {
|
|
76
|
+
await this.upstream.editMessageText(this.target.conversationId, this.messageId, text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Marking the message failed must not mask the original error.
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Edit the rolling preview now; on `final` render + send the complete result. */
|
|
84
|
+
async sync(final) {
|
|
85
|
+
if (!this.text)
|
|
86
|
+
return;
|
|
87
|
+
if (final) {
|
|
88
|
+
await this.waitForCooldown();
|
|
89
|
+
await this.sendFinal();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!this.messageId)
|
|
93
|
+
return;
|
|
94
|
+
const preview = this.pendingPreview ?? makePreview(this.text, REGULAR_MESSAGE_MAX);
|
|
95
|
+
if (this.finishing || this.isCoolingDown()) {
|
|
96
|
+
this.pendingPreview = preview;
|
|
97
|
+
if (!this.finishing)
|
|
98
|
+
this.scheduleCooldownFlush();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (this.sentPreview !== preview) {
|
|
102
|
+
this.pendingPreview = undefined;
|
|
103
|
+
try {
|
|
104
|
+
await this.upstream.editMessageText(this.target.conversationId, this.messageId, preview);
|
|
105
|
+
this.sentPreview = preview;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (!isRateLimitError(error))
|
|
109
|
+
throw error;
|
|
110
|
+
if (this.pendingPreview === undefined)
|
|
111
|
+
this.pendingPreview = preview;
|
|
112
|
+
this.startCooldown(error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Send the final message. Plain mode: first chunk edits the existing message,
|
|
118
|
+
* overflow chunks are sent as new messages. Rich mode (richFinal + rich
|
|
119
|
+
* formatting): upgrade the same message to a Rich Message; fall back to plain
|
|
120
|
+
* once on a format error.
|
|
121
|
+
*/
|
|
122
|
+
async sendFinal() {
|
|
123
|
+
const mode = this.options.formatting?.mode ?? 'auto';
|
|
124
|
+
if (this.options.richFinal && mode !== 'plain') {
|
|
125
|
+
await this.sendRichFinal();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const chunks = splitByGraphemes(this.text, REGULAR_MESSAGE_MAX);
|
|
129
|
+
if (this.messageId && this.sentPreview !== chunks[0]) {
|
|
130
|
+
await this.editFinalText(chunks[0] ?? '');
|
|
131
|
+
this.sentPreview = chunks[0];
|
|
132
|
+
}
|
|
133
|
+
else if (!this.messageId) {
|
|
134
|
+
const sent = await this.upstream.sendMessage(this.target.conversationId, chunks[0] ?? '', targetOptions(this.target));
|
|
135
|
+
this.messageId = sent.messageId;
|
|
136
|
+
}
|
|
137
|
+
for (const chunk of chunks.slice(1)) {
|
|
138
|
+
await this.upstream.sendMessage(this.target.conversationId, chunk, {
|
|
139
|
+
messageThreadId: this.target.threadId,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Upgrade the in-place message to a Rich Message with plain-once fallback. */
|
|
144
|
+
async sendRichFinal() {
|
|
145
|
+
const source = this.text;
|
|
146
|
+
if (!source)
|
|
147
|
+
return;
|
|
148
|
+
await sendWithFallback(source, { mode: 'rich-markdown' }, async (plan) => {
|
|
149
|
+
if (plan.kind === 'rich') {
|
|
150
|
+
// Edit the same message into the first rich segment; overflow as new
|
|
151
|
+
// rich messages.
|
|
152
|
+
if (this.messageId) {
|
|
153
|
+
await this.editFinalRich({ markdown: plan.texts[0] ?? '' });
|
|
154
|
+
}
|
|
155
|
+
for (const text of plan.texts.slice(1)) {
|
|
156
|
+
await this.upstream.sendRichMessage(this.target.conversationId, { markdown: text });
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Plain fallback on the same message + new overflow messages.
|
|
161
|
+
if (this.messageId) {
|
|
162
|
+
await this.editFinalText(plan.chunks[0] ?? '');
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const sent = await this.upstream.sendMessage(this.target.conversationId, plan.chunks[0] ?? '', targetOptions(this.target));
|
|
166
|
+
this.messageId = sent.messageId;
|
|
167
|
+
}
|
|
168
|
+
for (const chunk of plan.chunks.slice(1)) {
|
|
169
|
+
await this.upstream.sendMessage(this.target.conversationId, chunk, {
|
|
170
|
+
messageThreadId: this.target.threadId,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** Retry only the in-place final edit after a Telegram-requested cooldown. */
|
|
176
|
+
async editFinalText(text) {
|
|
177
|
+
while (true) {
|
|
178
|
+
await this.waitForCooldown();
|
|
179
|
+
try {
|
|
180
|
+
await this.upstream.editMessageText(this.target.conversationId, this.messageId, text);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (!isRateLimitError(error))
|
|
185
|
+
throw error;
|
|
186
|
+
this.startCooldown(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** The rich final path uses Telegram's edit endpoint too, so it shares 429 recovery. */
|
|
191
|
+
async editFinalRich(message) {
|
|
192
|
+
while (true) {
|
|
193
|
+
await this.waitForCooldown();
|
|
194
|
+
try {
|
|
195
|
+
await this.upstream.editMessageRich(this.target.conversationId, this.messageId, message);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (!isRateLimitError(error))
|
|
200
|
+
throw error;
|
|
201
|
+
this.startCooldown(error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
isCoolingDown() {
|
|
206
|
+
return Date.now() < this.cooldownUntil;
|
|
207
|
+
}
|
|
208
|
+
/** Honor the server's retry hint, extending (never shortening) cooldown. */
|
|
209
|
+
startCooldown(error) {
|
|
210
|
+
const retryAfter = error.parameters?.retryAfter;
|
|
211
|
+
const delayMs = Number.isFinite(retryAfter)
|
|
212
|
+
? Math.max(1, Math.ceil(retryAfter)) * 1000
|
|
213
|
+
: 1000;
|
|
214
|
+
this.cooldownUntil = Math.max(this.cooldownUntil, Date.now() + delayMs);
|
|
215
|
+
this.scheduleCooldownFlush();
|
|
216
|
+
}
|
|
217
|
+
/** One timer, one latest-value flush. It never replays a sequence of edits. */
|
|
218
|
+
scheduleCooldownFlush() {
|
|
219
|
+
if (this.finishing || this.finalized || this.cooldownTimer || !this.pendingPreview)
|
|
220
|
+
return;
|
|
221
|
+
this.cooldownTimer = setTimeout(() => {
|
|
222
|
+
this.cooldownTimer = undefined;
|
|
223
|
+
void this.enqueue(() => this.sync(false)).catch(() => { });
|
|
224
|
+
}, Math.max(1, this.cooldownUntil - Date.now()));
|
|
225
|
+
}
|
|
226
|
+
clearCooldownTimer() {
|
|
227
|
+
if (this.cooldownTimer)
|
|
228
|
+
clearTimeout(this.cooldownTimer);
|
|
229
|
+
this.cooldownTimer = undefined;
|
|
230
|
+
}
|
|
231
|
+
async waitForCooldown() {
|
|
232
|
+
while (this.isCoolingDown()) {
|
|
233
|
+
await new Promise((resolve) => {
|
|
234
|
+
setTimeout(resolve, Math.max(1, this.cooldownUntil - Date.now()));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
enqueue(task) {
|
|
239
|
+
const next = this.queue.then(task, task);
|
|
240
|
+
this.queue = next.then(() => undefined, () => undefined);
|
|
241
|
+
return next;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function messageOf(error) {
|
|
245
|
+
if (error instanceof Error)
|
|
246
|
+
return error.message;
|
|
247
|
+
return String(error);
|
|
248
|
+
}
|
|
249
|
+
function isRateLimitError(error) {
|
|
250
|
+
return error instanceof TelegramApiError && error.kind === 'rate-limit';
|
|
251
|
+
}
|
|
252
|
+
function targetOptions(target) {
|
|
253
|
+
return {
|
|
254
|
+
replyToMessageId: target.replyToMessageId,
|
|
255
|
+
messageThreadId: target.threadId,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Build a rolling preview for an in-progress stream: show the whole text up to
|
|
260
|
+
* `limit`, otherwise `…` + the newest `limit - 1` graphemes. This keeps the
|
|
261
|
+
* preview visibly changing once the model exceeds the single-message limit
|
|
262
|
+
* (plan §6.3), fixing the frozen `chunks[0]` behaviour.
|
|
263
|
+
*/
|
|
264
|
+
export function makePreview(text, limit) {
|
|
265
|
+
return makeRollingPreview(text, limit);
|
|
266
|
+
}
|
|
267
|
+
/** Internal: rolling head/tail preview implementation. */
|
|
268
|
+
function makeRollingPreview(text, limit) {
|
|
269
|
+
if (visibleLength(text) <= limit)
|
|
270
|
+
return text;
|
|
271
|
+
// Show `…` + the newest (limit-1) graphemes so the user sees output continue.
|
|
272
|
+
return `…${tailGraphemes(text, limit - 1)}`;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=streaming-reply.js.map
|