feat: social-mcp 初始实现

多平台社交自动化 MCP 服务,首批支持小红书。

- 13 个 MCP 工具:登录管理、内容浏览、发布、互动
- 13 个 REST API 端点,支持 Bearer token 认证和限流
- BrowserManager:串行队列、背压、崩溃恢复
- Cookie 持久化:原子写入、0600 权限
- 安全:DNS rebinding 防御、错误脱敏、深层日志 redact
- Docker 部署支持
- 28 个单元测试全部通过
This commit is contained in:
2026-02-28 22:57:22 +08:00
commit 8da5f40c9f
38 changed files with 11273 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
// ---------------------------------------------------------------------------
// CSS Selectors — centralised so that UI changes only require edits here.
// ---------------------------------------------------------------------------
export const XHS_SELECTORS = {
login: {
/** QR code image on the login modal / page. */
qrCodeImage: '.login-container .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',
},
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. */
shareCount: '.engage-bar .share-wrapper .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 .list-item',
/** Parent comment content text. */
commentContent: '.content',
/** Comment author name. */
commentAuthor: '.author .name',
/** Comment author avatar. */
commentAvatar: '.author .avatar img',
/** 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',
},
userProfile: {
/** Profile header container. */
headerContainer: '.user-info',
/** User nickname. */
nickname: '.user-info .user-name',
/** User avatar image. */
avatar: '.user-info .user-image img',
/** 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 .data-area .data-item',
/** Note count (displayed somewhere on the profile page). */
noteCountTab: '.reds-tab-item',
/** 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: '#note-title',
/** Content / body editor area on the publish form (contenteditable). */
contentEditor: '#note-content',
/** The tag / topic button that opens the topic input. */
tagButton: '#topicBtn',
/** Tag / topic input field for typing hashtags. */
tagInput: '#topicBtn input',
/** Topic / hashtag suggestion dropdown item. */
tagSuggestionItem: '.publish-topic-item, .topic-item',
/** "Publish" / submit button. */
publishButton: '.publishBtn',
/** 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: '.upload-item img, .img-item img, .image-item img',
/** 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) --------------------------------
interaction: {
/** Like button on the feed detail page. */
likeButton: '.engage-bar .like-wrapper, span.like-wrapper',
/** Like button in active/liked state. */
likeButtonActive: '.engage-bar .like-wrapper.active, span.like-wrapper.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 .collect-wrapper, span.collect-wrapper',
/** Favorite button in active/favorited state. */
favoriteButtonActive: '.engage-bar .collect-wrapper.active, span.collect-wrapper.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;