mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 15:19:03 +08:00
Merge branch 'dev' of https://github.com/xunchahaha/WeFlow into dev
This commit is contained in:
@@ -137,6 +137,28 @@ function createWindow(options: { autoShow?: boolean } = {}) {
|
||||
win.loadFile(join(__dirname, '../dist/index.html'))
|
||||
}
|
||||
|
||||
// 拦截请求,修改 Referer 和 User-Agent 以通过微信 CDN 鉴权
|
||||
session.defaultSession.webRequest.onBeforeSendHeaders(
|
||||
{
|
||||
urls: [
|
||||
'*://*.qpic.cn/*',
|
||||
'*://*.qlogo.cn/*',
|
||||
'*://*.wechat.com/*',
|
||||
'*://*.weixin.qq.com/*'
|
||||
]
|
||||
},
|
||||
(details, callback) => {
|
||||
details.requestHeaders['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"
|
||||
details.requestHeaders['Accept'] = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
||||
details.requestHeaders['Accept-Encoding'] = "gzip, deflate, br"
|
||||
details.requestHeaders['Accept-Language'] = "zh-CN,zh;q=0.9"
|
||||
details.requestHeaders['Referer'] = "https://servicewechat.com/"
|
||||
details.requestHeaders['Connection'] = "keep-alive"
|
||||
details.requestHeaders['Range'] = "bytes=0-"
|
||||
callback({ cancel: false, requestHeaders: details.requestHeaders })
|
||||
}
|
||||
)
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
@@ -687,6 +709,14 @@ function registerIpcHandlers() {
|
||||
return snsService.getTimeline(limit, offset, usernames, keyword, startTime, endTime)
|
||||
})
|
||||
|
||||
ipcMain.handle('sns:debugResource', async (_, url: string) => {
|
||||
return snsService.debugResource(url)
|
||||
})
|
||||
|
||||
ipcMain.handle('sns:proxyImage', async (_, url: string) => {
|
||||
return snsService.proxyImage(url)
|
||||
})
|
||||
|
||||
// 私聊克隆
|
||||
|
||||
|
||||
|
||||
@@ -217,6 +217,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 朋友圈
|
||||
sns: {
|
||||
getTimeline: (limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) =>
|
||||
ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime)
|
||||
ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime),
|
||||
debugResource: (url: string) => ipcRenderer.invoke('sns:debugResource', url),
|
||||
proxyImage: (url: string) => ipcRenderer.invoke('sns:proxyImage', url)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user