@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,469 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CiIaOW0V.js";
|
|
2
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
|
+
import { logVerbose as logVerbose$1 } from "openclaw/plugin-sdk/runtime-env";
|
|
4
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
|
+
import { normalizeHostname } from "openclaw/plugin-sdk/host-runtime";
|
|
6
|
+
import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
|
|
7
|
+
import { fetchWithRuntimeDispatcher } from "openclaw/plugin-sdk/runtime-fetch";
|
|
8
|
+
import { fetchRemoteMedia, saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
|
|
9
|
+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
10
|
+
//#region extensions/slack/src/file-reference.ts
|
|
11
|
+
function formatSlackFileReference(file) {
|
|
12
|
+
const name = normalizeOptionalString(file?.name) ?? "file";
|
|
13
|
+
const fileId = normalizeOptionalString(file?.id);
|
|
14
|
+
return fileId ? `${name} (fileId: ${fileId})` : name;
|
|
15
|
+
}
|
|
16
|
+
function formatSlackFileReferenceList(files) {
|
|
17
|
+
if (!files?.length) return "file";
|
|
18
|
+
return files.map((file) => formatSlackFileReference(file)).join(", ");
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region extensions/slack/src/monitor/media-types.ts
|
|
22
|
+
const MAX_SLACK_MEDIA_FILES = 8;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region extensions/slack/src/monitor/thread.ts
|
|
25
|
+
const THREAD_STARTER_CACHE = /* @__PURE__ */ new Map();
|
|
26
|
+
const THREAD_STARTER_CACHE_TTL_MS = 360 * 6e4;
|
|
27
|
+
const THREAD_STARTER_CACHE_MAX = 2e3;
|
|
28
|
+
function evictThreadStarterCache() {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [cacheKey, entry] of THREAD_STARTER_CACHE.entries()) if (now - entry.cachedAt > THREAD_STARTER_CACHE_TTL_MS) THREAD_STARTER_CACHE.delete(cacheKey);
|
|
31
|
+
pruneMapToMaxSize(THREAD_STARTER_CACHE, THREAD_STARTER_CACHE_MAX);
|
|
32
|
+
}
|
|
33
|
+
function formatSlackFilePlaceholder(files) {
|
|
34
|
+
return `[attached: ${formatSlackFileReferenceList(files)}]`;
|
|
35
|
+
}
|
|
36
|
+
async function resolveSlackThreadStarter(params) {
|
|
37
|
+
evictThreadStarterCache();
|
|
38
|
+
const cacheKey = `${params.channelId}:${params.threadTs}`;
|
|
39
|
+
const cached = THREAD_STARTER_CACHE.get(cacheKey);
|
|
40
|
+
if (cached && Date.now() - cached.cachedAt <= THREAD_STARTER_CACHE_TTL_MS) return cached.value;
|
|
41
|
+
if (cached) THREAD_STARTER_CACHE.delete(cacheKey);
|
|
42
|
+
try {
|
|
43
|
+
const message = (await params.client.conversations.replies({
|
|
44
|
+
channel: params.channelId,
|
|
45
|
+
ts: params.threadTs,
|
|
46
|
+
limit: 1,
|
|
47
|
+
inclusive: true
|
|
48
|
+
}))?.messages?.[0];
|
|
49
|
+
const text = (message?.text ?? "").trim();
|
|
50
|
+
const files = message?.files?.length ? message.files : void 0;
|
|
51
|
+
if (!message || !text && !files) return null;
|
|
52
|
+
const starter = {
|
|
53
|
+
text: text || formatSlackFilePlaceholder(files),
|
|
54
|
+
userId: message.user,
|
|
55
|
+
botId: message.bot_id,
|
|
56
|
+
ts: message.ts,
|
|
57
|
+
files
|
|
58
|
+
};
|
|
59
|
+
if (THREAD_STARTER_CACHE.has(cacheKey)) THREAD_STARTER_CACHE.delete(cacheKey);
|
|
60
|
+
THREAD_STARTER_CACHE.set(cacheKey, {
|
|
61
|
+
value: starter,
|
|
62
|
+
cachedAt: Date.now()
|
|
63
|
+
});
|
|
64
|
+
evictThreadStarterCache();
|
|
65
|
+
return starter;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logVerbose$1(`slack thread starter fetch failed channel=${params.channelId} ts=${params.threadTs}: ${formatErrorMessage(err)}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function resetSlackThreadStarterCacheForTest() {
|
|
72
|
+
THREAD_STARTER_CACHE.clear();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fetches the most recent messages in a Slack thread (excluding the current message).
|
|
76
|
+
* Used to populate thread context when a new thread session starts.
|
|
77
|
+
*
|
|
78
|
+
* Uses cursor pagination and keeps only the latest N retained messages so long threads
|
|
79
|
+
* still produce up-to-date context without unbounded memory growth.
|
|
80
|
+
*/
|
|
81
|
+
async function resolveSlackThreadHistory(params) {
|
|
82
|
+
const maxMessages = params.limit ?? 20;
|
|
83
|
+
if (!Number.isFinite(maxMessages) || maxMessages <= 0) return [];
|
|
84
|
+
const fetchLimit = 200;
|
|
85
|
+
const retained = [];
|
|
86
|
+
let cursor;
|
|
87
|
+
try {
|
|
88
|
+
do {
|
|
89
|
+
const response = await params.client.conversations.replies({
|
|
90
|
+
channel: params.channelId,
|
|
91
|
+
ts: params.threadTs,
|
|
92
|
+
limit: fetchLimit,
|
|
93
|
+
inclusive: true,
|
|
94
|
+
...cursor ? { cursor } : {}
|
|
95
|
+
});
|
|
96
|
+
for (const msg of response.messages ?? []) {
|
|
97
|
+
if (!msg.text?.trim() && !msg.files?.length) continue;
|
|
98
|
+
if (params.currentMessageTs && msg.ts === params.currentMessageTs) continue;
|
|
99
|
+
retained.push(msg);
|
|
100
|
+
}
|
|
101
|
+
if (retained.length > maxMessages) retained.splice(0, retained.length - maxMessages);
|
|
102
|
+
const next = response.response_metadata?.next_cursor;
|
|
103
|
+
cursor = typeof next === "string" && next.trim().length > 0 ? next.trim() : void 0;
|
|
104
|
+
} while (cursor);
|
|
105
|
+
return retained.map((msg) => ({
|
|
106
|
+
text: msg.text?.trim() ? msg.text : formatSlackFilePlaceholder(msg.files),
|
|
107
|
+
userId: msg.user,
|
|
108
|
+
botId: msg.bot_id,
|
|
109
|
+
ts: msg.ts,
|
|
110
|
+
files: msg.files
|
|
111
|
+
}));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logVerbose$1(`slack thread history fetch failed channel=${params.channelId} ts=${params.threadTs}: ${formatErrorMessage(err)}`);
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region extensions/slack/src/monitor/media.ts
|
|
119
|
+
var media_exports = /* @__PURE__ */ __exportAll({
|
|
120
|
+
MAX_SLACK_MEDIA_FILES: () => 8,
|
|
121
|
+
SLACK_MEDIA_READ_IDLE_TIMEOUT_MS: () => SLACK_MEDIA_READ_IDLE_TIMEOUT_MS,
|
|
122
|
+
SLACK_MEDIA_TOTAL_TIMEOUT_MS: () => SLACK_MEDIA_TOTAL_TIMEOUT_MS,
|
|
123
|
+
fetchWithSlackAuth: () => fetchWithSlackAuth,
|
|
124
|
+
resetSlackThreadStarterCacheForTest: () => resetSlackThreadStarterCacheForTest,
|
|
125
|
+
resolveSlackAttachmentContent: () => resolveSlackAttachmentContent,
|
|
126
|
+
resolveSlackMedia: () => resolveSlackMedia,
|
|
127
|
+
resolveSlackThreadHistory: () => resolveSlackThreadHistory,
|
|
128
|
+
resolveSlackThreadStarter: () => resolveSlackThreadStarter
|
|
129
|
+
});
|
|
130
|
+
function isSlackHostname(hostname) {
|
|
131
|
+
const normalized = normalizeHostname(hostname);
|
|
132
|
+
if (!normalized) return false;
|
|
133
|
+
return [
|
|
134
|
+
"slack.com",
|
|
135
|
+
"slack-edge.com",
|
|
136
|
+
"slack-files.com"
|
|
137
|
+
].some((suffix) => normalized === suffix || normalized.endsWith(`.${suffix}`));
|
|
138
|
+
}
|
|
139
|
+
function assertSlackFileUrl(rawUrl) {
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = new URL(rawUrl);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(`Invalid Slack file URL: ${rawUrl}`);
|
|
145
|
+
}
|
|
146
|
+
if (parsed.protocol !== "https:") throw new Error(`Refusing Slack file URL with non-HTTPS protocol: ${parsed.protocol}`);
|
|
147
|
+
if (!isSlackHostname(parsed.hostname)) throw new Error(`Refusing to send Slack token to non-Slack host "${parsed.hostname}" (url: ${rawUrl})`);
|
|
148
|
+
return parsed;
|
|
149
|
+
}
|
|
150
|
+
function createSlackAuthHeaders(token) {
|
|
151
|
+
return { Authorization: `Bearer ${token}` };
|
|
152
|
+
}
|
|
153
|
+
function createSlackMediaRequest(url, token) {
|
|
154
|
+
return {
|
|
155
|
+
url: assertSlackFileUrl(url).href,
|
|
156
|
+
requestInit: { headers: createSlackAuthHeaders(token) }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function isMockedFetch(fetchImpl) {
|
|
160
|
+
if (typeof fetchImpl !== "function") return false;
|
|
161
|
+
return typeof fetchImpl.mock === "object";
|
|
162
|
+
}
|
|
163
|
+
function createSlackMediaFetch() {
|
|
164
|
+
return async (input, init) => {
|
|
165
|
+
const url = resolveRequestUrl(input);
|
|
166
|
+
if (!url) throw new Error("Unsupported fetch input: expected string, URL, or Request");
|
|
167
|
+
const parsed = assertSlackFileUrl(url);
|
|
168
|
+
return ("dispatcher" in (init ?? {}) && !isMockedFetch(globalThis.fetch) ? fetchWithRuntimeDispatcher : globalThis.fetch)(parsed.href, {
|
|
169
|
+
...init,
|
|
170
|
+
redirect: "manual"
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function resolveSlackFetchForRuntime() {
|
|
175
|
+
return isMockedFetch(globalThis.fetch) ? globalThis.fetch : fetchWithRuntimeDispatcher;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Fetches a URL with Authorization header while keeping same-origin redirects
|
|
179
|
+
* authenticated and dropping auth once the redirect crosses origins.
|
|
180
|
+
*/
|
|
181
|
+
async function fetchWithSlackAuth(url, token) {
|
|
182
|
+
const parsed = assertSlackFileUrl(url);
|
|
183
|
+
const authHeaders = createSlackAuthHeaders(token);
|
|
184
|
+
const fetchImpl = resolveSlackFetchForRuntime();
|
|
185
|
+
const initialRes = await fetchImpl(parsed.href, {
|
|
186
|
+
headers: authHeaders,
|
|
187
|
+
redirect: "manual"
|
|
188
|
+
});
|
|
189
|
+
if (initialRes.status < 300 || initialRes.status >= 400) return initialRes;
|
|
190
|
+
const redirectUrl = initialRes.headers.get("location");
|
|
191
|
+
if (!redirectUrl) return initialRes;
|
|
192
|
+
let resolvedUrl;
|
|
193
|
+
try {
|
|
194
|
+
resolvedUrl = new URL(redirectUrl, parsed.href);
|
|
195
|
+
} catch {
|
|
196
|
+
return initialRes;
|
|
197
|
+
}
|
|
198
|
+
if (resolvedUrl.protocol !== "https:") return initialRes;
|
|
199
|
+
if (resolvedUrl.origin === parsed.origin) return fetchImpl(resolvedUrl.toString(), {
|
|
200
|
+
headers: authHeaders,
|
|
201
|
+
redirect: "follow"
|
|
202
|
+
});
|
|
203
|
+
return fetchImpl(resolvedUrl.toString(), { redirect: "follow" });
|
|
204
|
+
}
|
|
205
|
+
const SLACK_MEDIA_SSRF_POLICY = {
|
|
206
|
+
allowedHostnames: [
|
|
207
|
+
"*.slack.com",
|
|
208
|
+
"*.slack-edge.com",
|
|
209
|
+
"*.slack-files.com"
|
|
210
|
+
],
|
|
211
|
+
hostnameAllowlist: [
|
|
212
|
+
"*.slack.com",
|
|
213
|
+
"*.slack-edge.com",
|
|
214
|
+
"*.slack-files.com"
|
|
215
|
+
],
|
|
216
|
+
allowRfc2544BenchmarkRange: true
|
|
217
|
+
};
|
|
218
|
+
const SLACK_MEDIA_READ_IDLE_TIMEOUT_MS = 6e4;
|
|
219
|
+
const SLACK_MEDIA_TOTAL_TIMEOUT_MS = 12e4;
|
|
220
|
+
function mergeAbortSignals(signals) {
|
|
221
|
+
const activeSignals = signals.filter((signal) => Boolean(signal));
|
|
222
|
+
if (activeSignals.length === 0) return;
|
|
223
|
+
if (activeSignals.length === 1) return activeSignals[0];
|
|
224
|
+
if (typeof AbortSignal.any === "function") return AbortSignal.any(activeSignals);
|
|
225
|
+
const controller = new AbortController();
|
|
226
|
+
for (const signal of activeSignals) if (signal.aborted) {
|
|
227
|
+
controller.abort();
|
|
228
|
+
return controller.signal;
|
|
229
|
+
}
|
|
230
|
+
const abort = () => {
|
|
231
|
+
controller.abort();
|
|
232
|
+
for (const signal of activeSignals) signal.removeEventListener("abort", abort);
|
|
233
|
+
};
|
|
234
|
+
for (const signal of activeSignals) signal.addEventListener("abort", abort, { once: true });
|
|
235
|
+
return controller.signal;
|
|
236
|
+
}
|
|
237
|
+
async function fetchSlackMedia(params) {
|
|
238
|
+
const timeoutAbortController = params.totalTimeoutMs ? new AbortController() : void 0;
|
|
239
|
+
const signal = mergeAbortSignals([
|
|
240
|
+
params.abortSignal,
|
|
241
|
+
params.options.requestInit?.signal ?? void 0,
|
|
242
|
+
timeoutAbortController?.signal
|
|
243
|
+
]);
|
|
244
|
+
let timedOut = false;
|
|
245
|
+
let timeoutHandle = null;
|
|
246
|
+
const fetchPromise = fetchRemoteMedia({
|
|
247
|
+
...params.options,
|
|
248
|
+
readIdleTimeoutMs: params.readIdleTimeoutMs ?? 6e4,
|
|
249
|
+
...signal ? { requestInit: {
|
|
250
|
+
...params.options.requestInit,
|
|
251
|
+
signal
|
|
252
|
+
} } : {}
|
|
253
|
+
}).catch((error) => {
|
|
254
|
+
if (timedOut) return new Promise(() => {});
|
|
255
|
+
throw error;
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
if (!params.totalTimeoutMs) return await fetchPromise;
|
|
259
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
260
|
+
timeoutHandle = setTimeout(() => {
|
|
261
|
+
timedOut = true;
|
|
262
|
+
timeoutAbortController?.abort();
|
|
263
|
+
reject(/* @__PURE__ */ new Error(`slack media download timed out after ${params.totalTimeoutMs}ms`));
|
|
264
|
+
}, params.totalTimeoutMs);
|
|
265
|
+
timeoutHandle.unref?.();
|
|
266
|
+
});
|
|
267
|
+
return await Promise.race([fetchPromise, timeoutPromise]);
|
|
268
|
+
} finally {
|
|
269
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Slack voice messages (audio clips, huddle recordings) carry a `subtype` of
|
|
274
|
+
* `"slack_audio"` but are served with a `video/*` MIME type (e.g. `video/mp4`,
|
|
275
|
+
* `video/webm`). Override the primary type to `audio/` so the
|
|
276
|
+
* media-understanding pipeline routes them to transcription.
|
|
277
|
+
*/
|
|
278
|
+
function resolveSlackMediaMimetype(file, fetchedContentType) {
|
|
279
|
+
const mime = fetchedContentType ?? file.mimetype;
|
|
280
|
+
if (file.subtype === "slack_audio" && mime?.startsWith("video/")) return mime.replace("video/", "audio/");
|
|
281
|
+
return mime;
|
|
282
|
+
}
|
|
283
|
+
function looksLikeHtmlBuffer(buffer) {
|
|
284
|
+
const head = normalizeLowercaseStringOrEmpty(buffer.subarray(0, 512).toString("utf-8").replace(/^\s+/, ""));
|
|
285
|
+
return head.startsWith("<!doctype html") || head.startsWith("<html");
|
|
286
|
+
}
|
|
287
|
+
const MAX_SLACK_MEDIA_CONCURRENCY = 3;
|
|
288
|
+
const MAX_SLACK_FORWARDED_ATTACHMENTS = 8;
|
|
289
|
+
async function fetchFreshSlackFileUrl(params) {
|
|
290
|
+
if (!params.file.id || !params.client) return null;
|
|
291
|
+
try {
|
|
292
|
+
const freshFile = (await params.client.files.info({ file: params.file.id })).file;
|
|
293
|
+
const freshUrl = freshFile?.url_private_download ?? freshFile?.url_private;
|
|
294
|
+
if (freshUrl) {
|
|
295
|
+
logVerbose$1(`slack: refreshed file URL via files.info for file id=${params.file.id}`);
|
|
296
|
+
return freshUrl;
|
|
297
|
+
}
|
|
298
|
+
logVerbose$1(`slack: files.info returned no private URL for file id=${params.file.id}`);
|
|
299
|
+
return null;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logVerbose$1(`slack: files.info failed for file id=${params.file.id}: ${formatErrorMessage(error)}`);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function downloadSlackMediaFile(params) {
|
|
306
|
+
const { url: slackUrl, requestInit } = createSlackMediaRequest(params.url, params.token);
|
|
307
|
+
const fetched = await fetchSlackMedia({
|
|
308
|
+
options: {
|
|
309
|
+
url: slackUrl,
|
|
310
|
+
fetchImpl: createSlackMediaFetch(),
|
|
311
|
+
requestInit,
|
|
312
|
+
filePathHint: params.file.name,
|
|
313
|
+
maxBytes: params.maxBytes,
|
|
314
|
+
ssrfPolicy: SLACK_MEDIA_SSRF_POLICY
|
|
315
|
+
},
|
|
316
|
+
readIdleTimeoutMs: params.readIdleTimeoutMs,
|
|
317
|
+
totalTimeoutMs: params.totalTimeoutMs ?? 12e4,
|
|
318
|
+
abortSignal: params.abortSignal
|
|
319
|
+
});
|
|
320
|
+
if (fetched.buffer.byteLength > params.maxBytes) return null;
|
|
321
|
+
const fileMime = normalizeOptionalLowercaseString(params.file.mimetype);
|
|
322
|
+
const fileName = normalizeLowercaseStringOrEmpty(params.file.name);
|
|
323
|
+
if (!(fileMime === "text/html" || fileName.endsWith(".html") || fileName.endsWith(".htm"))) {
|
|
324
|
+
if (normalizeOptionalLowercaseString(fetched.contentType?.split(";")[0]) === "text/html" || looksLikeHtmlBuffer(fetched.buffer)) return null;
|
|
325
|
+
}
|
|
326
|
+
const effectiveMime = resolveSlackMediaMimetype(params.file, fetched.contentType);
|
|
327
|
+
const saved = await saveMediaBuffer(fetched.buffer, effectiveMime, "inbound", params.maxBytes);
|
|
328
|
+
const label = fetched.fileName ?? params.file.name;
|
|
329
|
+
const contentType = effectiveMime ?? saved.contentType;
|
|
330
|
+
return {
|
|
331
|
+
path: saved.path,
|
|
332
|
+
...contentType ? { contentType } : {},
|
|
333
|
+
placeholder: `[Slack file: ${formatSlackFileReference({
|
|
334
|
+
...params.file,
|
|
335
|
+
name: label
|
|
336
|
+
})}]`
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function isForwardedSlackAttachment(attachment) {
|
|
340
|
+
return attachment.is_share === true;
|
|
341
|
+
}
|
|
342
|
+
function resolveForwardedAttachmentImageUrl(attachment) {
|
|
343
|
+
const rawUrl = attachment.image_url?.trim();
|
|
344
|
+
if (!rawUrl) return null;
|
|
345
|
+
try {
|
|
346
|
+
const parsed = new URL(rawUrl);
|
|
347
|
+
if (parsed.protocol !== "https:" || !isSlackHostname(parsed.hostname)) return null;
|
|
348
|
+
return parsed.toString();
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function mapLimit(items, limit, fn) {
|
|
354
|
+
if (items.length === 0) return [];
|
|
355
|
+
const results = [];
|
|
356
|
+
results.length = items.length;
|
|
357
|
+
let nextIndex = 0;
|
|
358
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
359
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
360
|
+
while (true) {
|
|
361
|
+
const idx = nextIndex++;
|
|
362
|
+
if (idx >= items.length) return;
|
|
363
|
+
results[idx] = await fn(items[idx]);
|
|
364
|
+
}
|
|
365
|
+
}));
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Downloads all files attached to a Slack message and returns them as an array.
|
|
370
|
+
* Returns `null` when no files could be downloaded.
|
|
371
|
+
*/
|
|
372
|
+
async function resolveSlackMedia(params) {
|
|
373
|
+
const files = params.files ?? [];
|
|
374
|
+
const results = (await mapLimit(files.length > 8 ? files.slice(0, 8) : files, MAX_SLACK_MEDIA_CONCURRENCY, async (file) => {
|
|
375
|
+
const eventUrl = file.url_private_download ?? file.url_private;
|
|
376
|
+
const url = eventUrl ?? await fetchFreshSlackFileUrl({
|
|
377
|
+
file,
|
|
378
|
+
client: params.client
|
|
379
|
+
});
|
|
380
|
+
if (!url) return null;
|
|
381
|
+
const result = await downloadSlackMediaFile({
|
|
382
|
+
file,
|
|
383
|
+
url,
|
|
384
|
+
token: params.token,
|
|
385
|
+
maxBytes: params.maxBytes,
|
|
386
|
+
readIdleTimeoutMs: params.readIdleTimeoutMs,
|
|
387
|
+
totalTimeoutMs: params.totalTimeoutMs,
|
|
388
|
+
abortSignal: params.abortSignal
|
|
389
|
+
}).catch(() => null);
|
|
390
|
+
if (result || !eventUrl) return result;
|
|
391
|
+
const freshUrl = await fetchFreshSlackFileUrl({
|
|
392
|
+
file,
|
|
393
|
+
client: params.client
|
|
394
|
+
});
|
|
395
|
+
if (!freshUrl) return null;
|
|
396
|
+
return await downloadSlackMediaFile({
|
|
397
|
+
file,
|
|
398
|
+
url: freshUrl,
|
|
399
|
+
token: params.token,
|
|
400
|
+
maxBytes: params.maxBytes,
|
|
401
|
+
readIdleTimeoutMs: params.readIdleTimeoutMs,
|
|
402
|
+
totalTimeoutMs: params.totalTimeoutMs,
|
|
403
|
+
abortSignal: params.abortSignal
|
|
404
|
+
}).catch(() => null);
|
|
405
|
+
})).filter((entry) => Boolean(entry));
|
|
406
|
+
return results.length > 0 ? results : null;
|
|
407
|
+
}
|
|
408
|
+
/** Extracts text and media from forwarded-message attachments. Returns null when empty. */
|
|
409
|
+
async function resolveSlackAttachmentContent(params) {
|
|
410
|
+
const attachments = params.attachments;
|
|
411
|
+
if (!attachments || attachments.length === 0) return null;
|
|
412
|
+
const forwardedAttachments = attachments.filter((attachment) => isForwardedSlackAttachment(attachment)).slice(0, MAX_SLACK_FORWARDED_ATTACHMENTS);
|
|
413
|
+
if (forwardedAttachments.length === 0) return null;
|
|
414
|
+
const textBlocks = [];
|
|
415
|
+
const allMedia = [];
|
|
416
|
+
for (const att of forwardedAttachments) {
|
|
417
|
+
const text = att.text?.trim() || att.fallback?.trim();
|
|
418
|
+
if (text) {
|
|
419
|
+
const author = att.author_name;
|
|
420
|
+
const heading = author ? `[Forwarded message from ${author}]` : "[Forwarded message]";
|
|
421
|
+
textBlocks.push(`${heading}\n${text}`);
|
|
422
|
+
}
|
|
423
|
+
const imageUrl = resolveForwardedAttachmentImageUrl(att);
|
|
424
|
+
if (imageUrl) try {
|
|
425
|
+
const { url: slackUrl, requestInit } = createSlackMediaRequest(imageUrl, params.token);
|
|
426
|
+
const fetched = await fetchSlackMedia({
|
|
427
|
+
options: {
|
|
428
|
+
url: slackUrl,
|
|
429
|
+
fetchImpl: createSlackMediaFetch(),
|
|
430
|
+
requestInit,
|
|
431
|
+
maxBytes: params.maxBytes,
|
|
432
|
+
ssrfPolicy: SLACK_MEDIA_SSRF_POLICY
|
|
433
|
+
},
|
|
434
|
+
readIdleTimeoutMs: params.readIdleTimeoutMs,
|
|
435
|
+
totalTimeoutMs: params.totalTimeoutMs ?? 12e4,
|
|
436
|
+
abortSignal: params.abortSignal
|
|
437
|
+
});
|
|
438
|
+
if (fetched.buffer.byteLength <= params.maxBytes) {
|
|
439
|
+
const saved = await saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", params.maxBytes);
|
|
440
|
+
const label = fetched.fileName ?? "forwarded image";
|
|
441
|
+
allMedia.push({
|
|
442
|
+
path: saved.path,
|
|
443
|
+
contentType: fetched.contentType ?? saved.contentType,
|
|
444
|
+
placeholder: `[Forwarded image: ${label}]`
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
} catch {}
|
|
448
|
+
if (att.files && att.files.length > 0) {
|
|
449
|
+
const fileMedia = await resolveSlackMedia({
|
|
450
|
+
files: att.files,
|
|
451
|
+
client: params.client,
|
|
452
|
+
token: params.token,
|
|
453
|
+
maxBytes: params.maxBytes,
|
|
454
|
+
readIdleTimeoutMs: params.readIdleTimeoutMs,
|
|
455
|
+
totalTimeoutMs: params.totalTimeoutMs,
|
|
456
|
+
abortSignal: params.abortSignal
|
|
457
|
+
});
|
|
458
|
+
if (fileMedia) allMedia.push(...fileMedia);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const combinedText = textBlocks.join("\n\n");
|
|
462
|
+
if (!combinedText && allMedia.length === 0) return null;
|
|
463
|
+
return {
|
|
464
|
+
text: combinedText,
|
|
465
|
+
media: allMedia
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
//#endregion
|
|
469
|
+
export { MAX_SLACK_MEDIA_FILES as a, resolveSlackThreadStarter as i, resolveSlackMedia as n, formatSlackFileReference as o, resolveSlackThreadHistory as r, media_exports as t };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { a as resolveSlackAccount, t as listEnabledSlackAccounts } from "./accounts-ClAPP5ry.js";
|
|
2
|
+
import { n as isSlackInteractiveRepliesEnabled } from "./interactive-replies-qAIfuBor.js";
|
|
3
|
+
import { createActionGate } from "openclaw/plugin-sdk/channel-actions";
|
|
4
|
+
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
//#region extensions/slack/src/message-actions.ts
|
|
7
|
+
function listSlackMessageActions(cfg, accountId) {
|
|
8
|
+
const accounts = (accountId ? [resolveSlackAccount({
|
|
9
|
+
cfg,
|
|
10
|
+
accountId
|
|
11
|
+
})] : listEnabledSlackAccounts(cfg)).filter((account) => account.enabled && account.botTokenSource !== "none");
|
|
12
|
+
if (accounts.length === 0) return [];
|
|
13
|
+
const isActionEnabled = (key, defaultValue = true) => {
|
|
14
|
+
for (const account of accounts) if (createActionGate(account.actions ?? cfg.channels?.slack?.actions)(key, defaultValue)) return true;
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
const actions = new Set(["send"]);
|
|
18
|
+
if (isActionEnabled("reactions")) {
|
|
19
|
+
actions.add("react");
|
|
20
|
+
actions.add("reactions");
|
|
21
|
+
}
|
|
22
|
+
if (isActionEnabled("messages")) {
|
|
23
|
+
actions.add("read");
|
|
24
|
+
actions.add("edit");
|
|
25
|
+
actions.add("delete");
|
|
26
|
+
actions.add("download-file");
|
|
27
|
+
actions.add("upload-file");
|
|
28
|
+
}
|
|
29
|
+
if (isActionEnabled("pins")) {
|
|
30
|
+
actions.add("pin");
|
|
31
|
+
actions.add("unpin");
|
|
32
|
+
actions.add("list-pins");
|
|
33
|
+
}
|
|
34
|
+
if (isActionEnabled("memberInfo")) actions.add("member-info");
|
|
35
|
+
if (isActionEnabled("emojiList")) actions.add("emoji-list");
|
|
36
|
+
return Array.from(actions);
|
|
37
|
+
}
|
|
38
|
+
function extractSlackToolSend(args) {
|
|
39
|
+
return extractToolSend(args, "sendMessage");
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region extensions/slack/src/message-tool-api.ts
|
|
43
|
+
const SLACK_MESSAGE_ID_ACTIONS = [
|
|
44
|
+
"react",
|
|
45
|
+
"reactions",
|
|
46
|
+
"edit",
|
|
47
|
+
"delete",
|
|
48
|
+
"pin",
|
|
49
|
+
"unpin"
|
|
50
|
+
];
|
|
51
|
+
function createSlackFileActionSchema() {
|
|
52
|
+
return { fileId: Type.Optional(Type.String({ description: "Slack file id, starting with \"F\" (for example F0B0LTT8M36). Required for action=\"download-file\". Read it from inbound Slack file metadata at event.files[].id. This is not the Slack message timestamp/messageId." })) };
|
|
53
|
+
}
|
|
54
|
+
function createSlackMessageIdActionSchema() {
|
|
55
|
+
const description = "Slack message timestamp/message id (for example \"1777423717.666499\"). Used by react, reactions, edit, delete, pin, and unpin actions. Not used by download-file, which requires fileId from event.files[].id.";
|
|
56
|
+
return {
|
|
57
|
+
messageId: Type.Optional(Type.String({ description })),
|
|
58
|
+
message_id: Type.Optional(Type.String({ description: `${description} Alias for messageId.` }))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function createSlackSendActionSchema() {
|
|
62
|
+
return {
|
|
63
|
+
topLevel: Type.Optional(Type.Boolean({ description: "Slack-only opt-out for action=\"send\" from a threaded same-channel context. Set true to post a new parent-channel message instead of inheriting the current Slack thread. `threadId: null` is accepted as the same top-level request." })),
|
|
64
|
+
replyBroadcast: Type.Optional(Type.Boolean({ description: "Slack-only opt-in for action=\"send\" thread replies. Set true with threadId or replyTo on text/block sends to also broadcast the reply to the parent channel. Not supported for media or upload-file." }))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function createSlackTopLevelActionSchema() {
|
|
68
|
+
return { topLevel: Type.Optional(Type.Boolean({ description: "Slack-only opt-out from threaded same-channel context. Set true to post at the channel root instead of inheriting the current Slack thread." })) };
|
|
69
|
+
}
|
|
70
|
+
function describeSlackMessageTool({ cfg, accountId }) {
|
|
71
|
+
const actions = listSlackMessageActions(cfg, accountId);
|
|
72
|
+
const capabilities = /* @__PURE__ */ new Set();
|
|
73
|
+
const schema = [];
|
|
74
|
+
if (actions.includes("send")) capabilities.add("presentation");
|
|
75
|
+
if (isSlackInteractiveRepliesEnabled({
|
|
76
|
+
cfg,
|
|
77
|
+
accountId
|
|
78
|
+
})) capabilities.add("presentation");
|
|
79
|
+
if (actions.includes("download-file")) schema.push({
|
|
80
|
+
properties: createSlackFileActionSchema(),
|
|
81
|
+
actions: ["download-file"]
|
|
82
|
+
});
|
|
83
|
+
if (actions.includes("send")) schema.push({
|
|
84
|
+
properties: createSlackSendActionSchema(),
|
|
85
|
+
actions: ["send"]
|
|
86
|
+
});
|
|
87
|
+
if (actions.includes("upload-file")) schema.push({
|
|
88
|
+
properties: createSlackTopLevelActionSchema(),
|
|
89
|
+
actions: ["upload-file"]
|
|
90
|
+
});
|
|
91
|
+
const messageIdActions = [];
|
|
92
|
+
for (const action of SLACK_MESSAGE_ID_ACTIONS) if (actions.includes(action)) messageIdActions.push(action);
|
|
93
|
+
if (messageIdActions.length > 0) schema.push({
|
|
94
|
+
properties: createSlackMessageIdActionSchema(),
|
|
95
|
+
actions: messageIdActions
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
actions,
|
|
99
|
+
capabilities: Array.from(capabilities),
|
|
100
|
+
schema: schema.length > 0 ? schema : null
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { extractSlackToolSend as n, listSlackMessageActions as r, describeSlackMessageTool as t };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CiIaOW0V.js";
|
|
2
|
+
import { D as buildSlackSlashCommandMatcher, p as isSlackChannelAllowedByPolicy } from "./room-context-0vovmZPU.js";
|
|
3
|
+
import { t as monitorSlackProvider } from "./provider-D7uAN3Fq.js";
|
|
4
|
+
import { o as resolveSlackThreadTs } from "./replies-Xe_jMR6o.js";
|
|
5
|
+
//#region extensions/slack/src/monitor.ts
|
|
6
|
+
var monitor_exports = /* @__PURE__ */ __exportAll({
|
|
7
|
+
buildSlackSlashCommandMatcher: () => buildSlackSlashCommandMatcher,
|
|
8
|
+
isSlackChannelAllowedByPolicy: () => isSlackChannelAllowedByPolicy,
|
|
9
|
+
monitorSlackProvider: () => monitorSlackProvider,
|
|
10
|
+
resolveSlackThreadTs: () => resolveSlackThreadTs
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { monitor_exports as t };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region extensions/slack/src/monitor/mrkdwn.ts
|
|
2
|
+
function escapeSlackMrkdwn(value) {
|
|
3
|
+
return value.replaceAll("\\", "\\\\").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replace(/([*_`~])/g, "\\$1");
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { escapeSlackMrkdwn as t };
|