@acedatacloud/skills 2026.714.3 → 2026.715.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/skills/telegram/SKILL.md +20 -6
  3. package/skills/telegram/scripts/tg.py +34 -3
  4. package/skills/xiaohongshu/SKILL.md +76 -136
  5. package/skills/xiaohongshu/tests/test_browser_contract.py +79 -0
  6. package/skills/xiaohongshu/README.md +0 -36
  7. package/skills/xiaohongshu/requirements.txt +0 -1
  8. package/skills/xiaohongshu/scripts/vendor/LICENSE.autoclaw-xiaohongshu-skills +0 -21
  9. package/skills/xiaohongshu/scripts/vendor/title_utils.py +0 -57
  10. package/skills/xiaohongshu/scripts/vendor/xhs/__init__.py +0 -1
  11. package/skills/xiaohongshu/scripts/vendor/xhs/cdp.py +0 -940
  12. package/skills/xiaohongshu/scripts/vendor/xhs/comment.py +0 -283
  13. package/skills/xiaohongshu/scripts/vendor/xhs/errors.py +0 -92
  14. package/skills/xiaohongshu/scripts/vendor/xhs/feed_detail.py +0 -550
  15. package/skills/xiaohongshu/scripts/vendor/xhs/feeds.py +0 -49
  16. package/skills/xiaohongshu/scripts/vendor/xhs/human.py +0 -79
  17. package/skills/xiaohongshu/scripts/vendor/xhs/like_favorite.py +0 -188
  18. package/skills/xiaohongshu/scripts/vendor/xhs/login.py +0 -377
  19. package/skills/xiaohongshu/scripts/vendor/xhs/publish.py +0 -1208
  20. package/skills/xiaohongshu/scripts/vendor/xhs/publish_long_article.py +0 -293
  21. package/skills/xiaohongshu/scripts/vendor/xhs/publish_video.py +0 -183
  22. package/skills/xiaohongshu/scripts/vendor/xhs/search.py +0 -226
  23. package/skills/xiaohongshu/scripts/vendor/xhs/selectors.py +0 -97
  24. package/skills/xiaohongshu/scripts/vendor/xhs/stealth.py +0 -316
  25. package/skills/xiaohongshu/scripts/vendor/xhs/types.py +0 -473
  26. package/skills/xiaohongshu/scripts/vendor/xhs/urls.py +0 -30
  27. package/skills/xiaohongshu/scripts/vendor/xhs/user_profile.py +0 -111
  28. package/skills/xiaohongshu/scripts/xiaohongshu.py +0 -1182
  29. package/skills/xiaohongshu/tests/test_xiaohongshu.py +0 -967
