Merge branch 'main' into docs/mermaid-diagrams

This commit is contained in:
Davendra Patel 2026-01-29 11:33:33 +05:30 committed by GitHub
commit 9b3edef5af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 26 additions and 8 deletions

View File

@ -550,10 +550,11 @@ describe("applyMediaUnderstanding", () => {
it("escapes XML special characters in filenames to prevent injection", async () => { it("escapes XML special characters in filenames to prevent injection", async () => {
const { applyMediaUnderstanding } = await loadApply(); const { applyMediaUnderstanding } = await loadApply();
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-"));
// Create file with XML special characters in the name (what filesystem allows) // Use & in filename — valid on all platforms (including Windows, which
// forbids < and > in NTFS filenames) and still requires XML escaping.
// Note: The sanitizeFilename in store.ts would strip most dangerous chars, // Note: The sanitizeFilename in store.ts would strip most dangerous chars,
// but we test that even if some slip through, they get escaped in output // but we test that even if some slip through, they get escaped in output
const filePath = path.join(dir, "file<test>.txt"); const filePath = path.join(dir, "file&test.txt");
await fs.writeFile(filePath, "safe content"); await fs.writeFile(filePath, "safe content");
const ctx: MsgContext = { const ctx: MsgContext = {
@ -575,10 +576,9 @@ describe("applyMediaUnderstanding", () => {
expect(result.appliedFile).toBe(true); expect(result.appliedFile).toBe(true);
// Verify XML special chars are escaped in the output // Verify XML special chars are escaped in the output
expect(ctx.Body).toContain("&lt;"); expect(ctx.Body).toContain("&amp;");
expect(ctx.Body).toContain("&gt;"); // The name attribute should contain the escaped form, not a raw unescaped &
// The raw < and > should not appear unescaped in the name attribute expect(ctx.Body).toMatch(/name="file&amp;test\.txt"/);
expect(ctx.Body).not.toMatch(/name="[^"]*<[^"]*"/);
}); });
it("normalizes MIME types to prevent attribute injection", async () => { it("normalizes MIME types to prevent attribute injection", async () => {

View File

@ -228,6 +228,7 @@ export const dispatchTelegramMessage = async ({
onVoiceRecording: sendRecordVoice, onVoiceRecording: sendRecordVoice,
linkPreview: telegramCfg.linkPreview, linkPreview: telegramCfg.linkPreview,
replyQuoteText, replyQuoteText,
notifyEmptyResponse: info.kind === "final",
}); });
}, },
onError: (err, info) => { onError: (err, info) => {

View File

@ -488,7 +488,7 @@ export const registerTelegramNativeCommands = ({
cfg, cfg,
dispatcherOptions: { dispatcherOptions: {
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
deliver: async (payload) => { deliver: async (payload, info) => {
await deliverReplies({ await deliverReplies({
replies: [payload], replies: [payload],
chatId: String(chatId), chatId: String(chatId),
@ -501,6 +501,7 @@ export const registerTelegramNativeCommands = ({
tableMode, tableMode,
chunkMode, chunkMode,
linkPreview: telegramCfg.linkPreview, linkPreview: telegramCfg.linkPreview,
notifyEmptyResponse: info.kind === "final",
}); });
}, },
onError: (err, info) => { onError: (err, info) => {

View File

@ -44,7 +44,9 @@ export async function deliverReplies(params: {
linkPreview?: boolean; linkPreview?: boolean;
/** Optional quote text for Telegram reply_parameters. */ /** Optional quote text for Telegram reply_parameters. */
replyQuoteText?: string; replyQuoteText?: string;
}) { /** If true, send a fallback message when all replies are empty. Default: false */
notifyEmptyResponse?: boolean;
}): Promise<{ delivered: boolean }> {
const { const {
replies, replies,
chatId, chatId,
@ -58,6 +60,7 @@ export async function deliverReplies(params: {
} = params; } = params;
const chunkMode = params.chunkMode ?? "length"; const chunkMode = params.chunkMode ?? "length";
let hasReplied = false; let hasReplied = false;
let skippedEmpty = 0;
const chunkText = (markdown: string) => { const chunkText = (markdown: string) => {
const markdownChunks = const markdownChunks =
chunkMode === "newline" chunkMode === "newline"
@ -85,6 +88,7 @@ export async function deliverReplies(params: {
continue; continue;
} }
runtime.error?.(danger("reply missing text/media")); runtime.error?.(danger("reply missing text/media"));
skippedEmpty++;
continue; continue;
} }
const replyToId = replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId); const replyToId = replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId);
@ -268,6 +272,18 @@ export async function deliverReplies(params: {
} }
} }
} }
// If all replies were empty and notifyEmptyResponse is enabled, send a fallback message
// Check both: (1) replies with no content (skippedEmpty), (2) no replies at all (empty array)
if (!hasReplied && (skippedEmpty > 0 || replies.length === 0) && params.notifyEmptyResponse) {
const fallbackText = "No response generated. Please try again.";
await sendTelegramText(bot, chatId, fallbackText, runtime, {
messageThreadId,
});
hasReplied = true;
}
return { delivered: hasReplied };
} }
export async function resolveMedia( export async function resolveMedia(