Revert "feat: enrich mutual friend identities in export dialog"

This reverts commit f3027da43885a67583099008991dbfc4def3f4d1.
This commit is contained in:
aits2026
2026-03-06 20:15:51 +08:00
parent 94a010c9b2
commit 2d4a5fc62f
5 changed files with 147 additions and 617 deletions

View File

@@ -52,7 +52,6 @@ interface SnsContactIdentity {
remark?: string remark?: string
nickName?: string nickName?: string
displayName: string displayName: string
avatarUrl?: string
} }
interface ParsedLikeUser { interface ParsedLikeUser {
@@ -80,7 +79,6 @@ interface ArkmeLikeDetail {
remark?: string remark?: string
nickName?: string nickName?: string
displayName: string displayName: string
avatarUrl?: string
source: 'xml' | 'legacy' source: 'xml' | 'legacy'
} }
@@ -94,7 +92,6 @@ interface ArkmeCommentDetail {
remark?: string remark?: string
nickName?: string nickName?: string
displayName: string displayName: string
avatarUrl?: string
content: string content: string
refCommentId: string refCommentId: string
refNickname?: string refNickname?: string
@@ -105,7 +102,6 @@ interface ArkmeCommentDetail {
refRemark?: string refRemark?: string
refNickName?: string refNickName?: string
refDisplayName?: string refDisplayName?: string
refAvatarUrl?: string
emojis?: { url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }[] emojis?: { url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }[]
source: 'xml' | 'legacy' source: 'xml' | 'legacy'
} }
@@ -327,7 +323,6 @@ class SnsService {
let alias: string | undefined let alias: string | undefined
let remark: string | undefined let remark: string | undefined
let nickName: string | undefined let nickName: string | undefined
let avatarUrl = this.toOptionalString(cached?.avatarUrl)
try { try {
const contactResult = await wcdbService.getContact(normalized) const contactResult = await wcdbService.getContact(normalized)
@@ -341,17 +336,6 @@ class SnsService {
// 联系人补全失败不影响导出 // 联系人补全失败不影响导出
} }
if (!avatarUrl) {
try {
const avatarResult = await wcdbService.getAvatarUrls([normalized])
if (avatarResult.success && avatarResult.map) {
avatarUrl = this.toOptionalString(avatarResult.map[normalized])
}
} catch {
// 头像补全失败不影响导出
}
}
const displayName = remark || nickName || alias || cached?.displayName || normalized const displayName = remark || nickName || alias || cached?.displayName || normalized
return { return {
username: normalized, username: normalized,
@@ -360,8 +344,7 @@ class SnsService {
wechatId: alias, wechatId: alias,
remark, remark,
nickName, nickName,
displayName, displayName
avatarUrl
} }
})() })()
identityCache.set(normalized, pending) identityCache.set(normalized, pending)
@@ -429,7 +412,6 @@ class SnsService {
remark: identity?.remark, remark: identity?.remark,
nickName: identity?.nickName, nickName: identity?.nickName,
displayName: identity?.displayName || nickname || username || '', displayName: identity?.displayName || nickname || username || '',
avatarUrl: identity?.avatarUrl,
source: likeSource source: likeSource
}) })
} }
@@ -501,7 +483,6 @@ class SnsService {
remark: actor?.remark, remark: actor?.remark,
nickName: actor?.nickName, nickName: actor?.nickName,
displayName: actor?.displayName || nickname || username || '', displayName: actor?.displayName || nickname || username || '',
avatarUrl: actor?.avatarUrl,
content: comment.content || '', content: comment.content || '',
refCommentId: comment.refCommentId || '', refCommentId: comment.refCommentId || '',
refNickname: comment.refNickname || refActor?.displayName, refNickname: comment.refNickname || refActor?.displayName,
@@ -512,7 +493,6 @@ class SnsService {
refRemark: refActor?.remark, refRemark: refActor?.remark,
refNickName: refActor?.nickName, refNickName: refActor?.nickName,
refDisplayName: refActor?.displayName, refDisplayName: refActor?.displayName,
refAvatarUrl: refActor?.avatarUrl,
emojis: comment.emojis, emojis: comment.emojis,
source: commentSource source: commentSource
}) })
@@ -1041,8 +1021,7 @@ class SnsService {
const result = await wcdbService.getSnsTimeline(limit, offset, usernames, keyword, startTime, endTime) const result = await wcdbService.getSnsTimeline(limit, offset, usernames, keyword, startTime, endTime)
if (!result.success || !result.timeline || result.timeline.length === 0) return result if (!result.success || !result.timeline || result.timeline.length === 0) return result
const identityCache = new Map<string, Promise<SnsContactIdentity | null>>() const enrichedTimeline = result.timeline.map((post: any) => {
const enrichedTimeline = await Promise.all(result.timeline.map(async (post: any) => {
const contact = this.contactCache.get(post.username) const contact = this.contactCache.get(post.username)
const isVideoPost = post.type === 15 const isVideoPost = post.type === 15
const videoKey = extractVideoKey(post.rawXml || '') const videoKey = extractVideoKey(post.rawXml || '')
@@ -1082,22 +1061,14 @@ class SnsService {
finalComments = this.fixCommentRefs(dllComments) finalComments = this.fixCommentRefs(dllComments)
} }
const normalizedPost: SnsPost = {
...post,
comments: finalComments
}
const { likesDetail, commentsDetail } = await this.buildArkmeInteractionDetails(normalizedPost, identityCache)
return { return {
...post, ...post,
avatarUrl: contact?.avatarUrl, avatarUrl: contact?.avatarUrl,
nickname: post.nickname || contact?.displayName || post.username, nickname: post.nickname || contact?.displayName || post.username,
media: fixedMedia, media: fixedMedia,
comments: finalComments, comments: finalComments
likesDetail,
commentsDetail
} }
})) })
return { ...result, timeline: enrichedTimeline } return { ...result, timeline: enrichedTimeline }
} }

View File

