mirror of
https://fastgit.cc/github.com/hicccc77/WeFlow
synced 2026-04-20 21:01:15 +08:00
Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import './preload-env'
|
||||
import './preload-env'
|
||||
import { app, BrowserWindow, ipcMain, nativeTheme, session, Tray, Menu, nativeImage } from 'electron'
|
||||
import { Worker } from 'worker_threads'
|
||||
import { randomUUID } from 'crypto'
|
||||
@@ -31,6 +31,7 @@ import { destroyNotificationWindow, registerNotificationHandlers, showNotificati
|
||||
import { httpService } from './services/httpService'
|
||||
import { messagePushService } from './services/messagePushService'
|
||||
import { insightService } from './services/insightService'
|
||||
import { normalizeWeiboCookieInput, weiboService } from './services/social/weiboService'
|
||||
import { bizService } from './services/bizService'
|
||||
|
||||
// 配置自动更新
|
||||
@@ -1660,6 +1661,32 @@ function registerIpcHandlers() {
|
||||
return insightService.generateFootprintInsight(payload)
|
||||
})
|
||||
|
||||
ipcMain.handle('social:saveWeiboCookie', async (_, rawInput: string) => {
|
||||
try {
|
||||
if (!configService) {
|
||||
return { success: false, error: 'Config service is not initialized' }
|
||||
}
|
||||
const normalized = normalizeWeiboCookieInput(rawInput)
|
||||
configService.set('aiInsightWeiboCookie' as any, normalized as any)
|
||||
weiboService.clearCache()
|
||||
return { success: true, normalized, hasCookie: Boolean(normalized) }
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message || 'Failed to save Weibo cookie' }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('social:validateWeiboUid', async (_, uid: string) => {
|
||||
try {
|
||||
if (!configService) {
|
||||
return { success: false, error: 'Config service is not initialized' }
|
||||
}
|
||||
const cookie = String(configService.get('aiInsightWeiboCookie' as any) || '')
|
||||
return await weiboService.validateUid(uid, cookie)
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message || 'Failed to validate Weibo UID' }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('config:clear', async () => {
|
||||
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
|
||||
const result = setSystemLaunchAtStartup(false)
|
||||
@@ -3866,3 +3893,7 @@ app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
// 暴露给渲染进程的 API
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
@@ -557,5 +557,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
privateSegments?: Array<{ displayName?: string; session_id?: string; incoming_count?: number; outgoing_count?: number; message_count?: number; replied?: boolean }>
|
||||
mentionGroups?: Array<{ displayName?: string; session_id?: string; count?: number }>
|
||||
}) => ipcRenderer.invoke('insight:generateFootprintInsight', payload)
|
||||
},
|
||||
|
||||
social: {
|
||||
saveWeiboCookie: (rawInput: string) => ipcRenderer.invoke('social:saveWeiboCookie', rawInput),
|
||||
validateWeiboUid: (uid: string) => ipcRenderer.invoke('social:validateWeiboUid', uid)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { join } from 'path'
|
||||
import { join } from 'path'
|
||||
import { app, safeStorage } from 'electron'
|
||||
import crypto from 'crypto'
|
||||
import Store from 'electron-store'
|
||||
@@ -82,6 +82,7 @@ interface ConfigSchema {
|
||||
aiInsightApiModel: string
|
||||
aiInsightSilenceDays: number
|
||||
aiInsightAllowContext: boolean
|
||||
aiInsightAllowSocialContext: boolean
|
||||
aiInsightWhitelistEnabled: boolean
|
||||
aiInsightWhitelist: string[]
|
||||
/** 活跃分析冷却时间(分钟),0 表示无冷却 */
|
||||
@@ -113,7 +114,8 @@ const ENCRYPTED_STRING_KEYS: Set<string> = new Set([
|
||||
'authPassword',
|
||||
'httpApiToken',
|
||||
'aiModelApiKey',
|
||||
'aiInsightApiKey'
|
||||
'aiInsightApiKey',
|
||||
'aiInsightWeiboCookie'
|
||||
])
|
||||
const ENCRYPTED_BOOL_KEYS: Set<string> = new Set(['authEnabled', 'authUseHello'])
|
||||
const ENCRYPTED_NUMBER_KEYS: Set<string> = new Set(['imageXorKey'])
|
||||
@@ -196,15 +198,19 @@ export class ConfigService {
|
||||
aiInsightApiModel: 'gpt-4o-mini',
|
||||
aiInsightSilenceDays: 3,
|
||||
aiInsightAllowContext: false,
|
||||
aiInsightAllowSocialContext: false,
|
||||
aiInsightWhitelistEnabled: false,
|
||||
aiInsightWhitelist: [],
|
||||
aiInsightCooldownMinutes: 120,
|
||||
aiInsightScanIntervalHours: 4,
|
||||
aiInsightContextCount: 40,
|
||||
aiInsightSocialContextCount: 3,
|
||||
aiInsightSystemPrompt: '',
|
||||
aiInsightTelegramEnabled: false,
|
||||
aiInsightTelegramToken: '',
|
||||
aiInsightTelegramChatIds: '',
|
||||
aiInsightWeiboCookie: '',
|
||||
aiInsightWeiboBindings: {},
|
||||
aiFootprintEnabled: false,
|
||||
aiFootprintSystemPrompt: '',
|
||||
aiInsightDebugLogEnabled: false
|
||||
@@ -825,3 +831,4 @@ export class ConfigService {
|
||||
this.unlockPassword = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* insightService.ts
|
||||
*
|
||||
* AI 见解后台服务:
|
||||
@@ -21,6 +21,7 @@ import { URL } from 'url'
|
||||
import { app, Notification } from 'electron'
|
||||
import { ConfigService } from './config'
|
||||
import { chatService, ChatSession, Message } from './chatService'
|
||||
import { weiboService } from './social/weiboService'
|
||||
|
||||
// ─── 常量 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -46,6 +47,10 @@ const INSIGHT_CONFIG_KEYS = new Set([
|
||||
'aiModelApiBaseUrl',
|
||||
'aiModelApiKey',
|
||||
'aiModelApiModel',
|
||||
'aiInsightAllowSocialContext',
|
||||
'aiInsightSocialContextCount',
|
||||
'aiInsightWeiboCookie',
|
||||
'aiInsightWeiboBindings',
|
||||
'dbPath',
|
||||
'decryptKey',
|
||||
'myWxid'
|
||||
@@ -318,6 +323,10 @@ class InsightService {
|
||||
if (!INSIGHT_CONFIG_KEYS.has(normalizedKey)) return
|
||||
|
||||
// 数据库相关配置变更后,丢弃缓存并强制下次重连
|
||||
if (normalizedKey === 'aiInsightAllowSocialContext' || normalizedKey === 'aiInsightSocialContextCount' || normalizedKey === 'aiInsightWeiboCookie' || normalizedKey === 'aiInsightWeiboBindings') {
|
||||
weiboService.clearCache()
|
||||
}
|
||||
|
||||
if (normalizedKey === 'dbPath' || normalizedKey === 'decryptKey' || normalizedKey === 'myWxid') {
|
||||
this.clearRuntimeCache()
|
||||
}
|
||||
@@ -350,6 +359,7 @@ class InsightService {
|
||||
this.lastSeenTimestamp.clear()
|
||||
this.todayTriggers.clear()
|
||||
this.todayDate = getStartOfDay()
|
||||
weiboService.clearCache()
|
||||
}
|
||||
|
||||
private clearTimers(): void {
|
||||
@@ -786,6 +796,50 @@ ${topMentionText}
|
||||
return total
|
||||
}
|
||||
|
||||
private formatWeiboTimestamp(raw: string): string {
|
||||
const parsed = Date.parse(String(raw || ''))
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return String(raw || '').trim()
|
||||
}
|
||||
return new Date(parsed).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
private async getSocialContextSection(sessionId: string): Promise<string> {
|
||||
const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true
|
||||
if (!allowSocialContext) return ''
|
||||
|
||||
const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim()
|
||||
const hasCookie = rawCookie.length > 0
|
||||
|
||||
const bindings =
|
||||
(this.config.get('aiInsightWeiboBindings') as Record<string, { uid?: string; screenName?: string }> | undefined) || {}
|
||||
const binding = bindings[sessionId]
|
||||
const uid = String(binding?.uid || '').trim()
|
||||
if (!uid) return ''
|
||||
|
||||
const socialCountRaw = Number(this.config.get('aiInsightSocialContextCount') || 3)
|
||||
const socialCount = Math.max(1, Math.min(5, Math.floor(socialCountRaw) || 3))
|
||||
|
||||
try {
|
||||
const posts = await weiboService.fetchRecentPosts(uid, rawCookie, socialCount)
|
||||
if (posts.length === 0) return ''
|
||||
|
||||
const lines = posts.map((post) => {
|
||||
const time = this.formatWeiboTimestamp(post.createdAt)
|
||||
const text = post.text.length > 180 ? `${post.text.slice(0, 180)}...` : post.text
|
||||
return `[微博 ${time}] ${text}`
|
||||
})
|
||||
insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`)
|
||||
const riskHint = hasCookie
|
||||
? ''
|
||||
: '\n提示:未配置微博 Cookie,使用移动端公开接口抓取,可能因平台风控导致获取失败或内容较少。'
|
||||
return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}${riskHint}`
|
||||
} catch (error) {
|
||||
insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// ── 沉默联系人扫描 ──────────────────────────────────────────────────────────
|
||||
|
||||
private scheduleSilenceScan(): void {
|
||||
@@ -1028,6 +1082,8 @@ ${topMentionText}
|
||||
}
|
||||
}
|
||||
|
||||
const socialContextSection = await this.getSocialContextSection(sessionId)
|
||||
|
||||
// ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)────
|
||||
const DEFAULT_SYSTEM_PROMPT = `你是用户的私人关系观察助手,名叫"见解"。你的任务是主动提供有价值的观察和建议。
|
||||
|
||||
@@ -1060,6 +1116,7 @@ ${topMentionText}
|
||||
`时间统计:${todayStatsDesc}`,
|
||||
`全局统计:${globalStatsDesc}`,
|
||||
contextSection,
|
||||
socialContextSection,
|
||||
'请给出你的见解(≤80字):'
|
||||
].filter(Boolean).join('\n\n')
|
||||
|
||||
@@ -1190,3 +1247,5 @@ ${topMentionText}
|
||||
}
|
||||
|
||||
export const insightService = new InsightService()
|
||||
|
||||
|
||||
|
||||
367
electron/services/social/weiboService.ts
Normal file
367
electron/services/social/weiboService.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import https from 'https'
|
||||
import { createHash } from 'crypto'
|
||||
import { URL } from 'url'
|
||||
|
||||
const WEIBO_TIMEOUT_MS = 10_000
|
||||
const WEIBO_MAX_POSTS = 5
|
||||
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_MOBILE_USER_AGENT =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'
|
||||
|
||||
interface BrowserCookieEntry {
|
||||
domain?: string
|
||||
name?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
interface WeiboUserInfo {
|
||||
id?: number | string
|
||||
screen_name?: string
|
||||
}
|
||||
|
||||
interface WeiboWaterFallItem {
|
||||
id?: number | string
|
||||
idstr?: string
|
||||
mblogid?: string
|
||||
created_at?: string
|
||||
text_raw?: string
|
||||
isLongText?: boolean
|
||||
user?: WeiboUserInfo
|
||||
retweeted_status?: WeiboWaterFallItem
|
||||
}
|
||||
|
||||
interface WeiboWaterFallResponse {
|
||||
ok?: number
|
||||
data?: {
|
||||
list?: WeiboWaterFallItem[]
|
||||
next_cursor?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface WeiboStatusShowResponse {
|
||||
id?: number | string
|
||||
idstr?: string
|
||||
mblogid?: string
|
||||
created_at?: string
|
||||
text_raw?: string
|
||||
user?: WeiboUserInfo
|
||||
retweeted_status?: WeiboWaterFallItem
|
||||
}
|
||||
|
||||
interface MWeiboCard {
|
||||
mblog?: WeiboWaterFallItem
|
||||
card_group?: MWeiboCard[]
|
||||
}
|
||||
|
||||
interface MWeiboContainerResponse {
|
||||
ok?: number
|
||||
data?: {
|
||||
cards?: MWeiboCard[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface WeiboRecentPost {
|
||||
id: string
|
||||
createdAt: string
|
||||
url: string
|
||||
text: string
|
||||
screenName?: string
|
||||
}
|
||||
|
||||
interface CachedRecentPosts {
|
||||
expiresAt: number
|
||||
posts: WeiboRecentPost[]
|
||||
}
|
||||
|
||||
function requestJson<T>(url: string, options: { cookie?: string; referer?: string; userAgent?: string }): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let urlObj: URL
|
||||
try {
|
||||
urlObj = new URL(url)
|
||||
} catch {
|
||||
reject(new Error(`无效的微博请求地址:${url}`))
|
||||
return
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
Referer: options.referer || 'https://weibo.com',
|
||||
'User-Agent': options.userAgent || WEIBO_USER_AGENT,
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
if (options.cookie) {
|
||||
headers.Cookie = options.cookie
|
||||
}
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || 443,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
headers
|
||||
},
|
||||
(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 响应'))
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
req.setTimeout(WEIBO_TIMEOUT_MS, () => {
|
||||
req.destroy()
|
||||
reject(new Error('微博请求超时'))
|
||||
})
|
||||
|
||||
req.on('error', reject)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeCookieArray(entries: BrowserCookieEntry[]): string {
|
||||
const picked = new Map<string, string>()
|
||||
|
||||
for (const entry of entries) {
|
||||
const name = String(entry?.name || '').trim()
|
||||
const value = String(entry?.value || '').trim()
|
||||
const domain = String(entry?.domain || '').trim().toLowerCase()
|
||||
|
||||
if (!name || !value) continue
|
||||
if (domain && !domain.includes('weibo.com') && !domain.includes('weibo.cn')) continue
|
||||
|
||||
picked.set(name, value)
|
||||
}
|
||||
|
||||
return Array.from(picked.entries())
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
export function normalizeWeiboCookieInput(rawInput: string): string {
|
||||
const trimmed = String(rawInput || '').trim()
|
||||
if (!trimmed) return ''
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown
|
||||
if (Array.isArray(parsed)) {
|
||||
const normalized = normalizeCookieArray(parsed as BrowserCookieEntry[])
|
||||
if (normalized) return normalized
|
||||
throw new Error('Cookie JSON 中未找到可用的微博 Cookie 项')
|
||||
}
|
||||
} catch (error) {
|
||||
if (!(error instanceof SyntaxError)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed.replace(/^Cookie:\s*/i, '').trim()
|
||||
}
|
||||
|
||||
function normalizeWeiboUid(input: string): string {
|
||||
const trimmed = String(input || '').trim()
|
||||
const directMatch = trimmed.match(/^\d{5,}$/)
|
||||
if (directMatch) return directMatch[0]
|
||||
|
||||
const linkMatch = trimmed.match(/(?:weibo\.com|m\.weibo\.cn)\/u\/(\d{5,})/i)
|
||||
if (linkMatch) return linkMatch[1]
|
||||
|
||||
throw new Error('请输入有效的微博 UID(纯数字)')
|
||||
}
|
||||
|
||||
function sanitizeWeiboText(text: string): string {
|
||||
return String(text || '')
|
||||
.replace(/\u200b|\u200c|\u200d|\ufeff/g, '')
|
||||
.replace(/https?:\/\/t\.cn\/[A-Za-z0-9]+/g, ' ')
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function mergeRetweetText(item: Pick<WeiboWaterFallItem, 'text_raw' | 'retweeted_status'>): string {
|
||||
const baseText = sanitizeWeiboText(item.text_raw || '')
|
||||
const retweetText = sanitizeWeiboText(item.retweeted_status?.text_raw || '')
|
||||
if (!retweetText) return baseText
|
||||
if (!baseText || baseText === '转发微博') return `转发:${retweetText}`
|
||||
return `${baseText}\n\n转发内容:${retweetText}`
|
||||
}
|
||||
|
||||
function buildCacheKey(uid: string, count: number, cookie: string): string {
|
||||
const cookieHash = createHash('sha1').update(cookie).digest('hex')
|
||||
return `${uid}:${count}:${cookieHash}`
|
||||
}
|
||||
|
||||
class WeiboService {
|
||||
private recentPostsCache = new Map<string, CachedRecentPosts>()
|
||||
|
||||
clearCache(): void {
|
||||
this.recentPostsCache.clear()
|
||||
}
|
||||
|
||||
async validateUid(
|
||||
uidInput: string,
|
||||
cookieInput: string
|
||||
): Promise<{ success: boolean; uid?: string; screenName?: string; error?: string }> {
|
||||
try {
|
||||
const uid = normalizeWeiboUid(uidInput)
|
||||
const cookie = normalizeWeiboCookieInput(cookieInput)
|
||||
if (!cookie) {
|
||||
return { success: true, uid }
|
||||
}
|
||||
|
||||
const timeline = await this.fetchTimeline(uid, cookie)
|
||||
const firstItem = timeline.data?.list?.[0]
|
||||
if (!firstItem) {
|
||||
return { success: false, error: '该微博账号暂无可读取的近期公开内容,或当前 Cookie 已失效' }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
uid,
|
||||
screenName: firstItem.user?.screen_name
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: (error as Error).message || '微博 UID 校验失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRecentPosts(
|
||||
uidInput: string,
|
||||
cookieInput: string,
|
||||
requestedCount: number
|
||||
): Promise<WeiboRecentPost[]> {
|
||||
const uid = normalizeWeiboUid(uidInput)
|
||||
const cookie = normalizeWeiboCookieInput(cookieInput)
|
||||
const hasCookie = Boolean(cookie)
|
||||
|
||||
const count = Math.max(1, Math.min(WEIBO_MAX_POSTS, Math.floor(Number(requestedCount) || 0)))
|
||||
const cacheKey = buildCacheKey(uid, count, hasCookie ? cookie : '__no_cookie_mobile__')
|
||||
const cached = this.recentPostsCache.get(cacheKey)
|
||||
const now = Date.now()
|
||||
|
||||
if (cached && cached.expiresAt > now) {
|
||||
return cached.posts
|
||||
}
|
||||
|
||||
const rawItems = hasCookie
|
||||
? (await this.fetchTimeline(uid, cookie)).data?.list || []
|
||||
: await this.fetchMobileTimeline(uid)
|
||||
const posts: WeiboRecentPost[] = []
|
||||
|
||||
for (const item of rawItems) {
|
||||
if (posts.length >= count) break
|
||||
|
||||
const id = String(item.idstr || item.id || '').trim()
|
||||
if (!id) continue
|
||||
|
||||
let text = mergeRetweetText(item)
|
||||
if (item.isLongText && hasCookie) {
|
||||
try {
|
||||
const detail = await this.fetchDetail(id, cookie)
|
||||
text = mergeRetweetText(detail)
|
||||
} catch {
|
||||
// 长文补抓失败时回退到列表摘要
|
||||
}
|
||||
}
|
||||
|
||||
text = sanitizeWeiboText(text)
|
||||
if (!text) continue
|
||||
|
||||
posts.push({
|
||||
id,
|
||||
createdAt: String(item.created_at || ''),
|
||||
url: `https://m.weibo.cn/detail/${id}`,
|
||||
text,
|
||||
screenName: item.user?.screen_name
|
||||
})
|
||||
}
|
||||
|
||||
this.recentPostsCache.set(cacheKey, {
|
||||
expiresAt: now + WEIBO_CACHE_TTL_MS,
|
||||
posts
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
private fetchTimeline(uid: string, cookie: string): Promise<WeiboWaterFallResponse> {
|
||||
return requestJson<WeiboWaterFallResponse>(
|
||||
`https://weibo.com/ajax/profile/getWaterFallContent?uid=${encodeURIComponent(uid)}`,
|
||||
{
|
||||
cookie,
|
||||
referer: `https://weibo.com/u/${encodeURIComponent(uid)}`
|
||||
}
|
||||
).then((response) => {
|
||||
if (response.ok !== 1 || !Array.isArray(response.data?.list)) {
|
||||
throw new Error('微博时间线获取失败,请检查 Cookie 是否仍然有效')
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
private fetchMobileTimeline(uid: string): Promise<WeiboWaterFallItem[]> {
|
||||
const containerid = `107603${uid}`
|
||||
return requestJson<MWeiboContainerResponse>(
|
||||
`https://m.weibo.cn/api/container/getIndex?type=uid&value=${encodeURIComponent(uid)}&containerid=${encodeURIComponent(containerid)}`,
|
||||
{
|
||||
referer: `https://m.weibo.cn/u/${encodeURIComponent(uid)}`,
|
||||
userAgent: WEIBO_MOBILE_USER_AGENT
|
||||
}
|
||||
).then((response) => {
|
||||
if (response.ok !== 1 || !Array.isArray(response.data?.cards)) {
|
||||
throw new Error('微博时间线获取失败,请稍后重试')
|
||||
}
|
||||
|
||||
const rows: WeiboWaterFallItem[] = []
|
||||
for (const card of response.data.cards) {
|
||||
if (card?.mblog) rows.push(card.mblog)
|
||||
if (Array.isArray(card?.card_group)) {
|
||||
for (const subCard of card.card_group) {
|
||||
if (subCard?.mblog) rows.push(subCard.mblog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
throw new Error('该微博账号暂无可读取的近期公开内容')
|
||||
}
|
||||
|
||||
return rows
|
||||
})
|
||||
}
|
||||
|
||||
private fetchDetail(id: string, cookie: string): Promise<WeiboStatusShowResponse> {
|
||||
return requestJson<WeiboStatusShowResponse>(
|
||||
`https://weibo.com/ajax/statuses/show?id=${encodeURIComponent(id)}&isGetLongText=true`,
|
||||
{
|
||||
cookie,
|
||||
referer: `https://weibo.com/detail/${encodeURIComponent(id)}`
|
||||
}
|
||||
).then((response) => {
|
||||
if (!response || (!response.id && !response.idstr)) {
|
||||
throw new Error('微博详情获取失败')
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const weiboService = new WeiboService()
|
||||
Reference in New Issue
Block a user