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/index.js
CHANGED
|
@@ -585,6 +585,7 @@ function loadConfig(configPath, workingDirectory) {
|
|
|
585
585
|
}
|
|
586
586
|
if (process.env.SPARKECODER_PORT) {
|
|
587
587
|
rawConfig.server = {
|
|
588
|
+
...rawConfig.server ?? {},
|
|
588
589
|
port: parseInt(process.env.SPARKECODER_PORT, 10),
|
|
589
590
|
host: rawConfig.server?.host ?? "127.0.0.1"
|
|
590
591
|
};
|
|
@@ -7669,6 +7670,105 @@ var init_cap_image_count = __esm({
|
|
|
7669
7670
|
}
|
|
7670
7671
|
});
|
|
7671
7672
|
|
|
7673
|
+
// src/utils/sanitize-images.ts
|
|
7674
|
+
function hasImageMagic(bytes) {
|
|
7675
|
+
if (bytes.length < 12) return false;
|
|
7676
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return true;
|
|
7677
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return true;
|
|
7678
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return true;
|
|
7679
|
+
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;
|
|
7680
|
+
return false;
|
|
7681
|
+
}
|
|
7682
|
+
function extractBase64(part) {
|
|
7683
|
+
const raw = typeof part?.data === "string" ? part.data : typeof part?.image === "string" ? part.image : null;
|
|
7684
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
7685
|
+
if (/^https?:\/\//i.test(raw)) return null;
|
|
7686
|
+
const dataUrl = raw.match(/^data:[^;,]+;base64,([\s\S]*)$/);
|
|
7687
|
+
return dataUrl ? dataUrl[1] : raw;
|
|
7688
|
+
}
|
|
7689
|
+
function isInvalidImagePart(part) {
|
|
7690
|
+
if (!part || typeof part !== "object") return false;
|
|
7691
|
+
const t = part.type;
|
|
7692
|
+
if (t !== "image" && t !== "image-data" && t !== "media") return false;
|
|
7693
|
+
const mt = part.mediaType;
|
|
7694
|
+
const b64 = extractBase64(part);
|
|
7695
|
+
if (b64 === null) {
|
|
7696
|
+
return typeof mt === "string" && mt.startsWith("image/") && !SUPPORTED_IMAGE_TYPES.includes(mt);
|
|
7697
|
+
}
|
|
7698
|
+
let bytes;
|
|
7699
|
+
try {
|
|
7700
|
+
bytes = Buffer.from(b64, "base64");
|
|
7701
|
+
} catch {
|
|
7702
|
+
return true;
|
|
7703
|
+
}
|
|
7704
|
+
if (bytes.length === 0) return true;
|
|
7705
|
+
return !hasImageMagic(bytes);
|
|
7706
|
+
}
|
|
7707
|
+
function placeholder() {
|
|
7708
|
+
return { type: "text", text: INVALID_IMAGE_PLACEHOLDER };
|
|
7709
|
+
}
|
|
7710
|
+
function sanitizeInvalidImages(messages) {
|
|
7711
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
7712
|
+
let mutated = false;
|
|
7713
|
+
let dropped = 0;
|
|
7714
|
+
const out = messages.slice();
|
|
7715
|
+
for (let i = 0; i < out.length; i++) {
|
|
7716
|
+
const msg = out[i];
|
|
7717
|
+
if (!Array.isArray(msg.content)) continue;
|
|
7718
|
+
let contentCloned = false;
|
|
7719
|
+
const ensureCloned = () => {
|
|
7720
|
+
if (contentCloned) return;
|
|
7721
|
+
out[i] = { ...msg, content: [...msg.content] };
|
|
7722
|
+
contentCloned = true;
|
|
7723
|
+
};
|
|
7724
|
+
const content = () => out[i].content;
|
|
7725
|
+
for (let j = 0; j < content().length; j++) {
|
|
7726
|
+
const part = content()[j];
|
|
7727
|
+
if (isInvalidImagePart(part)) {
|
|
7728
|
+
ensureCloned();
|
|
7729
|
+
out[i].content[j] = placeholder();
|
|
7730
|
+
mutated = true;
|
|
7731
|
+
dropped++;
|
|
7732
|
+
continue;
|
|
7733
|
+
}
|
|
7734
|
+
if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
|
|
7735
|
+
const innerValue = part.output.value;
|
|
7736
|
+
let innerMutated = false;
|
|
7737
|
+
const newValue = innerValue.slice();
|
|
7738
|
+
for (let k = 0; k < newValue.length; k++) {
|
|
7739
|
+
if (isInvalidImagePart(newValue[k])) {
|
|
7740
|
+
newValue[k] = placeholder();
|
|
7741
|
+
innerMutated = true;
|
|
7742
|
+
dropped++;
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7745
|
+
if (innerMutated) {
|
|
7746
|
+
ensureCloned();
|
|
7747
|
+
out[i].content[j] = {
|
|
7748
|
+
...part,
|
|
7749
|
+
output: { ...part.output, value: newValue }
|
|
7750
|
+
};
|
|
7751
|
+
mutated = true;
|
|
7752
|
+
}
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
if (mutated) {
|
|
7757
|
+
console.warn(
|
|
7758
|
+
`[sanitize-images] Replaced ${dropped} invalid image part(s) with a text placeholder (non-image bytes / unsupported type) to avoid provider 400 "invalid image" errors.`
|
|
7759
|
+
);
|
|
7760
|
+
}
|
|
7761
|
+
return mutated ? out : messages;
|
|
7762
|
+
}
|
|
7763
|
+
var SUPPORTED_IMAGE_TYPES, INVALID_IMAGE_PLACEHOLDER;
|
|
7764
|
+
var init_sanitize_images = __esm({
|
|
7765
|
+
"src/utils/sanitize-images.ts"() {
|
|
7766
|
+
"use strict";
|
|
7767
|
+
SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
7768
|
+
INVALID_IMAGE_PLACEHOLDER = "[invalid image omitted \u2014 the data was not a valid jpeg/png/gif/webp]";
|
|
7769
|
+
}
|
|
7770
|
+
});
|
|
7771
|
+
|
|
7672
7772
|
// src/agent/model-limits.ts
|
|
7673
7773
|
function getModelLimits(modelId) {
|
|
7674
7774
|
const normalized = modelId.trim().toLowerCase();
|
|
@@ -7984,6 +8084,7 @@ var init_context = __esm({
|
|
|
7984
8084
|
init_prompts();
|
|
7985
8085
|
init_sanitize_messages();
|
|
7986
8086
|
init_cap_image_count();
|
|
8087
|
+
init_sanitize_images();
|
|
7987
8088
|
init_model_limits();
|
|
7988
8089
|
TOOL_OUTPUT_TRIM_CHARS = 400;
|
|
7989
8090
|
COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -8033,6 +8134,7 @@ ${summaryContent}`
|
|
|
8033
8134
|
messages = repairToolPairing(messages);
|
|
8034
8135
|
messages = ensureToolResultsFollowCalls(messages);
|
|
8035
8136
|
messages = ensureEndsWithUserOrTool(messages);
|
|
8137
|
+
messages = sanitizeInvalidImages(messages);
|
|
8036
8138
|
messages = capImageCount(messages);
|
|
8037
8139
|
messages = this.enforceHardCap(messages);
|
|
8038
8140
|
return messages;
|
|
@@ -8044,28 +8146,35 @@ ${summaryContent}`
|
|
|
8044
8146
|
* repairs tool pairing so dropping can't orphan a tool result.
|
|
8045
8147
|
*/
|
|
8046
8148
|
enforceHardCap(messages) {
|
|
8047
|
-
const {
|
|
8048
|
-
const
|
|
8149
|
+
const { rollingTarget } = getModelLimits(this.modelId);
|
|
8150
|
+
const MAX_MESSAGES = 120;
|
|
8151
|
+
const tokenCeiling = Math.max(2e4, Math.floor(rollingTarget * 1.5));
|
|
8049
8152
|
const tokens = messages.map((m) => this.messageTokens(m));
|
|
8050
8153
|
let total = tokens.reduce((a, b) => a + b, 0);
|
|
8051
|
-
if (total <= ceiling) return messages;
|
|
8052
8154
|
const hasLeadingSummary = messages.length > 0 && messages[0].role === "system";
|
|
8053
8155
|
const firstBody = hasLeadingSummary ? 1 : 0;
|
|
8054
8156
|
const lastIndex = messages.length - 1;
|
|
8157
|
+
const bodyCount = messages.length - firstBody;
|
|
8158
|
+
if (bodyCount <= MAX_MESSAGES && total <= tokenCeiling) return messages;
|
|
8055
8159
|
let start = firstBody;
|
|
8056
|
-
|
|
8160
|
+
const countDropTarget = messages.length - MAX_MESSAGES;
|
|
8161
|
+
while (start < countDropTarget && start < lastIndex) {
|
|
8162
|
+
total -= tokens[start];
|
|
8163
|
+
start += 1;
|
|
8164
|
+
}
|
|
8165
|
+
while (start < lastIndex && total > tokenCeiling) {
|
|
8057
8166
|
total -= tokens[start];
|
|
8058
8167
|
start += 1;
|
|
8059
8168
|
}
|
|
8060
8169
|
let out = hasLeadingSummary ? [messages[0], ...messages.slice(start)] : messages.slice(start);
|
|
8061
|
-
if (total >
|
|
8170
|
+
if (total > tokenCeiling) {
|
|
8062
8171
|
out = out.map((m) => hardTruncateMessageText(m));
|
|
8063
8172
|
}
|
|
8064
8173
|
out = repairToolPairing(out);
|
|
8065
8174
|
out = ensureToolResultsFollowCalls(out);
|
|
8066
8175
|
out = ensureEndsWithUserOrTool(out);
|
|
8067
8176
|
console.warn(
|
|
8068
|
-
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${
|
|
8177
|
+
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${tokenCeiling} est-tokens / ${MAX_MESSAGES} msgs).`
|
|
8069
8178
|
);
|
|
8070
8179
|
return out;
|
|
8071
8180
|
}
|
|
@@ -8489,7 +8598,7 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8489
8598
|
const adapter = getSlackAdapter();
|
|
8490
8599
|
if (!adapter) return { ok: false, error: "slack_not_configured" };
|
|
8491
8600
|
const key2 = reactionKey(channel, timestamp);
|
|
8492
|
-
const
|
|
8601
|
+
const inFlight2 = (async () => {
|
|
8493
8602
|
try {
|
|
8494
8603
|
const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
|
|
8495
8604
|
if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
|
|
@@ -8501,11 +8610,11 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
8501
8610
|
return { ok: false, error: err?.message || "unknown" };
|
|
8502
8611
|
}
|
|
8503
8612
|
})();
|
|
8504
|
-
pendingAdds.set(key2,
|
|
8505
|
-
void
|
|
8506
|
-
if (pendingAdds.get(key2) ===
|
|
8613
|
+
pendingAdds.set(key2, inFlight2);
|
|
8614
|
+
void inFlight2.finally(() => {
|
|
8615
|
+
if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
|
|
8507
8616
|
});
|
|
8508
|
-
return
|
|
8617
|
+
return inFlight2;
|
|
8509
8618
|
}
|
|
8510
8619
|
async function removeLoadingReaction(channel, timestamp) {
|
|
8511
8620
|
const adapter = getSlackAdapter();
|
|
@@ -8753,17 +8862,28 @@ async function fetchBotParticipatedInThread(channel, threadTs) {
|
|
|
8753
8862
|
const self = await ensureSlackSelfIdentity();
|
|
8754
8863
|
if (!self) return false;
|
|
8755
8864
|
try {
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8865
|
+
let cursor = "";
|
|
8866
|
+
for (let page = 0; page < 10; page++) {
|
|
8867
|
+
const params = new URLSearchParams({ channel, ts: threadTs, limit: "200" });
|
|
8868
|
+
if (cursor) params.set("cursor", cursor);
|
|
8869
|
+
const res = await fetch(`https://slack.com/api/conversations.replies?${params.toString()}`, {
|
|
8870
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
8871
|
+
});
|
|
8872
|
+
const data = await res.json().catch(() => ({}));
|
|
8873
|
+
if (!data?.ok) {
|
|
8874
|
+
console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
|
|
8875
|
+
return false;
|
|
8876
|
+
}
|
|
8877
|
+
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
8878
|
+
if (messages.some(
|
|
8879
|
+
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
8880
|
+
)) {
|
|
8881
|
+
return true;
|
|
8882
|
+
}
|
|
8883
|
+
cursor = String(data.response_metadata?.next_cursor || "");
|
|
8884
|
+
if (!cursor) break;
|
|
8762
8885
|
}
|
|
8763
|
-
|
|
8764
|
-
return messages.some(
|
|
8765
|
-
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
8766
|
-
);
|
|
8886
|
+
return false;
|
|
8767
8887
|
} catch (err) {
|
|
8768
8888
|
console.warn(`[slack] conversations.replies error:`, err?.message || err);
|
|
8769
8889
|
return false;
|
|
@@ -9215,7 +9335,7 @@ async function reconcileOnce(now = Date.now()) {
|
|
|
9215
9335
|
entry2.updatedAt = Date.now();
|
|
9216
9336
|
const nudged = {
|
|
9217
9337
|
...entry2.event,
|
|
9218
|
-
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no reply,
|
|
9338
|
+
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no substantive reply, post a brief acknowledgement.]
|
|
9219
9339
|
${entry2.event.content}`,
|
|
9220
9340
|
wake: "now"
|
|
9221
9341
|
};
|
|
@@ -9623,22 +9743,444 @@ function getChannel(id) {
|
|
|
9623
9743
|
function listChannels() {
|
|
9624
9744
|
return Array.from(channels.values());
|
|
9625
9745
|
}
|
|
9626
|
-
function listOutboundChannels() {
|
|
9627
|
-
return listChannels().filter((c) => c.canSend());
|
|
9746
|
+
function listOutboundChannels() {
|
|
9747
|
+
return listChannels().filter((c) => c.canSend());
|
|
9748
|
+
}
|
|
9749
|
+
var channels;
|
|
9750
|
+
var init_registry = __esm({
|
|
9751
|
+
"src/integrations/channels/registry.ts"() {
|
|
9752
|
+
"use strict";
|
|
9753
|
+
init_web();
|
|
9754
|
+
init_slack();
|
|
9755
|
+
init_system();
|
|
9756
|
+
init_schedule();
|
|
9757
|
+
init_webhook();
|
|
9758
|
+
channels = /* @__PURE__ */ new Map();
|
|
9759
|
+
for (const c of [webChannel, slackChannel, systemChannel, scheduleChannel, webhookChannel]) {
|
|
9760
|
+
channels.set(c.id, c);
|
|
9761
|
+
}
|
|
9762
|
+
}
|
|
9763
|
+
});
|
|
9764
|
+
|
|
9765
|
+
// src/integrations/slack/files.ts
|
|
9766
|
+
var files_exports = {};
|
|
9767
|
+
__export(files_exports, {
|
|
9768
|
+
INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
|
|
9769
|
+
MAX_BYTES: () => MAX_BYTES,
|
|
9770
|
+
formatFileBlock: () => formatFileBlock,
|
|
9771
|
+
ingestSlackFiles: () => ingestSlackFiles
|
|
9772
|
+
});
|
|
9773
|
+
function inferFileName(file) {
|
|
9774
|
+
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
9775
|
+
}
|
|
9776
|
+
function inferContentType(file) {
|
|
9777
|
+
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
9778
|
+
return "application/octet-stream";
|
|
9779
|
+
}
|
|
9780
|
+
function formatBytes(n) {
|
|
9781
|
+
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
9782
|
+
if (n < 1024) return `${n} B`;
|
|
9783
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
9784
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
9785
|
+
}
|
|
9786
|
+
function sleep(ms) {
|
|
9787
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
9788
|
+
}
|
|
9789
|
+
function imageMagic(bytes) {
|
|
9790
|
+
if (bytes.length < 12) return null;
|
|
9791
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
9792
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
|
|
9793
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
|
|
9794
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
|
|
9795
|
+
return null;
|
|
9796
|
+
}
|
|
9797
|
+
function requiresRasterMagic(contentType) {
|
|
9798
|
+
const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
|
|
9799
|
+
return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
|
|
9800
|
+
}
|
|
9801
|
+
function validateDownloadedBytes(bytes, declaredContentType) {
|
|
9802
|
+
if (!requiresRasterMagic(declaredContentType)) return null;
|
|
9803
|
+
const actual = imageMagic(bytes);
|
|
9804
|
+
if (!actual) return "invalid_image_bytes";
|
|
9805
|
+
return null;
|
|
9806
|
+
}
|
|
9807
|
+
async function fetchSlackPrivateFile(sourceUrl, botToken) {
|
|
9808
|
+
let lastError = "unknown";
|
|
9809
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
9810
|
+
try {
|
|
9811
|
+
const res = await fetch(sourceUrl, {
|
|
9812
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
9813
|
+
});
|
|
9814
|
+
if (res.status === 429 || res.status >= 500) {
|
|
9815
|
+
lastError = `slack_fetch_${res.status}`;
|
|
9816
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
9817
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
|
|
9818
|
+
if (attempt < 2) {
|
|
9819
|
+
await sleep(waitMs);
|
|
9820
|
+
continue;
|
|
9821
|
+
}
|
|
9822
|
+
}
|
|
9823
|
+
if (!res.ok) {
|
|
9824
|
+
return { ok: false, error: `slack_fetch_${res.status}` };
|
|
9825
|
+
}
|
|
9826
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
9827
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
|
|
9828
|
+
return { ok: false, error: "size_exceeded" };
|
|
9829
|
+
}
|
|
9830
|
+
const ab = await res.arrayBuffer();
|
|
9831
|
+
if (ab.byteLength > MAX_BYTES) {
|
|
9832
|
+
return { ok: false, error: "size_exceeded" };
|
|
9833
|
+
}
|
|
9834
|
+
return { ok: true, bytes: Buffer.from(ab) };
|
|
9835
|
+
} catch (err) {
|
|
9836
|
+
lastError = `slack_fetch_error:${err?.message || "unknown"}`;
|
|
9837
|
+
if (attempt < 2) {
|
|
9838
|
+
await sleep(250 * (attempt + 1));
|
|
9839
|
+
continue;
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
}
|
|
9843
|
+
return { ok: false, error: lastError };
|
|
9844
|
+
}
|
|
9845
|
+
function withTimeout(p, ms, label) {
|
|
9846
|
+
return new Promise((resolve13, reject) => {
|
|
9847
|
+
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
9848
|
+
p.then(
|
|
9849
|
+
(v) => {
|
|
9850
|
+
clearTimeout(t);
|
|
9851
|
+
resolve13(v);
|
|
9852
|
+
},
|
|
9853
|
+
(e) => {
|
|
9854
|
+
clearTimeout(t);
|
|
9855
|
+
reject(e);
|
|
9856
|
+
}
|
|
9857
|
+
);
|
|
9858
|
+
});
|
|
9859
|
+
}
|
|
9860
|
+
async function ingestOne(file, sessionId, botToken) {
|
|
9861
|
+
const fileName = inferFileName(file);
|
|
9862
|
+
const contentType = inferContentType(file);
|
|
9863
|
+
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
9864
|
+
const base = {
|
|
9865
|
+
slackFileId: file.id,
|
|
9866
|
+
fileName,
|
|
9867
|
+
contentType,
|
|
9868
|
+
sizeBytes: declaredSize
|
|
9869
|
+
};
|
|
9870
|
+
const sourceUrl = file.url_private_download || file.url_private;
|
|
9871
|
+
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
9872
|
+
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
9873
|
+
}
|
|
9874
|
+
if (declaredSize > MAX_BYTES) {
|
|
9875
|
+
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
9876
|
+
}
|
|
9877
|
+
let bytes;
|
|
9878
|
+
const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
|
|
9879
|
+
if (!fetched.ok) {
|
|
9880
|
+
return { ...base, shortUrl: null, error: fetched.error };
|
|
9881
|
+
}
|
|
9882
|
+
bytes = fetched.bytes;
|
|
9883
|
+
const byteError = validateDownloadedBytes(bytes, contentType);
|
|
9884
|
+
if (byteError) {
|
|
9885
|
+
console.warn(
|
|
9886
|
+
`[slack-files] refusing to upload ${fileName}: Slack metadata says ${contentType}, but downloaded bytes are not a supported image (${bytes.slice(0, 32).toString("utf8").replace(/\s+/g, " ").slice(0, 32)})`
|
|
9887
|
+
);
|
|
9888
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
|
|
9889
|
+
}
|
|
9890
|
+
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
9891
|
+
let upload;
|
|
9892
|
+
try {
|
|
9893
|
+
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
9894
|
+
} catch (err) {
|
|
9895
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
9896
|
+
}
|
|
9897
|
+
try {
|
|
9898
|
+
const putRes = await fetch(upload.uploadUrl, {
|
|
9899
|
+
method: "PUT",
|
|
9900
|
+
headers: { "Content-Type": contentType },
|
|
9901
|
+
body: bytes
|
|
9902
|
+
});
|
|
9903
|
+
if (!putRes.ok) {
|
|
9904
|
+
return {
|
|
9905
|
+
...base,
|
|
9906
|
+
sizeBytes: bytes.length,
|
|
9907
|
+
shortUrl: null,
|
|
9908
|
+
error: `gcs_put_${putRes.status}`
|
|
9909
|
+
};
|
|
9910
|
+
}
|
|
9911
|
+
} catch (err) {
|
|
9912
|
+
return {
|
|
9913
|
+
...base,
|
|
9914
|
+
sizeBytes: bytes.length,
|
|
9915
|
+
shortUrl: null,
|
|
9916
|
+
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
9917
|
+
};
|
|
9918
|
+
}
|
|
9919
|
+
try {
|
|
9920
|
+
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
9921
|
+
} catch (err) {
|
|
9922
|
+
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
9923
|
+
}
|
|
9924
|
+
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
9925
|
+
// server somehow forgot to return it (older remote-server versions).
|
|
9926
|
+
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
9927
|
+
return {
|
|
9928
|
+
...base,
|
|
9929
|
+
sizeBytes: bytes.length,
|
|
9930
|
+
shortUrl
|
|
9931
|
+
};
|
|
9932
|
+
}
|
|
9933
|
+
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
9934
|
+
try {
|
|
9935
|
+
const u = new URL(uploadUrl);
|
|
9936
|
+
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
9937
|
+
return `${u.origin}/f/${fileId}`;
|
|
9938
|
+
} catch {
|
|
9939
|
+
return null;
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9942
|
+
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
9943
|
+
if (!Array.isArray(files) || files.length === 0) return [];
|
|
9944
|
+
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
9945
|
+
if (!isRemoteConfigured2()) {
|
|
9946
|
+
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
9947
|
+
return files.map((f) => ({
|
|
9948
|
+
slackFileId: f.id,
|
|
9949
|
+
fileName: inferFileName(f),
|
|
9950
|
+
contentType: inferContentType(f),
|
|
9951
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9952
|
+
shortUrl: null,
|
|
9953
|
+
error: "storage_unconfigured"
|
|
9954
|
+
}));
|
|
9955
|
+
}
|
|
9956
|
+
const botToken = getSlackBotToken();
|
|
9957
|
+
if (!botToken) {
|
|
9958
|
+
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
9959
|
+
return files.map((f) => ({
|
|
9960
|
+
slackFileId: f.id,
|
|
9961
|
+
fileName: inferFileName(f),
|
|
9962
|
+
contentType: inferContentType(f),
|
|
9963
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9964
|
+
shortUrl: null,
|
|
9965
|
+
error: "no_bot_token"
|
|
9966
|
+
}));
|
|
9967
|
+
}
|
|
9968
|
+
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
9969
|
+
const startedAt = Date.now();
|
|
9970
|
+
const pipeline = Promise.allSettled(
|
|
9971
|
+
files.map(
|
|
9972
|
+
(f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
|
|
9973
|
+
if (String(err?.message || err).includes("_timeout")) {
|
|
9974
|
+
return {
|
|
9975
|
+
slackFileId: f.id,
|
|
9976
|
+
fileName: inferFileName(f),
|
|
9977
|
+
contentType: inferContentType(f),
|
|
9978
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9979
|
+
shortUrl: null,
|
|
9980
|
+
error: "timeout"
|
|
9981
|
+
};
|
|
9982
|
+
}
|
|
9983
|
+
throw err;
|
|
9984
|
+
})
|
|
9985
|
+
)
|
|
9986
|
+
);
|
|
9987
|
+
const settled = await pipeline;
|
|
9988
|
+
const results = settled.map((s, i) => {
|
|
9989
|
+
if (s.status === "fulfilled") return s.value;
|
|
9990
|
+
const f = files[i];
|
|
9991
|
+
return {
|
|
9992
|
+
slackFileId: f.id,
|
|
9993
|
+
fileName: inferFileName(f),
|
|
9994
|
+
contentType: inferContentType(f),
|
|
9995
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
9996
|
+
shortUrl: null,
|
|
9997
|
+
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
9998
|
+
};
|
|
9999
|
+
});
|
|
10000
|
+
const okCount = results.filter((r) => r.shortUrl).length;
|
|
10001
|
+
console.log(
|
|
10002
|
+
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
10003
|
+
);
|
|
10004
|
+
return results;
|
|
10005
|
+
}
|
|
10006
|
+
function formatFileBlock(files) {
|
|
10007
|
+
if (!files || files.length === 0) return "";
|
|
10008
|
+
const lines = ["[files]"];
|
|
10009
|
+
for (const f of files) {
|
|
10010
|
+
const sizeLabel = formatBytes(f.sizeBytes);
|
|
10011
|
+
if (f.shortUrl) {
|
|
10012
|
+
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
10013
|
+
} else {
|
|
10014
|
+
lines.push(
|
|
10015
|
+
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
10016
|
+
);
|
|
10017
|
+
}
|
|
10018
|
+
}
|
|
10019
|
+
return lines.join("\n");
|
|
10020
|
+
}
|
|
10021
|
+
var MAX_BYTES, INGEST_TIMEOUT_MS;
|
|
10022
|
+
var init_files = __esm({
|
|
10023
|
+
"src/integrations/slack/files.ts"() {
|
|
10024
|
+
"use strict";
|
|
10025
|
+
init_client3();
|
|
10026
|
+
MAX_BYTES = 100 * 1024 * 1024;
|
|
10027
|
+
INGEST_TIMEOUT_MS = 2500;
|
|
10028
|
+
}
|
|
10029
|
+
});
|
|
10030
|
+
|
|
10031
|
+
// src/integrations/slack/read.ts
|
|
10032
|
+
var read_exports = {};
|
|
10033
|
+
__export(read_exports, {
|
|
10034
|
+
findChannelByName: () => findChannelByName,
|
|
10035
|
+
findUsers: () => findUsers,
|
|
10036
|
+
getChannelHistory: () => getChannelHistory,
|
|
10037
|
+
getPermalink: () => getPermalink,
|
|
10038
|
+
getThreadReplies: () => getThreadReplies,
|
|
10039
|
+
ingestMessageFiles: () => ingestMessageFiles
|
|
10040
|
+
});
|
|
10041
|
+
async function slackGet(method, params) {
|
|
10042
|
+
const token = getSlackBotToken();
|
|
10043
|
+
if (!token) return { ok: false, error: "slack_not_configured" };
|
|
10044
|
+
try {
|
|
10045
|
+
const qs = new URLSearchParams(params).toString();
|
|
10046
|
+
const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
|
|
10047
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
10048
|
+
});
|
|
10049
|
+
const json = await res.json().catch(() => ({}));
|
|
10050
|
+
if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
|
|
10051
|
+
return { ok: true, json };
|
|
10052
|
+
} catch (err) {
|
|
10053
|
+
return { ok: false, error: err?.message || "unknown" };
|
|
10054
|
+
}
|
|
10055
|
+
}
|
|
10056
|
+
function clampLimit(n, def, max) {
|
|
10057
|
+
const v = typeof n === "number" && Number.isFinite(n) ? n : def;
|
|
10058
|
+
return String(Math.min(Math.max(Math.floor(v), 1), max));
|
|
10059
|
+
}
|
|
10060
|
+
function liteMessage(m) {
|
|
10061
|
+
return {
|
|
10062
|
+
ts: String(m?.ts ?? ""),
|
|
10063
|
+
threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
|
|
10064
|
+
user: typeof m?.user === "string" ? m.user : void 0,
|
|
10065
|
+
botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
|
|
10066
|
+
text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
|
|
10067
|
+
files: Array.isArray(m?.files) ? m.files.map((f) => ({ id: String(f?.id ?? ""), name: f?.name || f?.title, mimetype: f?.mimetype, size: typeof f?.size === "number" ? f.size : void 0 })) : void 0,
|
|
10068
|
+
replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
|
|
10069
|
+
};
|
|
10070
|
+
}
|
|
10071
|
+
async function enrichUserNames(messages) {
|
|
10072
|
+
const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
|
|
10073
|
+
const names = /* @__PURE__ */ new Map();
|
|
10074
|
+
await Promise.all(
|
|
10075
|
+
ids.map(async (id) => {
|
|
10076
|
+
const info = await resolveSlackUserInfo(id).catch(() => null);
|
|
10077
|
+
if (info?.name) names.set(id, info.name);
|
|
10078
|
+
})
|
|
10079
|
+
);
|
|
10080
|
+
return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
|
|
10081
|
+
}
|
|
10082
|
+
async function getChannelHistory(channel, limit) {
|
|
10083
|
+
const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
|
|
10084
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10085
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10086
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10087
|
+
}
|
|
10088
|
+
async function getThreadReplies(channel, threadTs, limit) {
|
|
10089
|
+
const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
|
|
10090
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10091
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10092
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10093
|
+
}
|
|
10094
|
+
async function getPermalink(channel, messageTs) {
|
|
10095
|
+
const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
|
|
10096
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10097
|
+
return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
|
|
10098
|
+
}
|
|
10099
|
+
async function findChannelByName(name) {
|
|
10100
|
+
const clean = name.replace(/^#/, "").trim().toLowerCase();
|
|
10101
|
+
let cursor;
|
|
10102
|
+
for (let page = 0; page < 10; page++) {
|
|
10103
|
+
const params = {
|
|
10104
|
+
types: "public_channel,private_channel",
|
|
10105
|
+
exclude_archived: "true",
|
|
10106
|
+
limit: "999"
|
|
10107
|
+
};
|
|
10108
|
+
if (cursor) params.cursor = cursor;
|
|
10109
|
+
const r = await slackGet("conversations.list", params);
|
|
10110
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10111
|
+
const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
|
|
10112
|
+
if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
|
|
10113
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10114
|
+
if (!cursor) break;
|
|
10115
|
+
}
|
|
10116
|
+
return { ok: false, error: "channel_not_found" };
|
|
10117
|
+
}
|
|
10118
|
+
async function findUsers(query, max = 10) {
|
|
10119
|
+
const q = query.trim().toLowerCase();
|
|
10120
|
+
if (!q) return { ok: false, error: "empty_query" };
|
|
10121
|
+
if (q.includes("@")) {
|
|
10122
|
+
const r = await slackGet("users.lookupByEmail", { email: query.trim() });
|
|
10123
|
+
if (r.ok && r.json.user) {
|
|
10124
|
+
const u = r.json.user;
|
|
10125
|
+
return { ok: true, data: [{ id: String(u.id), name: u.profile?.display_name || u.real_name || u.name, realName: u.real_name, email: u.profile?.email }] };
|
|
10126
|
+
}
|
|
10127
|
+
if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
|
|
10128
|
+
return { ok: false, error: r.error };
|
|
10129
|
+
}
|
|
10130
|
+
}
|
|
10131
|
+
const matches = [];
|
|
10132
|
+
let cursor;
|
|
10133
|
+
for (let page = 0; page < 10 && matches.length < max; page++) {
|
|
10134
|
+
const params = { limit: "1000" };
|
|
10135
|
+
if (cursor) params.cursor = cursor;
|
|
10136
|
+
const r = await slackGet("users.list", params);
|
|
10137
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10138
|
+
for (const u of r.json.members || []) {
|
|
10139
|
+
if (u?.deleted || u?.is_bot) continue;
|
|
10140
|
+
const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
|
|
10141
|
+
const email = String(u?.profile?.email || "");
|
|
10142
|
+
if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
|
|
10143
|
+
matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
|
|
10144
|
+
if (matches.length >= max) break;
|
|
10145
|
+
}
|
|
10146
|
+
}
|
|
10147
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10148
|
+
if (!cursor) break;
|
|
10149
|
+
}
|
|
10150
|
+
return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
|
|
10151
|
+
}
|
|
10152
|
+
async function getSingleMessage(channel, ts, threadTs) {
|
|
10153
|
+
if (threadTs) {
|
|
10154
|
+
const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
|
|
10155
|
+
if (rr2.ok && Array.isArray(rr2.json.messages)) {
|
|
10156
|
+
const match = rr2.json.messages.find((m) => m?.ts === ts);
|
|
10157
|
+
if (match) return match;
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
|
|
10161
|
+
if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
|
|
10162
|
+
const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
|
|
10163
|
+
if (rr.ok && Array.isArray(rr.json.messages)) {
|
|
10164
|
+
return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
|
|
10165
|
+
}
|
|
10166
|
+
return null;
|
|
10167
|
+
}
|
|
10168
|
+
async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
|
|
10169
|
+
const msg = await getSingleMessage(channel, ts, threadTs);
|
|
10170
|
+
if (!msg) return { ok: false, error: "message_not_found" };
|
|
10171
|
+
const files = Array.isArray(msg.files) ? msg.files : [];
|
|
10172
|
+
if (files.length === 0) return { ok: true, data: [] };
|
|
10173
|
+
const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
|
|
10174
|
+
const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
|
|
10175
|
+
return {
|
|
10176
|
+
ok: true,
|
|
10177
|
+
data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
|
|
10178
|
+
};
|
|
9628
10179
|
}
|
|
9629
|
-
var
|
|
9630
|
-
|
|
9631
|
-
"src/integrations/channels/registry.ts"() {
|
|
10180
|
+
var init_read = __esm({
|
|
10181
|
+
"src/integrations/slack/read.ts"() {
|
|
9632
10182
|
"use strict";
|
|
9633
|
-
|
|
9634
|
-
init_slack();
|
|
9635
|
-
init_system();
|
|
9636
|
-
init_schedule();
|
|
9637
|
-
init_webhook();
|
|
9638
|
-
channels = /* @__PURE__ */ new Map();
|
|
9639
|
-
for (const c of [webChannel, slackChannel, systemChannel, scheduleChannel, webhookChannel]) {
|
|
9640
|
-
channels.set(c.id, c);
|
|
9641
|
-
}
|
|
10183
|
+
init_client3();
|
|
9642
10184
|
}
|
|
9643
10185
|
});
|
|
9644
10186
|
|
|
@@ -9652,7 +10194,16 @@ async function postMessage(opts) {
|
|
|
9652
10194
|
let ref;
|
|
9653
10195
|
switch (opts.channel) {
|
|
9654
10196
|
case "slack": {
|
|
9655
|
-
|
|
10197
|
+
let slackChannel2 = opts.to.trim();
|
|
10198
|
+
if (slackChannel2.startsWith("#")) {
|
|
10199
|
+
const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
|
|
10200
|
+
const found = await findChannelByName2(slackChannel2);
|
|
10201
|
+
if (!found.ok || !found.data?.id) {
|
|
10202
|
+
return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
|
|
10203
|
+
}
|
|
10204
|
+
slackChannel2 = found.data.id;
|
|
10205
|
+
}
|
|
10206
|
+
const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
|
|
9656
10207
|
ref = slackRef;
|
|
9657
10208
|
break;
|
|
9658
10209
|
}
|
|
@@ -9959,6 +10510,46 @@ function buildMessengerTool() {
|
|
|
9959
10510
|
}
|
|
9960
10511
|
});
|
|
9961
10512
|
}
|
|
10513
|
+
function buildSlackTool(opts) {
|
|
10514
|
+
return tool13({
|
|
10515
|
+
description: "Read from Slack like a human would. Actions: history (channel[, limit] \u2192 recent messages, newest first, with sender names + file metadata), replies (channel, threadTs[, limit] \u2192 full thread), permalink (channel, messageTs \u2192 shareable link), find_channel (query=name \u2192 channel id), find_user (query=name|email \u2192 matching members), user_info (user=U0123 \u2192 name/email), fetch_files (channel, messageTs[, threadTs] \u2192 re-uploads that message's attachments and returns fetchable URLs you can curl). Use this to scroll back, read context, and open files others posted before replying.",
|
|
10516
|
+
inputSchema: slackInputSchema,
|
|
10517
|
+
execute: async (input) => {
|
|
10518
|
+
if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
|
|
10519
|
+
switch (input.action) {
|
|
10520
|
+
case "history": {
|
|
10521
|
+
if (!input.channel) return { ok: false, error: "channel required" };
|
|
10522
|
+
return getChannelHistory(input.channel, input.limit);
|
|
10523
|
+
}
|
|
10524
|
+
case "replies": {
|
|
10525
|
+
if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
|
|
10526
|
+
return getThreadReplies(input.channel, input.threadTs, input.limit);
|
|
10527
|
+
}
|
|
10528
|
+
case "permalink": {
|
|
10529
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
10530
|
+
return getPermalink(input.channel, input.messageTs);
|
|
10531
|
+
}
|
|
10532
|
+
case "find_channel": {
|
|
10533
|
+
if (!input.query) return { ok: false, error: "query (channel name) required" };
|
|
10534
|
+
return findChannelByName(input.query);
|
|
10535
|
+
}
|
|
10536
|
+
case "find_user": {
|
|
10537
|
+
if (!input.query) return { ok: false, error: "query (name or email) required" };
|
|
10538
|
+
return findUsers(input.query);
|
|
10539
|
+
}
|
|
10540
|
+
case "user_info": {
|
|
10541
|
+
if (!input.user) return { ok: false, error: "user required" };
|
|
10542
|
+
const info = await resolveSlackUserInfo(input.user);
|
|
10543
|
+
return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
|
|
10544
|
+
}
|
|
10545
|
+
case "fetch_files": {
|
|
10546
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
10547
|
+
return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
|
|
10548
|
+
}
|
|
10549
|
+
}
|
|
10550
|
+
}
|
|
10551
|
+
});
|
|
10552
|
+
}
|
|
9962
10553
|
function buildScheduleTool(opts) {
|
|
9963
10554
|
return tool13({
|
|
9964
10555
|
description: "Recurring prompts. Actions: create (required: name, cron, prompt), list, update (required: id; any of name/cron/prompt/enabled/replyChannel), delete (required: id), pause (required: id), resume (required: id). Cron is standard 5-field syntax.",
|
|
@@ -10040,15 +10631,18 @@ function createOrchestratorActionTools(opts) {
|
|
|
10040
10631
|
return {
|
|
10041
10632
|
agent: buildAgentTool(opts),
|
|
10042
10633
|
messenger: buildMessengerTool(),
|
|
10634
|
+
slack: buildSlackTool(opts),
|
|
10043
10635
|
schedule: buildScheduleTool(opts),
|
|
10044
10636
|
webhook: buildWebhookTool(opts)
|
|
10045
10637
|
};
|
|
10046
10638
|
}
|
|
10047
|
-
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10639
|
+
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, slackInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10048
10640
|
var init_orchestrator_actions = __esm({
|
|
10049
10641
|
"src/tools/orchestrator-actions.ts"() {
|
|
10050
10642
|
"use strict";
|
|
10051
10643
|
init_messenger();
|
|
10644
|
+
init_read();
|
|
10645
|
+
init_client3();
|
|
10052
10646
|
init_schedules_store();
|
|
10053
10647
|
init_config();
|
|
10054
10648
|
init_webhooks_store();
|
|
@@ -10106,6 +10700,15 @@ var init_orchestrator_actions = __esm({
|
|
|
10106
10700
|
threadTs: z14.string().optional().describe("post + slack: reply in this thread."),
|
|
10107
10701
|
subject: z14.string().optional().describe("post + email: subject (future).")
|
|
10108
10702
|
});
|
|
10703
|
+
slackInputSchema = z14.object({
|
|
10704
|
+
action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
|
|
10705
|
+
channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
|
|
10706
|
+
threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
|
|
10707
|
+
messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
|
|
10708
|
+
query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
|
|
10709
|
+
user: z14.string().optional().describe("user_info: user id (U0123)."),
|
|
10710
|
+
limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
|
|
10711
|
+
});
|
|
10109
10712
|
scheduleInputSchema = z14.object({
|
|
10110
10713
|
action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
|
|
10111
10714
|
// create / update
|
|
@@ -15746,226 +16349,69 @@ function verifySlackSignature(opts) {
|
|
|
15746
16349
|
// src/server/routes/slack.ts
|
|
15747
16350
|
init_client3();
|
|
15748
16351
|
init_slack();
|
|
15749
|
-
|
|
15750
|
-
// src/integrations/slack/files.ts
|
|
15751
|
-
init_client3();
|
|
15752
|
-
var MAX_BYTES = 100 * 1024 * 1024;
|
|
15753
|
-
var INGEST_TIMEOUT_MS = 2500;
|
|
15754
|
-
function inferFileName(file) {
|
|
15755
|
-
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
15756
|
-
}
|
|
15757
|
-
function inferContentType(file) {
|
|
15758
|
-
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
15759
|
-
return "application/octet-stream";
|
|
15760
|
-
}
|
|
15761
|
-
function formatBytes(n) {
|
|
15762
|
-
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
15763
|
-
if (n < 1024) return `${n} B`;
|
|
15764
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
15765
|
-
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
15766
|
-
}
|
|
15767
|
-
function withTimeout(p, ms, label) {
|
|
15768
|
-
return new Promise((resolve13, reject) => {
|
|
15769
|
-
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
15770
|
-
p.then(
|
|
15771
|
-
(v) => {
|
|
15772
|
-
clearTimeout(t);
|
|
15773
|
-
resolve13(v);
|
|
15774
|
-
},
|
|
15775
|
-
(e) => {
|
|
15776
|
-
clearTimeout(t);
|
|
15777
|
-
reject(e);
|
|
15778
|
-
}
|
|
15779
|
-
);
|
|
15780
|
-
});
|
|
15781
|
-
}
|
|
15782
|
-
async function ingestOne(file, sessionId, botToken) {
|
|
15783
|
-
const fileName = inferFileName(file);
|
|
15784
|
-
const contentType = inferContentType(file);
|
|
15785
|
-
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
15786
|
-
const base = {
|
|
15787
|
-
slackFileId: file.id,
|
|
15788
|
-
fileName,
|
|
15789
|
-
contentType,
|
|
15790
|
-
sizeBytes: declaredSize
|
|
15791
|
-
};
|
|
15792
|
-
const sourceUrl = file.url_private_download || file.url_private;
|
|
15793
|
-
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
15794
|
-
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
15795
|
-
}
|
|
15796
|
-
if (declaredSize > MAX_BYTES) {
|
|
15797
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15798
|
-
}
|
|
15799
|
-
let bytes;
|
|
15800
|
-
try {
|
|
15801
|
-
const res = await fetch(sourceUrl, {
|
|
15802
|
-
headers: { Authorization: `Bearer ${botToken}` }
|
|
15803
|
-
});
|
|
15804
|
-
if (!res.ok) {
|
|
15805
|
-
return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
|
|
15806
|
-
}
|
|
15807
|
-
const ab = await res.arrayBuffer();
|
|
15808
|
-
if (ab.byteLength > MAX_BYTES) {
|
|
15809
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
15810
|
-
}
|
|
15811
|
-
bytes = Buffer.from(ab);
|
|
15812
|
-
} catch (err) {
|
|
15813
|
-
return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
|
|
15814
|
-
}
|
|
15815
|
-
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15816
|
-
let upload;
|
|
15817
|
-
try {
|
|
15818
|
-
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
15819
|
-
} catch (err) {
|
|
15820
|
-
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
15821
|
-
}
|
|
15822
|
-
try {
|
|
15823
|
-
const putRes = await fetch(upload.uploadUrl, {
|
|
15824
|
-
method: "PUT",
|
|
15825
|
-
headers: { "Content-Type": contentType },
|
|
15826
|
-
body: bytes
|
|
15827
|
-
});
|
|
15828
|
-
if (!putRes.ok) {
|
|
15829
|
-
return {
|
|
15830
|
-
...base,
|
|
15831
|
-
sizeBytes: bytes.length,
|
|
15832
|
-
shortUrl: null,
|
|
15833
|
-
error: `gcs_put_${putRes.status}`
|
|
15834
|
-
};
|
|
15835
|
-
}
|
|
15836
|
-
} catch (err) {
|
|
15837
|
-
return {
|
|
15838
|
-
...base,
|
|
15839
|
-
sizeBytes: bytes.length,
|
|
15840
|
-
shortUrl: null,
|
|
15841
|
-
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
15842
|
-
};
|
|
15843
|
-
}
|
|
15844
|
-
try {
|
|
15845
|
-
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
15846
|
-
} catch (err) {
|
|
15847
|
-
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
15848
|
-
}
|
|
15849
|
-
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
15850
|
-
// server somehow forgot to return it (older remote-server versions).
|
|
15851
|
-
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
15852
|
-
return {
|
|
15853
|
-
...base,
|
|
15854
|
-
sizeBytes: bytes.length,
|
|
15855
|
-
shortUrl
|
|
15856
|
-
};
|
|
15857
|
-
}
|
|
15858
|
-
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
15859
|
-
try {
|
|
15860
|
-
const u = new URL(uploadUrl);
|
|
15861
|
-
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
15862
|
-
return `${u.origin}/f/${fileId}`;
|
|
15863
|
-
} catch {
|
|
15864
|
-
return null;
|
|
15865
|
-
}
|
|
15866
|
-
}
|
|
15867
|
-
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
15868
|
-
if (!Array.isArray(files) || files.length === 0) return [];
|
|
15869
|
-
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
15870
|
-
if (!isRemoteConfigured2()) {
|
|
15871
|
-
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
15872
|
-
return files.map((f) => ({
|
|
15873
|
-
slackFileId: f.id,
|
|
15874
|
-
fileName: inferFileName(f),
|
|
15875
|
-
contentType: inferContentType(f),
|
|
15876
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15877
|
-
shortUrl: null,
|
|
15878
|
-
error: "storage_unconfigured"
|
|
15879
|
-
}));
|
|
15880
|
-
}
|
|
15881
|
-
const botToken = getSlackBotToken();
|
|
15882
|
-
if (!botToken) {
|
|
15883
|
-
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
15884
|
-
return files.map((f) => ({
|
|
15885
|
-
slackFileId: f.id,
|
|
15886
|
-
fileName: inferFileName(f),
|
|
15887
|
-
contentType: inferContentType(f),
|
|
15888
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15889
|
-
shortUrl: null,
|
|
15890
|
-
error: "no_bot_token"
|
|
15891
|
-
}));
|
|
15892
|
-
}
|
|
15893
|
-
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
15894
|
-
const startedAt = Date.now();
|
|
15895
|
-
const pipeline = Promise.allSettled(
|
|
15896
|
-
files.map((f) => ingestOne(f, sessionId, botToken))
|
|
15897
|
-
);
|
|
15898
|
-
let settled;
|
|
15899
|
-
try {
|
|
15900
|
-
settled = await withTimeout(pipeline, timeoutMs, "ingest");
|
|
15901
|
-
} catch (err) {
|
|
15902
|
-
console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
|
|
15903
|
-
return files.map((f) => ({
|
|
15904
|
-
slackFileId: f.id,
|
|
15905
|
-
fileName: inferFileName(f),
|
|
15906
|
-
contentType: inferContentType(f),
|
|
15907
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15908
|
-
shortUrl: null,
|
|
15909
|
-
error: "timeout"
|
|
15910
|
-
}));
|
|
15911
|
-
}
|
|
15912
|
-
const results = settled.map((s, i) => {
|
|
15913
|
-
if (s.status === "fulfilled") return s.value;
|
|
15914
|
-
const f = files[i];
|
|
15915
|
-
return {
|
|
15916
|
-
slackFileId: f.id,
|
|
15917
|
-
fileName: inferFileName(f),
|
|
15918
|
-
contentType: inferContentType(f),
|
|
15919
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
15920
|
-
shortUrl: null,
|
|
15921
|
-
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
15922
|
-
};
|
|
15923
|
-
});
|
|
15924
|
-
const okCount = results.filter((r) => r.shortUrl).length;
|
|
15925
|
-
console.log(
|
|
15926
|
-
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
15927
|
-
);
|
|
15928
|
-
return results;
|
|
15929
|
-
}
|
|
15930
|
-
function formatFileBlock(files) {
|
|
15931
|
-
if (!files || files.length === 0) return "";
|
|
15932
|
-
const lines = ["[files]"];
|
|
15933
|
-
for (const f of files) {
|
|
15934
|
-
const sizeLabel = formatBytes(f.sizeBytes);
|
|
15935
|
-
if (f.shortUrl) {
|
|
15936
|
-
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
15937
|
-
} else {
|
|
15938
|
-
lines.push(
|
|
15939
|
-
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
15940
|
-
);
|
|
15941
|
-
}
|
|
15942
|
-
}
|
|
15943
|
-
return lines.join("\n");
|
|
15944
|
-
}
|
|
15945
|
-
|
|
15946
|
-
// src/server/routes/slack.ts
|
|
16352
|
+
init_files();
|
|
15947
16353
|
init_webhook_events();
|
|
15948
16354
|
init_inbox();
|
|
15949
16355
|
var recentlyHandled = /* @__PURE__ */ new Map();
|
|
16356
|
+
var recentlyDeniedReplies = /* @__PURE__ */ new Map();
|
|
16357
|
+
var inFlight = /* @__PURE__ */ new Set();
|
|
15950
16358
|
var MAX_RECENT = 1e3;
|
|
16359
|
+
function deliveryKey(channel, ts) {
|
|
16360
|
+
if (!channel || !ts) return null;
|
|
16361
|
+
return `${channel}\u241F${ts}`;
|
|
16362
|
+
}
|
|
15951
16363
|
function wasHandled(channel, ts) {
|
|
15952
|
-
|
|
15953
|
-
|
|
16364
|
+
const key2 = deliveryKey(channel, ts);
|
|
16365
|
+
if (!key2) return false;
|
|
16366
|
+
return recentlyHandled.has(key2) || inFlight.has(key2);
|
|
16367
|
+
}
|
|
16368
|
+
function markInFlight(channel, ts) {
|
|
16369
|
+
const key2 = deliveryKey(channel, ts);
|
|
16370
|
+
if (key2) inFlight.add(key2);
|
|
16371
|
+
}
|
|
16372
|
+
function clearInFlight(channel, ts) {
|
|
16373
|
+
const key2 = deliveryKey(channel, ts);
|
|
16374
|
+
if (key2) inFlight.delete(key2);
|
|
15954
16375
|
}
|
|
15955
16376
|
function markHandled(channel, ts) {
|
|
15956
|
-
|
|
15957
|
-
|
|
16377
|
+
const key2 = deliveryKey(channel, ts);
|
|
16378
|
+
if (!key2) return;
|
|
16379
|
+
inFlight.delete(key2);
|
|
15958
16380
|
recentlyHandled.set(key2, Date.now());
|
|
15959
16381
|
if (recentlyHandled.size > MAX_RECENT) {
|
|
15960
16382
|
const oldest = recentlyHandled.keys().next().value;
|
|
15961
16383
|
if (oldest) recentlyHandled.delete(oldest);
|
|
15962
16384
|
}
|
|
15963
16385
|
}
|
|
16386
|
+
function wasDeniedReplySent(channel, ts) {
|
|
16387
|
+
const key2 = deliveryKey(channel, ts);
|
|
16388
|
+
if (!key2) return false;
|
|
16389
|
+
return recentlyDeniedReplies.has(key2);
|
|
16390
|
+
}
|
|
16391
|
+
function markDeniedReplySent(channel, ts) {
|
|
16392
|
+
const key2 = deliveryKey(channel, ts);
|
|
16393
|
+
if (!key2) return;
|
|
16394
|
+
recentlyDeniedReplies.set(key2, Date.now());
|
|
16395
|
+
if (recentlyDeniedReplies.size > MAX_RECENT) {
|
|
16396
|
+
const oldest = recentlyDeniedReplies.keys().next().value;
|
|
16397
|
+
if (oldest) recentlyDeniedReplies.delete(oldest);
|
|
16398
|
+
}
|
|
16399
|
+
}
|
|
15964
16400
|
var slack = new Hono6();
|
|
15965
16401
|
slack.post("/events", async (c) => {
|
|
15966
|
-
const signingSecret = getSlackSigningSecret();
|
|
15967
|
-
if (!signingSecret) return c.json({ error: "slack not configured" }, 503);
|
|
15968
16402
|
const rawBody = await c.req.text();
|
|
16403
|
+
const signingSecret = getSlackSigningSecret();
|
|
16404
|
+
if (!signingSecret) {
|
|
16405
|
+
let unsignedPayload;
|
|
16406
|
+
try {
|
|
16407
|
+
unsignedPayload = JSON.parse(rawBody);
|
|
16408
|
+
} catch {
|
|
16409
|
+
}
|
|
16410
|
+
if (unsignedPayload?.type === "url_verification" && typeof unsignedPayload.challenge === "string") {
|
|
16411
|
+
return c.json({ challenge: unsignedPayload.challenge });
|
|
16412
|
+
}
|
|
16413
|
+
return c.json({ error: "slack not configured" }, 503);
|
|
16414
|
+
}
|
|
15969
16415
|
const verification = verifySlackSignature({
|
|
15970
16416
|
signingSecret,
|
|
15971
16417
|
timestampHeader: c.req.header("x-slack-request-timestamp") || null,
|
|
@@ -16019,65 +16465,79 @@ slack.post("/events", async (c) => {
|
|
|
16019
16465
|
if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
|
|
16020
16466
|
markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
|
|
16021
16467
|
}
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
|
|
16468
|
+
if (wasHandled(ev.channel, ev.ts)) {
|
|
16469
|
+
updateEvent(auditId, { status: "dropped", dropReason: "duplicate_delivery" });
|
|
16470
|
+
return c.json({ ok: true });
|
|
16471
|
+
}
|
|
16472
|
+
markInFlight(ev.channel, ev.ts);
|
|
16473
|
+
let routed = false;
|
|
16474
|
+
try {
|
|
16475
|
+
const orchestratorId = await findOrCreateOrchestratorId();
|
|
16476
|
+
if (orchestratorId) {
|
|
16477
|
+
inbound.content = await normalizeSlackMentions(inbound.content);
|
|
16478
|
+
if (ev.user) {
|
|
16479
|
+
const info = await resolveSlackUserInfo(ev.user);
|
|
16480
|
+
if (info?.name) {
|
|
16481
|
+
const emailSuffix = info.email ? ` (${info.email})` : "";
|
|
16482
|
+
const enriched = `user=${info.name} <@${ev.user}>${emailSuffix}`;
|
|
16483
|
+
inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
|
|
16484
|
+
}
|
|
16031
16485
|
}
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
|
|
16035
|
-
|
|
16036
|
-
|
|
16037
|
-
|
|
16038
|
-
|
|
16039
|
-
|
|
16040
|
-
|
|
16041
|
-
|
|
16042
|
-
const block = formatFileBlock(ingested);
|
|
16043
|
-
if (block) inbound.content = `${inbound.content}
|
|
16486
|
+
const slackFiles = Array.isArray(ev.files) ? ev.files : [];
|
|
16487
|
+
if (ev.channel && ev.ts) {
|
|
16488
|
+
void addLoadingReaction(String(ev.channel), String(ev.ts));
|
|
16489
|
+
}
|
|
16490
|
+
let ingestedCount = 0;
|
|
16491
|
+
if (slackFiles.length > 0) {
|
|
16492
|
+
try {
|
|
16493
|
+
const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
|
|
16494
|
+
const block = formatFileBlock(ingested);
|
|
16495
|
+
if (block) inbound.content = `${inbound.content}
|
|
16044
16496
|
${block}`;
|
|
16045
|
-
|
|
16046
|
-
|
|
16047
|
-
|
|
16048
|
-
|
|
16497
|
+
ingestedCount = ingested.filter((f) => f.shortUrl).length;
|
|
16498
|
+
} catch (err) {
|
|
16499
|
+
console.warn("[slack-files] ingestion threw:", err?.message || err);
|
|
16500
|
+
inbound.content = `${inbound.content}
|
|
16049
16501
|
[files] (ingestion failed: ${err?.message || "unknown"})`;
|
|
16502
|
+
}
|
|
16050
16503
|
}
|
|
16504
|
+
pushToInbox(orchestratorId, inbound);
|
|
16505
|
+
routed = true;
|
|
16506
|
+
markHandled(ev.channel, ev.ts);
|
|
16507
|
+
updateEvent(auditId, {
|
|
16508
|
+
status: "routed",
|
|
16509
|
+
sessionId: orchestratorId,
|
|
16510
|
+
...slackFiles.length > 0 ? {
|
|
16511
|
+
// Preserve the original meta (ts, thread_ts, team,
|
|
16512
|
+
// event_subtype) from recordEvent above — updateEvent does a
|
|
16513
|
+
// shallow merge, so we have to re-include them.
|
|
16514
|
+
meta: {
|
|
16515
|
+
ts: ev.ts,
|
|
16516
|
+
thread_ts: ev.thread_ts,
|
|
16517
|
+
team: ev.team,
|
|
16518
|
+
event_subtype: ev.subtype,
|
|
16519
|
+
fileCount: slackFiles.length,
|
|
16520
|
+
ingestedCount
|
|
16521
|
+
}
|
|
16522
|
+
} : {}
|
|
16523
|
+
});
|
|
16524
|
+
} else {
|
|
16525
|
+
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
16051
16526
|
}
|
|
16052
|
-
|
|
16053
|
-
|
|
16054
|
-
status: "routed",
|
|
16055
|
-
sessionId: orchestratorId,
|
|
16056
|
-
...slackFiles.length > 0 ? {
|
|
16057
|
-
// Preserve the original meta (ts, thread_ts, team,
|
|
16058
|
-
// event_subtype) from recordEvent above — updateEvent does a
|
|
16059
|
-
// shallow merge, so we have to re-include them.
|
|
16060
|
-
meta: {
|
|
16061
|
-
ts: ev.ts,
|
|
16062
|
-
thread_ts: ev.thread_ts,
|
|
16063
|
-
team: ev.team,
|
|
16064
|
-
event_subtype: ev.subtype,
|
|
16065
|
-
fileCount: slackFiles.length,
|
|
16066
|
-
ingestedCount
|
|
16067
|
-
}
|
|
16068
|
-
} : {}
|
|
16069
|
-
});
|
|
16070
|
-
} else {
|
|
16071
|
-
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
16527
|
+
} finally {
|
|
16528
|
+
if (!routed) clearInFlight(ev.channel, ev.ts);
|
|
16072
16529
|
}
|
|
16073
16530
|
} else if (dropReason) {
|
|
16074
16531
|
updateEvent(auditId, { status: "dropped", dropReason });
|
|
16075
16532
|
const userFacingDrops = ["user_not_allowed", "channel_not_allowed", "dm_blocked"];
|
|
16076
16533
|
if (userFacingDrops.includes(dropReason)) {
|
|
16077
16534
|
console.log(`[slack] dropped event from user=${payload.event.user} channel=${payload.event.channel}: ${dropReason}`);
|
|
16078
|
-
|
|
16079
|
-
|
|
16080
|
-
|
|
16535
|
+
if (!wasDeniedReplySent(payload.event.channel, payload.event.ts)) {
|
|
16536
|
+
markDeniedReplySent(payload.event.channel, payload.event.ts);
|
|
16537
|
+
void sendDeniedReply(payload.event, dropReason).catch((err) => {
|
|
16538
|
+
console.warn(`[slack] denied-reply failed:`, err?.message || err);
|
|
16539
|
+
});
|
|
16540
|
+
}
|
|
16081
16541
|
}
|
|
16082
16542
|
}
|
|
16083
16543
|
}
|
|
@@ -16102,7 +16562,7 @@ async function findOrCreateOrchestratorId() {
|
|
|
16102
16562
|
return null;
|
|
16103
16563
|
}
|
|
16104
16564
|
}
|
|
16105
|
-
async function sendDeniedReply(event) {
|
|
16565
|
+
async function sendDeniedReply(event, reason) {
|
|
16106
16566
|
const policy = getSlackDeniedReplyPolicy();
|
|
16107
16567
|
if (!policy.enabled) return;
|
|
16108
16568
|
const adapter = getSlackAdapter();
|
|
@@ -16111,7 +16571,7 @@ async function sendDeniedReply(event) {
|
|
|
16111
16571
|
const channel = String(event?.channel || "");
|
|
16112
16572
|
if (!channel) return;
|
|
16113
16573
|
const threadTs = event?.thread_ts || event?.ts;
|
|
16114
|
-
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`);
|
|
16574
|
+
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`).replace(/\{reason\}/g, reason);
|
|
16115
16575
|
const result = await adapter.postMessage({ channel, text, threadTs });
|
|
16116
16576
|
if (!result.ok) {
|
|
16117
16577
|
console.warn(`[slack] denied-reply post failed: ${result.error}`);
|