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/server/index.js
CHANGED
|
@@ -1098,6 +1098,7 @@ function loadConfig(configPath, workingDirectory) {
|
|
|
1098
1098
|
}
|
|
1099
1099
|
if (process.env.SPARKECODER_PORT) {
|
|
1100
1100
|
rawConfig.server = {
|
|
1101
|
+
...rawConfig.server ?? {},
|
|
1101
1102
|
port: parseInt(process.env.SPARKECODER_PORT, 10),
|
|
1102
1103
|
host: rawConfig.server?.host ?? "127.0.0.1"
|
|
1103
1104
|
};
|
|
@@ -8580,7 +8581,7 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8580
8581
|
const adapter = getSlackAdapter();
|
|
8581
8582
|
if (!adapter) return { ok: false, error: "slack_not_configured" };
|
|
8582
8583
|
const key2 = reactionKey(channel, timestamp);
|
|
8583
|
-
const
|
|
8584
|
+
const inFlight2 = (async () => {
|
|
8584
8585
|
try {
|
|
8585
8586
|
const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
|
|
8586
8587
|
if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
|
|
@@ -8592,11 +8593,11 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8592
8593
|
return { ok: false, error: err?.message || "unknown" };
|
|
8593
8594
|
}
|
|
8594
8595
|
})();
|
|
8595
|
-
pendingAdds.set(key2,
|
|
8596
|
-
void
|
|
8597
|
-
if (pendingAdds.get(key2) ===
|
|
8596
|
+
pendingAdds.set(key2, inFlight2);
|
|
8597
|
+
void inFlight2.finally(() => {
|
|
8598
|
+
if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
|
|
8598
8599
|
});
|
|
8599
|
-
return
|
|
8600
|
+
return inFlight2;
|
|
8600
8601
|
}
|
|
8601
8602
|
async function removeLoadingReaction(channel, timestamp) {
|
|
8602
8603
|
const adapter = getSlackAdapter();
|
|
@@ -8844,17 +8845,28 @@ async function fetchBotParticipatedInThread(channel, threadTs) {
|
|
|
8844
8845
|
const self = await ensureSlackSelfIdentity();
|
|
8845
8846
|
if (!self) return false;
|
|
8846
8847
|
try {
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8848
|
+
let cursor = "";
|
|
8849
|
+
for (let page = 0; page < 10; page++) {
|
|
8850
|
+
const params = new URLSearchParams({ channel, ts: threadTs, limit: "200" });
|
|
8851
|
+
if (cursor) params.set("cursor", cursor);
|
|
8852
|
+
const res = await fetch(`https://slack.com/api/conversations.replies?${params.toString()}`, {
|
|
8853
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
8854
|
+
});
|
|
8855
|
+
const data = await res.json().catch(() => ({}));
|
|
8856
|
+
if (!data?.ok) {
|
|
8857
|
+
console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
|
|
8858
|
+
return false;
|
|
8859
|
+
}
|
|
8860
|
+
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
8861
|
+
if (messages.some(
|
|
8862
|
+
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
8863
|
+
)) {
|
|
8864
|
+
return true;
|
|
8865
|
+
}
|
|
8866
|
+
cursor = String(data.response_metadata?.next_cursor || "");
|
|
8867
|
+
if (!cursor) break;
|
|
8853
8868
|
}
|
|
8854
|
-
|
|
8855
|
-
return messages.some(
|
|
8856
|
-
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
8857
|
-
);
|
|
8869
|
+
return false;
|
|
8858
8870
|
} catch (err) {
|
|
8859
8871
|
console.warn(`[slack] conversations.replies error:`, err?.message || err);
|
|
8860
8872
|
return false;
|
|
@@ -9306,7 +9318,7 @@ async function reconcileOnce(now = Date.now()) {
|
|
|
9306
9318
|
entry2.updatedAt = Date.now();
|
|
9307
9319
|
const nudged = {
|
|
9308
9320
|
...entry2.event,
|
|
9309
|
-
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,
|
|
9321
|
+
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.]
|
|
9310
9322
|
${entry2.event.content}`,
|
|
9311
9323
|
wake: "now"
|
|
9312
9324
|
};
|
|
@@ -9733,6 +9745,428 @@ var init_registry = __esm({
|
|
|
9733
9745
|
}
|
|
9734
9746
|
});
|
|
9735
9747
|
|
|
9748
|
+
// src/integrations/slack/files.ts
|
|
9749
|
+
var files_exports = {};
|
|
9750
|
+
__export(files_exports, {
|
|
9751
|
+
INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
|
|
9752
|
+
MAX_BYTES: () => MAX_BYTES,
|
|
9753
|
+
formatFileBlock: () => formatFileBlock,
|
|
9754
|
+
ingestSlackFiles: () => ingestSlackFiles
|
|
9755
|
+
});
|
|
9756
|
+
function inferFileName(file) {
|
|
9757
|
+
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
9758
|
+
}
|
|
9759
|
+
function inferContentType(file) {
|
|
9760
|
+
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
9761
|
+
return "application/octet-stream";
|
|
9762
|
+
}
|
|
9763
|
+
function formatBytes(n) {
|
|
9764
|
+
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
9765
|
+
if (n < 1024) return `${n} B`;
|
|
9766
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
9767
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
9768
|
+
}
|
|
9769
|
+
function sleep(ms) {
|
|
9770
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
9771
|
+
}
|
|
9772
|
+
function imageMagic(bytes) {
|
|
9773
|
+
if (bytes.length < 12) return null;
|
|
9774
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
9775
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
|
|
9776
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
|
|
9777
|
+
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";
|
|
9778
|
+
return null;
|
|
9779
|
+
}
|
|
9780
|
+
function requiresRasterMagic(contentType) {
|
|
9781
|
+
const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
|
|
9782
|
+
return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
|
|
9783
|
+
}
|
|
9784
|
+
function validateDownloadedBytes(bytes, declaredContentType) {
|
|
9785
|
+
if (!requiresRasterMagic(declaredContentType)) return null;
|
|
9786
|
+
const actual = imageMagic(bytes);
|
|
9787
|
+
if (!actual) return "invalid_image_bytes";
|
|
9788
|
+
return null;
|
|
9789
|
+
}
|
|
9790
|
+
async function fetchSlackPrivateFile(sourceUrl, botToken) {
|
|
9791
|
+
let lastError = "unknown";
|
|
9792
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
9793
|
+
try {
|
|
9794
|
+
const res = await fetch(sourceUrl, {
|
|
9795
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
9796
|
+
});
|
|
9797
|
+
if (res.status === 429 || res.status >= 500) {
|
|
9798
|
+
lastError = `slack_fetch_${res.status}`;
|
|
9799
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
9800
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
|
|
9801
|
+
if (attempt < 2) {
|
|
9802
|
+
await sleep(waitMs);
|
|
9803
|
+
continue;
|
|
9804
|
+
}
|
|
9805
|
+
}
|
|
9806
|
+
if (!res.ok) {
|
|
9807
|
+
return { ok: false, error: `slack_fetch_${res.status}` };
|
|
9808
|
+
}
|
|
9809
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
9810
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
|
|
9811
|
+
return { ok: false, error: "size_exceeded" };
|
|
9812
|
+
}
|
|
9813
|
+
const ab = await res.arrayBuffer();
|
|
9814
|
+
if (ab.byteLength > MAX_BYTES) {
|
|
9815
|
+
return { ok: false, error: "size_exceeded" };
|
|
9816
|
+
}
|
|
9817
|
+
return { ok: true, bytes: Buffer.from(ab) };
|
|
9818
|
+
} catch (err) {
|
|
9819
|
+
lastError = `slack_fetch_error:${err?.message || "unknown"}`;
|
|
9820
|
+
if (attempt < 2) {
|
|
9821
|
+
await sleep(250 * (attempt + 1));
|
|
9822
|
+
continue;
|
|
9823
|
+
}
|
|
9824
|
+
}
|
|
9825
|
+
}
|
|
9826
|
+
return { ok: false, error: lastError };
|
|
9827
|
+
}
|
|
9828
|
+
function withTimeout(p, ms, label) {
|
|
9829
|
+
return new Promise((resolve13, reject) => {
|
|
9830
|
+
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
9831
|
+
p.then(
|
|
9832
|
+
(v) => {
|
|
9833
|
+
clearTimeout(t);
|
|
9834
|
+
resolve13(v);
|
|
9835
|
+
},
|
|
9836
|
+
(e) => {
|
|
9837
|
+
clearTimeout(t);
|
|
9838
|
+
reject(e);
|
|
9839
|
+
}
|
|
9840
|
+
);
|
|
9841
|
+
});
|
|
9842
|
+
}
|
|
9843
|
+
async function ingestOne(file, sessionId, botToken) {
|
|
9844
|
+
const fileName = inferFileName(file);
|
|
9845
|
+
const contentType = inferContentType(file);
|
|
9846
|
+
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
9847
|
+
const base = {
|
|
9848
|
+
slackFileId: file.id,
|
|
9849
|
+
fileName,
|
|
9850
|
+
contentType,
|
|
9851
|
+
sizeBytes: declaredSize
|
|
9852
|
+
};
|
|
9853
|
+
const sourceUrl = file.url_private_download || file.url_private;
|
|
9854
|
+
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
9855
|
+
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
9856
|
+
}
|
|
9857
|
+
if (declaredSize > MAX_BYTES) {
|
|
9858
|
+
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
9859
|
+
}
|
|
9860
|
+
let bytes;
|
|
9861
|
+
const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
|
|
9862
|
+
if (!fetched.ok) {
|
|
9863
|
+
return { ...base, shortUrl: null, error: fetched.error };
|
|
9864
|
+
}
|
|
9865
|
+
bytes = fetched.bytes;
|
|
9866
|
+
const byteError = validateDownloadedBytes(bytes, contentType);
|
|
9867
|
+
if (byteError) {
|
|
9868
|
+
console.warn(
|
|
9869
|
+
`[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)})`
|
|
9870
|
+
);
|
|
9871
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
|
|
9872
|
+
}
|
|
9873
|
+
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
9874
|
+
let upload;
|
|
9875
|
+
try {
|
|
9876
|
+
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
9877
|
+
} catch (err) {
|
|
9878
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
9879
|
+
}
|
|
9880
|
+
try {
|
|
9881
|
+
const putRes = await fetch(upload.uploadUrl, {
|
|
9882
|
+
method: "PUT",
|
|
9883
|
+
headers: { "Content-Type": contentType },
|
|
9884
|
+
body: bytes
|
|
9885
|
+
});
|
|
9886
|
+
if (!putRes.ok) {
|
|
9887
|
+
return {
|
|
9888
|
+
...base,
|
|
9889
|
+
sizeBytes: bytes.length,
|
|
9890
|
+
shortUrl: null,
|
|
9891
|
+
error: `gcs_put_${putRes.status}`
|
|
9892
|
+
};
|
|
9893
|
+
}
|
|
9894
|
+
} catch (err) {
|
|
9895
|
+
return {
|
|
9896
|
+
...base,
|
|
9897
|
+
sizeBytes: bytes.length,
|
|
9898
|
+
shortUrl: null,
|
|
9899
|
+
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
9900
|
+
};
|
|
9901
|
+
}
|
|
9902
|
+
try {
|
|
9903
|
+
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
9904
|
+
} catch (err) {
|
|
9905
|
+
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
9906
|
+
}
|
|
9907
|
+
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
9908
|
+
// server somehow forgot to return it (older remote-server versions).
|
|
9909
|
+
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
9910
|
+
return {
|
|
9911
|
+
...base,
|
|
9912
|
+
sizeBytes: bytes.length,
|
|
9913
|
+
shortUrl
|
|
9914
|
+
};
|
|
9915
|
+
}
|
|
9916
|
+
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
9917
|
+
try {
|
|
9918
|
+
const u = new URL(uploadUrl);
|
|
9919
|
+
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
9920
|
+
return `${u.origin}/f/${fileId}`;
|
|
9921
|
+
} catch {
|
|
9922
|
+
return null;
|
|
9923
|
+
}
|
|
9924
|
+
}
|
|
9925
|
+
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
9926
|
+
if (!Array.isArray(files) || files.length === 0) return [];
|
|
9927
|
+
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
9928
|
+
if (!isRemoteConfigured2()) {
|
|
9929
|
+
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
9930
|
+
return files.map((f) => ({
|
|
9931
|
+
slackFileId: f.id,
|
|
9932
|
+
fileName: inferFileName(f),
|
|
9933
|
+
contentType: inferContentType(f),
|
|
9934
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9935
|
+
shortUrl: null,
|
|
9936
|
+
error: "storage_unconfigured"
|
|
9937
|
+
}));
|
|
9938
|
+
}
|
|
9939
|
+
const botToken = getSlackBotToken();
|
|
9940
|
+
if (!botToken) {
|
|
9941
|
+
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
9942
|
+
return files.map((f) => ({
|
|
9943
|
+
slackFileId: f.id,
|
|
9944
|
+
fileName: inferFileName(f),
|
|
9945
|
+
contentType: inferContentType(f),
|
|
9946
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9947
|
+
shortUrl: null,
|
|
9948
|
+
error: "no_bot_token"
|
|
9949
|
+
}));
|
|
9950
|
+
}
|
|
9951
|
+
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
9952
|
+
const startedAt = Date.now();
|
|
9953
|
+
const pipeline = Promise.allSettled(
|
|
9954
|
+
files.map(
|
|
9955
|
+
(f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
|
|
9956
|
+
if (String(err?.message || err).includes("_timeout")) {
|
|
9957
|
+
return {
|
|
9958
|
+
slackFileId: f.id,
|
|
9959
|
+
fileName: inferFileName(f),
|
|
9960
|
+
contentType: inferContentType(f),
|
|
9961
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9962
|
+
shortUrl: null,
|
|
9963
|
+
error: "timeout"
|
|
9964
|
+
};
|
|
9965
|
+
}
|
|
9966
|
+
throw err;
|
|
9967
|
+
})
|
|
9968
|
+
)
|
|
9969
|
+
);
|
|
9970
|
+
const settled = await pipeline;
|
|
9971
|
+
const results = settled.map((s, i) => {
|
|
9972
|
+
if (s.status === "fulfilled") return s.value;
|
|
9973
|
+
const f = files[i];
|
|
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: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
9981
|
+
};
|
|
9982
|
+
});
|
|
9983
|
+
const okCount = results.filter((r) => r.shortUrl).length;
|
|
9984
|
+
console.log(
|
|
9985
|
+
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
9986
|
+
);
|
|
9987
|
+
return results;
|
|
9988
|
+
}
|
|
9989
|
+
function formatFileBlock(files) {
|
|
9990
|
+
if (!files || files.length === 0) return "";
|
|
9991
|
+
const lines = ["[files]"];
|
|
9992
|
+
for (const f of files) {
|
|
9993
|
+
const sizeLabel = formatBytes(f.sizeBytes);
|
|
9994
|
+
if (f.shortUrl) {
|
|
9995
|
+
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
9996
|
+
} else {
|
|
9997
|
+
lines.push(
|
|
9998
|
+
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
9999
|
+
);
|
|
10000
|
+
}
|
|
10001
|
+
}
|
|
10002
|
+
return lines.join("\n");
|
|
10003
|
+
}
|
|
10004
|
+
var MAX_BYTES, INGEST_TIMEOUT_MS;
|
|
10005
|
+
var init_files = __esm({
|
|
10006
|
+
"src/integrations/slack/files.ts"() {
|
|
10007
|
+
"use strict";
|
|
10008
|
+
init_client3();
|
|
10009
|
+
MAX_BYTES = 100 * 1024 * 1024;
|
|
10010
|
+
INGEST_TIMEOUT_MS = 2500;
|
|
10011
|
+
}
|
|
10012
|
+
});
|
|
10013
|
+
|
|
10014
|
+
// src/integrations/slack/read.ts
|
|
10015
|
+
var read_exports = {};
|
|
10016
|
+
__export(read_exports, {
|
|
10017
|
+
findChannelByName: () => findChannelByName,
|
|
10018
|
+
findUsers: () => findUsers,
|
|
10019
|
+
getChannelHistory: () => getChannelHistory,
|
|
10020
|
+
getPermalink: () => getPermalink,
|
|
10021
|
+
getThreadReplies: () => getThreadReplies,
|
|
10022
|
+
ingestMessageFiles: () => ingestMessageFiles
|
|
10023
|
+
});
|
|
10024
|
+
async function slackGet(method, params) {
|
|
10025
|
+
const token = getSlackBotToken();
|
|
10026
|
+
if (!token) return { ok: false, error: "slack_not_configured" };
|
|
10027
|
+
try {
|
|
10028
|
+
const qs = new URLSearchParams(params).toString();
|
|
10029
|
+
const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
|
|
10030
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
10031
|
+
});
|
|
10032
|
+
const json = await res.json().catch(() => ({}));
|
|
10033
|
+
if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
|
|
10034
|
+
return { ok: true, json };
|
|
10035
|
+
} catch (err) {
|
|
10036
|
+
return { ok: false, error: err?.message || "unknown" };
|
|
10037
|
+
}
|
|
10038
|
+
}
|
|
10039
|
+
function clampLimit(n, def, max) {
|
|
10040
|
+
const v = typeof n === "number" && Number.isFinite(n) ? n : def;
|
|
10041
|
+
return String(Math.min(Math.max(Math.floor(v), 1), max));
|
|
10042
|
+
}
|
|
10043
|
+
function liteMessage(m) {
|
|
10044
|
+
return {
|
|
10045
|
+
ts: String(m?.ts ?? ""),
|
|
10046
|
+
threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
|
|
10047
|
+
user: typeof m?.user === "string" ? m.user : void 0,
|
|
10048
|
+
botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
|
|
10049
|
+
text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
|
|
10050
|
+
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,
|
|
10051
|
+
replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
|
|
10052
|
+
};
|
|
10053
|
+
}
|
|
10054
|
+
async function enrichUserNames(messages) {
|
|
10055
|
+
const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
|
|
10056
|
+
const names = /* @__PURE__ */ new Map();
|
|
10057
|
+
await Promise.all(
|
|
10058
|
+
ids.map(async (id) => {
|
|
10059
|
+
const info = await resolveSlackUserInfo(id).catch(() => null);
|
|
10060
|
+
if (info?.name) names.set(id, info.name);
|
|
10061
|
+
})
|
|
10062
|
+
);
|
|
10063
|
+
return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
|
|
10064
|
+
}
|
|
10065
|
+
async function getChannelHistory(channel, limit) {
|
|
10066
|
+
const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
|
|
10067
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10068
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10069
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10070
|
+
}
|
|
10071
|
+
async function getThreadReplies(channel, threadTs, limit) {
|
|
10072
|
+
const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
|
|
10073
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10074
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10075
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10076
|
+
}
|
|
10077
|
+
async function getPermalink(channel, messageTs) {
|
|
10078
|
+
const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
|
|
10079
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10080
|
+
return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
|
|
10081
|
+
}
|
|
10082
|
+
async function findChannelByName(name) {
|
|
10083
|
+
const clean = name.replace(/^#/, "").trim().toLowerCase();
|
|
10084
|
+
let cursor;
|
|
10085
|
+
for (let page = 0; page < 10; page++) {
|
|
10086
|
+
const params = {
|
|
10087
|
+
types: "public_channel,private_channel",
|
|
10088
|
+
exclude_archived: "true",
|
|
10089
|
+
limit: "999"
|
|
10090
|
+
};
|
|
10091
|
+
if (cursor) params.cursor = cursor;
|
|
10092
|
+
const r = await slackGet("conversations.list", params);
|
|
10093
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10094
|
+
const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
|
|
10095
|
+
if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
|
|
10096
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10097
|
+
if (!cursor) break;
|
|
10098
|
+
}
|
|
10099
|
+
return { ok: false, error: "channel_not_found" };
|
|
10100
|
+
}
|
|
10101
|
+
async function findUsers(query, max = 10) {
|
|
10102
|
+
const q = query.trim().toLowerCase();
|
|
10103
|
+
if (!q) return { ok: false, error: "empty_query" };
|
|
10104
|
+
if (q.includes("@")) {
|
|
10105
|
+
const r = await slackGet("users.lookupByEmail", { email: query.trim() });
|
|
10106
|
+
if (r.ok && r.json.user) {
|
|
10107
|
+
const u = r.json.user;
|
|
10108
|
+
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 }] };
|
|
10109
|
+
}
|
|
10110
|
+
if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
|
|
10111
|
+
return { ok: false, error: r.error };
|
|
10112
|
+
}
|
|
10113
|
+
}
|
|
10114
|
+
const matches = [];
|
|
10115
|
+
let cursor;
|
|
10116
|
+
for (let page = 0; page < 10 && matches.length < max; page++) {
|
|
10117
|
+
const params = { limit: "1000" };
|
|
10118
|
+
if (cursor) params.cursor = cursor;
|
|
10119
|
+
const r = await slackGet("users.list", params);
|
|
10120
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10121
|
+
for (const u of r.json.members || []) {
|
|
10122
|
+
if (u?.deleted || u?.is_bot) continue;
|
|
10123
|
+
const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
|
|
10124
|
+
const email = String(u?.profile?.email || "");
|
|
10125
|
+
if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
|
|
10126
|
+
matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
|
|
10127
|
+
if (matches.length >= max) break;
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10131
|
+
if (!cursor) break;
|
|
10132
|
+
}
|
|
10133
|
+
return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
|
|
10134
|
+
}
|
|
10135
|
+
async function getSingleMessage(channel, ts, threadTs) {
|
|
10136
|
+
if (threadTs) {
|
|
10137
|
+
const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
|
|
10138
|
+
if (rr2.ok && Array.isArray(rr2.json.messages)) {
|
|
10139
|
+
const match = rr2.json.messages.find((m) => m?.ts === ts);
|
|
10140
|
+
if (match) return match;
|
|
10141
|
+
}
|
|
10142
|
+
}
|
|
10143
|
+
const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
|
|
10144
|
+
if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
|
|
10145
|
+
const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
|
|
10146
|
+
if (rr.ok && Array.isArray(rr.json.messages)) {
|
|
10147
|
+
return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
|
|
10148
|
+
}
|
|
10149
|
+
return null;
|
|
10150
|
+
}
|
|
10151
|
+
async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
|
|
10152
|
+
const msg = await getSingleMessage(channel, ts, threadTs);
|
|
10153
|
+
if (!msg) return { ok: false, error: "message_not_found" };
|
|
10154
|
+
const files = Array.isArray(msg.files) ? msg.files : [];
|
|
10155
|
+
if (files.length === 0) return { ok: true, data: [] };
|
|
10156
|
+
const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
|
|
10157
|
+
const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
|
|
10158
|
+
return {
|
|
10159
|
+
ok: true,
|
|
10160
|
+
data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
|
|
10161
|
+
};
|
|
10162
|
+
}
|
|
10163
|
+
var init_read = __esm({
|
|
10164
|
+
"src/integrations/slack/read.ts"() {
|
|
10165
|
+
"use strict";
|
|
10166
|
+
init_client3();
|
|
10167
|
+
}
|
|
10168
|
+
});
|
|
10169
|
+
|
|
9736
10170
|
// src/integrations/channels/messenger.ts
|
|
9737
10171
|
async function postMessage(opts) {
|
|
9738
10172
|
const adapter = getChannel(opts.channel);
|
|
@@ -9743,7 +10177,16 @@ async function postMessage(opts) {
|
|
|
9743
10177
|
let ref;
|
|
9744
10178
|
switch (opts.channel) {
|
|
9745
10179
|
case "slack": {
|
|
9746
|
-
|
|
10180
|
+
let slackChannel2 = opts.to.trim();
|
|
10181
|
+
if (slackChannel2.startsWith("#")) {
|
|
10182
|
+
const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
|
|
10183
|
+
const found = await findChannelByName2(slackChannel2);
|
|
10184
|
+
if (!found.ok || !found.data?.id) {
|
|
10185
|
+
return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
|
|
10186
|
+
}
|
|
10187
|
+
slackChannel2 = found.data.id;
|
|
10188
|
+
}
|
|
10189
|
+
const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
|
|
9747
10190
|
ref = slackRef;
|
|
9748
10191
|
break;
|
|
9749
10192
|
}
|
|
@@ -10050,6 +10493,46 @@ function buildMessengerTool() {
|
|
|
10050
10493
|
}
|
|
10051
10494
|
});
|
|
10052
10495
|
}
|
|
10496
|
+
function buildSlackTool(opts) {
|
|
10497
|
+
return tool13({
|
|
10498
|
+
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.",
|
|
10499
|
+
inputSchema: slackInputSchema,
|
|
10500
|
+
execute: async (input) => {
|
|
10501
|
+
if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
|
|
10502
|
+
switch (input.action) {
|
|
10503
|
+
case "history": {
|
|
10504
|
+
if (!input.channel) return { ok: false, error: "channel required" };
|
|
10505
|
+
return getChannelHistory(input.channel, input.limit);
|
|
10506
|
+
}
|
|
10507
|
+
case "replies": {
|
|
10508
|
+
if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
|
|
10509
|
+
return getThreadReplies(input.channel, input.threadTs, input.limit);
|
|
10510
|
+
}
|
|
10511
|
+
case "permalink": {
|
|
10512
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
10513
|
+
return getPermalink(input.channel, input.messageTs);
|
|
10514
|
+
}
|
|
10515
|
+
case "find_channel": {
|
|
10516
|
+
if (!input.query) return { ok: false, error: "query (channel name) required" };
|
|
10517
|
+
return findChannelByName(input.query);
|
|
10518
|
+
}
|
|
10519
|
+
case "find_user": {
|
|
10520
|
+
if (!input.query) return { ok: false, error: "query (name or email) required" };
|
|
10521
|
+
return findUsers(input.query);
|
|
10522
|
+
}
|
|
10523
|
+
case "user_info": {
|
|
10524
|
+
if (!input.user) return { ok: false, error: "user required" };
|
|
10525
|
+
const info = await resolveSlackUserInfo(input.user);
|
|
10526
|
+
return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
|
|
10527
|
+
}
|
|
10528
|
+
case "fetch_files": {
|
|
10529
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
10530
|
+
return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
|
|
10531
|
+
}
|
|
10532
|
+
}
|
|
10533
|
+
}
|
|
10534
|
+
});
|
|
10535
|
+
}
|
|
10053
10536
|
function buildScheduleTool(opts) {
|
|
10054
10537
|
return tool13({
|
|
10055
10538
|
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.",
|
|
@@ -10131,15 +10614,18 @@ function createOrchestratorActionTools(opts) {
|
|
|
10131
10614
|
return {
|
|
10132
10615
|
agent: buildAgentTool(opts),
|
|
10133
10616
|
messenger: buildMessengerTool(),
|
|
10617
|
+
slack: buildSlackTool(opts),
|
|
10134
10618
|
schedule: buildScheduleTool(opts),
|
|
10135
10619
|
webhook: buildWebhookTool(opts)
|
|
10136
10620
|
};
|
|
10137
10621
|
}
|
|
10138
|
-
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10622
|
+
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, slackInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10139
10623
|
var init_orchestrator_actions = __esm({
|
|
10140
10624
|
"src/tools/orchestrator-actions.ts"() {
|
|
10141
10625
|
"use strict";
|
|
10142
10626
|
init_messenger();
|
|
10627
|
+
init_read();
|
|
10628
|
+
init_client3();
|
|
10143
10629
|
init_schedules_store();
|
|
10144
10630
|
init_config();
|
|
10145
10631
|
init_webhooks_store();
|
|
@@ -10197,6 +10683,15 @@ var init_orchestrator_actions = __esm({
|
|
|
10197
10683
|
threadTs: z14.string().optional().describe("post + slack: reply in this thread."),
|
|
10198
10684
|
subject: z14.string().optional().describe("post + email: subject (future).")
|
|
10199
10685
|
});
|
|
10686
|
+
slackInputSchema = z14.object({
|
|
10687
|
+
action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
|
|
10688
|
+
channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
|
|
10689
|
+
threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
|
|
10690
|
+
messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
|
|
10691
|
+
query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
|
|
10692
|
+
user: z14.string().optional().describe("user_info: user id (U0123)."),
|
|
10693
|
+
limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
|
|
10694
|
+
});
|
|
10200
10695
|
scheduleInputSchema = z14.object({
|
|
10201
10696
|
action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
|
|
10202
10697
|
// create / update
|
|
@@ -15834,226 +16329,69 @@ function verifySlackSignature(opts) {
|
|
|
15834
16329
|
// src/server/routes/slack.ts
|
|
15835
16330
|
init_client3();
|
|
15836
16331
|
init_slack();
|
|
15837
|
-
|
|
15838
|
-
// src/integrations/slack/files.ts
|
|
15839
|
-
init_client3();
|
|
15840
|
-
var MAX_BYTES = 100 * 1024 * 1024;
|
|
15841
|
-
var INGEST_TIMEOUT_MS = 2500;
|
|
15842
|
-
function inferFileName(file) {
|
|
15843
|
-
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
15844
|
-
}
|
|
15845
|
-
function inferContentType(file) {
|
|
15846
|
-
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
15847
|
-
return "application/octet-stream";
|
|
15848
|
-
}
|
|
15849
|
-
function formatBytes(n) {
|
|
15850
|
-
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
15851
|
-
if (n < 1024) return `${n} B`;
|
|
15852
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
15853
|
-
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
15854
|
-
}
|
|
15855
|
-
function withTimeout(p, ms, label) {
|
|
15856
|
-
return new Promise((resolve13, reject) => {
|
|
15857
|
-
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
15858
|
-
p.then(
|
|
15859
|
-
(v) => {
|
|
15860
|
-
clearTimeout(t);
|
|
15861
|
-
resolve13(v);
|
|
15862
|
-
},
|
|
15863
|
-
(e) => {
|
|
15864
|
-
clearTimeout(t);
|
|
15865
|
-
reject(e);
|
|
15866
|
-
}
|
|
15867
|
-
);
|
|
15868
|
-
});
|
|
15869
|
-
}
|
|
15870
|
-
async function ingestOne(file, sessionId, botToken) {
|
|
15871
|
-
const fileName = inferFileName(file);
|
|
15872
|
-
const contentType = inferContentType(file);
|
|
15873
|
-
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
15874
|
-
const base = {
|
|
15875
|
-
slackFileId: file.id,
|
|
15876
|
-
fileName,
|
|
15877
|
-
contentType,
|
|
15878
|
-
sizeBytes: declaredSize
|
|
15879
|
-
};
|
|
15880
|
-
const sourceUrl = file.url_private_download || file.url_private;
|
|
15881
|
-
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
15882
|
-
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
15883
|
-
}
|
|
15884
|
-
if (declaredSize > MAX_BYTES) {
|
|
15885
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15886
|
-
}
|
|
15887
|
-
let bytes;
|
|
15888
|
-
try {
|
|
15889
|
-
const res = await fetch(sourceUrl, {
|
|
15890
|
-
headers: { Authorization: `Bearer ${botToken}` }
|
|
15891
|
-
});
|
|
15892
|
-
if (!res.ok) {
|
|
15893
|
-
return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
|
|
15894
|
-
}
|
|
15895
|
-
const ab = await res.arrayBuffer();
|
|
15896
|
-
if (ab.byteLength > MAX_BYTES) {
|
|
15897
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15898
|
-
}
|
|
15899
|
-
bytes = Buffer.from(ab);
|
|
15900
|
-
} catch (err) {
|
|
15901
|
-
return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
|
|
15902
|
-
}
|
|
15903
|
-
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15904
|
-
let upload;
|
|
15905
|
-
try {
|
|
15906
|
-
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
15907
|
-
} catch (err) {
|
|
15908
|
-
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
15909
|
-
}
|
|
15910
|
-
try {
|
|
15911
|
-
const putRes = await fetch(upload.uploadUrl, {
|
|
15912
|
-
method: "PUT",
|
|
15913
|
-
headers: { "Content-Type": contentType },
|
|
15914
|
-
body: bytes
|
|
15915
|
-
});
|
|
15916
|
-
if (!putRes.ok) {
|
|
15917
|
-
return {
|
|
15918
|
-
...base,
|
|
15919
|
-
sizeBytes: bytes.length,
|
|
15920
|
-
shortUrl: null,
|
|
15921
|
-
error: `gcs_put_${putRes.status}`
|
|
15922
|
-
};
|
|
15923
|
-
}
|
|
15924
|
-
} catch (err) {
|
|
15925
|
-
return {
|
|
15926
|
-
...base,
|
|
15927
|
-
sizeBytes: bytes.length,
|
|
15928
|
-
shortUrl: null,
|
|
15929
|
-
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
15930
|
-
};
|
|
15931
|
-
}
|
|
15932
|
-
try {
|
|
15933
|
-
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
15934
|
-
} catch (err) {
|
|
15935
|
-
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
15936
|
-
}
|
|
15937
|
-
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
15938
|
-
// server somehow forgot to return it (older remote-server versions).
|
|
15939
|
-
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
15940
|
-
return {
|
|
15941
|
-
...base,
|
|
15942
|
-
sizeBytes: bytes.length,
|
|
15943
|
-
shortUrl
|
|
15944
|
-
};
|
|
15945
|
-
}
|
|
15946
|
-
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
15947
|
-
try {
|
|
15948
|
-
const u = new URL(uploadUrl);
|
|
15949
|
-
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
15950
|
-
return `${u.origin}/f/${fileId}`;
|
|
15951
|
-
} catch {
|
|
15952
|
-
return null;
|
|
15953
|
-
}
|
|
15954
|
-
}
|
|
15955
|
-
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
15956
|
-
if (!Array.isArray(files) || files.length === 0) return [];
|
|
15957
|
-
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15958
|
-
if (!isRemoteConfigured2()) {
|
|
15959
|
-
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
15960
|
-
return files.map((f) => ({
|
|
15961
|
-
slackFileId: f.id,
|
|
15962
|
-
fileName: inferFileName(f),
|
|
15963
|
-
contentType: inferContentType(f),
|
|
15964
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15965
|
-
shortUrl: null,
|
|
15966
|
-
error: "storage_unconfigured"
|
|
15967
|
-
}));
|
|
15968
|
-
}
|
|
15969
|
-
const botToken = getSlackBotToken();
|
|
15970
|
-
if (!botToken) {
|
|
15971
|
-
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
15972
|
-
return files.map((f) => ({
|
|
15973
|
-
slackFileId: f.id,
|
|
15974
|
-
fileName: inferFileName(f),
|
|
15975
|
-
contentType: inferContentType(f),
|
|
15976
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15977
|
-
shortUrl: null,
|
|
15978
|
-
error: "no_bot_token"
|
|
15979
|
-
}));
|
|
15980
|
-
}
|
|
15981
|
-
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
15982
|
-
const startedAt = Date.now();
|
|
15983
|
-
const pipeline = Promise.allSettled(
|
|
15984
|
-
files.map((f) => ingestOne(f, sessionId, botToken))
|
|
15985
|
-
);
|
|
15986
|
-
let settled;
|
|
15987
|
-
try {
|
|
15988
|
-
settled = await withTimeout(pipeline, timeoutMs, "ingest");
|
|
15989
|
-
} catch (err) {
|
|
15990
|
-
console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
|
|
15991
|
-
return files.map((f) => ({
|
|
15992
|
-
slackFileId: f.id,
|
|
15993
|
-
fileName: inferFileName(f),
|
|
15994
|
-
contentType: inferContentType(f),
|
|
15995
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15996
|
-
shortUrl: null,
|
|
15997
|
-
error: "timeout"
|
|
15998
|
-
}));
|
|
15999
|
-
}
|
|
16000
|
-
const results = settled.map((s, i) => {
|
|
16001
|
-
if (s.status === "fulfilled") return s.value;
|
|
16002
|
-
const f = files[i];
|
|
16003
|
-
return {
|
|
16004
|
-
slackFileId: f.id,
|
|
16005
|
-
fileName: inferFileName(f),
|
|
16006
|
-
contentType: inferContentType(f),
|
|
16007
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16008
|
-
shortUrl: null,
|
|
16009
|
-
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
16010
|
-
};
|
|
16011
|
-
});
|
|
16012
|
-
const okCount = results.filter((r) => r.shortUrl).length;
|
|
16013
|
-
console.log(
|
|
16014
|
-
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
16015
|
-
);
|
|
16016
|
-
return results;
|
|
16017
|
-
}
|
|
16018
|
-
function formatFileBlock(files) {
|
|
16019
|
-
if (!files || files.length === 0) return "";
|
|
16020
|
-
const lines = ["[files]"];
|
|
16021
|
-
for (const f of files) {
|
|
16022
|
-
const sizeLabel = formatBytes(f.sizeBytes);
|
|
16023
|
-
if (f.shortUrl) {
|
|
16024
|
-
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
16025
|
-
} else {
|
|
16026
|
-
lines.push(
|
|
16027
|
-
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
16028
|
-
);
|
|
16029
|
-
}
|
|
16030
|
-
}
|
|
16031
|
-
return lines.join("\n");
|
|
16032
|
-
}
|
|
16033
|
-
|
|
16034
|
-
// src/server/routes/slack.ts
|
|
16332
|
+
init_files();
|
|
16035
16333
|
init_webhook_events();
|
|
16036
16334
|
init_inbox();
|
|
16037
16335
|
var recentlyHandled = /* @__PURE__ */ new Map();
|
|
16336
|
+
var recentlyDeniedReplies = /* @__PURE__ */ new Map();
|
|
16337
|
+
var inFlight = /* @__PURE__ */ new Set();
|
|
16038
16338
|
var MAX_RECENT = 1e3;
|
|
16339
|
+
function deliveryKey(channel, ts) {
|
|
16340
|
+
if (!channel || !ts) return null;
|
|
16341
|
+
return `${channel}\u241F${ts}`;
|
|
16342
|
+
}
|
|
16039
16343
|
function wasHandled(channel, ts) {
|
|
16040
|
-
|
|
16041
|
-
|
|
16344
|
+
const key2 = deliveryKey(channel, ts);
|
|
16345
|
+
if (!key2) return false;
|
|
16346
|
+
return recentlyHandled.has(key2) || inFlight.has(key2);
|
|
16347
|
+
}
|
|
16348
|
+
function markInFlight(channel, ts) {
|
|
16349
|
+
const key2 = deliveryKey(channel, ts);
|
|
16350
|
+
if (key2) inFlight.add(key2);
|
|
16351
|
+
}
|
|
16352
|
+
function clearInFlight(channel, ts) {
|
|
16353
|
+
const key2 = deliveryKey(channel, ts);
|
|
16354
|
+
if (key2) inFlight.delete(key2);
|
|
16042
16355
|
}
|
|
16043
16356
|
function markHandled(channel, ts) {
|
|
16044
|
-
|
|
16045
|
-
|
|
16357
|
+
const key2 = deliveryKey(channel, ts);
|
|
16358
|
+
if (!key2) return;
|
|
16359
|
+
inFlight.delete(key2);
|
|
16046
16360
|
recentlyHandled.set(key2, Date.now());
|
|
16047
16361
|
if (recentlyHandled.size > MAX_RECENT) {
|
|
16048
16362
|
const oldest = recentlyHandled.keys().next().value;
|
|
16049
16363
|
if (oldest) recentlyHandled.delete(oldest);
|
|
16050
16364
|
}
|
|
16051
16365
|
}
|
|
16366
|
+
function wasDeniedReplySent(channel, ts) {
|
|
16367
|
+
const key2 = deliveryKey(channel, ts);
|
|
16368
|
+
if (!key2) return false;
|
|
16369
|
+
return recentlyDeniedReplies.has(key2);
|
|
16370
|
+
}
|
|
16371
|
+
function markDeniedReplySent(channel, ts) {
|
|
16372
|
+
const key2 = deliveryKey(channel, ts);
|
|
16373
|
+
if (!key2) return;
|
|
16374
|
+
recentlyDeniedReplies.set(key2, Date.now());
|
|
16375
|
+
if (recentlyDeniedReplies.size > MAX_RECENT) {
|
|
16376
|
+
const oldest = recentlyDeniedReplies.keys().next().value;
|
|
16377
|
+
if (oldest) recentlyDeniedReplies.delete(oldest);
|
|
16378
|
+
}
|
|
16379
|
+
}
|
|
16052
16380
|
var slack = new Hono6();
|
|
16053
16381
|
slack.post("/events", async (c) => {
|
|
16054
|
-
const signingSecret = getSlackSigningSecret();
|
|
16055
|
-
if (!signingSecret) return c.json({ error: "slack not configured" }, 503);
|
|
16056
16382
|
const rawBody = await c.req.text();
|
|
16383
|
+
const signingSecret = getSlackSigningSecret();
|
|
16384
|
+
if (!signingSecret) {
|
|
16385
|
+
let unsignedPayload;
|
|
16386
|
+
try {
|
|
16387
|
+
unsignedPayload = JSON.parse(rawBody);
|
|
16388
|
+
} catch {
|
|
16389
|
+
}
|
|
16390
|
+
if (unsignedPayload?.type === "url_verification" && typeof unsignedPayload.challenge === "string") {
|
|
16391
|
+
return c.json({ challenge: unsignedPayload.challenge });
|
|
16392
|
+
}
|
|
16393
|
+
return c.json({ error: "slack not configured" }, 503);
|
|
16394
|
+
}
|
|
16057
16395
|
const verification = verifySlackSignature({
|
|
16058
16396
|
signingSecret,
|
|
16059
16397
|
timestampHeader: c.req.header("x-slack-request-timestamp") || null,
|
|
@@ -16107,65 +16445,79 @@ slack.post("/events", async (c) => {
|
|
|
16107
16445
|
if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
|
|
16108
16446
|
markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
|
|
16109
16447
|
}
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
|
|
16117
|
-
|
|
16118
|
-
|
|
16448
|
+
if (wasHandled(ev.channel, ev.ts)) {
|
|
16449
|
+
updateEvent(auditId, { status: "dropped", dropReason: "duplicate_delivery" });
|
|
16450
|
+
return c.json({ ok: true });
|
|
16451
|
+
}
|
|
16452
|
+
markInFlight(ev.channel, ev.ts);
|
|
16453
|
+
let routed = false;
|
|
16454
|
+
try {
|
|
16455
|
+
const orchestratorId = await findOrCreateOrchestratorId();
|
|
16456
|
+
if (orchestratorId) {
|
|
16457
|
+
inbound.content = await normalizeSlackMentions(inbound.content);
|
|
16458
|
+
if (ev.user) {
|
|
16459
|
+
const info = await resolveSlackUserInfo(ev.user);
|
|
16460
|
+
if (info?.name) {
|
|
16461
|
+
const emailSuffix = info.email ? ` (${info.email})` : "";
|
|
16462
|
+
const enriched = `user=${info.name} <@${ev.user}>${emailSuffix}`;
|
|
16463
|
+
inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
|
|
16464
|
+
}
|
|
16119
16465
|
}
|
|
16120
|
-
|
|
16121
|
-
|
|
16122
|
-
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
-
const block = formatFileBlock(ingested);
|
|
16131
|
-
if (block) inbound.content = `${inbound.content}
|
|
16466
|
+
const slackFiles = Array.isArray(ev.files) ? ev.files : [];
|
|
16467
|
+
if (ev.channel && ev.ts) {
|
|
16468
|
+
void addLoadingReaction(String(ev.channel), String(ev.ts));
|
|
16469
|
+
}
|
|
16470
|
+
let ingestedCount = 0;
|
|
16471
|
+
if (slackFiles.length > 0) {
|
|
16472
|
+
try {
|
|
16473
|
+
const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
|
|
16474
|
+
const block = formatFileBlock(ingested);
|
|
16475
|
+
if (block) inbound.content = `${inbound.content}
|
|
16132
16476
|
${block}`;
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16477
|
+
ingestedCount = ingested.filter((f) => f.shortUrl).length;
|
|
16478
|
+
} catch (err) {
|
|
16479
|
+
console.warn("[slack-files] ingestion threw:", err?.message || err);
|
|
16480
|
+
inbound.content = `${inbound.content}
|
|
16137
16481
|
[files] (ingestion failed: ${err?.message || "unknown"})`;
|
|
16482
|
+
}
|
|
16138
16483
|
}
|
|
16484
|
+
pushToInbox(orchestratorId, inbound);
|
|
16485
|
+
routed = true;
|
|
16486
|
+
markHandled(ev.channel, ev.ts);
|
|
16487
|
+
updateEvent(auditId, {
|
|
16488
|
+
status: "routed",
|
|
16489
|
+
sessionId: orchestratorId,
|
|
16490
|
+
...slackFiles.length > 0 ? {
|
|
16491
|
+
// Preserve the original meta (ts, thread_ts, team,
|
|
16492
|
+
// event_subtype) from recordEvent above — updateEvent does a
|
|
16493
|
+
// shallow merge, so we have to re-include them.
|
|
16494
|
+
meta: {
|
|
16495
|
+
ts: ev.ts,
|
|
16496
|
+
thread_ts: ev.thread_ts,
|
|
16497
|
+
team: ev.team,
|
|
16498
|
+
event_subtype: ev.subtype,
|
|
16499
|
+
fileCount: slackFiles.length,
|
|
16500
|
+
ingestedCount
|
|
16501
|
+
}
|
|
16502
|
+
} : {}
|
|
16503
|
+
});
|
|
16504
|
+
} else {
|
|
16505
|
+
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
16139
16506
|
}
|
|
16140
|
-
|
|
16141
|
-
|
|
16142
|
-
status: "routed",
|
|
16143
|
-
sessionId: orchestratorId,
|
|
16144
|
-
...slackFiles.length > 0 ? {
|
|
16145
|
-
// Preserve the original meta (ts, thread_ts, team,
|
|
16146
|
-
// event_subtype) from recordEvent above — updateEvent does a
|
|
16147
|
-
// shallow merge, so we have to re-include them.
|
|
16148
|
-
meta: {
|
|
16149
|
-
ts: ev.ts,
|
|
16150
|
-
thread_ts: ev.thread_ts,
|
|
16151
|
-
team: ev.team,
|
|
16152
|
-
event_subtype: ev.subtype,
|
|
16153
|
-
fileCount: slackFiles.length,
|
|
16154
|
-
ingestedCount
|
|
16155
|
-
}
|
|
16156
|
-
} : {}
|
|
16157
|
-
});
|
|
16158
|
-
} else {
|
|
16159
|
-
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
16507
|
+
} finally {
|
|
16508
|
+
if (!routed) clearInFlight(ev.channel, ev.ts);
|
|
16160
16509
|
}
|
|
16161
16510
|
} else if (dropReason) {
|
|
16162
16511
|
updateEvent(auditId, { status: "dropped", dropReason });
|
|
16163
16512
|
const userFacingDrops = ["user_not_allowed", "channel_not_allowed", "dm_blocked"];
|
|
16164
16513
|
if (userFacingDrops.includes(dropReason)) {
|
|
16165
16514
|
console.log(`[slack] dropped event from user=${payload.event.user} channel=${payload.event.channel}: ${dropReason}`);
|
|
16166
|
-
|
|
16167
|
-
|
|
16168
|
-
|
|
16515
|
+
if (!wasDeniedReplySent(payload.event.channel, payload.event.ts)) {
|
|
16516
|
+
markDeniedReplySent(payload.event.channel, payload.event.ts);
|
|
16517
|
+
void sendDeniedReply(payload.event, dropReason).catch((err) => {
|
|
16518
|
+
console.warn(`[slack] denied-reply failed:`, err?.message || err);
|
|
16519
|
+
});
|
|
16520
|
+
}
|
|
16169
16521
|
}
|
|
16170
16522
|
}
|
|
16171
16523
|
}
|
|
@@ -16190,7 +16542,7 @@ async function findOrCreateOrchestratorId() {
|
|
|
16190
16542
|
return null;
|
|
16191
16543
|
}
|
|
16192
16544
|
}
|
|
16193
|
-
async function sendDeniedReply(event) {
|
|
16545
|
+
async function sendDeniedReply(event, reason) {
|
|
16194
16546
|
const policy = getSlackDeniedReplyPolicy();
|
|
16195
16547
|
if (!policy.enabled) return;
|
|
16196
16548
|
const adapter = getSlackAdapter();
|
|
@@ -16199,7 +16551,7 @@ async function sendDeniedReply(event) {
|
|
|
16199
16551
|
const channel = String(event?.channel || "");
|
|
16200
16552
|
if (!channel) return;
|
|
16201
16553
|
const threadTs = event?.thread_ts || event?.ts;
|
|
16202
|
-
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`);
|
|
16554
|
+
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`).replace(/\{reason\}/g, reason);
|
|
16203
16555
|
const result = await adapter.postMessage({ channel, text, threadTs });
|
|
16204
16556
|
if (!result.ok) {
|
|
16205
16557
|
console.warn(`[slack] denied-reply post failed: ${result.error}`);
|