mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
fix(slack): process thread broadcasts as messages
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Slack/Assistant: accept Slack Assistant DM `message_changed` events when their metadata identifies the human sender, while continuing to drop self-authored bot edits. Fixes #55445. Thanks @AlfredPros.
|
||||
- Slack/native streaming: suppress reasoning-only payloads before `chat.startStream`/`appendStream`, so Claude extended-thinking blocks no longer appear as visible Slack messages. Fixes #59687. Thanks @vision-ifc.
|
||||
- Slack/block replies: keep multi-part block deliveries in the first Slack reply thread when `replyToMode` is `first`, matching text reply threading instead of leaking later blocks into the channel. Fixes #49341. Thanks @pholmstr and @xiwuqi.
|
||||
- Slack/thread broadcasts: process `thread_broadcast` events as user messages so replies sent with "Also send to channel" reach the agent instead of becoming metadata-only system events. Fixes #56605 and #4351. Thanks @clawSean and @jlowin.
|
||||
- Agents/failover: stop body-less HTTP 400/422 proxy failures from defaulting to `"format"` classification, so embedded retries surface the opaque provider failure instead of falling into a compaction loop. Fixes #66462. (#67024) Thanks @altaywtf and @HongzhuLiu.
|
||||
- Plugins/loader: use cached discovery-mode snapshot loads for read-only plugin capability lookups, keep snapshot caches isolated from active Gateway registries, and make same-plugin channel/HTTP route re-registration idempotent so repeated snapshot or hot-reload paths no longer rerun full plugin side effects or accumulate duplicate surfaces. Fixes #51781, #52031, #54181, and #57514. Thanks @livingghost, @okuyam2y, @ShionEria, and @bbshih.
|
||||
- Plugins/loader: reuse the compatible active Gateway registry for broad runtime plugin ensure calls after a gateway-bindable boot load, so non-bundled plugins no longer re-run `register()` during the same boot path. Fixes #69250. Thanks @markthebest12.
|
||||
|
||||
@@ -773,7 +773,8 @@ Same-chat `/approve` also works in Slack channels and DMs that already support c
|
||||
|
||||
## Events and operational behavior
|
||||
|
||||
- Message edits/deletes/thread broadcasts are mapped into system events.
|
||||
- Message edits/deletes are mapped into system events.
|
||||
- Thread broadcasts ("Also send to channel" thread replies) are processed as normal user messages.
|
||||
- Reaction add/remove events are mapped into system events.
|
||||
- Member join/leave, channel created/renamed, and pin add/remove events are mapped into system events.
|
||||
- `channel_id_changed` can migrate channel config keys when `configWrites` is enabled.
|
||||
|
||||
@@ -41,7 +41,7 @@ describe("resolveSlackMessageSubtypeHandler", () => {
|
||||
expect(handler?.describe("general")).toContain("deleted");
|
||||
});
|
||||
|
||||
it("resolves thread_broadcast metadata and identifiers", () => {
|
||||
it("does not treat thread_broadcast as a metadata-only system event", () => {
|
||||
const event = {
|
||||
type: "message",
|
||||
subtype: "thread_broadcast",
|
||||
@@ -51,13 +51,7 @@ describe("resolveSlackMessageSubtypeHandler", () => {
|
||||
user: "U1",
|
||||
} as unknown as SlackMessageEvent;
|
||||
|
||||
const handler = resolveSlackMessageSubtypeHandler(event);
|
||||
expect(handler?.eventKind).toBe("thread_broadcast");
|
||||
expect(handler?.resolveSenderId(event)).toBe("U1");
|
||||
expect(handler?.resolveChannelId(event)).toBe("C1");
|
||||
expect(handler?.resolveChannelType(event)).toBeUndefined();
|
||||
expect(handler?.contextKey(event)).toBe("slack:thread:broadcast:C1:123.456");
|
||||
expect(handler?.describe("general")).toContain("broadcast");
|
||||
expect(resolveSlackMessageSubtypeHandler(event)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for regular messages", () => {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { SlackMessageEvent } from "../../types.js";
|
||||
import type {
|
||||
SlackMessageChangedEvent,
|
||||
SlackMessageDeletedEvent,
|
||||
SlackThreadBroadcastEvent,
|
||||
} from "../types.js";
|
||||
import type { SlackMessageChangedEvent, SlackMessageDeletedEvent } from "../types.js";
|
||||
|
||||
type SupportedSubtype = "message_changed" | "message_deleted" | "thread_broadcast";
|
||||
type SupportedSubtype = "message_changed" | "message_deleted";
|
||||
|
||||
export type SlackMessageSubtypeHandler = {
|
||||
subtype: SupportedSubtype;
|
||||
@@ -59,39 +55,16 @@ const deletedHandler: SlackMessageSubtypeHandler = {
|
||||
resolveChannelType: () => undefined,
|
||||
};
|
||||
|
||||
const threadBroadcastHandler: SlackMessageSubtypeHandler = {
|
||||
subtype: "thread_broadcast",
|
||||
eventKind: "thread_broadcast",
|
||||
describe: (channelLabel) => `Slack thread reply broadcast in ${channelLabel}.`,
|
||||
contextKey: (event) => {
|
||||
const thread = event as SlackThreadBroadcastEvent;
|
||||
const channelId = thread.channel ?? "unknown";
|
||||
const messageId = thread.message?.ts ?? thread.event_ts ?? "unknown";
|
||||
return `slack:thread:broadcast:${channelId}:${messageId}`;
|
||||
},
|
||||
resolveSenderId: (event) => {
|
||||
const thread = event as SlackThreadBroadcastEvent;
|
||||
return thread.user ?? thread.message?.user ?? thread.message?.bot_id;
|
||||
},
|
||||
resolveChannelId: (event) => (event as SlackThreadBroadcastEvent).channel,
|
||||
resolveChannelType: () => undefined,
|
||||
};
|
||||
|
||||
const SUBTYPE_HANDLER_REGISTRY: Record<SupportedSubtype, SlackMessageSubtypeHandler> = {
|
||||
message_changed: changedHandler,
|
||||
message_deleted: deletedHandler,
|
||||
thread_broadcast: threadBroadcastHandler,
|
||||
};
|
||||
|
||||
export function resolveSlackMessageSubtypeHandler(
|
||||
event: SlackMessageEvent,
|
||||
): SlackMessageSubtypeHandler | undefined {
|
||||
const subtype = event.subtype;
|
||||
if (
|
||||
subtype !== "message_changed" &&
|
||||
subtype !== "message_deleted" &&
|
||||
subtype !== "thread_broadcast"
|
||||
) {
|
||||
if (subtype !== "message_changed" && subtype !== "message_deleted") {
|
||||
return undefined;
|
||||
}
|
||||
return SUBTYPE_HANDLER_REGISTRY[subtype];
|
||||
|
||||
@@ -189,18 +189,6 @@ describe("registerSlackMessageEvents", () => {
|
||||
},
|
||||
calls: 0,
|
||||
},
|
||||
{
|
||||
name: "blocks thread_broadcast system events without an authenticated sender",
|
||||
input: {
|
||||
overrides: { dmPolicy: "open" },
|
||||
event: {
|
||||
...makeThreadBroadcastEvent(),
|
||||
user: undefined,
|
||||
message: { ts: "123.456" },
|
||||
},
|
||||
},
|
||||
calls: 0,
|
||||
},
|
||||
];
|
||||
it.each(cases)("$name", async ({ input, calls }) => {
|
||||
await runMessageCase(input);
|
||||
@@ -224,6 +212,25 @@ describe("registerSlackMessageEvents", () => {
|
||||
expect(messageQueueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes thread_broadcast events to the message handler", async () => {
|
||||
const { handleSlackMessage } = await invokeRegisteredHandler({
|
||||
eventName: "message",
|
||||
overrides: { dmPolicy: "open" },
|
||||
event: makeThreadBroadcastEvent({ channel: "C1", user: "U1" }),
|
||||
});
|
||||
|
||||
expect(handleSlackMessage).toHaveBeenCalledTimes(1);
|
||||
expect(handleSlackMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subtype: "thread_broadcast",
|
||||
channel: "C1",
|
||||
user: "U1",
|
||||
}),
|
||||
{ source: "message" },
|
||||
);
|
||||
expect(messageQueueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rehydrates assistant DM message_changed events with a metadata user as inbound messages", async () => {
|
||||
const { handleSlackMessage } = await invokeRegisteredHandler({
|
||||
eventName: "message",
|
||||
|
||||
@@ -126,6 +126,47 @@ describe("createSlackMessageHandler", () => {
|
||||
expect(enqueueMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("accepts thread_broadcast messages from the message stream", async () => {
|
||||
const { handler, trackEvent } = createHandlerWithTracker();
|
||||
|
||||
await handler(
|
||||
{
|
||||
type: "message",
|
||||
subtype: "thread_broadcast",
|
||||
channel: "C111",
|
||||
user: "U111",
|
||||
ts: "1709000000.000300",
|
||||
text: "also send to channel",
|
||||
thread_ts: "1709000000.000100",
|
||||
} as never,
|
||||
{ source: "message" },
|
||||
);
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(resolveThreadTsMock).toHaveBeenCalledTimes(1);
|
||||
expect(enqueueMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops message subtypes that do not carry user message text", async () => {
|
||||
const { handler, trackEvent } = createHandlerWithTracker();
|
||||
|
||||
await handler(
|
||||
{
|
||||
type: "message",
|
||||
subtype: "channel_join",
|
||||
channel: "C111",
|
||||
user: "U111",
|
||||
ts: "1709000000.000400",
|
||||
text: "<@U111> joined the channel",
|
||||
} as never,
|
||||
{ source: "message" },
|
||||
);
|
||||
|
||||
expect(trackEvent).not.toHaveBeenCalled();
|
||||
expect(resolveThreadTsMock).not.toHaveBeenCalled();
|
||||
expect(enqueueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("flushes pending top-level buffered keys before immediate non-debounce follow-ups", async () => {
|
||||
const handler = createSlackMessageHandler({
|
||||
ctx: createContext(),
|
||||
|
||||
@@ -195,7 +195,8 @@ export function createSlackMessageHandler(params: {
|
||||
opts.source === "message" &&
|
||||
message.subtype &&
|
||||
message.subtype !== "file_share" &&
|
||||
message.subtype !== "bot_message"
|
||||
message.subtype !== "bot_message" &&
|
||||
message.subtype !== "thread_broadcast"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user