ibi-ai-talk 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/index.common.js +26 -3
- package/dist/index.common.js.map +1 -1
- package/dist/index.umd.js +26 -3
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/package.json +1 -1
- package/src/index.vue +25 -2
package/dist/index.umd.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t():"function"===typeof define&&define.amd?define([],t):"object"===typeof exports?exports["index"]=t():e["index"]=t()})("undefined"!==typeof self?self:this,(()=>(()=>{"use strict";var e={};(()=>{e.d=(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})}})(),(()=>{e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})(),(()=>{e.p=""})();var t={};if(e.d(t,{default:()=>Z}),"undefined"!==typeof window){var n=window.document.currentScript,o=n&&n.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);o&&(e.p=o[1])}var s=function(){var e=this,t=e._self._c;return t("div")},i=[];function r(){try{if("undefined"===typeof Module)throw new Error("Opus库未加载,Module对象不存在");throw"undefined"!==typeof Module.instance&&"function"===typeof Module.instance._opus_decoder_get_size&&(window.ModuleInstance=Module.instance,console.log("Opus库加载成功(使用Module.instance)","success")),"function"===typeof Module._opus_decoder_get_size&&(window.ModuleInstance=Module,console.log("Opus库加载成功(使用全局Module)","success")),new Error("Opus解码函数未找到,可能Module结构不正确")}catch(e){console.log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${e.message}`,"error")}}let c=null;function a(){try{if(c)return c;if(!window.ModuleInstance)return void console.log("无法创建Opus编码器:ModuleInstance不可用","error");const e=window.ModuleInstance,t=16e3,n=1,o=2048;return c={channels:n,sampleRate:t,frameSize:960,maxPacketSize:4e3,module:e,init:function(){try{const t=e._opus_encoder_get_size(this.channels);if(console.log(`Opus编码器大小: ${t}字节`,"info"),this.encoderPtr=e._malloc(t),!this.encoderPtr)throw new Error("无法分配编码器内存");const n=e._opus_encoder_init(this.encoderPtr,this.sampleRate,this.channels,o);if(n<0)throw new Error(`Opus编码器初始化失败: ${n}`);return e._opus_encoder_ctl(this.encoderPtr,4002,16e3),e._opus_encoder_ctl(this.encoderPtr,4010,5),e._opus_encoder_ctl(this.encoderPtr,4016,1),console.log("Opus编码器初始化成功","success"),!0}catch(t){return this.encoderPtr&&(e._free(this.encoderPtr),this.encoderPtr=null),console.log(`Opus编码器初始化失败: ${t.message}`,"error"),!1}},encode:function(e){if(!this.encoderPtr&&!this.init())return null;try{const t=this.module,n=t._malloc(2*e.length);for(let r=0;r<e.length;r++)t.HEAP16[(n>>1)+r]=e[r];const o=t._malloc(this.maxPacketSize),s=t._opus_encode(this.encoderPtr,n,this.frameSize,o,this.maxPacketSize);if(s<0)throw new Error(`Opus编码失败: ${s}`);const i=new Uint8Array(s);for(let e=0;e<s;e++)i[e]=t.HEAPU8[o+e];return t._free(n),t._free(o),i}catch(t){return console.log(`Opus编码出错: ${t.message}`,"error"),null}},destroy:function(){this.encoderPtr&&(this.module._free(this.encoderPtr),this.encoderPtr=null)}},c.init(),c}catch(e){return console.log(`创建Opus编码器失败: ${e.message}`,"error"),!1}}class l{#e=[];#t=[];#n=null;#o=null;enqueue(e,...t){if(0===t.length)this.#e.push(e);else{const n=[e,...t].filter((e=>e));if(0===n.length)return;this.#e.push(...n)}this.#o&&(this.#o(),this.#o=null,this.#n=null),this.#s()}async dequeue(e=1,t=1/0,n=null){return 0===this.#e.length&&await this.#i(),this.#e.length>=e?this.#r():new Promise(((o,s)=>{let i=null;const r={resolve:o,reject:s,min:e,onTimeout:n,timer:i};Number.isFinite(t)&&(r.timer=setTimeout((()=>{this.#c(r),n&&n(this.#e.length),o(this.#r())}),t)),this.#t.push(r)}))}#i(){return this.#n||(this.#n=new Promise((e=>this.#o=e))),this.#n}#s(){for(let e=this.#t.length-1;e>=0;e--){const t=this.#t[e];this.#e.length>=t.min&&(this.#c(t),t.resolve(this.#r()))}}#c(e){const t=this.#t.indexOf(e);-1!==t&&(this.#t.splice(t,1),e.timer&&clearTimeout(e.timer))}#r(){const e=[...this.#e];return this.#e.length=0,e}get length(){return this.#e.length}}class u{constructor(e,t,n,o,s){this.opusDecoder=e,this.audioContext=t,this.sampleRate=n,this.channels=o,this.minAudioDuration=s,this.queue=[],this.activeQueue=new l,this.pendingAudioBufferQueue=[],this.audioBufferQueue=new l,this.playing=!1,this.endOfStream=!1,this.source=null,this.totalSamples=0,this.lastPlayTime=0}pushAudioBuffer(e){this.audioBufferQueue.enqueue(...e)}async getPendingAudioBufferQueue(){[this.pendingAudioBufferQueue,this.audioBufferQueue]=[await this.audioBufferQueue.dequeue(),new l]}async getQueue(e){let t=[];const n=e-this.queue.length>0?e-this.queue.length:1;[t,this.activeQueue]=[await this.activeQueue.dequeue(n),new l],this.queue.push(...t)}convertInt16ToFloat32(e){const t=new Float32Array(e.length);for(let n=0;n<e.length;n++)t[n]=e[n]/32768;return t}async decodeOpusFrames(){if(this.opusDecoder){console.log("Opus解码器启动","info");while(1){let t=[];for(const n of this.pendingAudioBufferQueue)try{const e=this.opusDecoder.decode(n);if(e&&e.length>0){const n=this.convertInt16ToFloat32(e);for(let e=0;e<n.length;e++)t.push(n[e])}}catch(e){console.log("Opus解码失败: "+e.message,"error")}if(t.length>0){for(let e=0;e<t.length;e++)this.activeQueue.enqueue(t[e]);this.totalSamples+=t.length}else console.log("没有成功解码的样本","warning");await this.getPendingAudioBufferQueue()}}else console.log("Opus解码器未初始化,无法解码","error")}async startPlaying(){let e=this.audioContext.currentTime;while(1){const t=this.sampleRate*this.minAudioDuration*2;!this.playing&&this.queue.length<t&&await this.getQueue(t),this.playing=!0;while(this.playing&&this.queue.length>0){const t=.12,n=Math.floor(this.sampleRate*t),o=Math.min(this.queue.length,n);if(0===o)break;const s=this.queue.splice(0,o),i=this.audioContext.createBuffer(this.channels,s.length,this.sampleRate);i.copyToChannel(new Float32Array(s),0),this.source=this.audioContext.createBufferSource(),this.source.buffer=i;const r=this.audioContext.currentTime,c=Math.max(e,r);this.source.connect(this.audioContext.destination),this.source.start(c);const a=i.duration;if(e=c+a,this.lastPlayTime=c,this.queue.length<n)break}await this.getQueue(t)}}}function d(e,t,n,o,s){return new u(e,t,n,o,s)}class h{constructor(){this.SAMPLE_RATE=16e3,this.CHANNELS=1,this.FRAME_SIZE=960,this.MIN_AUDIO_DURATION=.12,this.audioContext=null,this.opusDecoder=null,this.streamingContext=null,this.queue=new l,this.isPlaying=!1}getAudioContext(){return this.audioContext||(this.audioContext=new(window.AudioContext||window.webkitAudioContext)({sampleRate:this.SAMPLE_RATE,latencyHint:"interactive"}),console.log("创建音频上下文,采样率: "+this.SAMPLE_RATE+"Hz","debug")),this.audioContext}async initOpusDecoder(){if(this.opusDecoder)return this.opusDecoder;try{if("undefined"===typeof window.ModuleInstance){if("undefined"===typeof Module)throw new Error("Opus库未加载,ModuleInstance和Module对象都不存在");window.ModuleInstance=Module,console.log("使用全局Module作为ModuleInstance","info")}const e=window.ModuleInstance;if(this.opusDecoder={channels:this.CHANNELS,rate:this.SAMPLE_RATE,frameSize:this.FRAME_SIZE,module:e,decoderPtr:null,init:function(){if(this.decoderPtr)return!0;const t=e._opus_decoder_get_size(this.channels);if(console.log(`Opus解码器大小: ${t}字节`,"debug"),this.decoderPtr=e._malloc(t),!this.decoderPtr)throw new Error("无法分配解码器内存");const n=e._opus_decoder_init(this.decoderPtr,this.rate,this.channels);if(n<0)throw this.destroy(),new Error(`Opus解码器初始化失败: ${n}`);return console.log("Opus解码器初始化成功","success"),!0},decode:function(e){if(!this.decoderPtr&&!this.init())throw new Error("解码器未初始化且无法初始化");try{const t=this.module,n=t._malloc(e.length);t.HEAPU8.set(e,n);const o=t._malloc(2*this.frameSize),s=t._opus_decode(this.decoderPtr,n,e.length,o,this.frameSize,0);if(s<0)throw t._free(n),t._free(o),new Error(`Opus解码失败: ${s}`);const i=new Int16Array(s);for(let e=0;e<s;e++)i[e]=t.HEAP16[(o>>1)+e];return t._free(n),t._free(o),i}catch(t){return console.log(`Opus解码错误: ${t.message}`,"error"),new Int16Array(0)}},destroy:function(){this.decoderPtr&&(this.module._free(this.decoderPtr),this.decoderPtr=null)}},!this.opusDecoder.init())throw new Error("Opus解码器初始化失败");return this.opusDecoder}catch(e){throw console.log(`Opus解码器初始化失败: ${e.message}`,"error"),this.opusDecoder=null,e}}async startAudioBuffering(){console.log("开始音频缓冲...","info"),this.initOpusDecoder().catch((e=>{console.log(`预初始化Opus解码器失败: ${e.message}`,"warning")}));const e=400;while(1){const t=await this.queue.dequeue(6,e,(e=>{console.log(`缓冲超时,当前缓冲包数: ${e},开始播放`,"info")}));t.length&&(console.log(`已缓冲 ${t.length} 个音频包,开始播放`,"info"),this.streamingContext.pushAudioBuffer(t));while(1){const e=await this.queue.dequeue(99,30);if(!e.length)break;this.streamingContext.pushAudioBuffer(e)}}}async playBufferedAudio(){try{if(this.audioContext=this.getAudioContext(),!this.opusDecoder){console.log("初始化Opus解码器...","info");try{if(this.opusDecoder=await this.initOpusDecoder(),!this.opusDecoder)throw new Error("解码器初始化失败");console.log("Opus解码器初始化成功","success")}catch(e){return console.log("Opus解码器初始化失败: "+e.message,"error"),void(this.isPlaying=!1)}}this.streamingContext||(this.streamingContext=d(this.opusDecoder,this.audioContext,this.SAMPLE_RATE,this.CHANNELS,this.MIN_AUDIO_DURATION)),this.streamingContext.decodeOpusFrames(),this.streamingContext.startPlaying()}catch(e){console.log(`播放已缓冲的音频出错: ${e.message}`,"error"),this.isPlaying=!1,this.streamingContext=null}}enqueueAudioData(e){e.length>0?this.queue.enqueue(e):(console.log("收到空音频数据帧,可能是结束标志","warning"),this.isPlaying&&this.streamingContext&&(this.streamingContext.endOfStream=!0))}async preload(){console.log("预加载Opus解码器...","info");try{await this.initOpusDecoder(),console.log("Opus解码器预加载成功","success")}catch(e){console.log(`Opus解码器预加载失败: ${e.message},将在需要时重试`,"warning")}}async start(){await this.preload(),this.playBufferedAudio(),this.startAudioBuffering()}}let p=null;function g(){return p||(p=new h),p}let f=[],m=null,y=[],w=null;function b(e){w=e}async function C(){const e=await fetch("/default-mcp-tools.json").then((e=>e.json())),t=localStorage.getItem("mcpTools");if(t)try{f=JSON.parse(t)}catch(n){console.log("加载MCP工具失败,使用默认工具","warning"),f=[...e]}else f=[...e]}function S(){const e=document.getElementById("mcpToolsContainer"),t=document.getElementById("mcpToolsCount");t.textContent=`${f.length} 个工具`,0!==f.length?e.innerHTML=f.map(((e,t)=>{const n=e.inputSchema.properties?Object.keys(e.inputSchema.properties).length:0,o=e.inputSchema.required?e.inputSchema.required.length:0,s=e.mockResponse&&Object.keys(e.mockResponse).length>0;return`\n <div class="mcp-tool-card">\n <div class="mcp-tool-header">\n <div class="mcp-tool-name">${e.name}</div>\n <div class="mcp-tool-actions">\n <button onclick="window.mcpModule.editMcpTool(${t})"\n style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #2196f3; color: white; cursor: pointer; font-size: 12px;">\n ✏️ 编辑\n </button>\n <button onclick="window.mcpModule.deleteMcpTool(${t})"\n style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #f44336; color: white; cursor: pointer; font-size: 12px;">\n 🗑️ 删除\n </button>\n </div>\n </div>\n <div class="mcp-tool-description">${e.description}</div>\n <div class="mcp-tool-info">\n <div class="mcp-tool-info-row">\n <span class="mcp-tool-info-label">参数数量:</span>\n <span class="mcp-tool-info-value">${n} 个 ${o>0?`(${o} 个必填)`:""}</span>\n </div>\n <div class="mcp-tool-info-row">\n <span class="mcp-tool-info-label">模拟返回:</span>\n <span class="mcp-tool-info-value">${s?"✅ 已配置: "+JSON.stringify(e.mockResponse):"⚪ 使用默认"}</span>\n </div>\n </div>\n </div>\n `})).join(""):e.innerHTML='<div style="text-align: center; padding: 30px; color: #999;">暂无工具,点击下方按钮添加新工具</div>'}function v(){const e=document.getElementById("mcpPropertiesContainer");0!==y.length?e.innerHTML=y.map(((e,t)=>`\n <div class="mcp-property-item">\n <div class="mcp-property-header">\n <span class="mcp-property-name">${e.name}</span>\n <button type="button" onclick="window.mcpModule.deleteMcpProperty(${t})"\n style="padding: 3px 8px; border: none; border-radius: 3px; background-color: #f44336; color: white; cursor: pointer; font-size: 11px;">\n 删除\n </button>\n </div>\n <div class="mcp-property-row">\n <div>\n <label class="mcp-small-label">参数名称 *</label>\n <input type="text" class="mcp-small-input" value="${e.name}"\n onchange="window.mcpModule.updateMcpProperty(${t}, 'name', this.value)" required>\n </div>\n <div>\n <label class="mcp-small-label">数据类型 *</label>\n <select class="mcp-small-input" onchange="window.mcpModule.updateMcpProperty(${t}, 'type', this.value)">\n <option value="string" ${"string"===e.type?"selected":""}>字符串</option>\n <option value="integer" ${"integer"===e.type?"selected":""}>整数</option>\n <option value="number" ${"number"===e.type?"selected":""}>数字</option>\n <option value="boolean" ${"boolean"===e.type?"selected":""}>布尔值</option>\n <option value="array" ${"array"===e.type?"selected":""}>数组</option>\n <option value="object" ${"object"===e.type?"selected":""}>对象</option>\n </select>\n </div>\n </div>\n ${"integer"===e.type||"number"===e.type?`\n <div class="mcp-property-row">\n <div>\n <label class="mcp-small-label">最小值</label>\n <input type="number" class="mcp-small-input" value="${void 0!==e.minimum?e.minimum:""}"\n placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${t}, 'minimum', this.value ? parseFloat(this.value) : undefined)">\n </div>\n <div>\n <label class="mcp-small-label">最大值</label>\n <input type="number" class="mcp-small-input" value="${void 0!==e.maximum?e.maximum:""}"\n placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${t}, 'maximum', this.value ? parseFloat(this.value) : undefined)">\n </div>\n </div>\n `:""}\n <div class="mcp-property-row-full">\n <label class="mcp-small-label">参数描述</label>\n <input type="text" class="mcp-small-input" value="${e.description||""}"\n placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${t}, 'description', this.value)">\n </div>\n <label class="mcp-checkbox-label">\n <input type="checkbox" ${e.required?"checked":""}\n onchange="window.mcpModule.updateMcpProperty(${t}, 'required', this.checked)">\n 必填参数\n </label>\n </div>\n `)).join(""):e.innerHTML='<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>'}function x(e,t,n){if("name"===t){const t=y.some(((t,o)=>o!==e&&t.name===n));if(t)return alert("参数名称已存在,请使用不同的名称"),void v()}y[e][t]=n,"type"===t&&"integer"!==n&&"number"!==n&&(delete y[e].minimum,delete y[e].maximum,v())}function M(e){y.splice(e,1),v()}function k(e=null){const t=w&&w.readyState===WebSocket.OPEN;if(t)return void alert("WebSocket 已连接,无法编辑工具");m=e;const n=document.getElementById("mcpErrorContainer");if(n.innerHTML="",null!==e){document.getElementById("mcpModalTitle").textContent="编辑工具";const t=f[e];document.getElementById("mcpToolName").value=t.name,document.getElementById("mcpToolDescription").value=t.description,document.getElementById("mcpMockResponse").value=t.mockResponse?JSON.stringify(t.mockResponse,null,2):"",y=[];const n=t.inputSchema;n.properties&&Object.keys(n.properties).forEach((e=>{const t=n.properties[e];y.push({name:e,type:t.type||"string",minimum:t.minimum,maximum:t.maximum,description:t.description||"",required:n.required&&n.required.includes(e)})}))}else document.getElementById("mcpModalTitle").textContent="添加工具",document.getElementById("mcpToolForm").reset(),y=[];v(),document.getElementById("mcpToolModal").style.display="block"}function _(e){k(e)}function P(e){const t=w&&w.readyState===WebSocket.OPEN;if(t)alert("WebSocket 已连接,无法编辑工具");else if(confirm(`确定要删除工具 "${f[e].name}" 吗?`)){const t=f[e].name;f.splice(e,1),E(),S(),console.log(`已删除工具: ${t}`,"info")}}function E(){localStorage.setItem("mcpTools",JSON.stringify(f))}function R(){return f.map((e=>({name:e.name,description:e.description,inputSchema:e.inputSchema})))}function O(e,t){const n=f.find((t=>t.name===e));if(!n)return console.log(`未找到工具: ${e}`,"error"),{success:!1,error:`未知工具: ${e}`};if("self.drink_car_list"==n.name){console.log("准备触发 gotoOrderEvent 事件");const t=new CustomEvent("gotoOrderEvent");return window.dispatchEvent(t),{success:!0,message:`工具 ${e} 执行成功`,data:sessionStorage.getItem("cartList")||[]}}if("self.drink_car_reset"==n.name){console.log("准备触发 resetOrderEvent 事件");const n=new CustomEvent("resetOrderEvent",{detail:t});return window.dispatchEvent(n),{success:!0,message:`工具 ${e} 执行成功`,data:sessionStorage.getItem("cartList")||[]}}if("self.drink_order"==n.name){console.log("准备触发 orderEvent 事件");const e=new CustomEvent("orderEvent");window.dispatchEvent(e)}}window.mcpModule={updateMcpProperty:x,deleteMcpProperty:M,editMcpTool:_,deleteMcpTool:P};class ${constructor(){this.isRecording=!1,this.audioContext=null,this.analyser=null,this.audioProcessor=null,this.audioProcessorType=null,this.audioSource=null,this.opusEncoder=null,this.pcmDataBuffer=new Int16Array,this.audioBuffers=[],this.totalAudioSize=0,this.visualizationRequest=null,this.recordingTimer=null,this.websocket=null,this.onRecordingStart=null,this.onRecordingStop=null,this.onVisualizerUpdate=null}setWebSocket(e){this.websocket=e}getAudioContext(){const e=g();return e.getAudioContext()}initEncoder(){return this.opusEncoder||(this.opusEncoder=a()),this.opusEncoder}getAudioProcessorCode(){return"\n class AudioRecorderProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n this.buffers = [];\n this.frameSize = 960;\n this.buffer = new Int16Array(this.frameSize);\n this.bufferIndex = 0;\n this.isRecording = false;\n\n this.port.onmessage = (event) => {\n if (event.data.command === 'start') {\n this.isRecording = true;\n this.port.postMessage({ type: 'status', status: 'started' });\n } else if (event.data.command === 'stop') {\n this.isRecording = false;\n\n if (this.bufferIndex > 0) {\n const finalBuffer = this.buffer.slice(0, this.bufferIndex);\n this.port.postMessage({\n type: 'buffer',\n buffer: finalBuffer\n });\n this.bufferIndex = 0;\n }\n\n this.port.postMessage({ type: 'status', status: 'stopped' });\n }\n };\n }\n\n process(inputs, outputs, parameters) {\n if (!this.isRecording) return true;\n\n const input = inputs[0][0];\n if (!input) return true;\n\n for (let i = 0; i < input.length; i++) {\n if (this.bufferIndex >= this.frameSize) {\n this.port.postMessage({\n type: 'buffer',\n buffer: this.buffer.slice(0)\n });\n this.bufferIndex = 0;\n }\n\n this.buffer[this.bufferIndex++] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767)));\n }\n\n return true;\n }\n }\n\n registerProcessor('audio-recorder-processor', AudioRecorderProcessor);\n "}async createAudioProcessor(){this.audioContext=this.getAudioContext();try{if(this.audioContext.audioWorklet){const e=new Blob([this.getAudioProcessorCode()],{type:"application/javascript"}),t=URL.createObjectURL(e);await this.audioContext.audioWorklet.addModule(t),URL.revokeObjectURL(t);const n=new AudioWorkletNode(this.audioContext,"audio-recorder-processor");n.port.onmessage=e=>{"buffer"===e.data.type&&this.processPCMBuffer(e.data.buffer)},console.log("使用AudioWorklet处理音频","success");const o=this.audioContext.createGain();return o.gain.value=0,n.connect(o),o.connect(this.audioContext.destination),{node:n,type:"worklet"}}return console.log("AudioWorklet不可用,使用ScriptProcessorNode作为回退方案","warning"),this.createScriptProcessor()}catch(e){return console.log(`创建音频处理器失败: ${e.message},尝试回退方案`,"error"),this.createScriptProcessor()}}createScriptProcessor(){try{const e=4096,t=this.audioContext.createScriptProcessor(e,1,1);t.onaudioprocess=e=>{if(!this.isRecording)return;const t=e.inputBuffer.getChannelData(0),n=new Int16Array(t.length);for(let o=0;o<t.length;o++)n[o]=Math.max(-32768,Math.min(32767,Math.floor(32767*t[o])));this.processPCMBuffer(n)};const n=this.audioContext.createGain();return n.gain.value=0,t.connect(n),n.connect(this.audioContext.destination),console.log("使用ScriptProcessorNode作为回退方案成功","warning"),{node:t,type:"processor"}}catch(e){return console.log(`回退方案也失败: ${e.message}`,"error"),null}}processPCMBuffer(e){if(!this.isRecording)return;const t=new Int16Array(this.pcmDataBuffer.length+e.length);t.set(this.pcmDataBuffer),t.set(e,this.pcmDataBuffer.length),this.pcmDataBuffer=t;const n=960;while(this.pcmDataBuffer.length>=n){const e=this.pcmDataBuffer.slice(0,n);this.pcmDataBuffer=this.pcmDataBuffer.slice(n),this.encodeAndSendOpus(e)}}encodeAndSendOpus(e=null){if(this.opusEncoder)try{if(e){const n=this.opusEncoder.encode(e);if(n&&n.length>0){if(this.audioBuffers.push(n.buffer),this.totalAudioSize+=n.length,this.websocket&&this.websocket.readyState===WebSocket.OPEN)try{this.websocket.send(n.buffer)}catch(t){console.log(`WebSocket发送错误: ${t.message}`,"error")}}else log("Opus编码失败,无有效数据返回","error")}else if(this.pcmDataBuffer.length>0){const e=960;if(this.pcmDataBuffer.length<e){const t=new Int16Array(e);t.set(this.pcmDataBuffer),this.encodeAndSendOpus(t)}else this.encodeAndSendOpus(this.pcmDataBuffer.slice(0,e));this.pcmDataBuffer=new Int16Array(0)}}catch(t){console.log(`Opus编码错误: ${t.message}`,"error")}else console.log("Opus编码器未初始化","error")}async start(){try{if(!this.initEncoder())return console.log("无法启动录音: Opus编码器初始化失败","error"),!1;const e=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,sampleRate:16e3,channelCount:1,latency:{ideal:.02,max:.05},googNoiseSuppression:!0,googNoiseSuppression2:3,googAutoGainControl:!0,googHighpassFilter:!0}});this.audioContext=this.getAudioContext(),"suspended"===this.audioContext.state&&await this.audioContext.resume();const t=await this.createAudioProcessor();if(!t)return console.log("无法创建音频处理器","error"),!1;if(this.audioProcessor=t.node,this.audioProcessorType=t.type,this.audioSource=this.audioContext.createMediaStreamSource(e),this.analyser=this.audioContext.createAnalyser(),this.analyser.fftSize=2048,this.audioSource.connect(this.analyser),this.audioSource.connect(this.audioProcessor),this.pcmDataBuffer=new Int16Array,this.audioBuffers=[],this.totalAudioSize=0,this.isRecording=!0,"worklet"===this.audioProcessorType&&this.audioProcessor.port&&this.audioProcessor.port.postMessage({command:"start"}),!this.websocket||this.websocket.readyState!==WebSocket.OPEN)return console.log("WebSocket未连接,无法发送开始消息","error"),!1;{const e={type:"listen",mode:localStorage.getItem("listenMode")||"wakeup",state:"start"};console.log(`发送录音开始消息: ${JSON.stringify(e)}`,"info"),this.websocket.send(JSON.stringify(e))}let n=0;return this.recordingTimer=setInterval((()=>{n+=.1,this.onRecordingStart&&this.onRecordingStart(n)}),100),console.log("开始PCM直接录音","success"),!0}catch(e){return console.log(`直接录音启动错误: ${e.message}`,"error"),this.isRecording=!1,!1}}stop(){if(!this.isRecording)return!1;try{if(this.isRecording=!1,this.audioProcessor&&("worklet"===this.audioProcessorType&&this.audioProcessor.port&&this.audioProcessor.port.postMessage({command:"stop"}),this.audioProcessor.disconnect(),this.audioProcessor=null),this.audioSource&&(this.audioSource.disconnect(),this.audioSource=null),this.visualizationRequest&&(cancelAnimationFrame(this.visualizationRequest),this.visualizationRequest=null),this.recordingTimer&&(clearInterval(this.recordingTimer),this.recordingTimer=null),this.encodeAndSendOpus(),this.websocket&&this.websocket.readyState===WebSocket.OPEN){const e=new Uint8Array(0);this.websocket.send(e);const t={type:"listen",mode:localStorage.getItem("listenMode")||"wakeup",state:"stop"};this.websocket.send(JSON.stringify(t)),console.log("已发送录音停止信号","info")}return this.onRecordingStop&&this.onRecordingStop(),console.log("停止PCM直接录音","success"),!0}catch(e){return console.log(`直接录音停止错误: ${e.message}`,"error"),!1}}getAnalyser(){return this.analyser}}let A=null;function I(){return A||(A=new $),A}async function T(e,t){if(!B(t))return;const n=await D(e,t);if(!n)return void console.log("无法从OTA服务器获取信息","error");const{websocket:o}=n;if(!o||!o.url)return void console.log("OTA响应中缺少websocket信息","error");let s=new URL(o.url);o.token&&(o.token.startsWith("Bearer ")?s.searchParams.append("authorization",o.token):s.searchParams.append("authorization","Bearer "+o.token)),s.searchParams.append("device-id",t.deviceId),s.searchParams.append("client-id",t.clientId);const i=s.toString();return console.log(`正在连接: ${i}`,"info"),new WebSocket(s.toString())}function B(e){return e.deviceMac?!!e.clientId||(console.log("客户端ID不能为空","error"),!1):(console.log("设备MAC地址不能为空","error"),!1)}async function D(e,t){try{const n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","Device-Id":t.deviceId,"Client-Id":t.clientId},body:JSON.stringify({version:0,uuid:"",application:{name:"xiaozhi-web-test",version:"1.0.0",compile_time:"2025-04-16 10:00:00",idf_version:"4.4.3",elf_sha256:"1234567890abcdef1234567890abcdef1234567890abcdef"},ota:{label:"xiaozhi-web-test"},board:{type:"xiaozhi-web-test",ssid:"xiaozhi-web-test",rssi:0,channel:0,ip:"192.168.1.1",mac:t.deviceMac},flash_size:0,minimum_free_heap_size:0,mac_address:t.deviceMac,chip_model_name:"",chip_info:{model:0,cores:0,revision:0,features:0},partition_table:[{label:"",type:0,subtype:0,address:0,size:0}]})});if(!n.ok)throw new Error(`${n.status} ${n.statusText}`);const o=await n.json();return o}catch(n){return null}}function N(){return{deviceId:localStorage.getItem("MAC"),deviceName:"测试设备",deviceMac:localStorage.getItem("MAC"),clientId:"web_test_client",token:"your-token1"}}function z(){const e=localStorage.getItem("otaUrl");localStorage.setItem("xz_tester_otaUrl",e)}class q{constructor(){this.websocket=null,this.onConnectionStateChange=null,this.onRecordButtonStateChange=null,this.onSessionStateChange=null,this.onSessionEmotionChange=null,this.currentSessionId=null,this.isRemoteSpeaking=!1,this.heartbeatTimer=null,this.reconnectConfig={maxRetries:10,baseDelay:1e3,maxDelay:3e4,retryCount:0,reconnectTimer:null,isReconnecting:!1,manualDisconnect:!1}}startHeartbeat(){this.stopHeartbeat(),this.sendHeartbeat()}stopHeartbeat(){this.heartbeatTimer&&(clearTimeout(this.heartbeatTimer),this.heartbeatTimer=null)}sendHeartbeat(){if(this.websocket?.readyState===WebSocket.OPEN)try{this.websocket.send("1")}catch(e){console.error("心跳发送失败:",e)}this.heartbeatTimer=setTimeout((()=>{this.sendHeartbeat()}),5e3)}async sendHelloMessage(){if(!this.websocket||this.websocket.readyState!==WebSocket.OPEN)return!1;this.startHeartbeat();try{const e=N(),t={type:"hello",device_id:e.deviceId,device_name:e.deviceName,device_mac:e.deviceMac,token:e.token,seat:localStorage.getItem("SEAT"),features:{mcp:!0}};return console.log("发送hello握手消息","info"),this.websocket.send(JSON.stringify(t)),new Promise((e=>{const t=setTimeout((()=>{console.log("等待hello响应超时","error"),console.log('提示: 请尝试点击"测试认证"按钮进行连接排查',"info"),e(!1)}),5e3),n=o=>{try{const s=JSON.parse(o.data);"hello"===s.type&&s.session_id&&(console.log(`服务器握手成功,会话ID: ${s.session_id}`,"success"),this.reconnectConfig.retryCount=0,clearTimeout(t),this.websocket.removeEventListener("message",n),e(!0))}catch(s){}};this.websocket.addEventListener("message",n)}))}catch(e){return console.log(`发送hello消息错误: ${e.message}`,"error"),!1}}handleTextMessage(e){if("hello"===e.type);else if("tts"===e.type)this.handleTTSMessage(e);else if("audio"===e.type);else if("stt"===e.type){const t=new CustomEvent("wsSendMessage",{detail:e});window.dispatchEvent(t)}else if("llm"===e.type);else if("mcp"===e.type)this.handleMCPMessage(e);else if("json_data"===e.type){if("drinks"===e.state){const t=new CustomEvent("drinkListEvent",{detail:e.data});window.dispatchEvent(t)}else if("book"===e.state){const t=new CustomEvent("bookListEvent",{detail:e});window.dispatchEvent(t)}}else if("view_action"===e.type){const t=new CustomEvent("viewActionEvent",{detail:e.state});window.dispatchEvent(t)}else console.log(`未知消息类型: ${e.type}`,"warning")}handleTTSMessage(e){if("start"===e.state){console.log("服务器开始发送语音","info"),this.currentSessionId=e.session_id;const t=new CustomEvent("startThink");window.dispatchEvent(t),this.isRemoteSpeaking=!0,this.onSessionStateChange&&this.onSessionStateChange(!0)}else if("sentence_start"===e.state){const t=new CustomEvent("startVolic",{detail:e.text});window.dispatchEvent(t),console.log(`服务器发送语音段: ${e.text}`,"info")}else if("sentence_end"===e.state)console.log(`语音段结束: ${e.text}`,"info");else if("stop"===e.state){const e=new CustomEvent("stopVolic");window.dispatchEvent(e),console.log("服务器语音传输结束","info"),this.isRemoteSpeaking=!1,this.onRecordButtonStateChange&&this.onRecordButtonStateChange(!1),this.onSessionStateChange&&this.onSessionStateChange(!1)}}handleMCPMessage(e){const t=e.payload||{};if(console.log(`服务器下发: ${JSON.stringify(e)}`,"info"),"tools/list"===t.method){const n=R(),o=JSON.stringify({session_id:e.session_id||"",type:"mcp",payload:{jsonrpc:"2.0",id:t.id,result:{tools:n}}});console.log(`客户端上报: ${o}`,"info"),this.websocket.send(o),console.log(`回复MCP工具列表: ${n.length} 个工具`,"info")}else if("tools/call"===t.method){const n=t.params?.name,o=t.params?.arguments;console.log(`调用工具: ${n} 参数: ${JSON.stringify(o)}`,"info");const s=O(n,o),i=JSON.stringify({session_id:e.session_id||"",type:"mcp",payload:{jsonrpc:"2.0",id:t.id,result:{content:[{type:"text",text:JSON.stringify(s)}],isError:!1}}});console.log(`客户端上报: ${i}`,"info"),this.websocket.send(i)}else"initialize"===t.method?console.log(`收到工具初始化请求: ${JSON.stringify(t.params)}`,"info"):console.log(`未知的MCP方法: ${t.method}`,"warning")}async handleBinaryMessage(e){try{let t;if(e instanceof ArrayBuffer)t=e;else{if(!(e instanceof Blob))return void console.log("收到未知类型的二进制数据: "+typeof e,"warning");t=await e.arrayBuffer(),console.log(`收到Blob音频数据,大小: ${t.byteLength}字节`,"debug")}const n=new Uint8Array(t),o=g();o.enqueueAudioData(n)}catch(t){console.log(`处理二进制消息出错: ${t.message}`,"error")}}calculateReconnectDelay(){const e=Math.min(this.reconnectConfig.baseDelay*Math.pow(2,this.reconnectConfig.retryCount),this.reconnectConfig.maxDelay),t=.2*e*(Math.random()-.5);return Math.round(e+t)}triggerReconnect(){if(this.reconnectConfig.manualDisconnect)return void console.log("手动断开连接,不进行自动重连","info");if(this.reconnectConfig.retryCount>=this.reconnectConfig.maxRetries)return console.log(`已达到最大重连次数(${this.reconnectConfig.maxRetries}),停止重连`,"error"),this.reconnectConfig.isReconnecting=!1,void(this.onConnectionStateChange&&this.onConnectionStateChange(!1));const e=this.calculateReconnectDelay();this.reconnectConfig.retryCount++,console.log(`准备进行第${this.reconnectConfig.retryCount}次重连,延迟${e}ms`,"info"),this.reconnectConfig.reconnectTimer=setTimeout((async()=>{console.log(`开始第${this.reconnectConfig.retryCount}次重连`,"info");try{const e=await this.connect();e?(console.log("重连成功","success"),this.reconnectConfig.isReconnecting=!1):(console.log(`第${this.reconnectConfig.retryCount}次重连失败`,"error"),this.triggerReconnect())}catch(e){console.log(`重连出错: ${e.message}`,"error"),this.triggerReconnect()}}),e)}stopReconnect(){this.reconnectConfig.reconnectTimer&&(clearTimeout(this.reconnectConfig.reconnectTimer),this.reconnectConfig.reconnectTimer=null),this.reconnectConfig.isReconnecting=!1,this.reconnectConfig.retryCount=0}async connect(){this.stopReconnect();const e=N();console.log("正在检查OTA状态...","info"),z();try{const t=localStorage.getItem("xz_tester_otaUrl"),n=await T(t,e);if(void 0===n)return this.reconnectConfig.isReconnecting||this.reconnectConfig.manualDisconnect||(this.reconnectConfig.isReconnecting=!0,this.triggerReconnect()),!1;this.websocket=n,this.websocket.binaryType="arraybuffer",b(this.websocket);const o=I();return o.setWebSocket(this.websocket),this.setupEventHandlers(),!0}catch(t){return console.log(`连接错误: ${t.message}`,"error"),this.onConnectionStateChange&&this.onConnectionStateChange(!1),this.reconnectConfig.isReconnecting||this.reconnectConfig.manualDisconnect||(this.reconnectConfig.isReconnecting=!0,this.triggerReconnect()),!1}}setupEventHandlers(){this.websocket.onopen=async()=>{const e=localStorage.getItem("xz_tester_wsUrl");console.log(`已连接到服务器: ${e}`,"success"),this.onConnectionStateChange&&this.onConnectionStateChange(!0),this.isRemoteSpeaking=!1,this.onSessionStateChange&&this.onSessionStateChange(!1),await this.sendHelloMessage()},this.websocket.onclose=e=>{console.log(`已断开连接,代码: ${e.code}, 原因: ${e.reason}`,"info"),this.stopHeartbeat(),b(null);const t=I();t.stop(),this.onConnectionStateChange&&this.onConnectionStateChange(!1),this.reconnectConfig.manualDisconnect||this.reconnectConfig.isReconnecting||1e3!==e.code&&1001!==e.code&&(console.log("检测到异常断开连接,准备自动重连","warning"),this.reconnectConfig.isReconnecting=!0,this.triggerReconnect()),this.reconnectConfig.manualDisconnect=!1},this.websocket.onerror=e=>{console.log(`WebSocket错误: ${e.message||"未知错误"}`,"error"),this.onConnectionStateChange&&this.onConnectionStateChange(!1)},this.websocket.onmessage=e=>{try{if("string"===typeof e.data){const t=JSON.parse(e.data);this.handleTextMessage(t)}else this.handleBinaryMessage(e.data)}catch(t){console.log(`WebSocket消息处理错误: ${t.message}`,"error")}}}disconnect(){if(this.reconnectConfig.manualDisconnect=!0,this.stopReconnect(),!this.websocket)return;this.websocket.close(1e3,"Manual disconnect");const e=I();e.stop()}sendTextMessage(e){try{const t={session_id:this.currentSessionId,type:"abort",reason:"wake_word_detected"};this.websocket.send(JSON.stringify(t)),console.log("发送打断消息","info");const n={type:"listen",mode:localStorage.getItem("listenMode")||"wakeup",state:"detect",text:e};return this.websocket.send(JSON.stringify(n)),console.log(`发送文本消息: ${e}`,"info6666"),!0}catch(t){return console.log(`发送消息错误: ${t.message}`,"error"),!1}}getWebSocket(){return this.websocket}isConnected(){return this.websocket&&this.websocket.readyState===WebSocket.OPEN}getReconnectStatus(){return{isReconnecting:this.reconnectConfig.isReconnecting,retryCount:this.reconnectConfig.retryCount,maxRetries:this.reconnectConfig.maxRetries}}resetReconnectConfig(){this.stopReconnect(),this.reconnectConfig.retryCount=0,this.reconnectConfig.isReconnecting=!1,this.reconnectConfig.manualDisconnect=!1}}let W=null;function H(){return W||(W=new q),W}async function j(){const e=H();await e.connect()}async function L(){const e=H();await e.disconnect()}async function U(){const e=I();await e.start()}function F(){const e=H();return e.isConnected()}const J={name:"ibiAiTalk",data(){return{audioPlayer:null}},props:{listenMode:{type:String,default:"wakeup"},otaUrl:{type:String,default:""},macAddress:{type:String,default:""}},async mounted(){localStorage.setItem("MAC",this.macAddress),localStorage.setItem("otaUrl",this.otaUrl),localStorage.setItem("listenMode",this.listenMode),r(),a(),C(),F()||await j(),this.audioPlayer=g(),await this.audioPlayer.start(),await U()},beforeDestroy(){this.audioPlayer.stop().catch((()=>{})),F()&&L().catch((()=>{}))}},Q=J;function V(e,t,n,o,s,i,r,c){var a,l="function"===typeof e?e.options:e;if(t&&(l.render=t,l.staticRenderFns=n,l._compiled=!0),o&&(l.functional=!0),i&&(l._scopeId="data-v-"+i),r?(a=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),s&&s.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(r)},l._ssrRegister=a):s&&(a=c?function(){s.call(this,(l.functional?this.parent:this).$root.$options.shadowRoot)}:s),a)if(l.functional){l._injectStyles=a;var u=l.render;l.render=function(e,t){return a.call(t),u(e,t)}}else{var d=l.beforeCreate;l.beforeCreate=d?[].concat(d,a):[a]}return{exports:e,options:l}}var G=V(Q,s,i,!1,null,null,null);const X=G.exports,Z=X;return t=t["default"],t})()));
|
|
1
|
+
(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t():"function"===typeof define&&define.amd?define([],t):"object"===typeof exports?exports["index"]=t():e["index"]=t()})("undefined"!==typeof self?self:this,(()=>(()=>{"use strict";var e={};(()=>{e.d=(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})}})(),(()=>{e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})(),(()=>{e.p=""})();var t={};if(e.d(t,{default:()=>Z}),"undefined"!==typeof window){var n=window.document.currentScript,o=n&&n.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);o&&(e.p=o[1])}var s=function(){var e=this,t=e._self._c;return t("div")},i=[];function r(){try{if("undefined"===typeof Module)throw new Error("Opus库未加载,Module对象不存在");throw"undefined"!==typeof Module.instance&&"function"===typeof Module.instance._opus_decoder_get_size&&(window.ModuleInstance=Module.instance,console.log("Opus库加载成功(使用Module.instance)","success")),"function"===typeof Module._opus_decoder_get_size&&(window.ModuleInstance=Module,console.log("Opus库加载成功(使用全局Module)","success")),new Error("Opus解码函数未找到,可能Module结构不正确")}catch(e){console.log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${e.message}`,"error")}}let c=null;function a(){try{if(c)return c;if(!window.ModuleInstance)return void console.log("无法创建Opus编码器:ModuleInstance不可用","error");const e=window.ModuleInstance,t=16e3,n=1,o=2048;return c={channels:n,sampleRate:t,frameSize:960,maxPacketSize:4e3,module:e,init:function(){try{const t=e._opus_encoder_get_size(this.channels);if(console.log(`Opus编码器大小: ${t}字节`,"info"),this.encoderPtr=e._malloc(t),!this.encoderPtr)throw new Error("无法分配编码器内存");const n=e._opus_encoder_init(this.encoderPtr,this.sampleRate,this.channels,o);if(n<0)throw new Error(`Opus编码器初始化失败: ${n}`);return e._opus_encoder_ctl(this.encoderPtr,4002,16e3),e._opus_encoder_ctl(this.encoderPtr,4010,5),e._opus_encoder_ctl(this.encoderPtr,4016,1),console.log("Opus编码器初始化成功","success"),!0}catch(t){return this.encoderPtr&&(e._free(this.encoderPtr),this.encoderPtr=null),console.log(`Opus编码器初始化失败: ${t.message}`,"error"),!1}},encode:function(e){if(!this.encoderPtr&&!this.init())return null;try{const t=this.module,n=t._malloc(2*e.length);for(let r=0;r<e.length;r++)t.HEAP16[(n>>1)+r]=e[r];const o=t._malloc(this.maxPacketSize),s=t._opus_encode(this.encoderPtr,n,this.frameSize,o,this.maxPacketSize);if(s<0)throw new Error(`Opus编码失败: ${s}`);const i=new Uint8Array(s);for(let e=0;e<s;e++)i[e]=t.HEAPU8[o+e];return t._free(n),t._free(o),i}catch(t){return console.log(`Opus编码出错: ${t.message}`,"error"),null}},destroy:function(){this.encoderPtr&&(this.module._free(this.encoderPtr),this.encoderPtr=null)}},c.init(),c}catch(e){return console.log(`创建Opus编码器失败: ${e.message}`,"error"),!1}}class l{#e=[];#t=[];#n=null;#o=null;enqueue(e,...t){if(0===t.length)this.#e.push(e);else{const n=[e,...t].filter((e=>e));if(0===n.length)return;this.#e.push(...n)}this.#o&&(this.#o(),this.#o=null,this.#n=null),this.#s()}async dequeue(e=1,t=1/0,n=null){return 0===this.#e.length&&await this.#i(),this.#e.length>=e?this.#r():new Promise(((o,s)=>{let i=null;const r={resolve:o,reject:s,min:e,onTimeout:n,timer:i};Number.isFinite(t)&&(r.timer=setTimeout((()=>{this.#c(r),n&&n(this.#e.length),o(this.#r())}),t)),this.#t.push(r)}))}#i(){return this.#n||(this.#n=new Promise((e=>this.#o=e))),this.#n}#s(){for(let e=this.#t.length-1;e>=0;e--){const t=this.#t[e];this.#e.length>=t.min&&(this.#c(t),t.resolve(this.#r()))}}#c(e){const t=this.#t.indexOf(e);-1!==t&&(this.#t.splice(t,1),e.timer&&clearTimeout(e.timer))}#r(){const e=[...this.#e];return this.#e.length=0,e}get length(){return this.#e.length}}class u{constructor(e,t,n,o,s){this.opusDecoder=e,this.audioContext=t,this.sampleRate=n,this.channels=o,this.minAudioDuration=s,this.queue=[],this.activeQueue=new l,this.pendingAudioBufferQueue=[],this.audioBufferQueue=new l,this.playing=!1,this.endOfStream=!1,this.source=null,this.totalSamples=0,this.lastPlayTime=0}pushAudioBuffer(e){this.audioBufferQueue.enqueue(...e)}async getPendingAudioBufferQueue(){[this.pendingAudioBufferQueue,this.audioBufferQueue]=[await this.audioBufferQueue.dequeue(),new l]}async getQueue(e){let t=[];const n=e-this.queue.length>0?e-this.queue.length:1;[t,this.activeQueue]=[await this.activeQueue.dequeue(n),new l],this.queue.push(...t)}convertInt16ToFloat32(e){const t=new Float32Array(e.length);for(let n=0;n<e.length;n++)t[n]=e[n]/32768;return t}async decodeOpusFrames(){if(this.opusDecoder){console.log("Opus解码器启动","info");while(1){let t=[];for(const n of this.pendingAudioBufferQueue)try{const e=this.opusDecoder.decode(n);if(e&&e.length>0){const n=this.convertInt16ToFloat32(e);for(let e=0;e<n.length;e++)t.push(n[e])}}catch(e){console.log("Opus解码失败: "+e.message,"error")}if(t.length>0){for(let e=0;e<t.length;e++)this.activeQueue.enqueue(t[e]);this.totalSamples+=t.length}else console.log("没有成功解码的样本","warning");await this.getPendingAudioBufferQueue()}}else console.log("Opus解码器未初始化,无法解码","error")}async startPlaying(){let e=this.audioContext.currentTime;while(1){const t=this.sampleRate*this.minAudioDuration*2;!this.playing&&this.queue.length<t&&await this.getQueue(t),this.playing=!0;while(this.playing&&this.queue.length>0){const t=.12,n=Math.floor(this.sampleRate*t),o=Math.min(this.queue.length,n);if(0===o)break;const s=this.queue.splice(0,o),i=this.audioContext.createBuffer(this.channels,s.length,this.sampleRate);i.copyToChannel(new Float32Array(s),0),this.source=this.audioContext.createBufferSource(),this.source.buffer=i;const r=this.audioContext.currentTime,c=Math.max(e,r);this.source.connect(this.audioContext.destination),this.source.start(c);const a=i.duration;if(e=c+a,this.lastPlayTime=c,this.queue.length<n)break}await this.getQueue(t)}}}function d(e,t,n,o,s){return new u(e,t,n,o,s)}class h{constructor(){this.SAMPLE_RATE=16e3,this.CHANNELS=1,this.FRAME_SIZE=960,this.MIN_AUDIO_DURATION=.12,this.audioContext=null,this.opusDecoder=null,this.streamingContext=null,this.queue=new l,this.isPlaying=!1}getAudioContext(){return this.audioContext||(this.audioContext=new(window.AudioContext||window.webkitAudioContext)({sampleRate:this.SAMPLE_RATE,latencyHint:"interactive"}),console.log("创建音频上下文,采样率: "+this.SAMPLE_RATE+"Hz","debug")),this.audioContext}async initOpusDecoder(){if(this.opusDecoder)return this.opusDecoder;try{if("undefined"===typeof window.ModuleInstance){if("undefined"===typeof Module)throw new Error("Opus库未加载,ModuleInstance和Module对象都不存在");window.ModuleInstance=Module,console.log("使用全局Module作为ModuleInstance","info")}const e=window.ModuleInstance;if(this.opusDecoder={channels:this.CHANNELS,rate:this.SAMPLE_RATE,frameSize:this.FRAME_SIZE,module:e,decoderPtr:null,init:function(){if(this.decoderPtr)return!0;const t=e._opus_decoder_get_size(this.channels);if(console.log(`Opus解码器大小: ${t}字节`,"debug"),this.decoderPtr=e._malloc(t),!this.decoderPtr)throw new Error("无法分配解码器内存");const n=e._opus_decoder_init(this.decoderPtr,this.rate,this.channels);if(n<0)throw this.destroy(),new Error(`Opus解码器初始化失败: ${n}`);return console.log("Opus解码器初始化成功","success"),!0},decode:function(e){if(!this.decoderPtr&&!this.init())throw new Error("解码器未初始化且无法初始化");try{const t=this.module,n=t._malloc(e.length);t.HEAPU8.set(e,n);const o=t._malloc(2*this.frameSize),s=t._opus_decode(this.decoderPtr,n,e.length,o,this.frameSize,0);if(s<0)throw t._free(n),t._free(o),new Error(`Opus解码失败: ${s}`);const i=new Int16Array(s);for(let e=0;e<s;e++)i[e]=t.HEAP16[(o>>1)+e];return t._free(n),t._free(o),i}catch(t){return console.log(`Opus解码错误: ${t.message}`,"error"),new Int16Array(0)}},destroy:function(){this.decoderPtr&&(this.module._free(this.decoderPtr),this.decoderPtr=null)}},!this.opusDecoder.init())throw new Error("Opus解码器初始化失败");return this.opusDecoder}catch(e){throw console.log(`Opus解码器初始化失败: ${e.message}`,"error"),this.opusDecoder=null,e}}async startAudioBuffering(){console.log("开始音频缓冲...","info"),this.initOpusDecoder().catch((e=>{console.log(`预初始化Opus解码器失败: ${e.message}`,"warning")}));const e=400;while(1){const t=await this.queue.dequeue(6,e,(e=>{console.log(`缓冲超时,当前缓冲包数: ${e},开始播放`,"info")}));t.length&&(console.log(`已缓冲 ${t.length} 个音频包,开始播放`,"info"),this.streamingContext.pushAudioBuffer(t));while(1){const e=await this.queue.dequeue(99,30);if(!e.length)break;this.streamingContext.pushAudioBuffer(e)}}}async playBufferedAudio(){try{if(this.audioContext=this.getAudioContext(),!this.opusDecoder){console.log("初始化Opus解码器...","info");try{if(this.opusDecoder=await this.initOpusDecoder(),!this.opusDecoder)throw new Error("解码器初始化失败");console.log("Opus解码器初始化成功","success")}catch(e){return console.log("Opus解码器初始化失败: "+e.message,"error"),void(this.isPlaying=!1)}}this.streamingContext||(this.streamingContext=d(this.opusDecoder,this.audioContext,this.SAMPLE_RATE,this.CHANNELS,this.MIN_AUDIO_DURATION)),this.streamingContext.decodeOpusFrames(),this.streamingContext.startPlaying()}catch(e){console.log(`播放已缓冲的音频出错: ${e.message}`,"error"),this.isPlaying=!1,this.streamingContext=null}}enqueueAudioData(e){e.length>0?this.queue.enqueue(e):(console.log("收到空音频数据帧,可能是结束标志","warning"),this.isPlaying&&this.streamingContext&&(this.streamingContext.endOfStream=!0))}async preload(){console.log("预加载Opus解码器...","info");try{await this.initOpusDecoder(),console.log("Opus解码器预加载成功","success")}catch(e){console.log(`Opus解码器预加载失败: ${e.message},将在需要时重试`,"warning")}}async start(){await this.preload(),this.playBufferedAudio(),this.startAudioBuffering()}}let p=null;function g(){return p||(p=new h),p}let f=[],m=null,y=[],w=null;function b(e){w=e}async function S(){const e=await fetch("/default-mcp-tools.json").then((e=>e.json())),t=localStorage.getItem("mcpTools");if(t)try{f=JSON.parse(t)}catch(n){console.log("加载MCP工具失败,使用默认工具","warning"),f=[...e]}else f=[...e]}function v(){const e=document.getElementById("mcpToolsContainer"),t=document.getElementById("mcpToolsCount");t.textContent=`${f.length} 个工具`,0!==f.length?e.innerHTML=f.map(((e,t)=>{const n=e.inputSchema.properties?Object.keys(e.inputSchema.properties).length:0,o=e.inputSchema.required?e.inputSchema.required.length:0,s=e.mockResponse&&Object.keys(e.mockResponse).length>0;return`\n <div class="mcp-tool-card">\n <div class="mcp-tool-header">\n <div class="mcp-tool-name">${e.name}</div>\n <div class="mcp-tool-actions">\n <button onclick="window.mcpModule.editMcpTool(${t})"\n style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #2196f3; color: white; cursor: pointer; font-size: 12px;">\n ✏️ 编辑\n </button>\n <button onclick="window.mcpModule.deleteMcpTool(${t})"\n style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #f44336; color: white; cursor: pointer; font-size: 12px;">\n 🗑️ 删除\n </button>\n </div>\n </div>\n <div class="mcp-tool-description">${e.description}</div>\n <div class="mcp-tool-info">\n <div class="mcp-tool-info-row">\n <span class="mcp-tool-info-label">参数数量:</span>\n <span class="mcp-tool-info-value">${n} 个 ${o>0?`(${o} 个必填)`:""}</span>\n </div>\n <div class="mcp-tool-info-row">\n <span class="mcp-tool-info-label">模拟返回:</span>\n <span class="mcp-tool-info-value">${s?"✅ 已配置: "+JSON.stringify(e.mockResponse):"⚪ 使用默认"}</span>\n </div>\n </div>\n </div>\n `})).join(""):e.innerHTML='<div style="text-align: center; padding: 30px; color: #999;">暂无工具,点击下方按钮添加新工具</div>'}function C(){const e=document.getElementById("mcpPropertiesContainer");0!==y.length?e.innerHTML=y.map(((e,t)=>`\n <div class="mcp-property-item">\n <div class="mcp-property-header">\n <span class="mcp-property-name">${e.name}</span>\n <button type="button" onclick="window.mcpModule.deleteMcpProperty(${t})"\n style="padding: 3px 8px; border: none; border-radius: 3px; background-color: #f44336; color: white; cursor: pointer; font-size: 11px;">\n 删除\n </button>\n </div>\n <div class="mcp-property-row">\n <div>\n <label class="mcp-small-label">参数名称 *</label>\n <input type="text" class="mcp-small-input" value="${e.name}"\n onchange="window.mcpModule.updateMcpProperty(${t}, 'name', this.value)" required>\n </div>\n <div>\n <label class="mcp-small-label">数据类型 *</label>\n <select class="mcp-small-input" onchange="window.mcpModule.updateMcpProperty(${t}, 'type', this.value)">\n <option value="string" ${"string"===e.type?"selected":""}>字符串</option>\n <option value="integer" ${"integer"===e.type?"selected":""}>整数</option>\n <option value="number" ${"number"===e.type?"selected":""}>数字</option>\n <option value="boolean" ${"boolean"===e.type?"selected":""}>布尔值</option>\n <option value="array" ${"array"===e.type?"selected":""}>数组</option>\n <option value="object" ${"object"===e.type?"selected":""}>对象</option>\n </select>\n </div>\n </div>\n ${"integer"===e.type||"number"===e.type?`\n <div class="mcp-property-row">\n <div>\n <label class="mcp-small-label">最小值</label>\n <input type="number" class="mcp-small-input" value="${void 0!==e.minimum?e.minimum:""}"\n placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${t}, 'minimum', this.value ? parseFloat(this.value) : undefined)">\n </div>\n <div>\n <label class="mcp-small-label">最大值</label>\n <input type="number" class="mcp-small-input" value="${void 0!==e.maximum?e.maximum:""}"\n placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${t}, 'maximum', this.value ? parseFloat(this.value) : undefined)">\n </div>\n </div>\n `:""}\n <div class="mcp-property-row-full">\n <label class="mcp-small-label">参数描述</label>\n <input type="text" class="mcp-small-input" value="${e.description||""}"\n placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${t}, 'description', this.value)">\n </div>\n <label class="mcp-checkbox-label">\n <input type="checkbox" ${e.required?"checked":""}\n onchange="window.mcpModule.updateMcpProperty(${t}, 'required', this.checked)">\n 必填参数\n </label>\n </div>\n `)).join(""):e.innerHTML='<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>'}function k(e,t,n){if("name"===t){const t=y.some(((t,o)=>o!==e&&t.name===n));if(t)return alert("参数名称已存在,请使用不同的名称"),void C()}y[e][t]=n,"type"===t&&"integer"!==n&&"number"!==n&&(delete y[e].minimum,delete y[e].maximum,C())}function x(e){y.splice(e,1),C()}function M(e=null){const t=w&&w.readyState===WebSocket.OPEN;if(t)return void alert("WebSocket 已连接,无法编辑工具");m=e;const n=document.getElementById("mcpErrorContainer");if(n.innerHTML="",null!==e){document.getElementById("mcpModalTitle").textContent="编辑工具";const t=f[e];document.getElementById("mcpToolName").value=t.name,document.getElementById("mcpToolDescription").value=t.description,document.getElementById("mcpMockResponse").value=t.mockResponse?JSON.stringify(t.mockResponse,null,2):"",y=[];const n=t.inputSchema;n.properties&&Object.keys(n.properties).forEach((e=>{const t=n.properties[e];y.push({name:e,type:t.type||"string",minimum:t.minimum,maximum:t.maximum,description:t.description||"",required:n.required&&n.required.includes(e)})}))}else document.getElementById("mcpModalTitle").textContent="添加工具",document.getElementById("mcpToolForm").reset(),y=[];C(),document.getElementById("mcpToolModal").style.display="block"}function _(e){M(e)}function P(e){const t=w&&w.readyState===WebSocket.OPEN;if(t)alert("WebSocket 已连接,无法编辑工具");else if(confirm(`确定要删除工具 "${f[e].name}" 吗?`)){const t=f[e].name;f.splice(e,1),E(),v(),console.log(`已删除工具: ${t}`,"info")}}function E(){localStorage.setItem("mcpTools",JSON.stringify(f))}function O(){return f.map((e=>({name:e.name,description:e.description,inputSchema:e.inputSchema})))}function R(e,t){const n=f.find((t=>t.name===e));if(!n)return console.log(`未找到工具: ${e}`,"error"),{success:!1,error:`未知工具: ${e}`};if("self.drink_car_list"==n.name){console.log("准备触发 gotoOrderEvent 事件");const t=new CustomEvent("gotoOrderEvent");return window.dispatchEvent(t),{success:!0,message:`工具 ${e} 执行成功`,data:sessionStorage.getItem("cartList")||[]}}if("self.drink_car_reset"==n.name){console.log("准备触发 resetOrderEvent 事件");const n=new CustomEvent("resetOrderEvent",{detail:t});return window.dispatchEvent(n),{success:!0,message:`工具 ${e} 执行成功`,data:sessionStorage.getItem("cartList")||[]}}if("self.drink_order"==n.name){console.log("准备触发 orderEvent 事件");const e=new CustomEvent("orderEvent");window.dispatchEvent(e)}}window.mcpModule={updateMcpProperty:k,deleteMcpProperty:x,editMcpTool:_,deleteMcpTool:P};class ${constructor(){this.isRecording=!1,this.audioContext=null,this.analyser=null,this.audioProcessor=null,this.audioProcessorType=null,this.audioSource=null,this.opusEncoder=null,this.pcmDataBuffer=new Int16Array,this.audioBuffers=[],this.totalAudioSize=0,this.visualizationRequest=null,this.recordingTimer=null,this.websocket=null,this.onRecordingStart=null,this.onRecordingStop=null,this.onVisualizerUpdate=null}setWebSocket(e){this.websocket=e}getAudioContext(){const e=g();return e.getAudioContext()}initEncoder(){return this.opusEncoder||(this.opusEncoder=a()),this.opusEncoder}getAudioProcessorCode(){return"\n class AudioRecorderProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n this.buffers = [];\n this.frameSize = 960;\n this.buffer = new Int16Array(this.frameSize);\n this.bufferIndex = 0;\n this.isRecording = false;\n\n this.port.onmessage = (event) => {\n if (event.data.command === 'start') {\n this.isRecording = true;\n this.port.postMessage({ type: 'status', status: 'started' });\n } else if (event.data.command === 'stop') {\n this.isRecording = false;\n\n if (this.bufferIndex > 0) {\n const finalBuffer = this.buffer.slice(0, this.bufferIndex);\n this.port.postMessage({\n type: 'buffer',\n buffer: finalBuffer\n });\n this.bufferIndex = 0;\n }\n\n this.port.postMessage({ type: 'status', status: 'stopped' });\n }\n };\n }\n\n process(inputs, outputs, parameters) {\n if (!this.isRecording) return true;\n\n const input = inputs[0][0];\n if (!input) return true;\n\n for (let i = 0; i < input.length; i++) {\n if (this.bufferIndex >= this.frameSize) {\n this.port.postMessage({\n type: 'buffer',\n buffer: this.buffer.slice(0)\n });\n this.bufferIndex = 0;\n }\n\n this.buffer[this.bufferIndex++] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767)));\n }\n\n return true;\n }\n }\n\n registerProcessor('audio-recorder-processor', AudioRecorderProcessor);\n "}async createAudioProcessor(){this.audioContext=this.getAudioContext();try{if(this.audioContext.audioWorklet){const e=new Blob([this.getAudioProcessorCode()],{type:"application/javascript"}),t=URL.createObjectURL(e);await this.audioContext.audioWorklet.addModule(t),URL.revokeObjectURL(t);const n=new AudioWorkletNode(this.audioContext,"audio-recorder-processor");n.port.onmessage=e=>{"buffer"===e.data.type&&this.processPCMBuffer(e.data.buffer)},console.log("使用AudioWorklet处理音频","success");const o=this.audioContext.createGain();return o.gain.value=0,n.connect(o),o.connect(this.audioContext.destination),{node:n,type:"worklet"}}return console.log("AudioWorklet不可用,使用ScriptProcessorNode作为回退方案","warning"),this.createScriptProcessor()}catch(e){return console.log(`创建音频处理器失败: ${e.message},尝试回退方案`,"error"),this.createScriptProcessor()}}createScriptProcessor(){try{const e=4096,t=this.audioContext.createScriptProcessor(e,1,1);t.onaudioprocess=e=>{if(!this.isRecording)return;const t=e.inputBuffer.getChannelData(0),n=new Int16Array(t.length);for(let o=0;o<t.length;o++)n[o]=Math.max(-32768,Math.min(32767,Math.floor(32767*t[o])));this.processPCMBuffer(n)};const n=this.audioContext.createGain();return n.gain.value=0,t.connect(n),n.connect(this.audioContext.destination),console.log("使用ScriptProcessorNode作为回退方案成功","warning"),{node:t,type:"processor"}}catch(e){return console.log(`回退方案也失败: ${e.message}`,"error"),null}}processPCMBuffer(e){if(!this.isRecording)return;const t=new Int16Array(this.pcmDataBuffer.length+e.length);t.set(this.pcmDataBuffer),t.set(e,this.pcmDataBuffer.length),this.pcmDataBuffer=t;const n=960;while(this.pcmDataBuffer.length>=n){const e=this.pcmDataBuffer.slice(0,n);this.pcmDataBuffer=this.pcmDataBuffer.slice(n),this.encodeAndSendOpus(e)}}encodeAndSendOpus(e=null){if(this.opusEncoder)try{if(e){const n=this.opusEncoder.encode(e);if(n&&n.length>0){if(this.audioBuffers.push(n.buffer),this.totalAudioSize+=n.length,this.websocket&&this.websocket.readyState===WebSocket.OPEN)try{this.websocket.send(n.buffer)}catch(t){console.log(`WebSocket发送错误: ${t.message}`,"error")}}else log("Opus编码失败,无有效数据返回","error")}else if(this.pcmDataBuffer.length>0){const e=960;if(this.pcmDataBuffer.length<e){const t=new Int16Array(e);t.set(this.pcmDataBuffer),this.encodeAndSendOpus(t)}else this.encodeAndSendOpus(this.pcmDataBuffer.slice(0,e));this.pcmDataBuffer=new Int16Array(0)}}catch(t){console.log(`Opus编码错误: ${t.message}`,"error")}else console.log("Opus编码器未初始化","error")}async start(){try{if(!this.initEncoder())return console.log("无法启动录音: Opus编码器初始化失败","error"),!1;const e=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,sampleRate:16e3,channelCount:1,latency:{ideal:.02,max:.05},googNoiseSuppression:!0,googNoiseSuppression2:3,googAutoGainControl:!0,googHighpassFilter:!0}});this.audioContext=this.getAudioContext(),"suspended"===this.audioContext.state&&await this.audioContext.resume();const t=await this.createAudioProcessor();if(!t)return console.log("无法创建音频处理器","error"),!1;if(this.audioProcessor=t.node,this.audioProcessorType=t.type,this.audioSource=this.audioContext.createMediaStreamSource(e),this.analyser=this.audioContext.createAnalyser(),this.analyser.fftSize=2048,this.audioSource.connect(this.analyser),this.audioSource.connect(this.audioProcessor),this.pcmDataBuffer=new Int16Array,this.audioBuffers=[],this.totalAudioSize=0,this.isRecording=!0,"worklet"===this.audioProcessorType&&this.audioProcessor.port&&this.audioProcessor.port.postMessage({command:"start"}),!this.websocket||this.websocket.readyState!==WebSocket.OPEN)return console.log("WebSocket未连接,无法发送开始消息","error"),!1;{const e={type:"listen",mode:localStorage.getItem("listenMode")||"wakeup",state:"start"};console.log(`发送录音开始消息: ${JSON.stringify(e)}`,"info"),this.websocket.send(JSON.stringify(e))}let n=0;return this.recordingTimer=setInterval((()=>{n+=.1,this.onRecordingStart&&this.onRecordingStart(n)}),100),console.log("开始PCM直接录音","success"),!0}catch(e){return console.log(`直接录音启动错误: ${e.message}`,"error"),this.isRecording=!1,!1}}stop(){if(!this.isRecording)return!1;try{if(this.isRecording=!1,this.audioProcessor&&("worklet"===this.audioProcessorType&&this.audioProcessor.port&&this.audioProcessor.port.postMessage({command:"stop"}),this.audioProcessor.disconnect(),this.audioProcessor=null),this.audioSource&&(this.audioSource.disconnect(),this.audioSource=null),this.visualizationRequest&&(cancelAnimationFrame(this.visualizationRequest),this.visualizationRequest=null),this.recordingTimer&&(clearInterval(this.recordingTimer),this.recordingTimer=null),this.encodeAndSendOpus(),this.websocket&&this.websocket.readyState===WebSocket.OPEN){const e=new Uint8Array(0);this.websocket.send(e);const t={type:"listen",mode:localStorage.getItem("listenMode")||"wakeup",state:"stop"};this.websocket.send(JSON.stringify(t)),console.log("已发送录音停止信号","info")}return this.onRecordingStop&&this.onRecordingStop(),console.log("停止PCM直接录音","success"),!0}catch(e){return console.log(`直接录音停止错误: ${e.message}`,"error"),!1}}getAnalyser(){return this.analyser}}let A=null;function I(){return A||(A=new $),A}async function T(e,t){if(!B(t))return;const n=await D(e,t);if(!n)return void console.log("无法从OTA服务器获取信息","error");const{websocket:o}=n;if(!o||!o.url)return void console.log("OTA响应中缺少websocket信息","error");let s=new URL(o.url);o.token&&(o.token.startsWith("Bearer ")?s.searchParams.append("authorization",o.token):s.searchParams.append("authorization","Bearer "+o.token)),s.searchParams.append("device-id",t.deviceId),s.searchParams.append("client-id",t.clientId);const i=s.toString();return console.log(`正在连接: ${i}`,"info"),new WebSocket(s.toString())}function B(e){return e.deviceMac?!!e.clientId||(console.log("客户端ID不能为空","error"),!1):(console.log("设备MAC地址不能为空","error"),!1)}async function D(e,t){try{const n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","Device-Id":t.deviceId,"Client-Id":t.clientId},body:JSON.stringify({version:0,uuid:"",application:{name:"xiaozhi-web-test",version:"1.0.0",compile_time:"2025-04-16 10:00:00",idf_version:"4.4.3",elf_sha256:"1234567890abcdef1234567890abcdef1234567890abcdef"},ota:{label:"xiaozhi-web-test"},board:{type:"xiaozhi-web-test",ssid:"xiaozhi-web-test",rssi:0,channel:0,ip:"192.168.1.1",mac:t.deviceMac},flash_size:0,minimum_free_heap_size:0,mac_address:t.deviceMac,chip_model_name:"",chip_info:{model:0,cores:0,revision:0,features:0},partition_table:[{label:"",type:0,subtype:0,address:0,size:0}]})});if(!n.ok)throw new Error(`${n.status} ${n.statusText}`);const o=await n.json();return o}catch(n){return null}}function N(){return{deviceId:localStorage.getItem("MAC"),deviceName:"测试设备",deviceMac:localStorage.getItem("MAC"),clientId:"web_test_client",token:"your-token1"}}function z(){const e=localStorage.getItem("otaUrl");localStorage.setItem("xz_tester_otaUrl",e)}class q{constructor(){this.websocket=null,this.onConnectionStateChange=null,this.onRecordButtonStateChange=null,this.onSessionStateChange=null,this.onSessionEmotionChange=null,this.currentSessionId=null,this.isRemoteSpeaking=!1,this.heartbeatTimer=null,this.reconnectConfig={maxRetries:10,baseDelay:1e3,maxDelay:3e4,retryCount:0,reconnectTimer:null,isReconnecting:!1,manualDisconnect:!1}}startHeartbeat(){this.stopHeartbeat(),this.sendHeartbeat()}stopHeartbeat(){this.heartbeatTimer&&(clearTimeout(this.heartbeatTimer),this.heartbeatTimer=null)}sendHeartbeat(){if(this.websocket?.readyState===WebSocket.OPEN)try{this.websocket.send("1")}catch(e){console.error("心跳发送失败:",e)}this.heartbeatTimer=setTimeout((()=>{this.sendHeartbeat()}),5e3)}async sendHelloMessage(){if(!this.websocket||this.websocket.readyState!==WebSocket.OPEN)return!1;this.startHeartbeat();try{const e=N(),t={type:"hello",device_id:e.deviceId,device_name:e.deviceName,device_mac:e.deviceMac,token:e.token,seat:localStorage.getItem("SEAT"),features:{mcp:!0}};return console.log("发送hello握手消息","info"),this.websocket.send(JSON.stringify(t)),new Promise((e=>{const t=setTimeout((()=>{console.log("等待hello响应超时","error"),console.log('提示: 请尝试点击"测试认证"按钮进行连接排查',"info"),e(!1)}),5e3),n=o=>{try{const s=JSON.parse(o.data);"hello"===s.type&&s.session_id&&(console.log(`服务器握手成功,会话ID: ${s.session_id}`,"success"),this.reconnectConfig.retryCount=0,clearTimeout(t),this.websocket.removeEventListener("message",n),e(!0))}catch(s){}};this.websocket.addEventListener("message",n)}))}catch(e){return console.log(`发送hello消息错误: ${e.message}`,"error"),!1}}handleTextMessage(e){if("hello"===e.type);else if("tts"===e.type)this.handleTTSMessage(e);else if("audio"===e.type);else if("stt"===e.type){const t=new CustomEvent("wsSendMessage",{detail:e});window.dispatchEvent(t)}else if("llm"===e.type);else if("mcp"===e.type)this.handleMCPMessage(e);else if("json_data"===e.type){if("drinks"===e.state){const t=new CustomEvent("drinkListEvent",{detail:e.data});window.dispatchEvent(t)}else if("book"===e.state){const t=new CustomEvent("bookListEvent",{detail:e});window.dispatchEvent(t)}}else if("view_action"===e.type){const t=new CustomEvent("viewActionEvent",{detail:e.state});window.dispatchEvent(t)}else console.log(`未知消息类型: ${e.type}`,"warning")}handleTTSMessage(e){if("start"===e.state){console.log("服务器开始发送语音","info"),this.currentSessionId=e.session_id;const t=new CustomEvent("startThink");window.dispatchEvent(t),this.isRemoteSpeaking=!0,this.onSessionStateChange&&this.onSessionStateChange(!0)}else if("sentence_start"===e.state){const t=new CustomEvent("startVolic",{detail:e.text});window.dispatchEvent(t),console.log(`服务器发送语音段: ${e.text}`,"info")}else if("sentence_end"===e.state)console.log(`语音段结束: ${e.text}`,"info");else if("stop"===e.state){const e=new CustomEvent("stopVolic");window.dispatchEvent(e),console.log("服务器语音传输结束","info"),this.isRemoteSpeaking=!1,this.onRecordButtonStateChange&&this.onRecordButtonStateChange(!1),this.onSessionStateChange&&this.onSessionStateChange(!1)}}handleMCPMessage(e){const t=e.payload||{};if(console.log(`服务器下发: ${JSON.stringify(e)}`,"info"),"tools/list"===t.method){const n=O(),o=JSON.stringify({session_id:e.session_id||"",type:"mcp",payload:{jsonrpc:"2.0",id:t.id,result:{tools:n}}});console.log(`客户端上报: ${o}`,"info"),this.websocket.send(o),console.log(`回复MCP工具列表: ${n.length} 个工具`,"info")}else if("tools/call"===t.method){const n=t.params?.name,o=t.params?.arguments;console.log(`调用工具: ${n} 参数: ${JSON.stringify(o)}`,"info");const s=R(n,o),i=JSON.stringify({session_id:e.session_id||"",type:"mcp",payload:{jsonrpc:"2.0",id:t.id,result:{content:[{type:"text",text:JSON.stringify(s)}],isError:!1}}});console.log(`客户端上报: ${i}`,"info"),this.websocket.send(i)}else"initialize"===t.method?console.log(`收到工具初始化请求: ${JSON.stringify(t.params)}`,"info"):console.log(`未知的MCP方法: ${t.method}`,"warning")}async handleBinaryMessage(e){try{let t;if(e instanceof ArrayBuffer)t=e;else{if(!(e instanceof Blob))return void console.log("收到未知类型的二进制数据: "+typeof e,"warning");t=await e.arrayBuffer(),console.log(`收到Blob音频数据,大小: ${t.byteLength}字节`,"debug")}const n=new Uint8Array(t),o=g();o.enqueueAudioData(n)}catch(t){console.log(`处理二进制消息出错: ${t.message}`,"error")}}calculateReconnectDelay(){const e=Math.min(this.reconnectConfig.baseDelay*Math.pow(2,this.reconnectConfig.retryCount),this.reconnectConfig.maxDelay),t=.2*e*(Math.random()-.5);return Math.round(e+t)}triggerReconnect(){if(this.reconnectConfig.manualDisconnect)return void console.log("手动断开连接,不进行自动重连","info");if(this.reconnectConfig.retryCount>=this.reconnectConfig.maxRetries)return console.log(`已达到最大重连次数(${this.reconnectConfig.maxRetries}),停止重连`,"error"),this.reconnectConfig.isReconnecting=!1,void(this.onConnectionStateChange&&this.onConnectionStateChange(!1));const e=this.calculateReconnectDelay();this.reconnectConfig.retryCount++,console.log(`准备进行第${this.reconnectConfig.retryCount}次重连,延迟${e}ms`,"info"),this.reconnectConfig.reconnectTimer=setTimeout((async()=>{console.log(`开始第${this.reconnectConfig.retryCount}次重连`,"info");try{const e=await this.connect();e?(console.log("重连成功","success"),this.reconnectConfig.isReconnecting=!1):(console.log(`第${this.reconnectConfig.retryCount}次重连失败`,"error"),this.triggerReconnect())}catch(e){console.log(`重连出错: ${e.message}`,"error"),this.triggerReconnect()}}),e)}stopReconnect(){this.reconnectConfig.reconnectTimer&&(clearTimeout(this.reconnectConfig.reconnectTimer),this.reconnectConfig.reconnectTimer=null),this.reconnectConfig.isReconnecting=!1,this.reconnectConfig.retryCount=0}async connect(){this.stopReconnect();const e=N();console.log("正在检查OTA状态...","info"),z();try{const t=localStorage.getItem("xz_tester_otaUrl"),n=await T(t,e);if(void 0===n)return this.reconnectConfig.isReconnecting||this.reconnectConfig.manualDisconnect||(this.reconnectConfig.isReconnecting=!0,this.triggerReconnect()),!1;this.websocket=n,this.websocket.binaryType="arraybuffer",b(this.websocket);const o=I();return o.setWebSocket(this.websocket),this.setupEventHandlers(),!0}catch(t){return console.log(`连接错误: ${t.message}`,"error"),this.onConnectionStateChange&&this.onConnectionStateChange(!1),this.reconnectConfig.isReconnecting||this.reconnectConfig.manualDisconnect||(this.reconnectConfig.isReconnecting=!0,this.triggerReconnect()),!1}}setupEventHandlers(){this.websocket.onopen=async()=>{const e=localStorage.getItem("xz_tester_wsUrl");console.log(`已连接到服务器: ${e}`,"success"),this.onConnectionStateChange&&this.onConnectionStateChange(!0),this.isRemoteSpeaking=!1,this.onSessionStateChange&&this.onSessionStateChange(!1),await this.sendHelloMessage()},this.websocket.onclose=e=>{console.log(`已断开连接,代码: ${e.code}, 原因: ${e.reason}`,"info"),this.stopHeartbeat(),b(null);const t=I();t.stop(),this.onConnectionStateChange&&this.onConnectionStateChange(!1),this.reconnectConfig.manualDisconnect||this.reconnectConfig.isReconnecting||1e3!==e.code&&1001!==e.code&&(console.log("检测到异常断开连接,准备自动重连","warning"),this.reconnectConfig.isReconnecting=!0,this.triggerReconnect()),this.reconnectConfig.manualDisconnect=!1},this.websocket.onerror=e=>{console.log(`WebSocket错误: ${e.message||"未知错误"}`,"error"),this.onConnectionStateChange&&this.onConnectionStateChange(!1)},this.websocket.onmessage=e=>{try{if("string"===typeof e.data){const t=JSON.parse(e.data);this.handleTextMessage(t)}else this.handleBinaryMessage(e.data)}catch(t){console.log(`WebSocket消息处理错误: ${t.message}`,"error")}}}disconnect(){if(this.reconnectConfig.manualDisconnect=!0,this.stopReconnect(),!this.websocket)return;this.websocket.close(1e3,"Manual disconnect");const e=I();e.stop()}sendTextMessage(e){try{const t={session_id:this.currentSessionId,type:"abort",reason:"wake_word_detected"};this.websocket.send(JSON.stringify(t)),console.log("发送打断消息","info");const n={type:"listen",mode:localStorage.getItem("listenMode")||"wakeup",state:"detect",text:e};return this.websocket.send(JSON.stringify(n)),console.log(`发送文本消息: ${e}`,"info6666"),!0}catch(t){return console.log(`发送消息错误: ${t.message}`,"error"),!1}}getWebSocket(){return this.websocket}isConnected(){return this.websocket&&this.websocket.readyState===WebSocket.OPEN}getReconnectStatus(){return{isReconnecting:this.reconnectConfig.isReconnecting,retryCount:this.reconnectConfig.retryCount,maxRetries:this.reconnectConfig.maxRetries}}resetReconnectConfig(){this.stopReconnect(),this.reconnectConfig.retryCount=0,this.reconnectConfig.isReconnecting=!1,this.reconnectConfig.manualDisconnect=!1}}let W=null;function j(){return W||(W=new q),W}async function H(){const e=j();await e.connect()}async function L(){const e=j();await e.disconnect()}async function U(){const e=I();await e.start()}function J(){const e=j();return e.isConnected()}const F={name:"ibiAiTalk",data(){return{audioPlayer:null}},props:{listenMode:{type:String,default:"wakeup"},agentId:{type:String,default:""},env:{type:String,default:"test"},macAddress:{type:String,default:""}},async mounted(){localStorage.setItem("MAC",this.macAddress),localStorage.setItem("listenMode",this.listenMode),localStorage.setItem("agentId",this.agentId);let e="";"test"===this.env?e="https://test-ai-talk-manage.ptdplat.com":"prod"===this.env&&(e="https://ai-talk-manage.ptdcloud.com");const t=await fetch(`${e}/device/addByAgent`,{method:"POST",headers:{"Content-Type":"application/json",authorization:"Bearer z6frotkj-8vdw-moy1-vc6j-manpewkvob48"},body:JSON.stringify({macAddress:this.macAddress,agentId:this.agentId})});console.log(t,"添加设备"),localStorage.setItem("otaUrl",`${e}/xiaozhi/ota/`),r(),a(),S(),J()||await H(),this.audioPlayer=g(),await this.audioPlayer.start(),await U()},beforeDestroy(){this.audioPlayer.stop().catch((()=>{})),J()&&L().catch((()=>{}))}},Q=F;function V(e,t,n,o,s,i,r,c){var a,l="function"===typeof e?e.options:e;if(t&&(l.render=t,l.staticRenderFns=n,l._compiled=!0),o&&(l.functional=!0),i&&(l._scopeId="data-v-"+i),r?(a=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),s&&s.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(r)},l._ssrRegister=a):s&&(a=c?function(){s.call(this,(l.functional?this.parent:this).$root.$options.shadowRoot)}:s),a)if(l.functional){l._injectStyles=a;var u=l.render;l.render=function(e,t){return a.call(t),u(e,t)}}else{var d=l.beforeCreate;l.beforeCreate=d?[].concat(d,a):[a]}return{exports:e,options:l}}var G=V(Q,s,i,!1,null,null,null);const X=G.exports,Z=X;return t=t["default"],t})()));
|
|
2
2
|
//# sourceMappingURL=index.umd.min.js.map
|