mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
Merge remote-tracking branch 'upstream/dev'
This commit is contained in:
@@ -688,8 +688,16 @@ export class ConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
private getUserDataPath(): string {
|
||||
const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
|
||||
if (workerUserDataPath) {
|
||||
return workerUserDataPath
|
||||
}
|
||||
return app?.getPath?.('userData') || process.cwd()
|
||||
}
|
||||
|
||||
getCacheBasePath(): string {
|
||||
return join(app.getPath('userData'), 'cache')
|
||||
return join(this.getUserDataPath(), 'cache')
|
||||
}
|
||||
|
||||
getAll(): Partial<ConfigSchema> {
|
||||
|
||||
@@ -186,6 +186,33 @@ body {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.quoted-message {
|
||||
border-left: 3px solid rgba(79, 70, 229, 0.35);
|
||||
background: rgba(79, 70, 229, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.message.sent .quoted-message {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-left-color: rgba(37, 99, 235, 0.35);
|
||||
}
|
||||
|
||||
.quoted-sender {
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quoted-text {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-link-card {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -186,6 +186,33 @@ body {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.quoted-message {
|
||||
border-left: 3px solid rgba(79, 70, 229, 0.35);
|
||||
background: rgba(79, 70, 229, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.message.sent .quoted-message {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-left-color: rgba(37, 99, 235, 0.35);
|
||||
}
|
||||
|
||||
.quoted-sender {
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quoted-text {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-link-card {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -46,6 +46,8 @@ interface ChatLabMessage {
|
||||
timestamp: number
|
||||
type: number
|
||||
content: string | null
|
||||
platformMessageId?: string
|
||||
replyToMessageId?: string
|
||||
chatRecords?: any[] // 嵌套的聊天记录
|
||||
}
|
||||
|
||||
@@ -952,6 +954,18 @@ class ExportService {
|
||||
return fallback
|
||||
}
|
||||
|
||||
private getRowField(row: Record<string, any>, keys: string[]): any {
|
||||
for (const key of keys) {
|
||||
if (row && Object.prototype.hasOwnProperty.call(row, key)) {
|
||||
const value = row[key]
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private normalizeUnsignedIntToken(value: unknown): string {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return '0'
|
||||
@@ -963,14 +977,14 @@ class ExportService {
|
||||
return String(Math.floor(num))
|
||||
}
|
||||
|
||||
private getStableMessageKey(msg: { localId?: unknown; createTime?: unknown; serverId?: unknown }): string {
|
||||
private getStableMessageKey(msg: { localId?: unknown; createTime?: unknown; serverId?: unknown; serverIdRaw?: unknown }): string {
|
||||
const localId = this.normalizeUnsignedIntToken(msg?.localId)
|
||||
const createTime = this.normalizeUnsignedIntToken(msg?.createTime)
|
||||
const serverId = this.normalizeUnsignedIntToken(msg?.serverId)
|
||||
const serverId = this.normalizeUnsignedIntToken(msg?.serverIdRaw ?? msg?.serverId)
|
||||
return `${localId}:${createTime}:${serverId}`
|
||||
}
|
||||
|
||||
private getMediaCacheKey(msg: { localType?: unknown; localId?: unknown; createTime?: unknown; serverId?: unknown }): string {
|
||||
private getMediaCacheKey(msg: { localType?: unknown; localId?: unknown; createTime?: unknown; serverId?: unknown; serverIdRaw?: unknown }): string {
|
||||
const localType = this.normalizeUnsignedIntToken(msg?.localType)
|
||||
return `${localType}_${this.getStableMessageKey(msg)}`
|
||||
}
|
||||
@@ -1620,7 +1634,13 @@ class ExportService {
|
||||
if (type === '6') return title ? `[文件] ${title}` : '[文件]'
|
||||
if (type === '19') return this.formatForwardChatRecordContent(normalizedContent)
|
||||
if (type === '33' || type === '36') return title ? `[小程序] ${title}` : '[小程序]'
|
||||
if (type === '57') return title || '[引用消息]'
|
||||
if (type === '57') {
|
||||
const quoteDisplay = this.extractQuotedReplyDisplay(content)
|
||||
if (quoteDisplay) {
|
||||
return this.buildQuotedReplyText(quoteDisplay)
|
||||
}
|
||||
return title || '[引用消息]'
|
||||
}
|
||||
if (type === '5' || type === '49') return title ? `[链接] ${title}` : '[链接]'
|
||||
return title ? `[链接] ${title}` : '[链接]'
|
||||
}
|
||||
@@ -1629,6 +1649,10 @@ class ExportService {
|
||||
case 266287972401: return this.cleanSystemMessage(content) // 拍一拍
|
||||
case 244813135921: {
|
||||
// 引用消息
|
||||
const quoteDisplay = this.extractQuotedReplyDisplay(content)
|
||||
if (quoteDisplay) {
|
||||
return this.buildQuotedReplyText(quoteDisplay)
|
||||
}
|
||||
const title = this.extractXmlValue(content, 'title')
|
||||
return title || '[引用消息]'
|
||||
}
|
||||
@@ -1662,7 +1686,13 @@ class ExportService {
|
||||
if (xmlType === '6') return title ? `[文件] ${title}` : '[文件]'
|
||||
if (xmlType === '19') return this.formatForwardChatRecordContent(normalizedContent)
|
||||
if (xmlType === '33' || xmlType === '36') return title ? `[小程序] ${title}` : '[小程序]'
|
||||
if (xmlType === '57') return title || '[引用消息]'
|
||||
if (xmlType === '57') {
|
||||
const quoteDisplay = this.extractQuotedReplyDisplay(content)
|
||||
if (quoteDisplay) {
|
||||
return this.buildQuotedReplyText(quoteDisplay)
|
||||
}
|
||||
return title || '[引用消息]'
|
||||
}
|
||||
if (xmlType === '5' || xmlType === '49') return title ? `[链接] ${title}` : '[链接]'
|
||||
|
||||
// 有 title 就返回 title
|
||||
@@ -1787,6 +1817,10 @@ class ExportService {
|
||||
return `[小程序]${appName}`
|
||||
}
|
||||
if (subType === 57) {
|
||||
const quoteDisplay = this.extractQuotedReplyDisplay(safeContent)
|
||||
if (quoteDisplay) {
|
||||
return this.buildQuotedReplyText(quoteDisplay)
|
||||
}
|
||||
return title || '[引用消息]'
|
||||
}
|
||||
if (title) {
|
||||
@@ -1798,6 +1832,161 @@ class ExportService {
|
||||
return '[其他消息]'
|
||||
}
|
||||
|
||||
private formatQuotedReferencePreview(content: string, type?: string): string {
|
||||
const safeContent = content || ''
|
||||
const referType = Number.parseInt(String(type || ''), 10)
|
||||
if (!Number.isFinite(referType)) {
|
||||
const sanitized = this.sanitizeQuotedContent(safeContent)
|
||||
return sanitized || '[消息]'
|
||||
}
|
||||
|
||||
if (referType === 49) {
|
||||
const normalized = this.normalizeAppMessageContent(safeContent)
|
||||
const title =
|
||||
this.extractXmlValue(normalized, 'title') ||
|
||||
this.extractXmlValue(normalized, 'filename') ||
|
||||
this.extractXmlValue(normalized, 'appname')
|
||||
if (title) return this.stripSenderPrefix(title)
|
||||
|
||||
const subTypeRaw = this.extractAppMessageType(normalized)
|
||||
const subType = subTypeRaw ? parseInt(subTypeRaw, 10) : 0
|
||||
if (subType === 6) return '[文件]'
|
||||
if (subType === 19) return '[聊天记录]'
|
||||
if (subType === 33 || subType === 36) return '[小程序]'
|
||||
return '[链接]'
|
||||
}
|
||||
|
||||
return this.formatPlainExportContent(safeContent, referType, { exportVoiceAsText: false }) || '[消息]'
|
||||
}
|
||||
|
||||
private resolveQuotedSenderUsername(fromusr?: string, chatusr?: string): string {
|
||||
const normalizedChatUsr = String(chatusr || '').trim()
|
||||
const normalizedFromUsr = String(fromusr || '').trim()
|
||||
|
||||
if (normalizedChatUsr) {
|
||||
return normalizedChatUsr
|
||||
}
|
||||
|
||||
if (normalizedFromUsr.endsWith('@chatroom')) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return normalizedFromUsr
|
||||
}
|
||||
|
||||
private buildQuotedReplyText(display: {
|
||||
replyText: string
|
||||
quotedSender?: string
|
||||
quotedPreview: string
|
||||
}): string {
|
||||
const quoteLabel = display.quotedSender
|
||||
? `${display.quotedSender}:${display.quotedPreview}`
|
||||
: display.quotedPreview
|
||||
if (display.replyText) {
|
||||
return `${display.replyText}[引用 ${quoteLabel}]`
|
||||
}
|
||||
return `[引用 ${quoteLabel}]`
|
||||
}
|
||||
|
||||
private extractQuotedReplyDisplay(content: string): {
|
||||
replyText: string
|
||||
quotedSender?: string
|
||||
quotedPreview: string
|
||||
} | null {
|
||||
try {
|
||||
const normalized = this.normalizeAppMessageContent(content || '')
|
||||
const referMsgStart = normalized.indexOf('<refermsg>')
|
||||
const referMsgEnd = normalized.indexOf('</refermsg>')
|
||||
if (referMsgStart === -1 || referMsgEnd === -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const referMsgXml = normalized.substring(referMsgStart, referMsgEnd + 11)
|
||||
const quoteInfo = this.parseQuoteMessage(normalized)
|
||||
const replyText = this.stripSenderPrefix(this.extractXmlValue(normalized, 'title') || '')
|
||||
const quotedPreview = this.formatQuotedReferencePreview(
|
||||
this.extractXmlValue(referMsgXml, 'content'),
|
||||
this.extractXmlValue(referMsgXml, 'type')
|
||||
)
|
||||
|
||||
if (!replyText && !quotedPreview) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
replyText,
|
||||
quotedSender: quoteInfo.sender || undefined,
|
||||
quotedPreview: quotedPreview || '[消息]'
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private isQuotedReplyMessage(localType: number, content: string): boolean {
|
||||
if (localType === 244813135921) return true
|
||||
const normalized = this.normalizeAppMessageContent(content || '')
|
||||
if (!(localType === 49 || normalized.includes('<appmsg') || normalized.includes('<msg>'))) {
|
||||
return false
|
||||
}
|
||||
const subType = this.extractAppMessageType(normalized)
|
||||
return subType === '57' || normalized.includes('<refermsg>')
|
||||
}
|
||||
|
||||
private async resolveQuotedReplyDisplayWithNames(args: {
|
||||
content: string
|
||||
isGroup: boolean
|
||||
displayNamePreference: ExportOptions['displayNamePreference']
|
||||
getContact: (username: string) => Promise<{ success: boolean; contact?: any; error?: string }>
|
||||
groupNicknamesMap: Map<string, string>
|
||||
cleanedMyWxid: string
|
||||
rawMyWxid?: string
|
||||
myDisplayName?: string
|
||||
}): Promise<{
|
||||
replyText: string
|
||||
quotedSender?: string
|
||||
quotedPreview: string
|
||||
} | null> {
|
||||
const base = this.extractQuotedReplyDisplay(args.content)
|
||||
if (!base) return null
|
||||
if (base.quotedSender) return base
|
||||
|
||||
const normalized = this.normalizeAppMessageContent(args.content || '')
|
||||
const referMsgStart = normalized.indexOf('<refermsg>')
|
||||
const referMsgEnd = normalized.indexOf('</refermsg>')
|
||||
if (referMsgStart === -1 || referMsgEnd === -1) {
|
||||
return base
|
||||
}
|
||||
|
||||
const referMsgXml = normalized.substring(referMsgStart, referMsgEnd + 11)
|
||||
const quotedSenderUsername = this.resolveQuotedSenderUsername(
|
||||
this.extractXmlValue(referMsgXml, 'fromusr'),
|
||||
this.extractXmlValue(referMsgXml, 'chatusr')
|
||||
)
|
||||
if (!quotedSenderUsername) {
|
||||
return base
|
||||
}
|
||||
|
||||
const isQuotedSelf = this.isSameWxid(quotedSenderUsername, args.cleanedMyWxid)
|
||||
const fallbackDisplayName = isQuotedSelf
|
||||
? (args.myDisplayName || quotedSenderUsername)
|
||||
: quotedSenderUsername
|
||||
|
||||
const profile = await this.resolveExportDisplayProfile(
|
||||
quotedSenderUsername,
|
||||
args.displayNamePreference,
|
||||
args.getContact,
|
||||
args.groupNicknamesMap,
|
||||
fallbackDisplayName,
|
||||
isQuotedSelf ? [args.rawMyWxid, args.cleanedMyWxid] : []
|
||||
)
|
||||
|
||||
return {
|
||||
...base,
|
||||
quotedSender: profile.displayName || fallbackDisplayName || base.quotedSender
|
||||
}
|
||||
}
|
||||
|
||||
private parseDurationSeconds(value: string): number | null {
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) return null
|
||||
@@ -2462,6 +2651,32 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private extractChatLabReplyToMessageId(content: string): string | undefined {
|
||||
try {
|
||||
const normalized = this.normalizeAppMessageContent(content || '')
|
||||
const referMsgStart = normalized.indexOf('<refermsg>')
|
||||
const referMsgEnd = normalized.indexOf('</refermsg>')
|
||||
if (referMsgStart === -1 || referMsgEnd === -1) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const referMsgXml = normalized.substring(referMsgStart, referMsgEnd + 11)
|
||||
const replyToMessageIdRaw = this.normalizeUnsignedIntToken(this.extractXmlValue(referMsgXml, 'svrid'))
|
||||
return replyToMessageIdRaw !== '0' ? replyToMessageIdRaw : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private getExportPlatformMessageId(msg: { serverIdRaw?: unknown; serverId?: unknown }): string | undefined {
|
||||
const value = this.normalizeUnsignedIntToken(msg.serverIdRaw ?? msg.serverId)
|
||||
return value !== '0' ? value : undefined
|
||||
}
|
||||
|
||||
private getExportReplyToMessageId(content: string): string | undefined {
|
||||
return this.extractChatLabReplyToMessageId(content)
|
||||
}
|
||||
|
||||
private extractArkmeAppMessageMeta(content: string, localType: number): Record<string, any> | null {
|
||||
if (!content) return null
|
||||
|
||||
@@ -3507,6 +3722,13 @@ class ExportService {
|
||||
'msg_id', 'msgId', 'MsgId', 'id',
|
||||
'WCDB_CT_local_id'
|
||||
], 0)
|
||||
const rawServerIdValue = this.getRowField(row, [
|
||||
'server_id', 'serverId', 'ServerId',
|
||||
'msg_server_id', 'msgServerId', 'MsgServerId',
|
||||
'svr_id', 'svrId', 'msg_svr_id', 'msgSvrId', 'MsgSvrId',
|
||||
'WCDB_CT_server_id'
|
||||
])
|
||||
const serverIdRaw = this.normalizeUnsignedIntToken(rawServerIdValue)
|
||||
const serverId = this.getIntFromRow(row, [
|
||||
'server_id', 'serverId', 'ServerId',
|
||||
'msg_server_id', 'msgServerId', 'MsgServerId',
|
||||
@@ -3598,6 +3820,7 @@ class ExportService {
|
||||
rows.push({
|
||||
localId,
|
||||
serverId,
|
||||
serverIdRaw: serverIdRaw !== '0' ? serverIdRaw : undefined,
|
||||
createTime,
|
||||
localType,
|
||||
content,
|
||||
@@ -4440,6 +4663,16 @@ class ExportService {
|
||||
content: content
|
||||
}
|
||||
|
||||
const platformMessageId = this.normalizeUnsignedIntToken(msg.serverIdRaw ?? msg.serverId)
|
||||
if (platformMessageId !== '0') {
|
||||
message.platformMessageId = platformMessageId
|
||||
}
|
||||
|
||||
const replyToMessageId = this.extractChatLabReplyToMessageId(msg.content)
|
||||
if (replyToMessageId) {
|
||||
message.replyToMessageId = replyToMessageId
|
||||
}
|
||||
|
||||
// 如果有聊天记录,添加为嵌套字段
|
||||
if (msg.chatRecordList && msg.chatRecordList.length > 0) {
|
||||
const chatRecords: any[] = []
|
||||
@@ -4895,6 +5128,20 @@ class ExportService {
|
||||
)
|
||||
}
|
||||
|
||||
const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({
|
||||
content: msg.content,
|
||||
isGroup,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
getContact: getContactCached,
|
||||
groupNicknamesMap,
|
||||
cleanedMyWxid,
|
||||
rawMyWxid,
|
||||
myDisplayName: myInfo.displayName || cleanedMyWxid
|
||||
})
|
||||
if (quotedReplyDisplay) {
|
||||
content = this.buildQuotedReplyText(quotedReplyDisplay)
|
||||
}
|
||||
|
||||
// 获取发送者信息用于名称显示
|
||||
const senderWxid = msg.senderUsername
|
||||
const contact = senderWxid
|
||||
@@ -4938,6 +5185,12 @@ class ExportService {
|
||||
senderAvatarKey: msg.senderUsername
|
||||
}
|
||||
|
||||
const platformMessageId = this.getExportPlatformMessageId(msg)
|
||||
if (platformMessageId) msgObj.platformMessageId = platformMessageId
|
||||
|
||||
const replyToMessageId = this.getExportReplyToMessageId(msg.content)
|
||||
if (replyToMessageId) msgObj.replyToMessageId = replyToMessageId
|
||||
|
||||
const appMsgMeta = this.extractArkmeAppMessageMeta(msg.content, msg.localType)
|
||||
if (appMsgMeta) {
|
||||
if (
|
||||
@@ -4947,6 +5200,10 @@ class ExportService {
|
||||
Object.assign(msgObj, appMsgMeta)
|
||||
}
|
||||
}
|
||||
if (quotedReplyDisplay) {
|
||||
if (quotedReplyDisplay.quotedSender) msgObj.quotedSender = quotedReplyDisplay.quotedSender
|
||||
if (quotedReplyDisplay.quotedPreview) msgObj.quotedContent = quotedReplyDisplay.quotedPreview
|
||||
}
|
||||
|
||||
if (options.format === 'arkme-json') {
|
||||
const contactCardMeta = this.extractArkmeContactCardMeta(msg.content, msg.localType)
|
||||
@@ -5144,6 +5401,8 @@ class ExportService {
|
||||
senderID,
|
||||
source: message.source
|
||||
}
|
||||
if (message.platformMessageId) compactMessage.platformMessageId = message.platformMessageId
|
||||
if (message.replyToMessageId) compactMessage.replyToMessageId = message.replyToMessageId
|
||||
if (message.locationLat != null) compactMessage.locationLat = message.locationLat
|
||||
if (message.locationLng != null) compactMessage.locationLng = message.locationLng
|
||||
if (message.locationPoiname) compactMessage.locationPoiname = message.locationPoiname
|
||||
@@ -5781,6 +6040,20 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({
|
||||
content: msg.content,
|
||||
isGroup,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
getContact: getContactCached,
|
||||
groupNicknamesMap,
|
||||
cleanedMyWxid,
|
||||
rawMyWxid,
|
||||
myDisplayName: myInfo.displayName || cleanedMyWxid
|
||||
})
|
||||
if (quotedReplyDisplay) {
|
||||
enrichedContentValue = this.buildQuotedReplyText(quotedReplyDisplay)
|
||||
}
|
||||
|
||||
// 调试日志
|
||||
if (msg.localType === 3 || msg.localType === 47) {
|
||||
}
|
||||
@@ -6028,6 +6301,20 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({
|
||||
content: msg.content,
|
||||
isGroup,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
getContact: getContactCached,
|
||||
groupNicknamesMap,
|
||||
cleanedMyWxid,
|
||||
rawMyWxid,
|
||||
myDisplayName: myInfo.displayName || cleanedMyWxid
|
||||
})
|
||||
if (quotedReplyDisplay) {
|
||||
enrichedContentValue = this.buildQuotedReplyText(quotedReplyDisplay)
|
||||
}
|
||||
|
||||
appendRow(useCompactColumns
|
||||
? [
|
||||
i + 1,
|
||||
@@ -6381,6 +6668,20 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({
|
||||
content: msg.content,
|
||||
isGroup,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
getContact: getContactCached,
|
||||
groupNicknamesMap,
|
||||
cleanedMyWxid,
|
||||
rawMyWxid,
|
||||
myDisplayName: myInfo.displayName || cleanedMyWxid
|
||||
})
|
||||
if (quotedReplyDisplay) {
|
||||
enrichedContentValue = this.buildQuotedReplyText(quotedReplyDisplay)
|
||||
}
|
||||
|
||||
let senderRole: string
|
||||
let senderWxid: string
|
||||
let senderNickname: string
|
||||
@@ -6522,7 +6823,7 @@ class ExportService {
|
||||
control,
|
||||
collectProgressReporter
|
||||
)
|
||||
const totalMessages = collected.rows.length
|
||||
let totalMessages = collected.rows.length
|
||||
if (totalMessages === 0) {
|
||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
||||
}
|
||||
@@ -6550,7 +6851,13 @@ class ExportService {
|
||||
? await this.getGroupNicknamesForRoom(sessionId, groupNicknameCandidates)
|
||||
: new Map<string, string>()
|
||||
|
||||
const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime)
|
||||
const sortedMessages = collected.rows
|
||||
.sort((a, b) => a.createTime - b.createTime)
|
||||
.filter((msg) => !this.isQuotedReplyMessage(msg.localType, msg.content || ''))
|
||||
totalMessages = sortedMessages.length
|
||||
if (totalMessages === 0) {
|
||||
return { success: false, error: '该会话在指定时间范围内没有可导出的消息' }
|
||||
}
|
||||
|
||||
const voiceMessages = options.exportVoiceAsText
|
||||
? sortedMessages.filter(msg => msg.localType === 34)
|
||||
@@ -6742,10 +7049,11 @@ class ExportService {
|
||||
msg.isSend
|
||||
) || '')
|
||||
const src = this.getWeCloneSource(msg, typeName, mediaItem)
|
||||
const platformMessageId = this.getExportPlatformMessageId(msg) || ''
|
||||
|
||||
const row = [
|
||||
i + 1,
|
||||
i + 1,
|
||||
platformMessageId,
|
||||
typeName,
|
||||
msg.isSend ? 1 : 0,
|
||||
talker,
|
||||
@@ -6945,6 +7253,7 @@ class ExportService {
|
||||
if (collected.rows.length === 0) {
|
||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
||||
}
|
||||
const totalMessages = collected.rows.length
|
||||
|
||||
const senderUsernames = new Set<string>()
|
||||
let senderScanIndex = 0
|
||||
@@ -6987,6 +7296,7 @@ class ExportService {
|
||||
: []
|
||||
|
||||
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||
const mediaDirCache = new Set<string>()
|
||||
|
||||
if (mediaMessages.length > 0) {
|
||||
await this.preloadMediaLookupCaches(sessionId, mediaMessages, {
|
||||
@@ -7219,8 +7529,18 @@ class ExportService {
|
||||
|
||||
const timeText = this.formatTimestamp(msg.createTime)
|
||||
const typeName = this.getMessageTypeName(msg.localType)
|
||||
const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({
|
||||
content: msg.content,
|
||||
isGroup,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
getContact: getContactCached,
|
||||
groupNicknamesMap,
|
||||
cleanedMyWxid,
|
||||
rawMyWxid,
|
||||
myDisplayName: myInfo.displayName || cleanedMyWxid
|
||||
})
|
||||
|
||||
let textContent = this.formatHtmlMessageText(
|
||||
let textContent = quotedReplyDisplay?.replyText || this.formatHtmlMessageText(
|
||||
msg.content,
|
||||
msg.localType,
|
||||
cleanedMyWxid,
|
||||
@@ -7251,7 +7571,7 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
const linkCard = this.extractHtmlLinkCard(msg.content, msg.localType)
|
||||
const linkCard = quotedReplyDisplay ? null : this.extractHtmlLinkCard(msg.content, msg.localType)
|
||||
|
||||
let mediaHtml = ''
|
||||
if (mediaItem?.kind === 'image') {
|
||||
@@ -7267,25 +7587,40 @@ class ExportService {
|
||||
mediaHtml = `<video class="message-media video" controls preload="metadata"${posterAttr} src="${this.escapeAttribute(encodeURI(mediaItem.relativePath))}"></video>`
|
||||
}
|
||||
|
||||
const textHtml = linkCard
|
||||
? `<div class="message-text"><a class="message-link-card" href="${this.escapeAttribute(linkCard.url)}" target="_blank" rel="noopener noreferrer">${this.renderTextWithEmoji(linkCard.title).replace(/\r?\n/g, '<br />')}</a></div>`
|
||||
: (textContent
|
||||
? `<div class="message-text">${this.renderTextWithEmoji(textContent).replace(/\r?\n/g, '<br />')}</div>`
|
||||
: '')
|
||||
const textHtml = quotedReplyDisplay
|
||||
? (() => {
|
||||
const quotedSenderHtml = quotedReplyDisplay.quotedSender
|
||||
? `<div class="quoted-sender">${this.escapeHtml(quotedReplyDisplay.quotedSender)}</div>`
|
||||
: ''
|
||||
const quotedPreviewHtml = `<div class="quoted-text">${this.renderTextWithEmoji(quotedReplyDisplay.quotedPreview).replace(/\r?\n/g, '<br />')}</div>`
|
||||
const replyTextHtml = textContent
|
||||
? `<div class="message-text">${this.renderTextWithEmoji(textContent).replace(/\r?\n/g, '<br />')}</div>`
|
||||
: ''
|
||||
return `<div class="quoted-message">${quotedSenderHtml}${quotedPreviewHtml}</div>${replyTextHtml}`
|
||||
})()
|
||||
: (linkCard
|
||||
? `<div class="message-text"><a class="message-link-card" href="${this.escapeAttribute(linkCard.url)}" target="_blank" rel="noopener noreferrer">${this.renderTextWithEmoji(linkCard.title).replace(/\r?\n/g, '<br />')}</a></div>`
|
||||
: (textContent
|
||||
? `<div class="message-text">${this.renderTextWithEmoji(textContent).replace(/\r?\n/g, '<br />')}</div>`
|
||||
: ''))
|
||||
const senderNameHtml = isGroup
|
||||
? `<div class="sender-name">${this.escapeHtml(resolvedSenderName)}</div>`
|
||||
: ''
|
||||
const timeHtml = `<div class="message-time">${this.escapeHtml(timeText)}</div>`
|
||||
const messageBody = `${timeHtml}${senderNameHtml}<div class="message-content">${mediaHtml}${textHtml}</div>`
|
||||
const platformMessageId = this.getExportPlatformMessageId(msg)
|
||||
const replyToMessageId = this.getExportReplyToMessageId(msg.content)
|
||||
|
||||
// Compact JSON object
|
||||
const itemObj = {
|
||||
const itemObj: Record<string, any> = {
|
||||
i: i + 1, // index
|
||||
t: msg.createTime, // timestamp
|
||||
s: isSenderMe ? 1 : 0, // isSend
|
||||
a: avatarHtml, // avatar HTML
|
||||
b: messageBody // body HTML
|
||||
}
|
||||
if (platformMessageId) itemObj.p = platformMessageId
|
||||
if (replyToMessageId) itemObj.r = replyToMessageId
|
||||
|
||||
writeBuf.push(JSON.stringify(itemObj))
|
||||
|
||||
@@ -7333,8 +7668,10 @@ class ExportService {
|
||||
// Render Item Function
|
||||
const renderItem = (item, index) => {
|
||||
const isSenderMe = item.s === 1;
|
||||
const platformIdAttr = item.p ? \` data-platform-message-id="\${item.p}"\` : '';
|
||||
const replyToAttr = item.r ? \` data-reply-to-message-id="\${item.r}"\` : '';
|
||||
return \`
|
||||
<div class="message \${isSenderMe ? 'sent' : 'received'}" data-index="\${item.i}">
|
||||
<div class="message \${isSenderMe ? 'sent' : 'received'}" data-index="\${item.i}"\${platformIdAttr}\${replyToAttr}>
|
||||
<div class="message-row">
|
||||
<div class="avatar">\${item.a}</div>
|
||||
<div class="bubble">
|
||||
|
||||
@@ -1226,7 +1226,7 @@ class HttpService {
|
||||
* 映射 Type 49 子类型
|
||||
*/
|
||||
private mapType49(msg: Message): number {
|
||||
const xmlType = msg.xmlType
|
||||
const xmlType = this.resolveType49Subtype(msg)
|
||||
|
||||
switch (xmlType) {
|
||||
case '5': // 链接
|
||||
@@ -1250,10 +1250,97 @@ class HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
private extractType49Subtype(rawContent: string): string {
|
||||
const content = String(rawContent || '')
|
||||
if (!content) return ''
|
||||
|
||||
const appmsgMatch = /<appmsg[\s\S]*?>([\s\S]*?)<\/appmsg>/i.exec(content)
|
||||
if (appmsgMatch) {
|
||||
const appmsgInner = appmsgMatch[1]
|
||||
.replace(/<refermsg[\s\S]*?<\/refermsg>/gi, '')
|
||||
.replace(/<patMsg[\s\S]*?<\/patMsg>/gi, '')
|
||||
const typeMatch = /<type>([\s\S]*?)<\/type>/i.exec(appmsgInner)
|
||||
if (typeMatch) {
|
||||
return typeMatch[1].replace(/<!\[CDATA\[/g, '').replace(/\]\]>/g, '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackMatch = /<type>([\s\S]*?)<\/type>/i.exec(content)
|
||||
if (fallbackMatch) {
|
||||
return fallbackMatch[1].replace(/<!\[CDATA\[/g, '').replace(/\]\]>/g, '').trim()
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private resolveType49Subtype(msg: Message): string {
|
||||
const xmlType = String(msg.xmlType || '').trim()
|
||||
if (xmlType) return xmlType
|
||||
|
||||
const extractedType = this.extractType49Subtype(msg.rawContent)
|
||||
if (extractedType) return extractedType
|
||||
|
||||
switch (msg.appMsgKind) {
|
||||
case 'official-link':
|
||||
case 'link':
|
||||
return '5'
|
||||
case 'file':
|
||||
return '6'
|
||||
case 'chat-record':
|
||||
return '19'
|
||||
case 'miniapp':
|
||||
return '33'
|
||||
case 'quote':
|
||||
return '57'
|
||||
case 'transfer':
|
||||
return '2000'
|
||||
case 'red-packet':
|
||||
return '2001'
|
||||
case 'music':
|
||||
return '3'
|
||||
default:
|
||||
if (msg.linkUrl) return '5'
|
||||
if (msg.fileName) return '6'
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
private getType49Content(msg: Message): string {
|
||||
const subtype = this.resolveType49Subtype(msg)
|
||||
const title = msg.linkTitle || msg.fileName || ''
|
||||
|
||||
switch (subtype) {
|
||||
case '5':
|
||||
case '49':
|
||||
return title ? `[链接] ${title}` : '[链接]'
|
||||
case '6':
|
||||
return title ? `[文件] ${title}` : '[文件]'
|
||||
case '19':
|
||||
return title ? `[聊天记录] ${title}` : '[聊天记录]'
|
||||
case '33':
|
||||
case '36':
|
||||
return title ? `[小程序] ${title}` : '[小程序]'
|
||||
case '57':
|
||||
return msg.parsedContent || title || '[引用消息]'
|
||||
case '2000':
|
||||
return title ? `[转账] ${title}` : '[转账]'
|
||||
case '2001':
|
||||
return title ? `[红包] ${title}` : '[红包]'
|
||||
case '3':
|
||||
return title ? `[音乐] ${title}` : '[音乐]'
|
||||
default:
|
||||
return msg.parsedContent || title || '[消息]'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息内容
|
||||
*/
|
||||
private getMessageContent(msg: Message): string | null {
|
||||
if (msg.localType === 49) {
|
||||
return this.getType49Content(msg)
|
||||
}
|
||||
|
||||
// 优先使用已解析的内容
|
||||
if (msg.parsedContent) {
|
||||
return msg.parsedContent
|
||||
@@ -1276,7 +1363,7 @@ class HttpService {
|
||||
case 48:
|
||||
return '[位置]'
|
||||
case 49:
|
||||
return msg.linkTitle || msg.fileName || '[消息]'
|
||||
return this.getType49Content(msg)
|
||||
default:
|
||||
return msg.rawContent || null
|
||||
}
|
||||
|
||||
@@ -262,6 +262,7 @@ export class KeyServiceMac {
|
||||
): Promise<string> {
|
||||
const helperPath = this.getHelperPath()
|
||||
const waitMs = Math.max(timeoutMs, 30_000)
|
||||
const timeoutSec = Math.ceil(waitMs / 1000) + 30
|
||||
const pid = await this.getWeChatPid()
|
||||
onStatus?.(`已找到微信进程 PID=${pid},正在定位目标函数...`, 0)
|
||||
// 最佳努力清理同路径残留 helper(普通权限)
|
||||
@@ -378,12 +379,22 @@ export class KeyServiceMac {
|
||||
): Promise<string> {
|
||||
const helperPath = this.getHelperPath()
|
||||
const waitMs = Math.max(timeoutMs, 30_000)
|
||||
const timeoutSec = Math.ceil(waitMs / 1000) + 30
|
||||
const pid = await this.getWeChatPid()
|
||||
// 用 AppleScript 的 quoted form 组装命令,避免复杂 shell 拼接导致整条失败
|
||||
// 通过 try/on error 回传详细错误,避免只看到 "Command failed"
|
||||
const scriptLines = [
|
||||
`set helperPath to ${JSON.stringify(helperPath)}`,
|
||||
`set cmd to quoted form of helperPath & " ${pid} ${waitMs} 2>&1"`,
|
||||
'do shell script cmd with administrator privileges'
|
||||
`set cmd to quoted form of helperPath & " ${pid} ${waitMs}"`,
|
||||
`set timeoutSec to ${timeoutSec}`,
|
||||
'try',
|
||||
'with timeout of timeoutSec seconds',
|
||||
'set outText to do shell script cmd with administrator privileges',
|
||||
'end timeout',
|
||||
'return "WF_OK::" & outText',
|
||||
'on error errMsg number errNum partial result pr',
|
||||
'return "WF_ERR::" & errNum & "::" & errMsg & "::" & (pr as text)',
|
||||
'end try'
|
||||
]
|
||||
onStatus?.('已准备就绪,现在登录微信或退出登录后重新登录微信', 0)
|
||||
|
||||
@@ -400,6 +411,16 @@ export class KeyServiceMac {
|
||||
|
||||
const lines = String(stdout).split(/\r?\n/).map(x => x.trim()).filter(Boolean)
|
||||
if (!lines.length) throw new Error('elevated helper returned empty output')
|
||||
const joined = lines.join('\n')
|
||||
|
||||
if (joined.startsWith('WF_ERR::')) {
|
||||
const parts = joined.split('::')
|
||||
const errNum = parts[1] || 'unknown'
|
||||
const errMsg = parts[2] || 'unknown'
|
||||
const partial = parts.slice(3).join('::')
|
||||
throw new Error(`elevated helper failed: errNum=${errNum}, errMsg=${errMsg}, partial=${partial || '(empty)'}`)
|
||||
}
|
||||
const normalizedOutput = joined.startsWith('WF_OK::') ? joined.slice('WF_OK::'.length) : joined
|
||||
|
||||
// 从所有行里提取所有 JSON 对象(同一行可能有多个拼接),找含 key/result 的那个
|
||||
const extractJsonObjects = (s: string): any[] => {
|
||||
@@ -411,7 +432,7 @@ export class KeyServiceMac {
|
||||
}
|
||||
return results
|
||||
}
|
||||
const fullOutput = lines.join('\n')
|
||||
const fullOutput = normalizedOutput
|
||||
const allJson = extractJsonObjects(fullOutput)
|
||||
// 优先找 success=true && key 字段
|
||||
const successPayload = allJson.find(p => p?.success === true && typeof p?.key === 'string')
|
||||
|
||||
Reference in New Issue
Block a user