mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:34:31 +00:00
Slack: capture Block Kit view closed events
This commit is contained in:
@@ -31,9 +31,25 @@ type RegisteredViewHandler = (args: {
|
|||||||
};
|
};
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
type RegisteredViewClosedHandler = (args: {
|
||||||
|
ack: () => Promise<void>;
|
||||||
|
body: {
|
||||||
|
user?: { id?: string };
|
||||||
|
team?: { id?: string };
|
||||||
|
view?: {
|
||||||
|
id?: string;
|
||||||
|
callback_id?: string;
|
||||||
|
private_metadata?: string;
|
||||||
|
state?: { values?: Record<string, Record<string, Record<string, unknown>>> };
|
||||||
|
};
|
||||||
|
is_cleared?: boolean;
|
||||||
|
};
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
function createContext() {
|
function createContext() {
|
||||||
let handler: RegisteredHandler | null = null;
|
let handler: RegisteredHandler | null = null;
|
||||||
let viewHandler: RegisteredViewHandler | null = null;
|
let viewHandler: RegisteredViewHandler | null = null;
|
||||||
|
let viewClosedHandler: RegisteredViewClosedHandler | null = null;
|
||||||
const app = {
|
const app = {
|
||||||
action: vi.fn((_matcher: RegExp, next: RegisteredHandler) => {
|
action: vi.fn((_matcher: RegExp, next: RegisteredHandler) => {
|
||||||
handler = next;
|
handler = next;
|
||||||
@@ -41,6 +57,9 @@ function createContext() {
|
|||||||
view: vi.fn((_matcher: RegExp, next: RegisteredViewHandler) => {
|
view: vi.fn((_matcher: RegExp, next: RegisteredViewHandler) => {
|
||||||
viewHandler = next;
|
viewHandler = next;
|
||||||
}),
|
}),
|
||||||
|
viewClosed: vi.fn((_matcher: RegExp, next: RegisteredViewClosedHandler) => {
|
||||||
|
viewClosedHandler = next;
|
||||||
|
}),
|
||||||
client: {
|
client: {
|
||||||
chat: {
|
chat: {
|
||||||
update: vi.fn().mockResolvedValue(undefined),
|
update: vi.fn().mockResolvedValue(undefined),
|
||||||
@@ -61,6 +80,7 @@ function createContext() {
|
|||||||
resolveSessionKey,
|
resolveSessionKey,
|
||||||
getHandler: () => handler,
|
getHandler: () => handler,
|
||||||
getViewHandler: () => viewHandler,
|
getViewHandler: () => viewHandler,
|
||||||
|
getViewClosedHandler: () => viewClosedHandler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,4 +251,69 @@ describe("registerSlackInteractionEvents", () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("captures modal close events and enqueues view closed event", async () => {
|
||||||
|
enqueueSystemEventMock.mockReset();
|
||||||
|
const { ctx, getViewClosedHandler, resolveSessionKey } = createContext();
|
||||||
|
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||||
|
const viewClosedHandler = getViewClosedHandler();
|
||||||
|
expect(viewClosedHandler).toBeTruthy();
|
||||||
|
|
||||||
|
const ack = vi.fn().mockResolvedValue(undefined);
|
||||||
|
await viewClosedHandler!({
|
||||||
|
ack,
|
||||||
|
body: {
|
||||||
|
user: { id: "U900" },
|
||||||
|
team: { id: "T1" },
|
||||||
|
is_cleared: true,
|
||||||
|
view: {
|
||||||
|
id: "V900",
|
||||||
|
callback_id: "openclaw:deploy_form",
|
||||||
|
private_metadata: "run:123",
|
||||||
|
state: {
|
||||||
|
values: {
|
||||||
|
env_block: {
|
||||||
|
env_select: {
|
||||||
|
type: "static_select",
|
||||||
|
selected_option: {
|
||||||
|
text: { type: "plain_text", text: "Canary" },
|
||||||
|
value: "canary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ack).toHaveBeenCalled();
|
||||||
|
expect(resolveSessionKey).toHaveBeenCalledWith({});
|
||||||
|
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||||
|
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||||
|
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
|
||||||
|
interactionType: string;
|
||||||
|
actionId: string;
|
||||||
|
callbackId: string;
|
||||||
|
viewId: string;
|
||||||
|
userId: string;
|
||||||
|
isCleared: boolean;
|
||||||
|
privateMetadata: string;
|
||||||
|
inputs: Array<{ actionId: string; selectedValues?: string[] }>;
|
||||||
|
};
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
interactionType: "view_closed",
|
||||||
|
actionId: "view:openclaw:deploy_form",
|
||||||
|
callbackId: "openclaw:deploy_form",
|
||||||
|
viewId: "V900",
|
||||||
|
userId: "U900",
|
||||||
|
isCleared: true,
|
||||||
|
privateMetadata: "run:123",
|
||||||
|
});
|
||||||
|
expect(payload.inputs).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ actionId: "env_select", selectedValues: ["canary"] }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type SelectOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type InteractionSummary = {
|
type InteractionSummary = {
|
||||||
interactionType?: "block_action" | "view_submission";
|
interactionType?: "block_action" | "view_submission" | "view_closed";
|
||||||
actionId: string;
|
actionId: string;
|
||||||
blockId?: string;
|
blockId?: string;
|
||||||
actionType?: string;
|
actionType?: string;
|
||||||
@@ -322,4 +322,65 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const viewClosed = (
|
||||||
|
ctx.app as unknown as {
|
||||||
|
viewClosed?: (
|
||||||
|
matcher: RegExp,
|
||||||
|
handler: (args: { ack: () => Promise<void>; body: unknown }) => Promise<void>,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
).viewClosed;
|
||||||
|
if (typeof viewClosed !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle modal close events so agent workflows can react to cancelled forms.
|
||||||
|
viewClosed(
|
||||||
|
new RegExp(`^${OPENCLAW_ACTION_PREFIX}`),
|
||||||
|
async ({ ack, body }: { ack: () => Promise<void>; body: unknown }) => {
|
||||||
|
await ack();
|
||||||
|
|
||||||
|
const typedBody = body as {
|
||||||
|
user?: { id?: string };
|
||||||
|
team?: { id?: string };
|
||||||
|
view?: {
|
||||||
|
id?: string;
|
||||||
|
callback_id?: string;
|
||||||
|
private_metadata?: string;
|
||||||
|
state?: { values?: unknown };
|
||||||
|
};
|
||||||
|
is_cleared?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const callbackId = typedBody.view?.callback_id ?? "unknown";
|
||||||
|
const userId = typedBody.user?.id ?? "unknown";
|
||||||
|
const viewId = typedBody.view?.id;
|
||||||
|
const inputs = summarizeViewState(typedBody.view?.state?.values);
|
||||||
|
const eventPayload = {
|
||||||
|
interactionType: "view_closed",
|
||||||
|
actionId: `view:${callbackId}`,
|
||||||
|
callbackId,
|
||||||
|
viewId,
|
||||||
|
userId,
|
||||||
|
teamId: typedBody.team?.id,
|
||||||
|
isCleared: typedBody.is_cleared === true,
|
||||||
|
privateMetadata: typedBody.view?.private_metadata,
|
||||||
|
inputs,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.runtime.log?.(
|
||||||
|
`slack:interaction view_closed callback=${callbackId} user=${userId} cleared=${
|
||||||
|
typedBody.is_cleared === true
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, {
|
||||||
|
sessionKey: ctx.resolveSlackSystemEventSessionKey({}),
|
||||||
|
contextKey: ["slack:interaction:view-closed", callbackId, viewId, userId]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(":"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user