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/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
语音交互参数
|
|
2
|
-
listenMode:
|
|
3
|
-
|
|
2
|
+
listenMode: 监听模式,"realtime"(实时对话),默认值为"wakeup(唤醒词对话)"。
|
|
3
|
+
env: 当前环境变量,可选值为"test"(测试环境)和"prod"(生产环境),默认值为"test"。
|
|
4
4
|
macAddress: 设备MAC地址,默认值为"00:00:00:00:00:00"。
|
|
5
|
+
agentId: 智能体ID,默认值为""。
|
|
5
6
|
default-mcp-tools.json: 设备默认参数配置文件"。
|
|
6
7
|
<script src="/libopus.js"></script> 在html页面中引入opus编码器库
|
|
7
8
|
<!-- 组件中使用 -->
|
|
8
9
|
<template>
|
|
9
|
-
<ibiAiTalk :listenMode="listenMode" :
|
|
10
|
+
<ibiAiTalk :listenMode="listenMode" :env="env" :macAddress="macAddress" :agentId="agentId" />
|
|
10
11
|
</template>
|
|
11
12
|
|
|
12
13
|
<script>
|
|
@@ -16,8 +17,9 @@ export default {
|
|
|
16
17
|
data() {
|
|
17
18
|
return {
|
|
18
19
|
listenMode: "wakeup", // 对话模式
|
|
19
|
-
|
|
20
|
+
env: "test", // 环境变量
|
|
20
21
|
macAddress: "", // 设备MAC地址
|
|
22
|
+
agentId: "", // 智能体ID
|
|
21
23
|
};
|
|
22
24
|
},
|
|
23
25
|
};
|
package/dist/index.common.js
CHANGED
|
@@ -51,7 +51,7 @@ if (typeof window !== 'undefined') {
|
|
|
51
51
|
// Indicate to webpack that this file can be concatenated
|
|
52
52
|
/* harmony default export */ const setPublicPath = (null);
|
|
53
53
|
|
|
54
|
-
;// CONCATENATED MODULE: ./node_modules/@vue/vue-loader-v15/lib/loaders/templateLoader.js??ruleSet[1].rules[2]!./node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/index.vue?vue&type=template&id=
|
|
54
|
+
;// CONCATENATED MODULE: ./node_modules/@vue/vue-loader-v15/lib/loaders/templateLoader.js??ruleSet[1].rules[2]!./node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/index.vue?vue&type=template&id=14905db4
|
|
55
55
|
var render = function render(){var _vm=this,_c=_vm._self._c;return _c("div")
|
|
56
56
|
}
|
|
57
57
|
var staticRenderFns = []
|
|
@@ -2490,10 +2490,14 @@ function isWsConnected() {
|
|
|
2490
2490
|
type: String,
|
|
2491
2491
|
default: "wakeup",
|
|
2492
2492
|
},
|
|
2493
|
-
|
|
2493
|
+
agentId: {
|
|
2494
2494
|
type: String,
|
|
2495
2495
|
default: "",
|
|
2496
2496
|
},
|
|
2497
|
+
env: {
|
|
2498
|
+
type: String,
|
|
2499
|
+
default: "test",
|
|
2500
|
+
},
|
|
2497
2501
|
macAddress: {
|
|
2498
2502
|
type: String,
|
|
2499
2503
|
default: "",
|
|
@@ -2501,8 +2505,27 @@ function isWsConnected() {
|
|
|
2501
2505
|
},
|
|
2502
2506
|
async mounted() {
|
|
2503
2507
|
localStorage.setItem("MAC", this.macAddress);
|
|
2504
|
-
localStorage.setItem("otaUrl", this.otaUrl);
|
|
2505
2508
|
localStorage.setItem("listenMode", this.listenMode);
|
|
2509
|
+
localStorage.setItem("agentId", this.agentId);
|
|
2510
|
+
let envUrl = "";
|
|
2511
|
+
if (this.env === "test") {
|
|
2512
|
+
envUrl = "https://test-ai-talk-manage.ptdplat.com";
|
|
2513
|
+
} else if (this.env === "prod") {
|
|
2514
|
+
envUrl = "https://ai-talk-manage.ptdcloud.com";
|
|
2515
|
+
}
|
|
2516
|
+
const res = await fetch(`${envUrl}/device/addByAgent`, {
|
|
2517
|
+
method: "POST",
|
|
2518
|
+
headers: {
|
|
2519
|
+
"Content-Type": "application/json",
|
|
2520
|
+
"authorization":"Bearer " + 'z6frotkj-8vdw-moy1-vc6j-manpewkvob48'
|
|
2521
|
+
},
|
|
2522
|
+
body: JSON.stringify({
|
|
2523
|
+
macAddress: this.macAddress,
|
|
2524
|
+
agentId: this.agentId,
|
|
2525
|
+
}),
|
|
2526
|
+
});
|
|
2527
|
+
console.log(res, "添加设备");
|
|
2528
|
+
localStorage.setItem("otaUrl", `${envUrl}/xiaozhi/ota/`);
|
|
2506
2529
|
checkOpusLoaded();
|
|
2507
2530
|
initOpusEncoder();
|
|
2508
2531
|
initMcpTools();
|
package/dist/index.common.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.common.js","mappings":";;UAAA;UACA;;;;;WCDA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;;;;;;;;;;;;ACAA;AACA;;AAEA;AACA;AACA,MAAM,KAAuC,EAAE,yBAQ5C;;AAEH;AACA;AACA,IAAI,qBAAuB;AAC3B;AACA;;AAEA;AACA,oDAAe,IAAI;;;ACtBnB,+BAA+B,6BAA6B;AAC5D;AACA;;;;ACFA;AACO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA,MAAM;AACN,yDAAyD,YAAY;AACrE;AACA;;;AAGA;AACA;AACO;AACP;AACA;AACA,gCAAgC;AAChC;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,kCAAkC;AAClC,kCAAkC;AAClC,kCAAkC;;AAElC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA8C,YAAY;;AAE1D;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,yDAAyD,IAAI;AAC7D;;AAEA;AACA,yEAAyE;;AAEzE;AACA,yEAAyE;;AAEzE;AACA,yEAAyE;;AAEzE;AACA;AACA,kBAAkB;AAClB;AACA;AACA;AACA;AACA,iDAAiD,cAAc;AAC/D;AACA;AACA,aAAa;;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,oEAAoE;;AAEpE;AACA,oCAAoC,oBAAoB;AACxD;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,qDAAqD,WAAW;AAChE;;AAEA;AACA;AACA,oCAAoC,gBAAgB;AACpD;AACA;;AAEA;AACA;AACA;;AAEA;AACA,kBAAkB;AAClB,6CAA6C,cAAc;AAC3D;AACA;AACA,aAAa;;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,MAAM;AACN,oCAAoC,cAAc;AAClD;AACA;AACA;;ACxKe;AACf;AACA,4BAA4B,IAAI;;AAEhC;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6BAA6B;;AAE7B;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB;;AAEA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,+CAA+C,QAAQ;AACvD;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;ACjGgD;;AAEhD;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,qBAAqB;AACrB,2BAA2B,aAAa,IAAI;AAC5C,uCAAuC;AACvC,gCAAgC,aAAa,IAAI;AACjD,0BAA0B;AAC1B,8BAA8B;AAC9B,wBAAwB;AACxB,2BAA2B;AAC3B,2BAA2B;AAC3B;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,UAAU,aAAa;AACvB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU,aAAa;AACvB;AACA;AACA;;AAEA;AACA;AACA;AACA,oBAAoB,sBAAsB;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,sBAAsB;AAClD;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;;AAEA;AACA;AACA,wBAAwB,2BAA2B;AACnD;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,0DAA0D;;AAE1D;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACvLA;AACgD;AACa;;AAE7D;AACO;AACP;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,qBAAqB,aAAa;AAClC;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,oCAAoC,YAAY;;AAEhD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,6CAA6C,IAAI;AACjD;;AAEA;AACA;AACA,SAAS;;AAET;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,2CAA2C,eAAe;AAC1D;;AAEA;AACA,4BAA4B,oBAAoB;AAChD;AACA;;AAEA;AACA;;AAEA;AACA,YAAY;AACZ,qCAAqC,cAAc;AACnD;AACA;AACA,SAAS;;AAET;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;;AAEA;AACA,MAAM;AACN,mCAAmC,cAAc;AACjD;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,oCAAoC,cAAc;AAClD,KAAK;;AAEL;AACA;AACA;AACA,oCAAoC,MAAM;AAC1C,OAAO;AACP;AACA,2BAA2B,gBAAgB;AAC3C;AACA;;AAEA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;;AAEA;AACA,gCAAgC,sBAAsB;AACtD;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,MAAM;AACN,iCAAiC,cAAc;AAC/C;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA,yBAAyB,cAAc;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;;;ACrQA;AACA;AACA;AACA;AACA,sBAAsB;;AAEtB;AACA;AACA,WAAW,WAAW;AACtB;AACO;AACP;AACA;;AAEA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA,IAAI;AACJ;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA,6BAA6B,iBAAiB;;AAE9C;AACA;AACA,uCAAuC,eAAe,YAAY;AAClE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,iDAAiD,UAAU;AAC3D;AACA,wEAAwE,MAAM;AAC9E,sDAAsD,cAAc,oBAAoB,2BAA2B,cAAc,iBAAiB,gBAAgB;AAClK;AACA;AACA,0EAA0E,MAAM;AAChF,sDAAsD,cAAc,oBAAoB,2BAA2B,cAAc,iBAAiB,gBAAgB;AAClK;AACA;AACA;AACA;AACA,oDAAoD,iBAAiB;AACrE;AACA;AACA;AACA,4DAA4D,YAAY;AACxE,gCAAgC,eAAe;AAC/C,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yBAAyB;AACzB;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,uCAAuC,eAAe,aAAa,gBAAgB;AACnF;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,kDAAkD,UAAU;AAC5D,oFAAoF,MAAM;AAC1F,6CAA6C,cAAc,oBAAoB,2BAA2B,cAAc,iBAAiB,gBAAgB;AACzJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB;AACrB,uEAAuE,MAAM;AAC7E;AACA;AACA;AACA,mGAAmG,MAAM;AACzG;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB;AACrB,wFAAwF,MAAM;AAC9F;AACA;AACA;AACA;AACA;AACA,qBAAqB;AACrB,wFAAwF,MAAM;AAC9F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,oFAAoF,MAAM;AAC1F;AACA;AACA,yCAAyC;AACzC,mEAAmE,MAAM;AACzE;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,mBAAmB,yBAAyB;AAC5C;AACA;AACA;AACA,GAAG;AACH;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA,GAAG;;AAEH;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,OAAO;AACP;AACA,IAAI;AACJ;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,kBAAkB;AAClB;AACA;;AAEA;AACA,yBAAyB;;AAEzB;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;;AAEA,iBAAiB;;AAEjB;AACA;AACA,0BAA0B,KAAK;AAC/B,IAAI;AACJ;AACA,0BAA0B,KAAK;AAC/B;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,uDAAuD,QAAQ;AAC/D;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B,qBAAqB;AAC/C;AACA;AACA;AACA;AACA,0BAA0B,SAAS;AACnC;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA,GAAG;AACH;;AAEA;AACA;AACA;AACO;AACP;AACA;AACA,0BAA0B,SAAS;AACnC;AACA;AACA,sBAAsB,SAAS;AAC/B;AACA;;AAEA;AACA,2CAA2C;AAC3C;AACA;AACA;AACA;AACA,qBAAqB,UAAU;AAC/B;AACA;AACA,IAAI;AACJ,4CAA4C;AAC5C;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,uBAAuB,UAAU;AACjC;AACA;AACA,IAAI;AACJ,uCAAuC;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACpgBA;AACkD;AACL;AAC7C;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,cAAc;AACtC;AACA;AACA;AACA;AACA;AACA;AACA,yBAAyB,eAAe;AACxC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oDAAoD,mCAAmC;AACvF,0BAA0B;AAC1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iCAAiC;AACjC;AACA;AACA;AACA,oDAAoD,mCAAmC;AACvF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAoC,kBAAkB;AACtD;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA,sBAAsB,cAAc;AACpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,kBAAkB;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAe;AACf,MAAM;AACN,8BAA8B,sBAAsB;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc;AACd,4CAA4C,cAAc;AAC1D;AACA;AACA,UAAU;AACV;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA,YAAY;AACZ;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN,+BAA+B,cAAc;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,wBAAwB;AAC7C;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA+C,kBAAkB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAuB,8BAA8B;AACrD;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,MAAM;AACN,+BAA+B,cAAc;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,iBAAiB;AAClE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN,+BAA+B,cAAc;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,yBAAgB;AAChC;AACA;AACA;AACA;AACA;;;ACnaA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY,YAAY;AACxB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yBAAyB,MAAM;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,uBAAuB,2BAA2B;AAClD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB;AACA;AACA;AACA;AACA,6BAA6B,8CAA8C;AAC3E,oCAAoC,qDAAqD;AACzF,aAAa;AACb,SAAS;AACT;AACA,wCAAwC,YAAY,EAAE,eAAe;AACrE;AACA;AACA,uBAAuB;AACvB,MAAM;AACN,qBAAqB;AACrB;AACA;;AClHA;AACA;AACA;AACA;AACA,kBAAkB,OAAO;AACzB;AACA,oBAAoB,OAAO;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;;ACtCA;AACsD;AACO;AAChB;AACI;AAK7B;AACpB;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B;AAC1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2BAA2B;AAC3B;AACA,qBAAqB,SAAS;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA,iCAAiC,oBAAoB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY;AACZ;AACA;AACA;AACA;AACA;AACA,OAAO;AACP,MAAM;AACN,kCAAkC,cAAc;AAChD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA,MAAM;AACN,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA,MAAM;AACN,MAAM;AACN;AACA,MAAM;AACN;AACA;AACA;AACA,SAAS;AACT;AACA,QAAQ;AACR;AACA;AACA,SAAS;AACT;AACA;AACA,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA,MAAM;AACN,6BAA6B,aAAa;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA,+BAA+B,aAAa;AAC5C,MAAM;AACN,4BAA4B,aAAa;AACzC,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B,wBAAwB;AAClD;AACA;AACA,oBAAoB,WAAW;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW;AACX,SAAS;AACT,OAAO;AACP,4BAA4B,aAAa;AACzC;AACA,gCAAgC,cAAc;AAC9C,MAAM;AACN;AACA;AACA;AACA;AACA,iBAAiB,UAAU,MAAM,yBAAyB;AAC1D;AACA;AACA;AACA,qBAAqB,cAAc;AACnC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,WAAW;AACX,SAAS;AACT,OAAO;AACP;AACA,4BAA4B,aAAa;AACzC;AACA,MAAM;AACN;AACA,sBAAsB,+BAA+B;AACrD;AACA;AACA,MAAM;AACN,+BAA+B,eAAe;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA,4BAA4B,uBAAuB;AACnD;AACA;AACA,QAAQ;AACR,qCAAqC,YAAY;AACjD;AACA;AACA;AACA;AACA,0BAA0B,cAAc;AACxC;AACA,MAAM;AACN,gCAAgC,cAAc;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,gCAAgC;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,gCAAgC,QAAQ,MAAM;AAC5D;AACA;AACA;AACA;AACA;AACA,wBAAwB,gCAAgC;AACxD;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA,gBAAgB,gCAAgC;AAChD;AACA;AACA;AACA;AACA,QAAQ;AACR,6BAA6B,cAAc;AAC3C;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mBAAmB,SAAS;AAC5B;AACA,IAAI,kBAAkB;AACtB;AACA;AACA;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,YAAe;AACrB;AACA;AACA,4BAA4B,yBAAgB;AAC5C;AACA;AACA;AACA;AACA;AACA,MAAM;AACN,2BAA2B,cAAc;AACzC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA8B,IAAI;AAClC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,WAAW,QAAQ,aAAa;AACrD;AACA;AACA;AACA;AACA;AACA,MAAM,YAAe;AACrB;AACA;AACA,4BAA4B,yBAAgB;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kCAAkC,wBAAwB;AAC1D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA,QAAQ;AACR,wCAAwC,cAAc;AACtD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B,yBAAgB;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B,KAAK;AAClC;AACA;AACA,MAAM;AACN,6BAA6B,cAAc;AAC3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;;;ACpjBA;AACiD;AACI;;AAErD;AACO;AACP,oBAAoB,mBAAmB;AACvC;AACA;;AAEA;AACO;AACP,oBAAoB,mBAAmB;AACvC;AACA;;AAEA;AACO;AACP,wBAAwB,yBAAgB;AACxC;AACA;;AAEA;AACO;AACP;AACA;AACA;;;AAGA;AACO;AACP,oBAAoB,mBAAmB;AACvC;AACA;;;AC9ByE;AACtB;AACH;AAMjB;AAC/B,wFAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,eAAe;AACnB,IAAI,eAAe;AACnB,IAAI,YAAY;AAChB,SAAS,aAAa;AACtB,YAAY,uBAAuB;AACnC;AACA,uBAAuB,cAAc;AACrC;AACA,UAAU,qBAAqB;AAC/B;AACA;AACA;AACA;AACA,QAAQ,aAAa;AACrB,MAAM,qBAAqB;AAC3B;AACA;AACA,CAAC,EAAC;;;ACtD2H,CAAC,iEAAe,iDAAG,EAAC;;ACAjJ;;AAEA;AACA;AACA;;AAEe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;AC/FmF;AAC3B;AACL;;;AAGnD;AACA,CAAgG;AAChG,gBAAgB,kBAAU;AAC1B,EAAE,0BAAM;AACR,EAAE,MAAM;AACR,EAAE,eAAe;AACjB;AACA;AACA;AACA;AACA;AACA;;AAEA,4CAAe;;AClBS;AACA;AACxB,gDAAe,KAAG;AACI","sources":["webpack://ibi-ai-talk/webpack/bootstrap","webpack://ibi-ai-talk/webpack/runtime/define property getters","webpack://ibi-ai-talk/webpack/runtime/hasOwnProperty shorthand","webpack://ibi-ai-talk/webpack/runtime/publicPath","webpack://ibi-ai-talk/./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js","webpack://ibi-ai-talk/./src/index.vue?93f4","webpack://ibi-ai-talk/./src/utils/opus-codec.js","webpack://ibi-ai-talk/./src/utils/blocking-queue.js","webpack://ibi-ai-talk/./src/utils/stream-context.js","webpack://ibi-ai-talk/./src/utils/player.js","webpack://ibi-ai-talk/./src/utils/tools.js","webpack://ibi-ai-talk/./src/utils/recorder.js","webpack://ibi-ai-talk/./src/utils/ota-connector.js","webpack://ibi-ai-talk/./src/utils/manager.js","webpack://ibi-ai-talk/./src/utils/websocket.js","webpack://ibi-ai-talk/./src/utils/controller.js","webpack://ibi-ai-talk/src/index.vue","webpack://ibi-ai-talk/./src/index.vue?ac85","webpack://ibi-ai-talk/./node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js","webpack://ibi-ai-talk/./src/index.vue","webpack://ibi-ai-talk/./node_modules/@vue/cli-service/lib/commands/build/entry-lib.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","__webpack_require__.p = \"\";","/* eslint-disable no-var */\n// This file is imported into lib/wc client bundles.\n\nif (typeof window !== 'undefined') {\n var currentScript = window.document.currentScript\n if (process.env.NEED_CURRENTSCRIPT_POLYFILL) {\n var getCurrentScript = require('@soda/get-current-script')\n currentScript = getCurrentScript()\n\n // for backward compatibility, because previously we directly included the polyfill\n if (!('currentScript' in document)) {\n Object.defineProperty(document, 'currentScript', { get: getCurrentScript })\n }\n }\n\n var src = currentScript && currentScript.src.match(/(.+\\/)[^/]+\\.js(\\?.*)?$/)\n if (src) {\n __webpack_public_path__ = src[1] // eslint-disable-line\n }\n}\n\n// Indicate to webpack that this file can be concatenated\nexport default null\n","var render = function render(){var _vm=this,_c=_vm._self._c;return _c(\"div\")\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","// 检查Opus库是否已加载\nexport function checkOpusLoaded() {\n try {\n // 检查Module是否存在(本地库导出的全局变量)\n if (typeof Module === 'undefined') {\n throw new Error('Opus库未加载,Module对象不存在');\n }\n\n // 尝试先使用Module.instance(libopus.js最后一行导出方式)\n if (typeof Module.instance !== 'undefined' && typeof Module.instance._opus_decoder_get_size === 'function') {\n // 使用Module.instance对象替换全局Module对象\n window.ModuleInstance = Module.instance;\n console.log('Opus库加载成功(使用Module.instance)', 'success');\n }\n\n // 如果没有Module.instance,检查全局Module函数\n if (typeof Module._opus_decoder_get_size === 'function') {\n window.ModuleInstance = Module;\n console.log('Opus库加载成功(使用全局Module)', 'success');\n }\n\n throw new Error('Opus解码函数未找到,可能Module结构不正确');\n } catch (err) {\n console.log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error');\n }\n}\n\n\n// 创建一个Opus编码器\nlet opusEncoder = null;\nexport function initOpusEncoder() {\n try {\n if (opusEncoder) {\n return opusEncoder; // 已经初始化过\n }\n\n if (!window.ModuleInstance) {\n console.log('无法创建Opus编码器:ModuleInstance不可用', 'error');\n return;\n }\n\n // 初始化一个Opus编码器\n const mod = window.ModuleInstance;\n const sampleRate = 16000; // 16kHz采样率\n const channels = 1; // 单声道\n const application = 2048; // OPUS_APPLICATION_VOIP = 2048\n\n // 创建编码器\n opusEncoder = {\n channels: channels,\n sampleRate: sampleRate,\n frameSize: 960, // 60ms @ 16kHz = 60 * 16 = 960 samples\n maxPacketSize: 4000, // 最大包大小\n module: mod,\n\n // 初始化编码器\n init: function () {\n try {\n // 获取编码器大小\n const encoderSize = mod._opus_encoder_get_size(this.channels);\n console.log(`Opus编码器大小: ${encoderSize}字节`, 'info');\n\n // 分配内存\n this.encoderPtr = mod._malloc(encoderSize);\n if (!this.encoderPtr) {\n throw new Error(\"无法分配编码器内存\");\n }\n\n // 初始化编码器\n const err = mod._opus_encoder_init(\n this.encoderPtr,\n this.sampleRate,\n this.channels,\n application\n );\n\n if (err < 0) {\n throw new Error(`Opus编码器初始化失败: ${err}`);\n }\n\n // 设置位率 (16kbps)\n mod._opus_encoder_ctl(this.encoderPtr, 4002, 16000); // OPUS_SET_BITRATE\n\n // 设置复杂度 (0-10, 越高质量越好但CPU使用越多)\n mod._opus_encoder_ctl(this.encoderPtr, 4010, 5); // OPUS_SET_COMPLEXITY\n\n // 设置使用DTX (不传输静音帧)\n mod._opus_encoder_ctl(this.encoderPtr, 4016, 1); // OPUS_SET_DTX\n\n console.log(\"Opus编码器初始化成功\", 'success');\n return true;\n } catch (error) {\n if (this.encoderPtr) {\n mod._free(this.encoderPtr);\n this.encoderPtr = null;\n }\n console.log(`Opus编码器初始化失败: ${error.message}`, 'error');\n return false;\n }\n },\n\n // 编码PCM数据为Opus\n encode: function (pcmData) {\n if (!this.encoderPtr) {\n if (!this.init()) {\n return null;\n }\n }\n\n try {\n const mod = this.module;\n\n // 为PCM数据分配内存\n const pcmPtr = mod._malloc(pcmData.length * 2); // 2字节/int16\n\n // 将PCM数据复制到HEAP\n for (let i = 0; i < pcmData.length; i++) {\n mod.HEAP16[(pcmPtr >> 1) + i] = pcmData[i];\n }\n\n // 为输出分配内存\n const outPtr = mod._malloc(this.maxPacketSize);\n\n // 进行编码\n const encodedLen = mod._opus_encode(\n this.encoderPtr,\n pcmPtr,\n this.frameSize,\n outPtr,\n this.maxPacketSize\n );\n\n if (encodedLen < 0) {\n throw new Error(`Opus编码失败: ${encodedLen}`);\n }\n\n // 复制编码后的数据\n const opusData = new Uint8Array(encodedLen);\n for (let i = 0; i < encodedLen; i++) {\n opusData[i] = mod.HEAPU8[outPtr + i];\n }\n\n // 释放内存\n mod._free(pcmPtr);\n mod._free(outPtr);\n\n return opusData;\n } catch (error) {\n console.log(`Opus编码出错: ${error.message}`, 'error');\n return null;\n }\n },\n\n // 销毁编码器\n destroy: function () {\n if (this.encoderPtr) {\n this.module._free(this.encoderPtr);\n this.encoderPtr = null;\n }\n }\n };\n\n opusEncoder.init();\n return opusEncoder;\n } catch (error) {\n console.log(`创建Opus编码器失败: ${error.message}`, 'error');\n return false;\n }\n}","export default class BlockingQueue {\n #items = [];\n #waiters = []; // {resolve, reject, min, timer, onTimeout}\n\n /* 空队列一次性闸门 */\n #emptyPromise = null;\n #emptyResolve = null;\n\n /* 生产者:把数据塞进去 */\n enqueue(item, ...restItems) {\n if (restItems.length === 0) {\n this.#items.push(item);\n }\n // 如果有额外参数,批量处理所有项\n else {\n const items = [item, ...restItems].filter(i => i);\n if (items.length === 0) return;\n this.#items.push(...items);\n }\n // 若有空队列闸门,一次性放行所有等待者\n if (this.#emptyResolve) {\n this.#emptyResolve();\n this.#emptyResolve = null;\n this.#emptyPromise = null;\n }\n\n // 唤醒所有正在等的 waiter\n this.#wakeWaiters();\n }\n\n /* 消费者:min 条或 timeout ms 先到谁 */\n async dequeue(min = 1, timeout = Infinity, onTimeout = null) {\n // 1. 若空,等第一次数据到达(所有调用共享同一个 promise)\n if (this.#items.length === 0) {\n await this.#waitForFirstItem();\n }\n\n // 立即满足\n if (this.#items.length >= min) {\n return this.#flush();\n }\n\n // 需要等待\n return new Promise((resolve, reject) => {\n let timer = null;\n const waiter = { resolve, reject, min, onTimeout, timer };\n\n // 超时逻辑\n if (Number.isFinite(timeout)) {\n waiter.timer = setTimeout(() => {\n this.#removeWaiter(waiter);\n if (onTimeout) onTimeout(this.#items.length);\n resolve(this.#flush());\n }, timeout);\n }\n\n this.#waiters.push(waiter);\n });\n }\n\n /* 空队列闸门生成器 */\n #waitForFirstItem() {\n if (!this.#emptyPromise) {\n this.#emptyPromise = new Promise(r => (this.#emptyResolve = r));\n }\n return this.#emptyPromise;\n }\n\n /* 内部:每次数据变动后,检查哪些 waiter 已满足 */\n #wakeWaiters() {\n for (let i = this.#waiters.length - 1; i >= 0; i--) {\n const w = this.#waiters[i];\n if (this.#items.length >= w.min) {\n this.#removeWaiter(w);\n w.resolve(this.#flush());\n }\n }\n }\n\n #removeWaiter(waiter) {\n const idx = this.#waiters.indexOf(waiter);\n if (idx !== -1) {\n this.#waiters.splice(idx, 1);\n if (waiter.timer) clearTimeout(waiter.timer);\n }\n }\n\n #flush() {\n const snapshot = [...this.#items];\n this.#items.length = 0;\n return snapshot;\n }\n\n /* 当前缓存长度(不含等待者) */\n get length() {\n return this.#items.length;\n }\n}","import BlockingQueue from \"./blocking-queue.js\";\n\n// 音频流播放上下文类\nexport class StreamingContext {\n constructor(\n opusDecoder,\n audioContext,\n sampleRate,\n channels,\n minAudioDuration\n ) {\n this.opusDecoder = opusDecoder;\n this.audioContext = audioContext;\n\n // 音频参数\n this.sampleRate = sampleRate;\n this.channels = channels;\n this.minAudioDuration = minAudioDuration;\n\n // 初始化队列和状态\n this.queue = []; // 已解码的PCM队列。正在播放\n this.activeQueue = new BlockingQueue(); // 已解码的PCM队列。准备播放\n this.pendingAudioBufferQueue = []; // 待处理的缓存队列\n this.audioBufferQueue = new BlockingQueue(); // 缓存队列\n this.playing = false; // 是否正在播放\n this.endOfStream = false; // 是否收到结束信号\n this.source = null; // 当前音频源\n this.totalSamples = 0; // 累积的总样本数\n this.lastPlayTime = 0; // 上次播放的时间戳\n }\n\n // 缓存音频数组\n pushAudioBuffer(item) {\n this.audioBufferQueue.enqueue(...item);\n }\n\n // 获取需要处理缓存队列,单线程:在audioBufferQueue一直更新的状态下不会出现安全问题\n async getPendingAudioBufferQueue() {\n // 原子交换 + 清空\n [this.pendingAudioBufferQueue, this.audioBufferQueue] = [\n await this.audioBufferQueue.dequeue(),\n new BlockingQueue(),\n ];\n }\n\n // 获取正在播放已解码的PCM队列,单线程:在activeQueue一直更新的状态下不会出现安全问题\n async getQueue(minSamples) {\n let TepArray = [];\n const num =\n minSamples - this.queue.length > 0 ? minSamples - this.queue.length : 1;\n // 原子交换 + 清空\n [TepArray, this.activeQueue] = [\n await this.activeQueue.dequeue(num),\n new BlockingQueue(),\n ];\n this.queue.push(...TepArray);\n }\n\n // 将Int16音频数据转换为Float32音频数据\n convertInt16ToFloat32(int16Data) {\n const float32Data = new Float32Array(int16Data.length);\n for (let i = 0; i < int16Data.length; i++) {\n // 将[-32768,32767]范围转换为[-1,1],统一使用32768.0避免不对称失真\n float32Data[i] = int16Data[i] / 32768.0;\n }\n return float32Data;\n }\n\n // 将Opus数据解码为PCM\n async decodeOpusFrames() {\n if (!this.opusDecoder) {\n console.log(\"Opus解码器未初始化,无法解码\", \"error\");\n return;\n } else {\n console.log(\"Opus解码器启动\", \"info\");\n }\n\n while (true) {\n let decodedSamples = [];\n for (const frame of this.pendingAudioBufferQueue) {\n try {\n // 使用Opus解码器解码\n const frameData = this.opusDecoder.decode(frame);\n if (frameData && frameData.length > 0) {\n // 转换为Float32\n const floatData = this.convertInt16ToFloat32(frameData);\n // 使用循环替代展开运算符\n for (let i = 0; i < floatData.length; i++) {\n decodedSamples.push(floatData[i]);\n }\n }\n } catch (error) {\n console.log(\"Opus解码失败: \" + error.message, \"error\");\n }\n }\n\n if (decodedSamples.length > 0) {\n // 使用循环替代展开运算符\n for (let i = 0; i < decodedSamples.length; i++) {\n this.activeQueue.enqueue(decodedSamples[i]);\n }\n this.totalSamples += decodedSamples.length;\n } else {\n console.log(\"没有成功解码的样本\", \"warning\");\n }\n await this.getPendingAudioBufferQueue();\n }\n }\n\n // 开始播放音频\n async startPlaying() {\n let scheduledEndTime = this.audioContext.currentTime; // 跟踪已调度音频的结束时间\n\n while (true) {\n // 初始缓冲:等待足够的样本再开始播放\n const minSamples = this.sampleRate * this.minAudioDuration * 2;\n if (!this.playing && this.queue.length < minSamples) {\n await this.getQueue(minSamples);\n }\n this.playing = true;\n\n // 持续播放队列中的音频,每次播放一个小块\n while (this.playing && this.queue.length > 0) {\n // 每次播放120ms的音频(2个Opus包)\n const playDuration = 0.12;\n const targetSamples = Math.floor(this.sampleRate * playDuration);\n const actualSamples = Math.min(this.queue.length, targetSamples);\n\n if (actualSamples === 0) break;\n\n const currentSamples = this.queue.splice(0, actualSamples);\n const audioBuffer = this.audioContext.createBuffer(\n this.channels,\n currentSamples.length,\n this.sampleRate\n );\n audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);\n\n // 创建音频源\n this.source = this.audioContext.createBufferSource();\n this.source.buffer = audioBuffer;\n\n // 精确调度播放时间\n const currentTime = this.audioContext.currentTime;\n const startTime = Math.max(scheduledEndTime, currentTime);\n\n // 直接连接到输出\n this.source.connect(this.audioContext.destination);\n\n this.source.start(startTime);\n\n // 更新下一个音频块的调度时间\n const duration = audioBuffer.duration;\n scheduledEndTime = startTime + duration;\n this.lastPlayTime = startTime;\n\n // 如果队列中数据不足,等待新数据\n if (this.queue.length < targetSamples) {\n break;\n }\n }\n\n // 等待新数据\n await this.getQueue(minSamples);\n }\n }\n}\n\n// 创建streamingContext实例的工厂函数\nexport function createStreamingContext(\n opusDecoder,\n audioContext,\n sampleRate,\n channels,\n minAudioDuration\n) {\n return new StreamingContext(\n opusDecoder,\n audioContext,\n sampleRate,\n channels,\n minAudioDuration\n );\n}\n","// 音频播放模块\nimport BlockingQueue from \"./blocking-queue.js\";\nimport { createStreamingContext } from \"./stream-context.js\";\n\n// 音频播放器类\nexport class AudioPlayer {\n constructor() {\n // 音频参数\n this.SAMPLE_RATE = 16000;\n this.CHANNELS = 1;\n this.FRAME_SIZE = 960;\n this.MIN_AUDIO_DURATION = 0.12;\n\n // 状态\n this.audioContext = null;\n this.opusDecoder = null;\n this.streamingContext = null;\n this.queue = new BlockingQueue();\n this.isPlaying = false;\n }\n\n // 获取或创建AudioContext\n getAudioContext() {\n if (!this.audioContext) {\n this.audioContext = new (window.AudioContext ||\n window.webkitAudioContext)({\n sampleRate: this.SAMPLE_RATE,\n latencyHint: \"interactive\",\n });\n console.log(\n \"创建音频上下文,采样率: \" + this.SAMPLE_RATE + \"Hz\",\n \"debug\"\n );\n }\n return this.audioContext;\n }\n\n // 初始化Opus解码器\n async initOpusDecoder() {\n if (this.opusDecoder) return this.opusDecoder;\n\n try {\n if (typeof window.ModuleInstance === \"undefined\") {\n if (typeof Module !== \"undefined\") {\n window.ModuleInstance = Module;\n console.log(\"使用全局Module作为ModuleInstance\", \"info\");\n } else {\n throw new Error(\"Opus库未加载,ModuleInstance和Module对象都不存在\");\n }\n }\n\n const mod = window.ModuleInstance;\n\n this.opusDecoder = {\n channels: this.CHANNELS,\n rate: this.SAMPLE_RATE,\n frameSize: this.FRAME_SIZE,\n module: mod,\n decoderPtr: null,\n\n init: function () {\n if (this.decoderPtr) return true;\n\n const decoderSize = mod._opus_decoder_get_size(this.channels);\n console.log(`Opus解码器大小: ${decoderSize}字节`, \"debug\");\n\n this.decoderPtr = mod._malloc(decoderSize);\n if (!this.decoderPtr) {\n throw new Error(\"无法分配解码器内存\");\n }\n\n const err = mod._opus_decoder_init(\n this.decoderPtr,\n this.rate,\n this.channels\n );\n\n if (err < 0) {\n this.destroy();\n throw new Error(`Opus解码器初始化失败: ${err}`);\n }\n\n console.log(\"Opus解码器初始化成功\", \"success\");\n return true;\n },\n\n decode: function (opusData) {\n if (!this.decoderPtr) {\n if (!this.init()) {\n throw new Error(\"解码器未初始化且无法初始化\");\n }\n }\n\n try {\n const mod = this.module;\n\n const opusPtr = mod._malloc(opusData.length);\n mod.HEAPU8.set(opusData, opusPtr);\n\n const pcmPtr = mod._malloc(this.frameSize * 2);\n\n const decodedSamples = mod._opus_decode(\n this.decoderPtr,\n opusPtr,\n opusData.length,\n pcmPtr,\n this.frameSize,\n 0\n );\n\n if (decodedSamples < 0) {\n mod._free(opusPtr);\n mod._free(pcmPtr);\n throw new Error(`Opus解码失败: ${decodedSamples}`);\n }\n\n const decodedData = new Int16Array(decodedSamples);\n for (let i = 0; i < decodedSamples; i++) {\n decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];\n }\n\n mod._free(opusPtr);\n mod._free(pcmPtr);\n\n return decodedData;\n } catch (error) {\n console.log(`Opus解码错误: ${error.message}`, \"error\");\n return new Int16Array(0);\n }\n },\n\n destroy: function () {\n if (this.decoderPtr) {\n this.module._free(this.decoderPtr);\n this.decoderPtr = null;\n }\n },\n };\n\n if (!this.opusDecoder.init()) {\n throw new Error(\"Opus解码器初始化失败\");\n }\n\n return this.opusDecoder;\n } catch (error) {\n console.log(`Opus解码器初始化失败: ${error.message}`, \"error\");\n this.opusDecoder = null;\n throw error;\n }\n }\n\n // 启动音频缓冲\n async startAudioBuffering() {\n console.log(\"开始音频缓冲...\", \"info\");\n\n this.initOpusDecoder().catch((error) => {\n console.log(`预初始化Opus解码器失败: ${error.message}`, \"warning\");\n });\n\n const timeout = 400;\n while (true) {\n const packets = await this.queue.dequeue(6, timeout, (count) => {\n console.log(`缓冲超时,当前缓冲包数: ${count},开始播放`, \"info\");\n });\n if (packets.length) {\n console.log(`已缓冲 ${packets.length} 个音频包,开始播放`, \"info\");\n this.streamingContext.pushAudioBuffer(packets);\n }\n\n while (true) {\n const data = await this.queue.dequeue(99, 30);\n if (data.length) {\n this.streamingContext.pushAudioBuffer(data);\n } else {\n break;\n }\n }\n }\n }\n\n // 播放已缓冲的音频\n async playBufferedAudio() {\n try {\n this.audioContext = this.getAudioContext();\n\n if (!this.opusDecoder) {\n console.log(\"初始化Opus解码器...\", \"info\");\n try {\n this.opusDecoder = await this.initOpusDecoder();\n if (!this.opusDecoder) {\n throw new Error(\"解码器初始化失败\");\n }\n console.log(\"Opus解码器初始化成功\", \"success\");\n } catch (error) {\n console.log(\"Opus解码器初始化失败: \" + error.message, \"error\");\n this.isPlaying = false;\n return;\n }\n }\n\n if (!this.streamingContext) {\n this.streamingContext = createStreamingContext(\n this.opusDecoder,\n this.audioContext,\n this.SAMPLE_RATE,\n this.CHANNELS,\n this.MIN_AUDIO_DURATION\n );\n }\n\n this.streamingContext.decodeOpusFrames();\n this.streamingContext.startPlaying();\n } catch (error) {\n console.log(`播放已缓冲的音频出错: ${error.message}`, \"error\");\n this.isPlaying = false;\n this.streamingContext = null;\n }\n }\n\n // 添加音频数据到队列\n enqueueAudioData(opusData) {\n if (opusData.length > 0) {\n this.queue.enqueue(opusData);\n } else {\n console.log(\"收到空音频数据帧,可能是结束标志\", \"warning\");\n if (this.isPlaying && this.streamingContext) {\n this.streamingContext.endOfStream = true;\n }\n }\n }\n\n // 预加载解码器\n async preload() {\n console.log(\"预加载Opus解码器...\", \"info\");\n try {\n await this.initOpusDecoder();\n console.log(\"Opus解码器预加载成功\", \"success\");\n } catch (error) {\n console.log(\n `Opus解码器预加载失败: ${error.message},将在需要时重试`,\n \"warning\"\n );\n }\n }\n\n // 启动播放系统\n async start() {\n await this.preload();\n this.playBufferedAudio();\n this.startAudioBuffering();\n }\n}\n\n// 创建单例\nlet audioPlayerInstance = null;\n\nexport function getAudioPlayer() {\n if (!audioPlayerInstance) {\n audioPlayerInstance = new AudioPlayer();\n }\n return audioPlayerInstance;\n}\n","// 全局变量\nlet mcpTools = [];\nlet mcpEditingIndex = null;\nlet mcpProperties = [];\nlet websocket = null; // 将从外部设置\n\n/**\n * 设置 WebSocket 实例\n * @param {WebSocket} ws - WebSocket 连接实例\n */\nexport function setWebSocket(ws) {\n websocket = ws;\n}\n\n/**\n * 初始化 MCP 工具\n */\nexport async function initMcpTools() {\n // 加载默认工具数据\n const defaultMcpTools = await fetch(\"/default-mcp-tools.json\").then((res) =>\n res.json()\n );\n\n const savedTools = localStorage.getItem(\"mcpTools\");\n if (savedTools) {\n try {\n mcpTools = JSON.parse(savedTools);\n } catch (e) {\n console.log(\"加载MCP工具失败,使用默认工具\", \"warning\");\n mcpTools = [...defaultMcpTools];\n }\n } else {\n mcpTools = [...defaultMcpTools];\n }\n\n // renderMcpTools();\n // setupMcpEventListeners();\n}\n\n/**\n * 渲染工具列表\n */\nfunction renderMcpTools() {\n const container = document.getElementById(\"mcpToolsContainer\");\n const countSpan = document.getElementById(\"mcpToolsCount\");\n\n countSpan.textContent = `${mcpTools.length} 个工具`;\n\n if (mcpTools.length === 0) {\n container.innerHTML =\n '<div style=\"text-align: center; padding: 30px; color: #999;\">暂无工具,点击下方按钮添加新工具</div>';\n return;\n }\n\n container.innerHTML = mcpTools\n .map((tool, index) => {\n const paramCount = tool.inputSchema.properties\n ? Object.keys(tool.inputSchema.properties).length\n : 0;\n const requiredCount = tool.inputSchema.required\n ? tool.inputSchema.required.length\n : 0;\n const hasMockResponse =\n tool.mockResponse && Object.keys(tool.mockResponse).length > 0;\n\n return `\n <div class=\"mcp-tool-card\">\n <div class=\"mcp-tool-header\">\n <div class=\"mcp-tool-name\">${tool.name}</div>\n <div class=\"mcp-tool-actions\">\n <button onclick=\"window.mcpModule.editMcpTool(${index})\"\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(${index})\"\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\">${tool.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\">${paramCount} 个 ${\n requiredCount > 0 ? `(${requiredCount} 个必填)` : \"\"\n }</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\">${\n hasMockResponse\n ? \"✅ 已配置: \" + JSON.stringify(tool.mockResponse)\n : \"⚪ 使用默认\"\n }</span>\n </div>\n </div>\n </div>\n `;\n })\n .join(\"\");\n}\n\n/**\n * 渲染参数列表\n */\nfunction renderMcpProperties() {\n const container = document.getElementById(\"mcpPropertiesContainer\");\n\n if (mcpProperties.length === 0) {\n container.innerHTML =\n '<div style=\"text-align: center; padding: 20px; color: #999; font-size: 14px;\">暂无参数,点击下方按钮添加参数</div>';\n return;\n }\n\n container.innerHTML = mcpProperties\n .map(\n (prop, index) => `\n <div class=\"mcp-property-item\">\n <div class=\"mcp-property-header\">\n <span class=\"mcp-property-name\">${prop.name}</span>\n <button type=\"button\" onclick=\"window.mcpModule.deleteMcpProperty(${index})\"\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=\"${\n prop.name\n }\"\n onchange=\"window.mcpModule.updateMcpProperty(${index}, '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(${index}, 'type', this.value)\">\n <option value=\"string\" ${\n prop.type === \"string\" ? \"selected\" : \"\"\n }>字符串</option>\n <option value=\"integer\" ${\n prop.type === \"integer\" ? \"selected\" : \"\"\n }>整数</option>\n <option value=\"number\" ${\n prop.type === \"number\" ? \"selected\" : \"\"\n }>数字</option>\n <option value=\"boolean\" ${\n prop.type === \"boolean\" ? \"selected\" : \"\"\n }>布尔值</option>\n <option value=\"array\" ${\n prop.type === \"array\" ? \"selected\" : \"\"\n }>数组</option>\n <option value=\"object\" ${\n prop.type === \"object\" ? \"selected\" : \"\"\n }>对象</option>\n </select>\n </div>\n </div>\n ${\n prop.type === \"integer\" || prop.type === \"number\"\n ? `\n <div class=\"mcp-property-row\">\n <div>\n <label class=\"mcp-small-label\">最小值</label>\n <input type=\"number\" class=\"mcp-small-input\" value=\"${\n prop.minimum !== undefined ? prop.minimum : \"\"\n }\"\n placeholder=\"可选\" onchange=\"window.mcpModule.updateMcpProperty(${index}, '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=\"${\n prop.maximum !== undefined ? prop.maximum : \"\"\n }\"\n placeholder=\"可选\" onchange=\"window.mcpModule.updateMcpProperty(${index}, 'maximum', this.value ? parseFloat(this.value) : undefined)\">\n </div>\n </div>\n `\n : \"\"\n }\n <div class=\"mcp-property-row-full\">\n <label class=\"mcp-small-label\">参数描述</label>\n <input type=\"text\" class=\"mcp-small-input\" value=\"${\n prop.description || \"\"\n }\"\n placeholder=\"可选\" onchange=\"window.mcpModule.updateMcpProperty(${index}, 'description', this.value)\">\n </div>\n <label class=\"mcp-checkbox-label\">\n <input type=\"checkbox\" ${prop.required ? \"checked\" : \"\"}\n onchange=\"window.mcpModule.updateMcpProperty(${index}, 'required', this.checked)\">\n 必填参数\n </label>\n </div>\n `\n )\n .join(\"\");\n}\n\n/**\n * 添加参数\n */\nfunction addMcpProperty() {\n mcpProperties.push({\n name: `param_${mcpProperties.length + 1}`,\n type: \"string\",\n required: false,\n description: \"\",\n });\n renderMcpProperties();\n}\n\n/**\n * 更新参数\n */\nfunction updateMcpProperty(index, field, value) {\n if (field === \"name\") {\n const isDuplicate = mcpProperties.some(\n (p, i) => i !== index && p.name === value\n );\n if (isDuplicate) {\n alert(\"参数名称已存在,请使用不同的名称\");\n renderMcpProperties();\n return;\n }\n }\n\n mcpProperties[index][field] = value;\n\n if (field === \"type\" && value !== \"integer\" && value !== \"number\") {\n delete mcpProperties[index].minimum;\n delete mcpProperties[index].maximum;\n renderMcpProperties();\n }\n}\n\n/**\n * 删除参数\n */\nfunction deleteMcpProperty(index) {\n mcpProperties.splice(index, 1);\n renderMcpProperties();\n}\n\n/**\n * 设置事件监听\n */\nfunction setupMcpEventListeners() {\n const toggleBtn = document.getElementById(\"toggleMcpTools\");\n const panel = document.getElementById(\"mcpToolsPanel\");\n const addBtn = document.getElementById(\"addMcpToolBtn\");\n const modal = document.getElementById(\"mcpToolModal\");\n const closeBtn = document.getElementById(\"closeMcpModalBtn\");\n const cancelBtn = document.getElementById(\"cancelMcpBtn\");\n const form = document.getElementById(\"mcpToolForm\");\n const addPropertyBtn = document.getElementById(\"addMcpPropertyBtn\");\n\n toggleBtn.addEventListener(\"click\", () => {\n const isExpanded = panel.classList.contains(\"expanded\");\n panel.classList.toggle(\"expanded\");\n toggleBtn.textContent = isExpanded ? \"展开\" : \"收起\";\n });\n\n addBtn.addEventListener(\"click\", () => openMcpModal());\n closeBtn.addEventListener(\"click\", closeMcpModal);\n cancelBtn.addEventListener(\"click\", closeMcpModal);\n addPropertyBtn.addEventListener(\"click\", addMcpProperty);\n\n modal.addEventListener(\"click\", (e) => {\n if (e.target === modal) closeMcpModal();\n });\n\n form.addEventListener(\"submit\", handleMcpSubmit);\n}\n\n/**\n * 打开模态框\n */\nfunction openMcpModal(index = null) {\n const isConnected = websocket && websocket.readyState === WebSocket.OPEN;\n if (isConnected) {\n alert(\"WebSocket 已连接,无法编辑工具\");\n return;\n }\n\n mcpEditingIndex = index;\n const errorContainer = document.getElementById(\"mcpErrorContainer\");\n errorContainer.innerHTML = \"\";\n\n if (index !== null) {\n document.getElementById(\"mcpModalTitle\").textContent = \"编辑工具\";\n const tool = mcpTools[index];\n document.getElementById(\"mcpToolName\").value = tool.name;\n document.getElementById(\"mcpToolDescription\").value = tool.description;\n document.getElementById(\"mcpMockResponse\").value = tool.mockResponse\n ? JSON.stringify(tool.mockResponse, null, 2)\n : \"\";\n\n mcpProperties = [];\n const schema = tool.inputSchema;\n if (schema.properties) {\n Object.keys(schema.properties).forEach((key) => {\n const prop = schema.properties[key];\n mcpProperties.push({\n name: key,\n type: prop.type || \"string\",\n minimum: prop.minimum,\n maximum: prop.maximum,\n description: prop.description || \"\",\n required: schema.required && schema.required.includes(key),\n });\n });\n }\n } else {\n document.getElementById(\"mcpModalTitle\").textContent = \"添加工具\";\n document.getElementById(\"mcpToolForm\").reset();\n mcpProperties = [];\n }\n\n renderMcpProperties();\n document.getElementById(\"mcpToolModal\").style.display = \"block\";\n}\n\n/**\n * 关闭模态框\n */\nfunction closeMcpModal() {\n document.getElementById(\"mcpToolModal\").style.display = \"none\";\n mcpEditingIndex = null;\n document.getElementById(\"mcpToolForm\").reset();\n mcpProperties = [];\n document.getElementById(\"mcpErrorContainer\").innerHTML = \"\";\n}\n\n/**\n * 处理表单提交\n */\nfunction handleMcpSubmit(e) {\n e.preventDefault();\n const errorContainer = document.getElementById(\"mcpErrorContainer\");\n errorContainer.innerHTML = \"\";\n\n const name = document.getElementById(\"mcpToolName\").value.trim();\n const description = document\n .getElementById(\"mcpToolDescription\")\n .value.trim();\n const mockResponseText = document\n .getElementById(\"mcpMockResponse\")\n .value.trim();\n\n // 检查名称重复\n const isDuplicate = mcpTools.some(\n (tool, index) => tool.name === name && index !== mcpEditingIndex\n );\n\n if (isDuplicate) {\n showMcpError(\"工具名称已存在,请使用不同的名称\");\n return;\n }\n\n // 解析模拟返回结果\n let mockResponse = null;\n if (mockResponseText) {\n try {\n mockResponse = JSON.parse(mockResponseText);\n } catch (e) {\n showMcpError(\"模拟返回结果不是有效的 JSON 格式: \" + e.message);\n return;\n }\n }\n\n // 构建 inputSchema\n const inputSchema = {\n type: \"object\",\n properties: {},\n required: [],\n };\n\n mcpProperties.forEach((prop) => {\n const propSchema = { type: prop.type };\n\n if (prop.description) {\n propSchema.description = prop.description;\n }\n\n if (prop.type === \"integer\" || prop.type === \"number\") {\n if (prop.minimum !== undefined && prop.minimum !== \"\") {\n propSchema.minimum = prop.minimum;\n }\n if (prop.maximum !== undefined && prop.maximum !== \"\") {\n propSchema.maximum = prop.maximum;\n }\n }\n\n inputSchema.properties[prop.name] = propSchema;\n\n if (prop.required) {\n inputSchema.required.push(prop.name);\n }\n });\n\n if (inputSchema.required.length === 0) {\n delete inputSchema.required;\n }\n\n const tool = { name, description, inputSchema, mockResponse };\n\n if (mcpEditingIndex !== null) {\n mcpTools[mcpEditingIndex] = tool;\n console.log(`已更新工具: ${name}`, \"success\");\n } else {\n mcpTools.push(tool);\n console.log(`已添加工具: ${name}`, \"success\");\n }\n\n saveMcpTools();\n renderMcpTools();\n closeMcpModal();\n}\n\n/**\n * 显示错误\n */\nfunction showMcpError(message) {\n const errorContainer = document.getElementById(\"mcpErrorContainer\");\n errorContainer.innerHTML = `<div class=\"mcp-error\">${message}</div>`;\n}\n\n/**\n * 编辑工具\n */\nfunction editMcpTool(index) {\n openMcpModal(index);\n}\n\n/**\n * 删除工具\n */\nfunction deleteMcpTool(index) {\n const isConnected = websocket && websocket.readyState === WebSocket.OPEN;\n if (isConnected) {\n alert(\"WebSocket 已连接,无法编辑工具\");\n return;\n }\n if (confirm(`确定要删除工具 \"${mcpTools[index].name}\" 吗?`)) {\n const toolName = mcpTools[index].name;\n mcpTools.splice(index, 1);\n saveMcpTools();\n renderMcpTools();\n console.log(`已删除工具: ${toolName}`, \"info\");\n }\n}\n\n/**\n * 保存工具\n */\nfunction saveMcpTools() {\n localStorage.setItem(\"mcpTools\", JSON.stringify(mcpTools));\n}\n\n/**\n * 获取工具列表\n */\nexport function getMcpTools() {\n return mcpTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n }));\n}\n\n/**\n * 执行工具调用\n */\nexport function executeMcpTool(toolName, toolArgs) {\n const tool = mcpTools.find((t) => t.name === toolName);\n if (!tool) {\n console.log(`未找到工具: ${toolName}`, \"error\");\n return {\n success: false,\n error: `未知工具: ${toolName}`,\n };\n }\n\n if (tool.name == \"self.drink_car_list\") {\n console.log(\"准备触发 gotoOrderEvent 事件\"); // 增加此日志\n const event = new CustomEvent(\"gotoOrderEvent\");\n window.dispatchEvent(event);\n return {\n success: true,\n message: `工具 ${toolName} 执行成功`,\n data: sessionStorage.getItem('cartList') || [],\n };\n } else if (tool.name == \"self.drink_car_reset\") {\n console.log(\"准备触发 resetOrderEvent 事件\"); // 增加此日志\n const event = new CustomEvent(\"resetOrderEvent\", {\n detail: toolArgs,\n });\n window.dispatchEvent(event);\n return {\n success: true,\n message: `工具 ${toolName} 执行成功`,\n data: sessionStorage.getItem('cartList') || [],\n }\n } else if (tool.name == \"self.drink_order\") {\n console.log(\"准备触发 orderEvent 事件\"); // 增加此日志\n const event = new CustomEvent(\"orderEvent\");\n window.dispatchEvent(event);\n }\n}\n\n// 暴露全局方法供 HTML 内联事件调用\nwindow.mcpModule = {\n updateMcpProperty,\n deleteMcpProperty,\n editMcpTool,\n deleteMcpTool,\n};\n","// 音频录制模块\r\nimport { initOpusEncoder } from \"./opus-codec.js\";\r\nimport { getAudioPlayer } from \"./player.js\";\r\n\r\n// 音频录制器类\r\nexport class AudioRecorder {\r\n constructor() {\r\n this.isRecording = false;\r\n this.audioContext = null;\r\n this.analyser = null;\r\n this.audioProcessor = null;\r\n this.audioProcessorType = null;\r\n this.audioSource = null;\r\n this.opusEncoder = null;\r\n this.pcmDataBuffer = new Int16Array();\r\n this.audioBuffers = [];\r\n this.totalAudioSize = 0;\r\n this.visualizationRequest = null;\r\n this.recordingTimer = null;\r\n this.websocket = null;\r\n\r\n // 回调函数\r\n this.onRecordingStart = null;\r\n this.onRecordingStop = null;\r\n this.onVisualizerUpdate = null;\r\n }\r\n\r\n // 设置WebSocket实例\r\n setWebSocket(ws) {\r\n this.websocket = ws;\r\n }\r\n\r\n // 获取AudioContext实例\r\n getAudioContext() {\r\n const audioPlayer = getAudioPlayer();\r\n return audioPlayer.getAudioContext();\r\n }\r\n\r\n // 初始化编码器\r\n initEncoder() {\r\n if (!this.opusEncoder) {\r\n this.opusEncoder = initOpusEncoder();\r\n }\r\n return this.opusEncoder;\r\n }\r\n\r\n // PCM处理器代码\r\n getAudioProcessorCode() {\r\n return `\r\n class AudioRecorderProcessor extends AudioWorkletProcessor {\r\n constructor() {\r\n super();\r\n this.buffers = [];\r\n this.frameSize = 960;\r\n this.buffer = new Int16Array(this.frameSize);\r\n this.bufferIndex = 0;\r\n this.isRecording = false;\r\n\r\n this.port.onmessage = (event) => {\r\n if (event.data.command === 'start') {\r\n this.isRecording = true;\r\n this.port.postMessage({ type: 'status', status: 'started' });\r\n } else if (event.data.command === 'stop') {\r\n this.isRecording = false;\r\n\r\n if (this.bufferIndex > 0) {\r\n const finalBuffer = this.buffer.slice(0, this.bufferIndex);\r\n this.port.postMessage({\r\n type: 'buffer',\r\n buffer: finalBuffer\r\n });\r\n this.bufferIndex = 0;\r\n }\r\n\r\n this.port.postMessage({ type: 'status', status: 'stopped' });\r\n }\r\n };\r\n }\r\n\r\n process(inputs, outputs, parameters) {\r\n if (!this.isRecording) return true;\r\n\r\n const input = inputs[0][0];\r\n if (!input) return true;\r\n\r\n for (let i = 0; i < input.length; i++) {\r\n if (this.bufferIndex >= this.frameSize) {\r\n this.port.postMessage({\r\n type: 'buffer',\r\n buffer: this.buffer.slice(0)\r\n });\r\n this.bufferIndex = 0;\r\n }\r\n\r\n this.buffer[this.bufferIndex++] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767)));\r\n }\r\n\r\n return true;\r\n }\r\n }\r\n\r\n registerProcessor('audio-recorder-processor', AudioRecorderProcessor);\r\n `;\r\n }\r\n\r\n // 创建音频处理器\r\n async createAudioProcessor() {\r\n this.audioContext = this.getAudioContext();\r\n\r\n try {\r\n if (this.audioContext.audioWorklet) {\r\n const blob = new Blob([this.getAudioProcessorCode()], {\r\n type: \"application/javascript\",\r\n });\r\n const url = URL.createObjectURL(blob);\r\n await this.audioContext.audioWorklet.addModule(url);\r\n URL.revokeObjectURL(url);\r\n\r\n const audioProcessor = new AudioWorkletNode(\r\n this.audioContext,\r\n \"audio-recorder-processor\"\r\n );\r\n\r\n audioProcessor.port.onmessage = (event) => {\r\n if (event.data.type === \"buffer\") {\r\n this.processPCMBuffer(event.data.buffer);\r\n }\r\n };\r\n\r\n console.log(\"使用AudioWorklet处理音频\", \"success\");\r\n\r\n const silent = this.audioContext.createGain();\r\n silent.gain.value = 0;\r\n audioProcessor.connect(silent);\r\n silent.connect(this.audioContext.destination);\r\n return { node: audioProcessor, type: \"worklet\" };\r\n } else {\r\n console.log(\r\n \"AudioWorklet不可用,使用ScriptProcessorNode作为回退方案\",\r\n \"warning\"\r\n );\r\n return this.createScriptProcessor();\r\n }\r\n } catch (error) {\r\n console.log(\r\n `创建音频处理器失败: ${error.message},尝试回退方案`,\r\n \"error\"\r\n );\r\n return this.createScriptProcessor();\r\n }\r\n }\r\n\r\n // 创建ScriptProcessor作为回退\r\n createScriptProcessor() {\r\n try {\r\n const frameSize = 4096;\r\n const scriptProcessor = this.audioContext.createScriptProcessor(\r\n frameSize,\r\n 1,\r\n 1\r\n );\r\n\r\n scriptProcessor.onaudioprocess = (event) => {\r\n if (!this.isRecording) return;\r\n\r\n const input = event.inputBuffer.getChannelData(0);\r\n const buffer = new Int16Array(input.length);\r\n\r\n for (let i = 0; i < input.length; i++) {\r\n buffer[i] = Math.max(\r\n -32768,\r\n Math.min(32767, Math.floor(input[i] * 32767))\r\n );\r\n }\r\n\r\n this.processPCMBuffer(buffer);\r\n };\r\n\r\n const silent = this.audioContext.createGain();\r\n silent.gain.value = 0;\r\n scriptProcessor.connect(silent);\r\n silent.connect(this.audioContext.destination);\r\n\r\n console.log(\"使用ScriptProcessorNode作为回退方案成功\", \"warning\");\r\n return { node: scriptProcessor, type: \"processor\" };\r\n } catch (fallbackError) {\r\n console.log(`回退方案也失败: ${fallbackError.message}`, \"error\");\r\n return null;\r\n }\r\n }\r\n\r\n // 处理PCM缓冲数据\r\n processPCMBuffer(buffer) {\r\n if (!this.isRecording) return;\r\n\r\n const newBuffer = new Int16Array(this.pcmDataBuffer.length + buffer.length);\r\n newBuffer.set(this.pcmDataBuffer);\r\n newBuffer.set(buffer, this.pcmDataBuffer.length);\r\n this.pcmDataBuffer = newBuffer;\r\n\r\n const samplesPerFrame = 960;\r\n\r\n while (this.pcmDataBuffer.length >= samplesPerFrame) {\r\n const frameData = this.pcmDataBuffer.slice(0, samplesPerFrame);\r\n this.pcmDataBuffer = this.pcmDataBuffer.slice(samplesPerFrame);\r\n\r\n this.encodeAndSendOpus(frameData);\r\n }\r\n }\r\n\r\n // 编码并发送Opus数据\r\n encodeAndSendOpus(pcmData = null) {\r\n if (!this.opusEncoder) {\r\n console.log(\"Opus编码器未初始化\", \"error\");\r\n return;\r\n }\r\n\r\n try {\r\n if (pcmData) {\r\n const opusData = this.opusEncoder.encode(pcmData);\r\n if (opusData && opusData.length > 0) {\r\n this.audioBuffers.push(opusData.buffer);\r\n this.totalAudioSize += opusData.length;\r\n\r\n if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {\r\n try {\r\n this.websocket.send(opusData.buffer);\r\n } catch (error) {\r\n console.log(`WebSocket发送错误: ${error.message}`, \"error\");\r\n }\r\n }\r\n } else {\r\n log(\"Opus编码失败,无有效数据返回\", \"error\");\r\n }\r\n } else {\r\n if (this.pcmDataBuffer.length > 0) {\r\n const samplesPerFrame = 960;\r\n if (this.pcmDataBuffer.length < samplesPerFrame) {\r\n const paddedBuffer = new Int16Array(samplesPerFrame);\r\n paddedBuffer.set(this.pcmDataBuffer);\r\n this.encodeAndSendOpus(paddedBuffer);\r\n } else {\r\n this.encodeAndSendOpus(\r\n this.pcmDataBuffer.slice(0, samplesPerFrame)\r\n );\r\n }\r\n this.pcmDataBuffer = new Int16Array(0);\r\n }\r\n }\r\n } catch (error) {\r\n console.log(`Opus编码错误: ${error.message}`, \"error\");\r\n }\r\n }\r\n\r\n // 开始录音\r\n async start() {\r\n try {\r\n if (!this.initEncoder()) {\r\n console.log(\"无法启动录音: Opus编码器初始化失败\", \"error\");\r\n return false;\r\n }\r\n\r\n const stream = await navigator.mediaDevices.getUserMedia({\r\n audio: {\r\n echoCancellation: true,\r\n noiseSuppression: true,\r\n sampleRate: 16000,\r\n channelCount: 1,\r\n latency: { ideal: 0.02, max: 0.05 },\r\n // Chrome 扩展参数(非标准,可能变动)\r\n googNoiseSuppression: true, // 启用 Chrome 噪声抑制\r\n googNoiseSuppression2: 3, // 级别设置(1-3,数值越高抑制越强,不同版本可能有差异)\r\n googAutoGainControl: true, // 自动增益控制\r\n googHighpassFilter: true, // 高通滤波器(过滤低频噪声)\r\n },\r\n });\r\n\r\n this.audioContext = this.getAudioContext();\r\n\r\n if (this.audioContext.state === \"suspended\") {\r\n await this.audioContext.resume();\r\n }\r\n\r\n const processorResult = await this.createAudioProcessor();\r\n if (!processorResult) {\r\n console.log(\"无法创建音频处理器\", \"error\");\r\n return false;\r\n }\r\n\r\n this.audioProcessor = processorResult.node;\r\n this.audioProcessorType = processorResult.type;\r\n\r\n this.audioSource = this.audioContext.createMediaStreamSource(stream);\r\n this.analyser = this.audioContext.createAnalyser();\r\n this.analyser.fftSize = 2048;\r\n\r\n this.audioSource.connect(this.analyser);\r\n this.audioSource.connect(this.audioProcessor);\r\n\r\n this.pcmDataBuffer = new Int16Array();\r\n this.audioBuffers = [];\r\n this.totalAudioSize = 0;\r\n this.isRecording = true;\r\n\r\n if (this.audioProcessorType === \"worklet\" && this.audioProcessor.port) {\r\n this.audioProcessor.port.postMessage({ command: \"start\" });\r\n }\r\n\r\n // 发送监听开始消息\r\n if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {\r\n const listenMessage = {\r\n type: \"listen\",\r\n mode: localStorage.getItem(\"listenMode\") || \"wakeup\",\r\n state: \"start\",\r\n };\r\n\r\n console.log(\r\n `发送录音开始消息: ${JSON.stringify(listenMessage)}`,\r\n \"info\"\r\n );\r\n this.websocket.send(JSON.stringify(listenMessage));\r\n } else {\r\n console.log(\"WebSocket未连接,无法发送开始消息\", \"error\");\r\n return false;\r\n }\r\n\r\n // 启动录音计时器\r\n let recordingSeconds = 0;\r\n this.recordingTimer = setInterval(() => {\r\n recordingSeconds += 0.1;\r\n if (this.onRecordingStart) {\r\n this.onRecordingStart(recordingSeconds);\r\n }\r\n }, 100);\r\n\r\n console.log(\"开始PCM直接录音\", \"success\");\r\n return true;\r\n } catch (error) {\r\n console.log(`直接录音启动错误: ${error.message}`, \"error\");\r\n this.isRecording = false;\r\n return false;\r\n }\r\n }\r\n\r\n // 停止录音\r\n stop() {\r\n if (!this.isRecording) return false;\r\n\r\n try {\r\n this.isRecording = false;\r\n\r\n if (this.audioProcessor) {\r\n if (this.audioProcessorType === \"worklet\" && this.audioProcessor.port) {\r\n this.audioProcessor.port.postMessage({ command: \"stop\" });\r\n }\r\n\r\n this.audioProcessor.disconnect();\r\n this.audioProcessor = null;\r\n }\r\n\r\n if (this.audioSource) {\r\n this.audioSource.disconnect();\r\n this.audioSource = null;\r\n }\r\n\r\n if (this.visualizationRequest) {\r\n cancelAnimationFrame(this.visualizationRequest);\r\n this.visualizationRequest = null;\r\n }\r\n\r\n if (this.recordingTimer) {\r\n clearInterval(this.recordingTimer);\r\n this.recordingTimer = null;\r\n }\r\n\r\n // 编码并发送剩余的数据\r\n this.encodeAndSendOpus();\r\n\r\n // 发送结束信号\r\n if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {\r\n const emptyOpusFrame = new Uint8Array(0);\r\n this.websocket.send(emptyOpusFrame);\r\n\r\n const stopMessage = {\r\n type: \"listen\",\r\n mode: localStorage.getItem(\"listenMode\") || \"wakeup\",\r\n state: \"stop\",\r\n };\r\n\r\n this.websocket.send(JSON.stringify(stopMessage));\r\n console.log(\"已发送录音停止信号\", \"info\");\r\n }\r\n\r\n if (this.onRecordingStop) {\r\n this.onRecordingStop();\r\n }\r\n\r\n console.log(\"停止PCM直接录音\", \"success\");\r\n return true;\r\n } catch (error) {\r\n console.log(`直接录音停止错误: ${error.message}`, \"error\");\r\n return false;\r\n }\r\n }\r\n\r\n // 获取分析器\r\n getAnalyser() {\r\n return this.analyser;\r\n }\r\n}\r\n\r\n// 创建单例\r\nlet audioRecorderInstance = null;\r\n\r\nexport function getAudioRecorder() {\r\n if (!audioRecorderInstance) {\r\n audioRecorderInstance = new AudioRecorder();\r\n }\r\n return audioRecorderInstance;\r\n}\r\n","// WebSocket 连接\r\nexport async function webSocketConnect(otaUrl, config) {\r\n\r\n if (!validateConfig(config)) {\r\n return;\r\n }\r\n\r\n // 发送OTA请求并获取返回的websocket信息\r\n const otaResult = await sendOTA(otaUrl, config);\r\n if (!otaResult) {\r\n console.log('无法从OTA服务器获取信息', 'error');\r\n return;\r\n }\r\n\r\n // 从OTA响应中提取websocket信息\r\n const { websocket } = otaResult;\r\n if (!websocket || !websocket.url) {\r\n console.log('OTA响应中缺少websocket信息', 'error');\r\n return;\r\n }\r\n\r\n // 使用OTA返回的websocket URL\r\n let connUrl = new URL(websocket.url);\r\n\r\n // 添加token参数(从OTA响应中获取)\r\n if (websocket.token) {\r\n if (websocket.token.startsWith(\"Bearer \")) {\r\n connUrl.searchParams.append('authorization', websocket.token);\r\n } else {\r\n connUrl.searchParams.append('authorization', 'Bearer ' + websocket.token);\r\n }\r\n }\r\n\r\n // 添加认证参数(保持原有逻辑)\r\n connUrl.searchParams.append('device-id', config.deviceId);\r\n connUrl.searchParams.append('client-id', config.clientId);\r\n\r\n const wsurl = connUrl.toString()\r\n\r\n console.log(`正在连接: ${wsurl}`, 'info');\r\n\r\n return new WebSocket(connUrl.toString());\r\n}\r\n\r\n// 验证配置\r\nfunction validateConfig(config) {\r\n if (!config.deviceMac) {\r\n console.log('设备MAC地址不能为空', 'error');\r\n return false;\r\n }\r\n if (!config.clientId) {\r\n console.log('客户端ID不能为空', 'error');\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n// 判断wsUrl路径是否存在错误\r\nfunction validateWsUrl(wsUrl) {\r\n if (wsUrl === '') return false;\r\n // 检查URL格式\r\n if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {\r\n console.log('URL格式错误,必须以ws://或wss://开头', 'error');\r\n return false;\r\n }\r\n return true\r\n}\r\n\r\n\r\n// OTA发送请求,验证状态,并返回响应数据\r\nasync function sendOTA(otaUrl, config) {\r\n try {\r\n const res = await fetch(otaUrl, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Device-Id': config.deviceId,\r\n 'Client-Id': config.clientId\r\n },\r\n body: JSON.stringify({\r\n version: 0,\r\n uuid: '',\r\n application: {\r\n name: 'xiaozhi-web-test',\r\n version: '1.0.0',\r\n compile_time: '2025-04-16 10:00:00',\r\n idf_version: '4.4.3',\r\n elf_sha256: '1234567890abcdef1234567890abcdef1234567890abcdef'\r\n },\r\n ota: { label: 'xiaozhi-web-test' },\r\n board: {\r\n type: 'xiaozhi-web-test',\r\n ssid: 'xiaozhi-web-test',\r\n rssi: 0,\r\n channel: 0,\r\n ip: '192.168.1.1',\r\n mac: config.deviceMac\r\n },\r\n flash_size: 0,\r\n minimum_free_heap_size: 0,\r\n mac_address: config.deviceMac,\r\n chip_model_name: '',\r\n chip_info: { model: 0, cores: 0, revision: 0, features: 0 },\r\n partition_table: [{ label: '', type: 0, subtype: 0, address: 0, size: 0 }]\r\n })\r\n });\r\n\r\n if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);\r\n\r\n const result = await res.json();\r\n return result; // 返回完整的响应数据\r\n } catch (err) {\r\n return null; // 失败返回null\r\n }\r\n}","// 生成随机MAC地址\r\nfunction generateRandomMac() {\r\n const hexDigits = \"0123456789ABCDEF\";\r\n let mac = \"\";\r\n for (let i = 0; i < 6; i++) {\r\n if (i > 0) mac += \":\";\r\n for (let j = 0; j < 2; j++) {\r\n mac += hexDigits.charAt(Math.floor(Math.random() * 16));\r\n }\r\n }\r\n return mac;\r\n}\r\n\r\n// 加载配置\r\nexport function loadConfig() {\r\n // 从localStorage加载MAC地址,如果没有则生成新的\r\n let savedMac = localStorage.getItem(\"xz_tester_deviceMac\");\r\n if (!savedMac) {\r\n savedMac = generateRandomMac();\r\n localStorage.setItem(\"xz_tester_deviceMac\", savedMac);\r\n }\r\n}\r\n\r\n// 获取配置值\r\nexport function getConfig() {\r\n return {\r\n deviceId: localStorage.getItem(\"MAC\"), // 使用MAC地址作为deviceId\r\n deviceName: \"测试设备\",\r\n deviceMac: localStorage.getItem(\"MAC\"),\r\n clientId: \"web_test_client\",\r\n token: \"your-token1\",\r\n };\r\n}\r\n\r\n// 保存连接URL\r\nexport function saveConnectionUrls() {\r\n const otaUrl = localStorage.getItem(\"otaUrl\");\r\n localStorage.setItem(\"xz_tester_otaUrl\", otaUrl);\r\n}\r\n","// WebSocket消息处理模块\r\nimport { webSocketConnect } from \"./ota-connector.js\";\r\nimport { getConfig, saveConnectionUrls } from \"./manager.js\";\r\nimport { getAudioPlayer } from \"./player.js\";\r\nimport { getAudioRecorder } from \"./recorder.js\";\r\nimport {\r\n getMcpTools,\r\n executeMcpTool,\r\n setWebSocket as setMcpWebSocket,\r\n} from \"./tools.js\";\r\n\r\n// WebSocket处理器类\r\nexport class WebSocketHandler {\r\n constructor() {\r\n this.websocket = null;\r\n this.onConnectionStateChange = null;\r\n this.onRecordButtonStateChange = null;\r\n this.onSessionStateChange = null;\r\n this.onSessionEmotionChange = null;\r\n this.currentSessionId = null;\r\n this.isRemoteSpeaking = false;\r\n this.heartbeatTimer = null;\r\n\r\n // 重连相关配置\r\n this.reconnectConfig = {\r\n maxRetries: 10, // 最大重连次数\r\n baseDelay: 1000, // 基础重连延迟(ms)\r\n maxDelay: 30000, // 最大重连延迟(ms)\r\n retryCount: 0, // 当前重连次数\r\n reconnectTimer: null, // 重连定时器\r\n isReconnecting: false, // 是否正在重连中\r\n manualDisconnect: false, // 是否手动断开连接\r\n };\r\n }\r\n\r\n // 在 WebSocketHandler 类中添加\r\n startHeartbeat() {\r\n this.stopHeartbeat(); // 先清除之前的定时器\r\n this.sendHeartbeat();\r\n }\r\n\r\n stopHeartbeat() {\r\n if (this.heartbeatTimer) {\r\n clearTimeout(this.heartbeatTimer);\r\n this.heartbeatTimer = null;\r\n }\r\n }\r\n\r\n sendHeartbeat() {\r\n if (this.websocket?.readyState === WebSocket.OPEN) {\r\n try {\r\n this.websocket.send(\"1\");\r\n } catch (error) {\r\n console.error(\"心跳发送失败:\", error);\r\n }\r\n }\r\n\r\n this.heartbeatTimer = setTimeout(() => {\r\n this.sendHeartbeat();\r\n }, 5000);\r\n }\r\n\r\n // 发送hello握手消息\r\n async sendHelloMessage() {\r\n if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN)\r\n return false;\r\n\r\n this.startHeartbeat(); // 启动心跳\r\n try {\r\n const config = getConfig();\r\n\r\n const helloMessage = {\r\n type: \"hello\",\r\n device_id: config.deviceId,\r\n device_name: config.deviceName,\r\n device_mac: config.deviceMac,\r\n token: config.token,\r\n seat: localStorage.getItem(\"SEAT\"),\r\n features: {\r\n mcp: true,\r\n },\r\n };\r\n\r\n console.log(\"发送hello握手消息\", \"info\");\r\n this.websocket.send(JSON.stringify(helloMessage));\r\n\r\n return new Promise((resolve) => {\r\n const timeout = setTimeout(() => {\r\n console.log(\"等待hello响应超时\", \"error\");\r\n console.log('提示: 请尝试点击\"测试认证\"按钮进行连接排查', \"info\");\r\n resolve(false);\r\n }, 5000);\r\n\r\n const onMessageHandler = (event) => {\r\n try {\r\n const response = JSON.parse(event.data);\r\n if (response.type === \"hello\" && response.session_id) {\r\n console.log(\r\n `服务器握手成功,会话ID: ${response.session_id}`,\r\n \"success\"\r\n );\r\n // 握手成功后重置重连计数器\r\n this.reconnectConfig.retryCount = 0;\r\n clearTimeout(timeout);\r\n this.websocket.removeEventListener(\"message\", onMessageHandler);\r\n resolve(true);\r\n }\r\n } catch (e) {\r\n // 忽略非JSON消息\r\n }\r\n };\r\n\r\n this.websocket.addEventListener(\"message\", onMessageHandler);\r\n });\r\n } catch (error) {\r\n console.log(`发送hello消息错误: ${error.message}`, \"error\");\r\n return false;\r\n }\r\n }\r\n\r\n // 处理文本消息\r\n handleTextMessage(message) {\r\n if (message.type === \"hello\") {\r\n } else if (message.type === \"tts\") {\r\n this.handleTTSMessage(message);\r\n } else if (message.type === \"audio\") {\r\n } else if (message.type === \"stt\") {\r\n const event = new CustomEvent(\"wsSendMessage\", {\r\n detail: message,\r\n });\r\n window.dispatchEvent(event);\r\n } else if (message.type === \"llm\") {\r\n } else if (message.type === \"mcp\") {\r\n this.handleMCPMessage(message);\r\n } else if (message.type === \"json_data\") {\r\n if (message.state === \"drinks\") {\r\n const event = new CustomEvent(\"drinkListEvent\", {\r\n detail: message.data,\r\n });\r\n window.dispatchEvent(event);\r\n } else if (message.state === \"book\") {\r\n const event = new CustomEvent(\"bookListEvent\", {\r\n detail: message\r\n });\r\n window.dispatchEvent(event);\r\n }\r\n } else if (message.type === \"view_action\") {\r\n const event = new CustomEvent(\"viewActionEvent\", {\r\n detail: message.state,\r\n });\r\n window.dispatchEvent(event);\r\n } else {\r\n console.log(`未知消息类型: ${message.type}`, \"warning\");\r\n }\r\n }\r\n\r\n // 处理TTS消息\r\n handleTTSMessage(message) {\r\n if (message.state === \"start\") {\r\n console.log(\"服务器开始发送语音\", \"info\");\r\n this.currentSessionId = message.session_id;\r\n const event = new CustomEvent(\"startThink\");\r\n window.dispatchEvent(event);\r\n this.isRemoteSpeaking = true;\r\n if (this.onSessionStateChange) {\r\n this.onSessionStateChange(true);\r\n }\r\n } else if (message.state === \"sentence_start\") {\r\n const event = new CustomEvent(\"startVolic\", {\r\n detail: message.text,\r\n });\r\n window.dispatchEvent(event);\r\n console.log(`服务器发送语音段: ${message.text}`, \"info\");\r\n } else if (message.state === \"sentence_end\") {\r\n console.log(`语音段结束: ${message.text}`, \"info\");\r\n } else if (message.state === \"stop\") {\r\n const event = new CustomEvent(\"stopVolic\");\r\n window.dispatchEvent(event);\r\n console.log(\"服务器语音传输结束\", \"info\");\r\n this.isRemoteSpeaking = false;\r\n if (this.onRecordButtonStateChange) {\r\n this.onRecordButtonStateChange(false);\r\n }\r\n if (this.onSessionStateChange) {\r\n this.onSessionStateChange(false);\r\n }\r\n }\r\n }\r\n\r\n // 处理MCP消息\r\n handleMCPMessage(message) {\r\n const payload = message.payload || {};\r\n console.log(`服务器下发: ${JSON.stringify(message)}`, \"info\");\r\n\r\n if (payload.method === \"tools/list\") {\r\n const tools = getMcpTools();\r\n const replyMessage = JSON.stringify({\r\n session_id: message.session_id || \"\",\r\n type: \"mcp\",\r\n payload: {\r\n jsonrpc: \"2.0\",\r\n id: payload.id,\r\n result: {\r\n tools: tools,\r\n },\r\n },\r\n });\r\n console.log(`客户端上报: ${replyMessage}`, \"info\");\r\n this.websocket.send(replyMessage);\r\n console.log(`回复MCP工具列表: ${tools.length} 个工具`, \"info\");\r\n } else if (payload.method === \"tools/call\") {\r\n const toolName = payload.params?.name;\r\n const toolArgs = payload.params?.arguments;\r\n\r\n console.log(\r\n `调用工具: ${toolName} 参数: ${JSON.stringify(toolArgs)}`,\r\n \"info\"\r\n );\r\n\r\n const result = executeMcpTool(toolName, toolArgs);\r\n\r\n const replyMessage = JSON.stringify({\r\n session_id: message.session_id || \"\",\r\n type: \"mcp\",\r\n payload: {\r\n jsonrpc: \"2.0\",\r\n id: payload.id,\r\n result: {\r\n content: [\r\n {\r\n type: \"text\",\r\n text: JSON.stringify(result),\r\n },\r\n ],\r\n isError: false,\r\n },\r\n },\r\n });\r\n\r\n console.log(`客户端上报: ${replyMessage}`, \"info\");\r\n this.websocket.send(replyMessage);\r\n } else if (payload.method === \"initialize\") {\r\n console.log(\r\n `收到工具初始化请求: ${JSON.stringify(payload.params)}`,\r\n \"info\"\r\n );\r\n } else {\r\n console.log(`未知的MCP方法: ${payload.method}`, \"warning\");\r\n }\r\n }\r\n\r\n // 处理二进制消息\r\n async handleBinaryMessage(data) {\r\n try {\r\n let arrayBuffer;\r\n if (data instanceof ArrayBuffer) {\r\n arrayBuffer = data;\r\n } else if (data instanceof Blob) {\r\n arrayBuffer = await data.arrayBuffer();\r\n console.log(\r\n `收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`,\r\n \"debug\"\r\n );\r\n } else {\r\n console.log(`收到未知类型的二进制数据: ${typeof data}`, \"warning\");\r\n return;\r\n }\r\n\r\n const opusData = new Uint8Array(arrayBuffer);\r\n const audioPlayer = getAudioPlayer();\r\n audioPlayer.enqueueAudioData(opusData);\r\n } catch (error) {\r\n console.log(`处理二进制消息出错: ${error.message}`, \"error\");\r\n }\r\n }\r\n\r\n // 计算重连延迟(指数退避策略)\r\n calculateReconnectDelay() {\r\n // 指数退避 + 随机抖动,避免多个客户端同时重连\r\n const delay = Math.min(\r\n this.reconnectConfig.baseDelay *\r\n Math.pow(2, this.reconnectConfig.retryCount),\r\n this.reconnectConfig.maxDelay\r\n );\r\n // 添加±20%的随机抖动\r\n const jitter = delay * 0.2 * (Math.random() - 0.5);\r\n return Math.round(delay + jitter);\r\n }\r\n\r\n // 触发自动重连\r\n triggerReconnect() {\r\n // 如果是手动断开连接,不进行重连\r\n if (this.reconnectConfig.manualDisconnect) {\r\n console.log(\"手动断开连接,不进行自动重连\", \"info\");\r\n return;\r\n }\r\n\r\n // 检查是否达到最大重连次数\r\n if (this.reconnectConfig.retryCount >= this.reconnectConfig.maxRetries) {\r\n console.log(\r\n `已达到最大重连次数(${this.reconnectConfig.maxRetries}),停止重连`,\r\n \"error\"\r\n );\r\n this.reconnectConfig.isReconnecting = false;\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n return;\r\n }\r\n\r\n // 计算重连延迟\r\n const delay = this.calculateReconnectDelay();\r\n this.reconnectConfig.retryCount++;\r\n\r\n console.log(\r\n `准备进行第${this.reconnectConfig.retryCount}次重连,延迟${delay}ms`,\r\n \"info\"\r\n );\r\n\r\n // 设置重连定时器\r\n this.reconnectConfig.reconnectTimer = setTimeout(async () => {\r\n console.log(`开始第${this.reconnectConfig.retryCount}次重连`, \"info\");\r\n try {\r\n const success = await this.connect();\r\n if (success) {\r\n console.log(\"重连成功\", \"success\");\r\n this.reconnectConfig.isReconnecting = false;\r\n } else {\r\n console.log(\r\n `第${this.reconnectConfig.retryCount}次重连失败`,\r\n \"error\"\r\n );\r\n this.triggerReconnect();\r\n }\r\n } catch (error) {\r\n console.log(`重连出错: ${error.message}`, \"error\");\r\n this.triggerReconnect();\r\n }\r\n }, delay);\r\n }\r\n\r\n // 停止自动重连\r\n stopReconnect() {\r\n if (this.reconnectConfig.reconnectTimer) {\r\n clearTimeout(this.reconnectConfig.reconnectTimer);\r\n this.reconnectConfig.reconnectTimer = null;\r\n }\r\n this.reconnectConfig.isReconnecting = false;\r\n this.reconnectConfig.retryCount = 0;\r\n }\r\n\r\n // 连接WebSocket服务器\r\n async connect() {\r\n // 如果正在重连中,先停止之前的重连\r\n this.stopReconnect();\r\n\r\n const config = getConfig();\r\n console.log(\"正在检查OTA状态...\", \"info\");\r\n saveConnectionUrls();\r\n\r\n try {\r\n const otaUrl = localStorage.getItem(\"xz_tester_otaUrl\");\r\n const ws = await webSocketConnect(otaUrl, config);\r\n if (ws === undefined) {\r\n // 连接失败,触发重连\r\n if (\r\n !this.reconnectConfig.isReconnecting &&\r\n !this.reconnectConfig.manualDisconnect\r\n ) {\r\n this.reconnectConfig.isReconnecting = true;\r\n this.triggerReconnect();\r\n }\r\n return false;\r\n }\r\n\r\n this.websocket = ws;\r\n\r\n // 设置接收二进制数据的类型为ArrayBuffer\r\n this.websocket.binaryType = \"arraybuffer\";\r\n\r\n // 设置 MCP 模块的 WebSocket 实例\r\n setMcpWebSocket(this.websocket);\r\n\r\n // 设置录音器的WebSocket\r\n const audioRecorder = getAudioRecorder();\r\n audioRecorder.setWebSocket(this.websocket);\r\n\r\n this.setupEventHandlers();\r\n\r\n return true;\r\n } catch (error) {\r\n console.log(`连接错误: ${error.message}`, \"error\");\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n\r\n // 连接出错,触发重连\r\n if (\r\n !this.reconnectConfig.isReconnecting &&\r\n !this.reconnectConfig.manualDisconnect\r\n ) {\r\n this.reconnectConfig.isReconnecting = true;\r\n this.triggerReconnect();\r\n }\r\n\r\n return false;\r\n }\r\n }\r\n\r\n // 设置事件处理器\r\n setupEventHandlers() {\r\n this.websocket.onopen = async () => {\r\n const url = localStorage.getItem(\"xz_tester_wsUrl\");\r\n console.log(`已连接到服务器: ${url}`, \"success\");\r\n\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(true);\r\n }\r\n\r\n // 连接成功后,默认状态为聆听中\r\n this.isRemoteSpeaking = false;\r\n if (this.onSessionStateChange) {\r\n this.onSessionStateChange(false);\r\n }\r\n\r\n await this.sendHelloMessage();\r\n };\r\n\r\n this.websocket.onclose = (event) => {\r\n console.log(\r\n `已断开连接,代码: ${event.code}, 原因: ${event.reason}`,\r\n \"info\"\r\n );\r\n this.stopHeartbeat();\r\n\r\n // 清除MCP的WebSocket引用\r\n setMcpWebSocket(null);\r\n\r\n // 停止录音\r\n const audioRecorder = getAudioRecorder();\r\n audioRecorder.stop();\r\n\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n\r\n // 如果不是手动断开连接,触发自动重连\r\n if (\r\n !this.reconnectConfig.manualDisconnect &&\r\n !this.reconnectConfig.isReconnecting\r\n ) {\r\n // 1000: 正常关闭, 1001: 客户端离开, 这两种情况不自动重连\r\n if (event.code !== 1000 && event.code !== 1001) {\r\n console.log(\"检测到异常断开连接,准备自动重连\", \"warning\");\r\n this.reconnectConfig.isReconnecting = true;\r\n this.triggerReconnect();\r\n }\r\n }\r\n\r\n // 重置手动断开标记(以便下次可以重连)\r\n this.reconnectConfig.manualDisconnect = false;\r\n };\r\n\r\n this.websocket.onerror = (error) => {\r\n console.log(`WebSocket错误: ${error.message || \"未知错误\"}`, \"error\");\r\n\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n };\r\n\r\n this.websocket.onmessage = (event) => {\r\n try {\r\n if (typeof event.data === \"string\") {\r\n const message = JSON.parse(event.data);\r\n this.handleTextMessage(message);\r\n } else {\r\n this.handleBinaryMessage(event.data);\r\n }\r\n } catch (error) {\r\n console.log(`WebSocket消息处理错误: ${error.message}`, \"error\");\r\n }\r\n };\r\n }\r\n\r\n // 断开连接\r\n disconnect() {\r\n // 标记为手动断开连接\r\n this.reconnectConfig.manualDisconnect = true;\r\n this.stopReconnect();\r\n\r\n if (!this.websocket) return;\r\n\r\n // 正常关闭连接\r\n this.websocket.close(1000, \"Manual disconnect\");\r\n const audioRecorder = getAudioRecorder();\r\n audioRecorder.stop();\r\n }\r\n\r\n // 发送文本消息\r\n sendTextMessage(text) {\r\n try {\r\n // 如果对方正在说话,先发送打断消息\r\n const abortMessage = {\r\n session_id: this.currentSessionId,\r\n type: \"abort\",\r\n reason: \"wake_word_detected\",\r\n };\r\n this.websocket.send(JSON.stringify(abortMessage));\r\n console.log(\"发送打断消息\", \"info\");\r\n\r\n const listenMessage = {\r\n type: \"listen\",\r\n mode: localStorage.getItem(\"listenMode\") || \"wakeup\",\r\n state: \"detect\",\r\n text: text,\r\n };\r\n\r\n this.websocket.send(JSON.stringify(listenMessage));\r\n console.log(`发送文本消息: ${text}`, \"info6666\");\r\n\r\n return true;\r\n } catch (error) {\r\n console.log(`发送消息错误: ${error.message}`, \"error\");\r\n return false;\r\n }\r\n }\r\n\r\n // 获取WebSocket实例\r\n getWebSocket() {\r\n return this.websocket;\r\n }\r\n\r\n // 检查是否已连接\r\n isConnected() {\r\n return this.websocket && this.websocket.readyState === WebSocket.OPEN;\r\n }\r\n\r\n // 获取重连状态\r\n getReconnectStatus() {\r\n return {\r\n isReconnecting: this.reconnectConfig.isReconnecting,\r\n retryCount: this.reconnectConfig.retryCount,\r\n maxRetries: this.reconnectConfig.maxRetries,\r\n };\r\n }\r\n\r\n // 重置重连配置\r\n resetReconnectConfig() {\r\n this.stopReconnect();\r\n this.reconnectConfig.retryCount = 0;\r\n this.reconnectConfig.isReconnecting = false;\r\n this.reconnectConfig.manualDisconnect = false;\r\n }\r\n}\r\n\r\n// 创建单例\r\nlet wsHandlerInstance = null;\r\n\r\nexport function getWebSocketHandler() {\r\n if (!wsHandlerInstance) {\r\n wsHandlerInstance = new WebSocketHandler();\r\n }\r\n return wsHandlerInstance;\r\n}\r\n","// UI控制模块\nimport { getAudioRecorder } from \"./recorder.js\";\nimport { getWebSocketHandler } from \"./websocket.js\";\n\n// ws连接事件监听\nexport async function wsConnectEventListeners() {\n const wsHandler = getWebSocketHandler();\n await wsHandler.connect();\n}\n\n// ws关闭事件监听\nexport async function wsCloseEventListeners() {\n const wsHandler = getWebSocketHandler();\n await wsHandler.disconnect();\n}\n\n// 语音播放\nexport async function startEventAudioPlayer() {\n const audioRecorder = getAudioRecorder();\n await audioRecorder.start();\n}\n\n// 语音暂停\nexport async function stopEventAudioPlayer() {\n const audioRecorder = getAudioRecorder();\n await audioRecorder.stop();\n}\n\n\n// 判断ws是否连接\nexport function isWsConnected() {\n const wsHandler = getWebSocketHandler();\n return wsHandler.isConnected();\n}","<template></template>\r\n\r\n<script>\r\nimport { checkOpusLoaded, initOpusEncoder } from \"@/utils/opus-codec.js\";\r\nimport { getAudioPlayer } from \"@/utils/player.js\";\r\nimport { initMcpTools } from \"@/utils/tools.js\";\r\nimport {\r\n wsConnectEventListeners,\r\n wsCloseEventListeners,\r\n isWsConnected,\r\n startEventAudioPlayer,\r\n} from \"@/utils/controller.js\";\r\nexport default {\r\n name: \"ibiAiTalk\",\r\n data() {\r\n return {\r\n audioPlayer: null,\r\n };\r\n },\r\n props: {\r\n listenMode: {\r\n type: String,\r\n default: \"wakeup\",\r\n },\r\n otaUrl: {\r\n type: String,\r\n default: \"\",\r\n },\r\n macAddress: {\r\n type: String,\r\n default: \"\",\r\n },\r\n },\r\n async mounted() {\r\n localStorage.setItem(\"MAC\", this.macAddress);\r\n localStorage.setItem(\"otaUrl\", this.otaUrl);\r\n localStorage.setItem(\"listenMode\", this.listenMode);\r\n checkOpusLoaded();\r\n initOpusEncoder();\r\n initMcpTools();\r\n if (!isWsConnected()) {\r\n await wsConnectEventListeners();\r\n }\r\n this.audioPlayer = getAudioPlayer();\r\n await this.audioPlayer.start();\r\n await startEventAudioPlayer();\r\n },\r\n beforeDestroy() {\r\n this.audioPlayer.stop().catch(() => {});\r\n // 关闭WebSocket连接\r\n if (isWsConnected()) {\r\n wsCloseEventListeners().catch(() => {});\r\n }\r\n },\r\n};\r\n</script>\r\n\r\n<style></style>\r\n","import mod from \"-!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js\"; export default mod; export * from \"-!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js\"","/* globals __VUE_SSR_CONTEXT__ */\n\n// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).\n// This module is a runtime utility for cleaner component module output and will\n// be included in the final webpack user bundle.\n\nexport default function normalizeComponent(\n scriptExports,\n render,\n staticRenderFns,\n functionalTemplate,\n injectStyles,\n scopeId,\n moduleIdentifier /* server only */,\n shadowMode /* vue-cli only */\n) {\n // Vue.extend constructor export interop\n var options =\n typeof scriptExports === 'function' ? scriptExports.options : scriptExports\n\n // render functions\n if (render) {\n options.render = render\n options.staticRenderFns = staticRenderFns\n options._compiled = true\n }\n\n // functional template\n if (functionalTemplate) {\n options.functional = true\n }\n\n // scopedId\n if (scopeId) {\n options._scopeId = 'data-v-' + scopeId\n }\n\n var hook\n if (moduleIdentifier) {\n // server build\n hook = function (context) {\n // 2.3 injection\n context =\n context || // cached call\n (this.$vnode && this.$vnode.ssrContext) || // stateful\n (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional\n // 2.2 with runInNewContext: true\n if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {\n context = __VUE_SSR_CONTEXT__\n }\n // inject component styles\n if (injectStyles) {\n injectStyles.call(this, context)\n }\n // register component module identifier for async chunk inferrence\n if (context && context._registeredComponents) {\n context._registeredComponents.add(moduleIdentifier)\n }\n }\n // used by ssr in case component is cached and beforeCreate\n // never gets called\n options._ssrRegister = hook\n } else if (injectStyles) {\n hook = shadowMode\n ? function () {\n injectStyles.call(\n this,\n (options.functional ? this.parent : this).$root.$options.shadowRoot\n )\n }\n : injectStyles\n }\n\n if (hook) {\n if (options.functional) {\n // for template-only hot-reload because in that case the render fn doesn't\n // go through the normalizer\n options._injectStyles = hook\n // register for functional component in vue file\n var originalRender = options.render\n options.render = function renderWithStyleInjection(h, context) {\n hook.call(context)\n return originalRender(h, context)\n }\n } else {\n // inject component registration as beforeCreate hook\n var existing = options.beforeCreate\n options.beforeCreate = existing ? [].concat(existing, hook) : [hook]\n }\n }\n\n return {\n exports: scriptExports,\n options: options\n }\n}\n","import { render, staticRenderFns } from \"./index.vue?vue&type=template&id=70fe37c2\"\nimport script from \"./index.vue?vue&type=script&lang=js\"\nexport * from \"./index.vue?vue&type=script&lang=js\"\n\n\n/* normalize component */\nimport normalizer from \"!../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import './setPublicPath'\nimport mod from '~entry'\nexport default mod\nexport * from '~entry'\n"],"names":[],"sourceRoot":""}
|
|
1
|
+
{"version":3,"file":"index.common.js","mappings":";;UAAA;UACA;;;;;WCDA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;;;;;;;;;;;;ACAA;AACA;;AAEA;AACA;AACA,MAAM,KAAuC,EAAE,yBAQ5C;;AAEH;AACA;AACA,IAAI,qBAAuB;AAC3B;AACA;;AAEA;AACA,oDAAe,IAAI;;;ACtBnB,+BAA+B,6BAA6B;AAC5D;AACA;;;;ACFA;AACO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA,MAAM;AACN,yDAAyD,YAAY;AACrE;AACA;;;AAGA;AACA;AACO;AACP;AACA;AACA,gCAAgC;AAChC;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,kCAAkC;AAClC,kCAAkC;AAClC,kCAAkC;;AAElC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA8C,YAAY;;AAE1D;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,yDAAyD,IAAI;AAC7D;;AAEA;AACA,yEAAyE;;AAEzE;AACA,yEAAyE;;AAEzE;AACA,yEAAyE;;AAEzE;AACA;AACA,kBAAkB;AAClB;AACA;AACA;AACA;AACA,iDAAiD,cAAc;AAC/D;AACA;AACA,aAAa;;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,oEAAoE;;AAEpE;AACA,oCAAoC,oBAAoB;AACxD;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,qDAAqD,WAAW;AAChE;;AAEA;AACA;AACA,oCAAoC,gBAAgB;AACpD;AACA;;AAEA;AACA;AACA;;AAEA;AACA,kBAAkB;AAClB,6CAA6C,cAAc;AAC3D;AACA;AACA,aAAa;;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,MAAM;AACN,oCAAoC,cAAc;AAClD;AACA;AACA;;ACxKe;AACf;AACA,4BAA4B,IAAI;;AAEhC;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6BAA6B;;AAE7B;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB;;AAEA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,+CAA+C,QAAQ;AACvD;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;ACjGgD;;AAEhD;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,qBAAqB;AACrB,2BAA2B,aAAa,IAAI;AAC5C,uCAAuC;AACvC,gCAAgC,aAAa,IAAI;AACjD,0BAA0B;AAC1B,8BAA8B;AAC9B,wBAAwB;AACxB,2BAA2B;AAC3B,2BAA2B;AAC3B;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,UAAU,aAAa;AACvB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU,aAAa;AACvB;AACA;AACA;;AAEA;AACA;AACA;AACA,oBAAoB,sBAAsB;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,sBAAsB;AAClD;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;;AAEA;AACA;AACA,wBAAwB,2BAA2B;AACnD;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,0DAA0D;;AAE1D;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACvLA;AACgD;AACa;;AAE7D;AACO;AACP;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,qBAAqB,aAAa;AAClC;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,oCAAoC,YAAY;;AAEhD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,6CAA6C,IAAI;AACjD;;AAEA;AACA;AACA,SAAS;;AAET;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,2CAA2C,eAAe;AAC1D;;AAEA;AACA,4BAA4B,oBAAoB;AAChD;AACA;;AAEA;AACA;;AAEA;AACA,YAAY;AACZ,qCAAqC,cAAc;AACnD;AACA;AACA,SAAS;;AAET;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;;AAEA;AACA,MAAM;AACN,mCAAmC,cAAc;AACjD;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,oCAAoC,cAAc;AAClD,KAAK;;AAEL;AACA;AACA;AACA,oCAAoC,MAAM;AAC1C,OAAO;AACP;AACA,2BAA2B,gBAAgB;AAC3C;AACA;;AAEA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;;AAEA;AACA,gCAAgC,sBAAsB;AACtD;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,MAAM;AACN,iCAAiC,cAAc;AAC/C;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA,yBAAyB,cAAc;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;;;ACrQA;AACA;AACA;AACA;AACA,sBAAsB;;AAEtB;AACA;AACA,WAAW,WAAW;AACtB;AACO;AACP;AACA;;AAEA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA,IAAI;AACJ;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA,6BAA6B,iBAAiB;;AAE9C;AACA;AACA,uCAAuC,eAAe,YAAY;AAClE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,iDAAiD,UAAU;AAC3D;AACA,wEAAwE,MAAM;AAC9E,sDAAsD,cAAc,oBAAoB,2BAA2B,cAAc,iBAAiB,gBAAgB;AAClK;AACA;AACA,0EAA0E,MAAM;AAChF,sDAAsD,cAAc,oBAAoB,2BAA2B,cAAc,iBAAiB,gBAAgB;AAClK;AACA;AACA;AACA;AACA,oDAAoD,iBAAiB;AACrE;AACA;AACA;AACA,4DAA4D,YAAY;AACxE,gCAAgC,eAAe;AAC/C,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yBAAyB;AACzB;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,uCAAuC,eAAe,aAAa,gBAAgB;AACnF;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,kDAAkD,UAAU;AAC5D,oFAAoF,MAAM;AAC1F,6CAA6C,cAAc,oBAAoB,2BAA2B,cAAc,iBAAiB,gBAAgB;AACzJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB;AACrB,uEAAuE,MAAM;AAC7E;AACA;AACA;AACA,mGAAmG,MAAM;AACzG;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA,yBAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB;AACrB,wFAAwF,MAAM;AAC9F;AACA;AACA;AACA;AACA;AACA,qBAAqB;AACrB,wFAAwF,MAAM;AAC9F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,oFAAoF,MAAM;AAC1F;AACA;AACA,yCAAyC;AACzC,mEAAmE,MAAM;AACzE;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,mBAAmB,yBAAyB;AAC5C;AACA;AACA;AACA,GAAG;AACH;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA,GAAG;;AAEH;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,OAAO;AACP;AACA,IAAI;AACJ;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,kBAAkB;AAClB;AACA;;AAEA;AACA,yBAAyB;;AAEzB;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;;AAEA,iBAAiB;;AAEjB;AACA;AACA,0BAA0B,KAAK;AAC/B,IAAI;AACJ;AACA,0BAA0B,KAAK;AAC/B;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,uDAAuD,QAAQ;AAC/D;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B,qBAAqB;AAC/C;AACA;AACA;AACA;AACA,0BAA0B,SAAS;AACnC;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA,GAAG;AACH;;AAEA;AACA;AACA;AACO;AACP;AACA;AACA,0BAA0B,SAAS;AACnC;AACA;AACA,sBAAsB,SAAS;AAC/B;AACA;;AAEA;AACA,2CAA2C;AAC3C;AACA;AACA;AACA;AACA,qBAAqB,UAAU;AAC/B;AACA;AACA,IAAI;AACJ,4CAA4C;AAC5C;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,uBAAuB,UAAU;AACjC;AACA;AACA,IAAI;AACJ,uCAAuC;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACpgBA;AACkD;AACL;AAC7C;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,cAAc;AACtC;AACA;AACA;AACA;AACA;AACA;AACA,yBAAyB,eAAe;AACxC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oDAAoD,mCAAmC;AACvF,0BAA0B;AAC1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iCAAiC;AACjC;AACA;AACA;AACA,oDAAoD,mCAAmC;AACvF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAoC,kBAAkB;AACtD;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA,sBAAsB,cAAc;AACpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,kBAAkB;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAe;AACf,MAAM;AACN,8BAA8B,sBAAsB;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc;AACd,4CAA4C,cAAc;AAC1D;AACA;AACA,UAAU;AACV;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA,YAAY;AACZ;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN,+BAA+B,cAAc;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,wBAAwB;AAC7C;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA+C,kBAAkB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAuB,8BAA8B;AACrD;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,MAAM;AACN,+BAA+B,cAAc;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,iBAAiB;AAClE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN,+BAA+B,cAAc;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,yBAAgB;AAChC;AACA;AACA;AACA;AACA;;;ACnaA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY,YAAY;AACxB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yBAAyB,MAAM;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB,uBAAuB,2BAA2B;AAClD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB;AACA;AACA;AACA;AACA,6BAA6B,8CAA8C;AAC3E,oCAAoC,qDAAqD;AACzF,aAAa;AACb,SAAS;AACT;AACA,wCAAwC,YAAY,EAAE,eAAe;AACrE;AACA;AACA,uBAAuB;AACvB,MAAM;AACN,qBAAqB;AACrB;AACA;;AClHA;AACA;AACA;AACA;AACA,kBAAkB,OAAO;AACzB;AACA,oBAAoB,OAAO;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;;ACtCA;AACsD;AACO;AAChB;AACI;AAK7B;AACpB;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B;AAC1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2BAA2B;AAC3B;AACA,qBAAqB,SAAS;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA,iCAAiC,oBAAoB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY;AACZ;AACA;AACA;AACA;AACA;AACA,OAAO;AACP,MAAM;AACN,kCAAkC,cAAc;AAChD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA,MAAM;AACN,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA,MAAM;AACN,MAAM;AACN;AACA,MAAM;AACN;AACA;AACA;AACA,SAAS;AACT;AACA,QAAQ;AACR;AACA;AACA,SAAS;AACT;AACA;AACA,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA,MAAM;AACN,6BAA6B,aAAa;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA,+BAA+B,aAAa;AAC5C,MAAM;AACN,4BAA4B,aAAa;AACzC,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B,wBAAwB;AAClD;AACA;AACA,oBAAoB,WAAW;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW;AACX,SAAS;AACT,OAAO;AACP,4BAA4B,aAAa;AACzC;AACA,gCAAgC,cAAc;AAC9C,MAAM;AACN;AACA;AACA;AACA;AACA,iBAAiB,UAAU,MAAM,yBAAyB;AAC1D;AACA;AACA;AACA,qBAAqB,cAAc;AACnC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,WAAW;AACX,SAAS;AACT,OAAO;AACP;AACA,4BAA4B,aAAa;AACzC;AACA,MAAM;AACN;AACA,sBAAsB,+BAA+B;AACrD;AACA;AACA,MAAM;AACN,+BAA+B,eAAe;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA,4BAA4B,uBAAuB;AACnD;AACA;AACA,QAAQ;AACR,qCAAqC,YAAY;AACjD;AACA;AACA;AACA;AACA,0BAA0B,cAAc;AACxC;AACA,MAAM;AACN,gCAAgC,cAAc;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,gCAAgC;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,gCAAgC,QAAQ,MAAM;AAC5D;AACA;AACA;AACA;AACA;AACA,wBAAwB,gCAAgC;AACxD;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA,gBAAgB,gCAAgC;AAChD;AACA;AACA;AACA;AACA,QAAQ;AACR,6BAA6B,cAAc;AAC3C;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mBAAmB,SAAS;AAC5B;AACA,IAAI,kBAAkB;AACtB;AACA;AACA;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,YAAe;AACrB;AACA;AACA,4BAA4B,yBAAgB;AAC5C;AACA;AACA;AACA;AACA;AACA,MAAM;AACN,2BAA2B,cAAc;AACzC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA8B,IAAI;AAClC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,WAAW,QAAQ,aAAa;AACrD;AACA;AACA;AACA;AACA;AACA,MAAM,YAAe;AACrB;AACA;AACA,4BAA4B,yBAAgB;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kCAAkC,wBAAwB;AAC1D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA,QAAQ;AACR,wCAAwC,cAAc;AACtD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B,yBAAgB;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B,KAAK;AAClC;AACA;AACA,MAAM;AACN,6BAA6B,cAAc;AAC3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;;;ACpjBA;AACiD;AACI;;AAErD;AACO;AACP,oBAAoB,mBAAmB;AACvC;AACA;;AAEA;AACO;AACP,oBAAoB,mBAAmB;AACvC;AACA;;AAEA;AACO;AACP,wBAAwB,yBAAgB;AACxC;AACA;;AAEA;AACO;AACP;AACA;AACA;;;AAGA;AACO;AACP,oBAAoB,mBAAmB;AACvC;AACA;;;AC9ByE;AACtB;AACH;AAMjB;AAC/B,wFAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,eAAe;AACnB,IAAI,eAAe;AACnB,IAAI,YAAY;AAChB,SAAS,aAAa;AACtB,YAAY,uBAAuB;AACnC;AACA,uBAAuB,cAAc;AACrC;AACA,UAAU,qBAAqB;AAC/B;AACA;AACA;AACA;AACA,QAAQ,aAAa;AACrB,MAAM,qBAAqB;AAC3B;AACA;AACA,CAAC,EAAC;;;AC7E2H,CAAC,iEAAe,iDAAG,EAAC;;ACAjJ;;AAEA;AACA;AACA;;AAEe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;AC/FmF;AAC3B;AACL;;;AAGnD;AACA,CAAgG;AAChG,gBAAgB,kBAAU;AAC1B,EAAE,0BAAM;AACR,EAAE,MAAM;AACR,EAAE,eAAe;AACjB;AACA;AACA;AACA;AACA;AACA;;AAEA,4CAAe;;AClBS;AACA;AACxB,gDAAe,KAAG;AACI","sources":["webpack://ibi-ai-talk/webpack/bootstrap","webpack://ibi-ai-talk/webpack/runtime/define property getters","webpack://ibi-ai-talk/webpack/runtime/hasOwnProperty shorthand","webpack://ibi-ai-talk/webpack/runtime/publicPath","webpack://ibi-ai-talk/./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js","webpack://ibi-ai-talk/./src/index.vue?be80","webpack://ibi-ai-talk/./src/utils/opus-codec.js","webpack://ibi-ai-talk/./src/utils/blocking-queue.js","webpack://ibi-ai-talk/./src/utils/stream-context.js","webpack://ibi-ai-talk/./src/utils/player.js","webpack://ibi-ai-talk/./src/utils/tools.js","webpack://ibi-ai-talk/./src/utils/recorder.js","webpack://ibi-ai-talk/./src/utils/ota-connector.js","webpack://ibi-ai-talk/./src/utils/manager.js","webpack://ibi-ai-talk/./src/utils/websocket.js","webpack://ibi-ai-talk/./src/utils/controller.js","webpack://ibi-ai-talk/src/index.vue","webpack://ibi-ai-talk/./src/index.vue?ac85","webpack://ibi-ai-talk/./node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js","webpack://ibi-ai-talk/./src/index.vue","webpack://ibi-ai-talk/./node_modules/@vue/cli-service/lib/commands/build/entry-lib.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","__webpack_require__.p = \"\";","/* eslint-disable no-var */\n// This file is imported into lib/wc client bundles.\n\nif (typeof window !== 'undefined') {\n var currentScript = window.document.currentScript\n if (process.env.NEED_CURRENTSCRIPT_POLYFILL) {\n var getCurrentScript = require('@soda/get-current-script')\n currentScript = getCurrentScript()\n\n // for backward compatibility, because previously we directly included the polyfill\n if (!('currentScript' in document)) {\n Object.defineProperty(document, 'currentScript', { get: getCurrentScript })\n }\n }\n\n var src = currentScript && currentScript.src.match(/(.+\\/)[^/]+\\.js(\\?.*)?$/)\n if (src) {\n __webpack_public_path__ = src[1] // eslint-disable-line\n }\n}\n\n// Indicate to webpack that this file can be concatenated\nexport default null\n","var render = function render(){var _vm=this,_c=_vm._self._c;return _c(\"div\")\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","// 检查Opus库是否已加载\nexport function checkOpusLoaded() {\n try {\n // 检查Module是否存在(本地库导出的全局变量)\n if (typeof Module === 'undefined') {\n throw new Error('Opus库未加载,Module对象不存在');\n }\n\n // 尝试先使用Module.instance(libopus.js最后一行导出方式)\n if (typeof Module.instance !== 'undefined' && typeof Module.instance._opus_decoder_get_size === 'function') {\n // 使用Module.instance对象替换全局Module对象\n window.ModuleInstance = Module.instance;\n console.log('Opus库加载成功(使用Module.instance)', 'success');\n }\n\n // 如果没有Module.instance,检查全局Module函数\n if (typeof Module._opus_decoder_get_size === 'function') {\n window.ModuleInstance = Module;\n console.log('Opus库加载成功(使用全局Module)', 'success');\n }\n\n throw new Error('Opus解码函数未找到,可能Module结构不正确');\n } catch (err) {\n console.log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error');\n }\n}\n\n\n// 创建一个Opus编码器\nlet opusEncoder = null;\nexport function initOpusEncoder() {\n try {\n if (opusEncoder) {\n return opusEncoder; // 已经初始化过\n }\n\n if (!window.ModuleInstance) {\n console.log('无法创建Opus编码器:ModuleInstance不可用', 'error');\n return;\n }\n\n // 初始化一个Opus编码器\n const mod = window.ModuleInstance;\n const sampleRate = 16000; // 16kHz采样率\n const channels = 1; // 单声道\n const application = 2048; // OPUS_APPLICATION_VOIP = 2048\n\n // 创建编码器\n opusEncoder = {\n channels: channels,\n sampleRate: sampleRate,\n frameSize: 960, // 60ms @ 16kHz = 60 * 16 = 960 samples\n maxPacketSize: 4000, // 最大包大小\n module: mod,\n\n // 初始化编码器\n init: function () {\n try {\n // 获取编码器大小\n const encoderSize = mod._opus_encoder_get_size(this.channels);\n console.log(`Opus编码器大小: ${encoderSize}字节`, 'info');\n\n // 分配内存\n this.encoderPtr = mod._malloc(encoderSize);\n if (!this.encoderPtr) {\n throw new Error(\"无法分配编码器内存\");\n }\n\n // 初始化编码器\n const err = mod._opus_encoder_init(\n this.encoderPtr,\n this.sampleRate,\n this.channels,\n application\n );\n\n if (err < 0) {\n throw new Error(`Opus编码器初始化失败: ${err}`);\n }\n\n // 设置位率 (16kbps)\n mod._opus_encoder_ctl(this.encoderPtr, 4002, 16000); // OPUS_SET_BITRATE\n\n // 设置复杂度 (0-10, 越高质量越好但CPU使用越多)\n mod._opus_encoder_ctl(this.encoderPtr, 4010, 5); // OPUS_SET_COMPLEXITY\n\n // 设置使用DTX (不传输静音帧)\n mod._opus_encoder_ctl(this.encoderPtr, 4016, 1); // OPUS_SET_DTX\n\n console.log(\"Opus编码器初始化成功\", 'success');\n return true;\n } catch (error) {\n if (this.encoderPtr) {\n mod._free(this.encoderPtr);\n this.encoderPtr = null;\n }\n console.log(`Opus编码器初始化失败: ${error.message}`, 'error');\n return false;\n }\n },\n\n // 编码PCM数据为Opus\n encode: function (pcmData) {\n if (!this.encoderPtr) {\n if (!this.init()) {\n return null;\n }\n }\n\n try {\n const mod = this.module;\n\n // 为PCM数据分配内存\n const pcmPtr = mod._malloc(pcmData.length * 2); // 2字节/int16\n\n // 将PCM数据复制到HEAP\n for (let i = 0; i < pcmData.length; i++) {\n mod.HEAP16[(pcmPtr >> 1) + i] = pcmData[i];\n }\n\n // 为输出分配内存\n const outPtr = mod._malloc(this.maxPacketSize);\n\n // 进行编码\n const encodedLen = mod._opus_encode(\n this.encoderPtr,\n pcmPtr,\n this.frameSize,\n outPtr,\n this.maxPacketSize\n );\n\n if (encodedLen < 0) {\n throw new Error(`Opus编码失败: ${encodedLen}`);\n }\n\n // 复制编码后的数据\n const opusData = new Uint8Array(encodedLen);\n for (let i = 0; i < encodedLen; i++) {\n opusData[i] = mod.HEAPU8[outPtr + i];\n }\n\n // 释放内存\n mod._free(pcmPtr);\n mod._free(outPtr);\n\n return opusData;\n } catch (error) {\n console.log(`Opus编码出错: ${error.message}`, 'error');\n return null;\n }\n },\n\n // 销毁编码器\n destroy: function () {\n if (this.encoderPtr) {\n this.module._free(this.encoderPtr);\n this.encoderPtr = null;\n }\n }\n };\n\n opusEncoder.init();\n return opusEncoder;\n } catch (error) {\n console.log(`创建Opus编码器失败: ${error.message}`, 'error');\n return false;\n }\n}","export default class BlockingQueue {\n #items = [];\n #waiters = []; // {resolve, reject, min, timer, onTimeout}\n\n /* 空队列一次性闸门 */\n #emptyPromise = null;\n #emptyResolve = null;\n\n /* 生产者:把数据塞进去 */\n enqueue(item, ...restItems) {\n if (restItems.length === 0) {\n this.#items.push(item);\n }\n // 如果有额外参数,批量处理所有项\n else {\n const items = [item, ...restItems].filter(i => i);\n if (items.length === 0) return;\n this.#items.push(...items);\n }\n // 若有空队列闸门,一次性放行所有等待者\n if (this.#emptyResolve) {\n this.#emptyResolve();\n this.#emptyResolve = null;\n this.#emptyPromise = null;\n }\n\n // 唤醒所有正在等的 waiter\n this.#wakeWaiters();\n }\n\n /* 消费者:min 条或 timeout ms 先到谁 */\n async dequeue(min = 1, timeout = Infinity, onTimeout = null) {\n // 1. 若空,等第一次数据到达(所有调用共享同一个 promise)\n if (this.#items.length === 0) {\n await this.#waitForFirstItem();\n }\n\n // 立即满足\n if (this.#items.length >= min) {\n return this.#flush();\n }\n\n // 需要等待\n return new Promise((resolve, reject) => {\n let timer = null;\n const waiter = { resolve, reject, min, onTimeout, timer };\n\n // 超时逻辑\n if (Number.isFinite(timeout)) {\n waiter.timer = setTimeout(() => {\n this.#removeWaiter(waiter);\n if (onTimeout) onTimeout(this.#items.length);\n resolve(this.#flush());\n }, timeout);\n }\n\n this.#waiters.push(waiter);\n });\n }\n\n /* 空队列闸门生成器 */\n #waitForFirstItem() {\n if (!this.#emptyPromise) {\n this.#emptyPromise = new Promise(r => (this.#emptyResolve = r));\n }\n return this.#emptyPromise;\n }\n\n /* 内部:每次数据变动后,检查哪些 waiter 已满足 */\n #wakeWaiters() {\n for (let i = this.#waiters.length - 1; i >= 0; i--) {\n const w = this.#waiters[i];\n if (this.#items.length >= w.min) {\n this.#removeWaiter(w);\n w.resolve(this.#flush());\n }\n }\n }\n\n #removeWaiter(waiter) {\n const idx = this.#waiters.indexOf(waiter);\n if (idx !== -1) {\n this.#waiters.splice(idx, 1);\n if (waiter.timer) clearTimeout(waiter.timer);\n }\n }\n\n #flush() {\n const snapshot = [...this.#items];\n this.#items.length = 0;\n return snapshot;\n }\n\n /* 当前缓存长度(不含等待者) */\n get length() {\n return this.#items.length;\n }\n}","import BlockingQueue from \"./blocking-queue.js\";\n\n// 音频流播放上下文类\nexport class StreamingContext {\n constructor(\n opusDecoder,\n audioContext,\n sampleRate,\n channels,\n minAudioDuration\n ) {\n this.opusDecoder = opusDecoder;\n this.audioContext = audioContext;\n\n // 音频参数\n this.sampleRate = sampleRate;\n this.channels = channels;\n this.minAudioDuration = minAudioDuration;\n\n // 初始化队列和状态\n this.queue = []; // 已解码的PCM队列。正在播放\n this.activeQueue = new BlockingQueue(); // 已解码的PCM队列。准备播放\n this.pendingAudioBufferQueue = []; // 待处理的缓存队列\n this.audioBufferQueue = new BlockingQueue(); // 缓存队列\n this.playing = false; // 是否正在播放\n this.endOfStream = false; // 是否收到结束信号\n this.source = null; // 当前音频源\n this.totalSamples = 0; // 累积的总样本数\n this.lastPlayTime = 0; // 上次播放的时间戳\n }\n\n // 缓存音频数组\n pushAudioBuffer(item) {\n this.audioBufferQueue.enqueue(...item);\n }\n\n // 获取需要处理缓存队列,单线程:在audioBufferQueue一直更新的状态下不会出现安全问题\n async getPendingAudioBufferQueue() {\n // 原子交换 + 清空\n [this.pendingAudioBufferQueue, this.audioBufferQueue] = [\n await this.audioBufferQueue.dequeue(),\n new BlockingQueue(),\n ];\n }\n\n // 获取正在播放已解码的PCM队列,单线程:在activeQueue一直更新的状态下不会出现安全问题\n async getQueue(minSamples) {\n let TepArray = [];\n const num =\n minSamples - this.queue.length > 0 ? minSamples - this.queue.length : 1;\n // 原子交换 + 清空\n [TepArray, this.activeQueue] = [\n await this.activeQueue.dequeue(num),\n new BlockingQueue(),\n ];\n this.queue.push(...TepArray);\n }\n\n // 将Int16音频数据转换为Float32音频数据\n convertInt16ToFloat32(int16Data) {\n const float32Data = new Float32Array(int16Data.length);\n for (let i = 0; i < int16Data.length; i++) {\n // 将[-32768,32767]范围转换为[-1,1],统一使用32768.0避免不对称失真\n float32Data[i] = int16Data[i] / 32768.0;\n }\n return float32Data;\n }\n\n // 将Opus数据解码为PCM\n async decodeOpusFrames() {\n if (!this.opusDecoder) {\n console.log(\"Opus解码器未初始化,无法解码\", \"error\");\n return;\n } else {\n console.log(\"Opus解码器启动\", \"info\");\n }\n\n while (true) {\n let decodedSamples = [];\n for (const frame of this.pendingAudioBufferQueue) {\n try {\n // 使用Opus解码器解码\n const frameData = this.opusDecoder.decode(frame);\n if (frameData && frameData.length > 0) {\n // 转换为Float32\n const floatData = this.convertInt16ToFloat32(frameData);\n // 使用循环替代展开运算符\n for (let i = 0; i < floatData.length; i++) {\n decodedSamples.push(floatData[i]);\n }\n }\n } catch (error) {\n console.log(\"Opus解码失败: \" + error.message, \"error\");\n }\n }\n\n if (decodedSamples.length > 0) {\n // 使用循环替代展开运算符\n for (let i = 0; i < decodedSamples.length; i++) {\n this.activeQueue.enqueue(decodedSamples[i]);\n }\n this.totalSamples += decodedSamples.length;\n } else {\n console.log(\"没有成功解码的样本\", \"warning\");\n }\n await this.getPendingAudioBufferQueue();\n }\n }\n\n // 开始播放音频\n async startPlaying() {\n let scheduledEndTime = this.audioContext.currentTime; // 跟踪已调度音频的结束时间\n\n while (true) {\n // 初始缓冲:等待足够的样本再开始播放\n const minSamples = this.sampleRate * this.minAudioDuration * 2;\n if (!this.playing && this.queue.length < minSamples) {\n await this.getQueue(minSamples);\n }\n this.playing = true;\n\n // 持续播放队列中的音频,每次播放一个小块\n while (this.playing && this.queue.length > 0) {\n // 每次播放120ms的音频(2个Opus包)\n const playDuration = 0.12;\n const targetSamples = Math.floor(this.sampleRate * playDuration);\n const actualSamples = Math.min(this.queue.length, targetSamples);\n\n if (actualSamples === 0) break;\n\n const currentSamples = this.queue.splice(0, actualSamples);\n const audioBuffer = this.audioContext.createBuffer(\n this.channels,\n currentSamples.length,\n this.sampleRate\n );\n audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);\n\n // 创建音频源\n this.source = this.audioContext.createBufferSource();\n this.source.buffer = audioBuffer;\n\n // 精确调度播放时间\n const currentTime = this.audioContext.currentTime;\n const startTime = Math.max(scheduledEndTime, currentTime);\n\n // 直接连接到输出\n this.source.connect(this.audioContext.destination);\n\n this.source.start(startTime);\n\n // 更新下一个音频块的调度时间\n const duration = audioBuffer.duration;\n scheduledEndTime = startTime + duration;\n this.lastPlayTime = startTime;\n\n // 如果队列中数据不足,等待新数据\n if (this.queue.length < targetSamples) {\n break;\n }\n }\n\n // 等待新数据\n await this.getQueue(minSamples);\n }\n }\n}\n\n// 创建streamingContext实例的工厂函数\nexport function createStreamingContext(\n opusDecoder,\n audioContext,\n sampleRate,\n channels,\n minAudioDuration\n) {\n return new StreamingContext(\n opusDecoder,\n audioContext,\n sampleRate,\n channels,\n minAudioDuration\n );\n}\n","// 音频播放模块\nimport BlockingQueue from \"./blocking-queue.js\";\nimport { createStreamingContext } from \"./stream-context.js\";\n\n// 音频播放器类\nexport class AudioPlayer {\n constructor() {\n // 音频参数\n this.SAMPLE_RATE = 16000;\n this.CHANNELS = 1;\n this.FRAME_SIZE = 960;\n this.MIN_AUDIO_DURATION = 0.12;\n\n // 状态\n this.audioContext = null;\n this.opusDecoder = null;\n this.streamingContext = null;\n this.queue = new BlockingQueue();\n this.isPlaying = false;\n }\n\n // 获取或创建AudioContext\n getAudioContext() {\n if (!this.audioContext) {\n this.audioContext = new (window.AudioContext ||\n window.webkitAudioContext)({\n sampleRate: this.SAMPLE_RATE,\n latencyHint: \"interactive\",\n });\n console.log(\n \"创建音频上下文,采样率: \" + this.SAMPLE_RATE + \"Hz\",\n \"debug\"\n );\n }\n return this.audioContext;\n }\n\n // 初始化Opus解码器\n async initOpusDecoder() {\n if (this.opusDecoder) return this.opusDecoder;\n\n try {\n if (typeof window.ModuleInstance === \"undefined\") {\n if (typeof Module !== \"undefined\") {\n window.ModuleInstance = Module;\n console.log(\"使用全局Module作为ModuleInstance\", \"info\");\n } else {\n throw new Error(\"Opus库未加载,ModuleInstance和Module对象都不存在\");\n }\n }\n\n const mod = window.ModuleInstance;\n\n this.opusDecoder = {\n channels: this.CHANNELS,\n rate: this.SAMPLE_RATE,\n frameSize: this.FRAME_SIZE,\n module: mod,\n decoderPtr: null,\n\n init: function () {\n if (this.decoderPtr) return true;\n\n const decoderSize = mod._opus_decoder_get_size(this.channels);\n console.log(`Opus解码器大小: ${decoderSize}字节`, \"debug\");\n\n this.decoderPtr = mod._malloc(decoderSize);\n if (!this.decoderPtr) {\n throw new Error(\"无法分配解码器内存\");\n }\n\n const err = mod._opus_decoder_init(\n this.decoderPtr,\n this.rate,\n this.channels\n );\n\n if (err < 0) {\n this.destroy();\n throw new Error(`Opus解码器初始化失败: ${err}`);\n }\n\n console.log(\"Opus解码器初始化成功\", \"success\");\n return true;\n },\n\n decode: function (opusData) {\n if (!this.decoderPtr) {\n if (!this.init()) {\n throw new Error(\"解码器未初始化且无法初始化\");\n }\n }\n\n try {\n const mod = this.module;\n\n const opusPtr = mod._malloc(opusData.length);\n mod.HEAPU8.set(opusData, opusPtr);\n\n const pcmPtr = mod._malloc(this.frameSize * 2);\n\n const decodedSamples = mod._opus_decode(\n this.decoderPtr,\n opusPtr,\n opusData.length,\n pcmPtr,\n this.frameSize,\n 0\n );\n\n if (decodedSamples < 0) {\n mod._free(opusPtr);\n mod._free(pcmPtr);\n throw new Error(`Opus解码失败: ${decodedSamples}`);\n }\n\n const decodedData = new Int16Array(decodedSamples);\n for (let i = 0; i < decodedSamples; i++) {\n decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];\n }\n\n mod._free(opusPtr);\n mod._free(pcmPtr);\n\n return decodedData;\n } catch (error) {\n console.log(`Opus解码错误: ${error.message}`, \"error\");\n return new Int16Array(0);\n }\n },\n\n destroy: function () {\n if (this.decoderPtr) {\n this.module._free(this.decoderPtr);\n this.decoderPtr = null;\n }\n },\n };\n\n if (!this.opusDecoder.init()) {\n throw new Error(\"Opus解码器初始化失败\");\n }\n\n return this.opusDecoder;\n } catch (error) {\n console.log(`Opus解码器初始化失败: ${error.message}`, \"error\");\n this.opusDecoder = null;\n throw error;\n }\n }\n\n // 启动音频缓冲\n async startAudioBuffering() {\n console.log(\"开始音频缓冲...\", \"info\");\n\n this.initOpusDecoder().catch((error) => {\n console.log(`预初始化Opus解码器失败: ${error.message}`, \"warning\");\n });\n\n const timeout = 400;\n while (true) {\n const packets = await this.queue.dequeue(6, timeout, (count) => {\n console.log(`缓冲超时,当前缓冲包数: ${count},开始播放`, \"info\");\n });\n if (packets.length) {\n console.log(`已缓冲 ${packets.length} 个音频包,开始播放`, \"info\");\n this.streamingContext.pushAudioBuffer(packets);\n }\n\n while (true) {\n const data = await this.queue.dequeue(99, 30);\n if (data.length) {\n this.streamingContext.pushAudioBuffer(data);\n } else {\n break;\n }\n }\n }\n }\n\n // 播放已缓冲的音频\n async playBufferedAudio() {\n try {\n this.audioContext = this.getAudioContext();\n\n if (!this.opusDecoder) {\n console.log(\"初始化Opus解码器...\", \"info\");\n try {\n this.opusDecoder = await this.initOpusDecoder();\n if (!this.opusDecoder) {\n throw new Error(\"解码器初始化失败\");\n }\n console.log(\"Opus解码器初始化成功\", \"success\");\n } catch (error) {\n console.log(\"Opus解码器初始化失败: \" + error.message, \"error\");\n this.isPlaying = false;\n return;\n }\n }\n\n if (!this.streamingContext) {\n this.streamingContext = createStreamingContext(\n this.opusDecoder,\n this.audioContext,\n this.SAMPLE_RATE,\n this.CHANNELS,\n this.MIN_AUDIO_DURATION\n );\n }\n\n this.streamingContext.decodeOpusFrames();\n this.streamingContext.startPlaying();\n } catch (error) {\n console.log(`播放已缓冲的音频出错: ${error.message}`, \"error\");\n this.isPlaying = false;\n this.streamingContext = null;\n }\n }\n\n // 添加音频数据到队列\n enqueueAudioData(opusData) {\n if (opusData.length > 0) {\n this.queue.enqueue(opusData);\n } else {\n console.log(\"收到空音频数据帧,可能是结束标志\", \"warning\");\n if (this.isPlaying && this.streamingContext) {\n this.streamingContext.endOfStream = true;\n }\n }\n }\n\n // 预加载解码器\n async preload() {\n console.log(\"预加载Opus解码器...\", \"info\");\n try {\n await this.initOpusDecoder();\n console.log(\"Opus解码器预加载成功\", \"success\");\n } catch (error) {\n console.log(\n `Opus解码器预加载失败: ${error.message},将在需要时重试`,\n \"warning\"\n );\n }\n }\n\n // 启动播放系统\n async start() {\n await this.preload();\n this.playBufferedAudio();\n this.startAudioBuffering();\n }\n}\n\n// 创建单例\nlet audioPlayerInstance = null;\n\nexport function getAudioPlayer() {\n if (!audioPlayerInstance) {\n audioPlayerInstance = new AudioPlayer();\n }\n return audioPlayerInstance;\n}\n","// 全局变量\nlet mcpTools = [];\nlet mcpEditingIndex = null;\nlet mcpProperties = [];\nlet websocket = null; // 将从外部设置\n\n/**\n * 设置 WebSocket 实例\n * @param {WebSocket} ws - WebSocket 连接实例\n */\nexport function setWebSocket(ws) {\n websocket = ws;\n}\n\n/**\n * 初始化 MCP 工具\n */\nexport async function initMcpTools() {\n // 加载默认工具数据\n const defaultMcpTools = await fetch(\"/default-mcp-tools.json\").then((res) =>\n res.json()\n );\n\n const savedTools = localStorage.getItem(\"mcpTools\");\n if (savedTools) {\n try {\n mcpTools = JSON.parse(savedTools);\n } catch (e) {\n console.log(\"加载MCP工具失败,使用默认工具\", \"warning\");\n mcpTools = [...defaultMcpTools];\n }\n } else {\n mcpTools = [...defaultMcpTools];\n }\n\n // renderMcpTools();\n // setupMcpEventListeners();\n}\n\n/**\n * 渲染工具列表\n */\nfunction renderMcpTools() {\n const container = document.getElementById(\"mcpToolsContainer\");\n const countSpan = document.getElementById(\"mcpToolsCount\");\n\n countSpan.textContent = `${mcpTools.length} 个工具`;\n\n if (mcpTools.length === 0) {\n container.innerHTML =\n '<div style=\"text-align: center; padding: 30px; color: #999;\">暂无工具,点击下方按钮添加新工具</div>';\n return;\n }\n\n container.innerHTML = mcpTools\n .map((tool, index) => {\n const paramCount = tool.inputSchema.properties\n ? Object.keys(tool.inputSchema.properties).length\n : 0;\n const requiredCount = tool.inputSchema.required\n ? tool.inputSchema.required.length\n : 0;\n const hasMockResponse =\n tool.mockResponse && Object.keys(tool.mockResponse).length > 0;\n\n return `\n <div class=\"mcp-tool-card\">\n <div class=\"mcp-tool-header\">\n <div class=\"mcp-tool-name\">${tool.name}</div>\n <div class=\"mcp-tool-actions\">\n <button onclick=\"window.mcpModule.editMcpTool(${index})\"\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(${index})\"\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\">${tool.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\">${paramCount} 个 ${\n requiredCount > 0 ? `(${requiredCount} 个必填)` : \"\"\n }</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\">${\n hasMockResponse\n ? \"✅ 已配置: \" + JSON.stringify(tool.mockResponse)\n : \"⚪ 使用默认\"\n }</span>\n </div>\n </div>\n </div>\n `;\n })\n .join(\"\");\n}\n\n/**\n * 渲染参数列表\n */\nfunction renderMcpProperties() {\n const container = document.getElementById(\"mcpPropertiesContainer\");\n\n if (mcpProperties.length === 0) {\n container.innerHTML =\n '<div style=\"text-align: center; padding: 20px; color: #999; font-size: 14px;\">暂无参数,点击下方按钮添加参数</div>';\n return;\n }\n\n container.innerHTML = mcpProperties\n .map(\n (prop, index) => `\n <div class=\"mcp-property-item\">\n <div class=\"mcp-property-header\">\n <span class=\"mcp-property-name\">${prop.name}</span>\n <button type=\"button\" onclick=\"window.mcpModule.deleteMcpProperty(${index})\"\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=\"${\n prop.name\n }\"\n onchange=\"window.mcpModule.updateMcpProperty(${index}, '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(${index}, 'type', this.value)\">\n <option value=\"string\" ${\n prop.type === \"string\" ? \"selected\" : \"\"\n }>字符串</option>\n <option value=\"integer\" ${\n prop.type === \"integer\" ? \"selected\" : \"\"\n }>整数</option>\n <option value=\"number\" ${\n prop.type === \"number\" ? \"selected\" : \"\"\n }>数字</option>\n <option value=\"boolean\" ${\n prop.type === \"boolean\" ? \"selected\" : \"\"\n }>布尔值</option>\n <option value=\"array\" ${\n prop.type === \"array\" ? \"selected\" : \"\"\n }>数组</option>\n <option value=\"object\" ${\n prop.type === \"object\" ? \"selected\" : \"\"\n }>对象</option>\n </select>\n </div>\n </div>\n ${\n prop.type === \"integer\" || prop.type === \"number\"\n ? `\n <div class=\"mcp-property-row\">\n <div>\n <label class=\"mcp-small-label\">最小值</label>\n <input type=\"number\" class=\"mcp-small-input\" value=\"${\n prop.minimum !== undefined ? prop.minimum : \"\"\n }\"\n placeholder=\"可选\" onchange=\"window.mcpModule.updateMcpProperty(${index}, '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=\"${\n prop.maximum !== undefined ? prop.maximum : \"\"\n }\"\n placeholder=\"可选\" onchange=\"window.mcpModule.updateMcpProperty(${index}, 'maximum', this.value ? parseFloat(this.value) : undefined)\">\n </div>\n </div>\n `\n : \"\"\n }\n <div class=\"mcp-property-row-full\">\n <label class=\"mcp-small-label\">参数描述</label>\n <input type=\"text\" class=\"mcp-small-input\" value=\"${\n prop.description || \"\"\n }\"\n placeholder=\"可选\" onchange=\"window.mcpModule.updateMcpProperty(${index}, 'description', this.value)\">\n </div>\n <label class=\"mcp-checkbox-label\">\n <input type=\"checkbox\" ${prop.required ? \"checked\" : \"\"}\n onchange=\"window.mcpModule.updateMcpProperty(${index}, 'required', this.checked)\">\n 必填参数\n </label>\n </div>\n `\n )\n .join(\"\");\n}\n\n/**\n * 添加参数\n */\nfunction addMcpProperty() {\n mcpProperties.push({\n name: `param_${mcpProperties.length + 1}`,\n type: \"string\",\n required: false,\n description: \"\",\n });\n renderMcpProperties();\n}\n\n/**\n * 更新参数\n */\nfunction updateMcpProperty(index, field, value) {\n if (field === \"name\") {\n const isDuplicate = mcpProperties.some(\n (p, i) => i !== index && p.name === value\n );\n if (isDuplicate) {\n alert(\"参数名称已存在,请使用不同的名称\");\n renderMcpProperties();\n return;\n }\n }\n\n mcpProperties[index][field] = value;\n\n if (field === \"type\" && value !== \"integer\" && value !== \"number\") {\n delete mcpProperties[index].minimum;\n delete mcpProperties[index].maximum;\n renderMcpProperties();\n }\n}\n\n/**\n * 删除参数\n */\nfunction deleteMcpProperty(index) {\n mcpProperties.splice(index, 1);\n renderMcpProperties();\n}\n\n/**\n * 设置事件监听\n */\nfunction setupMcpEventListeners() {\n const toggleBtn = document.getElementById(\"toggleMcpTools\");\n const panel = document.getElementById(\"mcpToolsPanel\");\n const addBtn = document.getElementById(\"addMcpToolBtn\");\n const modal = document.getElementById(\"mcpToolModal\");\n const closeBtn = document.getElementById(\"closeMcpModalBtn\");\n const cancelBtn = document.getElementById(\"cancelMcpBtn\");\n const form = document.getElementById(\"mcpToolForm\");\n const addPropertyBtn = document.getElementById(\"addMcpPropertyBtn\");\n\n toggleBtn.addEventListener(\"click\", () => {\n const isExpanded = panel.classList.contains(\"expanded\");\n panel.classList.toggle(\"expanded\");\n toggleBtn.textContent = isExpanded ? \"展开\" : \"收起\";\n });\n\n addBtn.addEventListener(\"click\", () => openMcpModal());\n closeBtn.addEventListener(\"click\", closeMcpModal);\n cancelBtn.addEventListener(\"click\", closeMcpModal);\n addPropertyBtn.addEventListener(\"click\", addMcpProperty);\n\n modal.addEventListener(\"click\", (e) => {\n if (e.target === modal) closeMcpModal();\n });\n\n form.addEventListener(\"submit\", handleMcpSubmit);\n}\n\n/**\n * 打开模态框\n */\nfunction openMcpModal(index = null) {\n const isConnected = websocket && websocket.readyState === WebSocket.OPEN;\n if (isConnected) {\n alert(\"WebSocket 已连接,无法编辑工具\");\n return;\n }\n\n mcpEditingIndex = index;\n const errorContainer = document.getElementById(\"mcpErrorContainer\");\n errorContainer.innerHTML = \"\";\n\n if (index !== null) {\n document.getElementById(\"mcpModalTitle\").textContent = \"编辑工具\";\n const tool = mcpTools[index];\n document.getElementById(\"mcpToolName\").value = tool.name;\n document.getElementById(\"mcpToolDescription\").value = tool.description;\n document.getElementById(\"mcpMockResponse\").value = tool.mockResponse\n ? JSON.stringify(tool.mockResponse, null, 2)\n : \"\";\n\n mcpProperties = [];\n const schema = tool.inputSchema;\n if (schema.properties) {\n Object.keys(schema.properties).forEach((key) => {\n const prop = schema.properties[key];\n mcpProperties.push({\n name: key,\n type: prop.type || \"string\",\n minimum: prop.minimum,\n maximum: prop.maximum,\n description: prop.description || \"\",\n required: schema.required && schema.required.includes(key),\n });\n });\n }\n } else {\n document.getElementById(\"mcpModalTitle\").textContent = \"添加工具\";\n document.getElementById(\"mcpToolForm\").reset();\n mcpProperties = [];\n }\n\n renderMcpProperties();\n document.getElementById(\"mcpToolModal\").style.display = \"block\";\n}\n\n/**\n * 关闭模态框\n */\nfunction closeMcpModal() {\n document.getElementById(\"mcpToolModal\").style.display = \"none\";\n mcpEditingIndex = null;\n document.getElementById(\"mcpToolForm\").reset();\n mcpProperties = [];\n document.getElementById(\"mcpErrorContainer\").innerHTML = \"\";\n}\n\n/**\n * 处理表单提交\n */\nfunction handleMcpSubmit(e) {\n e.preventDefault();\n const errorContainer = document.getElementById(\"mcpErrorContainer\");\n errorContainer.innerHTML = \"\";\n\n const name = document.getElementById(\"mcpToolName\").value.trim();\n const description = document\n .getElementById(\"mcpToolDescription\")\n .value.trim();\n const mockResponseText = document\n .getElementById(\"mcpMockResponse\")\n .value.trim();\n\n // 检查名称重复\n const isDuplicate = mcpTools.some(\n (tool, index) => tool.name === name && index !== mcpEditingIndex\n );\n\n if (isDuplicate) {\n showMcpError(\"工具名称已存在,请使用不同的名称\");\n return;\n }\n\n // 解析模拟返回结果\n let mockResponse = null;\n if (mockResponseText) {\n try {\n mockResponse = JSON.parse(mockResponseText);\n } catch (e) {\n showMcpError(\"模拟返回结果不是有效的 JSON 格式: \" + e.message);\n return;\n }\n }\n\n // 构建 inputSchema\n const inputSchema = {\n type: \"object\",\n properties: {},\n required: [],\n };\n\n mcpProperties.forEach((prop) => {\n const propSchema = { type: prop.type };\n\n if (prop.description) {\n propSchema.description = prop.description;\n }\n\n if (prop.type === \"integer\" || prop.type === \"number\") {\n if (prop.minimum !== undefined && prop.minimum !== \"\") {\n propSchema.minimum = prop.minimum;\n }\n if (prop.maximum !== undefined && prop.maximum !== \"\") {\n propSchema.maximum = prop.maximum;\n }\n }\n\n inputSchema.properties[prop.name] = propSchema;\n\n if (prop.required) {\n inputSchema.required.push(prop.name);\n }\n });\n\n if (inputSchema.required.length === 0) {\n delete inputSchema.required;\n }\n\n const tool = { name, description, inputSchema, mockResponse };\n\n if (mcpEditingIndex !== null) {\n mcpTools[mcpEditingIndex] = tool;\n console.log(`已更新工具: ${name}`, \"success\");\n } else {\n mcpTools.push(tool);\n console.log(`已添加工具: ${name}`, \"success\");\n }\n\n saveMcpTools();\n renderMcpTools();\n closeMcpModal();\n}\n\n/**\n * 显示错误\n */\nfunction showMcpError(message) {\n const errorContainer = document.getElementById(\"mcpErrorContainer\");\n errorContainer.innerHTML = `<div class=\"mcp-error\">${message}</div>`;\n}\n\n/**\n * 编辑工具\n */\nfunction editMcpTool(index) {\n openMcpModal(index);\n}\n\n/**\n * 删除工具\n */\nfunction deleteMcpTool(index) {\n const isConnected = websocket && websocket.readyState === WebSocket.OPEN;\n if (isConnected) {\n alert(\"WebSocket 已连接,无法编辑工具\");\n return;\n }\n if (confirm(`确定要删除工具 \"${mcpTools[index].name}\" 吗?`)) {\n const toolName = mcpTools[index].name;\n mcpTools.splice(index, 1);\n saveMcpTools();\n renderMcpTools();\n console.log(`已删除工具: ${toolName}`, \"info\");\n }\n}\n\n/**\n * 保存工具\n */\nfunction saveMcpTools() {\n localStorage.setItem(\"mcpTools\", JSON.stringify(mcpTools));\n}\n\n/**\n * 获取工具列表\n */\nexport function getMcpTools() {\n return mcpTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n }));\n}\n\n/**\n * 执行工具调用\n */\nexport function executeMcpTool(toolName, toolArgs) {\n const tool = mcpTools.find((t) => t.name === toolName);\n if (!tool) {\n console.log(`未找到工具: ${toolName}`, \"error\");\n return {\n success: false,\n error: `未知工具: ${toolName}`,\n };\n }\n\n if (tool.name == \"self.drink_car_list\") {\n console.log(\"准备触发 gotoOrderEvent 事件\"); // 增加此日志\n const event = new CustomEvent(\"gotoOrderEvent\");\n window.dispatchEvent(event);\n return {\n success: true,\n message: `工具 ${toolName} 执行成功`,\n data: sessionStorage.getItem('cartList') || [],\n };\n } else if (tool.name == \"self.drink_car_reset\") {\n console.log(\"准备触发 resetOrderEvent 事件\"); // 增加此日志\n const event = new CustomEvent(\"resetOrderEvent\", {\n detail: toolArgs,\n });\n window.dispatchEvent(event);\n return {\n success: true,\n message: `工具 ${toolName} 执行成功`,\n data: sessionStorage.getItem('cartList') || [],\n }\n } else if (tool.name == \"self.drink_order\") {\n console.log(\"准备触发 orderEvent 事件\"); // 增加此日志\n const event = new CustomEvent(\"orderEvent\");\n window.dispatchEvent(event);\n }\n}\n\n// 暴露全局方法供 HTML 内联事件调用\nwindow.mcpModule = {\n updateMcpProperty,\n deleteMcpProperty,\n editMcpTool,\n deleteMcpTool,\n};\n","// 音频录制模块\r\nimport { initOpusEncoder } from \"./opus-codec.js\";\r\nimport { getAudioPlayer } from \"./player.js\";\r\n\r\n// 音频录制器类\r\nexport class AudioRecorder {\r\n constructor() {\r\n this.isRecording = false;\r\n this.audioContext = null;\r\n this.analyser = null;\r\n this.audioProcessor = null;\r\n this.audioProcessorType = null;\r\n this.audioSource = null;\r\n this.opusEncoder = null;\r\n this.pcmDataBuffer = new Int16Array();\r\n this.audioBuffers = [];\r\n this.totalAudioSize = 0;\r\n this.visualizationRequest = null;\r\n this.recordingTimer = null;\r\n this.websocket = null;\r\n\r\n // 回调函数\r\n this.onRecordingStart = null;\r\n this.onRecordingStop = null;\r\n this.onVisualizerUpdate = null;\r\n }\r\n\r\n // 设置WebSocket实例\r\n setWebSocket(ws) {\r\n this.websocket = ws;\r\n }\r\n\r\n // 获取AudioContext实例\r\n getAudioContext() {\r\n const audioPlayer = getAudioPlayer();\r\n return audioPlayer.getAudioContext();\r\n }\r\n\r\n // 初始化编码器\r\n initEncoder() {\r\n if (!this.opusEncoder) {\r\n this.opusEncoder = initOpusEncoder();\r\n }\r\n return this.opusEncoder;\r\n }\r\n\r\n // PCM处理器代码\r\n getAudioProcessorCode() {\r\n return `\r\n class AudioRecorderProcessor extends AudioWorkletProcessor {\r\n constructor() {\r\n super();\r\n this.buffers = [];\r\n this.frameSize = 960;\r\n this.buffer = new Int16Array(this.frameSize);\r\n this.bufferIndex = 0;\r\n this.isRecording = false;\r\n\r\n this.port.onmessage = (event) => {\r\n if (event.data.command === 'start') {\r\n this.isRecording = true;\r\n this.port.postMessage({ type: 'status', status: 'started' });\r\n } else if (event.data.command === 'stop') {\r\n this.isRecording = false;\r\n\r\n if (this.bufferIndex > 0) {\r\n const finalBuffer = this.buffer.slice(0, this.bufferIndex);\r\n this.port.postMessage({\r\n type: 'buffer',\r\n buffer: finalBuffer\r\n });\r\n this.bufferIndex = 0;\r\n }\r\n\r\n this.port.postMessage({ type: 'status', status: 'stopped' });\r\n }\r\n };\r\n }\r\n\r\n process(inputs, outputs, parameters) {\r\n if (!this.isRecording) return true;\r\n\r\n const input = inputs[0][0];\r\n if (!input) return true;\r\n\r\n for (let i = 0; i < input.length; i++) {\r\n if (this.bufferIndex >= this.frameSize) {\r\n this.port.postMessage({\r\n type: 'buffer',\r\n buffer: this.buffer.slice(0)\r\n });\r\n this.bufferIndex = 0;\r\n }\r\n\r\n this.buffer[this.bufferIndex++] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767)));\r\n }\r\n\r\n return true;\r\n }\r\n }\r\n\r\n registerProcessor('audio-recorder-processor', AudioRecorderProcessor);\r\n `;\r\n }\r\n\r\n // 创建音频处理器\r\n async createAudioProcessor() {\r\n this.audioContext = this.getAudioContext();\r\n\r\n try {\r\n if (this.audioContext.audioWorklet) {\r\n const blob = new Blob([this.getAudioProcessorCode()], {\r\n type: \"application/javascript\",\r\n });\r\n const url = URL.createObjectURL(blob);\r\n await this.audioContext.audioWorklet.addModule(url);\r\n URL.revokeObjectURL(url);\r\n\r\n const audioProcessor = new AudioWorkletNode(\r\n this.audioContext,\r\n \"audio-recorder-processor\"\r\n );\r\n\r\n audioProcessor.port.onmessage = (event) => {\r\n if (event.data.type === \"buffer\") {\r\n this.processPCMBuffer(event.data.buffer);\r\n }\r\n };\r\n\r\n console.log(\"使用AudioWorklet处理音频\", \"success\");\r\n\r\n const silent = this.audioContext.createGain();\r\n silent.gain.value = 0;\r\n audioProcessor.connect(silent);\r\n silent.connect(this.audioContext.destination);\r\n return { node: audioProcessor, type: \"worklet\" };\r\n } else {\r\n console.log(\r\n \"AudioWorklet不可用,使用ScriptProcessorNode作为回退方案\",\r\n \"warning\"\r\n );\r\n return this.createScriptProcessor();\r\n }\r\n } catch (error) {\r\n console.log(\r\n `创建音频处理器失败: ${error.message},尝试回退方案`,\r\n \"error\"\r\n );\r\n return this.createScriptProcessor();\r\n }\r\n }\r\n\r\n // 创建ScriptProcessor作为回退\r\n createScriptProcessor() {\r\n try {\r\n const frameSize = 4096;\r\n const scriptProcessor = this.audioContext.createScriptProcessor(\r\n frameSize,\r\n 1,\r\n 1\r\n );\r\n\r\n scriptProcessor.onaudioprocess = (event) => {\r\n if (!this.isRecording) return;\r\n\r\n const input = event.inputBuffer.getChannelData(0);\r\n const buffer = new Int16Array(input.length);\r\n\r\n for (let i = 0; i < input.length; i++) {\r\n buffer[i] = Math.max(\r\n -32768,\r\n Math.min(32767, Math.floor(input[i] * 32767))\r\n );\r\n }\r\n\r\n this.processPCMBuffer(buffer);\r\n };\r\n\r\n const silent = this.audioContext.createGain();\r\n silent.gain.value = 0;\r\n scriptProcessor.connect(silent);\r\n silent.connect(this.audioContext.destination);\r\n\r\n console.log(\"使用ScriptProcessorNode作为回退方案成功\", \"warning\");\r\n return { node: scriptProcessor, type: \"processor\" };\r\n } catch (fallbackError) {\r\n console.log(`回退方案也失败: ${fallbackError.message}`, \"error\");\r\n return null;\r\n }\r\n }\r\n\r\n // 处理PCM缓冲数据\r\n processPCMBuffer(buffer) {\r\n if (!this.isRecording) return;\r\n\r\n const newBuffer = new Int16Array(this.pcmDataBuffer.length + buffer.length);\r\n newBuffer.set(this.pcmDataBuffer);\r\n newBuffer.set(buffer, this.pcmDataBuffer.length);\r\n this.pcmDataBuffer = newBuffer;\r\n\r\n const samplesPerFrame = 960;\r\n\r\n while (this.pcmDataBuffer.length >= samplesPerFrame) {\r\n const frameData = this.pcmDataBuffer.slice(0, samplesPerFrame);\r\n this.pcmDataBuffer = this.pcmDataBuffer.slice(samplesPerFrame);\r\n\r\n this.encodeAndSendOpus(frameData);\r\n }\r\n }\r\n\r\n // 编码并发送Opus数据\r\n encodeAndSendOpus(pcmData = null) {\r\n if (!this.opusEncoder) {\r\n console.log(\"Opus编码器未初始化\", \"error\");\r\n return;\r\n }\r\n\r\n try {\r\n if (pcmData) {\r\n const opusData = this.opusEncoder.encode(pcmData);\r\n if (opusData && opusData.length > 0) {\r\n this.audioBuffers.push(opusData.buffer);\r\n this.totalAudioSize += opusData.length;\r\n\r\n if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {\r\n try {\r\n this.websocket.send(opusData.buffer);\r\n } catch (error) {\r\n console.log(`WebSocket发送错误: ${error.message}`, \"error\");\r\n }\r\n }\r\n } else {\r\n log(\"Opus编码失败,无有效数据返回\", \"error\");\r\n }\r\n } else {\r\n if (this.pcmDataBuffer.length > 0) {\r\n const samplesPerFrame = 960;\r\n if (this.pcmDataBuffer.length < samplesPerFrame) {\r\n const paddedBuffer = new Int16Array(samplesPerFrame);\r\n paddedBuffer.set(this.pcmDataBuffer);\r\n this.encodeAndSendOpus(paddedBuffer);\r\n } else {\r\n this.encodeAndSendOpus(\r\n this.pcmDataBuffer.slice(0, samplesPerFrame)\r\n );\r\n }\r\n this.pcmDataBuffer = new Int16Array(0);\r\n }\r\n }\r\n } catch (error) {\r\n console.log(`Opus编码错误: ${error.message}`, \"error\");\r\n }\r\n }\r\n\r\n // 开始录音\r\n async start() {\r\n try {\r\n if (!this.initEncoder()) {\r\n console.log(\"无法启动录音: Opus编码器初始化失败\", \"error\");\r\n return false;\r\n }\r\n\r\n const stream = await navigator.mediaDevices.getUserMedia({\r\n audio: {\r\n echoCancellation: true,\r\n noiseSuppression: true,\r\n sampleRate: 16000,\r\n channelCount: 1,\r\n latency: { ideal: 0.02, max: 0.05 },\r\n // Chrome 扩展参数(非标准,可能变动)\r\n googNoiseSuppression: true, // 启用 Chrome 噪声抑制\r\n googNoiseSuppression2: 3, // 级别设置(1-3,数值越高抑制越强,不同版本可能有差异)\r\n googAutoGainControl: true, // 自动增益控制\r\n googHighpassFilter: true, // 高通滤波器(过滤低频噪声)\r\n },\r\n });\r\n\r\n this.audioContext = this.getAudioContext();\r\n\r\n if (this.audioContext.state === \"suspended\") {\r\n await this.audioContext.resume();\r\n }\r\n\r\n const processorResult = await this.createAudioProcessor();\r\n if (!processorResult) {\r\n console.log(\"无法创建音频处理器\", \"error\");\r\n return false;\r\n }\r\n\r\n this.audioProcessor = processorResult.node;\r\n this.audioProcessorType = processorResult.type;\r\n\r\n this.audioSource = this.audioContext.createMediaStreamSource(stream);\r\n this.analyser = this.audioContext.createAnalyser();\r\n this.analyser.fftSize = 2048;\r\n\r\n this.audioSource.connect(this.analyser);\r\n this.audioSource.connect(this.audioProcessor);\r\n\r\n this.pcmDataBuffer = new Int16Array();\r\n this.audioBuffers = [];\r\n this.totalAudioSize = 0;\r\n this.isRecording = true;\r\n\r\n if (this.audioProcessorType === \"worklet\" && this.audioProcessor.port) {\r\n this.audioProcessor.port.postMessage({ command: \"start\" });\r\n }\r\n\r\n // 发送监听开始消息\r\n if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {\r\n const listenMessage = {\r\n type: \"listen\",\r\n mode: localStorage.getItem(\"listenMode\") || \"wakeup\",\r\n state: \"start\",\r\n };\r\n\r\n console.log(\r\n `发送录音开始消息: ${JSON.stringify(listenMessage)}`,\r\n \"info\"\r\n );\r\n this.websocket.send(JSON.stringify(listenMessage));\r\n } else {\r\n console.log(\"WebSocket未连接,无法发送开始消息\", \"error\");\r\n return false;\r\n }\r\n\r\n // 启动录音计时器\r\n let recordingSeconds = 0;\r\n this.recordingTimer = setInterval(() => {\r\n recordingSeconds += 0.1;\r\n if (this.onRecordingStart) {\r\n this.onRecordingStart(recordingSeconds);\r\n }\r\n }, 100);\r\n\r\n console.log(\"开始PCM直接录音\", \"success\");\r\n return true;\r\n } catch (error) {\r\n console.log(`直接录音启动错误: ${error.message}`, \"error\");\r\n this.isRecording = false;\r\n return false;\r\n }\r\n }\r\n\r\n // 停止录音\r\n stop() {\r\n if (!this.isRecording) return false;\r\n\r\n try {\r\n this.isRecording = false;\r\n\r\n if (this.audioProcessor) {\r\n if (this.audioProcessorType === \"worklet\" && this.audioProcessor.port) {\r\n this.audioProcessor.port.postMessage({ command: \"stop\" });\r\n }\r\n\r\n this.audioProcessor.disconnect();\r\n this.audioProcessor = null;\r\n }\r\n\r\n if (this.audioSource) {\r\n this.audioSource.disconnect();\r\n this.audioSource = null;\r\n }\r\n\r\n if (this.visualizationRequest) {\r\n cancelAnimationFrame(this.visualizationRequest);\r\n this.visualizationRequest = null;\r\n }\r\n\r\n if (this.recordingTimer) {\r\n clearInterval(this.recordingTimer);\r\n this.recordingTimer = null;\r\n }\r\n\r\n // 编码并发送剩余的数据\r\n this.encodeAndSendOpus();\r\n\r\n // 发送结束信号\r\n if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {\r\n const emptyOpusFrame = new Uint8Array(0);\r\n this.websocket.send(emptyOpusFrame);\r\n\r\n const stopMessage = {\r\n type: \"listen\",\r\n mode: localStorage.getItem(\"listenMode\") || \"wakeup\",\r\n state: \"stop\",\r\n };\r\n\r\n this.websocket.send(JSON.stringify(stopMessage));\r\n console.log(\"已发送录音停止信号\", \"info\");\r\n }\r\n\r\n if (this.onRecordingStop) {\r\n this.onRecordingStop();\r\n }\r\n\r\n console.log(\"停止PCM直接录音\", \"success\");\r\n return true;\r\n } catch (error) {\r\n console.log(`直接录音停止错误: ${error.message}`, \"error\");\r\n return false;\r\n }\r\n }\r\n\r\n // 获取分析器\r\n getAnalyser() {\r\n return this.analyser;\r\n }\r\n}\r\n\r\n// 创建单例\r\nlet audioRecorderInstance = null;\r\n\r\nexport function getAudioRecorder() {\r\n if (!audioRecorderInstance) {\r\n audioRecorderInstance = new AudioRecorder();\r\n }\r\n return audioRecorderInstance;\r\n}\r\n","// WebSocket 连接\r\nexport async function webSocketConnect(otaUrl, config) {\r\n\r\n if (!validateConfig(config)) {\r\n return;\r\n }\r\n\r\n // 发送OTA请求并获取返回的websocket信息\r\n const otaResult = await sendOTA(otaUrl, config);\r\n if (!otaResult) {\r\n console.log('无法从OTA服务器获取信息', 'error');\r\n return;\r\n }\r\n\r\n // 从OTA响应中提取websocket信息\r\n const { websocket } = otaResult;\r\n if (!websocket || !websocket.url) {\r\n console.log('OTA响应中缺少websocket信息', 'error');\r\n return;\r\n }\r\n\r\n // 使用OTA返回的websocket URL\r\n let connUrl = new URL(websocket.url);\r\n\r\n // 添加token参数(从OTA响应中获取)\r\n if (websocket.token) {\r\n if (websocket.token.startsWith(\"Bearer \")) {\r\n connUrl.searchParams.append('authorization', websocket.token);\r\n } else {\r\n connUrl.searchParams.append('authorization', 'Bearer ' + websocket.token);\r\n }\r\n }\r\n\r\n // 添加认证参数(保持原有逻辑)\r\n connUrl.searchParams.append('device-id', config.deviceId);\r\n connUrl.searchParams.append('client-id', config.clientId);\r\n\r\n const wsurl = connUrl.toString()\r\n\r\n console.log(`正在连接: ${wsurl}`, 'info');\r\n\r\n return new WebSocket(connUrl.toString());\r\n}\r\n\r\n// 验证配置\r\nfunction validateConfig(config) {\r\n if (!config.deviceMac) {\r\n console.log('设备MAC地址不能为空', 'error');\r\n return false;\r\n }\r\n if (!config.clientId) {\r\n console.log('客户端ID不能为空', 'error');\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\n// 判断wsUrl路径是否存在错误\r\nfunction validateWsUrl(wsUrl) {\r\n if (wsUrl === '') return false;\r\n // 检查URL格式\r\n if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {\r\n console.log('URL格式错误,必须以ws://或wss://开头', 'error');\r\n return false;\r\n }\r\n return true\r\n}\r\n\r\n\r\n// OTA发送请求,验证状态,并返回响应数据\r\nasync function sendOTA(otaUrl, config) {\r\n try {\r\n const res = await fetch(otaUrl, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Device-Id': config.deviceId,\r\n 'Client-Id': config.clientId\r\n },\r\n body: JSON.stringify({\r\n version: 0,\r\n uuid: '',\r\n application: {\r\n name: 'xiaozhi-web-test',\r\n version: '1.0.0',\r\n compile_time: '2025-04-16 10:00:00',\r\n idf_version: '4.4.3',\r\n elf_sha256: '1234567890abcdef1234567890abcdef1234567890abcdef'\r\n },\r\n ota: { label: 'xiaozhi-web-test' },\r\n board: {\r\n type: 'xiaozhi-web-test',\r\n ssid: 'xiaozhi-web-test',\r\n rssi: 0,\r\n channel: 0,\r\n ip: '192.168.1.1',\r\n mac: config.deviceMac\r\n },\r\n flash_size: 0,\r\n minimum_free_heap_size: 0,\r\n mac_address: config.deviceMac,\r\n chip_model_name: '',\r\n chip_info: { model: 0, cores: 0, revision: 0, features: 0 },\r\n partition_table: [{ label: '', type: 0, subtype: 0, address: 0, size: 0 }]\r\n })\r\n });\r\n\r\n if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);\r\n\r\n const result = await res.json();\r\n return result; // 返回完整的响应数据\r\n } catch (err) {\r\n return null; // 失败返回null\r\n }\r\n}","// 生成随机MAC地址\r\nfunction generateRandomMac() {\r\n const hexDigits = \"0123456789ABCDEF\";\r\n let mac = \"\";\r\n for (let i = 0; i < 6; i++) {\r\n if (i > 0) mac += \":\";\r\n for (let j = 0; j < 2; j++) {\r\n mac += hexDigits.charAt(Math.floor(Math.random() * 16));\r\n }\r\n }\r\n return mac;\r\n}\r\n\r\n// 加载配置\r\nexport function loadConfig() {\r\n // 从localStorage加载MAC地址,如果没有则生成新的\r\n let savedMac = localStorage.getItem(\"xz_tester_deviceMac\");\r\n if (!savedMac) {\r\n savedMac = generateRandomMac();\r\n localStorage.setItem(\"xz_tester_deviceMac\", savedMac);\r\n }\r\n}\r\n\r\n// 获取配置值\r\nexport function getConfig() {\r\n return {\r\n deviceId: localStorage.getItem(\"MAC\"), // 使用MAC地址作为deviceId\r\n deviceName: \"测试设备\",\r\n deviceMac: localStorage.getItem(\"MAC\"),\r\n clientId: \"web_test_client\",\r\n token: \"your-token1\",\r\n };\r\n}\r\n\r\n// 保存连接URL\r\nexport function saveConnectionUrls() {\r\n const otaUrl = localStorage.getItem(\"otaUrl\");\r\n localStorage.setItem(\"xz_tester_otaUrl\", otaUrl);\r\n}\r\n","// WebSocket消息处理模块\r\nimport { webSocketConnect } from \"./ota-connector.js\";\r\nimport { getConfig, saveConnectionUrls } from \"./manager.js\";\r\nimport { getAudioPlayer } from \"./player.js\";\r\nimport { getAudioRecorder } from \"./recorder.js\";\r\nimport {\r\n getMcpTools,\r\n executeMcpTool,\r\n setWebSocket as setMcpWebSocket,\r\n} from \"./tools.js\";\r\n\r\n// WebSocket处理器类\r\nexport class WebSocketHandler {\r\n constructor() {\r\n this.websocket = null;\r\n this.onConnectionStateChange = null;\r\n this.onRecordButtonStateChange = null;\r\n this.onSessionStateChange = null;\r\n this.onSessionEmotionChange = null;\r\n this.currentSessionId = null;\r\n this.isRemoteSpeaking = false;\r\n this.heartbeatTimer = null;\r\n\r\n // 重连相关配置\r\n this.reconnectConfig = {\r\n maxRetries: 10, // 最大重连次数\r\n baseDelay: 1000, // 基础重连延迟(ms)\r\n maxDelay: 30000, // 最大重连延迟(ms)\r\n retryCount: 0, // 当前重连次数\r\n reconnectTimer: null, // 重连定时器\r\n isReconnecting: false, // 是否正在重连中\r\n manualDisconnect: false, // 是否手动断开连接\r\n };\r\n }\r\n\r\n // 在 WebSocketHandler 类中添加\r\n startHeartbeat() {\r\n this.stopHeartbeat(); // 先清除之前的定时器\r\n this.sendHeartbeat();\r\n }\r\n\r\n stopHeartbeat() {\r\n if (this.heartbeatTimer) {\r\n clearTimeout(this.heartbeatTimer);\r\n this.heartbeatTimer = null;\r\n }\r\n }\r\n\r\n sendHeartbeat() {\r\n if (this.websocket?.readyState === WebSocket.OPEN) {\r\n try {\r\n this.websocket.send(\"1\");\r\n } catch (error) {\r\n console.error(\"心跳发送失败:\", error);\r\n }\r\n }\r\n\r\n this.heartbeatTimer = setTimeout(() => {\r\n this.sendHeartbeat();\r\n }, 5000);\r\n }\r\n\r\n // 发送hello握手消息\r\n async sendHelloMessage() {\r\n if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN)\r\n return false;\r\n\r\n this.startHeartbeat(); // 启动心跳\r\n try {\r\n const config = getConfig();\r\n\r\n const helloMessage = {\r\n type: \"hello\",\r\n device_id: config.deviceId,\r\n device_name: config.deviceName,\r\n device_mac: config.deviceMac,\r\n token: config.token,\r\n seat: localStorage.getItem(\"SEAT\"),\r\n features: {\r\n mcp: true,\r\n },\r\n };\r\n\r\n console.log(\"发送hello握手消息\", \"info\");\r\n this.websocket.send(JSON.stringify(helloMessage));\r\n\r\n return new Promise((resolve) => {\r\n const timeout = setTimeout(() => {\r\n console.log(\"等待hello响应超时\", \"error\");\r\n console.log('提示: 请尝试点击\"测试认证\"按钮进行连接排查', \"info\");\r\n resolve(false);\r\n }, 5000);\r\n\r\n const onMessageHandler = (event) => {\r\n try {\r\n const response = JSON.parse(event.data);\r\n if (response.type === \"hello\" && response.session_id) {\r\n console.log(\r\n `服务器握手成功,会话ID: ${response.session_id}`,\r\n \"success\"\r\n );\r\n // 握手成功后重置重连计数器\r\n this.reconnectConfig.retryCount = 0;\r\n clearTimeout(timeout);\r\n this.websocket.removeEventListener(\"message\", onMessageHandler);\r\n resolve(true);\r\n }\r\n } catch (e) {\r\n // 忽略非JSON消息\r\n }\r\n };\r\n\r\n this.websocket.addEventListener(\"message\", onMessageHandler);\r\n });\r\n } catch (error) {\r\n console.log(`发送hello消息错误: ${error.message}`, \"error\");\r\n return false;\r\n }\r\n }\r\n\r\n // 处理文本消息\r\n handleTextMessage(message) {\r\n if (message.type === \"hello\") {\r\n } else if (message.type === \"tts\") {\r\n this.handleTTSMessage(message);\r\n } else if (message.type === \"audio\") {\r\n } else if (message.type === \"stt\") {\r\n const event = new CustomEvent(\"wsSendMessage\", {\r\n detail: message,\r\n });\r\n window.dispatchEvent(event);\r\n } else if (message.type === \"llm\") {\r\n } else if (message.type === \"mcp\") {\r\n this.handleMCPMessage(message);\r\n } else if (message.type === \"json_data\") {\r\n if (message.state === \"drinks\") {\r\n const event = new CustomEvent(\"drinkListEvent\", {\r\n detail: message.data,\r\n });\r\n window.dispatchEvent(event);\r\n } else if (message.state === \"book\") {\r\n const event = new CustomEvent(\"bookListEvent\", {\r\n detail: message\r\n });\r\n window.dispatchEvent(event);\r\n }\r\n } else if (message.type === \"view_action\") {\r\n const event = new CustomEvent(\"viewActionEvent\", {\r\n detail: message.state,\r\n });\r\n window.dispatchEvent(event);\r\n } else {\r\n console.log(`未知消息类型: ${message.type}`, \"warning\");\r\n }\r\n }\r\n\r\n // 处理TTS消息\r\n handleTTSMessage(message) {\r\n if (message.state === \"start\") {\r\n console.log(\"服务器开始发送语音\", \"info\");\r\n this.currentSessionId = message.session_id;\r\n const event = new CustomEvent(\"startThink\");\r\n window.dispatchEvent(event);\r\n this.isRemoteSpeaking = true;\r\n if (this.onSessionStateChange) {\r\n this.onSessionStateChange(true);\r\n }\r\n } else if (message.state === \"sentence_start\") {\r\n const event = new CustomEvent(\"startVolic\", {\r\n detail: message.text,\r\n });\r\n window.dispatchEvent(event);\r\n console.log(`服务器发送语音段: ${message.text}`, \"info\");\r\n } else if (message.state === \"sentence_end\") {\r\n console.log(`语音段结束: ${message.text}`, \"info\");\r\n } else if (message.state === \"stop\") {\r\n const event = new CustomEvent(\"stopVolic\");\r\n window.dispatchEvent(event);\r\n console.log(\"服务器语音传输结束\", \"info\");\r\n this.isRemoteSpeaking = false;\r\n if (this.onRecordButtonStateChange) {\r\n this.onRecordButtonStateChange(false);\r\n }\r\n if (this.onSessionStateChange) {\r\n this.onSessionStateChange(false);\r\n }\r\n }\r\n }\r\n\r\n // 处理MCP消息\r\n handleMCPMessage(message) {\r\n const payload = message.payload || {};\r\n console.log(`服务器下发: ${JSON.stringify(message)}`, \"info\");\r\n\r\n if (payload.method === \"tools/list\") {\r\n const tools = getMcpTools();\r\n const replyMessage = JSON.stringify({\r\n session_id: message.session_id || \"\",\r\n type: \"mcp\",\r\n payload: {\r\n jsonrpc: \"2.0\",\r\n id: payload.id,\r\n result: {\r\n tools: tools,\r\n },\r\n },\r\n });\r\n console.log(`客户端上报: ${replyMessage}`, \"info\");\r\n this.websocket.send(replyMessage);\r\n console.log(`回复MCP工具列表: ${tools.length} 个工具`, \"info\");\r\n } else if (payload.method === \"tools/call\") {\r\n const toolName = payload.params?.name;\r\n const toolArgs = payload.params?.arguments;\r\n\r\n console.log(\r\n `调用工具: ${toolName} 参数: ${JSON.stringify(toolArgs)}`,\r\n \"info\"\r\n );\r\n\r\n const result = executeMcpTool(toolName, toolArgs);\r\n\r\n const replyMessage = JSON.stringify({\r\n session_id: message.session_id || \"\",\r\n type: \"mcp\",\r\n payload: {\r\n jsonrpc: \"2.0\",\r\n id: payload.id,\r\n result: {\r\n content: [\r\n {\r\n type: \"text\",\r\n text: JSON.stringify(result),\r\n },\r\n ],\r\n isError: false,\r\n },\r\n },\r\n });\r\n\r\n console.log(`客户端上报: ${replyMessage}`, \"info\");\r\n this.websocket.send(replyMessage);\r\n } else if (payload.method === \"initialize\") {\r\n console.log(\r\n `收到工具初始化请求: ${JSON.stringify(payload.params)}`,\r\n \"info\"\r\n );\r\n } else {\r\n console.log(`未知的MCP方法: ${payload.method}`, \"warning\");\r\n }\r\n }\r\n\r\n // 处理二进制消息\r\n async handleBinaryMessage(data) {\r\n try {\r\n let arrayBuffer;\r\n if (data instanceof ArrayBuffer) {\r\n arrayBuffer = data;\r\n } else if (data instanceof Blob) {\r\n arrayBuffer = await data.arrayBuffer();\r\n console.log(\r\n `收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`,\r\n \"debug\"\r\n );\r\n } else {\r\n console.log(`收到未知类型的二进制数据: ${typeof data}`, \"warning\");\r\n return;\r\n }\r\n\r\n const opusData = new Uint8Array(arrayBuffer);\r\n const audioPlayer = getAudioPlayer();\r\n audioPlayer.enqueueAudioData(opusData);\r\n } catch (error) {\r\n console.log(`处理二进制消息出错: ${error.message}`, \"error\");\r\n }\r\n }\r\n\r\n // 计算重连延迟(指数退避策略)\r\n calculateReconnectDelay() {\r\n // 指数退避 + 随机抖动,避免多个客户端同时重连\r\n const delay = Math.min(\r\n this.reconnectConfig.baseDelay *\r\n Math.pow(2, this.reconnectConfig.retryCount),\r\n this.reconnectConfig.maxDelay\r\n );\r\n // 添加±20%的随机抖动\r\n const jitter = delay * 0.2 * (Math.random() - 0.5);\r\n return Math.round(delay + jitter);\r\n }\r\n\r\n // 触发自动重连\r\n triggerReconnect() {\r\n // 如果是手动断开连接,不进行重连\r\n if (this.reconnectConfig.manualDisconnect) {\r\n console.log(\"手动断开连接,不进行自动重连\", \"info\");\r\n return;\r\n }\r\n\r\n // 检查是否达到最大重连次数\r\n if (this.reconnectConfig.retryCount >= this.reconnectConfig.maxRetries) {\r\n console.log(\r\n `已达到最大重连次数(${this.reconnectConfig.maxRetries}),停止重连`,\r\n \"error\"\r\n );\r\n this.reconnectConfig.isReconnecting = false;\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n return;\r\n }\r\n\r\n // 计算重连延迟\r\n const delay = this.calculateReconnectDelay();\r\n this.reconnectConfig.retryCount++;\r\n\r\n console.log(\r\n `准备进行第${this.reconnectConfig.retryCount}次重连,延迟${delay}ms`,\r\n \"info\"\r\n );\r\n\r\n // 设置重连定时器\r\n this.reconnectConfig.reconnectTimer = setTimeout(async () => {\r\n console.log(`开始第${this.reconnectConfig.retryCount}次重连`, \"info\");\r\n try {\r\n const success = await this.connect();\r\n if (success) {\r\n console.log(\"重连成功\", \"success\");\r\n this.reconnectConfig.isReconnecting = false;\r\n } else {\r\n console.log(\r\n `第${this.reconnectConfig.retryCount}次重连失败`,\r\n \"error\"\r\n );\r\n this.triggerReconnect();\r\n }\r\n } catch (error) {\r\n console.log(`重连出错: ${error.message}`, \"error\");\r\n this.triggerReconnect();\r\n }\r\n }, delay);\r\n }\r\n\r\n // 停止自动重连\r\n stopReconnect() {\r\n if (this.reconnectConfig.reconnectTimer) {\r\n clearTimeout(this.reconnectConfig.reconnectTimer);\r\n this.reconnectConfig.reconnectTimer = null;\r\n }\r\n this.reconnectConfig.isReconnecting = false;\r\n this.reconnectConfig.retryCount = 0;\r\n }\r\n\r\n // 连接WebSocket服务器\r\n async connect() {\r\n // 如果正在重连中,先停止之前的重连\r\n this.stopReconnect();\r\n\r\n const config = getConfig();\r\n console.log(\"正在检查OTA状态...\", \"info\");\r\n saveConnectionUrls();\r\n\r\n try {\r\n const otaUrl = localStorage.getItem(\"xz_tester_otaUrl\");\r\n const ws = await webSocketConnect(otaUrl, config);\r\n if (ws === undefined) {\r\n // 连接失败,触发重连\r\n if (\r\n !this.reconnectConfig.isReconnecting &&\r\n !this.reconnectConfig.manualDisconnect\r\n ) {\r\n this.reconnectConfig.isReconnecting = true;\r\n this.triggerReconnect();\r\n }\r\n return false;\r\n }\r\n\r\n this.websocket = ws;\r\n\r\n // 设置接收二进制数据的类型为ArrayBuffer\r\n this.websocket.binaryType = \"arraybuffer\";\r\n\r\n // 设置 MCP 模块的 WebSocket 实例\r\n setMcpWebSocket(this.websocket);\r\n\r\n // 设置录音器的WebSocket\r\n const audioRecorder = getAudioRecorder();\r\n audioRecorder.setWebSocket(this.websocket);\r\n\r\n this.setupEventHandlers();\r\n\r\n return true;\r\n } catch (error) {\r\n console.log(`连接错误: ${error.message}`, \"error\");\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n\r\n // 连接出错,触发重连\r\n if (\r\n !this.reconnectConfig.isReconnecting &&\r\n !this.reconnectConfig.manualDisconnect\r\n ) {\r\n this.reconnectConfig.isReconnecting = true;\r\n this.triggerReconnect();\r\n }\r\n\r\n return false;\r\n }\r\n }\r\n\r\n // 设置事件处理器\r\n setupEventHandlers() {\r\n this.websocket.onopen = async () => {\r\n const url = localStorage.getItem(\"xz_tester_wsUrl\");\r\n console.log(`已连接到服务器: ${url}`, \"success\");\r\n\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(true);\r\n }\r\n\r\n // 连接成功后,默认状态为聆听中\r\n this.isRemoteSpeaking = false;\r\n if (this.onSessionStateChange) {\r\n this.onSessionStateChange(false);\r\n }\r\n\r\n await this.sendHelloMessage();\r\n };\r\n\r\n this.websocket.onclose = (event) => {\r\n console.log(\r\n `已断开连接,代码: ${event.code}, 原因: ${event.reason}`,\r\n \"info\"\r\n );\r\n this.stopHeartbeat();\r\n\r\n // 清除MCP的WebSocket引用\r\n setMcpWebSocket(null);\r\n\r\n // 停止录音\r\n const audioRecorder = getAudioRecorder();\r\n audioRecorder.stop();\r\n\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n\r\n // 如果不是手动断开连接,触发自动重连\r\n if (\r\n !this.reconnectConfig.manualDisconnect &&\r\n !this.reconnectConfig.isReconnecting\r\n ) {\r\n // 1000: 正常关闭, 1001: 客户端离开, 这两种情况不自动重连\r\n if (event.code !== 1000 && event.code !== 1001) {\r\n console.log(\"检测到异常断开连接,准备自动重连\", \"warning\");\r\n this.reconnectConfig.isReconnecting = true;\r\n this.triggerReconnect();\r\n }\r\n }\r\n\r\n // 重置手动断开标记(以便下次可以重连)\r\n this.reconnectConfig.manualDisconnect = false;\r\n };\r\n\r\n this.websocket.onerror = (error) => {\r\n console.log(`WebSocket错误: ${error.message || \"未知错误\"}`, \"error\");\r\n\r\n if (this.onConnectionStateChange) {\r\n this.onConnectionStateChange(false);\r\n }\r\n };\r\n\r\n this.websocket.onmessage = (event) => {\r\n try {\r\n if (typeof event.data === \"string\") {\r\n const message = JSON.parse(event.data);\r\n this.handleTextMessage(message);\r\n } else {\r\n this.handleBinaryMessage(event.data);\r\n }\r\n } catch (error) {\r\n console.log(`WebSocket消息处理错误: ${error.message}`, \"error\");\r\n }\r\n };\r\n }\r\n\r\n // 断开连接\r\n disconnect() {\r\n // 标记为手动断开连接\r\n this.reconnectConfig.manualDisconnect = true;\r\n this.stopReconnect();\r\n\r\n if (!this.websocket) return;\r\n\r\n // 正常关闭连接\r\n this.websocket.close(1000, \"Manual disconnect\");\r\n const audioRecorder = getAudioRecorder();\r\n audioRecorder.stop();\r\n }\r\n\r\n // 发送文本消息\r\n sendTextMessage(text) {\r\n try {\r\n // 如果对方正在说话,先发送打断消息\r\n const abortMessage = {\r\n session_id: this.currentSessionId,\r\n type: \"abort\",\r\n reason: \"wake_word_detected\",\r\n };\r\n this.websocket.send(JSON.stringify(abortMessage));\r\n console.log(\"发送打断消息\", \"info\");\r\n\r\n const listenMessage = {\r\n type: \"listen\",\r\n mode: localStorage.getItem(\"listenMode\") || \"wakeup\",\r\n state: \"detect\",\r\n text: text,\r\n };\r\n\r\n this.websocket.send(JSON.stringify(listenMessage));\r\n console.log(`发送文本消息: ${text}`, \"info6666\");\r\n\r\n return true;\r\n } catch (error) {\r\n console.log(`发送消息错误: ${error.message}`, \"error\");\r\n return false;\r\n }\r\n }\r\n\r\n // 获取WebSocket实例\r\n getWebSocket() {\r\n return this.websocket;\r\n }\r\n\r\n // 检查是否已连接\r\n isConnected() {\r\n return this.websocket && this.websocket.readyState === WebSocket.OPEN;\r\n }\r\n\r\n // 获取重连状态\r\n getReconnectStatus() {\r\n return {\r\n isReconnecting: this.reconnectConfig.isReconnecting,\r\n retryCount: this.reconnectConfig.retryCount,\r\n maxRetries: this.reconnectConfig.maxRetries,\r\n };\r\n }\r\n\r\n // 重置重连配置\r\n resetReconnectConfig() {\r\n this.stopReconnect();\r\n this.reconnectConfig.retryCount = 0;\r\n this.reconnectConfig.isReconnecting = false;\r\n this.reconnectConfig.manualDisconnect = false;\r\n }\r\n}\r\n\r\n// 创建单例\r\nlet wsHandlerInstance = null;\r\n\r\nexport function getWebSocketHandler() {\r\n if (!wsHandlerInstance) {\r\n wsHandlerInstance = new WebSocketHandler();\r\n }\r\n return wsHandlerInstance;\r\n}\r\n","// UI控制模块\nimport { getAudioRecorder } from \"./recorder.js\";\nimport { getWebSocketHandler } from \"./websocket.js\";\n\n// ws连接事件监听\nexport async function wsConnectEventListeners() {\n const wsHandler = getWebSocketHandler();\n await wsHandler.connect();\n}\n\n// ws关闭事件监听\nexport async function wsCloseEventListeners() {\n const wsHandler = getWebSocketHandler();\n await wsHandler.disconnect();\n}\n\n// 语音播放\nexport async function startEventAudioPlayer() {\n const audioRecorder = getAudioRecorder();\n await audioRecorder.start();\n}\n\n// 语音暂停\nexport async function stopEventAudioPlayer() {\n const audioRecorder = getAudioRecorder();\n await audioRecorder.stop();\n}\n\n\n// 判断ws是否连接\nexport function isWsConnected() {\n const wsHandler = getWebSocketHandler();\n return wsHandler.isConnected();\n}","<template></template>\r\n\r\n<script>\r\nimport { checkOpusLoaded, initOpusEncoder } from \"@/utils/opus-codec.js\";\r\nimport { getAudioPlayer } from \"@/utils/player.js\";\r\nimport { initMcpTools } from \"@/utils/tools.js\";\r\nimport {\r\n wsConnectEventListeners,\r\n wsCloseEventListeners,\r\n isWsConnected,\r\n startEventAudioPlayer,\r\n} from \"@/utils/controller.js\";\r\nexport default {\r\n name: \"ibiAiTalk\",\r\n data() {\r\n return {\r\n audioPlayer: null,\r\n };\r\n },\r\n props: {\r\n listenMode: {\r\n type: String,\r\n default: \"wakeup\",\r\n },\r\n agentId: {\r\n type: String,\r\n default: \"\",\r\n },\r\n env: {\r\n type: String,\r\n default: \"test\",\r\n },\r\n macAddress: {\r\n type: String,\r\n default: \"\",\r\n },\r\n },\r\n async mounted() {\r\n localStorage.setItem(\"MAC\", this.macAddress);\r\n localStorage.setItem(\"listenMode\", this.listenMode);\r\n localStorage.setItem(\"agentId\", this.agentId);\r\n let envUrl = \"\";\r\n if (this.env === \"test\") {\r\n envUrl = \"https://test-ai-talk-manage.ptdplat.com\";\r\n } else if (this.env === \"prod\") {\r\n envUrl = \"https://ai-talk-manage.ptdcloud.com\";\r\n }\r\n const res = await fetch(`${envUrl}/device/addByAgent`, {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n \"authorization\":\"Bearer \" + 'z6frotkj-8vdw-moy1-vc6j-manpewkvob48'\r\n },\r\n body: JSON.stringify({\r\n macAddress: this.macAddress,\r\n agentId: this.agentId,\r\n }),\r\n });\r\n console.log(res, \"添加设备\");\r\n localStorage.setItem(\"otaUrl\", `${envUrl}/xiaozhi/ota/`);\r\n checkOpusLoaded();\r\n initOpusEncoder();\r\n initMcpTools();\r\n if (!isWsConnected()) {\r\n await wsConnectEventListeners();\r\n }\r\n this.audioPlayer = getAudioPlayer();\r\n await this.audioPlayer.start();\r\n await startEventAudioPlayer();\r\n },\r\n beforeDestroy() {\r\n this.audioPlayer.stop().catch(() => {});\r\n // 关闭WebSocket连接\r\n if (isWsConnected()) {\r\n wsCloseEventListeners().catch(() => {});\r\n }\r\n },\r\n};\r\n</script>\r\n\r\n<style></style>\r\n","import mod from \"-!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js\"; export default mod; export * from \"-!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js\"","/* globals __VUE_SSR_CONTEXT__ */\n\n// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).\n// This module is a runtime utility for cleaner component module output and will\n// be included in the final webpack user bundle.\n\nexport default function normalizeComponent(\n scriptExports,\n render,\n staticRenderFns,\n functionalTemplate,\n injectStyles,\n scopeId,\n moduleIdentifier /* server only */,\n shadowMode /* vue-cli only */\n) {\n // Vue.extend constructor export interop\n var options =\n typeof scriptExports === 'function' ? scriptExports.options : scriptExports\n\n // render functions\n if (render) {\n options.render = render\n options.staticRenderFns = staticRenderFns\n options._compiled = true\n }\n\n // functional template\n if (functionalTemplate) {\n options.functional = true\n }\n\n // scopedId\n if (scopeId) {\n options._scopeId = 'data-v-' + scopeId\n }\n\n var hook\n if (moduleIdentifier) {\n // server build\n hook = function (context) {\n // 2.3 injection\n context =\n context || // cached call\n (this.$vnode && this.$vnode.ssrContext) || // stateful\n (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional\n // 2.2 with runInNewContext: true\n if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {\n context = __VUE_SSR_CONTEXT__\n }\n // inject component styles\n if (injectStyles) {\n injectStyles.call(this, context)\n }\n // register component module identifier for async chunk inferrence\n if (context && context._registeredComponents) {\n context._registeredComponents.add(moduleIdentifier)\n }\n }\n // used by ssr in case component is cached and beforeCreate\n // never gets called\n options._ssrRegister = hook\n } else if (injectStyles) {\n hook = shadowMode\n ? function () {\n injectStyles.call(\n this,\n (options.functional ? this.parent : this).$root.$options.shadowRoot\n )\n }\n : injectStyles\n }\n\n if (hook) {\n if (options.functional) {\n // for template-only hot-reload because in that case the render fn doesn't\n // go through the normalizer\n options._injectStyles = hook\n // register for functional component in vue file\n var originalRender = options.render\n options.render = function renderWithStyleInjection(h, context) {\n hook.call(context)\n return originalRender(h, context)\n }\n } else {\n // inject component registration as beforeCreate hook\n var existing = options.beforeCreate\n options.beforeCreate = existing ? [].concat(existing, hook) : [hook]\n }\n }\n\n return {\n exports: scriptExports,\n options: options\n }\n}\n","import { render, staticRenderFns } from \"./index.vue?vue&type=template&id=14905db4\"\nimport script from \"./index.vue?vue&type=script&lang=js\"\nexport * from \"./index.vue?vue&type=script&lang=js\"\n\n\n/* normalize component */\nimport normalizer from \"!../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import './setPublicPath'\nimport mod from '~entry'\nexport default mod\nexport * from '~entry'\n"],"names":[],"sourceRoot":""}
|