@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.11
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/README.md +168 -371
- package/dist/adapter.d.ts +36 -12
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +12 -7
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +358 -135
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +100 -36
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +71 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +168 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +30 -24
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +613 -224
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +22 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +97 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +127 -72
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +3 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +16 -0
- package/dist/adapters/slack/session.js.map +1 -0
- package/dist/adapters/slack/tools/attach.d.ts +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +193 -147
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +58 -111
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +9 -13
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +601 -567
- package/dist/agent.js.map +1 -1
- package/dist/commands/auto-reply.d.ts +16 -0
- package/dist/commands/auto-reply.d.ts.map +1 -0
- package/dist/commands/auto-reply.js +69 -0
- package/dist/commands/auto-reply.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +76 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/model.d.ts +14 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +112 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +28 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/registry.d.ts +7 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +14 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/sandbox.d.ts +10 -0
- package/dist/commands/sandbox.d.ts.map +1 -0
- package/dist/commands/sandbox.js +88 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/session-view.d.ts +5 -0
- package/dist/commands/session-view.d.ts.map +1 -0
- package/dist/commands/session-view.js +62 -0
- package/dist/commands/session-view.js.map +1 -0
- package/dist/commands/types.d.ts +41 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +14 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/config.d.ts +49 -30
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +313 -75
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +10 -42
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +14 -127
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +13 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +118 -64
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +9 -5
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +82 -18
- package/dist/execution-resolver.js.map +1 -1
- package/dist/file-guards.d.ts +6 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +48 -0
- package/dist/file-guards.js.map +1 -0
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +4 -11
- package/dist/instrument.js.map +1 -1
- package/dist/log.d.ts +1 -5
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -38
- package/dist/log.js.map +1 -1
- package/dist/{login.d.ts → login/index.d.ts} +16 -4
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +55 -17
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +7 -4
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1453 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/{link-token.d.ts → login/session.d.ts} +4 -3
- package/dist/login/session.d.ts.map +1 -0
- package/dist/{link-token.js → login/session.js} +1 -1
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +151 -373
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +42 -52
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +256 -111
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-orchestrator.d.ts +42 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +150 -0
- package/dist/runtime/conversation-orchestrator.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +27 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +211 -0
- package/dist/runtime/session-runtime.js.map +1 -0
- package/dist/sandbox/cloudflare.d.ts +15 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -0
- package/dist/sandbox/cloudflare.js +137 -0
- package/dist/sandbox/cloudflare.js.map +1 -0
- package/dist/sandbox/container.d.ts +2 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +5 -1
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/firecracker.d.ts +2 -1
- package/dist/sandbox/firecracker.d.ts.map +1 -1
- package/dist/sandbox/firecracker.js +6 -0
- package/dist/sandbox/firecracker.js.map +1 -1
- package/dist/sandbox/host.d.ts +2 -3
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +5 -5
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sandbox/index.d.ts +6 -4
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +9 -6
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/path-context.d.ts +4 -0
- package/dist/sandbox/path-context.d.ts.map +1 -0
- package/dist/sandbox/path-context.js +20 -0
- package/dist/sandbox/path-context.js.map +1 -0
- package/dist/sandbox/types.d.ts +17 -1
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +4 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-policy.d.ts +13 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +23 -0
- package/dist/session-policy.js.map +1 -0
- package/dist/session-store.d.ts +34 -3
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +184 -22
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/command.d.ts +5 -0
- package/dist/session-view/command.d.ts.map +1 -0
- package/dist/session-view/command.js +11 -0
- package/dist/session-view/command.js.map +1 -0
- package/dist/session-view/portal.d.ts +16 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +1742 -0
- package/dist/session-view/portal.js.map +1 -0
- package/dist/session-view/service.d.ts +34 -0
- package/dist/session-view/service.d.ts.map +1 -0
- package/dist/session-view/service.js +427 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +18 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +39 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/store.d.ts +3 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +22 -48
- package/dist/store.js.map +1 -1
- package/dist/tool-diagnostics.d.ts +2 -0
- package/dist/tool-diagnostics.d.ts.map +1 -0
- package/dist/tool-diagnostics.js +7 -0
- package/dist/tool-diagnostics.js.map +1 -0
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts +43 -2
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +48 -13
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/trigger.d.ts +31 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +98 -0
- package/dist/trigger.js.map +1 -0
- package/dist/ui-copy.d.ts +1 -0
- package/dist/ui-copy.d.ts.map +1 -1
- package/dist/ui-copy.js +3 -0
- package/dist/ui-copy.js.map +1 -1
- package/dist/vault-routing.d.ts +1 -7
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +6 -48
- package/dist/vault-routing.js.map +1 -1
- package/dist/vault.d.ts +21 -55
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +144 -263
- package/dist/vault.js.map +1 -1
- package/package.json +12 -10
- package/dist/bindings.d.ts +0 -63
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js +0 -94
- package/dist/bindings.js.map +0 -1
- package/dist/link-server.d.ts.map +0 -1
- package/dist/link-server.js +0 -839
- package/dist/link-server.js.map +0 -1
- package/dist/link-token.d.ts.map +0 -1
- package/dist/link-token.js.map +0 -1
- package/dist/login.d.ts.map +0 -1
- package/dist/login.js.map +0 -1
- package/dist/vault.test.d.ts +0 -2
- package/dist/vault.test.d.ts.map +0 -1
- package/dist/vault.test.js +0 -67
- package/dist/vault.test.js.map +0 -1
|
@@ -1,78 +1,56 @@
|
|
|
1
1
|
import { SocketModeClient } from "@slack/socket-mode";
|
|
2
2
|
import { WebClient } from "@slack/web-api";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, linkSync, readFileSync } from "fs";
|
|
4
4
|
import { readFile } from "fs/promises";
|
|
5
5
|
import { basename, join } from "path";
|
|
6
|
-
import { parseLoginCommand } from "../../login.js";
|
|
7
6
|
import * as log from "../../log.js";
|
|
8
|
-
import { PRODUCT_NAME,
|
|
7
|
+
import { PRODUCT_NAME, formatForceStopped, formatNothingRunning } from "../../ui-copy.js";
|
|
8
|
+
import { appendBotResponseLog, appendChannelLog, ChannelQueue, resolveOnlyScopedStopTarget, resolveStopTarget, withRetry, } from "../shared.js";
|
|
9
|
+
import { getThreadSessionFile } from "../../session-store.js";
|
|
10
|
+
import { evaluateAutoReplyPolicy } from "../../trigger.js";
|
|
9
11
|
import { createSlackAdapters } from "./context.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
return await fn();
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
24
|
-
// Check for rate limit errors
|
|
25
|
-
let isRateLimited = false;
|
|
26
|
-
// Check for rate_limited error code (Slack SDK)
|
|
27
|
-
if ("code" in lastError && lastError.code === "rate_limited") {
|
|
28
|
-
isRateLimited = true;
|
|
29
|
-
}
|
|
30
|
-
// Check for rate_limited in error response
|
|
31
|
-
if ("data" in lastError) {
|
|
32
|
-
const data = lastError
|
|
33
|
-
.data;
|
|
34
|
-
if (data?.error === "rate_limited" || data?.response?.status === 429) {
|
|
35
|
-
isRateLimited = true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (isRateLimited) {
|
|
39
|
-
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
40
|
-
log.logWarning(`Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
// Non-retryable error
|
|
45
|
-
throw lastError;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
throw lastError;
|
|
12
|
+
import { hasMaterializedSlackBranchSession } from "./branch-manager.js";
|
|
13
|
+
import { resolveSlackSessionKey } from "./session.js";
|
|
14
|
+
// Slack WebClient errors carry either `code: "rate_limited"` (retry-after) or
|
|
15
|
+
// the legacy `data.error === "rate_limited"` / 429 status shape.
|
|
16
|
+
function slackIsRateLimited(err) {
|
|
17
|
+
if (err.code === "rate_limited")
|
|
18
|
+
return true;
|
|
19
|
+
const data = err.data;
|
|
20
|
+
return data?.error === "rate_limited" || data?.response?.status === 429;
|
|
49
21
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
22
|
+
const slackRetry = (fn) => withRetry(fn, { isRateLimited: slackIsRateLimited });
|
|
23
|
+
function collectSlackText(value, parts) {
|
|
24
|
+
if (value === null || value === undefined)
|
|
25
|
+
return;
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (trimmed)
|
|
29
|
+
parts.push(trimmed);
|
|
30
|
+
return;
|
|
58
31
|
}
|
|
59
|
-
|
|
60
|
-
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
for (const item of value)
|
|
34
|
+
collectSlackText(item, parts);
|
|
35
|
+
return;
|
|
61
36
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
await work();
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
log.logWarning("Queue error", err instanceof Error ? err.message : String(err));
|
|
72
|
-
}
|
|
73
|
-
this.processing = false;
|
|
74
|
-
this.processNext();
|
|
37
|
+
if (typeof value !== "object")
|
|
38
|
+
return;
|
|
39
|
+
const obj = value;
|
|
40
|
+
for (const key of ["text", "fallback", "title", "value"]) {
|
|
41
|
+
collectSlackText(obj[key], parts);
|
|
75
42
|
}
|
|
43
|
+
collectSlackText(obj.fields, parts);
|
|
44
|
+
collectSlackText(obj.elements, parts);
|
|
45
|
+
collectSlackText(obj.blocks, parts);
|
|
46
|
+
}
|
|
47
|
+
function buildSlackAppMessageText(event) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
collectSlackText(event.text, parts);
|
|
50
|
+
collectSlackText(event.blocks, parts);
|
|
51
|
+
collectSlackText(event.attachments, parts);
|
|
52
|
+
const deduped = parts.filter((part, index) => parts.indexOf(part) === index);
|
|
53
|
+
return deduped.join("\n");
|
|
76
54
|
}
|
|
77
55
|
// ============================================================================
|
|
78
56
|
// SlackBot
|
|
@@ -80,6 +58,8 @@ class ChannelQueue {
|
|
|
80
58
|
export class SlackBot {
|
|
81
59
|
constructor(handler, config) {
|
|
82
60
|
this.botUserId = null;
|
|
61
|
+
this.botId = null;
|
|
62
|
+
this.ownMentionRegex = null;
|
|
83
63
|
this.startupTs = null; // Messages older than this are just logged, not processed
|
|
84
64
|
this.users = new Map();
|
|
85
65
|
this.channels = new Map();
|
|
@@ -88,33 +68,24 @@ export class SlackBot {
|
|
|
88
68
|
this.handler = handler;
|
|
89
69
|
this.workingDir = config.workingDir;
|
|
90
70
|
this.store = config.store;
|
|
91
|
-
this.socketClient = new SocketModeClient({
|
|
71
|
+
this.socketClient = new SocketModeClient({
|
|
72
|
+
appToken: config.appToken,
|
|
73
|
+
// Default 5s is too tight: brief event-loop stalls (e.g. backfill, sync fs)
|
|
74
|
+
// cause false pong timeouts; 4 in a row makes Slack drop the socket.
|
|
75
|
+
clientPingTimeout: 12_000,
|
|
76
|
+
});
|
|
92
77
|
this.webClient = new WebClient(config.botToken);
|
|
93
78
|
}
|
|
94
79
|
setEventsWatcher(watcher) {
|
|
95
80
|
this.eventsWatcher = watcher;
|
|
96
81
|
}
|
|
97
|
-
toBotEvent(event) {
|
|
98
|
-
return {
|
|
99
|
-
type: event.type,
|
|
100
|
-
conversationId: event.channel,
|
|
101
|
-
ts: event.ts,
|
|
102
|
-
thread_ts: event.thread_ts,
|
|
103
|
-
user: event.user,
|
|
104
|
-
text: event.text,
|
|
105
|
-
attachments: event.attachments?.map((attachment) => ({
|
|
106
|
-
name: attachment.original,
|
|
107
|
-
localPath: attachment.localPath,
|
|
108
|
-
})),
|
|
109
|
-
sessionKey: event.sessionKey,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
82
|
// ==========================================================================
|
|
113
83
|
// Public API
|
|
114
84
|
// ==========================================================================
|
|
115
85
|
async start() {
|
|
116
86
|
const auth = await this.webClient.auth.test();
|
|
117
87
|
this.botUserId = auth.user_id;
|
|
88
|
+
this.botId = typeof auth.bot_id === "string" ? auth.bot_id : null;
|
|
118
89
|
await Promise.all([this.fetchUsers(), this.fetchChannels()]);
|
|
119
90
|
log.logInfo(`Loaded ${this.channels.size} channels, ${this.users.size} users`);
|
|
120
91
|
await this.backfillAllChannels();
|
|
@@ -122,7 +93,7 @@ export class SlackBot {
|
|
|
122
93
|
await this.socketClient.start();
|
|
123
94
|
// Record startup time - messages older than this are just logged, not processed
|
|
124
95
|
this.startupTs = (Date.now() / 1000).toFixed(6);
|
|
125
|
-
log.logConnected();
|
|
96
|
+
log.logConnected("Slack");
|
|
126
97
|
}
|
|
127
98
|
getUser(userId) {
|
|
128
99
|
return this.users.get(userId);
|
|
@@ -136,19 +107,74 @@ export class SlackBot {
|
|
|
136
107
|
getAllChannels() {
|
|
137
108
|
return Array.from(this.channels.values());
|
|
138
109
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
110
|
+
stripOwnMention(text) {
|
|
111
|
+
const source = text ?? "";
|
|
112
|
+
if (!this.botUserId)
|
|
113
|
+
return source.trim();
|
|
114
|
+
if (!this.ownMentionRegex || !this.ownMentionRegex.source.includes(this.botUserId)) {
|
|
115
|
+
this.ownMentionRegex = new RegExp(`<@${this.botUserId}>`, "gi");
|
|
116
|
+
}
|
|
117
|
+
return source.replace(this.ownMentionRegex, "").trim();
|
|
118
|
+
}
|
|
119
|
+
async postMessage(channel, text) {
|
|
120
|
+
return slackRetry(async () => {
|
|
121
|
+
const result = await this.webClient.chat.postMessage({ channel, text });
|
|
142
122
|
return result.ts;
|
|
143
123
|
});
|
|
144
124
|
}
|
|
145
|
-
async
|
|
146
|
-
return
|
|
147
|
-
await this.webClient.chat.
|
|
125
|
+
async postEphemeral(channel, user, text) {
|
|
126
|
+
return slackRetry(async () => {
|
|
127
|
+
await this.webClient.chat.postEphemeral({ channel, user, text });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async postEphemeralBlocks(channel, user, text, blocks) {
|
|
131
|
+
return slackRetry(async () => {
|
|
132
|
+
await this.webClient.chat.postEphemeral({ channel, user, text, blocks: blocks });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async postMessageBlocks(channel, text, blocks) {
|
|
136
|
+
return slackRetry(async () => {
|
|
137
|
+
const result = await this.webClient.chat.postMessage({
|
|
138
|
+
channel,
|
|
139
|
+
text,
|
|
140
|
+
blocks: blocks,
|
|
141
|
+
});
|
|
142
|
+
return result.ts;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async postPrivate(conversationId, userId, text) {
|
|
146
|
+
await this.postEphemeral(conversationId, userId, text);
|
|
147
|
+
}
|
|
148
|
+
async postPrivateDiagnostic(conversationId, userId, text, options) {
|
|
149
|
+
if (options?.style !== "muted") {
|
|
150
|
+
await this.postPrivate(conversationId, userId, options?.style === "error" ? `_${text}_` : text);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const CONTEXT_TEXT_LIMIT = 3000;
|
|
154
|
+
const blockText = text.length > CONTEXT_TEXT_LIMIT
|
|
155
|
+
? text.substring(0, CONTEXT_TEXT_LIMIT - 20) + "\n_(truncated)_"
|
|
156
|
+
: text;
|
|
157
|
+
await this.postEphemeralBlocks(conversationId, userId, text, [
|
|
158
|
+
{ type: "context", elements: [{ type: "mrkdwn", text: blockText }] },
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
async openDirectMessage(userId) {
|
|
162
|
+
return slackRetry(async () => {
|
|
163
|
+
const result = await this.webClient.conversations.open({ users: userId });
|
|
164
|
+
const channelId = result.channel?.id;
|
|
165
|
+
if (!channelId) {
|
|
166
|
+
throw new Error(`Failed to open DM for user ${userId}`);
|
|
167
|
+
}
|
|
168
|
+
return channelId;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async updateMessage(channel, ts, text) {
|
|
172
|
+
return slackRetry(async () => {
|
|
173
|
+
await this.webClient.chat.update({ channel, ts, text });
|
|
148
174
|
});
|
|
149
175
|
}
|
|
150
176
|
async deleteMessage(channel, ts) {
|
|
151
|
-
return
|
|
177
|
+
return slackRetry(async () => {
|
|
152
178
|
await this.webClient.chat.delete({ channel, ts });
|
|
153
179
|
});
|
|
154
180
|
}
|
|
@@ -157,7 +183,7 @@ export class SlackBot {
|
|
|
157
183
|
// ==========================================================================
|
|
158
184
|
/** Set the status for an assistant thread (shows "thinking" state) */
|
|
159
185
|
async setAssistantStatus(channel, threadTs, status) {
|
|
160
|
-
return
|
|
186
|
+
return slackRetry(async () => {
|
|
161
187
|
await this.webClient.assistant.threads.setStatus({
|
|
162
188
|
channel_id: channel,
|
|
163
189
|
thread_ts: threadTs,
|
|
@@ -166,7 +192,7 @@ export class SlackBot {
|
|
|
166
192
|
});
|
|
167
193
|
}
|
|
168
194
|
async postInThread(channel, threadTs, text) {
|
|
169
|
-
return
|
|
195
|
+
return slackRetry(async () => {
|
|
170
196
|
// Use Block Kit section for long messages to trigger Slack's "Show more" collapsing (~700 chars)
|
|
171
197
|
const SECTION_TEXT_LIMIT = 3000;
|
|
172
198
|
if (text.length > 500) {
|
|
@@ -186,7 +212,7 @@ export class SlackBot {
|
|
|
186
212
|
});
|
|
187
213
|
}
|
|
188
214
|
async postInThreadBlocks(channel, threadTs, text, blocks) {
|
|
189
|
-
return
|
|
215
|
+
return slackRetry(async () => {
|
|
190
216
|
const result = await this.webClient.chat.postMessage({
|
|
191
217
|
channel,
|
|
192
218
|
thread_ts: threadTs,
|
|
@@ -197,7 +223,7 @@ export class SlackBot {
|
|
|
197
223
|
});
|
|
198
224
|
}
|
|
199
225
|
async uploadFile(channel, filePath, title, threadTs) {
|
|
200
|
-
return
|
|
226
|
+
return slackRetry(async () => {
|
|
201
227
|
const fileName = title || basename(filePath);
|
|
202
228
|
const fileContent = readFileSync(filePath);
|
|
203
229
|
await this.webClient.files.uploadV2({
|
|
@@ -209,29 +235,32 @@ export class SlackBot {
|
|
|
209
235
|
});
|
|
210
236
|
});
|
|
211
237
|
}
|
|
212
|
-
/**
|
|
213
|
-
* Log a message to log.jsonl (SYNC)
|
|
214
|
-
* This is the ONLY place messages are written to log.jsonl
|
|
215
|
-
*/
|
|
216
238
|
logToFile(channel, entry) {
|
|
217
|
-
|
|
218
|
-
if (!existsSync(channelDir))
|
|
219
|
-
mkdirSync(channelDir, { recursive: true });
|
|
220
|
-
appendFileSync(join(channelDir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
239
|
+
appendChannelLog(this.workingDir, channel, entry);
|
|
221
240
|
}
|
|
222
|
-
/**
|
|
223
|
-
* Log a bot response to log.jsonl
|
|
224
|
-
*/
|
|
225
241
|
logBotResponse(channel, text, ts, threadTs) {
|
|
226
|
-
this.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
appendBotResponseLog(this.workingDir, channel, text, ts, threadTs);
|
|
243
|
+
}
|
|
244
|
+
aliasSyntheticEventThread(channel, threadTs, eventTs) {
|
|
245
|
+
const conversationDir = join(this.workingDir, channel);
|
|
246
|
+
const source = getThreadSessionFile(conversationDir, `${channel}:${eventTs}`);
|
|
247
|
+
const target = getThreadSessionFile(conversationDir, `${channel}:${threadTs}`);
|
|
248
|
+
if (source === target)
|
|
249
|
+
return;
|
|
250
|
+
try {
|
|
251
|
+
linkSync(source, target);
|
|
252
|
+
log.logInfo(`Aliased synthetic event session ${source} -> ${target}`);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
const code = err.code;
|
|
256
|
+
if (code === "EEXIST")
|
|
257
|
+
return;
|
|
258
|
+
if (code === "ENOENT") {
|
|
259
|
+
log.logWarning(`Cannot alias synthetic event session; source missing: ${source}`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
log.logWarning(`Failed to alias synthetic event session ${source} -> ${target}`, err instanceof Error ? err.message : String(err));
|
|
263
|
+
}
|
|
235
264
|
}
|
|
236
265
|
getPlatformInfo() {
|
|
237
266
|
return {
|
|
@@ -243,6 +272,9 @@ export class SlackBot {
|
|
|
243
272
|
userName: u.userName,
|
|
244
273
|
displayName: u.displayName,
|
|
245
274
|
})),
|
|
275
|
+
diagnostics: {
|
|
276
|
+
showUsageSummary: true,
|
|
277
|
+
},
|
|
246
278
|
};
|
|
247
279
|
}
|
|
248
280
|
// ==========================================================================
|
|
@@ -253,20 +285,27 @@ export class SlackBot {
|
|
|
253
285
|
* Returns true if enqueued, false if queue is full (max 5).
|
|
254
286
|
*/
|
|
255
287
|
enqueueEvent(event) {
|
|
256
|
-
const
|
|
288
|
+
const conversationId = event.conversationId;
|
|
289
|
+
const queue = this.getQueue(conversationId);
|
|
257
290
|
if (queue.size() >= 5) {
|
|
258
|
-
log.logWarning(`Event queue full for ${
|
|
291
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
259
292
|
return false;
|
|
260
293
|
}
|
|
261
|
-
log.logInfo(`Enqueueing event for ${
|
|
294
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
262
295
|
queue.enqueue(() => {
|
|
263
296
|
const slackEvent = {
|
|
264
|
-
type:
|
|
265
|
-
|
|
297
|
+
type: event.type,
|
|
298
|
+
conversationId,
|
|
299
|
+
conversationKind: event.conversationKind,
|
|
300
|
+
channel: conversationId,
|
|
266
301
|
ts: event.ts,
|
|
267
302
|
thread_ts: event.thread_ts,
|
|
268
303
|
user: event.user,
|
|
269
304
|
text: event.text,
|
|
305
|
+
attachments: event.attachments?.map((attachment) => ({
|
|
306
|
+
original: attachment.name,
|
|
307
|
+
localPath: attachment.localPath,
|
|
308
|
+
})),
|
|
270
309
|
sessionKey: event.sessionKey,
|
|
271
310
|
};
|
|
272
311
|
const adapters = createSlackAdapters(slackEvent, this, true);
|
|
@@ -280,11 +319,28 @@ export class SlackBot {
|
|
|
280
319
|
getQueue(channelId) {
|
|
281
320
|
let queue = this.queues.get(channelId);
|
|
282
321
|
if (!queue) {
|
|
283
|
-
queue = new ChannelQueue();
|
|
322
|
+
queue = new ChannelQueue("Slack");
|
|
284
323
|
this.queues.set(channelId, queue);
|
|
285
324
|
}
|
|
286
325
|
return queue;
|
|
287
326
|
}
|
|
327
|
+
resolveQueueKey(conversationId, sessionKey) {
|
|
328
|
+
if (!sessionKey.includes(":"))
|
|
329
|
+
return sessionKey;
|
|
330
|
+
if (sessionKey.includes(":event:"))
|
|
331
|
+
return sessionKey;
|
|
332
|
+
return hasMaterializedSlackBranchSession(join(this.workingDir, conversationId), sessionKey)
|
|
333
|
+
? sessionKey
|
|
334
|
+
: conversationId;
|
|
335
|
+
}
|
|
336
|
+
shouldTriggerSharedThreadReply(channelId, threadTs) {
|
|
337
|
+
if (!threadTs)
|
|
338
|
+
return false;
|
|
339
|
+
const sessionKey = resolveSlackSessionKey(channelId, threadTs);
|
|
340
|
+
if (this.handler.isRunning(sessionKey))
|
|
341
|
+
return true;
|
|
342
|
+
return hasMaterializedSlackBranchSession(join(this.workingDir, channelId), sessionKey);
|
|
343
|
+
}
|
|
288
344
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
345
|
buildHomeView() {
|
|
290
346
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -387,21 +443,22 @@ export class SlackBot {
|
|
|
387
443
|
});
|
|
388
444
|
}
|
|
389
445
|
else {
|
|
390
|
-
const timestampFormatter = new Intl.DateTimeFormat(undefined, {
|
|
391
|
-
month: "short",
|
|
392
|
-
day: "numeric",
|
|
393
|
-
hour: "2-digit",
|
|
394
|
-
minute: "2-digit",
|
|
395
|
-
});
|
|
396
446
|
for (const ev of periodicEvents) {
|
|
397
447
|
const channelLabel = ev.platform === "slack"
|
|
398
448
|
? (() => {
|
|
399
|
-
const channel = this.channels.get(ev.
|
|
400
|
-
const channelName = channel ? `#${channel.name}` : ev.
|
|
449
|
+
const channel = this.channels.get(ev.conversationId);
|
|
450
|
+
const channelName = channel ? `#${channel.name}` : ev.conversationId;
|
|
401
451
|
return `${ev.platform}:${channelName}`;
|
|
402
452
|
})()
|
|
403
|
-
: `${ev.platform}:${ev.
|
|
404
|
-
const nextStr = ev.nextRun
|
|
453
|
+
: `${ev.platform}:${ev.conversationId}`;
|
|
454
|
+
const nextStr = ev.nextRun
|
|
455
|
+
? new Date(ev.nextRun).toLocaleString("en-US", {
|
|
456
|
+
month: "short",
|
|
457
|
+
day: "numeric",
|
|
458
|
+
hour: "2-digit",
|
|
459
|
+
minute: "2-digit",
|
|
460
|
+
})
|
|
461
|
+
: "—";
|
|
405
462
|
blocks.push({
|
|
406
463
|
type: "section",
|
|
407
464
|
text: {
|
|
@@ -420,25 +477,224 @@ export class SlackBot {
|
|
|
420
477
|
});
|
|
421
478
|
return { type: "home", blocks };
|
|
422
479
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Resolve which session key to stop.
|
|
425
|
-
* When stop is called from a thread, the thread session (channelId:thread_ts) might
|
|
426
|
-
* not be running — but the channel session (channelId) might be, because the bot's
|
|
427
|
-
* reply to a top-level mention creates a thread. Check both, prefer thread first.
|
|
428
|
-
*/
|
|
429
480
|
resolveStopTarget(channelId, threadTs) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
481
|
+
const directTarget = resolveStopTarget({
|
|
482
|
+
handler: this.handler,
|
|
483
|
+
conversationId: channelId,
|
|
484
|
+
sessionKey: resolveSlackSessionKey(channelId, threadTs),
|
|
485
|
+
});
|
|
486
|
+
if (directTarget)
|
|
487
|
+
return directTarget;
|
|
488
|
+
if (threadTs)
|
|
437
489
|
return null;
|
|
490
|
+
return resolveOnlyScopedStopTarget(this.handler, channelId);
|
|
491
|
+
}
|
|
492
|
+
isStopText(text) {
|
|
493
|
+
const normalized = text.trim().toLowerCase();
|
|
494
|
+
return normalized === "stop" || normalized === "/stop";
|
|
495
|
+
}
|
|
496
|
+
createCommandAdapters(conversationId, userId, userName, text, ts, options = {}) {
|
|
497
|
+
const message = {
|
|
498
|
+
id: ts,
|
|
499
|
+
sessionKey: conversationId,
|
|
500
|
+
conversationKind: options.ephemeralChannelId ? "shared" : "direct",
|
|
501
|
+
userId,
|
|
502
|
+
userName,
|
|
503
|
+
text,
|
|
504
|
+
attachments: [],
|
|
505
|
+
};
|
|
506
|
+
const respond = async (responseText) => {
|
|
507
|
+
if (options.ephemeralChannelId) {
|
|
508
|
+
await this.postEphemeral(options.ephemeralChannelId, userId, responseText);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const messageTs = await this.postMessage(conversationId, responseText);
|
|
512
|
+
this.logBotResponse(conversationId, responseText, messageTs);
|
|
513
|
+
};
|
|
514
|
+
const respondMuted = async (responseText) => {
|
|
515
|
+
const CONTEXT_TEXT_LIMIT = 3000;
|
|
516
|
+
const blockText = responseText.length > CONTEXT_TEXT_LIMIT
|
|
517
|
+
? responseText.substring(0, CONTEXT_TEXT_LIMIT - 20) + "\n_(truncated)_"
|
|
518
|
+
: responseText;
|
|
519
|
+
const blocks = [{ type: "context", elements: [{ type: "mrkdwn", text: blockText }] }];
|
|
520
|
+
if (options.ephemeralChannelId) {
|
|
521
|
+
await this.postEphemeralBlocks(options.ephemeralChannelId, userId, responseText, blocks);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const messageTs = await this.postMessageBlocks(conversationId, responseText, blocks);
|
|
525
|
+
this.logBotResponse(conversationId, responseText, messageTs);
|
|
526
|
+
};
|
|
527
|
+
const responseCtx = {
|
|
528
|
+
respond,
|
|
529
|
+
replaceResponse: respond,
|
|
530
|
+
respondDiagnostic: async (responseText, responseOptions) => {
|
|
531
|
+
if (responseOptions?.style === "muted") {
|
|
532
|
+
await respondMuted(responseText);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
await respond(responseOptions?.style === "error" ? `_${responseText}_` : responseText);
|
|
536
|
+
},
|
|
537
|
+
respondToolResult: async (result) => {
|
|
538
|
+
const duration = (result.durationMs / 1000).toFixed(1);
|
|
539
|
+
await respond(`${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`);
|
|
540
|
+
},
|
|
541
|
+
setTyping: async () => { },
|
|
542
|
+
setWorking: async () => { },
|
|
543
|
+
uploadFile: async (filePath, title) => {
|
|
544
|
+
await this.uploadFile(conversationId, filePath, title);
|
|
545
|
+
},
|
|
546
|
+
deleteResponse: async () => { },
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
message,
|
|
550
|
+
responseCtx,
|
|
551
|
+
platform: this.getPlatformInfo(),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
createSlashCommandBot(conversationId, threadTs) {
|
|
555
|
+
return {
|
|
556
|
+
start: async () => { },
|
|
557
|
+
postMessage: async (_channel, text) => {
|
|
558
|
+
if (threadTs) {
|
|
559
|
+
return this.postInThread(conversationId, threadTs, text);
|
|
560
|
+
}
|
|
561
|
+
return this.postMessage(conversationId, text);
|
|
562
|
+
},
|
|
563
|
+
updateMessage: async (channel, ts, text) => {
|
|
564
|
+
await this.updateMessage(channel, ts, text);
|
|
565
|
+
},
|
|
566
|
+
enqueueEvent: (event) => this.enqueueEvent(event),
|
|
567
|
+
getPlatformInfo: () => this.getPlatformInfo(),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async routeSlashLoginCommand(payload) {
|
|
571
|
+
const commandSuffix = payload.text?.trim();
|
|
572
|
+
const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
|
|
573
|
+
const createdAt = new Date();
|
|
574
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
575
|
+
const sourceChannelId = payload.channel_id;
|
|
576
|
+
const isDirectMessage = sourceChannelId.startsWith("D");
|
|
577
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
578
|
+
this.logToFile(sourceChannelId, {
|
|
579
|
+
date: createdAt.toISOString(),
|
|
580
|
+
ts: eventTs,
|
|
581
|
+
user: payload.user_id,
|
|
582
|
+
userName,
|
|
583
|
+
text: commandText,
|
|
584
|
+
attachments: [],
|
|
585
|
+
isBot: false,
|
|
586
|
+
});
|
|
587
|
+
const event = {
|
|
588
|
+
type: isDirectMessage ? "dm" : "private_command",
|
|
589
|
+
conversationId: sourceChannelId,
|
|
590
|
+
conversationKind: isDirectMessage ? "direct" : "shared",
|
|
591
|
+
ts: eventTs,
|
|
592
|
+
user: payload.user_id,
|
|
593
|
+
text: commandText,
|
|
594
|
+
attachments: [],
|
|
595
|
+
sessionKey: sourceChannelId,
|
|
596
|
+
};
|
|
597
|
+
const adapters = this.createCommandAdapters(sourceChannelId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: sourceChannelId });
|
|
598
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
599
|
+
}
|
|
600
|
+
async routeSlashNewCommand(payload) {
|
|
601
|
+
const conversationId = payload.channel_id;
|
|
602
|
+
if (!conversationId.startsWith("D")) {
|
|
603
|
+
await this.postEphemeral(conversationId, payload.user_id, `為了避免誤清除共享上下文,${payload.command} 目前只能在與 ${PRODUCT_NAME} 的私訊中使用。`);
|
|
604
|
+
return;
|
|
438
605
|
}
|
|
439
|
-
|
|
606
|
+
const createdAt = new Date();
|
|
607
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
608
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
609
|
+
this.logToFile(conversationId, {
|
|
610
|
+
date: createdAt.toISOString(),
|
|
611
|
+
ts: eventTs,
|
|
612
|
+
user: payload.user_id,
|
|
613
|
+
userName,
|
|
614
|
+
text: payload.command,
|
|
615
|
+
attachments: [],
|
|
616
|
+
isBot: false,
|
|
617
|
+
});
|
|
618
|
+
const commandBot = this.createSlashCommandBot(conversationId);
|
|
619
|
+
await this.handler.handleNewCommand(conversationId, conversationId, commandBot);
|
|
620
|
+
}
|
|
621
|
+
async routeSlashModelCommand(payload) {
|
|
622
|
+
const conversationId = payload.channel_id;
|
|
623
|
+
const isDirectMessage = conversationId.startsWith("D");
|
|
624
|
+
const createdAt = new Date();
|
|
625
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
626
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
627
|
+
const commandSuffix = payload.text?.trim();
|
|
628
|
+
const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
|
|
629
|
+
this.logToFile(conversationId, {
|
|
630
|
+
date: createdAt.toISOString(),
|
|
631
|
+
ts: eventTs,
|
|
632
|
+
user: payload.user_id,
|
|
633
|
+
userName,
|
|
634
|
+
text: commandText,
|
|
635
|
+
attachments: [],
|
|
636
|
+
isBot: false,
|
|
637
|
+
});
|
|
638
|
+
const sessionKey = conversationId;
|
|
639
|
+
const event = {
|
|
640
|
+
type: isDirectMessage ? "dm" : "mention",
|
|
641
|
+
conversationId,
|
|
642
|
+
conversationKind: isDirectMessage ? "direct" : "shared",
|
|
643
|
+
ts: eventTs,
|
|
644
|
+
user: payload.user_id,
|
|
645
|
+
text: commandText,
|
|
646
|
+
attachments: [],
|
|
647
|
+
sessionKey,
|
|
648
|
+
};
|
|
649
|
+
const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
|
|
650
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
651
|
+
}
|
|
652
|
+
async routeSlashSandboxCommand(payload) {
|
|
653
|
+
await this.routeSlashModelCommand(payload);
|
|
654
|
+
}
|
|
655
|
+
async routeSlashAutoReplyCommand(payload) {
|
|
656
|
+
await this.routeSlashModelCommand(payload);
|
|
657
|
+
}
|
|
658
|
+
async routeSlashSessionCommand(payload) {
|
|
659
|
+
const conversationId = payload.channel_id;
|
|
660
|
+
const isDirectMessage = conversationId.startsWith("D");
|
|
661
|
+
const createdAt = new Date();
|
|
662
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
663
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
664
|
+
const commandText = payload.command;
|
|
665
|
+
this.logToFile(conversationId, {
|
|
666
|
+
date: createdAt.toISOString(),
|
|
667
|
+
ts: eventTs,
|
|
668
|
+
user: payload.user_id,
|
|
669
|
+
userName,
|
|
670
|
+
text: commandText,
|
|
671
|
+
attachments: [],
|
|
672
|
+
isBot: false,
|
|
673
|
+
});
|
|
674
|
+
const sessionKey = conversationId;
|
|
675
|
+
const event = {
|
|
676
|
+
type: isDirectMessage ? "dm" : "mention",
|
|
677
|
+
conversationId,
|
|
678
|
+
conversationKind: isDirectMessage ? "direct" : "shared",
|
|
679
|
+
ts: eventTs,
|
|
680
|
+
user: payload.user_id,
|
|
681
|
+
text: commandText,
|
|
682
|
+
attachments: [],
|
|
683
|
+
sessionKey,
|
|
684
|
+
};
|
|
685
|
+
const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
|
|
686
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
440
687
|
}
|
|
441
688
|
setupEventHandlers() {
|
|
689
|
+
this.socketClient.on("disconnect", (err) => {
|
|
690
|
+
log.logWarning("Slack socket disconnect", err ? String(err) : "");
|
|
691
|
+
});
|
|
692
|
+
this.socketClient.on("error", (err) => {
|
|
693
|
+
log.logWarning("Slack socket error", err ? String(err) : "");
|
|
694
|
+
});
|
|
695
|
+
this.socketClient.on("unable_to_socket_mode_start", (err) => {
|
|
696
|
+
log.logWarning("Slack socket unable_to_start", err ? String(err) : "");
|
|
697
|
+
});
|
|
442
698
|
// Channel @mentions
|
|
443
699
|
this.socketClient.on("app_mention", ({ event, ack }) => {
|
|
444
700
|
const e = event;
|
|
@@ -449,28 +705,31 @@ export class SlackBot {
|
|
|
449
705
|
}
|
|
450
706
|
// Top-level mentions use a persistent channel session.
|
|
451
707
|
// Thread replies get their own isolated session (channelId:thread_ts).
|
|
452
|
-
const sessionKey = e.
|
|
708
|
+
const sessionKey = resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
453
709
|
const slackEvent = {
|
|
454
710
|
type: "mention",
|
|
711
|
+
conversationId: e.channel,
|
|
712
|
+
conversationKind: "shared",
|
|
455
713
|
channel: e.channel,
|
|
456
714
|
ts: e.ts,
|
|
457
715
|
thread_ts: e.thread_ts,
|
|
458
716
|
user: e.user,
|
|
459
|
-
text: e.text
|
|
717
|
+
text: this.stripOwnMention(e.text),
|
|
460
718
|
files: e.files,
|
|
461
719
|
sessionKey,
|
|
462
720
|
};
|
|
463
|
-
|
|
464
|
-
// Also downloads attachments in background and stores local paths
|
|
465
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
721
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
466
722
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
467
723
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
468
724
|
log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
|
|
725
|
+
void attachmentsPromise.catch((err) => {
|
|
726
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
727
|
+
});
|
|
469
728
|
ack();
|
|
470
729
|
return;
|
|
471
730
|
}
|
|
472
731
|
// Check for stop command - execute immediately, don't queue!
|
|
473
|
-
if (slackEvent.text
|
|
732
|
+
if (this.isStopText(slackEvent.text)) {
|
|
474
733
|
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
475
734
|
if (stopTarget) {
|
|
476
735
|
this.handler.handleStop(stopTarget, e.channel, this);
|
|
@@ -478,32 +737,46 @@ export class SlackBot {
|
|
|
478
737
|
else {
|
|
479
738
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
480
739
|
}
|
|
740
|
+
void attachmentsPromise.catch((err) => {
|
|
741
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
742
|
+
});
|
|
481
743
|
ack();
|
|
482
744
|
return;
|
|
483
745
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
// SYNC: Check if busy (per-thread)
|
|
491
|
-
if (this.handler.isRunning(sessionKey)) {
|
|
492
|
-
this.postMessage(e.channel, formatAlreadyWorking("slack", "@mama stop", { scope: "thread" }));
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
this.getQueue(sessionKey).enqueue(() => {
|
|
496
|
-
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
497
|
-
return this.handler.handleEvent(this.toBotEvent(slackEvent), this, adapters, false);
|
|
498
|
-
});
|
|
499
|
-
}
|
|
746
|
+
this.getQueue(this.resolveQueueKey(e.channel, sessionKey)).enqueue(async () => {
|
|
747
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
748
|
+
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
749
|
+
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
750
|
+
});
|
|
500
751
|
ack();
|
|
501
752
|
});
|
|
502
753
|
// All messages (for logging) + DMs (for triggering)
|
|
503
754
|
this.socketClient.on("message", ({ event, ack }) => {
|
|
504
755
|
const e = event;
|
|
505
|
-
|
|
506
|
-
|
|
756
|
+
const hasFiles = !!e.files && e.files.length > 0;
|
|
757
|
+
const hasSlackContent = !!e.text || hasFiles || !!e.blocks?.length || !!e.attachments?.length;
|
|
758
|
+
const isOwnBotMessage = (!!e.user && e.user === this.botUserId) || (!!this.botId && e.bot_id === this.botId);
|
|
759
|
+
if (isOwnBotMessage) {
|
|
760
|
+
ack();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const isExternalBotMessage = !!e.bot_id || e.subtype === "bot_message";
|
|
764
|
+
if (isExternalBotMessage) {
|
|
765
|
+
if (e.subtype !== undefined && e.subtype !== "bot_message" && e.subtype !== "file_share") {
|
|
766
|
+
ack();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (!hasSlackContent) {
|
|
770
|
+
ack();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
void this.logExternalBotMessage(e).catch((err) => {
|
|
774
|
+
log.logWarning("Failed to log Slack bot message", String(err));
|
|
775
|
+
});
|
|
776
|
+
ack();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (!e.user) {
|
|
507
780
|
ack();
|
|
508
781
|
return;
|
|
509
782
|
}
|
|
@@ -511,39 +784,44 @@ export class SlackBot {
|
|
|
511
784
|
ack();
|
|
512
785
|
return;
|
|
513
786
|
}
|
|
514
|
-
if (!
|
|
787
|
+
if (!hasSlackContent) {
|
|
515
788
|
ack();
|
|
516
789
|
return;
|
|
517
790
|
}
|
|
518
791
|
const isDM = e.channel_type === "im";
|
|
792
|
+
const conversationKind = isDM ? "direct" : "shared";
|
|
519
793
|
const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
|
|
520
794
|
// Skip channel @mentions - already handled by app_mention event
|
|
521
795
|
if (!isDM && isBotMention) {
|
|
522
796
|
ack();
|
|
523
797
|
return;
|
|
524
798
|
}
|
|
799
|
+
const isSharedThreadReply = !isDM && this.shouldTriggerSharedThreadReply(e.channel, e.thread_ts);
|
|
800
|
+
const sessionKey = isDM || isSharedThreadReply ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
|
|
525
801
|
const slackEvent = {
|
|
526
802
|
type: isDM ? "dm" : "mention",
|
|
803
|
+
conversationId: e.channel,
|
|
804
|
+
conversationKind,
|
|
527
805
|
channel: e.channel,
|
|
528
806
|
ts: e.ts,
|
|
529
807
|
thread_ts: e.thread_ts,
|
|
530
808
|
user: e.user,
|
|
531
|
-
text: (e.text
|
|
809
|
+
text: this.stripOwnMention(e.text),
|
|
532
810
|
files: e.files,
|
|
533
|
-
sessionKey
|
|
811
|
+
sessionKey,
|
|
534
812
|
};
|
|
535
|
-
|
|
536
|
-
// Also downloads attachments in background and stores local paths
|
|
537
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
813
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
538
814
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
539
815
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
540
816
|
log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
|
|
817
|
+
void attachmentsPromise.catch((err) => {
|
|
818
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
819
|
+
});
|
|
541
820
|
ack();
|
|
542
821
|
return;
|
|
543
822
|
}
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
if (!isDM && e.thread_ts && slackEvent.text.toLowerCase().trim() === "stop") {
|
|
823
|
+
// Stop command for DM or shared-channel thread reply (app_mention handles "@mama stop").
|
|
824
|
+
if ((isDM || (!isDM && e.thread_ts)) && this.isStopText(slackEvent.text)) {
|
|
547
825
|
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
548
826
|
if (stopTarget) {
|
|
549
827
|
this.handler.handleStop(stopTarget, e.channel, this);
|
|
@@ -551,41 +829,105 @@ export class SlackBot {
|
|
|
551
829
|
else {
|
|
552
830
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
553
831
|
}
|
|
832
|
+
void attachmentsPromise.catch((err) => {
|
|
833
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
834
|
+
});
|
|
554
835
|
ack();
|
|
555
836
|
return;
|
|
556
837
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
ack();
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
if (this.handler.isRunning(dmSessionKey)) {
|
|
578
|
-
this.postMessage(e.channel, formatAlreadyWorking("slack", "stop"));
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
this.getQueue(dmSessionKey).enqueue(() => {
|
|
582
|
-
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
583
|
-
return this.handler.handleEvent(this.toBotEvent(slackEvent), this, adapters, false);
|
|
584
|
-
});
|
|
585
|
-
}
|
|
838
|
+
const enqueueTriggered = () => {
|
|
839
|
+
const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
840
|
+
this.getQueue(this.resolveQueueKey(e.channel, activeSessionKey)).enqueue(async () => {
|
|
841
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
842
|
+
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
843
|
+
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
844
|
+
});
|
|
845
|
+
};
|
|
846
|
+
const logOnly = () => {
|
|
847
|
+
void attachmentsPromise.catch((err) => {
|
|
848
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
if (isDM || isSharedThreadReply) {
|
|
852
|
+
enqueueTriggered();
|
|
853
|
+
ack();
|
|
854
|
+
return;
|
|
586
855
|
}
|
|
856
|
+
// Shared-channel non-mention, non-thread: gate via auto-reply policy.
|
|
857
|
+
// evaluateAutoReplyPolicy never throws — judge errors/timeouts surface as
|
|
858
|
+
// trigger:false with a distinct reason, and the user message has already
|
|
859
|
+
// been queued for logging via logUserMessage above.
|
|
860
|
+
evaluateAutoReplyPolicy({
|
|
861
|
+
event: slackEvent,
|
|
862
|
+
workingDir: this.workingDir,
|
|
863
|
+
}).then((triggerResult) => {
|
|
864
|
+
if (triggerResult.trigger)
|
|
865
|
+
enqueueTriggered();
|
|
866
|
+
else
|
|
867
|
+
logOnly();
|
|
868
|
+
});
|
|
587
869
|
ack();
|
|
588
870
|
});
|
|
871
|
+
this.socketClient.on("slash_commands", async ({ body, ack }) => {
|
|
872
|
+
const payload = body;
|
|
873
|
+
await ack();
|
|
874
|
+
if (!payload.command || !payload.channel_id || !payload.user_id) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const handlerPromise = payload.command === "/pi-login"
|
|
878
|
+
? this.routeSlashLoginCommand({
|
|
879
|
+
command: payload.command,
|
|
880
|
+
text: payload.text,
|
|
881
|
+
channel_id: payload.channel_id,
|
|
882
|
+
user_id: payload.user_id,
|
|
883
|
+
user_name: payload.user_name,
|
|
884
|
+
})
|
|
885
|
+
: payload.command === "/pi-new"
|
|
886
|
+
? this.routeSlashNewCommand({
|
|
887
|
+
command: payload.command,
|
|
888
|
+
channel_id: payload.channel_id,
|
|
889
|
+
user_id: payload.user_id,
|
|
890
|
+
user_name: payload.user_name,
|
|
891
|
+
})
|
|
892
|
+
: payload.command === "/pi-session"
|
|
893
|
+
? this.routeSlashSessionCommand({
|
|
894
|
+
command: payload.command,
|
|
895
|
+
channel_id: payload.channel_id,
|
|
896
|
+
user_id: payload.user_id,
|
|
897
|
+
user_name: payload.user_name,
|
|
898
|
+
})
|
|
899
|
+
: payload.command === "/pi-model"
|
|
900
|
+
? this.routeSlashModelCommand({
|
|
901
|
+
command: payload.command,
|
|
902
|
+
text: payload.text,
|
|
903
|
+
channel_id: payload.channel_id,
|
|
904
|
+
user_id: payload.user_id,
|
|
905
|
+
user_name: payload.user_name,
|
|
906
|
+
})
|
|
907
|
+
: payload.command === "/pi-sandbox"
|
|
908
|
+
? this.routeSlashSandboxCommand({
|
|
909
|
+
command: payload.command,
|
|
910
|
+
text: payload.text,
|
|
911
|
+
channel_id: payload.channel_id,
|
|
912
|
+
user_id: payload.user_id,
|
|
913
|
+
user_name: payload.user_name,
|
|
914
|
+
})
|
|
915
|
+
: payload.command === "/pi-auto-reply"
|
|
916
|
+
? this.routeSlashAutoReplyCommand({
|
|
917
|
+
command: payload.command,
|
|
918
|
+
text: payload.text,
|
|
919
|
+
channel_id: payload.channel_id,
|
|
920
|
+
user_id: payload.user_id,
|
|
921
|
+
user_name: payload.user_name,
|
|
922
|
+
})
|
|
923
|
+
: null;
|
|
924
|
+
if (!handlerPromise) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
handlerPromise.catch((err) => {
|
|
928
|
+
log.logWarning("Slack slash command error", err instanceof Error ? err.message : String(err));
|
|
929
|
+
});
|
|
930
|
+
});
|
|
589
931
|
// App Home tab
|
|
590
932
|
this.socketClient.on("app_home_opened", ({ event, ack }) => {
|
|
591
933
|
const e = event;
|
|
@@ -616,8 +958,7 @@ export class SlackBot {
|
|
|
616
958
|
// Use handler's forceStop method
|
|
617
959
|
this.handler.forceStop(sessionKey);
|
|
618
960
|
// Notify in channel
|
|
619
|
-
|
|
620
|
-
await this.postMessage(channelId, formatForceStopped("slack", actorLabel));
|
|
961
|
+
await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
|
|
621
962
|
// Refresh home tab
|
|
622
963
|
if (userId) {
|
|
623
964
|
this.webClient.views
|
|
@@ -632,15 +973,22 @@ export class SlackBot {
|
|
|
632
973
|
});
|
|
633
974
|
}
|
|
634
975
|
/**
|
|
635
|
-
* Log a user message to log.jsonl
|
|
636
|
-
* Downloads attachments in background via store
|
|
976
|
+
* Log a user message to log.jsonl after attachments are ready.
|
|
637
977
|
*/
|
|
638
|
-
logUserMessage(event) {
|
|
978
|
+
async logUserMessage(event) {
|
|
639
979
|
const user = this.users.get(event.user);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
980
|
+
let attachments = [];
|
|
981
|
+
let attachmentError;
|
|
982
|
+
if (event.files) {
|
|
983
|
+
try {
|
|
984
|
+
attachments = await this.store.processAttachments(event.channel, event.files, event.ts);
|
|
985
|
+
}
|
|
986
|
+
catch (err) {
|
|
987
|
+
attachmentError = err;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
// Always write the text log, even if attachment processing failed — we want
|
|
991
|
+
// a record of the user message regardless of file-handling errors.
|
|
644
992
|
this.logToFile(event.channel, {
|
|
645
993
|
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
646
994
|
ts: event.ts,
|
|
@@ -652,6 +1000,29 @@ export class SlackBot {
|
|
|
652
1000
|
attachments,
|
|
653
1001
|
isBot: false,
|
|
654
1002
|
});
|
|
1003
|
+
if (attachmentError)
|
|
1004
|
+
throw attachmentError;
|
|
1005
|
+
return attachments;
|
|
1006
|
+
}
|
|
1007
|
+
async logExternalBotMessage(event) {
|
|
1008
|
+
const attachments = event.files
|
|
1009
|
+
? await this.store.processAttachments(event.channel, event.files, event.ts)
|
|
1010
|
+
: [];
|
|
1011
|
+
const botName = event.username ?? event.bot_profile?.name ?? event.bot_profile?.real_name ?? event.bot_id;
|
|
1012
|
+
this.logToFile(event.channel, {
|
|
1013
|
+
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
1014
|
+
ts: event.ts,
|
|
1015
|
+
threadTs: event.thread_ts,
|
|
1016
|
+
user: event.bot_id ? `bot:${event.bot_id}` : "external-bot",
|
|
1017
|
+
userName: botName,
|
|
1018
|
+
displayName: botName,
|
|
1019
|
+
text: buildSlackAppMessageText(event),
|
|
1020
|
+
attachments,
|
|
1021
|
+
isBot: true,
|
|
1022
|
+
botId: event.bot_id,
|
|
1023
|
+
appId: event.app_id ?? event.bot_profile?.app_id,
|
|
1024
|
+
subtype: event.subtype,
|
|
1025
|
+
});
|
|
655
1026
|
return attachments;
|
|
656
1027
|
}
|
|
657
1028
|
// ==========================================================================
|
|
@@ -664,13 +1035,15 @@ export class SlackBot {
|
|
|
664
1035
|
return timestamps;
|
|
665
1036
|
const content = await readFile(logPath, "utf-8");
|
|
666
1037
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
667
|
-
for (
|
|
1038
|
+
for (let i = 0; i < lines.length; i++) {
|
|
668
1039
|
try {
|
|
669
|
-
const entry = JSON.parse(
|
|
1040
|
+
const entry = JSON.parse(lines[i]);
|
|
670
1041
|
if (entry.ts)
|
|
671
1042
|
timestamps.add(entry.ts);
|
|
672
1043
|
}
|
|
673
|
-
catch {
|
|
1044
|
+
catch (err) {
|
|
1045
|
+
log.logWarning(`Skipping malformed log entry at ${logPath}:${i + 1}`, err instanceof Error ? err.message : String(err));
|
|
1046
|
+
}
|
|
674
1047
|
}
|
|
675
1048
|
return timestamps;
|
|
676
1049
|
}
|
|
@@ -700,14 +1073,26 @@ export class SlackBot {
|
|
|
700
1073
|
cursor = result.response_metadata?.next_cursor;
|
|
701
1074
|
pageCount++;
|
|
702
1075
|
} while (cursor && pageCount < maxPages);
|
|
703
|
-
// Filter: include mama's messages,
|
|
1076
|
+
// Filter: include mama's messages, external app/bot messages, and user messages.
|
|
704
1077
|
const relevantMessages = allMessages.filter((msg) => {
|
|
705
1078
|
if (!msg.ts || existingTs.has(msg.ts))
|
|
706
1079
|
return false; // Skip duplicates
|
|
707
1080
|
if (msg.user === this.botUserId)
|
|
708
1081
|
return true;
|
|
709
|
-
|
|
710
|
-
|
|
1082
|
+
const isExternalBotMessage = !!msg.bot_id || msg.subtype === "bot_message";
|
|
1083
|
+
if (isExternalBotMessage) {
|
|
1084
|
+
if (this.botId && msg.bot_id === this.botId)
|
|
1085
|
+
return false;
|
|
1086
|
+
if (msg.subtype !== undefined &&
|
|
1087
|
+
msg.subtype !== "bot_message" &&
|
|
1088
|
+
msg.subtype !== "file_share") {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
return (!!msg.text ||
|
|
1092
|
+
!!(msg.files && msg.files.length > 0) ||
|
|
1093
|
+
!!msg.blocks?.length ||
|
|
1094
|
+
!!msg.attachments?.length);
|
|
1095
|
+
}
|
|
711
1096
|
if (msg.subtype !== undefined && msg.subtype !== "file_share")
|
|
712
1097
|
return false;
|
|
713
1098
|
if (!msg.user)
|
|
@@ -721,16 +1106,20 @@ export class SlackBot {
|
|
|
721
1106
|
// Log each message to log.jsonl
|
|
722
1107
|
for (const msg of relevantMessages) {
|
|
723
1108
|
const isMamaMessage = msg.user === this.botUserId;
|
|
1109
|
+
const isExternalBotMessage = !isMamaMessage && (!!msg.bot_id || msg.subtype === "bot_message");
|
|
1110
|
+
if (isExternalBotMessage) {
|
|
1111
|
+
await this.logExternalBotMessage({ ...msg, channel: channelId, ts: msg.ts });
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
724
1114
|
const user = this.users.get(msg.user);
|
|
725
|
-
|
|
726
|
-
const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
|
|
727
|
-
// Process attachments - queues downloads in background
|
|
1115
|
+
const text = this.stripOwnMention(msg.text);
|
|
728
1116
|
const attachments = msg.files
|
|
729
|
-
? this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
1117
|
+
? await this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
730
1118
|
: [];
|
|
731
1119
|
this.logToFile(channelId, {
|
|
732
1120
|
date: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
733
1121
|
ts: msg.ts,
|
|
1122
|
+
threadTs: msg.thread_ts,
|
|
734
1123
|
user: isMamaMessage ? "bot" : msg.user,
|
|
735
1124
|
userName: isMamaMessage ? undefined : user?.userName,
|
|
736
1125
|
displayName: isMamaMessage ? undefined : user?.displayName,
|