sparkecoder 0.1.140 → 0.1.141
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/agent/index.js +554 -4
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +628 -274
- package/dist/cli.js.map +1 -1
- package/dist/index.js +624 -272
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +624 -272
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +1 -1
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/.next/static/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/runtime-config.json +2 -2
- package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +14 -3
- package/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
- package/web/.next/standalone/web/.next/static/static/chunks/20ca4e35e9bb3e94.js +0 -3
- package/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
- /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -585,6 +585,7 @@ function loadConfig(configPath, workingDirectory) {
|
|
|
585
585
|
}
|
|
586
586
|
if (process.env.SPARKECODER_PORT) {
|
|
587
587
|
rawConfig.server = {
|
|
588
|
+
...rawConfig.server ?? {},
|
|
588
589
|
port: parseInt(process.env.SPARKECODER_PORT, 10),
|
|
589
590
|
host: rawConfig.server?.host ?? "127.0.0.1"
|
|
590
591
|
};
|
|
@@ -8597,7 +8598,7 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8597
8598
|
const adapter = getSlackAdapter();
|
|
8598
8599
|
if (!adapter) return { ok: false, error: "slack_not_configured" };
|
|
8599
8600
|
const key2 = reactionKey(channel, timestamp);
|
|
8600
|
-
const
|
|
8601
|
+
const inFlight2 = (async () => {
|
|
8601
8602
|
try {
|
|
8602
8603
|
const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
|
|
8603
8604
|
if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
|
|
@@ -8609,11 +8610,11 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8609
8610
|
return { ok: false, error: err?.message || "unknown" };
|
|
8610
8611
|
}
|
|
8611
8612
|
})();
|
|
8612
|
-
pendingAdds.set(key2,
|
|
8613
|
-
void
|
|
8614
|
-
if (pendingAdds.get(key2) ===
|
|
8613
|
+
pendingAdds.set(key2, inFlight2);
|
|
8614
|
+
void inFlight2.finally(() => {
|
|
8615
|
+
if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
|
|
8615
8616
|
});
|
|
8616
|
-
return
|
|
8617
|
+
return inFlight2;
|
|
8617
8618
|
}
|
|
8618
8619
|
async function removeLoadingReaction(channel, timestamp) {
|
|
8619
8620
|
const adapter = getSlackAdapter();
|
|
@@ -8861,17 +8862,28 @@ async function fetchBotParticipatedInThread(channel, threadTs) {
|
|
|
8861
8862
|
const self = await ensureSlackSelfIdentity();
|
|
8862
8863
|
if (!self) return false;
|
|
8863
8864
|
try {
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8865
|
+
let cursor = "";
|
|
8866
|
+
for (let page = 0; page < 10; page++) {
|
|
8867
|
+
const params = new URLSearchParams({ channel, ts: threadTs, limit: "200" });
|
|
8868
|
+
if (cursor) params.set("cursor", cursor);
|
|
8869
|
+
const res = await fetch(`https://slack.com/api/conversations.replies?${params.toString()}`, {
|
|
8870
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
8871
|
+
});
|
|
8872
|
+
const data = await res.json().catch(() => ({}));
|
|
8873
|
+
if (!data?.ok) {
|
|
8874
|
+
console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
|
|
8875
|
+
return false;
|
|
8876
|
+
}
|
|
8877
|
+
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
8878
|
+
if (messages.some(
|
|
8879
|
+
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
8880
|
+
)) {
|
|
8881
|
+
return true;
|
|
8882
|
+
}
|
|
8883
|
+
cursor = String(data.response_metadata?.next_cursor || "");
|
|
8884
|
+
if (!cursor) break;
|
|
8870
8885
|
}
|
|
8871
|
-
|
|
8872
|
-
return messages.some(
|
|
8873
|
-
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
8874
|
-
);
|
|
8886
|
+
return false;
|
|
8875
8887
|
} catch (err) {
|
|
8876
8888
|
console.warn(`[slack] conversations.replies error:`, err?.message || err);
|
|
8877
8889
|
return false;
|
|
@@ -9323,7 +9335,7 @@ async function reconcileOnce(now = Date.now()) {
|
|
|
9323
9335
|
entry2.updatedAt = Date.now();
|
|
9324
9336
|
const nudged = {
|
|
9325
9337
|
...entry2.event,
|
|
9326
|
-
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no reply,
|
|
9338
|
+
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no substantive reply, post a brief acknowledgement.]
|
|
9327
9339
|
${entry2.event.content}`,
|
|
9328
9340
|
wake: "now"
|
|
9329
9341
|
};
|
|
@@ -9750,6 +9762,428 @@ var init_registry = __esm({
|
|
|
9750
9762
|
}
|
|
9751
9763
|
});
|
|
9752
9764
|
|
|
9765
|
+
// src/integrations/slack/files.ts
|
|
9766
|
+
var files_exports = {};
|
|
9767
|
+
__export(files_exports, {
|
|
9768
|
+
INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
|
|
9769
|
+
MAX_BYTES: () => MAX_BYTES,
|
|
9770
|
+
formatFileBlock: () => formatFileBlock,
|
|
9771
|
+
ingestSlackFiles: () => ingestSlackFiles
|
|
9772
|
+
});
|
|
9773
|
+
function inferFileName(file) {
|
|
9774
|
+
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
9775
|
+
}
|
|
9776
|
+
function inferContentType(file) {
|
|
9777
|
+
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
9778
|
+
return "application/octet-stream";
|
|
9779
|
+
}
|
|
9780
|
+
function formatBytes(n) {
|
|
9781
|
+
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
9782
|
+
if (n < 1024) return `${n} B`;
|
|
9783
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
9784
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
9785
|
+
}
|
|
9786
|
+
function sleep(ms) {
|
|
9787
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
9788
|
+
}
|
|
9789
|
+
function imageMagic(bytes) {
|
|
9790
|
+
if (bytes.length < 12) return null;
|
|
9791
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
9792
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
|
|
9793
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
|
|
9794
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
|
|
9795
|
+
return null;
|
|
9796
|
+
}
|
|
9797
|
+
function requiresRasterMagic(contentType) {
|
|
9798
|
+
const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
|
|
9799
|
+
return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
|
|
9800
|
+
}
|
|
9801
|
+
function validateDownloadedBytes(bytes, declaredContentType) {
|
|
9802
|
+
if (!requiresRasterMagic(declaredContentType)) return null;
|
|
9803
|
+
const actual = imageMagic(bytes);
|
|
9804
|
+
if (!actual) return "invalid_image_bytes";
|
|
9805
|
+
return null;
|
|
9806
|
+
}
|
|
9807
|
+
async function fetchSlackPrivateFile(sourceUrl, botToken) {
|
|
9808
|
+
let lastError = "unknown";
|
|
9809
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
9810
|
+
try {
|
|
9811
|
+
const res = await fetch(sourceUrl, {
|
|
9812
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
9813
|
+
});
|
|
9814
|
+
if (res.status === 429 || res.status >= 500) {
|
|
9815
|
+
lastError = `slack_fetch_${res.status}`;
|
|
9816
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
9817
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
|
|
9818
|
+
if (attempt < 2) {
|
|
9819
|
+
await sleep(waitMs);
|
|
9820
|
+
continue;
|
|
9821
|
+
}
|
|
9822
|
+
}
|
|
9823
|
+
if (!res.ok) {
|
|
9824
|
+
return { ok: false, error: `slack_fetch_${res.status}` };
|
|
9825
|
+
}
|
|
9826
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
9827
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
|
|
9828
|
+
return { ok: false, error: "size_exceeded" };
|
|
9829
|
+
}
|
|
9830
|
+
const ab = await res.arrayBuffer();
|
|
9831
|
+
if (ab.byteLength > MAX_BYTES) {
|
|
9832
|
+
return { ok: false, error: "size_exceeded" };
|
|
9833
|
+
}
|
|
9834
|
+
return { ok: true, bytes: Buffer.from(ab) };
|
|
9835
|
+
} catch (err) {
|
|
9836
|
+
lastError = `slack_fetch_error:${err?.message || "unknown"}`;
|
|
9837
|
+
if (attempt < 2) {
|
|
9838
|
+
await sleep(250 * (attempt + 1));
|
|
9839
|
+
continue;
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
}
|
|
9843
|
+
return { ok: false, error: lastError };
|
|
9844
|
+
}
|
|
9845
|
+
function withTimeout(p, ms, label) {
|
|
9846
|
+
return new Promise((resolve13, reject) => {
|
|
9847
|
+
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
9848
|
+
p.then(
|
|
9849
|
+
(v) => {
|
|
9850
|
+
clearTimeout(t);
|
|
9851
|
+
resolve13(v);
|
|
9852
|
+
},
|
|
9853
|
+
(e) => {
|
|
9854
|
+
clearTimeout(t);
|
|
9855
|
+
reject(e);
|
|
9856
|
+
}
|
|
9857
|
+
);
|
|
9858
|
+
});
|
|
9859
|
+
}
|
|
9860
|
+
async function ingestOne(file, sessionId, botToken) {
|
|
9861
|
+
const fileName = inferFileName(file);
|
|
9862
|
+
const contentType = inferContentType(file);
|
|
9863
|
+
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
9864
|
+
const base = {
|
|
9865
|
+
slackFileId: file.id,
|
|
9866
|
+
fileName,
|
|
9867
|
+
contentType,
|
|
9868
|
+
sizeBytes: declaredSize
|
|
9869
|
+
};
|
|
9870
|
+
const sourceUrl = file.url_private_download || file.url_private;
|
|
9871
|
+
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
9872
|
+
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
9873
|
+
}
|
|
9874
|
+
if (declaredSize > MAX_BYTES) {
|
|
9875
|
+
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
9876
|
+
}
|
|
9877
|
+
let bytes;
|
|
9878
|
+
const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
|
|
9879
|
+
if (!fetched.ok) {
|
|
9880
|
+
return { ...base, shortUrl: null, error: fetched.error };
|
|
9881
|
+
}
|
|
9882
|
+
bytes = fetched.bytes;
|
|
9883
|
+
const byteError = validateDownloadedBytes(bytes, contentType);
|
|
9884
|
+
if (byteError) {
|
|
9885
|
+
console.warn(
|
|
9886
|
+
`[slack-files] refusing to upload ${fileName}: Slack metadata says ${contentType}, but downloaded bytes are not a supported image (${bytes.slice(0, 32).toString("utf8").replace(/\s+/g, " ").slice(0, 32)})`
|
|
9887
|
+
);
|
|
9888
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
|
|
9889
|
+
}
|
|
9890
|
+
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
9891
|
+
let upload;
|
|
9892
|
+
try {
|
|
9893
|
+
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
9894
|
+
} catch (err) {
|
|
9895
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
9896
|
+
}
|
|
9897
|
+
try {
|
|
9898
|
+
const putRes = await fetch(upload.uploadUrl, {
|
|
9899
|
+
method: "PUT",
|
|
9900
|
+
headers: { "Content-Type": contentType },
|
|
9901
|
+
body: bytes
|
|
9902
|
+
});
|
|
9903
|
+
if (!putRes.ok) {
|
|
9904
|
+
return {
|
|
9905
|
+
...base,
|
|
9906
|
+
sizeBytes: bytes.length,
|
|
9907
|
+
shortUrl: null,
|
|
9908
|
+
error: `gcs_put_${putRes.status}`
|
|
9909
|
+
};
|
|
9910
|
+
}
|
|
9911
|
+
} catch (err) {
|
|
9912
|
+
return {
|
|
9913
|
+
...base,
|
|
9914
|
+
sizeBytes: bytes.length,
|
|
9915
|
+
shortUrl: null,
|
|
9916
|
+
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
9917
|
+
};
|
|
9918
|
+
}
|
|
9919
|
+
try {
|
|
9920
|
+
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
9921
|
+
} catch (err) {
|
|
9922
|
+
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
9923
|
+
}
|
|
9924
|
+
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
9925
|
+
// server somehow forgot to return it (older remote-server versions).
|
|
9926
|
+
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
9927
|
+
return {
|
|
9928
|
+
...base,
|
|
9929
|
+
sizeBytes: bytes.length,
|
|
9930
|
+
shortUrl
|
|
9931
|
+
};
|
|
9932
|
+
}
|
|
9933
|
+
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
9934
|
+
try {
|
|
9935
|
+
const u = new URL(uploadUrl);
|
|
9936
|
+
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
9937
|
+
return `${u.origin}/f/${fileId}`;
|
|
9938
|
+
} catch {
|
|
9939
|
+
return null;
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9942
|
+
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
9943
|
+
if (!Array.isArray(files) || files.length === 0) return [];
|
|
9944
|
+
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
9945
|
+
if (!isRemoteConfigured2()) {
|
|
9946
|
+
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
9947
|
+
return files.map((f) => ({
|
|
9948
|
+
slackFileId: f.id,
|
|
9949
|
+
fileName: inferFileName(f),
|
|
9950
|
+
contentType: inferContentType(f),
|
|
9951
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9952
|
+
shortUrl: null,
|
|
9953
|
+
error: "storage_unconfigured"
|
|
9954
|
+
}));
|
|
9955
|
+
}
|
|
9956
|
+
const botToken = getSlackBotToken();
|
|
9957
|
+
if (!botToken) {
|
|
9958
|
+
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
9959
|
+
return files.map((f) => ({
|
|
9960
|
+
slackFileId: f.id,
|
|
9961
|
+
fileName: inferFileName(f),
|
|
9962
|
+
contentType: inferContentType(f),
|
|
9963
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9964
|
+
shortUrl: null,
|
|
9965
|
+
error: "no_bot_token"
|
|
9966
|
+
}));
|
|
9967
|
+
}
|
|
9968
|
+
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
9969
|
+
const startedAt = Date.now();
|
|
9970
|
+
const pipeline = Promise.allSettled(
|
|
9971
|
+
files.map(
|
|
9972
|
+
(f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
|
|
9973
|
+
if (String(err?.message || err).includes("_timeout")) {
|
|
9974
|
+
return {
|
|
9975
|
+
slackFileId: f.id,
|
|
9976
|
+
fileName: inferFileName(f),
|
|
9977
|
+
contentType: inferContentType(f),
|
|
9978
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9979
|
+
shortUrl: null,
|
|
9980
|
+
error: "timeout"
|
|
9981
|
+
};
|
|
9982
|
+
}
|
|
9983
|
+
throw err;
|
|
9984
|
+
})
|
|
9985
|
+
)
|
|
9986
|
+
);
|
|
9987
|
+
const settled = await pipeline;
|
|
9988
|
+
const results = settled.map((s, i) => {
|
|
9989
|
+
if (s.status === "fulfilled") return s.value;
|
|
9990
|
+
const f = files[i];
|
|
9991
|
+
return {
|
|
9992
|
+
slackFileId: f.id,
|
|
9993
|
+
fileName: inferFileName(f),
|
|
9994
|
+
contentType: inferContentType(f),
|
|
9995
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9996
|
+
shortUrl: null,
|
|
9997
|
+
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
9998
|
+
};
|
|
9999
|
+
});
|
|
10000
|
+
const okCount = results.filter((r) => r.shortUrl).length;
|
|
10001
|
+
console.log(
|
|
10002
|
+
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
10003
|
+
);
|
|
10004
|
+
return results;
|
|
10005
|
+
}
|
|
10006
|
+
function formatFileBlock(files) {
|
|
10007
|
+
if (!files || files.length === 0) return "";
|
|
10008
|
+
const lines = ["[files]"];
|
|
10009
|
+
for (const f of files) {
|
|
10010
|
+
const sizeLabel = formatBytes(f.sizeBytes);
|
|
10011
|
+
if (f.shortUrl) {
|
|
10012
|
+
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
10013
|
+
} else {
|
|
10014
|
+
lines.push(
|
|
10015
|
+
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
10016
|
+
);
|
|
10017
|
+
}
|
|
10018
|
+
}
|
|
10019
|
+
return lines.join("\n");
|
|
10020
|
+
}
|
|
10021
|
+
var MAX_BYTES, INGEST_TIMEOUT_MS;
|
|
10022
|
+
var init_files = __esm({
|
|
10023
|
+
"src/integrations/slack/files.ts"() {
|
|
10024
|
+
"use strict";
|
|
10025
|
+
init_client3();
|
|
10026
|
+
MAX_BYTES = 100 * 1024 * 1024;
|
|
10027
|
+
INGEST_TIMEOUT_MS = 2500;
|
|
10028
|
+
}
|
|
10029
|
+
});
|
|
10030
|
+
|
|
10031
|
+
// src/integrations/slack/read.ts
|
|
10032
|
+
var read_exports = {};
|
|
10033
|
+
__export(read_exports, {
|
|
10034
|
+
findChannelByName: () => findChannelByName,
|
|
10035
|
+
findUsers: () => findUsers,
|
|
10036
|
+
getChannelHistory: () => getChannelHistory,
|
|
10037
|
+
getPermalink: () => getPermalink,
|
|
10038
|
+
getThreadReplies: () => getThreadReplies,
|
|
10039
|
+
ingestMessageFiles: () => ingestMessageFiles
|
|
10040
|
+
});
|
|
10041
|
+
async function slackGet(method, params) {
|
|
10042
|
+
const token = getSlackBotToken();
|
|
10043
|
+
if (!token) return { ok: false, error: "slack_not_configured" };
|
|
10044
|
+
try {
|
|
10045
|
+
const qs = new URLSearchParams(params).toString();
|
|
10046
|
+
const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
|
|
10047
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
10048
|
+
});
|
|
10049
|
+
const json = await res.json().catch(() => ({}));
|
|
10050
|
+
if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
|
|
10051
|
+
return { ok: true, json };
|
|
10052
|
+
} catch (err) {
|
|
10053
|
+
return { ok: false, error: err?.message || "unknown" };
|
|
10054
|
+
}
|
|
10055
|
+
}
|
|
10056
|
+
function clampLimit(n, def, max) {
|
|
10057
|
+
const v = typeof n === "number" && Number.isFinite(n) ? n : def;
|
|
10058
|
+
return String(Math.min(Math.max(Math.floor(v), 1), max));
|
|
10059
|
+
}
|
|
10060
|
+
function liteMessage(m) {
|
|
10061
|
+
return {
|
|
10062
|
+
ts: String(m?.ts ?? ""),
|
|
10063
|
+
threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
|
|
10064
|
+
user: typeof m?.user === "string" ? m.user : void 0,
|
|
10065
|
+
botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
|
|
10066
|
+
text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
|
|
10067
|
+
files: Array.isArray(m?.files) ? m.files.map((f) => ({ id: String(f?.id ?? ""), name: f?.name || f?.title, mimetype: f?.mimetype, size: typeof f?.size === "number" ? f.size : void 0 })) : void 0,
|
|
10068
|
+
replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
|
|
10069
|
+
};
|
|
10070
|
+
}
|
|
10071
|
+
async function enrichUserNames(messages) {
|
|
10072
|
+
const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
|
|
10073
|
+
const names = /* @__PURE__ */ new Map();
|
|
10074
|
+
await Promise.all(
|
|
10075
|
+
ids.map(async (id) => {
|
|
10076
|
+
const info = await resolveSlackUserInfo(id).catch(() => null);
|
|
10077
|
+
if (info?.name) names.set(id, info.name);
|
|
10078
|
+
})
|
|
10079
|
+
);
|
|
10080
|
+
return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
|
|
10081
|
+
}
|
|
10082
|
+
async function getChannelHistory(channel, limit) {
|
|
10083
|
+
const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
|
|
10084
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10085
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10086
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10087
|
+
}
|
|
10088
|
+
async function getThreadReplies(channel, threadTs, limit) {
|
|
10089
|
+
const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
|
|
10090
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10091
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10092
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10093
|
+
}
|
|
10094
|
+
async function getPermalink(channel, messageTs) {
|
|
10095
|
+
const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
|
|
10096
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10097
|
+
return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
|
|
10098
|
+
}
|
|
10099
|
+
async function findChannelByName(name) {
|
|
10100
|
+
const clean = name.replace(/^#/, "").trim().toLowerCase();
|
|
10101
|
+
let cursor;
|
|
10102
|
+
for (let page = 0; page < 10; page++) {
|
|
10103
|
+
const params = {
|
|
10104
|
+
types: "public_channel,private_channel",
|
|
10105
|
+
exclude_archived: "true",
|
|
10106
|
+
limit: "999"
|
|
10107
|
+
};
|
|
10108
|
+
if (cursor) params.cursor = cursor;
|
|
10109
|
+
const r = await slackGet("conversations.list", params);
|
|
10110
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10111
|
+
const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
|
|
10112
|
+
if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
|
|
10113
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10114
|
+
if (!cursor) break;
|
|
10115
|
+
}
|
|
10116
|
+
return { ok: false, error: "channel_not_found" };
|
|
10117
|
+
}
|
|
10118
|
+
async function findUsers(query, max = 10) {
|
|
10119
|
+
const q = query.trim().toLowerCase();
|
|
10120
|
+
if (!q) return { ok: false, error: "empty_query" };
|
|
10121
|
+
if (q.includes("@")) {
|
|
10122
|
+
const r = await slackGet("users.lookupByEmail", { email: query.trim() });
|
|
10123
|
+
if (r.ok && r.json.user) {
|
|
10124
|
+
const u = r.json.user;
|
|
10125
|
+
return { ok: true, data: [{ id: String(u.id), name: u.profile?.display_name || u.real_name || u.name, realName: u.real_name, email: u.profile?.email }] };
|
|
10126
|
+
}
|
|
10127
|
+
if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
|
|
10128
|
+
return { ok: false, error: r.error };
|
|
10129
|
+
}
|
|
10130
|
+
}
|
|
10131
|
+
const matches = [];
|
|
10132
|
+
let cursor;
|
|
10133
|
+
for (let page = 0; page < 10 && matches.length < max; page++) {
|
|
10134
|
+
const params = { limit: "1000" };
|
|
10135
|
+
if (cursor) params.cursor = cursor;
|
|
10136
|
+
const r = await slackGet("users.list", params);
|
|
10137
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10138
|
+
for (const u of r.json.members || []) {
|
|
10139
|
+
if (u?.deleted || u?.is_bot) continue;
|
|
10140
|
+
const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
|
|
10141
|
+
const email = String(u?.profile?.email || "");
|
|
10142
|
+
if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
|
|
10143
|
+
matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
|
|
10144
|
+
if (matches.length >= max) break;
|
|
10145
|
+
}
|
|
10146
|
+
}
|
|
10147
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10148
|
+
if (!cursor) break;
|
|
10149
|
+
}
|
|
10150
|
+
return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
|
|
10151
|
+
}
|
|
10152
|
+
async function getSingleMessage(channel, ts, threadTs) {
|
|
10153
|
+
if (threadTs) {
|
|
10154
|
+
const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
|
|
10155
|
+
if (rr2.ok && Array.isArray(rr2.json.messages)) {
|
|
10156
|
+
const match = rr2.json.messages.find((m) => m?.ts === ts);
|
|
10157
|
+
if (match) return match;
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
|
|
10161
|
+
if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
|
|
10162
|
+
const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
|
|
10163
|
+
if (rr.ok && Array.isArray(rr.json.messages)) {
|
|
10164
|
+
return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
|
|
10165
|
+
}
|
|
10166
|
+
return null;
|
|
10167
|
+
}
|
|
10168
|
+
async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
|
|
10169
|
+
const msg = await getSingleMessage(channel, ts, threadTs);
|
|
10170
|
+
if (!msg) return { ok: false, error: "message_not_found" };
|
|
10171
|
+
const files = Array.isArray(msg.files) ? msg.files : [];
|
|
10172
|
+
if (files.length === 0) return { ok: true, data: [] };
|
|
10173
|
+
const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
|
|
10174
|
+
const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
|
|
10175
|
+
return {
|
|
10176
|
+
ok: true,
|
|
10177
|
+
data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
|
|
10178
|
+
};
|
|
10179
|
+
}
|
|
10180
|
+
var init_read = __esm({
|
|
10181
|
+
"src/integrations/slack/read.ts"() {
|
|
10182
|
+
"use strict";
|
|
10183
|
+
init_client3();
|
|
10184
|
+
}
|
|
10185
|
+
});
|
|
10186
|
+
|
|
9753
10187
|
// src/integrations/channels/messenger.ts
|
|
9754
10188
|
async function postMessage(opts) {
|
|
9755
10189
|
const adapter = getChannel(opts.channel);
|
|
@@ -9760,7 +10194,16 @@ async function postMessage(opts) {
|
|
|
9760
10194
|
let ref;
|
|
9761
10195
|
switch (opts.channel) {
|
|
9762
10196
|
case "slack": {
|
|
9763
|
-
|
|
10197
|
+
let slackChannel2 = opts.to.trim();
|
|
10198
|
+
if (slackChannel2.startsWith("#")) {
|
|
10199
|
+
const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
|
|
10200
|
+
const found = await findChannelByName2(slackChannel2);
|
|
10201
|
+
if (!found.ok || !found.data?.id) {
|
|
10202
|
+
return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
|
|
10203
|
+
}
|
|
10204
|
+
slackChannel2 = found.data.id;
|
|
10205
|
+
}
|
|
10206
|
+
const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
|
|
9764
10207
|
ref = slackRef;
|
|
9765
10208
|
break;
|
|
9766
10209
|
}
|
|
@@ -10067,6 +10510,46 @@ function buildMessengerTool() {
|
|
|
10067
10510
|
}
|
|
10068
10511
|
});
|
|
10069
10512
|
}
|
|
10513
|
+
function buildSlackTool(opts) {
|
|
10514
|
+
return tool13({
|
|
10515
|
+
description: "Read from Slack like a human would. Actions: history (channel[, limit] \u2192 recent messages, newest first, with sender names + file metadata), replies (channel, threadTs[, limit] \u2192 full thread), permalink (channel, messageTs \u2192 shareable link), find_channel (query=name \u2192 channel id), find_user (query=name|email \u2192 matching members), user_info (user=U0123 \u2192 name/email), fetch_files (channel, messageTs[, threadTs] \u2192 re-uploads that message's attachments and returns fetchable URLs you can curl). Use this to scroll back, read context, and open files others posted before replying.",
|
|
10516
|
+
inputSchema: slackInputSchema,
|
|
10517
|
+
execute: async (input) => {
|
|
10518
|
+
if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
|
|
10519
|
+
switch (input.action) {
|
|
10520
|
+
case "history": {
|
|
10521
|
+
if (!input.channel) return { ok: false, error: "channel required" };
|
|
10522
|
+
return getChannelHistory(input.channel, input.limit);
|
|
10523
|
+
}
|
|
10524
|
+
case "replies": {
|
|
10525
|
+
if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
|
|
10526
|
+
return getThreadReplies(input.channel, input.threadTs, input.limit);
|
|
10527
|
+
}
|
|
10528
|
+
case "permalink": {
|
|
10529
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
10530
|
+
return getPermalink(input.channel, input.messageTs);
|
|
10531
|
+
}
|
|
10532
|
+
case "find_channel": {
|
|
10533
|
+
if (!input.query) return { ok: false, error: "query (channel name) required" };
|
|
10534
|
+
return findChannelByName(input.query);
|
|
10535
|
+
}
|
|
10536
|
+
case "find_user": {
|
|
10537
|
+
if (!input.query) return { ok: false, error: "query (name or email) required" };
|
|
10538
|
+
return findUsers(input.query);
|
|
10539
|
+
}
|
|
10540
|
+
case "user_info": {
|
|
10541
|
+
if (!input.user) return { ok: false, error: "user required" };
|
|
10542
|
+
const info = await resolveSlackUserInfo(input.user);
|
|
10543
|
+
return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
|
|
10544
|
+
}
|
|
10545
|
+
case "fetch_files": {
|
|
10546
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
10547
|
+
return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
|
|
10548
|
+
}
|
|
10549
|
+
}
|
|
10550
|
+
}
|
|
10551
|
+
});
|
|
10552
|
+
}
|
|
10070
10553
|
function buildScheduleTool(opts) {
|
|
10071
10554
|
return tool13({
|
|
10072
10555
|
description: "Recurring prompts. Actions: create (required: name, cron, prompt), list, update (required: id; any of name/cron/prompt/enabled/replyChannel), delete (required: id), pause (required: id), resume (required: id). Cron is standard 5-field syntax.",
|
|
@@ -10148,15 +10631,18 @@ function createOrchestratorActionTools(opts) {
|
|
|
10148
10631
|
return {
|
|
10149
10632
|
agent: buildAgentTool(opts),
|
|
10150
10633
|
messenger: buildMessengerTool(),
|
|
10634
|
+
slack: buildSlackTool(opts),
|
|
10151
10635
|
schedule: buildScheduleTool(opts),
|
|
10152
10636
|
webhook: buildWebhookTool(opts)
|
|
10153
10637
|
};
|
|
10154
10638
|
}
|
|
10155
|
-
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10639
|
+
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, slackInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10156
10640
|
var init_orchestrator_actions = __esm({
|
|
10157
10641
|
"src/tools/orchestrator-actions.ts"() {
|
|
10158
10642
|
"use strict";
|
|
10159
10643
|
init_messenger();
|
|
10644
|
+
init_read();
|
|
10645
|
+
init_client3();
|
|
10160
10646
|
init_schedules_store();
|
|
10161
10647
|
init_config();
|
|
10162
10648
|
init_webhooks_store();
|
|
@@ -10214,6 +10700,15 @@ var init_orchestrator_actions = __esm({
|
|
|
10214
10700
|
threadTs: z14.string().optional().describe("post + slack: reply in this thread."),
|
|
10215
10701
|
subject: z14.string().optional().describe("post + email: subject (future).")
|
|
10216
10702
|
});
|
|
10703
|
+
slackInputSchema = z14.object({
|
|
10704
|
+
action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
|
|
10705
|
+
channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
|
|
10706
|
+
threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
|
|
10707
|
+
messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
|
|
10708
|
+
query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
|
|
10709
|
+
user: z14.string().optional().describe("user_info: user id (U0123)."),
|
|
10710
|
+
limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
|
|
10711
|
+
});
|
|
10217
10712
|
scheduleInputSchema = z14.object({
|
|
10218
10713
|
action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
|
|
10219
10714
|
// create / update
|
|
@@ -15854,226 +16349,69 @@ function verifySlackSignature(opts) {
|
|
|
15854
16349
|
// src/server/routes/slack.ts
|
|
15855
16350
|
init_client3();
|
|
15856
16351
|
init_slack();
|
|
15857
|
-
|
|
15858
|
-
// src/integrations/slack/files.ts
|
|
15859
|
-
init_client3();
|
|
15860
|
-
var MAX_BYTES = 100 * 1024 * 1024;
|
|
15861
|
-
var INGEST_TIMEOUT_MS = 2500;
|
|
15862
|
-
function inferFileName(file) {
|
|
15863
|
-
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
15864
|
-
}
|
|
15865
|
-
function inferContentType(file) {
|
|
15866
|
-
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
15867
|
-
return "application/octet-stream";
|
|
15868
|
-
}
|
|
15869
|
-
function formatBytes(n) {
|
|
15870
|
-
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
15871
|
-
if (n < 1024) return `${n} B`;
|
|
15872
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
15873
|
-
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
15874
|
-
}
|
|
15875
|
-
function withTimeout(p, ms, label) {
|
|
15876
|
-
return new Promise((resolve13, reject) => {
|
|
15877
|
-
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
15878
|
-
p.then(
|
|
15879
|
-
(v) => {
|
|
15880
|
-
clearTimeout(t);
|
|
15881
|
-
resolve13(v);
|
|
15882
|
-
},
|
|
15883
|
-
(e) => {
|
|
15884
|
-
clearTimeout(t);
|
|
15885
|
-
reject(e);
|
|
15886
|
-
}
|
|
15887
|
-
);
|
|
15888
|
-
});
|
|
15889
|
-
}
|
|
15890
|
-
async function ingestOne(file, sessionId, botToken) {
|
|
15891
|
-
const fileName = inferFileName(file);
|
|
15892
|
-
const contentType = inferContentType(file);
|
|
15893
|
-
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
15894
|
-
const base = {
|
|
15895
|
-
slackFileId: file.id,
|
|
15896
|
-
fileName,
|
|
15897
|
-
contentType,
|
|
15898
|
-
sizeBytes: declaredSize
|
|
15899
|
-
};
|
|
15900
|
-
const sourceUrl = file.url_private_download || file.url_private;
|
|
15901
|
-
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
15902
|
-
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
15903
|
-
}
|
|
15904
|
-
if (declaredSize > MAX_BYTES) {
|
|
15905
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15906
|
-
}
|
|
15907
|
-
let bytes;
|
|
15908
|
-
try {
|
|
15909
|
-
const res = await fetch(sourceUrl, {
|
|
15910
|
-
headers: { Authorization: `Bearer ${botToken}` }
|
|
15911
|
-
});
|
|
15912
|
-
if (!res.ok) {
|
|
15913
|
-
return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
|
|
15914
|
-
}
|
|
15915
|
-
const ab = await res.arrayBuffer();
|
|
15916
|
-
if (ab.byteLength > MAX_BYTES) {
|
|
15917
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15918
|
-
}
|
|
15919
|
-
bytes = Buffer.from(ab);
|
|
15920
|
-
} catch (err) {
|
|
15921
|
-
return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
|
|
15922
|
-
}
|
|
15923
|
-
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15924
|
-
let upload;
|
|
15925
|
-
try {
|
|
15926
|
-
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
15927
|
-
} catch (err) {
|
|
15928
|
-
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
15929
|
-
}
|
|
15930
|
-
try {
|
|
15931
|
-
const putRes = await fetch(upload.uploadUrl, {
|
|
15932
|
-
method: "PUT",
|
|
15933
|
-
headers: { "Content-Type": contentType },
|
|
15934
|
-
body: bytes
|
|
15935
|
-
});
|
|
15936
|
-
if (!putRes.ok) {
|
|
15937
|
-
return {
|
|
15938
|
-
...base,
|
|
15939
|
-
sizeBytes: bytes.length,
|
|
15940
|
-
shortUrl: null,
|
|
15941
|
-
error: `gcs_put_${putRes.status}`
|
|
15942
|
-
};
|
|
15943
|
-
}
|
|
15944
|
-
} catch (err) {
|
|
15945
|
-
return {
|
|
15946
|
-
...base,
|
|
15947
|
-
sizeBytes: bytes.length,
|
|
15948
|
-
shortUrl: null,
|
|
15949
|
-
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
15950
|
-
};
|
|
15951
|
-
}
|
|
15952
|
-
try {
|
|
15953
|
-
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
15954
|
-
} catch (err) {
|
|
15955
|
-
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
15956
|
-
}
|
|
15957
|
-
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
15958
|
-
// server somehow forgot to return it (older remote-server versions).
|
|
15959
|
-
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
15960
|
-
return {
|
|
15961
|
-
...base,
|
|
15962
|
-
sizeBytes: bytes.length,
|
|
15963
|
-
shortUrl
|
|
15964
|
-
};
|
|
15965
|
-
}
|
|
15966
|
-
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
15967
|
-
try {
|
|
15968
|
-
const u = new URL(uploadUrl);
|
|
15969
|
-
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
15970
|
-
return `${u.origin}/f/${fileId}`;
|
|
15971
|
-
} catch {
|
|
15972
|
-
return null;
|
|
15973
|
-
}
|
|
15974
|
-
}
|
|
15975
|
-
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
15976
|
-
if (!Array.isArray(files) || files.length === 0) return [];
|
|
15977
|
-
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15978
|
-
if (!isRemoteConfigured2()) {
|
|
15979
|
-
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
15980
|
-
return files.map((f) => ({
|
|
15981
|
-
slackFileId: f.id,
|
|
15982
|
-
fileName: inferFileName(f),
|
|
15983
|
-
contentType: inferContentType(f),
|
|
15984
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15985
|
-
shortUrl: null,
|
|
15986
|
-
error: "storage_unconfigured"
|
|
15987
|
-
}));
|
|
15988
|
-
}
|
|
15989
|
-
const botToken = getSlackBotToken();
|
|
15990
|
-
if (!botToken) {
|
|
15991
|
-
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
15992
|
-
return files.map((f) => ({
|
|
15993
|
-
slackFileId: f.id,
|
|
15994
|
-
fileName: inferFileName(f),
|
|
15995
|
-
contentType: inferContentType(f),
|
|
15996
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15997
|
-
shortUrl: null,
|
|
15998
|
-
error: "no_bot_token"
|
|
15999
|
-
}));
|
|
16000
|
-
}
|
|
16001
|
-
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
16002
|
-
const startedAt = Date.now();
|
|
16003
|
-
const pipeline = Promise.allSettled(
|
|
16004
|
-
files.map((f) => ingestOne(f, sessionId, botToken))
|
|
16005
|
-
);
|
|
16006
|
-
let settled;
|
|
16007
|
-
try {
|
|
16008
|
-
settled = await withTimeout(pipeline, timeoutMs, "ingest");
|
|
16009
|
-
} catch (err) {
|
|
16010
|
-
console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
|
|
16011
|
-
return files.map((f) => ({
|
|
16012
|
-
slackFileId: f.id,
|
|
16013
|
-
fileName: inferFileName(f),
|
|
16014
|
-
contentType: inferContentType(f),
|
|
16015
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16016
|
-
shortUrl: null,
|
|
16017
|
-
error: "timeout"
|
|
16018
|
-
}));
|
|
16019
|
-
}
|
|
16020
|
-
const results = settled.map((s, i) => {
|
|
16021
|
-
if (s.status === "fulfilled") return s.value;
|
|
16022
|
-
const f = files[i];
|
|
16023
|
-
return {
|
|
16024
|
-
slackFileId: f.id,
|
|
16025
|
-
fileName: inferFileName(f),
|
|
16026
|
-
contentType: inferContentType(f),
|
|
16027
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16028
|
-
shortUrl: null,
|
|
16029
|
-
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
16030
|
-
};
|
|
16031
|
-
});
|
|
16032
|
-
const okCount = results.filter((r) => r.shortUrl).length;
|
|
16033
|
-
console.log(
|
|
16034
|
-
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
16035
|
-
);
|
|
16036
|
-
return results;
|
|
16037
|
-
}
|
|
16038
|
-
function formatFileBlock(files) {
|
|
16039
|
-
if (!files || files.length === 0) return "";
|
|
16040
|
-
const lines = ["[files]"];
|
|
16041
|
-
for (const f of files) {
|
|
16042
|
-
const sizeLabel = formatBytes(f.sizeBytes);
|
|
16043
|
-
if (f.shortUrl) {
|
|
16044
|
-
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
16045
|
-
} else {
|
|
16046
|
-
lines.push(
|
|
16047
|
-
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
16048
|
-
);
|
|
16049
|
-
}
|
|
16050
|
-
}
|
|
16051
|
-
return lines.join("\n");
|
|
16052
|
-
}
|
|
16053
|
-
|
|
16054
|
-
// src/server/routes/slack.ts
|
|
16352
|
+
init_files();
|
|
16055
16353
|
init_webhook_events();
|
|
16056
16354
|
init_inbox();
|
|
16057
16355
|
var recentlyHandled = /* @__PURE__ */ new Map();
|
|
16356
|
+
var recentlyDeniedReplies = /* @__PURE__ */ new Map();
|
|
16357
|
+
var inFlight = /* @__PURE__ */ new Set();
|
|
16058
16358
|
var MAX_RECENT = 1e3;
|
|
16359
|
+
function deliveryKey(channel, ts) {
|
|
16360
|
+
if (!channel || !ts) return null;
|
|
16361
|
+
return `${channel}\u241F${ts}`;
|
|
16362
|
+
}
|
|
16059
16363
|
function wasHandled(channel, ts) {
|
|
16060
|
-
|
|
16061
|
-
|
|
16364
|
+
const key2 = deliveryKey(channel, ts);
|
|
16365
|
+
if (!key2) return false;
|
|
16366
|
+
return recentlyHandled.has(key2) || inFlight.has(key2);
|
|
16367
|
+
}
|
|
16368
|
+
function markInFlight(channel, ts) {
|
|
16369
|
+
const key2 = deliveryKey(channel, ts);
|
|
16370
|
+
if (key2) inFlight.add(key2);
|
|
16371
|
+
}
|
|
16372
|
+
function clearInFlight(channel, ts) {
|
|
16373
|
+
const key2 = deliveryKey(channel, ts);
|
|
16374
|
+
if (key2) inFlight.delete(key2);
|
|
16062
16375
|
}
|
|
16063
16376
|
function markHandled(channel, ts) {
|
|
16064
|
-
|
|
16065
|
-
|
|
16377
|
+
const key2 = deliveryKey(channel, ts);
|
|
16378
|
+
if (!key2) return;
|
|
16379
|
+
inFlight.delete(key2);
|
|
16066
16380
|
recentlyHandled.set(key2, Date.now());
|
|
16067
16381
|
if (recentlyHandled.size > MAX_RECENT) {
|
|
16068
16382
|
const oldest = recentlyHandled.keys().next().value;
|
|
16069
16383
|
if (oldest) recentlyHandled.delete(oldest);
|
|
16070
16384
|
}
|
|
16071
16385
|
}
|
|
16386
|
+
function wasDeniedReplySent(channel, ts) {
|
|
16387
|
+
const key2 = deliveryKey(channel, ts);
|
|
16388
|
+
if (!key2) return false;
|
|
16389
|
+
return recentlyDeniedReplies.has(key2);
|
|
16390
|
+
}
|
|
16391
|
+
function markDeniedReplySent(channel, ts) {
|
|
16392
|
+
const key2 = deliveryKey(channel, ts);
|
|
16393
|
+
if (!key2) return;
|
|
16394
|
+
recentlyDeniedReplies.set(key2, Date.now());
|
|
16395
|
+
if (recentlyDeniedReplies.size > MAX_RECENT) {
|
|
16396
|
+
const oldest = recentlyDeniedReplies.keys().next().value;
|
|
16397
|
+
if (oldest) recentlyDeniedReplies.delete(oldest);
|
|
16398
|
+
}
|
|
16399
|
+
}
|
|
16072
16400
|
var slack = new Hono6();
|
|
16073
16401
|
slack.post("/events", async (c) => {
|
|
16074
|
-
const signingSecret = getSlackSigningSecret();
|
|
16075
|
-
if (!signingSecret) return c.json({ error: "slack not configured" }, 503);
|
|
16076
16402
|
const rawBody = await c.req.text();
|
|
16403
|
+
const signingSecret = getSlackSigningSecret();
|
|
16404
|
+
if (!signingSecret) {
|
|
16405
|
+
let unsignedPayload;
|
|
16406
|
+
try {
|
|
16407
|
+
unsignedPayload = JSON.parse(rawBody);
|
|
16408
|
+
} catch {
|
|
16409
|
+
}
|
|
16410
|
+
if (unsignedPayload?.type === "url_verification" && typeof unsignedPayload.challenge === "string") {
|
|
16411
|
+
return c.json({ challenge: unsignedPayload.challenge });
|
|
16412
|
+
}
|
|
16413
|
+
return c.json({ error: "slack not configured" }, 503);
|
|
16414
|
+
}
|
|
16077
16415
|
const verification = verifySlackSignature({
|
|
16078
16416
|
signingSecret,
|
|
16079
16417
|
timestampHeader: c.req.header("x-slack-request-timestamp") || null,
|
|
@@ -16127,65 +16465,79 @@ slack.post("/events", async (c) => {
|
|
|
16127
16465
|
if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
|
|
16128
16466
|
markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
|
|
16129
16467
|
}
|
|
16130
|
-
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
16468
|
+
if (wasHandled(ev.channel, ev.ts)) {
|
|
16469
|
+
updateEvent(auditId, { status: "dropped", dropReason: "duplicate_delivery" });
|
|
16470
|
+
return c.json({ ok: true });
|
|
16471
|
+
}
|
|
16472
|
+
markInFlight(ev.channel, ev.ts);
|
|
16473
|
+
let routed = false;
|
|
16474
|
+
try {
|
|
16475
|
+
const orchestratorId = await findOrCreateOrchestratorId();
|
|
16476
|
+
if (orchestratorId) {
|
|
16477
|
+
inbound.content = await normalizeSlackMentions(inbound.content);
|
|
16478
|
+
if (ev.user) {
|
|
16479
|
+
const info = await resolveSlackUserInfo(ev.user);
|
|
16480
|
+
if (info?.name) {
|
|
16481
|
+
const emailSuffix = info.email ? ` (${info.email})` : "";
|
|
16482
|
+
const enriched = `user=${info.name} <@${ev.user}>${emailSuffix}`;
|
|
16483
|
+
inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
|
|
16484
|
+
}
|
|
16139
16485
|
}
|
|
16140
|
-
|
|
16141
|
-
|
|
16142
|
-
|
|
16143
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
|
|
16150
|
-
const block = formatFileBlock(ingested);
|
|
16151
|
-
if (block) inbound.content = `${inbound.content}
|
|
16486
|
+
const slackFiles = Array.isArray(ev.files) ? ev.files : [];
|
|
16487
|
+
if (ev.channel && ev.ts) {
|
|
16488
|
+
void addLoadingReaction(String(ev.channel), String(ev.ts));
|
|
16489
|
+
}
|
|
16490
|
+
let ingestedCount = 0;
|
|
16491
|
+
if (slackFiles.length > 0) {
|
|
16492
|
+
try {
|
|
16493
|
+
const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
|
|
16494
|
+
const block = formatFileBlock(ingested);
|
|
16495
|
+
if (block) inbound.content = `${inbound.content}
|
|
16152
16496
|
${block}`;
|
|
16153
|
-
|
|
16154
|
-
|
|
16155
|
-
|
|
16156
|
-
|
|
16497
|
+
ingestedCount = ingested.filter((f) => f.shortUrl).length;
|
|
16498
|
+
} catch (err) {
|
|
16499
|
+
console.warn("[slack-files] ingestion threw:", err?.message || err);
|
|
16500
|
+
inbound.content = `${inbound.content}
|
|
16157
16501
|
[files] (ingestion failed: ${err?.message || "unknown"})`;
|
|
16502
|
+
}
|
|
16158
16503
|
}
|
|
16504
|
+
pushToInbox(orchestratorId, inbound);
|
|
16505
|
+
routed = true;
|
|
16506
|
+
markHandled(ev.channel, ev.ts);
|
|
16507
|
+
updateEvent(auditId, {
|
|
16508
|
+
status: "routed",
|
|
16509
|
+
sessionId: orchestratorId,
|
|
16510
|
+
...slackFiles.length > 0 ? {
|
|
16511
|
+
// Preserve the original meta (ts, thread_ts, team,
|
|
16512
|
+
// event_subtype) from recordEvent above — updateEvent does a
|
|
16513
|
+
// shallow merge, so we have to re-include them.
|
|
16514
|
+
meta: {
|
|
16515
|
+
ts: ev.ts,
|
|
16516
|
+
thread_ts: ev.thread_ts,
|
|
16517
|
+
team: ev.team,
|
|
16518
|
+
event_subtype: ev.subtype,
|
|
16519
|
+
fileCount: slackFiles.length,
|
|
16520
|
+
ingestedCount
|
|
16521
|
+
}
|
|
16522
|
+
} : {}
|
|
16523
|
+
});
|
|
16524
|
+
} else {
|
|
16525
|
+
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
16159
16526
|
}
|
|
16160
|
-
|
|
16161
|
-
|
|
16162
|
-
status: "routed",
|
|
16163
|
-
sessionId: orchestratorId,
|
|
16164
|
-
...slackFiles.length > 0 ? {
|
|
16165
|
-
// Preserve the original meta (ts, thread_ts, team,
|
|
16166
|
-
// event_subtype) from recordEvent above — updateEvent does a
|
|
16167
|
-
// shallow merge, so we have to re-include them.
|
|
16168
|
-
meta: {
|
|
16169
|
-
ts: ev.ts,
|
|
16170
|
-
thread_ts: ev.thread_ts,
|
|
16171
|
-
team: ev.team,
|
|
16172
|
-
event_subtype: ev.subtype,
|
|
16173
|
-
fileCount: slackFiles.length,
|
|
16174
|
-
ingestedCount
|
|
16175
|
-
}
|
|
16176
|
-
} : {}
|
|
16177
|
-
});
|
|
16178
|
-
} else {
|
|
16179
|
-
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
16527
|
+
} finally {
|
|
16528
|
+
if (!routed) clearInFlight(ev.channel, ev.ts);
|
|
16180
16529
|
}
|
|
16181
16530
|
} else if (dropReason) {
|
|
16182
16531
|
updateEvent(auditId, { status: "dropped", dropReason });
|
|
16183
16532
|
const userFacingDrops = ["user_not_allowed", "channel_not_allowed", "dm_blocked"];
|
|
16184
16533
|
if (userFacingDrops.includes(dropReason)) {
|
|
16185
16534
|
console.log(`[slack] dropped event from user=${payload.event.user} channel=${payload.event.channel}: ${dropReason}`);
|
|
16186
|
-
|
|
16187
|
-
|
|
16188
|
-
|
|
16535
|
+
if (!wasDeniedReplySent(payload.event.channel, payload.event.ts)) {
|
|
16536
|
+
markDeniedReplySent(payload.event.channel, payload.event.ts);
|
|
16537
|
+
void sendDeniedReply(payload.event, dropReason).catch((err) => {
|
|
16538
|
+
console.warn(`[slack] denied-reply failed:`, err?.message || err);
|
|
16539
|
+
});
|
|
16540
|
+
}
|
|
16189
16541
|
}
|
|
16190
16542
|
}
|
|
16191
16543
|
}
|
|
@@ -16210,7 +16562,7 @@ async function findOrCreateOrchestratorId() {
|
|
|
16210
16562
|
return null;
|
|
16211
16563
|
}
|
|
16212
16564
|
}
|
|
16213
|
-
async function sendDeniedReply(event) {
|
|
16565
|
+
async function sendDeniedReply(event, reason) {
|
|
16214
16566
|
const policy = getSlackDeniedReplyPolicy();
|
|
16215
16567
|
if (!policy.enabled) return;
|
|
16216
16568
|
const adapter = getSlackAdapter();
|
|
@@ -16219,7 +16571,7 @@ async function sendDeniedReply(event) {
|
|
|
16219
16571
|
const channel = String(event?.channel || "");
|
|
16220
16572
|
if (!channel) return;
|
|
16221
16573
|
const threadTs = event?.thread_ts || event?.ts;
|
|
16222
|
-
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`);
|
|
16574
|
+
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`).replace(/\{reason\}/g, reason);
|
|
16223
16575
|
const result = await adapter.postMessage({ channel, text, threadTs });
|
|
16224
16576
|
if (!result.ok) {
|
|
16225
16577
|
console.warn(`[slack] denied-reply post failed: ${result.error}`);
|