@llblab/pi-telegram 0.2.9 → 0.3.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 +40 -26
- package/docs/architecture.md +62 -35
- package/index.ts +388 -1936
- package/lib/api.ts +647 -76
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +721 -0
- package/lib/config.ts +157 -0
- package/lib/media.ts +211 -36
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +264 -18
- package/lib/preview.ts +451 -29
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +575 -281
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +207 -17
- package/lib/updates.ts +392 -99
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -23
- package/lib/model-switch.ts +0 -62
- package/tests/api.test.ts +0 -89
- package/tests/attachments.test.ts +0 -132
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -77
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -129
- package/tests/preview.test.ts +0 -441
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -475
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -132
- package/tests/updates.test.ts +0 -357
package/index.ts
CHANGED
|
@@ -3,1976 +3,428 @@
|
|
|
3
3
|
* Keeps the runtime wiring in one place while delegating reusable domain logic to /lib modules
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
extractFirstTelegramMessageText,
|
|
28
|
-
extractTelegramMessagesText,
|
|
29
|
-
guessMediaType,
|
|
30
|
-
} from "./lib/media.ts";
|
|
31
|
-
import {
|
|
32
|
-
buildTelegramModelMenuState,
|
|
33
|
-
getCanonicalModelId,
|
|
34
|
-
handleTelegramMenuCallbackEntry,
|
|
35
|
-
handleTelegramModelMenuCallbackAction,
|
|
36
|
-
handleTelegramStatusMenuCallbackAction,
|
|
37
|
-
handleTelegramThinkingMenuCallbackAction,
|
|
38
|
-
sendTelegramModelMenuMessage,
|
|
39
|
-
sendTelegramStatusMessage,
|
|
40
|
-
updateTelegramModelMenuMessage,
|
|
41
|
-
updateTelegramStatusMessage,
|
|
42
|
-
updateTelegramThinkingMenuMessage,
|
|
43
|
-
type ScopedTelegramModel,
|
|
44
|
-
type TelegramModelMenuState,
|
|
45
|
-
type TelegramReplyMarkup,
|
|
46
|
-
type ThinkingLevel,
|
|
47
|
-
} from "./lib/menu.ts";
|
|
48
|
-
import {
|
|
49
|
-
buildTelegramModelSwitchContinuationText,
|
|
50
|
-
canRestartTelegramTurnForModelSwitch,
|
|
51
|
-
restartTelegramModelSwitchContinuation,
|
|
52
|
-
shouldTriggerPendingTelegramModelSwitchAbort,
|
|
53
|
-
} from "./lib/model-switch.ts";
|
|
54
|
-
import { runTelegramPollLoop } from "./lib/polling.ts";
|
|
55
|
-
import {
|
|
56
|
-
buildTelegramAgentEndPlan,
|
|
57
|
-
buildTelegramAgentStartPlan,
|
|
58
|
-
buildTelegramSessionShutdownState,
|
|
59
|
-
buildTelegramSessionStartState,
|
|
60
|
-
canDispatchTelegramTurnState,
|
|
61
|
-
clearTelegramQueuePromptPriority,
|
|
62
|
-
compareTelegramQueueItems,
|
|
63
|
-
consumeDispatchedTelegramPrompt,
|
|
64
|
-
executeTelegramControlItemRuntime,
|
|
65
|
-
executeTelegramQueueDispatchPlan,
|
|
66
|
-
formatQueuedTelegramItemsStatus,
|
|
67
|
-
getNextTelegramToolExecutionCount,
|
|
68
|
-
partitionTelegramQueueItemsForHistory,
|
|
69
|
-
planNextTelegramQueueAction,
|
|
70
|
-
prioritizeTelegramQueuePrompt,
|
|
71
|
-
removeTelegramQueueItemsByMessageIds,
|
|
72
|
-
shouldDispatchAfterTelegramAgentEnd,
|
|
73
|
-
shouldStartTelegramPolling,
|
|
74
|
-
type PendingTelegramControlItem,
|
|
75
|
-
type PendingTelegramTurn,
|
|
76
|
-
type TelegramQueueItem,
|
|
77
|
-
} from "./lib/queue.ts";
|
|
78
|
-
import {
|
|
79
|
-
registerTelegramAttachmentTool,
|
|
80
|
-
registerTelegramCommands,
|
|
81
|
-
registerTelegramLifecycleHooks,
|
|
82
|
-
} from "./lib/registration.ts";
|
|
83
|
-
import {
|
|
84
|
-
MAX_MESSAGE_LENGTH,
|
|
85
|
-
renderMarkdownPreviewText,
|
|
86
|
-
renderTelegramMessage,
|
|
87
|
-
type TelegramRenderMode,
|
|
88
|
-
} from "./lib/rendering.ts";
|
|
89
|
-
import {
|
|
90
|
-
clearTelegramPreview,
|
|
91
|
-
finalizeTelegramMarkdownPreview,
|
|
92
|
-
finalizeTelegramPreview,
|
|
93
|
-
flushTelegramPreview,
|
|
94
|
-
type TelegramPreviewRuntimeState,
|
|
95
|
-
} from "./lib/preview.ts";
|
|
96
|
-
import {
|
|
97
|
-
buildTelegramReplyTransport,
|
|
98
|
-
sendTelegramMarkdownReply,
|
|
99
|
-
sendTelegramPlainReply,
|
|
100
|
-
} from "./lib/replies.ts";
|
|
101
|
-
import {
|
|
102
|
-
getTelegramBotTokenInputDefault,
|
|
103
|
-
getTelegramBotTokenPromptSpec,
|
|
104
|
-
} from "./lib/setup.ts";
|
|
105
|
-
import { buildStatusHtml } from "./lib/status.ts";
|
|
106
|
-
import {
|
|
107
|
-
buildTelegramPromptTurn,
|
|
108
|
-
truncateTelegramQueueSummary,
|
|
109
|
-
} from "./lib/turns.ts";
|
|
110
|
-
import {
|
|
111
|
-
collectTelegramReactionEmojis,
|
|
112
|
-
executeTelegramUpdate,
|
|
113
|
-
getTelegramAuthorizationState,
|
|
114
|
-
} from "./lib/updates.ts";
|
|
115
|
-
|
|
116
|
-
// --- Telegram API Types ---
|
|
117
|
-
|
|
118
|
-
interface TelegramApiResponse<T> {
|
|
119
|
-
ok: boolean;
|
|
120
|
-
result?: T;
|
|
121
|
-
description?: string;
|
|
122
|
-
error_code?: number;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface TelegramUser {
|
|
126
|
-
id: number;
|
|
127
|
-
is_bot: boolean;
|
|
128
|
-
first_name: string;
|
|
129
|
-
username?: string;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface TelegramChat {
|
|
133
|
-
id: number;
|
|
134
|
-
type: string;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
interface TelegramPhotoSize {
|
|
138
|
-
file_id: string;
|
|
139
|
-
file_size?: number;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
interface TelegramDocument {
|
|
143
|
-
file_id: string;
|
|
144
|
-
file_name?: string;
|
|
145
|
-
mime_type?: string;
|
|
146
|
-
file_size?: number;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
interface TelegramVideo {
|
|
150
|
-
file_id: string;
|
|
151
|
-
file_name?: string;
|
|
152
|
-
mime_type?: string;
|
|
153
|
-
file_size?: number;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
interface TelegramAudio {
|
|
157
|
-
file_id: string;
|
|
158
|
-
file_name?: string;
|
|
159
|
-
mime_type?: string;
|
|
160
|
-
file_size?: number;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
interface TelegramVoice {
|
|
164
|
-
file_id: string;
|
|
165
|
-
mime_type?: string;
|
|
166
|
-
file_size?: number;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
interface TelegramAnimation {
|
|
170
|
-
file_id: string;
|
|
171
|
-
file_name?: string;
|
|
172
|
-
mime_type?: string;
|
|
173
|
-
file_size?: number;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
interface TelegramSticker {
|
|
177
|
-
file_id: string;
|
|
178
|
-
emoji?: string;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
interface TelegramFileInfo {
|
|
182
|
-
file_id: string;
|
|
183
|
-
fileName: string;
|
|
184
|
-
mimeType?: string;
|
|
185
|
-
isImage: boolean;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
interface TelegramMessage {
|
|
189
|
-
message_id: number;
|
|
190
|
-
chat: TelegramChat;
|
|
191
|
-
from?: TelegramUser;
|
|
192
|
-
text?: string;
|
|
193
|
-
caption?: string;
|
|
194
|
-
media_group_id?: string;
|
|
195
|
-
photo?: TelegramPhotoSize[];
|
|
196
|
-
document?: TelegramDocument;
|
|
197
|
-
video?: TelegramVideo;
|
|
198
|
-
audio?: TelegramAudio;
|
|
199
|
-
voice?: TelegramVoice;
|
|
200
|
-
animation?: TelegramAnimation;
|
|
201
|
-
sticker?: TelegramSticker;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
interface TelegramCallbackQuery {
|
|
205
|
-
id: string;
|
|
206
|
-
from: TelegramUser;
|
|
207
|
-
message?: TelegramMessage;
|
|
208
|
-
data?: string;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
interface TelegramReactionTypeEmoji {
|
|
212
|
-
type: "emoji";
|
|
213
|
-
emoji: string;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
interface TelegramReactionTypeCustomEmoji {
|
|
217
|
-
type: "custom_emoji";
|
|
218
|
-
custom_emoji_id: string;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
interface TelegramReactionTypePaid {
|
|
222
|
-
type: "paid";
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
type TelegramReactionType =
|
|
226
|
-
| TelegramReactionTypeEmoji
|
|
227
|
-
| TelegramReactionTypeCustomEmoji
|
|
228
|
-
| TelegramReactionTypePaid;
|
|
229
|
-
|
|
230
|
-
interface TelegramMessageReactionUpdated {
|
|
231
|
-
chat: TelegramChat;
|
|
232
|
-
message_id: number;
|
|
233
|
-
user?: TelegramUser;
|
|
234
|
-
actor_chat?: TelegramChat;
|
|
235
|
-
old_reaction: TelegramReactionType[];
|
|
236
|
-
new_reaction: TelegramReactionType[];
|
|
237
|
-
date: number;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
interface TelegramUpdate {
|
|
241
|
-
update_id: number;
|
|
242
|
-
message?: TelegramMessage;
|
|
243
|
-
edited_message?: TelegramMessage;
|
|
244
|
-
callback_query?: TelegramCallbackQuery;
|
|
245
|
-
message_reaction?: TelegramMessageReactionUpdated;
|
|
246
|
-
deleted_business_messages?: { message_ids?: unknown };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
interface TelegramGetFileResult {
|
|
250
|
-
file_path: string;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
interface TelegramSentMessage {
|
|
254
|
-
message_id: number;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
interface TelegramBotCommand {
|
|
258
|
-
command: string;
|
|
259
|
-
description: string;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// --- Extension State Types ---
|
|
263
|
-
|
|
264
|
-
interface DownloadedTelegramFile {
|
|
265
|
-
path: string;
|
|
266
|
-
fileName: string;
|
|
267
|
-
isImage: boolean;
|
|
268
|
-
mimeType?: string;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
type ActiveTelegramTurn = PendingTelegramTurn;
|
|
272
|
-
|
|
273
|
-
type TelegramPreviewState = TelegramPreviewRuntimeState;
|
|
274
|
-
|
|
275
|
-
interface TelegramMediaGroupState {
|
|
276
|
-
messages: TelegramMessage[];
|
|
277
|
-
flushTimer?: ReturnType<typeof setTimeout>;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const AGENT_DIR = join(homedir(), ".pi", "agent");
|
|
281
|
-
const CONFIG_PATH = join(AGENT_DIR, "telegram.json");
|
|
282
|
-
const TEMP_DIR = join(AGENT_DIR, "tmp", "telegram");
|
|
283
|
-
const TELEGRAM_PREFIX = "[telegram]";
|
|
284
|
-
const MAX_ATTACHMENTS_PER_TURN = 10;
|
|
285
|
-
const PREVIEW_THROTTLE_MS = 750;
|
|
286
|
-
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
|
287
|
-
const TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS = 1200;
|
|
288
|
-
const SYSTEM_PROMPT_SUFFIX = `
|
|
289
|
-
|
|
290
|
-
Telegram bridge extension is active.
|
|
291
|
-
- Messages forwarded from Telegram are prefixed with "[telegram]".
|
|
292
|
-
- [telegram] messages may include local temp file paths for Telegram attachments. Read those files as needed.
|
|
293
|
-
- If a [telegram] user asked for a file or generated artifact, use the telegram_attach tool with the local file path so the extension can send it with your next final reply.
|
|
294
|
-
- Do not assume mentioning a local file path in plain text will send it to Telegram. Use telegram_attach.`;
|
|
295
|
-
|
|
296
|
-
// --- Generic Utilities ---
|
|
297
|
-
|
|
298
|
-
function isTelegramPrompt(prompt: string): boolean {
|
|
299
|
-
return prompt.trimStart().startsWith(TELEGRAM_PREFIX);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function sanitizeFileName(name: string): string {
|
|
303
|
-
return name.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function parseTelegramCommand(
|
|
307
|
-
text: string,
|
|
308
|
-
): { name: string; args: string } | undefined {
|
|
309
|
-
const trimmed = text.trim();
|
|
310
|
-
if (!trimmed.startsWith("/")) return undefined;
|
|
311
|
-
const [head, ...tail] = trimmed.split(/\s+/);
|
|
312
|
-
const name = head.slice(1).split("@")[0]?.toLowerCase();
|
|
313
|
-
if (!name) return undefined;
|
|
314
|
-
return { name, args: tail.join(" ").trim() };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function getCliScopedModelPatterns(): string[] | undefined {
|
|
318
|
-
const args = process.argv.slice(2);
|
|
319
|
-
for (let i = 0; i < args.length; i++) {
|
|
320
|
-
const arg = args[i];
|
|
321
|
-
if (arg === "--models") {
|
|
322
|
-
const value = args[i + 1] ?? "";
|
|
323
|
-
const patterns = value
|
|
324
|
-
.split(",")
|
|
325
|
-
.map((pattern) => pattern.trim())
|
|
326
|
-
.filter(Boolean);
|
|
327
|
-
return patterns.length > 0 ? patterns : undefined;
|
|
328
|
-
}
|
|
329
|
-
if (arg.startsWith("--models=")) {
|
|
330
|
-
const patterns = arg
|
|
331
|
-
.slice("--models=".length)
|
|
332
|
-
.split(",")
|
|
333
|
-
.map((pattern) => pattern.trim())
|
|
334
|
-
.filter(Boolean);
|
|
335
|
-
return patterns.length > 0 ? patterns : undefined;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return undefined;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
|
|
342
|
-
return label.length <= maxLength
|
|
343
|
-
? label
|
|
344
|
-
: `${label.slice(0, maxLength - 1)}…`;
|
|
345
|
-
}
|
|
6
|
+
import * as Api from "./lib/api.ts";
|
|
7
|
+
import * as Attachments from "./lib/attachments.ts";
|
|
8
|
+
import * as Commands from "./lib/commands.ts";
|
|
9
|
+
import * as Config from "./lib/config.ts";
|
|
10
|
+
import * as Media from "./lib/media.ts";
|
|
11
|
+
import * as Menu from "./lib/menu.ts";
|
|
12
|
+
import * as Model from "./lib/model.ts";
|
|
13
|
+
import * as Pi from "./lib/pi.ts";
|
|
14
|
+
import * as Polling from "./lib/polling.ts";
|
|
15
|
+
import * as Preview from "./lib/preview.ts";
|
|
16
|
+
import * as Queue from "./lib/queue.ts";
|
|
17
|
+
import * as Registration from "./lib/registration.ts";
|
|
18
|
+
import * as Replies from "./lib/replies.ts";
|
|
19
|
+
import * as Runtime from "./lib/runtime.ts";
|
|
20
|
+
import * as Setup from "./lib/setup.ts";
|
|
21
|
+
import * as Status from "./lib/status.ts";
|
|
22
|
+
import * as Turns from "./lib/turns.ts";
|
|
23
|
+
import * as Updates from "./lib/updates.ts";
|
|
24
|
+
|
|
25
|
+
type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
|
|
26
|
+
type RuntimeTelegramQueueItem = Queue.TelegramQueueItem<Pi.ExtensionContext>;
|
|
346
27
|
|
|
347
28
|
// --- Extension Runtime ---
|
|
348
29
|
|
|
349
|
-
export
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
let nextPriorityReactionOrder = 0;
|
|
386
|
-
let activeTelegramTurn: ActiveTelegramTurn | undefined;
|
|
387
|
-
let activeTelegramToolExecutions = 0;
|
|
388
|
-
let pendingTelegramModelSwitch: ScopedTelegramModel | undefined;
|
|
389
|
-
let telegramTurnDispatchPending = false;
|
|
390
|
-
let typingInterval: ReturnType<typeof setInterval> | undefined;
|
|
391
|
-
let currentAbort: (() => void) | undefined;
|
|
392
|
-
let preserveQueuedTurnsAsHistory = false;
|
|
393
|
-
let compactionInProgress = false;
|
|
394
|
-
let setupInProgress = false;
|
|
395
|
-
let previewState: TelegramPreviewState | undefined;
|
|
396
|
-
let draftSupport: "unknown" | "supported" | "unsupported" = "unknown";
|
|
397
|
-
let nextDraftId = 0;
|
|
398
|
-
let currentTelegramModel: Model<any> | undefined;
|
|
399
|
-
const mediaGroups = new Map<string, TelegramMediaGroupState>();
|
|
400
|
-
const modelMenus = new Map<number, TelegramModelMenuState>();
|
|
401
|
-
|
|
402
|
-
// --- Runtime State ---
|
|
403
|
-
|
|
404
|
-
function allocateDraftId(): number {
|
|
405
|
-
nextDraftId = nextDraftId >= TELEGRAM_DRAFT_ID_MAX ? 1 : nextDraftId + 1;
|
|
406
|
-
return nextDraftId;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function canDispatchQueuedTelegramTurn(ctx: ExtensionContext): boolean {
|
|
410
|
-
return canDispatchTelegramTurnState({
|
|
411
|
-
compactionInProgress,
|
|
412
|
-
hasActiveTelegramTurn: !!activeTelegramTurn,
|
|
413
|
-
hasPendingTelegramDispatch: telegramTurnDispatchPending,
|
|
414
|
-
isIdle: ctx.isIdle(),
|
|
415
|
-
hasPendingMessages: ctx.hasPendingMessages(),
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function executeQueuedTelegramControlItem(
|
|
420
|
-
item: PendingTelegramControlItem,
|
|
421
|
-
ctx: ExtensionContext,
|
|
422
|
-
): void {
|
|
423
|
-
void executeTelegramControlItemRuntime(item, {
|
|
424
|
-
ctx,
|
|
425
|
-
sendTextReply,
|
|
426
|
-
onSettled: () => {
|
|
427
|
-
updateStatus(ctx);
|
|
428
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
429
|
-
},
|
|
30
|
+
export default function (pi: Pi.ExtensionAPI) {
|
|
31
|
+
const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
|
|
32
|
+
const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
|
|
33
|
+
const configStore = Config.createTelegramConfigStore();
|
|
34
|
+
const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
|
|
35
|
+
const pendingModelSwitchStore =
|
|
36
|
+
Model.createPendingModelSwitchStore<
|
|
37
|
+
Model.ScopedTelegramModel<ActivePiModel>
|
|
38
|
+
>();
|
|
39
|
+
const modelMenuRuntime = Menu.createTelegramModelMenuRuntime<ActivePiModel>();
|
|
40
|
+
const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
|
|
41
|
+
getBotToken: configStore.getBotToken,
|
|
42
|
+
});
|
|
43
|
+
const mediaGroupRuntime =
|
|
44
|
+
Media.createTelegramMediaGroupController<Api.TelegramMessage>();
|
|
45
|
+
const telegramQueueStore =
|
|
46
|
+
Queue.createTelegramQueueStore<Pi.ExtensionContext>();
|
|
47
|
+
const pollingControllerState = Polling.createTelegramPollingControllerState();
|
|
48
|
+
const { getStatusLines, updateStatus } =
|
|
49
|
+
Status.createTelegramBridgeStatusRuntime<
|
|
50
|
+
Pi.ExtensionContext,
|
|
51
|
+
RuntimeTelegramQueueItem
|
|
52
|
+
>({
|
|
53
|
+
getConfig: configStore.get,
|
|
54
|
+
isPollingActive: Polling.createTelegramPollingActivityReader(
|
|
55
|
+
pollingControllerState,
|
|
56
|
+
),
|
|
57
|
+
getActiveSourceMessageIds: activeTurnRuntime.getSourceMessageIds,
|
|
58
|
+
hasActiveTurn: activeTurnRuntime.has,
|
|
59
|
+
hasDispatchPending: bridgeRuntime.lifecycle.hasDispatchPending,
|
|
60
|
+
isCompactionInProgress: bridgeRuntime.lifecycle.isCompactionInProgress,
|
|
61
|
+
getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
62
|
+
hasPendingModelSwitch: pendingModelSwitchStore.has,
|
|
63
|
+
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
64
|
+
formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
|
|
65
|
+
getRecentRuntimeEvents: runtimeEvents.getEvents,
|
|
430
66
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
onPromptDispatchStart: (chatId) => {
|
|
447
|
-
telegramTurnDispatchPending = true;
|
|
448
|
-
startTypingLoop(ctx, chatId);
|
|
449
|
-
updateStatus(ctx);
|
|
450
|
-
},
|
|
451
|
-
sendUserMessage: (content) => {
|
|
452
|
-
pi.sendUserMessage(content);
|
|
453
|
-
},
|
|
454
|
-
onPromptDispatchFailure: (message) => {
|
|
455
|
-
telegramTurnDispatchPending = false;
|
|
456
|
-
stopTypingLoop();
|
|
457
|
-
updateStatus(ctx, `dispatch failed: ${message}`);
|
|
458
|
-
},
|
|
459
|
-
onIdle: () => {
|
|
460
|
-
updateStatus(ctx);
|
|
461
|
-
},
|
|
67
|
+
const currentModelRuntime = Model.createCurrentModelRuntime<
|
|
68
|
+
Pi.ExtensionContext,
|
|
69
|
+
ActivePiModel
|
|
70
|
+
>({
|
|
71
|
+
getContextModel: Pi.getExtensionContextModel,
|
|
72
|
+
updateStatus,
|
|
73
|
+
});
|
|
74
|
+
const queueMutationRuntime =
|
|
75
|
+
Queue.createTelegramQueueMutationController<Pi.ExtensionContext>({
|
|
76
|
+
...telegramQueueStore,
|
|
77
|
+
getNextPriorityReactionOrder:
|
|
78
|
+
bridgeRuntime.queue.getNextPriorityReactionOrder,
|
|
79
|
+
incrementNextPriorityReactionOrder:
|
|
80
|
+
bridgeRuntime.queue.incrementNextPriorityReactionOrder,
|
|
81
|
+
updateStatus,
|
|
462
82
|
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// --- Status ---
|
|
466
|
-
|
|
467
|
-
function updateStatus(ctx: ExtensionContext, error?: string): void {
|
|
468
|
-
const theme = ctx.ui.theme;
|
|
469
|
-
const label = theme.fg("accent", "telegram");
|
|
470
|
-
if (error) {
|
|
471
|
-
ctx.ui.setStatus(
|
|
472
|
-
"telegram",
|
|
473
|
-
`${label} ${theme.fg("error", "error")} ${theme.fg("muted", error)}`,
|
|
474
|
-
);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
if (!config.botToken) {
|
|
478
|
-
ctx.ui.setStatus(
|
|
479
|
-
"telegram",
|
|
480
|
-
`${label} ${theme.fg("muted", "not configured")}`,
|
|
481
|
-
);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
if (!pollingPromise) {
|
|
485
|
-
ctx.ui.setStatus(
|
|
486
|
-
"telegram",
|
|
487
|
-
`${label} ${theme.fg("muted", "disconnected")}`,
|
|
488
|
-
);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
if (!config.allowedUserId) {
|
|
492
|
-
ctx.ui.setStatus(
|
|
493
|
-
"telegram",
|
|
494
|
-
`${label} ${theme.fg("warning", "awaiting pairing")}`,
|
|
495
|
-
);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (compactionInProgress) {
|
|
499
|
-
const queued = theme.fg(
|
|
500
|
-
"muted",
|
|
501
|
-
formatQueuedTelegramItemsStatus(queuedTelegramItems),
|
|
502
|
-
);
|
|
503
|
-
ctx.ui.setStatus(
|
|
504
|
-
"telegram",
|
|
505
|
-
`${label} ${theme.fg("accent", "compacting")}${queued}`,
|
|
506
|
-
);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
if (
|
|
510
|
-
activeTelegramTurn ||
|
|
511
|
-
telegramTurnDispatchPending ||
|
|
512
|
-
queuedTelegramItems.length > 0
|
|
513
|
-
) {
|
|
514
|
-
const queued = theme.fg(
|
|
515
|
-
"muted",
|
|
516
|
-
formatQueuedTelegramItemsStatus(queuedTelegramItems),
|
|
517
|
-
);
|
|
518
|
-
ctx.ui.setStatus(
|
|
519
|
-
"telegram",
|
|
520
|
-
`${label} ${theme.fg("accent", "processing")}${queued}`,
|
|
521
|
-
);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
ctx.ui.setStatus(
|
|
525
|
-
"telegram",
|
|
526
|
-
`${label} ${theme.fg("success", "connected")}`,
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
83
|
|
|
530
84
|
// --- Telegram API ---
|
|
531
85
|
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
fileName: string,
|
|
548
|
-
options?: { signal?: AbortSignal },
|
|
549
|
-
): Promise<TResponse> => {
|
|
550
|
-
return telegramApi.callMultipart<TResponse>(
|
|
551
|
-
method,
|
|
552
|
-
fields,
|
|
553
|
-
fileField,
|
|
554
|
-
filePath,
|
|
555
|
-
fileName,
|
|
556
|
-
options,
|
|
557
|
-
);
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const downloadTelegramBridgeFile = (
|
|
561
|
-
fileId: string,
|
|
562
|
-
suggestedName: string,
|
|
563
|
-
): Promise<string> => {
|
|
564
|
-
return telegramApi.downloadFile(fileId, suggestedName, TEMP_DIR);
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
const answerCallbackQuery = (
|
|
568
|
-
callbackQueryId: string,
|
|
569
|
-
text?: string,
|
|
570
|
-
): Promise<void> => {
|
|
571
|
-
return telegramApi.answerCallbackQuery(callbackQueryId, text);
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
// --- Message Delivery & Preview ---
|
|
575
|
-
|
|
576
|
-
function startTypingLoop(ctx: ExtensionContext, chatId?: number): void {
|
|
577
|
-
const targetChatId = chatId ?? activeTelegramTurn?.chatId;
|
|
578
|
-
if (typingInterval || targetChatId === undefined) return;
|
|
579
|
-
|
|
580
|
-
const sendTyping = async (): Promise<void> => {
|
|
581
|
-
try {
|
|
582
|
-
await callTelegramApi("sendChatAction", {
|
|
583
|
-
chat_id: targetChatId,
|
|
584
|
-
action: "typing",
|
|
585
|
-
});
|
|
586
|
-
} catch (error) {
|
|
587
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
588
|
-
updateStatus(ctx, `typing failed: ${message}`);
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
void sendTyping();
|
|
593
|
-
typingInterval = setInterval(() => {
|
|
594
|
-
void sendTyping();
|
|
595
|
-
}, 4000);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function stopTypingLoop(): void {
|
|
599
|
-
if (!typingInterval) return;
|
|
600
|
-
clearInterval(typingInterval);
|
|
601
|
-
typingInterval = undefined;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function isAssistantMessage(message: AgentMessage): boolean {
|
|
605
|
-
return (message as unknown as { role?: string }).role === "assistant";
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function extractTextContent(content: unknown): string {
|
|
609
|
-
const blocks = Array.isArray(content) ? content : [];
|
|
610
|
-
return blocks
|
|
611
|
-
.filter(
|
|
612
|
-
(block): block is { type: string; text?: string } =>
|
|
613
|
-
typeof block === "object" && block !== null && "type" in block,
|
|
614
|
-
)
|
|
615
|
-
.filter(
|
|
616
|
-
(block) => block.type === "text" && typeof block.text === "string",
|
|
617
|
-
)
|
|
618
|
-
.map((block) => block.text as string)
|
|
619
|
-
.join("")
|
|
620
|
-
.trim();
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function getMessageText(message: AgentMessage): string {
|
|
624
|
-
return extractTextContent(
|
|
625
|
-
(message as unknown as Record<string, unknown>).content,
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function createPreviewState(): TelegramPreviewState {
|
|
630
|
-
return {
|
|
631
|
-
mode: draftSupport === "unsupported" ? "message" : "draft",
|
|
632
|
-
pendingText: "",
|
|
633
|
-
lastSentText: "",
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function isTelegramMessageNotModifiedError(error: unknown): boolean {
|
|
638
|
-
return (
|
|
639
|
-
error instanceof Error &&
|
|
640
|
-
error.message.includes("message is not modified")
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async function editTelegramMessageText(
|
|
645
|
-
body: Record<string, unknown>,
|
|
646
|
-
): Promise<"edited" | "unchanged"> {
|
|
647
|
-
try {
|
|
648
|
-
await callTelegramApi("editMessageText", body);
|
|
649
|
-
return "edited";
|
|
650
|
-
} catch (error) {
|
|
651
|
-
if (isTelegramMessageNotModifiedError(error)) return "unchanged";
|
|
652
|
-
throw error;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const replyTransport = buildTelegramReplyTransport<TelegramReplyMarkup>({
|
|
657
|
-
sendMessage: async (body) => {
|
|
658
|
-
return callTelegramApi<TelegramSentMessage>("sendMessage", body);
|
|
659
|
-
},
|
|
660
|
-
editMessage: async (body) => {
|
|
661
|
-
await editTelegramMessageText(body);
|
|
662
|
-
},
|
|
86
|
+
const {
|
|
87
|
+
callMultipart,
|
|
88
|
+
deleteWebhook,
|
|
89
|
+
getUpdates,
|
|
90
|
+
setMyCommands,
|
|
91
|
+
sendTypingAction,
|
|
92
|
+
sendMessageDraft,
|
|
93
|
+
sendMessage,
|
|
94
|
+
downloadFile: downloadTelegramBridgeFile,
|
|
95
|
+
editMessageText: editTelegramMessageText,
|
|
96
|
+
answerCallbackQuery,
|
|
97
|
+
prepareTempDir,
|
|
98
|
+
} = Api.createDefaultTelegramBridgeApiRuntime({
|
|
99
|
+
getBotToken: configStore.getBotToken,
|
|
100
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
663
101
|
});
|
|
664
102
|
|
|
665
|
-
|
|
666
|
-
return {
|
|
667
|
-
getState: () => previewState,
|
|
668
|
-
setState: (state: TelegramPreviewState | undefined) => {
|
|
669
|
-
previewState = state;
|
|
670
|
-
},
|
|
671
|
-
clearScheduledFlush: (state: TelegramPreviewState) => {
|
|
672
|
-
if (!state.flushTimer) return;
|
|
673
|
-
clearTimeout(state.flushTimer);
|
|
674
|
-
state.flushTimer = undefined;
|
|
675
|
-
},
|
|
676
|
-
maxMessageLength: MAX_MESSAGE_LENGTH,
|
|
677
|
-
renderPreviewText: renderMarkdownPreviewText,
|
|
678
|
-
getDraftSupport: () => draftSupport,
|
|
679
|
-
setDraftSupport: (support: "unknown" | "supported" | "unsupported") => {
|
|
680
|
-
draftSupport = support;
|
|
681
|
-
},
|
|
682
|
-
allocateDraftId,
|
|
683
|
-
sendDraft: async (chatId: number, draftId: number, text: string) => {
|
|
684
|
-
await callTelegramApi("sendMessageDraft", {
|
|
685
|
-
chat_id: chatId,
|
|
686
|
-
draft_id: draftId,
|
|
687
|
-
text,
|
|
688
|
-
});
|
|
689
|
-
},
|
|
690
|
-
sendMessage: async (
|
|
691
|
-
chatId: number,
|
|
692
|
-
text: string,
|
|
693
|
-
options?: { parseMode?: "HTML" },
|
|
694
|
-
) => {
|
|
695
|
-
return callTelegramApi<TelegramSentMessage>("sendMessage", {
|
|
696
|
-
chat_id: chatId,
|
|
697
|
-
text,
|
|
698
|
-
parse_mode: options?.parseMode,
|
|
699
|
-
});
|
|
700
|
-
},
|
|
701
|
-
editMessageText: async (
|
|
702
|
-
chatId: number,
|
|
703
|
-
messageId: number,
|
|
704
|
-
text: string,
|
|
705
|
-
options?: { parseMode?: "HTML" },
|
|
706
|
-
) => {
|
|
707
|
-
await editTelegramMessageText({
|
|
708
|
-
chat_id: chatId,
|
|
709
|
-
message_id: messageId,
|
|
710
|
-
text,
|
|
711
|
-
parse_mode: options?.parseMode,
|
|
712
|
-
});
|
|
713
|
-
},
|
|
714
|
-
renderTelegramMessage,
|
|
715
|
-
sendRenderedChunks: replyTransport.sendRenderedChunks,
|
|
716
|
-
editRenderedMessage: replyTransport.editRenderedMessage,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
async function clearPreview(chatId: number): Promise<void> {
|
|
721
|
-
await clearTelegramPreview(chatId, getPreviewRuntimeDeps());
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
async function flushPreview(chatId: number): Promise<void> {
|
|
725
|
-
await flushTelegramPreview(chatId, getPreviewRuntimeDeps());
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function schedulePreviewFlush(chatId: number): void {
|
|
729
|
-
if (!previewState || previewState.flushTimer) return;
|
|
730
|
-
previewState.flushTimer = setTimeout(() => {
|
|
731
|
-
void flushPreview(chatId);
|
|
732
|
-
}, PREVIEW_THROTTLE_MS);
|
|
733
|
-
}
|
|
103
|
+
// --- Message Delivery & Preview ---
|
|
734
104
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
105
|
+
const promptDispatchRuntime =
|
|
106
|
+
Runtime.createTelegramPromptDispatchRuntime<Pi.ExtensionContext>({
|
|
107
|
+
lifecycle: bridgeRuntime.lifecycle,
|
|
108
|
+
typing: bridgeRuntime.typing,
|
|
109
|
+
getDefaultChatId: activeTurnRuntime.getChatId,
|
|
110
|
+
sendTypingAction,
|
|
111
|
+
updateStatus,
|
|
112
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
113
|
+
});
|
|
738
114
|
|
|
739
|
-
|
|
740
|
-
chatId: number,
|
|
741
|
-
markdown: string,
|
|
742
|
-
): Promise<boolean> {
|
|
743
|
-
return finalizeTelegramMarkdownPreview(
|
|
744
|
-
chatId,
|
|
745
|
-
markdown,
|
|
746
|
-
getPreviewRuntimeDeps(),
|
|
747
|
-
);
|
|
748
|
-
}
|
|
115
|
+
// --- Reply Runtime Wiring ---
|
|
749
116
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
117
|
+
const {
|
|
118
|
+
replyTransport,
|
|
119
|
+
sendTextReply,
|
|
120
|
+
sendMarkdownReply,
|
|
121
|
+
editInteractiveMessage,
|
|
122
|
+
sendInteractiveMessage,
|
|
123
|
+
} =
|
|
124
|
+
Replies.createTelegramRenderedMessageDeliveryRuntime<Menu.TelegramReplyMarkup>(
|
|
758
125
|
{
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
replyTransport.sendRenderedChunks(chatId, chunks),
|
|
126
|
+
sendMessage,
|
|
127
|
+
editMessage: editTelegramMessageText,
|
|
762
128
|
},
|
|
763
|
-
options,
|
|
764
129
|
);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
sendRenderedChunks: async (chunks) => {
|
|
775
|
-
if (chunks.length === 0) {
|
|
776
|
-
return sendTextReply(chatId, replyToMessageId, markdown);
|
|
777
|
-
}
|
|
778
|
-
return replyTransport.sendRenderedChunks(chatId, chunks);
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
async function sendQueuedAttachments(
|
|
784
|
-
turn: ActiveTelegramTurn,
|
|
785
|
-
): Promise<void> {
|
|
786
|
-
await sendQueuedTelegramAttachments(turn, {
|
|
787
|
-
sendMultipart: async (method, fields, fileField, filePath, fileName) => {
|
|
788
|
-
await callTelegramMultipartApi<TelegramSentMessage>(
|
|
789
|
-
method,
|
|
790
|
-
fields,
|
|
791
|
-
fileField,
|
|
792
|
-
filePath,
|
|
793
|
-
fileName,
|
|
794
|
-
);
|
|
795
|
-
},
|
|
130
|
+
const dispatchNextQueuedTelegramTurn =
|
|
131
|
+
Queue.createTelegramQueueDispatchRuntime<Pi.ExtensionContext>({
|
|
132
|
+
...telegramQueueStore,
|
|
133
|
+
isCompactionInProgress: bridgeRuntime.lifecycle.isCompactionInProgress,
|
|
134
|
+
hasActiveTurn: activeTurnRuntime.has,
|
|
135
|
+
hasDispatchPending: bridgeRuntime.lifecycle.hasDispatchPending,
|
|
136
|
+
isIdle: Pi.isExtensionContextIdle,
|
|
137
|
+
hasPendingMessages: Pi.hasExtensionContextPendingMessages,
|
|
138
|
+
updateStatus,
|
|
796
139
|
sendTextReply,
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
typeof message.errorMessage === "string"
|
|
812
|
-
? message.errorMessage
|
|
813
|
-
: undefined;
|
|
814
|
-
const text = extractTextContent(message.content);
|
|
815
|
-
return { text: text || undefined, stopReason, errorMessage };
|
|
816
|
-
}
|
|
817
|
-
return {};
|
|
818
|
-
}
|
|
140
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
141
|
+
...promptDispatchRuntime,
|
|
142
|
+
sendUserMessage: piRuntime.sendUserMessage,
|
|
143
|
+
}).dispatchNext;
|
|
144
|
+
const previewRuntime = Preview.createTelegramAssistantPreviewRuntime({
|
|
145
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
146
|
+
isAssistantMessage: Replies.isAssistantAgentMessage,
|
|
147
|
+
getMessageText: Replies.getAgentMessageText,
|
|
148
|
+
getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
|
|
149
|
+
sendDraft: sendMessageDraft,
|
|
150
|
+
sendMessage,
|
|
151
|
+
editMessageText: editTelegramMessageText,
|
|
152
|
+
...replyTransport,
|
|
153
|
+
});
|
|
819
154
|
|
|
820
155
|
// --- Bridge Setup ---
|
|
821
156
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const nextConfig: TelegramConfig = { ...config, botToken: token.trim() };
|
|
839
|
-
const response = await fetch(
|
|
840
|
-
`https://api.telegram.org/bot${nextConfig.botToken}/getMe`,
|
|
841
|
-
);
|
|
842
|
-
const data = (await response.json()) as TelegramApiResponse<TelegramUser>;
|
|
843
|
-
if (!data.ok || !data.result) {
|
|
844
|
-
ctx.ui.notify(
|
|
845
|
-
data.description || "Invalid Telegram bot token",
|
|
846
|
-
"error",
|
|
847
|
-
);
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
nextConfig.botId = data.result.id;
|
|
852
|
-
nextConfig.botUsername = data.result.username;
|
|
853
|
-
config = nextConfig;
|
|
854
|
-
await writeTelegramConfig(AGENT_DIR, CONFIG_PATH, config);
|
|
855
|
-
ctx.ui.notify(
|
|
856
|
-
`Telegram bot connected: @${config.botUsername ?? "unknown"}`,
|
|
857
|
-
"info",
|
|
858
|
-
);
|
|
859
|
-
ctx.ui.notify(
|
|
860
|
-
"Send /start to your bot in Telegram to pair this extension with your account.",
|
|
861
|
-
"info",
|
|
862
|
-
);
|
|
863
|
-
await startPolling(ctx);
|
|
864
|
-
updateStatus(ctx);
|
|
865
|
-
} finally {
|
|
866
|
-
setupInProgress = false;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
async function registerTelegramBotCommands(): Promise<void> {
|
|
871
|
-
const commands: TelegramBotCommand[] = [
|
|
872
|
-
{
|
|
873
|
-
command: "start",
|
|
874
|
-
description: "Show help and pair the Telegram bridge",
|
|
875
|
-
},
|
|
876
|
-
{
|
|
877
|
-
command: "status",
|
|
878
|
-
description: "Show model, usage, cost, and context status",
|
|
879
|
-
},
|
|
880
|
-
{ command: "model", description: "Open the interactive model selector" },
|
|
881
|
-
{ command: "compact", description: "Compact the current pi session" },
|
|
882
|
-
{ command: "stop", description: "Abort the current pi task" },
|
|
883
|
-
];
|
|
884
|
-
await callTelegramApi<boolean>("setMyCommands", { commands });
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function getCurrentTelegramModel(
|
|
888
|
-
ctx: ExtensionContext,
|
|
889
|
-
): Model<any> | undefined {
|
|
890
|
-
return currentTelegramModel ?? ctx.model;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// --- Interactive Menu State & Builders ---
|
|
894
|
-
|
|
895
|
-
async function getModelMenuState(
|
|
896
|
-
chatId: number,
|
|
897
|
-
ctx: ExtensionContext,
|
|
898
|
-
): Promise<TelegramModelMenuState> {
|
|
899
|
-
const settingsManager = SettingsManager.create(ctx.cwd);
|
|
900
|
-
await settingsManager.reload();
|
|
901
|
-
ctx.modelRegistry.refresh();
|
|
902
|
-
const activeModel = getCurrentTelegramModel(ctx);
|
|
903
|
-
const availableModels = ctx.modelRegistry.getAvailable();
|
|
904
|
-
const cliScopedModels = getCliScopedModelPatterns();
|
|
905
|
-
const configuredScopedModels =
|
|
906
|
-
cliScopedModels ?? settingsManager.getEnabledModels() ?? [];
|
|
907
|
-
return buildTelegramModelMenuState({
|
|
908
|
-
chatId,
|
|
909
|
-
activeModel,
|
|
910
|
-
availableModels,
|
|
911
|
-
configuredScopedModelPatterns: configuredScopedModels,
|
|
912
|
-
cliScopedModelPatterns: cliScopedModels ?? undefined,
|
|
157
|
+
const modelSwitchController =
|
|
158
|
+
Model.createTelegramModelSwitchControllerRuntime<
|
|
159
|
+
Pi.ExtensionContext,
|
|
160
|
+
Model.ScopedTelegramModel<ActivePiModel>
|
|
161
|
+
>({
|
|
162
|
+
isIdle: Pi.isExtensionContextIdle,
|
|
163
|
+
getPendingModelSwitch: pendingModelSwitchStore.get,
|
|
164
|
+
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
165
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
166
|
+
getAbortHandler: bridgeRuntime.abort.getHandler,
|
|
167
|
+
hasAbortHandler: bridgeRuntime.abort.hasHandler,
|
|
168
|
+
getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
169
|
+
allocateItemOrder: bridgeRuntime.queue.allocateItemOrder,
|
|
170
|
+
allocateControlOrder: bridgeRuntime.queue.allocateControlOrder,
|
|
171
|
+
appendQueuedItem: queueMutationRuntime.append,
|
|
172
|
+
updateStatus,
|
|
913
173
|
});
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
)
|
|
932
|
-
await updateTelegramThinkingMenuMessage(
|
|
933
|
-
state,
|
|
934
|
-
getCurrentTelegramModel(ctx),
|
|
935
|
-
pi.getThinkingLevel(),
|
|
936
|
-
{ editInteractiveMessage, sendInteractiveMessage },
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
async function editInteractiveMessage(
|
|
941
|
-
chatId: number,
|
|
942
|
-
messageId: number,
|
|
943
|
-
text: string,
|
|
944
|
-
mode: TelegramRenderMode,
|
|
945
|
-
replyMarkup: TelegramReplyMarkup,
|
|
946
|
-
): Promise<void> {
|
|
947
|
-
await replyTransport.editRenderedMessage(
|
|
948
|
-
chatId,
|
|
949
|
-
messageId,
|
|
950
|
-
renderTelegramMessage(text, { mode }),
|
|
951
|
-
{ replyMarkup },
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async function sendInteractiveMessage(
|
|
956
|
-
chatId: number,
|
|
957
|
-
text: string,
|
|
958
|
-
mode: TelegramRenderMode,
|
|
959
|
-
replyMarkup: TelegramReplyMarkup,
|
|
960
|
-
): Promise<number | undefined> {
|
|
961
|
-
return replyTransport.sendRenderedChunks(
|
|
962
|
-
chatId,
|
|
963
|
-
renderTelegramMessage(text, { mode }),
|
|
964
|
-
{ replyMarkup },
|
|
965
|
-
);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
async function ensureIdleOrNotify(
|
|
969
|
-
ctx: ExtensionContext,
|
|
970
|
-
chatId: number,
|
|
971
|
-
replyToMessageId: number,
|
|
972
|
-
busyMessage: string,
|
|
973
|
-
): Promise<boolean> {
|
|
974
|
-
if (ctx.isIdle()) return true;
|
|
975
|
-
await sendTextReply(chatId, replyToMessageId, busyMessage);
|
|
976
|
-
return false;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
async function showStatusMessage(
|
|
980
|
-
state: TelegramModelMenuState,
|
|
981
|
-
ctx: ExtensionContext,
|
|
982
|
-
): Promise<void> {
|
|
983
|
-
await updateTelegramStatusMessage(
|
|
984
|
-
state,
|
|
985
|
-
buildStatusHtml(ctx, getCurrentTelegramModel(ctx)),
|
|
986
|
-
getCurrentTelegramModel(ctx),
|
|
987
|
-
pi.getThinkingLevel(),
|
|
988
|
-
{ editInteractiveMessage, sendInteractiveMessage },
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
async function sendStatusMessage(
|
|
993
|
-
chatId: number,
|
|
994
|
-
replyToMessageId: number,
|
|
995
|
-
ctx: ExtensionContext,
|
|
996
|
-
): Promise<void> {
|
|
997
|
-
const isIdle = await ensureIdleOrNotify(
|
|
998
|
-
ctx,
|
|
999
|
-
chatId,
|
|
1000
|
-
replyToMessageId,
|
|
1001
|
-
"Cannot open status while pi is busy. Send /stop first.",
|
|
1002
|
-
);
|
|
1003
|
-
if (!isIdle) return;
|
|
1004
|
-
const state = await getModelMenuState(chatId, ctx);
|
|
1005
|
-
const messageId = await sendTelegramStatusMessage(
|
|
1006
|
-
state,
|
|
1007
|
-
buildStatusHtml(ctx, getCurrentTelegramModel(ctx)),
|
|
1008
|
-
getCurrentTelegramModel(ctx),
|
|
1009
|
-
pi.getThinkingLevel(),
|
|
1010
|
-
{ editInteractiveMessage, sendInteractiveMessage },
|
|
1011
|
-
);
|
|
1012
|
-
if (messageId === undefined) return;
|
|
1013
|
-
state.messageId = messageId;
|
|
1014
|
-
state.mode = "status";
|
|
1015
|
-
modelMenus.set(messageId, state);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function canOfferInFlightTelegramModelSwitch(ctx: ExtensionContext): boolean {
|
|
1019
|
-
return canRestartTelegramTurnForModelSwitch({
|
|
1020
|
-
isIdle: ctx.isIdle(),
|
|
1021
|
-
hasActiveTelegramTurn: !!activeTelegramTurn,
|
|
1022
|
-
hasAbortHandler: !!currentAbort,
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
function createTelegramControlItem(
|
|
1027
|
-
chatId: number,
|
|
1028
|
-
replyToMessageId: number,
|
|
1029
|
-
controlType: PendingTelegramControlItem["controlType"],
|
|
1030
|
-
statusSummary: string,
|
|
1031
|
-
execute: PendingTelegramControlItem["execute"],
|
|
1032
|
-
): PendingTelegramControlItem {
|
|
1033
|
-
const queueOrder = nextQueuedTelegramItemOrder++;
|
|
1034
|
-
return {
|
|
1035
|
-
kind: "control",
|
|
1036
|
-
controlType,
|
|
1037
|
-
chatId,
|
|
1038
|
-
replyToMessageId,
|
|
1039
|
-
queueOrder,
|
|
1040
|
-
queueLane: "control",
|
|
1041
|
-
laneOrder: nextQueuedTelegramControlOrder++,
|
|
1042
|
-
statusSummary,
|
|
1043
|
-
execute,
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function enqueueTelegramControlItem(
|
|
1048
|
-
item: PendingTelegramControlItem,
|
|
1049
|
-
ctx: ExtensionContext,
|
|
1050
|
-
): void {
|
|
1051
|
-
queuedTelegramItems.push(item);
|
|
1052
|
-
reorderQueuedTelegramTurns(ctx);
|
|
1053
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function createTelegramModelSwitchContinuationTurn(
|
|
1057
|
-
turn: ActiveTelegramTurn,
|
|
1058
|
-
selection: ScopedTelegramModel,
|
|
1059
|
-
): PendingTelegramTurn {
|
|
1060
|
-
const statusLabel = truncateTelegramQueueSummary(
|
|
1061
|
-
`continue on ${selection.model.id}`,
|
|
1062
|
-
4,
|
|
1063
|
-
32,
|
|
1064
|
-
);
|
|
1065
|
-
return {
|
|
1066
|
-
kind: "prompt",
|
|
1067
|
-
chatId: turn.chatId,
|
|
1068
|
-
replyToMessageId: turn.replyToMessageId,
|
|
1069
|
-
sourceMessageIds: [],
|
|
1070
|
-
queueOrder: nextQueuedTelegramItemOrder++,
|
|
1071
|
-
queueLane: "control",
|
|
1072
|
-
laneOrder: nextQueuedTelegramControlOrder++,
|
|
1073
|
-
queuedAttachments: [],
|
|
1074
|
-
content: [
|
|
1075
|
-
{
|
|
1076
|
-
type: "text",
|
|
1077
|
-
text: buildTelegramModelSwitchContinuationText(
|
|
1078
|
-
TELEGRAM_PREFIX,
|
|
1079
|
-
selection.model,
|
|
1080
|
-
selection.thinkingLevel,
|
|
1081
|
-
),
|
|
1082
|
-
},
|
|
1083
|
-
],
|
|
1084
|
-
historyText: `Continue interrupted Telegram request on ${getCanonicalModelId(selection.model)}`,
|
|
1085
|
-
statusSummary: `↻ ${statusLabel || "continue"}`,
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function queueTelegramModelSwitchContinuation(
|
|
1090
|
-
turn: ActiveTelegramTurn,
|
|
1091
|
-
selection: ScopedTelegramModel,
|
|
1092
|
-
ctx: ExtensionContext,
|
|
1093
|
-
): void {
|
|
1094
|
-
queuedTelegramItems.push(
|
|
1095
|
-
createTelegramModelSwitchContinuationTurn(turn, selection),
|
|
1096
|
-
);
|
|
1097
|
-
reorderQueuedTelegramTurns(ctx);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function triggerPendingTelegramModelSwitchAbort(
|
|
1101
|
-
ctx: ExtensionContext,
|
|
1102
|
-
): boolean {
|
|
1103
|
-
if (
|
|
1104
|
-
!shouldTriggerPendingTelegramModelSwitchAbort({
|
|
1105
|
-
hasPendingModelSwitch: !!pendingTelegramModelSwitch,
|
|
1106
|
-
hasActiveTelegramTurn: !!activeTelegramTurn,
|
|
1107
|
-
hasAbortHandler: !!currentAbort,
|
|
1108
|
-
activeToolExecutions: activeTelegramToolExecutions,
|
|
1109
|
-
})
|
|
1110
|
-
) {
|
|
1111
|
-
return false;
|
|
1112
|
-
}
|
|
1113
|
-
const selection = pendingTelegramModelSwitch;
|
|
1114
|
-
const turn = activeTelegramTurn;
|
|
1115
|
-
const abort = currentAbort;
|
|
1116
|
-
if (!selection || !turn || !abort) return false;
|
|
1117
|
-
pendingTelegramModelSwitch = undefined;
|
|
1118
|
-
queueTelegramModelSwitchContinuation(turn, selection, ctx);
|
|
1119
|
-
abort();
|
|
1120
|
-
return true;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
async function openModelMenu(
|
|
1124
|
-
chatId: number,
|
|
1125
|
-
replyToMessageId: number,
|
|
1126
|
-
ctx: ExtensionContext,
|
|
1127
|
-
): Promise<void> {
|
|
1128
|
-
if (!ctx.isIdle() && !canOfferInFlightTelegramModelSwitch(ctx)) {
|
|
1129
|
-
await sendTextReply(
|
|
1130
|
-
chatId,
|
|
1131
|
-
replyToMessageId,
|
|
1132
|
-
"Cannot switch model while pi is busy. Send /stop first.",
|
|
1133
|
-
);
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
const state = await getModelMenuState(chatId, ctx);
|
|
1137
|
-
if (state.allModels.length === 0) {
|
|
1138
|
-
await sendTextReply(
|
|
1139
|
-
chatId,
|
|
1140
|
-
replyToMessageId,
|
|
1141
|
-
"No available models with configured auth.",
|
|
1142
|
-
);
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
const activeModel = getCurrentTelegramModel(ctx);
|
|
1146
|
-
const messageId = await sendTelegramModelMenuMessage(state, activeModel, {
|
|
1147
|
-
editInteractiveMessage,
|
|
1148
|
-
sendInteractiveMessage,
|
|
1149
|
-
});
|
|
1150
|
-
if (messageId === undefined) return;
|
|
1151
|
-
state.messageId = messageId;
|
|
1152
|
-
state.mode = "model";
|
|
1153
|
-
modelMenus.set(messageId, state);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
async function handleStatusCallbackAction(
|
|
1157
|
-
query: TelegramCallbackQuery,
|
|
1158
|
-
state: TelegramModelMenuState,
|
|
1159
|
-
ctx: ExtensionContext,
|
|
1160
|
-
): Promise<boolean> {
|
|
1161
|
-
return handleTelegramStatusMenuCallbackAction(
|
|
1162
|
-
query.id,
|
|
1163
|
-
query.data,
|
|
1164
|
-
getCurrentTelegramModel(ctx),
|
|
1165
|
-
{
|
|
1166
|
-
updateModelMenuMessage: async () => updateModelMenuMessage(state, ctx),
|
|
1167
|
-
updateThinkingMenuMessage: async () =>
|
|
1168
|
-
updateThinkingMenuMessage(state, ctx),
|
|
1169
|
-
answerCallbackQuery,
|
|
1170
|
-
},
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
async function handleThinkingCallbackAction(
|
|
1175
|
-
query: TelegramCallbackQuery,
|
|
1176
|
-
state: TelegramModelMenuState,
|
|
1177
|
-
ctx: ExtensionContext,
|
|
1178
|
-
): Promise<boolean> {
|
|
1179
|
-
return handleTelegramThinkingMenuCallbackAction(
|
|
1180
|
-
query.id,
|
|
1181
|
-
query.data,
|
|
1182
|
-
getCurrentTelegramModel(ctx),
|
|
1183
|
-
{
|
|
1184
|
-
setThinkingLevel: (level) => {
|
|
1185
|
-
pi.setThinkingLevel(level);
|
|
1186
|
-
updateStatus(ctx);
|
|
1187
|
-
},
|
|
1188
|
-
getCurrentThinkingLevel: () => pi.getThinkingLevel(),
|
|
1189
|
-
updateStatusMessage: async () => showStatusMessage(state, ctx),
|
|
1190
|
-
answerCallbackQuery,
|
|
1191
|
-
},
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
async function handleModelCallbackAction(
|
|
1196
|
-
query: TelegramCallbackQuery,
|
|
1197
|
-
state: TelegramModelMenuState,
|
|
1198
|
-
ctx: ExtensionContext,
|
|
1199
|
-
): Promise<boolean> {
|
|
1200
|
-
try {
|
|
1201
|
-
return await handleTelegramModelMenuCallbackAction(
|
|
1202
|
-
query.id,
|
|
1203
|
-
{
|
|
1204
|
-
data: query.data,
|
|
1205
|
-
state,
|
|
1206
|
-
activeModel: getCurrentTelegramModel(ctx),
|
|
1207
|
-
currentThinkingLevel: pi.getThinkingLevel(),
|
|
1208
|
-
isIdle: ctx.isIdle(),
|
|
1209
|
-
canRestartBusyRun: !!activeTelegramTurn && !!currentAbort,
|
|
1210
|
-
hasActiveToolExecutions: activeTelegramToolExecutions > 0,
|
|
1211
|
-
},
|
|
1212
|
-
{
|
|
1213
|
-
updateModelMenuMessage: async () =>
|
|
1214
|
-
updateModelMenuMessage(state, ctx),
|
|
1215
|
-
updateStatusMessage: async () => showStatusMessage(state, ctx),
|
|
1216
|
-
answerCallbackQuery,
|
|
1217
|
-
setModel: (model) => pi.setModel(model),
|
|
1218
|
-
setCurrentModel: (model) => {
|
|
1219
|
-
currentTelegramModel = model;
|
|
1220
|
-
updateStatus(ctx);
|
|
1221
|
-
},
|
|
1222
|
-
setThinkingLevel: (level) => {
|
|
1223
|
-
pi.setThinkingLevel(level);
|
|
1224
|
-
updateStatus(ctx);
|
|
1225
|
-
},
|
|
1226
|
-
stagePendingModelSwitch: (selection) => {
|
|
1227
|
-
pendingTelegramModelSwitch = selection;
|
|
1228
|
-
updateStatus(ctx);
|
|
1229
|
-
},
|
|
1230
|
-
restartInterruptedTelegramTurn: (selection) => {
|
|
1231
|
-
return restartTelegramModelSwitchContinuation({
|
|
1232
|
-
activeTurn: activeTelegramTurn,
|
|
1233
|
-
abort: currentAbort,
|
|
1234
|
-
selection,
|
|
1235
|
-
queueContinuation: (turn, nextSelection) => {
|
|
1236
|
-
queueTelegramModelSwitchContinuation(turn, nextSelection, ctx);
|
|
1237
|
-
},
|
|
1238
|
-
});
|
|
1239
|
-
},
|
|
1240
|
-
},
|
|
1241
|
-
);
|
|
1242
|
-
} catch (error) {
|
|
1243
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1244
|
-
await answerCallbackQuery(query.id, message);
|
|
1245
|
-
return true;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
async function handleAuthorizedTelegramCallbackQuery(
|
|
1250
|
-
query: TelegramCallbackQuery,
|
|
1251
|
-
ctx: ExtensionContext,
|
|
1252
|
-
): Promise<void> {
|
|
1253
|
-
const messageId = query.message?.message_id;
|
|
1254
|
-
await handleTelegramMenuCallbackEntry(
|
|
1255
|
-
query.id,
|
|
1256
|
-
query.data,
|
|
1257
|
-
messageId ? modelMenus.get(messageId) : undefined,
|
|
1258
|
-
{
|
|
1259
|
-
handleStatusAction: async () => {
|
|
1260
|
-
const state = messageId ? modelMenus.get(messageId) : undefined;
|
|
1261
|
-
if (!state) return false;
|
|
1262
|
-
return handleStatusCallbackAction(query, state, ctx);
|
|
1263
|
-
},
|
|
1264
|
-
handleThinkingAction: async () => {
|
|
1265
|
-
const state = messageId ? modelMenus.get(messageId) : undefined;
|
|
1266
|
-
if (!state) return false;
|
|
1267
|
-
return handleThinkingCallbackAction(query, state, ctx);
|
|
1268
|
-
},
|
|
1269
|
-
handleModelAction: async () => {
|
|
1270
|
-
const state = messageId ? modelMenus.get(messageId) : undefined;
|
|
1271
|
-
if (!state) return false;
|
|
1272
|
-
return handleModelCallbackAction(query, state, ctx);
|
|
1273
|
-
},
|
|
1274
|
-
answerCallbackQuery,
|
|
1275
|
-
},
|
|
1276
|
-
);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// --- Status Rendering ---
|
|
1280
|
-
|
|
1281
|
-
// --- Turn Queue & Message Dispatch ---
|
|
1282
|
-
|
|
1283
|
-
async function buildTelegramFiles(
|
|
1284
|
-
messages: TelegramMessage[],
|
|
1285
|
-
): Promise<DownloadedTelegramFile[]> {
|
|
1286
|
-
const downloaded: DownloadedTelegramFile[] = [];
|
|
1287
|
-
for (const file of collectTelegramFileInfos(messages)) {
|
|
1288
|
-
const path = await downloadTelegramBridgeFile(
|
|
1289
|
-
file.file_id,
|
|
1290
|
-
file.fileName,
|
|
1291
|
-
);
|
|
1292
|
-
downloaded.push({
|
|
1293
|
-
path,
|
|
1294
|
-
fileName: file.fileName,
|
|
1295
|
-
isImage: file.isImage,
|
|
1296
|
-
mimeType: file.mimeType,
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
return downloaded;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
function reorderQueuedTelegramTurns(ctx: ExtensionContext): void {
|
|
1303
|
-
queuedTelegramItems.sort(compareTelegramQueueItems);
|
|
1304
|
-
updateStatus(ctx);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
function removePendingMediaGroupMessages(messageIds: number[]): void {
|
|
1308
|
-
if (messageIds.length === 0 || mediaGroups.size === 0) return;
|
|
1309
|
-
const deletedMessageIds = new Set(messageIds);
|
|
1310
|
-
for (const [key, state] of mediaGroups.entries()) {
|
|
1311
|
-
if (
|
|
1312
|
-
!state.messages.some((message) =>
|
|
1313
|
-
deletedMessageIds.has(message.message_id),
|
|
1314
|
-
)
|
|
1315
|
-
) {
|
|
1316
|
-
continue;
|
|
1317
|
-
}
|
|
1318
|
-
if (state.flushTimer) clearTimeout(state.flushTimer);
|
|
1319
|
-
mediaGroups.delete(key);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
function removeQueuedTelegramTurnsByMessageIds(
|
|
1324
|
-
messageIds: number[],
|
|
1325
|
-
ctx: ExtensionContext,
|
|
1326
|
-
): number {
|
|
1327
|
-
const result = removeTelegramQueueItemsByMessageIds(
|
|
1328
|
-
queuedTelegramItems,
|
|
1329
|
-
messageIds,
|
|
1330
|
-
);
|
|
1331
|
-
if (result.removedCount === 0) return 0;
|
|
1332
|
-
queuedTelegramItems = result.items;
|
|
1333
|
-
updateStatus(ctx);
|
|
1334
|
-
return result.removedCount;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
function clearQueuedTelegramTurnPriorityByMessageId(
|
|
1338
|
-
messageId: number,
|
|
1339
|
-
ctx: ExtensionContext,
|
|
1340
|
-
): boolean {
|
|
1341
|
-
const result = clearTelegramQueuePromptPriority(
|
|
1342
|
-
queuedTelegramItems,
|
|
1343
|
-
messageId,
|
|
1344
|
-
);
|
|
1345
|
-
if (!result.changed) return false;
|
|
1346
|
-
queuedTelegramItems = result.items;
|
|
1347
|
-
reorderQueuedTelegramTurns(ctx);
|
|
1348
|
-
return true;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
function prioritizeQueuedTelegramTurnByMessageId(
|
|
1352
|
-
messageId: number,
|
|
1353
|
-
ctx: ExtensionContext,
|
|
1354
|
-
): boolean {
|
|
1355
|
-
const result = prioritizeTelegramQueuePrompt(
|
|
1356
|
-
queuedTelegramItems,
|
|
1357
|
-
messageId,
|
|
1358
|
-
nextPriorityReactionOrder,
|
|
1359
|
-
);
|
|
1360
|
-
if (!result.changed) return false;
|
|
1361
|
-
queuedTelegramItems = result.items;
|
|
1362
|
-
nextPriorityReactionOrder += 1;
|
|
1363
|
-
reorderQueuedTelegramTurns(ctx);
|
|
1364
|
-
return true;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
async function handleAuthorizedTelegramReactionUpdate(
|
|
1368
|
-
reactionUpdate: TelegramMessageReactionUpdated,
|
|
1369
|
-
ctx: ExtensionContext,
|
|
1370
|
-
): Promise<void> {
|
|
1371
|
-
const reactionUser = reactionUpdate.user;
|
|
1372
|
-
if (
|
|
1373
|
-
reactionUpdate.chat.type !== "private" ||
|
|
1374
|
-
!reactionUser ||
|
|
1375
|
-
reactionUser.is_bot ||
|
|
1376
|
-
reactionUser.id !== config.allowedUserId
|
|
1377
|
-
) {
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
const oldEmojis = collectTelegramReactionEmojis(
|
|
1381
|
-
reactionUpdate.old_reaction,
|
|
1382
|
-
);
|
|
1383
|
-
const newEmojis = collectTelegramReactionEmojis(
|
|
1384
|
-
reactionUpdate.new_reaction,
|
|
1385
|
-
);
|
|
1386
|
-
const dislikeAdded = !oldEmojis.has("👎") && newEmojis.has("👎");
|
|
1387
|
-
if (dislikeAdded) {
|
|
1388
|
-
removePendingMediaGroupMessages([reactionUpdate.message_id]);
|
|
1389
|
-
removeQueuedTelegramTurnsByMessageIds([reactionUpdate.message_id], ctx);
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
const likeRemoved = oldEmojis.has("👍") && !newEmojis.has("👍");
|
|
1393
|
-
if (likeRemoved) {
|
|
1394
|
-
clearQueuedTelegramTurnPriorityByMessageId(
|
|
1395
|
-
reactionUpdate.message_id,
|
|
1396
|
-
ctx,
|
|
1397
|
-
);
|
|
1398
|
-
}
|
|
1399
|
-
const likeAdded = !oldEmojis.has("👍") && newEmojis.has("👍");
|
|
1400
|
-
if (!likeAdded) return;
|
|
1401
|
-
prioritizeQueuedTelegramTurnByMessageId(reactionUpdate.message_id, ctx);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
async function createTelegramTurn(
|
|
1405
|
-
messages: TelegramMessage[],
|
|
1406
|
-
historyTurns: PendingTelegramTurn[] = [],
|
|
1407
|
-
): Promise<PendingTelegramTurn> {
|
|
1408
|
-
return buildTelegramPromptTurn({
|
|
1409
|
-
telegramPrefix: TELEGRAM_PREFIX,
|
|
1410
|
-
messages,
|
|
1411
|
-
historyTurns,
|
|
1412
|
-
queueOrder: nextQueuedTelegramItemOrder++,
|
|
1413
|
-
rawText: extractTelegramMessagesText(messages),
|
|
1414
|
-
files: await buildTelegramFiles(messages),
|
|
1415
|
-
readBinaryFile: async (path) => readFile(path),
|
|
1416
|
-
inferImageMimeType: guessMediaType,
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
async function handleStopCommand(
|
|
1421
|
-
message: TelegramMessage,
|
|
1422
|
-
ctx: ExtensionContext,
|
|
1423
|
-
): Promise<void> {
|
|
1424
|
-
if (currentAbort) {
|
|
1425
|
-
pendingTelegramModelSwitch = undefined;
|
|
1426
|
-
if (queuedTelegramItems.length > 0) {
|
|
1427
|
-
preserveQueuedTurnsAsHistory = true;
|
|
1428
|
-
}
|
|
1429
|
-
currentAbort();
|
|
1430
|
-
updateStatus(ctx);
|
|
1431
|
-
await sendTextReply(
|
|
1432
|
-
message.chat.id,
|
|
1433
|
-
message.message_id,
|
|
1434
|
-
"Aborted current turn.",
|
|
1435
|
-
);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
await sendTextReply(message.chat.id, message.message_id, "No active turn.");
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
async function handleCompactCommand(
|
|
1442
|
-
message: TelegramMessage,
|
|
1443
|
-
ctx: ExtensionContext,
|
|
1444
|
-
): Promise<void> {
|
|
1445
|
-
if (
|
|
1446
|
-
!ctx.isIdle() ||
|
|
1447
|
-
ctx.hasPendingMessages() ||
|
|
1448
|
-
activeTelegramTurn ||
|
|
1449
|
-
telegramTurnDispatchPending ||
|
|
1450
|
-
queuedTelegramItems.length > 0 ||
|
|
1451
|
-
compactionInProgress
|
|
1452
|
-
) {
|
|
1453
|
-
await sendTextReply(
|
|
1454
|
-
message.chat.id,
|
|
1455
|
-
message.message_id,
|
|
1456
|
-
"Cannot compact while pi or the Telegram queue is busy. Wait for queued turns to finish or send /stop first.",
|
|
1457
|
-
);
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
compactionInProgress = true;
|
|
1461
|
-
updateStatus(ctx);
|
|
1462
|
-
try {
|
|
1463
|
-
ctx.compact({
|
|
1464
|
-
onComplete: () => {
|
|
1465
|
-
compactionInProgress = false;
|
|
1466
|
-
updateStatus(ctx);
|
|
1467
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
1468
|
-
void sendTextReply(
|
|
1469
|
-
message.chat.id,
|
|
1470
|
-
message.message_id,
|
|
1471
|
-
"Compaction completed.",
|
|
1472
|
-
);
|
|
1473
|
-
},
|
|
1474
|
-
onError: (error) => {
|
|
1475
|
-
compactionInProgress = false;
|
|
1476
|
-
updateStatus(ctx);
|
|
1477
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
1478
|
-
const errorMessage =
|
|
1479
|
-
error instanceof Error ? error.message : String(error);
|
|
1480
|
-
void sendTextReply(
|
|
1481
|
-
message.chat.id,
|
|
1482
|
-
message.message_id,
|
|
1483
|
-
`Compaction failed: ${errorMessage}`,
|
|
1484
|
-
);
|
|
1485
|
-
},
|
|
1486
|
-
});
|
|
1487
|
-
} catch (error) {
|
|
1488
|
-
compactionInProgress = false;
|
|
1489
|
-
updateStatus(ctx);
|
|
1490
|
-
const errorMessage =
|
|
1491
|
-
error instanceof Error ? error.message : String(error);
|
|
1492
|
-
await sendTextReply(
|
|
1493
|
-
message.chat.id,
|
|
1494
|
-
message.message_id,
|
|
1495
|
-
`Compaction failed: ${errorMessage}`,
|
|
1496
|
-
);
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
await sendTextReply(
|
|
1500
|
-
message.chat.id,
|
|
1501
|
-
message.message_id,
|
|
1502
|
-
"Compaction started.",
|
|
1503
|
-
);
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
async function handleStatusCommand(
|
|
1507
|
-
message: TelegramMessage,
|
|
1508
|
-
ctx: ExtensionContext,
|
|
1509
|
-
): Promise<void> {
|
|
1510
|
-
enqueueTelegramControlItem(
|
|
1511
|
-
createTelegramControlItem(
|
|
1512
|
-
message.chat.id,
|
|
1513
|
-
message.message_id,
|
|
1514
|
-
"status",
|
|
1515
|
-
"⚡ status",
|
|
1516
|
-
async (controlCtx) => {
|
|
1517
|
-
await sendStatusMessage(
|
|
1518
|
-
message.chat.id,
|
|
1519
|
-
message.message_id,
|
|
1520
|
-
controlCtx,
|
|
1521
|
-
);
|
|
1522
|
-
},
|
|
1523
|
-
),
|
|
1524
|
-
ctx,
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
async function handleModelCommand(
|
|
1529
|
-
message: TelegramMessage,
|
|
1530
|
-
ctx: ExtensionContext,
|
|
1531
|
-
): Promise<void> {
|
|
1532
|
-
enqueueTelegramControlItem(
|
|
1533
|
-
createTelegramControlItem(
|
|
1534
|
-
message.chat.id,
|
|
1535
|
-
message.message_id,
|
|
1536
|
-
"model",
|
|
1537
|
-
"⚡ model",
|
|
1538
|
-
async (controlCtx) => {
|
|
1539
|
-
await openModelMenu(message.chat.id, message.message_id, controlCtx);
|
|
1540
|
-
},
|
|
1541
|
-
),
|
|
1542
|
-
ctx,
|
|
1543
|
-
);
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
async function handleHelpCommand(
|
|
1547
|
-
message: TelegramMessage,
|
|
1548
|
-
commandName: string,
|
|
1549
|
-
ctx: ExtensionContext,
|
|
1550
|
-
): Promise<void> {
|
|
1551
|
-
let helpText =
|
|
1552
|
-
"Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop.";
|
|
1553
|
-
if (commandName === "start") {
|
|
1554
|
-
try {
|
|
1555
|
-
await registerTelegramBotCommands();
|
|
1556
|
-
} catch (error) {
|
|
1557
|
-
const errorMessage =
|
|
1558
|
-
error instanceof Error ? error.message : String(error);
|
|
1559
|
-
helpText += `\n\nWarning: failed to register bot commands menu: ${errorMessage}`;
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
await sendTextReply(message.chat.id, message.message_id, helpText);
|
|
1563
|
-
if (config.allowedUserId === undefined && message.from) {
|
|
1564
|
-
config.allowedUserId = message.from.id;
|
|
1565
|
-
await writeTelegramConfig(AGENT_DIR, CONFIG_PATH, config);
|
|
1566
|
-
updateStatus(ctx);
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
async function handleTelegramCommand(
|
|
1571
|
-
commandName: string | undefined,
|
|
1572
|
-
message: TelegramMessage,
|
|
1573
|
-
ctx: ExtensionContext,
|
|
1574
|
-
): Promise<boolean> {
|
|
1575
|
-
if (!commandName) return false;
|
|
1576
|
-
const handlers: Partial<Record<string, () => Promise<void>>> = {
|
|
1577
|
-
stop: () => handleStopCommand(message, ctx),
|
|
1578
|
-
compact: () => handleCompactCommand(message, ctx),
|
|
1579
|
-
status: () => handleStatusCommand(message, ctx),
|
|
1580
|
-
model: () => handleModelCommand(message, ctx),
|
|
1581
|
-
help: () => handleHelpCommand(message, commandName, ctx),
|
|
1582
|
-
start: () => handleHelpCommand(message, commandName, ctx),
|
|
1583
|
-
};
|
|
1584
|
-
const handler = handlers[commandName];
|
|
1585
|
-
if (!handler) return false;
|
|
1586
|
-
await handler();
|
|
1587
|
-
return true;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
async function enqueueTelegramTurn(
|
|
1591
|
-
messages: TelegramMessage[],
|
|
1592
|
-
ctx: ExtensionContext,
|
|
1593
|
-
): Promise<void> {
|
|
1594
|
-
const historyResult = preserveQueuedTurnsAsHistory
|
|
1595
|
-
? partitionTelegramQueueItemsForHistory(queuedTelegramItems)
|
|
1596
|
-
: { historyTurns: [], remainingItems: queuedTelegramItems };
|
|
1597
|
-
queuedTelegramItems = historyResult.remainingItems;
|
|
1598
|
-
preserveQueuedTurnsAsHistory = false;
|
|
1599
|
-
const turn = await createTelegramTurn(messages, historyResult.historyTurns);
|
|
1600
|
-
queuedTelegramItems.push(turn);
|
|
1601
|
-
updateStatus(ctx);
|
|
1602
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
async function dispatchAuthorizedTelegramMessages(
|
|
1606
|
-
messages: TelegramMessage[],
|
|
1607
|
-
ctx: ExtensionContext,
|
|
1608
|
-
): Promise<void> {
|
|
1609
|
-
const firstMessage = messages[0];
|
|
1610
|
-
if (!firstMessage) return;
|
|
1611
|
-
const rawText = extractFirstTelegramMessageText(messages);
|
|
1612
|
-
const commandName = parseTelegramCommand(rawText)?.name;
|
|
1613
|
-
const handled = await handleTelegramCommand(commandName, firstMessage, ctx);
|
|
1614
|
-
if (handled) return;
|
|
1615
|
-
await enqueueTelegramTurn(messages, ctx);
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
async function handleAuthorizedTelegramMessage(
|
|
1619
|
-
message: TelegramMessage,
|
|
1620
|
-
ctx: ExtensionContext,
|
|
1621
|
-
): Promise<void> {
|
|
1622
|
-
if (message.media_group_id) {
|
|
1623
|
-
const key = `${message.chat.id}:${message.media_group_id}`;
|
|
1624
|
-
const existing = mediaGroups.get(key) ?? { messages: [] };
|
|
1625
|
-
existing.messages.push(message);
|
|
1626
|
-
if (existing.flushTimer) clearTimeout(existing.flushTimer);
|
|
1627
|
-
existing.flushTimer = setTimeout(() => {
|
|
1628
|
-
const state = mediaGroups.get(key);
|
|
1629
|
-
mediaGroups.delete(key);
|
|
1630
|
-
if (!state) return;
|
|
1631
|
-
void dispatchAuthorizedTelegramMessages(state.messages, ctx);
|
|
1632
|
-
}, TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS);
|
|
1633
|
-
mediaGroups.set(key, existing);
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
await dispatchAuthorizedTelegramMessages([message], ctx);
|
|
1638
|
-
}
|
|
174
|
+
const menuActions = Menu.createTelegramMenuActionRuntimeWithStateBuilder<
|
|
175
|
+
ActivePiModel,
|
|
176
|
+
Pi.ExtensionContext
|
|
177
|
+
>({
|
|
178
|
+
runtime: modelMenuRuntime,
|
|
179
|
+
createSettingsManager: Pi.createSettingsManager,
|
|
180
|
+
getActiveModel: currentModelRuntime.get,
|
|
181
|
+
getThinkingLevel: piRuntime.getThinkingLevel,
|
|
182
|
+
buildStatusHtml: Status.createTelegramStatusHtmlBuilder({
|
|
183
|
+
getActiveModel: currentModelRuntime.get,
|
|
184
|
+
}),
|
|
185
|
+
storeModelMenuState: modelMenuRuntime.storeState,
|
|
186
|
+
isIdle: Pi.isExtensionContextIdle,
|
|
187
|
+
canOfferInFlightModelSwitch: modelSwitchController.canOfferInFlightSwitch,
|
|
188
|
+
sendTextReply,
|
|
189
|
+
editInteractiveMessage,
|
|
190
|
+
sendInteractiveMessage,
|
|
191
|
+
});
|
|
1639
192
|
|
|
1640
|
-
|
|
1641
|
-
userId: number,
|
|
1642
|
-
ctx: ExtensionContext,
|
|
1643
|
-
): Promise<boolean> {
|
|
1644
|
-
const authorization = getTelegramAuthorizationState(
|
|
1645
|
-
userId,
|
|
1646
|
-
config.allowedUserId,
|
|
1647
|
-
);
|
|
1648
|
-
if (authorization.kind !== "pair") return false;
|
|
1649
|
-
config.allowedUserId = authorization.userId;
|
|
1650
|
-
await writeTelegramConfig(AGENT_DIR, CONFIG_PATH, config);
|
|
1651
|
-
updateStatus(ctx);
|
|
1652
|
-
return true;
|
|
1653
|
-
}
|
|
193
|
+
// --- Polling ---
|
|
1654
194
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
195
|
+
const pollingRuntime = Polling.createTelegramPollingControllerRuntime<
|
|
196
|
+
Api.TelegramUpdate,
|
|
197
|
+
Pi.ExtensionContext
|
|
198
|
+
>({
|
|
199
|
+
state: pollingControllerState,
|
|
200
|
+
getConfig: configStore.get,
|
|
201
|
+
hasBotToken: configStore.hasBotToken,
|
|
202
|
+
deleteWebhook,
|
|
203
|
+
getUpdates,
|
|
204
|
+
persistConfig: configStore.persist,
|
|
205
|
+
handleUpdate: Updates.createTelegramPairedUpdateRuntime<
|
|
206
|
+
Pi.ExtensionContext,
|
|
207
|
+
Api.TelegramUpdate
|
|
208
|
+
>({
|
|
209
|
+
getAllowedUserId: configStore.getAllowedUserId,
|
|
210
|
+
setAllowedUserId: configStore.setAllowedUserId,
|
|
211
|
+
persistConfig: configStore.persist,
|
|
212
|
+
updateStatus,
|
|
213
|
+
removePendingMediaGroupMessages: mediaGroupRuntime.removeMessages,
|
|
214
|
+
removeQueuedTelegramTurnsByMessageIds:
|
|
215
|
+
queueMutationRuntime.removeByMessageIds,
|
|
216
|
+
clearQueuedTelegramTurnPriorityByMessageId:
|
|
217
|
+
queueMutationRuntime.clearPriorityByMessageId,
|
|
218
|
+
prioritizeQueuedTelegramTurnByMessageId:
|
|
219
|
+
queueMutationRuntime.prioritizeByMessageId,
|
|
1673
220
|
answerCallbackQuery,
|
|
1674
|
-
handleAuthorizedTelegramCallbackQuery:
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
221
|
+
handleAuthorizedTelegramCallbackQuery:
|
|
222
|
+
Menu.createTelegramMenuCallbackHandlerForContext<
|
|
223
|
+
Api.TelegramCallbackQuery,
|
|
224
|
+
Pi.ExtensionContext,
|
|
225
|
+
ActivePiModel
|
|
226
|
+
>({
|
|
227
|
+
getStoredModelMenuState: modelMenuRuntime.getState,
|
|
228
|
+
getActiveModel: currentModelRuntime.get,
|
|
229
|
+
getThinkingLevel: piRuntime.getThinkingLevel,
|
|
230
|
+
setThinkingLevel: piRuntime.setThinkingLevel,
|
|
231
|
+
updateStatus,
|
|
232
|
+
updateModelMenuMessage: menuActions.updateModelMenuMessage,
|
|
233
|
+
updateThinkingMenuMessage: menuActions.updateThinkingMenuMessage,
|
|
234
|
+
updateStatusMessage: menuActions.updateStatusMessage,
|
|
235
|
+
answerCallbackQuery,
|
|
236
|
+
isIdle: Pi.isExtensionContextIdle,
|
|
237
|
+
hasActiveTelegramTurn: activeTurnRuntime.has,
|
|
238
|
+
hasAbortHandler: bridgeRuntime.abort.hasHandler,
|
|
239
|
+
getActiveToolExecutions:
|
|
240
|
+
bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
241
|
+
setModel: piRuntime.setModel,
|
|
242
|
+
setCurrentModel: currentModelRuntime.setCurrentModel,
|
|
243
|
+
stagePendingModelSwitch: modelSwitchController.stagePendingSwitch,
|
|
244
|
+
restartInterruptedTelegramTurn:
|
|
245
|
+
modelSwitchController.restartInterruptedTurn,
|
|
246
|
+
}),
|
|
1680
247
|
sendTextReply,
|
|
1681
|
-
handleAuthorizedTelegramMessage:
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
248
|
+
handleAuthorizedTelegramMessage:
|
|
249
|
+
Media.createTelegramMediaGroupDispatchRuntime<
|
|
250
|
+
Api.TelegramMessage,
|
|
251
|
+
Pi.ExtensionContext
|
|
252
|
+
>({
|
|
253
|
+
mediaGroups: mediaGroupRuntime,
|
|
254
|
+
dispatchMessages: Commands.createTelegramCommandOrPromptRuntime<
|
|
255
|
+
Api.TelegramMessage,
|
|
256
|
+
Pi.ExtensionContext
|
|
257
|
+
>({
|
|
258
|
+
extractRawText: Media.extractFirstTelegramMessageText,
|
|
259
|
+
handleCommand: Commands.createTelegramCommandHandlerTargetRuntime<
|
|
260
|
+
Api.TelegramMessage,
|
|
261
|
+
Pi.ExtensionContext
|
|
262
|
+
>({
|
|
263
|
+
hasAbortHandler: bridgeRuntime.abort.hasHandler,
|
|
264
|
+
clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
265
|
+
hasQueuedTelegramItems: telegramQueueStore.hasQueuedItems,
|
|
266
|
+
setPreserveQueuedTurnsAsHistory:
|
|
267
|
+
bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
268
|
+
abortCurrentTurn: bridgeRuntime.abort.abortTurn,
|
|
269
|
+
isIdle: Pi.isExtensionContextIdle,
|
|
270
|
+
hasPendingMessages: Pi.hasExtensionContextPendingMessages,
|
|
271
|
+
hasActiveTelegramTurn: activeTurnRuntime.has,
|
|
272
|
+
hasDispatchPending: bridgeRuntime.lifecycle.hasDispatchPending,
|
|
273
|
+
isCompactionInProgress:
|
|
274
|
+
bridgeRuntime.lifecycle.isCompactionInProgress,
|
|
275
|
+
setCompactionInProgress:
|
|
276
|
+
bridgeRuntime.lifecycle.setCompactionInProgress,
|
|
277
|
+
updateStatus,
|
|
278
|
+
dispatchNextQueuedTelegramTurn,
|
|
279
|
+
compact: Pi.compactExtensionContext,
|
|
280
|
+
allocateItemOrder: bridgeRuntime.queue.allocateItemOrder,
|
|
281
|
+
allocateControlOrder: bridgeRuntime.queue.allocateControlOrder,
|
|
282
|
+
appendControlItem: queueMutationRuntime.append,
|
|
283
|
+
showStatus: menuActions.sendStatusMessage,
|
|
284
|
+
openModelMenu: menuActions.openModelMenu,
|
|
285
|
+
getAllowedUserId: configStore.getAllowedUserId,
|
|
286
|
+
setAllowedUserId: configStore.setAllowedUserId,
|
|
287
|
+
setMyCommands,
|
|
288
|
+
persistConfig: configStore.persist,
|
|
289
|
+
sendTextReply,
|
|
290
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
291
|
+
}),
|
|
292
|
+
enqueueTurn: Queue.createTelegramPromptEnqueueController<
|
|
293
|
+
Api.TelegramMessage,
|
|
294
|
+
Pi.ExtensionContext
|
|
295
|
+
>({
|
|
296
|
+
...telegramQueueStore,
|
|
297
|
+
getPreserveQueuedTurnsAsHistory:
|
|
298
|
+
bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
299
|
+
setPreserveQueuedTurnsAsHistory:
|
|
300
|
+
bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
301
|
+
createTurn:
|
|
302
|
+
Turns.createTelegramPromptTurnRuntimeBuilder<Api.TelegramMessage>(
|
|
303
|
+
{
|
|
304
|
+
allocateQueueOrder: bridgeRuntime.queue.allocateItemOrder,
|
|
305
|
+
downloadFile: downloadTelegramBridgeFile,
|
|
306
|
+
},
|
|
307
|
+
),
|
|
308
|
+
updateStatus,
|
|
309
|
+
dispatchNextQueuedTelegramTurn,
|
|
310
|
+
}).enqueue,
|
|
311
|
+
}).dispatchMessages,
|
|
312
|
+
}).handleMessage,
|
|
313
|
+
handleAuthorizedTelegramEditedMessage:
|
|
314
|
+
Turns.createTelegramQueuedPromptEditRuntime<
|
|
315
|
+
Api.TelegramMessage,
|
|
316
|
+
Pi.ExtensionContext
|
|
317
|
+
>({
|
|
318
|
+
...telegramQueueStore,
|
|
319
|
+
updateStatus,
|
|
320
|
+
}).updateFromEditedMessage,
|
|
321
|
+
}).handleUpdate,
|
|
322
|
+
stopTypingLoop: bridgeRuntime.typing.stop,
|
|
323
|
+
updateStatus,
|
|
324
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
325
|
+
});
|
|
1753
326
|
|
|
1754
327
|
// --- Extension Registration ---
|
|
1755
328
|
|
|
1756
|
-
registerTelegramAttachmentTool(pi, {
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
statPath: stat,
|
|
329
|
+
Registration.registerTelegramAttachmentTool(pi, {
|
|
330
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
331
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
1760
332
|
});
|
|
1761
333
|
|
|
1762
|
-
registerTelegramCommands(pi, {
|
|
1763
|
-
promptForConfig
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
},
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
stopPolling,
|
|
334
|
+
Registration.registerTelegramCommands(pi, {
|
|
335
|
+
promptForConfig: Setup.createTelegramSetupPromptRuntime({
|
|
336
|
+
getConfig: configStore.get,
|
|
337
|
+
setConfig: configStore.set,
|
|
338
|
+
setupGuard: bridgeRuntime.setup,
|
|
339
|
+
getMe: Api.fetchTelegramBotIdentity,
|
|
340
|
+
persistConfig: configStore.persist,
|
|
341
|
+
startPolling: pollingRuntime.start,
|
|
342
|
+
updateStatus,
|
|
343
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
344
|
+
}),
|
|
345
|
+
getStatusLines,
|
|
346
|
+
reloadConfig: configStore.load,
|
|
347
|
+
hasBotToken: configStore.hasBotToken,
|
|
348
|
+
startPolling: pollingRuntime.start,
|
|
349
|
+
stopPolling: pollingRuntime.stop,
|
|
1779
350
|
updateStatus,
|
|
1780
351
|
});
|
|
1781
352
|
|
|
1782
353
|
// --- Lifecycle Hooks ---
|
|
1783
354
|
|
|
1784
|
-
registerTelegramLifecycleHooks(pi, {
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
if (startPlan.activeTurn) {
|
|
1859
|
-
activeTelegramTurn = { ...startPlan.activeTurn };
|
|
1860
|
-
previewState = createPreviewState();
|
|
1861
|
-
startTypingLoop(ctx);
|
|
1862
|
-
}
|
|
1863
|
-
updateStatus(ctx);
|
|
1864
|
-
},
|
|
1865
|
-
onToolExecutionStart: () => {
|
|
1866
|
-
activeTelegramToolExecutions = getNextTelegramToolExecutionCount({
|
|
1867
|
-
hasActiveTurn: !!activeTelegramTurn,
|
|
1868
|
-
currentCount: activeTelegramToolExecutions,
|
|
1869
|
-
event: "start",
|
|
1870
|
-
});
|
|
1871
|
-
},
|
|
1872
|
-
onToolExecutionEnd: (_event, ctx) => {
|
|
1873
|
-
activeTelegramToolExecutions = getNextTelegramToolExecutionCount({
|
|
1874
|
-
hasActiveTurn: !!activeTelegramTurn,
|
|
1875
|
-
currentCount: activeTelegramToolExecutions,
|
|
1876
|
-
event: "end",
|
|
1877
|
-
});
|
|
1878
|
-
if (!activeTelegramTurn) return;
|
|
1879
|
-
triggerPendingTelegramModelSwitchAbort(ctx);
|
|
1880
|
-
},
|
|
1881
|
-
onMessageStart: async (event, _ctx) => {
|
|
1882
|
-
const nextEvent = event as { message: AgentMessage };
|
|
1883
|
-
if (!activeTelegramTurn || !isAssistantMessage(nextEvent.message)) return;
|
|
1884
|
-
if (
|
|
1885
|
-
previewState &&
|
|
1886
|
-
(previewState.pendingText.trim().length > 0 ||
|
|
1887
|
-
previewState.lastSentText.trim().length > 0)
|
|
1888
|
-
) {
|
|
1889
|
-
const previousText = previewState.pendingText.trim();
|
|
1890
|
-
if (previousText.length > 0) {
|
|
1891
|
-
await finalizeMarkdownPreview(
|
|
1892
|
-
activeTelegramTurn.chatId,
|
|
1893
|
-
previousText,
|
|
1894
|
-
);
|
|
1895
|
-
} else {
|
|
1896
|
-
await finalizePreview(activeTelegramTurn.chatId);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
previewState = createPreviewState();
|
|
1900
|
-
},
|
|
1901
|
-
onMessageUpdate: async (event, _ctx) => {
|
|
1902
|
-
const nextEvent = event as { message: AgentMessage };
|
|
1903
|
-
if (!activeTelegramTurn || !isAssistantMessage(nextEvent.message)) return;
|
|
1904
|
-
if (!previewState) {
|
|
1905
|
-
previewState = createPreviewState();
|
|
1906
|
-
}
|
|
1907
|
-
previewState.pendingText = getMessageText(nextEvent.message);
|
|
1908
|
-
schedulePreviewFlush(activeTelegramTurn.chatId);
|
|
1909
|
-
},
|
|
1910
|
-
onAgentEnd: async (event, ctx) => {
|
|
1911
|
-
const turn = activeTelegramTurn;
|
|
1912
|
-
currentAbort = undefined;
|
|
1913
|
-
stopTypingLoop();
|
|
1914
|
-
activeTelegramTurn = undefined;
|
|
1915
|
-
activeTelegramToolExecutions = 0;
|
|
1916
|
-
pendingTelegramModelSwitch = undefined;
|
|
1917
|
-
telegramTurnDispatchPending = false;
|
|
1918
|
-
updateStatus(ctx);
|
|
1919
|
-
const assistant = turn
|
|
1920
|
-
? extractAssistantText((event as { messages: AgentMessage[] }).messages)
|
|
1921
|
-
: {};
|
|
1922
|
-
const finalText = assistant.text;
|
|
1923
|
-
const endPlan = buildTelegramAgentEndPlan({
|
|
1924
|
-
hasTurn: !!turn,
|
|
1925
|
-
stopReason: assistant.stopReason,
|
|
1926
|
-
hasFinalText: !!finalText,
|
|
1927
|
-
hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
|
|
1928
|
-
preserveQueuedTurnsAsHistory,
|
|
1929
|
-
});
|
|
1930
|
-
if (!turn) {
|
|
1931
|
-
if (endPlan.shouldDispatchNext) {
|
|
1932
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
1933
|
-
}
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
if (endPlan.shouldClearPreview) {
|
|
1937
|
-
await clearPreview(turn.chatId);
|
|
1938
|
-
}
|
|
1939
|
-
if (endPlan.shouldSendErrorMessage) {
|
|
1940
|
-
await sendTextReply(
|
|
1941
|
-
turn.chatId,
|
|
1942
|
-
turn.replyToMessageId,
|
|
1943
|
-
assistant.errorMessage ||
|
|
1944
|
-
"Telegram bridge: pi failed while processing the request.",
|
|
1945
|
-
);
|
|
1946
|
-
if (endPlan.shouldDispatchNext) {
|
|
1947
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
1948
|
-
}
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
if (previewState) {
|
|
1952
|
-
previewState.pendingText = finalText ?? previewState.pendingText;
|
|
1953
|
-
}
|
|
1954
|
-
if (endPlan.kind === "text" && finalText) {
|
|
1955
|
-
const finalized = await finalizeMarkdownPreview(turn.chatId, finalText);
|
|
1956
|
-
if (!finalized) {
|
|
1957
|
-
await clearPreview(turn.chatId);
|
|
1958
|
-
await sendMarkdownReply(
|
|
1959
|
-
turn.chatId,
|
|
1960
|
-
turn.replyToMessageId,
|
|
1961
|
-
finalText,
|
|
1962
|
-
);
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
if (endPlan.shouldSendAttachmentNotice) {
|
|
1966
|
-
await sendTextReply(
|
|
1967
|
-
turn.chatId,
|
|
1968
|
-
turn.replyToMessageId,
|
|
1969
|
-
"Attached requested file(s).",
|
|
1970
|
-
);
|
|
1971
|
-
}
|
|
1972
|
-
await sendQueuedAttachments(turn);
|
|
1973
|
-
if (endPlan.shouldDispatchNext) {
|
|
1974
|
-
dispatchNextQueuedTelegramTurn(ctx);
|
|
1975
|
-
}
|
|
1976
|
-
},
|
|
355
|
+
Registration.registerTelegramLifecycleHooks(pi, {
|
|
356
|
+
...Queue.createTelegramSessionLifecycleRuntime<
|
|
357
|
+
Pi.ExtensionContext,
|
|
358
|
+
RuntimeTelegramQueueItem,
|
|
359
|
+
ActivePiModel
|
|
360
|
+
>({
|
|
361
|
+
getCurrentModel: Pi.getExtensionContextModel,
|
|
362
|
+
loadConfig: configStore.load,
|
|
363
|
+
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
364
|
+
setCurrentModel: currentModelRuntime.set,
|
|
365
|
+
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
366
|
+
syncCounters: bridgeRuntime.queue.syncCounters,
|
|
367
|
+
syncFlags: bridgeRuntime.lifecycle.syncFlags,
|
|
368
|
+
prepareTempDir,
|
|
369
|
+
updateStatus,
|
|
370
|
+
clearPendingMediaGroups: mediaGroupRuntime.clear,
|
|
371
|
+
clearModelMenuState: modelMenuRuntime.clear,
|
|
372
|
+
getActiveTurnChatId: activeTurnRuntime.getChatId,
|
|
373
|
+
clearPreview: previewRuntime.clear,
|
|
374
|
+
clearActiveTurn: activeTurnRuntime.clear,
|
|
375
|
+
clearAbort: bridgeRuntime.abort.clearHandler,
|
|
376
|
+
stopPolling: pollingRuntime.stop,
|
|
377
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
378
|
+
}),
|
|
379
|
+
onBeforeAgentStart: Registration.createTelegramBeforeAgentStartHook(),
|
|
380
|
+
onModelSelect: currentModelRuntime.onModelSelect,
|
|
381
|
+
...Queue.createTelegramAgentLifecycleHooks<
|
|
382
|
+
Queue.PendingTelegramTurn,
|
|
383
|
+
Pi.ExtensionContext,
|
|
384
|
+
unknown
|
|
385
|
+
>({
|
|
386
|
+
setAbortHandler: Runtime.createTelegramContextAbortHandlerSetter(
|
|
387
|
+
bridgeRuntime.abort,
|
|
388
|
+
),
|
|
389
|
+
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
390
|
+
hasPendingDispatch: bridgeRuntime.lifecycle.hasDispatchPending,
|
|
391
|
+
hasActiveTurn: activeTurnRuntime.has,
|
|
392
|
+
resetToolExecutions: bridgeRuntime.lifecycle.resetActiveToolExecutions,
|
|
393
|
+
resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
394
|
+
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
395
|
+
clearDispatchPending: bridgeRuntime.lifecycle.clearDispatchPending,
|
|
396
|
+
setActiveTurn: activeTurnRuntime.set,
|
|
397
|
+
createPreviewState: previewRuntime.resetState,
|
|
398
|
+
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
399
|
+
updateStatus,
|
|
400
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
401
|
+
extractAssistant: Replies.extractLatestAssistantMessageText,
|
|
402
|
+
getPreserveQueuedTurnsAsHistory:
|
|
403
|
+
bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
404
|
+
resetRuntimeState: Runtime.createTelegramAgentEndResetter({
|
|
405
|
+
abort: bridgeRuntime.abort,
|
|
406
|
+
typing: bridgeRuntime.typing,
|
|
407
|
+
clearActiveTurn: activeTurnRuntime.clear,
|
|
408
|
+
resetToolExecutions: bridgeRuntime.lifecycle.resetActiveToolExecutions,
|
|
409
|
+
clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
410
|
+
clearDispatchPending: bridgeRuntime.lifecycle.clearDispatchPending,
|
|
411
|
+
}),
|
|
412
|
+
dispatchNextQueuedTelegramTurn,
|
|
413
|
+
clearPreview: previewRuntime.clear,
|
|
414
|
+
setPreviewPendingText: previewRuntime.setPendingText,
|
|
415
|
+
finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
|
|
416
|
+
sendMarkdownReply,
|
|
417
|
+
sendTextReply,
|
|
418
|
+
sendQueuedAttachments: Attachments.createTelegramQueuedAttachmentSender({
|
|
419
|
+
sendMultipart: callMultipart,
|
|
420
|
+
sendTextReply,
|
|
421
|
+
recordRuntimeEvent: runtimeEvents.record,
|
|
422
|
+
}),
|
|
423
|
+
getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
424
|
+
setActiveToolExecutions: bridgeRuntime.lifecycle.setActiveToolExecutions,
|
|
425
|
+
triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
|
|
426
|
+
}),
|
|
427
|
+
onMessageStart: previewRuntime.onMessageStart,
|
|
428
|
+
onMessageUpdate: previewRuntime.onMessageUpdate,
|
|
1977
429
|
});
|
|
1978
430
|
}
|