@jackwener/opencli 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/.github/workflows/build-extension.yml +2 -6
  2. package/.github/workflows/ci.yml +21 -1
  3. package/README.md +35 -6
  4. package/README.zh-CN.md +12 -5
  5. package/SKILL.md +2 -0
  6. package/dist/browser/cdp.d.ts +2 -1
  7. package/dist/browser/discover.d.ts +4 -1
  8. package/dist/browser/discover.js +6 -2
  9. package/dist/browser/errors.d.ts +2 -2
  10. package/dist/browser/errors.js +4 -12
  11. package/dist/browser/mcp.d.ts +2 -1
  12. package/dist/build-manifest.d.ts +2 -0
  13. package/dist/build-manifest.js +39 -14
  14. package/dist/build-manifest.test.js +21 -0
  15. package/dist/capabilityRouting.d.ts +2 -0
  16. package/dist/capabilityRouting.js +2 -1
  17. package/dist/cli-manifest.json +1111 -112
  18. package/dist/cli.js +34 -3
  19. package/dist/clis/36kr/article.d.ts +1 -0
  20. package/dist/clis/36kr/article.js +62 -0
  21. package/dist/clis/36kr/hot.d.ts +3 -0
  22. package/dist/clis/36kr/hot.js +80 -0
  23. package/dist/clis/36kr/hot.test.d.ts +1 -0
  24. package/dist/clis/36kr/hot.test.js +15 -0
  25. package/dist/clis/36kr/news.d.ts +1 -0
  26. package/dist/clis/36kr/news.js +51 -0
  27. package/dist/clis/36kr/news.test.d.ts +1 -0
  28. package/dist/clis/36kr/news.test.js +85 -0
  29. package/dist/clis/36kr/search.d.ts +1 -0
  30. package/dist/clis/36kr/search.js +72 -0
  31. package/dist/clis/bilibili/comments.d.ts +5 -0
  32. package/dist/clis/bilibili/comments.js +40 -0
  33. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  34. package/dist/clis/bilibili/comments.test.js +82 -0
  35. package/dist/clis/chatgpt/ask.js +29 -14
  36. package/dist/clis/chatgpt/ax.d.ts +6 -0
  37. package/dist/clis/chatgpt/ax.js +172 -1
  38. package/dist/clis/chatgpt/model.d.ts +1 -0
  39. package/dist/clis/chatgpt/model.js +24 -0
  40. package/dist/clis/chatgpt/send.js +12 -3
  41. package/dist/clis/douban/download.d.ts +1 -0
  42. package/dist/clis/douban/download.js +67 -0
  43. package/dist/clis/douban/download.test.d.ts +1 -0
  44. package/dist/clis/douban/download.test.js +170 -0
  45. package/dist/clis/douban/photos.d.ts +1 -0
  46. package/dist/clis/douban/photos.js +34 -0
  47. package/dist/clis/douban/utils.d.ts +25 -0
  48. package/dist/clis/douban/utils.js +190 -1
  49. package/dist/clis/douban/utils.test.d.ts +1 -0
  50. package/dist/clis/douban/utils.test.js +64 -0
  51. package/dist/clis/imdb/person.d.ts +1 -0
  52. package/dist/clis/imdb/person.js +203 -0
  53. package/dist/clis/imdb/reviews.d.ts +1 -0
  54. package/dist/clis/imdb/reviews.js +88 -0
  55. package/dist/clis/imdb/search.d.ts +1 -0
  56. package/dist/clis/imdb/search.js +161 -0
  57. package/dist/clis/imdb/title.d.ts +1 -0
  58. package/dist/clis/imdb/title.js +93 -0
  59. package/dist/clis/imdb/top.d.ts +1 -0
  60. package/dist/clis/imdb/top.js +53 -0
  61. package/dist/clis/imdb/trending.d.ts +1 -0
  62. package/dist/clis/imdb/trending.js +52 -0
  63. package/dist/clis/imdb/utils.d.ts +46 -0
  64. package/dist/clis/imdb/utils.js +285 -0
  65. package/dist/clis/imdb/utils.test.d.ts +1 -0
  66. package/dist/clis/imdb/utils.test.js +88 -0
  67. package/dist/clis/jd/item.d.ts +4 -0
  68. package/dist/clis/jd/item.js +16 -15
  69. package/dist/clis/jd/item.test.js +16 -1
  70. package/dist/clis/linux-do/categories.yaml +38 -9
  71. package/dist/clis/linux-do/category.d.ts +1 -0
  72. package/dist/clis/linux-do/category.js +36 -0
  73. package/dist/clis/linux-do/feed.d.ts +45 -0
  74. package/dist/clis/linux-do/feed.js +397 -0
  75. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  76. package/dist/clis/linux-do/feed.test.js +118 -0
  77. package/dist/clis/linux-do/hot.d.ts +1 -0
  78. package/dist/clis/linux-do/hot.js +25 -0
  79. package/dist/clis/linux-do/latest.d.ts +1 -0
  80. package/dist/clis/linux-do/latest.js +18 -0
  81. package/dist/clis/linux-do/tags.yaml +41 -0
  82. package/dist/clis/linux-do/topic.yaml +41 -3
  83. package/dist/clis/linux-do/user-posts.yaml +67 -0
  84. package/dist/clis/linux-do/user-topics.yaml +54 -0
  85. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  86. package/dist/clis/paperreview/commands.test.js +243 -0
  87. package/dist/clis/paperreview/feedback.d.ts +1 -0
  88. package/dist/clis/paperreview/feedback.js +52 -0
  89. package/dist/clis/paperreview/review.d.ts +1 -0
  90. package/dist/clis/paperreview/review.js +37 -0
  91. package/dist/clis/paperreview/submit.d.ts +1 -0
  92. package/dist/clis/paperreview/submit.js +85 -0
  93. package/dist/clis/paperreview/utils.d.ts +46 -0
  94. package/dist/clis/paperreview/utils.js +197 -0
  95. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  96. package/dist/clis/paperreview/utils.test.js +49 -0
  97. package/dist/clis/producthunt/browse.d.ts +1 -0
  98. package/dist/clis/producthunt/browse.js +99 -0
  99. package/dist/clis/producthunt/hot.d.ts +1 -0
  100. package/dist/clis/producthunt/hot.js +110 -0
  101. package/dist/clis/producthunt/posts.d.ts +1 -0
  102. package/dist/clis/producthunt/posts.js +28 -0
  103. package/dist/clis/producthunt/today.d.ts +1 -0
  104. package/dist/clis/producthunt/today.js +35 -0
  105. package/dist/clis/producthunt/utils.d.ts +29 -0
  106. package/dist/clis/producthunt/utils.js +99 -0
  107. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  108. package/dist/clis/producthunt/utils.test.js +64 -0
  109. package/dist/clis/twitter/article.js +4 -28
  110. package/dist/clis/twitter/likes.d.ts +24 -0
  111. package/dist/clis/twitter/likes.js +217 -0
  112. package/dist/clis/twitter/likes.test.d.ts +1 -0
  113. package/dist/clis/twitter/likes.test.js +85 -0
  114. package/dist/clis/twitter/profile.js +4 -28
  115. package/dist/clis/twitter/search.js +2 -1
  116. package/dist/clis/twitter/search.test.js +2 -0
  117. package/dist/clis/twitter/shared.d.ts +6 -0
  118. package/dist/clis/twitter/shared.js +35 -0
  119. package/dist/clis/twitter/timeline.js +2 -13
  120. package/dist/clis/weixin/download.d.ts +17 -0
  121. package/dist/clis/weixin/download.js +88 -20
  122. package/dist/clis/weread/book.js +2 -2
  123. package/dist/clis/weread/commands.test.d.ts +3 -0
  124. package/dist/clis/weread/commands.test.js +43 -0
  125. package/dist/clis/weread/highlights.js +2 -2
  126. package/dist/clis/weread/notebooks.js +2 -2
  127. package/dist/clis/weread/notes.js +3 -3
  128. package/dist/clis/weread/shelf.js +2 -2
  129. package/dist/clis/weread/utils.d.ts +4 -4
  130. package/dist/clis/weread/utils.js +32 -14
  131. package/dist/clis/weread/utils.test.js +1 -28
  132. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  133. package/dist/clis/xiaohongshu/comments.js +74 -0
  134. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  135. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  136. package/dist/clis/xiaohongshu/publish.js +114 -18
  137. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  138. package/dist/clis/xiaohongshu/publish.test.js +119 -0
  139. package/dist/commanderAdapter.d.ts +1 -0
  140. package/dist/commanderAdapter.js +176 -29
  141. package/dist/commanderAdapter.test.d.ts +1 -0
  142. package/dist/commanderAdapter.test.js +62 -0
  143. package/dist/daemon.js +17 -1
  144. package/dist/discovery.js +8 -14
  145. package/dist/doctor.d.ts +1 -0
  146. package/dist/doctor.js +9 -2
  147. package/dist/download/index.js +63 -51
  148. package/dist/download/index.test.js +17 -4
  149. package/dist/errors.d.ts +3 -1
  150. package/dist/errors.js +15 -32
  151. package/dist/execution.d.ts +1 -3
  152. package/dist/execution.js +21 -1
  153. package/dist/hooks.js +2 -0
  154. package/dist/main.js +5 -0
  155. package/dist/output.js +5 -1
  156. package/dist/pipeline/executor.js +3 -4
  157. package/dist/plugin-manifest.d.ts +70 -0
  158. package/dist/plugin-manifest.js +160 -0
  159. package/dist/plugin-manifest.test.d.ts +4 -0
  160. package/dist/plugin-manifest.test.js +179 -0
  161. package/dist/plugin.d.ts +38 -5
  162. package/dist/plugin.js +267 -33
  163. package/dist/plugin.test.js +220 -3
  164. package/dist/registry.d.ts +4 -0
  165. package/dist/registry.js +2 -0
  166. package/dist/runtime-detect.d.ts +21 -0
  167. package/dist/runtime-detect.js +32 -0
  168. package/dist/runtime-detect.test.d.ts +1 -0
  169. package/dist/runtime-detect.test.js +27 -0
  170. package/dist/runtime.js +1 -1
  171. package/dist/serialization.d.ts +2 -0
  172. package/dist/serialization.js +6 -0
  173. package/dist/types.d.ts +1 -0
  174. package/dist/update-check.d.ts +22 -0
  175. package/dist/update-check.js +112 -0
  176. package/dist/weixin-download.test.d.ts +1 -0
  177. package/dist/weixin-download.test.js +30 -0
  178. package/dist/weread-private-api-regression.test.d.ts +1 -0
  179. package/dist/weread-private-api-regression.test.js +122 -0
  180. package/dist/yaml-schema.d.ts +3 -0
  181. package/dist/yaml-schema.js +18 -1
  182. package/docs/.vitepress/config.mts +4 -0
  183. package/docs/adapters/browser/36kr.md +47 -0
  184. package/docs/adapters/browser/douban.md +14 -0
  185. package/docs/adapters/browser/imdb.md +47 -0
  186. package/docs/adapters/browser/jd.md +2 -2
  187. package/docs/adapters/browser/linux-do.md +181 -20
  188. package/docs/adapters/browser/paperreview.md +43 -0
  189. package/docs/adapters/browser/producthunt.md +49 -0
  190. package/docs/adapters/desktop/chatgpt.md +5 -0
  191. package/docs/adapters/index.md +6 -2
  192. package/docs/advanced/download.md +4 -0
  193. package/docs/advanced/rate-limiter-plugin.md +99 -0
  194. package/docs/guide/electron-app-cli.md +200 -0
  195. package/docs/guide/getting-started.md +1 -0
  196. package/docs/guide/plugins.md +87 -0
  197. package/docs/zh/guide/electron-app-cli.md +188 -0
  198. package/docs/zh/guide/getting-started.md +1 -0
  199. package/docs/zh/guide/plugins.md +65 -0
  200. package/extension/package.json +1 -0
  201. package/extension/scripts/package-release.mjs +179 -0
  202. package/extension/src/background.ts +2 -0
  203. package/package.json +4 -1
  204. package/scripts/postinstall.js +10 -0
  205. package/src/browser/cdp.ts +2 -1
  206. package/src/browser/discover.ts +8 -3
  207. package/src/browser/errors.ts +13 -14
  208. package/src/browser/mcp.ts +2 -1
  209. package/src/build-manifest.test.ts +23 -0
  210. package/src/build-manifest.ts +40 -15
  211. package/src/capabilityRouting.ts +2 -1
  212. package/src/cli.ts +35 -3
  213. package/src/clis/36kr/article.ts +69 -0
  214. package/src/clis/36kr/hot.test.ts +19 -0
  215. package/src/clis/36kr/hot.ts +100 -0
  216. package/src/clis/36kr/news.test.ts +90 -0
  217. package/src/clis/36kr/news.ts +54 -0
  218. package/src/clis/36kr/search.ts +78 -0
  219. package/src/clis/bilibili/comments.test.ts +102 -0
  220. package/src/clis/bilibili/comments.ts +44 -0
  221. package/src/clis/chatgpt/ask.ts +28 -14
  222. package/src/clis/chatgpt/ax.ts +180 -1
  223. package/src/clis/chatgpt/model.ts +27 -0
  224. package/src/clis/chatgpt/send.ts +16 -6
  225. package/src/clis/douban/download.test.ts +196 -0
  226. package/src/clis/douban/download.ts +78 -0
  227. package/src/clis/douban/photos.ts +36 -0
  228. package/src/clis/douban/utils.test.ts +97 -0
  229. package/src/clis/douban/utils.ts +232 -1
  230. package/src/clis/imdb/person.ts +232 -0
  231. package/src/clis/imdb/reviews.ts +111 -0
  232. package/src/clis/imdb/search.ts +179 -0
  233. package/src/clis/imdb/title.ts +121 -0
  234. package/src/clis/imdb/top.ts +67 -0
  235. package/src/clis/imdb/trending.ts +66 -0
  236. package/src/clis/imdb/utils.test.ts +117 -0
  237. package/src/clis/imdb/utils.ts +305 -0
  238. package/src/clis/jd/item.test.ts +18 -1
  239. package/src/clis/jd/item.ts +18 -15
  240. package/src/clis/linux-do/categories.yaml +38 -9
  241. package/src/clis/linux-do/category.ts +37 -0
  242. package/src/clis/linux-do/feed.test.ts +132 -0
  243. package/src/clis/linux-do/feed.ts +501 -0
  244. package/src/clis/linux-do/hot.ts +26 -0
  245. package/src/clis/linux-do/latest.ts +19 -0
  246. package/src/clis/linux-do/tags.yaml +41 -0
  247. package/src/clis/linux-do/topic.yaml +41 -3
  248. package/src/clis/linux-do/user-posts.yaml +67 -0
  249. package/src/clis/linux-do/user-topics.yaml +54 -0
  250. package/src/clis/paperreview/commands.test.ts +283 -0
  251. package/src/clis/paperreview/feedback.ts +64 -0
  252. package/src/clis/paperreview/review.ts +47 -0
  253. package/src/clis/paperreview/submit.ts +119 -0
  254. package/src/clis/paperreview/utils.test.ts +68 -0
  255. package/src/clis/paperreview/utils.ts +276 -0
  256. package/src/clis/producthunt/browse.ts +109 -0
  257. package/src/clis/producthunt/hot.ts +127 -0
  258. package/src/clis/producthunt/posts.ts +29 -0
  259. package/src/clis/producthunt/today.ts +37 -0
  260. package/src/clis/producthunt/utils.test.ts +72 -0
  261. package/src/clis/producthunt/utils.ts +122 -0
  262. package/src/clis/twitter/article.ts +5 -28
  263. package/src/clis/twitter/likes.test.ts +91 -0
  264. package/src/clis/twitter/likes.ts +256 -0
  265. package/src/clis/twitter/profile.ts +5 -28
  266. package/src/clis/twitter/search.test.ts +2 -0
  267. package/src/clis/twitter/search.ts +3 -1
  268. package/src/clis/twitter/shared.ts +45 -0
  269. package/src/clis/twitter/timeline.ts +2 -13
  270. package/src/clis/weixin/download.ts +114 -20
  271. package/src/clis/weread/book.ts +2 -2
  272. package/src/clis/weread/commands.test.ts +57 -0
  273. package/src/clis/weread/highlights.ts +2 -2
  274. package/src/clis/weread/notebooks.ts +2 -2
  275. package/src/clis/weread/notes.ts +3 -3
  276. package/src/clis/weread/shelf.ts +2 -2
  277. package/src/clis/weread/utils.test.ts +1 -32
  278. package/src/clis/weread/utils.ts +41 -16
  279. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  280. package/src/clis/xiaohongshu/comments.ts +81 -0
  281. package/src/clis/xiaohongshu/publish.test.ts +137 -0
  282. package/src/clis/xiaohongshu/publish.ts +129 -18
  283. package/src/commanderAdapter.test.ts +78 -0
  284. package/src/commanderAdapter.ts +188 -24
  285. package/src/daemon.ts +19 -1
  286. package/src/discovery.ts +8 -15
  287. package/src/doctor.ts +13 -2
  288. package/src/download/index.test.ts +14 -4
  289. package/src/download/index.ts +67 -55
  290. package/src/errors.ts +25 -66
  291. package/src/execution.ts +28 -3
  292. package/src/hooks.ts +1 -0
  293. package/src/main.ts +6 -0
  294. package/src/output.ts +3 -1
  295. package/src/pipeline/executor.ts +4 -6
  296. package/src/plugin-manifest.test.ts +223 -0
  297. package/src/plugin-manifest.ts +206 -0
  298. package/src/plugin.test.ts +246 -2
  299. package/src/plugin.ts +338 -36
  300. package/src/registry.ts +6 -1
  301. package/src/runtime-detect.test.ts +30 -0
  302. package/src/runtime-detect.ts +36 -0
  303. package/src/runtime.ts +1 -1
  304. package/src/serialization.ts +4 -0
  305. package/src/types.ts +1 -0
  306. package/src/update-check.ts +114 -0
  307. package/src/weixin-download.test.ts +64 -0
  308. package/src/weread-private-api-regression.test.ts +150 -0
  309. package/src/yaml-schema.ts +20 -0
  310. package/tests/e2e/browser-auth.test.ts +13 -9
  311. package/tests/e2e/browser-public-extended.test.ts +1 -1
  312. package/tests/e2e/browser-public.test.ts +62 -4
  313. package/tests/e2e/helpers.ts +2 -1
  314. package/tests/e2e/public-commands.test.ts +37 -3
  315. package/tests/smoke/api-health.test.ts +1 -1
  316. package/vitest.config.ts +10 -0
  317. package/dist/clis/linux-do/category.yaml +0 -51
  318. package/dist/clis/linux-do/hot.yaml +0 -50
  319. package/dist/clis/linux-do/latest.yaml +0 -40
  320. package/src/clis/linux-do/category.yaml +0 -51
  321. package/src/clis/linux-do/hot.yaml +0 -50
  322. package/src/clis/linux-do/latest.yaml +0 -40
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Douban adapter utilities.
3
3
  */
