mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-06 07:26:48 +00:00
This commit is contained in:
@@ -2530,7 +2530,7 @@ class ChatService {
|
||||
const rawRows = result.messages as Record<string, any>[]
|
||||
const hasMore = rawRows.length > pageLimit
|
||||
const selectedRows = hasMore ? rawRows.slice(0, pageLimit) : rawRows
|
||||
const mapped = this.mapRowsToMessages(selectedRows)
|
||||
const mapped = this.mapRowsToMessages(selectedRows, sessionId)
|
||||
const visible = mapped.filter((msg) => this.isMessageVisibleForSession(sessionId, msg))
|
||||
const outputMessages = (visible.length === 0 && mapped.length > 0)
|
||||
? mapped
|
||||
@@ -2541,6 +2541,7 @@ class ChatService {
|
||||
const normalized = this.normalizeMessageOrder(outputMessages)
|
||||
if (normalized.length > 0) {
|
||||
await this.repairEmojiMessages(normalized)
|
||||
await this.resolveQuotedMessages(normalized, sessionId)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -2597,7 +2598,7 @@ class ChatService {
|
||||
}
|
||||
|
||||
// 转换为 Message 对象
|
||||
const messages = this.mapRowsToMessages(res.messages as Record<string, any>[])
|
||||
const messages = this.mapRowsToMessages(res.messages as Record<string, any>[], sessionId)
|
||||
const normalized = this.normalizeMessageOrder(messages)
|
||||
|
||||
// 并发检查并修复缺失 CDN URL 的表情包
|
||||
@@ -2834,7 +2835,7 @@ class ChatService {
|
||||
|
||||
const rowsToProcess = queuedRows
|
||||
queuedRows = []
|
||||
const mappedMessages = this.mapRowsToMessages(rowsToProcess)
|
||||
const mappedMessages = this.mapRowsToMessages(rowsToProcess, sessionId)
|
||||
for (let index = 0; index < mappedMessages.length; index += 1) {
|
||||
const msg = mappedMessages[index]
|
||||
rawRowsConsumed += 1
|
||||
@@ -4825,8 +4826,8 @@ class ChatService {
|
||||
/**
|
||||
* HTTP API 复用消息解析逻辑,确保和应用内展示一致。
|
||||
*/
|
||||
mapRowsToMessagesForApi(rows: Record<string, any>[]): Message[] {
|
||||
return this.mapRowsToMessages(rows)
|
||||
mapRowsToMessagesForApi(rows: Record<string, any>[], sessionId: string): Message[] {
|
||||
return this.mapRowsToMessages(rows, sessionId)
|
||||
}
|
||||
|
||||
mapRowsToMessagesLiteForApi(rows: Record<string, any>[]): Message[] {
|
||||
@@ -4880,7 +4881,7 @@ class ChatService {
|
||||
return messages
|
||||
}
|
||||
|
||||
private mapRowsToMessages(rows: Record<string, any>[]): Message[] {
|
||||
private mapRowsToMessages(rows: Record<string, any>[], sessionId: string): Message[] {
|
||||
const myWxid = this.configService.get('myWxid')
|
||||
|
||||
const messages: Message[] = []
|
||||
@@ -5000,11 +5001,23 @@ class ChatService {
|
||||
encrypVer = imageInfo.encrypVer
|
||||
cdnThumbUrl = imageInfo.cdnThumbUrl
|
||||
imageDatName = this.parseImageDatNameFromRow(row)
|
||||
// 解析图片消息中的引用信息
|
||||
const quoteInfo = this.parseMediaQuoteMessage(content, sessionId)
|
||||
if (quoteInfo.content) quotedContent = quoteInfo.content
|
||||
if (quoteInfo.sender) quotedSender = quoteInfo.sender
|
||||
} else if (localType === 43) {
|
||||
// 视频消息:优先从 packed_info_data 提取真实文件名(32位十六进制),再回退 XML
|
||||
videoMd5 = this.parseVideoFileNameFromRow(row, content)
|
||||
// 解析视频消息中的引用信息
|
||||
const quoteInfo = this.parseMediaQuoteMessage(content, sessionId)
|
||||
if (quoteInfo.content) quotedContent = quoteInfo.content
|
||||
if (quoteInfo.sender) quotedSender = quoteInfo.sender
|
||||
} else if (localType === 34 && content) {
|
||||
voiceDurationSeconds = this.parseVoiceDurationSeconds(content)
|
||||
// 解析语音消息中的引用信息
|
||||
const quoteInfo = this.parseMediaQuoteMessage(content, sessionId)
|
||||
if (quoteInfo.content) quotedContent = quoteInfo.content
|
||||
if (quoteInfo.sender) quotedSender = quoteInfo.sender
|
||||
} else if (localType === 42 && content) {
|
||||
// 名片消息
|
||||
const cardInfo = this.parseCardInfo(content)
|
||||
@@ -5760,6 +5773,116 @@ class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析媒体消息(图片/视频/语音)中的引用信息
|
||||
* 这些消息的引用信息在 <extcommoninfo><refermsg> 中
|
||||
*/
|
||||
private parseMediaQuoteMessage(content: string, sessionId: string): { content?: string; sender?: string } {
|
||||
try {
|
||||
const normalizedContent = this.decodeHtmlEntities(content || '')
|
||||
const referMsgStart = normalizedContent.indexOf('<refermsg>')
|
||||
const referMsgEnd = normalizedContent.indexOf('</refermsg>')
|
||||
|
||||
if (referMsgStart === -1 || referMsgEnd === -1) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const referMsgXml = normalizedContent.substring(referMsgStart, referMsgEnd + 11)
|
||||
const svrid = this.extractXmlValue(referMsgXml, 'svrid')
|
||||
|
||||
console.log('[DEBUG] parseMediaQuoteMessage - svrid:', svrid)
|
||||
|
||||
if (!svrid) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// 简化方案:返回 svrid 标记
|
||||
console.log('[DEBUG] parseMediaQuoteMessage - 返回标记:', `__SVRID__${svrid}__`)
|
||||
return { content: `__SVRID__${svrid}__` }
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
async resolveQuotedMessages(messages: Message[], sessionId: string): Promise<void> {
|
||||
console.log('[DEBUG] resolveQuotedMessages - 开始解析,消息数量:', messages.length)
|
||||
const svridsToResolve: Array<{ msg: Message; svrid: string }> = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.quotedContent && msg.quotedContent.startsWith('__SVRID__')) {
|
||||
const match = msg.quotedContent.match(/__SVRID__(.+?)__/)
|
||||
if (match) {
|
||||
console.log('[DEBUG] resolveQuotedMessages - 找到需要解析的svrid:', match[1])
|
||||
svridsToResolve.push({ msg, svrid: match[1] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[DEBUG] resolveQuotedMessages - 需要解析的数量:', svridsToResolve.length)
|
||||
|
||||
if (svridsToResolve.length === 0) return
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
svridsToResolve.map(({ svrid }) => {
|
||||
console.log('[DEBUG] resolveQuotedMessages - 查询svrid:', svrid, 'sessionId:', sessionId)
|
||||
return wcdbService.getMessageByServerId(sessionId, svrid)
|
||||
})
|
||||
)
|
||||
|
||||
console.log('[DEBUG] resolveQuotedMessages - 查询结果数量:', results.length)
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i]
|
||||
const { msg, svrid } = svridsToResolve[i]
|
||||
|
||||
console.log('[DEBUG] resolveQuotedMessages - 处理结果', i, ':', {
|
||||
status: result.status,
|
||||
success: result.status === 'fulfilled' ? result.value.success : false,
|
||||
hasRow: result.status === 'fulfilled' && result.value.row ? true : false,
|
||||
error: result.status === 'fulfilled' ? result.value.error : undefined,
|
||||
svrid
|
||||
})
|
||||
|
||||
if (result.status === 'fulfilled' && result.value.success && result.value.row) {
|
||||
const localType = parseInt(result.value.row.local_type || '0', 10)
|
||||
const rawMessageContent = result.value.row.message_content
|
||||
const rawCompressContent = result.value.row.compress_content
|
||||
|
||||
console.log('[DEBUG] resolveQuotedMessages - 原始数据:', {
|
||||
hasMessageContent: !!rawMessageContent,
|
||||
hasCompressContent: !!rawCompressContent,
|
||||
messageContentType: typeof rawMessageContent,
|
||||
messageContentLength: rawMessageContent ? rawMessageContent.length : 0
|
||||
})
|
||||
|
||||
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent)
|
||||
|
||||
console.log('[DEBUG] resolveQuotedMessages - 解码后:', { localType, contentLength: content.length, contentPreview: content.substring(0, 50) })
|
||||
|
||||
if (localType === 1) {
|
||||
msg.quotedContent = this.sanitizeQuotedContent(content)
|
||||
} else if (localType === 3) {
|
||||
msg.quotedContent = '[图片]'
|
||||
} else if (localType === 34) {
|
||||
msg.quotedContent = '[语音]'
|
||||
} else if (localType === 43) {
|
||||
msg.quotedContent = '[视频]'
|
||||
} else if (localType === 47) {
|
||||
msg.quotedContent = '[动画表情]'
|
||||
} else if (localType === 49) {
|
||||
msg.quotedContent = '[链接]'
|
||||
} else {
|
||||
msg.quotedContent = '[消息]'
|
||||
}
|
||||
console.log('[DEBUG] resolveQuotedMessages - 更新后的quotedContent:', msg.quotedContent)
|
||||
} else {
|
||||
msg.quotedContent = '[引用消息]'
|
||||
console.log('[DEBUG] resolveQuotedMessages - 查询失败,使用占位符')
|
||||
}
|
||||
}
|
||||
console.log('[DEBUG] resolveQuotedMessages - 完成')
|
||||
}
|
||||
|
||||
private extractPreferredQuotedText(referMsgXml: string): string {
|
||||
if (!referMsgXml) return ''
|
||||
|
||||
@@ -8792,7 +8915,7 @@ class ChatService {
|
||||
return { success: false, error: result.error || '查询语音消息失败' }
|
||||
}
|
||||
|
||||
let allVoiceMessages: Message[] = this.mapRowsToMessages(result.rows as Record<string, any>[])
|
||||
let allVoiceMessages: Message[] = this.mapRowsToMessages(result.rows as Record<string, any>[], sessionId)
|
||||
|
||||
// 按 createTime 降序排序
|
||||
allVoiceMessages.sort((a, b) => b.createTime - a.createTime)
|
||||
@@ -8835,7 +8958,7 @@ class ChatService {
|
||||
return { success: false, error: result.error || '查询图片消息失败' }
|
||||
}
|
||||
|
||||
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[])
|
||||
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[], sessionId)
|
||||
let allImages: Array<{ imageMd5?: string; imageDatName?: string; createTime?: number }> = mapped
|
||||
.filter(msg => msg.localType === 3)
|
||||
.map(msg => ({
|
||||
@@ -8960,7 +9083,7 @@ class ChatService {
|
||||
if (!result.success || !Array.isArray(result.rows) || result.rows.length === 0) continue
|
||||
if (result.rows.length >= perTypeFetch) maybeHasMore = true
|
||||
|
||||
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[])
|
||||
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[], sessionId)
|
||||
for (const message of mapped) {
|
||||
const resourceType = this.resolveResourceType(message)
|
||||
if (!resourceType || !typeSet.has(resourceType)) continue
|
||||
|
||||
@@ -3536,7 +3536,49 @@ class ExportService {
|
||||
return result
|
||||
}
|
||||
|
||||
private parseQuoteMessage(content: string): { content?: string; sender?: string; type?: string } {
|
||||
private async resolveQuotedMessagesForExport(messages: any[], sessionId: string): Promise<void> {
|
||||
const svridsToResolve: Array<{ msg: any; svrid: string }> = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.replyToMessageId && msg.quotedContent === '[消息]') {
|
||||
svridsToResolve.push({ msg, svrid: msg.replyToMessageId })
|
||||
}
|
||||
}
|
||||
|
||||
if (svridsToResolve.length === 0) return
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
svridsToResolve.map(({ svrid }) => wcdbService.getMessageByServerId(sessionId, svrid))
|
||||
)
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i]
|
||||
const { msg } = svridsToResolve[i]
|
||||
|
||||
if (result.status === 'fulfilled' && result.value.success && result.value.row) {
|
||||
const localType = parseInt(result.value.row.local_type || '0', 10)
|
||||
const rawMessageContent = result.value.row.message_content
|
||||
const rawCompressContent = result.value.row.compress_content
|
||||
const content = chatService['decodeMessageContent'](rawMessageContent, rawCompressContent)
|
||||
|
||||
if (localType === 1) {
|
||||
msg.quotedContent = chatService['sanitizeQuotedContent'](content)
|
||||
} else if (localType === 3) {
|
||||
msg.quotedContent = '[图片]'
|
||||
} else if (localType === 34) {
|
||||
msg.quotedContent = '[语音]'
|
||||
} else if (localType === 43) {
|
||||
msg.quotedContent = '[视频]'
|
||||
} else if (localType === 47) {
|
||||
msg.quotedContent = '[动画表情]'
|
||||
} else if (localType === 49) {
|
||||
msg.quotedContent = '[链接]'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseQuoteMessage(content: string): { content?: string; sender?: string; type?: string; svrid?: string } {
|
||||
try {
|
||||
const normalized = this.normalizeAppMessageContent(content || '')
|
||||
const referMsgStart = normalized.indexOf('<refermsg>')
|
||||
@@ -3553,6 +3595,7 @@ class ExportService {
|
||||
|
||||
const referContent = this.extractXmlValue(referMsgXml, 'content')
|
||||
const referType = this.extractXmlValue(referMsgXml, 'type')
|
||||
const svrid = this.extractXmlValue(referMsgXml, 'svrid')
|
||||
let displayContent = referContent
|
||||
|
||||
switch (referType) {
|
||||
@@ -3775,6 +3818,7 @@ class ExportService {
|
||||
if (quoteInfo.content) meta.quotedContent = quoteInfo.content
|
||||
if (quoteInfo.sender) meta.quotedSender = quoteInfo.sender
|
||||
if (quoteInfo.type) meta.quotedType = quoteInfo.type
|
||||
if (quoteInfo.svrid) meta.quotedSvrid = quoteInfo.svrid
|
||||
}
|
||||
|
||||
if (appMsgKind === 'link') {
|
||||
@@ -6935,6 +6979,9 @@ class ExportService {
|
||||
|
||||
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||
|
||||
// 解析引用消息
|
||||
await this.resolveQuotedMessagesForExport(collected.rows, sessionId)
|
||||
|
||||
const voiceMessages = options.exportVoiceAsText
|
||||
? collected.rows.filter(msg => msg.localType === 34)
|
||||
: []
|
||||
@@ -7139,7 +7186,8 @@ class ExportService {
|
||||
rawMyWxid,
|
||||
myDisplayName: myInfo.displayName || cleanedMyWxid
|
||||
})
|
||||
if (quotedReplyDisplay) {
|
||||
// 对于媒体消息,不要让引用信息覆盖媒体路径
|
||||
if (quotedReplyDisplay && !mediaItem) {
|
||||
content = this.buildQuotedReplyText(quotedReplyDisplay)
|
||||
}
|
||||
|
||||
@@ -7674,6 +7722,9 @@ class ExportService {
|
||||
|
||||
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||
|
||||
// 解析引用消息
|
||||
await this.resolveQuotedMessagesForExport(collected.rows, sessionId)
|
||||
|
||||
const voiceMessages = options.exportVoiceAsText
|
||||
? collected.rows.filter(msg => msg.localType === 34)
|
||||
: []
|
||||
@@ -8552,6 +8603,9 @@ class ExportService {
|
||||
|
||||
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||
|
||||
// 解析引用消息
|
||||
await this.resolveQuotedMessagesForExport(collected.rows, sessionId)
|
||||
|
||||
const voiceMessages = options.exportVoiceAsText
|
||||
? collected.rows.filter(msg => msg.localType === 34)
|
||||
: []
|
||||
|
||||
@@ -38,6 +38,7 @@ export class WcdbCore {
|
||||
private wcdbMarkAllSessionsRead: any = null
|
||||
private wcdbGetMessages: any = null
|
||||
private wcdbGetMessageCount: any = null
|
||||
private wcdbGetMessageByServerId: any = null
|
||||
private wcdbGetDisplayNames: any = null
|
||||
private wcdbGetAvatarUrls: any = null
|
||||
private wcdbGetGroupMemberCount: any = null
|
||||
@@ -824,6 +825,9 @@ export class WcdbCore {
|
||||
// wcdb_status wcdb_get_message_count(wcdb_handle handle, const char* username, int32_t* out_count)
|
||||
this.wcdbGetMessageCount = this.lib.func('int32 wcdb_get_message_count(int64 handle, const char* username, _Out_ int32* outCount)')
|
||||
|
||||
// wcdb_status wcdb_get_message_by_svrid(wcdb_handle handle, const char* session_id, const char* svrid, char** out_json)
|
||||
this.wcdbGetMessageByServerId = this.lib.func('int32 wcdb_get_message_by_svrid(int64 handle, const char* sessionId, const char* svrid, _Out_ void** outJson)')
|
||||
|
||||
// wcdb_status wcdb_get_display_names(wcdb_handle handle, const char* usernames_json, char** out_json)
|
||||
this.wcdbGetDisplayNames = this.lib.func('int32 wcdb_get_display_names(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
|
||||
|
||||
@@ -1807,6 +1811,30 @@ export class WcdbCore {
|
||||
}
|
||||
}
|
||||
|
||||
async getMessageByServerId(sessionId: string, svrid: string): Promise<{ success: boolean; row?: any; error?: string }> {
|
||||
if (!this.ensureReady()) {
|
||||
return { success: false, error: 'WCDB 未连接' }
|
||||
}
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbGetMessageByServerId(this.handle, sessionId, svrid, outPtr)
|
||||
if (result !== 0) {
|
||||
return { success: false, error: `查询消息失败: ${result}` }
|
||||
}
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) {
|
||||
return { success: true, row: null }
|
||||
}
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
if (!parsed || Object.keys(parsed).length === 0) {
|
||||
return { success: true, row: null }
|
||||
}
|
||||
return { success: true, row: parsed }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async getMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
|
||||
if (!this.ensureReady()) {
|
||||
return { success: false, error: 'WCDB 未连接' }
|
||||
|
||||
@@ -229,6 +229,13 @@ export class WcdbService {
|
||||
return this.callWorker('getMessageCount', { sessionId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 server_id 查询单条消息
|
||||
*/
|
||||
async getMessageByServerId(sessionId: string, svrid: string): Promise<{ success: boolean; row?: any; error?: string }> {
|
||||
return this.callWorker('getMessageByServerId', { sessionId, svrid })
|
||||
}
|
||||
|
||||
async getMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
|
||||
return this.callWorker('getMessageCounts', { sessionIds })
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ if (parentPort) {
|
||||
case 'getMessageCount':
|
||||
result = await core.getMessageCount(payload.sessionId)
|
||||
break
|
||||
case 'getMessageByServerId':
|
||||
result = await core.getMessageByServerId(payload.sessionId, payload.svrid)
|
||||
break
|
||||
case 'getMessageCounts':
|
||||
result = await core.getMessageCounts(payload.sessionIds)
|
||||
break
|
||||
|
||||
1108
package-lock.json
generated
1108
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
||||
"electron:build": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vscode/sudo-prompt": "^9.3.2",
|
||||
"echarts": "^6.0.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"electron-store": "^11.0.2",
|
||||
@@ -43,7 +44,6 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sherpa-onnx-node": "^1.10.38",
|
||||
"silk-wasm": "^3.7.1",
|
||||
"@vscode/sudo-prompt": "^9.3.2",
|
||||
"wechat-emojis": "^1.0.2",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
@@ -51,12 +51,13 @@
|
||||
"@electron/rebuild": "^4.0.2",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"electron": "^41.1.1",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "^0.28.0",
|
||||
"sass": "^1.98.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-electron": "^0.28.8",
|
||||
"vite-plugin-electron-renderer": "^0.14.6"
|
||||
|
||||
Binary file not shown.
@@ -9687,7 +9687,7 @@ function MessageBubble({
|
||||
// 渲染消息内容
|
||||
const renderContent = () => {
|
||||
if (isImage) {
|
||||
return (
|
||||
const imageContent = (
|
||||
<div
|
||||
ref={imageContainerRef}
|
||||
className={`image-stage ${imageStageLockHeight ? 'locked' : ''}`}
|
||||
@@ -9745,13 +9745,24 @@ function MessageBubble({
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (hasQuote) {
|
||||
return renderBubbleWithQuote(
|
||||
renderQuotedMessageBlock(renderTextWithEmoji(cleanMessageContent(quotedContent))),
|
||||
imageContent
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="bubble-content">{imageContent}</div>
|
||||
}
|
||||
|
||||
// 视频消息
|
||||
if (isVideo) {
|
||||
let videoContent: React.ReactNode
|
||||
|
||||
// 未进入可视区域时显示占位符
|
||||
if (!isVideoVisible) {
|
||||
return (
|
||||
videoContent = (
|
||||
<div className="video-placeholder" ref={videoContainerRef as React.RefObject<HTMLDivElement>}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
||||
@@ -9759,20 +9770,16 @@ function MessageBubble({
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 加载中
|
||||
if (videoLoading) {
|
||||
return (
|
||||
} else if (videoLoading) {
|
||||
// 加载中
|
||||
videoContent = (
|
||||
<div className="video-loading" ref={videoContainerRef as React.RefObject<HTMLDivElement>}>
|
||||
<Loader2 size={20} className="spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 视频不存在 - 添加点击重试功能
|
||||
if (!videoInfo?.exists || !videoInfo.videoUrl) {
|
||||
return (
|
||||
} else if (!videoInfo?.exists || !videoInfo.videoUrl) {
|
||||
// 视频不存在 - 添加点击重试功能
|
||||
videoContent = (
|
||||
<button
|
||||
className={`video-unavailable ${videoClicked ? 'clicked' : ''}`}
|
||||
ref={videoContainerRef as React.RefObject<HTMLButtonElement>}
|
||||
@@ -9792,27 +9799,36 @@ function MessageBubble({
|
||||
<span className="video-action">{videoClicked ? '已点击…' : '点击重试'}</span>
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
// 默认显示缩略图,点击打开独立播放窗口
|
||||
const thumbSrc = videoInfo.thumbUrl || videoInfo.coverUrl
|
||||
videoContent = (
|
||||
<div className="video-thumb-wrapper" ref={videoContainerRef as React.RefObject<HTMLDivElement>} onClick={handlePlayVideo}>
|
||||
{thumbSrc ? (
|
||||
<img src={thumbSrc} alt="视频缩略图" className="video-thumb" loading="lazy" decoding="async" />
|
||||
) : (
|
||||
<div className="video-thumb-placeholder">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="video-play-button">
|
||||
<Play size={32} fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 默认显示缩略图,点击打开独立播放窗口
|
||||
const thumbSrc = videoInfo.thumbUrl || videoInfo.coverUrl
|
||||
return (
|
||||
<div className="video-thumb-wrapper" ref={videoContainerRef as React.RefObject<HTMLDivElement>} onClick={handlePlayVideo}>
|
||||
{thumbSrc ? (
|
||||
<img src={thumbSrc} alt="视频缩略图" className="video-thumb" loading="lazy" decoding="async" />
|
||||
) : (
|
||||
<div className="video-thumb-placeholder">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="video-play-button">
|
||||
<Play size={32} fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
if (hasQuote) {
|
||||
return renderBubbleWithQuote(
|
||||
renderQuotedMessageBlock(renderTextWithEmoji(cleanMessageContent(quotedContent))),
|
||||
videoContent
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="bubble-content">{videoContent}</div>
|
||||
}
|
||||
|
||||
if (isVoice) {
|
||||
@@ -9900,7 +9916,7 @@ function MessageBubble({
|
||||
void requestVoiceTranscript()
|
||||
}
|
||||
|
||||
return (
|
||||
const voiceContent = (
|
||||
<div className="voice-stack">
|
||||
<div className={`voice-message ${isVoicePlaying ? 'playing' : ''}`} onClick={handleToggle}>
|
||||
<button
|
||||
@@ -9983,6 +9999,15 @@ function MessageBubble({
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (hasQuote) {
|
||||
return renderBubbleWithQuote(
|
||||
renderQuotedMessageBlock(renderTextWithEmoji(cleanMessageContent(quotedContent))),
|
||||
voiceContent
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="bubble-content">{voiceContent}</div>
|
||||
}
|
||||
|
||||
// 名片消息
|
||||
|
||||
Reference in New Issue
Block a user