@jackwener/opencli 1.6.0 → 1.6.2
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 +8 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -45
- package/README.zh-CN.md +32 -34
- package/autoresearch/browse-tasks.json +18 -20
- package/autoresearch/commands/debug.ts +163 -0
- package/autoresearch/commands/fix.ts +145 -0
- package/autoresearch/commands/plan.ts +88 -0
- package/autoresearch/commands/run.ts +138 -0
- package/autoresearch/config.ts +82 -0
- package/autoresearch/engine.ts +359 -0
- package/autoresearch/eval-all.ts +127 -0
- package/autoresearch/eval-browse.ts +1 -1
- package/autoresearch/eval-publish.ts +238 -0
- package/autoresearch/eval-save.ts +249 -0
- package/autoresearch/eval-skill.ts +14 -8
- package/autoresearch/eval-v2ex.ts +220 -0
- package/autoresearch/eval-zhihu.ts +230 -0
- package/autoresearch/logger.ts +69 -0
- package/autoresearch/presets/combined-reliability.ts +27 -0
- package/autoresearch/presets/index.ts +23 -0
- package/autoresearch/presets/operate-reliability.ts +24 -0
- package/autoresearch/presets/save-reliability.ts +26 -0
- package/autoresearch/presets/skill-quality.ts +20 -0
- package/autoresearch/presets/v2ex-reliability.ts +24 -0
- package/autoresearch/presets/zhihu-reliability.ts +25 -0
- package/autoresearch/publish-tasks.json +345 -0
- package/autoresearch/run-save.sh +11 -0
- package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
- package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
- package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
- package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
- package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
- package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
- package/autoresearch/save-tasks.json +281 -0
- package/autoresearch/v2ex-tasks.json +899 -0
- package/autoresearch/zhihu-tasks.json +848 -0
- package/bun.lock +615 -0
- package/dist/browser/base-page.d.ts +4 -2
- package/dist/browser/base-page.js +37 -4
- package/dist/browser/bridge.js +10 -8
- package/dist/browser/cdp.js +2 -6
- package/dist/browser/daemon-client.d.ts +11 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.d.ts +4 -2
- package/dist/browser/dom-helpers.js +42 -31
- package/dist/browser/dom-snapshot.js +23 -1
- package/dist/browser/page.d.ts +7 -2
- package/dist/browser/page.js +112 -30
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +1 -0
- package/dist/cli-manifest.json +1133 -182
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +48 -7
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +88 -0
- package/dist/clis/1688/item.d.ts +70 -0
- package/dist/clis/1688/item.js +187 -0
- package/dist/clis/1688/item.test.d.ts +1 -0
- package/dist/clis/1688/item.test.js +67 -0
- package/dist/clis/1688/search.d.ts +56 -0
- package/dist/clis/1688/search.js +309 -0
- package/dist/clis/1688/search.test.d.ts +1 -0
- package/dist/clis/1688/search.test.js +75 -0
- package/dist/clis/1688/shared.d.ts +112 -0
- package/dist/clis/1688/shared.js +514 -0
- package/dist/clis/1688/shared.test.d.ts +1 -0
- package/dist/clis/1688/shared.test.js +57 -0
- package/dist/clis/1688/store.d.ts +45 -0
- package/dist/clis/1688/store.js +226 -0
- package/dist/clis/1688/store.test.d.ts +1 -0
- package/dist/clis/1688/store.test.js +62 -0
- package/dist/clis/amazon/bestsellers.d.ts +0 -20
- package/dist/clis/amazon/bestsellers.js +6 -129
- package/dist/clis/amazon/bestsellers.test.js +12 -3
- package/dist/clis/amazon/movers-shakers.d.ts +1 -0
- package/dist/clis/amazon/movers-shakers.js +7 -0
- package/dist/clis/amazon/new-releases.d.ts +1 -0
- package/dist/clis/amazon/new-releases.js +7 -0
- package/dist/clis/amazon/rankings.d.ts +59 -0
- package/dist/clis/amazon/rankings.js +226 -0
- package/dist/clis/amazon/rankings.test.d.ts +1 -0
- package/dist/clis/amazon/rankings.test.js +41 -0
- package/dist/clis/amazon/shared.d.ts +11 -0
- package/dist/clis/amazon/shared.js +121 -11
- package/dist/clis/amazon/shared.test.js +11 -0
- package/dist/clis/bilibili/comments.js +2 -2
- package/dist/clis/bilibili/comments.test.js +3 -2
- package/dist/clis/bilibili/download.js +2 -1
- package/dist/clis/bilibili/subtitle.js +4 -3
- package/dist/clis/bilibili/subtitle.test.js +2 -1
- package/dist/clis/bilibili/utils.d.ts +5 -0
- package/dist/clis/bilibili/utils.js +30 -0
- package/dist/clis/bilibili/utils.test.d.ts +1 -0
- package/dist/clis/bilibili/utils.test.js +17 -0
- package/dist/clis/douban/marks.js +1 -1
- package/dist/clis/douban/subject.yaml +50 -19
- package/dist/clis/doubao/utils.js +32 -12
- package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
- package/dist/clis/douyin/_shared/transcode.test.js +0 -2
- package/dist/clis/douyin/draft.test.js +0 -2
- package/dist/clis/facebook/search.test.js +0 -2
- package/dist/clis/gemini/ask.js +9 -3
- package/dist/clis/gemini/ask.test.d.ts +1 -0
- package/dist/clis/gemini/ask.test.js +100 -0
- package/dist/clis/gemini/reply-state.test.d.ts +1 -0
- package/dist/clis/gemini/reply-state.test.js +641 -0
- package/dist/clis/gemini/utils.d.ts +44 -1
- package/dist/clis/gemini/utils.js +528 -61
- package/dist/clis/gemini/utils.test.js +149 -2
- package/dist/clis/hupu/detail.d.ts +1 -0
- package/dist/clis/hupu/detail.js +72 -0
- package/dist/clis/hupu/hot.yaml +43 -0
- package/dist/clis/hupu/like.d.ts +1 -0
- package/dist/clis/hupu/like.js +75 -0
- package/dist/clis/hupu/reply.d.ts +1 -0
- package/dist/clis/hupu/reply.js +71 -0
- package/dist/clis/hupu/search.d.ts +1 -0
- package/dist/clis/hupu/search.js +59 -0
- package/dist/clis/hupu/unlike.d.ts +1 -0
- package/dist/clis/hupu/unlike.js +75 -0
- package/dist/clis/hupu/utils.d.ts +20 -0
- package/dist/clis/hupu/utils.js +319 -0
- package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
- package/dist/clis/instagram/_shared/private-publish.js +1030 -0
- package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
- package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
- package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
- package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
- package/dist/clis/instagram/_shared/runtime-info.js +81 -0
- package/dist/clis/instagram/note.d.ts +1 -0
- package/dist/clis/instagram/note.js +222 -0
- package/dist/clis/instagram/note.test.d.ts +1 -0
- package/dist/clis/instagram/note.test.js +81 -0
- package/dist/clis/instagram/post.d.ts +4 -0
- package/dist/clis/instagram/post.js +1496 -0
- package/dist/clis/instagram/post.test.d.ts +1 -0
- package/dist/clis/instagram/post.test.js +1647 -0
- package/dist/clis/instagram/reel.d.ts +1 -0
- package/dist/clis/instagram/reel.js +826 -0
- package/dist/clis/instagram/reel.test.d.ts +1 -0
- package/dist/clis/instagram/reel.test.js +167 -0
- package/dist/clis/instagram/story.d.ts +1 -0
- package/dist/clis/instagram/story.js +115 -0
- package/dist/clis/instagram/story.test.d.ts +1 -0
- package/dist/clis/instagram/story.test.js +167 -0
- package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
- package/dist/clis/sinafinance/stock-rank.js +65 -0
- package/dist/clis/substack/utils.test.js +0 -2
- package/dist/clis/twitter/post.js +72 -45
- package/dist/clis/twitter/post.test.d.ts +1 -0
- package/dist/clis/twitter/post.test.js +116 -0
- package/dist/clis/twitter/reply.d.ts +12 -0
- package/dist/clis/twitter/reply.js +257 -35
- package/dist/clis/twitter/reply.test.d.ts +1 -0
- package/dist/clis/twitter/reply.test.js +151 -0
- package/dist/clis/twitter/search.js +67 -5
- package/dist/clis/twitter/search.test.js +83 -5
- package/dist/clis/xianyu/chat.d.ts +7 -0
- package/dist/clis/xianyu/chat.js +146 -0
- package/dist/clis/xianyu/chat.test.d.ts +1 -0
- package/dist/clis/xianyu/chat.test.js +15 -0
- package/dist/clis/xianyu/item.d.ts +7 -0
- package/dist/clis/xianyu/item.js +152 -0
- package/dist/clis/xianyu/item.test.d.ts +1 -0
- package/dist/clis/xianyu/item.test.js +56 -0
- package/dist/clis/xianyu/search.d.ts +10 -0
- package/dist/clis/xianyu/search.js +134 -0
- package/dist/clis/xianyu/search.test.d.ts +1 -0
- package/dist/clis/xianyu/search.test.js +17 -0
- package/dist/clis/xianyu/utils.d.ts +1 -0
- package/dist/clis/xianyu/utils.js +8 -0
- package/dist/clis/xiaoe/catalog.yaml +129 -0
- package/dist/clis/xiaoe/content.yaml +43 -0
- package/dist/clis/xiaoe/courses.yaml +73 -0
- package/dist/clis/xiaoe/detail.yaml +39 -0
- package/dist/clis/xiaoe/play-url.yaml +124 -0
- package/dist/clis/xiaohongshu/comments.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
- package/dist/clis/xiaohongshu/download.test.js +0 -2
- package/dist/clis/xiaohongshu/note.test.js +0 -2
- package/dist/clis/xiaohongshu/publish.test.js +0 -2
- package/dist/clis/xiaohongshu/search.js +29 -20
- package/dist/clis/xiaohongshu/search.test.js +56 -48
- package/dist/clis/yuanbao/ask.d.ts +21 -0
- package/dist/clis/yuanbao/ask.js +427 -0
- package/dist/clis/yuanbao/ask.test.d.ts +1 -0
- package/dist/clis/yuanbao/ask.test.js +124 -0
- package/dist/clis/yuanbao/new.d.ts +1 -0
- package/dist/clis/yuanbao/new.js +70 -0
- package/dist/clis/yuanbao/new.test.d.ts +1 -0
- package/dist/clis/yuanbao/new.test.js +30 -0
- package/dist/clis/yuanbao/shared.d.ts +13 -0
- package/dist/clis/yuanbao/shared.js +49 -0
- package/dist/clis/zhihu/question.js +30 -19
- package/dist/clis/zhihu/question.test.js +34 -16
- package/dist/commanderAdapter.js +8 -4
- package/dist/commanderAdapter.test.js +42 -0
- package/dist/completion.js +3 -1
- package/dist/completion.test.d.ts +1 -0
- package/dist/completion.test.js +23 -0
- package/dist/doctor.js +1 -1
- package/dist/electron-apps.d.ts +2 -0
- package/dist/electron-apps.js +7 -1
- package/dist/errors.js +1 -1
- package/dist/execution.js +25 -35
- package/dist/explore.js +1 -1
- package/dist/launcher.d.ts +4 -0
- package/dist/launcher.js +64 -8
- package/dist/launcher.test.js +88 -7
- package/dist/output.d.ts +2 -0
- package/dist/output.js +10 -1
- package/dist/output.test.d.ts +0 -3
- package/dist/output.test.js +59 -92
- package/dist/pipeline/executor.test.js +0 -2
- package/dist/pipeline/steps/download.test.js +0 -2
- package/dist/registry.d.ts +2 -0
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +1 -0
- package/dist/types.d.ts +9 -2
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/1688.md +52 -0
- package/docs/adapters/browser/36kr.md +2 -1
- package/docs/adapters/browser/doubao.md +5 -1
- package/docs/adapters/browser/hupu.md +53 -0
- package/docs/adapters/browser/sinafinance.md +32 -2
- package/docs/adapters/browser/weibo.md +6 -1
- package/docs/adapters/browser/wikipedia.md +2 -0
- package/docs/adapters/browser/xianyu.md +42 -0
- package/docs/adapters/browser/xiaoe.md +44 -0
- package/docs/adapters/browser/yuanbao.md +64 -0
- package/docs/adapters/index.md +14 -5
- package/docs/comparison.md +1 -1
- package/docs/developer/ai-workflow.md +2 -2
- package/docs/developer/contributing.md +1 -1
- package/docs/developer/testing.md +2 -0
- package/docs/guide/plugins.md +1 -0
- package/docs/guide/troubleshooting.md +11 -0
- package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
- package/docs/zh/guide/plugins.md +1 -0
- package/extension/dist/background.js +1127 -0
- package/extension/src/background.test.ts +39 -0
- package/extension/src/background.ts +223 -34
- package/extension/src/cdp.ts +194 -4
- package/extension/src/protocol.ts +22 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +1 -1
- package/skills/opencli-explorer/SKILL.md +1 -1
- package/skills/opencli-oneshot/SKILL.md +2 -2
- package/skills/opencli-operate/SKILL.md +120 -27
- package/skills/opencli-usage/SKILL.md +31 -20
- package/skills/opencli-usage/browser.md +114 -16
- package/skills/opencli-usage/public-api.md +32 -3
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +50 -0
- package/skills/smart-search/references/sources-other.md +42 -0
- package/skills/smart-search/references/sources-shopping.md +31 -0
- package/skills/smart-search/references/sources-social.md +51 -0
- package/skills/smart-search/references/sources-tech.md +42 -0
- package/skills/smart-search/references/sources-travel.md +20 -0
- package/src/browser/base-page.ts +41 -6
- package/src/browser/bridge.ts +11 -8
- package/src/browser/cdp.ts +1 -8
- package/src/browser/daemon-client.ts +11 -1
- package/src/browser/dom-helpers.ts +43 -31
- package/src/browser/dom-snapshot.ts +23 -1
- package/src/browser/page.ts +115 -31
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.ts +2 -0
- package/src/cli.test.ts +133 -0
- package/src/cli.ts +73 -11
- package/src/clis/1688/item.test.ts +69 -0
- package/src/clis/1688/item.ts +282 -0
- package/src/clis/1688/search.test.ts +81 -0
- package/src/clis/1688/search.ts +402 -0
- package/src/clis/1688/shared.test.ts +75 -0
- package/src/clis/1688/shared.ts +623 -0
- package/src/clis/1688/store.test.ts +69 -0
- package/src/clis/1688/store.ts +300 -0
- package/src/clis/amazon/bestsellers.test.ts +12 -3
- package/src/clis/amazon/bestsellers.ts +6 -178
- package/src/clis/amazon/movers-shakers.ts +8 -0
- package/src/clis/amazon/new-releases.ts +8 -0
- package/src/clis/amazon/rankings.test.ts +47 -0
- package/src/clis/amazon/rankings.ts +312 -0
- package/src/clis/amazon/shared.test.ts +16 -0
- package/src/clis/amazon/shared.ts +134 -12
- package/src/clis/bilibili/comments.test.ts +4 -3
- package/src/clis/bilibili/comments.ts +2 -2
- package/src/clis/bilibili/download.ts +2 -1
- package/src/clis/bilibili/subtitle.test.ts +2 -1
- package/src/clis/bilibili/subtitle.ts +4 -3
- package/src/clis/bilibili/utils.test.ts +21 -0
- package/src/clis/bilibili/utils.ts +27 -0
- package/src/clis/douban/marks.ts +1 -1
- package/src/clis/douban/subject.yaml +50 -19
- package/src/clis/doubao/utils.ts +32 -12
- package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
- package/src/clis/douyin/_shared/transcode.test.ts +0 -2
- package/src/clis/douyin/draft.test.ts +0 -2
- package/src/clis/facebook/search.test.ts +0 -2
- package/src/clis/gemini/ask.test.ts +116 -0
- package/src/clis/gemini/ask.ts +10 -3
- package/src/clis/gemini/reply-state.test.ts +708 -0
- package/src/clis/gemini/utils.test.ts +184 -2
- package/src/clis/gemini/utils.ts +588 -60
- package/src/clis/hupu/detail.ts +126 -0
- package/src/clis/hupu/hot.yaml +43 -0
- package/src/clis/hupu/like.ts +76 -0
- package/src/clis/hupu/reply.ts +76 -0
- package/src/clis/hupu/search.ts +95 -0
- package/src/clis/hupu/unlike.ts +76 -0
- package/src/clis/hupu/utils.ts +381 -0
- package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
- package/src/clis/instagram/_shared/private-publish.ts +1303 -0
- package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
- package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
- package/src/clis/instagram/_shared/runtime-info.ts +91 -0
- package/src/clis/instagram/note.test.ts +96 -0
- package/src/clis/instagram/note.ts +254 -0
- package/src/clis/instagram/post.test.ts +1716 -0
- package/src/clis/instagram/post.ts +1620 -0
- package/src/clis/instagram/reel.test.ts +191 -0
- package/src/clis/instagram/reel.ts +886 -0
- package/src/clis/instagram/story.test.ts +191 -0
- package/src/clis/instagram/story.ts +151 -0
- package/src/clis/sinafinance/stock-rank.ts +68 -0
- package/src/clis/substack/utils.test.ts +0 -2
- package/src/clis/twitter/post.test.ts +157 -0
- package/src/clis/twitter/post.ts +82 -48
- package/src/clis/twitter/reply.test.ts +177 -0
- package/src/clis/twitter/reply.ts +285 -39
- package/src/clis/twitter/search.test.ts +88 -5
- package/src/clis/twitter/search.ts +68 -5
- package/src/clis/xianyu/chat.test.ts +20 -0
- package/src/clis/xianyu/chat.ts +175 -0
- package/src/clis/xianyu/item.test.ts +67 -0
- package/src/clis/xianyu/item.ts +172 -0
- package/src/clis/xianyu/search.test.ts +22 -0
- package/src/clis/xianyu/search.ts +151 -0
- package/src/clis/xianyu/utils.ts +9 -0
- package/src/clis/xiaoe/catalog.yaml +129 -0
- package/src/clis/xiaoe/content.yaml +43 -0
- package/src/clis/xiaoe/courses.yaml +73 -0
- package/src/clis/xiaoe/detail.yaml +39 -0
- package/src/clis/xiaoe/play-url.yaml +124 -0
- package/src/clis/xiaohongshu/comments.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
- package/src/clis/xiaohongshu/download.test.ts +0 -2
- package/src/clis/xiaohongshu/note.test.ts +0 -2
- package/src/clis/xiaohongshu/publish.test.ts +0 -2
- package/src/clis/xiaohongshu/search.test.ts +59 -48
- package/src/clis/xiaohongshu/search.ts +31 -21
- package/src/clis/yuanbao/ask.test.ts +156 -0
- package/src/clis/yuanbao/ask.ts +522 -0
- package/src/clis/yuanbao/new.test.ts +36 -0
- package/src/clis/yuanbao/new.ts +81 -0
- package/src/clis/yuanbao/shared.ts +57 -0
- package/src/clis/zhihu/question.test.ts +42 -17
- package/src/clis/zhihu/question.ts +31 -26
- package/src/commanderAdapter.test.ts +51 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/completion.test.ts +30 -0
- package/src/completion.ts +3 -1
- package/src/doctor.ts +1 -1
- package/src/electron-apps.ts +9 -1
- package/src/errors.ts +1 -1
- package/src/execution.ts +26 -30
- package/src/explore.ts +1 -1
- package/src/launcher.test.ts +121 -7
- package/src/launcher.ts +87 -9
- package/src/output.test.ts +50 -90
- package/src/output.ts +10 -1
- package/src/pipeline/executor.test.ts +0 -2
- package/src/pipeline/steps/download.test.ts +0 -2
- package/src/registry.ts +2 -0
- package/src/serialization.ts +2 -0
- package/src/types.ts +9 -2
- package/tests/e2e/browser-auth.test.ts +9 -0
- package/CLI-EXPLORER.md +0 -724
- package/CLI-ONESHOT.md +0 -216
- package/SKILL.md +0 -59
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './reel.js';
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { ArgumentError } from '../../errors.js';
|
|
6
|
+
import { getRegistry } from '../../registry.js';
|
|
7
|
+
import './reel.js';
|
|
8
|
+
const tempDirs = [];
|
|
9
|
+
function createTempVideo(name = 'demo.mp4', bytes = Buffer.from('video')) {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-reel-'));
|
|
11
|
+
tempDirs.push(dir);
|
|
12
|
+
const filePath = path.join(dir, name);
|
|
13
|
+
fs.writeFileSync(filePath, bytes);
|
|
14
|
+
return filePath;
|
|
15
|
+
}
|
|
16
|
+
function createPageMock(evaluateResults, overrides = {}) {
|
|
17
|
+
const evaluate = vi.fn();
|
|
18
|
+
for (const result of evaluateResults) {
|
|
19
|
+
evaluate.mockResolvedValueOnce(result);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
evaluate,
|
|
24
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
25
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
31
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
33
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
37
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
38
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
39
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
41
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
42
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
44
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
insertText: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
getCurrentUrl: vi.fn().mockResolvedValue(null),
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
afterAll(() => {
|
|
51
|
+
for (const dir of tempDirs) {
|
|
52
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
describe('instagram reel registration', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.restoreAllMocks();
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.restoreAllMocks();
|
|
61
|
+
});
|
|
62
|
+
it('registers the reel command with a required-value video arg', () => {
|
|
63
|
+
const cmd = getRegistry().get('instagram/reel');
|
|
64
|
+
expect(cmd).toBeDefined();
|
|
65
|
+
expect(cmd?.browser).toBe(true);
|
|
66
|
+
expect(cmd?.args.some((arg) => arg.name === 'video' && !arg.required && arg.valueRequired)).toBe(true);
|
|
67
|
+
expect(cmd?.args.some((arg) => arg.name === 'content' && arg.positional && !arg.required)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('rejects missing --video before browser work', async () => {
|
|
70
|
+
const page = createPageMock([]);
|
|
71
|
+
const cmd = getRegistry().get('instagram/reel');
|
|
72
|
+
await expect(cmd.func(page, { content: 'hello reel' })).rejects.toThrow(ArgumentError);
|
|
73
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
it('rejects unsupported video formats', async () => {
|
|
76
|
+
const videoPath = createTempVideo('demo.mov');
|
|
77
|
+
const page = createPageMock([]);
|
|
78
|
+
const cmd = getRegistry().get('instagram/reel');
|
|
79
|
+
await expect(cmd.func(page, { video: videoPath })).rejects.toThrow('Unsupported video format');
|
|
80
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
it('uploads a reel video without caption and shares it', async () => {
|
|
83
|
+
const videoPath = createTempVideo();
|
|
84
|
+
const page = createPageMock([
|
|
85
|
+
{ ok: false }, // dismiss residual dialogs
|
|
86
|
+
{ ok: true }, // ensure composer open
|
|
87
|
+
{ ok: true }, // composer upload input ready
|
|
88
|
+
{ ok: true, selectors: ['[data-opencli-reel-upload-index="0"]', '[data-opencli-reel-upload-index="1"]'] }, // resolve upload selector
|
|
89
|
+
{ count: 1 }, // file bound to input
|
|
90
|
+
{ state: 'preview', detail: 'Crop Back Next' }, // preview detected
|
|
91
|
+
{ ok: true, label: 'OK' }, // dismiss reels nux
|
|
92
|
+
{ ok: true, label: 'Next' }, // move from crop to edit
|
|
93
|
+
{ state: 'edit' }, // edit stage
|
|
94
|
+
{ ok: true, label: 'Next' }, // move from edit to composer
|
|
95
|
+
{ state: 'composer' }, // composer stage
|
|
96
|
+
{ ok: true, label: 'Share' }, // share
|
|
97
|
+
{ ok: true, url: 'https://www.instagram.com/reel/REEL123/' }, // success
|
|
98
|
+
]);
|
|
99
|
+
const cmd = getRegistry().get('instagram/reel');
|
|
100
|
+
const result = await cmd.func(page, { video: videoPath });
|
|
101
|
+
expect(page.setFileInput).toHaveBeenCalledWith([videoPath], '[data-opencli-reel-upload-index="0"]');
|
|
102
|
+
expect(page.insertText).not.toHaveBeenCalled();
|
|
103
|
+
expect(result).toEqual([
|
|
104
|
+
{
|
|
105
|
+
status: '✅ Posted',
|
|
106
|
+
detail: 'Single reel shared successfully',
|
|
107
|
+
url: 'https://www.instagram.com/reel/REEL123/',
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
it('copies query-style local video filenames to a safe temp upload path before setFileInput', async () => {
|
|
112
|
+
const videoPath = createTempVideo('demo.mp4?sign=abc&t=123video.MP4');
|
|
113
|
+
const page = createPageMock([
|
|
114
|
+
{ ok: false },
|
|
115
|
+
{ ok: true },
|
|
116
|
+
{ ok: true },
|
|
117
|
+
{ ok: true, selectors: ['[data-opencli-reel-upload-index="0"]'] },
|
|
118
|
+
{ count: 1 },
|
|
119
|
+
{ state: 'preview', detail: 'Crop Back Next' },
|
|
120
|
+
{ ok: true, label: 'OK' },
|
|
121
|
+
{ ok: true, label: 'Next' },
|
|
122
|
+
{ state: 'edit' },
|
|
123
|
+
{ ok: true, label: 'Next' },
|
|
124
|
+
{ state: 'composer' },
|
|
125
|
+
{ ok: true, label: 'Share' },
|
|
126
|
+
{ ok: true, url: 'https://www.instagram.com/reel/REELSAFE123/' },
|
|
127
|
+
]);
|
|
128
|
+
const cmd = getRegistry().get('instagram/reel');
|
|
129
|
+
await cmd.func(page, { video: videoPath });
|
|
130
|
+
const uploadPaths = page.setFileInput.mock.calls[0]?.[0] ?? [];
|
|
131
|
+
expect(uploadPaths).toHaveLength(1);
|
|
132
|
+
expect(uploadPaths[0]).not.toBe(videoPath);
|
|
133
|
+
expect(String(uploadPaths[0])).toContain('opencli-instagram-video-real');
|
|
134
|
+
expect(String(uploadPaths[0]).toLowerCase()).toContain('.mp4');
|
|
135
|
+
});
|
|
136
|
+
it('uploads a reel video with caption and shares it', async () => {
|
|
137
|
+
const videoPath = createTempVideo('captioned.mp4');
|
|
138
|
+
const page = createPageMock([
|
|
139
|
+
{ ok: false }, // dismiss residual dialogs
|
|
140
|
+
{ ok: true }, // ensure composer open
|
|
141
|
+
{ ok: true }, // composer upload input ready
|
|
142
|
+
{ ok: true, selectors: ['[data-opencli-reel-upload-index="0"]'] }, // resolve upload selector
|
|
143
|
+
{ count: 1 }, // file bound to input
|
|
144
|
+
{ state: 'preview', detail: 'Crop Back Next' }, // preview detected
|
|
145
|
+
{ ok: true, label: 'OK' }, // dismiss reels nux
|
|
146
|
+
{ ok: true, label: 'Next' }, // move from crop to edit
|
|
147
|
+
{ state: 'edit' }, // edit stage
|
|
148
|
+
{ ok: true, label: 'Next' }, // move from edit to composer
|
|
149
|
+
{ state: 'composer' }, // composer stage
|
|
150
|
+
{ ok: true }, // focus caption editor
|
|
151
|
+
{ ok: true }, // post-insert event dispatch
|
|
152
|
+
{ ok: true }, // caption matches
|
|
153
|
+
{ ok: true, label: 'Share' }, // share
|
|
154
|
+
{ ok: true, url: 'https://www.instagram.com/reel/REEL456/' }, // success
|
|
155
|
+
]);
|
|
156
|
+
const cmd = getRegistry().get('instagram/reel');
|
|
157
|
+
const result = await cmd.func(page, { video: videoPath, content: 'hello reel' });
|
|
158
|
+
expect(page.insertText).toHaveBeenCalledWith('hello reel');
|
|
159
|
+
expect(result).toEqual([
|
|
160
|
+
{
|
|
161
|
+
status: '✅ Posted',
|
|
162
|
+
detail: 'Single reel shared successfully',
|
|
163
|
+
url: 'https://www.instagram.com/reel/REEL456/',
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ArgumentError, CommandExecutionError } from '../../errors.js';
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
import { publishStoryViaPrivateApi, resolveInstagramPrivatePublishConfig, } from './_shared/private-publish.js';
|
|
6
|
+
import { resolveInstagramRuntimeInfo } from './_shared/runtime-info.js';
|
|
7
|
+
const INSTAGRAM_HOME_URL = 'https://www.instagram.com/';
|
|
8
|
+
const SUPPORTED_STORY_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|
9
|
+
const SUPPORTED_STORY_VIDEO_EXTENSIONS = new Set(['.mp4']);
|
|
10
|
+
function requirePage(page) {
|
|
11
|
+
if (!page)
|
|
12
|
+
throw new CommandExecutionError('Browser session required for instagram story');
|
|
13
|
+
return page;
|
|
14
|
+
}
|
|
15
|
+
function validateInstagramStoryArgs(kwargs) {
|
|
16
|
+
if (kwargs.media === undefined) {
|
|
17
|
+
throw new ArgumentError('Argument "media" is required.', 'Provide --media /path/to/file.jpg or --media /path/to/file.mp4');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function normalizeStoryMediaItem(kwargs) {
|
|
21
|
+
const raw = String(kwargs.media ?? '').trim();
|
|
22
|
+
const parts = raw.split(',').map((part) => part.trim()).filter(Boolean);
|
|
23
|
+
if (parts.length === 0) {
|
|
24
|
+
throw new ArgumentError('Argument "media" is required.', 'Provide --media /path/to/file.jpg or --media /path/to/file.mp4');
|
|
25
|
+
}
|
|
26
|
+
if (parts.length > 1) {
|
|
27
|
+
throw new ArgumentError('Instagram story currently supports a single media item.', 'Provide one image or one video path with --media');
|
|
28
|
+
}
|
|
29
|
+
const resolved = path.resolve(parts[0]);
|
|
30
|
+
if (!fs.existsSync(resolved)) {
|
|
31
|
+
throw new ArgumentError(`Story media file not found: ${resolved}`);
|
|
32
|
+
}
|
|
33
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
34
|
+
if (SUPPORTED_STORY_IMAGE_EXTENSIONS.has(ext)) {
|
|
35
|
+
return { type: 'image', filePath: resolved };
|
|
36
|
+
}
|
|
37
|
+
if (SUPPORTED_STORY_VIDEO_EXTENSIONS.has(ext)) {
|
|
38
|
+
return { type: 'video', filePath: resolved };
|
|
39
|
+
}
|
|
40
|
+
throw new ArgumentError(`Unsupported story media format: ${ext}`, 'Supported formats: images (.jpg, .jpeg, .png, .webp) and videos (.mp4)');
|
|
41
|
+
}
|
|
42
|
+
async function resolveCurrentUserId(page) {
|
|
43
|
+
const cookies = await page.getCookies({ domain: 'instagram.com' });
|
|
44
|
+
return cookies.find((cookie) => cookie.name === 'ds_user_id')?.value || '';
|
|
45
|
+
}
|
|
46
|
+
async function resolveCurrentUsername(page, currentUserId = '') {
|
|
47
|
+
if (!currentUserId)
|
|
48
|
+
return '';
|
|
49
|
+
const runtimeInfo = await resolveInstagramRuntimeInfo(page);
|
|
50
|
+
const apiResult = await page.evaluate(`
|
|
51
|
+
(async () => {
|
|
52
|
+
const userId = ${JSON.stringify(currentUserId)};
|
|
53
|
+
const appId = ${JSON.stringify(runtimeInfo.appId || '')};
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(
|
|
56
|
+
'https://www.instagram.com/api/v1/users/' + encodeURIComponent(userId) + '/info/',
|
|
57
|
+
{
|
|
58
|
+
credentials: 'include',
|
|
59
|
+
headers: appId ? { 'X-IG-App-ID': appId } : {},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
if (!res.ok) return { ok: false };
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
const username = data?.user?.username || '';
|
|
65
|
+
return { ok: !!username, username };
|
|
66
|
+
} catch {
|
|
67
|
+
return { ok: false };
|
|
68
|
+
}
|
|
69
|
+
})()
|
|
70
|
+
`);
|
|
71
|
+
return apiResult?.ok && apiResult.username ? apiResult.username : '';
|
|
72
|
+
}
|
|
73
|
+
function buildStorySuccessResult(mediaItem, url) {
|
|
74
|
+
return [{
|
|
75
|
+
status: '✅ Posted',
|
|
76
|
+
detail: mediaItem.type === 'video'
|
|
77
|
+
? 'Single video story shared successfully'
|
|
78
|
+
: 'Single story shared successfully',
|
|
79
|
+
url,
|
|
80
|
+
}];
|
|
81
|
+
}
|
|
82
|
+
cli({
|
|
83
|
+
site: 'instagram',
|
|
84
|
+
name: 'story',
|
|
85
|
+
description: 'Post a single Instagram story image or video',
|
|
86
|
+
domain: 'www.instagram.com',
|
|
87
|
+
strategy: Strategy.UI,
|
|
88
|
+
browser: true,
|
|
89
|
+
timeoutSeconds: 300,
|
|
90
|
+
args: [
|
|
91
|
+
{ name: 'media', required: false, valueRequired: true, help: 'Path to a single story image or video file' },
|
|
92
|
+
],
|
|
93
|
+
columns: ['status', 'detail', 'url'],
|
|
94
|
+
validateArgs: validateInstagramStoryArgs,
|
|
95
|
+
func: async (page, kwargs) => {
|
|
96
|
+
const browserPage = requirePage(page);
|
|
97
|
+
const mediaItem = normalizeStoryMediaItem(kwargs);
|
|
98
|
+
const currentUserId = await resolveCurrentUserId(browserPage);
|
|
99
|
+
const privateConfig = await resolveInstagramPrivatePublishConfig(browserPage);
|
|
100
|
+
const storyResult = await publishStoryViaPrivateApi({
|
|
101
|
+
page: browserPage,
|
|
102
|
+
mediaItem,
|
|
103
|
+
content: '',
|
|
104
|
+
apiContext: privateConfig.apiContext,
|
|
105
|
+
jazoest: privateConfig.jazoest,
|
|
106
|
+
currentUserId,
|
|
107
|
+
});
|
|
108
|
+
const username = await resolveCurrentUsername(browserPage, currentUserId);
|
|
109
|
+
const mediaPk = storyResult.mediaPk || storyResult.uploadId;
|
|
110
|
+
const url = username && mediaPk
|
|
111
|
+
? new URL(`/stories/${username}/${mediaPk}/`, INSTAGRAM_HOME_URL).toString()
|
|
112
|
+
: '';
|
|
113
|
+
return buildStorySuccessResult(mediaItem, url);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './story.js';
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { ArgumentError } from '../../errors.js';
|
|
6
|
+
import { getRegistry } from '../../registry.js';
|
|
7
|
+
import * as privatePublish from './_shared/private-publish.js';
|
|
8
|
+
import './story.js';
|
|
9
|
+
const tempDirs = [];
|
|
10
|
+
function createTempFile(name, bytes = Buffer.from('story-media')) {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-story-'));
|
|
12
|
+
tempDirs.push(dir);
|
|
13
|
+
const filePath = path.join(dir, name);
|
|
14
|
+
fs.writeFileSync(filePath, bytes);
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
function createPageMock(evaluateResults = [], overrides = {}) {
|
|
18
|
+
const evaluate = vi.fn();
|
|
19
|
+
for (const result of evaluateResults) {
|
|
20
|
+
evaluate.mockResolvedValueOnce(result);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
evaluate,
|
|
25
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
26
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
32
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
34
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
38
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
39
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
41
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
43
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
45
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
insertText: vi.fn().mockResolvedValue(undefined),
|
|
47
|
+
getCurrentUrl: vi.fn().mockResolvedValue(null),
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
afterAll(() => {
|
|
52
|
+
for (const dir of tempDirs) {
|
|
53
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
describe('instagram story registration', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
it('registers the story command with a required-value media arg', () => {
|
|
64
|
+
const cmd = getRegistry().get('instagram/story');
|
|
65
|
+
expect(cmd).toBeDefined();
|
|
66
|
+
expect(cmd?.browser).toBe(true);
|
|
67
|
+
expect(cmd?.args.some((arg) => arg.name === 'media' && !arg.required && arg.valueRequired)).toBe(true);
|
|
68
|
+
expect(cmd?.args.some((arg) => arg.name === 'content')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it('rejects missing --media before browser work', async () => {
|
|
71
|
+
const page = createPageMock();
|
|
72
|
+
const cmd = getRegistry().get('instagram/story');
|
|
73
|
+
await expect(cmd.func(page, {})).rejects.toThrow(ArgumentError);
|
|
74
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it('rejects multiple media inputs for a single story', async () => {
|
|
77
|
+
const first = createTempFile('one.jpg');
|
|
78
|
+
const second = createTempFile('two.mp4');
|
|
79
|
+
const page = createPageMock();
|
|
80
|
+
const cmd = getRegistry().get('instagram/story');
|
|
81
|
+
await expect(cmd.func(page, { media: `${first},${second}` })).rejects.toThrow('single media');
|
|
82
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
it('rejects unsupported story formats', async () => {
|
|
85
|
+
const filePath = createTempFile('story.mov');
|
|
86
|
+
const page = createPageMock();
|
|
87
|
+
const cmd = getRegistry().get('instagram/story');
|
|
88
|
+
await expect(cmd.func(page, { media: filePath })).rejects.toThrow('Unsupported story media format');
|
|
89
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
it('publishes a single image story through the private route', async () => {
|
|
92
|
+
const imagePath = createTempFile('story.jpg');
|
|
93
|
+
const page = createPageMock([
|
|
94
|
+
{ appId: '936619743392459', csrfToken: '', instagramAjax: 'ajax' },
|
|
95
|
+
{ ok: true, username: 'tsezi_ray' },
|
|
96
|
+
], {
|
|
97
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '123', domain: 'instagram.com' }]),
|
|
98
|
+
});
|
|
99
|
+
const cmd = getRegistry().get('instagram/story');
|
|
100
|
+
vi.spyOn(privatePublish, 'resolveInstagramPrivatePublishConfig').mockResolvedValue({
|
|
101
|
+
apiContext: {
|
|
102
|
+
asbdId: '359341',
|
|
103
|
+
csrfToken: 'csrf-token',
|
|
104
|
+
igAppId: '936619743392459',
|
|
105
|
+
igWwwClaim: 'claim',
|
|
106
|
+
instagramAjax: 'ajax',
|
|
107
|
+
webSessionId: 'session',
|
|
108
|
+
},
|
|
109
|
+
jazoest: '22047',
|
|
110
|
+
});
|
|
111
|
+
vi.spyOn(privatePublish, 'publishStoryViaPrivateApi').mockResolvedValue({
|
|
112
|
+
mediaPk: '1234567890',
|
|
113
|
+
uploadId: '1234567890',
|
|
114
|
+
});
|
|
115
|
+
const result = await cmd.func(page, { media: imagePath });
|
|
116
|
+
expect(privatePublish.publishStoryViaPrivateApi).toHaveBeenCalledWith(expect.objectContaining({
|
|
117
|
+
page,
|
|
118
|
+
mediaItem: { type: 'image', filePath: imagePath },
|
|
119
|
+
content: '',
|
|
120
|
+
}));
|
|
121
|
+
expect(result).toEqual([
|
|
122
|
+
{
|
|
123
|
+
status: '✅ Posted',
|
|
124
|
+
detail: 'Single story shared successfully',
|
|
125
|
+
url: 'https://www.instagram.com/stories/tsezi_ray/1234567890/',
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
it('publishes a single video story through the private route', async () => {
|
|
130
|
+
const videoPath = createTempFile('story.mp4');
|
|
131
|
+
const page = createPageMock([
|
|
132
|
+
{ appId: '936619743392459', csrfToken: '', instagramAjax: 'ajax' },
|
|
133
|
+
{ ok: true, username: 'tsezi_ray' },
|
|
134
|
+
], {
|
|
135
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '123', domain: 'instagram.com' }]),
|
|
136
|
+
});
|
|
137
|
+
const cmd = getRegistry().get('instagram/story');
|
|
138
|
+
vi.spyOn(privatePublish, 'resolveInstagramPrivatePublishConfig').mockResolvedValue({
|
|
139
|
+
apiContext: {
|
|
140
|
+
asbdId: '359341',
|
|
141
|
+
csrfToken: 'csrf-token',
|
|
142
|
+
igAppId: '936619743392459',
|
|
143
|
+
igWwwClaim: 'claim',
|
|
144
|
+
instagramAjax: 'ajax',
|
|
145
|
+
webSessionId: 'session',
|
|
146
|
+
},
|
|
147
|
+
jazoest: '22047',
|
|
148
|
+
});
|
|
149
|
+
vi.spyOn(privatePublish, 'publishStoryViaPrivateApi').mockResolvedValue({
|
|
150
|
+
mediaPk: '9988776655',
|
|
151
|
+
uploadId: '9988776655',
|
|
152
|
+
});
|
|
153
|
+
const result = await cmd.func(page, { media: videoPath });
|
|
154
|
+
expect(privatePublish.publishStoryViaPrivateApi).toHaveBeenCalledWith(expect.objectContaining({
|
|
155
|
+
page,
|
|
156
|
+
mediaItem: { type: 'video', filePath: videoPath },
|
|
157
|
+
content: '',
|
|
158
|
+
}));
|
|
159
|
+
expect(result).toEqual([
|
|
160
|
+
{
|
|
161
|
+
status: '✅ Posted',
|
|
162
|
+
detail: 'Single video story shared successfully',
|
|
163
|
+
url: 'https://www.instagram.com/stories/tsezi_ray/9988776655/',
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sinafinance stock rank
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: 'sinafinance',
|
|
7
|
+
name: 'stock-rank',
|
|
8
|
+
description: '新浪财经热搜榜',
|
|
9
|
+
domain: 'finance.sina.cn',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
navigateBefore: false,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'market', type: 'string', default: 'cn', choices: ['cn', 'hk', 'us', 'wh', 'ft'], help: 'Market: cn (A股), hk (港股), us (美股), wh (外汇), ft (期货)' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['rank', 'name', 'symbol', 'market', 'price', 'change', 'url'],
|
|
16
|
+
func: async (page, _args) => {
|
|
17
|
+
const market = _args.market || 'cn';
|
|
18
|
+
await page.goto('https://finance.sina.cn/');
|
|
19
|
+
await page.wait({ selector: '#actionSearch', timeout: 10000 });
|
|
20
|
+
const payload = await page.evaluate(`
|
|
21
|
+
(async () => {
|
|
22
|
+
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
|
23
|
+
const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
24
|
+
const marketType = ${JSON.stringify(market)};
|
|
25
|
+
|
|
26
|
+
const searchBtn = document.querySelector('#actionSearch');
|
|
27
|
+
if (searchBtn) {
|
|
28
|
+
searchBtn.dispatchEvent(new Event('tap', { bubbles: true }));
|
|
29
|
+
await wait(3000);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tabEl = document.querySelector('[data-type="' + marketType + '"]');
|
|
33
|
+
const marketName = tabEl?.textContent || marketType;
|
|
34
|
+
if (marketType !== 'cn' && tabEl) {
|
|
35
|
+
tabEl.click();
|
|
36
|
+
await wait(2000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const results = [];
|
|
40
|
+
document.querySelectorAll('#stock-list .j-stock-row').forEach(el => {
|
|
41
|
+
const rankEl = el.querySelector('.rank');
|
|
42
|
+
const nameEl = el.querySelector('.j-sname');
|
|
43
|
+
const codeEl = el.querySelector('.stock-code');
|
|
44
|
+
const priceEl = el.querySelector('.j-price');
|
|
45
|
+
const changeEl = el.querySelector('.j-change');
|
|
46
|
+
const openUrl = el.getAttribute('open-url') || '';
|
|
47
|
+
const fullUrl = openUrl ? 'https:' + openUrl : '';
|
|
48
|
+
results.push({
|
|
49
|
+
rank: cleanText(rankEl?.textContent || ''),
|
|
50
|
+
name: cleanText(nameEl?.textContent || ''),
|
|
51
|
+
symbol: cleanText(codeEl?.textContent || ''),
|
|
52
|
+
market: cleanText(marketName),
|
|
53
|
+
price: cleanText(priceEl?.textContent || ''),
|
|
54
|
+
change: cleanText(changeEl?.textContent || ''),
|
|
55
|
+
url: fullUrl,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return results;
|
|
59
|
+
})()
|
|
60
|
+
`);
|
|
61
|
+
if (!Array.isArray(payload))
|
|
62
|
+
return [];
|
|
63
|
+
return payload;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -12,8 +12,6 @@ function createPageMock(evaluateResult) {
|
|
|
12
12
|
getFormState: vi.fn().mockResolvedValue({}),
|
|
13
13
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
14
14
|
tabs: vi.fn().mockResolvedValue([]),
|
|
15
|
-
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
16
|
-
newTab: vi.fn().mockResolvedValue(undefined),
|
|
17
15
|
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
18
16
|
networkRequests: vi.fn().mockResolvedValue([]),
|
|
19
17
|
consoleMessages: vi.fn().mockResolvedValue([]),
|