@@ -1,473 +0,0 @@
1
- """小红书数据类型定义,对应 Go types.go。"""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass, field
6
-
7
- # ========== Feed 列表 ==========
8
-
9
-
10
- @dataclass
11
- class ImageInfo:
12
- image_scene: str = ""
13
- url: str = ""
14
-
15
- @classmethod
16
- def from_dict(cls, d: dict) -> ImageInfo:
17
- return cls(
18
- image_scene=d.get("imageScene", ""),
19
- url=d.get("url", ""),
20
- )
21
-
22
-
23
- @dataclass
24
- class VideoCapability:
25
- duration: int = 0 # 秒
26
-
27
- @classmethod
28
- def from_dict(cls, d: dict) -> VideoCapability:
29
- return cls(duration=d.get("duration", 0))
30
-
31
-
32
- @dataclass
33
- class Video:
34
- capa: VideoCapability = field(default_factory=VideoCapability)
35
-
36
- @classmethod
37
- def from_dict(cls, d: dict) -> Video:
38
- return cls(capa=VideoCapability.from_dict(d.get("capa", {})))
39
-
40
-
41
- @dataclass
42
- class Cover:
43
- width: int = 0
44
- height: int = 0
45
- url: str = ""
46
- file_id: str = ""
47
- url_pre: str = ""
48
- url_default: str = ""
49
- info_list: list[ImageInfo] = field(default_factory=list)
50
-
51
- @classmethod
52
- def from_dict(cls, d: dict) -> Cover:
53
- return cls(
54
- width=d.get("width", 0),
55
- height=d.get("height", 0),
56
- url=d.get("url", ""),
57
- file_id=d.get("fileId", ""),
58
- url_pre=d.get("urlPre", ""),
59
- url_default=d.get("urlDefault", ""),
60
- info_list=[ImageInfo.from_dict(i) for i in d.get("infoList", [])],
61
- )
62
-
63
-
64
- @dataclass
65
- class User:
66
- user_id: str = ""
67
- nickname: str = ""
68
- nick_name: str = ""
69
- avatar: str = ""
70
-
71
- @classmethod
72
- def from_dict(cls, d: dict) -> User:
73
- return cls(
74
- user_id=d.get("userId", ""),
75
- nickname=d.get("nickname", ""),
76
- nick_name=d.get("nickName", ""),
77
- avatar=d.get("avatar", ""),
78
- )
79
-
80
-
81
- @dataclass
82
- class InteractInfo:
83
- liked: bool = False
84
- liked_count: str = ""
85
- shared_count: str = ""
86
- comment_count: str = ""
87
- collected_count: str = ""
88
- collected: bool = False
89
-
90
- @classmethod
91
- def from_dict(cls, d: dict) -> InteractInfo:
92
- return cls(
93
- liked=d.get("liked", False),
94
- liked_count=d.get("likedCount", ""),
95
- shared_count=d.get("sharedCount", ""),
96
- comment_count=d.get("commentCount", ""),
97
- collected_count=d.get("collectedCount", ""),
98
- collected=d.get("collected", False),
99
- )
100
-
101
-
102
- @dataclass
103
- class NoteCard:
104
- type: str = ""
105
- display_title: str = ""
106
- user: User = field(default_factory=User)
107
- interact_info: InteractInfo = field(default_factory=InteractInfo)
108
- cover: Cover = field(default_factory=Cover)
109
- video: Video | None = None
110
-
111
- @classmethod
112
- def from_dict(cls, d: dict) -> NoteCard:
113
- video_data = d.get("video")
114
- return cls(
115
- type=d.get("type", ""),
116
- display_title=d.get("displayTitle", ""),
117
- user=User.from_dict(d.get("user", {})),
118
- interact_info=InteractInfo.from_dict(d.get("interactInfo", {})),
119
- cover=Cover.from_dict(d.get("cover", {})),
120
- video=Video.from_dict(video_data) if video_data else None,
121
- )
122
-
123
-
124
- @dataclass
125
- class Feed:
126
- xsec_token: str = ""
127
- id: str = ""
128
- model_type: str = ""
129
- note_card: NoteCard = field(default_factory=NoteCard)
130
- index: int = 0
131
-
132
- @classmethod
133
- def from_dict(cls, d: dict) -> Feed:
134
- return cls(
135
- xsec_token=d.get("xsecToken", ""),
136
- id=d.get("id", ""),
137
- model_type=d.get("modelType", ""),
138
- note_card=NoteCard.from_dict(d.get("noteCard", {})),
139
- index=d.get("index", 0),
140
- )
141
-
142
- def to_dict(self) -> dict:
143
- """序列化为 JSON 兼容的字典。"""
144
- result: dict = {
145
- "id": self.id,
146
- "xsecToken": self.xsec_token,
147
- "modelType": self.model_type,
148
- "index": self.index,
149
- "displayTitle": self.note_card.display_title,
150
- "type": self.note_card.type,
151
- "user": {
152
- "userId": self.note_card.user.user_id,
153
- "nickname": self.note_card.user.nickname or self.note_card.user.nick_name,
154
- },
155
- "interactInfo": {
156
- "likedCount": self.note_card.interact_info.liked_count,
157
- "collectedCount": self.note_card.interact_info.collected_count,
158
- "commentCount": self.note_card.interact_info.comment_count,
159
- "sharedCount": self.note_card.interact_info.shared_count,
160
- },
161
- }
162
- cover = self.note_card.cover
163
- if cover.url or cover.url_default:
164
- result["cover"] = cover.url or cover.url_default
165
- if self.note_card.video:
166
- result["video"] = {"duration": self.note_card.video.capa.duration}
167
- return result
168
-
169
-
170
- # ========== Feed 详情 ==========
171
-
172
-
173
- @dataclass
174
- class DetailImageInfo:
175
- width: int = 0
176
- height: int = 0
177
- url_default: str = ""
178
- url_pre: str = ""
179
- live_photo: bool = False
180
-
181
- @classmethod
182
- def from_dict(cls, d: dict) -> DetailImageInfo:
183
- return cls(
184
- width=d.get("width", 0),
185
- height=d.get("height", 0),
186
- url_default=d.get("urlDefault", ""),
187
- url_pre=d.get("urlPre", ""),
188
- live_photo=d.get("livePhoto", False),
189
- )
190
-
191
-
192
- @dataclass
193
- class Comment:
194
- id: str = ""
195
- note_id: str = ""
196
- content: str = ""
197
- like_count: str = ""
198
- create_time: int = 0
199
- ip_location: str = ""
200
- liked: bool = False
201
- user_info: User = field(default_factory=User)
202
- sub_comment_count: str = ""
203
- sub_comments: list[Comment] = field(default_factory=list)
204
- show_tags: list[str] = field(default_factory=list)
205
-
206
- @classmethod
207
- def from_dict(cls, d: dict) -> Comment:
208
- return cls(
209
- id=d.get("id", ""),
210
- note_id=d.get("noteId", ""),
211
- content=d.get("content", ""),
212
- like_count=d.get("likeCount", ""),
213
- create_time=d.get("createTime", 0),
214
- ip_location=d.get("ipLocation", ""),
215
- liked=d.get("liked", False),
216
- user_info=User.from_dict(d.get("userInfo", {})),
217
- sub_comment_count=d.get("subCommentCount", ""),
218
- sub_comments=[cls.from_dict(c) for c in d.get("subComments", []) or []],
219
- show_tags=d.get("showTags", []) or [],
220
- )
221
-
222
- def to_dict(self) -> dict:
223
- result: dict = {
224
- "id": self.id,
225
- "content": self.content,
226
- "likeCount": self.like_count,
227
- "createTime": self.create_time,
228
- "ipLocation": self.ip_location,
229
- "user": {
230
- "userId": self.user_info.user_id,
231
- "nickname": self.user_info.nickname or self.user_info.nick_name,
232
- },
233
- "subCommentCount": self.sub_comment_count,
234
- }
235
- if self.sub_comments:
236
- result["subComments"] = [c.to_dict() for c in self.sub_comments]
237
- return result
238
-
239
-
240
- @dataclass
241
- class CommentList:
242
- list_: list[Comment] = field(default_factory=list)
243
- cursor: str = ""
244
- has_more: bool = False
245
-
246
- @classmethod
247
- def from_dict(cls, d: dict) -> CommentList:
248
- return cls(
249
- list_=[Comment.from_dict(c) for c in d.get("list", []) or []],
250
- cursor=d.get("cursor", ""),
251
- has_more=d.get("hasMore", False),
252
- )
253
-
254
-
255
- @dataclass
256
- class FeedDetail:
257
- note_id: str = ""
258
- xsec_token: str = ""
259
- title: str = ""
260
- desc: str = ""
261
- body: str = "" # DOM 正文(干净文本,不含 [话题] 标记)
262
- tags: list[str] = field(default_factory=list) # 话题标签列表
263
- type: str = ""
264
- time: int = 0
265
- ip_location: str = ""
266
- user: User = field(default_factory=User)
267
- interact_info: InteractInfo = field(default_factory=InteractInfo)
268
- image_list: list[DetailImageInfo] = field(default_factory=list)
269
-
270
- @classmethod
271
- def from_dict(cls, d: dict) -> FeedDetail:
272
- return cls(
273
- note_id=d.get("noteId", ""),
274
- xsec_token=d.get("xsecToken", ""),
275
- title=d.get("title", ""),
276
- desc=d.get("desc", ""),
277
- body=d.get("_domBody", ""),
278
- tags=d.get("_domTags", []),
279
- type=d.get("type", ""),
280
- time=d.get("time", 0),
281
- ip_location=d.get("ipLocation", ""),
282
- user=User.from_dict(d.get("user", {})),
283
- interact_info=InteractInfo.from_dict(d.get("interactInfo", {})),
284
- image_list=[DetailImageInfo.from_dict(i) for i in d.get("imageList", []) or []],
285
- )
286
-
287
- def to_dict(self) -> dict:
288
- return {
289
- "noteId": self.note_id,
290
- "title": self.title,
291
- "desc": self.desc,
292
- "body": self.body,
293
- "tags": self.tags,
294
- "type": self.type,
295
- "time": self.time,
296
- "ipLocation": self.ip_location,
297
- "user": {
298
- "userId": self.user.user_id,
299
- "nickname": self.user.nickname or self.user.nick_name,
300
- },
301
- "interactInfo": {
302
- "liked": self.interact_info.liked,
303
- "likedCount": self.interact_info.liked_count,
304
- "collectedCount": self.interact_info.collected_count,
305
- "collected": self.interact_info.collected,
306
- "commentCount": self.interact_info.comment_count,
307
- "sharedCount": self.interact_info.shared_count,
308
- },
309
- "imageList": [
310
- {
311
- "width": img.width,
312
- "height": img.height,
313
- "urlDefault": img.url_default,
314
- }
315
- for img in self.image_list
316
- ],
317
- }
318
-
319
-
320
- @dataclass
321
- class FeedDetailResponse:
322
- note: FeedDetail = field(default_factory=FeedDetail)
323
- comments: CommentList = field(default_factory=CommentList)
324
-
325
- @classmethod
326
- def from_dict(cls, d: dict) -> FeedDetailResponse:
327
- return cls(
328
- note=FeedDetail.from_dict(d.get("note", {})),
329
- comments=CommentList.from_dict(d.get("comments", {})),
330
- )
331
-
332
- def to_dict(self) -> dict:
333
- return {
334
- "note": self.note.to_dict(),
335
- "comments": [c.to_dict() for c in self.comments.list_],
336
- }
337
-
338
-
339
- # ========== 用户主页 ==========
340
-
341
-
342
- @dataclass
343
- class UserBasicInfo:
344
- gender: int = 0
345
- ip_location: str = ""
346
- desc: str = ""
347
- imageb: str = ""
348
- nickname: str = ""
349
- images: str = ""
350
- red_id: str = ""
351
-
352
- @classmethod
353
- def from_dict(cls, d: dict) -> UserBasicInfo:
354
- return cls(
355
- gender=d.get("gender", 0),
356
- ip_location=d.get("ipLocation", ""),
357
- desc=d.get("desc", ""),
358
- imageb=d.get("imageb", ""),
359
- nickname=d.get("nickname", ""),
360
- images=d.get("images", ""),
361
- red_id=d.get("redId", ""),
362
- )
363
-
364
-
365
- @dataclass
366
- class UserInteraction:
367
- type: str = ""
368
- name: str = ""
369
- count: str = ""
370
-
371
- @classmethod
372
- def from_dict(cls, d: dict) -> UserInteraction:
373
- return cls(
374
- type=d.get("type", ""),
375
- name=d.get("name", ""),
376
- count=d.get("count", ""),
377
- )
378
-
379
-
380
- @dataclass
381
- class UserProfileResponse:
382
- user_basic_info: UserBasicInfo = field(default_factory=UserBasicInfo)
383
- interactions: list[UserInteraction] = field(default_factory=list)
384
- feeds: list[Feed] = field(default_factory=list)
385
-
386
- def to_dict(self) -> dict:
387
- return {
388
- "basicInfo": {
389
- "nickname": self.user_basic_info.nickname,
390
- "redId": self.user_basic_info.red_id,
391
- "desc": self.user_basic_info.desc,
392
- "gender": self.user_basic_info.gender,
393
- "ipLocation": self.user_basic_info.ip_location,
394
- },
395
- "interactions": [
396
- {"type": i.type, "name": i.name, "count": i.count} for i in self.interactions
397
- ],
398
- "feeds": [f.to_dict() for f in self.feeds],
399
- }
400
-
401
-
402
- # ========== 搜索 ==========
403
-
404
-
405
- @dataclass
406
- class FilterOption:
407
- """搜索筛选选项。"""
408
-
409
- sort_by: str = "" # 综合|最新|最多点赞|最多评论|最多收藏
410
- note_type: str = "" # 不限|视频|图文
411
- publish_time: str = "" # 不限|一天内|一周内|半年内
412
- search_scope: str = "" # 不限|已看过|未看过|已关注
413
- location: str = "" # 不限|同城|附近
414
-
415
-
416
- # ========== 发布 ==========
417
-
418
-
419
- @dataclass
420
- class PublishImageContent:
421
- """图文发布内容。"""
422
-
423
- title: str = ""
424
- content: str = ""
425
- tags: list[str] = field(default_factory=list)
426
- image_paths: list[str] = field(default_factory=list)
427
- schedule_time: str | None = None # ISO8601 格式,None 表示立即发布
428
- is_original: bool = False
429
- visibility: str = "" # 公开可见(默认)|仅自己可见|仅互关好友可见
430
-
431
-
432
- @dataclass
433
- class PublishVideoContent:
434
- """视频发布内容。"""
435
-
436
- title: str = ""
437
- content: str = ""
438
- tags: list[str] = field(default_factory=list)
439
- video_path: str = ""
440
- schedule_time: str | None = None # ISO8601 格式
441
- visibility: str = "" # 公开可见(默认)|仅自己可见|仅互关好友可见
442
-
443
-
444
- # ========== 互动 ==========
445
-
446
-
447
- @dataclass
448
- class ActionResult:
449
- """通用动作响应(点赞/收藏等)。"""
450
-
451
- feed_id: str = ""
452
- success: bool = False
453
- message: str = ""
454
-
455
- def to_dict(self) -> dict:
456
- return {
457
- "feed_id": self.feed_id,
458
- "success": self.success,
459
- "message": self.message,
460
- }
461
-
462
-
463
- # ========== 评论加载配置 ==========
464
-
465
-
466
- @dataclass
467
- class CommentLoadConfig:
468
- """评论加载配置。"""
469
-
470
- click_more_replies: bool = False
471
- max_replies_threshold: int = 10
472
- max_comment_items: int = 0 # 0 = 不限
473
- scroll_speed: str = "normal" # slow|normal|fast
@@ -1,30 +0,0 @@
1
- """小红书 URL 常量和构建函数。"""
2
-
3
- from urllib.parse import urlencode
4
-
5
- # 基础页面
6
- EXPLORE_URL = "https://www.xiaohongshu.com/explore"
7
- HOME_URL = "https://www.xiaohongshu.com"
8
- PUBLISH_URL = "https://creator.xiaohongshu.com/publish/publish?source=official"
9
-
10
-
11
- def make_feed_detail_url(feed_id: str, xsec_token: str, xsec_source: str = "pc_feed") -> str:
12
- """构建 feed 详情页 URL。"""
13
- return (
14
- f"https://www.xiaohongshu.com/explore/{feed_id}"
15
- f"?xsec_token={xsec_token}&xsec_source={xsec_source}"
16
- )
17
-
18
-
19
- def make_search_url(keyword: str) -> str:
20
- """构建搜索结果页 URL。"""
21
- params = urlencode({"keyword": keyword, "source": "web_explore_feed"})
22
- return f"https://www.xiaohongshu.com/search_result?{params}"
23
-
24
-
25
- def make_user_profile_url(user_id: str, xsec_token: str) -> str:
26
- """构建用户主页 URL。"""
27
- return (
28
- f"https://www.xiaohongshu.com/user/profile/{user_id}"
29
- f"?xsec_token={xsec_token}&xsec_source=pc_note"
30
- )
@@ -1,111 +0,0 @@
1
- """用户主页,对应 Go xiaohongshu/user_profile.go。"""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import logging
7
- import time
8
-
9
- from .cdp import Page
10
- from .types import Feed, UserBasicInfo, UserInteraction, UserProfileResponse
11
- from .urls import make_user_profile_url
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- # 提取用户数据的 JS
16
- _EXTRACT_USER_DATA_JS = """
17
- (() => {
18
- if (window.__INITIAL_STATE__ &&
19
- window.__INITIAL_STATE__.user &&
20
- window.__INITIAL_STATE__.user.userPageData) {
21
- const userPageData = window.__INITIAL_STATE__.user.userPageData;
22
- const data = userPageData.value !== undefined ? userPageData.value : userPageData._value;
23
- if (data) {
24
- return JSON.stringify(data);
25
- }
26
- }
27
- return "";
28
- })()
29
- """
30
-
31
- _EXTRACT_USER_NOTES_JS = """
32
- (() => {
33
- if (window.__INITIAL_STATE__ &&
34
- window.__INITIAL_STATE__.user &&
35
- window.__INITIAL_STATE__.user.notes) {
36
- const notes = window.__INITIAL_STATE__.user.notes;
37
- const data = notes.value !== undefined ? notes.value : notes._value;
38
- if (data) {
39
- return JSON.stringify(data);
40
- }
41
- }
42
- return "";
43
- })()
44
- """
45
-
46
-
47
- def get_user_profile(page: Page, user_id: str, xsec_token: str) -> UserProfileResponse:
48
- """获取用户主页信息及帖子。
49
-
50
- Args:
51
- page: CDP 页面对象。
52
- user_id: 用户 ID。
53
- xsec_token: xsec_token。
54
-
55
- Raises:
56
- RuntimeError: 数据提取失败。
57
- """
58
- url = make_user_profile_url(user_id, xsec_token)
59
- page.navigate(url)
60
- page.wait_for_load()
61
- page.wait_dom_stable()
62
-
63
- return _extract_user_profile_data(page)
64
-
65
-
66
- def _extract_user_profile_data(page: Page) -> UserProfileResponse:
67
- """从页面提取用户资料数据。"""
68
- # 等待 __INITIAL_STATE__
69
- _wait_for_initial_state(page)
70
-
71
- # 提取用户信息
72
- user_data_result = page.evaluate(_EXTRACT_USER_DATA_JS)
73
- if not user_data_result:
74
- raise RuntimeError("user.userPageData.value not found in __INITIAL_STATE__")
75
-
76
- # 提取用户帖子
77
- notes_result = page.evaluate(_EXTRACT_USER_NOTES_JS)
78
- if not notes_result:
79
- raise RuntimeError("user.notes.value not found in __INITIAL_STATE__")
80
-
81
- # 解析用户信息
82
- user_page_data = json.loads(user_data_result)
83
- basic_info = UserBasicInfo.from_dict(user_page_data.get("basicInfo", {}))
84
- interactions = [UserInteraction.from_dict(i) for i in user_page_data.get("interactions", [])]
85
-
86
- # 解析帖子(双重数组,展平)
87
- notes_feeds_raw = json.loads(notes_result)
88
- feeds: list[Feed] = []
89
- for feed_group in notes_feeds_raw:
90
- if isinstance(feed_group, list):
91
- for f in feed_group:
92
- feeds.append(Feed.from_dict(f))
93
- elif isinstance(feed_group, dict):
94
- feeds.append(Feed.from_dict(feed_group))
95
-
96
- return UserProfileResponse(
97
- user_basic_info=basic_info,
98
- interactions=interactions,
99
- feeds=feeds,
100
- )
101
-
102
-
103
- def _wait_for_initial_state(page: Page, timeout: float = 10.0) -> None:
104
- """等待 __INITIAL_STATE__ 就绪。"""
105
- deadline = time.monotonic() + timeout
106
- while time.monotonic() < deadline:
107
- ready = page.evaluate("window.__INITIAL_STATE__ !== undefined")
108
- if ready:
109
- return
110
- time.sleep(0.5)
111
- logger.warning("等待 __INITIAL_STATE__ 超时")