移除不准确的 feedCount 和 shareCount 字段,新增用户主页入口
- 删除 UserProfile.feedCount(tab 选择器已失效,改用 feeds.length 展示) - 删除 FeedDetail.shareCount(实际无法获取分享数) - 用户信息栏新增"主页"按钮,点击查看当前登录用户主页 - extractInitialState 补充 userProfile/homeFeed/explore key 提取
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user