mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 23:56:07 +00:00
fix(slack): improve bot parity
This commit is contained in:
@@ -29,6 +29,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Codex app-server: report Codex-native tool execution to diagnostics so long-running native `bash`, web, file, and MCP tools no longer look like stale embedded runs to the watchdog. (#80217)
|
||||
- Telegram: preserve blank lines between manually indented bullet blocks and following numbered sections in rendered replies. Fixes #76998. Thanks @evgyur.
|
||||
- Slack: pass configured agent identity through draft preview sends so partial streaming replies keep custom username/avatar on the initial Slack message. Fixes #38235. (#38237) Thanks @lacymorrow.
|
||||
- Slack: support `allowBots: "mentions"` for bot-authored messages that mention the receiving bot, matching the documented Discord-style mode without accepting every bot message. Fixes #43587. (#43588) Thanks @raw34.
|
||||
- Gateway/agents: keep structured reasons when active-run queueing fails and deprecate the legacy boolean queue helper, so steering and subagent wake diagnostics distinguish completed, non-streaming, and compacting runs. Fixes #80156. Thanks @markus-lassfolk.
|
||||
- Agents/UI: compact exec and tool progress rows by hiding redundant shell tool names, replacing known workspace paths with short context markers, and preserving Discord trace scrubbing for compact command lines.
|
||||
- ACPX: run and await the embedded ACP backend startup probe by default so the gateway `ready` signal no longer fires before the acpx runtime has either become usable or reported a probe failure; set `OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=0` to restore lazy startup. Fixes #79596. Thanks @bzelones.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
da702349b376821e0bc1420a945287dea0bccc79298e269abb028718983e94a5 config-baseline.json
|
||||
8c647da77392bd4e87aac07fbdfc7592bbd656dc09f8844759d2c65dc374bd0d config-baseline.core.json
|
||||
80f0f51caedf14dc2138d975b62852ff7c5cf085df1c734c9de279f5859a7eeb config-baseline.channel.json
|
||||
dba159f639977bb96d79f0b78de2c6de48d25ed6ba1590f55812affb7ca6e4b0 config-baseline.plugin.json
|
||||
f8d50da8d51a648598ed9165a6994af254e73e64ad037dc26b4742198b078a8c config-baseline.json
|
||||
67c58457ed2b525975cdb053489f92a5f840c8cf982666393e111fd327dd132e config-baseline.core.json
|
||||
a543b4d5132b4b0bcafa38e20d9ad07c78df1dc2b73633a6fdb03990cf3af918 config-baseline.channel.json
|
||||
18f71e9d4a62fe68fbd5bf18d5833a4e380fc705ad641769e1cf05794286344c config-baseline.plugin.json
|
||||
|
||||
@@ -88,6 +88,30 @@ describe("createSlackDraftStream", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards identity to the initial send call", async () => {
|
||||
const identity = { username: "test-agent", iconEmoji: ":robot_face:" };
|
||||
const send = vi.fn<DraftSendFn>(async () => slackDraftSendResult("111.222"));
|
||||
const stream = createSlackDraftStream({
|
||||
target: "channel:C123",
|
||||
cfg: TEST_CFG,
|
||||
token: "xoxb-test",
|
||||
throttleMs: 250,
|
||||
identity,
|
||||
send,
|
||||
edit: vi.fn<DraftEditFn>(async () => {}),
|
||||
remove: vi.fn<DraftRemoveFn>(async () => {}),
|
||||
});
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
|
||||
expect(send).toHaveBeenCalledWith(
|
||||
"channel:C123",
|
||||
"hello",
|
||||
expect.objectContaining({ identity }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not send duplicate text", async () => {
|
||||
const { stream, send, edit } = createDraftStreamHarness();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { deleteSlackMessage, editSlackMessage } from "./actions.js";
|
||||
import { SLACK_TEXT_LIMIT } from "./limits.js";
|
||||
import type { SlackSendIdentity } from "./send.js";
|
||||
import { sendMessageSlack } from "./send.js";
|
||||
|
||||
const DEFAULT_THROTTLE_MS = 1000;
|
||||
@@ -32,6 +33,7 @@ export function createSlackDraftStream(params: {
|
||||
cfg: OpenClawConfig;
|
||||
token: string;
|
||||
accountId?: string;
|
||||
identity?: SlackSendIdentity;
|
||||
maxChars?: number;
|
||||
throttleMs?: number;
|
||||
resolveThreadTs?: () => string | undefined;
|
||||
@@ -92,6 +94,7 @@ export function createSlackDraftStream(params: {
|
||||
token: params.token,
|
||||
accountId: params.accountId,
|
||||
threadTs: params.resolveThreadTs?.(),
|
||||
identity: params.identity,
|
||||
...(blocks ? { blocks } : {}),
|
||||
});
|
||||
streamChannelId = sent.channelId || streamChannelId;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { normalizeSlackSlug } from "./allow-list.js";
|
||||
export type SlackChannelConfigResolved = {
|
||||
allowed: boolean;
|
||||
requireMention: boolean;
|
||||
allowBots?: boolean;
|
||||
allowBots?: boolean | "mentions";
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
@@ -21,7 +21,7 @@ export type SlackChannelConfigResolved = {
|
||||
type SlackChannelConfigEntry = {
|
||||
enabled?: boolean;
|
||||
requireMention?: boolean;
|
||||
allowBots?: boolean;
|
||||
allowBots?: boolean | "mentions";
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
|
||||
@@ -921,6 +921,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
cfg,
|
||||
token: ctx.botToken,
|
||||
accountId: account.accountId,
|
||||
identity: slackIdentity,
|
||||
maxChars: Math.min(ctx.textLimit, SLACK_TEXT_LIMIT),
|
||||
resolveThreadTs: () => {
|
||||
const ts = replyPlan.peekThreadTs();
|
||||
|
||||
@@ -618,6 +618,84 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
expect(members).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drops bot-authored room messages without mention when allowBots is mentions", async () => {
|
||||
const members = vi.fn();
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: { enabled: true },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
appClient: { conversations: { members } } as unknown as App["client"],
|
||||
defaultRequireMention: false,
|
||||
channelsConfig: {
|
||||
C123: { users: ["B0AGV8EQYA3"] },
|
||||
},
|
||||
});
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
createSlackAccount({ allowBots: "mentions" }),
|
||||
createBotRoomMessage({ text: "status failed" }),
|
||||
);
|
||||
|
||||
expect(prepared).toBeNull();
|
||||
expect(members).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows bot-authored room messages with explicit mention when allowBots is mentions", async () => {
|
||||
const members = vi.fn();
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: { enabled: true },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
appClient: { conversations: { members } } as unknown as App["client"],
|
||||
defaultRequireMention: false,
|
||||
channelsConfig: {
|
||||
C123: { users: ["B0AGV8EQYA3"] },
|
||||
},
|
||||
});
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
createSlackAccount({ allowBots: "mentions" }),
|
||||
createBotRoomMessage({ text: "hey <@B1> status failed" }),
|
||||
);
|
||||
|
||||
assertPrepared(prepared);
|
||||
expect(prepared.ctxPayload.RawBody).toContain("status failed");
|
||||
expect(members).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows bot-authored DM messages when allowBots is mentions", async () => {
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: { enabled: true },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
defaultRequireMention: false,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Bot" }) as any;
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
createSlackAccount({ allowBots: "mentions" }),
|
||||
createSlackMessage({
|
||||
channel: "D123",
|
||||
channel_type: "im",
|
||||
text: "bot DM",
|
||||
bot_id: "B0AGV8EQYA3",
|
||||
subtype: "bot_message",
|
||||
}),
|
||||
);
|
||||
|
||||
assertPrepared(prepared);
|
||||
expect(prepared.ctxPayload.RawBody).toContain("bot DM");
|
||||
});
|
||||
|
||||
it("drops bot-authored room messages when owner presence lookup fails (#59284)", async () => {
|
||||
const members = vi.fn().mockRejectedValue(new Error("missing_scope"));
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
|
||||
@@ -101,7 +101,7 @@ type SlackConversationContext = {
|
||||
isRoom: boolean;
|
||||
isRoomish: boolean;
|
||||
channelConfig: ReturnType<typeof resolveSlackChannelConfig> | null;
|
||||
allowBots: boolean;
|
||||
allowBotsMode: "off" | "all" | "mentions";
|
||||
isBotMessage: boolean;
|
||||
};
|
||||
|
||||
@@ -149,11 +149,13 @@ async function resolveSlackConversationContext(params: {
|
||||
allowNameMatching: ctx.allowNameMatching,
|
||||
})
|
||||
: null;
|
||||
const allowBots =
|
||||
const allowBotsSetting =
|
||||
channelConfig?.allowBots ??
|
||||
account.config?.allowBots ??
|
||||
cfg.channels?.slack?.allowBots ??
|
||||
false;
|
||||
const allowBotsMode: "off" | "all" | "mentions" =
|
||||
allowBotsSetting === "mentions" ? "mentions" : allowBotsSetting ? "all" : "off";
|
||||
|
||||
return {
|
||||
channelInfo,
|
||||
@@ -164,7 +166,7 @@ async function resolveSlackConversationContext(params: {
|
||||
isRoom,
|
||||
isRoomish,
|
||||
channelConfig,
|
||||
allowBots,
|
||||
allowBotsMode,
|
||||
isBotMessage: Boolean(message.bot_id),
|
||||
};
|
||||
}
|
||||
@@ -176,14 +178,14 @@ async function authorizeSlackInboundMessage(params: {
|
||||
conversation: SlackConversationContext;
|
||||
}): Promise<SlackAuthorizationContext | null> {
|
||||
const { ctx, account, message, conversation } = params;
|
||||
const { isDirectMessage, channelName, resolvedChannelType, isBotMessage, allowBots } =
|
||||
const { isDirectMessage, channelName, resolvedChannelType, isBotMessage, allowBotsMode } =
|
||||
conversation;
|
||||
|
||||
if (isBotMessage) {
|
||||
if (message.user && ctx.botUserId && message.user === ctx.botUserId) {
|
||||
return null;
|
||||
}
|
||||
if (!allowBots) {
|
||||
if (allowBotsMode === "off") {
|
||||
logVerbose(`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`);
|
||||
return null;
|
||||
}
|
||||
@@ -273,7 +275,7 @@ export async function prepareSlackMessage(params: {
|
||||
isRoom,
|
||||
isRoomish,
|
||||
channelConfig,
|
||||
allowBots,
|
||||
allowBotsMode,
|
||||
isBotMessage,
|
||||
} = conversation;
|
||||
const authorization = await authorizeSlackInboundMessage({
|
||||
@@ -463,6 +465,8 @@ export async function prepareSlackMessage(params: {
|
||||
...(ctx.threadRequireExplicitMention ? { allowedImplicitMentionKinds: [] } : {}),
|
||||
},
|
||||
});
|
||||
const effectiveWasMentioned = messageIngress.activationAccess.effectiveWasMentioned ?? false;
|
||||
const shouldBypassMention = messageIngress.activationAccess.shouldBypassMention ?? false;
|
||||
const senderGate = messageIngress.senderAccess.gate;
|
||||
if (isRoom && senderGate?.allowed === false) {
|
||||
logVerbose(`Blocked unauthorized slack sender ${senderId} (not in channel users)`);
|
||||
@@ -471,7 +475,7 @@ export async function prepareSlackMessage(params: {
|
||||
if (
|
||||
isRoom &&
|
||||
isBotMessage &&
|
||||
allowBots &&
|
||||
allowBotsMode !== "off" &&
|
||||
!(await authorizeSlackBotRoomMessage({
|
||||
ctx,
|
||||
channelId: message.channel,
|
||||
@@ -484,6 +488,14 @@ export async function prepareSlackMessage(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isBotMessage && allowBotsMode === "mentions") {
|
||||
const botMentioned = isDirectMessage || effectiveWasMentioned || shouldBypassMention;
|
||||
if (!botMentioned) {
|
||||
logVerbose("slack: drop bot message (allowBots=mentions, missing mention)");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const threadContextAllowFromLower = isRoom
|
||||
? channelUsersAllowlistConfigured
|
||||
? normalizeAllowListLower(channelConfig?.users)
|
||||
@@ -508,8 +520,6 @@ export async function prepareSlackMessage(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveWasMentioned = messageIngress.activationAccess.effectiveWasMentioned ?? false;
|
||||
const shouldBypassMention = messageIngress.activationAccess.shouldBypassMention ?? false;
|
||||
if (isRoom && shouldRequireMention && messageIngress.activationAccess.shouldSkip) {
|
||||
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel message");
|
||||
const pendingText = (message.text ?? "").trim();
|
||||
|
||||
@@ -36,8 +36,8 @@ export type SlackChannelConfig = {
|
||||
/** Optional tool policy overrides for this channel. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). */
|
||||
allowBots?: boolean;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). Set to "mentions" to only allow bot messages that @mention this bot. */
|
||||
allowBots?: boolean | "mentions";
|
||||
/** Allowlist of users that can invoke the bot in this channel. */
|
||||
users?: Array<string | number>;
|
||||
/** Optional skill filter for this channel. */
|
||||
@@ -143,8 +143,8 @@ export type SlackAccountConfig = {
|
||||
userToken?: string;
|
||||
/** If true, restrict user token to read operations only. Default: true. */
|
||||
userTokenReadOnly?: boolean;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). */
|
||||
allowBots?: boolean;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). Set to "mentions" to only allow bot messages that @mention this bot. */
|
||||
allowBots?: boolean | "mentions";
|
||||
/**
|
||||
* Break-glass override: allow mutable identity matching (name/slug) in allowlists.
|
||||
* Default behavior is ID-only matching.
|
||||
|
||||
@@ -952,7 +952,7 @@ export const SlackChannelSchema = z
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
allowBots: z.boolean().optional(),
|
||||
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
@@ -1010,7 +1010,7 @@ export const SlackAccountSchema = z
|
||||
appToken: SecretInputSchema.optional().register(sensitive),
|
||||
userToken: SecretInputSchema.optional().register(sensitive),
|
||||
userTokenReadOnly: z.boolean().optional().default(true),
|
||||
allowBots: z.boolean().optional(),
|
||||
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),
|
||||
dangerouslyAllowNameMatching: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
groupPolicy: GroupPolicySchema.optional(),
|
||||
|
||||
Reference in New Issue
Block a user