mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 07:01:40 +03:00
chore: Lint extensions folder.
This commit is contained in:
@@ -21,7 +21,9 @@ type Logger = {
|
||||
|
||||
function resolveMode(input: string): "off" | "serve" | "funnel" {
|
||||
const raw = input.trim().toLowerCase();
|
||||
if (raw === "serve" || raw === "off") return raw;
|
||||
if (raw === "serve" || raw === "off") {
|
||||
return raw;
|
||||
}
|
||||
return "funnel";
|
||||
}
|
||||
|
||||
|
||||
@@ -72,19 +72,25 @@ function findPackageRoot(startDir: string, name: string): string | null {
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const raw = fs.readFileSync(pkgPath, "utf8");
|
||||
const pkg = JSON.parse(raw) as { name?: string };
|
||||
if (pkg.name === name) return dir;
|
||||
if (pkg.name === name) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors and keep walking
|
||||
}
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
if (parent === dir) {
|
||||
return null;
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOpenClawRoot(): string {
|
||||
if (coreRootCache) return coreRootCache;
|
||||
if (coreRootCache) {
|
||||
return coreRootCache;
|
||||
}
|
||||
const override = process.env.OPENCLAW_ROOT?.trim();
|
||||
if (override) {
|
||||
coreRootCache = override;
|
||||
@@ -128,7 +134,9 @@ async function importCoreModule<T>(relativePath: string): Promise<T> {
|
||||
}
|
||||
|
||||
export async function loadCoreAgentDeps(): Promise<CoreAgentDeps> {
|
||||
if (coreDepsPromise) return coreDepsPromise;
|
||||
if (coreDepsPromise) {
|
||||
return coreDepsPromise;
|
||||
}
|
||||
|
||||
coreDepsPromise = (async () => {
|
||||
const [
|
||||
|
||||
@@ -21,7 +21,9 @@ import { escapeXml, mapVoiceToPolly } from "./voice-mapping.js";
|
||||
|
||||
function resolveDefaultStoreBase(config: VoiceCallConfig, storePath?: string): string {
|
||||
const rawOverride = storePath?.trim() || config.store?.trim();
|
||||
if (rawOverride) return resolveUserPath(rawOverride);
|
||||
if (rawOverride) {
|
||||
return resolveUserPath(rawOverride);
|
||||
}
|
||||
const preferred = path.join(os.homedir(), ".openclaw", "voice-calls");
|
||||
const candidates = [preferred].map((dir) => resolveUserPath(dir));
|
||||
const existing =
|
||||
@@ -322,21 +324,27 @@ export class CallManager {
|
||||
|
||||
private clearTranscriptWaiter(callId: CallId): void {
|
||||
const waiter = this.transcriptWaiters.get(callId);
|
||||
if (!waiter) return;
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(waiter.timeout);
|
||||
this.transcriptWaiters.delete(callId);
|
||||
}
|
||||
|
||||
private rejectTranscriptWaiter(callId: CallId, reason: string): void {
|
||||
const waiter = this.transcriptWaiters.get(callId);
|
||||
if (!waiter) return;
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
this.clearTranscriptWaiter(callId);
|
||||
waiter.reject(new Error(reason));
|
||||
}
|
||||
|
||||
private resolveTranscriptWaiter(callId: CallId, transcript: string): void {
|
||||
const waiter = this.transcriptWaiters.get(callId);
|
||||
if (!waiter) return;
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
this.clearTranscriptWaiter(callId);
|
||||
waiter.resolve(transcript);
|
||||
}
|
||||
@@ -520,7 +528,9 @@ export class CallManager {
|
||||
private findCall(callIdOrProviderCallId: string): CallRecord | undefined {
|
||||
// Try direct lookup by internal callId
|
||||
const directCall = this.activeCalls.get(callIdOrProviderCallId);
|
||||
if (directCall) return directCall;
|
||||
if (directCall) {
|
||||
return directCall;
|
||||
}
|
||||
|
||||
// Try lookup by providerCallId
|
||||
return this.getCallByProviderCallId(callIdOrProviderCallId);
|
||||
@@ -648,13 +658,19 @@ export class CallManager {
|
||||
const initialMessage =
|
||||
typeof call.metadata?.initialMessage === "string" ? call.metadata.initialMessage.trim() : "";
|
||||
|
||||
if (!initialMessage) return;
|
||||
if (!initialMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.provider || !call.providerCallId) 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;
|
||||
if (this.provider.name === "twilio") {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.speakInitialMessage(call.providerCallId);
|
||||
}
|
||||
@@ -740,7 +756,9 @@ export class CallManager {
|
||||
*/
|
||||
private transitionState(call: CallRecord, newState: CallState): void {
|
||||
// No-op for same state or already terminal
|
||||
if (call.state === newState || TerminalStates.has(call.state)) return;
|
||||
if (call.state === newState || TerminalStates.has(call.state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Terminal states can always be reached from non-terminal
|
||||
if (TerminalStates.has(newState)) {
|
||||
@@ -797,7 +815,9 @@ export class CallManager {
|
||||
*/
|
||||
private loadActiveCalls(): void {
|
||||
const logPath = path.join(this.storePath, "calls.jsonl");
|
||||
if (!fs.existsSync(logPath)) return;
|
||||
if (!fs.existsSync(logPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read file synchronously and parse lines
|
||||
const content = fs.readFileSync(logPath, "utf-8");
|
||||
@@ -807,7 +827,9 @@ export class CallManager {
|
||||
const callMap = new Map<CallId, CallRecord>();
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const call = CallRecordSchema.parse(JSON.parse(line));
|
||||
callMap.set(call.callId, call);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import type { CallId, CallRecord, CallState, NormalizedEvent } from "../types.js";
|
||||
import { TerminalStates } from "../types.js";
|
||||
import type { CallRecord, CallState, NormalizedEvent } from "../types.js";
|
||||
import type { CallManagerContext } from "./context.js";
|
||||
import { findCall } from "./lookup.js";
|
||||
import { addTranscriptEntry, transitionState } from "./state.js";
|
||||
@@ -81,7 +80,9 @@ function createInboundCall(params: {
|
||||
}
|
||||
|
||||
export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): void {
|
||||
if (ctx.processedEventIds.has(event.id)) return;
|
||||
if (ctx.processedEventIds.has(event.id)) {
|
||||
return;
|
||||
}
|
||||
ctx.processedEventIds.add(event.id);
|
||||
|
||||
let call = findCall({
|
||||
@@ -107,7 +108,9 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v
|
||||
event.callId = call.callId;
|
||||
}
|
||||
|
||||
if (!call) return;
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.providerCallId && !call.providerCallId) {
|
||||
call.providerCallId = event.providerCallId;
|
||||
@@ -160,7 +163,9 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v
|
||||
clearMaxDurationTimer(ctx, call.callId);
|
||||
rejectTranscriptWaiter(ctx, call.callId, `Call ended: ${event.reason}`);
|
||||
ctx.activeCalls.delete(call.callId);
|
||||
if (call.providerCallId) ctx.providerCallIdMap.delete(call.providerCallId);
|
||||
if (call.providerCallId) {
|
||||
ctx.providerCallIdMap.delete(call.providerCallId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "call.error":
|
||||
@@ -171,7 +176,9 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v
|
||||
clearMaxDurationTimer(ctx, call.callId);
|
||||
rejectTranscriptWaiter(ctx, call.callId, `Call error: ${event.error}`);
|
||||
ctx.activeCalls.delete(call.callId);
|
||||
if (call.providerCallId) ctx.providerCallIdMap.delete(call.providerCallId);
|
||||
if (call.providerCallId) {
|
||||
ctx.providerCallIdMap.delete(call.providerCallId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ export function findCall(params: {
|
||||
callIdOrProviderCallId: string;
|
||||
}): CallRecord | undefined {
|
||||
const directCall = params.activeCalls.get(params.callIdOrProviderCallId);
|
||||
if (directCall) return directCall;
|
||||
if (directCall) {
|
||||
return directCall;
|
||||
}
|
||||
return getCallByProviderCallId({
|
||||
activeCalls: params.activeCalls,
|
||||
providerCallIdMap: params.providerCallIdMap,
|
||||
|
||||
@@ -119,9 +119,15 @@ export async function speak(
|
||||
text: string,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
if (!call) return { success: false, error: "Call not found" };
|
||||
if (!ctx.provider || !call.providerCallId) return { success: false, error: "Call not connected" };
|
||||
if (TerminalStates.has(call.state)) return { success: false, error: "Call has ended" };
|
||||
if (!call) {
|
||||
return { success: false, error: "Call not found" };
|
||||
}
|
||||
if (!ctx.provider || !call.providerCallId) {
|
||||
return { success: false, error: "Call not connected" };
|
||||
}
|
||||
if (TerminalStates.has(call.state)) {
|
||||
return { success: false, error: "Call has ended" };
|
||||
}
|
||||
|
||||
try {
|
||||
transitionState(call, "speaking");
|
||||
@@ -197,9 +203,15 @@ export async function continueCall(
|
||||
prompt: string,
|
||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
if (!call) return { success: false, error: "Call not found" };
|
||||
if (!ctx.provider || !call.providerCallId) return { success: false, error: "Call not connected" };
|
||||
if (TerminalStates.has(call.state)) return { success: false, error: "Call has ended" };
|
||||
if (!call) {
|
||||
return { success: false, error: "Call not found" };
|
||||
}
|
||||
if (!ctx.provider || !call.providerCallId) {
|
||||
return { success: false, error: "Call not connected" };
|
||||
}
|
||||
if (TerminalStates.has(call.state)) {
|
||||
return { success: false, error: "Call has ended" };
|
||||
}
|
||||
|
||||
try {
|
||||
await speak(ctx, callId, prompt);
|
||||
@@ -227,9 +239,15 @@ export async function endCall(
|
||||
callId: CallId,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
if (!call) return { success: false, error: "Call not found" };
|
||||
if (!ctx.provider || !call.providerCallId) return { success: false, error: "Call not connected" };
|
||||
if (TerminalStates.has(call.state)) return { success: true };
|
||||
if (!call) {
|
||||
return { success: false, error: "Call not found" };
|
||||
}
|
||||
if (!ctx.provider || !call.providerCallId) {
|
||||
return { success: false, error: "Call not connected" };
|
||||
}
|
||||
if (TerminalStates.has(call.state)) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.provider.hangupCall({
|
||||
@@ -247,7 +265,9 @@ export async function endCall(
|
||||
rejectTranscriptWaiter(ctx, callId, "Call ended: hangup-bot");
|
||||
|
||||
ctx.activeCalls.delete(callId);
|
||||
if (call.providerCallId) ctx.providerCallIdMap.delete(call.providerCallId);
|
||||
if (call.providerCallId) {
|
||||
ctx.providerCallIdMap.delete(call.providerCallId);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
|
||||
@@ -13,7 +13,9 @@ const StateOrder: readonly CallState[] = [
|
||||
|
||||
export function transitionState(call: CallRecord, newState: CallState): void {
|
||||
// No-op for same state or already terminal.
|
||||
if (call.state === newState || TerminalStates.has(call.state)) return;
|
||||
if (call.state === newState || TerminalStates.has(call.state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Terminal states can always be reached from non-terminal.
|
||||
if (TerminalStates.has(newState)) {
|
||||
|
||||
@@ -32,7 +32,9 @@ export function loadActiveCallsFromStore(storePath: string): {
|
||||
|
||||
const callMap = new Map<CallId, CallRecord>();
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const call = CallRecordSchema.parse(JSON.parse(line));
|
||||
callMap.set(call.callId, call);
|
||||
@@ -46,7 +48,9 @@ export function loadActiveCallsFromStore(storePath: string): {
|
||||
const processedEventIds = new Set<string>();
|
||||
|
||||
for (const [callId, call] of callMap) {
|
||||
if (TerminalStates.has(call.state)) continue;
|
||||
if (TerminalStates.has(call.state)) {
|
||||
continue;
|
||||
}
|
||||
activeCalls.set(callId, call);
|
||||
if (call.providerCallId) {
|
||||
providerCallIdMap.set(call.providerCallId, callId);
|
||||
|
||||
@@ -40,7 +40,9 @@ export function startMaxDurationTimer(params: {
|
||||
|
||||
export function clearTranscriptWaiter(ctx: CallManagerContext, callId: CallId): void {
|
||||
const waiter = ctx.transcriptWaiters.get(callId);
|
||||
if (!waiter) return;
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(waiter.timeout);
|
||||
ctx.transcriptWaiters.delete(callId);
|
||||
}
|
||||
@@ -51,7 +53,9 @@ export function rejectTranscriptWaiter(
|
||||
reason: string,
|
||||
): void {
|
||||
const waiter = ctx.transcriptWaiters.get(callId);
|
||||
if (!waiter) return;
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
clearTranscriptWaiter(ctx, callId);
|
||||
waiter.reject(new Error(reason));
|
||||
}
|
||||
@@ -62,7 +66,9 @@ export function resolveTranscriptWaiter(
|
||||
transcript: string,
|
||||
): void {
|
||||
const waiter = ctx.transcriptWaiters.get(callId);
|
||||
if (!waiter) return;
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
clearTranscriptWaiter(ctx, callId);
|
||||
waiter.resolve(transcript);
|
||||
}
|
||||
|
||||
@@ -295,7 +295,9 @@ export class MediaStreamHandler {
|
||||
|
||||
private getTtsQueue(streamSid: string): TtsQueueEntry[] {
|
||||
const existing = this.ttsQueues.get(streamSid);
|
||||
if (existing) return existing;
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const queue: TtsQueueEntry[] = [];
|
||||
this.ttsQueues.set(streamSid, queue);
|
||||
return queue;
|
||||
@@ -339,7 +341,9 @@ export class MediaStreamHandler {
|
||||
|
||||
private clearTtsState(streamSid: string): void {
|
||||
const queue = this.ttsQueues.get(streamSid);
|
||||
if (queue) queue.length = 0;
|
||||
if (queue) {
|
||||
queue.length = 0;
|
||||
}
|
||||
this.ttsActiveControllers.get(streamSid)?.abort();
|
||||
this.ttsActiveControllers.delete(streamSid);
|
||||
this.ttsPlaying.delete(streamSid);
|
||||
|
||||
@@ -37,11 +37,15 @@ export class MockProvider implements VoiceCallProvider {
|
||||
if (Array.isArray(payload.events)) {
|
||||
for (const evt of payload.events) {
|
||||
const normalized = this.normalizeEvent(evt);
|
||||
if (normalized) events.push(normalized);
|
||||
if (normalized) {
|
||||
events.push(normalized);
|
||||
}
|
||||
}
|
||||
} else if (payload.event) {
|
||||
const normalized = this.normalizeEvent(payload.event);
|
||||
if (normalized) events.push(normalized);
|
||||
if (normalized) {
|
||||
events.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return { events, statusCode: 200 };
|
||||
@@ -51,7 +55,9 @@ export class MockProvider implements VoiceCallProvider {
|
||||
}
|
||||
|
||||
private normalizeEvent(evt: Partial<NormalizedEvent>): NormalizedEvent | null {
|
||||
if (!evt.type || !evt.callId) return null;
|
||||
if (!evt.type || !evt.callId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base = {
|
||||
id: evt.id || crypto.randomUUID(),
|
||||
|
||||
@@ -123,7 +123,9 @@ export class PlivoProvider implements VoiceCallProvider {
|
||||
if (flow === "xml-speak") {
|
||||
const callId = this.getCallIdFromQuery(ctx);
|
||||
const pending = callId ? this.pendingSpeakByCallId.get(callId) : undefined;
|
||||
if (callId) this.pendingSpeakByCallId.delete(callId);
|
||||
if (callId) {
|
||||
this.pendingSpeakByCallId.delete(callId);
|
||||
}
|
||||
|
||||
const xml = pending
|
||||
? PlivoProvider.xmlSpeak(pending.text, pending.locale)
|
||||
@@ -139,7 +141,9 @@ export class PlivoProvider implements VoiceCallProvider {
|
||||
if (flow === "xml-listen") {
|
||||
const callId = this.getCallIdFromQuery(ctx);
|
||||
const pending = callId ? this.pendingListenByCallId.get(callId) : undefined;
|
||||
if (callId) this.pendingListenByCallId.delete(callId);
|
||||
if (callId) {
|
||||
this.pendingListenByCallId.delete(callId);
|
||||
}
|
||||
|
||||
const actionUrl = this.buildActionUrl(ctx, {
|
||||
flow: "getinput",
|
||||
@@ -393,7 +397,9 @@ export class PlivoProvider implements VoiceCallProvider {
|
||||
|
||||
private static normalizeNumber(numberOrSip: string): string {
|
||||
const trimmed = numberOrSip.trim();
|
||||
if (trimmed.toLowerCase().startsWith("sip:")) return trimmed;
|
||||
if (trimmed.toLowerCase().startsWith("sip:")) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.replace(/[^\d+]/g, "");
|
||||
}
|
||||
|
||||
@@ -440,12 +446,16 @@ export class PlivoProvider implements VoiceCallProvider {
|
||||
opts: { flow: string; callId?: string },
|
||||
): string | null {
|
||||
const base = PlivoProvider.baseWebhookUrlFromCtx(ctx);
|
||||
if (!base) return null;
|
||||
if (!base) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const u = new URL(base);
|
||||
u.searchParams.set("provider", "plivo");
|
||||
u.searchParams.set("flow", opts.flow);
|
||||
if (opts.callId) u.searchParams.set("callId", opts.callId);
|
||||
if (opts.callId) {
|
||||
u.searchParams.set("callId", opts.callId);
|
||||
}
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
@@ -478,7 +488,9 @@ export class PlivoProvider implements VoiceCallProvider {
|
||||
|
||||
for (const key of candidates) {
|
||||
const value = params.get(key);
|
||||
if (value && value.trim()) return value.trim();
|
||||
if (value && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,9 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession {
|
||||
|
||||
this.ws.on("error", (error) => {
|
||||
console.error("[RealtimeSTT] WebSocket error:", error);
|
||||
if (!this.connected) reject(error);
|
||||
if (!this.connected) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on("close", (code, reason) => {
|
||||
@@ -258,7 +260,9 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession {
|
||||
}
|
||||
|
||||
sendAudio(muLawData: Buffer): void {
|
||||
if (!this.connected) return;
|
||||
if (!this.connected) {
|
||||
return;
|
||||
}
|
||||
this.sendEvent({
|
||||
type: "input_audio_buffer.append",
|
||||
audio: muLawData.toString("base64"),
|
||||
|
||||
@@ -205,10 +205,14 @@ function linearToMulaw(sample: number): number {
|
||||
|
||||
// Get sign bit
|
||||
const sign = sample < 0 ? 0x80 : 0;
|
||||
if (sample < 0) sample = -sample;
|
||||
if (sample < 0) {
|
||||
sample = -sample;
|
||||
}
|
||||
|
||||
// Clip to prevent overflow
|
||||
if (sample > CLIP) sample = CLIP;
|
||||
if (sample > CLIP) {
|
||||
sample = CLIP;
|
||||
}
|
||||
|
||||
// Add bias and find segment
|
||||
sample += BIAS;
|
||||
|
||||
@@ -85,10 +85,14 @@ export class TwilioProvider implements VoiceCallProvider {
|
||||
*/
|
||||
private deleteStoredTwimlForProviderCall(providerCallId: string): void {
|
||||
const webhookUrl = this.callWebhookUrls.get(providerCallId);
|
||||
if (!webhookUrl) return;
|
||||
if (!webhookUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callIdMatch = webhookUrl.match(/callId=([^&]+)/);
|
||||
if (!callIdMatch) return;
|
||||
if (!callIdMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deleteStoredTwiml(callIdMatch[1]);
|
||||
}
|
||||
@@ -212,8 +216,12 @@ export class TwilioProvider implements VoiceCallProvider {
|
||||
* Parse Twilio direction to normalized format.
|
||||
*/
|
||||
private static parseDirection(direction: string | null): "inbound" | "outbound" | undefined {
|
||||
if (direction === "inbound") return "inbound";
|
||||
if (direction === "outbound-api" || direction === "outbound-dial") return "outbound";
|
||||
if (direction === "inbound") {
|
||||
return "inbound";
|
||||
}
|
||||
if (direction === "outbound-api" || direction === "outbound-dial") {
|
||||
return "outbound";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -291,7 +299,9 @@ export class TwilioProvider implements VoiceCallProvider {
|
||||
* When a call is answered, connects to media stream for bidirectional audio.
|
||||
*/
|
||||
private generateTwimlResponse(ctx?: WebhookContext): string {
|
||||
if (!ctx) return TwilioProvider.EMPTY_TWIML;
|
||||
if (!ctx) {
|
||||
return TwilioProvider.EMPTY_TWIML;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(ctx.rawBody);
|
||||
const type = typeof ctx.query?.type === "string" ? ctx.query.type.trim() : undefined;
|
||||
@@ -512,12 +522,16 @@ export class TwilioProvider implements VoiceCallProvider {
|
||||
// Generate audio with core TTS (returns mu-law at 8kHz)
|
||||
const muLawAudio = await ttsProvider.synthesizeForTelephony(text);
|
||||
for (const chunk of chunkAudio(muLawAudio, CHUNK_SIZE)) {
|
||||
if (signal.aborted) break;
|
||||
if (signal.aborted) {
|
||||
break;
|
||||
}
|
||||
handler.sendAudio(streamSid, chunk);
|
||||
|
||||
// Pace the audio to match real-time playback
|
||||
await new Promise((resolve) => setTimeout(resolve, CHUNK_DELAY_MS));
|
||||
if (signal.aborted) break;
|
||||
if (signal.aborted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!signal.aborted) {
|
||||
|
||||
@@ -34,7 +34,9 @@ type Logger = {
|
||||
};
|
||||
|
||||
function isLoopbackBind(bind: string | undefined): boolean {
|
||||
if (!bind) return false;
|
||||
if (!bind) {
|
||||
return false;
|
||||
}
|
||||
return bind === "127.0.0.1" || bind === "::1" || bind === "localhost";
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,13 @@ function clamp16(value: number): number {
|
||||
* Resample 16-bit PCM (little-endian mono) to 8kHz using linear interpolation.
|
||||
*/
|
||||
export function resamplePcmTo8k(input: Buffer, inputSampleRate: number): Buffer {
|
||||
if (inputSampleRate === TELEPHONY_SAMPLE_RATE) return input;
|
||||
if (inputSampleRate === TELEPHONY_SAMPLE_RATE) {
|
||||
return input;
|
||||
}
|
||||
const inputSamples = Math.floor(input.length / 2);
|
||||
if (inputSamples === 0) return Buffer.alloc(0);
|
||||
if (inputSamples === 0) {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
|
||||
const ratio = inputSampleRate / TELEPHONY_SAMPLE_RATE;
|
||||
const outputSamples = Math.floor(inputSamples / ratio);
|
||||
@@ -68,8 +72,12 @@ function linearToMulaw(sample: number): number {
|
||||
const CLIP = 32635;
|
||||
|
||||
const sign = sample < 0 ? 0x80 : 0;
|
||||
if (sample < 0) sample = -sample;
|
||||
if (sample > CLIP) sample = CLIP;
|
||||
if (sample < 0) {
|
||||
sample = -sample;
|
||||
}
|
||||
if (sample > CLIP) {
|
||||
sample = CLIP;
|
||||
}
|
||||
|
||||
sample += BIAS;
|
||||
let exponent = 7;
|
||||
|
||||
@@ -45,16 +45,20 @@ export function createTelephonyTtsProvider(params: {
|
||||
}
|
||||
|
||||
function applyTtsOverride(coreConfig: CoreConfig, override?: VoiceCallTtsConfig): CoreConfig {
|
||||
if (!override) return coreConfig;
|
||||
if (!override) {
|
||||
return coreConfig;
|
||||
}
|
||||
|
||||
const base = coreConfig.messages?.tts;
|
||||
const merged = mergeTtsConfig(base, override);
|
||||
if (!merged) return coreConfig;
|
||||
if (!merged) {
|
||||
return coreConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...coreConfig,
|
||||
messages: {
|
||||
...(coreConfig.messages ?? {}),
|
||||
...coreConfig.messages,
|
||||
tts: merged,
|
||||
},
|
||||
};
|
||||
@@ -64,9 +68,15 @@ function mergeTtsConfig(
|
||||
base?: VoiceCallTtsConfig,
|
||||
override?: VoiceCallTtsConfig,
|
||||
): VoiceCallTtsConfig | undefined {
|
||||
if (!base && !override) return undefined;
|
||||
if (!override) return base;
|
||||
if (!base) return override;
|
||||
if (!base && !override) {
|
||||
return undefined;
|
||||
}
|
||||
if (!override) {
|
||||
return base;
|
||||
}
|
||||
if (!base) {
|
||||
return override;
|
||||
}
|
||||
return deepMerge(base, override);
|
||||
}
|
||||
|
||||
@@ -76,7 +86,9 @@ function deepMerge<T>(base: T, override: T): T {
|
||||
}
|
||||
const result: Record<string, unknown> = { ...base };
|
||||
for (const [key, value] of Object.entries(override)) {
|
||||
if (value === undefined) continue;
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
const existing = (base as Record<string, unknown>)[key];
|
||||
if (isPlainObject(existing) && isPlainObject(value)) {
|
||||
result[key] = deepMerge(existing, value);
|
||||
|
||||
@@ -3,7 +3,9 @@ import path from "node:path";
|
||||
|
||||
export function resolveUserPath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
if (trimmed.startsWith("~")) {
|
||||
const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir());
|
||||
return path.resolve(expanded);
|
||||
|
||||
@@ -39,7 +39,9 @@ export const DEFAULT_POLLY_VOICE = "Polly.Joanna";
|
||||
* @returns Polly voice name suitable for Twilio TwiML
|
||||
*/
|
||||
export function mapVoiceToPolly(voice: string | undefined): string {
|
||||
if (!voice) return DEFAULT_POLLY_VOICE;
|
||||
if (!voice) {
|
||||
return DEFAULT_POLLY_VOICE;
|
||||
}
|
||||
|
||||
// Already a Polly/Google voice - pass through
|
||||
if (voice.startsWith("Polly.") || voice.startsWith("Google.")) {
|
||||
|
||||
@@ -29,7 +29,9 @@ function plivoV3Signature(params: {
|
||||
const u = new URL(params.urlWithQuery);
|
||||
const baseNoQuery = `${u.protocol}//${u.host}${u.pathname}`;
|
||||
const queryPairs: Array<[string, string]> = [];
|
||||
for (const [k, v] of u.searchParams.entries()) queryPairs.push([k, v]);
|
||||
for (const [k, v] of u.searchParams.entries()) {
|
||||
queryPairs.push([k, v]);
|
||||
}
|
||||
|
||||
const queryMap = new Map<string, string[]>();
|
||||
for (const [k, v] of queryPairs) {
|
||||
@@ -37,8 +39,8 @@ function plivoV3Signature(params: {
|
||||
}
|
||||
|
||||
const sortedQuery = Array.from(queryMap.keys())
|
||||
.sort()
|
||||
.flatMap((k) => [...(queryMap.get(k) ?? [])].sort().map((v) => `${k}=${v}`))
|
||||
.toSorted()
|
||||
.flatMap((k) => [...(queryMap.get(k) ?? [])].toSorted().map((v) => `${k}=${v}`))
|
||||
.join("&");
|
||||
|
||||
const postParams = new URLSearchParams(params.postBody);
|
||||
@@ -48,8 +50,8 @@ function plivoV3Signature(params: {
|
||||
}
|
||||
|
||||
const sortedPost = Array.from(postMap.keys())
|
||||
.sort()
|
||||
.flatMap((k) => [...(postMap.get(k) ?? [])].sort().map((v) => `${k}${v}`))
|
||||
.toSorted()
|
||||
.flatMap((k) => [...(postMap.get(k) ?? [])].toSorted().map((v) => `${k}${v}`))
|
||||
.join("");
|
||||
|
||||
const hasPost = sortedPost.length > 0;
|
||||
@@ -71,7 +73,7 @@ function plivoV3Signature(params: {
|
||||
|
||||
function twilioSignature(params: { authToken: string; url: string; postBody: string }): string {
|
||||
let dataToSign = params.url;
|
||||
const sortedParams = Array.from(new URLSearchParams(params.postBody).entries()).sort((a, b) =>
|
||||
const sortedParams = Array.from(new URLSearchParams(params.postBody).entries()).toSorted((a, b) =>
|
||||
a[0].localeCompare(b[0]),
|
||||
);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export function validateTwilioSignature(
|
||||
let dataToSign = url;
|
||||
|
||||
// Sort params alphabetically and append key+value
|
||||
const sortedParams = Array.from(params.entries()).sort((a, b) =>
|
||||
const sortedParams = Array.from(params.entries()).toSorted((a, b) =>
|
||||
a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0,
|
||||
);
|
||||
|
||||
@@ -129,9 +129,15 @@ function getHeader(
|
||||
}
|
||||
|
||||
function isLoopbackAddress(address?: string): boolean {
|
||||
if (!address) return false;
|
||||
if (address === "127.0.0.1" || address === "::1") return true;
|
||||
if (address.startsWith("::ffff:127.")) return true;
|
||||
if (!address) {
|
||||
return false;
|
||||
}
|
||||
if (address === "127.0.0.1" || address === "::1") {
|
||||
return true;
|
||||
}
|
||||
if (address.startsWith("::ffff:127.")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -272,7 +278,9 @@ type PlivoParamMap = Record<string, string[]>;
|
||||
function toParamMapFromSearchParams(sp: URLSearchParams): PlivoParamMap {
|
||||
const map: PlivoParamMap = {};
|
||||
for (const [key, value] of sp.entries()) {
|
||||
if (!map[key]) map[key] = [];
|
||||
if (!map[key]) {
|
||||
map[key] = [];
|
||||
}
|
||||
map[key].push(value);
|
||||
}
|
||||
return map;
|
||||
@@ -280,8 +288,8 @@ function toParamMapFromSearchParams(sp: URLSearchParams): PlivoParamMap {
|
||||
|
||||
function sortedQueryString(params: PlivoParamMap): string {
|
||||
const parts: string[] = [];
|
||||
for (const key of Object.keys(params).sort()) {
|
||||
const values = [...params[key]].sort();
|
||||
for (const key of Object.keys(params).toSorted()) {
|
||||
const values = [...params[key]].toSorted();
|
||||
for (const value of values) {
|
||||
parts.push(`${key}=${value}`);
|
||||
}
|
||||
@@ -291,8 +299,8 @@ function sortedQueryString(params: PlivoParamMap): string {
|
||||
|
||||
function sortedParamsString(params: PlivoParamMap): string {
|
||||
const parts: string[] = [];
|
||||
for (const key of Object.keys(params).sort()) {
|
||||
const values = [...params[key]].sort();
|
||||
for (const key of Object.keys(params).toSorted()) {
|
||||
const values = [...params[key]].toSorted();
|
||||
for (const value of values) {
|
||||
parts.push(`${key}${value}`);
|
||||
}
|
||||
@@ -355,7 +363,9 @@ function validatePlivoV3Signature(params: {
|
||||
.map((s) => normalizeSignatureBase64(s));
|
||||
|
||||
for (const sig of provided) {
|
||||
if (timingSafeEqualString(expected, sig)) return true;
|
||||
if (timingSafeEqualString(expected, sig)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -367,7 +367,9 @@ function runTailscaleCommand(
|
||||
|
||||
export async function getTailscaleSelfInfo(): Promise<TailscaleSelfInfo | null> {
|
||||
const { code, stdout } = await runTailscaleCommand(["status", "--json"]);
|
||||
if (code !== 0) return null;
|
||||
if (code !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = JSON.parse(stdout);
|
||||
|
||||
Reference in New Issue
Block a user