@fecp/designer 5.6.18 → 5.6.21

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.
Files changed (61) hide show
  1. package/es/designer/package.json.mjs +1 -1
  2. package/es/designer.css +141 -63
  3. package/es/node_modules/@vant/use/dist/index.esm.mjs +1 -1
  4. package/es/node_modules/d3-sankey/node_modules/d3-path/src/path.mjs +73 -0
  5. package/es/node_modules/d3-sankey/node_modules/d3-shape/src/array.mjs +4 -0
  6. package/es/node_modules/d3-sankey/node_modules/d3-shape/src/constant.mjs +8 -0
  7. package/es/node_modules/d3-sankey/node_modules/d3-shape/src/link/index.mjs +45 -0
  8. package/es/node_modules/d3-sankey/node_modules/d3-shape/src/point.mjs +10 -0
  9. package/es/node_modules/d3-sankey/src/sankeyLinkHorizontal.mjs +1 -1
  10. package/es/node_modules/vant/es/toast/function-call.mjs +1 -1
  11. package/es/packages/mobile/src/assets/images/ai2.png.mjs +4 -0
  12. package/es/packages/mobile/src/components/ai/AiChat/AiChat.vue.mjs +155 -23
  13. package/es/packages/mobile/src/components/ai/ChatInput/ChatInput.vue.mjs +4 -6
  14. package/es/packages/mobile/src/components/ai/ChatPanel/ChatPanel.vue.mjs +71 -11
  15. package/es/packages/mobile/src/components/ai/MarkdownRenderer/MarkdownRenderer.vue.mjs +37 -3
  16. package/es/packages/mobile/src/components/custom/appDetail/AppDetail.vue.mjs +2 -2
  17. package/es/packages/mobile/src/components/form/uploader/Uploader.vue.mjs +1 -2
  18. package/es/packages/mobile/src/composables/useAI/adapters/h5.mjs +256 -0
  19. package/es/packages/mobile/src/composables/useAI/adapters/index.mjs +3 -3
  20. package/es/packages/mobile/src/composables/useAI/useChat.mjs +10 -3
  21. package/es/packages/mobile/src/composables/usePageEvents.mjs +1 -1
  22. package/es/packages/mobile/src/composables/useVoiceTouch.mjs +2 -0
  23. package/es/packages/mobile/src/index.vue.mjs +20 -8
  24. package/es/packages/mobile/src/utils/datasource.mjs +1 -1
  25. package/es/packages/vue/src/components/bus/approvalHistory/ApproveIdea.vue.mjs +2 -2
  26. package/es/packages/vue/src/components/dialog/DialogRenderer.vue2.mjs +0 -1
  27. package/es/packages/vue/src/components/forms/number/Number.vue.mjs +0 -1
  28. package/es/packages/vue/src/components/forms/text/Text.vue.mjs +0 -1
  29. package/es/packages/vue/src/components/table/TableColumn.vue.mjs +0 -1
  30. package/es/packages/vue/src/composables/usePageEvents.mjs +0 -1
  31. package/es/packages/vue/src/utils/datasource.mjs +0 -20
  32. package/lib/designer/package.json.js +1 -1
  33. package/lib/designer.css +141 -63
  34. package/lib/node_modules/d3-sankey/node_modules/d3-path/src/path.js +73 -0
  35. package/lib/node_modules/d3-sankey/node_modules/d3-shape/src/array.js +4 -0
  36. package/lib/node_modules/d3-sankey/node_modules/d3-shape/src/constant.js +8 -0
  37. package/lib/node_modules/d3-sankey/node_modules/d3-shape/src/link/index.js +45 -0
  38. package/lib/node_modules/d3-sankey/node_modules/d3-shape/src/point.js +10 -0
  39. package/lib/node_modules/d3-sankey/src/sankeyLinkHorizontal.js +1 -1
  40. package/lib/packages/mobile/src/assets/images/ai2.png.js +4 -0
  41. package/lib/packages/mobile/src/components/ai/AiChat/AiChat.vue.js +154 -22
  42. package/lib/packages/mobile/src/components/ai/ChatInput/ChatInput.vue.js +4 -6
  43. package/lib/packages/mobile/src/components/ai/ChatPanel/ChatPanel.vue.js +73 -13
  44. package/lib/packages/mobile/src/components/ai/MarkdownRenderer/MarkdownRenderer.vue.js +37 -3
  45. package/lib/packages/mobile/src/components/custom/appDetail/AppDetail.vue.js +2 -2
  46. package/lib/packages/mobile/src/components/form/uploader/Uploader.vue.js +1 -2
  47. package/lib/packages/mobile/src/composables/useAI/adapters/h5.js +256 -0
  48. package/lib/packages/mobile/src/composables/useAI/adapters/index.js +3 -3
  49. package/lib/packages/mobile/src/composables/useAI/useChat.js +10 -3
  50. package/lib/packages/mobile/src/composables/usePageEvents.js +1 -1
  51. package/lib/packages/mobile/src/composables/useVoiceTouch.js +2 -0
  52. package/lib/packages/mobile/src/index.vue.js +19 -7
  53. package/lib/packages/mobile/src/utils/datasource.js +1 -1
  54. package/lib/packages/vue/src/components/bus/approvalHistory/ApproveIdea.vue.js +2 -2
  55. package/lib/packages/vue/src/components/dialog/DialogRenderer.vue2.js +0 -1
  56. package/lib/packages/vue/src/components/forms/number/Number.vue.js +0 -1
  57. package/lib/packages/vue/src/components/forms/text/Text.vue.js +0 -1
  58. package/lib/packages/vue/src/components/table/TableColumn.vue.js +0 -1
  59. package/lib/packages/vue/src/composables/usePageEvents.js +0 -1
  60. package/lib/packages/vue/src/utils/datasource.js +2 -22
  61. package/package.json +1 -1
