@geminixiang/mama 0.2.0-beta.0 → 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 +171 -334
- package/dist/adapter.d.ts +36 -10
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +10 -5
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +349 -114
- 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 +102 -31
- 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 +29 -22
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +620 -186
- 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 +136 -71
- 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 +2 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +190 -123
- 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 +57 -59
- 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 -10
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +645 -555
- 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 +53 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +320 -55
- 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 +15 -128
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +16 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +127 -58
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +24 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +115 -0
- package/dist/execution-resolver.js.map +1 -0
- 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 +3 -3
- package/dist/instrument.js.map +1 -1
- package/dist/log.d.ts +3 -7
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +20 -45
- package/dist/log.js.map +1 -1
- package/dist/login/index.d.ts +41 -0
- package/dist/login/index.d.ts.map +1 -0
- package/dist/login/index.js +202 -0
- package/dist/login/index.js.map +1 -0
- package/dist/login/portal.d.ts +19 -0
- 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/login/session.d.ts +33 -0
- package/dist/login/session.d.ts.map +1 -0
- package/dist/login/session.js +68 -0
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +229 -264
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +79 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +437 -0
- package/dist/provisioner.js.map +1 -0
- 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 +16 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +126 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +17 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +212 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +11 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +89 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +22 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +54 -0
- package/dist/sandbox/index.js.map +1 -0
- 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 +67 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +2 -2
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +6 -4
- 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 +35 -8
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +182 -23
- 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 +4 -7
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +26 -52
- 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 +62 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +138 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +8 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- 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 +12 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +36 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +4 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +16 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +72 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +264 -0
- package/dist/vault.js.map +1 -0
- package/package.json +16 -13
|
@@ -1,76 +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
6
|
import * as log from "../../log.js";
|
|
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";
|
|
7
11
|
import { createSlackAdapters } from "./context.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
return await fn();
|
|
19
|
-
}
|
|
20
|
-
catch (err) {
|
|
21
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
22
|
-
// Check for rate limit errors
|
|
23
|
-
let isRateLimited = false;
|
|
24
|
-
// Check for rate_limited error code (Slack SDK)
|
|
25
|
-
if ("code" in lastError && lastError.code === "rate_limited") {
|
|
26
|
-
isRateLimited = true;
|
|
27
|
-
}
|
|
28
|
-
// Check for rate_limited in error response
|
|
29
|
-
if ("data" in lastError) {
|
|
30
|
-
const data = lastError
|
|
31
|
-
.data;
|
|
32
|
-
if (data?.error === "rate_limited" || data?.response?.status === 429) {
|
|
33
|
-
isRateLimited = true;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (isRateLimited) {
|
|
37
|
-
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
38
|
-
log.logWarning(`Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
39
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
// Non-retryable error
|
|
43
|
-
throw lastError;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
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;
|
|
47
21
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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;
|
|
52
31
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
for (const item of value)
|
|
34
|
+
collectSlackText(item, parts);
|
|
35
|
+
return;
|
|
56
36
|
}
|
|
57
|
-
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return;
|
|
63
|
-
this.processing = true;
|
|
64
|
-
const work = this.queue.shift();
|
|
65
|
-
try {
|
|
66
|
-
await work();
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
log.logWarning("Queue error", err instanceof Error ? err.message : String(err));
|
|
70
|
-
}
|
|
71
|
-
this.processing = false;
|
|
72
|
-
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);
|
|
73
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");
|
|
74
54
|
}
|
|
75
55
|
// ============================================================================
|
|
76
56
|
// SlackBot
|
|
@@ -78,6 +58,8 @@ class ChannelQueue {
|
|
|
78
58
|
export class SlackBot {
|
|
79
59
|
constructor(handler, config) {
|
|
80
60
|
this.botUserId = null;
|
|
61
|
+
this.botId = null;
|
|
62
|
+
this.ownMentionRegex = null;
|
|
81
63
|
this.startupTs = null; // Messages older than this are just logged, not processed
|
|
82
64
|
this.users = new Map();
|
|
83
65
|
this.channels = new Map();
|
|
@@ -86,7 +68,12 @@ export class SlackBot {
|
|
|
86
68
|
this.handler = handler;
|
|
87
69
|
this.workingDir = config.workingDir;
|
|
88
70
|
this.store = config.store;
|
|
89
|
-
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
|
+
});
|
|
90
77
|
this.webClient = new WebClient(config.botToken);
|
|
91
78
|
}
|
|
92
79
|
setEventsWatcher(watcher) {
|
|
@@ -98,6 +85,7 @@ export class SlackBot {
|
|
|
98
85
|
async start() {
|
|
99
86
|
const auth = await this.webClient.auth.test();
|
|
100
87
|
this.botUserId = auth.user_id;
|
|
88
|
+
this.botId = typeof auth.bot_id === "string" ? auth.bot_id : null;
|
|
101
89
|
await Promise.all([this.fetchUsers(), this.fetchChannels()]);
|
|
102
90
|
log.logInfo(`Loaded ${this.channels.size} channels, ${this.users.size} users`);
|
|
103
91
|
await this.backfillAllChannels();
|
|
@@ -105,7 +93,7 @@ export class SlackBot {
|
|
|
105
93
|
await this.socketClient.start();
|
|
106
94
|
// Record startup time - messages older than this are just logged, not processed
|
|
107
95
|
this.startupTs = (Date.now() / 1000).toFixed(6);
|
|
108
|
-
log.logConnected();
|
|
96
|
+
log.logConnected("Slack");
|
|
109
97
|
}
|
|
110
98
|
getUser(userId) {
|
|
111
99
|
return this.users.get(userId);
|
|
@@ -119,19 +107,74 @@ export class SlackBot {
|
|
|
119
107
|
getAllChannels() {
|
|
120
108
|
return Array.from(this.channels.values());
|
|
121
109
|
}
|
|
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
|
+
}
|
|
122
119
|
async postMessage(channel, text) {
|
|
123
|
-
return
|
|
120
|
+
return slackRetry(async () => {
|
|
124
121
|
const result = await this.webClient.chat.postMessage({ channel, text });
|
|
125
122
|
return result.ts;
|
|
126
123
|
});
|
|
127
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
|
+
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
|
+
}
|
|
128
171
|
async updateMessage(channel, ts, text) {
|
|
129
|
-
return
|
|
172
|
+
return slackRetry(async () => {
|
|
130
173
|
await this.webClient.chat.update({ channel, ts, text });
|
|
131
174
|
});
|
|
132
175
|
}
|
|
133
176
|
async deleteMessage(channel, ts) {
|
|
134
|
-
return
|
|
177
|
+
return slackRetry(async () => {
|
|
135
178
|
await this.webClient.chat.delete({ channel, ts });
|
|
136
179
|
});
|
|
137
180
|
}
|
|
@@ -140,7 +183,7 @@ export class SlackBot {
|
|
|
140
183
|
// ==========================================================================
|
|
141
184
|
/** Set the status for an assistant thread (shows "thinking" state) */
|
|
142
185
|
async setAssistantStatus(channel, threadTs, status) {
|
|
143
|
-
return
|
|
186
|
+
return slackRetry(async () => {
|
|
144
187
|
await this.webClient.assistant.threads.setStatus({
|
|
145
188
|
channel_id: channel,
|
|
146
189
|
thread_ts: threadTs,
|
|
@@ -149,7 +192,7 @@ export class SlackBot {
|
|
|
149
192
|
});
|
|
150
193
|
}
|
|
151
194
|
async postInThread(channel, threadTs, text) {
|
|
152
|
-
return
|
|
195
|
+
return slackRetry(async () => {
|
|
153
196
|
// Use Block Kit section for long messages to trigger Slack's "Show more" collapsing (~700 chars)
|
|
154
197
|
const SECTION_TEXT_LIMIT = 3000;
|
|
155
198
|
if (text.length > 500) {
|
|
@@ -169,7 +212,7 @@ export class SlackBot {
|
|
|
169
212
|
});
|
|
170
213
|
}
|
|
171
214
|
async postInThreadBlocks(channel, threadTs, text, blocks) {
|
|
172
|
-
return
|
|
215
|
+
return slackRetry(async () => {
|
|
173
216
|
const result = await this.webClient.chat.postMessage({
|
|
174
217
|
channel,
|
|
175
218
|
thread_ts: threadTs,
|
|
@@ -180,7 +223,7 @@ export class SlackBot {
|
|
|
180
223
|
});
|
|
181
224
|
}
|
|
182
225
|
async uploadFile(channel, filePath, title, threadTs) {
|
|
183
|
-
return
|
|
226
|
+
return slackRetry(async () => {
|
|
184
227
|
const fileName = title || basename(filePath);
|
|
185
228
|
const fileContent = readFileSync(filePath);
|
|
186
229
|
await this.webClient.files.uploadV2({
|
|
@@ -192,29 +235,32 @@ export class SlackBot {
|
|
|
192
235
|
});
|
|
193
236
|
});
|
|
194
237
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Log a message to log.jsonl (SYNC)
|
|
197
|
-
* This is the ONLY place messages are written to log.jsonl
|
|
198
|
-
*/
|
|
199
238
|
logToFile(channel, entry) {
|
|
200
|
-
|
|
201
|
-
if (!existsSync(dir))
|
|
202
|
-
mkdirSync(dir, { recursive: true });
|
|
203
|
-
appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
239
|
+
appendChannelLog(this.workingDir, channel, entry);
|
|
204
240
|
}
|
|
205
|
-
/**
|
|
206
|
-
* Log a bot response to log.jsonl
|
|
207
|
-
*/
|
|
208
241
|
logBotResponse(channel, text, ts, threadTs) {
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
264
|
}
|
|
219
265
|
getPlatformInfo() {
|
|
220
266
|
return {
|
|
@@ -226,6 +272,9 @@ export class SlackBot {
|
|
|
226
272
|
userName: u.userName,
|
|
227
273
|
displayName: u.displayName,
|
|
228
274
|
})),
|
|
275
|
+
diagnostics: {
|
|
276
|
+
showUsageSummary: true,
|
|
277
|
+
},
|
|
229
278
|
};
|
|
230
279
|
}
|
|
231
280
|
// ==========================================================================
|
|
@@ -236,14 +285,30 @@ export class SlackBot {
|
|
|
236
285
|
* Returns true if enqueued, false if queue is full (max 5).
|
|
237
286
|
*/
|
|
238
287
|
enqueueEvent(event) {
|
|
239
|
-
const
|
|
288
|
+
const conversationId = event.conversationId;
|
|
289
|
+
const queue = this.getQueue(conversationId);
|
|
240
290
|
if (queue.size() >= 5) {
|
|
241
|
-
log.logWarning(`Event queue full for ${
|
|
291
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
242
292
|
return false;
|
|
243
293
|
}
|
|
244
|
-
log.logInfo(`Enqueueing event for ${
|
|
294
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
245
295
|
queue.enqueue(() => {
|
|
246
|
-
const
|
|
296
|
+
const slackEvent = {
|
|
297
|
+
type: event.type,
|
|
298
|
+
conversationId,
|
|
299
|
+
conversationKind: event.conversationKind,
|
|
300
|
+
channel: conversationId,
|
|
301
|
+
ts: event.ts,
|
|
302
|
+
thread_ts: event.thread_ts,
|
|
303
|
+
user: event.user,
|
|
304
|
+
text: event.text,
|
|
305
|
+
attachments: event.attachments?.map((attachment) => ({
|
|
306
|
+
original: attachment.name,
|
|
307
|
+
localPath: attachment.localPath,
|
|
308
|
+
})),
|
|
309
|
+
sessionKey: event.sessionKey,
|
|
310
|
+
};
|
|
311
|
+
const adapters = createSlackAdapters(slackEvent, this, true);
|
|
247
312
|
return this.handler.handleEvent(event, this, adapters, true);
|
|
248
313
|
});
|
|
249
314
|
return true;
|
|
@@ -254,11 +319,28 @@ export class SlackBot {
|
|
|
254
319
|
getQueue(channelId) {
|
|
255
320
|
let queue = this.queues.get(channelId);
|
|
256
321
|
if (!queue) {
|
|
257
|
-
queue = new ChannelQueue();
|
|
322
|
+
queue = new ChannelQueue("Slack");
|
|
258
323
|
this.queues.set(channelId, queue);
|
|
259
324
|
}
|
|
260
325
|
return queue;
|
|
261
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
|
+
}
|
|
262
344
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
263
345
|
buildHomeView() {
|
|
264
346
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -267,12 +349,12 @@ export class SlackBot {
|
|
|
267
349
|
type: "section",
|
|
268
350
|
text: {
|
|
269
351
|
type: "mrkdwn",
|
|
270
|
-
text:
|
|
352
|
+
text: `*${PRODUCT_NAME}*\nStart a new task or check on running work.`,
|
|
271
353
|
},
|
|
272
354
|
accessory: {
|
|
273
355
|
type: "image",
|
|
274
356
|
image_url: "https://media1.tenor.com/m/lfDATg4Bhc0AAAAC/happy-cat.gif",
|
|
275
|
-
alt_text:
|
|
357
|
+
alt_text: PRODUCT_NAME,
|
|
276
358
|
},
|
|
277
359
|
},
|
|
278
360
|
];
|
|
@@ -364,11 +446,11 @@ export class SlackBot {
|
|
|
364
446
|
for (const ev of periodicEvents) {
|
|
365
447
|
const channelLabel = ev.platform === "slack"
|
|
366
448
|
? (() => {
|
|
367
|
-
const channel = this.channels.get(ev.
|
|
368
|
-
const channelName = channel ? `#${channel.name}` : ev.
|
|
449
|
+
const channel = this.channels.get(ev.conversationId);
|
|
450
|
+
const channelName = channel ? `#${channel.name}` : ev.conversationId;
|
|
369
451
|
return `${ev.platform}:${channelName}`;
|
|
370
452
|
})()
|
|
371
|
-
: `${ev.platform}:${ev.
|
|
453
|
+
: `${ev.platform}:${ev.conversationId}`;
|
|
372
454
|
const nextStr = ev.nextRun
|
|
373
455
|
? new Date(ev.nextRun).toLocaleString("en-US", {
|
|
374
456
|
month: "short",
|
|
@@ -395,25 +477,231 @@ export class SlackBot {
|
|
|
395
477
|
});
|
|
396
478
|
return { type: "home", blocks };
|
|
397
479
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Resolve which session key to stop.
|
|
400
|
-
* When stop is called from a thread, the thread session (channelId:thread_ts) might
|
|
401
|
-
* not be running — but the channel session (channelId) might be, because the bot's
|
|
402
|
-
* reply to a top-level mention creates a thread. Check both, prefer thread first.
|
|
403
|
-
*/
|
|
404
480
|
resolveStopTarget(channelId, threadTs) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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)
|
|
412
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} 的登入連結,請到私訊完成設定。`);
|
|
413
592
|
}
|
|
414
|
-
|
|
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);
|
|
415
694
|
}
|
|
416
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
|
+
});
|
|
417
705
|
// Channel @mentions
|
|
418
706
|
this.socketClient.on("app_mention", ({ event, ack }) => {
|
|
419
707
|
const e = event;
|
|
@@ -424,55 +712,78 @@ export class SlackBot {
|
|
|
424
712
|
}
|
|
425
713
|
// Top-level mentions use a persistent channel session.
|
|
426
714
|
// Thread replies get their own isolated session (channelId:thread_ts).
|
|
427
|
-
const sessionKey = e.
|
|
715
|
+
const sessionKey = resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
428
716
|
const slackEvent = {
|
|
429
717
|
type: "mention",
|
|
718
|
+
conversationId: e.channel,
|
|
719
|
+
conversationKind: "shared",
|
|
430
720
|
channel: e.channel,
|
|
431
721
|
ts: e.ts,
|
|
432
722
|
thread_ts: e.thread_ts,
|
|
433
723
|
user: e.user,
|
|
434
|
-
text: e.text
|
|
724
|
+
text: this.stripOwnMention(e.text),
|
|
435
725
|
files: e.files,
|
|
436
726
|
sessionKey,
|
|
437
727
|
};
|
|
438
|
-
|
|
439
|
-
// Also downloads attachments in background and stores local paths
|
|
440
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
728
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
441
729
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
442
730
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
443
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
|
+
});
|
|
444
735
|
ack();
|
|
445
736
|
return;
|
|
446
737
|
}
|
|
447
738
|
// Check for stop command - execute immediately, don't queue!
|
|
448
|
-
if (slackEvent.text
|
|
739
|
+
if (this.isStopText(slackEvent.text)) {
|
|
449
740
|
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
450
741
|
if (stopTarget) {
|
|
451
742
|
this.handler.handleStop(stopTarget, e.channel, this);
|
|
452
743
|
}
|
|
453
744
|
else {
|
|
454
|
-
this.postMessage(e.channel, "
|
|
745
|
+
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
455
746
|
}
|
|
747
|
+
void attachmentsPromise.catch((err) => {
|
|
748
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
749
|
+
});
|
|
456
750
|
ack();
|
|
457
751
|
return;
|
|
458
752
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
this.getQueue(sessionKey).enqueue(() => {
|
|
465
|
-
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
466
|
-
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
467
|
-
});
|
|
468
|
-
}
|
|
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
|
+
});
|
|
469
758
|
ack();
|
|
470
759
|
});
|
|
471
760
|
// All messages (for logging) + DMs (for triggering)
|
|
472
761
|
this.socketClient.on("message", ({ event, ack }) => {
|
|
473
762
|
const e = event;
|
|
474
|
-
|
|
475
|
-
|
|
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) {
|
|
476
787
|
ack();
|
|
477
788
|
return;
|
|
478
789
|
}
|
|
@@ -480,75 +791,150 @@ export class SlackBot {
|
|
|
480
791
|
ack();
|
|
481
792
|
return;
|
|
482
793
|
}
|
|
483
|
-
if (!
|
|
794
|
+
if (!hasSlackContent) {
|
|
484
795
|
ack();
|
|
485
796
|
return;
|
|
486
797
|
}
|
|
487
798
|
const isDM = e.channel_type === "im";
|
|
799
|
+
const conversationKind = isDM ? "direct" : "shared";
|
|
488
800
|
const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
|
|
489
801
|
// Skip channel @mentions - already handled by app_mention event
|
|
490
802
|
if (!isDM && isBotMention) {
|
|
491
803
|
ack();
|
|
492
804
|
return;
|
|
493
805
|
}
|
|
806
|
+
const isSharedThreadReply = !isDM && this.shouldTriggerSharedThreadReply(e.channel, e.thread_ts);
|
|
807
|
+
const sessionKey = isDM || isSharedThreadReply ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
|
|
494
808
|
const slackEvent = {
|
|
495
809
|
type: isDM ? "dm" : "mention",
|
|
810
|
+
conversationId: e.channel,
|
|
811
|
+
conversationKind,
|
|
496
812
|
channel: e.channel,
|
|
497
813
|
ts: e.ts,
|
|
498
814
|
thread_ts: e.thread_ts,
|
|
499
815
|
user: e.user,
|
|
500
|
-
text: (e.text
|
|
816
|
+
text: this.stripOwnMention(e.text),
|
|
501
817
|
files: e.files,
|
|
502
|
-
sessionKey
|
|
818
|
+
sessionKey,
|
|
503
819
|
};
|
|
504
|
-
|
|
505
|
-
// Also downloads attachments in background and stores local paths
|
|
506
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
820
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
507
821
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
508
822
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
509
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
|
+
});
|
|
510
827
|
ack();
|
|
511
828
|
return;
|
|
512
829
|
}
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
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)) {
|
|
516
832
|
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
517
833
|
if (stopTarget) {
|
|
518
834
|
this.handler.handleStop(stopTarget, e.channel, this);
|
|
519
835
|
}
|
|
520
836
|
else {
|
|
521
|
-
this.postMessage(e.channel, "
|
|
837
|
+
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
522
838
|
}
|
|
839
|
+
void attachmentsPromise.catch((err) => {
|
|
840
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
841
|
+
});
|
|
523
842
|
ack();
|
|
524
843
|
return;
|
|
525
844
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
else {
|
|
544
|
-
this.getQueue(dmSessionKey).enqueue(() => {
|
|
545
|
-
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
546
|
-
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
547
|
-
});
|
|
548
|
-
}
|
|
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;
|
|
549
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
|
+
});
|
|
550
876
|
ack();
|
|
551
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
|
+
});
|
|
552
938
|
// App Home tab
|
|
553
939
|
this.socketClient.on("app_home_opened", ({ event, ack }) => {
|
|
554
940
|
const e = event;
|
|
@@ -579,7 +965,7 @@ export class SlackBot {
|
|
|
579
965
|
// Use handler's forceStop method
|
|
580
966
|
this.handler.forceStop(sessionKey);
|
|
581
967
|
// Notify in channel
|
|
582
|
-
await this.postMessage(channelId,
|
|
968
|
+
await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
|
|
583
969
|
// Refresh home tab
|
|
584
970
|
if (userId) {
|
|
585
971
|
this.webClient.views
|
|
@@ -594,15 +980,22 @@ export class SlackBot {
|
|
|
594
980
|
});
|
|
595
981
|
}
|
|
596
982
|
/**
|
|
597
|
-
* Log a user message to log.jsonl
|
|
598
|
-
* Downloads attachments in background via store
|
|
983
|
+
* Log a user message to log.jsonl after attachments are ready.
|
|
599
984
|
*/
|
|
600
|
-
logUserMessage(event) {
|
|
985
|
+
async logUserMessage(event) {
|
|
601
986
|
const user = this.users.get(event.user);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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.
|
|
606
999
|
this.logToFile(event.channel, {
|
|
607
1000
|
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
608
1001
|
ts: event.ts,
|
|
@@ -614,6 +1007,29 @@ export class SlackBot {
|
|
|
614
1007
|
attachments,
|
|
615
1008
|
isBot: false,
|
|
616
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
|
+
});
|
|
617
1033
|
return attachments;
|
|
618
1034
|
}
|
|
619
1035
|
// ==========================================================================
|
|
@@ -626,13 +1042,15 @@ export class SlackBot {
|
|
|
626
1042
|
return timestamps;
|
|
627
1043
|
const content = await readFile(logPath, "utf-8");
|
|
628
1044
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
629
|
-
for (
|
|
1045
|
+
for (let i = 0; i < lines.length; i++) {
|
|
630
1046
|
try {
|
|
631
|
-
const entry = JSON.parse(
|
|
1047
|
+
const entry = JSON.parse(lines[i]);
|
|
632
1048
|
if (entry.ts)
|
|
633
1049
|
timestamps.add(entry.ts);
|
|
634
1050
|
}
|
|
635
|
-
catch {
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
log.logWarning(`Skipping malformed log entry at ${logPath}:${i + 1}`, err instanceof Error ? err.message : String(err));
|
|
1053
|
+
}
|
|
636
1054
|
}
|
|
637
1055
|
return timestamps;
|
|
638
1056
|
}
|
|
@@ -662,14 +1080,26 @@ export class SlackBot {
|
|
|
662
1080
|
cursor = result.response_metadata?.next_cursor;
|
|
663
1081
|
pageCount++;
|
|
664
1082
|
} while (cursor && pageCount < maxPages);
|
|
665
|
-
// Filter: include mama's messages,
|
|
1083
|
+
// Filter: include mama's messages, external app/bot messages, and user messages.
|
|
666
1084
|
const relevantMessages = allMessages.filter((msg) => {
|
|
667
1085
|
if (!msg.ts || existingTs.has(msg.ts))
|
|
668
1086
|
return false; // Skip duplicates
|
|
669
1087
|
if (msg.user === this.botUserId)
|
|
670
1088
|
return true;
|
|
671
|
-
|
|
672
|
-
|
|
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
|
+
}
|
|
673
1103
|
if (msg.subtype !== undefined && msg.subtype !== "file_share")
|
|
674
1104
|
return false;
|
|
675
1105
|
if (!msg.user)
|
|
@@ -683,16 +1113,20 @@ export class SlackBot {
|
|
|
683
1113
|
// Log each message to log.jsonl
|
|
684
1114
|
for (const msg of relevantMessages) {
|
|
685
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
|
+
}
|
|
686
1121
|
const user = this.users.get(msg.user);
|
|
687
|
-
|
|
688
|
-
const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
|
|
689
|
-
// Process attachments - queues downloads in background
|
|
1122
|
+
const text = this.stripOwnMention(msg.text);
|
|
690
1123
|
const attachments = msg.files
|
|
691
|
-
? this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
1124
|
+
? await this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
692
1125
|
: [];
|
|
693
1126
|
this.logToFile(channelId, {
|
|
694
1127
|
date: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
695
1128
|
ts: msg.ts,
|
|
1129
|
+
threadTs: msg.thread_ts,
|
|
696
1130
|
user: isMamaMessage ? "bot" : msg.user,
|
|
697
1131
|
userName: isMamaMessage ? undefined : user?.userName,
|
|
698
1132
|
displayName: isMamaMessage ? undefined : user?.displayName,
|