@jackwener/opencli 1.7.8 → 1.7.10

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 (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  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/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -0,0 +1,517 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import { __test__ } from './item.js';
4
+
5
+ const originalPerformance = globalThis.performance;
6
+ const originalWindow = globalThis.window;
7
+ const originalDocument = globalThis.document;
8
+
9
+ function restoreGlobals() {
10
+ globalThis.performance = originalPerformance;
11
+ globalThis.window = originalWindow;
12
+ globalThis.document = originalDocument;
13
+ }
14
+
15
+ describe('jd item image helpers', () => {
16
+ it('normalizes JD item URL input to a SKU before building selectors', () => {
17
+ expect(__test__.normalizeJdSkuInput('100328272886')).toBe('100328272886');
18
+ expect(__test__.normalizeJdSkuInput('https://item.jd.com/100328272886.html?purchasetab=gfgm')).toBe('100328272886');
19
+ expect(__test__.normalizeJdSkuInput('skuId=10218494560141')).toBe('10218494560141');
20
+ });
21
+
22
+ it('normalizes protocol-relative and thumbnail-sized JD image URLs', () => {
23
+ expect(__test__.normalizeJdImageUrl('//img10.360buyimg.com/imgzone/jfs/a.jpg.avif')).toBe('https://img10.360buyimg.com/imgzone/jfs/a.jpg.avif');
24
+ expect(__test__.normalizeJdImageSize('https://img10.360buyimg.com/pcpubliccms/s228x228_jfs/t1/a.jpg.avif')).toBe('https://img10.360buyimg.com/pcpubliccms/jfs/t1/a.jpg.avif');
25
+ expect(__test__.normalizeJdImageSize('https://img10.360buyimg.com/n1/s450x450_jfs/t1/a.jpg.avif')).toBe('https://img10.360buyimg.com/n1/jfs/t1/a.jpg.avif');
26
+ });
27
+
28
+ it('accepts only product main images for mainImages', () => {
29
+ expect(__test__.isJdMainImage('https://img10.360buyimg.com/pcpubliccms/jfs/t1/main.jpg.avif')).toBe(true);
30
+ expect(__test__.isJdMainImage('https://img10.360buyimg.com/pcpubliccms/s228x228_jfs/t1/main.jpg.avif')).toBe(true);
31
+ expect(__test__.isJdMainImage('https://img10.360buyimg.com/n1/jfs/t1/main.jpg.avif')).toBe(true);
32
+
33
+ expect(__test__.isJdMainImage('https://img10.360buyimg.com/imgzone/jfs/t1/detail.jpg.avif')).toBe(false);
34
+ expect(__test__.isJdMainImage('https://img10.360buyimg.com/shaidan/jfs/t1/user.jpg.avif')).toBe(false);
35
+ expect(__test__.isJdMainImage('https://img10.360buyimg.com/babel/jfs/t1/recommend.jpg.avif')).toBe(false);
36
+ });
37
+
38
+ it('accepts only detail-area image CDN paths for detailImages', () => {
39
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/imgzone/jfs/t1/detail.jpg.avif')).toBe(true);
40
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/jdcms/jfs/t1/detail.jpg.avif')).toBe(true);
41
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/babel/jfs/t1/detail.jpg.avif')).toBe(true);
42
+
43
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/pcpubliccms/jfs/t1/main.jpg.avif')).toBe(false);
44
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/pcpubliccms/s228x228_jfs/t1/thumb.jpg.avif')).toBe(false);
45
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/n1/jfs/t1/main.jpg.avif')).toBe(false);
46
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/sku/jfs/t1/color-option.gif')).toBe(false);
47
+ expect(__test__.isJdWareGraphicDetailImage('https://img10.360buyimg.com/sku/jfs/t1/ware-graphic.jpg')).toBe(true);
48
+ expect(__test__.isJdWareGraphicDetailImage('https://img10.360buyimg.com/sku/s228x228_jfs/t1/thumb.jpg')).toBe(false);
49
+ expect(__test__.isJdDetailImage('https://img10.360buyimg.com/shaidan/jfs/t1/user.jpg.avif')).toBe(false);
50
+ });
51
+
52
+ it('keeps legacy avifImages restricted to detail images only', () => {
53
+ expect(__test__.extractAvifImages([
54
+ 'https://img10.360buyimg.com/pcpubliccms/jfs/t1/main.jpg.avif',
55
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/detail.jpg.avif',
56
+ 'https://img10.360buyimg.com/shaidan/jfs/t1/user.jpg.avif',
57
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/detail.gif',
58
+ ], 10)).toEqual([
59
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/detail.jpg.avif',
60
+ ]);
61
+ });
62
+
63
+ it('prioritizes JPG detail images before PNG banners and GIFs when limiting detailImages', () => {
64
+ expect(__test__.orderJdDetailImages([
65
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/hero-a.gif',
66
+ 'https://img11.360buyimg.com/imgzone/jfs/t1/banner.png.avif',
67
+ 'https://img12.360buyimg.com/imgzone/jfs/t1/326893/25/18592/117159/68c264f5F9a41addf/2c9ad60b7f390339.jpg.avif',
68
+ 'https://img12.360buyimg.com/imgzone/jfs/t1/330152/17/11906/130964/68c264f2Ffcf6e5c1/c2ccb28722dc47ce.jpg.avif',
69
+ 'https://img11.360buyimg.com/cms/jfs/t1/banner.gif',
70
+ ])).toEqual([
71
+ 'https://img12.360buyimg.com/imgzone/jfs/t1/326893/25/18592/117159/68c264f5F9a41addf/2c9ad60b7f390339.jpg.avif',
72
+ 'https://img12.360buyimg.com/imgzone/jfs/t1/330152/17/11906/130964/68c264f2Ffcf6e5c1/c2ccb28722dc47ce.jpg.avif',
73
+ 'https://img11.360buyimg.com/imgzone/jfs/t1/banner.png.avif',
74
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/hero-a.gif',
75
+ 'https://img11.360buyimg.com/cms/jfs/t1/banner.gif',
76
+ ]);
77
+ });
78
+
79
+ it('extracts valid JD price payload values', () => {
80
+ expect(__test__.extractPriceFromPayload([{ id: 'J_100291143898', p: '6999.00' }])).toBe('6999.00');
81
+ expect(__test__.extractPriceFromPayload([{ id: 'J_100291143898', p: '-1.00', op: '7299.00' }])).toBe('7299.00');
82
+ expect(__test__.extractPriceFromPayload([])).toBe('');
83
+ });
84
+
85
+ it('extracts visible JD price text from DOM', () => {
86
+ const dom = new JSDOM(`
87
+ <div>
88
+ <span>预售价</span>
89
+ <span><span>¥</span><span>12221</span></span>
90
+ </div>
91
+ `);
92
+ const previousDocument = globalThis.document;
93
+ globalThis.document = dom.window.document;
94
+
95
+ try {
96
+ expect(__test__.normalizePriceText('¥ 12221')).toBe('12221');
97
+ expect(__test__.extractPriceFromDom('100291143898')).toBe('12221');
98
+ }
99
+ finally {
100
+ globalThis.document = previousDocument;
101
+ }
102
+ });
103
+
104
+ it('extracts only gallery main images and detail-container images from DOM', () => {
105
+ const dom = new JSDOM(`
106
+ <div class="_gallery_116km_1">
107
+ <img src="https://img10.360buyimg.com/pcpubliccms/s228x228_jfs/t1/main-a.jpg.avif" />
108
+ <img data-src="//img10.360buyimg.com/n1/s450x450_jfs/t1/main-b.jpg.avif" />
109
+ <img src="https://img10.360buyimg.com/imgzone/jfs/t1/wrong-detail-in-gallery.jpg.avif" />
110
+ </div>
111
+ <div class="recommend">
112
+ <img src="https://img10.360buyimg.com/babel/jfs/t1/recommend.jpg.avif" />
113
+ <img src="https://img10.360buyimg.com/pcpubliccms/jfs/t1/recommend-main-like.jpg.avif" />
114
+ </div>
115
+ <h2 id="SPXQ-title">商品详情</h2>
116
+ <div class="detail-content-wrap">
117
+ <img src="https://img10.360buyimg.com/imgzone/jfs/t1/detail-hero.gif" />
118
+ <img src="https://img10.360buyimg.com/imgzone/jfs/t1/detail-a.jpg.avif" />
119
+ <source srcset="https://img11.360buyimg.com/imgzone/jfs/t1/detail-source.jpg.avif 1x" />
120
+ <img src="https://img10.360buyimg.com/pcpubliccms/jfs/t1/wrong-main-in-detail.jpg.avif" />
121
+ <img src="https://img10.360buyimg.com/shaidan/jfs/t1/wrong-user.jpg.avif" />
122
+ </div>
123
+ <div id="spec-list">
124
+ <img src="https://img10.360buyimg.com/pcpubliccms/s48x48_jfs/t1/wrong-sku-option.jpg.avif" />
125
+ </div>
126
+ `);
127
+ const previousDocument = globalThis.document;
128
+ globalThis.document = dom.window.document;
129
+
130
+ try {
131
+ expect(__test__.extractMainImages(10)).toEqual([
132
+ 'https://img10.360buyimg.com/pcpubliccms/jfs/t1/main-a.jpg.avif',
133
+ 'https://img10.360buyimg.com/n1/jfs/t1/main-b.jpg.avif',
134
+ ]);
135
+ expect(__test__.extractDetailImagesFromDom(10)).toEqual([
136
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/detail-a.jpg.avif',
137
+ 'https://img11.360buyimg.com/imgzone/jfs/t1/detail-source.jpg.avif',
138
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/detail-hero.gif',
139
+ ]);
140
+ }
141
+ finally {
142
+ globalThis.document = previousDocument;
143
+ }
144
+ });
145
+
146
+ it('collects JD detail images from computed background images', () => {
147
+ const dom = new JSDOM(`
148
+ <div id="J-detail">
149
+ <div class="ssd-module computed-bg"></div>
150
+ <div class="ssd-module ignored-bg"></div>
151
+ </div>
152
+ `);
153
+ const previousDocument = globalThis.document;
154
+ const previousGetComputedStyle = globalThis.getComputedStyle;
155
+ globalThis.document = dom.window.document;
156
+ globalThis.getComputedStyle = ((element: Element) => ({
157
+ background: '',
158
+ backgroundImage: element.classList.contains('computed-bg')
159
+ ? 'url("//img10.360buyimg.com/imgzone/jfs/t1/computed-detail.jpg.avif")'
160
+ : 'none',
161
+ })) as typeof getComputedStyle;
162
+
163
+ try {
164
+ expect(__test__.extractDetailImagesFromDom(10)).toEqual([
165
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/computed-detail.jpg.avif',
166
+ ]);
167
+ }
168
+ finally {
169
+ globalThis.document = previousDocument;
170
+ globalThis.getComputedStyle = previousGetComputedStyle;
171
+ }
172
+ });
173
+
174
+ it('collects JD detail images from inline JSON-like script text', () => {
175
+ const dom = new JSDOM(`
176
+ <h2 id="SPXQ-title">商品详情</h2>
177
+ <script>
178
+ window.__DETAIL_DATA__ = {
179
+ images: [
180
+ "https://img10.360buyimg.com/imgzone/jfs/t1/script-detail-a.jpg.avif",
181
+ "//img11.360buyimg.com/imgzone/jfs/t1/script-detail-b.gif"
182
+ ]
183
+ };
184
+ </script>
185
+ `);
186
+ const previousDocument = globalThis.document;
187
+ globalThis.document = dom.window.document;
188
+
189
+ try {
190
+ expect(__test__.extractDetailImagesFromDom(10)).toEqual([
191
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/script-detail-a.jpg.avif',
192
+ 'https://img11.360buyimg.com/imgzone/jfs/t1/script-detail-b.gif',
193
+ ]);
194
+ }
195
+ finally {
196
+ globalThis.document = previousDocument;
197
+ }
198
+ });
199
+
200
+ it('collects JD detail images from same-origin iframe content', () => {
201
+ const dom = new JSDOM(`
202
+ <h2 id="SPXQ-title">商品详情</h2>
203
+ <iframe id="detail-frame"></iframe>
204
+ `, { url: 'https://item.jd.com/100328272886.html' });
205
+ const frameDom = new JSDOM(`
206
+ <div id="J-detail">
207
+ <img src="https://img10.360buyimg.com/imgzone/jfs/t1/frame-detail-a.jpg.avif" />
208
+ <div style="background-image:url(//img11.360buyimg.com/cms/jfs/t1/frame-detail-b.jpg.avif)"></div>
209
+ </div>
210
+ `, { url: 'https://item.jd.com/detail-frame.html' });
211
+ const iframe = dom.window.document.getElementById('detail-frame') as HTMLIFrameElement;
212
+ Object.defineProperty(iframe, 'contentDocument', { value: frameDom.window.document, configurable: true });
213
+ Object.defineProperty(iframe, 'contentWindow', { value: frameDom.window, configurable: true });
214
+
215
+ const previousDocument = globalThis.document;
216
+ globalThis.document = dom.window.document;
217
+
218
+ try {
219
+ expect(__test__.extractDetailImagesFromDom(10)).toEqual([
220
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/frame-detail-a.jpg.avif',
221
+ 'https://img11.360buyimg.com/cms/jfs/t1/frame-detail-b.jpg.avif',
222
+ ]);
223
+ }
224
+ finally {
225
+ globalThis.document = previousDocument;
226
+ }
227
+ });
228
+
229
+ it('collects JD detail images from page data objects', async () => {
230
+ const dom = new JSDOM(`
231
+ <h2 id="SPXQ-title">商品详情</h2>
232
+ `);
233
+ globalThis.document = dom.window.document;
234
+ globalThis.window = dom.window as unknown as Window;
235
+ (globalThis as typeof globalThis & { __PAGE_DATA__?: unknown }).__PAGE_DATA__ = {
236
+ detail: {
237
+ images: [
238
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/page-data-a.jpg.avif',
239
+ { src: '//img11.360buyimg.com/imgzone/jfs/t1/page-data-b.webp' },
240
+ ],
241
+ },
242
+ };
243
+
244
+ try {
245
+ await expect(__test__.extractDetailImagesFromPage(10)).resolves.toEqual([
246
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/page-data-a.jpg.avif',
247
+ 'https://img11.360buyimg.com/imgzone/jfs/t1/page-data-b.webp',
248
+ ]);
249
+ }
250
+ finally {
251
+ delete (globalThis as typeof globalThis & { __PAGE_DATA__?: unknown }).__PAGE_DATA__;
252
+ restoreGlobals();
253
+ }
254
+ });
255
+
256
+ it('collects JD detail images from network resource text', async () => {
257
+ const dom = new JSDOM(`
258
+ <h2 id="SPXQ-title">商品详情</h2>
259
+ `);
260
+ const previousFetch = globalThis.fetch;
261
+ globalThis.document = dom.window.document;
262
+ globalThis.window = dom.window as unknown as Window;
263
+ const fakePerformance = {
264
+ getEntriesByType: () => [{ name: 'https://cdn.jd.com/detail/data.json' }],
265
+ now: () => 0,
266
+ } as Performance;
267
+ globalThis.performance = fakePerformance;
268
+ globalThis.fetch = (async () => ({
269
+ ok: true,
270
+ headers: { get: () => 'application/json' },
271
+ text: async () => JSON.stringify({
272
+ detail: {
273
+ imgs: ['https://img10.360buyimg.com/imgzone/jfs/t1/network-detail-a.jpg.avif'],
274
+ },
275
+ }),
276
+ })) as typeof fetch;
277
+
278
+ try {
279
+ await expect(__test__.extractDetailImagesFromPage(10)).resolves.toEqual([
280
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/network-detail-a.jpg.avif',
281
+ ]);
282
+ }
283
+ finally {
284
+ globalThis.fetch = previousFetch;
285
+ restoreGlobals();
286
+ }
287
+ });
288
+
289
+ it('collects JD detail images from WareGraphic graphicContent', async () => {
290
+ const dom = new JSDOM(`
291
+ <h2 id="SPXQ-title">商品详情</h2>
292
+ `);
293
+ const previousFetch = globalThis.fetch;
294
+ globalThis.document = dom.window.document;
295
+ globalThis.window = dom.window as unknown as Window;
296
+ globalThis.performance = {
297
+ getEntriesByType: () => [
298
+ { name: 'https://api.m.jd.com/client.action?functionId=pc_item_getWareGraphic&skuId=100328272886' },
299
+ ],
300
+ now: () => 0,
301
+ } as Performance;
302
+ globalThis.fetch = (async () => ({
303
+ ok: true,
304
+ headers: { get: () => 'application/json' },
305
+ text: async () => JSON.stringify({
306
+ data: {
307
+ graphicContent: `
308
+ <div style="background-image:url(//img30.360buyimg.com/sku/jfs/t1/ware-a.jpg)"></div>
309
+ <img src="https://img30.360buyimg.com/sku/jfs/t1/ware-b.png" />
310
+ <a href="https://item.jd.com/100328272886.html">not image</a>
311
+ `,
312
+ },
313
+ }),
314
+ })) as typeof fetch;
315
+
316
+ try {
317
+ await expect(__test__.extractDetailImagesFromPage(10)).resolves.toEqual([
318
+ 'https://img30.360buyimg.com/sku/jfs/t1/ware-a.jpg',
319
+ 'https://img30.360buyimg.com/sku/jfs/t1/ware-b.png',
320
+ ]);
321
+ }
322
+ finally {
323
+ globalThis.fetch = previousFetch;
324
+ restoreGlobals();
325
+ }
326
+ });
327
+
328
+ it('collects images from every repeated JD detail module', () => {
329
+ const dom = new JSDOM(`
330
+ <h2 id="SPXQ-title">商品详情</h2>
331
+ <div class="ssd-module-wrap">
332
+ <img src="https://img10.360buyimg.com/imgzone/jfs/t1/detail-a.jpg.avif" />
333
+ </div>
334
+ <div class="ssd-module-wrap">
335
+ <img src="https://img11.360buyimg.com/imgzone/jfs/t1/detail-b.jpg.avif" />
336
+ </div>
337
+ <div class="ssd-module-wrap">
338
+ <img src="https://img12.360buyimg.com/imgzone/jfs/t1/detail-c.gif" />
339
+ </div>
340
+ `);
341
+ const previousDocument = globalThis.document;
342
+ globalThis.document = dom.window.document;
343
+
344
+ try {
345
+ expect(__test__.extractDetailImagesFromDom(10)).toEqual([
346
+ 'https://img10.360buyimg.com/imgzone/jfs/t1/detail-a.jpg.avif',
347
+ 'https://img11.360buyimg.com/imgzone/jfs/t1/detail-b.jpg.avif',
348
+ 'https://img12.360buyimg.com/imgzone/jfs/t1/detail-c.gif',
349
+ ]);
350
+ }
351
+ finally {
352
+ globalThis.document = previousDocument;
353
+ }
354
+ });
355
+
356
+ it('reports detail scroll progress so lazy-loaded detail images can stabilize', () => {
357
+ const dom = new JSDOM(`
358
+ <h2 id="SPXQ-title">商品详情</h2>
359
+ <div class="detail-content-wrap">
360
+ <img src="https://img10.360buyimg.com/imgzone/jfs/t1/detail-a.jpg.avif" />
361
+ <img src="https://img10.360buyimg.com/imgzone/jfs/t1/detail-b.jpg.avif" />
362
+ </div>
363
+ `);
364
+ const previousDocument = globalThis.document;
365
+ const previousWindow = globalThis.window;
366
+ globalThis.document = dom.window.document;
367
+ globalThis.window = dom.window as unknown as Window & typeof globalThis;
368
+ Object.defineProperty(dom.window, 'scrollY', { value: 1800, configurable: true });
369
+ Object.defineProperty(dom.window, 'innerHeight', { value: 900, configurable: true });
370
+ Object.defineProperty(dom.window.document.documentElement, 'scrollHeight', { value: 2600, configurable: true });
371
+
372
+ try {
373
+ expect(__test__.getJdDetailScrollSnapshot(10)).toMatchObject({
374
+ detailImageCount: 2,
375
+ scrollY: 1800,
376
+ viewportHeight: 900,
377
+ scrollHeight: 2600,
378
+ nearBottom: true,
379
+ });
380
+ }
381
+ finally {
382
+ globalThis.document = previousDocument;
383
+ globalThis.window = previousWindow;
384
+ }
385
+ });
386
+
387
+ it('parses structured specs without pairing unrelated body text', () => {
388
+ expect(__test__.extractSpecsFromText([
389
+ '品牌:美的(Midea)',
390
+ '商品编号',
391
+ '100291143898',
392
+ '洗涤容量',
393
+ '能效等级',
394
+ '一级能效',
395
+ '类型',
396
+ '加入购物车',
397
+ ].join('\n'))).toEqual({
398
+ 品牌: '美的(Midea)',
399
+ 商品编号: '100291143898',
400
+ 能效等级: '一级能效',
401
+ });
402
+ });
403
+
404
+ it('extracts selected specs from the newer JD spec-list DOM', () => {
405
+ const dom = new JSDOM(`
406
+ <div id="spec-list" class="page-right-spec">
407
+ <div class="horizontal-layout specification-series-layout">
408
+ <div class="layout-label">系列品</div>
409
+ <div class="layout-content">
410
+ <div class="specification-series-item specification-series-item--selected">
411
+ <span class="specification-series-item-text">【年度新品】玉兔3.0pro 12kg</span>
412
+ </div>
413
+ <div class="specification-series-item"><span class="specification-series-item-text">其他系列</span></div>
414
+ </div>
415
+ </div>
416
+ <div class="specifications-panel-content">
417
+ <div class="specification-group">
418
+ <div class="specification-group-label">款式</div>
419
+ <div class="specification-group-content">
420
+ <div class="specification-item-sku has-image specification-item-sku--selected" title="">
421
+ <img class="specification-item-sku-image" alt="洗烘套装" src="https://img13.360buyimg.com/pcpubliccms/s48x48_jfs/t1/spec.jpg.avif" />
422
+ <span class="specification-item-sku-text">洗烘套装</span>
423
+ </div>
424
+ <div class="specification-item-sku has-image"><span class="specification-item-sku-text">滚筒单洗</span></div>
425
+ </div>
426
+ </div>
427
+ </div>
428
+ </div>
429
+ `);
430
+ const previousDocument = globalThis.document;
431
+ globalThis.document = dom.window.document;
432
+
433
+ try {
434
+ expect(__test__.extractSpecs()).toEqual({
435
+ 系列品: '【年度新品】玉兔3.0pro 12kg',
436
+ 款式: '洗烘套装',
437
+ });
438
+ }
439
+ finally {
440
+ globalThis.document = previousDocument;
441
+ }
442
+ });
443
+
444
+ it('detects whether the loaded page is the expected JD product page', () => {
445
+ const dom = new JSDOM(`
446
+ <html>
447
+ <head><title>京东</title></head>
448
+ <body><div>请登录</div><div class="sku-title">商品标题</div><div id="spec-list"></div></body>
449
+ </html>
450
+ `, { url: 'https://item.jd.com/100291143898.html' });
451
+ const previousDocument = globalThis.document;
452
+ const previousLocation = globalThis.location;
453
+ globalThis.document = dom.window.document;
454
+ globalThis.location = dom.window.location;
455
+
456
+ try {
457
+ expect(__test__.detectJdPageState('100291143898')).toMatchObject({
458
+ isProductPage: true,
459
+ hasProductMarker: true,
460
+ onExpectedItemUrl: true,
461
+ looksBlocked: false,
462
+ isLoginPage: false,
463
+ hasSecurityChallenge: false,
464
+ });
465
+ document.body.innerHTML = '<div>请登录后完成安全验证</div>';
466
+ expect(__test__.detectJdPageState('100291143898')).toMatchObject({
467
+ isProductPage: false,
468
+ looksBlocked: true,
469
+ hasSecurityChallenge: true,
470
+ });
471
+ const riskDom = new JSDOM(`
472
+ <html>
473
+ <head><title>京东验证</title></head>
474
+ <body><div>京东验证</div></body>
475
+ </html>
476
+ `, { url: 'https://cfe.m.jd.com/privatedomain/risk_handler/03101900/?returnurl=https%3A%2F%2Fitem.jd.com%2F100291143898.html' });
477
+ globalThis.document = riskDom.window.document;
478
+ globalThis.location = riskDom.window.location;
479
+ expect(__test__.detectJdPageState('100291143898')).toMatchObject({
480
+ isProductPage: false,
481
+ looksBlocked: true,
482
+ hasSecurityChallenge: true,
483
+ });
484
+ }
485
+ finally {
486
+ globalThis.document = previousDocument;
487
+ globalThis.location = previousLocation;
488
+ }
489
+ });
490
+
491
+ it('does not treat JD login page as a product page', () => {
492
+ const dom = new JSDOM(`
493
+ <html>
494
+ <head><title>京东-欢迎登录</title></head>
495
+ <body><img src="https://img10.360buyimg.com/img/jfs/login.png" /></body>
496
+ </html>
497
+ `, { url: 'https://passport.jd.com/new/login.aspx?ReturnUrl=https%3A%2F%2Fitem.jd.com%2F100291143898.html' });
498
+ const previousDocument = globalThis.document;
499
+ const previousLocation = globalThis.location;
500
+ globalThis.document = dom.window.document;
501
+ globalThis.location = dom.window.location;
502
+
503
+ try {
504
+ expect(__test__.detectJdPageState('100291143898')).toMatchObject({
505
+ isProductPage: false,
506
+ hasProductMarker: false,
507
+ onExpectedItemUrl: false,
508
+ looksBlocked: true,
509
+ isLoginPage: true,
510
+ });
511
+ }
512
+ finally {
513
+ globalThis.document = previousDocument;
514
+ globalThis.location = previousLocation;
515
+ }
516
+ });
517
+ });
@@ -19,7 +19,7 @@ cli({
19
19
  { name: 'limit', type: 'int', default: 5, help: 'Number of comments' },
20
20
  ],
21
21
  columns: ['rank', 'score', 'author', 'text'],
22
- func: async (_page, kwargs) => {
22
+ func: async (kwargs) => {
23
23
  const postId = gqlEscape(parsePostId(String(kwargs['url-or-id'])));
24
24
  const limit = Number(kwargs.limit ?? 5);
25
25
  // Fetch post title and comments in parallel
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "curated", limit: ${limit}}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "frontpage", limit: ${limit}}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "new", limit: ${limit}}}) {
@@ -18,7 +18,7 @@ cli({
18
18
  },
19
19
  ],
20
20
  columns: ['title', 'author', 'karma', 'comments', 'tags', 'content', 'url'],
21
- func: async (_page, kwargs) => {
21
+ func: async (kwargs) => {
22
22
  const postId = parsePostId(String(kwargs['url-or-id']));
23
23
  const query = `query PostsSingle {
24
24
  post(input: {selector: {documentId: "${gqlEscape(postId)}"}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query Sequences {
15
15
  sequences(input: {terms: {view: "communitySequences", limit: ${limit}}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "shortform", limit: ${limit}}}) {
@@ -19,7 +19,7 @@ cli({
19
19
  { name: 'limit', type: 'int', default: 10, help: 'Number of results' },
20
20
  ],
21
21
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
22
- func: async (_page, kwargs) => {
22
+ func: async (kwargs) => {
23
23
  const tagInput = String(kwargs.tag);
24
24
  const limit = Number(kwargs.limit ?? 10);
25
25
  const tag = await resolveTagId(tagInput);
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 20, help: 'Number of results' }],
11
11
  columns: ['rank', 'name', 'posts'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 20);
14
14
  const query = `query Tags {
15
15
  tags(input: {terms: {view: "coreTags", limit: ${limit}}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "top", after: "${daysAgo(30)}", limit: ${limit}}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "top", after: "${daysAgo(7)}", limit: ${limit}}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "top", after: "${daysAgo(365)}", limit: ${limit}}}) {
@@ -9,7 +9,7 @@ cli({
9
9
  browser: false,
10
10
  args: [{ name: 'limit', type: 'int', default: 10, help: 'Number of results' }],
11
11
  columns: ['rank', 'title', 'author', 'karma', 'comments', 'url'],
12
- func: async (_page, kwargs) => {
12
+ func: async (kwargs) => {
13
13
  const limit = Number(kwargs.limit ?? 10);
14
14
  const query = `query PostsList {
15
15
  posts(input: {terms: {view: "top", limit: ${limit}}}) {
@@ -18,7 +18,7 @@ cli({
18
18
  { name: 'limit', type: 'int', default: 10, help: 'Number of results' },
19
19
  ],
20
20
  columns: ['rank', 'title', 'karma', 'comments', 'date', 'url'],
21
- func: async (_page, kwargs) => {
21
+ func: async (kwargs) => {
22
22
  const username = String(kwargs.username);
23
23
  const limit = Number(kwargs.limit ?? 10);
24
24
  const user = await resolveUserId(username);