@llblab/pi-telegram 0.20.4 → 0.20.6
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/BACKLOG.md +44 -0
- package/CHANGELOG.md +22 -0
- package/README.md +2 -0
- package/docs/architecture.md +8 -8
- package/docs/multi-instance-bus.md +1 -1
- package/docs/outbound.md +6 -0
- package/index.ts +3 -0
- package/lib/bindings.ts +127 -34
- package/lib/bus-api.ts +5 -3
- package/lib/bus-leader.ts +7 -0
- package/lib/bus.ts +43 -1
- package/lib/config.ts +2 -2
- package/lib/lifecycle.ts +4 -4
- package/lib/locks.ts +12 -1
- package/lib/logs.ts +2 -2
- package/lib/outbound-attachments.ts +170 -0
- package/lib/outbound-markup.ts +39 -4
- package/lib/paths.ts +2 -1
- package/lib/prompts.ts +3 -2
- package/lib/queue.ts +46 -1
- package/lib/routing.ts +91 -13
- package/lib/setup.ts +7 -1
- package/lib/status.ts +15 -14
- package/lib/telegram-api.ts +44 -6
- package/package.json +1 -1
|
@@ -88,6 +88,7 @@ export interface TelegramQueuedOutboundAttachmentView {
|
|
|
88
88
|
|
|
89
89
|
export interface TelegramOutboundAttachmentQueueTargetView {
|
|
90
90
|
queuedAttachments: TelegramQueuedOutboundAttachmentView[];
|
|
91
|
+
guestQueryId?: string;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
|
|
@@ -96,6 +97,42 @@ export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutbou
|
|
|
96
97
|
target?: TelegramTarget;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
export type TelegramGuestCachedAttachmentResult =
|
|
101
|
+
| {
|
|
102
|
+
type: "document";
|
|
103
|
+
id: string;
|
|
104
|
+
title: string;
|
|
105
|
+
document_file_id: string;
|
|
106
|
+
caption?: string;
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
type: "photo";
|
|
110
|
+
id: string;
|
|
111
|
+
photo_file_id: string;
|
|
112
|
+
caption?: string;
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
type: "audio";
|
|
116
|
+
id: string;
|
|
117
|
+
audio_file_id: string;
|
|
118
|
+
caption?: string;
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
type: "voice";
|
|
122
|
+
id: string;
|
|
123
|
+
voice_file_id: string;
|
|
124
|
+
title: string;
|
|
125
|
+
caption?: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
interface TelegramGuestStagingMessage {
|
|
129
|
+
message_id?: number;
|
|
130
|
+
document?: { file_id?: string };
|
|
131
|
+
photo?: Array<{ file_id?: string; file_size?: number }>;
|
|
132
|
+
audio?: { file_id?: string };
|
|
133
|
+
voice?: { file_id?: string };
|
|
134
|
+
}
|
|
135
|
+
|
|
99
136
|
function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
|
|
100
137
|
const normalized = path.toLowerCase();
|
|
101
138
|
return (
|
|
@@ -107,6 +144,27 @@ function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
|
|
|
107
144
|
);
|
|
108
145
|
}
|
|
109
146
|
|
|
147
|
+
function getTelegramGuestAttachmentTransport(path: string): {
|
|
148
|
+
method: "sendDocument" | "sendPhoto" | "sendAudio" | "sendVoice";
|
|
149
|
+
fileField: "document" | "photo" | "audio" | "voice";
|
|
150
|
+
} {
|
|
151
|
+
const normalized = path.toLowerCase();
|
|
152
|
+
if (
|
|
153
|
+
normalized.endsWith(".jpg") ||
|
|
154
|
+
normalized.endsWith(".jpeg") ||
|
|
155
|
+
normalized.endsWith(".png")
|
|
156
|
+
) {
|
|
157
|
+
return { method: "sendPhoto", fileField: "photo" };
|
|
158
|
+
}
|
|
159
|
+
if (normalized.endsWith(".ogg") || normalized.endsWith(".opus")) {
|
|
160
|
+
return { method: "sendVoice", fileField: "voice" };
|
|
161
|
+
}
|
|
162
|
+
if (normalized.endsWith(".mp3")) {
|
|
163
|
+
return { method: "sendAudio", fileField: "audio" };
|
|
164
|
+
}
|
|
165
|
+
return { method: "sendDocument", fileField: "document" };
|
|
166
|
+
}
|
|
167
|
+
|
|
110
168
|
function formatTelegramOutboundAttachmentSizeLimitError(
|
|
111
169
|
size: number,
|
|
112
170
|
maxSize: number,
|
|
@@ -388,6 +446,14 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
388
446
|
statPath: options.statPath,
|
|
389
447
|
});
|
|
390
448
|
}
|
|
449
|
+
if (
|
|
450
|
+
options.activeTurn.guestQueryId &&
|
|
451
|
+
options.activeTurn.queuedAttachments.length + options.paths.length > 1
|
|
452
|
+
) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
"Telegram Guest Mode supports one attachment per reply; no attachment was queued",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
391
457
|
if (
|
|
392
458
|
options.activeTurn.queuedAttachments.length + options.paths.length >
|
|
393
459
|
options.maxAttachmentsPerTurn
|
|
@@ -414,6 +480,110 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
414
480
|
};
|
|
415
481
|
}
|
|
416
482
|
|
|
483
|
+
export async function deliverTelegramGuestCachedAttachment(options: {
|
|
484
|
+
guestQueryId: string;
|
|
485
|
+
stagingChatId: number;
|
|
486
|
+
stagingTarget?: TelegramTarget;
|
|
487
|
+
attachment: TelegramQueuedOutboundAttachmentView;
|
|
488
|
+
caption?: string;
|
|
489
|
+
sendMultipart: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
490
|
+
answerGuestQuery: (
|
|
491
|
+
guestQueryId: string,
|
|
492
|
+
result: TelegramGuestCachedAttachmentResult,
|
|
493
|
+
) => Promise<void>;
|
|
494
|
+
answerGuestText?: (guestQueryId: string, text: string) => Promise<void>;
|
|
495
|
+
fallbackText?: string;
|
|
496
|
+
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
497
|
+
recordRuntimeEvent?: TelegramOutboundAttachmentRuntimeEventRecorderPort["recordRuntimeEvent"];
|
|
498
|
+
}): Promise<void> {
|
|
499
|
+
const transport = getTelegramGuestAttachmentTransport(options.attachment.path);
|
|
500
|
+
let stagingMessageId: number | undefined;
|
|
501
|
+
let answerAttempted = false;
|
|
502
|
+
try {
|
|
503
|
+
const message = (await options.sendMultipart(
|
|
504
|
+
transport.method,
|
|
505
|
+
{
|
|
506
|
+
chat_id: String(options.stagingChatId),
|
|
507
|
+
...getTelegramMultipartTargetFields(options.stagingTarget),
|
|
508
|
+
},
|
|
509
|
+
transport.fileField,
|
|
510
|
+
options.attachment.path,
|
|
511
|
+
options.attachment.fileName,
|
|
512
|
+
)) as TelegramGuestStagingMessage;
|
|
513
|
+
stagingMessageId = message.message_id;
|
|
514
|
+
const caption = options.caption
|
|
515
|
+
? Array.from(options.caption).slice(0, 1024).join("")
|
|
516
|
+
: undefined;
|
|
517
|
+
let result: TelegramGuestCachedAttachmentResult;
|
|
518
|
+
if (transport.fileField === "photo") {
|
|
519
|
+
const photo = [...(message.photo ?? [])]
|
|
520
|
+
.sort((left, right) => (left.file_size ?? 0) - (right.file_size ?? 0))
|
|
521
|
+
.at(-1);
|
|
522
|
+
if (!photo?.file_id) throw new Error("Guest staging upload returned no photo file_id");
|
|
523
|
+
result = {
|
|
524
|
+
type: "photo",
|
|
525
|
+
id: "attachment-1",
|
|
526
|
+
photo_file_id: photo.file_id,
|
|
527
|
+
...(caption ? { caption } : {}),
|
|
528
|
+
};
|
|
529
|
+
} else if (transport.fileField === "audio") {
|
|
530
|
+
if (!message.audio?.file_id)
|
|
531
|
+
throw new Error("Guest staging upload returned no audio file_id");
|
|
532
|
+
result = {
|
|
533
|
+
type: "audio",
|
|
534
|
+
id: "attachment-1",
|
|
535
|
+
audio_file_id: message.audio.file_id,
|
|
536
|
+
...(caption ? { caption } : {}),
|
|
537
|
+
};
|
|
538
|
+
} else if (transport.fileField === "voice") {
|
|
539
|
+
if (!message.voice?.file_id)
|
|
540
|
+
throw new Error("Guest staging upload returned no voice file_id");
|
|
541
|
+
result = {
|
|
542
|
+
type: "voice",
|
|
543
|
+
id: "attachment-1",
|
|
544
|
+
voice_file_id: message.voice.file_id,
|
|
545
|
+
title: options.attachment.fileName,
|
|
546
|
+
...(caption ? { caption } : {}),
|
|
547
|
+
};
|
|
548
|
+
} else {
|
|
549
|
+
if (!message.document?.file_id)
|
|
550
|
+
throw new Error("Guest staging upload returned no document file_id");
|
|
551
|
+
result = {
|
|
552
|
+
type: "document",
|
|
553
|
+
id: "attachment-1",
|
|
554
|
+
title: options.attachment.fileName,
|
|
555
|
+
document_file_id: message.document.file_id,
|
|
556
|
+
...(caption ? { caption } : {}),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
answerAttempted = true;
|
|
560
|
+
await options.answerGuestQuery(options.guestQueryId, result);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
if (
|
|
563
|
+
!answerAttempted &&
|
|
564
|
+
options.answerGuestText &&
|
|
565
|
+
options.fallbackText
|
|
566
|
+
) {
|
|
567
|
+
answerAttempted = true;
|
|
568
|
+
await options.answerGuestText(options.guestQueryId, options.fallbackText);
|
|
569
|
+
} else {
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
} finally {
|
|
573
|
+
if (stagingMessageId !== undefined) {
|
|
574
|
+
try {
|
|
575
|
+
await options.deleteMessage(options.stagingChatId, stagingMessageId);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
options.recordRuntimeEvent?.("attachment", error, {
|
|
578
|
+
phase: "guest-staging-cleanup",
|
|
579
|
+
chatId: options.stagingChatId,
|
|
580
|
+
messageId: stagingMessageId,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
417
587
|
export async function sendTelegramOutboundMessage(options: {
|
|
418
588
|
text: string;
|
|
419
589
|
chatId?: number;
|
package/lib/outbound-markup.ts
CHANGED
|
@@ -64,6 +64,36 @@ function isTopLevelClosingFence(
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function collectPairedTelegramVoiceActionBody(
|
|
68
|
+
markdown: string,
|
|
69
|
+
bodyStart: number,
|
|
70
|
+
commentContent: string,
|
|
71
|
+
): { content: string; end: number } | undefined {
|
|
72
|
+
const normalizedContent = commentContent.trim();
|
|
73
|
+
if (
|
|
74
|
+
!normalizedContent.startsWith("telegram_voice") ||
|
|
75
|
+
!isTelegramActionCommentContent(commentContent)
|
|
76
|
+
) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
let offset = bodyStart;
|
|
80
|
+
while (offset < markdown.length) {
|
|
81
|
+
const lineEnd = getMarkdownLineEnd(markdown, offset);
|
|
82
|
+
const line = getMarkdownLineText(markdown, offset, lineEnd);
|
|
83
|
+
if (line === "<!-- /telegram_voice -->") {
|
|
84
|
+
const body = markdown.slice(bodyStart, offset).trim();
|
|
85
|
+
if (!body) return undefined;
|
|
86
|
+
return {
|
|
87
|
+
content: `${commentContent.trimEnd()}\n${body}`,
|
|
88
|
+
end: lineEnd,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (line.startsWith("<!--")) return undefined;
|
|
92
|
+
offset = lineEnd;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
67
97
|
function collectInlineClosedTelegramActionBody(
|
|
68
98
|
markdown: string,
|
|
69
99
|
bodyStart: number,
|
|
@@ -117,14 +147,19 @@ export function collectTopLevelHtmlComments(markdown: string): {
|
|
|
117
147
|
const closesOnOpeningLine = closeIndex < lineEnd;
|
|
118
148
|
const hasOnlyWhitespaceAfterClose =
|
|
119
149
|
line.slice(closeColumn + 3).trim() === "";
|
|
120
|
-
const
|
|
150
|
+
const pairedVoiceBody =
|
|
121
151
|
closesOnOpeningLine && hasOnlyWhitespaceAfterClose
|
|
152
|
+
? collectPairedTelegramVoiceActionBody(markdown, lineEnd, content)
|
|
153
|
+
: undefined;
|
|
154
|
+
const inlineBody =
|
|
155
|
+
!pairedVoiceBody && closesOnOpeningLine && hasOnlyWhitespaceAfterClose
|
|
122
156
|
? collectInlineClosedTelegramActionBody(markdown, lineEnd, content)
|
|
123
157
|
: undefined;
|
|
124
|
-
|
|
125
|
-
|
|
158
|
+
const recoveredBody = pairedVoiceBody ?? inlineBody;
|
|
159
|
+
if (recoveredBody) {
|
|
160
|
+
end = recoveredBody.end;
|
|
126
161
|
raw = markdown.slice(offset, end);
|
|
127
|
-
content =
|
|
162
|
+
content = recoveredBody.content;
|
|
128
163
|
}
|
|
129
164
|
comments.push({ raw, content, start: offset, end });
|
|
130
165
|
offset = getMarkdownLineEnd(markdown, end);
|
package/lib/paths.ts
CHANGED
|
@@ -76,9 +76,10 @@ export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
|
|
|
76
76
|
logs: string;
|
|
77
77
|
} {
|
|
78
78
|
const suffix = getTelegramProfilePathSuffix(profileName);
|
|
79
|
+
const profileSlug = suffix.slice(1);
|
|
79
80
|
return {
|
|
80
81
|
state: `~/.pi/agent/tmp/telegram/state${suffix}.json`,
|
|
81
|
-
logs: `~/.pi/agent/tmp/telegram/logs${
|
|
82
|
+
logs: `~/.pi/agent/tmp/telegram/logs${profileSlug ? `.${profileSlug}` : ""}.jsonl`,
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
|
package/lib/prompts.ts
CHANGED
|
@@ -16,7 +16,7 @@ Telegram bridge available. Do not use it from local/TUI prompts unless explicitl
|
|
|
16
16
|
|
|
17
17
|
const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
18
18
|
|
|
19
|
-
Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`; hidden comments are valid only for explicit \`telegram_voice\` or \`telegram_button\` actions with payload
|
|
19
|
+
Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`; hidden comments are valid only for explicit \`telegram_voice\` or \`telegram_button\` actions with payload. For voice use a top-level HTML action: \`<!-- telegram_voice: Speak this. -->\`, multiline \`<!-- telegram_voice lang=ru\nSpeak this.\n-->\`, or paired \`<!-- telegram_voice lang=ru -->\nSpeak this.\n<!-- /telegram_voice -->\`.`;
|
|
20
20
|
|
|
21
21
|
function buildTelegramHelpText(profileName?: string): string {
|
|
22
22
|
const diagnosticsPaths = getTelegramDiagnosticsDisplayPaths(profileName);
|
|
@@ -37,7 +37,8 @@ How to answer Telegram turns:
|
|
|
37
37
|
Assistant-authored Telegram actions:
|
|
38
38
|
- \`telegram_voice\` and \`telegram_button\` are hidden top-level HTML comments, not Pi tools.
|
|
39
39
|
- Put action comments at column zero, outside code, quotes, lists, and indented examples.
|
|
40
|
-
- Voice forms: \`<!-- telegram_voice text="Short summary"
|
|
40
|
+
- Voice forms: \`<!-- telegram_voice text="Short summary" -->\`, \`<!-- telegram_voice: Short summary -->\`, multiline \`<!-- telegram_voice lang=ru\nShort summary.\n-->\`, or paired \`<!-- telegram_voice lang=ru -->\nShort summary.\n<!-- /telegram_voice -->\`.
|
|
41
|
+
- Keep the complete action at top level and include a non-empty voice payload.
|
|
41
42
|
- Keep voice text TTS-friendly; avoid raw Markdown, code, and tables in voice text.
|
|
42
43
|
- Voice delivery generates and attaches OGG automatically; do not also call \`telegram_attach\` for the same audio.
|
|
43
44
|
- Button forms: \`<!-- telegram_button: OK -->\`, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\`, or multiline \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\`.
|
package/lib/queue.ts
CHANGED
|
@@ -899,6 +899,16 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
899
899
|
options?: { parseMode?: string },
|
|
900
900
|
) => Promise<void>;
|
|
901
901
|
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
902
|
+
sendGuestAttachment?: (
|
|
903
|
+
turn: TTurn,
|
|
904
|
+
attachment: QueuedAttachment,
|
|
905
|
+
caption?: string,
|
|
906
|
+
) => Promise<void>;
|
|
907
|
+
sendGuestVoiceReply?: (
|
|
908
|
+
turn: TTurn,
|
|
909
|
+
plan: TelegramAgentEndOutboundReplyPlan<TReplyMarkup>,
|
|
910
|
+
caption?: string,
|
|
911
|
+
) => Promise<void>;
|
|
902
912
|
planOutboundReply?: (
|
|
903
913
|
markdown: string,
|
|
904
914
|
) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
|
|
@@ -958,6 +968,8 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
958
968
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
959
969
|
answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
|
|
960
970
|
sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
|
|
971
|
+
sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
|
|
972
|
+
sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
|
|
961
973
|
planOutboundReply?: TelegramAgentEndRuntimeDeps<
|
|
962
974
|
TTurn,
|
|
963
975
|
TReplyMarkup
|
|
@@ -1083,6 +1095,8 @@ export function createTelegramAgentEndHook<
|
|
|
1083
1095
|
sendQueuedAttachments: deps.sendQueuedAttachments,
|
|
1084
1096
|
answerGuestQuery: deps.answerGuestQuery,
|
|
1085
1097
|
sendGuestReply: deps.sendGuestReply,
|
|
1098
|
+
sendGuestAttachment: deps.sendGuestAttachment,
|
|
1099
|
+
sendGuestVoiceReply: deps.sendGuestVoiceReply,
|
|
1086
1100
|
planOutboundReply: deps.planOutboundReply,
|
|
1087
1101
|
sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
|
|
1088
1102
|
getDefaultChatId: deps.getDefaultChatId,
|
|
@@ -1181,7 +1195,38 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1181
1195
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1182
1196
|
return;
|
|
1183
1197
|
}
|
|
1184
|
-
|
|
1198
|
+
const [guestAttachment] = turn.queuedAttachments;
|
|
1199
|
+
if (guestAttachment && deps.sendGuestAttachment) {
|
|
1200
|
+
try {
|
|
1201
|
+
await deps.sendGuestAttachment(
|
|
1202
|
+
turn,
|
|
1203
|
+
guestAttachment,
|
|
1204
|
+
finalText || undefined,
|
|
1205
|
+
);
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1208
|
+
phase: "guest-attachment",
|
|
1209
|
+
guestQueryId: turn.guestQueryId,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
} else if (
|
|
1213
|
+
outboundReply &&
|
|
1214
|
+
(outboundReply.voiceText || outboundReply.voiceReplies?.length) &&
|
|
1215
|
+
deps.sendGuestVoiceReply
|
|
1216
|
+
) {
|
|
1217
|
+
try {
|
|
1218
|
+
await deps.sendGuestVoiceReply(
|
|
1219
|
+
turn,
|
|
1220
|
+
outboundReply,
|
|
1221
|
+
finalText || undefined,
|
|
1222
|
+
);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1225
|
+
phase: "guest-voice",
|
|
1226
|
+
guestQueryId: turn.guestQueryId,
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
} else if (finalText) {
|
|
1185
1230
|
if (deps.sendGuestReply) {
|
|
1186
1231
|
await deps.sendGuestReply(turn.guestQueryId, finalText);
|
|
1187
1232
|
} else {
|
package/lib/routing.ts
CHANGED
|
@@ -21,10 +21,67 @@ import * as TextGroups from "./text-groups.ts";
|
|
|
21
21
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
22
22
|
import * as Turns from "./turns.ts";
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
interface TelegramPromptPeerView {
|
|
25
|
+
id?: unknown;
|
|
26
|
+
username?: unknown;
|
|
27
|
+
first_name?: unknown;
|
|
28
|
+
last_name?: unknown;
|
|
29
|
+
title?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatTelegramPromptPeer(
|
|
33
|
+
peer: TelegramPromptPeerView | undefined,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (!peer) return undefined;
|
|
36
|
+
if (typeof peer.username === "string" && peer.username.length > 0) {
|
|
37
|
+
return peer.username;
|
|
38
|
+
}
|
|
39
|
+
const displayName = [peer.first_name, peer.last_name]
|
|
40
|
+
.filter(
|
|
41
|
+
(part): part is string =>
|
|
42
|
+
typeof part === "string" && part.length > 0,
|
|
43
|
+
)
|
|
44
|
+
.join(" ");
|
|
45
|
+
if (displayName) return displayName;
|
|
46
|
+
if (typeof peer.title === "string" && peer.title.length > 0) {
|
|
47
|
+
return peer.title;
|
|
48
|
+
}
|
|
49
|
+
return typeof peer.id === "number" ? String(peer.id) : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isTelegramPromptOwnerPeer(
|
|
53
|
+
peer: TelegramPromptPeerView | undefined,
|
|
54
|
+
ownerUserId: number | undefined,
|
|
55
|
+
): boolean {
|
|
56
|
+
return ownerUserId !== undefined && peer?.id === ownerUserId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveTelegramGuestPromptPeer(input: {
|
|
60
|
+
chatType?: string;
|
|
61
|
+
chat?: TelegramPromptPeerView;
|
|
62
|
+
from?: TelegramPromptPeerView;
|
|
63
|
+
replyFrom?: TelegramPromptPeerView;
|
|
64
|
+
guestBotCallerUser?: TelegramPromptPeerView;
|
|
65
|
+
guestBotCallerChat?: TelegramPromptPeerView;
|
|
66
|
+
ownerUserId?: number;
|
|
67
|
+
}): string | undefined {
|
|
68
|
+
if (input.chatType !== "private") {
|
|
69
|
+
return formatTelegramPromptPeer(input.chat);
|
|
70
|
+
}
|
|
71
|
+
if (!isTelegramPromptOwnerPeer(input.from, input.ownerUserId)) {
|
|
72
|
+
return formatTelegramPromptPeer(input.from);
|
|
73
|
+
}
|
|
74
|
+
for (const candidate of [
|
|
75
|
+
input.replyFrom,
|
|
76
|
+
input.chat,
|
|
77
|
+
input.guestBotCallerUser,
|
|
78
|
+
input.guestBotCallerChat,
|
|
79
|
+
]) {
|
|
80
|
+
if (isTelegramPromptOwnerPeer(candidate, input.ownerUserId)) continue;
|
|
81
|
+
const peer = formatTelegramPromptPeer(candidate);
|
|
82
|
+
if (peer) return peer;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
28
85
|
}
|
|
29
86
|
|
|
30
87
|
function appendTelegramSourceAttachmentSection(
|
|
@@ -1812,22 +1869,43 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1812
1869
|
const gm = guestMessage as unknown as Record<string, unknown>;
|
|
1813
1870
|
// Build telegram prefix with guest context
|
|
1814
1871
|
const chatRaw = gm.chat as Record<string, unknown>;
|
|
1815
|
-
const chatTitle = chatRaw?.title as string | undefined;
|
|
1816
1872
|
const chatType = chatRaw?.type as string;
|
|
1817
1873
|
const fromRaw = gm.from as Record<string, unknown> | undefined;
|
|
1818
1874
|
const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
|
|
1819
1875
|
const replyFromRaw = replyMsg?.from as Record<string, unknown> | undefined;
|
|
1820
|
-
const
|
|
1876
|
+
const guestBotCallerUser = gm.guest_bot_caller_user as
|
|
1877
|
+
| Record<string, unknown>
|
|
1878
|
+
| undefined;
|
|
1879
|
+
const guestBotCallerChat = gm.guest_bot_caller_chat as
|
|
1880
|
+
| Record<string, unknown>
|
|
1881
|
+
| undefined;
|
|
1882
|
+
const ownerUserId = deps.configStore.getAllowedUserId();
|
|
1821
1883
|
const replyPeer = formatTelegramPromptPeer(replyFromRaw);
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
:
|
|
1884
|
+
const guestPeer = resolveTelegramGuestPromptPeer({
|
|
1885
|
+
chatType,
|
|
1886
|
+
chat: chatRaw,
|
|
1887
|
+
from: fromRaw,
|
|
1888
|
+
replyFrom: replyFromRaw,
|
|
1889
|
+
guestBotCallerUser,
|
|
1890
|
+
guestBotCallerChat,
|
|
1891
|
+
ownerUserId,
|
|
1892
|
+
});
|
|
1826
1893
|
const prefixParts = ["telegram"];
|
|
1827
|
-
if (
|
|
1828
|
-
prefixParts.push(`guest:${chatTitle}`);
|
|
1829
|
-
} else if (chatType === "private" && guestPeer) {
|
|
1894
|
+
if (guestPeer) {
|
|
1830
1895
|
prefixParts.push(`guest:${guestPeer}`);
|
|
1896
|
+
} else if (chatType === "private") {
|
|
1897
|
+
deps.recordRuntimeEvent?.(
|
|
1898
|
+
"guest",
|
|
1899
|
+
new Error("Private Guest Mode remote peer could not be resolved"),
|
|
1900
|
+
{
|
|
1901
|
+
phase: "peer-attribution",
|
|
1902
|
+
chatId: typeof chatRaw?.id === "number" ? chatRaw.id : undefined,
|
|
1903
|
+
fromId: typeof fromRaw?.id === "number" ? fromRaw.id : undefined,
|
|
1904
|
+
hasReplyFrom: !!replyFromRaw,
|
|
1905
|
+
hasCallerUser: !!guestBotCallerUser,
|
|
1906
|
+
hasCallerChat: !!guestBotCallerChat,
|
|
1907
|
+
},
|
|
1908
|
+
);
|
|
1831
1909
|
}
|
|
1832
1910
|
const telegramPrefix = `[${prefixParts.join("|")}]`;
|
|
1833
1911
|
// Extract reply context
|
package/lib/setup.ts
CHANGED
|
@@ -197,8 +197,14 @@ export function createTelegramSetupPromptRuntime<
|
|
|
197
197
|
promptEditor: (label, value) => ctx.ui.editor(label, value),
|
|
198
198
|
getMe: deps.getMe,
|
|
199
199
|
persistConfig: async (config) => {
|
|
200
|
-
|
|
200
|
+
const previousConfig = deps.getConfig();
|
|
201
201
|
deps.setConfig(config);
|
|
202
|
+
try {
|
|
203
|
+
await deps.persistConfig(config);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
deps.setConfig(previousConfig);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
202
208
|
},
|
|
203
209
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
204
210
|
startPolling: () => deps.startPolling(ctx),
|
package/lib/status.ts
CHANGED
|
@@ -181,6 +181,7 @@ export interface TelegramBridgeStatusLineState {
|
|
|
181
181
|
hasBotToken?: boolean;
|
|
182
182
|
botUsername?: string;
|
|
183
183
|
activeProfileName?: string;
|
|
184
|
+
diagnosticPaths?: { state: string; logs: string };
|
|
184
185
|
allowedUserId?: number;
|
|
185
186
|
botThreadMode?: "unknown" | "enabled" | "disabled";
|
|
186
187
|
botThreadModeUpdatedAtMs?: number;
|
|
@@ -259,6 +260,9 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
259
260
|
statusKey?: string;
|
|
260
261
|
getConfig: () => TelegramBridgeStatusConfig;
|
|
261
262
|
getActiveProfileName?: () => string | undefined;
|
|
263
|
+
getDiagnosticPaths?: (
|
|
264
|
+
profileName?: string,
|
|
265
|
+
) => { state: string; logs: string };
|
|
262
266
|
isPollingActive: () => boolean;
|
|
263
267
|
getActiveSourceMessageIds: () => number[] | undefined;
|
|
264
268
|
hasActiveTurn: () => boolean;
|
|
@@ -620,10 +624,12 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
620
624
|
getBridgeStatusLineState: () => {
|
|
621
625
|
const config = deps.getConfig();
|
|
622
626
|
const botThreadMode = deps.getBotThreadMode?.();
|
|
627
|
+
const activeProfileName = deps.getActiveProfileName?.();
|
|
623
628
|
return {
|
|
624
629
|
hasBotToken: Boolean(config.botToken),
|
|
625
630
|
botUsername: config.botUsername,
|
|
626
|
-
activeProfileName
|
|
631
|
+
activeProfileName,
|
|
632
|
+
diagnosticPaths: deps.getDiagnosticPaths?.(activeProfileName),
|
|
627
633
|
allowedUserId: config.allowedUserId,
|
|
628
634
|
botThreadMode: botThreadMode?.threadMode,
|
|
629
635
|
botThreadModeUpdatedAtMs: botThreadMode?.updatedAtMs,
|
|
@@ -774,9 +780,6 @@ export function buildTelegramStatusBarText(
|
|
|
774
780
|
return `${label} ${theme.fg("warning", "electing")}${queued}`;
|
|
775
781
|
if (!state.pollingActive && state.busRole !== "follower")
|
|
776
782
|
return `${theme.fg("accent", "telegram")} ${theme.fg("muted", "disconnected")}${queued}`;
|
|
777
|
-
if (state.compactionInProgress) {
|
|
778
|
-
return `${label} ${theme.fg("warning", "compacting")}${queued}`;
|
|
779
|
-
}
|
|
780
783
|
if (state.processing) {
|
|
781
784
|
const processingStatus = state.queuedStatus
|
|
782
785
|
? "active"
|
|
@@ -1058,19 +1061,18 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1058
1061
|
? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
1059
1062
|
: ""
|
|
1060
1063
|
}`;
|
|
1061
|
-
const executionState = state.
|
|
1062
|
-
? "
|
|
1063
|
-
: state.
|
|
1064
|
-
? "
|
|
1065
|
-
:
|
|
1066
|
-
? "active"
|
|
1067
|
-
: "idle";
|
|
1064
|
+
const executionState = state.pendingDispatch
|
|
1065
|
+
? "pending dispatch"
|
|
1066
|
+
: state.activeSourceMessageIds?.length
|
|
1067
|
+
? "active"
|
|
1068
|
+
: "idle";
|
|
1068
1069
|
const profileSuffix = state.activeProfileName
|
|
1069
1070
|
? `.${state.activeProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
|
|
1070
1071
|
: "";
|
|
1071
|
-
const
|
|
1072
|
+
const profileSlug = profileSuffix.slice(1);
|
|
1073
|
+
const diagnosticsPaths = state.diagnosticPaths ?? {
|
|
1072
1074
|
state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
|
|
1073
|
-
logs: `~/.pi/agent/tmp/telegram/logs${
|
|
1075
|
+
logs: `~/.pi/agent/tmp/telegram/logs${profileSlug ? `.${profileSlug}` : ""}.jsonl`,
|
|
1074
1076
|
};
|
|
1075
1077
|
return [
|
|
1076
1078
|
"connection:",
|
|
@@ -1251,7 +1253,6 @@ function buildContextSummary(
|
|
|
1251
1253
|
}
|
|
1252
1254
|
|
|
1253
1255
|
function buildStatusSummary(ctx: TelegramStatusContext): string {
|
|
1254
|
-
if (ctx.isCompactionInProgress?.()) return "compacting";
|
|
1255
1256
|
if (ctx.hasPendingMessages?.()) return "pending";
|
|
1256
1257
|
if (ctx.isIdle?.() === false) return "active";
|
|
1257
1258
|
if (ctx.isIdle?.() === true) return "idle";
|
package/lib/telegram-api.ts
CHANGED
|
@@ -287,6 +287,44 @@ export interface TelegramFileDownloadOptions {
|
|
|
287
287
|
maxFileSizeBytes?: number;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
export type TelegramGuestCachedMediaResult =
|
|
291
|
+
| {
|
|
292
|
+
type: "document";
|
|
293
|
+
id: string;
|
|
294
|
+
title: string;
|
|
295
|
+
document_file_id: string;
|
|
296
|
+
caption?: string;
|
|
297
|
+
parse_mode?: string;
|
|
298
|
+
}
|
|
299
|
+
| {
|
|
300
|
+
type: "photo";
|
|
301
|
+
id: string;
|
|
302
|
+
photo_file_id: string;
|
|
303
|
+
caption?: string;
|
|
304
|
+
parse_mode?: string;
|
|
305
|
+
}
|
|
306
|
+
| {
|
|
307
|
+
type: "audio";
|
|
308
|
+
id: string;
|
|
309
|
+
audio_file_id: string;
|
|
310
|
+
caption?: string;
|
|
311
|
+
parse_mode?: string;
|
|
312
|
+
}
|
|
313
|
+
| {
|
|
314
|
+
type: "voice";
|
|
315
|
+
id: string;
|
|
316
|
+
voice_file_id: string;
|
|
317
|
+
title: string;
|
|
318
|
+
caption?: string;
|
|
319
|
+
parse_mode?: string;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export interface TelegramAnswerGuestQueryOptions {
|
|
323
|
+
parseMode?: string;
|
|
324
|
+
richMessage?: TelegramInputRichMessage;
|
|
325
|
+
result?: TelegramGuestCachedMediaResult;
|
|
326
|
+
}
|
|
327
|
+
|
|
290
328
|
export interface TelegramAnswerCallbackQueryOptions {
|
|
291
329
|
recordRuntimeEvent?: (
|
|
292
330
|
kind: "api",
|
|
@@ -322,7 +360,7 @@ export interface TelegramApiClient {
|
|
|
322
360
|
answerGuestQuery?: (
|
|
323
361
|
guestQueryId: string,
|
|
324
362
|
text?: string,
|
|
325
|
-
options?:
|
|
363
|
+
options?: TelegramAnswerGuestQueryOptions,
|
|
326
364
|
) => Promise<void>;
|
|
327
365
|
}
|
|
328
366
|
|
|
@@ -401,7 +439,7 @@ export interface TelegramBridgeApiRuntime {
|
|
|
401
439
|
answerGuestQuery: (
|
|
402
440
|
guestQueryId: string,
|
|
403
441
|
text?: string,
|
|
404
|
-
options?:
|
|
442
|
+
options?: TelegramAnswerGuestQueryOptions,
|
|
405
443
|
) => Promise<void>;
|
|
406
444
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
407
445
|
prepareTempDir: () => Promise<number>;
|
|
@@ -1312,12 +1350,12 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1312
1350
|
answerGuestQuery: (
|
|
1313
1351
|
guestQueryId: string,
|
|
1314
1352
|
text: string | undefined,
|
|
1315
|
-
options:
|
|
1316
|
-
| { parseMode?: string; richMessage?: TelegramInputRichMessage }
|
|
1317
|
-
| undefined,
|
|
1353
|
+
options: TelegramAnswerGuestQueryOptions | undefined,
|
|
1318
1354
|
) => {
|
|
1319
1355
|
const body: Record<string, unknown> = { guest_query_id: guestQueryId };
|
|
1320
|
-
if (
|
|
1356
|
+
if (options?.result) {
|
|
1357
|
+
body.result = options.result;
|
|
1358
|
+
} else if (text !== undefined || options?.richMessage) {
|
|
1321
1359
|
const inputContent: Record<string, unknown> = options?.richMessage
|
|
1322
1360
|
? { rich_message: options.richMessage }
|
|
1323
1361
|
: { message_text: text };
|