mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat: 解决了一些问题
This commit is contained in:
@@ -328,7 +328,7 @@ class ChatService {
|
||||
const cached = this.avatarCache.get(username)
|
||||
// 如果缓存有效且有头像,直接使用;如果没有头像,也需要重新尝试获取
|
||||
// 额外检查:如果头像是无效的 hex 格式(以 ffd8 开头),也需要重新获取
|
||||
const isValidAvatar = cached?.avatarUrl &&
|
||||
const isValidAvatar = cached?.avatarUrl &&
|
||||
!cached.avatarUrl.includes('base64,ffd8') // 检测错误的 hex 格式
|
||||
if (cached && now - cached.updatedAt < this.avatarCacheTtlMs && isValidAvatar) {
|
||||
result[username] = {
|
||||
@@ -970,7 +970,7 @@ class ChatService {
|
||||
const title = this.extractXmlValue(content, 'title')
|
||||
return title || '[引用消息]'
|
||||
case 266287972401:
|
||||
return '[拍一拍]'
|
||||
return this.cleanPatMessage(content)
|
||||
case 81604378673:
|
||||
return '[聊天记录]'
|
||||
case 8594229559345:
|
||||
@@ -1659,6 +1659,37 @@ class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理拍一拍消息
|
||||
* 格式示例: 我拍了拍 "梨绒" ງ໐໐໓ ຖiງht620000wxid_...
|
||||
*/
|
||||
private cleanPatMessage(content: string): string {
|
||||
if (!content) return '[拍一拍]'
|
||||
|
||||
// 1. 尝试匹配标准的 "A拍了拍B" 格式
|
||||
// 这里的正则比较宽泛,为了兼容不同的语言环境
|
||||
const match = /^(.+?拍了拍.+?)(?:[\r\n]|$|ງ|wxid_)/.exec(content)
|
||||
if (match) {
|
||||
return `[拍一拍] ${match[1].trim()}`
|
||||
}
|
||||
|
||||
// 2. 如果匹配失败,尝试清理掉疑似的 garbage (wxid, 乱码)
|
||||
let cleaned = content.replace(/wxid_[a-zA-Z0-9_-]+/g, '') // 移除 wxid
|
||||
cleaned = cleaned.replace(/[ງ໐໓ຖiht]+/g, ' ') // 移除已知的乱码字符
|
||||
cleaned = cleaned.replace(/\d{6,}/g, '') // 移除长数字
|
||||
cleaned = cleaned.replace(/\s+/g, ' ').trim() // 清理空格
|
||||
|
||||
// 移除不可见字符
|
||||
cleaned = this.cleanUtf16(cleaned)
|
||||
|
||||
// 如果清理后还有内容,返回
|
||||
if (cleaned && cleaned.length > 1 && !cleaned.includes('xml')) {
|
||||
return `[拍一拍] ${cleaned}`
|
||||
}
|
||||
|
||||
return '[拍一拍]'
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码消息内容(处理 BLOB 和压缩数据)
|
||||
*/
|
||||
@@ -2323,7 +2354,7 @@ class ChatService {
|
||||
/**
|
||||
* getVoiceData (绕过WCDB的buggy getVoiceData,直接用execQuery读取)
|
||||
*/
|
||||
async getVoiceData(sessionId: string, msgId: string, createTime?: number, serverId?: string | number): Promise<{ success: boolean; data?: string; error?: string }> {
|
||||
async getVoiceData(sessionId: string, msgId: string, createTime?: number, serverId?: string | number, senderWxidOpt?: string): Promise<{ success: boolean; data?: string; error?: string }> {
|
||||
const startTime = Date.now()
|
||||
try {
|
||||
const localId = parseInt(msgId, 10)
|
||||
@@ -2332,7 +2363,7 @@ class ChatService {
|
||||
}
|
||||
|
||||
let msgCreateTime = createTime
|
||||
let senderWxid: string | null = null
|
||||
let senderWxid: string | null = senderWxidOpt || null
|
||||
|
||||
// 如果前端没传 createTime,才需要查询消息(这个很慢)
|
||||
if (!msgCreateTime) {
|
||||
@@ -2403,7 +2434,7 @@ class ChatService {
|
||||
console.log(`[Voice] getVoiceDataFromMediaDb: ${t4 - t3}ms`)
|
||||
|
||||
if (!silkData) {
|
||||
return { success: false, error: '未找到语音数据' }
|
||||
return { success: false, error: '未找到语音数据 (请确保已在微信中播放过该语音)' }
|
||||
}
|
||||
|
||||
const t5 = Date.now()
|
||||
@@ -2471,11 +2502,20 @@ class ChatService {
|
||||
const t2 = Date.now()
|
||||
console.log(`[Voice] listMediaDbs: ${t2 - t1}ms`)
|
||||
|
||||
if (!mediaDbsResult.success || !mediaDbsResult.data || mediaDbsResult.data.length === 0) {
|
||||
let files = mediaDbsResult.success && mediaDbsResult.data ? (mediaDbsResult.data as string[]) : []
|
||||
|
||||
// Fallback: 如果 WCDB DLL 没找到,手动查找
|
||||
if (files.length === 0) {
|
||||
console.warn('[Voice] listMediaDbs returned empty, trying manual search')
|
||||
files = await this.findMediaDbsManually()
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error('[Voice] No media DBs found')
|
||||
return null
|
||||
}
|
||||
|
||||
mediaDbFiles = mediaDbsResult.data as string[]
|
||||
mediaDbFiles = files
|
||||
this.mediaDbsCache = mediaDbFiles // 永久缓存
|
||||
}
|
||||
|
||||
@@ -2854,7 +2894,8 @@ class ChatService {
|
||||
sessionId: string,
|
||||
msgId: string,
|
||||
createTime?: number,
|
||||
onPartial?: (text: string) => void
|
||||
onPartial?: (text: string) => void,
|
||||
senderWxid?: string
|
||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||
const startTime = Date.now()
|
||||
console.log(`[Transcribe] 开始转写: sessionId=${sessionId}, msgId=${msgId}, createTime=${createTime}`)
|
||||
@@ -2926,7 +2967,7 @@ class ChatService {
|
||||
console.log(`[Transcribe] WAV缓存未命中,调用 getVoiceData`)
|
||||
const t3 = Date.now()
|
||||
// 调用 getVoiceData 获取并解码
|
||||
const voiceResult = await this.getVoiceData(sessionId, msgId, msgCreateTime, serverId)
|
||||
const voiceResult = await this.getVoiceData(sessionId, msgId, msgCreateTime, serverId, senderWxid)
|
||||
const t4 = Date.now()
|
||||
console.log(`[Transcribe] getVoiceData: ${t4 - t3}ms, success=${voiceResult.success}`)
|
||||
|
||||
@@ -3098,7 +3139,7 @@ class ChatService {
|
||||
|
||||
private resolveAccountDir(dbPath: string, wxid: string): string | null {
|
||||
const normalized = dbPath.replace(/[\\\\/]+$/, '')
|
||||
|
||||
|
||||
// 如果 dbPath 本身指向 db_storage 目录下的文件(如某个 .db 文件)
|
||||
// 则向上回溯到账号目录
|
||||
if (basename(normalized).toLowerCase() === 'db_storage') {
|
||||
@@ -3108,14 +3149,14 @@ class ChatService {
|
||||
if (basename(dir).toLowerCase() === 'db_storage') {
|
||||
return dirname(dir)
|
||||
}
|
||||
|
||||
|
||||
// 否则,dbPath 应该是数据库根目录(如 xwechat_files)
|
||||
// 账号目录应该是 {dbPath}/{wxid}
|
||||
const accountDirWithWxid = join(normalized, wxid)
|
||||
if (existsSync(accountDirWithWxid)) {
|
||||
return accountDirWithWxid
|
||||
}
|
||||
|
||||
|
||||
// 兜底:返回 dbPath 本身(可能 dbPath 已经是账号目录)
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { wcdbService } from './wcdbService'
|
||||
import { imageDecryptService } from './imageDecryptService'
|
||||
import { chatService } from './chatService'
|
||||
import { videoService } from './videoService'
|
||||
import { voiceTranscribeService } from './voiceTranscribeService'
|
||||
import { EXPORT_HTML_STYLES } from './exportHtmlStyles'
|
||||
|
||||
// ChatLab 格式类型定义
|
||||
@@ -1032,15 +1033,15 @@ class ExportService {
|
||||
/**
|
||||
* 转写语音为文字
|
||||
*/
|
||||
private async transcribeVoice(sessionId: string, msgId: string): Promise<string> {
|
||||
private async transcribeVoice(sessionId: string, msgId: string, createTime: number, senderWxid: string | null): Promise<string> {
|
||||
try {
|
||||
const transcript = await chatService.getVoiceTranscript(sessionId, msgId)
|
||||
const transcript = await chatService.getVoiceTranscript(sessionId, msgId, createTime, undefined, senderWxid || undefined)
|
||||
if (transcript.success && transcript.transcript) {
|
||||
return `[语音转文字] ${transcript.transcript}`
|
||||
}
|
||||
return '[语音消息 - 转文字失败]'
|
||||
return `[语音消息 - 转文字失败: ${transcript.error || '未知错误'}]`
|
||||
} catch (e) {
|
||||
return '[语音消息 - 转文字失败]'
|
||||
return `[语音消息 - 转文字失败: ${String(e)}]`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1655,6 +1656,10 @@ class ExportService {
|
||||
phase: 'preparing'
|
||||
})
|
||||
|
||||
if (options.exportVoiceAsText) {
|
||||
await this.ensureVoiceModel(onProgress)
|
||||
}
|
||||
|
||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||
const allMessages = collected.rows
|
||||
if (isGroup) {
|
||||
@@ -1719,7 +1724,7 @@ class ExportService {
|
||||
// 并行转写语音,限制 4 个并发(转写比较耗资源)
|
||||
const VOICE_CONCURRENCY = 4
|
||||
await parallelLimit(voiceMessages, VOICE_CONCURRENCY, async (msg) => {
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId))
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId), msg.createTime, msg.senderUsername)
|
||||
voiceTranscriptMap.set(msg.localId, transcript)
|
||||
})
|
||||
}
|
||||
@@ -1849,6 +1854,10 @@ class ExportService {
|
||||
phase: 'preparing'
|
||||
})
|
||||
|
||||
if (options.exportVoiceAsText) {
|
||||
await this.ensureVoiceModel(onProgress)
|
||||
}
|
||||
|
||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
||||
|
||||
@@ -1904,7 +1913,7 @@ class ExportService {
|
||||
|
||||
const VOICE_CONCURRENCY = 4
|
||||
await parallelLimit(voiceMessages, VOICE_CONCURRENCY, async (msg) => {
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId))
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId), msg.createTime, msg.senderUsername)
|
||||
voiceTranscriptMap.set(msg.localId, transcript)
|
||||
})
|
||||
}
|
||||
@@ -2088,6 +2097,10 @@ class ExportService {
|
||||
phase: 'preparing'
|
||||
})
|
||||
|
||||
if (options.exportVoiceAsText) {
|
||||
await this.ensureVoiceModel(onProgress)
|
||||
}
|
||||
|
||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||
|
||||
|
||||
@@ -2202,11 +2215,11 @@ class ExportService {
|
||||
}
|
||||
|
||||
// 预加载群昵称 (仅群聊且完整列模式)
|
||||
console.log('🔍 预加载群昵称检查: isGroup=', isGroup, 'useCompactColumns=', useCompactColumns, 'sessionId=', sessionId)
|
||||
console.log('预加载群昵称检查: isGroup=', isGroup, 'useCompactColumns=', useCompactColumns, 'sessionId=', sessionId)
|
||||
const groupNicknamesMap = (isGroup && !useCompactColumns)
|
||||
? await this.getGroupNicknamesForRoom(sessionId)
|
||||
: new Map<string, string>()
|
||||
console.log('🔍 群昵称Map大小:', groupNicknamesMap.size)
|
||||
console.log('群昵称Map大小:', groupNicknamesMap.size)
|
||||
|
||||
|
||||
// 填充数据
|
||||
@@ -2267,7 +2280,7 @@ class ExportService {
|
||||
|
||||
const VOICE_CONCURRENCY = 4
|
||||
await parallelLimit(voiceMessages, VOICE_CONCURRENCY, async (msg) => {
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId))
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId), msg.createTime, msg.senderUsername)
|
||||
voiceTranscriptMap.set(msg.localId, transcript)
|
||||
})
|
||||
}
|
||||
@@ -2417,6 +2430,41 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保语音转写模型已下载
|
||||
*/
|
||||
private async ensureVoiceModel(onProgress?: (progress: ExportProgress) => void): Promise<boolean> {
|
||||
try {
|
||||
const status = await voiceTranscribeService.getModelStatus()
|
||||
if (status.success && status.exists) {
|
||||
return true
|
||||
}
|
||||
|
||||
onProgress?.({
|
||||
current: 0,
|
||||
total: 100,
|
||||
currentSession: '正在下载 AI 模型',
|
||||
phase: 'preparing'
|
||||
})
|
||||
|
||||
const downloadResult = await voiceTranscribeService.downloadModel((progress: any) => {
|
||||
if (progress.percent !== undefined) {
|
||||
onProgress?.({
|
||||
current: progress.percent,
|
||||
total: 100,
|
||||
currentSession: `正在下载 AI 模型 (${progress.percent.toFixed(0)}%)`,
|
||||
phase: 'preparing'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return downloadResult.success
|
||||
} catch (e) {
|
||||
console.error('Auto download model failed:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出单个会话为 TXT 格式(默认与 Excel 精简列一致)
|
||||
*/
|
||||
@@ -2442,6 +2490,10 @@ class ExportService {
|
||||
phase: 'preparing'
|
||||
})
|
||||
|
||||
if (options.exportVoiceAsText) {
|
||||
await this.ensureVoiceModel(onProgress)
|
||||
}
|
||||
|
||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||
const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime)
|
||||
|
||||
@@ -2495,7 +2547,7 @@ class ExportService {
|
||||
|
||||
const VOICE_CONCURRENCY = 4
|
||||
await parallelLimit(voiceMessages, VOICE_CONCURRENCY, async (msg) => {
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId))
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId), msg.createTime, msg.senderUsername)
|
||||
voiceTranscriptMap.set(msg.localId, transcript)
|
||||
})
|
||||
}
|
||||
@@ -2613,6 +2665,10 @@ class ExportService {
|
||||
phase: 'preparing'
|
||||
})
|
||||
|
||||
if (options.exportVoiceAsText) {
|
||||
await this.ensureVoiceModel(onProgress)
|
||||
}
|
||||
|
||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||
if (isGroup) {
|
||||
await this.mergeGroupMembers(sessionId, collected.memberSet, options.exportAvatars === true)
|
||||
@@ -2673,7 +2729,7 @@ class ExportService {
|
||||
|
||||
const VOICE_CONCURRENCY = 4
|
||||
await parallelLimit(voiceMessages, VOICE_CONCURRENCY, async (msg) => {
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId))
|
||||
const transcript = await this.transcribeVoice(sessionId, String(msg.localId), msg.createTime, msg.senderUsername)
|
||||
voiceTranscriptMap.set(msg.localId, transcript)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,25 @@ import { wcdbService } from './wcdbService'
|
||||
import { ConfigService } from './config'
|
||||
import { ContactCacheService } from './contactCacheService'
|
||||
|
||||
export interface SnsLivePhoto {
|
||||
url: string
|
||||
thumb: string
|
||||
md5?: string
|
||||
token?: string
|
||||
key?: string
|
||||
encIdx?: string
|
||||
}
|
||||
|
||||
export interface SnsMedia {
|
||||
url: string
|
||||
thumb: string
|
||||
md5?: string
|
||||
token?: string
|
||||
key?: string
|
||||
encIdx?: string
|
||||
livePhoto?: SnsLivePhoto
|
||||
}
|
||||
|
||||
export interface SnsPost {
|
||||
id: string
|
||||
username: string
|
||||
@@ -10,11 +29,25 @@ export interface SnsPost {
|
||||
createTime: number
|
||||
contentDesc: string
|
||||
type?: number
|
||||
media: { url: string; thumb: string }[]
|
||||
media: SnsMedia[]
|
||||
likes: string[]
|
||||
comments: { id: string; nickname: string; content: string; refCommentId: string; refNickname?: string }[]
|
||||
rawXml?: string // 原始 XML 数据
|
||||
}
|
||||
|
||||
const fixSnsUrl = (url: string, token?: string) => {
|
||||
if (!url) return url;
|
||||
|
||||
// 1. 统一使用 https
|
||||
// 2. 将 /150 (缩略图) 强制改为 /0 (原图)
|
||||
let fixedUrl = url.replace('http://', 'https://').replace(/\/150($|\?)/, '/0$1');
|
||||
|
||||
if (!token || fixedUrl.includes('token=')) return fixedUrl;
|
||||
|
||||
const connector = fixedUrl.includes('?') ? '&' : '?';
|
||||
return `${fixedUrl}${connector}token=${token}&idx=1`;
|
||||
};
|
||||
|
||||
class SnsService {
|
||||
private contactCache: ContactCacheService
|
||||
|
||||
@@ -35,14 +68,50 @@ class SnsService {
|
||||
})
|
||||
|
||||
if (result.success && result.timeline) {
|
||||
const enrichedTimeline = result.timeline.map((post: any) => {
|
||||
const enrichedTimeline = result.timeline.map((post: any, index: number) => {
|
||||
const contact = this.contactCache.get(post.username)
|
||||
|
||||
// 修复媒体 URL,如果是 http 则尝试用 https (虽然 qpic 可能不支持强制 https,但通常支持)
|
||||
const fixedMedia = post.media.map((m: any) => ({
|
||||
url: m.url.replace('http://', 'https://'),
|
||||
thumb: m.thumb.replace('http://', 'https://')
|
||||
}))
|
||||
// 修复媒体 URL
|
||||
const fixedMedia = post.media.map((m: any, mIdx: number) => {
|
||||
const base = {
|
||||
url: fixSnsUrl(m.url, m.token),
|
||||
thumb: fixSnsUrl(m.thumb, m.token),
|
||||
md5: m.md5,
|
||||
token: m.token,
|
||||
key: m.key,
|
||||
encIdx: m.encIdx || m.enc_idx, // 兼容不同命名
|
||||
livePhoto: m.livePhoto ? {
|
||||
...m.livePhoto,
|
||||
url: fixSnsUrl(m.livePhoto.url, m.livePhoto.token),
|
||||
thumb: fixSnsUrl(m.livePhoto.thumb, m.livePhoto.token),
|
||||
token: m.livePhoto.token,
|
||||
key: m.livePhoto.key
|
||||
} : undefined
|
||||
}
|
||||
|
||||
// [MOCK] 模拟数据:如果后端没返回 key (说明 DLL 未更新),注入一些 Mock 数据以便前端开发
|
||||
if (!base.key) {
|
||||
base.key = 'mock_key_for_dev'
|
||||
if (!base.token) {
|
||||
base.token = 'mock_token_for_dev'
|
||||
base.url = fixSnsUrl(base.url, base.token)
|
||||
base.thumb = fixSnsUrl(base.thumb, base.token)
|
||||
}
|
||||
base.encIdx = '1'
|
||||
|
||||
// 强制给第一个帖子的第一张图加 LivePhoto 模拟
|
||||
if (index === 0 && mIdx === 0 && !base.livePhoto) {
|
||||
base.livePhoto = {
|
||||
url: fixSnsUrl('https://tm.sh/d4cb0.mp4', 'mock_live_token'),
|
||||
thumb: base.thumb,
|
||||
token: 'mock_live_token',
|
||||
key: 'mock_live_key'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
})
|
||||
|
||||
return {
|
||||
...post,
|
||||
@@ -59,6 +128,128 @@ class SnsService {
|
||||
console.log('[SnsService] Returning result:', result)
|
||||
return result
|
||||
}
|
||||
async debugResource(url: string): Promise<{ success: boolean; status?: number; headers?: any; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const { app, net } = require('electron')
|
||||
// Remove mocking 'require' if it causes issues, but here we need 'net' or 'https'
|
||||
// implementing with 'https' for reliability if 'net' is main-process only special
|
||||
const https = require('https')
|
||||
const urlObj = new URL(url)
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090719) XWEB/8351",
|
||||
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Referer": "https://servicewechat.com/",
|
||||
"Connection": "keep-alive",
|
||||
"Range": "bytes=0-10" // Keep our range check
|
||||
}
|
||||
}
|
||||
|
||||
const req = https.request(options, (res: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
status: res.statusCode,
|
||||
headers: {
|
||||
'x-enc': res.headers['x-enc'],
|
||||
'content-length': res.headers['content-length'],
|
||||
'content-type': res.headers['content-type']
|
||||
}
|
||||
})
|
||||
req.destroy() // We only need headers
|
||||
})
|
||||
|
||||
req.on('error', (e: any) => {
|
||||
resolve({ success: false, error: e.message })
|
||||
})
|
||||
|
||||
req.end()
|
||||
} catch (e: any) {
|
||||
resolve({ success: false, error: e.message })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private imageCache = new Map<string, string>()
|
||||
|
||||
async proxyImage(url: string): Promise<{ success: boolean; dataUrl?: string; error?: string }> {
|
||||
// Check cache
|
||||
if (this.imageCache.has(url)) {
|
||||
return { success: true, dataUrl: this.imageCache.get(url) }
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const https = require('https')
|
||||
const zlib = require('zlib')
|
||||
const urlObj = new URL(url)
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090719) XWEB/8351",
|
||||
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Referer": "https://servicewechat.com/",
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
}
|
||||
|
||||
const req = https.request(options, (res: any) => {
|
||||
if (res.statusCode !== 200) {
|
||||
resolve({ success: false, error: `HTTP ${res.statusCode}` })
|
||||
return
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
let stream = res
|
||||
|
||||
// Handle gzip compression
|
||||
const encoding = res.headers['content-encoding']
|
||||
if (encoding === 'gzip') {
|
||||
stream = res.pipe(zlib.createGunzip())
|
||||
} else if (encoding === 'deflate') {
|
||||
stream = res.pipe(zlib.createInflate())
|
||||
} else if (encoding === 'br') {
|
||||
stream = res.pipe(zlib.createBrotliDecompress())
|
||||
}
|
||||
|
||||
stream.on('data', (chunk: Buffer) => chunks.push(chunk))
|
||||
stream.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks)
|
||||
const contentType = res.headers['content-type'] || 'image/jpeg'
|
||||
const base64 = buffer.toString('base64')
|
||||
const dataUrl = `data:${contentType};base64,${base64}`
|
||||
|
||||
// Cache
|
||||
this.imageCache.set(url, dataUrl)
|
||||
|
||||
resolve({ success: true, dataUrl })
|
||||
})
|
||||
stream.on('error', (e: any) => {
|
||||
resolve({ success: false, error: e.message })
|
||||
})
|
||||
})
|
||||
|
||||
req.on('error', (e: any) => {
|
||||
resolve({ success: false, error: e.message })
|
||||
})
|
||||
|
||||
req.end()
|
||||
} catch (e: any) {
|
||||
resolve({ success: false, error: e.message })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const snsService = new SnsService()
|
||||
|
||||
@@ -246,14 +246,15 @@ export class WcdbCore {
|
||||
|
||||
// InitProtection (Added for security)
|
||||
try {
|
||||
this.wcdbInitProtection = this.lib.func('bool InitProtection(const char* resourcePath)')
|
||||
const protectionOk = this.wcdbInitProtection(dllDir)
|
||||
if (!protectionOk) {
|
||||
console.error('Core security check failed')
|
||||
this.wcdbInitProtection = this.lib.func('int32 InitProtection(const char* resourcePath)')
|
||||
const protectionCode = this.wcdbInitProtection(dllDir)
|
||||
if (protectionCode !== 0) {
|
||||
console.error('Core security check failed:', protectionCode)
|
||||
lastDllInitError = `初始化失败,错误码: ${protectionCode}`
|
||||
return false
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('InitProtection symbol not found:', e)
|
||||
console.warn('InitProtection symbol not found or failed:', e)
|
||||
}
|
||||
|
||||
// 定义类型
|
||||
|
||||
Reference in New Issue
Block a user