publishport-opencli 1.0.1 → 1.0.3
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/README.md +61 -61
- package/README.zh-CN.md +68 -68
- package/cli-manifest.json +502 -59
- package/clis/1688/shared.js +2 -2
- package/clis/36kr/news.js +1 -1
- package/clis/_atlassian/shared.js +1 -1
- package/clis/_shared/article/auth.js +1 -1
- package/clis/_shared/article/publish.js +1 -1
- package/clis/_shared/content-guard.js +1 -1
- package/clis/_shared/self-hosted-sites.js +2 -0
- package/clis/_shared/site-auth.js +1 -1
- package/clis/_shared/token-auth.js +2 -2
- package/clis/_shared/video-publish.js +1 -1
- package/clis/aibase/news.js +1 -1
- package/clis/amazon/shared.js +2 -2
- package/clis/antigravity/SKILL.md +12 -12
- package/clis/antigravity/storage.js +1 -1
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/archive/item.js +1 -1
- package/clis/archive/search.js +1 -1
- package/clis/archive/snapshots.js +1 -1
- package/clis/archive/wayback.js +1 -1
- package/clis/arxiv/author.js +1 -1
- package/clis/baijiahao/publish.js +1 -1
- package/clis/bbc/utils.js +1 -1
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/utils.js +1 -1
- package/clis/booking/search.js +1 -1
- package/clis/boss/utils.js +1 -1
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/detail.js +1 -1
- package/clis/chatgpt/history.js +1 -1
- package/clis/chatgpt/project-file-add.js +1 -1
- package/clis/chatgpt/project-list.js +1 -1
- package/clis/chatgpt/utils.js +2 -2
- package/clis/chess/utils.js +1 -1
- package/clis/claude/ask.js +1 -1
- package/clis/claude/history.js +1 -1
- package/clis/claude/utils.js +1 -1
- package/clis/codex/extract-diff.js +1 -1
- package/clis/coingecko/coin.js +1 -1
- package/clis/crates/utils.js +1 -1
- package/clis/csdn/stats.js +1 -1
- package/clis/dblp/utils.js +1 -1
- package/clis/defillama/utils.js +1 -1
- package/clis/devto/publish.js +1 -1
- package/clis/discord-app/utils.js +1 -1
- package/clis/dockerhub/utils.js +1 -1
- package/clis/douban/utils.js +1 -1
- package/clis/douyin/_shared/tos-upload.js +1 -1
- package/clis/douyin/hashtag.js +1 -1
- package/clis/douyin/publish-image.js +1 -1
- package/clis/douyin/publish.js +1 -1
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/endoflife/utils.js +1 -1
- package/clis/flathub/utils.js +1 -1
- package/clis/gemini/detail.js +1 -1
- package/clis/gemini/read.js +1 -1
- package/clis/geogebra/add-circle.js +1 -1
- package/clis/geogebra/add-line.js +1 -1
- package/clis/geogebra/add-point.js +1 -1
- package/clis/geogebra/add-polygon.js +1 -1
- package/clis/geogebra/eval.js +1 -1
- package/clis/geogebra/hexagon.js +1 -1
- package/clis/geogebra/info.js +1 -1
- package/clis/geogebra/triangle.js +1 -1
- package/clis/ghost/login.js +1 -0
- package/clis/ghost/publish.js +1 -1
- package/clis/ghost/shared.js +24 -1
- package/clis/ghost/sites.js +1 -0
- package/clis/ghost/whoami.js +1 -1
- package/clis/gitee/user.js +2 -2
- package/clis/github-trending/repos.js +1 -1
- package/clis/goproxy/utils.js +1 -1
- package/clis/grok/export-all.js +1 -1
- package/clis/grok/export.js +1 -1
- package/clis/hashnode/publish.js +1 -1
- package/clis/hf/datasets.js +1 -1
- package/clis/hf/models.js +1 -1
- package/clis/hf/paper.js +1 -1
- package/clis/hf/spaces.js +1 -1
- package/clis/homebrew/utils.js +1 -1
- package/clis/instagram/note.js +1 -1
- package/clis/jimeng/generate.js +1 -1
- package/clis/juejin/utils.js +1 -1
- package/clis/kuaishou/publish.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lichess/utils.js +1 -1
- package/clis/linkedin/search.js +2 -2
- package/clis/linux-do/feed.js +2 -2
- package/clis/lobsters/domain.js +1 -1
- package/clis/mastodon/login.js +1 -0
- package/clis/mastodon/post.js +1 -1
- package/clis/mastodon/shared.js +10 -10
- package/clis/mastodon/sites.js +1 -0
- package/clis/mastodon/whoami.js +1 -1
- package/clis/maven/utils.js +1 -1
- package/clis/mdn/search.js +1 -1
- package/clis/medium/tag.js +1 -1
- package/clis/notebooklm/current.js +1 -1
- package/clis/notebooklm/get.js +1 -1
- package/clis/notebooklm/history.js +1 -1
- package/clis/notebooklm/note-list.js +1 -1
- package/clis/notebooklm/notes-get.js +1 -1
- package/clis/notebooklm/open.js +1 -1
- package/clis/notebooklm/source-fulltext.js +1 -1
- package/clis/notebooklm/source-get.js +1 -1
- package/clis/notebooklm/source-guide.js +1 -1
- package/clis/notebooklm/source-list.js +1 -1
- package/clis/notebooklm/summary.js +1 -1
- package/clis/notebooklm/utils.js +1 -1
- package/clis/npm/utils.js +1 -1
- package/clis/nuget/utils.js +1 -1
- package/clis/nvd/cve.js +1 -1
- package/clis/oeis/utils.js +1 -1
- package/clis/ones/common.js +2 -2
- package/clis/ones/me.js +1 -1
- package/clis/ones/tasks.js +1 -1
- package/clis/ones/token-info.js +1 -1
- package/clis/ones/worklog.js +1 -1
- package/clis/openalex/utils.js +1 -1
- package/clis/oschina/stats.js +1 -1
- package/clis/osv/utils.js +1 -1
- package/clis/packagist/utils.js +1 -1
- package/clis/pixiv/detail.js +1 -1
- package/clis/pixiv/user.js +1 -1
- package/clis/producthunt/utils.js +1 -1
- package/clis/pypi/utils.js +1 -1
- package/clis/qiita/gql.js +2 -2
- package/clis/rest-countries/utils.js +1 -1
- package/clis/rfc/utils.js +1 -1
- package/clis/rubygems/utils.js +1 -1
- package/clis/semanticscholar/utils.js +1 -1
- package/clis/slock/errors.js +1 -1
- package/clis/spotify/spotify.js +1 -1
- package/clis/spotify/utils.js +1 -1
- package/clis/stackoverflow/utils.js +1 -1
- package/clis/steam/utils.js +1 -1
- package/clis/substack/auth.js +2 -2
- package/clis/suno/generate.js +2 -2
- package/clis/suno/list.js +3 -3
- package/clis/telegram/send.js +1 -1
- package/clis/tiktok/creator-videos.js +1 -1
- package/clis/tiktok/publish.js +1 -1
- package/clis/tiktok/utils.js +1 -1
- package/clis/trae-cn/activity.js +1 -1
- package/clis/trae-cn/approve.js +1 -1
- package/clis/trae-cn/ask.js +1 -1
- package/clis/trae-cn/dump.js +1 -1
- package/clis/trae-cn/export.js +1 -1
- package/clis/trae-cn/model.js +1 -1
- package/clis/trae-cn/new.js +1 -1
- package/clis/trae-cn/read.js +1 -1
- package/clis/trae-cn/screenshot.js +1 -1
- package/clis/trae-cn/select-model.js +1 -1
- package/clis/trae-cn/send.js +1 -1
- package/clis/trae-cn/setup.js +1 -1
- package/clis/trae-cn/status.js +1 -1
- package/clis/trae-cn/targets.js +1 -1
- package/clis/trae-cn/watch.js +1 -1
- package/clis/trae-solo/state-fs.js +1 -1
- package/clis/tumblr/publish.js +1 -1
- package/clis/tvmaze/utils.js +1 -1
- package/clis/twitter/bookmark-folder.js +1 -1
- package/clis/twitter/download.js +1 -1
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +2 -2
- package/clis/twitter/likes.js +1 -1
- package/clis/twitter/list-add-batch.js +1 -1
- package/clis/twitter/list-add-core.js +1 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +1 -1
- package/clis/twitter/list-delete.js +3 -3
- package/clis/twitter/list-remove-batch.js +1 -1
- package/clis/twitter/list-remove.js +1 -1
- package/clis/twitter/list-tweets.js +1 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/search.js +1 -1
- package/clis/twitter/shared.js +1 -1
- package/clis/twitter/tweets.js +1 -1
- package/clis/uisdc/news.js +1 -1
- package/clis/v2ex/daily.js +2 -2
- package/clis/v2ex/me.js +2 -2
- package/clis/v2ex/notifications.js +1 -1
- package/clis/wikidata/utils.js +1 -1
- package/clis/wikipedia/page.js +2 -2
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/utils.js +1 -1
- package/clis/wordpress/login.js +1 -1
- package/clis/wordpress/publish.js +1 -1
- package/clis/wordpress/shared.js +3 -3
- package/clis/wordpress/sites.js +1 -0
- package/clis/wordpress/whoami.js +1 -1
- package/clis/woshipm/stats.js +1 -1
- package/clis/xiaoe/content.js +1 -1
- package/clis/xiaohongshu/draft-delete.js +1 -1
- package/clis/xiaohongshu/draft-open.js +1 -1
- package/clis/xiaohongshu/draft-utils.js +2 -2
- package/clis/xiaohongshu/publish-video.js +1 -1
- package/clis/xiaohongshu/publish.js +1 -1
- package/clis/xiaoyuzhou/auth.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/yollomi/background.js +1 -1
- package/clis/yollomi/edit.js +1 -1
- package/clis/yollomi/generate.js +1 -1
- package/clis/yollomi/try-on.js +1 -1
- package/clis/youtube/publish.js +1 -1
- package/clis/zhihu/answer-comments.js +1 -1
- package/clis/zhihu/answer-detail.js +1 -1
- package/clis/zhihu/collection.js +2 -2
- package/clis/zhihu/collections.js +1 -1
- package/clis/zhihu/question.js +1 -1
- package/clis/zhihu/search.js +1 -1
- package/clis/zhihu/target.js +1 -1
- package/clis/zhihu/user-arg.js +1 -1
- package/dist/src/adapter-shadow.js +1 -1
- package/dist/src/browser/analyze.js +1 -1
- package/dist/src/browser/article-extract.d.ts +1 -1
- package/dist/src/browser/base-page.js +3 -3
- package/dist/src/browser/cdp.js +1 -1
- package/dist/src/browser/daemon-client.d.ts +1 -1
- package/dist/src/browser/daemon-lifecycle.js +6 -6
- package/dist/src/browser/daemon-version.js +1 -1
- package/dist/src/browser/dom-snapshot.d.ts +1 -1
- package/dist/src/browser/dom-snapshot.js +2 -2
- package/dist/src/browser/errors.js +2 -2
- package/dist/src/browser/network-cache.js +1 -1
- package/dist/src/browser/profile.js +1 -1
- package/dist/src/browser/target-resolver.js +4 -4
- package/dist/src/browser/verify-fixture.js +1 -1
- package/dist/src/browser-session-lock.js +1 -1
- package/dist/src/build-manifest.d.ts +1 -1
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +4 -4
- package/dist/src/cli-argv-preprocess.js +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +19 -19
- package/dist/src/commanderAdapter.js +1 -1
- package/dist/src/commands/auth.js +2 -2
- package/dist/src/commands/daemon.d.ts +4 -4
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/completion-shared.js +12 -12
- package/dist/src/completion.d.ts +2 -2
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.js +1 -1
- package/dist/src/daemon-utils.d.ts +1 -1
- package/dist/src/daemon-utils.js +1 -1
- package/dist/src/daemon.d.ts +2 -2
- package/dist/src/daemon.js +1 -1
- package/dist/src/discovery.d.ts +7 -7
- package/dist/src/discovery.js +2 -2
- package/dist/src/doctor.d.ts +1 -1
- package/dist/src/doctor.js +6 -6
- package/dist/src/electron-apps.d.ts +1 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/errors.d.ts +2 -2
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.js +1 -1
- package/dist/src/external.d.ts +1 -1
- package/dist/src/external.js +1 -1
- package/dist/src/help.d.ts +1 -1
- package/dist/src/help.js +3 -3
- package/dist/src/hooks.d.ts +1 -1
- package/dist/src/launcher.js +2 -2
- package/dist/src/logger.d.ts +1 -1
- package/dist/src/main.d.ts +1 -1
- package/dist/src/main.js +1 -1
- package/dist/src/observation/artifact.js +2 -2
- package/dist/src/package-paths.js +1 -1
- package/dist/src/package-paths.test.d.ts +1 -0
- package/dist/src/plugin-manifest.d.ts +12 -3
- package/dist/src/plugin-manifest.js +1 -1
- package/dist/src/plugin-scaffold.d.ts +2 -2
- package/dist/src/plugin-scaffold.js +9 -9
- package/dist/src/plugin.d.ts +4 -4
- package/dist/src/plugin.js +10 -10
- package/dist/src/rate-limit.js +1 -1
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/runtime-detect.d.ts +1 -1
- package/dist/src/serialization.js +1 -1
- package/dist/src/skills.js +6 -6
- package/dist/src/user-runtime.d.ts +8 -0
- package/dist/src/user-runtime.js +1 -0
- package/dist/src/user-runtime.test.d.ts +1 -0
- package/dist/src/validate.js +1 -1
- package/dist/src/validate.test.d.ts +1 -1
- package/docs/guide/browser-bridge.md +23 -23
- package/docs/guide/electron-app-cli.md +3 -3
- package/docs/guide/exit-codes.md +3 -3
- package/docs/guide/extending-opencli.md +37 -37
- package/docs/guide/getting-started.md +19 -19
- package/docs/guide/installation.md +6 -6
- package/docs/guide/plugins.md +32 -32
- package/docs/guide/remote-orchestration.md +5 -5
- package/docs/guide/troubleshooting.md +9 -9
- package/package.json +3 -2
- package/scripts/fetch-adapters.js +6 -6
- package/scripts/postinstall.js +14 -14
- package/skills/opencli-adapter-author/SKILL.md +24 -24
- package/skills/opencli-adapter-author/references/adapter-template.md +13 -13
- package/skills/opencli-adapter-author/references/api-discovery.md +25 -25
- package/skills/opencli-adapter-author/references/coverage-matrix.md +3 -3
- package/skills/opencli-adapter-author/references/field-decode-playbook.md +10 -10
- package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +2 -2
- package/skills/opencli-adapter-author/references/site-memory.md +11 -11
- package/skills/opencli-adapter-author/references/site-recon.md +7 -7
- package/skills/opencli-adapter-author/references/strategy-selection.md +3 -3
- package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +4 -4
- package/skills/opencli-adapter-author/references/typed-errors.md +2 -2
- package/skills/opencli-autofix/SKILL.md +29 -29
- package/skills/opencli-browser/SKILL.md +55 -55
- package/skills/opencli-browser-sitemap/SKILL.md +7 -7
- package/skills/opencli-sitemap-author/SKILL.md +11 -11
- package/skills/opencli-sitemap-author/references/sitemap-schema.md +19 -19
- package/skills/opencli-usage/SKILL.md +43 -43
package/clis/zhihu/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as v,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as K,AuthRequiredError as S,CommandExecutionError as $,EmptyResultError as q}from"@jackwener/opencli/errors";import{stripHtml as M}from"./text.js";function U(D){const B=D.object||{};if(B.id!=null)return`${B.type||""}:${B.id}`;return null}function A(D){const B=D.id==null?"":String(D.id);if(D.type==="answer"){const N=D.question?.id==null?"":String(D.question.id);return N&&B?`https://www.zhihu.com/question/${N}/answer/${B}`:""}if(D.type==="article")return B?`https://zhuanlan.zhihu.com/p/${B}`:"";if(D.type==="question")return B?`https://www.zhihu.com/question/${B}`:"";return""}function L(D){if(typeof D!=="string"||!D)return"";try{const B=new URL(D);if(B.hostname==="api.zhihu.com"&&B.pathname==="/search_v3")return`https://www.zhihu.com/api/v4/search_v3${B.search}`;if(B.hostname==="www.zhihu.com"&&B.pathname==="/api/v4/search_v3")return B.toString()}catch{return""}return""}const C=1000,w=20,Q=["all","answer","article","question"];function R(D){const B=Number(D??10);if(!Number.isInteger(B)||B<=0||B>C)throw new K(`zhihu search --limit must be a positive integer no greater than ${C}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");return B}function z(D){const B=String(D||"").trim();if(!B)throw new K("zhihu search query must not be empty","Example:
|
|
1
|
+
import{cli as v,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as K,AuthRequiredError as S,CommandExecutionError as $,EmptyResultError as q}from"@jackwener/opencli/errors";import{stripHtml as M}from"./text.js";function U(D){const B=D.object||{};if(B.id!=null)return`${B.type||""}:${B.id}`;return null}function A(D){const B=D.id==null?"":String(D.id);if(D.type==="answer"){const N=D.question?.id==null?"":String(D.question.id);return N&&B?`https://www.zhihu.com/question/${N}/answer/${B}`:""}if(D.type==="article")return B?`https://zhuanlan.zhihu.com/p/${B}`:"";if(D.type==="question")return B?`https://www.zhihu.com/question/${B}`:"";return""}function L(D){if(typeof D!=="string"||!D)return"";try{const B=new URL(D);if(B.hostname==="api.zhihu.com"&&B.pathname==="/search_v3")return`https://www.zhihu.com/api/v4/search_v3${B.search}`;if(B.hostname==="www.zhihu.com"&&B.pathname==="/api/v4/search_v3")return B.toString()}catch{return""}return""}const C=1000,w=20,Q=["all","answer","article","question"];function R(D){const B=Number(D??10);if(!Number.isInteger(B)||B<=0||B>C)throw new K(`zhihu search --limit must be a positive integer no greater than ${C}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");return B}function z(D){const B=String(D||"").trim();if(!B)throw new K("zhihu search query must not be empty","Example: ppcli zhihu search codex");return B}function I(D){const B=String(D||"all");if(!Q.includes(B))throw new K(`zhihu search --type must be one of: ${Q.join(", ")}`,"Example: ppcli zhihu search codex --type answer");return B}function H(D){if(D&&typeof D==="object"&&"data"in D&&"session"in D)return D.data;return D}function P(D,B){const N=H(D);if(!N||typeof N!=="object"||Array.isArray(N))throw new $("Zhihu search returned malformed payload");if(N.__httpError){const O=N.__httpError;if(O===401||O===403)throw new S("www.zhihu.com","Failed to fetch search results from Zhihu");throw new $(`Zhihu search request failed${O?` (HTTP ${O})`:""}`,"Try again later or rerun with -v for more detail")}if(N.__fetchError)throw new $("Zhihu search request failed",String(N.__fetchError));if(!Array.isArray(N.data))throw new $("Zhihu search returned malformed data list",`URL: ${B}`);if(!N.paging||typeof N.paging!=="object")throw new $("Zhihu search returned malformed paging data",`URL: ${B}`);return N}function T(D){return`
|
|
2
2
|
(async () => {
|
|
3
3
|
try {
|
|
4
4
|
const r = await fetch(${JSON.stringify(D)}, { credentials: 'include' });
|
package/clis/zhihu/target.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CliError as o}from"@jackwener/opencli/errors";const c=/^user:([A-Za-z0-9_-]+)$/,w=/^question:(\d+)$/,u=/^answer:(\d+):(\d+)$/,l=/^article:(\d+)$/,p=/^\/people\/([A-Za-z0-9_-]+)\/?$/,d=/^\/question\/(\d+)\/?$/,m=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,E=/^\/p\/(\d+)\/?$/,_=/^https:\/\/(?::)?@/i;function f(n){return n.protocol==="https:"&&n.username===""&&n.password===""&&n.port===""}export function parseTarget(n){const e=String(n).trim();if(_.test(e))throw new o("INVALID_INPUT","Zhihu write commands require a normal HTTPS Zhihu URL without malformed authority","Example: https://www.zhihu.com/question/123456");if(e.startsWith("answer:")&&!u.test(e))throw new o("INVALID_INPUT","Invalid answer target, expected answer:<questionId>:<answerId>","Example:
|
|
1
|
+
import{CliError as o}from"@jackwener/opencli/errors";const c=/^user:([A-Za-z0-9_-]+)$/,w=/^question:(\d+)$/,u=/^answer:(\d+):(\d+)$/,l=/^article:(\d+)$/,p=/^\/people\/([A-Za-z0-9_-]+)\/?$/,d=/^\/question\/(\d+)\/?$/,m=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,E=/^\/p\/(\d+)\/?$/,_=/^https:\/\/(?::)?@/i;function f(n){return n.protocol==="https:"&&n.username===""&&n.password===""&&n.port===""}export function parseTarget(n){const e=String(n).trim();if(_.test(e))throw new o("INVALID_INPUT","Zhihu write commands require a normal HTTPS Zhihu URL without malformed authority","Example: https://www.zhihu.com/question/123456");if(e.startsWith("answer:")&&!u.test(e))throw new o("INVALID_INPUT","Invalid answer target, expected answer:<questionId>:<answerId>","Example: ppcli zhihu like answer:123:456 --execute");let t=e.match(c);if(t)return{kind:"user",slug:t[1],url:`https://www.zhihu.com/people/${t[1]}`};t=e.match(w);if(t)return{kind:"question",id:t[1],url:`https://www.zhihu.com/question/${t[1]}`};t=e.match(u);if(t)return{kind:"answer",questionId:t[1],id:t[2],url:`https://www.zhihu.com/question/${t[1]}/answer/${t[2]}`};t=e.match(l);if(t)return{kind:"article",id:t[1],url:`https://zhuanlan.zhihu.com/p/${t[1]}`};try{const i=new URL(e);if(!f(i))throw Error("unsupported zhihu url variant");if(i.hostname==="www.zhihu.com"){const r=i.pathname.match(p);if(r){const a=r[1];return{kind:"user",slug:a,url:`https://www.zhihu.com/people/${a}`}}const h=i.pathname.match(d);if(h)return{kind:"question",id:h[1],url:`https://www.zhihu.com/question/${h[1]}`};const s=i.pathname.match(m);if(s)return{kind:"answer",questionId:s[1],id:s[2],url:`https://www.zhihu.com/question/${s[1]}/answer/${s[2]}`}}if(i.hostname==="zhuanlan.zhihu.com"){const r=i.pathname.match(E);if(r)return{kind:"article",id:r[1],url:`https://zhuanlan.zhihu.com/p/${r[1]}`}}}catch{}throw new o("INVALID_INPUT","Zhihu write commands require a Zhihu URL or typed target like question:123 or answer:123:456","Example: ppcli zhihu like answer:123:456 --execute")}export function assertAllowedKinds(n,e){if(!{follow:["user","question"],like:["answer","article"],favorite:["answer","article"],comment:["answer","article"],answer:["question"]}[n]?.includes(e.kind))throw new o("UNSUPPORTED_TARGET",`zhihu ${n} does not support ${e.kind} targets`);return e}export const __test__={parseTarget,assertAllowedKinds};
|
package/clis/zhihu/user-arg.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CliError as J}from"@jackwener/opencli/errors";const N=/^[A-Za-z0-9_-]+$/,Q=/^user:([A-Za-z0-9_-]+)$/,V=/^\/people\/([A-Za-z0-9_-]+)\/?$/;export function parseZhihuUser(K){const q=String(K??"").trim();if(!q)throw new J("INVALID_INPUT","A Zhihu user is required","Example:
|
|
1
|
+
import{CliError as J}from"@jackwener/opencli/errors";const N=/^[A-Za-z0-9_-]+$/,Q=/^user:([A-Za-z0-9_-]+)$/,V=/^\/people\/([A-Za-z0-9_-]+)\/?$/;export function parseZhihuUser(K){const q=String(K??"").trim();if(!q)throw new J("INVALID_INPUT","A Zhihu user is required","Example: ppcli zhihu user wen-jie-16-47");const B=q.match(Q);if(B)return B[1];if(N.test(q))return q;try{const z=new URL(q);if(z.protocol==="https:"&&z.hostname==="www.zhihu.com"){const D=z.pathname.match(V);if(D)return D[1]}}catch{}throw new J("INVALID_INPUT",`Invalid Zhihu user: ${q}`,"Use a url_token (wen-jie-16-47) or a people URL (https://www.zhihu.com/people/wen-jie-16-47)")}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as d from"node:fs";import*as y from"node:os";import*as o from"node:path";import{fileURLToPath as j}from"node:url";import{findPackageRoot as w,getCliManifestPath as S}from"./package-paths.js";function g(){return o.join(w(j(import.meta.url)),"clis")}function f(e){try{return d.readdirSync(e,{withFileTypes:!0})}catch{return[]}}function C(e){try{const n=d.readFileSync(S(e),"utf-8"),t=JSON.parse(n),i=new Set;for(const s of t){const r=s.sourceFile??s.modulePath;if(r)i.add(o.resolve(e,r))}return i}catch{return new Set}}export function findShadowedUserAdapters(e={}){const n=e.userClisDir??o.join(y.homedir(),".
|
|
1
|
+
import*as d from"node:fs";import*as y from"node:os";import*as o from"node:path";import{fileURLToPath as j}from"node:url";import{findPackageRoot as w,getCliManifestPath as S}from"./package-paths.js";function g(){return o.join(w(j(import.meta.url)),"clis")}function f(e){try{return d.readdirSync(e,{withFileTypes:!0})}catch{return[]}}function C(e){try{const n=d.readFileSync(S(e),"utf-8"),t=JSON.parse(n),i=new Set;for(const s of t){const r=s.sourceFile??s.modulePath;if(r)i.add(o.resolve(e,r))}return i}catch{return new Set}}export function findShadowedUserAdapters(e={}){const n=e.userClisDir??o.join(y.homedir(),".ppcli","clis"),t=e.builtinClisDir??g(),i=C(t),s=[];for(const r of f(n)){if(!r.isDirectory())continue;const a=r.name,l=o.join(n,a),m=o.join(t,a);for(const c of f(l)){if(!c.isFile()||!c.name.endsWith(".js"))continue;const p=o.join(l,c.name),u=o.join(m,c.name),h=o.resolve(u);if(!i.has(h))continue;s.push({name:`${a}/${c.name.replace(/\.js$/,"")}`,userPath:p,builtinPath:u})}}return s.sort((r,a)=>r.name.localeCompare(a.name))}export function formatAdapterShadowIssue(e){const n=e.slice(0,10),t=["Local adapter overrides shadow packaged adapters:"];for(const i of n)t.push(` ${i.name}: ${i.userPath} overrides ${i.builtinPath}`);if(e.length>n.length)t.push(` ... and ${e.length-n.length} more`);t.push("Remove the local ~/.ppcli/clis copy, or run ppcli adapter reset <site>, when you want packaged updates.");return t.join(`
|
|
2
2
|
`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const H=[{vendor:"aliyun_waf",cookiePatterns:[/^acw_sc__v2$/,/^acw_tc$/,/^ssxmod_itna/],bodyPatterns:[/arg1\s*=\s*['"][0-9A-F]{30,}/,/\/ntc_captcha\//i],implication:"Direct Node-side fetch/curl will return the slider HTML. Validate the endpoint in browser context first; HTML COOKIE adapters still finish with Node-side fetch + page.getCookies."},{vendor:"cloudflare",cookiePatterns:[/^__cf_bm$/,/^cf_clearance$/,/^__cfduid$/],bodyPatterns:[/Cloudflare Ray ID/i,/Checking your browser before accessing/i,/cf-chl-/i],implication:"Cloudflare bot check. Start from a real browser session; probe in browser context first. HTML COOKIE adapters still finish with Node-side fetch + page.getCookies."},{vendor:"akamai",cookiePatterns:[/^_abck$/,/^bm_sz$/,/^bm_sv$/],bodyPatterns:[/akamai/i],implication:"Akamai Bot Manager. Probe in browser context first; keep final HTML COOKIE adapters on Node-side fetch + page.getCookies."},{vendor:"geetest",cookiePatterns:[],bodyPatterns:[/geetest/i,/gt_captcha/i],implication:"Geetest slider/puzzle captcha. Agent cannot bypass programmatically — requires UI strategy or human-in-loop."}];export function detectAntiBot(q){const M=[];let Q=null;for(const V of H){const Z=[];for(const $ of V.cookiePatterns){const X=q.cookieNames.find((O)=>$.test(O));if(X)Z.push(`cookie:${X}`)}for(const $ of V.bodyPatterns)for(const X of q.networkEntries)if(X.bodyPreview&&$.test(X.bodyPreview)){Z.push(`body:${X.url}`);break}if(Z.length>0&&!Q){Q=V;M.push(...Z)}}if(!Q)return{detected:!1,vendor:null,evidence:[],implication:"No known anti-bot signatures. Try Node-side COOKIE fetch first; if endpoint validation is blocked, retry from browser context."};return{detected:!0,vendor:Q.vendor,evidence:M,implication:Q.implication}}const I=/(?:analytics|beacon|collect|telemetry|tracking|sentry|doubleclick|google-analytics|googletagmanager|adservice|\/ads?(?:[/?#]|$)|metrics?|pixel|personalization|experiment|\/events?(?:[/?#]|$))/i,U=/^(?:data|items?|results?|records?|list|rows?|edges?|nodes?|timeline|users?|title|name|text|content|body|price|amount|id|url|avatar|nickname|desc|comments?|likes?|shares?|total|page|cursor|next|rank|score|date|time|author)$/i,B=/^(?:event|events|trace|traceid|sessionid|clientid|visitorid|experiment|abtest|beacon|analytics|metrics?|pixel|log|logs)$/i;function P(q){return Math.max(0,Math.min(1,Math.round(q*100)/100))}function R(q){if(!q)return null;const M=q.trim();if(!M)return null;try{return JSON.parse(M)}catch{return M}}function Y(q,M="$",Q=[],V=0){if(V>4||Q.length>=24)return Q;if(Array.isArray(q)){Q.push(`${M}:array(${q.length})`);if(q.length>0)Y(q[0],`${M}[0]`,Q,V+1);return Q}if(!q||typeof q!=="object"){Q.push(`${M}:${typeof q}`);return Q}const Z=Object.entries(q).slice(0,12);for(const[$,X]of Z){Q.push(`${M}.${$}`);if(Array.isArray(X))Q.push(`${M}.${$}:array(${X.length})`);else if(X&&typeof X==="object")Y(X,`${M}.${$}`,Q,V+1);else Q.push(`${M}.${$}:${typeof X}`)}return Q}function G(q,M,Q=0){if(Q>4||!q||typeof q!=="object")return 0;if(Array.isArray(q))return q.slice(0,3).reduce((V,Z)=>V+G(Z,M,Q+1),0);return Object.entries(q).reduce((V,[Z,$])=>V+(M(Z)?1:0)+G($,M,Q+1),0)}function T(q,M=0){if(M>4||!q||typeof q!=="object")return!1;if(Array.isArray(q))return q.length>0;return Object.values(q).some((Q)=>T(Q,M+1))}export function scoreEndpointEvidence(q){const M=[],Q=R(q.bodyPreview);let V=0;if(q.status>=200&&q.status<300){V+=0.15;M.push("2xx status")}else if(q.status===401||q.status===403)return{url:q.url,status:q.status,contentType:q.contentType,real_data_score:0.05,verdict:"blocked",reasons:["auth-blocked status"],sample_paths:[]};else M.push(`non-2xx status ${q.status}`);if(/json/i.test(q.contentType)){V+=0.2;M.push("json content-type")}else if(/html/i.test(q.contentType)){V-=0.25;M.push("html content-type")}else if(/javascript|text/i.test(q.contentType)){V+=0.05;M.push("text/script content-type")}if(I.test(q.url)){V-=0.3;M.push("telemetry-like url")}const Z=Y(Q).slice(0,8);if(typeof Q==="string"){if(/^\s*</.test(Q)||/<!doctype html|<html/i.test(Q)){V-=0.25;M.push("html body")}else if(Q.trim().length>20){V+=0.05;M.push("non-empty text body")}}else if(Array.isArray(Q))if(Q.length>0){V+=0.3;M.push("non-empty top-level array")}else{V-=0.15;M.push("empty array")}else if(Q&&typeof Q==="object"){if(Object.keys(Q).length===0){V-=0.15;M.push("empty object")}else{V+=0.12;M.push("json object body")}const L=G(Q,(W)=>U.test(W));if(L>0){V+=Math.min(0.3,L*0.05);M.push(`${L} business-like key${L===1?"":"s"}`)}if(T(Q)){V+=0.2;M.push("nested non-empty array")}const z=G(Q,(W)=>B.test(W));if(z>0&&L===0){V-=Math.min(0.25,z*0.08);M.push(`${z} tracking-like key${z===1?"":"s"} without business keys`)}}const $=P(V),X=$>=0.65?"likely_data":$>=0.35?"maybe_data":"noise";return{url:q.url,status:q.status,contentType:q.contentType,real_data_score:$,verdict:X,reasons:M,sample_paths:Z}}export function scoreNetworkEvidence(q){return q.networkEntries.map(scoreEndpointEvidence).filter((M)=>M.verdict!=="noise"||M.real_data_score>0).sort((M,Q)=>Q.real_data_score-M.real_data_score).slice(0,8)}export function classifyPattern(q){const M=q.networkEntries.filter((X)=>/json/i.test(X.contentType)),V=scoreNetworkEvidence(q).filter((X)=>X.verdict==="likely_data"||X.verdict==="maybe_data").length,Z=q.networkEntries.filter((X)=>X.status===401||X.status===403).length,$=q.initialState.__INITIAL_STATE__||q.initialState.__NUXT__||q.initialState.__NEXT_DATA__||q.initialState.__APOLLO_STATE__;if(Z>=2&&M.length>=1)return{pattern:"D",reason:`${Z} auth-failing API responses seen — endpoint is token-gated`,json_responses:M.length,real_data_candidates:V,auth_failures:Z};if($)return{pattern:"B",reason:`SSR state global present: ${Object.entries(q.initialState).filter(([,O])=>O).map(([O])=>O).join(", ")}`,json_responses:M.length,real_data_candidates:V,auth_failures:Z};if(V>=1)return{pattern:"A",reason:`${V} captured response${V===1?"":"s"} look like real data — inspect api_candidates before choosing a strategy`,json_responses:M.length,real_data_candidates:V,auth_failures:Z};return{pattern:"C",reason:M.length>0?`${M.length} JSON response${M.length===1?"":"s"} observed, but none look like target data — likely telemetry/side-channel; treat as HTML/DOM until an endpoint validates`:"No JSON XHR and no SSR state — HTML scrape (Pattern C); escalate to E manually if WebSocket traffic appears",json_responses:M.length,real_data_candidates:V,auth_failures:Z}}export function findNearestAdapter(q,M){let Q;try{Q=new URL(q).hostname}catch{return null}const V=Q.replace(/^www\./,""),Z=V.split("."),$=Z.slice(-2).join("."),X=Z.length>1?Z[Z.length-2]:V,O=new Map;for(const z of M.values()){const W=z.domain?.toLowerCase();if(W&&(V.endsWith(W)||W.endsWith($))||z.site.toLowerCase()===X?.toLowerCase()||V.includes(z.site.toLowerCase())){const J=O.get(z.site)??[];J.push(z);O.set(z.site,J)}}if(O.size===0)return null;let L=null;for(const z of O)if(!L||z[1].length>L[1].length)L=z;if(!L)return null;return{site:L[0],example_commands:L[1].slice(0,5).map((z)=>`${z.site} ${z.name}`),reason:`${L[1].length} existing adapter${L[1].length===1?"":"s"} target this site — reuse strategy/cookie config`}}export function analyzeSite(q,M){const Q=classifyPattern(q),V=detectAntiBot(q),Z=scoreNetworkEvidence(q),$=findNearestAdapter(q.finalUrl,M);let X;if(V.detected)X=V.implication;else if(Q.pattern==="A")X="Inspect `api_candidates`, then replay the best endpoint and record the status/content-type/sample shape in your strategy note; do not choose API strategy from XHR count alone.";else if(Q.pattern==="B")X='Read the SSR global via `
|
|
1
|
+
const H=[{vendor:"aliyun_waf",cookiePatterns:[/^acw_sc__v2$/,/^acw_tc$/,/^ssxmod_itna/],bodyPatterns:[/arg1\s*=\s*['"][0-9A-F]{30,}/,/\/ntc_captcha\//i],implication:"Direct Node-side fetch/curl will return the slider HTML. Validate the endpoint in browser context first; HTML COOKIE adapters still finish with Node-side fetch + page.getCookies."},{vendor:"cloudflare",cookiePatterns:[/^__cf_bm$/,/^cf_clearance$/,/^__cfduid$/],bodyPatterns:[/Cloudflare Ray ID/i,/Checking your browser before accessing/i,/cf-chl-/i],implication:"Cloudflare bot check. Start from a real browser session; probe in browser context first. HTML COOKIE adapters still finish with Node-side fetch + page.getCookies."},{vendor:"akamai",cookiePatterns:[/^_abck$/,/^bm_sz$/,/^bm_sv$/],bodyPatterns:[/akamai/i],implication:"Akamai Bot Manager. Probe in browser context first; keep final HTML COOKIE adapters on Node-side fetch + page.getCookies."},{vendor:"geetest",cookiePatterns:[],bodyPatterns:[/geetest/i,/gt_captcha/i],implication:"Geetest slider/puzzle captcha. Agent cannot bypass programmatically — requires UI strategy or human-in-loop."}];export function detectAntiBot(q){const M=[];let Q=null;for(const V of H){const Z=[];for(const $ of V.cookiePatterns){const X=q.cookieNames.find((O)=>$.test(O));if(X)Z.push(`cookie:${X}`)}for(const $ of V.bodyPatterns)for(const X of q.networkEntries)if(X.bodyPreview&&$.test(X.bodyPreview)){Z.push(`body:${X.url}`);break}if(Z.length>0&&!Q){Q=V;M.push(...Z)}}if(!Q)return{detected:!1,vendor:null,evidence:[],implication:"No known anti-bot signatures. Try Node-side COOKIE fetch first; if endpoint validation is blocked, retry from browser context."};return{detected:!0,vendor:Q.vendor,evidence:M,implication:Q.implication}}const I=/(?:analytics|beacon|collect|telemetry|tracking|sentry|doubleclick|google-analytics|googletagmanager|adservice|\/ads?(?:[/?#]|$)|metrics?|pixel|personalization|experiment|\/events?(?:[/?#]|$))/i,U=/^(?:data|items?|results?|records?|list|rows?|edges?|nodes?|timeline|users?|title|name|text|content|body|price|amount|id|url|avatar|nickname|desc|comments?|likes?|shares?|total|page|cursor|next|rank|score|date|time|author)$/i,B=/^(?:event|events|trace|traceid|sessionid|clientid|visitorid|experiment|abtest|beacon|analytics|metrics?|pixel|log|logs)$/i;function P(q){return Math.max(0,Math.min(1,Math.round(q*100)/100))}function R(q){if(!q)return null;const M=q.trim();if(!M)return null;try{return JSON.parse(M)}catch{return M}}function Y(q,M="$",Q=[],V=0){if(V>4||Q.length>=24)return Q;if(Array.isArray(q)){Q.push(`${M}:array(${q.length})`);if(q.length>0)Y(q[0],`${M}[0]`,Q,V+1);return Q}if(!q||typeof q!=="object"){Q.push(`${M}:${typeof q}`);return Q}const Z=Object.entries(q).slice(0,12);for(const[$,X]of Z){Q.push(`${M}.${$}`);if(Array.isArray(X))Q.push(`${M}.${$}:array(${X.length})`);else if(X&&typeof X==="object")Y(X,`${M}.${$}`,Q,V+1);else Q.push(`${M}.${$}:${typeof X}`)}return Q}function G(q,M,Q=0){if(Q>4||!q||typeof q!=="object")return 0;if(Array.isArray(q))return q.slice(0,3).reduce((V,Z)=>V+G(Z,M,Q+1),0);return Object.entries(q).reduce((V,[Z,$])=>V+(M(Z)?1:0)+G($,M,Q+1),0)}function T(q,M=0){if(M>4||!q||typeof q!=="object")return!1;if(Array.isArray(q))return q.length>0;return Object.values(q).some((Q)=>T(Q,M+1))}export function scoreEndpointEvidence(q){const M=[],Q=R(q.bodyPreview);let V=0;if(q.status>=200&&q.status<300){V+=0.15;M.push("2xx status")}else if(q.status===401||q.status===403)return{url:q.url,status:q.status,contentType:q.contentType,real_data_score:0.05,verdict:"blocked",reasons:["auth-blocked status"],sample_paths:[]};else M.push(`non-2xx status ${q.status}`);if(/json/i.test(q.contentType)){V+=0.2;M.push("json content-type")}else if(/html/i.test(q.contentType)){V-=0.25;M.push("html content-type")}else if(/javascript|text/i.test(q.contentType)){V+=0.05;M.push("text/script content-type")}if(I.test(q.url)){V-=0.3;M.push("telemetry-like url")}const Z=Y(Q).slice(0,8);if(typeof Q==="string"){if(/^\s*</.test(Q)||/<!doctype html|<html/i.test(Q)){V-=0.25;M.push("html body")}else if(Q.trim().length>20){V+=0.05;M.push("non-empty text body")}}else if(Array.isArray(Q))if(Q.length>0){V+=0.3;M.push("non-empty top-level array")}else{V-=0.15;M.push("empty array")}else if(Q&&typeof Q==="object"){if(Object.keys(Q).length===0){V-=0.15;M.push("empty object")}else{V+=0.12;M.push("json object body")}const L=G(Q,(W)=>U.test(W));if(L>0){V+=Math.min(0.3,L*0.05);M.push(`${L} business-like key${L===1?"":"s"}`)}if(T(Q)){V+=0.2;M.push("nested non-empty array")}const z=G(Q,(W)=>B.test(W));if(z>0&&L===0){V-=Math.min(0.25,z*0.08);M.push(`${z} tracking-like key${z===1?"":"s"} without business keys`)}}const $=P(V),X=$>=0.65?"likely_data":$>=0.35?"maybe_data":"noise";return{url:q.url,status:q.status,contentType:q.contentType,real_data_score:$,verdict:X,reasons:M,sample_paths:Z}}export function scoreNetworkEvidence(q){return q.networkEntries.map(scoreEndpointEvidence).filter((M)=>M.verdict!=="noise"||M.real_data_score>0).sort((M,Q)=>Q.real_data_score-M.real_data_score).slice(0,8)}export function classifyPattern(q){const M=q.networkEntries.filter((X)=>/json/i.test(X.contentType)),V=scoreNetworkEvidence(q).filter((X)=>X.verdict==="likely_data"||X.verdict==="maybe_data").length,Z=q.networkEntries.filter((X)=>X.status===401||X.status===403).length,$=q.initialState.__INITIAL_STATE__||q.initialState.__NUXT__||q.initialState.__NEXT_DATA__||q.initialState.__APOLLO_STATE__;if(Z>=2&&M.length>=1)return{pattern:"D",reason:`${Z} auth-failing API responses seen — endpoint is token-gated`,json_responses:M.length,real_data_candidates:V,auth_failures:Z};if($)return{pattern:"B",reason:`SSR state global present: ${Object.entries(q.initialState).filter(([,O])=>O).map(([O])=>O).join(", ")}`,json_responses:M.length,real_data_candidates:V,auth_failures:Z};if(V>=1)return{pattern:"A",reason:`${V} captured response${V===1?"":"s"} look like real data — inspect api_candidates before choosing a strategy`,json_responses:M.length,real_data_candidates:V,auth_failures:Z};return{pattern:"C",reason:M.length>0?`${M.length} JSON response${M.length===1?"":"s"} observed, but none look like target data — likely telemetry/side-channel; treat as HTML/DOM until an endpoint validates`:"No JSON XHR and no SSR state — HTML scrape (Pattern C); escalate to E manually if WebSocket traffic appears",json_responses:M.length,real_data_candidates:V,auth_failures:Z}}export function findNearestAdapter(q,M){let Q;try{Q=new URL(q).hostname}catch{return null}const V=Q.replace(/^www\./,""),Z=V.split("."),$=Z.slice(-2).join("."),X=Z.length>1?Z[Z.length-2]:V,O=new Map;for(const z of M.values()){const W=z.domain?.toLowerCase();if(W&&(V.endsWith(W)||W.endsWith($))||z.site.toLowerCase()===X?.toLowerCase()||V.includes(z.site.toLowerCase())){const J=O.get(z.site)??[];J.push(z);O.set(z.site,J)}}if(O.size===0)return null;let L=null;for(const z of O)if(!L||z[1].length>L[1].length)L=z;if(!L)return null;return{site:L[0],example_commands:L[1].slice(0,5).map((z)=>`${z.site} ${z.name}`),reason:`${L[1].length} existing adapter${L[1].length===1?"":"s"} target this site — reuse strategy/cookie config`}}export function analyzeSite(q,M){const Q=classifyPattern(q),V=detectAntiBot(q),Z=scoreNetworkEvidence(q),$=findNearestAdapter(q.finalUrl,M);let X;if(V.detected)X=V.implication;else if(Q.pattern==="A")X="Inspect `api_candidates`, then replay the best endpoint and record the status/content-type/sample shape in your strategy note; do not choose API strategy from XHR count alone.";else if(Q.pattern==="B")X='Read the SSR global via `ppcli browser eval "JSON.stringify(window.__INITIAL_STATE__ ?? window.__NUXT__ ?? window.__NEXT_DATA__ ?? window.__APOLLO_STATE__)"` — no API needed.';else if(Q.pattern==="C")X="No API visible — use SSR HTML scrape (e.g. `ppcli browser extract`) against the rendered page.";else if(Q.pattern==="D")X="Endpoints need auth. Re-open the page from a signed-in session, then retry analyze; see `field-decode-playbook` §4 for token tracing.";else if(Q.pattern==="E")X="WebSocket stream detected — find the underlying HTTP poll/long-poll endpoint; raw WS is not supported.";else X="No strong signal. Manually inspect `ppcli browser network --all` and pick a pattern.";return{requested_url:q.requestedUrl,final_url:q.finalUrl,title:q.title,pattern:Q,anti_bot:V,initial_state:q.initialState,api_candidates:Z,nearest_adapter:$,recommended_next_step:X}}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Readability runs in the page's own window because it needs real DOM APIs
|
|
24
24
|
* (getComputedStyle, treeWalker). Running it Node-side would require jsdom —
|
|
25
|
-
* a heavy dep the rest of
|
|
25
|
+
* a heavy dep the rest of ppcli doesn't need.
|
|
26
26
|
*/
|
|
27
27
|
export interface ExtractArticleOptions {
|
|
28
28
|
/** CSS selectors removed from the cloned document before Readability runs. */
|
|
@@ -132,7 +132,7 @@ ${G}
|
|
|
132
132
|
}
|
|
133
133
|
return { ok: false, reason: 'not_checkable', tag, role };
|
|
134
134
|
})()
|
|
135
|
-
`)}async setChecked(G,H,W={}){const Q=await Y(this,G,W),Z=await this.readCheckableState();if(Z?.ok!==!0)throw new q({code:"not_checkable",message:`Target "${G}" is not a checkbox, radio, switch, or aria-checked control.`,hint:"Use `
|
|
135
|
+
`)}async setChecked(G,H,W={}){const Q=await Y(this,G,W),Z=await this.readCheckableState();if(Z?.ok!==!0)throw new q({code:"not_checkable",message:`Target "${G}" is not a checkbox, radio, switch, or aria-checked control.`,hint:"Use `ppcli browser state` or `browser find` to pick an input[type=checkbox], input[type=radio], or role=checkbox/switch target."});if(Z.disabled)throw new q({code:"not_checkable",message:`Target "${G}" is disabled and cannot be ${H?"checked":"unchecked"}.`,hint:"Pick an enabled control, or inspect the form state before retrying."});if((Z.kind==="radio"||Z.kind==="menuitemradio")&&!H)throw new q({code:"not_checkable",message:`Target "${G}" is a radio button and cannot be unchecked directly.`,hint:"Select another radio option in the same group instead."});if(Z.checked===H)return{...Q,checked:H,changed:!1,...Z.kind?{kind:Z.kind}:{}};const _=await this.click(G,W),$=await this.readCheckableState();if($?.ok!==!0||$.checked!==H)throw new q({code:"not_checkable",message:`Target "${G}" did not become ${H?"checked":"unchecked"} after click.`,hint:"The control may be custom, disabled by app logic, or require a different target such as its visible label."});return{matches_n:_.matches_n,match_level:_.match_level,checked:H,changed:!0,...$.kind?{kind:$.kind}:{}}}async setFileInputBySelector(G,H){const W=this.setFileInput;if(typeof W==="function"){await W.call(this,G,H);return}const Q=this.cdp;if(typeof Q!=="function")throw Error("File upload requires setFileInput or CDP support from the active browser backend.");await Q.call(this,"DOM.enable",{}).catch(()=>{return});const _=(await Q.call(this,"DOM.getDocument",{}))?.root?.nodeId;if(typeof _!=="number")throw Error("DOM.getDocument returned no root node.");const D=(await Q.call(this,"DOM.querySelector",{nodeId:_,selector:H}))?.nodeId;if(typeof D!=="number"||D<=0)throw Error(`No element found matching selector: ${H}`);await Q.call(this,"DOM.setFileInputFiles",{files:G,nodeId:D})}async uploadFiles(G,H,W={}){if(!Array.isArray(H)||H.length===0)throw new q({code:"not_file_input",message:"No files were provided for upload.",hint:"Pass one or more local file paths after the target."});const Q=await Y(this,G,W),Z="data-opencli-upload-target",_=`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,$=`[${Z}="${_}"]`;let D=!1,B=null;try{B=await this.evaluateWithArgs(`
|
|
136
136
|
(() => {
|
|
137
137
|
const el = window.__resolved;
|
|
138
138
|
if (!el || el.nodeType !== 1) return { ok: false, reason: 'not_file_input' };
|
|
@@ -146,7 +146,7 @@ ${G}
|
|
|
146
146
|
accept: el.getAttribute('accept') || '',
|
|
147
147
|
};
|
|
148
148
|
})()
|
|
149
|
-
`,{markerAttr:Z,markerValue:_});D=B?.ok===!0;if(!D)throw new q({code:"not_file_input",message:`Target "${G}" is not an input[type=file].`,hint:'Use `
|
|
149
|
+
`,{markerAttr:Z,markerValue:_});D=B?.ok===!0;if(!D)throw new q({code:"not_file_input",message:`Target "${G}" is not an input[type=file].`,hint:'Use `ppcli browser find --css "input[type=file]"` or inspect `compound` output from browser state/find.'});if(H.length>1&&!B?.multiple)throw new q({code:"not_file_input",message:`Target "${G}" does not allow multiple files, but ${H.length} files were provided.`,hint:"Pass one file, or choose a file input with the multiple attribute."});await this.setFileInputBySelector(H,$);const X=await this.evaluate(`
|
|
150
150
|
(() => {
|
|
151
151
|
const el = window.__resolved;
|
|
152
152
|
const names = [];
|
|
@@ -195,7 +195,7 @@ ${G}
|
|
|
195
195
|
};
|
|
196
196
|
return { source: measure(sourceEl), target: measure(targetEl) };
|
|
197
197
|
})()
|
|
198
|
-
`);if(B?.source?.visible!==!0)throw Error(`Drag source "${G}" is not visible at drag time.`);if(B?.target?.visible!==!0)throw Error(`Drag target "${H}" has no visible bounding box.`);if(!await this.tryNativeDrag({x:B.source.x,y:B.source.y},{x:B.target.x,y:B.target.y}))throw Error("Native drag requires CDP Input.dispatchMouseEvent support.");return{dragged:!0,source:G,target:H,source_matches_n:Q.matches_n,target_matches_n:$.matches_n,source_match_level:Q.match_level,target_match_level:$.match_level}}finally{await this.evaluate("delete window.__opencli_drag_source").catch(()=>{})}}async fillText(G,H,W={}){const Q=await Y(this,G,W);let Z=!1,_=!1;try{Z=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded");_=await this.tryCdpOnResolvedElement("DOM.focus")}catch{}if((await this.evaluate(z({skipScroll:Z,skipFocus:_})))?.ok!==!0)throw new q({code:"not_editable",message:`Target "${G}" is not a fillable input, textarea, or contenteditable element.`,hint:"Use `
|
|
198
|
+
`);if(B?.source?.visible!==!0)throw Error(`Drag source "${G}" is not visible at drag time.`);if(B?.target?.visible!==!0)throw Error(`Drag target "${H}" has no visible bounding box.`);if(!await this.tryNativeDrag({x:B.source.x,y:B.source.y},{x:B.target.x,y:B.target.y}))throw Error("Native drag requires CDP Input.dispatchMouseEvent support.");return{dragged:!0,source:G,target:H,source_matches_n:Q.matches_n,target_matches_n:$.matches_n,source_match_level:Q.match_level,target_match_level:$.match_level}}finally{await this.evaluate("delete window.__opencli_drag_source").catch(()=>{})}}async fillText(G,H,W={}){const Q=await Y(this,G,W);let Z=!1,_=!1;try{Z=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded");_=await this.tryCdpOnResolvedElement("DOM.focus")}catch{}if((await this.evaluate(z({skipScroll:Z,skipFocus:_})))?.ok!==!0)throw new q({code:"not_editable",message:`Target "${G}" is not a fillable input, textarea, or contenteditable element.`,hint:"Use `ppcli browser state` to pick an editable target, or use `browser type` for keyboard-like interactions."});const D=await this.tryNativeType(H);if(!D)await this.evaluate(V(H));let B=await this.evaluate(U(H));if(D&&B?.ok!==!0){await this.evaluate(V(H));B=await this.evaluate(U(H))}const X=B&&"actual"in B?B.actual:"",L=B&&"mode"in B?B.mode:void 0;return{...Q,filled:!0,verified:B?.ok===!0,expected:H,actual:X,length:X.length,...L?{mode:L}:{}}}async pressKey(G){const H=f(G);if(!await this.tryNativeKeyPress(H.key,H.modifiers))await this.evaluate(I(H.key,H.modifiers))}async scrollTo(G,H={}){const W=await Y(this,G,H),Q=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded"),Z=await this.evaluate(y({skipScroll:Q}));if(Z&&typeof Z==="object")return{...Z,matches_n:W.matches_n,match_level:W.match_level};return{matches_n:W.matches_n,match_level:W.match_level}}async getFormState(){return await this.evaluate(E())}async scroll(G="down",H=500){await this.evaluate(T(G,H))}async autoScroll(G){const H=G?.times??3,W=G?.delayMs??2000;await this.evaluate(h(H,W))}async networkRequests(G=!1){const H=await this.evaluate(R(G));return Array.isArray(H)?H:[]}async consoleMessages(G="info"){return[]}async wait(G){if(typeof G==="number"){if(G>=1)try{const H=G*1000;await this.evaluate(g(H,Math.min(500,H)));return}catch{}await new Promise((H)=>setTimeout(H,G*1000));return}if(typeof G.time==="number"){await new Promise((H)=>setTimeout(H,G.time*1000));return}if(G.selector){const H=(G.timeout??10)*1000;await this.evaluate(J(G.selector,H));return}if(G.text){const H=(G.timeout??30)*1000;await this.evaluate(S(G.text,H))}}async snapshot(G={}){if(G.source==="ax"){const W=this.cdp;if(typeof W!=="function")throw new M("BROWSER_AX_UNAVAILABLE","AX snapshot requires CDP support from the active browser backend.","Use the default DOM state, or update/reload the Browser Bridge extension.");const Q=await this.collectAxSnapshotTrees(W),Z=F(Q,{maxDepth:G.maxDepth,interactiveOnly:G.interactive});this._axRefs=Z.refs;return Z.text}this._axRefs.clear();const H=N({viewportExpand:G.viewportExpand??2000,maxDepth:Math.max(1,Math.min(Number(G.maxDepth)||50,200)),interactiveOnly:G.interactive??!1,maxTextLength:G.maxTextLength??120,includeScrollInfo:!0,bboxDedup:!0,previousHashes:this._prevSnapshotHashes});try{const W=await this.evaluate(H);try{const Q=await this.evaluate("window.__opencli_prev_hashes");this._prevSnapshotHashes=typeof Q==="string"?Q:null}catch{}return W}catch(W){if(process.env.DEBUG_SNAPSHOT)process.stderr.write(`[snapshot] DOM snapshot failed, falling back to accessibility tree: ${W?.message?.slice(0,200)}
|
|
199
199
|
`);return this._basicSnapshot(G)}}async collectAxSnapshotTrees(G){await G.call(this,"Accessibility.enable",{});const W=[{tree:await G.call(this,"Accessibility.getFullAXTree",{})}],Q=await G.call(this,"Page.getFrameTree",{}).catch(()=>null),Z=c(Q);for(const _ of Z){if(_.sessionId)await G.call(this,"Accessibility.enable",C(_)).catch(()=>null);const $=await G.call(this,"Accessibility.getFullAXTree",j(_)).catch(()=>null);if($)W.push({tree:$,frame:_})}return W}async getCurrentUrl(){if(this._lastUrl)return this._lastUrl;try{const G=await this.evaluate("window.location.href");if(typeof G==="string"&&G){this._lastUrl=G;return G}}catch{}return null}async installInterceptor(G){const{generateInterceptorJs:H}=await import("../interceptor.js");await this.evaluate(H(JSON.stringify(G),{arrayName:"__opencli_xhr",patchGuard:"__opencli_interceptor_patched"}))}async getInterceptedRequests(){const{generateReadInterceptedJs:G}=await import("../interceptor.js"),H=await this.evaluate(G("__opencli_xhr"));return Array.isArray(H)?H:[]}async waitForCapture(G=10){const H=G*1000;await this.evaluate(b(H))}async _basicSnapshot(G={}){const W=`
|
|
200
200
|
(async () => {
|
|
201
201
|
function buildTree(node, depth) {
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(L)=>{if(L?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.proxyAuth){this.on("Fetch.requestPaused",(L)=>{const Y=L;if(Y.requestId)this.send("Fetch.continueRequest",{requestId:Y.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(L)=>{const Y=L;if(!Y.requestId)return;this.send("Fetch.continueWithAuth",{requestId:Y.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(L){Z.close();X(L instanceof Error?L:Error(String(L)));return}K(new O(this))});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `opencli browser dialog accept`;托管 profile 可 `opencli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|
|
1
|
+
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(L)=>{if(L?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.proxyAuth){this.on("Fetch.requestPaused",(L)=>{const Y=L;if(Y.requestId)this.send("Fetch.continueRequest",{requestId:Y.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(L)=>{const Y=L;if(!Y.requestId)return;this.send("Fetch.continueWithAuth",{requestId:Y.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(L){Z.close();X(L instanceof Error?L:Error(String(L)));return}K(new O(this))});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import{spawn as C}from"node:child_process";import{fileURLToPath as F}from"node:url";import*as M from"node:fs";import*as K from"node:path";import{DEFAULT_DAEMON_PORT}from"../constants.js";import{BrowserConnectError as J}from"../errors.js";import{PKG_VERSION as N}from"../version.js";import{waitForBridgeReady as b}from"./bridge-readiness.js";import{fetchDaemonStatus as B,getDaemonHealth as g,requestDaemonShutdown as O}from"./daemon-transport.js";export function resolveDaemonLaunchSpec(){const z=K.dirname(F(import.meta.url)),Q=K.resolve(z,".."),W=K.join(Q,"daemon.ts"),Z=K.join(Q,"daemon.js"),Y=M.existsSync(W),X=Y?W:Z,$=!!process.versions?.bun,U=Y&&!$;return{binary:process.execPath,args:U?["--import","tsx/esm",X]:[X],scriptPath:X}}export function spawnDaemonProcess(){const z=resolveDaemonLaunchSpec(),Q=C(z.binary,z.args,{detached:!0,stdio:"ignore",env:{...process.env}});Q.unref();return Q}export async function waitForDaemonStop(z){const Q=Date.now()+z;while(Date.now()<Q){await x(200);if((await g()).state==="stopped")return!0}return!1}export async function waitForDaemonStatus(z){const Q=Date.now()+z;while(Date.now()<Q){const W=await B({timeout:Math.min(1000,Math.max(100,Q-Date.now()))});if(W)return W;await x(200)}return null}export const daemonLifecycleHooks={requestDaemonShutdown:O,spawnDaemonProcess,waitForDaemonStop};export async function restartDaemon(z={}){const Q=await B();let W=Q===null;if(Q){W=await O()&&await waitForDaemonStop(z.stopTimeoutMs??3000);if(!W)return{previousStatus:Q,status:Q,stopped:!1,spawned:!1}}spawnDaemonProcess();const Z=await waitForDaemonStatus(z.startTimeoutMs??5000);return{previousStatus:Q,status:Z,stopped:W,spawned:!0}}export async function ensureBrowserBridgeReady(z={}){const W=(z.timeoutSeconds&&z.timeoutSeconds>0?z.timeoutSeconds:10)*1000,Z=z.verbose??!0,Y=z.contextId,X=await g({contextId:Y}),$=X.status?.daemonVersion,U=!!X.status&&(!$||$!==N);let q=!1,k=null;if(U){const y=$?`v${$} ≠ v${N}`:`pre-version daemon, CLI is v${N}`;if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⚠️ Stale daemon detected (${y}). Restarting...
|
|
2
2
|
`);let j=await daemonLifecycleHooks.requestDaemonShutdown()&&await daemonLifecycleHooks.waitForDaemonStop(3000);if(!j){const G=X.status?.pid;if(typeof G==="number"&&Number.isInteger(G)&&G>0){try{process.kill(G,"SIGKILL")}catch{}j=await daemonLifecycleHooks.waitForDaemonStop(2000)}}if(!j)throw new J("Stale daemon could not be replaced",`A stale daemon (${y}) is running but did not shut down (graceful + SIGKILL both failed).
|
|
3
|
-
Run manually:
|
|
3
|
+
Run manually: ppcli daemon stop`,"daemon-not-running");q=!0}if(!q&&X.state==="ready")return{health:X,spawnedProcess:k};if(!q&&X.state==="profile-required")throw V(X,Y);if(q||X.state==="stopped"){if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⏳ Starting daemon...
|
|
4
4
|
`);k=daemonLifecycleHooks.spawnDaemonProcess()}else if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY)){process.stderr.write(`⏳ Waiting for Chrome/Chromium extension to connect...
|
|
5
|
-
`);process.stderr.write(` Make sure Chrome or Chromium is open and the
|
|
6
|
-
`)}const A=await b(g,{timeoutMs:W,contextId:Y});if(A.state==="ready")return{health:A,spawnedProcess:k};throw V(A,Y)}function V(z,Q){if(z.state==="profile-required")return new J("Multiple Browser Bridge profiles are connected",`Select one with --profile <name>, OPENCLI_PROFILE=<name>, or
|
|
7
|
-
Run
|
|
5
|
+
`);process.stderr.write(` Make sure Chrome or Chromium is open and the ppcli extension is enabled.
|
|
6
|
+
`)}const A=await b(g,{timeoutMs:W,contextId:Y});if(A.state==="ready")return{health:A,spawnedProcess:k};throw V(A,Y)}function V(z,Q){if(z.state==="profile-required")return new J("Multiple Browser Bridge profiles are connected",`Select one with --profile <name>, OPENCLI_PROFILE=<name>, or ppcli profile use <name>.
|
|
7
|
+
Run ppcli profile list to see connected profiles.`,"profile-required");if(z.state==="profile-disconnected"){const W=Q??z.status.contextId??"unknown";return new J(`Browser profile "${W}" is not connected`,"Open the matching Chrome profile and make sure the ppcli extension is enabled, or choose another profile with ppcli profile use <name>.","profile-disconnected")}if(z.state==="no-extension")return new J("Browser Bridge extension not connected",`Make sure Chrome/Chromium is open and the ppcli extension is enabled.
|
|
8
8
|
If not installed:
|
|
9
|
-
1. Download: https://github.com/jackwener/
|
|
10
|
-
`+" 2. Open chrome://extensions → Developer Mode → Load unpacked","extension-not-connected");return new J("Failed to start
|
|
9
|
+
1. Download: https://github.com/jackwener/OpenCLI/releases
|
|
10
|
+
`+" 2. Open chrome://extensions → Developer Mode → Load unpacked","extension-not-connected");return new J("Failed to start ppcli daemon",`Try running manually:
|
|
11
11
|
node ${resolveDaemonLaunchSpec().scriptPath}
|
|
12
12
|
Make sure port ${DEFAULT_DAEMON_PORT} is available.`,"daemon-not-running")}function x(z){return new Promise((Q)=>setTimeout(Q,z))}export{DEFAULT_DAEMON_PORT};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export function isDaemonStale(e,n){if(!e||!n)return!1;return!e.daemonVersion||e.daemonVersion!==n}export function formatDaemonVersion(e){return e?.daemonVersion?`v${e.daemonVersion}`:"version unknown"}export function staleDaemonIssue(e,n){return`Stale daemon detected: daemon ${formatDaemonVersion(e)} != CLI v${n}.
|
|
2
|
-
Run:
|
|
2
|
+
Run: ppcli daemon restart`}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DOM Snapshot Engine — Advanced DOM pruning for LLM consumption.
|
|
3
3
|
*
|
|
4
|
-
* Inspired by browser-use's multi-layer pruning pipeline, adapted for
|
|
4
|
+
* Inspired by browser-use's multi-layer pruning pipeline, adapted for ppcli's
|
|
5
5
|
* Chrome Extension + CDP architecture. Runs entirely in-page via Runtime.evaluate.
|
|
6
6
|
*
|
|
7
7
|
* Pipeline:
|
|
@@ -735,7 +735,7 @@ import{COMPOUND_INFO_JS as U}from"./compound.js";export function scrollToRefJs(q
|
|
|
735
735
|
if (!doc || !doc.body) {
|
|
736
736
|
const attrs = serializeAttrs(el);
|
|
737
737
|
const frameLabel = '[F' + crossOriginIndex + ']';
|
|
738
|
-
lines.push(indent + '|iframe|' + frameLabel + '<iframe' + (attrs ? ' ' + attrs : '') + ' /> (cross-origin, use:
|
|
738
|
+
lines.push(indent + '|iframe|' + frameLabel + '<iframe' + (attrs ? ' ' + attrs : '') + ' /> (cross-origin, use: ppcli browser frames + browser eval --frame <index>)');
|
|
739
739
|
crossOriginIndex++;
|
|
740
740
|
return false;
|
|
741
741
|
}
|
|
@@ -750,7 +750,7 @@ import{COMPOUND_INFO_JS as U}from"./compound.js";export function scrollToRefJs(q
|
|
|
750
750
|
} catch {
|
|
751
751
|
const attrs = serializeAttrs(el);
|
|
752
752
|
const frameLabel = '[F' + crossOriginIndex + ']';
|
|
753
|
-
lines.push(indent + '|iframe|' + frameLabel + '<iframe' + (attrs ? ' ' + attrs : '') + ' /> (blocked, use:
|
|
753
|
+
lines.push(indent + '|iframe|' + frameLabel + '<iframe' + (attrs ? ' ' + attrs : '') + ' /> (blocked, use: ppcli browser frames + browser eval --frame <index>)');
|
|
754
754
|
crossOriginIndex++;
|
|
755
755
|
return false;
|
|
756
756
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{BrowserConnectError as z}from"../errors.js";import{DEFAULT_DAEMON_PORT as J}from"../constants.js";const K=["Extension disconnected","Extension not connected","attach failed","Detached while handling command","Debugger is not attached to the tab","no longer exists","No tab with id","CDP connection","Daemon command failed","No window with id"],Q=["Inspected target navigated or closed"];function W(q){return q instanceof Error?q.message:String(q)}export function classifyBrowserError(q){const x=W(q);if(K.some((H)=>x.includes(H)))return{kind:"extension-transient",retryable:!0,delayMs:1500};if(Q.some((H)=>x.includes(H)))return{kind:"target-navigation",retryable:!0,delayMs:200};if(x.includes("-32000")&&/target|context/i.test(x))return{kind:"target-navigation",retryable:!0,delayMs:200};return{kind:"non-retryable",retryable:!1,delayMs:0}}export function isTransientBrowserError(q){return classifyBrowserError(q).retryable}export function formatBrowserConnectError(q,x){switch(q){case"daemon-not-running":return new z("Cannot connect to
|
|
1
|
+
import{BrowserConnectError as z}from"../errors.js";import{DEFAULT_DAEMON_PORT as J}from"../constants.js";const K=["Extension disconnected","Extension not connected","attach failed","Detached while handling command","Debugger is not attached to the tab","no longer exists","No tab with id","CDP connection","Daemon command failed","No window with id"],Q=["Inspected target navigated or closed"];function W(q){return q instanceof Error?q.message:String(q)}export function classifyBrowserError(q){const x=W(q);if(K.some((H)=>x.includes(H)))return{kind:"extension-transient",retryable:!0,delayMs:1500};if(Q.some((H)=>x.includes(H)))return{kind:"target-navigation",retryable:!0,delayMs:200};if(x.includes("-32000")&&/target|context/i.test(x))return{kind:"target-navigation",retryable:!0,delayMs:200};return{kind:"non-retryable",retryable:!1,delayMs:0}}export function isTransientBrowserError(q){return classifyBrowserError(q).retryable}export function formatBrowserConnectError(q,x){switch(q){case"daemon-not-running":return new z("Cannot connect to ppcli daemon."+(x?`
|
|
2
2
|
|
|
3
|
-
${x}`:""),`Run \`
|
|
3
|
+
${x}`:""),`Run \`ppcli doctor\` to diagnose, or \`ppcli daemon restart\` to force a fresh daemon. Default port is ${J}.`,q);case"extension-not-connected":return new z("Browser Bridge extension is not connected."+(x?`
|
|
4
4
|
|
|
5
5
|
${x}`:""),"Install the extension from GitHub Releases, then reload.",q);case"command-failed":return new z(`Browser command failed: ${x??"unknown error"}`,void 0,q);default:return new z(x??"Failed to connect to browser",void 0,q)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as z from"node:fs";import*as V from"node:os";import*as O from"node:path";export const DEFAULT_TTL_MS=86400000;function W(){return process.env.OPENCLI_CACHE_DIR||O.join(V.homedir(),".
|
|
1
|
+
import*as z from"node:fs";import*as V from"node:os";import*as O from"node:path";export const DEFAULT_TTL_MS=86400000;function W(){return process.env.OPENCLI_CACHE_DIR||O.join(V.homedir(),".ppcli","cache")}export function getCachePath(B,q=W()){const G=B.replace(/[^a-zA-Z0-9_-]+/g,"_");return O.join(q,"browser-network",`${G}.json`)}export function saveNetworkCache(B,q,G){const J=getCachePath(B,G);z.mkdirSync(O.dirname(J),{recursive:!0});const I={version:1,session:B,savedAt:new Date().toISOString(),entries:q};let H;try{H=z.openSync(J,z.constants.O_WRONLY|z.constants.O_CREAT|z.constants.O_TRUNC,384);z.fchmodSync(H,384);z.writeFileSync(H,JSON.stringify(I),"utf8")}finally{if(H!==void 0)z.closeSync(H)}}export function loadNetworkCache(B,q={}){const G=getCachePath(B,q.baseDir);let J;try{J=z.readFileSync(G,"utf-8")}catch{return{status:"missing"}}let I;try{const K=JSON.parse(J);if(!K||K.version!==1||!Array.isArray(K.entries))return{status:"corrupt"};I=K}catch{return{status:"corrupt"}}const H=q.ttlMs??DEFAULT_TTL_MS,R=q.now??Date.now(),Q=Date.parse(I.savedAt);if(!Number.isFinite(Q))return{status:"corrupt"};const N=R-Q;if(N>H)return{status:"expired",file:I,ageMs:N};return{status:"ok",file:I,ageMs:N}}export function findEntry(B,q){return B.entries.find((G)=>G.key===q)??null}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as Q from"node:fs";import*as S from"node:os";import*as O from"node:path";export const DEFAULT_CONTEXT_ID="default";function U(){const F=process.env.OPENCLI_CONFIG_DIR||O.join(S.homedir(),".
|
|
1
|
+
import*as Q from"node:fs";import*as S from"node:os";import*as O from"node:path";export const DEFAULT_CONTEXT_ID="default";function U(){const F=process.env.OPENCLI_CONFIG_DIR||O.join(S.homedir(),".ppcli");return O.join(F,"browser-profiles.json")}export function managedProfilesBaseDir(){const F=process.env.OPENCLI_CONFIG_DIR||O.join(S.homedir(),".ppcli");return O.join(F,"chrome-profiles")}export function normalizeContextId(F){return F?.trim()||void 0}export function emptyProfileConfig(){return{version:1,aliases:{}}}function V(F){return typeof F==="object"&&F!==null&&typeof F.userDataDir==="string"&&F.userDataDir.trim().length>0&&typeof F.port==="number"&&Number.isInteger(F.port)}export function loadProfileConfig(){try{const F=Q.readFileSync(U(),"utf-8"),j=JSON.parse(F),B=j.aliases&&typeof j.aliases==="object"?Object.fromEntries(Object.entries(j.aliases).filter((J)=>{const[K,L]=J;return typeof K==="string"&&K.trim().length>0&&typeof L==="string"&&L.trim().length>0})):{},H=j.managed&&typeof j.managed==="object"?Object.fromEntries(Object.entries(j.managed).filter((J)=>{const[K,L]=J;return typeof K==="string"&&K.trim().length>0&&V(L)})):void 0;return{version:1,aliases:B,...typeof j.defaultContextId==="string"&&j.defaultContextId.trim()?{defaultContextId:j.defaultContextId.trim()}:{},...H&&Object.keys(H).length>0?{managed:H}:{}}}catch{return emptyProfileConfig()}}export function saveProfileConfig(F){const j=U();Q.mkdirSync(O.dirname(j),{recursive:!0});Q.writeFileSync(j,JSON.stringify(F,null,2)+`
|
|
2
2
|
`,"utf-8")}function W(F){const j=loadProfileConfig();return normalizeContextId(F)??normalizeContextId(process.env.OPENCLI_PROFILE)??normalizeContextId(j.defaultContextId)}export function resolveProfile(F){const j=loadProfileConfig(),B=W(F);if(!B)return{kind:"extension"};const H=j.managed?.[B];if(H)return{kind:"managed",name:B,entry:H};return{kind:"extension",contextId:j.aliases[B]??B}}export function resolveProfileContextId(F){const j=resolveProfile(F);if(j.kind==="managed")return;return j.contextId}export function getManagedProfile(F){return loadProfileConfig().managed?.[normalizeContextId(F)??""]}export function listManagedProfiles(){const F=loadProfileConfig().managed??{};return Object.entries(F).map(([j,B])=>({name:j,entry:B})).sort((j,B)=>j.name.localeCompare(B.name))}const X=/^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;export function addManagedProfile(F,j){const B=normalizeContextId(F);if(!B)throw Error("profile 名不能为空");if(!X.test(B))throw Error("profile 名只能包含字母/数字/下划线/连字符,且以字母或数字开头(最长 32 字符)");const H=loadProfileConfig();if(H.managed?.[B])throw Error(`托管 profile "${B}" 已存在`);if(H.aliases[B])throw Error(`"${B}" 已被扩展 profile 别名占用,换个名字`);H.managed={...H.managed,[B]:j};saveProfileConfig(H);return H}export function updateManagedProfile(F,j){const B=normalizeContextId(F);if(!B)throw Error("profile 名不能为空");const H=loadProfileConfig();if(!H.managed?.[B])throw Error(`托管 profile "${B}" 不存在`);H.managed={...H.managed,[B]:j};saveProfileConfig(H);return H}export function removeManagedProfileEntry(F){const j=normalizeContextId(F),B=loadProfileConfig(),H=j?B.managed?.[j]:void 0;if(!j||!H||!B.managed)throw Error(`托管 profile "${F}" 不存在`);delete B.managed[j];if(Object.keys(B.managed).length===0)delete B.managed;if(B.defaultContextId===j)delete B.defaultContextId;saveProfileConfig(B);return H}export function aliasForContextId(F,j){for(const[B,H]of Object.entries(F.aliases))if(H===j)return B;return}export function renameProfile(F,j){const B=normalizeContextId(F),H=normalizeContextId(j);if(!B)throw Error("profile contextId is required");if(!H)throw Error("profile alias is required");const J=loadProfileConfig();if(J.managed?.[H])throw Error(`"${H}" 已被托管 profile 占用,换个别名`);for(const[K,L]of Object.entries(J.aliases))if(K!==H&&L===B)delete J.aliases[K];J.aliases[H]=B;saveProfileConfig(J);return J}export function setDefaultProfile(F){const j=normalizeContextId(F);if(!j)throw Error("profile is required");const B=loadProfileConfig();B.defaultContextId=B.managed?.[j]?j:B.aliases[j]??j;saveProfileConfig(B);return B}
|
|
@@ -111,7 +111,7 @@ export function resolveTargetJs(j,g={}){const q=JSON.stringify(j),w=g.nth!==void
|
|
|
111
111
|
ok: false,
|
|
112
112
|
code: 'not_found',
|
|
113
113
|
message: 'ref=' + ref + ' not found in DOM',
|
|
114
|
-
hint: 'The element may have been removed. Re-run \`
|
|
114
|
+
hint: 'The element may have been removed. Re-run \`ppcli browser state\` to get a fresh snapshot.',
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -148,7 +148,7 @@ export function resolveTargetJs(j,g={}){const q=JSON.stringify(j),w=g.nth!==void
|
|
|
148
148
|
code: 'stale_ref',
|
|
149
149
|
message: 'ref=' + ref + ' was <' + fp.tag + '>' + (fp.text ? '"' + fp.text + '"' : '')
|
|
150
150
|
+ ' but now points to <' + liveFp.tag + '>' + (liveFp.text ? '"' + liveFp.text.slice(0, 30) + '"' : ''),
|
|
151
|
-
hint: 'The page has changed since the last snapshot. Re-run \`
|
|
151
|
+
hint: 'The page has changed since the last snapshot. Re-run \`ppcli browser state\` to refresh.',
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -171,7 +171,7 @@ export function resolveTargetJs(j,g={}){const q=JSON.stringify(j),w=g.nth!==void
|
|
|
171
171
|
ok: false,
|
|
172
172
|
code: 'selector_not_found',
|
|
173
173
|
message: 'CSS selector "' + ref + '" matched 0 elements',
|
|
174
|
-
hint: 'The element may not exist or may be hidden. Re-run \`
|
|
174
|
+
hint: 'The element may not exist or may be hidden. Re-run \`ppcli browser state\` to check, or use \`ppcli browser find --css\` to explore candidates.',
|
|
175
175
|
matches_n: 0,
|
|
176
176
|
};
|
|
177
177
|
}
|
|
@@ -204,7 +204,7 @@ export function resolveTargetJs(j,g={}){const q=JSON.stringify(j),w=g.nth!==void
|
|
|
204
204
|
ok: false,
|
|
205
205
|
code: 'selector_ambiguous',
|
|
206
206
|
message: 'CSS selector "' + ref + '" matched ' + matches.length + ' elements',
|
|
207
|
-
hint: 'Pass --nth <n> (0-based) to pick one, or use a more specific selector. Use \`
|
|
207
|
+
hint: 'Pass --nth <n> (0-based) to pick one, or use a more specific selector. Use \`ppcli browser find --css\` to list all candidates.',
|
|
208
208
|
candidates: candidates,
|
|
209
209
|
matches_n: matches.length,
|
|
210
210
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as O from"node:fs";import*as b from"node:os";import*as M from"node:path";const I=12,K=1,S=[/^id$/i,/_id$/i,/Id$/,/^short_id$/i,/^bvid$/i,/^aid$/i,/^tid$/i,/^asin$/i,/^sku$/i,/^isbn$/i,/^doi$/i,/^slug$/i,/^hn_id$/i,/^username$/i,/^handle$/i,/^uri$/i];export function fixturePath(z,G){return M.join(b.homedir(),".
|
|
1
|
+
import*as O from"node:fs";import*as b from"node:os";import*as M from"node:path";const I=12,K=1,S=[/^id$/i,/_id$/i,/Id$/,/^short_id$/i,/^bvid$/i,/^aid$/i,/^tid$/i,/^asin$/i,/^sku$/i,/^isbn$/i,/^doi$/i,/^slug$/i,/^hn_id$/i,/^username$/i,/^handle$/i,/^uri$/i];export function fixturePath(z,G){return M.join(b.homedir(),".ppcli","sites",z,"verify",`${G}.json`)}export function loadFixture(z,G){const q=fixturePath(z,G);if(!O.existsSync(q))return null;try{const B=O.readFileSync(q,"utf-8");return JSON.parse(B)}catch(B){throw Error(`Failed to parse fixture ${q}: ${B instanceof Error?B.message:String(B)}`)}}export function writeFixture(z,G,q){const B=fixturePath(z,G);O.mkdirSync(M.dirname(B),{recursive:!0});O.writeFileSync(B,`${JSON.stringify(q,null,2)}
|
|
2
2
|
`,"utf-8");return B}export function deriveFixture(z,G){const q={};if(z.length===0){q.rowCount={min:0};return{...G?{args:G}:{},expect:q}}q.rowCount={min:1};const B=z[0],Z=Object.keys(B);q.columns=Z;const R={};for(const V of Z){const F=new Set;for(const H of z){const Q=H[V];F.add(j(Q))}R[V]=[...F].sort().join("|")}q.types=R;return{...G?{args:G}:{},expect:q}}export function validateRows(z,G){const q=[],B=G.expect;if(!B)return q;if(B.rowCount){const{min:Q,max:W}=B.rowCount;if(typeof Q==="number"&&z.length<Q)q.push({rule:"rowCount",detail:`got ${z.length} rows, expected at least ${Q}`});if(typeof W==="number"&&z.length>W)q.push({rule:"rowCount",detail:`got ${z.length} rows, expected at most ${W}`})}const Z=B.columns??[],R=B.types??{},V=B.patterns??{},F=B.notEmpty??[],H={};for(const[Q,W]of Object.entries(V))try{H[Q]=new RegExp(W)}catch(J){q.push({rule:"pattern",detail:`pattern for "${Q}" invalid: ${J instanceof Error?J.message:String(J)}`})}z.forEach((Q,W)=>{for(const J of Z)if(!(J in Q))q.push({rule:"column",detail:`missing column "${J}"`,rowIndex:W});for(const[J,$]of Object.entries(R)){if(!(J in Q))continue;const C=j(Q[J]);if(!A(C,$))q.push({rule:"type",detail:`"${J}" is ${C}, expected ${$}`,rowIndex:W})}for(const[J,$]of Object.entries(H)){if(!(J in Q))continue;const C=Q[J];if(C===null||C===void 0)continue;if(!$.test(String(C)))q.push({rule:"pattern",detail:`"${J}"=${JSON.stringify(String(C).slice(0,60))} does not match /${$.source}/`,rowIndex:W})}for(const J of F){const $=Q[J];if($===null||$===void 0||String($).trim()==="")q.push({rule:"notEmpty",detail:`"${J}" is empty`,rowIndex:W})}for(const[J,$]of Object.entries(B.mustNotContain??{})){if(!(J in Q))continue;const C=Q[J];if(C===null||C===void 0)continue;const N=String(C);for(const Y of $)if(N.includes(Y))q.push({rule:"mustNotContain",detail:`"${J}" contains forbidden substring ${JSON.stringify(Y)}`,rowIndex:W})}for(const J of B.mustBeTruthy??[]){if(!(J in Q))continue;if(!Q[J])q.push({rule:"mustBeTruthy",detail:`"${J}" is falsy (${JSON.stringify(Q[J])}) — likely silent fallback`,rowIndex:W})}});return q}export function validateRowShape(z,G={}){const q=[],B=G.maxTopLevelKeys??I,Z=G.maxNestedDepth??K;z.forEach((R,V)=>{const F=Object.keys(R);if(F.length>B)q.push({rule:"shapeKeyCount",detail:`row has ${F.length} top-level keys, expected at most ${B}`,rowIndex:V});for(const[H,Q]of Object.entries(R)){const W=U(Q);if(W>Z)q.push({rule:"shapeDepth",detail:`"${H}" nesting depth is ${W}, expected at most ${Z}`,rowIndex:V});for(const J of X(Q,H))q.push({rule:"shapeNestedId",detail:`id-shaped field "${J}" must be a top-level row key`,rowIndex:V})}});return q}export function expandFixtureArgs(z){if(!z)return[];if(Array.isArray(z))return z.map((q)=>String(q));const G=[];for(const[q,B]of Object.entries(z))G.push(`--${q}`,String(B));return G}export function parseSeedArgs(z){if(z===void 0)return;const G=z.trim();if(!G)return;try{const q=JSON.parse(G);if(Array.isArray(q))return q;if(q!==null&&typeof q==="object")return q;return[q]}catch{return[z]}}function j(z){if(z===null)return"null";if(Array.isArray(z))return"array";return typeof z}function U(z){if(z===null||z===void 0||typeof z!=="object")return 0;if(Array.isArray(z)){if(z.length===0)return 1;return 1+Math.max(...z.map(U))}const G=Object.values(z);if(G.length===0)return 1;return 1+Math.max(...G.map(U))}function X(z,G){if(z===null||z===void 0||typeof z!=="object")return[];const q=[];if(Array.isArray(z)){z.forEach((B,Z)=>{q.push(...X(B,`${G}[${Z}]`))});return q}for(const[B,Z]of Object.entries(z)){const R=`${G}.${B}`;if(L(B))q.push(R);q.push(...X(Z,R))}return q}function L(z){return S.some((G)=>G.test(z))}function A(z,G){const q=G.split("|").map((B)=>B.trim()).filter(Boolean);if(q.length===0)return!0;if(q.includes("any"))return!0;return q.includes(z)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{openSync as q,closeSync as H,writeSync as b,readFileSync as j,unlinkSync as Y,statSync as v,mkdirSync as D,utimesSync as Q}from"node:fs";import{join as N}from"node:path";import{homedir as F}from"node:os";const V=N(F(),".
|
|
1
|
+
import{openSync as q,closeSync as H,writeSync as b,readFileSync as j,unlinkSync as Y,statSync as v,mkdirSync as D,utimesSync as Q}from"node:fs";import{join as N}from"node:path";import{homedir as F}from"node:os";const V=N(F(),".ppcli"),Z=N(V,"browser-adapter-session.lock");function m(z){if(!z)return Z;const x=z.replace(/[^A-Za-z0-9_-]/g,"_");return N(V,`browser-adapter-session.${x}.lock`)}const W=180000,$=600000,u=60;function w(z){return new Promise((x)=>setTimeout(x,z))}function K(z){try{process.kill(z,0);return!0}catch(x){return x?.code==="EPERM"}}function U(z){try{const x=parseInt(j(z,"utf8").trim(),10);return Number.isFinite(x)?x:null}catch{return null}}function g(z){try{const x=q(z,"wx");try{b(x,String(process.pid))}finally{H(x)}return!0}catch(x){if(x?.code==="EEXIST")return!1;throw x}}function C(z){try{const x=U(z),G=Date.now()-v(z).mtimeMs;if(x!==null&&!K(x)||G>$)Y(z)}catch{}}export async function withBrowserSessionLock(z,x){try{D(V,{recursive:!0})}catch{}const G=m(x),B=Date.now()+W;while(!g(G)){C(G);if(g(G))break;if(Date.now()>=B)throw Error(`browser session lock busy: waited ${Math.round(W/1000)}s for another browser command to finish`);await w(u)}const J=setInterval(()=>{try{const X=new Date;Q(G,X,X)}catch{}},60000);if(typeof J.unref==="function")J.unref();try{return await z()}finally{clearInterval(J);try{Y(G)}catch{}}}export async function withBrowserSessionLockIf(z,x,G){return z?withBrowserSessionLock(x,G):x()}export const __lockInternals={LOCK_PATH:Z,ACQUIRE_TIMEOUT_MS:W,STALE_AFTER_MS:$};
|
|
@@ -70,7 +70,7 @@ export declare function serializeManifest(manifest: ManifestEntry[]): string;
|
|
|
70
70
|
* Metadata audit: every positional arg must carry a non-empty `help` string.
|
|
71
71
|
*
|
|
72
72
|
* Why this is a hard gate (not advisory):
|
|
73
|
-
* - `
|
|
73
|
+
* - `ppcli twitter followers --help` rendered `Arguments:\n user ` with
|
|
74
74
|
* an empty trailing column. Agents and humans both saw a blank field —
|
|
75
75
|
* impossible to recover the parameter's purpose without reading source.
|
|
76
76
|
* - This is metadata completeness, not stylistic taste; failing closed is
|
|
@@ -8,7 +8,7 @@ Manifest NOT written. Likely cause: stale dist/ or a broken adapter import.
|
|
|
8
8
|
Always run via tsx (\`npm run build-manifest\`), not against compiled dist/.
|
|
9
9
|
`);process.exit(1)}const X=findManifestMetadataIssues(J);if(X.length>0){process.stderr.write(`❌ ${X.length} positional arg(s) missing \`help\` text:
|
|
10
10
|
`);for(const Q of X){const Y=Q.sourceFile?` (${Q.sourceFile})`:"";process.stderr.write(` - ${Q.site}/${Q.command} positional "${Q.arg}"${Y}
|
|
11
|
-
`)}process.stderr.write("\nEvery positional arg must declare a non-empty `help` string so\n`
|
|
11
|
+
`)}process.stderr.write("\nEvery positional arg must declare a non-empty `help` string so\n`ppcli <site> <cmd> --help` shows callers what the parameter is for.\nAdd `help: '...'` to each arg above and re-run the build.\n");process.exit(1)}const G=v(H);if(G){const Q=diffRemovedEntries(G,J);if(Q.length>q.allowRemovals){process.stderr.write(`❌ ${Q.length} manifest entries would be removed; refusing to overwrite.
|
|
12
12
|
`);const Y=Q.slice(0,20);for(const Z of Y)process.stderr.write(` - ${Z}
|
|
13
13
|
`);if(Q.length>Y.length)process.stderr.write(` ... ${Q.length-Y.length} more
|
|
14
14
|
`);process.stderr.write(`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* argv preprocessing: rewrite `
|
|
3
|
-
* into `
|
|
2
|
+
* argv preprocessing: rewrite `ppcli browser <session> <subcommand> ...`
|
|
3
|
+
* into `ppcli browser --session <session> <subcommand> ...` so commander
|
|
4
4
|
* (which can't combine a parent positional with subcommand dispatch) can parse it.
|
|
5
5
|
*
|
|
6
6
|
* The user-facing form is positional; the internal form uses --session. Help text
|
|
@@ -17,7 +17,7 @@ export declare function getBrowserSubcommandNames(): ReadonlySet<string>;
|
|
|
17
17
|
*
|
|
18
18
|
* Only acts when `browser` is the root command (i.e. the first non-flag token
|
|
19
19
|
* after any leading root options), so it can't mis-interpret occurrences of
|
|
20
|
-
* the literal word `browser` deeper in the argv (e.g. `
|
|
20
|
+
* the literal word `browser` deeper in the argv (e.g. `ppcli adapter init
|
|
21
21
|
* browser/x`, or a URL value containing `browser`).
|
|
22
22
|
*
|
|
23
23
|
* Leaves argv unchanged when:
|
|
@@ -52,7 +52,7 @@ export interface DashPositionalManifestEntry {
|
|
|
52
52
|
browser?: boolean;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* `
|
|
55
|
+
* `ppcli boss detail -abc123def` fails because commander parses
|
|
56
56
|
* `-abc123def` as an unknown option rather than the required
|
|
57
57
|
* `<security-id>` positional. BOSS 直聘 securityId tokens are opaque
|
|
58
58
|
* strings that can legitimately start with `-` (issue #1160), and the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const C=new Set(["analyze","back","bind","check","click","close","console","dblclick","dialog","drag","eval","extract","fill","find","focus","frames","get","help","hover","init","keys","network","open","screenshot","scroll","select","state","tab","type","unbind","uncheck","upload","verify","wait"]),W=new Set(["--profile"]);export function getBrowserSubcommandNames(){return C}export function rewriteBrowserArgv(X){const H=[...X];let z=0;while(z<H.length){const Z=H[z];if(!Z.startsWith("-"))break;if(Z.includes("=")){z+=1;continue}if(W.has(Z)&&z+1<H.length)z+=2;else z+=1}if(H[z]!=="browser")return H;const J=z+1,Q=H[J];if(Q===void 0)return H;if(Q==="--session"||Q==="--session="||Q.startsWith("--session="))throw new BrowserSessionArgvError("The `--session` flag is no longer a public option. Use the positional form:
|
|
1
|
+
const C=new Set(["analyze","back","bind","check","click","close","console","dblclick","dialog","drag","eval","extract","fill","find","focus","frames","get","help","hover","init","keys","network","open","screenshot","scroll","select","state","tab","type","unbind","uncheck","upload","verify","wait"]),W=new Set(["--profile"]);export function getBrowserSubcommandNames(){return C}export function rewriteBrowserArgv(X){const H=[...X];let z=0;while(z<H.length){const Z=H[z];if(!Z.startsWith("-"))break;if(Z.includes("=")){z+=1;continue}if(W.has(Z)&&z+1<H.length)z+=2;else z+=1}if(H[z]!=="browser")return H;const J=z+1,Q=H[J];if(Q===void 0)return H;if(Q==="--session"||Q==="--session="||Q.startsWith("--session="))throw new BrowserSessionArgvError("The `--session` flag is no longer a public option. Use the positional form: ppcli browser <session> <command>");if(Q.startsWith("-"))return H;if(C.has(Q))return H;H.splice(J,1,"--session",Q);f(H,J+2);return H}function f(X,H){const z=X.findIndex((J,Q)=>Q>=H&&C.has(J));if(z===-1)return;for(let J=z+1;J<X.length;J+=1){const Q=X[J];if(Q==="--")return;if(Q.startsWith("--window=")){const Z=X.splice(J,1);X.splice(z,0,...Z);return}if(Q==="--window"){const Z=X[J+1];if(Z===void 0||Z==="--")return;const G=X.splice(J,2);X.splice(z,0,...G);return}}}export class BrowserSessionArgvError extends Error{constructor(X){super(X);this.name="BrowserSessionArgvError"}}function q(X){const H=new Map([["-h","none"],["--help","none"],["-v","none"],["--verbose","none"],["-f","required"],["--format","required"],["--trace","required"]]);if(X.browser){H.set("--window","required");H.set("--site-session","required");H.set("--keep-tab","required")}for(const z of X.args??[]){if(z.positional)continue;H.set(`--${z.name}`,z.required||z.valueRequired?"required":"optional")}return H}function U(X,H,z){const J=X[H];if(!J||J==="--")return null;const Q=J.indexOf("="),Z=Q===-1?J:J.slice(0,Q),G=z.get(Z);if(!G&&Q===-1&&J.startsWith("-")&&!J.startsWith("--")&&J.length>2){const V=J.slice(0,2);if(z.get(V)==="required")return{values:[J],nextIndex:H+1}}if(!G)return null;if(Q!==-1||G==="none")return{values:[J],nextIndex:H+1};const T=X[H+1];if(G==="required")return T===void 0?{values:[J],nextIndex:H+1}:{values:[J,T],nextIndex:H+2};if(T!==void 0&&!T.startsWith("-"))return{values:[J,T],nextIndex:H+2};return{values:[J],nextIndex:H+1}}export function escapeLeadingDashPositional(X,H){const z=[...X],J=new Map;for(const Y of H)if(Y.args?.find((y)=>y.positional)?.required)J.set(Y.site+"/"+Y.name,Y);let Q=0;while(Q<z.length){const Y=z[Q];if(!Y.startsWith("-"))break;if(Y.includes("=")){Q+=1;continue}if(W.has(Y)&&Q+1<z.length)Q+=2;else Q+=1}const Z=z[Q],G=z[Q+1],T=Q+2;if(!Z||!G||T>=z.length)return z;const V=J.get(Z+"/"+G);if(!V)return z;const K=q(V),F=[];let $=T;while($<z.length){const Y=z[$];if(Y==="--")return z;const D=U(z,$,K);if(D){F.push(...D.values);$=D.nextIndex;continue}if(!Y.startsWith("-"))return z;if(Y.startsWith("--"))return z;break}if($>=z.length)return z;const N=z[$],L=[],P=[];$+=1;while($<z.length){const Y=U(z,$,K);if(Y){L.push(...Y.values);$=Y.nextIndex;continue}P.push(z[$]);$+=1}return[...z.slice(0,T),...F,...L,"--",N,...P]}
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function selectFreshByTimestamp<T extends {
|
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* Check whether the site-memory scaffolding exists under
|
|
18
|
-
* ~/.
|
|
18
|
+
* ~/.ppcli/sites/<site>/. Agents have a strong tendency to forget to write
|
|
19
19
|
* endpoints.json / notes.md after a successful verify, which dooms the next
|
|
20
20
|
* agent to redo recon from scratch. Surfacing the current state as part of
|
|
21
21
|
* verify's final report converts that "silent skip" into a visible nudge;
|