fix(matrix): preserve send aliases and voice intent

This commit is contained in:
Vincent Koc
2026-03-22 20:35:40 -07:00
committed by GitHub
parent 1354f37c88
commit 50bc625203
6 changed files with 78 additions and 2 deletions

View File

@@ -220,6 +220,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
- Plugins/Matrix: accept shared send-tool media aliases (`mediaUrl`, `filePath`, `path`) and preserve `asVoice` / `audioAsVoice` through Matrix action dispatch so media-only sends and voice-message intents reach the plugin send layer correctly. Thanks @psacc and @vincentkoc.
- Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc.
- macOS/node service startup: use `openclaw node start/stop --json` from the Mac app instead of the removed `openclaw service node ...` command shape, so current CLI installs expose the full node exec surface again. (#46843) Fixes #43171. Thanks @Br1an67.
- macOS/launch at login: stop emitting `KeepAlive` for the desktop app launch agent so OpenClaw no longer relaunches immediately after a manual quit while launch at login remains enabled. (#40213) Thanks @stablegenius49.

View File

@@ -181,4 +181,30 @@ describe("matrixMessageActions account propagation", () => {
{ mediaLocalRoots: undefined },
);
});
it("accepts shared media aliases and forwards voice-send intent", async () => {
await matrixMessageActions.handleAction?.(
createContext({
action: "send",
accountId: "ops",
params: {
to: "room:!room:example",
filePath: "/tmp/clip.mp3",
asVoice: true,
},
}),
);
expect(mocks.handleMatrixAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "sendMessage",
accountId: "ops",
content: undefined,
mediaUrl: "/tmp/clip.mp3",
audioAsVoice: true,
}),
expect.any(Object),
{ mediaLocalRoots: undefined },
);
});
});

View File

@@ -162,13 +162,23 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
if (action === "send") {
const to = readStringParam(params, "to", { required: true });
const mediaUrl = readStringParam(params, "media", { trim: false });
const mediaUrl =
readStringParam(params, "media", { trim: false }) ??
readStringParam(params, "mediaUrl", { trim: false }) ??
readStringParam(params, "filePath", { trim: false }) ??
readStringParam(params, "path", { trim: false });
const content = readStringParam(params, "message", {
required: !mediaUrl,
allowEmpty: true,
});
const replyTo = readStringParam(params, "replyTo");
const threadId = readStringParam(params, "threadId");
const audioAsVoice =
typeof params.asVoice === "boolean"
? params.asVoice
: typeof params.audioAsVoice === "boolean"
? params.audioAsVoice
: undefined;
return await dispatch({
action: "sendMessage",
to,
@@ -176,6 +186,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
mediaUrl: mediaUrl ?? undefined,
replyToId: replyTo ?? undefined,
threadId: threadId ?? undefined,
audioAsVoice,
});
}

View File

@@ -21,6 +21,7 @@ export async function sendMatrixMessage(
mediaUrl?: string;
replyToId?: string;
threadId?: string;
audioAsVoice?: boolean;
} = {},
) {
return await sendMessageMatrix(to, content, {
@@ -29,6 +30,7 @@ export async function sendMatrixMessage(
mediaLocalRoots: opts.mediaLocalRoots,
replyToId: opts.replyToId,
threadId: opts.threadId,
audioAsVoice: opts.audioAsVoice,
accountId: opts.accountId ?? undefined,
client: opts.client,
timeoutMs: opts.timeoutMs,

View File

@@ -249,6 +249,31 @@ describe("handleMatrixAction pollVote", () => {
});
});
it("accepts shared media aliases and voice-send flags", async () => {
const cfg = { channels: { matrix: { actions: { messages: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "sendMessage",
accountId: "ops",
to: "room:!room:example",
path: "/tmp/clip.mp3",
asVoice: true,
},
cfg,
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test"] },
);
expect(mocks.sendMatrixMessage).toHaveBeenCalledWith("room:!room:example", undefined, {
cfg,
accountId: "ops",
mediaUrl: "/tmp/clip.mp3",
mediaLocalRoots: ["/tmp/openclaw-matrix-test"],
replyToId: undefined,
threadId: undefined,
audioAsVoice: true,
});
});
it("passes mediaLocalRoots to profile updates", async () => {
const cfg = { channels: { matrix: { actions: { profile: true } } } } as CoreConfig;
await handleMatrixAction(

View File

@@ -219,7 +219,11 @@ export async function handleMatrixAction(
switch (action) {
case "sendMessage": {
const to = readStringParam(params, "to", { required: true });
const mediaUrl = readStringParam(params, "mediaUrl");
const mediaUrl =
readStringParam(params, "mediaUrl", { trim: false }) ??
readStringParam(params, "media", { trim: false }) ??
readStringParam(params, "filePath", { trim: false }) ??
readStringParam(params, "path", { trim: false });
const content = readStringParam(params, "content", {
required: !mediaUrl,
allowEmpty: true,
@@ -227,11 +231,18 @@ export async function handleMatrixAction(
const replyToId =
readStringParam(params, "replyToId") ?? readStringParam(params, "replyTo");
const threadId = readStringParam(params, "threadId");
const audioAsVoice =
typeof readRawParam(params, "audioAsVoice") === "boolean"
? (readRawParam(params, "audioAsVoice") as boolean)
: typeof readRawParam(params, "asVoice") === "boolean"
? (readRawParam(params, "asVoice") as boolean)
: undefined;
const result = await sendMatrixMessage(to, content, {
mediaUrl: mediaUrl ?? undefined,
mediaLocalRoots: opts.mediaLocalRoots,
replyToId: replyToId ?? undefined,
threadId: threadId ?? undefined,
audioAsVoice,
...clientOpts,
});
return jsonResult({ ok: true, result });