type TextContent = { type: 'text'; content: string } type VoiceContent = { type: 'voice'; duration?: number } type LocationContent = { type: 'location' poiname?: string label?: string lat: number lng: number } type CardContent = { type: 'card'; username: string; nickname: string; avatarUrl?: string } type ShareContent = { type: 'share' title: string des?: string url: string appname?: string typeVal?: string } type ForwardedMessageItem = { messageType: number sender?: string sentAt?: string text: string nested?: ForwardedMessageItem[] } type ForwardBundleContent = { type: 'forwardBundle' title: string description?: string items: ForwardedMessageItem[] } type MiniProgramContent = { type: 'miniProgram' title: string description?: string appName?: string iconUrl?: string thumbMd5?: string thumbDatName?: string thumbDataUrl?: string } type RedPacketContent = { type: 'redPacket' title: string description?: string url?: string } type VoipContent = { type: 'voip'; duration?: number; status: string; roomType?: number } type ImageContent = { type: 'image' md5?: string datName?: string aeskey?: string encrypVer?: number } type VideoContent = { type: 'video' md5?: string newMd5?: string rawMd5?: string duration?: number width?: number height?: number } type StickerContent = { type: 'sticker' md5?: string url?: string thumbUrl?: string encryptUrl?: string aeskey?: string } type QuoteContent = { type: 'quote' title?: string content?: string sender?: string quotedContent?: string quotedSender?: string quotedType?: string quotedImageMd5?: string quotedImageDatName?: string } type SystemContent = { type: 'system' content: string raw?: string pat?: boolean recall?: { targetId?: string targetIds?: string[] replacement: string actor?: string sessionId?: string recallTime?: number } } type UnknownContent = { type: 'unknown'; raw: string; messageType?: string | number } export type ParsedContent = | TextContent | VoiceContent | LocationContent | CardContent | ShareContent | ForwardBundleContent | MiniProgramContent | RedPacketContent | VoipContent | ImageContent | VideoContent | StickerContent | QuoteContent | SystemContent | UnknownContent export function parseMessageContent(content: string, messageType: number): ParsedContent { if (!content || typeof content !== 'string') { return { type: 'unknown', raw: content || '' } } const normalized = content.trim() switch (messageType) { case 1: return { type: 'text', content: normalized } case 34: return { type: 'voice' } case 3: return parseImageMessage(normalized) case 42: return parseCardMessage(normalized) case 43: return parseVideoMessage(normalized) case 47: return parseStickerMessage(normalized) case 48: return parseLocationMessage(normalized) case 49: return parseShareMessage(normalized) case 50: return parseVoipMessage(normalized) case 10000: case 10002: return parseSystemMessage(normalized) default: return { type: 'unknown', raw: normalized, messageType } } } function parseVideoMessage(content: string): ParsedContent { const decoded = decodeXmlEntities(stripChatroomPrefix(content)) const md5 = normalizeMd5(extractXmlAttribute(decoded, 'videomsg', 'md5')) const newMd5 = normalizeMd5(extractXmlAttribute(decoded, 'videomsg', 'newmd5')) const rawMd5 = normalizeMd5(extractXmlAttribute(decoded, 'videomsg', 'rawmd5')) if (!md5 && !newMd5 && !rawMd5) return { type: 'unknown', raw: content } const duration = Number(extractXmlAttribute(decoded, 'videomsg', 'playlength')) || undefined const width = Number(extractXmlAttribute(decoded, 'videomsg', 'cdnthumbwidth')) || undefined const height = Number(extractXmlAttribute(decoded, 'videomsg', 'cdnthumbheight')) || undefined return { type: 'video', md5, newMd5, rawMd5, duration, width, height } } function parseSystemMessage(content: string): ParsedContent { const stripped = stripChatroomPrefix(content) const decoded = decodeXmlEntities(stripped) const recall = extractRecallMessage(decoded) if (recall) { return { type: 'system', content: recall.replacement, raw: content, recall } } const delChatroomMemberText = extractDelChatroomMemberText(decoded) if (delChatroomMemberText) { return { type: 'system', content: normalizeSystemText(delChatroomMemberText), raw: content } } const plainText = extractXmlNodeText(decoded, 'plain') || extractXmlNodeText(decoded, 'text') || extractXmlNodeText(decoded, 'title') || extractXmlValue(decoded, 'plain') || extractXmlValue(decoded, 'text') || extractXmlValue(decoded, 'title') || '' const normalized = normalizeSystemText(plainText || fallbackSystemText(decoded)) return { type: 'system', content: normalized || '[系统消息]', raw: content } } function extractRecallMessage(xml: string): | { targetId?: string targetIds?: string[] replacement: string actor?: string sessionId?: string recallTime?: number } | undefined { if (!/ let md5 = extractXmlAttribute(content, 'img', 'md5') || extractXmlValue(content, 'md5') || '' let aeskey = extractXmlAttribute(content, 'img', 'aeskey') || extractXmlValue(content, 'aeskey') || undefined const encrypVerStr = extractXmlAttribute(content, 'img', 'encrypver') || extractXmlValue(content, 'encrypver') || '0' let datName = '' // 如果 XML 格式解析失败,尝试 JSON 格式 if (!md5) { try { const json = JSON.parse(content) // 可能是引用消息格式 { type: "...", content: "md5", ... } if ( json.content && typeof json.content === 'string' && /^[a-f0-9]{32}$/i.test(json.content) ) { md5 = json.content } else if (json.md5 && typeof json.md5 === 'string') { md5 = json.md5 } if (json.datName && typeof json.datName === 'string') { datName = json.datName } if (json.imageDatName && typeof json.imageDatName === 'string') { datName = json.imageDatName } // 尝试从其他字段获取 aeskey if (!aeskey && json.aeskey) { aeskey = json.aeskey } if (!aeskey && json.aeskey_v2) { aeskey = json.aeskey_v2 } } catch { // 不是 JSON 格式 } } const encrypVer = parseInt(encrypVerStr, 10) if (!md5 && !datName) { return { type: 'unknown', raw: content } } return { type: 'image', md5: md5 || undefined, datName: datName || undefined, aeskey, encrypVer } } function parseStickerMessage(content: string): ParsedContent { // 表情包消息可能包含 md5 或 url const md5 = extractXmlAttribute(content, 'emoji', 'md5') || extractXmlValue(content, 'md5') || extractXmlAttribute(content, 'sticker', 'md5') || extractLooseHexMd5(content) || '' const url = decodeXmlUrl( extractXmlValue(content, 'url') || extractXmlAttribute(content, 'emoji', 'cdnurl') || extractXmlAttribute(content, 'emoji', 'url') || extractXmlAttribute(content, 'emoji', 'thumburl') || extractLooseAttribute(content, 'cdnurl') || extractLooseAttribute(content, 'url') || extractLooseAttribute(content, 'thumburl') || '' ) const thumbUrl = decodeXmlUrl( extractXmlAttribute(content, 'emoji', 'thumburl') || extractLooseAttribute(content, 'thumburl') ) const encryptUrl = decodeXmlUrl( extractXmlAttribute(content, 'emoji', 'encrypturl') || extractLooseAttribute(content, 'encrypturl') ) const aeskey = extractXmlAttribute(content, 'emoji', 'aeskey') || extractLooseAttribute(content, 'aeskey') || undefined if (!md5 && !url && !thumbUrl && !encryptUrl) { return { type: 'unknown', raw: content } } return { type: 'sticker', md5, url: url || thumbUrl || undefined, thumbUrl: thumbUrl || undefined, encryptUrl: encryptUrl || undefined, aeskey } } export function parseStickerMessageFromRow( row: Record, content: string ): ParsedContent { const supplementalPayload = [ content, pickRowString(row, ['emoji_md5', 'emojiMd5', 'md5']), pickRowString(row, ['emoji_cdn_url', 'emojiCdnUrl', 'cdnurl', 'emoji_url', 'emojiUrl']), decodeSupplementalPayload( pickRowString(row, [ 'packed_info_data', 'packed_info', 'packedInfoData', 'packedInfo', 'PackedInfoData', 'PackedInfo', 'WCDB_CT_packed_info_data', 'WCDB_CT_packed_info' ]) ), decodeSupplementalPayload(pickRowString(row, ['reserved0', 'Reserved0', 'WCDB_CT_reserved0'])) ] .filter(Boolean) .join('\n') const directMd5 = normalizeMd5(pickRowString(row, ['emoji_md5', 'emojiMd5', 'md5'])) const directUrl = decodeXmlUrl( String( pickRowString(row, ['emoji_cdn_url', 'emojiCdnUrl', 'cdnurl', 'emoji_url', 'emojiUrl']) || '' ) ) const parsed = parseStickerMessage(supplementalPayload) if (parsed.type === 'sticker') { return { ...parsed, md5: parsed.md5 || directMd5, url: parsed.url || directUrl || undefined } } if (directMd5 || directUrl) { return { type: 'sticker', md5: directMd5, url: directUrl || undefined } } return parsed } function parseCardMessage(content: string): ParsedContent { const username = extractXmlValue(content, 'username') || extractXmlValue(content, 'cardUsername') || '' const nickname = extractXmlValue(content, 'nickname') || extractXmlValue(content, 'cardNickname') || '' const avatarUrl = extractXmlValue(content, 'avatarUrl') || extractXmlValue(content, 'smallHeadImgUrl') || undefined if (!username && !nickname) { return { type: 'unknown', raw: content } } return { type: 'card', username, nickname, avatarUrl } } function parseLocationMessage(content: string): ParsedContent { const poiname = extractXmlValue(content, 'poiname') || extractXmlValue(content, 'poiName') || '' const label = extractXmlValue(content, 'label') || '' const latStr = extractXmlAttribute(content, 'location', 'x') || extractXmlAttribute(content, 'location', 'latitude') || '0' const lngStr = extractXmlAttribute(content, 'location', 'y') || extractXmlAttribute(content, 'location', 'longitude') || '0' const lat = parseFloat(latStr) const lng = parseFloat(lngStr) if (!poiname && lat === 0 && lng === 0) { return { type: 'unknown', raw: content } } return { type: 'location', poiname, label, lat, lng } } function parseShareMessage(content: string): ParsedContent { const appMsgType = extractAppMsgType(content) const isFileMessage = appMsgType === '6' || appMsgType === '74' if (appMsgType === '19') { return parseForwardBundle(content) } if (!isFileMessage && / 0) return forwardBundle } if (appMsgType === '47' || /<(?:emoji|sticker|emoticon)\b/i.test(content)) { const sticker = parseStickerMessage(content) if (sticker.type === 'sticker') return sticker } if (appMsgType === '57' || content.includes('')) { const quote = parseQuoteMessage(content) const title = extractXmlValue(content, 'title') || undefined return { type: 'quote', title, content: title, quotedContent: quote.content || '[引用消息]', quotedSender: quote.sender, quotedType: quote.type, quotedImageMd5: quote.imageMd5, quotedImageDatName: quote.imageDatName } } if (appMsgType === '33' || appMsgType === '36') { return { type: 'miniProgram', title: extractXmlValue(content, 'title') || '小程序', description: extractXmlValue(content, 'des') || undefined, appName: extractXmlValue(content, 'sourcedisplayname') || extractXmlValue(content, 'appname') || '小程序', iconUrl: decodeXmlUrl(extractXmlValue(content, 'weappiconurl')) || undefined, thumbMd5: normalizeMd5( extractXmlValue(content, 'cdnthumbmd5') || extractXmlValue(content, 'md5') ) } } if (appMsgType === '2001') { return { type: 'redPacket', title: extractXmlValue(content, 'title') || '微信红包', description: extractXmlValue(content, 'des') || '恭喜发财,大吉大利', url: decodeXmlUrl(extractXmlValue(content, 'url')) || undefined } } const title = extractXmlValue(content, 'title') || '' const des = extractXmlValue(content, 'des') || extractXmlValue(content, 'desc') || '' const url = extractXmlValue(content, 'url') || '' const appname = extractXmlValue(content, 'appname') || extractXmlValue(content, 'appInfo') || '' const typeVal = extractXmlValue(content, 'type') || '' if (!title && !url) { return { type: 'unknown', raw: content } } return { type: 'share', title, des, url, appname, typeVal } } function parseForwardBundle(content: string): ForwardBundleContent { const normalized = decodeXmlEntities(stripChatroomPrefix(content)) const title = decodeXmlEntities(extractXmlValue(normalized, 'title')) || '聊天记录' const description = decodeXmlEntities(extractXmlValue(normalized, 'des')) || undefined const containers = Array.from( normalized.matchAll(/]*>([\s\S]*?)<\/recorditem>/gi), (match) => match[1] || '' ) const sources = containers.length ? containers : [normalized] const items = dedupeForwardedItems(sources.flatMap((source) => parseForwardedItems(source))) return { type: 'forwardBundle', title, description, items } } function parseForwardedItems(container: string, depth = 0): ForwardedMessageItem[] { if (!container || depth > 4) return [] const variants = new Set([container, decodeXmlEntities(container)]) for (const match of container.matchAll(//g)) { if (match[1]) variants.add(decodeXmlEntities(match[1])) } const items: ForwardedMessageItem[] = [] for (const variant of variants) { for (const match of variant.matchAll(/]*)>([\s\S]*?)<\/dataitem>/gi)) { const attributes = match[1] || '' const body = match[2] || '' const attrType = /datatype\s*=\s*["']?(\d+)/i.exec(attributes)?.[1] const messageType = Number.parseInt(attrType || extractXmlValue(body, 'datatype') || '0', 10) const sender = decodeXmlEntities(extractXmlValue(body, 'sourcename')) || undefined const sentAt = extractXmlValue(body, 'sourcetime') || undefined const title = decodeXmlEntities(extractXmlValue(body, 'datatitle')) const description = decodeXmlEntities( extractXmlValue(body, 'datadesc') || extractXmlValue(body, 'content') ) const nestedXml = extractXmlBody(body, 'recordxml') const nested = messageType === 17 && nestedXml ? parseForwardedItems(decodeXmlEntities(nestedXml), depth + 1) : undefined const text = description || title || forwardedTypeLabel(messageType) if (!sender && !text && !nested?.length) continue items.push({ messageType: Number.isFinite(messageType) ? messageType : 0, sender, sentAt, text: text || '[消息]', nested: nested?.length ? nested : undefined }) } } return dedupeForwardedItems(items) } function dedupeForwardedItems(items: ForwardedMessageItem[]): ForwardedMessageItem[] { const seen = new Set() return items.filter((item) => { const key = `${item.messageType}|${item.sender || ''}|${item.sentAt || ''}|${item.text}` if (seen.has(key)) return false seen.add(key) return true }) } function forwardedTypeLabel(messageType: number): string { switch (messageType) { case 3: return '[图片]' case 34: return '[语音]' case 43: return '[视频]' case 47: return '[表情包]' case 8: case 49: return '[文件或分享]' case 17: return '[聊天记录]' default: return '[消息]' } } function extractXmlBody(xml: string, tagName: string): string { const match = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i').exec(xml) if (!match?.[1]) return '' return match[1].replace(/^$/, '$1').trim() } function parseQuoteMessage(content: string): { content?: string sender?: string type?: string imageMd5?: string imageDatName?: string } { const referMsgStart = content.indexOf('') const referMsgEnd = content.indexOf('') if (referMsgStart === -1 || referMsgEnd === -1) return {} const referMsgXml = content.substring(referMsgStart, referMsgEnd + ''.length) const displayName = sanitizeQuotedContent(extractXmlValue(referMsgXml, 'displayname')) const chatUser = sanitizeQuotedSenderId(extractXmlValue(referMsgXml, 'chatusr')) const fromUser = sanitizeQuotedSenderId(extractXmlValue(referMsgXml, 'fromusr')) const sender = displayName || chatUser || fromUser || undefined const referContent = extractXmlValue(referMsgXml, 'content') const referType = extractXmlValue(referMsgXml, 'type') switch (referType) { case '1': return { sender, content: sanitizeQuotedContent(referContent), type: referType } case '3': { const image = parseImageMessage(referContent) return { sender, content: '[图片]', type: referType, imageMd5: image.type === 'image' ? image.md5 : undefined, imageDatName: image.type === 'image' ? image.datName : undefined } } case '34': return { sender, content: '[语音]', type: referType } case '43': return { sender, content: '[视频]', type: referType } case '47': return { sender, content: '[表情]', type: referType } case '49': return { sender, content: extractXmlValue(referMsgXml, 'title') || '[分享消息]', type: referType } default: return { sender, content: sanitizeQuotedContent(referContent) || '[引用消息]', type: referType } } } function extractAppMsgType(content: string): string { const appmsgMatch = /([\s\S]*?)<\/appmsg>/i.exec(content) if (!appmsgMatch) return extractXmlValue(content, 'type') const inner = appmsgMatch[1] .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') const typeMatch = /([\s\S]*?)<\/type>/i.exec(inner) return typeMatch?.[1]?.trim() || '' } function sanitizeQuotedContent(content: string): string { const decoded = String(content || '') .replace(/^wxid_[^:\n]+:\s*/i, '') .trim() if (/^(wxid_[\w-]+|[a-z][a-z0-9_-]{5,})$/i.test(decoded)) return '' return decoded } function sanitizeQuotedSenderId(value: string): string { return decodeXmlEntities(String(value || '')).trim() } function stripChatroomPrefix(content: string): string { return String(content || '') .replace(/^[0-9a-z_-]+@chatroom:\s*/i, '') .replace(/^wxid_[^:\n]+:\s*/i, '') .trim() } function normalizeSystemText(content: string): string { return String(content || '') .replace(/\s+/g, ' ') .replace(/\s+([,.;!?])/g, '$1') .trim() } function parseVoipMessage(content: string): ParsedContent { const roomTypeStr = extractXmlValue(content, 'room_type') const msg = extractXmlValue(content, 'msg') || '' const durationStr = extractXmlValue(content, 'duration') || '0' const roomType = roomTypeStr ? parseInt(roomTypeStr, 10) : 0 const duration = parseInt(durationStr, 10) let status = msg if (!status) { status = roomType === 1 ? '[视频通话]' : '[语音通话]' } return { type: 'voip', duration, status, roomType } } function extractXmlValue(xml: string, tagName: string): string { const patterns = [ new RegExp(`<${tagName}[^>]*>`, 'i'), new RegExp(`<${tagName}[^>]*>`, 'i'), new RegExp(`<${tagName}[^>]*>([^<]*)`, 'i'), new RegExp(`${tagName}=["']([^"']*)["']`, 'i') ] for (const pattern of patterns) { const match = xml.match(pattern) if (match && match[1]) { return match[1].trim() } } return '' } function extractXmlAttribute(xml: string, tagName: string, attrName: string): string { const pattern = new RegExp( `<${tagName}\\b[^>]*?(?:\\s|^)${attrName}\\s*=\\s*["']([^"']*)["']`, 'i' ) const match = xml.match(pattern) return match ? match[1].trim() : '' } function extractLooseAttribute(content: string, attrName: string): string { const quoted = new RegExp(`${attrName}\\s*=\\s*["']([^"']+)["']`, 'i').exec(content) if (quoted?.[1]) return quoted[1].trim() const unquoted = new RegExp(`${attrName}\\s*=\\s*([^"']+?)(?=\\s|/|>)`, 'i').exec(content) return unquoted?.[1]?.trim() || '' } function decodeXmlUrl(value: string): string { const normalized = String(value || '') .replace(/&/g, '&') .trim() if (!normalized) return '' if (!normalized.includes('%')) return normalized try { return decodeURIComponent(normalized) } catch { return normalized } } function decodeXmlEntities(value: string): string { return String(value || '') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/&/g, '&') } function extractXmlNodeText(xml: string, tagName: string): string { const match = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i').exec(xml) if (!match?.[1]) return '' return normalizeSystemText( decodeXmlEntities( match[1].replace(//g, '$1').replace(/<[^>]+>/g, ' ') ) ) } function fallbackSystemText(xml: string): string { return normalizeSystemText( decodeXmlEntities( String(xml || '') .replace(//g, '$1') .replace(/<[^>]+>/g, ' ') ) ) } function extractDelChatroomMemberText(xml: string): string { if (!/]+delchatroommember/i.test(xml)) return '' const plainMatch = /]*><\/plain>/i.exec(xml) if (plainMatch?.[1]) return plainMatch[1].trim() const textMatch = /]*><\/text>/i.exec(xml) if (textMatch?.[1]) return textMatch[1].trim() return '' } function normalizeMd5(value: unknown): string | undefined { const md5 = String(value || '') .trim() .toLowerCase() return /^[a-f0-9]{32}$/.test(md5) ? md5 : undefined } function extractLooseHexMd5(content: string): string | undefined { if (!content) return undefined const match = /(?:emoji|sticker|md5)[^a-fA-F0-9]{0,32}([a-fA-F0-9]{32})/i.exec(content) || /([a-fA-F0-9]{32})/i.exec(content) return normalizeMd5(match?.[1] || match?.[0]) } function decodeSupplementalPayload(raw: unknown): string { if (!raw) return '' if (typeof raw === 'string' && !/^[a-fA-F0-9]+$/.test(raw.trim())) return raw.trim() const buffer = decodePackedInfo(raw) if (!buffer || buffer.length === 0) return '' const decoded = buffer.toString('utf-8') const replacementCount = (decoded.match(/\uFFFD/g) || []).length if (replacementCount < decoded.length * 0.2) { return decoded.replace(/\uFFFD/g, '') } return Array.from(buffer) .map((byte) => (byte >= 0x20 && byte <= 0x7e ? String.fromCharCode(byte) : ' ')) .join('') } export function parseImageDatNameFromRow(row: Record): string | undefined { const packed = pickRowString(row, [ 'packed_info_data', 'packed_info', 'packedInfoData', 'packedInfo', 'PackedInfoData', 'PackedInfo', 'WCDB_CT_packed_info_data', 'WCDB_CT_packed_info', 'WCDB_CT_PackedInfoData', 'WCDB_CT_PackedInfo' ]) const buffer = decodePackedInfo(packed) if (!buffer || buffer.length === 0) return undefined const printable = Array.from(buffer).map((byte) => (byte >= 0x20 && byte <= 0x7e ? byte : 0x20)) const text = Buffer.from(printable).toString('utf-8') const match = /([0-9a-fA-F]{8,})(?:\.t)?\.dat/.exec(text) if (match?.[1]) return match[1].toLowerCase() const hexMatch = /([0-9a-fA-F]{16,})/.exec(text) return hexMatch?.[1]?.toLowerCase() } export function parseImageBufferDataUrlFromRow(row: Record): string | undefined { const raw = pickRowString(row, [ 'ImgBuf', 'imgBuf', 'img_buf', 'imageBuffer', 'image_buffer', 'thumbBuffer', 'thumb_buffer', 'WCDB_CT_img_buf', 'WCDB_CT_ImgBuf' ]) const buffer = decodeInlineImageBuffer(raw) if (!buffer || buffer.length === 0) return undefined const mime = detectImageMime(buffer) return mime ? `data:${mime};base64,${buffer.toString('base64')}` : undefined } function decodeInlineImageBuffer(raw: unknown): Buffer | null { if (!raw) return null if (Buffer.isBuffer(raw)) return raw if (raw instanceof Uint8Array) return Buffer.from(raw) if (Array.isArray(raw)) return Buffer.from(raw) if (typeof raw === 'object') { const record = raw as { buffer?: unknown; data?: unknown } return decodeInlineImageBuffer(record.buffer ?? record.data) } if (typeof raw !== 'string') return null const value = raw.trim() const dataUrl = /^data:image\/[a-z0-9.+-]+;base64,(.+)$/i.exec(value) const encoded = dataUrl?.[1] || value if (!/^[a-z0-9+/]+={0,2}$/i.test(encoded)) return null try { return Buffer.from(encoded, 'base64') } catch { return null } } function detectImageMime(buffer: Buffer): string | undefined { if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { return 'image/jpeg' } if ( buffer.length >= 8 && buffer[0] === 0x89 && buffer.subarray(1, 4).toString('ascii') === 'PNG' ) { return 'image/png' } if (buffer.length >= 6 && /^GIF8[79]a$/.test(buffer.subarray(0, 6).toString('ascii'))) { return 'image/gif' } if ( buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP' ) { return 'image/webp' } return undefined } function pickRowString(row: Record, keys: string[]): unknown { for (const key of keys) { if (Object.prototype.hasOwnProperty.call(row, key)) return row[key] const foundKey = Object.keys(row).find( (candidate) => candidate.toLowerCase() === key.toLowerCase() ) if (foundKey) return row[foundKey] } return undefined } function decodePackedInfo(raw: unknown): Buffer | null { if (!raw) return null if (Buffer.isBuffer(raw)) return raw if (raw instanceof Uint8Array) return Buffer.from(raw) if (Array.isArray(raw)) return Buffer.from(raw) if (typeof raw === 'string') { const trimmed = raw.trim() if (/^[a-fA-F0-9]+$/.test(trimmed) && trimmed.length % 2 === 0) { try { return Buffer.from(trimmed, 'hex') } catch { // Try base64 below. } } try { return Buffer.from(trimmed, 'base64') } catch { // Unsupported packed_info encoding. } } if (typeof raw === 'object' && raw && Array.isArray((raw as { data?: unknown }).data)) { return Buffer.from((raw as { data: number[] }).data) } return null }