mirror of
https://github.com/wassname/pi-telegram.git
synced 2026-06-27 18:24:59 +08:00
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
/**
|
|
* Telegram turn-building helpers
|
|
* Owns prompt-turn summary and content construction so queued Telegram turns are assembled consistently
|
|
*/
|
|
|
|
import { basename } from "node:path";
|
|
|
|
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
|
|
import {
|
|
collectTelegramMessageIds,
|
|
formatTelegramHistoryText,
|
|
} from "./media.ts";
|
|
import type { PendingTelegramTurn } from "./queue.ts";
|
|
|
|
export interface TelegramTurnMessageLike {
|
|
message_id: number;
|
|
chat: { id: number };
|
|
}
|
|
|
|
export interface DownloadedTelegramTurnFileLike {
|
|
path: string;
|
|
fileName: string;
|
|
isImage: boolean;
|
|
mimeType?: string;
|
|
}
|
|
|
|
export function truncateTelegramQueueSummary(
|
|
text: string,
|
|
maxWords = 5,
|
|
maxLength = 40,
|
|
): string {
|
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
if (!normalized) return "";
|
|
const words = normalized.split(" ");
|
|
let summary = words.slice(0, maxWords).join(" ");
|
|
if (summary.length === 0) summary = normalized;
|
|
if (summary.length > maxLength) {
|
|
summary = summary.slice(0, maxLength).trimEnd();
|
|
}
|
|
return summary.length < normalized.length || words.length > maxWords
|
|
? `${summary}…`
|
|
: summary;
|
|
}
|
|
|
|
export function formatTelegramTurnStatusSummary(
|
|
rawText: string,
|
|
files: DownloadedTelegramTurnFileLike[],
|
|
): string {
|
|
const textSummary = truncateTelegramQueueSummary(rawText);
|
|
if (textSummary) return textSummary;
|
|
if (files.length === 1) {
|
|
const fileName = basename(
|
|
files[0]?.fileName || files[0]?.path || "attachment",
|
|
);
|
|
return `📎 ${truncateTelegramQueueSummary(fileName, 4, 32) || "attachment"}`;
|
|
}
|
|
if (files.length > 1) return `📎 ${files.length} attachments`;
|
|
return "(empty message)";
|
|
}
|
|
|
|
export function buildTelegramTurnPrompt(options: {
|
|
telegramPrefix: string;
|
|
rawText: string;
|
|
files: DownloadedTelegramTurnFileLike[];
|
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
}): string {
|
|
// Let pi handle `!` shell commands and `/` slash commands natively - don't prepend [telegram] prefix
|
|
const raw = options.rawText.trimStart();
|
|
let prompt = (raw.startsWith("!") || raw.startsWith("/")) ? "" : options.telegramPrefix;
|
|
if ((options.historyTurns?.length ?? 0) > 0) {
|
|
prompt +=
|
|
"\n\nEarlier Telegram messages arrived after an aborted turn. Treat them as prior user messages, in order:";
|
|
for (const [index, turn] of (options.historyTurns ?? []).entries()) {
|
|
prompt += `\n\n${index + 1}. ${turn.historyText}`;
|
|
}
|
|
prompt += "\n\nCurrent Telegram message:";
|
|
}
|
|
if (options.rawText.length > 0) {
|
|
prompt +=
|
|
(options.historyTurns?.length ?? 0) > 0
|
|
? `\n${options.rawText}`
|
|
: ` ${options.rawText}`;
|
|
}
|
|
if (options.files.length > 0) {
|
|
prompt += "\n\nTelegram attachments were saved locally:";
|
|
for (const file of options.files) {
|
|
prompt += `\n- ${file.path}`;
|
|
}
|
|
}
|
|
return prompt;
|
|
}
|
|
|
|
export async function buildTelegramPromptTurn(options: {
|
|
telegramPrefix: string;
|
|
messages: TelegramTurnMessageLike[];
|
|
historyTurns?: PendingTelegramTurn[];
|
|
queueOrder: number;
|
|
rawText: string;
|
|
files: DownloadedTelegramTurnFileLike[];
|
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
inferImageMimeType: (path: string) => string | undefined;
|
|
}): Promise<PendingTelegramTurn> {
|
|
const firstMessage = options.messages[0];
|
|
if (!firstMessage) {
|
|
throw new Error("Missing Telegram message for turn creation");
|
|
}
|
|
const content: Array<TextContent | ImageContent> = [
|
|
{
|
|
type: "text",
|
|
text: buildTelegramTurnPrompt({
|
|
telegramPrefix: options.telegramPrefix,
|
|
rawText: options.rawText,
|
|
files: options.files,
|
|
historyTurns: options.historyTurns,
|
|
}),
|
|
},
|
|
];
|
|
for (const file of options.files) {
|
|
if (!file.isImage) continue;
|
|
const mediaType = file.mimeType || options.inferImageMimeType(file.path);
|
|
if (!mediaType) continue;
|
|
const buffer = await options.readBinaryFile(file.path);
|
|
content.push({
|
|
type: "image",
|
|
data: Buffer.from(buffer).toString("base64"),
|
|
mimeType: mediaType,
|
|
});
|
|
}
|
|
return {
|
|
kind: "prompt",
|
|
chatId: firstMessage.chat.id,
|
|
replyToMessageId: firstMessage.message_id,
|
|
sourceMessageIds: collectTelegramMessageIds(options.messages),
|
|
queueOrder: options.queueOrder,
|
|
queueLane: "default",
|
|
laneOrder: options.queueOrder,
|
|
queuedAttachments: [],
|
|
content,
|
|
historyText: formatTelegramHistoryText(options.rawText, options.files),
|
|
statusSummary: formatTelegramTurnStatusSummary(
|
|
options.rawText,
|
|
options.files,
|
|
),
|
|
};
|
|
}
|