1- import { load } from 'cheerio ' ;
2-
1+ import { config } from '@/config ' ;
2+ import ConfigNotFoundError from '@/errors/types/config-not-found' ;
33import type { Route } from '@/types' ;
4+ import { buildBilibiliArticleFeedItem , parseBilibiliArticlePage } from '@/utils/bilibili-article' ;
5+ import { parseBilibiliOpusArticle } from '@/utils/bilibili-opus' ;
46import cacheGeneral from '@/utils/cache' ;
57import got from '@/utils/got' ;
68import { parseDate } from '@/utils/parse-date' ;
79
810import cache from './cache' ;
911
1012export const route : Route = {
11- path : '/user/article/:uid' ,
13+ path : '/user/article/:uid/:loginUid? ' ,
1214 categories : [ 'social-media' ] ,
1315 example : '/bilibili/user/article/334958638' ,
14- parameters : { uid : '用户 id, 可在 UP 主主页中找到' } ,
16+ parameters : {
17+ uid : 'UP 主用户 id,可在其主页中找到' ,
18+ loginUid : '可选,用于选择 BILIBILI_COOKIE_{loginUid} 对应的登录用户' ,
19+ } ,
1520 features : {
16- requireConfig : false ,
21+ requireConfig : [
22+ {
23+ name : 'BILIBILI_COOKIE_*' ,
24+ optional : true ,
25+ description : '配置 BILIBILI_COOKIE_{loginUid} 后,可在路由末尾传入 loginUid,使用该登录用户已有的访问权限读取其已解锁的图文正文。' ,
26+ } ,
27+ ] ,
1728 requirePuppeteer : false ,
1829 antiCrawler : false ,
1930 supportBT : false ,
@@ -23,65 +34,107 @@ export const route: Route = {
2334 radar : [
2435 {
2536 source : [ 'space.bilibili.com/:uid' ] ,
37+ target : '/user/article/:uid' ,
2638 } ,
2739 ] ,
2840 name : 'UP 主图文' ,
2941 maintainers : [ 'lengthmin' , 'Qixingchen' , 'hyoban' ] ,
3042 handler,
43+ description : '逐篇抓取图文正文。配置 BILIBILI\\_COOKIE\\_{loginUid} 后,可通过 loginUid 参数选择登录用户,并使用该账号已有的访问权限读取其已解锁的充电专属内容。未配置时仅返回公开内容或预览。' ,
3144} ;
3245
3346async function handler ( ctx ) {
3447 const uid = ctx . req . param ( 'uid' ) ;
35- const name = await cache . getUsernameFromUID ( uid ) ;
48+ const loginUid = ctx . req . param ( 'loginUid' ) ;
49+ const cookie = loginUid ? config . bilibili . cookies [ loginUid ] : cache . getConfiguredCookie ( ) ;
50+
51+ if ( loginUid && ! cookie ) {
52+ throw new ConfigNotFoundError ( `Missing BILIBILI_COOKIE_${ loginUid } ` ) ;
53+ }
54+
55+ const referer = `https://space.bilibili.com/${ uid } /article` ;
56+ const headers = {
57+ Referer : referer ,
58+ 'User-Agent' : config . ua ,
59+ ...( cookie && { Cookie : cookie } ) ,
60+ } ;
3661 const response = await got ( {
3762 method : 'get' ,
3863 url : `https://api.bilibili.com/x/polymer/web-dynamic/v1/opus/feed/space?host_mid=${ uid } ` ,
39- headers : {
40- Referer : `https://space.bilibili.com/${ uid } /article` ,
41- } ,
64+ headers,
4265 } ) ;
66+
67+ if ( response . data . code !== 0 || ! Array . isArray ( response . data . data ?. items ) ) {
68+ throw new Error ( `Failed to fetch Bilibili articles: ${ response . data . message || `API code ${ response . data . code } ` } ` ) ;
69+ }
70+
4371 const data = response . data . data ;
44- const title = `${ name } 的 bilibili 图文` ;
4572 const link = `https://space.bilibili.com/${ uid } /article` ;
46- const description = `${ name } 的 bilibili 图文` ;
47- const cookie = await cache . getCookie ( ) ;
73+ const cacheContext = loginUid || 'default' ;
4874
49- const item = await Promise . all (
75+ const resolvedItems = await Promise . all (
5076 data . items . map ( async ( item ) => {
51- const link = 'https:' + item . jump_url ;
52- const data = await cacheGeneral . tryGet (
53- link ,
54- async ( ) =>
55- (
56- await got ( {
57- method : 'get' ,
58- url : link ,
59- headers : {
60- Referer : `https://space.bilibili.com/${ uid } /article` ,
61- Cookie : cookie ,
62- } ,
63- } )
64- ) . data
65- ) ;
66-
67- const $ = load ( data as string ) ;
68- const description = $ ( '.opus-module-content' ) . html ( ) ;
69- const pubDate = $ ( '.opus-module-author__pub__text' ) . text ( ) . replace ( '编辑于 ' , '' ) ;
70-
71- const single = {
72- title : item . content ,
77+ const link = item . jump_url . startsWith ( '//' ) ? `https:${ item . jump_url } ` : item . jump_url ;
78+ let article = { } ;
79+
80+ try {
81+ article = await cacheGeneral . tryGet ( `bilibili:article:v4:${ item . opus_id } :${ cacheContext } ` , async ( ) => {
82+ const detail = await got ( {
83+ method : 'get' ,
84+ url : `https://api.bilibili.com/x/polymer/web-dynamic/v1/opus/detail?id=${ encodeURIComponent ( item . opus_id ) } ` ,
85+ headers,
86+ } ) ;
87+ const opusArticle = parseBilibiliOpusArticle ( detail . data ) ;
88+ if ( opusArticle . description ) {
89+ return opusArticle ;
90+ }
91+
92+ const page = await got ( {
93+ method : 'get' ,
94+ url : link ,
95+ headers,
96+ } ) ;
97+ const pageArticle = parseBilibiliArticlePage ( page . data as string ) ;
98+ if ( ! pageArticle . description ) {
99+ throw new Error ( `Bilibili article body is unavailable: ${ link } ` ) ;
100+ }
101+ return pageArticle ;
102+ } ) ;
103+ } catch {
104+ // An authenticated title-only result must not be cached or
105+ // published. Anonymous routes retain their preview fallback.
106+ }
107+
108+ const feedItem = buildBilibiliArticleFeedItem ( {
109+ article,
110+ fallbackTitle : item . content ,
73111 link,
74- description : description || item . content ,
75- // 2019年11月11日 08:50
76- pubDate : pubDate ? parseDate ( pubDate , 'YYYY年MM月DD日 HH:mm' ) : undefined ,
112+ authenticated : Boolean ( cookie ) ,
113+ } ) ;
114+
115+ if ( ! feedItem ) {
116+ return ;
117+ }
118+
119+ return {
120+ ...feedItem ,
121+ pubDate : feedItem . pubDate ? parseDate ( feedItem . pubDate , typeof feedItem . pubDate === 'number' ? 'X' : 'YYYY年MM月DD日 HH:mm' ) : undefined ,
77122 } ;
78- return single ;
79123 } )
80124 ) ;
125+ const item = resolvedItems . filter ( ( item ) => item !== undefined ) ;
126+
127+ if ( cookie && item . length === 0 ) {
128+ throw new Error ( 'No authenticated Bilibili article bodies were returned. Verify that the configured BILIBILI_COOKIE_* account can access these articles.' ) ;
129+ }
130+
131+ const name = item . find ( ( article ) => article . author ) ?. author || uid ;
132+ const title = `${ name } 的 bilibili 图文` ;
133+
81134 return {
82135 title,
83136 link,
84- description,
137+ description : title ,
85138 item,
86139 } ;
87140}
0 commit comments