zj-plugin-intelligent 1.2.26-beta.2 → 1.2.26-beta.3
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.
|
@@ -1012,7 +1012,7 @@ eval("\n\nvar _interopRequireWildcard = __webpack_require__(/*! ./node_modules/@
|
|
|
1012
1012
|
/***/ (function(module, exports, __webpack_require__) {
|
|
1013
1013
|
|
|
1014
1014
|
"use strict";
|
|
1015
|
-
eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! ./node_modules/@babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\").default;\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/asyncToGenerator */ \"./node_modules/@babel/runtime/helpers/asyncToGenerator.js\"));\n\nvar _typeof2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/typeof */ \"./node_modules/@babel/runtime/helpers/typeof.js\"));\n\nvar _logo = _interopRequireDefault(__webpack_require__(/*! @/assets/logo.png */ \"./src/assets/logo.png\"));\n\nvar _markdownIt = _interopRequireDefault(__webpack_require__(/*! markdown-it */ \"./node_modules/markdown-it/index.mjs\"));\n\nvar _kbChat = __webpack_require__(/*! ./api/kbChat */ \"./src/lib/knowledgeBase/api/kbChat.js\");\n\nvar _knowledgeBase = __webpack_require__(/*! ./api/knowledgeBase */ \"./src/lib/knowledgeBase/api/knowledgeBase.js\");\n\nvar _native = __webpack_require__(/*! ./native.js */ \"./src/lib/knowledgeBase/native.js\");\n\nvar _request = __webpack_require__(/*! @lib/utils/request.js */ \"./src/lib/utils/request.js\");\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar AI_AVATAR_URL = 'https://qiguanbang.obs.cn-east-3.myhuaweicloud.com/production/image/AlAgent/doc_query.png'; // 与 ChatDetail 使用相同的 Markdown 配置\n\nvar md = new _markdownIt.default({\n breaks: true,\n html: false,\n linkify: true,\n typographer: true\n});\nmd.enable('table');\nvar _default = {\n name: 'KbChatMain',\n props: {\n companyId: {\n type: String,\n default: ''\n },\n userno: {\n type: String,\n default: ''\n },\n imAccount: {\n type: [String, Object],\n default: ''\n },\n userName: {\n type: String,\n default: ''\n },\n // 是否移动端:由父级(KnowledgeBaseMain)传入;未传时组件自身按窗口宽度判断\n isMobile: {\n type: Boolean,\n default: null\n }\n },\n data: function data() {\n // 首帧前计算是否移动端,避免 H5 先显示 PC 布局再切换的闪烁(与 AI 助理一致)\n // 宿主没有 Element UI 时(H5/平板横屏)一律走自绘 UI 分支,避免 el-* 未知元素报错\n var _isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n\n return {\n initDone: false,\n activeSessionId: '',\n sessionList: [],\n // 默认折叠侧边栏:避免新用户进入时先展开再合住的闪烁,\n // 加载出会话后再按规则决定是否展开\n sidebarCollapsed: true,\n messages: [],\n inputText: '',\n streaming: false,\n streamMsg: null,\n showToolInfo: false,\n contentCache: {},\n eventSource: null,\n streamRequestId: 0,\n suggestedQuestions: [],\n // 消息多选模式(豆包式):PC 由消息操作区「删除/多选」进入,移动端长按气泡弹出菜单进入\n selectMode: false,\n // 已选中的消息 id 列表(后端主键)\n selectedMsgIds: [],\n // 当前公司是否没有知识库文档(无资料时展示遮罩)\n noDocument: false,\n // 移动端状态(仿 AI 助理):showH5List=true 显示会话列表页,false 显示聊天详情页\n localIsMobile: _isMobile,\n showH5List: _isMobile,\n // 移动端会话「…」弹出的操作单\n mobileAction: null,\n // 移动端长按气泡弹出的消息操作单(复制 / 删除 / 多选)\n msgAction: null,\n // 移动端侧滑手势(参考 AI 助理):记录起手坐标与是否已触发\n _swipeStartX: 0,\n _swipeStartY: 0,\n _swipeHandled: false,\n _swipeFromLeftEdge: false,\n _swipeFromRightEdge: false,\n _swipeFromEdge: false\n };\n },\n computed: {\n /** 实际是否移动端:优先用父级 prop,未传则用自身窗口判断 */\n isMobileActive: function isMobileActive() {\n return this.isMobile !== null ? this.isMobile : this.localIsMobile;\n },\n aiAvatar: function aiAvatar() {\n return AI_AVATAR_URL;\n },\n userAvatar: function userAvatar() {\n var account = this.imAccount;\n console.log('KbChatMain - imAccount 原始值:', account); // 尝试解析字符串为对象\n\n if (typeof account === 'string') {\n try {\n account = JSON.parse(account);\n console.log('KbChatMain - imAccount 解析后:', account);\n } catch (e) {\n // 如果解析失败,检查是否是纯 URL 字符串\n if (account.trim() && (account.includes('http://') || account.includes('https://'))) {\n console.log('KbChatMain - imAccount 是 URL:', account.trim());\n return account.trim();\n }\n }\n } // 如果是对象,尝试多个可能的头像字段(优先使用 headImg)\n\n\n if (account && (0, _typeof2.default)(account) === 'object') {\n var avatarFields = ['headImg', 'headPortrait', 'avatar', 'avatarUrl', 'headImage', 'photoUrl', 'portrait', 'headUrl', 'userHeadUrl', 'userAvatar'];\n\n for (var _i = 0, _avatarFields = avatarFields; _i < _avatarFields.length; _i++) {\n var field = _avatarFields[_i];\n\n if (account[field] && String(account[field]).trim()) {\n console.log(\"KbChatMain - \\u627E\\u5230\\u5934\\u50CF\\u5B57\\u6BB5 \".concat(field, \":\"), account[field]);\n return String(account[field]).trim();\n }\n }\n }\n\n console.log('KbChatMain - 使用默认头像');\n return _logo.default;\n },\n // 是否有真实头像(区别于默认 fallback 图片)\n hasUserAvatar: function hasUserAvatar() {\n return this.userAvatar && this.userAvatar !== _logo.default;\n },\n // 无头像时的文字头像(与 zjkjAvatar 一致:取名字最后两个字)\n userAvatarText: function userAvatarText() {\n var name = this.userName || '';\n return name ? name.substring(name.length - 2) : '';\n },\n // 可删除的消息 id(仅后端已落库、带回 id 的消息可删)\n deletableMsgIds: function deletableMsgIds() {\n return this.messages.filter(function (m) {\n return m.id;\n }).map(function (m) {\n return m.id;\n });\n },\n // 是否已全选\n isAllSelected: function isAllSelected() {\n return this.deletableMsgIds.length > 0 && this.selectedMsgIds.length === this.deletableMsgIds.length;\n }\n },\n watch: {\n companyId: function companyId() {\n this.initDone = false;\n this.finishStream();\n this.loadSuggestedQuestions();\n this.checkKbDocuments();\n this.initSession();\n },\n userno: function userno() {\n this.initDone = false;\n this.finishStream();\n this.activeSessionId = '';\n this.messages = [];\n this.initSession();\n },\n // 移动端进入时默认先显示会话列表页(仿 AI 助理)\n isMobileActive: function isMobileActive(val) {\n if (val) {\n // 无资料时遮罩只在详情页可见(列表页 .kb-chat-main 被隐藏),故保持停在详情页\n this.showH5List = !this.noDocument;\n this.mobileAction = null;\n }\n }\n },\n mounted: function mounted() {\n var _this = this;\n\n // 独立使用(父级未传 isMobile)时自检窗口宽度\n this._onResize = this.handleResize;\n window.addEventListener('resize', this._onResize, {\n passive: true\n }); // Android WebView 兼容:在 document 级别监听 touchmove/touchend,\n // 避免可滚动子元素消费事件后不冒泡到父级(参考 AI 助理)\n\n this._swipeMoveHandler = this._onSwipeMove;\n this._swipeEndHandler = this._onSwipeEnd;\n this._popStateHandler = this._onPopState;\n document.addEventListener('touchmove', this._swipeMoveHandler, {\n passive: true\n });\n document.addEventListener('touchend', this._swipeEndHandler, {\n passive: true\n }); // 拦截 Android 系统返回手势:通过 history.pushState + popstate\n\n window.addEventListener('popstate', this._popStateHandler); // 移动端首屏为会话列表页,推入一条历史状态供系统返回拦截\n\n if (this.isMobileActive) {\n history.pushState({\n page: 'kbChatList'\n }, '');\n }\n\n this.initSession();\n this.checkKbDocuments(); // v-show 隐藏期间加载的历史消息无法滚动(scrollHeight 为 0),\n // 监听组件由隐藏变为可见时,滚动到最新问答;\n // 同时每次变为可见时刷新\"猜您想问\"(管理端可能刚更新过)\n\n if (typeof IntersectionObserver !== 'undefined' && this.$el) {\n this._visibilityObserver = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) {\n return e.isIntersecting;\n })) {\n _this.loadSuggestedQuestions(); // 管理端可能刚上传/删除了文档,重新校验遮罩状态\n\n\n _this.checkKbDocuments();\n\n if (_this.messages.length) {\n _this.$nextTick(function () {\n return _this.scrollToBottom();\n });\n }\n }\n });\n\n this._visibilityObserver.observe(this.$el);\n } else {\n this.loadSuggestedQuestions();\n }\n },\n beforeDestroy: function beforeDestroy() {\n if (this._visibilityObserver) {\n this._visibilityObserver.disconnect();\n\n this._visibilityObserver = null;\n }\n\n if (this._onResize) {\n window.removeEventListener('resize', this._onResize);\n }\n\n if (this._swipeMoveHandler) {\n document.removeEventListener('touchmove', this._swipeMoveHandler);\n }\n\n if (this._swipeEndHandler) {\n document.removeEventListener('touchend', this._swipeEndHandler);\n }\n\n if (this._popStateHandler) {\n window.removeEventListener('popstate', this._popStateHandler);\n }\n\n this.streamRequestId++;\n this.closeEventSource();\n this.clearTypeTimer();\n this.onMsgTouchEnd();\n this.contentCache = {};\n },\n methods: {\n // 独立使用时自检移动端(父级已传 isMobile 时忽略)\n handleResize: function handleResize() {\n var now = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n this.localIsMobile = now;\n },\n renderMarkdown: function renderMarkdown(text) {\n if (!text) return '';\n\n try {\n return md.render(text);\n } catch (err) {\n return this.escapeHtml(text);\n }\n },\n // 与 ChatDetail 一致的增量渲染:完整行走 markdown、未完成尾行转义拼接,\n // 借助 markdown-it breaks:true 实现逐行浮现效果。\n renderStreaming: function renderStreaming(text, msgKey) {\n if (!text) return '';\n var cache = this.contentCache[msgKey];\n var lines = text.split('\\n');\n var lastLineIdx = lines.length - 1;\n\n if (!cache) {\n try {\n var html = md.render(text);\n this.$set(this.contentCache, msgKey, {\n renderedLines: lastLineIdx,\n html: html,\n fullText: text\n });\n return html;\n } catch (_unused) {\n return this.escapeHtml(text);\n }\n }\n\n if (text === cache.fullText) return cache.html;\n cache.fullText = text; // 行数没变,仅末尾追加字符,直接拼接尾行\n\n if (lastLineIdx === cache.renderedLines) {\n return cache.html + this.escapeHtml(lines[lastLineIdx]);\n } // 行数变少/文本回退截断,全量重渲染\n\n\n if (lastLineIdx < cache.renderedLines) {\n try {\n var fullLines = lines.slice(0, lastLineIdx);\n var newBaseHtml = '';\n\n if (fullLines.length > 0) {\n newBaseHtml = md.render(fullLines.join('\\n'));\n }\n\n cache.html = newBaseHtml;\n cache.renderedLines = lastLineIdx;\n\n var _lastLine = this.escapeHtml(lines[lastLineIdx]);\n\n return cache.html + (_lastLine || '');\n } catch (_unused2) {\n return this.escapeHtml(text);\n }\n } // 新增完整行\n\n\n if (cache.renderedLines < lastLineIdx) {\n var allFullLines = lines.slice(0, lastLineIdx);\n var _newBaseHtml = '';\n\n if (allFullLines.length > 0) {\n _newBaseHtml = md.render(allFullLines.join('\\n'));\n }\n\n cache.html = _newBaseHtml;\n cache.renderedLines = lastLineIdx;\n }\n\n var lastLine = this.escapeHtml(lines[lastLineIdx]);\n cache.renderedLines = lastLineIdx;\n return cache.html + (lastLine || '');\n },\n escapeHtml: function escapeHtml(text) {\n if (!text) return '';\n var map = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(text).replace(/[&<>\"']/g, function (c) {\n return map[c];\n });\n },\n autoResizeTextarea: function autoResizeTextarea() {\n var ta = this.$refs.taRef;\n if (!ta) return; // 移动端从单行起增高(与 38px 最小高度一致),PC 保持 100px 基准\n\n var baseH = this.isMobileActive ? 38 : 100;\n var maxH = this.isMobileActive ? 120 : 180;\n ta.style.height = \"\".concat(baseH, \"px\");\n var realH = ta.scrollHeight;\n\n if (realH > baseH) {\n ta.style.height = \"\".concat(Math.min(realH, maxH), \"px\");\n }\n },\n newLine: function newLine(e) {\n var _this2 = this;\n\n e.preventDefault();\n var ta = this.$refs.taRef;\n var start = ta.selectionStart;\n var end = ta.selectionEnd;\n this.inputText = this.inputText.slice(0, start) + '\\n' + this.inputText.slice(end);\n this.$nextTick(function () {\n ta.selectionStart = ta.selectionEnd = start + 1;\n\n _this2.autoResizeTextarea();\n });\n },\n\n /**\n * 加载\"猜您想问\"问题(按公司,所有用户可见)\n */\n loadSuggestedQuestions: function loadSuggestedQuestions() {\n var _this3 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {\n var list;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n if (_this3.companyId) {\n _context.next = 3;\n break;\n }\n\n _this3.suggestedQuestions = [];\n return _context.abrupt(\"return\");\n\n case 3:\n _context.prev = 3;\n _context.next = 6;\n return (0, _knowledgeBase.getSuggestedQuestions)(_this3.companyId);\n\n case 6:\n _context.t0 = _context.sent;\n\n if (_context.t0) {\n _context.next = 9;\n break;\n }\n\n _context.t0 = [];\n\n case 9:\n list = _context.t0;\n _this3.suggestedQuestions = list.map(function (q) {\n return q.questionText;\n });\n _context.next = 16;\n break;\n\n case 13:\n _context.prev = 13;\n _context.t1 = _context[\"catch\"](3);\n console.error('加载猜您想问失败', _context.t1);\n\n case 16:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, null, [[3, 13]]);\n }))();\n },\n\n /**\n * 检查当前公司是否有「已索引」的知识库文档,无可用资料时展示遮罩提示\n * 仅已索引(indexStatus === 1)的文档才能被检索,未索引/索引中/索引失败都不算\n */\n checkKbDocuments: function checkKbDocuments() {\n var _this4 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var data, isEmpty;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n if (_this4.companyId) {\n _context2.next = 3;\n break;\n }\n\n _this4.noDocument = false;\n return _context2.abrupt(\"return\");\n\n case 3:\n _context2.prev = 3;\n _context2.next = 6;\n return (0, _knowledgeBase.getFileList)({\n companyId: _this4.companyId,\n indexStatus: 1,\n pageNum: 1,\n pageSize: 1\n });\n\n case 6:\n data = _context2.sent;\n isEmpty = (data && data.total || 0) === 0;\n _this4.noDocument = isEmpty; // 无资料时把会话固定回第一个\n\n if (isEmpty) {\n _this4.lockFirstSession(); // 无资料遮罩位于聊天详情页的 .kb-chat-main 内,而移动端首屏是会话列表页\n // (h5-list-mode 下 .kb-chat-main 被 display:none 隐藏),若停留在列表页\n // 则遮罩永远不可见;故无资料时强制切到详情页把遮罩展示出来\n\n\n if (_this4.isMobileActive) {\n _this4.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n }\n }\n\n _context2.next = 16;\n break;\n\n case 12:\n _context2.prev = 12;\n _context2.t0 = _context2[\"catch\"](3);\n // 查询失败(如无管理权限)时不展示遮罩,避免误拦截\n console.error('检查知识库文档失败', _context2.t0);\n _this4.noDocument = false;\n\n case 16:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, null, [[3, 12]]);\n }))();\n },\n initSession: function initSession() {\n var _this5 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {\n var list;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n if (_this5.companyId) {\n _context3.next = 2;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 2:\n if (!(_this5.initDone && (_this5.messages.length || _this5.streaming || _this5.inputText))) {\n _context3.next = 4;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 4:\n _this5.initDone = false;\n _context3.prev = 5;\n _context3.next = 8;\n return (0, _kbChat.getKbSessionList)(_this5.companyId);\n\n case 8:\n _context3.t0 = _context3.sent;\n\n if (_context3.t0) {\n _context3.next = 11;\n break;\n }\n\n _context3.t0 = [];\n\n case 11:\n list = _context3.t0;\n _this5.sessionList = list;\n\n if (!(list.length > 0)) {\n _context3.next = 19;\n break;\n }\n\n // 只有 1 个会话且无消息(新用户首次进入自动生成的空会话)时,\n // 默认折叠侧边栏只显示聊天主界面;在 selectSession 前同步设置,\n // 避免「先展开再合住」的闪烁\n _this5.sidebarCollapsed = list.length === 1 && !list[0].lastContent;\n _context3.next = 17;\n return _this5.selectSession(list[0].sessionId);\n\n case 17:\n _context3.next = 21;\n break;\n\n case 19:\n _context3.next = 21;\n return _this5.autoCreateSession();\n\n case 21:\n _context3.next = 28;\n break;\n\n case 23:\n _context3.prev = 23;\n _context3.t1 = _context3[\"catch\"](5);\n console.error('获取会话列表失败,尝试自动创建', _context3.t1);\n _context3.next = 28;\n return _this5.autoCreateSession();\n\n case 28:\n _context3.prev = 28;\n _this5.initDone = true;\n return _context3.finish(28);\n\n case 31:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _callee3, null, [[5, 23, 28, 31]]);\n }))();\n },\n autoCreateSession: function autoCreateSession() {\n var _this6 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {\n var data;\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n if (_this6.companyId) {\n _context4.next = 3;\n break;\n }\n\n console.warn('[KbChat] 无法创建会话:companyId 为空');\n return _context4.abrupt(\"return\");\n\n case 3:\n console.log('[KbChat] 创建会话,companyId:', _this6.companyId);\n _context4.prev = 4;\n _context4.next = 7;\n return (0, _kbChat.createKbSession)(_this6.companyId);\n\n case 7:\n data = _context4.sent;\n console.log('[KbChat] 会话创建成功,sessionId:', data.sessionId);\n\n _this6.finishStream();\n\n _this6.activeSessionId = data.sessionId;\n _this6.streamRequestId++;\n _this6.messages = [];\n _this6.sessionList = [{\n sessionId: data.sessionId,\n title: '',\n lastContent: ''\n }].concat(_this6.sessionList || []); // 新建会话时保持侧边栏展开,方便继续切换会话\n\n _this6.sidebarCollapsed = false;\n\n _this6.$nextTick(function () {\n return _this6.scrollToBottom();\n });\n\n _context4.next = 21;\n break;\n\n case 18:\n _context4.prev = 18;\n _context4.t0 = _context4[\"catch\"](4);\n console.error('自动创建会话失败', _context4.t0);\n\n case 21:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _callee4, null, [[4, 18]]);\n }))();\n },\n\n /** 新建会话(豆包式「新对话」) */\n handleNewSession: function handleNewSession() {\n var _this7 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee5() {\n return regeneratorRuntime.wrap(function _callee5$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n if (!_this7.noDocument) {\n _context5.next = 2;\n break;\n }\n\n return _context5.abrupt(\"return\");\n\n case 2:\n if (_this7.streaming) _this7.handleStop();\n _context5.next = 5;\n return _this7.autoCreateSession();\n\n case 5:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _callee5);\n }))();\n },\n\n /** 切换会话 */\n handleSelectSession: function handleSelectSession(item) {\n var _this8 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee6() {\n return regeneratorRuntime.wrap(function _callee6$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context6.next = 2;\n break;\n }\n\n return _context6.abrupt(\"return\");\n\n case 2:\n if (!_this8.noDocument) {\n _context6.next = 5;\n break;\n }\n\n _this8.lockFirstSession();\n\n return _context6.abrupt(\"return\");\n\n case 5:\n if (!(item.sessionId === _this8.activeSessionId && _this8.messages.length)) {\n _context6.next = 8;\n break;\n }\n\n if (_this8.isMobileActive) {\n _this8.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n return _context6.abrupt(\"return\");\n\n case 8:\n _context6.next = 10;\n return _this8.selectSession(item.sessionId);\n\n case 10:\n // 移动端:选中后跳到聊天详情页\n if (_this8.isMobileActive) {\n _this8.showH5List = false; // 推入历史状态,使 Android 系统返回手势先触发 popstate 被拦截\n\n history.pushState({\n page: 'kbChatDetail'\n }, ''); // 列表页时聊天区被 display:none,scrollHeight 为 0 导致上面的滚动无效;\n // 切到详情页、容器可见后需再滚一次,默认定位到最新消息\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n case 11:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _callee6);\n }))();\n },\n\n /** 返回宿主 App 原生页面(参考 AI 助理 handleNativeBack) */\n handleNativeBack: function handleNativeBack() {\n if (this.$hybrid && typeof this.$hybrid.closePage === 'function') {\n this.$hybrid.closePage();\n } else if (window.history.length > 1) {\n window.history.back();\n }\n },\n\n /** 会话列表页返回按钮:返回原生页面 */\n handleSidebarBack: function handleSidebarBack() {\n this.handleNativeBack();\n },\n // ── H5 侧滑手势(参考 AI 助理)──\n // 从左/右屏幕边缘起手横滑触发返回,避免页面内部横向滚动被误判\n onSwipeTouchStart: function onSwipeTouchStart(e) {\n if (!this.isMobileActive) return;\n var t = e.touches[0];\n this._swipeStartX = t.clientX;\n this._swipeStartY = t.clientY;\n this._swipeHandled = false;\n var edge = Math.min(56, Math.round(window.innerWidth * 0.09));\n this._swipeFromLeftEdge = t.clientX <= edge;\n this._swipeFromRightEdge = t.clientX >= window.innerWidth - edge;\n this._swipeFromEdge = this._swipeFromLeftEdge || this._swipeFromRightEdge;\n },\n _swipeShouldTrigger: function _swipeShouldTrigger(diffX, diffY) {\n if (!this._swipeFromEdge) return false; // 从左边缘 → 右滑;从右边缘 → 左滑\n\n var directionOk = this._swipeFromLeftEdge && diffX >= 60 || this._swipeFromRightEdge && diffX <= -60;\n if (!directionOk) return false; // 水平距离明显大于垂直距离(排除上下滚动干扰)\n\n return Math.abs(diffX) >= Math.abs(diffY) * 2;\n },\n _onSwipeMove: function _onSwipeMove(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.touches || !e.touches.length) return;\n var diffX = e.touches[0].clientX - this._swipeStartX;\n var diffY = e.touches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n // touchend 兜底,兼容不支持 touchmove 的场景\n _onSwipeEnd: function _onSwipeEnd(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.changedTouches || !e.changedTouches.length) return;\n var diffX = e.changedTouches[0].clientX - this._swipeStartX;\n var diffY = e.changedTouches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n\n /** 侧滑触发:列表页 → 返回原生页面;详情页 → 回会话列表页 */\n _triggerSwipeAction: function _triggerSwipeAction() {\n if (this.showH5List) {\n this.handleNativeBack();\n } else {\n this.handleMobileBack();\n }\n },\n\n /**\n * 拦截 Android 系统返回手势(history.pushState + popstate)\n * WebView 先回退历史栈触发 popstate,这里拦截后导航到列表页,避免 Activity 被直接关闭\n */\n _onPopState: function _onPopState() {\n if (!this.isMobileActive) return;\n\n if (!this.showH5List) {\n // 详情页 → 返回会话列表页\n this.showH5List = true;\n history.pushState({\n page: 'kbChatList'\n }, '');\n } else {\n // 列表页 → 直接关闭 WebView 返回原生页面\n this.handleNativeBack();\n }\n },\n\n /** 移动端详情页返回:无资料遮罩时返回原生页面;否则回到会话列表页 */\n handleMobileBack: function handleMobileBack() {\n // 无资料遮罩期间无法进入会话,返回到宿主 App 原生页面\n if (this.noDocument) {\n this.handleNativeBack();\n return;\n } // 正常情况:回到会话列表页\n\n\n this.showH5List = true;\n },\n\n /** 无资料遮罩期间:把当前会话固定回列表中的第一个 */\n lockFirstSession: function lockFirstSession() {\n var first = this.sessionList[0];\n\n if (first && first.sessionId && first.sessionId !== this.activeSessionId) {\n this.selectSession(first.sessionId);\n }\n },\n\n /** 会话项「…」菜单:重命名 / 删除 */\n handleSessionMenu: function handleSessionMenu(cmd, item) {\n if (!item || !item.sessionId) return;\n\n if (cmd === 'rename') {\n this.handleRenameSession(item);\n } else if (cmd === 'delete') {\n this.handleDeleteSession(item);\n }\n },\n // ==================== 移动端会话操作单 ====================\n openMobileAction: function openMobileAction(item) {\n if (this.noDocument) return;\n this.mobileAction = item;\n },\n doMobileRename: function doMobileRename(item) {\n this.mobileAction = null;\n this.handleRenameSession(item);\n },\n doMobileDelete: function doMobileDelete(item) {\n this.mobileAction = null;\n this.handleDeleteSession(item);\n },\n\n /** 重命名会话 */\n handleRenameSession: function handleRenameSession(item) {\n var _this9 = this;\n\n if (!item || !item.sessionId) return;\n this.$prompt('请输入新的会话名称', '重命名会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n inputValue: item.title || '',\n inputPattern: /\\S+/,\n inputErrorMessage: '名称不能为空'\n }).then(function (_ref) {\n var value = _ref.value;\n var title = (value || '').trim();\n if (!title) return;\n return (0, _kbChat.renameKbSession)(item.sessionId, title).then(function () {\n item.title = title;\n\n _this9.$message.success('重命名成功');\n });\n }).catch(function (err) {\n // 取消或校验失败,不提示错误\n if (err && err !== 'cancel' && err !== 'close') {\n console.error('重命名失败', err);\n\n _this9.$message.error('重命名失败');\n }\n });\n },\n\n /** 删除会话 */\n handleDeleteSession: function handleDeleteSession(item) {\n var _this10 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {\n return regeneratorRuntime.wrap(function _callee7$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context7.next = 2;\n break;\n }\n\n return _context7.abrupt(\"return\");\n\n case 2:\n if (!(_this10.sessionList.length <= 1)) {\n _context7.next = 5;\n break;\n }\n\n _this10.$message.warning('至少保留一个会话');\n\n return _context7.abrupt(\"return\");\n\n case 5:\n _context7.prev = 5;\n _context7.next = 8;\n return _this10.$confirm('删除后无法恢复,确定删除该会话吗?', '删除会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 8:\n _context7.next = 13;\n break;\n\n case 10:\n _context7.prev = 10;\n _context7.t0 = _context7[\"catch\"](5);\n return _context7.abrupt(\"return\");\n\n case 13:\n _context7.prev = 13;\n _context7.next = 16;\n return (0, _kbChat.deleteKbSession)(item.sessionId);\n\n case 16:\n _this10.sessionList = (_this10.sessionList || []).filter(function (s) {\n return s.sessionId !== item.sessionId;\n });\n\n if (!(_this10.activeSessionId === item.sessionId)) {\n _context7.next = 28;\n break;\n }\n\n _this10.finishStream();\n\n _this10.activeSessionId = '';\n _this10.messages = [];\n\n if (!(_this10.sessionList.length > 0)) {\n _context7.next = 26;\n break;\n }\n\n _context7.next = 24;\n return _this10.selectSession(_this10.sessionList[0].sessionId);\n\n case 24:\n _context7.next = 28;\n break;\n\n case 26:\n _context7.next = 28;\n return _this10.autoCreateSession();\n\n case 28:\n _this10.$message.success('删除成功');\n\n _context7.next = 35;\n break;\n\n case 31:\n _context7.prev = 31;\n _context7.t1 = _context7[\"catch\"](13);\n console.error('删除会话失败', _context7.t1);\n\n _this10.$message.error('删除失败');\n\n case 35:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _callee7, null, [[5, 10], [13, 31]]);\n }))();\n },\n\n /** 会话列表时间格式化:今天显示时分,更早显示月日 */\n formatTime: function formatTime(time) {\n if (!time) return '';\n var d = new Date(time);\n if (isNaN(d.getTime())) return '';\n var now = new Date();\n var sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();\n\n var pad = function pad(n) {\n return n < 10 ? '0' + n : '' + n;\n };\n\n if (sameDay) {\n return pad(d.getHours()) + ':' + pad(d.getMinutes());\n }\n\n return pad(d.getMonth() + 1) + '-' + pad(d.getDate());\n },\n\n /** 后端消息 → 前端消息对象(保留 id/msgKey,删除功能需要) */\n mapMessage: function mapMessage(m, i) {\n var thinking = m.thinking || m.reasoning || m.reasoning_result || m.reasoningContent || m.reasoning_content || m.thinkingContent || m.thinking_content || '';\n return {\n id: m.id,\n msgKey: m.msgKey || m.msg_key || '',\n role: m.role,\n content: m.content || '',\n thinking: thinking,\n _key: m.createTime ? 'msg_' + m.createTime + '_' + i : 'msg_' + Date.now() + '_' + Math.random()\n };\n },\n selectSession: function selectSession(sessionId) {\n var _this11 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee8() {\n var raw, msgs;\n return regeneratorRuntime.wrap(function _callee8$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n if (_this11.streaming) _this11.handleStop();\n\n _this11.exitSelectMode();\n\n _this11.msgAction = null;\n _this11.activeSessionId = sessionId;\n _this11.messages = [];\n _this11.streamMsg = null;\n _context8.prev = 6;\n _context8.next = 9;\n return (0, _kbChat.getKbSessionMessages)(sessionId);\n\n case 9:\n raw = _context8.sent;\n msgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || [];\n _this11.messages = msgs.map(function (m, i) {\n return _this11.mapMessage(m, i);\n });\n\n _this11.$nextTick(function () {\n return _this11.scrollToBottom();\n });\n\n _context8.next = 18;\n break;\n\n case 15:\n _context8.prev = 15;\n _context8.t0 = _context8[\"catch\"](6);\n console.error('加载消息失败', _context8.t0);\n\n case 18:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _callee8, null, [[6, 15]]);\n }))();\n },\n\n /**\n * 回读当前会话消息,补全后端持久化后生成的 id。\n * 流式回答期间消息只存在于本地数组(无 id),无法按 id 删除;\n * 流式结束后调用一次即可拿到 id。已全部有 id 时跳过,避免多余请求。\n *\n * 注意:不能直接用后端数组整体覆盖 this.messages。后端回读可能包含重复/多余记录\n * (如同一会话里用户消息被落库两次),若整体覆盖会把重复消息直接渲染出来。\n * 这里以本地数组为基准,只用后端记录按「角色 + 内容」顺序回填 id,保证界面条数不变。\n */\n syncMessageIds: function syncMessageIds() {\n var _this12 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee9() {\n var raw, backendMsgs, cursor;\n return regeneratorRuntime.wrap(function _callee9$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n if (!(!_this12.activeSessionId || _this12.streaming)) {\n _context9.next = 2;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 2:\n if (!(_this12.messages.length && _this12.messages.every(function (m) {\n return m.id;\n }))) {\n _context9.next = 4;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 4:\n _context9.prev = 4;\n _context9.next = 7;\n return (0, _kbChat.getKbSessionMessages)(_this12.activeSessionId);\n\n case 7:\n raw = _context9.sent;\n backendMsgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || []; // 按顺序把本地消息与后端记录配对(角色一致且内容匹配),只补 id/msgKey\n\n cursor = 0;\n\n _this12.messages.forEach(function (m) {\n if (m.id) return;\n\n var role = _this12.normRole(m.role);\n\n for (var i = cursor; i < backendMsgs.length; i++) {\n var b = backendMsgs[i];\n\n if (_this12.normRole(b.role) === role && String(b.content || '').trim() === String(m.content || '').trim()) {\n m.id = b.id;\n if (!m.msgKey) m.msgKey = b.msgKey || b.msg_key || '';\n cursor = i + 1;\n return;\n }\n }\n });\n\n _context9.next = 16;\n break;\n\n case 13:\n _context9.prev = 13;\n _context9.t0 = _context9[\"catch\"](4);\n console.error('同步消息 id 失败', _context9.t0);\n\n case 16:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _callee9, null, [[4, 13]]);\n }))();\n },\n // ==================== 消息多选 / 删除 ====================\n\n /** 进入多选模式(preselectMsg 传入时默认勾选该条所在的整组问答,对应豆包式「删除」入口) */\n enterSelectMode: function enterSelectMode(preselectMsg) {\n var _this13 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee10() {\n var preselectId, idx;\n return regeneratorRuntime.wrap(function _callee10$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n if (!(_this13.streaming || !_this13.messages.length)) {\n _context10.next = 2;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 2:\n // 点击条的后端持久化 id(模板上「删除」按钮仅在 msg.id 存在时渲染,故必有值)\n preselectId = preselectMsg ? preselectMsg.id : null; // 先回读消息确保每条都有 id(刚流式完成的消息本地没有 id)。\n // 回读只补 id、不重建数组,定位可用 id 或对象引用。\n\n _context10.next = 5;\n return _this13.syncMessageIds();\n\n case 5:\n _this13.selectMode = true;\n _this13.selectedMsgIds = [];\n idx = preselectId != null ? _this13.messages.findIndex(function (m) {\n return m.id === preselectId;\n }) : -1;\n if (idx === -1 && preselectMsg) idx = _this13.messages.indexOf(preselectMsg);\n\n if (idx !== -1) {\n // 默认勾选整组问答(用户问 + AI 答),便于一键删除\n _this13.selectedMsgIds = _this13.getQaPairIdsAt(idx);\n } // 滚动定位:让默认勾选的整组问答都进入视野(长回答会把用户问顶出屏幕)\n\n\n _this13.scrollSelectedIntoView();\n\n case 11:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _callee10);\n }))();\n },\n\n /**\n * 把首个被选中的消息行滚动到容器内可见。\n * 选中行是「用户问 + AI 答」中的靠前一条,滚到它即能同时看到整组。\n * 若靠前那条已在视野内则不动,避免无谓跳动。\n */\n scrollSelectedIntoView: function scrollSelectedIntoView() {\n var _this14 = this;\n\n this.$nextTick(function () {\n var container = _this14.$refs.messagesRef;\n if (!container) return;\n var rows = container.querySelectorAll('.msg-row');\n if (!rows.length) return; // 找到第一个处于选中态、且带 id 的行(与 selectedMsgIds 顺序无关,取 DOM 顺序)\n\n var target = null;\n\n for (var i = 0; i < rows.length; i++) {\n var box = rows[i].querySelector('.msg-check-box.checked');\n\n if (box) {\n target = rows[i];\n break;\n }\n }\n\n if (!target) return;\n var cRect = container.getBoundingClientRect();\n var tRect = target.getBoundingClientRect(); // 目标行顶部已在容器可视区内则无需滚动\n\n if (tRect.top >= cRect.top && tRect.top < cRect.bottom) return; // 滚动到目标行顶部(留 8px 间距)\n\n container.scrollTop += tRect.top - cRect.top - 8;\n });\n },\n exitSelectMode: function exitSelectMode() {\n this.selectMode = false;\n this.selectedMsgIds = [];\n },\n isMsgSelected: function isMsgSelected(msg) {\n return !!msg.id && this.selectedMsgIds.indexOf(msg.id) !== -1;\n },\n\n /** 归一化 role,兼容后端可能返回的大小写/空白 */\n normRole: function normRole(role) {\n return String(role == null ? '' : role).trim().toLowerCase();\n },\n\n /**\n * 取与下标 idx 处消息成对的另一条:用户问 ↔ AI 答视为一组问答。\n * 只认紧邻的一条(问答对在列表里必然相邻),相邻非同组则不配对。\n * 不依赖 role 的具体拼写(user/assistant 的大小写、空白均容错)。\n */\n getQaPartnerAt: function getQaPartnerAt(idx) {\n if (idx < 0 || idx >= this.messages.length) return null;\n var cur = this.messages[idx];\n if (!cur) return null;\n var role = this.normRole(cur.role);\n if (role !== 'user' && role !== 'assistant') return null; // 只认紧邻的那一条:问答对在列表里必然相邻。\n // 若相邻消息不是异角色(例如回答已被删除),说明当前消息已无配对,\n // 不能继续向前/向后扫描,否则会误勾到上一个问答的回答。\n\n var neighbor = role === 'user' ? this.messages[idx + 1] : this.messages[idx - 1];\n if (!neighbor) return null;\n var neighborRole = this.normRole(neighbor.role);\n var expect = role === 'user' ? 'assistant' : 'user';\n return neighborRole === expect ? neighbor : null;\n },\n\n /** 下标 idx 处消息所在问答对的可删 id 列表(用户问 + AI 答) */\n getQaPairIdsAt: function getQaPairIdsAt(idx) {\n var ids = [];\n var cur = this.messages[idx];\n if (cur && cur.id) ids.push(cur.id);\n var partner = this.getQaPartnerAt(idx);\n if (partner && partner.id && ids.indexOf(partner.id) === -1) ids.push(partner.id);\n return ids;\n },\n\n /**\n * 勾选单条消息:勾上时连带把问答对里另一条一起勾上;\n * 取消时只取消当前这条,从而支持「先整组勾选、再单独取消一条」的单条选择。\n */\n toggleSelectMessage: function toggleSelectMessage(msg) {\n var _this15 = this;\n\n if (!msg.id) return;\n var i = this.selectedMsgIds.indexOf(msg.id);\n\n if (i === -1) {\n var idx = this.messages.indexOf(msg);\n var pairIds = idx === -1 ? [msg.id] : this.getQaPairIdsAt(idx);\n pairIds.forEach(function (id) {\n if (_this15.selectedMsgIds.indexOf(id) === -1) _this15.selectedMsgIds.push(id);\n });\n } else {\n this.selectedMsgIds.splice(i, 1);\n }\n },\n toggleSelectAll: function toggleSelectAll() {\n var ids = this.deletableMsgIds;\n this.selectedMsgIds = this.selectedMsgIds.length === ids.length ? [] : ids.slice();\n },\n\n /** 多选模式下点击整行即切换选中 */\n onMsgRowClick: function onMsgRowClick(msg) {\n // 长按弹出菜单后浏览器还会补一次 click,短暂屏蔽避免误触\n if (Date.now() < this._ignoreClickUntil) return;\n if (this.selectMode) this.toggleSelectMessage(msg);\n },\n\n /** 移动端长按气泡弹出消息操作单(豆包式:复制 / 删除 / 多选) */\n onMsgTouchStart: function onMsgTouchStart(msg) {\n var _this16 = this;\n\n if (!this.isMobileActive || this.selectMode) return;\n this._longPressMsg = msg;\n clearTimeout(this._longPressTimer);\n this._longPressTimer = setTimeout(function () {\n _this16._longPressTimer = null; // 用户正在长按选中文本(复制用),此时不弹菜单,避免抢走选择行为\n\n var sel = window.getSelection && window.getSelection();\n if (sel && String(sel).trim()) return;\n\n _this16.openMsgAction(_this16._longPressMsg);\n }, 500);\n },\n onMsgTouchEnd: function onMsgTouchEnd() {\n if (this._longPressTimer) {\n clearTimeout(this._longPressTimer);\n this._longPressTimer = null;\n }\n },\n openMsgAction: function openMsgAction(msg) {\n this.msgAction = msg; // 长按抬手后浏览器可能补发 click,短暂屏蔽,避免刚弹出的菜单被立即关闭\n\n this._ignoreClickUntil = Date.now() + 350;\n },\n closeMsgAction: function closeMsgAction() {\n if (Date.now() < this._ignoreClickUntil) return;\n this.msgAction = null;\n },\n\n /** 移动端消息操作单:复制 / 删除(删除进入多选模式,与豆包一致) */\n doMsgAction: function doMsgAction(action) {\n var msg = this.msgAction;\n this.msgAction = null;\n if (!msg) return;\n\n if (action === 'copy') {\n this.copyText(msg.content);\n } else if (action === 'delete') {\n this.enterSelectMode(msg);\n }\n },\n\n /** 批量删除选中的消息 */\n handleDeleteSelected: function handleDeleteSelected() {\n var _this17 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee11() {\n var ids;\n return regeneratorRuntime.wrap(function _callee11$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n if (!(!_this17.selectedMsgIds.length || !_this17.activeSessionId)) {\n _context11.next = 2;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 2:\n ids = _this17.selectedMsgIds.slice();\n _context11.prev = 3;\n _context11.next = 6;\n return _this17.$confirm(\"\\u5220\\u9664\\u540E\\u65E0\\u6CD5\\u6062\\u590D\\uFF0C\\u786E\\u5B9A\\u5220\\u9664\\u9009\\u4E2D\\u7684 \".concat(ids.length, \" \\u6761\\u6D88\\u606F\\u5417\\uFF1F\"), '删除消息', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 6:\n _context11.next = 11;\n break;\n\n case 8:\n _context11.prev = 8;\n _context11.t0 = _context11[\"catch\"](3);\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.prev = 11;\n _context11.next = 14;\n return (0, _kbChat.deleteKbMessages)(_this17.activeSessionId, ids);\n\n case 14:\n _this17.messages = _this17.messages.filter(function (m) {\n return ids.indexOf(m.id) === -1;\n });\n\n _this17.exitSelectMode();\n\n _this17.$message.success('删除成功');\n\n _this17.refreshSessionList();\n\n _context11.next = 24;\n break;\n\n case 20:\n _context11.prev = 20;\n _context11.t1 = _context11[\"catch\"](11);\n console.error('批量删除消息失败', _context11.t1);\n\n _this17.$message.error('删除失败');\n\n case 24:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _callee11, null, [[3, 8], [11, 20]]);\n }))();\n },\n handleResetSession: function handleResetSession() {\n var _this18 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee12() {\n return regeneratorRuntime.wrap(function _callee12$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n if (!_this18.activeSessionId) {\n _context12.next = 8;\n break;\n }\n\n _context12.prev = 1;\n _context12.next = 4;\n return (0, _kbChat.closeKbSession)(_this18.activeSessionId);\n\n case 4:\n _context12.next = 8;\n break;\n\n case 6:\n _context12.prev = 6;\n _context12.t0 = _context12[\"catch\"](1);\n\n case 8:\n _context12.next = 10;\n return _this18.autoCreateSession();\n\n case 10:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _callee12, null, [[1, 6]]);\n }))();\n },\n handleSend: function handleSend(quickText) {\n var _this19 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee13() {\n var text, requestSessionId, requestId, msgKey, msgIdx, streamMsg, isCurrentStream, answerBuffer, finalContent, streamDone, url;\n return regeneratorRuntime.wrap(function _callee13$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n text = typeof quickText === 'string' ? quickText.trim() : _this19.inputText.trim();\n\n if (!(!text || !_this19.activeSessionId || _this19.streaming)) {\n _context13.next = 3;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 3:\n _this19.exitSelectMode();\n\n _this19.inputText = '';\n requestSessionId = _this19.activeSessionId;\n requestId = ++_this19.streamRequestId;\n\n _this19.clearTypeTimer();\n\n _this19.messages.push({\n role: 'user',\n content: text\n });\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n }); // 流式消息:作为 messages 中的对象(带 _key/loading),与 ChatDetail 一致\n\n\n msgKey = 'stream_' + requestId;\n msgIdx = _this19.messages.push({\n role: 'assistant',\n content: '',\n thinking: '',\n toolCalls: [],\n loading: true,\n _key: msgKey\n }) - 1;\n streamMsg = _this19.messages[msgIdx];\n _this19.streamMsg = streamMsg;\n _this19.streaming = true;\n\n isCurrentStream = function isCurrentStream() {\n return requestId === _this19.streamRequestId && requestSessionId === _this19.activeSessionId && _this19.streamMsg === streamMsg;\n }; // SSE 只把数据累积到 answerBuffer。typeTimer(30ms) 节流按「完整换行」逐行把内容\n // 推进到 msg.content:每个 tick 最多只推进「到下一个 \\n 为止」的一行,未完成的尾行\n // 保持不动;配合 renderStreaming(breaks:true) 实现「一行显示一行」。\n // 不是打字机模式(不逐字)、也不是一次性整段渲染(不会一出就是全文)。\n\n\n answerBuffer = '';\n finalContent = '';\n streamDone = false;\n _this19._typeTimer = setInterval(function () {\n if (!isCurrentStream()) return;\n var msg = _this19.messages[msgIdx];\n var source = finalContent || answerBuffer;\n var curLen = msg._prevReveal || 0;\n var nextLen = curLen; // 找到「当前已揭示进度之后」的第一条完整换行,仅推进这一行\n\n var nl = source.indexOf('\\n', curLen);\n\n if (nl !== -1) {\n nextLen = nl + 1;\n } else if (streamDone) {\n // 流结束且没有更多换行:最后一段未以换行结尾的内容一次性露出(此时即完整一行)\n nextLen = source.length;\n }\n\n if (nextLen > curLen) {\n msg._prevReveal = nextLen;\n msg.content = source.substring(0, nextLen);\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n } // 流结束且所有内容都已逐行露出 → 收尾\n\n\n if (streamDone && nextLen >= source.length) {\n _this19.finishStream(); // 后端已在此刻落库,回读一次补全消息 id(单条删除需要)\n\n\n _this19.syncMessageIds();\n }\n }, 30);\n url = (0, _kbChat.createKbStreamUrl)(_this19.activeSessionId, text);\n _this19.eventSource = new EventSource(url);\n\n _this19.eventSource.addEventListener('tool_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n\n if (e.data && _this19.messages[msgIdx]) {\n if (!_this19.messages[msgIdx].toolCalls) _this19.messages[msgIdx].toolCalls = [];\n\n _this19.messages[msgIdx].toolCalls.push(e.data);\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n }\n });\n\n _this19.eventSource.addEventListener('answer', function (e) {\n if (!isCurrentStream() || streamDone) return;\n answerBuffer += e.data;\n });\n\n _this19.eventSource.addEventListener('agent_result', function (e) {\n if (!isCurrentStream() || streamDone) return; // 与档案君(AiChat.vue)对齐:AGENT_RESULT 为最终真相源。\n // 只记录 finalContent 并结束流,不打断已在逐行推进的 content,交给 typeTimer 以 target 推进/校正。\n\n if (e.data) finalContent = e.data;\n streamDone = true;\n\n _this19.closeEventSource();\n });\n\n _this19.eventSource.addEventListener('msgKey', function (e) {\n if (!isCurrentStream()) return;\n\n if (e.data && _this19.messages[msgIdx]) {\n _this19.messages[msgIdx].msgKey = e.data;\n }\n });\n\n _this19.eventSource.addEventListener('reasoning', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking += e.data;\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n });\n\n _this19.eventSource.addEventListener('reasoning_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking = e.data;\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n });\n\n _this19.eventSource.addEventListener('error', function () {\n if (!isCurrentStream() || streamDone) return;\n if (!answerBuffer) answerBuffer = '❌ 连接中断';\n streamDone = true;\n\n _this19.closeEventSource();\n });\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _callee13);\n }))();\n },\n handleStop: function handleStop() {\n this.streamRequestId++;\n\n if (this.activeSessionId) {\n (0, _kbChat.stopKbStream)(this.activeSessionId).catch(function () {});\n }\n\n this.finishStream();\n },\n finishStream: function finishStream() {\n var _this20 = this;\n\n this.clearTypeTimer();\n\n if (this.streamMsg) {\n this.$delete(this.contentCache, this.streamMsg._key);\n this.streamMsg.loading = false;\n this.streamMsg = null;\n }\n\n this.closeEventSource();\n this.streaming = false;\n this.refreshSessionList();\n this.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n },\n\n /** 流式结束后刷新会话列表(更新标题/最后消息) */\n refreshSessionList: function refreshSessionList() {\n var _this21 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee14() {\n var list;\n return regeneratorRuntime.wrap(function _callee14$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n if (_this21.companyId) {\n _context14.next = 2;\n break;\n }\n\n return _context14.abrupt(\"return\");\n\n case 2:\n _context14.prev = 2;\n _context14.next = 5;\n return (0, _kbChat.getKbSessionList)(_this21.companyId);\n\n case 5:\n _context14.t0 = _context14.sent;\n\n if (_context14.t0) {\n _context14.next = 8;\n break;\n }\n\n _context14.t0 = [];\n\n case 8:\n list = _context14.t0;\n _this21.sessionList = list;\n _context14.next = 15;\n break;\n\n case 12:\n _context14.prev = 12;\n _context14.t1 = _context14[\"catch\"](2);\n console.error('刷新会话列表失败', _context14.t1);\n\n case 15:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _callee14, null, [[2, 12]]);\n }))();\n },\n clearTypeTimer: function clearTypeTimer() {\n if (this._typeTimer) {\n clearInterval(this._typeTimer);\n this._typeTimer = null;\n }\n },\n closeEventSource: function closeEventSource() {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n },\n copyText: function copyText(text) {\n if (navigator.clipboard) {\n navigator.clipboard.writeText(text);\n }\n },\n scrollToBottom: function scrollToBottom() {\n var el = this.$refs.messagesRef;\n\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack://ZjPluginIntelligent/./src/lib/knowledgeBase/KbChatMain.vue?./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options");
|
|
1015
|
+
eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! ./node_modules/@babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\").default;\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/asyncToGenerator */ \"./node_modules/@babel/runtime/helpers/asyncToGenerator.js\"));\n\nvar _typeof2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/typeof */ \"./node_modules/@babel/runtime/helpers/typeof.js\"));\n\nvar _logo = _interopRequireDefault(__webpack_require__(/*! @/assets/logo.png */ \"./src/assets/logo.png\"));\n\nvar _markdownIt = _interopRequireDefault(__webpack_require__(/*! markdown-it */ \"./node_modules/markdown-it/index.mjs\"));\n\nvar _kbChat = __webpack_require__(/*! ./api/kbChat */ \"./src/lib/knowledgeBase/api/kbChat.js\");\n\nvar _knowledgeBase = __webpack_require__(/*! ./api/knowledgeBase */ \"./src/lib/knowledgeBase/api/knowledgeBase.js\");\n\nvar _native = __webpack_require__(/*! ./native.js */ \"./src/lib/knowledgeBase/native.js\");\n\nvar _request = __webpack_require__(/*! @lib/utils/request.js */ \"./src/lib/utils/request.js\");\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar AI_AVATAR_URL = 'https://qiguanbang.obs.cn-east-3.myhuaweicloud.com/production/image/AlAgent/doc_query.png'; // 与 ChatDetail 使用相同的 Markdown 配置\n\nvar md = new _markdownIt.default({\n breaks: true,\n html: false,\n linkify: true,\n typographer: true\n});\nmd.enable('table');\nvar _default = {\n name: 'KbChatMain',\n props: {\n companyId: {\n type: String,\n default: ''\n },\n userno: {\n type: String,\n default: ''\n },\n imAccount: {\n type: [String, Object],\n default: ''\n },\n userName: {\n type: String,\n default: ''\n },\n // 是否移动端:由父级(KnowledgeBaseMain)传入;未传时组件自身按窗口宽度判断\n isMobile: {\n type: Boolean,\n default: null\n }\n },\n data: function data() {\n // 首帧前计算是否移动端,避免 H5 先显示 PC 布局再切换的闪烁(与 AI 助理一致)\n // 宿主没有 Element UI 时(H5/平板横屏)一律走自绘 UI 分支,避免 el-* 未知元素报错\n var _isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n\n return {\n initDone: false,\n activeSessionId: '',\n sessionList: [],\n // 默认折叠侧边栏:避免新用户进入时先展开再合住的闪烁,\n // 加载出会话后再按规则决定是否展开\n sidebarCollapsed: true,\n messages: [],\n inputText: '',\n streaming: false,\n streamMsg: null,\n showToolInfo: false,\n contentCache: {},\n eventSource: null,\n streamRequestId: 0,\n suggestedQuestions: [],\n // 消息多选模式(豆包式):PC 由消息操作区「删除/多选」进入,移动端长按气泡弹出菜单进入\n selectMode: false,\n // 已选中的消息 id 列表(后端主键)\n selectedMsgIds: [],\n // 当前公司是否没有知识库文档(无资料时展示遮罩)\n noDocument: false,\n // 移动端状态(仿 AI 助理):showH5List=true 显示会话列表页,false 显示聊天详情页\n localIsMobile: _isMobile,\n showH5List: _isMobile,\n // 移动端会话「…」弹出的操作单\n mobileAction: null,\n // 移动端长按气泡弹出的消息操作单(复制 / 删除 / 多选)\n msgAction: null,\n // 移动端侧滑手势(参考 AI 助理):记录起手坐标与是否已触发\n _swipeStartX: 0,\n _swipeStartY: 0,\n _swipeHandled: false,\n _swipeFromLeftEdge: false,\n _swipeFromRightEdge: false,\n _swipeFromEdge: false\n };\n },\n computed: {\n /** 实际是否移动端:优先用父级 prop,未传则用自身窗口判断 */\n isMobileActive: function isMobileActive() {\n return this.isMobile !== null ? this.isMobile : this.localIsMobile;\n },\n aiAvatar: function aiAvatar() {\n return AI_AVATAR_URL;\n },\n userAvatar: function userAvatar() {\n var account = this.imAccount;\n console.log('KbChatMain - imAccount 原始值:', account); // 尝试解析字符串为对象\n\n if (typeof account === 'string') {\n try {\n account = JSON.parse(account);\n console.log('KbChatMain - imAccount 解析后:', account);\n } catch (e) {\n // 如果解析失败,检查是否是纯 URL 字符串\n if (account.trim() && (account.includes('http://') || account.includes('https://'))) {\n console.log('KbChatMain - imAccount 是 URL:', account.trim());\n return account.trim();\n }\n }\n } // 如果是对象,尝试多个可能的头像字段(优先使用 headImg)\n\n\n if (account && (0, _typeof2.default)(account) === 'object') {\n var avatarFields = ['headImg', 'headPortrait', 'avatar', 'avatarUrl', 'headImage', 'photoUrl', 'portrait', 'headUrl', 'userHeadUrl', 'userAvatar'];\n\n for (var _i = 0, _avatarFields = avatarFields; _i < _avatarFields.length; _i++) {\n var field = _avatarFields[_i];\n\n if (account[field] && String(account[field]).trim()) {\n console.log(\"KbChatMain - \\u627E\\u5230\\u5934\\u50CF\\u5B57\\u6BB5 \".concat(field, \":\"), account[field]);\n return String(account[field]).trim();\n }\n }\n }\n\n console.log('KbChatMain - 使用默认头像');\n return _logo.default;\n },\n // 是否有真实头像(区别于默认 fallback 图片)\n hasUserAvatar: function hasUserAvatar() {\n return this.userAvatar && this.userAvatar !== _logo.default;\n },\n // 无头像时的文字头像(与 zjkjAvatar 一致:取名字最后两个字)\n userAvatarText: function userAvatarText() {\n var name = this.userName || '';\n return name ? name.substring(name.length - 2) : '';\n },\n // 可删除的消息 id(仅后端已落库、带回 id 的消息可删)\n deletableMsgIds: function deletableMsgIds() {\n return this.messages.filter(function (m) {\n return m.id;\n }).map(function (m) {\n return m.id;\n });\n },\n // 是否已全选\n isAllSelected: function isAllSelected() {\n return this.deletableMsgIds.length > 0 && this.selectedMsgIds.length === this.deletableMsgIds.length;\n }\n },\n watch: {\n companyId: function companyId() {\n this.initDone = false;\n this.finishStream();\n this.loadSuggestedQuestions();\n this.checkKbDocuments();\n this.initSession();\n },\n userno: function userno() {\n this.initDone = false;\n this.finishStream();\n this.activeSessionId = '';\n this.messages = [];\n this.initSession();\n },\n // 移动端进入时默认先显示会话列表页(仿 AI 助理)\n isMobileActive: function isMobileActive(val) {\n if (val) {\n // 无资料时遮罩只在详情页可见(列表页 .kb-chat-main 被隐藏),故保持停在详情页\n this.showH5List = !this.noDocument;\n this.mobileAction = null;\n }\n }\n },\n mounted: function mounted() {\n var _this = this;\n\n // 独立使用(父级未传 isMobile)时自检窗口宽度\n this._onResize = this.handleResize;\n window.addEventListener('resize', this._onResize, {\n passive: true\n }); // Android WebView 兼容:在 document 级别监听 touchmove/touchend,\n // 避免可滚动子元素消费事件后不冒泡到父级(参考 AI 助理)\n\n this._swipeMoveHandler = this._onSwipeMove;\n this._swipeEndHandler = this._onSwipeEnd;\n this._popStateHandler = this._onPopState;\n document.addEventListener('touchmove', this._swipeMoveHandler, {\n passive: true\n });\n document.addEventListener('touchend', this._swipeEndHandler, {\n passive: true\n }); // 拦截 Android 系统返回手势:通过 history.pushState + popstate\n\n window.addEventListener('popstate', this._popStateHandler); // 移动端首屏为会话列表页,推入一条历史状态供系统返回拦截\n\n if (this.isMobileActive) {\n history.pushState({\n page: 'kbChatList'\n }, '');\n }\n\n this.initSession();\n this.checkKbDocuments(); // v-show 隐藏期间加载的历史消息无法滚动(scrollHeight 为 0),\n // 监听组件由隐藏变为可见时,滚动到最新问答;\n // 同时每次变为可见时刷新\"猜您想问\"(管理端可能刚更新过)\n\n if (typeof IntersectionObserver !== 'undefined' && this.$el) {\n this._visibilityObserver = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) {\n return e.isIntersecting;\n })) {\n _this.loadSuggestedQuestions(); // 管理端可能刚上传/删除了文档,重新校验遮罩状态\n\n\n _this.checkKbDocuments();\n\n if (_this.messages.length) {\n _this.$nextTick(function () {\n return _this.scrollToBottom();\n });\n }\n }\n });\n\n this._visibilityObserver.observe(this.$el);\n } else {\n this.loadSuggestedQuestions();\n }\n },\n beforeDestroy: function beforeDestroy() {\n if (this._visibilityObserver) {\n this._visibilityObserver.disconnect();\n\n this._visibilityObserver = null;\n }\n\n if (this._onResize) {\n window.removeEventListener('resize', this._onResize);\n }\n\n if (this._swipeMoveHandler) {\n document.removeEventListener('touchmove', this._swipeMoveHandler);\n }\n\n if (this._swipeEndHandler) {\n document.removeEventListener('touchend', this._swipeEndHandler);\n }\n\n if (this._popStateHandler) {\n window.removeEventListener('popstate', this._popStateHandler);\n }\n\n this.streamRequestId++;\n this.closeEventSource();\n this.clearTypeTimer();\n this.onMsgTouchEnd();\n this.contentCache = {};\n },\n methods: {\n // 独立使用时自检移动端(父级已传 isMobile 时忽略)\n handleResize: function handleResize() {\n var now = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n this.localIsMobile = now;\n },\n renderMarkdown: function renderMarkdown(text) {\n if (!text) return '';\n\n try {\n return md.render(text);\n } catch (err) {\n return this.escapeHtml(text);\n }\n },\n // 与 ChatDetail 一致的增量渲染:完整行走 markdown、未完成尾行转义拼接,\n // 借助 markdown-it breaks:true 实现逐行浮现效果。\n renderStreaming: function renderStreaming(text, msgKey) {\n if (!text) return '';\n var cache = this.contentCache[msgKey];\n var lines = text.split('\\n');\n var lastLineIdx = lines.length - 1;\n\n if (!cache) {\n try {\n var html = md.render(text);\n this.$set(this.contentCache, msgKey, {\n renderedLines: lastLineIdx,\n html: html,\n fullText: text\n });\n return html;\n } catch (_unused) {\n return this.escapeHtml(text);\n }\n }\n\n if (text === cache.fullText) return cache.html;\n cache.fullText = text; // 行数没变,仅末尾追加字符,直接拼接尾行\n\n if (lastLineIdx === cache.renderedLines) {\n return cache.html + this.escapeHtml(lines[lastLineIdx]);\n } // 行数变少/文本回退截断,全量重渲染\n\n\n if (lastLineIdx < cache.renderedLines) {\n try {\n var fullLines = lines.slice(0, lastLineIdx);\n var newBaseHtml = '';\n\n if (fullLines.length > 0) {\n newBaseHtml = md.render(fullLines.join('\\n'));\n }\n\n cache.html = newBaseHtml;\n cache.renderedLines = lastLineIdx;\n\n var _lastLine = this.escapeHtml(lines[lastLineIdx]);\n\n return cache.html + (_lastLine || '');\n } catch (_unused2) {\n return this.escapeHtml(text);\n }\n } // 新增完整行\n\n\n if (cache.renderedLines < lastLineIdx) {\n var allFullLines = lines.slice(0, lastLineIdx);\n var _newBaseHtml = '';\n\n if (allFullLines.length > 0) {\n _newBaseHtml = md.render(allFullLines.join('\\n'));\n }\n\n cache.html = _newBaseHtml;\n cache.renderedLines = lastLineIdx;\n }\n\n var lastLine = this.escapeHtml(lines[lastLineIdx]);\n cache.renderedLines = lastLineIdx;\n return cache.html + (lastLine || '');\n },\n escapeHtml: function escapeHtml(text) {\n if (!text) return '';\n var map = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(text).replace(/[&<>\"']/g, function (c) {\n return map[c];\n });\n },\n autoResizeTextarea: function autoResizeTextarea() {\n var ta = this.$refs.taRef;\n if (!ta) return; // 移动端从单行起增高(与 38px 最小高度一致),PC 保持 100px 基准\n\n var baseH = this.isMobileActive ? 38 : 100;\n var maxH = this.isMobileActive ? 120 : 180;\n ta.style.height = \"\".concat(baseH, \"px\");\n var realH = ta.scrollHeight;\n\n if (realH > baseH) {\n ta.style.height = \"\".concat(Math.min(realH, maxH), \"px\");\n }\n },\n newLine: function newLine(e) {\n var _this2 = this;\n\n e.preventDefault();\n var ta = this.$refs.taRef;\n var start = ta.selectionStart;\n var end = ta.selectionEnd;\n this.inputText = this.inputText.slice(0, start) + '\\n' + this.inputText.slice(end);\n this.$nextTick(function () {\n ta.selectionStart = ta.selectionEnd = start + 1;\n\n _this2.autoResizeTextarea();\n });\n },\n\n /**\n * 加载\"猜您想问\"问题(按公司,所有用户可见)\n */\n loadSuggestedQuestions: function loadSuggestedQuestions() {\n var _this3 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {\n var list;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n if (_this3.companyId) {\n _context.next = 3;\n break;\n }\n\n _this3.suggestedQuestions = [];\n return _context.abrupt(\"return\");\n\n case 3:\n _context.prev = 3;\n _context.next = 6;\n return (0, _knowledgeBase.getSuggestedQuestions)(_this3.companyId);\n\n case 6:\n _context.t0 = _context.sent;\n\n if (_context.t0) {\n _context.next = 9;\n break;\n }\n\n _context.t0 = [];\n\n case 9:\n list = _context.t0;\n _this3.suggestedQuestions = list.map(function (q) {\n return q.questionText;\n });\n _context.next = 16;\n break;\n\n case 13:\n _context.prev = 13;\n _context.t1 = _context[\"catch\"](3);\n console.error('加载猜您想问失败', _context.t1);\n\n case 16:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, null, [[3, 13]]);\n }))();\n },\n\n /**\n * 检查当前公司是否有「已索引」的知识库文档,无可用资料时展示遮罩提示\n * 仅已索引(indexStatus === 1)的文档才能被检索,未索引/索引中/索引失败都不算\n */\n checkKbDocuments: function checkKbDocuments() {\n var _this4 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var data, isEmpty;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n if (_this4.companyId) {\n _context2.next = 3;\n break;\n }\n\n _this4.noDocument = false;\n return _context2.abrupt(\"return\");\n\n case 3:\n _context2.prev = 3;\n _context2.next = 6;\n return (0, _knowledgeBase.getFileList)({\n companyId: _this4.companyId,\n indexStatus: 1,\n pageNum: 1,\n pageSize: 1\n });\n\n case 6:\n data = _context2.sent;\n isEmpty = (data && data.total || 0) === 0;\n _this4.noDocument = isEmpty; // 无资料时把会话固定回第一个\n\n if (isEmpty) {\n _this4.lockFirstSession(); // 无资料遮罩位于聊天详情页的 .kb-chat-main 内,而移动端首屏是会话列表页\n // (h5-list-mode 下 .kb-chat-main 被 display:none 隐藏),若停留在列表页\n // 则遮罩永远不可见;故无资料时强制切到详情页把遮罩展示出来\n\n\n if (_this4.isMobileActive) {\n _this4.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n }\n }\n\n _context2.next = 16;\n break;\n\n case 12:\n _context2.prev = 12;\n _context2.t0 = _context2[\"catch\"](3);\n // 查询失败(如无管理权限)时不展示遮罩,避免误拦截\n console.error('检查知识库文档失败', _context2.t0);\n _this4.noDocument = false;\n\n case 16:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, null, [[3, 12]]);\n }))();\n },\n initSession: function initSession() {\n var _this5 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {\n var list;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n if (_this5.companyId) {\n _context3.next = 2;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 2:\n if (!(_this5.initDone && (_this5.messages.length || _this5.streaming || _this5.inputText))) {\n _context3.next = 4;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 4:\n _this5.initDone = false;\n _context3.prev = 5;\n _context3.next = 8;\n return (0, _kbChat.getKbSessionList)(_this5.companyId);\n\n case 8:\n _context3.t0 = _context3.sent;\n\n if (_context3.t0) {\n _context3.next = 11;\n break;\n }\n\n _context3.t0 = [];\n\n case 11:\n list = _context3.t0;\n _this5.sessionList = list;\n\n if (!(list.length > 0)) {\n _context3.next = 19;\n break;\n }\n\n // 只有 1 个会话且无消息(新用户首次进入自动生成的空会话)时,\n // 默认折叠侧边栏只显示聊天主界面;在 selectSession 前同步设置,\n // 避免「先展开再合住」的闪烁\n _this5.sidebarCollapsed = list.length === 1 && !list[0].lastContent;\n _context3.next = 17;\n return _this5.selectSession(list[0].sessionId);\n\n case 17:\n _context3.next = 21;\n break;\n\n case 19:\n _context3.next = 21;\n return _this5.autoCreateSession();\n\n case 21:\n _context3.next = 28;\n break;\n\n case 23:\n _context3.prev = 23;\n _context3.t1 = _context3[\"catch\"](5);\n console.error('获取会话列表失败,尝试自动创建', _context3.t1);\n _context3.next = 28;\n return _this5.autoCreateSession();\n\n case 28:\n _context3.prev = 28;\n _this5.initDone = true;\n return _context3.finish(28);\n\n case 31:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _callee3, null, [[5, 23, 28, 31]]);\n }))();\n },\n autoCreateSession: function autoCreateSession() {\n var _this6 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {\n var data;\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n if (_this6.companyId) {\n _context4.next = 3;\n break;\n }\n\n console.warn('[KbChat] 无法创建会话:companyId 为空');\n return _context4.abrupt(\"return\");\n\n case 3:\n console.log('[KbChat] 创建会话,companyId:', _this6.companyId);\n _context4.prev = 4;\n _context4.next = 7;\n return (0, _kbChat.createKbSession)(_this6.companyId);\n\n case 7:\n data = _context4.sent;\n console.log('[KbChat] 会话创建成功,sessionId:', data.sessionId);\n\n _this6.finishStream();\n\n _this6.activeSessionId = data.sessionId;\n _this6.streamRequestId++;\n _this6.messages = [];\n _this6.sessionList = [{\n sessionId: data.sessionId,\n title: '',\n lastContent: ''\n }].concat(_this6.sessionList || []); // 新建会话时保持侧边栏展开,方便继续切换会话\n\n _this6.sidebarCollapsed = false;\n\n _this6.$nextTick(function () {\n return _this6.scrollToBottom();\n });\n\n _context4.next = 21;\n break;\n\n case 18:\n _context4.prev = 18;\n _context4.t0 = _context4[\"catch\"](4);\n console.error('自动创建会话失败', _context4.t0);\n\n case 21:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _callee4, null, [[4, 18]]);\n }))();\n },\n\n /** 新建会话(豆包式「新对话」) */\n handleNewSession: function handleNewSession() {\n var _this7 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee5() {\n return regeneratorRuntime.wrap(function _callee5$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n if (!_this7.noDocument) {\n _context5.next = 2;\n break;\n }\n\n return _context5.abrupt(\"return\");\n\n case 2:\n if (_this7.streaming) _this7.handleStop();\n _context5.next = 5;\n return _this7.autoCreateSession();\n\n case 5:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _callee5);\n }))();\n },\n\n /** 切换会话 */\n handleSelectSession: function handleSelectSession(item) {\n var _this8 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee6() {\n return regeneratorRuntime.wrap(function _callee6$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context6.next = 2;\n break;\n }\n\n return _context6.abrupt(\"return\");\n\n case 2:\n if (!_this8.noDocument) {\n _context6.next = 5;\n break;\n }\n\n _this8.lockFirstSession();\n\n return _context6.abrupt(\"return\");\n\n case 5:\n if (!(item.sessionId === _this8.activeSessionId && _this8.messages.length)) {\n _context6.next = 8;\n break;\n }\n\n if (_this8.isMobileActive) {\n _this8.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n return _context6.abrupt(\"return\");\n\n case 8:\n _context6.next = 10;\n return _this8.selectSession(item.sessionId);\n\n case 10:\n // 移动端:选中后跳到聊天详情页\n if (_this8.isMobileActive) {\n _this8.showH5List = false; // 推入历史状态,使 Android 系统返回手势先触发 popstate 被拦截\n\n history.pushState({\n page: 'kbChatDetail'\n }, ''); // 列表页时聊天区被 display:none,scrollHeight 为 0 导致上面的滚动无效;\n // 切到详情页、容器可见后需再滚一次,默认定位到最新消息\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n case 11:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _callee6);\n }))();\n },\n\n /** 返回宿主 App 原生页面(参考 AI 助理 handleNativeBack) */\n handleNativeBack: function handleNativeBack() {\n if (this.$hybrid && typeof this.$hybrid.closePage === 'function') {\n this.$hybrid.closePage();\n } else if (window.history.length > 1) {\n window.history.back();\n }\n },\n\n /** 会话列表页返回按钮:返回原生页面 */\n handleSidebarBack: function handleSidebarBack() {\n this.handleNativeBack();\n },\n // ── H5 侧滑手势(参考 AI 助理)──\n // 从左/右屏幕边缘起手横滑触发返回,避免页面内部横向滚动被误判\n onSwipeTouchStart: function onSwipeTouchStart(e) {\n if (!this.isMobileActive) return;\n var t = e.touches[0];\n this._swipeStartX = t.clientX;\n this._swipeStartY = t.clientY;\n this._swipeHandled = false;\n var edge = Math.min(56, Math.round(window.innerWidth * 0.09));\n this._swipeFromLeftEdge = t.clientX <= edge;\n this._swipeFromRightEdge = t.clientX >= window.innerWidth - edge;\n this._swipeFromEdge = this._swipeFromLeftEdge || this._swipeFromRightEdge;\n },\n _swipeShouldTrigger: function _swipeShouldTrigger(diffX, diffY) {\n if (!this._swipeFromEdge) return false; // 从左边缘 → 右滑;从右边缘 → 左滑\n\n var directionOk = this._swipeFromLeftEdge && diffX >= 60 || this._swipeFromRightEdge && diffX <= -60;\n if (!directionOk) return false; // 水平距离明显大于垂直距离(排除上下滚动干扰)\n\n return Math.abs(diffX) >= Math.abs(diffY) * 2;\n },\n _onSwipeMove: function _onSwipeMove(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.touches || !e.touches.length) return;\n var diffX = e.touches[0].clientX - this._swipeStartX;\n var diffY = e.touches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n // touchend 兜底,兼容不支持 touchmove 的场景\n _onSwipeEnd: function _onSwipeEnd(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.changedTouches || !e.changedTouches.length) return;\n var diffX = e.changedTouches[0].clientX - this._swipeStartX;\n var diffY = e.changedTouches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n\n /** 侧滑触发:列表页 → 返回原生页面;详情页 → 回会话列表页 */\n _triggerSwipeAction: function _triggerSwipeAction() {\n if (this.showH5List) {\n this.handleNativeBack();\n } else {\n this.handleMobileBack();\n }\n },\n\n /**\n * 拦截 Android 系统返回手势(history.pushState + popstate)\n * WebView 先回退历史栈触发 popstate,这里拦截后导航到列表页,避免 Activity 被直接关闭\n */\n _onPopState: function _onPopState() {\n if (!this.isMobileActive) return;\n\n if (!this.showH5List) {\n // 详情页 → 返回会话列表页\n this.showH5List = true;\n history.pushState({\n page: 'kbChatList'\n }, '');\n } else {\n // 列表页 → 直接关闭 WebView 返回原生页面\n this.handleNativeBack();\n }\n },\n\n /** 移动端详情页返回:无资料遮罩时返回原生页面;否则回到会话列表页 */\n handleMobileBack: function handleMobileBack() {\n // 无资料遮罩期间无法进入会话,返回到宿主 App 原生页面\n if (this.noDocument) {\n this.handleNativeBack();\n return;\n } // 正常情况:回到会话列表页\n\n\n this.showH5List = true;\n },\n\n /** 无资料遮罩期间:把当前会话固定回列表中的第一个 */\n lockFirstSession: function lockFirstSession() {\n var first = this.sessionList[0];\n\n if (first && first.sessionId && first.sessionId !== this.activeSessionId) {\n this.selectSession(first.sessionId);\n }\n },\n\n /** 会话项「…」菜单:重命名 / 删除 */\n handleSessionMenu: function handleSessionMenu(cmd, item) {\n if (!item || !item.sessionId) return;\n\n if (cmd === 'rename') {\n this.handleRenameSession(item);\n } else if (cmd === 'delete') {\n this.handleDeleteSession(item);\n }\n },\n // ==================== 移动端会话操作单 ====================\n openMobileAction: function openMobileAction(item) {\n if (this.noDocument) return;\n this.mobileAction = item;\n },\n doMobileRename: function doMobileRename(item) {\n this.mobileAction = null;\n this.handleRenameSession(item);\n },\n doMobileDelete: function doMobileDelete(item) {\n this.mobileAction = null;\n this.handleDeleteSession(item);\n },\n\n /** 重命名会话 */\n handleRenameSession: function handleRenameSession(item) {\n var _this9 = this;\n\n if (!item || !item.sessionId) return;\n this.$prompt('请输入新的会话名称', '重命名会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n inputValue: item.title || '',\n inputPattern: /\\S+/,\n inputErrorMessage: '名称不能为空'\n }).then(function (_ref) {\n var value = _ref.value;\n var title = (value || '').trim();\n if (!title) return;\n return (0, _kbChat.renameKbSession)(item.sessionId, title).then(function () {\n item.title = title;\n\n _this9.$message.success('重命名成功');\n });\n }).catch(function (err) {\n // 取消或校验失败,不提示错误\n if (err && err !== 'cancel' && err !== 'close') {\n console.error('重命名失败', err);\n\n _this9.$message.error('重命名失败');\n }\n });\n },\n\n /** 删除会话 */\n handleDeleteSession: function handleDeleteSession(item) {\n var _this10 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {\n return regeneratorRuntime.wrap(function _callee7$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context7.next = 2;\n break;\n }\n\n return _context7.abrupt(\"return\");\n\n case 2:\n if (!(_this10.sessionList.length <= 1)) {\n _context7.next = 5;\n break;\n }\n\n _this10.$message.warning('至少保留一个会话');\n\n return _context7.abrupt(\"return\");\n\n case 5:\n _context7.prev = 5;\n _context7.next = 8;\n return _this10.$confirm('删除后无法恢复,确定删除该会话吗?', '删除会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 8:\n _context7.next = 13;\n break;\n\n case 10:\n _context7.prev = 10;\n _context7.t0 = _context7[\"catch\"](5);\n return _context7.abrupt(\"return\");\n\n case 13:\n _context7.prev = 13;\n _context7.next = 16;\n return (0, _kbChat.deleteKbSession)(item.sessionId);\n\n case 16:\n _this10.sessionList = (_this10.sessionList || []).filter(function (s) {\n return s.sessionId !== item.sessionId;\n });\n\n if (!(_this10.activeSessionId === item.sessionId)) {\n _context7.next = 28;\n break;\n }\n\n _this10.finishStream();\n\n _this10.activeSessionId = '';\n _this10.messages = [];\n\n if (!(_this10.sessionList.length > 0)) {\n _context7.next = 26;\n break;\n }\n\n _context7.next = 24;\n return _this10.selectSession(_this10.sessionList[0].sessionId);\n\n case 24:\n _context7.next = 28;\n break;\n\n case 26:\n _context7.next = 28;\n return _this10.autoCreateSession();\n\n case 28:\n _this10.$message.success('删除成功');\n\n _context7.next = 35;\n break;\n\n case 31:\n _context7.prev = 31;\n _context7.t1 = _context7[\"catch\"](13);\n console.error('删除会话失败', _context7.t1);\n\n _this10.$message.error('删除失败');\n\n case 35:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _callee7, null, [[5, 10], [13, 31]]);\n }))();\n },\n\n /** 会话列表时间格式化:今天显示时分,更早显示月日 */\n formatTime: function formatTime(time) {\n if (!time) return '';\n var d = new Date(time);\n if (isNaN(d.getTime())) return '';\n var now = new Date();\n var sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();\n\n var pad = function pad(n) {\n return n < 10 ? '0' + n : '' + n;\n };\n\n if (sameDay) {\n return pad(d.getHours()) + ':' + pad(d.getMinutes());\n }\n\n return pad(d.getMonth() + 1) + '-' + pad(d.getDate());\n },\n\n /** 后端消息 → 前端消息对象(保留 id/msgKey,删除功能需要) */\n mapMessage: function mapMessage(m, i) {\n var thinking = m.thinking || m.reasoning || m.reasoning_result || m.reasoningContent || m.reasoning_content || m.thinkingContent || m.thinking_content || '';\n return {\n id: m.id,\n msgKey: m.msgKey || m.msg_key || '',\n role: m.role,\n content: m.content || '',\n thinking: thinking,\n _key: m.createTime ? 'msg_' + m.createTime + '_' + i : 'msg_' + Date.now() + '_' + Math.random()\n };\n },\n selectSession: function selectSession(sessionId) {\n var _this11 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee8() {\n var raw, msgs;\n return regeneratorRuntime.wrap(function _callee8$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n if (_this11.streaming) _this11.handleStop();\n\n _this11.exitSelectMode();\n\n _this11.msgAction = null;\n _this11.activeSessionId = sessionId;\n _this11.messages = [];\n _this11.streamMsg = null;\n _context8.prev = 6;\n _context8.next = 9;\n return (0, _kbChat.getKbSessionMessages)(sessionId);\n\n case 9:\n raw = _context8.sent;\n msgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || [];\n console.log('[KbChat] selectSession 回读', {\n sessionId: sessionId,\n rawIsArray: Array.isArray(raw),\n rawKeys: raw && !Array.isArray(raw) ? Object.keys(raw) : null,\n count: msgs.length,\n list: msgs.map(function (m) {\n return {\n id: m.id,\n role: m.role,\n content: String(m.content || '').slice(0, 25)\n };\n })\n });\n _this11.messages = _this11.dedupeMessages(msgs).map(function (m, i) {\n return _this11.mapMessage(m, i);\n });\n\n _this11.$nextTick(function () {\n return _this11.scrollToBottom();\n });\n\n _context8.next = 19;\n break;\n\n case 16:\n _context8.prev = 16;\n _context8.t0 = _context8[\"catch\"](6);\n console.error('加载消息失败', _context8.t0);\n\n case 19:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _callee8, null, [[6, 16]]);\n }))();\n },\n\n /**\n * 后端回读消息去重。\n * 后端存在同一问答对被重复落库的情况(同一会话里出现两套相同的「问+答」),\n * 直接渲染会让用户看到重复问答。这里按「问答对」为单位去重:\n * 把消息切成组(用户问 + 其后的 AI 答为一组),整组内容完全相同的只保留第一次出现的。\n * 相比「只和上一条比」的相邻去重,能覆盖组与组之间被隔开、或 content 有空白差异的情况。\n */\n dedupeMessages: function dedupeMessages(msgs) {\n var _this12 = this;\n\n // 先切组:以 user 消息为一个新组的起点,其后连续的 assistant 归入同组\n var groups = [];\n msgs.forEach(function (m) {\n var role = _this12.normRole(m.role);\n\n if (role === 'user' || !groups.length) {\n groups.push([m]);\n } else {\n groups[groups.length - 1].push(m);\n }\n });\n var seen = {};\n var out = [];\n groups.forEach(function (group) {\n var signature = group.map(function (m) {\n return _this12.normRole(m.role) + \"\\x01\" + String(m.content || '').replace(/\\s+/g, '');\n }).join(\"\\x02\");\n if (signature && seen[signature]) return;\n if (signature) seen[signature] = true;\n out.push.apply(out, group);\n });\n return out;\n },\n\n /**\n * 回读当前会话消息,补全后端持久化后生成的 id。\n * 流式回答期间消息只存在于本地数组(无 id),无法按 id 删除;\n * 流式结束后调用一次即可拿到 id。已全部有 id 时跳过,避免多余请求。\n *\n * 注意:不能直接用后端数组整体覆盖 this.messages。后端回读可能包含重复/多余记录\n * (如同一会话里用户消息被落库两次),若整体覆盖会把重复消息直接渲染出来。\n * 这里以本地数组为基准,只用后端记录按「角色 + 内容」顺序回填 id,保证界面条数不变。\n */\n syncMessageIds: function syncMessageIds() {\n var _this13 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee9() {\n var raw, backendMsgs, cursor;\n return regeneratorRuntime.wrap(function _callee9$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n if (!(!_this13.activeSessionId || _this13.streaming)) {\n _context9.next = 2;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 2:\n if (!(_this13.messages.length && _this13.messages.every(function (m) {\n return m.id;\n }))) {\n _context9.next = 4;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 4:\n _context9.prev = 4;\n _context9.next = 7;\n return (0, _kbChat.getKbSessionMessages)(_this13.activeSessionId);\n\n case 7:\n raw = _context9.sent;\n backendMsgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || []; // 按顺序把本地消息与后端记录配对(角色一致且内容匹配),只补 id/msgKey\n\n cursor = 0;\n\n _this13.messages.forEach(function (m) {\n if (m.id) return;\n\n var role = _this13.normRole(m.role);\n\n for (var i = cursor; i < backendMsgs.length; i++) {\n var b = backendMsgs[i];\n\n if (_this13.normRole(b.role) === role && String(b.content || '').trim() === String(m.content || '').trim()) {\n m.id = b.id;\n if (!m.msgKey) m.msgKey = b.msgKey || b.msg_key || '';\n cursor = i + 1;\n return;\n }\n }\n });\n\n _context9.next = 16;\n break;\n\n case 13:\n _context9.prev = 13;\n _context9.t0 = _context9[\"catch\"](4);\n console.error('同步消息 id 失败', _context9.t0);\n\n case 16:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _callee9, null, [[4, 13]]);\n }))();\n },\n // ==================== 消息多选 / 删除 ====================\n\n /** 进入多选模式(preselectMsg 传入时默认勾选该条所在的整组问答,对应豆包式「删除」入口) */\n enterSelectMode: function enterSelectMode(preselectMsg) {\n var _this14 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee10() {\n var preselectId, idx;\n return regeneratorRuntime.wrap(function _callee10$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n if (!(_this14.streaming || !_this14.messages.length)) {\n _context10.next = 2;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 2:\n // 点击条的后端持久化 id(模板上「删除」按钮仅在 msg.id 存在时渲染,故必有值)\n preselectId = preselectMsg ? preselectMsg.id : null; // 先回读消息确保每条都有 id(刚流式完成的消息本地没有 id)。\n // 回读只补 id、不重建数组,定位可用 id 或对象引用。\n\n _context10.next = 5;\n return _this14.syncMessageIds();\n\n case 5:\n _this14.selectMode = true;\n _this14.selectedMsgIds = [];\n idx = preselectId != null ? _this14.messages.findIndex(function (m) {\n return m.id === preselectId;\n }) : -1;\n if (idx === -1 && preselectMsg) idx = _this14.messages.indexOf(preselectMsg);\n\n if (idx !== -1) {\n // 默认勾选整组问答(用户问 + AI 答),便于一键删除\n _this14.selectedMsgIds = _this14.getQaPairIdsAt(idx);\n } // 滚动定位:让默认勾选的整组问答都进入视野(长回答会把用户问顶出屏幕)\n\n\n _this14.scrollSelectedIntoView();\n\n case 11:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _callee10);\n }))();\n },\n\n /**\n * 把首个被选中的消息行滚动到容器内可见。\n * 选中行是「用户问 + AI 答」中的靠前一条,滚到它即能同时看到整组。\n * 若靠前那条已在视野内则不动,避免无谓跳动。\n */\n scrollSelectedIntoView: function scrollSelectedIntoView() {\n var _this15 = this;\n\n this.$nextTick(function () {\n var container = _this15.$refs.messagesRef;\n if (!container) return;\n var rows = container.querySelectorAll('.msg-row');\n if (!rows.length) return; // 找到第一个处于选中态、且带 id 的行(与 selectedMsgIds 顺序无关,取 DOM 顺序)\n\n var target = null;\n\n for (var i = 0; i < rows.length; i++) {\n var box = rows[i].querySelector('.msg-check-box.checked');\n\n if (box) {\n target = rows[i];\n break;\n }\n }\n\n if (!target) return;\n var cRect = container.getBoundingClientRect();\n var tRect = target.getBoundingClientRect(); // 目标行顶部已在容器可视区内则无需滚动\n\n if (tRect.top >= cRect.top && tRect.top < cRect.bottom) return; // 滚动到目标行顶部(留 8px 间距)\n\n container.scrollTop += tRect.top - cRect.top - 8;\n });\n },\n exitSelectMode: function exitSelectMode() {\n this.selectMode = false;\n this.selectedMsgIds = [];\n },\n isMsgSelected: function isMsgSelected(msg) {\n return !!msg.id && this.selectedMsgIds.indexOf(msg.id) !== -1;\n },\n\n /** 归一化 role,兼容后端可能返回的大小写/空白 */\n normRole: function normRole(role) {\n return String(role == null ? '' : role).trim().toLowerCase();\n },\n\n /**\n * 取与下标 idx 处消息成对的另一条:用户问 ↔ AI 答视为一组问答。\n * 只认紧邻的一条(问答对在列表里必然相邻),相邻非同组则不配对。\n * 不依赖 role 的具体拼写(user/assistant 的大小写、空白均容错)。\n */\n getQaPartnerAt: function getQaPartnerAt(idx) {\n if (idx < 0 || idx >= this.messages.length) return null;\n var cur = this.messages[idx];\n if (!cur) return null;\n var role = this.normRole(cur.role);\n if (role !== 'user' && role !== 'assistant') return null; // 只认紧邻的那一条:问答对在列表里必然相邻。\n // 若相邻消息不是异角色(例如回答已被删除),说明当前消息已无配对,\n // 不能继续向前/向后扫描,否则会误勾到上一个问答的回答。\n\n var neighbor = role === 'user' ? this.messages[idx + 1] : this.messages[idx - 1];\n if (!neighbor) return null;\n var neighborRole = this.normRole(neighbor.role);\n var expect = role === 'user' ? 'assistant' : 'user';\n return neighborRole === expect ? neighbor : null;\n },\n\n /** 下标 idx 处消息所在问答对的可删 id 列表(用户问 + AI 答) */\n getQaPairIdsAt: function getQaPairIdsAt(idx) {\n var ids = [];\n var cur = this.messages[idx];\n if (cur && cur.id) ids.push(cur.id);\n var partner = this.getQaPartnerAt(idx);\n if (partner && partner.id && ids.indexOf(partner.id) === -1) ids.push(partner.id);\n return ids;\n },\n\n /**\n * 勾选单条消息:勾上时连带把问答对里另一条一起勾上;\n * 取消时只取消当前这条,从而支持「先整组勾选、再单独取消一条」的单条选择。\n */\n toggleSelectMessage: function toggleSelectMessage(msg) {\n var _this16 = this;\n\n if (!msg.id) return;\n var i = this.selectedMsgIds.indexOf(msg.id);\n\n if (i === -1) {\n var idx = this.messages.indexOf(msg);\n var pairIds = idx === -1 ? [msg.id] : this.getQaPairIdsAt(idx);\n pairIds.forEach(function (id) {\n if (_this16.selectedMsgIds.indexOf(id) === -1) _this16.selectedMsgIds.push(id);\n });\n } else {\n this.selectedMsgIds.splice(i, 1);\n }\n },\n toggleSelectAll: function toggleSelectAll() {\n var ids = this.deletableMsgIds;\n this.selectedMsgIds = this.selectedMsgIds.length === ids.length ? [] : ids.slice();\n },\n\n /** 多选模式下点击整行即切换选中 */\n onMsgRowClick: function onMsgRowClick(msg) {\n // 长按弹出菜单后浏览器还会补一次 click,短暂屏蔽避免误触\n if (Date.now() < this._ignoreClickUntil) return;\n if (this.selectMode) this.toggleSelectMessage(msg);\n },\n\n /** 移动端长按气泡弹出消息操作单(豆包式:复制 / 删除 / 多选) */\n onMsgTouchStart: function onMsgTouchStart(msg) {\n var _this17 = this;\n\n if (!this.isMobileActive || this.selectMode) return;\n this._longPressMsg = msg;\n clearTimeout(this._longPressTimer);\n this._longPressTimer = setTimeout(function () {\n _this17._longPressTimer = null; // 用户正在长按选中文本(复制用),此时不弹菜单,避免抢走选择行为\n\n var sel = window.getSelection && window.getSelection();\n if (sel && String(sel).trim()) return;\n\n _this17.openMsgAction(_this17._longPressMsg);\n }, 500);\n },\n onMsgTouchEnd: function onMsgTouchEnd() {\n if (this._longPressTimer) {\n clearTimeout(this._longPressTimer);\n this._longPressTimer = null;\n }\n },\n openMsgAction: function openMsgAction(msg) {\n this.msgAction = msg; // 长按抬手后浏览器可能补发 click,短暂屏蔽,避免刚弹出的菜单被立即关闭\n\n this._ignoreClickUntil = Date.now() + 350;\n },\n closeMsgAction: function closeMsgAction() {\n if (Date.now() < this._ignoreClickUntil) return;\n this.msgAction = null;\n },\n\n /** 移动端消息操作单:复制 / 删除(删除进入多选模式,与豆包一致) */\n doMsgAction: function doMsgAction(action) {\n var msg = this.msgAction;\n this.msgAction = null;\n if (!msg) return;\n\n if (action === 'copy') {\n this.copyText(msg.content);\n } else if (action === 'delete') {\n this.enterSelectMode(msg);\n }\n },\n\n /** 批量删除选中的消息 */\n handleDeleteSelected: function handleDeleteSelected() {\n var _this18 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee11() {\n var ids;\n return regeneratorRuntime.wrap(function _callee11$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n if (!(!_this18.selectedMsgIds.length || !_this18.activeSessionId)) {\n _context11.next = 2;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 2:\n ids = _this18.selectedMsgIds.slice();\n _context11.prev = 3;\n _context11.next = 6;\n return _this18.$confirm(\"\\u5220\\u9664\\u540E\\u65E0\\u6CD5\\u6062\\u590D\\uFF0C\\u786E\\u5B9A\\u5220\\u9664\\u9009\\u4E2D\\u7684 \".concat(ids.length, \" \\u6761\\u6D88\\u606F\\u5417\\uFF1F\"), '删除消息', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 6:\n _context11.next = 11;\n break;\n\n case 8:\n _context11.prev = 8;\n _context11.t0 = _context11[\"catch\"](3);\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.prev = 11;\n _context11.next = 14;\n return (0, _kbChat.deleteKbMessages)(_this18.activeSessionId, ids);\n\n case 14:\n _this18.messages = _this18.messages.filter(function (m) {\n return ids.indexOf(m.id) === -1;\n });\n\n _this18.exitSelectMode();\n\n _this18.$message.success('删除成功');\n\n _this18.refreshSessionList();\n\n _context11.next = 24;\n break;\n\n case 20:\n _context11.prev = 20;\n _context11.t1 = _context11[\"catch\"](11);\n console.error('批量删除消息失败', _context11.t1);\n\n _this18.$message.error('删除失败');\n\n case 24:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _callee11, null, [[3, 8], [11, 20]]);\n }))();\n },\n handleResetSession: function handleResetSession() {\n var _this19 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee12() {\n return regeneratorRuntime.wrap(function _callee12$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n if (!_this19.activeSessionId) {\n _context12.next = 8;\n break;\n }\n\n _context12.prev = 1;\n _context12.next = 4;\n return (0, _kbChat.closeKbSession)(_this19.activeSessionId);\n\n case 4:\n _context12.next = 8;\n break;\n\n case 6:\n _context12.prev = 6;\n _context12.t0 = _context12[\"catch\"](1);\n\n case 8:\n _context12.next = 10;\n return _this19.autoCreateSession();\n\n case 10:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _callee12, null, [[1, 6]]);\n }))();\n },\n handleSend: function handleSend(quickText) {\n var _this20 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee13() {\n var text, requestSessionId, requestId, msgKey, msgIdx, streamMsg, isCurrentStream, answerBuffer, finalContent, streamDone, url;\n return regeneratorRuntime.wrap(function _callee13$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n text = typeof quickText === 'string' ? quickText.trim() : _this20.inputText.trim();\n\n if (!(!text || !_this20.activeSessionId || _this20.streaming)) {\n _context13.next = 3;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 3:\n _this20.exitSelectMode();\n\n _this20.inputText = '';\n requestSessionId = _this20.activeSessionId;\n requestId = ++_this20.streamRequestId;\n\n _this20.clearTypeTimer();\n\n _this20.messages.push({\n role: 'user',\n content: text\n });\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n }); // 流式消息:作为 messages 中的对象(带 _key/loading),与 ChatDetail 一致\n\n\n msgKey = 'stream_' + requestId;\n msgIdx = _this20.messages.push({\n role: 'assistant',\n content: '',\n thinking: '',\n toolCalls: [],\n loading: true,\n _key: msgKey\n }) - 1;\n streamMsg = _this20.messages[msgIdx];\n _this20.streamMsg = streamMsg;\n _this20.streaming = true;\n\n isCurrentStream = function isCurrentStream() {\n return requestId === _this20.streamRequestId && requestSessionId === _this20.activeSessionId && _this20.streamMsg === streamMsg;\n }; // SSE 只把数据累积到 answerBuffer。typeTimer(30ms) 节流按「完整换行」逐行把内容\n // 推进到 msg.content:每个 tick 最多只推进「到下一个 \\n 为止」的一行,未完成的尾行\n // 保持不动;配合 renderStreaming(breaks:true) 实现「一行显示一行」。\n // 不是打字机模式(不逐字)、也不是一次性整段渲染(不会一出就是全文)。\n\n\n answerBuffer = '';\n finalContent = '';\n streamDone = false;\n _this20._typeTimer = setInterval(function () {\n if (!isCurrentStream()) return;\n var msg = _this20.messages[msgIdx];\n var source = finalContent || answerBuffer;\n var curLen = msg._prevReveal || 0;\n var nextLen = curLen; // 找到「当前已揭示进度之后」的第一条完整换行,仅推进这一行\n\n var nl = source.indexOf('\\n', curLen);\n\n if (nl !== -1) {\n nextLen = nl + 1;\n } else if (streamDone) {\n // 流结束且没有更多换行:最后一段未以换行结尾的内容一次性露出(此时即完整一行)\n nextLen = source.length;\n }\n\n if (nextLen > curLen) {\n msg._prevReveal = nextLen;\n msg.content = source.substring(0, nextLen);\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n } // 流结束且所有内容都已逐行露出 → 收尾\n\n\n if (streamDone && nextLen >= source.length) {\n _this20.finishStream(); // 后端已在此刻落库,回读一次补全消息 id(单条删除需要)\n\n\n _this20.syncMessageIds();\n }\n }, 30);\n url = (0, _kbChat.createKbStreamUrl)(_this20.activeSessionId, text);\n _this20.eventSource = new EventSource(url);\n\n _this20.eventSource.addEventListener('tool_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n\n if (e.data && _this20.messages[msgIdx]) {\n if (!_this20.messages[msgIdx].toolCalls) _this20.messages[msgIdx].toolCalls = [];\n\n _this20.messages[msgIdx].toolCalls.push(e.data);\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n }\n });\n\n _this20.eventSource.addEventListener('answer', function (e) {\n if (!isCurrentStream() || streamDone) return;\n answerBuffer += e.data;\n });\n\n _this20.eventSource.addEventListener('agent_result', function (e) {\n if (!isCurrentStream() || streamDone) return; // 与档案君(AiChat.vue)对齐:AGENT_RESULT 为最终真相源。\n // 只记录 finalContent 并结束流,不打断已在逐行推进的 content,交给 typeTimer 以 target 推进/校正。\n\n if (e.data) finalContent = e.data;\n streamDone = true;\n\n _this20.closeEventSource();\n });\n\n _this20.eventSource.addEventListener('msgKey', function (e) {\n if (!isCurrentStream()) return;\n\n if (e.data && _this20.messages[msgIdx]) {\n _this20.messages[msgIdx].msgKey = e.data;\n }\n });\n\n _this20.eventSource.addEventListener('reasoning', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking += e.data;\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n });\n\n _this20.eventSource.addEventListener('reasoning_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking = e.data;\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n });\n\n _this20.eventSource.addEventListener('error', function () {\n if (!isCurrentStream() || streamDone) return;\n if (!answerBuffer) answerBuffer = '❌ 连接中断';\n streamDone = true;\n\n _this20.closeEventSource();\n });\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _callee13);\n }))();\n },\n handleStop: function handleStop() {\n this.streamRequestId++;\n\n if (this.activeSessionId) {\n (0, _kbChat.stopKbStream)(this.activeSessionId).catch(function () {});\n }\n\n this.finishStream();\n },\n finishStream: function finishStream() {\n var _this21 = this;\n\n this.clearTypeTimer();\n\n if (this.streamMsg) {\n this.$delete(this.contentCache, this.streamMsg._key);\n this.streamMsg.loading = false;\n this.streamMsg = null;\n }\n\n this.closeEventSource();\n this.streaming = false;\n this.refreshSessionList();\n this.$nextTick(function () {\n return _this21.scrollToBottom();\n });\n },\n\n /** 流式结束后刷新会话列表(更新标题/最后消息) */\n refreshSessionList: function refreshSessionList() {\n var _this22 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee14() {\n var list;\n return regeneratorRuntime.wrap(function _callee14$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n if (_this22.companyId) {\n _context14.next = 2;\n break;\n }\n\n return _context14.abrupt(\"return\");\n\n case 2:\n _context14.prev = 2;\n _context14.next = 5;\n return (0, _kbChat.getKbSessionList)(_this22.companyId);\n\n case 5:\n _context14.t0 = _context14.sent;\n\n if (_context14.t0) {\n _context14.next = 8;\n break;\n }\n\n _context14.t0 = [];\n\n case 8:\n list = _context14.t0;\n _this22.sessionList = list;\n _context14.next = 15;\n break;\n\n case 12:\n _context14.prev = 12;\n _context14.t1 = _context14[\"catch\"](2);\n console.error('刷新会话列表失败', _context14.t1);\n\n case 15:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _callee14, null, [[2, 12]]);\n }))();\n },\n clearTypeTimer: function clearTypeTimer() {\n if (this._typeTimer) {\n clearInterval(this._typeTimer);\n this._typeTimer = null;\n }\n },\n closeEventSource: function closeEventSource() {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n },\n copyText: function copyText(text) {\n if (navigator.clipboard) {\n navigator.clipboard.writeText(text);\n }\n },\n scrollToBottom: function scrollToBottom() {\n var el = this.$refs.messagesRef;\n\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack://ZjPluginIntelligent/./src/lib/knowledgeBase/KbChatMain.vue?./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options");
|
|
1016
1016
|
|
|
1017
1017
|
/***/ }),
|
|
1018
1018
|
|
|
@@ -14747,7 +14747,7 @@ eval("// Generated by CoffeeScript 1.12.7\n(function() {\n var XMLDocument, XML
|
|
|
14747
14747
|
/*! exports provided: name, version, main, private, scripts, dependencies, devDependencies, browserslist, directories, keywords, license, files, default */
|
|
14748
14748
|
/***/ (function(module) {
|
|
14749
14749
|
|
|
14750
|
-
eval("module.exports = JSON.parse(\"{\\\"name\\\":\\\"zj-plugin-intelligent\\\",\\\"version\\\":\\\"1.2.26-beta.
|
|
14750
|
+
eval("module.exports = JSON.parse(\"{\\\"name\\\":\\\"zj-plugin-intelligent\\\",\\\"version\\\":\\\"1.2.26-beta.3\\\",\\\"main\\\":\\\"lib/ZjPluginIntelligent.umd.min.js\\\",\\\"private\\\":false,\\\"scripts\\\":{\\\"serve\\\":\\\"set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve\\\",\\\"build\\\":\\\"set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build\\\",\\\"lib\\\":\\\"node scripts/build-lib.js\\\",\\\"lib:test\\\":\\\"node scripts/build-lib.js test\\\"},\\\"dependencies\\\":{\\\"@jboltai/tokui\\\":\\\"^0.1.6\\\",\\\"crypto-js\\\":\\\"^4.2.0\\\",\\\"docx\\\":\\\"^8.5.0\\\",\\\"html2canvas\\\":\\\"^1.4.1\\\",\\\"mammoth\\\":\\\"^1.6.0\\\",\\\"markdown-it\\\":\\\"^13.0.2\\\",\\\"marked\\\":\\\"^4.3.0\\\",\\\"xlsx\\\":\\\"^0.18.5\\\"},\\\"devDependencies\\\":{\\\"@babel/preset-env\\\":\\\"^7.14.9\\\",\\\"@vue/cli-plugin-babel\\\":\\\"~4.5.0\\\",\\\"@vue/cli-plugin-eslint\\\":\\\"~4.5.0\\\",\\\"@vue/cli-plugin-vuex\\\":\\\"~4.5.0\\\",\\\"@vue/cli-service\\\":\\\"~4.5.0\\\",\\\"axios\\\":\\\"^0.27.2\\\",\\\"babel-eslint\\\":\\\"^10.1.0\\\",\\\"babel-plugin-component\\\":\\\"^1.1.1\\\",\\\"compression-webpack-plugin\\\":\\\"^6.0.3\\\",\\\"core-js\\\":\\\"^3.6.5\\\",\\\"element-ui\\\":\\\"^2.15.4\\\",\\\"eslint\\\":\\\"^6.8.0\\\",\\\"eslint-plugin-vue\\\":\\\"^7.13.0\\\",\\\"js-cookie\\\":\\\"^2.2.1\\\",\\\"obs-upload\\\":\\\"^1.0.4\\\",\\\"pako\\\":\\\"^2.1.0\\\",\\\"reconnecting-websocket\\\":\\\"^4.4.0\\\",\\\"sass\\\":\\\"^1.71.0\\\",\\\"sass-loader\\\":\\\"^8.0.2\\\",\\\"snowflake-id\\\":\\\"^1.1.0\\\",\\\"vue\\\":\\\"^2.6.11\\\",\\\"vue-template-compiler\\\":\\\"^2.6.11\\\",\\\"vuex\\\":\\\"^3.4.0\\\",\\\"webpack-bundle-analyzer\\\":\\\"^4.4.2\\\"},\\\"browserslist\\\":[\\\"> 1%\\\",\\\"last 2 versions\\\",\\\"not dead\\\"],\\\"directories\\\":{\\\"lib\\\":\\\"lib\\\"},\\\"keywords\\\":[],\\\"license\\\":\\\"ISC\\\",\\\"files\\\":[\\\"lib\\\"]}\");\n\n//# sourceURL=webpack://ZjPluginIntelligent/./package.json?");
|
|
14751
14751
|
|
|
14752
14752
|
/***/ }),
|
|
14753
14753
|
|
|
@@ -1021,7 +1021,7 @@ eval("\n\nvar _interopRequireWildcard = __webpack_require__(/*! ./node_modules/@
|
|
|
1021
1021
|
/***/ (function(module, exports, __webpack_require__) {
|
|
1022
1022
|
|
|
1023
1023
|
"use strict";
|
|
1024
|
-
eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! ./node_modules/@babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\").default;\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/asyncToGenerator */ \"./node_modules/@babel/runtime/helpers/asyncToGenerator.js\"));\n\nvar _typeof2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/typeof */ \"./node_modules/@babel/runtime/helpers/typeof.js\"));\n\nvar _logo = _interopRequireDefault(__webpack_require__(/*! @/assets/logo.png */ \"./src/assets/logo.png\"));\n\nvar _markdownIt = _interopRequireDefault(__webpack_require__(/*! markdown-it */ \"./node_modules/markdown-it/index.mjs\"));\n\nvar _kbChat = __webpack_require__(/*! ./api/kbChat */ \"./src/lib/knowledgeBase/api/kbChat.js\");\n\nvar _knowledgeBase = __webpack_require__(/*! ./api/knowledgeBase */ \"./src/lib/knowledgeBase/api/knowledgeBase.js\");\n\nvar _native = __webpack_require__(/*! ./native.js */ \"./src/lib/knowledgeBase/native.js\");\n\nvar _request = __webpack_require__(/*! @lib/utils/request.js */ \"./src/lib/utils/request.js\");\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar AI_AVATAR_URL = 'https://qiguanbang.obs.cn-east-3.myhuaweicloud.com/production/image/AlAgent/doc_query.png'; // 与 ChatDetail 使用相同的 Markdown 配置\n\nvar md = new _markdownIt.default({\n breaks: true,\n html: false,\n linkify: true,\n typographer: true\n});\nmd.enable('table');\nvar _default = {\n name: 'KbChatMain',\n props: {\n companyId: {\n type: String,\n default: ''\n },\n userno: {\n type: String,\n default: ''\n },\n imAccount: {\n type: [String, Object],\n default: ''\n },\n userName: {\n type: String,\n default: ''\n },\n // 是否移动端:由父级(KnowledgeBaseMain)传入;未传时组件自身按窗口宽度判断\n isMobile: {\n type: Boolean,\n default: null\n }\n },\n data: function data() {\n // 首帧前计算是否移动端,避免 H5 先显示 PC 布局再切换的闪烁(与 AI 助理一致)\n // 宿主没有 Element UI 时(H5/平板横屏)一律走自绘 UI 分支,避免 el-* 未知元素报错\n var _isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n\n return {\n initDone: false,\n activeSessionId: '',\n sessionList: [],\n // 默认折叠侧边栏:避免新用户进入时先展开再合住的闪烁,\n // 加载出会话后再按规则决定是否展开\n sidebarCollapsed: true,\n messages: [],\n inputText: '',\n streaming: false,\n streamMsg: null,\n showToolInfo: false,\n contentCache: {},\n eventSource: null,\n streamRequestId: 0,\n suggestedQuestions: [],\n // 消息多选模式(豆包式):PC 由消息操作区「删除/多选」进入,移动端长按气泡弹出菜单进入\n selectMode: false,\n // 已选中的消息 id 列表(后端主键)\n selectedMsgIds: [],\n // 当前公司是否没有知识库文档(无资料时展示遮罩)\n noDocument: false,\n // 移动端状态(仿 AI 助理):showH5List=true 显示会话列表页,false 显示聊天详情页\n localIsMobile: _isMobile,\n showH5List: _isMobile,\n // 移动端会话「…」弹出的操作单\n mobileAction: null,\n // 移动端长按气泡弹出的消息操作单(复制 / 删除 / 多选)\n msgAction: null,\n // 移动端侧滑手势(参考 AI 助理):记录起手坐标与是否已触发\n _swipeStartX: 0,\n _swipeStartY: 0,\n _swipeHandled: false,\n _swipeFromLeftEdge: false,\n _swipeFromRightEdge: false,\n _swipeFromEdge: false\n };\n },\n computed: {\n /** 实际是否移动端:优先用父级 prop,未传则用自身窗口判断 */\n isMobileActive: function isMobileActive() {\n return this.isMobile !== null ? this.isMobile : this.localIsMobile;\n },\n aiAvatar: function aiAvatar() {\n return AI_AVATAR_URL;\n },\n userAvatar: function userAvatar() {\n var account = this.imAccount;\n console.log('KbChatMain - imAccount 原始值:', account); // 尝试解析字符串为对象\n\n if (typeof account === 'string') {\n try {\n account = JSON.parse(account);\n console.log('KbChatMain - imAccount 解析后:', account);\n } catch (e) {\n // 如果解析失败,检查是否是纯 URL 字符串\n if (account.trim() && (account.includes('http://') || account.includes('https://'))) {\n console.log('KbChatMain - imAccount 是 URL:', account.trim());\n return account.trim();\n }\n }\n } // 如果是对象,尝试多个可能的头像字段(优先使用 headImg)\n\n\n if (account && (0, _typeof2.default)(account) === 'object') {\n var avatarFields = ['headImg', 'headPortrait', 'avatar', 'avatarUrl', 'headImage', 'photoUrl', 'portrait', 'headUrl', 'userHeadUrl', 'userAvatar'];\n\n for (var _i = 0, _avatarFields = avatarFields; _i < _avatarFields.length; _i++) {\n var field = _avatarFields[_i];\n\n if (account[field] && String(account[field]).trim()) {\n console.log(\"KbChatMain - \\u627E\\u5230\\u5934\\u50CF\\u5B57\\u6BB5 \".concat(field, \":\"), account[field]);\n return String(account[field]).trim();\n }\n }\n }\n\n console.log('KbChatMain - 使用默认头像');\n return _logo.default;\n },\n // 是否有真实头像(区别于默认 fallback 图片)\n hasUserAvatar: function hasUserAvatar() {\n return this.userAvatar && this.userAvatar !== _logo.default;\n },\n // 无头像时的文字头像(与 zjkjAvatar 一致:取名字最后两个字)\n userAvatarText: function userAvatarText() {\n var name = this.userName || '';\n return name ? name.substring(name.length - 2) : '';\n },\n // 可删除的消息 id(仅后端已落库、带回 id 的消息可删)\n deletableMsgIds: function deletableMsgIds() {\n return this.messages.filter(function (m) {\n return m.id;\n }).map(function (m) {\n return m.id;\n });\n },\n // 是否已全选\n isAllSelected: function isAllSelected() {\n return this.deletableMsgIds.length > 0 && this.selectedMsgIds.length === this.deletableMsgIds.length;\n }\n },\n watch: {\n companyId: function companyId() {\n this.initDone = false;\n this.finishStream();\n this.loadSuggestedQuestions();\n this.checkKbDocuments();\n this.initSession();\n },\n userno: function userno() {\n this.initDone = false;\n this.finishStream();\n this.activeSessionId = '';\n this.messages = [];\n this.initSession();\n },\n // 移动端进入时默认先显示会话列表页(仿 AI 助理)\n isMobileActive: function isMobileActive(val) {\n if (val) {\n // 无资料时遮罩只在详情页可见(列表页 .kb-chat-main 被隐藏),故保持停在详情页\n this.showH5List = !this.noDocument;\n this.mobileAction = null;\n }\n }\n },\n mounted: function mounted() {\n var _this = this;\n\n // 独立使用(父级未传 isMobile)时自检窗口宽度\n this._onResize = this.handleResize;\n window.addEventListener('resize', this._onResize, {\n passive: true\n }); // Android WebView 兼容:在 document 级别监听 touchmove/touchend,\n // 避免可滚动子元素消费事件后不冒泡到父级(参考 AI 助理)\n\n this._swipeMoveHandler = this._onSwipeMove;\n this._swipeEndHandler = this._onSwipeEnd;\n this._popStateHandler = this._onPopState;\n document.addEventListener('touchmove', this._swipeMoveHandler, {\n passive: true\n });\n document.addEventListener('touchend', this._swipeEndHandler, {\n passive: true\n }); // 拦截 Android 系统返回手势:通过 history.pushState + popstate\n\n window.addEventListener('popstate', this._popStateHandler); // 移动端首屏为会话列表页,推入一条历史状态供系统返回拦截\n\n if (this.isMobileActive) {\n history.pushState({\n page: 'kbChatList'\n }, '');\n }\n\n this.initSession();\n this.checkKbDocuments(); // v-show 隐藏期间加载的历史消息无法滚动(scrollHeight 为 0),\n // 监听组件由隐藏变为可见时,滚动到最新问答;\n // 同时每次变为可见时刷新\"猜您想问\"(管理端可能刚更新过)\n\n if (typeof IntersectionObserver !== 'undefined' && this.$el) {\n this._visibilityObserver = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) {\n return e.isIntersecting;\n })) {\n _this.loadSuggestedQuestions(); // 管理端可能刚上传/删除了文档,重新校验遮罩状态\n\n\n _this.checkKbDocuments();\n\n if (_this.messages.length) {\n _this.$nextTick(function () {\n return _this.scrollToBottom();\n });\n }\n }\n });\n\n this._visibilityObserver.observe(this.$el);\n } else {\n this.loadSuggestedQuestions();\n }\n },\n beforeDestroy: function beforeDestroy() {\n if (this._visibilityObserver) {\n this._visibilityObserver.disconnect();\n\n this._visibilityObserver = null;\n }\n\n if (this._onResize) {\n window.removeEventListener('resize', this._onResize);\n }\n\n if (this._swipeMoveHandler) {\n document.removeEventListener('touchmove', this._swipeMoveHandler);\n }\n\n if (this._swipeEndHandler) {\n document.removeEventListener('touchend', this._swipeEndHandler);\n }\n\n if (this._popStateHandler) {\n window.removeEventListener('popstate', this._popStateHandler);\n }\n\n this.streamRequestId++;\n this.closeEventSource();\n this.clearTypeTimer();\n this.onMsgTouchEnd();\n this.contentCache = {};\n },\n methods: {\n // 独立使用时自检移动端(父级已传 isMobile 时忽略)\n handleResize: function handleResize() {\n var now = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n this.localIsMobile = now;\n },\n renderMarkdown: function renderMarkdown(text) {\n if (!text) return '';\n\n try {\n return md.render(text);\n } catch (err) {\n return this.escapeHtml(text);\n }\n },\n // 与 ChatDetail 一致的增量渲染:完整行走 markdown、未完成尾行转义拼接,\n // 借助 markdown-it breaks:true 实现逐行浮现效果。\n renderStreaming: function renderStreaming(text, msgKey) {\n if (!text) return '';\n var cache = this.contentCache[msgKey];\n var lines = text.split('\\n');\n var lastLineIdx = lines.length - 1;\n\n if (!cache) {\n try {\n var html = md.render(text);\n this.$set(this.contentCache, msgKey, {\n renderedLines: lastLineIdx,\n html: html,\n fullText: text\n });\n return html;\n } catch (_unused) {\n return this.escapeHtml(text);\n }\n }\n\n if (text === cache.fullText) return cache.html;\n cache.fullText = text; // 行数没变,仅末尾追加字符,直接拼接尾行\n\n if (lastLineIdx === cache.renderedLines) {\n return cache.html + this.escapeHtml(lines[lastLineIdx]);\n } // 行数变少/文本回退截断,全量重渲染\n\n\n if (lastLineIdx < cache.renderedLines) {\n try {\n var fullLines = lines.slice(0, lastLineIdx);\n var newBaseHtml = '';\n\n if (fullLines.length > 0) {\n newBaseHtml = md.render(fullLines.join('\\n'));\n }\n\n cache.html = newBaseHtml;\n cache.renderedLines = lastLineIdx;\n\n var _lastLine = this.escapeHtml(lines[lastLineIdx]);\n\n return cache.html + (_lastLine || '');\n } catch (_unused2) {\n return this.escapeHtml(text);\n }\n } // 新增完整行\n\n\n if (cache.renderedLines < lastLineIdx) {\n var allFullLines = lines.slice(0, lastLineIdx);\n var _newBaseHtml = '';\n\n if (allFullLines.length > 0) {\n _newBaseHtml = md.render(allFullLines.join('\\n'));\n }\n\n cache.html = _newBaseHtml;\n cache.renderedLines = lastLineIdx;\n }\n\n var lastLine = this.escapeHtml(lines[lastLineIdx]);\n cache.renderedLines = lastLineIdx;\n return cache.html + (lastLine || '');\n },\n escapeHtml: function escapeHtml(text) {\n if (!text) return '';\n var map = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(text).replace(/[&<>\"']/g, function (c) {\n return map[c];\n });\n },\n autoResizeTextarea: function autoResizeTextarea() {\n var ta = this.$refs.taRef;\n if (!ta) return; // 移动端从单行起增高(与 38px 最小高度一致),PC 保持 100px 基准\n\n var baseH = this.isMobileActive ? 38 : 100;\n var maxH = this.isMobileActive ? 120 : 180;\n ta.style.height = \"\".concat(baseH, \"px\");\n var realH = ta.scrollHeight;\n\n if (realH > baseH) {\n ta.style.height = \"\".concat(Math.min(realH, maxH), \"px\");\n }\n },\n newLine: function newLine(e) {\n var _this2 = this;\n\n e.preventDefault();\n var ta = this.$refs.taRef;\n var start = ta.selectionStart;\n var end = ta.selectionEnd;\n this.inputText = this.inputText.slice(0, start) + '\\n' + this.inputText.slice(end);\n this.$nextTick(function () {\n ta.selectionStart = ta.selectionEnd = start + 1;\n\n _this2.autoResizeTextarea();\n });\n },\n\n /**\n * 加载\"猜您想问\"问题(按公司,所有用户可见)\n */\n loadSuggestedQuestions: function loadSuggestedQuestions() {\n var _this3 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {\n var list;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n if (_this3.companyId) {\n _context.next = 3;\n break;\n }\n\n _this3.suggestedQuestions = [];\n return _context.abrupt(\"return\");\n\n case 3:\n _context.prev = 3;\n _context.next = 6;\n return (0, _knowledgeBase.getSuggestedQuestions)(_this3.companyId);\n\n case 6:\n _context.t0 = _context.sent;\n\n if (_context.t0) {\n _context.next = 9;\n break;\n }\n\n _context.t0 = [];\n\n case 9:\n list = _context.t0;\n _this3.suggestedQuestions = list.map(function (q) {\n return q.questionText;\n });\n _context.next = 16;\n break;\n\n case 13:\n _context.prev = 13;\n _context.t1 = _context[\"catch\"](3);\n console.error('加载猜您想问失败', _context.t1);\n\n case 16:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, null, [[3, 13]]);\n }))();\n },\n\n /**\n * 检查当前公司是否有「已索引」的知识库文档,无可用资料时展示遮罩提示\n * 仅已索引(indexStatus === 1)的文档才能被检索,未索引/索引中/索引失败都不算\n */\n checkKbDocuments: function checkKbDocuments() {\n var _this4 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var data, isEmpty;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n if (_this4.companyId) {\n _context2.next = 3;\n break;\n }\n\n _this4.noDocument = false;\n return _context2.abrupt(\"return\");\n\n case 3:\n _context2.prev = 3;\n _context2.next = 6;\n return (0, _knowledgeBase.getFileList)({\n companyId: _this4.companyId,\n indexStatus: 1,\n pageNum: 1,\n pageSize: 1\n });\n\n case 6:\n data = _context2.sent;\n isEmpty = (data && data.total || 0) === 0;\n _this4.noDocument = isEmpty; // 无资料时把会话固定回第一个\n\n if (isEmpty) {\n _this4.lockFirstSession(); // 无资料遮罩位于聊天详情页的 .kb-chat-main 内,而移动端首屏是会话列表页\n // (h5-list-mode 下 .kb-chat-main 被 display:none 隐藏),若停留在列表页\n // 则遮罩永远不可见;故无资料时强制切到详情页把遮罩展示出来\n\n\n if (_this4.isMobileActive) {\n _this4.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n }\n }\n\n _context2.next = 16;\n break;\n\n case 12:\n _context2.prev = 12;\n _context2.t0 = _context2[\"catch\"](3);\n // 查询失败(如无管理权限)时不展示遮罩,避免误拦截\n console.error('检查知识库文档失败', _context2.t0);\n _this4.noDocument = false;\n\n case 16:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, null, [[3, 12]]);\n }))();\n },\n initSession: function initSession() {\n var _this5 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {\n var list;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n if (_this5.companyId) {\n _context3.next = 2;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 2:\n if (!(_this5.initDone && (_this5.messages.length || _this5.streaming || _this5.inputText))) {\n _context3.next = 4;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 4:\n _this5.initDone = false;\n _context3.prev = 5;\n _context3.next = 8;\n return (0, _kbChat.getKbSessionList)(_this5.companyId);\n\n case 8:\n _context3.t0 = _context3.sent;\n\n if (_context3.t0) {\n _context3.next = 11;\n break;\n }\n\n _context3.t0 = [];\n\n case 11:\n list = _context3.t0;\n _this5.sessionList = list;\n\n if (!(list.length > 0)) {\n _context3.next = 19;\n break;\n }\n\n // 只有 1 个会话且无消息(新用户首次进入自动生成的空会话)时,\n // 默认折叠侧边栏只显示聊天主界面;在 selectSession 前同步设置,\n // 避免「先展开再合住」的闪烁\n _this5.sidebarCollapsed = list.length === 1 && !list[0].lastContent;\n _context3.next = 17;\n return _this5.selectSession(list[0].sessionId);\n\n case 17:\n _context3.next = 21;\n break;\n\n case 19:\n _context3.next = 21;\n return _this5.autoCreateSession();\n\n case 21:\n _context3.next = 28;\n break;\n\n case 23:\n _context3.prev = 23;\n _context3.t1 = _context3[\"catch\"](5);\n console.error('获取会话列表失败,尝试自动创建', _context3.t1);\n _context3.next = 28;\n return _this5.autoCreateSession();\n\n case 28:\n _context3.prev = 28;\n _this5.initDone = true;\n return _context3.finish(28);\n\n case 31:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _callee3, null, [[5, 23, 28, 31]]);\n }))();\n },\n autoCreateSession: function autoCreateSession() {\n var _this6 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {\n var data;\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n if (_this6.companyId) {\n _context4.next = 3;\n break;\n }\n\n console.warn('[KbChat] 无法创建会话:companyId 为空');\n return _context4.abrupt(\"return\");\n\n case 3:\n console.log('[KbChat] 创建会话,companyId:', _this6.companyId);\n _context4.prev = 4;\n _context4.next = 7;\n return (0, _kbChat.createKbSession)(_this6.companyId);\n\n case 7:\n data = _context4.sent;\n console.log('[KbChat] 会话创建成功,sessionId:', data.sessionId);\n\n _this6.finishStream();\n\n _this6.activeSessionId = data.sessionId;\n _this6.streamRequestId++;\n _this6.messages = [];\n _this6.sessionList = [{\n sessionId: data.sessionId,\n title: '',\n lastContent: ''\n }].concat(_this6.sessionList || []); // 新建会话时保持侧边栏展开,方便继续切换会话\n\n _this6.sidebarCollapsed = false;\n\n _this6.$nextTick(function () {\n return _this6.scrollToBottom();\n });\n\n _context4.next = 21;\n break;\n\n case 18:\n _context4.prev = 18;\n _context4.t0 = _context4[\"catch\"](4);\n console.error('自动创建会话失败', _context4.t0);\n\n case 21:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _callee4, null, [[4, 18]]);\n }))();\n },\n\n /** 新建会话(豆包式「新对话」) */\n handleNewSession: function handleNewSession() {\n var _this7 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee5() {\n return regeneratorRuntime.wrap(function _callee5$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n if (!_this7.noDocument) {\n _context5.next = 2;\n break;\n }\n\n return _context5.abrupt(\"return\");\n\n case 2:\n if (_this7.streaming) _this7.handleStop();\n _context5.next = 5;\n return _this7.autoCreateSession();\n\n case 5:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _callee5);\n }))();\n },\n\n /** 切换会话 */\n handleSelectSession: function handleSelectSession(item) {\n var _this8 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee6() {\n return regeneratorRuntime.wrap(function _callee6$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context6.next = 2;\n break;\n }\n\n return _context6.abrupt(\"return\");\n\n case 2:\n if (!_this8.noDocument) {\n _context6.next = 5;\n break;\n }\n\n _this8.lockFirstSession();\n\n return _context6.abrupt(\"return\");\n\n case 5:\n if (!(item.sessionId === _this8.activeSessionId && _this8.messages.length)) {\n _context6.next = 8;\n break;\n }\n\n if (_this8.isMobileActive) {\n _this8.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n return _context6.abrupt(\"return\");\n\n case 8:\n _context6.next = 10;\n return _this8.selectSession(item.sessionId);\n\n case 10:\n // 移动端:选中后跳到聊天详情页\n if (_this8.isMobileActive) {\n _this8.showH5List = false; // 推入历史状态,使 Android 系统返回手势先触发 popstate 被拦截\n\n history.pushState({\n page: 'kbChatDetail'\n }, ''); // 列表页时聊天区被 display:none,scrollHeight 为 0 导致上面的滚动无效;\n // 切到详情页、容器可见后需再滚一次,默认定位到最新消息\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n case 11:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _callee6);\n }))();\n },\n\n /** 返回宿主 App 原生页面(参考 AI 助理 handleNativeBack) */\n handleNativeBack: function handleNativeBack() {\n if (this.$hybrid && typeof this.$hybrid.closePage === 'function') {\n this.$hybrid.closePage();\n } else if (window.history.length > 1) {\n window.history.back();\n }\n },\n\n /** 会话列表页返回按钮:返回原生页面 */\n handleSidebarBack: function handleSidebarBack() {\n this.handleNativeBack();\n },\n // ── H5 侧滑手势(参考 AI 助理)──\n // 从左/右屏幕边缘起手横滑触发返回,避免页面内部横向滚动被误判\n onSwipeTouchStart: function onSwipeTouchStart(e) {\n if (!this.isMobileActive) return;\n var t = e.touches[0];\n this._swipeStartX = t.clientX;\n this._swipeStartY = t.clientY;\n this._swipeHandled = false;\n var edge = Math.min(56, Math.round(window.innerWidth * 0.09));\n this._swipeFromLeftEdge = t.clientX <= edge;\n this._swipeFromRightEdge = t.clientX >= window.innerWidth - edge;\n this._swipeFromEdge = this._swipeFromLeftEdge || this._swipeFromRightEdge;\n },\n _swipeShouldTrigger: function _swipeShouldTrigger(diffX, diffY) {\n if (!this._swipeFromEdge) return false; // 从左边缘 → 右滑;从右边缘 → 左滑\n\n var directionOk = this._swipeFromLeftEdge && diffX >= 60 || this._swipeFromRightEdge && diffX <= -60;\n if (!directionOk) return false; // 水平距离明显大于垂直距离(排除上下滚动干扰)\n\n return Math.abs(diffX) >= Math.abs(diffY) * 2;\n },\n _onSwipeMove: function _onSwipeMove(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.touches || !e.touches.length) return;\n var diffX = e.touches[0].clientX - this._swipeStartX;\n var diffY = e.touches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n // touchend 兜底,兼容不支持 touchmove 的场景\n _onSwipeEnd: function _onSwipeEnd(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.changedTouches || !e.changedTouches.length) return;\n var diffX = e.changedTouches[0].clientX - this._swipeStartX;\n var diffY = e.changedTouches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n\n /** 侧滑触发:列表页 → 返回原生页面;详情页 → 回会话列表页 */\n _triggerSwipeAction: function _triggerSwipeAction() {\n if (this.showH5List) {\n this.handleNativeBack();\n } else {\n this.handleMobileBack();\n }\n },\n\n /**\n * 拦截 Android 系统返回手势(history.pushState + popstate)\n * WebView 先回退历史栈触发 popstate,这里拦截后导航到列表页,避免 Activity 被直接关闭\n */\n _onPopState: function _onPopState() {\n if (!this.isMobileActive) return;\n\n if (!this.showH5List) {\n // 详情页 → 返回会话列表页\n this.showH5List = true;\n history.pushState({\n page: 'kbChatList'\n }, '');\n } else {\n // 列表页 → 直接关闭 WebView 返回原生页面\n this.handleNativeBack();\n }\n },\n\n /** 移动端详情页返回:无资料遮罩时返回原生页面;否则回到会话列表页 */\n handleMobileBack: function handleMobileBack() {\n // 无资料遮罩期间无法进入会话,返回到宿主 App 原生页面\n if (this.noDocument) {\n this.handleNativeBack();\n return;\n } // 正常情况:回到会话列表页\n\n\n this.showH5List = true;\n },\n\n /** 无资料遮罩期间:把当前会话固定回列表中的第一个 */\n lockFirstSession: function lockFirstSession() {\n var first = this.sessionList[0];\n\n if (first && first.sessionId && first.sessionId !== this.activeSessionId) {\n this.selectSession(first.sessionId);\n }\n },\n\n /** 会话项「…」菜单:重命名 / 删除 */\n handleSessionMenu: function handleSessionMenu(cmd, item) {\n if (!item || !item.sessionId) return;\n\n if (cmd === 'rename') {\n this.handleRenameSession(item);\n } else if (cmd === 'delete') {\n this.handleDeleteSession(item);\n }\n },\n // ==================== 移动端会话操作单 ====================\n openMobileAction: function openMobileAction(item) {\n if (this.noDocument) return;\n this.mobileAction = item;\n },\n doMobileRename: function doMobileRename(item) {\n this.mobileAction = null;\n this.handleRenameSession(item);\n },\n doMobileDelete: function doMobileDelete(item) {\n this.mobileAction = null;\n this.handleDeleteSession(item);\n },\n\n /** 重命名会话 */\n handleRenameSession: function handleRenameSession(item) {\n var _this9 = this;\n\n if (!item || !item.sessionId) return;\n this.$prompt('请输入新的会话名称', '重命名会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n inputValue: item.title || '',\n inputPattern: /\\S+/,\n inputErrorMessage: '名称不能为空'\n }).then(function (_ref) {\n var value = _ref.value;\n var title = (value || '').trim();\n if (!title) return;\n return (0, _kbChat.renameKbSession)(item.sessionId, title).then(function () {\n item.title = title;\n\n _this9.$message.success('重命名成功');\n });\n }).catch(function (err) {\n // 取消或校验失败,不提示错误\n if (err && err !== 'cancel' && err !== 'close') {\n console.error('重命名失败', err);\n\n _this9.$message.error('重命名失败');\n }\n });\n },\n\n /** 删除会话 */\n handleDeleteSession: function handleDeleteSession(item) {\n var _this10 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {\n return regeneratorRuntime.wrap(function _callee7$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context7.next = 2;\n break;\n }\n\n return _context7.abrupt(\"return\");\n\n case 2:\n if (!(_this10.sessionList.length <= 1)) {\n _context7.next = 5;\n break;\n }\n\n _this10.$message.warning('至少保留一个会话');\n\n return _context7.abrupt(\"return\");\n\n case 5:\n _context7.prev = 5;\n _context7.next = 8;\n return _this10.$confirm('删除后无法恢复,确定删除该会话吗?', '删除会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 8:\n _context7.next = 13;\n break;\n\n case 10:\n _context7.prev = 10;\n _context7.t0 = _context7[\"catch\"](5);\n return _context7.abrupt(\"return\");\n\n case 13:\n _context7.prev = 13;\n _context7.next = 16;\n return (0, _kbChat.deleteKbSession)(item.sessionId);\n\n case 16:\n _this10.sessionList = (_this10.sessionList || []).filter(function (s) {\n return s.sessionId !== item.sessionId;\n });\n\n if (!(_this10.activeSessionId === item.sessionId)) {\n _context7.next = 28;\n break;\n }\n\n _this10.finishStream();\n\n _this10.activeSessionId = '';\n _this10.messages = [];\n\n if (!(_this10.sessionList.length > 0)) {\n _context7.next = 26;\n break;\n }\n\n _context7.next = 24;\n return _this10.selectSession(_this10.sessionList[0].sessionId);\n\n case 24:\n _context7.next = 28;\n break;\n\n case 26:\n _context7.next = 28;\n return _this10.autoCreateSession();\n\n case 28:\n _this10.$message.success('删除成功');\n\n _context7.next = 35;\n break;\n\n case 31:\n _context7.prev = 31;\n _context7.t1 = _context7[\"catch\"](13);\n console.error('删除会话失败', _context7.t1);\n\n _this10.$message.error('删除失败');\n\n case 35:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _callee7, null, [[5, 10], [13, 31]]);\n }))();\n },\n\n /** 会话列表时间格式化:今天显示时分,更早显示月日 */\n formatTime: function formatTime(time) {\n if (!time) return '';\n var d = new Date(time);\n if (isNaN(d.getTime())) return '';\n var now = new Date();\n var sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();\n\n var pad = function pad(n) {\n return n < 10 ? '0' + n : '' + n;\n };\n\n if (sameDay) {\n return pad(d.getHours()) + ':' + pad(d.getMinutes());\n }\n\n return pad(d.getMonth() + 1) + '-' + pad(d.getDate());\n },\n\n /** 后端消息 → 前端消息对象(保留 id/msgKey,删除功能需要) */\n mapMessage: function mapMessage(m, i) {\n var thinking = m.thinking || m.reasoning || m.reasoning_result || m.reasoningContent || m.reasoning_content || m.thinkingContent || m.thinking_content || '';\n return {\n id: m.id,\n msgKey: m.msgKey || m.msg_key || '',\n role: m.role,\n content: m.content || '',\n thinking: thinking,\n _key: m.createTime ? 'msg_' + m.createTime + '_' + i : 'msg_' + Date.now() + '_' + Math.random()\n };\n },\n selectSession: function selectSession(sessionId) {\n var _this11 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee8() {\n var raw, msgs;\n return regeneratorRuntime.wrap(function _callee8$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n if (_this11.streaming) _this11.handleStop();\n\n _this11.exitSelectMode();\n\n _this11.msgAction = null;\n _this11.activeSessionId = sessionId;\n _this11.messages = [];\n _this11.streamMsg = null;\n _context8.prev = 6;\n _context8.next = 9;\n return (0, _kbChat.getKbSessionMessages)(sessionId);\n\n case 9:\n raw = _context8.sent;\n msgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || [];\n _this11.messages = msgs.map(function (m, i) {\n return _this11.mapMessage(m, i);\n });\n\n _this11.$nextTick(function () {\n return _this11.scrollToBottom();\n });\n\n _context8.next = 18;\n break;\n\n case 15:\n _context8.prev = 15;\n _context8.t0 = _context8[\"catch\"](6);\n console.error('加载消息失败', _context8.t0);\n\n case 18:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _callee8, null, [[6, 15]]);\n }))();\n },\n\n /**\n * 回读当前会话消息,补全后端持久化后生成的 id。\n * 流式回答期间消息只存在于本地数组(无 id),无法按 id 删除;\n * 流式结束后调用一次即可拿到 id。已全部有 id 时跳过,避免多余请求。\n *\n * 注意:不能直接用后端数组整体覆盖 this.messages。后端回读可能包含重复/多余记录\n * (如同一会话里用户消息被落库两次),若整体覆盖会把重复消息直接渲染出来。\n * 这里以本地数组为基准,只用后端记录按「角色 + 内容」顺序回填 id,保证界面条数不变。\n */\n syncMessageIds: function syncMessageIds() {\n var _this12 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee9() {\n var raw, backendMsgs, cursor;\n return regeneratorRuntime.wrap(function _callee9$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n if (!(!_this12.activeSessionId || _this12.streaming)) {\n _context9.next = 2;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 2:\n if (!(_this12.messages.length && _this12.messages.every(function (m) {\n return m.id;\n }))) {\n _context9.next = 4;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 4:\n _context9.prev = 4;\n _context9.next = 7;\n return (0, _kbChat.getKbSessionMessages)(_this12.activeSessionId);\n\n case 7:\n raw = _context9.sent;\n backendMsgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || []; // 按顺序把本地消息与后端记录配对(角色一致且内容匹配),只补 id/msgKey\n\n cursor = 0;\n\n _this12.messages.forEach(function (m) {\n if (m.id) return;\n\n var role = _this12.normRole(m.role);\n\n for (var i = cursor; i < backendMsgs.length; i++) {\n var b = backendMsgs[i];\n\n if (_this12.normRole(b.role) === role && String(b.content || '').trim() === String(m.content || '').trim()) {\n m.id = b.id;\n if (!m.msgKey) m.msgKey = b.msgKey || b.msg_key || '';\n cursor = i + 1;\n return;\n }\n }\n });\n\n _context9.next = 16;\n break;\n\n case 13:\n _context9.prev = 13;\n _context9.t0 = _context9[\"catch\"](4);\n console.error('同步消息 id 失败', _context9.t0);\n\n case 16:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _callee9, null, [[4, 13]]);\n }))();\n },\n // ==================== 消息多选 / 删除 ====================\n\n /** 进入多选模式(preselectMsg 传入时默认勾选该条所在的整组问答,对应豆包式「删除」入口) */\n enterSelectMode: function enterSelectMode(preselectMsg) {\n var _this13 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee10() {\n var preselectId, idx;\n return regeneratorRuntime.wrap(function _callee10$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n if (!(_this13.streaming || !_this13.messages.length)) {\n _context10.next = 2;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 2:\n // 点击条的后端持久化 id(模板上「删除」按钮仅在 msg.id 存在时渲染,故必有值)\n preselectId = preselectMsg ? preselectMsg.id : null; // 先回读消息确保每条都有 id(刚流式完成的消息本地没有 id)。\n // 回读只补 id、不重建数组,定位可用 id 或对象引用。\n\n _context10.next = 5;\n return _this13.syncMessageIds();\n\n case 5:\n _this13.selectMode = true;\n _this13.selectedMsgIds = [];\n idx = preselectId != null ? _this13.messages.findIndex(function (m) {\n return m.id === preselectId;\n }) : -1;\n if (idx === -1 && preselectMsg) idx = _this13.messages.indexOf(preselectMsg);\n\n if (idx !== -1) {\n // 默认勾选整组问答(用户问 + AI 答),便于一键删除\n _this13.selectedMsgIds = _this13.getQaPairIdsAt(idx);\n } // 滚动定位:让默认勾选的整组问答都进入视野(长回答会把用户问顶出屏幕)\n\n\n _this13.scrollSelectedIntoView();\n\n case 11:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _callee10);\n }))();\n },\n\n /**\n * 把首个被选中的消息行滚动到容器内可见。\n * 选中行是「用户问 + AI 答」中的靠前一条,滚到它即能同时看到整组。\n * 若靠前那条已在视野内则不动,避免无谓跳动。\n */\n scrollSelectedIntoView: function scrollSelectedIntoView() {\n var _this14 = this;\n\n this.$nextTick(function () {\n var container = _this14.$refs.messagesRef;\n if (!container) return;\n var rows = container.querySelectorAll('.msg-row');\n if (!rows.length) return; // 找到第一个处于选中态、且带 id 的行(与 selectedMsgIds 顺序无关,取 DOM 顺序)\n\n var target = null;\n\n for (var i = 0; i < rows.length; i++) {\n var box = rows[i].querySelector('.msg-check-box.checked');\n\n if (box) {\n target = rows[i];\n break;\n }\n }\n\n if (!target) return;\n var cRect = container.getBoundingClientRect();\n var tRect = target.getBoundingClientRect(); // 目标行顶部已在容器可视区内则无需滚动\n\n if (tRect.top >= cRect.top && tRect.top < cRect.bottom) return; // 滚动到目标行顶部(留 8px 间距)\n\n container.scrollTop += tRect.top - cRect.top - 8;\n });\n },\n exitSelectMode: function exitSelectMode() {\n this.selectMode = false;\n this.selectedMsgIds = [];\n },\n isMsgSelected: function isMsgSelected(msg) {\n return !!msg.id && this.selectedMsgIds.indexOf(msg.id) !== -1;\n },\n\n /** 归一化 role,兼容后端可能返回的大小写/空白 */\n normRole: function normRole(role) {\n return String(role == null ? '' : role).trim().toLowerCase();\n },\n\n /**\n * 取与下标 idx 处消息成对的另一条:用户问 ↔ AI 答视为一组问答。\n * 只认紧邻的一条(问答对在列表里必然相邻),相邻非同组则不配对。\n * 不依赖 role 的具体拼写(user/assistant 的大小写、空白均容错)。\n */\n getQaPartnerAt: function getQaPartnerAt(idx) {\n if (idx < 0 || idx >= this.messages.length) return null;\n var cur = this.messages[idx];\n if (!cur) return null;\n var role = this.normRole(cur.role);\n if (role !== 'user' && role !== 'assistant') return null; // 只认紧邻的那一条:问答对在列表里必然相邻。\n // 若相邻消息不是异角色(例如回答已被删除),说明当前消息已无配对,\n // 不能继续向前/向后扫描,否则会误勾到上一个问答的回答。\n\n var neighbor = role === 'user' ? this.messages[idx + 1] : this.messages[idx - 1];\n if (!neighbor) return null;\n var neighborRole = this.normRole(neighbor.role);\n var expect = role === 'user' ? 'assistant' : 'user';\n return neighborRole === expect ? neighbor : null;\n },\n\n /** 下标 idx 处消息所在问答对的可删 id 列表(用户问 + AI 答) */\n getQaPairIdsAt: function getQaPairIdsAt(idx) {\n var ids = [];\n var cur = this.messages[idx];\n if (cur && cur.id) ids.push(cur.id);\n var partner = this.getQaPartnerAt(idx);\n if (partner && partner.id && ids.indexOf(partner.id) === -1) ids.push(partner.id);\n return ids;\n },\n\n /**\n * 勾选单条消息:勾上时连带把问答对里另一条一起勾上;\n * 取消时只取消当前这条,从而支持「先整组勾选、再单独取消一条」的单条选择。\n */\n toggleSelectMessage: function toggleSelectMessage(msg) {\n var _this15 = this;\n\n if (!msg.id) return;\n var i = this.selectedMsgIds.indexOf(msg.id);\n\n if (i === -1) {\n var idx = this.messages.indexOf(msg);\n var pairIds = idx === -1 ? [msg.id] : this.getQaPairIdsAt(idx);\n pairIds.forEach(function (id) {\n if (_this15.selectedMsgIds.indexOf(id) === -1) _this15.selectedMsgIds.push(id);\n });\n } else {\n this.selectedMsgIds.splice(i, 1);\n }\n },\n toggleSelectAll: function toggleSelectAll() {\n var ids = this.deletableMsgIds;\n this.selectedMsgIds = this.selectedMsgIds.length === ids.length ? [] : ids.slice();\n },\n\n /** 多选模式下点击整行即切换选中 */\n onMsgRowClick: function onMsgRowClick(msg) {\n // 长按弹出菜单后浏览器还会补一次 click,短暂屏蔽避免误触\n if (Date.now() < this._ignoreClickUntil) return;\n if (this.selectMode) this.toggleSelectMessage(msg);\n },\n\n /** 移动端长按气泡弹出消息操作单(豆包式:复制 / 删除 / 多选) */\n onMsgTouchStart: function onMsgTouchStart(msg) {\n var _this16 = this;\n\n if (!this.isMobileActive || this.selectMode) return;\n this._longPressMsg = msg;\n clearTimeout(this._longPressTimer);\n this._longPressTimer = setTimeout(function () {\n _this16._longPressTimer = null; // 用户正在长按选中文本(复制用),此时不弹菜单,避免抢走选择行为\n\n var sel = window.getSelection && window.getSelection();\n if (sel && String(sel).trim()) return;\n\n _this16.openMsgAction(_this16._longPressMsg);\n }, 500);\n },\n onMsgTouchEnd: function onMsgTouchEnd() {\n if (this._longPressTimer) {\n clearTimeout(this._longPressTimer);\n this._longPressTimer = null;\n }\n },\n openMsgAction: function openMsgAction(msg) {\n this.msgAction = msg; // 长按抬手后浏览器可能补发 click,短暂屏蔽,避免刚弹出的菜单被立即关闭\n\n this._ignoreClickUntil = Date.now() + 350;\n },\n closeMsgAction: function closeMsgAction() {\n if (Date.now() < this._ignoreClickUntil) return;\n this.msgAction = null;\n },\n\n /** 移动端消息操作单:复制 / 删除(删除进入多选模式,与豆包一致) */\n doMsgAction: function doMsgAction(action) {\n var msg = this.msgAction;\n this.msgAction = null;\n if (!msg) return;\n\n if (action === 'copy') {\n this.copyText(msg.content);\n } else if (action === 'delete') {\n this.enterSelectMode(msg);\n }\n },\n\n /** 批量删除选中的消息 */\n handleDeleteSelected: function handleDeleteSelected() {\n var _this17 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee11() {\n var ids;\n return regeneratorRuntime.wrap(function _callee11$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n if (!(!_this17.selectedMsgIds.length || !_this17.activeSessionId)) {\n _context11.next = 2;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 2:\n ids = _this17.selectedMsgIds.slice();\n _context11.prev = 3;\n _context11.next = 6;\n return _this17.$confirm(\"\\u5220\\u9664\\u540E\\u65E0\\u6CD5\\u6062\\u590D\\uFF0C\\u786E\\u5B9A\\u5220\\u9664\\u9009\\u4E2D\\u7684 \".concat(ids.length, \" \\u6761\\u6D88\\u606F\\u5417\\uFF1F\"), '删除消息', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 6:\n _context11.next = 11;\n break;\n\n case 8:\n _context11.prev = 8;\n _context11.t0 = _context11[\"catch\"](3);\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.prev = 11;\n _context11.next = 14;\n return (0, _kbChat.deleteKbMessages)(_this17.activeSessionId, ids);\n\n case 14:\n _this17.messages = _this17.messages.filter(function (m) {\n return ids.indexOf(m.id) === -1;\n });\n\n _this17.exitSelectMode();\n\n _this17.$message.success('删除成功');\n\n _this17.refreshSessionList();\n\n _context11.next = 24;\n break;\n\n case 20:\n _context11.prev = 20;\n _context11.t1 = _context11[\"catch\"](11);\n console.error('批量删除消息失败', _context11.t1);\n\n _this17.$message.error('删除失败');\n\n case 24:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _callee11, null, [[3, 8], [11, 20]]);\n }))();\n },\n handleResetSession: function handleResetSession() {\n var _this18 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee12() {\n return regeneratorRuntime.wrap(function _callee12$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n if (!_this18.activeSessionId) {\n _context12.next = 8;\n break;\n }\n\n _context12.prev = 1;\n _context12.next = 4;\n return (0, _kbChat.closeKbSession)(_this18.activeSessionId);\n\n case 4:\n _context12.next = 8;\n break;\n\n case 6:\n _context12.prev = 6;\n _context12.t0 = _context12[\"catch\"](1);\n\n case 8:\n _context12.next = 10;\n return _this18.autoCreateSession();\n\n case 10:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _callee12, null, [[1, 6]]);\n }))();\n },\n handleSend: function handleSend(quickText) {\n var _this19 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee13() {\n var text, requestSessionId, requestId, msgKey, msgIdx, streamMsg, isCurrentStream, answerBuffer, finalContent, streamDone, url;\n return regeneratorRuntime.wrap(function _callee13$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n text = typeof quickText === 'string' ? quickText.trim() : _this19.inputText.trim();\n\n if (!(!text || !_this19.activeSessionId || _this19.streaming)) {\n _context13.next = 3;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 3:\n _this19.exitSelectMode();\n\n _this19.inputText = '';\n requestSessionId = _this19.activeSessionId;\n requestId = ++_this19.streamRequestId;\n\n _this19.clearTypeTimer();\n\n _this19.messages.push({\n role: 'user',\n content: text\n });\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n }); // 流式消息:作为 messages 中的对象(带 _key/loading),与 ChatDetail 一致\n\n\n msgKey = 'stream_' + requestId;\n msgIdx = _this19.messages.push({\n role: 'assistant',\n content: '',\n thinking: '',\n toolCalls: [],\n loading: true,\n _key: msgKey\n }) - 1;\n streamMsg = _this19.messages[msgIdx];\n _this19.streamMsg = streamMsg;\n _this19.streaming = true;\n\n isCurrentStream = function isCurrentStream() {\n return requestId === _this19.streamRequestId && requestSessionId === _this19.activeSessionId && _this19.streamMsg === streamMsg;\n }; // SSE 只把数据累积到 answerBuffer。typeTimer(30ms) 节流按「完整换行」逐行把内容\n // 推进到 msg.content:每个 tick 最多只推进「到下一个 \\n 为止」的一行,未完成的尾行\n // 保持不动;配合 renderStreaming(breaks:true) 实现「一行显示一行」。\n // 不是打字机模式(不逐字)、也不是一次性整段渲染(不会一出就是全文)。\n\n\n answerBuffer = '';\n finalContent = '';\n streamDone = false;\n _this19._typeTimer = setInterval(function () {\n if (!isCurrentStream()) return;\n var msg = _this19.messages[msgIdx];\n var source = finalContent || answerBuffer;\n var curLen = msg._prevReveal || 0;\n var nextLen = curLen; // 找到「当前已揭示进度之后」的第一条完整换行,仅推进这一行\n\n var nl = source.indexOf('\\n', curLen);\n\n if (nl !== -1) {\n nextLen = nl + 1;\n } else if (streamDone) {\n // 流结束且没有更多换行:最后一段未以换行结尾的内容一次性露出(此时即完整一行)\n nextLen = source.length;\n }\n\n if (nextLen > curLen) {\n msg._prevReveal = nextLen;\n msg.content = source.substring(0, nextLen);\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n } // 流结束且所有内容都已逐行露出 → 收尾\n\n\n if (streamDone && nextLen >= source.length) {\n _this19.finishStream(); // 后端已在此刻落库,回读一次补全消息 id(单条删除需要)\n\n\n _this19.syncMessageIds();\n }\n }, 30);\n url = (0, _kbChat.createKbStreamUrl)(_this19.activeSessionId, text);\n _this19.eventSource = new EventSource(url);\n\n _this19.eventSource.addEventListener('tool_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n\n if (e.data && _this19.messages[msgIdx]) {\n if (!_this19.messages[msgIdx].toolCalls) _this19.messages[msgIdx].toolCalls = [];\n\n _this19.messages[msgIdx].toolCalls.push(e.data);\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n }\n });\n\n _this19.eventSource.addEventListener('answer', function (e) {\n if (!isCurrentStream() || streamDone) return;\n answerBuffer += e.data;\n });\n\n _this19.eventSource.addEventListener('agent_result', function (e) {\n if (!isCurrentStream() || streamDone) return; // 与档案君(AiChat.vue)对齐:AGENT_RESULT 为最终真相源。\n // 只记录 finalContent 并结束流,不打断已在逐行推进的 content,交给 typeTimer 以 target 推进/校正。\n\n if (e.data) finalContent = e.data;\n streamDone = true;\n\n _this19.closeEventSource();\n });\n\n _this19.eventSource.addEventListener('msgKey', function (e) {\n if (!isCurrentStream()) return;\n\n if (e.data && _this19.messages[msgIdx]) {\n _this19.messages[msgIdx].msgKey = e.data;\n }\n });\n\n _this19.eventSource.addEventListener('reasoning', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking += e.data;\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n });\n\n _this19.eventSource.addEventListener('reasoning_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking = e.data;\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n });\n\n _this19.eventSource.addEventListener('error', function () {\n if (!isCurrentStream() || streamDone) return;\n if (!answerBuffer) answerBuffer = '❌ 连接中断';\n streamDone = true;\n\n _this19.closeEventSource();\n });\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _callee13);\n }))();\n },\n handleStop: function handleStop() {\n this.streamRequestId++;\n\n if (this.activeSessionId) {\n (0, _kbChat.stopKbStream)(this.activeSessionId).catch(function () {});\n }\n\n this.finishStream();\n },\n finishStream: function finishStream() {\n var _this20 = this;\n\n this.clearTypeTimer();\n\n if (this.streamMsg) {\n this.$delete(this.contentCache, this.streamMsg._key);\n this.streamMsg.loading = false;\n this.streamMsg = null;\n }\n\n this.closeEventSource();\n this.streaming = false;\n this.refreshSessionList();\n this.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n },\n\n /** 流式结束后刷新会话列表(更新标题/最后消息) */\n refreshSessionList: function refreshSessionList() {\n var _this21 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee14() {\n var list;\n return regeneratorRuntime.wrap(function _callee14$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n if (_this21.companyId) {\n _context14.next = 2;\n break;\n }\n\n return _context14.abrupt(\"return\");\n\n case 2:\n _context14.prev = 2;\n _context14.next = 5;\n return (0, _kbChat.getKbSessionList)(_this21.companyId);\n\n case 5:\n _context14.t0 = _context14.sent;\n\n if (_context14.t0) {\n _context14.next = 8;\n break;\n }\n\n _context14.t0 = [];\n\n case 8:\n list = _context14.t0;\n _this21.sessionList = list;\n _context14.next = 15;\n break;\n\n case 12:\n _context14.prev = 12;\n _context14.t1 = _context14[\"catch\"](2);\n console.error('刷新会话列表失败', _context14.t1);\n\n case 15:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _callee14, null, [[2, 12]]);\n }))();\n },\n clearTypeTimer: function clearTypeTimer() {\n if (this._typeTimer) {\n clearInterval(this._typeTimer);\n this._typeTimer = null;\n }\n },\n closeEventSource: function closeEventSource() {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n },\n copyText: function copyText(text) {\n if (navigator.clipboard) {\n navigator.clipboard.writeText(text);\n }\n },\n scrollToBottom: function scrollToBottom() {\n var el = this.$refs.messagesRef;\n\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack://ZjPluginIntelligent/./src/lib/knowledgeBase/KbChatMain.vue?./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options");
|
|
1024
|
+
eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! ./node_modules/@babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\").default;\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/asyncToGenerator */ \"./node_modules/@babel/runtime/helpers/asyncToGenerator.js\"));\n\nvar _typeof2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/typeof */ \"./node_modules/@babel/runtime/helpers/typeof.js\"));\n\nvar _logo = _interopRequireDefault(__webpack_require__(/*! @/assets/logo.png */ \"./src/assets/logo.png\"));\n\nvar _markdownIt = _interopRequireDefault(__webpack_require__(/*! markdown-it */ \"./node_modules/markdown-it/index.mjs\"));\n\nvar _kbChat = __webpack_require__(/*! ./api/kbChat */ \"./src/lib/knowledgeBase/api/kbChat.js\");\n\nvar _knowledgeBase = __webpack_require__(/*! ./api/knowledgeBase */ \"./src/lib/knowledgeBase/api/knowledgeBase.js\");\n\nvar _native = __webpack_require__(/*! ./native.js */ \"./src/lib/knowledgeBase/native.js\");\n\nvar _request = __webpack_require__(/*! @lib/utils/request.js */ \"./src/lib/utils/request.js\");\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar AI_AVATAR_URL = 'https://qiguanbang.obs.cn-east-3.myhuaweicloud.com/production/image/AlAgent/doc_query.png'; // 与 ChatDetail 使用相同的 Markdown 配置\n\nvar md = new _markdownIt.default({\n breaks: true,\n html: false,\n linkify: true,\n typographer: true\n});\nmd.enable('table');\nvar _default = {\n name: 'KbChatMain',\n props: {\n companyId: {\n type: String,\n default: ''\n },\n userno: {\n type: String,\n default: ''\n },\n imAccount: {\n type: [String, Object],\n default: ''\n },\n userName: {\n type: String,\n default: ''\n },\n // 是否移动端:由父级(KnowledgeBaseMain)传入;未传时组件自身按窗口宽度判断\n isMobile: {\n type: Boolean,\n default: null\n }\n },\n data: function data() {\n // 首帧前计算是否移动端,避免 H5 先显示 PC 布局再切换的闪烁(与 AI 助理一致)\n // 宿主没有 Element UI 时(H5/平板横屏)一律走自绘 UI 分支,避免 el-* 未知元素报错\n var _isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n\n return {\n initDone: false,\n activeSessionId: '',\n sessionList: [],\n // 默认折叠侧边栏:避免新用户进入时先展开再合住的闪烁,\n // 加载出会话后再按规则决定是否展开\n sidebarCollapsed: true,\n messages: [],\n inputText: '',\n streaming: false,\n streamMsg: null,\n showToolInfo: false,\n contentCache: {},\n eventSource: null,\n streamRequestId: 0,\n suggestedQuestions: [],\n // 消息多选模式(豆包式):PC 由消息操作区「删除/多选」进入,移动端长按气泡弹出菜单进入\n selectMode: false,\n // 已选中的消息 id 列表(后端主键)\n selectedMsgIds: [],\n // 当前公司是否没有知识库文档(无资料时展示遮罩)\n noDocument: false,\n // 移动端状态(仿 AI 助理):showH5List=true 显示会话列表页,false 显示聊天详情页\n localIsMobile: _isMobile,\n showH5List: _isMobile,\n // 移动端会话「…」弹出的操作单\n mobileAction: null,\n // 移动端长按气泡弹出的消息操作单(复制 / 删除 / 多选)\n msgAction: null,\n // 移动端侧滑手势(参考 AI 助理):记录起手坐标与是否已触发\n _swipeStartX: 0,\n _swipeStartY: 0,\n _swipeHandled: false,\n _swipeFromLeftEdge: false,\n _swipeFromRightEdge: false,\n _swipeFromEdge: false\n };\n },\n computed: {\n /** 实际是否移动端:优先用父级 prop,未传则用自身窗口判断 */\n isMobileActive: function isMobileActive() {\n return this.isMobile !== null ? this.isMobile : this.localIsMobile;\n },\n aiAvatar: function aiAvatar() {\n return AI_AVATAR_URL;\n },\n userAvatar: function userAvatar() {\n var account = this.imAccount;\n console.log('KbChatMain - imAccount 原始值:', account); // 尝试解析字符串为对象\n\n if (typeof account === 'string') {\n try {\n account = JSON.parse(account);\n console.log('KbChatMain - imAccount 解析后:', account);\n } catch (e) {\n // 如果解析失败,检查是否是纯 URL 字符串\n if (account.trim() && (account.includes('http://') || account.includes('https://'))) {\n console.log('KbChatMain - imAccount 是 URL:', account.trim());\n return account.trim();\n }\n }\n } // 如果是对象,尝试多个可能的头像字段(优先使用 headImg)\n\n\n if (account && (0, _typeof2.default)(account) === 'object') {\n var avatarFields = ['headImg', 'headPortrait', 'avatar', 'avatarUrl', 'headImage', 'photoUrl', 'portrait', 'headUrl', 'userHeadUrl', 'userAvatar'];\n\n for (var _i = 0, _avatarFields = avatarFields; _i < _avatarFields.length; _i++) {\n var field = _avatarFields[_i];\n\n if (account[field] && String(account[field]).trim()) {\n console.log(\"KbChatMain - \\u627E\\u5230\\u5934\\u50CF\\u5B57\\u6BB5 \".concat(field, \":\"), account[field]);\n return String(account[field]).trim();\n }\n }\n }\n\n console.log('KbChatMain - 使用默认头像');\n return _logo.default;\n },\n // 是否有真实头像(区别于默认 fallback 图片)\n hasUserAvatar: function hasUserAvatar() {\n return this.userAvatar && this.userAvatar !== _logo.default;\n },\n // 无头像时的文字头像(与 zjkjAvatar 一致:取名字最后两个字)\n userAvatarText: function userAvatarText() {\n var name = this.userName || '';\n return name ? name.substring(name.length - 2) : '';\n },\n // 可删除的消息 id(仅后端已落库、带回 id 的消息可删)\n deletableMsgIds: function deletableMsgIds() {\n return this.messages.filter(function (m) {\n return m.id;\n }).map(function (m) {\n return m.id;\n });\n },\n // 是否已全选\n isAllSelected: function isAllSelected() {\n return this.deletableMsgIds.length > 0 && this.selectedMsgIds.length === this.deletableMsgIds.length;\n }\n },\n watch: {\n companyId: function companyId() {\n this.initDone = false;\n this.finishStream();\n this.loadSuggestedQuestions();\n this.checkKbDocuments();\n this.initSession();\n },\n userno: function userno() {\n this.initDone = false;\n this.finishStream();\n this.activeSessionId = '';\n this.messages = [];\n this.initSession();\n },\n // 移动端进入时默认先显示会话列表页(仿 AI 助理)\n isMobileActive: function isMobileActive(val) {\n if (val) {\n // 无资料时遮罩只在详情页可见(列表页 .kb-chat-main 被隐藏),故保持停在详情页\n this.showH5List = !this.noDocument;\n this.mobileAction = null;\n }\n }\n },\n mounted: function mounted() {\n var _this = this;\n\n // 独立使用(父级未传 isMobile)时自检窗口宽度\n this._onResize = this.handleResize;\n window.addEventListener('resize', this._onResize, {\n passive: true\n }); // Android WebView 兼容:在 document 级别监听 touchmove/touchend,\n // 避免可滚动子元素消费事件后不冒泡到父级(参考 AI 助理)\n\n this._swipeMoveHandler = this._onSwipeMove;\n this._swipeEndHandler = this._onSwipeEnd;\n this._popStateHandler = this._onPopState;\n document.addEventListener('touchmove', this._swipeMoveHandler, {\n passive: true\n });\n document.addEventListener('touchend', this._swipeEndHandler, {\n passive: true\n }); // 拦截 Android 系统返回手势:通过 history.pushState + popstate\n\n window.addEventListener('popstate', this._popStateHandler); // 移动端首屏为会话列表页,推入一条历史状态供系统返回拦截\n\n if (this.isMobileActive) {\n history.pushState({\n page: 'kbChatList'\n }, '');\n }\n\n this.initSession();\n this.checkKbDocuments(); // v-show 隐藏期间加载的历史消息无法滚动(scrollHeight 为 0),\n // 监听组件由隐藏变为可见时,滚动到最新问答;\n // 同时每次变为可见时刷新\"猜您想问\"(管理端可能刚更新过)\n\n if (typeof IntersectionObserver !== 'undefined' && this.$el) {\n this._visibilityObserver = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) {\n return e.isIntersecting;\n })) {\n _this.loadSuggestedQuestions(); // 管理端可能刚上传/删除了文档,重新校验遮罩状态\n\n\n _this.checkKbDocuments();\n\n if (_this.messages.length) {\n _this.$nextTick(function () {\n return _this.scrollToBottom();\n });\n }\n }\n });\n\n this._visibilityObserver.observe(this.$el);\n } else {\n this.loadSuggestedQuestions();\n }\n },\n beforeDestroy: function beforeDestroy() {\n if (this._visibilityObserver) {\n this._visibilityObserver.disconnect();\n\n this._visibilityObserver = null;\n }\n\n if (this._onResize) {\n window.removeEventListener('resize', this._onResize);\n }\n\n if (this._swipeMoveHandler) {\n document.removeEventListener('touchmove', this._swipeMoveHandler);\n }\n\n if (this._swipeEndHandler) {\n document.removeEventListener('touchend', this._swipeEndHandler);\n }\n\n if (this._popStateHandler) {\n window.removeEventListener('popstate', this._popStateHandler);\n }\n\n this.streamRequestId++;\n this.closeEventSource();\n this.clearTypeTimer();\n this.onMsgTouchEnd();\n this.contentCache = {};\n },\n methods: {\n // 独立使用时自检移动端(父级已传 isMobile 时忽略)\n handleResize: function handleResize() {\n var now = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n this.localIsMobile = now;\n },\n renderMarkdown: function renderMarkdown(text) {\n if (!text) return '';\n\n try {\n return md.render(text);\n } catch (err) {\n return this.escapeHtml(text);\n }\n },\n // 与 ChatDetail 一致的增量渲染:完整行走 markdown、未完成尾行转义拼接,\n // 借助 markdown-it breaks:true 实现逐行浮现效果。\n renderStreaming: function renderStreaming(text, msgKey) {\n if (!text) return '';\n var cache = this.contentCache[msgKey];\n var lines = text.split('\\n');\n var lastLineIdx = lines.length - 1;\n\n if (!cache) {\n try {\n var html = md.render(text);\n this.$set(this.contentCache, msgKey, {\n renderedLines: lastLineIdx,\n html: html,\n fullText: text\n });\n return html;\n } catch (_unused) {\n return this.escapeHtml(text);\n }\n }\n\n if (text === cache.fullText) return cache.html;\n cache.fullText = text; // 行数没变,仅末尾追加字符,直接拼接尾行\n\n if (lastLineIdx === cache.renderedLines) {\n return cache.html + this.escapeHtml(lines[lastLineIdx]);\n } // 行数变少/文本回退截断,全量重渲染\n\n\n if (lastLineIdx < cache.renderedLines) {\n try {\n var fullLines = lines.slice(0, lastLineIdx);\n var newBaseHtml = '';\n\n if (fullLines.length > 0) {\n newBaseHtml = md.render(fullLines.join('\\n'));\n }\n\n cache.html = newBaseHtml;\n cache.renderedLines = lastLineIdx;\n\n var _lastLine = this.escapeHtml(lines[lastLineIdx]);\n\n return cache.html + (_lastLine || '');\n } catch (_unused2) {\n return this.escapeHtml(text);\n }\n } // 新增完整行\n\n\n if (cache.renderedLines < lastLineIdx) {\n var allFullLines = lines.slice(0, lastLineIdx);\n var _newBaseHtml = '';\n\n if (allFullLines.length > 0) {\n _newBaseHtml = md.render(allFullLines.join('\\n'));\n }\n\n cache.html = _newBaseHtml;\n cache.renderedLines = lastLineIdx;\n }\n\n var lastLine = this.escapeHtml(lines[lastLineIdx]);\n cache.renderedLines = lastLineIdx;\n return cache.html + (lastLine || '');\n },\n escapeHtml: function escapeHtml(text) {\n if (!text) return '';\n var map = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(text).replace(/[&<>\"']/g, function (c) {\n return map[c];\n });\n },\n autoResizeTextarea: function autoResizeTextarea() {\n var ta = this.$refs.taRef;\n if (!ta) return; // 移动端从单行起增高(与 38px 最小高度一致),PC 保持 100px 基准\n\n var baseH = this.isMobileActive ? 38 : 100;\n var maxH = this.isMobileActive ? 120 : 180;\n ta.style.height = \"\".concat(baseH, \"px\");\n var realH = ta.scrollHeight;\n\n if (realH > baseH) {\n ta.style.height = \"\".concat(Math.min(realH, maxH), \"px\");\n }\n },\n newLine: function newLine(e) {\n var _this2 = this;\n\n e.preventDefault();\n var ta = this.$refs.taRef;\n var start = ta.selectionStart;\n var end = ta.selectionEnd;\n this.inputText = this.inputText.slice(0, start) + '\\n' + this.inputText.slice(end);\n this.$nextTick(function () {\n ta.selectionStart = ta.selectionEnd = start + 1;\n\n _this2.autoResizeTextarea();\n });\n },\n\n /**\n * 加载\"猜您想问\"问题(按公司,所有用户可见)\n */\n loadSuggestedQuestions: function loadSuggestedQuestions() {\n var _this3 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {\n var list;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n if (_this3.companyId) {\n _context.next = 3;\n break;\n }\n\n _this3.suggestedQuestions = [];\n return _context.abrupt(\"return\");\n\n case 3:\n _context.prev = 3;\n _context.next = 6;\n return (0, _knowledgeBase.getSuggestedQuestions)(_this3.companyId);\n\n case 6:\n _context.t0 = _context.sent;\n\n if (_context.t0) {\n _context.next = 9;\n break;\n }\n\n _context.t0 = [];\n\n case 9:\n list = _context.t0;\n _this3.suggestedQuestions = list.map(function (q) {\n return q.questionText;\n });\n _context.next = 16;\n break;\n\n case 13:\n _context.prev = 13;\n _context.t1 = _context[\"catch\"](3);\n console.error('加载猜您想问失败', _context.t1);\n\n case 16:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, null, [[3, 13]]);\n }))();\n },\n\n /**\n * 检查当前公司是否有「已索引」的知识库文档,无可用资料时展示遮罩提示\n * 仅已索引(indexStatus === 1)的文档才能被检索,未索引/索引中/索引失败都不算\n */\n checkKbDocuments: function checkKbDocuments() {\n var _this4 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var data, isEmpty;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n if (_this4.companyId) {\n _context2.next = 3;\n break;\n }\n\n _this4.noDocument = false;\n return _context2.abrupt(\"return\");\n\n case 3:\n _context2.prev = 3;\n _context2.next = 6;\n return (0, _knowledgeBase.getFileList)({\n companyId: _this4.companyId,\n indexStatus: 1,\n pageNum: 1,\n pageSize: 1\n });\n\n case 6:\n data = _context2.sent;\n isEmpty = (data && data.total || 0) === 0;\n _this4.noDocument = isEmpty; // 无资料时把会话固定回第一个\n\n if (isEmpty) {\n _this4.lockFirstSession(); // 无资料遮罩位于聊天详情页的 .kb-chat-main 内,而移动端首屏是会话列表页\n // (h5-list-mode 下 .kb-chat-main 被 display:none 隐藏),若停留在列表页\n // 则遮罩永远不可见;故无资料时强制切到详情页把遮罩展示出来\n\n\n if (_this4.isMobileActive) {\n _this4.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n }\n }\n\n _context2.next = 16;\n break;\n\n case 12:\n _context2.prev = 12;\n _context2.t0 = _context2[\"catch\"](3);\n // 查询失败(如无管理权限)时不展示遮罩,避免误拦截\n console.error('检查知识库文档失败', _context2.t0);\n _this4.noDocument = false;\n\n case 16:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, null, [[3, 12]]);\n }))();\n },\n initSession: function initSession() {\n var _this5 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {\n var list;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n if (_this5.companyId) {\n _context3.next = 2;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 2:\n if (!(_this5.initDone && (_this5.messages.length || _this5.streaming || _this5.inputText))) {\n _context3.next = 4;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 4:\n _this5.initDone = false;\n _context3.prev = 5;\n _context3.next = 8;\n return (0, _kbChat.getKbSessionList)(_this5.companyId);\n\n case 8:\n _context3.t0 = _context3.sent;\n\n if (_context3.t0) {\n _context3.next = 11;\n break;\n }\n\n _context3.t0 = [];\n\n case 11:\n list = _context3.t0;\n _this5.sessionList = list;\n\n if (!(list.length > 0)) {\n _context3.next = 19;\n break;\n }\n\n // 只有 1 个会话且无消息(新用户首次进入自动生成的空会话)时,\n // 默认折叠侧边栏只显示聊天主界面;在 selectSession 前同步设置,\n // 避免「先展开再合住」的闪烁\n _this5.sidebarCollapsed = list.length === 1 && !list[0].lastContent;\n _context3.next = 17;\n return _this5.selectSession(list[0].sessionId);\n\n case 17:\n _context3.next = 21;\n break;\n\n case 19:\n _context3.next = 21;\n return _this5.autoCreateSession();\n\n case 21:\n _context3.next = 28;\n break;\n\n case 23:\n _context3.prev = 23;\n _context3.t1 = _context3[\"catch\"](5);\n console.error('获取会话列表失败,尝试自动创建', _context3.t1);\n _context3.next = 28;\n return _this5.autoCreateSession();\n\n case 28:\n _context3.prev = 28;\n _this5.initDone = true;\n return _context3.finish(28);\n\n case 31:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _callee3, null, [[5, 23, 28, 31]]);\n }))();\n },\n autoCreateSession: function autoCreateSession() {\n var _this6 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {\n var data;\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n if (_this6.companyId) {\n _context4.next = 3;\n break;\n }\n\n console.warn('[KbChat] 无法创建会话:companyId 为空');\n return _context4.abrupt(\"return\");\n\n case 3:\n console.log('[KbChat] 创建会话,companyId:', _this6.companyId);\n _context4.prev = 4;\n _context4.next = 7;\n return (0, _kbChat.createKbSession)(_this6.companyId);\n\n case 7:\n data = _context4.sent;\n console.log('[KbChat] 会话创建成功,sessionId:', data.sessionId);\n\n _this6.finishStream();\n\n _this6.activeSessionId = data.sessionId;\n _this6.streamRequestId++;\n _this6.messages = [];\n _this6.sessionList = [{\n sessionId: data.sessionId,\n title: '',\n lastContent: ''\n }].concat(_this6.sessionList || []); // 新建会话时保持侧边栏展开,方便继续切换会话\n\n _this6.sidebarCollapsed = false;\n\n _this6.$nextTick(function () {\n return _this6.scrollToBottom();\n });\n\n _context4.next = 21;\n break;\n\n case 18:\n _context4.prev = 18;\n _context4.t0 = _context4[\"catch\"](4);\n console.error('自动创建会话失败', _context4.t0);\n\n case 21:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _callee4, null, [[4, 18]]);\n }))();\n },\n\n /** 新建会话(豆包式「新对话」) */\n handleNewSession: function handleNewSession() {\n var _this7 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee5() {\n return regeneratorRuntime.wrap(function _callee5$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n if (!_this7.noDocument) {\n _context5.next = 2;\n break;\n }\n\n return _context5.abrupt(\"return\");\n\n case 2:\n if (_this7.streaming) _this7.handleStop();\n _context5.next = 5;\n return _this7.autoCreateSession();\n\n case 5:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _callee5);\n }))();\n },\n\n /** 切换会话 */\n handleSelectSession: function handleSelectSession(item) {\n var _this8 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee6() {\n return regeneratorRuntime.wrap(function _callee6$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context6.next = 2;\n break;\n }\n\n return _context6.abrupt(\"return\");\n\n case 2:\n if (!_this8.noDocument) {\n _context6.next = 5;\n break;\n }\n\n _this8.lockFirstSession();\n\n return _context6.abrupt(\"return\");\n\n case 5:\n if (!(item.sessionId === _this8.activeSessionId && _this8.messages.length)) {\n _context6.next = 8;\n break;\n }\n\n if (_this8.isMobileActive) {\n _this8.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n return _context6.abrupt(\"return\");\n\n case 8:\n _context6.next = 10;\n return _this8.selectSession(item.sessionId);\n\n case 10:\n // 移动端:选中后跳到聊天详情页\n if (_this8.isMobileActive) {\n _this8.showH5List = false; // 推入历史状态,使 Android 系统返回手势先触发 popstate 被拦截\n\n history.pushState({\n page: 'kbChatDetail'\n }, ''); // 列表页时聊天区被 display:none,scrollHeight 为 0 导致上面的滚动无效;\n // 切到详情页、容器可见后需再滚一次,默认定位到最新消息\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n case 11:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _callee6);\n }))();\n },\n\n /** 返回宿主 App 原生页面(参考 AI 助理 handleNativeBack) */\n handleNativeBack: function handleNativeBack() {\n if (this.$hybrid && typeof this.$hybrid.closePage === 'function') {\n this.$hybrid.closePage();\n } else if (window.history.length > 1) {\n window.history.back();\n }\n },\n\n /** 会话列表页返回按钮:返回原生页面 */\n handleSidebarBack: function handleSidebarBack() {\n this.handleNativeBack();\n },\n // ── H5 侧滑手势(参考 AI 助理)──\n // 从左/右屏幕边缘起手横滑触发返回,避免页面内部横向滚动被误判\n onSwipeTouchStart: function onSwipeTouchStart(e) {\n if (!this.isMobileActive) return;\n var t = e.touches[0];\n this._swipeStartX = t.clientX;\n this._swipeStartY = t.clientY;\n this._swipeHandled = false;\n var edge = Math.min(56, Math.round(window.innerWidth * 0.09));\n this._swipeFromLeftEdge = t.clientX <= edge;\n this._swipeFromRightEdge = t.clientX >= window.innerWidth - edge;\n this._swipeFromEdge = this._swipeFromLeftEdge || this._swipeFromRightEdge;\n },\n _swipeShouldTrigger: function _swipeShouldTrigger(diffX, diffY) {\n if (!this._swipeFromEdge) return false; // 从左边缘 → 右滑;从右边缘 → 左滑\n\n var directionOk = this._swipeFromLeftEdge && diffX >= 60 || this._swipeFromRightEdge && diffX <= -60;\n if (!directionOk) return false; // 水平距离明显大于垂直距离(排除上下滚动干扰)\n\n return Math.abs(diffX) >= Math.abs(diffY) * 2;\n },\n _onSwipeMove: function _onSwipeMove(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.touches || !e.touches.length) return;\n var diffX = e.touches[0].clientX - this._swipeStartX;\n var diffY = e.touches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n // touchend 兜底,兼容不支持 touchmove 的场景\n _onSwipeEnd: function _onSwipeEnd(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.changedTouches || !e.changedTouches.length) return;\n var diffX = e.changedTouches[0].clientX - this._swipeStartX;\n var diffY = e.changedTouches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n\n /** 侧滑触发:列表页 → 返回原生页面;详情页 → 回会话列表页 */\n _triggerSwipeAction: function _triggerSwipeAction() {\n if (this.showH5List) {\n this.handleNativeBack();\n } else {\n this.handleMobileBack();\n }\n },\n\n /**\n * 拦截 Android 系统返回手势(history.pushState + popstate)\n * WebView 先回退历史栈触发 popstate,这里拦截后导航到列表页,避免 Activity 被直接关闭\n */\n _onPopState: function _onPopState() {\n if (!this.isMobileActive) return;\n\n if (!this.showH5List) {\n // 详情页 → 返回会话列表页\n this.showH5List = true;\n history.pushState({\n page: 'kbChatList'\n }, '');\n } else {\n // 列表页 → 直接关闭 WebView 返回原生页面\n this.handleNativeBack();\n }\n },\n\n /** 移动端详情页返回:无资料遮罩时返回原生页面;否则回到会话列表页 */\n handleMobileBack: function handleMobileBack() {\n // 无资料遮罩期间无法进入会话,返回到宿主 App 原生页面\n if (this.noDocument) {\n this.handleNativeBack();\n return;\n } // 正常情况:回到会话列表页\n\n\n this.showH5List = true;\n },\n\n /** 无资料遮罩期间:把当前会话固定回列表中的第一个 */\n lockFirstSession: function lockFirstSession() {\n var first = this.sessionList[0];\n\n if (first && first.sessionId && first.sessionId !== this.activeSessionId) {\n this.selectSession(first.sessionId);\n }\n },\n\n /** 会话项「…」菜单:重命名 / 删除 */\n handleSessionMenu: function handleSessionMenu(cmd, item) {\n if (!item || !item.sessionId) return;\n\n if (cmd === 'rename') {\n this.handleRenameSession(item);\n } else if (cmd === 'delete') {\n this.handleDeleteSession(item);\n }\n },\n // ==================== 移动端会话操作单 ====================\n openMobileAction: function openMobileAction(item) {\n if (this.noDocument) return;\n this.mobileAction = item;\n },\n doMobileRename: function doMobileRename(item) {\n this.mobileAction = null;\n this.handleRenameSession(item);\n },\n doMobileDelete: function doMobileDelete(item) {\n this.mobileAction = null;\n this.handleDeleteSession(item);\n },\n\n /** 重命名会话 */\n handleRenameSession: function handleRenameSession(item) {\n var _this9 = this;\n\n if (!item || !item.sessionId) return;\n this.$prompt('请输入新的会话名称', '重命名会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n inputValue: item.title || '',\n inputPattern: /\\S+/,\n inputErrorMessage: '名称不能为空'\n }).then(function (_ref) {\n var value = _ref.value;\n var title = (value || '').trim();\n if (!title) return;\n return (0, _kbChat.renameKbSession)(item.sessionId, title).then(function () {\n item.title = title;\n\n _this9.$message.success('重命名成功');\n });\n }).catch(function (err) {\n // 取消或校验失败,不提示错误\n if (err && err !== 'cancel' && err !== 'close') {\n console.error('重命名失败', err);\n\n _this9.$message.error('重命名失败');\n }\n });\n },\n\n /** 删除会话 */\n handleDeleteSession: function handleDeleteSession(item) {\n var _this10 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {\n return regeneratorRuntime.wrap(function _callee7$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context7.next = 2;\n break;\n }\n\n return _context7.abrupt(\"return\");\n\n case 2:\n if (!(_this10.sessionList.length <= 1)) {\n _context7.next = 5;\n break;\n }\n\n _this10.$message.warning('至少保留一个会话');\n\n return _context7.abrupt(\"return\");\n\n case 5:\n _context7.prev = 5;\n _context7.next = 8;\n return _this10.$confirm('删除后无法恢复,确定删除该会话吗?', '删除会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 8:\n _context7.next = 13;\n break;\n\n case 10:\n _context7.prev = 10;\n _context7.t0 = _context7[\"catch\"](5);\n return _context7.abrupt(\"return\");\n\n case 13:\n _context7.prev = 13;\n _context7.next = 16;\n return (0, _kbChat.deleteKbSession)(item.sessionId);\n\n case 16:\n _this10.sessionList = (_this10.sessionList || []).filter(function (s) {\n return s.sessionId !== item.sessionId;\n });\n\n if (!(_this10.activeSessionId === item.sessionId)) {\n _context7.next = 28;\n break;\n }\n\n _this10.finishStream();\n\n _this10.activeSessionId = '';\n _this10.messages = [];\n\n if (!(_this10.sessionList.length > 0)) {\n _context7.next = 26;\n break;\n }\n\n _context7.next = 24;\n return _this10.selectSession(_this10.sessionList[0].sessionId);\n\n case 24:\n _context7.next = 28;\n break;\n\n case 26:\n _context7.next = 28;\n return _this10.autoCreateSession();\n\n case 28:\n _this10.$message.success('删除成功');\n\n _context7.next = 35;\n break;\n\n case 31:\n _context7.prev = 31;\n _context7.t1 = _context7[\"catch\"](13);\n console.error('删除会话失败', _context7.t1);\n\n _this10.$message.error('删除失败');\n\n case 35:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _callee7, null, [[5, 10], [13, 31]]);\n }))();\n },\n\n /** 会话列表时间格式化:今天显示时分,更早显示月日 */\n formatTime: function formatTime(time) {\n if (!time) return '';\n var d = new Date(time);\n if (isNaN(d.getTime())) return '';\n var now = new Date();\n var sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();\n\n var pad = function pad(n) {\n return n < 10 ? '0' + n : '' + n;\n };\n\n if (sameDay) {\n return pad(d.getHours()) + ':' + pad(d.getMinutes());\n }\n\n return pad(d.getMonth() + 1) + '-' + pad(d.getDate());\n },\n\n /** 后端消息 → 前端消息对象(保留 id/msgKey,删除功能需要) */\n mapMessage: function mapMessage(m, i) {\n var thinking = m.thinking || m.reasoning || m.reasoning_result || m.reasoningContent || m.reasoning_content || m.thinkingContent || m.thinking_content || '';\n return {\n id: m.id,\n msgKey: m.msgKey || m.msg_key || '',\n role: m.role,\n content: m.content || '',\n thinking: thinking,\n _key: m.createTime ? 'msg_' + m.createTime + '_' + i : 'msg_' + Date.now() + '_' + Math.random()\n };\n },\n selectSession: function selectSession(sessionId) {\n var _this11 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee8() {\n var raw, msgs;\n return regeneratorRuntime.wrap(function _callee8$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n if (_this11.streaming) _this11.handleStop();\n\n _this11.exitSelectMode();\n\n _this11.msgAction = null;\n _this11.activeSessionId = sessionId;\n _this11.messages = [];\n _this11.streamMsg = null;\n _context8.prev = 6;\n _context8.next = 9;\n return (0, _kbChat.getKbSessionMessages)(sessionId);\n\n case 9:\n raw = _context8.sent;\n msgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || [];\n console.log('[KbChat] selectSession 回读', {\n sessionId: sessionId,\n rawIsArray: Array.isArray(raw),\n rawKeys: raw && !Array.isArray(raw) ? Object.keys(raw) : null,\n count: msgs.length,\n list: msgs.map(function (m) {\n return {\n id: m.id,\n role: m.role,\n content: String(m.content || '').slice(0, 25)\n };\n })\n });\n _this11.messages = _this11.dedupeMessages(msgs).map(function (m, i) {\n return _this11.mapMessage(m, i);\n });\n\n _this11.$nextTick(function () {\n return _this11.scrollToBottom();\n });\n\n _context8.next = 19;\n break;\n\n case 16:\n _context8.prev = 16;\n _context8.t0 = _context8[\"catch\"](6);\n console.error('加载消息失败', _context8.t0);\n\n case 19:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _callee8, null, [[6, 16]]);\n }))();\n },\n\n /**\n * 后端回读消息去重。\n * 后端存在同一问答对被重复落库的情况(同一会话里出现两套相同的「问+答」),\n * 直接渲染会让用户看到重复问答。这里按「问答对」为单位去重:\n * 把消息切成组(用户问 + 其后的 AI 答为一组),整组内容完全相同的只保留第一次出现的。\n * 相比「只和上一条比」的相邻去重,能覆盖组与组之间被隔开、或 content 有空白差异的情况。\n */\n dedupeMessages: function dedupeMessages(msgs) {\n var _this12 = this;\n\n // 先切组:以 user 消息为一个新组的起点,其后连续的 assistant 归入同组\n var groups = [];\n msgs.forEach(function (m) {\n var role = _this12.normRole(m.role);\n\n if (role === 'user' || !groups.length) {\n groups.push([m]);\n } else {\n groups[groups.length - 1].push(m);\n }\n });\n var seen = {};\n var out = [];\n groups.forEach(function (group) {\n var signature = group.map(function (m) {\n return _this12.normRole(m.role) + \"\\x01\" + String(m.content || '').replace(/\\s+/g, '');\n }).join(\"\\x02\");\n if (signature && seen[signature]) return;\n if (signature) seen[signature] = true;\n out.push.apply(out, group);\n });\n return out;\n },\n\n /**\n * 回读当前会话消息,补全后端持久化后生成的 id。\n * 流式回答期间消息只存在于本地数组(无 id),无法按 id 删除;\n * 流式结束后调用一次即可拿到 id。已全部有 id 时跳过,避免多余请求。\n *\n * 注意:不能直接用后端数组整体覆盖 this.messages。后端回读可能包含重复/多余记录\n * (如同一会话里用户消息被落库两次),若整体覆盖会把重复消息直接渲染出来。\n * 这里以本地数组为基准,只用后端记录按「角色 + 内容」顺序回填 id,保证界面条数不变。\n */\n syncMessageIds: function syncMessageIds() {\n var _this13 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee9() {\n var raw, backendMsgs, cursor;\n return regeneratorRuntime.wrap(function _callee9$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n if (!(!_this13.activeSessionId || _this13.streaming)) {\n _context9.next = 2;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 2:\n if (!(_this13.messages.length && _this13.messages.every(function (m) {\n return m.id;\n }))) {\n _context9.next = 4;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 4:\n _context9.prev = 4;\n _context9.next = 7;\n return (0, _kbChat.getKbSessionMessages)(_this13.activeSessionId);\n\n case 7:\n raw = _context9.sent;\n backendMsgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || []; // 按顺序把本地消息与后端记录配对(角色一致且内容匹配),只补 id/msgKey\n\n cursor = 0;\n\n _this13.messages.forEach(function (m) {\n if (m.id) return;\n\n var role = _this13.normRole(m.role);\n\n for (var i = cursor; i < backendMsgs.length; i++) {\n var b = backendMsgs[i];\n\n if (_this13.normRole(b.role) === role && String(b.content || '').trim() === String(m.content || '').trim()) {\n m.id = b.id;\n if (!m.msgKey) m.msgKey = b.msgKey || b.msg_key || '';\n cursor = i + 1;\n return;\n }\n }\n });\n\n _context9.next = 16;\n break;\n\n case 13:\n _context9.prev = 13;\n _context9.t0 = _context9[\"catch\"](4);\n console.error('同步消息 id 失败', _context9.t0);\n\n case 16:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _callee9, null, [[4, 13]]);\n }))();\n },\n // ==================== 消息多选 / 删除 ====================\n\n /** 进入多选模式(preselectMsg 传入时默认勾选该条所在的整组问答,对应豆包式「删除」入口) */\n enterSelectMode: function enterSelectMode(preselectMsg) {\n var _this14 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee10() {\n var preselectId, idx;\n return regeneratorRuntime.wrap(function _callee10$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n if (!(_this14.streaming || !_this14.messages.length)) {\n _context10.next = 2;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 2:\n // 点击条的后端持久化 id(模板上「删除」按钮仅在 msg.id 存在时渲染,故必有值)\n preselectId = preselectMsg ? preselectMsg.id : null; // 先回读消息确保每条都有 id(刚流式完成的消息本地没有 id)。\n // 回读只补 id、不重建数组,定位可用 id 或对象引用。\n\n _context10.next = 5;\n return _this14.syncMessageIds();\n\n case 5:\n _this14.selectMode = true;\n _this14.selectedMsgIds = [];\n idx = preselectId != null ? _this14.messages.findIndex(function (m) {\n return m.id === preselectId;\n }) : -1;\n if (idx === -1 && preselectMsg) idx = _this14.messages.indexOf(preselectMsg);\n\n if (idx !== -1) {\n // 默认勾选整组问答(用户问 + AI 答),便于一键删除\n _this14.selectedMsgIds = _this14.getQaPairIdsAt(idx);\n } // 滚动定位:让默认勾选的整组问答都进入视野(长回答会把用户问顶出屏幕)\n\n\n _this14.scrollSelectedIntoView();\n\n case 11:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _callee10);\n }))();\n },\n\n /**\n * 把首个被选中的消息行滚动到容器内可见。\n * 选中行是「用户问 + AI 答」中的靠前一条,滚到它即能同时看到整组。\n * 若靠前那条已在视野内则不动,避免无谓跳动。\n */\n scrollSelectedIntoView: function scrollSelectedIntoView() {\n var _this15 = this;\n\n this.$nextTick(function () {\n var container = _this15.$refs.messagesRef;\n if (!container) return;\n var rows = container.querySelectorAll('.msg-row');\n if (!rows.length) return; // 找到第一个处于选中态、且带 id 的行(与 selectedMsgIds 顺序无关,取 DOM 顺序)\n\n var target = null;\n\n for (var i = 0; i < rows.length; i++) {\n var box = rows[i].querySelector('.msg-check-box.checked');\n\n if (box) {\n target = rows[i];\n break;\n }\n }\n\n if (!target) return;\n var cRect = container.getBoundingClientRect();\n var tRect = target.getBoundingClientRect(); // 目标行顶部已在容器可视区内则无需滚动\n\n if (tRect.top >= cRect.top && tRect.top < cRect.bottom) return; // 滚动到目标行顶部(留 8px 间距)\n\n container.scrollTop += tRect.top - cRect.top - 8;\n });\n },\n exitSelectMode: function exitSelectMode() {\n this.selectMode = false;\n this.selectedMsgIds = [];\n },\n isMsgSelected: function isMsgSelected(msg) {\n return !!msg.id && this.selectedMsgIds.indexOf(msg.id) !== -1;\n },\n\n /** 归一化 role,兼容后端可能返回的大小写/空白 */\n normRole: function normRole(role) {\n return String(role == null ? '' : role).trim().toLowerCase();\n },\n\n /**\n * 取与下标 idx 处消息成对的另一条:用户问 ↔ AI 答视为一组问答。\n * 只认紧邻的一条(问答对在列表里必然相邻),相邻非同组则不配对。\n * 不依赖 role 的具体拼写(user/assistant 的大小写、空白均容错)。\n */\n getQaPartnerAt: function getQaPartnerAt(idx) {\n if (idx < 0 || idx >= this.messages.length) return null;\n var cur = this.messages[idx];\n if (!cur) return null;\n var role = this.normRole(cur.role);\n if (role !== 'user' && role !== 'assistant') return null; // 只认紧邻的那一条:问答对在列表里必然相邻。\n // 若相邻消息不是异角色(例如回答已被删除),说明当前消息已无配对,\n // 不能继续向前/向后扫描,否则会误勾到上一个问答的回答。\n\n var neighbor = role === 'user' ? this.messages[idx + 1] : this.messages[idx - 1];\n if (!neighbor) return null;\n var neighborRole = this.normRole(neighbor.role);\n var expect = role === 'user' ? 'assistant' : 'user';\n return neighborRole === expect ? neighbor : null;\n },\n\n /** 下标 idx 处消息所在问答对的可删 id 列表(用户问 + AI 答) */\n getQaPairIdsAt: function getQaPairIdsAt(idx) {\n var ids = [];\n var cur = this.messages[idx];\n if (cur && cur.id) ids.push(cur.id);\n var partner = this.getQaPartnerAt(idx);\n if (partner && partner.id && ids.indexOf(partner.id) === -1) ids.push(partner.id);\n return ids;\n },\n\n /**\n * 勾选单条消息:勾上时连带把问答对里另一条一起勾上;\n * 取消时只取消当前这条,从而支持「先整组勾选、再单独取消一条」的单条选择。\n */\n toggleSelectMessage: function toggleSelectMessage(msg) {\n var _this16 = this;\n\n if (!msg.id) return;\n var i = this.selectedMsgIds.indexOf(msg.id);\n\n if (i === -1) {\n var idx = this.messages.indexOf(msg);\n var pairIds = idx === -1 ? [msg.id] : this.getQaPairIdsAt(idx);\n pairIds.forEach(function (id) {\n if (_this16.selectedMsgIds.indexOf(id) === -1) _this16.selectedMsgIds.push(id);\n });\n } else {\n this.selectedMsgIds.splice(i, 1);\n }\n },\n toggleSelectAll: function toggleSelectAll() {\n var ids = this.deletableMsgIds;\n this.selectedMsgIds = this.selectedMsgIds.length === ids.length ? [] : ids.slice();\n },\n\n /** 多选模式下点击整行即切换选中 */\n onMsgRowClick: function onMsgRowClick(msg) {\n // 长按弹出菜单后浏览器还会补一次 click,短暂屏蔽避免误触\n if (Date.now() < this._ignoreClickUntil) return;\n if (this.selectMode) this.toggleSelectMessage(msg);\n },\n\n /** 移动端长按气泡弹出消息操作单(豆包式:复制 / 删除 / 多选) */\n onMsgTouchStart: function onMsgTouchStart(msg) {\n var _this17 = this;\n\n if (!this.isMobileActive || this.selectMode) return;\n this._longPressMsg = msg;\n clearTimeout(this._longPressTimer);\n this._longPressTimer = setTimeout(function () {\n _this17._longPressTimer = null; // 用户正在长按选中文本(复制用),此时不弹菜单,避免抢走选择行为\n\n var sel = window.getSelection && window.getSelection();\n if (sel && String(sel).trim()) return;\n\n _this17.openMsgAction(_this17._longPressMsg);\n }, 500);\n },\n onMsgTouchEnd: function onMsgTouchEnd() {\n if (this._longPressTimer) {\n clearTimeout(this._longPressTimer);\n this._longPressTimer = null;\n }\n },\n openMsgAction: function openMsgAction(msg) {\n this.msgAction = msg; // 长按抬手后浏览器可能补发 click,短暂屏蔽,避免刚弹出的菜单被立即关闭\n\n this._ignoreClickUntil = Date.now() + 350;\n },\n closeMsgAction: function closeMsgAction() {\n if (Date.now() < this._ignoreClickUntil) return;\n this.msgAction = null;\n },\n\n /** 移动端消息操作单:复制 / 删除(删除进入多选模式,与豆包一致) */\n doMsgAction: function doMsgAction(action) {\n var msg = this.msgAction;\n this.msgAction = null;\n if (!msg) return;\n\n if (action === 'copy') {\n this.copyText(msg.content);\n } else if (action === 'delete') {\n this.enterSelectMode(msg);\n }\n },\n\n /** 批量删除选中的消息 */\n handleDeleteSelected: function handleDeleteSelected() {\n var _this18 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee11() {\n var ids;\n return regeneratorRuntime.wrap(function _callee11$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n if (!(!_this18.selectedMsgIds.length || !_this18.activeSessionId)) {\n _context11.next = 2;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 2:\n ids = _this18.selectedMsgIds.slice();\n _context11.prev = 3;\n _context11.next = 6;\n return _this18.$confirm(\"\\u5220\\u9664\\u540E\\u65E0\\u6CD5\\u6062\\u590D\\uFF0C\\u786E\\u5B9A\\u5220\\u9664\\u9009\\u4E2D\\u7684 \".concat(ids.length, \" \\u6761\\u6D88\\u606F\\u5417\\uFF1F\"), '删除消息', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 6:\n _context11.next = 11;\n break;\n\n case 8:\n _context11.prev = 8;\n _context11.t0 = _context11[\"catch\"](3);\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.prev = 11;\n _context11.next = 14;\n return (0, _kbChat.deleteKbMessages)(_this18.activeSessionId, ids);\n\n case 14:\n _this18.messages = _this18.messages.filter(function (m) {\n return ids.indexOf(m.id) === -1;\n });\n\n _this18.exitSelectMode();\n\n _this18.$message.success('删除成功');\n\n _this18.refreshSessionList();\n\n _context11.next = 24;\n break;\n\n case 20:\n _context11.prev = 20;\n _context11.t1 = _context11[\"catch\"](11);\n console.error('批量删除消息失败', _context11.t1);\n\n _this18.$message.error('删除失败');\n\n case 24:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _callee11, null, [[3, 8], [11, 20]]);\n }))();\n },\n handleResetSession: function handleResetSession() {\n var _this19 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee12() {\n return regeneratorRuntime.wrap(function _callee12$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n if (!_this19.activeSessionId) {\n _context12.next = 8;\n break;\n }\n\n _context12.prev = 1;\n _context12.next = 4;\n return (0, _kbChat.closeKbSession)(_this19.activeSessionId);\n\n case 4:\n _context12.next = 8;\n break;\n\n case 6:\n _context12.prev = 6;\n _context12.t0 = _context12[\"catch\"](1);\n\n case 8:\n _context12.next = 10;\n return _this19.autoCreateSession();\n\n case 10:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _callee12, null, [[1, 6]]);\n }))();\n },\n handleSend: function handleSend(quickText) {\n var _this20 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee13() {\n var text, requestSessionId, requestId, msgKey, msgIdx, streamMsg, isCurrentStream, answerBuffer, finalContent, streamDone, url;\n return regeneratorRuntime.wrap(function _callee13$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n text = typeof quickText === 'string' ? quickText.trim() : _this20.inputText.trim();\n\n if (!(!text || !_this20.activeSessionId || _this20.streaming)) {\n _context13.next = 3;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 3:\n _this20.exitSelectMode();\n\n _this20.inputText = '';\n requestSessionId = _this20.activeSessionId;\n requestId = ++_this20.streamRequestId;\n\n _this20.clearTypeTimer();\n\n _this20.messages.push({\n role: 'user',\n content: text\n });\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n }); // 流式消息:作为 messages 中的对象(带 _key/loading),与 ChatDetail 一致\n\n\n msgKey = 'stream_' + requestId;\n msgIdx = _this20.messages.push({\n role: 'assistant',\n content: '',\n thinking: '',\n toolCalls: [],\n loading: true,\n _key: msgKey\n }) - 1;\n streamMsg = _this20.messages[msgIdx];\n _this20.streamMsg = streamMsg;\n _this20.streaming = true;\n\n isCurrentStream = function isCurrentStream() {\n return requestId === _this20.streamRequestId && requestSessionId === _this20.activeSessionId && _this20.streamMsg === streamMsg;\n }; // SSE 只把数据累积到 answerBuffer。typeTimer(30ms) 节流按「完整换行」逐行把内容\n // 推进到 msg.content:每个 tick 最多只推进「到下一个 \\n 为止」的一行,未完成的尾行\n // 保持不动;配合 renderStreaming(breaks:true) 实现「一行显示一行」。\n // 不是打字机模式(不逐字)、也不是一次性整段渲染(不会一出就是全文)。\n\n\n answerBuffer = '';\n finalContent = '';\n streamDone = false;\n _this20._typeTimer = setInterval(function () {\n if (!isCurrentStream()) return;\n var msg = _this20.messages[msgIdx];\n var source = finalContent || answerBuffer;\n var curLen = msg._prevReveal || 0;\n var nextLen = curLen; // 找到「当前已揭示进度之后」的第一条完整换行,仅推进这一行\n\n var nl = source.indexOf('\\n', curLen);\n\n if (nl !== -1) {\n nextLen = nl + 1;\n } else if (streamDone) {\n // 流结束且没有更多换行:最后一段未以换行结尾的内容一次性露出(此时即完整一行)\n nextLen = source.length;\n }\n\n if (nextLen > curLen) {\n msg._prevReveal = nextLen;\n msg.content = source.substring(0, nextLen);\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n } // 流结束且所有内容都已逐行露出 → 收尾\n\n\n if (streamDone && nextLen >= source.length) {\n _this20.finishStream(); // 后端已在此刻落库,回读一次补全消息 id(单条删除需要)\n\n\n _this20.syncMessageIds();\n }\n }, 30);\n url = (0, _kbChat.createKbStreamUrl)(_this20.activeSessionId, text);\n _this20.eventSource = new EventSource(url);\n\n _this20.eventSource.addEventListener('tool_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n\n if (e.data && _this20.messages[msgIdx]) {\n if (!_this20.messages[msgIdx].toolCalls) _this20.messages[msgIdx].toolCalls = [];\n\n _this20.messages[msgIdx].toolCalls.push(e.data);\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n }\n });\n\n _this20.eventSource.addEventListener('answer', function (e) {\n if (!isCurrentStream() || streamDone) return;\n answerBuffer += e.data;\n });\n\n _this20.eventSource.addEventListener('agent_result', function (e) {\n if (!isCurrentStream() || streamDone) return; // 与档案君(AiChat.vue)对齐:AGENT_RESULT 为最终真相源。\n // 只记录 finalContent 并结束流,不打断已在逐行推进的 content,交给 typeTimer 以 target 推进/校正。\n\n if (e.data) finalContent = e.data;\n streamDone = true;\n\n _this20.closeEventSource();\n });\n\n _this20.eventSource.addEventListener('msgKey', function (e) {\n if (!isCurrentStream()) return;\n\n if (e.data && _this20.messages[msgIdx]) {\n _this20.messages[msgIdx].msgKey = e.data;\n }\n });\n\n _this20.eventSource.addEventListener('reasoning', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking += e.data;\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n });\n\n _this20.eventSource.addEventListener('reasoning_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking = e.data;\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n });\n\n _this20.eventSource.addEventListener('error', function () {\n if (!isCurrentStream() || streamDone) return;\n if (!answerBuffer) answerBuffer = '❌ 连接中断';\n streamDone = true;\n\n _this20.closeEventSource();\n });\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _callee13);\n }))();\n },\n handleStop: function handleStop() {\n this.streamRequestId++;\n\n if (this.activeSessionId) {\n (0, _kbChat.stopKbStream)(this.activeSessionId).catch(function () {});\n }\n\n this.finishStream();\n },\n finishStream: function finishStream() {\n var _this21 = this;\n\n this.clearTypeTimer();\n\n if (this.streamMsg) {\n this.$delete(this.contentCache, this.streamMsg._key);\n this.streamMsg.loading = false;\n this.streamMsg = null;\n }\n\n this.closeEventSource();\n this.streaming = false;\n this.refreshSessionList();\n this.$nextTick(function () {\n return _this21.scrollToBottom();\n });\n },\n\n /** 流式结束后刷新会话列表(更新标题/最后消息) */\n refreshSessionList: function refreshSessionList() {\n var _this22 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee14() {\n var list;\n return regeneratorRuntime.wrap(function _callee14$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n if (_this22.companyId) {\n _context14.next = 2;\n break;\n }\n\n return _context14.abrupt(\"return\");\n\n case 2:\n _context14.prev = 2;\n _context14.next = 5;\n return (0, _kbChat.getKbSessionList)(_this22.companyId);\n\n case 5:\n _context14.t0 = _context14.sent;\n\n if (_context14.t0) {\n _context14.next = 8;\n break;\n }\n\n _context14.t0 = [];\n\n case 8:\n list = _context14.t0;\n _this22.sessionList = list;\n _context14.next = 15;\n break;\n\n case 12:\n _context14.prev = 12;\n _context14.t1 = _context14[\"catch\"](2);\n console.error('刷新会话列表失败', _context14.t1);\n\n case 15:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _callee14, null, [[2, 12]]);\n }))();\n },\n clearTypeTimer: function clearTypeTimer() {\n if (this._typeTimer) {\n clearInterval(this._typeTimer);\n this._typeTimer = null;\n }\n },\n closeEventSource: function closeEventSource() {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n },\n copyText: function copyText(text) {\n if (navigator.clipboard) {\n navigator.clipboard.writeText(text);\n }\n },\n scrollToBottom: function scrollToBottom() {\n var el = this.$refs.messagesRef;\n\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack://ZjPluginIntelligent/./src/lib/knowledgeBase/KbChatMain.vue?./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options");
|
|
1025
1025
|
|
|
1026
1026
|
/***/ }),
|
|
1027
1027
|
|
|
@@ -14756,7 +14756,7 @@ eval("// Generated by CoffeeScript 1.12.7\n(function() {\n var XMLDocument, XML
|
|
|
14756
14756
|
/*! exports provided: name, version, main, private, scripts, dependencies, devDependencies, browserslist, directories, keywords, license, files, default */
|
|
14757
14757
|
/***/ (function(module) {
|
|
14758
14758
|
|
|
14759
|
-
eval("module.exports = JSON.parse(\"{\\\"name\\\":\\\"zj-plugin-intelligent\\\",\\\"version\\\":\\\"1.2.26-beta.
|
|
14759
|
+
eval("module.exports = JSON.parse(\"{\\\"name\\\":\\\"zj-plugin-intelligent\\\",\\\"version\\\":\\\"1.2.26-beta.3\\\",\\\"main\\\":\\\"lib/ZjPluginIntelligent.umd.min.js\\\",\\\"private\\\":false,\\\"scripts\\\":{\\\"serve\\\":\\\"set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve\\\",\\\"build\\\":\\\"set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build\\\",\\\"lib\\\":\\\"node scripts/build-lib.js\\\",\\\"lib:test\\\":\\\"node scripts/build-lib.js test\\\"},\\\"dependencies\\\":{\\\"@jboltai/tokui\\\":\\\"^0.1.6\\\",\\\"crypto-js\\\":\\\"^4.2.0\\\",\\\"docx\\\":\\\"^8.5.0\\\",\\\"html2canvas\\\":\\\"^1.4.1\\\",\\\"mammoth\\\":\\\"^1.6.0\\\",\\\"markdown-it\\\":\\\"^13.0.2\\\",\\\"marked\\\":\\\"^4.3.0\\\",\\\"xlsx\\\":\\\"^0.18.5\\\"},\\\"devDependencies\\\":{\\\"@babel/preset-env\\\":\\\"^7.14.9\\\",\\\"@vue/cli-plugin-babel\\\":\\\"~4.5.0\\\",\\\"@vue/cli-plugin-eslint\\\":\\\"~4.5.0\\\",\\\"@vue/cli-plugin-vuex\\\":\\\"~4.5.0\\\",\\\"@vue/cli-service\\\":\\\"~4.5.0\\\",\\\"axios\\\":\\\"^0.27.2\\\",\\\"babel-eslint\\\":\\\"^10.1.0\\\",\\\"babel-plugin-component\\\":\\\"^1.1.1\\\",\\\"compression-webpack-plugin\\\":\\\"^6.0.3\\\",\\\"core-js\\\":\\\"^3.6.5\\\",\\\"element-ui\\\":\\\"^2.15.4\\\",\\\"eslint\\\":\\\"^6.8.0\\\",\\\"eslint-plugin-vue\\\":\\\"^7.13.0\\\",\\\"js-cookie\\\":\\\"^2.2.1\\\",\\\"obs-upload\\\":\\\"^1.0.4\\\",\\\"pako\\\":\\\"^2.1.0\\\",\\\"reconnecting-websocket\\\":\\\"^4.4.0\\\",\\\"sass\\\":\\\"^1.71.0\\\",\\\"sass-loader\\\":\\\"^8.0.2\\\",\\\"snowflake-id\\\":\\\"^1.1.0\\\",\\\"vue\\\":\\\"^2.6.11\\\",\\\"vue-template-compiler\\\":\\\"^2.6.11\\\",\\\"vuex\\\":\\\"^3.4.0\\\",\\\"webpack-bundle-analyzer\\\":\\\"^4.4.2\\\"},\\\"browserslist\\\":[\\\"> 1%\\\",\\\"last 2 versions\\\",\\\"not dead\\\"],\\\"directories\\\":{\\\"lib\\\":\\\"lib\\\"},\\\"keywords\\\":[],\\\"license\\\":\\\"ISC\\\",\\\"files\\\":[\\\"lib\\\"]}\");\n\n//# sourceURL=webpack://ZjPluginIntelligent/./package.json?");
|
|
14760
14760
|
|
|
14761
14761
|
/***/ }),
|
|
14762
14762
|
|
|
@@ -1021,7 +1021,7 @@ eval("\n\nvar _interopRequireWildcard = __webpack_require__(/*! ./node_modules/@
|
|
|
1021
1021
|
/***/ (function(module, exports, __webpack_require__) {
|
|
1022
1022
|
|
|
1023
1023
|
"use strict";
|
|
1024
|
-
eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! ./node_modules/@babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\").default;\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/asyncToGenerator */ \"./node_modules/@babel/runtime/helpers/asyncToGenerator.js\"));\n\nvar _typeof2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/typeof */ \"./node_modules/@babel/runtime/helpers/typeof.js\"));\n\nvar _logo = _interopRequireDefault(__webpack_require__(/*! @/assets/logo.png */ \"./src/assets/logo.png\"));\n\nvar _markdownIt = _interopRequireDefault(__webpack_require__(/*! markdown-it */ \"./node_modules/markdown-it/index.mjs\"));\n\nvar _kbChat = __webpack_require__(/*! ./api/kbChat */ \"./src/lib/knowledgeBase/api/kbChat.js\");\n\nvar _knowledgeBase = __webpack_require__(/*! ./api/knowledgeBase */ \"./src/lib/knowledgeBase/api/knowledgeBase.js\");\n\nvar _native = __webpack_require__(/*! ./native.js */ \"./src/lib/knowledgeBase/native.js\");\n\nvar _request = __webpack_require__(/*! @lib/utils/request.js */ \"./src/lib/utils/request.js\");\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar AI_AVATAR_URL = 'https://qiguanbang.obs.cn-east-3.myhuaweicloud.com/production/image/AlAgent/doc_query.png'; // 与 ChatDetail 使用相同的 Markdown 配置\n\nvar md = new _markdownIt.default({\n breaks: true,\n html: false,\n linkify: true,\n typographer: true\n});\nmd.enable('table');\nvar _default = {\n name: 'KbChatMain',\n props: {\n companyId: {\n type: String,\n default: ''\n },\n userno: {\n type: String,\n default: ''\n },\n imAccount: {\n type: [String, Object],\n default: ''\n },\n userName: {\n type: String,\n default: ''\n },\n // 是否移动端:由父级(KnowledgeBaseMain)传入;未传时组件自身按窗口宽度判断\n isMobile: {\n type: Boolean,\n default: null\n }\n },\n data: function data() {\n // 首帧前计算是否移动端,避免 H5 先显示 PC 布局再切换的闪烁(与 AI 助理一致)\n // 宿主没有 Element UI 时(H5/平板横屏)一律走自绘 UI 分支,避免 el-* 未知元素报错\n var _isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n\n return {\n initDone: false,\n activeSessionId: '',\n sessionList: [],\n // 默认折叠侧边栏:避免新用户进入时先展开再合住的闪烁,\n // 加载出会话后再按规则决定是否展开\n sidebarCollapsed: true,\n messages: [],\n inputText: '',\n streaming: false,\n streamMsg: null,\n showToolInfo: false,\n contentCache: {},\n eventSource: null,\n streamRequestId: 0,\n suggestedQuestions: [],\n // 消息多选模式(豆包式):PC 由消息操作区「删除/多选」进入,移动端长按气泡弹出菜单进入\n selectMode: false,\n // 已选中的消息 id 列表(后端主键)\n selectedMsgIds: [],\n // 当前公司是否没有知识库文档(无资料时展示遮罩)\n noDocument: false,\n // 移动端状态(仿 AI 助理):showH5List=true 显示会话列表页,false 显示聊天详情页\n localIsMobile: _isMobile,\n showH5List: _isMobile,\n // 移动端会话「…」弹出的操作单\n mobileAction: null,\n // 移动端长按气泡弹出的消息操作单(复制 / 删除 / 多选)\n msgAction: null,\n // 移动端侧滑手势(参考 AI 助理):记录起手坐标与是否已触发\n _swipeStartX: 0,\n _swipeStartY: 0,\n _swipeHandled: false,\n _swipeFromLeftEdge: false,\n _swipeFromRightEdge: false,\n _swipeFromEdge: false\n };\n },\n computed: {\n /** 实际是否移动端:优先用父级 prop,未传则用自身窗口判断 */\n isMobileActive: function isMobileActive() {\n return this.isMobile !== null ? this.isMobile : this.localIsMobile;\n },\n aiAvatar: function aiAvatar() {\n return AI_AVATAR_URL;\n },\n userAvatar: function userAvatar() {\n var account = this.imAccount;\n console.log('KbChatMain - imAccount 原始值:', account); // 尝试解析字符串为对象\n\n if (typeof account === 'string') {\n try {\n account = JSON.parse(account);\n console.log('KbChatMain - imAccount 解析后:', account);\n } catch (e) {\n // 如果解析失败,检查是否是纯 URL 字符串\n if (account.trim() && (account.includes('http://') || account.includes('https://'))) {\n console.log('KbChatMain - imAccount 是 URL:', account.trim());\n return account.trim();\n }\n }\n } // 如果是对象,尝试多个可能的头像字段(优先使用 headImg)\n\n\n if (account && (0, _typeof2.default)(account) === 'object') {\n var avatarFields = ['headImg', 'headPortrait', 'avatar', 'avatarUrl', 'headImage', 'photoUrl', 'portrait', 'headUrl', 'userHeadUrl', 'userAvatar'];\n\n for (var _i = 0, _avatarFields = avatarFields; _i < _avatarFields.length; _i++) {\n var field = _avatarFields[_i];\n\n if (account[field] && String(account[field]).trim()) {\n console.log(\"KbChatMain - \\u627E\\u5230\\u5934\\u50CF\\u5B57\\u6BB5 \".concat(field, \":\"), account[field]);\n return String(account[field]).trim();\n }\n }\n }\n\n console.log('KbChatMain - 使用默认头像');\n return _logo.default;\n },\n // 是否有真实头像(区别于默认 fallback 图片)\n hasUserAvatar: function hasUserAvatar() {\n return this.userAvatar && this.userAvatar !== _logo.default;\n },\n // 无头像时的文字头像(与 zjkjAvatar 一致:取名字最后两个字)\n userAvatarText: function userAvatarText() {\n var name = this.userName || '';\n return name ? name.substring(name.length - 2) : '';\n },\n // 可删除的消息 id(仅后端已落库、带回 id 的消息可删)\n deletableMsgIds: function deletableMsgIds() {\n return this.messages.filter(function (m) {\n return m.id;\n }).map(function (m) {\n return m.id;\n });\n },\n // 是否已全选\n isAllSelected: function isAllSelected() {\n return this.deletableMsgIds.length > 0 && this.selectedMsgIds.length === this.deletableMsgIds.length;\n }\n },\n watch: {\n companyId: function companyId() {\n this.initDone = false;\n this.finishStream();\n this.loadSuggestedQuestions();\n this.checkKbDocuments();\n this.initSession();\n },\n userno: function userno() {\n this.initDone = false;\n this.finishStream();\n this.activeSessionId = '';\n this.messages = [];\n this.initSession();\n },\n // 移动端进入时默认先显示会话列表页(仿 AI 助理)\n isMobileActive: function isMobileActive(val) {\n if (val) {\n // 无资料时遮罩只在详情页可见(列表页 .kb-chat-main 被隐藏),故保持停在详情页\n this.showH5List = !this.noDocument;\n this.mobileAction = null;\n }\n }\n },\n mounted: function mounted() {\n var _this = this;\n\n // 独立使用(父级未传 isMobile)时自检窗口宽度\n this._onResize = this.handleResize;\n window.addEventListener('resize', this._onResize, {\n passive: true\n }); // Android WebView 兼容:在 document 级别监听 touchmove/touchend,\n // 避免可滚动子元素消费事件后不冒泡到父级(参考 AI 助理)\n\n this._swipeMoveHandler = this._onSwipeMove;\n this._swipeEndHandler = this._onSwipeEnd;\n this._popStateHandler = this._onPopState;\n document.addEventListener('touchmove', this._swipeMoveHandler, {\n passive: true\n });\n document.addEventListener('touchend', this._swipeEndHandler, {\n passive: true\n }); // 拦截 Android 系统返回手势:通过 history.pushState + popstate\n\n window.addEventListener('popstate', this._popStateHandler); // 移动端首屏为会话列表页,推入一条历史状态供系统返回拦截\n\n if (this.isMobileActive) {\n history.pushState({\n page: 'kbChatList'\n }, '');\n }\n\n this.initSession();\n this.checkKbDocuments(); // v-show 隐藏期间加载的历史消息无法滚动(scrollHeight 为 0),\n // 监听组件由隐藏变为可见时,滚动到最新问答;\n // 同时每次变为可见时刷新\"猜您想问\"(管理端可能刚更新过)\n\n if (typeof IntersectionObserver !== 'undefined' && this.$el) {\n this._visibilityObserver = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) {\n return e.isIntersecting;\n })) {\n _this.loadSuggestedQuestions(); // 管理端可能刚上传/删除了文档,重新校验遮罩状态\n\n\n _this.checkKbDocuments();\n\n if (_this.messages.length) {\n _this.$nextTick(function () {\n return _this.scrollToBottom();\n });\n }\n }\n });\n\n this._visibilityObserver.observe(this.$el);\n } else {\n this.loadSuggestedQuestions();\n }\n },\n beforeDestroy: function beforeDestroy() {\n if (this._visibilityObserver) {\n this._visibilityObserver.disconnect();\n\n this._visibilityObserver = null;\n }\n\n if (this._onResize) {\n window.removeEventListener('resize', this._onResize);\n }\n\n if (this._swipeMoveHandler) {\n document.removeEventListener('touchmove', this._swipeMoveHandler);\n }\n\n if (this._swipeEndHandler) {\n document.removeEventListener('touchend', this._swipeEndHandler);\n }\n\n if (this._popStateHandler) {\n window.removeEventListener('popstate', this._popStateHandler);\n }\n\n this.streamRequestId++;\n this.closeEventSource();\n this.clearTypeTimer();\n this.onMsgTouchEnd();\n this.contentCache = {};\n },\n methods: {\n // 独立使用时自检移动端(父级已传 isMobile 时忽略)\n handleResize: function handleResize() {\n var now = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n this.localIsMobile = now;\n },\n renderMarkdown: function renderMarkdown(text) {\n if (!text) return '';\n\n try {\n return md.render(text);\n } catch (err) {\n return this.escapeHtml(text);\n }\n },\n // 与 ChatDetail 一致的增量渲染:完整行走 markdown、未完成尾行转义拼接,\n // 借助 markdown-it breaks:true 实现逐行浮现效果。\n renderStreaming: function renderStreaming(text, msgKey) {\n if (!text) return '';\n var cache = this.contentCache[msgKey];\n var lines = text.split('\\n');\n var lastLineIdx = lines.length - 1;\n\n if (!cache) {\n try {\n var html = md.render(text);\n this.$set(this.contentCache, msgKey, {\n renderedLines: lastLineIdx,\n html: html,\n fullText: text\n });\n return html;\n } catch (_unused) {\n return this.escapeHtml(text);\n }\n }\n\n if (text === cache.fullText) return cache.html;\n cache.fullText = text; // 行数没变,仅末尾追加字符,直接拼接尾行\n\n if (lastLineIdx === cache.renderedLines) {\n return cache.html + this.escapeHtml(lines[lastLineIdx]);\n } // 行数变少/文本回退截断,全量重渲染\n\n\n if (lastLineIdx < cache.renderedLines) {\n try {\n var fullLines = lines.slice(0, lastLineIdx);\n var newBaseHtml = '';\n\n if (fullLines.length > 0) {\n newBaseHtml = md.render(fullLines.join('\\n'));\n }\n\n cache.html = newBaseHtml;\n cache.renderedLines = lastLineIdx;\n\n var _lastLine = this.escapeHtml(lines[lastLineIdx]);\n\n return cache.html + (_lastLine || '');\n } catch (_unused2) {\n return this.escapeHtml(text);\n }\n } // 新增完整行\n\n\n if (cache.renderedLines < lastLineIdx) {\n var allFullLines = lines.slice(0, lastLineIdx);\n var _newBaseHtml = '';\n\n if (allFullLines.length > 0) {\n _newBaseHtml = md.render(allFullLines.join('\\n'));\n }\n\n cache.html = _newBaseHtml;\n cache.renderedLines = lastLineIdx;\n }\n\n var lastLine = this.escapeHtml(lines[lastLineIdx]);\n cache.renderedLines = lastLineIdx;\n return cache.html + (lastLine || '');\n },\n escapeHtml: function escapeHtml(text) {\n if (!text) return '';\n var map = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(text).replace(/[&<>\"']/g, function (c) {\n return map[c];\n });\n },\n autoResizeTextarea: function autoResizeTextarea() {\n var ta = this.$refs.taRef;\n if (!ta) return; // 移动端从单行起增高(与 38px 最小高度一致),PC 保持 100px 基准\n\n var baseH = this.isMobileActive ? 38 : 100;\n var maxH = this.isMobileActive ? 120 : 180;\n ta.style.height = \"\".concat(baseH, \"px\");\n var realH = ta.scrollHeight;\n\n if (realH > baseH) {\n ta.style.height = \"\".concat(Math.min(realH, maxH), \"px\");\n }\n },\n newLine: function newLine(e) {\n var _this2 = this;\n\n e.preventDefault();\n var ta = this.$refs.taRef;\n var start = ta.selectionStart;\n var end = ta.selectionEnd;\n this.inputText = this.inputText.slice(0, start) + '\\n' + this.inputText.slice(end);\n this.$nextTick(function () {\n ta.selectionStart = ta.selectionEnd = start + 1;\n\n _this2.autoResizeTextarea();\n });\n },\n\n /**\n * 加载\"猜您想问\"问题(按公司,所有用户可见)\n */\n loadSuggestedQuestions: function loadSuggestedQuestions() {\n var _this3 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {\n var list;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n if (_this3.companyId) {\n _context.next = 3;\n break;\n }\n\n _this3.suggestedQuestions = [];\n return _context.abrupt(\"return\");\n\n case 3:\n _context.prev = 3;\n _context.next = 6;\n return (0, _knowledgeBase.getSuggestedQuestions)(_this3.companyId);\n\n case 6:\n _context.t0 = _context.sent;\n\n if (_context.t0) {\n _context.next = 9;\n break;\n }\n\n _context.t0 = [];\n\n case 9:\n list = _context.t0;\n _this3.suggestedQuestions = list.map(function (q) {\n return q.questionText;\n });\n _context.next = 16;\n break;\n\n case 13:\n _context.prev = 13;\n _context.t1 = _context[\"catch\"](3);\n console.error('加载猜您想问失败', _context.t1);\n\n case 16:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, null, [[3, 13]]);\n }))();\n },\n\n /**\n * 检查当前公司是否有「已索引」的知识库文档,无可用资料时展示遮罩提示\n * 仅已索引(indexStatus === 1)的文档才能被检索,未索引/索引中/索引失败都不算\n */\n checkKbDocuments: function checkKbDocuments() {\n var _this4 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var data, isEmpty;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n if (_this4.companyId) {\n _context2.next = 3;\n break;\n }\n\n _this4.noDocument = false;\n return _context2.abrupt(\"return\");\n\n case 3:\n _context2.prev = 3;\n _context2.next = 6;\n return (0, _knowledgeBase.getFileList)({\n companyId: _this4.companyId,\n indexStatus: 1,\n pageNum: 1,\n pageSize: 1\n });\n\n case 6:\n data = _context2.sent;\n isEmpty = (data && data.total || 0) === 0;\n _this4.noDocument = isEmpty; // 无资料时把会话固定回第一个\n\n if (isEmpty) {\n _this4.lockFirstSession(); // 无资料遮罩位于聊天详情页的 .kb-chat-main 内,而移动端首屏是会话列表页\n // (h5-list-mode 下 .kb-chat-main 被 display:none 隐藏),若停留在列表页\n // 则遮罩永远不可见;故无资料时强制切到详情页把遮罩展示出来\n\n\n if (_this4.isMobileActive) {\n _this4.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n }\n }\n\n _context2.next = 16;\n break;\n\n case 12:\n _context2.prev = 12;\n _context2.t0 = _context2[\"catch\"](3);\n // 查询失败(如无管理权限)时不展示遮罩,避免误拦截\n console.error('检查知识库文档失败', _context2.t0);\n _this4.noDocument = false;\n\n case 16:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, null, [[3, 12]]);\n }))();\n },\n initSession: function initSession() {\n var _this5 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {\n var list;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n if (_this5.companyId) {\n _context3.next = 2;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 2:\n if (!(_this5.initDone && (_this5.messages.length || _this5.streaming || _this5.inputText))) {\n _context3.next = 4;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 4:\n _this5.initDone = false;\n _context3.prev = 5;\n _context3.next = 8;\n return (0, _kbChat.getKbSessionList)(_this5.companyId);\n\n case 8:\n _context3.t0 = _context3.sent;\n\n if (_context3.t0) {\n _context3.next = 11;\n break;\n }\n\n _context3.t0 = [];\n\n case 11:\n list = _context3.t0;\n _this5.sessionList = list;\n\n if (!(list.length > 0)) {\n _context3.next = 19;\n break;\n }\n\n // 只有 1 个会话且无消息(新用户首次进入自动生成的空会话)时,\n // 默认折叠侧边栏只显示聊天主界面;在 selectSession 前同步设置,\n // 避免「先展开再合住」的闪烁\n _this5.sidebarCollapsed = list.length === 1 && !list[0].lastContent;\n _context3.next = 17;\n return _this5.selectSession(list[0].sessionId);\n\n case 17:\n _context3.next = 21;\n break;\n\n case 19:\n _context3.next = 21;\n return _this5.autoCreateSession();\n\n case 21:\n _context3.next = 28;\n break;\n\n case 23:\n _context3.prev = 23;\n _context3.t1 = _context3[\"catch\"](5);\n console.error('获取会话列表失败,尝试自动创建', _context3.t1);\n _context3.next = 28;\n return _this5.autoCreateSession();\n\n case 28:\n _context3.prev = 28;\n _this5.initDone = true;\n return _context3.finish(28);\n\n case 31:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _callee3, null, [[5, 23, 28, 31]]);\n }))();\n },\n autoCreateSession: function autoCreateSession() {\n var _this6 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {\n var data;\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n if (_this6.companyId) {\n _context4.next = 3;\n break;\n }\n\n console.warn('[KbChat] 无法创建会话:companyId 为空');\n return _context4.abrupt(\"return\");\n\n case 3:\n console.log('[KbChat] 创建会话,companyId:', _this6.companyId);\n _context4.prev = 4;\n _context4.next = 7;\n return (0, _kbChat.createKbSession)(_this6.companyId);\n\n case 7:\n data = _context4.sent;\n console.log('[KbChat] 会话创建成功,sessionId:', data.sessionId);\n\n _this6.finishStream();\n\n _this6.activeSessionId = data.sessionId;\n _this6.streamRequestId++;\n _this6.messages = [];\n _this6.sessionList = [{\n sessionId: data.sessionId,\n title: '',\n lastContent: ''\n }].concat(_this6.sessionList || []); // 新建会话时保持侧边栏展开,方便继续切换会话\n\n _this6.sidebarCollapsed = false;\n\n _this6.$nextTick(function () {\n return _this6.scrollToBottom();\n });\n\n _context4.next = 21;\n break;\n\n case 18:\n _context4.prev = 18;\n _context4.t0 = _context4[\"catch\"](4);\n console.error('自动创建会话失败', _context4.t0);\n\n case 21:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _callee4, null, [[4, 18]]);\n }))();\n },\n\n /** 新建会话(豆包式「新对话」) */\n handleNewSession: function handleNewSession() {\n var _this7 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee5() {\n return regeneratorRuntime.wrap(function _callee5$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n if (!_this7.noDocument) {\n _context5.next = 2;\n break;\n }\n\n return _context5.abrupt(\"return\");\n\n case 2:\n if (_this7.streaming) _this7.handleStop();\n _context5.next = 5;\n return _this7.autoCreateSession();\n\n case 5:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _callee5);\n }))();\n },\n\n /** 切换会话 */\n handleSelectSession: function handleSelectSession(item) {\n var _this8 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee6() {\n return regeneratorRuntime.wrap(function _callee6$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context6.next = 2;\n break;\n }\n\n return _context6.abrupt(\"return\");\n\n case 2:\n if (!_this8.noDocument) {\n _context6.next = 5;\n break;\n }\n\n _this8.lockFirstSession();\n\n return _context6.abrupt(\"return\");\n\n case 5:\n if (!(item.sessionId === _this8.activeSessionId && _this8.messages.length)) {\n _context6.next = 8;\n break;\n }\n\n if (_this8.isMobileActive) {\n _this8.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n return _context6.abrupt(\"return\");\n\n case 8:\n _context6.next = 10;\n return _this8.selectSession(item.sessionId);\n\n case 10:\n // 移动端:选中后跳到聊天详情页\n if (_this8.isMobileActive) {\n _this8.showH5List = false; // 推入历史状态,使 Android 系统返回手势先触发 popstate 被拦截\n\n history.pushState({\n page: 'kbChatDetail'\n }, ''); // 列表页时聊天区被 display:none,scrollHeight 为 0 导致上面的滚动无效;\n // 切到详情页、容器可见后需再滚一次,默认定位到最新消息\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n case 11:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _callee6);\n }))();\n },\n\n /** 返回宿主 App 原生页面(参考 AI 助理 handleNativeBack) */\n handleNativeBack: function handleNativeBack() {\n if (this.$hybrid && typeof this.$hybrid.closePage === 'function') {\n this.$hybrid.closePage();\n } else if (window.history.length > 1) {\n window.history.back();\n }\n },\n\n /** 会话列表页返回按钮:返回原生页面 */\n handleSidebarBack: function handleSidebarBack() {\n this.handleNativeBack();\n },\n // ── H5 侧滑手势(参考 AI 助理)──\n // 从左/右屏幕边缘起手横滑触发返回,避免页面内部横向滚动被误判\n onSwipeTouchStart: function onSwipeTouchStart(e) {\n if (!this.isMobileActive) return;\n var t = e.touches[0];\n this._swipeStartX = t.clientX;\n this._swipeStartY = t.clientY;\n this._swipeHandled = false;\n var edge = Math.min(56, Math.round(window.innerWidth * 0.09));\n this._swipeFromLeftEdge = t.clientX <= edge;\n this._swipeFromRightEdge = t.clientX >= window.innerWidth - edge;\n this._swipeFromEdge = this._swipeFromLeftEdge || this._swipeFromRightEdge;\n },\n _swipeShouldTrigger: function _swipeShouldTrigger(diffX, diffY) {\n if (!this._swipeFromEdge) return false; // 从左边缘 → 右滑;从右边缘 → 左滑\n\n var directionOk = this._swipeFromLeftEdge && diffX >= 60 || this._swipeFromRightEdge && diffX <= -60;\n if (!directionOk) return false; // 水平距离明显大于垂直距离(排除上下滚动干扰)\n\n return Math.abs(diffX) >= Math.abs(diffY) * 2;\n },\n _onSwipeMove: function _onSwipeMove(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.touches || !e.touches.length) return;\n var diffX = e.touches[0].clientX - this._swipeStartX;\n var diffY = e.touches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n // touchend 兜底,兼容不支持 touchmove 的场景\n _onSwipeEnd: function _onSwipeEnd(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.changedTouches || !e.changedTouches.length) return;\n var diffX = e.changedTouches[0].clientX - this._swipeStartX;\n var diffY = e.changedTouches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n\n /** 侧滑触发:列表页 → 返回原生页面;详情页 → 回会话列表页 */\n _triggerSwipeAction: function _triggerSwipeAction() {\n if (this.showH5List) {\n this.handleNativeBack();\n } else {\n this.handleMobileBack();\n }\n },\n\n /**\n * 拦截 Android 系统返回手势(history.pushState + popstate)\n * WebView 先回退历史栈触发 popstate,这里拦截后导航到列表页,避免 Activity 被直接关闭\n */\n _onPopState: function _onPopState() {\n if (!this.isMobileActive) return;\n\n if (!this.showH5List) {\n // 详情页 → 返回会话列表页\n this.showH5List = true;\n history.pushState({\n page: 'kbChatList'\n }, '');\n } else {\n // 列表页 → 直接关闭 WebView 返回原生页面\n this.handleNativeBack();\n }\n },\n\n /** 移动端详情页返回:无资料遮罩时返回原生页面;否则回到会话列表页 */\n handleMobileBack: function handleMobileBack() {\n // 无资料遮罩期间无法进入会话,返回到宿主 App 原生页面\n if (this.noDocument) {\n this.handleNativeBack();\n return;\n } // 正常情况:回到会话列表页\n\n\n this.showH5List = true;\n },\n\n /** 无资料遮罩期间:把当前会话固定回列表中的第一个 */\n lockFirstSession: function lockFirstSession() {\n var first = this.sessionList[0];\n\n if (first && first.sessionId && first.sessionId !== this.activeSessionId) {\n this.selectSession(first.sessionId);\n }\n },\n\n /** 会话项「…」菜单:重命名 / 删除 */\n handleSessionMenu: function handleSessionMenu(cmd, item) {\n if (!item || !item.sessionId) return;\n\n if (cmd === 'rename') {\n this.handleRenameSession(item);\n } else if (cmd === 'delete') {\n this.handleDeleteSession(item);\n }\n },\n // ==================== 移动端会话操作单 ====================\n openMobileAction: function openMobileAction(item) {\n if (this.noDocument) return;\n this.mobileAction = item;\n },\n doMobileRename: function doMobileRename(item) {\n this.mobileAction = null;\n this.handleRenameSession(item);\n },\n doMobileDelete: function doMobileDelete(item) {\n this.mobileAction = null;\n this.handleDeleteSession(item);\n },\n\n /** 重命名会话 */\n handleRenameSession: function handleRenameSession(item) {\n var _this9 = this;\n\n if (!item || !item.sessionId) return;\n this.$prompt('请输入新的会话名称', '重命名会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n inputValue: item.title || '',\n inputPattern: /\\S+/,\n inputErrorMessage: '名称不能为空'\n }).then(function (_ref) {\n var value = _ref.value;\n var title = (value || '').trim();\n if (!title) return;\n return (0, _kbChat.renameKbSession)(item.sessionId, title).then(function () {\n item.title = title;\n\n _this9.$message.success('重命名成功');\n });\n }).catch(function (err) {\n // 取消或校验失败,不提示错误\n if (err && err !== 'cancel' && err !== 'close') {\n console.error('重命名失败', err);\n\n _this9.$message.error('重命名失败');\n }\n });\n },\n\n /** 删除会话 */\n handleDeleteSession: function handleDeleteSession(item) {\n var _this10 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {\n return regeneratorRuntime.wrap(function _callee7$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context7.next = 2;\n break;\n }\n\n return _context7.abrupt(\"return\");\n\n case 2:\n if (!(_this10.sessionList.length <= 1)) {\n _context7.next = 5;\n break;\n }\n\n _this10.$message.warning('至少保留一个会话');\n\n return _context7.abrupt(\"return\");\n\n case 5:\n _context7.prev = 5;\n _context7.next = 8;\n return _this10.$confirm('删除后无法恢复,确定删除该会话吗?', '删除会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 8:\n _context7.next = 13;\n break;\n\n case 10:\n _context7.prev = 10;\n _context7.t0 = _context7[\"catch\"](5);\n return _context7.abrupt(\"return\");\n\n case 13:\n _context7.prev = 13;\n _context7.next = 16;\n return (0, _kbChat.deleteKbSession)(item.sessionId);\n\n case 16:\n _this10.sessionList = (_this10.sessionList || []).filter(function (s) {\n return s.sessionId !== item.sessionId;\n });\n\n if (!(_this10.activeSessionId === item.sessionId)) {\n _context7.next = 28;\n break;\n }\n\n _this10.finishStream();\n\n _this10.activeSessionId = '';\n _this10.messages = [];\n\n if (!(_this10.sessionList.length > 0)) {\n _context7.next = 26;\n break;\n }\n\n _context7.next = 24;\n return _this10.selectSession(_this10.sessionList[0].sessionId);\n\n case 24:\n _context7.next = 28;\n break;\n\n case 26:\n _context7.next = 28;\n return _this10.autoCreateSession();\n\n case 28:\n _this10.$message.success('删除成功');\n\n _context7.next = 35;\n break;\n\n case 31:\n _context7.prev = 31;\n _context7.t1 = _context7[\"catch\"](13);\n console.error('删除会话失败', _context7.t1);\n\n _this10.$message.error('删除失败');\n\n case 35:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _callee7, null, [[5, 10], [13, 31]]);\n }))();\n },\n\n /** 会话列表时间格式化:今天显示时分,更早显示月日 */\n formatTime: function formatTime(time) {\n if (!time) return '';\n var d = new Date(time);\n if (isNaN(d.getTime())) return '';\n var now = new Date();\n var sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();\n\n var pad = function pad(n) {\n return n < 10 ? '0' + n : '' + n;\n };\n\n if (sameDay) {\n return pad(d.getHours()) + ':' + pad(d.getMinutes());\n }\n\n return pad(d.getMonth() + 1) + '-' + pad(d.getDate());\n },\n\n /** 后端消息 → 前端消息对象(保留 id/msgKey,删除功能需要) */\n mapMessage: function mapMessage(m, i) {\n var thinking = m.thinking || m.reasoning || m.reasoning_result || m.reasoningContent || m.reasoning_content || m.thinkingContent || m.thinking_content || '';\n return {\n id: m.id,\n msgKey: m.msgKey || m.msg_key || '',\n role: m.role,\n content: m.content || '',\n thinking: thinking,\n _key: m.createTime ? 'msg_' + m.createTime + '_' + i : 'msg_' + Date.now() + '_' + Math.random()\n };\n },\n selectSession: function selectSession(sessionId) {\n var _this11 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee8() {\n var raw, msgs;\n return regeneratorRuntime.wrap(function _callee8$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n if (_this11.streaming) _this11.handleStop();\n\n _this11.exitSelectMode();\n\n _this11.msgAction = null;\n _this11.activeSessionId = sessionId;\n _this11.messages = [];\n _this11.streamMsg = null;\n _context8.prev = 6;\n _context8.next = 9;\n return (0, _kbChat.getKbSessionMessages)(sessionId);\n\n case 9:\n raw = _context8.sent;\n msgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || [];\n _this11.messages = msgs.map(function (m, i) {\n return _this11.mapMessage(m, i);\n });\n\n _this11.$nextTick(function () {\n return _this11.scrollToBottom();\n });\n\n _context8.next = 18;\n break;\n\n case 15:\n _context8.prev = 15;\n _context8.t0 = _context8[\"catch\"](6);\n console.error('加载消息失败', _context8.t0);\n\n case 18:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _callee8, null, [[6, 15]]);\n }))();\n },\n\n /**\n * 回读当前会话消息,补全后端持久化后生成的 id。\n * 流式回答期间消息只存在于本地数组(无 id),无法按 id 删除;\n * 流式结束后调用一次即可拿到 id。已全部有 id 时跳过,避免多余请求。\n *\n * 注意:不能直接用后端数组整体覆盖 this.messages。后端回读可能包含重复/多余记录\n * (如同一会话里用户消息被落库两次),若整体覆盖会把重复消息直接渲染出来。\n * 这里以本地数组为基准,只用后端记录按「角色 + 内容」顺序回填 id,保证界面条数不变。\n */\n syncMessageIds: function syncMessageIds() {\n var _this12 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee9() {\n var raw, backendMsgs, cursor;\n return regeneratorRuntime.wrap(function _callee9$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n if (!(!_this12.activeSessionId || _this12.streaming)) {\n _context9.next = 2;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 2:\n if (!(_this12.messages.length && _this12.messages.every(function (m) {\n return m.id;\n }))) {\n _context9.next = 4;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 4:\n _context9.prev = 4;\n _context9.next = 7;\n return (0, _kbChat.getKbSessionMessages)(_this12.activeSessionId);\n\n case 7:\n raw = _context9.sent;\n backendMsgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || []; // 按顺序把本地消息与后端记录配对(角色一致且内容匹配),只补 id/msgKey\n\n cursor = 0;\n\n _this12.messages.forEach(function (m) {\n if (m.id) return;\n\n var role = _this12.normRole(m.role);\n\n for (var i = cursor; i < backendMsgs.length; i++) {\n var b = backendMsgs[i];\n\n if (_this12.normRole(b.role) === role && String(b.content || '').trim() === String(m.content || '').trim()) {\n m.id = b.id;\n if (!m.msgKey) m.msgKey = b.msgKey || b.msg_key || '';\n cursor = i + 1;\n return;\n }\n }\n });\n\n _context9.next = 16;\n break;\n\n case 13:\n _context9.prev = 13;\n _context9.t0 = _context9[\"catch\"](4);\n console.error('同步消息 id 失败', _context9.t0);\n\n case 16:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _callee9, null, [[4, 13]]);\n }))();\n },\n // ==================== 消息多选 / 删除 ====================\n\n /** 进入多选模式(preselectMsg 传入时默认勾选该条所在的整组问答,对应豆包式「删除」入口) */\n enterSelectMode: function enterSelectMode(preselectMsg) {\n var _this13 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee10() {\n var preselectId, idx;\n return regeneratorRuntime.wrap(function _callee10$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n if (!(_this13.streaming || !_this13.messages.length)) {\n _context10.next = 2;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 2:\n // 点击条的后端持久化 id(模板上「删除」按钮仅在 msg.id 存在时渲染,故必有值)\n preselectId = preselectMsg ? preselectMsg.id : null; // 先回读消息确保每条都有 id(刚流式完成的消息本地没有 id)。\n // 回读只补 id、不重建数组,定位可用 id 或对象引用。\n\n _context10.next = 5;\n return _this13.syncMessageIds();\n\n case 5:\n _this13.selectMode = true;\n _this13.selectedMsgIds = [];\n idx = preselectId != null ? _this13.messages.findIndex(function (m) {\n return m.id === preselectId;\n }) : -1;\n if (idx === -1 && preselectMsg) idx = _this13.messages.indexOf(preselectMsg);\n\n if (idx !== -1) {\n // 默认勾选整组问答(用户问 + AI 答),便于一键删除\n _this13.selectedMsgIds = _this13.getQaPairIdsAt(idx);\n } // 滚动定位:让默认勾选的整组问答都进入视野(长回答会把用户问顶出屏幕)\n\n\n _this13.scrollSelectedIntoView();\n\n case 11:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _callee10);\n }))();\n },\n\n /**\n * 把首个被选中的消息行滚动到容器内可见。\n * 选中行是「用户问 + AI 答」中的靠前一条,滚到它即能同时看到整组。\n * 若靠前那条已在视野内则不动,避免无谓跳动。\n */\n scrollSelectedIntoView: function scrollSelectedIntoView() {\n var _this14 = this;\n\n this.$nextTick(function () {\n var container = _this14.$refs.messagesRef;\n if (!container) return;\n var rows = container.querySelectorAll('.msg-row');\n if (!rows.length) return; // 找到第一个处于选中态、且带 id 的行(与 selectedMsgIds 顺序无关,取 DOM 顺序)\n\n var target = null;\n\n for (var i = 0; i < rows.length; i++) {\n var box = rows[i].querySelector('.msg-check-box.checked');\n\n if (box) {\n target = rows[i];\n break;\n }\n }\n\n if (!target) return;\n var cRect = container.getBoundingClientRect();\n var tRect = target.getBoundingClientRect(); // 目标行顶部已在容器可视区内则无需滚动\n\n if (tRect.top >= cRect.top && tRect.top < cRect.bottom) return; // 滚动到目标行顶部(留 8px 间距)\n\n container.scrollTop += tRect.top - cRect.top - 8;\n });\n },\n exitSelectMode: function exitSelectMode() {\n this.selectMode = false;\n this.selectedMsgIds = [];\n },\n isMsgSelected: function isMsgSelected(msg) {\n return !!msg.id && this.selectedMsgIds.indexOf(msg.id) !== -1;\n },\n\n /** 归一化 role,兼容后端可能返回的大小写/空白 */\n normRole: function normRole(role) {\n return String(role == null ? '' : role).trim().toLowerCase();\n },\n\n /**\n * 取与下标 idx 处消息成对的另一条:用户问 ↔ AI 答视为一组问答。\n * 只认紧邻的一条(问答对在列表里必然相邻),相邻非同组则不配对。\n * 不依赖 role 的具体拼写(user/assistant 的大小写、空白均容错)。\n */\n getQaPartnerAt: function getQaPartnerAt(idx) {\n if (idx < 0 || idx >= this.messages.length) return null;\n var cur = this.messages[idx];\n if (!cur) return null;\n var role = this.normRole(cur.role);\n if (role !== 'user' && role !== 'assistant') return null; // 只认紧邻的那一条:问答对在列表里必然相邻。\n // 若相邻消息不是异角色(例如回答已被删除),说明当前消息已无配对,\n // 不能继续向前/向后扫描,否则会误勾到上一个问答的回答。\n\n var neighbor = role === 'user' ? this.messages[idx + 1] : this.messages[idx - 1];\n if (!neighbor) return null;\n var neighborRole = this.normRole(neighbor.role);\n var expect = role === 'user' ? 'assistant' : 'user';\n return neighborRole === expect ? neighbor : null;\n },\n\n /** 下标 idx 处消息所在问答对的可删 id 列表(用户问 + AI 答) */\n getQaPairIdsAt: function getQaPairIdsAt(idx) {\n var ids = [];\n var cur = this.messages[idx];\n if (cur && cur.id) ids.push(cur.id);\n var partner = this.getQaPartnerAt(idx);\n if (partner && partner.id && ids.indexOf(partner.id) === -1) ids.push(partner.id);\n return ids;\n },\n\n /**\n * 勾选单条消息:勾上时连带把问答对里另一条一起勾上;\n * 取消时只取消当前这条,从而支持「先整组勾选、再单独取消一条」的单条选择。\n */\n toggleSelectMessage: function toggleSelectMessage(msg) {\n var _this15 = this;\n\n if (!msg.id) return;\n var i = this.selectedMsgIds.indexOf(msg.id);\n\n if (i === -1) {\n var idx = this.messages.indexOf(msg);\n var pairIds = idx === -1 ? [msg.id] : this.getQaPairIdsAt(idx);\n pairIds.forEach(function (id) {\n if (_this15.selectedMsgIds.indexOf(id) === -1) _this15.selectedMsgIds.push(id);\n });\n } else {\n this.selectedMsgIds.splice(i, 1);\n }\n },\n toggleSelectAll: function toggleSelectAll() {\n var ids = this.deletableMsgIds;\n this.selectedMsgIds = this.selectedMsgIds.length === ids.length ? [] : ids.slice();\n },\n\n /** 多选模式下点击整行即切换选中 */\n onMsgRowClick: function onMsgRowClick(msg) {\n // 长按弹出菜单后浏览器还会补一次 click,短暂屏蔽避免误触\n if (Date.now() < this._ignoreClickUntil) return;\n if (this.selectMode) this.toggleSelectMessage(msg);\n },\n\n /** 移动端长按气泡弹出消息操作单(豆包式:复制 / 删除 / 多选) */\n onMsgTouchStart: function onMsgTouchStart(msg) {\n var _this16 = this;\n\n if (!this.isMobileActive || this.selectMode) return;\n this._longPressMsg = msg;\n clearTimeout(this._longPressTimer);\n this._longPressTimer = setTimeout(function () {\n _this16._longPressTimer = null; // 用户正在长按选中文本(复制用),此时不弹菜单,避免抢走选择行为\n\n var sel = window.getSelection && window.getSelection();\n if (sel && String(sel).trim()) return;\n\n _this16.openMsgAction(_this16._longPressMsg);\n }, 500);\n },\n onMsgTouchEnd: function onMsgTouchEnd() {\n if (this._longPressTimer) {\n clearTimeout(this._longPressTimer);\n this._longPressTimer = null;\n }\n },\n openMsgAction: function openMsgAction(msg) {\n this.msgAction = msg; // 长按抬手后浏览器可能补发 click,短暂屏蔽,避免刚弹出的菜单被立即关闭\n\n this._ignoreClickUntil = Date.now() + 350;\n },\n closeMsgAction: function closeMsgAction() {\n if (Date.now() < this._ignoreClickUntil) return;\n this.msgAction = null;\n },\n\n /** 移动端消息操作单:复制 / 删除(删除进入多选模式,与豆包一致) */\n doMsgAction: function doMsgAction(action) {\n var msg = this.msgAction;\n this.msgAction = null;\n if (!msg) return;\n\n if (action === 'copy') {\n this.copyText(msg.content);\n } else if (action === 'delete') {\n this.enterSelectMode(msg);\n }\n },\n\n /** 批量删除选中的消息 */\n handleDeleteSelected: function handleDeleteSelected() {\n var _this17 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee11() {\n var ids;\n return regeneratorRuntime.wrap(function _callee11$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n if (!(!_this17.selectedMsgIds.length || !_this17.activeSessionId)) {\n _context11.next = 2;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 2:\n ids = _this17.selectedMsgIds.slice();\n _context11.prev = 3;\n _context11.next = 6;\n return _this17.$confirm(\"\\u5220\\u9664\\u540E\\u65E0\\u6CD5\\u6062\\u590D\\uFF0C\\u786E\\u5B9A\\u5220\\u9664\\u9009\\u4E2D\\u7684 \".concat(ids.length, \" \\u6761\\u6D88\\u606F\\u5417\\uFF1F\"), '删除消息', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 6:\n _context11.next = 11;\n break;\n\n case 8:\n _context11.prev = 8;\n _context11.t0 = _context11[\"catch\"](3);\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.prev = 11;\n _context11.next = 14;\n return (0, _kbChat.deleteKbMessages)(_this17.activeSessionId, ids);\n\n case 14:\n _this17.messages = _this17.messages.filter(function (m) {\n return ids.indexOf(m.id) === -1;\n });\n\n _this17.exitSelectMode();\n\n _this17.$message.success('删除成功');\n\n _this17.refreshSessionList();\n\n _context11.next = 24;\n break;\n\n case 20:\n _context11.prev = 20;\n _context11.t1 = _context11[\"catch\"](11);\n console.error('批量删除消息失败', _context11.t1);\n\n _this17.$message.error('删除失败');\n\n case 24:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _callee11, null, [[3, 8], [11, 20]]);\n }))();\n },\n handleResetSession: function handleResetSession() {\n var _this18 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee12() {\n return regeneratorRuntime.wrap(function _callee12$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n if (!_this18.activeSessionId) {\n _context12.next = 8;\n break;\n }\n\n _context12.prev = 1;\n _context12.next = 4;\n return (0, _kbChat.closeKbSession)(_this18.activeSessionId);\n\n case 4:\n _context12.next = 8;\n break;\n\n case 6:\n _context12.prev = 6;\n _context12.t0 = _context12[\"catch\"](1);\n\n case 8:\n _context12.next = 10;\n return _this18.autoCreateSession();\n\n case 10:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _callee12, null, [[1, 6]]);\n }))();\n },\n handleSend: function handleSend(quickText) {\n var _this19 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee13() {\n var text, requestSessionId, requestId, msgKey, msgIdx, streamMsg, isCurrentStream, answerBuffer, finalContent, streamDone, url;\n return regeneratorRuntime.wrap(function _callee13$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n text = typeof quickText === 'string' ? quickText.trim() : _this19.inputText.trim();\n\n if (!(!text || !_this19.activeSessionId || _this19.streaming)) {\n _context13.next = 3;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 3:\n _this19.exitSelectMode();\n\n _this19.inputText = '';\n requestSessionId = _this19.activeSessionId;\n requestId = ++_this19.streamRequestId;\n\n _this19.clearTypeTimer();\n\n _this19.messages.push({\n role: 'user',\n content: text\n });\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n }); // 流式消息:作为 messages 中的对象(带 _key/loading),与 ChatDetail 一致\n\n\n msgKey = 'stream_' + requestId;\n msgIdx = _this19.messages.push({\n role: 'assistant',\n content: '',\n thinking: '',\n toolCalls: [],\n loading: true,\n _key: msgKey\n }) - 1;\n streamMsg = _this19.messages[msgIdx];\n _this19.streamMsg = streamMsg;\n _this19.streaming = true;\n\n isCurrentStream = function isCurrentStream() {\n return requestId === _this19.streamRequestId && requestSessionId === _this19.activeSessionId && _this19.streamMsg === streamMsg;\n }; // SSE 只把数据累积到 answerBuffer。typeTimer(30ms) 节流按「完整换行」逐行把内容\n // 推进到 msg.content:每个 tick 最多只推进「到下一个 \\n 为止」的一行,未完成的尾行\n // 保持不动;配合 renderStreaming(breaks:true) 实现「一行显示一行」。\n // 不是打字机模式(不逐字)、也不是一次性整段渲染(不会一出就是全文)。\n\n\n answerBuffer = '';\n finalContent = '';\n streamDone = false;\n _this19._typeTimer = setInterval(function () {\n if (!isCurrentStream()) return;\n var msg = _this19.messages[msgIdx];\n var source = finalContent || answerBuffer;\n var curLen = msg._prevReveal || 0;\n var nextLen = curLen; // 找到「当前已揭示进度之后」的第一条完整换行,仅推进这一行\n\n var nl = source.indexOf('\\n', curLen);\n\n if (nl !== -1) {\n nextLen = nl + 1;\n } else if (streamDone) {\n // 流结束且没有更多换行:最后一段未以换行结尾的内容一次性露出(此时即完整一行)\n nextLen = source.length;\n }\n\n if (nextLen > curLen) {\n msg._prevReveal = nextLen;\n msg.content = source.substring(0, nextLen);\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n } // 流结束且所有内容都已逐行露出 → 收尾\n\n\n if (streamDone && nextLen >= source.length) {\n _this19.finishStream(); // 后端已在此刻落库,回读一次补全消息 id(单条删除需要)\n\n\n _this19.syncMessageIds();\n }\n }, 30);\n url = (0, _kbChat.createKbStreamUrl)(_this19.activeSessionId, text);\n _this19.eventSource = new EventSource(url);\n\n _this19.eventSource.addEventListener('tool_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n\n if (e.data && _this19.messages[msgIdx]) {\n if (!_this19.messages[msgIdx].toolCalls) _this19.messages[msgIdx].toolCalls = [];\n\n _this19.messages[msgIdx].toolCalls.push(e.data);\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n }\n });\n\n _this19.eventSource.addEventListener('answer', function (e) {\n if (!isCurrentStream() || streamDone) return;\n answerBuffer += e.data;\n });\n\n _this19.eventSource.addEventListener('agent_result', function (e) {\n if (!isCurrentStream() || streamDone) return; // 与档案君(AiChat.vue)对齐:AGENT_RESULT 为最终真相源。\n // 只记录 finalContent 并结束流,不打断已在逐行推进的 content,交给 typeTimer 以 target 推进/校正。\n\n if (e.data) finalContent = e.data;\n streamDone = true;\n\n _this19.closeEventSource();\n });\n\n _this19.eventSource.addEventListener('msgKey', function (e) {\n if (!isCurrentStream()) return;\n\n if (e.data && _this19.messages[msgIdx]) {\n _this19.messages[msgIdx].msgKey = e.data;\n }\n });\n\n _this19.eventSource.addEventListener('reasoning', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking += e.data;\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n });\n\n _this19.eventSource.addEventListener('reasoning_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking = e.data;\n\n _this19.$nextTick(function () {\n return _this19.scrollToBottom();\n });\n });\n\n _this19.eventSource.addEventListener('error', function () {\n if (!isCurrentStream() || streamDone) return;\n if (!answerBuffer) answerBuffer = '❌ 连接中断';\n streamDone = true;\n\n _this19.closeEventSource();\n });\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _callee13);\n }))();\n },\n handleStop: function handleStop() {\n this.streamRequestId++;\n\n if (this.activeSessionId) {\n (0, _kbChat.stopKbStream)(this.activeSessionId).catch(function () {});\n }\n\n this.finishStream();\n },\n finishStream: function finishStream() {\n var _this20 = this;\n\n this.clearTypeTimer();\n\n if (this.streamMsg) {\n this.$delete(this.contentCache, this.streamMsg._key);\n this.streamMsg.loading = false;\n this.streamMsg = null;\n }\n\n this.closeEventSource();\n this.streaming = false;\n this.refreshSessionList();\n this.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n },\n\n /** 流式结束后刷新会话列表(更新标题/最后消息) */\n refreshSessionList: function refreshSessionList() {\n var _this21 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee14() {\n var list;\n return regeneratorRuntime.wrap(function _callee14$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n if (_this21.companyId) {\n _context14.next = 2;\n break;\n }\n\n return _context14.abrupt(\"return\");\n\n case 2:\n _context14.prev = 2;\n _context14.next = 5;\n return (0, _kbChat.getKbSessionList)(_this21.companyId);\n\n case 5:\n _context14.t0 = _context14.sent;\n\n if (_context14.t0) {\n _context14.next = 8;\n break;\n }\n\n _context14.t0 = [];\n\n case 8:\n list = _context14.t0;\n _this21.sessionList = list;\n _context14.next = 15;\n break;\n\n case 12:\n _context14.prev = 12;\n _context14.t1 = _context14[\"catch\"](2);\n console.error('刷新会话列表失败', _context14.t1);\n\n case 15:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _callee14, null, [[2, 12]]);\n }))();\n },\n clearTypeTimer: function clearTypeTimer() {\n if (this._typeTimer) {\n clearInterval(this._typeTimer);\n this._typeTimer = null;\n }\n },\n closeEventSource: function closeEventSource() {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n },\n copyText: function copyText(text) {\n if (navigator.clipboard) {\n navigator.clipboard.writeText(text);\n }\n },\n scrollToBottom: function scrollToBottom() {\n var el = this.$refs.messagesRef;\n\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack://ZjPluginIntelligent/./src/lib/knowledgeBase/KbChatMain.vue?./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options");
|
|
1024
|
+
eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! ./node_modules/@babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\").default;\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/asyncToGenerator */ \"./node_modules/@babel/runtime/helpers/asyncToGenerator.js\"));\n\nvar _typeof2 = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/helpers/typeof */ \"./node_modules/@babel/runtime/helpers/typeof.js\"));\n\nvar _logo = _interopRequireDefault(__webpack_require__(/*! @/assets/logo.png */ \"./src/assets/logo.png\"));\n\nvar _markdownIt = _interopRequireDefault(__webpack_require__(/*! markdown-it */ \"./node_modules/markdown-it/index.mjs\"));\n\nvar _kbChat = __webpack_require__(/*! ./api/kbChat */ \"./src/lib/knowledgeBase/api/kbChat.js\");\n\nvar _knowledgeBase = __webpack_require__(/*! ./api/knowledgeBase */ \"./src/lib/knowledgeBase/api/knowledgeBase.js\");\n\nvar _native = __webpack_require__(/*! ./native.js */ \"./src/lib/knowledgeBase/native.js\");\n\nvar _request = __webpack_require__(/*! @lib/utils/request.js */ \"./src/lib/utils/request.js\");\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar AI_AVATAR_URL = 'https://qiguanbang.obs.cn-east-3.myhuaweicloud.com/production/image/AlAgent/doc_query.png'; // 与 ChatDetail 使用相同的 Markdown 配置\n\nvar md = new _markdownIt.default({\n breaks: true,\n html: false,\n linkify: true,\n typographer: true\n});\nmd.enable('table');\nvar _default = {\n name: 'KbChatMain',\n props: {\n companyId: {\n type: String,\n default: ''\n },\n userno: {\n type: String,\n default: ''\n },\n imAccount: {\n type: [String, Object],\n default: ''\n },\n userName: {\n type: String,\n default: ''\n },\n // 是否移动端:由父级(KnowledgeBaseMain)传入;未传时组件自身按窗口宽度判断\n isMobile: {\n type: Boolean,\n default: null\n }\n },\n data: function data() {\n // 首帧前计算是否移动端,避免 H5 先显示 PC 布局再切换的闪烁(与 AI 助理一致)\n // 宿主没有 Element UI 时(H5/平板横屏)一律走自绘 UI 分支,避免 el-* 未知元素报错\n var _isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n\n return {\n initDone: false,\n activeSessionId: '',\n sessionList: [],\n // 默认折叠侧边栏:避免新用户进入时先展开再合住的闪烁,\n // 加载出会话后再按规则决定是否展开\n sidebarCollapsed: true,\n messages: [],\n inputText: '',\n streaming: false,\n streamMsg: null,\n showToolInfo: false,\n contentCache: {},\n eventSource: null,\n streamRequestId: 0,\n suggestedQuestions: [],\n // 消息多选模式(豆包式):PC 由消息操作区「删除/多选」进入,移动端长按气泡弹出菜单进入\n selectMode: false,\n // 已选中的消息 id 列表(后端主键)\n selectedMsgIds: [],\n // 当前公司是否没有知识库文档(无资料时展示遮罩)\n noDocument: false,\n // 移动端状态(仿 AI 助理):showH5List=true 显示会话列表页,false 显示聊天详情页\n localIsMobile: _isMobile,\n showH5List: _isMobile,\n // 移动端会话「…」弹出的操作单\n mobileAction: null,\n // 移动端长按气泡弹出的消息操作单(复制 / 删除 / 多选)\n msgAction: null,\n // 移动端侧滑手势(参考 AI 助理):记录起手坐标与是否已触发\n _swipeStartX: 0,\n _swipeStartY: 0,\n _swipeHandled: false,\n _swipeFromLeftEdge: false,\n _swipeFromRightEdge: false,\n _swipeFromEdge: false\n };\n },\n computed: {\n /** 实际是否移动端:优先用父级 prop,未传则用自身窗口判断 */\n isMobileActive: function isMobileActive() {\n return this.isMobile !== null ? this.isMobile : this.localIsMobile;\n },\n aiAvatar: function aiAvatar() {\n return AI_AVATAR_URL;\n },\n userAvatar: function userAvatar() {\n var account = this.imAccount;\n console.log('KbChatMain - imAccount 原始值:', account); // 尝试解析字符串为对象\n\n if (typeof account === 'string') {\n try {\n account = JSON.parse(account);\n console.log('KbChatMain - imAccount 解析后:', account);\n } catch (e) {\n // 如果解析失败,检查是否是纯 URL 字符串\n if (account.trim() && (account.includes('http://') || account.includes('https://'))) {\n console.log('KbChatMain - imAccount 是 URL:', account.trim());\n return account.trim();\n }\n }\n } // 如果是对象,尝试多个可能的头像字段(优先使用 headImg)\n\n\n if (account && (0, _typeof2.default)(account) === 'object') {\n var avatarFields = ['headImg', 'headPortrait', 'avatar', 'avatarUrl', 'headImage', 'photoUrl', 'portrait', 'headUrl', 'userHeadUrl', 'userAvatar'];\n\n for (var _i = 0, _avatarFields = avatarFields; _i < _avatarFields.length; _i++) {\n var field = _avatarFields[_i];\n\n if (account[field] && String(account[field]).trim()) {\n console.log(\"KbChatMain - \\u627E\\u5230\\u5934\\u50CF\\u5B57\\u6BB5 \".concat(field, \":\"), account[field]);\n return String(account[field]).trim();\n }\n }\n }\n\n console.log('KbChatMain - 使用默认头像');\n return _logo.default;\n },\n // 是否有真实头像(区别于默认 fallback 图片)\n hasUserAvatar: function hasUserAvatar() {\n return this.userAvatar && this.userAvatar !== _logo.default;\n },\n // 无头像时的文字头像(与 zjkjAvatar 一致:取名字最后两个字)\n userAvatarText: function userAvatarText() {\n var name = this.userName || '';\n return name ? name.substring(name.length - 2) : '';\n },\n // 可删除的消息 id(仅后端已落库、带回 id 的消息可删)\n deletableMsgIds: function deletableMsgIds() {\n return this.messages.filter(function (m) {\n return m.id;\n }).map(function (m) {\n return m.id;\n });\n },\n // 是否已全选\n isAllSelected: function isAllSelected() {\n return this.deletableMsgIds.length > 0 && this.selectedMsgIds.length === this.deletableMsgIds.length;\n }\n },\n watch: {\n companyId: function companyId() {\n this.initDone = false;\n this.finishStream();\n this.loadSuggestedQuestions();\n this.checkKbDocuments();\n this.initSession();\n },\n userno: function userno() {\n this.initDone = false;\n this.finishStream();\n this.activeSessionId = '';\n this.messages = [];\n this.initSession();\n },\n // 移动端进入时默认先显示会话列表页(仿 AI 助理)\n isMobileActive: function isMobileActive(val) {\n if (val) {\n // 无资料时遮罩只在详情页可见(列表页 .kb-chat-main 被隐藏),故保持停在详情页\n this.showH5List = !this.noDocument;\n this.mobileAction = null;\n }\n }\n },\n mounted: function mounted() {\n var _this = this;\n\n // 独立使用(父级未传 isMobile)时自检窗口宽度\n this._onResize = this.handleResize;\n window.addEventListener('resize', this._onResize, {\n passive: true\n }); // Android WebView 兼容:在 document 级别监听 touchmove/touchend,\n // 避免可滚动子元素消费事件后不冒泡到父级(参考 AI 助理)\n\n this._swipeMoveHandler = this._onSwipeMove;\n this._swipeEndHandler = this._onSwipeEnd;\n this._popStateHandler = this._onPopState;\n document.addEventListener('touchmove', this._swipeMoveHandler, {\n passive: true\n });\n document.addEventListener('touchend', this._swipeEndHandler, {\n passive: true\n }); // 拦截 Android 系统返回手势:通过 history.pushState + popstate\n\n window.addEventListener('popstate', this._popStateHandler); // 移动端首屏为会话列表页,推入一条历史状态供系统返回拦截\n\n if (this.isMobileActive) {\n history.pushState({\n page: 'kbChatList'\n }, '');\n }\n\n this.initSession();\n this.checkKbDocuments(); // v-show 隐藏期间加载的历史消息无法滚动(scrollHeight 为 0),\n // 监听组件由隐藏变为可见时,滚动到最新问答;\n // 同时每次变为可见时刷新\"猜您想问\"(管理端可能刚更新过)\n\n if (typeof IntersectionObserver !== 'undefined' && this.$el) {\n this._visibilityObserver = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) {\n return e.isIntersecting;\n })) {\n _this.loadSuggestedQuestions(); // 管理端可能刚上传/删除了文档,重新校验遮罩状态\n\n\n _this.checkKbDocuments();\n\n if (_this.messages.length) {\n _this.$nextTick(function () {\n return _this.scrollToBottom();\n });\n }\n }\n });\n\n this._visibilityObserver.observe(this.$el);\n } else {\n this.loadSuggestedQuestions();\n }\n },\n beforeDestroy: function beforeDestroy() {\n if (this._visibilityObserver) {\n this._visibilityObserver.disconnect();\n\n this._visibilityObserver = null;\n }\n\n if (this._onResize) {\n window.removeEventListener('resize', this._onResize);\n }\n\n if (this._swipeMoveHandler) {\n document.removeEventListener('touchmove', this._swipeMoveHandler);\n }\n\n if (this._swipeEndHandler) {\n document.removeEventListener('touchend', this._swipeEndHandler);\n }\n\n if (this._popStateHandler) {\n window.removeEventListener('popstate', this._popStateHandler);\n }\n\n this.streamRequestId++;\n this.closeEventSource();\n this.clearTypeTimer();\n this.onMsgTouchEnd();\n this.contentCache = {};\n },\n methods: {\n // 独立使用时自检移动端(父级已传 isMobile 时忽略)\n handleResize: function handleResize() {\n var now = typeof window !== 'undefined' && (window.innerWidth <= 768 || !(0, _native.hasElementUI)(this));\n this.localIsMobile = now;\n },\n renderMarkdown: function renderMarkdown(text) {\n if (!text) return '';\n\n try {\n return md.render(text);\n } catch (err) {\n return this.escapeHtml(text);\n }\n },\n // 与 ChatDetail 一致的增量渲染:完整行走 markdown、未完成尾行转义拼接,\n // 借助 markdown-it breaks:true 实现逐行浮现效果。\n renderStreaming: function renderStreaming(text, msgKey) {\n if (!text) return '';\n var cache = this.contentCache[msgKey];\n var lines = text.split('\\n');\n var lastLineIdx = lines.length - 1;\n\n if (!cache) {\n try {\n var html = md.render(text);\n this.$set(this.contentCache, msgKey, {\n renderedLines: lastLineIdx,\n html: html,\n fullText: text\n });\n return html;\n } catch (_unused) {\n return this.escapeHtml(text);\n }\n }\n\n if (text === cache.fullText) return cache.html;\n cache.fullText = text; // 行数没变,仅末尾追加字符,直接拼接尾行\n\n if (lastLineIdx === cache.renderedLines) {\n return cache.html + this.escapeHtml(lines[lastLineIdx]);\n } // 行数变少/文本回退截断,全量重渲染\n\n\n if (lastLineIdx < cache.renderedLines) {\n try {\n var fullLines = lines.slice(0, lastLineIdx);\n var newBaseHtml = '';\n\n if (fullLines.length > 0) {\n newBaseHtml = md.render(fullLines.join('\\n'));\n }\n\n cache.html = newBaseHtml;\n cache.renderedLines = lastLineIdx;\n\n var _lastLine = this.escapeHtml(lines[lastLineIdx]);\n\n return cache.html + (_lastLine || '');\n } catch (_unused2) {\n return this.escapeHtml(text);\n }\n } // 新增完整行\n\n\n if (cache.renderedLines < lastLineIdx) {\n var allFullLines = lines.slice(0, lastLineIdx);\n var _newBaseHtml = '';\n\n if (allFullLines.length > 0) {\n _newBaseHtml = md.render(allFullLines.join('\\n'));\n }\n\n cache.html = _newBaseHtml;\n cache.renderedLines = lastLineIdx;\n }\n\n var lastLine = this.escapeHtml(lines[lastLineIdx]);\n cache.renderedLines = lastLineIdx;\n return cache.html + (lastLine || '');\n },\n escapeHtml: function escapeHtml(text) {\n if (!text) return '';\n var map = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(text).replace(/[&<>\"']/g, function (c) {\n return map[c];\n });\n },\n autoResizeTextarea: function autoResizeTextarea() {\n var ta = this.$refs.taRef;\n if (!ta) return; // 移动端从单行起增高(与 38px 最小高度一致),PC 保持 100px 基准\n\n var baseH = this.isMobileActive ? 38 : 100;\n var maxH = this.isMobileActive ? 120 : 180;\n ta.style.height = \"\".concat(baseH, \"px\");\n var realH = ta.scrollHeight;\n\n if (realH > baseH) {\n ta.style.height = \"\".concat(Math.min(realH, maxH), \"px\");\n }\n },\n newLine: function newLine(e) {\n var _this2 = this;\n\n e.preventDefault();\n var ta = this.$refs.taRef;\n var start = ta.selectionStart;\n var end = ta.selectionEnd;\n this.inputText = this.inputText.slice(0, start) + '\\n' + this.inputText.slice(end);\n this.$nextTick(function () {\n ta.selectionStart = ta.selectionEnd = start + 1;\n\n _this2.autoResizeTextarea();\n });\n },\n\n /**\n * 加载\"猜您想问\"问题(按公司,所有用户可见)\n */\n loadSuggestedQuestions: function loadSuggestedQuestions() {\n var _this3 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {\n var list;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n if (_this3.companyId) {\n _context.next = 3;\n break;\n }\n\n _this3.suggestedQuestions = [];\n return _context.abrupt(\"return\");\n\n case 3:\n _context.prev = 3;\n _context.next = 6;\n return (0, _knowledgeBase.getSuggestedQuestions)(_this3.companyId);\n\n case 6:\n _context.t0 = _context.sent;\n\n if (_context.t0) {\n _context.next = 9;\n break;\n }\n\n _context.t0 = [];\n\n case 9:\n list = _context.t0;\n _this3.suggestedQuestions = list.map(function (q) {\n return q.questionText;\n });\n _context.next = 16;\n break;\n\n case 13:\n _context.prev = 13;\n _context.t1 = _context[\"catch\"](3);\n console.error('加载猜您想问失败', _context.t1);\n\n case 16:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee, null, [[3, 13]]);\n }))();\n },\n\n /**\n * 检查当前公司是否有「已索引」的知识库文档,无可用资料时展示遮罩提示\n * 仅已索引(indexStatus === 1)的文档才能被检索,未索引/索引中/索引失败都不算\n */\n checkKbDocuments: function checkKbDocuments() {\n var _this4 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {\n var data, isEmpty;\n return regeneratorRuntime.wrap(function _callee2$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n if (_this4.companyId) {\n _context2.next = 3;\n break;\n }\n\n _this4.noDocument = false;\n return _context2.abrupt(\"return\");\n\n case 3:\n _context2.prev = 3;\n _context2.next = 6;\n return (0, _knowledgeBase.getFileList)({\n companyId: _this4.companyId,\n indexStatus: 1,\n pageNum: 1,\n pageSize: 1\n });\n\n case 6:\n data = _context2.sent;\n isEmpty = (data && data.total || 0) === 0;\n _this4.noDocument = isEmpty; // 无资料时把会话固定回第一个\n\n if (isEmpty) {\n _this4.lockFirstSession(); // 无资料遮罩位于聊天详情页的 .kb-chat-main 内,而移动端首屏是会话列表页\n // (h5-list-mode 下 .kb-chat-main 被 display:none 隐藏),若停留在列表页\n // 则遮罩永远不可见;故无资料时强制切到详情页把遮罩展示出来\n\n\n if (_this4.isMobileActive) {\n _this4.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n }\n }\n\n _context2.next = 16;\n break;\n\n case 12:\n _context2.prev = 12;\n _context2.t0 = _context2[\"catch\"](3);\n // 查询失败(如无管理权限)时不展示遮罩,避免误拦截\n console.error('检查知识库文档失败', _context2.t0);\n _this4.noDocument = false;\n\n case 16:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _callee2, null, [[3, 12]]);\n }))();\n },\n initSession: function initSession() {\n var _this5 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {\n var list;\n return regeneratorRuntime.wrap(function _callee3$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n if (_this5.companyId) {\n _context3.next = 2;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 2:\n if (!(_this5.initDone && (_this5.messages.length || _this5.streaming || _this5.inputText))) {\n _context3.next = 4;\n break;\n }\n\n return _context3.abrupt(\"return\");\n\n case 4:\n _this5.initDone = false;\n _context3.prev = 5;\n _context3.next = 8;\n return (0, _kbChat.getKbSessionList)(_this5.companyId);\n\n case 8:\n _context3.t0 = _context3.sent;\n\n if (_context3.t0) {\n _context3.next = 11;\n break;\n }\n\n _context3.t0 = [];\n\n case 11:\n list = _context3.t0;\n _this5.sessionList = list;\n\n if (!(list.length > 0)) {\n _context3.next = 19;\n break;\n }\n\n // 只有 1 个会话且无消息(新用户首次进入自动生成的空会话)时,\n // 默认折叠侧边栏只显示聊天主界面;在 selectSession 前同步设置,\n // 避免「先展开再合住」的闪烁\n _this5.sidebarCollapsed = list.length === 1 && !list[0].lastContent;\n _context3.next = 17;\n return _this5.selectSession(list[0].sessionId);\n\n case 17:\n _context3.next = 21;\n break;\n\n case 19:\n _context3.next = 21;\n return _this5.autoCreateSession();\n\n case 21:\n _context3.next = 28;\n break;\n\n case 23:\n _context3.prev = 23;\n _context3.t1 = _context3[\"catch\"](5);\n console.error('获取会话列表失败,尝试自动创建', _context3.t1);\n _context3.next = 28;\n return _this5.autoCreateSession();\n\n case 28:\n _context3.prev = 28;\n _this5.initDone = true;\n return _context3.finish(28);\n\n case 31:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _callee3, null, [[5, 23, 28, 31]]);\n }))();\n },\n autoCreateSession: function autoCreateSession() {\n var _this6 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {\n var data;\n return regeneratorRuntime.wrap(function _callee4$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n if (_this6.companyId) {\n _context4.next = 3;\n break;\n }\n\n console.warn('[KbChat] 无法创建会话:companyId 为空');\n return _context4.abrupt(\"return\");\n\n case 3:\n console.log('[KbChat] 创建会话,companyId:', _this6.companyId);\n _context4.prev = 4;\n _context4.next = 7;\n return (0, _kbChat.createKbSession)(_this6.companyId);\n\n case 7:\n data = _context4.sent;\n console.log('[KbChat] 会话创建成功,sessionId:', data.sessionId);\n\n _this6.finishStream();\n\n _this6.activeSessionId = data.sessionId;\n _this6.streamRequestId++;\n _this6.messages = [];\n _this6.sessionList = [{\n sessionId: data.sessionId,\n title: '',\n lastContent: ''\n }].concat(_this6.sessionList || []); // 新建会话时保持侧边栏展开,方便继续切换会话\n\n _this6.sidebarCollapsed = false;\n\n _this6.$nextTick(function () {\n return _this6.scrollToBottom();\n });\n\n _context4.next = 21;\n break;\n\n case 18:\n _context4.prev = 18;\n _context4.t0 = _context4[\"catch\"](4);\n console.error('自动创建会话失败', _context4.t0);\n\n case 21:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _callee4, null, [[4, 18]]);\n }))();\n },\n\n /** 新建会话(豆包式「新对话」) */\n handleNewSession: function handleNewSession() {\n var _this7 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee5() {\n return regeneratorRuntime.wrap(function _callee5$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n if (!_this7.noDocument) {\n _context5.next = 2;\n break;\n }\n\n return _context5.abrupt(\"return\");\n\n case 2:\n if (_this7.streaming) _this7.handleStop();\n _context5.next = 5;\n return _this7.autoCreateSession();\n\n case 5:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _callee5);\n }))();\n },\n\n /** 切换会话 */\n handleSelectSession: function handleSelectSession(item) {\n var _this8 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee6() {\n return regeneratorRuntime.wrap(function _callee6$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context6.next = 2;\n break;\n }\n\n return _context6.abrupt(\"return\");\n\n case 2:\n if (!_this8.noDocument) {\n _context6.next = 5;\n break;\n }\n\n _this8.lockFirstSession();\n\n return _context6.abrupt(\"return\");\n\n case 5:\n if (!(item.sessionId === _this8.activeSessionId && _this8.messages.length)) {\n _context6.next = 8;\n break;\n }\n\n if (_this8.isMobileActive) {\n _this8.showH5List = false;\n history.pushState({\n page: 'kbChatDetail'\n }, '');\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n return _context6.abrupt(\"return\");\n\n case 8:\n _context6.next = 10;\n return _this8.selectSession(item.sessionId);\n\n case 10:\n // 移动端:选中后跳到聊天详情页\n if (_this8.isMobileActive) {\n _this8.showH5List = false; // 推入历史状态,使 Android 系统返回手势先触发 popstate 被拦截\n\n history.pushState({\n page: 'kbChatDetail'\n }, ''); // 列表页时聊天区被 display:none,scrollHeight 为 0 导致上面的滚动无效;\n // 切到详情页、容器可见后需再滚一次,默认定位到最新消息\n\n _this8.$nextTick(function () {\n return _this8.scrollToBottom();\n });\n }\n\n case 11:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _callee6);\n }))();\n },\n\n /** 返回宿主 App 原生页面(参考 AI 助理 handleNativeBack) */\n handleNativeBack: function handleNativeBack() {\n if (this.$hybrid && typeof this.$hybrid.closePage === 'function') {\n this.$hybrid.closePage();\n } else if (window.history.length > 1) {\n window.history.back();\n }\n },\n\n /** 会话列表页返回按钮:返回原生页面 */\n handleSidebarBack: function handleSidebarBack() {\n this.handleNativeBack();\n },\n // ── H5 侧滑手势(参考 AI 助理)──\n // 从左/右屏幕边缘起手横滑触发返回,避免页面内部横向滚动被误判\n onSwipeTouchStart: function onSwipeTouchStart(e) {\n if (!this.isMobileActive) return;\n var t = e.touches[0];\n this._swipeStartX = t.clientX;\n this._swipeStartY = t.clientY;\n this._swipeHandled = false;\n var edge = Math.min(56, Math.round(window.innerWidth * 0.09));\n this._swipeFromLeftEdge = t.clientX <= edge;\n this._swipeFromRightEdge = t.clientX >= window.innerWidth - edge;\n this._swipeFromEdge = this._swipeFromLeftEdge || this._swipeFromRightEdge;\n },\n _swipeShouldTrigger: function _swipeShouldTrigger(diffX, diffY) {\n if (!this._swipeFromEdge) return false; // 从左边缘 → 右滑;从右边缘 → 左滑\n\n var directionOk = this._swipeFromLeftEdge && diffX >= 60 || this._swipeFromRightEdge && diffX <= -60;\n if (!directionOk) return false; // 水平距离明显大于垂直距离(排除上下滚动干扰)\n\n return Math.abs(diffX) >= Math.abs(diffY) * 2;\n },\n _onSwipeMove: function _onSwipeMove(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.touches || !e.touches.length) return;\n var diffX = e.touches[0].clientX - this._swipeStartX;\n var diffY = e.touches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n // touchend 兜底,兼容不支持 touchmove 的场景\n _onSwipeEnd: function _onSwipeEnd(e) {\n if (!this.isMobileActive || this._swipeHandled) return;\n if (!e.changedTouches || !e.changedTouches.length) return;\n var diffX = e.changedTouches[0].clientX - this._swipeStartX;\n var diffY = e.changedTouches[0].clientY - this._swipeStartY;\n if (!this._swipeShouldTrigger(diffX, diffY)) return;\n this._swipeHandled = true;\n\n this._triggerSwipeAction();\n },\n\n /** 侧滑触发:列表页 → 返回原生页面;详情页 → 回会话列表页 */\n _triggerSwipeAction: function _triggerSwipeAction() {\n if (this.showH5List) {\n this.handleNativeBack();\n } else {\n this.handleMobileBack();\n }\n },\n\n /**\n * 拦截 Android 系统返回手势(history.pushState + popstate)\n * WebView 先回退历史栈触发 popstate,这里拦截后导航到列表页,避免 Activity 被直接关闭\n */\n _onPopState: function _onPopState() {\n if (!this.isMobileActive) return;\n\n if (!this.showH5List) {\n // 详情页 → 返回会话列表页\n this.showH5List = true;\n history.pushState({\n page: 'kbChatList'\n }, '');\n } else {\n // 列表页 → 直接关闭 WebView 返回原生页面\n this.handleNativeBack();\n }\n },\n\n /** 移动端详情页返回:无资料遮罩时返回原生页面;否则回到会话列表页 */\n handleMobileBack: function handleMobileBack() {\n // 无资料遮罩期间无法进入会话,返回到宿主 App 原生页面\n if (this.noDocument) {\n this.handleNativeBack();\n return;\n } // 正常情况:回到会话列表页\n\n\n this.showH5List = true;\n },\n\n /** 无资料遮罩期间:把当前会话固定回列表中的第一个 */\n lockFirstSession: function lockFirstSession() {\n var first = this.sessionList[0];\n\n if (first && first.sessionId && first.sessionId !== this.activeSessionId) {\n this.selectSession(first.sessionId);\n }\n },\n\n /** 会话项「…」菜单:重命名 / 删除 */\n handleSessionMenu: function handleSessionMenu(cmd, item) {\n if (!item || !item.sessionId) return;\n\n if (cmd === 'rename') {\n this.handleRenameSession(item);\n } else if (cmd === 'delete') {\n this.handleDeleteSession(item);\n }\n },\n // ==================== 移动端会话操作单 ====================\n openMobileAction: function openMobileAction(item) {\n if (this.noDocument) return;\n this.mobileAction = item;\n },\n doMobileRename: function doMobileRename(item) {\n this.mobileAction = null;\n this.handleRenameSession(item);\n },\n doMobileDelete: function doMobileDelete(item) {\n this.mobileAction = null;\n this.handleDeleteSession(item);\n },\n\n /** 重命名会话 */\n handleRenameSession: function handleRenameSession(item) {\n var _this9 = this;\n\n if (!item || !item.sessionId) return;\n this.$prompt('请输入新的会话名称', '重命名会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n inputValue: item.title || '',\n inputPattern: /\\S+/,\n inputErrorMessage: '名称不能为空'\n }).then(function (_ref) {\n var value = _ref.value;\n var title = (value || '').trim();\n if (!title) return;\n return (0, _kbChat.renameKbSession)(item.sessionId, title).then(function () {\n item.title = title;\n\n _this9.$message.success('重命名成功');\n });\n }).catch(function (err) {\n // 取消或校验失败,不提示错误\n if (err && err !== 'cancel' && err !== 'close') {\n console.error('重命名失败', err);\n\n _this9.$message.error('重命名失败');\n }\n });\n },\n\n /** 删除会话 */\n handleDeleteSession: function handleDeleteSession(item) {\n var _this10 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {\n return regeneratorRuntime.wrap(function _callee7$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n if (!(!item || !item.sessionId)) {\n _context7.next = 2;\n break;\n }\n\n return _context7.abrupt(\"return\");\n\n case 2:\n if (!(_this10.sessionList.length <= 1)) {\n _context7.next = 5;\n break;\n }\n\n _this10.$message.warning('至少保留一个会话');\n\n return _context7.abrupt(\"return\");\n\n case 5:\n _context7.prev = 5;\n _context7.next = 8;\n return _this10.$confirm('删除后无法恢复,确定删除该会话吗?', '删除会话', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 8:\n _context7.next = 13;\n break;\n\n case 10:\n _context7.prev = 10;\n _context7.t0 = _context7[\"catch\"](5);\n return _context7.abrupt(\"return\");\n\n case 13:\n _context7.prev = 13;\n _context7.next = 16;\n return (0, _kbChat.deleteKbSession)(item.sessionId);\n\n case 16:\n _this10.sessionList = (_this10.sessionList || []).filter(function (s) {\n return s.sessionId !== item.sessionId;\n });\n\n if (!(_this10.activeSessionId === item.sessionId)) {\n _context7.next = 28;\n break;\n }\n\n _this10.finishStream();\n\n _this10.activeSessionId = '';\n _this10.messages = [];\n\n if (!(_this10.sessionList.length > 0)) {\n _context7.next = 26;\n break;\n }\n\n _context7.next = 24;\n return _this10.selectSession(_this10.sessionList[0].sessionId);\n\n case 24:\n _context7.next = 28;\n break;\n\n case 26:\n _context7.next = 28;\n return _this10.autoCreateSession();\n\n case 28:\n _this10.$message.success('删除成功');\n\n _context7.next = 35;\n break;\n\n case 31:\n _context7.prev = 31;\n _context7.t1 = _context7[\"catch\"](13);\n console.error('删除会话失败', _context7.t1);\n\n _this10.$message.error('删除失败');\n\n case 35:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _callee7, null, [[5, 10], [13, 31]]);\n }))();\n },\n\n /** 会话列表时间格式化:今天显示时分,更早显示月日 */\n formatTime: function formatTime(time) {\n if (!time) return '';\n var d = new Date(time);\n if (isNaN(d.getTime())) return '';\n var now = new Date();\n var sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();\n\n var pad = function pad(n) {\n return n < 10 ? '0' + n : '' + n;\n };\n\n if (sameDay) {\n return pad(d.getHours()) + ':' + pad(d.getMinutes());\n }\n\n return pad(d.getMonth() + 1) + '-' + pad(d.getDate());\n },\n\n /** 后端消息 → 前端消息对象(保留 id/msgKey,删除功能需要) */\n mapMessage: function mapMessage(m, i) {\n var thinking = m.thinking || m.reasoning || m.reasoning_result || m.reasoningContent || m.reasoning_content || m.thinkingContent || m.thinking_content || '';\n return {\n id: m.id,\n msgKey: m.msgKey || m.msg_key || '',\n role: m.role,\n content: m.content || '',\n thinking: thinking,\n _key: m.createTime ? 'msg_' + m.createTime + '_' + i : 'msg_' + Date.now() + '_' + Math.random()\n };\n },\n selectSession: function selectSession(sessionId) {\n var _this11 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee8() {\n var raw, msgs;\n return regeneratorRuntime.wrap(function _callee8$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n if (_this11.streaming) _this11.handleStop();\n\n _this11.exitSelectMode();\n\n _this11.msgAction = null;\n _this11.activeSessionId = sessionId;\n _this11.messages = [];\n _this11.streamMsg = null;\n _context8.prev = 6;\n _context8.next = 9;\n return (0, _kbChat.getKbSessionMessages)(sessionId);\n\n case 9:\n raw = _context8.sent;\n msgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || [];\n console.log('[KbChat] selectSession 回读', {\n sessionId: sessionId,\n rawIsArray: Array.isArray(raw),\n rawKeys: raw && !Array.isArray(raw) ? Object.keys(raw) : null,\n count: msgs.length,\n list: msgs.map(function (m) {\n return {\n id: m.id,\n role: m.role,\n content: String(m.content || '').slice(0, 25)\n };\n })\n });\n _this11.messages = _this11.dedupeMessages(msgs).map(function (m, i) {\n return _this11.mapMessage(m, i);\n });\n\n _this11.$nextTick(function () {\n return _this11.scrollToBottom();\n });\n\n _context8.next = 19;\n break;\n\n case 16:\n _context8.prev = 16;\n _context8.t0 = _context8[\"catch\"](6);\n console.error('加载消息失败', _context8.t0);\n\n case 19:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _callee8, null, [[6, 16]]);\n }))();\n },\n\n /**\n * 后端回读消息去重。\n * 后端存在同一问答对被重复落库的情况(同一会话里出现两套相同的「问+答」),\n * 直接渲染会让用户看到重复问答。这里按「问答对」为单位去重:\n * 把消息切成组(用户问 + 其后的 AI 答为一组),整组内容完全相同的只保留第一次出现的。\n * 相比「只和上一条比」的相邻去重,能覆盖组与组之间被隔开、或 content 有空白差异的情况。\n */\n dedupeMessages: function dedupeMessages(msgs) {\n var _this12 = this;\n\n // 先切组:以 user 消息为一个新组的起点,其后连续的 assistant 归入同组\n var groups = [];\n msgs.forEach(function (m) {\n var role = _this12.normRole(m.role);\n\n if (role === 'user' || !groups.length) {\n groups.push([m]);\n } else {\n groups[groups.length - 1].push(m);\n }\n });\n var seen = {};\n var out = [];\n groups.forEach(function (group) {\n var signature = group.map(function (m) {\n return _this12.normRole(m.role) + \"\\x01\" + String(m.content || '').replace(/\\s+/g, '');\n }).join(\"\\x02\");\n if (signature && seen[signature]) return;\n if (signature) seen[signature] = true;\n out.push.apply(out, group);\n });\n return out;\n },\n\n /**\n * 回读当前会话消息,补全后端持久化后生成的 id。\n * 流式回答期间消息只存在于本地数组(无 id),无法按 id 删除;\n * 流式结束后调用一次即可拿到 id。已全部有 id 时跳过,避免多余请求。\n *\n * 注意:不能直接用后端数组整体覆盖 this.messages。后端回读可能包含重复/多余记录\n * (如同一会话里用户消息被落库两次),若整体覆盖会把重复消息直接渲染出来。\n * 这里以本地数组为基准,只用后端记录按「角色 + 内容」顺序回填 id,保证界面条数不变。\n */\n syncMessageIds: function syncMessageIds() {\n var _this13 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee9() {\n var raw, backendMsgs, cursor;\n return regeneratorRuntime.wrap(function _callee9$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n if (!(!_this13.activeSessionId || _this13.streaming)) {\n _context9.next = 2;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 2:\n if (!(_this13.messages.length && _this13.messages.every(function (m) {\n return m.id;\n }))) {\n _context9.next = 4;\n break;\n }\n\n return _context9.abrupt(\"return\");\n\n case 4:\n _context9.prev = 4;\n _context9.next = 7;\n return (0, _kbChat.getKbSessionMessages)(_this13.activeSessionId);\n\n case 7:\n raw = _context9.sent;\n backendMsgs = Array.isArray(raw) ? raw : raw.messages || raw.records || raw.list || []; // 按顺序把本地消息与后端记录配对(角色一致且内容匹配),只补 id/msgKey\n\n cursor = 0;\n\n _this13.messages.forEach(function (m) {\n if (m.id) return;\n\n var role = _this13.normRole(m.role);\n\n for (var i = cursor; i < backendMsgs.length; i++) {\n var b = backendMsgs[i];\n\n if (_this13.normRole(b.role) === role && String(b.content || '').trim() === String(m.content || '').trim()) {\n m.id = b.id;\n if (!m.msgKey) m.msgKey = b.msgKey || b.msg_key || '';\n cursor = i + 1;\n return;\n }\n }\n });\n\n _context9.next = 16;\n break;\n\n case 13:\n _context9.prev = 13;\n _context9.t0 = _context9[\"catch\"](4);\n console.error('同步消息 id 失败', _context9.t0);\n\n case 16:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _callee9, null, [[4, 13]]);\n }))();\n },\n // ==================== 消息多选 / 删除 ====================\n\n /** 进入多选模式(preselectMsg 传入时默认勾选该条所在的整组问答,对应豆包式「删除」入口) */\n enterSelectMode: function enterSelectMode(preselectMsg) {\n var _this14 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee10() {\n var preselectId, idx;\n return regeneratorRuntime.wrap(function _callee10$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n if (!(_this14.streaming || !_this14.messages.length)) {\n _context10.next = 2;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 2:\n // 点击条的后端持久化 id(模板上「删除」按钮仅在 msg.id 存在时渲染,故必有值)\n preselectId = preselectMsg ? preselectMsg.id : null; // 先回读消息确保每条都有 id(刚流式完成的消息本地没有 id)。\n // 回读只补 id、不重建数组,定位可用 id 或对象引用。\n\n _context10.next = 5;\n return _this14.syncMessageIds();\n\n case 5:\n _this14.selectMode = true;\n _this14.selectedMsgIds = [];\n idx = preselectId != null ? _this14.messages.findIndex(function (m) {\n return m.id === preselectId;\n }) : -1;\n if (idx === -1 && preselectMsg) idx = _this14.messages.indexOf(preselectMsg);\n\n if (idx !== -1) {\n // 默认勾选整组问答(用户问 + AI 答),便于一键删除\n _this14.selectedMsgIds = _this14.getQaPairIdsAt(idx);\n } // 滚动定位:让默认勾选的整组问答都进入视野(长回答会把用户问顶出屏幕)\n\n\n _this14.scrollSelectedIntoView();\n\n case 11:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _callee10);\n }))();\n },\n\n /**\n * 把首个被选中的消息行滚动到容器内可见。\n * 选中行是「用户问 + AI 答」中的靠前一条,滚到它即能同时看到整组。\n * 若靠前那条已在视野内则不动,避免无谓跳动。\n */\n scrollSelectedIntoView: function scrollSelectedIntoView() {\n var _this15 = this;\n\n this.$nextTick(function () {\n var container = _this15.$refs.messagesRef;\n if (!container) return;\n var rows = container.querySelectorAll('.msg-row');\n if (!rows.length) return; // 找到第一个处于选中态、且带 id 的行(与 selectedMsgIds 顺序无关,取 DOM 顺序)\n\n var target = null;\n\n for (var i = 0; i < rows.length; i++) {\n var box = rows[i].querySelector('.msg-check-box.checked');\n\n if (box) {\n target = rows[i];\n break;\n }\n }\n\n if (!target) return;\n var cRect = container.getBoundingClientRect();\n var tRect = target.getBoundingClientRect(); // 目标行顶部已在容器可视区内则无需滚动\n\n if (tRect.top >= cRect.top && tRect.top < cRect.bottom) return; // 滚动到目标行顶部(留 8px 间距)\n\n container.scrollTop += tRect.top - cRect.top - 8;\n });\n },\n exitSelectMode: function exitSelectMode() {\n this.selectMode = false;\n this.selectedMsgIds = [];\n },\n isMsgSelected: function isMsgSelected(msg) {\n return !!msg.id && this.selectedMsgIds.indexOf(msg.id) !== -1;\n },\n\n /** 归一化 role,兼容后端可能返回的大小写/空白 */\n normRole: function normRole(role) {\n return String(role == null ? '' : role).trim().toLowerCase();\n },\n\n /**\n * 取与下标 idx 处消息成对的另一条:用户问 ↔ AI 答视为一组问答。\n * 只认紧邻的一条(问答对在列表里必然相邻),相邻非同组则不配对。\n * 不依赖 role 的具体拼写(user/assistant 的大小写、空白均容错)。\n */\n getQaPartnerAt: function getQaPartnerAt(idx) {\n if (idx < 0 || idx >= this.messages.length) return null;\n var cur = this.messages[idx];\n if (!cur) return null;\n var role = this.normRole(cur.role);\n if (role !== 'user' && role !== 'assistant') return null; // 只认紧邻的那一条:问答对在列表里必然相邻。\n // 若相邻消息不是异角色(例如回答已被删除),说明当前消息已无配对,\n // 不能继续向前/向后扫描,否则会误勾到上一个问答的回答。\n\n var neighbor = role === 'user' ? this.messages[idx + 1] : this.messages[idx - 1];\n if (!neighbor) return null;\n var neighborRole = this.normRole(neighbor.role);\n var expect = role === 'user' ? 'assistant' : 'user';\n return neighborRole === expect ? neighbor : null;\n },\n\n /** 下标 idx 处消息所在问答对的可删 id 列表(用户问 + AI 答) */\n getQaPairIdsAt: function getQaPairIdsAt(idx) {\n var ids = [];\n var cur = this.messages[idx];\n if (cur && cur.id) ids.push(cur.id);\n var partner = this.getQaPartnerAt(idx);\n if (partner && partner.id && ids.indexOf(partner.id) === -1) ids.push(partner.id);\n return ids;\n },\n\n /**\n * 勾选单条消息:勾上时连带把问答对里另一条一起勾上;\n * 取消时只取消当前这条,从而支持「先整组勾选、再单独取消一条」的单条选择。\n */\n toggleSelectMessage: function toggleSelectMessage(msg) {\n var _this16 = this;\n\n if (!msg.id) return;\n var i = this.selectedMsgIds.indexOf(msg.id);\n\n if (i === -1) {\n var idx = this.messages.indexOf(msg);\n var pairIds = idx === -1 ? [msg.id] : this.getQaPairIdsAt(idx);\n pairIds.forEach(function (id) {\n if (_this16.selectedMsgIds.indexOf(id) === -1) _this16.selectedMsgIds.push(id);\n });\n } else {\n this.selectedMsgIds.splice(i, 1);\n }\n },\n toggleSelectAll: function toggleSelectAll() {\n var ids = this.deletableMsgIds;\n this.selectedMsgIds = this.selectedMsgIds.length === ids.length ? [] : ids.slice();\n },\n\n /** 多选模式下点击整行即切换选中 */\n onMsgRowClick: function onMsgRowClick(msg) {\n // 长按弹出菜单后浏览器还会补一次 click,短暂屏蔽避免误触\n if (Date.now() < this._ignoreClickUntil) return;\n if (this.selectMode) this.toggleSelectMessage(msg);\n },\n\n /** 移动端长按气泡弹出消息操作单(豆包式:复制 / 删除 / 多选) */\n onMsgTouchStart: function onMsgTouchStart(msg) {\n var _this17 = this;\n\n if (!this.isMobileActive || this.selectMode) return;\n this._longPressMsg = msg;\n clearTimeout(this._longPressTimer);\n this._longPressTimer = setTimeout(function () {\n _this17._longPressTimer = null; // 用户正在长按选中文本(复制用),此时不弹菜单,避免抢走选择行为\n\n var sel = window.getSelection && window.getSelection();\n if (sel && String(sel).trim()) return;\n\n _this17.openMsgAction(_this17._longPressMsg);\n }, 500);\n },\n onMsgTouchEnd: function onMsgTouchEnd() {\n if (this._longPressTimer) {\n clearTimeout(this._longPressTimer);\n this._longPressTimer = null;\n }\n },\n openMsgAction: function openMsgAction(msg) {\n this.msgAction = msg; // 长按抬手后浏览器可能补发 click,短暂屏蔽,避免刚弹出的菜单被立即关闭\n\n this._ignoreClickUntil = Date.now() + 350;\n },\n closeMsgAction: function closeMsgAction() {\n if (Date.now() < this._ignoreClickUntil) return;\n this.msgAction = null;\n },\n\n /** 移动端消息操作单:复制 / 删除(删除进入多选模式,与豆包一致) */\n doMsgAction: function doMsgAction(action) {\n var msg = this.msgAction;\n this.msgAction = null;\n if (!msg) return;\n\n if (action === 'copy') {\n this.copyText(msg.content);\n } else if (action === 'delete') {\n this.enterSelectMode(msg);\n }\n },\n\n /** 批量删除选中的消息 */\n handleDeleteSelected: function handleDeleteSelected() {\n var _this18 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee11() {\n var ids;\n return regeneratorRuntime.wrap(function _callee11$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n if (!(!_this18.selectedMsgIds.length || !_this18.activeSessionId)) {\n _context11.next = 2;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 2:\n ids = _this18.selectedMsgIds.slice();\n _context11.prev = 3;\n _context11.next = 6;\n return _this18.$confirm(\"\\u5220\\u9664\\u540E\\u65E0\\u6CD5\\u6062\\u590D\\uFF0C\\u786E\\u5B9A\\u5220\\u9664\\u9009\\u4E2D\\u7684 \".concat(ids.length, \" \\u6761\\u6D88\\u606F\\u5417\\uFF1F\"), '删除消息', {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning'\n });\n\n case 6:\n _context11.next = 11;\n break;\n\n case 8:\n _context11.prev = 8;\n _context11.t0 = _context11[\"catch\"](3);\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.prev = 11;\n _context11.next = 14;\n return (0, _kbChat.deleteKbMessages)(_this18.activeSessionId, ids);\n\n case 14:\n _this18.messages = _this18.messages.filter(function (m) {\n return ids.indexOf(m.id) === -1;\n });\n\n _this18.exitSelectMode();\n\n _this18.$message.success('删除成功');\n\n _this18.refreshSessionList();\n\n _context11.next = 24;\n break;\n\n case 20:\n _context11.prev = 20;\n _context11.t1 = _context11[\"catch\"](11);\n console.error('批量删除消息失败', _context11.t1);\n\n _this18.$message.error('删除失败');\n\n case 24:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _callee11, null, [[3, 8], [11, 20]]);\n }))();\n },\n handleResetSession: function handleResetSession() {\n var _this19 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee12() {\n return regeneratorRuntime.wrap(function _callee12$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n if (!_this19.activeSessionId) {\n _context12.next = 8;\n break;\n }\n\n _context12.prev = 1;\n _context12.next = 4;\n return (0, _kbChat.closeKbSession)(_this19.activeSessionId);\n\n case 4:\n _context12.next = 8;\n break;\n\n case 6:\n _context12.prev = 6;\n _context12.t0 = _context12[\"catch\"](1);\n\n case 8:\n _context12.next = 10;\n return _this19.autoCreateSession();\n\n case 10:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _callee12, null, [[1, 6]]);\n }))();\n },\n handleSend: function handleSend(quickText) {\n var _this20 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee13() {\n var text, requestSessionId, requestId, msgKey, msgIdx, streamMsg, isCurrentStream, answerBuffer, finalContent, streamDone, url;\n return regeneratorRuntime.wrap(function _callee13$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n text = typeof quickText === 'string' ? quickText.trim() : _this20.inputText.trim();\n\n if (!(!text || !_this20.activeSessionId || _this20.streaming)) {\n _context13.next = 3;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 3:\n _this20.exitSelectMode();\n\n _this20.inputText = '';\n requestSessionId = _this20.activeSessionId;\n requestId = ++_this20.streamRequestId;\n\n _this20.clearTypeTimer();\n\n _this20.messages.push({\n role: 'user',\n content: text\n });\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n }); // 流式消息:作为 messages 中的对象(带 _key/loading),与 ChatDetail 一致\n\n\n msgKey = 'stream_' + requestId;\n msgIdx = _this20.messages.push({\n role: 'assistant',\n content: '',\n thinking: '',\n toolCalls: [],\n loading: true,\n _key: msgKey\n }) - 1;\n streamMsg = _this20.messages[msgIdx];\n _this20.streamMsg = streamMsg;\n _this20.streaming = true;\n\n isCurrentStream = function isCurrentStream() {\n return requestId === _this20.streamRequestId && requestSessionId === _this20.activeSessionId && _this20.streamMsg === streamMsg;\n }; // SSE 只把数据累积到 answerBuffer。typeTimer(30ms) 节流按「完整换行」逐行把内容\n // 推进到 msg.content:每个 tick 最多只推进「到下一个 \\n 为止」的一行,未完成的尾行\n // 保持不动;配合 renderStreaming(breaks:true) 实现「一行显示一行」。\n // 不是打字机模式(不逐字)、也不是一次性整段渲染(不会一出就是全文)。\n\n\n answerBuffer = '';\n finalContent = '';\n streamDone = false;\n _this20._typeTimer = setInterval(function () {\n if (!isCurrentStream()) return;\n var msg = _this20.messages[msgIdx];\n var source = finalContent || answerBuffer;\n var curLen = msg._prevReveal || 0;\n var nextLen = curLen; // 找到「当前已揭示进度之后」的第一条完整换行,仅推进这一行\n\n var nl = source.indexOf('\\n', curLen);\n\n if (nl !== -1) {\n nextLen = nl + 1;\n } else if (streamDone) {\n // 流结束且没有更多换行:最后一段未以换行结尾的内容一次性露出(此时即完整一行)\n nextLen = source.length;\n }\n\n if (nextLen > curLen) {\n msg._prevReveal = nextLen;\n msg.content = source.substring(0, nextLen);\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n } // 流结束且所有内容都已逐行露出 → 收尾\n\n\n if (streamDone && nextLen >= source.length) {\n _this20.finishStream(); // 后端已在此刻落库,回读一次补全消息 id(单条删除需要)\n\n\n _this20.syncMessageIds();\n }\n }, 30);\n url = (0, _kbChat.createKbStreamUrl)(_this20.activeSessionId, text);\n _this20.eventSource = new EventSource(url);\n\n _this20.eventSource.addEventListener('tool_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n\n if (e.data && _this20.messages[msgIdx]) {\n if (!_this20.messages[msgIdx].toolCalls) _this20.messages[msgIdx].toolCalls = [];\n\n _this20.messages[msgIdx].toolCalls.push(e.data);\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n }\n });\n\n _this20.eventSource.addEventListener('answer', function (e) {\n if (!isCurrentStream() || streamDone) return;\n answerBuffer += e.data;\n });\n\n _this20.eventSource.addEventListener('agent_result', function (e) {\n if (!isCurrentStream() || streamDone) return; // 与档案君(AiChat.vue)对齐:AGENT_RESULT 为最终真相源。\n // 只记录 finalContent 并结束流,不打断已在逐行推进的 content,交给 typeTimer 以 target 推进/校正。\n\n if (e.data) finalContent = e.data;\n streamDone = true;\n\n _this20.closeEventSource();\n });\n\n _this20.eventSource.addEventListener('msgKey', function (e) {\n if (!isCurrentStream()) return;\n\n if (e.data && _this20.messages[msgIdx]) {\n _this20.messages[msgIdx].msgKey = e.data;\n }\n });\n\n _this20.eventSource.addEventListener('reasoning', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking += e.data;\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n });\n\n _this20.eventSource.addEventListener('reasoning_result', function (e) {\n if (!isCurrentStream() || streamDone) return;\n streamMsg.thinking = e.data;\n\n _this20.$nextTick(function () {\n return _this20.scrollToBottom();\n });\n });\n\n _this20.eventSource.addEventListener('error', function () {\n if (!isCurrentStream() || streamDone) return;\n if (!answerBuffer) answerBuffer = '❌ 连接中断';\n streamDone = true;\n\n _this20.closeEventSource();\n });\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _callee13);\n }))();\n },\n handleStop: function handleStop() {\n this.streamRequestId++;\n\n if (this.activeSessionId) {\n (0, _kbChat.stopKbStream)(this.activeSessionId).catch(function () {});\n }\n\n this.finishStream();\n },\n finishStream: function finishStream() {\n var _this21 = this;\n\n this.clearTypeTimer();\n\n if (this.streamMsg) {\n this.$delete(this.contentCache, this.streamMsg._key);\n this.streamMsg.loading = false;\n this.streamMsg = null;\n }\n\n this.closeEventSource();\n this.streaming = false;\n this.refreshSessionList();\n this.$nextTick(function () {\n return _this21.scrollToBottom();\n });\n },\n\n /** 流式结束后刷新会话列表(更新标题/最后消息) */\n refreshSessionList: function refreshSessionList() {\n var _this22 = this;\n\n return (0, _asyncToGenerator2.default)( /*#__PURE__*/regeneratorRuntime.mark(function _callee14() {\n var list;\n return regeneratorRuntime.wrap(function _callee14$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n if (_this22.companyId) {\n _context14.next = 2;\n break;\n }\n\n return _context14.abrupt(\"return\");\n\n case 2:\n _context14.prev = 2;\n _context14.next = 5;\n return (0, _kbChat.getKbSessionList)(_this22.companyId);\n\n case 5:\n _context14.t0 = _context14.sent;\n\n if (_context14.t0) {\n _context14.next = 8;\n break;\n }\n\n _context14.t0 = [];\n\n case 8:\n list = _context14.t0;\n _this22.sessionList = list;\n _context14.next = 15;\n break;\n\n case 12:\n _context14.prev = 12;\n _context14.t1 = _context14[\"catch\"](2);\n console.error('刷新会话列表失败', _context14.t1);\n\n case 15:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _callee14, null, [[2, 12]]);\n }))();\n },\n clearTypeTimer: function clearTypeTimer() {\n if (this._typeTimer) {\n clearInterval(this._typeTimer);\n this._typeTimer = null;\n }\n },\n closeEventSource: function closeEventSource() {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n },\n copyText: function copyText(text) {\n if (navigator.clipboard) {\n navigator.clipboard.writeText(text);\n }\n },\n scrollToBottom: function scrollToBottom() {\n var el = this.$refs.messagesRef;\n\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack://ZjPluginIntelligent/./src/lib/knowledgeBase/KbChatMain.vue?./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options");
|
|
1025
1025
|
|
|
1026
1026
|
/***/ }),
|
|
1027
1027
|
|
|
@@ -14756,7 +14756,7 @@ eval("// Generated by CoffeeScript 1.12.7\n(function() {\n var XMLDocument, XML
|
|
|
14756
14756
|
/*! exports provided: name, version, main, private, scripts, dependencies, devDependencies, browserslist, directories, keywords, license, files, default */
|
|
14757
14757
|
/***/ (function(module) {
|
|
14758
14758
|
|
|
14759
|
-
eval("module.exports = JSON.parse(\"{\\\"name\\\":\\\"zj-plugin-intelligent\\\",\\\"version\\\":\\\"1.2.26-beta.
|
|
14759
|
+
eval("module.exports = JSON.parse(\"{\\\"name\\\":\\\"zj-plugin-intelligent\\\",\\\"version\\\":\\\"1.2.26-beta.3\\\",\\\"main\\\":\\\"lib/ZjPluginIntelligent.umd.min.js\\\",\\\"private\\\":false,\\\"scripts\\\":{\\\"serve\\\":\\\"set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve\\\",\\\"build\\\":\\\"set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build\\\",\\\"lib\\\":\\\"node scripts/build-lib.js\\\",\\\"lib:test\\\":\\\"node scripts/build-lib.js test\\\"},\\\"dependencies\\\":{\\\"@jboltai/tokui\\\":\\\"^0.1.6\\\",\\\"crypto-js\\\":\\\"^4.2.0\\\",\\\"docx\\\":\\\"^8.5.0\\\",\\\"html2canvas\\\":\\\"^1.4.1\\\",\\\"mammoth\\\":\\\"^1.6.0\\\",\\\"markdown-it\\\":\\\"^13.0.2\\\",\\\"marked\\\":\\\"^4.3.0\\\",\\\"xlsx\\\":\\\"^0.18.5\\\"},\\\"devDependencies\\\":{\\\"@babel/preset-env\\\":\\\"^7.14.9\\\",\\\"@vue/cli-plugin-babel\\\":\\\"~4.5.0\\\",\\\"@vue/cli-plugin-eslint\\\":\\\"~4.5.0\\\",\\\"@vue/cli-plugin-vuex\\\":\\\"~4.5.0\\\",\\\"@vue/cli-service\\\":\\\"~4.5.0\\\",\\\"axios\\\":\\\"^0.27.2\\\",\\\"babel-eslint\\\":\\\"^10.1.0\\\",\\\"babel-plugin-component\\\":\\\"^1.1.1\\\",\\\"compression-webpack-plugin\\\":\\\"^6.0.3\\\",\\\"core-js\\\":\\\"^3.6.5\\\",\\\"element-ui\\\":\\\"^2.15.4\\\",\\\"eslint\\\":\\\"^6.8.0\\\",\\\"eslint-plugin-vue\\\":\\\"^7.13.0\\\",\\\"js-cookie\\\":\\\"^2.2.1\\\",\\\"obs-upload\\\":\\\"^1.0.4\\\",\\\"pako\\\":\\\"^2.1.0\\\",\\\"reconnecting-websocket\\\":\\\"^4.4.0\\\",\\\"sass\\\":\\\"^1.71.0\\\",\\\"sass-loader\\\":\\\"^8.0.2\\\",\\\"snowflake-id\\\":\\\"^1.1.0\\\",\\\"vue\\\":\\\"^2.6.11\\\",\\\"vue-template-compiler\\\":\\\"^2.6.11\\\",\\\"vuex\\\":\\\"^3.4.0\\\",\\\"webpack-bundle-analyzer\\\":\\\"^4.4.2\\\"},\\\"browserslist\\\":[\\\"> 1%\\\",\\\"last 2 versions\\\",\\\"not dead\\\"],\\\"directories\\\":{\\\"lib\\\":\\\"lib\\\"},\\\"keywords\\\":[],\\\"license\\\":\\\"ISC\\\",\\\"files\\\":[\\\"lib\\\"]}\");\n\n//# sourceURL=webpack://ZjPluginIntelligent/./package.json?");
|
|
14760
14760
|
|
|
14761
14761
|
/***/ }),
|
|
14762
14762
|
|