Merge 1b9b43771e into da71eaebd2
This commit is contained in:
commit
85ffde1882
@ -337,6 +337,8 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"channels.slack.userTokenReadOnly": "Slack User Token Read Only",
|
"channels.slack.userTokenReadOnly": "Slack User Token Read Only",
|
||||||
"channels.slack.thread.historyScope": "Slack Thread History Scope",
|
"channels.slack.thread.historyScope": "Slack Thread History Scope",
|
||||||
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
|
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
|
||||||
|
"channels.slack.thread.followOnMention": "Slack Thread Follow on Mention",
|
||||||
|
"channels.slack.thread.followIdleMinutes": "Slack Thread Follow Idle Minutes",
|
||||||
"channels.mattermost.botToken": "Mattermost Bot Token",
|
"channels.mattermost.botToken": "Mattermost Bot Token",
|
||||||
"channels.mattermost.baseUrl": "Mattermost Base URL",
|
"channels.mattermost.baseUrl": "Mattermost Base URL",
|
||||||
"channels.mattermost.chatmode": "Mattermost Chat Mode",
|
"channels.mattermost.chatmode": "Mattermost Chat Mode",
|
||||||
@ -469,6 +471,10 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||||
"channels.slack.thread.inheritParent":
|
"channels.slack.thread.inheritParent":
|
||||||
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
|
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
|
||||||
|
"channels.slack.thread.followOnMention":
|
||||||
|
"If true, continue responding in threads where the bot was mentioned without requiring repeated @mentions (default: false).",
|
||||||
|
"channels.slack.thread.followIdleMinutes":
|
||||||
|
"Minutes of inactivity before a thread is no longer considered 'active' for followOnMention (default: 60).",
|
||||||
"channels.mattermost.botToken":
|
"channels.mattermost.botToken":
|
||||||
"Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
|
"Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
|
||||||
"channels.mattermost.baseUrl":
|
"channels.mattermost.baseUrl":
|
||||||
|
|||||||
@ -73,6 +73,16 @@ export type SlackThreadConfig = {
|
|||||||
historyScope?: "thread" | "channel";
|
historyScope?: "thread" | "channel";
|
||||||
/** If true, thread sessions inherit the parent channel transcript. Default: false. */
|
/** If true, thread sessions inherit the parent channel transcript. Default: false. */
|
||||||
inheritParent?: boolean;
|
inheritParent?: boolean;
|
||||||
|
/**
|
||||||
|
* If true, continue responding in threads where the bot was mentioned
|
||||||
|
* without requiring repeated @mentions. Default: false.
|
||||||
|
*/
|
||||||
|
followOnMention?: boolean;
|
||||||
|
/**
|
||||||
|
* Minutes of inactivity before a thread is no longer considered "active"
|
||||||
|
* for followOnMention. Default: 60.
|
||||||
|
*/
|
||||||
|
followIdleMinutes?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SlackAccountConfig = {
|
export type SlackAccountConfig = {
|
||||||
|
|||||||
@ -392,6 +392,8 @@ export const SlackThreadSchema = z
|
|||||||
.object({
|
.object({
|
||||||
historyScope: z.enum(["thread", "channel"]).optional(),
|
historyScope: z.enum(["thread", "channel"]).optional(),
|
||||||
inheritParent: z.boolean().optional(),
|
inheritParent: z.boolean().optional(),
|
||||||
|
followOnMention: z.boolean().optional(),
|
||||||
|
followIdleMinutes: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,8 @@ export type SlackMonitorContext = {
|
|||||||
replyToMode: "off" | "first" | "all";
|
replyToMode: "off" | "first" | "all";
|
||||||
threadHistoryScope: "thread" | "channel";
|
threadHistoryScope: "thread" | "channel";
|
||||||
threadInheritParent: boolean;
|
threadInheritParent: boolean;
|
||||||
|
threadFollowOnMention: boolean;
|
||||||
|
threadFollowIdleMinutes: number;
|
||||||
slashCommand: Required<import("../../config/config.js").SlackSlashCommandConfig>;
|
slashCommand: Required<import("../../config/config.js").SlackSlashCommandConfig>;
|
||||||
textLimit: number;
|
textLimit: number;
|
||||||
ackReactionScope: string;
|
ackReactionScope: string;
|
||||||
@ -112,6 +114,10 @@ export type SlackMonitorContext = {
|
|||||||
threadTs?: string;
|
threadTs?: string;
|
||||||
status: string;
|
status: string;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
|
/** Check if a thread is active (bot was mentioned recently). */
|
||||||
|
isActiveThread: (channelId: string, threadTs: string) => boolean;
|
||||||
|
/** Mark a thread as active (called when bot is mentioned in a thread). */
|
||||||
|
markThreadActive: (channelId: string, threadTs: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createSlackMonitorContext(params: {
|
export function createSlackMonitorContext(params: {
|
||||||
@ -143,6 +149,8 @@ export function createSlackMonitorContext(params: {
|
|||||||
replyToMode: SlackMonitorContext["replyToMode"];
|
replyToMode: SlackMonitorContext["replyToMode"];
|
||||||
threadHistoryScope: SlackMonitorContext["threadHistoryScope"];
|
threadHistoryScope: SlackMonitorContext["threadHistoryScope"];
|
||||||
threadInheritParent: SlackMonitorContext["threadInheritParent"];
|
threadInheritParent: SlackMonitorContext["threadInheritParent"];
|
||||||
|
threadFollowOnMention?: boolean;
|
||||||
|
threadFollowIdleMinutes?: number;
|
||||||
slashCommand: SlackMonitorContext["slashCommand"];
|
slashCommand: SlackMonitorContext["slashCommand"];
|
||||||
textLimit: number;
|
textLimit: number;
|
||||||
ackReactionScope: string;
|
ackReactionScope: string;
|
||||||
@ -152,6 +160,34 @@ export function createSlackMonitorContext(params: {
|
|||||||
const channelHistories = new Map<string, HistoryEntry[]>();
|
const channelHistories = new Map<string, HistoryEntry[]>();
|
||||||
const logger = getChildLogger({ module: "slack-auto-reply" });
|
const logger = getChildLogger({ module: "slack-auto-reply" });
|
||||||
|
|
||||||
|
// Thread following: track threads where the bot was mentioned
|
||||||
|
// Key: "channelId:threadTs", Value: timestamp when last active
|
||||||
|
const activeThreads = new Map<string, number>();
|
||||||
|
const threadFollowOnMention = params.threadFollowOnMention ?? false;
|
||||||
|
const threadFollowIdleMinutes = params.threadFollowIdleMinutes ?? 60;
|
||||||
|
const threadFollowIdleMs = threadFollowIdleMinutes * 60 * 1000;
|
||||||
|
|
||||||
|
const isActiveThread = (channelId: string, threadTs: string): boolean => {
|
||||||
|
if (!threadFollowOnMention) return false;
|
||||||
|
const key = `${channelId}:${threadTs}`;
|
||||||
|
const lastActive = activeThreads.get(key);
|
||||||
|
if (!lastActive) return false;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastActive > threadFollowIdleMs) {
|
||||||
|
// Thread has gone idle, remove it
|
||||||
|
activeThreads.delete(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const markThreadActive = (channelId: string, threadTs: string): void => {
|
||||||
|
if (!threadFollowOnMention) return;
|
||||||
|
const key = `${channelId}:${threadTs}`;
|
||||||
|
activeThreads.set(key, Date.now());
|
||||||
|
logVerbose(`slack: marked thread ${key} as active for followOnMention`);
|
||||||
|
};
|
||||||
|
|
||||||
const channelCache = new Map<
|
const channelCache = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@ -387,6 +423,8 @@ export function createSlackMonitorContext(params: {
|
|||||||
replyToMode: params.replyToMode,
|
replyToMode: params.replyToMode,
|
||||||
threadHistoryScope: params.threadHistoryScope,
|
threadHistoryScope: params.threadHistoryScope,
|
||||||
threadInheritParent: params.threadInheritParent,
|
threadInheritParent: params.threadInheritParent,
|
||||||
|
threadFollowOnMention,
|
||||||
|
threadFollowIdleMinutes,
|
||||||
slashCommand: params.slashCommand,
|
slashCommand: params.slashCommand,
|
||||||
textLimit: params.textLimit,
|
textLimit: params.textLimit,
|
||||||
ackReactionScope: params.ackReactionScope,
|
ackReactionScope: params.ackReactionScope,
|
||||||
@ -400,5 +438,7 @@ export function createSlackMonitorContext(params: {
|
|||||||
resolveChannelName,
|
resolveChannelName,
|
||||||
resolveUserName,
|
resolveUserName,
|
||||||
setSlackThreadStatus,
|
setSlackThreadStatus,
|
||||||
|
isActiveThread,
|
||||||
|
markThreadActive,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -223,11 +223,16 @@ export async function prepareSlackMessage(params: {
|
|||||||
canResolveExplicit: Boolean(ctx.botUserId),
|
canResolveExplicit: Boolean(ctx.botUserId),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
// Check if this is an "active thread" where the bot was previously mentioned
|
||||||
|
const isInActiveThread = Boolean(
|
||||||
|
!isDirectMessage && message.thread_ts && ctx.isActiveThread(message.channel, message.thread_ts),
|
||||||
|
);
|
||||||
|
|
||||||
const implicitMention = Boolean(
|
const implicitMention = Boolean(
|
||||||
!isDirectMessage &&
|
!isDirectMessage &&
|
||||||
ctx.botUserId &&
|
ctx.botUserId &&
|
||||||
message.thread_ts &&
|
message.thread_ts &&
|
||||||
message.parent_user_id === ctx.botUserId,
|
(message.parent_user_id === ctx.botUserId || isInActiveThread),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sender = message.user ? await ctx.resolveUserName(message.user) : null;
|
const sender = message.user ? await ctx.resolveUserName(message.user) : null;
|
||||||
@ -306,6 +311,18 @@ export async function prepareSlackMessage(params: {
|
|||||||
commandAuthorized,
|
commandAuthorized,
|
||||||
});
|
});
|
||||||
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||||
|
|
||||||
|
// Mark thread as active if the bot was explicitly mentioned (for followOnMention feature)
|
||||||
|
if (effectiveWasMentioned && message.thread_ts && ctx.threadFollowOnMention) {
|
||||||
|
ctx.markThreadActive(message.channel, message.thread_ts);
|
||||||
|
}
|
||||||
|
// Also mark if this is a reply that creates a thread (the reply itself starts the thread)
|
||||||
|
if (effectiveWasMentioned && !message.thread_ts && message.ts && ctx.threadFollowOnMention) {
|
||||||
|
// If the bot responds, it will create a thread with this ts - mark it preemptively
|
||||||
|
// Actually, we should mark the thread when the bot responds, not here
|
||||||
|
// For now, just handle existing threads
|
||||||
|
}
|
||||||
|
|
||||||
if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
|
if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
|
||||||
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel message");
|
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel message");
|
||||||
const pendingText = (message.text ?? "").trim();
|
const pendingText = (message.text ?? "").trim();
|
||||||
|
|||||||
@ -118,6 +118,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
const replyToMode = slackCfg.replyToMode ?? "off";
|
const replyToMode = slackCfg.replyToMode ?? "off";
|
||||||
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
||||||
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
||||||
|
const threadFollowOnMention = slackCfg.thread?.followOnMention ?? false;
|
||||||
|
const threadFollowIdleMinutes = slackCfg.thread?.followIdleMinutes ?? 60;
|
||||||
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
||||||
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId);
|
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId);
|
||||||
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||||
@ -199,6 +201,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
replyToMode,
|
replyToMode,
|
||||||
threadHistoryScope,
|
threadHistoryScope,
|
||||||
threadInheritParent,
|
threadInheritParent,
|
||||||
|
threadFollowOnMention,
|
||||||
|
threadFollowIdleMinutes,
|
||||||
slashCommand,
|
slashCommand,
|
||||||
textLimit,
|
textLimit,
|
||||||
ackReactionScope,
|
ackReactionScope,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user