mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 12:07:01 +08:00
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import type { Contact } from '../../shared/types'
|
|
import {
|
|
emptyContactResolution,
|
|
normalizeContactName,
|
|
type ContactResolutionCandidate,
|
|
type ContactResolutionMatch,
|
|
type ContactResolutionResult
|
|
} from '../../shared/contact-resolution'
|
|
|
|
export type ContactResolutionScope = 'any' | 'person' | 'group'
|
|
|
|
const displayName = (contact: Contact): string =>
|
|
contact.m_nsNickName || contact.remark || contact.wechatNickname || contact.m_nsUsrName
|
|
|
|
const aliases = (contact: Contact): Array<{ value: string; primary: boolean }> => {
|
|
const groupName = contact.m_nsNickName?.trim() || ''
|
|
const safeGroupAlias =
|
|
contact.type === 'group' && groupName && !/群(?:聊)?$/.test(groupName)
|
|
? [{ value: `${groupName}群`, primary: false }]
|
|
: []
|
|
return [
|
|
{ value: contact.m_nsNickName, primary: true },
|
|
{ value: contact.remark || '', primary: false },
|
|
{ value: contact.wechatNickname || '', primary: false },
|
|
{ value: contact.m_nsUsrName, primary: false },
|
|
...safeGroupAlias
|
|
].filter((item) => Boolean(normalizeContactName(item.value)))
|
|
}
|
|
|
|
/**
|
|
* The one main-process authority that converts a user/Agent supplied name to
|
|
* an existing conversation. It only auto-confirms an exact canonical alias.
|
|
* Fuzzy discovery intentionally returns candidates rather than a guessed ID.
|
|
*/
|
|
export function resolveContact(
|
|
query: string,
|
|
contacts: Contact[],
|
|
scope: ContactResolutionScope = 'any'
|
|
): ContactResolutionResult {
|
|
const normalizedQuery = normalizeContactName(query)
|
|
if (!normalizedQuery) return emptyContactResolution()
|
|
const matches = new Map<string, { contact: Contact; matchedBy: ContactResolutionMatch }>()
|
|
|
|
for (const contact of contacts) {
|
|
if (!contact.md5) continue
|
|
if (scope === 'person' && contact.type !== 'user') continue
|
|
if (scope === 'group' && contact.type !== 'group') continue
|
|
for (const alias of aliases(contact)) {
|
|
if (normalizeContactName(alias.value) !== normalizedQuery) continue
|
|
const rawExact =
|
|
alias.value.trim().normalize('NFKC').toLocaleLowerCase() ===
|
|
query.trim().normalize('NFKC').toLocaleLowerCase()
|
|
const matchedBy: ContactResolutionMatch = alias.primary
|
|
? rawExact
|
|
? 'exact'
|
|
: 'normalized'
|
|
: 'alias'
|
|
const current = matches.get(contact.md5)
|
|
if (!current || (current.matchedBy === 'alias' && matchedBy !== 'alias')) {
|
|
matches.set(contact.md5, { contact, matchedBy })
|
|
}
|
|
}
|
|
}
|
|
|
|
const candidates: ContactResolutionCandidate[] = Array.from(matches.values())
|
|
.map(({ contact, matchedBy }) => ({
|
|
conversationId: contact.md5,
|
|
displayName: displayName(contact),
|
|
matchedBy,
|
|
confidence: 1
|
|
}))
|
|
.sort((left, right) => left.displayName.localeCompare(right.displayName, 'zh-CN'))
|
|
if (candidates.length !== 1) {
|
|
return {
|
|
...emptyContactResolution(),
|
|
candidates,
|
|
ambiguous: candidates.length > 1
|
|
}
|
|
}
|
|
const candidate = candidates[0]
|
|
const contact = matches.get(candidate.conversationId)!.contact
|
|
return {
|
|
matched: true,
|
|
personId: contact.m_nsUsrName,
|
|
conversationId: contact.md5,
|
|
canonicalName: displayName(contact),
|
|
displayName: candidate.displayName,
|
|
matchedBy: candidate.matchedBy,
|
|
confidence: candidate.confidence,
|
|
candidates,
|
|
ambiguous: false
|
|
}
|
|
}
|