@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/lib/commands.ts
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram command routing helpers
|
|
3
|
+
* Owns slash-command normalization and command side-effect branching behind runtime ports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { pairTelegramUserIfNeeded } from "./config.ts";
|
|
7
|
+
import {
|
|
8
|
+
createTelegramControlItemBuilder,
|
|
9
|
+
createTelegramControlQueueController,
|
|
10
|
+
type PendingTelegramControlItem,
|
|
11
|
+
} from "./queue.ts";
|
|
12
|
+
|
|
13
|
+
export interface ParsedTelegramCommand {
|
|
14
|
+
name: string;
|
|
15
|
+
args: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TelegramBotCommandDefinition {
|
|
19
|
+
command: string;
|
|
20
|
+
description: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const TELEGRAM_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [
|
|
24
|
+
{
|
|
25
|
+
command: "start",
|
|
26
|
+
description: "Show help and pair the Telegram bridge",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
command: "status",
|
|
30
|
+
description: "Show model, usage, cost, and context status",
|
|
31
|
+
},
|
|
32
|
+
{ command: "model", description: "Open the interactive model selector" },
|
|
33
|
+
{ command: "compact", description: "Compact the current pi session" },
|
|
34
|
+
{ command: "stop", description: "Abort the current pi task" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export interface TelegramBotCommandRegistrationDeps {
|
|
38
|
+
setMyCommands: (
|
|
39
|
+
commands: readonly TelegramBotCommandDefinition[],
|
|
40
|
+
) => Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function registerTelegramBotCommands(
|
|
44
|
+
deps: TelegramBotCommandRegistrationDeps,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createTelegramBotCommandRegistrar(
|
|
50
|
+
deps: TelegramBotCommandRegistrationDeps,
|
|
51
|
+
): () => Promise<void> {
|
|
52
|
+
return () => registerTelegramBotCommands(deps);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type TelegramCommandAction =
|
|
56
|
+
| { kind: "ignore"; executionMode: "ignored" }
|
|
57
|
+
| { kind: "stop"; executionMode: "immediate" }
|
|
58
|
+
| { kind: "compact"; executionMode: "immediate" }
|
|
59
|
+
| { kind: "status"; executionMode: "control-queue" }
|
|
60
|
+
| { kind: "model"; executionMode: "control-queue" }
|
|
61
|
+
| {
|
|
62
|
+
kind: "help";
|
|
63
|
+
commandName: "help" | "start";
|
|
64
|
+
executionMode: "immediate";
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type TelegramCommandExecutionMode =
|
|
68
|
+
| "ignored"
|
|
69
|
+
| "immediate"
|
|
70
|
+
| "control-queue";
|
|
71
|
+
|
|
72
|
+
export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
73
|
+
handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
74
|
+
handleCompact: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
75
|
+
handleStatus: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
76
|
+
handleModel: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
77
|
+
handleHelp: (
|
|
78
|
+
message: TMessage,
|
|
79
|
+
commandName: "help" | "start",
|
|
80
|
+
ctx: TContext,
|
|
81
|
+
) => Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TelegramStopCommandDeps {
|
|
85
|
+
hasAbortHandler: () => boolean;
|
|
86
|
+
clearPendingModelSwitch: () => void;
|
|
87
|
+
hasQueuedTelegramItems: () => boolean;
|
|
88
|
+
setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
|
|
89
|
+
abortCurrentTurn: () => void;
|
|
90
|
+
updateStatus: () => void;
|
|
91
|
+
sendTextReply: (text: string) => Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface TelegramRuntimeEventRecorderPort {
|
|
95
|
+
recordRuntimeEvent?: (
|
|
96
|
+
category: string,
|
|
97
|
+
error: unknown,
|
|
98
|
+
details?: Record<string, unknown>,
|
|
99
|
+
) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TelegramCompactCommandDeps
|
|
103
|
+
extends TelegramRuntimeEventRecorderPort {
|
|
104
|
+
isIdle: () => boolean;
|
|
105
|
+
hasPendingMessages: () => boolean;
|
|
106
|
+
hasActiveTelegramTurn: () => boolean;
|
|
107
|
+
hasDispatchPending: () => boolean;
|
|
108
|
+
hasQueuedTelegramItems: () => boolean;
|
|
109
|
+
isCompactionInProgress: () => boolean;
|
|
110
|
+
setCompactionInProgress: (inProgress: boolean) => void;
|
|
111
|
+
updateStatus: () => void;
|
|
112
|
+
dispatchNextQueuedTelegramTurn: () => void;
|
|
113
|
+
compact: (callbacks: {
|
|
114
|
+
onComplete: () => void;
|
|
115
|
+
onError: (error: unknown) => void;
|
|
116
|
+
}) => void;
|
|
117
|
+
sendTextReply: (text: string) => Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface TelegramHelpCommandDeps {
|
|
121
|
+
senderUserId?: number;
|
|
122
|
+
getAllowedUserId: () => number | undefined;
|
|
123
|
+
setAllowedUserId: (userId: number) => void;
|
|
124
|
+
registerBotCommands: () => Promise<void>;
|
|
125
|
+
persistConfig: () => Promise<void>;
|
|
126
|
+
updateStatus: () => void;
|
|
127
|
+
sendTextReply: (text: string) => Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type TelegramControlCommandType =
|
|
131
|
+
PendingTelegramControlItem<unknown>["controlType"];
|
|
132
|
+
|
|
133
|
+
export interface TelegramQueuedControlCommandDeps<TContext> {
|
|
134
|
+
enqueueControlItem: (
|
|
135
|
+
controlType: TelegramControlCommandType,
|
|
136
|
+
statusSummary: string,
|
|
137
|
+
execute: (ctx: TContext) => Promise<void>,
|
|
138
|
+
) => void;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface TelegramCommandRuntimeMessage {
|
|
142
|
+
chat: { id: number };
|
|
143
|
+
message_id: number;
|
|
144
|
+
from?: { id?: number };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface TelegramCommandMessageTarget {
|
|
148
|
+
chatId: number;
|
|
149
|
+
replyToMessageId: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface TelegramCommandTargetRuntimeDeps<TContext> {
|
|
153
|
+
enqueueControlItem: (
|
|
154
|
+
target: TelegramCommandMessageTarget,
|
|
155
|
+
ctx: TContext,
|
|
156
|
+
controlType: TelegramControlCommandType,
|
|
157
|
+
statusSummary: string,
|
|
158
|
+
execute: (ctx: TContext) => Promise<void>,
|
|
159
|
+
) => void;
|
|
160
|
+
showStatus: (
|
|
161
|
+
chatId: number,
|
|
162
|
+
replyToMessageId: number,
|
|
163
|
+
ctx: TContext,
|
|
164
|
+
) => Promise<void>;
|
|
165
|
+
openModelMenu: (
|
|
166
|
+
chatId: number,
|
|
167
|
+
replyToMessageId: number,
|
|
168
|
+
ctx: TContext,
|
|
169
|
+
) => Promise<void>;
|
|
170
|
+
sendTextReply: (
|
|
171
|
+
chatId: number,
|
|
172
|
+
replyToMessageId: number,
|
|
173
|
+
text: string,
|
|
174
|
+
) => Promise<unknown>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface TelegramCommandTargetRuntime<
|
|
178
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
179
|
+
TContext,
|
|
180
|
+
> {
|
|
181
|
+
enqueueControlItem: (
|
|
182
|
+
message: TMessage,
|
|
183
|
+
ctx: TContext,
|
|
184
|
+
controlType: TelegramControlCommandType,
|
|
185
|
+
statusSummary: string,
|
|
186
|
+
execute: (ctx: TContext) => Promise<void>,
|
|
187
|
+
) => void;
|
|
188
|
+
showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
189
|
+
openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
190
|
+
sendTextReply: (message: TMessage, text: string) => Promise<void>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getTelegramCommandMessageTarget(
|
|
194
|
+
message: TelegramCommandRuntimeMessage,
|
|
195
|
+
): TelegramCommandMessageTarget {
|
|
196
|
+
return {
|
|
197
|
+
chatId: message.chat.id,
|
|
198
|
+
replyToMessageId: message.message_id,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface TelegramCommandControlQueueRuntimeDeps<TContext> {
|
|
203
|
+
createControlItem: (options: {
|
|
204
|
+
chatId: number;
|
|
205
|
+
replyToMessageId: number;
|
|
206
|
+
controlType: TelegramControlCommandType;
|
|
207
|
+
statusSummary: string;
|
|
208
|
+
execute: (ctx: TContext) => Promise<void>;
|
|
209
|
+
}) => PendingTelegramControlItem<TContext>;
|
|
210
|
+
appendControlItem: (
|
|
211
|
+
item: PendingTelegramControlItem<TContext>,
|
|
212
|
+
ctx: TContext,
|
|
213
|
+
) => void;
|
|
214
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createTelegramCommandControlQueueRuntime<TContext>(
|
|
218
|
+
deps: TelegramCommandControlQueueRuntimeDeps<TContext>,
|
|
219
|
+
): TelegramCommandTargetRuntimeDeps<TContext>["enqueueControlItem"] {
|
|
220
|
+
const controlQueueController = createTelegramControlQueueController({
|
|
221
|
+
appendControlItem: deps.appendControlItem,
|
|
222
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
223
|
+
});
|
|
224
|
+
return createTelegramCommandControlEnqueueAdapter({
|
|
225
|
+
createControlItem: deps.createControlItem,
|
|
226
|
+
enqueueControlItem: controlQueueController.enqueue,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function createTelegramCommandControlEnqueueAdapter<TContext>(deps: {
|
|
231
|
+
createControlItem: (options: {
|
|
232
|
+
chatId: number;
|
|
233
|
+
replyToMessageId: number;
|
|
234
|
+
controlType: TelegramControlCommandType;
|
|
235
|
+
statusSummary: string;
|
|
236
|
+
execute: (ctx: TContext) => Promise<void>;
|
|
237
|
+
}) => PendingTelegramControlItem<TContext>;
|
|
238
|
+
enqueueControlItem: (
|
|
239
|
+
item: PendingTelegramControlItem<TContext>,
|
|
240
|
+
ctx: TContext,
|
|
241
|
+
) => void;
|
|
242
|
+
}): TelegramCommandTargetRuntimeDeps<TContext>["enqueueControlItem"] {
|
|
243
|
+
return (target, ctx, controlType, statusSummary, execute) => {
|
|
244
|
+
deps.enqueueControlItem(
|
|
245
|
+
deps.createControlItem({
|
|
246
|
+
...target,
|
|
247
|
+
controlType,
|
|
248
|
+
statusSummary,
|
|
249
|
+
execute,
|
|
250
|
+
}),
|
|
251
|
+
ctx,
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type TelegramCommandTargetQueueRuntimeDeps<TContext> =
|
|
257
|
+
TelegramCommandControlQueueRuntimeDeps<TContext> &
|
|
258
|
+
Omit<TelegramCommandTargetRuntimeDeps<TContext>, "enqueueControlItem">;
|
|
259
|
+
|
|
260
|
+
export function createTelegramCommandTargetQueueRuntime<
|
|
261
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
262
|
+
TContext,
|
|
263
|
+
>(
|
|
264
|
+
deps: TelegramCommandTargetQueueRuntimeDeps<TContext>,
|
|
265
|
+
): TelegramCommandTargetRuntime<TMessage, TContext> {
|
|
266
|
+
return createTelegramCommandTargetRuntime({
|
|
267
|
+
enqueueControlItem: createTelegramCommandControlQueueRuntime({
|
|
268
|
+
createControlItem: deps.createControlItem,
|
|
269
|
+
appendControlItem: deps.appendControlItem,
|
|
270
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
271
|
+
}),
|
|
272
|
+
showStatus: deps.showStatus,
|
|
273
|
+
openModelMenu: deps.openModelMenu,
|
|
274
|
+
sendTextReply: deps.sendTextReply,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createTelegramCommandTargetRuntime<
|
|
279
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
280
|
+
TContext,
|
|
281
|
+
>(
|
|
282
|
+
deps: TelegramCommandTargetRuntimeDeps<TContext>,
|
|
283
|
+
): TelegramCommandTargetRuntime<TMessage, TContext> {
|
|
284
|
+
return {
|
|
285
|
+
enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
|
|
286
|
+
deps.enqueueControlItem(
|
|
287
|
+
getTelegramCommandMessageTarget(message),
|
|
288
|
+
ctx,
|
|
289
|
+
controlType,
|
|
290
|
+
statusSummary,
|
|
291
|
+
execute,
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
showStatus: (message, ctx) => {
|
|
295
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
296
|
+
return deps.showStatus(target.chatId, target.replyToMessageId, ctx);
|
|
297
|
+
},
|
|
298
|
+
openModelMenu: (message, ctx) => {
|
|
299
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
300
|
+
return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx);
|
|
301
|
+
},
|
|
302
|
+
sendTextReply: async (message, text) => {
|
|
303
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
304
|
+
await deps.sendTextReply(target.chatId, target.replyToMessageId, text);
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
|
|
310
|
+
extractRawText: (messages: TMessage[]) => string;
|
|
311
|
+
handleCommand: (
|
|
312
|
+
commandName: string | undefined,
|
|
313
|
+
message: TMessage,
|
|
314
|
+
ctx: TContext,
|
|
315
|
+
) => Promise<boolean>;
|
|
316
|
+
enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface TelegramCommandRuntimeDeps<
|
|
320
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
321
|
+
TContext,
|
|
322
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
323
|
+
hasAbortHandler: () => boolean;
|
|
324
|
+
clearPendingModelSwitch: () => void;
|
|
325
|
+
hasQueuedTelegramItems: () => boolean;
|
|
326
|
+
setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
|
|
327
|
+
abortCurrentTurn: () => void;
|
|
328
|
+
isIdle: (ctx: TContext) => boolean;
|
|
329
|
+
hasPendingMessages: (ctx: TContext) => boolean;
|
|
330
|
+
hasActiveTelegramTurn: () => boolean;
|
|
331
|
+
hasDispatchPending: () => boolean;
|
|
332
|
+
isCompactionInProgress: () => boolean;
|
|
333
|
+
setCompactionInProgress: (inProgress: boolean) => void;
|
|
334
|
+
updateStatus: (ctx: TContext) => void;
|
|
335
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
336
|
+
compact: (
|
|
337
|
+
ctx: TContext,
|
|
338
|
+
callbacks: { onComplete: () => void; onError: (error: unknown) => void },
|
|
339
|
+
) => void;
|
|
340
|
+
enqueueControlItem: (
|
|
341
|
+
message: TMessage,
|
|
342
|
+
ctx: TContext,
|
|
343
|
+
controlType: TelegramControlCommandType,
|
|
344
|
+
statusSummary: string,
|
|
345
|
+
execute: (ctx: TContext) => Promise<void>,
|
|
346
|
+
) => void;
|
|
347
|
+
showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
348
|
+
openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
349
|
+
getAllowedUserId: () => number | undefined;
|
|
350
|
+
setAllowedUserId: (userId: number) => void;
|
|
351
|
+
registerBotCommands: () => Promise<void>;
|
|
352
|
+
persistConfig: () => Promise<void>;
|
|
353
|
+
sendTextReply: (message: TMessage, text: string) => Promise<void>;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const TELEGRAM_HELP_TEXT =
|
|
357
|
+
"Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop.";
|
|
358
|
+
|
|
359
|
+
function getTelegramCommandErrorMessage(error: unknown): string {
|
|
360
|
+
return error instanceof Error ? error.message : String(error);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function parseTelegramCommand(
|
|
364
|
+
text: string,
|
|
365
|
+
): ParsedTelegramCommand | undefined {
|
|
366
|
+
const trimmed = text.trim();
|
|
367
|
+
if (!trimmed.startsWith("/")) return undefined;
|
|
368
|
+
const [head, ...tail] = trimmed.split(/\s+/);
|
|
369
|
+
const name = head.slice(1).split("@")[0]?.toLowerCase();
|
|
370
|
+
if (!name) return undefined;
|
|
371
|
+
return { name, args: tail.join(" ").trim() };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function buildTelegramCommandAction(
|
|
375
|
+
commandName: string | undefined,
|
|
376
|
+
): TelegramCommandAction {
|
|
377
|
+
switch (commandName) {
|
|
378
|
+
case "stop":
|
|
379
|
+
return { kind: "stop", executionMode: "immediate" };
|
|
380
|
+
case "compact":
|
|
381
|
+
return { kind: "compact", executionMode: "immediate" };
|
|
382
|
+
case "status":
|
|
383
|
+
return { kind: "status", executionMode: "control-queue" };
|
|
384
|
+
case "model":
|
|
385
|
+
return { kind: "model", executionMode: "control-queue" };
|
|
386
|
+
case "help":
|
|
387
|
+
case "start":
|
|
388
|
+
return { kind: "help", commandName, executionMode: "immediate" };
|
|
389
|
+
default:
|
|
390
|
+
return { kind: "ignore", executionMode: "ignored" };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function getTelegramCommandExecutionMode(
|
|
395
|
+
action: TelegramCommandAction,
|
|
396
|
+
): TelegramCommandExecutionMode {
|
|
397
|
+
return action.executionMode;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function handleTelegramStopCommand(
|
|
401
|
+
deps: TelegramStopCommandDeps,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
if (!deps.hasAbortHandler()) {
|
|
404
|
+
await deps.sendTextReply("No active turn.");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
deps.clearPendingModelSwitch();
|
|
408
|
+
if (deps.hasQueuedTelegramItems()) {
|
|
409
|
+
deps.setPreserveQueuedTurnsAsHistory(true);
|
|
410
|
+
}
|
|
411
|
+
deps.abortCurrentTurn();
|
|
412
|
+
deps.updateStatus();
|
|
413
|
+
await deps.sendTextReply("Aborted current turn.");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function handleTelegramCompactCommand(
|
|
417
|
+
deps: TelegramCompactCommandDeps,
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
if (
|
|
420
|
+
!deps.isIdle() ||
|
|
421
|
+
deps.hasPendingMessages() ||
|
|
422
|
+
deps.hasActiveTelegramTurn() ||
|
|
423
|
+
deps.hasDispatchPending() ||
|
|
424
|
+
deps.hasQueuedTelegramItems() ||
|
|
425
|
+
deps.isCompactionInProgress()
|
|
426
|
+
) {
|
|
427
|
+
await deps.sendTextReply(
|
|
428
|
+
"Cannot compact while pi or the Telegram queue is busy. Wait for queued turns to finish or send /stop first.",
|
|
429
|
+
);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
deps.setCompactionInProgress(true);
|
|
433
|
+
deps.updateStatus();
|
|
434
|
+
try {
|
|
435
|
+
deps.compact({
|
|
436
|
+
onComplete: () => {
|
|
437
|
+
deps.setCompactionInProgress(false);
|
|
438
|
+
deps.updateStatus();
|
|
439
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
440
|
+
void deps.sendTextReply("Compaction completed.");
|
|
441
|
+
},
|
|
442
|
+
onError: (error) => {
|
|
443
|
+
deps.setCompactionInProgress(false);
|
|
444
|
+
deps.updateStatus();
|
|
445
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
446
|
+
deps.recordRuntimeEvent?.("compact", error);
|
|
447
|
+
const errorMessage = getTelegramCommandErrorMessage(error);
|
|
448
|
+
void deps.sendTextReply(`Compaction failed: ${errorMessage}`);
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
deps.setCompactionInProgress(false);
|
|
453
|
+
deps.updateStatus();
|
|
454
|
+
deps.recordRuntimeEvent?.("compact", error);
|
|
455
|
+
const errorMessage = getTelegramCommandErrorMessage(error);
|
|
456
|
+
await deps.sendTextReply(`Compaction failed: ${errorMessage}`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
await deps.sendTextReply("Compaction started.");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function handleTelegramHelpCommand(
|
|
463
|
+
commandName: "help" | "start",
|
|
464
|
+
deps: TelegramHelpCommandDeps,
|
|
465
|
+
): Promise<void> {
|
|
466
|
+
let helpText = TELEGRAM_HELP_TEXT;
|
|
467
|
+
if (commandName === "start") {
|
|
468
|
+
try {
|
|
469
|
+
await deps.registerBotCommands();
|
|
470
|
+
} catch (error) {
|
|
471
|
+
const errorMessage = getTelegramCommandErrorMessage(error);
|
|
472
|
+
helpText += `\n\nWarning: failed to register bot commands menu: ${errorMessage}`;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
await deps.sendTextReply(helpText);
|
|
476
|
+
if (deps.senderUserId === undefined) return;
|
|
477
|
+
await pairTelegramUserIfNeeded(deps.senderUserId, {
|
|
478
|
+
allowedUserId: deps.getAllowedUserId(),
|
|
479
|
+
ctx: undefined,
|
|
480
|
+
setAllowedUserId: deps.setAllowedUserId,
|
|
481
|
+
persistConfig: deps.persistConfig,
|
|
482
|
+
updateStatus: deps.updateStatus,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export async function handleTelegramStatusCommand<TContext>(
|
|
487
|
+
deps: TelegramQueuedControlCommandDeps<TContext> & {
|
|
488
|
+
showStatus: (ctx: TContext) => Promise<void>;
|
|
489
|
+
},
|
|
490
|
+
): Promise<void> {
|
|
491
|
+
deps.enqueueControlItem("status", "⚡ status", deps.showStatus);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function handleTelegramModelCommand<TContext>(
|
|
495
|
+
deps: TelegramQueuedControlCommandDeps<TContext> & {
|
|
496
|
+
openModelMenu: (ctx: TContext) => Promise<void>;
|
|
497
|
+
},
|
|
498
|
+
): Promise<void> {
|
|
499
|
+
deps.enqueueControlItem("model", "⚡ model", deps.openModelMenu);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export async function executeTelegramCommandAction<TMessage, TContext>(
|
|
503
|
+
action: TelegramCommandAction,
|
|
504
|
+
message: TMessage,
|
|
505
|
+
ctx: TContext,
|
|
506
|
+
deps: TelegramCommandActionDeps<TMessage, TContext>,
|
|
507
|
+
): Promise<boolean> {
|
|
508
|
+
switch (action.kind) {
|
|
509
|
+
case "ignore":
|
|
510
|
+
return false;
|
|
511
|
+
case "stop":
|
|
512
|
+
await deps.handleStop(message, ctx);
|
|
513
|
+
return true;
|
|
514
|
+
case "compact":
|
|
515
|
+
await deps.handleCompact(message, ctx);
|
|
516
|
+
return true;
|
|
517
|
+
case "status":
|
|
518
|
+
await deps.handleStatus(message, ctx);
|
|
519
|
+
return true;
|
|
520
|
+
case "model":
|
|
521
|
+
await deps.handleModel(message, ctx);
|
|
522
|
+
return true;
|
|
523
|
+
case "help":
|
|
524
|
+
await deps.handleHelp(message, action.commandName, ctx);
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export interface TelegramCommandHandlerTargetRuntimeDeps<
|
|
530
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
531
|
+
TContext,
|
|
532
|
+
>
|
|
533
|
+
extends
|
|
534
|
+
Omit<
|
|
535
|
+
TelegramCommandRuntimeDeps<TMessage, TContext>,
|
|
536
|
+
| "enqueueControlItem"
|
|
537
|
+
| "showStatus"
|
|
538
|
+
| "openModelMenu"
|
|
539
|
+
| "sendTextReply"
|
|
540
|
+
| "registerBotCommands"
|
|
541
|
+
>,
|
|
542
|
+
Omit<TelegramCommandTargetQueueRuntimeDeps<TContext>, "createControlItem">,
|
|
543
|
+
TelegramBotCommandRegistrationDeps {
|
|
544
|
+
allocateItemOrder: () => number;
|
|
545
|
+
allocateControlOrder: () => number;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function createTelegramCommandHandlerTargetRuntime<
|
|
549
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
550
|
+
TContext,
|
|
551
|
+
>(
|
|
552
|
+
deps: TelegramCommandHandlerTargetRuntimeDeps<TMessage, TContext>,
|
|
553
|
+
): (
|
|
554
|
+
commandName: string | undefined,
|
|
555
|
+
message: TMessage,
|
|
556
|
+
ctx: TContext,
|
|
557
|
+
) => Promise<boolean> {
|
|
558
|
+
const commandTargetRuntime = createTelegramCommandTargetQueueRuntime<
|
|
559
|
+
TMessage,
|
|
560
|
+
TContext
|
|
561
|
+
>({
|
|
562
|
+
createControlItem: createTelegramControlItemBuilder<TContext>({
|
|
563
|
+
allocateItemOrder: deps.allocateItemOrder,
|
|
564
|
+
allocateControlOrder: deps.allocateControlOrder,
|
|
565
|
+
}),
|
|
566
|
+
appendControlItem: deps.appendControlItem,
|
|
567
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
568
|
+
showStatus: deps.showStatus,
|
|
569
|
+
openModelMenu: deps.openModelMenu,
|
|
570
|
+
sendTextReply: deps.sendTextReply,
|
|
571
|
+
});
|
|
572
|
+
return createTelegramCommandHandler({
|
|
573
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
574
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
575
|
+
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
576
|
+
setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
|
|
577
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
578
|
+
isIdle: deps.isIdle,
|
|
579
|
+
hasPendingMessages: deps.hasPendingMessages,
|
|
580
|
+
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
581
|
+
hasDispatchPending: deps.hasDispatchPending,
|
|
582
|
+
isCompactionInProgress: deps.isCompactionInProgress,
|
|
583
|
+
setCompactionInProgress: deps.setCompactionInProgress,
|
|
584
|
+
updateStatus: deps.updateStatus,
|
|
585
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
586
|
+
compact: deps.compact,
|
|
587
|
+
enqueueControlItem: commandTargetRuntime.enqueueControlItem,
|
|
588
|
+
showStatus: commandTargetRuntime.showStatus,
|
|
589
|
+
openModelMenu: commandTargetRuntime.openModelMenu,
|
|
590
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
591
|
+
setAllowedUserId: deps.setAllowedUserId,
|
|
592
|
+
registerBotCommands: createTelegramBotCommandRegistrar({
|
|
593
|
+
setMyCommands: deps.setMyCommands,
|
|
594
|
+
}),
|
|
595
|
+
persistConfig: deps.persistConfig,
|
|
596
|
+
sendTextReply: commandTargetRuntime.sendTextReply,
|
|
597
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function createTelegramCommandHandler<
|
|
602
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
603
|
+
TContext,
|
|
604
|
+
>(deps: TelegramCommandRuntimeDeps<TMessage, TContext>) {
|
|
605
|
+
return async function handleTelegramCommand(
|
|
606
|
+
commandName: string | undefined,
|
|
607
|
+
message: TMessage,
|
|
608
|
+
ctx: TContext,
|
|
609
|
+
): Promise<boolean> {
|
|
610
|
+
return handleTelegramCommandRuntime(commandName, message, ctx, deps);
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
|
|
615
|
+
deps: TelegramCommandOrPromptRuntimeDeps<TMessage, TContext>,
|
|
616
|
+
) {
|
|
617
|
+
return {
|
|
618
|
+
dispatchMessages: async (
|
|
619
|
+
messages: TMessage[],
|
|
620
|
+
ctx: TContext,
|
|
621
|
+
): Promise<void> => {
|
|
622
|
+
const firstMessage = messages[0];
|
|
623
|
+
if (!firstMessage) return;
|
|
624
|
+
const commandName = parseTelegramCommand(
|
|
625
|
+
deps.extractRawText(messages),
|
|
626
|
+
)?.name;
|
|
627
|
+
const handled = await deps.handleCommand(commandName, firstMessage, ctx);
|
|
628
|
+
if (handled) return;
|
|
629
|
+
await deps.enqueueTurn(messages, ctx);
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function handleTelegramCommandRuntime<
|
|
635
|
+
TMessage extends TelegramCommandRuntimeMessage,
|
|
636
|
+
TContext,
|
|
637
|
+
>(
|
|
638
|
+
commandName: string | undefined,
|
|
639
|
+
message: TMessage,
|
|
640
|
+
ctx: TContext,
|
|
641
|
+
deps: TelegramCommandRuntimeDeps<TMessage, TContext>,
|
|
642
|
+
): Promise<boolean> {
|
|
643
|
+
const sendReplyFor = (nextMessage: TMessage) => (text: string) =>
|
|
644
|
+
deps.sendTextReply(nextMessage, text);
|
|
645
|
+
const updateStatusFor = (commandCtx: TContext) => () =>
|
|
646
|
+
deps.updateStatus(commandCtx);
|
|
647
|
+
const enqueueControlFor =
|
|
648
|
+
(nextMessage: TMessage, commandCtx: TContext) =>
|
|
649
|
+
(
|
|
650
|
+
controlType: TelegramControlCommandType,
|
|
651
|
+
statusSummary: string,
|
|
652
|
+
execute: (ctx: TContext) => Promise<void>,
|
|
653
|
+
) => {
|
|
654
|
+
deps.enqueueControlItem(
|
|
655
|
+
nextMessage,
|
|
656
|
+
commandCtx,
|
|
657
|
+
controlType,
|
|
658
|
+
statusSummary,
|
|
659
|
+
execute,
|
|
660
|
+
);
|
|
661
|
+
};
|
|
662
|
+
return executeTelegramCommandAction(
|
|
663
|
+
buildTelegramCommandAction(commandName),
|
|
664
|
+
message,
|
|
665
|
+
ctx,
|
|
666
|
+
{
|
|
667
|
+
handleStop: async (nextMessage, commandCtx) => {
|
|
668
|
+
await handleTelegramStopCommand({
|
|
669
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
670
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
671
|
+
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
672
|
+
setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
|
|
673
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
674
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
675
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
676
|
+
});
|
|
677
|
+
},
|
|
678
|
+
handleCompact: async (nextMessage, commandCtx) => {
|
|
679
|
+
await handleTelegramCompactCommand({
|
|
680
|
+
isIdle: () => deps.isIdle(commandCtx),
|
|
681
|
+
hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
|
|
682
|
+
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
683
|
+
hasDispatchPending: deps.hasDispatchPending,
|
|
684
|
+
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
685
|
+
isCompactionInProgress: deps.isCompactionInProgress,
|
|
686
|
+
setCompactionInProgress: deps.setCompactionInProgress,
|
|
687
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
688
|
+
dispatchNextQueuedTelegramTurn: () =>
|
|
689
|
+
deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
690
|
+
compact: (callbacks) => deps.compact(commandCtx, callbacks),
|
|
691
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
692
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
handleStatus: async (nextMessage, commandCtx) => {
|
|
696
|
+
await handleTelegramStatusCommand<TContext>({
|
|
697
|
+
enqueueControlItem: enqueueControlFor(nextMessage, commandCtx),
|
|
698
|
+
showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx),
|
|
699
|
+
});
|
|
700
|
+
},
|
|
701
|
+
handleModel: async (nextMessage, commandCtx) => {
|
|
702
|
+
await handleTelegramModelCommand<TContext>({
|
|
703
|
+
enqueueControlItem: enqueueControlFor(nextMessage, commandCtx),
|
|
704
|
+
openModelMenu: (controlCtx) =>
|
|
705
|
+
deps.openModelMenu(nextMessage, controlCtx),
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
|
|
709
|
+
await handleTelegramHelpCommand(nextCommandName, {
|
|
710
|
+
senderUserId: nextMessage.from?.id,
|
|
711
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
712
|
+
setAllowedUserId: deps.setAllowedUserId,
|
|
713
|
+
registerBotCommands: deps.registerBotCommands,
|
|
714
|
+
persistConfig: deps.persistConfig,
|
|
715
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
716
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
717
|
+
});
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
}
|