fix: restore notify init + Plivo numbers (#846) (thanks @vrknetha)
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
- Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging.
|
- Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging.
|
||||||
- Security: expand `clawdbot security audit` checks (model hygiene, config includes, plugin allowlists, exposure matrix) and extend `--fix` to tighten more sensitive state paths.
|
- Security: expand `clawdbot security audit` checks (model hygiene, config includes, plugin allowlists, exposure matrix) and extend `--fix` to tighten more sensitive state paths.
|
||||||
- Security: add `SECURITY.md` reporting policy.
|
- Security: add `SECURITY.md` reporting policy.
|
||||||
|
- Voice Call: add Plivo provider for the voice-call plugin. (#846) — thanks @vrknetha.
|
||||||
- Plugins: add Zalo channel plugin with gateway HTTP hooks and onboarding install prompt. (#854) — thanks @longmaba.
|
- Plugins: add Zalo channel plugin with gateway HTTP hooks and onboarding install prompt. (#854) — thanks @longmaba.
|
||||||
- Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require `--accept-risk` for `--non-interactive`.
|
- Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require `--accept-risk` for `--non-interactive`.
|
||||||
- Docs: expand gateway security hardening guidance and incident response checklist.
|
- Docs: expand gateway security hardening guidance and incident response checklist.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type { VoiceCallProvider } from "./providers/base.js";
|
|||||||
|
|
||||||
class FakeProvider implements VoiceCallProvider {
|
class FakeProvider implements VoiceCallProvider {
|
||||||
readonly name = "plivo" as const;
|
readonly name = "plivo" as const;
|
||||||
|
readonly playTtsCalls: PlayTtsInput[] = [];
|
||||||
|
|
||||||
verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult {
|
verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -31,7 +32,9 @@ class FakeProvider implements VoiceCallProvider {
|
|||||||
return { providerCallId: "request-uuid", status: "initiated" };
|
return { providerCallId: "request-uuid", status: "initiated" };
|
||||||
}
|
}
|
||||||
async hangupCall(_input: HangupCallInput): Promise<void> {}
|
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 startListening(_input: StartListeningInput): Promise<void> {}
|
||||||
async stopListening(_input: StopListeningInput): 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("call-uuid")?.callId).toBe(callId);
|
||||||
expect(manager.getCallByProviderCallId("request-uuid")).toBeUndefined();
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -672,41 +672,13 @@ export class CallManager {
|
|||||||
|
|
||||||
if (!initialMessage) return;
|
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;
|
if (!this.provider || !call.providerCallId) return;
|
||||||
|
|
||||||
// Twilio has provider-specific state for speaking (<Say> fallback) and can
|
// Twilio has provider-specific state for speaking (<Say> fallback) and can
|
||||||
// fail for inbound calls; keep existing Twilio behavior unchanged.
|
// fail for inbound calls; keep existing Twilio behavior unchanged.
|
||||||
if (this.provider.name === "twilio") return;
|
if (this.provider.name === "twilio") return;
|
||||||
|
|
||||||
// Clear the initial message so it only plays once.
|
void this.speakInitialMessage(call.providerCallId);
|
||||||
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)
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ export class PlivoProvider implements VoiceCallProvider {
|
|||||||
private static normalizeNumber(numberOrSip: string): string {
|
private static normalizeNumber(numberOrSip: string): string {
|
||||||
const trimmed = numberOrSip.trim();
|
const trimmed = numberOrSip.trim();
|
||||||
if (trimmed.toLowerCase().startsWith("sip:")) return trimmed;
|
if (trimmed.toLowerCase().startsWith("sip:")) return trimmed;
|
||||||
return trimmed.startsWith("+") ? trimmed.slice(1) : trimmed;
|
return trimmed.replace(/[^\d+]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static xmlEmpty(): string {
|
private static xmlEmpty(): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user