@llblab/pi-telegram 0.9.7 → 0.9.9
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/CHANGELOG.md +14 -0
- package/index.ts +6 -0
- package/lib/api.ts +24 -5
- package/lib/prompts.ts +2 -2
- package/lib/queue.ts +9 -2
- package/lib/replies.ts +29 -0
- package/lib/routing.ts +74 -3
- package/lib/updates.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.9: Guest Mode HTML Rendering
|
|
4
|
+
|
|
5
|
+
- `[Guest Mode]` Guest replies now render through the same `renderTelegramMessage` pipeline as direct messages: Markdown → HTML → `answerGuestQuery` with `parse_mode: "HTML"`. Bold, italic, code, links, lists, and tables render identically in guest and DM replies.
|
|
6
|
+
- `[Replies]` Added `createGuestMarkdownReplySender` in the replies domain — guest rendering stays encapsulated within `replies.ts` and `index.ts` no longer imports from `rendering.ts` directly.
|
|
7
|
+
- `[API]` Simplified `answerGuestQuery` title to a fixed `"Response"` string (the `InlineQueryResultArticle` title is hidden in guest mode and was previously generated by stripping HTML/Markdown from the message text).
|
|
8
|
+
- `[API]` Removed `replyMarkup` parameter from `answerGuestQuery` — inline keyboards are not supported in guest mode because `callback_query` from inline results carries `inline_message_id` instead of `chat_id`/`message_id`, which the existing callback routing cannot handle.
|
|
9
|
+
|
|
10
|
+
## 0.9.8: Guest Mode Context
|
|
11
|
+
|
|
12
|
+
- `[Guest Mode]` Extended the [telegram] prefix with |from:user (sender) and |guest:GroupName (source chat for group guest messages) so the agent sees who sent the message and where from. Private guest chats omit the guest: suffix.
|
|
13
|
+
- `[Guest Mode]` Added |from:user to the [reply] block so the agent knows the original author of a replied-to message in guest mode.
|
|
14
|
+
- `[Guest Mode]` Formatted guest prompt text identically to regular DMs through buildTelegramTurnPrompt, including [attachments] and [outputs] sections with file downloads and inbound handler processing.
|
|
15
|
+
- `[Prompts]` Added compact agent guidance explaining guest-mode prefix suffixes (|from:, |guest:) and reply-from context.
|
|
16
|
+
|
|
3
17
|
## 0.9.7: Bot API 10.0 Alignment
|
|
4
18
|
|
|
5
19
|
- `[Dependencies]` Migrated peer dependencies and imports from `@mariozechner/*` to `@earendil-works/*` (`pi-agent-core`, `pi-ai`, `pi-coding-agent`). Impact: the extension now tracks the new `@earendil-works` package scope; transitive `@mariozechner` packages remain in the lockfile until their upstreams migrate.
|
package/index.ts
CHANGED
|
@@ -157,6 +157,11 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
157
157
|
|
|
158
158
|
// --- Message Delivery & Preview ---
|
|
159
159
|
|
|
160
|
+
const sendGuestReply = Replies.createGuestMarkdownReplySender({
|
|
161
|
+
renderTelegramMessage: Replies.renderTelegramMessage,
|
|
162
|
+
answerGuestQuery,
|
|
163
|
+
});
|
|
164
|
+
|
|
160
165
|
const promptDispatchRuntime =
|
|
161
166
|
Runtime.createTelegramPromptDispatchRuntime<Pi.ExtensionContext>({
|
|
162
167
|
lifecycle,
|
|
@@ -497,6 +502,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
497
502
|
sendTextReply,
|
|
498
503
|
sendQueuedAttachments: queuedAttachmentSender,
|
|
499
504
|
answerGuestQuery,
|
|
505
|
+
sendGuestReply,
|
|
500
506
|
planOutboundReply: outboundReplyPlanner,
|
|
501
507
|
sendOutboundReplyArtifacts: outboundReplyArtifactSender,
|
|
502
508
|
isCurrentOwner: lockOwnershipGuard.ownsContext,
|
package/lib/api.ts
CHANGED
|
@@ -161,6 +161,7 @@ export interface TelegramGuestMessage {
|
|
|
161
161
|
guest_query_id: string;
|
|
162
162
|
guest_bot_caller_user?: TelegramUser;
|
|
163
163
|
guest_bot_caller_chat?: TelegramChat;
|
|
164
|
+
reply_to_message?: TelegramMessage;
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
export interface TelegramUpdate {
|
|
@@ -255,7 +256,11 @@ export interface TelegramApiClient {
|
|
|
255
256
|
callbackQueryId: string,
|
|
256
257
|
text?: string,
|
|
257
258
|
) => Promise<void>;
|
|
258
|
-
answerGuestQuery?: (
|
|
259
|
+
answerGuestQuery?: (
|
|
260
|
+
guestQueryId: string,
|
|
261
|
+
text?: string,
|
|
262
|
+
options?: { parseMode?: string },
|
|
263
|
+
) => Promise<void>;
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
export interface TelegramBridgeApiRuntimeDeps {
|
|
@@ -313,7 +318,11 @@ export interface TelegramBridgeApiRuntime {
|
|
|
313
318
|
callbackQueryId: string,
|
|
314
319
|
text?: string,
|
|
315
320
|
) => Promise<void>;
|
|
316
|
-
answerGuestQuery: (
|
|
321
|
+
answerGuestQuery: (
|
|
322
|
+
guestQueryId: string,
|
|
323
|
+
text?: string,
|
|
324
|
+
options?: { parseMode?: string },
|
|
325
|
+
) => Promise<void>;
|
|
317
326
|
prepareTempDir: () => Promise<number>;
|
|
318
327
|
}
|
|
319
328
|
|
|
@@ -784,14 +793,24 @@ export function createTelegramBridgeApiRuntime(
|
|
|
784
793
|
answerCallbackQuery: (callbackQueryId, text) => {
|
|
785
794
|
return deps.client.answerCallbackQuery(callbackQueryId, text);
|
|
786
795
|
},
|
|
787
|
-
answerGuestQuery: (
|
|
796
|
+
answerGuestQuery: (
|
|
797
|
+
guestQueryId: string,
|
|
798
|
+
text: string | undefined,
|
|
799
|
+
options: { parseMode?: string } | undefined,
|
|
800
|
+
) => {
|
|
788
801
|
const body: Record<string, unknown> = { guest_query_id: guestQueryId };
|
|
789
802
|
if (text !== undefined) {
|
|
803
|
+
const inputContent: Record<string, unknown> = {
|
|
804
|
+
message_text: text,
|
|
805
|
+
};
|
|
806
|
+
if (options?.parseMode) {
|
|
807
|
+
inputContent.parse_mode = options.parseMode;
|
|
808
|
+
}
|
|
790
809
|
body.result = {
|
|
791
810
|
type: "article",
|
|
792
811
|
id: "1",
|
|
793
|
-
title:
|
|
794
|
-
input_message_content:
|
|
812
|
+
title: "Response",
|
|
813
|
+
input_message_content: inputContent,
|
|
795
814
|
};
|
|
796
815
|
}
|
|
797
816
|
return callRecorded<void>("answerGuestQuery", body);
|
package/lib/prompts.ts
CHANGED
|
@@ -12,8 +12,8 @@ const SYSTEM_PROMPT_SUFFIX = `
|
|
|
12
12
|
Telegram bridge extension is active.
|
|
13
13
|
|
|
14
14
|
Inbound context:
|
|
15
|
-
- \`[telegram]\` marks Telegram-originated messages.
|
|
16
|
-
- \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
|
|
15
|
+
- \`[telegram]\` marks Telegram-originated messages. Suffixes \`|from:user\` (sender) and \`|guest:group\` (guest mode — message from another chat where the bot is not a member) may be present; the bot sees the message as if forwarded from that user/chat.
|
|
16
|
+
- \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Suffix \`|from:user\` identifies the original author in guest-mode replies. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
|
|
17
17
|
- \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains inbound-handler stdout such as transcriptions or extracted text for those attachments.
|
|
18
18
|
- Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
|
|
19
19
|
|
package/lib/queue.ts
CHANGED
|
@@ -788,7 +788,8 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
788
788
|
text: string,
|
|
789
789
|
) => Promise<unknown>;
|
|
790
790
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
791
|
-
answerGuestQuery?: (guestQueryId: string, text?: string) => Promise<void>;
|
|
791
|
+
answerGuestQuery?: (guestQueryId: string, text?: string, options?: { parseMode?: string }) => Promise<void>;
|
|
792
|
+
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
792
793
|
planOutboundReply?: (
|
|
793
794
|
markdown: string,
|
|
794
795
|
) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
|
|
@@ -837,6 +838,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
837
838
|
sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
|
|
838
839
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
839
840
|
answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
|
|
841
|
+
sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
|
|
840
842
|
planOutboundReply?: TelegramAgentEndRuntimeDeps<
|
|
841
843
|
TTurn,
|
|
842
844
|
TReplyMarkup
|
|
@@ -958,6 +960,7 @@ export function createTelegramAgentEndHook<
|
|
|
958
960
|
sendTextReply: deps.sendTextReply,
|
|
959
961
|
sendQueuedAttachments: deps.sendQueuedAttachments,
|
|
960
962
|
answerGuestQuery: deps.answerGuestQuery,
|
|
963
|
+
sendGuestReply: deps.sendGuestReply,
|
|
961
964
|
planOutboundReply: deps.planOutboundReply,
|
|
962
965
|
sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
|
|
963
966
|
getDefaultChatId: deps.getDefaultChatId,
|
|
@@ -1027,7 +1030,11 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1027
1030
|
return;
|
|
1028
1031
|
}
|
|
1029
1032
|
if (finalText) {
|
|
1030
|
-
|
|
1033
|
+
if (deps.sendGuestReply) {
|
|
1034
|
+
await deps.sendGuestReply(turn.guestQueryId, finalText);
|
|
1035
|
+
} else {
|
|
1036
|
+
await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
|
|
1037
|
+
}
|
|
1031
1038
|
}
|
|
1032
1039
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1033
1040
|
return;
|
package/lib/replies.ts
CHANGED
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
type TelegramRenderMode,
|
|
12
12
|
} from "./rendering.ts";
|
|
13
13
|
|
|
14
|
+
export {
|
|
15
|
+
renderTelegramMessage,
|
|
16
|
+
type TelegramRenderedChunk,
|
|
17
|
+
type TelegramRenderMode,
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
// --- Reply Dedup ---
|
|
15
21
|
|
|
16
22
|
/** Non-persistent reply deduplication for a single agent turn.
|
|
@@ -420,3 +426,26 @@ export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
|
|
|
420
426
|
return inner(chatId, effectiveReplyTo, markdown, options);
|
|
421
427
|
};
|
|
422
428
|
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Guest reply sender: renders Markdown → HTML, sends via answerGuestQuery.
|
|
432
|
+
* Keeps guest rendering inside the replies domain so the orchestration layer
|
|
433
|
+
* (index.ts) does not import from rendering.ts directly.
|
|
434
|
+
*/
|
|
435
|
+
export function createGuestMarkdownReplySender(deps: {
|
|
436
|
+
renderTelegramMessage: (
|
|
437
|
+
text: string,
|
|
438
|
+
options?: { mode?: TelegramRenderMode },
|
|
439
|
+
) => TelegramRenderedChunk[];
|
|
440
|
+
answerGuestQuery: (
|
|
441
|
+
guestQueryId: string,
|
|
442
|
+
text?: string,
|
|
443
|
+
options?: { parseMode?: string },
|
|
444
|
+
) => Promise<void>;
|
|
445
|
+
}) {
|
|
446
|
+
return async (guestQueryId: string, markdown: string) => {
|
|
447
|
+
const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
|
|
448
|
+
const html = chunks.length > 0 ? chunks[0].text : markdown;
|
|
449
|
+
await deps.answerGuestQuery(guestQueryId, html, { parseMode: "HTML" });
|
|
450
|
+
};
|
|
451
|
+
}
|
package/lib/routing.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import * as OutboundHandlers from "./outbound-handlers.ts";
|
|
8
8
|
import * as Commands from "./commands.ts";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
9
10
|
import type { TelegramConfigStore } from "./config.ts";
|
|
10
11
|
import type { TelegramInboundHandlerRuntime } from "./inbound-handlers.ts";
|
|
11
12
|
import * as Media from "./media.ts";
|
|
@@ -371,7 +372,71 @@ export function createTelegramInboundRouteRuntime<
|
|
|
371
372
|
ctx: TContext,
|
|
372
373
|
): Promise<void> => {
|
|
373
374
|
const text = guestMessage.text ?? "";
|
|
375
|
+
const gm = guestMessage as unknown as Record<string, unknown>;
|
|
376
|
+
// Build telegram prefix with guest context
|
|
377
|
+
const fromRaw = gm.from as Record<string, unknown> | undefined;
|
|
378
|
+
const fromName =
|
|
379
|
+
(fromRaw?.username as string) ||
|
|
380
|
+
(fromRaw?.first_name as string) ||
|
|
381
|
+
"";
|
|
382
|
+
const chatRaw = gm.chat as Record<string, unknown>;
|
|
383
|
+
const chatTitle = chatRaw?.title as string | undefined;
|
|
384
|
+
const chatType = chatRaw?.type as string;
|
|
385
|
+
const prefixParts = ["telegram"];
|
|
386
|
+
if (fromName) prefixParts.push(`from:${fromName}`);
|
|
387
|
+
if (chatType !== "private" && chatTitle) {
|
|
388
|
+
prefixParts.push(`guest:${chatTitle}`);
|
|
389
|
+
}
|
|
390
|
+
const telegramPrefix = `[${prefixParts.join("|")}]`;
|
|
391
|
+
// Extract reply context
|
|
392
|
+
const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
|
|
393
|
+
const replyText =
|
|
394
|
+
replyMsg
|
|
395
|
+
? ((replyMsg.text as string) || (replyMsg.caption as string) || "").trim()
|
|
396
|
+
: "";
|
|
397
|
+
const replyFrom =
|
|
398
|
+
replyMsg
|
|
399
|
+
? (replyMsg.from as Record<string, unknown> | undefined)?.username as string | undefined
|
|
400
|
+
: undefined;
|
|
401
|
+
// Download files, run inbound handlers
|
|
402
|
+
const guestMsg = guestMessage as unknown as Media.TelegramMediaMessage;
|
|
403
|
+
const files = await Media.downloadTelegramMessageFiles([guestMsg], {
|
|
404
|
+
downloadFile: deps.downloadFile,
|
|
405
|
+
});
|
|
406
|
+
const processed = await deps.inboundHandlerRuntime.process(files, text, ctx);
|
|
407
|
+
let rawText = processed.rawText || text;
|
|
408
|
+
// Append reply context after handler processing
|
|
409
|
+
if (replyText) {
|
|
410
|
+
const replyBlock = replyFrom
|
|
411
|
+
? `[reply|from:${replyFrom}] ${replyText}`
|
|
412
|
+
: `[reply] ${replyText}`;
|
|
413
|
+
rawText = `${rawText}\n\n${replyBlock}`;
|
|
414
|
+
}
|
|
415
|
+
const promptText = Turns.buildTelegramTurnPrompt({
|
|
416
|
+
telegramPrefix,
|
|
417
|
+
rawText,
|
|
418
|
+
files,
|
|
419
|
+
promptFiles: processed.promptFiles,
|
|
420
|
+
handlerOutputs: processed.handlerOutputs,
|
|
421
|
+
});
|
|
374
422
|
const order = deps.bridgeRuntime.queue.allocateItemOrder();
|
|
423
|
+
const content: Queue.TelegramPromptContent[] = [
|
|
424
|
+
{ type: "text", text: promptText },
|
|
425
|
+
];
|
|
426
|
+
for (const file of processed.promptFiles) {
|
|
427
|
+
if (file.isImage && file.mimeType) {
|
|
428
|
+
try {
|
|
429
|
+
const buffer = await readFile(file.path);
|
|
430
|
+
content.push({
|
|
431
|
+
type: "image",
|
|
432
|
+
data: Buffer.from(buffer).toString("base64"),
|
|
433
|
+
mimeType: file.mimeType,
|
|
434
|
+
});
|
|
435
|
+
} catch {
|
|
436
|
+
// skip unreadable files
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
375
440
|
const guestTurn: Queue.PendingTelegramTurn = {
|
|
376
441
|
kind: "prompt",
|
|
377
442
|
chatId: 0,
|
|
@@ -382,9 +447,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
382
447
|
queueLane: "default",
|
|
383
448
|
laneOrder: order,
|
|
384
449
|
queuedAttachments: [],
|
|
385
|
-
content
|
|
386
|
-
historyText:
|
|
387
|
-
|
|
450
|
+
content,
|
|
451
|
+
historyText: Turns.formatTelegramTurnStatusSummary(
|
|
452
|
+
processed.rawText || text,
|
|
453
|
+
processed.promptFiles,
|
|
454
|
+
processed.handlerOutputs,
|
|
455
|
+
),
|
|
456
|
+
statusSummary: Turns.truncateTelegramQueueSummary(
|
|
457
|
+
processed.rawText || text,
|
|
458
|
+
),
|
|
388
459
|
};
|
|
389
460
|
const items = deps.telegramQueueStore.getQueuedItems();
|
|
390
461
|
deps.telegramQueueStore.setQueuedItems(
|
package/lib/updates.ts
CHANGED