mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 18:01:46 +00:00
feat(slack): track thread participation for auto-reply without @mention (#29165)
* feat(slack): track thread participation for auto-reply without @mention * fix(slack): scope thread participation cache by accountId and capture actual reply thread ts * fix(slack): capture reply thread ts from all delivery paths and only after success * Slack: add changelog for thread participation cache behavior --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
|
||||
import { resolveAgentOutboundIdentity } from "../../../infra/outbound/identity.js";
|
||||
import { removeSlackReaction } from "../../actions.js";
|
||||
import { createSlackDraftStream } from "../../draft-stream.js";
|
||||
import { recordSlackThreadParticipation } from "../../sent-thread-cache.js";
|
||||
import {
|
||||
applyAppendOnlyStreamUpdate,
|
||||
buildStatusFinalPreviewText,
|
||||
@@ -189,6 +190,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
});
|
||||
let streamSession: SlackStreamSession | null = null;
|
||||
let streamFailed = false;
|
||||
let usedReplyThreadTs: string | undefined;
|
||||
|
||||
const deliverNormally = async (payload: ReplyPayload, forcedThreadTs?: string): Promise<void> => {
|
||||
const replyThreadTs = forcedThreadTs ?? replyPlan.nextThreadTs();
|
||||
@@ -203,6 +205,10 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
replyToMode: prepared.replyToMode,
|
||||
...(slackIdentity ? { identity: slackIdentity } : {}),
|
||||
});
|
||||
// Record the thread ts only after confirmed delivery success.
|
||||
if (replyThreadTs) {
|
||||
usedReplyThreadTs ??= replyThreadTs;
|
||||
}
|
||||
replyPlan.markSent();
|
||||
};
|
||||
|
||||
@@ -235,6 +241,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
teamId: ctx.teamId,
|
||||
userId: message.user,
|
||||
});
|
||||
usedReplyThreadTs ??= streamThreadTs;
|
||||
replyPlan.markSent();
|
||||
return;
|
||||
}
|
||||
@@ -324,7 +331,13 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
token: ctx.botToken,
|
||||
accountId: account.accountId,
|
||||
maxChars: Math.min(ctx.textLimit, 4000),
|
||||
resolveThreadTs: () => replyPlan.nextThreadTs(),
|
||||
resolveThreadTs: () => {
|
||||
const ts = replyPlan.nextThreadTs();
|
||||
if (ts) {
|
||||
usedReplyThreadTs ??= ts;
|
||||
}
|
||||
return ts;
|
||||
},
|
||||
onMessageSent: () => replyPlan.markSent(),
|
||||
log: logVerbose,
|
||||
warn: logVerbose,
|
||||
@@ -425,6 +438,14 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
|
||||
const anyReplyDelivered = queuedFinal || (counts.block ?? 0) > 0 || (counts.final ?? 0) > 0;
|
||||
|
||||
// Record thread participation only when we actually delivered a reply and
|
||||
// know the thread ts that was used (set by deliverNormally, streaming start,
|
||||
// or draft stream). Falls back to statusThreadTs for edge cases.
|
||||
const participationThreadTs = usedReplyThreadTs ?? statusThreadTs;
|
||||
if (anyReplyDelivered && participationThreadTs) {
|
||||
recordSlackThreadParticipation(account.accountId, message.channel, participationThreadTs);
|
||||
}
|
||||
|
||||
if (!anyReplyDelivered) {
|
||||
await draftStream.clear();
|
||||
if (prepared.isRoomish) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { resolveThreadSessionKeys } from "../../../routing/session-key.js";
|
||||
import { resolveSlackReplyToMode, type ResolvedSlackAccount } from "../../accounts.js";
|
||||
import { reactSlackMessage } from "../../actions.js";
|
||||
import { sendMessageSlack } from "../../send.js";
|
||||
import { hasSlackThreadParticipation } from "../../sent-thread-cache.js";
|
||||
import { resolveSlackThreadContext } from "../../threading.js";
|
||||
import type { SlackMessageEvent } from "../../types.js";
|
||||
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js";
|
||||
@@ -210,7 +211,8 @@ export async function prepareSlackMessage(params: {
|
||||
!isDirectMessage &&
|
||||
ctx.botUserId &&
|
||||
message.thread_ts &&
|
||||
message.parent_user_id === ctx.botUserId,
|
||||
(message.parent_user_id === ctx.botUserId ||
|
||||
hasSlackThreadParticipation(account.accountId, message.channel, message.thread_ts)),
|
||||
);
|
||||
|
||||
const sender = message.user ? await ctx.resolveUserName(message.user) : null;
|
||||
@@ -259,7 +261,10 @@ export async function prepareSlackMessage(params: {
|
||||
useAccessGroups: ctx.useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: allowFromLower.length > 0, allowed: ownerAuthorized },
|
||||
{ configured: channelUsersAllowlistConfigured, allowed: channelCommandAuthorized },
|
||||
{
|
||||
configured: channelUsersAllowlistConfigured,
|
||||
allowed: channelCommandAuthorized,
|
||||
},
|
||||
],
|
||||
allowTextCommands,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
|
||||
Reference in New Issue
Block a user