更新
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 把「日志聚合出来的会话」对应回「账号真实会话列表」里的那一条。
|
||||
*
|
||||
* 这条链路决定手动发送打给谁,所以匹配必须是分级的、宁缺毋滥的:
|
||||
* 抖音昵称大量重复(还有「用户1234567」「未知用户」这类占位名),
|
||||
* 早先版本把 UID / 会话 ID / 昵称写在同一个 OR 谓词里交给 Array.find,
|
||||
* 于是列表里靠前的一条只要昵称相同就胜出,哪怕后面有 UID 精确匹配的那条
|
||||
* —— 结果就是消息发给了同名的另一个人。
|
||||
*/
|
||||
|
||||
export const isGenericPeerName = (name, peerUid = '') => {
|
||||
const value = String(name || '').trim()
|
||||
if (!value) return true
|
||||
if (value === '未知用户') return true
|
||||
if (peerUid && value === String(peerUid)) return true
|
||||
if (/^\d+$/.test(value)) return true
|
||||
if (/^用户\d+$/.test(value)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export const isConvId = (id) => /^0:1:\d+:\d+$/.test(String(id || '').trim())
|
||||
|
||||
export const extractPeerUid = (conv) => {
|
||||
const raw = String(conv?.sender_id || '').trim()
|
||||
if (!raw) return ''
|
||||
if (/^\d+$/.test(raw)) return raw
|
||||
if (isConvId(raw)) return raw.split(':')[3] || ''
|
||||
const last = raw.split(':').pop()
|
||||
return /^\d+$/.test(last || '') ? last : ''
|
||||
}
|
||||
|
||||
/** 会话条目代表的对方 UID:sender_id 优先,其次会话 ID 末段。 */
|
||||
export const conversationPeerUid = (item) =>
|
||||
extractPeerUid(item) || extractPeerUid({ sender_id: item?.conversation_id })
|
||||
|
||||
/**
|
||||
* 分级匹配,命中一级就返回,绝不降级:
|
||||
* 1) 对方 UID 精确相等 —— 有 UID 时只认 UID
|
||||
* 2) 会话 ID 完全相等
|
||||
* 3) 会话 ID 末段等于 sender_id,且全列表唯一
|
||||
* 4) 昵称相等,且昵称不是占位名、且全列表唯一
|
||||
* 任何一级出现多个候选都返回 null:宁可不匹配,也不能猜错人。
|
||||
*/
|
||||
export const matchPeerConversation = (list, conv) => {
|
||||
const items = Array.isArray(list) ? list : []
|
||||
if (!items.length || !conv) return null
|
||||
|
||||
const peerUid = extractPeerUid(conv)
|
||||
if (peerUid) {
|
||||
return items.find((item) => conversationPeerUid(item) === peerUid) || null
|
||||
}
|
||||
|
||||
const rawId = String(conv.sender_id || '').trim()
|
||||
if (rawId) {
|
||||
const exact = items.find(
|
||||
(item) => String(item.conversation_id || '').trim() === rawId
|
||||
)
|
||||
if (exact) return exact
|
||||
const suffix = items.filter((item) =>
|
||||
String(item.conversation_id || '').endsWith(`:${rawId}`)
|
||||
)
|
||||
if (suffix.length === 1) return suffix[0]
|
||||
if (suffix.length > 1) return null
|
||||
}
|
||||
|
||||
const name = String(conv.sender_name || '').trim()
|
||||
if (!name || isGenericPeerName(name)) return null
|
||||
const byName = items.filter(
|
||||
(item) => String(item.sender_name || '').trim() === name
|
||||
)
|
||||
return byName.length === 1 ? byName[0] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析手动发送要用的 conversation_id;无法确认对方身份时返回空串,
|
||||
* 由调用方提示用户,而不是拿一个「差不多」的会话把消息发出去。
|
||||
*/
|
||||
export const resolveConversationId = (list, conv) => {
|
||||
if (!conv) return ''
|
||||
const peerUid = extractPeerUid(conv)
|
||||
const match = matchPeerConversation(list, conv)
|
||||
|
||||
if (match?.conversation_id) {
|
||||
const matchedPeer = conversationPeerUid(match)
|
||||
// 最后一道断言:匹配到的会话必须和当前会话指向同一个人。
|
||||
if (!peerUid || !matchedPeer || matchedPeer === peerUid) {
|
||||
return String(match.conversation_id)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const raw = String(conv.sender_id || '').trim()
|
||||
if (isConvId(raw)) return raw
|
||||
// 裸 UID 是安全的:后端会用「当前账号 UID + 该 UID」拼出本账号的会话。
|
||||
if (/^\d+$/.test(raw)) return raw
|
||||
return ''
|
||||
}
|
||||
+29
-52
@@ -18,6 +18,12 @@ import {
|
||||
buildStickerPayload,
|
||||
parseMessageContent
|
||||
} from '../utils/messageContent'
|
||||
import {
|
||||
isGenericPeerName,
|
||||
extractPeerUid,
|
||||
matchPeerConversation,
|
||||
resolveConversationId as resolvePeerConversationId
|
||||
} from '../utils/peerMatch'
|
||||
|
||||
const route = useRoute()
|
||||
const logs = ref([])
|
||||
@@ -241,26 +247,6 @@ const getAccountName = (accountId) => {
|
||||
const getAccount = (accountId) =>
|
||||
accounts.value.find((a) => Number(a.id) === Number(accountId)) || null
|
||||
|
||||
const isGenericPeerName = (name, peerUid = '') => {
|
||||
const value = (name || '').trim()
|
||||
if (!value) return true
|
||||
if (peerUid && value === peerUid) return true
|
||||
if (/^\d+$/.test(value)) return true
|
||||
if (/^用户\d+$/.test(value)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const extractPeerUid = (conv) => {
|
||||
const raw = String(conv?.sender_id || '').trim()
|
||||
if (!raw) return ''
|
||||
if (/^\d+$/.test(raw)) return raw
|
||||
if (/^0:1:\d+:\d+$/.test(raw)) {
|
||||
return raw.split(':')[3] || ''
|
||||
}
|
||||
const last = raw.split(':').pop()
|
||||
return /^\d+$/.test(last || '') ? last : ''
|
||||
}
|
||||
|
||||
const peerUidFromLog = (log) => {
|
||||
const uid = extractPeerUid({ sender_id: log.sender_id })
|
||||
if (uid) return uid
|
||||
@@ -272,11 +258,18 @@ const peerUidFromLog = (log) => {
|
||||
if (fromConv) return fromConv
|
||||
}
|
||||
}
|
||||
if (log.sender_name && log.sender_name !== '[系统发送]') {
|
||||
const byName = list.find(
|
||||
(c) => c.sender_name === log.sender_name && extractPeerUid(c)
|
||||
// 昵称回退只在「非占位名 + 全列表唯一」时可信:抖音重名很多,
|
||||
// 猜错会把两个人的消息并进同一个会话,随后手动发送就发错人。
|
||||
if (
|
||||
log.sender_name
|
||||
&& log.sender_name !== '[系统发送]'
|
||||
&& !isGenericPeerName(log.sender_name)
|
||||
) {
|
||||
const byName = list.filter(
|
||||
(c) => String(c.sender_name || '').trim() === String(log.sender_name).trim()
|
||||
&& extractPeerUid(c)
|
||||
)
|
||||
if (byName) return extractPeerUid(byName)
|
||||
if (byName.length === 1) return extractPeerUid(byName[0])
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -310,17 +303,8 @@ const convKey = (log, peerUid = undefined) => {
|
||||
return `${log.account_id}::name:${log.sender_name || 'unknown'}`
|
||||
}
|
||||
|
||||
const findPeerMeta = (conv) => {
|
||||
const list = convListCache.value[conv.account_id] || []
|
||||
const peerUid = extractPeerUid(conv)
|
||||
return list.find(
|
||||
(c) =>
|
||||
c.sender_name === conv.sender_name ||
|
||||
c.conversation_id === conv.sender_id ||
|
||||
(peerUid && (c.sender_id === peerUid || extractPeerUid(c) === peerUid)) ||
|
||||
(conv.sender_id && c.conversation_id?.endsWith(`:${conv.sender_id}`))
|
||||
) || null
|
||||
}
|
||||
const findPeerMeta = (conv) =>
|
||||
matchPeerConversation(convListCache.value[conv.account_id] || [], conv)
|
||||
|
||||
const enrichConversation = (conv) => {
|
||||
const peer = findPeerMeta(conv)
|
||||
@@ -333,8 +317,6 @@ const enrichConversation = (conv) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isConvId = (id) => /^0:1:\d+:\d+$/.test(String(id || '').trim())
|
||||
|
||||
const fetchConvList = async (accountId) => {
|
||||
if (!accountId) return []
|
||||
if (convListCache.value[accountId]) {
|
||||
@@ -349,20 +331,8 @@ const fetchConvList = async (accountId) => {
|
||||
}
|
||||
}
|
||||
|
||||
const resolveConversationId = (conv) => {
|
||||
const peerUid = extractPeerUid(conv)
|
||||
const list = convListCache.value[conv.account_id] || []
|
||||
const match = list.find(
|
||||
(c) =>
|
||||
(peerUid && (c.sender_id === peerUid || extractPeerUid(c) === peerUid)) ||
|
||||
c.sender_name === conv.sender_name ||
|
||||
c.conversation_id === conv.sender_id ||
|
||||
(conv.sender_id && c.conversation_id?.endsWith(`:${conv.sender_id}`))
|
||||
)
|
||||
if (match?.conversation_id) return match.conversation_id
|
||||
if (isConvId(conv.sender_id)) return conv.sender_id
|
||||
return conv.sender_id || ''
|
||||
}
|
||||
const resolveConversationId = (conv) =>
|
||||
resolvePeerConversationId(convListCache.value[conv.account_id] || [], conv)
|
||||
|
||||
const isAccountOnline = (accountId) => {
|
||||
const acc = accounts.value.find(a => a.id === accountId)
|
||||
@@ -531,13 +501,20 @@ const sendMessage = async () => {
|
||||
}
|
||||
const conversationId = resolveConversationId(conv)
|
||||
if (!conversationId) {
|
||||
message.error('无法解析会话 ID,请刷新日志或到私信收发页重试')
|
||||
message.error(
|
||||
'无法确认这条会话对应的抖音用户(常见于昵称重复或对方资料未解析),'
|
||||
+ '已阻止发送以免发错人;请到「私信收发」页选中该用户后再发'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sending.value = true
|
||||
try {
|
||||
const body = { conversation_id: conversationId, content }
|
||||
// 把界面认定的收件人一并告诉后端:写入点会在发出去之前核对,
|
||||
// 万一会话匹配选错了人(例如昵称重复),发送会被拒绝而不是发错。
|
||||
const expectedPeerUid = extractPeerUid(conv)
|
||||
if (expectedPeerUid) body.peer_uid = expectedPeerUid
|
||||
const parsed = parseMessageContent(content)
|
||||
if (parsed.type === 'sticker') {
|
||||
body.message_type = 'sticker'
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
parseMessageContent
|
||||
} from '../utils/messageContent'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { conversationPeerUid } from '../utils/peerMatch'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const accounts = ref([])
|
||||
@@ -119,6 +120,9 @@ const sendMessage = async () => {
|
||||
sending.value = true
|
||||
try {
|
||||
const body = { conversation_id: selectedConv.value.conversation_id, content }
|
||||
// 会话是用户在列表里点选的,把它的对方 UID 一并送去后端核对收件人
|
||||
const expectedPeerUid = conversationPeerUid(selectedConv.value)
|
||||
if (expectedPeerUid) body.peer_uid = expectedPeerUid
|
||||
const parsed = parseMessageContent(content)
|
||||
if (parsed.type === 'sticker') {
|
||||
body.message_type = 'sticker'
|
||||
|
||||
Reference in New Issue
Block a user