@jackwener/opencli 1.6.1 → 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/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/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 +1135 -184
- 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/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/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
package/src/browser/page.ts
CHANGED
|
@@ -58,25 +58,17 @@ export class Page extends BasePage {
|
|
|
58
58
|
this._tabId = result.tabId;
|
|
59
59
|
}
|
|
60
60
|
this._lastUrl = url;
|
|
61
|
-
// Inject stealth
|
|
62
|
-
|
|
63
|
-
await sendCommand('exec', {
|
|
64
|
-
code: generateStealthJs(),
|
|
65
|
-
...this._cmdOpts(),
|
|
66
|
-
});
|
|
67
|
-
} catch {
|
|
68
|
-
// Non-fatal: stealth is best-effort
|
|
69
|
-
}
|
|
70
|
-
// Smart settle: use DOM stability detection instead of fixed sleep.
|
|
71
|
-
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
|
|
61
|
+
// Inject stealth + settle in a single round-trip instead of two sequential exec calls.
|
|
62
|
+
// The stealth guard flag prevents double-injection; settle uses DOM stability detection.
|
|
72
63
|
if (options?.waitUntil !== 'none') {
|
|
73
64
|
const maxMs = options?.settleMs ?? 1000;
|
|
74
|
-
const
|
|
75
|
-
|
|
65
|
+
const combinedCode = `${generateStealthJs()};\n${waitForDomStableJs(maxMs, Math.min(500, maxMs))}`;
|
|
66
|
+
const combinedOpts = {
|
|
67
|
+
code: combinedCode,
|
|
76
68
|
...this._cmdOpts(),
|
|
77
69
|
};
|
|
78
70
|
try {
|
|
79
|
-
await sendCommand('exec',
|
|
71
|
+
await sendCommand('exec', combinedOpts);
|
|
80
72
|
} catch (err) {
|
|
81
73
|
if (!isRetryableSettleError(err)) throw err;
|
|
82
74
|
// SPA client-side redirects can invalidate the CDP target after
|
|
@@ -84,14 +76,21 @@ export class Page extends BasePage {
|
|
|
84
76
|
// to load, then retry the settle probe once.
|
|
85
77
|
try {
|
|
86
78
|
await new Promise((r) => setTimeout(r, 200));
|
|
87
|
-
await sendCommand('exec',
|
|
79
|
+
await sendCommand('exec', combinedOpts);
|
|
88
80
|
} catch (retryErr) {
|
|
89
81
|
if (!isRetryableSettleError(retryErr)) throw retryErr;
|
|
90
|
-
// Retry also failed — give up silently. Settle is best-effort
|
|
91
|
-
// after successful navigation; the next real command will surface
|
|
92
|
-
// any persistent target error immediately.
|
|
93
82
|
}
|
|
94
83
|
}
|
|
84
|
+
} else {
|
|
85
|
+
// Even with waitUntil='none', still inject stealth (best-effort)
|
|
86
|
+
try {
|
|
87
|
+
await sendCommand('exec', {
|
|
88
|
+
code: generateStealthJs(),
|
|
89
|
+
...this._cmdOpts(),
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
// Non-fatal: stealth is best-effort
|
|
93
|
+
}
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
|
|
@@ -121,6 +120,9 @@ export class Page extends BasePage {
|
|
|
121
120
|
await sendCommand('close-window', { ...this._wsOpt() });
|
|
122
121
|
} catch {
|
|
123
122
|
// Window may already be closed or daemon may be down
|
|
123
|
+
} finally {
|
|
124
|
+
this._tabId = undefined;
|
|
125
|
+
this._lastUrl = null;
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -129,18 +131,6 @@ export class Page extends BasePage {
|
|
|
129
131
|
return Array.isArray(result) ? result : [];
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
async closeTab(index?: number): Promise<void> {
|
|
133
|
-
await sendCommand('tabs', { op: 'close', ...this._wsOpt(), ...(index !== undefined ? { index } : {}) });
|
|
134
|
-
// Invalidate cached tabId — the closed tab might have been our active one.
|
|
135
|
-
// We can't know for sure (close-by-index doesn't return tabId), so reset.
|
|
136
|
-
this._tabId = undefined;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async newTab(): Promise<void> {
|
|
140
|
-
const result = await sendCommand('tabs', { op: 'new', ...this._wsOpt() }) as { tabId?: number };
|
|
141
|
-
if (result?.tabId) this._tabId = result.tabId;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
async selectTab(index: number): Promise<void> {
|
|
145
135
|
const result = await sendCommand('tabs', { op: 'select', index, ...this._wsOpt() }) as { selected?: number };
|
|
146
136
|
if (result?.selected) this._tabId = result.selected;
|
|
@@ -164,6 +154,19 @@ export class Page extends BasePage {
|
|
|
164
154
|
return base64;
|
|
165
155
|
}
|
|
166
156
|
|
|
157
|
+
async startNetworkCapture(pattern: string = ''): Promise<void> {
|
|
158
|
+
await sendCommand('network-capture-start', {
|
|
159
|
+
pattern,
|
|
160
|
+
...this._cmdOpts(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async readNetworkCapture(): Promise<unknown[]> {
|
|
165
|
+
const result = await sendCommand('network-capture-read', {
|
|
166
|
+
...this._cmdOpts(),
|
|
167
|
+
});
|
|
168
|
+
return Array.isArray(result) ? result : [];
|
|
169
|
+
}
|
|
167
170
|
/**
|
|
168
171
|
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
169
172
|
* Chrome reads the files directly from the local filesystem, avoiding the
|
|
@@ -180,6 +183,16 @@ export class Page extends BasePage {
|
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
async insertText(text: string): Promise<void> {
|
|
187
|
+
const result = await sendCommand('insert-text', {
|
|
188
|
+
text,
|
|
189
|
+
...this._cmdOpts(),
|
|
190
|
+
}) as { inserted?: boolean };
|
|
191
|
+
if (!result?.inserted) {
|
|
192
|
+
throw new Error('insertText returned no inserted flag — command may not be supported by the extension');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
async cdp(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
184
197
|
return sendCommand('cdp', {
|
|
185
198
|
cdpMethod: method,
|
|
@@ -188,6 +201,78 @@ export class Page extends BasePage {
|
|
|
188
201
|
});
|
|
189
202
|
}
|
|
190
203
|
|
|
204
|
+
/** CDP native click fallback — called when JS el.click() fails */
|
|
205
|
+
protected override async tryNativeClick(x: number, y: number): Promise<boolean> {
|
|
206
|
+
try {
|
|
207
|
+
await this.nativeClick(x, y);
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Precise click using DOM.getContentQuads/getBoxModel for inline elements */
|
|
215
|
+
async clickWithQuads(ref: string): Promise<void> {
|
|
216
|
+
const safeRef = JSON.stringify(ref);
|
|
217
|
+
const cssSelector = `[data-opencli-ref="${ref.replace(/"/g, '\\"')}"]`;
|
|
218
|
+
|
|
219
|
+
// Scroll element into view first
|
|
220
|
+
await this.evaluate(`
|
|
221
|
+
(() => {
|
|
222
|
+
const el = document.querySelector('[data-opencli-ref="' + ${safeRef} + '"]');
|
|
223
|
+
if (el) el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
224
|
+
return !!el;
|
|
225
|
+
})()
|
|
226
|
+
`);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
// Find DOM node via CDP
|
|
230
|
+
const doc = await this.cdp('DOM.getDocument', {}) as { root: { nodeId: number } };
|
|
231
|
+
const result = await this.cdp('DOM.querySelectorAll', {
|
|
232
|
+
nodeId: doc.root.nodeId,
|
|
233
|
+
selector: cssSelector,
|
|
234
|
+
}) as { nodeIds: number[] };
|
|
235
|
+
|
|
236
|
+
if (!result.nodeIds?.length) throw new Error('DOM node not found');
|
|
237
|
+
|
|
238
|
+
const nodeId = result.nodeIds[0];
|
|
239
|
+
|
|
240
|
+
// Try getContentQuads first (precise for inline elements)
|
|
241
|
+
try {
|
|
242
|
+
const quads = await this.cdp('DOM.getContentQuads', { nodeId }) as { quads: number[][] };
|
|
243
|
+
if (quads.quads?.length) {
|
|
244
|
+
const q = quads.quads[0];
|
|
245
|
+
const cx = (q[0] + q[2] + q[4] + q[6]) / 4;
|
|
246
|
+
const cy = (q[1] + q[3] + q[5] + q[7]) / 4;
|
|
247
|
+
await this.nativeClick(Math.round(cx), Math.round(cy));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
} catch { /* fallthrough */ }
|
|
251
|
+
|
|
252
|
+
// Try getBoxModel
|
|
253
|
+
try {
|
|
254
|
+
const box = await this.cdp('DOM.getBoxModel', { nodeId }) as { model: { content: number[] } };
|
|
255
|
+
if (box.model?.content) {
|
|
256
|
+
const c = box.model.content;
|
|
257
|
+
const cx = (c[0] + c[2] + c[4] + c[6]) / 4;
|
|
258
|
+
const cy = (c[1] + c[3] + c[5] + c[7]) / 4;
|
|
259
|
+
await this.nativeClick(Math.round(cx), Math.round(cy));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
} catch { /* fallthrough */ }
|
|
263
|
+
} catch { /* fallthrough */ }
|
|
264
|
+
|
|
265
|
+
// Final fallback: regular click
|
|
266
|
+
await this.evaluate(`
|
|
267
|
+
(() => {
|
|
268
|
+
const el = document.querySelector('[data-opencli-ref="' + ${safeRef} + '"]');
|
|
269
|
+
if (!el) throw new Error('Element not found: ' + ${safeRef});
|
|
270
|
+
el.click();
|
|
271
|
+
return 'clicked';
|
|
272
|
+
})()
|
|
273
|
+
`);
|
|
274
|
+
}
|
|
275
|
+
|
|
191
276
|
async nativeClick(x: number, y: number): Promise<void> {
|
|
192
277
|
await this.cdp('Input.dispatchMouseEvent', {
|
|
193
278
|
type: 'mousePressed',
|
|
@@ -228,4 +313,3 @@ export class Page extends BasePage {
|
|
|
228
313
|
});
|
|
229
314
|
}
|
|
230
315
|
}
|
|
231
|
-
|
package/src/browser.test.ts
CHANGED
|
@@ -136,7 +136,7 @@ describe('BrowserBridge state', () => {
|
|
|
136
136
|
|
|
137
137
|
it('fails fast when daemon is running but extension is disconnected', async () => {
|
|
138
138
|
vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
|
|
139
|
-
vi.spyOn(daemonClient, '
|
|
139
|
+
vi.spyOn(daemonClient, 'fetchDaemonStatus').mockResolvedValue({ extensionConnected: false } as any);
|
|
140
140
|
|
|
141
141
|
const bridge = new BrowserBridge();
|
|
142
142
|
|
package/src/build-manifest.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface ManifestEntry {
|
|
|
33
33
|
type?: string;
|
|
34
34
|
default?: unknown;
|
|
35
35
|
required?: boolean;
|
|
36
|
+
valueRequired?: boolean;
|
|
36
37
|
positional?: boolean;
|
|
37
38
|
help?: string;
|
|
38
39
|
choices?: string[];
|
|
@@ -62,6 +63,7 @@ function toManifestArgs(args: CliCommand['args']): ManifestEntry['args'] {
|
|
|
62
63
|
type: arg.type ?? 'str',
|
|
63
64
|
default: arg.default,
|
|
64
65
|
required: !!arg.required,
|
|
66
|
+
valueRequired: !!arg.valueRequired || undefined,
|
|
65
67
|
positional: arg.positional || undefined,
|
|
66
68
|
help: arg.help ?? '',
|
|
67
69
|
choices: arg.choices,
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { IPage } from './types.js';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
mockExploreUrl,
|
|
6
|
+
mockRenderExploreSummary,
|
|
7
|
+
mockGenerateCliFromUrl,
|
|
8
|
+
mockRenderGenerateSummary,
|
|
9
|
+
mockRecordSession,
|
|
10
|
+
mockRenderRecordSummary,
|
|
11
|
+
mockCascadeProbe,
|
|
12
|
+
mockRenderCascadeResult,
|
|
13
|
+
mockGetBrowserFactory,
|
|
14
|
+
mockBrowserSession,
|
|
15
|
+
} = vi.hoisted(() => ({
|
|
16
|
+
mockExploreUrl: vi.fn(),
|
|
17
|
+
mockRenderExploreSummary: vi.fn(),
|
|
18
|
+
mockGenerateCliFromUrl: vi.fn(),
|
|
19
|
+
mockRenderGenerateSummary: vi.fn(),
|
|
20
|
+
mockRecordSession: vi.fn(),
|
|
21
|
+
mockRenderRecordSummary: vi.fn(),
|
|
22
|
+
mockCascadeProbe: vi.fn(),
|
|
23
|
+
mockRenderCascadeResult: vi.fn(),
|
|
24
|
+
mockGetBrowserFactory: vi.fn(() => ({ name: 'BrowserFactory' })),
|
|
25
|
+
mockBrowserSession: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('./explore.js', () => ({
|
|
29
|
+
exploreUrl: mockExploreUrl,
|
|
30
|
+
renderExploreSummary: mockRenderExploreSummary,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('./generate.js', () => ({
|
|
34
|
+
generateCliFromUrl: mockGenerateCliFromUrl,
|
|
35
|
+
renderGenerateSummary: mockRenderGenerateSummary,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock('./record.js', () => ({
|
|
39
|
+
recordSession: mockRecordSession,
|
|
40
|
+
renderRecordSummary: mockRenderRecordSummary,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('./cascade.js', () => ({
|
|
44
|
+
cascadeProbe: mockCascadeProbe,
|
|
45
|
+
renderCascadeResult: mockRenderCascadeResult,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock('./runtime.js', () => ({
|
|
49
|
+
getBrowserFactory: mockGetBrowserFactory,
|
|
50
|
+
browserSession: mockBrowserSession,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
import { createProgram } from './cli.js';
|
|
54
|
+
|
|
55
|
+
describe('built-in browser commands verbose wiring', () => {
|
|
56
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
60
|
+
process.exitCode = undefined;
|
|
61
|
+
|
|
62
|
+
mockExploreUrl.mockReset().mockResolvedValue({ ok: true });
|
|
63
|
+
mockRenderExploreSummary.mockReset().mockReturnValue('explore-summary');
|
|
64
|
+
mockGenerateCliFromUrl.mockReset().mockResolvedValue({ ok: true });
|
|
65
|
+
mockRenderGenerateSummary.mockReset().mockReturnValue('generate-summary');
|
|
66
|
+
mockRecordSession.mockReset().mockResolvedValue({ candidateCount: 1 });
|
|
67
|
+
mockRenderRecordSummary.mockReset().mockReturnValue('record-summary');
|
|
68
|
+
mockCascadeProbe.mockReset().mockResolvedValue({ ok: true });
|
|
69
|
+
mockRenderCascadeResult.mockReset().mockReturnValue('cascade-summary');
|
|
70
|
+
mockGetBrowserFactory.mockClear();
|
|
71
|
+
mockBrowserSession.mockReset().mockImplementation(async (_factory, fn) => {
|
|
72
|
+
const page = {
|
|
73
|
+
goto: vi.fn(),
|
|
74
|
+
wait: vi.fn(),
|
|
75
|
+
} as unknown as IPage;
|
|
76
|
+
return fn(page);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('enables OPENCLI_VERBOSE for explore via the real CLI command', async () => {
|
|
81
|
+
const program = createProgram('', '');
|
|
82
|
+
|
|
83
|
+
await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com', '-v']);
|
|
84
|
+
|
|
85
|
+
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
86
|
+
expect(mockExploreUrl).toHaveBeenCalledWith(
|
|
87
|
+
'https://example.com',
|
|
88
|
+
expect.objectContaining({ workspace: 'explore:example.com' }),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('enables OPENCLI_VERBOSE for generate via the real CLI command', async () => {
|
|
93
|
+
const program = createProgram('', '');
|
|
94
|
+
|
|
95
|
+
await program.parseAsync(['node', 'opencli', 'generate', 'https://example.com', '-v']);
|
|
96
|
+
|
|
97
|
+
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
98
|
+
expect(mockGenerateCliFromUrl).toHaveBeenCalledWith(
|
|
99
|
+
expect.objectContaining({ url: 'https://example.com', workspace: 'generate:example.com' }),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('enables OPENCLI_VERBOSE for record via the real CLI command', async () => {
|
|
104
|
+
const program = createProgram('', '');
|
|
105
|
+
|
|
106
|
+
await program.parseAsync(['node', 'opencli', 'record', 'https://example.com', '-v']);
|
|
107
|
+
|
|
108
|
+
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
109
|
+
expect(mockRecordSession).toHaveBeenCalledWith(
|
|
110
|
+
expect.objectContaining({ url: 'https://example.com' }),
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('enables OPENCLI_VERBOSE for cascade via the real CLI command', async () => {
|
|
115
|
+
const program = createProgram('', '');
|
|
116
|
+
|
|
117
|
+
await program.parseAsync(['node', 'opencli', 'cascade', 'https://example.com', '-v']);
|
|
118
|
+
|
|
119
|
+
expect(process.env.OPENCLI_VERBOSE).toBe('1');
|
|
120
|
+
expect(mockBrowserSession).toHaveBeenCalled();
|
|
121
|
+
expect(mockCascadeProbe).toHaveBeenCalledWith(expect.any(Object), 'https://example.com');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('leaves OPENCLI_VERBOSE unset when verbose is omitted', async () => {
|
|
125
|
+
const program = createProgram('', '');
|
|
126
|
+
|
|
127
|
+
await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com']);
|
|
128
|
+
|
|
129
|
+
expect(process.env.OPENCLI_VERBOSE).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
consoleLogSpy.mockClear();
|
|
133
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -25,7 +25,11 @@ async function getOperatePage(): Promise<import('./types.js').IPage> {
|
|
|
25
25
|
return bridge.connect({ timeout: 30, workspace: 'operate:default' });
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function applyVerbose(opts: { verbose?: boolean }): void {
|
|
29
|
+
if (opts.verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command {
|
|
29
33
|
const program = new Command();
|
|
30
34
|
// enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
|
|
31
35
|
// prerequisite for passThroughOptions to forward --help/--version to external binaries
|
|
@@ -145,7 +149,16 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
145
149
|
.option('--wait <s>', '', '3')
|
|
146
150
|
.option('--auto', 'Enable interactive fuzzing')
|
|
147
151
|
.option('--click <labels>', 'Comma-separated labels to click before fuzzing')
|
|
148
|
-
.
|
|
152
|
+
.option('-v, --verbose', 'Debug output')
|
|
153
|
+
.action(async (url: string, opts: {
|
|
154
|
+
site?: string;
|
|
155
|
+
goal?: string;
|
|
156
|
+
wait: string;
|
|
157
|
+
auto?: boolean;
|
|
158
|
+
click?: string;
|
|
159
|
+
verbose?: boolean;
|
|
160
|
+
}) => {
|
|
161
|
+
applyVerbose(opts);
|
|
149
162
|
const { exploreUrl, renderExploreSummary } = await import('./explore.js');
|
|
150
163
|
const clickLabels = opts.click
|
|
151
164
|
? opts.click.split(',').map((s: string) => s.trim())
|
|
@@ -168,7 +181,9 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
168
181
|
.description('Synthesize CLIs from explore')
|
|
169
182
|
.argument('<target>')
|
|
170
183
|
.option('--top <n>', '', '3')
|
|
184
|
+
.option('-v, --verbose', 'Debug output')
|
|
171
185
|
.action(async (target, opts) => {
|
|
186
|
+
applyVerbose(opts);
|
|
172
187
|
const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js');
|
|
173
188
|
console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) })));
|
|
174
189
|
});
|
|
@@ -179,7 +194,13 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
179
194
|
.argument('<url>')
|
|
180
195
|
.option('--goal <text>')
|
|
181
196
|
.option('--site <name>')
|
|
182
|
-
.
|
|
197
|
+
.option('-v, --verbose', 'Debug output')
|
|
198
|
+
.action(async (url: string, opts: {
|
|
199
|
+
goal?: string;
|
|
200
|
+
site?: string;
|
|
201
|
+
verbose?: boolean;
|
|
202
|
+
}) => {
|
|
203
|
+
applyVerbose(opts);
|
|
183
204
|
const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
|
|
184
205
|
const workspace = `generate:${inferHost(url, opts.site)}`;
|
|
185
206
|
const r = await generateCliFromUrl({
|
|
@@ -203,7 +224,15 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
203
224
|
.option('--out <dir>', 'Output directory for candidates')
|
|
204
225
|
.option('--poll <ms>', 'Poll interval in milliseconds', '2000')
|
|
205
226
|
.option('--timeout <ms>', 'Auto-stop after N milliseconds (default: 60000)', '60000')
|
|
206
|
-
.
|
|
227
|
+
.option('-v, --verbose', 'Debug output')
|
|
228
|
+
.action(async (url: string, opts: {
|
|
229
|
+
site?: string;
|
|
230
|
+
out?: string;
|
|
231
|
+
poll: string;
|
|
232
|
+
timeout: string;
|
|
233
|
+
verbose?: boolean;
|
|
234
|
+
}) => {
|
|
235
|
+
applyVerbose(opts);
|
|
207
236
|
const { recordSession, renderRecordSummary } = await import('./record.js');
|
|
208
237
|
const result = await recordSession({
|
|
209
238
|
BrowserFactory: getBrowserFactory(),
|
|
@@ -222,7 +251,12 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
222
251
|
.description('Strategy cascade: find simplest working strategy')
|
|
223
252
|
.argument('<url>')
|
|
224
253
|
.option('--site <name>')
|
|
225
|
-
.
|
|
254
|
+
.option('-v, --verbose', 'Debug output')
|
|
255
|
+
.action(async (url: string, opts: {
|
|
256
|
+
site?: string;
|
|
257
|
+
verbose?: boolean;
|
|
258
|
+
}) => {
|
|
259
|
+
applyVerbose(opts);
|
|
226
260
|
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
|
|
227
261
|
const workspace = `cascade:${inferHost(url, opts.site)}`;
|
|
228
262
|
const result = await browserSession(getBrowserFactory(), async (page) => {
|
|
@@ -302,7 +336,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
302
336
|
|
|
303
337
|
operate.command('state').description('Page state: URL, title, interactive elements with [N] indices')
|
|
304
338
|
.action(operateAction(async (page) => {
|
|
305
|
-
const snapshot = await page.snapshot({ viewportExpand:
|
|
339
|
+
const snapshot = await page.snapshot({ viewportExpand: 2000 });
|
|
306
340
|
const url = await page.getCurrentUrl?.() ?? '';
|
|
307
341
|
console.log(`URL: ${url}\n`);
|
|
308
342
|
console.log(typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2));
|
|
@@ -372,7 +406,23 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
372
406
|
await page.click(index);
|
|
373
407
|
await page.wait(0.3);
|
|
374
408
|
await page.typeText(index, text);
|
|
375
|
-
|
|
409
|
+
// Detect autocomplete/combobox fields and wait for dropdown suggestions
|
|
410
|
+
const isAutocomplete = await page.evaluate(`
|
|
411
|
+
(() => {
|
|
412
|
+
const el = document.querySelector('[data-opencli-ref="${index}"]');
|
|
413
|
+
if (!el) return false;
|
|
414
|
+
const role = el.getAttribute('role');
|
|
415
|
+
const ac = el.getAttribute('aria-autocomplete');
|
|
416
|
+
const list = el.getAttribute('list');
|
|
417
|
+
return role === 'combobox' || ac === 'list' || ac === 'both' || !!list;
|
|
418
|
+
})()
|
|
419
|
+
`);
|
|
420
|
+
if (isAutocomplete) {
|
|
421
|
+
await page.wait(0.4);
|
|
422
|
+
console.log(`Typed "${text}" into autocomplete [${index}] — use state to see suggestions`);
|
|
423
|
+
} else {
|
|
424
|
+
console.log(`Typed "${text}" into element [${index}]`);
|
|
425
|
+
}
|
|
376
426
|
}));
|
|
377
427
|
|
|
378
428
|
operate.command('select').argument('<index>', 'Element index of <select>').argument('<option>', 'Option text')
|
|
@@ -589,19 +639,25 @@ cli({
|
|
|
589
639
|
console.log(`🔍 Verifying ${name}...\n`);
|
|
590
640
|
console.log(` Loading: ${filePath}`);
|
|
591
641
|
|
|
642
|
+
// Read adapter to check if it defines a 'limit' arg
|
|
643
|
+
const adapterSrc = fs.readFileSync(filePath, 'utf-8');
|
|
644
|
+
const hasLimitArg = /['"]limit['"]/.test(adapterSrc);
|
|
645
|
+
const limitFlag = hasLimitArg ? ' --limit 3' : '';
|
|
646
|
+
const verifyCmd = `node dist/main.js ${site} ${command}${limitFlag}`;
|
|
647
|
+
|
|
592
648
|
try {
|
|
593
|
-
const output = execSync(
|
|
649
|
+
const output = execSync(verifyCmd, {
|
|
594
650
|
cwd: path.join(path.dirname(import.meta.url.replace('file://', '')), '..'),
|
|
595
651
|
timeout: 30000,
|
|
596
652
|
encoding: 'utf-8',
|
|
597
653
|
env: process.env,
|
|
598
654
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
599
655
|
});
|
|
600
|
-
console.log(` Executing: opencli ${site} ${command}
|
|
656
|
+
console.log(` Executing: opencli ${site} ${command}${limitFlag}\n`);
|
|
601
657
|
console.log(output);
|
|
602
658
|
console.log(`\n ✓ Adapter works!`);
|
|
603
659
|
} catch (err: any) {
|
|
604
|
-
console.log(` Executing: opencli ${site} ${command}
|
|
660
|
+
console.log(` Executing: opencli ${site} ${command}${limitFlag}\n`);
|
|
605
661
|
if (err.stdout) console.log(err.stdout);
|
|
606
662
|
if (err.stderr) console.error(err.stderr.slice(0, 500));
|
|
607
663
|
console.log(`\n ✗ Adapter failed. Fix the code and try again.`);
|
|
@@ -628,7 +684,9 @@ cli({
|
|
|
628
684
|
.description('Diagnose opencli browser bridge connectivity')
|
|
629
685
|
.option('--no-live', 'Skip live browser connectivity test')
|
|
630
686
|
.option('--sessions', 'Show active automation sessions', false)
|
|
687
|
+
.option('-v, --verbose', 'Debug output')
|
|
631
688
|
.action(async (opts) => {
|
|
689
|
+
applyVerbose(opts);
|
|
632
690
|
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
|
|
633
691
|
const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
|
|
634
692
|
console.log(renderBrowserDoctorReport(report));
|
|
@@ -938,7 +996,11 @@ cli({
|
|
|
938
996
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
939
997
|
});
|
|
940
998
|
|
|
941
|
-
program
|
|
999
|
+
return program;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
1003
|
+
createProgram(BUILTIN_CLIS, USER_CLIS).parse();
|
|
942
1004
|
}
|
|
943
1005
|
|
|
944
1006
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __test__ } from './item.js';
|
|
3
|
+
|
|
4
|
+
describe('1688 item normalization', () => {
|
|
5
|
+
it('normalizes public item payload into contract fields', () => {
|
|
6
|
+
const result = __test__.normalizeItemPayload({
|
|
7
|
+
href: 'https://detail.1688.com/offer/887904326744.html',
|
|
8
|
+
title: '法式春季长袖开衫连衣裙女新款大码女装碎花吊带裙套装142077 - 阿里巴巴',
|
|
9
|
+
bodyText: `
|
|
10
|
+
青岛沁澜衣品服装有限公司
|
|
11
|
+
入驻13年
|
|
12
|
+
主营:大码女装
|
|
13
|
+
店铺回头率
|
|
14
|
+
87%
|
|
15
|
+
山东青岛
|
|
16
|
+
3套起批
|
|
17
|
+
已售1600+套
|
|
18
|
+
支持定制logo
|
|
19
|
+
`,
|
|
20
|
+
offerTitle: '法式春季长袖开衫连衣裙女新款大码女装碎花吊带裙套装142077',
|
|
21
|
+
offerId: 887904326744,
|
|
22
|
+
seller: {
|
|
23
|
+
companyName: '青岛沁澜衣品服装有限公司',
|
|
24
|
+
memberId: 'b2b-1641351767',
|
|
25
|
+
winportUrl: 'https://yinuoweierfushi.1688.com/page/index.html?spm=a1',
|
|
26
|
+
},
|
|
27
|
+
trade: {
|
|
28
|
+
beginAmount: 3,
|
|
29
|
+
priceDisplay: '96.00-98.00',
|
|
30
|
+
unit: '套',
|
|
31
|
+
saleCount: 1655,
|
|
32
|
+
offerIDatacenterSellInfo: {
|
|
33
|
+
面料名称: '莫代尔',
|
|
34
|
+
主面料成分: '莫代尔纤维',
|
|
35
|
+
sellPointModel: '{"ignore":true}',
|
|
36
|
+
},
|
|
37
|
+
offerPriceModel: {
|
|
38
|
+
currentPrices: [
|
|
39
|
+
{ beginAmount: 3, price: '98.00' },
|
|
40
|
+
{ beginAmount: 50, price: '97.00' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
gallery: {
|
|
45
|
+
mainImage: ['https://example.com/1.jpg'],
|
|
46
|
+
offerImgList: ['https://example.com/2.jpg'],
|
|
47
|
+
wlImageInfos: [{ fullPathImageURI: 'https://example.com/3.jpg' }],
|
|
48
|
+
},
|
|
49
|
+
services: [
|
|
50
|
+
{ serviceName: '延期必赔', agreeDeliveryHours: 360 },
|
|
51
|
+
{ serviceName: '品质保障' },
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.offer_id).toBe('887904326744');
|
|
56
|
+
expect(result.member_id).toBe('b2b-1641351767');
|
|
57
|
+
expect(result.shop_id).toBe('yinuoweierfushi');
|
|
58
|
+
expect(result.seller_url).toBe('https://yinuoweierfushi.1688.com');
|
|
59
|
+
expect(result.price_text).toBe('¥96.00-98.00');
|
|
60
|
+
expect(result.moq_text).toBe('3套起批');
|
|
61
|
+
expect(result.origin_place).toBe('山东青岛');
|
|
62
|
+
expect(result.delivery_days_text).toBe('360小时内发货');
|
|
63
|
+
expect(result.private_label_text).toBe('支持定制logo');
|
|
64
|
+
expect(result.visible_attributes).toEqual([
|
|
65
|
+
{ key: '面料名称', value: '莫代尔' },
|
|
66
|
+
{ key: '主面料成分', value: '莫代尔纤维' },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|