Files
social-mcp/src/platforms/xiaohongshu/selectors.ts
T
kurihada 64dbc45265 新增评论通知功能:MCP工具 + REST端点 + 前端通知面板
- 新增 xhs_get_comment_notifications / xhs_reply_notification MCP工具
- 通知获取前先读取首页未读小红点数字,无未读则直接返回空,避免重复处理
- 新增 REST 端点 GET /notifications/comments 和 POST /notifications/reply
- 前端小红书页面新增「通知」按钮和 NotificationPanel slide-over 组件
- 通知面板支持查看评论通知列表和行内回复
2026-03-02 18:46:52 +08:00

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;