@@ -1,17 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
3
  ;/* empty css */
4
+ ;/* empty css */
4
5
  ;/* empty css */
5
6
  ;/* empty css */
6
- ;/* empty css */
7
7
  ;/* empty css */
8
8
  const Vue = require("vue");
9
9
  const index$1 = require("../MarkdownRenderer/index.js");
10
10
  ;/* empty css */
11
11
  const _pluginVue_exportHelper = require("../../../../../../_virtual/_plugin-vue_export-helper.js");
12
12
  const index = require("../../../../../../node_modules/vant/es/empty/index.js");
13
- const index$2 = require("../../../../../../node_modules/vant/es/loading/index.js");
14
- const index$3 = require("../../../../../../node_modules/vant/es/icon/index.js");
13
+ const functionCall = require("../../../../../../node_modules/vant/es/toast/function-call.js");
14
+ require("../../../../../../node_modules/vant/es/toast/index.js");
15
+ const index$2 = require("../../../../../../node_modules/vant/es/icon/index.js");
16
+ const index$3 = require("../../../../../../node_modules/vant/es/loading/index.js");
15
17
  const _hoisted_1 = {
16
18
  key: 0,
17
19
  class: "chat-panel__empty"
@@ -39,13 +41,23 @@ const _hoisted_7 = {
39
41
  };
40
42
  const _hoisted_8 = {
41
43
  key: 2,
44
+ class: "chat-panel__actions"
45
+ };
46
+ const _hoisted_9 = ["onClick"];
47
+ const _hoisted_10 = ["onClick"];
48
+ const _hoisted_11 = {
49
+ key: 1,
50
+ class: "iconfont icon-mobileyinliang"
51
+ };
52
+ const _hoisted_12 = {
53
+ key: 3,
42
54
  class: "chat-panel__status"
43
55
  };
44
- const _hoisted_9 = {
56
+ const _hoisted_13 = {
45
57
  key: 1,
46
58
  class: "chat-panel__streaming-dot"
47
59
  };
48
- const _hoisted_10 = ["onClick"];
60
+ const _hoisted_14 = ["onClick"];
49
61
  const _sfc_main = {
50
62
  __name: "ChatPanel",
51
63
  props: {
@@ -56,9 +68,11 @@ const _sfc_main = {
56
68
  /** 是否自动滚动到底部 */
57
69
  autoScroll: { type: Boolean, default: true },
58
70
  /** 两条消息间隔多久显示时间戳(ms),默认 5 分钟 */
59
- timeGap: { type: Number, default: 5 * 60 * 1e3 }
71
+ timeGap: { type: Number, default: 5 * 60 * 1e3 },
72
+ /** 正在朗读的消息 id(用于高亮"停止"态) */
73
+ speakingId: { type: String, default: "" }
60
74
  },
61
- emits: ["retry", "scroll-to-bottom", "navigate"],
75
+ emits: ["retry", "scroll-to-bottom", "navigate", "speak"],
62
76
  setup(__props, { expose: __expose, emit: __emit }) {
63
77
  const props = __props;
64
78
  const emit = __emit;
@@ -81,6 +95,34 @@ const _sfc_main = {
81
95
  const D = d.getDate();
82
96
  return `${M}/${D} ${hh}:${mm}`;
83
97
  }
98
+ async function onCopy(msg) {
99
+ var _a;
100
+ try {
101
+ const text = msg.content || "";
102
+ if ((_a = navigator.clipboard) == null ? void 0 : _a.writeText) {
103
+ await navigator.clipboard.writeText(text);
104
+ } else {
105
+ const ta = document.createElement("textarea");
106
+ ta.value = text;
107
+ ta.style.position = "fixed";
108
+ ta.style.opacity = "0";
109
+ document.body.appendChild(ta);
110
+ ta.select();
111
+ document.execCommand("copy");
112
+ document.body.removeChild(ta);
113
+ }
114
+ msg.__copied = true;
115
+ functionCall.showToast("已复制");
116
+ setTimeout(() => {
117
+ msg.__copied = false;
118
+ }, 1500);
119
+ } catch (e) {
120
+ functionCall.showToast("复制失败");
121
+ }
122
+ }
123
+ function onSpeak(msg) {
124
+ emit("speak", msg);
125
+ }
84
126
  function scrollToBottom() {
85
127
  if (!panelRef.value) return;
86
128
  const el = panelRef.value;
@@ -137,8 +179,8 @@ const _sfc_main = {
137
179
  __expose({ scrollToBottom });
138
180
  return (_ctx, _cache) => {
139
181
  const _component_van_empty = index.Empty;
140
- const _component_van_loading = index$2.Loading;
141
- const _component_van_icon = index$3.Icon;
182
+ const _component_van_icon = index$2.Icon;
183
+ const _component_van_loading = index$3.Loading;
142
184
  return Vue.openBlock(), Vue.createElementBlock("div", {
143
185
  class: "chat-panel",
144
186
  ref_key: "panelRef",
@@ -185,11 +227,29 @@ const _sfc_main = {
185
227
  onNavigate: _cache[0] || (_cache[0] = ($event) => emit("navigate", $event))
186
228
  }, null, 8, ["content", "is-streaming"])
187
229
  ])) : (Vue.openBlock(), Vue.createElementBlock("div", _hoisted_7, Vue.toDisplayString(msg.content), 1)),
188
- ["sending", "streaming", "error"].includes(msg.status) ? (Vue.openBlock(), Vue.createElementBlock("div", _hoisted_8, [
230
+ msg.role === "assistant" && msg.status === "done" && msg.content && !msg.isWelcome ? (Vue.openBlock(), Vue.createElementBlock("div", _hoisted_8, [
231
+ Vue.createElementVNode("span", {
232
+ class: "chat-panel__action",
233
+ onClick: ($event) => onCopy(msg)
234
+ }, _cache[3] || (_cache[3] = [
235
+ Vue.createElementVNode("i", { class: "iconfont icon-mobilefuzhi" }, null, -1)
236
+ ]), 8, _hoisted_9),
237
+ Vue.createElementVNode("span", {
238
+ class: Vue.normalizeClass(["chat-panel__action", { "chat-panel__action--active": __props.speakingId === msg.id }]),
239
+ onClick: ($event) => onSpeak(msg)
240
+ }, [
241
+ __props.speakingId === msg.id ? (Vue.openBlock(), Vue.createBlock(_component_van_icon, {
242
+ key: 0,
243
+ name: "pause",
244
+ size: "14"
245
+ })) : (Vue.openBlock(), Vue.createElementBlock("i", _hoisted_11))
246
+ ], 10, _hoisted_10)
247
+ ])) : Vue.createCommentVNode("", true),
248
+ ["sending", "streaming", "error"].includes(msg.status) ? (Vue.openBlock(), Vue.createElementBlock("div", _hoisted_12, [
189
249
  msg.status === "sending" ? (Vue.openBlock(), Vue.createBlock(_component_van_loading, {
190
250
  key: 0,
191
251
  size: "14"
192
- })) : msg.status === "streaming" ? (Vue.openBlock(), Vue.createElementBlock("span", _hoisted_9, _cache[3] || (_cache[3] = [
252
+ })) : msg.status === "streaming" ? (Vue.openBlock(), Vue.createElementBlock("span", _hoisted_13, _cache[4] || (_cache[4] = [
193
253
  Vue.createElementVNode("i", null, null, -1),
194
254
  Vue.createElementVNode("i", null, null, -1),
195
255
  Vue.createElementVNode("i", null, null, -1)
@@ -204,7 +264,7 @@ const _sfc_main = {
204
264
  size: "14"
205
265
  }),
206
266
  Vue.createTextVNode(" " + Vue.toDisplayString(msg.error || "发送失败"), 1)
207
- ], 8, _hoisted_10)) : Vue.createCommentVNode("", true)
267
+ ], 8, _hoisted_14)) : Vue.createCommentVNode("", true)
208
268
  ])) : Vue.createCommentVNode("", true)
209
269
  ], 2))
210
270
  ], 2);
@@ -228,5 +288,5 @@ const _sfc_main = {
228
288
  };
229
289
  }
230
290
  };
