@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,210 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+
4
+ const { mockApiGet, mockResolveBvid } = vi.hoisted(() => ({
5
+ mockApiGet: vi.fn(),
6
+ mockResolveBvid: vi.fn(),
7
+ }));
8
+
9
+ vi.mock('./utils.js', async (importOriginal) => ({
10
+ ...(await importOriginal()),
11
+ apiGet: mockApiGet,
12
+ resolveBvid: mockResolveBvid,
13
+ }));
14
+
15
+ import { getRegistry } from '@jackwener/opencli/registry';
16
+ import './summary.js';
17
+
18
+ describe('bilibili summary', () => {
19
+ const command = getRegistry().get('bilibili/summary');
20
+ const page = {};
21
+
22
+ beforeEach(() => {
23
+ mockApiGet.mockReset();
24
+ mockResolveBvid.mockReset();
25
+ mockResolveBvid.mockRejectedValue(new Error('short link not found'));
26
+ });
27
+
28
+ function mockView(data = { aid: 114, cid: 222, owner: { mid: 333 } }) {
29
+ mockApiGet.mockResolvedValueOnce({ code: 0, data });
30
+ }
31
+
32
+ function mockConclusion(modelResult) {
33
+ mockApiGet.mockResolvedValueOnce({
34
+ code: 0,
35
+ data: {
36
+ code: 0,
37
+ model_result: modelResult,
38
+ },
39
+ });
40
+ }
41
+
42
+ it('returns the summary plus timestamped outline rows', async () => {
43
+ mockView();
44
+ mockConclusion({
45
+ summary: '整体总结',
46
+ outline: [
47
+ {
48
+ title: '第一节',
49
+ timestamp: 0,
50
+ part_outline: [
51
+ { timestamp: 12, content: '要点A' },
52
+ { timestamp: 3725, content: '要点B' },
53
+ ],
54
+ },
55
+ ],
56
+ });
57
+
58
+ const result = await command.func(page, { bvid: 'BV1xxx' });
59
+
60
+ expect(mockApiGet).toHaveBeenNthCalledWith(1, page, '/x/web-interface/view', { params: { bvid: 'BV1xxx' } });
61
+ expect(mockApiGet).toHaveBeenNthCalledWith(2, page, '/x/web-interface/view/conclusion/get', {
62
+ params: { bvid: 'BV1xxx', cid: 222, up_mid: 333 },
63
+ signed: true,
64
+ });
65
+ expect(result).toEqual([
66
+ { time: '', content: '整体总结' },
67
+ { time: '00:00', content: '# 第一节' },
68
+ { time: '00:12', content: '要点A' },
69
+ { time: '1:02:05', content: '要点B' },
70
+ ]);
71
+ });
72
+
73
+ it('returns just the summary when the video has no outline', async () => {
74
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
75
+ mockConclusion({ summary: '只有总结', outline: [] });
76
+
77
+ await expect(command.func(page, { bvid: 'BV1xxx' })).resolves.toEqual([
78
+ { time: '', content: '只有总结' },
79
+ ]);
80
+ });
81
+
82
+ it('parses model_result when Bilibili returns it as a JSON string', async () => {
83
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
84
+ mockConclusion(JSON.stringify({ summary: '字符串总结', outline: [] }));
85
+
86
+ await expect(command.func(page, { bvid: 'BV1xxx' })).resolves.toEqual([
87
+ { time: '', content: '字符串总结' },
88
+ ]);
89
+ });
90
+
91
+ it('normalizes Bilibili video URLs before calling the APIs', async () => {
92
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
93
+ mockConclusion({ summary: 'URL 总结', outline: [] });
94
+
95
+ await command.func(page, {
96
+ bvid: 'https://www.bilibili.com/video/BV1abc12345/?spm_id_from=333.1007',
97
+ });
98
+
99
+ expect(mockApiGet).toHaveBeenNthCalledWith(1, page, '/x/web-interface/view', { params: { bvid: 'BV1abc12345' } });
100
+ });
101
+
102
+ it('resolves b23.tv short links through the shared resolver', async () => {
103
+ mockResolveBvid.mockResolvedValueOnce('BVshort12345');
104
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
105
+ mockConclusion({ summary: '短链总结', outline: [] });
106
+
107
+ await command.func(page, { bvid: 'https://b23.tv/abc' });
108
+
109
+ expect(mockResolveBvid).toHaveBeenCalledWith('https://b23.tv/abc');
110
+ expect(mockApiGet).toHaveBeenNthCalledWith(1, page, '/x/web-interface/view', { params: { bvid: 'BVshort12345' } });
111
+ });
112
+
113
+ it('rejects invalid inputs before calling Bilibili APIs', async () => {
114
+ const cases = [
115
+ '',
116
+ 'javascript:alert(1)',
117
+ 'https://example.com/video/BV1abc12345',
118
+ 'https://share.note.youdao.com/video/BV1abc12345',
119
+ 'https://www.bilibili.com/read/cv12345',
120
+ ];
121
+
122
+ for (const bvid of cases) {
123
+ await expect(command.func(page, { bvid })).rejects.toBeInstanceOf(ArgumentError);
124
+ }
125
+ expect(mockApiGet).not.toHaveBeenCalled();
126
+ });
127
+
128
+ it('maps unresolved short-code inputs to ArgumentError without calling APIs', async () => {
129
+ await expect(command.func(page, { bvid: 'not-a-bv' })).rejects.toBeInstanceOf(ArgumentError);
130
+
131
+ expect(mockResolveBvid).toHaveBeenCalledWith('not-a-bv');
132
+ expect(mockApiGet).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it('throws EmptyResultError when Bilibili has not generated an AI summary for the video', async () => {
136
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
137
+ mockApiGet.mockResolvedValueOnce({ code: 0, data: { code: 1, model_result: {} } });
138
+
139
+ await expect(command.func(page, { bvid: 'BV1xxx' })).rejects.toBeInstanceOf(EmptyResultError);
140
+ });
141
+
142
+ it('throws CommandExecutionError when the view payload is malformed', async () => {
143
+ mockApiGet.mockResolvedValueOnce({ code: 0, data: {} });
144
+
145
+ await expect(command.func(page, { bvid: 'BVbroken' })).rejects.toSatisfy(
146
+ (err) => err instanceof CommandExecutionError && /cid\/up_mid/.test(err.message),
147
+ );
148
+ });
149
+
150
+ it('throws CommandExecutionError when the view API returns a non-auth error', async () => {
151
+ mockApiGet.mockResolvedValueOnce({ code: -404, message: '啥都木有' });
152
+
153
+ await expect(command.func(page, { bvid: 'BVbroken' })).rejects.toSatisfy(
154
+ (err) => err instanceof CommandExecutionError && /啥都木有.*-404/.test(err.message),
155
+ );
156
+ });
157
+
158
+ it('maps conclusion auth or permission errors to AuthRequiredError', async () => {
159
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
160
+ mockApiGet.mockResolvedValueOnce({ code: -403, message: '访问权限不足' });
161
+
162
+ await expect(command.func(page, { bvid: 'BV1xxx' })).rejects.toBeInstanceOf(AuthRequiredError);
163
+ });
164
+
165
+ it('maps conclusion non-auth API errors to CommandExecutionError', async () => {
166
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
167
+ mockApiGet.mockResolvedValueOnce({ code: -500, message: 'server error' });
168
+
169
+ await expect(command.func(page, { bvid: 'BV1xxx' })).rejects.toSatisfy(
170
+ (err) => err instanceof CommandExecutionError && /server error.*-500/.test(err.message),
171
+ );
172
+ });
173
+
174
+ it('throws CommandExecutionError for malformed conclusion API payloads', async () => {
175
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
176
+ mockApiGet.mockResolvedValueOnce(null);
177
+
178
+ await expect(command.func(page, { bvid: 'BV1xxx' })).rejects.toBeInstanceOf(CommandExecutionError);
179
+ });
180
+
181
+ it('throws CommandExecutionError for malformed model_result JSON', async () => {
182
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
183
+ mockConclusion('{bad json');
184
+
185
+ await expect(command.func(page, { bvid: 'BV1xxx' })).rejects.toSatisfy(
186
+ (err) => err instanceof CommandExecutionError && /model_result JSON/.test(err.message),
187
+ );
188
+ });
189
+
190
+ it('throws CommandExecutionError for malformed outline shapes', async () => {
191
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
192
+ mockConclusion({ summary: '坏 outline', outline: {} });
193
+
194
+ await expect(command.func(page, { bvid: 'BV1xxx' })).rejects.toSatisfy(
195
+ (err) => err instanceof CommandExecutionError && /outline/.test(err.message),
196
+ );
197
+ });
198
+
199
+ it('throws CommandExecutionError for malformed part outline shapes', async () => {
200
+ mockView({ aid: 1, cid: 2, owner: { mid: 3 } });
201
+ mockConclusion({
202
+ summary: '坏 part_outline',
203
+ outline: [{ title: '段落', timestamp: 0, part_outline: {} }],
204
+ });
205
+
206
+ await expect(command.func(page, { bvid: 'BV1xxx' })).rejects.toSatisfy(
207
+ (err) => err instanceof CommandExecutionError && /part outline/.test(err.message),
208
+ );
209
+ });
210
+ });
@@ -2,7 +2,7 @@
2
2
  * Bilibili shared helpers: WBI signing, authenticated fetch, nav data, UID resolution.
3
3
  */
4
4
  import https from 'node:https';
5
- import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
5
+ import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
6
6
  /**
7
7
  * Resolve Bilibili short URL / short code to BV ID.
8
8
  * Supports: BV1MV9NBtENN, XYzsqGa, b23.tv/XYzsqGa, https://b23.tv/XYzsqGa
@@ -12,7 +12,22 @@ export function resolveBvid(input) {
12
12
  if (/^BV[A-Za-z0-9]+$/i.test(trimmed)) {
13
13
  return Promise.resolve(trimmed);
14
14
  }
15
+ try {
16
+ const parsed = new URL(trimmed);
17
+ if (/(\.|^)bilibili\.com$/i.test(parsed.hostname)) {
18
+ const match = parsed.pathname.match(/\/(?:video|bangumi\/play)\/(BV[A-Za-z0-9]+)/i);
19
+ if (match) {
20
+ return Promise.resolve(match[1]);
21
+ }
22
+ }
23
+ }
24
+ catch {
25
+ // Non-URL inputs fall through to b23.tv short-code resolution.
26
+ }
15
27
  const shortCode = trimmed.replace(/^https?:\/\//, '').replace(/^(www\.)?b23\.tv\//, '');
28
+ if (!/^[A-Za-z0-9]+$/.test(shortCode)) {
29
+ return Promise.reject(new Error(`Cannot resolve BV ID from invalid b23.tv short code: ${trimmed}`));
30
+ }
16
31
  const url = 'https://b23.tv/' + shortCode;
17
32
  return new Promise((resolve, reject) => {
18
33
  const req = https.get(url, (res) => {
@@ -29,7 +44,7 @@ export function resolveBvid(input) {
29
44
  reject(new Error(`Cannot resolve BV ID from short URL: ${trimmed}`));
30
45
  });
31
46
  req.on('error', reject);
32
- req.setTimeout(5000, () => { req.destroy(); reject(new Error(`Timeout resolving short URL: ${trimmed}`)); });
47
+ req.setTimeout(4000, () => { req.destroy(); reject(new Error(`Timeout resolving short URL: ${trimmed}`)); });
33
48
  });
34
49
  }
35
50
  const MIXIN_KEY_ENC_TAB = [
@@ -104,6 +119,38 @@ export async function fetchJson(page, url) {
104
119
  }
105
120
  `);
106
121
  }
122
+ /**
123
+ * POST form-encoded params to a Bilibili API endpoint.
124
+ * Runs inside the logged-in browser context and auto-attaches the bili_jct CSRF token,
125
+ * which Bilibili requires on every authenticated write request.
126
+ */
127
+ export async function apiPost(page, path, opts = {}) {
128
+ const params = opts.params ?? {};
129
+ const stringified = Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)]));
130
+ const paramsJs = JSON.stringify(stringified);
131
+ const urlJs = JSON.stringify(`https://api.bilibili.com${path}`);
132
+ return page.evaluate(`
133
+ async () => {
134
+ const csrf = (document.cookie.match(/bili_jct=([^;]+)/) || [])[1] || "";
135
+ const body = new URLSearchParams(${paramsJs});
136
+ body.set("csrf", csrf);
137
+ const res = await fetch(${urlJs}, {
138
+ method: "POST",
139
+ credentials: "include",
140
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
141
+ body: body.toString(),
142
+ });
143
+ // Bilibili write endpoints can return an HTML risk-control page (e.g. HTTP 412)
144
+ // instead of JSON. Surface that as a structured error rather than a parse crash.
145
+ const text = await res.text();
146
+ try {
147
+ return JSON.parse(text);
148
+ } catch {
149
+ return { code: -1, message: "Non-JSON response (HTTP " + res.status + "): " + text.slice(0, 200) };
150
+ }
151
+ }
152
+ `);
153
+ }
107
154
  export async function getSelfUid(page) {
108
155
  const nav = await getNavData(page);
109
156
  const mid = nav?.data?.mid;
@@ -119,8 +166,19 @@ export async function resolveUid(page, input) {
119
166
  params: { search_type: 'bili_user', keyword: input },
120
167
  signed: true,
121
168
  });
