@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,283 +0,0 @@
1
- """评论操作,对应 Go xiaohongshu/comment_feed.go。"""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
-
7
- from .cdp import Page
8
- from .feed_detail import _check_end_container, _check_page_accessible, _get_comment_count
9
- from .human import sleep_random
10
- from .selectors import (
11
- COMMENT_INPUT_FIELD,
12
- COMMENT_INPUT_TRIGGER,
13
- COMMENT_SUBMIT_BUTTON,
14
- PARENT_COMMENT,
15
- REPLY_BUTTON,
16
- )
17
- from .urls import make_feed_detail_url
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
-
22
- def post_comment(
23
- page: Page,
24
- feed_id: str,
25
- xsec_token: str,
26
- content: str,
27
- xsec_source: str = "pc_feed",
28
- ) -> None:
29
- """发表评论到 Feed。
30
-
31
- Args:
32
- page: CDP 页面对象。
33
- feed_id: Feed ID。
34
- xsec_token: xsec_token。
35
- content: 评论内容。
36
-
37
- Raises:
38
- RuntimeError: 评论失败。
39
- """
40
- url = make_feed_detail_url(feed_id, xsec_token, xsec_source)
41
- logger.info("打开 feed 详情页: %s", url)
42
-
43
- page.navigate(url)
44
- page.wait_for_load()
45
- page.wait_dom_stable()
46
- sleep_random(800, 1500)
47
-
48
- _check_page_accessible(page)
49
- initial_matches = _comment_content_matches(page, content)
50
-
51
- # 点击评论输入触发区域
52
- if not page.has_element(COMMENT_INPUT_TRIGGER):
53
- raise RuntimeError("未找到评论输入框,该帖子可能不支持评论或网页端不可访问")
54
-
55
- page.click_element(COMMENT_INPUT_TRIGGER)
56
- sleep_random(400, 800)
57
-
58
- # 输入评论内容(CDP 逐字输入)
59
- page.wait_for_element(COMMENT_INPUT_FIELD, timeout=5)
60
- page.input_content_editable(COMMENT_INPUT_FIELD, content)
61
- sleep_random(600, 1200)
62
-
63
- # 点击提交
64
- if not page.has_element(COMMENT_SUBMIT_BUTTON):
65
- raise RuntimeError("评论提交按钮不可用")
66
- page.click_element(COMMENT_SUBMIT_BUTTON)
67
- sleep_random(800, 1500)
68
-
69
- if not _comment_editor_empty(page):
70
- raise RuntimeError("评论未确认提交成功")
71
- if not _wait_for_comment_content(page, content, initial_matches):
72
- raise RuntimeError("评论未在页面中出现")
73
-
74
- logger.info("评论发送成功: feed=%s", feed_id)
75
-
76
-
77
- def reply_comment(
78
- page: Page,
79
- feed_id: str,
80
- xsec_token: str,
81
- content: str,
82
- comment_id: str = "",
83
- user_id: str = "",
84
- xsec_source: str = "pc_feed",
85
- ) -> None:
86
- """回复指定评论。
87
-
88
- 通过 comment_id 或 user_id 定位评论,然后回复。
89
-
90
- Args:
91
- page: CDP 页面对象。
92
- feed_id: Feed ID。
93
- xsec_token: xsec_token。
94
- content: 回复内容。
95
- comment_id: 评论 ID(优先使用)。
96
- user_id: 用户 ID(备选)。
97
-
98
- Raises:
99
- RuntimeError: 回复失败。
100
- """
101
- if not comment_id and not user_id:
102
- raise ValueError("comment_id 和 user_id 至少提供一个")
103
-
104
- url = make_feed_detail_url(feed_id, xsec_token, xsec_source)
105
- logger.info("打开 feed 详情页进行回复: %s", url)
106
-
107
- page.navigate(url)
108
- page.wait_for_load()
109
- page.wait_dom_stable()
110
- sleep_random(800, 1500)
111
-
112
- _check_page_accessible(page)
113
- sleep_random(1500, 2500)
114
- initial_matches = _comment_content_matches(page, content)
115
-
116
- # 查找目标评论
117
- target_selector = _find_and_scroll_to_comment(page, comment_id, user_id)
118
- if not target_selector:
119
- raise RuntimeError(f"未找到评论 (commentID: {comment_id}, userID: {user_id})")
120
-
121
- sleep_random(800, 1500)
122
-
123
- # 点击回复按钮
124
- reply_selector = f"{target_selector} {REPLY_BUTTON}"
125
- page.click_element(reply_selector)
126
- sleep_random(800, 1500)
127
-
128
- # 输入回复内容(CDP 逐字输入)
129
- page.wait_for_element(COMMENT_INPUT_FIELD, timeout=5)
130
- page.input_content_editable(COMMENT_INPUT_FIELD, content)
131
- sleep_random(600, 1200)
132
-
133
- # 点击提交
134
- if not page.has_element(COMMENT_SUBMIT_BUTTON):
135
- raise RuntimeError("回复提交按钮不可用")
136
- page.click_element(COMMENT_SUBMIT_BUTTON)
137
- sleep_random(1500, 2500)
138
-
139
- if not _comment_editor_empty(page):
140
- raise RuntimeError("回复未确认提交成功")
141
- if not _wait_for_comment_content(page, content, initial_matches):
142
- raise RuntimeError("回复未在页面中出现")
143
-
144
- logger.info("回复评论成功")
145
-
146
-
147
- def _find_and_scroll_to_comment(
148
- page: Page,
149
- comment_id: str,
150
- user_id: str,
151
- max_attempts: int = 100,
152
- ) -> str | None:
153
- """查找并滚动到目标评论。"""
154
- logger.info("开始查找评论 - commentID: %s, userID: %s", comment_id, user_id)
155
-
156
- # 先滚动到评论区
157
- page.scroll_element_into_view(".comments-container")
158
- sleep_random(800, 1500)
159
-
160
- last_count = 0
161
- stagnant = 0
162
-
163
- for attempt in range(max_attempts):
164
- if _locate_target_comment(page, comment_id, user_id, attempt):
165
- return f"#comment-{comment_id}" if comment_id else '[data-acedata-reply-target="true"]'
166
-
167
- # 检查是否到底
168
- if _check_end_container(page):
169
- logger.info("已到达评论底部,未找到目标评论")
170
- break
171
-
172
- # 停滞检测
173
- current_count = _get_comment_count(page)
174
- if current_count != last_count:
175
- last_count = current_count
176
- stagnant = 0
177
- else:
178
- stagnant += 1
179
- if stagnant >= 10:
180
- logger.info("评论数量停滞超过10次")
181
- break
182
-
183
- # 滚动到最后一条评论
184
- if current_count > 0:
185
- page.scroll_nth_element_into_view(PARENT_COMMENT, current_count - 1)
186
- sleep_random(200, 500)
187
-
188
- # 继续滚动
189
- page.evaluate("window.scrollBy(0, window.innerHeight * 0.8)")
190
- sleep_random(400, 800)
191
-
192
- if _locate_target_comment(page, comment_id, user_id, attempt):
193
- return f"#comment-{comment_id}" if comment_id else '[data-acedata-reply-target="true"]'
194
-
195
- sleep_random(600, 1200)
196
-
197
- return None
198
-
199
-
200
- def _locate_target_comment(page: Page, comment_id: str, user_id: str, attempt: int) -> bool:
201
- if comment_id:
202
- selector = f"#comment-{comment_id}"
203
- if page.has_element(selector):
204
- logger.info("通过 commentID 找到评论 (尝试 %d 次)", attempt + 1)
205
- page.scroll_element_into_view(selector)
206
- return True
207
-
208
- if user_id:
209
- found = page.evaluate(
210
- f"""
211
- (() => {{
212
- const userId = {_js_str(user_id)};
213
- const els = document.querySelectorAll(
214
- '.parent-comment, .comment-item, .comment'
215
- );
216
- for (const el of els) {{
217
- if ([...el.querySelectorAll('[data-user-id]')]
218
- .some(node => node.getAttribute('data-user-id') === userId)) {{
219
- document.querySelectorAll('[data-acedata-reply-target]')
220
- .forEach(node => node.removeAttribute('data-acedata-reply-target'));
221
- el.setAttribute('data-acedata-reply-target', 'true');
222
- el.scrollIntoView({{behavior: 'smooth', block: 'center'}});
223
- return true;
224
- }}
225
- }}
226
- return false;
227
- }})()
228
- """
229
- )
230
- if found:
231
- logger.info("通过 userID 找到评论 (尝试 %d 次)", attempt + 1)
232
- return True
233
- return False
234
-
235
-
236
- def _js_str(s: str) -> str:
237
- """将 Python 字符串转为 JS 字面量(含引号)。"""
238
- import json
239
-
240
- return json.dumps(s)
241
-
242
-
243
- def _comment_editor_empty(page: Page) -> bool:
244
- return bool(
245
- page.evaluate(
246
- f"""
247
- (() => {{
248
- const editor = document.querySelector({_js_str(COMMENT_INPUT_FIELD)});
249
- if (!editor) return true;
250
- return !((editor.value || editor.textContent || '').trim());
251
- }})()
252
- """
253
- )
254
- )
255
-
256
-
257
- def _comment_content_matches(page: Page, content: str) -> int:
258
- return int(
259
- page.evaluate(
260
- f"""
261
- (() => {{
262
- const expected = {_js_str(content)};
263
- return [...document.querySelectorAll(
264
- '.parent-comment .content, .comment-item .content, .comment .content'
265
- )].filter(node => (node.textContent || '').trim() === expected).length;
266
- }})()
267
- """
268
- )
269
- or 0
270
- )
271
-
272
-
273
- def _wait_for_comment_content(
274
- page: Page, content: str, initial_matches: int, timeout: float = 10
275
- ) -> bool:
276
- import time
277
-
278
- deadline = time.monotonic() + timeout
279
- while time.monotonic() < deadline:
280
- if _comment_content_matches(page, content) > initial_matches:
281
- return True
282
- time.sleep(0.3)
283
- return False
@@ -1,92 +0,0 @@
1
- """小红书自动化异常体系。"""
2
-
3
-
4
- class XHSError(Exception):
5
- """小红书自动化基础异常。"""
6
-
7
-
8
- class NoFeedsError(XHSError):
9
- """没有捕获到 feeds 数据。"""
10
-
11
- def __init__(self) -> None:
12
- super().__init__("没有捕获到 feeds 数据")
13
-
14
-
15
- class NoFeedDetailError(XHSError):
16
- """没有捕获到 feed 详情数据。"""
17
-
18
- def __init__(self) -> None:
19
- super().__init__("没有捕获到 feed 详情数据")
20
-
21
-
22
- class NotLoggedInError(XHSError):
23
- """未登录。"""
24
-
25
- def __init__(self) -> None:
26
- super().__init__("未登录,请先扫码登录")
27
-
28
-
29
- class PageNotAccessibleError(XHSError):
30
- """页面不可访问。"""
31
-
32
- def __init__(self, reason: str) -> None:
33
- self.reason = reason
34
- super().__init__(f"笔记不可访问: {reason}")
35
-
36
-
37
- class UploadTimeoutError(XHSError):
38
- """上传超时。"""
39
-
40
-
41
- class PublishError(XHSError):
42
- """发布失败。"""
43
-
44
-
45
- class AccountRiskControlError(PublishError):
46
- """账号被风控,无法发布。
47
-
48
- XHS 后端业务错误码(HTTP 200 + code≠0):
49
- - -9136: 因违反社区规范禁止发笔记
50
- """
51
-
52
- def __init__(self, code: int, msg: str) -> None:
53
- self.code = code
54
- self.msg = msg
55
- super().__init__(f"账号被风控(code={code}):{msg}")
56
-
57
-
58
- class TitleTooLongError(PublishError):
59
- """标题超过长度限制。"""
60
-
61
- def __init__(self, current: str, maximum: str) -> None:
62
- self.current = current
63
- self.maximum = maximum
64
- super().__init__(f"当前输入长度为{current},最大长度为{maximum}")
65
-
66
-
67
- class ContentTooLongError(PublishError):
68
- """正文超过长度限制。"""
69
-
70
- def __init__(self, current: str, maximum: str) -> None:
71
- self.current = current
72
- self.maximum = maximum
73
- super().__init__(f"当前输入长度为{current},最大长度为{maximum}")
74
-
75
-
76
- class RateLimitError(XHSError):
77
- """请求频率过高,验证码获取失败。"""
78
-
79
- def __init__(self) -> None:
80
- super().__init__("请求太频繁,验证码获取失败,请重启浏览器后重试")
81
-
82
-
83
- class CDPError(XHSError):
84
- """CDP 通信异常。"""
85
-
86
-
87
- class ElementNotFoundError(XHSError):
88
- """页面元素未找到。"""
89
-
90
- def __init__(self, selector: str) -> None:
91
- self.selector = selector
92
- super().__init__(f"未找到元素: {selector}")