231
- const _ChatPanel = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-e842ef15"]]);
291
+ const _ChatPanel = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-c2dc87cf"]]);
232
292
  exports.default = _ChatPanel;
@@ -3,6 +3,7 @@ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toString
3
3
  const Vue = require("vue");
4
4
  const index = require("../../../../../../node_modules/markdown-it/lib/index.js");
5
5
  const index$1 = require("../../../../../../_virtual/index.js");
6
+ const purify_es = require("../../../../../../node_modules/dompurify/dist/purify.es.js");
6
7
  ;/* empty css */
7
8
  ;/* empty css */
8
9
  const _pluginVue_exportHelper = require("../../../../../../_virtual/_plugin-vue_export-helper.js");
@@ -20,7 +21,7 @@ const _sfc_main = {
20
21
  const props = __props;
21
22
  const md = new index.default({
22
23
  html: false,
23
- // 安全:不允许原始 HTML
24
+ // 安全:不允许原始 HTML(标题样式在渲染后字符串层面转换,见 enhanceTitles)
24
25
  linkify: true,
25
26
  // 自动将 URL 转为链接
26
27
  breaks: true,
@@ -45,16 +46,40 @@ const _sfc_main = {
45
46
  });
46
47
  const emit = __emit;
47
48
  const contentRef = Vue.ref(null);
49
+ let _purifyHookInstalled = false;
50
+ function installPurifyHook() {
51
+ if (_purifyHookInstalled) return;
52
+ _purifyHookInstalled = true;
53
+ purify_es.default.addHook("uponSanitizeAttribute", (node, data) => {
54
+ if (data && data.attrName === "href" && data.attrValue && data.attrValue.startsWith("navigateToPage:")) {
55
+ data.keepAttr = true;
56
+ }
57
+ });
58
+ }
59
+ installPurifyHook();
48
60
  const renderedHtml = Vue.computed(() => {
49
61
  if (!props.content) return "";
50
62
  try {
51
- return md.render(props.content);
63
+ const html = md.render(props.content);
64
+ return purify_es.default.sanitize(html, {
65
+ // 保留 navigateToPage: 自定义跳转协议链接(markdown 生成的跳转链接不受影响)
66
+ ALLOWED_URI_REGEXP: /^(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|navigateToPage):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$)/i
67
+ });
52
68
  } catch (e) {
53
69
  console.warn("[MarkdownRenderer] 渲染失败:", e.message);
54
70
  return md.utils.escapeHtml(props.content);
55
71
  }
56
72
  });
57
73
  function onClick(e) {
74
+ const jumpBlock = e.target.closest("blockquote.jump-block");
75
+ if (jumpBlock) {
76
+ const link2 = jumpBlock.querySelector('a[href^="navigateToPage:"]');
77
+ if (link2) {
78
+ e.preventDefault();
79
+ emit("navigate", link2.getAttribute("href"));
80
+ return;
81
+ }
82
+ }
58
83
  const link = e.target.closest("a");
59
84
  if (!link) return;
60
85
  const href = link.getAttribute("href");
@@ -62,6 +87,14 @@ const _sfc_main = {
62
87
  e.preventDefault();
63
88
  emit("navigate", href);
64
89
  }
90
+ function markJumpBlocks(el) {
91
+ if (!el) return;
92
+ el.querySelectorAll("blockquote").forEach((bq) => {
93
+ if (bq.querySelector('a[href^="navigateToPage:"]')) {
94
+ bq.classList.add("jump-block");
95
+ }
96
+ });
97
+ }
65
98
  let _mermaidInstance = null;
66
99
  let _mermaidLoaded = false;
67
100
  async function ensureMermaid() {
@@ -110,6 +143,7 @@ const _sfc_main = {
110
143
  () => {
111
144
  Vue.nextTick(() => {
112
145
  renderMermaidBlocks(contentRef.value);
146
+ markJumpBlocks(contentRef.value);
113
147
  });
114
148
  },
115
149
  { immediate: true }
@@ -125,5 +159,5 @@ const _sfc_main = {
125
159
  };
126
160
  }
127
161
  };
128
- const _MarkdownRenderer = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-52fb67bc"]]);
162
+ const _MarkdownRenderer = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-18a7f752"]]);
129
163
  exports.default = _MarkdownRenderer;
@@ -93,7 +93,7 @@ const _sfc_main = {
93
93
  }
94
94
  operationPageId.value = data.mobileAppPage;
95
95
  canMobileApproval.value = data.mobileApproval != "1";
96
- opinionTypeOptions.value = data.approvalBtn.filter(
96
+ opinionTypeOptions.value = (data.approvalBtn || []).filter(
97
97
  (item) => item.approveType != "C1"
98
98
  );
99
99
  expandingTabOptions.value = JSON.parse(data.mobileDetailTabs || "[]").map(
@@ -187,5 +187,5 @@ const _sfc_main = {
187
187
  };
188
188
  }
189
189
  };
190
- const _AppDetail = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-2edc3d85"]]);
190
+ const _AppDetail = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-87ab39f1"]]);
191
191
  exports.default = _AppDetail;
@@ -16,9 +16,8 @@ const dataSourceUtil = require("../../../utils/dataSourceUtil.js");
16
16
  ;/* empty css */
17
17
  const _pluginVue_exportHelper = require("../../../../../../_virtual/_plugin-vue_export-helper.js");
18
18
  const index = require("../../../../../../node_modules/vant/es/uploader/index.js");
19
- const functionCall = require("../../../../../../node_modules/vant/es/toast/function-call.js");
20
- require("../../../../../../node_modules/vant/es/toast/index.js");
21
19
  const index$1 = require("../../../../../../node_modules/vant/es/icon/index.js");
20
+ const functionCall = require("../../../../../../node_modules/vant/es/toast/function-call.js");
22
21
  const _hoisted_1 = {
23
22
  key: 0,
24
23
  class: "preview-cover van-ellipsis"
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ function checkGetUserMedia() {
4
+ return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
5
+ }
6
+ function checkMediaRecorder() {
7
+ return typeof MediaRecorder !== "undefined";
8
+ }
9
+ function getSupportedMimeType() {
10
+ const types = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg;codecs=opus"];
11
+ for (const type of types) {
12
+ if (MediaRecorder.isTypeSupported(type)) {
13
+ return type;
14
+ }
15
+ }
16
+ return "";
17
+ }
18
+ function createH5Adapter(sessionId, ctx) {
19
+ function createStreamRequest(options) {
20
+ const {
21
+ url,
22
+ method = "POST",
23
+ headers = {},
24
+ body = null,
25
+ onMessage,
26
+ onComplete,
27
+ onError,
28
+ timeout = 6e4
29
+ } = options;
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => {
32
+ controller.abort();
33
+ onError(new Error("请求超时"));
34
+ }, timeout);
35
+ (async () => {
36
+ var _a, _b, _c;
37
+ try {
38
+ const res = await fetch(url, {
39
+ method,
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ ...headers
43
+ },
44
+ body: body ? JSON.stringify(body) : null,
45
+ signal: controller.signal
46
+ });
47
+ if (!res.ok) {
48
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
49
+ }
50
+ const reader = res.body.getReader();
51
+ const decoder = new TextDecoder("utf-8");
52
+ let buffer = "";
53
+ while (true) {
54
+ const { done, value } = await reader.read();
55
+ if (done) break;
56
+ buffer += decoder.decode(value, { stream: true });
57
+ const lines = buffer.split("\n");
58
+ buffer = lines.pop() || "";
59
+ for (const line of lines) {
60
+ const trimmed = line.trim();
61
+ if (!trimmed || !trimmed.startsWith("data:")) continue;
62
+ const data = trimmed.slice(5).trim();
63
+ if (data === "[DONE]") {
64
+ clearTimeout(timer);
65
+ onComplete();
66
+ return;
67
+ }
68
+ try {
69
+ const parsed = JSON.parse(data);
70
+ const chunk = ((_c = (_b = (_a = parsed.choices) == null ? void 0 : _a[0]) == null ? void 0 : _b.delta) == null ? void 0 : _c.content) || parsed.content || parsed.text || "";
71
+ if (chunk) onMessage(chunk);
72
+ } catch {
73
+ onMessage(data);
74
+ }
75
+ }
76
+ }
77
+ clearTimeout(timer);
78
+ onComplete();
79
+ } catch (err) {
80
+ clearTimeout(timer);
81
+ if (err.name !== "AbortError") {
82
+ onError(err);
83
+ }
84
+ }
85
+ })();
86
+ return {
87
+ abort: () => {
88
+ clearTimeout(timer);
89
+ controller.abort();
90
+ }
91
+ };
92
+ }
93
+ let mediaStream = null;
94
+ let mediaRecorder = null;
95
+ let recordedChunks = [];
96
+ let isRecording = false;
97
+ let recordStartTime = 0;
98
+ let maxDurationTimer = null;
99
+ async function startRecord(options = {}) {
100
+ if (!checkGetUserMedia()) {
101
+ throw new Error("当前浏览器不支持麦克风录音,请使用 HTTPS 或 localhost 访问");
102
+ }
103
+ if (!checkMediaRecorder()) {
104
+ throw new Error("当前浏览器不支持 MediaRecorder,请升级浏览器或使用 Chrome/Edge");
105
+ }
106
+ if (isRecording) {
107
+ throw new Error("已在录制中");
108
+ }
109
+ const { maxDuration = 6e4 } = options;
110
+ try {
111
+ mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
112
+ const mimeType = getSupportedMimeType();
113
+ const recorderOptions = mimeType ? { mimeType } : {};
114
+ mediaRecorder = new MediaRecorder(mediaStream, recorderOptions);
115
+ recordedChunks = [];
116
+ mediaRecorder.ondataavailable = (event) => {
117
+ if (event.data && event.data.size > 0) {
118
+ recordedChunks.push(event.data);
119
+ }
120
+ };
121
+ mediaRecorder.onerror = (event) => {
122
+ console.error("[H5Adapter] MediaRecorder 错误:", event.error);
123
+ cleanupRecording();
124
+ };
125
+ mediaRecorder.start();
126
+ isRecording = true;
127
+ recordStartTime = Date.now();
128
+ if (maxDuration > 0) {
129
+ maxDurationTimer = setTimeout(() => {
130
+ if (isRecording) {
131
+ stopRecord().catch(() => {
132
+ });
133
+ }
134
+ }, maxDuration);
135
+ }
136
+ console.log("[H5Adapter] 开始录音");
137
+ } catch (err) {
138
+ cleanupRecording();
139
+ if (err.name === "NotAllowedError") {
140
+ throw new Error("麦克风权限被拒绝,请在浏览器设置中允许访问麦克风");
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+ async function stopRecord() {
146
+ if (!isRecording || !mediaRecorder) {
147
+ throw new Error("未在录制中");
148
+ }
149
+ return new Promise((resolve, reject) => {
150
+ mediaRecorder.onstop = () => {
151
+ const duration = Date.now() - recordStartTime;
152
+ const mimeType = mediaRecorder.mimeType || "audio/webm";
153
+ mimeType.includes("mp4") ? "mp4" : "webm";
154
+ const audioBlob = new Blob(recordedChunks, { type: mimeType });
155
+ cleanupRecording();
156
+ console.log(`[H5Adapter] 录音结束,时长 ${duration}ms,大小 ${(audioBlob.size / 1024).toFixed(1)}KB`);
157
+ resolve({ audioBlob, duration });
158
+ };
159
+ mediaRecorder.onerror = (event) => {
160
+ var _a;
161
+ cleanupRecording();
162
+ reject(new Error(`录制错误: ${((_a = event.error) == null ? void 0 : _a.message) || "未知错误"}`));
163
+ };
164
+ if (mediaRecorder.state === "recording") {
165
+ mediaRecorder.requestData();
166
+ }
167
+ mediaRecorder.stop();
168
+ });
169
+ }
170
+ function cancelRecord() {
171
+ if (mediaRecorder && mediaRecorder.state === "recording") {
172
+ mediaRecorder.onstop = null;
173
+ mediaRecorder.stop();
174
+ }
175
+ cleanupRecording();
176
+ console.log("[H5Adapter] 录音已取消");
177
+ }
178
+ function cleanupRecording() {
179
+ if (maxDurationTimer) {
180
+ clearTimeout(maxDurationTimer);
181
+ maxDurationTimer = null;
182
+ }
183
+ if (mediaStream) {
184
+ mediaStream.getTracks().forEach((track) => track.stop());
185
+ mediaStream = null;
186
+ }
187
+ mediaRecorder = null;
188
+ recordedChunks = [];
189
+ isRecording = false;
190
+ }
191
+ function getStream() {
192
+ return mediaStream;
193
+ }
194
+ async function voiceToText(audioBlob) {
195
+ if (!audioBlob || audioBlob.size === 0) {
196
+ throw new Error("音频数据为空");
197
+ }
198
+ if (!ctx.$http) {
199
+ throw new Error("H5 适配器需要传入 httpClient(如 ctx.$http)才能调用后端 ASR 接口");
200
+ }
201
+ const formData = new FormData();
202
+ const mimeType = audioBlob.type || "audio/webm";
203
+ const ext = mimeType.includes("mp4") ? "mp4" : "webm";
204
+ formData.append("file", audioBlob, `recording.${ext}`);
205
+ formData.append("sessionId", sessionId);
206
+ try {
207
+ const data = await ctx.$http.postForm(`${ctx.$envConfig.servers.ai}/speech/transcribe`, formData, false, true);
208
+ return data.text;
209
+ } catch (err) {
210
+ throw new Error(`语音转文字请求失败: ${err.message || "未知错误"}`);
211
+ }
212
+ }
213
+ function getCapabilities() {
214
+ var _a, _b;
215
+ return {
216
+ supportStreamRequest: true,
217
+ supportVoiceRecord: checkGetUserMedia() && checkMediaRecorder(),
218
+ supportVoiceToText: !!((_b = (_a = ctx == null ? void 0 : ctx.$envConfig) == null ? void 0 : _a.servers) == null ? void 0 : _b.base),
219
+ supportFileUpload: true
220
+ };
221
+ }
222
+ let status = "idle";
223
+ function init() {
224
+ if (!checkGetUserMedia()) {
225
+ console.warn("[H5Adapter] getUserMedia 不可用 — 请使用 HTTPS 或 localhost");
226
+ }
227
+ if (!checkMediaRecorder()) {
228
+ console.warn("[H5Adapter] MediaRecorder 不可用 — 请升级浏览器");
229
+ }
230
+ status = "ready";
231
+ console.log("[H5Adapter] 初始化完成");
232
+ }
233
+ function destroy() {
234
+ if (isRecording) {
235
+ cancelRecord();
236
+ }
237
+ status = "idle";
238
+ console.log("[H5Adapter] 已销毁");
239
+ }
240
+ return {
241
+ name: "h5",
242
+ get status() {
243
+ return status;
244
+ },
245
+ init,
246
+ destroy,
247
+ createStreamRequest,
248
+ startRecord,
249
+ stopRecord,
250
+ cancelRecord,
251
+ voiceToText,
252
+ getStream,
253
+ getCapabilities
254
+ };
255
+ }
256
+ exports.createH5Adapter = createH5Adapter;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const detector = require("./detector.js");
4
- const mock = require("./mock.js");
4
+ const h5 = require("./h5.js");
5
5
  const wecom = require("./wecom.js");
6
6
  const miniprogram = require("./miniprogram.js");
7
7
  let cachedAdapter = null;
@@ -19,7 +19,7 @@ function getAdapter(options = {}) {
19
19
  break;
20
20
  case detector.PlatformType.WEB:
21
21
  default:
22
- cachedAdapter = mock.createMockAdapter();
22
+ cachedAdapter = h5.createH5Adapter(options.sessionId, options.ctx);
23
23
  break;
24
24
  }
25
25
  return cachedAdapter;
@@ -27,7 +27,7 @@ function getAdapter(options = {}) {
27
27
  exports.PlatformType = detector.PlatformType;
28
28
  exports.detectPlatform = detector.detectPlatform;
29
29
  exports.detectPlatformAsync = detector.detectPlatformAsync;
30
- exports.createMockAdapter = mock.createMockAdapter;
30
+ exports.createH5Adapter = h5.createH5Adapter;
31
31
  exports.createWeComAdapter = wecom.createWeComAdapter;
32
32
  exports.createMiniProgramAdapter = miniprogram.createMiniProgramAdapter;
33
33
  exports.getAdapter = getAdapter;
@@ -118,10 +118,17 @@ function useChat(options = {}) {
118
118
  });
119
119
  }
120
120
  function stop() {
121
- if (streamController) {
122
- streamController.abort();
123
- finishStream(types.MessageStatus.CANCELLED, "用户取消");
121
+ if (!streamController) return;
122
+ streamController.abort();
123
+ const lastMsg = messages.value[messages.value.length - 1];
124
+ const isEmptyReply = lastMsg && lastMsg.role === types.MessageRole.ASSISTANT && lastMsg.status === types.MessageStatus.STREAMING && !lastMsg.content.trim();
125
+ if (isEmptyReply) {
126
+ messages.value.pop();
127
+ isStreaming.value = false;
128
+ streamController = null;
129
+ return;
124
130
  }
131
+ finishStream(types.MessageStatus.CANCELLED, "用户取消");
125
132
  }
126
133
  async function retry() {
127
134
  const lastMsg = messages.value[messages.value.length - 1];
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const Vue = require("vue");
4
4
  ;/* empty css */
5
- ;/* empty css */
6
5
  ;/* empty css */
7
6
  ;/* empty css */
7
+ ;/* empty css */
8
8
  require("../utils/eventBus.js");
9
9
  require("../utils/pageHistory.js");
10
10
  ;/* empty css */
@@ -26,6 +26,8 @@ function useVoiceTouch({ emit, voiceStatus, disabled }) {
26
26
  }
27
27
  }
28
28
  function onVoiceStart(e) {
29
+ window.__aiVoiceLog = window.__aiVoiceLog || [];
30
+ window.__aiVoiceLog.push(`[useVoiceTouch] onVoiceStart 触发 disabled=${disabled == null ? void 0 : disabled.value} _voiceActive=${_voiceActive}`);
29
31
  if ((disabled == null ? void 0 : disabled.value) || _voiceActive) return;
30
32
  e.preventDefault();
31
33
  _voiceActive = true;
@@ -17,6 +17,7 @@ require("../../../node_modules/element-plus/es/index.js");
17
17
  const Vue = require("vue");
18
18
  const page = require("./page.vue.js");
19
19
  const index$4 = require("./components/ai/AiChat/index.js");
20
+ const ai2 = require("./assets/images/ai2.png.js");
20
21
  const index$5 = require("./api/index.js");
21
22
  const eventBus = require("./utils/eventBus.js");
22
23
  const pageHistory = require("./utils/pageHistory.js");
@@ -32,6 +33,7 @@ const index$2 = require("../../../node_modules/vant/es/floating-bubble/index.js"
32
33
  const index$6 = require("../../../node_modules/element-plus/es/components/message/index.js");
33
34
  const index$1 = require("../../../node_modules/vant/es/empty/index.js");
34
35
  const index$3 = require("../../../node_modules/vant/es/popup/index.js");
36
+ const _hoisted_1 = ["src"];
35
37
  const _sfc_main = {
36
38
  __name: "index",
37
39
  props: {
@@ -171,8 +173,8 @@ const _sfc_main = {
171
173
  }
172
174
  const targetPage = common.findPageByKey(pageList.value, page2);
173
175
  showChatPopup.value = false;
174
- loadPage(targetPage.id);
175
176
  pageParams.value = params;
177
+ loadPage(targetPage.id);
176
178
  }
177
179
  Vue.provide("pageList", pageList);
178
180
  Vue.onMounted(async () => {
@@ -225,23 +227,33 @@ const _sfc_main = {
225
227
  ]),
226
228
  _: 1
227
229
  }, 512)) : Vue.createCommentVNode("", true),
228
- !Vue.unref(noTargetPage) ? (Vue.openBlock(), Vue.createBlock(_component_van_floating_bubble, {
230
+ Vue.unref(currentPage) ? (Vue.openBlock(), Vue.createBlock(_component_van_floating_bubble, {
229
231
  key: 1,
230
- style: { "z-index": "100" },
232
+ style: { "z-index": "100", "background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)" },
231
233
  axis: "xy",
232
- icon: "chat",
233
234
  magnetic: "x",
234
235
  gap: 12,
235
236
  offset: Vue.unref(offset),
236
237
  "onUpdate:offset": _cache[0] || (_cache[0] = ($event) => Vue.isRef(offset) ? offset.value = $event : null),
237
238
  onClick: openChatPopup
238
- }, null, 8, ["offset"])) : Vue.createCommentVNode("", true),
239
+ }, {
240
+ default: Vue.withCtx(() => [
241
+ Vue.createElementVNode("img", {
242
+ src: Vue.unref(ai2.default),
243
+ class: "ai-bubble-icon"
244
+ }, null, 8, _hoisted_1)
245
+ ]),
246
+ _: 1
247
+ }, 8, ["offset"])) : Vue.createCommentVNode("", true),
239
248
  Vue.createVNode(_component_van_popup, {
240
249
  teleport: "body",
241
250
  show: Vue.unref(showChatPopup),
242
251
  "onUpdate:show": _cache[2] || (_cache[2] = ($event) => Vue.isRef(showChatPopup) ? showChatPopup.value = $event : null),
243
252
  position: "right",
244
- style: { width: "100%", height: "100%" },
253
+ style: {
254
+ width: "100%",
255
+ height: "100%"
256
+ },
245
257
  "lock-scroll": true
246
258
  }, {
247
259
  default: Vue.withCtx(() => [
@@ -257,5 +269,5 @@ const _sfc_main = {
257
269
  };
258
270
  }
259
271
  };
260
- const MobileApp = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-32479e3c"]]);
272
+ const MobileApp = /* @__PURE__ */ _pluginVue_exportHelper.default(_sfc_main, [["__scopeId", "data-v-197d0cb3"]]);
261
273
  exports.default = MobileApp;