移除不准确的 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;
|
collected_count?: string;
|
||||||
commentCount?: string;
|
commentCount?: string;
|
||||||
comment_count?: string;
|
comment_count?: string;
|
||||||
shareCount?: string;
|
|
||||||
share_count?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawNoteUser {
|
interface RawNoteUser {
|
||||||
@@ -335,10 +333,6 @@ function parseDetailFromState(
|
|||||||
const commentCount = parseCountString(
|
const commentCount = parseCountString(
|
||||||
interact?.commentCount ?? interact?.comment_count ?? '0',
|
interact?.commentCount ?? interact?.comment_count ?? '0',
|
||||||
);
|
);
|
||||||
const shareCount = parseCountString(
|
|
||||||
interact?.shareCount ?? interact?.share_count ?? '0',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
const createTimeRaw = noteData.time ?? noteData.createTime ?? noteData.create_time;
|
const createTimeRaw = noteData.time ?? noteData.createTime ?? noteData.create_time;
|
||||||
const createTime = createTimeRaw
|
const createTime = createTimeRaw
|
||||||
@@ -377,7 +371,6 @@ function parseDetailFromState(
|
|||||||
likeCount,
|
likeCount,
|
||||||
collectCount,
|
collectCount,
|
||||||
commentCount,
|
commentCount,
|
||||||
shareCount,
|
|
||||||
isLiked: false,
|
isLiked: false,
|
||||||
isFavorited: false,
|
isFavorited: false,
|
||||||
createTime,
|
createTime,
|
||||||
@@ -495,8 +488,6 @@ async function scrapeDetailFromDom(
|
|||||||
const likeCount = await extractCount(page, SEL.likeCount);
|
const likeCount = await extractCount(page, SEL.likeCount);
|
||||||
const collectCount = await extractCount(page, SEL.collectCount);
|
const collectCount = await extractCount(page, SEL.collectCount);
|
||||||
const commentCount = await extractCount(page, SEL.commentCount);
|
const commentCount = await extractCount(page, SEL.commentCount);
|
||||||
const shareCount = await extractCount(page, SEL.shareCount);
|
|
||||||
|
|
||||||
// Create time
|
// Create time
|
||||||
const createTime = await page
|
const createTime = await page
|
||||||
.$eval(SEL.createTime, (el) => el.textContent?.trim() ?? '')
|
.$eval(SEL.createTime, (el) => el.textContent?.trim() ?? '')
|
||||||
@@ -534,7 +525,6 @@ async function scrapeDetailFromDom(
|
|||||||
likeCount,
|
likeCount,
|
||||||
collectCount,
|
collectCount,
|
||||||
commentCount,
|
commentCount,
|
||||||
shareCount,
|
|
||||||
isLiked: false,
|
isLiked: false,
|
||||||
isFavorited: false,
|
isFavorited: false,
|
||||||
createTime,
|
createTime,
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ async function extractInitialState(page: Page): Promise<InitialState | null> {
|
|||||||
if (!s || typeof s !== 'object') return null;
|
if (!s || typeof s !== 'object') return null;
|
||||||
try {
|
try {
|
||||||
// structuredClone strips Vue proxies and produces a plain object.
|
// 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);
|
return JSON.stringify(plain);
|
||||||
} catch {
|
} catch {
|
||||||
// structuredClone may fail on some Vue internals — fall back to
|
// 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 MAX_DEPTH = 20;
|
||||||
const seen = new Set<unknown>();
|
const seen = new Set<unknown>();
|
||||||
const result = JSON.stringify(
|
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) {
|
function (_key, value) {
|
||||||
if (typeof value === 'function') return undefined;
|
if (typeof value === 'function') return undefined;
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export const XHS_SELECTORS = {
|
|||||||
/** Comment count. */
|
/** Comment count. */
|
||||||
commentCount: '.engage-bar .chat-wrapper .count',
|
commentCount: '.engage-bar .chat-wrapper .count',
|
||||||
/** Share count. */
|
/** Share count. */
|
||||||
shareCount: '.engage-bar .share-wrapper .count',
|
|
||||||
/** Publish / create time text. */
|
/** Publish / create time text. */
|
||||||
createTime: '.note-scroller .bottom-container .date',
|
createTime: '.note-scroller .bottom-container .date',
|
||||||
/** IP location. */
|
/** IP location. */
|
||||||
@@ -122,8 +121,6 @@ export const XHS_SELECTORS = {
|
|||||||
ipLocation: '.user-info .user-ip',
|
ipLocation: '.user-info .user-ip',
|
||||||
/** Follower / following / interaction count elements. */
|
/** Follower / following / interaction count elements. */
|
||||||
followCount: '.user-info .user-interactions > div',
|
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. */
|
/** Individual feed items on the user profile. */
|
||||||
feedItem: '.feeds-container .note-item',
|
feedItem: '.feeds-container .note-item',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export interface FeedDetail {
|
|||||||
likeCount: number;
|
likeCount: number;
|
||||||
collectCount: number;
|
collectCount: number;
|
||||||
commentCount: number;
|
commentCount: number;
|
||||||
shareCount: number;
|
|
||||||
isLiked: boolean;
|
isLiked: boolean;
|
||||||
isFavorited: boolean;
|
isFavorited: boolean;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
@@ -89,7 +88,6 @@ export interface UserProfile {
|
|||||||
follows: number;
|
follows: number;
|
||||||
fans: number;
|
fans: number;
|
||||||
interaction: number;
|
interaction: number;
|
||||||
feedCount: number;
|
|
||||||
feeds: Feed[];
|
feeds: Feed[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export async function getUserProfile(
|
|||||||
if (initialState) {
|
if (initialState) {
|
||||||
const profile = parseProfileFromState(initialState, userId, xsecToken);
|
const profile = parseProfileFromState(initialState, userId, xsecToken);
|
||||||
if (profile) {
|
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;
|
return profile;
|
||||||
}
|
}
|
||||||
log.debug('__INITIAL_STATE__ found but no profile data extracted, falling back to DOM');
|
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 fans = toNumber(interactions?.fans ?? userInfo.fans ?? 0);
|
||||||
const interaction = toNumber(interactions?.interaction ?? userInfo.interaction ?? 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.
|
// Notes / feeds on the profile page.
|
||||||
const rawNotes: RawProfileNote[] =
|
const rawNotes: RawProfileNote[] =
|
||||||
userPageData?.notes ?? state.userProfile?.notes ?? [];
|
userPageData?.notes ?? state.userProfile?.notes ?? [];
|
||||||
@@ -238,7 +232,6 @@ function parseProfileFromState(
|
|||||||
follows,
|
follows,
|
||||||
fans,
|
fans,
|
||||||
interaction,
|
interaction,
|
||||||
feedCount,
|
|
||||||
feeds,
|
feeds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -359,21 +352,6 @@ async function scrapeProfileFromDom(
|
|||||||
const fans = parseCountString(dataCounts[1] ?? '0');
|
const fans = parseCountString(dataCounts[1] ?? '0');
|
||||||
const interaction = parseCountString(dataCounts[2] ?? '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.
|
// Scrape feed items on the profile page.
|
||||||
const feedElements = await page.$$(SEL.feedItem);
|
const feedElements = await page.$$(SEL.feedItem);
|
||||||
const feeds: Feed[] = [];
|
const feeds: Feed[] = [];
|
||||||
@@ -430,7 +408,6 @@ async function scrapeProfileFromDom(
|
|||||||
follows,
|
follows,
|
||||||
fans,
|
fans,
|
||||||
interaction,
|
interaction,
|
||||||
feedCount,
|
|
||||||
feeds,
|
feeds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export interface FeedDetail {
|
|||||||
likeCount: number;
|
likeCount: number;
|
||||||
collectCount: number;
|
collectCount: number;
|
||||||
commentCount: number;
|
commentCount: number;
|
||||||
shareCount: number;
|
|
||||||
isLiked: boolean;
|
isLiked: boolean;
|
||||||
isFavorited: boolean;
|
isFavorited: boolean;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
@@ -74,7 +73,6 @@ export interface UserProfile {
|
|||||||
follows: number;
|
follows: number;
|
||||||
fans: number;
|
fans: number;
|
||||||
interaction: number;
|
interaction: number;
|
||||||
feedCount: number;
|
|
||||||
feeds: Feed[];
|
feeds: Feed[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
|
|||||||
{ label: '点赞', value: detail.likeCount },
|
{ label: '点赞', value: detail.likeCount },
|
||||||
{ label: '收藏', value: detail.collectCount },
|
{ label: '收藏', value: detail.collectCount },
|
||||||
{ label: '评论', value: detail.commentCount },
|
{ label: '评论', value: detail.commentCount },
|
||||||
{ label: '分享', value: detail.shareCount },
|
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
|
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
|
||||||
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
|
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
|
||||||
|
|||||||
@@ -54,12 +54,11 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{[
|
{[
|
||||||
{ label: '关注', value: profile.follows },
|
{ label: '关注', value: profile.follows },
|
||||||
{ label: '粉丝', value: profile.fans },
|
{ label: '粉丝', value: profile.fans },
|
||||||
{ label: '获赞与收藏', value: profile.interaction },
|
{ label: '获赞与收藏', value: profile.interaction },
|
||||||
{ label: '笔记', value: profile.feedCount },
|
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
|
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
|
||||||
<p className="text-lg font-bold">{formatNumber(s.value)}</p>
|
<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>
|
<span className="text-xs text-dark-muted">{status.userId.slice(0, 12)}…</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 size="sm" variant="danger" onClick={() => void handleLogout()} loading={logoutLoading}>
|
||||||
退出
|
退出
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user