fix: use final heartbeat payload
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Heartbeat replies now strip repeated `HEARTBEAT_OK` tails to avoid accidental “OK OK” spam.
|
- Heartbeat replies now strip repeated `HEARTBEAT_OK` tails to avoid accidental “OK OK” spam.
|
||||||
|
- Heartbeat delivery now uses the last non-empty payload, preventing tool preambles from swallowing the final reply.
|
||||||
- Heartbeat failure logs now include the error reason instead of `[object Object]`.
|
- Heartbeat failure logs now include the error reason instead of `[object Object]`.
|
||||||
- Duration strings now accept `h` (hours) where durations are parsed (e.g., heartbeat intervals).
|
- Duration strings now accept `h` (hours) where durations are parsed (e.g., heartbeat intervals).
|
||||||
- WhatsApp inbound now normalizes more wrapper types so quoted reply bodies are extracted reliably.
|
- WhatsApp inbound now normalizes more wrapper types so quoted reply bodies are extracted reliably.
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
|
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
|
||||||
|
import * as replyModule from "../auto-reply/reply.js";
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
|
runHeartbeatOnce,
|
||||||
resolveHeartbeatDeliveryTarget,
|
resolveHeartbeatDeliveryTarget,
|
||||||
resolveHeartbeatIntervalMs,
|
resolveHeartbeatIntervalMs,
|
||||||
resolveHeartbeatPrompt,
|
resolveHeartbeatPrompt,
|
||||||
@@ -113,3 +118,60 @@ describe("resolveHeartbeatDeliveryTarget", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("runHeartbeatOnce", () => {
|
||||||
|
it("uses the last non-empty payload for delivery", async () => {
|
||||||
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-hb-"));
|
||||||
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
storePath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
main: {
|
||||||
|
sessionId: "sid",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
lastChannel: "whatsapp",
|
||||||
|
lastTo: "+1555",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cfg: ClawdisConfig = {
|
||||||
|
agent: {
|
||||||
|
heartbeat: { every: "5m", target: "whatsapp", to: "+1555" },
|
||||||
|
},
|
||||||
|
routing: { allowFrom: ["*"] },
|
||||||
|
session: { store: storePath },
|
||||||
|
};
|
||||||
|
|
||||||
|
replySpy.mockResolvedValue([
|
||||||
|
{ text: "Let me check..." },
|
||||||
|
{ text: "Final alert" },
|
||||||
|
]);
|
||||||
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
|
messageId: "m1",
|
||||||
|
toJid: "jid",
|
||||||
|
});
|
||||||
|
|
||||||
|
await runHeartbeatOnce({
|
||||||
|
cfg,
|
||||||
|
deps: { sendWhatsApp, getQueueSize: () => 0, nowMs: () => 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendWhatsApp).toHaveBeenCalledWith(
|
||||||
|
"+1555",
|
||||||
|
"Final alert",
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
replySpy.mockRestore();
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -93,6 +93,25 @@ function resolveHeartbeatSession(cfg: ClawdisConfig) {
|
|||||||
return { sessionKey, storePath, store, entry };
|
return { sessionKey, storePath, store, entry };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveHeartbeatReplyPayload(
|
||||||
|
replyResult: ReplyPayload | ReplyPayload[] | undefined,
|
||||||
|
): ReplyPayload | undefined {
|
||||||
|
if (!replyResult) return undefined;
|
||||||
|
if (!Array.isArray(replyResult)) return replyResult;
|
||||||
|
for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
|
||||||
|
const payload = replyResult[idx];
|
||||||
|
if (!payload) continue;
|
||||||
|
if (
|
||||||
|
payload.text ||
|
||||||
|
payload.mediaUrl ||
|
||||||
|
(payload.mediaUrls && payload.mediaUrls.length > 0)
|
||||||
|
) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveHeartbeatSender(params: {
|
function resolveHeartbeatSender(params: {
|
||||||
allowFrom: Array<string | number>;
|
allowFrom: Array<string | number>;
|
||||||
lastTo?: string;
|
lastTo?: string;
|
||||||
@@ -318,9 +337,7 @@ export async function runHeartbeatOnce(opts: {
|
|||||||
{ isHeartbeat: true },
|
{ isHeartbeat: true },
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
const replyPayload = Array.isArray(replyResult)
|
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
|
||||||
? replyResult[0]
|
|
||||||
: replyResult;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!replyPayload ||
|
!replyPayload ||
|
||||||
|
|||||||
@@ -190,6 +190,25 @@ function isSilentReply(payload?: ReplyPayload): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveHeartbeatReplyPayload(
|
||||||
|
replyResult: ReplyPayload | ReplyPayload[] | undefined,
|
||||||
|
): ReplyPayload | undefined {
|
||||||
|
if (!replyResult) return undefined;
|
||||||
|
if (!Array.isArray(replyResult)) return replyResult;
|
||||||
|
for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
|
||||||
|
const payload = replyResult[idx];
|
||||||
|
if (!payload) continue;
|
||||||
|
if (
|
||||||
|
payload.text ||
|
||||||
|
payload.mediaUrl ||
|
||||||
|
(payload.mediaUrls && payload.mediaUrls.length > 0)
|
||||||
|
) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export async function runWebHeartbeatOnce(opts: {
|
export async function runWebHeartbeatOnce(opts: {
|
||||||
cfg?: ReturnType<typeof loadConfig>;
|
cfg?: ReturnType<typeof loadConfig>;
|
||||||
to: string;
|
to: string;
|
||||||
@@ -291,9 +310,7 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
{ isHeartbeat: true },
|
{ isHeartbeat: true },
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
const replyPayload = Array.isArray(replyResult)
|
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
|
||||||
? replyResult[0]
|
|
||||||
: replyResult;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!replyPayload ||
|
!replyPayload ||
|
||||||
|
|||||||
Reference in New Issue
Block a user