@llblab/pi-telegram 0.16.5 → 0.17.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 +14 -8
- package/BACKLOG.md +8 -3
- package/CHANGELOG.md +21 -0
- package/README.md +9 -5
- package/docs/README.md +2 -1
- package/docs/architecture.md +14 -12
- package/docs/outbound.md +2 -2
- package/docs/public-api.md +3 -0
- package/docs/sections.md +4 -2
- package/docs/telegram-bot-api-rich-messages.md +890 -0
- package/docs/ui-style.md +1 -1
- package/index.ts +11 -2
- package/lib/commands.ts +2 -2
- package/lib/keyboard.ts +39 -0
- package/lib/lifecycle.ts +6 -0
- package/lib/menu-model.ts +3 -3
- package/lib/menu-settings.ts +2 -2
- package/lib/menu.ts +4 -4
- package/lib/outbound-attachments.ts +1 -0
- package/lib/outbound-buttons.ts +1 -0
- package/lib/outbound-voice.ts +2 -0
- package/lib/polling.ts +21 -8
- package/lib/preview.ts +152 -196
- package/lib/prompts.ts +3 -3
- package/lib/queue.ts +1 -1
- package/lib/rendering.ts +3 -349
- package/lib/replies.ts +199 -37
- package/lib/routing.ts +2 -2
- package/lib/sections.ts +15 -18
- package/lib/telegram-api.ts +103 -15
- package/lib/updates.ts +2 -1
- package/package.json +1 -1
package/lib/rendering.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram
|
|
2
|
+
* Telegram UI/compat rendering helpers
|
|
3
3
|
* Zones: telegram rendering, shared text utils
|
|
4
|
-
* Converts
|
|
4
|
+
* Converts bridge-owned UI/status/menu/interactive text into Telegram-safe plain text and HTML chunks with chunk-boundary handling
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export const MAX_MESSAGE_LENGTH = 4096;
|
|
@@ -488,353 +488,7 @@ function splitLeadingMarkdownBlankLines(markdown: string): {
|
|
|
488
488
|
};
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
export interface TelegramPreviewSnapshotState {
|
|
494
|
-
pendingText: string;
|
|
495
|
-
lastSentText: string;
|
|
496
|
-
lastSentParseMode?: "HTML";
|
|
497
|
-
lastSentStrategy?: TelegramPreviewRenderStrategy;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
export interface TelegramPreviewSnapshot extends TelegramRenderedChunk {
|
|
501
|
-
sourceText: string;
|
|
502
|
-
strategy: TelegramPreviewRenderStrategy;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function buildTelegramPreviewFlushText(options: {
|
|
506
|
-
state: TelegramPreviewSnapshotState;
|
|
507
|
-
maxMessageLength: number;
|
|
508
|
-
renderPreviewText: (markdown: string) => string;
|
|
509
|
-
}): string | undefined {
|
|
510
|
-
const rawText = options.state.pendingText.trim();
|
|
511
|
-
const previewText = options.renderPreviewText(rawText).trim();
|
|
512
|
-
if (!previewText || previewText === options.state.lastSentText) {
|
|
513
|
-
return undefined;
|
|
514
|
-
}
|
|
515
|
-
return previewText.length > options.maxMessageLength
|
|
516
|
-
? previewText.slice(0, options.maxMessageLength)
|
|
517
|
-
: previewText;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function buildTelegramPlainPreviewSnapshot(options: {
|
|
521
|
-
sourceText: string;
|
|
522
|
-
state: TelegramPreviewSnapshotState;
|
|
523
|
-
maxMessageLength: number;
|
|
524
|
-
renderPreviewText: (markdown: string) => string;
|
|
525
|
-
}): TelegramPreviewSnapshot | undefined {
|
|
526
|
-
const previewText = options.renderPreviewText(options.sourceText).trim();
|
|
527
|
-
if (!previewText) return undefined;
|
|
528
|
-
const truncatedPreviewText =
|
|
529
|
-
previewText.length > options.maxMessageLength
|
|
530
|
-
? previewText.slice(0, options.maxMessageLength)
|
|
531
|
-
: previewText;
|
|
532
|
-
if (
|
|
533
|
-
truncatedPreviewText === options.state.lastSentText &&
|
|
534
|
-
options.state.lastSentStrategy === "plain"
|
|
535
|
-
) {
|
|
536
|
-
return undefined;
|
|
537
|
-
}
|
|
538
|
-
return {
|
|
539
|
-
text: truncatedPreviewText,
|
|
540
|
-
sourceText: options.sourceText,
|
|
541
|
-
strategy: "plain",
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
interface TelegramStablePreviewSplit {
|
|
546
|
-
stableMarkdown: string;
|
|
547
|
-
unstableTail: string;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function buildTelegramStablePreviewSplit(
|
|
551
|
-
lines: string[],
|
|
552
|
-
stableEndIndex: number,
|
|
553
|
-
): TelegramStablePreviewSplit {
|
|
554
|
-
return {
|
|
555
|
-
stableMarkdown: lines.slice(0, stableEndIndex).join("\n"),
|
|
556
|
-
unstableTail: lines.slice(stableEndIndex).join("\n"),
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function collectTelegramStablePreviewTextBlockLines(
|
|
561
|
-
lines: string[],
|
|
562
|
-
index: number,
|
|
563
|
-
): { nextIndex: number } {
|
|
564
|
-
let nextIndex = index;
|
|
565
|
-
while (nextIndex < lines.length) {
|
|
566
|
-
const current = lines[nextIndex] ?? "";
|
|
567
|
-
const following = lines[nextIndex + 1] ?? "";
|
|
568
|
-
if (current.trim().length === 0) break;
|
|
569
|
-
if (
|
|
570
|
-
nextIndex !== index &&
|
|
571
|
-
(isFencedCodeStart(current) ||
|
|
572
|
-
canStartIndentedCodeBlock(lines, nextIndex) ||
|
|
573
|
-
/^\s*>/.test(current) ||
|
|
574
|
-
(current.includes("|") && isMarkdownTableSeparator(following)))
|
|
575
|
-
) {
|
|
576
|
-
break;
|
|
577
|
-
}
|
|
578
|
-
nextIndex += 1;
|
|
579
|
-
}
|
|
580
|
-
return { nextIndex };
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function splitTelegramStablePreviewMarkdown(
|
|
584
|
-
markdown: string,
|
|
585
|
-
): TelegramStablePreviewSplit {
|
|
586
|
-
const normalized = normalizeMarkdownDocument(markdown);
|
|
587
|
-
if (normalized.length === 0) return { stableMarkdown: "", unstableTail: "" };
|
|
588
|
-
const lines = normalized.split("\n");
|
|
589
|
-
let index = 0;
|
|
590
|
-
let stableEndIndex = 0;
|
|
591
|
-
while (index < lines.length) {
|
|
592
|
-
while (index < lines.length && (lines[index] ?? "").trim().length === 0) {
|
|
593
|
-
index += 1;
|
|
594
|
-
}
|
|
595
|
-
if (index >= lines.length) break;
|
|
596
|
-
const blockStart = index;
|
|
597
|
-
const line = lines[index] ?? "";
|
|
598
|
-
const nextLine = lines[index + 1] ?? "";
|
|
599
|
-
const fence = parseMarkdownFence(line);
|
|
600
|
-
if (fence) {
|
|
601
|
-
const block = collectFencedMarkdownCodeLines(lines, index, fence);
|
|
602
|
-
if (!block.closed) {
|
|
603
|
-
return buildTelegramStablePreviewSplit(lines, stableEndIndex);
|
|
604
|
-
}
|
|
605
|
-
index = block.nextIndex;
|
|
606
|
-
stableEndIndex = index;
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
if (line.includes("|") && isMarkdownTableSeparator(nextLine)) {
|
|
610
|
-
const block = collectMarkdownTableBlockLines(lines, index);
|
|
611
|
-
index = block.nextIndex;
|
|
612
|
-
if (index >= lines.length) {
|
|
613
|
-
return buildTelegramStablePreviewSplit(lines, stableEndIndex);
|
|
614
|
-
}
|
|
615
|
-
stableEndIndex = index;
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
if (canStartIndentedCodeBlock(lines, index)) {
|
|
619
|
-
const block = collectIndentedMarkdownCodeLines(lines, index);
|
|
620
|
-
index = block.nextIndex;
|
|
621
|
-
if (index >= lines.length) {
|
|
622
|
-
return buildTelegramStablePreviewSplit(lines, stableEndIndex);
|
|
623
|
-
}
|
|
624
|
-
stableEndIndex = index;
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
if (/^\s*>/.test(line)) {
|
|
628
|
-
const block = collectMarkdownQuoteBlockLines(lines, index);
|
|
629
|
-
index = block.nextIndex;
|
|
630
|
-
if (index >= lines.length) {
|
|
631
|
-
return buildTelegramStablePreviewSplit(lines, stableEndIndex);
|
|
632
|
-
}
|
|
633
|
-
stableEndIndex = index;
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
const block = collectTelegramStablePreviewTextBlockLines(lines, blockStart);
|
|
637
|
-
index = block.nextIndex;
|
|
638
|
-
if (index >= lines.length) {
|
|
639
|
-
return buildTelegramStablePreviewSplit(lines, stableEndIndex);
|
|
640
|
-
}
|
|
641
|
-
stableEndIndex = index;
|
|
642
|
-
}
|
|
643
|
-
return buildTelegramStablePreviewSplit(lines, stableEndIndex);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function renderTelegramStablePreviewChunk(options: {
|
|
647
|
-
stableMarkdown: string;
|
|
648
|
-
maxMessageLength: number;
|
|
649
|
-
renderTelegramMessage: (
|
|
650
|
-
text: string,
|
|
651
|
-
options?: { mode?: TelegramRenderMode },
|
|
652
|
-
) => TelegramRenderedChunk[];
|
|
653
|
-
}): TelegramRenderedChunk | undefined {
|
|
654
|
-
const stableChunk = options.renderTelegramMessage(options.stableMarkdown, {
|
|
655
|
-
mode: "markdown",
|
|
656
|
-
})[0];
|
|
657
|
-
if (!stableChunk || stableChunk.text.length === 0) return undefined;
|
|
658
|
-
if (stableChunk.text.length > options.maxMessageLength) return undefined;
|
|
659
|
-
return stableChunk;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function appendTelegramUnstablePreviewTail(options: {
|
|
663
|
-
previewText: string;
|
|
664
|
-
stableMarkdown: string;
|
|
665
|
-
unstableTail: string;
|
|
666
|
-
maxMessageLength: number;
|
|
667
|
-
}): string {
|
|
668
|
-
if (options.unstableTail.length === 0) return options.previewText;
|
|
669
|
-
const tail = splitLeadingMarkdownBlankLines(options.unstableTail);
|
|
670
|
-
const minimumBlankLinesBeforeTail = endsWithMarkdownHeadingLine(
|
|
671
|
-
options.stableMarkdown,
|
|
672
|
-
)
|
|
673
|
-
? 1
|
|
674
|
-
: 0;
|
|
675
|
-
const blankLinesBeforeTail = Math.max(
|
|
676
|
-
tail.blankLines,
|
|
677
|
-
minimumBlankLinesBeforeTail,
|
|
678
|
-
);
|
|
679
|
-
const separator =
|
|
680
|
-
tail.remainingText.length > 0 ? "\n".repeat(blankLinesBeforeTail + 1) : "";
|
|
681
|
-
const tailText = escapeHtml(tail.remainingText);
|
|
682
|
-
const candidate = `${options.previewText}${separator}${tailText}`;
|
|
683
|
-
return candidate.length <= options.maxMessageLength
|
|
684
|
-
? candidate
|
|
685
|
-
: options.previewText;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function isTelegramPreviewSnapshotUnchanged(options: {
|
|
689
|
-
text: string;
|
|
690
|
-
parseMode?: "HTML";
|
|
691
|
-
state: TelegramPreviewSnapshotState;
|
|
692
|
-
strategy: TelegramPreviewRenderStrategy;
|
|
693
|
-
}): boolean {
|
|
694
|
-
return (
|
|
695
|
-
options.text === options.state.lastSentText &&
|
|
696
|
-
options.parseMode === options.state.lastSentParseMode &&
|
|
697
|
-
options.strategy === options.state.lastSentStrategy
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
export function buildTelegramPreviewSnapshot(options: {
|
|
702
|
-
state: TelegramPreviewSnapshotState;
|
|
703
|
-
maxMessageLength: number;
|
|
704
|
-
renderPreviewText: (markdown: string) => string;
|
|
705
|
-
renderTelegramMessage: (
|
|
706
|
-
text: string,
|
|
707
|
-
options?: { mode?: TelegramRenderMode },
|
|
708
|
-
) => TelegramRenderedChunk[];
|
|
709
|
-
}): TelegramPreviewSnapshot | undefined {
|
|
710
|
-
const sourceText = options.state.pendingText.trim();
|
|
711
|
-
if (!sourceText) return undefined;
|
|
712
|
-
const split = splitTelegramStablePreviewMarkdown(sourceText);
|
|
713
|
-
if (split.stableMarkdown.length === 0) {
|
|
714
|
-
return buildTelegramPlainPreviewSnapshot({
|
|
715
|
-
sourceText,
|
|
716
|
-
state: options.state,
|
|
717
|
-
maxMessageLength: options.maxMessageLength,
|
|
718
|
-
renderPreviewText: options.renderPreviewText,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
const stableChunk = renderTelegramStablePreviewChunk({
|
|
722
|
-
stableMarkdown: split.stableMarkdown,
|
|
723
|
-
maxMessageLength: options.maxMessageLength,
|
|
724
|
-
renderTelegramMessage: options.renderTelegramMessage,
|
|
725
|
-
});
|
|
726
|
-
if (!stableChunk) {
|
|
727
|
-
return buildTelegramPlainPreviewSnapshot({
|
|
728
|
-
sourceText,
|
|
729
|
-
state: options.state,
|
|
730
|
-
maxMessageLength: options.maxMessageLength,
|
|
731
|
-
renderPreviewText: options.renderPreviewText,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
const previewText = appendTelegramUnstablePreviewTail({
|
|
735
|
-
previewText: stableChunk.text,
|
|
736
|
-
stableMarkdown: split.stableMarkdown,
|
|
737
|
-
unstableTail: split.unstableTail,
|
|
738
|
-
maxMessageLength: options.maxMessageLength,
|
|
739
|
-
});
|
|
740
|
-
if (
|
|
741
|
-
isTelegramPreviewSnapshotUnchanged({
|
|
742
|
-
text: previewText,
|
|
743
|
-
parseMode: stableChunk.parseMode,
|
|
744
|
-
state: options.state,
|
|
745
|
-
strategy: "rich-stable-blocks",
|
|
746
|
-
})
|
|
747
|
-
) {
|
|
748
|
-
return undefined;
|
|
749
|
-
}
|
|
750
|
-
return {
|
|
751
|
-
text: previewText,
|
|
752
|
-
parseMode: stableChunk.parseMode,
|
|
753
|
-
sourceText,
|
|
754
|
-
strategy: "rich-stable-blocks",
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
export function renderMarkdownPreviewText(markdown: string): string {
|
|
759
|
-
const normalized = normalizeMarkdownDocument(markdown);
|
|
760
|
-
if (normalized.length === 0) return "";
|
|
761
|
-
const output: string[] = [];
|
|
762
|
-
const lines = normalized.split("\n");
|
|
763
|
-
let activeFence: { marker: "`" | "~"; length: number } | undefined;
|
|
764
|
-
for (const rawLine of lines) {
|
|
765
|
-
const line = rawLine ?? "";
|
|
766
|
-
const fence = parseMarkdownFence(line);
|
|
767
|
-
if (activeFence) {
|
|
768
|
-
if (fence && isMatchingMarkdownFence(line, activeFence)) {
|
|
769
|
-
activeFence = undefined;
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
if (line.trim().length === 0) {
|
|
773
|
-
output.push("");
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
output.push(line);
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
if (fence) {
|
|
780
|
-
activeFence = { marker: fence.marker, length: fence.length };
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
|
-
if (line.trim().length === 0) {
|
|
784
|
-
output.push("");
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
if (isMarkdownTableSeparator(line)) {
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
const heading = matchMarkdownHeadingLine(line);
|
|
791
|
-
if (heading) {
|
|
792
|
-
output.push(stripInlineMarkdownToPlainText(heading[2] ?? ""));
|
|
793
|
-
continue;
|
|
794
|
-
}
|
|
795
|
-
const task = line.match(/^(\s*)([-*+]|\d+\.)\s+\[([ xX])\]\s+(.+)$/);
|
|
796
|
-
if (task) {
|
|
797
|
-
const indent = " ".repeat((task[1] ?? "").length);
|
|
798
|
-
const listMarker = task[2] ?? "-";
|
|
799
|
-
const checkboxMarker =
|
|
800
|
-
(task[3] ?? " ").toLowerCase() === "x" ? "[x]" : "[ ]";
|
|
801
|
-
const taskPrefix = isMarkdownNumberedListMarker(listMarker)
|
|
802
|
-
? `${listMarker} ${checkboxMarker}`
|
|
803
|
-
: checkboxMarker;
|
|
804
|
-
output.push(
|
|
805
|
-
`${indent}${taskPrefix} ${stripInlineMarkdownToPlainText(task[4] ?? "")}`,
|
|
806
|
-
);
|
|
807
|
-
continue;
|
|
808
|
-
}
|
|
809
|
-
const bullet = line.match(/^(\s*)[-*+]\s+(.+)$/);
|
|
810
|
-
if (bullet) {
|
|
811
|
-
output.push(
|
|
812
|
-
`${" ".repeat((bullet[1] ?? "").length)}- ${stripInlineMarkdownToPlainText(bullet[2] ?? "")}`,
|
|
813
|
-
);
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
const numbered = line.match(/^(\s*\d+\.)\s+(.+)$/);
|
|
817
|
-
if (numbered) {
|
|
818
|
-
output.push(
|
|
819
|
-
`${numbered[1]} ${stripInlineMarkdownToPlainText(numbered[2] ?? "")}`,
|
|
820
|
-
);
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
const quote = line.match(/^\s*>\s?(.+)$/);
|
|
824
|
-
if (quote) {
|
|
825
|
-
output.push(`> ${stripInlineMarkdownToPlainText(quote[1] ?? "")}`);
|
|
826
|
-
continue;
|
|
827
|
-
}
|
|
828
|
-
if (/^\s*([-*_]\s*){3,}\s*$/.test(line)) {
|
|
829
|
-
output.push("────────");
|
|
830
|
-
continue;
|
|
831
|
-
}
|
|
832
|
-
output.push(stripInlineMarkdownToPlainText(line));
|
|
833
|
-
}
|
|
834
|
-
return output.join("\n");
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// --- Rich Markdown Rendering ---
|
|
491
|
+
// --- UI Markdown-to-Telegram-HTML Rendering ---
|
|
838
492
|
|
|
839
493
|
function renderDelimitedInlineStyle(
|
|
840
494
|
text: string,
|