122
- const results = payload?.data?.result ?? [];
123
- if (results.length > 0)
124
- return String(results[0].mid);
169
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !payload.data || typeof payload.data !== 'object' || Array.isArray(payload.data) || !Object.hasOwn(payload.data, 'result')) {
170
+ throw new CommandExecutionError(`Bilibili user search returned malformed result for ${input}`);
171
+ }
172
+ const results = payload.data.result;
173
+ if (!Array.isArray(results)) {
174
+ throw new CommandExecutionError(`Bilibili user search returned malformed result for ${input}`);
175
+ }
176
+ if (results.length > 0) {
177
+ const mid = String(results[0]?.mid ?? '').trim();
178
+ if (!mid) {
179
+ throw new CommandExecutionError(`Bilibili user search returned malformed mid for ${input}`);
180
+ }
181
+ return mid;
182
+ }
125
183
  throw new EmptyResultError(`bilibili user search: ${input}`, 'User may not exist or username may have changed.');
126
184
  }
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { resolveBvid } from './utils.js';
2
+ import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { resolveBvid, resolveUid } from './utils.js';
3
4
  describe('resolveBvid', () => {
4
5
  it('passes through a valid BV ID', async () => {
5
6
  expect(await resolveBvid('BV1MV9NBtENN')).toBe('BV1MV9NBtENN');
@@ -10,8 +11,51 @@ describe('resolveBvid', () => {
10
11
  it('handles non-string input via String() coercion', async () => {
11
12
  expect(await resolveBvid('BV123abc')).toBe('BV123abc');
12
13
  });
14
+ it('extracts BV IDs from bilibili video URLs', async () => {
15
+ expect(await resolveBvid('https://www.bilibili.com/video/BV1xx411c7mD/?spm_id_from=333.1007')).toBe('BV1xx411c7mD');
16
+ expect(await resolveBvid('https://m.bilibili.com/video/BV1Je9EBnEha')).toBe('BV1Je9EBnEha');
17
+ });
13
18
  it('rejects invalid input that cannot be resolved', async () => {
14
19
  // A random string that b23.tv won't resolve — should timeout or fail
15
20
  await expect(resolveBvid('not-a-valid-code-99999')).rejects.toThrow();
16
21
  });
17
22
  });
23
+
24
+ describe('resolveUid', () => {
25
+ function pageWithUserSearchResult(result) {
26
+ return {
27
+ evaluate: async (script) => {
28
+ if (String(script).includes('/x/web-interface/nav')) {
29
+ return {
30
+ data: {
31
+ wbi_img: {
32
+ img_url: 'https://i0.hdslb.com/bfs/wbi/abcdefghijklmnopqrstuvwxyz123456.png',
33
+ sub_url: 'https://i0.hdslb.com/bfs/wbi/ABCDEFGHIJKLMNOPQRSTUVWXYZ123456.png',
34
+ },
35
+ },
36
+ };
37
+ }
38
+ return result;
39
+ },
40
+ };
41
+ }
42
+
43
+ it('returns numeric uid input without searching', async () => {
44
+ expect(await resolveUid({}, '12345')).toBe('12345');
45
+ });
46
+
47
+ it('fails closed when user search payload lacks result', async () => {
48
+ await expect(resolveUid(pageWithUserSearchResult({ code: 0, data: {} }), 'missing'))
49
+ .rejects.toBeInstanceOf(CommandExecutionError);
50
+ });
51
+
52
+ it('fails closed when user search result row lacks mid', async () => {
53
+ await expect(resolveUid(pageWithUserSearchResult({ code: 0, data: { result: [{}] } }), 'missing-mid'))
54
+ .rejects.toBeInstanceOf(CommandExecutionError);
55
+ });
56
+
57
+ it('keeps explicit no-user result as EmptyResultError', async () => {
58
+ await expect(resolveUid(pageWithUserSearchResult({ code: 0, data: { result: [] } }), 'nobody'))
59
+ .rejects.toBeInstanceOf(EmptyResultError);
60
+ });
61
+ });