mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:44:31 +00:00
refactor(voice-call): split manager into facade and context slices
This commit is contained in:
@@ -8,18 +8,32 @@ export type TranscriptWaiter = {
|
||||
timeout: NodeJS.Timeout;
|
||||
};
|
||||
|
||||
export type CallManagerContext = {
|
||||
export type CallManagerRuntimeState = {
|
||||
activeCalls: Map<CallId, CallRecord>;
|
||||
providerCallIdMap: Map<string, CallId>;
|
||||
processedEventIds: Set<string>;
|
||||
/** Provider call IDs we already sent a reject hangup for; avoids duplicate hangup calls. */
|
||||
rejectedProviderCallIds: Set<string>;
|
||||
/** Optional runtime hook invoked after an event transitions a call into answered state. */
|
||||
onCallAnswered?: (call: CallRecord) => void;
|
||||
};
|
||||
|
||||
export type CallManagerRuntimeDeps = {
|
||||
provider: VoiceCallProvider | null;
|
||||
config: VoiceCallConfig;
|
||||
storePath: string;
|
||||
webhookUrl: string | null;
|
||||
};
|
||||
|
||||
export type CallManagerTransientState = {
|
||||
transcriptWaiters: Map<CallId, TranscriptWaiter>;
|
||||
maxDurationTimers: Map<CallId, NodeJS.Timeout>;
|
||||
};
|
||||
|
||||
export type CallManagerHooks = {
|
||||
/** Optional runtime hook invoked after an event transitions a call into answered state. */
|
||||
onCallAnswered?: (call: CallRecord) => void;
|
||||
};
|
||||
|
||||
export type CallManagerContext = CallManagerRuntimeState &
|
||||
CallManagerRuntimeDeps &
|
||||
CallManagerTransientState &
|
||||
CallManagerHooks;
|
||||
|
||||
@@ -13,10 +13,21 @@ import {
|
||||
startMaxDurationTimer,
|
||||
} from "./timers.js";
|
||||
|
||||
function shouldAcceptInbound(
|
||||
config: CallManagerContext["config"],
|
||||
from: string | undefined,
|
||||
): boolean {
|
||||
type EventContext = Pick<
|
||||
CallManagerContext,
|
||||
| "activeCalls"
|
||||
| "providerCallIdMap"
|
||||
| "processedEventIds"
|
||||
| "rejectedProviderCallIds"
|
||||
| "provider"
|
||||
| "config"
|
||||
| "storePath"
|
||||
| "transcriptWaiters"
|
||||
| "maxDurationTimers"
|
||||
| "onCallAnswered"
|
||||
>;
|
||||
|
||||
function shouldAcceptInbound(config: EventContext["config"], from: string | undefined): boolean {
|
||||
const { inboundPolicy: policy, allowFrom } = config;
|
||||
|
||||
switch (policy) {
|
||||
@@ -49,7 +60,7 @@ function shouldAcceptInbound(
|
||||
}
|
||||
|
||||
function createInboundCall(params: {
|
||||
ctx: CallManagerContext;
|
||||
ctx: EventContext;
|
||||
providerCallId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
@@ -80,7 +91,7 @@ function createInboundCall(params: {
|
||||
return callRecord;
|
||||
}
|
||||
|
||||
export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): void {
|
||||
export function processEvent(ctx: EventContext, event: NormalizedEvent): void {
|
||||
if (ctx.processedEventIds.has(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,39 @@ import {
|
||||
} from "./timers.js";
|
||||
import { generateNotifyTwiml } from "./twiml.js";
|
||||
|
||||
type InitiateContext = Pick<
|
||||
CallManagerContext,
|
||||
"activeCalls" | "providerCallIdMap" | "provider" | "config" | "storePath" | "webhookUrl"
|
||||
>;
|
||||
|
||||
type SpeakContext = Pick<
|
||||
CallManagerContext,
|
||||
"activeCalls" | "providerCallIdMap" | "provider" | "config" | "storePath"
|
||||
>;
|
||||
|
||||
type ConversationContext = Pick<
|
||||
CallManagerContext,
|
||||
| "activeCalls"
|
||||
| "providerCallIdMap"
|
||||
| "provider"
|
||||
| "config"
|
||||
| "storePath"
|
||||
| "transcriptWaiters"
|
||||
| "maxDurationTimers"
|
||||
>;
|
||||
|
||||
type EndCallContext = Pick<
|
||||
CallManagerContext,
|
||||
| "activeCalls"
|
||||
| "providerCallIdMap"
|
||||
| "provider"
|
||||
| "storePath"
|
||||
| "transcriptWaiters"
|
||||
| "maxDurationTimers"
|
||||
>;
|
||||
|
||||
export async function initiateCall(
|
||||
ctx: CallManagerContext,
|
||||
ctx: InitiateContext,
|
||||
to: string,
|
||||
sessionKey?: string,
|
||||
options?: OutboundCallOptions | string,
|
||||
@@ -113,7 +144,7 @@ export async function initiateCall(
|
||||
}
|
||||
|
||||
export async function speak(
|
||||
ctx: CallManagerContext,
|
||||
ctx: SpeakContext,
|
||||
callId: CallId,
|
||||
text: string,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
@@ -149,7 +180,7 @@ export async function speak(
|
||||
}
|
||||
|
||||
export async function speakInitialMessage(
|
||||
ctx: CallManagerContext,
|
||||
ctx: ConversationContext,
|
||||
providerCallId: string,
|
||||
): Promise<void> {
|
||||
const call = getCallByProviderCallId({
|
||||
@@ -197,7 +228,7 @@ export async function speakInitialMessage(
|
||||
}
|
||||
|
||||
export async function continueCall(
|
||||
ctx: CallManagerContext,
|
||||
ctx: ConversationContext,
|
||||
callId: CallId,
|
||||
prompt: string,
|
||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||
@@ -234,7 +265,7 @@ export async function continueCall(
|
||||
}
|
||||
|
||||
export async function endCall(
|
||||
ctx: CallManagerContext,
|
||||
ctx: EndCallContext,
|
||||
callId: CallId,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
|
||||
@@ -2,7 +2,20 @@ import type { CallManagerContext } from "./context.js";
|
||||
import { TerminalStates, type CallId } from "../types.js";
|
||||
import { persistCallRecord } from "./store.js";
|
||||
|
||||
export function clearMaxDurationTimer(ctx: CallManagerContext, callId: CallId): void {
|
||||
type TimerContext = Pick<
|
||||
CallManagerContext,
|
||||
"activeCalls" | "maxDurationTimers" | "config" | "storePath" | "transcriptWaiters"
|
||||
>;
|
||||
type MaxDurationTimerContext = Pick<
|
||||
TimerContext,
|
||||
"activeCalls" | "maxDurationTimers" | "config" | "storePath"
|
||||
>;
|
||||
type TranscriptWaiterContext = Pick<TimerContext, "transcriptWaiters">;
|
||||
|
||||
export function clearMaxDurationTimer(
|
||||
ctx: Pick<MaxDurationTimerContext, "maxDurationTimers">,
|
||||
callId: CallId,
|
||||
): void {
|
||||
const timer = ctx.maxDurationTimers.get(callId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
@@ -11,7 +24,7 @@ export function clearMaxDurationTimer(ctx: CallManagerContext, callId: CallId):
|
||||
}
|
||||
|
||||
export function startMaxDurationTimer(params: {
|
||||
ctx: CallManagerContext;
|
||||
ctx: MaxDurationTimerContext;
|
||||
callId: CallId;
|
||||
onTimeout: (callId: CallId) => Promise<void>;
|
||||
}): void {
|
||||
@@ -38,7 +51,7 @@ export function startMaxDurationTimer(params: {
|
||||
params.ctx.maxDurationTimers.set(params.callId, timer);
|
||||
}
|
||||
|
||||
export function clearTranscriptWaiter(ctx: CallManagerContext, callId: CallId): void {
|
||||
export function clearTranscriptWaiter(ctx: TranscriptWaiterContext, callId: CallId): void {
|
||||
const waiter = ctx.transcriptWaiters.get(callId);
|
||||
if (!waiter) {
|
||||
return;
|
||||
@@ -48,7 +61,7 @@ export function clearTranscriptWaiter(ctx: CallManagerContext, callId: CallId):
|
||||
}
|
||||
|
||||
export function rejectTranscriptWaiter(
|
||||
ctx: CallManagerContext,
|
||||
ctx: TranscriptWaiterContext,
|
||||
callId: CallId,
|
||||
reason: string,
|
||||
): void {
|
||||
@@ -61,7 +74,7 @@ export function rejectTranscriptWaiter(
|
||||
}
|
||||
|
||||
export function resolveTranscriptWaiter(
|
||||
ctx: CallManagerContext,
|
||||
ctx: TranscriptWaiterContext,
|
||||
callId: CallId,
|
||||
transcript: string,
|
||||
): void {
|
||||
@@ -73,7 +86,7 @@ export function resolveTranscriptWaiter(
|
||||
waiter.resolve(transcript);
|
||||
}
|
||||
|
||||
export function waitForFinalTranscript(ctx: CallManagerContext, callId: CallId): Promise<string> {
|
||||
export function waitForFinalTranscript(ctx: TimerContext, callId: CallId): Promise<string> {
|
||||
// Only allow one in-flight waiter per call.
|
||||
rejectTranscriptWaiter(ctx, callId, "Transcript waiter replaced");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user