mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 04:31:23 +00:00
feat(heartbeat): add directPolicy and restore default direct delivery
This commit is contained in:
@@ -234,4 +234,32 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts heartbeat directPolicy enum values", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
agents: {
|
||||
defaults: { heartbeat: { target: "last", directPolicy: "block" } },
|
||||
list: [{ id: "pi", heartbeat: { directPolicy: "allow" } }],
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid heartbeat directPolicy values", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
agents: {
|
||||
defaults: { heartbeat: { directPolicy: "maybe" } },
|
||||
list: [{ id: "pi" }],
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
const hasIssue = res.issues.some(
|
||||
(issue) => issue.path === "agents.defaults.heartbeat.directPolicy",
|
||||
);
|
||||
expect(hasIssue).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1238,6 +1238,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Shows degraded/error heartbeat alerts when true so operator channels surface problems promptly. Keep enabled in production so broken channel states are visible.",
|
||||
"channels.defaults.heartbeat.useIndicator":
|
||||
"Enables concise indicator-style heartbeat rendering instead of verbose status text where supported. Use indicator mode for dense dashboards with many active channels.",
|
||||
"agents.defaults.heartbeat.directPolicy":
|
||||
'Controls whether heartbeat delivery may target direct/DM chats: "allow" (default) permits DM delivery and "block" suppresses direct-target sends.',
|
||||
"agents.list.*.heartbeat.directPolicy":
|
||||
'Per-agent override for heartbeat direct/DM delivery policy; use "block" for agents that should only send heartbeat alerts to non-DM destinations.',
|
||||
"channels.telegram.configWrites":
|
||||
"Allow Telegram to write config in response to channel events/commands (default: true).",
|
||||
"channels.telegram.botToken":
|
||||
|
||||
@@ -402,6 +402,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"Compaction Memory Flush Soft Threshold",
|
||||
"agents.defaults.compaction.memoryFlush.prompt": "Compaction Memory Flush Prompt",
|
||||
"agents.defaults.compaction.memoryFlush.systemPrompt": "Compaction Memory Flush System Prompt",
|
||||
"agents.defaults.heartbeat.directPolicy": "Heartbeat Direct Policy",
|
||||
"agents.list.*.heartbeat.directPolicy": "Heartbeat Direct Policy",
|
||||
"agents.defaults.heartbeat.suppressToolErrorWarnings": "Heartbeat Suppress Tool Error Warnings",
|
||||
"agents.defaults.sandbox.browser.network": "Sandbox Browser Network",
|
||||
"agents.defaults.sandbox.browser.cdpSourceRange": "Sandbox Browser CDP Source Port Range",
|
||||
|
||||
@@ -213,6 +213,8 @@ export type AgentDefaultsConfig = {
|
||||
session?: string;
|
||||
/** Delivery target ("last", "none", or a channel id). */
|
||||
target?: "last" | "none" | ChannelId;
|
||||
/** Direct/DM delivery policy. Default: "allow". */
|
||||
directPolicy?: "allow" | "block";
|
||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). Supports :topic:NNN suffix for Telegram topics. */
|
||||
to?: string;
|
||||
/** Optional account id for multi-account channels. */
|
||||
|
||||
@@ -26,6 +26,7 @@ export const HeartbeatSchema = z
|
||||
session: z.string().optional(),
|
||||
includeReasoning: z.boolean().optional(),
|
||||
target: z.string().optional(),
|
||||
directPolicy: z.union([z.literal("allow"), z.literal("block")]).optional(),
|
||||
to: z.string().optional(),
|
||||
accountId: z.string().optional(),
|
||||
prompt: z.string().optional(),
|
||||
|
||||
@@ -325,6 +325,30 @@ describe("resolveHeartbeatDeliveryTarget", () => {
|
||||
lastAccountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow direct target by default",
|
||||
cfg: { agents: { defaults: { heartbeat: { target: "last" } } } },
|
||||
entry: { ...baseEntry, lastChannel: "telegram", lastTo: "5232990709" },
|
||||
expected: {
|
||||
channel: "telegram",
|
||||
to: "5232990709",
|
||||
accountId: undefined,
|
||||
lastChannel: "telegram",
|
||||
lastAccountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "block direct target when directPolicy is block",
|
||||
cfg: { agents: { defaults: { heartbeat: { target: "last", directPolicy: "block" } } } },
|
||||
entry: { ...baseEntry, lastChannel: "telegram", lastTo: "5232990709" },
|
||||
expected: {
|
||||
channel: "none",
|
||||
reason: "dm-blocked",
|
||||
accountId: undefined,
|
||||
lastChannel: "telegram",
|
||||
lastAccountId: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
expect(
|
||||
|
||||
@@ -301,7 +301,7 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
expect(resolved.to).toBe("63448508");
|
||||
});
|
||||
|
||||
it("blocks heartbeat delivery to Slack DMs and avoids inherited threadId", () => {
|
||||
it("allows heartbeat delivery to Slack DMs and avoids inherited threadId by default", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
@@ -317,12 +317,34 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("slack");
|
||||
expect(resolved.to).toBe("user:U123");
|
||||
expect(resolved.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("blocks heartbeat delivery to Slack DMs when directPolicy is block", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
entry: {
|
||||
sessionId: "sess-heartbeat-outbound",
|
||||
updatedAt: 1,
|
||||
lastChannel: "slack",
|
||||
lastTo: "user:U123",
|
||||
lastThreadId: "1739142736.000100",
|
||||
},
|
||||
heartbeat: {
|
||||
target: "last",
|
||||
directPolicy: "block",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("none");
|
||||
expect(resolved.reason).toBe("dm-blocked");
|
||||
expect(resolved.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("blocks heartbeat delivery to Discord DMs", () => {
|
||||
it("allows heartbeat delivery to Discord DMs by default", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
@@ -337,11 +359,11 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("none");
|
||||
expect(resolved.reason).toBe("dm-blocked");
|
||||
expect(resolved.channel).toBe("discord");
|
||||
expect(resolved.to).toBe("user:12345");
|
||||
});
|
||||
|
||||
it("blocks heartbeat delivery to Telegram direct chats", () => {
|
||||
it("allows heartbeat delivery to Telegram direct chats by default", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
@@ -356,6 +378,26 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("telegram");
|
||||
expect(resolved.to).toBe("5232990709");
|
||||
});
|
||||
|
||||
it("blocks heartbeat delivery to Telegram direct chats when directPolicy is block", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
entry: {
|
||||
sessionId: "sess-heartbeat-telegram-direct",
|
||||
updatedAt: 1,
|
||||
lastChannel: "telegram",
|
||||
lastTo: "5232990709",
|
||||
},
|
||||
heartbeat: {
|
||||
target: "last",
|
||||
directPolicy: "block",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("none");
|
||||
expect(resolved.reason).toBe("dm-blocked");
|
||||
});
|
||||
@@ -379,7 +421,7 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
expect(resolved.to).toBe("-1001234567890");
|
||||
});
|
||||
|
||||
it("blocks heartbeat delivery to WhatsApp direct chats", () => {
|
||||
it("allows heartbeat delivery to WhatsApp direct chats by default", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
@@ -394,8 +436,8 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("none");
|
||||
expect(resolved.reason).toBe("dm-blocked");
|
||||
expect(resolved.channel).toBe("whatsapp");
|
||||
expect(resolved.to).toBe("+15551234567");
|
||||
});
|
||||
|
||||
it("keeps heartbeat delivery to WhatsApp groups", () => {
|
||||
@@ -417,7 +459,7 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
expect(resolved.to).toBe("120363140186826074@g.us");
|
||||
});
|
||||
|
||||
it("uses session chatType hint when target parser cannot classify", () => {
|
||||
it("uses session chatType hint when target parser cannot classify and allows direct by default", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
@@ -433,6 +475,27 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("imessage");
|
||||
expect(resolved.to).toBe("chat-guid-unknown-shape");
|
||||
});
|
||||
|
||||
it("blocks session chatType direct hints when directPolicy is block", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
entry: {
|
||||
sessionId: "sess-heartbeat-imessage-direct",
|
||||
updatedAt: 1,
|
||||
lastChannel: "imessage",
|
||||
lastTo: "chat-guid-unknown-shape",
|
||||
chatType: "direct",
|
||||
},
|
||||
heartbeat: {
|
||||
target: "last",
|
||||
directPolicy: "block",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("none");
|
||||
expect(resolved.reason).toBe("dm-blocked");
|
||||
});
|
||||
|
||||
@@ -330,7 +330,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
to: resolved.to,
|
||||
sessionChatType: sessionChatTypeHint,
|
||||
});
|
||||
if (deliveryChatType === "direct") {
|
||||
if (deliveryChatType === "direct" && heartbeat?.directPolicy === "block") {
|
||||
return buildNoHeartbeatDeliveryTarget({
|
||||
reason: "dm-blocked",
|
||||
accountId: effectiveAccountId,
|
||||
|
||||
Reference in New Issue
Block a user