fix: add per-channel markdown table conversion (#1495) (thanks @odysseus0)

This commit is contained in:
Peter Steinberger
2026-01-23 17:56:50 +00:00
parent 37e5f077b8
commit b77e730657
64 changed files with 837 additions and 186 deletions

View File

@@ -8,6 +8,7 @@ import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
import { clearHistoryEntries } from "../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { danger, logVerbose } from "../globals.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { deliverReplies } from "./bot/delivery.js";
import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
import { createTelegramDraftStream } from "./draft-stream.js";
@@ -123,6 +124,11 @@ export const dispatchTelegramMessage = async ({
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, route.agentId),
};
const tableMode = resolveMarkdownTableMode({
cfg,
channel: "telegram",
accountId: route.accountId,
});
const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
@@ -144,6 +150,7 @@ export const dispatchTelegramMessage = async ({
replyToMode,
textLimit,
messageThreadId: resolvedThreadId,
tableMode,
onVoiceRecording: sendRecordVoice,
});
},

View File

@@ -15,6 +15,7 @@ import { resolveTelegramCustomCommands } from "../config/telegram-custom-command
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
import { danger, logVerbose } from "../globals.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { resolveAgentRoute } from "../routing/resolve-route.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
import type { ChannelGroupPolicy } from "../config/group-policy.js";
@@ -269,6 +270,11 @@ export const registerTelegramNativeCommands = ({
id: isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId),
},
});
const tableMode = resolveMarkdownTableMode({
cfg,
channel: "telegram",
accountId: route.accountId,
});
const skillFilter = firstDefined(topicConfig?.skills, groupConfig?.skills);
const systemPromptParts = [
groupConfig?.systemPrompt?.trim() || null,
@@ -327,6 +333,7 @@ export const registerTelegramNativeCommands = ({
replyToMode,
textLimit,
messageThreadId: resolvedThreadId,
tableMode,
});
},
onError: (err, info) => {

View File

@@ -3,6 +3,7 @@ import { markdownToTelegramChunks, markdownToTelegramHtml } from "../format.js";
import { splitTelegramCaption } from "../caption.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
import type { ReplyToMode } from "../../config/config.js";
import type { MarkdownTableMode } from "../../config/types.base.js";
import { danger, logVerbose } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { mediaKindFromMime } from "../../media/constants.js";
@@ -26,6 +27,7 @@ export async function deliverReplies(params: {
replyToMode: ReplyToMode;
textLimit: number;
messageThreadId?: number;
tableMode?: MarkdownTableMode;
/** Callback invoked before sending a voice message to switch typing indicator. */
onVoiceRecording?: () => Promise<void> | void;
}) {
@@ -49,7 +51,9 @@ export async function deliverReplies(params: {
? [reply.mediaUrl]
: [];
if (mediaList.length === 0) {
const chunks = markdownToTelegramChunks(reply.text || "", textLimit);
const chunks = markdownToTelegramChunks(reply.text || "", textLimit, {
tableMode: params.tableMode,
});
for (const chunk of chunks) {
await sendTelegramText(bot, chatId, chunk.html, runtime, {
replyToMessageId:
@@ -139,7 +143,9 @@ export async function deliverReplies(params: {
// Send deferred follow-up text right after the first media item.
// Chunk it in case it's extremely long (same logic as text-only replies).
if (pendingFollowUpText && isFirstMedia) {
const chunks = markdownToTelegramChunks(pendingFollowUpText, textLimit);
const chunks = markdownToTelegramChunks(pendingFollowUpText, textLimit, {
tableMode: params.tableMode,
});
for (const chunk of chunks) {
const replyToMessageIdFollowup =
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined;

View File

@@ -5,6 +5,7 @@ import {
type MarkdownIR,
} from "../markdown/ir.js";
import { renderMarkdownWithMarkers } from "../markdown/render.js";
import type { MarkdownTableMode } from "../config/types.base.js";
export type TelegramFormattedChunk = {
html: string;
@@ -46,12 +47,15 @@ function renderTelegramHtml(ir: MarkdownIR): string {
});
}
export function markdownToTelegramHtml(markdown: string): string {
export function markdownToTelegramHtml(
markdown: string,
options: { tableMode?: MarkdownTableMode } = {},
): string {
const ir = markdownToIR(markdown ?? "", {
linkify: true,
headingStyle: "none",
blockquotePrefix: "",
tableMode: "bullets",
tableMode: options.tableMode,
});
return renderTelegramHtml(ir);
}
@@ -59,12 +63,13 @@ export function markdownToTelegramHtml(markdown: string): string {
export function markdownToTelegramChunks(
markdown: string,
limit: number,
options: { tableMode?: MarkdownTableMode } = {},
): TelegramFormattedChunk[] {
const ir = markdownToIR(markdown ?? "", {
linkify: true,
headingStyle: "none",
blockquotePrefix: "",
tableMode: "bullets",
tableMode: options.tableMode,
});
const chunks = chunkMarkdownIR(ir, limit);
return chunks.map((chunk) => ({

View File

@@ -17,6 +17,7 @@ import { loadWebMedia } from "../web/media.js";
import { resolveTelegramAccount } from "./accounts.js";
import { resolveTelegramFetch } from "./fetch.js";
import { markdownToTelegramHtml } from "./format.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { splitTelegramCaption } from "./caption.js";
import { recordSentMessage } from "./sent-message-cache.js";
import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js";
@@ -310,7 +311,12 @@ export async function sendMessageTelegram(
throw new Error("Message must be non-empty for Telegram sends");
}
const textMode = opts.textMode ?? "markdown";
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
const tableMode = resolveMarkdownTableMode({
cfg,
channel: "telegram",
accountId: account.accountId,
});
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text, { tableMode });
const textParams = hasThreadParams
? {
parse_mode: "HTML" as const,