@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.3
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 +133 -78
- package/dist/adapter.d.ts +22 -10
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +10 -7
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +228 -69
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +92 -34
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +23 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +57 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +19 -11
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +356 -96
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +21 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +96 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +100 -67
- 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/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +141 -74
- 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 +49 -109
- 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 +4 -11
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +116 -196
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +1 -20
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +1 -21
- package/dist/bindings.js.map +1 -1
- package/dist/config.d.ts +9 -27
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +89 -63
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +13 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +102 -18
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +18 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +86 -35
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +1 -3
- package/dist/execution-resolver.js.map +1 -1
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +5 -11
- package/dist/instrument.js.map +1 -1
- package/dist/{login.d.ts → login/index.d.ts} +2 -2
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +2 -2
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +6 -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} +1 -1
- 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 +175 -119
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +17 -43
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +84 -50
- package/dist/provisioner.js.map +1 -1
- package/dist/sandbox/host.d.ts +0 -2
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +1 -5
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -0
- 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 +27 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +162 -9
- 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 +9 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +766 -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 +380 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +16 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +38 -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 +15 -35
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +3 -0
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +27 -8
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- 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 -2
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +1 -7
- package/dist/vault-routing.js.map +1 -1
- package/package.json +1 -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
|
@@ -3,10 +3,11 @@ import { WebClient } from "@slack/web-api";
|
|
|
3
3
|
import { appendFileSync, existsSync, mkdirSync, 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";
|
|
9
8
|
import { createSlackAdapters } from "./context.js";
|
|
9
|
+
import { hasMaterializedSlackBranchSession } from "./branch-manager.js";
|
|
10
|
+
import { resolveSlackSessionKey } from "./session.js";
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Exponential backoff utility for Slack API calls
|
|
12
13
|
// ============================================================================
|
|
@@ -94,21 +95,6 @@ export class SlackBot {
|
|
|
94
95
|
setEventsWatcher(watcher) {
|
|
95
96
|
this.eventsWatcher = watcher;
|
|
96
97
|
}
|
|
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
98
|
// ==========================================================================
|
|
113
99
|
// Public API
|
|
114
100
|
// ==========================================================================
|
|
@@ -136,15 +122,30 @@ export class SlackBot {
|
|
|
136
122
|
getAllChannels() {
|
|
137
123
|
return Array.from(this.channels.values());
|
|
138
124
|
}
|
|
139
|
-
async postMessage(
|
|
125
|
+
async postMessage(channel, text) {
|
|
140
126
|
return withRetry(async () => {
|
|
141
|
-
const result = await this.webClient.chat.postMessage({ channel
|
|
127
|
+
const result = await this.webClient.chat.postMessage({ channel, text });
|
|
142
128
|
return result.ts;
|
|
143
129
|
});
|
|
144
130
|
}
|
|
145
|
-
async
|
|
131
|
+
async postEphemeral(channel, user, text) {
|
|
132
|
+
return withRetry(async () => {
|
|
133
|
+
await this.webClient.chat.postEphemeral({ channel, user, text });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async openDirectMessage(userId) {
|
|
137
|
+
return withRetry(async () => {
|
|
138
|
+
const result = await this.webClient.conversations.open({ users: userId });
|
|
139
|
+
const channelId = result.channel?.id;
|
|
140
|
+
if (!channelId) {
|
|
141
|
+
throw new Error(`Failed to open DM for user ${userId}`);
|
|
142
|
+
}
|
|
143
|
+
return channelId;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async updateMessage(channel, ts, text) {
|
|
146
147
|
return withRetry(async () => {
|
|
147
|
-
await this.webClient.chat.update({ channel
|
|
148
|
+
await this.webClient.chat.update({ channel, ts, text });
|
|
148
149
|
});
|
|
149
150
|
}
|
|
150
151
|
async deleteMessage(channel, ts) {
|
|
@@ -214,10 +215,10 @@ export class SlackBot {
|
|
|
214
215
|
* This is the ONLY place messages are written to log.jsonl
|
|
215
216
|
*/
|
|
216
217
|
logToFile(channel, entry) {
|
|
217
|
-
const
|
|
218
|
-
if (!existsSync(
|
|
219
|
-
mkdirSync(
|
|
220
|
-
appendFileSync(join(
|
|
218
|
+
const dir = join(this.workingDir, channel);
|
|
219
|
+
if (!existsSync(dir))
|
|
220
|
+
mkdirSync(dir, { recursive: true });
|
|
221
|
+
appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
221
222
|
}
|
|
222
223
|
/**
|
|
223
224
|
* Log a bot response to log.jsonl
|
|
@@ -243,6 +244,9 @@ export class SlackBot {
|
|
|
243
244
|
userName: u.userName,
|
|
244
245
|
displayName: u.displayName,
|
|
245
246
|
})),
|
|
247
|
+
diagnostics: {
|
|
248
|
+
showUsageSummary: true,
|
|
249
|
+
},
|
|
246
250
|
};
|
|
247
251
|
}
|
|
248
252
|
// ==========================================================================
|
|
@@ -253,20 +257,27 @@ export class SlackBot {
|
|
|
253
257
|
* Returns true if enqueued, false if queue is full (max 5).
|
|
254
258
|
*/
|
|
255
259
|
enqueueEvent(event) {
|
|
256
|
-
const
|
|
260
|
+
const conversationId = event.conversationId;
|
|
261
|
+
const queue = this.getQueue(conversationId);
|
|
257
262
|
if (queue.size() >= 5) {
|
|
258
|
-
log.logWarning(`Event queue full for ${
|
|
263
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
259
264
|
return false;
|
|
260
265
|
}
|
|
261
|
-
log.logInfo(`Enqueueing event for ${
|
|
266
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
262
267
|
queue.enqueue(() => {
|
|
263
268
|
const slackEvent = {
|
|
264
|
-
type:
|
|
265
|
-
|
|
269
|
+
type: event.type,
|
|
270
|
+
conversationId,
|
|
271
|
+
conversationKind: event.conversationKind,
|
|
272
|
+
channel: conversationId,
|
|
266
273
|
ts: event.ts,
|
|
267
274
|
thread_ts: event.thread_ts,
|
|
268
275
|
user: event.user,
|
|
269
276
|
text: event.text,
|
|
277
|
+
attachments: event.attachments?.map((attachment) => ({
|
|
278
|
+
original: attachment.name,
|
|
279
|
+
localPath: attachment.localPath,
|
|
280
|
+
})),
|
|
270
281
|
sessionKey: event.sessionKey,
|
|
271
282
|
};
|
|
272
283
|
const adapters = createSlackAdapters(slackEvent, this, true);
|
|
@@ -285,6 +296,13 @@ export class SlackBot {
|
|
|
285
296
|
}
|
|
286
297
|
return queue;
|
|
287
298
|
}
|
|
299
|
+
resolveQueueKey(conversationId, sessionKey) {
|
|
300
|
+
if (!sessionKey.includes(":"))
|
|
301
|
+
return sessionKey;
|
|
302
|
+
return hasMaterializedSlackBranchSession(join(this.workingDir, conversationId), sessionKey)
|
|
303
|
+
? sessionKey
|
|
304
|
+
: conversationId;
|
|
305
|
+
}
|
|
288
306
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
307
|
buildHomeView() {
|
|
290
308
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -387,21 +405,22 @@ export class SlackBot {
|
|
|
387
405
|
});
|
|
388
406
|
}
|
|
389
407
|
else {
|
|
390
|
-
const timestampFormatter = new Intl.DateTimeFormat(undefined, {
|
|
391
|
-
month: "short",
|
|
392
|
-
day: "numeric",
|
|
393
|
-
hour: "2-digit",
|
|
394
|
-
minute: "2-digit",
|
|
395
|
-
});
|
|
396
408
|
for (const ev of periodicEvents) {
|
|
397
409
|
const channelLabel = ev.platform === "slack"
|
|
398
410
|
? (() => {
|
|
399
|
-
const channel = this.channels.get(ev.
|
|
400
|
-
const channelName = channel ? `#${channel.name}` : ev.
|
|
411
|
+
const channel = this.channels.get(ev.conversationId);
|
|
412
|
+
const channelName = channel ? `#${channel.name}` : ev.conversationId;
|
|
401
413
|
return `${ev.platform}:${channelName}`;
|
|
402
414
|
})()
|
|
403
|
-
: `${ev.platform}:${ev.
|
|
404
|
-
const nextStr = ev.nextRun
|
|
415
|
+
: `${ev.platform}:${ev.conversationId}`;
|
|
416
|
+
const nextStr = ev.nextRun
|
|
417
|
+
? new Date(ev.nextRun).toLocaleString("en-US", {
|
|
418
|
+
month: "short",
|
|
419
|
+
day: "numeric",
|
|
420
|
+
hour: "2-digit",
|
|
421
|
+
minute: "2-digit",
|
|
422
|
+
})
|
|
423
|
+
: "—";
|
|
405
424
|
blocks.push({
|
|
406
425
|
type: "section",
|
|
407
426
|
text: {
|
|
@@ -425,18 +444,223 @@ export class SlackBot {
|
|
|
425
444
|
* When stop is called from a thread, the thread session (channelId:thread_ts) might
|
|
426
445
|
* not be running — but the channel session (channelId) might be, because the bot's
|
|
427
446
|
* reply to a top-level mention creates a thread. Check both, prefer thread first.
|
|
447
|
+
*
|
|
448
|
+
* For top-level stop requests, fall back to the only running scoped thread session in
|
|
449
|
+
* this conversation when there is exactly one candidate. This keeps DM/channel stop
|
|
450
|
+
* useful after thread sessions were introduced without guessing between multiple threads.
|
|
428
451
|
*/
|
|
429
452
|
resolveStopTarget(channelId, threadTs) {
|
|
430
453
|
if (threadTs) {
|
|
431
|
-
const threadKey =
|
|
454
|
+
const threadKey = resolveSlackSessionKey(channelId, threadTs);
|
|
432
455
|
if (this.handler.isRunning(threadKey))
|
|
433
456
|
return threadKey;
|
|
434
|
-
// Fall back to channel session — the thread may have been spawned by a top-level run
|
|
435
457
|
if (this.handler.isRunning(channelId))
|
|
436
458
|
return channelId;
|
|
437
459
|
return null;
|
|
438
460
|
}
|
|
439
|
-
|
|
461
|
+
if (this.handler.isRunning(channelId))
|
|
462
|
+
return channelId;
|
|
463
|
+
const runningInConversation = this.handler
|
|
464
|
+
.getRunningSessions()
|
|
465
|
+
.map((session) => session.sessionKey)
|
|
466
|
+
.filter((sessionKey) => sessionKey.startsWith(`${channelId}:`));
|
|
467
|
+
return runningInConversation.length === 1 ? runningInConversation[0] : null;
|
|
468
|
+
}
|
|
469
|
+
createDirectCommandAdapters(conversationId, userId, userName, text, ts) {
|
|
470
|
+
const message = {
|
|
471
|
+
id: ts,
|
|
472
|
+
sessionKey: conversationId,
|
|
473
|
+
conversationKind: "direct",
|
|
474
|
+
userId,
|
|
475
|
+
userName,
|
|
476
|
+
text,
|
|
477
|
+
attachments: [],
|
|
478
|
+
};
|
|
479
|
+
const responseCtx = {
|
|
480
|
+
respond: async (responseText) => {
|
|
481
|
+
const messageTs = await this.postMessage(conversationId, responseText);
|
|
482
|
+
this.logBotResponse(conversationId, responseText, messageTs);
|
|
483
|
+
},
|
|
484
|
+
replaceResponse: async (responseText) => {
|
|
485
|
+
const messageTs = await this.postMessage(conversationId, responseText);
|
|
486
|
+
this.logBotResponse(conversationId, responseText, messageTs);
|
|
487
|
+
},
|
|
488
|
+
respondDiagnostic: async (responseText) => {
|
|
489
|
+
const messageTs = await this.postMessage(conversationId, responseText);
|
|
490
|
+
this.logBotResponse(conversationId, responseText, messageTs);
|
|
491
|
+
},
|
|
492
|
+
respondToolResult: async (result) => {
|
|
493
|
+
const duration = (result.durationMs / 1000).toFixed(1);
|
|
494
|
+
const text = `${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`;
|
|
495
|
+
const messageTs = await this.postMessage(conversationId, text);
|
|
496
|
+
this.logBotResponse(conversationId, text, messageTs);
|
|
497
|
+
},
|
|
498
|
+
setTyping: async () => { },
|
|
499
|
+
setWorking: async () => { },
|
|
500
|
+
uploadFile: async (filePath, title) => {
|
|
501
|
+
await this.uploadFile(conversationId, filePath, title);
|
|
502
|
+
},
|
|
503
|
+
deleteResponse: async () => { },
|
|
504
|
+
};
|
|
505
|
+
return {
|
|
506
|
+
message,
|
|
507
|
+
responseCtx,
|
|
508
|
+
platform: this.getPlatformInfo(),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
createPrivateSessionCommandAdapters(conversationId, userId, userName, text, ts, options) {
|
|
512
|
+
const message = {
|
|
513
|
+
id: ts,
|
|
514
|
+
sessionKey: conversationId,
|
|
515
|
+
conversationKind: options.ephemeralChannelId ? "shared" : "direct",
|
|
516
|
+
userId,
|
|
517
|
+
userName,
|
|
518
|
+
text,
|
|
519
|
+
attachments: [],
|
|
520
|
+
};
|
|
521
|
+
let hasResponded = false;
|
|
522
|
+
const respondPrivately = async (responseText) => {
|
|
523
|
+
if (options.ephemeralChannelId) {
|
|
524
|
+
await this.postEphemeral(options.ephemeralChannelId, userId, responseText);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const messageTs = await this.postMessage(conversationId, responseText);
|
|
528
|
+
this.logBotResponse(conversationId, responseText, messageTs);
|
|
529
|
+
};
|
|
530
|
+
const responseCtx = {
|
|
531
|
+
respond: async (responseText) => {
|
|
532
|
+
hasResponded = true;
|
|
533
|
+
await respondPrivately(responseText);
|
|
534
|
+
},
|
|
535
|
+
replaceResponse: async (responseText) => {
|
|
536
|
+
if (!hasResponded) {
|
|
537
|
+
hasResponded = true;
|
|
538
|
+
}
|
|
539
|
+
await respondPrivately(responseText);
|
|
540
|
+
},
|
|
541
|
+
respondDiagnostic: async (responseText) => {
|
|
542
|
+
await respondPrivately(responseText);
|
|
543
|
+
},
|
|
544
|
+
respondToolResult: async (result) => {
|
|
545
|
+
const duration = (result.durationMs / 1000).toFixed(1);
|
|
546
|
+
const formatted = `${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`;
|
|
547
|
+
await respondPrivately(formatted);
|
|
548
|
+
},
|
|
549
|
+
setTyping: async () => { },
|
|
550
|
+
setWorking: async () => { },
|
|
551
|
+
uploadFile: async (filePath, title) => {
|
|
552
|
+
await this.uploadFile(conversationId, filePath, title);
|
|
553
|
+
},
|
|
554
|
+
deleteResponse: async () => { },
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
message,
|
|
558
|
+
responseCtx,
|
|
559
|
+
platform: this.getPlatformInfo(),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
createSlashCommandBot(conversationId, threadTs) {
|
|
563
|
+
return {
|
|
564
|
+
start: async () => { },
|
|
565
|
+
postMessage: async (_channel, text) => {
|
|
566
|
+
if (threadTs) {
|
|
567
|
+
return this.postInThread(conversationId, threadTs, text);
|
|
568
|
+
}
|
|
569
|
+
return this.postMessage(conversationId, text);
|
|
570
|
+
},
|
|
571
|
+
updateMessage: async (channel, ts, text) => {
|
|
572
|
+
await this.updateMessage(channel, ts, text);
|
|
573
|
+
},
|
|
574
|
+
enqueueEvent: (event) => this.enqueueEvent(event),
|
|
575
|
+
getPlatformInfo: () => this.getPlatformInfo(),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
async routeSlashLoginCommand(payload) {
|
|
579
|
+
const commandSuffix = payload.text?.trim();
|
|
580
|
+
const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
|
|
581
|
+
const createdAt = new Date();
|
|
582
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
583
|
+
const sourceChannelId = payload.channel_id;
|
|
584
|
+
const isDirectMessage = sourceChannelId.startsWith("D");
|
|
585
|
+
const targetChannelId = isDirectMessage
|
|
586
|
+
? sourceChannelId
|
|
587
|
+
: await this.openDirectMessage(payload.user_id);
|
|
588
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
589
|
+
this.logToFile(targetChannelId, {
|
|
590
|
+
date: createdAt.toISOString(),
|
|
591
|
+
ts: eventTs,
|
|
592
|
+
user: payload.user_id,
|
|
593
|
+
userName,
|
|
594
|
+
text: commandText,
|
|
595
|
+
attachments: [],
|
|
596
|
+
isBot: false,
|
|
597
|
+
});
|
|
598
|
+
if (!isDirectMessage) {
|
|
599
|
+
await this.postEphemeral(sourceChannelId, payload.user_id, `我已私訊你 ${PRODUCT_NAME} 的登入連結,請到私訊完成設定。`);
|
|
600
|
+
}
|
|
601
|
+
const event = {
|
|
602
|
+
type: "dm",
|
|
603
|
+
conversationId: targetChannelId,
|
|
604
|
+
conversationKind: "direct",
|
|
605
|
+
ts: eventTs,
|
|
606
|
+
user: payload.user_id,
|
|
607
|
+
text: commandText,
|
|
608
|
+
attachments: [],
|
|
609
|
+
sessionKey: targetChannelId,
|
|
610
|
+
};
|
|
611
|
+
const adapters = this.createDirectCommandAdapters(targetChannelId, payload.user_id, userName, commandText, eventTs);
|
|
612
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
613
|
+
}
|
|
614
|
+
async routeSlashNewCommand(payload) {
|
|
615
|
+
const conversationId = payload.channel_id;
|
|
616
|
+
if (!conversationId.startsWith("D")) {
|
|
617
|
+
await this.postEphemeral(conversationId, payload.user_id, `為了避免誤清除共享上下文,${payload.command} 目前只能在與 ${PRODUCT_NAME} 的私訊中使用。`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const createdAt = new Date();
|
|
621
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
622
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
623
|
+
this.logToFile(conversationId, {
|
|
624
|
+
date: createdAt.toISOString(),
|
|
625
|
+
ts: eventTs,
|
|
626
|
+
user: payload.user_id,
|
|
627
|
+
userName,
|
|
628
|
+
text: payload.command,
|
|
629
|
+
attachments: [],
|
|
630
|
+
isBot: false,
|
|
631
|
+
});
|
|
632
|
+
const commandBot = this.createSlashCommandBot(conversationId);
|
|
633
|
+
await this.handler.handleNew(conversationId, conversationId, commandBot);
|
|
634
|
+
}
|
|
635
|
+
async routeSlashSessionCommand(payload) {
|
|
636
|
+
const conversationId = payload.channel_id;
|
|
637
|
+
const isDirectMessage = conversationId.startsWith("D");
|
|
638
|
+
const createdAt = new Date();
|
|
639
|
+
const eventTs = (createdAt.getTime() / 1000).toFixed(6);
|
|
640
|
+
const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
|
|
641
|
+
const commandText = payload.command;
|
|
642
|
+
this.logToFile(conversationId, {
|
|
643
|
+
date: createdAt.toISOString(),
|
|
644
|
+
ts: eventTs,
|
|
645
|
+
user: payload.user_id,
|
|
646
|
+
userName,
|
|
647
|
+
text: commandText,
|
|
648
|
+
attachments: [],
|
|
649
|
+
isBot: false,
|
|
650
|
+
});
|
|
651
|
+
const sessionKey = conversationId;
|
|
652
|
+
const event = {
|
|
653
|
+
type: "dm",
|
|
654
|
+
conversationId,
|
|
655
|
+
conversationKind: isDirectMessage ? "direct" : "shared",
|
|
656
|
+
ts: eventTs,
|
|
657
|
+
user: payload.user_id,
|
|
658
|
+
text: commandText,
|
|
659
|
+
attachments: [],
|
|
660
|
+
sessionKey,
|
|
661
|
+
};
|
|
662
|
+
const adapters = this.createPrivateSessionCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
|
|
663
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
440
664
|
}
|
|
441
665
|
setupEventHandlers() {
|
|
442
666
|
// Channel @mentions
|
|
@@ -449,9 +673,11 @@ export class SlackBot {
|
|
|
449
673
|
}
|
|
450
674
|
// Top-level mentions use a persistent channel session.
|
|
451
675
|
// Thread replies get their own isolated session (channelId:thread_ts).
|
|
452
|
-
const sessionKey = e.
|
|
676
|
+
const sessionKey = resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
453
677
|
const slackEvent = {
|
|
454
678
|
type: "mention",
|
|
679
|
+
conversationId: e.channel,
|
|
680
|
+
conversationKind: "shared",
|
|
455
681
|
channel: e.channel,
|
|
456
682
|
ts: e.ts,
|
|
457
683
|
thread_ts: e.thread_ts,
|
|
@@ -460,12 +686,13 @@ export class SlackBot {
|
|
|
460
686
|
files: e.files,
|
|
461
687
|
sessionKey,
|
|
462
688
|
};
|
|
463
|
-
|
|
464
|
-
// Also downloads attachments in background and stores local paths
|
|
465
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
689
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
466
690
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
467
691
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
468
692
|
log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
|
|
693
|
+
void attachmentsPromise.catch((err) => {
|
|
694
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
695
|
+
});
|
|
469
696
|
ack();
|
|
470
697
|
return;
|
|
471
698
|
}
|
|
@@ -478,25 +705,17 @@ export class SlackBot {
|
|
|
478
705
|
else {
|
|
479
706
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
480
707
|
}
|
|
708
|
+
void attachmentsPromise.catch((err) => {
|
|
709
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
710
|
+
});
|
|
481
711
|
ack();
|
|
482
712
|
return;
|
|
483
713
|
}
|
|
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
|
-
}
|
|
714
|
+
this.getQueue(this.resolveQueueKey(e.channel, sessionKey)).enqueue(async () => {
|
|
715
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
716
|
+
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
717
|
+
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
718
|
+
});
|
|
500
719
|
ack();
|
|
501
720
|
});
|
|
502
721
|
// All messages (for logging) + DMs (for triggering)
|
|
@@ -516,28 +735,34 @@ export class SlackBot {
|
|
|
516
735
|
return;
|
|
517
736
|
}
|
|
518
737
|
const isDM = e.channel_type === "im";
|
|
738
|
+
const conversationKind = isDM ? "direct" : "shared";
|
|
519
739
|
const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
|
|
520
740
|
// Skip channel @mentions - already handled by app_mention event
|
|
521
741
|
if (!isDM && isBotMention) {
|
|
522
742
|
ack();
|
|
523
743
|
return;
|
|
524
744
|
}
|
|
745
|
+
const isSharedThreadReply = !isDM && !!e.thread_ts;
|
|
746
|
+
const sessionKey = isDM || isSharedThreadReply ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
|
|
525
747
|
const slackEvent = {
|
|
526
748
|
type: isDM ? "dm" : "mention",
|
|
749
|
+
conversationId: e.channel,
|
|
750
|
+
conversationKind,
|
|
527
751
|
channel: e.channel,
|
|
528
752
|
ts: e.ts,
|
|
529
753
|
thread_ts: e.thread_ts,
|
|
530
754
|
user: e.user,
|
|
531
755
|
text: (e.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim(),
|
|
532
756
|
files: e.files,
|
|
533
|
-
sessionKey
|
|
757
|
+
sessionKey,
|
|
534
758
|
};
|
|
535
|
-
|
|
536
|
-
// Also downloads attachments in background and stores local paths
|
|
537
|
-
slackEvent.attachments = this.logUserMessage(slackEvent);
|
|
759
|
+
const attachmentsPromise = this.logUserMessage(slackEvent);
|
|
538
760
|
// Only trigger processing for messages AFTER startup (not replayed old messages)
|
|
539
761
|
if (this.startupTs && e.ts < this.startupTs) {
|
|
540
762
|
log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
|
|
763
|
+
void attachmentsPromise.catch((err) => {
|
|
764
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
765
|
+
});
|
|
541
766
|
ack();
|
|
542
767
|
return;
|
|
543
768
|
}
|
|
@@ -551,41 +776,79 @@ export class SlackBot {
|
|
|
551
776
|
else {
|
|
552
777
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
553
778
|
}
|
|
779
|
+
void attachmentsPromise.catch((err) => {
|
|
780
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
781
|
+
});
|
|
554
782
|
ack();
|
|
555
783
|
return;
|
|
556
784
|
}
|
|
557
|
-
//
|
|
558
|
-
if (isDM) {
|
|
559
|
-
const
|
|
785
|
+
// Trigger handler for DMs and bare replies inside shared-channel threads.
|
|
786
|
+
if (isDM || isSharedThreadReply) {
|
|
787
|
+
const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
560
788
|
// Check for stop command - execute immediately, don't queue!
|
|
561
789
|
if (slackEvent.text.toLowerCase().trim() === "stop") {
|
|
562
|
-
|
|
563
|
-
|
|
790
|
+
const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
|
|
791
|
+
if (stopTarget) {
|
|
792
|
+
this.handler.handleStop(stopTarget, e.channel, this); // Don't await, don't queue
|
|
564
793
|
}
|
|
565
794
|
else {
|
|
566
795
|
this.postMessage(e.channel, formatNothingRunning("slack"));
|
|
567
796
|
}
|
|
797
|
+
void attachmentsPromise.catch((err) => {
|
|
798
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
799
|
+
});
|
|
568
800
|
ack();
|
|
569
801
|
return;
|
|
570
802
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
}
|
|
803
|
+
this.getQueue(this.resolveQueueKey(e.channel, activeSessionKey)).enqueue(async () => {
|
|
804
|
+
slackEvent.attachments = await attachmentsPromise;
|
|
805
|
+
const adapters = createSlackAdapters(slackEvent, this, false);
|
|
806
|
+
return this.handler.handleEvent(slackEvent, this, adapters, false);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
void attachmentsPromise.catch((err) => {
|
|
811
|
+
log.logWarning("Failed to log Slack message", String(err));
|
|
812
|
+
});
|
|
586
813
|
}
|
|
587
814
|
ack();
|
|
588
815
|
});
|
|
816
|
+
this.socketClient.on("slash_commands", async ({ body, ack }) => {
|
|
817
|
+
const payload = body;
|
|
818
|
+
await ack();
|
|
819
|
+
if (!payload.command || !payload.channel_id || !payload.user_id) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const handlerPromise = payload.command === "/pi-login"
|
|
823
|
+
? this.routeSlashLoginCommand({
|
|
824
|
+
command: payload.command,
|
|
825
|
+
text: payload.text,
|
|
826
|
+
channel_id: payload.channel_id,
|
|
827
|
+
user_id: payload.user_id,
|
|
828
|
+
user_name: payload.user_name,
|
|
829
|
+
})
|
|
830
|
+
: payload.command === "/pi-new"
|
|
831
|
+
? this.routeSlashNewCommand({
|
|
832
|
+
command: payload.command,
|
|
833
|
+
channel_id: payload.channel_id,
|
|
834
|
+
user_id: payload.user_id,
|
|
835
|
+
user_name: payload.user_name,
|
|
836
|
+
})
|
|
837
|
+
: payload.command === "/pi-session"
|
|
838
|
+
? this.routeSlashSessionCommand({
|
|
839
|
+
command: payload.command,
|
|
840
|
+
channel_id: payload.channel_id,
|
|
841
|
+
user_id: payload.user_id,
|
|
842
|
+
user_name: payload.user_name,
|
|
843
|
+
})
|
|
844
|
+
: null;
|
|
845
|
+
if (!handlerPromise) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
handlerPromise.catch((err) => {
|
|
849
|
+
log.logWarning("Slack slash command error", err instanceof Error ? err.message : String(err));
|
|
850
|
+
});
|
|
851
|
+
});
|
|
589
852
|
// App Home tab
|
|
590
853
|
this.socketClient.on("app_home_opened", ({ event, ack }) => {
|
|
591
854
|
const e = event;
|
|
@@ -616,8 +879,7 @@ export class SlackBot {
|
|
|
616
879
|
// Use handler's forceStop method
|
|
617
880
|
this.handler.forceStop(sessionKey);
|
|
618
881
|
// Notify in channel
|
|
619
|
-
|
|
620
|
-
await this.postMessage(channelId, formatForceStopped("slack", actorLabel));
|
|
882
|
+
await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
|
|
621
883
|
// Refresh home tab
|
|
622
884
|
if (userId) {
|
|
623
885
|
this.webClient.views
|
|
@@ -632,14 +894,12 @@ export class SlackBot {
|
|
|
632
894
|
});
|
|
633
895
|
}
|
|
634
896
|
/**
|
|
635
|
-
* Log a user message to log.jsonl
|
|
636
|
-
* Downloads attachments in background via store
|
|
897
|
+
* Log a user message to log.jsonl after attachments are ready.
|
|
637
898
|
*/
|
|
638
|
-
logUserMessage(event) {
|
|
899
|
+
async logUserMessage(event) {
|
|
639
900
|
const user = this.users.get(event.user);
|
|
640
|
-
// Process attachments - queues downloads in background
|
|
641
901
|
const attachments = event.files
|
|
642
|
-
? this.store.processAttachments(event.channel, event.files, event.ts)
|
|
902
|
+
? await this.store.processAttachments(event.channel, event.files, event.ts)
|
|
643
903
|
: [];
|
|
644
904
|
this.logToFile(event.channel, {
|
|
645
905
|
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
@@ -724,13 +984,13 @@ export class SlackBot {
|
|
|
724
984
|
const user = this.users.get(msg.user);
|
|
725
985
|
// Strip @mentions from text (same as live messages)
|
|
726
986
|
const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
|
|
727
|
-
// Process attachments - queues downloads in background
|
|
728
987
|
const attachments = msg.files
|
|
729
|
-
? this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
988
|
+
? await this.store.processAttachments(channelId, msg.files, msg.ts)
|
|
730
989
|
: [];
|
|
731
990
|
this.logToFile(channelId, {
|
|
732
991
|
date: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
733
992
|
ts: msg.ts,
|
|
993
|
+
threadTs: msg.thread_ts,
|
|
734
994
|
user: isMamaMessage ? "bot" : msg.user,
|
|
735
995
|
userName: isMamaMessage ? undefined : user?.userName,
|
|
736
996
|
displayName: isMamaMessage ? undefined : user?.displayName,
|