fix(signal): classify filename-only voice notes

This commit is contained in:
Peter Steinberger
2026-04-25 06:45:54 +01:00
parent f44759073b
commit 537a8e25ed
5 changed files with 49 additions and 2 deletions

View File

@@ -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.

View File

@@ -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).

View File

@@ -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 };
}

View File

@@ -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(

View File

@@ -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", () => {