@openclaw/slack 2026.5.12-beta.7
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-inspect-D7AZNs8C.js +77 -0
- package/dist/account-inspect-api.js +10 -0
- package/dist/accounts-ClAPP5ry.js +139 -0
- package/dist/accounts.runtime-DDVcLJUI.js +2 -0
- package/dist/action-runtime-e2UhRsNx.js +350 -0
- package/dist/action-runtime.runtime-BFcqMbOm.js +2 -0
- package/dist/actions-CYLFK-Zy.js +292 -0
- package/dist/actions.runtime-CO3OaTLb.js +2 -0
- package/dist/allow-list-BPnnlRPL.js +82 -0
- package/dist/api.js +21 -0
- package/dist/approval-handler.runtime-CmeRr9qA.js +256 -0
- package/dist/blocks-input-CwTFVImV.js +29 -0
- package/dist/blocks-render-BIDw-Pom.js +161 -0
- package/dist/channel-DRjHBTDB.js +1020 -0
- package/dist/channel-api-B_nZwosg.js +20 -0
- package/dist/channel-config-api.js +2 -0
- package/dist/channel-entry.js +22 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.setup-Cayn7afd.js +73 -0
- package/dist/client-CPe4GmDR.js +103 -0
- package/dist/config-api-B_jq4NJW.js +2 -0
- package/dist/config-schema-D9B5LB_L.js +167 -0
- package/dist/configured-state.js +11 -0
- package/dist/contract-api.js +5 -0
- package/dist/directory-config-B3JiHeB7.js +54 -0
- package/dist/directory-contract-api.js +2 -0
- package/dist/directory-live-Bf16GwDh.js +133 -0
- package/dist/doctor-contract-KUjHnkQm.js +147 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/errors-BYFHR24f.js +109 -0
- package/dist/exec-approvals-7xUNgLi9.js +58 -0
- package/dist/group-policy-CyLUK6My.js +41 -0
- package/dist/http-routes-api.js +2 -0
- package/dist/inbound-contract-test-api.js +3 -0
- package/dist/index.js +33 -0
- package/dist/interactive-replies-api.js +2 -0
- package/dist/interactive-replies-qAIfuBor.js +173 -0
- package/dist/magic-string.es-BMaGRRZ1.js +1011 -0
- package/dist/media-D1XCd1uP.js +469 -0
- package/dist/message-tool-api-6lowf9zE.js +104 -0
- package/dist/message-tool-api.js +2 -0
- package/dist/monitor-a97o17G6.js +13 -0
- package/dist/mrkdwn-Cax-eSfK.js +6 -0
- package/dist/outbound-adapter-B_5sEhCg.js +174 -0
- package/dist/outbound-payload-test-api.js +2 -0
- package/dist/outbound-payload.test-harness-CVCamg1x.js +13558 -0
- package/dist/pipeline.runtime-DT0hLnq2.js +1379 -0
- package/dist/plugin-routes-DtTPmga1.js +20 -0
- package/dist/prepare-D3YqV8jB.js +1482 -0
- package/dist/prepare.test-helpers-DVcjRhfG.js +49 -0
- package/dist/probe-3eZf1FjI.js +42 -0
- package/dist/provider-D7uAN3Fq.js +3235 -0
- package/dist/registry-CeaoNfoP.js +39 -0
- package/dist/replies-Xe_jMR6o.js +139 -0
- package/dist/reply-blocks-Z5l6_R6H.js +14 -0
- package/dist/resolve-allowlist-common-Bk3clYPK.js +43 -0
- package/dist/resolve-channels-BRYqyNVJ.js +81 -0
- package/dist/resolve-users-Bd_SdP8j.js +113 -0
- package/dist/rolldown-runtime-CiIaOW0V.js +13 -0
- package/dist/room-context-0vovmZPU.js +787 -0
- package/dist/runtime-Bo-KHM-F.js +8 -0
- package/dist/runtime-api-Dd1xIV5v.js +9 -0
- package/dist/runtime-api.js +14 -0
- package/dist/runtime-setter-api.js +2 -0
- package/dist/scopes-CDevO8jg.js +74 -0
- package/dist/secret-contract-Bo6lbSkh.js +141 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/security-audit-BtHGnD3d.js +51 -0
- package/dist/security-contract-api.js +2 -0
- package/dist/send-D_A9kL-C.js +721 -0
- package/dist/send.runtime-BRE_ncCU.js +2 -0
- package/dist/send.runtime-_l76lUuL.js +2 -0
- package/dist/setup-core-B9NetDkM.js +320 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/setup-surface-D88QBVOW.js +128 -0
- package/dist/shared-D8U42xFL.js +208 -0
- package/dist/slash-commands.runtime-22kgyst2.js +19 -0
- package/dist/slash-dispatch.runtime-BJgT0jwV.js +32 -0
- package/dist/slash-plugin-commands.runtime-CF-n3MeP.js +2 -0
- package/dist/slash-skill-commands.runtime-BMs0VjTe.js +7 -0
- package/dist/streaming-compat-RkZgTmQ2.js +43 -0
- package/dist/target-parsing-CQmv-iSm.js +55 -0
- package/dist/targets-B1tYCAr6.js +2 -0
- package/dist/test-api.js +8 -0
- package/dist/thread-ts-C2x7c5PP.js +24 -0
- package/openclaw.plugin.json +2405 -0
- package/package.json +84 -0
|
@@ -0,0 +1,3235 @@
|
|
|
1
|
+
import { a as resolveSlackAccount, d as resolveSlackBotToken, o as resolveSlackAccountAllowFrom, s as resolveSlackAccountDmPolicy, u as resolveSlackAppToken } from "./accounts-ClAPP5ry.js";
|
|
2
|
+
import { i as isSlackExecApprovalClientEnabled, n as isSlackExecApprovalApprover, r as isSlackExecApprovalAuthorizedSender } from "./exec-approvals-7xUNgLi9.js";
|
|
3
|
+
import "./blocks-render-BIDw-Pom.js";
|
|
4
|
+
import { i as truncateSlackText, r as SLACK_TEXT_LIMIT } from "./thread-ts-C2x7c5PP.js";
|
|
5
|
+
import { c as resolveSlackWebClientOptions } from "./client-CPe4GmDR.js";
|
|
6
|
+
import { n as normalizeAllowList } from "./allow-list-BPnnlRPL.js";
|
|
7
|
+
import "./blocks-input-CwTFVImV.js";
|
|
8
|
+
import { n as registerSlackHttpHandler, r as normalizeSlackWebhookPath } from "./registry-CeaoNfoP.js";
|
|
9
|
+
import { t as formatSlackError } from "./errors-BYFHR24f.js";
|
|
10
|
+
import { t as resolveSlackChannelAllowlist } from "./resolve-channels-BRYqyNVJ.js";
|
|
11
|
+
import { t as resolveSlackUserAllowlist } from "./resolve-users-Bd_SdP8j.js";
|
|
12
|
+
import { C as resolveOpenProviderRuntimeGroupPolicy, D as buildSlackSlashCommandMatcher, E as warnMissingProviderGroupPolicyFallbackOnce, O as resolveSlackSlashCommandConfig, S as resolveDefaultGroupPolicy, _ as resolveSlackChannelLabel, d as resolveSlackEffectiveAllowFrom, f as createSlackMonitorContext, g as resolveSlackChannelConfig, h as resolveSlackChatType, i as parsePluginBindingApprovalCustomId, k as stripSlackMentionsForCommandDetection, l as authorizeSlackSystemEventSender, m as normalizeSlackChannelType, n as authorizeSlackDirectMessage, p as isSlackChannelAllowedByPolicy, r as buildPluginBindingResolvedText, s as resolvePluginConversationBindingApproval, t as resolveSlackRoomContextHints, u as resolveSlackCommandIngress, v as getRuntimeConfig$1, y as isDangerousNameMatchingEnabled } from "./room-context-0vovmZPU.js";
|
|
13
|
+
import { t as escapeSlackMrkdwn } from "./mrkdwn-Cax-eSfK.js";
|
|
14
|
+
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
15
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
16
|
+
import { normalizeAccountId, normalizeMainKey } from "openclaw/plugin-sdk/routing";
|
|
17
|
+
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";
|
|
18
|
+
import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
|
|
19
|
+
import { addAllowlistUserEntriesFromConfigEntry, buildAllowlistResolutionSummary, mergeAllowlist, patchAllowlistUsersInConfigEntries, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
|
|
20
|
+
import { computeBackoff, createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, sleepWithAbort, warn } from "openclaw/plugin-sdk/runtime-env";
|
|
21
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
22
|
+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
23
|
+
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
|
|
24
|
+
import { chunkItems } from "openclaw/plugin-sdk/text-chunking";
|
|
25
|
+
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "openclaw/plugin-sdk/native-command-config-runtime";
|
|
26
|
+
import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
|
|
27
|
+
import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
|
|
28
|
+
import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
29
|
+
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
30
|
+
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
31
|
+
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
|
32
|
+
import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
|
|
33
|
+
import { replaceConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
|
34
|
+
import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
|
35
|
+
import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
|
|
36
|
+
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
37
|
+
import { formatCommandArgMenuTitle, resolveCommandAuthorization, resolveNativeCommandSessionTargets, resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth-native";
|
|
38
|
+
import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
|
|
39
|
+
import { createInteractiveConversationBindingHelpers, dispatchPluginInteractiveHandler } from "openclaw/plugin-sdk/plugin-runtime";
|
|
40
|
+
import { createChannelInboundDebouncer, shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
|
|
41
|
+
import { generateSecureToken } from "openclaw/plugin-sdk/secure-random-runtime";
|
|
42
|
+
//#region extensions/slack/src/channel-migration.ts
|
|
43
|
+
function resolveAccountChannels(cfg, accountId) {
|
|
44
|
+
if (!accountId) return {};
|
|
45
|
+
const normalized = normalizeAccountId(accountId);
|
|
46
|
+
const accounts = cfg.channels?.slack?.accounts;
|
|
47
|
+
if (!accounts || typeof accounts !== "object") return {};
|
|
48
|
+
const exact = accounts[normalized];
|
|
49
|
+
if (exact?.channels) return { channels: exact.channels };
|
|
50
|
+
const matchKey = Object.keys(accounts).find((key) => normalizeLowercaseStringOrEmpty(key) === normalizeLowercaseStringOrEmpty(normalized));
|
|
51
|
+
return { channels: matchKey ? accounts[matchKey]?.channels : void 0 };
|
|
52
|
+
}
|
|
53
|
+
function migrateSlackChannelsInPlace(channels, oldChannelId, newChannelId) {
|
|
54
|
+
if (!channels) return {
|
|
55
|
+
migrated: false,
|
|
56
|
+
skippedExisting: false
|
|
57
|
+
};
|
|
58
|
+
if (oldChannelId === newChannelId) return {
|
|
59
|
+
migrated: false,
|
|
60
|
+
skippedExisting: false
|
|
61
|
+
};
|
|
62
|
+
if (!Object.hasOwn(channels, oldChannelId)) return {
|
|
63
|
+
migrated: false,
|
|
64
|
+
skippedExisting: false
|
|
65
|
+
};
|
|
66
|
+
if (Object.hasOwn(channels, newChannelId)) return {
|
|
67
|
+
migrated: false,
|
|
68
|
+
skippedExisting: true
|
|
69
|
+
};
|
|
70
|
+
channels[newChannelId] = channels[oldChannelId];
|
|
71
|
+
delete channels[oldChannelId];
|
|
72
|
+
return {
|
|
73
|
+
migrated: true,
|
|
74
|
+
skippedExisting: false
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function migrateSlackChannelConfig(params) {
|
|
78
|
+
const scopes = [];
|
|
79
|
+
let migrated = false;
|
|
80
|
+
let skippedExisting = false;
|
|
81
|
+
const accountChannels = resolveAccountChannels(params.cfg, params.accountId).channels;
|
|
82
|
+
if (accountChannels) {
|
|
83
|
+
const result = migrateSlackChannelsInPlace(accountChannels, params.oldChannelId, params.newChannelId);
|
|
84
|
+
if (result.migrated) {
|
|
85
|
+
migrated = true;
|
|
86
|
+
scopes.push("account");
|
|
87
|
+
}
|
|
88
|
+
if (result.skippedExisting) skippedExisting = true;
|
|
89
|
+
}
|
|
90
|
+
const globalChannels = params.cfg.channels?.slack?.channels;
|
|
91
|
+
if (globalChannels) {
|
|
92
|
+
const result = migrateSlackChannelsInPlace(globalChannels, params.oldChannelId, params.newChannelId);
|
|
93
|
+
if (result.migrated) {
|
|
94
|
+
migrated = true;
|
|
95
|
+
scopes.push("global");
|
|
96
|
+
}
|
|
97
|
+
if (result.skippedExisting) skippedExisting = true;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
migrated,
|
|
101
|
+
skippedExisting,
|
|
102
|
+
scopes
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region extensions/slack/src/monitor/events/channels.ts
|
|
107
|
+
function registerSlackChannelEvents(params) {
|
|
108
|
+
const { ctx, trackEvent } = params;
|
|
109
|
+
const enqueueChannelSystemEvent = (params) => {
|
|
110
|
+
if (!ctx.isChannelAllowed({
|
|
111
|
+
channelId: params.channelId,
|
|
112
|
+
channelName: params.channelName,
|
|
113
|
+
channelType: "channel"
|
|
114
|
+
})) return;
|
|
115
|
+
const label = resolveSlackChannelLabel({
|
|
116
|
+
channelId: params.channelId,
|
|
117
|
+
channelName: params.channelName
|
|
118
|
+
});
|
|
119
|
+
const sessionKey = ctx.resolveSlackSystemEventSessionKey({
|
|
120
|
+
channelId: params.channelId,
|
|
121
|
+
channelType: "channel"
|
|
122
|
+
});
|
|
123
|
+
enqueueSystemEvent(`Slack channel ${params.kind}: ${label}.`, {
|
|
124
|
+
sessionKey,
|
|
125
|
+
contextKey: `slack:channel:${params.kind}:${params.channelId ?? params.channelName ?? "unknown"}`
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
ctx.app.event("channel_created", async ({ event, body }) => {
|
|
129
|
+
try {
|
|
130
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
131
|
+
trackEvent?.();
|
|
132
|
+
const payload = event;
|
|
133
|
+
const channelId = payload.channel?.id;
|
|
134
|
+
const channelName = payload.channel?.name;
|
|
135
|
+
enqueueChannelSystemEvent({
|
|
136
|
+
kind: "created",
|
|
137
|
+
channelId,
|
|
138
|
+
channelName
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
ctx.runtime.error?.(danger(`slack channel created handler failed: ${formatErrorMessage(err)}`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
ctx.app.event("channel_rename", async ({ event, body }) => {
|
|
145
|
+
try {
|
|
146
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
147
|
+
trackEvent?.();
|
|
148
|
+
const payload = event;
|
|
149
|
+
const channelId = payload.channel?.id;
|
|
150
|
+
enqueueChannelSystemEvent({
|
|
151
|
+
kind: "renamed",
|
|
152
|
+
channelId,
|
|
153
|
+
channelName: payload.channel?.name_normalized ?? payload.channel?.name
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
ctx.runtime.error?.(danger(`slack channel rename handler failed: ${formatErrorMessage(err)}`));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
ctx.app.event("channel_id_changed", async ({ event, body }) => {
|
|
160
|
+
try {
|
|
161
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
162
|
+
trackEvent?.();
|
|
163
|
+
const payload = event;
|
|
164
|
+
const oldChannelId = payload.old_channel_id;
|
|
165
|
+
const newChannelId = payload.new_channel_id;
|
|
166
|
+
if (!oldChannelId || !newChannelId) return;
|
|
167
|
+
const label = resolveSlackChannelLabel({
|
|
168
|
+
channelId: newChannelId,
|
|
169
|
+
channelName: (await ctx.resolveChannelName(newChannelId))?.name
|
|
170
|
+
});
|
|
171
|
+
ctx.runtime.log?.(warn(`[slack] Channel ID changed: ${oldChannelId} → ${newChannelId} (${label})`));
|
|
172
|
+
if (!resolveChannelConfigWrites({
|
|
173
|
+
cfg: ctx.cfg,
|
|
174
|
+
channelId: "slack",
|
|
175
|
+
accountId: ctx.accountId
|
|
176
|
+
})) {
|
|
177
|
+
ctx.runtime.log?.(warn("[slack] Config writes disabled; skipping channel config migration."));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const currentConfig = getRuntimeConfig();
|
|
181
|
+
const migration = migrateSlackChannelConfig({
|
|
182
|
+
cfg: currentConfig,
|
|
183
|
+
accountId: ctx.accountId,
|
|
184
|
+
oldChannelId,
|
|
185
|
+
newChannelId
|
|
186
|
+
});
|
|
187
|
+
if (migration.migrated) {
|
|
188
|
+
migrateSlackChannelConfig({
|
|
189
|
+
cfg: ctx.cfg,
|
|
190
|
+
accountId: ctx.accountId,
|
|
191
|
+
oldChannelId,
|
|
192
|
+
newChannelId
|
|
193
|
+
});
|
|
194
|
+
await replaceConfigFile({
|
|
195
|
+
nextConfig: currentConfig,
|
|
196
|
+
afterWrite: { mode: "auto" }
|
|
197
|
+
});
|
|
198
|
+
ctx.runtime.log?.(warn("[slack] Channel config migrated and saved successfully."));
|
|
199
|
+
} else if (migration.skippedExisting) ctx.runtime.log?.(warn(`[slack] Channel config already exists for ${newChannelId}; leaving ${oldChannelId} unchanged`));
|
|
200
|
+
else ctx.runtime.log?.(warn(`[slack] No config found for old channel ID ${oldChannelId}; migration logged only`));
|
|
201
|
+
} catch (err) {
|
|
202
|
+
ctx.runtime.error?.(danger(`slack channel_id_changed handler failed: ${formatErrorMessage(err)}`));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region extensions/slack/src/monitor/events/home.ts
|
|
208
|
+
function buildSlackHomeView() {
|
|
209
|
+
return {
|
|
210
|
+
type: "home",
|
|
211
|
+
callback_id: "openclaw:home",
|
|
212
|
+
blocks: [
|
|
213
|
+
{
|
|
214
|
+
type: "header",
|
|
215
|
+
text: {
|
|
216
|
+
type: "plain_text",
|
|
217
|
+
text: "OpenClaw"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: "section",
|
|
222
|
+
text: {
|
|
223
|
+
type: "mrkdwn",
|
|
224
|
+
text: "Send a DM, mention OpenClaw in a channel, or use `/openclaw` to start a session."
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: "context",
|
|
229
|
+
elements: [{
|
|
230
|
+
type: "mrkdwn",
|
|
231
|
+
text: "This Home tab is safe to show to any workspace member who opens the app."
|
|
232
|
+
}]
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function registerSlackHomeEvents(params) {
|
|
238
|
+
const { ctx, trackEvent } = params;
|
|
239
|
+
ctx.app.event("app_home_opened", async ({ event, body }) => {
|
|
240
|
+
try {
|
|
241
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
242
|
+
trackEvent?.();
|
|
243
|
+
const payload = event;
|
|
244
|
+
if (!payload.user || payload.tab === "messages") return;
|
|
245
|
+
await ctx.app.client.views.publish({
|
|
246
|
+
token: ctx.botToken,
|
|
247
|
+
user_id: payload.user,
|
|
248
|
+
view: buildSlackHomeView()
|
|
249
|
+
});
|
|
250
|
+
} catch (err) {
|
|
251
|
+
ctx.runtime.error?.(danger(`slack app home handler failed: ${formatErrorMessage(err)}`));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region extensions/slack/src/interactive-dispatch.ts
|
|
257
|
+
async function dispatchSlackPluginInteractiveHandler(params) {
|
|
258
|
+
return await dispatchPluginInteractiveHandler({
|
|
259
|
+
channel: "slack",
|
|
260
|
+
data: params.data,
|
|
261
|
+
dedupeId: params.interactionId,
|
|
262
|
+
onMatched: params.onMatched,
|
|
263
|
+
invoke: ({ registration, namespace, payload }) => registration.handler({
|
|
264
|
+
...params.ctx,
|
|
265
|
+
channel: "slack",
|
|
266
|
+
interaction: {
|
|
267
|
+
...params.ctx.interaction,
|
|
268
|
+
data: params.data,
|
|
269
|
+
namespace,
|
|
270
|
+
payload
|
|
271
|
+
},
|
|
272
|
+
respond: params.respond,
|
|
273
|
+
...createInteractiveConversationBindingHelpers({
|
|
274
|
+
registration,
|
|
275
|
+
senderId: params.ctx.senderId,
|
|
276
|
+
conversation: {
|
|
277
|
+
channel: "slack",
|
|
278
|
+
accountId: params.ctx.accountId,
|
|
279
|
+
conversationId: params.ctx.conversationId,
|
|
280
|
+
parentConversationId: params.ctx.parentConversationId,
|
|
281
|
+
threadId: params.ctx.threadId
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region extensions/slack/src/monitor/events/interactions.block-actions.ts
|
|
289
|
+
function readOptionValues(options) {
|
|
290
|
+
if (!Array.isArray(options)) return;
|
|
291
|
+
const values = options.map((option) => option && typeof option === "object" ? option.value : null).filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
292
|
+
return values.length > 0 ? values : void 0;
|
|
293
|
+
}
|
|
294
|
+
function readOptionLabels(options) {
|
|
295
|
+
if (!Array.isArray(options)) return;
|
|
296
|
+
const labels = options.map((option) => option && typeof option === "object" ? option.text?.text ?? null : null).filter((label) => typeof label === "string" && label.trim().length > 0);
|
|
297
|
+
return labels.length > 0 ? labels : void 0;
|
|
298
|
+
}
|
|
299
|
+
function uniqueNonEmptyStrings(values) {
|
|
300
|
+
const unique = [];
|
|
301
|
+
const seen = /* @__PURE__ */ new Set();
|
|
302
|
+
for (const entry of values) {
|
|
303
|
+
if (typeof entry !== "string") continue;
|
|
304
|
+
const trimmed = entry.trim();
|
|
305
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
306
|
+
seen.add(trimmed);
|
|
307
|
+
unique.push(trimmed);
|
|
308
|
+
}
|
|
309
|
+
return unique;
|
|
310
|
+
}
|
|
311
|
+
function collectRichTextFragments(value, out) {
|
|
312
|
+
if (!value || typeof value !== "object") return;
|
|
313
|
+
const typed = value;
|
|
314
|
+
if (typeof typed.text === "string" && typed.text.trim().length > 0) out.push(typed.text.trim());
|
|
315
|
+
if (Array.isArray(typed.elements)) for (const child of typed.elements) collectRichTextFragments(child, out);
|
|
316
|
+
}
|
|
317
|
+
function summarizeRichTextPreview(value) {
|
|
318
|
+
const fragments = [];
|
|
319
|
+
collectRichTextFragments(value, fragments);
|
|
320
|
+
if (fragments.length === 0) return;
|
|
321
|
+
const joined = fragments.join(" ").replace(/\s+/g, " ").trim();
|
|
322
|
+
if (!joined) return;
|
|
323
|
+
const max = 120;
|
|
324
|
+
return joined.length <= max ? joined : `${joined.slice(0, max - 1)}…`;
|
|
325
|
+
}
|
|
326
|
+
function readInteractionAction(raw) {
|
|
327
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
328
|
+
return raw;
|
|
329
|
+
}
|
|
330
|
+
function summarizeAction(action) {
|
|
331
|
+
const typed = action;
|
|
332
|
+
const actionType = typed.type;
|
|
333
|
+
const selectedUsers = uniqueNonEmptyStrings([...typed.selected_user ? [typed.selected_user] : [], ...Array.isArray(typed.selected_users) ? typed.selected_users : []]);
|
|
334
|
+
const selectedChannels = uniqueNonEmptyStrings([...typed.selected_channel ? [typed.selected_channel] : [], ...Array.isArray(typed.selected_channels) ? typed.selected_channels : []]);
|
|
335
|
+
const selectedConversations = uniqueNonEmptyStrings([...typed.selected_conversation ? [typed.selected_conversation] : [], ...Array.isArray(typed.selected_conversations) ? typed.selected_conversations : []]);
|
|
336
|
+
const selectedValues = uniqueNonEmptyStrings([
|
|
337
|
+
...typed.selected_option?.value ? [typed.selected_option.value] : [],
|
|
338
|
+
...readOptionValues(typed.selected_options) ?? [],
|
|
339
|
+
...selectedUsers,
|
|
340
|
+
...selectedChannels,
|
|
341
|
+
...selectedConversations
|
|
342
|
+
]);
|
|
343
|
+
const selectedLabels = uniqueNonEmptyStrings([...typed.selected_option?.text?.text ? [typed.selected_option.text.text] : [], ...readOptionLabels(typed.selected_options) ?? []]);
|
|
344
|
+
const inputValue = typeof typed.value === "string" ? typed.value : void 0;
|
|
345
|
+
const inputNumber = actionType === "number_input" && inputValue != null ? Number.parseFloat(inputValue) : void 0;
|
|
346
|
+
const parsedNumber = Number.isFinite(inputNumber) ? inputNumber : void 0;
|
|
347
|
+
const inputEmail = actionType === "email_text_input" && inputValue?.includes("@") ? inputValue : void 0;
|
|
348
|
+
let inputUrl;
|
|
349
|
+
if (actionType === "url_text_input" && inputValue) try {
|
|
350
|
+
inputUrl = new URL(inputValue).toString();
|
|
351
|
+
} catch {
|
|
352
|
+
inputUrl = void 0;
|
|
353
|
+
}
|
|
354
|
+
const richTextValue = actionType === "rich_text_input" ? typed.rich_text_value : void 0;
|
|
355
|
+
const richTextPreview = summarizeRichTextPreview(richTextValue);
|
|
356
|
+
return {
|
|
357
|
+
actionType,
|
|
358
|
+
inputKind: actionType === "number_input" ? "number" : actionType === "email_text_input" ? "email" : actionType === "url_text_input" ? "url" : actionType === "rich_text_input" ? "rich_text" : inputValue != null ? "text" : void 0,
|
|
359
|
+
value: typed.value,
|
|
360
|
+
selectedValues: selectedValues.length > 0 ? selectedValues : void 0,
|
|
361
|
+
selectedUsers: selectedUsers.length > 0 ? selectedUsers : void 0,
|
|
362
|
+
selectedChannels: selectedChannels.length > 0 ? selectedChannels : void 0,
|
|
363
|
+
selectedConversations: selectedConversations.length > 0 ? selectedConversations : void 0,
|
|
364
|
+
selectedLabels: selectedLabels.length > 0 ? selectedLabels : void 0,
|
|
365
|
+
selectedDate: typed.selected_date,
|
|
366
|
+
selectedTime: typed.selected_time,
|
|
367
|
+
selectedDateTime: typeof typed.selected_date_time === "number" ? typed.selected_date_time : void 0,
|
|
368
|
+
inputValue,
|
|
369
|
+
inputNumber: parsedNumber,
|
|
370
|
+
inputEmail,
|
|
371
|
+
inputUrl,
|
|
372
|
+
richTextValue,
|
|
373
|
+
richTextPreview,
|
|
374
|
+
workflowTriggerUrl: typed.workflow?.trigger_url,
|
|
375
|
+
workflowId: typed.workflow?.workflow_id
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function isBulkActionsBlock(block) {
|
|
379
|
+
return block.type === "actions" && Array.isArray(block.elements) && block.elements.length > 0 && block.elements.every((el) => typeof el.action_id === "string" && el.action_id.includes("_all_"));
|
|
380
|
+
}
|
|
381
|
+
function formatInteractionSelectionLabel(params) {
|
|
382
|
+
if (params.summary.actionType === "button" && params.buttonText?.trim()) return params.buttonText.trim();
|
|
383
|
+
if (params.summary.selectedLabels?.length) {
|
|
384
|
+
if (params.summary.selectedLabels.length <= 3) return params.summary.selectedLabels.join(", ");
|
|
385
|
+
return `${params.summary.selectedLabels.slice(0, 3).join(", ")} +${params.summary.selectedLabels.length - 3}`;
|
|
386
|
+
}
|
|
387
|
+
if (params.summary.selectedValues?.length) {
|
|
388
|
+
if (params.summary.selectedValues.length <= 3) return params.summary.selectedValues.join(", ");
|
|
389
|
+
return `${params.summary.selectedValues.slice(0, 3).join(", ")} +${params.summary.selectedValues.length - 3}`;
|
|
390
|
+
}
|
|
391
|
+
if (params.summary.selectedDate) return params.summary.selectedDate;
|
|
392
|
+
if (params.summary.selectedTime) return params.summary.selectedTime;
|
|
393
|
+
if (typeof params.summary.selectedDateTime === "number") return (/* @__PURE__ */ new Date(params.summary.selectedDateTime * 1e3)).toISOString();
|
|
394
|
+
if (params.summary.richTextPreview) return params.summary.richTextPreview;
|
|
395
|
+
if (params.summary.value?.trim()) return params.summary.value.trim();
|
|
396
|
+
return params.actionId;
|
|
397
|
+
}
|
|
398
|
+
function formatInteractionConfirmationText(params) {
|
|
399
|
+
const userId = normalizeOptionalString(params.userId);
|
|
400
|
+
const actor = userId ? ` by <@${userId}>` : "";
|
|
401
|
+
return `:white_check_mark: *${escapeSlackMrkdwn(params.selectedLabel)}* selected${actor}`;
|
|
402
|
+
}
|
|
403
|
+
function buildSlackPluginInteractionData(params) {
|
|
404
|
+
const actionId = normalizeOptionalString(params.actionId) ?? "";
|
|
405
|
+
if (!actionId) return null;
|
|
406
|
+
const payload = normalizeOptionalString(params.summary.value) || params.summary.selectedValues?.map((value) => normalizeOptionalString(value)).find(Boolean) || "";
|
|
407
|
+
if (actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`)) return payload || null;
|
|
408
|
+
return payload ? `${actionId}:${payload}` : actionId;
|
|
409
|
+
}
|
|
410
|
+
function isSlackReplyActionId(actionId) {
|
|
411
|
+
return actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`);
|
|
412
|
+
}
|
|
413
|
+
function buildSlackPluginInteractionId(params) {
|
|
414
|
+
const primaryValue = normalizeOptionalString(params.summary.value) || params.summary.selectedValues?.map((value) => normalizeOptionalString(value)).find(Boolean) || "";
|
|
415
|
+
return [
|
|
416
|
+
normalizeOptionalString(params.userId) ?? "",
|
|
417
|
+
normalizeOptionalString(params.channelId) ?? "",
|
|
418
|
+
normalizeOptionalString(params.messageTs) ?? "",
|
|
419
|
+
normalizeOptionalString(params.triggerId) ?? "",
|
|
420
|
+
normalizeOptionalString(params.actionId) ?? "",
|
|
421
|
+
primaryValue
|
|
422
|
+
].join(":");
|
|
423
|
+
}
|
|
424
|
+
function parseSlackBlockAction(params) {
|
|
425
|
+
const typedBody = params.body;
|
|
426
|
+
const typedAction = readInteractionAction(params.action);
|
|
427
|
+
if (!typedAction) {
|
|
428
|
+
params.log?.(`slack:interaction malformed action payload channel=${typedBody.channel?.id ?? typedBody.container?.channel_id ?? "unknown"} user=${typedBody.user?.id ?? "unknown"}`);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const typedActionWithText = typedAction;
|
|
432
|
+
return {
|
|
433
|
+
typedBody,
|
|
434
|
+
typedAction,
|
|
435
|
+
typedActionWithText,
|
|
436
|
+
actionId: typeof typedActionWithText.action_id === "string" ? typedActionWithText.action_id : "unknown",
|
|
437
|
+
blockId: typedActionWithText.block_id,
|
|
438
|
+
userId: typedBody.user?.id ?? "unknown",
|
|
439
|
+
channelId: typedBody.channel?.id ?? typedBody.container?.channel_id,
|
|
440
|
+
messageTs: typedBody.message?.ts ?? typedBody.container?.message_ts,
|
|
441
|
+
threadTs: typedBody.container?.thread_ts,
|
|
442
|
+
actionSummary: summarizeAction(typedAction)
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function respondEphemeral(respond, text) {
|
|
446
|
+
if (!respond) return;
|
|
447
|
+
try {
|
|
448
|
+
await respond({
|
|
449
|
+
text,
|
|
450
|
+
response_type: "ephemeral"
|
|
451
|
+
});
|
|
452
|
+
} catch {}
|
|
453
|
+
}
|
|
454
|
+
async function updateSlackInteractionMessage(params) {
|
|
455
|
+
if (!params.channelId || !params.messageTs) return;
|
|
456
|
+
await params.ctx.app.client.chat.update({
|
|
457
|
+
channel: params.channelId,
|
|
458
|
+
ts: params.messageTs,
|
|
459
|
+
text: params.text,
|
|
460
|
+
...params.blocks ? { blocks: params.blocks } : {}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
async function authorizeSlackBlockAction(params) {
|
|
464
|
+
const auth = await authorizeSlackSystemEventSender({
|
|
465
|
+
ctx: params.ctx,
|
|
466
|
+
senderId: params.parsed.userId,
|
|
467
|
+
channelId: params.parsed.channelId,
|
|
468
|
+
expectedSenderId: params.parsed.userId,
|
|
469
|
+
interactiveEvent: true
|
|
470
|
+
});
|
|
471
|
+
if (auth.allowed) return auth;
|
|
472
|
+
params.ctx.runtime.log?.(`slack:interaction drop action=${params.parsed.actionId} user=${params.parsed.userId} channel=${params.parsed.channelId ?? "unknown"} reason=${auth.reason ?? "unauthorized"}`);
|
|
473
|
+
await respondEphemeral(params.respond, "You are not authorized to use this control.");
|
|
474
|
+
return { allowed: false };
|
|
475
|
+
}
|
|
476
|
+
async function handleSlackPluginBindingApproval(params) {
|
|
477
|
+
const pluginBindingApproval = parsePluginBindingApprovalCustomId(params.pluginInteractionData);
|
|
478
|
+
if (!pluginBindingApproval) return false;
|
|
479
|
+
const resolved = await resolvePluginConversationBindingApproval({
|
|
480
|
+
approvalId: pluginBindingApproval.approvalId,
|
|
481
|
+
decision: pluginBindingApproval.decision,
|
|
482
|
+
senderId: params.parsed.userId
|
|
483
|
+
});
|
|
484
|
+
try {
|
|
485
|
+
await updateSlackInteractionMessage({
|
|
486
|
+
ctx: params.ctx,
|
|
487
|
+
channelId: params.parsed.channelId,
|
|
488
|
+
messageTs: params.parsed.messageTs,
|
|
489
|
+
text: params.parsed.typedBody.message?.text ?? "",
|
|
490
|
+
blocks: []
|
|
491
|
+
});
|
|
492
|
+
} catch {}
|
|
493
|
+
await respondEphemeral(params.respond, buildPluginBindingResolvedText(resolved));
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
async function handleSlackExecApprovalInteraction(params) {
|
|
497
|
+
const approval = parseExecApprovalCommandText(params.pluginInteractionData);
|
|
498
|
+
if (!approval) return false;
|
|
499
|
+
const pluginApprovalAuthorizedSender = isSlackExecApprovalApprover({
|
|
500
|
+
cfg: params.ctx.cfg,
|
|
501
|
+
accountId: params.ctx.accountId,
|
|
502
|
+
senderId: params.parsed.userId
|
|
503
|
+
});
|
|
504
|
+
const execApprovalAuthorizedSender = isSlackExecApprovalAuthorizedSender({
|
|
505
|
+
cfg: params.ctx.cfg,
|
|
506
|
+
accountId: params.ctx.accountId,
|
|
507
|
+
senderId: params.parsed.userId
|
|
508
|
+
});
|
|
509
|
+
if (!(approval.approvalId.startsWith("plugin:") ? pluginApprovalAuthorizedSender : execApprovalAuthorizedSender || pluginApprovalAuthorizedSender)) {
|
|
510
|
+
params.ctx.runtime.log?.(`slack:interaction drop exec approval user=${params.parsed.userId} (not authorized)`);
|
|
511
|
+
await respondEphemeral(params.respond, "You are not authorized to approve this request.");
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
await resolveApprovalOverGateway({
|
|
516
|
+
cfg: params.ctx.cfg,
|
|
517
|
+
approvalId: approval.approvalId,
|
|
518
|
+
decision: approval.decision,
|
|
519
|
+
senderId: params.parsed.userId,
|
|
520
|
+
allowPluginFallback: pluginApprovalAuthorizedSender,
|
|
521
|
+
clientDisplayName: `Slack approval (${params.parsed.userId.trim() || "unknown"})`
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
params.ctx.runtime.log?.(`slack:interaction exec approval resolve failed id=${approval.approvalId}: ${String(error)}`);
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
await updateSlackInteractionMessage({
|
|
529
|
+
ctx: params.ctx,
|
|
530
|
+
channelId: params.parsed.channelId,
|
|
531
|
+
messageTs: params.parsed.messageTs,
|
|
532
|
+
text: params.parsed.typedBody.message?.text ?? "",
|
|
533
|
+
blocks: []
|
|
534
|
+
});
|
|
535
|
+
} catch {}
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
async function dispatchSlackPluginInteraction(params) {
|
|
539
|
+
const pluginInteractionId = buildSlackPluginInteractionId({
|
|
540
|
+
userId: params.parsed.userId,
|
|
541
|
+
channelId: params.parsed.channelId,
|
|
542
|
+
messageTs: params.parsed.messageTs,
|
|
543
|
+
triggerId: params.parsed.typedBody.trigger_id,
|
|
544
|
+
actionId: params.parsed.actionId,
|
|
545
|
+
summary: params.parsed.actionSummary
|
|
546
|
+
});
|
|
547
|
+
if (await handleSlackPluginBindingApproval({
|
|
548
|
+
ctx: params.ctx,
|
|
549
|
+
parsed: params.parsed,
|
|
550
|
+
pluginInteractionData: params.pluginInteractionData,
|
|
551
|
+
respond: params.respond
|
|
552
|
+
})) return true;
|
|
553
|
+
const pluginResult = await dispatchSlackPluginInteractiveHandler({
|
|
554
|
+
data: params.pluginInteractionData,
|
|
555
|
+
interactionId: pluginInteractionId,
|
|
556
|
+
ctx: {
|
|
557
|
+
accountId: params.ctx.accountId,
|
|
558
|
+
interactionId: pluginInteractionId,
|
|
559
|
+
conversationId: params.parsed.channelId ?? "",
|
|
560
|
+
parentConversationId: void 0,
|
|
561
|
+
threadId: params.parsed.threadTs,
|
|
562
|
+
senderId: params.parsed.userId,
|
|
563
|
+
senderUsername: void 0,
|
|
564
|
+
auth: params.auth,
|
|
565
|
+
interaction: {
|
|
566
|
+
kind: params.parsed.actionSummary.actionType === "button" ? "button" : "select",
|
|
567
|
+
actionId: params.parsed.actionId,
|
|
568
|
+
blockId: params.parsed.blockId,
|
|
569
|
+
messageTs: params.parsed.messageTs,
|
|
570
|
+
threadTs: params.parsed.threadTs,
|
|
571
|
+
value: params.parsed.actionSummary.value,
|
|
572
|
+
selectedValues: params.parsed.actionSummary.selectedValues,
|
|
573
|
+
selectedLabels: params.parsed.actionSummary.selectedLabels,
|
|
574
|
+
triggerId: params.parsed.typedBody.trigger_id,
|
|
575
|
+
responseUrl: params.parsed.typedBody.response_url
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
respond: {
|
|
579
|
+
acknowledge: async () => {},
|
|
580
|
+
reply: async ({ text, responseType }) => {
|
|
581
|
+
if (!text) return;
|
|
582
|
+
await params.respond?.({
|
|
583
|
+
text,
|
|
584
|
+
response_type: responseType ?? "ephemeral"
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
followUp: async ({ text, responseType }) => {
|
|
588
|
+
if (!text) return;
|
|
589
|
+
await params.respond?.({
|
|
590
|
+
text,
|
|
591
|
+
response_type: responseType ?? "ephemeral"
|
|
592
|
+
});
|
|
593
|
+
},
|
|
594
|
+
editMessage: async ({ text, blocks }) => {
|
|
595
|
+
await updateSlackInteractionMessage({
|
|
596
|
+
ctx: params.ctx,
|
|
597
|
+
channelId: params.parsed.channelId,
|
|
598
|
+
messageTs: params.parsed.messageTs,
|
|
599
|
+
text: text ?? params.parsed.typedBody.message?.text ?? "",
|
|
600
|
+
blocks: Array.isArray(blocks) ? blocks : void 0
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
return pluginResult.matched && pluginResult.handled;
|
|
606
|
+
}
|
|
607
|
+
async function resolveSlackBlockActionCommandAuthorized(params) {
|
|
608
|
+
const commandsAllowFrom = params.ctx.cfg.commands?.allowFrom;
|
|
609
|
+
if (commandsAllowFrom != null && typeof commandsAllowFrom === "object" && (Array.isArray(commandsAllowFrom.slack) || Array.isArray(commandsAllowFrom["*"]))) return resolveCommandAuthorization({
|
|
610
|
+
ctx: {
|
|
611
|
+
Provider: "slack",
|
|
612
|
+
Surface: "slack",
|
|
613
|
+
OriginatingChannel: "slack",
|
|
614
|
+
AccountId: params.ctx.accountId,
|
|
615
|
+
ChatType: params.auth.channelType === "im" ? "direct" : "group",
|
|
616
|
+
From: params.parsed.channelId ? `slack:${params.parsed.channelId}` : "slack",
|
|
617
|
+
SenderId: params.parsed.userId
|
|
618
|
+
},
|
|
619
|
+
cfg: params.ctx.cfg,
|
|
620
|
+
commandAuthorized: false
|
|
621
|
+
}).isAuthorizedSender;
|
|
622
|
+
const isDirectMessage = params.auth.channelType === "im";
|
|
623
|
+
const isRoom = params.auth.channelType === "channel" || params.auth.channelType === "group";
|
|
624
|
+
const allowFromLower = await resolveSlackEffectiveAllowFrom(params.ctx, { includePairingStore: isDirectMessage });
|
|
625
|
+
const senderName = (await params.ctx.resolveUserName(params.parsed.userId).catch(() => void 0))?.name;
|
|
626
|
+
let channelUsers = [];
|
|
627
|
+
if (isRoom && params.parsed.channelId) {
|
|
628
|
+
const channelConfig = resolveSlackChannelConfig({
|
|
629
|
+
channelId: params.parsed.channelId,
|
|
630
|
+
channelName: params.auth.channelName,
|
|
631
|
+
channels: params.ctx.channelsConfig,
|
|
632
|
+
channelKeys: params.ctx.channelsConfigKeys,
|
|
633
|
+
defaultRequireMention: params.ctx.defaultRequireMention,
|
|
634
|
+
allowNameMatching: params.ctx.allowNameMatching
|
|
635
|
+
});
|
|
636
|
+
channelUsers = Array.isArray(channelConfig?.users) ? channelConfig.users : [];
|
|
637
|
+
}
|
|
638
|
+
return (await resolveSlackCommandIngress({
|
|
639
|
+
ctx: params.ctx,
|
|
640
|
+
senderId: params.parsed.userId,
|
|
641
|
+
senderName,
|
|
642
|
+
channelType: params.auth.channelType ?? "channel",
|
|
643
|
+
channelId: params.parsed.channelId ?? "slack-interaction",
|
|
644
|
+
ownerAllowFromLower: allowFromLower,
|
|
645
|
+
channelUsers,
|
|
646
|
+
allowTextCommands: false,
|
|
647
|
+
hasControlCommand: true,
|
|
648
|
+
eventKind: "button",
|
|
649
|
+
modeWhenAccessGroupsOff: "configured"
|
|
650
|
+
})).commandAccess.authorized;
|
|
651
|
+
}
|
|
652
|
+
function enqueueSlackBlockActionEvent(params) {
|
|
653
|
+
const eventPayload = {
|
|
654
|
+
interactionType: "block_action",
|
|
655
|
+
actionId: params.parsed.actionId,
|
|
656
|
+
blockId: params.parsed.blockId,
|
|
657
|
+
...params.parsed.actionSummary,
|
|
658
|
+
userId: params.parsed.userId,
|
|
659
|
+
teamId: params.parsed.typedBody.team?.id,
|
|
660
|
+
triggerId: params.parsed.typedBody.trigger_id,
|
|
661
|
+
responseUrl: params.parsed.typedBody.response_url,
|
|
662
|
+
channelId: params.parsed.channelId,
|
|
663
|
+
messageTs: params.parsed.messageTs,
|
|
664
|
+
threadTs: params.parsed.threadTs
|
|
665
|
+
};
|
|
666
|
+
params.ctx.runtime.log?.(`slack:interaction action=${params.parsed.actionId} type=${params.parsed.actionSummary.actionType ?? "unknown"} user=${params.parsed.userId} channel=${params.parsed.channelId}`);
|
|
667
|
+
const sessionKey = params.ctx.resolveSlackSystemEventSessionKey({
|
|
668
|
+
channelId: params.parsed.channelId,
|
|
669
|
+
channelType: params.auth.channelType,
|
|
670
|
+
senderId: params.parsed.userId,
|
|
671
|
+
threadTs: params.parsed.threadTs
|
|
672
|
+
});
|
|
673
|
+
const contextParts = [
|
|
674
|
+
"slack:interaction",
|
|
675
|
+
params.parsed.channelId,
|
|
676
|
+
params.parsed.messageTs,
|
|
677
|
+
params.parsed.actionId
|
|
678
|
+
].filter(Boolean);
|
|
679
|
+
if (enqueueSystemEvent(params.formatSystemEvent(eventPayload), {
|
|
680
|
+
sessionKey,
|
|
681
|
+
contextKey: contextParts.join(":"),
|
|
682
|
+
deliveryContext: {
|
|
683
|
+
channel: "slack",
|
|
684
|
+
to: params.auth.channelType === "im" ? `user:${params.parsed.userId}` : params.parsed.channelId ? `channel:${params.parsed.channelId}` : void 0,
|
|
685
|
+
accountId: params.ctx.accountId,
|
|
686
|
+
threadId: params.parsed.threadTs
|
|
687
|
+
},
|
|
688
|
+
trusted: false
|
|
689
|
+
})) requestHeartbeat({
|
|
690
|
+
source: "hook",
|
|
691
|
+
intent: "immediate",
|
|
692
|
+
reason: "hook:slack-interaction",
|
|
693
|
+
sessionKey,
|
|
694
|
+
heartbeat: { target: "last" }
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function buildSlackConfirmationBlocks(params) {
|
|
698
|
+
const selectedLabel = formatInteractionSelectionLabel({
|
|
699
|
+
actionId: params.parsed.actionId,
|
|
700
|
+
summary: params.parsed.actionSummary,
|
|
701
|
+
buttonText: params.parsed.typedActionWithText.text?.text
|
|
702
|
+
});
|
|
703
|
+
let updatedBlocks = params.originalBlocks.map((block) => {
|
|
704
|
+
const typedBlock = block;
|
|
705
|
+
if (typedBlock.type === "actions" && typedBlock.block_id === params.parsed.blockId) return {
|
|
706
|
+
type: "context",
|
|
707
|
+
elements: [{
|
|
708
|
+
type: "mrkdwn",
|
|
709
|
+
text: formatInteractionConfirmationText({
|
|
710
|
+
selectedLabel,
|
|
711
|
+
userId: params.parsed.userId
|
|
712
|
+
})
|
|
713
|
+
}]
|
|
714
|
+
};
|
|
715
|
+
return block;
|
|
716
|
+
});
|
|
717
|
+
if (!updatedBlocks.some((block) => {
|
|
718
|
+
const typedBlock = block;
|
|
719
|
+
return typedBlock.type === "actions" && !isBulkActionsBlock(typedBlock);
|
|
720
|
+
})) updatedBlocks = updatedBlocks.filter((block, index) => {
|
|
721
|
+
const typedBlock = block;
|
|
722
|
+
if (isBulkActionsBlock(typedBlock)) return false;
|
|
723
|
+
if (typedBlock.type !== "divider") return true;
|
|
724
|
+
const next = updatedBlocks[index + 1];
|
|
725
|
+
return !next || !isBulkActionsBlock(next);
|
|
726
|
+
});
|
|
727
|
+
return updatedBlocks;
|
|
728
|
+
}
|
|
729
|
+
async function updateSlackLegacyBlockAction(params) {
|
|
730
|
+
const originalBlocks = params.parsed.typedBody.message?.blocks;
|
|
731
|
+
if (!Array.isArray(originalBlocks) || !params.parsed.channelId || !params.parsed.messageTs || !params.parsed.blockId) return;
|
|
732
|
+
try {
|
|
733
|
+
await updateSlackInteractionMessage({
|
|
734
|
+
ctx: params.ctx,
|
|
735
|
+
channelId: params.parsed.channelId,
|
|
736
|
+
messageTs: params.parsed.messageTs,
|
|
737
|
+
text: params.parsed.typedBody.message?.text ?? "",
|
|
738
|
+
blocks: buildSlackConfirmationBlocks({
|
|
739
|
+
parsed: params.parsed,
|
|
740
|
+
originalBlocks
|
|
741
|
+
})
|
|
742
|
+
});
|
|
743
|
+
} catch {
|
|
744
|
+
await respondEphemeral(params.respond, `Button "${params.parsed.actionId}" clicked!`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function handleSlackBlockAction(params) {
|
|
748
|
+
const { ack, body, action, respond } = params.args;
|
|
749
|
+
await ack();
|
|
750
|
+
if (params.ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
751
|
+
params.ctx.runtime.log?.("slack:interaction drop block action payload (mismatched app/team)");
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const parsed = parseSlackBlockAction({
|
|
755
|
+
body,
|
|
756
|
+
action,
|
|
757
|
+
log: params.ctx.runtime.log
|
|
758
|
+
});
|
|
759
|
+
if (!parsed) return;
|
|
760
|
+
params.trackEvent?.();
|
|
761
|
+
const pluginInteractionData = buildSlackPluginInteractionData({
|
|
762
|
+
actionId: parsed.actionId,
|
|
763
|
+
summary: parsed.actionSummary
|
|
764
|
+
});
|
|
765
|
+
if (pluginInteractionData && isSlackReplyActionId(parsed.actionId)) {
|
|
766
|
+
if (await handleSlackExecApprovalInteraction({
|
|
767
|
+
ctx: params.ctx,
|
|
768
|
+
parsed,
|
|
769
|
+
pluginInteractionData,
|
|
770
|
+
respond
|
|
771
|
+
})) return;
|
|
772
|
+
}
|
|
773
|
+
const auth = await authorizeSlackBlockAction({
|
|
774
|
+
ctx: params.ctx,
|
|
775
|
+
parsed,
|
|
776
|
+
respond
|
|
777
|
+
});
|
|
778
|
+
if (!auth.allowed) return;
|
|
779
|
+
if (pluginInteractionData && isSlackReplyActionId(parsed.actionId)) {
|
|
780
|
+
if (await handleSlackPluginBindingApproval({
|
|
781
|
+
ctx: params.ctx,
|
|
782
|
+
parsed,
|
|
783
|
+
pluginInteractionData,
|
|
784
|
+
respond
|
|
785
|
+
})) return;
|
|
786
|
+
} else if (pluginInteractionData) {
|
|
787
|
+
const isAuthorizedSender = await resolveSlackBlockActionCommandAuthorized({
|
|
788
|
+
ctx: params.ctx,
|
|
789
|
+
parsed,
|
|
790
|
+
auth
|
|
791
|
+
});
|
|
792
|
+
if (await dispatchSlackPluginInteraction({
|
|
793
|
+
ctx: params.ctx,
|
|
794
|
+
parsed,
|
|
795
|
+
pluginInteractionData,
|
|
796
|
+
auth: { isAuthorizedSender },
|
|
797
|
+
respond
|
|
798
|
+
})) return;
|
|
799
|
+
}
|
|
800
|
+
enqueueSlackBlockActionEvent({
|
|
801
|
+
ctx: params.ctx,
|
|
802
|
+
parsed,
|
|
803
|
+
auth,
|
|
804
|
+
formatSystemEvent: params.formatSystemEvent
|
|
805
|
+
});
|
|
806
|
+
await updateSlackLegacyBlockAction({
|
|
807
|
+
ctx: params.ctx,
|
|
808
|
+
parsed,
|
|
809
|
+
respond
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
function registerSlackBlockActionHandler(params) {
|
|
813
|
+
if (typeof params.ctx.app.action !== "function") return;
|
|
814
|
+
params.ctx.app.action(/.+/, async (args) => {
|
|
815
|
+
await handleSlackBlockAction({
|
|
816
|
+
ctx: params.ctx,
|
|
817
|
+
trackEvent: params.trackEvent,
|
|
818
|
+
args,
|
|
819
|
+
formatSystemEvent: params.formatSystemEvent
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region extensions/slack/src/modal-metadata.ts
|
|
825
|
+
function parseSlackModalPrivateMetadata(raw) {
|
|
826
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return {};
|
|
827
|
+
try {
|
|
828
|
+
const parsed = JSON.parse(raw);
|
|
829
|
+
return {
|
|
830
|
+
sessionKey: normalizeOptionalString(parsed.sessionKey),
|
|
831
|
+
channelId: normalizeOptionalString(parsed.channelId),
|
|
832
|
+
channelType: normalizeOptionalString(parsed.channelType),
|
|
833
|
+
userId: normalizeOptionalString(parsed.userId)
|
|
834
|
+
};
|
|
835
|
+
} catch {
|
|
836
|
+
return {};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region extensions/slack/src/monitor/events/interactions.modal.ts
|
|
841
|
+
function resolveModalSessionRouting(params) {
|
|
842
|
+
const metadata = params.metadata;
|
|
843
|
+
if (metadata.sessionKey) return {
|
|
844
|
+
sessionKey: metadata.sessionKey,
|
|
845
|
+
channelId: metadata.channelId,
|
|
846
|
+
channelType: metadata.channelType
|
|
847
|
+
};
|
|
848
|
+
if (metadata.channelId) return {
|
|
849
|
+
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({
|
|
850
|
+
channelId: metadata.channelId,
|
|
851
|
+
channelType: metadata.channelType,
|
|
852
|
+
senderId: params.userId
|
|
853
|
+
}),
|
|
854
|
+
channelId: metadata.channelId,
|
|
855
|
+
channelType: metadata.channelType
|
|
856
|
+
};
|
|
857
|
+
return { sessionKey: params.ctx.resolveSlackSystemEventSessionKey({}) };
|
|
858
|
+
}
|
|
859
|
+
function summarizeSlackViewLifecycleContext(view) {
|
|
860
|
+
const rootViewId = view.root_view_id;
|
|
861
|
+
const previousViewId = view.previous_view_id;
|
|
862
|
+
return {
|
|
863
|
+
rootViewId,
|
|
864
|
+
previousViewId,
|
|
865
|
+
externalId: view.external_id,
|
|
866
|
+
viewHash: view.hash,
|
|
867
|
+
isStackedView: Boolean(previousViewId)
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function resolveSlackModalEventBase(params) {
|
|
871
|
+
const metadata = parseSlackModalPrivateMetadata(params.body.view?.private_metadata);
|
|
872
|
+
const callbackId = params.body.view?.callback_id ?? "unknown";
|
|
873
|
+
const userId = params.body.user?.id ?? "unknown";
|
|
874
|
+
const viewId = params.body.view?.id;
|
|
875
|
+
const inputs = params.summarizeViewState(params.body.view?.state?.values);
|
|
876
|
+
const sessionRouting = resolveModalSessionRouting({
|
|
877
|
+
ctx: params.ctx,
|
|
878
|
+
metadata,
|
|
879
|
+
userId
|
|
880
|
+
});
|
|
881
|
+
return {
|
|
882
|
+
callbackId,
|
|
883
|
+
userId,
|
|
884
|
+
expectedUserId: metadata.userId,
|
|
885
|
+
viewId,
|
|
886
|
+
sessionRouting,
|
|
887
|
+
payload: {
|
|
888
|
+
actionId: `view:${callbackId}`,
|
|
889
|
+
callbackId,
|
|
890
|
+
viewId,
|
|
891
|
+
userId,
|
|
892
|
+
teamId: params.body.team?.id,
|
|
893
|
+
...summarizeSlackViewLifecycleContext({
|
|
894
|
+
root_view_id: params.body.view?.root_view_id,
|
|
895
|
+
previous_view_id: params.body.view?.previous_view_id,
|
|
896
|
+
external_id: params.body.view?.external_id,
|
|
897
|
+
hash: params.body.view?.hash
|
|
898
|
+
}),
|
|
899
|
+
privateMetadata: params.body.view?.private_metadata,
|
|
900
|
+
routedChannelId: sessionRouting.channelId,
|
|
901
|
+
routedChannelType: sessionRouting.channelType,
|
|
902
|
+
inputs
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
async function emitSlackModalLifecycleEvent(params) {
|
|
907
|
+
const { callbackId, userId, expectedUserId, viewId, sessionRouting, payload } = resolveSlackModalEventBase({
|
|
908
|
+
ctx: params.ctx,
|
|
909
|
+
body: params.body,
|
|
910
|
+
summarizeViewState: params.summarizeViewState
|
|
911
|
+
});
|
|
912
|
+
const isViewClosed = params.interactionType === "view_closed";
|
|
913
|
+
const isCleared = params.body.is_cleared === true;
|
|
914
|
+
const eventPayload = isViewClosed ? {
|
|
915
|
+
interactionType: params.interactionType,
|
|
916
|
+
...payload,
|
|
917
|
+
isCleared
|
|
918
|
+
} : {
|
|
919
|
+
interactionType: params.interactionType,
|
|
920
|
+
...payload
|
|
921
|
+
};
|
|
922
|
+
if (isViewClosed) params.ctx.runtime.log?.(`slack:interaction view_closed callback=${callbackId} user=${userId} cleared=${isCleared}`);
|
|
923
|
+
else params.ctx.runtime.log?.(`slack:interaction view_submission callback=${callbackId} user=${userId} inputs=${payload.inputs.length}`);
|
|
924
|
+
if (!expectedUserId) {
|
|
925
|
+
params.ctx.runtime.log?.(`slack:interaction drop modal callback=${callbackId} user=${userId} reason=missing-expected-user`);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const auth = await authorizeSlackSystemEventSender({
|
|
929
|
+
ctx: params.ctx,
|
|
930
|
+
senderId: userId,
|
|
931
|
+
channelId: sessionRouting.channelId,
|
|
932
|
+
channelType: sessionRouting.channelType,
|
|
933
|
+
expectedSenderId: expectedUserId,
|
|
934
|
+
interactiveEvent: true
|
|
935
|
+
});
|
|
936
|
+
if (!auth.allowed) {
|
|
937
|
+
params.ctx.runtime.log?.(`slack:interaction drop modal callback=${callbackId} user=${userId} reason=${auth.reason ?? "unauthorized"}`);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
enqueueSystemEvent(params.formatSystemEvent(eventPayload), {
|
|
941
|
+
sessionKey: sessionRouting.sessionKey,
|
|
942
|
+
contextKey: [
|
|
943
|
+
params.contextPrefix,
|
|
944
|
+
callbackId,
|
|
945
|
+
viewId,
|
|
946
|
+
userId
|
|
947
|
+
].filter(Boolean).join(":")
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function registerModalLifecycleHandler(params) {
|
|
951
|
+
params.register(params.matcher, async ({ ack, body }) => {
|
|
952
|
+
await ack();
|
|
953
|
+
if (params.ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
954
|
+
params.ctx.runtime.log?.(`slack:interaction drop ${params.interactionType} payload (mismatched app/team)`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
params.trackEvent?.();
|
|
958
|
+
await emitSlackModalLifecycleEvent({
|
|
959
|
+
ctx: params.ctx,
|
|
960
|
+
body,
|
|
961
|
+
interactionType: params.interactionType,
|
|
962
|
+
contextPrefix: params.contextPrefix,
|
|
963
|
+
summarizeViewState: params.summarizeViewState,
|
|
964
|
+
formatSystemEvent: params.formatSystemEvent
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region extensions/slack/src/monitor/events/interactions.ts
|
|
970
|
+
const OPENCLAW_ACTION_PREFIX = "openclaw:";
|
|
971
|
+
const SLACK_INTERACTION_EVENT_PREFIX = "Slack interaction: ";
|
|
972
|
+
const REDACTED_INTERACTION_VALUE = "[redacted]";
|
|
973
|
+
const SLACK_INTERACTION_EVENT_MAX_CHARS = 2400;
|
|
974
|
+
const SLACK_INTERACTION_STRING_MAX_CHARS = 160;
|
|
975
|
+
const SLACK_INTERACTION_ARRAY_MAX_ITEMS = 64;
|
|
976
|
+
const SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS = 3;
|
|
977
|
+
const SLACK_INTERACTION_REDACTED_KEYS = new Set([
|
|
978
|
+
"triggerId",
|
|
979
|
+
"responseUrl",
|
|
980
|
+
"workflowTriggerUrl",
|
|
981
|
+
"privateMetadata",
|
|
982
|
+
"viewHash"
|
|
983
|
+
]);
|
|
984
|
+
function sanitizeSlackInteractionPayloadValue(value, key) {
|
|
985
|
+
if (value === void 0) return;
|
|
986
|
+
if (key && SLACK_INTERACTION_REDACTED_KEYS.has(key)) {
|
|
987
|
+
if (typeof value !== "string" || value.trim().length === 0) return;
|
|
988
|
+
return REDACTED_INTERACTION_VALUE;
|
|
989
|
+
}
|
|
990
|
+
if (typeof value === "string") return truncateSlackText(value, SLACK_INTERACTION_STRING_MAX_CHARS);
|
|
991
|
+
if (Array.isArray(value)) {
|
|
992
|
+
const sanitized = value.slice(0, SLACK_INTERACTION_ARRAY_MAX_ITEMS).map((entry) => sanitizeSlackInteractionPayloadValue(entry)).filter((entry) => entry !== void 0);
|
|
993
|
+
if (value.length > SLACK_INTERACTION_ARRAY_MAX_ITEMS) sanitized.push(`…+${value.length - SLACK_INTERACTION_ARRAY_MAX_ITEMS} more`);
|
|
994
|
+
return sanitized;
|
|
995
|
+
}
|
|
996
|
+
if (!value || typeof value !== "object") return value;
|
|
997
|
+
const output = {};
|
|
998
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
999
|
+
const sanitized = sanitizeSlackInteractionPayloadValue(entryValue, entryKey);
|
|
1000
|
+
if (sanitized === void 0) continue;
|
|
1001
|
+
if (typeof sanitized === "string" && sanitized.length === 0) continue;
|
|
1002
|
+
if (Array.isArray(sanitized) && sanitized.length === 0) continue;
|
|
1003
|
+
output[entryKey] = sanitized;
|
|
1004
|
+
}
|
|
1005
|
+
return output;
|
|
1006
|
+
}
|
|
1007
|
+
function buildCompactSlackInteractionPayload(payload) {
|
|
1008
|
+
const rawInputs = Array.isArray(payload.inputs) ? payload.inputs : [];
|
|
1009
|
+
const compactInputs = rawInputs.slice(0, SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS).flatMap((entry) => {
|
|
1010
|
+
if (!entry || typeof entry !== "object") return [];
|
|
1011
|
+
const typed = entry;
|
|
1012
|
+
return [{
|
|
1013
|
+
actionId: typed.actionId,
|
|
1014
|
+
blockId: typed.blockId,
|
|
1015
|
+
actionType: typed.actionType,
|
|
1016
|
+
inputKind: typed.inputKind,
|
|
1017
|
+
selectedValues: typed.selectedValues,
|
|
1018
|
+
selectedLabels: typed.selectedLabels,
|
|
1019
|
+
inputValue: typed.inputValue,
|
|
1020
|
+
inputNumber: typed.inputNumber,
|
|
1021
|
+
selectedDate: typed.selectedDate,
|
|
1022
|
+
selectedTime: typed.selectedTime,
|
|
1023
|
+
selectedDateTime: typed.selectedDateTime,
|
|
1024
|
+
richTextPreview: typed.richTextPreview
|
|
1025
|
+
}];
|
|
1026
|
+
});
|
|
1027
|
+
return {
|
|
1028
|
+
interactionType: payload.interactionType,
|
|
1029
|
+
actionId: payload.actionId,
|
|
1030
|
+
callbackId: payload.callbackId,
|
|
1031
|
+
actionType: payload.actionType,
|
|
1032
|
+
userId: payload.userId,
|
|
1033
|
+
teamId: payload.teamId,
|
|
1034
|
+
channelId: payload.channelId ?? payload.routedChannelId,
|
|
1035
|
+
messageTs: payload.messageTs,
|
|
1036
|
+
threadTs: payload.threadTs,
|
|
1037
|
+
viewId: payload.viewId,
|
|
1038
|
+
isCleared: payload.isCleared,
|
|
1039
|
+
selectedValues: payload.selectedValues,
|
|
1040
|
+
selectedLabels: payload.selectedLabels,
|
|
1041
|
+
selectedDate: payload.selectedDate,
|
|
1042
|
+
selectedTime: payload.selectedTime,
|
|
1043
|
+
selectedDateTime: payload.selectedDateTime,
|
|
1044
|
+
workflowId: payload.workflowId,
|
|
1045
|
+
routedChannelType: payload.routedChannelType,
|
|
1046
|
+
inputs: compactInputs.length > 0 ? compactInputs : void 0,
|
|
1047
|
+
inputsOmitted: rawInputs.length > SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS ? rawInputs.length - SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS : void 0,
|
|
1048
|
+
payloadTruncated: true
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function formatSlackInteractionSystemEvent(payload) {
|
|
1052
|
+
const toEventText = (value) => `${SLACK_INTERACTION_EVENT_PREFIX}${JSON.stringify(value)}`;
|
|
1053
|
+
const sanitizedPayload = sanitizeSlackInteractionPayloadValue(payload) ?? {};
|
|
1054
|
+
let eventText = toEventText(sanitizedPayload);
|
|
1055
|
+
if (eventText.length <= SLACK_INTERACTION_EVENT_MAX_CHARS) return eventText;
|
|
1056
|
+
eventText = toEventText(sanitizeSlackInteractionPayloadValue(buildCompactSlackInteractionPayload(sanitizedPayload)));
|
|
1057
|
+
if (eventText.length <= SLACK_INTERACTION_EVENT_MAX_CHARS) return eventText;
|
|
1058
|
+
return toEventText({
|
|
1059
|
+
interactionType: sanitizedPayload.interactionType,
|
|
1060
|
+
actionId: sanitizedPayload.actionId ?? "unknown",
|
|
1061
|
+
userId: sanitizedPayload.userId,
|
|
1062
|
+
channelId: sanitizedPayload.channelId ?? sanitizedPayload.routedChannelId,
|
|
1063
|
+
payloadTruncated: true
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
function summarizeViewState(values) {
|
|
1067
|
+
if (!values || typeof values !== "object") return [];
|
|
1068
|
+
const entries = [];
|
|
1069
|
+
for (const [blockId, blockValue] of Object.entries(values)) {
|
|
1070
|
+
if (!blockValue || typeof blockValue !== "object") continue;
|
|
1071
|
+
for (const [actionId, rawAction] of Object.entries(blockValue)) {
|
|
1072
|
+
if (!rawAction || typeof rawAction !== "object") continue;
|
|
1073
|
+
const actionSummary = summarizeAction(rawAction);
|
|
1074
|
+
entries.push({
|
|
1075
|
+
blockId,
|
|
1076
|
+
actionId,
|
|
1077
|
+
...actionSummary
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return entries;
|
|
1082
|
+
}
|
|
1083
|
+
function registerSlackInteractionEvents(params) {
|
|
1084
|
+
const { ctx, trackEvent } = params;
|
|
1085
|
+
registerSlackBlockActionHandler({
|
|
1086
|
+
ctx,
|
|
1087
|
+
trackEvent,
|
|
1088
|
+
formatSystemEvent: formatSlackInteractionSystemEvent
|
|
1089
|
+
});
|
|
1090
|
+
if (typeof ctx.app.view !== "function") return;
|
|
1091
|
+
const modalMatcher = new RegExp(`^${OPENCLAW_ACTION_PREFIX}`);
|
|
1092
|
+
registerModalLifecycleHandler({
|
|
1093
|
+
register: (matcher, handler) => ctx.app.view(matcher, handler),
|
|
1094
|
+
matcher: modalMatcher,
|
|
1095
|
+
ctx,
|
|
1096
|
+
trackEvent,
|
|
1097
|
+
interactionType: "view_submission",
|
|
1098
|
+
contextPrefix: "slack:interaction:view",
|
|
1099
|
+
summarizeViewState,
|
|
1100
|
+
formatSystemEvent: formatSlackInteractionSystemEvent
|
|
1101
|
+
});
|
|
1102
|
+
const viewClosed = ctx.app.viewClosed;
|
|
1103
|
+
if (typeof viewClosed !== "function") return;
|
|
1104
|
+
registerModalLifecycleHandler({
|
|
1105
|
+
register: viewClosed,
|
|
1106
|
+
matcher: modalMatcher,
|
|
1107
|
+
ctx,
|
|
1108
|
+
trackEvent,
|
|
1109
|
+
interactionType: "view_closed",
|
|
1110
|
+
contextPrefix: "slack:interaction:view-closed",
|
|
1111
|
+
summarizeViewState,
|
|
1112
|
+
formatSystemEvent: formatSlackInteractionSystemEvent
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region extensions/slack/src/monitor/events/system-event-context.ts
|
|
1117
|
+
async function authorizeAndResolveSlackSystemEventContext(params) {
|
|
1118
|
+
const { ctx, senderId, channelId, channelType, eventKind } = params;
|
|
1119
|
+
const auth = await authorizeSlackSystemEventSender({
|
|
1120
|
+
ctx,
|
|
1121
|
+
senderId,
|
|
1122
|
+
channelId,
|
|
1123
|
+
channelType
|
|
1124
|
+
});
|
|
1125
|
+
if (!auth.allowed) {
|
|
1126
|
+
logVerbose(`slack: drop ${eventKind} sender ${senderId ?? "unknown"} channel=${channelId ?? "unknown"} reason=${auth.reason ?? "unauthorized"}`);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
channelLabel: resolveSlackChannelLabel({
|
|
1131
|
+
channelId,
|
|
1132
|
+
channelName: auth.channelName
|
|
1133
|
+
}),
|
|
1134
|
+
sessionKey: ctx.resolveSlackSystemEventSessionKey({
|
|
1135
|
+
channelId,
|
|
1136
|
+
channelType: auth.channelType,
|
|
1137
|
+
senderId
|
|
1138
|
+
})
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
//#endregion
|
|
1142
|
+
//#region extensions/slack/src/monitor/events/members.ts
|
|
1143
|
+
function registerSlackMemberEvents(params) {
|
|
1144
|
+
const { ctx, trackEvent } = params;
|
|
1145
|
+
const handleMemberChannelEvent = async (params) => {
|
|
1146
|
+
try {
|
|
1147
|
+
if (ctx.shouldDropMismatchedSlackEvent(params.body)) return;
|
|
1148
|
+
trackEvent?.();
|
|
1149
|
+
const payload = params.event;
|
|
1150
|
+
const channelId = payload.channel;
|
|
1151
|
+
const channelInfo = channelId ? await ctx.resolveChannelName(channelId) : {};
|
|
1152
|
+
const channelType = payload.channel_type ?? channelInfo?.type;
|
|
1153
|
+
const ingressContext = await authorizeAndResolveSlackSystemEventContext({
|
|
1154
|
+
ctx,
|
|
1155
|
+
senderId: payload.user,
|
|
1156
|
+
channelId,
|
|
1157
|
+
channelType,
|
|
1158
|
+
eventKind: `member-${params.verb}`
|
|
1159
|
+
});
|
|
1160
|
+
if (!ingressContext) return;
|
|
1161
|
+
enqueueSystemEvent(`Slack: ${(payload.user ? await ctx.resolveUserName(payload.user) : {})?.name ?? payload.user ?? "someone"} ${params.verb} ${ingressContext.channelLabel}.`, {
|
|
1162
|
+
sessionKey: ingressContext.sessionKey,
|
|
1163
|
+
contextKey: `slack:member:${params.verb}:${channelId ?? "unknown"}:${payload.user ?? "unknown"}`
|
|
1164
|
+
});
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
ctx.runtime.error?.(danger(`slack ${params.verb} handler failed: ${formatErrorMessage(err)}`));
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
ctx.app.event("member_joined_channel", async ({ event, body }) => {
|
|
1170
|
+
await handleMemberChannelEvent({
|
|
1171
|
+
verb: "joined",
|
|
1172
|
+
event,
|
|
1173
|
+
body
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
ctx.app.event("member_left_channel", async ({ event, body }) => {
|
|
1177
|
+
await handleMemberChannelEvent({
|
|
1178
|
+
verb: "left",
|
|
1179
|
+
event,
|
|
1180
|
+
body
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
//#endregion
|
|
1185
|
+
//#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
|
|
1186
|
+
const SUBTYPE_HANDLER_REGISTRY = {
|
|
1187
|
+
message_changed: {
|
|
1188
|
+
subtype: "message_changed",
|
|
1189
|
+
eventKind: "message_changed",
|
|
1190
|
+
describe: (channelLabel) => `Slack message edited in ${channelLabel}.`,
|
|
1191
|
+
contextKey: (event) => {
|
|
1192
|
+
const changed = event;
|
|
1193
|
+
return `slack:message:changed:${changed.channel ?? "unknown"}:${changed.message?.ts ?? changed.previous_message?.ts ?? changed.event_ts ?? "unknown"}`;
|
|
1194
|
+
},
|
|
1195
|
+
resolveSenderId: (event) => {
|
|
1196
|
+
const changed = event;
|
|
1197
|
+
return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id;
|
|
1198
|
+
},
|
|
1199
|
+
resolveChannelId: (event) => event.channel,
|
|
1200
|
+
resolveChannelType: () => void 0
|
|
1201
|
+
},
|
|
1202
|
+
message_deleted: {
|
|
1203
|
+
subtype: "message_deleted",
|
|
1204
|
+
eventKind: "message_deleted",
|
|
1205
|
+
describe: (channelLabel) => `Slack message deleted in ${channelLabel}.`,
|
|
1206
|
+
contextKey: (event) => {
|
|
1207
|
+
const deleted = event;
|
|
1208
|
+
return `slack:message:deleted:${deleted.channel ?? "unknown"}:${deleted.deleted_ts ?? deleted.event_ts ?? "unknown"}`;
|
|
1209
|
+
},
|
|
1210
|
+
resolveSenderId: (event) => {
|
|
1211
|
+
const deleted = event;
|
|
1212
|
+
return deleted.previous_message?.user ?? deleted.previous_message?.bot_id;
|
|
1213
|
+
},
|
|
1214
|
+
resolveChannelId: (event) => event.channel,
|
|
1215
|
+
resolveChannelType: () => void 0
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
function resolveSlackMessageSubtypeHandler(event) {
|
|
1219
|
+
const subtype = event.subtype;
|
|
1220
|
+
if (subtype !== "message_changed" && subtype !== "message_deleted") return;
|
|
1221
|
+
return SUBTYPE_HANDLER_REGISTRY[subtype];
|
|
1222
|
+
}
|
|
1223
|
+
//#endregion
|
|
1224
|
+
//#region extensions/slack/src/monitor/events/messages.ts
|
|
1225
|
+
function asRecord$1(value) {
|
|
1226
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1227
|
+
}
|
|
1228
|
+
function asString(value) {
|
|
1229
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1230
|
+
}
|
|
1231
|
+
function isSlackUserId(value) {
|
|
1232
|
+
return /^[UW][A-Z0-9]+$/.test(value);
|
|
1233
|
+
}
|
|
1234
|
+
function addUserCandidate(candidates, value, botUserId) {
|
|
1235
|
+
const id = asString(value);
|
|
1236
|
+
if (!id || id === botUserId || !isSlackUserId(id)) return;
|
|
1237
|
+
candidates.add(id);
|
|
1238
|
+
}
|
|
1239
|
+
function collectMetadataUserCandidates(candidates, value, botUserId) {
|
|
1240
|
+
const payload = asRecord$1(asRecord$1(value)?.event_payload);
|
|
1241
|
+
if (!payload) return;
|
|
1242
|
+
for (const key of [
|
|
1243
|
+
"user",
|
|
1244
|
+
"user_id",
|
|
1245
|
+
"actor_user_id",
|
|
1246
|
+
"author_user_id",
|
|
1247
|
+
"slack_user_id"
|
|
1248
|
+
]) addUserCandidate(candidates, payload[key], botUserId);
|
|
1249
|
+
}
|
|
1250
|
+
function resolveAssistantMessageChangedSender(params) {
|
|
1251
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1252
|
+
collectMetadataUserCandidates(candidates, params.message?.metadata, params.botUserId);
|
|
1253
|
+
return candidates.size === 1 ? [...candidates][0] : void 0;
|
|
1254
|
+
}
|
|
1255
|
+
function isSelfAttributedMessageChange(params) {
|
|
1256
|
+
const topUser = asString(params.event.user);
|
|
1257
|
+
const messageUser = asString(params.message?.user);
|
|
1258
|
+
const messageBotId = asString(params.message?.bot_id);
|
|
1259
|
+
return Boolean(params.ctx.botUserId) && (topUser === params.ctx.botUserId || messageUser === params.ctx.botUserId) || Boolean(params.ctx.botId) && messageBotId === params.ctx.botId;
|
|
1260
|
+
}
|
|
1261
|
+
function resolveAssistantMessageChangedInbound(params) {
|
|
1262
|
+
if (params.event.subtype !== "message_changed") return;
|
|
1263
|
+
const changed = params.event;
|
|
1264
|
+
const message = asRecord$1(changed.message);
|
|
1265
|
+
if (!message || !isSelfAttributedMessageChange({
|
|
1266
|
+
event: changed,
|
|
1267
|
+
message,
|
|
1268
|
+
ctx: params.ctx
|
|
1269
|
+
})) return;
|
|
1270
|
+
if (normalizeSlackChannelType(asString(changed.channel_type), changed.channel) !== "im") return;
|
|
1271
|
+
const senderId = resolveAssistantMessageChangedSender({
|
|
1272
|
+
message,
|
|
1273
|
+
botUserId: params.ctx.botUserId
|
|
1274
|
+
});
|
|
1275
|
+
if (!senderId) return;
|
|
1276
|
+
return {
|
|
1277
|
+
type: "message",
|
|
1278
|
+
channel: changed.channel ?? params.event.channel,
|
|
1279
|
+
channel_type: "im",
|
|
1280
|
+
user: senderId,
|
|
1281
|
+
text: asString(message.text),
|
|
1282
|
+
ts: asString(message.ts) ?? asString(changed.event_ts),
|
|
1283
|
+
thread_ts: asString(message.thread_ts),
|
|
1284
|
+
event_ts: changed.event_ts,
|
|
1285
|
+
files: Array.isArray(message.files) ? message.files : void 0,
|
|
1286
|
+
attachments: Array.isArray(message.attachments) ? message.attachments : void 0
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function registerSlackMessageEvents(params) {
|
|
1290
|
+
const { ctx, handleSlackMessage } = params;
|
|
1291
|
+
const handleIncomingMessageEvent = async ({ event, body }) => {
|
|
1292
|
+
try {
|
|
1293
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
1294
|
+
const message = event;
|
|
1295
|
+
const assistantChangedInbound = resolveAssistantMessageChangedInbound({
|
|
1296
|
+
event: message,
|
|
1297
|
+
ctx
|
|
1298
|
+
});
|
|
1299
|
+
if (assistantChangedInbound) {
|
|
1300
|
+
await handleSlackMessage(assistantChangedInbound, { source: "message" });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (message.subtype === "message_changed" && isSelfAttributedMessageChange({
|
|
1304
|
+
event: message,
|
|
1305
|
+
message: asRecord$1(message.message),
|
|
1306
|
+
ctx
|
|
1307
|
+
})) return;
|
|
1308
|
+
const subtypeHandler = resolveSlackMessageSubtypeHandler(message);
|
|
1309
|
+
if (subtypeHandler) {
|
|
1310
|
+
const channelId = subtypeHandler.resolveChannelId(message);
|
|
1311
|
+
const ingressContext = await authorizeAndResolveSlackSystemEventContext({
|
|
1312
|
+
ctx,
|
|
1313
|
+
senderId: subtypeHandler.resolveSenderId(message),
|
|
1314
|
+
channelId,
|
|
1315
|
+
channelType: subtypeHandler.resolveChannelType(message),
|
|
1316
|
+
eventKind: subtypeHandler.eventKind
|
|
1317
|
+
});
|
|
1318
|
+
if (!ingressContext) return;
|
|
1319
|
+
enqueueSystemEvent(subtypeHandler.describe(ingressContext.channelLabel), {
|
|
1320
|
+
sessionKey: ingressContext.sessionKey,
|
|
1321
|
+
contextKey: subtypeHandler.contextKey(message)
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
await handleSlackMessage(message, { source: "message" });
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
ctx.app.event("message", async ({ event, body }) => {
|
|
1331
|
+
await handleIncomingMessageEvent({
|
|
1332
|
+
event,
|
|
1333
|
+
body
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
ctx.app.event("app_mention", async ({ event, body }) => {
|
|
1337
|
+
try {
|
|
1338
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
1339
|
+
const mention = event;
|
|
1340
|
+
const channelType = normalizeSlackChannelType(mention.channel_type, mention.channel);
|
|
1341
|
+
if (channelType === "im" || channelType === "mpim") return;
|
|
1342
|
+
await handleSlackMessage(mention, {
|
|
1343
|
+
source: "app_mention",
|
|
1344
|
+
wasMentioned: true
|
|
1345
|
+
});
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region extensions/slack/src/monitor/events/pins.ts
|
|
1353
|
+
async function handleSlackPinEvent(params) {
|
|
1354
|
+
const { ctx, trackEvent, body, event, action, contextKeySuffix, errorLabel } = params;
|
|
1355
|
+
try {
|
|
1356
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
1357
|
+
trackEvent?.();
|
|
1358
|
+
const payload = event;
|
|
1359
|
+
const channelId = payload.channel_id;
|
|
1360
|
+
const ingressContext = await authorizeAndResolveSlackSystemEventContext({
|
|
1361
|
+
ctx,
|
|
1362
|
+
senderId: payload.user,
|
|
1363
|
+
channelId,
|
|
1364
|
+
eventKind: "pin"
|
|
1365
|
+
});
|
|
1366
|
+
if (!ingressContext) return;
|
|
1367
|
+
const userLabel = (payload.user ? await ctx.resolveUserName(payload.user) : {})?.name ?? payload.user ?? "someone";
|
|
1368
|
+
const itemType = payload.item?.type ?? "item";
|
|
1369
|
+
const messageId = payload.item?.message?.ts ?? payload.event_ts;
|
|
1370
|
+
enqueueSystemEvent(`Slack: ${userLabel} ${action} a ${itemType} in ${ingressContext.channelLabel}.`, {
|
|
1371
|
+
sessionKey: ingressContext.sessionKey,
|
|
1372
|
+
contextKey: `slack:pin:${contextKeySuffix}:${channelId ?? "unknown"}:${messageId ?? "unknown"}`
|
|
1373
|
+
});
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
ctx.runtime.error?.(danger(`slack ${errorLabel} handler failed: ${formatErrorMessage(err)}`));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function registerSlackPinEvents(params) {
|
|
1379
|
+
const { ctx, trackEvent } = params;
|
|
1380
|
+
ctx.app.event("pin_added", async ({ event, body }) => {
|
|
1381
|
+
await handleSlackPinEvent({
|
|
1382
|
+
ctx,
|
|
1383
|
+
trackEvent,
|
|
1384
|
+
body,
|
|
1385
|
+
event,
|
|
1386
|
+
action: "pinned",
|
|
1387
|
+
contextKeySuffix: "added",
|
|
1388
|
+
errorLabel: "pin added"
|
|
1389
|
+
});
|
|
1390
|
+
});
|
|
1391
|
+
ctx.app.event("pin_removed", async ({ event, body }) => {
|
|
1392
|
+
await handleSlackPinEvent({
|
|
1393
|
+
ctx,
|
|
1394
|
+
trackEvent,
|
|
1395
|
+
body,
|
|
1396
|
+
event,
|
|
1397
|
+
action: "unpinned",
|
|
1398
|
+
contextKeySuffix: "removed",
|
|
1399
|
+
errorLabel: "pin removed"
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
//#endregion
|
|
1404
|
+
//#region extensions/slack/src/monitor/events/reactions.ts
|
|
1405
|
+
function registerSlackReactionEvents(params) {
|
|
1406
|
+
const { ctx, trackEvent } = params;
|
|
1407
|
+
const handleReactionEvent = async (event, action) => {
|
|
1408
|
+
try {
|
|
1409
|
+
const item = event.item;
|
|
1410
|
+
if (!item || item.type !== "message") return;
|
|
1411
|
+
trackEvent?.();
|
|
1412
|
+
const ingressContext = await authorizeAndResolveSlackSystemEventContext({
|
|
1413
|
+
ctx,
|
|
1414
|
+
senderId: event.user,
|
|
1415
|
+
channelId: item.channel,
|
|
1416
|
+
eventKind: "reaction"
|
|
1417
|
+
});
|
|
1418
|
+
if (!ingressContext) return;
|
|
1419
|
+
const actorInfoPromise = event.user ? ctx.resolveUserName(event.user) : Promise.resolve(void 0);
|
|
1420
|
+
const authorInfoPromise = event.item_user ? ctx.resolveUserName(event.item_user) : Promise.resolve(void 0);
|
|
1421
|
+
const [actorInfo, authorInfo] = await Promise.all([actorInfoPromise, authorInfoPromise]);
|
|
1422
|
+
const actorLabel = actorInfo?.name ?? event.user;
|
|
1423
|
+
const emojiLabel = event.reaction ?? "emoji";
|
|
1424
|
+
const authorLabel = authorInfo?.name ?? event.item_user;
|
|
1425
|
+
const baseText = `Slack reaction ${action}: :${emojiLabel}: by ${actorLabel} in ${ingressContext.channelLabel} msg ${item.ts}`;
|
|
1426
|
+
enqueueSystemEvent(authorLabel ? `${baseText} from ${authorLabel}` : baseText, {
|
|
1427
|
+
sessionKey: ingressContext.sessionKey,
|
|
1428
|
+
contextKey: `slack:reaction:${action}:${item.channel}:${item.ts}:${event.user}:${emojiLabel}`
|
|
1429
|
+
});
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
ctx.runtime.error?.(danger(`slack reaction handler failed: ${formatErrorMessage(err)}`));
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
ctx.app.event("reaction_added", async ({ event, body }) => {
|
|
1435
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
1436
|
+
await handleReactionEvent(event, "added");
|
|
1437
|
+
});
|
|
1438
|
+
ctx.app.event("reaction_removed", async ({ event, body }) => {
|
|
1439
|
+
if (ctx.shouldDropMismatchedSlackEvent(body)) return;
|
|
1440
|
+
await handleReactionEvent(event, "removed");
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
//#endregion
|
|
1444
|
+
//#region extensions/slack/src/monitor/events.ts
|
|
1445
|
+
function registerSlackMonitorEvents(params) {
|
|
1446
|
+
registerSlackMessageEvents({
|
|
1447
|
+
ctx: params.ctx,
|
|
1448
|
+
handleSlackMessage: params.handleSlackMessage
|
|
1449
|
+
});
|
|
1450
|
+
registerSlackReactionEvents({
|
|
1451
|
+
ctx: params.ctx,
|
|
1452
|
+
trackEvent: params.trackEvent
|
|
1453
|
+
});
|
|
1454
|
+
registerSlackMemberEvents({
|
|
1455
|
+
ctx: params.ctx,
|
|
1456
|
+
trackEvent: params.trackEvent
|
|
1457
|
+
});
|
|
1458
|
+
registerSlackChannelEvents({
|
|
1459
|
+
ctx: params.ctx,
|
|
1460
|
+
trackEvent: params.trackEvent
|
|
1461
|
+
});
|
|
1462
|
+
registerSlackPinEvents({
|
|
1463
|
+
ctx: params.ctx,
|
|
1464
|
+
trackEvent: params.trackEvent
|
|
1465
|
+
});
|
|
1466
|
+
registerSlackHomeEvents({
|
|
1467
|
+
ctx: params.ctx,
|
|
1468
|
+
trackEvent: params.trackEvent
|
|
1469
|
+
});
|
|
1470
|
+
registerSlackInteractionEvents({
|
|
1471
|
+
ctx: params.ctx,
|
|
1472
|
+
trackEvent: params.trackEvent
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region extensions/slack/src/monitor/message-handler/debounce-key.ts
|
|
1477
|
+
function resolveSlackSenderId(message) {
|
|
1478
|
+
return message.user ?? message.bot_id ?? null;
|
|
1479
|
+
}
|
|
1480
|
+
function isSlackDirectMessageChannel(channelId) {
|
|
1481
|
+
return channelId.startsWith("D");
|
|
1482
|
+
}
|
|
1483
|
+
function isTopLevelSlackMessage(message) {
|
|
1484
|
+
return !message.thread_ts && !message.parent_user_id;
|
|
1485
|
+
}
|
|
1486
|
+
function buildTopLevelSlackConversationKey(message, accountId) {
|
|
1487
|
+
if (!isTopLevelSlackMessage(message)) return null;
|
|
1488
|
+
const senderId = resolveSlackSenderId(message);
|
|
1489
|
+
if (!senderId) return null;
|
|
1490
|
+
return `slack:${accountId}:${message.channel}:${senderId}`;
|
|
1491
|
+
}
|
|
1492
|
+
function buildSlackDebounceKey(message, accountId) {
|
|
1493
|
+
const senderId = resolveSlackSenderId(message);
|
|
1494
|
+
if (!senderId) return null;
|
|
1495
|
+
const messageTs = message.ts ?? message.event_ts;
|
|
1496
|
+
return `slack:${accountId}:${message.thread_ts ? `${message.channel}:${message.thread_ts}` : message.parent_user_id && messageTs ? `${message.channel}:maybe-thread:${messageTs}` : messageTs && !isSlackDirectMessageChannel(message.channel) ? `${message.channel}:${messageTs}` : message.channel}:${senderId}`;
|
|
1497
|
+
}
|
|
1498
|
+
//#endregion
|
|
1499
|
+
//#region extensions/slack/src/monitor/thread-resolution.ts
|
|
1500
|
+
const DEFAULT_THREAD_TS_CACHE_TTL_MS = 6e4;
|
|
1501
|
+
const DEFAULT_THREAD_TS_CACHE_MAX = 500;
|
|
1502
|
+
const normalizeThreadTs = (threadTs) => {
|
|
1503
|
+
const trimmed = threadTs?.trim();
|
|
1504
|
+
return trimmed ? trimmed : void 0;
|
|
1505
|
+
};
|
|
1506
|
+
const markAmbiguousThreadReply = (message) => ({
|
|
1507
|
+
...message,
|
|
1508
|
+
_ambiguousThreadReply: true
|
|
1509
|
+
});
|
|
1510
|
+
async function resolveThreadTsFromHistory(params) {
|
|
1511
|
+
try {
|
|
1512
|
+
const response = await params.client.conversations.history({
|
|
1513
|
+
channel: params.channelId,
|
|
1514
|
+
latest: params.messageTs,
|
|
1515
|
+
oldest: params.messageTs,
|
|
1516
|
+
inclusive: true,
|
|
1517
|
+
limit: 1
|
|
1518
|
+
});
|
|
1519
|
+
return normalizeThreadTs((response.messages?.find((entry) => entry.ts === params.messageTs) ?? response.messages?.[0])?.thread_ts);
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
if (shouldLogVerbose()) logVerbose(`slack inbound: failed to resolve thread_ts via conversations.history for channel=${params.channelId} ts=${params.messageTs}: ${formatSlackError(err)}`);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function createSlackThreadTsResolver(params) {
|
|
1526
|
+
const ttlMs = Math.max(0, params.cacheTtlMs ?? DEFAULT_THREAD_TS_CACHE_TTL_MS);
|
|
1527
|
+
const maxSize = Math.max(0, params.maxSize ?? DEFAULT_THREAD_TS_CACHE_MAX);
|
|
1528
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1529
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1530
|
+
const getCached = (key, now) => {
|
|
1531
|
+
const entry = cache.get(key);
|
|
1532
|
+
if (!entry) return;
|
|
1533
|
+
if (ttlMs > 0 && now - entry.updatedAt > ttlMs) {
|
|
1534
|
+
cache.delete(key);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
cache.delete(key);
|
|
1538
|
+
cache.set(key, {
|
|
1539
|
+
...entry,
|
|
1540
|
+
updatedAt: now
|
|
1541
|
+
});
|
|
1542
|
+
return entry.threadTs;
|
|
1543
|
+
};
|
|
1544
|
+
const setCached = (key, threadTs, now) => {
|
|
1545
|
+
cache.delete(key);
|
|
1546
|
+
cache.set(key, {
|
|
1547
|
+
threadTs,
|
|
1548
|
+
updatedAt: now
|
|
1549
|
+
});
|
|
1550
|
+
pruneMapToMaxSize(cache, maxSize);
|
|
1551
|
+
};
|
|
1552
|
+
return { resolve: async (request) => {
|
|
1553
|
+
const { message } = request;
|
|
1554
|
+
if (!message.parent_user_id || message.thread_ts || !message.ts) return message;
|
|
1555
|
+
const cacheKey = `${message.channel}:${message.ts}`;
|
|
1556
|
+
const cached = getCached(cacheKey, Date.now());
|
|
1557
|
+
if (cached !== void 0) return cached ? {
|
|
1558
|
+
...message,
|
|
1559
|
+
thread_ts: cached
|
|
1560
|
+
} : markAmbiguousThreadReply(message);
|
|
1561
|
+
if (shouldLogVerbose()) logVerbose(`slack inbound: missing thread_ts for thread reply channel=${message.channel} ts=${message.ts} source=${request.source}`);
|
|
1562
|
+
let pending = inflight.get(cacheKey);
|
|
1563
|
+
if (!pending) {
|
|
1564
|
+
pending = resolveThreadTsFromHistory({
|
|
1565
|
+
client: params.client,
|
|
1566
|
+
channelId: message.channel,
|
|
1567
|
+
messageTs: message.ts
|
|
1568
|
+
});
|
|
1569
|
+
inflight.set(cacheKey, pending);
|
|
1570
|
+
}
|
|
1571
|
+
let resolved;
|
|
1572
|
+
try {
|
|
1573
|
+
resolved = await pending;
|
|
1574
|
+
} finally {
|
|
1575
|
+
inflight.delete(cacheKey);
|
|
1576
|
+
}
|
|
1577
|
+
setCached(cacheKey, resolved ?? null, Date.now());
|
|
1578
|
+
if (resolved) {
|
|
1579
|
+
if (shouldLogVerbose()) logVerbose(`slack inbound: resolved missing thread_ts channel=${message.channel} ts=${message.ts} -> thread_ts=${resolved}`);
|
|
1580
|
+
return {
|
|
1581
|
+
...message,
|
|
1582
|
+
thread_ts: resolved
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
if (shouldLogVerbose()) logVerbose(`slack inbound: could not resolve missing thread_ts channel=${message.channel} ts=${message.ts}; marking reply ambiguous`);
|
|
1586
|
+
return markAmbiguousThreadReply(message);
|
|
1587
|
+
} };
|
|
1588
|
+
}
|
|
1589
|
+
//#endregion
|
|
1590
|
+
//#region extensions/slack/src/monitor/message-handler.ts
|
|
1591
|
+
let slackMessagePipelinePromise;
|
|
1592
|
+
function loadSlackMessagePipeline() {
|
|
1593
|
+
slackMessagePipelinePromise ??= import("./pipeline.runtime-DT0hLnq2.js");
|
|
1594
|
+
return slackMessagePipelinePromise;
|
|
1595
|
+
}
|
|
1596
|
+
const APP_MENTION_RETRY_TTL_MS = 6e4;
|
|
1597
|
+
var SlackRetryableInboundError = class extends Error {
|
|
1598
|
+
constructor(message, options) {
|
|
1599
|
+
super(message, options);
|
|
1600
|
+
this.name = "SlackRetryableInboundError";
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
function shouldDebounceSlackMessage(message, cfg) {
|
|
1604
|
+
return shouldDebounceTextInbound({
|
|
1605
|
+
text: stripSlackMentionsForCommandDetection(message.text ?? ""),
|
|
1606
|
+
cfg,
|
|
1607
|
+
hasMedia: Boolean(message.files && message.files.length > 0)
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
function buildSeenMessageKey(channelId, ts) {
|
|
1611
|
+
if (!channelId || !ts) return null;
|
|
1612
|
+
return `${channelId}:${ts}`;
|
|
1613
|
+
}
|
|
1614
|
+
function createSlackMessageHandler(params) {
|
|
1615
|
+
const { ctx, account, trackEvent } = params;
|
|
1616
|
+
const { debounceMs, debouncer } = createChannelInboundDebouncer({
|
|
1617
|
+
cfg: ctx.cfg,
|
|
1618
|
+
channel: "slack",
|
|
1619
|
+
buildKey: (entry) => buildSlackDebounceKey(entry.message, ctx.accountId),
|
|
1620
|
+
shouldDebounce: (entry) => shouldDebounceSlackMessage(entry.message, ctx.cfg),
|
|
1621
|
+
onFlush: async (entries) => {
|
|
1622
|
+
const last = entries.at(-1);
|
|
1623
|
+
if (!last) return;
|
|
1624
|
+
const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId);
|
|
1625
|
+
const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId);
|
|
1626
|
+
if (flushedKey && topLevelConversationKey) {
|
|
1627
|
+
const pendingKeys = pendingTopLevelDebounceKeys.get(topLevelConversationKey);
|
|
1628
|
+
if (pendingKeys) {
|
|
1629
|
+
pendingKeys.delete(flushedKey);
|
|
1630
|
+
if (pendingKeys.size === 0) pendingTopLevelDebounceKeys.delete(topLevelConversationKey);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const combinedText = entries.length === 1 ? last.message.text ?? "" : entries.map((entry) => entry.message.text ?? "").filter(Boolean).join("\n");
|
|
1634
|
+
const combinedMentioned = entries.some((entry) => Boolean(entry.opts.wasMentioned));
|
|
1635
|
+
const syntheticMessage = {
|
|
1636
|
+
...last.message,
|
|
1637
|
+
text: combinedText
|
|
1638
|
+
};
|
|
1639
|
+
const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts);
|
|
1640
|
+
try {
|
|
1641
|
+
const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
|
|
1642
|
+
const prepared = await prepareSlackMessage({
|
|
1643
|
+
ctx,
|
|
1644
|
+
account,
|
|
1645
|
+
message: syntheticMessage,
|
|
1646
|
+
opts: {
|
|
1647
|
+
...last.opts,
|
|
1648
|
+
wasMentioned: combinedMentioned || last.opts.wasMentioned
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
if (!prepared) return;
|
|
1652
|
+
if (seenMessageKey) {
|
|
1653
|
+
pruneAppMentionRetryKeys(Date.now());
|
|
1654
|
+
if (last.opts.source === "app_mention") appMentionDispatchedKeys.set(seenMessageKey, Date.now() + APP_MENTION_RETRY_TTL_MS);
|
|
1655
|
+
else if (last.opts.source === "message" && appMentionDispatchedKeys.has(seenMessageKey)) {
|
|
1656
|
+
appMentionDispatchedKeys.delete(seenMessageKey);
|
|
1657
|
+
appMentionRetryKeys.delete(seenMessageKey);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
appMentionRetryKeys.delete(seenMessageKey);
|
|
1661
|
+
}
|
|
1662
|
+
if (entries.length > 1) {
|
|
1663
|
+
const ids = entries.map((entry) => entry.message.ts).filter(Boolean);
|
|
1664
|
+
if (ids.length > 0) {
|
|
1665
|
+
prepared.ctxPayload.MessageSids = ids;
|
|
1666
|
+
prepared.ctxPayload.MessageSidFirst = ids[0];
|
|
1667
|
+
prepared.ctxPayload.MessageSidLast = ids[ids.length - 1];
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
await dispatchPreparedSlackMessage(prepared);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
if (error instanceof SlackRetryableInboundError) {
|
|
1673
|
+
if (seenMessageKey) appMentionDispatchedKeys.delete(seenMessageKey);
|
|
1674
|
+
ctx.releaseSeenMessage(last.message.channel, last.message.ts);
|
|
1675
|
+
}
|
|
1676
|
+
throw error;
|
|
1677
|
+
}
|
|
1678
|
+
},
|
|
1679
|
+
onError: (err) => {
|
|
1680
|
+
ctx.runtime.error?.(`slack inbound debounce flush failed: ${formatErrorMessage(err)}`);
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
const threadTsResolver = createSlackThreadTsResolver({ client: ctx.app.client });
|
|
1684
|
+
const pendingTopLevelDebounceKeys = /* @__PURE__ */ new Map();
|
|
1685
|
+
const appMentionRetryKeys = /* @__PURE__ */ new Map();
|
|
1686
|
+
const appMentionDispatchedKeys = /* @__PURE__ */ new Map();
|
|
1687
|
+
const pruneAppMentionRetryKeys = (now) => {
|
|
1688
|
+
for (const [key, expiresAt] of appMentionRetryKeys) if (expiresAt <= now) appMentionRetryKeys.delete(key);
|
|
1689
|
+
for (const [key, expiresAt] of appMentionDispatchedKeys) if (expiresAt <= now) appMentionDispatchedKeys.delete(key);
|
|
1690
|
+
};
|
|
1691
|
+
const rememberAppMentionRetryKey = (key) => {
|
|
1692
|
+
const now = Date.now();
|
|
1693
|
+
pruneAppMentionRetryKeys(now);
|
|
1694
|
+
appMentionRetryKeys.set(key, now + APP_MENTION_RETRY_TTL_MS);
|
|
1695
|
+
};
|
|
1696
|
+
const consumeAppMentionRetryKey = (key) => {
|
|
1697
|
+
pruneAppMentionRetryKeys(Date.now());
|
|
1698
|
+
if (!appMentionRetryKeys.has(key)) return false;
|
|
1699
|
+
appMentionRetryKeys.delete(key);
|
|
1700
|
+
return true;
|
|
1701
|
+
};
|
|
1702
|
+
return async (message, opts) => {
|
|
1703
|
+
if (opts.source === "message" && message.type !== "message") return;
|
|
1704
|
+
if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
|
|
1705
|
+
const seenMessageKey = buildSeenMessageKey(message.channel, message.ts);
|
|
1706
|
+
const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts) : false;
|
|
1707
|
+
if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
|
|
1708
|
+
if (seenMessageKey && wasSeen) {
|
|
1709
|
+
if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
|
|
1710
|
+
}
|
|
1711
|
+
trackEvent?.();
|
|
1712
|
+
const resolvedMessage = await threadTsResolver.resolve({
|
|
1713
|
+
message,
|
|
1714
|
+
source: opts.source
|
|
1715
|
+
});
|
|
1716
|
+
const debounceKey = buildSlackDebounceKey(resolvedMessage, ctx.accountId);
|
|
1717
|
+
const conversationKey = buildTopLevelSlackConversationKey(resolvedMessage, ctx.accountId);
|
|
1718
|
+
const canDebounce = debounceMs > 0 && shouldDebounceSlackMessage(resolvedMessage, ctx.cfg);
|
|
1719
|
+
if (!canDebounce && conversationKey) {
|
|
1720
|
+
const pendingKeys = pendingTopLevelDebounceKeys.get(conversationKey);
|
|
1721
|
+
if (pendingKeys && pendingKeys.size > 0) {
|
|
1722
|
+
const keysToFlush = Array.from(pendingKeys);
|
|
1723
|
+
for (const pendingKey of keysToFlush) await debouncer.flushKey(pendingKey);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (canDebounce && debounceKey && conversationKey) {
|
|
1727
|
+
const pendingKeys = pendingTopLevelDebounceKeys.get(conversationKey) ?? /* @__PURE__ */ new Set();
|
|
1728
|
+
pendingKeys.add(debounceKey);
|
|
1729
|
+
pendingTopLevelDebounceKeys.set(conversationKey, pendingKeys);
|
|
1730
|
+
}
|
|
1731
|
+
await debouncer.enqueue({
|
|
1732
|
+
message: resolvedMessage,
|
|
1733
|
+
opts
|
|
1734
|
+
});
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
//#endregion
|
|
1738
|
+
//#region extensions/slack/src/monitor/reconnect-policy.ts
|
|
1739
|
+
const SLACK_AUTH_ERROR_RE = /account_inactive|invalid_auth|token_revoked|token_expired|not_authed|org_login_required|team_access_not_granted|missing_scope|cannot_find_service|invalid_token/i;
|
|
1740
|
+
const NO_ERROR_DETAIL = "no error detail";
|
|
1741
|
+
const SLACK_SOCKET_RECONNECT_POLICY = {
|
|
1742
|
+
initialMs: 2e3,
|
|
1743
|
+
maxMs: 3e4,
|
|
1744
|
+
factor: 1.8,
|
|
1745
|
+
jitter: .25,
|
|
1746
|
+
maxAttempts: 12
|
|
1747
|
+
};
|
|
1748
|
+
function getSocketEmitter(app) {
|
|
1749
|
+
const receiver = app.receiver;
|
|
1750
|
+
const client = receiver && typeof receiver === "object" ? receiver.client : void 0;
|
|
1751
|
+
if (!client || typeof client !== "object") return null;
|
|
1752
|
+
const on = client.on;
|
|
1753
|
+
const off = client.off;
|
|
1754
|
+
if (typeof on !== "function" || typeof off !== "function") return null;
|
|
1755
|
+
return {
|
|
1756
|
+
on: (event, listener) => on.call(client, event, listener),
|
|
1757
|
+
off: (event, listener) => off.call(client, event, listener)
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function waitForSlackSocketDisconnect(app, abortSignal) {
|
|
1761
|
+
return new Promise((resolve) => {
|
|
1762
|
+
const emitter = getSocketEmitter(app);
|
|
1763
|
+
if (!emitter) {
|
|
1764
|
+
abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), { once: true });
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const disconnectListener = () => resolveOnce({ event: "disconnect" });
|
|
1768
|
+
const startFailListener = (error) => resolveOnce({
|
|
1769
|
+
event: "unable_to_socket_mode_start",
|
|
1770
|
+
error
|
|
1771
|
+
});
|
|
1772
|
+
const errorListener = (error) => resolveOnce({
|
|
1773
|
+
event: "error",
|
|
1774
|
+
error
|
|
1775
|
+
});
|
|
1776
|
+
const abortListener = () => resolveOnce({ event: "disconnect" });
|
|
1777
|
+
const cleanup = () => {
|
|
1778
|
+
emitter.off("disconnected", disconnectListener);
|
|
1779
|
+
emitter.off("unable_to_socket_mode_start", startFailListener);
|
|
1780
|
+
emitter.off("error", errorListener);
|
|
1781
|
+
abortSignal?.removeEventListener("abort", abortListener);
|
|
1782
|
+
};
|
|
1783
|
+
const resolveOnce = (value) => {
|
|
1784
|
+
cleanup();
|
|
1785
|
+
resolve(value);
|
|
1786
|
+
};
|
|
1787
|
+
emitter.on("disconnected", disconnectListener);
|
|
1788
|
+
emitter.on("unable_to_socket_mode_start", startFailListener);
|
|
1789
|
+
emitter.on("error", errorListener);
|
|
1790
|
+
abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Detect non-recoverable Slack API / auth errors that should NOT be retried.
|
|
1795
|
+
* These indicate permanent credential problems (revoked bot, deactivated account, etc.)
|
|
1796
|
+
* and retrying will never succeed — continuing to retry blocks the entire gateway.
|
|
1797
|
+
*/
|
|
1798
|
+
function isNonRecoverableSlackAuthError(error) {
|
|
1799
|
+
return SLACK_AUTH_ERROR_RE.test(formatUnknownError(error, ""));
|
|
1800
|
+
}
|
|
1801
|
+
function formatUnknownError(error, fallback = NO_ERROR_DETAIL) {
|
|
1802
|
+
return formatSlackError(error, fallback);
|
|
1803
|
+
}
|
|
1804
|
+
//#endregion
|
|
1805
|
+
//#region extensions/slack/src/monitor/provider-support.ts
|
|
1806
|
+
const OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS = 15e3;
|
|
1807
|
+
const OPENCLAW_SLACK_SOCKET_START_FAILED_EVENT = "unable_to_socket_mode_start";
|
|
1808
|
+
const OPENCLAW_SLACK_NATIVE_RECONNECT_OBSERVER_KEY = "__openclawNativeReconnectFailureObserver";
|
|
1809
|
+
const SLACK_SOCKET_PONG_TIMEOUT_WARNING_PREFIX = "A pong wasn't received from the server";
|
|
1810
|
+
const SLACK_SOCKET_PING_TIMEOUT_WARNING_PREFIX = "A ping wasn't received from the server";
|
|
1811
|
+
const SLACK_SOCKET_LOG_LEVEL_IGNORED_WARNING_RE = /^The logLevel given to .+ was ignored as you also gave logger$/;
|
|
1812
|
+
function isConstructorFunction(value) {
|
|
1813
|
+
return typeof value === "function";
|
|
1814
|
+
}
|
|
1815
|
+
function installSlackNativeReconnectFailureObserver(receiver) {
|
|
1816
|
+
if (!receiver || typeof receiver !== "object") return;
|
|
1817
|
+
const client = Reflect.get(receiver, "client");
|
|
1818
|
+
if (!client || typeof client !== "object") return;
|
|
1819
|
+
if (Reflect.get(client, OPENCLAW_SLACK_NATIVE_RECONNECT_OBSERVER_KEY)) return;
|
|
1820
|
+
const delayReconnectAttempt = Reflect.get(client, "delayReconnectAttempt");
|
|
1821
|
+
const emit = Reflect.get(client, "emit");
|
|
1822
|
+
if (typeof delayReconnectAttempt !== "function" || typeof emit !== "function") return;
|
|
1823
|
+
Reflect.set(client, OPENCLAW_SLACK_NATIVE_RECONNECT_OBSERVER_KEY, true);
|
|
1824
|
+
Reflect.set(client, "delayReconnectAttempt", function patchedDelayReconnectAttempt(callback) {
|
|
1825
|
+
if (typeof callback !== "function") return delayReconnectAttempt.call(this, callback);
|
|
1826
|
+
const nextFailureCount = Number(Reflect.get(this, "numOfConsecutiveReconnectionFailures") ?? 0) + 1;
|
|
1827
|
+
Reflect.set(this, "numOfConsecutiveReconnectionFailures", nextFailureCount);
|
|
1828
|
+
const pingTimeoutMs = Number(Reflect.get(this, "clientPingTimeoutMS"));
|
|
1829
|
+
const delayMs = (Number.isFinite(pingTimeoutMs) && pingTimeoutMs >= 0 ? pingTimeoutMs : OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS) * nextFailureCount;
|
|
1830
|
+
const logger = Reflect.get(this, "logger");
|
|
1831
|
+
logger?.debug?.(`Before trying to reconnect, this client will wait for ${delayMs} milliseconds`);
|
|
1832
|
+
return new Promise((resolve, reject) => {
|
|
1833
|
+
setTimeout(() => {
|
|
1834
|
+
if (Reflect.get(this, "shuttingDown")) {
|
|
1835
|
+
logger?.debug?.("Client shutting down, will not attempt reconnect.");
|
|
1836
|
+
resolve(void 0);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
logger?.debug?.("Continuing with reconnect...");
|
|
1840
|
+
emit.call(this, "reconnecting");
|
|
1841
|
+
Promise.resolve(callback.call(this)).then(resolve, (error) => {
|
|
1842
|
+
if (callback === Reflect.get(this, "start")) {
|
|
1843
|
+
emit.call(this, OPENCLAW_SLACK_SOCKET_START_FAILED_EVENT, error);
|
|
1844
|
+
resolve(void 0);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
reject(error);
|
|
1848
|
+
});
|
|
1849
|
+
}, delayMs);
|
|
1850
|
+
});
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
function resolveSlackBoltModule(value) {
|
|
1854
|
+
if (!value || typeof value !== "object") return null;
|
|
1855
|
+
const app = Reflect.get(value, "App");
|
|
1856
|
+
const httpReceiver = Reflect.get(value, "HTTPReceiver");
|
|
1857
|
+
const socketModeReceiver = Reflect.get(value, "SocketModeReceiver");
|
|
1858
|
+
if (!isConstructorFunction(app) || !isConstructorFunction(httpReceiver) || !isConstructorFunction(socketModeReceiver)) return null;
|
|
1859
|
+
return {
|
|
1860
|
+
App: app,
|
|
1861
|
+
HTTPReceiver: httpReceiver,
|
|
1862
|
+
SocketModeReceiver: socketModeReceiver
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
function resolveSlackBoltInterop(params) {
|
|
1866
|
+
const { defaultImport, namespaceImport } = params;
|
|
1867
|
+
const nestedDefault = defaultImport && typeof defaultImport === "object" ? Reflect.get(defaultImport, "default") : void 0;
|
|
1868
|
+
const namespaceDefault = namespaceImport && typeof namespaceImport === "object" ? Reflect.get(namespaceImport, "default") : void 0;
|
|
1869
|
+
const namespaceReceiver = namespaceImport && typeof namespaceImport === "object" ? Reflect.get(namespaceImport, "HTTPReceiver") : void 0;
|
|
1870
|
+
const namespaceSocketModeReceiver = namespaceImport && typeof namespaceImport === "object" ? Reflect.get(namespaceImport, "SocketModeReceiver") : void 0;
|
|
1871
|
+
const directModule = resolveSlackBoltModule(defaultImport) ?? resolveSlackBoltModule(nestedDefault) ?? resolveSlackBoltModule(namespaceDefault) ?? resolveSlackBoltModule(namespaceImport);
|
|
1872
|
+
if (directModule) return directModule;
|
|
1873
|
+
if (isConstructorFunction(defaultImport) && isConstructorFunction(namespaceReceiver) && isConstructorFunction(namespaceSocketModeReceiver)) return {
|
|
1874
|
+
App: defaultImport,
|
|
1875
|
+
HTTPReceiver: namespaceReceiver,
|
|
1876
|
+
SocketModeReceiver: namespaceSocketModeReceiver
|
|
1877
|
+
};
|
|
1878
|
+
throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
|
|
1879
|
+
}
|
|
1880
|
+
function publishSlackConnectedStatus(setStatus) {
|
|
1881
|
+
if (!setStatus) return;
|
|
1882
|
+
setStatus({
|
|
1883
|
+
connected: true,
|
|
1884
|
+
lastConnectedAt: Date.now(),
|
|
1885
|
+
healthState: "healthy",
|
|
1886
|
+
lastError: null
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
function publishSlackDisconnectedStatus(setStatus, error) {
|
|
1890
|
+
if (!setStatus) return;
|
|
1891
|
+
const at = Date.now();
|
|
1892
|
+
const message = error ? formatUnknownError(error) : void 0;
|
|
1893
|
+
setStatus({
|
|
1894
|
+
connected: false,
|
|
1895
|
+
healthState: "disconnected",
|
|
1896
|
+
lastDisconnect: message ? {
|
|
1897
|
+
at,
|
|
1898
|
+
error: message
|
|
1899
|
+
} : { at },
|
|
1900
|
+
lastError: message ?? null
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
function isSlackSocketHeartbeatTimeoutWarning(args) {
|
|
1904
|
+
return typeof args[0] === "string" && (args[0].startsWith(SLACK_SOCKET_PONG_TIMEOUT_WARNING_PREFIX) || args[0].startsWith(SLACK_SOCKET_PING_TIMEOUT_WARNING_PREFIX));
|
|
1905
|
+
}
|
|
1906
|
+
function isSlackSocketSelfInflictedLoggerWarning(args) {
|
|
1907
|
+
return typeof args[0] === "string" && SLACK_SOCKET_LOG_LEVEL_IGNORED_WARNING_RE.test(args[0]);
|
|
1908
|
+
}
|
|
1909
|
+
function formatSlackSdkLogArgs(args) {
|
|
1910
|
+
return args.map((arg) => formatUnknownError(arg, "")).filter(Boolean).join(" ");
|
|
1911
|
+
}
|
|
1912
|
+
function createSlackSocketModeLogger(sink = console) {
|
|
1913
|
+
let level = "info";
|
|
1914
|
+
let name = "socket-mode";
|
|
1915
|
+
const prefix = () => `socket-mode:${name}`;
|
|
1916
|
+
let lastMessage;
|
|
1917
|
+
const remember = (args) => {
|
|
1918
|
+
const message = formatSlackSdkLogArgs([prefix(), ...args]);
|
|
1919
|
+
if (message) lastMessage = message;
|
|
1920
|
+
};
|
|
1921
|
+
return {
|
|
1922
|
+
debug: () => {},
|
|
1923
|
+
info: () => {},
|
|
1924
|
+
warn: (...args) => {
|
|
1925
|
+
if (isSlackSocketHeartbeatTimeoutWarning(args) || isSlackSocketSelfInflictedLoggerWarning(args)) return;
|
|
1926
|
+
remember(args);
|
|
1927
|
+
sink.warn(prefix(), ...args);
|
|
1928
|
+
},
|
|
1929
|
+
error: (...args) => {
|
|
1930
|
+
remember(args);
|
|
1931
|
+
sink.error(prefix(), ...args);
|
|
1932
|
+
},
|
|
1933
|
+
setLevel: (nextLevel) => {
|
|
1934
|
+
level = nextLevel;
|
|
1935
|
+
},
|
|
1936
|
+
getLevel: () => level,
|
|
1937
|
+
setName: (nextName) => {
|
|
1938
|
+
name = nextName;
|
|
1939
|
+
},
|
|
1940
|
+
getLastMessage: () => lastMessage
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
function asRecord(value) {
|
|
1944
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1945
|
+
}
|
|
1946
|
+
function shouldSkipOpenClawSlackSelfEvent(args) {
|
|
1947
|
+
const botId = args.context?.botId;
|
|
1948
|
+
const botUserId = args.context?.botUserId;
|
|
1949
|
+
const message = asRecord(args.message);
|
|
1950
|
+
if (message?.subtype === "bot_message" && botId && message.bot_id === botId) return true;
|
|
1951
|
+
const event = asRecord(args.event);
|
|
1952
|
+
if (event?.type === "message" && event.subtype === "message_changed" && event.user === botUserId) return false;
|
|
1953
|
+
const eventsWhichShouldBeKept = new Set(["member_joined_channel", "member_left_channel"]);
|
|
1954
|
+
return Boolean(botUserId && event && event.user === botUserId && typeof event.type === "string" && !eventsWhichShouldBeKept.has(event.type));
|
|
1955
|
+
}
|
|
1956
|
+
function createSlackBoltApp(params) {
|
|
1957
|
+
const socketModeLogger = createSlackSocketModeLogger();
|
|
1958
|
+
const socketModeReceiverOptions = {
|
|
1959
|
+
appToken: params.appToken ?? "",
|
|
1960
|
+
autoReconnectEnabled: true,
|
|
1961
|
+
clientPingTimeout: params.socketMode?.clientPingTimeout ?? OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS,
|
|
1962
|
+
logger: socketModeLogger,
|
|
1963
|
+
installerOptions: { clientOptions: params.clientOptions }
|
|
1964
|
+
};
|
|
1965
|
+
if (params.socketMode?.serverPingTimeout !== void 0) socketModeReceiverOptions.serverPingTimeout = params.socketMode.serverPingTimeout;
|
|
1966
|
+
if (params.socketMode?.pingPongLoggingEnabled !== void 0) socketModeReceiverOptions.pingPongLoggingEnabled = params.socketMode.pingPongLoggingEnabled;
|
|
1967
|
+
const receiver = params.slackMode === "socket" ? new params.interop.SocketModeReceiver(socketModeReceiverOptions) : new params.interop.HTTPReceiver({
|
|
1968
|
+
signingSecret: params.signingSecret ?? "",
|
|
1969
|
+
endpoints: params.slackWebhookPath
|
|
1970
|
+
});
|
|
1971
|
+
if (params.slackMode === "socket") installSlackNativeReconnectFailureObserver(receiver);
|
|
1972
|
+
const app = new params.interop.App({
|
|
1973
|
+
token: params.botToken,
|
|
1974
|
+
receiver,
|
|
1975
|
+
clientOptions: params.clientOptions,
|
|
1976
|
+
ignoreSelf: false,
|
|
1977
|
+
tokenVerificationEnabled: false
|
|
1978
|
+
});
|
|
1979
|
+
app.use(async (args) => {
|
|
1980
|
+
if (shouldSkipOpenClawSlackSelfEvent(args)) return;
|
|
1981
|
+
await args.next();
|
|
1982
|
+
});
|
|
1983
|
+
return {
|
|
1984
|
+
app,
|
|
1985
|
+
receiver,
|
|
1986
|
+
socketModeLogger
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function createSlackSocketDisconnectWaiter(app, abortSignal) {
|
|
1990
|
+
const waiterAbortController = new AbortController();
|
|
1991
|
+
const relayAbort = () => waiterAbortController.abort();
|
|
1992
|
+
let latest;
|
|
1993
|
+
abortSignal?.addEventListener("abort", relayAbort, { once: true });
|
|
1994
|
+
return {
|
|
1995
|
+
promise: waitForSlackSocketDisconnect(app, waiterAbortController.signal).then((value) => {
|
|
1996
|
+
latest = value;
|
|
1997
|
+
return value;
|
|
1998
|
+
}),
|
|
1999
|
+
getLatest: () => latest,
|
|
2000
|
+
cancel: () => {
|
|
2001
|
+
waiterAbortController.abort();
|
|
2002
|
+
abortSignal?.removeEventListener("abort", relayAbort);
|
|
2003
|
+
},
|
|
2004
|
+
complete: () => {
|
|
2005
|
+
abortSignal?.removeEventListener("abort", relayAbort);
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
async function startSlackSocketAndWaitForDisconnect(params) {
|
|
2010
|
+
const disconnectWaiter = createSlackSocketDisconnectWaiter(params.app, params.abortSignal);
|
|
2011
|
+
try {
|
|
2012
|
+
await Promise.resolve(params.app.start());
|
|
2013
|
+
if (params.abortSignal?.aborted) {
|
|
2014
|
+
disconnectWaiter.cancel();
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
params.onStarted?.();
|
|
2018
|
+
const disconnect = await disconnectWaiter.promise;
|
|
2019
|
+
disconnectWaiter.complete();
|
|
2020
|
+
return disconnect;
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
await Promise.resolve();
|
|
2023
|
+
const disconnect = disconnectWaiter.getLatest();
|
|
2024
|
+
disconnectWaiter.cancel();
|
|
2025
|
+
if ((err === void 0 || err === null || err === "") && disconnect?.error !== void 0) throw disconnect.error;
|
|
2026
|
+
if (err === void 0 || err === null || err === "") {
|
|
2027
|
+
const suffix = disconnect ? ` after ${disconnect.event}` : "";
|
|
2028
|
+
throw new Error(`Slack Socket Mode start failed${suffix} without error detail`, { cause: err });
|
|
2029
|
+
}
|
|
2030
|
+
throw err;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
function resolveSlackSocketShutdownClient(app) {
|
|
2034
|
+
if (!app || typeof app !== "object") return;
|
|
2035
|
+
const receiver = Reflect.get(app, "receiver");
|
|
2036
|
+
if (!receiver || typeof receiver !== "object") return;
|
|
2037
|
+
const client = Reflect.get(receiver, "client");
|
|
2038
|
+
if (!client || typeof client !== "object") return;
|
|
2039
|
+
return client;
|
|
2040
|
+
}
|
|
2041
|
+
async function gracefulStopSlackApp(app) {
|
|
2042
|
+
const socketClient = resolveSlackSocketShutdownClient(app);
|
|
2043
|
+
if (socketClient) socketClient.shuttingDown = true;
|
|
2044
|
+
await Promise.resolve(app.stop()).catch(() => void 0);
|
|
2045
|
+
}
|
|
2046
|
+
function formatSlackResolvedLabel(params) {
|
|
2047
|
+
const extras = params.extra?.filter(Boolean) ?? [];
|
|
2048
|
+
const suffix = extras.length > 0 ? ` (id:${params.id}, ${extras.join(", ")})` : ` (id:${params.id})`;
|
|
2049
|
+
return `${params.input}→${params.name ?? params.id}${suffix}`;
|
|
2050
|
+
}
|
|
2051
|
+
function formatSlackChannelResolved(entry) {
|
|
2052
|
+
const id = entry.id ?? entry.input;
|
|
2053
|
+
return formatSlackResolvedLabel({
|
|
2054
|
+
input: entry.input,
|
|
2055
|
+
id,
|
|
2056
|
+
name: entry.name,
|
|
2057
|
+
extra: entry.archived ? ["archived"] : []
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function formatSlackUserResolved(entry) {
|
|
2061
|
+
const id = entry.id ?? entry.input;
|
|
2062
|
+
return formatSlackResolvedLabel({
|
|
2063
|
+
input: entry.input,
|
|
2064
|
+
id,
|
|
2065
|
+
name: entry.name,
|
|
2066
|
+
extra: entry.note ? [entry.note] : []
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
//#endregion
|
|
2070
|
+
//#region extensions/slack/src/monitor/external-arg-menu-store.ts
|
|
2071
|
+
const SLACK_EXTERNAL_ARG_MENU_TOKEN_BYTES = 18;
|
|
2072
|
+
const SLACK_EXTERNAL_ARG_MENU_TOKEN_LENGTH = Math.ceil(SLACK_EXTERNAL_ARG_MENU_TOKEN_BYTES * 8 / 6);
|
|
2073
|
+
const SLACK_EXTERNAL_ARG_MENU_TOKEN_PATTERN = new RegExp(`^[A-Za-z0-9_-]{${SLACK_EXTERNAL_ARG_MENU_TOKEN_LENGTH}}$`);
|
|
2074
|
+
const SLACK_EXTERNAL_ARG_MENU_TTL_MS = 600 * 1e3;
|
|
2075
|
+
const SLACK_EXTERNAL_ARG_MENU_PREFIX = "openclaw_cmdarg_ext:";
|
|
2076
|
+
function pruneSlackExternalArgMenuStore(store, now) {
|
|
2077
|
+
for (const [token, entry] of store.entries()) if (entry.expiresAt <= now) store.delete(token);
|
|
2078
|
+
}
|
|
2079
|
+
function createSlackExternalArgMenuToken(store) {
|
|
2080
|
+
let token = "";
|
|
2081
|
+
do
|
|
2082
|
+
token = generateSecureToken(SLACK_EXTERNAL_ARG_MENU_TOKEN_BYTES);
|
|
2083
|
+
while (store.has(token));
|
|
2084
|
+
return token;
|
|
2085
|
+
}
|
|
2086
|
+
function createSlackExternalArgMenuStore() {
|
|
2087
|
+
const store = /* @__PURE__ */ new Map();
|
|
2088
|
+
return {
|
|
2089
|
+
create(params, now = Date.now()) {
|
|
2090
|
+
pruneSlackExternalArgMenuStore(store, now);
|
|
2091
|
+
const token = createSlackExternalArgMenuToken(store);
|
|
2092
|
+
store.set(token, {
|
|
2093
|
+
choices: params.choices,
|
|
2094
|
+
userId: params.userId,
|
|
2095
|
+
expiresAt: now + SLACK_EXTERNAL_ARG_MENU_TTL_MS
|
|
2096
|
+
});
|
|
2097
|
+
return token;
|
|
2098
|
+
},
|
|
2099
|
+
readToken(raw) {
|
|
2100
|
+
if (typeof raw !== "string" || !raw.startsWith("openclaw_cmdarg_ext:")) return;
|
|
2101
|
+
const token = raw.slice(20).trim();
|
|
2102
|
+
return SLACK_EXTERNAL_ARG_MENU_TOKEN_PATTERN.test(token) ? token : void 0;
|
|
2103
|
+
},
|
|
2104
|
+
get(token, now = Date.now()) {
|
|
2105
|
+
pruneSlackExternalArgMenuStore(store, now);
|
|
2106
|
+
return store.get(token);
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
//#endregion
|
|
2111
|
+
//#region extensions/slack/src/monitor/slash.ts
|
|
2112
|
+
const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg";
|
|
2113
|
+
const SLACK_COMMAND_ARG_ACTION_LISTENER = /^openclaw_cmdarg/;
|
|
2114
|
+
const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg";
|
|
2115
|
+
const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5;
|
|
2116
|
+
const SLACK_COMMAND_ARG_OVERFLOW_MIN = 3;
|
|
2117
|
+
const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5;
|
|
2118
|
+
const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100;
|
|
2119
|
+
const SLACK_COMMAND_ARG_SELECT_OPTION_TEXT_MAX = 75;
|
|
2120
|
+
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 150;
|
|
2121
|
+
const SLACK_COMMAND_ARG_BUTTON_TEXT_MAX = 75;
|
|
2122
|
+
const SLACK_COMMAND_ARG_BUTTON_VALUE_MAX = 2e3;
|
|
2123
|
+
const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
|
|
2124
|
+
const SLACK_HEADER_TEXT_MAX = 150;
|
|
2125
|
+
const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
|
|
2126
|
+
let slashCommandsRuntimePromise = null;
|
|
2127
|
+
let slashDispatchRuntimePromise = null;
|
|
2128
|
+
let slackPluginCommandsRuntimePromise = null;
|
|
2129
|
+
let slashSkillCommandsRuntimePromise = null;
|
|
2130
|
+
function loadSlashCommandsRuntime() {
|
|
2131
|
+
slashCommandsRuntimePromise ??= import("./slash-commands.runtime-22kgyst2.js");
|
|
2132
|
+
return slashCommandsRuntimePromise;
|
|
2133
|
+
}
|
|
2134
|
+
function loadSlashDispatchRuntime() {
|
|
2135
|
+
slashDispatchRuntimePromise ??= import("./slash-dispatch.runtime-BJgT0jwV.js");
|
|
2136
|
+
return slashDispatchRuntimePromise;
|
|
2137
|
+
}
|
|
2138
|
+
function loadSlackPluginCommandsRuntime() {
|
|
2139
|
+
slackPluginCommandsRuntimePromise ??= import("./slash-plugin-commands.runtime-CF-n3MeP.js");
|
|
2140
|
+
return slackPluginCommandsRuntimePromise;
|
|
2141
|
+
}
|
|
2142
|
+
function loadSlashSkillCommandsRuntime() {
|
|
2143
|
+
slashSkillCommandsRuntimePromise ??= import("./slash-skill-commands.runtime-BMs0VjTe.js");
|
|
2144
|
+
return slashSkillCommandsRuntimePromise;
|
|
2145
|
+
}
|
|
2146
|
+
function resolveSlackCommandMenuModelContext(params) {
|
|
2147
|
+
if (!params.sessionKey.trim()) return {};
|
|
2148
|
+
try {
|
|
2149
|
+
const defaultModel = resolveDefaultModelForAgent({
|
|
2150
|
+
cfg: params.cfg,
|
|
2151
|
+
agentId: params.agentId
|
|
2152
|
+
});
|
|
2153
|
+
const store = loadSessionStore(resolveStorePath(params.cfg.session?.store, { agentId: params.agentId }));
|
|
2154
|
+
const entry = store[params.sessionKey];
|
|
2155
|
+
if (entry?.modelOverrideSource === "auto" && normalizeOptionalString(entry.modelOverride)) return {
|
|
2156
|
+
provider: defaultModel.provider,
|
|
2157
|
+
model: defaultModel.model
|
|
2158
|
+
};
|
|
2159
|
+
const override = resolveStoredModelOverride({
|
|
2160
|
+
sessionEntry: entry,
|
|
2161
|
+
sessionStore: store,
|
|
2162
|
+
sessionKey: params.sessionKey,
|
|
2163
|
+
defaultProvider: defaultModel.provider
|
|
2164
|
+
});
|
|
2165
|
+
if (override?.model) return {
|
|
2166
|
+
provider: override.provider || defaultModel.provider,
|
|
2167
|
+
model: override.model
|
|
2168
|
+
};
|
|
2169
|
+
const provider = normalizeOptionalString(entry?.providerOverride) ?? normalizeOptionalString(entry?.modelProvider);
|
|
2170
|
+
const model = normalizeOptionalString(entry?.modelOverride) ?? normalizeOptionalString(entry?.model);
|
|
2171
|
+
return {
|
|
2172
|
+
...provider ? { provider } : {},
|
|
2173
|
+
...model ? { model } : {}
|
|
2174
|
+
};
|
|
2175
|
+
} catch {
|
|
2176
|
+
return {};
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
const slackExternalArgMenuStore = createSlackExternalArgMenuStore();
|
|
2180
|
+
function buildSlackArgMenuConfirm(params) {
|
|
2181
|
+
return {
|
|
2182
|
+
title: {
|
|
2183
|
+
type: "plain_text",
|
|
2184
|
+
text: "Confirm selection"
|
|
2185
|
+
},
|
|
2186
|
+
text: {
|
|
2187
|
+
type: "mrkdwn",
|
|
2188
|
+
text: truncateSlackText(`Run */${escapeSlackMrkdwn(params.command)}* with *${escapeSlackMrkdwn(params.arg)}* set to this value?`, SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX)
|
|
2189
|
+
},
|
|
2190
|
+
confirm: {
|
|
2191
|
+
type: "plain_text",
|
|
2192
|
+
text: "Run command"
|
|
2193
|
+
},
|
|
2194
|
+
deny: {
|
|
2195
|
+
type: "plain_text",
|
|
2196
|
+
text: "Cancel"
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
function storeSlackExternalArgMenu(params) {
|
|
2201
|
+
return slackExternalArgMenuStore.create({
|
|
2202
|
+
choices: params.choices,
|
|
2203
|
+
userId: params.userId
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
function readSlackExternalArgMenuToken(raw) {
|
|
2207
|
+
return slackExternalArgMenuStore.readToken(raw);
|
|
2208
|
+
}
|
|
2209
|
+
function encodeSlackCommandArgValue(parts) {
|
|
2210
|
+
return [
|
|
2211
|
+
SLACK_COMMAND_ARG_VALUE_PREFIX,
|
|
2212
|
+
encodeURIComponent(parts.command),
|
|
2213
|
+
encodeURIComponent(parts.arg),
|
|
2214
|
+
encodeURIComponent(parts.value),
|
|
2215
|
+
encodeURIComponent(parts.userId)
|
|
2216
|
+
].join("|");
|
|
2217
|
+
}
|
|
2218
|
+
function parseSlackCommandArgValue(raw) {
|
|
2219
|
+
if (!raw) return null;
|
|
2220
|
+
const parts = raw.split("|");
|
|
2221
|
+
if (parts.length !== 5 || parts[0] !== SLACK_COMMAND_ARG_VALUE_PREFIX) return null;
|
|
2222
|
+
const [, command, arg, value, userId] = parts;
|
|
2223
|
+
if (!command || !arg || !value || !userId) return null;
|
|
2224
|
+
const decode = (text) => {
|
|
2225
|
+
try {
|
|
2226
|
+
return decodeURIComponent(text);
|
|
2227
|
+
} catch {
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
const decodedCommand = decode(command);
|
|
2232
|
+
const decodedArg = decode(arg);
|
|
2233
|
+
const decodedValue = decode(value);
|
|
2234
|
+
const decodedUserId = decode(userId);
|
|
2235
|
+
if (!decodedCommand || !decodedArg || !decodedValue || !decodedUserId) return null;
|
|
2236
|
+
return {
|
|
2237
|
+
command: decodedCommand,
|
|
2238
|
+
arg: decodedArg,
|
|
2239
|
+
value: decodedValue,
|
|
2240
|
+
userId: decodedUserId
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
function buildSlackArgMenuOptions(choices) {
|
|
2244
|
+
return choices.map((choice) => ({
|
|
2245
|
+
text: {
|
|
2246
|
+
type: "plain_text",
|
|
2247
|
+
text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_SELECT_OPTION_TEXT_MAX)
|
|
2248
|
+
},
|
|
2249
|
+
value: choice.value
|
|
2250
|
+
}));
|
|
2251
|
+
}
|
|
2252
|
+
function buildSlackCommandArgMenuBlocks(params) {
|
|
2253
|
+
const encodedChoices = params.choices.map((choice) => ({
|
|
2254
|
+
label: choice.label,
|
|
2255
|
+
value: encodeSlackCommandArgValue({
|
|
2256
|
+
command: params.command,
|
|
2257
|
+
arg: params.arg,
|
|
2258
|
+
value: choice.value,
|
|
2259
|
+
userId: params.userId
|
|
2260
|
+
})
|
|
2261
|
+
}));
|
|
2262
|
+
const canUseStaticSelect = encodedChoices.every((choice) => choice.value.length <= SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX);
|
|
2263
|
+
const canUseOverflow = canUseStaticSelect && encodedChoices.length >= SLACK_COMMAND_ARG_OVERFLOW_MIN && encodedChoices.length <= SLACK_COMMAND_ARG_OVERFLOW_MAX;
|
|
2264
|
+
const canUseExternalSelect = params.supportsExternalSelect && canUseStaticSelect && encodedChoices.length > SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX;
|
|
2265
|
+
const rows = canUseOverflow ? [{
|
|
2266
|
+
type: "actions",
|
|
2267
|
+
elements: [{
|
|
2268
|
+
type: "overflow",
|
|
2269
|
+
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
|
2270
|
+
confirm: buildSlackArgMenuConfirm({
|
|
2271
|
+
command: params.command,
|
|
2272
|
+
arg: params.arg
|
|
2273
|
+
}),
|
|
2274
|
+
options: buildSlackArgMenuOptions(encodedChoices)
|
|
2275
|
+
}]
|
|
2276
|
+
}] : canUseExternalSelect ? [{
|
|
2277
|
+
type: "actions",
|
|
2278
|
+
block_id: `${SLACK_EXTERNAL_ARG_MENU_PREFIX}${params.createExternalMenuToken(encodedChoices)}`,
|
|
2279
|
+
elements: [{
|
|
2280
|
+
type: "external_select",
|
|
2281
|
+
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
|
2282
|
+
confirm: buildSlackArgMenuConfirm({
|
|
2283
|
+
command: params.command,
|
|
2284
|
+
arg: params.arg
|
|
2285
|
+
}),
|
|
2286
|
+
min_query_length: 0,
|
|
2287
|
+
placeholder: {
|
|
2288
|
+
type: "plain_text",
|
|
2289
|
+
text: `Search ${params.arg}`
|
|
2290
|
+
}
|
|
2291
|
+
}]
|
|
2292
|
+
}] : encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect ? chunkItems(encodedChoices.filter((choice) => choice.value.length <= SLACK_COMMAND_ARG_BUTTON_VALUE_MAX), SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map((choices, rowIndex) => ({
|
|
2293
|
+
type: "actions",
|
|
2294
|
+
elements: choices.map((choice, colIndex) => ({
|
|
2295
|
+
type: "button",
|
|
2296
|
+
action_id: `${SLACK_COMMAND_ARG_ACTION_ID}_${rowIndex}_${colIndex}`,
|
|
2297
|
+
text: {
|
|
2298
|
+
type: "plain_text",
|
|
2299
|
+
text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_BUTTON_TEXT_MAX)
|
|
2300
|
+
},
|
|
2301
|
+
value: choice.value,
|
|
2302
|
+
confirm: buildSlackArgMenuConfirm({
|
|
2303
|
+
command: params.command,
|
|
2304
|
+
arg: params.arg
|
|
2305
|
+
})
|
|
2306
|
+
}))
|
|
2307
|
+
})) : chunkItems(encodedChoices, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map((choices, index) => ({
|
|
2308
|
+
type: "actions",
|
|
2309
|
+
elements: [{
|
|
2310
|
+
type: "static_select",
|
|
2311
|
+
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
|
2312
|
+
confirm: buildSlackArgMenuConfirm({
|
|
2313
|
+
command: params.command,
|
|
2314
|
+
arg: params.arg
|
|
2315
|
+
}),
|
|
2316
|
+
placeholder: {
|
|
2317
|
+
type: "plain_text",
|
|
2318
|
+
text: index === 0 ? `Choose ${params.arg}` : `Choose ${params.arg} (${index + 1})`
|
|
2319
|
+
},
|
|
2320
|
+
options: buildSlackArgMenuOptions(choices)
|
|
2321
|
+
}]
|
|
2322
|
+
}));
|
|
2323
|
+
const headerText = truncateSlackText(`/${params.command}: choose ${params.arg}`, SLACK_HEADER_TEXT_MAX);
|
|
2324
|
+
const sectionText = truncateSlackText(params.title, 3e3);
|
|
2325
|
+
const contextText = truncateSlackText(`Select one option to continue /${params.command} (${params.arg})`, 3e3);
|
|
2326
|
+
const visibleRows = rows.slice(0, SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX);
|
|
2327
|
+
return [
|
|
2328
|
+
{
|
|
2329
|
+
type: "header",
|
|
2330
|
+
text: {
|
|
2331
|
+
type: "plain_text",
|
|
2332
|
+
text: headerText
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
{
|
|
2336
|
+
type: "section",
|
|
2337
|
+
text: {
|
|
2338
|
+
type: "mrkdwn",
|
|
2339
|
+
text: sectionText
|
|
2340
|
+
}
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
type: "context",
|
|
2344
|
+
elements: [{
|
|
2345
|
+
type: "mrkdwn",
|
|
2346
|
+
text: contextText
|
|
2347
|
+
}]
|
|
2348
|
+
},
|
|
2349
|
+
...visibleRows
|
|
2350
|
+
];
|
|
2351
|
+
}
|
|
2352
|
+
async function registerSlackMonitorSlashCommands(params) {
|
|
2353
|
+
const { ctx, account, trackEvent } = params;
|
|
2354
|
+
const cfg = ctx.cfg;
|
|
2355
|
+
const runtime = ctx.runtime;
|
|
2356
|
+
const supportsInteractiveArgMenus = typeof ctx.app.action === "function";
|
|
2357
|
+
let supportsExternalArgMenus = typeof ctx.app.options === "function";
|
|
2358
|
+
const slashCommand = resolveSlackSlashCommandConfig(ctx.slashCommand ?? account.config.slashCommand);
|
|
2359
|
+
const handleSlashCommand = async (p) => {
|
|
2360
|
+
const { command, ack, respond, body, prompt, commandArgs, commandDefinition } = p;
|
|
2361
|
+
try {
|
|
2362
|
+
if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
2363
|
+
await ack();
|
|
2364
|
+
runtime.log?.(`slack: drop slash command from user=${command.user_id ?? "unknown"} channel=${command.channel_id ?? "unknown"} (mismatched app/team)`);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
trackEvent?.();
|
|
2368
|
+
if (!prompt.trim()) {
|
|
2369
|
+
await ack({
|
|
2370
|
+
text: "Message required.",
|
|
2371
|
+
response_type: "ephemeral"
|
|
2372
|
+
});
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
await ack();
|
|
2376
|
+
if (ctx.botUserId && command.user_id === ctx.botUserId) return;
|
|
2377
|
+
const channelInfo = await ctx.resolveChannelName(command.channel_id);
|
|
2378
|
+
const channelType = normalizeSlackChannelType(channelInfo?.type ?? (command.channel_name === "directmessage" ? "im" : void 0), command.channel_id);
|
|
2379
|
+
const chatType = resolveSlackChatType(channelType);
|
|
2380
|
+
const isDirectMessage = channelType === "im";
|
|
2381
|
+
const isGroupDm = channelType === "mpim";
|
|
2382
|
+
const isRoom = channelType === "channel" || channelType === "group";
|
|
2383
|
+
const isRoomish = isRoom || isGroupDm;
|
|
2384
|
+
if (!ctx.isChannelAllowed({
|
|
2385
|
+
channelId: command.channel_id,
|
|
2386
|
+
channelName: channelInfo?.name,
|
|
2387
|
+
channelType
|
|
2388
|
+
})) {
|
|
2389
|
+
await respond({
|
|
2390
|
+
text: "This channel is not allowed.",
|
|
2391
|
+
response_type: "ephemeral"
|
|
2392
|
+
});
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const effectiveAllowFromLower = await resolveSlackEffectiveAllowFrom(ctx, { includePairingStore: isDirectMessage });
|
|
2396
|
+
let commandAuthorized = false;
|
|
2397
|
+
let channelConfig = null;
|
|
2398
|
+
if (isDirectMessage) {
|
|
2399
|
+
if (!await authorizeSlackDirectMessage({
|
|
2400
|
+
ctx,
|
|
2401
|
+
accountId: ctx.accountId,
|
|
2402
|
+
senderId: command.user_id,
|
|
2403
|
+
allowFromLower: effectiveAllowFromLower,
|
|
2404
|
+
resolveSenderName: ctx.resolveUserName,
|
|
2405
|
+
sendPairingReply: async (text) => {
|
|
2406
|
+
await respond({
|
|
2407
|
+
text,
|
|
2408
|
+
response_type: "ephemeral"
|
|
2409
|
+
});
|
|
2410
|
+
},
|
|
2411
|
+
onDisabled: async () => {
|
|
2412
|
+
await respond({
|
|
2413
|
+
text: "Slack DMs are disabled.",
|
|
2414
|
+
response_type: "ephemeral"
|
|
2415
|
+
});
|
|
2416
|
+
},
|
|
2417
|
+
onUnauthorized: async ({ allowMatchMeta }) => {
|
|
2418
|
+
logVerbose(`slack: blocked slash sender ${command.user_id} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`);
|
|
2419
|
+
await respond({
|
|
2420
|
+
text: "You are not authorized to use this command.",
|
|
2421
|
+
response_type: "ephemeral"
|
|
2422
|
+
});
|
|
2423
|
+
},
|
|
2424
|
+
log: logVerbose
|
|
2425
|
+
})) return;
|
|
2426
|
+
}
|
|
2427
|
+
if (isRoom) {
|
|
2428
|
+
channelConfig = resolveSlackChannelConfig({
|
|
2429
|
+
channelId: command.channel_id,
|
|
2430
|
+
channelName: channelInfo?.name,
|
|
2431
|
+
channels: ctx.channelsConfig,
|
|
2432
|
+
channelKeys: ctx.channelsConfigKeys,
|
|
2433
|
+
defaultRequireMention: ctx.defaultRequireMention,
|
|
2434
|
+
allowNameMatching: ctx.allowNameMatching
|
|
2435
|
+
});
|
|
2436
|
+
if (ctx.useAccessGroups) {
|
|
2437
|
+
const channelAllowlistConfigured = (ctx.channelsConfigKeys?.length ?? 0) > 0;
|
|
2438
|
+
const channelAllowed = channelConfig?.allowed !== false;
|
|
2439
|
+
if (!isSlackChannelAllowedByPolicy({
|
|
2440
|
+
groupPolicy: ctx.groupPolicy,
|
|
2441
|
+
channelAllowlistConfigured,
|
|
2442
|
+
channelAllowed
|
|
2443
|
+
})) {
|
|
2444
|
+
await respond({
|
|
2445
|
+
text: "This channel is not allowed.",
|
|
2446
|
+
response_type: "ephemeral"
|
|
2447
|
+
});
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const hasExplicitConfig = Boolean(channelConfig?.matchSource);
|
|
2451
|
+
if (!channelAllowed && (ctx.groupPolicy !== "open" || hasExplicitConfig)) {
|
|
2452
|
+
await respond({
|
|
2453
|
+
text: "This channel is not allowed.",
|
|
2454
|
+
response_type: "ephemeral"
|
|
2455
|
+
});
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
const senderName = (await ctx.resolveUserName(command.user_id))?.name ?? command.user_name ?? command.user_id;
|
|
2461
|
+
const slashIngress = await resolveSlackCommandIngress({
|
|
2462
|
+
ctx,
|
|
2463
|
+
senderId: command.user_id,
|
|
2464
|
+
senderName,
|
|
2465
|
+
channelType: channelType ?? "channel",
|
|
2466
|
+
channelId: command.channel_id,
|
|
2467
|
+
ownerAllowFromLower: effectiveAllowFromLower,
|
|
2468
|
+
channelUsers: isRoom ? channelConfig?.users : void 0,
|
|
2469
|
+
allowTextCommands: false,
|
|
2470
|
+
hasControlCommand: false,
|
|
2471
|
+
eventKind: "slash-command",
|
|
2472
|
+
modeWhenAccessGroupsOff: "configured"
|
|
2473
|
+
});
|
|
2474
|
+
const senderGate = slashIngress.senderAccess.gate;
|
|
2475
|
+
if (isRoom && senderGate?.allowed === false) {
|
|
2476
|
+
await respond({
|
|
2477
|
+
text: "You are not authorized to use this command here.",
|
|
2478
|
+
response_type: "ephemeral"
|
|
2479
|
+
});
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
commandAuthorized = slashIngress.commandAccess.authorized;
|
|
2483
|
+
if (isRoomish) {
|
|
2484
|
+
if (ctx.useAccessGroups && !commandAuthorized) {
|
|
2485
|
+
await respond({
|
|
2486
|
+
text: "You are not authorized to use this command.",
|
|
2487
|
+
response_type: "ephemeral"
|
|
2488
|
+
});
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
let resolvedSlashRoute;
|
|
2493
|
+
const resolveSlashRoute = async () => {
|
|
2494
|
+
if (resolvedSlashRoute) return resolvedSlashRoute;
|
|
2495
|
+
const { resolveAgentRoute } = await loadSlashDispatchRuntime();
|
|
2496
|
+
resolvedSlashRoute = resolveAgentRoute({
|
|
2497
|
+
cfg,
|
|
2498
|
+
channel: "slack",
|
|
2499
|
+
accountId: account.accountId,
|
|
2500
|
+
teamId: ctx.teamId || void 0,
|
|
2501
|
+
peer: {
|
|
2502
|
+
kind: isDirectMessage ? "direct" : isRoom ? "channel" : "group",
|
|
2503
|
+
id: isDirectMessage ? command.user_id : command.channel_id
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
return resolvedSlashRoute;
|
|
2507
|
+
};
|
|
2508
|
+
if (commandDefinition && supportsInteractiveArgMenus) {
|
|
2509
|
+
const { resolveCommandArgMenu } = await loadSlashCommandsRuntime();
|
|
2510
|
+
const menuRoute = !(commandArgs?.raw && !commandArgs.values) && commandDefinition.args?.some((arg) => typeof arg.choices === "function" && commandArgs?.values?.[arg.name] == null) ? await resolveSlashRoute() : void 0;
|
|
2511
|
+
const menu = resolveCommandArgMenu({
|
|
2512
|
+
command: commandDefinition,
|
|
2513
|
+
args: commandArgs,
|
|
2514
|
+
cfg,
|
|
2515
|
+
...menuRoute ? resolveSlackCommandMenuModelContext({
|
|
2516
|
+
cfg,
|
|
2517
|
+
agentId: menuRoute.agentId,
|
|
2518
|
+
sessionKey: menuRoute.sessionKey
|
|
2519
|
+
}) : {}
|
|
2520
|
+
});
|
|
2521
|
+
if (menu) {
|
|
2522
|
+
const commandLabel = commandDefinition.nativeName ?? commandDefinition.key;
|
|
2523
|
+
const title = formatCommandArgMenuTitle({
|
|
2524
|
+
command: commandDefinition,
|
|
2525
|
+
menu
|
|
2526
|
+
});
|
|
2527
|
+
await respond({
|
|
2528
|
+
text: title,
|
|
2529
|
+
blocks: buildSlackCommandArgMenuBlocks({
|
|
2530
|
+
title,
|
|
2531
|
+
command: commandLabel,
|
|
2532
|
+
arg: menu.arg.name,
|
|
2533
|
+
choices: menu.choices,
|
|
2534
|
+
userId: command.user_id,
|
|
2535
|
+
supportsExternalSelect: supportsExternalArgMenus,
|
|
2536
|
+
createExternalMenuToken: (choices) => storeSlackExternalArgMenu({
|
|
2537
|
+
choices,
|
|
2538
|
+
userId: command.user_id
|
|
2539
|
+
})
|
|
2540
|
+
}),
|
|
2541
|
+
response_type: "ephemeral"
|
|
2542
|
+
});
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const channelName = channelInfo?.name;
|
|
2547
|
+
const roomLabel = channelName ? `#${channelName}` : `#${command.channel_id}`;
|
|
2548
|
+
const { deliverSlackSlashReplies, dispatchReplyWithDispatcher, finalizeInboundContext, recordInboundSessionMetaSafe, resolveAgentRoute, resolveChunkMode, resolveConversationLabel, resolveMarkdownTableMode } = await loadSlashDispatchRuntime();
|
|
2549
|
+
const route = resolvedSlashRoute ?? resolveAgentRoute({
|
|
2550
|
+
cfg,
|
|
2551
|
+
channel: "slack",
|
|
2552
|
+
accountId: account.accountId,
|
|
2553
|
+
teamId: ctx.teamId || void 0,
|
|
2554
|
+
peer: {
|
|
2555
|
+
kind: isDirectMessage ? "direct" : isRoom ? "channel" : "group",
|
|
2556
|
+
id: isDirectMessage ? command.user_id : command.channel_id
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
const { untrustedChannelMetadata, groupSystemPrompt } = resolveSlackRoomContextHints({
|
|
2560
|
+
isRoomish,
|
|
2561
|
+
channelInfo,
|
|
2562
|
+
channelConfig
|
|
2563
|
+
});
|
|
2564
|
+
const { sessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({
|
|
2565
|
+
agentId: route.agentId,
|
|
2566
|
+
sessionPrefix: slashCommand.sessionPrefix,
|
|
2567
|
+
userId: command.user_id,
|
|
2568
|
+
targetSessionKey: route.sessionKey,
|
|
2569
|
+
lowercaseSessionKey: true
|
|
2570
|
+
});
|
|
2571
|
+
const ctxPayload = finalizeInboundContext({
|
|
2572
|
+
Body: prompt,
|
|
2573
|
+
BodyForAgent: prompt,
|
|
2574
|
+
RawBody: prompt,
|
|
2575
|
+
CommandBody: prompt,
|
|
2576
|
+
CommandArgs: commandArgs,
|
|
2577
|
+
From: isDirectMessage ? `slack:${command.user_id}` : isRoom ? `slack:channel:${command.channel_id}` : `slack:group:${command.channel_id}`,
|
|
2578
|
+
To: `slash:${command.user_id}`,
|
|
2579
|
+
ChatType: chatType,
|
|
2580
|
+
ConversationLabel: resolveConversationLabel({
|
|
2581
|
+
ChatType: chatType,
|
|
2582
|
+
SenderName: senderName,
|
|
2583
|
+
GroupSubject: isRoomish ? roomLabel : void 0,
|
|
2584
|
+
From: isDirectMessage ? `slack:${command.user_id}` : isRoom ? `slack:channel:${command.channel_id}` : `slack:group:${command.channel_id}`
|
|
2585
|
+
}) ?? (isDirectMessage ? senderName : roomLabel),
|
|
2586
|
+
GroupSubject: isRoomish ? roomLabel : void 0,
|
|
2587
|
+
GroupSpace: ctx.teamId || void 0,
|
|
2588
|
+
GroupSystemPrompt: groupSystemPrompt,
|
|
2589
|
+
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : void 0,
|
|
2590
|
+
SenderName: senderName,
|
|
2591
|
+
SenderId: command.user_id,
|
|
2592
|
+
Provider: "slack",
|
|
2593
|
+
Surface: "slack",
|
|
2594
|
+
WasMentioned: true,
|
|
2595
|
+
MessageSid: command.trigger_id,
|
|
2596
|
+
Timestamp: Date.now(),
|
|
2597
|
+
SessionKey: sessionKey,
|
|
2598
|
+
CommandTargetSessionKey: commandTargetSessionKey,
|
|
2599
|
+
AccountId: route.accountId,
|
|
2600
|
+
CommandSource: "native",
|
|
2601
|
+
CommandAuthorized: commandAuthorized,
|
|
2602
|
+
OriginatingChannel: "slack",
|
|
2603
|
+
OriginatingTo: `user:${command.user_id}`
|
|
2604
|
+
});
|
|
2605
|
+
await recordInboundSessionMetaSafe({
|
|
2606
|
+
cfg,
|
|
2607
|
+
agentId: route.agentId,
|
|
2608
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
2609
|
+
ctx: ctxPayload,
|
|
2610
|
+
onError: (err) => runtime.error?.(danger(`slack slash: failed updating session meta: ${formatErrorMessage(err)}`))
|
|
2611
|
+
});
|
|
2612
|
+
const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
|
|
2613
|
+
cfg,
|
|
2614
|
+
agentId: route.agentId,
|
|
2615
|
+
channel: "slack",
|
|
2616
|
+
accountId: route.accountId
|
|
2617
|
+
});
|
|
2618
|
+
const deliverSlashPayloads = async (replies) => {
|
|
2619
|
+
await deliverSlackSlashReplies({
|
|
2620
|
+
replies,
|
|
2621
|
+
respond,
|
|
2622
|
+
ephemeral: slashCommand.ephemeral,
|
|
2623
|
+
textLimit: ctx.textLimit,
|
|
2624
|
+
chunkMode: resolveChunkMode(cfg, "slack", route.accountId),
|
|
2625
|
+
tableMode: resolveMarkdownTableMode({
|
|
2626
|
+
cfg,
|
|
2627
|
+
channel: "slack",
|
|
2628
|
+
accountId: route.accountId
|
|
2629
|
+
})
|
|
2630
|
+
});
|
|
2631
|
+
};
|
|
2632
|
+
const { counts } = await dispatchReplyWithDispatcher({
|
|
2633
|
+
ctx: ctxPayload,
|
|
2634
|
+
cfg,
|
|
2635
|
+
dispatcherOptions: {
|
|
2636
|
+
...replyPipeline,
|
|
2637
|
+
deliver: async (payload) => deliverSlashPayloads([payload]),
|
|
2638
|
+
onError: (err, info) => {
|
|
2639
|
+
runtime.error?.(danger(`slack slash ${info.kind} reply failed: ${formatSlackError(err)}`));
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
replyOptions: {
|
|
2643
|
+
skillFilter: channelConfig?.skills,
|
|
2644
|
+
onModelSelected
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
if (counts.final + counts.tool + counts.block === 0) await deliverSlashPayloads([]);
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
runtime.error?.(danger(`slack slash handler failed: ${formatErrorMessage(err)}`));
|
|
2650
|
+
await respond({
|
|
2651
|
+
text: "Sorry, something went wrong handling that command.",
|
|
2652
|
+
response_type: "ephemeral"
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
const nativeEnabled = resolveNativeCommandsEnabled({
|
|
2657
|
+
providerId: "slack",
|
|
2658
|
+
providerSetting: account.config.commands?.native,
|
|
2659
|
+
globalSetting: cfg.commands?.native
|
|
2660
|
+
});
|
|
2661
|
+
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
|
|
2662
|
+
providerId: "slack",
|
|
2663
|
+
providerSetting: account.config.commands?.nativeSkills,
|
|
2664
|
+
globalSetting: cfg.commands?.nativeSkills
|
|
2665
|
+
});
|
|
2666
|
+
let nativeCommands = [];
|
|
2667
|
+
let slashCommandsRuntime = null;
|
|
2668
|
+
if (nativeEnabled) {
|
|
2669
|
+
slashCommandsRuntime = await loadSlashCommandsRuntime();
|
|
2670
|
+
const skillCommands = nativeSkillsEnabled ? (await loadSlashSkillCommandsRuntime()).listSkillCommandsForAgents({ cfg }) : [];
|
|
2671
|
+
nativeCommands = slashCommandsRuntime.listNativeCommandSpecsForConfig(cfg, {
|
|
2672
|
+
skillCommands,
|
|
2673
|
+
provider: "slack"
|
|
2674
|
+
});
|
|
2675
|
+
const existingNativeNames = new Set(nativeCommands.map((c) => normalizeLowercaseStringOrEmpty(c.name)).filter(Boolean));
|
|
2676
|
+
const { listProviderPluginCommandSpecs } = await loadSlackPluginCommandsRuntime();
|
|
2677
|
+
for (const pluginCommand of listProviderPluginCommandSpecs("slack")) {
|
|
2678
|
+
const normalizedName = normalizeLowercaseStringOrEmpty(pluginCommand.name);
|
|
2679
|
+
if (!normalizedName || existingNativeNames.has(normalizedName)) continue;
|
|
2680
|
+
existingNativeNames.add(normalizedName);
|
|
2681
|
+
nativeCommands.push(pluginCommand);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (nativeCommands.length > 0) {
|
|
2685
|
+
if (!slashCommandsRuntime) throw new Error("Missing commands runtime for native Slack commands.");
|
|
2686
|
+
for (const command of nativeCommands) ctx.app.command(`/${command.name}`, async ({ command: cmd, ack, respond, body }) => {
|
|
2687
|
+
const commandDefinition = slashCommandsRuntime.findCommandByNativeName(command.name, "slack");
|
|
2688
|
+
const rawText = cmd.text?.trim() ?? "";
|
|
2689
|
+
const commandArgs = commandDefinition ? slashCommandsRuntime.parseCommandArgs(commandDefinition, rawText) : rawText ? { raw: rawText } : void 0;
|
|
2690
|
+
await handleSlashCommand({
|
|
2691
|
+
command: cmd,
|
|
2692
|
+
ack,
|
|
2693
|
+
respond,
|
|
2694
|
+
body,
|
|
2695
|
+
prompt: commandDefinition ? slashCommandsRuntime.buildCommandTextFromArgs(commandDefinition, commandArgs) : rawText ? `/${command.name} ${rawText}` : `/${command.name}`,
|
|
2696
|
+
commandArgs,
|
|
2697
|
+
commandDefinition: commandDefinition ?? void 0
|
|
2698
|
+
});
|
|
2699
|
+
});
|
|
2700
|
+
} else if (slashCommand.enabled) ctx.app.command(buildSlackSlashCommandMatcher(slashCommand.name), async ({ command, ack, respond, body }) => {
|
|
2701
|
+
await handleSlashCommand({
|
|
2702
|
+
command,
|
|
2703
|
+
ack,
|
|
2704
|
+
respond,
|
|
2705
|
+
body,
|
|
2706
|
+
prompt: command.text?.trim() ?? ""
|
|
2707
|
+
});
|
|
2708
|
+
});
|
|
2709
|
+
else logVerbose("slack: slash commands disabled");
|
|
2710
|
+
if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) return;
|
|
2711
|
+
const registerArgOptions = () => {
|
|
2712
|
+
const appWithOptions = ctx.app;
|
|
2713
|
+
if (typeof appWithOptions.options !== "function") return;
|
|
2714
|
+
appWithOptions.options(SLACK_COMMAND_ARG_ACTION_ID, async ({ ack, body }) => {
|
|
2715
|
+
if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
2716
|
+
await ack({ options: [] });
|
|
2717
|
+
runtime.log?.("slack: drop slash arg options payload (mismatched app/team)");
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
trackEvent?.();
|
|
2721
|
+
const typedBody = body;
|
|
2722
|
+
const token = readSlackExternalArgMenuToken(typedBody.actions?.[0]?.block_id ?? typedBody.block_id);
|
|
2723
|
+
if (!token) {
|
|
2724
|
+
await ack({ options: [] });
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
const entry = slackExternalArgMenuStore.get(token);
|
|
2728
|
+
if (!entry) {
|
|
2729
|
+
await ack({ options: [] });
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const requesterUserId = typedBody.user?.id?.trim();
|
|
2733
|
+
if (!requesterUserId || requesterUserId !== entry.userId) {
|
|
2734
|
+
await ack({ options: [] });
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const query = normalizeLowercaseStringOrEmpty(typedBody.value);
|
|
2738
|
+
await ack({ options: entry.choices.filter((choice) => !query || normalizeLowercaseStringOrEmpty(choice.label).includes(query)).slice(0, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map((choice) => ({
|
|
2739
|
+
text: {
|
|
2740
|
+
type: "plain_text",
|
|
2741
|
+
text: choice.label.slice(0, 75)
|
|
2742
|
+
},
|
|
2743
|
+
value: choice.value
|
|
2744
|
+
})) });
|
|
2745
|
+
});
|
|
2746
|
+
};
|
|
2747
|
+
try {
|
|
2748
|
+
registerArgOptions();
|
|
2749
|
+
} catch (err) {
|
|
2750
|
+
supportsExternalArgMenus = false;
|
|
2751
|
+
logVerbose(`slack: external arg-menu registration failed, falling back to static menus: ${formatErrorMessage(err)}`);
|
|
2752
|
+
}
|
|
2753
|
+
const registerArgAction = (actionId) => {
|
|
2754
|
+
ctx.app.action(actionId, async (args) => {
|
|
2755
|
+
const { ack, body, respond } = args;
|
|
2756
|
+
const action = args.action;
|
|
2757
|
+
await ack();
|
|
2758
|
+
if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
|
2759
|
+
runtime.log?.("slack: drop slash arg action payload (mismatched app/team)");
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
const respondFn = respond ?? (async (payload) => {
|
|
2763
|
+
if (!body.channel?.id || !body.user?.id) return;
|
|
2764
|
+
await ctx.app.client.chat.postEphemeral({
|
|
2765
|
+
token: ctx.botToken,
|
|
2766
|
+
channel: body.channel.id,
|
|
2767
|
+
user: body.user.id,
|
|
2768
|
+
text: payload.text,
|
|
2769
|
+
blocks: payload.blocks
|
|
2770
|
+
});
|
|
2771
|
+
});
|
|
2772
|
+
const parsed = parseSlackCommandArgValue(action?.value ?? action?.selected_option?.value);
|
|
2773
|
+
if (!parsed) {
|
|
2774
|
+
await respondFn({
|
|
2775
|
+
text: "Sorry, that button is no longer valid.",
|
|
2776
|
+
response_type: "ephemeral"
|
|
2777
|
+
});
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
if (body.user?.id && parsed.userId !== body.user.id) {
|
|
2781
|
+
await respondFn({
|
|
2782
|
+
text: "That menu is for another user.",
|
|
2783
|
+
response_type: "ephemeral"
|
|
2784
|
+
});
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
const { buildCommandTextFromArgs, findCommandByNativeName } = await loadSlashCommandsRuntime();
|
|
2788
|
+
const commandDefinition = findCommandByNativeName(parsed.command, "slack");
|
|
2789
|
+
const commandArgs = { values: { [parsed.arg]: parsed.value } };
|
|
2790
|
+
const prompt = commandDefinition ? buildCommandTextFromArgs(commandDefinition, commandArgs) : `/${parsed.command} ${parsed.value}`;
|
|
2791
|
+
const user = body.user;
|
|
2792
|
+
const userName = user && "name" in user && user.name ? user.name : user && "username" in user && user.username ? user.username : user?.id ?? "";
|
|
2793
|
+
const triggerId = "trigger_id" in body ? body.trigger_id : void 0;
|
|
2794
|
+
await handleSlashCommand({
|
|
2795
|
+
command: {
|
|
2796
|
+
user_id: user?.id ?? "",
|
|
2797
|
+
user_name: userName,
|
|
2798
|
+
channel_id: body.channel?.id ?? "",
|
|
2799
|
+
channel_name: body.channel?.name ?? body.channel?.id ?? "",
|
|
2800
|
+
trigger_id: triggerId
|
|
2801
|
+
},
|
|
2802
|
+
ack: async () => {},
|
|
2803
|
+
respond: respondFn,
|
|
2804
|
+
body,
|
|
2805
|
+
prompt,
|
|
2806
|
+
commandArgs,
|
|
2807
|
+
commandDefinition: commandDefinition ?? void 0
|
|
2808
|
+
});
|
|
2809
|
+
});
|
|
2810
|
+
};
|
|
2811
|
+
registerArgAction(SLACK_COMMAND_ARG_ACTION_LISTENER);
|
|
2812
|
+
}
|
|
2813
|
+
//#endregion
|
|
2814
|
+
//#region extensions/slack/src/monitor/provider.ts
|
|
2815
|
+
let slackBoltInterop;
|
|
2816
|
+
async function getSlackBoltInterop() {
|
|
2817
|
+
if (!slackBoltInterop) {
|
|
2818
|
+
const slackBoltModule = await import("@slack/bolt");
|
|
2819
|
+
slackBoltInterop = resolveSlackBoltInterop({
|
|
2820
|
+
defaultImport: slackBoltModule.default,
|
|
2821
|
+
namespaceImport: slackBoltModule
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
return slackBoltInterop;
|
|
2825
|
+
}
|
|
2826
|
+
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
2827
|
+
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
|
|
2828
|
+
function resolveStableSlackUserIdEntry(raw) {
|
|
2829
|
+
const trimmed = raw.trim();
|
|
2830
|
+
if (!trimmed) return;
|
|
2831
|
+
const mention = /^<@([A-Z][A-Z0-9]+)>$/i.exec(trimmed);
|
|
2832
|
+
if (mention) return mention[1]?.toUpperCase();
|
|
2833
|
+
const prefixed = /^(?:slack:|user:)([A-Z][A-Z0-9]+)$/i.exec(trimmed);
|
|
2834
|
+
if (prefixed) return prefixed[1]?.toUpperCase();
|
|
2835
|
+
return /^[UW][A-Z0-9]+$/i.test(trimmed) ? trimmed.toUpperCase() : void 0;
|
|
2836
|
+
}
|
|
2837
|
+
function resolveStableSlackUserAllowlistEntries(entries) {
|
|
2838
|
+
const resolved = [];
|
|
2839
|
+
for (const input of entries) {
|
|
2840
|
+
const id = resolveStableSlackUserIdEntry(input);
|
|
2841
|
+
if (id) resolved.push({
|
|
2842
|
+
input,
|
|
2843
|
+
resolved: true,
|
|
2844
|
+
id
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
return resolved;
|
|
2848
|
+
}
|
|
2849
|
+
function formatSlackSocketReconnectMessage(params) {
|
|
2850
|
+
const maxAttempts = params.maxAttempts > 0 ? String(params.maxAttempts) : "∞";
|
|
2851
|
+
const suffix = params.error ? ` (${formatUnknownError(params.error)})` : "";
|
|
2852
|
+
return `slack socket disconnected (${params.event}); reconnecting in ${Math.round(params.delayMs / 1e3)}s (attempt ${params.attempt}/${maxAttempts})${suffix}`;
|
|
2853
|
+
}
|
|
2854
|
+
function formatSlackSocketStartRetryMessage(params) {
|
|
2855
|
+
const maxAttempts = params.maxAttempts > 0 ? String(params.maxAttempts) : "∞";
|
|
2856
|
+
const reason = formatUnknownError(params.error, "Slack Socket Mode start failed without error detail");
|
|
2857
|
+
const sdkContext = params.sdkContext?.trim() ? `; last SDK log: ${params.sdkContext.trim()}` : "";
|
|
2858
|
+
return `slack socket mode failed to start; retry ${params.attempt}/${maxAttempts} in ${Math.round(params.delayMs / 1e3)}s reason="${reason}${sdkContext}"`;
|
|
2859
|
+
}
|
|
2860
|
+
function parseApiAppIdFromAppToken(raw) {
|
|
2861
|
+
const token = raw?.trim();
|
|
2862
|
+
if (!token) return;
|
|
2863
|
+
return /^xapp-\d-([a-z0-9]+)-/i.exec(token)?.[1]?.toUpperCase();
|
|
2864
|
+
}
|
|
2865
|
+
async function monitorSlackProvider(opts = {}) {
|
|
2866
|
+
const cfg = opts.config ?? getRuntimeConfig$1();
|
|
2867
|
+
const runtime = opts.runtime ?? createNonExitingRuntime();
|
|
2868
|
+
let account = resolveSlackAccount({
|
|
2869
|
+
cfg,
|
|
2870
|
+
accountId: opts.accountId
|
|
2871
|
+
});
|
|
2872
|
+
if (!account.enabled) {
|
|
2873
|
+
runtime.log?.(`[${account.accountId}] slack account disabled; monitor startup skipped`);
|
|
2874
|
+
if (opts.abortSignal?.aborted) return;
|
|
2875
|
+
await new Promise((resolve) => {
|
|
2876
|
+
opts.abortSignal?.addEventListener("abort", () => resolve(), { once: true });
|
|
2877
|
+
});
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
const historyLimit = Math.max(0, account.config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT);
|
|
2881
|
+
const dmHistoryLimit = Math.max(0, account.config.dmHistoryLimit ?? 0);
|
|
2882
|
+
const sessionCfg = cfg.session;
|
|
2883
|
+
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
|
2884
|
+
const mainKey = normalizeMainKey(sessionCfg?.mainKey);
|
|
2885
|
+
const slackMode = opts.mode ?? account.config.mode ?? "socket";
|
|
2886
|
+
const slackWebhookPath = normalizeSlackWebhookPath(account.config.webhookPath);
|
|
2887
|
+
const signingSecret = normalizeResolvedSecretInputString({
|
|
2888
|
+
value: account.config.signingSecret,
|
|
2889
|
+
path: `channels.slack.accounts.${account.accountId}.signingSecret`
|
|
2890
|
+
});
|
|
2891
|
+
const botToken = resolveSlackBotToken(opts.botToken ?? account.botToken);
|
|
2892
|
+
const appToken = resolveSlackAppToken(opts.appToken ?? account.appToken);
|
|
2893
|
+
if (!botToken || slackMode !== "http" && !appToken) {
|
|
2894
|
+
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).` : `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).`;
|
|
2895
|
+
throw new Error(missing);
|
|
2896
|
+
}
|
|
2897
|
+
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).`);
|
|
2898
|
+
const slackCfg = account.config;
|
|
2899
|
+
const dmConfig = slackCfg.dm;
|
|
2900
|
+
const dmEnabled = dmConfig?.enabled ?? true;
|
|
2901
|
+
const dmPolicy = resolveSlackAccountDmPolicy({
|
|
2902
|
+
cfg,
|
|
2903
|
+
accountId: account.accountId
|
|
2904
|
+
}) ?? "pairing";
|
|
2905
|
+
let allowFrom = resolveSlackAccountAllowFrom({
|
|
2906
|
+
cfg,
|
|
2907
|
+
accountId: account.accountId
|
|
2908
|
+
});
|
|
2909
|
+
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
|
|
2910
|
+
const groupDmChannels = dmConfig?.groupChannels;
|
|
2911
|
+
let channelsConfig = slackCfg.channels;
|
|
2912
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
2913
|
+
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
|
|
2914
|
+
providerConfigPresent: cfg.channels?.slack !== void 0,
|
|
2915
|
+
groupPolicy: slackCfg.groupPolicy,
|
|
2916
|
+
defaultGroupPolicy
|
|
2917
|
+
});
|
|
2918
|
+
warnMissingProviderGroupPolicyFallbackOnce({
|
|
2919
|
+
providerMissingFallbackApplied,
|
|
2920
|
+
providerKey: "slack",
|
|
2921
|
+
accountId: account.accountId,
|
|
2922
|
+
log: (message) => runtime.log?.(warn(message))
|
|
2923
|
+
});
|
|
2924
|
+
const resolveToken = account.userToken || botToken;
|
|
2925
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
2926
|
+
const reactionMode = slackCfg.reactionNotifications ?? "own";
|
|
2927
|
+
const reactionAllowlist = slackCfg.reactionAllowlist ?? [];
|
|
2928
|
+
const replyToMode = slackCfg.replyToMode ?? "off";
|
|
2929
|
+
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
|
2930
|
+
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
|
2931
|
+
const threadRequireExplicitMention = slackCfg.thread?.requireExplicitMention ?? false;
|
|
2932
|
+
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
|
2933
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(slackCfg);
|
|
2934
|
+
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, { fallbackLimit: SLACK_TEXT_LIMIT });
|
|
2935
|
+
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
|
2936
|
+
const typingReaction = slackCfg.typingReaction?.trim() ?? "";
|
|
2937
|
+
const mediaMaxBytes = (opts.mediaMaxMb ?? slackCfg.mediaMaxMb ?? 20) * 1024 * 1024;
|
|
2938
|
+
const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
|
|
2939
|
+
const clientOptions = resolveSlackWebClientOptions();
|
|
2940
|
+
const { app, receiver, socketModeLogger } = createSlackBoltApp({
|
|
2941
|
+
interop: await getSlackBoltInterop(),
|
|
2942
|
+
slackMode,
|
|
2943
|
+
botToken,
|
|
2944
|
+
appToken: appToken ?? void 0,
|
|
2945
|
+
signingSecret: signingSecret ?? void 0,
|
|
2946
|
+
slackWebhookPath,
|
|
2947
|
+
clientOptions,
|
|
2948
|
+
...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {}
|
|
2949
|
+
});
|
|
2950
|
+
const gracefulStop = async () => {
|
|
2951
|
+
await gracefulStopSlackApp(app);
|
|
2952
|
+
};
|
|
2953
|
+
const slackHttpHandler = slackMode === "http" && receiver ? async (req, res) => {
|
|
2954
|
+
const httpReceiver = receiver;
|
|
2955
|
+
const guard = installRequestBodyLimitGuard(req, res, {
|
|
2956
|
+
maxBytes: SLACK_WEBHOOK_MAX_BODY_BYTES,
|
|
2957
|
+
timeoutMs: SLACK_WEBHOOK_BODY_TIMEOUT_MS,
|
|
2958
|
+
responseFormat: "text"
|
|
2959
|
+
});
|
|
2960
|
+
if (guard.isTripped()) return;
|
|
2961
|
+
try {
|
|
2962
|
+
await Promise.resolve(httpReceiver.requestListener(req, res));
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
if (!guard.isTripped()) throw err;
|
|
2965
|
+
} finally {
|
|
2966
|
+
guard.dispose();
|
|
2967
|
+
}
|
|
2968
|
+
} : null;
|
|
2969
|
+
let unregisterHttpHandler = null;
|
|
2970
|
+
let botUserId = "";
|
|
2971
|
+
let botId = "";
|
|
2972
|
+
let teamId = "";
|
|
2973
|
+
let apiAppId = "";
|
|
2974
|
+
const expectedApiAppIdFromAppToken = parseApiAppIdFromAppToken(appToken);
|
|
2975
|
+
try {
|
|
2976
|
+
const auth = await app.client.auth.test({ token: botToken });
|
|
2977
|
+
botUserId = auth.user_id ?? "";
|
|
2978
|
+
botId = auth.bot_id ?? "";
|
|
2979
|
+
teamId = auth.team_id ?? "";
|
|
2980
|
+
apiAppId = auth.api_app_id ?? "";
|
|
2981
|
+
} catch {}
|
|
2982
|
+
if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token api_app_id=${apiAppId} but app token looks like api_app_id=${expectedApiAppIdFromAppToken}`);
|
|
2983
|
+
const ctx = createSlackMonitorContext({
|
|
2984
|
+
cfg,
|
|
2985
|
+
accountId: account.accountId,
|
|
2986
|
+
botToken,
|
|
2987
|
+
app,
|
|
2988
|
+
runtime,
|
|
2989
|
+
botUserId,
|
|
2990
|
+
botId,
|
|
2991
|
+
teamId,
|
|
2992
|
+
apiAppId,
|
|
2993
|
+
historyLimit,
|
|
2994
|
+
dmHistoryLimit,
|
|
2995
|
+
sessionScope,
|
|
2996
|
+
mainKey,
|
|
2997
|
+
dmEnabled,
|
|
2998
|
+
dmPolicy,
|
|
2999
|
+
allowFrom,
|
|
3000
|
+
allowNameMatching,
|
|
3001
|
+
groupDmEnabled,
|
|
3002
|
+
groupDmChannels,
|
|
3003
|
+
defaultRequireMention: slackCfg.requireMention,
|
|
3004
|
+
channelsConfig,
|
|
3005
|
+
groupPolicy,
|
|
3006
|
+
useAccessGroups,
|
|
3007
|
+
reactionMode,
|
|
3008
|
+
reactionAllowlist,
|
|
3009
|
+
replyToMode,
|
|
3010
|
+
threadHistoryScope,
|
|
3011
|
+
threadInheritParent,
|
|
3012
|
+
threadRequireExplicitMention,
|
|
3013
|
+
slashCommand,
|
|
3014
|
+
textLimit,
|
|
3015
|
+
ackReactionScope,
|
|
3016
|
+
typingReaction,
|
|
3017
|
+
mediaMaxBytes,
|
|
3018
|
+
removeAckAfterReply
|
|
3019
|
+
});
|
|
3020
|
+
const trackEvent = opts.setStatus ? () => {
|
|
3021
|
+
opts.setStatus({
|
|
3022
|
+
lastEventAt: Date.now(),
|
|
3023
|
+
lastInboundAt: Date.now()
|
|
3024
|
+
});
|
|
3025
|
+
} : void 0;
|
|
3026
|
+
const handleSlackMessage = createSlackMessageHandler({
|
|
3027
|
+
ctx,
|
|
3028
|
+
account,
|
|
3029
|
+
trackEvent
|
|
3030
|
+
});
|
|
3031
|
+
if (isSlackExecApprovalClientEnabled({
|
|
3032
|
+
cfg,
|
|
3033
|
+
accountId: account.accountId
|
|
3034
|
+
})) registerChannelRuntimeContext({
|
|
3035
|
+
channelRuntime: opts.channelRuntime,
|
|
3036
|
+
channelId: "slack",
|
|
3037
|
+
accountId: account.accountId,
|
|
3038
|
+
capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
|
|
3039
|
+
context: {
|
|
3040
|
+
app,
|
|
3041
|
+
config: slackCfg.execApprovals ?? {}
|
|
3042
|
+
},
|
|
3043
|
+
abortSignal: opts.abortSignal
|
|
3044
|
+
});
|
|
3045
|
+
registerSlackMonitorEvents({
|
|
3046
|
+
ctx,
|
|
3047
|
+
account,
|
|
3048
|
+
handleSlackMessage,
|
|
3049
|
+
trackEvent
|
|
3050
|
+
});
|
|
3051
|
+
await registerSlackMonitorSlashCommands({
|
|
3052
|
+
ctx,
|
|
3053
|
+
account,
|
|
3054
|
+
trackEvent
|
|
3055
|
+
});
|
|
3056
|
+
if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
|
|
3057
|
+
path: slackWebhookPath,
|
|
3058
|
+
handler: slackHttpHandler,
|
|
3059
|
+
log: runtime.log,
|
|
3060
|
+
accountId: account.accountId
|
|
3061
|
+
});
|
|
3062
|
+
if (resolveToken) (async () => {
|
|
3063
|
+
if (opts.abortSignal?.aborted) return;
|
|
3064
|
+
if (channelsConfig && Object.keys(channelsConfig).length > 0) try {
|
|
3065
|
+
const entries = Object.keys(channelsConfig).filter((key) => key !== "*");
|
|
3066
|
+
if (entries.length > 0) {
|
|
3067
|
+
const resolved = await resolveSlackChannelAllowlist({
|
|
3068
|
+
token: resolveToken,
|
|
3069
|
+
entries
|
|
3070
|
+
});
|
|
3071
|
+
const nextChannels = { ...channelsConfig };
|
|
3072
|
+
const mapping = [];
|
|
3073
|
+
const unresolved = [];
|
|
3074
|
+
for (const entry of resolved) {
|
|
3075
|
+
const source = channelsConfig?.[entry.input];
|
|
3076
|
+
if (!source) continue;
|
|
3077
|
+
if (!entry.resolved || !entry.id) {
|
|
3078
|
+
unresolved.push(entry.input);
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
mapping.push(formatSlackChannelResolved(entry));
|
|
3082
|
+
const existing = nextChannels[entry.id] ?? {};
|
|
3083
|
+
nextChannels[entry.id] = {
|
|
3084
|
+
...source,
|
|
3085
|
+
...existing
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
channelsConfig = nextChannels;
|
|
3089
|
+
ctx.channelsConfig = nextChannels;
|
|
3090
|
+
summarizeMapping("slack channels", mapping, unresolved, runtime);
|
|
3091
|
+
}
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
runtime.log?.(`slack channel resolve failed; using config entries. ${formatUnknownError(err)}`);
|
|
3094
|
+
}
|
|
3095
|
+
const allowEntries = normalizeStringEntries(allowFrom).filter((entry) => entry !== "*");
|
|
3096
|
+
if (allowEntries.length > 0) {
|
|
3097
|
+
const stableResolvedUsers = resolveStableSlackUserAllowlistEntries(allowEntries);
|
|
3098
|
+
if (stableResolvedUsers.length > 0) {
|
|
3099
|
+
const { mapping, additions } = buildAllowlistResolutionSummary(stableResolvedUsers, { formatResolved: formatSlackUserResolved });
|
|
3100
|
+
allowFrom = mergeAllowlist({
|
|
3101
|
+
existing: allowFrom,
|
|
3102
|
+
additions
|
|
3103
|
+
});
|
|
3104
|
+
ctx.allowFrom = normalizeAllowList(allowFrom);
|
|
3105
|
+
summarizeMapping("slack users", mapping, [], runtime);
|
|
3106
|
+
}
|
|
3107
|
+
if (allowNameMatching) try {
|
|
3108
|
+
const { mapping, unresolved, additions } = buildAllowlistResolutionSummary(await resolveSlackUserAllowlist({
|
|
3109
|
+
token: resolveToken,
|
|
3110
|
+
entries: allowEntries
|
|
3111
|
+
}), { formatResolved: formatSlackUserResolved });
|
|
3112
|
+
allowFrom = mergeAllowlist({
|
|
3113
|
+
existing: allowFrom,
|
|
3114
|
+
additions
|
|
3115
|
+
});
|
|
3116
|
+
ctx.allowFrom = normalizeAllowList(allowFrom);
|
|
3117
|
+
summarizeMapping("slack users", mapping, unresolved, runtime);
|
|
3118
|
+
} catch (err) {
|
|
3119
|
+
runtime.log?.(`slack user resolve failed; using config entries. ${formatUnknownError(err)}`);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
if (channelsConfig && Object.keys(channelsConfig).length > 0) {
|
|
3123
|
+
const userEntries = /* @__PURE__ */ new Set();
|
|
3124
|
+
for (const channel of Object.values(channelsConfig)) addAllowlistUserEntriesFromConfigEntry(userEntries, channel);
|
|
3125
|
+
if (userEntries.size > 0) {
|
|
3126
|
+
const stableResolvedUsers = resolveStableSlackUserAllowlistEntries(Array.from(userEntries));
|
|
3127
|
+
if (stableResolvedUsers.length > 0) {
|
|
3128
|
+
const { resolvedMap, mapping } = buildAllowlistResolutionSummary(stableResolvedUsers, { formatResolved: formatSlackUserResolved });
|
|
3129
|
+
const nextChannels = patchAllowlistUsersInConfigEntries({
|
|
3130
|
+
entries: channelsConfig,
|
|
3131
|
+
resolvedMap
|
|
3132
|
+
});
|
|
3133
|
+
channelsConfig = nextChannels;
|
|
3134
|
+
ctx.channelsConfig = nextChannels;
|
|
3135
|
+
summarizeMapping("slack channel users", mapping, [], runtime);
|
|
3136
|
+
}
|
|
3137
|
+
if (allowNameMatching) try {
|
|
3138
|
+
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(await resolveSlackUserAllowlist({
|
|
3139
|
+
token: resolveToken,
|
|
3140
|
+
entries: Array.from(userEntries)
|
|
3141
|
+
}), { formatResolved: formatSlackUserResolved });
|
|
3142
|
+
const nextChannels = patchAllowlistUsersInConfigEntries({
|
|
3143
|
+
entries: channelsConfig,
|
|
3144
|
+
resolvedMap
|
|
3145
|
+
});
|
|
3146
|
+
channelsConfig = nextChannels;
|
|
3147
|
+
ctx.channelsConfig = nextChannels;
|
|
3148
|
+
summarizeMapping("slack channel users", mapping, unresolved, runtime);
|
|
3149
|
+
} catch (err) {
|
|
3150
|
+
runtime.log?.(`slack channel user resolve failed; using config entries. ${formatUnknownError(err)}`);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
})();
|
|
3155
|
+
const stopOnAbort = () => {
|
|
3156
|
+
if (opts.abortSignal?.aborted && slackMode === "socket") gracefulStop();
|
|
3157
|
+
};
|
|
3158
|
+
opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true });
|
|
3159
|
+
try {
|
|
3160
|
+
if (slackMode === "socket") {
|
|
3161
|
+
let reconnectAttempts = 0;
|
|
3162
|
+
let hasLoggedSocketConnected = false;
|
|
3163
|
+
while (!opts.abortSignal?.aborted) try {
|
|
3164
|
+
const disconnect = await startSlackSocketAndWaitForDisconnect({
|
|
3165
|
+
app,
|
|
3166
|
+
abortSignal: opts.abortSignal,
|
|
3167
|
+
onStarted: () => {
|
|
3168
|
+
reconnectAttempts = 0;
|
|
3169
|
+
publishSlackConnectedStatus(opts.setStatus);
|
|
3170
|
+
if (!hasLoggedSocketConnected) {
|
|
3171
|
+
hasLoggedSocketConnected = true;
|
|
3172
|
+
runtime.log?.("slack socket mode connected");
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
if (!disconnect) break;
|
|
3177
|
+
if (opts.abortSignal?.aborted) break;
|
|
3178
|
+
publishSlackDisconnectedStatus(opts.setStatus, disconnect.error);
|
|
3179
|
+
if (disconnect.error && isNonRecoverableSlackAuthError(disconnect.error)) {
|
|
3180
|
+
runtime.error?.(`slack socket mode disconnected due to non-recoverable auth error — skipping channel (${formatUnknownError(disconnect.error)})`);
|
|
3181
|
+
throw disconnect.error instanceof Error ? disconnect.error : new Error(formatUnknownError(disconnect.error));
|
|
3182
|
+
}
|
|
3183
|
+
reconnectAttempts += 1;
|
|
3184
|
+
if (SLACK_SOCKET_RECONNECT_POLICY.maxAttempts > 0 && reconnectAttempts >= SLACK_SOCKET_RECONNECT_POLICY.maxAttempts) throw new Error(`Slack socket mode reconnect max attempts reached (${reconnectAttempts}/${SLACK_SOCKET_RECONNECT_POLICY.maxAttempts}) after ${disconnect.event}`);
|
|
3185
|
+
const delayMs = computeBackoff(SLACK_SOCKET_RECONNECT_POLICY, reconnectAttempts);
|
|
3186
|
+
runtime.log?.(warn(formatSlackSocketReconnectMessage({
|
|
3187
|
+
event: disconnect.event,
|
|
3188
|
+
attempt: reconnectAttempts,
|
|
3189
|
+
maxAttempts: SLACK_SOCKET_RECONNECT_POLICY.maxAttempts,
|
|
3190
|
+
delayMs,
|
|
3191
|
+
error: disconnect.error
|
|
3192
|
+
})));
|
|
3193
|
+
await gracefulStop();
|
|
3194
|
+
try {
|
|
3195
|
+
await sleepWithAbort(delayMs, opts.abortSignal);
|
|
3196
|
+
} catch {
|
|
3197
|
+
break;
|
|
3198
|
+
}
|
|
3199
|
+
} catch (err) {
|
|
3200
|
+
if (isNonRecoverableSlackAuthError(err)) {
|
|
3201
|
+
runtime.error?.(`slack socket mode failed to start due to non-recoverable auth error — skipping channel (${formatUnknownError(err)})`);
|
|
3202
|
+
throw err;
|
|
3203
|
+
}
|
|
3204
|
+
reconnectAttempts += 1;
|
|
3205
|
+
if (SLACK_SOCKET_RECONNECT_POLICY.maxAttempts > 0 && reconnectAttempts >= SLACK_SOCKET_RECONNECT_POLICY.maxAttempts) throw err;
|
|
3206
|
+
const delayMs = computeBackoff(SLACK_SOCKET_RECONNECT_POLICY, reconnectAttempts);
|
|
3207
|
+
runtime.error?.(formatSlackSocketStartRetryMessage({
|
|
3208
|
+
attempt: reconnectAttempts,
|
|
3209
|
+
maxAttempts: SLACK_SOCKET_RECONNECT_POLICY.maxAttempts,
|
|
3210
|
+
delayMs,
|
|
3211
|
+
error: err,
|
|
3212
|
+
sdkContext: socketModeLogger.getLastMessage()
|
|
3213
|
+
}));
|
|
3214
|
+
try {
|
|
3215
|
+
await sleepWithAbort(delayMs, opts.abortSignal);
|
|
3216
|
+
} catch {
|
|
3217
|
+
break;
|
|
3218
|
+
}
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
runtime.log?.(`slack http mode listening at ${slackWebhookPath}`);
|
|
3223
|
+
if (!opts.abortSignal?.aborted) await new Promise((resolve) => {
|
|
3224
|
+
opts.abortSignal?.addEventListener("abort", () => resolve(), { once: true });
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
} finally {
|
|
3228
|
+
opts.abortSignal?.removeEventListener("abort", stopOnAbort);
|
|
3229
|
+
unregisterHttpHandler?.();
|
|
3230
|
+
await gracefulStop();
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy;
|
|
3234
|
+
//#endregion
|
|
3235
|
+
export { resolveSlackRuntimeGroupPolicy as n, monitorSlackProvider as t };
|