mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(slack extension): preserve thread IDs for read + outbound delivery (#23836)
* Slack Extension: preserve thread IDs in reads and outbound sends * Slack extension: fix threadTs typing and action test context * Update CHANGELOG.md
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/Extension: forward `message read` `threadId` to `readMessages` and use delivery-context `threadId` as outbound `thread_ts` fallback so extension replies/reads stay in the correct Slack thread. (#22216, #22485, #23836) Thanks @vincentkoc, @lan17 and @dorukardahan.
|
||||
- Config/Memory: allow `"mistral"` in `agents.defaults.memorySearch.provider` and `agents.defaults.memorySearch.fallback` schema validation. (#14934) Thanks @ThomsenDrake.
|
||||
- Security/Feishu: enforce ID-only allowlist matching for DM/group sender authorization, normalize Feishu ID prefixes during checks, and ignore mutable display names so display-name collisions cannot satisfy allowlist entries. This ships in the next npm release. Thanks @jiseoung for reporting.
|
||||
- Feishu/Commands: in group chats, command authorization now falls back to top-level `channels.feishu.allowFrom` when per-group `allowFrom` is not set, so `/command` no longer gets blocked by an unintended empty allowlist. (#23756)
|
||||
|
||||
106
extensions/slack/src/channel.test.ts
Normal file
106
extensions/slack/src/channel.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const handleSlackActionMock = vi.fn();
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getSlackRuntime: () => ({
|
||||
channel: {
|
||||
slack: {
|
||||
handleSlackAction: handleSlackActionMock,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import { slackPlugin } from "./channel.js";
|
||||
|
||||
describe("slackPlugin actions", () => {
|
||||
it("forwards read threadId to Slack action handler", async () => {
|
||||
handleSlackActionMock.mockResolvedValueOnce({ messages: [], hasMore: false });
|
||||
const handleAction = slackPlugin.actions?.handleAction;
|
||||
expect(handleAction).toBeDefined();
|
||||
|
||||
await handleAction!({
|
||||
action: "read",
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
cfg: {},
|
||||
params: {
|
||||
channelId: "C123",
|
||||
threadId: "1712345678.123456",
|
||||
},
|
||||
});
|
||||
|
||||
expect(handleSlackActionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "readMessages",
|
||||
channelId: "C123",
|
||||
threadId: "1712345678.123456",
|
||||
}),
|
||||
{},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("slackPlugin outbound", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("uses threadId as threadTs fallback for sendText", async () => {
|
||||
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-text" });
|
||||
const sendText = slackPlugin.outbound?.sendText;
|
||||
expect(sendText).toBeDefined();
|
||||
|
||||
const result = await sendText!({
|
||||
cfg,
|
||||
to: "C123",
|
||||
text: "hello",
|
||||
accountId: "default",
|
||||
threadId: "1712345678.123456",
|
||||
deps: { sendSlack },
|
||||
});
|
||||
|
||||
expect(sendSlack).toHaveBeenCalledWith(
|
||||
"C123",
|
||||
"hello",
|
||||
expect.objectContaining({
|
||||
threadTs: "1712345678.123456",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ channel: "slack", messageId: "m-text" });
|
||||
});
|
||||
|
||||
it("prefers replyToId over threadId for sendMedia", async () => {
|
||||
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-media" });
|
||||
const sendMedia = slackPlugin.outbound?.sendMedia;
|
||||
expect(sendMedia).toBeDefined();
|
||||
|
||||
const result = await sendMedia!({
|
||||
cfg,
|
||||
to: "C999",
|
||||
text: "caption",
|
||||
mediaUrl: "https://example.com/image.png",
|
||||
accountId: "default",
|
||||
replyToId: "1712000000.000001",
|
||||
threadId: "1712345678.123456",
|
||||
deps: { sendSlack },
|
||||
});
|
||||
|
||||
expect(sendSlack).toHaveBeenCalledWith(
|
||||
"C999",
|
||||
"caption",
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://example.com/image.png",
|
||||
threadTs: "1712000000.000001",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ channel: "slack", messageId: "m-media" });
|
||||
});
|
||||
});
|
||||
@@ -245,6 +245,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
await handleSlackMessageAction({
|
||||
providerId: meta.id,
|
||||
ctx,
|
||||
includeReadThreadId: true,
|
||||
invoke: async (action, cfg, toolContext) =>
|
||||
await getSlackRuntime().channel.slack.handleSlackAction(action, cfg, toolContext),
|
||||
}),
|
||||
@@ -324,28 +325,30 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
deliveryMode: "direct",
|
||||
chunker: null,
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, cfg }) => {
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId, cfg }) => {
|
||||
const send = deps?.sendSlack ?? getSlackRuntime().channel.slack.sendMessageSlack;
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const token = getTokenForOperation(account, "write");
|
||||
const botToken = account.botToken?.trim();
|
||||
const tokenOverride = token && token !== botToken ? token : undefined;
|
||||
const threadTsValue = replyToId ?? threadId;
|
||||
const result = await send(to, text, {
|
||||
threadTs: replyToId ?? undefined,
|
||||
threadTs: threadTsValue != null ? String(threadTsValue) : undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
...(tokenOverride ? { token: tokenOverride } : {}),
|
||||
});
|
||||
return { channel: "slack", ...result };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, cfg }) => {
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, threadId, cfg }) => {
|
||||
const send = deps?.sendSlack ?? getSlackRuntime().channel.slack.sendMessageSlack;
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const token = getTokenForOperation(account, "write");
|
||||
const botToken = account.botToken?.trim();
|
||||
const tokenOverride = token && token !== botToken ? token : undefined;
|
||||
const threadTsValue = replyToId ?? threadId;
|
||||
const result = await send(to, text, {
|
||||
mediaUrl,
|
||||
threadTs: replyToId ?? undefined,
|
||||
threadTs: threadTsValue != null ? String(threadTsValue) : undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
...(tokenOverride ? { token: tokenOverride } : {}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user