@siact/sime-x-vue 0.0.14 → 0.0.16

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.
@@ -89,30 +89,30 @@
89
89
  return history;
90
90
  }
91
91
 
92
- const _hoisted_1$1 = {
92
+ const _hoisted_1$2 = {
93
93
  key: 0,
94
94
  class: "ai-chat__welcome"
95
95
  };
96
- const _hoisted_2$1 = { class: "ai-chat__welcome-header" };
97
- const _hoisted_3$1 = { class: "ai-chat__welcome-title" };
98
- const _hoisted_4$1 = { class: "ai-chat__welcome-desc" };
99
- const _hoisted_5$1 = { class: "ai-chat__input-area" };
100
- const _hoisted_6$1 = { class: "ai-chat__input-wrapper" };
101
- const _hoisted_7$1 = ["disabled"];
102
- const _hoisted_8$1 = {
96
+ const _hoisted_2$2 = { class: "ai-chat__welcome-header" };
97
+ const _hoisted_3$2 = { class: "ai-chat__welcome-title" };
98
+ const _hoisted_4$2 = { class: "ai-chat__welcome-desc" };
99
+ const _hoisted_5$2 = { class: "ai-chat__input-area" };
100
+ const _hoisted_6$2 = { class: "ai-chat__input-wrapper" };
101
+ const _hoisted_7$2 = ["disabled"];
102
+ const _hoisted_8$2 = {
103
103
  key: 0,
104
104
  class: "ai-chat__suggestions"
105
105
  };
106
- const _hoisted_9$1 = ["onClick"];
107
- const _hoisted_10$1 = { class: "ai-chat__messages-inner" };
108
- const _hoisted_11$1 = { class: "ai-chat__message-content" };
109
- const _hoisted_12$1 = ["innerHTML"];
110
- const _hoisted_13$1 = {
106
+ const _hoisted_9$2 = ["onClick"];
107
+ const _hoisted_10$2 = { class: "ai-chat__messages-inner" };
108
+ const _hoisted_11$2 = { class: "ai-chat__message-content" };
109
+ const _hoisted_12$2 = ["innerHTML"];
110
+ const _hoisted_13$2 = {
111
111
  key: 1,
112
112
  class: "ai-chat__reasoning"
113
113
  };
114
- const _hoisted_14 = ["onClick"];
115
- const _hoisted_15 = {
114
+ const _hoisted_14$1 = ["onClick"];
115
+ const _hoisted_15$1 = {
116
116
  key: 0,
117
117
  class: "ai-chat__reasoning-streaming"
118
118
  };
@@ -186,7 +186,7 @@
186
186
  };
187
187
  const _hoisted_30 = { class: "ai-chat__input-wrapper" };
188
188
  const _hoisted_31 = ["disabled"];
189
- const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({
189
+ const _sfc_main$3 = /* @__PURE__ */ vue.defineComponent({
190
190
  __name: "ai-chat",
191
191
  props: {
192
192
  api: {},
@@ -384,17 +384,17 @@
384
384
  return vue.openBlock(), vue.createElementBlock("div", {
385
385
  class: vue.normalizeClass(["ai-chat", { "ai-chat--full-width": __props.fullWidth }])
386
386
  }, [
387
- isEmpty.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [
388
- vue.createElementVNode("div", _hoisted_2$1, [
389
- vue.createElementVNode("h1", _hoisted_3$1, vue.toDisplayString(__props.welcomeTitle), 1),
390
- vue.createElementVNode("p", _hoisted_4$1, vue.toDisplayString(__props.welcomeDescription), 1)
387
+ isEmpty.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$2, [
388
+ vue.createElementVNode("div", _hoisted_2$2, [
389
+ vue.createElementVNode("h1", _hoisted_3$2, vue.toDisplayString(__props.welcomeTitle), 1),
390
+ vue.createElementVNode("p", _hoisted_4$2, vue.toDisplayString(__props.welcomeDescription), 1)
391
391
  ]),
392
- vue.createElementVNode("div", _hoisted_5$1, [
392
+ vue.createElementVNode("div", _hoisted_5$2, [
393
393
  vue.createElementVNode("form", {
394
394
  class: "ai-chat__form",
395
395
  onSubmit: vue.withModifiers(handleSubmit, ["prevent"])
396
396
  }, [
397
- vue.createElementVNode("div", _hoisted_6$1, [
397
+ vue.createElementVNode("div", _hoisted_6$2, [
398
398
  vue.withDirectives(vue.createElementVNode("textarea", {
399
399
  ref_key: "textareaRef",
400
400
  ref: textareaRef,
@@ -430,16 +430,16 @@
430
430
  }),
431
431
  vue.createElementVNode("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
432
432
  ], -1)
433
- ])], 8, _hoisted_7$1)
433
+ ])], 8, _hoisted_7$2)
434
434
  ])
435
435
  ], 32),
436
- __props.suggestions.length > 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_8$1, [
436
+ __props.suggestions.length > 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_8$2, [
437
437
  (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(__props.suggestions, (suggestion) => {
438
438
  return vue.openBlock(), vue.createElementBlock("button", {
439
439
  key: suggestion,
440
440
  class: "ai-chat__suggestion",
441
441
  onClick: ($event) => handleSuggestionClick(suggestion)
442
- }, vue.toDisplayString(suggestion), 9, _hoisted_9$1);
442
+ }, vue.toDisplayString(suggestion), 9, _hoisted_9$2);
443
443
  }), 128))
444
444
  ])) : vue.createCommentVNode("", true)
445
445
  ])
@@ -450,7 +450,7 @@
450
450
  class: "ai-chat__messages",
451
451
  onScroll: handleScroll
452
452
  }, [
453
- vue.createElementVNode("div", _hoisted_10$1, [
453
+ vue.createElementVNode("div", _hoisted_10$2, [
454
454
  (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(chat).messages, (message) => {
455
455
  return vue.openBlock(), vue.createElementBlock("div", {
456
456
  key: message.id,
@@ -464,13 +464,13 @@
464
464
  key: 0,
465
465
  class: vue.normalizeClass(["ai-chat__message", `ai-chat__message--${message.role}`])
466
466
  }, [
467
- vue.createElementVNode("div", _hoisted_11$1, [
467
+ vue.createElementVNode("div", _hoisted_11$2, [
468
468
  vue.createElementVNode("div", {
469
469
  class: "ai-chat__message-text",
470
470
  innerHTML: renderMarkdown(part.text)
471
- }, null, 8, _hoisted_12$1)
471
+ }, null, 8, _hoisted_12$2)
472
472
  ])
473
- ], 2)) : part.type === "reasoning" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_13$1, [
473
+ ], 2)) : part.type === "reasoning" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_13$2, [
474
474
  vue.createElementVNode("button", {
475
475
  class: "ai-chat__reasoning-trigger",
476
476
  onClick: ($event) => toggleReasoning(message.id)
@@ -487,8 +487,8 @@
487
487
  vue.createElementVNode("polyline", { points: "9 18 15 12 9 6" }, null, -1)
488
488
  ])], 2)),
489
489
  _cache[4] || (_cache[4] = vue.createElementVNode("span", null, "思考过程", -1)),
490
- isStreamingMessage(message.id) && partIndex === message.parts.length - 1 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_15)) : vue.createCommentVNode("", true)
491
- ], 8, _hoisted_14),
490
+ isStreamingMessage(message.id) && partIndex === message.parts.length - 1 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_15$1)) : vue.createCommentVNode("", true)
491
+ ], 8, _hoisted_14$1),
492
492
  reasoningOpen[message.id] ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_16, vue.toDisplayString(part.text), 1)) : vue.createCommentVNode("", true)
