publishport-opencli 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -61
- package/README.zh-CN.md +68 -68
- package/cli-manifest.json +954 -61
- 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/quora/__fixtures__/dom.html +39 -0
- package/clis/quora/answer-comments.js +9 -0
- package/clis/quora/answer-detail.js +1 -0
- package/clis/quora/answer-queue.js +1 -0
- package/clis/quora/comment.js +23 -0
- package/clis/quora/create-answer.js +4 -4
- package/clis/quora/create-question.js +3 -3
- package/clis/quora/follow.js +15 -0
- package/clis/quora/followers.js +1 -0
- package/clis/quora/question.js +1 -0
- package/clis/quora/search.js +1 -0
- package/clis/quora/shared.js +26 -11
- package/clis/quora/stats.js +1 -0
- package/clis/quora/upvote.js +14 -0
- package/clis/quora/user-answers.js +1 -0
- package/clis/quora/user.js +1 -0
- 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/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;
|