Compare commits

..

No commits in common. '6ae5fb58cad7e019934d969e123b2e5423c74b24' and '492735e7185066c7690d50911bdc821de553524f' have entirely different histories.

  1. 1
      src/assets/style/chat.css
  2. 3
      src/components/abstract.vue
  3. 16
      src/components/chat-new.vue
  4. 60
      src/store/app.js
  5. 2
      src/tests/store-smoke.test.js
  6. 50
      src/utils/index.js
  7. 20
      src/views/chat.vue

@ -945,7 +945,6 @@ img.avatarUrl-img[lazy="loading"] {
.elder-wrapper .time-show span { .elder-wrapper .time-show span {
line-height: 18rem; line-height: 18rem;
padding: 4rem 8rem; padding: 4rem 8rem;
font-size: 12rem;
} }
.elder-wrapper .chat-txt-content { .elder-wrapper .chat-txt-content {

@ -43,8 +43,7 @@ const abstractList = computed(() => [
icon: rightsIcon, icon: rightsIcon,
}, },
] ]
.filter(item => item.num > (import.meta.env.VITE_USER_NODE_ENV === 'development' ? .filter(item => item.num > 0)
-1 : 0))
) )
const businessHandle = (userId) => { const businessHandle = (userId) => {
if ( if (

@ -39,17 +39,11 @@ const { isWX, tokenExist, lostChatInfoNum } = defineProps({
}, },
}) })
const store = inject('appStore', null) const store = inject('appStore', null)
// 直接从 pinia store 取响应式字段,避免 inject('chatArr') 在打包后拿到空默认 ref const chatArr = inject('chatArr', ref([]))
const chatArr = computed({ const isChatLoading = inject('isChatLoading', ref(false))
get: () => store?.chatArr || [], const sessionId = inject('sessionId', ref(''))
set: (val) => { const userId = inject('userId', ref(''))
if (store) store.chatArr = val const isLostMessage = inject('isLostMessage', ref(null))
},
})
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 mainContainerRef = ref(null)
const webSocketChannel = '95460000' // 投诉直连渠道号 const webSocketChannel = '95460000' // 投诉直连渠道号
// mescroll 实例(非响应式) // mescroll 实例(非响应式)

@ -739,69 +739,15 @@ export const useAppStore = defineStore('app', () => {
pushConversationMessage(sendParmas.value, 'user') pushConversationMessage(sendParmas.value, 'user')
} }
// 消息列表:写入 chatList,并同步追加到页面渲染用的 chatArr // 消息列表
function setChatList(payload) { function setChatList(payload) {
if (payload == null || payload === '') return []
const list = Array.isArray(payload) ? payload : [payload] const list = Array.isArray(payload) ? payload : [payload]
const appended = []
list.forEach((entry) => { list.forEach((entry) => {
if (entry == null) return 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) 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) { function delChatList(index) {

@ -24,8 +24,6 @@ describe('store/index.js smoke', () => {
expect(store.userInfo.name).toBe('测试') expect(store.userInfo.name).toBe('测试')
expect(store.sessionId).toBe('sid-1') expect(store.sessionId).toBe('sid-1')
expect(store.chatList.length).toBeGreaterThan(0) 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') expect(store.sendParmas.msgText).toBe('hello')
}) })
}) })

