mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 15:01:41 +03:00
* fix(cron): pass agent identity through delivery path Cron delivery messages now include agent identity (name, avatar) in outbound messages. Identity fields are passed best-effort for Slack (graceful fallback if chat:write.customize scope is missing). Fixes #16218 * fix: fix Slack cron delivery identity (#16242) (thanks @robbyczgw-cla) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Security/Archive: enforce archive extraction entry/size limits to prevent resource exhaustion from high-expansion ZIP/TAR archives. Thanks @vincentkoc.
|
- Security/Archive: enforce archive extraction entry/size limits to prevent resource exhaustion from high-expansion ZIP/TAR archives. Thanks @vincentkoc.
|
||||||
- Security/Media: reject oversized base64-backed input media before decoding to avoid large allocations. Thanks @vincentkoc.
|
- Security/Media: reject oversized base64-backed input media before decoding to avoid large allocations. Thanks @vincentkoc.
|
||||||
- Security/Gateway: reject oversized base64 chat attachments before decoding to avoid large allocations. Thanks @vincentkoc.
|
- Security/Gateway: reject oversized base64 chat attachments before decoding to avoid large allocations. Thanks @vincentkoc.
|
||||||
|
- Cron/Slack: preserve agent identity (name and icon) when cron jobs deliver outbound messages. (#16242) Thanks @robbyczgw-cla.
|
||||||
|
|
||||||
## 2026.2.14
|
## 2026.2.14
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ openclaw gateway
|
|||||||
- Config tokens override env fallback.
|
- Config tokens override env fallback.
|
||||||
- `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` env fallback applies only to the default account.
|
- `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` env fallback applies only to the default account.
|
||||||
- `userToken` (`xoxp-...`) is config-only (no env fallback) and defaults to read-only behavior (`userTokenReadOnly: true`).
|
- `userToken` (`xoxp-...`) is config-only (no env fallback) and defaults to read-only behavior (`userTokenReadOnly: true`).
|
||||||
|
- Optional: add `chat:write.customize` if you want outgoing messages to use the active agent identity (custom `username` and icon). `icon_emoji` uses `:emoji_name:` syntax.
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
For actions/directory reads, user token can be preferred when configured. For writes, bot token remains preferred; user-token writes are only allowed when `userTokenReadOnly: false` and bot token is unavailable.
|
For actions/directory reads, user token can be preferred when configured. For writes, bot token remains preferred; user-token writes are only allowed when `userTokenReadOnly: false` and bot token is unavailable.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("../../../slack/send.js", () => ({
|
vi.mock("../../../slack/send.js", () => ({
|
||||||
sendMessageSlack: vi.fn().mockResolvedValue({ ts: "1234.5678", channel: "C123" }),
|
sendMessageSlack: vi.fn().mockResolvedValue({ messageId: "1234.5678", channelId: "C123" }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../../plugins/hook-runner-global.js", () => ({
|
vi.mock("../../../plugins/hook-runner-global.js", () => ({
|
||||||
@@ -37,6 +37,45 @@ describe("slack outbound hook wiring", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards identity opts when present", async () => {
|
||||||
|
vi.mocked(getGlobalHookRunner).mockReturnValue(null);
|
||||||
|
|
||||||
|
await slackOutbound.sendText({
|
||||||
|
to: "C123",
|
||||||
|
text: "hello",
|
||||||
|
accountId: "default",
|
||||||
|
replyToId: "1111.2222",
|
||||||
|
username: "My Agent",
|
||||||
|
icon_url: "https://example.com/avatar.png",
|
||||||
|
icon_emoji: ":should_not_send:",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessageSlack).toHaveBeenCalledWith("C123", "hello", {
|
||||||
|
threadTs: "1111.2222",
|
||||||
|
accountId: "default",
|
||||||
|
username: "My Agent",
|
||||||
|
icon_url: "https://example.com/avatar.png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards icon_emoji only when icon_url is absent", async () => {
|
||||||
|
vi.mocked(getGlobalHookRunner).mockReturnValue(null);
|
||||||
|
|
||||||
|
await slackOutbound.sendText({
|
||||||
|
to: "C123",
|
||||||
|
text: "hello",
|
||||||
|
accountId: "default",
|
||||||
|
replyToId: "1111.2222",
|
||||||
|
icon_emoji: ":lobster:",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessageSlack).toHaveBeenCalledWith("C123", "hello", {
|
||||||
|
threadTs: "1111.2222",
|
||||||
|
accountId: "default",
|
||||||
|
icon_emoji: ":lobster:",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("calls message_sending hook before sending", async () => {
|
it("calls message_sending hook before sending", async () => {
|
||||||
const mockRunner = {
|
const mockRunner = {
|
||||||
hasHooks: vi.fn().mockReturnValue(true),
|
hasHooks: vi.fn().mockReturnValue(true),
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
|||||||
deliveryMode: "direct",
|
deliveryMode: "direct",
|
||||||
chunker: null,
|
chunker: null,
|
||||||
textChunkLimit: 4000,
|
textChunkLimit: 4000,
|
||||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
|
sendText: async ({
|
||||||
|
to,
|
||||||
|
text,
|
||||||
|
accountId,
|
||||||
|
deps,
|
||||||
|
replyToId,
|
||||||
|
threadId,
|
||||||
|
username,
|
||||||
|
icon_url,
|
||||||
|
icon_emoji,
|
||||||
|
}) => {
|
||||||
const send = deps?.sendSlack ?? sendMessageSlack;
|
const send = deps?.sendSlack ?? sendMessageSlack;
|
||||||
// Use threadId fallback so routed tool notifications stay in the Slack thread.
|
// Use threadId fallback so routed tool notifications stay in the Slack thread.
|
||||||
const threadTs = replyToId ?? (threadId != null ? String(threadId) : undefined);
|
const threadTs = replyToId ?? (threadId != null ? String(threadId) : undefined);
|
||||||
@@ -35,10 +45,24 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
|||||||
const result = await send(to, finalText, {
|
const result = await send(to, finalText, {
|
||||||
threadTs,
|
threadTs,
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
|
...(username ? { username } : {}),
|
||||||
|
...(icon_url ? { icon_url } : {}),
|
||||||
|
...(icon_emoji && !icon_url ? { icon_emoji } : {}),
|
||||||
});
|
});
|
||||||
return { channel: "slack", ...result };
|
return { channel: "slack", ...result };
|
||||||
},
|
},
|
||||||
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, threadId }) => {
|
sendMedia: async ({
|
||||||
|
to,
|
||||||
|
text,
|
||||||
|
mediaUrl,
|
||||||
|
accountId,
|
||||||
|
deps,
|
||||||
|
replyToId,
|
||||||
|
threadId,
|
||||||
|
username,
|
||||||
|
icon_url,
|
||||||
|
icon_emoji,
|
||||||
|
}) => {
|
||||||
const send = deps?.sendSlack ?? sendMessageSlack;
|
const send = deps?.sendSlack ?? sendMessageSlack;
|
||||||
// Use threadId fallback so routed tool notifications stay in the Slack thread.
|
// Use threadId fallback so routed tool notifications stay in the Slack thread.
|
||||||
const threadTs = replyToId ?? (threadId != null ? String(threadId) : undefined);
|
const threadTs = replyToId ?? (threadId != null ? String(threadId) : undefined);
|
||||||
@@ -68,6 +92,9 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
|||||||
mediaUrl,
|
mediaUrl,
|
||||||
threadTs,
|
threadTs,
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
|
...(username ? { username } : {}),
|
||||||
|
...(icon_url ? { icon_url } : {}),
|
||||||
|
...(icon_emoji && !icon_url ? { icon_emoji } : {}),
|
||||||
});
|
});
|
||||||
return { channel: "slack", ...result };
|
return { channel: "slack", ...result };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export type ChannelOutboundContext = {
|
|||||||
replyToId?: string | null;
|
replyToId?: string | null;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
|
username?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
icon_emoji?: string;
|
||||||
deps?: OutboundSendDeps;
|
deps?: OutboundSendDeps;
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { getCliSessionId, setCliSessionId } from "../../agents/cli-session.js";
|
|||||||
import { lookupContextTokens } from "../../agents/context.js";
|
import { lookupContextTokens } from "../../agents/context.js";
|
||||||
import { resolveCronStyleNow } from "../../agents/current-time.js";
|
import { resolveCronStyleNow } from "../../agents/current-time.js";
|
||||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
|
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
|
||||||
|
import { resolveAgentAvatar } from "../../agents/identity-avatar.js";
|
||||||
|
import { resolveAgentIdentity } from "../../agents/identity.js";
|
||||||
import { loadModelCatalog } from "../../agents/model-catalog.js";
|
import { loadModelCatalog } from "../../agents/model-catalog.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
import {
|
import {
|
||||||
@@ -555,21 +557,41 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
logWarn(`[cron:${params.job.id}] ${message}`);
|
logWarn(`[cron:${params.job.id}] ${message}`);
|
||||||
return withRunSession({ status: "ok", summary, outputText });
|
return withRunSession({ status: "ok", summary, outputText });
|
||||||
}
|
}
|
||||||
// Shared subagent announce flow is text-based; keep direct outbound delivery
|
const agentIdentity = resolveAgentIdentity(cfgWithAgentDefaults, agentId);
|
||||||
// for media/channel payloads so structured content is preserved.
|
const avatar = resolveAgentAvatar(cfgWithAgentDefaults, agentId);
|
||||||
if (deliveryPayloadHasStructuredContent) {
|
const icon_url = avatar.kind === "remote" ? avatar.url : undefined;
|
||||||
|
const username = agentIdentity?.name?.trim() || undefined;
|
||||||
|
const rawEmoji = agentIdentity?.emoji?.trim();
|
||||||
|
// Slack `icon_emoji` requires :emoji_name: (not a Unicode emoji).
|
||||||
|
const icon_emoji =
|
||||||
|
!icon_url && rawEmoji && /^:[^:\\s]+:$/.test(rawEmoji) ? rawEmoji : undefined;
|
||||||
|
|
||||||
|
// Shared subagent announce flow is text-based. When we have an explicit sender
|
||||||
|
// identity to preserve, prefer direct outbound delivery even for plain-text payloads.
|
||||||
|
if (deliveryPayloadHasStructuredContent || username || icon_url || icon_emoji) {
|
||||||
try {
|
try {
|
||||||
const deliveryResults = await deliverOutboundPayloads({
|
const payloadsForDelivery =
|
||||||
cfg: cfgWithAgentDefaults,
|
deliveryPayloadHasStructuredContent && deliveryPayloads.length > 0
|
||||||
channel: resolvedDelivery.channel,
|
? deliveryPayloads
|
||||||
to: resolvedDelivery.to,
|
: synthesizedText
|
||||||
accountId: resolvedDelivery.accountId,
|
? [{ text: synthesizedText }]
|
||||||
threadId: resolvedDelivery.threadId,
|
: [];
|
||||||
payloads: deliveryPayloads,
|
if (payloadsForDelivery.length > 0) {
|
||||||
bestEffort: deliveryBestEffort,
|
const deliveryResults = await deliverOutboundPayloads({
|
||||||
deps: createOutboundSendDeps(params.deps),
|
cfg: cfgWithAgentDefaults,
|
||||||
});
|
channel: resolvedDelivery.channel,
|
||||||
delivered = deliveryResults.length > 0;
|
to: resolvedDelivery.to,
|
||||||
|
accountId: resolvedDelivery.accountId,
|
||||||
|
threadId: resolvedDelivery.threadId,
|
||||||
|
payloads: payloadsForDelivery,
|
||||||
|
username,
|
||||||
|
icon_url,
|
||||||
|
icon_emoji,
|
||||||
|
bestEffort: deliveryBestEffort,
|
||||||
|
deps: createOutboundSendDeps(params.deps),
|
||||||
|
});
|
||||||
|
delivered = deliveryResults.length > 0;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!deliveryBestEffort) {
|
if (!deliveryBestEffort) {
|
||||||
return withRunSession({ status: "error", summary, outputText, error: String(err) });
|
return withRunSession({ status: "error", summary, outputText, error: String(err) });
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ async function createChannelHandler(params: {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
replyToId?: string | null;
|
replyToId?: string | null;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
|
username?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
icon_emoji?: string;
|
||||||
deps?: OutboundSendDeps;
|
deps?: OutboundSendDeps;
|
||||||
gifPlayback?: boolean;
|
gifPlayback?: boolean;
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
@@ -101,6 +104,9 @@ async function createChannelHandler(params: {
|
|||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
replyToId: params.replyToId,
|
replyToId: params.replyToId,
|
||||||
threadId: params.threadId,
|
threadId: params.threadId,
|
||||||
|
username: params.username,
|
||||||
|
icon_url: params.icon_url,
|
||||||
|
icon_emoji: params.icon_emoji,
|
||||||
deps: params.deps,
|
deps: params.deps,
|
||||||
gifPlayback: params.gifPlayback,
|
gifPlayback: params.gifPlayback,
|
||||||
silent: params.silent,
|
silent: params.silent,
|
||||||
@@ -119,6 +125,9 @@ function createPluginHandler(params: {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
replyToId?: string | null;
|
replyToId?: string | null;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
|
username?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
icon_emoji?: string;
|
||||||
deps?: OutboundSendDeps;
|
deps?: OutboundSendDeps;
|
||||||
gifPlayback?: boolean;
|
gifPlayback?: boolean;
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
@@ -145,6 +154,9 @@ function createPluginHandler(params: {
|
|||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
replyToId: params.replyToId,
|
replyToId: params.replyToId,
|
||||||
threadId: params.threadId,
|
threadId: params.threadId,
|
||||||
|
username: params.username,
|
||||||
|
icon_url: params.icon_url,
|
||||||
|
icon_emoji: params.icon_emoji,
|
||||||
gifPlayback: params.gifPlayback,
|
gifPlayback: params.gifPlayback,
|
||||||
deps: params.deps,
|
deps: params.deps,
|
||||||
silent: params.silent,
|
silent: params.silent,
|
||||||
@@ -159,6 +171,9 @@ function createPluginHandler(params: {
|
|||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
replyToId: params.replyToId,
|
replyToId: params.replyToId,
|
||||||
threadId: params.threadId,
|
threadId: params.threadId,
|
||||||
|
username: params.username,
|
||||||
|
icon_url: params.icon_url,
|
||||||
|
icon_emoji: params.icon_emoji,
|
||||||
gifPlayback: params.gifPlayback,
|
gifPlayback: params.gifPlayback,
|
||||||
deps: params.deps,
|
deps: params.deps,
|
||||||
silent: params.silent,
|
silent: params.silent,
|
||||||
@@ -172,6 +187,9 @@ function createPluginHandler(params: {
|
|||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
replyToId: params.replyToId,
|
replyToId: params.replyToId,
|
||||||
threadId: params.threadId,
|
threadId: params.threadId,
|
||||||
|
username: params.username,
|
||||||
|
icon_url: params.icon_url,
|
||||||
|
icon_emoji: params.icon_emoji,
|
||||||
gifPlayback: params.gifPlayback,
|
gifPlayback: params.gifPlayback,
|
||||||
deps: params.deps,
|
deps: params.deps,
|
||||||
silent: params.silent,
|
silent: params.silent,
|
||||||
@@ -189,6 +207,9 @@ export async function deliverOutboundPayloads(params: {
|
|||||||
payloads: ReplyPayload[];
|
payloads: ReplyPayload[];
|
||||||
replyToId?: string | null;
|
replyToId?: string | null;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
|
username?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
icon_emoji?: string;
|
||||||
deps?: OutboundSendDeps;
|
deps?: OutboundSendDeps;
|
||||||
gifPlayback?: boolean;
|
gifPlayback?: boolean;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
@@ -271,6 +292,9 @@ async function deliverOutboundPayloadsCore(params: {
|
|||||||
payloads: ReplyPayload[];
|
payloads: ReplyPayload[];
|
||||||
replyToId?: string | null;
|
replyToId?: string | null;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
|
username?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
icon_emoji?: string;
|
||||||
deps?: OutboundSendDeps;
|
deps?: OutboundSendDeps;
|
||||||
gifPlayback?: boolean;
|
gifPlayback?: boolean;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
@@ -299,6 +323,9 @@ async function deliverOutboundPayloadsCore(params: {
|
|||||||
accountId,
|
accountId,
|
||||||
replyToId: params.replyToId,
|
replyToId: params.replyToId,
|
||||||
threadId: params.threadId,
|
threadId: params.threadId,
|
||||||
|
username: params.username,
|
||||||
|
icon_url: params.icon_url,
|
||||||
|
icon_emoji: params.icon_emoji,
|
||||||
gifPlayback: params.gifPlayback,
|
gifPlayback: params.gifPlayback,
|
||||||
silent: params.silent,
|
silent: params.silent,
|
||||||
});
|
});
|
||||||
|
|||||||
+85
-6
@@ -33,8 +33,83 @@ type SlackSendOpts = {
|
|||||||
mediaUrl?: string;
|
mediaUrl?: string;
|
||||||
client?: WebClient;
|
client?: WebClient;
|
||||||
threadTs?: string;
|
threadTs?: string;
|
||||||
|
username?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
icon_emoji?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function hasCustomIdentity(opts: SlackSendOpts): boolean {
|
||||||
|
return Boolean(opts.username || opts.icon_url || opts.icon_emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSlackCustomizeScopeError(err: unknown): boolean {
|
||||||
|
if (!(err instanceof Error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const maybeData = err as Error & {
|
||||||
|
data?: {
|
||||||
|
error?: string;
|
||||||
|
needed?: string;
|
||||||
|
response_metadata?: { scopes?: string[]; acceptedScopes?: string[] };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const code = maybeData.data?.error?.toLowerCase();
|
||||||
|
if (code !== "missing_scope") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const needed = maybeData.data?.needed?.toLowerCase();
|
||||||
|
if (needed?.includes("chat:write.customize")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const scopes = [
|
||||||
|
...(maybeData.data?.response_metadata?.scopes ?? []),
|
||||||
|
...(maybeData.data?.response_metadata?.acceptedScopes ?? []),
|
||||||
|
].map((scope) => scope.toLowerCase());
|
||||||
|
return scopes.includes("chat:write.customize");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postSlackMessageBestEffort(params: {
|
||||||
|
client: WebClient;
|
||||||
|
channelId: string;
|
||||||
|
text: string;
|
||||||
|
threadTs?: string;
|
||||||
|
opts: SlackSendOpts;
|
||||||
|
}) {
|
||||||
|
const basePayload = {
|
||||||
|
channel: params.channelId,
|
||||||
|
text: params.text,
|
||||||
|
thread_ts: params.threadTs,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
// Slack Web API types model icon_url and icon_emoji as mutually exclusive.
|
||||||
|
// Build payloads in explicit branches so TS and runtime stay aligned.
|
||||||
|
if (params.opts.icon_url) {
|
||||||
|
return await params.client.chat.postMessage({
|
||||||
|
...basePayload,
|
||||||
|
...(params.opts.username ? { username: params.opts.username } : {}),
|
||||||
|
icon_url: params.opts.icon_url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (params.opts.icon_emoji) {
|
||||||
|
return await params.client.chat.postMessage({
|
||||||
|
...basePayload,
|
||||||
|
...(params.opts.username ? { username: params.opts.username } : {}),
|
||||||
|
icon_emoji: params.opts.icon_emoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await params.client.chat.postMessage({
|
||||||
|
...basePayload,
|
||||||
|
...(params.opts.username ? { username: params.opts.username } : {}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!hasCustomIdentity(params.opts) || !isSlackCustomizeScopeError(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
logVerbose("slack send: missing chat:write.customize, retrying without custom identity");
|
||||||
|
return params.client.chat.postMessage(basePayload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type SlackSendResult = {
|
export type SlackSendResult = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
@@ -182,19 +257,23 @@ export async function sendMessageSlack(
|
|||||||
maxBytes: mediaMaxBytes,
|
maxBytes: mediaMaxBytes,
|
||||||
});
|
});
|
||||||
for (const chunk of rest) {
|
for (const chunk of rest) {
|
||||||
const response = await client.chat.postMessage({
|
const response = await postSlackMessageBestEffort({
|
||||||
channel: channelId,
|
client,
|
||||||
|
channelId,
|
||||||
text: chunk,
|
text: chunk,
|
||||||
thread_ts: opts.threadTs,
|
threadTs: opts.threadTs,
|
||||||
|
opts,
|
||||||
});
|
});
|
||||||
lastMessageId = response.ts ?? lastMessageId;
|
lastMessageId = response.ts ?? lastMessageId;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const chunk of chunks.length ? chunks : [""]) {
|
for (const chunk of chunks.length ? chunks : [""]) {
|
||||||
const response = await client.chat.postMessage({
|
const response = await postSlackMessageBestEffort({
|
||||||
channel: channelId,
|
client,
|
||||||
|
channelId,
|
||||||
text: chunk,
|
text: chunk,
|
||||||
thread_ts: opts.threadTs,
|
threadTs: opts.threadTs,
|
||||||
|
opts,
|
||||||
});
|
});
|
||||||
lastMessageId = response.ts ?? lastMessageId;
|
lastMessageId = response.ts ?? lastMessageId;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user