@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,164 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
import { getRegistry } from '../../registry.js';
|
|
4
|
+
import { parseNoteId, buildNoteUrl } from './note-helpers.js';
|
|
5
|
+
import './note.js';
|
|
6
|
+
|
|
7
|
+
function createPageMock(evaluateResult: any): IPage {
|
|
8
|
+
return {
|
|
9
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
11
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
17
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
19
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
23
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
24
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
28
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
29
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
30
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('parseNoteId', () => {
|
|
35
|
+
it('extracts ID from /explore/ URL', () => {
|
|
36
|
+
expect(parseNoteId('https://www.xiaohongshu.com/explore/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('extracts ID from /search_result/ URL with query params', () => {
|
|
40
|
+
expect(parseNoteId('https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc')).toBe('69c131c9000000002800be4c');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('extracts ID from /note/ URL', () => {
|
|
44
|
+
expect(parseNoteId('https://www.xiaohongshu.com/note/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns raw string when no URL pattern matches', () => {
|
|
48
|
+
expect(parseNoteId('69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('trims whitespace', () => {
|
|
52
|
+
expect(parseNoteId(' 69c131c9000000002800be4c ')).toBe('69c131c9000000002800be4c');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('buildNoteUrl', () => {
|
|
57
|
+
it('returns full URL as-is when given https URL', () => {
|
|
58
|
+
const url = 'https://www.xiaohongshu.com/search_result/abc123?xsec_token=tok';
|
|
59
|
+
expect(buildNoteUrl(url)).toBe(url);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('constructs /explore/ URL for bare note ID', () => {
|
|
63
|
+
expect(buildNoteUrl('abc123')).toBe('https://www.xiaohongshu.com/explore/abc123');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('xiaohongshu note', () => {
|
|
68
|
+
const command = getRegistry().get('xiaohongshu/note');
|
|
69
|
+
|
|
70
|
+
it('is registered', () => {
|
|
71
|
+
expect(command).toBeDefined();
|
|
72
|
+
expect(command!.func).toBeTypeOf('function');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns note content as field/value rows', async () => {
|
|
76
|
+
const page = createPageMock({
|
|
77
|
+
loginWall: false,
|
|
78
|
+
notFound: false,
|
|
79
|
+
title: '尚界Z7实车体验',
|
|
80
|
+
desc: '今天去看了实车,外观很帅',
|
|
81
|
+
author: '小红薯用户',
|
|
82
|
+
likes: '257',
|
|
83
|
+
collects: '98',
|
|
84
|
+
comments: '45',
|
|
85
|
+
tags: ['#尚界Z7', '#鸿蒙智行'],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = (await command!.func!(page, { 'note-id': '69c131c9000000002800be4c' })) as any[];
|
|
89
|
+
|
|
90
|
+
expect((page.goto as any).mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
|
|
91
|
+
expect(result).toEqual([
|
|
92
|
+
{ field: 'title', value: '尚界Z7实车体验' },
|
|
93
|
+
{ field: 'author', value: '小红薯用户' },
|
|
94
|
+
{ field: 'content', value: '今天去看了实车,外观很帅' },
|
|
95
|
+
{ field: 'likes', value: '257' },
|
|
96
|
+
{ field: 'collects', value: '98' },
|
|
97
|
+
{ field: 'comments', value: '45' },
|
|
98
|
+
{ field: 'tags', value: '#尚界Z7, #鸿蒙智行' },
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('parses note ID from full /explore/ URL', async () => {
|
|
103
|
+
const page = createPageMock({
|
|
104
|
+
loginWall: false, notFound: false,
|
|
105
|
+
title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await command!.func!(page, {
|
|
109
|
+
'note-id': 'https://www.xiaohongshu.com/explore/69c131c9000000002800be4c?xsec_token=abc',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect((page.goto as any).mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('preserves full search_result URL with xsec_token for navigation', async () => {
|
|
116
|
+
const page = createPageMock({
|
|
117
|
+
loginWall: false, notFound: false,
|
|
118
|
+
title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const fullUrl = 'https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc';
|
|
122
|
+
await command!.func!(page, { 'note-id': fullUrl });
|
|
123
|
+
|
|
124
|
+
// Should navigate to the full URL as-is, not strip the token
|
|
125
|
+
expect((page.goto as any).mock.calls[0][0]).toBe(fullUrl);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('throws AuthRequiredError on login wall', async () => {
|
|
129
|
+
const page = createPageMock({ loginWall: true, notFound: false });
|
|
130
|
+
|
|
131
|
+
await expect(command!.func!(page, { 'note-id': 'abc123' })).rejects.toThrow('Note content requires login');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('throws EmptyResultError when note is not found', async () => {
|
|
135
|
+
const page = createPageMock({ loginWall: false, notFound: true });
|
|
136
|
+
|
|
137
|
+
await expect(command!.func!(page, { 'note-id': 'abc123' })).rejects.toThrow('returned no data');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('normalizes placeholder text to 0 for zero-count metrics', async () => {
|
|
141
|
+
const page = createPageMock({
|
|
142
|
+
loginWall: false, notFound: false,
|
|
143
|
+
title: 'New note', desc: 'Just posted', author: 'Author',
|
|
144
|
+
likes: '赞', collects: '收藏', comments: '评论', tags: [],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = (await command!.func!(page, { 'note-id': 'abc123' })) as any[];
|
|
148
|
+
expect(result.find((r: any) => r.field === 'likes')!.value).toBe('0');
|
|
149
|
+
expect(result.find((r: any) => r.field === 'collects')!.value).toBe('0');
|
|
150
|
+
expect(result.find((r: any) => r.field === 'comments')!.value).toBe('0');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('omits tags row when no tags present', async () => {
|
|
154
|
+
const page = createPageMock({
|
|
155
|
+
loginWall: false, notFound: false,
|
|
156
|
+
title: 'No tags', desc: 'Content', author: 'Author',
|
|
157
|
+
likes: '1', collects: '2', comments: '3', tags: [],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = (await command!.func!(page, { 'note-id': 'abc123' })) as any[];
|
|
161
|
+
expect(result.find((r: any) => r.field === 'tags')).toBeUndefined();
|
|
162
|
+
expect(result).toHaveLength(6);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
|
|
8
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
10
|
+
import { parseNoteId, buildNoteUrl } from './note-helpers.js';
|
|
11
|
+
|
|
12
|
+
cli({
|
|
13
|
+
site: 'xiaohongshu',
|
|
14
|
+
name: 'note',
|
|
15
|
+
description: '获取小红书笔记正文和互动数据',
|
|
16
|
+
domain: 'www.xiaohongshu.com',
|
|
17
|
+
strategy: Strategy.COOKIE,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['field', 'value'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const raw = String(kwargs['note-id']);
|
|
24
|
+
const noteId = parseNoteId(raw);
|
|
25
|
+
const url = buildNoteUrl(raw);
|
|
26
|
+
|
|
27
|
+
await page.goto(url);
|
|
28
|
+
await page.wait(3);
|
|
29
|
+
|
|
30
|
+
const data = await page.evaluate(`
|
|
31
|
+
(() => {
|
|
32
|
+
const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
|
|
33
|
+
const notFound = /页面不见了|笔记不存在|无法浏览/.test(document.body.innerText || '')
|
|
34
|
+
|
|
35
|
+
const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
36
|
+
|
|
37
|
+
const title = clean(document.querySelector('#detail-title, .title'))
|
|
38
|
+
const desc = clean(document.querySelector('#detail-desc, .desc, .note-text'))
|
|
39
|
+
const author = clean(document.querySelector('.username, .author-wrapper .name'))
|
|
40
|
+
const likes = clean(document.querySelector('.like-wrapper .count'))
|
|
41
|
+
const collects = clean(document.querySelector('.collect-wrapper .count'))
|
|
42
|
+
const comments = clean(document.querySelector('.chat-wrapper .count'))
|
|
43
|
+
|
|
44
|
+
// Try to extract tags/topics
|
|
45
|
+
const tags = []
|
|
46
|
+
document.querySelectorAll('#detail-desc a.tag, #detail-desc a[href*="search_result"]').forEach(el => {
|
|
47
|
+
const t = (el.textContent || '').trim()
|
|
48
|
+
if (t) tags.push(t)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return { loginWall, notFound, title, desc, author, likes, collects, comments, tags }
|
|
52
|
+
})()
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
if (!data || typeof data !== 'object') {
|
|
56
|
+
throw new EmptyResultError('xiaohongshu/note', 'Unexpected evaluate response');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ((data as any).loginWall) {
|
|
60
|
+
throw new AuthRequiredError('www.xiaohongshu.com', 'Note content requires login');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ((data as any).notFound) {
|
|
64
|
+
throw new EmptyResultError('xiaohongshu/note', `Note ${noteId} not found or unavailable — it may have been deleted or restricted`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const d = data as any;
|
|
68
|
+
// XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
|
|
69
|
+
// normalize to '0' unless the value looks numeric.
|
|
70
|
+
const numOrZero = (v: string) => /^\d+/.test(v) ? v : '0';
|
|
71
|
+
const rows = [
|
|
72
|
+
{ field: 'title', value: d.title || '' },
|
|
73
|
+
{ field: 'author', value: d.author || '' },
|
|
74
|
+
{ field: 'content', value: d.desc || '' },
|
|
75
|
+
{ field: 'likes', value: numOrZero(d.likes || '') },
|
|
76
|
+
{ field: 'collects', value: numOrZero(d.collects || '') },
|
|
77
|
+
{ field: 'comments', value: numOrZero(d.comments || '') },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
if (d.tags?.length) {
|
|
81
|
+
rows.push({ field: 'tags', value: d.tags.join(', ') });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return rows;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
@@ -8,7 +8,7 @@ import { getRegistry } from '../../registry.js';
|
|
|
8
8
|
import type { IPage } from '../../types.js';
|
|
9
9
|
import './publish.js';
|
|
10
10
|
|
|
11
|
-
function createPageMock(evaluateResults: any[]): IPage {
|
|
11
|
+
function createPageMock(evaluateResults: any[], overrides: Partial<IPage> = {}): IPage {
|
|
12
12
|
const evaluate = vi.fn();
|
|
13
13
|
for (const result of evaluateResults) {
|
|
14
14
|
evaluate.mockResolvedValueOnce(result);
|
|
@@ -37,10 +37,88 @@ function createPageMock(evaluateResults: any[]): IPage {
|
|
|
37
37
|
getCookies: vi.fn().mockResolvedValue([]),
|
|
38
38
|
screenshot: vi.fn().mockResolvedValue(''),
|
|
39
39
|
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
...overrides,
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
describe('xiaohongshu publish', () => {
|
|
45
|
+
it('prefers CDP setFileInput upload when the page supports it', async () => {
|
|
46
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
47
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
48
|
+
|
|
49
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
50
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
51
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
52
|
+
|
|
53
|
+
const setFileInput = vi.fn().mockResolvedValue(undefined);
|
|
54
|
+
const page = createPageMock([
|
|
55
|
+
'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
|
|
56
|
+
{ ok: true, target: '上传图文', text: '上传图文' },
|
|
57
|
+
{ state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
|
|
58
|
+
'input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]',
|
|
59
|
+
false,
|
|
60
|
+
true,
|
|
61
|
+
{ ok: true, sel: 'input[maxlength="20"]' },
|
|
62
|
+
{ ok: true, sel: '[contenteditable="true"][class*="content"]' },
|
|
63
|
+
true,
|
|
64
|
+
'https://creator.xiaohongshu.com/publish/success',
|
|
65
|
+
'发布成功',
|
|
66
|
+
], {
|
|
67
|
+
setFileInput,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await cmd!.func!(page, {
|
|
71
|
+
title: 'CDP上传优先',
|
|
72
|
+
content: '优先走 setFileInput 主路径',
|
|
73
|
+
images: imagePath,
|
|
74
|
+
topics: '',
|
|
75
|
+
draft: false,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(setFileInput).toHaveBeenCalledWith(
|
|
79
|
+
[imagePath],
|
|
80
|
+
expect.stringContaining('input[type="file"][accept*="image"]'),
|
|
81
|
+
);
|
|
82
|
+
const evaluateCalls = (page.evaluate as any).mock.calls.map((args: any[]) => String(args[0]));
|
|
83
|
+
expect(evaluateCalls.some((code: string) => code.includes('atob(img.base64)'))).toBe(false);
|
|
84
|
+
expect(result).toEqual([
|
|
85
|
+
{
|
|
86
|
+
status: '✅ 发布成功',
|
|
87
|
+
detail: '"CDP上传优先" · 1张图片 · 发布成功',
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('fails fast when only a generic file input exists on the page', async () => {
|
|
93
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
94
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
95
|
+
|
|
96
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
97
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
98
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
99
|
+
|
|
100
|
+
const setFileInput = vi.fn().mockResolvedValue(undefined);
|
|
101
|
+
const page = createPageMock([
|
|
102
|
+
'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
|
|
103
|
+
{ ok: true, target: '上传图文', text: '上传图文' },
|
|
104
|
+
{ state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
|
|
105
|
+
null,
|
|
106
|
+
], {
|
|
107
|
+
setFileInput,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await expect(cmd!.func!(page, {
|
|
111
|
+
title: '不要走泛化上传',
|
|
112
|
+
content: 'generic file input 应该直接报错',
|
|
113
|
+
images: imagePath,
|
|
114
|
+
topics: '',
|
|
115
|
+
draft: false,
|
|
116
|
+
})).rejects.toThrow('Image injection failed: No file input found on page');
|
|
117
|
+
|
|
118
|
+
expect(setFileInput).not.toHaveBeenCalled();
|
|
119
|
+
expect(page.screenshot).toHaveBeenCalledWith({ path: '/tmp/xhs_publish_upload_debug.png' });
|
|
120
|
+
});
|
|
121
|
+
|
|
44
122
|
it('selects the image-text tab and publishes successfully', async () => {
|
|
45
123
|
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
46
124
|
expect(cmd?.func).toBeTypeOf('function');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Flow:
|
|
5
5
|
* 1. Navigate to creator publish page
|
|
6
|
-
* 2. Upload images via
|
|
6
|
+
* 2. Upload images via CDP DOM.setFileInputFiles (with base64 fallback)
|
|
7
7
|
* 3. Fill title and body text
|
|
8
8
|
* 4. Add topic hashtags
|
|
9
9
|
* 5. Publish (or save as draft)
|
|
@@ -43,44 +43,98 @@ const TITLE_SELECTORS = [
|
|
|
43
43
|
'input[maxlength]',
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
const SUPPORTED_EXTENSIONS: Record<string, string> = {
|
|
47
|
+
'.jpg': 'image/jpeg',
|
|
48
|
+
'.jpeg': 'image/jpeg',
|
|
49
|
+
'.png': 'image/png',
|
|
50
|
+
'.gif': 'image/gif',
|
|
51
|
+
'.webp': 'image/webp',
|
|
52
|
+
};
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
55
|
+
* Validate image paths: check existence and extension.
|
|
56
|
+
* Returns resolved absolute paths.
|
|
51
57
|
*/
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
const mimeType = mimeMap[ext];
|
|
64
|
-
if (!mimeType) throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
|
|
65
|
-
const base64 = fs.readFileSync(absPath).toString('base64');
|
|
66
|
-
return { name: path.basename(absPath), mimeType, base64 };
|
|
58
|
+
function validateImagePaths(filePaths: string[]): string[] {
|
|
59
|
+
return filePaths.map((filePath) => {
|
|
60
|
+
const absPath = path.resolve(filePath);
|
|
61
|
+
if (!fs.existsSync(absPath)) throw new Error(`Image file not found: ${absPath}`);
|
|
62
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
63
|
+
if (!SUPPORTED_EXTENSIONS[ext]) {
|
|
64
|
+
throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
|
|
65
|
+
}
|
|
66
|
+
return absPath;
|
|
67
|
+
});
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/** CSS selector for image-accepting file inputs. */
|
|
71
|
+
const IMAGE_INPUT_SELECTOR = 'input[type="file"][accept*="image"],'
|
|
72
|
+
+ 'input[type="file"][accept*=".jpg"],'
|
|
73
|
+
+ 'input[type="file"][accept*=".jpeg"],'
|
|
74
|
+
+ 'input[type="file"][accept*=".png"],'
|
|
75
|
+
+ 'input[type="file"][accept*=".gif"],'
|
|
76
|
+
+ 'input[type="file"][accept*=".webp"]';
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* a synthetic 'change' event on the input element.
|
|
79
|
+
* Upload images via CDP DOM.setFileInputFiles — Chrome reads files directly
|
|
80
|
+
* from the local filesystem, avoiding base64 payload size limits.
|
|
73
81
|
*
|
|
74
|
-
*
|
|
82
|
+
* Falls back to the legacy base64 DataTransfer approach if the extension
|
|
83
|
+
* does not support set-file-input (e.g. older extension version).
|
|
75
84
|
*/
|
|
76
|
-
async function
|
|
85
|
+
async function uploadImages(
|
|
86
|
+
page: IPage,
|
|
87
|
+
absPaths: string[],
|
|
88
|
+
): Promise<{ ok: boolean; count: number; error?: string }> {
|
|
89
|
+
// ── Primary: CDP DOM.setFileInputFiles ──────────────────────────────
|
|
90
|
+
if (page.setFileInput) {
|
|
91
|
+
try {
|
|
92
|
+
// Find image-accepting file input on the page
|
|
93
|
+
const selector: string | null = await page.evaluate(`
|
|
94
|
+
(() => {
|
|
95
|
+
const sels = ${JSON.stringify(IMAGE_INPUT_SELECTOR)};
|
|
96
|
+
const el = document.querySelector(sels);
|
|
97
|
+
return el ? sels : null;
|
|
98
|
+
})()
|
|
99
|
+
`);
|
|
100
|
+
if (!selector) {
|
|
101
|
+
return { ok: false, count: 0, error: 'No file input found on page' };
|
|
102
|
+
}
|
|
103
|
+
await page.setFileInput(absPaths, selector);
|
|
104
|
+
return { ok: true, count: absPaths.length };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// If set-file-input action is not supported by extension, fall through to legacy
|
|
107
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
108
|
+
if (msg.includes('Unknown action') || msg.includes('not supported')) {
|
|
109
|
+
// Extension too old — fall through to legacy base64 method
|
|
110
|
+
} else {
|
|
111
|
+
return { ok: false, count: 0, error: msg };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Fallback: legacy base64 DataTransfer injection ─────────────────
|
|
117
|
+
const images = absPaths.map((absPath) => {
|
|
118
|
+
const base64 = fs.readFileSync(absPath).toString('base64');
|
|
119
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
120
|
+
return { name: path.basename(absPath), mimeType: SUPPORTED_EXTENSIONS[ext], base64 };
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Warn if total payload is large — this may fail with older extensions
|
|
124
|
+
const totalBytes = images.reduce((sum, img) => sum + img.base64.length, 0);
|
|
125
|
+
if (totalBytes > 500_000) {
|
|
126
|
+
console.warn(
|
|
127
|
+
`[warn] Total image payload is ${(totalBytes / 1024 / 1024).toFixed(1)}MB (base64). ` +
|
|
128
|
+
'This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, ' +
|
|
129
|
+
'or compress images before publishing.'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
77
133
|
const payload = JSON.stringify(images);
|
|
78
134
|
return page.evaluate(`
|
|
79
135
|
(async () => {
|
|
80
136
|
const images = ${payload};
|
|
81
137
|
|
|
82
|
-
// Only use image-capable file inputs. Do not fall back to a generic uploader,
|
|
83
|
-
// otherwise we can accidentally feed images into the video upload flow.
|
|
84
138
|
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
85
139
|
const input = inputs.find(el => {
|
|
86
140
|
const accept = el.getAttribute('accept') || '';
|
|
@@ -346,8 +400,8 @@ cli({
|
|
|
346
400
|
if (imagePaths.length > MAX_IMAGES)
|
|
347
401
|
throw new Error(`Too many images: ${imagePaths.length} (max ${MAX_IMAGES})`);
|
|
348
402
|
|
|
349
|
-
//
|
|
350
|
-
const
|
|
403
|
+
// Validate image paths before navigating (fast-fail on bad paths / unsupported formats)
|
|
404
|
+
const absImagePaths = validateImagePaths(imagePaths);
|
|
351
405
|
|
|
352
406
|
// ── Step 1: Navigate to publish page ──────────────────────────────────────
|
|
353
407
|
await page.goto(PUBLISH_URL);
|
|
@@ -377,7 +431,7 @@ cli({
|
|
|
377
431
|
}
|
|
378
432
|
|
|
379
433
|
// ── Step 3: Upload images ──────────────────────────────────────────────────
|
|
380
|
-
const upload = await
|
|
434
|
+
const upload = await uploadImages(page, absImagePaths);
|
|
381
435
|
if (!upload.ok) {
|
|
382
436
|
await page.screenshot({ path: '/tmp/xhs_publish_upload_debug.png' });
|
|
383
437
|
throw new Error(
|
|
@@ -532,7 +586,7 @@ cli({
|
|
|
532
586
|
status: isSuccess ? `✅ ${verb}` : '⚠️ 操作完成,请在浏览器中确认',
|
|
533
587
|
detail: [
|
|
534
588
|
`"${title}"`,
|
|
535
|
-
`${
|
|
589
|
+
`${absImagePaths.length}张图片`,
|
|
536
590
|
topics.length ? `话题: ${topics.join(' ')}` : '',
|
|
537
591
|
successMsg || finalUrl || '',
|
|
538
592
|
]
|
|
@@ -41,15 +41,16 @@ describe('xiaohongshu search', () => {
|
|
|
41
41
|
expect(cmd?.func).toBeTypeOf('function');
|
|
42
42
|
|
|
43
43
|
const page = createPageMock([
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
results: [],
|
|
47
|
-
},
|
|
44
|
+
// First evaluate: early login-wall check (returns true)
|
|
45
|
+
true,
|
|
48
46
|
]);
|
|
49
47
|
|
|
50
48
|
await expect(cmd!.func!(page, { query: '特斯拉', limit: 5 })).rejects.toThrow(
|
|
51
49
|
'Xiaohongshu search results are blocked behind a login wall'
|
|
52
50
|
);
|
|
51
|
+
|
|
52
|
+
// autoScroll must NOT be called when a login wall is detected early
|
|
53
|
+
expect(page.autoScroll).not.toHaveBeenCalled();
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
it('returns ranked results with search_result url and author_url preserved', async () => {
|
|
@@ -62,6 +63,9 @@ describe('xiaohongshu search', () => {
|
|
|
62
63
|
'https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40?xsec_token=user-token&xsec_source=pc_search';
|
|
63
64
|
|
|
64
65
|
const page = createPageMock([
|
|
66
|
+
// First evaluate: early login-wall check (returns false → no wall)
|
|
67
|
+
false,
|
|
68
|
+
// Second evaluate: main DOM extraction
|
|
65
69
|
{
|
|
66
70
|
loginWall: false,
|
|
67
71
|
results: [
|
|
@@ -99,6 +103,9 @@ describe('xiaohongshu search', () => {
|
|
|
99
103
|
expect(cmd?.func).toBeTypeOf('function');
|
|
100
104
|
|
|
101
105
|
const page = createPageMock([
|
|
106
|
+
// First evaluate: early login-wall check (returns false → no wall)
|
|
107
|
+
false,
|
|
108
|
+
// Second evaluate: main DOM extraction
|
|
102
109
|
{
|
|
103
110
|
loginWall: false,
|
|
104
111
|
results: [
|
|
@@ -44,6 +44,19 @@ cli({
|
|
|
44
44
|
);
|
|
45
45
|
await page.wait(3);
|
|
46
46
|
|
|
47
|
+
// Early login-wall detection: XHS may show a login gate instead of
|
|
48
|
+
// results. Check *before* autoScroll to avoid crashing on a page
|
|
49
|
+
// that has no meaningful content to scroll through.
|
|
50
|
+
const loginCheck = await page.evaluate(`
|
|
51
|
+
(() => /登录后查看搜索结果/.test(document.body?.innerText || ''))()
|
|
52
|
+
`);
|
|
53
|
+
if (loginCheck) {
|
|
54
|
+
throw new AuthRequiredError(
|
|
55
|
+
'www.xiaohongshu.com',
|
|
56
|
+
'Xiaohongshu search results are blocked behind a login wall',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
// Scroll a couple of times to load more results
|
|
48
61
|
await page.autoScroll({ times: 2 });
|
|
49
62
|
|
|
@@ -75,6 +75,7 @@ describe('extractXhsUserNotes', () => {
|
|
|
75
75
|
title: 'First note',
|
|
76
76
|
type: 'video',
|
|
77
77
|
likes: '4.6万',
|
|
78
|
+
cover: '',
|
|
78
79
|
url: 'https://www.xiaohongshu.com/user/profile/user-1/note-1?xsec_token=abc&xsec_source=pc_user',
|
|
79
80
|
},
|
|
80
81
|
{
|
|
@@ -82,11 +83,33 @@ describe('extractXhsUserNotes', () => {
|
|
|
82
83
|
title: 'Second note',
|
|
83
84
|
type: 'normal',
|
|
84
85
|
likes: '42',
|
|
86
|
+
cover: '',
|
|
85
87
|
url: 'https://www.xiaohongshu.com/user/profile/fallback-user/note-2',
|
|
86
88
|
},
|
|
87
89
|
]);
|
|
88
90
|
});
|
|
89
91
|
|
|
92
|
+
it('extracts cover urls with fallback priority urlDefault -> urlPre -> url', () => {
|
|
93
|
+
const rows = extractXhsUserNotes(
|
|
94
|
+
{
|
|
95
|
+
noteGroups: [
|
|
96
|
+
[
|
|
97
|
+
{ noteCard: { noteId: 'cover-1', cover: { urlDefault: 'https://img.example/default.jpg', urlPre: 'https://img.example/pre.jpg', url: 'https://img.example/raw.jpg' } } },
|
|
98
|
+
{ noteCard: { noteId: 'cover-2', cover: { urlPre: 'https://img.example/pre-only.jpg', url: 'https://img.example/raw-only.jpg' } } },
|
|
99
|
+
{ noteCard: { noteId: 'cover-3', cover: { url: 'https://img.example/raw-fallback.jpg' } } },
|
|
100
|
+
],
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
'fallback-user'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(rows.map(row => row.cover)).toEqual([
|
|
107
|
+
'https://img.example/default.jpg',
|
|
108
|
+
'https://img.example/pre-only.jpg',
|
|
109
|
+
'https://img.example/raw-fallback.jpg',
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
90
113
|
it('deduplicates repeated notes by note id', () => {
|
|
91
114
|
const rows = extractXhsUserNotes(
|
|
92
115
|
{
|
|
@@ -8,6 +8,7 @@ export interface XhsUserNoteRow {
|
|
|
8
8
|
title: string;
|
|
9
9
|
type: string;
|
|
10
10
|
likes: string;
|
|
11
|
+
cover: string;
|
|
11
12
|
url: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -72,11 +73,14 @@ export function extractXhsUserNotes(snapshot: XhsUserPageSnapshot, fallbackUserI
|
|
|
72
73
|
const xsecToken = toCleanString(entry?.xsecToken ?? entry?.xsec_token ?? noteCard.xsecToken ?? noteCard.xsec_token);
|
|
73
74
|
const likes = toCleanString(noteCard.interactInfo?.likedCount ?? noteCard.interact_info?.liked_count ?? 0) || '0';
|
|
74
75
|
|
|
76
|
+
const cover = toCleanString(noteCard.cover?.urlDefault ?? noteCard.cover?.urlPre ?? noteCard.cover?.url ?? '');
|
|
77
|
+
|
|
75
78
|
rows.push({
|
|
76
79
|
id: noteId,
|
|
77
80
|
title: toCleanString(noteCard.displayTitle ?? noteCard.display_title ?? noteCard.title),
|
|
78
81
|
type: toCleanString(noteCard.type),
|
|
79
82
|
likes,
|
|
83
|
+
cover,
|
|
80
84
|
url: buildXhsNoteUrl(userId || fallbackUserId, noteId, xsecToken),
|
|
81
85
|
});
|
|
82
86
|
}
|