@siact/sime-x-vue 0.0.7 → 0.0.8
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/dist/sime-x-vue.mjs +1263 -53
- package/dist/sime-x-vue.mjs.map +1 -1
- package/dist/sime-x-vue.umd.js +1262 -51
- package/dist/sime-x-vue.umd.js.map +1 -1
- package/dist/style.css +412 -83
- package/package.json +1 -1
- package/types/components/sime-provider.vue.d.ts +4 -4
- package/types/components/tool-card.vue.d.ts +28 -0
- package/types/components/voice-assistant.vue.d.ts +44 -0
- package/types/composables/index.d.ts +6 -0
- package/types/composables/use-agent-invoke.d.ts +79 -0
- package/types/composables/use-bubble.d.ts +39 -0
- package/types/composables/use-tts.d.ts +17 -0
- package/types/composables/use-voice-recognition.d.ts +28 -0
- package/types/index.d.ts +1 -0
- package/types/lib/data-stream-parser.d.ts +84 -0
- package/types/types.d.ts +11 -1
package/dist/sime-x-vue.umd.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
return clientCommandKey2;
|
|
32
32
|
})(clientCommandKey || {});
|
|
33
33
|
|
|
34
|
-
const _sfc_main$
|
|
34
|
+
const _sfc_main$4 = /* @__PURE__ */ vue.defineComponent({
|
|
35
35
|
__name: "sime-provider",
|
|
36
36
|
props: {
|
|
37
37
|
project: {},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
setup(__props) {
|
|
46
46
|
const props = __props;
|
|
47
|
-
const hostBridge = vue.shallowRef(new simeBridge.HostBridge());
|
|
47
|
+
const hostBridge = vue.shallowRef(new simeBridge.HostBridge({ debug: false }));
|
|
48
48
|
const startListeningRef = vue.shallowRef(async () => {
|
|
49
49
|
});
|
|
50
50
|
const stopListeningRef = vue.shallowRef(async () => {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
chatbotUrl: () => props.chatbotUrl,
|
|
60
60
|
appId: () => props.appId,
|
|
61
61
|
appToken: () => props.appToken,
|
|
62
|
-
voiceConfig: () => props.voiceConfig,
|
|
62
|
+
voiceConfig: () => props.voiceConfig || { appId: "", apiKey: "", websocketUrl: "" },
|
|
63
63
|
startListening: () => startListeningRef.value(),
|
|
64
64
|
stopListening: () => stopListeningRef.value(),
|
|
65
65
|
toggleCollapse: () => toggleCollapseRef.value(),
|
|
@@ -108,18 +108,18 @@
|
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
const _hoisted_1$
|
|
112
|
-
const _hoisted_2$
|
|
113
|
-
const _hoisted_3$
|
|
114
|
-
const _hoisted_4$
|
|
111
|
+
const _hoisted_1$3 = { class: "content-container" };
|
|
112
|
+
const _hoisted_2$3 = { class: "status-header" };
|
|
113
|
+
const _hoisted_3$2 = { class: "status-text" };
|
|
114
|
+
const _hoisted_4$2 = {
|
|
115
115
|
key: 0,
|
|
116
116
|
class: "transcription-content"
|
|
117
117
|
};
|
|
118
|
-
const _hoisted_5$
|
|
118
|
+
const _hoisted_5$2 = {
|
|
119
119
|
key: 1,
|
|
120
120
|
class: "placeholder-text"
|
|
121
121
|
};
|
|
122
|
-
const _sfc_main$
|
|
122
|
+
const _sfc_main$3 = /* @__PURE__ */ vue.defineComponent({
|
|
123
123
|
__name: "voice-status",
|
|
124
124
|
props: {
|
|
125
125
|
status: {},
|
|
@@ -181,9 +181,9 @@
|
|
|
181
181
|
])
|
|
182
182
|
])
|
|
183
183
|
], -1)),
|
|
184
|
-
vue.createElementVNode("div", _hoisted_1$
|
|
185
|
-
vue.createElementVNode("div", _hoisted_2$
|
|
186
|
-
vue.createElementVNode("span", _hoisted_3$
|
|
184
|
+
vue.createElementVNode("div", _hoisted_1$3, [
|
|
185
|
+
vue.createElementVNode("div", _hoisted_2$3, [
|
|
186
|
+
vue.createElementVNode("span", _hoisted_3$2, vue.toDisplayString(statusLabel.value), 1)
|
|
187
187
|
]),
|
|
188
188
|
vue.createElementVNode("div", {
|
|
189
189
|
class: vue.normalizeClass(["text-window", { "has-text": !!__props.transcriptionText }])
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
mode: "out-in"
|
|
194
194
|
}, {
|
|
195
195
|
default: vue.withCtx(() => [
|
|
196
|
-
__props.transcriptionText ? (vue.openBlock(), vue.createElementBlock("p", _hoisted_4$
|
|
196
|
+
__props.transcriptionText ? (vue.openBlock(), vue.createElementBlock("p", _hoisted_4$2, vue.toDisplayString(__props.transcriptionText), 1)) : __props.status === "wake" ? (vue.openBlock(), vue.createElementBlock("p", _hoisted_5$2, "Listening...")) : vue.createCommentVNode("", true)
|
|
197
197
|
]),
|
|
198
198
|
_: 1
|
|
199
199
|
})
|
|
@@ -215,14 +215,14 @@
|
|
|
215
215
|
return target;
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
-
const VoiceStatus = /* @__PURE__ */ _export_sfc(_sfc_main$
|
|
218
|
+
const VoiceStatus = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-c9fa6caf"]]);
|
|
219
219
|
|
|
220
|
-
const _hoisted_1$
|
|
220
|
+
const _hoisted_1$2 = {
|
|
221
221
|
key: 0,
|
|
222
222
|
class: "execution-bubble"
|
|
223
223
|
};
|
|
224
|
-
const _hoisted_2$
|
|
225
|
-
const _sfc_main$
|
|
224
|
+
const _hoisted_2$2 = { class: "exec-text" };
|
|
225
|
+
const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({
|
|
226
226
|
__name: "execution-status",
|
|
227
227
|
props: {
|
|
228
228
|
visible: { type: Boolean },
|
|
@@ -232,8 +232,8 @@
|
|
|
232
232
|
return (_ctx, _cache) => {
|
|
233
233
|
return vue.openBlock(), vue.createBlock(vue.Transition, { name: "exec-bubble" }, {
|
|
234
234
|
default: vue.withCtx(() => [
|
|
235
|
-
__props.visible ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$
|
|
236
|
-
vue.createElementVNode("span", _hoisted_2$
|
|
235
|
+
__props.visible ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$2, [
|
|
236
|
+
vue.createElementVNode("span", _hoisted_2$2, vue.toDisplayString(__props.text || "执行中"), 1),
|
|
237
237
|
_cache[0] || (_cache[0] = vue.createElementVNode("div", { class: "loading-dots" }, [
|
|
238
238
|
vue.createElementVNode("span", { class: "dot" }),
|
|
239
239
|
vue.createElementVNode("span", { class: "dot" }),
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
}
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
-
const ExecutionStatus = /* @__PURE__ */ _export_sfc(_sfc_main$
|
|
250
|
+
const ExecutionStatus = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-8244ff0d"]]);
|
|
251
251
|
|
|
252
252
|
const ensureMicrophonePermission = async () => {
|
|
253
253
|
if (typeof navigator === "undefined" || typeof window === "undefined") {
|
|
@@ -316,22 +316,22 @@
|
|
|
316
316
|
}
|
|
317
317
|
};
|
|
318
318
|
|
|
319
|
-
const _hoisted_1 = ["data-theme"];
|
|
320
|
-
const _hoisted_2 = { class: "fab-avatar-wrapper" };
|
|
321
|
-
const _hoisted_3 = ["src"];
|
|
322
|
-
const _hoisted_4 = { class: "header-left" };
|
|
323
|
-
const _hoisted_5 = { class: "logo-icon" };
|
|
324
|
-
const _hoisted_6 = ["src"];
|
|
325
|
-
const _hoisted_7 = { class: "title" };
|
|
326
|
-
const _hoisted_8 = { class: "actions" };
|
|
327
|
-
const _hoisted_9 = ["title"];
|
|
328
|
-
const _hoisted_10 = {
|
|
319
|
+
const _hoisted_1$1 = ["data-theme"];
|
|
320
|
+
const _hoisted_2$1 = { class: "fab-avatar-wrapper" };
|
|
321
|
+
const _hoisted_3$1 = ["src"];
|
|
322
|
+
const _hoisted_4$1 = { class: "header-left" };
|
|
323
|
+
const _hoisted_5$1 = { class: "logo-icon" };
|
|
324
|
+
const _hoisted_6$1 = ["src"];
|
|
325
|
+
const _hoisted_7$1 = { class: "title" };
|
|
326
|
+
const _hoisted_8$1 = { class: "actions" };
|
|
327
|
+
const _hoisted_9$1 = ["title"];
|
|
328
|
+
const _hoisted_10$1 = {
|
|
329
329
|
key: 0,
|
|
330
330
|
class: "voice-indicator"
|
|
331
331
|
};
|
|
332
|
-
const _hoisted_11 = ["title"];
|
|
333
|
-
const _hoisted_12 = ["title"];
|
|
334
|
-
const _hoisted_13 = {
|
|
332
|
+
const _hoisted_11$1 = ["title"];
|
|
333
|
+
const _hoisted_12$1 = ["title"];
|
|
334
|
+
const _hoisted_13$1 = {
|
|
335
335
|
width: "16",
|
|
336
336
|
height: "16",
|
|
337
337
|
viewBox: "0 0 24 24",
|
|
@@ -340,7 +340,7 @@
|
|
|
340
340
|
const _hoisted_14 = ["d"];
|
|
341
341
|
const _hoisted_15 = ["src"];
|
|
342
342
|
const FAB_SAFE_GAP = 24;
|
|
343
|
-
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
343
|
+
const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
|
|
344
344
|
__name: "sime-x",
|
|
345
345
|
props: {
|
|
346
346
|
xLogo: {},
|
|
@@ -706,9 +706,9 @@
|
|
|
706
706
|
emit("wakeUp", false);
|
|
707
707
|
}
|
|
708
708
|
};
|
|
709
|
-
const handleIframeLoad = (event) => {
|
|
709
|
+
const handleIframeLoad = async (event) => {
|
|
710
710
|
aiChatbotX.setIframeElement(event.target);
|
|
711
|
-
aiChatbotX.setTheme(
|
|
711
|
+
aiChatbotX.setTheme(currentTheme.value);
|
|
712
712
|
};
|
|
713
713
|
vue.watch(
|
|
714
714
|
() => [aiChatbotX.chatbotUrl()],
|
|
@@ -765,14 +765,14 @@
|
|
|
765
765
|
visible: isProcessing.value,
|
|
766
766
|
text: transcriptionText.value
|
|
767
767
|
}, null, 8, ["visible", "text"])),
|
|
768
|
-
vue.createElementVNode("div", _hoisted_2, [
|
|
768
|
+
vue.createElementVNode("div", _hoisted_2$1, [
|
|
769
769
|
vue.createElementVNode("img", {
|
|
770
770
|
src: __props.xLogo ? __props.xLogo : "/sime.png",
|
|
771
771
|
alt: "assistant",
|
|
772
772
|
style: vue.normalizeStyle({
|
|
773
773
|
width: __props.xSize?.width + "px"
|
|
774
774
|
})
|
|
775
|
-
}, null, 12, _hoisted_3),
|
|
775
|
+
}, null, 12, _hoisted_3$1),
|
|
776
776
|
vue.createVNode(vue.Transition, { name: "indicator-fade" }, {
|
|
777
777
|
default: vue.withCtx(() => [
|
|
778
778
|
voiceStatus.value === "listening" ? (vue.openBlock(), vue.createElementBlock("div", {
|
|
@@ -837,17 +837,17 @@
|
|
|
837
837
|
class: "x-dialog-header",
|
|
838
838
|
onMousedown: vue.withModifiers(startDrag, ["stop"])
|
|
839
839
|
}, [
|
|
840
|
-
vue.createElementVNode("div", _hoisted_4, [
|
|
841
|
-
vue.createElementVNode("div", _hoisted_5, [
|
|
840
|
+
vue.createElementVNode("div", _hoisted_4$1, [
|
|
841
|
+
vue.createElementVNode("div", _hoisted_5$1, [
|
|
842
842
|
vue.createElementVNode("img", {
|
|
843
843
|
src: __props.xLogo ? __props.xLogo : "/sime.png",
|
|
844
844
|
alt: "assistant",
|
|
845
845
|
class: "logo"
|
|
846
|
-
}, null, 8, _hoisted_6)
|
|
846
|
+
}, null, 8, _hoisted_6$1)
|
|
847
847
|
]),
|
|
848
|
-
vue.createElementVNode("span", _hoisted_7, vue.toDisplayString(__props.xTitle), 1)
|
|
848
|
+
vue.createElementVNode("span", _hoisted_7$1, vue.toDisplayString(__props.xTitle), 1)
|
|
849
849
|
]),
|
|
850
|
-
vue.createElementVNode("div", _hoisted_8, [
|
|
850
|
+
vue.createElementVNode("div", _hoisted_8$1, [
|
|
851
851
|
vue.createElementVNode("button", {
|
|
852
852
|
class: "action-btn theme-btn",
|
|
853
853
|
title: "开启新对话",
|
|
@@ -911,8 +911,8 @@
|
|
|
911
911
|
"stroke-linejoin": "round"
|
|
912
912
|
})
|
|
913
913
|
], -1)),
|
|
914
|
-
voiceStatus.value !== "standby" ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_10)) : vue.createCommentVNode("", true)
|
|
915
|
-
], 10, _hoisted_9),
|
|
914
|
+
voiceStatus.value !== "standby" ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_10$1)) : vue.createCommentVNode("", true)
|
|
915
|
+
], 10, _hoisted_9$1),
|
|
916
916
|
vue.createElementVNode("button", {
|
|
917
917
|
class: "action-btn theme-btn",
|
|
918
918
|
onClick: vue.withModifiers(cycleTheme, ["stop"]),
|
|
@@ -936,13 +936,13 @@
|
|
|
936
936
|
fill: "currentColor"
|
|
937
937
|
})
|
|
938
938
|
], -1)
|
|
939
|
-
])], 8, _hoisted_11),
|
|
939
|
+
])], 8, _hoisted_11$1),
|
|
940
940
|
vue.createElementVNode("button", {
|
|
941
941
|
class: "action-btn collapse-btn",
|
|
942
942
|
onClick: vue.withModifiers(toggleCollapse, ["stop"]),
|
|
943
943
|
title: isCollapsed.value ? "展开" : "折叠"
|
|
944
944
|
}, [
|
|
945
|
-
(vue.openBlock(), vue.createElementBlock("svg", _hoisted_13, [
|
|
945
|
+
(vue.openBlock(), vue.createElementBlock("svg", _hoisted_13$1, [
|
|
946
946
|
vue.createElementVNode("path", {
|
|
947
947
|
d: isCollapsed.value ? "M18 15L12 9L6 15" : "M6 9L12 15L18 9",
|
|
948
948
|
stroke: "currentColor",
|
|
@@ -951,7 +951,7 @@
|
|
|
951
951
|
"stroke-linejoin": "round"
|
|
952
952
|
}, null, 8, _hoisted_14)
|
|
953
953
|
]))
|
|
954
|
-
], 8, _hoisted_12),
|
|
954
|
+
], 8, _hoisted_12$1),
|
|
955
955
|
vue.createElementVNode("button", {
|
|
956
956
|
class: "action-btn minimize-btn",
|
|
957
957
|
onClick: _cache[2] || (_cache[2] = vue.withModifiers(($event) => toggleDialog(false), ["stop"])),
|
|
@@ -990,14 +990,1225 @@
|
|
|
990
990
|
]),
|
|
991
991
|
_: 1
|
|
992
992
|
})
|
|
993
|
-
], 8, _hoisted_1);
|
|
993
|
+
], 8, _hoisted_1$1);
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const simeX = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-91f104d1"]]);
|
|
999
|
+
|
|
1000
|
+
function useTTS(getVoiceConfig) {
|
|
1001
|
+
const isSpeaking = vue.ref(false);
|
|
1002
|
+
let instance = null;
|
|
1003
|
+
let initPromise = null;
|
|
1004
|
+
let audioCtx = null;
|
|
1005
|
+
let sentenceBuffer = "";
|
|
1006
|
+
const sentenceDelimiters = /[。!?;\n.!?;]/;
|
|
1007
|
+
const stripMarkdown = (text) => text.replace(/```[\s\S]*?```/g, "").replace(/\|[^\n]*\|/g, "").replace(/#{1,6}\s*/g, "").replace(/\*\*(.*?)\*\*/g, "$1").replace(/\*(.*?)\*/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[-*+]\s+/g, "").replace(/>\s+/g, "").replace(/\n{2,}/g, "。").replace(/\n/g, ",").trim();
|
|
1008
|
+
const warmUpAudio = () => {
|
|
1009
|
+
if (!audioCtx || audioCtx.state === "closed") {
|
|
1010
|
+
try {
|
|
1011
|
+
audioCtx = new AudioContext();
|
|
1012
|
+
} catch {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (audioCtx.state === "suspended") {
|
|
1017
|
+
audioCtx.resume();
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
let onQueueEmptyCb = null;
|
|
1021
|
+
const ensureInstance = async () => {
|
|
1022
|
+
if (instance) return instance;
|
|
1023
|
+
if (initPromise) return initPromise;
|
|
1024
|
+
const vc = getVoiceConfig();
|
|
1025
|
+
if (!vc || !vc.apiSecret) {
|
|
1026
|
+
console.warn("[TTS] 缺少 voiceConfig 或 apiSecret,语音播报已禁用");
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
initPromise = (async () => {
|
|
1030
|
+
try {
|
|
1031
|
+
const tts = new webVoiceKit.SpeechSynthesizerStandalone({
|
|
1032
|
+
appId: vc.appId,
|
|
1033
|
+
apiKey: vc.ttsApiKey || vc.apiKey,
|
|
1034
|
+
apiSecret: vc.apiSecret,
|
|
1035
|
+
websocketUrl: vc.ttsWebsocketUrl || "wss://tts-api.xfyun.cn/v2/tts",
|
|
1036
|
+
vcn: vc.ttsVcn || "xiaoyan",
|
|
1037
|
+
speed: 60,
|
|
1038
|
+
volume: 50,
|
|
1039
|
+
pitch: 50,
|
|
1040
|
+
aue: "raw",
|
|
1041
|
+
auf: "audio/L16;rate=16000",
|
|
1042
|
+
tte: "UTF8",
|
|
1043
|
+
autoPlay: true
|
|
1044
|
+
});
|
|
1045
|
+
tts.onStart(() => {
|
|
1046
|
+
isSpeaking.value = true;
|
|
1047
|
+
});
|
|
1048
|
+
tts.onEnd(() => {
|
|
1049
|
+
});
|
|
1050
|
+
tts.onQueueEmpty(() => {
|
|
1051
|
+
isSpeaking.value = false;
|
|
1052
|
+
onQueueEmptyCb?.();
|
|
1053
|
+
});
|
|
1054
|
+
tts.onError((err) => {
|
|
1055
|
+
console.error("[TTS] Error:", err);
|
|
1056
|
+
isSpeaking.value = false;
|
|
1057
|
+
});
|
|
1058
|
+
if (audioCtx && audioCtx.state === "running") {
|
|
1059
|
+
tts.audioContext = audioCtx;
|
|
1060
|
+
tts.gainNode = audioCtx.createGain();
|
|
1061
|
+
tts.gainNode.connect(audioCtx.destination);
|
|
1062
|
+
}
|
|
1063
|
+
instance = tts;
|
|
1064
|
+
initPromise = null;
|
|
1065
|
+
return tts;
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
console.error("[TTS] 初始化失败:", err);
|
|
1068
|
+
initPromise = null;
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
})();
|
|
1072
|
+
return initPromise;
|
|
1073
|
+
};
|
|
1074
|
+
const speak = async (text) => {
|
|
1075
|
+
const clean = stripMarkdown(text);
|
|
1076
|
+
if (!clean.trim()) return;
|
|
1077
|
+
const tts = await ensureInstance();
|
|
1078
|
+
if (!tts) return;
|
|
1079
|
+
try {
|
|
1080
|
+
tts.speak(clean);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
console.error("[TTS] speak 失败:", err);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
const feed = (delta) => {
|
|
1086
|
+
sentenceBuffer += delta;
|
|
1087
|
+
while (true) {
|
|
1088
|
+
const match = sentenceBuffer.match(sentenceDelimiters);
|
|
1089
|
+
if (!match || match.index === void 0) break;
|
|
1090
|
+
const sentence = sentenceBuffer.slice(0, match.index + 1).trim();
|
|
1091
|
+
sentenceBuffer = sentenceBuffer.slice(match.index + 1);
|
|
1092
|
+
if (sentence.length > 0) speak(sentence);
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
const flush = () => {
|
|
1096
|
+
const remaining = sentenceBuffer.trim();
|
|
1097
|
+
sentenceBuffer = "";
|
|
1098
|
+
if (remaining.length > 0) speak(remaining);
|
|
1099
|
+
};
|
|
1100
|
+
const stop = () => {
|
|
1101
|
+
sentenceBuffer = "";
|
|
1102
|
+
isSpeaking.value = false;
|
|
1103
|
+
if (instance) {
|
|
1104
|
+
try {
|
|
1105
|
+
instance.stop();
|
|
1106
|
+
} catch {
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
const setOnQueueEmpty = (cb) => {
|
|
1111
|
+
onQueueEmptyCb = cb;
|
|
1112
|
+
};
|
|
1113
|
+
const destroy = () => {
|
|
1114
|
+
stop();
|
|
1115
|
+
if (instance) {
|
|
1116
|
+
try {
|
|
1117
|
+
instance.destroy();
|
|
1118
|
+
} catch {
|
|
1119
|
+
}
|
|
1120
|
+
instance = null;
|
|
1121
|
+
}
|
|
1122
|
+
if (audioCtx) {
|
|
1123
|
+
try {
|
|
1124
|
+
audioCtx.close();
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
audioCtx = null;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
return {
|
|
1131
|
+
isSpeaking,
|
|
1132
|
+
warmUpAudio,
|
|
1133
|
+
speak,
|
|
1134
|
+
feed,
|
|
1135
|
+
flush,
|
|
1136
|
+
stop,
|
|
1137
|
+
destroy,
|
|
1138
|
+
setOnQueueEmpty
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function useBubble(options = {}) {
|
|
1143
|
+
const visible = vue.ref(false);
|
|
1144
|
+
const fadingOut = vue.ref(false);
|
|
1145
|
+
const stackRef = vue.ref(null);
|
|
1146
|
+
let dismissTimer = null;
|
|
1147
|
+
const show = vue.computed(() => visible.value && !fadingOut.value);
|
|
1148
|
+
const style = vue.computed(() => ({
|
|
1149
|
+
width: options.bubbleSize?.width || void 0,
|
|
1150
|
+
maxHeight: options.bubbleSize?.maxHeight || void 0
|
|
1151
|
+
}));
|
|
1152
|
+
const open = () => {
|
|
1153
|
+
cancelDismiss();
|
|
1154
|
+
fadingOut.value = false;
|
|
1155
|
+
visible.value = true;
|
|
1156
|
+
};
|
|
1157
|
+
const cancelDismiss = () => {
|
|
1158
|
+
if (dismissTimer) {
|
|
1159
|
+
clearTimeout(dismissTimer);
|
|
1160
|
+
dismissTimer = null;
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
const scheduleDismiss = () => {
|
|
1164
|
+
cancelDismiss();
|
|
1165
|
+
if (options.isSpeaking?.value) return;
|
|
1166
|
+
if (options.isInvoking?.value) return;
|
|
1167
|
+
const delay = options.dismissDelay ?? 4e3;
|
|
1168
|
+
dismissTimer = setTimeout(() => {
|
|
1169
|
+
fadingOut.value = true;
|
|
1170
|
+
setTimeout(() => {
|
|
1171
|
+
visible.value = false;
|
|
1172
|
+
fadingOut.value = false;
|
|
1173
|
+
}, 400);
|
|
1174
|
+
}, delay);
|
|
1175
|
+
};
|
|
1176
|
+
const hide = () => {
|
|
1177
|
+
cancelDismiss();
|
|
1178
|
+
fadingOut.value = false;
|
|
1179
|
+
visible.value = false;
|
|
1180
|
+
};
|
|
1181
|
+
const scrollToBottom = () => {
|
|
1182
|
+
vue.nextTick(() => {
|
|
1183
|
+
if (stackRef.value) {
|
|
1184
|
+
stackRef.value.scrollTop = stackRef.value.scrollHeight;
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
};
|
|
1188
|
+
const destroy = () => {
|
|
1189
|
+
cancelDismiss();
|
|
1190
|
+
};
|
|
1191
|
+
return {
|
|
1192
|
+
visible,
|
|
1193
|
+
fadingOut,
|
|
1194
|
+
show,
|
|
1195
|
+
style,
|
|
1196
|
+
stackRef,
|
|
1197
|
+
open,
|
|
1198
|
+
hide,
|
|
1199
|
+
cancelDismiss,
|
|
1200
|
+
scheduleDismiss,
|
|
1201
|
+
scrollToBottom,
|
|
1202
|
+
destroy
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function useVoiceRecognition(options) {
|
|
1207
|
+
const voiceStatus = vue.ref("standby");
|
|
1208
|
+
const isTranscribing = vue.ref(false);
|
|
1209
|
+
const isInitializing = vue.ref(false);
|
|
1210
|
+
const transcriptionText = vue.ref("");
|
|
1211
|
+
const wakeAnimating = vue.ref(false);
|
|
1212
|
+
let detector = null;
|
|
1213
|
+
let transcriber = null;
|
|
1214
|
+
const initTranscriber = () => {
|
|
1215
|
+
if (transcriber) return;
|
|
1216
|
+
const vc = options.getVoiceConfig();
|
|
1217
|
+
if (!vc || !vc.appId || !vc.apiKey || !vc.websocketUrl) {
|
|
1218
|
+
console.error("[VoiceRecognition] 缺少 voiceConfig,无法初始化转写器");
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
transcriber = new webVoiceKit.SpeechTranscriberStandalone({
|
|
1222
|
+
appId: vc.appId,
|
|
1223
|
+
apiKey: vc.apiKey,
|
|
1224
|
+
websocketUrl: vc.websocketUrl,
|
|
1225
|
+
autoStop: {
|
|
1226
|
+
enabled: true,
|
|
1227
|
+
silenceTimeoutMs: 2e3,
|
|
1228
|
+
noSpeechTimeoutMs: 5e3,
|
|
1229
|
+
maxDurationMs: 45e3
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
transcriber.onResult((result) => {
|
|
1233
|
+
transcriptionText.value = result.transcript || "";
|
|
1234
|
+
});
|
|
1235
|
+
transcriber.onAutoStop(async () => {
|
|
1236
|
+
const finalText = transcriptionText.value;
|
|
1237
|
+
await stopTranscribing();
|
|
1238
|
+
transcriptionText.value = "";
|
|
1239
|
+
if (finalText.trim()) {
|
|
1240
|
+
options.onTranscriptionDone?.(finalText);
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
transcriber.onError((error) => {
|
|
1244
|
+
console.error("[VoiceRecognition] 转写错误:", error);
|
|
1245
|
+
stopTranscribing();
|
|
1246
|
+
transcriptionText.value = "";
|
|
1247
|
+
});
|
|
1248
|
+
};
|
|
1249
|
+
const startTranscribing = async () => {
|
|
1250
|
+
if (isTranscribing.value) return;
|
|
1251
|
+
if (!transcriber) initTranscriber();
|
|
1252
|
+
if (!transcriber) return;
|
|
1253
|
+
try {
|
|
1254
|
+
await transcriber.start();
|
|
1255
|
+
isTranscribing.value = true;
|
|
1256
|
+
transcriptionText.value = "";
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
console.error("[VoiceRecognition] 启动转写失败:", error);
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
const stopTranscribing = async () => {
|
|
1262
|
+
if (!transcriber || !transcriber.isActive()) {
|
|
1263
|
+
isTranscribing.value = false;
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
await transcriber.stop();
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
console.error("[VoiceRecognition] 停止转写失败:", error);
|
|
1270
|
+
} finally {
|
|
1271
|
+
isTranscribing.value = false;
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
const initDetector = () => {
|
|
1275
|
+
if (detector || isInitializing.value) return;
|
|
1276
|
+
if (!options.modelPath) {
|
|
1277
|
+
console.error("[VoiceRecognition] 未传入 modelPath,无法启用唤醒词");
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
isInitializing.value = true;
|
|
1281
|
+
try {
|
|
1282
|
+
detector = new webVoiceKit.WakeWordDetectorStandalone({
|
|
1283
|
+
modelPath: options.modelPath,
|
|
1284
|
+
sampleRate: 16e3,
|
|
1285
|
+
usePartial: true,
|
|
1286
|
+
autoReset: {
|
|
1287
|
+
enabled: true,
|
|
1288
|
+
resetDelayMs: 4e3
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
detector.setWakeWords(options.wakeWords || ["你好", "您好"]);
|
|
1292
|
+
detector.onWake(async () => {
|
|
1293
|
+
wakeAnimating.value = true;
|
|
1294
|
+
options.onWake?.();
|
|
1295
|
+
await startTranscribing();
|
|
1296
|
+
setTimeout(() => {
|
|
1297
|
+
wakeAnimating.value = false;
|
|
1298
|
+
}, 1200);
|
|
1299
|
+
});
|
|
1300
|
+
detector.onError((error) => {
|
|
1301
|
+
console.error("[VoiceRecognition] 唤醒监听错误:", error);
|
|
1302
|
+
voiceStatus.value = "standby";
|
|
1303
|
+
stopTranscribing();
|
|
1304
|
+
});
|
|
1305
|
+
} finally {
|
|
1306
|
+
isInitializing.value = false;
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
const toggleVoiceMode = async (targetState) => {
|
|
1310
|
+
const permission = await ensureMicrophonePermission();
|
|
1311
|
+
if (!permission || isInitializing.value) return;
|
|
1312
|
+
if (!detector) {
|
|
1313
|
+
initDetector();
|
|
1314
|
+
if (!detector) return;
|
|
1315
|
+
}
|
|
1316
|
+
const isListening = voiceStatus.value === "listening";
|
|
1317
|
+
const shouldStart = targetState !== void 0 ? targetState : !isListening;
|
|
1318
|
+
if (isListening === shouldStart) return;
|
|
1319
|
+
try {
|
|
1320
|
+
if (shouldStart) {
|
|
1321
|
+
await detector.start();
|
|
1322
|
+
voiceStatus.value = "listening";
|
|
1323
|
+
} else {
|
|
1324
|
+
await detector.stop();
|
|
1325
|
+
voiceStatus.value = "standby";
|
|
1326
|
+
transcriptionText.value = "";
|
|
1327
|
+
await stopTranscribing();
|
|
1328
|
+
}
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
console.error("[VoiceRecognition] 监听切换失败:", error);
|
|
1331
|
+
voiceStatus.value = "standby";
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
const abortTranscription = async () => {
|
|
1335
|
+
transcriptionText.value = "";
|
|
1336
|
+
await stopTranscribing();
|
|
1337
|
+
};
|
|
1338
|
+
const destroy = async () => {
|
|
1339
|
+
if (detector) {
|
|
1340
|
+
try {
|
|
1341
|
+
if (detector.isActive()) await detector.stop();
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1344
|
+
detector = null;
|
|
1345
|
+
}
|
|
1346
|
+
if (transcriber) {
|
|
1347
|
+
try {
|
|
1348
|
+
if (transcriber.isActive()) await transcriber.stop();
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
transcriber = null;
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
return {
|
|
1355
|
+
voiceStatus,
|
|
1356
|
+
isTranscribing,
|
|
1357
|
+
isInitializing,
|
|
1358
|
+
transcriptionText,
|
|
1359
|
+
wakeAnimating,
|
|
1360
|
+
startTranscribing,
|
|
1361
|
+
stopTranscribing,
|
|
1362
|
+
abortTranscription,
|
|
1363
|
+
toggleVoiceMode,
|
|
1364
|
+
destroy
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const DATA_STREAM_LINE_RE = /^[0-9a-f]:/;
|
|
1369
|
+
function detectFormat(firstChunk) {
|
|
1370
|
+
const trimmed = firstChunk.trimStart();
|
|
1371
|
+
if (trimmed.startsWith("data:")) {
|
|
1372
|
+
const firstLine = trimmed.split("\n")[0];
|
|
1373
|
+
const payload = firstLine.slice(5).trim();
|
|
1374
|
+
try {
|
|
1375
|
+
const parsed = JSON.parse(payload);
|
|
1376
|
+
if (parsed && typeof parsed.type === "string") {
|
|
1377
|
+
return "ui-message-stream";
|
|
1378
|
+
}
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
if (DATA_STREAM_LINE_RE.test(payload)) {
|
|
1382
|
+
return "data-stream";
|
|
1383
|
+
}
|
|
1384
|
+
return "ui-message-stream";
|
|
1385
|
+
}
|
|
1386
|
+
if (DATA_STREAM_LINE_RE.test(trimmed)) {
|
|
1387
|
+
return "data-stream";
|
|
1388
|
+
}
|
|
1389
|
+
return "plain-text";
|
|
1390
|
+
}
|
|
1391
|
+
function processUIMessageStreamEvent(payload, callbacks) {
|
|
1392
|
+
const trimmed = payload.trim();
|
|
1393
|
+
if (!trimmed || trimmed === "[DONE]") {
|
|
1394
|
+
callbacks.onFinish?.({});
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
let parsed;
|
|
1398
|
+
try {
|
|
1399
|
+
parsed = JSON.parse(trimmed);
|
|
1400
|
+
} catch {
|
|
1401
|
+
console.warn("[DataStreamParser] failed to parse UI message stream event:", trimmed.slice(0, 100));
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const type = parsed?.type;
|
|
1405
|
+
if (!type) return;
|
|
1406
|
+
switch (type) {
|
|
1407
|
+
case "text-delta":
|
|
1408
|
+
if (typeof parsed.delta === "string") {
|
|
1409
|
+
callbacks.onTextDelta?.(parsed.delta);
|
|
1410
|
+
}
|
|
1411
|
+
break;
|
|
1412
|
+
case "tool-input-start":
|
|
1413
|
+
callbacks.onToolCallStart?.(parsed.toolCallId, parsed.toolName);
|
|
1414
|
+
break;
|
|
1415
|
+
case "tool-input-delta":
|
|
1416
|
+
callbacks.onToolCallDelta?.(parsed.toolCallId, parsed.inputTextDelta);
|
|
1417
|
+
break;
|
|
1418
|
+
case "tool-input-available":
|
|
1419
|
+
callbacks.onToolCallComplete?.(parsed.toolCallId, parsed.toolName, parsed.input);
|
|
1420
|
+
break;
|
|
1421
|
+
case "tool-output-available":
|
|
1422
|
+
callbacks.onToolResult?.(parsed.toolCallId, parsed.output);
|
|
1423
|
+
break;
|
|
1424
|
+
case "finish-step":
|
|
1425
|
+
callbacks.onStepFinish?.(parsed);
|
|
1426
|
+
break;
|
|
1427
|
+
case "finish":
|
|
1428
|
+
callbacks.onFinish?.(parsed);
|
|
1429
|
+
break;
|
|
1430
|
+
case "error":
|
|
1431
|
+
callbacks.onError?.(parsed.errorText || parsed.error || "Unknown error");
|
|
1432
|
+
break;
|
|
1433
|
+
case "start":
|
|
1434
|
+
case "text-start":
|
|
1435
|
+
case "text-end":
|
|
1436
|
+
case "start-step":
|
|
1437
|
+
case "reasoning-start":
|
|
1438
|
+
case "reasoning-delta":
|
|
1439
|
+
case "reasoning-end":
|
|
1440
|
+
case "source-url":
|
|
1441
|
+
case "source-document":
|
|
1442
|
+
case "file":
|
|
1443
|
+
case "abort":
|
|
1444
|
+
break;
|
|
1445
|
+
default:
|
|
1446
|
+
if (type.startsWith("data-")) ; else {
|
|
1447
|
+
console.log("[DataStreamParser] unhandled UI message stream type:", type);
|
|
1448
|
+
}
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function parseLegacyProtocolLine(line, callbacks) {
|
|
1453
|
+
if (!line || !DATA_STREAM_LINE_RE.test(line)) return;
|
|
1454
|
+
const code = line[0];
|
|
1455
|
+
const rawValue = line.slice(2);
|
|
1456
|
+
let value;
|
|
1457
|
+
try {
|
|
1458
|
+
value = JSON.parse(rawValue);
|
|
1459
|
+
} catch {
|
|
1460
|
+
value = rawValue;
|
|
1461
|
+
}
|
|
1462
|
+
switch (code) {
|
|
1463
|
+
case "0":
|
|
1464
|
+
callbacks.onTextDelta?.(value);
|
|
1465
|
+
break;
|
|
1466
|
+
case "9":
|
|
1467
|
+
callbacks.onToolCallStart?.(value.toolCallId, value.toolName);
|
|
1468
|
+
break;
|
|
1469
|
+
case "b":
|
|
1470
|
+
callbacks.onToolCallDelta?.(value.toolCallId, value.argsTextDelta);
|
|
1471
|
+
break;
|
|
1472
|
+
case "c":
|
|
1473
|
+
callbacks.onToolCallComplete?.(value.toolCallId, value.toolName, value.args);
|
|
1474
|
+
break;
|
|
1475
|
+
case "a":
|
|
1476
|
+
callbacks.onToolResult?.(value.toolCallId, value.result);
|
|
1477
|
+
break;
|
|
1478
|
+
case "e":
|
|
1479
|
+
callbacks.onStepFinish?.(value);
|
|
1480
|
+
break;
|
|
1481
|
+
case "d":
|
|
1482
|
+
callbacks.onFinish?.(value);
|
|
1483
|
+
break;
|
|
1484
|
+
case "3":
|
|
1485
|
+
callbacks.onError?.(value);
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async function readDataStream(response, callbacks) {
|
|
1490
|
+
if (!response.body) return;
|
|
1491
|
+
const reader = response.body.getReader();
|
|
1492
|
+
const decoder = new TextDecoder();
|
|
1493
|
+
let buffer = "";
|
|
1494
|
+
let format = null;
|
|
1495
|
+
while (true) {
|
|
1496
|
+
const { value, done } = await reader.read();
|
|
1497
|
+
if (done) break;
|
|
1498
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1499
|
+
buffer += chunk;
|
|
1500
|
+
if (format === null && buffer.trim().length > 0) {
|
|
1501
|
+
format = detectFormat(buffer);
|
|
1502
|
+
console.log("[DataStreamParser] detected format:", format, "| first 200 chars:", buffer.slice(0, 200));
|
|
1503
|
+
}
|
|
1504
|
+
if (format === "plain-text") {
|
|
1505
|
+
const text = buffer;
|
|
1506
|
+
buffer = "";
|
|
1507
|
+
if (text) callbacks.onTextDelta?.(text);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (format === "ui-message-stream") {
|
|
1511
|
+
while (true) {
|
|
1512
|
+
const eventEnd = buffer.indexOf("\n\n");
|
|
1513
|
+
if (eventEnd === -1) break;
|
|
1514
|
+
const eventBlock = buffer.slice(0, eventEnd);
|
|
1515
|
+
buffer = buffer.slice(eventEnd + 2);
|
|
1516
|
+
const dataLines = eventBlock.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
|
|
1517
|
+
for (const dataLine of dataLines) {
|
|
1518
|
+
processUIMessageStreamEvent(dataLine, callbacks);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
if (format === "data-stream") {
|
|
1524
|
+
const isSSEWrapped = buffer.trimStart().startsWith("data:");
|
|
1525
|
+
if (isSSEWrapped) {
|
|
1526
|
+
while (true) {
|
|
1527
|
+
const eventEnd = buffer.indexOf("\n\n");
|
|
1528
|
+
if (eventEnd === -1) break;
|
|
1529
|
+
const eventBlock = buffer.slice(0, eventEnd);
|
|
1530
|
+
buffer = buffer.slice(eventEnd + 2);
|
|
1531
|
+
const dataLines = eventBlock.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
|
|
1532
|
+
for (const dl of dataLines) {
|
|
1533
|
+
const t = dl.trim();
|
|
1534
|
+
if (!t || t === "[DONE]") {
|
|
1535
|
+
if (t === "[DONE]") callbacks.onFinish?.({});
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
parseLegacyProtocolLine(t, callbacks);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
} else {
|
|
1542
|
+
while (true) {
|
|
1543
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
1544
|
+
if (newlineIdx === -1) break;
|
|
1545
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
1546
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
1547
|
+
if (line) parseLegacyProtocolLine(line, callbacks);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const tail = decoder.decode();
|
|
1554
|
+
if (tail) buffer += tail;
|
|
1555
|
+
if (buffer.trim()) {
|
|
1556
|
+
if (format === "plain-text") {
|
|
1557
|
+
callbacks.onTextDelta?.(buffer);
|
|
1558
|
+
} else if (format === "ui-message-stream") {
|
|
1559
|
+
const dataLines = buffer.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
|
|
1560
|
+
for (const dl of dataLines) {
|
|
1561
|
+
processUIMessageStreamEvent(dl, callbacks);
|
|
1562
|
+
}
|
|
1563
|
+
} else if (format === "data-stream") {
|
|
1564
|
+
parseLegacyProtocolLine(buffer.trim(), callbacks);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
callbacks.onFinish?.({});
|
|
1568
|
+
}
|
|
1569
|
+
async function parseDataStreamToMessage(response, onUpdate) {
|
|
1570
|
+
let textContent = "";
|
|
1571
|
+
const parts = [];
|
|
1572
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
1573
|
+
const ensureTextPart = () => {
|
|
1574
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
1575
|
+
if (parts[i].type === "text") {
|
|
1576
|
+
return parts[i];
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
const textPart = { type: "text", text: "" };
|
|
1580
|
+
parts.push(textPart);
|
|
1581
|
+
return textPart;
|
|
1582
|
+
};
|
|
1583
|
+
const findToolPartIndex = (toolCallId) => {
|
|
1584
|
+
return parts.findIndex((p) => (p.type === "tool-call" || p.type === "tool-result") && p.toolCallId === toolCallId);
|
|
1585
|
+
};
|
|
1586
|
+
const emitUpdate = () => {
|
|
1587
|
+
onUpdate({ textContent, parts: [...parts], toolCalls: new Map(toolCalls) });
|
|
1588
|
+
};
|
|
1589
|
+
await readDataStream(response, {
|
|
1590
|
+
onTextDelta(text) {
|
|
1591
|
+
textContent += text;
|
|
1592
|
+
const textPart = ensureTextPart();
|
|
1593
|
+
textPart.text = textContent;
|
|
1594
|
+
emitUpdate();
|
|
1595
|
+
},
|
|
1596
|
+
onToolCallStart(toolCallId, toolName) {
|
|
1597
|
+
const tracker = {
|
|
1598
|
+
toolCallId,
|
|
1599
|
+
toolName,
|
|
1600
|
+
argsText: "",
|
|
1601
|
+
args: void 0,
|
|
1602
|
+
state: "partial-call"
|
|
1603
|
+
};
|
|
1604
|
+
toolCalls.set(toolCallId, tracker);
|
|
1605
|
+
const part = {
|
|
1606
|
+
type: "tool-call",
|
|
1607
|
+
toolCallId,
|
|
1608
|
+
toolName,
|
|
1609
|
+
args: void 0,
|
|
1610
|
+
state: "partial-call"
|
|
1611
|
+
};
|
|
1612
|
+
parts.push(part);
|
|
1613
|
+
emitUpdate();
|
|
1614
|
+
},
|
|
1615
|
+
onToolCallDelta(toolCallId, argsTextDelta) {
|
|
1616
|
+
const tracker = toolCalls.get(toolCallId);
|
|
1617
|
+
if (tracker) {
|
|
1618
|
+
tracker.argsText += argsTextDelta;
|
|
1619
|
+
try {
|
|
1620
|
+
tracker.args = JSON.parse(tracker.argsText);
|
|
1621
|
+
} catch {
|
|
1622
|
+
}
|
|
1623
|
+
const idx = findToolPartIndex(toolCallId);
|
|
1624
|
+
if (idx !== -1 && parts[idx].type === "tool-call") {
|
|
1625
|
+
parts[idx].args = tracker.args;
|
|
1626
|
+
}
|
|
1627
|
+
emitUpdate();
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1630
|
+
onToolCallComplete(toolCallId, toolName, args) {
|
|
1631
|
+
const tracker = toolCalls.get(toolCallId);
|
|
1632
|
+
if (tracker) {
|
|
1633
|
+
tracker.state = "call";
|
|
1634
|
+
tracker.args = typeof args === "string" ? safeJsonParse(args) : args;
|
|
1635
|
+
} else {
|
|
1636
|
+
toolCalls.set(toolCallId, {
|
|
1637
|
+
toolCallId,
|
|
1638
|
+
toolName,
|
|
1639
|
+
argsText: typeof args === "string" ? args : JSON.stringify(args),
|
|
1640
|
+
args: typeof args === "string" ? safeJsonParse(args) : args,
|
|
1641
|
+
state: "call"
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
const idx = findToolPartIndex(toolCallId);
|
|
1645
|
+
if (idx !== -1) {
|
|
1646
|
+
parts[idx].state = "call";
|
|
1647
|
+
parts[idx].toolName = toolName;
|
|
1648
|
+
parts[idx].args = toolCalls.get(toolCallId).args;
|
|
1649
|
+
} else {
|
|
1650
|
+
parts.push({
|
|
1651
|
+
type: "tool-call",
|
|
1652
|
+
toolCallId,
|
|
1653
|
+
toolName,
|
|
1654
|
+
args: toolCalls.get(toolCallId).args,
|
|
1655
|
+
state: "call"
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
emitUpdate();
|
|
1659
|
+
},
|
|
1660
|
+
onToolResult(toolCallId, result) {
|
|
1661
|
+
const tracker = toolCalls.get(toolCallId);
|
|
1662
|
+
if (tracker) {
|
|
1663
|
+
tracker.result = result;
|
|
1664
|
+
tracker.state = "result";
|
|
1665
|
+
}
|
|
1666
|
+
const idx = findToolPartIndex(toolCallId);
|
|
1667
|
+
if (idx !== -1) {
|
|
1668
|
+
const existing = parts[idx];
|
|
1669
|
+
const resultPart = {
|
|
1670
|
+
type: "tool-result",
|
|
1671
|
+
toolCallId,
|
|
1672
|
+
toolName: existing.toolName,
|
|
1673
|
+
args: existing.args,
|
|
1674
|
+
result,
|
|
1675
|
+
state: "result"
|
|
1676
|
+
};
|
|
1677
|
+
parts[idx] = resultPart;
|
|
1678
|
+
} else {
|
|
1679
|
+
parts.push({
|
|
1680
|
+
type: "tool-result",
|
|
1681
|
+
toolCallId,
|
|
1682
|
+
toolName: tracker?.toolName || "unknown",
|
|
1683
|
+
args: tracker?.args,
|
|
1684
|
+
result,
|
|
1685
|
+
state: "result"
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
emitUpdate();
|
|
1689
|
+
},
|
|
1690
|
+
onError(error) {
|
|
1691
|
+
console.error("[DataStreamParser] stream error:", error);
|
|
1692
|
+
},
|
|
1693
|
+
onStepFinish(_data) {
|
|
1694
|
+
emitUpdate();
|
|
1695
|
+
},
|
|
1696
|
+
onFinish(_data) {
|
|
1697
|
+
emitUpdate();
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
return { textContent, parts, toolCalls };
|
|
1701
|
+
}
|
|
1702
|
+
function safeJsonParse(str) {
|
|
1703
|
+
try {
|
|
1704
|
+
return JSON.parse(str);
|
|
1705
|
+
} catch {
|
|
1706
|
+
return str;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
const toolDisplayNames = {
|
|
1711
|
+
generateReport: "生成报告",
|
|
1712
|
+
searchKnowledge: "知识库检索",
|
|
1713
|
+
resolveInstanceTargets: "解析实例目标",
|
|
1714
|
+
getHistoryMetrics: "历史数据查询",
|
|
1715
|
+
getRealtimeMetrics: "实时数据查询",
|
|
1716
|
+
queryBitableData: "多维表格查询",
|
|
1717
|
+
searchUser: "搜索用户",
|
|
1718
|
+
createBitableRecord: "创建表格记录",
|
|
1719
|
+
timeTool: "时间工具",
|
|
1720
|
+
loadSkill: "加载技能",
|
|
1721
|
+
executeCommand: "执行命令",
|
|
1722
|
+
dataAnalyzer: "数据分析",
|
|
1723
|
+
dataPredictor: "数据预测"
|
|
1724
|
+
};
|
|
1725
|
+
function useAgentInvoke(options) {
|
|
1726
|
+
const { aiChatbotX, tts, bubble } = options;
|
|
1727
|
+
const sessionTimeoutMs = options.sessionTimeoutMs ?? 12e4;
|
|
1728
|
+
const maxHistoryTurns = options.maxHistoryTurns ?? 10;
|
|
1729
|
+
const isInvoking = vue.ref(false);
|
|
1730
|
+
const currentTextContent = vue.ref("");
|
|
1731
|
+
const currentToolParts = vue.ref([]);
|
|
1732
|
+
const executingTools = vue.ref(/* @__PURE__ */ new Set());
|
|
1733
|
+
const conversationHistory = vue.ref([]);
|
|
1734
|
+
let lastInteractionTime = 0;
|
|
1735
|
+
const checkSessionTimeout = () => {
|
|
1736
|
+
if (lastInteractionTime > 0 && Date.now() - lastInteractionTime > sessionTimeoutMs) {
|
|
1737
|
+
conversationHistory.value = [];
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
const appendToHistory = (role, content) => {
|
|
1741
|
+
conversationHistory.value.push({ role, content });
|
|
1742
|
+
const maxLen = maxHistoryTurns * 2;
|
|
1743
|
+
if (conversationHistory.value.length > maxLen) {
|
|
1744
|
+
conversationHistory.value = conversationHistory.value.slice(-maxLen);
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
const clearHistory = () => {
|
|
1748
|
+
conversationHistory.value = [];
|
|
1749
|
+
};
|
|
1750
|
+
let abortController = null;
|
|
1751
|
+
const hasAnyContent = vue.computed(() => {
|
|
1752
|
+
return !!(currentTextContent.value || currentToolParts.value.length > 0);
|
|
1753
|
+
});
|
|
1754
|
+
const toolDisplayName = (name) => toolDisplayNames[name] || name;
|
|
1755
|
+
const resetState = () => {
|
|
1756
|
+
currentTextContent.value = "";
|
|
1757
|
+
currentToolParts.value = [];
|
|
1758
|
+
executingTools.value = /* @__PURE__ */ new Set();
|
|
1759
|
+
};
|
|
1760
|
+
const executeHostCommands = async (toolCallId, result) => {
|
|
1761
|
+
if (!result || typeof result !== "object") return;
|
|
1762
|
+
const commands = result.commands;
|
|
1763
|
+
if (!Array.isArray(commands) || commands.length === 0) return;
|
|
1764
|
+
try {
|
|
1765
|
+
executingTools.value = /* @__PURE__ */ new Set([...executingTools.value, toolCallId]);
|
|
1766
|
+
for (const cmd of commands) {
|
|
1767
|
+
const args = Array.isArray(cmd.args) ? cmd.args : [];
|
|
1768
|
+
try {
|
|
1769
|
+
await aiChatbotX.executeCommand(cmd.name, args);
|
|
1770
|
+
} catch (cmdErr) {
|
|
1771
|
+
console.error(`[AgentInvoke] 执行命令 ${cmd.name} 失败:`, cmdErr);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
} finally {
|
|
1775
|
+
const next = new Set(executingTools.value);
|
|
1776
|
+
next.delete(toolCallId);
|
|
1777
|
+
executingTools.value = next;
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
const parseAssistantText = (payload) => {
|
|
1781
|
+
if (!payload) return "";
|
|
1782
|
+
if (typeof payload === "string") return payload;
|
|
1783
|
+
if (typeof payload === "object") {
|
|
1784
|
+
const data = payload;
|
|
1785
|
+
const directText = data.output || data.answer || data.message || data.result;
|
|
1786
|
+
if (typeof directText === "string" && directText.trim()) return directText;
|
|
1787
|
+
if (data.data && typeof data.data === "object") {
|
|
1788
|
+
const nested = data.data;
|
|
1789
|
+
const nestedText = nested.output || nested.answer || nested.message || nested.result;
|
|
1790
|
+
if (typeof nestedText === "string" && nestedText.trim()) return nestedText;
|
|
1791
|
+
}
|
|
1792
|
+
return JSON.stringify(payload);
|
|
1793
|
+
}
|
|
1794
|
+
return String(payload);
|
|
1795
|
+
};
|
|
1796
|
+
const invoke = async (question) => {
|
|
1797
|
+
const content = question.trim();
|
|
1798
|
+
if (!content) return;
|
|
1799
|
+
abort();
|
|
1800
|
+
checkSessionTimeout();
|
|
1801
|
+
resetState();
|
|
1802
|
+
tts.stop();
|
|
1803
|
+
isInvoking.value = true;
|
|
1804
|
+
bubble.open();
|
|
1805
|
+
let prevTextLength = 0;
|
|
1806
|
+
const processedToolResults = /* @__PURE__ */ new Set();
|
|
1807
|
+
abortController = new AbortController();
|
|
1808
|
+
const commands = await aiChatbotX.hostCommads();
|
|
1809
|
+
const historyToSend = conversationHistory.value.length > 0 ? [...conversationHistory.value] : void 0;
|
|
1810
|
+
try {
|
|
1811
|
+
const response = await fetch(options.endpoint.value, {
|
|
1812
|
+
method: "POST",
|
|
1813
|
+
headers: { "Content-Type": "application/json" },
|
|
1814
|
+
body: JSON.stringify({
|
|
1815
|
+
input: content,
|
|
1816
|
+
projectId: options.projectId || "",
|
|
1817
|
+
commands: commands.length > 0 ? commands : void 0,
|
|
1818
|
+
messages: historyToSend
|
|
1819
|
+
}),
|
|
1820
|
+
signal: abortController.signal
|
|
1821
|
+
});
|
|
1822
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1823
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1824
|
+
const isJsonResponse = contentType.includes("application/json");
|
|
1825
|
+
if (isJsonResponse) {
|
|
1826
|
+
const data = await response.json();
|
|
1827
|
+
const reply = parseAssistantText(data) || "已收到,但没有返回可展示的文本内容。";
|
|
1828
|
+
currentTextContent.value = reply;
|
|
1829
|
+
tts.speak(reply);
|
|
1830
|
+
appendToHistory("user", content);
|
|
1831
|
+
appendToHistory("assistant", reply);
|
|
1832
|
+
if (data.toolResults && Array.isArray(data.toolResults)) {
|
|
1833
|
+
for (const tr of data.toolResults) {
|
|
1834
|
+
const toolPart = {
|
|
1835
|
+
type: "tool-result",
|
|
1836
|
+
toolCallId: `invoke-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1837
|
+
toolName: tr.toolName,
|
|
1838
|
+
args: tr.args,
|
|
1839
|
+
result: tr.result,
|
|
1840
|
+
state: "result"
|
|
1841
|
+
};
|
|
1842
|
+
currentToolParts.value = [...currentToolParts.value, toolPart];
|
|
1843
|
+
if (tr.toolName === "executeCommand") {
|
|
1844
|
+
executeHostCommands(toolPart.toolCallId, tr.result);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
} else {
|
|
1849
|
+
await parseDataStreamToMessage(response, (result) => {
|
|
1850
|
+
currentTextContent.value = result.textContent;
|
|
1851
|
+
if (result.textContent.length > prevTextLength) {
|
|
1852
|
+
const delta = result.textContent.slice(prevTextLength);
|
|
1853
|
+
prevTextLength = result.textContent.length;
|
|
1854
|
+
tts.feed(delta);
|
|
1855
|
+
}
|
|
1856
|
+
const toolParts = result.parts.filter(
|
|
1857
|
+
(p) => p.type === "tool-call" || p.type === "tool-result"
|
|
1858
|
+
);
|
|
1859
|
+
currentToolParts.value = toolParts;
|
|
1860
|
+
for (const part of toolParts) {
|
|
1861
|
+
if (part.toolName === "executeCommand" && !processedToolResults.has(part.toolCallId)) {
|
|
1862
|
+
if (part.type === "tool-call" && part.state === "call" && part.args) {
|
|
1863
|
+
processedToolResults.add(part.toolCallId);
|
|
1864
|
+
executeHostCommands(part.toolCallId, part.args);
|
|
1865
|
+
} else if (part.type === "tool-result" && part.result) {
|
|
1866
|
+
processedToolResults.add(part.toolCallId);
|
|
1867
|
+
executeHostCommands(part.toolCallId, part.result);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
bubble.scrollToBottom();
|
|
1872
|
+
});
|
|
1873
|
+
tts.flush();
|
|
1874
|
+
const assistantReply = currentTextContent.value.trim();
|
|
1875
|
+
appendToHistory("user", content);
|
|
1876
|
+
if (assistantReply) {
|
|
1877
|
+
appendToHistory("assistant", assistantReply);
|
|
1878
|
+
}
|
|
1879
|
+
if (!assistantReply && currentToolParts.value.length === 0) {
|
|
1880
|
+
currentTextContent.value = "已收到,但没有返回可展示的文本内容。";
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
if (error.name === "AbortError") {
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
console.error("[AgentInvoke] invoke failed:", error);
|
|
1888
|
+
tts.stop();
|
|
1889
|
+
currentTextContent.value = "请求失败,请检查服务地址或稍后重试。";
|
|
1890
|
+
} finally {
|
|
1891
|
+
isInvoking.value = false;
|
|
1892
|
+
abortController = null;
|
|
1893
|
+
lastInteractionTime = Date.now();
|
|
1894
|
+
bubble.scheduleDismiss();
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
const abort = () => {
|
|
1898
|
+
if (abortController) {
|
|
1899
|
+
abortController.abort();
|
|
1900
|
+
abortController = null;
|
|
1901
|
+
}
|
|
1902
|
+
tts.stop();
|
|
1903
|
+
isInvoking.value = false;
|
|
1904
|
+
};
|
|
1905
|
+
return {
|
|
1906
|
+
isInvoking,
|
|
1907
|
+
currentTextContent,
|
|
1908
|
+
currentToolParts,
|
|
1909
|
+
executingTools,
|
|
1910
|
+
hasAnyContent,
|
|
1911
|
+
conversationHistory,
|
|
1912
|
+
toolDisplayName,
|
|
1913
|
+
invoke,
|
|
1914
|
+
abort,
|
|
1915
|
+
resetState,
|
|
1916
|
+
clearHistory
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const _hoisted_1 = { class: "agent-bubble" };
|
|
1921
|
+
const _hoisted_2 = {
|
|
1922
|
+
key: 0,
|
|
1923
|
+
class: "tool-steps"
|
|
1924
|
+
};
|
|
1925
|
+
const _hoisted_3 = { class: "tool-step__icon" };
|
|
1926
|
+
const _hoisted_4 = {
|
|
1927
|
+
key: 0,
|
|
1928
|
+
class: "tool-step__spinner",
|
|
1929
|
+
width: "14",
|
|
1930
|
+
height: "14",
|
|
1931
|
+
viewBox: "0 0 24 24",
|
|
1932
|
+
fill: "none"
|
|
1933
|
+
};
|
|
1934
|
+
const _hoisted_5 = {
|
|
1935
|
+
key: 1,
|
|
1936
|
+
width: "14",
|
|
1937
|
+
height: "14",
|
|
1938
|
+
viewBox: "0 0 24 24",
|
|
1939
|
+
fill: "none"
|
|
1940
|
+
};
|
|
1941
|
+
const _hoisted_6 = {
|
|
1942
|
+
key: 2,
|
|
1943
|
+
width: "14",
|
|
1944
|
+
height: "14",
|
|
1945
|
+
viewBox: "0 0 24 24",
|
|
1946
|
+
fill: "none"
|
|
1947
|
+
};
|
|
1948
|
+
const _hoisted_7 = { class: "tool-step__name" };
|
|
1949
|
+
const _hoisted_8 = {
|
|
1950
|
+
key: 0,
|
|
1951
|
+
class: "tool-step__tag tool-step__tag--exec"
|
|
1952
|
+
};
|
|
1953
|
+
const _hoisted_9 = {
|
|
1954
|
+
key: 1,
|
|
1955
|
+
class: "thinking-dots"
|
|
1956
|
+
};
|
|
1957
|
+
const _hoisted_10 = {
|
|
1958
|
+
key: 2,
|
|
1959
|
+
class: "agent-text"
|
|
1960
|
+
};
|
|
1961
|
+
const _hoisted_11 = {
|
|
1962
|
+
key: 0,
|
|
1963
|
+
class: "status-pill"
|
|
1964
|
+
};
|
|
1965
|
+
const _hoisted_12 = { class: "fab-avatar-wrapper" };
|
|
1966
|
+
const _hoisted_13 = ["src"];
|
|
1967
|
+
const currentTheme = "dark";
|
|
1968
|
+
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
1969
|
+
__name: "voice-assistant",
|
|
1970
|
+
props: {
|
|
1971
|
+
xLogo: {},
|
|
1972
|
+
xTitle: {},
|
|
1973
|
+
xSize: {},
|
|
1974
|
+
xTheme: {},
|
|
1975
|
+
wakeWords: {},
|
|
1976
|
+
modelPath: {},
|
|
1977
|
+
projectId: {},
|
|
1978
|
+
invokeUrl: {},
|
|
1979
|
+
voiceConfig: {},
|
|
1980
|
+
bubbleSize: {},
|
|
1981
|
+
bubbleDismissDelay: {}
|
|
1982
|
+
},
|
|
1983
|
+
setup(__props) {
|
|
1984
|
+
const props = __props;
|
|
1985
|
+
const aiChatbotX = injectStrict(AiChatbotXKey);
|
|
1986
|
+
const getVoiceConfig = () => {
|
|
1987
|
+
if (props.voiceConfig) return props.voiceConfig;
|
|
1988
|
+
try {
|
|
1989
|
+
return aiChatbotX.voiceConfig();
|
|
1990
|
+
} catch {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
const endpoint = vue.computed(() => {
|
|
1995
|
+
return props.invokeUrl || "http://localhost:3001/agent/zyy55sw40nrl801056m0o/stream-invoke";
|
|
1996
|
+
});
|
|
1997
|
+
const wakeResponses = ["您好"];
|
|
1998
|
+
const tts = useTTS(getVoiceConfig);
|
|
1999
|
+
const bubbleBridge = {
|
|
2000
|
+
open: () => {
|
|
2001
|
+
},
|
|
2002
|
+
scheduleDismiss: () => {
|
|
2003
|
+
},
|
|
2004
|
+
scrollToBottom: () => {
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
const agent = useAgentInvoke({
|
|
2008
|
+
endpoint,
|
|
2009
|
+
projectId: props.projectId,
|
|
2010
|
+
aiChatbotX,
|
|
2011
|
+
tts: {
|
|
2012
|
+
speak: tts.speak,
|
|
2013
|
+
feed: tts.feed,
|
|
2014
|
+
flush: tts.flush,
|
|
2015
|
+
stop: tts.stop
|
|
2016
|
+
},
|
|
2017
|
+
bubble: {
|
|
2018
|
+
open: () => bubbleBridge.open(),
|
|
2019
|
+
scheduleDismiss: () => bubbleBridge.scheduleDismiss(),
|
|
2020
|
+
scrollToBottom: () => bubbleBridge.scrollToBottom()
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
const bubble = useBubble({
|
|
2024
|
+
dismissDelay: props.bubbleDismissDelay,
|
|
2025
|
+
isSpeaking: tts.isSpeaking,
|
|
2026
|
+
isInvoking: agent.isInvoking,
|
|
2027
|
+
bubbleSize: props.bubbleSize
|
|
2028
|
+
});
|
|
2029
|
+
bubbleBridge.open = bubble.open;
|
|
2030
|
+
bubbleBridge.scheduleDismiss = bubble.scheduleDismiss;
|
|
2031
|
+
bubbleBridge.scrollToBottom = bubble.scrollToBottom;
|
|
2032
|
+
const { show: showBubble, style: bubbleStyle, stackRef: bubbleStackRef } = bubble;
|
|
2033
|
+
tts.setOnQueueEmpty(() => {
|
|
2034
|
+
if (!agent.isInvoking.value) {
|
|
2035
|
+
bubble.scheduleDismiss();
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
const interruptCurrentResponse = () => {
|
|
2039
|
+
agent.abort();
|
|
2040
|
+
agent.resetState();
|
|
2041
|
+
tts.stop();
|
|
2042
|
+
bubble.hide();
|
|
2043
|
+
};
|
|
2044
|
+
const voice = useVoiceRecognition({
|
|
2045
|
+
modelPath: props.modelPath,
|
|
2046
|
+
wakeWords: props.wakeWords,
|
|
2047
|
+
getVoiceConfig,
|
|
2048
|
+
onWake: () => {
|
|
2049
|
+
interruptCurrentResponse();
|
|
2050
|
+
tts.warmUpAudio();
|
|
2051
|
+
const text = wakeResponses[Math.floor(Math.random() * wakeResponses.length)];
|
|
2052
|
+
tts.speak(text);
|
|
2053
|
+
},
|
|
2054
|
+
onTranscriptionDone: (text) => {
|
|
2055
|
+
agent.invoke(text);
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
const toggleVoiceMode = async (targetState) => {
|
|
2059
|
+
tts.warmUpAudio();
|
|
2060
|
+
await voice.toggleVoiceMode(targetState);
|
|
2061
|
+
};
|
|
2062
|
+
const { voiceStatus, transcriptionText, wakeAnimating } = voice;
|
|
2063
|
+
const { isInvoking, currentTextContent, currentToolParts, executingTools, hasAnyContent, toolDisplayName } = agent;
|
|
2064
|
+
aiChatbotX?.registerVoiceMethods({
|
|
2065
|
+
start: () => toggleVoiceMode(true),
|
|
2066
|
+
stop: () => toggleVoiceMode(false),
|
|
2067
|
+
openDialog: async () => Promise.resolve(),
|
|
2068
|
+
closeDialog: async () => Promise.resolve(),
|
|
2069
|
+
toggleCollapse: async () => Promise.resolve()
|
|
2070
|
+
});
|
|
2071
|
+
vue.onBeforeUnmount(async () => {
|
|
2072
|
+
bubble.destroy();
|
|
2073
|
+
agent.abort();
|
|
2074
|
+
tts.destroy();
|
|
2075
|
+
await voice.destroy();
|
|
2076
|
+
});
|
|
2077
|
+
return (_ctx, _cache) => {
|
|
2078
|
+
return vue.openBlock(), vue.createElementBlock("div", {
|
|
2079
|
+
class: "voice-assistant",
|
|
2080
|
+
"data-theme": currentTheme
|
|
2081
|
+
}, [
|
|
2082
|
+
vue.createVNode(vue.Transition, { name: "bubble-fade" }, {
|
|
2083
|
+
default: vue.withCtx(() => [
|
|
2084
|
+
vue.unref(showBubble) ? (vue.openBlock(), vue.createElementBlock("div", {
|
|
2085
|
+
key: 0,
|
|
2086
|
+
class: "bubble-stack",
|
|
2087
|
+
ref_key: "bubbleStackRef",
|
|
2088
|
+
ref: bubbleStackRef,
|
|
2089
|
+
style: vue.normalizeStyle(vue.unref(bubbleStyle))
|
|
2090
|
+
}, [
|
|
2091
|
+
vue.createElementVNode("div", _hoisted_1, [
|
|
2092
|
+
vue.unref(currentToolParts).length > 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2, [
|
|
2093
|
+
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(currentToolParts), (toolPart) => {
|
|
2094
|
+
return vue.openBlock(), vue.createElementBlock("div", {
|
|
2095
|
+
key: toolPart.toolCallId,
|
|
2096
|
+
class: vue.normalizeClass(["tool-step", {
|
|
2097
|
+
"tool-step--loading": toolPart.state === "partial-call" || toolPart.state === "call",
|
|
2098
|
+
"tool-step--done": toolPart.state === "result",
|
|
2099
|
+
"tool-step--error": toolPart.state === "error",
|
|
2100
|
+
"tool-step--executing": vue.unref(executingTools).has(toolPart.toolCallId)
|
|
2101
|
+
}])
|
|
2102
|
+
}, [
|
|
2103
|
+
vue.createElementVNode("span", _hoisted_3, [
|
|
2104
|
+
toolPart.state === "partial-call" || toolPart.state === "call" ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_4, [..._cache[1] || (_cache[1] = [
|
|
2105
|
+
vue.createElementVNode("circle", {
|
|
2106
|
+
cx: "12",
|
|
2107
|
+
cy: "12",
|
|
2108
|
+
r: "10",
|
|
2109
|
+
stroke: "currentColor",
|
|
2110
|
+
"stroke-width": "2.5",
|
|
2111
|
+
"stroke-linecap": "round",
|
|
2112
|
+
"stroke-dasharray": "31.4 31.4"
|
|
2113
|
+
}, null, -1)
|
|
2114
|
+
])])) : toolPart.state === "result" ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_5, [..._cache[2] || (_cache[2] = [
|
|
2115
|
+
vue.createElementVNode("path", {
|
|
2116
|
+
d: "M20 6L9 17l-5-5",
|
|
2117
|
+
stroke: "currentColor",
|
|
2118
|
+
"stroke-width": "2.5",
|
|
2119
|
+
"stroke-linecap": "round",
|
|
2120
|
+
"stroke-linejoin": "round"
|
|
2121
|
+
}, null, -1)
|
|
2122
|
+
])])) : toolPart.state === "error" ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_6, [..._cache[3] || (_cache[3] = [
|
|
2123
|
+
vue.createElementVNode("circle", {
|
|
2124
|
+
cx: "12",
|
|
2125
|
+
cy: "12",
|
|
2126
|
+
r: "10",
|
|
2127
|
+
stroke: "currentColor",
|
|
2128
|
+
"stroke-width": "2"
|
|
2129
|
+
}, null, -1),
|
|
2130
|
+
vue.createElementVNode("path", {
|
|
2131
|
+
d: "M15 9l-6 6M9 9l6 6",
|
|
2132
|
+
stroke: "currentColor",
|
|
2133
|
+
"stroke-width": "2",
|
|
2134
|
+
"stroke-linecap": "round"
|
|
2135
|
+
}, null, -1)
|
|
2136
|
+
])])) : vue.createCommentVNode("", true)
|
|
2137
|
+
]),
|
|
2138
|
+
vue.createElementVNode("span", _hoisted_7, vue.toDisplayString(vue.unref(toolDisplayName)(toolPart.toolName)), 1),
|
|
2139
|
+
vue.unref(executingTools).has(toolPart.toolCallId) ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_8, "命令执行中")) : vue.createCommentVNode("", true)
|
|
2140
|
+
], 2);
|
|
2141
|
+
}), 128))
|
|
2142
|
+
])) : vue.createCommentVNode("", true),
|
|
2143
|
+
vue.unref(isInvoking) && !vue.unref(hasAnyContent) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9, [..._cache[4] || (_cache[4] = [
|
|
2144
|
+
vue.createElementVNode("span", null, null, -1),
|
|
2145
|
+
vue.createElementVNode("span", null, null, -1),
|
|
2146
|
+
vue.createElementVNode("span", null, null, -1)
|
|
2147
|
+
])])) : vue.createCommentVNode("", true),
|
|
2148
|
+
vue.unref(currentTextContent) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_10, vue.toDisplayString(vue.unref(currentTextContent)), 1)) : vue.createCommentVNode("", true)
|
|
2149
|
+
])
|
|
2150
|
+
], 4)) : vue.createCommentVNode("", true)
|
|
2151
|
+
]),
|
|
2152
|
+
_: 1
|
|
2153
|
+
}),
|
|
2154
|
+
vue.createElementVNode("div", {
|
|
2155
|
+
class: "assistant-fab",
|
|
2156
|
+
onClick: _cache[0] || (_cache[0] = ($event) => toggleVoiceMode())
|
|
2157
|
+
}, [
|
|
2158
|
+
vue.unref(transcriptionText) || vue.unref(isInvoking) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_11, vue.toDisplayString(vue.unref(isInvoking) ? "正在思考中..." : vue.unref(transcriptionText)), 1)) : vue.createCommentVNode("", true),
|
|
2159
|
+
vue.createElementVNode("div", _hoisted_12, [
|
|
2160
|
+
vue.createElementVNode("img", {
|
|
2161
|
+
src: __props.xLogo ? __props.xLogo : "/sime.png",
|
|
2162
|
+
alt: "voice assistant",
|
|
2163
|
+
style: vue.normalizeStyle({
|
|
2164
|
+
width: `${__props.xSize?.width || 88}px`
|
|
2165
|
+
})
|
|
2166
|
+
}, null, 12, _hoisted_13),
|
|
2167
|
+
vue.createVNode(vue.Transition, { name: "indicator-fade" }, {
|
|
2168
|
+
default: vue.withCtx(() => [
|
|
2169
|
+
vue.unref(voiceStatus) === "listening" ? (vue.openBlock(), vue.createElementBlock("div", {
|
|
2170
|
+
key: 0,
|
|
2171
|
+
class: vue.normalizeClass(["listening-badge", { "wake-active": vue.unref(wakeAnimating) }])
|
|
2172
|
+
}, [..._cache[5] || (_cache[5] = [
|
|
2173
|
+
vue.createElementVNode("div", { class: "listening-waves" }, [
|
|
2174
|
+
vue.createElementVNode("div", { class: "wave wave-1" }),
|
|
2175
|
+
vue.createElementVNode("div", { class: "wave wave-2" }),
|
|
2176
|
+
vue.createElementVNode("div", { class: "wave wave-3" })
|
|
2177
|
+
], -1),
|
|
2178
|
+
vue.createElementVNode("div", { class: "listening-icon" }, [
|
|
2179
|
+
vue.createElementVNode("svg", {
|
|
2180
|
+
width: "22",
|
|
2181
|
+
height: "22",
|
|
2182
|
+
viewBox: "0 0 24 24",
|
|
2183
|
+
fill: "none"
|
|
2184
|
+
}, [
|
|
2185
|
+
vue.createElementVNode("path", {
|
|
2186
|
+
d: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z",
|
|
2187
|
+
fill: "currentColor"
|
|
2188
|
+
}),
|
|
2189
|
+
vue.createElementVNode("path", {
|
|
2190
|
+
d: "M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5",
|
|
2191
|
+
stroke: "currentColor",
|
|
2192
|
+
"stroke-width": "2",
|
|
2193
|
+
"stroke-linecap": "round"
|
|
2194
|
+
})
|
|
2195
|
+
])
|
|
2196
|
+
], -1)
|
|
2197
|
+
])], 2)) : vue.createCommentVNode("", true)
|
|
2198
|
+
]),
|
|
2199
|
+
_: 1
|
|
2200
|
+
})
|
|
2201
|
+
])
|
|
2202
|
+
])
|
|
2203
|
+
]);
|
|
994
2204
|
};
|
|
995
2205
|
}
|
|
996
2206
|
});
|
|
997
2207
|
|
|
998
|
-
const
|
|
2208
|
+
const voiceAssistant = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9e420a26"]]);
|
|
999
2209
|
|
|
1000
|
-
exports.AiChatbotProvider = _sfc_main$
|
|
2210
|
+
exports.AiChatbotProvider = _sfc_main$4;
|
|
2211
|
+
exports.AiChatbotVoiceAssistant = voiceAssistant;
|
|
1001
2212
|
exports.AiChatbotX = simeX;
|
|
1002
2213
|
exports.AiChatbotXKey = AiChatbotXKey;
|
|
1003
2214
|
exports.clientCommandKey = clientCommandKey;
|