fix: restore notify init + Plivo numbers (#846) (thanks @vrknetha)

This commit is contained in:
Peter Steinberger
2026-01-15 07:28:14 +00:00
parent 2579609922
commit 3e6917c8ae
4 changed files with 40 additions and 32 deletions

View File

@@ -20,6 +20,7 @@ import type { VoiceCallProvider } from "./providers/base.js";
class FakeProvider implements VoiceCallProvider {
readonly name = "plivo" as const;
readonly playTtsCalls: PlayTtsInput[] = [];
verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult {
return { ok: true };
@@ -31,7 +32,9 @@ class FakeProvider implements VoiceCallProvider {
return { providerCallId: "request-uuid", status: "initiated" };
}
async hangupCall(_input: HangupCallInput): Promise<void> {}
async playTts(_input: PlayTtsInput): Promise<void> {}
async playTts(input: PlayTtsInput): Promise<void> {
this.playTtsCalls.push(input);
}
async startListening(_input: StartListeningInput): Promise<void> {}
async stopListening(_input: StopListeningInput): Promise<void> {}
}
@@ -69,5 +72,37 @@ describe("CallManager", () => {
expect(manager.getCallByProviderCallId("call-uuid")?.callId).toBe(callId);
expect(manager.getCallByProviderCallId("request-uuid")).toBeUndefined();
});
});
it("speaks initial message on answered for notify mode (non-Twilio)", async () => {
const config = VoiceCallConfigSchema.parse({
enabled: true,
provider: "plivo",
fromNumber: "+15550000000",
});
const storePath = path.join(os.tmpdir(), `clawdbot-voice-call-test-${Date.now()}`);
const provider = new FakeProvider();
const manager = new CallManager(config, storePath);
manager.initialize(provider, "https://example.com/voice/webhook");
const { callId, success } = await manager.initiateCall(
"+15550000002",
undefined,
{ message: "Hello there", mode: "notify" },
);
expect(success).toBe(true);
manager.processEvent({
id: "evt-2",
type: "call.answered",
callId,
providerCallId: "call-uuid",
timestamp: Date.now(),
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(provider.playTtsCalls).toHaveLength(1);
expect(provider.playTtsCalls[0]?.text).toBe("Hello there");
});
});

View File

@@ -672,41 +672,13 @@ export class CallManager {
if (!initialMessage) return;
// For outbound notify mode, we already use inline TwiML (provider-specific) to
// deliver the message and hang up; do not double-speak.
const mode = call.metadata?.mode as CallMode | undefined;
if (call.direction === "outbound" && mode === "notify") return;
if (!this.provider || !call.providerCallId) return;
// Twilio has provider-specific state for speaking (<Say> fallback) and can
// fail for inbound calls; keep existing Twilio behavior unchanged.
if (this.provider.name === "twilio") return;
// Clear the initial message so it only plays once.
if (call.metadata) {
delete call.metadata.initialMessage;
}
this.persistCallRecord(call);
void this.provider
.playTts({
callId: call.callId,
providerCallId: call.providerCallId,
text: initialMessage,
voice: this.config.tts.voice,
})
.then(() => {
this.addTranscriptEntry(call, "bot", initialMessage);
this.persistCallRecord(call);
})
.catch((err) => {
console.warn(
`[voice-call] Failed to speak initial message on answered: ${
err instanceof Error ? err.message : String(err)
}`,
);
});
void this.speakInitialMessage(call.providerCallId);
}
/**

View File

@@ -404,7 +404,7 @@ export class PlivoProvider implements VoiceCallProvider {
private static normalizeNumber(numberOrSip: string): string {
const trimmed = numberOrSip.trim();
if (trimmed.toLowerCase().startsWith("sip:")) return trimmed;
return trimmed.startsWith("+") ? trimmed.slice(1) : trimmed;
return trimmed.replace(/[^\d+]/g, "");
}
private static xmlEmpty(): string {