@@ -623,22 +623,18 @@
.session-mutual-friends-row { .session-mutual-friends-row {
display: grid; display: grid;
grid-template-columns: 36px minmax(0, 1fr) 72px 108px; grid-template-columns: 36px minmax(120px, 0.82fr) max-content 56px 96px minmax(0, 1.28fr);
gap: 12px; gap: 10px;
align-items: center; align-items: center;
padding: 12px; padding: 8px 12px;
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 68%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--border-color) 68%, transparent);
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
min-height: 72px; min-height: 42px;
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
&.unconfirmed {
background: color-mix(in srgb, var(--bg-secondary) 60%, transparent);
}
} }
.session-mutual-friends-rank, .session-mutual-friends-rank,
@@ -652,32 +648,6 @@
text-align: center; text-align: center;
} }
.session-mutual-friends-user {
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
}
.session-mutual-friends-user-avatar {
flex-shrink: 0;
}
.session-mutual-friends-user-main {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.session-mutual-friends-user-head {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.session-mutual-friends-name { .session-mutual-friends-name {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@@ -717,30 +687,6 @@
} }
} }
.session-mutual-friends-identity-badge {
border-radius: 999px;
padding: 3px 8px;
font-size: 10px;
line-height: 1;
color: #92400e;
border: 1px solid color-mix(in srgb, #d97706 34%, var(--border-color));
background: color-mix(in srgb, #f59e0b 11%, var(--bg-secondary));
white-space: nowrap;
}
.session-mutual-friends-identity {
min-width: 0;
color: var(--text-secondary);
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.secondary {
color: var(--text-tertiary);
}
}
.session-mutual-friends-desc { .session-mutual-friends-desc {
min-width: 0; min-width: 0;
color: var(--text-tertiary); color: var(--text-tertiary);
@@ -2034,7 +1980,6 @@
height: var(--contacts-default-list-height); height: var(--contacts-default-list-height);
overflow: hidden; overflow: hidden;
padding: 0 0 12px; padding: 0 0 12px;
position: relative;
} }
.contacts-virtuoso { .contacts-virtuoso {
@@ -2052,43 +1997,6 @@
} }
} }
.contacts-action-rail {
position: absolute;
top: 0;
right: 0;
bottom: 12px;
width: max(var(--contacts-action-col-width), 184px);
z-index: 18;
pointer-events: none;
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -12px;
width: 12px;
pointer-events: none;
background: linear-gradient(to right, transparent, var(--bg-primary));
}
}
.contacts-action-rail-row {
position: absolute;
right: 0;
width: 100%;
height: var(--contacts-row-height);
padding: 0 0 4px;
box-sizing: border-box;
display: flex;
justify-content: flex-end;
pointer-events: none;
&.selected .contacts-action-rail-card {
background: rgba(var(--primary-rgb), 0.08);
}
}
.table-bottom-scrollbar { .table-bottom-scrollbar {
flex: 0 0 auto; flex: 0 0 auto;
overflow-x: auto; overflow-x: auto;
@@ -2183,6 +2091,10 @@
&.selected .contact-item { &.selected .contact-item {
background: rgba(var(--primary-rgb), 0.08); background: rgba(var(--primary-rgb), 0.08);
} }
&.selected .row-action-cell {
background: rgba(var(--primary-rgb), 0.08);
}
} }
.contact-item { .contact-item {
@@ -2562,30 +2474,26 @@
} }
} }
.row-action-cell, .row-action-cell {
.contacts-action-rail-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 4px; gap: 4px;
width: 100%; width: var(--contacts-action-col-width);
min-width: 0; min-width: var(--contacts-action-col-width);
height: calc(var(--contacts-row-height) - 4px); flex-shrink: 0;
padding: 0 6px 0 0; position: sticky;
box-sizing: border-box; right: 0;
justify-content: center; z-index: 10;
background: var(--bg-primary); background: var(--bg-primary);
pointer-events: auto;
position: relative;
z-index: 1;
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
top: -12px; top: -12px;
bottom: -12px; bottom: -12px;
left: -12px; left: -8px;
width: 12px; width: 8px;
pointer-events: none; pointer-events: none;
background: linear-gradient(to right, transparent, var(--bg-primary)); background: linear-gradient(to right, transparent, var(--bg-primary));
} }
@@ -4253,19 +4161,13 @@
} }
.session-mutual-friends-row { .session-mutual-friends-row {
grid-template-columns: 28px minmax(0, 1fr) 56px 72px; grid-template-columns: 30px minmax(88px, 0.9fr) max-content 44px 74px;
gap: 8px; gap: 8px;
font-size: 12px; font-size: 12px;
} }
.session-mutual-friends-user { .session-mutual-friends-desc {
align-items: flex-start; display: none;
gap: 10px;
}
.session-mutual-friends-user-avatar {
width: 36px !important;
height: 36px !important;
} }
.session-load-detail-row { .session-load-detail-row {

View File

@@ -44,12 +44,11 @@ import {
subscribeBackgroundTasks subscribeBackgroundTasks
} from '../services/backgroundTaskMonitor' } from '../services/backgroundTaskMonitor'
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore' import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
import { Avatar } from '../components/Avatar'
import { SnsPostItem } from '../components/Sns/SnsPostItem' import { SnsPostItem } from '../components/Sns/SnsPostItem'
import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog' import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog'
import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog' import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog'
import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm' import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm'
import type { SnsCommentDetail, SnsLikeDetail, SnsPost } from '../types/sns' import type { SnsPost } from '../types/sns'
import { import {
cloneExportDateRange, cloneExportDateRange,
createDefaultDateRange, createDefaultDateRange,
@@ -73,7 +72,6 @@ type DisplayNamePreference = 'group-nickname' | 'remark' | 'nickname'
type TextExportFormat = 'chatlab' | 'chatlab-jsonl' | 'json' | 'arkme-json' | 'html' | 'txt' | 'excel' | 'weclone' | 'sql' type TextExportFormat = 'chatlab' | 'chatlab-jsonl' | 'json' | 'arkme-json' | 'html' | 'txt' | 'excel' | 'weclone' | 'sql'
type SnsTimelineExportFormat = 'json' | 'html' | 'arkmejson' type SnsTimelineExportFormat = 'json' | 'html' | 'arkmejson'
const CONTACTS_ROW_HEIGHT = 76
interface ExportOptions { interface ExportOptions {
format: TextExportFormat format: TextExportFormat
@@ -515,12 +513,6 @@ const getAvatarLetter = (name: string): string => {
return [...name][0] || '?' return [...name][0] || '?'
} }
const toOptionalString = (value: unknown): string | undefined => {
if (typeof value !== 'string') return undefined
const trimmed = value.trim()
return trimmed ? trimmed : undefined
}
const toComparableNameSet = (values: Array<string | undefined | null>): Set<string> => { const toComparableNameSet = (values: Array<string | undefined | null>): Set<string> => {
const set = new Set<string>() const set = new Set<string>()
for (const value of values) { for (const value of values) {
@@ -610,15 +602,7 @@ type SessionMutualFriendDirection = 'incoming' | 'outgoing' | 'bidirectional'
type SessionMutualFriendBehavior = 'likes' | 'comments' | 'both' type SessionMutualFriendBehavior = 'likes' | 'comments' | 'both'
interface SessionMutualFriendItem { interface SessionMutualFriendItem {
key: string
identityKey?: string
name: string name: string
username?: string
wxid?: string
wechatId?: string
remark?: string
avatarUrl?: string
isConfirmed: boolean
incomingLikeCount: number incomingLikeCount: number
incomingCommentCount: number incomingCommentCount: number
outgoingLikeCount: number outgoingLikeCount: number
@@ -637,63 +621,6 @@ interface SessionMutualFriendsMetric {
computedAt: number computedAt: number
} }
const getSessionMutualFriendIdentityKey = (username?: string, wechatId?: string): string | undefined => {
const normalizedUsername = toOptionalString(username)
if (normalizedUsername) return `u:${normalizedUsername}`
const normalizedWechatId = toOptionalString(wechatId)
if (normalizedWechatId) return `w:${normalizedWechatId}`
return undefined
}
const getSessionMutualFriendFallbackKey = (name: string): string => `n:${name || '未知用户'}`
const resolveSessionMutualFriendName = (params: {
displayName?: string
remark?: string
nickName?: string
wechatId?: string
nickname?: string
username?: string
}): string => {
return (
toOptionalString(params.displayName) ||
toOptionalString(params.remark) ||
toOptionalString(params.nickName) ||
toOptionalString(params.wechatId) ||
toOptionalString(params.nickname) ||
toOptionalString(params.username) ||
'未知用户'
)
}
const applySessionMutualFriendDerivedState = (item: SessionMutualFriendItem): void => {
const incomingTotal = item.incomingLikeCount + item.incomingCommentCount
const outgoingTotal = item.outgoingLikeCount + item.outgoingCommentCount
item.direction = incomingTotal > 0 && outgoingTotal > 0
? 'bidirectional'
: incomingTotal > 0
? 'incoming'
: 'outgoing'
item.behavior = summarizeMutualFriendBehavior(
item.incomingLikeCount + item.outgoingLikeCount,
item.incomingCommentCount + item.outgoingCommentCount
)
}
const mergeSessionMutualFriendItemProfile = (
target: SessionMutualFriendItem,
profile: Partial<Pick<SessionMutualFriendItem, 'identityKey' | 'username' | 'wxid' | 'wechatId' | 'remark' | 'avatarUrl' | 'name' | 'isConfirmed'>>
): void => {
if (profile.identityKey && !target.identityKey) target.identityKey = profile.identityKey
if (profile.username && !target.username) target.username = profile.username
if (profile.wxid && !target.wxid) target.wxid = profile.wxid
if (profile.wechatId && !target.wechatId) target.wechatId = profile.wechatId
if (profile.remark && !target.remark) target.remark = profile.remark
if (profile.avatarUrl && !target.avatarUrl) target.avatarUrl = profile.avatarUrl
if (profile.name && (!target.name || !target.isConfirmed)) target.name = profile.name
if (profile.isConfirmed) target.isConfirmed = true
}
interface SessionSnsRankCacheEntry { interface SessionSnsRankCacheEntry {
likes: SessionSnsRankItem[] likes: SessionSnsRankItem[]
comments: SessionSnsRankItem[] comments: SessionSnsRankItem[]
@@ -751,121 +678,55 @@ const buildSessionMutualFriendsMetric = (
): SessionMutualFriendsMetric => { ): SessionMutualFriendsMetric => {
const friendMap = new Map<string, SessionMutualFriendItem>() const friendMap = new Map<string, SessionMutualFriendItem>()
const ensureItem = (seed: {
name: string
username?: string
wxid?: string
wechatId?: string
remark?: string
avatarUrl?: string
identityKey?: string
isConfirmed: boolean
}): SessionMutualFriendItem => {
const key = seed.identityKey || getSessionMutualFriendFallbackKey(seed.name)
const existing = friendMap.get(key)
if (existing) {
mergeSessionMutualFriendItemProfile(existing, seed)
return existing
}
const created: SessionMutualFriendItem = {
key,
identityKey: seed.identityKey,
name: seed.name,
username: seed.username,
wxid: seed.wxid,
wechatId: seed.wechatId,
remark: seed.remark,
avatarUrl: seed.avatarUrl,
isConfirmed: seed.isConfirmed,
incomingLikeCount: 0,
incomingCommentCount: 0,
outgoingLikeCount: 0,
outgoingCommentCount: 0,
totalCount: 0,
latestTime: 0,
direction: 'incoming',
behavior: 'likes'
}
friendMap.set(key, created)
return created
}
for (const post of posts) { for (const post of posts) {
const createTime = Number(post?.createTime) || 0 const createTime = Number(post?.createTime) || 0
const likesDetail = Array.isArray(post?.likesDetail) && post.likesDetail.length > 0 const likes = Array.isArray(post?.likes) ? post.likes : []
? post.likesDetail const comments = Array.isArray(post?.comments) ? post.comments : []
: (Array.isArray(post?.likes) ? post.likes : []).map((likeNameRaw): SnsLikeDetail => ({
nickname: String(likeNameRaw || '').trim() || '未知用户',
displayName: String(likeNameRaw || '').trim() || '未知用户',
source: 'legacy'
}))
const commentsDetail = Array.isArray(post?.commentsDetail) && post.commentsDetail.length > 0
? post.commentsDetail
: (Array.isArray(post?.comments) ? post.comments : []).map((comment): SnsCommentDetail => ({
id: String(comment?.id || ''),
nickname: String(comment?.nickname || '').trim() || '未知用户',
displayName: String(comment?.nickname || '').trim() || '未知用户',
content: String(comment?.content || ''),
refCommentId: String(comment?.refCommentId || ''),
refNickname: comment?.refNickname,
refUsername: comment?.refUsername,
emojis: comment?.emojis,
source: 'legacy'
}))
for (const like of likesDetail) { for (const likeNameRaw of likes) {
const username = toOptionalString(like.username || like.wxid) const name = String(likeNameRaw || '').trim() || '未知用户'
const wechatId = toOptionalString(like.wechatId || like.alias) const existing = friendMap.get(name)
const identityKey = getSessionMutualFriendIdentityKey(username, wechatId) if (existing) {
const existing = ensureItem({ existing.incomingLikeCount += 1
name: resolveSessionMutualFriendName({ existing.totalCount += 1
displayName: like.displayName, existing.behavior = existing.incomingCommentCount > 0 ? 'both' : 'likes'
remark: like.remark, if (createTime > existing.latestTime) existing.latestTime = createTime
nickName: like.nickName, continue
wechatId, }
nickname: like.nickname, friendMap.set(name, {
username name,
}), incomingLikeCount: 1,
username, incomingCommentCount: 0,
wxid: username, outgoingLikeCount: 0,
wechatId, outgoingCommentCount: 0,
remark: toOptionalString(like.remark), totalCount: 1,
avatarUrl: toOptionalString(like.avatarUrl), latestTime: createTime,
identityKey, direction: 'incoming',
isConfirmed: Boolean(identityKey && username) behavior: 'likes'
}) })
existing.incomingLikeCount += 1
existing.totalCount += 1
if (createTime > existing.latestTime) existing.latestTime = createTime
applySessionMutualFriendDerivedState(existing)
} }
for (const comment of commentsDetail) { for (const comment of comments) {
const username = toOptionalString(comment.username || comment.wxid) const name = String(comment?.nickname || '').trim() || '未知用户'
const wechatId = toOptionalString(comment.wechatId || comment.alias) const existing = friendMap.get(name)
const identityKey = getSessionMutualFriendIdentityKey(username, wechatId) if (existing) {
const existing = ensureItem({ existing.incomingCommentCount += 1
name: resolveSessionMutualFriendName({ existing.totalCount += 1
displayName: comment.displayName, existing.behavior = existing.incomingLikeCount > 0 ? 'both' : 'comments'
remark: comment.remark, if (createTime > existing.latestTime) existing.latestTime = createTime
nickName: comment.nickName, continue
wechatId, }
nickname: comment.nickname, friendMap.set(name, {
username name,
}), incomingLikeCount: 0,
username, incomingCommentCount: 1,
wxid: username, outgoingLikeCount: 0,
wechatId, outgoingCommentCount: 0,
remark: toOptionalString(comment.remark), totalCount: 1,
avatarUrl: toOptionalString(comment.avatarUrl), latestTime: createTime,
identityKey, direction: 'incoming',
isConfirmed: Boolean(identityKey && username) behavior: 'comments'
}) })
existing.incomingCommentCount += 1
existing.totalCount += 1
if (createTime > existing.latestTime) existing.latestTime = createTime
applySessionMutualFriendDerivedState(existing)
} }
} }
@@ -1652,8 +1513,6 @@ function ExportPage() {
const [nowTick, setNowTick] = useState(Date.now()) const [nowTick, setNowTick] = useState(Date.now())
const [isContactsListAtTop, setIsContactsListAtTop] = useState(true) const [isContactsListAtTop, setIsContactsListAtTop] = useState(true)
const [isContactsHeaderDragging, setIsContactsHeaderDragging] = useState(false) const [isContactsHeaderDragging, setIsContactsHeaderDragging] = useState(false)
const [contactsListScrollTop, setContactsListScrollTop] = useState(0)
const [contactsVisibleRange, setContactsVisibleRange] = useState({ startIndex: 0, endIndex: -1 })
const [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({ const [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({
viewportWidth: 0, viewportWidth: 0,
contentWidth: 0 contentWidth: 0
@@ -2929,27 +2788,21 @@ function ExportPage() {
const getSessionMutualFriendProfile = useCallback((sessionId: string): { const getSessionMutualFriendProfile = useCallback((sessionId: string): {
displayName: string displayName: string
remark?: string candidateNames: Set<string>
wechatId?: string
avatarUrl?: string
primaryIdentityKey: string
candidateIdentityKeys: Set<string>
} => { } => {
const normalizedSessionId = String(sessionId || '').trim() const normalizedSessionId = String(sessionId || '').trim()
const contact = contactsList.find(item => item.username === normalizedSessionId) const contact = contactsList.find(item => item.username === normalizedSessionId)
const session = sessionsRef.current.find(item => item.username === normalizedSessionId) const session = sessionsRef.current.find(item => item.username === normalizedSessionId)
const displayName = contact?.displayName || contact?.remark || contact?.nickname || session?.displayName || normalizedSessionId const displayName = contact?.displayName || contact?.remark || contact?.nickname || session?.displayName || normalizedSessionId
const wechatId = toOptionalString(contact?.alias)
const candidateIdentityKeys = new Set<string>()
candidateIdentityKeys.add(`u:${normalizedSessionId}`)
if (wechatId) candidateIdentityKeys.add(`w:${wechatId}`)
return { return {
displayName, displayName,
remark: toOptionalString(contact?.remark), candidateNames: toComparableNameSet([
wechatId, displayName,
avatarUrl: toOptionalString(contact?.avatarUrl), contact?.displayName,
primaryIdentityKey: `u:${normalizedSessionId}`, contact?.remark,
candidateIdentityKeys contact?.nickname,
contact?.alias
])
} }
}, [contactsList]) }, [contactsList])
@@ -2961,54 +2814,41 @@ function ExportPage() {
const directMetric = directMetrics[normalizedTargetSessionId] const directMetric = directMetrics[normalizedTargetSessionId]
if (!directMetric) return null if (!directMetric) return null
const targetProfile = getSessionMutualFriendProfile(normalizedTargetSessionId) const { candidateNames } = getSessionMutualFriendProfile(normalizedTargetSessionId)
const mergedMap = new Map<string, SessionMutualFriendItem>() const mergedMap = new Map<string, SessionMutualFriendItem>()
for (const item of directMetric.items) { for (const item of directMetric.items) {
mergedMap.set(item.key, { ...item }) mergedMap.set(item.name, { ...item })
} }
for (const [sourceSessionId, sourceMetric] of Object.entries(directMetrics)) { for (const [sourceSessionId, sourceMetric] of Object.entries(directMetrics)) {
if (!sourceMetric || sourceSessionId === normalizedTargetSessionId) continue if (!sourceMetric || sourceSessionId === normalizedTargetSessionId) continue
const sourceProfile = getSessionMutualFriendProfile(sourceSessionId) const sourceProfile = getSessionMutualFriendProfile(sourceSessionId)
if (!sourceProfile.displayName) continue if (!sourceProfile.displayName) continue
const reverseMatches = sourceMetric.items.filter(item => { if (mergedMap.has(sourceProfile.displayName)) continue
if (!item.identityKey) return false
return targetProfile.candidateIdentityKeys.has(item.identityKey) const reverseMatches = sourceMetric.items.filter(item => candidateNames.has(item.name))
})
if (reverseMatches.length === 0) continue if (reverseMatches.length === 0) continue
const reverseCount = reverseMatches.reduce((sum, item) => sum + item.totalCount, 0) const reverseCount = reverseMatches.reduce((sum, item) => sum + item.totalCount, 0)
const reverseLikeCount = reverseMatches.reduce((sum, item) => sum + item.incomingLikeCount, 0) const reverseLikeCount = reverseMatches.reduce((sum, item) => sum + item.incomingLikeCount, 0)
const reverseCommentCount = reverseMatches.reduce((sum, item) => sum + item.incomingCommentCount, 0) const reverseCommentCount = reverseMatches.reduce((sum, item) => sum + item.incomingCommentCount, 0)
const reverseLatestTime = reverseMatches.reduce((latest, item) => Math.max(latest, item.latestTime), 0) const reverseLatestTime = reverseMatches.reduce((latest, item) => Math.max(latest, item.latestTime), 0)
const existing = mergedMap.get(sourceProfile.primaryIdentityKey) const existing = mergedMap.get(sourceProfile.displayName)
if (existing) { if (existing) {
mergeSessionMutualFriendItemProfile(existing, {
identityKey: sourceProfile.primaryIdentityKey,
username: sourceSessionId,
wxid: sourceSessionId,
wechatId: sourceProfile.wechatId,
remark: sourceProfile.remark,
avatarUrl: sourceProfile.avatarUrl,
name: sourceProfile.displayName,
isConfirmed: true
})
existing.outgoingLikeCount += reverseLikeCount existing.outgoingLikeCount += reverseLikeCount
existing.outgoingCommentCount += reverseCommentCount existing.outgoingCommentCount += reverseCommentCount
existing.totalCount += reverseCount existing.totalCount += reverseCount
existing.latestTime = Math.max(existing.latestTime, reverseLatestTime) existing.latestTime = Math.max(existing.latestTime, reverseLatestTime)
applySessionMutualFriendDerivedState(existing) existing.direction = (existing.incomingLikeCount + existing.incomingCommentCount) > 0
? 'bidirectional'
: 'outgoing'
existing.behavior = summarizeMutualFriendBehavior(
existing.incomingLikeCount + existing.outgoingLikeCount,
existing.incomingCommentCount + existing.outgoingCommentCount
)
} else { } else {
const created: SessionMutualFriendItem = { mergedMap.set(sourceProfile.displayName, {
key: sourceProfile.primaryIdentityKey,
identityKey: sourceProfile.primaryIdentityKey,
name: sourceProfile.displayName, name: sourceProfile.displayName,
username: sourceSessionId,
wxid: sourceSessionId,
wechatId: sourceProfile.wechatId,
remark: sourceProfile.remark,
avatarUrl: sourceProfile.avatarUrl,
isConfirmed: true,
incomingLikeCount: 0, incomingLikeCount: 0,
incomingCommentCount: 0, incomingCommentCount: 0,
outgoingLikeCount: reverseLikeCount, outgoingLikeCount: reverseLikeCount,
@@ -3017,8 +2857,7 @@ function ExportPage() {
latestTime: reverseLatestTime, latestTime: reverseLatestTime,
direction: 'outgoing', direction: 'outgoing',
behavior: summarizeMutualFriendBehavior(reverseLikeCount, reverseCommentCount) behavior: summarizeMutualFriendBehavior(reverseLikeCount, reverseCommentCount)
} })
mergedMap.set(created.key, created)
} }
} }
@@ -3048,7 +2887,7 @@ function ExportPage() {
for (const targetSessionId of allSessionIds) { for (const targetSessionId of allSessionIds) {
if (targetSessionId === normalizedSessionId) continue if (targetSessionId === normalizedSessionId) continue
const targetProfile = getSessionMutualFriendProfile(targetSessionId) const targetProfile = getSessionMutualFriendProfile(targetSessionId)
if (directMetric.items.some(item => item.identityKey && targetProfile.candidateIdentityKeys.has(item.identityKey))) { if (directMetric.items.some(item => targetProfile.candidateNames.has(item.name))) {
impactedSessionIds.add(targetSessionId) impactedSessionIds.add(targetSessionId)
} }
} }
@@ -4995,11 +4834,6 @@ function ExportPage() {
const endIndex = Number.isFinite(range?.endIndex) ? Math.max(startIndex, Math.floor(range.endIndex)) : startIndex const endIndex = Number.isFinite(range?.endIndex) ? Math.max(startIndex, Math.floor(range.endIndex)) : startIndex
sessionMediaMetricVisibleRangeRef.current = { startIndex, endIndex } sessionMediaMetricVisibleRangeRef.current = { startIndex, endIndex }
sessionMutualFriendsVisibleRangeRef.current = { startIndex, endIndex } sessionMutualFriendsVisibleRangeRef.current = { startIndex, endIndex }
setContactsVisibleRange(prev => (
prev.startIndex === startIndex && prev.endIndex === endIndex
? prev
: { startIndex, endIndex }
))
if (isLoadingSessionCountsRef.current || !isSessionCountStageReady) return if (isLoadingSessionCountsRef.current || !isSessionCountStageReady) return
const visibleTargets = collectVisibleSessionMetricTargets(filteredContacts) const visibleTargets = collectVisibleSessionMetricTargets(filteredContacts)
if (visibleTargets.length === 0) return if (visibleTargets.length === 0) return
@@ -5203,18 +5037,7 @@ function ExportPage() {
const items = sessionMutualFriendsDialogMetric?.items || [] const items = sessionMutualFriendsDialogMetric?.items || []
const keyword = sessionMutualFriendsSearch.trim().toLowerCase() const keyword = sessionMutualFriendsSearch.trim().toLowerCase()
if (!keyword) return items if (!keyword) return items
return items.filter((item) => { return items.filter(item => item.name.toLowerCase().includes(keyword))
const haystack = [
item.name,
item.remark,
item.wechatId,
item.wxid,
item.username
]
.map(value => String(value || '').toLowerCase())
.join(' ')
return haystack.includes(keyword)
})
}, [sessionMutualFriendsDialogMetric, sessionMutualFriendsSearch]) }, [sessionMutualFriendsDialogMetric, sessionMutualFriendsSearch])
const applySessionDetailStats = useCallback(( const applySessionDetailStats = useCallback((
@@ -5795,7 +5618,7 @@ function ExportPage() {
const taskCenterAlertCount = taskRunningCount + taskQueuedCount const taskCenterAlertCount = taskRunningCount + taskQueuedCount
const hasFilteredContacts = filteredContacts.length > 0 const hasFilteredContacts = filteredContacts.length > 0
const contactsTableMinWidth = useMemo(() => { const contactsTableMinWidth = useMemo(() => {
const baseWidth = 24 + 34 + 44 + 160 + 120 + (4 * 72) + (7 * 12) const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + 140 + (8 * 12)
const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0 const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0
const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0 const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0
return baseWidth + snsWidth + mutualFriendsWidth return baseWidth + snsWidth + mutualFriendsWidth
@@ -5991,6 +5814,11 @@ function ExportPage() {
const canExport = Boolean(matchedSession?.hasSession) const canExport = Boolean(matchedSession?.hasSession)
const isSessionBindingPending = !matchedSession && (isLoading || isSessionEnriching) const isSessionBindingPending = !matchedSession && (isLoading || isSessionEnriching)
const checked = canExport && selectedSessions.has(contact.username) const checked = canExport && selectedSessions.has(contact.username)
const isRunning = canExport && runningSessionIds.has(contact.username)
const isQueued = canExport && queuedSessionIds.has(contact.username)
const recentExportTimestamp = lastExportBySession[contact.username]
const hasRecentExport = canExport && Boolean(recentExportTimestamp)
const recentExportTime = hasRecentExport ? formatRecentExportTime(recentExportTimestamp, nowTick) : ''
const countedMessages = normalizeMessageCount(sessionMessageCounts[contact.username]) const countedMessages = normalizeMessageCount(sessionMessageCounts[contact.username])
const hintedMessages = normalizeMessageCount(matchedSession?.messageCountHint) const hintedMessages = normalizeMessageCount(matchedSession?.messageCountHint)
const displayedMessageCount = countedMessages ?? hintedMessages const displayedMessageCount = countedMessages ?? hintedMessages
@@ -6167,14 +5995,47 @@ function ExportPage() {
)} )}
</div> </div>
)} )}
<div className="row-action-cell">
<div className={`row-action-main ${hasRecentExport ? '' : 'single-line'}`.trim()}>
<div className={`row-export-action-stack ${hasRecentExport ? '' : 'single-line'}`.trim()}>
<button
type="button"
className={`row-export-link ${isRunning ? 'state-running' : ''} ${!canExport ? 'state-disabled' : ''}`}
disabled={!canExport || isRunning}
onClick={() => {
if (!matchedSession || !matchedSession.hasSession) return
openSingleExport({
...matchedSession,
displayName: contact.displayName || matchedSession.displayName || matchedSession.username
})
}}
>
{!canExport ? '暂无会话' : isRunning ? '导出中...' : isQueued ? '排队中' : '单会话导出'}
</button>
{hasRecentExport && <span className="row-export-time">{recentExportTime}</span>}
</div>
<button
className={`row-detail-btn ${showSessionDetailPanel && sessionDetail?.wxid === contact.username ? 'active' : ''}`}
onClick={() => openSessionDetail(contact.username)}
>
</button>
</div>
</div>
</div> </div>
</div> </div>
) )
}, [ }, [
lastExportBySession,
nowTick,
openContactSnsTimeline, openContactSnsTimeline,
openSessionDetail, openSessionDetail,
openSessionMutualFriendsDialog, openSessionMutualFriendsDialog,
openSingleExport,
queuedSessionIds,
runningSessionIds,
selectedSessions, selectedSessions,
sessionDetail?.wxid,
sessionContentMetrics, sessionContentMetrics,
sessionMutualFriendsMetrics, sessionMutualFriendsMetrics,
sessionLoadTraceMap, sessionLoadTraceMap,
@@ -6189,77 +6050,6 @@ function ExportPage() {
snsUserPostCountsStatus, snsUserPostCountsStatus,
toggleSelectSession toggleSelectSession
]) ])
const visibleContactsForActionRail = useMemo(() => {
if (!hasFilteredContacts || contactsVisibleRange.endIndex < contactsVisibleRange.startIndex || contactsVisibleRange.endIndex < 0) return []
const startIndex = Math.max(0, Math.min(filteredContacts.length - 1, contactsVisibleRange.startIndex))
const endIndex = Math.max(startIndex, Math.min(filteredContacts.length - 1, contactsVisibleRange.endIndex))
if (!Number.isFinite(startIndex) || !Number.isFinite(endIndex) || endIndex < startIndex) return []
return filteredContacts.slice(startIndex, endIndex + 1).map((contact, offset) => {
const index = startIndex + offset
return {
contact,
index,
top: (index * CONTACTS_ROW_HEIGHT) - contactsListScrollTop
}
})
}, [contactsListScrollTop, contactsVisibleRange.endIndex, contactsVisibleRange.startIndex, filteredContacts, hasFilteredContacts])
const renderContactActionRailItem = useCallback((contact: ContactInfo, index: number, top: number) => {
const matchedSession = sessionRowByUsername.get(contact.username)
const canExport = Boolean(matchedSession?.hasSession)
const checked = canExport && selectedSessions.has(contact.username)
const isRunning = canExport && runningSessionIds.has(contact.username)
const isQueued = canExport && queuedSessionIds.has(contact.username)
const recentExportTimestamp = lastExportBySession[contact.username]
const hasRecentExport = canExport && Boolean(recentExportTimestamp)
const recentExportTime = hasRecentExport ? formatRecentExportTime(recentExportTimestamp, nowTick) : ''
return (
<div
key={contact.username}
className={`contacts-action-rail-row ${checked ? 'selected' : ''}`}
style={{ top: `${top}px` }}
data-index={index}
>
<div className="contacts-action-rail-card">
<div className={`row-action-main ${hasRecentExport ? '' : 'single-line'}`.trim()}>
<div className={`row-export-action-stack ${hasRecentExport ? '' : 'single-line'}`.trim()}>
<button
type="button"
className={`row-export-link ${isRunning ? 'state-running' : ''} ${!canExport ? 'state-disabled' : ''}`}
disabled={!canExport || isRunning}
onClick={() => {
if (!matchedSession || !matchedSession.hasSession) return
openSingleExport({
...matchedSession,
displayName: contact.displayName || matchedSession.displayName || matchedSession.username
})
}}
>
{!canExport ? '暂无会话' : isRunning ? '导出中...' : isQueued ? '排队中' : '单会话导出'}
</button>
{hasRecentExport && <span className="row-export-time">{recentExportTime}</span>}
</div>
<button
className={`row-detail-btn ${showSessionDetailPanel && sessionDetail?.wxid === contact.username ? 'active' : ''}`}
onClick={() => openSessionDetail(contact.username)}
>
</button>
</div>
</div>
</div>
)
}, [
lastExportBySession,
nowTick,
openSessionDetail,
openSingleExport,
queuedSessionIds,
runningSessionIds,
selectedSessions,
sessionDetail?.wxid,
sessionRowByUsername
])
const handleContactsListWheelCapture = useCallback((event: WheelEvent<HTMLDivElement>) => { const handleContactsListWheelCapture = useCallback((event: WheelEvent<HTMLDivElement>) => {
const deltaY = event.deltaY const deltaY = event.deltaY
if (!deltaY) return if (!deltaY) return
@@ -6277,19 +6067,9 @@ function ExportPage() {
window.scrollBy({ top: deltaY, behavior: 'auto' }) window.scrollBy({ top: deltaY, behavior: 'auto' })
} }
}, [isContactsListAtTop]) }, [isContactsListAtTop])
const handleContactsListScrollCapture = useCallback((event: UIEvent<HTMLDivElement>) => {
const target = event.target
if (!(target instanceof HTMLDivElement)) return
const nextScrollTop = Math.max(0, target.scrollTop)
setContactsListScrollTop(prev => (
Math.abs(prev - nextScrollTop) > 1 ? nextScrollTop : prev
))
}, [])
useEffect(() => { useEffect(() => {
if (hasFilteredContacts) return if (hasFilteredContacts) return
setIsContactsListAtTop(true) setIsContactsListAtTop(true)
setContactsListScrollTop(0)
setContactsVisibleRange({ startIndex: 0, endIndex: -1 })
}, [hasFilteredContacts]) }, [hasFilteredContacts])
const chooseExportFolder = useCallback(async () => { const chooseExportFolder = useCallback(async () => {
const result = await window.electronAPI.dialog.openFile({ const result = await window.electronAPI.dialog.openFile({
@@ -6701,24 +6481,18 @@ function ExportPage() {
<div <div
className="contacts-list" className="contacts-list"
onWheelCapture={handleContactsListWheelCapture} onWheelCapture={handleContactsListWheelCapture}
onScrollCapture={handleContactsListScrollCapture}
> >
<Virtuoso <Virtuoso
ref={contactsVirtuosoRef} ref={contactsVirtuosoRef}
className="contacts-virtuoso" className="contacts-virtuoso"
data={filteredContacts} data={filteredContacts}
computeItemKey={(_, contact) => contact.username} computeItemKey={(_, contact) => contact.username}
fixedItemHeight={CONTACTS_ROW_HEIGHT} fixedItemHeight={76}
itemContent={renderContactRow} itemContent={renderContactRow}
rangeChanged={handleContactsRangeChanged} rangeChanged={handleContactsRangeChanged}
atTopStateChange={setIsContactsListAtTop} atTopStateChange={setIsContactsListAtTop}
overscan={420} overscan={420}
/> />
<div className="contacts-action-rail">
{visibleContactsForActionRail.map(({ contact, index, top }) => (
renderContactActionRailItem(contact, index, top)
))}
</div>
</div> </div>
)} )}
</div> </div>
@@ -7016,15 +6790,15 @@ function ExportPage() {
</div> </div>
<div className="session-mutual-friends-tip"> <div className="session-mutual-friends-tip">
ta
</div> </div>
<div className="session-mutual-friends-toolbar"> <div className="session-mutual-friends-toolbar">
<input <input
value={sessionMutualFriendsSearch} value={sessionMutualFriendsSearch}
onChange={(event) => setSessionMutualFriendsSearch(event.target.value)} onChange={(event) => setSessionMutualFriendsSearch(event.target.value)}
placeholder="搜索共同好友(备注 / 微信号 / wxid" placeholder="搜索共同好友"
aria-label="搜索共同好友(备注 / 微信号 / wxid" aria-label="搜索共同好友"
/> />
</div> </div>
@@ -7036,48 +6810,20 @@ function ExportPage() {
) : ( ) : (
<div className="session-mutual-friends-list"> <div className="session-mutual-friends-list">
{filteredSessionMutualFriendsDialogItems.map((item, index) => ( {filteredSessionMutualFriendsDialogItems.map((item, index) => (
<div className={`session-mutual-friends-row ${item.isConfirmed ? 'confirmed' : 'unconfirmed'}`} key={`${sessionMutualFriendsDialogTarget.username}-${item.key}`}> <div className="session-mutual-friends-row" key={`${sessionMutualFriendsDialogTarget.username}-${item.name}`}>
<span className="session-mutual-friends-rank">{index + 1}</span> <span className="session-mutual-friends-rank">{index + 1}</span>
<div className="session-mutual-friends-user"> <span className="session-mutual-friends-name" title={item.name}>{item.name}</span>
<Avatar <span className={`session-mutual-friends-source ${item.direction}`}>
src={item.avatarUrl} {getSessionMutualFriendDirectionLabel(item.direction)}
name={item.name} </span>
size={40}
shape="rounded"
className="session-mutual-friends-user-avatar"
/>
<div className="session-mutual-friends-user-main">
<div className="session-mutual-friends-user-head">
<span className="session-mutual-friends-name" title={item.name}>{item.name}</span>
<span className={`session-mutual-friends-source ${item.direction}`}>
{getSessionMutualFriendDirectionLabel(item.direction)}
</span>
{!item.isConfirmed && (
<span className="session-mutual-friends-identity-badge"></span>
)}
</div>
<div className="session-mutual-friends-identity" title={item.isConfirmed ? (item.wechatId ? `微信号: ${item.wechatId}` : '微信号: 未设置') : '仅从旧数据中解析到名字'}>
{item.isConfirmed ? (
item.wechatId ? `微信号: ${item.wechatId}` : '微信号: 未设置'
) : (
'仅从旧数据中解析到名字'
)}
</div>
<div className="session-mutual-friends-identity secondary" title={item.isConfirmed ? `wxid: ${item.wxid || item.username || ''}` : '没有可用的 wxid未做自动匹配'}>
{item.isConfirmed
? `wxid: ${item.wxid || item.username || ''}`
: '没有可用的 wxid未做自动匹配'}
</div>
<span
className="session-mutual-friends-desc"
title={describeSessionMutualFriendRelation(item, sessionMutualFriendsDialogTarget.displayName)}
>
{describeSessionMutualFriendRelation(item, sessionMutualFriendsDialogTarget.displayName)}
</span>
</div>
</div>
<span className="session-mutual-friends-count">{item.totalCount.toLocaleString('zh-CN')}</span> <span className="session-mutual-friends-count">{item.totalCount.toLocaleString('zh-CN')}</span>
<span className="session-mutual-friends-latest">{formatYmdDateFromSeconds(item.latestTime)}</span> <span className="session-mutual-friends-latest">{formatYmdDateFromSeconds(item.latestTime)}</span>
<span
className="session-mutual-friends-desc"
title={describeSessionMutualFriendRelation(item, sessionMutualFriendsDialogTarget.displayName)}
>
{describeSessionMutualFriendRelation(item, sessionMutualFriendsDialogTarget.displayName)}
</span>
</div> </div>
))} ))}
</div> </div>

View File

@@ -767,53 +767,7 @@ export interface ElectronAPI {
} }
}> }>
likes: Array<string> likes: Array<string>
comments: Array<{ comments: Array<{ id: string; nickname: string; content: string; refCommentId: string; refNickname?: string; emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }> }>
id: string
nickname: string
username?: string
content: string
refCommentId: string
refNickname?: string
refUsername?: string
emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }>
}>
likesDetail?: Array<{
nickname: string
username?: string
wxid?: string
alias?: string
wechatId?: string
remark?: string
nickName?: string
displayName: string
avatarUrl?: string
source: 'xml' | 'legacy'
}>
commentsDetail?: Array<{
id: string
nickname: string
username?: string
wxid?: string
alias?: string
wechatId?: string
remark?: string
nickName?: string
displayName: string
avatarUrl?: string
content: string
refCommentId: string
refNickname?: string
refUsername?: string
refWxid?: string
refAlias?: string
refWechatId?: string
refRemark?: string
refNickName?: string
refDisplayName?: string
refAvatarUrl?: string
emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }>
source: 'xml' | 'legacy'
}>
rawXml?: string rawXml?: string
}> }>
error?: string error?: string

View File

@@ -28,53 +28,12 @@ export interface SnsCommentEmoji {
export interface SnsComment { export interface SnsComment {
id: string id: string
nickname: string nickname: string
username?: string
content: string content: string
refCommentId: string refCommentId: string
refNickname?: string refNickname?: string
refUsername?: string
emojis?: SnsCommentEmoji[] emojis?: SnsCommentEmoji[]
} }
export interface SnsLikeDetail {
nickname: string
username?: string
wxid?: string
alias?: string
wechatId?: string
remark?: string
nickName?: string
displayName: string
avatarUrl?: string
source: 'xml' | 'legacy'
}
export interface SnsCommentDetail {
id: string
nickname: string
username?: string
wxid?: string
alias?: string
wechatId?: string
remark?: string
nickName?: string
displayName: string
avatarUrl?: string
content: string
refCommentId: string
refNickname?: string
refUsername?: string
refWxid?: string
refAlias?: string
refWechatId?: string
refRemark?: string
refNickName?: string
refDisplayName?: string
refAvatarUrl?: string
emojis?: SnsCommentEmoji[]
source: 'xml' | 'legacy'
}
export interface SnsPost { export interface SnsPost {
id: string id: string
tid?: string // 数据库主键(雪花 ID用于精确删除 tid?: string // 数据库主键(雪花 ID用于精确删除
@@ -87,8 +46,6 @@ export interface SnsPost {
media: SnsMedia[] media: SnsMedia[]
likes: string[] likes: string[]
comments: SnsComment[] comments: SnsComment[]
likesDetail?: SnsLikeDetail[]
commentsDetail?: SnsCommentDetail[]
rawXml?: string rawXml?: string
linkTitle?: string linkTitle?: string
linkUrl?: string linkUrl?: string