1- import { createHash } from 'node:crypto' ;
2-
3- import { Cookie } from 'tough-cookie' ;
4-
51import { config } from '@/config' ;
6- import ConfigNotFoundError from '@/errors/types/config-not-found' ;
72import cache from '@/utils/cache' ;
8- import got from '@/utils/got' ;
9- import { getPlaywrightPage } from '@/utils/playwright' ;
10-
11- /**
12- * 解析百度 cookie 字符串为 Playwright 可用的 cookie 对象数组
13- * 正确处理包含 '=' 的 cookie 值
14- */
15- export function parseBaiduCookies ( cookieStr : string ) : Array < { name : string ; value : string ; domain : string ; path : string } > {
16- return cookieStr
17- . split ( ';' )
18- . map ( ( c ) => Cookie . parse ( c . trim ( ) ) )
19- . filter ( ( c ) : c is Cookie => Boolean ( c ?. key ) )
20- . map ( ( c ) => ( {
21- name : c . key ,
22- value : c . value ,
23- domain : '.tieba.baidu.com' ,
24- path : '/' ,
25- } ) ) ;
26- }
27-
28- /**
29- * 检查 HTML 内容是否包含百度安全验证页面
30- */
31- export function checkSecurityVerification ( html : string ) : void {
32- if ( html . includes ( '安全验证' ) || html . includes ( '百度安全验证' ) ) {
33- throw new Error ( 'Baidu security verification required. The cookie may be expired or invalid. Please update your BAIDU_COOKIE.' ) ;
34- }
35- }
36-
37- /**
38- * 使用 Playwright 获取贴吧页面内容
39- * 包含统一的 cookie 设置、安全验证检查和缓存逻辑
40- * 带有重试机制处理瞬态错误
41- */
42- export async function getTiebaPageContent (
43- url : string ,
44- cacheKey : string ,
45- options : {
46- waitForSelector ?: string ;
47- timeout ?: number ;
48- retries ?: number ;
49- } = { }
50- ) : Promise < string > {
51- const cookie = config . baidu . cookie ;
52-
53- if ( ! cookie ) {
54- throw new ConfigNotFoundError ( 'Baidu Tieba RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#baidu">BAIDU_COOKIE</a>' ) ;
55- }
56-
57- const cookies = parseBaiduCookies ( cookie ) ;
58- const { waitForSelector = '.thread-card-wrapper, .virtual-list-item, .thread-content-box, .thread-card' , timeout = 3000 , retries = 3 } = options ;
59-
60- const data = await cache . tryGet (
61- cacheKey ,
62- async ( ) => {
63- let lastError : Error | undefined ;
64-
65- /* eslint-disable no-await-in-loop -- Intentional sequential retry logic */
66- for ( let attempt = 0 ; attempt < retries ; attempt ++ ) {
67- const { page, destroy } = await getPlaywrightPage ( url , {
68- onBeforeLoad : async ( page ) => {
69- if ( cookies . length > 0 ) {
70- await page . context ( ) . addCookies ( cookies ) ;
71- }
72- } ,
73- gotoConfig : { waitUntil : 'domcontentloaded' } ,
74- } ) ;
75-
76- try {
77- // 等待页面稳定
78- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
79-
80- // 动态等待内容加载
81- try {
82- await page . waitForSelector ( waitForSelector , { timeout } ) ;
83- } catch {
84- // 如果超时,继续执行
85- }
86-
87- const html = await page . content ( ) ;
88- checkSecurityVerification ( html ) ;
89- return html ;
90- } catch ( error ) {
91- lastError = error as Error ;
92- // 如果是最后一次尝试,抛出错误
93- if ( attempt === retries - 1 ) {
94- throw lastError ;
95- }
96- // 等待后重试
97- await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 * ( attempt + 1 ) ) ) ;
98- } finally {
99- await destroy ( ) ;
100- }
101- }
102- /* eslint-enable no-await-in-loop */
103- throw lastError || new Error ( 'Failed to fetch page content' ) ;
104- } ,
105- config . cache . routeExpire ,
106- false
107- ) ;
108-
109- return data ;
110- }
111-
112- /**
113- * 规范化 URL 为绝对地址
114- */
115- export function normalizeUrl ( href : string , base : string = 'https://tieba.baidu.com' ) : string {
116- if ( ! href ) {
117- return '' ;
118- }
119- if ( href . startsWith ( 'http' ) ) {
120- return href ;
121- }
122- const path = href . startsWith ( '/' ) ? href : `/${ href } ` ;
123- return `${ base } ${ path } ` ;
124- }
3+ import md5 from '@/utils/md5' ;
4+ import ofetch from '@/utils/ofetch' ;
1255
1266/**
1277 * 通过 /c/f/frs/page API 获取贴吧帖子列表
@@ -145,62 +25,57 @@ type TiebaClientParams = {
14525 sign ?: string ;
14626} ;
14727
148- function computeSign ( params : TiebaClientParams ) : string {
28+ const computeSign = ( params : Partial < TiebaClientParams > & Record < string , string > ) : string => {
14929 // oxlint-disable-next-line unicorn-js/require-array-sort-compare
15030 const sortedKeys = Object . keys ( params ) . toSorted ( ) ;
15131 const raw = sortedKeys . map ( ( key ) => `${ key } =${ params [ key ] } ` ) . join ( '' ) + TIEBA_CLIENT_SECRET ;
152- return createHash ( 'md5' ) . update ( raw ) . digest ( 'hex' ) ;
153- }
154-
155- export async function getTiebaForumData ( params : { kw : string ; cid ?: string ; isGood ?: boolean ; sortBy ?: string } ) : Promise < any > {
156- const cookie = config . baidu . cookie ;
157- if ( ! cookie ) {
158- throw new ConfigNotFoundError ( 'Baidu Tieba RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#baidu">BAIDU_COOKIE</a>' ) ;
159- }
160-
161- const bduss = cookie . match ( / B D U S S = ( [ ^ ; ] + ) / ) ?. [ 1 ] || '' ;
162- if ( ! bduss ) {
163- throw new ConfigNotFoundError ( 'BAIDU_COOKIE must contain BDUSS. Please check your cookie configuration.' ) ;
164- }
32+ return md5 ( raw ) ;
33+ } ;
16534
166- const apiParams : TiebaClientParams = {
167- _client_id : 'wappc_1234567890123_456' ,
35+ export const tiebaClientRequest = async ( path : string , params : Partial < TiebaClientParams > & Record < string , string > ) => {
36+ const bduss = config . baidu . cookie ?. match ( / B D U S S = ( [ ^ ; ] + ) / ) ?. [ 1 ] ;
37+ const apiParams : Partial < TiebaClientParams > & Record < string , string > = {
38+ _client_id : `wappc_${ Date . now ( ) } _${ Math . floor ( Math . random ( ) * 1000 ) } ` ,
16839 _client_type : '2' ,
16940 _client_version : '12.20.1.0' ,
17041 _phone_imei : '000000000000000' ,
17142 from : 'tieba' ,
172- kw : params . kw ,
173- rn : '30' ,
174- pn : '1' ,
175- BDUSS : bduss ,
43+ ...( bduss && { BDUSS : bduss } ) ,
44+ ...params ,
17645 } ;
177-
178- if ( params . isGood ) {
179- apiParams . is_good = '1' ;
180- }
181- if ( params . cid && params . cid !== '0' ) {
182- apiParams . cid = params . cid ;
183- }
184- if ( params . sortBy === 'replied' ) {
185- apiParams . sort_type = '1' ;
186- }
187-
18846 apiParams . sign = computeSign ( apiParams ) ;
18947
190- const url = 'https://tieba.baidu.com/c/f/frs/page' ;
191- const cacheKey = `tieba:api:forum:${ params . kw } :${ params . cid || '0' } :${ params . sortBy || 'created' } ` ;
192-
48+ const cacheKey = `tieba:api:${ path } :${ new URLSearchParams ( params ) . toString ( ) } ` ;
19349 const data = await cache . tryGet (
19450 cacheKey ,
195- async ( ) => {
196- const { data : response } = await got . post ( url , {
197- form : apiParams ,
198- } ) ;
199- return response ;
200- } ,
51+ ( ) =>
52+ ofetch ( `https://tieba.baidu.com ${ path } ` , {
53+ method : 'POST' ,
54+ body : new URLSearchParams ( apiParams ) ,
55+ parseResponse : JSON . parse , // content-type application/x-javascript
56+ } ) ,
20157 config . cache . routeExpire ,
20258 false
20359 ) ;
204-
60+ if ( Number ( data . error_code ) !== 0 ) {
61+ throw new Error ( `Tieba API error: ${ data . error_msg || data . error_code } ` ) ;
62+ }
20563 return data ;
206- }
64+ } ;
65+
66+ export const renderContent = ( items : any [ ] = [ ] ) =>
67+ items
68+ . map ( ( item ) => {
69+ switch ( Number ( item . type ) ) {
70+ case 1 :
71+ case 18 :
72+ return `<a href="${ item . link } ">${ item . text } </a>` ;
73+ case 2 :
74+ return `<img src="https://tb3.bdstatic.com/emoji/${ item . text } @2x.png" alt="${ item . c } ">` ;
75+ case 3 :
76+ return `<img src="${ item . origin_src || item . src } ">` ;
77+ default :
78+ return item . text ?? '' ;
79+ }
80+ } )
81+ . join ( '' ) ;
0 commit comments