@llblab/pi-telegram 0.11.2 → 0.12.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/AGENTS.md +15 -11
- package/BACKLOG.md +0 -10
- package/CHANGELOG.md +18 -0
- package/README.md +18 -14
- package/api/inbound.ts +14 -0
- package/api/keyboard.ts +10 -0
- package/api/outbound.ts +11 -0
- package/api/sections.ts +17 -0
- package/api/updates.ts +11 -0
- package/api/voice.ts +24 -0
- package/docs/README.md +7 -5
- package/docs/architecture.md +160 -225
- package/docs/callback-namespaces.md +3 -3
- package/docs/{inbound-handlers.md → inbound.md} +13 -10
- package/docs/locks.md +3 -3
- package/docs/{outbound-handlers.md → outbound.md} +13 -10
- package/docs/public-api.md +266 -0
- package/docs/{extension-sections.md → sections.md} +31 -27
- package/docs/ui-style.md +165 -0
- package/docs/{external-handlers.md → updates.md} +33 -31
- package/docs/voice.md +17 -14
- package/index.ts +84 -238
- package/lib/bindings.ts +301 -0
- package/lib/commands.ts +114 -1
- package/lib/config.ts +43 -2
- package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
- package/lib/lifecycle.ts +41 -6
- package/lib/menu-model.ts +3 -3
- package/lib/menu-queue.ts +1 -1
- package/lib/menu-settings.ts +21 -10
- package/lib/menu-status.ts +1 -1
- package/lib/menu.ts +1 -1
- package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
- package/lib/polling.ts +4 -3
- package/lib/preview.ts +1 -1
- package/lib/routing.ts +44 -3
- package/lib/{extension-sections.ts → sections.ts} +37 -8
- package/lib/updates.ts +121 -1
- package/lib/voice.ts +33 -14
- package/package.json +11 -1
- package/lib/external-handlers.ts +0 -166
package/lib/bindings.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bridge binding composition
|
|
3
|
+
* Zones: telegram, pi agent, orchestration
|
|
4
|
+
* Owns pi-facing tool, command, and lifecycle hook registration for the entrypoint
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Api from "./api.ts";
|
|
8
|
+
import * as CommandTemplates from "./command-templates.ts";
|
|
9
|
+
import * as Commands from "./commands.ts";
|
|
10
|
+
import * as Config from "./config.ts";
|
|
11
|
+
import * as Keyboard from "./keyboard.ts";
|
|
12
|
+
import * as Lifecycle from "./lifecycle.ts";
|
|
13
|
+
import * as Locks from "./locks.ts";
|
|
14
|
+
import * as Model from "./model.ts";
|
|
15
|
+
import * as OutboundAttachments from "./outbound-attachments.ts";
|
|
16
|
+
import * as OutboundHandlers from "./outbound.ts";
|
|
17
|
+
import * as Pi from "./pi.ts";
|
|
18
|
+
import * as Preview from "./preview.ts";
|
|
19
|
+
import * as Prompts from "./prompts.ts";
|
|
20
|
+
import * as Queue from "./queue.ts";
|
|
21
|
+
import * as Replies from "./replies.ts";
|
|
22
|
+
import * as Runtime from "./runtime.ts";
|
|
23
|
+
import * as Setup from "./setup.ts";
|
|
24
|
+
import * as Status from "./status.ts";
|
|
25
|
+
|
|
26
|
+
type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
|
|
27
|
+
|
|
28
|
+
type TelegramRuntimeEventRecorder = (
|
|
29
|
+
category: string,
|
|
30
|
+
error: unknown,
|
|
31
|
+
details?: Record<string, unknown>,
|
|
32
|
+
) => void;
|
|
33
|
+
|
|
34
|
+
type TelegramBridgeStatusUpdater =
|
|
35
|
+
Status.TelegramStatusRuntime<Pi.ExtensionContext>["updateStatus"];
|
|
36
|
+
|
|
37
|
+
interface TelegramCommandsAndToolsBindingDeps {
|
|
38
|
+
pi: Pi.ExtensionAPI;
|
|
39
|
+
configStore: Config.TelegramConfigStore;
|
|
40
|
+
setup: Setup.TelegramSetupGuard;
|
|
41
|
+
activeTurnRuntime: Queue.TelegramActiveTurnStore<Queue.PendingTelegramTurn>;
|
|
42
|
+
lockedPollingRuntime: Locks.TelegramLockedPollingRuntime<Pi.ExtensionContext>;
|
|
43
|
+
getStatusLines: () => string[];
|
|
44
|
+
updateStatus: TelegramBridgeStatusUpdater;
|
|
45
|
+
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function registerTelegramCommandsAndTools({
|
|
49
|
+
pi,
|
|
50
|
+
configStore,
|
|
51
|
+
setup,
|
|
52
|
+
activeTurnRuntime,
|
|
53
|
+
lockedPollingRuntime,
|
|
54
|
+
getStatusLines,
|
|
55
|
+
updateStatus,
|
|
56
|
+
recordRuntimeEvent,
|
|
57
|
+
}: TelegramCommandsAndToolsBindingDeps): void {
|
|
58
|
+
OutboundAttachments.registerTelegramOutboundAttachmentTool(pi, {
|
|
59
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
60
|
+
recordRuntimeEvent,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
Commands.registerTelegramBridgeCommands(pi, {
|
|
64
|
+
promptForConfig: Setup.createTelegramSetupPromptRuntime({
|
|
65
|
+
getConfig: configStore.get,
|
|
66
|
+
setConfig: configStore.set,
|
|
67
|
+
setupGuard: setup,
|
|
68
|
+
getMe: Api.fetchTelegramBotIdentity,
|
|
69
|
+
persistConfig: configStore.persist,
|
|
70
|
+
startPolling: lockedPollingRuntime.start,
|
|
71
|
+
updateStatus,
|
|
72
|
+
recordRuntimeEvent,
|
|
73
|
+
}),
|
|
74
|
+
getStatusLines,
|
|
75
|
+
reloadConfig: configStore.load,
|
|
76
|
+
hasBotToken: configStore.hasBotToken,
|
|
77
|
+
startPolling: lockedPollingRuntime.start,
|
|
78
|
+
stopPolling: lockedPollingRuntime.stop,
|
|
79
|
+
updateStatus,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface TelegramLifecycleBindingDeps {
|
|
84
|
+
pi: Pi.ExtensionAPI;
|
|
85
|
+
sessionLifecycleRuntime: Pick<
|
|
86
|
+
Lifecycle.TelegramLifecycleRegistrationDeps,
|
|
87
|
+
"onSessionStart" | "onSessionShutdown" | "onModelSelect"
|
|
88
|
+
>;
|
|
89
|
+
queueSessionLifecycle: Pick<
|
|
90
|
+
Lifecycle.TelegramLifecycleRegistrationDeps,
|
|
91
|
+
"onSessionShutdown"
|
|
92
|
+
>;
|
|
93
|
+
configStore: Pick<Config.TelegramConfigStore, "getOutboundHandlers">;
|
|
94
|
+
abort: Runtime.TelegramRuntimeAbortPort;
|
|
95
|
+
typing: Runtime.TelegramRuntimeTypingPort;
|
|
96
|
+
lifecycle: Runtime.TelegramRuntimeLifecyclePort;
|
|
97
|
+
activeTurnRuntime: Queue.TelegramActiveTurnStore<Queue.PendingTelegramTurn>;
|
|
98
|
+
telegramQueueStore: Queue.TelegramQueueStore<Pi.ExtensionContext>;
|
|
99
|
+
modelSwitchController: Model.TelegramModelSwitchController<
|
|
100
|
+
Pi.ExtensionContext,
|
|
101
|
+
Model.ScopedTelegramModel<ActivePiModel>
|
|
102
|
+
>;
|
|
103
|
+
previewRuntime: Preview.TelegramAssistantPreviewRuntime<
|
|
104
|
+
Pi.AgentEndEvent["messages"][number],
|
|
105
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
106
|
+
>;
|
|
107
|
+
promptDispatchRuntime: Runtime.TelegramPromptDispatchRuntime<Pi.ExtensionContext>;
|
|
108
|
+
deferredQueueDispatchRuntime: Queue.TelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>;
|
|
109
|
+
lockOwnershipGuard: Pick<
|
|
110
|
+
Locks.TelegramLockOwnershipGuard<Pi.ExtensionContext>,
|
|
111
|
+
"ownsContext"
|
|
112
|
+
>;
|
|
113
|
+
buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
|
|
114
|
+
callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
|
|
115
|
+
sendChatAction: NonNullable<
|
|
116
|
+
OutboundHandlers.TelegramVoiceReplySenderDeps["sendChatAction"]
|
|
117
|
+
>;
|
|
118
|
+
sendRecordVoiceAction: NonNullable<
|
|
119
|
+
OutboundHandlers.TelegramVoiceReplySenderDeps["sendRecordVoiceAction"]
|
|
120
|
+
>;
|
|
121
|
+
sendMarkdownReply: Queue.TelegramAgentEndHookRuntimeDeps<
|
|
122
|
+
Queue.PendingTelegramTurn,
|
|
123
|
+
Pi.ExtensionContext,
|
|
124
|
+
Pi.AgentEndEvent["messages"][number],
|
|
125
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
126
|
+
>["sendMarkdownReply"];
|
|
127
|
+
sendTextReply: Queue.TelegramAgentEndHookRuntimeDeps<
|
|
128
|
+
Queue.PendingTelegramTurn,
|
|
129
|
+
Pi.ExtensionContext,
|
|
130
|
+
Pi.AgentEndEvent["messages"][number],
|
|
131
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
132
|
+
>["sendTextReply"] &
|
|
133
|
+
NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendTextReply"]>;
|
|
134
|
+
dispatchNextQueuedTelegramTurn: (ctx: Pi.ExtensionContext) => void;
|
|
135
|
+
answerGuestQuery: NonNullable<
|
|
136
|
+
Queue.TelegramAgentEndHookRuntimeDeps<
|
|
137
|
+
Queue.PendingTelegramTurn,
|
|
138
|
+
Pi.ExtensionContext,
|
|
139
|
+
Pi.AgentEndEvent["messages"][number],
|
|
140
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
141
|
+
>["answerGuestQuery"]
|
|
142
|
+
>;
|
|
143
|
+
sendGuestReply: NonNullable<
|
|
144
|
+
Queue.TelegramAgentEndHookRuntimeDeps<
|
|
145
|
+
Queue.PendingTelegramTurn,
|
|
146
|
+
Pi.ExtensionContext,
|
|
147
|
+
Pi.AgentEndEvent["messages"][number],
|
|
148
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
149
|
+
>["sendGuestReply"]
|
|
150
|
+
>;
|
|
151
|
+
finalizeMarkdownPreview: Queue.TelegramAgentEndHookRuntimeDeps<
|
|
152
|
+
Queue.PendingTelegramTurn,
|
|
153
|
+
Pi.ExtensionContext,
|
|
154
|
+
Pi.AgentEndEvent["messages"][number],
|
|
155
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
156
|
+
>["finalizeMarkdownPreview"];
|
|
157
|
+
proactivePushChatIdGetter: () => number | undefined;
|
|
158
|
+
isProactivePushEnabled: () => boolean;
|
|
159
|
+
updateStatus: TelegramBridgeStatusUpdater;
|
|
160
|
+
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function registerTelegramLifecycleRuntimeHooks({
|
|
164
|
+
pi,
|
|
165
|
+
sessionLifecycleRuntime,
|
|
166
|
+
queueSessionLifecycle,
|
|
167
|
+
configStore,
|
|
168
|
+
abort,
|
|
169
|
+
typing,
|
|
170
|
+
lifecycle,
|
|
171
|
+
activeTurnRuntime,
|
|
172
|
+
telegramQueueStore,
|
|
173
|
+
modelSwitchController,
|
|
174
|
+
previewRuntime,
|
|
175
|
+
promptDispatchRuntime,
|
|
176
|
+
deferredQueueDispatchRuntime,
|
|
177
|
+
lockOwnershipGuard,
|
|
178
|
+
buttonActionStore,
|
|
179
|
+
callMultipart,
|
|
180
|
+
sendChatAction,
|
|
181
|
+
sendRecordVoiceAction,
|
|
182
|
+
sendMarkdownReply,
|
|
183
|
+
sendTextReply,
|
|
184
|
+
dispatchNextQueuedTelegramTurn,
|
|
185
|
+
answerGuestQuery,
|
|
186
|
+
sendGuestReply,
|
|
187
|
+
finalizeMarkdownPreview,
|
|
188
|
+
proactivePushChatIdGetter,
|
|
189
|
+
isProactivePushEnabled,
|
|
190
|
+
updateStatus,
|
|
191
|
+
recordRuntimeEvent,
|
|
192
|
+
}: TelegramLifecycleBindingDeps): void {
|
|
193
|
+
const agentEndResetter = Runtime.createTelegramAgentEndResetter({
|
|
194
|
+
abort,
|
|
195
|
+
typing,
|
|
196
|
+
clearActiveTurn: activeTurnRuntime.clear,
|
|
197
|
+
resetToolExecutions: lifecycle.resetActiveToolExecutions,
|
|
198
|
+
clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
199
|
+
clearDispatchPending: lifecycle.clearDispatchPending,
|
|
200
|
+
});
|
|
201
|
+
const queuedAttachmentSender =
|
|
202
|
+
OutboundAttachments.createTelegramQueuedOutboundAttachmentSender({
|
|
203
|
+
sendMultipart: callMultipart,
|
|
204
|
+
sendTextReply,
|
|
205
|
+
recordRuntimeEvent,
|
|
206
|
+
});
|
|
207
|
+
const outboundReplyPlanner =
|
|
208
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore);
|
|
209
|
+
const outboundReplyArtifactSender =
|
|
210
|
+
OutboundHandlers.createTelegramOutboundReplyArtifactSender({
|
|
211
|
+
execCommand: CommandTemplates.execCommandTemplate,
|
|
212
|
+
sendMultipart: callMultipart,
|
|
213
|
+
sendTextReply,
|
|
214
|
+
sendChatAction,
|
|
215
|
+
sendRecordVoiceAction,
|
|
216
|
+
getHandlers: configStore.getOutboundHandlers,
|
|
217
|
+
recordRuntimeEvent,
|
|
218
|
+
});
|
|
219
|
+
const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
|
|
220
|
+
Queue.PendingTelegramTurn,
|
|
221
|
+
Pi.ExtensionContext,
|
|
222
|
+
unknown,
|
|
223
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
224
|
+
>({
|
|
225
|
+
setAbortHandler: Runtime.createTelegramContextAbortHandlerSetter(abort),
|
|
226
|
+
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
227
|
+
hasPendingDispatch: lifecycle.hasDispatchPending,
|
|
228
|
+
hasActiveTurn: activeTurnRuntime.has,
|
|
229
|
+
resetToolExecutions: lifecycle.resetActiveToolExecutions,
|
|
230
|
+
resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
231
|
+
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
232
|
+
clearDispatchPending: lifecycle.clearDispatchPending,
|
|
233
|
+
setActiveTurn: activeTurnRuntime.set,
|
|
234
|
+
createPreviewState: previewRuntime.resetState,
|
|
235
|
+
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
236
|
+
updateStatus,
|
|
237
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
238
|
+
extractAssistant: Replies.extractLatestAssistantMessageText,
|
|
239
|
+
getPreserveQueuedTurnsAsHistory:
|
|
240
|
+
lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
241
|
+
resetRuntimeState: agentEndResetter,
|
|
242
|
+
dispatchNextQueuedTelegramTurn,
|
|
243
|
+
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
244
|
+
deferredQueueDispatchRuntime.request,
|
|
245
|
+
clearPreview: previewRuntime.clear,
|
|
246
|
+
setPreviewPendingText: previewRuntime.setPendingText,
|
|
247
|
+
finalizeMarkdownPreview,
|
|
248
|
+
sendMarkdownReply,
|
|
249
|
+
sendTextReply,
|
|
250
|
+
sendQueuedAttachments: queuedAttachmentSender,
|
|
251
|
+
answerGuestQuery,
|
|
252
|
+
sendGuestReply,
|
|
253
|
+
planOutboundReply: outboundReplyPlanner,
|
|
254
|
+
sendOutboundReplyArtifacts: outboundReplyArtifactSender,
|
|
255
|
+
isCurrentOwner: lockOwnershipGuard.ownsContext,
|
|
256
|
+
getDefaultChatId: proactivePushChatIdGetter,
|
|
257
|
+
isProactivePushEnabled,
|
|
258
|
+
recordRuntimeEvent,
|
|
259
|
+
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
260
|
+
setActiveToolExecutions: lifecycle.setActiveToolExecutions,
|
|
261
|
+
triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
Lifecycle.setResetTransportReplyDedup(Replies.resetTransportReplyDedup);
|
|
265
|
+
const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
|
|
266
|
+
agentLifecycleHooks.onAgentStart,
|
|
267
|
+
);
|
|
268
|
+
const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
|
|
269
|
+
setCompactionInProgress: lifecycle.setCompactionInProgress,
|
|
270
|
+
updateStatus,
|
|
271
|
+
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
272
|
+
stopTypingLoop: typing.stop,
|
|
273
|
+
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
274
|
+
deferredQueueDispatchRuntime.request,
|
|
275
|
+
dispatchNextQueuedTelegramTurn,
|
|
276
|
+
recordRuntimeEvent,
|
|
277
|
+
});
|
|
278
|
+
const messageActivityTypingHooks =
|
|
279
|
+
Lifecycle.createTelegramMessageActivityTypingHooks({
|
|
280
|
+
hasActiveTurn: activeTurnRuntime.has,
|
|
281
|
+
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
282
|
+
onMessageStart: previewRuntime.onMessageStart,
|
|
283
|
+
onMessageUpdate: previewRuntime.onMessageUpdate,
|
|
284
|
+
});
|
|
285
|
+
Lifecycle.registerTelegramLifecycleHooks(pi, {
|
|
286
|
+
...sessionLifecycleRuntime,
|
|
287
|
+
...agentLifecycleHooks,
|
|
288
|
+
async onSessionShutdown(event, ctx) {
|
|
289
|
+
compactionObserver.onSessionShutdown();
|
|
290
|
+
await queueSessionLifecycle.onSessionShutdown(event, ctx);
|
|
291
|
+
},
|
|
292
|
+
onSessionBeforeCompact: compactionObserver.onSessionBeforeCompact,
|
|
293
|
+
onSessionCompact: compactionObserver.onSessionCompact,
|
|
294
|
+
onAgentStart: agentStartWithDedupReset,
|
|
295
|
+
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
|
|
296
|
+
isProactivePushEnabled,
|
|
297
|
+
isCurrentOwner: lockOwnershipGuard.ownsContext,
|
|
298
|
+
}),
|
|
299
|
+
...messageActivityTypingHooks,
|
|
300
|
+
});
|
|
301
|
+
}
|
package/lib/commands.ts
CHANGED
|
@@ -307,6 +307,10 @@ export interface TelegramRuntimeEventRecorderPort {
|
|
|
307
307
|
) => void;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
export interface TelegramCompactConfirmationReplyMarkup {
|
|
311
|
+
inline_keyboard: { text: string; callback_data: string }[][];
|
|
312
|
+
}
|
|
313
|
+
|
|
310
314
|
export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort {
|
|
311
315
|
isIdle: () => boolean;
|
|
312
316
|
hasPendingMessages: () => boolean;
|
|
@@ -327,6 +331,42 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
|
|
|
327
331
|
onError: (error: unknown) => void;
|
|
328
332
|
}) => void;
|
|
329
333
|
sendTextReply: (text: string) => Promise<void>;
|
|
334
|
+
suppressStartNotice?: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface TelegramCompactConfirmationDeps {
|
|
338
|
+
sendInteractiveMessage: (
|
|
339
|
+
chatId: number,
|
|
340
|
+
text: string,
|
|
341
|
+
mode: "html" | "plain",
|
|
342
|
+
replyMarkup: TelegramCompactConfirmationReplyMarkup,
|
|
343
|
+
) => Promise<number | undefined>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface TelegramCompactConfirmationCallbackQuery {
|
|
347
|
+
id: string;
|
|
348
|
+
data?: string;
|
|
349
|
+
message?: { chat?: { id?: number }; message_id?: number };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface TelegramCompactConfirmationCallbackDeps<TContext> {
|
|
353
|
+
ctx: TContext;
|
|
354
|
+
answerCallbackQuery: (
|
|
355
|
+
callbackQueryId: string,
|
|
356
|
+
text?: string,
|
|
357
|
+
) => Promise<void>;
|
|
358
|
+
editInteractiveMessage: (
|
|
359
|
+
chatId: number,
|
|
360
|
+
messageId: number,
|
|
361
|
+
text: string,
|
|
362
|
+
mode: "html" | "plain",
|
|
363
|
+
replyMarkup: TelegramCompactConfirmationReplyMarkup,
|
|
364
|
+
) => Promise<void>;
|
|
365
|
+
runCompact: (
|
|
366
|
+
ctx: TContext,
|
|
367
|
+
chatId: number,
|
|
368
|
+
replyToMessageId: number,
|
|
369
|
+
) => Promise<void>;
|
|
330
370
|
}
|
|
331
371
|
|
|
332
372
|
export type TelegramControlCommandType =
|
|
@@ -580,6 +620,7 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
580
620
|
getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
|
|
581
621
|
persistConfig: () => Promise<void>;
|
|
582
622
|
sendTextReply: (message: TMessage, text: string) => Promise<void>;
|
|
623
|
+
sendInteractiveMessage?: TelegramCompactConfirmationDeps["sendInteractiveMessage"];
|
|
583
624
|
}
|
|
584
625
|
|
|
585
626
|
export const TELEGRAM_APP_MENU_INTRO_HTML = [
|
|
@@ -784,6 +825,69 @@ function dispatchNextQueuedTelegramTurnAfterCompact(
|
|
|
784
825
|
deps.dispatchNextQueuedTelegramTurn();
|
|
785
826
|
}
|
|
786
827
|
|
|
828
|
+
export function buildTelegramCompactConfirmationReplyMarkup(): TelegramCompactConfirmationReplyMarkup {
|
|
829
|
+
return {
|
|
830
|
+
inline_keyboard: [
|
|
831
|
+
[
|
|
832
|
+
{ text: "🗜 Yes, compact", callback_data: "compact:confirm" },
|
|
833
|
+
{ text: "❌ No", callback_data: "compact:cancel" },
|
|
834
|
+
],
|
|
835
|
+
],
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export function getTelegramCompactConfirmationHtml(): string {
|
|
840
|
+
return "<b>Compact session?</b>";
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export async function openTelegramCompactConfirmation(
|
|
844
|
+
chatId: number,
|
|
845
|
+
deps: TelegramCompactConfirmationDeps,
|
|
846
|
+
): Promise<void> {
|
|
847
|
+
await deps.sendInteractiveMessage(
|
|
848
|
+
chatId,
|
|
849
|
+
getTelegramCompactConfirmationHtml(),
|
|
850
|
+
"html",
|
|
851
|
+
buildTelegramCompactConfirmationReplyMarkup(),
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export async function handleTelegramCompactConfirmationCallback<TContext>(
|
|
856
|
+
query: TelegramCompactConfirmationCallbackQuery,
|
|
857
|
+
deps: TelegramCompactConfirmationCallbackDeps<TContext>,
|
|
858
|
+
): Promise<boolean> {
|
|
859
|
+
if (query.data !== "compact:confirm" && query.data !== "compact:cancel") {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
const chatId = query.message?.chat?.id;
|
|
863
|
+
const messageId = query.message?.message_id;
|
|
864
|
+
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
865
|
+
await deps.answerCallbackQuery(query.id, "Interactive message expired.");
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
if (query.data === "compact:cancel") {
|
|
869
|
+
await deps.editInteractiveMessage(
|
|
870
|
+
chatId,
|
|
871
|
+
messageId,
|
|
872
|
+
"Compaction cancelled.",
|
|
873
|
+
"plain",
|
|
874
|
+
{ inline_keyboard: [] },
|
|
875
|
+
);
|
|
876
|
+
await deps.answerCallbackQuery(query.id);
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
await deps.editInteractiveMessage(
|
|
880
|
+
chatId,
|
|
881
|
+
messageId,
|
|
882
|
+
"Compaction started.",
|
|
883
|
+
"plain",
|
|
884
|
+
{ inline_keyboard: [] },
|
|
885
|
+
);
|
|
886
|
+
await deps.answerCallbackQuery(query.id);
|
|
887
|
+
await deps.runCompact(deps.ctx, chatId, messageId);
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
|
|
787
891
|
export async function handleTelegramCompactCommand(
|
|
788
892
|
deps: TelegramCompactCommandDeps,
|
|
789
893
|
): Promise<void> {
|
|
@@ -834,7 +938,9 @@ export async function handleTelegramCompactCommand(
|
|
|
834
938
|
await deps.sendTextReply(`Compaction failed: ${errorMessage}`);
|
|
835
939
|
return;
|
|
836
940
|
}
|
|
837
|
-
|
|
941
|
+
if (!deps.suppressStartNotice) {
|
|
942
|
+
await deps.sendTextReply("Compaction started.");
|
|
943
|
+
}
|
|
838
944
|
if (compactionStillInProgress) deps.startTypingLoop?.();
|
|
839
945
|
}
|
|
840
946
|
|
|
@@ -978,6 +1084,7 @@ export function createTelegramCommandHandlerTargetRuntime<
|
|
|
978
1084
|
stopTypingLoop: deps.stopTypingLoop,
|
|
979
1085
|
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
980
1086
|
compact: deps.compact,
|
|
1087
|
+
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
981
1088
|
enqueueControlItem: commandTargetRuntime.enqueueControlItem,
|
|
982
1089
|
showStatus: commandTargetRuntime.showStatus,
|
|
983
1090
|
openModelMenu: commandTargetRuntime.openModelMenu,
|
|
@@ -1110,6 +1217,12 @@ async function handleTelegramCommandRuntime<
|
|
|
1110
1217
|
await deps.openQueueMenu(nextMessage, commandCtx);
|
|
1111
1218
|
},
|
|
1112
1219
|
handleCompact: async (nextMessage, commandCtx) => {
|
|
1220
|
+
if (deps.sendInteractiveMessage) {
|
|
1221
|
+
await openTelegramCompactConfirmation(nextMessage.chat.id, {
|
|
1222
|
+
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
1223
|
+
});
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1113
1226
|
await handleTelegramCompactCommand({
|
|
1114
1227
|
isIdle: () => deps.isIdle(commandCtx),
|
|
1115
1228
|
hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
|
package/lib/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram bridge config and pairing helpers
|
|
3
3
|
* Zones: telegram config, pairing, filesystem
|
|
4
|
-
* Owns persisted bot/session pairing state, local config storage, authorization policy, and first-user pairing side effects
|
|
4
|
+
* Owns persisted bot/session pairing state, local config storage, live config controls, authorization policy, and first-user pairing side effects
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
@@ -9,7 +9,7 @@ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join, resolve } from "node:path";
|
|
11
11
|
|
|
12
|
-
import type { TelegramInboundHandlerConfig } from "./inbound
|
|
12
|
+
import type { TelegramInboundHandlerConfig } from "./inbound.ts";
|
|
13
13
|
import type { CommandTemplateObjectConfig } from "./command-templates.ts";
|
|
14
14
|
|
|
15
15
|
const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
|
|
@@ -110,6 +110,22 @@ export function updateTelegramVoiceConfig(
|
|
|
110
110
|
return true;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export function bindGlobalTelegramConfigRuntime(
|
|
114
|
+
configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
|
|
115
|
+
): void {
|
|
116
|
+
setGlobalTelegramConfigRuntime({
|
|
117
|
+
updateVoiceConfig(voice) {
|
|
118
|
+
const current = configStore.get();
|
|
119
|
+
const next = {
|
|
120
|
+
...current,
|
|
121
|
+
voice: { ...(current.voice ?? {}), ...voice },
|
|
122
|
+
};
|
|
123
|
+
configStore.set(next);
|
|
124
|
+
void configStore.persist(next);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
export async function readTelegramConfig(
|
|
114
130
|
configPath: string,
|
|
115
131
|
): Promise<TelegramConfig> {
|
|
@@ -266,6 +282,16 @@ export function createTelegramTimeInjectionModeSetter(
|
|
|
266
282
|
): (injectionMode: TelegramTimeMode) => Promise<void> {
|
|
267
283
|
return async (injectionMode) => {
|
|
268
284
|
const current = configStore.get();
|
|
285
|
+
if (injectionMode === "hidden") {
|
|
286
|
+
const { injectionMode: _injectionMode, ...remainingTime } =
|
|
287
|
+
current.time ?? {};
|
|
288
|
+
const next = { ...current };
|
|
289
|
+
if (Object.keys(remainingTime).length > 0) next.time = remainingTime;
|
|
290
|
+
else delete next.time;
|
|
291
|
+
configStore.set(next);
|
|
292
|
+
await configStore.persist(next);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
269
295
|
const next = {
|
|
270
296
|
...current,
|
|
271
297
|
time: { ...(current.time ?? {}), injectionMode },
|
|
@@ -282,6 +308,21 @@ export function createTelegramProactivePushChatIdGetter(deps: {
|
|
|
282
308
|
return () => deps.getActiveTurnChatId() ?? deps.getAllowedUserId();
|
|
283
309
|
}
|
|
284
310
|
|
|
311
|
+
export function createTelegramConfigControls(
|
|
312
|
+
configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
|
|
313
|
+
) {
|
|
314
|
+
return {
|
|
315
|
+
isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
|
|
316
|
+
setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
|
|
317
|
+
getVoiceReplyMode: createTelegramVoiceReplyModeGetter(configStore),
|
|
318
|
+
isVoiceReplyModeConfigured:
|
|
319
|
+
createTelegramVoiceReplyModeConfiguredChecker(configStore),
|
|
320
|
+
setVoiceReplyMode: createTelegramVoiceReplyModeSetter(configStore),
|
|
321
|
+
getTimeInjectionMode: createTelegramTimeInjectionModeGetter(configStore),
|
|
322
|
+
setTimeInjectionMode: createTelegramTimeInjectionModeSetter(configStore),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
285
326
|
export type TelegramAuthorizationState =
|
|
286
327
|
| { kind: "pair"; userId: number }
|
|
287
328
|
| { kind: "allow" }
|
|
@@ -216,9 +216,7 @@ function matchesWildcard(pattern: string, value: string | undefined): boolean {
|
|
|
216
216
|
return new RegExp(`^${escaped}$`).test(normalizedValue);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function handlerHasSelectors(
|
|
220
|
-
handler: TelegramInboundHandlerConfig,
|
|
221
|
-
): boolean {
|
|
219
|
+
function handlerHasSelectors(handler: TelegramInboundHandlerConfig): boolean {
|
|
222
220
|
return (
|
|
223
221
|
normalizeStringList(handler.match).length > 0 ||
|
|
224
222
|
normalizeStringList(handler.mime).length > 0 ||
|
|
@@ -642,7 +640,10 @@ async function readBuiltInTelegramTextAttachment(
|
|
|
642
640
|
if (!isTelegramTextMimeType(file.mimeType)) return undefined;
|
|
643
641
|
const content = await readFile(file.path, "utf8");
|
|
644
642
|
const normalized = content.trim();
|
|
645
|
-
if (
|
|
643
|
+
if (
|
|
644
|
+
!normalized ||
|
|
645
|
+
Buffer.byteLength(normalized, "utf8") > BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES
|
|
646
|
+
) {
|
|
646
647
|
return undefined;
|
|
647
648
|
}
|
|
648
649
|
const name = file.fileName || basename(file.path);
|
package/lib/lifecycle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram lifecycle hook registration helpers
|
|
3
3
|
* Zones: pi agent lifecycle, telegram session
|
|
4
|
-
*
|
|
4
|
+
* Binds prepared Telegram lifecycle runtimes to pi extension lifecycle events
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
@@ -107,16 +107,15 @@ type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
|
|
|
107
107
|
export interface TelegramCompactionObserverRuntimeDeps<TContext> {
|
|
108
108
|
setCompactionInProgress: (inProgress: boolean) => void;
|
|
109
109
|
updateStatus: (ctx: TContext) => void;
|
|
110
|
+
startTypingLoop?: (ctx: TContext) => void;
|
|
111
|
+
stopTypingLoop?: () => void;
|
|
110
112
|
requestDeferredDispatchNextQueuedTelegramTurn: (
|
|
111
113
|
dispatch: (ctx: TContext) => void,
|
|
112
114
|
) => void;
|
|
113
115
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
114
116
|
recordRuntimeEvent?: (category: string, error: unknown) => void;
|
|
115
117
|
timeoutMs?: number;
|
|
116
|
-
setTimer?: (
|
|
117
|
-
callback: () => void,
|
|
118
|
-
ms: number,
|
|
119
|
-
) => TelegramLifecycleTimer;
|
|
118
|
+
setTimer?: (callback: () => void, ms: number) => TelegramLifecycleTimer;
|
|
120
119
|
clearTimer?: (timer: TelegramLifecycleTimer) => void;
|
|
121
120
|
}
|
|
122
121
|
|
|
@@ -149,11 +148,13 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
149
148
|
return {
|
|
150
149
|
onSessionBeforeCompact: (_event, ctx) => {
|
|
151
150
|
deps.setCompactionInProgress(true);
|
|
151
|
+
deps.startTypingLoop?.(ctx);
|
|
152
152
|
deps.updateStatus(ctx);
|
|
153
153
|
clearFallbackTimer();
|
|
154
154
|
fallbackTimer = setTimer(() => {
|
|
155
155
|
fallbackTimer = undefined;
|
|
156
156
|
deps.setCompactionInProgress(false);
|
|
157
|
+
deps.stopTypingLoop?.();
|
|
157
158
|
deps.updateStatus(ctx);
|
|
158
159
|
deps.recordRuntimeEvent?.(
|
|
159
160
|
"compact",
|
|
@@ -165,10 +166,44 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
165
166
|
onSessionCompact: (_event, ctx) => {
|
|
166
167
|
clearFallbackTimer();
|
|
167
168
|
deps.setCompactionInProgress(false);
|
|
169
|
+
deps.stopTypingLoop?.();
|
|
168
170
|
deps.updateStatus(ctx);
|
|
169
171
|
requestDispatch();
|
|
170
172
|
},
|
|
171
|
-
onSessionShutdown:
|
|
173
|
+
onSessionShutdown: () => {
|
|
174
|
+
clearFallbackTimer();
|
|
175
|
+
deps.stopTypingLoop?.();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface TelegramMessageActivityTypingDeps<TContext> {
|
|
181
|
+
hasActiveTurn: () => boolean;
|
|
182
|
+
startTypingLoop: (ctx: TContext) => void;
|
|
183
|
+
onMessageStart: TelegramLifecycleRegistrationDeps["onMessageStart"];
|
|
184
|
+
onMessageUpdate: TelegramLifecycleRegistrationDeps["onMessageUpdate"];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createTelegramMessageActivityTypingHooks<
|
|
188
|
+
TContext extends ExtensionContext,
|
|
189
|
+
>(
|
|
190
|
+
deps: TelegramMessageActivityTypingDeps<TContext>,
|
|
191
|
+
): Pick<
|
|
192
|
+
TelegramLifecycleRegistrationDeps,
|
|
193
|
+
"onMessageStart" | "onMessageUpdate"
|
|
194
|
+
> {
|
|
195
|
+
const ensureTyping = (ctx: TContext): void => {
|
|
196
|
+
if (deps.hasActiveTurn()) deps.startTypingLoop(ctx);
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
onMessageStart: async (event, ctx) => {
|
|
200
|
+
ensureTyping(ctx as TContext);
|
|
201
|
+
await deps.onMessageStart(event, ctx);
|
|
202
|
+
},
|
|
203
|
+
onMessageUpdate: async (event, ctx) => {
|
|
204
|
+
ensureTyping(ctx as TContext);
|
|
205
|
+
await deps.onMessageUpdate(event, ctx);
|
|
206
|
+
},
|
|
172
207
|
};
|
|
173
208
|
}
|
|
174
209
|
|
package/lib/menu-model.ts
CHANGED
|
@@ -983,7 +983,7 @@ export function buildModelMenuReplyMarkup(
|
|
|
983
983
|
callback_data: "model:scope:scoped",
|
|
984
984
|
},
|
|
985
985
|
{
|
|
986
|
-
text: state.scope === "all" ? "
|
|
986
|
+
text: state.scope === "all" ? "🟣 All" : "⚫️ All",
|
|
987
987
|
callback_data: "model:scope:all",
|
|
988
988
|
},
|
|
989
989
|
]);
|
|
@@ -1050,7 +1050,7 @@ export function buildModelDetailMenuReplyMarkup(
|
|
|
1050
1050
|
callback_data: "model:scope-enable",
|
|
1051
1051
|
},
|
|
1052
1052
|
{
|
|
1053
|
-
text: scoped ? "⚫️ All" : "
|
|
1053
|
+
text: scoped ? "⚫️ All" : "🟣 All",
|
|
1054
1054
|
callback_data: "model:scope-disable",
|
|
1055
1055
|
},
|
|
1056
1056
|
],
|
|
@@ -1091,7 +1091,7 @@ export function buildModelPageMenuReplyMarkup(
|
|
|
1091
1091
|
return {
|
|
1092
1092
|
text:
|
|
1093
1093
|
pageIndex === menuPage.page
|
|
1094
|
-
?
|
|
1094
|
+
? `🟣 ${pageIndex + 1}`
|
|
1095
1095
|
: String(pageIndex + 1),
|
|
1096
1096
|
callback_data: `model:page:${pageIndex}`,
|
|
1097
1097
|
};
|
package/lib/menu-queue.ts
CHANGED
|
@@ -162,7 +162,7 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
|
|
|
162
162
|
callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
|
-
text: isPriority ? "⚫️ Normal" : "
|
|
165
|
+
text: isPriority ? "⚫️ Normal" : "🟣 Normal",
|
|
166
166
|
callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
|
|
167
167
|
},
|
|
168
168
|
],
|