移除不准确的 feedCount 和 shareCount 字段,新增用户主页入口

- 删除 UserProfile.feedCount(tab 选择器已失效,改用 feeds.length 展示)
- 删除 FeedDetail.shareCount(实际无法获取分享数)
- 用户信息栏新增"主页"按钮,点击查看当前登录用户主页
- extractInitialState 补充 userProfile/homeFeed/explore key 提取
This commit is contained in:
2026-03-02 15:00:49 +08:00
parent 5a1f88de95
commit e252310f23
9 changed files with 9 additions and 46 deletions
-10
View File
@@ -109,8 +109,6 @@ interface RawNoteInteract {
collected_count?: string;
commentCount?: string;
comment_count?: string;
shareCount?: string;
share_count?: string;
}
interface RawNoteUser {
@@ -335,10 +333,6 @@ function parseDetailFromState(
const commentCount = parseCountString(
interact?.commentCount ?? interact?.comment_count ?? '0',
);
const shareCount = parseCountString(
interact?.shareCount ?? interact?.share_count ?? '0',
);
// Timestamps
const createTimeRaw = noteData.time ?? noteData.createTime ?? noteData.create_time;
const createTime = createTimeRaw
@@ -377,7 +371,6 @@ function parseDetailFromState(
likeCount,
collectCount,
commentCount,
shareCount,
isLiked: false,
isFavorited: false,
createTime,
@@ -495,8 +488,6 @@ async function scrapeDetailFromDom(
const likeCount = await extractCount(page, SEL.likeCount);
const collectCount = await extractCount(page, SEL.collectCount);
const commentCount = await extractCount(page, SEL.commentCount);
const shareCount = await extractCount(page, SEL.shareCount);
// Create time
const createTime = await page
.$eval(SEL.createTime, (el) => el.textContent?.trim() ?? '')
@@ -534,7 +525,6 @@ async function scrapeDetailFromDom(
likeCount,
collectCount,
commentCount,
shareCount,
isLiked: false,
isFavorited: false,
createTime,
+2 -2
View File
@@ -166,7 +166,7 @@ async function extractInitialState(page: Page): Promise<InitialState | null> {
if (!s || typeof s !== 'object') return null;
try {
// structuredClone strips Vue proxies and produces a plain object.
const plain = structuredClone({ noteData: s.noteData, note: s.note, feed: s.feed, feeds: s.feeds, user: s.user });
const plain = structuredClone({ noteData: s.noteData, note: s.note, feed: s.feed, feeds: s.feeds, user: s.user, userProfile: s.userProfile, homeFeed: s.homeFeed, explore: s.explore });
return JSON.stringify(plain);
} catch {
// structuredClone may fail on some Vue internals — fall back to
@@ -175,7 +175,7 @@ async function extractInitialState(page: Page): Promise<InitialState | null> {
const MAX_DEPTH = 20;
const seen = new Set<unknown>();
const result = JSON.stringify(
{ noteData: s.noteData, note: s.note, feed: s.feed, feeds: s.feeds, user: s.user },
{ noteData: s.noteData, note: s.note, feed: s.feed, feeds: s.feeds, user: s.user, userProfile: s.userProfile, homeFeed: s.homeFeed, explore: s.explore },
function (_key, value) {
if (typeof value === 'function') return undefined;
if (typeof value === 'object' && value !== null) {
-3
View File
@@ -72,7 +72,6 @@ export const XHS_SELECTORS = {
/** Comment count. */
commentCount: '.engage-bar .chat-wrapper .count',
/** Share count. */
shareCount: '.engage-bar .share-wrapper .count',
/** Publish / create time text. */
createTime: '.note-scroller .bottom-container .date',
/** IP location. */
@@ -122,8 +121,6 @@ export const XHS_SELECTORS = {
ipLocation: '.user-info .user-ip',
/** Follower / following / interaction count elements. */
followCount: '.user-info .user-interactions > div',
/** Note count (displayed somewhere on the profile page). */
noteCountTab: '.reds-tab-item',
/** Individual feed items on the user profile. */
feedItem: '.feeds-container .note-item',
},
-2
View File
@@ -53,7 +53,6 @@ export interface FeedDetail {
likeCount: number;
collectCount: number;
commentCount: number;
shareCount: number;
isLiked: boolean;
isFavorited: boolean;
createTime: string;
@@ -89,7 +88,6 @@ export interface UserProfile {
follows: number;
fans: number;
interaction: number;
feedCount: number;
feeds: Feed[];
}
+1 -24
View File
@@ -158,7 +158,7 @@ export async function getUserProfile(
if (initialState) {
const profile = parseProfileFromState(initialState, userId, xsecToken);
if (profile) {
log.info({ userId, feedCount: profile.feeds.length }, 'Extracted user profile from __INITIAL_STATE__');
log.info({ userId, feedsCount: profile.feeds.length }, 'Extracted user profile from __INITIAL_STATE__');
return profile;
}
log.debug('__INITIAL_STATE__ found but no profile data extracted, falling back to DOM');
@@ -215,12 +215,6 @@ function parseProfileFromState(
const fans = toNumber(interactions?.fans ?? userInfo.fans ?? 0);
const interaction = toNumber(interactions?.interaction ?? userInfo.interaction ?? 0);
// Note count.
const feedCount = toNumber(
userPageData?.noteCount ?? userPageData?.note_count ??
userInfo.noteCount ?? userInfo.note_count ?? 0,
);
// Notes / feeds on the profile page.
const rawNotes: RawProfileNote[] =
userPageData?.notes ?? state.userProfile?.notes ?? [];
@@ -238,7 +232,6 @@ function parseProfileFromState(
follows,
fans,
interaction,
feedCount,
feeds,
};
}
@@ -359,21 +352,6 @@ async function scrapeProfileFromDom(
const fans = parseCountString(dataCounts[1] ?? '0');
const interaction = parseCountString(dataCounts[2] ?? '0');
// Note count from tab — use a string expression to run in browser context
// without needing DOM types in our TypeScript config.
const feedCount = await page
.$$eval(SEL.noteCountTab, (tabs) => {
for (const tab of tabs) {
const text = tab.textContent ?? '';
if (text.includes('\u7B14\u8BB0')) {
const match = text.match(/\d+/);
return match ? parseInt(match[0], 10) : 0;
}
}
return 0;
})
.catch(() => 0);
// Scrape feed items on the profile page.
const feedElements = await page.$$(SEL.feedItem);
const feeds: Feed[] = [];
@@ -430,7 +408,6 @@ async function scrapeProfileFromDom(
follows,
fans,
interaction,
feedCount,
feeds,
};
}