import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { VoiceCallConfig } from "./config.js"; import type { CallManagerContext } from "./manager/context.js"; import { processEvent as processManagerEvent } from "./manager/events.js"; import { getCallByProviderCallId as getCallByProviderCallIdFromMaps } from "./manager/lookup.js"; import {
continueCall as continueCallWithContext,
endCall as endCallWithContext,
initiateCall as initiateCallWithContext,
sendDtmf as sendDtmfWithContext,
speak as speakWithContext,
speakInitialMessage as speakInitialMessageWithContext,
} from "./manager/outbound.js"; import {
getCallHistoryFromStore,
loadActiveCallsFromStore,
persistCallRecord,
} from "./manager/store.js"; import { startMaxDurationTimer } from "./manager/timers.js"; import type { VoiceCallProvider } from "./providers/base.js"; import {
TerminalStates,
type CallId,
type CallRecord,
type NormalizedEvent,
type OutboundCallOptions,
} from "./types.js"; import { resolveUserPath } from "./utils.js";
// Rebuild providerCallIdMap from verified calls only this.providerCallIdMap = new Map(); for (const [callId, call] of verified) { if (call.providerCallId) { this.providerCallIdMap.set(call.providerCallId, callId);
}
}
// Restart max-duration timers for restored calls that are past the answered state for (const [callId, call] of verified) { if (call.answeredAt && !TerminalStates.has(call.state)) { const elapsed = Date.now() - call.answeredAt; const maxDurationMs = this.config.maxDurationSeconds * 1000; if (elapsed >= maxDurationMs) { // Already expired — remove instead of keeping
verified.delete(callId); if (call.providerCallId) { this.providerCallIdMap.delete(call.providerCallId);
}
console.log(
`[voice-call] Skipping restored call ${callId} (max duration already elapsed)`,
); continue;
}
startMaxDurationTimer({
ctx: this.getContext(),
callId,
timeoutMs: maxDurationMs - elapsed,
onTimeout: async (id) => {
await endCallWithContext(this.getContext(), id, { reason: "timeout" });
},
});
console.log(`[voice-call] Restarted max-duration timer for restored call ${callId}`);
}
}
if (verified.size > 0) {
console.log(`[voice-call] Restored ${verified.size} active call(s) from store`);
}
}
for (const [callId, call] of candidates) { // Skip calls without a provider ID — can't verify if (!call.providerCallId) {
console.log(`[voice-call] Skipping restored call ${callId} (no providerCallId)`); continue;
}
// Notify mode should speak as soon as the provider reports "answered". // Conversation mode should defer only when the Twilio stream-connect path // is actually available; otherwise speak immediately on answered. const mode = (call.metadata?.mode as string | undefined) ?? "conversation"; if (mode === "conversation") { if (this.config.realtime.enabled) { return;
} const shouldWaitForStreamConnect = this.shouldDeferConversationInitialMessageUntilStreamConnect(); if (shouldWaitForStreamConnect) { return;
}
} elseif (mode !== "notify") { return;
}
if (!this.provider || !call.providerCallId) { return;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.