@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.10
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 +620 -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 +38 -52
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +212 -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 +138 -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 });
|
|
122
|
+
return result.ts;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
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
142
|
return result.ts;
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
-
async
|
|
146
|
-
|
|
147
|
-
|
|
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,231 @@ 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 targetChannelId = isDirectMessage
|
|
578
|
+
? sourceChannelId
|
|
579
|
+
: await this.openDirectMessage(payload.user_id);
|
|
580
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
581
|
+
this.logToFile(targetChannelId, {
|
|
582
|
+
date: createdAt.toISOString(),
|
|
583
|
+
ts: eventTs,
|
|
584
|
+
user: payload.user_id,
|
|
585
|
+
userName,
|
|
586
|
+
text: commandText,
|
|
587
|
+
attachments: [],
|
|
588
|
+
isBot: false,
|
|
589
|
+
});
|
|
590
|
+
if (!isDirectMessage) {
|
|
591
|
+
await this.postEphemeral(sourceChannelId, payload.user_id, `我已私訊你 ${PRODUCT_NAME} 的登入連結,請到私訊完成設定。`);
|
|
438
592
|
}
|
|
439
|
-
|
|
593
|
+
const event = {
|
|
594
|
+
type: "dm",
|
|
595
|
+
conversationId: targetChannelId,
|
|
596
|
+
...(isDirectMessage ? {} : { vaultConversationId: sourceChannelId }),
|
|
597
|
+
conversationKind: "direct",
|
|
598
|
+
ts: eventTs,
|
|
599
|
+
user: payload.user_id,
|
|
600
|
+
text: commandText,
|
|
601
|
+
attachments: [],
|
|
602
|
+
sessionKey: targetChannelId,
|
|
603
|
+
};
|
|
604
|
+
const adapters = this.createCommandAdapters(targetChannelId, payload.user_id, userName, commandText, eventTs);
|
|
605
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
606
|
+
}
|
|
607
|
+
async routeSlashNewCommand(payload) {
|
|
608
|
+
const conversationId = payload.channel_id;
|
|
609
|
+
if (!conversationId.startsWith("D")) {
|
|
610
|
+
await this.postEphemeral(conversationId, payload.user_id, `為了避免誤清除共享上下文,${payload.command} 目前只能在與 ${PRODUCT_NAME} 的私訊中使用。`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const createdAt = new Date();
|
|
614
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
615
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
616
|
+
this.logToFile(conversationId, {
|
|
617
|
+
date: createdAt.toISOString(),
|
|
618
|
+
ts: eventTs,
|
|
619
|
+
user: payload.user_id,
|
|
620
|
+
userName,
|
|
621
|
+
text: payload.command,
|
|
622
|
+
attachments: [],
|
|
623
|
+
isBot: false,
|
|
624
|
+
});
|
|
625
|
+
const commandBot = this.createSlashCommandBot(conversationId);
|
|
626
|
+
await this.handler.handleNewCommand(conversationId, conversationId, commandBot);
|
|
627
|
+
}
|
|
628
|
+
async routeSlashModelCommand(payload) {
|
|
629
|
+
const conversationId = payload.channel_id;
|
|
630
|
+
const isDirectMessage = conversationId.startsWith("D");
|
|
631
|
+
const createdAt = new Date();
|
|
632
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
633
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
634
|
+
const commandSuffix = payload.text?.trim();
|
|
635
|
+
const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
|
|
636
|
+
this.logToFile(conversationId, {
|
|
637
|
+
date: createdAt.toISOString(),
|
|
638
|
+
ts: eventTs,
|
|
639
|
+
user: payload.user_id,
|
|
640
|
+
userName,
|
|
641
|
+
text: commandText,
|
|
642
|
+
attachments: [],
|
|
643
|
+
isBot: false,
|
|
644
|
+
});
|
|
645
|
+
const sessionKey = conversationId;
|
|
646
|
+
const event = {
|
|
647
|
+
type: isDirectMessage ? "dm" : "mention",
|
|
648
|
+
conversationId,
|
|
649
|
+
conversationKind: isDirectMessage ? "direct" : "shared",
|
|
650
|
+
ts: eventTs,
|
|
651
|
+
user: payload.user_id,
|
|
652
|
+
text: commandText,
|
|
653
|
+
attachments: [],
|
|
654
|
+
sessionKey,
|
|
655
|
+
};
|
|
656
|
+
const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
|
|
657
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
658
|
+
}
|
|
659
|
+
async routeSlashSandboxCommand(payload) {
|
|
660
|
+
await this.routeSlashModelCommand(payload);
|
|
661
|
+
}
|
|
662
|
+
async routeSlashAutoReplyCommand(payload) {
|
|
663
|
+
await this.routeSlashModelCommand(payload);
|
|
664
|
+
}
|
|
665
|
+
async routeSlashSessionCommand(payload) {
|
|
666
|
+
const conversationId = payload.channel_id;
|
|
667
|
+
const isDirectMessage = conversationId.startsWith("D");
|
|
668
|
+
const createdAt = new Date();
|
|
669
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
670
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
671
|
+
const commandText = payload.command;
|
|
672
|
+
this.logToFile(conversationId, {
|
|
673
|
+
date: createdAt.toISOString(),
|
|
674
|
+
ts: eventTs,
|
|
675
|
+
user: payload.user_id,
|
|
676
|
+
userName,
|
|
677
|
+
text: commandText,
|
|
678
|
+
attachments: [],
|
|
679
|
+
isBot: false,
|
|
680
|
+
});
|
|
681
|
+
const sessionKey = conversationId;
|
|
682
|
+
const event = {
|
|
683
|
+
type: isDirectMessage ? "dm" : "mention",
|
|
684
|
+
conversationId,
|
|
685
|
+
conversationKind: isDirectMessage ? "direct" : "shared",
|
|
686
|
+
ts: eventTs,
|
|
687
|
+
user: payload.user_id,
|
|
688
|
+
text: commandText,
|
|
689
|
+
attachments: [],
|
|
690
|
+
sessionKey,
|
|
691
|
+
};
|
|
692
|
+
const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
|
|
693
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
440
694
|
}
|
|
441
695
|
setupEventHandlers() {
|
|
696
|
+
this.socketClient.on("disconnect", (err) => {
|
|
697
|
+
log.logWarning("Slack socket disconnect", err ? String(err) : "");
|
|
698
|
+
});
|
|
699
|
+
this.socketClient.on("error", (err) => {
|
|
700
|
+
log.logWarning("Slack socket error", err ? String(err) : "");
|
|
701
|
+
});
|
|
702
|
+
this.socketClient.on("unable_to_socket_mode_start", (err) => {
|
|
703
|
+
log.logWarning("Slack socket unable_to_start", err ? String(err) : "");
|
|
704
|
+
});
|
|
442
705
|
// Channel @mentions
|
|
443
706
|
this.socketClient.on("app_mention", ({ event, ack }) => {
|
|
444
707
|
const e = event;
|
|
@@ -449,28 +712,31 @@ export class SlackBot {
|
|
|
449
712
|
}
|
|
450
713
|
// Top-level mentions use a persistent channel session.
|
|
451
714
|
// Thread replies get their own isolated session (channelId:thread_ts).
|
|
452
|
-
const sessionKey = e.
|
|
715
|
+
const sessionKey = resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
453
716
|
const slackEvent = {
|
|
454
717
|
type: "mention",
|
|
718
|
+
conversationId: e.channel,
|
|
719
|
+
conversationKind: "shared",
|
|
455
720
|
channel: e.channel,
|
|
456
721
|
ts: e.ts,
|
|
457
722
|
thread_ts: e.thread_ts,
|
|
458
723
|
user: e.user,
|
|
459
|
-
text: e.text
|
|
724
|
+
text: this.stripOwnMention(e.text),
|
|
460
725
|
files: e.files,
|
|
461
726
|
sessionKey,
|
|
462
727
|
};
|
|
463
|
-
|
|
464
|
-
// Also downloads attachments in background and stores local paths
|
|
465
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
728
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
466
729
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
467
730
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
468
731
|
log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
|
|
732
|
+
void attachmentsPromise.catch((err) => {
|
|
733
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
734
|
+
});
|
|
469
735
|
ack();
|
|
470
736
|
return;
|
|
471
737
|
}
|
|
472
738
|
// Check for stop command - execute immediately, don't queue!
|
|
473
|
-
if (slackEvent.text
|
|
739
|
+
if (this.isStopText(slackEvent.text)) {
|
|
474
740
|
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
475
741
|
if (stopTarget) {
|
|
476
742
|
this.handler.handleStop(stopTarget, e.channel, this);
|
|
@@ -478,32 +744,46 @@ export class SlackBot {
|
|
|
478
744
|
else {
|
|
479
745
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
480
746
|
}
|
|
747
|
+
void attachmentsPromise.catch((err) => {
|
|
748
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
749
|
+
});
|
|
481
750
|
ack();
|
|
482
751
|
return;
|
|
483
752
|
}
|
|
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
|
-
}
|
|
753
|
+
this.getQueue(this.resolveQueueKey(e.channel, sessionKey)).enqueue(async () => {
|
|
754
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
755
|
+
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
756
|
+
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
757
|
+
});
|
|
500
758
|
ack();
|
|
501
759
|
});
|
|
502
760
|
// All messages (for logging) + DMs (for triggering)
|
|
503
761
|
this.socketClient.on("message", ({ event, ack }) => {
|
|
504
762
|
const e = event;
|
|
505
|
-
|
|
506
|
-
|
|
763
|
+
const hasFiles = !!e.files && e.files.length > 0;
|
|
764
|
+
const hasSlackContent = !!e.text || hasFiles || !!e.blocks?.length || !!e.attachments?.length;
|
|
765
|
+
const isOwnBotMessage = (!!e.user && e.user === this.botUserId) || (!!this.botId && e.bot_id === this.botId);
|
|
766
|
+
if (isOwnBotMessage) {
|
|
767
|
+
ack();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const isExternalBotMessage = !!e.bot_id || e.subtype === "bot_message";
|
|
771
|
+
if (isExternalBotMessage) {
|
|
772
|
+
if (e.subtype !== undefined && e.subtype !== "bot_message" && e.subtype !== "file_share") {
|
|
773
|
+
ack();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (!hasSlackContent) {
|
|
777
|
+
ack();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
void this.logExternalBotMessage(e).catch((err) => {
|
|
781
|
+
log.logWarning("Failed to log Slack bot message", String(err));
|
|
782
|
+
});
|
|
783
|
+
ack();
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (!e.user) {
|
|
507
787
|
ack();
|
|
508
788
|
return;
|
|
509
789
|
}
|
|
@@ -511,39 +791,44 @@ export class SlackBot {
|
|
|
511
791
|
ack();
|
|
512
792
|
return;
|
|
513
793
|
}
|
|
514
|
-
if (!
|
|
794
|
+
if (!hasSlackContent) {
|
|
515
795
|
ack();
|
|
516
796
|
return;
|
|
517
797
|
}
|
|
518
798
|
const isDM = e.channel_type === "im";
|
|
799
|
+
const conversationKind = isDM ? "direct" : "shared";
|
|
519
800
|
const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
|
|
520
801
|
// Skip channel @mentions - already handled by app_mention event
|
|
521
802
|
if (!isDM && isBotMention) {
|
|
522
803
|
ack();
|
|
523
804
|
return;
|
|
524
805
|
}
|
|
806
|
+
const isSharedThreadReply = !isDM && this.shouldTriggerSharedThreadReply(e.channel, e.thread_ts);
|
|
807
|
+
const sessionKey = isDM || isSharedThreadReply ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
|
|
525
808
|
const slackEvent = {
|
|
526
809
|
type: isDM ? "dm" : "mention",
|
|
810
|
+
conversationId: e.channel,
|
|
811
|
+
conversationKind,
|
|
527
812
|
channel: e.channel,
|
|
528
813
|
ts: e.ts,
|
|
529
814
|
thread_ts: e.thread_ts,
|
|
530
815
|
user: e.user,
|
|
531
|
-
text: (e.text
|
|
816
|
+
text: this.stripOwnMention(e.text),
|
|
532
817
|
files: e.files,
|
|
533
|
-
sessionKey
|
|
818
|
+
sessionKey,
|
|
534
819
|
};
|
|
535
|
-
|
|
536
|
-
// Also downloads attachments in background and stores local paths
|
|
537
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
820
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
538
821
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
539
822
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
540
823
|
log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
|
|
824
|
+
void attachmentsPromise.catch((err) => {
|
|
825
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
826
|
+
});
|
|
541
827
|
ack();
|
|
542
828
|
return;
|
|
543
829
|
}
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
if (!isDM && e.thread_ts && slackEvent.text.toLowerCase().trim() === "stop") {
|
|
830
|
+
// Stop command for DM or shared-channel thread reply (app_mention handles "@mama stop").
|
|
831
|
+
if ((isDM || (!isDM && e.thread_ts)) && this.isStopText(slackEvent.text)) {
|
|
547
832
|
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
548
833
|
if (stopTarget) {
|
|
549
834
|
this.handler.handleStop(stopTarget, e.channel, this);
|
|
@@ -551,41 +836,105 @@ export class SlackBot {
|
|
|
551
836
|
else {
|
|
552
837
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
553
838
|
}
|
|
839
|
+
void attachmentsPromise.catch((err) => {
|
|
840
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
841
|
+
});
|
|
554
842
|
ack();
|
|
555
843
|
return;
|
|
556
844
|
}
|
|
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
|
-
}
|
|
845
|
+
const enqueueTriggered = () => {
|
|
846
|
+
const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
847
|
+
this.getQueue(this.resolveQueueKey(e.channel, activeSessionKey)).enqueue(async () => {
|
|
848
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
849
|
+
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
850
|
+
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
851
|
+
});
|
|
852
|
+
};
|
|
853
|
+
const logOnly = () => {
|
|
854
|
+
void attachmentsPromise.catch((err) => {
|
|
855
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
if (isDM || isSharedThreadReply) {
|
|
859
|
+
enqueueTriggered();
|
|
860
|
+
ack();
|
|
861
|
+
return;
|
|
586
862
|
}
|
|
863
|
+
// Shared-channel non-mention, non-thread: gate via auto-reply policy.
|
|
864
|
+
// evaluateAutoReplyPolicy never throws — judge errors/timeouts surface as
|
|
865
|
+
// trigger:false with a distinct reason, and the user message has already
|
|
866
|
+
// been queued for logging via logUserMessage above.
|
|
867
|
+
evaluateAutoReplyPolicy({
|
|
868
|
+
event: slackEvent,
|
|
869
|
+
workingDir: this.workingDir,
|
|
870
|
+
}).then((triggerResult) => {
|
|
871
|
+
if (triggerResult.trigger)
|
|
872
|
+
enqueueTriggered();
|
|
873
|
+
else
|
|
874
|
+
logOnly();
|
|
875
|
+
});
|
|
587
876
|
ack();
|
|
588
877
|
});
|
|
878
|
+
this.socketClient.on("slash_commands", async ({ body, ack }) => {
|
|
879
|
+
const payload = body;
|
|
880
|
+
await ack();
|
|
881
|
+
if (!payload.command || !payload.channel_id || !payload.user_id) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const handlerPromise = payload.command === "/pi-login"
|
|
885
|
+
? this.routeSlashLoginCommand({
|
|
886
|
+
command: payload.command,
|
|
887
|
+
text: payload.text,
|
|
888
|
+
channel_id: payload.channel_id,
|
|
889
|
+
user_id: payload.user_id,
|
|
890
|
+
user_name: payload.user_name,
|
|
891
|
+
})
|
|
892
|
+
: payload.command === "/pi-new"
|
|
893
|
+
? this.routeSlashNewCommand({
|
|
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-session"
|
|
900
|
+
? this.routeSlashSessionCommand({
|
|
901
|
+
command: payload.command,
|
|
902
|
+
channel_id: payload.channel_id,
|
|
903
|
+
user_id: payload.user_id,
|
|
904
|
+
user_name: payload.user_name,
|
|
905
|
+
})
|
|
906
|
+
: payload.command === "/pi-model"
|
|
907
|
+
? this.routeSlashModelCommand({
|
|
908
|
+
command: payload.command,
|
|
909
|
+
text: payload.text,
|
|
910
|
+
channel_id: payload.channel_id,
|
|
911
|
+
user_id: payload.user_id,
|
|
912
|
+
user_name: payload.user_name,
|
|
913
|
+
})
|
|
914
|
+
: payload.command === "/pi-sandbox"
|
|
915
|
+
? this.routeSlashSandboxCommand({
|
|
916
|
+
command: payload.command,
|
|
917
|
+
text: payload.text,
|
|
918
|
+
channel_id: payload.channel_id,
|
|
919
|
+
user_id: payload.user_id,
|
|
920
|
+
user_name: payload.user_name,
|
|
921
|
+
})
|
|
922
|
+
: payload.command === "/pi-auto-reply"
|
|
923
|
+
? this.routeSlashAutoReplyCommand({
|
|
924
|
+
command: payload.command,
|
|
925
|
+
text: payload.text,
|
|
926
|
+
channel_id: payload.channel_id,
|
|
927
|
+
user_id: payload.user_id,
|
|
928
|
+
user_name: payload.user_name,
|
|
929
|
+
})
|
|
930
|
+
: null;
|
|
931
|
+
if (!handlerPromise) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
handlerPromise.catch((err) => {
|
|
935
|
+
log.logWarning("Slack slash command error", err instanceof Error ? err.message : String(err));
|
|
936
|
+
});
|
|
937
|
+
});
|
|
589
938
|
// App Home tab
|
|
590
939
|
this.socketClient.on("app_home_opened", ({ event, ack }) => {
|
|
591
940
|
const e = event;
|
|
@@ -616,8 +965,7 @@ export class SlackBot {
|
|
|
616
965
|
// Use handler's forceStop method
|
|
617
966
|
this.handler.forceStop(sessionKey);
|
|
618
967
|
// Notify in channel
|
|
619
|
-
|
|
620
|
-
await this.postMessage(channelId, formatForceStopped("slack", actorLabel));
|
|
968
|
+
await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
|
|
621
969
|
// Refresh home tab
|
|
622
970
|
if (userId) {
|
|
623
971
|
this.webClient.views
|
|
@@ -632,15 +980,22 @@ export class SlackBot {
|
|
|
632
980
|
});
|
|
633
981
|
}
|
|
634
982
|
/**
|
|
635
|
-
* Log a user message to log.jsonl
|
|
636
|
-
* Downloads attachments in background via store
|
|
983
|
+
* Log a user message to log.jsonl after attachments are ready.
|
|
637
984
|
*/
|
|
638
|
-
logUserMessage(event) {
|
|
985
|
+
async logUserMessage(event) {
|
|
639
986
|
const user = this.users.get(event.user);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
987
|
+
let attachments = [];
|
|
988
|
+
let attachmentError;
|
|
989
|
+
if (event.files) {
|
|
990
|
+
try {
|
|
991
|
+
attachments = await this.store.processAttachments(event.channel, event.files, event.ts);
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
attachmentError = err;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// Always write the text log, even if attachment processing failed — we want
|
|
998
|
+
// a record of the user message regardless of file-handling errors.
|
|
644
999
|
this.logToFile(event.channel, {
|
|
645
1000
|
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
646
1001
|
ts: event.ts,
|
|
@@ -652,6 +1007,29 @@ export class SlackBot {
|
|
|
652
1007
|
attachments,
|
|
653
1008
|
isBot: false,
|
|
654
1009
|
});
|
|
1010
|
+
if (attachmentError)
|
|
1011
|
+
throw attachmentError;
|
|
1012
|
+
return attachments;
|
|
1013
|
+
}
|
|
1014
|
+
async logExternalBotMessage(event) {
|
|
1015
|
+
const attachments = event.files
|
|
1016
|
+
? await this.store.processAttachments(event.channel, event.files, event.ts)
|
|
1017
|
+
: [];
|
|
1018
|
+
const botName = event.username ?? event.bot_profile?.name ?? event.bot_profile?.real_name ?? event.bot_id;
|
|
1019
|
+
this.logToFile(event.channel, {
|
|
1020
|
+
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
1021
|
+
ts: event.ts,
|
|
1022
|
+
threadTs: event.thread_ts,
|
|
1023
|
+
user: event.bot_id ? `bot:${event.bot_id}` : "external-bot",
|
|
1024
|
+
userName: botName,
|
|
1025
|
+
displayName: botName,
|
|
1026
|
+
text: buildSlackAppMessageText(event),
|
|
1027
|
+
attachments,
|
|
1028
|
+
isBot: true,
|
|
1029
|
+
botId: event.bot_id,
|
|
1030
|
+
appId: event.app_id ?? event.bot_profile?.app_id,
|
|
1031
|
+
subtype: event.subtype,
|
|
1032
|
+
});
|
|
655
1033
|
return attachments;
|
|
656
1034
|
}
|
|
657
1035
|
// ==========================================================================
|
|
@@ -664,13 +1042,15 @@ export class SlackBot {
|
|
|
664
1042
|
return timestamps;
|
|
665
1043
|
const content = await readFile(logPath, "utf-8");
|
|
666
1044
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
667
|
-
for (
|
|
1045
|
+
for (let i = 0; i < lines.length; i++) {
|
|
668
1046
|
try {
|
|
669
|
-
const entry = JSON.parse(
|
|
1047
|
+
const entry = JSON.parse(lines[i]);
|
|
670
1048
|
if (entry.ts)
|
|
671
1049
|
timestamps.add(entry.ts);
|
|
672
1050
|
}
|
|
673
|
-
catch {
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
log.logWarning(`Skipping malformed log entry at ${logPath}:${i + 1}`, err instanceof Error ? err.message : String(err));
|
|
1053
|
+
}
|
|
674
1054
|
}
|
|
675
1055
|
return timestamps;
|
|
676
1056
|
}
|
|
@@ -700,14 +1080,26 @@ export class SlackBot {
|
|
|
700
1080
|
cursor = result.response_metadata?.next_cursor;
|
|
701
1081
|
pageCount++;
|
|
702
1082
|
} while (cursor && pageCount < maxPages);
|
|
703
|
-
// Filter: include mama's messages,
|
|
1083
|
+
// Filter: include mama's messages, external app/bot messages, and user messages.
|
|
704
1084
|
const relevantMessages = allMessages.filter((msg) => {
|
|
705
1085
|
if (!msg.ts || existingTs.has(msg.ts))
|
|
706
1086
|
return false; // Skip duplicates
|
|
707
1087
|
if (msg.user === this.botUserId)
|
|
708
1088
|
return true;
|
|
709
|
-
|
|
710
|
-
|
|
1089
|
+
const isExternalBotMessage = !!msg.bot_id || msg.subtype === "bot_message";
|
|
1090
|
+
if (isExternalBotMessage) {
|
|
1091
|
+
if (this.botId && msg.bot_id === this.botId)
|
|
1092
|
+
return false;
|
|
1093
|
+
if (msg.subtype !== undefined &&
|
|
1094
|
+
msg.subtype !== "bot_message" &&
|
|
1095
|
+
msg.subtype !== "file_share") {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
return (!!msg.text ||
|
|
1099
|
+
!!(msg.files && msg.files.length > 0) ||
|
|
1100
|
+
!!msg.blocks?.length ||
|
|
1101
|
+
!!msg.attachments?.length);
|
|
1102
|
+
}
|
|
711
1103
|
if (msg.subtype !== undefined && msg.subtype !== "file_share")
|
|
712
1104
|
return false;
|
|
713
1105
|
if (!msg.user)
|
|
@@ -721,16 +1113,20 @@ export class SlackBot {
|
|
|
721
1113
|
// Log each message to log.jsonl
|
|
722
1114
|
for (const msg of relevantMessages) {
|
|
723
1115
|
const isMamaMessage = msg.user === this.botUserId;
|
|
1116
|
+
const isExternalBotMessage = !isMamaMessage && (!!msg.bot_id || msg.subtype === "bot_message");
|
|
1117
|
+
if (isExternalBotMessage) {
|
|
1118
|
+
await this.logExternalBotMessage({ ...msg, channel: channelId, ts: msg.ts });
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
724
1121
|
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
|
|
1122
|
+
const text = this.stripOwnMention(msg.text);
|
|
728
1123
|
const attachments = msg.files
|
|
729
|
-
? this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
1124
|
+
? await this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
730
1125
|
: [];
|
|
731
1126
|
this.logToFile(channelId, {
|
|
732
1127
|
date: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
733
1128
|
ts: msg.ts,
|
|
1129
|
+
threadTs: msg.thread_ts,
|
|
734
1130
|
user: isMamaMessage ? "bot" : msg.user,
|
|
735
1131
|
userName: isMamaMessage ? undefined : user?.userName,
|
|
736
1132
|
displayName: isMamaMessage ? undefined : user?.displayName,
|