@jackwener/opencli 1.5.5 → 1.5.7
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/CHANGELOG.md +26 -0
- package/README.md +31 -4
- package/README.zh-CN.md +40 -5
- package/SKILL.md +1 -1
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.js +30 -27
- package/dist/browser/daemon-client.d.ts +11 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.js +1 -0
- package/dist/browser/dom-helpers.test.js +14 -1
- package/dist/browser/mcp.js +18 -13
- package/dist/browser/page.d.ts +6 -0
- package/dist/browser/page.js +37 -2
- package/dist/browser/page.test.d.ts +1 -0
- package/dist/browser/page.test.js +44 -0
- package/dist/browser/stealth.js +198 -0
- package/dist/browser/stealth.test.d.ts +1 -0
- package/dist/browser/stealth.test.js +134 -0
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +5 -1
- package/dist/build-manifest.test.js +2 -0
- package/dist/cli-manifest.json +1821 -252
- package/dist/cli.js +20 -3
- package/dist/clis/antigravity/serve.d.ts +1 -1
- package/dist/clis/antigravity/serve.js +5 -8
- package/dist/clis/band/bands.d.ts +1 -0
- package/dist/clis/band/bands.js +72 -0
- package/dist/clis/band/mentions.d.ts +1 -0
- package/dist/clis/band/mentions.js +127 -0
- package/dist/clis/band/post.d.ts +1 -0
- package/dist/clis/band/post.js +175 -0
- package/dist/clis/band/posts.d.ts +1 -0
- package/dist/clis/band/posts.js +94 -0
- package/dist/clis/bilibili/subtitle.js +4 -0
- package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.test.js +48 -0
- package/dist/clis/chatwise/ask.js +0 -2
- package/dist/clis/chatwise/export.js +0 -2
- package/dist/clis/chatwise/history.js +0 -2
- package/dist/clis/chatwise/model.js +0 -2
- package/dist/clis/chatwise/new.js +1 -2
- package/dist/clis/chatwise/read.js +0 -2
- package/dist/clis/chatwise/screenshot.js +1 -2
- package/dist/clis/chatwise/send.js +0 -2
- package/dist/clis/chatwise/status.js +1 -2
- package/dist/clis/ctrip/search.d.ts +13 -0
- package/dist/clis/ctrip/search.js +73 -48
- package/dist/clis/ctrip/search.test.d.ts +1 -0
- package/dist/clis/ctrip/search.test.js +64 -0
- package/dist/clis/doubao/detail.d.ts +1 -0
- package/dist/clis/doubao/detail.js +33 -0
- package/dist/clis/doubao/detail.test.d.ts +1 -0
- package/dist/clis/doubao/detail.test.js +42 -0
- package/dist/clis/doubao/history.d.ts +1 -0
- package/dist/clis/doubao/history.js +28 -0
- package/dist/clis/doubao/history.test.d.ts +1 -0
- package/dist/clis/doubao/history.test.js +37 -0
- package/dist/clis/doubao/meeting-summary.d.ts +1 -0
- package/dist/clis/doubao/meeting-summary.js +39 -0
- package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
- package/dist/clis/doubao/meeting-transcript.js +36 -0
- package/dist/clis/doubao/utils.d.ts +27 -0
- package/dist/clis/doubao/utils.js +317 -0
- package/dist/clis/doubao/utils.test.d.ts +1 -0
- package/dist/clis/doubao/utils.test.js +24 -0
- package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
- package/dist/clis/douyin/_shared/public-api.js +29 -0
- package/dist/clis/douyin/_shared/sts2.js +8 -2
- package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/sts2.test.js +27 -0
- package/dist/clis/douyin/activities.js +4 -2
- package/dist/clis/douyin/activities.test.js +34 -1
- package/dist/clis/douyin/collections.js +1 -1
- package/dist/clis/douyin/collections.test.js +24 -2
- package/dist/clis/douyin/draft.d.ts +8 -11
- package/dist/clis/douyin/draft.js +302 -185
- package/dist/clis/douyin/draft.test.d.ts +1 -1
- package/dist/clis/douyin/draft.test.js +357 -2
- package/dist/clis/douyin/hashtag.js +9 -2
- package/dist/clis/douyin/hashtag.test.js +35 -2
- package/dist/clis/douyin/profile.js +1 -1
- package/dist/clis/douyin/profile.test.js +36 -1
- package/dist/clis/douyin/user-videos.d.ts +5 -0
- package/dist/clis/douyin/user-videos.js +74 -0
- package/dist/clis/douyin/user-videos.test.d.ts +1 -0
- package/dist/clis/douyin/user-videos.test.js +108 -0
- package/dist/clis/douyin/videos.js +22 -5
- package/dist/clis/douyin/videos.test.js +45 -2
- package/dist/clis/facebook/search.test.d.ts +5 -0
- package/dist/clis/facebook/search.test.js +60 -0
- package/dist/clis/facebook/search.yaml +4 -3
- package/dist/clis/instagram/download.d.ts +16 -0
- package/dist/clis/instagram/download.js +225 -0
- package/dist/clis/instagram/download.test.d.ts +1 -0
- package/dist/clis/instagram/download.test.js +118 -0
- package/dist/clis/notebooklm/bind-current.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.js +29 -0
- package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.test.js +35 -0
- package/dist/clis/notebooklm/binding.test.d.ts +1 -0
- package/dist/clis/notebooklm/binding.test.js +44 -0
- package/dist/clis/notebooklm/compat.test.d.ts +3 -0
- package/dist/clis/notebooklm/compat.test.js +16 -0
- package/dist/clis/notebooklm/current.d.ts +1 -0
- package/dist/clis/notebooklm/current.js +28 -0
- package/dist/clis/notebooklm/get.d.ts +1 -0
- package/dist/clis/notebooklm/get.js +37 -0
- package/dist/clis/notebooklm/history.d.ts +1 -0
- package/dist/clis/notebooklm/history.js +25 -0
- package/dist/clis/notebooklm/history.test.d.ts +1 -0
- package/dist/clis/notebooklm/history.test.js +58 -0
- package/dist/clis/notebooklm/list.d.ts +1 -0
- package/dist/clis/notebooklm/list.js +35 -0
- package/dist/clis/notebooklm/note-list.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.js +28 -0
- package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.test.js +56 -0
- package/dist/clis/notebooklm/notes-get.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.js +47 -0
- package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.test.js +72 -0
- package/dist/clis/notebooklm/rpc.d.ts +36 -0
- package/dist/clis/notebooklm/rpc.js +189 -0
- package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
- package/dist/clis/notebooklm/rpc.test.js +105 -0
- package/dist/clis/notebooklm/shared.d.ts +87 -0
- package/dist/clis/notebooklm/shared.js +3 -0
- package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.js +44 -0
- package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
- package/dist/clis/notebooklm/source-get.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.js +40 -0
- package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.test.js +84 -0
- package/dist/clis/notebooklm/source-guide.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.js +44 -0
- package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.test.js +104 -0
- package/dist/clis/notebooklm/source-list.d.ts +1 -0
- package/dist/clis/notebooklm/source-list.js +30 -0
- package/dist/clis/notebooklm/status.d.ts +1 -0
- package/dist/clis/notebooklm/status.js +31 -0
- package/dist/clis/notebooklm/summary.d.ts +1 -0
- package/dist/clis/notebooklm/summary.js +30 -0
- package/dist/clis/notebooklm/summary.test.d.ts +1 -0
- package/dist/clis/notebooklm/summary.test.js +78 -0
- package/dist/clis/notebooklm/utils.d.ts +37 -0
- package/dist/clis/notebooklm/utils.js +739 -0
- package/dist/clis/notebooklm/utils.test.d.ts +1 -0
- package/dist/clis/notebooklm/utils.test.js +390 -0
- package/dist/clis/ones/common.d.ts +32 -0
- package/dist/clis/ones/common.js +144 -0
- package/dist/clis/ones/enrich-tasks.d.ts +5 -0
- package/dist/clis/ones/enrich-tasks.js +37 -0
- package/dist/clis/ones/login.d.ts +1 -0
- package/dist/clis/ones/login.js +80 -0
- package/dist/clis/ones/logout.d.ts +1 -0
- package/dist/clis/ones/logout.js +17 -0
- package/dist/clis/ones/me.d.ts +1 -0
- package/dist/clis/ones/me.js +30 -0
- package/dist/clis/ones/my-tasks.d.ts +1 -0
- package/dist/clis/ones/my-tasks.js +120 -0
- package/dist/clis/ones/resolve-labels.d.ts +10 -0
- package/dist/clis/ones/resolve-labels.js +64 -0
- package/dist/clis/ones/task-helpers.d.ts +29 -0
- package/dist/clis/ones/task-helpers.js +212 -0
- package/dist/clis/ones/task-helpers.test.d.ts +1 -0
- package/dist/clis/ones/task-helpers.test.js +12 -0
- package/dist/clis/ones/task.d.ts +1 -0
- package/dist/clis/ones/task.js +66 -0
- package/dist/clis/ones/tasks.d.ts +1 -0
- package/dist/clis/ones/tasks.js +79 -0
- package/dist/clis/ones/token-info.d.ts +1 -0
- package/dist/clis/ones/token-info.js +42 -0
- package/dist/clis/ones/worklog.d.ts +11 -0
- package/dist/clis/ones/worklog.js +267 -0
- package/dist/clis/ones/worklog.test.d.ts +1 -0
- package/dist/clis/ones/worklog.test.js +20 -0
- package/dist/clis/spotify/spotify.d.ts +1 -0
- package/dist/clis/spotify/spotify.js +316 -0
- package/dist/clis/spotify/utils.d.ts +21 -0
- package/dist/clis/spotify/utils.js +66 -0
- package/dist/clis/spotify/utils.test.d.ts +1 -0
- package/dist/clis/spotify/utils.test.js +67 -0
- package/dist/clis/substack/utils.d.ts +4 -0
- package/dist/clis/substack/utils.js +8 -2
- package/dist/clis/substack/utils.test.d.ts +1 -0
- package/dist/clis/substack/utils.test.js +46 -0
- package/dist/clis/tieba/commands.test.d.ts +4 -0
- package/dist/clis/tieba/commands.test.js +79 -0
- package/dist/clis/tieba/hot.d.ts +1 -0
- package/dist/clis/tieba/hot.js +48 -0
- package/dist/clis/tieba/posts.d.ts +1 -0
- package/dist/clis/tieba/posts.js +85 -0
- package/dist/clis/tieba/read.d.ts +1 -0
- package/dist/clis/tieba/read.js +140 -0
- package/dist/clis/tieba/search.d.ts +1 -0
- package/dist/clis/tieba/search.js +108 -0
- package/dist/clis/tieba/utils.d.ts +101 -0
- package/dist/clis/tieba/utils.js +240 -0
- package/dist/clis/tieba/utils.test.d.ts +1 -0
- package/dist/clis/tieba/utils.test.js +290 -0
- package/dist/clis/v2ex/hot.yaml +4 -1
- package/dist/clis/v2ex/latest.yaml +4 -1
- package/dist/clis/v2ex/topic.yaml +6 -1
- package/dist/clis/weixin/download.d.ts +9 -0
- package/dist/clis/weixin/download.js +76 -6
- package/dist/clis/weread/book.js +206 -13
- package/dist/clis/weread/commands.test.js +331 -0
- package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
- package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
- package/dist/clis/weread/search-regression.test.d.ts +1 -0
- package/dist/clis/weread/search-regression.test.js +407 -0
- package/dist/clis/weread/search.js +143 -7
- package/dist/clis/weread/shelf.js +13 -95
- package/dist/clis/weread/utils.d.ts +56 -0
- package/dist/clis/weread/utils.js +234 -7
- package/dist/clis/weread/utils.test.js +71 -1
- package/dist/clis/xiaohongshu/comments.d.ts +3 -0
- package/dist/clis/xiaohongshu/comments.js +76 -17
- package/dist/clis/xiaohongshu/comments.test.js +70 -9
- package/dist/clis/xiaohongshu/download.d.ts +4 -1
- package/dist/clis/xiaohongshu/download.js +83 -22
- package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/download.test.js +75 -0
- package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
- package/dist/clis/xiaohongshu/note-helpers.js +23 -0
- package/dist/clis/xiaohongshu/note.d.ts +7 -0
- package/dist/clis/xiaohongshu/note.js +76 -0
- package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/note.test.js +136 -0
- package/dist/clis/xiaohongshu/publish.d.ts +1 -1
- package/dist/clis/xiaohongshu/publish.js +78 -31
- package/dist/clis/xiaohongshu/publish.test.js +66 -1
- package/dist/clis/xiaohongshu/search.js +9 -0
- package/dist/clis/xiaohongshu/search.test.js +10 -4
- package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.js +2 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
- package/dist/clis/xueqiu/comments.d.ts +118 -0
- package/dist/clis/xueqiu/comments.js +354 -0
- package/dist/clis/xueqiu/comments.test.d.ts +1 -0
- package/dist/clis/xueqiu/comments.test.js +696 -0
- package/dist/clis/youtube/search.js +57 -17
- package/dist/clis/youtube/transcript.js +2 -4
- package/dist/clis/youtube/utils.d.ts +9 -0
- package/dist/clis/youtube/utils.js +67 -3
- package/dist/clis/youtube/utils.test.d.ts +1 -0
- package/dist/clis/youtube/utils.test.js +37 -0
- package/dist/clis/youtube/video.js +16 -15
- package/dist/clis/zhihu/question.js +19 -17
- package/dist/clis/zhihu/question.test.d.ts +1 -0
- package/dist/clis/zhihu/question.test.js +54 -0
- package/dist/clis/zsxq/dynamics.d.ts +1 -0
- package/dist/clis/zsxq/dynamics.js +47 -0
- package/dist/clis/zsxq/groups.d.ts +1 -0
- package/dist/clis/zsxq/groups.js +32 -0
- package/dist/clis/zsxq/search.d.ts +1 -0
- package/dist/clis/zsxq/search.js +43 -0
- package/dist/clis/zsxq/search.test.d.ts +1 -0
- package/dist/clis/zsxq/search.test.js +24 -0
- package/dist/clis/zsxq/topic.d.ts +1 -0
- package/dist/clis/zsxq/topic.js +47 -0
- package/dist/clis/zsxq/topic.test.d.ts +1 -0
- package/dist/clis/zsxq/topic.test.js +29 -0
- package/dist/clis/zsxq/topics.d.ts +1 -0
- package/dist/clis/zsxq/topics.js +25 -0
- package/dist/clis/zsxq/topics.test.d.ts +1 -0
- package/dist/clis/zsxq/topics.test.js +24 -0
- package/dist/clis/zsxq/utils.d.ts +97 -0
- package/dist/clis/zsxq/utils.js +230 -0
- package/dist/commanderAdapter.js +10 -1
- package/dist/commanderAdapter.test.js +64 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +124 -0
- package/dist/commands/daemon.test.d.ts +1 -0
- package/dist/commands/daemon.test.js +185 -0
- package/dist/completion.js +3 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +25 -14
- package/dist/daemon.test.d.ts +1 -0
- package/dist/daemon.test.js +65 -0
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +47 -2
- package/dist/electron-apps.d.ts +29 -0
- package/dist/electron-apps.js +65 -0
- package/dist/electron-apps.test.d.ts +1 -0
- package/dist/electron-apps.test.js +43 -0
- package/dist/engine.test.js +41 -9
- package/dist/execution.js +20 -16
- package/dist/external-clis.yaml +17 -0
- package/dist/idle-manager.d.ts +19 -0
- package/dist/idle-manager.js +54 -0
- package/dist/launcher.d.ts +36 -0
- package/dist/launcher.js +152 -0
- package/dist/launcher.test.d.ts +1 -0
- package/dist/launcher.test.js +57 -0
- package/dist/main.js +3 -3
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +31 -3
- package/dist/registry.test.js +13 -0
- package/dist/runtime.d.ts +5 -3
- package/dist/runtime.js +12 -5
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +3 -0
- package/dist/serialization.test.js +17 -1
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +52 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +19 -0
- package/dist/types.d.ts +5 -0
- package/dist/weixin-download.test.js +14 -0
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/band.md +63 -0
- package/docs/adapters/browser/notebooklm.md +69 -0
- package/docs/adapters/browser/ones.md +59 -0
- package/docs/adapters/browser/spotify.md +62 -0
- package/docs/adapters/browser/tieba.md +45 -0
- package/docs/adapters/browser/xiaohongshu.md +19 -10
- package/docs/adapters/browser/xueqiu.md +5 -0
- package/docs/adapters/browser/zsxq.md +49 -0
- package/docs/adapters/index.md +67 -63
- package/docs/adapters-doc/ones.md +32 -0
- package/docs/guide/browser-bridge.md +12 -0
- package/docs/guide/troubleshooting.md +9 -4
- package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
- package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
- package/docs/zh/guide/browser-bridge.md +12 -0
- package/extension/dist/background.js +794 -513
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +189 -10
- package/extension/src/cdp.ts +54 -0
- package/extension/src/protocol.ts +11 -5
- package/package.json +1 -1
- package/scripts/postinstall.js +16 -0
- package/src/browser/cdp.ts +24 -17
- package/src/browser/daemon-client.ts +11 -1
- package/src/browser/dom-helpers.test.ts +15 -1
- package/src/browser/dom-helpers.ts +1 -0
- package/src/browser/mcp.ts +18 -13
- package/src/browser/page.test.ts +58 -0
- package/src/browser/page.ts +34 -2
- package/src/browser/stealth.test.ts +153 -0
- package/src/browser/stealth.ts +198 -0
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.test.ts +2 -0
- package/src/build-manifest.ts +6 -1
- package/src/cli.ts +21 -3
- package/src/clis/antigravity/SKILL.md +3 -12
- package/src/clis/antigravity/serve.ts +5 -10
- package/src/clis/band/bands.ts +76 -0
- package/src/clis/band/mentions.ts +134 -0
- package/src/clis/band/post.ts +187 -0
- package/src/clis/band/posts.ts +106 -0
- package/src/clis/bilibili/subtitle.test.ts +60 -0
- package/src/clis/bilibili/subtitle.ts +4 -0
- package/src/clis/chatwise/ask.ts +0 -2
- package/src/clis/chatwise/export.ts +0 -2
- package/src/clis/chatwise/history.ts +0 -2
- package/src/clis/chatwise/model.ts +0 -2
- package/src/clis/chatwise/new.ts +1 -2
- package/src/clis/chatwise/read.ts +0 -2
- package/src/clis/chatwise/screenshot.ts +1 -2
- package/src/clis/chatwise/send.ts +0 -2
- package/src/clis/chatwise/status.ts +1 -2
- package/src/clis/ctrip/search.test.ts +73 -0
- package/src/clis/ctrip/search.ts +97 -47
- package/src/clis/doubao/detail.test.ts +53 -0
- package/src/clis/doubao/detail.ts +41 -0
- package/src/clis/doubao/history.test.ts +45 -0
- package/src/clis/doubao/history.ts +32 -0
- package/src/clis/doubao/meeting-summary.ts +53 -0
- package/src/clis/doubao/meeting-transcript.ts +48 -0
- package/src/clis/doubao/utils.test.ts +45 -0
- package/src/clis/doubao/utils.ts +371 -0
- package/src/clis/douyin/_shared/public-api.ts +84 -0
- package/src/clis/douyin/_shared/sts2.test.ts +31 -0
- package/src/clis/douyin/_shared/sts2.ts +11 -3
- package/src/clis/douyin/activities.test.ts +41 -1
- package/src/clis/douyin/activities.ts +12 -3
- package/src/clis/douyin/collections.test.ts +35 -2
- package/src/clis/douyin/collections.ts +1 -1
- package/src/clis/douyin/draft.test.ts +444 -2
- package/src/clis/douyin/draft.ts +382 -218
- package/src/clis/douyin/hashtag.test.ts +42 -2
- package/src/clis/douyin/hashtag.ts +11 -3
- package/src/clis/douyin/profile.test.ts +43 -1
- package/src/clis/douyin/profile.ts +9 -2
- package/src/clis/douyin/user-videos.test.ts +122 -0
- package/src/clis/douyin/user-videos.ts +101 -0
- package/src/clis/douyin/videos.test.ts +52 -2
- package/src/clis/douyin/videos.ts +49 -15
- package/src/clis/facebook/search.test.ts +70 -0
- package/src/clis/facebook/search.yaml +4 -3
- package/src/clis/instagram/download.test.ts +159 -0
- package/src/clis/instagram/download.ts +286 -0
- package/src/clis/notebooklm/bind-current.test.ts +43 -0
- package/src/clis/notebooklm/bind-current.ts +36 -0
- package/src/clis/notebooklm/binding.test.ts +53 -0
- package/src/clis/notebooklm/compat.test.ts +19 -0
- package/src/clis/notebooklm/current.ts +38 -0
- package/src/clis/notebooklm/get.ts +53 -0
- package/src/clis/notebooklm/history.test.ts +70 -0
- package/src/clis/notebooklm/history.ts +36 -0
- package/src/clis/notebooklm/list.ts +40 -0
- package/src/clis/notebooklm/note-list.test.ts +64 -0
- package/src/clis/notebooklm/note-list.ts +42 -0
- package/src/clis/notebooklm/notes-get.test.ts +88 -0
- package/src/clis/notebooklm/notes-get.ts +67 -0
- package/src/clis/notebooklm/rpc.test.ts +126 -0
- package/src/clis/notebooklm/rpc.ts +286 -0
- package/src/clis/notebooklm/shared.ts +98 -0
- package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
- package/src/clis/notebooklm/source-fulltext.ts +69 -0
- package/src/clis/notebooklm/source-get.test.ts +100 -0
- package/src/clis/notebooklm/source-get.ts +60 -0
- package/src/clis/notebooklm/source-guide.test.ts +121 -0
- package/src/clis/notebooklm/source-guide.ts +69 -0
- package/src/clis/notebooklm/source-list.ts +45 -0
- package/src/clis/notebooklm/status.ts +34 -0
- package/src/clis/notebooklm/summary.test.ts +94 -0
- package/src/clis/notebooklm/summary.ts +45 -0
- package/src/clis/notebooklm/utils.test.ts +446 -0
- package/src/clis/notebooklm/utils.ts +893 -0
- package/src/clis/ones/common.ts +187 -0
- package/src/clis/ones/enrich-tasks.ts +47 -0
- package/src/clis/ones/login.ts +103 -0
- package/src/clis/ones/logout.ts +19 -0
- package/src/clis/ones/me.ts +34 -0
- package/src/clis/ones/my-tasks.ts +148 -0
- package/src/clis/ones/resolve-labels.ts +80 -0
- package/src/clis/ones/task-helpers.test.ts +14 -0
- package/src/clis/ones/task-helpers.ts +214 -0
- package/src/clis/ones/task.ts +79 -0
- package/src/clis/ones/tasks.ts +92 -0
- package/src/clis/ones/token-info.ts +46 -0
- package/src/clis/ones/worklog.test.ts +24 -0
- package/src/clis/ones/worklog.ts +306 -0
- package/src/clis/spotify/spotify.ts +328 -0
- package/src/clis/spotify/utils.test.ts +87 -0
- package/src/clis/spotify/utils.ts +92 -0
- package/src/clis/substack/utils.test.ts +54 -0
- package/src/clis/substack/utils.ts +10 -2
- package/src/clis/tieba/commands.test.ts +86 -0
- package/src/clis/tieba/hot.ts +52 -0
- package/src/clis/tieba/posts.ts +108 -0
- package/src/clis/tieba/read.ts +158 -0
- package/src/clis/tieba/search.ts +119 -0
- package/src/clis/tieba/utils.test.ts +322 -0
- package/src/clis/tieba/utils.ts +348 -0
- package/src/clis/v2ex/hot.yaml +4 -1
- package/src/clis/v2ex/latest.yaml +4 -1
- package/src/clis/v2ex/topic.yaml +6 -1
- package/src/clis/weixin/download.ts +95 -6
- package/src/clis/weread/book.ts +256 -13
- package/src/clis/weread/commands.test.ts +409 -0
- package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
- package/src/clis/weread/search-regression.test.ts +440 -0
- package/src/clis/weread/search.ts +189 -9
- package/src/clis/weread/shelf.ts +20 -122
- package/src/clis/weread/utils.test.ts +81 -1
- package/src/clis/weread/utils.ts +293 -7
- package/src/clis/xiaohongshu/comments.test.ts +85 -9
- package/src/clis/xiaohongshu/comments.ts +76 -17
- package/src/clis/xiaohongshu/download.test.ts +96 -0
- package/src/clis/xiaohongshu/download.ts +83 -22
- package/src/clis/xiaohongshu/note-helpers.ts +25 -0
- package/src/clis/xiaohongshu/note.test.ts +164 -0
- package/src/clis/xiaohongshu/note.ts +86 -0
- package/src/clis/xiaohongshu/publish.test.ts +79 -1
- package/src/clis/xiaohongshu/publish.ts +84 -30
- package/src/clis/xiaohongshu/search.test.ts +11 -4
- package/src/clis/xiaohongshu/search.ts +13 -0
- package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
- package/src/clis/xiaohongshu/user-helpers.ts +4 -0
- package/src/clis/xueqiu/comments.test.ts +823 -0
- package/src/clis/xueqiu/comments.ts +461 -0
- package/src/clis/youtube/search.ts +57 -17
- package/src/clis/youtube/transcript.ts +2 -4
- package/src/clis/youtube/utils.test.ts +43 -0
- package/src/clis/youtube/utils.ts +69 -0
- package/src/clis/youtube/video.ts +16 -15
- package/src/clis/zhihu/question.test.ts +71 -0
- package/src/clis/zhihu/question.ts +27 -15
- package/src/clis/zsxq/dynamics.ts +60 -0
- package/src/clis/zsxq/groups.ts +41 -0
- package/src/clis/zsxq/search.test.ts +29 -0
- package/src/clis/zsxq/search.ts +54 -0
- package/src/clis/zsxq/topic.test.ts +34 -0
- package/src/clis/zsxq/topic.ts +68 -0
- package/src/clis/zsxq/topics.test.ts +29 -0
- package/src/clis/zsxq/topics.ts +36 -0
- package/src/clis/zsxq/utils.ts +351 -0
- package/src/commanderAdapter.test.ts +77 -0
- package/src/commanderAdapter.ts +8 -1
- package/src/commands/daemon.test.ts +238 -0
- package/src/commands/daemon.ts +135 -0
- package/src/completion.ts +2 -1
- package/src/constants.ts +3 -0
- package/src/daemon.test.ts +88 -0
- package/src/daemon.ts +26 -14
- package/src/discovery.ts +52 -2
- package/src/electron-apps.test.ts +50 -0
- package/src/electron-apps.ts +89 -0
- package/src/engine.test.ts +45 -9
- package/src/execution.ts +24 -19
- package/src/external-clis.yaml +17 -0
- package/src/idle-manager.ts +60 -0
- package/src/launcher.test.ts +67 -0
- package/src/launcher.ts +185 -0
- package/src/main.ts +3 -2
- package/src/registry.test.ts +15 -0
- package/src/registry.ts +32 -3
- package/src/runtime.ts +13 -7
- package/src/serialization.test.ts +19 -1
- package/src/serialization.ts +2 -0
- package/src/tui.test.ts +23 -0
- package/src/tui.ts +65 -0
- package/src/types.ts +5 -0
- package/src/weixin-download.test.ts +27 -0
- package/tests/e2e/band-auth.test.ts +20 -0
- package/tests/e2e/browser-auth-helpers.ts +18 -0
- package/tests/e2e/browser-auth.test.ts +35 -47
- package/tests/e2e/browser-public-extended.test.ts +6 -2
- package/tests/e2e/browser-public.test.ts +288 -0
- package/tests/e2e/management.test.ts +1 -1
- package/tests/e2e/plugin-management.test.ts +1 -1
- package/vitest.config.ts +1 -0
- package/chatwise-opencli.ps1 +0 -82
- package/dist/clis/chatwise/shared.d.ts +0 -2
- package/dist/clis/chatwise/shared.js +0 -6
- package/dist/weread-private-api-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.js +0 -39
- package/src/clis/chatwise/shared.ts +0 -8
- package/src/weread-search-regression.test.ts +0 -44
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Xiaohongshu comments — DOM extraction from note detail page.
|
|
3
3
|
* XHS API requires signed requests, so we scrape the rendered DOM instead.
|
|
4
|
+
*
|
|
5
|
+
* Supports both top-level comments and nested replies (楼中楼) via
|
|
6
|
+
* the --with-replies flag.
|
|
4
7
|
*/
|
|
5
8
|
import { cli, Strategy } from '../../registry.js';
|
|
6
9
|
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
10
|
+
import { parseNoteId, buildNoteUrl } from './note-helpers.js';
|
|
11
|
+
function parseCommentLimit(raw, fallback = 20) {
|
|
12
|
+
const n = Number(raw);
|
|
13
|
+
if (!Number.isFinite(n))
|
|
14
|
+
return fallback;
|
|
15
|
+
return Math.max(1, Math.min(Math.floor(n), 50));
|
|
16
|
+
}
|
|
7
17
|
cli({
|
|
8
18
|
site: 'xiaohongshu',
|
|
9
19
|
name: 'comments',
|
|
10
|
-
description: '
|
|
20
|
+
description: '获取小红书笔记评论(支持楼中楼子回复)',
|
|
11
21
|
domain: 'www.xiaohongshu.com',
|
|
12
22
|
strategy: Strategy.COOKIE,
|
|
13
23
|
args: [
|
|
14
|
-
{ name: 'note-id', required: true, positional: true, help: 'Note ID or full
|
|
15
|
-
{ name: 'limit', type: 'int', default: 20, help: 'Number of comments (max 50)' },
|
|
24
|
+
{ name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
|
|
25
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of top-level comments (max 50)' },
|
|
26
|
+
{ name: 'with-replies', type: 'boolean', default: false, help: 'Include nested replies (楼中楼)' },
|
|
16
27
|
],
|
|
17
|
-
columns: ['rank', 'author', 'text', 'likes', 'time'],
|
|
28
|
+
columns: ['rank', 'author', 'text', 'likes', 'time', 'is_reply', 'reply_to'],
|
|
18
29
|
func: async (page, kwargs) => {
|
|
19
|
-
const limit =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
noteId = urlMatch[1];
|
|
25
|
-
await page.goto(`https://www.xiaohongshu.com/explore/${noteId}`);
|
|
30
|
+
const limit = parseCommentLimit(kwargs.limit);
|
|
31
|
+
const withReplies = Boolean(kwargs['with-replies']);
|
|
32
|
+
const raw = String(kwargs['note-id']);
|
|
33
|
+
const noteId = parseNoteId(raw);
|
|
34
|
+
await page.goto(buildNoteUrl(raw));
|
|
26
35
|
await page.wait(3);
|
|
27
36
|
const data = await page.evaluate(`
|
|
28
37
|
(async () => {
|
|
29
38
|
const wait = (ms) => new Promise(r => setTimeout(r, ms))
|
|
39
|
+
const withReplies = ${withReplies}
|
|
30
40
|
|
|
31
41
|
// Check login state
|
|
32
42
|
const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
|
|
@@ -41,6 +51,31 @@ cli({
|
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
54
|
+
const parseLikes = (el) => {
|
|
55
|
+
const raw = clean(el)
|
|
56
|
+
return /^\\d+$/.test(raw) ? Number(raw) : 0
|
|
57
|
+
}
|
|
58
|
+
const expandReplyThreads = async (root) => {
|
|
59
|
+
if (!withReplies || !root) return
|
|
60
|
+
const clickedTexts = new Set()
|
|
61
|
+
for (let round = 0; round < 3; round++) {
|
|
62
|
+
const expanders = Array.from(root.querySelectorAll('button, [role="button"], span, div')).filter(el => {
|
|
63
|
+
if (!(el instanceof HTMLElement)) return false
|
|
64
|
+
const text = clean(el)
|
|
65
|
+
if (!text || text.length > 24) return false
|
|
66
|
+
if (!/(展开|更多回复|全部回复|查看.*回复|共\\d+条回复)/.test(text)) return false
|
|
67
|
+
if (clickedTexts.has(text)) return false
|
|
68
|
+
return true
|
|
69
|
+
})
|
|
70
|
+
if (!expanders.length) break
|
|
71
|
+
for (const el of expanders) {
|
|
72
|
+
const text = clean(el)
|
|
73
|
+
el.click()
|
|
74
|
+
clickedTexts.add(text)
|
|
75
|
+
await wait(300)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
44
79
|
|
|
45
80
|
const results = []
|
|
46
81
|
const parents = document.querySelectorAll('.parent-comment')
|
|
@@ -50,13 +85,24 @@ cli({
|
|
|
50
85
|
|
|
51
86
|
const author = clean(item.querySelector('.author-wrapper .name, .user-name'))
|
|
52
87
|
const text = clean(item.querySelector('.content, .note-text'))
|
|
53
|
-
|
|
54
|
-
const likesRaw = clean(item.querySelector('.count'))
|
|
55
|
-
const likes = /^\\d+$/.test(likesRaw) ? Number(likesRaw) : 0
|
|
88
|
+
const likes = parseLikes(item.querySelector('.count'))
|
|
56
89
|
const time = clean(item.querySelector('.date, .time'))
|
|
57
90
|
|
|
58
91
|
if (!text) continue
|
|
59
|
-
results.push({ author, text, likes, time })
|
|
92
|
+
results.push({ author, text, likes, time, is_reply: false, reply_to: '' })
|
|
93
|
+
|
|
94
|
+
// Extract nested replies (楼中楼)
|
|
95
|
+
if (withReplies) {
|
|
96
|
+
await expandReplyThreads(p)
|
|
97
|
+
p.querySelectorAll('.reply-container .comment-item-sub, .sub-comment-list .comment-item').forEach(sub => {
|
|
98
|
+
const sAuthor = clean(sub.querySelector('.name, .user-name'))
|
|
99
|
+
const sText = clean(sub.querySelector('.content, .note-text'))
|
|
100
|
+
const sLikes = parseLikes(sub.querySelector('.count'))
|
|
101
|
+
const sTime = clean(sub.querySelector('.date, .time'))
|
|
102
|
+
if (!sText) return
|
|
103
|
+
results.push({ author: sAuthor, text: sText, likes: sLikes, time: sTime, is_reply: true, reply_to: author })
|
|
104
|
+
})
|
|
105
|
+
}
|
|
60
106
|
}
|
|
61
107
|
|
|
62
108
|
return { loginWall, results }
|
|
@@ -68,7 +114,20 @@ cli({
|
|
|
68
114
|
if (data.loginWall) {
|
|
69
115
|
throw new AuthRequiredError('www.xiaohongshu.com', 'Note comments require login');
|
|
70
116
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
117
|
+
const all = data.results ?? [];
|
|
118
|
+
// When limiting, count only top-level comments; their replies are included for free
|
|
119
|
+
if (withReplies) {
|
|
120
|
+
const limited = [];
|
|
121
|
+
let topCount = 0;
|
|
122
|
+
for (const c of all) {
|
|
123
|
+
if (!c.is_reply)
|
|
124
|
+
topCount++;
|
|
125
|
+
if (topCount > limit)
|
|
126
|
+
break;
|
|
127
|
+
limited.push(c);
|
|
128
|
+
}
|
|
129
|
+
return limited.map((c, i) => ({ rank: i + 1, ...c }));
|
|
130
|
+
}
|
|
131
|
+
return all.slice(0, limit).map((c, i) => ({ rank: i + 1, ...c }));
|
|
73
132
|
},
|
|
74
133
|
});
|
|
@@ -33,22 +33,20 @@ describe('xiaohongshu comments', () => {
|
|
|
33
33
|
const page = createPageMock({
|
|
34
34
|
loginWall: false,
|
|
35
35
|
results: [
|
|
36
|
-
{ author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01' },
|
|
37
|
-
{ author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02' },
|
|
36
|
+
{ author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01', is_reply: false, reply_to: '' },
|
|
37
|
+
{ author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02', is_reply: false, reply_to: '' },
|
|
38
38
|
],
|
|
39
39
|
});
|
|
40
40
|
const result = (await command.func(page, { 'note-id': '69aadbcb000000002202f131', limit: 5 }));
|
|
41
41
|
expect(page.goto.mock.calls[0][0]).toContain('/explore/69aadbcb000000002202f131');
|
|
42
|
-
expect(result).
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
]);
|
|
46
|
-
expect(result[0]).not.toHaveProperty('loginWall');
|
|
42
|
+
expect(result).toHaveLength(2);
|
|
43
|
+
expect(result[0]).toMatchObject({ rank: 1, author: 'Alice', text: 'Great note!', likes: 10 });
|
|
44
|
+
expect(result[1]).toMatchObject({ rank: 2, author: 'Bob', text: 'Very helpful', likes: 0 });
|
|
47
45
|
});
|
|
48
46
|
it('strips /explore/ prefix from full URL input', async () => {
|
|
49
47
|
const page = createPageMock({
|
|
50
48
|
loginWall: false,
|
|
51
|
-
results: [{ author: 'Alice', text: 'Nice', likes: 1, time: '2024-01-01' }],
|
|
49
|
+
results: [{ author: 'Alice', text: 'Nice', likes: 1, time: '2024-01-01', is_reply: false, reply_to: '' }],
|
|
52
50
|
});
|
|
53
51
|
await command.func(page, {
|
|
54
52
|
'note-id': 'https://www.xiaohongshu.com/explore/69aadbcb000000002202f131',
|
|
@@ -56,6 +54,15 @@ describe('xiaohongshu comments', () => {
|
|
|
56
54
|
});
|
|
57
55
|
expect(page.goto.mock.calls[0][0]).toContain('/explore/69aadbcb000000002202f131');
|
|
58
56
|
});
|
|
57
|
+
it('preserves full search_result URL with xsec_token for navigation', async () => {
|
|
58
|
+
const page = createPageMock({
|
|
59
|
+
loginWall: false,
|
|
60
|
+
results: [{ author: 'Alice', text: 'Nice', likes: 1, time: '2024-01-01', is_reply: false, reply_to: '' }],
|
|
61
|
+
});
|
|
62
|
+
const fullUrl = 'https://www.xiaohongshu.com/search_result/69aadbcb000000002202f131?xsec_token=abc&xsec_source=pc_search';
|
|
63
|
+
await command.func(page, { 'note-id': fullUrl, limit: 5 });
|
|
64
|
+
expect(page.goto.mock.calls[0][0]).toBe(fullUrl);
|
|
65
|
+
});
|
|
59
66
|
it('throws AuthRequiredError when login wall is detected', async () => {
|
|
60
67
|
const page = createPageMock({ loginWall: true, results: [] });
|
|
61
68
|
await expect(command.func(page, { 'note-id': 'abc123', limit: 5 })).rejects.toThrow('Note comments require login');
|
|
@@ -64,12 +71,14 @@ describe('xiaohongshu comments', () => {
|
|
|
64
71
|
const page = createPageMock({ loginWall: false, results: [] });
|
|
65
72
|
await expect(command.func(page, { 'note-id': 'abc123', limit: 5 })).resolves.toEqual([]);
|
|
66
73
|
});
|
|
67
|
-
it('respects the limit', async () => {
|
|
74
|
+
it('respects the limit for top-level comments', async () => {
|
|
68
75
|
const manyComments = Array.from({ length: 10 }, (_, i) => ({
|
|
69
76
|
author: `User${i}`,
|
|
70
77
|
text: `Comment ${i}`,
|
|
71
78
|
likes: i,
|
|
72
79
|
time: '2024-01-01',
|
|
80
|
+
is_reply: false,
|
|
81
|
+
reply_to: '',
|
|
73
82
|
}));
|
|
74
83
|
const page = createPageMock({ loginWall: false, results: manyComments });
|
|
75
84
|
const result = (await command.func(page, { 'note-id': 'abc123', limit: 3 }));
|
|
@@ -77,4 +86,56 @@ describe('xiaohongshu comments', () => {
|
|
|
77
86
|
expect(result[0].rank).toBe(1);
|
|
78
87
|
expect(result[2].rank).toBe(3);
|
|
79
88
|
});
|
|
89
|
+
it('clamps invalid negative limits to a safe minimum', async () => {
|
|
90
|
+
const page = createPageMock({
|
|
91
|
+
loginWall: false,
|
|
92
|
+
results: [
|
|
93
|
+
{ author: 'Alice', text: 'Great note!', likes: 10, time: '2024-01-01', is_reply: false, reply_to: '' },
|
|
94
|
+
{ author: 'Bob', text: 'Very helpful', likes: 0, time: '2024-01-02', is_reply: false, reply_to: '' },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
const result = (await command.func(page, { 'note-id': 'abc123', limit: -3 }));
|
|
98
|
+
expect(result).toHaveLength(1);
|
|
99
|
+
expect(result[0]).toMatchObject({ rank: 1, author: 'Alice' });
|
|
100
|
+
});
|
|
101
|
+
describe('--with-replies', () => {
|
|
102
|
+
it('includes reply rows with is_reply=true and reply_to set', async () => {
|
|
103
|
+
const page = createPageMock({
|
|
104
|
+
loginWall: false,
|
|
105
|
+
results: [
|
|
106
|
+
{ author: 'Alice', text: 'Main comment', likes: 10, time: '03-25', is_reply: false, reply_to: '' },
|
|
107
|
+
{ author: 'Bob', text: 'Reply to Alice', likes: 3, time: '03-25', is_reply: true, reply_to: 'Alice' },
|
|
108
|
+
{ author: 'Carol', text: 'Another top', likes: 5, time: '03-26', is_reply: false, reply_to: '' },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
const result = (await command.func(page, {
|
|
112
|
+
'note-id': 'abc123', limit: 50, 'with-replies': true,
|
|
113
|
+
}));
|
|
114
|
+
expect(result).toHaveLength(3);
|
|
115
|
+
expect(result[0]).toMatchObject({ author: 'Alice', is_reply: false, reply_to: '' });
|
|
116
|
+
expect(result[1]).toMatchObject({ author: 'Bob', is_reply: true, reply_to: 'Alice' });
|
|
117
|
+
expect(result[2]).toMatchObject({ author: 'Carol', is_reply: false, reply_to: '' });
|
|
118
|
+
const script = page.evaluate.mock.calls[0][0];
|
|
119
|
+
expect(script).toContain('共\\d+条回复');
|
|
120
|
+
expect(script).toContain('el.click()');
|
|
121
|
+
});
|
|
122
|
+
it('limits by top-level count, keeping attached replies', async () => {
|
|
123
|
+
const page = createPageMock({
|
|
124
|
+
loginWall: false,
|
|
125
|
+
results: [
|
|
126
|
+
{ author: 'A', text: 'Top 1', likes: 0, time: '', is_reply: false, reply_to: '' },
|
|
127
|
+
{ author: 'A1', text: 'Reply 1', likes: 0, time: '', is_reply: true, reply_to: 'A' },
|
|
128
|
+
{ author: 'A2', text: 'Reply 2', likes: 0, time: '', is_reply: true, reply_to: 'A' },
|
|
129
|
+
{ author: 'B', text: 'Top 2', likes: 0, time: '', is_reply: false, reply_to: '' },
|
|
130
|
+
{ author: 'C', text: 'Top 3', likes: 0, time: '', is_reply: false, reply_to: '' },
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
// Limit to 2 top-level comments — should include A + 2 replies + B = 4 rows
|
|
134
|
+
const result = (await command.func(page, {
|
|
135
|
+
'note-id': 'abc123', limit: 2, 'with-replies': true,
|
|
136
|
+
}));
|
|
137
|
+
expect(result).toHaveLength(4);
|
|
138
|
+
expect(result.map((r) => r.author)).toEqual(['A', 'A1', 'A2', 'B']);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
80
141
|
});
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Xiaohongshu download — download images and videos from a note.
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* opencli xiaohongshu download
|
|
5
|
+
* opencli xiaohongshu download <note-id-or-url> --output ./xhs
|
|
6
|
+
*
|
|
7
|
+
* Accepts a bare note ID, a full xiaohongshu.com URL (with xsec_token),
|
|
8
|
+
* or a short link (http://xhslink.com/...).
|
|
6
9
|
*/
|
|
7
10
|
export {};
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* Xiaohongshu download — download images and videos from a note.
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* opencli xiaohongshu download
|
|
5
|
+
* opencli xiaohongshu download <note-id-or-url> --output ./xhs
|
|
6
|
+
*
|
|
7
|
+
* Accepts a bare note ID, a full xiaohongshu.com URL (with xsec_token),
|
|
8
|
+
* or a short link (http://xhslink.com/...).
|
|
6
9
|
*/
|
|
7
10
|
import { cli, Strategy } from '../../registry.js';
|
|
8
11
|
import { formatCookieHeader } from '../../download/index.js';
|
|
9
12
|
import { downloadMedia } from '../../download/media-download.js';
|
|
13
|
+
import { buildNoteUrl, parseNoteId } from './note-helpers.js';
|
|
10
14
|
cli({
|
|
11
15
|
site: 'xiaohongshu',
|
|
12
16
|
name: 'download',
|
|
@@ -14,15 +18,15 @@ cli({
|
|
|
14
18
|
domain: 'www.xiaohongshu.com',
|
|
15
19
|
strategy: Strategy.COOKIE,
|
|
16
20
|
args: [
|
|
17
|
-
{ name: 'note-id', positional: true, required: true, help: 'Note ID
|
|
21
|
+
{ name: 'note-id', positional: true, required: true, help: 'Note ID, full URL, or short link' },
|
|
18
22
|
{ name: 'output', default: './xiaohongshu-downloads', help: 'Output directory' },
|
|
19
23
|
],
|
|
20
24
|
columns: ['index', 'type', 'status', 'size'],
|
|
21
25
|
func: async (page, kwargs) => {
|
|
22
|
-
const
|
|
26
|
+
const rawInput = String(kwargs['note-id']);
|
|
23
27
|
const output = kwargs.output;
|
|
24
|
-
|
|
25
|
-
await page.goto(
|
|
28
|
+
const noteId = parseNoteId(rawInput);
|
|
29
|
+
await page.goto(buildNoteUrl(rawInput));
|
|
26
30
|
// Extract note info and media URLs
|
|
27
31
|
const data = await page.evaluate(`
|
|
28
32
|
(() => {
|
|
@@ -32,6 +36,18 @@ cli({
|
|
|
32
36
|
author: '',
|
|
33
37
|
media: []
|
|
34
38
|
};
|
|
39
|
+
const seenMedia = new Set();
|
|
40
|
+
const pushMedia = (type, url) => {
|
|
41
|
+
if (!url) return;
|
|
42
|
+
const key = type + ':' + url;
|
|
43
|
+
if (seenMedia.has(key)) return;
|
|
44
|
+
seenMedia.add(key);
|
|
45
|
+
result.media.push({ type, url });
|
|
46
|
+
};
|
|
47
|
+
const locationMatch = (location.pathname || '').match(/\\/(?:explore|note|search_result|discovery\\/item)\\/([a-f0-9]+)/i);
|
|
48
|
+
if (locationMatch) {
|
|
49
|
+
result.noteId = locationMatch[1];
|
|
50
|
+
}
|
|
35
51
|
|
|
36
52
|
// Get title
|
|
37
53
|
const titleEl = document.querySelector('.title, #detail-title, .note-content .title');
|
|
@@ -57,7 +73,6 @@ cli({
|
|
|
57
73
|
document.querySelectorAll(selector).forEach(img => {
|
|
58
74
|
let src = img.src || img.getAttribute('data-src') || '';
|
|
59
75
|
if (src && (src.includes('xhscdn') || src.includes('xiaohongshu'))) {
|
|
60
|
-
// Convert to high quality URL (remove resize parameters)
|
|
61
76
|
src = src.split('?')[0];
|
|
62
77
|
src = src.replace(/\\/imageView\\d+\\/\\d+\\/w\\/\\d+/, '');
|
|
63
78
|
imageUrls.add(src);
|
|
@@ -65,26 +80,69 @@ cli({
|
|
|
65
80
|
});
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
// Get video
|
|
69
|
-
const videoSelectors = [
|
|
70
|
-
'video source',
|
|
71
|
-
'video[src]',
|
|
72
|
-
'.player video',
|
|
73
|
-
'.video-player video'
|
|
74
|
-
];
|
|
83
|
+
// Get video — prefer real URL from page state over blob: URLs
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
// Method 1: Extract from __INITIAL_STATE__ (SSR hydration data)
|
|
86
|
+
try {
|
|
87
|
+
const state = window.__INITIAL_STATE__;
|
|
88
|
+
if (state) {
|
|
89
|
+
const noteData = state.note?.noteDetailMap || state.note?.note || {};
|
|
90
|
+
for (const key of Object.keys(noteData)) {
|
|
91
|
+
const note = noteData[key]?.note || noteData[key];
|
|
92
|
+
const video = note?.video;
|
|
93
|
+
if (video) {
|
|
94
|
+
const vUrl = video.url || video.originVideoKey || video.consumer?.originVideoKey;
|
|
95
|
+
if (vUrl) {
|
|
96
|
+
const fullUrl = vUrl.startsWith('http') ? vUrl : 'https://sns-video-bd.xhscdn.com/' + vUrl;
|
|
97
|
+
pushMedia('video', fullUrl);
|
|
98
|
+
}
|
|
99
|
+
const streams = video.media?.stream?.h264 || [];
|
|
100
|
+
for (const stream of streams) {
|
|
101
|
+
if (stream.masterUrl) pushMedia('video', stream.masterUrl);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
81
104
|
}
|
|
82
|
-
}
|
|
105
|
+
}
|
|
106
|
+
} catch(e) {}
|
|
107
|
+
|
|
108
|
+
// Method 2: Extract video URLs from inline script JSON
|
|
109
|
+
if (result.media.filter(m => m.type === 'video').length === 0) {
|
|
110
|
+
try {
|
|
111
|
+
const scripts = document.querySelectorAll('script');
|
|
112
|
+
for (const s of scripts) {
|
|
113
|
+
const text = s.textContent || '';
|
|
114
|
+
const videoMatches = text.match(/https?:\\/\\/sns-video[^"'\\s]+\\.mp4[^"'\\s]*/g)
|
|
115
|
+
|| text.match(/https?:\\/\\/[^"'\\s]*xhscdn[^"'\\s]*\\.mp4[^"'\\s]*/g);
|
|
116
|
+
if (videoMatches) {
|
|
117
|
+
videoMatches.forEach(url => {
|
|
118
|
+
pushMedia('video', url.replace(/\\\\u002F/g, '/'));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch(e) {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Method 3: Fallback to DOM video elements, skip blob: URLs
|
|
126
|
+
if (result.media.filter(m => m.type === 'video').length === 0) {
|
|
127
|
+
const videoSelectors = [
|
|
128
|
+
'video source',
|
|
129
|
+
'video[src]',
|
|
130
|
+
'.player video',
|
|
131
|
+
'.video-player video'
|
|
132
|
+
];
|
|
133
|
+
for (const selector of videoSelectors) {
|
|
134
|
+
document.querySelectorAll(selector).forEach(v => {
|
|
135
|
+
const src = v.src || v.getAttribute('src') || '';
|
|
136
|
+
if (src && !src.startsWith('blob:')) {
|
|
137
|
+
pushMedia('video', src);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
83
141
|
}
|
|
84
142
|
|
|
85
143
|
// Add images to media
|
|
86
144
|
imageUrls.forEach(url => {
|
|
87
|
-
|
|
145
|
+
pushMedia('image', url);
|
|
88
146
|
});
|
|
89
147
|
|
|
90
148
|
return result;
|
|
@@ -95,11 +153,14 @@ cli({
|
|
|
95
153
|
}
|
|
96
154
|
// Extract cookies for authenticated downloads
|
|
97
155
|
const cookies = formatCookieHeader(await page.getCookies({ domain: 'xiaohongshu.com' }));
|
|
156
|
+
const resolvedNoteId = typeof data.noteId === 'string' && data.noteId.trim()
|
|
157
|
+
? data.noteId.trim()
|
|
158
|
+
: noteId;
|
|
98
159
|
return downloadMedia(data.media, {
|
|
99
160
|
output,
|
|
100
|
-
subdir:
|
|
161
|
+
subdir: resolvedNoteId,
|
|
101
162
|
cookies,
|
|
102
|
-
filenamePrefix:
|
|
163
|
+
filenamePrefix: resolvedNoteId,
|
|
103
164
|
timeout: 60000,
|
|
104
165
|
});
|
|
105
166
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './download.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockDownloadMedia, mockFormatCookieHeader } = vi.hoisted(() => ({
|
|
3
|
+
mockDownloadMedia: vi.fn(),
|
|
4
|
+
mockFormatCookieHeader: vi.fn(() => 'a=b'),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock('../../download/media-download.js', () => ({
|
|
7
|
+
downloadMedia: mockDownloadMedia,
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../../download/index.js', () => ({
|
|
10
|
+
formatCookieHeader: mockFormatCookieHeader,
|
|
11
|
+
}));
|
|
12
|
+
import { getRegistry } from '../../registry.js';
|
|
13
|
+
import './download.js';
|
|
14
|
+
function createPageMock(evaluateResult) {
|
|
15
|
+
return {
|
|
16
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
18
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
24
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
26
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
30
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
31
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
35
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'sid', value: 'secret', domain: '.xiaohongshu.com' }]),
|
|
36
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
37
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
describe('xiaohongshu download', () => {
|
|
41
|
+
const command = getRegistry().get('xiaohongshu/download');
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockDownloadMedia.mockReset();
|
|
44
|
+
mockFormatCookieHeader.mockClear();
|
|
45
|
+
mockDownloadMedia.mockResolvedValue([{ index: 1, type: 'video', status: 'success', size: '1 MB' }]);
|
|
46
|
+
});
|
|
47
|
+
it('preserves short links for navigation but uses canonical note id for output naming', async () => {
|
|
48
|
+
const page = createPageMock({
|
|
49
|
+
noteId: '69bc166f000000001a02069a',
|
|
50
|
+
media: [{ type: 'video', url: 'https://sns-video-hw.xhscdn.com/example.mp4' }],
|
|
51
|
+
});
|
|
52
|
+
const shortUrl = 'http://xhslink.com/o/4MKEjsZnhCz';
|
|
53
|
+
await command.func(page, { 'note-id': shortUrl, output: './out' });
|
|
54
|
+
expect(page.goto.mock.calls[0][0]).toBe(shortUrl);
|
|
55
|
+
expect(mockDownloadMedia).toHaveBeenCalledWith([{ type: 'video', url: 'https://sns-video-hw.xhscdn.com/example.mp4' }], expect.objectContaining({
|
|
56
|
+
output: './out',
|
|
57
|
+
subdir: '69bc166f000000001a02069a',
|
|
58
|
+
filenamePrefix: '69bc166f000000001a02069a',
|
|
59
|
+
cookies: 'a=b',
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
it('preserves full note URL with xsec_token for navigation', async () => {
|
|
63
|
+
const page = createPageMock({
|
|
64
|
+
noteId: '69bc166f000000001a02069a',
|
|
65
|
+
media: [{ type: 'image', url: 'https://ci.xiaohongshu.com/example.jpg' }],
|
|
66
|
+
});
|
|
67
|
+
const fullUrl = 'https://www.xiaohongshu.com/explore/69bc166f000000001a02069a?xsec_token=abc&xsec_source=pc_search';
|
|
68
|
+
await command.func(page, { 'note-id': fullUrl, output: './out' });
|
|
69
|
+
expect(page.goto.mock.calls[0][0]).toBe(fullUrl);
|
|
70
|
+
expect(mockDownloadMedia).toHaveBeenCalledWith([{ type: 'image', url: 'https://ci.xiaohongshu.com/example.jpg' }], expect.objectContaining({
|
|
71
|
+
subdir: '69bc166f000000001a02069a',
|
|
72
|
+
filenamePrefix: '69bc166f000000001a02069a',
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Side-effect-free helpers shared by xiaohongshu note and comments commands. */
|
|
2
|
+
/** Extract a bare note ID from a full URL or raw ID string. */
|
|
3
|
+
export declare function parseNoteId(input: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Build the best navigation URL for a note.
|
|
6
|
+
*
|
|
7
|
+
* XHS blocks direct `/explore/<id>` access without a valid `xsec_token`.
|
|
8
|
+
* When the user passes a full URL (from search results), we preserve it
|
|
9
|
+
* so the browser navigates with the token intact. For bare IDs we fall
|
|
10
|
+
* back to the `/explore/<id>` path (works when cookies carry enough context).
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildNoteUrl(input: string): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Side-effect-free helpers shared by xiaohongshu note and comments commands. */
|
|
2
|
+
/** Extract a bare note ID from a full URL or raw ID string. */
|
|
3
|
+
export function parseNoteId(input) {
|
|
4
|
+
const trimmed = input.trim();
|
|
5
|
+
const match = trimmed.match(/\/(?:explore|note|search_result)\/([a-f0-9]+)/);
|
|
6
|
+
return match ? match[1] : trimmed;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build the best navigation URL for a note.
|
|
10
|
+
*
|
|
11
|
+
* XHS blocks direct `/explore/<id>` access without a valid `xsec_token`.
|
|
12
|
+
* When the user passes a full URL (from search results), we preserve it
|
|
13
|
+
* so the browser navigates with the token intact. For bare IDs we fall
|
|
14
|
+
* back to the `/explore/<id>` path (works when cookies carry enough context).
|
|
15
|
+
*/
|
|
16
|
+
export function buildNoteUrl(input) {
|
|
17
|
+
const trimmed = input.trim();
|
|
18
|
+
if (/^https?:\/\//.test(trimmed)) {
|
|
19
|
+
// Full URL — navigate as-is; the browser will follow any redirects
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
return `https://www.xiaohongshu.com/explore/${trimmed}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu note — read full note content from a public note page.
|
|
3
|
+
*
|
|
4
|
+
* Extracts title, author, description text, and engagement metrics
|
|
5
|
+
* (likes, collects, comment count) via DOM extraction.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
9
|
+
import { parseNoteId, buildNoteUrl } from './note-helpers.js';
|
|
10
|
+
cli({
|
|
11
|
+
site: 'xiaohongshu',
|
|
12
|
+
name: 'note',
|
|
13
|
+
description: '获取小红书笔记正文和互动数据',
|
|
14
|
+
domain: 'www.xiaohongshu.com',
|
|
15
|
+
strategy: Strategy.COOKIE,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
|
|
18
|
+
],
|
|
19
|
+
columns: ['field', 'value'],
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
const raw = String(kwargs['note-id']);
|
|
22
|
+
const noteId = parseNoteId(raw);
|
|
23
|
+
const url = buildNoteUrl(raw);
|
|
24
|
+
await page.goto(url);
|
|
25
|
+
await page.wait(3);
|
|
26
|
+
const data = await page.evaluate(`
|
|
27
|
+
(() => {
|
|
28
|
+
const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
|
|
29
|
+
const notFound = /页面不见了|笔记不存在|无法浏览/.test(document.body.innerText || '')
|
|
30
|
+
|
|
31
|
+
const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
32
|
+
|
|
33
|
+
const title = clean(document.querySelector('#detail-title, .title'))
|
|
34
|
+
const desc = clean(document.querySelector('#detail-desc, .desc, .note-text'))
|
|
35
|
+
const author = clean(document.querySelector('.username, .author-wrapper .name'))
|
|
36
|
+
const likes = clean(document.querySelector('.like-wrapper .count'))
|
|
37
|
+
const collects = clean(document.querySelector('.collect-wrapper .count'))
|
|
38
|
+
const comments = clean(document.querySelector('.chat-wrapper .count'))
|
|
39
|
+
|
|
40
|
+
// Try to extract tags/topics
|
|
41
|
+
const tags = []
|
|
42
|
+
document.querySelectorAll('#detail-desc a.tag, #detail-desc a[href*="search_result"]').forEach(el => {
|
|
43
|
+
const t = (el.textContent || '').trim()
|
|
44
|
+
if (t) tags.push(t)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return { loginWall, notFound, title, desc, author, likes, collects, comments, tags }
|
|
48
|
+
})()
|
|
49
|
+
`);
|
|
50
|
+
if (!data || typeof data !== 'object') {
|
|
51
|
+
throw new EmptyResultError('xiaohongshu/note', 'Unexpected evaluate response');
|
|
52
|
+
}
|
|
53
|
+
if (data.loginWall) {
|
|
54
|
+
throw new AuthRequiredError('www.xiaohongshu.com', 'Note content requires login');
|
|
55
|
+
}
|
|
56
|
+
if (data.notFound) {
|
|
57
|
+
throw new EmptyResultError('xiaohongshu/note', `Note ${noteId} not found or unavailable — it may have been deleted or restricted`);
|
|
58
|
+
}
|
|
59
|
+
const d = data;
|
|
60
|
+
// XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
|
|
61
|
+
// normalize to '0' unless the value looks numeric.
|
|
62
|
+
const numOrZero = (v) => /^\d+/.test(v) ? v : '0';
|
|
63
|
+
const rows = [
|
|
64
|
+
{ field: 'title', value: d.title || '' },
|
|
65
|
+
{ field: 'author', value: d.author || '' },
|
|
66
|
+
{ field: 'content', value: d.desc || '' },
|
|
67
|
+
{ field: 'likes', value: numOrZero(d.likes || '') },
|
|
68
|
+
{ field: 'collects', value: numOrZero(d.collects || '') },
|
|
69
|
+
{ field: 'comments', value: numOrZero(d.comments || '') },
|
|
70
|
+
];
|
|
71
|
+
if (d.tags?.length) {
|
|
72
|
+
rows.push({ field: 'tags', value: d.tags.join(', ') });
|
|
73
|
+
}
|
|
74
|
+
return rows;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './note.js';
|