移除不准确的 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,
};
}
-2
View File
@@ -42,7 +42,6 @@ export interface FeedDetail {
likeCount: number;
collectCount: number;
commentCount: number;
shareCount: number;
isLiked: boolean;
isFavorited: boolean;
createTime: string;
@@ -74,7 +73,6 @@ export interface UserProfile {
follows: number;
fans: number;
interaction: number;
feedCount: number;
feeds: Feed[];
}
-1
View File
@@ -194,7 +194,6 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
{ label: '点赞', value: detail.likeCount },
{ label: '收藏', value: detail.collectCount },
{ label: '评论', value: detail.commentCount },
{ label: '分享', value: detail.shareCount },
].map((s) => (
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
+1 -2
View File
@@ -54,12 +54,11 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
<div className="grid grid-cols-3 gap-3">
{[
{ label: '关注', value: profile.follows },
{ label: '粉丝', value: profile.fans },
{ label: '获赞与收藏', value: profile.interaction },
{ label: '笔记', value: profile.feedCount },
].map((s) => (
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
+5
View File
@@ -261,6 +261,11 @@ export function XiaohongshuPage() {
<span className="text-xs text-dark-muted">{status.userId.slice(0, 12)}</span>
)}
</div>
{status.userId && (
<Button size="sm" variant="secondary" onClick={() => setUserView({ userId: status.userId!, xsecToken: '' })}>
</Button>
)}
<Button size="sm" variant="danger" onClick={() => void handleLogout()} loading={logoutLoading}>
退
</Button>