代码提交

master
wujingling0908 5 days ago
parent e27f886b03
commit ea4a323213
  1. 16
      src/components/chat-new.vue
  2. 60
      src/store/app.js
  3. 2
      src/tests/store-smoke.test.js
  4. 42
      src/utils/index.js
  5. 20
      src/views/chat.vue

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

@ -739,15 +739,69 @@ 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,6 +24,8 @@ 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,31 +469,57 @@ export function decryptBase64AesByVersionNo1(versionNo1Cipher, base64CipherText)
* 1) versionNo1 AES 解密 * 1) versionNo1 AES 解密
* 2) 若已是明文(非 JWT)则原样返回 * 2) 若已是明文(非 JWT)则原样返回
*/ */
export function decodeEntryOpenid(entryOpenid, versionNo1Cipher = "") { export function normalizeEntryOpenIdPlain(text) {
if (entryOpenid === undefined || entryOpenid === null || entryOpenid === "") return ""
const raw = String(entryOpenid).trim()
if (!raw) return ""
const normalizePlainOpenId = (text) => {
if (!text) return "" if (!text) return ""
let trimmed = String(text).replace(/^["']+|["']+$/g, "").trim() let trimmed = String(text).replace(/^["']+|["']+$/g, "").trim()
if (!trimmed) return "" if (!trimmed) return ""
// 1) JSON:{"iss":"hDjN0ynT..."}
try { try {
const parsed = JSON.parse(trimmed) const parsed = JSON.parse(trimmed)
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) {}
// 2) JWT:header.payload.sig,payload 里取 iss
if (/^eyJ/i.test(trimmed) && trimmed.includes(".")) {
try {
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) {}
}
// 3) 明文带 iss 前缀:iss + openId
if (trimmed.startsWith("iss") && trimmed.length > 3) {
const maybe = trimmed.slice(3).trim()
if (maybe) return maybe
}
return trimmed 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 = normalizePlainOpenId(plain) const openId = normalizeEntryOpenIdPlain(plain)
if (openId) return openId if (openId) return openId
} }
// 已是明文 openId(例如 hDjN0ynT...),不要把 JWT 当明文 // 已是明文 openId(例如 hDjN0ynT...),不要把 JWT 当明文
if (/^eyJ/i.test(raw)) return "" if (/^eyJ/i.test(raw)) return normalizeEntryOpenIdPlain(raw)
if (raw.length >= 20) return raw if (raw.length >= 20) return raw
return "" return ""
} }

@ -53,7 +53,6 @@ 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')
@ -96,7 +95,6 @@ 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 = {
@ -176,7 +174,6 @@ 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", {
@ -239,20 +236,23 @@ const initToken = async () => {
blacklistFlag: data.blacklistFlag || '', blacklistFlag: data.blacklistFlag || '',
} }
store.setUserInfo(nextUserInfo) store.setUserInfo(nextUserInfo)
const versionNo = nextUserInfo.versionNo1 || 'DyDzGOFvYWWyQ8jlL5BqjdtvRNj4/UJvWWLm890KZiJ/5LrmwLjwcpfDXI5w2knS' const versionNo = nextUserInfo.versionNo1 || ''
// Base64 AES 密文 -> 明文 // entryOpenid: Base64 AES 密文 -> 明文;明文若含 iss(JSON/前缀/JWT)再取出 openId
const base64Cipher = 'SjU9XqI98fNbuE5mk/r3AQy3q7sZtWE6ig/urNWQw9PfYhp5eb/J9iLezdOGsxda' const base64Cipher = data.entryOpenid || ''
const aesPlain = decryptBase64AesByVersionNo1(versionNo, base64Cipher) const aesPlain = decryptBase64AesByVersionNo1(versionNo, base64Cipher)
const apiDecoded = decodeEntryOpenid(nextUserInfo.entryOpenid, versionNo) const apiDecoded = decodeEntryOpenid(base64Cipher, versionNo)
console.log('Base64 AES 解密 =>', { console.log('Base64 AES 解密 =>', {
base64Cipher, base64Cipher,
aesPlain, aesPlain,
expectedPlain: 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y',
ok: aesPlain === 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y',
apiEntryOpenid: nextUserInfo.entryOpenid,
apiDecoded, apiDecoded,
expectedPlain: 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y',
ok: apiDecoded === 'hDjN0ynT8463W0jH98-eqBask4bW_UtDRjpnMFkqD-Y',
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