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