feat(messages): add whatsapp messagePrefix and responsePrefix auto
This commit is contained in:
@@ -77,7 +77,7 @@ Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/tok
|
|||||||
## Prefixes, threading, and replies
|
## Prefixes, threading, and replies
|
||||||
|
|
||||||
Outbound message formatting is centralized in `messages`:
|
Outbound message formatting is centralized in `messages`:
|
||||||
- `messages.responsePrefix` and `messages.messagePrefix`
|
- `messages.responsePrefix` (outbound prefix) and `whatsapp.messagePrefix` (WhatsApp inbound prefix)
|
||||||
- Reply threading via `replyToMode` and per-provider defaults
|
- Reply threading via `replyToMode` and per-provider defaults
|
||||||
|
|
||||||
Details: [Configuration](/gateway/configuration#messages) and provider docs.
|
Details: [Configuration](/gateway/configuration#messages) and provider docs.
|
||||||
|
|||||||
@@ -926,8 +926,7 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
|
|||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
messages: {
|
messages: {
|
||||||
messagePrefix: "[clawdbot]",
|
responsePrefix: "🦞", // or "auto"
|
||||||
responsePrefix: "🦞",
|
|
||||||
ackReaction: "👀",
|
ackReaction: "👀",
|
||||||
ackReactionScope: "group-mentions"
|
ackReactionScope: "group-mentions"
|
||||||
}
|
}
|
||||||
@@ -938,11 +937,13 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
|
|||||||
streaming, final replies) across providers unless already present.
|
streaming, final replies) across providers unless already present.
|
||||||
|
|
||||||
If `messages.responsePrefix` is unset, no prefix is applied by default.
|
If `messages.responsePrefix` is unset, no prefix is applied by default.
|
||||||
|
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
|
||||||
|
|
||||||
If `messages.messagePrefix` is unset, the default stays **unchanged**:
|
WhatsApp inbound prefix is configured via `whatsapp.messagePrefix` (deprecated:
|
||||||
`"[clawdbot]"` when `whatsapp.allowFrom` is empty, otherwise `""` (no prefix).
|
`messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when
|
||||||
When using `"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when
|
`whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
|
||||||
the routed agent has `identity.name` set.
|
`"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when the routed
|
||||||
|
agent has `identity.name` set.
|
||||||
|
|
||||||
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
|
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
|
||||||
on providers that support reactions (Slack/Discord/Telegram). Defaults to the
|
on providers that support reactions (Slack/Discord/Telegram). Defaults to the
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ Behavior:
|
|||||||
- `whatsapp.actions.reactions` (gate WhatsApp tool reactions).
|
- `whatsapp.actions.reactions` (gate WhatsApp tool reactions).
|
||||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
|
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
|
||||||
- `messages.groupChat.historyLimit`
|
- `messages.groupChat.historyLimit`
|
||||||
- `messages.messagePrefix` (inbound prefix)
|
- `whatsapp.messagePrefix` (inbound prefix; per-account: `whatsapp.accounts.<accountId>.messagePrefix`; deprecated: `messages.messagePrefix`)
|
||||||
- `messages.responsePrefix` (outbound prefix)
|
- `messages.responsePrefix` (outbound prefix)
|
||||||
- `agents.defaults.mediaMaxMb`
|
- `agents.defaults.mediaMaxMb`
|
||||||
- `agents.defaults.heartbeat.every`
|
- `agents.defaults.heartbeat.every`
|
||||||
|
|||||||
65
src/agents/identity.test.ts
Normal file
65
src/agents/identity.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import { resolveMessagePrefix, resolveResponsePrefix } from "./identity.js";
|
||||||
|
|
||||||
|
describe("message prefix resolution", () => {
|
||||||
|
it("returns configured messagePrefix override", () => {
|
||||||
|
const cfg: ClawdbotConfig = {};
|
||||||
|
expect(
|
||||||
|
resolveMessagePrefix(cfg, "main", {
|
||||||
|
configured: "[x]",
|
||||||
|
hasAllowFrom: true,
|
||||||
|
}),
|
||||||
|
).toBe("[x]");
|
||||||
|
expect(
|
||||||
|
resolveMessagePrefix(cfg, "main", {
|
||||||
|
configured: "",
|
||||||
|
hasAllowFrom: false,
|
||||||
|
}),
|
||||||
|
).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults messagePrefix based on allowFrom + identity", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: { list: [{ id: "main", identity: { name: "Richbot" } }] },
|
||||||
|
};
|
||||||
|
expect(resolveMessagePrefix(cfg, "main", { hasAllowFrom: true })).toBe("");
|
||||||
|
expect(resolveMessagePrefix(cfg, "main", { hasAllowFrom: false })).toBe(
|
||||||
|
"[Richbot]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to [clawdbot] when identity is missing", () => {
|
||||||
|
const cfg: ClawdbotConfig = {};
|
||||||
|
expect(resolveMessagePrefix(cfg, "main", { hasAllowFrom: false })).toBe(
|
||||||
|
"[clawdbot]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("response prefix resolution", () => {
|
||||||
|
it("does not apply any default when unset", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: { list: [{ id: "main", identity: { name: "Richbot" } }] },
|
||||||
|
};
|
||||||
|
expect(resolveResponsePrefix(cfg, "main")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns explicit responsePrefix when set", () => {
|
||||||
|
const cfg: ClawdbotConfig = { messages: { responsePrefix: "PFX" } };
|
||||||
|
expect(resolveResponsePrefix(cfg, "main")).toBe("PFX");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports responsePrefix: auto (identity-derived opt-in)", () => {
|
||||||
|
const withIdentity: ClawdbotConfig = {
|
||||||
|
agents: { list: [{ id: "main", identity: { name: "Richbot" } }] },
|
||||||
|
messages: { responsePrefix: "auto" },
|
||||||
|
};
|
||||||
|
expect(resolveResponsePrefix(withIdentity, "main")).toBe("[Richbot]");
|
||||||
|
|
||||||
|
const withoutIdentity: ClawdbotConfig = {
|
||||||
|
messages: { responsePrefix: "auto" },
|
||||||
|
};
|
||||||
|
expect(resolveResponsePrefix(withoutIdentity, "main")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -32,9 +32,9 @@ export function resolveIdentityNamePrefix(
|
|||||||
export function resolveMessagePrefix(
|
export function resolveMessagePrefix(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
opts?: { hasAllowFrom?: boolean; fallback?: string },
|
opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string },
|
||||||
): string {
|
): string {
|
||||||
const configured = cfg.messages?.messagePrefix;
|
const configured = opts?.configured ?? cfg.messages?.messagePrefix;
|
||||||
if (configured !== undefined) return configured;
|
if (configured !== undefined) return configured;
|
||||||
|
|
||||||
const hasAllowFrom = opts?.hasAllowFrom === true;
|
const hasAllowFrom = opts?.hasAllowFrom === true;
|
||||||
@@ -47,10 +47,15 @@ export function resolveMessagePrefix(
|
|||||||
|
|
||||||
export function resolveResponsePrefix(
|
export function resolveResponsePrefix(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
_agentId: string,
|
agentId: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const configured = cfg.messages?.responsePrefix;
|
const configured = cfg.messages?.responsePrefix;
|
||||||
if (configured !== undefined) return configured;
|
if (configured !== undefined) {
|
||||||
|
if (configured === "auto") {
|
||||||
|
return resolveIdentityNamePrefix(cfg, agentId);
|
||||||
|
}
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export async function routeReply(
|
|||||||
cfg,
|
cfg,
|
||||||
resolveAgentIdFromSessionKey(params.sessionKey),
|
resolveAgentIdFromSessionKey(params.sessionKey),
|
||||||
).responsePrefix
|
).responsePrefix
|
||||||
: cfg.messages?.responsePrefix;
|
: cfg.messages?.responsePrefix === "auto"
|
||||||
|
? undefined
|
||||||
|
: cfg.messages?.responsePrefix;
|
||||||
const normalized = normalizeReplyPayload(payload, {
|
const normalized = normalizeReplyPayload(payload, {
|
||||||
responsePrefix,
|
responsePrefix,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ export type WhatsAppActionConfig = {
|
|||||||
export type WhatsAppConfig = {
|
export type WhatsAppConfig = {
|
||||||
/** Optional per-account WhatsApp configuration (multi-account). */
|
/** Optional per-account WhatsApp configuration (multi-account). */
|
||||||
accounts?: Record<string, WhatsAppAccountConfig>;
|
accounts?: Record<string, WhatsAppAccountConfig>;
|
||||||
|
/**
|
||||||
|
* Inbound message prefix (WhatsApp only).
|
||||||
|
* Default: `[{agents.list[].identity.name}]` (or `[clawdbot]`) when allowFrom is empty, else `""`.
|
||||||
|
*/
|
||||||
|
messagePrefix?: string;
|
||||||
/** Direct message access policy (default: pairing). */
|
/** Direct message access policy (default: pairing). */
|
||||||
dmPolicy?: DmPolicy;
|
dmPolicy?: DmPolicy;
|
||||||
/**
|
/**
|
||||||
@@ -152,6 +157,8 @@ export type WhatsAppAccountConfig = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
/** If false, do not start this WhatsApp account provider. Default: true. */
|
/** If false, do not start this WhatsApp account provider. Default: true. */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
/** Inbound message prefix override for this account (WhatsApp only). */
|
||||||
|
messagePrefix?: string;
|
||||||
/** Override auth directory (Baileys multi-file auth state). */
|
/** Override auth directory (Baileys multi-file auth state). */
|
||||||
authDir?: string;
|
authDir?: string;
|
||||||
/** Direct message access policy (default: pairing). */
|
/** Direct message access policy (default: pairing). */
|
||||||
@@ -931,8 +938,15 @@ export type AudioConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type MessagesConfig = {
|
export type MessagesConfig = {
|
||||||
messagePrefix?: string; // Prefix added to all inbound messages (default: "[{agents.list[].identity.name}]" or "[clawdbot]" if no allowFrom, else "")
|
/** @deprecated Use `whatsapp.messagePrefix` (WhatsApp-only inbound prefix). */
|
||||||
responsePrefix?: string; // Prefix auto-added to all outbound replies (default: none)
|
messagePrefix?: string;
|
||||||
|
/**
|
||||||
|
* Prefix auto-added to all outbound replies.
|
||||||
|
* - string: explicit prefix
|
||||||
|
* - special value: `"auto"` derives `[{agents.list[].identity.name}]` for the routed agent (when set)
|
||||||
|
* Default: none
|
||||||
|
*/
|
||||||
|
responsePrefix?: string;
|
||||||
groupChat?: GroupChatConfig;
|
groupChat?: GroupChatConfig;
|
||||||
queue?: QueueConfig;
|
queue?: QueueConfig;
|
||||||
/** Emoji reaction used to acknowledge inbound messages (empty disables). */
|
/** Emoji reaction used to acknowledge inbound messages (empty disables). */
|
||||||
|
|||||||
@@ -1229,6 +1229,7 @@ export const ClawdbotSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
messagePrefix: z.string().optional(),
|
||||||
/** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
|
/** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
|
||||||
authDir: z.string().optional(),
|
authDir: z.string().optional(),
|
||||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||||
@@ -1268,6 +1269,7 @@ export const ClawdbotSchema = z.object({
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||||
|
messagePrefix: z.string().optional(),
|
||||||
selfChatMode: z.boolean().optional(),
|
selfChatMode: z.boolean().optional(),
|
||||||
allowFrom: z.array(z.string()).optional(),
|
allowFrom: z.array(z.string()).optional(),
|
||||||
groupAllowFrom: z.array(z.string()).optional(),
|
groupAllowFrom: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type ResolvedWhatsAppAccount = {
|
|||||||
accountId: string;
|
accountId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
messagePrefix?: string;
|
||||||
authDir: string;
|
authDir: string;
|
||||||
isLegacyAuthDir: boolean;
|
isLegacyAuthDir: boolean;
|
||||||
selfChatMode?: boolean;
|
selfChatMode?: boolean;
|
||||||
@@ -111,6 +112,10 @@ export function resolveWhatsAppAccount(params: {
|
|||||||
accountId,
|
accountId,
|
||||||
name: accountCfg?.name?.trim() || undefined,
|
name: accountCfg?.name?.trim() || undefined,
|
||||||
enabled,
|
enabled,
|
||||||
|
messagePrefix:
|
||||||
|
accountCfg?.messagePrefix ??
|
||||||
|
params.cfg.whatsapp?.messagePrefix ??
|
||||||
|
params.cfg.messages?.messagePrefix,
|
||||||
authDir,
|
authDir,
|
||||||
isLegacyAuthDir: isLegacy,
|
isLegacyAuthDir: isLegacy,
|
||||||
selfChatMode: accountCfg?.selfChatMode ?? params.cfg.whatsapp?.selfChatMode,
|
selfChatMode: accountCfg?.selfChatMode ?? params.cfg.whatsapp?.selfChatMode,
|
||||||
|
|||||||
@@ -784,6 +784,7 @@ export async function monitorWebProvider(
|
|||||||
...baseCfg,
|
...baseCfg,
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
...baseCfg.whatsapp,
|
...baseCfg.whatsapp,
|
||||||
|
messagePrefix: account.messagePrefix,
|
||||||
allowFrom: account.allowFrom,
|
allowFrom: account.allowFrom,
|
||||||
groupAllowFrom: account.groupAllowFrom,
|
groupAllowFrom: account.groupAllowFrom,
|
||||||
groupPolicy: account.groupPolicy,
|
groupPolicy: account.groupPolicy,
|
||||||
@@ -1039,8 +1040,9 @@ export async function monitorWebProvider(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildLine = (msg: WebInboundMsg, agentId: string) => {
|
const buildLine = (msg: WebInboundMsg, agentId: string) => {
|
||||||
// Build message prefix: explicit config > identity name > default based on allowFrom
|
// WhatsApp inbound prefix: whatsapp.messagePrefix > legacy messages.messagePrefix > identity/defaults
|
||||||
const messagePrefix = resolveMessagePrefix(cfg, agentId, {
|
const messagePrefix = resolveMessagePrefix(cfg, agentId, {
|
||||||
|
configured: cfg.whatsapp?.messagePrefix,
|
||||||
hasAllowFrom: (cfg.whatsapp?.allowFrom?.length ?? 0) > 0,
|
hasAllowFrom: (cfg.whatsapp?.allowFrom?.length ?? 0) > 0,
|
||||||
});
|
});
|
||||||
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
|
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
|
||||||
|
|||||||
Reference in New Issue
Block a user