493
493
  ])) : isToolPart(part) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_17, [
494
494
  vue.createElementVNode("div", {
@@ -693,1213 +693,1457 @@
693
693
  return target;
694
694
  };
695
695
 
696
- const aiChat = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-958fd919"]]);
696
+ const aiChat = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-958fd919"]]);
697
697
 
698
- class CommandManager {
699
- commands = /* @__PURE__ */ new Map();
700
- debug;
701
- constructor(options = {}) {
702
- this.debug = options.debug ?? false;
703
- }
704
- registerCommand(command) {
705
- this.commands.set(command.name, command);
706
- this.log("注册命令", `${command.name}: ${command.description}`);
707
- }
708
- unregisterCommand(name) {
709
- const deleted = this.commands.delete(name);
710
- if (deleted) {
711
- this.log("命令已注销", name);
698
+ const DATA_STREAM_LINE_RE = /^[0-9a-f]:/;
699
+ function detectFormat(firstChunk) {
700
+ const trimmed = firstChunk.trimStart();
701
+ if (trimmed.startsWith("data:")) {
702
+ const firstLine = trimmed.split("\n")[0];
703
+ const payload = firstLine.slice(5).trim();
704
+ try {
705
+ const parsed = JSON.parse(payload);
706
+ if (parsed && typeof parsed.type === "string") {
707
+ return "ui-message-stream";
708
+ }
709
+ } catch {
712
710
  }
713
- }
714
- async executeCommand(command, args = []) {
715
- const commandDef = this.commands.get(command);
716
- if (!commandDef) {
717
- throw new Error(`命令 "${command}" 未找到`);
711
+ if (DATA_STREAM_LINE_RE.test(payload)) {
712
+ return "data-stream";
718
713
  }
719
- this.log("执行命令", command, args);
720
- return await commandDef.handler(...args);
714
+ return "ui-message-stream";
721
715
  }
722
- getCommands() {
723
- return Array.from(this.commands.values()).map((cmd) => ({
724
- name: cmd.name,
725
- description: cmd.description,
726
- parameters: cmd.parameters
727
- }));
716
+ if (DATA_STREAM_LINE_RE.test(trimmed)) {
717
+ return "data-stream";
728
718
  }
729
- hasCommand(name) {
730
- return this.commands.has(name);
719
+ return "plain-text";
720
+ }
721
+ function processUIMessageStreamEvent(payload, callbacks) {
722
+ const trimmed = payload.trim();
723
+ if (!trimmed || trimmed === "[DONE]") {
724
+ callbacks.onFinish?.({});
725
+ return;
731
726
  }
732
- clear() {
733
- this.commands.clear();
734
- this.log("", "所有命令已清空");
727
+ let parsed;
728
+ try {
729
+ parsed = JSON.parse(trimmed);
730
+ } catch {
731
+ console.warn("[DataStreamParser] failed to parse UI message stream event:", trimmed.slice(0, 100));
732
+ return;
735
733
  }
736
- log(prefix, msg, ...args) {
737
- (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
738
- hour: "2-digit",
739
- minute: "2-digit",
740
- second: "2-digit"
741
- });
742
- console.log(
743
- `%c ${prefix}`,
744
- "background:#7c3aed;color:white;padding:2px 6px;border-radius:3px 0 0 3px;font-weight:bold;",
745
- `${msg}`
746
- );
747
- if (args.length > 0) {
748
- console.log(...args);
749
- }
734
+ const type = parsed?.type;
735
+ if (!type) return;
736
+ switch (type) {
737
+ case "text-delta":
738
+ if (typeof parsed.delta === "string") {
739
+ callbacks.onTextDelta?.(parsed.delta);
740
+ }
741
+ break;
742
+ case "tool-input-start":
743
+ callbacks.onToolCallStart?.(parsed.toolCallId, parsed.toolName);
744
+ break;
745
+ case "tool-input-delta":
746
+ callbacks.onToolCallDelta?.(parsed.toolCallId, parsed.inputTextDelta);
747
+ break;
748
+ case "tool-input-available":
749
+ callbacks.onToolCallComplete?.(parsed.toolCallId, parsed.toolName, parsed.input);
750
+ break;
751
+ case "tool-output-available":
752
+ callbacks.onToolResult?.(parsed.toolCallId, parsed.output);
753
+ break;
754
+ case "finish-step":
755
+ callbacks.onStepFinish?.(parsed);
756
+ break;
757
+ case "finish":
758
+ callbacks.onFinish?.(parsed);
759
+ break;
760
+ case "error":
761
+ case "tool-output-error":
762
+ callbacks.onError?.(parsed.errorText || parsed.error || "Unknown error", parsed);
763
+ break;
764
+ case "start":
765
+ case "text-start":
766
+ case "text-end":
767
+ case "start-step":
768
+ case "reasoning-start":
769
+ case "reasoning-delta":
770
+ case "reasoning-end":
771
+ case "source-url":
772
+ case "source-document":
773
+ case "file":
774
+ case "abort":
775
+ break;
776
+ default:
777
+ if (type.startsWith("data-")) ; else {
778
+ console.log("[DataStreamParser] unhandled UI message stream type:", type);
779
+ }
780
+ break;
750
781
  }
751
782
  }
752
-
753
- const AiChatbotXKey = Symbol("sime-x");
754
- function injectStrict(key, defaultValue, treatDefaultAsFactory) {
755
- let result;
756
- if (defaultValue === void 0) {
757
- result = vue.inject(key);
758
- } else if (treatDefaultAsFactory === true) {
759
- result = vue.inject(key, defaultValue, true);
760
- } else {
761
- result = vue.inject(key, defaultValue, false);
783
+ function parseLegacyProtocolLine(line, callbacks) {
784
+ if (!line || !DATA_STREAM_LINE_RE.test(line)) return;
785
+ const code = line[0];
786
+ const rawValue = line.slice(2);
787
+ let value;
788
+ try {
789
+ value = JSON.parse(rawValue);
790
+ } catch {
791
+ value = rawValue;
762
792
  }
763
- if (!result) {
764
- throw new Error(`Could not resolve ${key.description}`);
793
+ switch (code) {
794
+ case "0":
795
+ callbacks.onTextDelta?.(value);
796
+ break;
797
+ case "9":
798
+ callbacks.onToolCallStart?.(value.toolCallId, value.toolName);
799
+ break;
800
+ case "b":
801
+ callbacks.onToolCallDelta?.(value.toolCallId, value.argsTextDelta);
802
+ break;
803
+ case "c":
804
+ callbacks.onToolCallComplete?.(value.toolCallId, value.toolName, value.args);
805
+ break;
806
+ case "a":
807
+ callbacks.onToolResult?.(value.toolCallId, value.result);
808
+ break;
809
+ case "e":
810
+ callbacks.onStepFinish?.(value);
811
+ break;
812
+ case "d":
813
+ callbacks.onFinish?.(value);
814
+ break;
815
+ case "3":
816
+ callbacks.onError?.(value);
817
+ break;
765
818
  }
766
- return result;
767
819
  }
768
-
769
- const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
770
- __name: "sime-provider",
771
- props: {
772
- project: {},
773
- description: {},
774
- debug: { type: Boolean },
775
- chatbotUrl: {},
776
- appId: {},
777
- appToken: {},
778
- agentId: {}
779
- },
780
- setup(__props) {
781
- const props = __props;
782
- const commandManager = vue.shallowRef(new CommandManager({ debug: props.debug ?? false }));
783
- const startListeningRef = vue.shallowRef(async () => {
784
- });
785
- const stopListeningRef = vue.shallowRef(async () => {
786
- });
787
- const stopBroadcastRef = vue.shallowRef(async () => {
788
- });
789
- vue.provide(AiChatbotXKey, {
790
- chatbotUrl: () => props.chatbotUrl,
791
- appId: () => props.appId,
792
- appToken: () => props.appToken,
793
- agentId: () => props.agentId,
794
- startListening: () => startListeningRef.value(),
795
- stopListening: () => stopListeningRef.value(),
796
- stopBroadcast: () => stopBroadcastRef.value(),
797
- registerVoiceMethods: (methods) => {
798
- if (methods.stopBroadcast) stopBroadcastRef.value = methods.stopBroadcast;
799
- if (methods.start) startListeningRef.value = methods.start;
800
- if (methods.stop) stopListeningRef.value = methods.stop;
801
- },
802
- getCommads: async () => commandManager.value.getCommands(),
803
- registerCommand: (cmd) => {
804
- commandManager.value.registerCommand(cmd);
805
- },
806
- unregisterCommand: (name) => {
807
- commandManager.value.unregisterCommand(name);
808
- },
809
- async executeCommand(commandName, args = []) {
810
- return await commandManager.value.executeCommand(commandName, args);
811
- }
812
- });
813
- return (_ctx, _cache) => {
814
- return vue.renderSlot(_ctx.$slots, "default");
815
- };
816
- }
817
- });
818
-
819
- function useTTS(getVoiceConfig) {
820
- const isSpeaking = vue.ref(false);
821
- const hasPendingAudio = vue.ref(false);
822
- let instance = null;
823
- let initPromise = null;
824
- let audioCtx = null;
825
- let sentenceBuffer = "";
826
- const sentenceDelimiters = /[。!?;\n.!?;]/;
827
- 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();
828
- const warmUpAudio = () => {
829
- if (!audioCtx || audioCtx.state === "closed") {
830
- try {
831
- audioCtx = new AudioContext();
832
- } catch {
833
- return;
834
- }
820
+ async function readDataStream(response, callbacks) {
821
+ if (!response.body) return;
822
+ const reader = response.body.getReader();
823
+ const decoder = new TextDecoder();
824
+ let buffer = "";
825
+ let format = null;
826
+ while (true) {
827
+ const { value, done } = await reader.read();
828
+ if (done) break;
829
+ const chunk = decoder.decode(value, { stream: true });
830
+ buffer += chunk;
831
+ if (format === null && buffer.trim().length > 0) {
832
+ format = detectFormat(buffer);
833
+ console.log("[DataStreamParser] detected format:", format, "| first 200 chars:", buffer.slice(0, 200));
835
834
  }
836
- if (audioCtx.state === "suspended") {
837
- audioCtx.resume();
835
+ if (format === "plain-text") {
836
+ const text = buffer;
837
+ buffer = "";
838
+ if (text) callbacks.onTextDelta?.(text);
839
+ continue;
838
840
  }
839
- };
840
- let onQueueEmptyCb = null;
841
- const ensureInstance = async () => {
842
- if (instance) return instance;
843
- if (initPromise) return initPromise;
844
- const vc = getVoiceConfig();
845
- if (!vc || !vc.apiSecret) {
846
- console.warn("[TTS] 缺少 voiceConfig apiSecret,语音播报已禁用");
847
- return null;
848
- }
849
- initPromise = (async () => {
850
- try {
851
- const tts = new webVoiceKit.SpeechSynthesizerStandalone({
852
- appId: vc.appId,
853
- apiKey: vc.ttsApiKey || vc.apiKey,
854
- apiSecret: vc.apiSecret,
855
- websocketUrl: vc.ttsWebsocketUrl || "wss://tts-api.xfyun.cn/v2/tts",
856
- vcn: vc.ttsVcn || "xiaoyan",
857
- speed: vc.speed || 55,
858
- volume: vc.volume || 90,
859
- pitch: vc.pitch || 50,
860
- aue: "raw",
861
- auf: "audio/L16;rate=16000",
862
- tte: "UTF8",
863
- autoPlay: true
864
- });
865
- tts.onStart(() => {
866
- isSpeaking.value = true;
867
- });
868
- tts.onEnd(() => {
869
- });
870
- tts.onQueueEmpty(() => {
871
- isSpeaking.value = false;
872
- hasPendingAudio.value = false;
873
- onQueueEmptyCb?.();
874
- });
875
- tts.onError((err) => {
876
- console.error("[TTS] Error:", err);
877
- isSpeaking.value = false;
878
- });
879
- if (audioCtx && audioCtx.state === "running") {
880
- tts.audioContext = audioCtx;
881
- tts.gainNode = audioCtx.createGain();
882
- tts.gainNode.connect(audioCtx.destination);
841
+ if (format === "ui-message-stream") {
842
+ while (true) {
843
+ const eventEnd = buffer.indexOf("\n\n");
844
+ if (eventEnd === -1) break;
845
+ const eventBlock = buffer.slice(0, eventEnd);
846
+ buffer = buffer.slice(eventEnd + 2);
847
+ const dataLines = eventBlock.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
848
+ for (const dataLine of dataLines) {
849
+ processUIMessageStreamEvent(dataLine, callbacks);
883
850
  }
884
- instance = tts;
885
- initPromise = null;
886
- return tts;
887
- } catch (err) {
888
- console.error("[TTS] 初始化失败:", err);
889
- initPromise = null;
890
- return null;
891
851
  }
892
- })();
893
- return initPromise;
894
- };
895
- const speak = async (text) => {
896
- const clean = stripMarkdown(text);
897
- if (!clean.trim()) return;
898
- hasPendingAudio.value = true;
899
- const tts = await ensureInstance();
900
- if (!tts) return;
901
- try {
902
- tts.speak(clean);
903
- } catch (err) {
904
- console.error("[TTS] speak 失败:", err);
905
- }
906
- };
907
- const feed = (delta) => {
908
- sentenceBuffer += delta;
909
- while (true) {
910
- const match = sentenceBuffer.match(sentenceDelimiters);
911
- if (!match || match.index === void 0) break;
912
- const sentence = sentenceBuffer.slice(0, match.index + 1).trim();
913
- sentenceBuffer = sentenceBuffer.slice(match.index + 1);
914
- if (sentence.length > 0) speak(sentence);
852
+ continue;
915
853
  }
916
- };
917
- const flush = () => {
918
- const remaining = sentenceBuffer.trim();
919
- sentenceBuffer = "";
920
- if (remaining.length > 0) speak(remaining);
921
- };
922
- const stop = () => {
923
- sentenceBuffer = "";
924
- isSpeaking.value = false;
925
- hasPendingAudio.value = false;
926
- if (instance) {
927
- try {
928
- instance.stop();
929
- } catch {
854
+ if (format === "data-stream") {
855
+ const isSSEWrapped = buffer.trimStart().startsWith("data:");
856
+ if (isSSEWrapped) {
857
+ while (true) {
858
+ const eventEnd = buffer.indexOf("\n\n");
859
+ if (eventEnd === -1) break;
860
+ const eventBlock = buffer.slice(0, eventEnd);
861
+ buffer = buffer.slice(eventEnd + 2);
862
+ const dataLines = eventBlock.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
863
+ for (const dl of dataLines) {
864
+ const t = dl.trim();
865
+ if (!t || t === "[DONE]") {
866
+ if (t === "[DONE]") callbacks.onFinish?.({});
867
+ continue;
868
+ }
869
+ parseLegacyProtocolLine(t, callbacks);
870
+ }
871
+ }
872
+ } else {
873
+ while (true) {
874
+ const newlineIdx = buffer.indexOf("\n");
875
+ if (newlineIdx === -1) break;
876
+ const line = buffer.slice(0, newlineIdx).trim();
877
+ buffer = buffer.slice(newlineIdx + 1);
878
+ if (line) parseLegacyProtocolLine(line, callbacks);
879
+ }
930
880
  }
881
+ continue;
931
882
  }
932
- };
933
- const setOnQueueEmpty = (cb) => {
934
- onQueueEmptyCb = cb;
935
- };
936
- const destroy = () => {
937
- stop();
938
- if (instance) {
939
- try {
940
- instance.destroy();
941
- } catch {
883
+ }
884
+ const tail = decoder.decode();
885
+ if (tail) buffer += tail;
886
+ if (buffer.trim()) {
887
+ if (format === "plain-text") {
888
+ callbacks.onTextDelta?.(buffer);
889
+ } else if (format === "ui-message-stream") {
890
+ const dataLines = buffer.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
891
+ for (const dl of dataLines) {
892
+ processUIMessageStreamEvent(dl, callbacks);
942
893
  }
943
- instance = null;
894
+ } else if (format === "data-stream") {
895
+ parseLegacyProtocolLine(buffer.trim(), callbacks);
944
896
  }
945
- if (audioCtx) {
946
- try {
947
- audioCtx.close();
948
- } catch {
897
+ }
898
+ callbacks.onFinish?.({});
899
+ }
900
+ async function parseDataStreamToMessage(response, onUpdate) {
901
+ let textContent = "";
902
+ const parts = [];
903
+ const toolCalls = /* @__PURE__ */ new Map();
904
+ const ensureTextPart = () => {
905
+ for (let i = parts.length - 1; i >= 0; i--) {
906
+ if (parts[i].type === "text") {
907
+ return parts[i];
949
908
  }
950
- audioCtx = null;
951
909
  }
910
+ const textPart = { type: "text", text: "" };
911
+ parts.push(textPart);
912
+ return textPart;
952
913
  };
953
- return {
954
- isSpeaking,
955
- hasPendingAudio,
956
- warmUpAudio,
957
- speak,
958
- feed,
959
- flush,
960
- stop,
961
- destroy,
962
- setOnQueueEmpty
963
- };
964
- }
965
-
966
- function useBubble(options = {}) {
967
- const visible = vue.ref(false);
968
- const fadingOut = vue.ref(false);
969
- const stackRef = vue.ref(null);
970
- let dismissTimer = null;
971
- const hasOpened = vue.ref(false);
972
- const isTTSActive = () => !!(options.isSpeaking?.value || options.hasPendingAudio?.value);
973
- const isBusy = () => !!(options.isInvoking?.value || isTTSActive());
974
- const show = vue.computed(() => {
975
- if (!hasOpened.value) return false;
976
- if (isTTSActive()) return true;
977
- return visible.value && !fadingOut.value;
978
- });
979
- const style = vue.computed(() => ({
980
- width: options.bubbleSize?.width || void 0,
981
- maxHeight: options.bubbleSize?.maxHeight || void 0
982
- }));
983
- const open = () => {
984
- cancelDismiss();
985
- fadingOut.value = false;
986
- visible.value = true;
987
- hasOpened.value = true;
914
+ const findToolPartIndex = (toolCallId) => {
915
+ return parts.findIndex((p) => (p.type === "tool-call" || p.type === "tool-result") && p.toolCallId === toolCallId);
988
916
  };
989
- const cancelDismiss = () => {
990
- if (dismissTimer) {
991
- clearTimeout(dismissTimer);
992
- dismissTimer = null;
993
- }
917
+ const emitUpdate = () => {
918
+ onUpdate({ textContent, parts: [...parts], toolCalls: new Map(toolCalls) });
994
919
  };
995
- const scheduleDismiss = () => {
996
- cancelDismiss();
997
- if (isBusy()) return;
998
- const delay = options.dismissDelay ?? 4e3;
999
- dismissTimer = setTimeout(() => {
1000
- if (isBusy()) return;
1001
- fadingOut.value = true;
1002
- setTimeout(() => {
1003
- if (isBusy()) {
1004
- fadingOut.value = false;
1005
- return;
920
+ await readDataStream(response, {
921
+ onTextDelta(text) {
922
+ textContent += text;
923
+ const textPart = ensureTextPart();
924
+ textPart.text = textContent;
925
+ emitUpdate();
926
+ },
927
+ onToolCallStart(toolCallId, toolName) {
928
+ const tracker = {
929
+ toolCallId,
930
+ toolName,
931
+ argsText: "",
932
+ args: void 0,
933
+ state: "partial-call"
934
+ };
935
+ toolCalls.set(toolCallId, tracker);
936
+ const part = {
937
+ type: "tool-call",
938
+ toolCallId,
939
+ toolName,
940
+ args: void 0,
941
+ state: "partial-call"
942
+ };
943
+ parts.push(part);
944
+ emitUpdate();
945
+ },
946
+ onToolCallDelta(toolCallId, argsTextDelta) {
947
+ const tracker = toolCalls.get(toolCallId);
948
+ if (tracker) {
949
+ tracker.argsText += argsTextDelta;
950
+ try {
951
+ tracker.args = JSON.parse(tracker.argsText);
952
+ } catch {
1006
953
  }
1007
- visible.value = false;
1008
- fadingOut.value = false;
1009
- hasOpened.value = false;
1010
- }, 400);
1011
- }, delay);
1012
- };
1013
- const watchTTSRef = (ttsRef) => {
1014
- vue.watch(ttsRef, (active) => {
1015
- if (active && hasOpened.value) {
1016
- cancelDismiss();
1017
- if (fadingOut.value) fadingOut.value = false;
1018
- } else if (!active && hasOpened.value && !isBusy()) {
1019
- scheduleDismiss();
954
+ const idx = findToolPartIndex(toolCallId);
955
+ if (idx !== -1 && parts[idx].type === "tool-call") {
956
+ parts[idx].args = tracker.args;
957
+ }
958
+ emitUpdate();
1020
959
  }
1021
- });
1022
- };
1023
- if (options.isSpeaking) watchTTSRef(options.isSpeaking);
1024
- if (options.hasPendingAudio) watchTTSRef(options.hasPendingAudio);
1025
- const hide = () => {
1026
- cancelDismiss();
1027
- fadingOut.value = false;
1028
- visible.value = false;
1029
- hasOpened.value = false;
1030
- };
1031
- const scrollToBottom = () => {
1032
- vue.nextTick(() => {
1033
- if (stackRef.value) {
1034
- stackRef.value.scrollTop = stackRef.value.scrollHeight;
960
+ },
961
+ onToolCallComplete(toolCallId, toolName, args) {
962
+ const tracker = toolCalls.get(toolCallId);
963
+ if (tracker) {
964
+ tracker.state = "call";
965
+ tracker.args = typeof args === "string" ? safeJsonParse(args) : args;
966
+ } else {
967
+ toolCalls.set(toolCallId, {
968
+ toolCallId,
969
+ toolName,
970
+ argsText: typeof args === "string" ? args : JSON.stringify(args),
971
+ args: typeof args === "string" ? safeJsonParse(args) : args,
972
+ state: "call"
973
+ });
1035
974
  }
1036
- });
1037
- };
1038
- const destroy = () => {
1039
- cancelDismiss();
1040
- };
1041
- return {
1042
- visible,
1043
- fadingOut,
1044
- show,
1045
- style,
1046
- stackRef,
1047
- open,
1048
- hide,
1049
- cancelDismiss,
1050
- scheduleDismiss,
1051
- scrollToBottom,
1052
- destroy
1053
- };
1054
- }
1055
-
1056
- const ensureMicrophonePermission = async () => {
1057
- if (typeof navigator === "undefined" || typeof window === "undefined") {
1058
- console.log("当前环境不支持麦克风访问");
1059
- return false;
1060
- }
1061
- if (!navigator.mediaDevices?.getUserMedia || !navigator.mediaDevices?.enumerateDevices) {
1062
- console.log("当前环境不支持麦克风访问");
1063
- return false;
1064
- }
1065
- try {
1066
- const devices = await navigator.mediaDevices.enumerateDevices();
1067
- const audioInputDevices = devices.filter((device) => device.kind === "audioinput");
1068
- if (audioInputDevices.length === 0) {
1069
- console.log("未检测到麦克风设备,请连接麦克风后重试。");
1070
- return false;
1071
- }
1072
- if ("permissions" in navigator && navigator.permissions?.query) {
1073
- try {
1074
- const status = await navigator.permissions.query({ name: "microphone" });
1075
- if (status.state === "denied") {
1076
- console.log("麦克风权限被禁用,请在浏览器设置中开启。");
1077
- return false;
1078
- }
1079
- } catch (e) {
1080
- console.warn("Permission query not supported:", e);
975
+ const idx = findToolPartIndex(toolCallId);
976
+ if (idx !== -1) {
977
+ parts[idx].state = "call";
978
+ parts[idx].toolName = toolName;
979
+ parts[idx].args = toolCalls.get(toolCallId).args;
980
+ } else {
981
+ parts.push({
982
+ type: "tool-call",
983
+ toolCallId,
984
+ toolName,
985
+ args: toolCalls.get(toolCallId).args,
986
+ state: "call"
987
+ });
1081
988
  }
1082
- }
1083
- let stream = null;
1084
- try {
1085
- stream = await navigator.mediaDevices.getUserMedia({
1086
- audio: {
1087
- echoCancellation: true,
1088
- noiseSuppression: true,
1089
- autoGainControl: true
1090
- }
1091
- });
1092
- const audioTracks = stream.getAudioTracks();
1093
- if (audioTracks.length === 0) {
1094
- console.log("无法获取麦克风音频轨道。");
1095
- return false;
989
+ emitUpdate();
990
+ },
991
+ onToolResult(toolCallId, result) {
992
+ const tracker = toolCalls.get(toolCallId);
993
+ if (tracker) {
994
+ tracker.result = result;
995
+ tracker.state = "result";
1096
996
  }
1097
- const activeTrack = audioTracks[0];
1098
- if (!activeTrack.enabled || activeTrack.readyState !== "live") {
1099
- console.log("麦克风设备不可用,请检查设备连接。");
1100
- return false;
997
+ const idx = findToolPartIndex(toolCallId);
998
+ if (idx !== -1) {
999
+ const existing = parts[idx];
1000
+ const resultPart = {
1001
+ type: "tool-result",
1002
+ toolCallId,
1003
+ toolName: existing.toolName,
1004
+ args: existing.args,
1005
+ result,
1006
+ state: "result"
1007
+ };
1008
+ parts[idx] = resultPart;
1009
+ } else {
1010
+ parts.push({
1011
+ type: "tool-result",
1012
+ toolCallId,
1013
+ toolName: tracker?.toolName || "unknown",
1014
+ args: tracker?.args,
1015
+ result,
1016
+ state: "result"
1017
+ });
1101
1018
  }
1102
- return true;
1103
- } finally {
1104
- if (stream) {
1105
- stream.getTracks().forEach((track) => track.stop());
1019
+ emitUpdate();
1020
+ },
1021
+ onError(error, data) {
1022
+ const toolCallId = data?.toolCallId;
1023
+ if (toolCallId) {
1024
+ toolCalls.delete(toolCallId);
1025
+ const idx = findToolPartIndex(toolCallId);
1026
+ if (idx !== -1) {
1027
+ parts.splice(idx, 1);
1028
+ emitUpdate();
1029
+ }
1106
1030
  }
1031
+ console.error("[DataStreamParser] stream error:", error);
1032
+ },
1033
+ onStepFinish(_data) {
1034
+ emitUpdate();
1035
+ },
1036
+ onFinish(_data) {
1037
+ emitUpdate();
1107
1038
  }
1108
- } catch (error) {
1109
- console.error("Microphone permission check failed", error);
1110
- if (error.name === "NotFoundError" || error.name === "DevicesNotFoundError") {
1111
- console.log("未检测到麦克风设备,请连接麦克风后重试。");
1112
- } else if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") {
1113
- console.log("麦克风权限被拒绝,请在浏览器设置中允许访问。");
1114
- } else if (error.name === "NotReadableError" || error.name === "TrackStartError") {
1115
- console.log("麦克风被其他应用占用或无法访问。");
1116
- } else {
1117
- console.log("无法访问麦克风,请检查设备连接和浏览器权限。");
1118
- }
1119
- return false;
1039
+ });
1040
+ return { textContent, parts, toolCalls };
1041
+ }
1042
+ function safeJsonParse(str) {
1043
+ try {
1044
+ return JSON.parse(str);
1045
+ } catch {
1046
+ return str;
1120
1047
  }
1121
- };
1048
+ }
1122
1049
 
1123
- function useVoiceRecognition(options) {
1124
- const voiceStatus = vue.ref("standby");
1125
- const isTranscribing = vue.ref(false);
1126
- const isInitializing = vue.ref(false);
1127
- const transcriptionText = vue.ref("");
1128
- const wakeAnimating = vue.ref(false);
1129
- let detector = null;
1130
- let transcriber = null;
1131
- const initTranscriber = () => {
1132
- if (transcriber) return;
1133
- const vc = options.getVoiceConfig();
1134
- if (!vc || !vc.appId || !vc.apiKey || !vc.websocketUrl) {
1135
- console.error("[VoiceRecognition] 缺少 voiceConfig,无法初始化转写器");
1136
- return;
1050
+ const toolDisplayNames = {
1051
+ generateReport: "生成报告",
1052
+ searchKnowledge: "知识库检索",
1053
+ resolveInstanceTargets: "解析实例目标",
1054
+ getHistoryMetrics: "历史数据查询",
1055
+ getRealtimeMetrics: "实时数据查询",
1056
+ queryBitableData: "多维表格查询",
1057
+ searchUser: "搜索用户",
1058
+ createBitableRecord: "创建表格记录",
1059
+ timeTool: "时间工具",
1060
+ loadSkill: "加载技能",
1061
+ executeCommand: "执行命令",
1062
+ dataAnalyzer: "数据分析",
1063
+ dataPredictor: "数据预测"
1064
+ };
1065
+ function useAgentInvoke(options) {
1066
+ const { aiChatbotX, tts, bubble } = options;
1067
+ const sessionTimeoutMs = options.sessionTimeoutMs ?? 12e4;
1068
+ const maxHistoryTurns = options.maxHistoryTurns ?? 10;
1069
+ const isInvoking = vue.ref(false);
1070
+ const currentTextContent = vue.ref("");
1071
+ const currentToolParts = vue.ref([]);
1072
+ const executingTools = vue.ref(/* @__PURE__ */ new Set());
1073
+ const conversationHistory = vue.ref([]);
1074
+ let lastInteractionTime = 0;
1075
+ const checkSessionTimeout = () => {
1076
+ if (lastInteractionTime > 0 && Date.now() - lastInteractionTime > sessionTimeoutMs) {
1077
+ conversationHistory.value = [];
1137
1078
  }
1138
- transcriber = new webVoiceKit.SpeechTranscriberStandalone({
1139
- appId: vc.appId,
1140
- apiKey: vc.apiKey,
1141
- websocketUrl: vc.websocketUrl,
1142
- autoStop: {
1143
- enabled: true,
1144
- silenceTimeoutMs: 2e3,
1145
- noSpeechTimeoutMs: 5e3,
1146
- maxDurationMs: 45e3
1147
- }
1148
- });
1149
- transcriber.onResult((result) => {
1150
- transcriptionText.value = result.transcript || "";
1151
- });
1152
- transcriber.onAutoStop(async () => {
1153
- const finalText = transcriptionText.value;
1154
- await stopTranscribing();
1155
- transcriptionText.value = "";
1156
- if (finalText.trim()) {
1157
- options.onTranscriptionDone?.(finalText);
1158
- }
1159
- });
1160
- transcriber.onError((error) => {
1161
- console.error("[VoiceRecognition] 转写错误:", error);
1162
- stopTranscribing();
1163
- transcriptionText.value = "";
1164
- });
1165
1079
  };
1166
- const startTranscribing = async () => {
1167
- if (isTranscribing.value) return;
1168
- if (!transcriber) initTranscriber();
1169
- if (!transcriber) return;
1170
- try {
1171
- await transcriber.start();
1172
- isTranscribing.value = true;
1173
- transcriptionText.value = "";
1174
- } catch (error) {
1175
- console.error("[VoiceRecognition] 启动转写失败:", error);
1080
+ const appendToHistory = (role, content) => {
1081
+ conversationHistory.value.push({ role, content });
1082
+ const maxLen = maxHistoryTurns * 2;
1083
+ if (conversationHistory.value.length > maxLen) {
1084
+ conversationHistory.value = conversationHistory.value.slice(-maxLen);
1176
1085
  }
1177
1086
  };
1178
- const stopTranscribing = async () => {
1179
- if (!transcriber || !transcriber.isActive()) {
1180
- isTranscribing.value = false;
1181
- return;
1087
+ const clearHistory = () => {
1088
+ conversationHistory.value = [];
1089
+ };
1090
+ let abortController = null;
1091
+ const hasAnyContent = vue.computed(() => {
1092
+ return !!(currentTextContent.value || currentToolParts.value.length > 0);
1093
+ });
1094
+ const toolDisplayName = (name) => toolDisplayNames[name] || name;
1095
+ const resetState = () => {
1096
+ currentTextContent.value = "";
1097
+ currentToolParts.value = [];
1098
+ executingTools.value = /* @__PURE__ */ new Set();
1099
+ };
1100
+ const extractExecutableCommands = (payload) => {
1101
+ if (!payload || typeof payload !== "object") return [];
1102
+ const commands = payload.commands;
1103
+ if (!Array.isArray(commands) || commands.length === 0) return [];
1104
+ return commands.filter((cmd) => cmd && typeof cmd === "object" && typeof cmd.name === "string" && cmd.name.trim()).map((cmd) => ({
1105
+ name: cmd.name,
1106
+ args: Array.isArray(cmd.args) ? cmd.args : []
1107
+ }));
1108
+ };
1109
+ const buildCommandDefinitionMap = (commands) => {
1110
+ return new Map(commands.map((command) => [command.name, command]));
1111
+ };
1112
+ const toExecutableCommand = (toolName, payload, commandDefinitions) => {
1113
+ const commandDefinition = commandDefinitions.get(toolName);
1114
+ if (!commandDefinition) {
1115
+ return null;
1182
1116
  }
1183
- try {
1184
- await transcriber.stop();
1185
- } catch (error) {
1186
- console.error("[VoiceRecognition] 停止转写失败:", error);
1187
- } finally {
1188
- isTranscribing.value = false;
1117
+ const parameters = commandDefinition.parameters || [];
1118
+ if (Array.isArray(payload)) {
1119
+ return {
1120
+ name: toolName,
1121
+ args: payload
1122
+ };
1123
+ }
1124
+ if (!payload || typeof payload !== "object") {
1125
+ return {
1126
+ name: toolName,
1127
+ args: []
1128
+ };
1189
1129
  }
1130
+ const payloadRecord = payload;
1131
+ return {
1132
+ name: toolName,
1133
+ args: parameters.map((parameter) => payloadRecord[parameter.name])
1134
+ };
1190
1135
  };
1191
- const initDetector = () => {
1192
- if (detector || isInitializing.value) return;
1193
- if (!options.modelPath) {
1194
- console.error("[VoiceRecognition] 未传入 modelPath,无法启用唤醒词");
1195
- return;
1136
+ const resolveExecutableCommands = (toolName, payload, commandDefinitions) => {
1137
+ const extractedCommands = extractExecutableCommands(payload);
1138
+ if (extractedCommands.length > 0) {
1139
+ return extractedCommands;
1196
1140
  }
1197
- isInitializing.value = true;
1141
+ const directCommand = toExecutableCommand(toolName, payload, commandDefinitions);
1142
+ return directCommand ? [directCommand] : [];
1143
+ };
1144
+ const executeHostCommands = async (toolCallId, toolName, payload, commandDefinitions) => {
1145
+ const commands = resolveExecutableCommands(toolName, payload, commandDefinitions);
1146
+ if (commands.length === 0) return false;
1198
1147
  try {
1199
- detector = new webVoiceKit.WakeWordDetectorStandalone({
1200
- modelPath: options.modelPath,
1201
- sampleRate: 16e3,
1202
- usePartial: true,
1203
- autoReset: {
1204
- enabled: true,
1205
- resetDelayMs: 4e3
1148
+ executingTools.value = /* @__PURE__ */ new Set([...executingTools.value, toolCallId]);
1149
+ for (const cmd of commands) {
1150
+ try {
1151
+ await aiChatbotX.executeCommand(cmd.name, cmd.args);
1152
+ } catch (cmdErr) {
1153
+ console.error(`[AgentInvoke] 执行命令 ${cmd.name} 失败:`, cmdErr);
1206
1154
  }
1207
- });
1208
- detector.setWakeWords(options.wakeWords || ["你好", "您好"]);
1209
- detector.onWake(async () => {
1210
- wakeAnimating.value = true;
1211
- options.onWake?.();
1212
- await startTranscribing();
1213
- setTimeout(() => {
1214
- wakeAnimating.value = false;
1215
- }, 1200);
1216
- });
1217
- detector.onError((error) => {
1218
- console.error("[VoiceRecognition] 唤醒监听错误:", error);
1219
- voiceStatus.value = "standby";
1220
- stopTranscribing();
1221
- });
1155
+ }
1156
+ return true;
1222
1157
  } finally {
1223
- isInitializing.value = false;
1158
+ const next = new Set(executingTools.value);
1159
+ next.delete(toolCallId);
1160
+ executingTools.value = next;
1224
1161
  }
1225
1162
  };
1226
- const toggleVoiceMode = async (targetState) => {
1227
- const permission = await ensureMicrophonePermission();
1228
- if (!permission || isInitializing.value) return;
1229
- if (!detector) {
1230
- initDetector();
1231
- if (!detector) return;
1163
+ const parseAssistantText = (payload) => {
1164
+ if (!payload) return "";
1165
+ if (typeof payload === "string") return payload;
1166
+ if (typeof payload === "object") {
1167
+ const data = payload;
1168
+ const directText = data.output || data.answer || data.message || data.result;
1169
+ if (typeof directText === "string" && directText.trim()) return directText;
1170
+ if (data.data && typeof data.data === "object") {
1171
+ const nested = data.data;
1172
+ const nestedText = nested.output || nested.answer || nested.message || nested.result;
1173
+ if (typeof nestedText === "string" && nestedText.trim()) return nestedText;
1174
+ }
1175
+ return JSON.stringify(payload);
1232
1176
  }
1233
- const isListening = voiceStatus.value === "listening";
1234
- const shouldStart = targetState !== void 0 ? targetState : !isListening;
1235
- if (isListening === shouldStart) return;
1177
+ return String(payload);
1178
+ };
1179
+ const invoke = async (question) => {
1180
+ const content = question.trim();
1181
+ if (!content) return;
1182
+ abort();
1183
+ checkSessionTimeout();
1184
+ resetState();
1185
+ tts.stop();
1186
+ isInvoking.value = true;
1187
+ bubble.open();
1188
+ let prevTextLength = 0;
1189
+ const processedToolResults = /* @__PURE__ */ new Set();
1190
+ const processingToolResults = /* @__PURE__ */ new Set();
1191
+ abortController = new AbortController();
1192
+ const commands = await aiChatbotX.getCommads();
1193
+ const commandDefinitions = buildCommandDefinitionMap(commands);
1194
+ conversationHistory.value.length > 0 ? [...conversationHistory.value] : void 0;
1236
1195
  try {
1237
- if (shouldStart) {
1238
- await detector.start();
1239
- voiceStatus.value = "listening";
1196
+ const response = await fetch(options.endpoint, {
1197
+ method: "POST",
1198
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.appToken || ""}` },
1199
+ body: JSON.stringify({
1200
+ input: content,
1201
+ projectId: options.projectId || "",
1202
+ commands: commands.length > 0 ? commands : void 0
1203
+ // messages: historyToSend,
1204
+ }),
1205
+ signal: abortController.signal
1206
+ });
1207
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1208
+ const contentType = response.headers.get("content-type") || "";
1209
+ const isJsonResponse = contentType.includes("application/json");
1210
+ if (isJsonResponse) {
1211
+ const data = await response.json();
1212
+ const reply = parseAssistantText(data) || "已收到,但没有返回可展示的文本内容。";
1213
+ currentTextContent.value = reply;
1214
+ tts.speak(reply);
1215
+ appendToHistory("user", content);
1216
+ appendToHistory("assistant", reply);
1217
+ if (data.toolResults && Array.isArray(data.toolResults)) {
1218
+ for (const tr of data.toolResults) {
1219
+ const toolPart = {
1220
+ type: "tool-result",
1221
+ toolCallId: `invoke-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1222
+ toolName: tr.toolName,
1223
+ args: tr.args,
1224
+ result: tr.result,
1225
+ state: "result"
1226
+ };
1227
+ currentToolParts.value = [...currentToolParts.value, toolPart];
1228
+ if (commandDefinitions.has(tr.toolName)) {
1229
+ void executeHostCommands(toolPart.toolCallId, tr.toolName, tr.result, commandDefinitions);
1230
+ }
1231
+ }
1232
+ }
1240
1233
  } else {
1241
- await detector.stop();
1242
- voiceStatus.value = "standby";
1243
- transcriptionText.value = "";
1244
- await stopTranscribing();
1234
+ await parseDataStreamToMessage(response, (result) => {
1235
+ currentTextContent.value = result.textContent;
1236
+ if (result.textContent.length > prevTextLength) {
1237
+ const delta = result.textContent.slice(prevTextLength);
1238
+ prevTextLength = result.textContent.length;
1239
+ tts.feed(delta);
1240
+ }
1241
+ const toolParts = result.parts.filter(
1242
+ (p) => p.type === "tool-call" || p.type === "tool-result"
1243
+ );
1244
+ currentToolParts.value = toolParts;
1245
+ for (const part of toolParts) {
1246
+ if (commandDefinitions.has(part.toolName) && !processedToolResults.has(part.toolCallId) && !processingToolResults.has(part.toolCallId)) {
1247
+ if (part.type === "tool-call" && part.state === "call" && part.args) {
1248
+ processingToolResults.add(part.toolCallId);
1249
+ void executeHostCommands(part.toolCallId, part.toolName, part.args, commandDefinitions).then(
1250
+ (executed) => {
1251
+ if (executed) {
1252
+ processedToolResults.add(part.toolCallId);
1253
+ }
1254
+ processingToolResults.delete(part.toolCallId);
1255
+ }
1256
+ );
1257
+ } else if (part.type === "tool-result" && part.result) {
1258
+ processingToolResults.add(part.toolCallId);
1259
+ void executeHostCommands(part.toolCallId, part.toolName, part.result, commandDefinitions).then(
1260
+ (executed) => {
1261
+ if (executed) {
1262
+ processedToolResults.add(part.toolCallId);
1263
+ }
1264
+ processingToolResults.delete(part.toolCallId);
1265
+ }
1266
+ );
1267
+ }
1268
+ }
1269
+ }
1270
+ bubble.scrollToBottom();
1271
+ });
1272
+ tts.flush();
1273
+ const assistantReply = currentTextContent.value.trim();
1274
+ appendToHistory("user", content);
1275
+ if (assistantReply) {
1276
+ appendToHistory("assistant", assistantReply);
1277
+ }
1278
+ if (!assistantReply && currentToolParts.value.length === 0) {
1279
+ currentTextContent.value = "已收到,但没有返回可展示的文本内容。";
1280
+ }
1245
1281
  }
1246
1282
  } catch (error) {
1247
- console.error("[VoiceRecognition] 监听切换失败:", error);
1248
- voiceStatus.value = "standby";
1249
- }
1283
+ if (error.name === "AbortError") {
1284
+ return;
1285
+ }
1286
+ console.error("[AgentInvoke] invoke failed:", error);
1287
+ tts.stop();
1288
+ currentTextContent.value = "请求失败,请检查服务地址或稍后重试。";
1289
+ } finally {
1290
+ isInvoking.value = false;
1291
+ abortController = null;
1292
+ lastInteractionTime = Date.now();
1293
+ bubble.scheduleDismiss();
1294
+ }
1250
1295
  };
1251
- const abortTranscription = async () => {
1252
- transcriptionText.value = "";
1253
- await stopTranscribing();
1296
+ const abort = () => {
1297
+ if (abortController) {
1298
+ abortController.abort();
1299
+ abortController = null;
1300
+ }
1301
+ tts.stop();
1302
+ isInvoking.value = false;
1254
1303
  };
1255
- const destroy = async () => {
1256
- if (detector) {
1257
- try {
1258
- if (detector.isActive()) await detector.stop();
1259
- } catch {
1260
- }
1261
- detector = null;
1304
+ return {
1305
+ isInvoking,
1306
+ currentTextContent,
1307
+ currentToolParts,
1308
+ executingTools,
1309
+ hasAnyContent,
1310
+ conversationHistory,
1311
+ toolDisplayName,
1312
+ invoke,
1313
+ abort,
1314
+ resetState,
1315
+ clearHistory
1316
+ };
1317
+ }
1318
+
1319
+ function useBubble(options = {}) {
1320
+ const visible = vue.ref(false);
1321
+ const fadingOut = vue.ref(false);
1322
+ const stackRef = vue.ref(null);
1323
+ let dismissTimer = null;
1324
+ const hasOpened = vue.ref(false);
1325
+ const isTTSActive = () => !!(options.isSpeaking?.value || options.hasPendingAudio?.value);
1326
+ const isBusy = () => !!(options.isInvoking?.value || isTTSActive());
1327
+ const show = vue.computed(() => {
1328
+ if (!hasOpened.value) return false;
1329
+ if (isTTSActive()) return true;
1330
+ return visible.value && !fadingOut.value;
1331
+ });
1332
+ const style = vue.computed(() => ({
1333
+ width: options.bubbleSize?.width || void 0,
1334
+ maxHeight: options.bubbleSize?.maxHeight || void 0
1335
+ }));
1336
+ const open = () => {
1337
+ cancelDismiss();
1338
+ fadingOut.value = false;
1339
+ visible.value = true;
1340
+ hasOpened.value = true;
1341
+ };
1342
+ const cancelDismiss = () => {
1343
+ if (dismissTimer) {
1344
+ clearTimeout(dismissTimer);
1345
+ dismissTimer = null;
1262
1346
  }
1263
- if (transcriber) {
1264
- try {
1265
- if (transcriber.isActive()) await transcriber.stop();
1266
- } catch {
1347
+ };
1348
+ const scheduleDismiss = () => {
1349
+ cancelDismiss();
1350
+ if (isBusy()) return;
1351
+ const delay = options.dismissDelay ?? 4e3;
1352
+ dismissTimer = setTimeout(() => {
1353
+ if (isBusy()) return;
1354
+ fadingOut.value = true;
1355
+ setTimeout(() => {
1356
+ if (isBusy()) {
1357
+ fadingOut.value = false;
1358
+ return;
1359
+ }
1360
+ visible.value = false;
1361
+ fadingOut.value = false;
1362
+ hasOpened.value = false;
1363
+ }, 400);
1364
+ }, delay);
1365
+ };
1366
+ const watchTTSRef = (ttsRef) => {
1367
+ vue.watch(ttsRef, (active) => {
1368
+ if (active && hasOpened.value) {
1369
+ cancelDismiss();
1370
+ if (fadingOut.value) fadingOut.value = false;
1371
+ } else if (!active && hasOpened.value && !isBusy()) {
1372
+ scheduleDismiss();
1267
1373
  }
1268
- transcriber = null;
1269
- }
1374
+ });
1375
+ };
1376
+ if (options.isSpeaking) watchTTSRef(options.isSpeaking);
1377
+ if (options.hasPendingAudio) watchTTSRef(options.hasPendingAudio);
1378
+ const hide = () => {
1379
+ cancelDismiss();
1380
+ fadingOut.value = false;
1381
+ visible.value = false;
1382
+ hasOpened.value = false;
1383
+ };
1384
+ const scrollToBottom = () => {
1385
+ vue.nextTick(() => {
1386
+ if (stackRef.value) {
1387
+ stackRef.value.scrollTop = stackRef.value.scrollHeight;
1388
+ }
1389
+ });
1390
+ };
1391
+ const destroy = () => {
1392
+ cancelDismiss();
1270
1393
  };
1271
1394
  return {
1272
- voiceStatus,
1273
- isTranscribing,
1274
- isInitializing,
1275
- transcriptionText,
1276
- wakeAnimating,
1277
- startTranscribing,
1278
- stopTranscribing,
1279
- abortTranscription,
1280
- toggleVoiceMode,
1395
+ visible,
1396
+ fadingOut,
1397
+ show,
1398
+ style,
1399
+ stackRef,
1400
+ open,
1401
+ hide,
1402
+ cancelDismiss,
1403
+ scheduleDismiss,
1404
+ scrollToBottom,
1281
1405
  destroy
1282
1406
  };
1283
1407
  }
1284
1408
 
1285
- const DATA_STREAM_LINE_RE = /^[0-9a-f]:/;
1286
- function detectFormat(firstChunk) {
1287
- const trimmed = firstChunk.trimStart();
1288
- if (trimmed.startsWith("data:")) {
1289
- const firstLine = trimmed.split("\n")[0];
1290
- const payload = firstLine.slice(5).trim();
1291
- try {
1292
- const parsed = JSON.parse(payload);
1293
- if (parsed && typeof parsed.type === "string") {
1294
- return "ui-message-stream";
1295
- }
1296
- } catch {
1297
- }
1298
- if (DATA_STREAM_LINE_RE.test(payload)) {
1299
- return "data-stream";
1300
- }
1301
- return "ui-message-stream";
1409
+ const AiChatbotXKey = Symbol("sime-x");
1410
+ function injectStrict(key, defaultValue, treatDefaultAsFactory) {
1411
+ let result;
1412
+ if (defaultValue === void 0) {
1413
+ result = vue.inject(key);
1414
+ } else if (treatDefaultAsFactory === true) {
1415
+ result = vue.inject(key, defaultValue, true);
1416
+ } else {
1417
+ result = vue.inject(key, defaultValue, false);
1302
1418
  }
1303
- if (DATA_STREAM_LINE_RE.test(trimmed)) {
1304
- return "data-stream";
1419
+ if (!result) {
1420
+ throw new Error(`Could not resolve ${key.description}`);
1305
1421
  }
1306
- return "plain-text";
1422
+ return result;
1307
1423
  }
1308
- function processUIMessageStreamEvent(payload, callbacks) {
1309
- const trimmed = payload.trim();
1310
- if (!trimmed || trimmed === "[DONE]") {
1311
- callbacks.onFinish?.({});
1312
- return;
1313
- }
1314
- let parsed;
1315
- try {
1316
- parsed = JSON.parse(trimmed);
1317
- } catch {
1318
- console.warn("[DataStreamParser] failed to parse UI message stream event:", trimmed.slice(0, 100));
1319
- return;
1320
- }
1321
- const type = parsed?.type;
1322
- if (!type) return;
1323
- switch (type) {
1324
- case "text-delta":
1325
- if (typeof parsed.delta === "string") {
1326
- callbacks.onTextDelta?.(parsed.delta);
1424
+
1425
+ const _hoisted_1$1 = { class: "agent-bubble" };
1426
+ const _hoisted_2$1 = {
1427
+ key: 0,
1428
+ class: "tool-steps"
1429
+ };
1430
+ const _hoisted_3$1 = { class: "tool-step__icon" };
1431
+ const _hoisted_4$1 = {
1432
+ key: 0,
1433
+ class: "tool-step__spinner",
1434
+ width: "14",
1435
+ height: "14",
1436
+ viewBox: "0 0 24 24",
1437
+ fill: "none"
1438
+ };
1439
+ const _hoisted_5$1 = {
1440
+ key: 1,
1441
+ width: "14",
1442
+ height: "14",
1443
+ viewBox: "0 0 24 24",
1444
+ fill: "none"
1445
+ };
1446
+ const _hoisted_6$1 = {
1447
+ key: 2,
1448
+ width: "14",
1449
+ height: "14",
1450
+ viewBox: "0 0 24 24",
1451
+ fill: "none"
1452
+ };
1453
+ const _hoisted_7$1 = { class: "tool-step__name" };
1454
+ const _hoisted_8$1 = {
1455
+ key: 0,
1456
+ class: "tool-step__tag tool-step__tag--exec"
1457
+ };
1458
+ const _hoisted_9$1 = {
1459
+ key: 1,
1460
+ class: "thinking-dots"
1461
+ };
1462
+ const _hoisted_10$1 = {
1463
+ key: 2,
1464
+ class: "agent-text"
1465
+ };
1466
+ const _hoisted_11$1 = { class: "input-bar" };
1467
+ const _hoisted_12$1 = ["disabled"];
1468
+ const _hoisted_13$1 = ["disabled"];
1469
+ const _hoisted_14 = {
1470
+ key: 0,
1471
+ class: "btn-spinner",
1472
+ width: "18",
1473
+ height: "18",
1474
+ viewBox: "0 0 24 24",
1475
+ fill: "none"
1476
+ };
1477
+ const _hoisted_15 = {
1478
+ key: 1,
1479
+ width: "18",
1480
+ height: "18",
1481
+ viewBox: "0 0 24 24",
1482
+ fill: "none"
1483
+ };
1484
+ const currentTheme$1 = "dark";
1485
+ const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({
1486
+ __name: "command-test",
1487
+ props: {
1488
+ agentId: {},
1489
+ projectId: {},
1490
+ bubbleSize: {},
1491
+ bubbleDismissDelay: {}
1492
+ },
1493
+ setup(__props) {
1494
+ const props = __props;
1495
+ const aiChatbotX = injectStrict(AiChatbotXKey);
1496
+ const inputText = vue.ref("");
1497
+ const noopTts = {
1498
+ speak: (_text) => {
1499
+ },
1500
+ feed: (_delta) => {
1501
+ },
1502
+ flush: () => {
1503
+ },
1504
+ stop: () => {
1327
1505
  }
1328
- break;
1329
- case "tool-input-start":
1330
- callbacks.onToolCallStart?.(parsed.toolCallId, parsed.toolName);
1331
- break;
1332
- case "tool-input-delta":
1333
- callbacks.onToolCallDelta?.(parsed.toolCallId, parsed.inputTextDelta);
1334
- break;
1335
- case "tool-input-available":
1336
- callbacks.onToolCallComplete?.(parsed.toolCallId, parsed.toolName, parsed.input);
1337
- break;
1338
- case "tool-output-available":
1339
- callbacks.onToolResult?.(parsed.toolCallId, parsed.output);
1340
- break;
1341
- case "finish-step":
1342
- callbacks.onStepFinish?.(parsed);
1343
- break;
1344
- case "finish":
1345
- callbacks.onFinish?.(parsed);
1346
- break;
1347
- case "error":
1348
- case "tool-output-error":
1349
- callbacks.onError?.(parsed.errorText || parsed.error || "Unknown error", parsed);
1350
- break;
1351
- case "start":
1352
- case "text-start":
1353
- case "text-end":
1354
- case "start-step":
1355
- case "reasoning-start":
1356
- case "reasoning-delta":
1357
- case "reasoning-end":
1358
- case "source-url":
1359
- case "source-document":
1360
- case "file":
1361
- case "abort":
1362
- break;
1363
- default:
1364
- if (type.startsWith("data-")) ; else {
1365
- console.log("[DataStreamParser] unhandled UI message stream type:", type);
1506
+ };
1507
+ const bubbleBridge = {
1508
+ open: () => {
1509
+ },
1510
+ scheduleDismiss: () => {
1511
+ },
1512
+ scrollToBottom: () => {
1366
1513
  }
1367
- break;
1514
+ };
1515
+ const endpoint = `/sime/proxy/organizations/${aiChatbotX.organizationId()}/agents/${props.agentId}/stream-invoke`;
1516
+ const agent = useAgentInvoke({
1517
+ endpoint,
1518
+ appToken: aiChatbotX.appToken(),
1519
+ projectId: props.projectId,
1520
+ aiChatbotX,
1521
+ tts: noopTts,
1522
+ bubble: {
1523
+ open: () => bubbleBridge.open(),
1524
+ scheduleDismiss: () => bubbleBridge.scheduleDismiss(),
1525
+ scrollToBottom: () => bubbleBridge.scrollToBottom()
1526
+ }
1527
+ });
1528
+ const bubble = useBubble({
1529
+ dismissDelay: props.bubbleDismissDelay ?? 8e3,
1530
+ isInvoking: agent.isInvoking,
1531
+ bubbleSize: props.bubbleSize
1532
+ });
1533
+ bubbleBridge.open = bubble.open;
1534
+ bubbleBridge.scheduleDismiss = bubble.scheduleDismiss;
1535
+ bubbleBridge.scrollToBottom = bubble.scrollToBottom;
1536
+ const { show: showBubble, style: bubbleStyle, stackRef: bubbleStackRef } = bubble;
1537
+ const handleSubmit = () => {
1538
+ const text = inputText.value.trim();
1539
+ if (!text || agent.isInvoking.value) return;
1540
+ inputText.value = "";
1541
+ agent.invoke(text);
1542
+ };
1543
+ const { isInvoking, currentTextContent, currentToolParts, executingTools, hasAnyContent, toolDisplayName } = agent;
1544
+ vue.onBeforeUnmount(() => {
1545
+ bubble.destroy();
1546
+ agent.abort();
1547
+ });
1548
+ return (_ctx, _cache) => {
1549
+ return vue.openBlock(), vue.createElementBlock("div", {
1550
+ class: "command-test",
1551
+ "data-theme": currentTheme$1
1552
+ }, [
1553
+ vue.createVNode(vue.Transition, { name: "bubble-fade" }, {
1554
+ default: vue.withCtx(() => [
1555
+ vue.unref(showBubble) ? (vue.openBlock(), vue.createElementBlock("div", {
1556
+ key: 0,
1557
+ class: "bubble-stack",
1558
+ ref_key: "bubbleStackRef",
1559
+ ref: bubbleStackRef,
1560
+ style: vue.normalizeStyle(vue.unref(bubbleStyle))
1561
+ }, [
1562
+ vue.createElementVNode("div", _hoisted_1$1, [
1563
+ vue.unref(currentToolParts).length > 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2$1, [
1564
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(currentToolParts), (toolPart) => {
1565
+ return vue.openBlock(), vue.createElementBlock("div", {
1566
+ key: toolPart.toolCallId,
1567
+ class: vue.normalizeClass(["tool-step", {
1568
+ "tool-step--loading": toolPart.state === "partial-call" || toolPart.state === "call",
1569
+ "tool-step--done": toolPart.state === "result",
1570
+ "tool-step--error": toolPart.state === "error",
1571
+ "tool-step--executing": vue.unref(executingTools).has(toolPart.toolCallId)
1572
+ }])
1573
+ }, [
1574
+ vue.createElementVNode("span", _hoisted_3$1, [
1575
+ toolPart.state === "partial-call" || toolPart.state === "call" ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_4$1, [..._cache[1] || (_cache[1] = [
1576
+ vue.createElementVNode("circle", {
1577
+ cx: "12",
1578
+ cy: "12",
1579
+ r: "10",
1580
+ stroke: "currentColor",
1581
+ "stroke-width": "2.5",
1582
+ "stroke-linecap": "round",
1583
+ "stroke-dasharray": "31.4 31.4"
1584
+ }, null, -1)
1585
+ ])])) : toolPart.state === "result" ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_5$1, [..._cache[2] || (_cache[2] = [
1586
+ vue.createElementVNode("path", {
1587
+ d: "M20 6L9 17l-5-5",
1588
+ stroke: "currentColor",
1589
+ "stroke-width": "2.5",
1590
+ "stroke-linecap": "round",
1591
+ "stroke-linejoin": "round"
1592
+ }, null, -1)
1593
+ ])])) : toolPart.state === "error" ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_6$1, [..._cache[3] || (_cache[3] = [
1594
+ vue.createElementVNode("circle", {
1595
+ cx: "12",
1596
+ cy: "12",
1597
+ r: "10",
1598
+ stroke: "currentColor",
1599
+ "stroke-width": "2"
1600
+ }, null, -1),
1601
+ vue.createElementVNode("path", {
1602
+ d: "M15 9l-6 6M9 9l6 6",
1603
+ stroke: "currentColor",
1604
+ "stroke-width": "2",
1605
+ "stroke-linecap": "round"
1606
+ }, null, -1)
1607
+ ])])) : vue.createCommentVNode("", true)
1608
+ ]),
1609
+ vue.createElementVNode("span", _hoisted_7$1, vue.toDisplayString(vue.unref(toolDisplayName)(toolPart.toolName)), 1),
1610
+ vue.unref(executingTools).has(toolPart.toolCallId) ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_8$1, "命令执行中")) : vue.createCommentVNode("", true)
1611
+ ], 2);
1612
+ }), 128))
1613
+ ])) : vue.createCommentVNode("", true),
1614
+ vue.unref(isInvoking) && !vue.unref(hasAnyContent) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9$1, [..._cache[4] || (_cache[4] = [
1615
+ vue.createElementVNode("span", null, null, -1),
1616
+ vue.createElementVNode("span", null, null, -1),
1617
+ vue.createElementVNode("span", null, null, -1)
1618
+ ])])) : vue.createCommentVNode("", true),
1619
+ vue.unref(currentTextContent) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_10$1, vue.toDisplayString(vue.unref(currentTextContent)), 1)) : vue.createCommentVNode("", true)
1620
+ ])
1621
+ ], 4)) : vue.createCommentVNode("", true)
1622
+ ]),
1623
+ _: 1
1624
+ }),
1625
+ vue.createElementVNode("div", _hoisted_11$1, [
1626
+ vue.withDirectives(vue.createElementVNode("input", {
1627
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => inputText.value = $event),
1628
+ type: "text",
1629
+ class: "input-field",
1630
+ placeholder: "输入指令...",
1631
+ disabled: vue.unref(isInvoking),
1632
+ onKeydown: vue.withKeys(handleSubmit, ["enter"])
1633
+ }, null, 40, _hoisted_12$1), [
1634
+ [vue.vModelText, inputText.value]
1635
+ ]),
1636
+ vue.createElementVNode("button", {
1637
+ class: "submit-btn",
1638
+ disabled: vue.unref(isInvoking) || !inputText.value.trim(),
1639
+ onClick: handleSubmit
1640
+ }, [
1641
+ vue.unref(isInvoking) ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_14, [..._cache[5] || (_cache[5] = [
1642
+ vue.createElementVNode("circle", {
1643
+ cx: "12",
1644
+ cy: "12",
1645
+ r: "10",
1646
+ stroke: "currentColor",
1647
+ "stroke-width": "2.5",
1648
+ "stroke-linecap": "round",
1649
+ "stroke-dasharray": "31.4 31.4"
1650
+ }, null, -1)
1651
+ ])])) : (vue.openBlock(), vue.createElementBlock("svg", _hoisted_15, [..._cache[6] || (_cache[6] = [
1652
+ vue.createElementVNode("path", {
1653
+ d: "M22 2L11 13",
1654
+ stroke: "currentColor",
1655
+ "stroke-width": "2",
1656
+ "stroke-linecap": "round",
1657
+ "stroke-linejoin": "round"
1658
+ }, null, -1),
1659
+ vue.createElementVNode("path", {
1660
+ d: "M22 2L15 22l-4-9-9-4 20-7z",
1661
+ stroke: "currentColor",
1662
+ "stroke-width": "2",
1663
+ "stroke-linecap": "round",
1664
+ "stroke-linejoin": "round"
1665
+ }, null, -1)
1666
+ ])]))
1667
+ ], 8, _hoisted_13$1)
1668
+ ])
1669
+ ]);
1670
+ };
1368
1671
  }
1369
- }
1370
- function parseLegacyProtocolLine(line, callbacks) {
1371
- if (!line || !DATA_STREAM_LINE_RE.test(line)) return;
1372
- const code = line[0];
1373
- const rawValue = line.slice(2);
1374
- let value;
1375
- try {
1376
- value = JSON.parse(rawValue);
1377
- } catch {
1378
- value = rawValue;
1672
+ });
1673
+
1674
+ const commandTest = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-5c7468c4"]]);
1675
+
1676
+ class CommandManager {
1677
+ commands = /* @__PURE__ */ new Map();
1678
+ debug;
1679
+ constructor(options = {}) {
1680
+ this.debug = options.debug ?? false;
1379
1681
  }
1380
- switch (code) {
1381
- case "0":
1382
- callbacks.onTextDelta?.(value);
1383
- break;
1384
- case "9":
1385
- callbacks.onToolCallStart?.(value.toolCallId, value.toolName);
1386
- break;
1387
- case "b":
1388
- callbacks.onToolCallDelta?.(value.toolCallId, value.argsTextDelta);
1389
- break;
1390
- case "c":
1391
- callbacks.onToolCallComplete?.(value.toolCallId, value.toolName, value.args);
1392
- break;
1393
- case "a":
1394
- callbacks.onToolResult?.(value.toolCallId, value.result);
1395
- break;
1396
- case "e":
1397
- callbacks.onStepFinish?.(value);
1398
- break;
1399
- case "d":
1400
- callbacks.onFinish?.(value);
1401
- break;
1402
- case "3":
1403
- callbacks.onError?.(value);
1404
- break;
1682
+ registerCommand(command) {
1683
+ this.commands.set(command.name, command);
1684
+ this.log("注册命令", `${command.name}: ${command.description}`);
1405
1685
  }
1406
- }
1407
- async function readDataStream(response, callbacks) {
1408
- if (!response.body) return;
1409
- const reader = response.body.getReader();
1410
- const decoder = new TextDecoder();
1411
- let buffer = "";
1412
- let format = null;
1413
- while (true) {
1414
- const { value, done } = await reader.read();
1415
- if (done) break;
1416
- const chunk = decoder.decode(value, { stream: true });
1417
- buffer += chunk;
1418
- if (format === null && buffer.trim().length > 0) {
1419
- format = detectFormat(buffer);
1420
- console.log("[DataStreamParser] detected format:", format, "| first 200 chars:", buffer.slice(0, 200));
1421
- }
1422
- if (format === "plain-text") {
1423
- const text = buffer;
1424
- buffer = "";
1425
- if (text) callbacks.onTextDelta?.(text);
1426
- continue;
1427
- }
1428
- if (format === "ui-message-stream") {
1429
- while (true) {
1430
- const eventEnd = buffer.indexOf("\n\n");
1431
- if (eventEnd === -1) break;
1432
- const eventBlock = buffer.slice(0, eventEnd);
1433
- buffer = buffer.slice(eventEnd + 2);
1434
- const dataLines = eventBlock.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
1435
- for (const dataLine of dataLines) {
1436
- processUIMessageStreamEvent(dataLine, callbacks);
1437
- }
1438
- }
1439
- continue;
1686
+ unregisterCommand(name) {
1687
+ const deleted = this.commands.delete(name);
1688
+ if (deleted) {
1689
+ this.log("命令已注销", name);
1440
1690
  }
1441
- if (format === "data-stream") {
1442
- const isSSEWrapped = buffer.trimStart().startsWith("data:");
1443
- if (isSSEWrapped) {
1444
- while (true) {
1445
- const eventEnd = buffer.indexOf("\n\n");
1446
- if (eventEnd === -1) break;
1447
- const eventBlock = buffer.slice(0, eventEnd);
1448
- buffer = buffer.slice(eventEnd + 2);
1449
- const dataLines = eventBlock.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
1450
- for (const dl of dataLines) {
1451
- const t = dl.trim();
1452
- if (!t || t === "[DONE]") {
1453
- if (t === "[DONE]") callbacks.onFinish?.({});
1454
- continue;
1455
- }
1456
- parseLegacyProtocolLine(t, callbacks);
1457
- }
1458
- }
1459
- } else {
1460
- while (true) {
1461
- const newlineIdx = buffer.indexOf("\n");
1462
- if (newlineIdx === -1) break;
1463
- const line = buffer.slice(0, newlineIdx).trim();
1464
- buffer = buffer.slice(newlineIdx + 1);
1465
- if (line) parseLegacyProtocolLine(line, callbacks);
1466
- }
1467
- }
1468
- continue;
1691
+ }
1692
+ async executeCommand(command, args = []) {
1693
+ const commandDef = this.commands.get(command);
1694
+ if (!commandDef) {
1695
+ throw new Error(`命令 "${command}" 未找到`);
1469
1696
  }
1697
+ this.log("执行命令", command, args);
1698
+ return await commandDef.handler(...args);
1470
1699
  }
1471
- const tail = decoder.decode();
1472
- if (tail) buffer += tail;
1473
- if (buffer.trim()) {
1474
- if (format === "plain-text") {
1475
- callbacks.onTextDelta?.(buffer);
1476
- } else if (format === "ui-message-stream") {
1477
- const dataLines = buffer.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
1478
- for (const dl of dataLines) {
1479
- processUIMessageStreamEvent(dl, callbacks);
1480
- }
1481
- } else if (format === "data-stream") {
1482
- parseLegacyProtocolLine(buffer.trim(), callbacks);
1700
+ getCommands() {
1701
+ return Array.from(this.commands.values()).map((cmd) => ({
1702
+ name: cmd.name,
1703
+ description: cmd.description,
1704
+ parameters: cmd.parameters
1705
+ }));
1706
+ }
1707
+ hasCommand(name) {
1708
+ return this.commands.has(name);
1709
+ }
1710
+ clear() {
1711
+ this.commands.clear();
1712
+ this.log("", "所有命令已清空");
1713
+ }
1714
+ log(prefix, msg, ...args) {
1715
+ (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
1716
+ hour: "2-digit",
1717
+ minute: "2-digit",
1718
+ second: "2-digit"
1719
+ });
1720
+ console.log(
1721
+ `%c ${prefix}`,
1722
+ "background:#7c3aed;color:white;padding:2px 6px;border-radius:3px 0 0 3px;font-weight:bold;",
1723
+ `${msg}`
1724
+ );
1725
+ if (args.length > 0) {
1726
+ console.log(...args);
1483
1727
  }
1484
1728
  }
1485
- callbacks.onFinish?.({});
1486
1729
  }
1487
- async function parseDataStreamToMessage(response, onUpdate) {
1488
- let textContent = "";
1489
- const parts = [];
1490
- const toolCalls = /* @__PURE__ */ new Map();
1491
- const ensureTextPart = () => {
1492
- for (let i = parts.length - 1; i >= 0; i--) {
1493
- if (parts[i].type === "text") {
1494
- return parts[i];
1730
+
1731
+ const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
1732
+ __name: "sime-provider",
1733
+ props: {
1734
+ appToken: {},
1735
+ organizationId: {}
1736
+ },
1737
+ setup(__props) {
1738
+ const props = __props;
1739
+ const commandManager = vue.shallowRef(new CommandManager({ debug: false }));
1740
+ const startListeningRef = vue.shallowRef(async () => {
1741
+ });
1742
+ const stopListeningRef = vue.shallowRef(async () => {
1743
+ });
1744
+ const stopBroadcastRef = vue.shallowRef(async () => {
1745
+ });
1746
+ vue.provide(AiChatbotXKey, {
1747
+ appToken: () => props.appToken,
1748
+ organizationId: () => props.organizationId,
1749
+ startListening: () => startListeningRef.value(),
1750
+ stopListening: () => stopListeningRef.value(),
1751
+ stopBroadcast: () => stopBroadcastRef.value(),
1752
+ registerVoiceMethods: (methods) => {
1753
+ if (methods.stopBroadcast) stopBroadcastRef.value = methods.stopBroadcast;
1754
+ if (methods.start) startListeningRef.value = methods.start;
1755
+ if (methods.stop) stopListeningRef.value = methods.stop;
1756
+ },
1757
+ getCommads: async () => commandManager.value.getCommands(),
1758
+ registerCommand: (cmd) => {
1759
+ commandManager.value.registerCommand(cmd);
1760
+ },
1761
+ unregisterCommand: (name) => {
1762
+ commandManager.value.unregisterCommand(name);
1763
+ },
1764
+ async executeCommand(commandName, args = []) {
1765
+ return await commandManager.value.executeCommand(commandName, args);
1766
+ }
1767
+ });
1768
+ return (_ctx, _cache) => {
1769
+ return vue.renderSlot(_ctx.$slots, "default");
1770
+ };
1771
+ }
1772
+ });
1773
+
1774
+ function useTTS(getVoiceConfig) {
1775
+ const isSpeaking = vue.ref(false);
1776
+ const hasPendingAudio = vue.ref(false);
1777
+ let instance = null;
1778
+ let initPromise = null;
1779
+ let audioCtx = null;
1780
+ let sentenceBuffer = "";
1781
+ const sentenceDelimiters = /[。!?;\n.!?;]/;
1782
+ 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();
1783
+ const warmUpAudio = () => {
1784
+ if (!audioCtx || audioCtx.state === "closed") {
1785
+ try {
1786
+ audioCtx = new AudioContext();
1787
+ } catch {
1788
+ return;
1495
1789
  }
1496
1790
  }
1497
- const textPart = { type: "text", text: "" };
1498
- parts.push(textPart);
1499
- return textPart;
1500
- };
1501
- const findToolPartIndex = (toolCallId) => {
1502
- return parts.findIndex((p) => (p.type === "tool-call" || p.type === "tool-result") && p.toolCallId === toolCallId);
1503
- };
1504
- const emitUpdate = () => {
1505
- onUpdate({ textContent, parts: [...parts], toolCalls: new Map(toolCalls) });
1791
+ if (audioCtx.state === "suspended") {
1792
+ audioCtx.resume();
1793
+ }
1506
1794
  };
1507
- await readDataStream(response, {
1508
- onTextDelta(text) {
1509
- textContent += text;
1510
- const textPart = ensureTextPart();
1511
- textPart.text = textContent;
1512
- emitUpdate();
1513
- },
1514
- onToolCallStart(toolCallId, toolName) {
1515
- const tracker = {
1516
- toolCallId,
1517
- toolName,
1518
- argsText: "",
1519
- args: void 0,
1520
- state: "partial-call"
1521
- };
1522
- toolCalls.set(toolCallId, tracker);
1523
- const part = {
1524
- type: "tool-call",
1525
- toolCallId,
1526
- toolName,
1527
- args: void 0,
1528
- state: "partial-call"
1529
- };
1530
- parts.push(part);
1531
- emitUpdate();
1532
- },
1533
- onToolCallDelta(toolCallId, argsTextDelta) {
1534
- const tracker = toolCalls.get(toolCallId);
1535
- if (tracker) {
1536
- tracker.argsText += argsTextDelta;
1537
- try {
1538
- tracker.args = JSON.parse(tracker.argsText);
1539
- } catch {
1540
- }
1541
- const idx = findToolPartIndex(toolCallId);
1542
- if (idx !== -1 && parts[idx].type === "tool-call") {
1543
- parts[idx].args = tracker.args;
1544
- }
1545
- emitUpdate();
1546
- }
1547
- },
1548
- onToolCallComplete(toolCallId, toolName, args) {
1549
- const tracker = toolCalls.get(toolCallId);
1550
- if (tracker) {
1551
- tracker.state = "call";
1552
- tracker.args = typeof args === "string" ? safeJsonParse(args) : args;
1553
- } else {
1554
- toolCalls.set(toolCallId, {
1555
- toolCallId,
1556
- toolName,
1557
- argsText: typeof args === "string" ? args : JSON.stringify(args),
1558
- args: typeof args === "string" ? safeJsonParse(args) : args,
1559
- state: "call"
1560
- });
1561
- }
1562
- const idx = findToolPartIndex(toolCallId);
1563
- if (idx !== -1) {
1564
- parts[idx].state = "call";
1565
- parts[idx].toolName = toolName;
1566
- parts[idx].args = toolCalls.get(toolCallId).args;
1567
- } else {
1568
- parts.push({
1569
- type: "tool-call",
1570
- toolCallId,
1571
- toolName,
1572
- args: toolCalls.get(toolCallId).args,
1573
- state: "call"
1574
- });
1575
- }
1576
- emitUpdate();
1577
- },
1578
- onToolResult(toolCallId, result) {
1579
- const tracker = toolCalls.get(toolCallId);
1580
- if (tracker) {
1581
- tracker.result = result;
1582
- tracker.state = "result";
1583
- }
1584
- const idx = findToolPartIndex(toolCallId);
1585
- if (idx !== -1) {
1586
- const existing = parts[idx];
1587
- const resultPart = {
1588
- type: "tool-result",
1589
- toolCallId,
1590
- toolName: existing.toolName,
1591
- args: existing.args,
1592
- result,
1593
- state: "result"
1594
- };
1595
- parts[idx] = resultPart;
1596
- } else {
1597
- parts.push({
1598
- type: "tool-result",
1599
- toolCallId,
1600
- toolName: tracker?.toolName || "unknown",
1601
- args: tracker?.args,
1602
- result,
1603
- state: "result"
1604
- });
1605
- }
1606
- emitUpdate();
1607
- },
1608
- onError(error, data) {
1609
- const toolCallId = data?.toolCallId;
1610
- if (toolCallId) {
1611
- toolCalls.delete(toolCallId);
1612
- const idx = findToolPartIndex(toolCallId);
1613
- if (idx !== -1) {
1614
- parts.splice(idx, 1);
1615
- emitUpdate();
1616
- }
1617
- }
1618
- console.error("[DataStreamParser] stream error:", error);
1619
- },
1620
- onStepFinish(_data) {
1621
- emitUpdate();
1622
- },
1623
- onFinish(_data) {
1624
- emitUpdate();
1795
+ let onQueueEmptyCb = null;
1796
+ const ensureInstance = async () => {
1797
+ if (instance) return instance;
1798
+ if (initPromise) return initPromise;
1799
+ const vc = getVoiceConfig();
1800
+ if (!vc || !vc.apiSecret) {
1801
+ console.warn("[TTS] 缺少 voiceConfig 或 apiSecret,语音播报已禁用");
1802
+ return null;
1625
1803
  }
1626
- });
1627
- return { textContent, parts, toolCalls };
1628
- }
1629
- function safeJsonParse(str) {
1630
- try {
1631
- return JSON.parse(str);
1632
- } catch {
1633
- return str;
1634
- }
1635
- }
1636
-
1637
- const toolDisplayNames = {
1638
- generateReport: "生成报告",
1639
- searchKnowledge: "知识库检索",
1640
- resolveInstanceTargets: "解析实例目标",
1641
- getHistoryMetrics: "历史数据查询",
1642
- getRealtimeMetrics: "实时数据查询",
1643
- queryBitableData: "多维表格查询",
1644
- searchUser: "搜索用户",
1645
- createBitableRecord: "创建表格记录",
1646
- timeTool: "时间工具",
1647
- loadSkill: "加载技能",
1648
- executeCommand: "执行命令",
1649
- dataAnalyzer: "数据分析",
1650
- dataPredictor: "数据预测"
1651
- };
1652
- function useAgentInvoke(options) {
1653
- const { aiChatbotX, tts, bubble } = options;
1654
- const sessionTimeoutMs = options.sessionTimeoutMs ?? 12e4;
1655
- const maxHistoryTurns = options.maxHistoryTurns ?? 10;
1656
- const isInvoking = vue.ref(false);
1657
- const currentTextContent = vue.ref("");
1658
- const currentToolParts = vue.ref([]);
1659
- const executingTools = vue.ref(/* @__PURE__ */ new Set());
1660
- const conversationHistory = vue.ref([]);
1661
- let lastInteractionTime = 0;
1662
- const checkSessionTimeout = () => {
1663
- if (lastInteractionTime > 0 && Date.now() - lastInteractionTime > sessionTimeoutMs) {
1664
- conversationHistory.value = [];
1804
+ initPromise = (async () => {
1805
+ try {
1806
+ const tts = new webVoiceKit.SpeechSynthesizerStandalone({
1807
+ appId: vc.appId,
1808
+ apiKey: vc.ttsApiKey || vc.apiKey,
1809
+ apiSecret: vc.apiSecret,
1810
+ websocketUrl: vc.ttsWebsocketUrl || "wss://tts-api.xfyun.cn/v2/tts",
1811
+ vcn: vc.ttsVcn || "xiaoyan",
1812
+ speed: vc.speed || 55,
1813
+ volume: vc.volume || 90,
1814
+ pitch: vc.pitch || 50,
1815
+ aue: "raw",
1816
+ auf: "audio/L16;rate=16000",
1817
+ tte: "UTF8",
1818
+ autoPlay: true
1819
+ });
1820
+ tts.onStart(() => {
1821
+ isSpeaking.value = true;
1822
+ });
1823
+ tts.onEnd(() => {
1824
+ });
1825
+ tts.onQueueEmpty(() => {
1826
+ isSpeaking.value = false;
1827
+ hasPendingAudio.value = false;
1828
+ onQueueEmptyCb?.();
1829
+ });
1830
+ tts.onError((err) => {
1831
+ console.error("[TTS] Error:", err);
1832
+ isSpeaking.value = false;
1833
+ });
1834
+ if (audioCtx && audioCtx.state === "running") {
1835
+ tts.audioContext = audioCtx;
1836
+ tts.gainNode = audioCtx.createGain();
1837
+ tts.gainNode.connect(audioCtx.destination);
1838
+ }
1839
+ instance = tts;
1840
+ initPromise = null;
1841
+ return tts;
1842
+ } catch (err) {
1843
+ console.error("[TTS] 初始化失败:", err);
1844
+ initPromise = null;
1845
+ return null;
1846
+ }
1847
+ })();
1848
+ return initPromise;
1849
+ };
1850
+ const speak = async (text) => {
1851
+ const clean = stripMarkdown(text);
1852
+ if (!clean.trim()) return;
1853
+ hasPendingAudio.value = true;
1854
+ const tts = await ensureInstance();
1855
+ if (!tts) return;
1856
+ try {
1857
+ tts.speak(clean);
1858
+ } catch (err) {
1859
+ console.error("[TTS] speak 失败:", err);
1665
1860
  }
1666
1861
  };
1667
- const appendToHistory = (role, content) => {
1668
- conversationHistory.value.push({ role, content });
1669
- const maxLen = maxHistoryTurns * 2;
1670
- if (conversationHistory.value.length > maxLen) {
1671
- conversationHistory.value = conversationHistory.value.slice(-maxLen);
1862
+ const feed = (delta) => {
1863
+ sentenceBuffer += delta;
1864
+ while (true) {
1865
+ const match = sentenceBuffer.match(sentenceDelimiters);
1866
+ if (!match || match.index === void 0) break;
1867
+ const sentence = sentenceBuffer.slice(0, match.index + 1).trim();
1868
+ sentenceBuffer = sentenceBuffer.slice(match.index + 1);
1869
+ if (sentence.length > 0) speak(sentence);
1672
1870
  }
1673
1871
  };
1674
- const clearHistory = () => {
1675
- conversationHistory.value = [];
1872
+ const flush = () => {
1873
+ const remaining = sentenceBuffer.trim();
1874
+ sentenceBuffer = "";
1875
+ if (remaining.length > 0) speak(remaining);
1676
1876
  };
1677
- let abortController = null;
1678
- const hasAnyContent = vue.computed(() => {
1679
- return !!(currentTextContent.value || currentToolParts.value.length > 0);
1680
- });
1681
- const toolDisplayName = (name) => toolDisplayNames[name] || name;
1682
- const resetState = () => {
1683
- currentTextContent.value = "";
1684
- currentToolParts.value = [];
1685
- executingTools.value = /* @__PURE__ */ new Set();
1877
+ const stop = () => {
1878
+ sentenceBuffer = "";
1879
+ isSpeaking.value = false;
1880
+ hasPendingAudio.value = false;
1881
+ if (instance) {
1882
+ try {
1883
+ instance.stop();
1884
+ } catch {
1885
+ }
1886
+ }
1686
1887
  };
1687
- const extractExecutableCommands = (payload) => {
1688
- if (!payload || typeof payload !== "object") return [];
1689
- const commands = payload.commands;
1690
- if (!Array.isArray(commands) || commands.length === 0) return [];
1691
- return commands.filter((cmd) => cmd && typeof cmd === "object" && typeof cmd.name === "string" && cmd.name.trim()).map((cmd) => ({
1692
- name: cmd.name,
1693
- args: Array.isArray(cmd.args) ? cmd.args : []
1694
- }));
1888
+ const setOnQueueEmpty = (cb) => {
1889
+ onQueueEmptyCb = cb;
1695
1890
  };
1696
- const buildCommandDefinitionMap = (commands) => {
1697
- return new Map(commands.map((command) => [command.name, command]));
1891
+ const destroy = () => {
1892
+ stop();
1893
+ if (instance) {
1894
+ try {
1895
+ instance.destroy();
1896
+ } catch {
1897
+ }
1898
+ instance = null;
1899
+ }
1900
+ if (audioCtx) {
1901
+ try {
1902
+ audioCtx.close();
1903
+ } catch {
1904
+ }
1905
+ audioCtx = null;
1906
+ }
1698
1907
  };
1699
- const toExecutableCommand = (toolName, payload, commandDefinitions) => {
1700
- const commandDefinition = commandDefinitions.get(toolName);
1701
- if (!commandDefinition) {
1702
- return null;
1908
+ return {
1909
+ isSpeaking,
1910
+ hasPendingAudio,
1911
+ warmUpAudio,
1912
+ speak,
1913
+ feed,
1914
+ flush,
1915
+ stop,
1916
+ destroy,
1917
+ setOnQueueEmpty
1918
+ };
1919
+ }
1920
+
1921
+ const ensureMicrophonePermission = async () => {
1922
+ if (typeof navigator === "undefined" || typeof window === "undefined") {
1923
+ console.log("当前环境不支持麦克风访问");
1924
+ return false;
1925
+ }
1926
+ if (!navigator.mediaDevices?.getUserMedia || !navigator.mediaDevices?.enumerateDevices) {
1927
+ console.log("当前环境不支持麦克风访问");
1928
+ return false;
1929
+ }
1930
+ try {
1931
+ const devices = await navigator.mediaDevices.enumerateDevices();
1932
+ const audioInputDevices = devices.filter((device) => device.kind === "audioinput");
1933
+ if (audioInputDevices.length === 0) {
1934
+ console.log("未检测到麦克风设备,请连接麦克风后重试。");
1935
+ return false;
1703
1936
  }
1704
- const parameters = commandDefinition.parameters || [];
1705
- if (Array.isArray(payload)) {
1706
- return {
1707
- name: toolName,
1708
- args: payload
1709
- };
1937
+ if ("permissions" in navigator && navigator.permissions?.query) {
1938
+ try {
1939
+ const status = await navigator.permissions.query({ name: "microphone" });
1940
+ if (status.state === "denied") {
1941
+ console.log("麦克风权限被禁用,请在浏览器设置中开启。");
1942
+ return false;
1943
+ }
1944
+ } catch (e) {
1945
+ console.warn("Permission query not supported:", e);
1946
+ }
1710
1947
  }
1711
- if (!payload || typeof payload !== "object") {
1712
- return {
1713
- name: toolName,
1714
- args: []
1715
- };
1948
+ let stream = null;
1949
+ try {
1950
+ stream = await navigator.mediaDevices.getUserMedia({
1951
+ audio: {
1952
+ echoCancellation: true,
1953
+ noiseSuppression: true,
1954
+ autoGainControl: true
1955
+ }
1956
+ });
1957
+ const audioTracks = stream.getAudioTracks();
1958
+ if (audioTracks.length === 0) {
1959
+ console.log("无法获取麦克风音频轨道。");
1960
+ return false;
1961
+ }
1962
+ const activeTrack = audioTracks[0];
1963
+ if (!activeTrack.enabled || activeTrack.readyState !== "live") {
1964
+ console.log("麦克风设备不可用,请检查设备连接。");
1965
+ return false;
1966
+ }
1967
+ return true;
1968
+ } finally {
1969
+ if (stream) {
1970
+ stream.getTracks().forEach((track) => track.stop());
1971
+ }
1972
+ }
1973
+ } catch (error) {
1974
+ console.error("Microphone permission check failed", error);
1975
+ if (error.name === "NotFoundError" || error.name === "DevicesNotFoundError") {
1976
+ console.log("未检测到麦克风设备,请连接麦克风后重试。");
1977
+ } else if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") {
1978
+ console.log("麦克风权限被拒绝,请在浏览器设置中允许访问。");
1979
+ } else if (error.name === "NotReadableError" || error.name === "TrackStartError") {
1980
+ console.log("麦克风被其他应用占用或无法访问。");
1981
+ } else {
1982
+ console.log("无法访问麦克风,请检查设备连接和浏览器权限。");
1983
+ }
1984
+ return false;
1985
+ }
1986
+ };
1987
+
1988
+ function useVoiceRecognition(options) {
1989
+ const voiceStatus = vue.ref("standby");
1990
+ const isTranscribing = vue.ref(false);
1991
+ const isInitializing = vue.ref(false);
1992
+ const transcriptionText = vue.ref("");
1993
+ const wakeAnimating = vue.ref(false);
1994
+ let detector = null;
1995
+ let transcriber = null;
1996
+ const initTranscriber = () => {
1997
+ if (transcriber) return;
1998
+ const vc = options.getVoiceConfig();
1999
+ if (!vc || !vc.appId || !vc.apiKey || !vc.websocketUrl) {
2000
+ console.error("[VoiceRecognition] 缺少 voiceConfig,无法初始化转写器");
2001
+ return;
2002
+ }
2003
+ transcriber = new webVoiceKit.SpeechTranscriberStandalone({
2004
+ appId: vc.appId,
2005
+ apiKey: vc.apiKey,
2006
+ websocketUrl: vc.websocketUrl,
2007
+ autoStop: {
2008
+ enabled: true,
2009
+ silenceTimeoutMs: 2e3,
2010
+ noSpeechTimeoutMs: 5e3,
2011
+ maxDurationMs: 45e3
2012
+ }
2013
+ });
2014
+ transcriber.onResult((result) => {
2015
+ transcriptionText.value = result.transcript || "";
2016
+ });
2017
+ transcriber.onAutoStop(async () => {
2018
+ const finalText = transcriptionText.value;
2019
+ await stopTranscribing();
2020
+ transcriptionText.value = "";
2021
+ if (finalText.trim()) {
2022
+ options.onTranscriptionDone?.(finalText);
2023
+ }
2024
+ });
2025
+ transcriber.onError((error) => {
2026
+ console.error("[VoiceRecognition] 转写错误:", error);
2027
+ stopTranscribing();
2028
+ transcriptionText.value = "";
2029
+ });
2030
+ };
2031
+ const startTranscribing = async () => {
2032
+ if (isTranscribing.value) return;
2033
+ if (!transcriber) initTranscriber();
2034
+ if (!transcriber) return;
2035
+ try {
2036
+ await transcriber.start();
2037
+ isTranscribing.value = true;
2038
+ transcriptionText.value = "";
2039
+ } catch (error) {
2040
+ console.error("[VoiceRecognition] 启动转写失败:", error);
1716
2041
  }
1717
- const payloadRecord = payload;
1718
- return {
1719
- name: toolName,
1720
- args: parameters.map((parameter) => payloadRecord[parameter.name])
1721
- };
1722
2042
  };
1723
- const resolveExecutableCommands = (toolName, payload, commandDefinitions) => {
1724
- const extractedCommands = extractExecutableCommands(payload);
1725
- if (extractedCommands.length > 0) {
1726
- return extractedCommands;
2043
+ const stopTranscribing = async () => {
2044
+ if (!transcriber || !transcriber.isActive()) {
2045
+ isTranscribing.value = false;
2046
+ return;
2047
+ }
2048
+ try {
2049
+ await transcriber.stop();
2050
+ } catch (error) {
2051
+ console.error("[VoiceRecognition] 停止转写失败:", error);
2052
+ } finally {
2053
+ isTranscribing.value = false;
1727
2054
  }
1728
- const directCommand = toExecutableCommand(toolName, payload, commandDefinitions);
1729
- return directCommand ? [directCommand] : [];
1730
2055
  };
1731
- const executeHostCommands = async (toolCallId, toolName, payload, commandDefinitions) => {
1732
- const commands = resolveExecutableCommands(toolName, payload, commandDefinitions);
1733
- if (commands.length === 0) return false;
2056
+ const initDetector = () => {
2057
+ if (detector || isInitializing.value) return;
2058
+ if (!options.modelPath) {
2059
+ console.error("[VoiceRecognition] 未传入 modelPath,无法启用唤醒词");
2060
+ return;
2061
+ }
2062
+ isInitializing.value = true;
1734
2063
  try {
1735
- executingTools.value = /* @__PURE__ */ new Set([...executingTools.value, toolCallId]);
1736
- for (const cmd of commands) {
1737
- try {
1738
- await aiChatbotX.executeCommand(cmd.name, cmd.args);
1739
- } catch (cmdErr) {
1740
- console.error(`[AgentInvoke] 执行命令 ${cmd.name} 失败:`, cmdErr);
2064
+ detector = new webVoiceKit.WakeWordDetectorStandalone({
2065
+ modelPath: options.modelPath,
2066
+ sampleRate: 16e3,
2067
+ usePartial: true,
2068
+ autoReset: {
2069
+ enabled: true,
2070
+ resetDelayMs: 4e3
1741
2071
  }
1742
- }
1743
- return true;
2072
+ });
2073
+ detector.setWakeWords(options.wakeWords || ["你好", "您好"]);
2074
+ detector.onWake(async () => {
2075
+ wakeAnimating.value = true;
2076
+ options.onWake?.();
2077
+ await startTranscribing();
2078
+ setTimeout(() => {
2079
+ wakeAnimating.value = false;
2080
+ }, 1200);
2081
+ });
2082
+ detector.onError((error) => {
2083
+ console.error("[VoiceRecognition] 唤醒监听错误:", error);
2084
+ voiceStatus.value = "standby";
2085
+ stopTranscribing();
2086
+ });
1744
2087
  } finally {
1745
- const next = new Set(executingTools.value);
1746
- next.delete(toolCallId);
1747
- executingTools.value = next;
2088
+ isInitializing.value = false;
1748
2089
  }
1749
2090
  };
1750
- const parseAssistantText = (payload) => {
1751
- if (!payload) return "";
1752
- if (typeof payload === "string") return payload;
1753
- if (typeof payload === "object") {
1754
- const data = payload;
1755
- const directText = data.output || data.answer || data.message || data.result;
1756
- if (typeof directText === "string" && directText.trim()) return directText;
1757
- if (data.data && typeof data.data === "object") {
1758
- const nested = data.data;
1759
- const nestedText = nested.output || nested.answer || nested.message || nested.result;
1760
- if (typeof nestedText === "string" && nestedText.trim()) return nestedText;
1761
- }
1762
- return JSON.stringify(payload);
2091
+ const toggleVoiceMode = async (targetState) => {
2092
+ const permission = await ensureMicrophonePermission();
2093
+ if (!permission || isInitializing.value) return;
2094
+ if (!detector) {
2095
+ initDetector();
2096
+ if (!detector) return;
1763
2097
  }
1764
- return String(payload);
1765
- };
1766
- const invoke = async (question) => {
1767
- const content = question.trim();
1768
- if (!content) return;
1769
- abort();
1770
- checkSessionTimeout();
1771
- resetState();
1772
- tts.stop();
1773
- isInvoking.value = true;
1774
- bubble.open();
1775
- let prevTextLength = 0;
1776
- const processedToolResults = /* @__PURE__ */ new Set();
1777
- const processingToolResults = /* @__PURE__ */ new Set();
1778
- abortController = new AbortController();
1779
- const commands = await aiChatbotX.getCommads();
1780
- const commandDefinitions = buildCommandDefinitionMap(commands);
1781
- conversationHistory.value.length > 0 ? [...conversationHistory.value] : void 0;
2098
+ const isListening = voiceStatus.value === "listening";
2099
+ const shouldStart = targetState !== void 0 ? targetState : !isListening;
2100
+ if (isListening === shouldStart) return;
1782
2101
  try {
1783
- const response = await fetch(options.endpoint, {
1784
- method: "POST",
1785
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.appToken || ""}` },
1786
- body: JSON.stringify({
1787
- input: content,
1788
- projectId: options.projectId || "",
1789
- commands: commands.length > 0 ? commands : void 0
1790
- // messages: historyToSend,
1791
- }),
1792
- signal: abortController.signal
1793
- });
1794
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
1795
- const contentType = response.headers.get("content-type") || "";
1796
- const isJsonResponse = contentType.includes("application/json");
1797
- if (isJsonResponse) {
1798
- const data = await response.json();
1799
- const reply = parseAssistantText(data) || "已收到,但没有返回可展示的文本内容。";
1800
- currentTextContent.value = reply;
1801
- tts.speak(reply);
1802
- appendToHistory("user", content);
1803
- appendToHistory("assistant", reply);
1804
- if (data.toolResults && Array.isArray(data.toolResults)) {
1805
- for (const tr of data.toolResults) {
1806
- const toolPart = {
1807
- type: "tool-result",
1808
- toolCallId: `invoke-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1809
- toolName: tr.toolName,
1810
- args: tr.args,
1811
- result: tr.result,
1812
- state: "result"
1813
- };
1814
- currentToolParts.value = [...currentToolParts.value, toolPart];
1815
- if (commandDefinitions.has(tr.toolName)) {
1816
- void executeHostCommands(toolPart.toolCallId, tr.toolName, tr.result, commandDefinitions);
1817
- }
1818
- }
1819
- }
2102
+ if (shouldStart) {
2103
+ await detector.start();
2104
+ voiceStatus.value = "listening";
1820
2105
  } else {
1821
- await parseDataStreamToMessage(response, (result) => {
1822
- currentTextContent.value = result.textContent;
1823
- if (result.textContent.length > prevTextLength) {
1824
- const delta = result.textContent.slice(prevTextLength);
1825
- prevTextLength = result.textContent.length;
1826
- tts.feed(delta);
1827
- }
1828
- const toolParts = result.parts.filter(
1829
- (p) => p.type === "tool-call" || p.type === "tool-result"
1830
- );
1831
- currentToolParts.value = toolParts;
1832
- for (const part of toolParts) {
1833
- if (commandDefinitions.has(part.toolName) && !processedToolResults.has(part.toolCallId) && !processingToolResults.has(part.toolCallId)) {
1834
- if (part.type === "tool-call" && part.state === "call" && part.args) {
1835
- processingToolResults.add(part.toolCallId);
1836
- void executeHostCommands(part.toolCallId, part.toolName, part.args, commandDefinitions).then(
1837
- (executed) => {
1838
- if (executed) {
1839
- processedToolResults.add(part.toolCallId);
1840
- }
1841
- processingToolResults.delete(part.toolCallId);
1842
- }
1843
- );
1844
- } else if (part.type === "tool-result" && part.result) {
1845
- processingToolResults.add(part.toolCallId);
1846
- void executeHostCommands(part.toolCallId, part.toolName, part.result, commandDefinitions).then(
1847
- (executed) => {
1848
- if (executed) {
1849
- processedToolResults.add(part.toolCallId);
1850
- }
1851
- processingToolResults.delete(part.toolCallId);
1852
- }
1853
- );
1854
- }
1855
- }
1856
- }
1857
- bubble.scrollToBottom();
1858
- });
1859
- tts.flush();
1860
- const assistantReply = currentTextContent.value.trim();
1861
- appendToHistory("user", content);
1862
- if (assistantReply) {
1863
- appendToHistory("assistant", assistantReply);
1864
- }
1865
- if (!assistantReply && currentToolParts.value.length === 0) {
1866
- currentTextContent.value = "已收到,但没有返回可展示的文本内容。";
1867
- }
2106
+ await detector.stop();
2107
+ voiceStatus.value = "standby";
2108
+ transcriptionText.value = "";
2109
+ await stopTranscribing();
1868
2110
  }
1869
2111
  } catch (error) {
1870
- if (error.name === "AbortError") {
1871
- return;
1872
- }
1873
- console.error("[AgentInvoke] invoke failed:", error);
1874
- tts.stop();
1875
- currentTextContent.value = "请求失败,请检查服务地址或稍后重试。";
1876
- } finally {
1877
- isInvoking.value = false;
1878
- abortController = null;
1879
- lastInteractionTime = Date.now();
1880
- bubble.scheduleDismiss();
2112
+ console.error("[VoiceRecognition] 监听切换失败:", error);
2113
+ voiceStatus.value = "standby";
1881
2114
  }
1882
2115
  };
1883
- const abort = () => {
1884
- if (abortController) {
1885
- abortController.abort();
1886
- abortController = null;
2116
+ const abortTranscription = async () => {
2117
+ transcriptionText.value = "";
2118
+ await stopTranscribing();
2119
+ };
2120
+ const destroy = async () => {
2121
+ if (detector) {
2122
+ try {
2123
+ if (detector.isActive()) await detector.stop();
2124
+ } catch {
2125
+ }
2126
+ detector = null;
2127
+ }
2128
+ if (transcriber) {
2129
+ try {
2130
+ if (transcriber.isActive()) await transcriber.stop();
2131
+ } catch {
2132
+ }
2133
+ transcriber = null;
1887
2134
  }
1888
- tts.stop();
1889
- isInvoking.value = false;
1890
2135
  };
1891
2136
  return {
1892
- isInvoking,
1893
- currentTextContent,
1894
- currentToolParts,
1895
- executingTools,
1896
- hasAnyContent,
1897
- conversationHistory,
1898
- toolDisplayName,
1899
- invoke,
1900
- abort,
1901
- resetState,
1902
- clearHistory
2137
+ voiceStatus,
2138
+ isTranscribing,
2139
+ isInitializing,
2140
+ transcriptionText,
2141
+ wakeAnimating,
2142
+ startTranscribing,
2143
+ stopTranscribing,
2144
+ abortTranscription,
2145
+ toggleVoiceMode,
2146
+ destroy
1903
2147
  };
1904
2148
  }
1905
2149
 
@@ -1961,6 +2205,7 @@
1961
2205
  wakeWords: {},
1962
2206
  wakeResponses: {},
1963
2207
  modelPath: {},
2208
+ agentId: {},
1964
2209
  projectId: {},
1965
2210
  voiceConfig: {},
1966
2211
  bubbleSize: {},
@@ -1974,6 +2219,7 @@
1974
2219
  return null;
1975
2220
  };
1976
2221
  const wakeResponses = vue.computed(() => props.wakeResponses || ["在呢"]);
2222
+ const agentId = vue.computed(() => props.agentId);
1977
2223
  const tts = useTTS(getVoiceConfig);
1978
2224
  const bubbleBridge = {
1979
2225
  open: () => {
@@ -1983,7 +2229,7 @@
1983
2229
  scrollToBottom: () => {
1984
2230
  }
1985
2231
  };
1986
- const endpoint = `/sime/proxy/agent/${aiChatbotX.agentId()}/stream-invoke`;
2232
+ const endpoint = `/sime/proxy/organizations/${aiChatbotX.organizationId()}/agents/${agentId.value}/stream-invoke`;
1987
2233
  const agent = useAgentInvoke({
1988
2234
  endpoint,
1989
2235
  appToken: aiChatbotX.appToken(),
@@ -2180,7 +2426,7 @@
2180
2426
  }
2181
2427
  });
2182
2428
 
2183
- const voiceAssistant = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-76f6b7ef"]]);
2429
+ const voiceAssistant = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-59d72f34"]]);
2184
2430
 
2185
2431
  var clientCommandKey = /* @__PURE__ */ ((clientCommandKey2) => {
2186
2432
  clientCommandKey2["SET_THEME"] = "SiMeAgent_setTheme";
@@ -2195,6 +2441,7 @@
2195
2441
 
2196
2442
  exports.AgentChatTransport = AgentChatTransport;
2197
2443
  exports.AiChat = aiChat;
2444
+ exports.AiChatbotCommandTest = commandTest;
2198
2445
  exports.AiChatbotProvider = _sfc_main$1;
2199
2446
  exports.AiChatbotVoiceAssistant = voiceAssistant;
2200
2447
  exports.AiChatbotXKey = AiChatbotXKey;