@openclaw/slack 2026.7.2-beta.2 → 2026.7.2-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-configured-DzC407uv.js → account-configured-BwfXBo_F.js} +1 -1
- package/dist/{account-inspect-Di7ditqZ.js → account-inspect-Bvbtu3iH.js} +9 -2
- package/dist/account-inspect-api.js +1 -1
- package/dist/{accounts-BOJJiHSr.js → accounts-DQEbYKh9.js} +9 -0
- package/dist/accounts.runtime-BIKmJdtJ.js +2 -0
- package/dist/{action-runtime-Cee2ih0s.js → action-runtime-DqkRwO3Z.js} +10 -16
- package/dist/action-runtime.runtime-DEWOcFM4.js +2 -0
- package/dist/{actions-BZiuSTIq.js → actions-CF0tUY5q.js} +4 -4
- package/dist/{actions.runtime-DoD0-gxw.js → actions.runtime-hCbx4RXg.js} +1 -1
- package/dist/api.js +13 -14
- package/dist/{approval-handler.runtime-CUPtIM4k.js → approval-handler.runtime-Em8I1-8-.js} +2 -2
- package/dist/{channel-DaIiGUOp.js → channel-4-TRi1D6.js} +72 -40
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel-type-Cfm6_6n1.js → channel-type-Cu1yjNg2.js} +3 -3
- package/dist/{channel.setup-C-mLz50O.js → channel.setup-CgkADtZg.js} +6 -4
- package/dist/{config-schema-xPSnK2VK.js → config-schema-XjJGfwQC.js} +6 -5
- package/dist/{directory-config-DRkFujwG.js → directory-config-Bv0lcUlH.js} +1 -1
- package/dist/directory-contract-api.js +1 -1
- package/dist/{directory-live-D_yZhjgp.js → directory-live-CzUIB7th.js} +2 -2
- package/dist/{doctor-contract-DddfkMlF.js → doctor-contract-DKpe5VBr.js} +80 -12
- package/dist/doctor-contract-api.js +1 -1
- package/dist/{group-policy-85UWiriS.js → group-policy-Bw-65m3V.js} +67 -121
- package/dist/{interactive-replies-BilWUdcI.js → interactive-replies-Dm_t2R9S.js} +1 -1
- package/dist/interactive-replies-api.js +1 -1
- package/dist/{message-tool-api-CA9jZrCY.js → message-tool-api-C2pcf5du.js} +3 -3
- package/dist/message-tool-api.js +1 -1
- package/dist/{monitor-CalGciTs.js → monitor-CNFeF30h.js} +3 -3
- package/dist/{outbound-adapter-BiLKcnyH.js → outbound-adapter-CVi2uok0.js} +49 -5
- package/dist/{pipeline.runtime-DofAvTtb.js → pipeline.runtime-CTvokbty.js} +391 -279
- package/dist/{policy-BVs4LiSa.js → policy-b_dOPDCq.js} +1 -1
- package/dist/probe-Borhn9PQ.js +340 -0
- package/dist/{provider-CmeoUiIM.js → provider-B-HVL5ZE.js} +573 -306
- package/dist/{relay-source-DMmgtvg4.js → relay-source-Ea_P9VcO.js} +6 -11
- package/dist/{replies-CJJCp6qz.js → replies-B4bOdrYb.js} +3 -3
- package/dist/{resolve-channels-Rfts6POK.js → resolve-channels-KTc568SU.js} +1 -1
- package/dist/{resolve-users-y6wAxR8E.js → resolve-users-DeGJf1yp.js} +1 -1
- package/dist/{runtime-api-BGDyOt1D.js → runtime-api-BFfNpzDe.js} +1 -1
- package/dist/runtime-api.js +11 -11
- package/dist/{scopes-CuxNQZ6v.js → scopes-lx08L56K.js} +1 -2
- package/dist/{secret-contract-CMohXFWS.js → secret-contract-DxmvbxBF.js} +1 -0
- package/dist/secret-contract-api.js +1 -1
- package/dist/{send-DYp8ca-l.js → send-KuTDky7i.js} +28 -11
- package/dist/send.runtime-CXyxws1D.js +2 -0
- package/dist/{setup-core-CkwVLzFO.js → setup-core-CeiAVwQ3.js} +218 -53
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{setup-surface-oSd3NX9J.js → setup-surface-DLA0vHXp.js} +23 -13
- package/dist/{shared-D9yz5Otg.js → shared-C4yVB0Q-.js} +55 -9
- package/dist/{slash-dispatch.runtime-CSja9VuD.js → slash-dispatch.runtime-BT-mMm-f.js} +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +66 -10
- package/package.json +4 -4
- package/dist/accounts.runtime-KvftRRek.js +0 -2
- package/dist/action-runtime.runtime-DZfd7IFr.js +0 -2
- package/dist/client-WIPAS3Yr.js +0 -160
- package/dist/probe-CN0ssfLN.js +0 -48
- package/dist/send.runtime-CJLxAt26.js +0 -2
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { n as registerSlackHttpHandler, r as normalizeSlackWebhookPath } from "./registry-bbjH7IHX.js";
|
|
2
|
-
import { a as resolveSlackAccount, d as formatSlackBotTokenIdentityWarning, f as resolveSlackAppToken, i as resolveDefaultSlackAccountId, o as resolveSlackAccountAllowFrom, p as resolveSlackBotToken, s as resolveSlackAccountDmPolicy } from "./accounts-
|
|
3
|
-
import {
|
|
4
|
-
import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-
|
|
5
|
-
import {
|
|
6
|
-
import { n as getSlackRuntime
|
|
7
|
-
import { i as setSlackDefaultSendIdentity } from "./send-
|
|
8
|
-
import { t as resolveSlackChannelAllowlist } from "./resolve-channels-
|
|
9
|
-
import { t as resolveSlackUserAllowlist } from "./resolve-users-
|
|
10
|
-
import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-
|
|
2
|
+
import { a as resolveSlackAccount, d as formatSlackBotTokenIdentityWarning, f as resolveSlackAppToken, i as resolveDefaultSlackAccountId, o as resolveSlackAccountAllowFrom, p as resolveSlackBotToken, s as resolveSlackAccountDmPolicy } from "./accounts-DQEbYKh9.js";
|
|
3
|
+
import { F as SLACK_TEXT_LIMIT, G as isSlackQuestionActionId, K as decodeSlackQuestionAction, U as isSlackApprovalActionId, W as isSlackCallbackActionId, X as decodeSlackApprovalAction, Y as escapeSlackMrkdwn, a as allowListMatches, c as normalizeSlackAllowOwnerEntry, ft as isSlackApprovalAuthorizedSender, l as normalizeSlackSlug, mt as isSlackExecApprovalAuthorizedSender, nt as isSlackAnyNativeApprovalClientEnabled, o as normalizeAllowList, q as resolveSlackQuestionAction, s as normalizeAllowListLower, u as resolveSlackAllowListMatch, z as truncateSlackText } from "./group-policy-Bw-65m3V.js";
|
|
4
|
+
import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-Dm_t2R9S.js";
|
|
5
|
+
import { m as resolveSlackWebClientOptions, o as createSlackStartupAuthClient, r as formatSlackError, u as getSlackListenerUploadCompletionClient } from "./probe-Borhn9PQ.js";
|
|
6
|
+
import { n as getSlackRuntime } from "./runtime-JSVZSWAj.js";
|
|
7
|
+
import { i as setSlackDefaultSendIdentity } from "./send-KuTDky7i.js";
|
|
8
|
+
import { t as resolveSlackChannelAllowlist } from "./resolve-channels-KTc568SU.js";
|
|
9
|
+
import { t as resolveSlackUserAllowlist } from "./resolve-users-DeGJf1yp.js";
|
|
10
|
+
import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-b_dOPDCq.js";
|
|
11
11
|
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
12
12
|
import { asOptionalRecord, isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, normalizeStringEntriesLower, normalizeUniqueTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
13
|
import { normalizeAccountId, normalizeMainKey, resolveAgentRoute, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
|
14
|
-
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
|
|
14
|
+
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressDrain, createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
|
|
15
15
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
16
16
|
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
17
17
|
import { computeBackoff, createNonExitingRuntime, createSubsystemLogger, danger, getChildLogger, logVerbose, shouldLogVerbose, sleepWithAbort, warn } from "openclaw/plugin-sdk/runtime-env";
|
|
@@ -20,12 +20,12 @@ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plu
|
|
|
20
20
|
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
21
21
|
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
22
22
|
import { addAllowlistUserEntriesFromConfigEntry, buildAllowlistResolutionSummary, formatAllowlistMatchMeta, mergeAllowlist, patchAllowlistUsersInConfigEntries, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
|
|
23
|
-
import { collectErrorGraphCandidates, formatErrorMessage, isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
|
|
23
|
+
import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
|
|
24
24
|
import { asDateTimestampMs, parseFiniteNumber, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
25
25
|
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
|
|
26
26
|
import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
|
|
27
|
-
import { createDedupeCache
|
|
28
|
-
import { buildPluginBindingResolvedText, parsePluginBindingApprovalCustomId,
|
|
27
|
+
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
|
28
|
+
import { buildPluginBindingResolvedText, parsePluginBindingApprovalCustomId, resolveConversationLabel as resolveConversationLabel$1, resolvePluginConversationBindingApproval, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
|
29
29
|
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "openclaw/plugin-sdk/native-command-config-runtime";
|
|
30
30
|
import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
|
|
31
31
|
import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
|
|
@@ -45,6 +45,7 @@ import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
|
|
|
45
45
|
import { createInteractiveConversationBindingHelpers, dispatchPluginInteractiveHandler } from "openclaw/plugin-sdk/plugin-runtime";
|
|
46
46
|
import { createChannelIngressResolver, defineStableChannelIngressIdentity, readChannelIngressStoreAllowFromForDmPolicy } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
47
47
|
import { createChannelInboundDebouncer, shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
|
|
48
|
+
import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
48
49
|
import { generateSecureToken } from "openclaw/plugin-sdk/secure-random-runtime";
|
|
49
50
|
//#region extensions/slack/src/monitor/commands.ts
|
|
50
51
|
/**
|
|
@@ -150,10 +151,6 @@ function createSlackMonitorContext(params) {
|
|
|
150
151
|
const logger = getChildLogger({ module: "slack-auto-reply" });
|
|
151
152
|
const channelCache = /* @__PURE__ */ new Map();
|
|
152
153
|
const userCache = /* @__PURE__ */ new Map();
|
|
153
|
-
const seenMessages = createDedupeCache({
|
|
154
|
-
ttlMs: 6e4,
|
|
155
|
-
maxSize: 500
|
|
156
|
-
});
|
|
157
154
|
const channelDenialWarnings = createDedupeCache({
|
|
158
155
|
ttlMs: SLACK_CHANNEL_DENIAL_WARNING_TTL_MS,
|
|
159
156
|
maxSize: SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES
|
|
@@ -187,14 +184,6 @@ function createSlackMonitorContext(params) {
|
|
|
187
184
|
const id = normalizeOptionalString(channelId);
|
|
188
185
|
return id ? readLruMapEntry(channelCache, scopedKey(id, eventScope))?.info.type : void 0;
|
|
189
186
|
};
|
|
190
|
-
const markMessageSeen = (channelId, ts, eventScope) => {
|
|
191
|
-
if (!channelId || !ts) return false;
|
|
192
|
-
return seenMessages.check(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
193
|
-
};
|
|
194
|
-
const releaseSeenMessage = (channelId, ts, eventScope) => {
|
|
195
|
-
if (!channelId || !ts) return;
|
|
196
|
-
seenMessages.delete(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
197
|
-
};
|
|
198
187
|
const assistantContextKey = (channelId, threadTs, eventScope) => scopedKey(`${channelId}:${threadTs}`, eventScope);
|
|
199
188
|
const cleanupAssistantThreadContexts = () => {
|
|
200
189
|
const now = Date.now();
|
|
@@ -472,7 +461,6 @@ function createSlackMonitorContext(params) {
|
|
|
472
461
|
replyToMode: params.replyToMode,
|
|
473
462
|
threadHistoryScope: params.threadHistoryScope,
|
|
474
463
|
threadInheritParent: params.threadInheritParent,
|
|
475
|
-
threadRequireExplicitMention: params.threadRequireExplicitMention,
|
|
476
464
|
slashCommand: params.slashCommand,
|
|
477
465
|
textLimit: params.textLimit,
|
|
478
466
|
ackReactionScope: params.ackReactionScope,
|
|
@@ -480,8 +468,6 @@ function createSlackMonitorContext(params) {
|
|
|
480
468
|
mediaMaxBytes: params.mediaMaxBytes,
|
|
481
469
|
removeAckAfterReply: params.removeAckAfterReply,
|
|
482
470
|
logger,
|
|
483
|
-
markMessageSeen,
|
|
484
|
-
releaseSeenMessage,
|
|
485
471
|
shouldDropMismatchedSlackEvent,
|
|
486
472
|
resolveSlackSystemEventSessionKey,
|
|
487
473
|
isChannelAllowed,
|
|
@@ -1927,6 +1913,26 @@ async function handleSlackBlockAction(params) {
|
|
|
1927
1913
|
});
|
|
1928
1914
|
return;
|
|
1929
1915
|
}
|
|
1916
|
+
if (isSlackQuestionActionId(parsed.actionId)) {
|
|
1917
|
+
const question = decodeSlackQuestionAction(parsed.actionSummary.value);
|
|
1918
|
+
if (!question) {
|
|
1919
|
+
await respondEphemeral(respond, "This question action is invalid or expired.");
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (!(await authorizeSlackBlockAction({
|
|
1923
|
+
ctx: params.ctx,
|
|
1924
|
+
parsed,
|
|
1925
|
+
respond
|
|
1926
|
+
})).allowed) return;
|
|
1927
|
+
await resolveSlackQuestionAction({
|
|
1928
|
+
action: question,
|
|
1929
|
+
cfg: params.ctx.cfg,
|
|
1930
|
+
accountId: params.ctx.accountId,
|
|
1931
|
+
userId: parsed.userId,
|
|
1932
|
+
respond: async (text) => await respondEphemeral(respond, text)
|
|
1933
|
+
});
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1930
1936
|
const pluginInteractionData = buildSlackPluginInteractionData({
|
|
1931
1937
|
actionId: parsed.actionId,
|
|
1932
1938
|
summary: parsed.actionSummary
|
|
@@ -2649,6 +2655,350 @@ function resolveSlackEventScope(params) {
|
|
|
2649
2655
|
};
|
|
2650
2656
|
}
|
|
2651
2657
|
//#endregion
|
|
2658
|
+
//#region extensions/slack/src/monitor/reconnect-policy.ts
|
|
2659
|
+
const SLACK_AUTH_ERROR_RE = /account_inactive|invalid_auth|token_revoked|token_expired|not_authed|org_login_required|team_access_not_granted|user_removed_from_team|team_disabled|missing_scope|cannot_find_service|invalid_token/i;
|
|
2660
|
+
const NO_ERROR_DETAIL = "no error detail";
|
|
2661
|
+
const SLACK_SOCKET_RECONNECT_POLICY = {
|
|
2662
|
+
initialMs: 2e3,
|
|
2663
|
+
maxMs: 3e4,
|
|
2664
|
+
factor: 1.8,
|
|
2665
|
+
jitter: .25
|
|
2666
|
+
};
|
|
2667
|
+
const SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL = "https://docs.slack.dev/apis/events-api/using-socket-mode#using-multiple-connections";
|
|
2668
|
+
const SLACK_SOCKET_HELLO_MARKER = Buffer.from("\"hello\"");
|
|
2669
|
+
function getSocketEmitter(app) {
|
|
2670
|
+
const receiver = app.receiver;
|
|
2671
|
+
const client = receiver && typeof receiver === "object" ? receiver.client : void 0;
|
|
2672
|
+
if (!client || typeof client !== "object") return null;
|
|
2673
|
+
const on = client.on;
|
|
2674
|
+
const off = client.off;
|
|
2675
|
+
if (typeof on !== "function" || typeof off !== "function") return null;
|
|
2676
|
+
return {
|
|
2677
|
+
on: (event, listener) => on.call(client, event, listener),
|
|
2678
|
+
off: (event, listener) => off.call(client, event, listener)
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
function isBufferArray(value) {
|
|
2682
|
+
return Array.isArray(value) && value.every((entry) => Buffer.isBuffer(entry));
|
|
2683
|
+
}
|
|
2684
|
+
function resolveSlackSocketModeConnectionCount(message) {
|
|
2685
|
+
const buffer = Buffer.isBuffer(message) ? message : message instanceof ArrayBuffer ? Buffer.from(message) : isBufferArray(message) ? Buffer.concat(message) : void 0;
|
|
2686
|
+
if (!buffer?.includes(SLACK_SOCKET_HELLO_MARKER)) return;
|
|
2687
|
+
let payload;
|
|
2688
|
+
try {
|
|
2689
|
+
payload = JSON.parse(buffer.toString("utf8"));
|
|
2690
|
+
} catch {
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
const count = isRecord(payload) && payload.type === "hello" ? payload.num_connections : void 0;
|
|
2694
|
+
return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
|
|
2695
|
+
}
|
|
2696
|
+
function formatSlackSocketModeSharedConnectionWarning(activeConnections) {
|
|
2697
|
+
return [
|
|
2698
|
+
`slack socket mode reports ${activeConnections} active connections for this Slack app`,
|
|
2699
|
+
"Slack may deliver each event to any one connection",
|
|
2700
|
+
"ensure every OpenClaw gateway sharing this app has equivalent routing and authorization, or use a separate Slack app per gateway, one relay ingress, or HTTP Request URLs behind a load balancer",
|
|
2701
|
+
`See ${SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL}`
|
|
2702
|
+
].join("; ");
|
|
2703
|
+
}
|
|
2704
|
+
function registerSlackSocketModeConnectionDiagnostics(params) {
|
|
2705
|
+
const emitter = getSocketEmitter(params.app);
|
|
2706
|
+
if (!emitter) return () => {};
|
|
2707
|
+
let hasWarned = false;
|
|
2708
|
+
const listener = (message, isBinary) => {
|
|
2709
|
+
if (isBinary === true || hasWarned) return;
|
|
2710
|
+
const activeConnections = resolveSlackSocketModeConnectionCount(message);
|
|
2711
|
+
if (activeConnections === void 0 || activeConnections <= 1) return;
|
|
2712
|
+
hasWarned = true;
|
|
2713
|
+
params.onSharedConnection(activeConnections);
|
|
2714
|
+
};
|
|
2715
|
+
emitter.on("ws_message", listener);
|
|
2716
|
+
return () => {
|
|
2717
|
+
emitter.off("ws_message", listener);
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
function waitForSlackSocketDisconnect(app, abortSignal) {
|
|
2721
|
+
return new Promise((resolve) => {
|
|
2722
|
+
const emitter = getSocketEmitter(app);
|
|
2723
|
+
if (!emitter) {
|
|
2724
|
+
abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), { once: true });
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
const disconnectListener = () => resolveOnce({ event: "disconnect" });
|
|
2728
|
+
const startFailListener = (error) => resolveOnce({
|
|
2729
|
+
event: "unable_to_socket_mode_start",
|
|
2730
|
+
error
|
|
2731
|
+
});
|
|
2732
|
+
const errorListener = (error) => resolveOnce({
|
|
2733
|
+
event: "error",
|
|
2734
|
+
error
|
|
2735
|
+
});
|
|
2736
|
+
const abortListener = () => resolveOnce({ event: "disconnect" });
|
|
2737
|
+
const cleanup = () => {
|
|
2738
|
+
emitter.off("disconnected", disconnectListener);
|
|
2739
|
+
emitter.off("unable_to_socket_mode_start", startFailListener);
|
|
2740
|
+
emitter.off("error", errorListener);
|
|
2741
|
+
abortSignal?.removeEventListener("abort", abortListener);
|
|
2742
|
+
};
|
|
2743
|
+
const resolveOnce = (value) => {
|
|
2744
|
+
cleanup();
|
|
2745
|
+
resolve(value);
|
|
2746
|
+
};
|
|
2747
|
+
emitter.on("disconnected", disconnectListener);
|
|
2748
|
+
emitter.on("unable_to_socket_mode_start", startFailListener);
|
|
2749
|
+
emitter.on("error", errorListener);
|
|
2750
|
+
abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Detect permanent Slack account and credential failures.
|
|
2755
|
+
* Transient request and HTTP failures stay in OpenClaw's reconnect loop.
|
|
2756
|
+
*/
|
|
2757
|
+
function isNonRecoverableSlackAuthError(error) {
|
|
2758
|
+
return SLACK_AUTH_ERROR_RE.test(formatUnknownError(error, ""));
|
|
2759
|
+
}
|
|
2760
|
+
function formatUnknownError(error, fallback = NO_ERROR_DETAIL) {
|
|
2761
|
+
return formatSlackError(error, fallback);
|
|
2762
|
+
}
|
|
2763
|
+
//#endregion
|
|
2764
|
+
//#region extensions/slack/src/monitor/ingress.ts
|
|
2765
|
+
const SLACK_INGRESS_PAYLOAD_VERSION = 1;
|
|
2766
|
+
const SLACK_INGRESS_POLL_INTERVAL_MS = 1e3;
|
|
2767
|
+
const SLACK_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
|
|
2768
|
+
const SLACK_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
2769
|
+
const SLACK_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
|
|
2770
|
+
const SLACK_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
2771
|
+
const SLACK_INGRESS_FAILED_MAX_ENTRIES = 2e4;
|
|
2772
|
+
const SLACK_BOLT_AUTHORIZATION_ERROR = "slack_bolt_authorization_error";
|
|
2773
|
+
const SLACK_INGRESS_LIFECYCLE_CONTEXT_KEY = "openclawIngressLifecycle";
|
|
2774
|
+
/** Logical message identity: mirrors the retired guard key (team:channel:ts). */
|
|
2775
|
+
function resolveSlackRelayIngressEventId(event) {
|
|
2776
|
+
const ts = event.message.ts?.trim();
|
|
2777
|
+
if (!event.message.channel?.trim() || !ts) return `relay:${event.deliveryId}`;
|
|
2778
|
+
const team = event.message.team?.trim();
|
|
2779
|
+
return `message:${team ? `${team}:` : ""}${event.message.channel.trim()}:${ts}`;
|
|
2780
|
+
}
|
|
2781
|
+
var SlackIngressPayloadError = class extends Error {
|
|
2782
|
+
constructor(message) {
|
|
2783
|
+
super(message);
|
|
2784
|
+
this.name = "SlackIngressPayloadError";
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
function resolveSlackEventId(body) {
|
|
2788
|
+
const eventId = asOptionalRecord(body)?.event_id;
|
|
2789
|
+
return typeof eventId === "string" && eventId.trim() ? eventId.trim() : null;
|
|
2790
|
+
}
|
|
2791
|
+
function resolveSlackIngressLane(body, eventId) {
|
|
2792
|
+
const envelope = asOptionalRecord(body);
|
|
2793
|
+
const event = asOptionalRecord(envelope?.event);
|
|
2794
|
+
const item = asOptionalRecord(event?.item);
|
|
2795
|
+
const assistantThread = asOptionalRecord(event?.assistant_thread);
|
|
2796
|
+
const team = asOptionalRecord(envelope?.team);
|
|
2797
|
+
const teamId = [
|
|
2798
|
+
envelope?.team_id,
|
|
2799
|
+
team?.id,
|
|
2800
|
+
event?.team
|
|
2801
|
+
].find((value) => typeof value === "string" && value.trim())?.toString().trim() || "workspace";
|
|
2802
|
+
const channelId = [
|
|
2803
|
+
event?.channel,
|
|
2804
|
+
event?.channel_id,
|
|
2805
|
+
item?.channel,
|
|
2806
|
+
assistantThread?.channel_id
|
|
2807
|
+
].find((value) => typeof value === "string" && value.trim())?.toString().trim();
|
|
2808
|
+
if (channelId) return `team:${teamId}:conversation:${channelId}`;
|
|
2809
|
+
const userId = [event?.user, event?.user_id].find((value) => typeof value === "string" && value.trim())?.toString().trim();
|
|
2810
|
+
return userId ? `team:${teamId}:user:${userId}` : `event:${eventId}`;
|
|
2811
|
+
}
|
|
2812
|
+
function isSlackEventCallback(body) {
|
|
2813
|
+
return asOptionalRecord(body)?.type === "event_callback";
|
|
2814
|
+
}
|
|
2815
|
+
function assertSlackIngressPayload(payload, eventId) {
|
|
2816
|
+
if (payload.version !== SLACK_INGRESS_PAYLOAD_VERSION) throw new SlackIngressPayloadError(`Slack ingress payload ${eventId} was invalid.`);
|
|
2817
|
+
if (payload.kind === "relay") {
|
|
2818
|
+
if (!asOptionalRecord(payload.message)) throw new SlackIngressPayloadError(`Slack relay ingress payload ${eventId} was invalid.`);
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
if (!asOptionalRecord(payload.body) || resolveSlackEventId(payload.body) !== eventId) throw new SlackIngressPayloadError(`Slack ingress payload ${eventId} was invalid.`);
|
|
2822
|
+
}
|
|
2823
|
+
function resolveSlackIngressNonRetryableFailure(error) {
|
|
2824
|
+
for (const candidate of collectErrorGraphCandidates(error, (current) => [
|
|
2825
|
+
current.cause,
|
|
2826
|
+
current.error,
|
|
2827
|
+
current.original
|
|
2828
|
+
])) {
|
|
2829
|
+
if (candidate instanceof SlackIngressPayloadError || candidate instanceof SyntaxError) return {
|
|
2830
|
+
reason: "invalid-event",
|
|
2831
|
+
message: formatErrorMessage(candidate)
|
|
2832
|
+
};
|
|
2833
|
+
if (extractErrorCode(candidate) === SLACK_BOLT_AUTHORIZATION_ERROR || isNonRecoverableSlackAuthError(candidate)) return {
|
|
2834
|
+
reason: "slack-auth",
|
|
2835
|
+
message: formatErrorMessage(candidate)
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
return null;
|
|
2839
|
+
}
|
|
2840
|
+
function resolveSlackIngressTurnLifecycle(context) {
|
|
2841
|
+
const candidate = asOptionalRecord(context)?.[SLACK_INGRESS_LIFECYCLE_CONTEXT_KEY];
|
|
2842
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
2843
|
+
const lifecycle = candidate;
|
|
2844
|
+
return typeof lifecycle.onAdopted === "function" && lifecycle.abortSignal instanceof AbortSignal ? lifecycle : null;
|
|
2845
|
+
}
|
|
2846
|
+
function createSlackDurableIngress(options) {
|
|
2847
|
+
let queue = options.queue;
|
|
2848
|
+
let drain;
|
|
2849
|
+
let app;
|
|
2850
|
+
let relayDispatch;
|
|
2851
|
+
let running = false;
|
|
2852
|
+
let requested = false;
|
|
2853
|
+
let pumping;
|
|
2854
|
+
let pollTimer;
|
|
2855
|
+
let lastPrunedAt = 0;
|
|
2856
|
+
const getQueue = () => {
|
|
2857
|
+
queue ??= getSlackRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
2858
|
+
return queue;
|
|
2859
|
+
};
|
|
2860
|
+
const getDrain = () => {
|
|
2861
|
+
drain ??= createChannelIngressDrain({
|
|
2862
|
+
queue: getQueue(),
|
|
2863
|
+
adoptionStallTimeoutMs: options.adoptionStallTimeoutMs ?? DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
2864
|
+
retryPolicy: {
|
|
2865
|
+
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
2866
|
+
deadLetterMinAgeMs: DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS
|
|
2867
|
+
},
|
|
2868
|
+
resolveNonRetryableFailure: resolveSlackIngressNonRetryableFailure,
|
|
2869
|
+
deriveLaneKey: (record) => record.payload.kind === "relay" ? resolveSlackIngressLane({ event: record.payload.message }, record.id) : resolveSlackIngressLane(record.payload.body, record.id),
|
|
2870
|
+
...options.onLog ? { onLog: options.onLog } : {},
|
|
2871
|
+
...options.abortSignal ? { abortSignal: options.abortSignal } : {},
|
|
2872
|
+
dispatchClaimedEvent: async (event, lifecycle) => {
|
|
2873
|
+
assertSlackIngressPayload(event.payload, event.id);
|
|
2874
|
+
if (lifecycle.abortSignal.aborted) throw lifecycle.abortSignal.reason;
|
|
2875
|
+
const bound = bindIngressLifecycleToReplyOptions(lifecycle);
|
|
2876
|
+
if (event.payload.kind === "relay") {
|
|
2877
|
+
if (!relayDispatch) throw new Error("Slack relay ingress dispatcher is not attached.");
|
|
2878
|
+
await relayDispatch(event.payload.message, bound.turnAdoptionLifecycle);
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
if (!app) throw new Error("Slack ingress receiver is not attached to a Bolt app.");
|
|
2882
|
+
await app.processEvent({
|
|
2883
|
+
body: event.payload.body,
|
|
2884
|
+
ack: async () => {},
|
|
2885
|
+
...event.payload.retryNum === void 0 ? {} : { retryNum: event.payload.retryNum },
|
|
2886
|
+
...event.payload.retryReason === void 0 ? {} : { retryReason: event.payload.retryReason },
|
|
2887
|
+
customProperties: { [SLACK_INGRESS_LIFECYCLE_CONTEXT_KEY]: bound.turnAdoptionLifecycle }
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
return drain;
|
|
2892
|
+
};
|
|
2893
|
+
const pruneIfDue = async () => {
|
|
2894
|
+
const now = Date.now();
|
|
2895
|
+
if (now - lastPrunedAt < SLACK_INGRESS_PRUNE_INTERVAL_MS) return;
|
|
2896
|
+
await getQueue().prune({
|
|
2897
|
+
completedTtlMs: SLACK_INGRESS_COMPLETED_TTL_MS,
|
|
2898
|
+
completedMaxEntries: SLACK_INGRESS_COMPLETED_MAX_ENTRIES,
|
|
2899
|
+
failedTtlMs: SLACK_INGRESS_FAILED_TTL_MS,
|
|
2900
|
+
failedMaxEntries: SLACK_INGRESS_FAILED_MAX_ENTRIES,
|
|
2901
|
+
now
|
|
2902
|
+
});
|
|
2903
|
+
lastPrunedAt = now;
|
|
2904
|
+
};
|
|
2905
|
+
const runPump = async () => {
|
|
2906
|
+
try {
|
|
2907
|
+
for (;;) {
|
|
2908
|
+
requested = false;
|
|
2909
|
+
await pruneIfDue();
|
|
2910
|
+
const activeDrain = getDrain();
|
|
2911
|
+
const { started } = await activeDrain.drainOnce();
|
|
2912
|
+
await activeDrain.waitForIdle();
|
|
2913
|
+
if (!running || !requested && started === 0) break;
|
|
2914
|
+
}
|
|
2915
|
+
} catch (error) {
|
|
2916
|
+
options.onLog?.(`slack ingress drain failed: ${formatErrorMessage(error)}`);
|
|
2917
|
+
} finally {
|
|
2918
|
+
pumping = void 0;
|
|
2919
|
+
if (running && requested) requestDrain();
|
|
2920
|
+
}
|
|
2921
|
+
};
|
|
2922
|
+
const requestDrain = () => {
|
|
2923
|
+
requested = true;
|
|
2924
|
+
if (!running || pumping) return;
|
|
2925
|
+
pumping = runPump();
|
|
2926
|
+
};
|
|
2927
|
+
const acceptReceiverEvent = async (event) => {
|
|
2928
|
+
if (!isSlackEventCallback(event.body)) {
|
|
2929
|
+
if (!app) throw new Error("Slack ingress receiver is not attached to a Bolt app.");
|
|
2930
|
+
await app.processEvent(event);
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
const eventId = resolveSlackEventId(event.body);
|
|
2934
|
+
if (!eventId) throw new SlackIngressPayloadError("Slack Events API envelope missing event_id.");
|
|
2935
|
+
const receivedAt = Date.now();
|
|
2936
|
+
await getQueue().enqueue(eventId, {
|
|
2937
|
+
version: SLACK_INGRESS_PAYLOAD_VERSION,
|
|
2938
|
+
receivedAt,
|
|
2939
|
+
kind: "events-api",
|
|
2940
|
+
body: event.body,
|
|
2941
|
+
...event.retryNum === void 0 ? {} : { retryNum: event.retryNum },
|
|
2942
|
+
...event.retryReason === void 0 ? {} : { retryReason: event.retryReason }
|
|
2943
|
+
}, { receivedAt });
|
|
2944
|
+
await event.ack();
|
|
2945
|
+
requestDrain();
|
|
2946
|
+
};
|
|
2947
|
+
const acceptRelayEvent = async (event) => {
|
|
2948
|
+
const receivedAt = Date.now();
|
|
2949
|
+
await getQueue().enqueue(resolveSlackRelayIngressEventId(event), {
|
|
2950
|
+
version: SLACK_INGRESS_PAYLOAD_VERSION,
|
|
2951
|
+
receivedAt,
|
|
2952
|
+
kind: "relay",
|
|
2953
|
+
message: event.message
|
|
2954
|
+
}, { receivedAt });
|
|
2955
|
+
requestDrain();
|
|
2956
|
+
};
|
|
2957
|
+
return {
|
|
2958
|
+
wrapReceiver: (receiver) => {
|
|
2959
|
+
const client = Reflect.get(receiver, "client");
|
|
2960
|
+
return {
|
|
2961
|
+
init: (nextApp) => {
|
|
2962
|
+
app = nextApp;
|
|
2963
|
+
receiver.init({ processEvent: acceptReceiverEvent });
|
|
2964
|
+
},
|
|
2965
|
+
start: (...args) => receiver.start(...args),
|
|
2966
|
+
stop: (...args) => receiver.stop(...args),
|
|
2967
|
+
...client === void 0 ? {} : { client }
|
|
2968
|
+
};
|
|
2969
|
+
},
|
|
2970
|
+
acceptRelayEvent,
|
|
2971
|
+
attachRelayDispatch: (dispatch) => {
|
|
2972
|
+
relayDispatch = dispatch;
|
|
2973
|
+
},
|
|
2974
|
+
start: () => {
|
|
2975
|
+
if (running) return;
|
|
2976
|
+
running = true;
|
|
2977
|
+
pollTimer = setInterval(requestDrain, options.pollIntervalMs ?? SLACK_INGRESS_POLL_INTERVAL_MS);
|
|
2978
|
+
pollTimer.unref?.();
|
|
2979
|
+
requestDrain();
|
|
2980
|
+
},
|
|
2981
|
+
stop: async () => {
|
|
2982
|
+
running = false;
|
|
2983
|
+
if (pollTimer) {
|
|
2984
|
+
clearInterval(pollTimer);
|
|
2985
|
+
pollTimer = void 0;
|
|
2986
|
+
}
|
|
2987
|
+
drain?.dispose();
|
|
2988
|
+
await pumping;
|
|
2989
|
+
await drain?.waitForIdle();
|
|
2990
|
+
},
|
|
2991
|
+
waitForIdle: async () => {
|
|
2992
|
+
for (;;) {
|
|
2993
|
+
const activePump = pumping;
|
|
2994
|
+
if (!activePump) break;
|
|
2995
|
+
await activePump;
|
|
2996
|
+
}
|
|
2997
|
+
await drain?.waitForIdle();
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
//#endregion
|
|
2652
3002
|
//#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
|
|
2653
3003
|
const SUBTYPE_HANDLER_REGISTRY = {
|
|
2654
3004
|
message_changed: {
|
|
@@ -2776,6 +3126,7 @@ function registerSlackMessageEvents(params) {
|
|
|
2776
3126
|
return resolved.scope;
|
|
2777
3127
|
};
|
|
2778
3128
|
const handleIncomingMessageEvent = async ({ event, body, context, client }) => {
|
|
3129
|
+
const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
|
|
2779
3130
|
try {
|
|
2780
3131
|
const eventScope = resolveEventScope({
|
|
2781
3132
|
body,
|
|
@@ -2801,10 +3152,9 @@ function registerSlackMessageEvents(params) {
|
|
|
2801
3152
|
if (assistantChangedInbound) {
|
|
2802
3153
|
await handleSlackMessage(assistantChangedInbound, {
|
|
2803
3154
|
source: "message",
|
|
2804
|
-
...eventScope ? {
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
} : {}
|
|
3155
|
+
...eventScope ? { eventScope } : {},
|
|
3156
|
+
...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
|
|
3157
|
+
...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
|
|
2808
3158
|
});
|
|
2809
3159
|
return;
|
|
2810
3160
|
}
|
|
@@ -2832,12 +3182,12 @@ function registerSlackMessageEvents(params) {
|
|
|
2832
3182
|
}
|
|
2833
3183
|
await handleSlackMessage(message, {
|
|
2834
3184
|
source: "message",
|
|
2835
|
-
...eventScope ? {
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
} : {}
|
|
3185
|
+
...eventScope ? { eventScope } : {},
|
|
3186
|
+
...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
|
|
3187
|
+
...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
|
|
2839
3188
|
});
|
|
2840
3189
|
} catch (err) {
|
|
3190
|
+
if (turnAdoptionLifecycle) throw err;
|
|
2841
3191
|
ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
|
|
2842
3192
|
}
|
|
2843
3193
|
};
|
|
@@ -2846,6 +3196,7 @@ function registerSlackMessageEvents(params) {
|
|
|
2846
3196
|
});
|
|
2847
3197
|
ctx.app.event("app_mention", async (args) => {
|
|
2848
3198
|
const { event, body, context, client } = args;
|
|
3199
|
+
const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
|
|
2849
3200
|
try {
|
|
2850
3201
|
const eventScope = resolveEventScope({
|
|
2851
3202
|
body,
|
|
@@ -2872,12 +3223,12 @@ function registerSlackMessageEvents(params) {
|
|
|
2872
3223
|
await handleSlackMessage(mention, {
|
|
2873
3224
|
source: "app_mention",
|
|
2874
3225
|
wasMentioned: true,
|
|
2875
|
-
...eventScope ? {
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
} : {}
|
|
3226
|
+
...eventScope ? { eventScope } : {},
|
|
3227
|
+
...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
|
|
3228
|
+
...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
|
|
2879
3229
|
});
|
|
2880
3230
|
} catch (err) {
|
|
3231
|
+
if (turnAdoptionLifecycle) throw err;
|
|
2881
3232
|
ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
|
|
2882
3233
|
}
|
|
2883
3234
|
});
|
|
@@ -3036,41 +3387,58 @@ function registerSlackMonitorEvents(params) {
|
|
|
3036
3387
|
});
|
|
3037
3388
|
}
|
|
3038
3389
|
//#endregion
|
|
3039
|
-
//#region extensions/slack/src/monitor/
|
|
3040
|
-
const
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3390
|
+
//#region extensions/slack/src/monitor/message-dispatch-dedupe.ts
|
|
3391
|
+
const SLACK_MESSAGE_DISPATCH_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
|
|
3392
|
+
const SLACK_MESSAGE_DISPATCH_DEDUPE_MEMORY_MAX_ENTRIES = 2e4;
|
|
3393
|
+
const SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_MAX_ENTRIES = 2e4;
|
|
3394
|
+
const SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE = "global";
|
|
3395
|
+
const SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE_PREFIX = "slack.message-dispatch-dedupe";
|
|
3396
|
+
const SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_PLUGIN_ID = "slack-message-dispatch-dedupe";
|
|
3397
|
+
function buildSlackMessageDispatchReplayKey(params) {
|
|
3398
|
+
const channelId = params.channelId?.trim();
|
|
3399
|
+
const ts = params.ts?.trim();
|
|
3400
|
+
if (!channelId || !ts) return null;
|
|
3401
|
+
const teamId = params.teamId?.trim();
|
|
3402
|
+
return JSON.stringify([
|
|
3403
|
+
"message",
|
|
3404
|
+
params.accountId,
|
|
3405
|
+
teamId ?? "",
|
|
3406
|
+
channelId,
|
|
3407
|
+
ts
|
|
3408
|
+
]);
|
|
3409
|
+
}
|
|
3410
|
+
function createSlackMessageDispatchReplayGuard(params = {}) {
|
|
3411
|
+
return createChannelReplayGuard({
|
|
3412
|
+
dedupe: {
|
|
3413
|
+
ttlMs: SLACK_MESSAGE_DISPATCH_DEDUPE_TTL_MS,
|
|
3414
|
+
memoryMaxSize: SLACK_MESSAGE_DISPATCH_DEDUPE_MEMORY_MAX_ENTRIES,
|
|
3415
|
+
pluginId: SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_PLUGIN_ID,
|
|
3416
|
+
namespacePrefix: SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE_PREFIX,
|
|
3417
|
+
stateMaxEntries: SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_MAX_ENTRIES,
|
|
3418
|
+
...params.onDiskError ? { onDiskError: params.onDiskError } : {}
|
|
3419
|
+
},
|
|
3420
|
+
buildReplayKey: (event) => event.keys,
|
|
3421
|
+
namespace: () => SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
/** Claim one logical message key; an in-flight sibling claim settles to duplicate. */
|
|
3425
|
+
async function claimSlackMessageDispatchReplay(params) {
|
|
3426
|
+
let releaseRetries = 0;
|
|
3427
|
+
while (true) {
|
|
3428
|
+
const claim = await params.guard.claim({ keys: [params.key] });
|
|
3429
|
+
if (claim.kind === "claimed") return {
|
|
3430
|
+
kind: "claimed",
|
|
3431
|
+
handle: claim.handle
|
|
3432
|
+
};
|
|
3433
|
+
if (claim.kind === "duplicate" || claim.kind === "invalid") return { kind: "duplicate" };
|
|
3434
|
+
try {
|
|
3435
|
+
await claim.pending;
|
|
3436
|
+
return { kind: "duplicate" };
|
|
3437
|
+
} catch {
|
|
3438
|
+
releaseRetries += 1;
|
|
3439
|
+
if (releaseRetries > 1) return { kind: "duplicate" };
|
|
3055
3440
|
}
|
|
3056
3441
|
}
|
|
3057
|
-
});
|
|
3058
|
-
function makeKey(accountId, channelId, ts, teamId) {
|
|
3059
|
-
return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
3060
|
-
}
|
|
3061
|
-
async function hasSlackInboundMessageDelivery(params) {
|
|
3062
|
-
if (!params.accountId || !params.channelId || !params.ts) return false;
|
|
3063
|
-
return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts, params.teamId));
|
|
3064
|
-
}
|
|
3065
|
-
async function recordSlackInboundMessageDeliveries(params) {
|
|
3066
|
-
if (!params.accountId || params.messages.length === 0) return;
|
|
3067
|
-
const deliveredAt = Date.now();
|
|
3068
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3069
|
-
for (const message of params.messages) {
|
|
3070
|
-
if (!message.channel || !message.ts) continue;
|
|
3071
|
-
keys.add(makeKey(params.accountId, message.channel, message.ts, params.teamId));
|
|
3072
|
-
}
|
|
3073
|
-
await Promise.all(Array.from(keys, (key) => deliveredMessages.register(key, { deliveredAt }, { at: deliveredAt })));
|
|
3074
3442
|
}
|
|
3075
3443
|
//#endregion
|
|
3076
3444
|
//#region extensions/slack/src/monitor/message-handler/debounce-key.ts
|
|
@@ -3198,7 +3566,7 @@ function createSlackThreadTsResolver(params) {
|
|
|
3198
3566
|
}
|
|
3199
3567
|
//#endregion
|
|
3200
3568
|
//#region extensions/slack/src/monitor/message-handler.ts
|
|
3201
|
-
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-
|
|
3569
|
+
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-CTvokbty.js"));
|
|
3202
3570
|
function createSlackDispatchCompletion() {
|
|
3203
3571
|
let resolve;
|
|
3204
3572
|
let reject;
|
|
@@ -3211,7 +3579,6 @@ function createSlackDispatchCompletion() {
|
|
|
3211
3579
|
reject
|
|
3212
3580
|
};
|
|
3213
3581
|
}
|
|
3214
|
-
const APP_MENTION_RETRY_TTL_MS = 6e4;
|
|
3215
3582
|
const RETRYABLE_FLUSH_MAX_ATTEMPTS = 3;
|
|
3216
3583
|
const RETRYABLE_FLUSH_RETRY_DELAY_MS = 1e3;
|
|
3217
3584
|
const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
|
|
@@ -3225,12 +3592,9 @@ function shouldDebounceSlackMessage(message, cfg) {
|
|
|
3225
3592
|
hasMedia: Boolean(message.files && message.files.length > 0)
|
|
3226
3593
|
});
|
|
3227
3594
|
}
|
|
3228
|
-
function buildSeenMessageKey(channelId, ts, teamId) {
|
|
3229
|
-
if (!channelId || !ts) return null;
|
|
3230
|
-
return `${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
3231
|
-
}
|
|
3232
3595
|
function createSlackMessageHandler(params) {
|
|
3233
3596
|
const { ctx, account, trackEvent, onPrepared } = params;
|
|
3597
|
+
const dispatchReplayGuard = params.dispatchReplayGuard ?? createSlackMessageDispatchReplayGuard({ onDiskError: (error) => ctx.runtime.error?.(`slack message dispatch dedupe persistence failed: ${formatErrorMessage(error)}`) });
|
|
3234
3598
|
const { debounceMs, debouncer } = createChannelInboundDebouncer({
|
|
3235
3599
|
cfg: ctx.cfg,
|
|
3236
3600
|
channel: "slack",
|
|
@@ -3263,8 +3627,41 @@ function createSlackMessageHandler(params) {
|
|
|
3263
3627
|
const completions = entries.map((entry) => entry.opts.dispatchCompletion).filter((completion) => completion !== void 0);
|
|
3264
3628
|
try {
|
|
3265
3629
|
await (async () => {
|
|
3266
|
-
const
|
|
3267
|
-
|
|
3630
|
+
const claims = [];
|
|
3631
|
+
const claimedKeys = /* @__PURE__ */ new Set();
|
|
3632
|
+
const surviving = [];
|
|
3633
|
+
for (const entry of entries) {
|
|
3634
|
+
const replayKey = buildSlackMessageDispatchReplayKey({
|
|
3635
|
+
accountId: ctx.accountId,
|
|
3636
|
+
channelId: entry.message.channel,
|
|
3637
|
+
ts: entry.message.ts,
|
|
3638
|
+
teamId: entry.opts.eventScope?.teamId
|
|
3639
|
+
});
|
|
3640
|
+
if (!replayKey || claimedKeys.has(replayKey)) {
|
|
3641
|
+
surviving.push(entry);
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
const claim = await claimSlackMessageDispatchReplay({
|
|
3645
|
+
guard: dispatchReplayGuard,
|
|
3646
|
+
key: replayKey
|
|
3647
|
+
});
|
|
3648
|
+
if (claim.kind === "claimed") {
|
|
3649
|
+
claims.push(claim.handle);
|
|
3650
|
+
claimedKeys.add(replayKey);
|
|
3651
|
+
surviving.push(entry);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
const releaseClaims = (error) => {
|
|
3655
|
+
for (const handle of claims) handle.release(error === void 0 ? {} : { error });
|
|
3656
|
+
};
|
|
3657
|
+
const commitClaims = async () => {
|
|
3658
|
+
for (const handle of claims) await handle.commit();
|
|
3659
|
+
};
|
|
3660
|
+
const last = surviving.at(-1);
|
|
3661
|
+
if (!last) {
|
|
3662
|
+
releaseClaims();
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3268
3665
|
const teamId = last.opts.eventScope?.teamId;
|
|
3269
3666
|
const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
|
|
3270
3667
|
const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId, teamId);
|
|
@@ -3275,76 +3672,60 @@ function createSlackMessageHandler(params) {
|
|
|
3275
3672
|
if (pendingKeys.size === 0) pendingTopLevelDebounceKeys.delete(topLevelConversationKey);
|
|
3276
3673
|
}
|
|
3277
3674
|
}
|
|
3278
|
-
const combinedText =
|
|
3279
|
-
const combinedMentioned =
|
|
3675
|
+
const combinedText = surviving.length === 1 ? last.message.text ?? "" : surviving.map((entry) => entry.message.text ?? "").filter(Boolean).join("\n");
|
|
3676
|
+
const combinedMentioned = surviving.some((entry) => Boolean(entry.opts.wasMentioned));
|
|
3280
3677
|
const syntheticMessage = {
|
|
3281
3678
|
...last.message,
|
|
3282
3679
|
text: combinedText
|
|
3283
3680
|
};
|
|
3284
|
-
const
|
|
3681
|
+
const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
|
|
3682
|
+
const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, turnAdoptionLifecycle, ...lastOpts } = last.opts;
|
|
3683
|
+
let prepared;
|
|
3684
|
+
let settlementHandedOff = false;
|
|
3285
3685
|
try {
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
} finally {
|
|
3314
|
-
if (appMentionRetryKey) appMentionPreparingKeys.delete(appMentionRetryKey);
|
|
3686
|
+
prepared = await prepareSlackMessage({
|
|
3687
|
+
ctx,
|
|
3688
|
+
account,
|
|
3689
|
+
message: syntheticMessage,
|
|
3690
|
+
opts: {
|
|
3691
|
+
...lastOpts,
|
|
3692
|
+
wasMentioned: combinedMentioned || last.opts.wasMentioned
|
|
3693
|
+
}
|
|
3694
|
+
});
|
|
3695
|
+
if (!prepared) {
|
|
3696
|
+
releaseClaims();
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
prepared.turnAdoptionLifecycle = turnAdoptionLifecycle ? {
|
|
3700
|
+
...turnAdoptionLifecycle,
|
|
3701
|
+
onAdopted: async () => {
|
|
3702
|
+
settlementHandedOff = true;
|
|
3703
|
+
await commitClaims();
|
|
3704
|
+
await turnAdoptionLifecycle.onAdopted();
|
|
3705
|
+
},
|
|
3706
|
+
onDeferred: () => {
|
|
3707
|
+
settlementHandedOff = true;
|
|
3708
|
+
turnAdoptionLifecycle.onDeferred();
|
|
3709
|
+
},
|
|
3710
|
+
onAbandoned: () => {
|
|
3711
|
+
releaseClaims();
|
|
3712
|
+
turnAdoptionLifecycle.onAbandoned();
|
|
3315
3713
|
}
|
|
3316
|
-
}
|
|
3317
|
-
if (!prepared) return;
|
|
3714
|
+
} : turnAdoptionLifecycle;
|
|
3318
3715
|
onPrepared?.(prepared);
|
|
3319
|
-
if (
|
|
3320
|
-
const ids =
|
|
3716
|
+
if (surviving.length > 1) {
|
|
3717
|
+
const ids = surviving.map((entry) => entry.message.ts).filter(Boolean);
|
|
3321
3718
|
if (ids.length > 0) {
|
|
3322
3719
|
prepared.ctxPayload.MessageSids = ids;
|
|
3323
3720
|
prepared.ctxPayload.MessageSidFirst = ids[0];
|
|
3324
3721
|
prepared.ctxPayload.MessageSidLast = ids[ids.length - 1];
|
|
3325
3722
|
}
|
|
3326
3723
|
}
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
accountId: ctx.accountId,
|
|
3331
|
-
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
3332
|
-
messages: entries.map((entry) => entry.message)
|
|
3333
|
-
});
|
|
3334
|
-
} catch (error) {
|
|
3335
|
-
if (!isRetryableSlackInboundError(error)) await recordSlackInboundMessageDeliveries({
|
|
3336
|
-
accountId: ctx.accountId,
|
|
3337
|
-
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
3338
|
-
messages: entries.map((entry) => entry.message)
|
|
3339
|
-
});
|
|
3340
|
-
throw error;
|
|
3341
|
-
}
|
|
3724
|
+
await dispatchPreparedSlackMessage(prepared);
|
|
3725
|
+
if (!turnAdoptionLifecycle) await commitClaims();
|
|
3726
|
+
else if (!settlementHandedOff) releaseClaims();
|
|
3342
3727
|
} catch (error) {
|
|
3343
|
-
|
|
3344
|
-
const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts, entry.opts.eventScope?.teamId);
|
|
3345
|
-
if (entrySeenKey) appMentionDispatchedKeys.delete(entrySeenKey);
|
|
3346
|
-
ctx.releaseSeenMessage(entry.message.channel, entry.message.ts, entry.opts.eventScope);
|
|
3347
|
-
}
|
|
3728
|
+
releaseClaims(error);
|
|
3348
3729
|
throw error;
|
|
3349
3730
|
}
|
|
3350
3731
|
})();
|
|
@@ -3361,51 +3742,10 @@ function createSlackMessageHandler(params) {
|
|
|
3361
3742
|
});
|
|
3362
3743
|
const threadTsResolver = createSlackThreadTsResolver({ client: ctx.app.client });
|
|
3363
3744
|
const pendingTopLevelDebounceKeys = /* @__PURE__ */ new Map();
|
|
3364
|
-
const appMentionRetryKeys = /* @__PURE__ */ new Map();
|
|
3365
|
-
const appMentionPreparingKeys = /* @__PURE__ */ new Set();
|
|
3366
|
-
const appMentionDispatchedKeys = /* @__PURE__ */ new Map();
|
|
3367
|
-
const pruneAppMentionRetryKeys = (rawNow) => {
|
|
3368
|
-
const now = asDateTimestampMs(rawNow);
|
|
3369
|
-
if (now === void 0) {
|
|
3370
|
-
appMentionRetryKeys.clear();
|
|
3371
|
-
appMentionDispatchedKeys.clear();
|
|
3372
|
-
return false;
|
|
3373
|
-
}
|
|
3374
|
-
for (const [key, expiresAt] of appMentionRetryKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionRetryKeys.delete(key);
|
|
3375
|
-
for (const [key, expiresAt] of appMentionDispatchedKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionDispatchedKeys.delete(key);
|
|
3376
|
-
return true;
|
|
3377
|
-
};
|
|
3378
|
-
const rememberExpiringAppMentionKey = (map, key) => {
|
|
3379
|
-
const now = Date.now();
|
|
3380
|
-
if (!pruneAppMentionRetryKeys(now)) return;
|
|
3381
|
-
const expiresAt = resolveExpiresAtMsFromDurationMs(APP_MENTION_RETRY_TTL_MS, { nowMs: now });
|
|
3382
|
-
if (expiresAt !== void 0) map.set(key, expiresAt);
|
|
3383
|
-
};
|
|
3384
|
-
const rememberAppMentionRetryKey = (key) => {
|
|
3385
|
-
rememberExpiringAppMentionKey(appMentionRetryKeys, key);
|
|
3386
|
-
};
|
|
3387
|
-
const consumeAppMentionRetryKey = (key) => {
|
|
3388
|
-
if (!pruneAppMentionRetryKeys(Date.now())) return false;
|
|
3389
|
-
if (!appMentionRetryKeys.has(key)) return false;
|
|
3390
|
-
appMentionRetryKeys.delete(key);
|
|
3391
|
-
return true;
|
|
3392
|
-
};
|
|
3393
3745
|
async function enqueueSlackMessage(message, opts) {
|
|
3394
3746
|
if (opts.source === "message" && message.type !== "message") return;
|
|
3395
3747
|
if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
|
|
3396
3748
|
ctx.rememberSlackChannelType(message.channel, message.channel_type, opts.eventScope);
|
|
3397
|
-
const seenMessageKey = buildSeenMessageKey(message.channel, message.ts, opts.eventScope?.teamId);
|
|
3398
|
-
if (seenMessageKey && await hasSlackInboundMessageDelivery({
|
|
3399
|
-
accountId: ctx.accountId,
|
|
3400
|
-
channelId: message.channel,
|
|
3401
|
-
ts: message.ts,
|
|
3402
|
-
...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
|
|
3403
|
-
})) return;
|
|
3404
|
-
const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts, opts.eventScope) : false;
|
|
3405
|
-
if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
|
|
3406
|
-
if (seenMessageKey && wasSeen) {
|
|
3407
|
-
if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
|
|
3408
|
-
}
|
|
3409
3749
|
trackEvent?.();
|
|
3410
3750
|
const resolvedMessage = await (opts.eventScope ? createSlackThreadTsResolver({ client: opts.eventScope.client }) : threadTsResolver).resolve({
|
|
3411
3751
|
message,
|
|
@@ -3642,112 +3982,6 @@ function openSlackPresenceCooldownStore() {
|
|
|
3642
3982
|
});
|
|
3643
3983
|
}
|
|
3644
3984
|
//#endregion
|
|
3645
|
-
//#region extensions/slack/src/monitor/reconnect-policy.ts
|
|
3646
|
-
const SLACK_AUTH_ERROR_RE = /account_inactive|invalid_auth|token_revoked|token_expired|not_authed|org_login_required|team_access_not_granted|user_removed_from_team|team_disabled|missing_scope|cannot_find_service|invalid_token/i;
|
|
3647
|
-
const NO_ERROR_DETAIL = "no error detail";
|
|
3648
|
-
const SLACK_SOCKET_RECONNECT_POLICY = {
|
|
3649
|
-
initialMs: 2e3,
|
|
3650
|
-
maxMs: 3e4,
|
|
3651
|
-
factor: 1.8,
|
|
3652
|
-
jitter: .25
|
|
3653
|
-
};
|
|
3654
|
-
const SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL = "https://docs.slack.dev/apis/events-api/using-socket-mode#using-multiple-connections";
|
|
3655
|
-
const SLACK_SOCKET_HELLO_MARKER = Buffer.from("\"hello\"");
|
|
3656
|
-
function getSocketEmitter(app) {
|
|
3657
|
-
const receiver = app.receiver;
|
|
3658
|
-
const client = receiver && typeof receiver === "object" ? receiver.client : void 0;
|
|
3659
|
-
if (!client || typeof client !== "object") return null;
|
|
3660
|
-
const on = client.on;
|
|
3661
|
-
const off = client.off;
|
|
3662
|
-
if (typeof on !== "function" || typeof off !== "function") return null;
|
|
3663
|
-
return {
|
|
3664
|
-
on: (event, listener) => on.call(client, event, listener),
|
|
3665
|
-
off: (event, listener) => off.call(client, event, listener)
|
|
3666
|
-
};
|
|
3667
|
-
}
|
|
3668
|
-
function isBufferArray(value) {
|
|
3669
|
-
return Array.isArray(value) && value.every((entry) => Buffer.isBuffer(entry));
|
|
3670
|
-
}
|
|
3671
|
-
function resolveSlackSocketModeConnectionCount(message) {
|
|
3672
|
-
const buffer = Buffer.isBuffer(message) ? message : message instanceof ArrayBuffer ? Buffer.from(message) : isBufferArray(message) ? Buffer.concat(message) : void 0;
|
|
3673
|
-
if (!buffer?.includes(SLACK_SOCKET_HELLO_MARKER)) return;
|
|
3674
|
-
let payload;
|
|
3675
|
-
try {
|
|
3676
|
-
payload = JSON.parse(buffer.toString("utf8"));
|
|
3677
|
-
} catch {
|
|
3678
|
-
return;
|
|
3679
|
-
}
|
|
3680
|
-
const count = isRecord(payload) && payload.type === "hello" ? payload.num_connections : void 0;
|
|
3681
|
-
return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
|
|
3682
|
-
}
|
|
3683
|
-
function formatSlackSocketModeSharedConnectionWarning(activeConnections) {
|
|
3684
|
-
return [
|
|
3685
|
-
`slack socket mode reports ${activeConnections} active connections for this Slack app`,
|
|
3686
|
-
"Slack may deliver each event to any one connection",
|
|
3687
|
-
"ensure every OpenClaw gateway sharing this app has equivalent routing and authorization, or use a separate Slack app per gateway, one relay ingress, or HTTP Request URLs behind a load balancer",
|
|
3688
|
-
`See ${SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL}`
|
|
3689
|
-
].join("; ");
|
|
3690
|
-
}
|
|
3691
|
-
function registerSlackSocketModeConnectionDiagnostics(params) {
|
|
3692
|
-
const emitter = getSocketEmitter(params.app);
|
|
3693
|
-
if (!emitter) return () => {};
|
|
3694
|
-
let hasWarned = false;
|
|
3695
|
-
const listener = (message, isBinary) => {
|
|
3696
|
-
if (isBinary === true || hasWarned) return;
|
|
3697
|
-
const activeConnections = resolveSlackSocketModeConnectionCount(message);
|
|
3698
|
-
if (activeConnections === void 0 || activeConnections <= 1) return;
|
|
3699
|
-
hasWarned = true;
|
|
3700
|
-
params.onSharedConnection(activeConnections);
|
|
3701
|
-
};
|
|
3702
|
-
emitter.on("ws_message", listener);
|
|
3703
|
-
return () => {
|
|
3704
|
-
emitter.off("ws_message", listener);
|
|
3705
|
-
};
|
|
3706
|
-
}
|
|
3707
|
-
function waitForSlackSocketDisconnect(app, abortSignal) {
|
|
3708
|
-
return new Promise((resolve) => {
|
|
3709
|
-
const emitter = getSocketEmitter(app);
|
|
3710
|
-
if (!emitter) {
|
|
3711
|
-
abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), { once: true });
|
|
3712
|
-
return;
|
|
3713
|
-
}
|
|
3714
|
-
const disconnectListener = () => resolveOnce({ event: "disconnect" });
|
|
3715
|
-
const startFailListener = (error) => resolveOnce({
|
|
3716
|
-
event: "unable_to_socket_mode_start",
|
|
3717
|
-
error
|
|
3718
|
-
});
|
|
3719
|
-
const errorListener = (error) => resolveOnce({
|
|
3720
|
-
event: "error",
|
|
3721
|
-
error
|
|
3722
|
-
});
|
|
3723
|
-
const abortListener = () => resolveOnce({ event: "disconnect" });
|
|
3724
|
-
const cleanup = () => {
|
|
3725
|
-
emitter.off("disconnected", disconnectListener);
|
|
3726
|
-
emitter.off("unable_to_socket_mode_start", startFailListener);
|
|
3727
|
-
emitter.off("error", errorListener);
|
|
3728
|
-
abortSignal?.removeEventListener("abort", abortListener);
|
|
3729
|
-
};
|
|
3730
|
-
const resolveOnce = (value) => {
|
|
3731
|
-
cleanup();
|
|
3732
|
-
resolve(value);
|
|
3733
|
-
};
|
|
3734
|
-
emitter.on("disconnected", disconnectListener);
|
|
3735
|
-
emitter.on("unable_to_socket_mode_start", startFailListener);
|
|
3736
|
-
emitter.on("error", errorListener);
|
|
3737
|
-
abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
3738
|
-
});
|
|
3739
|
-
}
|
|
3740
|
-
/**
|
|
3741
|
-
* Detect permanent Slack account and credential failures.
|
|
3742
|
-
* Transient request and HTTP failures stay in OpenClaw's reconnect loop.
|
|
3743
|
-
*/
|
|
3744
|
-
function isNonRecoverableSlackAuthError(error) {
|
|
3745
|
-
return SLACK_AUTH_ERROR_RE.test(formatUnknownError(error, ""));
|
|
3746
|
-
}
|
|
3747
|
-
function formatUnknownError(error, fallback = NO_ERROR_DETAIL) {
|
|
3748
|
-
return formatSlackError(error, fallback);
|
|
3749
|
-
}
|
|
3750
|
-
//#endregion
|
|
3751
3985
|
//#region extensions/slack/src/monitor/provider-support.ts
|
|
3752
3986
|
const OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS = 15e3;
|
|
3753
3987
|
const OPENCLAW_SLACK_SOCKET_START_FAILED_EVENT = "unable_to_socket_mode_start";
|
|
@@ -3912,7 +4146,8 @@ function createSlackBoltApp(params) {
|
|
|
3912
4146
|
autoReconnectEnabled: true,
|
|
3913
4147
|
clientPingTimeout: params.socketMode?.clientPingTimeout ?? OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS,
|
|
3914
4148
|
logger: socketModeLogger,
|
|
3915
|
-
installerOptions: { clientOptions: params.clientOptions }
|
|
4149
|
+
installerOptions: { clientOptions: params.clientOptions },
|
|
4150
|
+
...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
|
|
3916
4151
|
};
|
|
3917
4152
|
if (params.socketMode?.serverPingTimeout !== void 0) socketModeReceiverOptions.serverPingTimeout = params.socketMode.serverPingTimeout;
|
|
3918
4153
|
if (params.socketMode?.pingPongLoggingEnabled !== void 0) socketModeReceiverOptions.pingPongLoggingEnabled = params.socketMode.pingPongLoggingEnabled;
|
|
@@ -3922,15 +4157,17 @@ function createSlackBoltApp(params) {
|
|
|
3922
4157
|
installSlackNativeReconnectFailureObserver(receiver);
|
|
3923
4158
|
} else if (params.slackMode === "http") receiver = new params.interop.HTTPReceiver({
|
|
3924
4159
|
signingSecret: params.signingSecret ?? "",
|
|
3925
|
-
endpoints: params.slackWebhookPath
|
|
4160
|
+
endpoints: params.slackWebhookPath,
|
|
4161
|
+
...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
|
|
3926
4162
|
});
|
|
3927
4163
|
else receiver = createSlackRelayReceiver();
|
|
4164
|
+
const appReceiver = receiver && params.wrapReceiver ? params.wrapReceiver(receiver) : receiver;
|
|
3928
4165
|
const app = new params.interop.App({
|
|
3929
|
-
token: params.
|
|
4166
|
+
token: params.token,
|
|
3930
4167
|
clientOptions: params.clientOptions,
|
|
3931
4168
|
ignoreSelf: false,
|
|
3932
4169
|
tokenVerificationEnabled: false,
|
|
3933
|
-
...
|
|
4170
|
+
...appReceiver ? { receiver: appReceiver } : {}
|
|
3934
4171
|
});
|
|
3935
4172
|
app.use(async (args) => {
|
|
3936
4173
|
if (shouldSkipOpenClawSlackSelfEvent(args)) return;
|
|
@@ -4178,7 +4415,7 @@ const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
|
|
|
4178
4415
|
const SLACK_HEADER_TEXT_MAX = 150;
|
|
4179
4416
|
const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
|
|
4180
4417
|
const loadSlashCommandsRuntime = createLazyRuntimeModule(() => import("./slash-commands.runtime-BTTMlmwy.js"));
|
|
4181
|
-
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-
|
|
4418
|
+
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-BT-mMm-f.js"));
|
|
4182
4419
|
const loadSlackPluginCommandsRuntime = createLazyRuntimeModule(() => import("./slash-plugin-commands.runtime-Dosfcx3X.js"));
|
|
4183
4420
|
const loadSlashSkillCommandsRuntime = createLazyRuntimeModule(() => import("./slash-skill-commands.runtime-CD1J2ODY.js"));
|
|
4184
4421
|
function resolveSlackCommandMenuModelContext(params) {
|
|
@@ -4918,7 +5155,7 @@ async function getSlackBoltInterop() {
|
|
|
4918
5155
|
}
|
|
4919
5156
|
return slackBoltInterop;
|
|
4920
5157
|
}
|
|
4921
|
-
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-
|
|
5158
|
+
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-Ea_P9VcO.js"));
|
|
4922
5159
|
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
4923
5160
|
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
|
|
4924
5161
|
function resolveStableSlackUserIdEntry(raw) {
|
|
@@ -5011,16 +5248,26 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5011
5248
|
path: `channels.slack.accounts.${account.accountId}.signingSecret`
|
|
5012
5249
|
});
|
|
5013
5250
|
const botToken = resolveSlackBotToken(opts.botToken ?? account.botToken);
|
|
5251
|
+
const userToken = account.userToken;
|
|
5014
5252
|
const appToken = resolveSlackAppToken(opts.appToken ?? account.appToken);
|
|
5015
5253
|
const relayConfig = slackMode === "relay" ? resolveSlackRelayConfig({
|
|
5016
5254
|
relay: account.config.relay,
|
|
5017
5255
|
accountId: account.accountId
|
|
5018
5256
|
}) : void 0;
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
throw new Error(missing);
|
|
5257
|
+
let token;
|
|
5258
|
+
if (account.identity === "user") {
|
|
5259
|
+
if (!userToken) throw new Error(`Slack user token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.userToken or SLACK_USER_TOKEN for default).`);
|
|
5260
|
+
if (slackMode === "socket" && !appToken) throw new Error(`Slack app token missing for user-identity socket mode account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.appToken or SLACK_APP_TOKEN for default).`);
|
|
5261
|
+
if (slackMode === "http" && !signingSecret) throw new Error(`Slack signing secret missing for user-identity HTTP mode account "${account.accountId}" (set channels.slack.signingSecret or channels.slack.accounts.${account.accountId}.signingSecret).`);
|
|
5262
|
+
token = userToken;
|
|
5263
|
+
} else {
|
|
5264
|
+
if (!botToken || slackMode === "socket" && !appToken) {
|
|
5265
|
+
const missing = slackMode === "http" ? `Slack bot token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken or SLACK_BOT_TOKEN for default).` : slackMode === "relay" ? `Slack bot token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken or SLACK_BOT_TOKEN for default).` : `Slack bot + app tokens missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken/appToken or SLACK_BOT_TOKEN/SLACK_APP_TOKEN for default).`;
|
|
5266
|
+
throw new Error(missing);
|
|
5267
|
+
}
|
|
5268
|
+
if (slackMode === "http" && !signingSecret) throw new Error(`Slack signing secret missing for account "${account.accountId}" (set channels.slack.signingSecret or channels.slack.accounts.${account.accountId}.signingSecret).`);
|
|
5269
|
+
token = botToken;
|
|
5022
5270
|
}
|
|
5023
|
-
if (slackMode === "http" && !signingSecret) throw new Error(`Slack signing secret missing for account "${account.accountId}" (set channels.slack.signingSecret or channels.slack.accounts.${account.accountId}.signingSecret).`);
|
|
5024
5271
|
const slackCfg = account.config;
|
|
5025
5272
|
const dmConfig = slackCfg.dm;
|
|
5026
5273
|
const dmEnabled = dmConfig?.enabled ?? true;
|
|
@@ -5060,7 +5307,6 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5060
5307
|
const replyToMode = slackCfg.replyToMode ?? "off";
|
|
5061
5308
|
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
|
5062
5309
|
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
|
5063
|
-
const threadRequireExplicitMention = slackCfg.thread?.requireExplicitMention ?? false;
|
|
5064
5310
|
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
|
5065
5311
|
const allowNameMatching = isDangerousNameMatchingEnabled(slackCfg);
|
|
5066
5312
|
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, { fallbackLimit: SLACK_TEXT_LIMIT });
|
|
@@ -5069,15 +5315,21 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5069
5315
|
const mediaMaxBytes = (opts.mediaMaxMb ?? slackCfg.mediaMaxMb ?? 20) * 1024 * 1024;
|
|
5070
5316
|
const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
|
|
5071
5317
|
const clientOptions = resolveSlackWebClientOptions();
|
|
5318
|
+
const durableIngress = createSlackDurableIngress({
|
|
5319
|
+
accountId: account.accountId,
|
|
5320
|
+
...runtime.log ? { onLog: runtime.log } : {},
|
|
5321
|
+
...opts.abortSignal ? { abortSignal: opts.abortSignal } : {}
|
|
5322
|
+
});
|
|
5072
5323
|
const { app, receiver, socketModeLogger } = createSlackBoltApp({
|
|
5073
5324
|
interop: await getSlackBoltInterop(),
|
|
5074
5325
|
slackMode,
|
|
5075
|
-
|
|
5326
|
+
token,
|
|
5076
5327
|
appToken: slackMode === "socket" ? appToken ?? void 0 : void 0,
|
|
5077
5328
|
signingSecret: slackMode === "http" ? signingSecret ?? void 0 : void 0,
|
|
5078
5329
|
slackWebhookPath,
|
|
5079
5330
|
clientOptions,
|
|
5080
|
-
...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {}
|
|
5331
|
+
...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {},
|
|
5332
|
+
wrapReceiver: durableIngress.wrapReceiver
|
|
5081
5333
|
});
|
|
5082
5334
|
const gracefulStop = async () => {
|
|
5083
5335
|
await gracefulStopSlackApp(app);
|
|
@@ -5112,12 +5364,12 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5112
5364
|
let authIdentityWarning;
|
|
5113
5365
|
let authTestIdentity;
|
|
5114
5366
|
try {
|
|
5115
|
-
const auth = await createSlackStartupAuthClient(
|
|
5367
|
+
const auth = await createSlackStartupAuthClient(token, clientOptions).auth.test();
|
|
5116
5368
|
const authUserId = normalizeOptionalString(auth.user_id) ?? "";
|
|
5117
5369
|
botId = normalizeOptionalString(auth.bot_id) ?? "";
|
|
5118
|
-
botUserId = botId ? authUserId : "";
|
|
5370
|
+
botUserId = account.identity === "user" ? authUserId : botId ? authUserId : "";
|
|
5119
5371
|
authTestIdentity = auth;
|
|
5120
|
-
authIdentityWarning = formatSlackBotTokenIdentityWarning({
|
|
5372
|
+
if (account.identity === "bot") authIdentityWarning = formatSlackBotTokenIdentityWarning({
|
|
5121
5373
|
auth,
|
|
5122
5374
|
accountId: account.accountId
|
|
5123
5375
|
});
|
|
@@ -5133,7 +5385,10 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5133
5385
|
});
|
|
5134
5386
|
const teamId = installationIdentity.kind === "workspace" ? installationIdentity.teamId : "";
|
|
5135
5387
|
const apiAppId = installationIdentity.kind === "degraded" ? "" : installationIdentity.apiAppId ?? "";
|
|
5136
|
-
if (authTestError !== void 0)
|
|
5388
|
+
if (authTestError !== void 0) {
|
|
5389
|
+
const identityFailureDetail = account.identity === "user" ? "explicit self-mention detection will be disabled until restart with a valid user token" : "explicit bot-mention detection will be disabled until restart with a valid bot token";
|
|
5390
|
+
runtime.log?.(warn(`[${account.accountId}] slack auth.test failed at boot (${authTestError}); ${identityFailureDetail}; required-mention channels will fail closed without another trusted activation signal`));
|
|
5391
|
+
}
|
|
5137
5392
|
if (authIdentityWarning) runtime.log?.(warn(authIdentityWarning));
|
|
5138
5393
|
const identityHealth = resolveSlackIdentityHealth({
|
|
5139
5394
|
installationIdentity,
|
|
@@ -5141,11 +5396,14 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5141
5396
|
authTestError,
|
|
5142
5397
|
authIdentityWarning
|
|
5143
5398
|
});
|
|
5144
|
-
if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken)
|
|
5399
|
+
if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) {
|
|
5400
|
+
const identityTokenLabel = account.identity === "user" ? "user token" : "bot token";
|
|
5401
|
+
runtime.error?.(`slack token mismatch: ${identityTokenLabel} app_id=${apiAppId} but app token looks like app_id=${expectedApiAppIdFromAppToken}`);
|
|
5402
|
+
}
|
|
5145
5403
|
const ctx = createSlackMonitorContext({
|
|
5146
5404
|
cfg,
|
|
5147
5405
|
accountId: account.accountId,
|
|
5148
|
-
botToken,
|
|
5406
|
+
botToken: token,
|
|
5149
5407
|
app,
|
|
5150
5408
|
runtime,
|
|
5151
5409
|
channelRuntime: opts.channelRuntime,
|
|
@@ -5173,7 +5431,6 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5173
5431
|
replyToMode,
|
|
5174
5432
|
threadHistoryScope,
|
|
5175
5433
|
threadInheritParent,
|
|
5176
|
-
threadRequireExplicitMention,
|
|
5177
5434
|
slashCommand,
|
|
5178
5435
|
textLimit,
|
|
5179
5436
|
ackReactionScope,
|
|
@@ -5232,6 +5489,7 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5232
5489
|
appHomeSlashCommandName: commandRegistration.mode === "single" ? commandRegistration.name : void 0,
|
|
5233
5490
|
trackEvent
|
|
5234
5491
|
});
|
|
5492
|
+
durableIngress.start();
|
|
5235
5493
|
presenceMonitor?.start();
|
|
5236
5494
|
if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
|
|
5237
5495
|
path: slackWebhookPath,
|
|
@@ -5397,9 +5655,17 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5397
5655
|
}
|
|
5398
5656
|
} else if (slackMode === "relay" && relayConfig) {
|
|
5399
5657
|
runtime.log?.(`slack relay mode connecting to ${relayConfig.url} gateway_id:${relayConfig.gatewayId}`);
|
|
5658
|
+
durableIngress.attachRelayDispatch(async (message, turnAdoptionLifecycle) => {
|
|
5659
|
+
await handleSlackMessage(message, {
|
|
5660
|
+
source: "message",
|
|
5661
|
+
wasMentioned: true,
|
|
5662
|
+
awaitDispatch: true,
|
|
5663
|
+
turnAdoptionLifecycle
|
|
5664
|
+
});
|
|
5665
|
+
});
|
|
5400
5666
|
await (await loadSlackRelaySource()).monitorSlackRelaySource({
|
|
5401
5667
|
config: relayConfig,
|
|
5402
|
-
|
|
5668
|
+
acceptRelayEvent: durableIngress.acceptRelayEvent,
|
|
5403
5669
|
runtime,
|
|
5404
5670
|
abortSignal: opts.abortSignal,
|
|
5405
5671
|
setStatus: opts.setStatus,
|
|
@@ -5417,9 +5683,10 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5417
5683
|
opts.abortSignal?.removeEventListener("abort", stopOnAbort);
|
|
5418
5684
|
unregisterSocketModeConnectionDiagnostics();
|
|
5419
5685
|
unregisterHttpHandler?.();
|
|
5686
|
+
await durableIngress.stop();
|
|
5420
5687
|
await gracefulStop();
|
|
5421
5688
|
}
|
|
5422
5689
|
}
|
|
5423
5690
|
const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy$1;
|
|
5424
5691
|
//#endregion
|
|
5425
|
-
export {
|
|
5692
|
+
export { stripSlackMentionsForCommandDetection as C, buildSlackSlashCommandMatcher as S, readSessionUpdatedAt as _, createSlackResponseUrlBudget as a, resolveStorePath$1 as b, formatUnknownError as c, resolveSlackCommandIngress as d, resolveSlackEffectiveAllowFrom as f, resolveSlackChatType as g, normalizeSlackChannelType as h, SlackResponseAlreadyReportedError as i, resolveConversationLabel$1 as l, parseSlackAssistantThreadMetadata as m, resolveSlackRuntimeGroupPolicy as n, authorizeSlackDirectMessage as o, buildSlackAssistantThreadMetadata as p, resolveSlackRoomContextHints as r, SLACK_SOCKET_RECONNECT_POLICY as s, monitorSlackProvider as t, authorizeSlackBotRoomMessage as u, resolveChannelContextVisibilityMode as v, updateLastRoute as x, resolveChannelResetConfig as y };
|