mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 14:02:56 +08:00
fix: preserve matrix approval metadata fallback
This commit is contained in:
@@ -208,6 +208,12 @@ Notes:
|
||||
- Media replies always send attachments normally. If a stale preview can no longer be reused safely, OpenClaw redacts it before sending the final media reply.
|
||||
- Preview edits cost extra Matrix API calls. Leave `streaming: "off"` if you want the most conservative rate-limit profile.
|
||||
|
||||
## Approval metadata
|
||||
|
||||
Matrix native approval prompts are normal `m.room.message` events with OpenClaw-specific custom event content under `com.openclaw.approval`. Matrix permits custom event-content keys, so stock clients still render the text body while OpenClaw-aware clients can read the structured approval id, kind, state, available decisions, and exec/plugin details.
|
||||
|
||||
When an approval prompt is too long for one Matrix event, OpenClaw chunks the visible text and attaches `com.openclaw.approval` to the first chunk only. Reactions for allow/deny decisions are bound to that first event, so long prompts keep the same approval target as single-event prompts.
|
||||
|
||||
### Self-hosted push rules for quiet finalized previews
|
||||
|
||||
`streaming: "quiet"` only notifies recipients once a block or turn is finalized — a per-user push rule has to match the finalized preview marker. See [Matrix push rules for quiet previews](/channels/matrix-push-rules) for the full recipe (recipient token, pusher check, rule install, per-homeserver notes).
|
||||
|
||||
@@ -301,6 +301,9 @@ Shared behavior:
|
||||
without a second Slack-local fallback layer
|
||||
- Matrix native DM/channel routing and reaction shortcuts handle both exec and plugin approvals;
|
||||
plugin authorization still comes from `channels.matrix.dm.allowFrom`
|
||||
- Matrix native prompts include `com.openclaw.approval` custom event content on the first prompt
|
||||
event so OpenClaw-aware Matrix clients can read structured approval state while stock clients
|
||||
keep the plain-text `/approve` fallback
|
||||
- the requester does not need to be an approver
|
||||
- the originating chat can approve directly with `/approve` when that chat already supports commands and replies
|
||||
- native Discord approval buttons route by approval id kind: `plugin:` ids go
|
||||
|
||||
@@ -228,6 +228,7 @@ describe("matrixApprovalNativeRuntime", () => {
|
||||
pendingPayload.text,
|
||||
expect.objectContaining({
|
||||
accountId: "default",
|
||||
extraContent: pendingPayload.extraContent,
|
||||
}),
|
||||
);
|
||||
expect(reactMessage).toHaveBeenCalledWith(
|
||||
|
||||
@@ -218,7 +218,6 @@ function buildMatrixApprovalMetadata(params: {
|
||||
kind: params.view.approvalKind,
|
||||
phase: params.view.phase,
|
||||
title: params.view.title,
|
||||
description: params.view.description ?? undefined,
|
||||
expiresAtMs: params.view.expiresAtMs,
|
||||
metadata: params.view.metadata,
|
||||
allowedDecisions: Array.from(params.allowedDecisions),
|
||||
@@ -433,6 +432,7 @@ export const matrixApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
extraContent: pendingPayload.extraContent,
|
||||
});
|
||||
}
|
||||
const messageIds = Array.from(
|
||||
|
||||
@@ -630,6 +630,28 @@ describe("sendMessageMatrix threads", () => {
|
||||
messageIds: ["$m1", "$m2", "$m3"],
|
||||
});
|
||||
});
|
||||
|
||||
it("merges extra content into only the first chunked text event", async () => {
|
||||
const { client, sendMessage } = makeClient();
|
||||
convertMarkdownTablesMock.mockImplementation(() => "first|second|third");
|
||||
chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("|"));
|
||||
|
||||
await sendMessageMatrix("room:!room:example", "ignored", {
|
||||
client,
|
||||
cfg: {} as never,
|
||||
extraContent: { "com.openclaw.approval": { id: "req-1" } },
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(3);
|
||||
expect(sendMessage.mock.calls[0]?.[1]).toMatchObject({
|
||||
body: "first",
|
||||
"com.openclaw.approval": { id: "req-1" },
|
||||
});
|
||||
expect(sendMessage.mock.calls[1]?.[1]).toMatchObject({ body: "second" });
|
||||
expect(sendMessage.mock.calls[1]?.[1]).not.toHaveProperty("com.openclaw.approval");
|
||||
expect(sendMessage.mock.calls[2]?.[1]).toMatchObject({ body: "third" });
|
||||
expect(sendMessage.mock.calls[2]?.[1]).not.toHaveProperty("com.openclaw.approval");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendSingleTextMessageMatrix", () => {
|
||||
|
||||
@@ -211,8 +211,11 @@ export async function sendMessageMatrix(
|
||||
const relation = threadId
|
||||
? buildThreadRelation(threadId, opts.replyToId)
|
||||
: buildReplyRelation(opts.replyToId);
|
||||
let pendingExtraContent = opts.extraContent;
|
||||
const sendContent = async (content: MatrixOutboundContent) => {
|
||||
const eventId = await client.sendMessage(roomId, content);
|
||||
const contentWithExtra = withMatrixExtraContentFields(content, pendingExtraContent);
|
||||
pendingExtraContent = undefined;
|
||||
const eventId = await client.sendMessage(roomId, contentWithExtra);
|
||||
return eventId;
|
||||
};
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ export type MatrixSendOpts = {
|
||||
replyToId?: string;
|
||||
threadId?: string | number | null;
|
||||
timeoutMs?: number;
|
||||
/** Additional Matrix event content fields to merge into the first sent event. */
|
||||
extraContent?: MatrixExtraContentFields;
|
||||
/** Send audio as voice message instead of audio file. Defaults to false. */
|
||||
audioAsVoice?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user