mirror of
https://fastgit.cc/github.com/hicccc77/WeFlow
synced 2026-04-20 21:01:15 +08:00
Merge branch 'main' into fix/ai-insight-weibo-ui-cookie-modal-timeout
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import https from 'https'
|
import https from 'https'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
|
|
||||||
const WEIBO_TIMEOUT_MS = 10_000
|
const WEIBO_TIMEOUT_MS = 10_000
|
||||||
const WEIBO_MAX_POSTS = 5
|
const WEIBO_MAX_POSTS = 5
|
||||||
const WEIBO_CACHE_TTL_MS = 30 * 60 * 1000
|
const WEIBO_CACHE_TTL_MS = 30 * 60 * 1000
|
||||||
const WEIBO_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
|
const WEIBO_USER_AGENT =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
|
||||||
|
|
||||||
interface BrowserCookieEntry {
|
interface BrowserCookieEntry {
|
||||||
domain?: string
|
domain?: string
|
||||||
@@ -66,44 +67,50 @@ function requestJson<T>(url: string, options: { cookie: string; referer?: string
|
|||||||
try {
|
try {
|
||||||
urlObj = new URL(url)
|
urlObj = new URL(url)
|
||||||
} catch {
|
} catch {
|
||||||
reject(new Error(无效的微博请求地址:))
|
reject(new Error(`无效的微博请求地址:${url}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = https.request({
|
const req = https.request(
|
||||||
hostname: urlObj.hostname,
|
{
|
||||||
port: urlObj.port || 443,
|
hostname: urlObj.hostname,
|
||||||
path: urlObj.pathname + urlObj.search,
|
port: urlObj.port || 443,
|
||||||
method: 'GET',
|
path: urlObj.pathname + urlObj.search,
|
||||||
headers: {
|
method: 'GET',
|
||||||
Accept: 'application/json, text/plain, */*',
|
headers: {
|
||||||
Referer: options.referer || 'https://weibo.com',
|
Accept: 'application/json, text/plain, */*',
|
||||||
'User-Agent': WEIBO_USER_AGENT,
|
Referer: options.referer || 'https://weibo.com',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'User-Agent': WEIBO_USER_AGENT,
|
||||||
Cookie: options.cookie
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
Cookie: options.cookie
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let raw = ''
|
||||||
|
res.setEncoding('utf8')
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
raw += chunk
|
||||||
|
})
|
||||||
|
res.on('end', () => {
|
||||||
|
const statusCode = res.statusCode || 0
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
reject(new Error(`微博接口返回异常状态码 ${statusCode}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(raw) as T)
|
||||||
|
} catch {
|
||||||
|
reject(new Error('微博接口返回了非 JSON 响应'))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, (res) => {
|
)
|
||||||
let raw = ''
|
|
||||||
res.setEncoding('utf8')
|
|
||||||
res.on('data', (chunk) => { raw += chunk })
|
|
||||||
res.on('end', () => {
|
|
||||||
const statusCode = res.statusCode || 0
|
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
|
||||||
reject(new Error(微博接口返回异常状态码 ))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(raw) as T)
|
|
||||||
} catch {
|
|
||||||
reject(new Error('微博接口返回了非 JSON 响应'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
req.setTimeout(WEIBO_TIMEOUT_MS, () => {
|
req.setTimeout(WEIBO_TIMEOUT_MS, () => {
|
||||||
req.destroy()
|
req.destroy()
|
||||||
reject(new Error('微博请求超时'))
|
reject(new Error('微博请求超时'))
|
||||||
})
|
})
|
||||||
|
|
||||||
req.on('error', reject)
|
req.on('error', reject)
|
||||||
req.end()
|
req.end()
|
||||||
})
|
})
|
||||||
@@ -111,15 +118,21 @@ function requestJson<T>(url: string, options: { cookie: string; referer?: string
|
|||||||
|
|
||||||
function normalizeCookieArray(entries: BrowserCookieEntry[]): string {
|
function normalizeCookieArray(entries: BrowserCookieEntry[]): string {
|
||||||
const picked = new Map<string, string>()
|
const picked = new Map<string, string>()
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const name = String(entry?.name || '').trim()
|
const name = String(entry?.name || '').trim()
|
||||||
const value = String(entry?.value || '').trim()
|
const value = String(entry?.value || '').trim()
|
||||||
const domain = String(entry?.domain || '').trim().toLowerCase()
|
const domain = String(entry?.domain || '').trim().toLowerCase()
|
||||||
|
|
||||||
if (!name || !value) continue
|
if (!name || !value) continue
|
||||||
if (domain && !domain.includes('weibo.com') && !domain.includes('weibo.cn')) continue
|
if (domain && !domain.includes('weibo.com') && !domain.includes('weibo.cn')) continue
|
||||||
|
|
||||||
picked.set(name, value)
|
picked.set(name, value)
|
||||||
}
|
}
|
||||||
return Array.from(picked.entries()).map(([name, value]) => ${name}=).join('; ')
|
|
||||||
|
return Array.from(picked.entries())
|
||||||
|
.map(([name, value]) => `${name}=${value}`)
|
||||||
|
.join('; ')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeWeiboCookieInput(rawInput: string): string {
|
export function normalizeWeiboCookieInput(rawInput: string): string {
|
||||||
@@ -134,7 +147,9 @@ export function normalizeWeiboCookieInput(rawInput: string): string {
|
|||||||
throw new Error('Cookie JSON 中未找到可用的微博 Cookie 项')
|
throw new Error('Cookie JSON 中未找到可用的微博 Cookie 项')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof SyntaxError)) throw error
|
if (!(error instanceof SyntaxError)) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmed.replace(/^Cookie:\s*/i, '').trim()
|
return trimmed.replace(/^Cookie:\s*/i, '').trim()
|
||||||
@@ -164,13 +179,13 @@ function mergeRetweetText(item: Pick<WeiboWaterFallItem, 'text_raw' | 'retweeted
|
|||||||
const baseText = sanitizeWeiboText(item.text_raw || '')
|
const baseText = sanitizeWeiboText(item.text_raw || '')
|
||||||
const retweetText = sanitizeWeiboText(item.retweeted_status?.text_raw || '')
|
const retweetText = sanitizeWeiboText(item.retweeted_status?.text_raw || '')
|
||||||
if (!retweetText) return baseText
|
if (!retweetText) return baseText
|
||||||
if (!baseText || baseText === '转发微博') return 转发:
|
if (!baseText || baseText === '转发微博') return `转发:${retweetText}`
|
||||||
return ${baseText}\n\n转发内容:
|
return `${baseText}\n\n转发内容:${retweetText}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCacheKey(uid: string, count: number, cookie: string): string {
|
function buildCacheKey(uid: string, count: number, cookie: string): string {
|
||||||
const cookieHash = createHash('sha1').update(cookie).digest('hex')
|
const cookieHash = createHash('sha1').update(cookie).digest('hex')
|
||||||
return ${uid}::
|
return `${uid}:${count}:${cookieHash}`
|
||||||
}
|
}
|
||||||
|
|
||||||
class WeiboService {
|
class WeiboService {
|
||||||
@@ -180,12 +195,15 @@ class WeiboService {
|
|||||||
this.recentPostsCache.clear()
|
this.recentPostsCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateUid(uidInput: string, cookieInput: string): Promise<{ success: boolean; uid?: string; screenName?: string; error?: string }> {
|
async validateUid(
|
||||||
|
uidInput: string,
|
||||||
|
cookieInput: string
|
||||||
|
): Promise<{ success: boolean; uid?: string; screenName?: string; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const uid = normalizeWeiboUid(uidInput)
|
const uid = normalizeWeiboUid(uidInput)
|
||||||
const cookie = normalizeWeiboCookieInput(cookieInput)
|
const cookie = normalizeWeiboCookieInput(cookieInput)
|
||||||
if (!cookie) {
|
if (!cookie) {
|
||||||
return { success: true, uid }
|
return { success: false, error: '请先填写有效的微博 Cookie' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeline = await this.fetchTimeline(uid, cookie)
|
const timeline = await this.fetchTimeline(uid, cookie)
|
||||||
@@ -193,14 +211,25 @@ class WeiboService {
|
|||||||
if (!firstItem) {
|
if (!firstItem) {
|
||||||
return { success: false, error: '该微博账号暂无可读取的近期公开内容,或当前 Cookie 已失效' }
|
return { success: false, error: '该微博账号暂无可读取的近期公开内容,或当前 Cookie 已失效' }
|
||||||
}
|
}
|
||||||
const screenName = firstItem.user?.screen_name
|
|
||||||
return { success: true, uid, screenName }
|
return {
|
||||||
|
success: true,
|
||||||
|
uid,
|
||||||
|
screenName: firstItem.user?.screen_name
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: (error as Error).message || '微博 UID 校验失败' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message || '微博 UID 校验失败'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchRecentPosts(uidInput: string, cookieInput: string, requestedCount: number): Promise<WeiboRecentPost[]> {
|
async fetchRecentPosts(
|
||||||
|
uidInput: string,
|
||||||
|
cookieInput: string,
|
||||||
|
requestedCount: number
|
||||||
|
): Promise<WeiboRecentPost[]> {
|
||||||
const uid = normalizeWeiboUid(uidInput)
|
const uid = normalizeWeiboUid(uidInput)
|
||||||
const cookie = normalizeWeiboCookieInput(cookieInput)
|
const cookie = normalizeWeiboCookieInput(cookieInput)
|
||||||
if (!cookie) return []
|
if (!cookie) return []
|
||||||
@@ -209,7 +238,10 @@ class WeiboService {
|
|||||||
const cacheKey = buildCacheKey(uid, count, cookie)
|
const cacheKey = buildCacheKey(uid, count, cookie)
|
||||||
const cached = this.recentPostsCache.get(cacheKey)
|
const cached = this.recentPostsCache.get(cacheKey)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (cached && cached.expiresAt > now) return cached.posts
|
|
||||||
|
if (cached && cached.expiresAt > now) {
|
||||||
|
return cached.posts
|
||||||
|
}
|
||||||
|
|
||||||
const timeline = await this.fetchTimeline(uid, cookie)
|
const timeline = await this.fetchTimeline(uid, cookie)
|
||||||
const rawItems = Array.isArray(timeline.data?.list) ? timeline.data.list : []
|
const rawItems = Array.isArray(timeline.data?.list) ? timeline.data.list : []
|
||||||
@@ -217,6 +249,7 @@ class WeiboService {
|
|||||||
|
|
||||||
for (const item of rawItems) {
|
for (const item of rawItems) {
|
||||||
if (posts.length >= count) break
|
if (posts.length >= count) break
|
||||||
|
|
||||||
const id = String(item.idstr || item.id || '').trim()
|
const id = String(item.idstr || item.id || '').trim()
|
||||||
if (!id) continue
|
if (!id) continue
|
||||||
|
|
||||||
@@ -226,15 +259,17 @@ class WeiboService {
|
|||||||
const detail = await this.fetchDetail(id, cookie)
|
const detail = await this.fetchDetail(id, cookie)
|
||||||
text = mergeRetweetText(detail)
|
text = mergeRetweetText(detail)
|
||||||
} catch {
|
} catch {
|
||||||
|
// 长文补抓失败时回退到列表摘要
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text = sanitizeWeiboText(text)
|
text = sanitizeWeiboText(text)
|
||||||
if (!text) continue
|
if (!text) continue
|
||||||
|
|
||||||
posts.push({
|
posts.push({
|
||||||
id,
|
id,
|
||||||
createdAt: String(item.created_at || ''),
|
createdAt: String(item.created_at || ''),
|
||||||
url: https://m.weibo.cn/detail/,
|
url: `https://m.weibo.cn/detail/${id}`,
|
||||||
text,
|
text,
|
||||||
screenName: item.user?.screen_name
|
screenName: item.user?.screen_name
|
||||||
})
|
})
|
||||||
@@ -244,13 +279,17 @@ class WeiboService {
|
|||||||
expiresAt: now + WEIBO_CACHE_TTL_MS,
|
expiresAt: now + WEIBO_CACHE_TTL_MS,
|
||||||
posts
|
posts
|
||||||
})
|
})
|
||||||
|
|
||||||
return posts
|
return posts
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchTimeline(uid: string, cookie: string): Promise<WeiboWaterFallResponse> {
|
private fetchTimeline(uid: string, cookie: string): Promise<WeiboWaterFallResponse> {
|
||||||
return requestJson<WeiboWaterFallResponse>(
|
return requestJson<WeiboWaterFallResponse>(
|
||||||
https://weibo.com/ajax/profile/getWaterFallContent?uid=,
|
`https://weibo.com/ajax/profile/getWaterFallContent?uid=${encodeURIComponent(uid)}`,
|
||||||
{ cookie, referer: https://weibo.com/u/ }
|
{
|
||||||
|
cookie,
|
||||||
|
referer: `https://weibo.com/u/${encodeURIComponent(uid)}`
|
||||||
|
}
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
if (response.ok !== 1 || !Array.isArray(response.data?.list)) {
|
if (response.ok !== 1 || !Array.isArray(response.data?.list)) {
|
||||||
throw new Error('微博时间线获取失败,请检查 Cookie 是否仍然有效')
|
throw new Error('微博时间线获取失败,请检查 Cookie 是否仍然有效')
|
||||||
@@ -261,8 +300,11 @@ class WeiboService {
|
|||||||
|
|
||||||
private fetchDetail(id: string, cookie: string): Promise<WeiboStatusShowResponse> {
|
private fetchDetail(id: string, cookie: string): Promise<WeiboStatusShowResponse> {
|
||||||
return requestJson<WeiboStatusShowResponse>(
|
return requestJson<WeiboStatusShowResponse>(
|
||||||
https://weibo.com/ajax/statuses/show?id=&isGetLongText=true,
|
`https://weibo.com/ajax/statuses/show?id=${encodeURIComponent(id)}&isGetLongText=true`,
|
||||||
{ cookie, referer: https://weibo.com/detail/ }
|
{
|
||||||
|
cookie,
|
||||||
|
referer: `https://weibo.com/detail/${encodeURIComponent(id)}`
|
||||||
|
}
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
if (!response || (!response.id && !response.idstr)) {
|
if (!response || (!response.id && !response.idstr)) {
|
||||||
throw new Error('微博详情获取失败')
|
throw new Error('微博详情获取失败')
|
||||||
|
|||||||
Reference in New Issue
Block a user