From ea4a32321331d3c3f624516edd2c38c35c5f9a6f Mon Sep 17 00:00:00 2001 From: wujingling0908 <2.com> Date: Tue, 29 Sep 2026 16:13:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat-new.vue | 16 +++++++--- src/store/app.js | 60 +++++++++++++++++++++++++++++++++-- src/tests/store-smoke.test.js | 2 ++ src/utils/index.js | 50 ++++++++++++++++++++++------- src/views/chat.vue | 20 ++++++------ 5 files changed, 118 insertions(+), 30 deletions(-) diff --git a/src/components/chat-new.vue b/src/components/chat-new.vue index 335b574..2fc5ea5 100644 --- a/src/components/chat-new.vue +++ b/src/components/chat-new.vue @@ -39,11 +39,17 @@ const { isWX, tokenExist, lostChatInfoNum } = defineProps({ }, }) const store = inject('appStore', null) -const chatArr = inject('chatArr', ref([])) -const isChatLoading = inject('isChatLoading', ref(false)) -const sessionId = inject('sessionId', ref('')) -const userId = inject('userId', ref('')) -const isLostMessage = inject('isLostMessage', ref(null)) +// 直接从 pinia store 取响应式字段,避免 inject('chatArr') 在打包后拿到空默认 ref +const chatArr = computed({ + get: () => store?.chatArr || [], + set: (val) => { + if (store) store.chatArr = val + }, +}) +const isChatLoading = computed(() => !!store?.isChatLoading) +const sessionId = computed(() => store?.sessionId || '') +const userId = computed(() => store?.userId || '') +const isLostMessage = computed(() => store?.isLostMessage ?? null) const mainContainerRef = ref(null) const webSocketChannel = '95460000' // 投诉直连渠道号 // mescroll 实例(非响应式) diff --git a/src/store/app.js b/src/store/app.js index 0879ac0..dda8bed 100644 --- a/src/store/app.js +++ b/src/store/app.js @@ -739,15 +739,69 @@ export const useAppStore = defineStore('app', () => { pushConversationMessage(sendParmas.value, 'user') } - // 消息列表 + // 消息列表:写入 chatList,并同步追加到页面渲染用的 chatArr function setChatList(payload) { + if (payload == null || payload === '') return [] + const list = Array.isArray(payload) ? payload : [payload] + const appended = [] + list.forEach((entry) => { if (entry == null) return - const normalizedItem = utils.normalizeMessage(entry) + + // 兼容接口偶发包一层 data + const rawItem = + typeof entry === 'object' && entry.data && !entry.msgText && !entry.answer + ? entry.data + : entry + + const normalizedItem = + typeof rawItem === 'object' + ? utils.normalizeMessage({ ...rawItem }) + : { msgText: String(rawItem), fromFlag: '2', reqAndResType: '01', msgType: '01' } + + // 兜底:保证 bot 文本消息有可渲染字段,避免 format 后被丢弃 + if (typeof normalizedItem === 'object') { + if (!normalizedItem.msgText) { + normalizedItem.msgText = + normalizedItem.answer || + normalizedItem.answerText || + normalizedItem.content || + normalizedItem.text || + '' + } + if (normalizedItem.fromFlag === undefined || normalizedItem.fromFlag === null || normalizedItem.fromFlag === '') { + normalizedItem.fromFlag = '2' + } + if (!normalizedItem.reqAndResType) normalizedItem.reqAndResType = '01' + if (!normalizedItem.msgType) normalizedItem.msgType = '01' + if (!normalizedItem.timestamp) normalizedItem.timestamp = Date.now() + } + chatList.value.push(normalizedItem) - pushConversationMessage(normalizedItem, 'bot') + + let message = formatConversationMessage(normalizedItem, 'bot') + if (!message && normalizedItem?.msgText) { + message = formatConversationMessage( + { + ...normalizedItem, + fromFlag: '2', + reqAndResType: '01', + msgType: '01', + }, + 'bot', + ) + } + if (message) appended.push(message) }) + + if (appended.length) { + // 用新数组赋值,避免个别打包环境下 push 对视图更新不稳定 + chatArr.value = chatArr.value.concat(appended) + isChatLoading.value = false + } + + return appended } function delChatList(index) { diff --git a/src/tests/store-smoke.test.js b/src/tests/store-smoke.test.js index 4e6e9cb..ca8763d 100644 --- a/src/tests/store-smoke.test.js +++ b/src/tests/store-smoke.test.js @@ -24,6 +24,8 @@ describe('store/index.js smoke', () => { expect(store.userInfo.name).toBe('测试') expect(store.sessionId).toBe('sid-1') expect(store.chatList.length).toBeGreaterThan(0) + expect(store.chatArr.length).toBeGreaterThan(0) + expect(store.chatArr[0].msgText).toBe('hi') expect(store.sendParmas.msgText).toBe('hello') }) }) diff --git a/src/utils/index.js b/src/utils/index.js index 964f578..f3708a3 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -469,31 +469,57 @@ export function decryptBase64AesByVersionNo1(versionNo1Cipher, base64CipherText) * 1) versionNo1 AES 解密 * 2) 若已是明文(非 JWT)则原样返回 */ -export function decodeEntryOpenid(entryOpenid, versionNo1Cipher = "") { - if (entryOpenid === undefined || entryOpenid === null || entryOpenid === "") return "" - const raw = String(entryOpenid).trim() - if (!raw) return "" +export function normalizeEntryOpenIdPlain(text) { + if (!text) return "" + let trimmed = String(text).replace(/^["']+|["']+$/g, "").trim() + if (!trimmed) return "" - const normalizePlainOpenId = (text) => { - if (!text) return "" - let trimmed = String(text).replace(/^["']+|["']+$/g, "").trim() - if (!trimmed) return "" + // 1) JSON:{"iss":"hDjN0ynT..."} + try { + const parsed = JSON.parse(trimmed) + const iss = parsed?.iss ?? parsed?.openId ?? parsed?.openid ?? "" + if (iss) return String(iss).trim() + } catch (e) {} + + // 2) JWT:header.payload.sig,payload 里取 iss + if (/^eyJ/i.test(trimmed) && trimmed.includes(".")) { try { - const parsed = JSON.parse(trimmed) + const payloadPart = trimmed.split(".")[1] || "" + const normalized = payloadPart.replace(/-/g, "+").replace(/_/g, "/") + const padLen = (4 - (normalized.length % 4)) % 4 + const padded = normalized + "=".repeat(padLen) + const json = decodeURIComponent( + Array.from(atob(padded), (c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`).join(""), + ) + const parsed = JSON.parse(json) const iss = parsed?.iss ?? parsed?.openId ?? parsed?.openid ?? "" if (iss) return String(iss).trim() } catch (e) {} - return trimmed } + // 3) 明文带 iss 前缀:iss + openId + if (trimmed.startsWith("iss") && trimmed.length > 3) { + const maybe = trimmed.slice(3).trim() + if (maybe) return maybe + } + + return trimmed +} + +export function decodeEntryOpenid(entryOpenid, versionNo1Cipher = "") { + if (entryOpenid === undefined || entryOpenid === null || entryOpenid === "") return "" + const raw = String(entryOpenid).trim() + if (!raw) return "" + if (versionNo1Cipher) { + // Base64 AES 密文解密后,再从明文中取 iss/openId const plain = decryptByVersionNo1(versionNo1Cipher, raw) - const openId = normalizePlainOpenId(plain) + const openId = normalizeEntryOpenIdPlain(plain) if (openId) return openId } // 已是明文 openId(例如 hDjN0ynT...),不要把 JWT 当明文 - if (/^eyJ/i.test(raw)) return "" + if (/^eyJ/i.test(raw)) return normalizeEntryOpenIdPlain(raw) if (raw.length >= 20) return raw return "" } diff --git a/src/views/chat.vue b/src/views/chat.vue index 2d44a0d..c3b72cc 100644 --- a/src/views/chat.vue +++ b/src/views/chat.vue @@ -53,7 +53,6 @@ const urlParams = ref({}) // 生命周期 onActivated(() => { // keep-alive 返回时不重新初始化 - console.log('[chat] activated, hasInitialized=', hasInitialized.value, 'isToken=', isToken.value) // 路由组件激活后再补一次滚动恢复,确保子组件 DOM 已显示 try { const raw = sessionStorage.getItem('yykf_chat_main_scroll_top') @@ -96,7 +95,6 @@ onMounted(async () => { if (chatScrollTop != null) sessionStorage.setItem('yykf_chat_main_scroll_top', chatScrollTop) urlParams.value = Utils.getUrlParams() - console.log('txfModules参数', urlParams.value.txfModules) console.log('this.urlParams--', urlParams.value) let userInfo = { @@ -176,7 +174,6 @@ onMounted(async () => { urlParams.value = Utils.getUrlParams() initToken() - console.log('window', window) // 渠道校验 try { const checkRes = await Post("config/downSource", { @@ -239,20 +236,23 @@ const initToken = async () => { blacklistFlag: data.blacklistFlag || '', } store.setUserInfo(nextUserInfo) - const versionNo = nextUserInfo.versionNo1 || 'DyDzGOFvYWWyQ8jlL5BqjdtvRNj4/UJvWWLm890KZiJ/5LrmwLjwcpfDXI5w2knS' - // Base64 AES 密文 -> 明文 - const base64Cipher = 'SjU9XqI98fNbuE5mk/r3AQy3q7sZtWE6ig/urNWQw9PfYhp5eb/J9iLezdOGsxda' + const versionNo = nextUserInfo.versionNo1 || '' + // entryOpenid: Base64 AES 密文 -> 明文;明文若含 iss(JSON/前缀/JWT)再取出 openId + const base64Cipher = data.entryOpenid || '' const aesPlain = decryptBase64AesByVersionNo1(versionNo, base64Cipher) - const apiDecoded = decodeEntryOpenid(nextUserInfo.entryOpenid, versionNo) + const apiDecoded = decodeEntryOpenid(base64Cipher, versionNo) console.log('Base64 AES 解密 =>', { base64Cipher, aesPlain, - expectedPlain: 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y', - ok: aesPlain === 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y', - apiEntryOpenid: nextUserInfo.entryOpenid, apiDecoded, + expectedPlain: 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y', + ok: apiDecoded === 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y', sourceUserId: nextUserInfo.sourceUserId, }) + if (apiDecoded) { + nextUserInfo.entryOpenidPlain = apiDecoded + store.setUserInfo(nextUserInfo) + } if (nextUserInfo.blacklistFlag === '1' && nextUserInfo.bpErrorContent) {