@jackwener/opencli 1.7.22 → 1.8.1

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 (346) hide show
  1. package/README.md +35 -194
  2. package/README.zh-CN.md +42 -260
  3. package/cli-manifest.json +8160 -4392
  4. package/clis/12306/me.js +73 -0
  5. package/clis/12306/orders.js +96 -0
  6. package/clis/12306/passengers.js +90 -0
  7. package/clis/12306/price.js +166 -0
  8. package/clis/12306/stations.js +66 -0
  9. package/clis/12306/train.js +91 -0
  10. package/clis/12306/trains.js +119 -0
  11. package/clis/12306/utils.js +272 -0
  12. package/clis/12306/utils.test.js +331 -0
  13. package/clis/36kr/article.js +6 -3
  14. package/clis/36kr/article.test.js +46 -0
  15. package/clis/_atlassian/shared.js +577 -0
  16. package/clis/_atlassian/shared.test.js +170 -0
  17. package/clis/apple-podcasts/commands.test.js +20 -0
  18. package/clis/apple-podcasts/search.js +2 -2
  19. package/clis/barchart/greeks.js +144 -56
  20. package/clis/barchart/greeks.test.js +138 -0
  21. package/clis/bilibili/comment.js +125 -0
  22. package/clis/bilibili/comment.test.js +153 -0
  23. package/clis/bilibili/comments.js +116 -21
  24. package/clis/bilibili/comments.test.js +77 -18
  25. package/clis/bilibili/subtitle.js +76 -31
  26. package/clis/bilibili/subtitle.test.js +156 -9
  27. package/clis/bilibili/summary.js +167 -0
  28. package/clis/bilibili/summary.test.js +210 -0
  29. package/clis/bilibili/utils.js +63 -5
  30. package/clis/bilibili/utils.test.js +45 -1
  31. package/clis/booking/booking.test.js +356 -0
  32. package/clis/booking/search.js +351 -0
  33. package/clis/chatgpt/envelope.test.js +108 -0
  34. package/clis/chatgpt/image.js +2 -2
  35. package/clis/chatgpt/image.test.js +6 -0
  36. package/clis/chatgpt/utils.js +148 -41
  37. package/clis/chatgpt/utils.test.js +92 -2
  38. package/clis/chess/analyze.js +35 -0
  39. package/clis/chess/analyze.test.js +79 -0
  40. package/clis/chess/game.js +114 -0
  41. package/clis/chess/game.test.js +178 -0
  42. package/clis/chess/games.js +67 -0
  43. package/clis/chess/games.test.js +164 -0
  44. package/clis/chess/stats.js +32 -0
  45. package/clis/chess/stats.test.js +79 -0
  46. package/clis/chess/utils.js +170 -0
  47. package/clis/chess/utils.test.js +230 -0
  48. package/clis/confluence/commands.test.js +195 -0
  49. package/clis/confluence/create.js +39 -0
  50. package/clis/confluence/page.js +23 -0
  51. package/clis/confluence/search.js +34 -0
  52. package/clis/confluence/shared.js +173 -0
  53. package/clis/confluence/update.js +38 -0
  54. package/clis/douyin/_shared/browser-fetch.js +44 -20
  55. package/clis/douyin/_shared/browser-fetch.test.js +22 -1
  56. package/clis/douyin/_shared/evaluate-result.js +16 -0
  57. package/clis/douyin/_shared/tos-upload.js +105 -69
  58. package/clis/douyin/_shared/vod-upload.js +212 -0
  59. package/clis/douyin/_shared/vod-upload.test.js +38 -0
  60. package/clis/douyin/delete.js +137 -4
  61. package/clis/douyin/delete.test.js +90 -1
  62. package/clis/douyin/hashtag.js +84 -23
  63. package/clis/douyin/hashtag.test.js +113 -0
  64. package/clis/douyin/publish-upload-id.test.js +170 -0
  65. package/clis/douyin/publish.js +88 -42
  66. package/clis/douyin/user-videos.js +9 -2
  67. package/clis/douyin/user-videos.test.js +43 -0
  68. package/clis/flomo/memos.js +228 -0
  69. package/clis/flomo/memos.test.js +144 -0
  70. package/clis/geogebra/add-circle.js +46 -0
  71. package/clis/geogebra/add-line.js +35 -0
  72. package/clis/geogebra/add-point.js +27 -0
  73. package/clis/geogebra/add-polygon.js +25 -0
  74. package/clis/geogebra/eval.js +35 -0
  75. package/clis/geogebra/geogebra.test.js +175 -0
  76. package/clis/geogebra/hexagon.js +62 -0
  77. package/clis/geogebra/info.js +72 -0
  78. package/clis/geogebra/list.js +35 -0
  79. package/clis/geogebra/triangle.js +60 -0
  80. package/clis/geogebra/utils.js +271 -0
  81. package/clis/gitee/search.js +2 -2
  82. package/clis/gitee/search.test.js +65 -0
  83. package/clis/jike/post.js +27 -17
  84. package/clis/jike/read.test.js +86 -0
  85. package/clis/jike/topic.js +32 -19
  86. package/clis/jike/user.js +33 -20
  87. package/clis/jira/attachments.js +28 -0
  88. package/clis/jira/commands.test.js +287 -0
  89. package/clis/jira/comments.js +28 -0
  90. package/clis/jira/issue.js +28 -0
  91. package/clis/jira/links.js +28 -0
  92. package/clis/jira/search.js +47 -0
  93. package/clis/jira/shared.js +256 -0
  94. package/clis/lesswrong/comments.js +1 -1
  95. package/clis/lesswrong/curated.js +1 -1
  96. package/clis/lesswrong/frontpage.js +1 -1
  97. package/clis/lesswrong/frontpage.test.js +37 -0
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/top-month.js +1 -1
  104. package/clis/lesswrong/top-week.js +1 -1
  105. package/clis/lesswrong/top-year.js +1 -1
  106. package/clis/lesswrong/top.js +1 -1
  107. package/clis/linkedin/connect.js +401 -0
  108. package/clis/linkedin/connect.test.js +213 -0
  109. package/clis/linkedin/inbox.js +234 -0
  110. package/clis/linkedin/inbox.test.js +152 -0
  111. package/clis/linkedin/job-detail.js +167 -0
  112. package/clis/linkedin/job-detail.test.js +38 -0
  113. package/clis/linkedin/jobs-preferences.js +113 -0
  114. package/clis/linkedin/jobs-preferences.test.js +43 -0
  115. package/clis/linkedin/people-search.js +262 -0
  116. package/clis/linkedin/people-search.test.js +216 -0
  117. package/clis/linkedin/post-analytics.js +74 -0
  118. package/clis/linkedin/post-analytics.test.js +40 -0
  119. package/clis/linkedin/posts-core.js +241 -0
  120. package/clis/linkedin/posts.js +22 -0
  121. package/clis/linkedin/posts.test.js +40 -0
  122. package/clis/linkedin/profile-analytics.js +104 -0
  123. package/clis/linkedin/profile-analytics.test.js +67 -0
  124. package/clis/linkedin/profile-experience.js +671 -0
  125. package/clis/linkedin/profile-experience.test.js +152 -0
  126. package/clis/linkedin/profile-projects.js +311 -0
  127. package/clis/linkedin/profile-projects.test.js +111 -0
  128. package/clis/linkedin/profile-read.js +148 -0
  129. package/clis/linkedin/profile-read.test.js +77 -0
  130. package/clis/linkedin/safe-send.js +357 -0
  131. package/clis/linkedin/safe-send.test.js +204 -0
  132. package/clis/linkedin/salesnav-inbox.js +210 -0
  133. package/clis/linkedin/salesnav-inbox.test.js +113 -0
  134. package/clis/linkedin/salesnav-message.js +360 -0
  135. package/clis/linkedin/salesnav-message.test.js +172 -0
  136. package/clis/linkedin/salesnav-search.js +186 -0
  137. package/clis/linkedin/salesnav-search.test.js +76 -0
  138. package/clis/linkedin/salesnav-thread.js +212 -0
  139. package/clis/linkedin/salesnav-thread.test.js +79 -0
  140. package/clis/linkedin/sent-invitations.js +92 -0
  141. package/clis/linkedin/sent-invitations.test.js +62 -0
  142. package/clis/linkedin/services-read.js +213 -0
  143. package/clis/linkedin/services-read.test.js +105 -0
  144. package/clis/linkedin/shared.js +124 -0
  145. package/clis/linkedin/thread-snapshot.js +214 -0
  146. package/clis/linkedin/thread-snapshot.test.js +89 -0
  147. package/clis/linkedin/timeline.js +14 -7
  148. package/clis/linkedin-learning/course.js +138 -0
  149. package/clis/linkedin-learning/course.test.js +114 -0
  150. package/clis/linkedin-learning/search.js +155 -0
  151. package/clis/linkedin-learning/search.test.js +144 -0
  152. package/clis/linkedin-learning/trending.js +133 -0
  153. package/clis/linkedin-learning/trending.test.js +123 -0
  154. package/clis/notebooklm/add-source.js +269 -0
  155. package/clis/notebooklm/add-source.test.js +97 -0
  156. package/clis/notebooklm/create.js +76 -0
  157. package/clis/notebooklm/create.test.js +58 -0
  158. package/clis/notebooklm/generate-audio.js +91 -0
  159. package/clis/notebooklm/generate-audio.test.js +63 -0
  160. package/clis/notebooklm/generate-slides.js +106 -0
  161. package/clis/notebooklm/generate-slides.test.js +75 -0
  162. package/clis/notebooklm/open.test.js +10 -10
  163. package/clis/notebooklm/rpc.js +20 -6
  164. package/clis/notebooklm/rpc.test.js +27 -1
  165. package/clis/notebooklm/utils.js +100 -24
  166. package/clis/notebooklm/utils.test.js +60 -1
  167. package/clis/notebooklm/write-note.js +103 -0
  168. package/clis/notebooklm/write-note.test.js +70 -0
  169. package/clis/pixiv/detail.js +41 -34
  170. package/clis/pixiv/detail.test.js +93 -0
  171. package/clis/pixiv/user.js +36 -31
  172. package/clis/pixiv/user.test.js +100 -0
  173. package/clis/pixiv/utils.js +56 -7
  174. package/clis/powerchina/search.js +3 -3
  175. package/clis/powerchina/search.test.js +27 -1
  176. package/clis/reddit/extract-media.test.js +149 -0
  177. package/clis/reddit/frontpage.js +47 -9
  178. package/clis/reddit/frontpage.test.js +34 -0
  179. package/clis/reddit/home.js +31 -1
  180. package/clis/reddit/home.test.js +46 -3
  181. package/clis/reddit/hot.js +32 -1
  182. package/clis/reddit/hot.test.js +15 -1
  183. package/clis/reddit/popular.js +39 -1
  184. package/clis/reddit/popular.test.js +26 -0
  185. package/clis/reddit/saved.js +1 -1
  186. package/clis/reddit/search.js +38 -1
  187. package/clis/reddit/search.test.js +26 -0
  188. package/clis/reddit/subreddit.js +52 -7
  189. package/clis/reddit/subreddit.test.js +31 -0
  190. package/clis/reddit/subscribed.js +165 -0
  191. package/clis/reddit/subscribed.test.js +168 -0
  192. package/clis/reddit/upvoted.js +1 -1
  193. package/clis/suno/commands.test.js +188 -0
  194. package/clis/suno/download.js +140 -0
  195. package/clis/suno/download.test.js +151 -0
  196. package/clis/suno/generate.js +231 -0
  197. package/clis/suno/generate.test.js +252 -0
  198. package/clis/suno/list.js +79 -0
  199. package/clis/suno/status.js +63 -0
  200. package/clis/suno/utils.js +549 -0
  201. package/clis/suno/utils.test.js +329 -0
  202. package/clis/twitter/device-follow.js +193 -0
  203. package/clis/twitter/device-follow.test.js +287 -0
  204. package/clis/twitter/download.js +443 -73
  205. package/clis/twitter/download.test.js +457 -0
  206. package/clis/twitter/followers.js +6 -2
  207. package/clis/twitter/followers.test.js +19 -1
  208. package/clis/twitter/following.js +14 -5
  209. package/clis/twitter/following.test.js +29 -0
  210. package/clis/twitter/likes.js +12 -4
  211. package/clis/twitter/likes.test.js +26 -1
  212. package/clis/twitter/list-add.js +1 -1
  213. package/clis/twitter/list-create.js +155 -0
  214. package/clis/twitter/list-create.test.js +169 -0
  215. package/clis/twitter/list-remove.js +13 -6
  216. package/clis/twitter/list-remove.test.js +74 -0
  217. package/clis/twitter/list-tweets.js +6 -2
  218. package/clis/twitter/list-tweets.test.js +41 -1
  219. package/clis/twitter/lists.js +31 -4
  220. package/clis/twitter/lists.test.js +152 -16
  221. package/clis/twitter/notifications.js +4 -4
  222. package/clis/twitter/post.js +62 -4
  223. package/clis/twitter/post.test.js +35 -3
  224. package/clis/twitter/profile.js +81 -28
  225. package/clis/twitter/profile.test.js +113 -2
  226. package/clis/twitter/quote.js +9 -4
  227. package/clis/twitter/reply.js +13 -10
  228. package/clis/twitter/reply.test.js +41 -0
  229. package/clis/twitter/search.js +7 -3
  230. package/clis/twitter/search.test.js +41 -0
  231. package/clis/twitter/shared.js +155 -0
  232. package/clis/twitter/shared.test.js +465 -1
  233. package/clis/twitter/thread.js +10 -2
  234. package/clis/twitter/thread.test.js +58 -0
  235. package/clis/twitter/timeline.js +6 -2
  236. package/clis/twitter/timeline.test.js +2 -0
  237. package/clis/twitter/tweets.js +3 -2
  238. package/clis/twitter/tweets.test.js +1 -1
  239. package/clis/twitter/utils.js +53 -16
  240. package/clis/upwork/detail.js +132 -0
  241. package/clis/upwork/feed.js +109 -0
  242. package/clis/upwork/search.js +115 -0
  243. package/clis/upwork/upwork.test.js +566 -0
  244. package/clis/upwork/utils.js +323 -0
  245. package/clis/weibo/delete.js +172 -0
  246. package/clis/weibo/delete.test.js +94 -0
  247. package/clis/weibo/publish.js +37 -14
  248. package/clis/weibo/publish.test.js +14 -5
  249. package/clis/weibo/user-posts.js +234 -0
  250. package/clis/weibo/user-posts.test.js +92 -0
  251. package/clis/weread/book-search.js +438 -0
  252. package/clis/weread/book-search.test.js +242 -0
  253. package/clis/weread/search-regression.test.js +98 -11
  254. package/clis/weread/search.js +32 -9
  255. package/clis/weread-official/book.js +135 -0
  256. package/clis/weread-official/commands.test.js +385 -0
  257. package/clis/weread-official/discover.js +107 -0
  258. package/clis/weread-official/list-apis.js +95 -0
  259. package/clis/weread-official/notes.js +171 -0
  260. package/clis/weread-official/readdata.js +158 -0
  261. package/clis/weread-official/review.js +93 -0
  262. package/clis/weread-official/search.js +106 -0
  263. package/clis/weread-official/shelf.js +97 -0
  264. package/clis/weread-official/utils.js +293 -0
  265. package/clis/weread-official/utils.test.js +242 -0
  266. package/clis/wikipedia/trending.js +7 -3
  267. package/clis/wikipedia/trending.test.js +57 -0
  268. package/clis/xianyu/chat.js +24 -109
  269. package/clis/xianyu/chat.test.js +5 -0
  270. package/clis/xianyu/im.js +322 -0
  271. package/clis/xianyu/im.test.js +253 -0
  272. package/clis/xianyu/inbox.js +96 -0
  273. package/clis/xianyu/messages.js +91 -0
  274. package/clis/xianyu/reply.js +82 -0
  275. package/clis/xiaohongshu/creator-note-detail.js +166 -28
  276. package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
  277. package/clis/xiaohongshu/creator-notes-summary.js +2 -1
  278. package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
  279. package/clis/xiaohongshu/creator-notes.js +252 -2
  280. package/clis/xiaohongshu/creator-notes.test.js +90 -1
  281. package/clis/xiaohongshu/creator-stats.js +2 -1
  282. package/clis/xiaohongshu/creator-stats.test.js +24 -0
  283. package/clis/xiaohongshu/delete-note.js +260 -0
  284. package/clis/xiaohongshu/delete-note.test.js +172 -0
  285. package/clis/xiaohongshu/download.js +97 -39
  286. package/clis/xiaohongshu/download.test.js +201 -0
  287. package/clis/xiaohongshu/publish.js +48 -8
  288. package/clis/xiaohongshu/publish.test.js +65 -10
  289. package/clis/xiaohongshu/user-helpers.test.js +41 -0
  290. package/clis/xiaohongshu/user.js +27 -4
  291. package/clis/xiaoyuzhou/download.js +1 -1
  292. package/clis/xiaoyuzhou/transcript.js +1 -1
  293. package/clis/youdao/note.js +258 -0
  294. package/clis/youdao/note.test.js +99 -0
  295. package/clis/youtube/transcript.js +397 -24
  296. package/clis/youtube/transcript.test.js +196 -6
  297. package/clis/zhihu/answer-comments.js +280 -0
  298. package/clis/zhihu/answer-comments.test.js +287 -0
  299. package/clis/zhihu/answer-detail.js +2 -19
  300. package/clis/zhihu/answer-detail.test.js +8 -0
  301. package/clis/zhihu/collection.js +17 -16
  302. package/clis/zhihu/collection.test.js +50 -3
  303. package/clis/zhihu/download.js +1 -1
  304. package/clis/zhihu/question.js +42 -17
  305. package/clis/zhihu/question.test.js +113 -11
  306. package/clis/zhihu/search.js +195 -43
  307. package/clis/zhihu/search.test.js +198 -0
  308. package/clis/zhihu/text.js +29 -0
  309. package/clis/zhihu/text.test.js +24 -0
  310. package/dist/src/browser/errors.js +4 -2
  311. package/dist/src/browser/errors.test.js +6 -0
  312. package/dist/src/browser/network-cache.js +13 -1
  313. package/dist/src/browser/network-cache.test.js +17 -0
  314. package/dist/src/browser/page.js +30 -4
  315. package/dist/src/browser/page.test.js +42 -0
  316. package/dist/src/browser/utils.d.ts +1 -1
  317. package/dist/src/cli-argv-preprocess.d.ts +26 -0
  318. package/dist/src/cli-argv-preprocess.js +138 -0
  319. package/dist/src/cli-argv-preprocess.test.js +79 -0
  320. package/dist/src/convention-audit.js +15 -8
  321. package/dist/src/convention-audit.test.js +21 -0
  322. package/dist/src/download/index.js +13 -1
  323. package/dist/src/download/index.test.js +23 -1
  324. package/dist/src/download/media-download.js +15 -2
  325. package/dist/src/download/media-download.test.d.ts +1 -0
  326. package/dist/src/download/media-download.test.js +112 -0
  327. package/dist/src/download/progress.js +2 -2
  328. package/dist/src/download/progress.test.js +12 -1
  329. package/dist/src/electron-apps.js +1 -1
  330. package/dist/src/electron-apps.test.js +7 -2
  331. package/dist/src/errors.d.ts +17 -0
  332. package/dist/src/errors.js +22 -0
  333. package/dist/src/external-clis.yaml +8 -0
  334. package/dist/src/main.js +14 -2
  335. package/dist/src/output.js +11 -1
  336. package/dist/src/output.test.js +6 -0
  337. package/dist/src/registry.js +1 -0
  338. package/dist/src/registry.test.js +11 -0
  339. package/dist/src/utils.d.ts +43 -0
  340. package/dist/src/utils.js +97 -0
  341. package/dist/src/utils.test.d.ts +1 -0
  342. package/dist/src/utils.test.js +155 -0
  343. package/package.json +8 -2
  344. package/scripts/silent-column-drop-baseline.json +0 -52
  345. package/scripts/typed-error-lint-baseline.json +28 -380
  346. package/clis/slock/_utils.js +0 -12