4
- import { CliError } from '../../errors.js';
4
+ import { ArgumentError, CliError, EmptyResultError } from '../../errors.js';
5
+ const DOUBAN_PHOTO_PAGE_SIZE = 30;
6
+ const MAX_DOUBAN_PHOTOS = 500;
5
7
  function clampLimit(limit) {
6
8
  return Math.max(1, Math.min(limit || 20, 50));
7
9
  }
10
+ function clampPhotoLimit(limit) {
11
+ return Math.max(1, Math.min(limit || 120, MAX_DOUBAN_PHOTOS));
12
+ }
8
13
  async function ensureDoubanReady(page) {
9
14
  const state = await page.evaluate(`
10
15
  (() => {
@@ -18,6 +23,190 @@ async function ensureDoubanReady(page) {
18
23
  throw new CliError('AUTH_REQUIRED', 'Douban requires a logged-in browser session before these commands can load data.', 'Please sign in to douban.com in the browser that opencli reuses, then rerun the command.');
19
24
  }
20
25
  }
26
+ export function normalizeDoubanSubjectId(subjectId) {
27
+ const normalized = String(subjectId || '').trim();
28
+ if (!/^\d+$/.test(normalized)) {
29
+ throw new ArgumentError(`Invalid Douban subject ID: ${subjectId}`);
30
+ }
31
+ return normalized;
32
+ }
33
+ export function promoteDoubanPhotoUrl(url, size = 'l') {
34
+ const normalized = String(url || '').trim();
35
+ if (!normalized)
36
+ return '';
37
+ if (/^[a-z]+:/i.test(normalized) && !/^https?:/i.test(normalized))
38
+ return '';
39
+ return normalized.replace(/\/view\/photo\/[^/]+\/public\//, `/view/photo/${size}/public/`);
40
+ }
41
+ export function resolveDoubanPhotoAssetUrl(candidates, baseUrl = '') {
42
+ for (const candidate of candidates) {
43
+ const normalized = String(candidate || '').trim();
44
+ if (!normalized)
45
+ continue;
46
+ let resolved = normalized;
47
+ try {
48
+ resolved = baseUrl
49
+ ? new URL(normalized, baseUrl).toString()
50
+ : new URL(normalized).toString();
51
+ }
52
+ catch {
53
+ resolved = normalized;
54
+ }
55
+ if (/^https?:\/\//i.test(resolved)) {
56
+ return resolved;
57
+ }
58
+ }
59
+ return '';
60
+ }
61
+ export function getDoubanPhotoExtension(url) {
62
+ const normalized = String(url || '').trim();
63
+ if (!normalized)
64
+ return '.jpg';
65
+ try {
66
+ const ext = new URL(normalized).pathname.match(/\.(jpe?g|png|gif|webp|avif|bmp)$/i)?.[0];
67
+ return ext || '.jpg';
68
+ }
69
+ catch {
70
+ const ext = normalized.match(/\.(jpe?g|png|gif|webp|avif|bmp)(?:$|[?#])/i)?.[0];
71
+ return ext ? ext.replace(/[?#].*$/, '') : '.jpg';
72
+ }
73
+ }
74
+ export async function loadDoubanSubjectPhotos(page, subjectId, options = {}) {
75
+ const normalizedId = normalizeDoubanSubjectId(subjectId);
76
+ const type = String(options.type || 'Rb').trim() || 'Rb';
77
+ const targetPhotoId = String(options.targetPhotoId || '').trim();
78
+ const safeLimit = targetPhotoId ? Number.MAX_SAFE_INTEGER : clampPhotoLimit(Number(options.limit) || 120);
79
+ const resolvePhotoAssetUrlSource = resolveDoubanPhotoAssetUrl.toString();
80
+ const galleryUrl = `https://movie.douban.com/subject/${normalizedId}/photos?type=${encodeURIComponent(type)}`;
81
+ await page.goto(galleryUrl);
82
+ await page.wait(2);
83
+ await ensureDoubanReady(page);
84
+ const data = await page.evaluate(`
85
+ (async () => {
86
+ const subjectId = ${JSON.stringify(normalizedId)};
87
+ const type = ${JSON.stringify(type)};
88
+ const limit = ${safeLimit};
89
+ const targetPhotoId = ${JSON.stringify(targetPhotoId)};
90
+ const pageSize = ${DOUBAN_PHOTO_PAGE_SIZE};
91
+ const resolveDoubanPhotoAssetUrl = ${resolvePhotoAssetUrlSource};
92
+
93
+ const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
94
+ const toAbsoluteUrl = (value) => {
95
+ if (!value) return '';
96
+ try {
97
+ return new URL(value, location.origin).toString();
98
+ } catch {
99
+ return value;
100
+ }
101
+ };
102
+ const promotePhotoUrl = (value) => {
103
+ const absolute = toAbsoluteUrl(value);
104
+ if (!absolute) return '';
105
+ if (/^[a-z]+:/i.test(absolute) && !/^https?:/i.test(absolute)) return '';
106
+ return absolute.replace(/\\/view\\/photo\\/[^/]+\\/public\\//, '/view/photo/l/public/');
107
+ };
108
+ const buildPageUrl = (start) => {
109
+ const url = new URL(location.href);
110
+ url.searchParams.set('type', type);
111
+ if (start > 0) url.searchParams.set('start', String(start));
112
+ else url.searchParams.delete('start');
113
+ return url.toString();
114
+ };
115
+ const getTitle = (doc) => {
116
+ const raw = normalize(doc.querySelector('#content h1')?.textContent)
117
+ || normalize(doc.querySelector('title')?.textContent);
118
+ return raw.replace(/\\s*\\(豆瓣\\)\\s*$/, '');
119
+ };
120
+ const extractPhotos = (doc, pageNumber) => {
121
+ const nodes = Array.from(doc.querySelectorAll('.poster-col3 li, .poster-col3l li, .article li'));
122
+ const rows = [];
123
+ for (const node of nodes) {
124
+ const link = node.querySelector('a[href*="/photos/photo/"]');
125
+ const img = node.querySelector('img');
126
+ if (!link || !img) continue;
127
+
128
+ const detailUrl = toAbsoluteUrl(link.getAttribute('href') || '');
129
+ const photoId = detailUrl.match(/\\/photo\\/(\\d+)/)?.[1] || '';
130
+ const thumbUrl = resolveDoubanPhotoAssetUrl([
131
+ img.getAttribute('data-origin'),
132
+ img.getAttribute('data-src'),
133
+ img.getAttribute('src'),
134
+ ], location.href);
135
+ const imageUrl = promotePhotoUrl(thumbUrl);
136
+ const title = normalize(link.getAttribute('title'))
137
+ || normalize(img.getAttribute('alt'))
138
+ || (photoId ? 'photo_' + photoId : 'photo_' + String(rows.length + 1));
139
+
140
+ if (!detailUrl || !thumbUrl || !imageUrl) continue;
141
+
142
+ rows.push({
143
+ photoId,
144
+ title,
145
+ imageUrl,
146
+ thumbUrl,
147
+ detailUrl,
148
+ page: pageNumber,
149
+ });
150
+ }
151
+ return rows;
152
+ };
153
+
154
+ const subjectTitle = getTitle(document);
155
+ const seen = new Set();
156
+ const photos = [];
157
+
158
+ for (let pageIndex = 0; photos.length < limit; pageIndex += 1) {
159
+ let doc = document;
160
+ if (pageIndex > 0) {
161
+ const response = await fetch(buildPageUrl(pageIndex * pageSize), { credentials: 'include' });
162
+ if (!response.ok) break;
163
+ const html = await response.text();
164
+ doc = new DOMParser().parseFromString(html, 'text/html');
165
+ }
166
+
167
+ const pagePhotos = extractPhotos(doc, pageIndex + 1);
168
+ if (!pagePhotos.length) break;
169
+
170
+ let appended = 0;
171
+ let foundTarget = false;
172
+ for (const photo of pagePhotos) {
173
+ const key = photo.photoId || photo.detailUrl || photo.imageUrl;
174
+ if (seen.has(key)) continue;
175
+ seen.add(key);
176
+ photos.push({
177
+ index: photos.length + 1,
178
+ ...photo,
179
+ });
180
+ appended += 1;
181
+ if (targetPhotoId && photo.photoId === targetPhotoId) {
182
+ foundTarget = true;
183
+ break;
184
+ }
185
+ if (photos.length >= limit) break;
186
+ }
187
+
188
+ if (foundTarget || pagePhotos.length < pageSize || appended === 0) break;
189
+ }
190
+
191
+ return {
192
+ subjectId,
193
+ subjectTitle,
194
+ type,
195
+ photos,
196
+ };
197
+ })()
198
+ `);
199
+ const photos = Array.isArray(data?.photos) ? data.photos : [];
200
+ if (!photos.length) {
201
+ throw new EmptyResultError('douban photos', 'No photos found. Try a different subject ID or a different --type value such as Rb.');
202
+ }
203
+ return {
204
+ subjectId: normalizedId,
205
+ subjectTitle: String(data?.subjectTitle || '').trim(),
206
+ type,
207
+ photos,
208
+ };
209
+ }
21
210
  export async function loadDoubanBookHot(page, limit) {
22
211
  const safeLimit = clampLimit(limit);
23
212
  await page.goto('https://book.douban.com/chart');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getDoubanPhotoExtension, loadDoubanSubjectPhotos, normalizeDoubanSubjectId, promoteDoubanPhotoUrl, resolveDoubanPhotoAssetUrl, } from './utils.js';
3
+ describe('douban utils', () => {
4
+ it('normalizes valid subject ids', () => {
5
+ expect(normalizeDoubanSubjectId(' 30382501 ')).toBe('30382501');
6
+ });
7
+ it('rejects invalid subject ids', () => {
8
+ expect(() => normalizeDoubanSubjectId('tt30382501')).toThrow('Invalid Douban subject ID');
9
+ });
10
+ it('promotes thumbnail urls to large photo urls', () => {
11
+ expect(promoteDoubanPhotoUrl('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp')).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp');
12
+ expect(promoteDoubanPhotoUrl('https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2578474613.jpg')).toBe('https://img9.doubanio.com/view/photo/l/public/p2578474613.jpg');
13
+ });
14
+ it('rejects non-http photo urls during promotion', () => {
15
+ expect(promoteDoubanPhotoUrl('data:image/gif;base64,abc')).toBe('');
16
+ });
17
+ it('prefers lazy-loaded photo urls over data placeholders', () => {
18
+ expect(resolveDoubanPhotoAssetUrl([
19
+ '',
20
+ 'https://img1.doubanio.com/view/photo/m/public/p2913450214.webp',
21
+ 'data:image/gif;base64,abc',
22
+ ], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp');
23
+ });
24
+ it('drops unsupported non-http photo urls when no real image url exists', () => {
25
+ expect(resolveDoubanPhotoAssetUrl(['data:image/gif;base64,abc', 'blob:https://movie.douban.com/example'], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('');
26
+ });
27
+ it('removes the default photo cap when scanning for an exact photo id', async () => {
28
+ const evaluate = vi.fn()
29
+ .mockResolvedValueOnce({ blocked: false, title: 'Some Movie', href: 'https://movie.douban.com/subject/30382501/photos?type=Rb' })
30
+ .mockResolvedValueOnce({
31
+ subjectId: '30382501',
32
+ subjectTitle: 'The Wandering Earth 2',
33
+ type: 'Rb',
34
+ photos: [
35
+ {
36
+ index: 731,
37
+ photoId: '2913450215',
38
+ title: 'Character poster',
39
+ imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
40
+ thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
41
+ detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
42
+ page: 25,
43
+ },
44
+ ],
45
+ });
46
+ const page = {
47
+ goto: vi.fn().mockResolvedValue(undefined),
48
+ wait: vi.fn().mockResolvedValue(undefined),
49
+ evaluate,
50
+ };
51
+ await loadDoubanSubjectPhotos(page, '30382501', {
52
+ type: 'Rb',
53
+ targetPhotoId: '2913450215',
54
+ });
55
+ const scanScript = evaluate.mock.calls[1]?.[0];
56
+ expect(scanScript).toContain('const targetPhotoId = "2913450215";');
57
+ expect(scanScript).toContain(`const limit = ${Number.MAX_SAFE_INTEGER};`);
58
+ expect(scanScript).toContain('for (let pageIndex = 0; photos.length < limit; pageIndex += 1)');
59
+ });
60
+ it('keeps image extensions when download urls contain query params', () => {
61
+ expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp?foo=1')).toBe('.webp');
62
+ expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.jpeg')).toBe('.jpeg');
63
+ });
64
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,203 @@
1
+ import { CommandExecutionError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import { forceEnglishUrl, getCurrentImdbId, isChallengePage, normalizeImdbId, waitForImdbPath, } from './utils.js';
4
+ /**
5
+ * Read IMDb person details from public profile pages.
6
+ */
7
+ cli({
8
+ site: 'imdb',
9
+ name: 'person',
10
+ description: 'Get actor or director info',
11
+ domain: 'www.imdb.com',
12
+ strategy: Strategy.PUBLIC,
13
+ browser: true,
14
+ args: [
15
+ { name: 'id', positional: true, required: true, help: 'IMDb person ID (nm0634240) or URL' },
16
+ { name: 'limit', type: 'int', default: 10, help: 'Max filmography entries' },
17
+ ],
18
+ columns: ['field', 'value'],
19
+ func: async (page, args) => {
20
+ const id = normalizeImdbId(String(args.id), 'nm');
21
+ // Clamp to 30 to match the internal evaluate cap
22
+ const limit = Math.max(1, Math.min(Number(args.limit) || 10, 30));
23
+ const url = forceEnglishUrl(`https://www.imdb.com/name/${id}/`);
24
+ await page.goto(url);
25
+ const onPersonPage = await waitForImdbPath(page, `^/name/${id}/`);
26
+ if (await isChallengePage(page)) {
27
+ throw new CommandExecutionError('IMDb blocked this request', 'Try again with a normal browser session or extension mode');
28
+ }
29
+ if (!onPersonPage) {
30
+ throw new CommandExecutionError(`Person page did not finish loading: ${id}`, 'Retry the command; if it persists, IMDb may have changed their navigation flow');
31
+ }
32
+ const currentId = await getCurrentImdbId(page, 'nm');
33
+ if (currentId && currentId !== id) {
34
+ throw new CommandExecutionError(`IMDb redirected to a different person: ${currentId}`, 'Retry the command; if it persists, the person page may have changed');
35
+ }
36
+ const data = await page.evaluate(`
37
+ (function() {
38
+ var result = {
39
+ nameId: '',
40
+ name: '',
41
+ description: '',
42
+ birthDate: '',
43
+ filmography: []
44
+ };
45
+
46
+ var scripts = document.querySelectorAll('script[type="application/ld+json"]');
47
+ for (var i = 0; i < scripts.length; i++) {
48
+ try {
49
+ var ld = JSON.parse(scripts[i].textContent || 'null');
50
+ if (ld && ld['@type'] === 'Person') {
51
+ if (typeof ld.url === 'string') {
52
+ var ldMatch = ld.url.match(/(nm\\d{7,8})/);
53
+ if (ldMatch) {
54
+ result.nameId = ldMatch[1];
55
+ }
56
+ }
57
+ result.name = result.name || ld.name || '';
58
+ result.description = result.description || ld.description || '';
59
+ break;
60
+ }
61
+ } catch (error) {
62
+ void error;
63
+ }
64
+ }
65
+
66
+ var nextDataEl = document.getElementById('__NEXT_DATA__');
67
+ if (!nextDataEl) {
68
+ return result;
69
+ }
70
+
71
+ try {
72
+ var nextData = JSON.parse(nextDataEl.textContent || 'null');
73
+ var pageProps = nextData && nextData.props && nextData.props.pageProps;
74
+ var above = pageProps && (pageProps.aboveTheFold || pageProps.aboveTheFoldData);
75
+ var main = pageProps && (pageProps.mainColumnData || pageProps.belowTheFold);
76
+
77
+ if (above) {
78
+ if (!result.nameId && above.id) {
79
+ result.nameId = String(above.id);
80
+ }
81
+ if (!result.name && above.nameText && above.nameText.text) {
82
+ result.name = above.nameText.text;
83
+ }
84
+
85
+ if (above.birthDate) {
86
+ if (above.birthDate.displayableProperty && above.birthDate.displayableProperty.value) {
87
+ result.birthDate = above.birthDate.displayableProperty.value.plainText || '';
88
+ }
89
+ if (!result.birthDate && above.birthDate.dateComponents) {
90
+ var dc = above.birthDate.dateComponents;
91
+ result.birthDate = [dc.year, dc.month, dc.day].filter(Boolean).join('-');
92
+ }
93
+ }
94
+
95
+ if (above.bio && above.bio.text && above.bio.text.plainText) {
96
+ result.description = above.bio.text.plainText.substring(0, 300);
97
+ }
98
+ }
99
+
100
+ var pushFilmography = function(title, year, role) {
101
+ if (!title) {
102
+ return;
103
+ }
104
+ result.filmography.push({
105
+ title: title,
106
+ year: year || '',
107
+ role: role || ''
108
+ });
109
+ };
110
+
111
+ var knownFor = main && main.knownForFeatureV2;
112
+ if (knownFor && Array.isArray(knownFor.credits)) {
113
+ for (var j = 0; j < knownFor.credits.length; j++) {
114
+ var knownNode = knownFor.credits[j];
115
+ if (!knownNode || !knownNode.title) {
116
+ continue;
117
+ }
118
+ var knownRole = '';
119
+ var knownRoleEdge = knownNode.creditedRoles && Array.isArray(knownNode.creditedRoles.edges)
120
+ ? knownNode.creditedRoles.edges[0]
121
+ : null;
122
+ if (knownRoleEdge && knownRoleEdge.node) {
123
+ knownRole = knownRoleEdge.node.text
124
+ || (knownRoleEdge.node.category ? knownRoleEdge.node.category.text || '' : '');
125
+ }
126
+ pushFilmography(
127
+ knownNode.title.titleText ? knownNode.title.titleText.text : '',
128
+ knownNode.title.releaseYear ? String(knownNode.title.releaseYear.year || '') : '',
129
+ knownRole
130
+ );
131
+ }
132
+ }
133
+
134
+ if (result.filmography.length === 0) {
135
+ var creditSources = [];
136
+ if (main && main.released && Array.isArray(main.released.edges)) {
137
+ creditSources.push(main.released.edges);
138
+ }
139
+ if (main && main.groupings && Array.isArray(main.groupings.edges)) {
140
+ creditSources.push(main.groupings.edges);
141
+ }
142
+
143
+ for (var k = 0; k < creditSources.length && result.filmography.length < 30; k++) {
144
+ var groups = creditSources[k];
145
+ for (var m = 0; m < groups.length && result.filmography.length < 30; m++) {
146
+ var groupNode = groups[m] && groups[m].node;
147
+ if (!groupNode) {
148
+ continue;
149
+ }
150
+
151
+ var roleName = groupNode.grouping ? groupNode.grouping.text || '' : '';
152
+ var credits = groupNode.credits && Array.isArray(groupNode.credits.edges)
153
+ ? groupNode.credits.edges
154
+ : [];
155
+ for (var n = 0; n < credits.length && result.filmography.length < 30; n++) {
156
+ var creditNode = credits[n] && credits[n].node;
157
+ if (!creditNode || !creditNode.title) {
158
+ continue;
159
+ }
160
+ pushFilmography(
161
+ creditNode.title.titleText ? creditNode.title.titleText.text : (creditNode.title.originalTitleText ? creditNode.title.originalTitleText.text : ''),
162
+ creditNode.title.releaseYear ? String(creditNode.title.releaseYear.year || '') : '',
163
+ roleName
164
+ );
165
+ }
166
+ }
167
+ }
168
+ }
169
+ } catch (error) {
170
+ void error;
171
+ }
172
+
173
+ return result;
174
+ })()
175
+ `);
176
+ if (!data || typeof data !== 'object' || !('name' in data) || !data.name) {
177
+ throw new CommandExecutionError(`Person not found: ${id}`, 'Check the person ID and try again');
178
+ }
179
+ const result = data;
180
+ if (result.nameId && result.nameId !== id) {
181
+ throw new CommandExecutionError(`IMDb returned a different person payload: ${result.nameId}`, 'Retry the command; if it persists, the person parser may need updating');
182
+ }
183
+ const filmography = Array.isArray(result.filmography) ? result.filmography : [];
184
+ // Override url with a clean canonical URL (no query params like ?language=en-US)
185
+ result.url = `https://www.imdb.com/name/${id}/`;
186
+ const rows = Object.entries(result)
187
+ .filter(([field, value]) => field !== 'filmography' && field !== 'nameId' && value !== '' && value != null)
188
+ .map(([field, value]) => ({ field, value: String(value) }));
189
+ if (filmography.length > 0) {
190
+ rows.push({ field: 'filmography', value: '' });
191
+ for (const entry of filmography.slice(0, limit)) {
192
+ const suffix = [entry.year ? `(${entry.year})` : '', entry.role ? `[${entry.role}]` : '']
193
+ .filter(Boolean)
194
+ .join(' ');
195
+ rows.push({
196
+ field: String(entry.title || ''),
197
+ value: suffix,
198
+ });
199
+ }
200
+ }
201
+ return rows;
202
+ },
203
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { CommandExecutionError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import { forceEnglishUrl, getCurrentImdbId, isChallengePage, normalizeImdbId, waitForImdbPath, waitForImdbReviewsReady, } from './utils.js';
4
+ /**
5
+ * Read IMDb user reviews from the first review page.
6
+ */
7
+ cli({
8
+ site: 'imdb',
9
+ name: 'reviews',
10
+ description: 'Get user reviews for a movie or TV show',
11
+ domain: 'www.imdb.com',
12
+ strategy: Strategy.PUBLIC,
13
+ browser: true,
14
+ args: [
15
+ { name: 'id', positional: true, required: true, help: 'IMDb title ID (tt1375666) or URL' },
16
+ { name: 'limit', type: 'int', default: 10, help: 'Number of reviews' },
17
+ ],
18
+ columns: ['rank', 'title', 'rating', 'author', 'date', 'text'],
19
+ func: async (page, args) => {
20
+ const id = normalizeImdbId(String(args.id), 'tt');
21
+ const limit = Math.max(1, Math.min(Number(args.limit) || 10, 25));
22
+ const url = forceEnglishUrl(`https://www.imdb.com/title/${id}/reviews/`);
23
+ await page.goto(url);
24
+ const onReviewsPage = await waitForImdbPath(page, `^/title/${id}/reviews/?$`);
25
+ const reviewsReady = await waitForImdbReviewsReady(page, 15000);
26
+ if (await isChallengePage(page)) {
27
+ throw new CommandExecutionError('IMDb blocked this request', 'Try again with a normal browser session or extension mode');
28
+ }
29
+ if (!onReviewsPage || !reviewsReady) {
30
+ throw new CommandExecutionError('IMDb reviews did not finish loading', 'Retry the command; if it persists, the review page structure may have changed');
31
+ }
32
+ const currentId = await getCurrentImdbId(page, 'tt');
33
+ if (currentId && currentId !== id) {
34
+ throw new CommandExecutionError(`IMDb redirected to a different title: ${currentId}`, 'Retry the command; if it persists, the review page may have changed');
35
+ }
36
+ const reviews = await page.evaluate(`
37
+ (function() {
38
+ var limit = ${limit};
39
+ var items = [];
40
+ var containers = document.querySelectorAll('article.user-review-item, [data-testid="review-card-parent"], .imdb-user-review, [data-testid="review-card"], .review-container');
41
+
42
+ for (var i = 0; i < containers.length && items.length < limit; i++) {
43
+ var el = containers[i];
44
+ var titleEl = el.querySelector('.title, [data-testid="review-summary"], a.title');
45
+ var ratingEl = el.querySelector('.review-rating .ipc-rating-star--rating, .rating-other-user-rating span:first-child, [data-testid="review-rating"]');
46
+ var authorEl = el.querySelector('.display-name-link a, [data-testid="author-link"], .author-text, a[href*="/user/"]');
47
+ var dateEl = el.querySelector('.review-date, [data-testid="review-date"]');
48
+ var textEl = el.querySelector('.content .text, .content .show-more__control, [data-testid="review-overflow"]');
49
+
50
+ var title = titleEl ? (titleEl.textContent || '').trim() : '';
51
+ var text = textEl ? (textEl.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 200) : '';
52
+
53
+ if (!title && !text) {
54
+ continue;
55
+ }
56
+
57
+ // Deduplicate: IMDb renders both preview and expanded versions of each review
58
+ var isDupe = false;
59
+ for (var d = 0; d < items.length; d++) {
60
+ if (items[d].title === title) { isDupe = true; break; }
61
+ }
62
+ if (isDupe) { continue; }
63
+
64
+ items.push({
65
+ title: title,
66
+ rating: ratingEl ? (ratingEl.textContent || '').trim() : '',
67
+ author: authorEl ? (authorEl.textContent || '').trim() : '',
68
+ date: dateEl ? (dateEl.textContent || '').trim() : '',
69
+ text: text
70
+ });
71
+ }
72
+
73
+ return items;
74
+ })()
75
+ `);
76
+ if (!Array.isArray(reviews)) {
77
+ return [];
78
+ }
79
+ return reviews.map((item, index) => ({
80
+ rank: index + 1,
81
+ title: item.title || '',
82
+ rating: item.rating || '',
83
+ author: item.author || '',
84
+ date: item.date || '',
85
+ text: item.text || '',
86
+ }));
87
+ },
88
+ });
@@ -0,0 +1 @@
1
+ export {};