Commit ed0dba69 by luoqi

revert: 撤掉 Twilio 网页拨号,只留占位拨打按钮

Twilio 不支持拨打中国大陆(CN 不在其 218 个自助可拨国家列表,需工单审批且高资费/美国主叫),
不适合国内患者召回外呼。撤掉集成,保留"拨打"按钮做占位,点击提示"需企业资质认证 + 接入国内合规外呼线路"。
生产改接国内线路(阿里云语音/腾讯云/容联等)时,本架构(token+voice回调+拨打组件)可平移。

- 删 telephony 后端模块 + use-twilio-call hook
- call-widget 简化为占位按钮 + toast 提示
- 移除 twilio / @twilio/voice-sdk 依赖、configuration twilio 块、wrap-response twilio 跳过、.env TWILIO 变量

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent ad06a8bf
......@@ -79,7 +79,6 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.3",
"twilio": "^6.0.2",
"winston": "^3.19.0",
"ws": "^8.21.0",
"zod": "^4.4.3"
......
......@@ -17,7 +17,6 @@ import { PlanAggregateModule } from './modules/plan-aggregate/plan-aggregate.mod
import { AgentModule } from './modules/agent/agent.module';
import { AiModule } from './modules/ai/ai.module';
import { RealtimeCoachModule } from './modules/realtime-coach/realtime-coach.module';
import { TelephonyModule } from './modules/telephony/telephony.module';
import { AdminModule } from './modules/admin/admin.module';
import { QueuesModule } from './queues/queues.module';
import { QueuesBullBoardModule } from './queues/bull-board.module';
......@@ -50,7 +49,6 @@ import { HealthController } from './health.controller';
AgentModule,
AiModule,
RealtimeCoachModule,
TelephonyModule,
AdminModule,
PlanAggregateModule,
],
......
......@@ -49,8 +49,6 @@ function shouldSkip(req: Request, res: Response): boolean {
const url = req.url ?? '';
if (url === '/health') return true;
if (url.startsWith('/api/openapi.json') || url.startsWith('/api/docs')) return true;
// Twilio voice webhook 返回裸 TwiML(XML),不能包成 {code,msg,data} JSON
if (url.startsWith('/pac/v1/twilio/voice')) return true;
const ct = res.getHeader('content-type');
if (typeof ct === 'string' && ct.startsWith('text/event-stream')) return true;
return false;
......
......@@ -37,18 +37,6 @@ export interface AppConfig {
alert: { webhookUrl: string };
cors: { origins: string[] };
exchangeCodeTtlSeconds: number;
/// Twilio 网页拨号(Voice JS SDK 点击拨号)。全部只服务端,Secret 绝不下发浏览器
twilio: {
accountSid: string;
apiKeySid: string;
apiKeySecret: string;
/// TwiML App SID(出站客户端呼叫的 Voice URL 指向我们的 /twilio/voice)
twimlAppSid: string;
/// 主叫号(Twilio 账号下的号码,如 +16365386601)
callerId: string;
/// 强制拨号目标(trial 账号只能打已验证号;设了就所有呼叫都打它,生产置空走 planId 解析)
forceTo: string;
};
}
export function loadConfig(): AppConfig {
......@@ -91,14 +79,6 @@ export function loadConfig(): AppConfig {
.filter(Boolean),
},
exchangeCodeTtlSeconds: Number(process.env.EXCHANGE_CODE_TTL_SECONDS ?? 60),
twilio: {
accountSid: process.env.TWILIO_ACCOUNT_SID ?? '',
apiKeySid: process.env.TWILIO_API_KEY_SID ?? '',
apiKeySecret: process.env.TWILIO_API_KEY_SECRET ?? '',
twimlAppSid: process.env.TWILIO_TWIML_APP_SID ?? '',
callerId: process.env.TWILIO_CALLER_ID ?? '',
forceTo: process.env.TWILIO_FORCE_TO ?? '',
},
};
}
......
import { Controller, Post, Body, Req, Res, ForbiddenException } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import type { Request, Response } from 'express';
import { Public } from '../../common/decorators/public.decorator';
import {
CurrentUser,
AuthenticatedUser,
} from '../../common/decorators/current-user.decorator';
import { TelephonyService } from './telephony.service';
/**
* Twilio 网页拨号端点。
* POST /pac/v1/twilio/token — 客服端拿 Voice AccessToken(需 JWT)
* POST /pac/v1/twilio/voice — Twilio 回调,返回 Dial TwiML(@Public,Twilio 调,无 JWT)
*/
@ApiTags('telephony')
@Controller('twilio')
export class TelephonyController {
constructor(private readonly telephony: TelephonyService) {}
@Post('token')
@ApiOperation({ summary: '签发 Twilio Voice AccessToken(网页点击拨号用)' })
token(@CurrentUser() user: AuthenticatedUser) {
if (!this.telephony.isEnabled()) {
throw new ForbiddenException('网页拨号未配置(缺 Twilio 凭证)');
}
return this.telephony.createVoiceToken(user.sub);
}
@Public()
@Post('voice')
@ApiOperation({ summary: 'Twilio 出站呼叫 TwiML 回调(返回 <Dial>)' })
async voice(
@Body() body: Record<string, string>,
@Req() _req: Request,
@Res() res: Response,
) {
// ⚠️ 生产应校验 X-Twilio-Signature(需 Auth Token);联调阶段先靠 forceTo + planId 解析收敛风险。
const twiml = await this.telephony.buildDialTwiml({ planId: body.planId });
res.type('text/xml').send(twiml);
}
}
import { Module } from '@nestjs/common';
import { TelephonyController } from './telephony.controller';
import { TelephonyService } from './telephony.service';
/**
* TelephonyModule — Twilio 网页点击拨号(Voice JS SDK)。
* PrismaModule 全局可用,无需在此 import。
*/
@Module({
controllers: [TelephonyController],
providers: [TelephonyService],
exports: [TelephonyService],
})
export class TelephonyModule {}
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import twilio from 'twilio';
import type { AppConfig } from '../../config/configuration';
import { PrismaService } from '../../prisma/prisma.service';
/**
* TelephonyService — Twilio 网页点击拨号(Voice JS SDK / WebRTC click-to-call)。
*
* 链路:
* 浏览器 @twilio/voice-sdk Device(用 AccessToken 注册)
* → device.connect({ params:{ planId } })
* → Twilio 回调 POST /pac/v1/twilio/voice(本服务返回 <Dial> TwiML)
* → Twilio 桥接到患者手机(PSTN)
*
* 安全:API Key Secret 只服务端持有(签 AccessToken),绝不下发浏览器。
* 患者真实号码不进浏览器 —— 前端只传 planId,这里查库解析真号再 Dial。
*
* ⚠️ Trial 账号只能拨"已验证"号码:设 TWILIO_FORCE_TO=已验证号 → 所有呼叫都打它(联调用);
* 生产置空 → 走 planId 解析患者真实号(需 Upgrade 解除 trial 限制 + 开 China geo permissions)。
*/
@Injectable()
export class TelephonyService {
private readonly logger = new Logger(TelephonyService.name);
constructor(
private readonly config: ConfigService<AppConfig, true>,
private readonly prisma: PrismaService,
) {}
private cfg() {
return this.config.get('twilio', { infer: true });
}
/** 网页拨号是否已配置(凭证齐全) */
isEnabled(): boolean {
const c = this.cfg();
return Boolean(c.accountSid && c.apiKeySid && c.apiKeySecret && c.twimlAppSid && c.callerId);
}
/**
* 签发 Voice AccessToken(短期凭证,可安全下发浏览器)。
* identity = 客服 user id(用于 Twilio 端标识这通客户端)。
*/
createVoiceToken(identity: string): { token: string; identity: string } {
const c = this.cfg();
const AccessToken = twilio.jwt.AccessToken;
const VoiceGrant = AccessToken.VoiceGrant;
const token = new AccessToken(c.accountSid, c.apiKeySid, c.apiKeySecret, {
identity,
ttl: 3600,
});
token.addGrant(
new VoiceGrant({
outgoingApplicationSid: c.twimlAppSid,
incomingAllow: false,
}),
);
return { token: token.toJwt(), identity };
}
/**
* 生成出站呼叫的 TwiML(Twilio 回调本端点时返回)。
* params.planId → 查库解析患者真实手机号;TWILIO_FORCE_TO 设了则强制拨它(trial 联调)。
*/
async buildDialTwiml(params: { planId?: string }): Promise<string> {
const c = this.cfg();
const VoiceResponse = twilio.twiml.VoiceResponse;
const resp = new VoiceResponse();
const to = c.forceTo || (params.planId ? await this.resolvePhone(params.planId) : '');
if (!to) {
resp.say({ language: 'zh-CN' }, '未找到可拨打的号码');
return resp.toString();
}
const dial = resp.dial({ callerId: c.callerId, answerOnBridge: true });
dial.number(to);
this.logger.log(`dial → ${maskPhone(to)} (callerId=${c.callerId}, planId=${params.planId ?? '-'})`);
return resp.toString();
}
/** planId → 患者手机号(真号,只服务端用) */
private async resolvePhone(planId: string): Promise<string> {
const plan = await this.prisma.followupPlan.findUnique({
where: { id: planId },
select: { patient: { select: { phone: true } } },
});
return plan?.patient?.phone ?? '';
}
}
function maskPhone(p: string): string {
if (p.length < 7) return '***';
return `${p.slice(0, 3)}****${p.slice(-4)}`;
}
......@@ -21,7 +21,6 @@
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@twilio/voice-sdk": "^2.18.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
......
'use client';
import { Phone, PhoneOff, Mic, MicOff } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTwilioCall } from './use-twilio-call';
const fmt = (s: number) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
import { Phone } from 'lucide-react';
import { toast } from 'sonner';
/**
* CallWidget — 详情页网页点击拨号(Twilio Voice)。
* 空闲显示"拨打";呼叫/通话中显示状态 + 计时 + 静音/挂断。
* CallWidget — 网页拨号占位按钮。
*
* 外呼真正落地需国内合规外呼线路(企业资质认证后开通);
* Twilio 等海外线路无法拨打中国大陆。现阶段点击仅提示,链路待接国内 provider。
*/
export function CallWidget({ planId }: { planId: string }) {
const { state, error, muted, seconds, dial, hangup, toggleMute, reset } = useTwilioCall();
if (state === 'idle') {
return (
<button
onClick={() => dial(planId)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-teal-600 text-white text-[12px] font-medium hover:bg-teal-700 transition-colors"
>
<Phone className="w-3.5 h-3.5" />
拨打
</button>
);
}
if (state === 'error' || state === 'ended') {
return (
<div className="inline-flex items-center gap-2 text-[11px]">
<span className={cn(state === 'error' ? 'text-rose-600' : 'text-slate-500')}>
{state === 'error' ? (error ?? '呼叫失败') : `已结束 ${fmt(seconds)}`}
</span>
<button
onClick={() => reset()}
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md border border-teal-300 text-teal-700 text-[11px] hover:bg-teal-50"
>
<Phone className="w-3 h-3" />
重拨
</button>
</div>
);
}
// connecting / ringing / in_call
const label = state === 'in_call' ? fmt(seconds) : state === 'ringing' ? '响铃中…' : '呼叫中…';
export function CallWidget() {
return (
<div className="inline-flex items-center gap-1.5">
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-emerald-50 text-emerald-700 text-[11px] font-medium tabular-nums">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
{label}
</span>
{state === 'in_call' && (
<button
onClick={toggleMute}
title={muted ? '取消静音' : '静音'}
className={cn(
'inline-flex items-center justify-center w-7 h-7 rounded-md border transition-colors',
muted ? 'border-amber-300 text-amber-600 bg-amber-50' : 'border-slate-200 text-slate-500 hover:bg-slate-50',
)}
>
{muted ? <MicOff className="w-3.5 h-3.5" /> : <Mic className="w-3.5 h-3.5" />}
</button>
)}
<button
onClick={hangup}
title="挂断"
className="inline-flex items-center justify-center w-7 h-7 rounded-md bg-rose-600 text-white hover:bg-rose-700 transition-colors"
>
<PhoneOff className="w-3.5 h-3.5" />
</button>
</div>
<button
onClick={() =>
toast('外呼功能待开通', {
description: '需完成企业资质认证 + 接入国内合规外呼线路后开放',
})
}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-teal-600 text-white text-[12px] font-medium hover:bg-teal-700 transition-colors"
>
<Phone className="w-3.5 h-3.5" />
拨打
</button>
);
}
......@@ -406,7 +406,7 @@ export function PlanDetailApp({
<h2 className="text-[14px] font-semibold text-slate-900 leading-tight">通话结果</h2>
<p className="text-[10.5px] text-slate-500 mt-0.5">提交后本次任务自动归档</p>
</div>
<CallWidget planId={plan.id} />
<CallWidget />
</header>
<div className="flex-1 min-h-0 overflow-y-auto p-3">
<OutcomeForm
......
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { Call, Device } from '@twilio/voice-sdk';
import { api } from '@/lib/api-client';
export type CallState = 'idle' | 'connecting' | 'ringing' | 'in_call' | 'ended' | 'error';
/**
* Twilio 网页点击拨号 hook(@twilio/voice-sdk)。
*
* 流程:dial(planId) → 拿 AccessToken → 建 Device → device.connect({ params:{ planId } })
* → Twilio 回调后端 /twilio/voice 返回 <Dial> → 桥接患者手机。
*
* Device/SDK 动态 import(只在点击时加载,避免 SSR + 首屏体积)。
*/
export function useTwilioCall() {
const [state, setState] = useState<CallState>('idle');
const [error, setError] = useState<string | null>(null);
const [muted, setMuted] = useState(false);
const [seconds, setSeconds] = useState(0);
const deviceRef = useRef<Device | null>(null);
const callRef = useRef<Call | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const stopTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
const cleanup = useCallback(() => {
stopTimer();
callRef.current = null;
if (deviceRef.current) {
try {
deviceRef.current.destroy();
} catch {
/* noop */
}
deviceRef.current = null;
}
}, []);
useEffect(() => cleanup, [cleanup]);
const hangup = useCallback(() => {
try {
callRef.current?.disconnect();
deviceRef.current?.disconnectAll();
} catch {
/* noop */
}
stopTimer();
setState('ended');
}, []);
const toggleMute = useCallback(() => {
const c = callRef.current;
if (!c) return;
const next = !muted;
c.mute(next);
setMuted(next);
}, [muted]);
const dial = useCallback(async (planId: string) => {
setError(null);
setMuted(false);
setSeconds(0);
setState('connecting');
try {
const { token } = await api.post<{ token: string; identity: string }>(
'/pac/v1/twilio/token',
);
const { Device } = await import('@twilio/voice-sdk');
const device = new Device(token, {
codecPreferences: ['opus', 'pcmu'] as never,
logLevel: 'error' as never,
});
deviceRef.current = device;
const call = await device.connect({ params: { planId } });
callRef.current = call;
call.on('ringing', () => setState('ringing'));
call.on('accept', () => {
setState('in_call');
stopTimer();
timerRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
});
call.on('disconnect', () => {
stopTimer();
setState('ended');
});
call.on('cancel', () => {
stopTimer();
setState('ended');
});
call.on('error', (e: { message?: string }) => {
stopTimer();
setError(e?.message ?? '通话错误');
setState('error');
});
} catch (e) {
setError(e instanceof Error ? e.message : '拨号失败');
setState('error');
cleanup();
}
}, [cleanup]);
const reset = useCallback(() => {
cleanup();
setState('idle');
setError(null);
setSeconds(0);
setMuted(false);
}, [cleanup]);
return { state, error, muted, seconds, dial, hangup, toggleMute, reset };
}
......@@ -144,9 +144,6 @@ importers:
socket.io:
specifier: ^4.8.3
version: 4.8.3
twilio:
specifier: ^6.0.2
version: 6.0.2
winston:
specifier: ^3.19.0
version: 3.19.0
......@@ -247,9 +244,6 @@ importers:
'@radix-ui/react-tabs':
specifier: ^1.1.13
version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@twilio/voice-sdk':
specifier: ^2.18.3
version: 2.18.3
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
......@@ -2206,13 +2200,6 @@ packages:
cpu: [arm64]
os: [win32]
'@twilio/voice-errors@1.7.0':
resolution: {integrity: sha512-9TvniWpzU0iy6SYFAcDP+HG+/mNz2yAHSs7+m0DZk86lE+LoTB6J/ZONTPuxXrXWi4tso/DulSHuA0w7nIQtGg==}
'@twilio/voice-sdk@2.18.3':
resolution: {integrity: sha512-sBa9Tw+aXVIqDVnFQXIoY+yZM8GI8v/fwt34EMElSUfvlb8kquDOwLv6wXrBOwSYrnlyJoUqjlAOWdFPizEBnw==}
engines: {node: '>= 12'}
'@tybys/wasm-util@0.10.2':
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
......@@ -2246,9 +2233,6 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/events@3.0.3':
resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==}
'@types/express-serve-static-core@5.1.1':
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
......@@ -2629,10 +2613,6 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
ai@6.0.184:
resolution: {integrity: sha512-j//zHkKvj5ra27l8izHco8cj1g1Pr7vx1ZK+hrzrkHvndgIRmdfZKOb6+RAPpvbk42qGIsuYvlYbGlVAu3erNQ==}
engines: {node: '>=18'}
......@@ -2778,9 +2758,6 @@ packages:
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
......@@ -2789,9 +2766,6 @@ packages:
resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==}
engines: {node: '>=4'}
axios@1.16.1:
resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
......@@ -3103,10 +3077,6 @@ packages:
resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==}
engines: {node: '>=18'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
......@@ -3215,9 +3185,6 @@ packages:
date-fns@4.4.0:
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
dayjs@1.11.21:
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
......@@ -3280,10 +3247,6 @@ packages:
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
......@@ -3711,15 +3674,6 @@ packages:
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
......@@ -3739,10 +3693,6 @@ packages:
resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==}
engines: {node: '>= 14.17'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
......@@ -3922,10 +3872,6 @@ packages:
resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==}
engines: {node: '>=10.19.0'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
......@@ -4514,10 +4460,6 @@ packages:
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
engines: {node: '>= 12.0.0'}
loglevel@1.9.2:
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
engines: {node: '>= 0.6.0'}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
......@@ -4991,10 +4933,6 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
......@@ -5201,10 +5139,6 @@ packages:
resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
engines: {node: '>= 10.13.0'}
scmp@2.1.0:
resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==}
deprecated: Just use Node.js's crypto.timingSafeEqual()
seek-bzip@2.0.0:
resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==}
hasBin: true
......@@ -5633,10 +5567,6 @@ packages:
resolution: {integrity: sha512-3xfzXE/yTjhh0S5dIWlE+3E+J9A09REpLI1ZqVh2+HrNZoVzZn0pkvjiRgVK/Ev3PF9XnaTwCntTx+CADWXcyA==}
hasBin: true
twilio@6.0.2:
resolution: {integrity: sha512-RN3TZxUtxLz2HBZVt62+LdZxQbrMVgYKtuzLgwmO7nqKvR+gQS5mCackD9hf4Y7MmoK/bX7tCm7kaJC8kC8zFA==}
engines: {node: '>=20.0.0'}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
......@@ -5888,10 +5818,6 @@ packages:
utf-8-validate:
optional: true
xmlbuilder@13.0.2:
resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==}
engines: {node: '>=6.0'}
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
......@@ -7818,16 +7744,6 @@ snapshots:
'@turbo/windows-arm64@2.9.9':
optional: true
'@twilio/voice-errors@1.7.0': {}
'@twilio/voice-sdk@2.18.3':
dependencies:
'@twilio/voice-errors': 1.7.0
'@types/events': 3.0.3
events: 3.3.0
loglevel: 1.9.2
tslib: 2.8.1
'@tybys/wasm-util@0.10.2':
dependencies:
tslib: 2.8.1
......@@ -7879,8 +7795,6 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/events@3.0.3': {}
'@types/express-serve-static-core@5.1.1':
dependencies:
'@types/node': 22.19.17
......@@ -8342,12 +8256,6 @@ snapshots:
acorn@8.16.0: {}
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
ai@6.0.184(zod@4.4.3):
dependencies:
'@ai-sdk/gateway': 3.0.115(zod@4.4.3)
......@@ -8512,24 +8420,12 @@ snapshots:
async@3.2.6: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
axe-core@4.11.4: {}
axios@1.16.1:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.5
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
- supports-color
axobject-query@4.1.0: {}
b4a@1.8.1: {}
......@@ -8862,10 +8758,6 @@ snapshots:
color-convert: 3.1.3
color-string: 2.1.4
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@2.20.3: {}
commander@4.1.1: {}
......@@ -8963,8 +8855,6 @@ snapshots:
date-fns@4.4.0: {}
dayjs@1.11.21: {}
debug@3.2.7:
dependencies:
ms: 2.1.3
......@@ -9007,8 +8897,6 @@ snapshots:
defu@6.1.7: {}
delayed-stream@1.0.0: {}
denque@2.1.0: {}
depd@2.0.0: {}
......@@ -9620,8 +9508,6 @@ snapshots:
fn.name@1.1.0: {}
follow-redirects@1.16.0: {}
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
......@@ -9650,14 +9536,6 @@ snapshots:
form-data-encoder@2.1.4: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.3
mime-types: 2.1.35
forwarded@0.2.0: {}
fresh@2.0.0: {}
......@@ -9854,13 +9732,6 @@ snapshots:
quick-lru: 5.1.1
resolve-alpn: 1.2.1
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
human-signals@2.1.0: {}
iconv-lite@0.7.2:
......@@ -10611,8 +10482,6 @@ snapshots:
safe-stable-stringify: 2.5.0
triple-beam: 1.4.1
loglevel@1.9.2: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
......@@ -11060,8 +10929,6 @@ snapshots:
forwarded: 0.2.0
ipaddr.js: 1.9.1
proxy-from-env@2.1.0: {}
punycode@2.3.1: {}
pure-rand@6.1.0: {}
......@@ -11274,8 +11141,6 @@ snapshots:
ajv-formats: 2.1.1(ajv@8.20.0)
ajv-keywords: 5.1.0(ajv@8.20.0)
scmp@2.1.0: {}
seek-bzip@2.0.0:
dependencies:
commander: 6.2.1
......@@ -11805,19 +11670,6 @@ snapshots:
'@turbo/windows-64': 2.9.9
'@turbo/windows-arm64': 2.9.9
twilio@6.0.2:
dependencies:
axios: 1.16.1
dayjs: 1.11.21
https-proxy-agent: 5.0.1
jsonwebtoken: 9.0.3
qs: 6.15.1
scmp: 2.1.0
xmlbuilder: 13.0.2
transitivePeerDependencies:
- debug
- supports-color
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
......@@ -12130,8 +11982,6 @@ snapshots:
ws@8.21.0: {}
xmlbuilder@13.0.2: {}
xmlhttprequest-ssl@2.1.2: {}
y18n@5.0.8: {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment