代码提交

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. 50
      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 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 实例(非响应式)

@ -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) {

@ -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')
})
})

@ -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 ""
}

@ -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) {

Loading…
Cancel
Save