sparkecoder 0.1.139 → 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.d.ts +3 -3
- package/dist/agent/index.js +661 -10
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +756 -294
- package/dist/cli.js.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/{index-BAsQWqZj.d.ts → index-Cl_eUatM.d.ts} +96 -96
- package/dist/index.d.ts +5 -5
- package/dist/index.js +752 -292
- package/dist/index.js.map +1 -1
- package/dist/{schema-Dz-wABVY.d.ts → schema-BSz4MzhJ.d.ts} +3 -3
- package/dist/{search-CVVfuBPZ.d.ts → search-DOzC4ojH.d.ts} +4 -4
- package/dist/server/index.js +752 -292
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.d.ts +3 -3
- 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/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → 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
|
};
|
|
@@ -7652,6 +7653,105 @@ var init_cap_image_count = __esm({
|
|
|
7652
7653
|
}
|
|
7653
7654
|
});
|
|
7654
7655
|
|
|
7656
|
+
// src/utils/sanitize-images.ts
|
|
7657
|
+
function hasImageMagic(bytes) {
|
|
7658
|
+
if (bytes.length < 12) return false;
|
|
7659
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return true;
|
|
7660
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return true;
|
|
7661
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return true;
|
|
7662
|
+
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 true;
|
|
7663
|
+
return false;
|
|
7664
|
+
}
|
|
7665
|
+
function extractBase64(part) {
|
|
7666
|
+
const raw = typeof part?.data === "string" ? part.data : typeof part?.image === "string" ? part.image : null;
|
|
7667
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
7668
|
+
if (/^https?:\/\//i.test(raw)) return null;
|
|
7669
|
+
const dataUrl = raw.match(/^data:[^;,]+;base64,([\s\S]*)$/);
|
|
7670
|
+
return dataUrl ? dataUrl[1] : raw;
|
|
7671
|
+
}
|
|
7672
|
+
function isInvalidImagePart(part) {
|
|
7673
|
+
if (!part || typeof part !== "object") return false;
|
|
7674
|
+
const t = part.type;
|
|
7675
|
+
if (t !== "image" && t !== "image-data" && t !== "media") return false;
|
|
7676
|
+
const mt = part.mediaType;
|
|
7677
|
+
const b64 = extractBase64(part);
|
|
7678
|
+
if (b64 === null) {
|
|
7679
|
+
return typeof mt === "string" && mt.startsWith("image/") && !SUPPORTED_IMAGE_TYPES.includes(mt);
|
|
7680
|
+
}
|
|
7681
|
+
let bytes;
|
|
7682
|
+
try {
|
|
7683
|
+
bytes = Buffer.from(b64, "base64");
|
|
7684
|
+
} catch {
|
|
7685
|
+
return true;
|
|
7686
|
+
}
|
|
7687
|
+
if (bytes.length === 0) return true;
|
|
7688
|
+
return !hasImageMagic(bytes);
|
|
7689
|
+
}
|
|
7690
|
+
function placeholder() {
|
|
7691
|
+
return { type: "text", text: INVALID_IMAGE_PLACEHOLDER };
|
|
7692
|
+
}
|
|
7693
|
+
function sanitizeInvalidImages(messages) {
|
|
7694
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
7695
|
+
let mutated = false;
|
|
7696
|
+
let dropped = 0;
|
|
7697
|
+
const out = messages.slice();
|
|
7698
|
+
for (let i = 0; i < out.length; i++) {
|
|
7699
|
+
const msg = out[i];
|
|
7700
|
+
if (!Array.isArray(msg.content)) continue;
|
|
7701
|
+
let contentCloned = false;
|
|
7702
|
+
const ensureCloned = () => {
|
|
7703
|
+
if (contentCloned) return;
|
|
7704
|
+
out[i] = { ...msg, content: [...msg.content] };
|
|
7705
|
+
contentCloned = true;
|
|
7706
|
+
};
|
|
7707
|
+
const content = () => out[i].content;
|
|
7708
|
+
for (let j = 0; j < content().length; j++) {
|
|
7709
|
+
const part = content()[j];
|
|
7710
|
+
if (isInvalidImagePart(part)) {
|
|
7711
|
+
ensureCloned();
|
|
7712
|
+
out[i].content[j] = placeholder();
|
|
7713
|
+
mutated = true;
|
|
7714
|
+
dropped++;
|
|
7715
|
+
continue;
|
|
7716
|
+
}
|
|
7717
|
+
if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
|
|
7718
|
+
const innerValue = part.output.value;
|
|
7719
|
+
let innerMutated = false;
|
|
7720
|
+
const newValue = innerValue.slice();
|
|
7721
|
+
for (let k = 0; k < newValue.length; k++) {
|
|
7722
|
+
if (isInvalidImagePart(newValue[k])) {
|
|
7723
|
+
newValue[k] = placeholder();
|
|
7724
|
+
innerMutated = true;
|
|
7725
|
+
dropped++;
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7728
|
+
if (innerMutated) {
|
|
7729
|
+
ensureCloned();
|
|
7730
|
+
out[i].content[j] = {
|
|
7731
|
+
...part,
|
|
7732
|
+
output: { ...part.output, value: newValue }
|
|
7733
|
+
};
|
|
7734
|
+
mutated = true;
|
|
7735
|
+
}
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
if (mutated) {
|
|
7740
|
+
console.warn(
|
|
7741
|
+
`[sanitize-images] Replaced ${dropped} invalid image part(s) with a text placeholder (non-image bytes / unsupported type) to avoid provider 400 "invalid image" errors.`
|
|
7742
|
+
);
|
|
7743
|
+
}
|
|
7744
|
+
return mutated ? out : messages;
|
|
7745
|
+
}
|
|
7746
|
+
var SUPPORTED_IMAGE_TYPES, INVALID_IMAGE_PLACEHOLDER;
|
|
7747
|
+
var init_sanitize_images = __esm({
|
|
7748
|
+
"src/utils/sanitize-images.ts"() {
|
|
7749
|
+
"use strict";
|
|
7750
|
+
SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
7751
|
+
INVALID_IMAGE_PLACEHOLDER = "[invalid image omitted \u2014 the data was not a valid jpeg/png/gif/webp]";
|
|
7752
|
+
}
|
|
7753
|
+
});
|
|
7754
|
+
|
|
7655
7755
|
// src/agent/model-limits.ts
|
|
7656
7756
|
function getModelLimits(modelId) {
|
|
7657
7757
|
const normalized = modelId.trim().toLowerCase();
|
|
@@ -7967,6 +8067,7 @@ var init_context = __esm({
|
|
|
7967
8067
|
init_prompts();
|
|
7968
8068
|
init_sanitize_messages();
|
|
7969
8069
|
init_cap_image_count();
|
|
8070
|
+
init_sanitize_images();
|
|
7970
8071
|
init_model_limits();
|
|
7971
8072
|
TOOL_OUTPUT_TRIM_CHARS = 400;
|
|
7972
8073
|
COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -8016,6 +8117,7 @@ ${summaryContent}`
|
|
|
8016
8117
|
messages = repairToolPairing(messages);
|
|
8017
8118
|
messages = ensureToolResultsFollowCalls(messages);
|
|
8018
8119
|
messages = ensureEndsWithUserOrTool(messages);
|
|
8120
|
+
messages = sanitizeInvalidImages(messages);
|
|
8019
8121
|
messages = capImageCount(messages);
|
|
8020
8122
|
messages = this.enforceHardCap(messages);
|
|
8021
8123
|
return messages;
|
|
@@ -8027,28 +8129,35 @@ ${summaryContent}`
|
|
|
8027
8129
|
* repairs tool pairing so dropping can't orphan a tool result.
|
|
8028
8130
|
*/
|
|
8029
8131
|
enforceHardCap(messages) {
|
|
8030
|
-
const {
|
|
8031
|
-
const
|
|
8132
|
+
const { rollingTarget } = getModelLimits(this.modelId);
|
|
8133
|
+
const MAX_MESSAGES = 120;
|
|
8134
|
+
const tokenCeiling = Math.max(2e4, Math.floor(rollingTarget * 1.5));
|
|
8032
8135
|
const tokens = messages.map((m) => this.messageTokens(m));
|
|
8033
8136
|
let total = tokens.reduce((a, b) => a + b, 0);
|
|
8034
|
-
if (total <= ceiling) return messages;
|
|
8035
8137
|
const hasLeadingSummary = messages.length > 0 && messages[0].role === "system";
|
|
8036
8138
|
const firstBody = hasLeadingSummary ? 1 : 0;
|
|
8037
8139
|
const lastIndex = messages.length - 1;
|
|
8140
|
+
const bodyCount = messages.length - firstBody;
|
|
8141
|
+
if (bodyCount <= MAX_MESSAGES && total <= tokenCeiling) return messages;
|
|
8038
8142
|
let start = firstBody;
|
|
8039
|
-
|
|
8143
|
+
const countDropTarget = messages.length - MAX_MESSAGES;
|
|
8144
|
+
while (start < countDropTarget && start < lastIndex) {
|
|
8145
|
+
total -= tokens[start];
|
|
8146
|
+
start += 1;
|
|
8147
|
+
}
|
|
8148
|
+
while (start < lastIndex && total > tokenCeiling) {
|
|
8040
8149
|
total -= tokens[start];
|
|
8041
8150
|
start += 1;
|
|
8042
8151
|
}
|
|
8043
8152
|
let out = hasLeadingSummary ? [messages[0], ...messages.slice(start)] : messages.slice(start);
|
|
8044
|
-
if (total >
|
|
8153
|
+
if (total > tokenCeiling) {
|
|
8045
8154
|
out = out.map((m) => hardTruncateMessageText(m));
|
|
8046
8155
|
}
|
|
8047
8156
|
out = repairToolPairing(out);
|
|
8048
8157
|
out = ensureToolResultsFollowCalls(out);
|
|
8049
8158
|
out = ensureEndsWithUserOrTool(out);
|
|
8050
8159
|
console.warn(
|
|
8051
|
-
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${
|
|
8160
|
+
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${tokenCeiling} est-tokens / ${MAX_MESSAGES} msgs).`
|
|
8052
8161
|
);
|
|
8053
8162
|
return out;
|
|
8054
8163
|
}
|
|
@@ -8472,7 +8581,7 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8472
8581
|
const adapter = getSlackAdapter();
|
|
8473
8582
|
if (!adapter) return { ok: false, error: "slack_not_configured" };
|
|
8474
8583
|
const key2 = reactionKey(channel, timestamp);
|
|
8475
|
-
const
|
|
8584
|
+
const inFlight2 = (async () => {
|
|
8476
8585
|
try {
|
|
8477
8586
|
const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
|
|
8478
8587
|
if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
|
|
@@ -8484,11 +8593,11 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8484
8593
|
return { ok: false, error: err?.message || "unknown" };
|
|
8485
8594
|
}
|
|
8486
8595
|
})();
|
|
8487
|
-
pendingAdds.set(key2,
|
|
8488
|
-
void
|
|
8489
|
-
if (pendingAdds.get(key2) ===
|
|
8596
|
+
pendingAdds.set(key2, inFlight2);
|
|
8597
|
+
void inFlight2.finally(() => {
|
|
8598
|
+
if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
|
|
8490
8599
|
});
|
|
8491
|
-
return
|
|
8600
|
+
return inFlight2;
|
|
8492
8601
|
}
|
|
8493
8602
|
async function removeLoadingReaction(channel, timestamp) {
|
|
8494
8603
|
const adapter = getSlackAdapter();
|
|
@@ -8736,17 +8845,28 @@ async function fetchBotParticipatedInThread(channel, threadTs) {
|
|
|
8736
8845
|
const self = await ensureSlackSelfIdentity();
|
|
8737
8846
|
if (!self) return false;
|
|
8738
8847
|
try {
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
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;
|
|
8745
8868
|
}
|
|
8746
|
-
|
|
8747
|
-
return messages.some(
|
|
8748
|
-
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
8749
|
-
);
|
|
8869
|
+
return false;
|
|
8750
8870
|
} catch (err) {
|
|
8751
8871
|
console.warn(`[slack] conversations.replies error:`, err?.message || err);
|
|
8752
8872
|
return false;
|
|
@@ -9198,7 +9318,7 @@ async function reconcileOnce(now = Date.now()) {
|
|
|
9198
9318
|
entry2.updatedAt = Date.now();
|
|
9199
9319
|
const nudged = {
|
|
9200
9320
|
...entry2.event,
|
|
9201
|
-
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.]
|
|
9202
9322
|
${entry2.event.content}`,
|
|
9203
9323
|
wake: "now"
|
|
9204
9324
|
};
|
|
@@ -9606,22 +9726,444 @@ function getChannel(id) {
|
|
|
9606
9726
|
function listChannels() {
|
|
9607
9727
|
return Array.from(channels.values());
|
|
9608
9728
|
}
|
|
9609
|
-
function listOutboundChannels() {
|
|
9610
|
-
return listChannels().filter((c) => c.canSend());
|
|
9729
|
+
function listOutboundChannels() {
|
|
9730
|
+
return listChannels().filter((c) => c.canSend());
|
|
9731
|
+
}
|
|
9732
|
+
var channels;
|
|
9733
|
+
var init_registry = __esm({
|
|
9734
|
+
"src/integrations/channels/registry.ts"() {
|
|
9735
|
+
"use strict";
|
|
9736
|
+
init_web();
|
|
9737
|
+
init_slack();
|
|
9738
|
+
init_system();
|
|
9739
|
+
init_schedule();
|
|
9740
|
+
init_webhook();
|
|
9741
|
+
channels = /* @__PURE__ */ new Map();
|
|
9742
|
+
for (const c of [webChannel, slackChannel, systemChannel, scheduleChannel, webhookChannel]) {
|
|
9743
|
+
channels.set(c.id, c);
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9746
|
+
});
|
|
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
|
+
};
|
|
9611
10162
|
}
|
|
9612
|
-
var
|
|
9613
|
-
|
|
9614
|
-
"src/integrations/channels/registry.ts"() {
|
|
10163
|
+
var init_read = __esm({
|
|
10164
|
+
"src/integrations/slack/read.ts"() {
|
|
9615
10165
|
"use strict";
|
|
9616
|
-
|
|
9617
|
-
init_slack();
|
|
9618
|
-
init_system();
|
|
9619
|
-
init_schedule();
|
|
9620
|
-
init_webhook();
|
|
9621
|
-
channels = /* @__PURE__ */ new Map();
|
|
9622
|
-
for (const c of [webChannel, slackChannel, systemChannel, scheduleChannel, webhookChannel]) {
|
|
9623
|
-
channels.set(c.id, c);
|
|
9624
|
-
}
|
|
10166
|
+
init_client3();
|
|
9625
10167
|
}
|
|
9626
10168
|
});
|
|
9627
10169
|
|
|
@@ -9635,7 +10177,16 @@ async function postMessage(opts) {
|
|
|
9635
10177
|
let ref;
|
|
9636
10178
|
switch (opts.channel) {
|
|
9637
10179
|
case "slack": {
|
|
9638
|
-
|
|
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 };
|
|
9639
10190
|
ref = slackRef;
|
|
9640
10191
|
break;
|
|
9641
10192
|
}
|
|
@@ -9942,6 +10493,46 @@ function buildMessengerTool() {
|
|
|
9942
10493
|
}
|
|
9943
10494
|
});
|
|
9944
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
|
+
}
|
|
9945
10536
|
function buildScheduleTool(opts) {
|
|
9946
10537
|
return tool13({
|
|
9947
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.",
|
|
@@ -10023,15 +10614,18 @@ function createOrchestratorActionTools(opts) {
|
|
|
10023
10614
|
return {
|
|
10024
10615
|
agent: buildAgentTool(opts),
|
|
10025
10616
|
messenger: buildMessengerTool(),
|
|
10617
|
+
slack: buildSlackTool(opts),
|
|
10026
10618
|
schedule: buildScheduleTool(opts),
|
|
10027
10619
|
webhook: buildWebhookTool(opts)
|
|
10028
10620
|
};
|
|
10029
10621
|
}
|
|
10030
|
-
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10622
|
+
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, slackInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10031
10623
|
var init_orchestrator_actions = __esm({
|
|
10032
10624
|
"src/tools/orchestrator-actions.ts"() {
|
|
10033
10625
|
"use strict";
|
|
10034
10626
|
init_messenger();
|
|
10627
|
+
init_read();
|
|
10628
|
+
init_client3();
|
|
10035
10629
|
init_schedules_store();
|
|
10036
10630
|
init_config();
|
|
10037
10631
|
init_webhooks_store();
|
|
@@ -10089,6 +10683,15 @@ var init_orchestrator_actions = __esm({
|
|
|
10089
10683
|
threadTs: z14.string().optional().describe("post + slack: reply in this thread."),
|
|
10090
10684
|
subject: z14.string().optional().describe("post + email: subject (future).")
|
|
10091
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
|
+
});
|
|
10092
10695
|
scheduleInputSchema = z14.object({
|
|
10093
10696
|
action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
|
|
10094
10697
|
// create / update
|
|
@@ -15726,226 +16329,69 @@ function verifySlackSignature(opts) {
|
|
|
15726
16329
|
// src/server/routes/slack.ts
|
|
15727
16330
|
init_client3();
|
|
15728
16331
|
init_slack();
|
|
15729
|
-
|
|
15730
|
-
// src/integrations/slack/files.ts
|
|
15731
|
-
init_client3();
|
|
15732
|
-
var MAX_BYTES = 100 * 1024 * 1024;
|
|
15733
|
-
var INGEST_TIMEOUT_MS = 2500;
|
|
15734
|
-
function inferFileName(file) {
|
|
15735
|
-
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
15736
|
-
}
|
|
15737
|
-
function inferContentType(file) {
|
|
15738
|
-
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
15739
|
-
return "application/octet-stream";
|
|
15740
|
-
}
|
|
15741
|
-
function formatBytes(n) {
|
|
15742
|
-
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
15743
|
-
if (n < 1024) return `${n} B`;
|
|
15744
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
15745
|
-
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
15746
|
-
}
|
|
15747
|
-
function withTimeout(p, ms, label) {
|
|
15748
|
-
return new Promise((resolve13, reject) => {
|
|
15749
|
-
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
15750
|
-
p.then(
|
|
15751
|
-
(v) => {
|
|
15752
|
-
clearTimeout(t);
|
|
15753
|
-
resolve13(v);
|
|
15754
|
-
},
|
|
15755
|
-
(e) => {
|
|
15756
|
-
clearTimeout(t);
|
|
15757
|
-
reject(e);
|
|
15758
|
-
}
|
|
15759
|
-
);
|
|
15760
|
-
});
|
|
15761
|
-
}
|
|
15762
|
-
async function ingestOne(file, sessionId, botToken) {
|
|
15763
|
-
const fileName = inferFileName(file);
|
|
15764
|
-
const contentType = inferContentType(file);
|
|
15765
|
-
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
15766
|
-
const base = {
|
|
15767
|
-
slackFileId: file.id,
|
|
15768
|
-
fileName,
|
|
15769
|
-
contentType,
|
|
15770
|
-
sizeBytes: declaredSize
|
|
15771
|
-
};
|
|
15772
|
-
const sourceUrl = file.url_private_download || file.url_private;
|
|
15773
|
-
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
15774
|
-
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
15775
|
-
}
|
|
15776
|
-
if (declaredSize > MAX_BYTES) {
|
|
15777
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15778
|
-
}
|
|
15779
|
-
let bytes;
|
|
15780
|
-
try {
|
|
15781
|
-
const res = await fetch(sourceUrl, {
|
|
15782
|
-
headers: { Authorization: `Bearer ${botToken}` }
|
|
15783
|
-
});
|
|
15784
|
-
if (!res.ok) {
|
|
15785
|
-
return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
|
|
15786
|
-
}
|
|
15787
|
-
const ab = await res.arrayBuffer();
|
|
15788
|
-
if (ab.byteLength > MAX_BYTES) {
|
|
15789
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15790
|
-
}
|
|
15791
|
-
bytes = Buffer.from(ab);
|
|
15792
|
-
} catch (err) {
|
|
15793
|
-
return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
|
|
15794
|
-
}
|
|
15795
|
-
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15796
|
-
let upload;
|
|
15797
|
-
try {
|
|
15798
|
-
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
15799
|
-
} catch (err) {
|
|
15800
|
-
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
15801
|
-
}
|
|
15802
|
-
try {
|
|
15803
|
-
const putRes = await fetch(upload.uploadUrl, {
|
|
15804
|
-
method: "PUT",
|
|
15805
|
-
headers: { "Content-Type": contentType },
|
|
15806
|
-
body: bytes
|
|
15807
|
-
});
|
|
15808
|
-
if (!putRes.ok) {
|
|
15809
|
-
return {
|
|
15810
|
-
...base,
|
|
15811
|
-
sizeBytes: bytes.length,
|
|
15812
|
-
shortUrl: null,
|
|
15813
|
-
error: `gcs_put_${putRes.status}`
|
|
15814
|
-
};
|
|
15815
|
-
}
|
|
15816
|
-
} catch (err) {
|
|
15817
|
-
return {
|
|
15818
|
-
...base,
|
|
15819
|
-
sizeBytes: bytes.length,
|
|
15820
|
-
shortUrl: null,
|
|
15821
|
-
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
15822
|
-
};
|
|
15823
|
-
}
|
|
15824
|
-
try {
|
|
15825
|
-
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
15826
|
-
} catch (err) {
|
|
15827
|
-
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
15828
|
-
}
|
|
15829
|
-
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
15830
|
-
// server somehow forgot to return it (older remote-server versions).
|
|
15831
|
-
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
15832
|
-
return {
|
|
15833
|
-
...base,
|
|
15834
|
-
sizeBytes: bytes.length,
|
|
15835
|
-
shortUrl
|
|
15836
|
-
};
|
|
15837
|
-
}
|
|
15838
|
-
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
15839
|
-
try {
|
|
15840
|
-
const u = new URL(uploadUrl);
|
|
15841
|
-
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
15842
|
-
return `${u.origin}/f/${fileId}`;
|
|
15843
|
-
} catch {
|
|
15844
|
-
return null;
|
|
15845
|
-
}
|
|
15846
|
-
}
|
|
15847
|
-
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
15848
|
-
if (!Array.isArray(files) || files.length === 0) return [];
|
|
15849
|
-
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15850
|
-
if (!isRemoteConfigured2()) {
|
|
15851
|
-
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
15852
|
-
return files.map((f) => ({
|
|
15853
|
-
slackFileId: f.id,
|
|
15854
|
-
fileName: inferFileName(f),
|
|
15855
|
-
contentType: inferContentType(f),
|
|
15856
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15857
|
-
shortUrl: null,
|
|
15858
|
-
error: "storage_unconfigured"
|
|
15859
|
-
}));
|
|
15860
|
-
}
|
|
15861
|
-
const botToken = getSlackBotToken();
|
|
15862
|
-
if (!botToken) {
|
|
15863
|
-
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
15864
|
-
return files.map((f) => ({
|
|
15865
|
-
slackFileId: f.id,
|
|
15866
|
-
fileName: inferFileName(f),
|
|
15867
|
-
contentType: inferContentType(f),
|
|
15868
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15869
|
-
shortUrl: null,
|
|
15870
|
-
error: "no_bot_token"
|
|
15871
|
-
}));
|
|
15872
|
-
}
|
|
15873
|
-
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
15874
|
-
const startedAt = Date.now();
|
|
15875
|
-
const pipeline = Promise.allSettled(
|
|
15876
|
-
files.map((f) => ingestOne(f, sessionId, botToken))
|
|
15877
|
-
);
|
|
15878
|
-
let settled;
|
|
15879
|
-
try {
|
|
15880
|
-
settled = await withTimeout(pipeline, timeoutMs, "ingest");
|
|
15881
|
-
} catch (err) {
|
|
15882
|
-
console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
|
|
15883
|
-
return files.map((f) => ({
|
|
15884
|
-
slackFileId: f.id,
|
|
15885
|
-
fileName: inferFileName(f),
|
|
15886
|
-
contentType: inferContentType(f),
|
|
15887
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15888
|
-
shortUrl: null,
|
|
15889
|
-
error: "timeout"
|
|
15890
|
-
}));
|
|
15891
|
-
}
|
|
15892
|
-
const results = settled.map((s, i) => {
|
|
15893
|
-
if (s.status === "fulfilled") return s.value;
|
|
15894
|
-
const f = files[i];
|
|
15895
|
-
return {
|
|
15896
|
-
slackFileId: f.id,
|
|
15897
|
-
fileName: inferFileName(f),
|
|
15898
|
-
contentType: inferContentType(f),
|
|
15899
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15900
|
-
shortUrl: null,
|
|
15901
|
-
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
15902
|
-
};
|
|
15903
|
-
});
|
|
15904
|
-
const okCount = results.filter((r) => r.shortUrl).length;
|
|
15905
|
-
console.log(
|
|
15906
|
-
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
15907
|
-
);
|
|
15908
|
-
return results;
|
|
15909
|
-
}
|
|
15910
|
-
function formatFileBlock(files) {
|
|
15911
|
-
if (!files || files.length === 0) return "";
|
|
15912
|
-
const lines = ["[files]"];
|
|
15913
|
-
for (const f of files) {
|
|
15914
|
-
const sizeLabel = formatBytes(f.sizeBytes);
|
|
15915
|
-
if (f.shortUrl) {
|
|
15916
|
-
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
15917
|
-
} else {
|
|
15918
|
-
lines.push(
|
|
15919
|
-
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
15920
|
-
);
|
|
15921
|
-
}
|
|
15922
|
-
}
|
|
15923
|
-
return lines.join("\n");
|
|
15924
|
-
}
|
|
15925
|
-
|
|
15926
|
-
// src/server/routes/slack.ts
|
|
16332
|
+
init_files();
|
|
15927
16333
|
init_webhook_events();
|
|
15928
16334
|
init_inbox();
|
|
15929
16335
|
var recentlyHandled = /* @__PURE__ */ new Map();
|
|
16336
|
+
var recentlyDeniedReplies = /* @__PURE__ */ new Map();
|
|
16337
|
+
var inFlight = /* @__PURE__ */ new Set();
|
|
15930
16338
|
var MAX_RECENT = 1e3;
|
|
16339
|
+
function deliveryKey(channel, ts) {
|
|
16340
|
+
if (!channel || !ts) return null;
|
|
16341
|
+
return `${channel}\u241F${ts}`;
|
|
16342
|
+
}
|
|
15931
16343
|
function wasHandled(channel, ts) {
|
|
15932
|
-
|
|
15933
|
-
|
|
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);
|
|
15934
16355
|
}
|
|
15935
16356
|
function markHandled(channel, ts) {
|
|
15936
|
-
|
|
15937
|
-
|
|
16357
|
+
const key2 = deliveryKey(channel, ts);
|
|
16358
|
+
if (!key2) return;
|
|
16359
|
+
inFlight.delete(key2);
|
|
15938
16360
|
recentlyHandled.set(key2, Date.now());
|
|
15939
16361
|
if (recentlyHandled.size > MAX_RECENT) {
|
|
15940
16362
|
const oldest = recentlyHandled.keys().next().value;
|
|
15941
16363
|
if (oldest) recentlyHandled.delete(oldest);
|
|
15942
16364
|
}
|
|
15943
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
|
+
}
|
|
15944
16380
|
var slack = new Hono6();
|
|
15945
16381
|
slack.post("/events", async (c) => {
|
|
15946
|
-
const signingSecret = getSlackSigningSecret();
|
|
15947
|
-
if (!signingSecret) return c.json({ error: "slack not configured" }, 503);
|
|
15948
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
|
+
}
|
|
15949
16395
|
const verification = verifySlackSignature({
|
|
15950
16396
|
signingSecret,
|
|
15951
16397
|
timestampHeader: c.req.header("x-slack-request-timestamp") || null,
|
|
@@ -15999,65 +16445,79 @@ slack.post("/events", async (c) => {
|
|
|
15999
16445
|
if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
|
|
16000
16446
|
markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
|
|
16001
16447
|
}
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
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
|
+
}
|
|
16011
16465
|
}
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
const block = formatFileBlock(ingested);
|
|
16023
|
-
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}
|
|
16024
16476
|
${block}`;
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
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}
|
|
16029
16481
|
[files] (ingestion failed: ${err?.message || "unknown"})`;
|
|
16482
|
+
}
|
|
16030
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" });
|
|
16031
16506
|
}
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
status: "routed",
|
|
16035
|
-
sessionId: orchestratorId,
|
|
16036
|
-
...slackFiles.length > 0 ? {
|
|
16037
|
-
// Preserve the original meta (ts, thread_ts, team,
|
|
16038
|
-
// event_subtype) from recordEvent above — updateEvent does a
|
|
16039
|
-
// shallow merge, so we have to re-include them.
|
|
16040
|
-
meta: {
|
|
16041
|
-
ts: ev.ts,
|
|
16042
|
-
thread_ts: ev.thread_ts,
|
|
16043
|
-
team: ev.team,
|
|
16044
|
-
event_subtype: ev.subtype,
|
|
16045
|
-
fileCount: slackFiles.length,
|
|
16046
|
-
ingestedCount
|
|
16047
|
-
}
|
|
16048
|
-
} : {}
|
|
16049
|
-
});
|
|
16050
|
-
} else {
|
|
16051
|
-
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
16507
|
+
} finally {
|
|
16508
|
+
if (!routed) clearInFlight(ev.channel, ev.ts);
|
|
16052
16509
|
}
|
|
16053
16510
|
} else if (dropReason) {
|
|
16054
16511
|
updateEvent(auditId, { status: "dropped", dropReason });
|
|
16055
16512
|
const userFacingDrops = ["user_not_allowed", "channel_not_allowed", "dm_blocked"];
|
|
16056
16513
|
if (userFacingDrops.includes(dropReason)) {
|
|
16057
16514
|
console.log(`[slack] dropped event from user=${payload.event.user} channel=${payload.event.channel}: ${dropReason}`);
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
|
|
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
|
+
}
|
|
16061
16521
|
}
|
|
16062
16522
|
}
|
|
16063
16523
|
}
|
|
@@ -16082,7 +16542,7 @@ async function findOrCreateOrchestratorId() {
|
|
|
16082
16542
|
return null;
|
|
16083
16543
|
}
|
|
16084
16544
|
}
|
|
16085
|
-
async function sendDeniedReply(event) {
|
|
16545
|
+
async function sendDeniedReply(event, reason) {
|
|
16086
16546
|
const policy = getSlackDeniedReplyPolicy();
|
|
16087
16547
|
if (!policy.enabled) return;
|
|
16088
16548
|
const adapter = getSlackAdapter();
|
|
@@ -16091,7 +16551,7 @@ async function sendDeniedReply(event) {
|
|
|
16091
16551
|
const channel = String(event?.channel || "");
|
|
16092
16552
|
if (!channel) return;
|
|
16093
16553
|
const threadTs = event?.thread_ts || event?.ts;
|
|
16094
|
-
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);
|
|
16095
16555
|
const result = await adapter.postMessage({ channel, text, threadTs });
|
|
16096
16556
|
if (!result.ok) {
|
|
16097
16557
|
console.warn(`[slack] denied-reply post failed: ${result.error}`);
|