@@ -1,94 +1,8 @@
1
- import { AuthRequiredError, selectorError } from '@jackwener/opencli/errors';
1
+ import { AuthRequiredError, CommandExecutionError, selectorError } from '@jackwener/opencli/errors';
2
2
  import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { buildChatUrl, buildExtractChatStateEvaluate, buildSendMessageEvaluate, requireEvaluateObject } from './im.js';
3
4
  import { normalizeNumericId } from './utils.js';
4
- function buildChatUrl(itemId, peerUserId) {
5
- return `https://www.goofish.com/im?itemId=${encodeURIComponent(itemId)}&peerUserId=${encodeURIComponent(peerUserId)}`;
6
- }
7
- function buildExtractChatStateEvaluate() {
8
- return `
9
- (() => {
10
- const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
11
- const bodyText = document.body?.innerText || '';
12
- const requiresAuth = /请先登录|登录后/.test(bodyText);
13
5
 
14
- const textarea = document.querySelector('textarea');
15
- const normalizeBtn = (s) => (s || '').replace(/\\s+/g, '').trim();
16
- const sendButton = Array.from(document.querySelectorAll('button'))
17
- .find((btn) => normalizeBtn(btn.textContent || '') === '发送');
18
- const topbar = document.querySelector('[class*="message-topbar"]');
19
- const itemCard = Array.from(document.querySelectorAll('a[href*="/item?id="]'))
20
- .find((el) => el.closest('main'));
21
- const itemTitleNode =
22
- document.querySelector('[class*="container"] [class*="title"]')
23
- || document.querySelector('[class*="item-main-info"] [class*="desc"]')
24
- || document.querySelector('[class*="headSkuInfo"]')
25
- || itemCard?.querySelector('[class*="title"]')
26
- || itemCard?.previousElementSibling?.querySelector?.('[class*="title"]');
27
-
28
- const messageRoot = document.querySelector('#message-list-scrollable');
29
- const visibleMessages = Array.from(
30
- (messageRoot || document).querySelectorAll('[class*="message"], [class*="msg"], [class*="bubble"]')
31
- ).map((el) => clean(el.textContent || ''))
32
- .filter(Boolean)
33
- .filter((text) => !['发送', '闲鱼号', '立即购买'].includes(text))
34
- .filter((text) => !/^消息\\d*\\+?$/.test(text))
35
- .slice(-20);
36
-
37
- return {
38
- requiresAuth,
39
- title: clean(document.title || ''),
40
- peer_name: clean(topbar?.querySelector('[class*="text1"]')?.textContent || ''),
41
- peer_masked_id: clean(topbar?.querySelector('[class*="text2"]')?.textContent || '').replace(/^\\(|\\)$/g, ''),
42
- item_title: clean(itemTitleNode?.textContent || ''),
43
- item_url: itemCard?.href || '',
44
- price: clean(itemCard?.querySelector('[class*="money"]')?.textContent || ''),
45
- location: clean(itemCard?.querySelector('[class*="delivery"] + [class*="delivery"], [class*="delivery"]:last-child')?.textContent || ''),
46
- can_input: Boolean(textarea && !textarea.disabled),
47
- can_send: Boolean(sendButton),
48
- visible_messages: visibleMessages,
49
- };
50
- })()
51
- `;
52
- }
53
- function buildSendMessageEvaluate(text) {
54
- return `
55
- (async () => {
56
- const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
57
- const textarea = document.querySelector('textarea');
58
- if (!textarea || textarea.disabled) {
59
- return { ok: false, reason: 'input-not-found' };
60
- }
61
-
62
- const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
63
- if (!setter) {
64
- return { ok: false, reason: 'textarea-setter-not-found' };
65
- }
66
-
67
- // Click textarea first to activate chat and trigger send button to appear
68
- textarea.click();
69
- textarea.focus();
70
- setter.call(textarea, ${JSON.stringify(text)});
71
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
72
- textarea.dispatchEvent(new Event('change', { bubbles: true }));
73
-
74
- // Poll up to 3s for send button (may appear after textarea interaction)
75
- const normalizeBtn = (s) => (s || '').replace(/\\s+/g, '').trim();
76
- let sendButton = null;
77
- for (let i = 0; i < 30; i++) {
78
- sendButton = Array.from(document.querySelectorAll('button'))
79
- .find((btn) => normalizeBtn(btn.textContent || '') === '发送');
80
- if (sendButton) break;
81
- await new Promise(r => setTimeout(r, 100));
82
- }
83
- if (!sendButton) {
84
- return { ok: false, reason: 'send-button-not-found' };
85
- }
86
-
87
- sendButton.click();
88
- return { ok: true };
89
- })()
90
- `;
91
- }
92
6
  cli({
93
7
  site: 'xianyu',
94
8
  name: 'chat',
@@ -111,7 +25,7 @@ cli({
111
25
  const text = String(kwargs.text || '').trim();
112
26
  await page.goto(url);
113
27
  await page.wait(2);
114
- const state = await page.evaluate(buildExtractChatStateEvaluate());
28
+ const state = requireEvaluateObject(await page.evaluate(buildExtractChatStateEvaluate()), 'chat');
115
29
  if (state?.requiresAuth) {
116
30
  throw new AuthRequiredError('www.goofish.com', 'Xianyu chat requires a logged-in browser session');
117
31
  }
@@ -120,37 +34,38 @@ cli({
120
34
  }
121
35
  if (!text) {
122
36
  return [{
123
- status: 'ready',
124
- peer_name: state.peer_name || '',
125
- item_title: state.item_title || '',
126
- price: state.price || '',
127
- location: state.location || '',
128
- message: (state.visible_messages || []).slice(-1)[0] || '',
129
- peer_user_id: userId,
130
- item_id: itemId,
131
- url,
132
- item_url: state.item_url || '',
133
- }];
134
- }
135
- const sent = await page.evaluate(buildSendMessageEvaluate(text));
136
- if (!sent?.ok) {
137
- throw selectorError('闲鱼发送按钮', `消息发送失败:${sent?.reason || 'unknown-reason'}`);
138
- }
139
- await page.wait(1);
140
- return [{
141
- status: 'sent',
37
+ status: 'ready',
142
38
  peer_name: state.peer_name || '',
143
39
  item_title: state.item_title || '',
144
40
  price: state.price || '',
145
41
  location: state.location || '',
146
- message: text,
42
+ message: (state.visible_messages || []).slice(-1)[0] || '',
147
43
  peer_user_id: userId,
148
44
  item_id: itemId,
149
45
  url,
150
46
  item_url: state.item_url || '',
151
47
  }];
48
+ }
49
+ const sent = requireEvaluateObject(await page.evaluate(buildSendMessageEvaluate(text)), 'chat send');
50
+ if (!sent?.ok) {
51
+ throw new CommandExecutionError(`Xianyu chat did not observe the sent message: ${sent?.reason || 'unknown-reason'}`);
52
+ }
53
+ await page.wait(1);
54
+ return [{
55
+ status: 'sent',
56
+ peer_name: state.peer_name || '',
57
+ item_title: state.item_title || '',
58
+ price: state.price || '',
59
+ location: state.location || '',
60
+ message: text,
61
+ peer_user_id: userId,
62
+ item_id: itemId,
63
+ url,
64
+ item_url: state.item_url || '',
65
+ }];
152
66
  },
153
67
  });
68
+
154
69
  export const __test__ = {
155
70
  normalizeNumericId,
156
71
  buildChatUrl,
@@ -41,6 +41,7 @@ describe('xianyu chat helpers', () => {
41
41
  const result = await runBrowserScript(`
42
42
  <main>
43
43
  <textarea></textarea>
44
+ <div id="message-list-scrollable"></div>
44
45
  </main>
45
46
  `, __test__.buildSendMessageEvaluate('还在吗?'), {
46
47
  beforeEval(window) {
@@ -53,6 +54,10 @@ describe('xianyu chat helpers', () => {
53
54
  button.textContent = '发 送';
54
55
  button.addEventListener('click', () => {
55
56
  sendClicked = true;
57
+ const row = window.document.createElement('div');
58
+ row.className = 'message-row';
59
+ row.innerHTML = '<div class="message-text">还在吗?</div>';
60
+ window.document.querySelector('#message-list-scrollable').append(row);
56
61
  });
57
62
  window.document.body.append(button);
58
63
  });
@@ -0,0 +1,322 @@
1
+ import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
2
+
3
+ export const DEFAULT_INBOX_LIMIT = 20;
4
+ export const MAX_INBOX_LIMIT = 100;
5
+ export const DEFAULT_MESSAGE_LIMIT = 50;
6
+ export const MAX_MESSAGE_LIMIT = 200;
7
+
8
+ export function normalizeLimit(value, defaultValue = DEFAULT_INBOX_LIMIT, maxValue = MAX_INBOX_LIMIT, label = 'limit') {
9
+ const raw = String(value ?? '').trim();
10
+ if (!raw) return defaultValue;
11
+ if (!/^\d+$/.test(raw)) {
12
+ throw new ArgumentError(`xianyu ${label} must be an integer between 1 and ${maxValue}`);
13
+ }
14
+ const n = Number(raw);
15
+ if (!Number.isSafeInteger(n) || n < 1 || n > maxValue) {
16
+ throw new ArgumentError(`xianyu ${label} must be an integer between 1 and ${maxValue}`);
17
+ }
18
+ return n;
19
+ }
20
+
21
+ export function normalizeRank(value) {
22
+ const raw = String(value ?? '').trim();
23
+ if (!raw) return 0;
24
+ if (!/^\d+$/.test(raw)) {
25
+ throw new ArgumentError('xianyu rank must be a positive integer from xianyu inbox');
26
+ }
27
+ const n = Number(raw);
28
+ if (!Number.isSafeInteger(n) || n < 1) {
29
+ throw new ArgumentError('xianyu rank must be a positive integer from xianyu inbox');
30
+ }
31
+ return n;
32
+ }
33
+
34
+ export function requireText(value, label) {
35
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
36
+ if (!text) {
37
+ throw new ArgumentError(`${label} cannot be empty`);
38
+ }
39
+ return text;
40
+ }
41
+
42
+ export function requireEvaluateObject(payload, label) {
43
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
44
+ throw new CommandExecutionError(`Xianyu ${label} returned malformed browser payload`);
45
+ }
46
+ return payload;
47
+ }
48
+
49
+ export function requireClickResult(payload, label) {
50
+ const result = requireEvaluateObject(payload, label);
51
+ if (result.ok !== true) {
52
+ throw new CommandExecutionError(`Xianyu ${label} failed: ${result.reason || 'unknown-reason'}`);
53
+ }
54
+ return result;
55
+ }
56
+
57
+ export function buildChatUrl(itemId, peerUserId) {
58
+ return `https://www.goofish.com/im?itemId=${encodeURIComponent(itemId)}&peerUserId=${encodeURIComponent(peerUserId)}`;
59
+ }
60
+
61
+ export function buildInboxUrl() {
62
+ return 'https://www.goofish.com/im';
63
+ }
64
+
65
+ export function buildClickInboxConversationEvaluate(index) {
66
+ return `
67
+ (() => {
68
+ const rows = Array.from(document.querySelectorAll('#conv-list-scrollable [class*="conversation-item"], a[href*="/im"], a[href*="itemId="][href*="peerUserId="]'));
69
+ const row = rows[${index}];
70
+ if (!row) return { ok: false, reason: 'row-not-found' };
71
+ row.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: window }));
72
+ row.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
73
+ row.click();
74
+ row.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
75
+ return { ok: true };
76
+ })()
77
+ `;
78
+ }
79
+
80
+ export function buildReadCurrentConversationUrlEvaluate() {
81
+ return `
82
+ (() => {
83
+ const url = location.href || '';
84
+ const params = new URL(url).searchParams;
85
+ return {
86
+ url,
87
+ item_id: params.get('itemId') || '',
88
+ peer_user_id: params.get('peerUserId') || '',
89
+ };
90
+ })()
91
+ `;
92
+ }
93
+
94
+ export function buildExtractInboxEvaluate(limit) {
95
+ return `
96
+ (() => {
97
+ const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
98
+ const bodyText = document.body?.innerText || '';
99
+ const requiresAuth = /请先登录|登录后|璇峰厛鐧诲綍|鐧诲綍鍚?/.test(bodyText);
100
+ const blocked = /验证码|安全验证|异常访问|楠岃瘉鐮亅瀹夊叏楠岃瘉|寮傚父璁块棶/.test(bodyText);
101
+
102
+ const absoluteUrl = (url) => {
103
+ try {
104
+ return new URL(url, location.href).href;
105
+ } catch {
106
+ return '';
107
+ }
108
+ };
109
+
110
+ const readId = (url, key) => {
111
+ try {
112
+ return new URL(url, location.href).searchParams.get(key) || '';
113
+ } catch {
114
+ const match = String(url || '').match(new RegExp('[?&]' + key + '=(\\\\d+)'));
115
+ return match ? match[1] : '';
116
+ }
117
+ };
118
+
119
+ const pick = (root, selectors) => {
120
+ for (const selector of selectors) {
121
+ const node = root.querySelector(selector);
122
+ const text = clean(node?.getAttribute?.('title') || node?.textContent || '');
123
+ if (text) return text;
124
+ }
125
+ return '';
126
+ };
127
+
128
+ const leafTexts = (root) => Array.from(root.querySelectorAll('div, span'))
129
+ .filter((node) => !Array.from(node.children || []).some((child) => ['DIV', 'SPAN'].includes(child.tagName)))
130
+ .map((node) => clean(node.textContent || ''))
131
+ .filter(Boolean);
132
+
133
+ const looksLikeTime = (text) => /^(刚刚|\\d+分钟前|\\d+小时前|\\d+天前|昨天|前天|\\d{1,2}:\\d{2}|\\d{4}-\\d{1,2}-\\d{1,2})$/.test(text);
134
+
135
+ const links = Array.from(document.querySelectorAll('a[href*="/im"], a[href*="itemId="][href*="peerUserId="]'));
136
+ const seen = new Set();
137
+ const items = [];
138
+ for (const link of links) {
139
+ const href = link.href || link.getAttribute('href') || '';
140
+ const itemId = readId(href, 'itemId');
141
+ const peerUserId = readId(href, 'peerUserId');
142
+ if (!itemId || !peerUserId) continue;
143
+ const key = itemId + ':' + peerUserId;
144
+ if (seen.has(key)) continue;
145
+ seen.add(key);
146
+
147
+ const root = link.closest('[class*="conversation"], [class*="session"], [class*="contact"], [class*="chat"], li, [role="listitem"]') || link;
148
+ const peerName = pick(root, ['[class*="name"]', '[class*="nick"]', '[class*="user"]', '[class*="text1"]']);
149
+ const itemTitle = pick(root, ['[class*="title"]', '[class*="item"] [class*="desc"]', '[class*="desc"]']);
150
+ const price = pick(root, ['[class*="money"]', '[class*="price"]']);
151
+ const lastMessage = pick(root, ['[class*="message"]', '[class*="msg"]', '[class*="content"]', '[class*="summary"]']);
152
+ const rootText = clean(root.textContent || '');
153
+ const unreadText = pick(root, ['[class*="badge"]', '[class*="unread"]', '[class*="red"]']);
154
+ const unreadCount = Number.parseInt(unreadText.replace(/\\D/g, ''), 10) || (unreadText ? 1 : 0);
155
+
156
+ items.push({
157
+ row_index: items.length,
158
+ peer_name: peerName,
159
+ peer_user_id: peerUserId,
160
+ item_id: itemId,
161
+ item_title: itemTitle,
162
+ price,
163
+ last_message: lastMessage || rootText,
164
+ unread: unreadCount > 0 || /unread|未读/.test(String(root.className || '') + ' ' + rootText),
165
+ unread_count: unreadCount,
166
+ url: absoluteUrl(href),
167
+ });
168
+ if (items.length >= ${limit}) break;
169
+ }
170
+
171
+ if (!items.length) {
172
+ const rows = Array.from(document.querySelectorAll('#conv-list-scrollable [class*="conversation-item"]')).slice(0, ${limit});
173
+ for (const row of rows) {
174
+ const texts = leafTexts(row)
175
+ .filter((text) => !/^\\d+$/.test(text) || row.querySelector('sup[title="' + text.replace(/"/g, '\\"') + '"]') == null);
176
+ const time = [...texts].reverse().find(looksLikeTime) || '';
177
+ const unreadTitle = clean(row.querySelector('sup')?.getAttribute('title') || row.querySelector('[class*="badge"], [class*="unread"]')?.textContent || '');
178
+ const unreadCount = Number.parseInt(unreadTitle.replace(/\\D/g, ''), 10) || (unreadTitle ? 1 : 0);
179
+ const peerName = texts.find((text) => text !== time && text !== unreadTitle && !/^\\[.*\\]$/.test(text)) || '';
180
+ const lastMessage = texts.find((text) => text !== peerName && text !== time && text !== unreadTitle) || '';
181
+ if (!peerName && !lastMessage) continue;
182
+ items.push({
183
+ row_index: items.length,
184
+ peer_name: peerName,
185
+ peer_user_id: '',
186
+ item_id: '',
187
+ item_title: '',
188
+ price: '',
189
+ last_message: lastMessage,
190
+ unread: unreadCount > 0,
191
+ unread_count: unreadCount,
192
+ url: '',
193
+ time,
194
+ });
195
+ if (items.length >= ${limit}) break;
196
+ }
197
+ }
198
+
199
+ return { requiresAuth, blocked, empty: !items.length, items };
200
+ })()
201
+ `;
202
+ }
203
+
204
+ export function buildExtractChatStateEvaluate(limit = DEFAULT_MESSAGE_LIMIT) {
205
+ return `
206
+ (() => {
207
+ const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
208
+ const bodyText = document.body?.innerText || '';
209
+ const requiresAuth = /请先登录|登录后|璇峰厛鐧诲綍|鐧诲綍鍚?/.test(bodyText);
210
+
211
+ const textarea = document.querySelector('textarea');
212
+ const normalizeBtn = (s) => String(s || '').replace(/\\s+/g, '').trim();
213
+ const sendButton = Array.from(document.querySelectorAll('button'))
214
+ .find((btn) => /^(发送|鍙戦€?)$/.test(normalizeBtn(btn.textContent || '')));
215
+ const topbar = document.querySelector('[class*="message-topbar"]');
216
+ const itemCard = Array.from(document.querySelectorAll('a[href*="/item?id="]'))
217
+ .find((el) => el.closest('main')) || document.querySelector('a[href*="/item?id="]');
218
+ const itemTitleNode =
219
+ document.querySelector('[class*="container"] [class*="title"]')
220
+ || document.querySelector('[class*="item-main-info"] [class*="desc"]')
221
+ || document.querySelector('[class*="headSkuInfo"]')
222
+ || itemCard?.querySelector('[class*="title"]')
223
+ || itemCard?.previousElementSibling?.querySelector?.('[class*="title"]');
224
+
225
+ const messageRoot = document.querySelector('#message-list-scrollable') || document.querySelector('[class*="message-list"]');
226
+ let visibleMessages = Array.from((messageRoot || document).querySelectorAll('[class*="message-row"]'))
227
+ .map((row) => {
228
+ const textNode = row.querySelector('[class*="message-text"]');
229
+ return clean(textNode?.textContent || '');
230
+ })
231
+ .filter(Boolean);
232
+
233
+ if (!visibleMessages.length) {
234
+ visibleMessages = Array.from(
235
+ (messageRoot || document).querySelectorAll('[class*="message"], [class*="msg"], [class*="bubble"]')
236
+ ).map((el) => clean(el.textContent || ''));
237
+ }
238
+
239
+ visibleMessages = visibleMessages
240
+ .filter(Boolean)
241
+ .filter((text) => !['发送', '闲鱼号', '立即购买', '鍙戦€?', '闂查奔鍙?', '绔嬪嵆璐拱'].includes(text))
242
+ .filter((text) => !/^消息\\d*\\+?$/.test(text) && !/^娑堟伅\\d*\\+?$/.test(text))
243
+ .slice(-${limit});
244
+
245
+ return {
246
+ requiresAuth,
247
+ title: clean(document.title || ''),
248
+ peer_name: clean(topbar?.querySelector('[class*="text1"]')?.textContent || ''),
249
+ peer_masked_id: clean(topbar?.querySelector('[class*="text2"]')?.textContent || '').replace(/^\\(|\\)$/g, ''),
250
+ item_title: clean(itemTitleNode?.textContent || ''),
251
+ item_url: itemCard?.href || '',
252
+ price: clean(itemCard?.querySelector('[class*="money"], [class*="price"]')?.textContent || ''),
253
+ location: clean(itemCard?.querySelector('[class*="delivery"] + [class*="delivery"], [class*="delivery"]:last-child')?.textContent || ''),
254
+ can_input: Boolean(textarea && !textarea.disabled),
255
+ can_send: Boolean(sendButton),
256
+ visible_messages: visibleMessages,
257
+ messages: visibleMessages.map((text, index) => ({ index: index + 1, text })),
258
+ };
259
+ })()
260
+ `;
261
+ }
262
+
263
+ export function buildSendMessageEvaluate(text) {
264
+ return `
265
+ (async () => {
266
+ const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
267
+ const readMessages = () => {
268
+ const messageRoot = document.querySelector('#message-list-scrollable') || document.querySelector('[class*="message-list"]');
269
+ let messages = Array.from((messageRoot || document).querySelectorAll('[class*="message-row"]'))
270
+ .map((row) => clean(row.querySelector('[class*="message-text"]')?.textContent || ''))
271
+ .filter(Boolean);
272
+ if (!messages.length) {
273
+ messages = Array.from(
274
+ (messageRoot || document).querySelectorAll('[class*="message"], [class*="msg"], [class*="bubble"]')
275
+ ).map((el) => clean(el.textContent || '')).filter(Boolean);
276
+ }
277
+ return messages
278
+ .filter((item) => !['发送', '闲鱼号', '立即购买', '鍙戦€?', '闂查奔鍙?', '绔嬪嵆璐拱'].includes(item))
279
+ .filter((item) => !/^消息\\d*\\+?$/.test(item) && !/^娑堟伅\\d*\\+?$/.test(item));
280
+ };
281
+
282
+ const textarea = document.querySelector('textarea');
283
+ if (!textarea || textarea.disabled) {
284
+ return { ok: false, reason: 'input-not-found' };
285
+ }
286
+
287
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
288
+ if (!setter) {
289
+ return { ok: false, reason: 'textarea-setter-not-found' };
290
+ }
291
+
292
+ const beforeMessages = readMessages();
293
+ textarea.click();
294
+ textarea.focus();
295
+ setter.call(textarea, ${JSON.stringify(text)});
296
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
297
+ textarea.dispatchEvent(new Event('change', { bubbles: true }));
298
+
299
+ const normalizeBtn = (s) => String(s || '').replace(/\\s+/g, '').trim();
300
+ let sendButton = null;
301
+ for (let i = 0; i < 30; i++) {
302
+ sendButton = Array.from(document.querySelectorAll('button'))
303
+ .find((btn) => /^(发送|鍙戦€?)$/.test(normalizeBtn(btn.textContent || '')));
304
+ if (sendButton) break;
305
+ await new Promise(r => setTimeout(r, 100));
306
+ }
307
+ if (!sendButton) {
308
+ return { ok: false, reason: 'send-button-not-found' };
309
+ }
310
+
311
+ sendButton.click();
312
+ for (let i = 0; i < 30; i++) {
313
+ await new Promise(r => setTimeout(r, 100));
314
+ const afterMessages = readMessages();
315
+ if (afterMessages.length > beforeMessages.length && afterMessages.at(-1) === ${JSON.stringify(text)}) {
316
+ return { ok: true };
317
+ }
318
+ }
319
+ return { ok: false, reason: 'send-postcondition-timeout' };
320
+ })()
321
+ `;
322
+ }