@jackwener/opencli 1.0.6 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
- package/.github/pull_request_template.md +7 -0
- package/.github/workflows/doc-check.yml +36 -0
- package/.github/workflows/docs.yml +7 -42
- package/CHANGELOG.md +23 -0
- package/CLI-EXPLORER.md +9 -8
- package/README.md +51 -10
- package/README.zh-CN.md +29 -11
- package/SKILL.md +102 -33
- package/dist/browser/cdp.js +6 -1
- package/dist/browser/page.d.ts +4 -1
- package/dist/browser/page.js +7 -1
- package/dist/build-manifest.js +23 -16
- package/dist/cli-manifest.json +951 -296
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +225 -148
- package/dist/clis/antigravity/serve.js +296 -47
- package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
- package/dist/clis/apple-podcasts/commands.test.js +76 -0
- package/dist/clis/apple-podcasts/search.js +2 -2
- package/dist/clis/apple-podcasts/top.js +9 -2
- package/dist/clis/arxiv/paper.js +21 -0
- package/dist/clis/arxiv/search.js +24 -0
- package/dist/clis/arxiv/utils.d.ts +18 -0
- package/dist/clis/arxiv/utils.js +49 -0
- package/dist/clis/bilibili/dynamic.js +1 -1
- package/dist/clis/bilibili/favorite.js +1 -1
- package/dist/clis/bilibili/feed.js +1 -1
- package/dist/clis/bilibili/following.js +1 -1
- package/dist/clis/bilibili/history.js +1 -1
- package/dist/clis/bilibili/me.js +1 -1
- package/dist/clis/bilibili/ranking.js +1 -1
- package/dist/clis/bilibili/search.js +3 -3
- package/dist/clis/bilibili/subtitle.js +1 -1
- package/dist/clis/bilibili/user-videos.js +1 -1
- package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
- package/dist/clis/bloomberg/businessweek.d.ts +1 -0
- package/dist/clis/bloomberg/businessweek.js +17 -0
- package/dist/clis/bloomberg/economics.d.ts +1 -0
- package/dist/clis/bloomberg/economics.js +17 -0
- package/dist/clis/bloomberg/feeds.d.ts +1 -0
- package/dist/clis/bloomberg/feeds.js +15 -0
- package/dist/clis/bloomberg/industries.d.ts +1 -0
- package/dist/clis/bloomberg/industries.js +17 -0
- package/dist/clis/bloomberg/main.d.ts +1 -0
- package/dist/clis/bloomberg/main.js +17 -0
- package/dist/clis/bloomberg/markets.d.ts +1 -0
- package/dist/clis/bloomberg/markets.js +17 -0
- package/dist/clis/bloomberg/news.d.ts +1 -0
- package/dist/clis/bloomberg/news.js +105 -0
- package/dist/clis/bloomberg/opinions.d.ts +1 -0
- package/dist/clis/bloomberg/opinions.js +17 -0
- package/dist/clis/bloomberg/politics.d.ts +1 -0
- package/dist/clis/bloomberg/politics.js +17 -0
- package/dist/clis/bloomberg/tech.d.ts +1 -0
- package/dist/clis/bloomberg/tech.js +17 -0
- package/dist/clis/bloomberg/utils.d.ts +34 -0
- package/dist/clis/bloomberg/utils.js +364 -0
- package/dist/clis/bloomberg/utils.test.d.ts +1 -0
- package/dist/clis/bloomberg/utils.test.js +129 -0
- package/dist/clis/boss/batchgreet.d.ts +1 -0
- package/dist/clis/boss/batchgreet.js +147 -0
- package/dist/clis/boss/chatlist.js +2 -2
- package/dist/clis/boss/detail.js +2 -2
- package/dist/clis/boss/exchange.d.ts +1 -0
- package/dist/clis/boss/exchange.js +111 -0
- package/dist/clis/boss/greet.d.ts +1 -0
- package/dist/clis/boss/greet.js +175 -0
- package/dist/clis/boss/invite.d.ts +1 -0
- package/dist/clis/boss/invite.js +158 -0
- package/dist/clis/boss/joblist.d.ts +1 -0
- package/dist/clis/boss/joblist.js +55 -0
- package/dist/clis/boss/mark.d.ts +1 -0
- package/dist/clis/boss/mark.js +141 -0
- package/dist/clis/boss/recommend.d.ts +1 -0
- package/dist/clis/boss/recommend.js +83 -0
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +1 -1
- package/dist/clis/boss/stats.d.ts +1 -0
- package/dist/clis/boss/stats.js +116 -0
- package/dist/clis/chaoxing/assignments.js +1 -1
- package/dist/clis/chaoxing/exams.js +1 -1
- package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
- package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
- package/dist/clis/chaoxing/utils.test.d.ts +1 -0
- package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
- package/dist/clis/chatgpt/read.js +1 -1
- package/dist/clis/chatwise/export.js +1 -1
- package/dist/clis/chatwise/model.js +2 -2
- package/dist/clis/chatwise/screenshot.js +1 -1
- package/dist/clis/codex/export.js +1 -1
- package/dist/clis/codex/model.js +2 -2
- package/dist/clis/codex/screenshot.js +1 -1
- package/dist/clis/coupang/add-to-cart.js +3 -4
- package/dist/clis/coupang/search.js +2 -4
- package/dist/clis/coupang/utils.test.d.ts +1 -0
- package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
- package/dist/clis/ctrip/search.js +1 -1
- package/dist/clis/cursor/export.js +1 -1
- package/dist/clis/cursor/model.js +2 -2
- package/dist/clis/cursor/screenshot.js +1 -1
- package/dist/clis/jike/comment.js +2 -3
- package/dist/clis/jike/create.js +1 -2
- package/dist/clis/jike/feed.js +0 -1
- package/dist/clis/jike/like.js +1 -2
- package/dist/clis/jike/notifications.js +0 -1
- package/dist/clis/jike/post.yaml +1 -0
- package/dist/clis/jike/repost.js +1 -2
- package/dist/clis/jike/search.js +2 -3
- package/dist/clis/jike/topic.yaml +1 -0
- package/dist/clis/jike/user.yaml +1 -0
- package/dist/clis/jimeng/history.yaml +0 -1
- package/dist/clis/linkedin/search.js +7 -7
- package/dist/clis/linux-do/category.yaml +1 -0
- package/dist/clis/linux-do/search.yaml +4 -3
- package/dist/clis/linux-do/topic.yaml +1 -0
- package/dist/clis/notion/export.js +1 -1
- package/dist/clis/reddit/comment.js +3 -4
- package/dist/clis/reddit/read.js +4 -5
- package/dist/clis/reddit/save.js +2 -3
- package/dist/clis/reddit/saved.js +0 -1
- package/dist/clis/reddit/search.yaml +1 -0
- package/dist/clis/reddit/subscribe.js +0 -1
- package/dist/clis/reddit/upvote.js +2 -3
- package/dist/clis/reddit/upvoted.js +0 -1
- package/dist/clis/reddit/user-comments.yaml +1 -0
- package/dist/clis/reddit/user-posts.yaml +1 -0
- package/dist/clis/reddit/user.yaml +1 -0
- package/dist/clis/reuters/search.js +1 -1
- package/dist/clis/sinafinance/news.d.ts +7 -0
- package/dist/clis/sinafinance/news.js +61 -0
- package/dist/clis/smzdm/search.js +2 -3
- package/dist/clis/stackoverflow/search.yaml +1 -0
- package/dist/clis/steam/top-sellers.yaml +29 -0
- package/dist/clis/twitter/accept.js +2 -2
- package/dist/clis/twitter/article.js +2 -2
- package/dist/clis/twitter/block.d.ts +1 -0
- package/dist/clis/twitter/block.js +88 -0
- package/dist/clis/twitter/delete.js +1 -1
- package/dist/clis/twitter/hide-reply.d.ts +1 -0
- package/dist/clis/twitter/hide-reply.js +66 -0
- package/dist/clis/twitter/like.js +1 -1
- package/dist/clis/twitter/post.js +1 -1
- package/dist/clis/twitter/reply-dm.js +1 -1
- package/dist/clis/twitter/reply.js +2 -2
- package/dist/clis/twitter/search.js +1 -1
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/trending.d.ts +1 -0
- package/dist/clis/twitter/trending.js +91 -0
- package/dist/clis/twitter/unblock.d.ts +1 -0
- package/dist/clis/twitter/unblock.js +71 -0
- package/dist/clis/v2ex/topic.yaml +1 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weread/book.js +1 -1
- package/dist/clis/weread/highlights.js +1 -1
- package/dist/clis/weread/notes.js +1 -1
- package/dist/clis/weread/search.js +1 -1
- package/dist/clis/wikipedia/search.d.ts +1 -0
- package/dist/clis/wikipedia/search.js +30 -0
- package/dist/clis/wikipedia/summary.d.ts +1 -0
- package/dist/clis/wikipedia/summary.js +28 -0
- package/dist/clis/wikipedia/utils.d.ts +8 -0
- package/dist/clis/wikipedia/utils.js +18 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +79 -5
- package/dist/clis/xiaohongshu/creator-note-detail.js +323 -70
- package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +258 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
- package/dist/clis/xiaohongshu/creator-notes.js +189 -71
- package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes.test.js +191 -0
- package/dist/clis/xiaohongshu/creator-profile.js +0 -1
- package/dist/clis/xiaohongshu/creator-stats.js +0 -1
- package/dist/clis/xiaohongshu/download.js +2 -3
- package/dist/clis/xiaohongshu/feed.yaml +0 -1
- package/dist/clis/xiaohongshu/notifications.yaml +0 -1
- package/dist/clis/xiaohongshu/search.js +2 -2
- package/dist/clis/xiaohongshu/user.js +1 -2
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/search.js +1 -1
- package/dist/clis/youtube/transcript.js +1 -1
- package/dist/clis/youtube/video.js +1 -1
- package/dist/clis/zhihu/download.js +1 -2
- package/dist/clis/zhihu/question.js +1 -1
- package/dist/clis/zhihu/search.yaml +4 -3
- package/dist/commanderAdapter.d.ts +21 -0
- package/dist/commanderAdapter.js +111 -0
- package/dist/{engine.d.ts → discovery.d.ts} +0 -6
- package/dist/{engine.js → discovery.js} +1 -98
- package/dist/download/index.d.ts +2 -6
- package/dist/download/index.js +19 -46
- package/dist/engine.test.d.ts +1 -1
- package/dist/engine.test.js +8 -7
- package/dist/execution.d.ts +22 -0
- package/dist/execution.js +129 -0
- package/dist/explore.js +121 -107
- package/dist/external-clis.yaml +48 -0
- package/dist/external.d.ts +25 -0
- package/dist/external.js +156 -0
- package/dist/main.js +1 -1
- package/dist/pipeline/steps/browser.js +8 -2
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +2 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +8 -0
- package/dist/serialization.d.ts +34 -0
- package/dist/serialization.js +63 -0
- package/dist/types.d.ts +4 -1
- package/docs/.vitepress/config.mts +14 -3
- package/docs/adapters/browser/arxiv.md +27 -0
- package/docs/adapters/browser/barchart.md +32 -0
- package/docs/adapters/browser/bloomberg.md +70 -0
- package/docs/adapters/browser/chaoxing.md +39 -0
- package/docs/adapters/browser/grok.md +35 -0
- package/docs/adapters/browser/hf.md +42 -0
- package/docs/adapters/browser/jike.md +45 -0
- package/docs/adapters/browser/jimeng.md +39 -0
- package/docs/adapters/browser/linux-do.md +45 -0
- package/docs/adapters/browser/sinafinance.md +35 -0
- package/docs/adapters/browser/stackoverflow.md +35 -0
- package/docs/adapters/browser/steam.md +26 -0
- package/docs/adapters/browser/twitter.md +3 -0
- package/docs/adapters/browser/weread.md +48 -0
- package/docs/adapters/browser/wikipedia.md +30 -0
- package/docs/adapters/browser/xiaohongshu.md +5 -1
- package/docs/adapters/desktop/chatgpt.md +3 -3
- package/docs/adapters/index.md +13 -0
- package/docs/advanced/download.md +4 -4
- package/docs/developer/architecture.md +17 -4
- package/package.json +1 -1
- package/scripts/check-doc-coverage.sh +69 -0
- package/scripts/copy-yaml.cjs +7 -0
- package/src/browser/cdp.ts +9 -4
- package/src/browser/page.ts +7 -1
- package/src/build-manifest.ts +25 -19
- package/src/cli.ts +253 -119
- package/src/clis/antigravity/serve.ts +323 -50
- package/src/clis/apple-podcasts/commands.test.ts +95 -0
- package/src/clis/apple-podcasts/search.ts +2 -2
- package/src/clis/apple-podcasts/top.ts +12 -2
- package/src/clis/arxiv/paper.ts +21 -0
- package/src/clis/arxiv/search.ts +24 -0
- package/src/clis/arxiv/utils.ts +63 -0
- package/src/clis/bilibili/dynamic.ts +1 -1
- package/src/clis/bilibili/favorite.ts +1 -1
- package/src/clis/bilibili/feed.ts +1 -1
- package/src/clis/bilibili/following.ts +1 -1
- package/src/clis/bilibili/history.ts +1 -1
- package/src/clis/bilibili/me.ts +1 -1
- package/src/clis/bilibili/ranking.ts +1 -1
- package/src/clis/bilibili/search.ts +3 -3
- package/src/clis/bilibili/subtitle.ts +1 -1
- package/src/clis/bilibili/user-videos.ts +1 -1
- package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
- package/src/clis/bloomberg/businessweek.ts +18 -0
- package/src/clis/bloomberg/economics.ts +18 -0
- package/src/clis/bloomberg/feeds.ts +16 -0
- package/src/clis/bloomberg/industries.ts +18 -0
- package/src/clis/bloomberg/main.ts +18 -0
- package/src/clis/bloomberg/markets.ts +18 -0
- package/src/clis/bloomberg/news.ts +136 -0
- package/src/clis/bloomberg/opinions.ts +18 -0
- package/src/clis/bloomberg/politics.ts +18 -0
- package/src/clis/bloomberg/tech.ts +18 -0
- package/src/clis/bloomberg/utils.test.ts +135 -0
- package/src/clis/bloomberg/utils.ts +429 -0
- package/src/clis/boss/batchgreet.ts +167 -0
- package/src/clis/boss/chatlist.ts +2 -2
- package/src/clis/boss/detail.ts +2 -2
- package/src/clis/boss/exchange.ts +126 -0
- package/src/clis/boss/greet.ts +198 -0
- package/src/clis/boss/invite.ts +177 -0
- package/src/clis/boss/joblist.ts +63 -0
- package/src/clis/boss/mark.ts +155 -0
- package/src/clis/boss/recommend.ts +94 -0
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +1 -1
- package/src/clis/boss/stats.ts +130 -0
- package/src/clis/chaoxing/assignments.ts +1 -1
- package/src/clis/chaoxing/exams.ts +1 -1
- package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
- package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
- package/src/clis/chatgpt/README.zh-CN.md +3 -3
- package/src/clis/chatgpt/read.ts +1 -1
- package/src/clis/chatwise/export.ts +1 -1
- package/src/clis/chatwise/model.ts +2 -2
- package/src/clis/chatwise/screenshot.ts +1 -1
- package/src/clis/codex/export.ts +1 -1
- package/src/clis/codex/model.ts +2 -2
- package/src/clis/codex/screenshot.ts +1 -1
- package/src/clis/coupang/add-to-cart.ts +3 -4
- package/src/clis/coupang/search.ts +2 -4
- package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
- package/src/clis/ctrip/search.ts +1 -1
- package/src/clis/cursor/export.ts +1 -1
- package/src/clis/cursor/model.ts +2 -2
- package/src/clis/cursor/screenshot.ts +1 -1
- package/src/clis/jike/comment.ts +2 -3
- package/src/clis/jike/create.ts +1 -2
- package/src/clis/jike/feed.ts +0 -1
- package/src/clis/jike/like.ts +1 -2
- package/src/clis/jike/notifications.ts +0 -1
- package/src/clis/jike/post.yaml +1 -0
- package/src/clis/jike/repost.ts +1 -2
- package/src/clis/jike/search.ts +2 -3
- package/src/clis/jike/topic.yaml +1 -0
- package/src/clis/jike/user.yaml +1 -0
- package/src/clis/jimeng/history.yaml +0 -1
- package/src/clis/linkedin/search.ts +7 -7
- package/src/clis/linux-do/category.yaml +1 -0
- package/src/clis/linux-do/search.yaml +4 -3
- package/src/clis/linux-do/topic.yaml +1 -0
- package/src/clis/notion/export.ts +1 -1
- package/src/clis/reddit/comment.ts +3 -4
- package/src/clis/reddit/read.ts +4 -5
- package/src/clis/reddit/save.ts +2 -3
- package/src/clis/reddit/saved.ts +0 -1
- package/src/clis/reddit/search.yaml +1 -0
- package/src/clis/reddit/subscribe.ts +0 -1
- package/src/clis/reddit/upvote.ts +2 -3
- package/src/clis/reddit/upvoted.ts +0 -1
- package/src/clis/reddit/user-comments.yaml +1 -0
- package/src/clis/reddit/user-posts.yaml +1 -0
- package/src/clis/reddit/user.yaml +1 -0
- package/src/clis/reuters/search.ts +1 -1
- package/src/clis/sinafinance/news.ts +76 -0
- package/src/clis/smzdm/search.ts +2 -3
- package/src/clis/stackoverflow/search.yaml +1 -0
- package/src/clis/steam/top-sellers.yaml +29 -0
- package/src/clis/twitter/accept.ts +2 -2
- package/src/clis/twitter/article.ts +2 -2
- package/src/clis/twitter/block.ts +92 -0
- package/src/clis/twitter/delete.ts +1 -1
- package/src/clis/twitter/hide-reply.ts +70 -0
- package/src/clis/twitter/like.ts +1 -1
- package/src/clis/twitter/post.ts +1 -1
- package/src/clis/twitter/reply-dm.ts +1 -1
- package/src/clis/twitter/reply.ts +2 -2
- package/src/clis/twitter/search.ts +1 -1
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/trending.ts +113 -0
- package/src/clis/twitter/unblock.ts +75 -0
- package/src/clis/v2ex/topic.yaml +1 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weread/book.ts +1 -1
- package/src/clis/weread/highlights.ts +1 -1
- package/src/clis/weread/notes.ts +1 -1
- package/src/clis/weread/search.ts +1 -1
- package/src/clis/wikipedia/search.ts +32 -0
- package/src/clis/wikipedia/summary.ts +28 -0
- package/src/clis/wikipedia/utils.ts +20 -0
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +272 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +425 -73
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
- package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
- package/src/clis/xiaohongshu/creator-notes.test.ts +211 -0
- package/src/clis/xiaohongshu/creator-notes.ts +254 -75
- package/src/clis/xiaohongshu/creator-profile.ts +0 -1
- package/src/clis/xiaohongshu/creator-stats.ts +0 -1
- package/src/clis/xiaohongshu/download.ts +2 -3
- package/src/clis/xiaohongshu/feed.yaml +0 -1
- package/src/clis/xiaohongshu/notifications.yaml +0 -1
- package/src/clis/xiaohongshu/search.ts +2 -2
- package/src/clis/xiaohongshu/user.ts +1 -2
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/search.ts +1 -1
- package/src/clis/youtube/transcript.ts +1 -1
- package/src/clis/youtube/video.ts +1 -1
- package/src/clis/zhihu/download.ts +1 -2
- package/src/clis/zhihu/question.ts +1 -1
- package/src/clis/zhihu/search.yaml +4 -3
- package/src/commanderAdapter.ts +113 -0
- package/src/daemon.ts +3 -3
- package/src/{engine.ts → discovery.ts} +1 -108
- package/src/download/index.ts +21 -54
- package/src/engine.test.ts +8 -7
- package/src/execution.ts +138 -0
- package/src/explore.ts +135 -109
- package/src/external-clis.yaml +48 -0
- package/src/external.ts +185 -0
- package/src/main.ts +1 -1
- package/src/pipeline/steps/browser.ts +7 -2
- package/src/registry.ts +5 -0
- package/src/runtime.ts +9 -0
- package/src/serialization.ts +79 -0
- package/src/types.ts +1 -1
- package/tests/e2e/browser-public.test.ts +25 -0
- package/tests/e2e/public-commands.test.ts +55 -1
- package/dist/clis/twitter/trending.yaml +0 -46
- package/src/clis/twitter/trending.yaml +0 -46
- /package/dist/{chaoxing.test.d.ts → clis/arxiv/paper.d.ts} +0 -0
- /package/dist/{coupang.test.d.ts → clis/arxiv/search.d.ts} +0 -0
- /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
- /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
- /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
- /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { CliError } from '../../errors.js';
|
|
2
|
+
export const BLOOMBERG_FEEDS = {
|
|
3
|
+
main: 'https://feeds.bloomberg.com/news.rss',
|
|
4
|
+
markets: 'https://feeds.bloomberg.com/markets/news.rss',
|
|
5
|
+
economics: 'https://feeds.bloomberg.com/economics/news.rss',
|
|
6
|
+
industries: 'https://feeds.bloomberg.com/industries/news.rss',
|
|
7
|
+
tech: 'https://feeds.bloomberg.com/technology/news.rss',
|
|
8
|
+
politics: 'https://feeds.bloomberg.com/politics/news.rss',
|
|
9
|
+
businessweek: 'https://feeds.bloomberg.com/businessweek/news.rss',
|
|
10
|
+
opinions: 'https://feeds.bloomberg.com/bview/news.rss',
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; opencli)';
|
|
13
|
+
export async function fetchBloombergFeed(name, limit = 1) {
|
|
14
|
+
const feedUrl = BLOOMBERG_FEEDS[name];
|
|
15
|
+
if (!feedUrl) {
|
|
16
|
+
throw new CliError('ARGUMENT', `Unknown Bloomberg feed: ${name}`);
|
|
17
|
+
}
|
|
18
|
+
const resp = await fetch(feedUrl, {
|
|
19
|
+
headers: { 'User-Agent': DEFAULT_USER_AGENT },
|
|
20
|
+
});
|
|
21
|
+
if (!resp.ok) {
|
|
22
|
+
throw new CliError('FETCH_ERROR', `Bloomberg RSS HTTP ${resp.status}`, 'Bloomberg may be temporarily unavailable; try again later.');
|
|
23
|
+
}
|
|
24
|
+
const xml = await resp.text();
|
|
25
|
+
const items = parseBloombergRss(xml);
|
|
26
|
+
if (!items.length) {
|
|
27
|
+
throw new CliError('NOT_FOUND', 'Bloomberg RSS feed returned no items', 'Bloomberg may have changed the feed format.');
|
|
28
|
+
}
|
|
29
|
+
const count = Math.max(1, Math.min(Number(limit) || 1, 20));
|
|
30
|
+
return items.slice(0, count);
|
|
31
|
+
}
|
|
32
|
+
export function parseBloombergRss(xml) {
|
|
33
|
+
const items = [];
|
|
34
|
+
const itemRegex = /<item\b[^>]*>([\s\S]*?)<\/item>/gi;
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = itemRegex.exec(xml))) {
|
|
37
|
+
const block = match[1];
|
|
38
|
+
const title = extractTagText(block, 'title');
|
|
39
|
+
const summary = extractTagText(block, 'description');
|
|
40
|
+
const link = extractTagText(block, 'link') || extractTagText(block, 'guid');
|
|
41
|
+
const mediaLinks = extractMediaLinksFromRssItem(block);
|
|
42
|
+
if (!title || !link)
|
|
43
|
+
continue;
|
|
44
|
+
items.push({
|
|
45
|
+
title,
|
|
46
|
+
summary,
|
|
47
|
+
link,
|
|
48
|
+
mediaLinks,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return items;
|
|
52
|
+
}
|
|
53
|
+
export function normalizeBloombergLink(input) {
|
|
54
|
+
const raw = String(input || '').trim();
|
|
55
|
+
if (!raw) {
|
|
56
|
+
throw new CliError('ARGUMENT', 'A Bloomberg link is required');
|
|
57
|
+
}
|
|
58
|
+
if (raw.startsWith('/'))
|
|
59
|
+
return `https://www.bloomberg.com${raw}`;
|
|
60
|
+
return raw;
|
|
61
|
+
}
|
|
62
|
+
export function validateBloombergLink(input) {
|
|
63
|
+
const normalized = normalizeBloombergLink(input);
|
|
64
|
+
let url;
|
|
65
|
+
try {
|
|
66
|
+
url = new URL(normalized);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new CliError('ARGUMENT', `Invalid Bloomberg link: ${input}`, 'Pass a full https://www.bloomberg.com/... URL or a relative Bloomberg path.');
|
|
70
|
+
}
|
|
71
|
+
if (!/(?:\.|^)bloomberg\.com$/i.test(url.hostname)) {
|
|
72
|
+
throw new CliError('ARGUMENT', `Expected a bloomberg.com link, got: ${url.hostname}`, 'Pass a Bloomberg article URL from bloomberg.com.');
|
|
73
|
+
}
|
|
74
|
+
return url.toString();
|
|
75
|
+
}
|
|
76
|
+
export function renderStoryBody(body) {
|
|
77
|
+
const blocks = Array.isArray(body?.content) ? body.content : [];
|
|
78
|
+
const parts = blocks
|
|
79
|
+
.map((block) => renderBlock(block, 0))
|
|
80
|
+
.map((part) => normalizeBlockText(part))
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
return parts.join('\n\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
83
|
+
}
|
|
84
|
+
export function extractStoryMediaLinks(story) {
|
|
85
|
+
const urls = new Set();
|
|
86
|
+
collectMediaUrls(story?.ledeImageUrl, urls);
|
|
87
|
+
collectMediaUrls(story?.socialImageUrl, urls);
|
|
88
|
+
collectMediaUrls(story?.lede, urls);
|
|
89
|
+
collectMediaUrls(story?.imageAttachments, urls);
|
|
90
|
+
collectMediaUrls(story?.videoAttachments, urls);
|
|
91
|
+
const mediaBlocks = Array.isArray(story?.body?.content)
|
|
92
|
+
? story.body.content.filter((block) => block?.type === 'media')
|
|
93
|
+
: [];
|
|
94
|
+
collectMediaUrls(mediaBlocks, urls);
|
|
95
|
+
return [...urls];
|
|
96
|
+
}
|
|
97
|
+
function renderBlock(block, depth) {
|
|
98
|
+
if (!block || typeof block !== 'object')
|
|
99
|
+
return '';
|
|
100
|
+
switch (block.type) {
|
|
101
|
+
case 'paragraph':
|
|
102
|
+
return renderInlineNodes(block.content || []);
|
|
103
|
+
case 'heading': {
|
|
104
|
+
const text = renderInlineNodes(block.content || []);
|
|
105
|
+
if (!text)
|
|
106
|
+
return '';
|
|
107
|
+
const level = Number(block.data?.level ?? block.data?.weight ?? 2);
|
|
108
|
+
const prefix = level <= 1 ? '# ' : level === 2 ? '## ' : '### ';
|
|
109
|
+
return `${prefix}${text}`;
|
|
110
|
+
}
|
|
111
|
+
case 'blockquote': {
|
|
112
|
+
const text = renderInlineNodes(block.content || []);
|
|
113
|
+
if (!text)
|
|
114
|
+
return '';
|
|
115
|
+
return text.split('\n').map((line) => line ? `> ${line}` : '>').join('\n');
|
|
116
|
+
}
|
|
117
|
+
case 'list':
|
|
118
|
+
return renderListBlock(block, depth);
|
|
119
|
+
case 'tabularData':
|
|
120
|
+
return renderTabularDataBlock(block);
|
|
121
|
+
case 'media':
|
|
122
|
+
return renderMediaBlock(block);
|
|
123
|
+
case 'inline-newsletter':
|
|
124
|
+
case 'newsletter':
|
|
125
|
+
case 'ad':
|
|
126
|
+
return '';
|
|
127
|
+
default: {
|
|
128
|
+
if (Array.isArray(block.content) && block.content.length > 0) {
|
|
129
|
+
const inlineText = renderInlineNodes(block.content);
|
|
130
|
+
if (inlineText)
|
|
131
|
+
return inlineText;
|
|
132
|
+
const nested = block.content.map((child) => renderBlock(child, depth + 1)).filter(Boolean);
|
|
133
|
+
if (nested.length)
|
|
134
|
+
return nested.join('\n');
|
|
135
|
+
}
|
|
136
|
+
return extractGenericText(block);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function renderInlineNodes(nodes) {
|
|
141
|
+
return nodes.map((node) => renderInlineNode(node)).join('');
|
|
142
|
+
}
|
|
143
|
+
function renderInlineNode(node) {
|
|
144
|
+
if (node == null)
|
|
145
|
+
return '';
|
|
146
|
+
if (typeof node === 'string')
|
|
147
|
+
return decodeXmlEntities(node);
|
|
148
|
+
switch (node.type) {
|
|
149
|
+
case 'text':
|
|
150
|
+
return decodeXmlEntities(node.value || '');
|
|
151
|
+
case 'linebreak':
|
|
152
|
+
return '\n';
|
|
153
|
+
case 'link':
|
|
154
|
+
case 'entity':
|
|
155
|
+
case 'strong':
|
|
156
|
+
case 'emphasis':
|
|
157
|
+
case 'italic':
|
|
158
|
+
case 'underline':
|
|
159
|
+
case 'span':
|
|
160
|
+
if (Array.isArray(node.content) && node.content.length > 0) {
|
|
161
|
+
return renderInlineNodes(node.content);
|
|
162
|
+
}
|
|
163
|
+
return decodeXmlEntities(node.value || '');
|
|
164
|
+
default:
|
|
165
|
+
if (Array.isArray(node.content) && node.content.length > 0) {
|
|
166
|
+
return renderInlineNodes(node.content);
|
|
167
|
+
}
|
|
168
|
+
if (typeof node.value === 'string')
|
|
169
|
+
return decodeXmlEntities(node.value);
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function renderListBlock(block, depth) {
|
|
174
|
+
const items = Array.isArray(block.content) ? block.content : [];
|
|
175
|
+
if (!items.length)
|
|
176
|
+
return '';
|
|
177
|
+
const listStyle = String(block.subType || block.data?.style || block.data?.listType || '');
|
|
178
|
+
const ordered = /\bordered\b|\bnumber(?:ed)?\b/i.test(listStyle);
|
|
179
|
+
let index = 1;
|
|
180
|
+
return items
|
|
181
|
+
.map((item) => {
|
|
182
|
+
const prefix = ordered ? `${index++}. ` : '- ';
|
|
183
|
+
return renderListItem(item, prefix, depth);
|
|
184
|
+
})
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join('\n');
|
|
187
|
+
}
|
|
188
|
+
function renderListItem(item, prefix, depth) {
|
|
189
|
+
const indent = ' '.repeat(depth);
|
|
190
|
+
const body = normalizeBlockText(renderListItemBody(item, depth + 1));
|
|
191
|
+
if (!body)
|
|
192
|
+
return '';
|
|
193
|
+
const lines = body.split('\n');
|
|
194
|
+
const head = `${indent}${prefix}${lines[0]}`;
|
|
195
|
+
if (lines.length === 1)
|
|
196
|
+
return head;
|
|
197
|
+
const continuationIndent = `${indent}${' '.repeat(prefix.length)}`;
|
|
198
|
+
const tail = lines.slice(1).map((line) => `${continuationIndent}${line}`).join('\n');
|
|
199
|
+
return `${head}\n${tail}`;
|
|
200
|
+
}
|
|
201
|
+
function renderListItemBody(item, depth) {
|
|
202
|
+
if (!item || typeof item !== 'object')
|
|
203
|
+
return '';
|
|
204
|
+
if (item.type === 'list-item' && Array.isArray(item.content)) {
|
|
205
|
+
const parts = item.content
|
|
206
|
+
.map((child) => child?.type === 'paragraph'
|
|
207
|
+
? renderInlineNodes(child.content || [])
|
|
208
|
+
: renderBlock(child, depth))
|
|
209
|
+
.map((part) => normalizeBlockText(part))
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
return parts.join('\n');
|
|
212
|
+
}
|
|
213
|
+
return renderBlock(item, depth);
|
|
214
|
+
}
|
|
215
|
+
function renderTabularDataBlock(block) {
|
|
216
|
+
const rows = block?.data?.rows ?? block?.data?.table?.rows ?? block?.content;
|
|
217
|
+
if (!Array.isArray(rows) || !rows.length) {
|
|
218
|
+
return extractGenericText(block.data || block.content || block);
|
|
219
|
+
}
|
|
220
|
+
const lines = rows
|
|
221
|
+
.map((row) => extractGenericText(row))
|
|
222
|
+
.map((line) => normalizeBlockText(line))
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
226
|
+
function renderMediaBlock(block) {
|
|
227
|
+
const candidates = [
|
|
228
|
+
block?.data?.chart?.caption,
|
|
229
|
+
block?.data?.attachment?.caption,
|
|
230
|
+
block?.data?.attachment?.title,
|
|
231
|
+
block?.data?.attachment?.subtitle,
|
|
232
|
+
block?.data?.video?.caption,
|
|
233
|
+
];
|
|
234
|
+
const caption = candidates
|
|
235
|
+
.map((value) => normalizeBlockText(stripHtml(String(value || ''))))
|
|
236
|
+
.find(Boolean);
|
|
237
|
+
return caption || '';
|
|
238
|
+
}
|
|
239
|
+
function extractGenericText(value) {
|
|
240
|
+
const parts = [];
|
|
241
|
+
collectText(value, parts);
|
|
242
|
+
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
243
|
+
}
|
|
244
|
+
function collectText(value, out) {
|
|
245
|
+
if (value == null)
|
|
246
|
+
return;
|
|
247
|
+
if (typeof value === 'string') {
|
|
248
|
+
const text = normalizeBlockText(stripHtml(decodeXmlEntities(value)));
|
|
249
|
+
if (text)
|
|
250
|
+
out.push(text);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (Array.isArray(value)) {
|
|
254
|
+
for (const item of value)
|
|
255
|
+
collectText(item, out);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (typeof value === 'object') {
|
|
259
|
+
if (typeof value.value === 'string') {
|
|
260
|
+
const text = normalizeBlockText(stripHtml(decodeXmlEntities(value.value)));
|
|
261
|
+
if (text)
|
|
262
|
+
out.push(text);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(value.content)) {
|
|
266
|
+
collectText(value.content, out);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
for (const entry of Object.values(value))
|
|
270
|
+
collectText(entry, out);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function extractTagText(block, tag) {
|
|
274
|
+
const safeTag = escapeRegExp(tag);
|
|
275
|
+
const match = block.match(new RegExp(`<${safeTag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${safeTag}>`, 'i'));
|
|
276
|
+
if (!match)
|
|
277
|
+
return '';
|
|
278
|
+
return normalizeBlockText(stripHtml(decodeXmlEntities(stripCdata(match[1]))));
|
|
279
|
+
}
|
|
280
|
+
function extractMediaLinksFromRssItem(block) {
|
|
281
|
+
const links = new Set();
|
|
282
|
+
const mediaRegex = /<(?:media:content|media:thumbnail|enclosure)\b[^>]*\burl="([^"]+)"[^>]*>/gi;
|
|
283
|
+
let match;
|
|
284
|
+
while ((match = mediaRegex.exec(block))) {
|
|
285
|
+
const url = decodeXmlEntities(match[1] || '').trim();
|
|
286
|
+
if (url)
|
|
287
|
+
links.add(url);
|
|
288
|
+
}
|
|
289
|
+
return [...links];
|
|
290
|
+
}
|
|
291
|
+
function collectMediaUrls(value, out, seen = new WeakSet()) {
|
|
292
|
+
if (value == null)
|
|
293
|
+
return;
|
|
294
|
+
if (typeof value === 'string') {
|
|
295
|
+
const normalized = normalizeMediaUrl(value);
|
|
296
|
+
if (normalized)
|
|
297
|
+
out.add(normalized);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(value)) {
|
|
301
|
+
for (const item of value)
|
|
302
|
+
collectMediaUrls(item, out, seen);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (typeof value === 'object') {
|
|
306
|
+
if (seen.has(value))
|
|
307
|
+
return;
|
|
308
|
+
seen.add(value);
|
|
309
|
+
for (const key of ['url', 'src', 'fallback', 'poster']) {
|
|
310
|
+
const candidate = value[key];
|
|
311
|
+
if (typeof candidate === 'string') {
|
|
312
|
+
const normalized = normalizeMediaUrl(candidate);
|
|
313
|
+
if (normalized)
|
|
314
|
+
out.add(normalized);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const entry of Object.values(value)) {
|
|
318
|
+
collectMediaUrls(entry, out, seen);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function normalizeMediaUrl(value) {
|
|
323
|
+
const url = decodeXmlEntities(String(value || '')).trim();
|
|
324
|
+
if (!/^https?:\/\//i.test(url))
|
|
325
|
+
return null;
|
|
326
|
+
if (!looksLikeMediaUrl(url))
|
|
327
|
+
return null;
|
|
328
|
+
return url;
|
|
329
|
+
}
|
|
330
|
+
function looksLikeMediaUrl(url) {
|
|
331
|
+
return /(?:assets\.bwbx\.io|resource\.bloomberg\.com|media\.bloomberg\.com)/i.test(url)
|
|
332
|
+
|| /\.(?:jpg|jpeg|png|webp|gif|svg|mp4|m3u8)(?:[?#].*)?$/i.test(url);
|
|
333
|
+
}
|
|
334
|
+
function stripCdata(value) {
|
|
335
|
+
const match = value.match(/^<!\[CDATA\[([\s\S]*?)\]\]>$/);
|
|
336
|
+
return match ? match[1] : value;
|
|
337
|
+
}
|
|
338
|
+
function stripHtml(value) {
|
|
339
|
+
return String(value || '').replace(/<[^>]+>/g, ' ');
|
|
340
|
+
}
|
|
341
|
+
function decodeXmlEntities(value) {
|
|
342
|
+
return String(value || '')
|
|
343
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
|
|
344
|
+
.replace(/&#(\d+);/g, (_m, code) => String.fromCodePoint(Number(code)))
|
|
345
|
+
.replace(/&#x([0-9a-f]+);/gi, (_m, code) => String.fromCodePoint(parseInt(code, 16)))
|
|
346
|
+
.replace(/&/g, '&')
|
|
347
|
+
.replace(/</g, '<')
|
|
348
|
+
.replace(/>/g, '>')
|
|
349
|
+
.replace(/"/g, '"')
|
|
350
|
+
.replace(/'/g, "'")
|
|
351
|
+
.replace(/'/g, "'")
|
|
352
|
+
.replace(/ /g, ' ');
|
|
353
|
+
}
|
|
354
|
+
function normalizeBlockText(value) {
|
|
355
|
+
return String(value || '')
|
|
356
|
+
.replace(/\r/g, '')
|
|
357
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
358
|
+
.replace(/\n[ \t]+/g, '\n')
|
|
359
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
360
|
+
.trim();
|
|
361
|
+
}
|
|
362
|
+
function escapeRegExp(value) {
|
|
363
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
364
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { extractStoryMediaLinks, parseBloombergRss, renderStoryBody } from './utils.js';
|
|
3
|
+
describe('Bloomberg utils', () => {
|
|
4
|
+
it('parses Bloomberg RSS items with summary, link, and deduped media links', () => {
|
|
5
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6
|
+
<rss xmlns:media="http://search.yahoo.com/mrss/">
|
|
7
|
+
<channel>
|
|
8
|
+
<item>
|
|
9
|
+
<title><![CDATA[Headline One]]></title>
|
|
10
|
+
<description><![CDATA[Summary <b>One</b> & more]]></description>
|
|
11
|
+
<link>https://www.bloomberg.com/news/articles/2026-03-19/example-one</link>
|
|
12
|
+
<media:content url="https://assets.bwbx.io/example-one.jpg" type="image/jpeg">
|
|
13
|
+
<media:thumbnail url="https://assets.bwbx.io/example-one.jpg" />
|
|
14
|
+
</media:content>
|
|
15
|
+
</item>
|
|
16
|
+
<item>
|
|
17
|
+
<title>Headline Two</title>
|
|
18
|
+
<description>Summary Two</description>
|
|
19
|
+
<guid isPermaLink="true">https://www.bloomberg.com/news/articles/2026-03-19/example-two</guid>
|
|
20
|
+
<enclosure url="https://assets.bwbx.io/example-two.png" type="image/png" />
|
|
21
|
+
</item>
|
|
22
|
+
</channel>
|
|
23
|
+
</rss>`;
|
|
24
|
+
const items = parseBloombergRss(xml);
|
|
25
|
+
expect(items).toHaveLength(2);
|
|
26
|
+
expect(items[0]).toEqual({
|
|
27
|
+
title: 'Headline One',
|
|
28
|
+
summary: 'Summary One & more',
|
|
29
|
+
link: 'https://www.bloomberg.com/news/articles/2026-03-19/example-one',
|
|
30
|
+
mediaLinks: ['https://assets.bwbx.io/example-one.jpg'],
|
|
31
|
+
});
|
|
32
|
+
expect(items[1]).toEqual({
|
|
33
|
+
title: 'Headline Two',
|
|
34
|
+
summary: 'Summary Two',
|
|
35
|
+
link: 'https://www.bloomberg.com/news/articles/2026-03-19/example-two',
|
|
36
|
+
mediaLinks: ['https://assets.bwbx.io/example-two.png'],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('renders Bloomberg story rich-text body into readable text', () => {
|
|
40
|
+
const body = {
|
|
41
|
+
type: 'document',
|
|
42
|
+
content: [
|
|
43
|
+
{ type: 'inline-newsletter', data: { position: 'top' }, content: [] },
|
|
44
|
+
{
|
|
45
|
+
type: 'paragraph',
|
|
46
|
+
data: {},
|
|
47
|
+
content: [
|
|
48
|
+
{ type: 'text', value: 'Lead paragraph with ' },
|
|
49
|
+
{ type: 'entity', content: [{ type: 'text', value: 'linked text' }] },
|
|
50
|
+
{ type: 'text', value: '.' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'heading',
|
|
55
|
+
data: { level: 2 },
|
|
56
|
+
content: [{ type: 'text', value: 'Key Points' }],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'list',
|
|
60
|
+
data: { style: 'unordered' },
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'list-item',
|
|
64
|
+
content: [
|
|
65
|
+
{ type: 'paragraph', content: [{ type: 'text', value: 'Point one' }] },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'list-item',
|
|
70
|
+
content: [
|
|
71
|
+
{ type: 'paragraph', content: [{ type: 'text', value: 'Point two' }] },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'blockquote',
|
|
78
|
+
content: [{ type: 'text', value: 'Quoted line' }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'media',
|
|
82
|
+
data: {
|
|
83
|
+
attachment: {
|
|
84
|
+
caption: '<p>Chart caption</p>',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{ type: 'ad', data: { num: 1 }, content: [] },
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
expect(renderStoryBody(body)).toBe([
|
|
92
|
+
'Lead paragraph with linked text.',
|
|
93
|
+
'## Key Points',
|
|
94
|
+
'- Point one\n- Point two',
|
|
95
|
+
'> Quoted line',
|
|
96
|
+
'Chart caption',
|
|
97
|
+
].join('\n\n'));
|
|
98
|
+
});
|
|
99
|
+
it('collects deduped story media links from lede, attachments, and body media', () => {
|
|
100
|
+
const story = {
|
|
101
|
+
ledeImageUrl: 'https://assets.bwbx.io/lede.webp',
|
|
102
|
+
lede: { url: 'https://assets.bwbx.io/lede.webp' },
|
|
103
|
+
socialImageUrl: 'https://assets.bwbx.io/social.png',
|
|
104
|
+
imageAttachments: {
|
|
105
|
+
one: { url: 'https://assets.bwbx.io/figure.jpg' },
|
|
106
|
+
},
|
|
107
|
+
body: {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'media',
|
|
111
|
+
data: {
|
|
112
|
+
chart: {
|
|
113
|
+
src: 'https://resource.bloomberg.com/images/chart.png',
|
|
114
|
+
fallback: 'https://assets.bwbx.io/chart-fallback.png',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
expect(extractStoryMediaLinks(story)).toEqual([
|
|
122
|
+
'https://assets.bwbx.io/lede.webp',
|
|
123
|
+
'https://assets.bwbx.io/social.png',
|
|
124
|
+
'https://assets.bwbx.io/figure.jpg',
|
|
125
|
+
'https://resource.bloomberg.com/images/chart.png',
|
|
126
|
+
'https://assets.bwbx.io/chart-fallback.png',
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOSS直聘 batchgreet — batch greet recommended candidates.
|
|
3
|
+
*
|
|
4
|
+
* Combines recommend (greetRecSortList) + greet (UI automation).
|
|
5
|
+
* Sends greeting messages to multiple candidates sequentially.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
cli({
|
|
9
|
+
site: 'boss',
|
|
10
|
+
name: 'batchgreet',
|
|
11
|
+
description: 'BOSS直聘批量向推荐候选人发送招呼',
|
|
12
|
+
domain: 'www.zhipin.com',
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
browser: true,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'job-id', default: '', help: 'Filter by encrypted job ID (greet all jobs if empty)' },
|
|
17
|
+
{ name: 'limit', type: 'int', default: 5, help: 'Max candidates to greet' },
|
|
18
|
+
{ name: 'text', default: '', help: 'Custom greeting message (uses default if empty)' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['name', 'status', 'detail'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
if (!page)
|
|
23
|
+
throw new Error('Browser page required');
|
|
24
|
+
const filterJobId = kwargs['job-id'] || '';
|
|
25
|
+
const limit = kwargs.limit || 5;
|
|
26
|
+
const text = kwargs.text || '你好,请问您对这个职位感兴趣吗?';
|
|
27
|
+
if (process.env.OPENCLI_VERBOSE) {
|
|
28
|
+
console.error(`[opencli:boss] Batch greeting up to ${limit} candidates...`);
|
|
29
|
+
}
|
|
30
|
+
await page.goto('https://www.zhipin.com/web/chat/index');
|
|
31
|
+
await page.wait({ time: 3 });
|
|
32
|
+
// Get recommended candidates
|
|
33
|
+
const listData = await page.evaluate(`
|
|
34
|
+
async () => {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const xhr = new XMLHttpRequest();
|
|
37
|
+
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
|
|
38
|
+
xhr.withCredentials = true;
|
|
39
|
+
xhr.timeout = 15000;
|
|
40
|
+
xhr.setRequestHeader('Accept', 'application/json');
|
|
41
|
+
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
|
|
42
|
+
xhr.onerror = () => reject(new Error('Network Error'));
|
|
43
|
+
xhr.send();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
`);
|
|
47
|
+
if (listData.code !== 0) {
|
|
48
|
+
if (listData.code === 7 || listData.code === 37) {
|
|
49
|
+
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`获取推荐列表失败: ${listData.message}`);
|
|
52
|
+
}
|
|
53
|
+
let candidates = listData.zpData?.friendList || [];
|
|
54
|
+
if (filterJobId) {
|
|
55
|
+
candidates = candidates.filter((f) => f.encryptJobId === filterJobId);
|
|
56
|
+
}
|
|
57
|
+
candidates = candidates.slice(0, limit);
|
|
58
|
+
if (candidates.length === 0) {
|
|
59
|
+
return [{ name: '-', status: '⚠️ 无候选人', detail: '当前没有待招呼的推荐候选人' }];
|
|
60
|
+
}
|
|
61
|
+
const results = [];
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
const numericUid = candidate.uid;
|
|
64
|
+
const friendName = candidate.name || '候选人';
|
|
65
|
+
try {
|
|
66
|
+
// Click on candidate
|
|
67
|
+
const clicked = await page.evaluate(`
|
|
68
|
+
async () => {
|
|
69
|
+
const item = document.querySelector('#_${numericUid}-0') || document.querySelector('[id^="_${numericUid}"]');
|
|
70
|
+
if (item) {
|
|
71
|
+
item.click();
|
|
72
|
+
return { clicked: true };
|
|
73
|
+
}
|
|
74
|
+
const items = document.querySelectorAll('.geek-item');
|
|
75
|
+
for (const el of items) {
|
|
76
|
+
if (el.id && el.id.startsWith('_${numericUid}')) {
|
|
77
|
+
el.click();
|
|
78
|
+
return { clicked: true };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { clicked: false };
|
|
82
|
+
}
|
|
83
|
+
`);
|
|
84
|
+
if (!clicked.clicked) {
|
|
85
|
+
results.push({ name: friendName, status: '❌ 跳过', detail: '在聊天列表中未找到' });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
await page.wait({ time: 2 });
|
|
89
|
+
// Type message
|
|
90
|
+
const typed = await page.evaluate(`
|
|
91
|
+
async () => {
|
|
92
|
+
const selectors = [
|
|
93
|
+
'.chat-editor [contenteditable="true"]',
|
|
94
|
+
'.chat-input [contenteditable="true"]',
|
|
95
|
+
'[contenteditable="true"]',
|
|
96
|
+
'textarea',
|
|
97
|
+
];
|
|
98
|
+
for (const sel of selectors) {
|
|
99
|
+
const el = document.querySelector(sel);
|
|
100
|
+
if (el && el.offsetParent !== null) {
|
|
101
|
+
el.focus();
|
|
102
|
+
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
|
103
|
+
el.value = ${JSON.stringify(text)};
|
|
104
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
105
|
+
} else {
|
|
106
|
+
el.textContent = '';
|
|
107
|
+
el.focus();
|
|
108
|
+
document.execCommand('insertText', false, ${JSON.stringify(text)});
|
|
109
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
110
|
+
}
|
|
111
|
+
return { found: true };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { found: false };
|
|
115
|
+
}
|
|
116
|
+
`);
|
|
117
|
+
if (!typed.found) {
|
|
118
|
+
results.push({ name: friendName, status: '❌ 失败', detail: '找不到消息输入框' });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
await page.wait({ time: 0.5 });
|
|
122
|
+
// Click send
|
|
123
|
+
const sent = await page.evaluate(`
|
|
124
|
+
async () => {
|
|
125
|
+
const btn = document.querySelector('.conversation-editor .submit')
|
|
126
|
+
|| document.querySelector('.submit-content .submit')
|
|
127
|
+
|| document.querySelector('.conversation-operate .submit');
|
|
128
|
+
if (btn) {
|
|
129
|
+
btn.click();
|
|
130
|
+
return { clicked: true };
|
|
131
|
+
}
|
|
132
|
+
return { clicked: false };
|
|
133
|
+
}
|
|
134
|
+
`);
|
|
135
|
+
if (!sent.clicked) {
|
|
136
|
+
await page.pressKey('Enter');
|
|
137
|
+
}
|
|
138
|
+
await page.wait({ time: 1.5 });
|
|
139
|
+
results.push({ name: friendName, status: '✅ 已发送', detail: text });
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
results.push({ name: friendName, status: '❌ 失败', detail: e.message?.substring(0, 80) || '未知错误' });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
@@ -9,7 +9,7 @@ cli({
|
|
|
9
9
|
args: [
|
|
10
10
|
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
11
11
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
12
|
-
{ name: '
|
|
12
|
+
{ name: 'job-id', default: '0', help: 'Filter by job ID (0=all)' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['name', 'job', 'last_msg', 'last_time', 'uid', 'security_id'],
|
|
15
15
|
func: async (page, kwargs) => {
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
throw new Error('Browser page required');
|
|
18
18
|
await page.goto('https://www.zhipin.com/web/chat/index');
|
|
19
19
|
await page.wait({ time: 2 });
|
|
20
|
-
const jobId = kwargs
|
|
20
|
+
const jobId = kwargs['job-id'] || '0';
|
|
21
21
|
const pageNum = kwargs.page || 1;
|
|
22
22
|
const limit = kwargs.limit || 20;
|
|
23
23
|
const targetUrl = `https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=${pageNum}&status=0&jobId=${jobId}`;
|
package/dist/clis/boss/detail.js
CHANGED
|
@@ -13,7 +13,7 @@ cli({
|
|
|
13
13
|
strategy: Strategy.COOKIE,
|
|
14
14
|
browser: true,
|
|
15
15
|
args: [
|
|
16
|
-
{ name: '
|
|
16
|
+
{ name: 'security-id', required: true, help: 'Security ID from search results (securityId field)' },
|
|
17
17
|
],
|
|
18
18
|
columns: [
|
|
19
19
|
'name', 'salary', 'experience', 'degree', 'city', 'district',
|
|
@@ -25,7 +25,7 @@ cli({
|
|
|
25
25
|
func: async (page, kwargs) => {
|
|
26
26
|
if (!page)
|
|
27
27
|
throw new Error('Browser page required');
|
|
28
|
-
const securityId = kwargs
|
|
28
|
+
const securityId = kwargs['security-id'];
|
|
29
29
|
// Navigate to zhipin.com first to establish cookie context (referrer + cookies)
|
|
30
30
|
await page.goto('https://www.zhipin.com/web/geek/job');
|
|
31
31
|
await page.wait({ time: 1 });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|