refactor: drop autoReply, add topic requireMention
Co-authored-by: kitze <kristijan.mkd@gmail.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
- Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup.
|
||||
- Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context.
|
||||
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
|
||||
- Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides).
|
||||
|
||||
### Fixes
|
||||
- Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests.
|
||||
|
||||
@@ -468,12 +468,16 @@ Set `telegram.enabled: false` to disable automatic startup.
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
|
||||
groups: {
|
||||
"*": { requireMention: true, autoReply: false },
|
||||
"*": { requireMention: true },
|
||||
"-1001234567890": {
|
||||
allowFrom: ["@admin"],
|
||||
systemPrompt: "Keep answers brief.",
|
||||
topics: {
|
||||
"99": { skills: ["search"], systemPrompt: "Stay on topic." }
|
||||
"99": {
|
||||
requireMention: false,
|
||||
skills: ["search"],
|
||||
systemPrompt: "Stay on topic."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -580,7 +584,7 @@ Slack runs in Socket Mode and requires both a bot token and app token:
|
||||
C123: { allow: true, requireMention: true },
|
||||
"#general": {
|
||||
allow: true,
|
||||
autoReply: false,
|
||||
requireMention: true,
|
||||
users: ["U123"],
|
||||
skills: ["docs"],
|
||||
systemPrompt: "Short answers only."
|
||||
|
||||
@@ -202,8 +202,7 @@ Notes:
|
||||
requireMention: true,
|
||||
users: ["987654321098765432"],
|
||||
skills: ["search", "docs"],
|
||||
systemPrompt: "Keep answers short.",
|
||||
autoReply: false
|
||||
systemPrompt: "Keep answers short."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +226,6 @@ Ack reactions are controlled globally via `messages.ackReaction` +
|
||||
- `guilds.<id>.users`: optional per-guild user allowlist (ids or names).
|
||||
- `guilds.<id>.channels.<channel>.allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
||||
- `guilds.<id>.channels.<channel>.requireMention`: mention gating for the channel.
|
||||
- `guilds.<id>.channels.<channel>.autoReply`: if `true`, reply to all messages (overrides `requireMention`).
|
||||
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
|
||||
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
|
||||
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).
|
||||
|
||||
@@ -159,7 +159,7 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
|
||||
"C123": { "allow": true, "requireMention": true },
|
||||
"#general": {
|
||||
"allow": true,
|
||||
"autoReply": false,
|
||||
"requireMention": true,
|
||||
"users": ["U123"],
|
||||
"skills": ["search", "docs"],
|
||||
"systemPrompt": "Keep answers short."
|
||||
@@ -212,7 +212,6 @@ Ack reactions are controlled globally via `messages.ackReaction` +
|
||||
Channel options (`slack.channels.<id>` or `slack.channels.<name>`):
|
||||
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
||||
- `requireMention`: mention gating for the channel.
|
||||
- `autoReply`: if `true`, reply to every message (overrides `requireMention`).
|
||||
- `users`: optional per-channel user allowlist.
|
||||
- `skills`: skill filter (omit = all skills, empty = none).
|
||||
- `systemPrompt`: extra system prompt for the channel (combined with topic/purpose).
|
||||
|
||||
@@ -117,12 +117,12 @@ Provider options:
|
||||
- `telegram.groupAllowFrom`: group sender allowlist (ids/usernames).
|
||||
- `telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
|
||||
- `telegram.groups.<id>.requireMention`: mention gating default.
|
||||
- `telegram.groups.<id>.autoReply`: reply to every message (overrides `requireMention`).
|
||||
- `telegram.groups.<id>.skills`: skill filter (omit = all skills, empty = none).
|
||||
- `telegram.groups.<id>.allowFrom`: per-group sender allowlist override.
|
||||
- `telegram.groups.<id>.systemPrompt`: extra system prompt for the group.
|
||||
- `telegram.groups.<id>.enabled`: disable the group when `false`.
|
||||
- `telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
|
||||
- `telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
|
||||
- `telegram.replyToMode`: `off | first | all`.
|
||||
- `telegram.textChunkLimit`: outbound chunk size (chars).
|
||||
- `telegram.streamMode`: `off | partial | block` (draft streaming).
|
||||
|
||||
@@ -44,11 +44,7 @@ function parseTelegramGroupId(value?: string | null) {
|
||||
return { chatId: raw, topicId: undefined };
|
||||
}
|
||||
|
||||
function hasOwn(obj: unknown, key: string): boolean {
|
||||
return Boolean(obj && typeof obj === "object" && Object.hasOwn(obj, key));
|
||||
}
|
||||
|
||||
function resolveTelegramAutoReply(params: {
|
||||
function resolveTelegramRequireMention(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
chatId?: string;
|
||||
topicId?: string;
|
||||
@@ -61,17 +57,17 @@ function resolveTelegramAutoReply(params: {
|
||||
topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
|
||||
const defaultTopicConfig =
|
||||
topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined;
|
||||
if (hasOwn(topicConfig, "autoReply")) {
|
||||
return (topicConfig as { autoReply?: boolean }).autoReply;
|
||||
if (typeof topicConfig?.requireMention === "boolean") {
|
||||
return topicConfig.requireMention;
|
||||
}
|
||||
if (hasOwn(defaultTopicConfig, "autoReply")) {
|
||||
return (defaultTopicConfig as { autoReply?: boolean }).autoReply;
|
||||
if (typeof defaultTopicConfig?.requireMention === "boolean") {
|
||||
return defaultTopicConfig.requireMention;
|
||||
}
|
||||
if (hasOwn(groupConfig, "autoReply")) {
|
||||
return (groupConfig as { autoReply?: boolean }).autoReply;
|
||||
if (typeof groupConfig?.requireMention === "boolean") {
|
||||
return groupConfig.requireMention;
|
||||
}
|
||||
if (hasOwn(groupDefault, "autoReply")) {
|
||||
return (groupDefault as { autoReply?: boolean }).autoReply;
|
||||
if (typeof groupDefault?.requireMention === "boolean") {
|
||||
return groupDefault.requireMention;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -107,8 +103,12 @@ export function resolveGroupRequireMention(params: {
|
||||
const groupSpace = ctx.GroupSpace?.trim();
|
||||
if (provider === "telegram") {
|
||||
const { chatId, topicId } = parseTelegramGroupId(groupId);
|
||||
const autoReply = resolveTelegramAutoReply({ cfg, chatId, topicId });
|
||||
if (typeof autoReply === "boolean") return !autoReply;
|
||||
const requireMention = resolveTelegramRequireMention({
|
||||
cfg,
|
||||
chatId,
|
||||
topicId,
|
||||
});
|
||||
if (typeof requireMention === "boolean") return requireMention;
|
||||
return resolveProviderGroupRequireMention({
|
||||
cfg,
|
||||
provider,
|
||||
@@ -138,9 +138,6 @@ export function resolveGroupRequireMention(params: {
|
||||
(groupRoom
|
||||
? channelEntries[normalizeDiscordSlug(groupRoom)]
|
||||
: undefined);
|
||||
if (entry && typeof entry.autoReply === "boolean") {
|
||||
return !entry.autoReply;
|
||||
}
|
||||
if (entry && typeof entry.requireMention === "boolean") {
|
||||
return entry.requireMention;
|
||||
}
|
||||
@@ -163,7 +160,7 @@ export function resolveGroupRequireMention(params: {
|
||||
channelName ?? "",
|
||||
normalizedName,
|
||||
].filter(Boolean);
|
||||
let matched: { requireMention?: boolean; autoReply?: boolean } | undefined;
|
||||
let matched: { requireMention?: boolean } | undefined;
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && channels[candidate]) {
|
||||
matched = channels[candidate];
|
||||
@@ -172,9 +169,6 @@ export function resolveGroupRequireMention(params: {
|
||||
}
|
||||
const fallback = channels["*"];
|
||||
const resolved = matched ?? fallback;
|
||||
if (typeof resolved?.autoReply === "boolean") {
|
||||
return !resolved.autoReply;
|
||||
}
|
||||
if (typeof resolved?.requireMention === "boolean") {
|
||||
return resolved.requireMention;
|
||||
}
|
||||
|
||||
@@ -237,12 +237,11 @@ export type TelegramActionConfig = {
|
||||
};
|
||||
|
||||
export type TelegramTopicConfig = {
|
||||
requireMention?: boolean;
|
||||
/** If specified, only load these skills for this topic. Omit = all skills; empty = no skills. */
|
||||
skills?: string[];
|
||||
/** If false, disable the bot for this topic. */
|
||||
enabled?: boolean;
|
||||
/** If true, reply to every message (no mention required). */
|
||||
autoReply?: boolean;
|
||||
/** Optional allowlist for topic senders (ids or usernames). */
|
||||
allowFrom?: Array<string | number>;
|
||||
/** Optional system prompt snippet for this topic. */
|
||||
@@ -257,8 +256,6 @@ export type TelegramGroupConfig = {
|
||||
topics?: Record<string, TelegramTopicConfig>;
|
||||
/** If false, disable the bot for this group (and its topics). */
|
||||
enabled?: boolean;
|
||||
/** If true, reply to every message (no mention required). */
|
||||
autoReply?: boolean;
|
||||
/** Optional allowlist for group senders (ids or usernames). */
|
||||
allowFrom?: Array<string | number>;
|
||||
/** Optional system prompt snippet for this group. */
|
||||
@@ -325,8 +322,6 @@ export type DiscordGuildChannelConfig = {
|
||||
skills?: string[];
|
||||
/** If false, disable the bot for this channel. */
|
||||
enabled?: boolean;
|
||||
/** If true, reply to every message (no mention required). */
|
||||
autoReply?: boolean;
|
||||
/** Optional allowlist for channel senders (ids or names). */
|
||||
users?: Array<string | number>;
|
||||
/** Optional system prompt snippet for this channel. */
|
||||
@@ -412,8 +407,6 @@ export type SlackChannelConfig = {
|
||||
allow?: boolean;
|
||||
/** Require mentioning the bot to trigger replies. */
|
||||
requireMention?: boolean;
|
||||
/** Reply to all messages without needing a mention. */
|
||||
autoReply?: boolean;
|
||||
/** Allowlist of users that can invoke the bot in this channel. */
|
||||
users?: Array<string | number>;
|
||||
/** Optional skill filter for this channel. */
|
||||
|
||||
@@ -787,7 +787,6 @@ export const ClawdbotSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
autoReply: z.boolean().optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
topics: z
|
||||
@@ -795,9 +794,9 @@ export const ClawdbotSchema = z.object({
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
autoReply: z.boolean().optional(),
|
||||
allowFrom: z
|
||||
.array(z.union([z.string(), z.number()]))
|
||||
.optional(),
|
||||
@@ -913,7 +912,6 @@ export const ClawdbotSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
autoReply: z.boolean().optional(),
|
||||
users: z
|
||||
.array(z.union([z.string(), z.number()]))
|
||||
.optional(),
|
||||
@@ -990,7 +988,6 @@ export const ClawdbotSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
autoReply: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
|
||||
@@ -101,7 +101,6 @@ describe("discord guild/channel resolution", () => {
|
||||
requireMention: true,
|
||||
skills: ["search"],
|
||||
enabled: false,
|
||||
autoReply: true,
|
||||
users: ["123"],
|
||||
systemPrompt: "Use short answers.",
|
||||
},
|
||||
@@ -126,7 +125,6 @@ describe("discord guild/channel resolution", () => {
|
||||
expect(help?.requireMention).toBe(true);
|
||||
expect(help?.skills).toEqual(["search"]);
|
||||
expect(help?.enabled).toBe(false);
|
||||
expect(help?.autoReply).toBe(true);
|
||||
expect(help?.users).toEqual(["123"]);
|
||||
expect(help?.systemPrompt).toBe("Use short answers.");
|
||||
});
|
||||
|
||||
@@ -101,7 +101,6 @@ export type DiscordGuildEntryResolved = {
|
||||
requireMention?: boolean;
|
||||
skills?: string[];
|
||||
enabled?: boolean;
|
||||
autoReply?: boolean;
|
||||
users?: Array<string | number>;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
@@ -113,7 +112,6 @@ export type DiscordChannelConfigResolved = {
|
||||
requireMention?: boolean;
|
||||
skills?: string[];
|
||||
enabled?: boolean;
|
||||
autoReply?: boolean;
|
||||
users?: Array<string | number>;
|
||||
systemPrompt?: string;
|
||||
};
|
||||
@@ -601,14 +599,8 @@ export function createDiscordMessageHandler(params: {
|
||||
guildHistories.set(message.channelId, history);
|
||||
}
|
||||
|
||||
const baseRequireMention =
|
||||
channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
|
||||
const shouldRequireMention =
|
||||
channelConfig?.autoReply === true
|
||||
? false
|
||||
: channelConfig?.autoReply === false
|
||||
? true
|
||||
: baseRequireMention;
|
||||
channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
|
||||
const hasAnyMention = Boolean(
|
||||
!isDirectMessage &&
|
||||
(message.mentionedEveryone ||
|
||||
@@ -1810,7 +1802,6 @@ export function resolveDiscordChannelConfig(params: {
|
||||
requireMention: byId.requireMention,
|
||||
skills: byId.skills,
|
||||
enabled: byId.enabled,
|
||||
autoReply: byId.autoReply,
|
||||
users: byId.users,
|
||||
systemPrompt: byId.systemPrompt,
|
||||
};
|
||||
@@ -1821,7 +1812,6 @@ export function resolveDiscordChannelConfig(params: {
|
||||
requireMention: entry.requireMention,
|
||||
skills: entry.skills,
|
||||
enabled: entry.enabled,
|
||||
autoReply: entry.autoReply,
|
||||
users: entry.users,
|
||||
systemPrompt: entry.systemPrompt,
|
||||
};
|
||||
@@ -1833,7 +1823,6 @@ export function resolveDiscordChannelConfig(params: {
|
||||
requireMention: entry.requireMention,
|
||||
skills: entry.skills,
|
||||
enabled: entry.enabled,
|
||||
autoReply: entry.autoReply,
|
||||
users: entry.users,
|
||||
systemPrompt: entry.systemPrompt,
|
||||
};
|
||||
|
||||
@@ -159,7 +159,6 @@ type SlackThreadBroadcastEvent = {
|
||||
type SlackChannelConfigResolved = {
|
||||
allowed: boolean;
|
||||
requireMention: boolean;
|
||||
autoReply?: boolean;
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
@@ -284,7 +283,6 @@ function resolveSlackChannelConfig(params: {
|
||||
enabled?: boolean;
|
||||
allow?: boolean;
|
||||
requireMention?: boolean;
|
||||
autoReply?: boolean;
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
@@ -308,7 +306,6 @@ function resolveSlackChannelConfig(params: {
|
||||
enabled?: boolean;
|
||||
allow?: boolean;
|
||||
requireMention?: boolean;
|
||||
autoReply?: boolean;
|
||||
users?: Array<string | number>;
|
||||
skills?: string[];
|
||||
systemPrompt?: string;
|
||||
@@ -341,14 +338,13 @@ function resolveSlackChannelConfig(params: {
|
||||
const requireMention =
|
||||
firstDefined(resolved.requireMention, fallback?.requireMention, true) ??
|
||||
true;
|
||||
const autoReply = firstDefined(resolved.autoReply, fallback?.autoReply);
|
||||
const users = firstDefined(resolved.users, fallback?.users);
|
||||
const skills = firstDefined(resolved.skills, fallback?.skills);
|
||||
const systemPrompt = firstDefined(
|
||||
resolved.systemPrompt,
|
||||
fallback?.systemPrompt,
|
||||
);
|
||||
return { allowed, requireMention, autoReply, users, skills, systemPrompt };
|
||||
return { allowed, requireMention, users, skills, systemPrompt };
|
||||
}
|
||||
|
||||
async function resolveSlackMedia(params: {
|
||||
@@ -810,11 +806,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
surface: "slack",
|
||||
});
|
||||
const shouldRequireMention = isRoom
|
||||
? channelConfig?.autoReply === true
|
||||
? false
|
||||
: channelConfig?.autoReply === false
|
||||
? true
|
||||
: (channelConfig?.requireMention ?? true)
|
||||
? (channelConfig?.requireMention ?? true)
|
||||
: false;
|
||||
const shouldBypassMention =
|
||||
allowTextCommands &&
|
||||
|
||||
@@ -704,6 +704,50 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("allows per-topic requireMention override", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
replySpy.mockReset();
|
||||
loadConfig.mockReturnValue({
|
||||
telegram: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
"-1001234567890": {
|
||||
requireMention: true,
|
||||
topics: {
|
||||
"99": { requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = onSpy.mock.calls[0][1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: {
|
||||
id: -1001234567890,
|
||||
type: "supergroup",
|
||||
title: "Forum Group",
|
||||
is_forum: true,
|
||||
},
|
||||
text: "hello",
|
||||
date: 1736380800,
|
||||
message_thread_id: 99,
|
||||
},
|
||||
me: { username: "clawdbot_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("honors groups default when no explicit group override exists", async () => {
|
||||
onSpy.mockReset();
|
||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||
|
||||
@@ -381,16 +381,11 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
(ent) => ent.type === "mention",
|
||||
);
|
||||
const baseRequireMention = resolveGroupRequireMention(chatId);
|
||||
const autoReplySetting = firstDefined(
|
||||
topicConfig?.autoReply,
|
||||
groupConfig?.autoReply,
|
||||
const requireMention = firstDefined(
|
||||
topicConfig?.requireMention,
|
||||
groupConfig?.requireMention,
|
||||
baseRequireMention,
|
||||
);
|
||||
const requireMention =
|
||||
autoReplySetting === true
|
||||
? false
|
||||
: autoReplySetting === false
|
||||
? true
|
||||
: baseRequireMention;
|
||||
const shouldBypassMention =
|
||||
isGroup &&
|
||||
requireMention &&
|
||||
|
||||
Reference in New Issue
Block a user