@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.
- package/README.md +35 -194
- package/README.zh-CN.md +42 -260
- package/cli-manifest.json +8160 -4392
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +13 -6
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +7 -3
- package/clis/twitter/search.test.js +41 -0
- package/clis/twitter/shared.js +155 -0
- package/clis/twitter/shared.test.js +465 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +98 -11
- package/clis/weread/search.js +32 -9
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +166 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +252 -2
- package/clis/xiaohongshu/creator-notes.test.js +90 -1
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +2 -19
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +17 -16
- package/clis/zhihu/collection.test.js +50 -3
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -17
- package/clis/zhihu/question.test.js +113 -11
- package/clis/zhihu/search.js +195 -43
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import { AuthRequiredError, CommandExecutionError, EmptyResultError
|
|
2
|
+
import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { apiGet, resolveBvid } from './utils.js';
|
|
4
4
|
cli({
|
|
5
5
|
site: 'bilibili',
|
|
6
6
|
name: 'subtitle',
|
|
7
7
|
access: 'read',
|
|
8
8
|
description: '获取 Bilibili 视频的字幕',
|
|
9
|
+
domain: 'www.bilibili.com',
|
|
9
10
|
strategy: Strategy.COOKIE,
|
|
10
11
|
args: [
|
|
11
12
|
{ name: 'bvid', required: true, positional: true, help: 'Bilibili 视频 BV ID(如 BV1xx411c7mD),或视频 URL / b23.tv 短链' },
|
|
@@ -16,53 +17,78 @@ cli({
|
|
|
16
17
|
if (!page)
|
|
17
18
|
throw new CommandExecutionError('Browser session required for bilibili subtitle');
|
|
18
19
|
const bvid = await resolveBvid(kwargs.bvid);
|
|
19
|
-
// 1.
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
// 1. 通过 view API 拿 cid。
|
|
21
|
+
// 以前的实现走 page.goto(/video/<bvid>) + window.__INITIAL_STATE__.videoData.cid,
|
|
22
|
+
// bangumi 绑定的 bvid(番剧/纪录片/电影/综艺)页面 state 不在 videoData 而在 epList,
|
|
23
|
+
// 导致 SELECTOR 错。view API 接受任何 bvid(UGC + PGC 都通),且不依赖 DOM 结构。
|
|
24
|
+
let view;
|
|
25
|
+
try {
|
|
26
|
+
view = await apiGet(page, '/x/web-interface/view', { params: { bvid } });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new CommandExecutionError(`获取视频信息失败: ${err?.message || err}`);
|
|
30
|
+
}
|
|
31
|
+
if (view?.code !== 0) {
|
|
32
|
+
throw new CommandExecutionError(`获取视频信息失败: ${view?.message ?? 'unknown'} (${view?.code})`);
|
|
33
|
+
}
|
|
34
|
+
const cid = view?.data?.cid;
|
|
26
35
|
if (!cid) {
|
|
27
|
-
throw
|
|
36
|
+
throw new CommandExecutionError(`无法从 view API 拿到 cid (bvid=${bvid})`);
|
|
37
|
+
}
|
|
38
|
+
// 2. 用带 Wbi 签名的 player/v2 拿字幕列表(之前 evaluate 里 fetch 因为没签名会 403)
|
|
39
|
+
let payload;
|
|
40
|
+
try {
|
|
41
|
+
payload = await apiGet(page, '/x/player/wbi/v2', {
|
|
42
|
+
params: { bvid, cid },
|
|
43
|
+
signed: true,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
throw new CommandExecutionError(`获取视频播放信息失败: ${err?.message || err}`);
|
|
48
|
+
}
|
|
49
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
50
|
+
throw new CommandExecutionError('获取到的视频播放信息对象不符合预期格式');
|
|
28
51
|
}
|
|
29
|
-
// 3. 在 Node 端使用 apiGet 获取带 Wbi 签名的字幕列表
|
|
30
|
-
// 之前纯靠 evaluate 里的 fetch 会失败,因为 B 站 /wbi/ 开头的接口强校验 w_rid,未签名直接被风控返回 403 HTML
|
|
31
|
-
const payload = await apiGet(page, '/x/player/wbi/v2', {
|
|
32
|
-
params: { bvid, cid },
|
|
33
|
-
signed: true, // 开启 wbi_sign 自动签名
|
|
34
|
-
});
|
|
35
52
|
if (payload.code !== 0) {
|
|
36
53
|
throw new CommandExecutionError(`获取视频播放信息失败: ${payload.message} (${payload.code})`);
|
|
37
54
|
}
|
|
38
55
|
const needLoginSubtitle = payload.data?.need_login_subtitle === true;
|
|
39
|
-
const subtitles = payload.data?.subtitle?.subtitles
|
|
56
|
+
const subtitles = payload.data?.subtitle?.subtitles;
|
|
57
|
+
if (!Array.isArray(subtitles)) {
|
|
58
|
+
throw new CommandExecutionError('获取到的字幕列表对象不符合数组格式');
|
|
59
|
+
}
|
|
40
60
|
if (subtitles.length === 0) {
|
|
41
61
|
if (needLoginSubtitle) {
|
|
42
62
|
throw new AuthRequiredError('bilibili.com', 'Bilibili subtitles are hidden behind login for this video. Please log in to bilibili.com in Chrome and retry.');
|
|
43
63
|
}
|
|
44
64
|
throw new EmptyResultError('bilibili subtitle', '此视频没有发现外挂或智能字幕。');
|
|
45
65
|
}
|
|
46
|
-
//
|
|
66
|
+
// 3. 选择目标字幕语言
|
|
47
67
|
const target = kwargs.lang
|
|
48
68
|
? subtitles.find((s) => s.lan === kwargs.lang) || subtitles[0]
|
|
49
69
|
: subtitles[0];
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
if (!target || typeof target !== 'object' || !Object.hasOwn(target, 'subtitle_url')) {
|
|
71
|
+
throw new CommandExecutionError('字幕条目缺少 subtitle_url 字段');
|
|
72
|
+
}
|
|
73
|
+
const targetSubUrl = typeof target.subtitle_url === 'string' ? target.subtitle_url.trim() : '';
|
|
74
|
+
if (!targetSubUrl) {
|
|
52
75
|
throw new AuthRequiredError('bilibili.com', '[风控拦截/未登录] 获取到的 subtitle_url 为空!请确保 CLI 已成功登录且风控未封锁此账号。');
|
|
53
76
|
}
|
|
54
77
|
const finalUrl = targetSubUrl.startsWith('//') ? 'https:' + targetSubUrl : targetSubUrl;
|
|
55
|
-
|
|
78
|
+
if (!/^https?:\/\//i.test(finalUrl)) {
|
|
79
|
+
throw new CommandExecutionError(`字幕 URL 非法: ${finalUrl}`);
|
|
80
|
+
}
|
|
81
|
+
// 4. 解析并拉取 CDN 的 JSON 文件
|
|
56
82
|
const fetchJs = `
|
|
57
83
|
(async () => {
|
|
58
84
|
const url = ${JSON.stringify(finalUrl)};
|
|
59
85
|
const res = await fetch(url);
|
|
60
86
|
const text = await res.text();
|
|
61
|
-
|
|
87
|
+
|
|
62
88
|
if (text.startsWith('<!DOCTYPE') || text.startsWith('<html')) {
|
|
63
89
|
return { error: 'HTML', text: text.substring(0, 100), url };
|
|
64
90
|
}
|
|
65
|
-
|
|
91
|
+
|
|
66
92
|
try {
|
|
67
93
|
const subJson = JSON.parse(text);
|
|
68
94
|
// B站真实返回格式是 { font_size: 0.4, font_color: "#FFFFFF", background_alpha: 0.5, background_color: "#9C27B0", Stroke: "none", type: "json" , body: [{from: 0, to: 0, content: ""}] }
|
|
@@ -74,20 +100,39 @@ cli({
|
|
|
74
100
|
}
|
|
75
101
|
})()
|
|
76
102
|
`;
|
|
77
|
-
|
|
103
|
+
let items;
|
|
104
|
+
try {
|
|
105
|
+
items = await page.evaluate(fetchJs);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
throw new CommandExecutionError(`字幕获取失败: ${err?.message || err}`);
|
|
109
|
+
}
|
|
78
110
|
if (items?.error) {
|
|
79
111
|
throw new CommandExecutionError(`字幕获取失败: ${items.error}${items.text ? ' — ' + items.text : ''}`);
|
|
80
112
|
}
|
|
81
|
-
|
|
113
|
+
if (!items || typeof items !== 'object' || items.success !== true) {
|
|
114
|
+
throw new CommandExecutionError('字幕获取结果对象不符合预期格式');
|
|
115
|
+
}
|
|
116
|
+
const finalItems = items.data;
|
|
82
117
|
if (!Array.isArray(finalItems)) {
|
|
83
118
|
throw new CommandExecutionError('解析到的字幕列表对象不符合数组格式');
|
|
84
119
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
if (finalItems.length === 0) {
|
|
121
|
+
throw new EmptyResultError('bilibili subtitle', '字幕文件中没有字幕片段。');
|
|
122
|
+
}
|
|
123
|
+
// 5. 数据映射
|
|
124
|
+
return finalItems.map((item, idx) => {
|
|
125
|
+
const from = Number(item?.from);
|
|
126
|
+
const to = Number(item?.to);
|
|
127
|
+
if (!item || typeof item !== 'object' || !Number.isFinite(from) || !Number.isFinite(to)) {
|
|
128
|
+
throw new CommandExecutionError('字幕片段缺少有效 from/to 时间戳');
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
index: idx + 1,
|
|
132
|
+
from: from.toFixed(2) + 's',
|
|
133
|
+
to: to.toFixed(2) + 's',
|
|
134
|
+
content: String(item.content ?? '')
|
|
135
|
+
};
|
|
136
|
+
});
|
|
92
137
|
},
|
|
93
138
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
3
|
const { mockApiGet } = vi.hoisted(() => ({
|
|
4
4
|
mockApiGet: vi.fn(),
|
|
5
5
|
}));
|
|
@@ -20,30 +20,177 @@ describe('bilibili subtitle', () => {
|
|
|
20
20
|
page.goto.mockClear();
|
|
21
21
|
page.evaluate.mockReset();
|
|
22
22
|
});
|
|
23
|
+
|
|
24
|
+
// 帮助函数:第一发 apiGet(view)固定返 cid=123456 的 OK 响应
|
|
25
|
+
const mockViewOk = (cid = 123456) =>
|
|
26
|
+
mockApiGet.mockResolvedValueOnce({ code: 0, data: { bvid: 'BV1GbXPBeEZm', cid } });
|
|
27
|
+
|
|
23
28
|
it('throws AuthRequiredError when bilibili hides subtitles behind login', async () => {
|
|
24
|
-
|
|
29
|
+
mockViewOk();
|
|
25
30
|
mockApiGet.mockResolvedValueOnce({
|
|
26
31
|
code: 0,
|
|
27
32
|
data: {
|
|
28
33
|
need_login_subtitle: true,
|
|
29
|
-
subtitle: {
|
|
30
|
-
subtitles: [],
|
|
31
|
-
},
|
|
34
|
+
subtitle: { subtitles: [] },
|
|
32
35
|
},
|
|
33
36
|
});
|
|
34
|
-
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toSatisfy(
|
|
37
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toSatisfy(
|
|
38
|
+
(err) => err instanceof AuthRequiredError && /login|登录/i.test(err.message),
|
|
39
|
+
);
|
|
35
40
|
});
|
|
41
|
+
|
|
36
42
|
it('throws EmptyResultError when a video truly has no subtitles', async () => {
|
|
37
|
-
|
|
43
|
+
mockViewOk();
|
|
44
|
+
mockApiGet.mockResolvedValueOnce({
|
|
45
|
+
code: 0,
|
|
46
|
+
data: {
|
|
47
|
+
need_login_subtitle: false,
|
|
48
|
+
subtitle: { subtitles: [] },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(EmptyResultError);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('throws CommandExecutionError when view API returns non-zero code', async () => {
|
|
55
|
+
// 番剧/地区限制等场景下 view API 也会返非零;之前路径走 SELECTOR 错,现在统一走 view 错
|
|
56
|
+
mockApiGet.mockResolvedValueOnce({ code: -404, message: '啥都木有' });
|
|
57
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('wraps view API fetch/json exceptions as CommandExecutionError', async () => {
|
|
61
|
+
mockApiGet.mockRejectedValueOnce(new SyntaxError('Unexpected token <'));
|
|
62
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws CommandExecutionError when view API succeeds but lacks cid', async () => {
|
|
66
|
+
mockApiGet.mockResolvedValueOnce({ code: 0, data: { bvid: 'BV1GbXPBeEZm' /* no cid */ } });
|
|
67
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(/cid/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('throws CommandExecutionError when player subtitle payload is malformed', async () => {
|
|
71
|
+
mockViewOk();
|
|
72
|
+
mockApiGet.mockResolvedValueOnce({
|
|
73
|
+
code: 0,
|
|
74
|
+
data: {
|
|
75
|
+
need_login_subtitle: false,
|
|
76
|
+
subtitle: { subtitles: { lan: 'zh-CN' } },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('throws CommandExecutionError when player API returns a non-object payload', async () => {
|
|
83
|
+
mockViewOk();
|
|
84
|
+
mockApiGet.mockResolvedValueOnce(null);
|
|
85
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
86
|
+
|
|
87
|
+
mockApiGet.mockReset();
|
|
88
|
+
mockViewOk();
|
|
89
|
+
mockApiGet.mockResolvedValueOnce([]);
|
|
90
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('throws AuthRequiredError only for explicit empty subtitle_url entries', async () => {
|
|
94
|
+
mockViewOk();
|
|
95
|
+
mockApiGet.mockResolvedValueOnce({
|
|
96
|
+
code: 0,
|
|
97
|
+
data: {
|
|
98
|
+
need_login_subtitle: false,
|
|
99
|
+
subtitle: { subtitles: [{ lan: 'zh-CN', subtitle_url: '' }] },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(AuthRequiredError);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('throws CommandExecutionError when subtitle entry lacks subtitle_url field', async () => {
|
|
106
|
+
mockViewOk();
|
|
107
|
+
mockApiGet.mockResolvedValueOnce({
|
|
108
|
+
code: 0,
|
|
109
|
+
data: {
|
|
110
|
+
need_login_subtitle: false,
|
|
111
|
+
subtitle: { subtitles: [{ lan: 'zh-CN' }] },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('wraps subtitle file fetch exceptions as CommandExecutionError', async () => {
|
|
118
|
+
mockViewOk();
|
|
119
|
+
mockApiGet.mockResolvedValueOnce({
|
|
120
|
+
code: 0,
|
|
121
|
+
data: {
|
|
122
|
+
need_login_subtitle: false,
|
|
123
|
+
subtitle: { subtitles: [{ lan: 'zh-CN', subtitle_url: '//example.com/sub.json' }] },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
page.evaluate.mockRejectedValueOnce(new Error('Failed to fetch'));
|
|
127
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('throws EmptyResultError when subtitle file has no cue rows', async () => {
|
|
131
|
+
mockViewOk();
|
|
132
|
+
mockApiGet.mockResolvedValueOnce({
|
|
133
|
+
code: 0,
|
|
134
|
+
data: {
|
|
135
|
+
need_login_subtitle: false,
|
|
136
|
+
subtitle: { subtitles: [{ lan: 'zh-CN', subtitle_url: '//example.com/sub.json' }] },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
page.evaluate.mockResolvedValueOnce({ success: true, data: [] });
|
|
140
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(EmptyResultError);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('throws CommandExecutionError when subtitle cue rows have malformed time ranges', async () => {
|
|
144
|
+
mockViewOk();
|
|
145
|
+
mockApiGet.mockResolvedValueOnce({
|
|
146
|
+
code: 0,
|
|
147
|
+
data: {
|
|
148
|
+
need_login_subtitle: false,
|
|
149
|
+
subtitle: { subtitles: [{ lan: 'zh-CN', subtitle_url: '//example.com/sub.json' }] },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
page.evaluate.mockResolvedValueOnce({ success: true, data: [{ from: 'bad', to: 1.5, content: 'hello' }] });
|
|
153
|
+
await expect(command.func(page, { bvid: 'BV1GbXPBeEZm' })).rejects.toThrow(CommandExecutionError);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('works for bangumi-bound bvid (PGC content) — same code path, view API returns cid + redirect_url', async () => {
|
|
157
|
+
// 回归保护:以前 page.goto(/video/<bvid>) 对 bangumi 走重定向,
|
|
158
|
+
// window.__INITIAL_STATE__.videoData 不存在 → SELECTOR 错。view API 不依赖页面结构,bangumi 同样能拿 cid。
|
|
159
|
+
mockApiGet.mockResolvedValueOnce({
|
|
160
|
+
code: 0,
|
|
161
|
+
data: {
|
|
162
|
+
bvid: 'BV1Py4y1D781',
|
|
163
|
+
cid: 267270412,
|
|
164
|
+
redirect_url: 'https://www.bilibili.com/bangumi/play/ep371508',
|
|
165
|
+
title: '【纪录片】灭绝的真相',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
38
168
|
mockApiGet.mockResolvedValueOnce({
|
|
39
169
|
code: 0,
|
|
40
170
|
data: {
|
|
41
171
|
need_login_subtitle: false,
|
|
42
172
|
subtitle: {
|
|
43
|
-
subtitles: [],
|
|
173
|
+
subtitles: [{ lan: 'zh-CN', subtitle_url: '//example.com/sub.json' }],
|
|
44
174
|
},
|
|
45
175
|
},
|
|
46
176
|
});
|
|
47
|
-
|
|
177
|
+
page.evaluate.mockResolvedValueOnce({
|
|
178
|
+
success: true,
|
|
179
|
+
data: [
|
|
180
|
+
{ from: 0, to: 1.5, content: 'hello' },
|
|
181
|
+
{ from: 1.5, to: 3.2, content: 'world' },
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
const out = await command.func(page, { bvid: 'BV1Py4y1D781' });
|
|
185
|
+
expect(out).toEqual([
|
|
186
|
+
{ index: 1, from: '0.00s', to: '1.50s', content: 'hello' },
|
|
187
|
+
{ index: 2, from: '1.50s', to: '3.20s', content: 'world' },
|
|
188
|
+
]);
|
|
189
|
+
// 关键:不再依赖 page.goto,所有 cid 解析走 apiGet
|
|
190
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
191
|
+
// 第一发 apiGet 一定是 view 端点
|
|
192
|
+
const firstCall = mockApiGet.mock.calls[0];
|
|
193
|
+
expect(firstCall[1]).toBe('/x/web-interface/view');
|
|
194
|
+
expect(firstCall[2]?.params?.bvid).toBe('BV1Py4y1D781');
|
|
48
195
|
});
|
|
49
196
|
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bilibili summary — fetches the official AI-generated video summary (the "AI总结"
|
|
3
|
+
* shown on the video page) via /x/web-interface/view/conclusion/get.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
6
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
7
|
+
import { apiGet, resolveBvid } from './utils.js';
|
|
8
|
+
|
|
9
|
+
const BILIBILI_HOST_RE = /(^|\.)bilibili\.com$/i;
|
|
10
|
+
const B23_HOST_RE = /(^|\.)b23\.tv$/i;
|
|
11
|
+
const BVID_RE = /^BV[A-Za-z0-9]+$/;
|
|
12
|
+
|
|
13
|
+
function formatTime(seconds) {
|
|
14
|
+
const s = Math.max(0, Math.floor(Number(seconds) || 0));
|
|
15
|
+
const h = Math.floor(s / 3600);
|
|
16
|
+
const m = Math.floor((s % 3600) / 60);
|
|
17
|
+
const sec = s % 60;
|
|
18
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
19
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(sec)}` : `${pad(m)}:${pad(sec)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readBvid(raw) {
|
|
23
|
+
const input = String(raw ?? '').trim();
|
|
24
|
+
if (!input) {
|
|
25
|
+
throw new ArgumentError('bilibili summary bvid cannot be empty', 'Pass a BV ID, Bilibili video URL, or b23.tv short link.');
|
|
26
|
+
}
|
|
27
|
+
if (BVID_RE.test(input)) {
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
30
|
+
let parsed = null;
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(input);
|
|
33
|
+
} catch {
|
|
34
|
+
// Bare b23.tv short codes are accepted by the shared resolver.
|
|
35
|
+
}
|
|
36
|
+
if (parsed) {
|
|
37
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
38
|
+
throw new ArgumentError('Bilibili summary URL must use http or https');
|
|
39
|
+
}
|
|
40
|
+
if (BILIBILI_HOST_RE.test(parsed.hostname)) {
|
|
41
|
+
const match = parsed.pathname.match(/\/(?:video|bangumi\/play)\/(BV[A-Za-z0-9]+)/i);
|
|
42
|
+
if (!match) {
|
|
43
|
+
throw new ArgumentError('Bilibili summary URL must contain a BV video id');
|
|
44
|
+
}
|
|
45
|
+
return match[1];
|
|
46
|
+
}
|
|
47
|
+
if (!B23_HOST_RE.test(parsed.hostname)) {
|
|
48
|
+
throw new ArgumentError('Bilibili summary URL must be a bilibili.com or b23.tv URL');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return await resolveBvid(input);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new ArgumentError(`Cannot resolve Bilibili BV ID from input: ${input}`, error instanceof Error ? error.message : String(error));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireOkPayload(payload, label) {
|
|
59
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
60
|
+
throw new CommandExecutionError(`Bilibili ${label} API returned a malformed payload`);
|
|
61
|
+
}
|
|
62
|
+
if (payload.code !== 0) {
|
|
63
|
+
const message = payload.message ?? 'unknown error';
|
|
64
|
+
if (payload.code === -101 || payload.code === -403 || /登录|权限|forbidden|permission|login/i.test(String(message))) {
|
|
65
|
+
throw new AuthRequiredError('bilibili.com', `Bilibili ${label} API requires login or permission: ${message} (${payload.code})`);
|
|
66
|
+
}
|
|
67
|
+
throw new CommandExecutionError(`Bilibili ${label} API failed: ${message} (${payload.code})`);
|
|
68
|
+
}
|
|
69
|
+
return payload.data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readModelResult(data, bvid) {
|
|
73
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
74
|
+
throw new CommandExecutionError('Bilibili conclusion API returned malformed data');
|
|
75
|
+
}
|
|
76
|
+
if (data.code !== 0) {
|
|
77
|
+
throw new EmptyResultError('bilibili summary', `Bilibili has not generated an AI summary for ${bvid}.`);
|
|
78
|
+
}
|
|
79
|
+
let modelResult = data.model_result;
|
|
80
|
+
if (typeof modelResult === 'string') {
|
|
81
|
+
try {
|
|
82
|
+
modelResult = JSON.parse(modelResult);
|
|
83
|
+
} catch {
|
|
84
|
+
throw new CommandExecutionError('Bilibili conclusion API returned malformed model_result JSON');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!modelResult || typeof modelResult !== 'object' || Array.isArray(modelResult)) {
|
|
88
|
+
throw new CommandExecutionError('Bilibili conclusion API returned malformed model_result');
|
|
89
|
+
}
|
|
90
|
+
const summary = String(modelResult.summary ?? '').trim();
|
|
91
|
+
if (!summary) {
|
|
92
|
+
throw new EmptyResultError('bilibili summary', `Bilibili has not generated an AI summary for ${bvid}.`);
|
|
93
|
+
}
|
|
94
|
+
const outline = modelResult.outline ?? [];
|
|
95
|
+
if (!Array.isArray(outline)) {
|
|
96
|
+
throw new CommandExecutionError('Bilibili conclusion API returned malformed outline');
|
|
97
|
+
}
|
|
98
|
+
return { summary, outline };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rowsFromModel(model) {
|
|
102
|
+
const rows = [{ time: '', content: model.summary }];
|
|
103
|
+
for (const section of model.outline) {
|
|
104
|
+
if (!section || typeof section !== 'object' || Array.isArray(section)) {
|
|
105
|
+
throw new CommandExecutionError('Bilibili conclusion API returned malformed outline section');
|
|
106
|
+
}
|
|
107
|
+
const sectionTitle = String(section.title ?? '').trim();
|
|
108
|
+
const sectionTime = formatTime(section.timestamp);
|
|
109
|
+
if (sectionTitle) {
|
|
110
|
+
rows.push({ time: sectionTime, content: `# ${sectionTitle}` });
|
|
111
|
+
}
|
|
112
|
+
const points = section.part_outline ?? [];
|
|
113
|
+
if (!Array.isArray(points)) {
|
|
114
|
+
throw new CommandExecutionError('Bilibili conclusion API returned malformed part outline');
|
|
115
|
+
}
|
|
116
|
+
for (const point of points) {
|
|
117
|
+
if (!point || typeof point !== 'object' || Array.isArray(point)) {
|
|
118
|
+
throw new CommandExecutionError('Bilibili conclusion API returned malformed outline point');
|
|
119
|
+
}
|
|
120
|
+
const content = String(point.content ?? '').trim();
|
|
121
|
+
if (content) {
|
|
122
|
+
rows.push({ time: formatTime(point.timestamp), content });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return rows;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
var command = cli({
|
|
130
|
+
site: 'bilibili',
|
|
131
|
+
name: 'summary',
|
|
132
|
+
access: 'read',
|
|
133
|
+
description: '获取 B站视频的官方 AI 总结(视频页「AI总结」同款,含分段大纲与时间戳)',
|
|
134
|
+
domain: 'www.bilibili.com',
|
|
135
|
+
strategy: Strategy.COOKIE,
|
|
136
|
+
args: [
|
|
137
|
+
{ name: 'bvid', required: true, positional: true, help: 'Video BV ID / URL / b23.tv short link' },
|
|
138
|
+
],
|
|
139
|
+
columns: ['time', 'content'],
|
|
140
|
+
func: async (page, kwargs) => {
|
|
141
|
+
if (!page) {
|
|
142
|
+
throw new CommandExecutionError('Browser session required for bilibili summary');
|
|
143
|
+
}
|
|
144
|
+
const bvid = await readBvid(kwargs.bvid);
|
|
145
|
+
const view = await apiGet(page, '/x/web-interface/view', { params: { bvid } });
|
|
146
|
+
const viewData = requireOkPayload(view, 'view');
|
|
147
|
+
const cid = viewData?.cid;
|
|
148
|
+
const upMid = viewData?.owner?.mid;
|
|
149
|
+
if (!cid || !upMid) {
|
|
150
|
+
throw new CommandExecutionError(`Bilibili view API did not return cid/up_mid for ${bvid}`);
|
|
151
|
+
}
|
|
152
|
+
const conclusion = await apiGet(page, '/x/web-interface/view/conclusion/get', {
|
|
153
|
+
params: { bvid, cid, up_mid: upMid },
|
|
154
|
+
signed: true,
|
|
155
|
+
});
|
|
156
|
+
const conclusionData = requireOkPayload(conclusion, 'conclusion');
|
|
157
|
+
return rowsFromModel(readModelResult(conclusionData, bvid));
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export const __test__ = {
|
|
162
|
+
command,
|
|
163
|
+
formatTime,
|
|
164
|
+
readBvid,
|
|
165
|
+
readModelResult,
|
|
166
|
+
rowsFromModel,
|
|
167
|
+
};
|