返回 AiToEarn
homeFeed.ts
根目录 / project / aitoearn-web / src / store / plugin / plats / xhs / homeFeed.ts
1 /**
2 * 小红书首页列表功能模块
3 */
4
5 import type { HomeFeedItem, HomeFeedListParams, HomeFeedListResult } from '../types'
6 import type { XhsHomeFeedItem, XhsHomeFeedResponse } from './types'
7
8 /**
9 * 首页列表游标管理器
10 */
11 class HomeFeedCursorManager {
12 /**
13 * 游标缓存,用于分页转换
14 * key: 页码, value: cursor_score
15 */
16 private cursorMap = new Map<number, string>()
17
18 /**
19 * 当前已请求的最大页码
20 */
21 private maxRequestedPage = 0
22
23 /**
24 * 重置游标缓存
25 */
26 reset(): void {
27 this.cursorMap.clear()
28 this.maxRequestedPage = 0
29 }
30
31 /**
32 * 获取当前页的游标
33 */
34 getCursor(page: number): string {
35 return page === 1 ? '' : this.cursorMap.get(page) || ''
36 }
37
38 /**
39 * 存储下一页的游标
40 */
41 setCursor(currentPage: number, cursorScore: string): void {
42 this.cursorMap.set(currentPage + 1, cursorScore)
43 this.maxRequestedPage = currentPage
44 }
45
46 /**
47 * 获取最大已请求页码
48 */
49 getMaxRequestedPage(): number {
50 return this.maxRequestedPage
51 }
52 }
53
54 /**
55 * 游标管理器实例
56 */
57 export const homeFeedCursor = new HomeFeedCursorManager()
58
59 /**
60 * 构建首页列表请求数据
61 */
62 export function buildHomeFeedRequestData(params: HomeFeedListParams, cursorScore: string): object {
63 const { page, size } = params
64
65 return {
66 cursor_score: cursorScore,
67 num: size,
68 refresh_type: page === 1 ? 1 : 3,
69 note_index: (page - 1) * size,
70 unread_begin_note_id: '',
71 unread_end_note_id: '',
72 unread_note_count: 0,
73 category: 'homefeed_recommend',
74 search_key: '',
75 need_num: Math.min(size, 10),
76 image_formats: ['jpg', 'webp', 'avif'],
77 need_filter_image: false,
78 }
79 }
80
81 /**
82 * 构建小红书作者主页链接
83 * @param userId 用户ID
84 * @param xsecToken 用户的 xsec_token
85 */
86 function buildAuthorUrl(userId: string, xsecToken: string): string {
87 const params = new URLSearchParams({
88 xsec_token: xsecToken,
89 xsec_source: 'pc_feed',
90 })
91 return `https://www.xiaohongshu.com/user/profile/${userId}?${params.toString()}`
92 }
93
94 /**
95 * 将小红书原始数据转换为统一格式
96 * 注:list不包含收藏数和话题,这些在详情中获取
97 */
98 export function transformToHomeFeedItem(item: XhsHomeFeedItem): HomeFeedItem {
99 const { note_card } = item
100
101 // 构建作者主页链接
102 const authorUrl = buildAuthorUrl(note_card.user?.user_id || '', note_card.user?.xsec_token || '')
103
104 return {
105 workId: item.id,
106 thumbnail: note_card.cover?.url_default || note_card.cover?.url_pre || '',
107 title: note_card.display_title || '',
108 authorAvatar: note_card.user?.avatar || '',
109 authorName: note_card.user?.nickname || note_card.user?.nick_name || '',
110 authorId: note_card.user?.user_id || '',
111 authorUrl,
112 likeCount: note_card.interact_info?.liked_count || '0',
113 isFollowed: note_card.interact_info?.followed ?? false,
114 isLiked: note_card.interact_info?.liked ?? false,
115 isVideo: note_card.type === 'video',
116 videoDuration: note_card.video?.capa?.duration,
117 origin: item,
118 thumbnailWidth: note_card.cover.width,
119 thumbnailHeight: note_card.cover.height,
120 }
121 }
122
123 /**
124 * 获取首页作品列表
125 * 使用游标分页转换为 page/size 分页
126 */
127 export async function getHomeFeedList(params: HomeFeedListParams): Promise<HomeFeedListResult> {
128 // 检查插件
129 if (!window.AIToEarnPlugin) {
130 return {
131 success: false,
132 message: '插件未安装或未就绪',
133 items: [],
134 hasMore: false,
135 }
136 }
137
138 const { page, size } = params
139
140 // 页码从1开始
141 if (page < 1) {
142 return {
143 success: false,
144 message: '页码必须大于0',
145 items: [],
146 hasMore: false,
147 }
148 }
149
150 // 如果请求的页码超过已请求的最大页码+1,需要顺序请求
151 // 游标分页只能顺序获取,不支持跳页
152 const maxPage = homeFeedCursor.getMaxRequestedPage()
153 if (page > maxPage + 1) {
154 return {
155 success: false,
156 message: `不支持跳页,请先请求第 ${maxPage + 1} 页`,
157 items: [],
158 hasMore: false,
159 }
160 }
161
162 // 如果是第1页,重置游标
163 if (page === 1) {
164 homeFeedCursor.reset()
165 }
166
167 // 获取当前页的游标
168 const cursorScore = homeFeedCursor.getCursor(page)
169
170 // 构建请求数据
171 const requestData = buildHomeFeedRequestData(params, cursorScore)
172
173 try {
174 const response = await window.AIToEarnPlugin.xhsRequest<XhsHomeFeedResponse>({
175 path: '/api/sns/web/v1/homefeed',
176 method: 'POST',
177 data: requestData,
178 })
179
180 if (!response.success || !response.data) {
181 return {
182 success: false,
183 message: response.msg || '获取首页列表失败',
184 items: [],
185 hasMore: false,
186 rawData: response,
187 }
188 }
189
190 // 存储下一页的游标
191 if (response.data.cursor_score) {
192 homeFeedCursor.setCursor(page, response.data.cursor_score)
193 }
194
195 // 转换数据格式
196 const items: HomeFeedItem[] = response.data.items
197 .filter(item => item.model_type === 'note' && item.note_card)
198 .map(item => transformToHomeFeedItem(item))
199
200 return {
201 success: true,
202 items,
203 hasMore: items.length >= size && !!response.data.cursor_score,
204 rawData: response,
205 }
206 }
207 catch (error) {
208 return {
209 success: false,
210 message: error instanceof Error ? error.message : '请求失败',
211 items: [],
212 hasMore: false,
213 }
214 }
215 }
216
216 lines TYPESCRIPT