@ -469,57 +469,31 @@ export function decryptBase64AesByVersionNo1(versionNo1Cipher, base64CipherText)
* 1) versionNo1 AES 解密 * 1) versionNo1 AES 解密
* 2) 若已是明文(非 JWT)则原样返回 * 2) 若已是明文(非 JWT)则原样返回
*/ */
export function normalizeEntryOpenIdPlain(text) { export function decodeEntryOpenid(entryOpenid, versionNo1Cipher = "") {
if (!text) return "" if (entryOpenid === undefined || entryOpenid === null || entryOpenid === "") return ""
let trimmed = String(text).replace(/^["']+|["']+$/g, "").trim() const raw = String(entryOpenid).trim()
if (!trimmed) return "" if (!raw) 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 const normalizePlainOpenId = (text) => {
if (/^eyJ/i.test(trimmed) && trimmed.includes(".")) { if (!text) return ""
let trimmed = String(text).replace(/^["']+|["']+$/g, "").trim()
if (!trimmed) return ""
try { try {
const payloadPart = trimmed.split(".")[1] || "" const parsed = JSON.parse(trimmed)
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 ?? "" const iss = parsed?.iss ?? parsed?.openId ?? parsed?.openid ?? ""
if (iss) return String(iss).trim() if (iss) return String(iss).trim()
} catch (e) {} } 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) { if (versionNo1Cipher) {
// Base64 AES 密文解密后,再从明文中取 iss/openId
const plain = decryptByVersionNo1(versionNo1Cipher, raw) const plain = decryptByVersionNo1(versionNo1Cipher, raw)
const openId = normalizeEntryOpenIdPlain(plain) const openId = normalizePlainOpenId(plain)
if (openId) return openId if (openId) return openId
} }
// 已是明文 openId(例如 hDjN0ynT...),不要把 JWT 当明文 // 已是明文 openId(例如 hDjN0ynT...),不要把 JWT 当明文
if (/^eyJ/i.test(raw)) return normalizeEntryOpenIdPlain(raw) if (/^eyJ/i.test(raw)) return ""
if (raw.length >= 20) return raw if (raw.length >= 20) return raw
return "" return ""
} }

@ -53,6 +53,7 @@ const urlParams = ref({})
// 生命周期 // 生命周期
onActivated(() => { onActivated(() => {
// keep-alive 返回时不重新初始化 // keep-alive 返回时不重新初始化
console.log('[chat] activated, hasInitialized=', hasInitialized.value, 'isToken=', isToken.value)
// 路由组件激活后再补一次滚动恢复,确保子组件 DOM 已显示 // 路由组件激活后再补一次滚动恢复,确保子组件 DOM 已显示
try { try {
const raw = sessionStorage.getItem('yykf_chat_main_scroll_top') const raw = sessionStorage.getItem('yykf_chat_main_scroll_top')
@ -95,6 +96,7 @@ onMounted(async () => {
if (chatScrollTop != null) sessionStorage.setItem('yykf_chat_main_scroll_top', chatScrollTop) if (chatScrollTop != null) sessionStorage.setItem('yykf_chat_main_scroll_top', chatScrollTop)
urlParams.value = Utils.getUrlParams() urlParams.value = Utils.getUrlParams()
console.log('txfModules参数', urlParams.value.txfModules)
console.log('this.urlParams--', urlParams.value) console.log('this.urlParams--', urlParams.value)
let userInfo = { let userInfo = {
@ -174,6 +176,7 @@ onMounted(async () => {
urlParams.value = Utils.getUrlParams() urlParams.value = Utils.getUrlParams()
initToken() initToken()
console.log('window', window)
// 渠道校验 // 渠道校验
try { try {
const checkRes = await Post("config/downSource", { const checkRes = await Post("config/downSource", {
@ -236,23 +239,20 @@ const initToken = async () => {
blacklistFlag: data.blacklistFlag || '', blacklistFlag: data.blacklistFlag || '',
} }
store.setUserInfo(nextUserInfo) store.setUserInfo(nextUserInfo)
const versionNo = nextUserInfo.versionNo1 || '' const versionNo = nextUserInfo.versionNo1 || 'DyDzGOFvYWWyQ8jlL5BqjdtvRNj4/UJvWWLm890KZiJ/5LrmwLjwcpfDXI5w2knS'
// entryOpenid: Base64 AES 密文 -> 明文;明文若含 iss(JSON/前缀/JWT)再取出 openId // Base64 AES 密文 -> 明文
const base64Cipher = data.entryOpenid || '' const base64Cipher = 'SjU9XqI98fNbuE5mk/r3AQy3q7sZtWE6ig/urNWQw9PfYhp5eb/J9iLezdOGsxda'
const aesPlain = decryptBase64AesByVersionNo1(versionNo, base64Cipher) const aesPlain = decryptBase64AesByVersionNo1(versionNo, base64Cipher)
const apiDecoded = decodeEntryOpenid(base64Cipher, versionNo) const apiDecoded = decodeEntryOpenid(nextUserInfo.entryOpenid, versionNo)
console.log('Base64 AES 解密 =>', { console.log('Base64 AES 解密 =>', {
base64Cipher, base64Cipher,
aesPlain, aesPlain,
apiDecoded,
expectedPlain: 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y', expectedPlain: 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y',
ok: apiDecoded === 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y', ok: aesPlain === 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y',
apiEntryOpenid: nextUserInfo.entryOpenid,
apiDecoded,
sourceUserId: nextUserInfo.sourceUserId, sourceUserId: nextUserInfo.sourceUserId,
}) })
if (apiDecoded) {
nextUserInfo.entryOpenidPlain = apiDecoded
store.setUserInfo(nextUserInfo)
}
if (nextUserInfo.blacklistFlag === '1' && nextUserInfo.bpErrorContent) { if (nextUserInfo.blacklistFlag === '1' && nextUserInfo.bpErrorContent) {

Loading…
Cancel
Save