Skip to content

Commit c274bcc

Browse files
committed
fix(telegram): honor table mode while chunking
1 parent 5f5e3b4 commit c274bcc

10 files changed

Lines changed: 126 additions & 9 deletions

File tree

extensions/telegram/src/format.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import {
33
markdownToTelegramChunks,
44
markdownToTelegramHtml,
5+
markdownToTelegramHtmlChunks,
56
renderTelegramHtmlText,
67
splitTelegramHtmlChunks,
78
telegramHtmlToPlainTextFallback,
@@ -174,6 +175,17 @@ describe("markdownToTelegramHtml", () => {
174175
expect(res).toContain("report/draft.\n\n3. Cognee");
175176
});
176177

178+
it("applies tableMode when rendering Telegram HTML chunks", () => {
179+
const input = ["| Name | Status |", "| --- | --- |", "| Bot | OK |"].join("\n");
180+
181+
const chunks = markdownToTelegramHtmlChunks(input, 4096, { tableMode: "bullets" });
182+
const rendered = chunks.join("");
183+
184+
expect(rendered).toContain("<b>Bot</b>");
185+
expect(rendered).toContain("• Status: OK");
186+
expect(rendered).not.toContain("| --- | --- |");
187+
});
188+
177189
it("does not insert Telegram list boundary spacing inside fenced code", () => {
178190
const input = ["```", " • literal bullet", "3. literal number", "```"].join("\n");
179191

extensions/telegram/src/format.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,10 @@ export function markdownToTelegramChunks(
766766
return renderTelegramChunksWithinHtmlLimit(ir, limit);
767767
}
768768

769-
export function markdownToTelegramHtmlChunks(markdown: string, limit: number): string[] {
770-
return markdownToTelegramChunks(markdown, limit).map((chunk) => chunk.html);
769+
export function markdownToTelegramHtmlChunks(
770+
markdown: string,
771+
limit: number,
772+
options: { tableMode?: MarkdownTableMode } = {},
773+
): string[] {
774+
return markdownToTelegramChunks(markdown, limit, options).map((chunk) => chunk.html);
771775
}

extensions/telegram/src/outbound-adapter.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ describe("telegramOutbound", () => {
6060
sendMessageTelegramMock.mockReset();
6161
});
6262

63+
it("resolves markdown table config for outbound chunking", () => {
64+
const input = ["| Name | Status |", "| --- | --- |", "| Bot | OK |"].join("\n");
65+
66+
const chunks = telegramOutbound.chunker!(input, 4096, {
67+
cfg: { channels: { telegram: { markdown: { tables: "bullets" } } } } as never,
68+
accountId: "ops",
69+
});
70+
const rendered = chunks.join("");
71+
72+
expect(rendered).toContain("<b>Bot</b>");
73+
expect(rendered).toContain("• Status: OK");
74+
expect(rendered).not.toContain("| --- | --- |");
75+
});
76+
6377
it("forwards mediaLocalRoots in direct media sends", async () => {
6478
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-media" });
6579

extensions/telegram/src/outbound-adapter.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
normalizeMessagePresentation,
88
renderMessagePresentationFallbackText,
99
} from "openclaw/plugin-sdk/interactive-runtime";
10+
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
1011
import type { OutboundDeliveryFormattingOptions } from "openclaw/plugin-sdk/outbound-runtime";
1112
import {
1213
resolveOutboundSendDep,
@@ -49,11 +50,20 @@ async function resolveDefaultTelegramSend(deps?: OutboundSendDeps): Promise<Tele
4950
function chunkTelegramOutboundText(
5051
text: string,
5152
limit: number,
52-
ctx?: { formatting?: OutboundDeliveryFormattingOptions },
53+
ctx?: {
54+
formatting?: OutboundDeliveryFormattingOptions;
55+
cfg?: NonNullable<TelegramSendOpts>["cfg"];
56+
accountId?: string | null;
57+
},
5358
): string[] {
59+
const tableMode = resolveMarkdownTableMode({
60+
cfg: ctx?.cfg,
61+
channel: "telegram",
62+
accountId: ctx?.accountId,
63+
});
5464
return ctx?.formatting?.parseMode === "HTML"
5565
? splitTelegramHtmlChunks(text, limit)
56-
: markdownToTelegramHtmlChunks(text, limit);
66+
: markdownToTelegramHtmlChunks(text, limit, { tableMode });
5767
}
5868

5969
async function resolveTelegramSendContext(params: {

src/channels/plugins/outbound.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ export type ChannelOutboundFormattedContext = ChannelOutboundContext & {
132132

133133
export type ChannelOutboundChunkContext = {
134134
formatting?: OutboundDeliveryFormattingOptions;
135+
cfg?: OpenClawConfig;
136+
accountId?: string | null;
135137
};
136138

137139
export type ChannelOutboundNormalizePayloadParams = {

src/infra/outbound/deliver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,8 @@ async function deliverOutboundPayloadsCore(
14121412
textLimit,
14131413
chunkMode,
14141414
formatting: params.formatting,
1415+
cfg,
1416+
accountId,
14151417
consumeReplyTo: (value) =>
14161418
applyReplyToConsumption(value, {
14171419
consumeImplicitReply: value.replyToIdSource === "implicit",

src/infra/outbound/message-plan.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,29 @@ describe("outbound message planning", () => {
103103
},
104104
]);
105105
});
106+
107+
it("passes config and account context to channel chunkers", () => {
108+
const contexts: unknown[] = [];
109+
110+
planOutboundTextMessageUnits({
111+
text: "abcd",
112+
textLimit: 2,
113+
chunker: (_text, _limit, ctx) => {
114+
contexts.push(ctx);
115+
return ["ab", "cd"];
116+
},
117+
overrides: {},
118+
formatting: { parseMode: "HTML" },
119+
cfg: { channels: { telegram: { markdown: { tables: "bullets" } } } } as never,
120+
accountId: "ops",
121+
});
122+
123+
expect(contexts).toEqual([
124+
{
125+
formatting: { parseMode: "HTML" },
126+
cfg: { channels: { telegram: { markdown: { tables: "bullets" } } } },
127+
accountId: "ops",
128+
},
129+
]);
130+
});
106131
});

src/infra/outbound/message-plan.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
chunkMarkdownTextWithMode,
44
type ChunkMode,
55
} from "../../auto-reply/chunk.js";
6+
import type { OpenClawConfig } from "../../config/types.js";
67
import type { OutboundDeliveryFormattingOptions } from "./formatting.js";
78
import type { ReplyToOverride } from "./reply-policy.js";
89

@@ -29,7 +30,11 @@ export type OutboundMessageUnit =
2930
export type OutboundMessageChunker = (
3031
text: string,
3132
limit: number,
32-
ctx?: { formatting?: OutboundDeliveryFormattingOptions },
33+
ctx?: {
34+
formatting?: OutboundDeliveryFormattingOptions;
35+
cfg?: OpenClawConfig;
36+
accountId?: string | null;
37+
},
3338
) => string[];
3439

3540
type PlanReplyToConsumption = <T extends OutboundMessageSendOverrides>(overrides: T) => T;
@@ -55,10 +60,14 @@ function chunkTextForPlan(params: {
5560
limit: number;
5661
chunker: OutboundMessageChunker;
5762
formatting?: OutboundDeliveryFormattingOptions;
63+
cfg?: OpenClawConfig;
64+
accountId?: string | null;
5865
}): string[] {
59-
return params.formatting
60-
? params.chunker(params.text, params.limit, { formatting: params.formatting })
61-
: params.chunker(params.text, params.limit);
66+
return params.chunker(params.text, params.limit, {
67+
formatting: params.formatting,
68+
cfg: params.cfg,
69+
accountId: params.accountId,
70+
});
6271
}
6372

6473
export function planOutboundTextMessageUnits(params: {
@@ -70,6 +79,8 @@ export function planOutboundTextMessageUnits(params: {
7079
textLimit?: number;
7180
chunkMode?: ChunkMode;
7281
formatting?: OutboundDeliveryFormattingOptions;
82+
cfg?: OpenClawConfig;
83+
accountId?: string | null;
7384
consumeReplyTo?: PlanReplyToConsumption;
7485
}): OutboundMessageUnit[] {
7586
const planTextUnit = (text: string): OutboundMessageUnit => ({
@@ -106,6 +117,8 @@ export function planOutboundTextMessageUnits(params: {
106117
limit: params.textLimit,
107118
chunker: params.chunker,
108119
formatting: params.formatting,
120+
cfg: params.cfg,
121+
accountId: params.accountId,
109122
});
110123
if (!chunks.length && blockChunk) {
111124
chunks.push(blockChunk);
@@ -122,6 +135,8 @@ export function planOutboundTextMessageUnits(params: {
122135
limit: params.textLimit,
123136
chunker: params.chunker,
124137
formatting: params.formatting,
138+
cfg: params.cfg,
139+
accountId: params.accountId,
125140
}).map(planChunkedTextUnit);
126141
}
127142

src/plugin-sdk/reply-payload.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,35 @@ describe("sendTextMediaPayload", () => {
210210
"explicit-reply",
211211
]);
212212
});
213+
214+
it("passes config and account context to text payload chunkers", async () => {
215+
const sendText = vi.fn(async ({ text }) => ({ channel: "test", messageId: text }));
216+
const chunker = vi.fn(() => ["ab", "cd"]);
217+
const cfg = { channels: { telegram: { markdown: { tables: "bullets" } } } };
218+
219+
await sendTextMediaPayload({
220+
channel: "test",
221+
ctx: {
222+
cfg: cfg as never,
223+
accountId: "ops",
224+
to: "target",
225+
text: "",
226+
payload: { text: "abcd" },
227+
formatting: { parseMode: "HTML" },
228+
},
229+
adapter: {
230+
textChunkLimit: 2,
231+
chunker,
232+
sendText,
233+
},
234+
});
235+
236+
expect(chunker).toHaveBeenCalledWith("abcd", 2, {
237+
formatting: { parseMode: "HTML" },
238+
cfg,
239+
accountId: "ops",
240+
});
241+
});
213242
});
214243

215244
describe("normalizeOutboundReplyPayload", () => {

src/plugin-sdk/reply-payload.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,11 @@ export async function sendTextMediaPayload(params: {
342342
const limit = params.adapter.textChunkLimit;
343343
const chunks =
344344
limit && params.adapter.chunker
345-
? params.adapter.chunker(text, limit, { formatting: params.ctx.formatting })
345+
? params.adapter.chunker(text, limit, {
346+
formatting: params.ctx.formatting,
347+
cfg: params.ctx.cfg,
348+
accountId: params.ctx.accountId,
349+
})
346350
: [text];
347351
let lastResult: Awaited<ReturnType<NonNullable<typeof params.adapter.sendText>>>;
348352
for (const chunk of chunks) {

0 commit comments

Comments
 (0)