diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 590d609..1366751 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -809,7 +809,7 @@ ${topMentionText} if (!allowSocialContext) return '' const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim() - if (!rawCookie) return '' + const hasCookie = rawCookie.length > 0 const bindings = (this.config.get('aiInsightWeiboBindings') as Record | undefined) || {} @@ -830,7 +830,10 @@ ${topMentionText} return `[微博 ${time}] ${text}` }) insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`) - return `近期公开社交平台内容(实验性,来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}` + const riskHint = hasCookie + ? '' + : '\n提示:未配置微博 Cookie,使用移动端公开接口抓取,可能因平台风控导致获取失败或内容较少。' + return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}${riskHint}` } catch (error) { insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`) return '' diff --git a/electron/services/social/weiboService.ts b/electron/services/social/weiboService.ts index 3f6905c..30a9a5f 100644 --- a/electron/services/social/weiboService.ts +++ b/electron/services/social/weiboService.ts @@ -7,6 +7,8 @@ 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 @@ -48,6 +50,18 @@ interface WeiboStatusShowResponse { retweeted_status?: WeiboWaterFallItem } +interface MWeiboCard { + mblog?: WeiboWaterFallItem + card_group?: MWeiboCard[] +} + +interface MWeiboContainerResponse { + ok?: number + data?: { + cards?: MWeiboCard[] + } +} + export interface WeiboRecentPost { id: string createdAt: string @@ -61,7 +75,7 @@ interface CachedRecentPosts { posts: WeiboRecentPost[] } -function requestJson(url: string, options: { cookie: string; referer?: string }): Promise { +function requestJson(url: string, options: { cookie?: string; referer?: string; userAgent?: string }): Promise { return new Promise((resolve, reject) => { let urlObj: URL try { @@ -71,19 +85,23 @@ function requestJson(url: string, options: { cookie: string; referer?: string return } + const headers: Record = { + 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: { - Accept: 'application/json, text/plain, */*', - Referer: options.referer || 'https://weibo.com', - 'User-Agent': WEIBO_USER_AGENT, - 'X-Requested-With': 'XMLHttpRequest', - Cookie: options.cookie - } + headers }, (res) => { let raw = '' @@ -232,10 +250,10 @@ class WeiboService { ): Promise { const uid = normalizeWeiboUid(uidInput) const cookie = normalizeWeiboCookieInput(cookieInput) - if (!cookie) return [] + const hasCookie = Boolean(cookie) const count = Math.max(1, Math.min(WEIBO_MAX_POSTS, Math.floor(Number(requestedCount) || 0))) - const cacheKey = buildCacheKey(uid, count, cookie) + const cacheKey = buildCacheKey(uid, count, hasCookie ? cookie : '__no_cookie_mobile__') const cached = this.recentPostsCache.get(cacheKey) const now = Date.now() @@ -243,8 +261,9 @@ class WeiboService { return cached.posts } - const timeline = await this.fetchTimeline(uid, cookie) - const rawItems = Array.isArray(timeline.data?.list) ? timeline.data.list : [] + const rawItems = hasCookie + ? (await this.fetchTimeline(uid, cookie)).data?.list || [] + : await this.fetchMobileTimeline(uid) const posts: WeiboRecentPost[] = [] for (const item of rawItems) { @@ -254,7 +273,7 @@ class WeiboService { if (!id) continue let text = mergeRetweetText(item) - if (item.isLongText) { + if (item.isLongText && hasCookie) { try { const detail = await this.fetchDetail(id, cookie) text = mergeRetweetText(detail) @@ -298,6 +317,37 @@ class WeiboService { }) } + private fetchMobileTimeline(uid: string): Promise { + const containerid = `107603${uid}` + return requestJson( + `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 { return requestJson( `https://weibo.com/ajax/statuses/show?id=${encodeURIComponent(id)}&isGetLongText=true`, diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 29aadf9..e79e518 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -3220,7 +3220,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { {!hasWeiboCookieConfigured && ( - 未配置微博 Cookie 时,开启后也不会发送社交平台内容。 + 未配置微博 Cookie 时,也会尝试抓取微博公开内容;但可能因平台风控导致获取失败或内容较少。 )} @@ -4625,7 +4625,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { 清空