@openclaw/slack 2026.7.2-beta.1 → 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-p6M2ULpv.js → action-runtime-DqkRwO3Z.js} +14 -17
- package/dist/action-runtime.runtime-DEWOcFM4.js +2 -0
- package/dist/{actions-DkTcWmpT.js → actions-CF0tUY5q.js} +5 -9
- package/dist/{actions.runtime-BSxpwgUc.js → actions.runtime-hCbx4RXg.js} +1 -1
- package/dist/api.js +13 -14
- package/dist/{approval-handler.runtime-UGbnz9OC.js → approval-handler.runtime-Em8I1-8-.js} +2 -2
- package/dist/{channel-B0_j6C6z.js → channel-4-TRi1D6.js} +75 -41
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel-type-BpeTezY4.js → channel-type-Cu1yjNg2.js} +3 -3
- package/dist/{channel.setup-BQFut21T.js → channel.setup-CgkADtZg.js} +6 -4
- package/dist/{config-schema-DYI01PV2.js → config-schema-XjJGfwQC.js} +18 -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-DX7yFsM-.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-C-b2ZnSl.js → group-policy-Bw-65m3V.js} +94 -135
- 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-BN_uuvrQ.js → monitor-CNFeF30h.js} +3 -3
- package/dist/{outbound-adapter-ECl0go7d.js → outbound-adapter-CVi2uok0.js} +49 -5
- package/dist/{pipeline.runtime-f0eJJoJL.js → pipeline.runtime-CTvokbty.js} +485 -251
- package/dist/{policy-f8b0edSi.js → policy-b_dOPDCq.js} +7 -7
- package/dist/probe-Borhn9PQ.js +340 -0
- package/dist/{provider-Ds-y-imP.js → provider-B-HVL5ZE.js} +831 -329
- package/dist/{relay-source-B-6LFUdz.js → relay-source-Ea_P9VcO.js} +6 -11
- package/dist/{replies-BMvf3ihj.js → replies-B4bOdrYb.js} +3 -3
- package/dist/{resolve-channels-DOMgNCWb.js → resolve-channels-KTc568SU.js} +1 -1
- package/dist/{resolve-users-aL6-Ssee.js → resolve-users-DeGJf1yp.js} +1 -1
- package/dist/{runtime-CauEvswP.js → runtime-JSVZSWAj.js} +2 -2
- package/dist/{runtime-api-CVb6CQjX.js → runtime-api-BFfNpzDe.js} +1 -1
- package/dist/runtime-api.js +12 -12
- package/dist/runtime-setter-api.js +1 -1
- package/dist/{scopes-Dw8wbU0j.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-NEfJ0GvM.js → send-KuTDky7i.js} +29 -12
- package/dist/send.runtime-CXyxws1D.js +2 -0
- package/dist/setup-core-CeiAVwQ3.js +502 -0
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{setup-surface-BN2WeG5c.js → setup-surface-DLA0vHXp.js} +23 -13
- package/dist/{shared-BLnB9R1T.js → shared-C4yVB0Q-.js} +64 -41
- package/dist/{slash-dispatch.runtime-C0Otuf_T.js → slash-dispatch.runtime-BT-mMm-f.js} +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +134 -10
- package/package.json +5 -7
- package/dist/accounts.runtime-KvftRRek.js +0 -2
- package/dist/action-runtime.runtime-DQstDkHG.js +0 -2
- package/dist/client-D2F6m8CP.js +0 -152
- package/dist/configured-state.js +0 -11
- package/dist/probe-CfALk0l4.js +0 -48
- package/dist/send.runtime-BkKpitPu.js +0 -2
- package/dist/setup-core-fGd6Gquy.js +0 -343
|
@@ -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 { o as
|
|
6
|
-
import {
|
|
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
|
/**
|
|
@@ -94,9 +95,26 @@ function resolveSlackChatType(channelType) {
|
|
|
94
95
|
return "channel";
|
|
95
96
|
}
|
|
96
97
|
//#endregion
|
|
98
|
+
//#region extensions/slack/src/monitor/lru-map-cache.ts
|
|
99
|
+
/** LRU-touch read: move hit to newest so pruneMapToMaxSize keeps active keys. */
|
|
100
|
+
function readLruMapEntry(cache, cacheKey) {
|
|
101
|
+
const cached = cache.get(cacheKey);
|
|
102
|
+
if (cached) {
|
|
103
|
+
cache.delete(cacheKey);
|
|
104
|
+
cache.set(cacheKey, cached);
|
|
105
|
+
}
|
|
106
|
+
return cached;
|
|
107
|
+
}
|
|
108
|
+
function writeLruMapEntry(cache, cacheKey, entry, maxEntries) {
|
|
109
|
+
cache.delete(cacheKey);
|
|
110
|
+
cache.set(cacheKey, entry);
|
|
111
|
+
pruneMapToMaxSize(cache, maxEntries);
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
97
114
|
//#region extensions/slack/src/monitor/context.ts
|
|
98
115
|
const SLACK_ASSISTANT_THREAD_CONTEXT_METADATA_EVENT = "assistant_thread_context";
|
|
99
116
|
const SLACK_CHANNEL_CACHE_MAX_ENTRIES = 1024;
|
|
117
|
+
const SLACK_USER_CACHE_MAX_ENTRIES = 2048;
|
|
100
118
|
const SLACK_CHANNEL_DENIAL_WARNING_TTL_MS = 5 * 6e4;
|
|
101
119
|
const SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES = 1024;
|
|
102
120
|
function buildSlackAssistantThreadMetadata(context) {
|
|
@@ -133,10 +151,6 @@ function createSlackMonitorContext(params) {
|
|
|
133
151
|
const logger = getChildLogger({ module: "slack-auto-reply" });
|
|
134
152
|
const channelCache = /* @__PURE__ */ new Map();
|
|
135
153
|
const userCache = /* @__PURE__ */ new Map();
|
|
136
|
-
const seenMessages = createDedupeCache({
|
|
137
|
-
ttlMs: 6e4,
|
|
138
|
-
maxSize: 500
|
|
139
|
-
});
|
|
140
154
|
const channelDenialWarnings = createDedupeCache({
|
|
141
155
|
ttlMs: SLACK_CHANNEL_DENIAL_WARNING_TTL_MS,
|
|
142
156
|
maxSize: SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES
|
|
@@ -150,46 +164,25 @@ function createSlackMonitorContext(params) {
|
|
|
150
164
|
const hasChannelAllowlistConfig = Object.keys(params.channelsConfig ?? {}).length > 0;
|
|
151
165
|
const channelsConfigKeys = Object.keys(params.channelsConfig ?? {});
|
|
152
166
|
const scopedKey = (key, eventScope) => eventScope ? `${params.accountId}:${eventScope.teamId}:${key}` : key;
|
|
153
|
-
const readCachedChannel = (cacheKey) => {
|
|
154
|
-
const cached = channelCache.get(cacheKey);
|
|
155
|
-
if (cached) {
|
|
156
|
-
channelCache.delete(cacheKey);
|
|
157
|
-
channelCache.set(cacheKey, cached);
|
|
158
|
-
}
|
|
159
|
-
return cached;
|
|
160
|
-
};
|
|
161
|
-
const writeCachedChannel = (cacheKey, entry) => {
|
|
162
|
-
channelCache.delete(cacheKey);
|
|
163
|
-
channelCache.set(cacheKey, entry);
|
|
164
|
-
pruneMapToMaxSize(channelCache, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
|
|
165
|
-
};
|
|
166
167
|
const rememberSlackChannelType = (channelId, channelType, eventScope) => {
|
|
167
168
|
const id = normalizeOptionalString(channelId);
|
|
168
169
|
const normalizedType = normalizeOptionalString(channelType)?.toLowerCase();
|
|
169
170
|
if (!id || normalizedType !== "im" && normalizedType !== "mpim" && normalizedType !== "channel" && normalizedType !== "group") return;
|
|
170
171
|
const cacheKey = scopedKey(id, eventScope);
|
|
171
|
-
const cached =
|
|
172
|
+
const cached = readLruMapEntry(channelCache, cacheKey);
|
|
172
173
|
const type = normalizeSlackChannelType(normalizedType, id);
|
|
173
174
|
if (cached?.info.type === type) return;
|
|
174
|
-
|
|
175
|
+
writeLruMapEntry(channelCache, cacheKey, {
|
|
175
176
|
info: {
|
|
176
177
|
...cached?.info,
|
|
177
178
|
type
|
|
178
179
|
},
|
|
179
180
|
metadataLoaded: cached?.metadataLoaded ?? false
|
|
180
|
-
});
|
|
181
|
+
}, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
|
|
181
182
|
};
|
|
182
183
|
const recallSlackChannelType = (channelId, eventScope) => {
|
|
183
184
|
const id = normalizeOptionalString(channelId);
|
|
184
|
-
return id ?
|
|
185
|
-
};
|
|
186
|
-
const markMessageSeen = (channelId, ts, eventScope) => {
|
|
187
|
-
if (!channelId || !ts) return false;
|
|
188
|
-
return seenMessages.check(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
189
|
-
};
|
|
190
|
-
const releaseSeenMessage = (channelId, ts, eventScope) => {
|
|
191
|
-
if (!channelId || !ts) return;
|
|
192
|
-
seenMessages.delete(scopedKey(`${channelId}:${ts}`, eventScope));
|
|
185
|
+
return id ? readLruMapEntry(channelCache, scopedKey(id, eventScope))?.info.type : void 0;
|
|
193
186
|
};
|
|
194
187
|
const assistantContextKey = (channelId, threadTs, eventScope) => scopedKey(`${channelId}:${threadTs}`, eventScope);
|
|
195
188
|
const cleanupAssistantThreadContexts = () => {
|
|
@@ -280,7 +273,7 @@ function createSlackMonitorContext(params) {
|
|
|
280
273
|
};
|
|
281
274
|
const resolveChannelName = async (channelId, eventScope) => {
|
|
282
275
|
const cacheKey = scopedKey(channelId, eventScope);
|
|
283
|
-
const cached =
|
|
276
|
+
const cached = readLruMapEntry(channelCache, cacheKey);
|
|
284
277
|
if (cached?.metadataLoaded) return cached.info;
|
|
285
278
|
try {
|
|
286
279
|
const info = await (eventScope?.client ?? params.app.client).conversations.info({
|
|
@@ -301,7 +294,7 @@ function createSlackMonitorContext(params) {
|
|
|
301
294
|
},
|
|
302
295
|
metadataLoaded: true
|
|
303
296
|
};
|
|
304
|
-
|
|
297
|
+
writeLruMapEntry(channelCache, cacheKey, entry, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
|
|
305
298
|
return entry.info;
|
|
306
299
|
} catch {
|
|
307
300
|
return cached?.info ?? {};
|
|
@@ -309,7 +302,7 @@ function createSlackMonitorContext(params) {
|
|
|
309
302
|
};
|
|
310
303
|
const resolveUserName = async (userId, eventScope) => {
|
|
311
304
|
const cacheKey = scopedKey(userId, eventScope);
|
|
312
|
-
const cached = userCache
|
|
305
|
+
const cached = readLruMapEntry(userCache, cacheKey);
|
|
313
306
|
if (cached) return cached;
|
|
314
307
|
try {
|
|
315
308
|
const info = await (eventScope?.client ?? params.app.client).users.info({
|
|
@@ -318,7 +311,7 @@ function createSlackMonitorContext(params) {
|
|
|
318
311
|
});
|
|
319
312
|
const profile = info.user?.profile;
|
|
320
313
|
const entry = { name: profile?.display_name || profile?.real_name || info.user?.name || void 0 };
|
|
321
|
-
userCache
|
|
314
|
+
writeLruMapEntry(userCache, cacheKey, entry, SLACK_USER_CACHE_MAX_ENTRIES);
|
|
322
315
|
return entry;
|
|
323
316
|
} catch {
|
|
324
317
|
return {};
|
|
@@ -468,7 +461,6 @@ function createSlackMonitorContext(params) {
|
|
|
468
461
|
replyToMode: params.replyToMode,
|
|
469
462
|
threadHistoryScope: params.threadHistoryScope,
|
|
470
463
|
threadInheritParent: params.threadInheritParent,
|
|
471
|
-
threadRequireExplicitMention: params.threadRequireExplicitMention,
|
|
472
464
|
slashCommand: params.slashCommand,
|
|
473
465
|
textLimit: params.textLimit,
|
|
474
466
|
ackReactionScope: params.ackReactionScope,
|
|
@@ -476,8 +468,6 @@ function createSlackMonitorContext(params) {
|
|
|
476
468
|
mediaMaxBytes: params.mediaMaxBytes,
|
|
477
469
|
removeAckAfterReply: params.removeAckAfterReply,
|
|
478
470
|
logger,
|
|
479
|
-
markMessageSeen,
|
|
480
|
-
releaseSeenMessage,
|
|
481
471
|
shouldDropMismatchedSlackEvent,
|
|
482
472
|
resolveSlackSystemEventSessionKey,
|
|
483
473
|
isChannelAllowed,
|
|
@@ -606,6 +596,20 @@ function resolveSlackInstallationIdentity(params) {
|
|
|
606
596
|
...enterpriseId ? { enterpriseId } : {}
|
|
607
597
|
};
|
|
608
598
|
}
|
|
599
|
+
function resolveSlackIdentityHealth(params) {
|
|
600
|
+
if (params.installationIdentity.kind === "enterprise") return {
|
|
601
|
+
healthState: "healthy",
|
|
602
|
+
lastError: null
|
|
603
|
+
};
|
|
604
|
+
const lastError = normalizeOptionalString(params.authTestError) ?? normalizeOptionalString(params.authIdentityWarning) ?? (params.installationIdentity.kind === "degraded" || !params.botUserId.trim() ? "slack bot identity unavailable" : void 0);
|
|
605
|
+
return lastError ? {
|
|
606
|
+
healthState: "degraded",
|
|
607
|
+
lastError
|
|
608
|
+
} : {
|
|
609
|
+
healthState: "healthy",
|
|
610
|
+
lastError: null
|
|
611
|
+
};
|
|
612
|
+
}
|
|
609
613
|
//#endregion
|
|
610
614
|
//#region extensions/slack/src/monitor/events/assistant.ts
|
|
611
615
|
const DEFAULT_ASSISTANT_PROMPTS = [
|
|
@@ -1909,6 +1913,26 @@ async function handleSlackBlockAction(params) {
|
|
|
1909
1913
|
});
|
|
1910
1914
|
return;
|
|
1911
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
|
+
}
|
|
1912
1936
|
const pluginInteractionData = buildSlackPluginInteractionData({
|
|
1913
1937
|
actionId: parsed.actionId,
|
|
1914
1938
|
summary: parsed.actionSummary
|
|
@@ -2631,6 +2655,350 @@ function resolveSlackEventScope(params) {
|
|
|
2631
2655
|
};
|
|
2632
2656
|
}
|
|
2633
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
|
|
2634
3002
|
//#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
|
|
2635
3003
|
const SUBTYPE_HANDLER_REGISTRY = {
|
|
2636
3004
|
message_changed: {
|
|
@@ -2758,6 +3126,7 @@ function registerSlackMessageEvents(params) {
|
|
|
2758
3126
|
return resolved.scope;
|
|
2759
3127
|
};
|
|
2760
3128
|
const handleIncomingMessageEvent = async ({ event, body, context, client }) => {
|
|
3129
|
+
const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
|
|
2761
3130
|
try {
|
|
2762
3131
|
const eventScope = resolveEventScope({
|
|
2763
3132
|
body,
|
|
@@ -2783,10 +3152,9 @@ function registerSlackMessageEvents(params) {
|
|
|
2783
3152
|
if (assistantChangedInbound) {
|
|
2784
3153
|
await handleSlackMessage(assistantChangedInbound, {
|
|
2785
3154
|
source: "message",
|
|
2786
|
-
...eventScope ? {
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
} : {}
|
|
3155
|
+
...eventScope ? { eventScope } : {},
|
|
3156
|
+
...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
|
|
3157
|
+
...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
|
|
2790
3158
|
});
|
|
2791
3159
|
return;
|
|
2792
3160
|
}
|
|
@@ -2814,12 +3182,12 @@ function registerSlackMessageEvents(params) {
|
|
|
2814
3182
|
}
|
|
2815
3183
|
await handleSlackMessage(message, {
|
|
2816
3184
|
source: "message",
|
|
2817
|
-
...eventScope ? {
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
} : {}
|
|
3185
|
+
...eventScope ? { eventScope } : {},
|
|
3186
|
+
...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
|
|
3187
|
+
...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
|
|
2821
3188
|
});
|
|
2822
3189
|
} catch (err) {
|
|
3190
|
+
if (turnAdoptionLifecycle) throw err;
|
|
2823
3191
|
ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
|
|
2824
3192
|
}
|
|
2825
3193
|
};
|
|
@@ -2828,6 +3196,7 @@ function registerSlackMessageEvents(params) {
|
|
|
2828
3196
|
});
|
|
2829
3197
|
ctx.app.event("app_mention", async (args) => {
|
|
2830
3198
|
const { event, body, context, client } = args;
|
|
3199
|
+
const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
|
|
2831
3200
|
try {
|
|
2832
3201
|
const eventScope = resolveEventScope({
|
|
2833
3202
|
body,
|
|
@@ -2854,12 +3223,12 @@ function registerSlackMessageEvents(params) {
|
|
|
2854
3223
|
await handleSlackMessage(mention, {
|
|
2855
3224
|
source: "app_mention",
|
|
2856
3225
|
wasMentioned: true,
|
|
2857
|
-
...eventScope ? {
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
} : {}
|
|
3226
|
+
...eventScope ? { eventScope } : {},
|
|
3227
|
+
...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
|
|
3228
|
+
...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
|
|
2861
3229
|
});
|
|
2862
3230
|
} catch (err) {
|
|
3231
|
+
if (turnAdoptionLifecycle) throw err;
|
|
2863
3232
|
ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
|
|
2864
3233
|
}
|
|
2865
3234
|
});
|
|
@@ -3018,41 +3387,58 @@ function registerSlackMonitorEvents(params) {
|
|
|
3018
3387
|
});
|
|
3019
3388
|
}
|
|
3020
3389
|
//#endregion
|
|
3021
|
-
//#region extensions/slack/src/monitor/
|
|
3022
|
-
const
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
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" };
|
|
3037
3440
|
}
|
|
3038
3441
|
}
|
|
3039
|
-
});
|
|
3040
|
-
function makeKey(accountId, channelId, ts, teamId) {
|
|
3041
|
-
return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
3042
|
-
}
|
|
3043
|
-
async function hasSlackInboundMessageDelivery(params) {
|
|
3044
|
-
if (!params.accountId || !params.channelId || !params.ts) return false;
|
|
3045
|
-
return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts, params.teamId));
|
|
3046
|
-
}
|
|
3047
|
-
async function recordSlackInboundMessageDeliveries(params) {
|
|
3048
|
-
if (!params.accountId || params.messages.length === 0) return;
|
|
3049
|
-
const deliveredAt = Date.now();
|
|
3050
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3051
|
-
for (const message of params.messages) {
|
|
3052
|
-
if (!message.channel || !message.ts) continue;
|
|
3053
|
-
keys.add(makeKey(params.accountId, message.channel, message.ts, params.teamId));
|
|
3054
|
-
}
|
|
3055
|
-
await Promise.all(Array.from(keys, (key) => deliveredMessages.register(key, { deliveredAt }, { at: deliveredAt })));
|
|
3056
3442
|
}
|
|
3057
3443
|
//#endregion
|
|
3058
3444
|
//#region extensions/slack/src/monitor/message-handler/debounce-key.ts
|
|
@@ -3180,7 +3566,7 @@ function createSlackThreadTsResolver(params) {
|
|
|
3180
3566
|
}
|
|
3181
3567
|
//#endregion
|
|
3182
3568
|
//#region extensions/slack/src/monitor/message-handler.ts
|
|
3183
|
-
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-
|
|
3569
|
+
const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-CTvokbty.js"));
|
|
3184
3570
|
function createSlackDispatchCompletion() {
|
|
3185
3571
|
let resolve;
|
|
3186
3572
|
let reject;
|
|
@@ -3193,7 +3579,6 @@ function createSlackDispatchCompletion() {
|
|
|
3193
3579
|
reject
|
|
3194
3580
|
};
|
|
3195
3581
|
}
|
|
3196
|
-
const APP_MENTION_RETRY_TTL_MS = 6e4;
|
|
3197
3582
|
const RETRYABLE_FLUSH_MAX_ATTEMPTS = 3;
|
|
3198
3583
|
const RETRYABLE_FLUSH_RETRY_DELAY_MS = 1e3;
|
|
3199
3584
|
const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
|
|
@@ -3207,12 +3592,9 @@ function shouldDebounceSlackMessage(message, cfg) {
|
|
|
3207
3592
|
hasMedia: Boolean(message.files && message.files.length > 0)
|
|
3208
3593
|
});
|
|
3209
3594
|
}
|
|
3210
|
-
function buildSeenMessageKey(channelId, ts, teamId) {
|
|
3211
|
-
if (!channelId || !ts) return null;
|
|
3212
|
-
return `${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
|
|
3213
|
-
}
|
|
3214
3595
|
function createSlackMessageHandler(params) {
|
|
3215
|
-
const { ctx, account, trackEvent } = params;
|
|
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)}`) });
|
|
3216
3598
|
const { debounceMs, debouncer } = createChannelInboundDebouncer({
|
|
3217
3599
|
cfg: ctx.cfg,
|
|
3218
3600
|
channel: "slack",
|
|
@@ -3245,8 +3627,41 @@ function createSlackMessageHandler(params) {
|
|
|
3245
3627
|
const completions = entries.map((entry) => entry.opts.dispatchCompletion).filter((completion) => completion !== void 0);
|
|
3246
3628
|
try {
|
|
3247
3629
|
await (async () => {
|
|
3248
|
-
const
|
|
3249
|
-
|
|
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
|
+
}
|
|
3250
3665
|
const teamId = last.opts.eventScope?.teamId;
|
|
3251
3666
|
const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
|
|
3252
3667
|
const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId, teamId);
|
|
@@ -3257,75 +3672,60 @@ function createSlackMessageHandler(params) {
|
|
|
3257
3672
|
if (pendingKeys.size === 0) pendingTopLevelDebounceKeys.delete(topLevelConversationKey);
|
|
3258
3673
|
}
|
|
3259
3674
|
}
|
|
3260
|
-
const combinedText =
|
|
3261
|
-
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));
|
|
3262
3677
|
const syntheticMessage = {
|
|
3263
3678
|
...last.message,
|
|
3264
3679
|
text: combinedText
|
|
3265
3680
|
};
|
|
3266
|
-
const
|
|
3681
|
+
const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
|
|
3682
|
+
const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, turnAdoptionLifecycle, ...lastOpts } = last.opts;
|
|
3683
|
+
let prepared;
|
|
3684
|
+
let settlementHandedOff = false;
|
|
3267
3685
|
try {
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
ctx,
|
|
3276
|
-
account,
|
|
3277
|
-
message: syntheticMessage,
|
|
3278
|
-
opts: {
|
|
3279
|
-
...lastOpts,
|
|
3280
|
-
wasMentioned: combinedMentioned || last.opts.wasMentioned,
|
|
3281
|
-
...seenMessageKey && lastOpts.source === "message" ? { shouldRecordDroppedHistory: () => !appMentionPreparingKeys.has(seenMessageKey) && !appMentionDispatchedKeys.has(seenMessageKey) } : {}
|
|
3282
|
-
}
|
|
3283
|
-
});
|
|
3284
|
-
if (result && seenMessageKey) {
|
|
3285
|
-
pruneAppMentionRetryKeys(Date.now());
|
|
3286
|
-
if (last.opts.source === "app_mention") rememberExpiringAppMentionKey(appMentionDispatchedKeys, seenMessageKey);
|
|
3287
|
-
else if (last.opts.source === "message" && appMentionDispatchedKeys.has(seenMessageKey)) {
|
|
3288
|
-
appMentionDispatchedKeys.delete(seenMessageKey);
|
|
3289
|
-
appMentionRetryKeys.delete(seenMessageKey);
|
|
3290
|
-
return null;
|
|
3291
|
-
}
|
|
3292
|
-
appMentionRetryKeys.delete(seenMessageKey);
|
|
3293
|
-
}
|
|
3294
|
-
return result;
|
|
3295
|
-
} finally {
|
|
3296
|
-
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
|
|
3297
3693
|
}
|
|
3298
|
-
})
|
|
3299
|
-
if (!prepared)
|
|
3300
|
-
|
|
3301
|
-
|
|
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();
|
|
3713
|
+
}
|
|
3714
|
+
} : turnAdoptionLifecycle;
|
|
3715
|
+
onPrepared?.(prepared);
|
|
3716
|
+
if (surviving.length > 1) {
|
|
3717
|
+
const ids = surviving.map((entry) => entry.message.ts).filter(Boolean);
|
|
3302
3718
|
if (ids.length > 0) {
|
|
3303
3719
|
prepared.ctxPayload.MessageSids = ids;
|
|
3304
3720
|
prepared.ctxPayload.MessageSidFirst = ids[0];
|
|
3305
3721
|
prepared.ctxPayload.MessageSidLast = ids[ids.length - 1];
|
|
3306
3722
|
}
|
|
3307
3723
|
}
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
accountId: ctx.accountId,
|
|
3312
|
-
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
3313
|
-
messages: entries.map((entry) => entry.message)
|
|
3314
|
-
});
|
|
3315
|
-
} catch (error) {
|
|
3316
|
-
if (!isRetryableSlackInboundError(error)) await recordSlackInboundMessageDeliveries({
|
|
3317
|
-
accountId: ctx.accountId,
|
|
3318
|
-
...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
|
|
3319
|
-
messages: entries.map((entry) => entry.message)
|
|
3320
|
-
});
|
|
3321
|
-
throw error;
|
|
3322
|
-
}
|
|
3724
|
+
await dispatchPreparedSlackMessage(prepared);
|
|
3725
|
+
if (!turnAdoptionLifecycle) await commitClaims();
|
|
3726
|
+
else if (!settlementHandedOff) releaseClaims();
|
|
3323
3727
|
} catch (error) {
|
|
3324
|
-
|
|
3325
|
-
const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts, entry.opts.eventScope?.teamId);
|
|
3326
|
-
if (entrySeenKey) appMentionDispatchedKeys.delete(entrySeenKey);
|
|
3327
|
-
ctx.releaseSeenMessage(entry.message.channel, entry.message.ts, entry.opts.eventScope);
|
|
3328
|
-
}
|
|
3728
|
+
releaseClaims(error);
|
|
3329
3729
|
throw error;
|
|
3330
3730
|
}
|
|
3331
3731
|
})();
|
|
@@ -3342,51 +3742,10 @@ function createSlackMessageHandler(params) {
|
|
|
3342
3742
|
});
|
|
3343
3743
|
const threadTsResolver = createSlackThreadTsResolver({ client: ctx.app.client });
|
|
3344
3744
|
const pendingTopLevelDebounceKeys = /* @__PURE__ */ new Map();
|
|
3345
|
-
const appMentionRetryKeys = /* @__PURE__ */ new Map();
|
|
3346
|
-
const appMentionPreparingKeys = /* @__PURE__ */ new Set();
|
|
3347
|
-
const appMentionDispatchedKeys = /* @__PURE__ */ new Map();
|
|
3348
|
-
const pruneAppMentionRetryKeys = (rawNow) => {
|
|
3349
|
-
const now = asDateTimestampMs(rawNow);
|
|
3350
|
-
if (now === void 0) {
|
|
3351
|
-
appMentionRetryKeys.clear();
|
|
3352
|
-
appMentionDispatchedKeys.clear();
|
|
3353
|
-
return false;
|
|
3354
|
-
}
|
|
3355
|
-
for (const [key, expiresAt] of appMentionRetryKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionRetryKeys.delete(key);
|
|
3356
|
-
for (const [key, expiresAt] of appMentionDispatchedKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionDispatchedKeys.delete(key);
|
|
3357
|
-
return true;
|
|
3358
|
-
};
|
|
3359
|
-
const rememberExpiringAppMentionKey = (map, key) => {
|
|
3360
|
-
const now = Date.now();
|
|
3361
|
-
if (!pruneAppMentionRetryKeys(now)) return;
|
|
3362
|
-
const expiresAt = resolveExpiresAtMsFromDurationMs(APP_MENTION_RETRY_TTL_MS, { nowMs: now });
|
|
3363
|
-
if (expiresAt !== void 0) map.set(key, expiresAt);
|
|
3364
|
-
};
|
|
3365
|
-
const rememberAppMentionRetryKey = (key) => {
|
|
3366
|
-
rememberExpiringAppMentionKey(appMentionRetryKeys, key);
|
|
3367
|
-
};
|
|
3368
|
-
const consumeAppMentionRetryKey = (key) => {
|
|
3369
|
-
if (!pruneAppMentionRetryKeys(Date.now())) return false;
|
|
3370
|
-
if (!appMentionRetryKeys.has(key)) return false;
|
|
3371
|
-
appMentionRetryKeys.delete(key);
|
|
3372
|
-
return true;
|
|
3373
|
-
};
|
|
3374
3745
|
async function enqueueSlackMessage(message, opts) {
|
|
3375
3746
|
if (opts.source === "message" && message.type !== "message") return;
|
|
3376
3747
|
if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
|
|
3377
3748
|
ctx.rememberSlackChannelType(message.channel, message.channel_type, opts.eventScope);
|
|
3378
|
-
const seenMessageKey = buildSeenMessageKey(message.channel, message.ts, opts.eventScope?.teamId);
|
|
3379
|
-
if (seenMessageKey && await hasSlackInboundMessageDelivery({
|
|
3380
|
-
accountId: ctx.accountId,
|
|
3381
|
-
channelId: message.channel,
|
|
3382
|
-
ts: message.ts,
|
|
3383
|
-
...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
|
|
3384
|
-
})) return;
|
|
3385
|
-
const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts, opts.eventScope) : false;
|
|
3386
|
-
if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
|
|
3387
|
-
if (seenMessageKey && wasSeen) {
|
|
3388
|
-
if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
|
|
3389
|
-
}
|
|
3390
3749
|
trackEvent?.();
|
|
3391
3750
|
const resolvedMessage = await (opts.eventScope ? createSlackThreadTsResolver({ client: opts.eventScope.client }) : threadTsResolver).resolve({
|
|
3392
3751
|
message,
|
|
@@ -3426,110 +3785,201 @@ function createSlackMessageHandler(params) {
|
|
|
3426
3785
|
};
|
|
3427
3786
|
}
|
|
3428
3787
|
//#endregion
|
|
3429
|
-
//#region extensions/slack/src/monitor/
|
|
3430
|
-
const
|
|
3431
|
-
const
|
|
3432
|
-
const
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3788
|
+
//#region extensions/slack/src/monitor/presence-monitor.ts
|
|
3789
|
+
const SLACK_PRESENCE_GREETING_COOLDOWN_MS = 480 * 60 * 1e3;
|
|
3790
|
+
const SLACK_PRESENCE_POLL_INTERVAL_MS = 6e4;
|
|
3791
|
+
const SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS = 8;
|
|
3792
|
+
const SLACK_PRESENCE_TARGET_TTL_MS = 1440 * 60 * 1e3;
|
|
3793
|
+
const SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL = 45;
|
|
3794
|
+
const SLACK_PRESENCE_MAX_TARGETS = 2e3;
|
|
3795
|
+
function resolveMode(channelConfig, accountConfig) {
|
|
3796
|
+
return channelConfig?.mode ?? accountConfig?.mode ?? "off";
|
|
3797
|
+
}
|
|
3798
|
+
function hasSlackPresenceEventsEnabled(params) {
|
|
3799
|
+
if (resolveMode(void 0, params.account) !== "off") return true;
|
|
3800
|
+
return Object.values(params.channels ?? {}).some((entry) => resolveMode(entry?.presenceEvents, void 0) !== "off");
|
|
3801
|
+
}
|
|
3802
|
+
function isTargetEligible(target) {
|
|
3803
|
+
if (target.mode === "on") return true;
|
|
3804
|
+
if (target.autoEligibleKind === "channel") return false;
|
|
3805
|
+
return target.participants.size <= SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS;
|
|
3806
|
+
}
|
|
3807
|
+
function formatSlackPresenceEvent(target, userId) {
|
|
3808
|
+
return [
|
|
3809
|
+
"Slack presence event:",
|
|
3810
|
+
`A human participant became active on Slack after being observed away: user_id=${JSON.stringify(userId)} channel_id=${JSON.stringify(target.channelId)}${target.threadId ? ` thread_ts=${JSON.stringify(target.threadId)}` : ""}.`,
|
|
3811
|
+
"Before greeting, retrieve relevant memory and wiki context for this immutable user_id, including a known timezone when available. Use their local time; if their timezone is unknown, do not guess.",
|
|
3812
|
+
"Send at most one short, natural greeting in this Slack conversation. Do not reveal private memory. If no greeting is appropriate, stay silent."
|
|
3813
|
+
].join("\n");
|
|
3814
|
+
}
|
|
3815
|
+
function resolveObservedTarget(params) {
|
|
3816
|
+
const { prepared } = params;
|
|
3817
|
+
const userId = prepared.message.user?.trim();
|
|
3818
|
+
if (!userId || prepared.message.bot_id || prepared.message.subtype === "bot_message") return null;
|
|
3819
|
+
const mode = resolveMode(prepared.channelConfig?.presenceEvents, params.accountConfig);
|
|
3820
|
+
if (mode === "off") return null;
|
|
3821
|
+
const channelId = prepared.message.channel;
|
|
3822
|
+
const rawThreadId = prepared.ctxPayload.MessageThreadId ?? prepared.ctxPayload.TransportThreadId ?? void 0;
|
|
3823
|
+
const threadId = rawThreadId === void 0 ? void 0 : String(rawThreadId);
|
|
3824
|
+
const channelType = prepared.message.channel_type;
|
|
3825
|
+
const autoEligibleKind = prepared.isDirectMessage ? "direct" : channelType === "mpim" ? "group" : threadId ? "thread" : "channel";
|
|
3826
|
+
if (mode === "auto" && autoEligibleKind === "channel") return null;
|
|
3447
3827
|
return {
|
|
3448
|
-
|
|
3449
|
-
|
|
3828
|
+
key: `${channelId}${threadId ? `:thread:${threadId}` : ":top"}`,
|
|
3829
|
+
mode,
|
|
3830
|
+
channelId,
|
|
3831
|
+
threadId,
|
|
3832
|
+
to: prepared.isDirectMessage ? `user:${userId}` : `channel:${channelId}`,
|
|
3833
|
+
sessionKey: prepared.route.sessionKey,
|
|
3834
|
+
agentId: prepared.route.agentId,
|
|
3835
|
+
participants: /* @__PURE__ */ new Map([[userId, params.nowMs]]),
|
|
3836
|
+
lastActivityAtMs: params.nowMs,
|
|
3837
|
+
autoEligibleKind
|
|
3450
3838
|
};
|
|
3451
3839
|
}
|
|
3452
|
-
function
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3458
|
-
let
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
"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",
|
|
3472
|
-
`See ${SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL}`
|
|
3473
|
-
].join("; ");
|
|
3474
|
-
}
|
|
3475
|
-
function registerSlackSocketModeConnectionDiagnostics(params) {
|
|
3476
|
-
const emitter = getSocketEmitter(params.app);
|
|
3477
|
-
if (!emitter) return () => {};
|
|
3478
|
-
let hasWarned = false;
|
|
3479
|
-
const listener = (message, isBinary) => {
|
|
3480
|
-
if (isBinary === true || hasWarned) return;
|
|
3481
|
-
const activeConnections = resolveSlackSocketModeConnectionCount(message);
|
|
3482
|
-
if (activeConnections === void 0 || activeConnections <= 1) return;
|
|
3483
|
-
hasWarned = true;
|
|
3484
|
-
params.onSharedConnection(activeConnections);
|
|
3840
|
+
function createSlackPresenceMonitor(params) {
|
|
3841
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3842
|
+
const presenceByUser = /* @__PURE__ */ new Map();
|
|
3843
|
+
const nowMs = params.nowMs ?? Date.now;
|
|
3844
|
+
const enqueue = params.enqueue ?? enqueueSystemEvent;
|
|
3845
|
+
const wake = params.wake ?? requestHeartbeat;
|
|
3846
|
+
let pollOffset = 0;
|
|
3847
|
+
let timer;
|
|
3848
|
+
let activePoll;
|
|
3849
|
+
let stopped = false;
|
|
3850
|
+
const pruneTargets = (now) => {
|
|
3851
|
+
for (const [key, target] of targets) if (now - target.lastActivityAtMs >= SLACK_PRESENCE_TARGET_TTL_MS) targets.delete(key);
|
|
3852
|
+
while (targets.size > SLACK_PRESENCE_MAX_TARGETS) {
|
|
3853
|
+
const oldestKey = targets.keys().next().value;
|
|
3854
|
+
if (typeof oldestKey !== "string") break;
|
|
3855
|
+
targets.delete(oldestKey);
|
|
3856
|
+
}
|
|
3857
|
+
const eligibleUsers = new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())));
|
|
3858
|
+
for (const userId of presenceByUser.keys()) if (!eligibleUsers.has(userId)) presenceByUser.delete(userId);
|
|
3485
3859
|
};
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3860
|
+
const observe = (prepared) => {
|
|
3861
|
+
const now = nowMs();
|
|
3862
|
+
pruneTargets(now);
|
|
3863
|
+
const observed = resolveObservedTarget({
|
|
3864
|
+
prepared,
|
|
3865
|
+
accountConfig: params.accountConfig,
|
|
3866
|
+
nowMs: now
|
|
3867
|
+
});
|
|
3868
|
+
if (!observed) return;
|
|
3869
|
+
const current = targets.get(observed.key);
|
|
3870
|
+
if (current) {
|
|
3871
|
+
current.mode = observed.mode;
|
|
3872
|
+
current.sessionKey = observed.sessionKey;
|
|
3873
|
+
current.agentId = observed.agentId;
|
|
3874
|
+
current.to = observed.to;
|
|
3875
|
+
current.lastActivityAtMs = now;
|
|
3876
|
+
for (const [participant, observedAt] of observed.participants) current.participants.set(participant, observedAt);
|
|
3877
|
+
targets.delete(observed.key);
|
|
3878
|
+
targets.set(observed.key, current);
|
|
3879
|
+
} else targets.set(observed.key, observed);
|
|
3880
|
+
pruneTargets(now);
|
|
3489
3881
|
};
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
|
|
3882
|
+
const emitTransition = (userId, now) => {
|
|
3883
|
+
const target = Array.from(targets.values()).filter((candidate) => candidate.participants.has(userId) && isTargetEligible(candidate)).toSorted((a, b) => (b.participants.get(userId) ?? 0) - (a.participants.get(userId) ?? 0))[0];
|
|
3884
|
+
if (!target) return;
|
|
3885
|
+
const cooldownKey = `${params.accountId}:${userId}`;
|
|
3886
|
+
let reserved;
|
|
3887
|
+
try {
|
|
3888
|
+
reserved = params.cooldownStore.registerIfAbsent(cooldownKey, now, { ttlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS });
|
|
3889
|
+
} catch (err) {
|
|
3890
|
+
params.error?.(`slack presence cooldown persistence failed: ${String(err)}`);
|
|
3496
3891
|
return;
|
|
3497
3892
|
}
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3893
|
+
if (!reserved) return;
|
|
3894
|
+
if (!enqueue(formatSlackPresenceEvent(target, userId), {
|
|
3895
|
+
sessionKey: target.sessionKey,
|
|
3896
|
+
contextKey: `slack:presence-active:${params.accountId}:${userId}`,
|
|
3897
|
+
deliveryContext: {
|
|
3898
|
+
channel: "slack",
|
|
3899
|
+
to: target.to,
|
|
3900
|
+
accountId: params.accountId,
|
|
3901
|
+
threadId: target.threadId
|
|
3902
|
+
}
|
|
3903
|
+
})) {
|
|
3904
|
+
params.cooldownStore.delete(cooldownKey);
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
wake({
|
|
3908
|
+
source: "notifications-event",
|
|
3909
|
+
intent: "immediate",
|
|
3910
|
+
reason: "wake",
|
|
3911
|
+
agentId: target.agentId,
|
|
3912
|
+
sessionKey: target.sessionKey,
|
|
3913
|
+
heartbeat: {
|
|
3914
|
+
target: "slack",
|
|
3915
|
+
to: target.to,
|
|
3916
|
+
accountId: params.accountId
|
|
3917
|
+
}
|
|
3502
3918
|
});
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3919
|
+
};
|
|
3920
|
+
const performPoll = async () => {
|
|
3921
|
+
const now = nowMs();
|
|
3922
|
+
pruneTargets(now);
|
|
3923
|
+
const candidates = Array.from(new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())))).toSorted();
|
|
3924
|
+
if (candidates.length === 0) return;
|
|
3925
|
+
const count = Math.min(candidates.length, SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL);
|
|
3926
|
+
const selected = Array.from({ length: count }, (_, index) => candidates[(pollOffset + index) % candidates.length]).filter((userId) => Boolean(userId));
|
|
3927
|
+
pollOffset = (pollOffset + count) % candidates.length;
|
|
3928
|
+
for (const userId of selected) {
|
|
3929
|
+
if (stopped) return;
|
|
3930
|
+
try {
|
|
3931
|
+
const response = await params.client.getPresence({ user: userId });
|
|
3932
|
+
if (stopped) return;
|
|
3933
|
+
const next = response.presence === "active" || response.presence === "away" ? response.presence : void 0;
|
|
3934
|
+
if (!next) continue;
|
|
3935
|
+
const previous = presenceByUser.get(userId);
|
|
3936
|
+
presenceByUser.set(userId, next);
|
|
3937
|
+
if (previous === "away" && next === "active") emitTransition(userId, now);
|
|
3938
|
+
} catch (err) {
|
|
3939
|
+
if (stopped) return;
|
|
3940
|
+
params.error?.(`slack presence poll failed for user ${userId}: ${String(err)}`);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
const pollOnce = () => {
|
|
3945
|
+
if (stopped) return Promise.resolve();
|
|
3946
|
+
if (activePoll) return activePoll;
|
|
3947
|
+
const run = performPoll().finally(() => {
|
|
3948
|
+
if (activePoll === run) activePoll = void 0;
|
|
3506
3949
|
});
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
return SLACK_AUTH_ERROR_RE.test(formatUnknownError(error, ""));
|
|
3950
|
+
activePoll = run;
|
|
3951
|
+
return run;
|
|
3952
|
+
};
|
|
3953
|
+
return {
|
|
3954
|
+
observe,
|
|
3955
|
+
pollOnce,
|
|
3956
|
+
start: () => {
|
|
3957
|
+
if (timer) return;
|
|
3958
|
+
stopped = false;
|
|
3959
|
+
params.log?.(`slack presence polling enabled for account ${params.accountId}`);
|
|
3960
|
+
timer = setInterval(() => void pollOnce(), SLACK_PRESENCE_POLL_INTERVAL_MS);
|
|
3961
|
+
timer.unref?.();
|
|
3962
|
+
},
|
|
3963
|
+
stop: async () => {
|
|
3964
|
+
stopped = true;
|
|
3965
|
+
if (timer) {
|
|
3966
|
+
clearInterval(timer);
|
|
3967
|
+
timer = void 0;
|
|
3968
|
+
}
|
|
3969
|
+
await activePoll;
|
|
3970
|
+
}
|
|
3971
|
+
};
|
|
3530
3972
|
}
|
|
3531
|
-
|
|
3532
|
-
|
|
3973
|
+
//#endregion
|
|
3974
|
+
//#region extensions/slack/src/monitor/presence-cooldown-store.ts
|
|
3975
|
+
const SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES = 25e3;
|
|
3976
|
+
function openSlackPresenceCooldownStore() {
|
|
3977
|
+
return getSlackRuntime().state.openSyncKeyedStore({
|
|
3978
|
+
namespace: "presence-greeting-cooldowns",
|
|
3979
|
+
maxEntries: SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES,
|
|
3980
|
+
overflowPolicy: "reject-new",
|
|
3981
|
+
defaultTtlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS
|
|
3982
|
+
});
|
|
3533
3983
|
}
|
|
3534
3984
|
//#endregion
|
|
3535
3985
|
//#region extensions/slack/src/monitor/provider-support.ts
|
|
@@ -3614,13 +4064,15 @@ function resolveSlackBoltInterop(params) {
|
|
|
3614
4064
|
};
|
|
3615
4065
|
throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
|
|
3616
4066
|
}
|
|
3617
|
-
function publishSlackConnectedStatus(setStatus
|
|
4067
|
+
function publishSlackConnectedStatus(setStatus, identityHealth = {
|
|
4068
|
+
healthState: "healthy",
|
|
4069
|
+
lastError: null
|
|
4070
|
+
}) {
|
|
3618
4071
|
if (!setStatus) return;
|
|
3619
4072
|
setStatus({
|
|
3620
4073
|
connected: true,
|
|
3621
4074
|
lastConnectedAt: Date.now(),
|
|
3622
|
-
|
|
3623
|
-
lastError: null
|
|
4075
|
+
...identityHealth
|
|
3624
4076
|
});
|
|
3625
4077
|
}
|
|
3626
4078
|
function publishSlackDisconnectedStatus(setStatus, error) {
|
|
@@ -3694,7 +4146,8 @@ function createSlackBoltApp(params) {
|
|
|
3694
4146
|
autoReconnectEnabled: true,
|
|
3695
4147
|
clientPingTimeout: params.socketMode?.clientPingTimeout ?? OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS,
|
|
3696
4148
|
logger: socketModeLogger,
|
|
3697
|
-
installerOptions: { clientOptions: params.clientOptions }
|
|
4149
|
+
installerOptions: { clientOptions: params.clientOptions },
|
|
4150
|
+
...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
|
|
3698
4151
|
};
|
|
3699
4152
|
if (params.socketMode?.serverPingTimeout !== void 0) socketModeReceiverOptions.serverPingTimeout = params.socketMode.serverPingTimeout;
|
|
3700
4153
|
if (params.socketMode?.pingPongLoggingEnabled !== void 0) socketModeReceiverOptions.pingPongLoggingEnabled = params.socketMode.pingPongLoggingEnabled;
|
|
@@ -3704,15 +4157,17 @@ function createSlackBoltApp(params) {
|
|
|
3704
4157
|
installSlackNativeReconnectFailureObserver(receiver);
|
|
3705
4158
|
} else if (params.slackMode === "http") receiver = new params.interop.HTTPReceiver({
|
|
3706
4159
|
signingSecret: params.signingSecret ?? "",
|
|
3707
|
-
endpoints: params.slackWebhookPath
|
|
4160
|
+
endpoints: params.slackWebhookPath,
|
|
4161
|
+
...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
|
|
3708
4162
|
});
|
|
3709
4163
|
else receiver = createSlackRelayReceiver();
|
|
4164
|
+
const appReceiver = receiver && params.wrapReceiver ? params.wrapReceiver(receiver) : receiver;
|
|
3710
4165
|
const app = new params.interop.App({
|
|
3711
|
-
token: params.
|
|
4166
|
+
token: params.token,
|
|
3712
4167
|
clientOptions: params.clientOptions,
|
|
3713
4168
|
ignoreSelf: false,
|
|
3714
4169
|
tokenVerificationEnabled: false,
|
|
3715
|
-
...
|
|
4170
|
+
...appReceiver ? { receiver: appReceiver } : {}
|
|
3716
4171
|
});
|
|
3717
4172
|
app.use(async (args) => {
|
|
3718
4173
|
if (shouldSkipOpenClawSlackSelfEvent(args)) return;
|
|
@@ -3960,7 +4415,7 @@ const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
|
|
|
3960
4415
|
const SLACK_HEADER_TEXT_MAX = 150;
|
|
3961
4416
|
const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
|
|
3962
4417
|
const loadSlashCommandsRuntime = createLazyRuntimeModule(() => import("./slash-commands.runtime-BTTMlmwy.js"));
|
|
3963
|
-
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-
|
|
4418
|
+
const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-BT-mMm-f.js"));
|
|
3964
4419
|
const loadSlackPluginCommandsRuntime = createLazyRuntimeModule(() => import("./slash-plugin-commands.runtime-Dosfcx3X.js"));
|
|
3965
4420
|
const loadSlashSkillCommandsRuntime = createLazyRuntimeModule(() => import("./slash-skill-commands.runtime-CD1J2ODY.js"));
|
|
3966
4421
|
function resolveSlackCommandMenuModelContext(params) {
|
|
@@ -4700,7 +5155,7 @@ async function getSlackBoltInterop() {
|
|
|
4700
5155
|
}
|
|
4701
5156
|
return slackBoltInterop;
|
|
4702
5157
|
}
|
|
4703
|
-
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-
|
|
5158
|
+
const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-Ea_P9VcO.js"));
|
|
4704
5159
|
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
4705
5160
|
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
|
|
4706
5161
|
function resolveStableSlackUserIdEntry(raw) {
|
|
@@ -4793,16 +5248,26 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4793
5248
|
path: `channels.slack.accounts.${account.accountId}.signingSecret`
|
|
4794
5249
|
});
|
|
4795
5250
|
const botToken = resolveSlackBotToken(opts.botToken ?? account.botToken);
|
|
5251
|
+
const userToken = account.userToken;
|
|
4796
5252
|
const appToken = resolveSlackAppToken(opts.appToken ?? account.appToken);
|
|
4797
5253
|
const relayConfig = slackMode === "relay" ? resolveSlackRelayConfig({
|
|
4798
5254
|
relay: account.config.relay,
|
|
4799
5255
|
accountId: account.accountId
|
|
4800
5256
|
}) : void 0;
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
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;
|
|
4804
5270
|
}
|
|
4805
|
-
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).`);
|
|
4806
5271
|
const slackCfg = account.config;
|
|
4807
5272
|
const dmConfig = slackCfg.dm;
|
|
4808
5273
|
const dmEnabled = dmConfig?.enabled ?? true;
|
|
@@ -4842,7 +5307,6 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4842
5307
|
const replyToMode = slackCfg.replyToMode ?? "off";
|
|
4843
5308
|
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
|
4844
5309
|
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
|
4845
|
-
const threadRequireExplicitMention = slackCfg.thread?.requireExplicitMention ?? false;
|
|
4846
5310
|
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
|
4847
5311
|
const allowNameMatching = isDangerousNameMatchingEnabled(slackCfg);
|
|
4848
5312
|
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, { fallbackLimit: SLACK_TEXT_LIMIT });
|
|
@@ -4851,15 +5315,21 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4851
5315
|
const mediaMaxBytes = (opts.mediaMaxMb ?? slackCfg.mediaMaxMb ?? 20) * 1024 * 1024;
|
|
4852
5316
|
const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
|
|
4853
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
|
+
});
|
|
4854
5323
|
const { app, receiver, socketModeLogger } = createSlackBoltApp({
|
|
4855
5324
|
interop: await getSlackBoltInterop(),
|
|
4856
5325
|
slackMode,
|
|
4857
|
-
|
|
5326
|
+
token,
|
|
4858
5327
|
appToken: slackMode === "socket" ? appToken ?? void 0 : void 0,
|
|
4859
5328
|
signingSecret: slackMode === "http" ? signingSecret ?? void 0 : void 0,
|
|
4860
5329
|
slackWebhookPath,
|
|
4861
5330
|
clientOptions,
|
|
4862
|
-
...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {}
|
|
5331
|
+
...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {},
|
|
5332
|
+
wrapReceiver: durableIngress.wrapReceiver
|
|
4863
5333
|
});
|
|
4864
5334
|
const gracefulStop = async () => {
|
|
4865
5335
|
await gracefulStopSlackApp(app);
|
|
@@ -4890,43 +5360,50 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4890
5360
|
let botUserId = "";
|
|
4891
5361
|
let botId = "";
|
|
4892
5362
|
const expectedApiAppIdFromAppToken = slackMode === "socket" ? parseApiAppIdFromAppToken(appToken) : void 0;
|
|
4893
|
-
let authTestFailed = false;
|
|
4894
5363
|
let authTestError;
|
|
4895
5364
|
let authIdentityWarning;
|
|
4896
5365
|
let authTestIdentity;
|
|
4897
5366
|
try {
|
|
4898
|
-
const auth = await
|
|
5367
|
+
const auth = await createSlackStartupAuthClient(token, clientOptions).auth.test();
|
|
4899
5368
|
const authUserId = normalizeOptionalString(auth.user_id) ?? "";
|
|
4900
5369
|
botId = normalizeOptionalString(auth.bot_id) ?? "";
|
|
4901
|
-
botUserId = botId ? authUserId : "";
|
|
5370
|
+
botUserId = account.identity === "user" ? authUserId : botId ? authUserId : "";
|
|
4902
5371
|
authTestIdentity = auth;
|
|
4903
|
-
authIdentityWarning = formatSlackBotTokenIdentityWarning({
|
|
5372
|
+
if (account.identity === "bot") authIdentityWarning = formatSlackBotTokenIdentityWarning({
|
|
4904
5373
|
auth,
|
|
4905
5374
|
accountId: account.accountId
|
|
4906
5375
|
});
|
|
4907
|
-
if (!authUserId && !enterpriseOrgInstall)
|
|
4908
|
-
authTestFailed = true;
|
|
4909
|
-
authTestError = "auth.test returned no user_id";
|
|
4910
|
-
}
|
|
5376
|
+
if (!authUserId && !enterpriseOrgInstall) authTestError = "auth.test returned no user_id";
|
|
4911
5377
|
} catch (err) {
|
|
4912
|
-
authTestFailed = true;
|
|
4913
5378
|
authTestError = err instanceof Error ? err.message : String(err);
|
|
4914
5379
|
}
|
|
4915
5380
|
const installationIdentity = resolveSlackInstallationIdentity({
|
|
4916
5381
|
enterpriseOrgInstall,
|
|
4917
|
-
auth:
|
|
5382
|
+
auth: authTestError === void 0 ? authTestIdentity : void 0,
|
|
4918
5383
|
authError: authTestError,
|
|
4919
5384
|
transportApiAppId: expectedApiAppIdFromAppToken
|
|
4920
5385
|
});
|
|
4921
5386
|
const teamId = installationIdentity.kind === "workspace" ? installationIdentity.teamId : "";
|
|
4922
5387
|
const apiAppId = installationIdentity.kind === "degraded" ? "" : installationIdentity.apiAppId ?? "";
|
|
4923
|
-
if (
|
|
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
|
+
}
|
|
4924
5392
|
if (authIdentityWarning) runtime.log?.(warn(authIdentityWarning));
|
|
4925
|
-
|
|
5393
|
+
const identityHealth = resolveSlackIdentityHealth({
|
|
5394
|
+
installationIdentity,
|
|
5395
|
+
botUserId,
|
|
5396
|
+
authTestError,
|
|
5397
|
+
authIdentityWarning
|
|
5398
|
+
});
|
|
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
|
+
}
|
|
4926
5403
|
const ctx = createSlackMonitorContext({
|
|
4927
5404
|
cfg,
|
|
4928
5405
|
accountId: account.accountId,
|
|
4929
|
-
botToken,
|
|
5406
|
+
botToken: token,
|
|
4930
5407
|
app,
|
|
4931
5408
|
runtime,
|
|
4932
5409
|
channelRuntime: opts.channelRuntime,
|
|
@@ -4954,7 +5431,6 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4954
5431
|
replyToMode,
|
|
4955
5432
|
threadHistoryScope,
|
|
4956
5433
|
threadInheritParent,
|
|
4957
|
-
threadRequireExplicitMention,
|
|
4958
5434
|
slashCommand,
|
|
4959
5435
|
textLimit,
|
|
4960
5436
|
ackReactionScope,
|
|
@@ -4968,10 +5444,24 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4968
5444
|
lastInboundAt: Date.now()
|
|
4969
5445
|
});
|
|
4970
5446
|
} : void 0;
|
|
5447
|
+
const presenceEventsEnabled = hasSlackPresenceEventsEnabled({
|
|
5448
|
+
account: slackCfg.presenceEvents,
|
|
5449
|
+
channels: slackCfg.channels
|
|
5450
|
+
});
|
|
5451
|
+
const presenceMonitor = installationIdentity.kind !== "enterprise" && presenceEventsEnabled ? createSlackPresenceMonitor({
|
|
5452
|
+
accountId: account.accountId,
|
|
5453
|
+
accountConfig: slackCfg.presenceEvents,
|
|
5454
|
+
client: app.client.users,
|
|
5455
|
+
cooldownStore: openSlackPresenceCooldownStore(),
|
|
5456
|
+
log: runtime.log,
|
|
5457
|
+
error: runtime.error
|
|
5458
|
+
}) : void 0;
|
|
5459
|
+
if (installationIdentity.kind === "enterprise" && presenceEventsEnabled) runtime.log?.(warn("slack presence events are unavailable for Enterprise Grid org installs"));
|
|
4971
5460
|
const handleSlackMessage = createSlackMessageHandler({
|
|
4972
5461
|
ctx,
|
|
4973
5462
|
account,
|
|
4974
|
-
trackEvent
|
|
5463
|
+
trackEvent,
|
|
5464
|
+
onPrepared: presenceMonitor?.observe
|
|
4975
5465
|
});
|
|
4976
5466
|
if (installationIdentity.kind !== "enterprise" && isSlackAnyNativeApprovalClientEnabled({
|
|
4977
5467
|
cfg,
|
|
@@ -4999,6 +5489,8 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
4999
5489
|
appHomeSlashCommandName: commandRegistration.mode === "single" ? commandRegistration.name : void 0,
|
|
5000
5490
|
trackEvent
|
|
5001
5491
|
});
|
|
5492
|
+
durableIngress.start();
|
|
5493
|
+
presenceMonitor?.start();
|
|
5002
5494
|
if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
|
|
5003
5495
|
path: slackWebhookPath,
|
|
5004
5496
|
handler: slackHttpHandler,
|
|
@@ -5113,10 +5605,10 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5113
5605
|
abortSignal: opts.abortSignal,
|
|
5114
5606
|
onStarted: () => {
|
|
5115
5607
|
reconnectAttempts = 0;
|
|
5116
|
-
publishSlackConnectedStatus(opts.setStatus);
|
|
5608
|
+
publishSlackConnectedStatus(opts.setStatus, identityHealth);
|
|
5117
5609
|
if (!hasLoggedSocketConnected) {
|
|
5118
5610
|
hasLoggedSocketConnected = true;
|
|
5119
|
-
runtime.log?.("slack socket mode connected");
|
|
5611
|
+
runtime.log?.(identityHealth.healthState === "degraded" ? "slack socket mode connected (degraded identity)" : "slack socket mode connected");
|
|
5120
5612
|
}
|
|
5121
5613
|
}
|
|
5122
5614
|
});
|
|
@@ -5163,9 +5655,17 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5163
5655
|
}
|
|
5164
5656
|
} else if (slackMode === "relay" && relayConfig) {
|
|
5165
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
|
+
});
|
|
5166
5666
|
await (await loadSlackRelaySource()).monitorSlackRelaySource({
|
|
5167
5667
|
config: relayConfig,
|
|
5168
|
-
|
|
5668
|
+
acceptRelayEvent: durableIngress.acceptRelayEvent,
|
|
5169
5669
|
runtime,
|
|
5170
5670
|
abortSignal: opts.abortSignal,
|
|
5171
5671
|
setStatus: opts.setStatus,
|
|
@@ -5178,13 +5678,15 @@ async function monitorSlackProvider(opts = {}) {
|
|
|
5178
5678
|
});
|
|
5179
5679
|
}
|
|
5180
5680
|
} finally {
|
|
5681
|
+
await presenceMonitor?.stop();
|
|
5181
5682
|
if (slackMode === "relay") setSlackDefaultSendIdentity(account.accountId, void 0);
|
|
5182
5683
|
opts.abortSignal?.removeEventListener("abort", stopOnAbort);
|
|
5183
5684
|
unregisterSocketModeConnectionDiagnostics();
|
|
5184
5685
|
unregisterHttpHandler?.();
|
|
5686
|
+
await durableIngress.stop();
|
|
5185
5687
|
await gracefulStop();
|
|
5186
5688
|
}
|
|
5187
5689
|
}
|
|
5188
5690
|
const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy$1;
|
|
5189
5691
|
//#endregion
|
|
5190
|
-
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 };
|