mirror of https://github.com/openclaw/openclaw.git
fix(slack): route system events to bound agent sessions (#34045)
* fix(slack): route system events via binding-aware session keys * fix(slack): pass sender to system event session resolver * fix(slack): include sender context for interaction session routing * fix(slack): include modal submitter in session routing * test(slack): cover binding-aware system event routing * test(slack): update interaction session key assertions * test(slack): assert reaction session routing carries sender * docs(changelog): note slack system event routing fix * Update CHANGELOG.md
This commit is contained in:
parent
7b5e64ef2e
commit
c1bb07bd16
|
|
@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
### Fixes
|
||||
|
||||
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.
|
||||
- Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus.
|
||||
- Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus.
|
||||
- TUI/session-key canonicalization: normalize `openclaw tui --session` values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { DmPolicy, GroupPolicy } from "../../config/types.js";
|
|||
import { logVerbose } from "../../globals.js";
|
||||
import { createDedupeCache } from "../../infra/dedupe.js";
|
||||
import { getChildLogger } from "../../logging.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { SlackMessageEvent } from "../types.js";
|
||||
import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
||||
|
|
@ -62,6 +63,7 @@ export type SlackMonitorContext = {
|
|||
resolveSlackSystemEventSessionKey: (params: {
|
||||
channelId?: string | null;
|
||||
channelType?: string | null;
|
||||
senderId?: string | null;
|
||||
}) => string;
|
||||
isChannelAllowed: (params: {
|
||||
channelId?: string;
|
||||
|
|
@ -151,6 +153,7 @@ export function createSlackMonitorContext(params: {
|
|||
const resolveSlackSystemEventSessionKey = (p: {
|
||||
channelId?: string | null;
|
||||
channelType?: string | null;
|
||||
senderId?: string | null;
|
||||
}) => {
|
||||
const channelId = p.channelId?.trim() ?? "";
|
||||
if (!channelId) {
|
||||
|
|
@ -165,6 +168,27 @@ export function createSlackMonitorContext(params: {
|
|||
? `slack:group:${channelId}`
|
||||
: `slack:channel:${channelId}`;
|
||||
const chatType = isDirectMessage ? "direct" : isGroup ? "group" : "channel";
|
||||
const senderId = p.senderId?.trim() ?? "";
|
||||
|
||||
// Resolve through shared channel/account bindings so system events route to
|
||||
// the same agent session as regular inbound messages.
|
||||
try {
|
||||
const peerKind = isDirectMessage ? "direct" : isGroup ? "group" : "channel";
|
||||
const peerId = isDirectMessage ? senderId : channelId;
|
||||
if (peerId) {
|
||||
const route = resolveAgentRoute({
|
||||
cfg: params.cfg,
|
||||
channel: "slack",
|
||||
accountId: params.accountId,
|
||||
teamId: params.teamId,
|
||||
peer: { kind: peerKind, id: peerId },
|
||||
});
|
||||
return route.sessionKey;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to legacy key derivation.
|
||||
}
|
||||
|
||||
return resolveSessionKey(
|
||||
params.sessionScope,
|
||||
{ From: from, ChatType: chatType, Provider: "slack" },
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ type SlackInteractionContextPrefix = "slack:interaction:view" | "slack:interacti
|
|||
function resolveModalSessionRouting(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
metadata: ReturnType<typeof parseSlackModalPrivateMetadata>;
|
||||
userId?: string;
|
||||
}): { sessionKey: string; channelId?: string; channelType?: string } {
|
||||
const metadata = params.metadata;
|
||||
if (metadata.sessionKey) {
|
||||
|
|
@ -91,6 +92,7 @@ function resolveModalSessionRouting(params: {
|
|||
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
senderId: params.userId,
|
||||
}),
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
|
|
@ -139,6 +141,7 @@ function resolveSlackModalEventBase(params: {
|
|||
const sessionRouting = resolveModalSessionRouting({
|
||||
ctx: params.ctx,
|
||||
metadata,
|
||||
userId,
|
||||
});
|
||||
return {
|
||||
callbackId,
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||
channelId: "C1",
|
||||
channelType: "channel",
|
||||
senderId: "U123",
|
||||
});
|
||||
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
|
@ -554,6 +555,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||
channelId: "C222",
|
||||
channelType: "channel",
|
||||
senderId: "U111",
|
||||
});
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||
|
|
@ -952,6 +954,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||
channelId: "D123",
|
||||
channelType: "im",
|
||||
senderId: "U777",
|
||||
});
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||
|
|
|
|||
|
|
@ -571,6 +571,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
|||
const sessionKey = ctx.resolveSlackSystemEventSessionKey({
|
||||
channelId: channelId,
|
||||
channelType: auth.channelType,
|
||||
senderId: userId,
|
||||
});
|
||||
|
||||
// Build context key - only include defined values to avoid "unknown" noise
|
||||
|
|
|
|||
|
|
@ -153,4 +153,26 @@ describe("registerSlackReactionEvents", () => {
|
|||
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes sender context when resolving reaction session keys", async () => {
|
||||
reactionQueueMock.mockClear();
|
||||
reactionAllowMock.mockReset().mockResolvedValue([]);
|
||||
const harness = createSlackSystemEventTestHarness();
|
||||
const resolveSessionKey = vi.fn().mockReturnValue("agent:ops:main");
|
||||
harness.ctx.resolveSlackSystemEventSessionKey = resolveSessionKey;
|
||||
registerSlackReactionEvents({ ctx: harness.ctx });
|
||||
const handler = harness.getHandler("reaction_added");
|
||||
expect(handler).toBeTruthy();
|
||||
|
||||
await handler!({
|
||||
event: buildReactionEvent({ user: "U777", channel: "D123" }),
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||
channelId: "D123",
|
||||
channelType: "im",
|
||||
senderId: "U777",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export async function authorizeAndResolveSlackSystemEventContext(params: {
|
|||
const sessionKey = ctx.resolveSlackSystemEventSessionKey({
|
||||
channelId,
|
||||
channelType: auth.channelType,
|
||||
senderId,
|
||||
});
|
||||
return {
|
||||
channelLabel,
|
||||
|
|
|
|||
|
|
@ -184,6 +184,53 @@ describe("resolveSlackSystemEventSessionKey", () => {
|
|||
"agent:main:slack:channel:c123",
|
||||
);
|
||||
});
|
||||
|
||||
it("routes channel system events through account bindings", () => {
|
||||
const ctx = createSlackMonitorContext({
|
||||
...baseParams(),
|
||||
accountId: "work",
|
||||
cfg: {
|
||||
bindings: [
|
||||
{
|
||||
agentId: "ops",
|
||||
match: {
|
||||
channel: "slack",
|
||||
accountId: "work",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(
|
||||
ctx.resolveSlackSystemEventSessionKey({ channelId: "C123", channelType: "channel" }),
|
||||
).toBe("agent:ops:slack:channel:c123");
|
||||
});
|
||||
|
||||
it("routes DM system events through direct-peer bindings when sender is known", () => {
|
||||
const ctx = createSlackMonitorContext({
|
||||
...baseParams(),
|
||||
accountId: "work",
|
||||
cfg: {
|
||||
bindings: [
|
||||
{
|
||||
agentId: "ops-dm",
|
||||
match: {
|
||||
channel: "slack",
|
||||
accountId: "work",
|
||||
peer: { kind: "direct", id: "U123" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(
|
||||
ctx.resolveSlackSystemEventSessionKey({
|
||||
channelId: "D123",
|
||||
channelType: "im",
|
||||
senderId: "U123",
|
||||
}),
|
||||
).toBe("agent:ops-dm:main");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isChannelAllowed with groupPolicy and channelsConfig", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue