@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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assertSpotifyCredentialsConfigured,
|
|
5
|
+
getFirstSpotifyTrack,
|
|
6
|
+
hasConfiguredSpotifyCredentials,
|
|
7
|
+
mapSpotifyTrackResults,
|
|
8
|
+
parseDotEnv,
|
|
9
|
+
resolveSpotifyCredentials,
|
|
10
|
+
} from './utils.js';
|
|
11
|
+
|
|
12
|
+
describe('spotify utils', () => {
|
|
13
|
+
it('parses dotenv-style credential files', () => {
|
|
14
|
+
const env = parseDotEnv(`
|
|
15
|
+
# Spotify credentials
|
|
16
|
+
SPOTIFY_CLIENT_ID=abc123
|
|
17
|
+
SPOTIFY_CLIENT_SECRET=def456
|
|
18
|
+
`);
|
|
19
|
+
|
|
20
|
+
expect(env).toEqual({
|
|
21
|
+
SPOTIFY_CLIENT_ID: 'abc123',
|
|
22
|
+
SPOTIFY_CLIENT_SECRET: 'def456',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('prefers explicit process env over file values', () => {
|
|
27
|
+
const credentials = resolveSpotifyCredentials(
|
|
28
|
+
{
|
|
29
|
+
SPOTIFY_CLIENT_ID: 'file-id',
|
|
30
|
+
SPOTIFY_CLIENT_SECRET: 'file-secret',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
SPOTIFY_CLIENT_ID: 'env-id',
|
|
34
|
+
SPOTIFY_CLIENT_SECRET: 'env-secret',
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(credentials).toEqual({
|
|
39
|
+
clientId: 'env-id',
|
|
40
|
+
clientSecret: 'env-secret',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('treats placeholder values as unconfigured credentials', () => {
|
|
45
|
+
expect(hasConfiguredSpotifyCredentials({
|
|
46
|
+
clientId: 'your_spotify_client_id_here',
|
|
47
|
+
clientSecret: 'your_spotify_client_secret_here',
|
|
48
|
+
})).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('throws a helpful CONFIG error for empty or placeholder credentials', () => {
|
|
52
|
+
expect(() => assertSpotifyCredentialsConfigured({
|
|
53
|
+
clientId: '',
|
|
54
|
+
clientSecret: '',
|
|
55
|
+
}, '/tmp/spotify.env')).toThrow(/Missing Spotify credentials/);
|
|
56
|
+
|
|
57
|
+
expect(() => assertSpotifyCredentialsConfigured({
|
|
58
|
+
clientId: 'your_spotify_client_id_here',
|
|
59
|
+
clientSecret: 'real-secret',
|
|
60
|
+
}, '/tmp/spotify.env')).toThrow(/Fill in SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('maps search payloads into stable track summaries', () => {
|
|
64
|
+
const results = mapSpotifyTrackResults({
|
|
65
|
+
tracks: {
|
|
66
|
+
items: [
|
|
67
|
+
{
|
|
68
|
+
name: 'Numb',
|
|
69
|
+
artists: [{ name: 'Linkin Park' }, { name: 'Jay-Z' }],
|
|
70
|
+
album: { name: 'Encore' },
|
|
71
|
+
uri: 'spotify:track:123',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(results).toEqual([
|
|
78
|
+
{
|
|
79
|
+
track: 'Numb',
|
|
80
|
+
artist: 'Linkin Park, Jay-Z',
|
|
81
|
+
album: 'Encore',
|
|
82
|
+
uri: 'spotify:track:123',
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
expect(getFirstSpotifyTrack({ tracks: { items: [] } })).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { CliError } from '../../errors.js';
|
|
2
|
+
|
|
3
|
+
export interface SpotifyCredentials {
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SpotifyTrackSummary {
|
|
9
|
+
track: string;
|
|
10
|
+
artist: string;
|
|
11
|
+
album: string;
|
|
12
|
+
uri: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SPOTIFY_PLACEHOLDER_PATTERNS = [
|
|
16
|
+
/^your_spotify_client_id_here$/i,
|
|
17
|
+
/^your_spotify_client_secret_here$/i,
|
|
18
|
+
/^your_.+_here$/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function parseDotEnv(content: string): Record<string, string> {
|
|
22
|
+
return Object.fromEntries(
|
|
23
|
+
content
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map(line => line.trim())
|
|
26
|
+
.filter(line => line && !line.startsWith('#') && line.includes('='))
|
|
27
|
+
.map(line => {
|
|
28
|
+
const index = line.indexOf('=');
|
|
29
|
+
return [line.slice(0, index).trim(), line.slice(index + 1).trim()] as [string, string];
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveSpotifyCredentials(
|
|
35
|
+
fileEnv: Record<string, string>,
|
|
36
|
+
processEnv: NodeJS.ProcessEnv = process.env,
|
|
37
|
+
): SpotifyCredentials {
|
|
38
|
+
return {
|
|
39
|
+
clientId: processEnv.SPOTIFY_CLIENT_ID || fileEnv.SPOTIFY_CLIENT_ID || '',
|
|
40
|
+
clientSecret: processEnv.SPOTIFY_CLIENT_SECRET || fileEnv.SPOTIFY_CLIENT_SECRET || '',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isPlaceholderCredential(value: string | null | undefined): boolean {
|
|
45
|
+
const normalized = value?.trim() || '';
|
|
46
|
+
if (!normalized) return false;
|
|
47
|
+
return SPOTIFY_PLACEHOLDER_PATTERNS.some(pattern => pattern.test(normalized));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hasConfiguredSpotifyCredentials(credentials: SpotifyCredentials): boolean {
|
|
51
|
+
return Boolean(credentials.clientId.trim()) &&
|
|
52
|
+
Boolean(credentials.clientSecret.trim()) &&
|
|
53
|
+
!isPlaceholderCredential(credentials.clientId) &&
|
|
54
|
+
!isPlaceholderCredential(credentials.clientSecret);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertSpotifyCredentialsConfigured(credentials: SpotifyCredentials, envFile: string): void {
|
|
58
|
+
if (hasConfiguredSpotifyCredentials(credentials)) return;
|
|
59
|
+
|
|
60
|
+
throw new CliError(
|
|
61
|
+
'CONFIG',
|
|
62
|
+
`Missing Spotify credentials.\n\n` +
|
|
63
|
+
`1. Go to https://developer.spotify.com/dashboard and create an app\n` +
|
|
64
|
+
`2. Add ${'http://127.0.0.1:8888/callback'} as a Redirect URI\n` +
|
|
65
|
+
`3. Copy your Client ID and Client Secret\n` +
|
|
66
|
+
`4. Open the file: ${envFile}\n` +
|
|
67
|
+
`5. Fill in SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET, then save\n` +
|
|
68
|
+
`6. Run: opencli spotify auth`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function mapSpotifyTrackResults(data: any): SpotifyTrackSummary[] {
|
|
73
|
+
const items = data?.tracks?.items;
|
|
74
|
+
if (!Array.isArray(items)) return [];
|
|
75
|
+
|
|
76
|
+
return items.map((track: any) => ({
|
|
77
|
+
track: track?.name || '',
|
|
78
|
+
artist: Array.isArray(track?.artists) ? track.artists.map((artist: any) => artist.name).join(', ') : '',
|
|
79
|
+
album: track?.album?.name || '',
|
|
80
|
+
uri: track?.uri || '',
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getFirstSpotifyTrack(data: any): { uri: string; name: string; artist: string } | null {
|
|
85
|
+
const track = mapSpotifyTrackResults(data)[0];
|
|
86
|
+
if (!track) return null;
|
|
87
|
+
return {
|
|
88
|
+
uri: track.uri,
|
|
89
|
+
name: track.track,
|
|
90
|
+
artist: track.artist,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
import { __test__, loadSubstackArchive, loadSubstackFeed } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function createPageMock(evaluateResult: unknown): IPage {
|
|
6
|
+
return {
|
|
7
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
9
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
getFormState: vi.fn().mockResolvedValue({}),
|
|
15
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
17
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
21
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
22
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
26
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
27
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
28
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('substack utils wait selectors', () => {
|
|
33
|
+
it('waits for both feed link shapes before scraping the feed', async () => {
|
|
34
|
+
const page = createPageMock([]);
|
|
35
|
+
|
|
36
|
+
await loadSubstackFeed(page, 'https://substack.com/', 5);
|
|
37
|
+
|
|
38
|
+
expect(page.wait).toHaveBeenCalledWith({
|
|
39
|
+
selector: __test__.FEED_POST_LINK_SELECTOR,
|
|
40
|
+
timeout: 5,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('waits for archive post links before scraping archive pages', async () => {
|
|
45
|
+
const page = createPageMock([]);
|
|
46
|
+
|
|
47
|
+
await loadSubstackArchive(page, 'https://example.substack.com', 5);
|
|
48
|
+
|
|
49
|
+
expect(page.wait).toHaveBeenCalledWith({
|
|
50
|
+
selector: __test__.ARCHIVE_POST_LINK_SELECTOR,
|
|
51
|
+
timeout: 5,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { CommandExecutionError } from '../../errors.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
3
|
|
|
4
|
+
const FEED_POST_LINK_SELECTOR = 'a[href*="/home/post/"], a[href*="/p/"]';
|
|
5
|
+
const ARCHIVE_POST_LINK_SELECTOR = 'a[href*="/p/"]';
|
|
6
|
+
|
|
4
7
|
export function buildSubstackBrowseUrl(category?: string): string {
|
|
5
8
|
if (!category || category === 'all') return 'https://substack.com/';
|
|
6
9
|
const slug = category === 'tech' ? 'technology' : category;
|
|
@@ -10,7 +13,7 @@ export function buildSubstackBrowseUrl(category?: string): string {
|
|
|
10
13
|
export async function loadSubstackFeed(page: IPage, url: string, limit: number): Promise<any[]> {
|
|
11
14
|
if (!page) throw new CommandExecutionError('Browser session required for substack feed');
|
|
12
15
|
await page.goto(url);
|
|
13
|
-
await page.wait({ selector:
|
|
16
|
+
await page.wait({ selector: FEED_POST_LINK_SELECTOR, timeout: 5 });
|
|
14
17
|
const data = await page.evaluate(`
|
|
15
18
|
(async () => {
|
|
16
19
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -79,7 +82,7 @@ export async function loadSubstackFeed(page: IPage, url: string, limit: number):
|
|
|
79
82
|
export async function loadSubstackArchive(page: IPage, baseUrl: string, limit: number): Promise<any[]> {
|
|
80
83
|
if (!page) throw new CommandExecutionError('Browser session required for substack archive');
|
|
81
84
|
await page.goto(`${baseUrl}/archive`);
|
|
82
|
-
await page.wait({ selector:
|
|
85
|
+
await page.wait({ selector: ARCHIVE_POST_LINK_SELECTOR, timeout: 5 });
|
|
83
86
|
const data = await page.evaluate(`
|
|
84
87
|
(async () => {
|
|
85
88
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -131,3 +134,8 @@ export async function loadSubstackArchive(page: IPage, baseUrl: string, limit: n
|
|
|
131
134
|
|
|
132
135
|
return Array.isArray(data) ? data : [];
|
|
133
136
|
}
|
|
137
|
+
|
|
138
|
+
export const __test__ = {
|
|
139
|
+
FEED_POST_LINK_SELECTOR,
|
|
140
|
+
ARCHIVE_POST_LINK_SELECTOR,
|
|
141
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Strategy, getRegistry } from '../../registry.js';
|
|
3
|
+
import './hot.js';
|
|
4
|
+
import './posts.js';
|
|
5
|
+
import './read.js';
|
|
6
|
+
import './search.js';
|
|
7
|
+
|
|
8
|
+
describe('tieba commands', () => {
|
|
9
|
+
it('registers all tieba commands as TypeScript adapters', () => {
|
|
10
|
+
const hot = getRegistry().get('tieba/hot');
|
|
11
|
+
const posts = getRegistry().get('tieba/posts');
|
|
12
|
+
const search = getRegistry().get('tieba/search');
|
|
13
|
+
const read = getRegistry().get('tieba/read');
|
|
14
|
+
|
|
15
|
+
expect(hot).toBeDefined();
|
|
16
|
+
expect(posts).toBeDefined();
|
|
17
|
+
expect(search).toBeDefined();
|
|
18
|
+
expect(read).toBeDefined();
|
|
19
|
+
expect(typeof hot?.func).toBe('function');
|
|
20
|
+
expect(typeof posts?.func).toBe('function');
|
|
21
|
+
expect(typeof search?.func).toBe('function');
|
|
22
|
+
expect(typeof read?.func).toBe('function');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps the intended browser strategies', () => {
|
|
26
|
+
const hot = getRegistry().get('tieba/hot');
|
|
27
|
+
const posts = getRegistry().get('tieba/posts');
|
|
28
|
+
const search = getRegistry().get('tieba/search');
|
|
29
|
+
const read = getRegistry().get('tieba/read');
|
|
30
|
+
|
|
31
|
+
expect(hot?.strategy).toBe(Strategy.PUBLIC);
|
|
32
|
+
expect(posts?.strategy).toBe(Strategy.COOKIE);
|
|
33
|
+
expect(search?.strategy).toBe(Strategy.COOKIE);
|
|
34
|
+
expect(read?.strategy).toBe(Strategy.COOKIE);
|
|
35
|
+
expect(hot?.browser).toBe(true);
|
|
36
|
+
expect(posts?.browser).toBe(true);
|
|
37
|
+
expect(search?.browser).toBe(true);
|
|
38
|
+
expect(read?.browser).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('keeps the public limit contract at 20 items for list commands', () => {
|
|
42
|
+
const hot = getRegistry().get('tieba/hot');
|
|
43
|
+
const posts = getRegistry().get('tieba/posts');
|
|
44
|
+
const search = getRegistry().get('tieba/search');
|
|
45
|
+
|
|
46
|
+
expect(hot?.args.find((arg) => arg.name === 'limit')?.default).toBe(20);
|
|
47
|
+
expect(posts?.args.find((arg) => arg.name === 'limit')?.default).toBe(20);
|
|
48
|
+
expect(search?.args.find((arg) => arg.name === 'limit')?.default).toBe(20);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects tieba read results when navigation lands on the wrong page number', async () => {
|
|
52
|
+
const read = getRegistry().get('tieba/read');
|
|
53
|
+
expect(read).toBeDefined();
|
|
54
|
+
expect(typeof read?.func).toBe('function');
|
|
55
|
+
const run = read?.func;
|
|
56
|
+
if (!run) throw new Error('tieba/read did not register a handler');
|
|
57
|
+
const page = {
|
|
58
|
+
goto: async () => undefined,
|
|
59
|
+
evaluate: async () => ({
|
|
60
|
+
pageMeta: {
|
|
61
|
+
pathname: '/p/10163164720',
|
|
62
|
+
pn: '1',
|
|
63
|
+
},
|
|
64
|
+
mainPost: {
|
|
65
|
+
title: '测试帖子',
|
|
66
|
+
author: '作者',
|
|
67
|
+
contentText: '正文',
|
|
68
|
+
structuredText: '',
|
|
69
|
+
visibleTime: '2026-03-29 12:00',
|
|
70
|
+
structuredTime: 0,
|
|
71
|
+
hasMedia: false,
|
|
72
|
+
},
|
|
73
|
+
replies: [],
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await expect(run(page as never, {
|
|
78
|
+
id: '10163164720',
|
|
79
|
+
page: 2,
|
|
80
|
+
limit: 5,
|
|
81
|
+
})).rejects.toMatchObject({
|
|
82
|
+
code: 'EMPTY_RESULT',
|
|
83
|
+
hint: expect.stringMatching(/requested page/i),
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EmptyResultError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import { normalizeTiebaLimit } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'tieba',
|
|
7
|
+
name: 'hot',
|
|
8
|
+
description: 'Tieba hot topics',
|
|
9
|
+
domain: 'tieba.baidu.com',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of items to return' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['rank', 'title', 'discussions', 'description'],
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
const limit = normalizeTiebaLimit(kwargs.limit);
|
|
19
|
+
// Use the default browser settle path so we do not scrape the previous page.
|
|
20
|
+
await page.goto('https://tieba.baidu.com/hottopic/browse/topicList?res_type=1');
|
|
21
|
+
|
|
22
|
+
const raw = await page.evaluate(`(() => {
|
|
23
|
+
const items = document.querySelectorAll('li.topic-top-item');
|
|
24
|
+
return Array.from(items).map((item) => {
|
|
25
|
+
const titleEl = item.querySelector('a.topic-text');
|
|
26
|
+
const numEl = item.querySelector('span.topic-num');
|
|
27
|
+
const descEl = item.querySelector('p.topic-top-item-desc');
|
|
28
|
+
const href = titleEl?.getAttribute('href') || '';
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
title: titleEl?.textContent?.trim() || '',
|
|
32
|
+
discussions: numEl?.textContent?.trim() || '',
|
|
33
|
+
description: descEl?.textContent?.trim() || '',
|
|
34
|
+
url: href.startsWith('http') ? href : 'https://tieba.baidu.com' + href,
|
|
35
|
+
};
|
|
36
|
+
}).filter((item) => item.title).slice(0, ${limit});
|
|
37
|
+
})()`);
|
|
38
|
+
|
|
39
|
+
const items = Array.isArray(raw) ? raw as Array<Record<string, string>> : [];
|
|
40
|
+
if (!items.length) {
|
|
41
|
+
throw new EmptyResultError('tieba hot', 'Tieba may have blocked the hot page, or the DOM structure may have changed');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return items.map((item, index) => ({
|
|
45
|
+
rank: index + 1,
|
|
46
|
+
title: item.title || '',
|
|
47
|
+
discussions: item.discussions || '',
|
|
48
|
+
description: item.description || '',
|
|
49
|
+
url: item.url || '',
|
|
50
|
+
}));
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { EmptyResultError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy, type CommandArgs } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
import {
|
|
5
|
+
buildTiebaPostCardsFromPagePc,
|
|
6
|
+
buildTiebaPostItems,
|
|
7
|
+
normalizeTiebaLimit,
|
|
8
|
+
signTiebaPcParams,
|
|
9
|
+
type RawTiebaPagePcFeedEntry,
|
|
10
|
+
} from './utils.js';
|
|
11
|
+
|
|
12
|
+
interface TiebaPagePcResponse {
|
|
13
|
+
error_code?: number;
|
|
14
|
+
page_data?: {
|
|
15
|
+
feed_list?: RawTiebaPagePcFeedEntry[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getForumPageNumber(kwargs: CommandArgs): number {
|
|
20
|
+
return Math.max(1, Number(kwargs.page || 1));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getForumUrl(kwargs: CommandArgs): string {
|
|
24
|
+
const forum = String(kwargs.forum || '');
|
|
25
|
+
return `https://tieba.baidu.com/f?kw=${encodeURIComponent(forum)}&ie=utf-8&pn=${(getForumPageNumber(kwargs) - 1) * 50}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Rebuild the signed page_pc request instead of scraping only the visible thread cards.
|
|
30
|
+
*/
|
|
31
|
+
function buildTiebaPagePcParams(kwargs: CommandArgs, limit: number): Record<string, string> {
|
|
32
|
+
return {
|
|
33
|
+
kw: encodeURIComponent(String(kwargs.forum || '')),
|
|
34
|
+
pn: String(getForumPageNumber(kwargs)),
|
|
35
|
+
sort_type: '-1',
|
|
36
|
+
is_newfrs: '1',
|
|
37
|
+
is_newfeed: '1',
|
|
38
|
+
rn: '30',
|
|
39
|
+
rn_need: String(Math.min(Math.max(limit + 10, 10), 30)),
|
|
40
|
+
tbs: '',
|
|
41
|
+
subapp_type: 'pc',
|
|
42
|
+
_client_type: '20',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Tieba expects the signed forum-list request to be replayed with the browser's cookies.
|
|
48
|
+
*/
|
|
49
|
+
async function fetchTiebaPagePc(page: IPage, kwargs: CommandArgs, limit: number): Promise<TiebaPagePcResponse> {
|
|
50
|
+
await page.goto(getForumUrl(kwargs), { waitUntil: 'none' });
|
|
51
|
+
await page.wait(2);
|
|
52
|
+
|
|
53
|
+
const params = buildTiebaPagePcParams(kwargs, limit);
|
|
54
|
+
const cookies = await page.getCookies({ domain: 'tieba.baidu.com' });
|
|
55
|
+
const cookieHeader = cookies.map((item) => `${item.name}=${item.value}`).join('; ');
|
|
56
|
+
const body = new URLSearchParams({
|
|
57
|
+
...params,
|
|
58
|
+
sign: signTiebaPcParams(params),
|
|
59
|
+
}).toString();
|
|
60
|
+
|
|
61
|
+
const response = await fetch('https://tieba.baidu.com/c/f/frs/page_pc', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
65
|
+
cookie: cookieHeader,
|
|
66
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
67
|
+
referer: getForumUrl(kwargs),
|
|
68
|
+
'user-agent': 'Mozilla/5.0',
|
|
69
|
+
},
|
|
70
|
+
body,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const text = await response.text();
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text) as TiebaPagePcResponse;
|
|
76
|
+
} catch {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cli({
|
|
82
|
+
site: 'tieba',
|
|
83
|
+
name: 'posts',
|
|
84
|
+
description: 'Browse posts in a tieba forum',
|
|
85
|
+
domain: 'tieba.baidu.com',
|
|
86
|
+
strategy: Strategy.COOKIE,
|
|
87
|
+
browser: true,
|
|
88
|
+
navigateBefore: false,
|
|
89
|
+
args: [
|
|
90
|
+
{ name: 'forum', positional: true, required: true, type: 'string', help: 'Forum name in Chinese' },
|
|
91
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
92
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of items to return' },
|
|
93
|
+
],
|
|
94
|
+
columns: ['rank', 'title', 'author', 'replies'],
|
|
95
|
+
func: async (page, kwargs) => {
|
|
96
|
+
const limit = normalizeTiebaLimit(kwargs.limit);
|
|
97
|
+
const payload = await fetchTiebaPagePc(page, kwargs, limit);
|
|
98
|
+
const rawFeeds = Array.isArray(payload.page_data?.feed_list) ? payload.page_data.feed_list : [];
|
|
99
|
+
const rawCards = buildTiebaPostCardsFromPagePc(rawFeeds);
|
|
100
|
+
const items = buildTiebaPostItems(rawCards, limit);
|
|
101
|
+
|
|
102
|
+
if (!items.length || payload.error_code) {
|
|
103
|
+
throw new EmptyResultError('tieba posts', 'Tieba may have blocked the forum page, or the DOM structure may have changed');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return items;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { EmptyResultError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy, type CommandArgs } from '../../registry.js';
|
|
3
|
+
import { buildTiebaReadItems, type RawTiebaReadPayload } from './utils.js';
|
|
4
|
+
|
|
5
|
+
type TiebaReadPageMeta = {
|
|
6
|
+
pathname?: string;
|
|
7
|
+
pn?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type RawTiebaReadPagePayload = RawTiebaReadPayload & {
|
|
11
|
+
pageMeta?: TiebaReadPageMeta;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getThreadUrl(kwargs: CommandArgs): string {
|
|
15
|
+
const threadId = String(kwargs.id || '');
|
|
16
|
+
const pageNumber = Math.max(1, Number(kwargs.page || 1));
|
|
17
|
+
return `https://tieba.baidu.com/p/${encodeURIComponent(threadId)}?pn=${pageNumber}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensure the browser actually landed on the requested thread page before we trust the DOM.
|
|
22
|
+
*/
|
|
23
|
+
function assertTiebaReadTargetPage(raw: RawTiebaReadPagePayload, kwargs: CommandArgs): void {
|
|
24
|
+
const expectedThreadId = String(kwargs.id || '').trim();
|
|
25
|
+
const expectedPageNumber = Math.max(1, Number(kwargs.page || 1));
|
|
26
|
+
const pathname = String(raw.pageMeta?.pathname || '').trim();
|
|
27
|
+
const actualThreadId = pathname.match(/^\/p\/(\d+)/)?.[1] || '';
|
|
28
|
+
const actualPn = String(raw.pageMeta?.pn || '').trim();
|
|
29
|
+
|
|
30
|
+
if (!actualThreadId || actualThreadId !== expectedThreadId) {
|
|
31
|
+
throw new EmptyResultError('tieba read', 'Tieba did not land on the requested thread page');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (expectedPageNumber > 1 && actualPn !== String(expectedPageNumber)) {
|
|
35
|
+
throw new EmptyResultError('tieba read', 'Tieba did not land on the requested page');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildExtractReadEvaluate(): string {
|
|
40
|
+
return `
|
|
41
|
+
(async () => {
|
|
42
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
43
|
+
const waitFor = async (predicate, timeoutMs = 4000) => {
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
while (Date.now() - start < timeoutMs) {
|
|
46
|
+
if (predicate()) return true;
|
|
47
|
+
await wait(100);
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
};
|
|
51
|
+
const normalizeText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
52
|
+
const getVueProps = (element) => {
|
|
53
|
+
const vue = element && element.__vue__ ? element.__vue__ : null;
|
|
54
|
+
return vue ? (vue._props || vue.$props || {}) : {};
|
|
55
|
+
};
|
|
56
|
+
const extractStructuredText = (content) => {
|
|
57
|
+
if (!Array.isArray(content)) return '';
|
|
58
|
+
return content
|
|
59
|
+
.map((part) => (part && typeof part === 'object' && typeof part.text === 'string') ? part.text : '')
|
|
60
|
+
.join('')
|
|
61
|
+
.replace(/\\s+/g, ' ')
|
|
62
|
+
.trim();
|
|
63
|
+
};
|
|
64
|
+
const parseFloor = (text) => {
|
|
65
|
+
const match = (text || '').match(/第(\\d+)楼/);
|
|
66
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
const hasMainTree = document.querySelector('.pb-title-wrap.pc-pb-title') || document.querySelector('.pb-content-wrap');
|
|
71
|
+
return Boolean(hasMainTree || document.querySelector('.pb-comment-item'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const titleNode = document.querySelector('.pb-title-wrap.pc-pb-title');
|
|
75
|
+
const titleProps = getVueProps(titleNode);
|
|
76
|
+
const mainUser = document.querySelector('.head-line.user-info:not(.no-extra-margin)');
|
|
77
|
+
const mainUserProps = getVueProps(mainUser);
|
|
78
|
+
const contentWrap = document.querySelector('.pb-content-wrap');
|
|
79
|
+
const contentProps = getVueProps(contentWrap);
|
|
80
|
+
const structuredContent = Array.isArray(contentProps.content) ? contentProps.content : [];
|
|
81
|
+
const visibleContent = normalizeText(
|
|
82
|
+
contentWrap?.querySelector('.pb-content-item .text')?.textContent
|
|
83
|
+
|| contentWrap?.querySelector('.text')?.textContent
|
|
84
|
+
|| contentWrap?.textContent
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
pageMeta: {
|
|
89
|
+
pathname: window.location.pathname || '',
|
|
90
|
+
pn: new URLSearchParams(window.location.search).get('pn') || '',
|
|
91
|
+
},
|
|
92
|
+
mainPost: {
|
|
93
|
+
title: typeof titleProps.title === 'string' && titleProps.title.trim()
|
|
94
|
+
? titleProps.title.trim()
|
|
95
|
+
: normalizeText(titleNode?.textContent).replace(/-百度贴吧$/, '').trim(),
|
|
96
|
+
author: normalizeText(
|
|
97
|
+
mainUser?.querySelector('.head-name')?.textContent
|
|
98
|
+
|| mainUser?.querySelector('.name-info .head-name')?.textContent
|
|
99
|
+
|| ''
|
|
100
|
+
),
|
|
101
|
+
fallbackAuthor: mainUserProps?.userShowInfo?.[0]?.text?.text || '',
|
|
102
|
+
contentText: visibleContent,
|
|
103
|
+
structuredText: extractStructuredText(structuredContent),
|
|
104
|
+
visibleTime: (() => {
|
|
105
|
+
const userText = normalizeText(mainUser?.textContent);
|
|
106
|
+
const match = userText.match(/(刚刚|昨天|前天|\\d+\\s*(?:分钟|小时|天)前|\\d{2}-\\d{2}(?:\\s+\\d{2}:\\d{2})?|\\d{4}-\\d{2}-\\d{2}(?:\\s+\\d{2}:\\d{2})?)/);
|
|
107
|
+
return match ? match[1].trim() : '';
|
|
108
|
+
})(),
|
|
109
|
+
structuredTime: mainUserProps?.descInfo?.time || 0,
|
|
110
|
+
hasMedia: structuredContent.length > 0 && !extractStructuredText(structuredContent),
|
|
111
|
+
},
|
|
112
|
+
replies: Array.from(document.querySelectorAll('.pb-comment-item')).map((item) => {
|
|
113
|
+
const meta = item.querySelector('.comment-desc-left')?.textContent?.replace(/\\s+/g, ' ').trim() || '';
|
|
114
|
+
return {
|
|
115
|
+
floor: parseFloor(meta),
|
|
116
|
+
author: item.querySelector('.head-name')?.textContent?.trim() || '',
|
|
117
|
+
content: item.querySelector('.comment-content .pb-content-item .text')?.textContent?.replace(/\\s+/g, ' ').trim() || '',
|
|
118
|
+
time: meta,
|
|
119
|
+
};
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
})()
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
cli({
|
|
127
|
+
site: 'tieba',
|
|
128
|
+
name: 'read',
|
|
129
|
+
description: 'Read a tieba thread',
|
|
130
|
+
domain: 'tieba.baidu.com',
|
|
131
|
+
strategy: Strategy.COOKIE,
|
|
132
|
+
browser: true,
|
|
133
|
+
navigateBefore: false,
|
|
134
|
+
args: [
|
|
135
|
+
{ name: 'id', positional: true, required: true, type: 'string', help: 'Thread ID' },
|
|
136
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
137
|
+
{ name: 'limit', type: 'int', default: 30, help: 'Number of replies to return' },
|
|
138
|
+
],
|
|
139
|
+
columns: ['floor', 'author', 'content', 'time'],
|
|
140
|
+
func: async (page, kwargs) => {
|
|
141
|
+
const pageNumber = Math.max(1, Number(kwargs.page || 1));
|
|
142
|
+
// Use the browser's normal settle path so we do not scrape stale DOM from the previous tab state.
|
|
143
|
+
await page.goto(getThreadUrl(kwargs));
|
|
144
|
+
|
|
145
|
+
const raw = (await page.evaluate(buildExtractReadEvaluate()) || {}) as RawTiebaReadPagePayload;
|
|
146
|
+
assertTiebaReadTargetPage(raw, kwargs);
|
|
147
|
+
|
|
148
|
+
const items = buildTiebaReadItems(raw, {
|
|
149
|
+
limit: kwargs.limit,
|
|
150
|
+
includeMainPost: pageNumber === 1,
|
|
151
|
+
});
|
|
152
|
+
if (!items.length) {
|
|
153
|
+
throw new EmptyResultError('tieba read', 'Tieba may have blocked the thread page, or the DOM structure may have changed');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return items;
|
|
157
|
+
},
|
|
158
|
+
});
|