mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
fix(slack): send proactive dm text directly
This commit is contained in:
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Slack/DMs: honor `dmHistoryLimit` for fresh 1:1 Slack DM sessions by backfilling recent conversation history before the current reply. Fixes #64427. Thanks @brantley-creator.
|
||||
- Slack/DMs: keep top-level direct messages on the stable DM session even when `replyToMode` targets Slack thread replies, preserving context across DM turns. Fixes #58832. Thanks @daye-jjeong.
|
||||
- Slack/delivery: preserve Slack Web API missing-scope details in outbound delivery errors, so queued retry state identifies the OAuth scope to add. Fixes #62391. Thanks @alexey-pelykh.
|
||||
- Slack/DMs: send text/block-only proactive DMs directly with `chat.postMessage(channel=<user id>)` while keeping conversation resolution for uploads and threaded sends. Fixes #62042. Thanks @MarkMolina.
|
||||
- Slack/mentions: resolve `<!subteam^...>` user-group mentions through Slack `usergroups.users.list` and treat them as explicit mentions only when the bot user is a member, so mention-gated agent channels wake for real user-group mentions without config-only allowlists. Fixes #73827. Thanks @CG-Intelligence-Agent-Jack.
|
||||
- Slack/message tool: let `read` fetch an exact Slack message timestamp, including a specific thread reply when paired with `threadId`, instead of returning only the parent thread or recent channel history. Fixes #53943. Thanks @zomars.
|
||||
- Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97.
|
||||
|
||||
@@ -714,7 +714,7 @@ Notes:
|
||||
- `user:<id>` for DMs
|
||||
- `channel:<id>` for channels
|
||||
|
||||
Slack DMs are opened via Slack conversation APIs when sending to user targets.
|
||||
Text/block-only Slack DMs can post directly to user IDs; file uploads and threaded sends open the DM via Slack conversation APIs first because those paths require a concrete conversation ID.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -115,6 +115,27 @@ describe("sendMessageSlack blocks", () => {
|
||||
expect(result).toEqual({ messageId: "171234.567", channelId: "C123" });
|
||||
});
|
||||
|
||||
it("posts user-target block messages directly without conversations.open", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
client.conversations.open.mockRejectedValueOnce(new Error("missing_scope"));
|
||||
|
||||
const result = await sendMessageSlack("user:U123", "", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [{ type: "divider" }],
|
||||
});
|
||||
|
||||
expect(client.conversations.open).not.toHaveBeenCalled();
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "U123",
|
||||
text: "Shared a Block Kit message",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ messageId: "171234.567", channelId: "U123" });
|
||||
});
|
||||
|
||||
it("derives fallback text from image blocks", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
await sendMessageSlack("channel:C123", "", {
|
||||
|
||||
@@ -300,6 +300,21 @@ function setSlackDmChannelCache(key: string, channelId: string): void {
|
||||
slackDmChannelCache.set(key, channelId);
|
||||
}
|
||||
|
||||
function isSlackUserRecipient(recipient: SlackRecipient): boolean {
|
||||
return recipient.kind === "user" || /^U[A-Z0-9]+$/i.test(recipient.id);
|
||||
}
|
||||
|
||||
function resolveDirectUserPostChannelId(params: {
|
||||
recipient: SlackRecipient;
|
||||
hasMedia: boolean;
|
||||
threadTs?: string;
|
||||
}): string | undefined {
|
||||
if (!isSlackUserRecipient(params.recipient) || params.hasMedia || params.threadTs) {
|
||||
return undefined;
|
||||
}
|
||||
return params.recipient.id;
|
||||
}
|
||||
|
||||
async function resolveChannelId(
|
||||
client: WebClient,
|
||||
recipient: SlackRecipient,
|
||||
@@ -309,10 +324,9 @@ async function resolveChannelId(
|
||||
// target string had no explicit prefix (parseSlackTarget defaults bare IDs
|
||||
// to "channel"). chat.postMessage tolerates user IDs directly, but
|
||||
// files.uploadV2 → completeUploadExternal validates channel_id against
|
||||
// ^[CGDZ][A-Z0-9]{8,}$ and rejects U-prefixed IDs. Always resolve user
|
||||
// IDs via conversations.open to obtain the DM channel ID.
|
||||
const isUserId = recipient.kind === "user" || /^U[A-Z0-9]+$/i.test(recipient.id);
|
||||
if (!isUserId) {
|
||||
// ^[CGDZ][A-Z0-9]{8,}$ and rejects U-prefixed IDs. Resolve user IDs via
|
||||
// conversations.open only for paths that require the concrete DM channel ID.
|
||||
if (!isSlackUserRecipient(recipient)) {
|
||||
return { channelId: recipient.id };
|
||||
}
|
||||
const cacheKey = createSlackDmCacheKey({
|
||||
@@ -484,10 +498,17 @@ async function sendMessageSlackQueuedInner(params: {
|
||||
}): Promise<SlackSendResult> {
|
||||
const { opts, cfg, account, token, recipient, blocks, trimmedMessage } = params;
|
||||
const client = opts.client ?? getSlackWriteClient(token);
|
||||
const { channelId } = await resolveChannelId(client, recipient, {
|
||||
accountId: account.accountId,
|
||||
token,
|
||||
const directUserPostChannelId = resolveDirectUserPostChannelId({
|
||||
recipient,
|
||||
hasMedia: Boolean(opts.mediaUrl),
|
||||
...(opts.threadTs ? { threadTs: opts.threadTs } : {}),
|
||||
});
|
||||
const { channelId } = directUserPostChannelId
|
||||
? { channelId: directUserPostChannelId }
|
||||
: await resolveChannelId(client, recipient, {
|
||||
accountId: account.accountId,
|
||||
token,
|
||||
});
|
||||
if (blocks) {
|
||||
if (opts.mediaUrl) {
|
||||
throw new Error("Slack send does not support blocks with mediaUrl");
|
||||
|
||||
@@ -145,8 +145,9 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("caches DM channel resolution per account", async () => {
|
||||
it("posts text-only user-target DMs directly without conversations.open", async () => {
|
||||
const client = createUploadTestClient();
|
||||
client.conversations.open.mockRejectedValueOnce(new Error("missing_scope"));
|
||||
|
||||
await sendMessageSlack("user:UABC123", "first", {
|
||||
token: "xoxb-test",
|
||||
@@ -159,12 +160,12 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
client,
|
||||
});
|
||||
|
||||
expect(client.conversations.open).toHaveBeenCalledTimes(1);
|
||||
expect(client.conversations.open).not.toHaveBeenCalled();
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
|
||||
expect(client.chat.postMessage).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
channel: "D99RESOLVED",
|
||||
channel: "UABC123",
|
||||
text: "second",
|
||||
}),
|
||||
);
|
||||
@@ -215,11 +216,13 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
token: "xoxb-test-a",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/first.png",
|
||||
});
|
||||
await sendMessageSlack("user:UABC123", "second", {
|
||||
token: "xoxb-test-b",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/second.png",
|
||||
});
|
||||
|
||||
expect(client.conversations.open).toHaveBeenCalledTimes(2);
|
||||
|
||||
Reference in New Issue
Block a user