@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
@@ -0,0 +1,253 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ buildChatUrl,
5
+ buildExtractChatStateEvaluate,
6
+ buildExtractInboxEvaluate,
7
+ buildSendMessageEvaluate,
8
+ normalizeLimit,
9
+ normalizeRank,
10
+ } from './im.js';
11
+ import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
12
+ import './messages.js';
13
+ import './reply.js';
14
+ import { getRegistry } from '@jackwener/opencli/registry';
15
+
16
+ async function runBrowserScript(html, script, { url = 'https://www.goofish.com/im', beforeEval } = {}) {
17
+ const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
18
+ beforeEval?.(dom.window);
19
+ return dom.window.eval(script);
20
+ }
21
+
22
+ describe('xianyu im shared helpers', () => {
23
+ it('strictly validates limits and ranks before browser side effects', () => {
24
+ expect(normalizeLimit(undefined, 20, 100)).toBe(20);
25
+ expect(normalizeLimit('', 20, 100)).toBe(20);
26
+ expect(normalizeLimit('100', 20, 100)).toBe(100);
27
+ expect(() => normalizeLimit(0, 20, 100)).toThrow(ArgumentError);
28
+ expect(() => normalizeLimit(3.8, 20, 100)).toThrow(ArgumentError);
29
+ expect(() => normalizeLimit(999, 20, 100)).toThrow(ArgumentError);
30
+ expect(normalizeRank(undefined)).toBe(0);
31
+ expect(normalizeRank(1)).toBe(1);
32
+ expect(() => normalizeRank(0)).toThrow(ArgumentError);
33
+ expect(() => normalizeRank('1.5')).toThrow(ArgumentError);
34
+ });
35
+
36
+ it('extracts recent inbox conversations from visible IM rows', async () => {
37
+ const result = await runBrowserScript(`
38
+ <main>
39
+ <a href="/im?itemId=10001&peerUserId=90001" class="conversation unread">
40
+ <div class="name">张三</div>
41
+ <div class="title">MacBook Pro 14</div>
42
+ <div class="money">¥5999</div>
43
+ <div class="message">还在吗?</div>
44
+ <span class="badge">2</span>
45
+ </a>
46
+ <a href="https://www.goofish.com/im?itemId=10002&peerUserId=90002" class="conversation">
47
+ <div class="name">李四</div>
48
+ <div class="title">iPhone 15</div>
49
+ <div class="message">明天能发货</div>
50
+ </a>
51
+ </main>
52
+ `, buildExtractInboxEvaluate(10));
53
+
54
+ expect(result.requiresAuth).toBe(false);
55
+ expect(result.items).toEqual([
56
+ {
57
+ peer_name: '张三',
58
+ peer_user_id: '90001',
59
+ item_id: '10001',
60
+ item_title: 'MacBook Pro 14',
61
+ price: '¥5999',
62
+ last_message: '还在吗?',
63
+ unread: true,
64
+ unread_count: 2,
65
+ url: 'https://www.goofish.com/im?itemId=10001&peerUserId=90001',
66
+ row_index: 0,
67
+ },
68
+ {
69
+ peer_name: '李四',
70
+ peer_user_id: '90002',
71
+ item_id: '10002',
72
+ item_title: 'iPhone 15',
73
+ price: '',
74
+ last_message: '明天能发货',
75
+ unread: false,
76
+ unread_count: 0,
77
+ url: 'https://www.goofish.com/im?itemId=10002&peerUserId=90002',
78
+ row_index: 1,
79
+ },
80
+ ]);
81
+ });
82
+
83
+ it('extracts inbox conversations from the real virtualized conversation row shape', async () => {
84
+ const result = await runBrowserScript(`
85
+ <main>
86
+ <div id="conv-list-scrollable">
87
+ <div class="rc-virtual-list">
88
+ <div class="rc-virtual-list-holder">
89
+ <div class="rc-virtual-list-holder-inner">
90
+ <div class="conversation-item--abc">
91
+ <div>
92
+ <div><span><sup title="3"><span>3</span></sup></span></div>
93
+ <div>
94
+ <div><div>通知消息</div></div>
95
+ <div>订单即将自动确认收货</div>
96
+ <div>3小时前</div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <div class="conversation-item--abc">
101
+ <div>
102
+ <div>
103
+ <div><div>隔壁猫小小</div></div>
104
+ <div>亲,喜欢可以拍下,有问题留言哦~会尽快回复</div>
105
+ <div>21小时前</div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </main>
114
+ `, buildExtractInboxEvaluate(10));
115
+
116
+ expect(result.items).toEqual([
117
+ expect.objectContaining({
118
+ peer_name: '通知消息',
119
+ last_message: '订单即将自动确认收货',
120
+ unread: true,
121
+ unread_count: 3,
122
+ }),
123
+ expect.objectContaining({
124
+ peer_name: '隔壁猫小小',
125
+ last_message: '亲,喜欢可以拍下,有问题留言哦~会尽快回复',
126
+ unread: false,
127
+ }),
128
+ ]);
129
+ });
130
+
131
+ it('extracts all visible messages for a specific chat', async () => {
132
+ const state = await runBrowserScript(`
133
+ <main>
134
+ <textarea></textarea>
135
+ <button>发送</button>
136
+ <div class="message-topbar"><span class="text1">张三</span><span class="text2">(90001)</span></div>
137
+ <a href="/item?id=10001"><span class="title">MacBook Pro 14</span><span class="money">¥5999</span></a>
138
+ <div id="message-list-scrollable">
139
+ <div class="bubble">你好</div>
140
+ <div class="message">还在吗?</div>
141
+ <div class="msg">在的</div>
142
+ </div>
143
+ </main>
144
+ `, buildExtractChatStateEvaluate());
145
+
146
+ expect(state.peer_name).toBe('张三');
147
+ expect(state.peer_masked_id).toBe('90001');
148
+ expect(state.item_title).toBe('MacBook Pro 14');
149
+ expect(state.messages).toEqual([
150
+ { index: 1, text: '你好' },
151
+ { index: 2, text: '还在吗?' },
152
+ { index: 3, text: '在的' },
153
+ ]);
154
+ expect(state.visible_messages).toEqual(['你好', '还在吗?', '在的']);
155
+ });
156
+
157
+ it('extracts messages from the real Xianyu message-row shape', async () => {
158
+ const state = await runBrowserScript(`
159
+ <main>
160
+ <textarea></textarea>
161
+ <button>发 送</button>
162
+ <div id="message-list-scrollable">
163
+ <div class="message-list-reverse--x">
164
+ <div class="message-row--a">
165
+ <div class="ant-dropdown-trigger message-content--b">
166
+ <div class="message-text--c message-text-right--d"><span>我发出的消息</span></div>
167
+ <div class="read-status-text--e">已读</div>
168
+ </div>
169
+ </div>
170
+ <div class="message-row--a">
171
+ <div class="ant-dropdown-trigger message-content--b">
172
+ <div class="message-text--c message-text-left--d"><span>对方回复</span></div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </main>
178
+ `, buildExtractChatStateEvaluate());
179
+
180
+ expect(state.can_send).toBe(true);
181
+ expect(state.messages).toEqual([
182
+ { index: 1, text: '我发出的消息' },
183
+ { index: 2, text: '对方回复' },
184
+ ]);
185
+ });
186
+
187
+
188
+ it('registers reply as an explicit write command', () => {
189
+ const command = getRegistry().get('xianyu/reply');
190
+ expect(command?.access).toBe('write');
191
+ expect(command?.columns).toEqual(['status', 'peer_name', 'item_title', 'price', 'location', 'message']);
192
+ });
193
+
194
+ it('builds chat URLs and requires post-submit message evidence', async () => {
195
+ expect(buildChatUrl('10001', '90001')).toBe('https://www.goofish.com/im?itemId=10001&peerUserId=90001');
196
+
197
+ let clicked = false;
198
+ const result = await runBrowserScript(`
199
+ <textarea></textarea>
200
+ <button>发送</button>
201
+ <div id="message-list-scrollable"></div>
202
+ `, buildSendMessageEvaluate('你好'), {
203
+ beforeEval(window) {
204
+ window.document.querySelector('button').addEventListener('click', () => {
205
+ clicked = true;
206
+ const row = window.document.createElement('div');
207
+ row.className = 'message-row';
208
+ row.innerHTML = '<div class="message-text">你好</div>';
209
+ window.document.querySelector('#message-list-scrollable').append(row);
210
+ });
211
+ },
212
+ });
213
+
214
+ expect(result).toEqual({ ok: true });
215
+ expect(clicked).toBe(true);
216
+
217
+ await expect(runBrowserScript('<textarea></textarea><button>发送</button>', buildSendMessageEvaluate('没证据'), {
218
+ beforeEval(window) {
219
+ window.setTimeout = (fn) => {
220
+ fn();
221
+ return 0;
222
+ };
223
+ },
224
+ }))
225
+ .resolves.toEqual({ ok: false, reason: 'send-postcondition-timeout' });
226
+ });
227
+
228
+ it('requires an explicit conversation target for messages and reply', async () => {
229
+ const messages = getRegistry().get('xianyu/messages');
230
+ const reply = getRegistry().get('xianyu/reply');
231
+ const page = {
232
+ goto: () => { throw new Error('should not navigate'); },
233
+ wait: () => {},
234
+ evaluate: () => ({}),
235
+ };
236
+
237
+ await expect(messages.func(page, { limit: 1 })).rejects.toBeInstanceOf(ArgumentError);
238
+ await expect(reply.func(page, { text: 'hello' })).rejects.toBeInstanceOf(ArgumentError);
239
+ await expect(messages.func(page, { item_id: '10001', user_id: '90001', rank: 1 })).rejects.toBeInstanceOf(ArgumentError);
240
+ });
241
+
242
+ it('treats malformed evaluate payloads as command failures', async () => {
243
+ const messages = getRegistry().get('xianyu/messages');
244
+ const page = {
245
+ goto: () => {},
246
+ wait: () => {},
247
+ evaluate: () => null,
248
+ };
249
+
250
+ await expect(messages.func(page, { item_id: '10001', user_id: '90001', limit: 1 }))
251
+ .rejects.toBeInstanceOf(CommandExecutionError);
252
+ });
253
+ });
@@ -0,0 +1,96 @@
1
+ import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import {
4
+ buildClickInboxConversationEvaluate,
5
+ buildExtractInboxEvaluate,
6
+ buildInboxUrl,
7
+ buildReadCurrentConversationUrlEvaluate,
8
+ DEFAULT_INBOX_LIMIT,
9
+ MAX_INBOX_LIMIT,
10
+ normalizeLimit,
11
+ requireClickResult,
12
+ requireEvaluateObject,
13
+ } from './im.js';
14
+
15
+ cli({
16
+ site: 'xianyu',
17
+ name: 'inbox',
18
+ access: 'read',
19
+ description: '列出闲鱼最近私信会话',
20
+ domain: 'www.goofish.com',
21
+ strategy: Strategy.COOKIE,
22
+ navigateBefore: false,
23
+ browser: true,
24
+ args: [
25
+ { name: 'limit', type: 'int', default: DEFAULT_INBOX_LIMIT, help: 'Number of conversations to return' },
26
+ { name: 'unread-only', type: 'bool', default: false, help: 'Return only conversations with unread messages' },
27
+ { name: 'resolve-ids', type: 'bool', default: false, help: 'Click each visible conversation to resolve item_id and peer_user_id from the chat URL' },
28
+ ],
29
+ columns: ['rank', 'peer_name', 'peer_user_id', 'item_id', 'item_title', 'price', 'last_message', 'unread', 'unread_count', 'url'],
30
+ func: async (page, kwargs) => {
31
+ const limit = normalizeLimit(kwargs.limit, DEFAULT_INBOX_LIMIT, MAX_INBOX_LIMIT, 'inbox --limit');
32
+ const unreadOnly = Boolean(kwargs['unread-only']);
33
+ const resolveIds = Boolean(kwargs['resolve-ids']);
34
+ let currentUrl = '';
35
+ if (page.getCurrentUrl) {
36
+ try {
37
+ currentUrl = await page.getCurrentUrl();
38
+ } catch {
39
+ currentUrl = '';
40
+ }
41
+ }
42
+ if (!/https:\/\/www\.goofish\.com\/im\b/.test(currentUrl)) {
43
+ await page.goto(buildInboxUrl());
44
+ }
45
+ await page.wait(4);
46
+ const payload = requireEvaluateObject(await page.evaluate(buildExtractInboxEvaluate(limit)), 'inbox');
47
+ if (payload?.requiresAuth) {
48
+ throw new AuthRequiredError('www.goofish.com', 'Xianyu inbox requires a logged-in browser session');
49
+ }
50
+ if (payload?.blocked) {
51
+ throw new AuthRequiredError('www.goofish.com', 'Xianyu inbox is blocked by verification or risk control');
52
+ }
53
+ if (!Array.isArray(payload.items)) {
54
+ throw new CommandExecutionError('Xianyu inbox returned malformed conversation list');
55
+ }
56
+ const items = payload.items;
57
+ if (!items.length) {
58
+ throw new EmptyResultError('xianyu inbox', 'No Xianyu inbox conversations were found');
59
+ }
60
+ let conversations = items.slice(0, limit);
61
+ if (unreadOnly) {
62
+ conversations = conversations.filter((item) => Boolean(item.unread));
63
+ }
64
+ if (resolveIds) {
65
+ for (const item of conversations) {
66
+ if (item.item_id && item.peer_user_id) continue;
67
+ const rowIndex = Number(item.row_index);
68
+ if (!Number.isInteger(rowIndex) || rowIndex < 0) continue;
69
+ requireClickResult(await page.evaluate(buildClickInboxConversationEvaluate(rowIndex)), 'inbox resolve-ids click');
70
+ await page.wait(1);
71
+ const current = requireEvaluateObject(await page.evaluate(buildReadCurrentConversationUrlEvaluate()), 'inbox current-url');
72
+ item.item_id = current?.item_id || item.item_id || '';
73
+ item.peer_user_id = current?.peer_user_id || item.peer_user_id || '';
74
+ item.url = current?.url || item.url || '';
75
+ }
76
+ }
77
+ return conversations.map((item, index) => ({
78
+ rank: index + 1,
79
+ peer_name: item.peer_name || '',
80
+ peer_user_id: item.peer_user_id || '',
81
+ item_id: item.item_id || '',
82
+ item_title: item.item_title || '',
83
+ price: item.price || '',
84
+ last_message: item.last_message || '',
85
+ unread: Boolean(item.unread),
86
+ unread_count: Number(item.unread_count || 0),
87
+ url: item.url || '',
88
+ }));
89
+ },
90
+ });
91
+
92
+ export const __test__ = {
93
+ buildInboxUrl,
94
+ buildExtractInboxEvaluate,
95
+ normalizeLimit,
96
+ };
@@ -0,0 +1,91 @@
1
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import {
4
+ buildChatUrl,
5
+ buildClickInboxConversationEvaluate,
6
+ buildExtractChatStateEvaluate,
7
+ DEFAULT_MESSAGE_LIMIT,
8
+ MAX_MESSAGE_LIMIT,
9
+ normalizeLimit,
10
+ normalizeRank,
11
+ requireClickResult,
12
+ requireEvaluateObject,
13
+ } from './im.js';
14
+ import { normalizeNumericId } from './utils.js';
15
+
16
+ cli({
17
+ site: 'xianyu',
18
+ name: 'messages',
19
+ access: 'read',
20
+ description: '读取指定闲鱼私信会话的最近聊天内容',
21
+ domain: 'www.goofish.com',
22
+ strategy: Strategy.COOKIE,
23
+ navigateBefore: false,
24
+ browser: true,
25
+ args: [
26
+ { name: 'item_id', positional: true, help: '闲鱼商品 item_id' },
27
+ { name: 'user_id', positional: true, help: '聊一聊对方的 user_id / peerUserId' },
28
+ { name: 'limit', type: 'int', default: DEFAULT_MESSAGE_LIMIT, help: 'Number of visible messages to return' },
29
+ { name: 'rank', type: 'int', default: 0, help: 'Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs' },
30
+ ],
31
+ columns: ['index', 'peer_name', 'item_title', 'message', 'item_id', 'peer_user_id', 'url'],
32
+ func: async (page, kwargs) => {
33
+ const hasItemId = kwargs.item_id != null && kwargs.item_id !== '';
34
+ const hasUserId = kwargs.user_id != null && kwargs.user_id !== '';
35
+ const rank = normalizeRank(kwargs.rank);
36
+ if (rank > 0 && (hasItemId || hasUserId)) {
37
+ throw new ArgumentError('xianyu messages accepts either item_id/user_id or --rank, not both');
38
+ }
39
+ if (rank === 0 && hasItemId !== hasUserId) {
40
+ throw new ArgumentError('xianyu messages requires both item_id and user_id, or --rank from xianyu inbox');
41
+ }
42
+ if (rank === 0 && !hasItemId && !hasUserId) {
43
+ throw new ArgumentError('xianyu messages requires item_id/user_id or --rank from xianyu inbox');
44
+ }
45
+ const hasIds = hasItemId && hasUserId;
46
+ const itemId = hasIds ? normalizeNumericId(kwargs.item_id, 'item_id', '1038951278192') : '';
47
+ const userId = hasIds ? normalizeNumericId(kwargs.user_id, 'user_id', '3650092411') : '';
48
+ const limit = normalizeLimit(kwargs.limit, DEFAULT_MESSAGE_LIMIT, MAX_MESSAGE_LIMIT, 'messages --limit');
49
+ let url = '';
50
+ if (hasIds) {
51
+ url = buildChatUrl(itemId, userId);
52
+ await page.goto(url);
53
+ } else {
54
+ if (!page.getCurrentUrl || !/https:\/\/www\.goofish\.com\/im\b/.test(await page.getCurrentUrl())) {
55
+ await page.goto('https://www.goofish.com/im');
56
+ }
57
+ }
58
+ await page.wait(2);
59
+ if (rank > 0) {
60
+ requireClickResult(await page.evaluate(buildClickInboxConversationEvaluate(rank - 1)), 'messages rank click');
61
+ await page.wait(2);
62
+ }
63
+ const state = requireEvaluateObject(await page.evaluate(buildExtractChatStateEvaluate(limit)), 'messages');
64
+ if (state?.requiresAuth) {
65
+ throw new AuthRequiredError('www.goofish.com', 'Xianyu messages requires a logged-in browser session');
66
+ }
67
+ if (!Array.isArray(state.messages)) {
68
+ throw new CommandExecutionError('Xianyu messages returned malformed message list');
69
+ }
70
+ const messages = state.messages;
71
+ if (!messages.length) {
72
+ throw new EmptyResultError('xianyu messages', 'No visible messages were found in this Xianyu conversation');
73
+ }
74
+ return messages.slice(-limit).map((message, index) => ({
75
+ index: index + 1,
76
+ peer_name: state.peer_name || '',
77
+ item_title: state.item_title || '',
78
+ message: message.text || '',
79
+ item_id: itemId,
80
+ peer_user_id: userId,
81
+ url: url || '',
82
+ }));
83
+ },
84
+ });
85
+
86
+ export const __test__ = {
87
+ buildChatUrl,
88
+ buildExtractChatStateEvaluate,
89
+ normalizeLimit,
90
+ normalizeRank,
91
+ };
@@ -0,0 +1,82 @@
1
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, selectorError } from '@jackwener/opencli/errors';
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { buildChatUrl, buildClickInboxConversationEvaluate, buildExtractChatStateEvaluate, buildSendMessageEvaluate, normalizeRank, requireClickResult, requireEvaluateObject, requireText } from './im.js';
4
+ import { normalizeNumericId } from './utils.js';
5
+
6
+ cli({
7
+ site: 'xianyu',
8
+ name: 'reply',
9
+ access: 'write',
10
+ description: '回复指定闲鱼私信会话',
11
+ domain: 'www.goofish.com',
12
+ strategy: Strategy.COOKIE,
13
+ navigateBefore: false,
14
+ browser: true,
15
+ args: [
16
+ { name: 'item_id', positional: true, help: '闲鱼商品 item_id' },
17
+ { name: 'user_id', positional: true, help: '聊一聊对方的 user_id / peerUserId' },
18
+ { name: 'text', required: true, help: 'Message text to send' },
19
+ { name: 'rank', type: 'int', default: 0, help: 'Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs' },
20
+ ],
21
+ columns: ['status', 'peer_name', 'item_title', 'price', 'location', 'message'],
22
+ func: async (page, kwargs) => {
23
+ const hasItemId = kwargs.item_id != null && kwargs.item_id !== '';
24
+ const hasUserId = kwargs.user_id != null && kwargs.user_id !== '';
25
+ const rank = normalizeRank(kwargs.rank);
26
+ if (rank > 0 && (hasItemId || hasUserId)) {
27
+ throw new ArgumentError('xianyu reply accepts either item_id/user_id or --rank, not both');
28
+ }
29
+ if (rank === 0 && hasItemId !== hasUserId) {
30
+ throw new ArgumentError('xianyu reply requires both item_id and user_id, or --rank from xianyu inbox');
31
+ }
32
+ if (rank === 0 && !hasItemId && !hasUserId) {
33
+ throw new ArgumentError('xianyu reply requires item_id/user_id or --rank from xianyu inbox');
34
+ }
35
+ const hasIds = hasItemId && hasUserId;
36
+ const itemId = hasIds ? normalizeNumericId(kwargs.item_id, 'item_id', '1038951278192') : '';
37
+ const userId = hasIds ? normalizeNumericId(kwargs.user_id, 'user_id', '3650092411') : '';
38
+ const text = requireText(kwargs.text, 'xianyu reply --text');
39
+ const url = hasIds ? buildChatUrl(itemId, userId) : '';
40
+ if (hasIds) {
41
+ await page.goto(url);
42
+ } else {
43
+ if (!page.getCurrentUrl || !/https:\/\/www\.goofish\.com\/im\b/.test(await page.getCurrentUrl())) {
44
+ await page.goto('https://www.goofish.com/im');
45
+ }
46
+ }
47
+ await page.wait(2);
48
+ if (rank > 0) {
49
+ requireClickResult(await page.evaluate(buildClickInboxConversationEvaluate(rank - 1)), 'reply rank click');
50
+ await page.wait(2);
51
+ }
52
+ const state = requireEvaluateObject(await page.evaluate(buildExtractChatStateEvaluate()), 'reply');
53
+ if (state?.requiresAuth) {
54
+ throw new AuthRequiredError('www.goofish.com', 'Xianyu reply requires a logged-in browser session');
55
+ }
56
+ if (!state?.can_input) {
57
+ throw selectorError('闲鱼聊天输入框', '未找到可用的聊天输入框,请确认该会话页已正确加载');
58
+ }
59
+ const sent = requireEvaluateObject(await page.evaluate(buildSendMessageEvaluate(text)), 'reply send');
60
+ if (!sent?.ok) {
61
+ throw new CommandExecutionError(`Xianyu reply did not observe the sent message: ${sent?.reason || 'unknown-reason'}`);
62
+ }
63
+ await page.wait(1);
64
+ return [{
65
+ status: 'sent',
66
+ peer_name: state.peer_name || '',
67
+ item_title: state.item_title || '',
68
+ price: state.price || '',
69
+ location: state.location || '',
70
+ message: text,
71
+ peer_user_id: userId,
72
+ item_id: itemId,
73
+ url: url || '',
74
+ item_url: state.item_url || '',
75
+ }];
76
+ },
77
+ });
78
+
79
+ export const __test__ = {
80
+ buildChatUrl,
81
+ buildSendMessageEvaluate,
82
+ };