@llblab/pi-telegram 0.17.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -32
- package/BACKLOG.md +59 -14
- package/CHANGELOG.md +40 -15
- package/README.md +63 -35
- package/docs/README.md +3 -1
- package/docs/architecture.md +55 -23
- package/docs/callback-namespaces.md +1 -1
- package/docs/inbound.md +1 -1
- package/docs/locks.md +0 -2
- package/docs/multi-instance-bus.md +483 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +12 -10
- package/docs/sections.md +2 -2
- package/docs/ui-style.md +76 -0
- package/index.ts +789 -32
- package/lib/bindings.ts +68 -12
- package/lib/bus-api.ts +314 -0
- package/lib/bus-follower.ts +853 -0
- package/lib/bus-leader.ts +915 -0
- package/lib/bus.ts +866 -0
- package/lib/command-templates.ts +9 -11
- package/lib/commands.ts +133 -47
- package/lib/config.ts +53 -5
- package/lib/lifecycle.ts +23 -7
- package/lib/locks.ts +230 -66
- package/lib/media.ts +30 -2
- package/lib/menu-model.ts +48 -17
- package/lib/menu-queue.ts +51 -20
- package/lib/menu-settings.ts +9 -5
- package/lib/menu-status.ts +3 -0
- package/lib/menu-thinking.ts +3 -0
- package/lib/menu.ts +67 -26
- package/lib/outbound-attachments.ts +102 -17
- package/lib/outbound-buttons.ts +6 -2
- package/lib/outbound-voice.ts +31 -11
- package/lib/outbound.ts +6 -4
- package/lib/ownership.ts +119 -0
- package/lib/pi.ts +26 -3
- package/lib/polling.ts +477 -7
- package/lib/preview.ts +141 -88
- package/lib/prompt-templates.ts +3 -3
- package/lib/prompts.ts +80 -30
- package/lib/queue.ts +193 -91
- package/lib/rendering.ts +0 -25
- package/lib/replies.ts +187 -55
- package/lib/routing.ts +1673 -9
- package/lib/runtime-log.ts +123 -0
- package/lib/runtime.ts +84 -12
- package/lib/sections.ts +28 -21
- package/lib/setup.ts +1 -1
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +405 -40
- package/lib/text-groups.ts +5 -1
- package/lib/thread-reconciler.ts +915 -0
- package/lib/threads.ts +2205 -0
- package/lib/turns.ts +48 -3
- package/lib/updates.ts +355 -32
- package/package.json +24 -2
- package/screenshot.png +0 -0
- package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/command-templates.ts
CHANGED
|
@@ -579,15 +579,13 @@ function evaluateCommandTemplateExpression(
|
|
|
579
579
|
): number {
|
|
580
580
|
let index = 0;
|
|
581
581
|
const source = expression.replace(/\s+/g, "");
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
function consume(char: string): boolean {
|
|
582
|
+
const peek = (): string | undefined => source[index];
|
|
583
|
+
const consume = (char: string): boolean => {
|
|
586
584
|
if (peek() !== char) return false;
|
|
587
585
|
index += 1;
|
|
588
586
|
return true;
|
|
589
|
-
}
|
|
590
|
-
|
|
587
|
+
};
|
|
588
|
+
const parsePrimary = (): number => {
|
|
591
589
|
if (consume("(")) {
|
|
592
590
|
const value = parseExpression();
|
|
593
591
|
if (!consume(")"))
|
|
@@ -610,8 +608,8 @@ function evaluateCommandTemplateExpression(
|
|
|
610
608
|
return Number(value);
|
|
611
609
|
}
|
|
612
610
|
throw new Error(`Invalid command template expression: ${expression}`);
|
|
613
|
-
}
|
|
614
|
-
|
|
611
|
+
};
|
|
612
|
+
const parseTerm = (): number => {
|
|
615
613
|
let value = parsePrimary();
|
|
616
614
|
while (true) {
|
|
617
615
|
if (consume("*")) value *= parsePrimary();
|
|
@@ -619,15 +617,15 @@ function evaluateCommandTemplateExpression(
|
|
|
619
617
|
else if (consume("%")) value %= parsePrimary();
|
|
620
618
|
else return value;
|
|
621
619
|
}
|
|
622
|
-
}
|
|
623
|
-
|
|
620
|
+
};
|
|
621
|
+
const parseExpression = (): number => {
|
|
624
622
|
let value = parseTerm();
|
|
625
623
|
while (true) {
|
|
626
624
|
if (consume("+")) value += parseTerm();
|
|
627
625
|
else if (consume("-")) value -= parseTerm();
|
|
628
626
|
else return value;
|
|
629
627
|
}
|
|
630
|
-
}
|
|
628
|
+
};
|
|
631
629
|
const value = parseExpression();
|
|
632
630
|
if (index !== source.length)
|
|
633
631
|
throw new Error(`Invalid command template expression: ${expression}`);
|
package/lib/commands.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { pairTelegramUserIfNeeded } from "./config.ts";
|
|
8
8
|
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
|
|
9
|
+
import type { TelegramBridgeStatusLineOptions } from "./status.ts";
|
|
9
10
|
import {
|
|
10
11
|
createTelegramControlItemBuilder,
|
|
11
12
|
createTelegramControlQueueController,
|
|
@@ -158,6 +159,7 @@ export const TELEGRAM_COMMAND_EMOJI = {
|
|
|
158
159
|
thinking: "🧠",
|
|
159
160
|
compact: "🗜",
|
|
160
161
|
queue: "🔢",
|
|
162
|
+
thread: "🧵",
|
|
161
163
|
next: "⏩",
|
|
162
164
|
continue: "▶️",
|
|
163
165
|
abort: "⏹️",
|
|
@@ -217,13 +219,13 @@ export const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinitio
|
|
|
217
219
|
},
|
|
218
220
|
{
|
|
219
221
|
command: "abort",
|
|
220
|
-
description: formatTelegramBotCommandDescription("abort", "Abort
|
|
222
|
+
description: formatTelegramBotCommandDescription("abort", "Abort Pi"),
|
|
221
223
|
},
|
|
222
224
|
{
|
|
223
225
|
command: "stop",
|
|
224
226
|
description: formatTelegramBotCommandDescription(
|
|
225
227
|
"stop",
|
|
226
|
-
"Abort
|
|
228
|
+
"Abort Pi & Clear queue",
|
|
227
229
|
),
|
|
228
230
|
},
|
|
229
231
|
];
|
|
@@ -282,6 +284,7 @@ export function createTelegramBotCommandRegistrar(
|
|
|
282
284
|
|
|
283
285
|
export interface TelegramBridgeCommandStartPollingOptions {
|
|
284
286
|
force?: boolean;
|
|
287
|
+
forceFreshLeaderThread?: boolean;
|
|
285
288
|
}
|
|
286
289
|
|
|
287
290
|
export interface TelegramBridgeCommandStartPollingResult {
|
|
@@ -293,7 +296,7 @@ export interface TelegramBridgeCommandStartPollingResult {
|
|
|
293
296
|
|
|
294
297
|
export interface TelegramBridgeCommandRegistrationDeps {
|
|
295
298
|
promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
296
|
-
getStatusLines: () => string[];
|
|
299
|
+
getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
|
|
297
300
|
reloadConfig: () => Promise<void>;
|
|
298
301
|
hasBotToken: () => boolean;
|
|
299
302
|
startPolling: (
|
|
@@ -319,7 +322,7 @@ function formatTelegramTakeoverPrompt(
|
|
|
319
322
|
const action = theme.fg("warning", "move singleton lock here?");
|
|
320
323
|
const from = theme.fg("muted", "from:");
|
|
321
324
|
const to = theme.fg("muted", "to:");
|
|
322
|
-
const source = owner ?? "another
|
|
325
|
+
const source = owner ?? "another Pi instance";
|
|
323
326
|
return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
|
|
324
327
|
}
|
|
325
328
|
|
|
@@ -335,19 +338,24 @@ export function registerTelegramBridgeCommands(
|
|
|
335
338
|
});
|
|
336
339
|
pi.registerCommand("telegram-status", {
|
|
337
340
|
description: "Show Telegram bridge status",
|
|
338
|
-
handler: async (
|
|
339
|
-
|
|
341
|
+
handler: async (args, ctx) => {
|
|
342
|
+
const verbose = /(^|\s)(--debug|debug|--verbose|verbose)(\s|$)/i.test(
|
|
343
|
+
args,
|
|
344
|
+
);
|
|
345
|
+
ctx.ui.notify(deps.getStatusLines({ verbose }).join("\n"), "info");
|
|
340
346
|
},
|
|
341
347
|
});
|
|
342
348
|
pi.registerCommand("telegram-connect", {
|
|
343
|
-
description: "Start the Telegram bridge in this
|
|
349
|
+
description: "Start the Telegram bridge in this Pi session",
|
|
344
350
|
handler: async (_args, ctx) => {
|
|
345
351
|
await deps.reloadConfig();
|
|
346
352
|
if (!deps.hasBotToken()) {
|
|
347
353
|
await deps.promptForConfig(ctx);
|
|
348
354
|
return;
|
|
349
355
|
}
|
|
350
|
-
let result = await deps.startPolling(ctx
|
|
356
|
+
let result = await deps.startPolling(ctx, {
|
|
357
|
+
forceFreshLeaderThread: true,
|
|
358
|
+
});
|
|
351
359
|
if (result && !result.ok && result.canTakeover) {
|
|
352
360
|
const confirmed = await ctx.ui.confirm(
|
|
353
361
|
formatTelegramTakeoverTitle(ctx),
|
|
@@ -358,7 +366,10 @@ export function registerTelegramBridgeCommands(
|
|
|
358
366
|
deps.updateStatus(ctx);
|
|
359
367
|
return;
|
|
360
368
|
}
|
|
361
|
-
result = await deps.startPolling(ctx, {
|
|
369
|
+
result = await deps.startPolling(ctx, {
|
|
370
|
+
force: true,
|
|
371
|
+
forceFreshLeaderThread: true,
|
|
372
|
+
});
|
|
362
373
|
}
|
|
363
374
|
if (result?.message) {
|
|
364
375
|
ctx.ui.notify(result.message, result.ok ? "info" : "warning");
|
|
@@ -367,7 +378,7 @@ export function registerTelegramBridgeCommands(
|
|
|
367
378
|
},
|
|
368
379
|
});
|
|
369
380
|
pi.registerCommand("telegram-disconnect", {
|
|
370
|
-
description: "Stop the Telegram bridge in this
|
|
381
|
+
description: "Stop the Telegram bridge in this Pi session",
|
|
371
382
|
handler: async (_args, ctx) => {
|
|
372
383
|
const message = await deps.stopPolling();
|
|
373
384
|
if (message) ctx.ui.notify(message, "info");
|
|
@@ -496,13 +507,18 @@ export interface TelegramCompactConfirmationDeps {
|
|
|
496
507
|
text: string,
|
|
497
508
|
mode: "markdown" | "html" | "plain",
|
|
498
509
|
replyMarkup: TelegramCompactConfirmationReplyMarkup,
|
|
510
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
499
511
|
) => Promise<number | undefined>;
|
|
500
512
|
}
|
|
501
513
|
|
|
502
514
|
export interface TelegramCompactConfirmationCallbackQuery {
|
|
503
515
|
id: string;
|
|
504
516
|
data?: string;
|
|
505
|
-
message?: {
|
|
517
|
+
message?: {
|
|
518
|
+
chat?: { id?: number };
|
|
519
|
+
message_id?: number;
|
|
520
|
+
message_thread_id?: number;
|
|
521
|
+
};
|
|
506
522
|
}
|
|
507
523
|
|
|
508
524
|
export interface TelegramCompactConfirmationCallbackDeps<TContext> {
|
|
@@ -522,6 +538,7 @@ export interface TelegramCompactConfirmationCallbackDeps<TContext> {
|
|
|
522
538
|
ctx: TContext,
|
|
523
539
|
chatId: number,
|
|
524
540
|
replyToMessageId: number,
|
|
541
|
+
target?: { chatId: number; threadId?: number },
|
|
525
542
|
) => Promise<void>;
|
|
526
543
|
}
|
|
527
544
|
|
|
@@ -529,16 +546,24 @@ export type TelegramControlCommandType =
|
|
|
529
546
|
PendingTelegramControlItem<unknown>["controlType"];
|
|
530
547
|
|
|
531
548
|
export interface TelegramCommandRuntimeMessage {
|
|
532
|
-
chat: { id: number };
|
|
549
|
+
chat: { id: number; type?: string; title?: string };
|
|
533
550
|
message_id: number;
|
|
551
|
+
message_thread_id?: number;
|
|
534
552
|
from?: { id?: number };
|
|
535
553
|
}
|
|
536
554
|
|
|
537
555
|
export interface TelegramCommandMessageTarget {
|
|
538
556
|
chatId: number;
|
|
557
|
+
threadId?: number;
|
|
539
558
|
replyToMessageId: number;
|
|
540
559
|
}
|
|
541
560
|
|
|
561
|
+
function canPairTelegramUserFromCommandMessage(
|
|
562
|
+
message: TelegramCommandRuntimeMessage,
|
|
563
|
+
): boolean {
|
|
564
|
+
return message.chat.type === undefined || message.chat.type === "private";
|
|
565
|
+
}
|
|
566
|
+
|
|
542
567
|
export interface TelegramCommandTargetRuntimeDeps<TContext> {
|
|
543
568
|
enqueueControlItem: (
|
|
544
569
|
target: TelegramCommandMessageTarget,
|
|
@@ -551,21 +576,25 @@ export interface TelegramCommandTargetRuntimeDeps<TContext> {
|
|
|
551
576
|
chatId: number,
|
|
552
577
|
replyToMessageId: number,
|
|
553
578
|
ctx: TContext,
|
|
579
|
+
threadId?: number,
|
|
554
580
|
) => Promise<void>;
|
|
555
581
|
openModelMenu: (
|
|
556
582
|
chatId: number,
|
|
557
583
|
replyToMessageId: number,
|
|
558
584
|
ctx: TContext,
|
|
585
|
+
threadId?: number,
|
|
559
586
|
) => Promise<void>;
|
|
560
587
|
openSettingsMenu?: (
|
|
561
588
|
chatId: number,
|
|
562
589
|
replyToMessageId: number,
|
|
563
590
|
ctx: TContext,
|
|
591
|
+
threadId?: number,
|
|
564
592
|
) => Promise<void>;
|
|
565
593
|
sendTextReply: (
|
|
566
594
|
chatId: number,
|
|
567
595
|
replyToMessageId: number,
|
|
568
596
|
text: string,
|
|
597
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
569
598
|
) => Promise<unknown>;
|
|
570
599
|
}
|
|
571
600
|
|
|
@@ -591,6 +620,10 @@ export function getTelegramCommandMessageTarget(
|
|
|
591
620
|
): TelegramCommandMessageTarget {
|
|
592
621
|
return {
|
|
593
622
|
chatId: message.chat.id,
|
|
623
|
+
threadId:
|
|
624
|
+
typeof message.message_thread_id === "number"
|
|
625
|
+
? message.message_thread_id
|
|
626
|
+
: undefined,
|
|
594
627
|
replyToMessageId: message.message_id,
|
|
595
628
|
};
|
|
596
629
|
}
|
|
@@ -598,6 +631,7 @@ export function getTelegramCommandMessageTarget(
|
|
|
598
631
|
export interface TelegramCommandControlQueueRuntimeDeps<TContext> {
|
|
599
632
|
createControlItem: (options: {
|
|
600
633
|
chatId: number;
|
|
634
|
+
target?: { chatId: number; threadId?: number };
|
|
601
635
|
replyToMessageId: number;
|
|
602
636
|
controlType: TelegramControlCommandType;
|
|
603
637
|
statusSummary: string;
|
|
@@ -626,6 +660,7 @@ export function createTelegramCommandControlQueueRuntime<TContext>(
|
|
|
626
660
|
export function createTelegramCommandControlEnqueueAdapter<TContext>(deps: {
|
|
627
661
|
createControlItem: (options: {
|
|
628
662
|
chatId: number;
|
|
663
|
+
target?: { chatId: number; threadId?: number };
|
|
629
664
|
replyToMessageId: number;
|
|
630
665
|
controlType: TelegramControlCommandType;
|
|
631
666
|
statusSummary: string;
|
|
@@ -690,11 +725,21 @@ export function createTelegramCommandTargetRuntime<
|
|
|
690
725
|
},
|
|
691
726
|
showStatus: (message, ctx) => {
|
|
692
727
|
const target = getTelegramCommandMessageTarget(message);
|
|
693
|
-
return deps.showStatus(
|
|
728
|
+
return deps.showStatus(
|
|
729
|
+
target.chatId,
|
|
730
|
+
target.replyToMessageId,
|
|
731
|
+
ctx,
|
|
732
|
+
target.threadId,
|
|
733
|
+
);
|
|
694
734
|
},
|
|
695
735
|
openModelMenu: (message, ctx) => {
|
|
696
736
|
const target = getTelegramCommandMessageTarget(message);
|
|
697
|
-
return deps.openModelMenu(
|
|
737
|
+
return deps.openModelMenu(
|
|
738
|
+
target.chatId,
|
|
739
|
+
target.replyToMessageId,
|
|
740
|
+
ctx,
|
|
741
|
+
target.threadId,
|
|
742
|
+
);
|
|
698
743
|
},
|
|
699
744
|
openSettingsMenu: async (message, ctx) => {
|
|
700
745
|
const target = getTelegramCommandMessageTarget(message);
|
|
@@ -703,20 +748,29 @@ export function createTelegramCommandTargetRuntime<
|
|
|
703
748
|
target.chatId,
|
|
704
749
|
target.replyToMessageId,
|
|
705
750
|
"Settings menu is unavailable.",
|
|
751
|
+
{ target },
|
|
706
752
|
);
|
|
707
753
|
return;
|
|
708
754
|
}
|
|
709
|
-
await deps.openSettingsMenu(
|
|
755
|
+
await deps.openSettingsMenu(
|
|
756
|
+
target.chatId,
|
|
757
|
+
target.replyToMessageId,
|
|
758
|
+
ctx,
|
|
759
|
+
target.threadId,
|
|
760
|
+
);
|
|
710
761
|
},
|
|
711
762
|
sendTextReply: async (message, text) => {
|
|
712
763
|
const target = getTelegramCommandMessageTarget(message);
|
|
713
|
-
await deps.sendTextReply(target.chatId, target.replyToMessageId, text
|
|
764
|
+
await deps.sendTextReply(target.chatId, target.replyToMessageId, text, {
|
|
765
|
+
target,
|
|
766
|
+
});
|
|
714
767
|
},
|
|
715
768
|
};
|
|
716
769
|
}
|
|
717
770
|
|
|
718
771
|
export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
|
|
719
772
|
extractRawText: (messages: TMessage[]) => string;
|
|
773
|
+
shouldIgnoreMessages?: (messages: TMessage[]) => boolean;
|
|
720
774
|
handleCommand: (
|
|
721
775
|
commandName: string | undefined,
|
|
722
776
|
message: TMessage,
|
|
@@ -756,7 +810,11 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
756
810
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
757
811
|
dispatch: (ctx: TContext) => void,
|
|
758
812
|
) => void;
|
|
759
|
-
startTypingLoop?: (
|
|
813
|
+
startTypingLoop?: (
|
|
814
|
+
ctx: TContext,
|
|
815
|
+
chatId?: number,
|
|
816
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
817
|
+
) => void;
|
|
760
818
|
stopTypingLoop?: () => void;
|
|
761
819
|
enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
762
820
|
compact: (
|
|
@@ -771,6 +829,10 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
771
829
|
execute: (ctx: TContext) => Promise<void>,
|
|
772
830
|
) => void;
|
|
773
831
|
showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
832
|
+
handleForumBootstrap?: (
|
|
833
|
+
message: TMessage,
|
|
834
|
+
ctx: TContext,
|
|
835
|
+
) => Promise<string | undefined>;
|
|
774
836
|
openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
775
837
|
openThinkingMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
776
838
|
openQueueMenu: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
@@ -785,14 +847,14 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
785
847
|
}
|
|
786
848
|
|
|
787
849
|
export const TELEGRAM_APP_MENU_INTRO_HTML = [
|
|
788
|
-
"<b
|
|
850
|
+
"<b>Pi Telegram</b>",
|
|
789
851
|
"",
|
|
790
852
|
`${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
|
|
791
853
|
`${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
|
|
792
854
|
`${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
|
|
793
855
|
`${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
|
|
794
|
-
`${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort
|
|
795
|
-
`${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort
|
|
856
|
+
`${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
|
|
857
|
+
`${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
|
|
796
858
|
].join("\n");
|
|
797
859
|
|
|
798
860
|
function escapeTelegramCommandMenuHtml(text: string): string {
|
|
@@ -825,15 +887,15 @@ function buildTelegramAppMenuIntroHtml(): string {
|
|
|
825
887
|
const extensionLines = buildTelegramExtensionCommandMenuLines();
|
|
826
888
|
if (extensionLines.length === 0) return TELEGRAM_APP_MENU_INTRO_HTML;
|
|
827
889
|
return [
|
|
828
|
-
"<b
|
|
890
|
+
"<b>Pi Telegram</b>",
|
|
829
891
|
"",
|
|
830
892
|
`${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
|
|
831
893
|
`${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
|
|
832
894
|
...extensionLines,
|
|
833
895
|
`${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
|
|
834
896
|
`${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
|
|
835
|
-
`${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort
|
|
836
|
-
`${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort
|
|
897
|
+
`${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
|
|
898
|
+
`${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
|
|
837
899
|
].join("\n");
|
|
838
900
|
}
|
|
839
901
|
|
|
@@ -852,7 +914,7 @@ export function createTelegramAppMenuHtmlBuilder<TContext>(deps: {
|
|
|
852
914
|
buildStatusHtml: (ctx: TContext) => string;
|
|
853
915
|
getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
|
|
854
916
|
}): (ctx: TContext) => string {
|
|
855
|
-
return
|
|
917
|
+
return (ctx) => {
|
|
856
918
|
return buildTelegramAppMenuHtml(
|
|
857
919
|
deps.buildStatusHtml(ctx),
|
|
858
920
|
deps.getPromptTemplateCommands?.(),
|
|
@@ -979,7 +1041,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
979
1041
|
return;
|
|
980
1042
|
}
|
|
981
1043
|
if (!deps.isIdle()) {
|
|
982
|
-
await deps.sendTextReply("
|
|
1044
|
+
await deps.sendTextReply("Pi is busy. Send /abort or /stop first.");
|
|
983
1045
|
return;
|
|
984
1046
|
}
|
|
985
1047
|
deps.dispatchNextQueuedTurn();
|
|
@@ -1029,14 +1091,17 @@ export function getTelegramCompactConfirmationHtml(): string {
|
|
|
1029
1091
|
}
|
|
1030
1092
|
|
|
1031
1093
|
export async function openTelegramCompactConfirmation(
|
|
1032
|
-
|
|
1094
|
+
target: TelegramCommandMessageTarget,
|
|
1033
1095
|
deps: TelegramCompactConfirmationDeps,
|
|
1034
1096
|
): Promise<void> {
|
|
1035
1097
|
await deps.sendInteractiveMessage(
|
|
1036
|
-
chatId,
|
|
1098
|
+
target.chatId,
|
|
1037
1099
|
getTelegramCompactConfirmationHtml(),
|
|
1038
1100
|
"html",
|
|
1039
1101
|
buildTelegramCompactConfirmationReplyMarkup(),
|
|
1102
|
+
target.threadId !== undefined
|
|
1103
|
+
? { target: { chatId: target.chatId, threadId: target.threadId } }
|
|
1104
|
+
: undefined,
|
|
1040
1105
|
);
|
|
1041
1106
|
}
|
|
1042
1107
|
|
|
@@ -1047,8 +1112,9 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
|
|
|
1047
1112
|
if (query.data !== "compact:confirm" && query.data !== "compact:cancel") {
|
|
1048
1113
|
return false;
|
|
1049
1114
|
}
|
|
1050
|
-
const
|
|
1051
|
-
const
|
|
1115
|
+
const callbackMessage = query.message;
|
|
1116
|
+
const chatId = callbackMessage?.chat?.id;
|
|
1117
|
+
const messageId = callbackMessage?.message_id;
|
|
1052
1118
|
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
1053
1119
|
await deps.answerCallbackQuery(query.id, "Interactive message expired.");
|
|
1054
1120
|
return true;
|
|
@@ -1067,12 +1133,18 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
|
|
|
1067
1133
|
await deps.editInteractiveMessage(
|
|
1068
1134
|
chatId,
|
|
1069
1135
|
messageId,
|
|
1070
|
-
"Compaction started
|
|
1136
|
+
`${formatTelegramCommandEmojiPrefix("compact")}Compaction started.`,
|
|
1071
1137
|
"plain",
|
|
1072
1138
|
{ inline_keyboard: [] },
|
|
1073
1139
|
);
|
|
1074
1140
|
await deps.answerCallbackQuery(query.id);
|
|
1075
|
-
|
|
1141
|
+
const threadId = callbackMessage?.message_thread_id;
|
|
1142
|
+
await deps.runCompact(
|
|
1143
|
+
deps.ctx,
|
|
1144
|
+
chatId,
|
|
1145
|
+
messageId,
|
|
1146
|
+
typeof threadId === "number" ? { chatId, threadId } : { chatId },
|
|
1147
|
+
);
|
|
1076
1148
|
return true;
|
|
1077
1149
|
}
|
|
1078
1150
|
|
|
@@ -1088,25 +1160,23 @@ export async function handleTelegramCompactCommand(
|
|
|
1088
1160
|
deps.isCompactionInProgress()
|
|
1089
1161
|
) {
|
|
1090
1162
|
await deps.sendTextReply(
|
|
1091
|
-
"Cannot compact while
|
|
1163
|
+
"Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
|
|
1092
1164
|
);
|
|
1093
1165
|
return;
|
|
1094
1166
|
}
|
|
1095
1167
|
deps.setCompactionInProgress(true);
|
|
1096
1168
|
deps.updateStatus();
|
|
1097
|
-
|
|
1169
|
+
deps.startTypingLoop?.();
|
|
1098
1170
|
try {
|
|
1099
1171
|
deps.compact({
|
|
1100
1172
|
onComplete: () => {
|
|
1101
|
-
compactionStillInProgress = false;
|
|
1102
1173
|
deps.stopTypingLoop?.();
|
|
1103
1174
|
deps.setCompactionInProgress(false);
|
|
1104
1175
|
deps.updateStatus();
|
|
1105
1176
|
dispatchNextQueuedTelegramTurnAfterCompact(deps);
|
|
1106
|
-
void deps.sendTextReply("Compaction completed.");
|
|
1177
|
+
void deps.sendTextReply("✅ Compaction completed.");
|
|
1107
1178
|
},
|
|
1108
1179
|
onError: (error) => {
|
|
1109
|
-
compactionStillInProgress = false;
|
|
1110
1180
|
deps.stopTypingLoop?.();
|
|
1111
1181
|
deps.setCompactionInProgress(false);
|
|
1112
1182
|
deps.updateStatus();
|
|
@@ -1117,7 +1187,6 @@ export async function handleTelegramCompactCommand(
|
|
|
1117
1187
|
},
|
|
1118
1188
|
});
|
|
1119
1189
|
} catch (error) {
|
|
1120
|
-
compactionStillInProgress = false;
|
|
1121
1190
|
deps.stopTypingLoop?.();
|
|
1122
1191
|
deps.setCompactionInProgress(false);
|
|
1123
1192
|
deps.updateStatus();
|
|
@@ -1127,9 +1196,10 @@ export async function handleTelegramCompactCommand(
|
|
|
1127
1196
|
return;
|
|
1128
1197
|
}
|
|
1129
1198
|
if (!deps.suppressStartNotice) {
|
|
1130
|
-
await deps.sendTextReply(
|
|
1199
|
+
await deps.sendTextReply(
|
|
1200
|
+
`${formatTelegramCommandEmojiPrefix("compact")}Compaction started.`,
|
|
1201
|
+
);
|
|
1131
1202
|
}
|
|
1132
|
-
if (compactionStillInProgress) deps.startTypingLoop?.();
|
|
1133
1203
|
}
|
|
1134
1204
|
|
|
1135
1205
|
function isTelegramStaleContextError(error: unknown): boolean {
|
|
@@ -1279,6 +1349,7 @@ export function createTelegramCommandHandlerTargetRuntime<
|
|
|
1279
1349
|
openThinkingMenu: deps.openThinkingMenu,
|
|
1280
1350
|
openQueueMenu: deps.openQueueMenu,
|
|
1281
1351
|
openSettingsMenu: commandTargetRuntime.openSettingsMenu,
|
|
1352
|
+
handleForumBootstrap: deps.handleForumBootstrap,
|
|
1282
1353
|
getAllowedUserId: deps.getAllowedUserId,
|
|
1283
1354
|
setAllowedUserId: deps.setAllowedUserId,
|
|
1284
1355
|
registerBotCommands: createTelegramBotCommandRegistrar({
|
|
@@ -1294,11 +1365,11 @@ export function createTelegramCommandHandler<
|
|
|
1294
1365
|
TMessage extends TelegramCommandRuntimeMessage,
|
|
1295
1366
|
TContext,
|
|
1296
1367
|
>(deps: TelegramCommandRuntimeDeps<TMessage, TContext>) {
|
|
1297
|
-
return async
|
|
1368
|
+
return async (
|
|
1298
1369
|
commandName: string | undefined,
|
|
1299
1370
|
message: TMessage,
|
|
1300
1371
|
ctx: TContext,
|
|
1301
|
-
): Promise<boolean> {
|
|
1372
|
+
): Promise<boolean> => {
|
|
1302
1373
|
return handleTelegramCommandRuntime(commandName, message, ctx, deps);
|
|
1303
1374
|
};
|
|
1304
1375
|
}
|
|
@@ -1313,6 +1384,7 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
|
|
|
1313
1384
|
): Promise<void> => {
|
|
1314
1385
|
const firstMessage = messages[0];
|
|
1315
1386
|
if (!firstMessage) return;
|
|
1387
|
+
if (deps.shouldIgnoreMessages?.(messages)) return;
|
|
1316
1388
|
const command = parseTelegramCommand(deps.extractRawText(messages));
|
|
1317
1389
|
const handled = await deps.handleCommand(
|
|
1318
1390
|
command?.name,
|
|
@@ -1415,9 +1487,10 @@ async function handleTelegramCommandRuntime<
|
|
|
1415
1487
|
},
|
|
1416
1488
|
handleCompact: async (nextMessage, commandCtx) => {
|
|
1417
1489
|
if (deps.sendInteractiveMessage) {
|
|
1418
|
-
await openTelegramCompactConfirmation(
|
|
1419
|
-
|
|
1420
|
-
|
|
1490
|
+
await openTelegramCompactConfirmation(
|
|
1491
|
+
getTelegramCommandMessageTarget(nextMessage),
|
|
1492
|
+
{ sendInteractiveMessage: deps.sendInteractiveMessage },
|
|
1493
|
+
);
|
|
1421
1494
|
return;
|
|
1422
1495
|
}
|
|
1423
1496
|
await handleTelegramCompactCommand({
|
|
@@ -1440,7 +1513,10 @@ async function handleTelegramCommandRuntime<
|
|
|
1440
1513
|
: undefined,
|
|
1441
1514
|
compact: (callbacks) => deps.compact(commandCtx, callbacks),
|
|
1442
1515
|
startTypingLoop: deps.startTypingLoop
|
|
1443
|
-
? () =>
|
|
1516
|
+
? () =>
|
|
1517
|
+
deps.startTypingLoop?.(commandCtx, nextMessage.chat.id, {
|
|
1518
|
+
target: getTelegramCommandMessageTarget(nextMessage),
|
|
1519
|
+
})
|
|
1444
1520
|
: undefined,
|
|
1445
1521
|
stopTypingLoop: deps.stopTypingLoop,
|
|
1446
1522
|
sendTextReply: sendReplyFor(nextMessage),
|
|
@@ -1465,7 +1541,7 @@ async function handleTelegramCommandRuntime<
|
|
|
1465
1541
|
await deps.openSettingsMenu?.(nextMessage, commandCtx);
|
|
1466
1542
|
}
|
|
1467
1543
|
: undefined,
|
|
1468
|
-
handleHelp: async (nextMessage,
|
|
1544
|
+
handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
|
|
1469
1545
|
try {
|
|
1470
1546
|
await deps.registerBotCommands();
|
|
1471
1547
|
} catch (error) {
|
|
@@ -1475,7 +1551,10 @@ async function handleTelegramCommandRuntime<
|
|
|
1475
1551
|
`Warning: failed to register bot commands menu: ${errorMessage}`,
|
|
1476
1552
|
);
|
|
1477
1553
|
}
|
|
1478
|
-
if (
|
|
1554
|
+
if (
|
|
1555
|
+
nextMessage.from?.id !== undefined &&
|
|
1556
|
+
canPairTelegramUserFromCommandMessage(nextMessage)
|
|
1557
|
+
) {
|
|
1479
1558
|
await pairTelegramUserIfNeeded(nextMessage.from.id, {
|
|
1480
1559
|
allowedUserId: deps.getAllowedUserId(),
|
|
1481
1560
|
ctx: undefined,
|
|
@@ -1484,6 +1563,13 @@ async function handleTelegramCommandRuntime<
|
|
|
1484
1563
|
updateStatus: updateStatusFor(commandCtx),
|
|
1485
1564
|
});
|
|
1486
1565
|
}
|
|
1566
|
+
const forumBootstrapMessage =
|
|
1567
|
+
nextCommandName === "start"
|
|
1568
|
+
? await deps.handleForumBootstrap?.(nextMessage, commandCtx)
|
|
1569
|
+
: undefined;
|
|
1570
|
+
if (forumBootstrapMessage) {
|
|
1571
|
+
await deps.sendTextReply(nextMessage, forumBootstrapMessage);
|
|
1572
|
+
}
|
|
1487
1573
|
await deps.showStatus(nextMessage, commandCtx);
|
|
1488
1574
|
},
|
|
1489
1575
|
},
|
package/lib/config.ts
CHANGED
|
@@ -120,8 +120,33 @@ export function updateTelegramVoiceConfig(
|
|
|
120
120
|
return true;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
type TelegramMutableConfigStore = Pick<
|
|
124
|
+
TelegramConfigStore,
|
|
125
|
+
"get" | "set" | "persist"
|
|
126
|
+
> & {
|
|
127
|
+
load?: () => Promise<void>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function isEmptyTelegramConfig(config: TelegramConfig): boolean {
|
|
131
|
+
return Object.keys(config).length === 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function loadLatestTelegramConfig(
|
|
135
|
+
configStore: TelegramMutableConfigStore,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
if (!configStore.load) return;
|
|
138
|
+
const before = configStore.get();
|
|
139
|
+
await configStore.load();
|
|
140
|
+
if (
|
|
141
|
+
!isEmptyTelegramConfig(before) &&
|
|
142
|
+
isEmptyTelegramConfig(configStore.get())
|
|
143
|
+
) {
|
|
144
|
+
configStore.set(before);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
export function bindGlobalTelegramConfigRuntime(
|
|
124
|
-
configStore:
|
|
149
|
+
configStore: TelegramMutableConfigStore,
|
|
125
150
|
): void {
|
|
126
151
|
setGlobalTelegramConfigRuntime({
|
|
127
152
|
updateVoiceConfig(voice) {
|
|
@@ -224,9 +249,10 @@ export function createTelegramProactivePushChecker(
|
|
|
224
249
|
}
|
|
225
250
|
|
|
226
251
|
export function createTelegramProactivePushSetter(
|
|
227
|
-
configStore:
|
|
252
|
+
configStore: TelegramMutableConfigStore,
|
|
228
253
|
): (enabled: boolean) => Promise<void> {
|
|
229
254
|
return async (enabled) => {
|
|
255
|
+
await loadLatestTelegramConfig(configStore);
|
|
230
256
|
const config = { ...configStore.get(), proactivePush: enabled };
|
|
231
257
|
configStore.set(config);
|
|
232
258
|
await configStore.persist(config);
|
|
@@ -254,9 +280,10 @@ export function createTelegramVoiceReplyModeConfiguredChecker(
|
|
|
254
280
|
}
|
|
255
281
|
|
|
256
282
|
export function createTelegramVoiceReplyModeSetter(
|
|
257
|
-
configStore:
|
|
283
|
+
configStore: TelegramMutableConfigStore,
|
|
258
284
|
): (replyMode: "manual" | "mirror" | "always" | undefined) => Promise<void> {
|
|
259
285
|
return async (replyMode) => {
|
|
286
|
+
await loadLatestTelegramConfig(configStore);
|
|
260
287
|
const current = configStore.get();
|
|
261
288
|
if (replyMode === undefined) {
|
|
262
289
|
const { replyMode: _replyMode, ...remainingVoice } = current.voice ?? {};
|
|
@@ -310,9 +337,10 @@ export function createTelegramTimeInjectionModeGetter(
|
|
|
310
337
|
}
|
|
311
338
|
|
|
312
339
|
export function createTelegramTimeInjectionModeSetter(
|
|
313
|
-
configStore:
|
|
340
|
+
configStore: TelegramMutableConfigStore,
|
|
314
341
|
): (injectionMode: TelegramTimeMode) => Promise<void> {
|
|
315
342
|
return async (injectionMode) => {
|
|
343
|
+
await loadLatestTelegramConfig(configStore);
|
|
316
344
|
const current = configStore.get();
|
|
317
345
|
if (injectionMode === "hidden") {
|
|
318
346
|
const { injectionMode: _injectionMode, ...remainingTime } =
|
|
@@ -333,6 +361,11 @@ export function createTelegramTimeInjectionModeSetter(
|
|
|
333
361
|
};
|
|
334
362
|
}
|
|
335
363
|
|
|
364
|
+
export interface TelegramProactivePushTarget {
|
|
365
|
+
chatId: number;
|
|
366
|
+
threadId?: number;
|
|
367
|
+
}
|
|
368
|
+
|
|
336
369
|
export function createTelegramProactivePushChatIdGetter(deps: {
|
|
337
370
|
getActiveTurnChatId: () => number | undefined;
|
|
338
371
|
getAllowedUserId: () => number | undefined;
|
|
@@ -340,8 +373,23 @@ export function createTelegramProactivePushChatIdGetter(deps: {
|
|
|
340
373
|
return () => deps.getActiveTurnChatId() ?? deps.getAllowedUserId();
|
|
341
374
|
}
|
|
342
375
|
|
|
376
|
+
export function createTelegramProactivePushTargetGetter(deps: {
|
|
377
|
+
getActiveTurnTarget: () => TelegramProactivePushTarget | undefined;
|
|
378
|
+
getAssignedTarget: () => TelegramProactivePushTarget | undefined;
|
|
379
|
+
getAllowedUserId: () => number | undefined;
|
|
380
|
+
}): () => TelegramProactivePushTarget | undefined {
|
|
381
|
+
return () => {
|
|
382
|
+
const activeTarget = deps.getActiveTurnTarget();
|
|
383
|
+
if (activeTarget) return activeTarget;
|
|
384
|
+
const assignedTarget = deps.getAssignedTarget();
|
|
385
|
+
if (assignedTarget) return assignedTarget;
|
|
386
|
+
const chatId = deps.getAllowedUserId();
|
|
387
|
+
return typeof chatId === "number" ? { chatId } : undefined;
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
343
391
|
export function createTelegramConfigControls(
|
|
344
|
-
configStore:
|
|
392
|
+
configStore: TelegramMutableConfigStore,
|
|
345
393
|
) {
|
|
346
394
|
return {
|
|
347
395
|
isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
|