feat(messages): add whatsapp messagePrefix and responsePrefix auto

This commit is contained in:
Peter Steinberger
2026-01-09 19:28:59 +00:00
parent 0a4cb0d264
commit 2977b296e6
10 changed files with 112 additions and 16 deletions

View 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();
});
});

View File

@@ -32,9 +32,9 @@ export function resolveIdentityNamePrefix(
export function resolveMessagePrefix(
cfg: ClawdbotConfig,
agentId: string,
opts?: { hasAllowFrom?: boolean; fallback?: string },
opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string },
): string {
const configured = cfg.messages?.messagePrefix;
const configured = opts?.configured ?? cfg.messages?.messagePrefix;
if (configured !== undefined) return configured;
const hasAllowFrom = opts?.hasAllowFrom === true;
@@ -47,10 +47,15 @@ export function resolveMessagePrefix(
export function resolveResponsePrefix(
cfg: ClawdbotConfig,
_agentId: string,
agentId: string,
): string | undefined {
const configured = cfg.messages?.responsePrefix;
if (configured !== undefined) return configured;
if (configured !== undefined) {
if (configured === "auto") {
return resolveIdentityNamePrefix(cfg, agentId);
}
return configured;
}
return undefined;
}

View File

@@ -69,7 +69,9 @@ export async function routeReply(
cfg,
resolveAgentIdFromSessionKey(params.sessionKey),
).responsePrefix
: cfg.messages?.responsePrefix;
: cfg.messages?.responsePrefix === "auto"
? undefined
: cfg.messages?.responsePrefix;
const normalized = normalizeReplyPayload(payload, {
responsePrefix,
});

View File

@@ -111,6 +111,11 @@ export type WhatsAppActionConfig = {
export type WhatsAppConfig = {
/** Optional per-account WhatsApp configuration (multi-account). */
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). */
dmPolicy?: DmPolicy;
/**
@@ -152,6 +157,8 @@ export type WhatsAppAccountConfig = {
name?: string;
/** If false, do not start this WhatsApp account provider. Default: true. */
enabled?: boolean;
/** Inbound message prefix override for this account (WhatsApp only). */
messagePrefix?: string;
/** Override auth directory (Baileys multi-file auth state). */
authDir?: string;
/** Direct message access policy (default: pairing). */
@@ -931,8 +938,15 @@ export type AudioConfig = {
};
export type MessagesConfig = {
messagePrefix?: string; // Prefix added to all inbound messages (default: "[{agents.list[].identity.name}]" or "[clawdbot]" if no allowFrom, else "")
responsePrefix?: string; // Prefix auto-added to all outbound replies (default: none)
/** @deprecated Use `whatsapp.messagePrefix` (WhatsApp-only inbound prefix). */
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;
queue?: QueueConfig;
/** Emoji reaction used to acknowledge inbound messages (empty disables). */

View File

@@ -1229,6 +1229,7 @@ export const ClawdbotSchema = z.object({
.object({
name: z.string().optional(),
enabled: z.boolean().optional(),
messagePrefix: z.string().optional(),
/** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
authDir: z.string().optional(),
dmPolicy: DmPolicySchema.optional().default("pairing"),
@@ -1268,6 +1269,7 @@ export const ClawdbotSchema = z.object({
)
.optional(),
dmPolicy: DmPolicySchema.optional().default("pairing"),
messagePrefix: z.string().optional(),
selfChatMode: z.boolean().optional(),
allowFrom: z.array(z.string()).optional(),
groupAllowFrom: z.array(z.string()).optional(),

View File

@@ -15,6 +15,7 @@ export type ResolvedWhatsAppAccount = {
accountId: string;
name?: string;
enabled: boolean;
messagePrefix?: string;
authDir: string;
isLegacyAuthDir: boolean;
selfChatMode?: boolean;
@@ -111,6 +112,10 @@ export function resolveWhatsAppAccount(params: {
accountId,
name: accountCfg?.name?.trim() || undefined,
enabled,
messagePrefix:
accountCfg?.messagePrefix ??
params.cfg.whatsapp?.messagePrefix ??
params.cfg.messages?.messagePrefix,
authDir,
isLegacyAuthDir: isLegacy,
selfChatMode: accountCfg?.selfChatMode ?? params.cfg.whatsapp?.selfChatMode,

View File

@@ -784,6 +784,7 @@ export async function monitorWebProvider(
...baseCfg,
whatsapp: {
...baseCfg.whatsapp,
messagePrefix: account.messagePrefix,
allowFrom: account.allowFrom,
groupAllowFrom: account.groupAllowFrom,
groupPolicy: account.groupPolicy,
@@ -1039,8 +1040,9 @@ export async function monitorWebProvider(
};
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, {
configured: cfg.whatsapp?.messagePrefix,
hasAllowFrom: (cfg.whatsapp?.allowFrom?.length ?? 0) > 0,
});
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";