voice-agent
Version:
A Vue.js voice agent plugin for real-time voice communication via WebSocket
1,206 lines (1,205 loc) • 83.3 kB
JavaScript
// Auto-injected CSS styles for voice-agent
(function() {
if (typeof document !== 'undefined') {
var styleId = 'voice-agent-styles-1760598088148';
var existingStyle = document.querySelector('style[data-voice-agent]');
if (!existingStyle) {
var style = document.createElement('style');
style.setAttribute('data-voice-agent', 'true');
style.textContent = ".loading-panel{display:flex;flex-direction:column;align-items:center;gap:20px;padding:40px;background:#fff;border-radius:16px;box-shadow:0 4px 20px #0000001a}.spinner{width:40px;height:40px;border:4px solid rgba(52,152,219,.3);border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.init-step{font-size:16px;color:#34495e;text-align:center;font-weight:500}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#000c;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);display:flex;justify-content:center;align-items:center;z-index:1000;animation:fadeIn .3s ease}.modal-content{background:#fff;overflow:hidden;width:100vw;height:100vh;box-shadow:0 10px 30px #0000004d;animation:slideUp .3s ease;display:flex;flex-direction:column}@media (min-width: 801px){.modal-overlay{justify-content:flex-end;background:#00000080}.modal-content{width:500px;height:100vh;border-radius:0;box-shadow:-5px 0 20px #0000004d;animation:slideInRight .3s ease}}@keyframes slideInRight{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}.modal-header{background:#4f008c;color:#fff;padding:50px 16px 8px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 10px #0000001a;flex-shrink:0;height:100px}.header-left{display:flex;align-items:center;gap:20px}.header-right{display:flex;align-items:center;gap:12px}.phone-selector{position:relative;display:flex;align-items:center}.phone-select{background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;padding:8px 12px;font-size:14px;cursor:pointer;outline:none;transition:all .3s ease}.phone-select:hover{background:#fff3}.phone-select option{background:#34495e;color:#fff}.phone-select-trigger{background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;padding:12px 16px;font-size:14px;cursor:pointer;outline:none;transition:all .3s ease;display:flex;align-items:center;justify-content:space-between;gap:12px;-webkit-user-select:none;user-select:none}.phone-select-trigger:hover{background:#fff3;border-color:#ffffff4d}.phone-select-trigger:active{transform:scale(.98)}.selected-phone{flex:1;text-align:left;font-weight:500}.dropdown-arrow{transition:transform .3s ease;opacity:.8;flex-shrink:0}.dropdown-arrow.rotate{transform:rotate(180deg)}.phone-dropdown{position:absolute;top:calc(100% + 8px);left:0;right:0;background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:12px;box-shadow:0 8px 25px #00000026;z-index:1000;overflow:hidden;animation:dropdownSlide .2s ease;max-height:300px;overflow-y:auto}.phone-option{padding:0 10px;color:#2c3e50;font-size:14px;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(0,0,0,.05);min-height:48px}.phone-option:last-child{border-bottom:none}.phone-option:hover{background:#f8f9fa}.phone-option:active{background:#e9ecef;transform:scale(.98)}.phone-option.selected{background:#e3f2fd;color:#1976d2;font-weight:600}.phone-option.selected:hover{background:#e3f2fd}.check-icon{color:#1976d2;opacity:.8;flex-shrink:0}.current-status{display:flex;align-items:center;gap:8px;font-size:13px}.status-dot{width:8px;height:8px;border-radius:50%;background:#95a5a6;transition:all .3s ease}.status-dot.connected{background:#27ae60;box-shadow:0 0 10px #27ae6080}.status-dot.recording{background:#e74c3c;animation:pulse 1.5s infinite;box-shadow:0 0 10px #e74c3c80}.status-dot.disconnected{background:#95a5a6}.status-text{font-weight:500}.header-clear-btn{background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;padding:6px;cursor:pointer;transition:all .3s ease;display:flex;align-items:center;justify-content:center}.header-clear-btn:hover{background:#fff3;border-color:#ffffff4d;transform:scale(1.05)}.close-btn{background:#ffffff1a;border:none;border-radius:8px;color:#fff;padding:8px;cursor:pointer;transition:all .3s ease;display:flex;align-items:center;justify-content:center}.close-btn:hover{background:#fff3}.chat-container{flex:1;display:flex;flex-direction:column;overflow:hidden;background:#f8f9fa;height:calc(100vh - 180px)}.audio-visualizer{display:flex;flex-direction:column;align-items:center;justify-content:center;padding-top:20px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff}.wave-container{display:flex;align-items:center;gap:4px;margin-bottom:20px}.wave{width:4px;height:20px;background:#fff;border-radius:2px;animation:wave 1.2s ease-in-out infinite}.recording-text{margin:0;font-size:16px;font-weight:500;opacity:.9}.conversation-area{flex:1;display:flex;flex-direction:column;overflow:hidden}.conversation-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px}.message-item{display:flex;max-width:80%}.message-item.user{align-self:flex-end}.message-item.assistant{align-self:flex-start}.message-bubble{background:#fff;border-radius:18px;padding:12px 16px;box-shadow:0 2px 8px #0000001a;position:relative;border:1px solid #e1e8ed}.message-item.user .message-bubble{background:#007bff;color:#fff;border-color:#007bff}.message-item.assistant .message-bubble{background:#fff;color:#2c3e50}.message-text{line-height:1.4;word-wrap:break-word}.loading-dots{display:inline-flex;gap:4px}.loading-dots span{width:6px;height:6px;border-radius:50%;background:#bdc3c7;animation:loadingDots 1.4s infinite ease-in-out}.loading-dots span:nth-child(1){animation-delay:-.32s}.loading-dots span:nth-child(2){animation-delay:-.16s}.loading-dots span:nth-child(3){animation-delay:0s}.audio-controls{margin-top:8px;display:flex;align-items:center;gap:8px}.play-audio-btn{background:#f1f3f4;border:none;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .3s ease;color:#5f6368}.play-audio-btn:hover{background:#e8eaed;transform:scale(1.1)}.play-audio-btn.playing{background:#34a853;color:#fff}.message-time{font-size:11px;color:#8e9aaf;margin-top:4px;text-align:right}.message-item.user .message-time{color:#ffffffb3}.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:#8e9aaf;padding:40px 20px}.empty-icon{font-size:48px;margin-bottom:16px;opacity:.6}.empty-state p{margin:8px 0;font-size:16px}.empty-hint{font-size:14px!important;opacity:.7}.call-controls{background:#fff;padding:10px 10px 30px;border-top:1px solid #e1e8ed;display:flex;justify-content:center;align-items:center;gap:16px;flex-shrink:0;height:80px;position:relative;z-index:20}.call-controls.video-mode{background:#0000004d;border-top:1px solid rgba(255,255,255,.2);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.call-btn{display:flex;align-items:center;border:none;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease;width:40px;height:40px;border-radius:50%;justify-content:center}.call-btn.mic-off-btn{background:#6c757d;color:#fff;box-shadow:0 2px 8px #6c757d4d}.call-btn.mic-on-btn{background:#28a745;color:#fff;box-shadow:0 2px 8px #28a7454d}.call-btn.reconnect-btn{background:#6c757d;color:#fff;box-shadow:0 2px 8px #6c757d4d}.call-btn.hangup-btn{background:#dc3545;color:#fff;box-shadow:0 2px 8px #dc35454d}.call-btn.video-off-btn{background:#6c757d;color:#fff;box-shadow:0 2px 8px #6c757d4d}.call-btn.video-on-btn{background:#28a745;color:#fff;box-shadow:0 2px 8px #28a7454d}.video-container{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;background:#000;border-radius:0;overflow:hidden;z-index:10}.video-container.video-hidden{display:none}.video-display{width:100%;height:100%;object-fit:cover;display:block}.video-status{position:absolute;top:20px;left:20px;background:#000000b3;padding:8px 16px;border-radius:20px;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);z-index:11;display:flex;align-items:center;gap:8px}.video-indicator{color:#e74c3c;font-size:14px;font-weight:500;display:flex;align-items:center;gap:6px}.content-area{flex:1;display:flex;flex-direction:column;overflow:hidden}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translateY(50px)}to{opacity:1;transform:translateY(0)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes wave{0%,40%,to{transform:scaleY(.4)}20%{transform:scaleY(1)}}@keyframes loadingDots{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.typing-indicator{animation:blink 1s infinite;color:#3498db;font-weight:700}@keyframes blink{0%,50%{opacity:1}51%,to{opacity:0}}@keyframes dropdownSlide{0%{opacity:0;transform:translateY(-10px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}\n";
document.head.appendChild(style);
}
}
})();
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { computed, ref, onMounted, onUnmounted, createElementBlock, createCommentVNode, openBlock, createElementVNode, toDisplayString, withModifiers, withDirectives, Fragment, renderList, normalizeClass, vShow, normalizeStyle, createTextVNode, nextTick, createApp } from "vue";
const _imports_0 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='%23fff'%20viewBox='0%200%2016%2016'%3e%3cpath%20d='M5.5%205.5A.5.5%200%200%201%206%206v6a.5.5%200%200%201-1%200V6a.5.5%200%200%201%20.5-.5zm2.5%200a.5.5%200%200%201%20.5.5v6a.5.5%200%200%201-1%200V6a.5.5%200%200%201%20.5-.5zm3%20.5a.5.5%200%200%200-1%200v6a.5.5%200%200%200%201%200V6a.5.5%200%200%200-.5-.5z'/%3e%3cpath%20fill-rule='evenodd'%20d='M14.5%203a1%201%200%200%201-1%201H13v9a2%202%200%200%201-2%202H5a2%202%200%200%201-2-2V4h-.5a1%201%200%200%201-1-1V2a1%201%200%200%201%201-1H6a1%201%200%200%201%201-1h2a1%201%200%200%201%201%201h3.5a1%201%200%200%201%201%201v1zM4.118%204%204%204.059V13a1%201%200%200%200%201%201h6a1%201%200%200%200%201-1V4.059L11.882%204H4.118zM2.5%203V2h11v1h-11z'/%3e%3c/svg%3e";
const _imports_1 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='20'%20height='20'%20viewBox='0%200%2024%2024'%20fill='%23ffffff'%3e%3cpath%20d='M18%206L6%2018M6%206l12%2012'%20stroke='%23ffffff'%20stroke-width='2'%20stroke-linecap='round'/%3e%3c/svg%3e";
const _imports_2 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23EA3323'%3e%3cpath%20d='M24%2024H0V0h24v24z'%20fill='none'/%3e%3ccircle%20cx='12'%20cy='12'%20r='8'/%3e%3c/svg%3e";
const _imports_3 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0zm0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M19%2011h-1.7c0%20.74-.16%201.43-.43%202.05l1.23%201.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9%203.34%209%205v.18l5.98%205.99zM4.27%203L3%204.27l6.01%206.01V11c0%201.66%201.33%203%202.99%203%20.22%200%20.44-.03.65-.08l1.66%201.66c-.71.33-1.5.52-2.31.52-2.76%200-5.3-2.1-5.3-5.1H5c0%203.41%202.72%206.23%206%206.72V21h2v-3.28c.91-.13%201.77-.45%202.54-.9L19.73%2021%2021%2019.73%204.27%203z'/%3e%3c/svg%3e";
const _imports_4 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M12%2015c1.66%200%202.99-1.34%202.99-3L15%206c0-1.66-1.34-3-3-3S9%204.34%209%206v6c0%201.66%201.34%203%203%203zm5.3-3c0%203-2.54%205.1-5.3%205.1S6.7%2015%206.7%2012H5c0%203.42%202.72%206.23%206%206.72V22h2v-3.28c3.28-.48%206-3.3%206-6.72h-1.7z'/%3e%3c/svg%3e";
const _imports_5 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0zm0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M21%206.5l-4%204V7c0-.55-.45-1-1-1H9.82L21%2017.18V6.5zM3.27%202L2%203.27%204.73%206H4c-.55%200-1%20.45-1%201v10c0%20.55.45%201%201%201h12c.21%200%20.39-.08.54-.18L19.73%2021%2021%2019.73%203.27%202z'/%3e%3c/svg%3e";
const _imports_6 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M17%2010.5V7c0-.55-.45-1-1-1H4c-.55%200-1%20.45-1%201v10c0%20.55.45%201%201%201h12c.55%200%201-.45%201-1v-3.5l4%204v-11l-4%204z'/%3e%3c/svg%3e";
const _imports_7 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M6.62%2010.79c1.44%202.83%203.76%205.14%206.59%206.59l2.2-2.2c.27-.27.67-.36%201.02-.24%201.12.37%202.33.57%203.57.57.55%200%201%20.45%201%201V20c0%20.55-.45%201-1%201-9.39%200-17-7.61-17-17%200-.55.45-1%201-1h3.5c.55%200%201%20.45%201%201%200%201.25.2%202.45.57%203.57.11.35.03.74-.25%201.02l-2.2%202.2z'/%3e%3c/svg%3e";
const _imports_8 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23fff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M17.34%2014.54l-1.43-1.43c.56-.73%201.05-1.5%201.47-2.32l-2.2-2.2c-.28-.28-.36-.67-.25-1.02.37-1.12.57-2.32.57-3.57%200-.55.45-1%201-1H20c.55%200%201%20.45%201%201%200%203.98-1.37%207.64-3.66%2010.54zm-2.82%202.81C11.63%2019.64%207.97%2021%204%2021c-.55%200-1-.45-1-1v-3.49c0-.55.45-1%201-1%201.24%200%202.45-.2%203.57-.57.35-.12.75-.03%201.02.24l2.2%202.2c.81-.42%201.58-.9%202.3-1.46L1.39%204.22l1.42-1.41L21.19%2021.2l-1.41%201.41-5.26-5.26z'/%3e%3c/svg%3e";
const DEFAULT_CONFIG = {
// WebSocket configuration
websocket: {
url: "",
token: "",
reconnect: false,
reconnectInterval: 5e3,
maxReconnectAttempts: 3
},
// Audio configuration
audio: {
sampleRate: 16e3,
channelCount: 1,
bitsPerSample: 16,
chunkDuration: 1e3,
// milliseconds
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
},
// Video configuration
video: {
enabled: false,
frameRate: 2,
quality: 0.8,
facingMode: "environment"
},
// User information configuration
user: {
phoneId: "",
phoneList: [],
// List of available phone numbers
sessionId: ""
},
// UI configuration
ui: {
showLogs: true,
showVisualizer: true,
logHeight: 300,
theme: "default"
// default, dark, light
},
// Message configuration
messages: {
startSession: "AI agent start session",
connecting: "Connecting...",
connected: "Connected",
disconnected: "Disconnected",
recording: "Recording",
stopped: "Stopped",
waiting: "Waiting"
},
// Callback functions
callbacks: {
onConnect: void 0,
// Connection established callback
onDisconnect: void 0,
// Connection lost callback
onError: void 0,
// Error callback
onMessage: void 0,
// Message received callback
onTranscript: void 0,
// Transcription completed callback
onAudioOutput: void 0
// Audio output callback
}
};
function mergeConfig(userConfig = {}) {
return deepMerge(DEFAULT_CONFIG, userConfig);
}
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
result[key] = deepMerge(target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
function validateConfig(config) {
const errors = [];
if (!config.websocket.url) {
errors.push("WebSocket URL is required");
}
if (!config.websocket.token) {
errors.push("WebSocket token is required");
}
if (config.audio.sampleRate && config.audio.sampleRate <= 0) {
errors.push("Audio sample rate must be greater than 0");
}
if (config.audio.channelCount && config.audio.channelCount <= 0) {
errors.push("Audio channel count must be greater than 0");
}
if (config.audio.chunkDuration && config.audio.chunkDuration <= 0) {
errors.push("Audio chunk duration must be greater than 0");
}
if (!config.user.phoneId && (!config.user.phoneList || config.user.phoneList.length === 0)) {
errors.push("Either phoneId or phoneList must be provided");
}
return errors;
}
class AudioQueueManager {
constructor() {
__publicField(this, "audioQueue", []);
// 音频片段队列
__publicField(this, "isPlaying", false);
// 是否正在播放
__publicField(this, "isPaused", false);
// 是否暂停(用于语音打断)
__publicField(this, "currentMessage", null);
// 当前关联的消息
__publicField(this, "audioContext", null);
// 音频上下文
__publicField(this, "gainNode", null);
// 音量控制节点
__publicField(this, "playbackStartTime", 0);
// 播放开始时间
__publicField(this, "scheduledBuffers", []);
// 已调度的音频缓冲区
__publicField(this, "nextStartTime", 0);
// 下一个音频片段的开始时间
__publicField(this, "sampleRate", 22050);
// 采样率
__publicField(this, "processingQueue", false);
}
// 是否正在处理队列
/**
* 初始化音频上下文
*/
async initAudioContext() {
if (!this.audioContext) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
this.audioContext = new AudioContextClass();
this.gainNode = this.audioContext.createGain();
this.gainNode.connect(this.audioContext.destination);
}
if (this.audioContext.state === "suspended") {
await this.audioContext.resume();
}
}
/**
* 添加音频片段到队列
*/
async enqueueAudio(base64Audio, message = null) {
try {
const audioBuffer = await this.decodeAudioData(base64Audio);
this.audioQueue.push({
buffer: audioBuffer,
base64Audio,
message,
timestamp: Date.now()
});
console.log(`[AudioQueue] 添加音频片段到队列,队列长度: ${this.audioQueue.length}`);
if (this.isPaused) {
console.log(`[AudioQueue] 暂停状态,音频片段已解码并添加到队列,等待恢复播放`);
return;
}
if (!this.isPlaying) {
await this.startPlayback(message);
}
} catch (error) {
console.error("[AudioQueue] 添加音频片段失败:", error);
}
}
/**
* 解码Base64音频数据为AudioBuffer
*/
async decodeAudioData(base64Audio) {
await this.initAudioContext();
const binaryString = atob(base64Audio);
const pcmData = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
pcmData[i] = binaryString.charCodeAt(i);
}
const audioBuffer = this.audioContext.createBuffer(
1,
// 单声道
pcmData.length / 2,
// 采样点数 (16位PCM)
this.sampleRate
// 采样率
);
const channelData = audioBuffer.getChannelData(0);
const dataView = new DataView(pcmData.buffer);
for (let i = 0; i < channelData.length; i++) {
const sample = dataView.getInt16(i * 2, true);
channelData[i] = sample / 32768;
}
return audioBuffer;
}
/**
* 开始播放队列中的音频
*/
async startPlayback(message = null) {
if (this.isPlaying || this.isPaused) {
return;
}
await this.initAudioContext();
this.isPlaying = true;
this.currentMessage = message;
this.playbackStartTime = this.audioContext.currentTime;
this.nextStartTime = this.playbackStartTime;
if (this.currentMessage) {
this.currentMessage.isPlaying = true;
}
console.log("[AudioQueue] 开始流式播放音频");
this.processQueue();
}
/**
* 处理音频队列
*/
async processQueue() {
if (this.processingQueue) {
return;
}
this.processingQueue = true;
while (this.isPlaying && !this.isPaused && (this.audioQueue.length > 0 || this.scheduledBuffers.length > 0)) {
if (this.isPaused || !this.isPlaying) {
console.log("[AudioQueue] 队列处理被打断,停止处理");
break;
}
if (this.audioQueue.length > 0) {
const audioItem = this.audioQueue.shift();
if (this.isPaused || !this.isPlaying) {
console.log("[AudioQueue] 在处理音频片段时被打断,停止处理");
break;
}
if (!audioItem.buffer && audioItem.base64Audio) {
try {
audioItem.buffer = await this.decodeAudioData(audioItem.base64Audio);
} catch (error) {
console.error("[AudioQueue] 解码音频失败:", error);
continue;
}
}
if (this.isPaused || !this.isPlaying) {
console.log("[AudioQueue] 解码完成后发现已被打断,停止处理");
break;
}
if (audioItem.buffer) {
await this.scheduleAudioBuffer(audioItem.buffer);
}
} else {
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
this.processingQueue = false;
if (!this.isPaused && this.isPlaying) {
if (this.scheduledBuffers.length > 0) {
const lastBuffer = this.scheduledBuffers[this.scheduledBuffers.length - 1];
const waitTime = (lastBuffer.stopTime - this.audioContext.currentTime) * 1e3;
if (waitTime > 0) {
setTimeout(() => {
if (!this.isPaused && this.isPlaying) {
this.stopPlayback();
}
}, waitTime);
}
} else {
this.stopPlayback();
}
}
}
/**
* 调度音频缓冲区播放
*/
async scheduleAudioBuffer(audioBuffer) {
if (!this.audioContext || !audioBuffer) {
return;
}
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.gainNode);
const startTime = this.nextStartTime;
const stopTime = startTime + audioBuffer.duration;
const bufferInfo = {
source,
startTime,
stopTime
};
this.scheduledBuffers.push(bufferInfo);
source.onended = () => {
const index2 = this.scheduledBuffers.indexOf(bufferInfo);
if (index2 > -1) {
this.scheduledBuffers.splice(index2, 1);
}
};
source.start(startTime);
this.nextStartTime = stopTime;
console.log(`[AudioQueue] 调度音频缓冲区播放,开始时间: ${startTime.toFixed(3)}s,持续时间: ${audioBuffer.duration.toFixed(3)}s`);
}
/**
* 打断当前播放(用于语音检测开始时)
*/
interrupt() {
console.log("[AudioQueue] 收到打断信号,立即停止所有音频播放");
this.isPaused = true;
this.isPlaying = false;
this.processingQueue = false;
this.scheduledBuffers.forEach((bufferInfo) => {
if (bufferInfo.source) {
try {
bufferInfo.source.stop();
} catch (error) {
console.log("[AudioQueue] 音频源已停止或未开始:", error.message);
}
}
});
this.scheduledBuffers = [];
this.audioQueue = [];
this.nextStartTime = 0;
console.log("[AudioQueue] 已清空音频队列和调度列表,不保留被打断的音频");
if (this.currentMessage) {
this.currentMessage.isPlaying = false;
}
console.log("[AudioQueue] 音频播放已完全打断,进入暂停状态,等待新音频");
}
/**
* 恢复播放(用于语音检测结束时)
*/
async resume() {
if (!this.isPaused) {
return;
}
console.log("[AudioQueue] 收到恢复信号,准备恢复音频播放");
this.isPaused = false;
if (this.audioQueue.length > 0) {
await this.startPlayback(this.currentMessage);
console.log("[AudioQueue] 音频播放已恢复");
} else {
console.log("[AudioQueue] 队列为空,等待新的音频片段");
}
}
/**
* 停止播放
*/
stopPlayback() {
console.log("[AudioQueue] 停止音频播放");
this.isPlaying = false;
this.isPaused = false;
this.scheduledBuffers.forEach((bufferInfo) => {
if (bufferInfo.source) {
try {
bufferInfo.source.stop();
} catch (error) {
}
}
});
this.scheduledBuffers = [];
this.nextStartTime = 0;
if (this.currentMessage) {
this.currentMessage.isPlaying = false;
this.currentMessage = null;
}
}
/**
* 清空队列
*/
clearQueue() {
this.audioQueue = [];
console.log("[AudioQueue] 音频队列已清空");
}
/**
* 销毁管理器
*/
destroy() {
console.log("[AudioQueue] 销毁音频队列管理器");
this.stopPlayback();
this.clearQueue();
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
this.gainNode = null;
this.currentMessage = null;
}
/**
* 获取队列状态
*/
getStatus() {
return {
queueLength: this.audioQueue.length,
isPlaying: this.isPlaying,
isPaused: this.isPaused,
scheduledBuffers: this.scheduledBuffers.length
};
}
}
const _Logger = class _Logger {
constructor() {
if (_Logger.instance) {
return _Logger.instance;
}
_Logger.instance = this;
}
/**
* 通用日志输出方法
*/
log(content, type = "info", module = "") {
const importantTypes = ["error", "warning", "success"];
const isImportantContent = content.includes("ready") || content.includes("Transcription complete") || content.includes("Final response") || content.includes("connection") || content.includes("initialized") || content.includes("stopped");
if (importantTypes.includes(type) || isImportantContent) {
const modulePrefix = module ? `[${module}] ` : "";
console.log(`[${type.toUpperCase()}] ${modulePrefix}${content}`);
}
}
/**
* 错误日志
*/
error(content, module = "") {
this.log(content, "error", module);
}
/**
* 警告日志
*/
warning(content, module = "") {
this.log(content, "warning", module);
}
/**
* 成功日志
*/
success(content, module = "") {
this.log(content, "success", module);
}
/**
* 信息日志
*/
info(content, module = "") {
this.log(content, "info", module);
}
/**
* 调试日志
*/
debug(content, module = "") {
this.log(content, "debug", module);
}
};
__publicField(_Logger, "instance", null);
let Logger = _Logger;
const logger = new Logger();
const log = (content, type = "info", module = "") => logger.log(content, type, module);
class WebSocketManager {
constructor() {
__publicField(this, "websocket", null);
__publicField(this, "config", null);
__publicField(this, "callbacks", {});
}
/**
* 初始化配置和回调
*/
init(config, callbacks = {}) {
this.config = config;
this.callbacks = callbacks;
}
/**
* 连接WebSocket
*/
connect(selectedPhoneNumber) {
return new Promise((resolve, reject) => {
try {
const wsUrl = `${this.config.websocket.url}?token=${this.config.websocket.token}`;
this.websocket = new WebSocket(wsUrl);
this.websocket.onopen = () => {
var _a, _b;
log("WebSocket connection successful", "success", "WebSocket");
const initMessage = {
cmd: "start_session",
content: ((_a = this.config.messages) == null ? void 0 : _a.startSession) || "AI agent start session",
phoneId: selectedPhoneNumber || this.config.user.phoneId,
sessionId: ((_b = this.config) == null ? void 0 : _b.sessionId) || ""
};
this.websocket.send(JSON.stringify(initMessage));
log(`Sent initialization message, using number: ${initMessage.phoneId}`, "info", "WebSocket");
resolve();
};
this.websocket.onmessage = (event) => {
if (this.callbacks.onMessage) {
this.callbacks.onMessage(event.data);
}
};
this.websocket.onclose = (event) => {
log(`Connection closed: ${event.code} - ${event.reason}`, "warning", "WebSocket");
if (this.callbacks.onClose) {
this.callbacks.onClose(event);
}
};
this.websocket.onerror = (error) => {
log("WebSocket connection error", "error", "WebSocket");
if (this.callbacks.onError) {
this.callbacks.onError(error);
}
reject(error);
};
} catch (error) {
reject(error);
}
});
}
/**
* 发送消息
*/
send(message) {
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
log("WebSocket not connected, cannot send message", "error", "WebSocket");
return false;
}
try {
this.websocket.send(JSON.stringify(message));
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Message send failed: ${errorMessage}`, "error", "WebSocket");
return false;
}
}
/**
* 发送音频数据
*/
sendAudioData(audioBuffer) {
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
log("WebSocket not connected, cannot send audio", "error", "WebSocket");
return false;
}
try {
const byteArray = new Uint8Array(audioBuffer.length * 2);
const dataView = new DataView(byteArray.buffer);
for (let i = 0; i < audioBuffer.length; i++) {
dataView.setInt16(i * 2, audioBuffer[i], true);
}
const base64Audio = btoa(String.fromCharCode(...byteArray));
const audioMessage = {
cmd: "audio_input",
content: base64Audio
};
this.websocket.send(JSON.stringify(audioMessage));
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Audio send failed: ${errorMessage}`, "error", "WebSocket");
return false;
}
}
/**
* 发送视频数据
*/
sendVideoData(base64Data) {
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
log("WebSocket not connected, cannot send video", "error", "WebSocket");
return false;
}
try {
const message = {
cmd: "video_input",
content: base64Data
};
this.websocket.send(JSON.stringify(message));
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Video data send failed: ${errorMessage}`, "error", "WebSocket");
return false;
}
}
/**
* 关闭连接
*/
close() {
if (this.websocket) {
log("WebSocket closed", "info", "WebSocket");
this.websocket.close();
this.websocket = null;
}
}
/**
* 检查连接状态
*/
isConnected() {
return this.websocket !== null && this.websocket.readyState === WebSocket.OPEN;
}
}
class AudioManager {
constructor() {
__publicField(this, "audioStream", null);
__publicField(this, "audioContext", null);
__publicField(this, "processor", null);
__publicField(this, "config", null);
__publicField(this, "callbacks", {});
}
/**
* 初始化配置和回调
*/
init(config, callbacks = {}) {
this.config = config;
this.callbacks = callbacks;
}
/**
* 请求麦克风权限并初始化音频流
*/
async requestMicrophonePermission() {
try {
this.audioStream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: this.config.audio.sampleRate,
channelCount: this.config.audio.channelCount,
echoCancellation: this.config.audio.echoCancellation,
noiseSuppression: this.config.audio.noiseSuppression,
autoGainControl: this.config.audio.autoGainControl
}
});
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Microphone permission request failed: ${errorMessage}`, "error", "Audio");
throw error;
}
}
/**
* 初始化音频处理
*/
async initAudioProcessing() {
try {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
this.audioContext = new AudioContextClass({
sampleRate: this.config.audio.sampleRate
});
log("Audio context initialized successfully", "success", "Audio");
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Audio initialization failed: ${errorMessage}`, "error", "Audio");
throw error;
}
}
/**
* 开始音频捕获
*/
startAudioCapture() {
if (!this.audioStream || !this.audioContext) {
log("Audio stream or context not ready", "error", "Audio");
return false;
}
try {
const source = this.audioContext.createMediaStreamSource(this.audioStream);
this.processor = this.audioContext.createScriptProcessor(4096, 1, 1);
let audioBuffer = [];
let lastSendTime = Date.now();
this.processor.onaudioprocess = (event) => {
const inputBuffer = event.inputBuffer;
const inputData = inputBuffer.getChannelData(0);
const pcm16Data = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
pcm16Data[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32767));
}
audioBuffer.push(...Array.from(pcm16Data));
const now = Date.now();
if (now - lastSendTime >= (this.config.audio.chunkDuration || 1e3) && audioBuffer.length > 0) {
if (this.callbacks.onAudioData) {
this.callbacks.onAudioData(audioBuffer);
}
audioBuffer = [];
lastSendTime = now;
}
};
source.connect(this.processor);
this.processor.connect(this.audioContext.destination);
log("Audio capture started", "success", "Audio");
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Audio capture startup failed: ${errorMessage}`, "error", "Audio");
return false;
}
}
/**
* 停止音频捕获
*/
stopAudioCapture() {
try {
if (this.processor) {
this.processor.disconnect();
this.processor = null;
log("Audio processor disconnected", "info", "Audio");
}
if (this.audioStream) {
this.audioStream.getTracks().forEach((track) => {
track.stop();
log(`Audio track stopped: ${track.label}`, "info", "Audio");
});
this.audioStream = null;
}
if (this.audioContext && this.audioContext.state !== "closed") {
this.audioContext.close();
this.audioContext = null;
log("Audio context closed", "info", "Audio");
}
log("Audio capture stopped successfully", "success", "Audio");
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Error stopping audio capture: ${errorMessage}`, "error", "Audio");
return false;
}
}
/**
* 恢复音频捕获
*/
async resumeAudioCapture() {
try {
await this.requestMicrophonePermission();
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
this.audioContext = new AudioContextClass({
sampleRate: this.config.audio.sampleRate
});
return this.startAudioCapture();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Failed to resume audio capture: ${errorMessage}`, "error", "Audio");
return false;
}
}
/**
* 播放音频响应
*/
async playAudioResponse(base64Audio, message) {
try {
log("Starting audio playback...", "info", "Audio");
const binaryString = atob(base64Audio);
const pcmData = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
pcmData[i] = binaryString.charCodeAt(i);
}
const wavData = this.pcmToWav(pcmData, 22050, 1);
const blob = new Blob([wavData], { type: "audio/wav" });
const audioUrl = URL.createObjectURL(blob);
const audio = new Audio(audioUrl);
if (message) {
message.audioInstance = audio;
message.isPlaying = true;
}
audio.onloadstart = () => log("Audio loading started", "info", "Audio");
audio.oncanplay = () => log("Audio can play", "info", "Audio");
audio.onplay = () => log("Audio playback started", "info", "Audio");
audio.onended = () => {
log("Audio playback completed", "info", "Audio");
URL.revokeObjectURL(audioUrl);
if (message) {
message.isPlaying = false;
message.audioInstance = null;
}
};
audio.onerror = () => {
log("Audio playback error", "error", "Audio");
URL.revokeObjectURL(audioUrl);
if (message) {
message.isPlaying = false;
message.audioInstance = null;
}
};
await audio.play();
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Audio playback failed: ${errorMessage}`, "error", "Audio");
console.error("Audio playback error:", error);
if (message) {
message.isPlaying = false;
message.audioInstance = null;
}
return false;
}
}
/**
* Convert PCM data to WAV format
*/
pcmToWav(pcmData, sampleRate, numChannels) {
const length = pcmData.length;
const buffer = new ArrayBuffer(44 + length);
const view = new DataView(buffer);
const writeString = (offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(0, "RIFF");
view.setUint32(4, 36 + length, true);
writeString(8, "WAVE");
writeString(12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * numChannels * 2, true);
view.setUint16(32, numChannels * 2, true);
view.setUint16(34, 16, true);
writeString(36, "data");
view.setUint32(40, length, true);
const dataView = new Uint8Array(buffer, 44);
dataView.set(pcmData);
return buffer;
}
/**
* 检查是否正在录音
*/
isRecording() {
return this.processor !== null && this.audioStream !== null;
}
/**
* 清理资源
*/
cleanup() {
this.stopAudioCapture();
}
}
class VideoManager {
constructor() {
__publicField(this, "videoStream", null);
__publicField(this, "videoElement", null);
__publicField(this, "canvas", null);
__publicField(this, "context", null);
__publicField(this, "config", null);
__publicField(this, "callbacks", {});
__publicField(this, "captureInterval", null);
__publicField(this, "isCapturing", false);
}
/**
* 初始化配置和回调
*/
init(config, callbacks = {}) {
this.config = config;
this.callbacks = callbacks;
}
/**
* 请求摄像头权限并初始化视频流
*/
async requestCameraPermission() {
var _a, _b;
try {
this.videoStream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: ((_b = (_a = this.config) == null ? void 0 : _a.video) == null ? void 0 : _b.facingMode) || "environment",
// 后置摄像头
width: { ideal: 640 },
height: { ideal: 480 }
},
audio: false
});
log("Camera permission granted successfully", "success", "Video");
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Camera permission request failed: ${errorMessage}`, "error", "Video");
throw error;
}
}
/**
* 初始化视频元素和画布
*/
async initVideoElements(videoElement) {
if (!this.videoStream) {
throw new Error("Video stream not available. Call requestCameraPermission first.");
}
this.videoElement = videoElement;
this.videoElement.srcObject = this.videoStream;
try {
await this.videoElement.play();
log("Video element started playing", "success", "Video");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Failed to play video: ${errorMessage}`, "error", "Video");
throw error;
}
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
return new Promise((resolve) => {
const handleLoadedMetadata = () => {
this.canvas.width = this.videoElement.videoWidth || 640;
this.canvas.height = this.videoElement.videoHeight || 480;
log(`Video initialized: ${this.canvas.width}x${this.canvas.height}`, "info", "Video");
this.videoElement.removeEventListener("loadedmetadata", handleLoadedMetadata);
resolve();
};
if (this.videoElement.readyState >= 1) {
handleLoadedMetadata();
} else {
this.videoElement.addEventListener("loadedmetadata", handleLoadedMetadata);
}
});
}
/**
* 开始捕获视频帧
*/
startCapture() {
var _a, _b;
if (this.isCapturing) return;
this.isCapturing = true;
const frameRate = ((_b = (_a = this.config) == null ? void 0 : _a.video) == null ? void 0 : _b.frameRate) || 2;
const interval = 1e3 / frameRate;
this.captureInterval = setInterval(() => {
this.captureFrame();
}, interval);
log(`Video capture started (${frameRate} FPS)`, "success", "Video");
}
/**
* 停止捕获视频帧
*/
stopCapture() {
if (!this.isCapturing) return;
this.isCapturing = false;
if (this.captureInterval) {
clearInterval(this.captureInterval);
this.captureInterval = null;
}
log("Video capture stopped", "info", "Video");
}
/**
* 捕获当前视频帧并转换为base64
*/
captureFrame() {
var _a, _b;
if (!this.videoElement || !this.canvas || !this.context) return;
try {
this.context.drawImage(
this.videoElement,
0,
0,
this.canvas.width,
this.canvas.height
);
const quality = ((_b = (_a = this.config) == null ? void 0 : _a.video) == null ? void 0 : _b.quality) || 0.8;
const base64Data = this.canvas.toDataURL("image/jpeg", quality).split(",")[1];
if (this.callbacks.onVideoFrame) {
this.callbacks.onVideoFrame(base64Data);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Failed to capture frame: ${errorMessage}`, "error", "Video");
}
}
/**
* 停止视频流
*/
stopVideoStream() {
this.stopCapture();
if (this.videoStream) {
this.videoStream.getTracks().forEach((track) => {
track.stop();
});
this.videoStream = null;
}
if (this.videoElement) {
this.videoElement.srcObject = null;
}
log("Video stream stopped", "info", "Video");
}
/**
* 检查是否正在捕获
*/
isVideoCapturing() {
return this.isCapturing;
}
/**
* 清理资源
*/
destroy() {
this.stopVideoStream();
this.canvas = null;
this.context = null;
this.videoElement = null;
}
}
class MessageHandler {
constructor() {
__publicField(this, "config", null);
__publicField(this, "callbacks", {});
__publicField(this, "audioOutputCache", "");
__publicField(this, "textOutputCache", "");
//@ts-ignore
__publicField(this, "currentUserMessage", null);
__publicField(this, "currentAssistantMessage", null);
__publicField(this, "shouldDiscardAudio", false);
}
/**
* 初始化配置和回调
*/
init(config, callbacks = {}) {
this.config = config;
this.callbacks = callbacks;
this.currentUserMessage = null;
}
/**
* 处理WebSocket消息
*/
handleMessage(messageData, conversationList, audioQueueManager) {
var _a, _b;
try {
const payload = JSON.parse(messageData);
log(`Received message: ${payload.cmd}`, "info", "Message");
if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onMessage) {
this.config.callbacks.onMessage(payload);
}
switch (payload.cmd) {
case "ready":
this.handleReady();
break;
case "speech_start":
this.handleSpeechStart(audioQueueManager);
break;
case "speech_stop":
this.handleSpeechStop();
break;
case "transcript_done":
this.handleTranscriptDone(payload, conversationList);
break;
case "response_create":
this.handleResponseCreate(conversationList);
break;
case "text_output":
this.handleTextOutput(payload, conversationList);
break;
case "audio_output":
this.handleAudioOutput(payload, audioQueueManager);
break;
case "audio_done":
this.handleAudioDone();
break;
case "text_done":
this.handleTextDone();
break;
case "final_response":
this.handleFinalResponse(payload);
break;
default:
log(`Unknown command: ${payload.cmd}`, "warning", "Message");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
log(`Message parsing error: ${errorMessage}`, "error", "Message");
}
}
/**
* 处理ready消息
*/
handleReady() {
log("Server ready, starting voice input", "success", "Message");
if (this.callbacks.onReady) {
this.callbacks.onReady();
}
}
/**
* 处理语音开始
*/
handleSpeechStart(audioQueueManager) {
var _a, _b;
log("Speech detected, stopping current audio playback", "info", "Message");
this.shouldDiscardAudio = true;
if (audioQueueManager) {
audioQueueManager.interrupt();
log("Audio playback interrupted, all future audio will be discarded until next response_create", "info", "Message");
}
if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onSpeechStart) {
this.config.callbacks.onSpeechStart();
}
}
/**
* 处理语音结束
*/
handleSpeechStop() {
var _a, _b;
log("Speech ended, but audio will remain discarded until next response_create", "info", "Message");
if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onSpeechStop) {
this.config.callbacks.onSpeechStop();
}
}
/**
* 处理转录完成
*/
handleTranscriptDone(payload, conversationList) {
var _a, _b;
log(`Transcription complete: ${payload.content}`, "info", "Message");
this.currentUserMessage = payload.content;
conversationList.value.push({
id: Date.now(),
type: "user",
content: payload.content,
timestamp: /* @__PURE__ */ new Date(),
audioData: null
});
if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onTranscript) {
this.config.callbacks.onTranscript(payload.content);
}
if (this.callbacks.scrollToBottom) {
this.callbacks.scrollToBottom();
}
}
/**
* 处理响应创建
*/
handleResponseCreate(conversationList) {
log("Starting response generation - creating new audio queue for this response", "info", "Message");
this.shouldDiscardAudio = false;
if (this.callbacks.initAudioQueueManager) {
this.callbacks.initAudioQueueManager();
}
this.audioOutputCache = "";
this.textOutputCache = "";
this.currentAssistantMessage = {
id: Date.now(),
type: "assistant",
content: "",
// Initial content is empty
timestamp: /* @__PURE__ */ new Date(),
audioData: null,
isLoading: true,
isStreaming: true
// Mark as streaming playback
};
conversationList.value.push(this.currentAssistantMessage);
if (this.callbacks.scrollToBottom) {
this.callbacks.scrollToBottom();
}
}
/**
* 处理文本输出
*/
handleTextOutput(payload, conversationList) {
log(`Text output segment: ${payload.content}`, "info", "Message");
this.textOutputCache += payload.content;
if (this.currentAssistantMessage) {
const messageIndex = conversationList.value.findIndex((msg) => msg.id === this.currentAssistantMessage.id);
if (messageIndex !== -1) {
conversationList.value[messageIndex] = {
...conversationList.value[messageIndex],
content: this.textOutputCache,
isLoading: false
};
if (this.callbacks.scrollToBottom) {
setTimeout(() => this.callbacks.scrollToBottom(), 0);
}
}
}
}
/**
* 处理音频输出
*/
handleAudioOutput(payload, audioQueueManager) {
var _a, _b, _c, _d;
if (this.shouldDiscardAudio) {
log(`Audio segment discarded due to speech interruption`, "info", "Message");
this.audioOutputCache += payload.content;
if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onAudioOutput) {
this.config.callbacks.onAudioOutput(payload.content);
}
return;
}
log(`Received audio segment, adding to current response queue`, "info", "Message");
if (audioQueueManager && payload.content) {
audioQueueManager.enqueueAudio(payload.content, this.currentAssistantMessage || void 0);
if (this.currentAssistantMessage && !this.currentAssistantMessage.hasStartedPlaying) {
this.currentAssistantMessage.hasStartedPlaying = true;
this.currentAssistantMessage.isPlaying = true;
log("Starting streaming audio playback for current response", "info", "Message");
}
}
this.audioOutputCache += payload.content;
if ((_d = (_c = this.config) == null ? void 0 : _c.callbacks) == null ? void 0 : _d.onAudioOutput) {
this.config.callbacks.onAudioOutput(payload.content);
}
}
/**
* 处理音频完成
*/
handleAudioDone() {
log("Audio transmission complete for current response", "info", "Message");
if (this.currentAssistantMessage && this.audioOutputCache.length > 0) {
this.currentAssistantMessage.audioData = this.audioOutputCache;
this