feat(slack): add channel-info and mark-read actions for unread tracking
Adds two new Slack actions to support unread message tracking: - channel-info: Returns channel metadata including last_read timestamp, unread_count, and unread_count_display. Enables bots to know what messages they haven't processed yet. - mark-read: Updates the bot's read cursor for a channel to a specific timestamp. Allows bots to mark messages as processed. Use case: AI assistants that need to catch up on missed messages after context compaction or session restart. By tracking the read position, they can fetch only new messages and avoid reprocessing. Example workflow: 1. Call channel-info to get lastRead timestamp 2. Call read with after=lastRead to get unread messages 3. Process messages 4. Call mark-read with the latest message timestamp AI-assisted: Built with Claude (Opus 4.5) via Clawdbot Testing: Lightly tested (type-checked, not runtime tested)
This commit is contained in:
parent
5d6a9da370
commit
1d6780f5f4
@ -5,10 +5,12 @@ import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||
import {
|
||||
deleteSlackMessage,
|
||||
editSlackMessage,
|
||||
getSlackChannelInfo,
|
||||
getSlackMemberInfo,
|
||||
listSlackEmojis,
|
||||
listSlackPins,
|
||||
listSlackReactions,
|
||||
markSlackChannelRead,
|
||||
pinSlackMessage,
|
||||
reactSlackMessage,
|
||||
readSlackMessages,
|
||||
@ -296,5 +298,30 @@ export async function handleSlackAction(
|
||||
return jsonResult({ ok: true, emojis });
|
||||
}
|
||||
|
||||
if (action === "channelInfo") {
|
||||
if (!isActionEnabled("channelInfo")) {
|
||||
throw new Error("Slack channel info is disabled.");
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const info = readOpts
|
||||
? await getSlackChannelInfo(channelId, readOpts)
|
||||
: await getSlackChannelInfo(channelId);
|
||||
return jsonResult({ ok: true, ...info });
|
||||
}
|
||||
|
||||
if (action === "markRead") {
|
||||
if (!isActionEnabled("channelInfo")) {
|
||||
throw new Error("Slack channel info is disabled.");
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const timestamp = readStringParam(params, "timestamp", { required: true });
|
||||
if (writeOpts) {
|
||||
await markSlackChannelRead(channelId, timestamp, writeOpts);
|
||||
} else {
|
||||
await markSlackChannelRead(channelId, timestamp);
|
||||
}
|
||||
return jsonResult({ ok: true, markedAt: timestamp });
|
||||
}
|
||||
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
|
||||
"role-add",
|
||||
"role-remove",
|
||||
"channel-info",
|
||||
"mark-read",
|
||||
"channel-list",
|
||||
"channel-create",
|
||||
"channel-edit",
|
||||
|
||||
@ -46,6 +46,10 @@ export function createSlackActions(providerId: string): ChannelMessageActionAdap
|
||||
}
|
||||
if (isActionEnabled("memberInfo")) actions.add("member-info");
|
||||
if (isActionEnabled("emojiList")) actions.add("emoji-list");
|
||||
if (isActionEnabled("channelInfo")) {
|
||||
actions.add("channel-info");
|
||||
actions.add("mark-read");
|
||||
}
|
||||
return Array.from(actions);
|
||||
},
|
||||
extractToolSend: ({ args }): ChannelToolSend | null => {
|
||||
@ -204,6 +208,30 @@ export function createSlackActions(providerId: string): ChannelMessageActionAdap
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "channel-info") {
|
||||
return await handleSlackAction(
|
||||
{
|
||||
action: "channelInfo",
|
||||
channelId: resolveChannelId(),
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "mark-read") {
|
||||
const timestamp = readStringParam(params, "timestamp", { required: true });
|
||||
return await handleSlackAction(
|
||||
{
|
||||
action: "markRead",
|
||||
channelId: resolveChannelId(),
|
||||
timestamp,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
||||
},
|
||||
};
|
||||
|
||||
@ -38,6 +38,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
|
||||
"role-add": "none",
|
||||
"role-remove": "none",
|
||||
"channel-info": "channelId",
|
||||
"mark-read": "channelId",
|
||||
"channel-list": "none",
|
||||
"channel-create": "none",
|
||||
"channel-edit": "channelId",
|
||||
|
||||
@ -258,3 +258,46 @@ export async function listSlackPins(
|
||||
const result = await client.pins.list({ channel: channelId });
|
||||
return (result.items ?? []) as SlackPin[];
|
||||
}
|
||||
|
||||
export type SlackChannelInfo = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
lastRead?: string;
|
||||
unreadCount?: number;
|
||||
unreadCountDisplay?: number;
|
||||
latest?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get channel info including unread state (last_read, unread_count).
|
||||
* Useful for tracking what messages the bot hasn't processed yet.
|
||||
*/
|
||||
export async function getSlackChannelInfo(
|
||||
channelId: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
): Promise<SlackChannelInfo> {
|
||||
const client = await getClient(opts);
|
||||
const result = await client.conversations.info({ channel: channelId });
|
||||
const channel = result.channel as Record<string, unknown> | undefined;
|
||||
return {
|
||||
id: channel?.id as string | undefined,
|
||||
name: channel?.name as string | undefined,
|
||||
lastRead: channel?.last_read as string | undefined,
|
||||
unreadCount: channel?.unread_count as number | undefined,
|
||||
unreadCountDisplay: channel?.unread_count_display as number | undefined,
|
||||
latest: (channel?.latest as Record<string, unknown> | undefined)?.ts as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a channel as read up to a specific timestamp.
|
||||
* Updates the bot's read cursor for the channel.
|
||||
*/
|
||||
export async function markSlackChannelRead(
|
||||
channelId: string,
|
||||
timestamp: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
): Promise<void> {
|
||||
const client = await getClient(opts);
|
||||
await client.conversations.mark({ channel: channelId, ts: timestamp });
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user