diff --git a/CHANGELOG.md b/CHANGELOG.md index 9caed7a24cd..2d3de6017c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Signal: preserve sender attachment filenames and resolve missing MIME types from those filenames, so Linux `signal-cli` voice notes without `contentType` still enter audio transcription. Fixes #48614. Thanks @mindfury. - Dashboard/security: avoid writing tokenized Control UI URLs or SSH hints to runtime logs, keeping gateway bearer fragments out of console-captured logs readable through `logs.tail`. (#70029) Thanks @Ziy1-Tan. - Providers/OpenRouter: treat DeepSeek refs as cache-TTL eligible without injecting Anthropic cache-control markers, aligning context pruning with OpenRouter-managed prompt caching. (#51983) Thanks @QuinnH496. - Discord/cron: deliver text-only isolated cron and heartbeat announce output from the canonical final assistant text once, avoiding duplicate Discord posts when streamed block payloads and the final answer contain the same content. Fixes #71406. Thanks @alexgross21. diff --git a/docs/channels/signal.md b/docs/channels/signal.md index eedd78154a5..39346f43e25 100644 --- a/docs/channels/signal.md +++ b/docs/channels/signal.md @@ -208,6 +208,7 @@ Groups: - Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000). - Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking. - Attachments supported (base64 fetched from `signal-cli`). +- Voice-note attachments use the `signal-cli` filename as a MIME fallback when `contentType` is missing, so audio transcription can still classify AAC voice memos. - Default media cap: `channels.signal.mediaMaxMb` (default 8). - Use `channels.signal.ignoreAttachments` to skip downloading media. - Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). diff --git a/extensions/signal/src/monitor.ts b/extensions/signal/src/monitor.ts index 3ef8b0797f1..8a3badcedc6 100644 --- a/extensions/signal/src/monitor.ts +++ b/extensions/signal/src/monitor.ts @@ -7,7 +7,11 @@ import { warnMissingProviderGroupPolicyFallbackOnce, } from "openclaw/plugin-sdk/config-runtime"; import { waitForTransportReady } from "openclaw/plugin-sdk/infra-runtime"; -import { estimateBase64DecodedBytes, saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime"; +import { + detectMime, + estimateBase64DecodedBytes, + saveMediaBuffer, +} from "openclaw/plugin-sdk/media-runtime"; import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history"; import { deliverTextOrMediaReply, @@ -294,11 +298,16 @@ async function fetchAttachment(params: { ); } const buffer = Buffer.from(result.data, "base64"); + const originalFilename = normalizeOptionalString(attachment.filename ?? undefined); + const contentType = + normalizeOptionalString(attachment.contentType ?? undefined) ?? + (await detectMime({ buffer, filePath: originalFilename })); const saved = await saveMediaBuffer( buffer, - attachment.contentType ?? undefined, + contentType, "inbound", params.maxBytes, + originalFilename, ); return { path: saved.path, contentType: saved.contentType }; } diff --git a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts index 8e77a0620d6..6df339b5a76 100644 --- a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts +++ b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts @@ -295,6 +295,37 @@ describe("signal createSignalEventHandler inbound context", () => { expect(capture.ctx?.MediaTypes).toEqual(["image/jpeg", "application/octet-stream"]); }); + it("threads resolved audio contentType for Signal voice attachments", async () => { + const handler = createSignalEventHandler( + createBaseSignalEventHandlerDeps({ + cfg: { + messages: { inbound: { debounceMs: 0 } }, + channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } }, + }, + ignoreAttachments: false, + fetchAttachment: async ({ attachment }) => ({ + path: `/tmp/${String(attachment.id)}.aac`, + contentType: "audio/aac", + }), + historyLimit: 0, + }), + ); + + await handler( + createSignalReceiveEvent({ + dataMessage: { + message: "", + attachments: [{ id: "voice1", contentType: undefined, filename: "voice.aac" }], + }, + }), + ); + + expect(capture.ctx).toBeTruthy(); + expect(capture.ctx?.MediaPath).toBe("/tmp/voice1.aac"); + expect(capture.ctx?.MediaType).toBe("audio/aac"); + expect(capture.ctx?.MediaTypes).toEqual(["audio/aac"]); + }); + it("drops own UUID inbound messages when only accountUuid is configured", async () => { const ownUuid = "123e4567-e89b-12d3-a456-426614174000"; const handler = createSignalEventHandler( diff --git a/src/media/mime.test.ts b/src/media/mime.test.ts index 68da516c23d..8f1fa91a63f 100644 --- a/src/media/mime.test.ts +++ b/src/media/mime.test.ts @@ -110,6 +110,11 @@ describe("mime detection", () => { const mime = await detectMime({ filePath: "/tmp/style.css" }); expect(mime).toBe("text/css"); }); + + it("detects AAC from a bare filename when buffer sniffing is inconclusive", async () => { + const mime = await detectMime({ buffer: Buffer.alloc(16), filePath: "voice.aac" }); + expect(mime).toBe("audio/aac"); + }); }); describe("extensionForMime", () => {