@llblab/pi-telegram 0.6.0 → 0.6.2
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 +3 -1
- package/docs/architecture.md +2 -2
- package/docs/attachment-handlers.md +1 -1
- package/docs/outbound-handlers.md +4 -2
- package/index.ts +31 -18
- package/lib/command-templates.ts +6 -3
- package/lib/locks.ts +2 -10
- package/lib/media.ts +23 -12
- package/lib/outbound-handlers.ts +49 -24
- package/lib/polling.ts +1 -1
- package/lib/prompts.ts +1 -1
- package/lib/queue.ts +87 -4
- package/lib/routing.ts +1 -1
- package/lib/runtime.ts +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ A TTS plus MP3-to-OGG setup can be expressed as `template: [...]`. The bridge pr
|
|
|
171
171
|
|
|
172
172
|
#### Buttons
|
|
173
173
|
|
|
174
|
-
Button blocks attach inline quick replies to the final text. Use one independent `telegram_button` block per action; its `label` is shown in Telegram and its body is sent back to pi when tapped:
|
|
174
|
+
Button blocks attach inline quick replies to the final text. Use one independent `telegram_button` block per action; its `label` is shown in Telegram and its body is sent back to pi when tapped. If the prompt should equal the label, the body can be omitted:
|
|
175
175
|
|
|
176
176
|
```md
|
|
177
177
|
I can continue.
|
|
@@ -179,6 +179,8 @@ I can continue.
|
|
|
179
179
|
<!-- telegram_button label="Continue"
|
|
180
180
|
Continue with the current plan.
|
|
181
181
|
-->
|
|
182
|
+
|
|
183
|
+
<!-- telegram_button label="OK" -->
|
|
182
184
|
```
|
|
183
185
|
|
|
184
186
|
Button prompts are routed back into the normal Telegram queue as prompt turns. Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
|
package/docs/architecture.md
CHANGED
|
@@ -112,7 +112,7 @@ Dispatch is gated by:
|
|
|
112
112
|
- `ctx.isIdle()` being true
|
|
113
113
|
- `ctx.hasPendingMessages()` being false
|
|
114
114
|
|
|
115
|
-
This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Telegram `/status` and `/model` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
|
|
115
|
+
This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Post-agent-end dispatch retries are scheduled through a session-bound deferred dispatcher that activates on session start, cancels timers on session shutdown, and skips callbacks from older generations before they touch `ExtensionContext`. Telegram `/status` and `/model` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
|
|
116
116
|
|
|
117
117
|
### Abort Behavior
|
|
118
118
|
|
|
@@ -155,7 +155,7 @@ Telegram prompt responses use explicit delivery context to attach outbound text,
|
|
|
155
155
|
|
|
156
156
|
Outbound files are sent only after the active Telegram turn completes, must be staged through the `telegram_attach` tool, are staged atomically per tool call, are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`, and use file-backed multipart blobs so large sends do not require preloading whole files into memory.
|
|
157
157
|
|
|
158
|
-
Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes the block body through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, top-level `timeout` wraps the whole sequence, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn. This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools.
|
|
158
|
+
Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes the block body through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, top-level `timeout` wraps the whole sequence, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text, or the button label when the body is omitted, as a normal Telegram prompt turn. This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools.
|
|
159
159
|
|
|
160
160
|
## Interactive Controls
|
|
161
161
|
|
|
@@ -39,7 +39,7 @@ Attachment handlers support these built-in placeholders:
|
|
|
39
39
|
|
|
40
40
|
`defaults` may provide additional placeholder values such as `{lang}` or `{model}`. `args` is only a string-array declaration of supported placeholders; defaults belong in `defaults` or inline placeholders such as `{lang=ru}`. Examples prefer explicit flag-style CLIs for readability, but positional forms such as `/path/to/stt {file} {lang=ru} {model=voxtral-mini-latest}` are equally valid when the target script supports them.
|
|
41
41
|
|
|
42
|
-
If a top-level one-step handler template has no `{file}` placeholder, the downloaded file path is appended as the last command arg
|
|
42
|
+
If a top-level one-step handler template has no `{file}` placeholder, the downloaded file path is appended as the last command arg as a one-step handler convenience. Composition steps are plain command templates and do not receive implicit file-path args; include `{file}` explicitly where needed.
|
|
43
43
|
|
|
44
44
|
## Ordered Fallbacks
|
|
45
45
|
|
|
@@ -75,7 +75,7 @@ For one-step `template` handlers, stdout remains the default result channel: the
|
|
|
75
75
|
|
|
76
76
|
## Buttons Markup
|
|
77
77
|
|
|
78
|
-
Assistant replies can include independent button blocks. The block body is the prompt sent back to pi when the user taps the button:
|
|
78
|
+
Assistant replies can include independent button blocks. The block body is the prompt sent back to pi when the user taps the button; omit the body when the prompt should equal the label:
|
|
79
79
|
|
|
80
80
|
```md
|
|
81
81
|
I can continue.
|
|
@@ -87,11 +87,13 @@ Continue with the current plan.
|
|
|
87
87
|
<!-- telegram_button label="Show risks"
|
|
88
88
|
List the main risks first.
|
|
89
89
|
-->
|
|
90
|
+
|
|
91
|
+
<!-- telegram_button label="Done" -->
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
Rules:
|
|
93
95
|
|
|
94
|
-
- `telegram_button label="Label"` creates one independent button row whose prompt is the block body.
|
|
96
|
+
- `telegram_button label="Label"` creates one independent button row whose prompt is the block body, or the label itself when the body is omitted.
|
|
95
97
|
- The opening `<!-- telegram_button` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown.
|
|
96
98
|
- Use one block per button; this mirrors HTML's singular element model and avoids a nested button DSL inside comments.
|
|
97
99
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
package/index.ts
CHANGED
|
@@ -46,10 +46,16 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
46
46
|
const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
|
|
47
47
|
getBotToken: configStore.getBotToken,
|
|
48
48
|
});
|
|
49
|
-
const mediaGroupRuntime =
|
|
50
|
-
|
|
49
|
+
const mediaGroupRuntime = Media.createTelegramMediaGroupController<
|
|
50
|
+
Api.TelegramMessage,
|
|
51
|
+
Pi.ExtensionContext
|
|
52
|
+
>();
|
|
51
53
|
const telegramQueueStore =
|
|
52
54
|
Queue.createTelegramQueueStore<Pi.ExtensionContext>();
|
|
55
|
+
const deferredQueueDispatchRuntime =
|
|
56
|
+
Queue.createTelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>({
|
|
57
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
58
|
+
});
|
|
53
59
|
const pollingControllerState = Polling.createTelegramPollingControllerState();
|
|
54
60
|
const { getStatusLines, updateStatus } =
|
|
55
61
|
Status.createTelegramBridgeStatusRuntime<
|
|
@@ -88,12 +94,14 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
88
94
|
updateStatus,
|
|
89
95
|
});
|
|
90
96
|
const attachmentHandlerRuntime =
|
|
91
|
-
AttachmentHandlers.createTelegramAttachmentHandlerRuntime<Pi.ExtensionContext>(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
AttachmentHandlers.createTelegramAttachmentHandlerRuntime<Pi.ExtensionContext>(
|
|
98
|
+
{
|
|
99
|
+
getHandlers: configStore.getAttachmentHandlers,
|
|
100
|
+
execCommand: CommandTemplates.execCommandTemplate,
|
|
101
|
+
getCwd: Pi.getExtensionContextCwd,
|
|
102
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
97
105
|
|
|
98
106
|
// --- Telegram API ---
|
|
99
107
|
|
|
@@ -149,6 +157,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
149
157
|
hasDispatchPending: bridgeRuntime.lifecycle.hasDispatchPending,
|
|
150
158
|
isIdle: Pi.isExtensionContextIdle,
|
|
151
159
|
hasPendingMessages: Pi.hasExtensionContextPendingMessages,
|
|
160
|
+
hasDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
152
161
|
updateStatus,
|
|
153
162
|
sendTextReply,
|
|
154
163
|
recordRuntimeEvent: runtimeEvents.record,
|
|
@@ -274,8 +283,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
274
283
|
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
275
284
|
syncCounters: bridgeRuntime.queue.syncCounters,
|
|
276
285
|
syncFlags: bridgeRuntime.lifecycle.syncFlags,
|
|
286
|
+
bindDeferredDispatchContext: deferredQueueDispatchRuntime.bind,
|
|
277
287
|
prepareTempDir,
|
|
278
288
|
updateStatus,
|
|
289
|
+
unbindDeferredDispatchContext: deferredQueueDispatchRuntime.unbind,
|
|
279
290
|
clearPendingMediaGroups: mediaGroupRuntime.clear,
|
|
280
291
|
clearModelMenuState: modelMenuRuntime.clear,
|
|
281
292
|
getActiveTurnChatId: activeTurnRuntime.getChatId,
|
|
@@ -352,6 +363,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
352
363
|
clearDispatchPending: bridgeRuntime.lifecycle.clearDispatchPending,
|
|
353
364
|
}),
|
|
354
365
|
dispatchNextQueuedTelegramTurn,
|
|
366
|
+
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
367
|
+
deferredQueueDispatchRuntime.request,
|
|
355
368
|
clearPreview: previewRuntime.clear,
|
|
356
369
|
setPreviewPendingText: previewRuntime.setPendingText,
|
|
357
370
|
finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
|
|
@@ -362,16 +375,16 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
362
375
|
sendTextReply,
|
|
363
376
|
recordRuntimeEvent: runtimeEvents.record,
|
|
364
377
|
}),
|
|
365
|
-
planOutboundReply:
|
|
366
|
-
buttonActionStore,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
378
|
+
planOutboundReply:
|
|
379
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore),
|
|
380
|
+
sendOutboundReplyArtifacts:
|
|
381
|
+
OutboundHandlers.createTelegramOutboundReplyArtifactSender({
|
|
382
|
+
execCommand: CommandTemplates.execCommandTemplate,
|
|
383
|
+
sendMultipart: callMultipart,
|
|
384
|
+
sendTextReply,
|
|
385
|
+
getHandlers: configStore.getOutboundHandlers,
|
|
386
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
387
|
+
}),
|
|
375
388
|
getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
376
389
|
setActiveToolExecutions: bridgeRuntime.lifecycle.setActiveToolExecutions,
|
|
377
390
|
triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
|
package/lib/command-templates.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface CommandTemplateExecOptions {
|
|
|
33
33
|
timeout?: number;
|
|
34
34
|
signal?: AbortSignal;
|
|
35
35
|
stdin?: string;
|
|
36
|
+
killGrace?: number;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface CommandTemplateExecResult {
|
|
@@ -207,18 +208,20 @@ export function execCommandTemplate(
|
|
|
207
208
|
let killed = false;
|
|
208
209
|
let settled = false;
|
|
209
210
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
211
|
+
let killTimeoutId: NodeJS.Timeout | undefined;
|
|
210
212
|
const killProcess = (): void => {
|
|
211
213
|
if (killed) return;
|
|
212
214
|
killed = true;
|
|
213
215
|
proc.kill("SIGTERM");
|
|
214
|
-
setTimeout(() => {
|
|
215
|
-
if (!
|
|
216
|
-
}, 5000);
|
|
216
|
+
killTimeoutId = setTimeout(() => {
|
|
217
|
+
if (!settled) proc.kill("SIGKILL");
|
|
218
|
+
}, options.killGrace ?? 5000);
|
|
217
219
|
};
|
|
218
220
|
const settle = (code: number): void => {
|
|
219
221
|
if (settled) return;
|
|
220
222
|
settled = true;
|
|
221
223
|
if (timeoutId) clearTimeout(timeoutId);
|
|
224
|
+
if (killTimeoutId) clearTimeout(killTimeoutId);
|
|
222
225
|
if (options.signal)
|
|
223
226
|
options.signal.removeEventListener("abort", killProcess);
|
|
224
227
|
resolve({ stdout, stderr, code, killed });
|
package/lib/locks.ts
CHANGED
|
@@ -246,13 +246,6 @@ export function createTelegramLockedPollingRuntime<
|
|
|
246
246
|
clearInterval(ownershipInterval);
|
|
247
247
|
ownershipInterval = undefined;
|
|
248
248
|
};
|
|
249
|
-
const updateStatusSafely = (ctx: TContext, phase: string) => {
|
|
250
|
-
try {
|
|
251
|
-
deps.updateStatus(ctx);
|
|
252
|
-
} catch (error) {
|
|
253
|
-
deps.recordRuntimeEvent?.("lock", error, { phase });
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
249
|
const suspendPolling = async () => {
|
|
257
250
|
stopOwnershipWatcher();
|
|
258
251
|
if (ownershipStop) {
|
|
@@ -261,7 +254,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
261
254
|
}
|
|
262
255
|
await deps.stopPolling();
|
|
263
256
|
};
|
|
264
|
-
const stopAfterOwnershipLoss = (
|
|
257
|
+
const stopAfterOwnershipLoss = () => {
|
|
265
258
|
if (ownershipStop) return;
|
|
266
259
|
stopOwnershipWatcher();
|
|
267
260
|
ownershipStop = deps
|
|
@@ -271,7 +264,6 @@ export function createTelegramLockedPollingRuntime<
|
|
|
271
264
|
)
|
|
272
265
|
.finally(() => {
|
|
273
266
|
ownershipStop = undefined;
|
|
274
|
-
updateStatusSafely(ctx, "ownership-loss-status");
|
|
275
267
|
});
|
|
276
268
|
};
|
|
277
269
|
const startOwnershipWatcher = (ctx: TContext) => {
|
|
@@ -279,7 +271,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
279
271
|
stopOwnershipWatcher();
|
|
280
272
|
ownershipInterval = setInterval(() => {
|
|
281
273
|
if (deps.lock.owns(owner)) return;
|
|
282
|
-
stopAfterOwnershipLoss(
|
|
274
|
+
stopAfterOwnershipLoss();
|
|
283
275
|
}, ownershipCheckMs);
|
|
284
276
|
ownershipInterval.unref?.();
|
|
285
277
|
};
|
package/lib/media.ts
CHANGED
|
@@ -59,17 +59,20 @@ export interface TelegramMediaGroupMessage {
|
|
|
59
59
|
media_group_id?: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export interface TelegramMediaGroupState<TMessage> {
|
|
62
|
+
export interface TelegramMediaGroupState<TMessage, TContext = unknown> {
|
|
63
63
|
messages: TMessage[];
|
|
64
|
+
context?: TContext;
|
|
64
65
|
flushTimer?: ReturnType<typeof setTimeout>;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export interface TelegramMediaGroupController<
|
|
68
69
|
TMessage extends TelegramMediaGroupMessage,
|
|
70
|
+
TContext = unknown,
|
|
69
71
|
> {
|
|
70
72
|
queueMessage: (options: {
|
|
71
73
|
message: TMessage;
|
|
72
|
-
|
|
74
|
+
context?: TContext;
|
|
75
|
+
dispatchMessages: (messages: TMessage[], ctx?: TContext) => void;
|
|
73
76
|
}) => boolean;
|
|
74
77
|
removeMessages: (messageIds: number[]) => number;
|
|
75
78
|
clear: () => void;
|
|
@@ -79,7 +82,7 @@ export interface TelegramMediaGroupDispatchRuntimeDeps<
|
|
|
79
82
|
TMessage extends TelegramMediaGroupMessage,
|
|
80
83
|
TContext,
|
|
81
84
|
> {
|
|
82
|
-
mediaGroups: TelegramMediaGroupController<TMessage>;
|
|
85
|
+
mediaGroups: TelegramMediaGroupController<TMessage, TContext>;
|
|
83
86
|
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -249,7 +252,7 @@ export function getTelegramMediaGroupKey(
|
|
|
249
252
|
export function removePendingTelegramMediaGroupMessages<
|
|
250
253
|
TMessage extends TelegramMediaGroupMessage,
|
|
251
254
|
>(
|
|
252
|
-
groups: Map<string, TelegramMediaGroupState<TMessage>>,
|
|
255
|
+
groups: Map<string, TelegramMediaGroupState<TMessage, unknown>>,
|
|
253
256
|
messageIds: number[],
|
|
254
257
|
clearTimer: (timer: ReturnType<typeof setTimeout>) => void,
|
|
255
258
|
): number {
|
|
@@ -273,24 +276,27 @@ export function removePendingTelegramMediaGroupMessages<
|
|
|
273
276
|
|
|
274
277
|
export function queueTelegramMediaGroupMessage<
|
|
275
278
|
TMessage extends TelegramMediaGroupMessage,
|
|
279
|
+
TContext = unknown,
|
|
276
280
|
>(options: {
|
|
277
281
|
message: TMessage;
|
|
278
|
-
|
|
282
|
+
context?: TContext;
|
|
283
|
+
groups: Map<string, TelegramMediaGroupState<TMessage, TContext>>;
|
|
279
284
|
debounceMs: number;
|
|
280
285
|
setTimer: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
281
286
|
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
|
|
282
|
-
dispatchMessages: (messages: TMessage[]) => void;
|
|
287
|
+
dispatchMessages: (messages: TMessage[], ctx?: TContext) => void;
|
|
283
288
|
}): boolean {
|
|
284
289
|
const key = getTelegramMediaGroupKey(options.message);
|
|
285
290
|
if (!key) return false;
|
|
286
291
|
const existing = options.groups.get(key) ?? { messages: [] };
|
|
287
292
|
existing.messages.push(options.message);
|
|
293
|
+
existing.context = options.context;
|
|
288
294
|
if (existing.flushTimer) options.clearTimer(existing.flushTimer);
|
|
289
295
|
existing.flushTimer = options.setTimer(() => {
|
|
290
296
|
const state = options.groups.get(key);
|
|
291
297
|
options.groups.delete(key);
|
|
292
298
|
if (!state) return;
|
|
293
|
-
options.dispatchMessages(state.messages);
|
|
299
|
+
options.dispatchMessages(state.messages, state.context);
|
|
294
300
|
}, options.debounceMs);
|
|
295
301
|
options.groups.set(key, existing);
|
|
296
302
|
return true;
|
|
@@ -298,10 +304,11 @@ export function queueTelegramMediaGroupMessage<
|
|
|
298
304
|
|
|
299
305
|
export function createTelegramMediaGroupController<
|
|
300
306
|
TMessage extends TelegramMediaGroupMessage,
|
|
307
|
+
TContext = unknown,
|
|
301
308
|
>(
|
|
302
309
|
options: TelegramMediaGroupControllerOptions = {},
|
|
303
|
-
): TelegramMediaGroupController<TMessage> {
|
|
304
|
-
const groups = new Map<string, TelegramMediaGroupState<TMessage>>();
|
|
310
|
+
): TelegramMediaGroupController<TMessage, TContext> {
|
|
311
|
+
const groups = new Map<string, TelegramMediaGroupState<TMessage, TContext>>();
|
|
305
312
|
const debounceMs = options.debounceMs ?? TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS;
|
|
306
313
|
const setTimer =
|
|
307
314
|
options.setTimer ??
|
|
@@ -309,9 +316,10 @@ export function createTelegramMediaGroupController<
|
|
|
309
316
|
setTimeout(callback, ms));
|
|
310
317
|
const clearTimer = options.clearTimer ?? clearTimeout;
|
|
311
318
|
return {
|
|
312
|
-
queueMessage: ({ message, dispatchMessages }) =>
|
|
319
|
+
queueMessage: ({ message, context, dispatchMessages }) =>
|
|
313
320
|
queueTelegramMediaGroupMessage({
|
|
314
321
|
message,
|
|
322
|
+
context,
|
|
315
323
|
groups,
|
|
316
324
|
debounceMs,
|
|
317
325
|
setTimer,
|
|
@@ -339,8 +347,11 @@ export function createTelegramMediaGroupDispatchRuntime<
|
|
|
339
347
|
handleMessage: async (message, ctx) => {
|
|
340
348
|
const queuedMediaGroup = deps.mediaGroups.queueMessage({
|
|
341
349
|
message,
|
|
342
|
-
|
|
343
|
-
|
|
350
|
+
context: ctx,
|
|
351
|
+
dispatchMessages: (messages, queuedCtx) => {
|
|
352
|
+
if (queuedCtx !== undefined) {
|
|
353
|
+
void deps.dispatchMessages(messages, queuedCtx);
|
|
354
|
+
}
|
|
344
355
|
},
|
|
345
356
|
});
|
|
346
357
|
if (queuedMediaGroup) return;
|
package/lib/outbound-handlers.ts
CHANGED
|
@@ -101,6 +101,11 @@ interface TelegramTopLevelHtmlComment {
|
|
|
101
101
|
end: number;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
interface TelegramTopLevelFenceState {
|
|
105
|
+
marker: "`" | "~";
|
|
106
|
+
length: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
function getMarkdownLineEnd(markdown: string, offset: number): number {
|
|
105
110
|
const newlineIndex = markdown.indexOf("\n", offset);
|
|
106
111
|
return newlineIndex === -1 ? markdown.length : newlineIndex + 1;
|
|
@@ -114,9 +119,29 @@ function getMarkdownLineText(
|
|
|
114
119
|
return markdown.slice(offset, end).replace(/\r?\n$/, "");
|
|
115
120
|
}
|
|
116
121
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
function getTopLevelOpeningFence(
|
|
123
|
+
line: string,
|
|
124
|
+
): TelegramTopLevelFenceState | undefined {
|
|
125
|
+
const match = line.match(/^(?: {0,3})(`{3,}|~{3,})/);
|
|
126
|
+
const sequence = match?.[1];
|
|
127
|
+
if (!sequence) return undefined;
|
|
128
|
+
return {
|
|
129
|
+
marker: sequence[0] as "`" | "~",
|
|
130
|
+
length: sequence.length,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isTopLevelClosingFence(
|
|
135
|
+
line: string,
|
|
136
|
+
fence: TelegramTopLevelFenceState,
|
|
137
|
+
): boolean {
|
|
138
|
+
const match = line.match(/^(?: {0,3})(`{3,}|~{3,})([ \t]*)$/);
|
|
139
|
+
const sequence = match?.[1];
|
|
140
|
+
return (
|
|
141
|
+
!!sequence &&
|
|
142
|
+
sequence[0] === fence.marker &&
|
|
143
|
+
sequence.length >= fence.length
|
|
144
|
+
);
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
function collectTopLevelHtmlComments(markdown: string): {
|
|
@@ -125,18 +150,18 @@ function collectTopLevelHtmlComments(markdown: string): {
|
|
|
125
150
|
} {
|
|
126
151
|
const comments: TelegramTopLevelHtmlComment[] = [];
|
|
127
152
|
let offset = 0;
|
|
128
|
-
let
|
|
153
|
+
let fence: TelegramTopLevelFenceState | undefined;
|
|
129
154
|
while (offset < markdown.length) {
|
|
130
155
|
const lineEnd = getMarkdownLineEnd(markdown, offset);
|
|
131
156
|
const line = getMarkdownLineText(markdown, offset, lineEnd);
|
|
132
|
-
if (
|
|
133
|
-
if (line
|
|
157
|
+
if (fence) {
|
|
158
|
+
if (isTopLevelClosingFence(line, fence)) fence = undefined;
|
|
134
159
|
offset = lineEnd;
|
|
135
160
|
continue;
|
|
136
161
|
}
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
|
|
162
|
+
const nextFence = getTopLevelOpeningFence(line);
|
|
163
|
+
if (nextFence) {
|
|
164
|
+
fence = nextFence;
|
|
140
165
|
offset = lineEnd;
|
|
141
166
|
continue;
|
|
142
167
|
}
|
|
@@ -174,19 +199,19 @@ function findTopLevelOpenOrPartialHtmlCommentIndex(markdown: string): number {
|
|
|
174
199
|
const { openCommentStart } = collectTopLevelHtmlComments(markdown);
|
|
175
200
|
if (openCommentStart !== undefined) return openCommentStart;
|
|
176
201
|
let offset = 0;
|
|
177
|
-
let
|
|
202
|
+
let fence: TelegramTopLevelFenceState | undefined;
|
|
178
203
|
while (offset < markdown.length) {
|
|
179
204
|
const lineEnd = getMarkdownLineEnd(markdown, offset);
|
|
180
205
|
const line = getMarkdownLineText(markdown, offset, lineEnd);
|
|
181
206
|
const isLastLine = lineEnd >= markdown.length;
|
|
182
|
-
if (
|
|
183
|
-
if (line
|
|
207
|
+
if (fence) {
|
|
208
|
+
if (isTopLevelClosingFence(line, fence)) fence = undefined;
|
|
184
209
|
offset = lineEnd;
|
|
185
210
|
continue;
|
|
186
211
|
}
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
|
|
212
|
+
const nextFence = getTopLevelOpeningFence(line);
|
|
213
|
+
if (nextFence) {
|
|
214
|
+
fence = nextFence;
|
|
190
215
|
offset = lineEnd;
|
|
191
216
|
continue;
|
|
192
217
|
}
|
|
@@ -251,9 +276,8 @@ function normalizeMarkdownAfterVoiceExtraction(markdown: string): string {
|
|
|
251
276
|
|
|
252
277
|
export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
253
278
|
const withoutClosedBlocks = replaceTopLevelHtmlComments(markdown, () => "");
|
|
254
|
-
const openBlockIndex =
|
|
255
|
-
withoutClosedBlocks
|
|
256
|
-
);
|
|
279
|
+
const openBlockIndex =
|
|
280
|
+
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
257
281
|
const previewMarkdown =
|
|
258
282
|
openBlockIndex >= 0
|
|
259
283
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
@@ -265,9 +289,8 @@ export function stripTelegramCommentMarkupForDelivery(
|
|
|
265
289
|
markdown: string,
|
|
266
290
|
): string {
|
|
267
291
|
const withoutClosedBlocks = replaceTopLevelHtmlComments(markdown, () => "");
|
|
268
|
-
const openBlockIndex =
|
|
269
|
-
withoutClosedBlocks
|
|
270
|
-
);
|
|
292
|
+
const openBlockIndex =
|
|
293
|
+
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
271
294
|
const deliveryMarkdown =
|
|
272
295
|
openBlockIndex >= 0
|
|
273
296
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
@@ -343,7 +366,9 @@ function getVoiceReplyCompositionStepTimeout(
|
|
|
343
366
|
): number {
|
|
344
367
|
const remaining = getRemainingVoiceReplyTimeout(handlerTimeout, startedAt);
|
|
345
368
|
const stepTimeout = getVoiceReplyConfiguredTimeout(step);
|
|
346
|
-
return stepTimeout === undefined
|
|
369
|
+
return stepTimeout === undefined
|
|
370
|
+
? remaining
|
|
371
|
+
: Math.min(stepTimeout, remaining);
|
|
347
372
|
}
|
|
348
373
|
|
|
349
374
|
function formatVoiceReplyExecutionFailure(
|
|
@@ -704,8 +729,8 @@ function parseButtonsCommentRows(
|
|
|
704
729
|
body: string | undefined,
|
|
705
730
|
): TelegramOutboundButtonAction[][] {
|
|
706
731
|
const attributes = parseButtonsCommentAttributes(head);
|
|
707
|
-
|
|
708
|
-
|
|
732
|
+
if (!attributes.label) return [];
|
|
733
|
+
const prompt = body?.trim() || attributes.label;
|
|
709
734
|
return [[{ text: attributes.label, prompt }]];
|
|
710
735
|
}
|
|
711
736
|
|
package/lib/polling.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface TelegramPollingRuntimeDeps<TContext> {
|
|
|
97
97
|
setPollingController: (controller: AbortController | undefined) => void;
|
|
98
98
|
stopTypingLoop: () => unknown;
|
|
99
99
|
runPollLoop: (ctx: TContext, signal: AbortSignal) => Promise<void>;
|
|
100
|
-
updateStatus: (ctx: TContext) => void;
|
|
100
|
+
updateStatus: (ctx: TContext, message?: string) => void;
|
|
101
101
|
createAbortController?: () => AbortController;
|
|
102
102
|
}
|
|
103
103
|
|
package/lib/prompts.ts
CHANGED
|
@@ -17,7 +17,7 @@ Telegram bridge extension is active.
|
|
|
17
17
|
- Do not assume mentioning a local file path in plain text will send it to Telegram. Use telegram_attach.
|
|
18
18
|
- For Telegram-native outbound actions, use hidden top-level Markdown comments instead of agent-side tool calls: write a normal answer plus correctly formatted column-zero \`telegram_voice\` or \`telegram_button\` blocks outside code, quotes, and lists. The bridge handles delivery after \`agent_end\`, so do not call or register transport/TTS/text-to-OGG tools for these actions.
|
|
19
19
|
- A \`telegram_voice\` block body is the text to synthesize through the extension's configured outbound-handler pipeline. It may be a short companion summary when useful, but no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
|
|
20
|
-
- Button blocks should contain quick reply prompts the user can tap; use independent blocks like \`<!-- telegram_button label="OK"\nPrompt text\n
|
|
20
|
+
- Button blocks should contain quick reply prompts the user can tap; use independent blocks like \`<!-- telegram_button label="OK"\nPrompt text\n-->\`, or \`<!-- telegram_button label="OK" -->\` when the prompt should equal the label. The callback prompt is routed back as a normal Telegram turn.`;
|
|
21
21
|
|
|
22
22
|
export function buildTelegramBridgeSystemPrompt(options: {
|
|
23
23
|
prompt: string;
|
package/lib/queue.ts
CHANGED
|
@@ -785,6 +785,9 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
785
785
|
resetRuntimeState: () => void;
|
|
786
786
|
updateStatus: (ctx: TContext) => void;
|
|
787
787
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
788
|
+
requestDeferredDispatchNextQueuedTelegramTurn: (
|
|
789
|
+
dispatch: (ctx: TContext) => void,
|
|
790
|
+
) => void;
|
|
788
791
|
clearPreview: (chatId: number) => Promise<void>;
|
|
789
792
|
setPreviewPendingText: (text: string) => void;
|
|
790
793
|
finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn>["finalizeMarkdownPreview"];
|
|
@@ -882,7 +885,9 @@ export function createTelegramAgentEndHook<
|
|
|
882
885
|
resetRuntimeState: deps.resetRuntimeState,
|
|
883
886
|
updateStatus: () => deps.updateStatus(ctx),
|
|
884
887
|
dispatchNextQueuedTelegramTurn: () => {
|
|
885
|
-
|
|
888
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
889
|
+
deps.dispatchNextQueuedTelegramTurn,
|
|
890
|
+
);
|
|
886
891
|
},
|
|
887
892
|
clearPreview: deps.clearPreview,
|
|
888
893
|
setPreviewPendingText: deps.setPreviewPendingText,
|
|
@@ -1020,15 +1025,18 @@ export interface TelegramSessionStateApplier<TQueueItem, TModel> {
|
|
|
1020
1025
|
applyShutdownState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
|
|
1021
1026
|
}
|
|
1022
1027
|
|
|
1023
|
-
export interface TelegramSessionStartRuntimeDeps<TModel = unknown> {
|
|
1028
|
+
export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
|
|
1029
|
+
ctx: TContext;
|
|
1024
1030
|
currentModel: TModel | undefined;
|
|
1025
1031
|
loadConfig: () => Promise<void>;
|
|
1026
1032
|
applyState: (state: TelegramSessionStartState<TModel>) => void;
|
|
1033
|
+
bindDeferredDispatchContext?: (ctx: TContext) => void;
|
|
1027
1034
|
prepareTempDir: () => Promise<unknown>;
|
|
1028
1035
|
updateStatus: () => void;
|
|
1029
1036
|
}
|
|
1030
1037
|
|
|
1031
1038
|
export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
|
|
1039
|
+
unbindDeferredDispatchContext?: () => void;
|
|
1032
1040
|
applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
|
|
1033
1041
|
clearPendingMediaGroups: () => void;
|
|
1034
1042
|
clearModelMenuState: () => void;
|
|
@@ -1047,8 +1055,10 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
|
|
|
1047
1055
|
getCurrentModel: (ctx: TContext) => TModel | undefined;
|
|
1048
1056
|
loadConfig: () => Promise<void>;
|
|
1049
1057
|
applySessionStartState: (state: TelegramSessionStartState<TModel>) => void;
|
|
1058
|
+
bindDeferredDispatchContext?: (ctx: TContext) => void;
|
|
1050
1059
|
prepareTempDir: () => Promise<unknown>;
|
|
1051
1060
|
updateStatus: (ctx: TContext) => void;
|
|
1061
|
+
unbindDeferredDispatchContext?: () => void;
|
|
1052
1062
|
applySessionShutdownState: (
|
|
1053
1063
|
state: TelegramSessionShutdownState<TQueueItem>,
|
|
1054
1064
|
) => void;
|
|
@@ -1185,18 +1195,20 @@ export function buildTelegramSessionShutdownState<
|
|
|
1185
1195
|
};
|
|
1186
1196
|
}
|
|
1187
1197
|
|
|
1188
|
-
export async function startTelegramSessionRuntime<TModel = unknown>(
|
|
1189
|
-
deps: TelegramSessionStartRuntimeDeps<TModel>,
|
|
1198
|
+
export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
|
|
1199
|
+
deps: TelegramSessionStartRuntimeDeps<TContext, TModel>,
|
|
1190
1200
|
): Promise<void> {
|
|
1191
1201
|
await deps.loadConfig();
|
|
1192
1202
|
deps.applyState(buildTelegramSessionStartState(deps.currentModel));
|
|
1193
1203
|
await deps.prepareTempDir();
|
|
1204
|
+
deps.bindDeferredDispatchContext?.(deps.ctx);
|
|
1194
1205
|
deps.updateStatus();
|
|
1195
1206
|
}
|
|
1196
1207
|
|
|
1197
1208
|
export async function shutdownTelegramSessionRuntime<TQueueItem>(
|
|
1198
1209
|
deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
|
|
1199
1210
|
): Promise<void> {
|
|
1211
|
+
deps.unbindDeferredDispatchContext?.();
|
|
1200
1212
|
deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
|
|
1201
1213
|
deps.clearPendingMediaGroups();
|
|
1202
1214
|
deps.clearModelMenuState();
|
|
@@ -1235,8 +1247,10 @@ export function createTelegramSessionLifecycleRuntime<
|
|
|
1235
1247
|
getCurrentModel: deps.getCurrentModel,
|
|
1236
1248
|
loadConfig: deps.loadConfig,
|
|
1237
1249
|
applySessionStartState: stateApplier.applyStartState,
|
|
1250
|
+
bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
|
|
1238
1251
|
prepareTempDir: deps.prepareTempDir,
|
|
1239
1252
|
updateStatus: deps.updateStatus,
|
|
1253
|
+
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
1240
1254
|
applySessionShutdownState: stateApplier.applyShutdownState,
|
|
1241
1255
|
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
1242
1256
|
clearModelMenuState: deps.clearModelMenuState,
|
|
@@ -1261,9 +1275,11 @@ export function createTelegramSessionLifecycleHooks<
|
|
|
1261
1275
|
): Promise<void> => {
|
|
1262
1276
|
try {
|
|
1263
1277
|
await startTelegramSessionRuntime({
|
|
1278
|
+
ctx,
|
|
1264
1279
|
currentModel: deps.getCurrentModel(ctx),
|
|
1265
1280
|
loadConfig: deps.loadConfig,
|
|
1266
1281
|
applyState: deps.applySessionStartState,
|
|
1282
|
+
bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
|
|
1267
1283
|
prepareTempDir: deps.prepareTempDir,
|
|
1268
1284
|
updateStatus: () => deps.updateStatus(ctx),
|
|
1269
1285
|
});
|
|
@@ -1275,6 +1291,7 @@ export function createTelegramSessionLifecycleHooks<
|
|
|
1275
1291
|
onSessionShutdown: async (): Promise<void> => {
|
|
1276
1292
|
try {
|
|
1277
1293
|
await shutdownTelegramSessionRuntime<TQueueItem>({
|
|
1294
|
+
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
1278
1295
|
applyState: deps.applySessionShutdownState,
|
|
1279
1296
|
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
1280
1297
|
clearModelMenuState: deps.clearModelMenuState,
|
|
@@ -1490,6 +1507,68 @@ export async function executeTelegramControlItemRuntime<TContext>(
|
|
|
1490
1507
|
}
|
|
1491
1508
|
}
|
|
1492
1509
|
|
|
1510
|
+
// --- Deferred Dispatch Runtime ---
|
|
1511
|
+
|
|
1512
|
+
export interface TelegramDeferredQueueDispatchRuntimeDeps extends TelegramRuntimeEventRecorderPort {
|
|
1513
|
+
delayMs?: number;
|
|
1514
|
+
setTimer?: (
|
|
1515
|
+
callback: () => void,
|
|
1516
|
+
ms: number,
|
|
1517
|
+
) => ReturnType<typeof setTimeout>;
|
|
1518
|
+
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
export interface TelegramDeferredQueueDispatchRuntime<TContext = unknown> {
|
|
1522
|
+
bind: (ctx: TContext) => void;
|
|
1523
|
+
unbind: () => void;
|
|
1524
|
+
isBound: () => boolean;
|
|
1525
|
+
request: (dispatchNextQueuedTelegramTurn: (ctx: TContext) => void) => void;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
|
|
1529
|
+
deps: TelegramDeferredQueueDispatchRuntimeDeps = {},
|
|
1530
|
+
): TelegramDeferredQueueDispatchRuntime<TContext> {
|
|
1531
|
+
let boundContext: TContext | undefined;
|
|
1532
|
+
let generation = 0;
|
|
1533
|
+
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
1534
|
+
const delayMs = deps.delayMs ?? 0;
|
|
1535
|
+
const setTimer =
|
|
1536
|
+
deps.setTimer ??
|
|
1537
|
+
((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
|
|
1538
|
+
setTimeout(callback, ms));
|
|
1539
|
+
const clearTimer =
|
|
1540
|
+
deps.clearTimer ??
|
|
1541
|
+
((timer: ReturnType<typeof setTimeout>): void => clearTimeout(timer));
|
|
1542
|
+
const clearTimers = (): void => {
|
|
1543
|
+
for (const timer of timers) clearTimer(timer);
|
|
1544
|
+
timers.clear();
|
|
1545
|
+
};
|
|
1546
|
+
return {
|
|
1547
|
+
bind: (ctx) => {
|
|
1548
|
+
boundContext = ctx;
|
|
1549
|
+
generation += 1;
|
|
1550
|
+
},
|
|
1551
|
+
unbind: () => {
|
|
1552
|
+
boundContext = undefined;
|
|
1553
|
+
generation += 1;
|
|
1554
|
+
clearTimers();
|
|
1555
|
+
},
|
|
1556
|
+
isBound: () => boundContext !== undefined,
|
|
1557
|
+
request: (dispatchNextQueuedTelegramTurn) => {
|
|
1558
|
+
if (boundContext === undefined) return;
|
|
1559
|
+
const scheduledGeneration = generation;
|
|
1560
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
1561
|
+
timer = setTimer(() => {
|
|
1562
|
+
timers.delete(timer);
|
|
1563
|
+
if (generation !== scheduledGeneration || boundContext === undefined)
|
|
1564
|
+
return;
|
|
1565
|
+
dispatchNextQueuedTelegramTurn(boundContext);
|
|
1566
|
+
}, delayMs);
|
|
1567
|
+
timers.add(timer);
|
|
1568
|
+
},
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1493
1572
|
// --- Dispatch Runtime ---
|
|
1494
1573
|
|
|
1495
1574
|
export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
|
|
@@ -1516,6 +1595,7 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
1516
1595
|
getQueuedItems: () => TelegramQueueItem<TContext>[];
|
|
1517
1596
|
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
1518
1597
|
canDispatch: (ctx: TContext) => boolean;
|
|
1598
|
+
hasDispatchContext?: () => boolean;
|
|
1519
1599
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
1520
1600
|
sendTextReply: TelegramControlRuntimeDeps<TContext>["sendTextReply"];
|
|
1521
1601
|
onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
|
|
@@ -1567,6 +1647,7 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
|
|
|
1567
1647
|
isIdle: deps.isIdle,
|
|
1568
1648
|
hasPendingMessages: deps.hasPendingMessages,
|
|
1569
1649
|
}),
|
|
1650
|
+
hasDispatchContext: deps.hasDispatchContext,
|
|
1570
1651
|
updateStatus: deps.updateStatus,
|
|
1571
1652
|
sendTextReply: deps.sendTextReply,
|
|
1572
1653
|
onPromptDispatchStart: deps.onPromptDispatchStart,
|
|
@@ -1582,6 +1663,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
1582
1663
|
let controlDispatchPending = false;
|
|
1583
1664
|
const controller: TelegramQueueDispatchController<TContext> = {
|
|
1584
1665
|
dispatchNext: (ctx) => {
|
|
1666
|
+
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
1585
1667
|
if (controlDispatchPending) {
|
|
1586
1668
|
deps.updateStatus(ctx);
|
|
1587
1669
|
return;
|
|
@@ -1603,6 +1685,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
1603
1685
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1604
1686
|
onSettled: () => {
|
|
1605
1687
|
controlDispatchPending = false;
|
|
1688
|
+
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
1606
1689
|
deps.updateStatus(ctx);
|
|
1607
1690
|
controller.dispatchNext(ctx);
|
|
1608
1691
|
},
|
package/lib/routing.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
41
41
|
>;
|
|
42
42
|
bridgeRuntime: TelegramBridgeRuntime;
|
|
43
43
|
activeTurnRuntime: Queue.TelegramActiveTurnStore;
|
|
44
|
-
mediaGroupRuntime: Media.TelegramMediaGroupController<TMessage>;
|
|
44
|
+
mediaGroupRuntime: Media.TelegramMediaGroupController<TMessage, TContext>;
|
|
45
45
|
telegramQueueStore: Queue.TelegramQueueStateStore<TContext>;
|
|
46
46
|
queueMutationRuntime: Queue.TelegramQueueMutationController<TContext>;
|
|
47
47
|
modelMenuRuntime: Menu.TelegramModelMenuRuntime<TModel>;
|
package/lib/runtime.ts
CHANGED