64dbc45265
- 新增 xhs_get_comment_notifications / xhs_reply_notification MCP工具 - 通知获取前先读取首页未读小红点数字,无未读则直接返回空,避免重复处理 - 新增 REST 端点 GET /notifications/comments 和 POST /notifications/reply - 前端小红书页面新增「通知」按钮和 NotificationPanel slide-over 组件 - 通知面板支持查看评论通知列表和行内回复
234 lines
10 KiB
TypeScript
234 lines
10 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// CSS Selectors — centralised so that UI changes only require edits here.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const XHS_SELECTORS = {
|
|
login: {
|
|
/** QR code image on the login modal (auto-appears after a few seconds). */
|
|
qrCodeImage: 'img.qrcode-img',
|
|
/** Element present only when the user is logged in (sidebar channel link). */
|
|
loggedInIndicator: '.user .link-wrapper .channel',
|
|
/** The "login" button that opens the QR code modal (if not already shown). */
|
|
loginButton: '.login-btn',
|
|
/** Logged-in user's avatar image in the sidebar. */
|
|
userAvatar: '.user .avatar img',
|
|
/** Logged-in user's profile link in the sidebar (href contains userId). */
|
|
userLink: '.user .link-wrapper a',
|
|
},
|
|
|
|
feed: {
|
|
/** Container for each feed card on the explore page. */
|
|
feedCard: '.note-item',
|
|
/** The cover image within a feed card. */
|
|
coverImage: '.note-item a.cover img',
|
|
/** The title/footer within a feed card. */
|
|
footerTitle: '.note-item .footer .title',
|
|
/** Author name within a feed card. */
|
|
authorName: '.note-item .footer .author-wrapper .name',
|
|
/** Author avatar within a feed card. */
|
|
authorAvatar: '.note-item .footer .author-wrapper .author-head img',
|
|
/** Like count within a feed card. */
|
|
likeCount: '.note-item .footer .like-wrapper .count',
|
|
},
|
|
|
|
search: {
|
|
/** Search result container. */
|
|
resultContainer: '#global-search-result-container',
|
|
/** Individual search result note items. */
|
|
noteItem: '.feeds-container .note-item',
|
|
/** Search result cover image. */
|
|
coverImage: '.feeds-container .note-item a.cover img',
|
|
/** Search result title. */
|
|
title: '.feeds-container .note-item .footer .title',
|
|
/** Search result author name. */
|
|
authorName: '.feeds-container .note-item .footer .author-wrapper .name',
|
|
/** Search result author avatar. */
|
|
authorAvatar: '.feeds-container .note-item .footer .author-wrapper .author-head img',
|
|
/** Search result like count. */
|
|
likeCount: '.feeds-container .note-item .footer .like-wrapper .count',
|
|
},
|
|
|
|
feedDetail: {
|
|
/** The main content container for a note detail page. */
|
|
noteContainer: '#noteContainer',
|
|
/** The title of the note. */
|
|
title: '#detail-title',
|
|
/** The description / body content of the note. */
|
|
description: '#detail-desc',
|
|
/** Individual images in an image note. */
|
|
images: '.note-image-list .note-image img',
|
|
/** The single hero image (some notes use this instead of a list). */
|
|
heroImage: '.note-hero img',
|
|
/** Video player element. */
|
|
video: '#videoplayer video',
|
|
/** Video player source. */
|
|
videoSource: '#videoplayer video source',
|
|
/** Tag links within the note body. */
|
|
tags: '#detail-desc a.tag',
|
|
/** Like count. */
|
|
likeCount: '.engage-bar .like-wrapper .count',
|
|
/** Collect (favorite) count. */
|
|
collectCount: '.engage-bar .collect-wrapper .count',
|
|
/** Comment count. */
|
|
commentCount: '.engage-bar .chat-wrapper .count',
|
|
/** Share count. */
|
|
/** Publish / create time text. */
|
|
createTime: '.note-scroller .bottom-container .date',
|
|
/** IP location. */
|
|
ipLocation: '.note-scroller .bottom-container .ip-location',
|
|
/** Author nickname on the detail page. */
|
|
authorName: '.author-container .info .name',
|
|
/** Author avatar on the detail page. */
|
|
authorAvatar: '.author-container .info .avatar img',
|
|
/** Author user ID link. */
|
|
authorLink: '.author-container .info a',
|
|
/** Comment list container. */
|
|
commentListContainer: '.comments-container .list-container',
|
|
/** Individual top-level comment items. */
|
|
commentItem: '.comments-container .list-container > .parent-comment > .comment-item',
|
|
/** Parent comment content text. */
|
|
commentContent: '.content',
|
|
/** Comment author name. */
|
|
commentAuthor: '.author .name',
|
|
/** Comment author avatar. */
|
|
commentAvatar: '.avatar img.avatar-item',
|
|
/** Comment like count. */
|
|
commentLikeCount: '.like .count',
|
|
/** Comment publish time. */
|
|
commentTime: '.date',
|
|
/** Comment IP location. */
|
|
commentIpLocation: '.ip-location',
|
|
/** Sub-comment (reply) items. */
|
|
subCommentItem: '.sub-comment-list .sub-comment-item',
|
|
/** "Show more comments" button. */
|
|
showMoreComments: '.comments-container .show-more',
|
|
/** "Load more replies" button within a comment thread. */
|
|
loadMoreReplies: '.sub-comment-list .show-more',
|
|
/** Sub-comment count text element (e.g. "展开 X 条回复"). */
|
|
subCommentCountText: '.sub-comment-list .show-more, .reply-container .show-more',
|
|
},
|
|
|
|
userProfile: {
|
|
/** Profile header container. */
|
|
headerContainer: '.user-info',
|
|
/** User nickname. */
|
|
nickname: '.user-info .user-name',
|
|
/** User avatar image (the img itself carries class user-image). */
|
|
avatar: '.user-info img.user-image',
|
|
/** User bio / description text. */
|
|
description: '.user-info .user-desc',
|
|
/** User gender icon or text. */
|
|
gender: '.user-info .gender-icon',
|
|
/** IP location. */
|
|
ipLocation: '.user-info .user-ip',
|
|
/** Follower / following / interaction count elements. */
|
|
followCount: '.user-info .user-interactions > div',
|
|
/** Individual feed items on the user profile. */
|
|
feedItem: '.feeds-container .note-item',
|
|
},
|
|
|
|
// -- Phase 4: Publish -----------------------------------------------------
|
|
|
|
publish: {
|
|
/** The file input element for uploading images on the creator publish page. */
|
|
imageFileInput: 'input[type="file"]',
|
|
/** Title input field on the publish form. */
|
|
titleInput: 'input.d-text[placeholder*="标题"]',
|
|
/** Content / body editor area on the publish form (contenteditable ProseMirror). */
|
|
contentEditor: '.tiptap.ProseMirror',
|
|
/** The tag / topic button that opens the topic input. */
|
|
tagButton: 'button.contentBtn.topic-btn',
|
|
/** Tag / topic input field for typing hashtags. */
|
|
tagInput: 'button.contentBtn.topic-btn input',
|
|
/** Topic / hashtag suggestion dropdown item. */
|
|
tagSuggestionItem: '.publish-topic-item, .topic-item',
|
|
/** "Publish" / submit button. */
|
|
publishButton: 'button.d-button:has-text("发布")',
|
|
/** Schedule / timing selector button. */
|
|
scheduleButton: '.timing-btn, button:has-text("定时")',
|
|
/** Schedule date/time input field. */
|
|
scheduleInput: '.timing-input input, .schedule-input input',
|
|
/** Original content declaration checkbox. */
|
|
originalCheckbox: '.original-checkbox input, input[type="checkbox"][name="original"]',
|
|
/** Visibility / permission setting button. */
|
|
visibilityButton: '.permission-btn, button:has-text("可见")',
|
|
/** Visibility option for public. */
|
|
visibilityPublic: '.permission-option:has-text("公开"), .visibility-option:has-text("公开")',
|
|
/** Visibility option for private. */
|
|
visibilityPrivate: '.permission-option:has-text("私密"), .visibility-option:has-text("私密")',
|
|
/** Visibility option for friends only. */
|
|
visibilityFriends: '.permission-option:has-text("好友"), .visibility-option:has-text("好友")',
|
|
/** Upload complete indicator (images uploaded and thumbnails visible). */
|
|
uploadedImageItem: '.img-upload-area .img-container',
|
|
/** Video upload complete indicator (video thumbnail visible). */
|
|
uploadedVideoItem: '.upload-video video, .video-item video, .video-container video',
|
|
/** Success indicator shown after publish completes. */
|
|
publishSuccess: '.success-panel, .publish-success, .note-success',
|
|
/** URL in the address bar after successful publish (used as a fallback check). */
|
|
publishSuccessUrlPattern: /\/publish\/success/,
|
|
},
|
|
|
|
// -- Phase 4: Comment / Reply ---------------------------------------------
|
|
|
|
comment: {
|
|
/** The comment input field / textarea on the feed detail page. */
|
|
commentInput: '#content-textarea',
|
|
/** Alternative comment input (contenteditable div). */
|
|
commentInputAlt: '[contenteditable][data-placeholder]',
|
|
/** Comment submit / send button. */
|
|
commentSubmitButton: '.comment-submit, button.submit, .btn-send',
|
|
/** Parent comment element (used to find specific comment by ID). */
|
|
commentItem: '.comment-item, .note-comment-item, [id^="comment-"]',
|
|
/** Reply button on an individual comment. */
|
|
commentReplyButton: '.reply-btn, .comment-reply',
|
|
/** Reply input that appears after clicking reply. */
|
|
replyInput: '.reply-input textarea, .reply-content [contenteditable], .reply-area textarea',
|
|
},
|
|
|
|
// -- Phase 4: Interaction (Like / Favorite) --------------------------------
|
|
|
|
// -- Phase 5: Notification ------------------------------------------------
|
|
|
|
notification: {
|
|
/** Each notification item container. */
|
|
container: '.container',
|
|
/** User avatar link (href contains userId + xsecToken). */
|
|
userAvatar: 'a.user-avatar',
|
|
/** User name link. */
|
|
userName: '.user-info a',
|
|
/** Interaction type hint (e.g. "评论了你的笔记"). */
|
|
interactionHint: '.interaction-hint span:first-child',
|
|
/** Notification time. */
|
|
interactionTime: '.interaction-time',
|
|
/** Comment content text. */
|
|
interactionContent: '.interaction-content',
|
|
/** Note thumbnail image (parent link href contains feedId + xsecToken). */
|
|
extraImage: '.extra img',
|
|
/** Reply button to expand inline reply. */
|
|
replyButton: '.action-reply',
|
|
/** Reply textarea that appears after clicking reply. */
|
|
replyInput: 'textarea.comment-input',
|
|
/** Reply submit button. */
|
|
replySubmit: 'button.submit',
|
|
/** Unread badge on the explore page bottom menu. */
|
|
unreadBadge: '#global > div.main-container > div.bottom-menu > div > li.link-wrapper.bottom-channel > a > div > div',
|
|
},
|
|
|
|
interaction: {
|
|
/** Like button on the feed detail page. */
|
|
likeButton: '.engage-bar-style .like-wrapper',
|
|
/** Like button in active/liked state. */
|
|
likeButtonActive: '.engage-bar-style .like-wrapper.like-active',
|
|
/** Like count element next to the like button. */
|
|
likeCount: '.engage-bar .like-wrapper .count',
|
|
/** Favorite / collect button on the feed detail page. */
|
|
favoriteButton: '.engage-bar-style .collect-wrapper',
|
|
/** Favorite button in active/favorited state. */
|
|
favoriteButtonActive: '.engage-bar-style .collect-wrapper.collect-active',
|
|
/** Favorite count element next to the favorite button. */
|
|
favoriteCount: '.engage-bar .collect-wrapper .count',
|
|
/** Container for the interaction bar at the bottom of a feed detail. */
|
|
interactionBar: '.interact-container, .engage-bar',
|
|
},
|
|
} as const;
|