@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.
@@ -31,7 +31,7 @@
31
31
  return clientCommandKey2;
32
32
  })(clientCommandKey || {});
33
33
 
34
- const _sfc_main$3 = /* @__PURE__ */ vue.defineComponent({
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$2 = { class: "content-container" };
112
- const _hoisted_2$2 = { class: "status-header" };
113
- const _hoisted_3$1 = { class: "status-text" };
114
- const _hoisted_4$1 = {
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$1 = {
118
+ const _hoisted_5$2 = {
119
119
  key: 1,
120
120
  class: "placeholder-text"
121
121
  };
122
- const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({
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$2, [
185
- vue.createElementVNode("div", _hoisted_2$2, [
186
- vue.createElementVNode("span", _hoisted_3$1, vue.toDisplayString(statusLabel.value), 1)
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$1, vue.toDisplayString(__props.transcriptionText), 1)) : __props.status === "wake" ? (vue.openBlock(), vue.createElementBlock("p", _hoisted_5$1, "Listening...")) : vue.createCommentVNode("", true)
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$2, [["__scopeId", "data-v-c9fa6caf"]]);
218
+ const VoiceStatus = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-c9fa6caf"]]);
219
219
 
220
- const _hoisted_1$1 = {
220
+ const _hoisted_1$2 = {
221
221
  key: 0,
222
222
  class: "execution-bubble"
223
223
  };
224
- const _hoisted_2$1 = { class: "exec-text" };
225
- const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
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$1, [
236
- vue.createElementVNode("span", _hoisted_2$1, vue.toDisplayString(__props.text || "执行中"), 1),
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$1, [["__scopeId", "data-v-8244ff0d"]]);
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("dark");
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 simeX = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-84e3f35e"]]);
2208
+ const voiceAssistant = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9e420a26"]]);
999
2209
 
1000
- exports.AiChatbotProvider = _sfc_main$3;
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;