publishport-opencli 1.0.21 → 1.0.23
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/clis/boss/utils.js +14 -8
- package/clis/xiaohongshu/publish.js +38 -22
- package/dist/src/browser/base-page.d.ts +14 -1
- package/dist/src/browser/base-page.js +1 -1
- package/dist/src/browser/cdp.js +1 -1
- package/dist/src/browser/dom-helpers.d.ts +24 -0
- package/dist/src/browser/dom-helpers.js +10 -10
- package/dist/src/browser/page.d.ts +1 -0
- package/dist/src/browser/page.js +4 -4
- package/dist/src/cli.js +1 -1
- package/dist/src/commands/daemon.d.ts +3 -1
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/types.d.ts +2 -1
- package/package.json +1 -1
package/clis/boss/utils.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import{ArgumentError as f,AuthRequiredError as p,CommandExecutionError as u,EmptyResultError as m}from"@jackwener/opencli/errors";const d="www.zhipin.com",w=`https://${d}/web/chat/index`,
|
|
1
|
+
import{ArgumentError as f,AuthRequiredError as p,CommandExecutionError as u,EmptyResultError as m}from"@jackwener/opencli/errors";const d="www.zhipin.com",w=`https://${d}/web/chat/index`,g=new Set([7,37]),S="Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。",b="该命令仅支持招聘端(BOSS 端)账号,请使用招聘者账号登录后重试。",I=15000;export function requirePage(e){if(!e)throw new u("Browser page required")}export function readPositiveInteger(e,t,n,o){const r=e===void 0||e===null||e===""?n:Number(e);if(!Number.isInteger(r)||r<1)throw new f(`boss ${t} must be a positive integer`);if(o!==void 0&&r>o)throw new f(`boss ${t} must be <= ${o}`);return r}export function readRequiredString(e,t){const n=String(e??"").trim();if(!n)throw new f(`boss ${t} cannot be empty`);return n}export async function navigateToChat(e,t=2){await e.goto(w);await e.wait({time:t})}export async function navigateTo(e,t,n=1){await e.goto(t);await e.wait({time:n})}export function checkAuth(e){if(g.has(e.code))throw new p(d,S)}function k(e){if(e.code===IDENTITY_MISMATCH_CODE)throw new p(d,b)}export function assertOk(e,t){if(!e||typeof e!=="object")throw new u(`${t?`${t}: `:""}Boss API returned malformed response`);if(e.code===0)return;checkAuth(e);k(e);const n=t?`${t}: `:"";throw new u(`${n}${e.message||"Unknown error"} (code=${e.code})`)}const y="__ppXhrError";function _(e,t){const n=`status=${e.status??"?"} responseURL=${e.responseURL||"(空)"} page=${e.page||"(未知)"}`;if(e.kind==="timeout")return new u(`Boss API request timed out: ${t} [${n}]`);if(e.kind==="parse")return new u(`Boss API returned non-JSON: ${t} [${n}] body=${e.body||"(空)"}`);const o=e.responseURL||"",r=o&&!o.startsWith(`https://${d}/`)?`请求被重定向到 ${o},跨域后浏览器拿不到响应。`:"请求没有拿到任何响应:跨域被拦、连接被断开或网络不通,不足以断定是平台风控。";return new u(`Boss API request failed: ${t} [${n}] ${r}`)}export async function bossFetch(e,t,n={}){const o=n.method??"GET",r=n.timeout??I,s=n.body??null,i=`
|
|
2
2
|
async () => {
|
|
3
|
-
return new Promise((resolve
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
4
|
const xhr = new XMLHttpRequest();
|
|
5
5
|
xhr.open(${JSON.stringify(o)}, ${JSON.stringify(t)}, true);
|
|
6
6
|
xhr.withCredentials = true;
|
|
7
7
|
xhr.timeout = ${r};
|
|
8
8
|
xhr.setRequestHeader('Accept', 'application/json');
|
|
9
9
|
${o==="POST"?"xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');":""}
|
|
10
|
+
const fail = (kind, extra) => resolve({ ${y}: Object.assign({
|
|
11
|
+
kind,
|
|
12
|
+
status: xhr.status,
|
|
13
|
+
responseURL: xhr.responseURL || '',
|
|
14
|
+
page: location.href,
|
|
15
|
+
}, extra || {}) });
|
|
10
16
|
xhr.onload = () => {
|
|
11
17
|
try { resolve(JSON.parse(xhr.responseText)); }
|
|
12
|
-
catch(e) {
|
|
18
|
+
catch(e) { fail('parse', { body: String(xhr.responseText || '').substring(0, 200) }); }
|
|
13
19
|
};
|
|
14
|
-
xhr.onerror = () =>
|
|
15
|
-
xhr.ontimeout = () =>
|
|
20
|
+
xhr.onerror = () => fail('network');
|
|
21
|
+
xhr.ontimeout = () => fail('timeout');
|
|
16
22
|
xhr.send(${s?JSON.stringify(s):"null"});
|
|
17
23
|
});
|
|
18
24
|
}
|
|
19
|
-
`;let
|
|
25
|
+
`;let a;try{a=await e.evaluate(i)}catch(c){if(c instanceof p||c instanceof u)throw c;const l=c instanceof Error?c.message:String(c);throw new u(`Boss API request failed: ${l}`)}if(a&&typeof a==="object"&&a[y])throw _(a[y],t);if(!a||typeof a!=="object")throw new u("Boss API returned malformed response");if(!n.allowNonZero&&a.code!==0)assertOk(a);return a}export async function fetchFriendList(e,t={}){const n=t.pageNum??1,o=t.jobId??"0",r=`https://${d}/wapi/zprelation/friend/getBossFriendListV2.json?page=${n}&status=0&jobId=${o}`,s=await bossFetch(e,r,{allowNonZero:t.allowNonZero});if(t.allowNonZero&&s.code!==0)return s;const i=s.zpData?.friendList;if(!Array.isArray(i))throw new u("Boss friend list response did not include zpData.friendList");return i}export async function fetchRecommendList(e){const t=`https://${d}/wapi/zprelation/friend/greetRecSortList`,o=(await bossFetch(e,t)).zpData?.friendList;if(!Array.isArray(o))throw new u("Boss recommend response did not include zpData.friendList");return o}export async function findFriendByUid(e,t,n={}){const o=n.maxPages??1,r=n.checkGreetList??!1;for(let s=1;s<=o;s++){const i=await fetchFriendList(e,{pageNum:s,allowNonZero:n.allowNonZero});if(n.allowNonZero&&!Array.isArray(i))return{friend:null,code:i.code};const a=Array.isArray(i)?i:[],c=a.find((l)=>l.encryptUid===t);if(c)return n.allowNonZero?{friend:c,code:0}:c;if(a.length===0)break}if(r){const i=(await fetchRecommendList(e)).find((a)=>a.encryptUid===t);if(i)return n.allowNonZero?{friend:i,code:0}:i}return n.allowNonZero?{friend:null,code:0}:null}export async function clickCandidateInList(e,t){const n=String(t).replace(/[^0-9]/g,"");return(await e.evaluate(`
|
|
20
26
|
async () => {
|
|
21
27
|
const uid = ${JSON.stringify(n)};
|
|
22
28
|
const item = document.querySelector('#_' + uid + '-0') || document.querySelector('[id^="_' + uid + '"]');
|
|
@@ -72,7 +78,7 @@ import{ArgumentError as f,AuthRequiredError as p,CommandExecutionError as u,Empt
|
|
|
72
78
|
}
|
|
73
79
|
return { clicked: false };
|
|
74
80
|
}
|
|
75
|
-
`)).clicked)await e.pressKey("Enter");return!0}export function verbose(e){if(process.env.OPENCLI_VERBOSE)console.error(`[ppcli:boss] ${e}`)}export const IDENTITY_MISMATCH_CODE=24;const
|
|
81
|
+
`)).clicked)await e.pressKey("Enter");return!0}export function verbose(e){if(process.env.OPENCLI_VERBOSE)console.error(`[ppcli:boss] ${e}`)}export const IDENTITY_MISMATCH_CODE=24;const h=`https://${d}/web/geek/chat`;export async function navigateToGeekChat(e,t=2){await e.goto(h);await e.wait({time:t})}export async function readEncryptSystemId(e){return await e.evaluate(`
|
|
76
82
|
(() => {
|
|
77
83
|
// 1. Vue app state / Pinia / $route.query
|
|
78
84
|
// The chat component reads encryptSystemId from the app runtime to build
|
|
@@ -158,4 +164,4 @@ import{ArgumentError as f,AuthRequiredError as p,CommandExecutionError as u,Empt
|
|
|
158
164
|
} catch (_) {}
|
|
159
165
|
return '';
|
|
160
166
|
})()
|
|
161
|
-
`)||""}export async function fetchGeekFriendLabelList(e,t={}){const n=t.labelId??0,o=t.encryptSystemId??"",r=`https://${d}/wapi/zprelation/friend/geekFilterByLabel?labelId=${n}&encryptSystemId=${encodeURIComponent(o)}`,s=await bossFetch(e,r,{allowNonZero:t.allowNonZero});if(t.allowNonZero&&s.code!==0)return s;const i=s.zpData?.friendList;if(!Array.isArray(i))throw new u("Boss geek chat list response did not include zpData.friendList");return i}export async function fetchGeekFriendInfoList(e,t=[]){if(!t.length)return[];const n=50,o=[];for(let r=0;r<t.length;r+=n){const i=`friendIds=${t.slice(r,r+n).map(String).join(",")}`,
|
|
167
|
+
`)||""}export async function fetchGeekFriendLabelList(e,t={}){const n=t.labelId??0,o=t.encryptSystemId??"",r=`https://${d}/wapi/zprelation/friend/geekFilterByLabel?labelId=${n}&encryptSystemId=${encodeURIComponent(o)}`,s=await bossFetch(e,r,{allowNonZero:t.allowNonZero});if(t.allowNonZero&&s.code!==0)return s;const i=s.zpData?.friendList;if(!Array.isArray(i))throw new u("Boss geek chat list response did not include zpData.friendList");return i}export async function fetchGeekFriendInfoList(e,t=[]){if(!t.length)return[];const n=50,o=[];for(let r=0;r<t.length;r+=n){const i=`friendIds=${t.slice(r,r+n).map(String).join(",")}`,c=(await bossFetch(e,`https://${d}/wapi/zprelation/friend/getGeekFriendList.json`,{method:"POST",body:i})).zpData?.result;if(!Array.isArray(c))throw new u("Boss geek friend enrichment response did not include zpData.result");o.push(...c)}return o}export async function findGeekFriendByUid(e,t,n={}){const r=(await fetchGeekFriendLabelList(e,{encryptSystemId:n.encryptSystemId})).find((i)=>i.encryptFriendId===t||String(i.uid)===String(t)||String(i.friendId)===String(t));if(!r)return null;const s=await fetchGeekFriendInfoList(e,[r.friendId]);return{...r,...s[0]||{}}}export async function fetchGeekHistoryMsg(e,t,n={}){const o=n.page??1,r=t.uid,s=encodeURIComponent(t.securityId||""),i=`https://${d}/wapi/zpchat/geek/historyMsg?bossId=${r}&securityId=${s}&page=${o}&c=20&src=0`,a=await bossFetch(e,i),c=a.zpData?.messages??a.zpData?.historyMsgList;if(!Array.isArray(c))throw new u("Boss geek history response did not include a message list");if(c.length===0)throw new m("boss chatmsg","Boss returned no messages for this chat.");return c}const x=`https://${d}/web/geek/job`;export async function navigateToGeekJob(e,t=2){await e.goto(x);await e.wait({time:t})}export async function geekGet(e,t,n={},o={}){const r=new URLSearchParams;for(const[c,l]of Object.entries(n)){if(l===void 0||l===null||l==="")continue;r.set(c,String(l))}const s=r.toString(),i=`https://${d}${t}${s?`?${s}`:""}`,a=await bossFetch(e,i,o);if(o.allowNonZero&&a.code!==0)return a;return a.zpData??{}}export async function geekPost(e,t,n={},o={}){const r=new URLSearchParams;for(const[i,a]of Object.entries(n)){if(a===void 0||a===null)continue;r.set(i,String(a))}const s=await bossFetch(e,`https://${d}${t}`,{...o,method:"POST",body:r.toString()});if(o.allowNonZero&&s.code!==0)return s;return s.zpData??{}}export async function fetchGeekUserInfo(e){return geekGet(e,"/wapi/zpuser/wap/getUserInfo.json")}export async function fetchGeekBossData(e,t){const n=await geekGet(e,"/wapi/zpchat/geek/getBossData",{bossId:t});return n?.data??n??{}}export async function fetchGeekLastMsgs(e,t=[]){if(!t.length)return[];const n=await geekGet(e,"/wapi/zpchat/geek/userLastMsg",{friendIds:t.map(String).join(",")});return Array.isArray(n)?n:n?.result??[]}export async function fetchGeekAttachmentResumes(e){return(await geekGet(e,"/wapi/zpgeek/resume/attachment/checkbox.json"))?.resumeList??[]}export async function resolveGeekResumeId(e,t){const n=await fetchGeekAttachmentResumes(e);if(t){if(!n.find((s)=>String(s.resumeId)===String(t))){const s=n.map((i)=>i.resumeId||"?").join(", ")||"(空)";throw new f(`指定的 --resume-id=${t} 不在账号简历列表 [${s}] 中`)}return String(t)}if(n.length===0)throw new m("boss resumelist","账号还没有绑定附件简历,请先用 boss resumeupload 上传,或在 BOSS 直聘 App 里上传。");if(n.length===1){const r=n[0].resumeId;if(!r)throw new u("唯一附件简历缺失 resumeId 字段");return String(r)}const o=n.map((r)=>`${r.resumeId}(${r.showName||"未命名"})`).join(", ");throw new f(`账号绑定了 ${n.length} 份附件简历,请用 --resume-id 指定其中一份:${o}`)}export async function resolveGeekBoss(e,t){const n=String(t??"").trim();if(!n)throw new f("boss id cannot be empty");if(/^\d+$/.test(n))return{id:n,friend:null};const o=await findGeekFriendByUid(e,n);if(!o)throw new m("boss chatlist",`未在沟通列表中找到 ${n},请先用 boss chatlist --side geek 查 uid。`);const r=o.uid??o.friendId;if(!r)throw new u("沟通列表条目缺失 uid / friendId");return{id:String(r),friend:o}}export async function resolveGeekBossId(e,t){return(await resolveGeekBoss(e,t)).id}export async function geekAddFriend(e,t,n=""){return geekGet(e,"/wapi/zpgeek/friend/add.json",{securityId:t,lid:n})}export async function geekOpenChat(e,t,n=3){await e.goto(`${h}?id=${encodeURIComponent(String(t))}`);await e.wait({time:n})}export const CITY_CODES={"全国":"100010000","北京":"101010100","上海":"101020100","广州":"101280100","深圳":"101280600","杭州":"101210100","成都":"101270100","南京":"101190100","武汉":"101200100","西安":"101110100","苏州":"101190400","长沙":"101250100","天津":"101030100","重庆":"101040100","郑州":"101180100","东莞":"101281600","青岛":"101120200","合肥":"101220100","佛山":"101280800","宁波":"101210400","厦门":"101230200","大连":"101070200","珠海":"101280700","无锡":"101190200","济南":"101120100","福州":"101230100","昆明":"101290100","哈尔滨":"101050100","沈阳":"101070100","石家庄":"101090100","贵阳":"101260100","南宁":"101300100","太原":"101100100","海口":"101310100","兰州":"101160100","乌鲁木齐":"101130100","长春":"101060100","南昌":"101240100","常州":"101191100","温州":"101210700","嘉兴":"101210300","徐州":"101190800","香港":"101320100"};export async function isRecruiterSide(e){await navigateToChat(e);const t=await bossFetch(e,`https://${d}/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0`,{allowNonZero:!0});if(t.code===0)return!0;if(t.code===IDENTITY_MISMATCH_CODE)return!1;assertOk(t,"boss 身份探测");return!1}
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionError as u,ArgumentError as
|
|
1
|
+
import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionError as u,ArgumentError as b}from"@jackwener/opencli/errors";import{cli as mt,Strategy as pt}from"@jackwener/opencli/registry";import{assertLiteralContent as wt}from"../_shared/content-guard.js";import{setInputFiles as bt}from"../_shared/file-inject.js";import{humanType as L,humanWait as v,humanMoveTo as gt,elementViewportCenter as xt}from"./human.js";const yt="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",V=18,z=20,K=3000,G="|||",S="基础",X=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],Zt=X.map(([t])=>t),B="文字配图",H="再写一张",Y="生成图片",W="下一步",J=".tiptap.ProseMirror",_t=["_onPublish","onPublish","_onSubmit","_handlePublish"],vt=["_onSave","_onSaveDraft","_onDraft"],F=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],Q=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],St={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function h(t){if(t&&typeof t==="object"&&typeof t.session==="string"&&Object.prototype.hasOwnProperty.call(t,"data"))return t.data;return t}function Tt(t){return t.map((e)=>{const n=M.resolve(e);if(!D.existsSync(n))throw new b(`Image file not found: ${n}`);const o=M.extname(n).toLowerCase();if(!St[o])throw new b(`Unsupported image format "${o}". Supported: jpg, png, gif, webp`);return n})}const Z='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function kt(t,e=15000){const n=500,o=Math.max(1,Math.ceil(e/n));for(let r=0;r<o;r++){if(await t.evaluate(`
|
|
2
2
|
(() => {
|
|
3
3
|
const sels = ${JSON.stringify(Z)};
|
|
4
4
|
return !!document.querySelector(sels);
|
|
5
5
|
})()
|
|
6
|
-
`))return!0;if(r<o-1)await t.wait({time:n/1000})}return!1}async function tt(t,e){if(!await kt(t))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};try{return{ok:!0,count:(await
|
|
6
|
+
`))return!0;if(r<o-1)await t.wait({time:n/1000})}return!1}async function tt(t,e){if(!await kt(t))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};try{return{ok:!0,count:(await bt(t,Z,e)).count??e.length}}catch(o){return{ok:!1,count:0,error:o instanceof Error?o.message:String(o)}}}async function et(t,e=30000){const n=2000,o=Math.ceil(e/n);for(let r=0;r<o;r++){if(!await t.evaluate(`
|
|
7
7
|
() => !!document.querySelector(
|
|
8
8
|
'[class*="upload"][class*="progress"], [class*="uploading"], [class*="loading"][class*="image"]'
|
|
9
9
|
)
|
|
10
|
-
`))return;await t.wait({time:n/1000})}}export function describeTextMismatch(t,e){const n=String(t??""),o=String(e??""),r=(m)=>m.split(/\n\s*\n/).filter((
|
|
10
|
+
`))return;await t.wait({time:n/1000})}}export function describeTextMismatch(t,e){const n=String(t??""),o=String(e??""),r=(m)=>m.split(/\n\s*\n/).filter((w)=>w.trim()).length;let i=0;while(i<n.length&&i<o.length&&n[i]===o[i])i++;const a=(m)=>JSON.stringify(m.slice(Math.max(0,i-15),i+25)),s=`内容校验不一致:期望 ${n.length} 字符 / ${r(n)} 段,实际 ${o.length} 字符 / ${r(o)} 段。`,d=i>=n.length&&i>=o.length?"":` 首个差异在第 ${i+1} 个字符:期望 ${a(n)},实际 ${a(o)}。`,c=r(n)>1&&r(o)<=1?" 段落数塌成 1 段,通常是编辑器把换行吃掉了。":"";return s+d+c}async function Et(t,e,n){const o=()=>t.evaluate(`
|
|
11
|
+
((sel) => {
|
|
12
|
+
const el = Array.from(document.querySelectorAll(sel)).find(n => n && n.offsetParent !== null);
|
|
13
|
+
if (!el) return -1;
|
|
14
|
+
// PM 的段落是编辑器根下的块级子节点;数它们比数 innerText 的换行可靠
|
|
15
|
+
// (innerText 会被 CSS 影响,而且空段落根本不产生换行符)。
|
|
16
|
+
return el.querySelectorAll('p, div[data-node-type], li').length || el.childElementCount;
|
|
17
|
+
})(${JSON.stringify(e)})
|
|
18
|
+
`),r=await o();let i="unavailable";if(typeof t.pressKey==="function"){i=await t.pressKey("Enter")||"native";await t.wait({time:0.15})}let a=await o();if(a>r)return;await t.evaluate(`
|
|
19
|
+
((sel) => {
|
|
20
|
+
const el = Array.from(document.querySelectorAll(sel)).find(n => n && n.offsetParent !== null);
|
|
21
|
+
if (!el) return false;
|
|
22
|
+
el.focus();
|
|
23
|
+
return document.execCommand('insertParagraph', false);
|
|
24
|
+
})(${JSON.stringify(e)})
|
|
25
|
+
`);await t.wait({time:0.2});a=await o();if(a>r)return;throw new u(`正文第 ${n} 处换行没能插入段落分隔(编辑器块数仍为 ${r},`+`按键通道=${i},execCommand 兜底也没生效)。继续打字会把整篇正文塞进一段,因此中止——`+`没换行的笔记发出去只能删了重发。
|
|
26
|
+
`+"请勿直接重试同一条命令,先看编辑器当前状态:最可能是有话题/表情联想浮层开着,"+"此时 Enter 被浮层当成「选中候选」吃掉。正文里如果带 # 号尤其容易触发。")}async function nt(t,e,n,o){const r=await t.evaluate(`
|
|
11
27
|
(function(selectors) {
|
|
12
28
|
const __opencli_xhs_fill_phase = "locate";
|
|
13
29
|
for (const sel of selectors) {
|
|
@@ -101,8 +117,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
101
117
|
return { ok: true };
|
|
102
118
|
})(${JSON.stringify(r.sel)})
|
|
103
119
|
`))?.ok){await t.screenshot({path:`/tmp/xhs_publish_${o}_debug.png`});throw Error(`Could not prepare ${o} input. Debug screenshot: /tmp/xhs_publish_${o}_debug.png`)}await t.wait({time:0.35});try{const d=String(n).split(`
|
|
104
|
-
`);for(let c=0;c<d.length;c++){if(c>0)
|
|
105
|
-
`);if(d[c])await L(t,d[c])}a=await t.evaluate(`
|
|
120
|
+
`);for(let c=0;c<d.length;c++){if(c>0)await Et(t,r.sel,c);if(d[c])await L(t,d[c])}a=await t.evaluate(`
|
|
106
121
|
((selector, expectedText) => {
|
|
107
122
|
const __opencli_xhs_fill_phase = "verify";
|
|
108
123
|
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
@@ -125,7 +140,8 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
125
140
|
const actual = normalize(el.innerText || el.textContent || '');
|
|
126
141
|
return { ok: actual === normalize(expectedText), actual };
|
|
127
142
|
})(${JSON.stringify(r.sel)}, ${JSON.stringify(n)})
|
|
128
|
-
`)}catch
|
|
143
|
+
`)}catch(d){if(d instanceof u)throw d;if(String(n).includes(`
|
|
144
|
+
`))throw new u(`写入${o}失败:${String(d?.message||d)}。`+"正文含换行,不能回退到单段写入方式(那会把多段正文塞进一段),因此中止。"+"⚠️ 编辑器可能已自动保存草稿:请勿直接重试,先用 `ppcli xiaohongshu drafts` 查看。");a=await i()}}else a=await i();if(!a?.ok){await t.screenshot({path:`/tmp/xhs_publish_${o}_debug.png`});const s=typeof a?.actual==="string"?a.actual:"";throw Error(`Failed to set ${o}. ${describeTextMismatch(n,s)}`+" ⚠️ 编辑器可能已自动保存草稿:请勿直接重试,先用 `ppcli xiaohongshu drafts` 查看,"+"已存在则改为在该草稿上继续操作或先删除,避免建出重复草稿。"+` Debug screenshot: /tmp/xhs_publish_${o}_debug.png`)}}async function $t(t,e){return h(await t.evaluate(`
|
|
129
145
|
(selectors => {
|
|
130
146
|
const el = selectors
|
|
131
147
|
.map(sel => Array.from(document.querySelectorAll(sel)))
|
|
@@ -141,7 +157,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
141
157
|
selection?.addRange(range);
|
|
142
158
|
return true;
|
|
143
159
|
})(${JSON.stringify(e)})
|
|
144
|
-
`))}function
|
|
160
|
+
`))}function te(t,{click:e=!1}={}){return`
|
|
145
161
|
(topicName => {
|
|
146
162
|
const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
|
|
147
163
|
const want = norm(topicName);
|
|
@@ -181,7 +197,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
181
197
|
text: (target.innerText || target.textContent || '').trim().slice(0, 40),
|
|
182
198
|
};
|
|
183
199
|
})(${JSON.stringify(t)})
|
|
184
|
-
`}function
|
|
200
|
+
`}function ee(t,e){return`
|
|
185
201
|
((topicName, selectors) => {
|
|
186
202
|
const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
|
|
187
203
|
const want = norm(topicName);
|
|
@@ -230,7 +246,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
230
246
|
}
|
|
231
247
|
return count;
|
|
232
248
|
})(${JSON.stringify(t)}, ${JSON.stringify(e)})
|
|
233
|
-
`}async function
|
|
249
|
+
`}async function ne(t,e){const n=`#${e}`;if(typeof t.insertText==="function")try{await t.insertText(n);return!0}catch{}return h(await t.evaluate(`
|
|
234
250
|
(text => {
|
|
235
251
|
const ok = document.execCommand('insertText', false, text);
|
|
236
252
|
const active = document.activeElement;
|
|
@@ -247,7 +263,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
247
263
|
if (!editor) return -1;
|
|
248
264
|
return (editor.innerText || editor.textContent || '').length;
|
|
249
265
|
})(${JSON.stringify(t)})
|
|
250
|
-
`}async function At(t,e,n){if(!await
|
|
266
|
+
`}async function At(t,e,n){if(!await $t(t,e))throw new u(`Could not attach topic "${n}": body editor not found`);const r=Number(h(await t.evaluate(ot(n,e))))||0;if(typeof t.pressKey==="function")try{await t.pressKey("Enter")}catch{}if(typeof t.insertText!=="function")throw new u(`Could not attach topic "${n}": page.insertText is unavailable`);try{await L(t,`#${n}`)}catch{throw new u(`Could not attach topic "${n}": failed to type inline topic query`)}await v(t,1.2);if(typeof t.pressKey!=="function")throw new u(`Could not attach topic "${n}": page.pressKey is unavailable`);try{await t.pressKey("Enter")}catch(a){throw new u(`Could not attach topic "${n}": failed to accept suggestion (${a&&a.message||a})`)}await t.wait({time:0.8});if((Number(h(await t.evaluate(ot(n,e))))||0)<=r)throw new u(`Could not attach topic "${n}": no real topic entity appeared after selection`)}async function Ct(t,e,n){const o=[],r=[];for(const i of n){let a=-1;try{const s=Number(h(await t.evaluate(rt(e))));a=Number.isFinite(s)?s:-1}catch{}try{await At(t,e,i);o.push(i);await t.wait({time:0.4})}catch(s){r.push({topic:i,reason:s&&s.message||String(s)});if(a>=0&&typeof t.pressKey==="function")try{const d=i.length+6;for(let c=0;c<d;c++){const m=Number(h(await t.evaluate(rt(e))));if(!Number.isFinite(m)||m<0||m<=a)break;await t.pressKey("Backspace");await t.wait({time:0.15})}}catch{}}}return{added:o,failed:r}}async function Nt(t){const e=await t.evaluate(`
|
|
251
267
|
() => {
|
|
252
268
|
const isVisible = (el) => {
|
|
253
269
|
if (!el || el.offsetParent === null) return false;
|
|
@@ -331,7 +347,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
331
347
|
}
|
|
332
348
|
return { ok: false };
|
|
333
349
|
})()
|
|
334
|
-
`));if(i?.ok)return i;await t.wait({time:o/1000})}return i}async function
|
|
350
|
+
`));if(i?.ok)return i;await t.wait({time:o/1000})}return i}async function Mt(t){const e=await t.evaluate(`
|
|
335
351
|
(() => {
|
|
336
352
|
const __opencli_xhs_focus_card = true;
|
|
337
353
|
const sel = ${JSON.stringify(J)};
|
|
@@ -348,7 +364,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
348
364
|
selection?.addRange(range);
|
|
349
365
|
return { ok: true };
|
|
350
366
|
})()
|
|
351
|
-
`);return h(e)}async function
|
|
367
|
+
`);return h(e)}async function Rt(t){const e=await t.evaluate(`
|
|
352
368
|
(() => {
|
|
353
369
|
const __opencli_xhs_card_text = true;
|
|
354
370
|
const sel = ${JSON.stringify(J)};
|
|
@@ -366,16 +382,16 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
366
382
|
const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
|
|
367
383
|
return { ok: true, count: editors.length, activeEmpty: !activeText };
|
|
368
384
|
})()
|
|
369
|
-
`);return h(e)}async function
|
|
385
|
+
`);return h(e)}async function It(t,e=8000){const n=300,o=Math.ceil(e/n);for(let r=0;r<o;r++){if((await it(t))?.count>=1)return!0;await t.wait({time:n/1000})}return!1}async function Ot(t,e,n=6000){const o=300,r=Math.ceil(n/o);for(let i=0;i<r;i++){const a=await it(t);if(a?.count>=e&&a?.activeEmpty)return!0;await t.wait({time:o/1000})}return!1}async function qt(t,e,n=4){for(let o=0;o<n;o++){if(!(await T(t,H))?.ok)return!1;if(await Ot(t,e,2500))return!0}return!1}async function Pt(t){const e=await t.evaluate(`
|
|
370
386
|
(() => {
|
|
371
387
|
const __opencli_xhs_preview_ready = true;
|
|
372
388
|
const ready = Array.from(document.querySelectorAll('button'))
|
|
373
389
|
.some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
|
|
374
390
|
return { ok: ready };
|
|
375
391
|
})()
|
|
376
|
-
`);return h(e)}async function
|
|
377
|
-
`);if(!(await
|
|
378
|
-
`);for(let a=0;a<i.length;a++){if(a>0&&typeof t.pressKey==="function")await t.pressKey("Enter");if(i[a])await L(t,i[a])}}else await t.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(e)})`);await t.wait({time:0.4});if(!(await
|
|
392
|
+
`);return h(e)}async function Dt(t,e=6){const n=400,o=Math.ceil(3000/n);for(let r=0;r<e;r++){if(!(await T(t,Y))?.ok&&r===0)return!1;for(let a=0;a<o;a++){if((await Pt(t))?.ok)return!0;await t.wait({time:n/1000})}}return!1}async function Lt(t,e,n){e=String(e).replace(/\\n/g,`
|
|
393
|
+
`);if(!(await Mt(t))?.ok)throw new u(`文字配图: could not focus card editor #${n+1}`);if(typeof t.insertText==="function"){const i=e.split(`
|
|
394
|
+
`);for(let a=0;a<i.length;a++){if(a>0&&typeof t.pressKey==="function")await t.pressKey("Enter");if(i[a])await L(t,i[a])}}else await t.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(e)})`);await t.wait({time:0.4});if(!(await Rt(t))?.ok)throw new u(`文字配图: card editor #${n+1} is empty after typing`)}async function Bt(t,e){if(!e||e===S)return S;const n=h(await t.evaluate(`
|
|
379
395
|
(async () => {
|
|
380
396
|
const __opencli_xhs_card_styles = true;
|
|
381
397
|
const want = ${JSON.stringify(e)};
|
|
@@ -436,7 +452,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
436
452
|
}
|
|
437
453
|
return { ok: true, count };
|
|
438
454
|
})()
|
|
439
|
-
`);return h(e)}async function st(t,e,n){const o=await at(t);if(!o||typeof o.count!=="number")throw new u(`${n}: could not verify current composer media count`);if(o.count<e){await t.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new u(`${n}: expected at least ${e} visible media item(s), got ${o.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function
|
|
455
|
+
`);return h(e)}async function st(t,e,n){const o=await at(t);if(!o||typeof o.count!=="number")throw new u(`${n}: could not verify current composer media count`);if(o.count<e){await t.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new u(`${n}: expected at least ${e} visible media item(s), got ${o.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function Jt(t,e,n){if(!(await T(t,B))?.ok){await t.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new u(`文字配图: could not click "${B}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await It(t)){await t.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new u(`文字配图: 写文字 card editor did not appear after clicking "${B}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let s=0;s<e.length;s++){if(s>0){if(!await qt(t,s+1)){await t.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new u(`文字配图: new card editor #${s+1} did not render after "${H}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Lt(t,e[s],s)}if(!await Dt(t)){await t.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new u(`文字配图: "${Y}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const i=await Bt(t,n);if(!(await T(t,W))?.ok){await t.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new u(`文字配图: could not click "${W}". Debug: /tmp/xhs_publish_next_debug.png`)}await t.wait({time:2});return i}async function lt(t){return t.evaluate(`
|
|
440
456
|
() => {
|
|
441
457
|
const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
|
|
442
458
|
const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
|
|
@@ -466,7 +482,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
466
482
|
const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
|
|
467
483
|
return { state, hasTitleInput, hasImageInput, hasVideoSurface };
|
|
468
484
|
}
|
|
469
|
-
`)}async function
|
|
485
|
+
`)}async function Ft(t,e=5000){const n=500,o=Math.max(1,Math.ceil(e/n));let r=await lt(t);for(let i=0;i<o;i++){if(r.state!=="video_surface")return r;if(i<o-1){await t.wait({time:n/1000});r=await lt(t)}}return r}async function Ut(t,e=1e4){const n=1000,o=Math.ceil(e/n);for(let r=0;r<o;r++){if(await t.evaluate(`
|
|
470
486
|
(() => {
|
|
471
487
|
const sels = ${JSON.stringify(F)};
|
|
472
488
|
for (const sel of sels) {
|
|
@@ -474,7 +490,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
474
490
|
if (el && el.offsetParent !== null) return true;
|
|
475
491
|
}
|
|
476
492
|
return false;
|
|
477
|
-
})()`))return!0;if(r<o-1)await t.wait({time:n/1000})}return!1}async function
|
|
493
|
+
})()`))return!0;if(r<o-1)await t.wait({time:n/1000})}return!1}async function jt(t){const e=h(await t.evaluate(`
|
|
478
494
|
(() => {
|
|
479
495
|
const __opencli_xhs_restore_srcs = true;
|
|
480
496
|
return Array.from(document.querySelectorAll('.img-container img'))
|
|
@@ -512,7 +528,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
512
528
|
}
|
|
513
529
|
return removed;
|
|
514
530
|
})(${JSON.stringify(e)})
|
|
515
|
-
`));return Number(n)||0}function
|
|
531
|
+
`));return Number(n)||0}function Vt(t){const e=String(t&&(t.url||t.key)||""),n=String(t&&t.method||"").toUpperCase(),o=Number(t&&t.status||0);if(n!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(e))return null;if(o<200||o>=300)return null;const r=e.match(/\/spectrum\/([^?#]+)/);return r?r[1]:e}function ut(t){return Math.min(45,6+t*4)}async function dt(t,e,n,o=90000){if(e<=0)return{ok:!0,completed:0};if(!n||typeof t.readNetworkCapture!=="function"){await t.wait({time:ut(e)});return{ok:!0,completed:null,viaFallback:!0}}const r=1500,a=Math.ceil(9000/r),s=Math.ceil(o/r),d=new Set;for(let c=0;c<s;c++){const m=await t.readNetworkCapture().catch(()=>[]);for(const w of Array.isArray(m)?m:[]){const f=Vt(w);if(f)d.add(f)}if(d.size>=e)return{ok:!0,completed:d.size};if(c>=a&&d.size===0){await t.wait({time:ut(e)});return{ok:!0,completed:null,viaFallback:!0}}await t.wait({time:r/1000})}return{ok:!1,completed:d.size}}async function zt(t,e,n){try{await t.goto("https://creator.xiaohongshu.com/new/note-manager")}catch{return null}const o=e.trim();for(let r=0;r<n;r++){await t.wait({time:1.5});let i;try{i=h(await t.evaluate(`(() => {
|
|
516
532
|
const noteIdRe = /"noteId":"([0-9a-f]{24})"/;
|
|
517
533
|
return Array.from(document.querySelectorAll('div.note[data-impression], div.note')).slice(0, 3).map((card) => {
|
|
518
534
|
const impression = card.getAttribute('data-impression') || '';
|
|
@@ -521,7 +537,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
521
537
|
const dateText = (card.querySelector('.time_status, .time')?.innerText || '').trim();
|
|
522
538
|
return { id, title, dateText };
|
|
523
539
|
}).filter((entry) => entry.title);
|
|
524
|
-
})()`))}catch{return null}if(Array.isArray(i)){const a=i.find((s)=>{const d=String(s.title||"").trim();if(!(d===o||d.startsWith(o)||o.startsWith(d)))return!1;const c=String(s.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!c)return!0;const m=Date.UTC(Number(c[1]),Number(c[2])-1,Number(c[3]),Number(c[4]),Number(c[5]))-28800000;return Math.abs(Date.now()-m)<=93600000});if(a)return a}}return null}mt({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:pt.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${G} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${S}。可选: ${X.map(([t,e])=>`${t}(${e})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号。平台话题库匹配不上的会自动跳过并在结果中提示,不影响发布"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail","url"],func:async(t,e)=>{if(!t)throw Error("Browser page required");const n=String(e.title??"").trim(),o=e.file?String(e.file).trim():"";let r=String(e.content??"").trim();if(o){if(r)throw new
|
|
540
|
+
})()`))}catch{return null}if(Array.isArray(i)){const a=i.find((s)=>{const d=String(s.title||"").trim();if(!(d===o||d.startsWith(o)||o.startsWith(d)))return!1;const c=String(s.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!c)return!0;const m=Date.UTC(Number(c[1]),Number(c[2])-1,Number(c[3]),Number(c[4]),Number(c[5]))-28800000;return Math.abs(Date.now()-m)<=93600000});if(a)return a}}return null}mt({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:pt.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${G} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${S}。可选: ${X.map(([t,e])=>`${t}(${e})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号。平台话题库匹配不上的会自动跳过并在结果中提示,不影响发布"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail","url"],func:async(t,e)=>{if(!t)throw Error("Browser page required");const n=String(e.title??"").trim(),o=e.file?String(e.file).trim():"";let r=String(e.content??"").trim();if(o){if(r)throw new b("正文位置参数和 --file 只能二选一,不能同时给");const l=M.resolve(o);if(!D.existsSync(l))throw new b(`--file 指向的文件不存在: ${l}`);r=D.readFileSync(l,"utf-8").trim();if(!r)throw new b(`--file 指向的文件内容为空: ${l}`)}const i=e.images?String(e.images).split(",").map((l)=>l.trim()).filter(Boolean):[],a=e.topics?String(e.topics).split(",").map((l)=>l.trim()).filter(Boolean):[],s=Boolean(e.draft),d=e["card-text"]?String(e["card-text"]):"",c=d?d.split(G).map((l)=>l.trim()).filter(Boolean):[],m=e["card-style"]?String(e["card-style"]).trim():"",w=c.length>0;if(!n)throw new b("--title is required");if(n.length>z)throw new b(`Title is ${n.length} chars — must be ≤ ${z}`);if(!r)throw new b("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");wt(r,{fileFlag:"--file"});if(!w&&i.length===0)throw new b("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(i.length>V)throw new b(`Too many images: ${i.length} (max ${V})`);if(w&&i.some((l)=>M.extname(l).toLowerCase()===".gif"))throw new b("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const f=Tt(i);await t.goto(yt);await t.wait({time:1});await t.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let k="";for(let l=0;l<30;l++){await t.wait({time:0.5});k=await t.evaluate("() => location.href");if(k.includes("creator.xiaohongshu.com"))break}if(!k.includes("creator.xiaohongshu.com")){await t.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${k}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const E=await Nt(t);if((await Ft(t,E?.ok?5000:2000)).state==="video_surface"){await t.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const l=E?.ok?`clicked "${E.text}"`:`visible candidates: ${(E?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${l}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const A=await jt(t);let C=m;if(w)C=await Jt(t,c,m);else{const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片注入失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await et(t);if(A.length){await ct(t,A);await t.wait({time:0.5})}const N=await at(t);if(N&&typeof N.count==="number"&&N.count>f.length){await t.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new u(`清掉恢复态旧图后编辑器内仍有 ${N.count} 张图,超过本次注入的 ${f.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await Ut(t)){await t.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new u("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(w)await st(t,c.length,"文字配图 generated images");if(w&&f.length>0){const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await et(t);await st(t,c.length+f.length,"文字配图 appended images")}if(w&&A.length){await ct(t,A);await t.wait({time:0.5})}await nt(t,F,n,"title");await v(t,0.6);await nt(t,Q,r,"content");await v(t,0.6);let R=[],I=[];if(a.length){const l=await Ct(t,Q,a);R=l.added;I=l.failed}await v(t,1,0.6);const O=await xt(t,["xhs-publish-btn"]);if(O){await gt(t,O.x,O.y);await v(t,0.4)}const U=s?["暂存离开","存草稿"]:["发布","发布笔记"],p=await t.evaluate(`
|
|
525
541
|
(cfg => {
|
|
526
542
|
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
527
543
|
const isVisible = (el) => {
|
|
@@ -609,4 +625,4 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
609
625
|
}
|
|
610
626
|
return '';
|
|
611
627
|
})(${JSON.stringify(ft)})
|
|
612
|
-
`),ht=!_.includes("/publish/publish");let q=j.length>0||ht;const P=s?"暂存成功":"发布成功";let y=null;if(!s){y=await
|
|
628
|
+
`),ht=!_.includes("/publish/publish");let q=j.length>0||ht;const P=s?"暂存成功":"发布成功";let y=null;if(!s){y=await zt(t,n,q?4:8);if(y)q=!0}if(!q){if(s)throw new u(`${P}无法确认:未观察到成功提示或提交后跳转。`+(_?`当前 URL: ${_}。`:"当前 URL 为空。")+"先用 `xiaohongshu drafts` 查草稿箱确认是否已存上;确认没有再重新运行本命令,避免堆出重复草稿。");throw new u(`${P}无法确认:未观察到成功提示或提交后跳转,创作者后台也暂未查到同标题笔记。`+(_?`当前 URL: ${_}。`:"")+"注意:发布按钮已被触发,笔记仍可能延迟入库。禁止立即重发——请等 1-2 分钟后用 `xiaohongshu creator-notes` 回查本篇标题是否已出现;确认不存在后才重新运行本 publish 命令,否则会重复发布。")}return[{status:`✅ ${P}`,detail:[`"${n}"`,w?`${c.length}张文字配图${f.length?` + ${f.length}张图片`:""}${C&&C!==S?` (${C})`:""}`:`${f.length}张图片`,R.length?`话题: ${R.join(" ")}`:"",I.length?`⚠️ 话题未附上(已自动跳过,不影响发布): ${I.map((l)=>l.topic).join(" ")}`:"",y?.id?`笔记ID: ${y.id}`:"",j||(y?"已在创作者后台确认":"")||_||""].filter(Boolean).join(" · "),url:y?.id?`https://www.xiaohongshu.com/explore/${y.id}`:""}]}});
|
|
@@ -148,7 +148,20 @@ export declare abstract class BasePage implements IPage {
|
|
|
148
148
|
to?: ResolveOptions;
|
|
149
149
|
}): Promise<DragResult>;
|
|
150
150
|
fillText(ref: string, text: string, opts?: ResolveOptions): Promise<FillTextResult>;
|
|
151
|
-
|
|
151
|
+
/**
|
|
152
|
+
* 按一个键。返回**实际走的通道**,因为两条通道的能力差别很大:
|
|
153
|
+
*
|
|
154
|
+
* - `'native'`:CDP `Input.dispatchKeyEvent`,是浏览器眼里的真按键,会触发默认
|
|
155
|
+
* 编辑动作,富文本编辑器认。
|
|
156
|
+
* - `'synthetic'`:页面内 `new KeyboardEvent(...)`。它 **untrusted**,**不触发浏览器
|
|
157
|
+
* 默认动作**——真机实测在普通 contenteditable 上按 Enter 毫无反应。反过来,自己监听
|
|
158
|
+
* keydown 处理的编辑器(tiptap/ProseMirror)照常响应,所以它并非一律无效。
|
|
159
|
+
*
|
|
160
|
+
* 差别既然这么大,就不该是静默的:调用方要能知道自己拿到的是真按键还是一个可能
|
|
161
|
+
* 什么都没做的合成事件。往编辑器里敲段落分隔这类「写内容」的调用尤其要检查——
|
|
162
|
+
* 与其静默产出一篇塌成一段的正文,不如报错。
|
|
163
|
+
*/
|
|
164
|
+
pressKey(key: string): Promise<'native' | 'synthetic'>;
|
|
152
165
|
scrollTo(ref: string, opts?: ResolveOptions): Promise<unknown>;
|
|
153
166
|
getFormState(): Promise<Record<string, unknown>>;
|
|
154
167
|
scroll(direction?: string, amount?: number): Promise<void>;
|
|
@@ -195,7 +195,7 @@ ${e}
|
|
|
195
195
|
};
|
|
196
196
|
return { source: measure(sourceEl), target: measure(targetEl) };
|
|
197
197
|
})()
|
|
198
|
-
`);if(o?.source?.visible!==!0)throw Error(`Drag source "${e}" is not visible at drag time.`);if(o?.target?.visible!==!0)throw Error(`Drag target "${t}" has no visible bounding box.`);if(!await this.tryNativeDrag({x:o.source.x,y:o.source.y},{x:o.target.x,y:o.target.y}))throw Error("Native drag requires CDP Input.dispatchMouseEvent support.");return{dragged:!0,source:e,target:t,source_matches_n:r.matches_n,target_matches_n:s.matches_n,source_match_level:r.match_level,target_match_level:s.match_level}}finally{await this.evaluate("delete window.__opencli_drag_source").catch(()=>{})}}async fillText(e,t,a={}){const r=await d(this,e,a);let n=!1,i=!1;try{n=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded");i=await this.tryCdpOnResolvedElement("DOM.focus")}catch{}if((await this.evaluate(y({skipScroll:n,skipFocus:i})))?.ok!==!0)throw new h({code:"not_editable",message:`Target "${e}" 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 l=await this.tryNativeType(t);if(!l)await this.evaluate(m(t));let o=await this.evaluate(v(t));if(l&&o?.ok!==!0){await this.evaluate(m(t));o=await this.evaluate(v(t))}const u=o&&"actual"in o?o.actual:"",c=o&&"mode"in o?o.mode:void 0;return{...r,filled:!0,verified:o?.ok===!0,expected:t,actual:u,length:u.length,...c?{mode:c}:{}}}async pressKey(e){const t=L(e);if(
|
|
198
|
+
`);if(o?.source?.visible!==!0)throw Error(`Drag source "${e}" is not visible at drag time.`);if(o?.target?.visible!==!0)throw Error(`Drag target "${t}" has no visible bounding box.`);if(!await this.tryNativeDrag({x:o.source.x,y:o.source.y},{x:o.target.x,y:o.target.y}))throw Error("Native drag requires CDP Input.dispatchMouseEvent support.");return{dragged:!0,source:e,target:t,source_matches_n:r.matches_n,target_matches_n:s.matches_n,source_match_level:r.match_level,target_match_level:s.match_level}}finally{await this.evaluate("delete window.__opencli_drag_source").catch(()=>{})}}async fillText(e,t,a={}){const r=await d(this,e,a);let n=!1,i=!1;try{n=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded");i=await this.tryCdpOnResolvedElement("DOM.focus")}catch{}if((await this.evaluate(y({skipScroll:n,skipFocus:i})))?.ok!==!0)throw new h({code:"not_editable",message:`Target "${e}" 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 l=await this.tryNativeType(t);if(!l)await this.evaluate(m(t));let o=await this.evaluate(v(t));if(l&&o?.ok!==!0){await this.evaluate(m(t));o=await this.evaluate(v(t))}const u=o&&"actual"in o?o.actual:"",c=o&&"mode"in o?o.mode:void 0;return{...r,filled:!0,verified:o?.ok===!0,expected:t,actual:u,length:u.length,...c?{mode:c}:{}}}async pressKey(e){const t=L(e);if(await this.tryNativeKeyPress(t.key,t.modifiers))return"native";await this.evaluate(C(t.key,t.modifiers));return"synthetic"}async scrollTo(e,t={}){const a=await d(this,e,t),r=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded"),n=await this.evaluate(U({skipScroll:r}));if(n&&typeof n==="object")return{...n,matches_n:a.matches_n,match_level:a.match_level};return{matches_n:a.matches_n,match_level:a.match_level}}async getFormState(){return await this.evaluate(I())}async scroll(e="down",t=500){await this.evaluate(S(e,t))}async autoScroll(e){const t=e?.times??3,a=e?.delayMs??2000;await this.evaluate(N(t,a))}async networkRequests(e=!1){const t=await this.evaluate(O(e));return Array.isArray(t)?t:[]}async consoleMessages(e="info"){return[]}async wait(e){if(typeof e==="number"){if(e>=1)try{const t=e*1000;await this.evaluate(D(t,Math.min(500,t)));return}catch{}await new Promise((t)=>setTimeout(t,e*1000));return}if(typeof e.time==="number"){await new Promise((t)=>setTimeout(t,e.time*1000));return}if(e.selector){const t=(e.timeout??10)*1000;await this.evaluate(R(e.selector,t));return}if(e.text){const t=(e.timeout??30)*1000;await this.evaluate(A(e.text,t))}}async snapshot(e={}){if(e.source==="ax"){const a=this.cdp;if(typeof a!=="function")throw new f("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 r=await this.collectAxSnapshotTrees(a),n=M(r,{maxDepth:e.maxDepth,interactiveOnly:e.interactive});this._axRefs=n.refs;return n.text}this._axRefs.clear();const t=x({viewportExpand:e.viewportExpand??2000,maxDepth:Math.max(1,Math.min(Number(e.maxDepth)||50,200)),interactiveOnly:e.interactive??!1,maxTextLength:e.maxTextLength??120,includeScrollInfo:!0,bboxDedup:!0,previousHashes:this._prevSnapshotHashes});try{const a=await this.evaluate(t);try{const r=await this.evaluate("window.__opencli_prev_hashes");this._prevSnapshotHashes=typeof r==="string"?r:null}catch{}return a}catch(a){if(process.env.DEBUG_SNAPSHOT)process.stderr.write(`[snapshot] DOM snapshot failed, falling back to accessibility tree: ${a?.message?.slice(0,200)}
|
|
199
199
|
`);return this._basicSnapshot(e)}}async collectAxSnapshotTrees(e){await e.call(this,"Accessibility.enable",{});const a=[{tree:await e.call(this,"Accessibility.getFullAXTree",{})}],r=await e.call(this,"Page.getFrameTree",{}).catch(()=>null),n=V(r);for(const i of n){if(i.sessionId)await e.call(this,"Accessibility.enable",k(i)).catch(()=>null);const s=await e.call(this,"Accessibility.getFullAXTree",b(i)).catch(()=>null);if(s)a.push({tree:s,frame:i})}return a}async getCurrentUrl(){if(this._lastUrl)return this._lastUrl;try{const e=await this.evaluate("window.location.href");if(typeof e==="string"&&e){this._lastUrl=e;return e}}catch{}return null}async installInterceptor(e){const{generateInterceptorJs:t}=await import("../interceptor.js");await this.evaluate(t(JSON.stringify(e),{arrayName:"__opencli_xhr",patchGuard:"__opencli_interceptor_patched"}))}async getInterceptedRequests(){const{generateReadInterceptedJs:e}=await import("../interceptor.js"),t=await this.evaluate(e("__opencli_xhr"));return Array.isArray(t)?t:[]}async waitForCapture(e=10){const t=e*1000;await this.evaluate(T(t))}async _basicSnapshot(e={}){const a=`
|
|
200
200
|
(async () => {
|
|
201
201
|
function buildTree(node, depth) {
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as p}from"ws";import{request as _}from"node:http";import{request as y}from"node:https";import{buildEvaluateExpression as E}from"./utils.js";import{generateStealthJs as b}from"./stealth.js";import{waitForDomStableJs as C}from"./dom-helpers.js";import{isRecord as l,saveBase64ToFile as P}from"../utils.js";import{getAllElectronApps as k}from"../electron-apps.js";import{BasePage as v}from"./base-page.js";const I=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(e){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const n=e?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!n)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let s=n;if(n.startsWith("http")){const t=await q(`${n.replace(/\/$/,"")}/json`),i=g(t);if(!i||!i.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");s=i.webSocketDebuggerUrl}return new Promise((t,i)=>{const r=new p(s),o=(e?.timeout??10)*1000,d=setTimeout(()=>{this._ws=null;r.close();i(Error("CDP connect timeout"))},o);r.on("open",async()=>{clearTimeout(d);this._ws=r;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(c)=>{if(c?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:b()});if(e?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:e.initScript});if(e?.windowMode==="background")await this.send("Emulation.setFocusEmulationEnabled",{enabled:!0}).catch(()=>{});else if(e?.windowMode==="foreground")await this.send("Page.bringToFront").catch(()=>{});if(e?.proxyAuth){this.on("Fetch.requestPaused",(c)=>{const u=c;if(u.requestId)this.send("Fetch.continueRequest",{requestId:u.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(c)=>{const u=c;if(!u.requestId)return;this.send("Fetch.continueWithAuth",{requestId:u.requestId,authChallengeResponse:{response:"ProvideCredentials",username:e.proxyAuth.username,password:e.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(c){r.close();i(c instanceof Error?c:Error(String(c)));return}const a=new m(this);if(e?.session)a.session=e.session;if(e?.contextId)a.contextId=e.contextId;t(a)});r.on("error",(a)=>{clearTimeout(d);i(a)});r.on("message",(a)=>{try{const c=JSON.parse(a.toString());if(c.id&&this._pending.has(c.id)){const u=this._pending.get(c.id);clearTimeout(u.timer);this._pending.delete(c.id);if(c.error)u.reject(Error(c.error.message));else u.resolve(c.result)}if(c.method){const u=this._eventListeners.get(c.method);if(u)for(const h of u)h(c.params)}}catch(c){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",c instanceof Error?c.message:c)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const e of this._pending.values()){clearTimeout(e.timer);e.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(e,n={},s=I){if(!this._ws||this._ws.readyState!==p.OPEN)throw Error("CDP connection is not open");const t=++this._idCounter;return new Promise((i,r)=>{const o=setTimeout(()=>{this._pending.delete(t);const d=e.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";r(Error(`CDP command '${e}' timed out after ${s/1000}s${d}`))},s);this._pending.set(t,{resolve:i,reject:r,timer:o});this._ws.send(JSON.stringify({id:t,method:e,params:n}))})}on(e,n){let s=this._eventListeners.get(e);if(!s){s=new Set;this._eventListeners.set(e,s)}s.add(n)}off(e,n){this._eventListeners.get(e)?.delete(n)}waitForEvent(e,n=15000){return new Promise((s,t)=>{const i=setTimeout(()=>{this.off(e,r);t(Error(`Timed out waiting for CDP event '${e}'`))},n),r=(o)=>{clearTimeout(i);this.off(e,r);s(o)};this.on(e,r)})}}class m extends v{bridge;session;contextId;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(e){super();this.bridge=e}async goto(e,n){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const s=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:e});await s;this._lastUrl=e;if(n?.waitUntil!=="none"){const t=n?.settleMs??1000;await this.evaluate(C(t,Math.min(500,t)))}}async evaluate(e,...n){const s=E(e,n),t=await this.bridge.send("Runtime.evaluate",{expression:s,returnByValue:!0,awaitPromise:!0});if(t.exceptionDetails)throw Error("Evaluate error: "+(t.exceptionDetails.exception?.description||"Unknown exception"));return t.result?.value}async getCookies(e={}){let n;try{const t=await this.bridge.send("Storage.getCookies");n=l(t)&&Array.isArray(t.cookies)?t.cookies:[]}catch{const t=await this.bridge.send("Network.getCookies",e.url?{urls:[e.url]}:{});n=l(t)&&Array.isArray(t.cookies)?t.cookies:[]}const s=e.domain??(e.url?D(e.url):void 0);return s?n.filter((t)=>f(t)&&N(t.domain,s)):n.filter(f)}async screenshot(e={}){const n=e.fullPage===!0,s=e.width&&e.width>0?Math.ceil(e.width):void 0,t=!n&&e.height&&e.height>0?Math.ceil(e.height):void 0,i=s!==void 0||t!==void 0;if(i){if(s!==void 0&&n)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:s,height:0,deviceScaleFactor:1});let r=s??0,o=t??0;if(n){const d=await this.bridge.send("Page.getLayoutMetrics"),a=l(d)?d:{},c=l(a.cssContentSize)?a.cssContentSize:void 0,u=l(a.contentSize)?a.contentSize:void 0,h=c??u;if(h&&typeof h.width==="number"&&typeof h.height==="number"){if(r===0)r=Math.ceil(h.width);o=Math.ceil(h.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:r,height:o,deviceScaleFactor:1})}try{const r=await this.bridge.send("Page.captureScreenshot",{format:e.format??"png",quality:e.format==="jpeg"?e.quality??80:void 0,captureBeyondViewport:!i&&n}),o=l(r)&&typeof r.data==="string"?r.data:"";if(e.path)await P(o,e.path);return o}finally{if(i)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(e=""){this._networkCapturePattern=e;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(n)=>{const s=n;if(!this._networkCapturePattern||s.request.url.includes(this._networkCapturePattern)){const t=this._networkEntries.push({url:s.request.url,method:s.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(s.requestId,t)}});this.bridge.on("Network.responseReceived",(n)=>{const s=n,t=this._pendingRequests.get(s.requestId);if(t!==void 0){this._networkEntries[t].responseStatus=s.response.status;this._networkEntries[t].responseContentType=s.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(n)=>{const s=n,t=this._pendingRequests.get(s.requestId);if(t!==void 0){const i=this.bridge.send("Network.getResponseBody",{requestId:s.requestId}).then((r)=>{const o=r;if(typeof o?.body==="string"){const d=o.body.length,a=d>CDP_RESPONSE_BODY_CAPTURE_LIMIT,c=a?o.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):o.body;this._networkEntries[t].responsePreview=o.base64Encoded?`base64:${c}`:c;this._networkEntries[t].responseBodyFullSize=d;this._networkEntries[t].responseBodyTruncated=a}}).catch((r)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${s.requestId}:`,r instanceof Error?r.message:r)}).finally(()=>{this._pendingBodyFetches.delete(i)});this._pendingBodyFetches.add(i);this._pendingRequests.delete(s.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const e=[...this._networkEntries];this._networkEntries=[];return e}async consoleMessages(e="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(n)=>{const s=n,t=(s.args||[]).map((i)=>i.value!==void 0?String(i.value):i.description||"").join(" ");this._consoleMessages.push({type:s.type,text:t,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(n)=>{const s=n,t=s.exceptionDetails?.exception?.description||s.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:t,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(e==="all")return[...this._consoleMessages];if(e==="error")return this._consoleMessages.filter((n)=>n.type==="error"||n.type==="warning");return this._consoleMessages.filter((n)=>n.type===e)}async tabs(){return[]}async selectTab(e){}async cdp(e,n={}){return this.bridge.send(e,n)}async handleJavaScriptDialog(e,n){await this.cdp("Page.handleJavaScriptDialog",{accept:e,...n!==void 0&&{promptText:n}})}async nativeClick(e,n){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:e,y:n});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:e,y:n,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:e,y:n,button:"left",clickCount:1})}async nativeType(e){await this.cdp("Input.insertText",{text:e})}async insertText(e){await this.nativeType(e)}async nativeKeyPress(e,n=[]){let s=0;for(const t of n){if(t==="Alt")s|=1;if(t==="Ctrl"||t==="Control")s|=2;if(t==="Meta")s|=4;if(t==="Shift")s|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:e,modifiers:s});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:e,modifiers:s})}}function D(e){try{return new URL(e).hostname}catch{return}}function f(e){return l(e)&&typeof e.name==="string"&&typeof e.value==="string"&&typeof e.domain==="string"}function N(e,n){const s=e.replace(/^\./,"").toLowerCase(),t=n.replace(/^\./,"").toLowerCase();return t===s||t.endsWith(`.${s}`)}function g(e){const n=S(process.env.OPENCLI_CDP_TARGET);return e.map((t,i)=>({target:t,index:i,score:w(t,n)})).filter(({score:t})=>Number.isFinite(t)).sort((t,i)=>{if(i.score!==t.score)return i.score-t.score;return t.index-i.index})[0]?.target}function w(e,n){if(!e.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const s=(e.type??"").toLowerCase(),t=(e.url??"").toLowerCase(),i=(e.title??"").toLowerCase(),r=`${i} ${t}`;if(!r.trim()&&!s)return Number.NEGATIVE_INFINITY;if(r.includes("devtools"))return Number.NEGATIVE_INFINITY;if(s==="background_page"||s==="service_worker")return Number.NEGATIVE_INFINITY;let o=0;if(n&&n.test(r))o+=1000;if(s==="app")o+=120;else if(s==="webview")o+=100;else if(s==="page")o+=80;else if(s==="iframe")o+=20;if(t.startsWith("http://localhost")||t.startsWith("https://localhost"))o+=90;if(t.startsWith("file://"))o+=60;if(t.startsWith("http://127.0.0.1")||t.startsWith("https://127.0.0.1"))o+=50;if(t.startsWith("about:blank"))o-=120;if(t===""||t==="about:blank")o-=40;if(i&&i!=="devtools")o+=25;const d=Object.values(k()).map((a)=>(a.displayName??a.processName).toLowerCase());for(const a of d)if(i.includes(a)){o+=120;break}for(const a of d)if(t.includes(a)){o+=100;break}return o}function S(e){const n=e?.trim();if(!n)return;return new RegExp(T(n.toLowerCase()))}function T(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:g,scoreCDPTarget:w};function q(e){return new Promise((n,s)=>{const t=new URL(e),i=(t.protocol==="https:"?y:_)(t,(r)=>{const o=r.statusCode??0;if(o<200||o>=300){r.resume();s(Error(`Failed to fetch CDP targets: HTTP ${o}`));return}const d=[];r.on("data",(a)=>d.push(Buffer.isBuffer(a)?a:Buffer.from(a)));r.on("end",()=>{try{n(JSON.parse(Buffer.concat(d).toString("utf8")))}catch(a){s(a instanceof Error?a:Error(String(a)))}})});i.on("error",s);i.setTimeout(1e4,()=>i.destroy(Error("Timed out fetching CDP targets")));i.end()})}
|
|
1
|
+
import{WebSocket as p}from"ws";import{request as _}from"node:http";import{request as y}from"node:https";import{buildEvaluateExpression as E}from"./utils.js";import{generateStealthJs as b}from"./stealth.js";import{waitForDomStableJs as C,keyEventMetadata as P}from"./dom-helpers.js";import{isRecord as h,saveBase64ToFile as v}from"../utils.js";import{getAllElectronApps as k}from"../electron-apps.js";import{BasePage as I}from"./base-page.js";const D=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(e){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const n=e?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!n)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let s=n;if(n.startsWith("http")){const t=await q(`${n.replace(/\/$/,"")}/json`),i=g(t);if(!i||!i.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");s=i.webSocketDebuggerUrl}return new Promise((t,i)=>{const r=new p(s),o=(e?.timeout??10)*1000,d=setTimeout(()=>{this._ws=null;r.close();i(Error("CDP connect timeout"))},o);r.on("open",async()=>{clearTimeout(d);this._ws=r;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(c)=>{if(c?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:b()});if(e?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:e.initScript});if(e?.windowMode==="background")await this.send("Emulation.setFocusEmulationEnabled",{enabled:!0}).catch(()=>{});else if(e?.windowMode==="foreground")await this.send("Page.bringToFront").catch(()=>{});if(e?.proxyAuth){this.on("Fetch.requestPaused",(c)=>{const u=c;if(u.requestId)this.send("Fetch.continueRequest",{requestId:u.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(c)=>{const u=c;if(!u.requestId)return;this.send("Fetch.continueWithAuth",{requestId:u.requestId,authChallengeResponse:{response:"ProvideCredentials",username:e.proxyAuth.username,password:e.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(c){r.close();i(c instanceof Error?c:Error(String(c)));return}const a=new m(this);if(e?.session)a.session=e.session;if(e?.contextId)a.contextId=e.contextId;t(a)});r.on("error",(a)=>{clearTimeout(d);i(a)});r.on("message",(a)=>{try{const c=JSON.parse(a.toString());if(c.id&&this._pending.has(c.id)){const u=this._pending.get(c.id);clearTimeout(u.timer);this._pending.delete(c.id);if(c.error)u.reject(Error(c.error.message));else u.resolve(c.result)}if(c.method){const u=this._eventListeners.get(c.method);if(u)for(const l of u)l(c.params)}}catch(c){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",c instanceof Error?c.message:c)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const e of this._pending.values()){clearTimeout(e.timer);e.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(e,n={},s=D){if(!this._ws||this._ws.readyState!==p.OPEN)throw Error("CDP connection is not open");const t=++this._idCounter;return new Promise((i,r)=>{const o=setTimeout(()=>{this._pending.delete(t);const d=e.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";r(Error(`CDP command '${e}' timed out after ${s/1000}s${d}`))},s);this._pending.set(t,{resolve:i,reject:r,timer:o});this._ws.send(JSON.stringify({id:t,method:e,params:n}))})}on(e,n){let s=this._eventListeners.get(e);if(!s){s=new Set;this._eventListeners.set(e,s)}s.add(n)}off(e,n){this._eventListeners.get(e)?.delete(n)}waitForEvent(e,n=15000){return new Promise((s,t)=>{const i=setTimeout(()=>{this.off(e,r);t(Error(`Timed out waiting for CDP event '${e}'`))},n),r=(o)=>{clearTimeout(i);this.off(e,r);s(o)};this.on(e,r)})}}class m extends I{bridge;session;contextId;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(e){super();this.bridge=e}async goto(e,n){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const s=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:e});await s;this._lastUrl=e;if(n?.waitUntil!=="none"){const t=n?.settleMs??1000;await this.evaluate(C(t,Math.min(500,t)))}}async evaluate(e,...n){const s=E(e,n),t=await this.bridge.send("Runtime.evaluate",{expression:s,returnByValue:!0,awaitPromise:!0});if(t.exceptionDetails)throw Error("Evaluate error: "+(t.exceptionDetails.exception?.description||"Unknown exception"));return t.result?.value}async getCookies(e={}){let n;try{const t=await this.bridge.send("Storage.getCookies");n=h(t)&&Array.isArray(t.cookies)?t.cookies:[]}catch{const t=await this.bridge.send("Network.getCookies",e.url?{urls:[e.url]}:{});n=h(t)&&Array.isArray(t.cookies)?t.cookies:[]}const s=e.domain??(e.url?N(e.url):void 0);return s?n.filter((t)=>f(t)&&S(t.domain,s)):n.filter(f)}async screenshot(e={}){const n=e.fullPage===!0,s=e.width&&e.width>0?Math.ceil(e.width):void 0,t=!n&&e.height&&e.height>0?Math.ceil(e.height):void 0,i=s!==void 0||t!==void 0;if(i){if(s!==void 0&&n)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:s,height:0,deviceScaleFactor:1});let r=s??0,o=t??0;if(n){const d=await this.bridge.send("Page.getLayoutMetrics"),a=h(d)?d:{},c=h(a.cssContentSize)?a.cssContentSize:void 0,u=h(a.contentSize)?a.contentSize:void 0,l=c??u;if(l&&typeof l.width==="number"&&typeof l.height==="number"){if(r===0)r=Math.ceil(l.width);o=Math.ceil(l.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:r,height:o,deviceScaleFactor:1})}try{const r=await this.bridge.send("Page.captureScreenshot",{format:e.format??"png",quality:e.format==="jpeg"?e.quality??80:void 0,captureBeyondViewport:!i&&n}),o=h(r)&&typeof r.data==="string"?r.data:"";if(e.path)await v(o,e.path);return o}finally{if(i)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(e=""){this._networkCapturePattern=e;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(n)=>{const s=n;if(!this._networkCapturePattern||s.request.url.includes(this._networkCapturePattern)){const t=this._networkEntries.push({url:s.request.url,method:s.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(s.requestId,t)}});this.bridge.on("Network.responseReceived",(n)=>{const s=n,t=this._pendingRequests.get(s.requestId);if(t!==void 0){this._networkEntries[t].responseStatus=s.response.status;this._networkEntries[t].responseContentType=s.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(n)=>{const s=n,t=this._pendingRequests.get(s.requestId);if(t!==void 0){const i=this.bridge.send("Network.getResponseBody",{requestId:s.requestId}).then((r)=>{const o=r;if(typeof o?.body==="string"){const d=o.body.length,a=d>CDP_RESPONSE_BODY_CAPTURE_LIMIT,c=a?o.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):o.body;this._networkEntries[t].responsePreview=o.base64Encoded?`base64:${c}`:c;this._networkEntries[t].responseBodyFullSize=d;this._networkEntries[t].responseBodyTruncated=a}}).catch((r)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${s.requestId}:`,r instanceof Error?r.message:r)}).finally(()=>{this._pendingBodyFetches.delete(i)});this._pendingBodyFetches.add(i);this._pendingRequests.delete(s.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const e=[...this._networkEntries];this._networkEntries=[];return e}async consoleMessages(e="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(n)=>{const s=n,t=(s.args||[]).map((i)=>i.value!==void 0?String(i.value):i.description||"").join(" ");this._consoleMessages.push({type:s.type,text:t,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(n)=>{const s=n,t=s.exceptionDetails?.exception?.description||s.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:t,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(e==="all")return[...this._consoleMessages];if(e==="error")return this._consoleMessages.filter((n)=>n.type==="error"||n.type==="warning");return this._consoleMessages.filter((n)=>n.type===e)}async tabs(){return[]}async selectTab(e){}async cdp(e,n={}){return this.bridge.send(e,n)}async handleJavaScriptDialog(e,n){await this.cdp("Page.handleJavaScriptDialog",{accept:e,...n!==void 0&&{promptText:n}})}async nativeClick(e,n){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:e,y:n});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:e,y:n,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:e,y:n,button:"left",clickCount:1})}async nativeType(e){await this.cdp("Input.insertText",{text:e})}async insertText(e){await this.nativeType(e)}async nativeKeyPress(e,n=[]){let s=0;for(const i of n){if(i==="Alt")s|=1;if(i==="Ctrl"||i==="Control")s|=2;if(i==="Meta")s|=4;if(i==="Shift")s|=8}const t=P(e,n);await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:e,modifiers:s,...t});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:e,modifiers:s,...t.code?{code:t.code}:{},...t.windowsVirtualKeyCode?{windowsVirtualKeyCode:t.windowsVirtualKeyCode,nativeVirtualKeyCode:t.windowsVirtualKeyCode}:{}})}}function N(e){try{return new URL(e).hostname}catch{return}}function f(e){return h(e)&&typeof e.name==="string"&&typeof e.value==="string"&&typeof e.domain==="string"}function S(e,n){const s=e.replace(/^\./,"").toLowerCase(),t=n.replace(/^\./,"").toLowerCase();return t===s||t.endsWith(`.${s}`)}function g(e){const n=T(process.env.OPENCLI_CDP_TARGET);return e.map((t,i)=>({target:t,index:i,score:w(t,n)})).filter(({score:t})=>Number.isFinite(t)).sort((t,i)=>{if(i.score!==t.score)return i.score-t.score;return t.index-i.index})[0]?.target}function w(e,n){if(!e.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const s=(e.type??"").toLowerCase(),t=(e.url??"").toLowerCase(),i=(e.title??"").toLowerCase(),r=`${i} ${t}`;if(!r.trim()&&!s)return Number.NEGATIVE_INFINITY;if(r.includes("devtools"))return Number.NEGATIVE_INFINITY;if(s==="background_page"||s==="service_worker")return Number.NEGATIVE_INFINITY;let o=0;if(n&&n.test(r))o+=1000;if(s==="app")o+=120;else if(s==="webview")o+=100;else if(s==="page")o+=80;else if(s==="iframe")o+=20;if(t.startsWith("http://localhost")||t.startsWith("https://localhost"))o+=90;if(t.startsWith("file://"))o+=60;if(t.startsWith("http://127.0.0.1")||t.startsWith("https://127.0.0.1"))o+=50;if(t.startsWith("about:blank"))o-=120;if(t===""||t==="about:blank")o-=40;if(i&&i!=="devtools")o+=25;const d=Object.values(k()).map((a)=>(a.displayName??a.processName).toLowerCase());for(const a of d)if(i.includes(a)){o+=120;break}for(const a of d)if(t.includes(a)){o+=100;break}return o}function T(e){const n=e?.trim();if(!n)return;return new RegExp(M(n.toLowerCase()))}function M(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:g,scoreCDPTarget:w};function q(e){return new Promise((n,s)=>{const t=new URL(e),i=(t.protocol==="https:"?y:_)(t,(r)=>{const o=r.statusCode??0;if(o<200||o>=300){r.resume();s(Error(`Failed to fetch CDP targets: HTTP ${o}`));return}const d=[];r.on("data",(a)=>d.push(Buffer.isBuffer(a)?a:Buffer.from(a)));r.on("end",()=>{try{n(JSON.parse(Buffer.concat(d).toString("utf8")))}catch(a){s(a instanceof Error?a:Error(String(a)))}})});i.on("error",s);i.setTimeout(1e4,()=>i.destroy(Error("Timed out fetching CDP targets")));i.end()})}
|
|
@@ -39,3 +39,27 @@ export declare function waitForCaptureJs(maxMs: number): string;
|
|
|
39
39
|
* Uses MutationObserver for near-instant resolution; falls back to reject after timeoutMs.
|
|
40
40
|
*/
|
|
41
41
|
export declare function waitForSelectorJs(selector: string, timeoutMs: number): string;
|
|
42
|
+
/**
|
|
43
|
+
* CDP `Input.dispatchKeyEvent` 的按键元数据。
|
|
44
|
+
*
|
|
45
|
+
* 只传 `key` 的 dispatchKeyEvent 是「按下了但什么都没发生」:Chrome 仅在收到 `text`
|
|
46
|
+
* 时才执行**默认编辑动作**,且缺 keyCode 会让依赖 `event.keyCode` 的前端跳过这次事件。
|
|
47
|
+
*
|
|
48
|
+
* 真机 A/B(普通 contenteditable,example.com):
|
|
49
|
+
* 只给 key → `L1` (什么都没发生)
|
|
50
|
+
* 补 code/text/vk → `L1<div><br></div>` (真的换段了)
|
|
51
|
+
*
|
|
52
|
+
* 注意适用范围:**自己监听 keydown 处理按键的富文本编辑器不受影响**——同一组对比在
|
|
53
|
+
* 真 tiptap/ProseMirror 上四种发法都能换段(PM 按 `event.key` 自行处理,不依赖默认
|
|
54
|
+
* 动作)。所以这个补丁修的是「靠浏览器默认行为的场景」:原生 contenteditable、
|
|
55
|
+
* 表单里的 Enter 提交、依赖 keyCode 的老代码。
|
|
56
|
+
*
|
|
57
|
+
* 只覆盖会产生编辑动作或需要 keyCode 的常用键;其余键原样返回空元数据,
|
|
58
|
+
* 行为与补丁前一致。纯函数。
|
|
59
|
+
*/
|
|
60
|
+
export declare function keyEventMetadata(key: string, modifiers?: string[]): {
|
|
61
|
+
code?: string;
|
|
62
|
+
text?: string;
|
|
63
|
+
windowsVirtualKeyCode?: number;
|
|
64
|
+
nativeVirtualKeyCode?: number;
|
|
65
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
function i(e,t){return`
|
|
2
2
|
const ref = ${e};
|
|
3
3
|
let el = document.querySelector('[data-opencli-ref="' + ref + '"]');
|
|
4
4
|
if (!el) el = document.querySelector('[data-ref="' + ref + '"]');
|
|
@@ -12,7 +12,7 @@ function o(e,t){return`
|
|
|
12
12
|
}
|
|
13
13
|
}`}export function clickJs(e){const t=JSON.stringify(e);return`
|
|
14
14
|
(() => {
|
|
15
|
-
${
|
|
15
|
+
${i(t,'a, button, input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])')}
|
|
16
16
|
if (!el) throw new Error('Element not found: ' + ref);
|
|
17
17
|
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
18
18
|
const rect = el.getBoundingClientRect();
|
|
@@ -25,9 +25,9 @@ function o(e,t){return`
|
|
|
25
25
|
return { status: 'js_failed', x, y, w: Math.round(rect.width), h: Math.round(rect.height), error: e.message };
|
|
26
26
|
}
|
|
27
27
|
})()
|
|
28
|
-
`}export function typeTextJs(e,t){const
|
|
28
|
+
`}export function typeTextJs(e,t){const o=JSON.stringify(e),n=JSON.stringify(t);return`
|
|
29
29
|
(() => {
|
|
30
|
-
${o
|
|
30
|
+
${i(o,'input, textarea, [contenteditable="true"]')}
|
|
31
31
|
if (!el) throw new Error('Element not found: ' + ref);
|
|
32
32
|
el.focus();
|
|
33
33
|
if (el.isContentEditable) {
|
|
@@ -56,16 +56,16 @@ function o(e,t){return`
|
|
|
56
56
|
}
|
|
57
57
|
return 'typed';
|
|
58
58
|
})()
|
|
59
|
-
`}export function pressKeyJs(e,t=[]){const
|
|
59
|
+
`}export function pressKeyJs(e,t=[]){const o=t.includes("Ctrl")||t.includes("Control"),n=t.includes("Alt"),c=t.includes("Meta"),r=t.includes("Shift");return`
|
|
60
60
|
(() => {
|
|
61
61
|
const el = document.activeElement || document.body;
|
|
62
62
|
const init = {
|
|
63
63
|
key: ${JSON.stringify(e)},
|
|
64
64
|
bubbles: true,
|
|
65
|
-
ctrlKey: ${
|
|
65
|
+
ctrlKey: ${o},
|
|
66
66
|
altKey: ${n},
|
|
67
|
-
metaKey: ${
|
|
68
|
-
shiftKey: ${
|
|
67
|
+
metaKey: ${c},
|
|
68
|
+
shiftKey: ${r},
|
|
69
69
|
};
|
|
70
70
|
el.dispatchEvent(new KeyboardEvent('keydown', init));
|
|
71
71
|
el.dispatchEvent(new KeyboardEvent('keyup', init));
|
|
@@ -81,7 +81,7 @@ function o(e,t){return`
|
|
|
81
81
|
};
|
|
82
82
|
check();
|
|
83
83
|
})
|
|
84
|
-
`}export function scrollJs(e,t){const
|
|
84
|
+
`}export function scrollJs(e,t){const o=e==="left"?-t:e==="right"?t:0,n=e==="up"?-t:e==="down"?t:0;return`window.scrollBy(${o}, ${n})`}export function autoScrollJs(e,t){return`
|
|
85
85
|
(async () => {
|
|
86
86
|
if (!document.body) return;
|
|
87
87
|
for (let i = 0; i < ${e}; i++) {
|
|
@@ -163,4 +163,4 @@ function o(e,t){return`
|
|
|
163
163
|
});
|
|
164
164
|
obs.observe(document.body || document.documentElement, { childList: true, subtree: true });
|
|
165
165
|
})
|
|
166
|
-
`}
|
|
166
|
+
`}export function keyEventMetadata(e,t=[]){const o=t.some((r)=>r==="Ctrl"||r==="Control"||r==="Alt"||r==="Meta"),c={Enter:{code:"Enter",vk:13,text:"\r"},Tab:{code:"Tab",vk:9,text:"\t"},Backspace:{code:"Backspace",vk:8},Delete:{code:"Delete",vk:46},Escape:{code:"Escape",vk:27},ArrowUp:{code:"ArrowUp",vk:38},ArrowDown:{code:"ArrowDown",vk:40},ArrowLeft:{code:"ArrowLeft",vk:37},ArrowRight:{code:"ArrowRight",vk:39},Home:{code:"Home",vk:36},End:{code:"End",vk:35},PageUp:{code:"PageUp",vk:33},PageDown:{code:"PageDown",vk:34}," ":{code:"Space",vk:32,text:" "},Space:{code:"Space",vk:32,text:" "}}[e];if(c)return{code:c.code,windowsVirtualKeyCode:c.vk,nativeVirtualKeyCode:c.vk,...c.text&&!o?{text:c.text}:{}};if(e.length===1){const r=e.toUpperCase(),s=/[A-Z0-9]/.test(r)?r.charCodeAt(0):void 0;return{...o?{}:{text:e},...s?{windowsVirtualKeyCode:s,nativeVirtualKeyCode:s}:{}}}return{}}
|
|
@@ -33,6 +33,7 @@ export declare class Page extends BasePage {
|
|
|
33
33
|
waitUntil?: 'load' | 'none';
|
|
34
34
|
settleMs?: number;
|
|
35
35
|
}): Promise<void>;
|
|
36
|
+
private _navigateOnce;
|
|
36
37
|
/** Get the active page identity (targetId) */
|
|
37
38
|
getActivePage(): string | undefined;
|
|
38
39
|
/** Bind this Page instance to a specific page identity (targetId). */
|
package/dist/src/browser/page.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import{sendCommand as a,sendCommandFull as
|
|
2
|
-
${
|
|
1
|
+
import{sendCommand as a,sendCommandFull as l}from"./daemon-client.js";import{buildEvaluateExpression as p}from"./utils.js";import{saveBase64ToFile as g}from"../utils.js";import{generateStealthJs as w,generateBeforeUnloadGuardJs as _}from"./stealth.js";import{waitForDomStableJs as k,keyEventMetadata as b}from"./dom-helpers.js";import{BasePage as v}from"./base-page.js";import{classifyBrowserError as h}from"./errors.js";import{BrowserConnectError as C}from"../errors.js";import{log as f}from"../logger.js";function m(t){if(t===null||typeof t!=="object")return!1;const{href:e,origin:s}=t;if(typeof e!=="string")return!1;return e===""||e==="about:blank"||s==="null"}function O(t,e){const s=e?.href;return new C(`Navigation to ${t} reported success but the tab is still on a blank page (${typeof s==="string"&&s?s:"about:blank"}).`,"The browser never loaded the page, so any request made from it would fail as a cross-origin error. Check that the target site is reachable in this browser.","command-failed")}function y(t){const s=(t instanceof Error?t.message:String(t)).toLowerCase();return s.includes("unknown action")&&s.includes("network-capture")||s.includes("network capture")&&s.includes("not supported")}function x(t){const e=t instanceof Error?t.message:String(t);return e.includes("stale page identity")||/^Page not found:\s*\S+\s*$/.test(e)}export class Page extends v{session;contextId;windowMode;surface;siteSession;_idleTimeout;constructor(t,e,s,n,r="browser",o){super();this.session=t;this.contextId=s;this.windowMode=n;this.surface=r;this.siteSession=o;this._idleTimeout=e}_page;_networkCaptureUnsupported=!1;_networkCaptureWarned=!1;_sessionOpts(){return{session:this.session,surface:this.surface,...this.contextId&&{contextId:this.contextId},...this._idleTimeout!=null&&{idleTimeout:this._idleTimeout},...this.windowMode&&{windowMode:this.windowMode},...this.siteSession&&{siteSession:this.siteSession}}}_cmdOpts(){return{session:this.session,surface:this.surface,...this.contextId&&{contextId:this.contextId},...this._page!==void 0&&{page:this._page},...this._idleTimeout!=null&&{idleTimeout:this._idleTimeout},...this.windowMode&&{windowMode:this.windowMode},...this.siteSession&&{siteSession:this.siteSession}}}async goto(t,e){const s=await this._navigateOnce(t,e);if(!m(s))return;f.warn(`Navigation to ${t} left the tab on a blank page; retrying once.`);const n=await this._navigateOnce(t,e);if(m(n))throw O(t,n)}async _navigateOnce(t,e){try{await a("exec",{code:_(),...this._cmdOpts()})}catch{}let s;try{s=await l("navigate",{url:t,...this._cmdOpts()})}catch(n){if(!x(n)||this._page===void 0)throw n;this._page=void 0;s=await l("navigate",{url:t,...this._cmdOpts()})}if(s.page)this._page=s.page;this._lastUrl=t;if(e?.waitUntil!=="none"){const n=e?.settleMs??1000,o={code:`${w()};
|
|
2
|
+
(${k(n,Math.min(500,n))}).then(function(reason){ return { reason: reason, href: location.href, origin: location.origin }; })`,...this._cmdOpts()};let c;try{c=await a("exec",o)}catch(i){const u=h(i);if(u.kind!=="target-navigation")throw i;try{await new Promise((d)=>setTimeout(d,u.delayMs));c=await a("exec",o)}catch(d){if(h(d).kind!=="target-navigation")throw d}}return c}try{await a("exec",{code:w(),...this._cmdOpts()})}catch{}return}getActivePage(){return this._page}setActivePage(t){this._page=t;this._lastUrl=null}_markUnsupportedNetworkCapture(){this._networkCaptureUnsupported=!0;if(this._networkCaptureWarned)return;this._networkCaptureWarned=!0;f.warn("Browser Bridge extension does not support network capture; continuing without it. Explore output may miss API endpoints until you reload or reinstall the extension.")}async evaluate(t,...e){const s=p(t,e);try{return await a("exec",{code:s,...this._cmdOpts()})}catch(n){const r=h(n);if(r.kind!=="target-navigation")throw n;await new Promise((o)=>setTimeout(o,r.delayMs));return a("exec",{code:s,...this._cmdOpts()})}}async getCookies(t={}){const e=await a("cookies",{...this._sessionOpts(),...t});return Array.isArray(e)?e:[]}async closeWindow(){try{await a("close-window",{...this._sessionOpts()})}catch{}finally{this._page=void 0;this._lastUrl=null;this._networkCaptureUnsupported=!1;this._networkCaptureWarned=!1}}async tabs(){const t=await a("tabs",{op:"list",...this._sessionOpts()});return Array.isArray(t)?t:[]}async newTab(t){const e=await l("tabs",{op:"new",...t!==void 0&&{url:t},...this._sessionOpts()});this._lastUrl=null;return e.page}async closeTab(t){const e={op:"close",...this._sessionOpts()};if(typeof t==="number")e.index=t;else if(typeof t==="string")e.page=t;else if(this._page!==void 0)e.page=this._page;const s=await a("tabs",e),n=typeof s?.closed==="string"?s.closed:void 0;if(n&&n===this._page||!n&&(t===void 0||t===this._page)){this._page=void 0;this._lastUrl=null}}async selectTab(t){const e=await l("tabs",{op:"select",...typeof t==="number"?{index:t}:{page:t},...this._sessionOpts()});if(e.page)this._page=e.page;this._lastUrl=null}async screenshot(t={}){const e=await a("screenshot",{...this._cmdOpts(),format:t.format,quality:t.quality,fullPage:t.fullPage,width:t.width,height:t.height});if(t.path)await g(e,t.path);return e}async startNetworkCapture(t=""){if(this._networkCaptureUnsupported)return!1;try{await a("network-capture-start",{pattern:t,...this._cmdOpts()});return!0}catch(e){if(!y(e))throw e;this._markUnsupportedNetworkCapture();return!1}}async readNetworkCapture(){if(this._networkCaptureUnsupported)return[];try{const t=await a("network-capture-read",{...this._cmdOpts()});return Array.isArray(t)?t:[]}catch(t){if(!y(t))throw t;this._markUnsupportedNetworkCapture();return[]}}async waitForDownload(t="",e=30000){return await a("wait-download",{pattern:t,timeoutMs:e,...this._cmdOpts()})}async setFileInput(t,e){if(!(await a("set-file-input",{files:t,selector:e,...this._cmdOpts()}))?.count)throw Error("setFileInput returned no count — command may not be supported by the extension")}async insertText(t){if(!(await a("insert-text",{text:t,...this._cmdOpts()}))?.inserted)throw Error("insertText returned no inserted flag — command may not be supported by the extension")}async frames(){const t=await a("frames",{...this._cmdOpts()});return Array.isArray(t)?t:[]}async evaluateInFrame(t,e){const s=p(t);return a("exec",{code:s,frameIndex:e,...this._cmdOpts()})}async cdp(t,e={}){return a("cdp",{cdpMethod:t,cdpParams:e,...this._cmdOpts()})}async handleJavaScriptDialog(t,e){await this.cdp("Page.handleJavaScriptDialog",{accept:t,...e!==void 0&&{promptText:e}})}async tryNativeClick(t,e){try{await this.nativeClick(t,e);return!0}catch{return!1}}async clickWithQuads(t){const e=JSON.stringify(t),s=`[data-opencli-ref="${t.replace(/"/g,"\\\"")}"]`;await this.evaluate(`
|
|
3
3
|
(() => {
|
|
4
4
|
const el = document.querySelector('[data-opencli-ref="' + ${e} + '"]');
|
|
5
5
|
if (el) el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
6
6
|
return !!el;
|
|
7
7
|
})()
|
|
8
|
-
`);try{const
|
|
8
|
+
`);try{const n=await this.cdp("DOM.getDocument",{}),r=await this.cdp("DOM.querySelectorAll",{nodeId:n.root.nodeId,selector:s});if(!r.nodeIds?.length)throw Error("DOM node not found");const o=r.nodeIds[0];try{const c=await this.cdp("DOM.getContentQuads",{nodeId:o});if(c.quads?.length){const i=c.quads[0],u=(i[0]+i[2]+i[4]+i[6])/4,d=(i[1]+i[3]+i[5]+i[7])/4;await this.nativeClick(Math.round(u),Math.round(d));return}}catch{}try{const c=await this.cdp("DOM.getBoxModel",{nodeId:o});if(c.model?.content){const i=c.model.content,u=(i[0]+i[2]+i[4]+i[6])/4,d=(i[1]+i[3]+i[5]+i[7])/4;await this.nativeClick(Math.round(u),Math.round(d));return}}catch{}}catch{}await this.evaluate(`
|
|
9
9
|
(() => {
|
|
10
10
|
const el = document.querySelector('[data-opencli-ref="' + ${e} + '"]');
|
|
11
11
|
if (!el) throw new Error('Element not found: ' + ${e});
|
|
12
12
|
el.click();
|
|
13
13
|
return 'clicked';
|
|
14
14
|
})()
|
|
15
|
-
`)}async nativeClick(t,e){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:t,y:e});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:t,y:e,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:t,y:e,button:"left",clickCount:1})}async nativeType(t){await this.cdp("Input.insertText",{text:t})}async nativeKeyPress(t,e=[]){let s=0;for(const
|
|
15
|
+
`)}async nativeClick(t,e){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:t,y:e});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:t,y:e,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:t,y:e,button:"left",clickCount:1})}async nativeType(t){await this.cdp("Input.insertText",{text:t})}async nativeKeyPress(t,e=[]){let s=0;for(const r of e){if(r==="Alt")s|=1;if(r==="Ctrl"||r==="Control")s|=2;if(r==="Meta")s|=4;if(r==="Shift")s|=8}const n=b(t,e);await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:t,modifiers:s,...n});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:t,modifiers:s,...n.code?{code:n.code}:{},...n.windowsVirtualKeyCode?{windowsVirtualKeyCode:n.windowsVirtualKeyCode,nativeVirtualKeyCode:n.windowsVirtualKeyCode}:{}})}}
|
package/dist/src/cli.js
CHANGED
|
@@ -129,4 +129,4 @@ cli({
|
|
|
129
129
|
✓ Adapter matches fixture (${x(r,n)}).`);const I=checkSiteMemory(r);printSiteMemoryReport(I,t.strictMemory);if(!I.ok&&t.strictMemory)process.exitCode=f.GENERIC_ERROR;return}console.log(`
|
|
130
130
|
✗ Adapter output does not match fixture:`);for(const I of te.slice(0,20)){const H=I.rowIndex!==void 0?`row[${I.rowIndex}] `:"";console.log(` - [${I.rule}] ${H}${I.detail}`)}if(te.length>20)console.log(` ... and ${te.length-20} more failure(s)`);process.exitCode=f.GENERIC_ERROR}catch(o){console.error(`Error: ${o instanceof Error?o.message:String(o)}`);process.exitCode=f.GENERIC_ERROR}});g.command("close").description("Release the current browser session tab lease").action(S(async(e)=>{await e.closeWindow?.();console.log("Browser session tab lease released")}));i.command("doctor").description("Diagnose ppcli browser bridge connectivity").option("-v, --verbose","Debug output").action(async(e)=>{br(e);const{runBrowserDoctor:t,renderBrowserDoctorReport:o}=await import("./doctor.js"),r=await t({cliVersion:Ee});console.log(o(r))});i.command("completion").description("Output shell completion script").argument("<shell>","Shell type: bash, zsh, or fish").action((e)=>{lo(e)});const se=i.command("plugin").description("Manage ppcli plugins"),Vt=se.description();se.command("install").description("Install a plugin from a git repository").argument("<source>","Plugin source (e.g. github:user/repo)").action(async(e)=>{const{installPlugin:t}=await import("./plugin.js"),{discoverPlugins:o}=await import("./discovery.js");try{const r=t(e);await o();if(Array.isArray(r))if(r.length===0)console.log("No plugins were installed (all skipped or incompatible).");else console.log(`✅ Installed ${r.length} plugin(s) from monorepo: ${r.join(", ")}`);else console.log(`✅ Plugin "${r}" installed successfully. Commands are ready to use.`)}catch(r){console.error(`Error: ${M(r)}`);process.exitCode=f.GENERIC_ERROR}});se.command("uninstall").description("Uninstall a plugin").argument("<name>","Plugin name").action(async(e)=>{const{uninstallPlugin:t}=await import("./plugin.js");try{t(e);console.log(`✅ Plugin "${e}" uninstalled.`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.GENERIC_ERROR}});se.command("update").description("Update a plugin (or all plugins) to the latest version").argument("[name]","Plugin name (required unless --all is passed)").option("--all","Update all installed plugins").action(async(e,t)=>{if(!e&&!t.all){console.error("Error: Please specify a plugin name or use the --all flag.");process.exitCode=f.USAGE_ERROR;return}if(e&&t.all){console.error("Error: Cannot specify both a plugin name and --all.");process.exitCode=f.USAGE_ERROR;return}const{updatePlugin:o,updateAllPlugins:r}=await import("./plugin.js"),{discoverPlugins:n}=await import("./discovery.js");if(t.all){const s=r();if(s.length>0)await n();let l=!1;console.log(" Update Results:");for(const d of s){if(d.success){console.log(` ✓ ${d.name}`);continue}l=!0;console.log(` ✗ ${d.name} — ${String(d.error)}`)}if(s.length===0){console.log(" No plugins installed.");return}console.log();if(l){console.error("Completed with some errors.");process.exitCode=f.GENERIC_ERROR}else console.log("✅ All plugins updated successfully.");return}try{o(e);await n();console.log(`✅ Plugin "${e}" updated successfully.`)}catch(s){console.error(`Error: ${M(s)}`);process.exitCode=f.GENERIC_ERROR}});se.command("list").description("List installed plugins").option("-f, --format <fmt>","Output format: table, json","table").action(async(e)=>{const{listPlugins:t}=await import("./plugin.js"),o=t();if(o.length===0){console.log(" No plugins installed.");console.log(" Install one with: ppcli plugin install github:user/repo");return}if(e.format==="json"){xe(o,{fmt:"json",columns:["name","commands","source"],title:"ppcli/plugins",source:"ppcli plugin list"});return}console.log();console.log(" Installed plugins");console.log();const r=o.filter((s)=>!s.monorepoName),n=new Map;for(const s of o){if(!s.monorepoName)continue;const l=n.get(s.monorepoName)??[];l.push(s);n.set(s.monorepoName,l)}for(const s of r){const l=s.version?` @${s.version}`:"",d=s.description?` — ${s.description}`:"",u=s.commands.length>0?` (${s.commands.join(", ")})`:"",m=s.source?` ← ${s.source}`:"";console.log(` ${s.name}${l}${d}${u}${m}`)}for(const[s,l]of n){console.log();console.log(` \uD83D\uDCE6 ${s} (monorepo)`);for(const d of l){const u=d.version?` @${d.version}`:"",m=d.description?` — ${d.description}`:"",w=d.commands.length>0?` (${d.commands.join(", ")})`:"";console.log(` ${d.name}${u}${m}${w}`)}}console.log();console.log(` ${o.length} plugin(s) installed`);console.log()});se.command("create").description("Create a new plugin scaffold").argument("<name>","Plugin name (lowercase, hyphens allowed)").option("-d, --dir <path>","Output directory (default: ./<name>)").option("--description <text>","Plugin description").action(async(e,t)=>{const{createPluginScaffold:o}=await import("./plugin-scaffold.js");try{const r=o(e,{dir:t.dir,description:t.description});console.log(`✅ Plugin scaffold created at ${r.dir}`);console.log();console.log(" Files created:");for(const n of r.files)console.log(` ${n}`);console.log();console.log(" Next steps:");console.log(` cd ${r.dir}`);console.log(` ppcli plugin install file://${r.dir}`);console.log(` ppcli ${e} hello`)}catch(r){console.error(`Error: ${M(r)}`);process.exitCode=f.GENERIC_ERROR}});const ye=i.command("adapter").description("Manage CLI adapters"),qt=ye.description();ye.command("status").description("Show which sites have local overrides vs using official baseline").action(async()=>{const e=await import("node:os"),t=k.join(e.homedir(),".ppcli","clis"),o=a;try{const n=(await O.promises.readdir(t,{withFileTypes:!0})).filter((l)=>l.isDirectory()).map((l)=>l.name).sort();let s=[];try{s=(await O.promises.readdir(o,{withFileTypes:!0})).filter((d)=>d.isDirectory()).map((d)=>d.name).sort()}catch{}if(n.length===0){console.log("No local adapter overrides. All sites use the official baseline.");return}console.log(`Local overrides in ~/.ppcli/clis/ (${n.length} sites):
|
|
131
131
|
`);for(const l of n){const u=s.includes(l)?"override":"custom";console.log(` ${l} [${u}]`)}console.log(`
|
|
132
|
-
Official baseline: ${s.length} sites in package`)}catch{console.log("No local adapter overrides. All sites use the official baseline.")}});ye.command("eject").description("Copy an official adapter to ~/.ppcli/clis/ for local editing").argument("<site>","Site name (e.g. twitter, bilibili)").action(async(e)=>{const t=await import("node:os"),o=k.join(t.homedir(),".ppcli","clis"),r=k.join(a,e),n=k.join(o,e);try{await O.promises.access(r)}catch{console.error(`Error: Site "${e}" not found in official adapters.`);process.exitCode=f.USAGE_ERROR;return}try{await O.promises.access(n);console.error(`Site "${e}" already exists in ~/.ppcli/clis/. Use "ppcli adapter reset ${e}" first to restore official version.`);process.exitCode=f.USAGE_ERROR;return}catch{}O.cpSync(r,n,{recursive:!0});console.log(`✅ Ejected "${e}" to ~/.ppcli/clis/${e}/`);console.log("You can now edit the adapter files. Changes take effect immediately.");console.log("Note: Official updates to this adapter will overwrite your changes.")});ye.command("reset").description("Remove local override and restore official adapter version").argument("[site]","Site name (e.g. twitter, bilibili)").option("--all","Reset all local overrides").action(async(e,t)=>{const o=await import("node:os"),r=k.join(o.homedir(),".ppcli","clis");if(t.all){try{const d=(await O.promises.readdir(r,{withFileTypes:!0})).filter((u)=>u.isDirectory());if(d.length===0){console.log("No local sites to reset.");return}for(const u of d)O.rmSync(k.join(r,u.name),{recursive:!0,force:!0});console.log(`✅ Reset ${d.length} site(s). All adapters now use official baseline.`)}catch{console.log("No local sites to reset.")}return}if(!e){console.error("Error: Please specify a site name or use --all.");process.exitCode=f.USAGE_ERROR;return}const n=k.join(r,e);try{await O.promises.access(n)}catch{console.error(`Site "${e}" has no local override.`);return}const s=O.existsSync(k.join(a,e));O.rmSync(n,{recursive:!0,force:!0});console.log(s?`✅ Reset "${e}". Now using official baseline.`:`✅ Removed custom site "${e}".`)});const U=i.command("profile").description("Manage Browser Bridge Chrome profiles"),Xt=U.description();U.command("list").description("List Chrome profiles: extension-connected and managed (dedicated Chrome instances)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e)=>{const t=await Vo(),o=qo(),r=t?.profiles??[],n=!!t&&!sr(t,Ee)&&Array.isArray(t.profiles),s=await Promise.all(De().map(async({name:u,entry:m})=>({name:u,port:m.port,userDataDir:m.userDataDir,running:await Qo(m),isDefault:o.defaultContextId===u,createdAt:m.createdAt??"",label:m.label??"",proxy:m.proxy?{scheme:m.proxy.scheme,host:m.proxy.host,port:m.proxy.port,hasAuth:!!m.proxy.username,username:m.proxy.username??""}:null,fingerprint:m.fingerprint?.enabled?{enabled:!0,timezone:m.fingerprint.timezone??"",locale:m.fingerprint.locale??"",hardwareConcurrency:m.fingerprint.hardwareConcurrency??0,deviceMemory:m.fingerprint.deviceMemory??0,screen:m.fingerprint.screen??null,webglVendor:m.fingerprint.webglVendor??"",webglRenderer:m.fingerprint.webglRenderer??"",canvasNoise:m.fingerprint.canvasNoise===!0}:{enabled:!1},webrtc:m.webrtc!==!1,clean:m.clean!==!1})));if(e.format==="json"){const u=n?r.map((m)=>({contextId:m.contextId,alias:_t(o,m.contextId)??"",connected:!0,version:m.extensionVersion??"",isDefault:o.defaultContextId===m.contextId})):[];console.log(JSON.stringify({managed:s,extension:u,daemonRunning:!!t,daemonStale:!!t&&!n,defaultProfile:o.defaultContextId??""},null,2));return}if(s.length>0){console.log("Managed profiles (dedicated Chrome instances)");console.log();for(const u of s){const m=u.isDefault?" default":"";console.log(` ${u.name}${m} — ${u.running?`running on port ${u.port}`:"stopped"}`)}console.log()}if(!t){console.log("Daemon is not running. Run ppcli doctor after opening Chrome.");return}if(!n){console.log(`Daemon ${nr(t)} is stale for CLI v${Ee}.`);console.log("Run: ppcli daemon restart");return}if(r.length===0){console.log("No Browser Bridge profiles connected.");console.log("Open a Chrome profile with the ppcli extension installed, then run ppcli profile list again.");return}const l=new Set(r.map((u)=>u.contextId));console.log("Connected Browser Bridge profiles");console.log();for(const u of r){const m=_t(o,u.contextId),w=o.defaultContextId===u.contextId?" default":"",x=m?` ${m}`:"",_=u.extensionVersion?` v${u.extensionVersion}`:" version unknown";console.log(` ${u.contextId}${x}${w} — connected${_}`)}if(s.some((u)=>u.running)){console.log();console.log("注意:托管 Chrome 也装了同一个扩展,上面的某个 contextId 可能就是它,");console.log(" 无法从这份列表分辨。写操作请显式带 --profile <名字>,不要依赖默认路由。")}const d=Object.entries(o.aliases).filter(([,u])=>!l.has(u));if(d.length>0||o.defaultContextId&&!l.has(o.defaultContextId)){console.log();console.log("Disconnected saved profiles:");const u=new Set;for(const[m,w]of d){u.add(w);console.log(` ${w} ${m} — not connected`)}if(o.defaultContextId&&!u.has(o.defaultContextId)&&!l.has(o.defaultContextId))console.log(` ${o.defaultContextId} — default, not connected`)}});const nt=(e)=>{if(e.config){const r=JSON.parse(e.config);if(typeof r.proxy==="string")r.proxy=Be(r.proxy);return r}const t={};if(e.label)t.label=e.label;if(e.proxy)t.proxy=Be(e.proxy);if(e.webrtc===!1)t.webrtc=!1;if(e.clean===!1)t.clean=!1;const o=e.fingerprint;if(o&&o!=="off")t.fingerprint={enabled:!0,spoof:o!=="lite",...e.fpTimezone?{timezone:e.fpTimezone}:{},...e.fpLocale?{locale:e.fpLocale}:{},...e.fpUa?{userAgent:e.fpUa}:{},...e.fpPlatform?{platform:e.fpPlatform}:{},...e.fpSeed?{seed:Number(e.fpSeed)}:{}};return t},st=(e)=>e?{scheme:e.scheme,host:e.host,port:e.port,hasAuth:!!e.username,username:e.username??""}:null,it=(e)=>e?.enabled?{enabled:!0,seed:e.seed,timezone:e.timezone??"",locale:e.locale??"",userAgent:e.userAgent??"",platform:e.platform??"",hardwareConcurrency:e.hardwareConcurrency??0,deviceMemory:e.deviceMemory??0,screen:e.screen??null,webglVendor:e.webglVendor??"",webglRenderer:e.webglRenderer??"",canvasNoise:e.canvasNoise===!0}:{enabled:!1},at=(e)=>e.option("--label <text>","Display label / note for the environment").option("--proxy <url>","Per-env proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("--fingerprint <mode>","Fingerprint mode: off | on (spoof soft signals) | lite (timezone/locale follow only)").option("--fp-timezone <iana>","Fingerprint timezone, e.g. America/New_York (must be set explicitly)").option("--fp-locale <locale>","Fingerprint locale, e.g. en-US (must be set explicitly)").option("--fp-ua <ua>","Override navigator.userAgent (use with care)").option("--fp-platform <p>","Override navigator.platform, e.g. Win32").option("--fp-seed <n>","Fixed fingerprint seed (for reproducible fingerprint)").option("--no-webrtc","Disable WebRTC leak protection (on by default)").option("--no-clean","Disable lean launch flags / telemetry suppression (on by default)").option("--config <json>","Full config as JSON: { label, proxy, fingerprint } — overrides granular flags");U.command("create").description("Create a managed profile: a dedicated Chrome instance with its own user data dir (for a second account)").argument("<name>","Profile name, e.g. work / brand2").option("--browser-path <path>","Explicit browser executable (defaults to auto-discovered Chrome)").option("--no-start","Create only; do not launch Chrome now").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=nt(t),{opts:r}=await $t({...o,browserPath:t.browserPath}),n=await Yo(e,{...r,browserPath:t.browserPath});if(t.start!==!1)await Ge(e,n);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,port:n.port,userDataDir:n.userDataDir,label:n.label??"",proxy:st(n.proxy),fingerprint:it(n.fingerprint),webrtc:n.webrtc!==!1,clean:n.clean!==!1,started:t.start!==!1},null,2));return}console.log(`✅ 托管 profile "${e}" 已创建(端口 ${n.port})`);console.log(` 数据目录:${n.userDataDir}`);if(n.proxy)console.log(` 代理:${n.proxy.scheme}://${n.proxy.host}:${n.proxy.port}`);if(n.fingerprint?.enabled)console.log(` 指纹:已开启${n.fingerprint.timezone?` 时区 ${n.fingerprint.timezone}`:""}${n.fingerprint.locale?` 语言 ${n.fingerprint.locale}`:""}`);if(t.start!==!1)console.log(` Chrome 已启动。用 ppcli <site> login --profile ${e} 在新窗口里登录账号。`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});at(U.commands[U.commands.length-1]);U.command("start").description("Launch the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=De().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在,先 ppcli profile create ${e}`);const o=await Ge(t.name,t.entry);console.log(`✅ profile "${e}" 运行中(${o})`)}catch(t){console.error(`Error: ${M(t)}`);process.exitCode=f.USAGE_ERROR}});U.command("stop").description("Gracefully close the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=De().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在`);const o=await tr(t.entry);console.log(o?`✅ profile "${e}" 已关闭`:`profile "${e}" 本来就没在运行`)}catch(t){console.error(`Error: ${M(t)}`);process.exitCode=f.USAGE_ERROR}});U.command("remove").description("Remove a managed profile (closes Chrome and deletes its login data)").argument("<name>","Managed profile name").option("--keep-data","Keep the user data dir on disk",!1).action(async(e,t)=>{try{await er(e,{keepData:t.keepData===!0});console.log(`✅ 托管 profile "${e}" 已删除${t.keepData?"(保留数据目录)":"(含登录数据)"}`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});U.command("update").description("Reconfigure a managed profile (label / proxy / fingerprint); takes effect on next start").argument("<name>","Managed profile name").option("--clear-proxy","Remove the proxy from this environment").option("--restart","Relaunch the environment after updating",!1).option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=nt(t),{opts:r}=await $t(o),n={};if(o.label!==void 0)n.label=o.label??null;if(t.clearProxy)n.proxy=null;else if(r.proxy)n.proxy=r.proxy;if(t.fingerprint==="off")n.fingerprint=null;else if(r.fingerprint)n.fingerprint=r.fingerprint;if(o.webrtc!==void 0)n.webrtc=o.webrtc;if(o.clean!==void 0)n.clean=o.clean;const{entry:s,wasRunning:l}=await or(e,n);if(t.restart&&l)await Ge(e,s);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,label:s.label??"",proxy:st(s.proxy),fingerprint:it(s.fingerprint),webrtc:s.webrtc!==!1,clean:s.clean!==!1,wasRunning:l,restarted:!!(t.restart&&l)},null,2));return}console.log(`✅ 托管 profile "${e}" 配置已更新${l?"(已关闭,重启后生效)":""}`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});at(U.commands[U.commands.length-1]);U.command("test-proxy").description("Test proxy connectivity and detect exit IP / country / timezone").argument("<url>","Proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=Be(e),r=await rr(o);if(t.format==="json"){console.log(JSON.stringify(r,null,2));return}if(r.ok){console.log(`✅ 代理可用 — 出口 IP ${r.ip}`);console.log(` 位置:${r.country??"?"}${r.city?" / "+r.city:""}`);console.log(` 时区:${r.timezone??"?"} 语言:${r.locale??"?"}`)}else{console.log(`❌ 代理不可用:${r.error}`);process.exitCode=f.USAGE_ERROR}}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});U.command("rename").description("Assign a local alias to a connected Browser Bridge profile").argument("<contextId>","Profile contextId from ppcli profile list").argument("<alias>","Local alias, e.g. work or personal").action((e,t)=>{try{Xo(e,t);console.log(`Profile ${e} is now aliased as ${t}.`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});U.command("use").description("Set the default Browser Bridge profile for future commands").argument("<profile>","Profile alias or contextId").action((e)=>{try{const t=Zo(e);console.log(`Default Browser Bridge profile: ${t.defaultContextId??e}`)}catch(t){console.error(`Error: ${M(t)}`);process.exitCode=f.USAGE_ERROR}});const ue=i.command("daemon").description("Manage the ppcli daemon"),Kt=ue.description();ue.command("status").description("Show daemon status").action(async()=>{await Fo()});ue.command("stop").description("Stop the daemon").action(async()=>{await Lo()});ue.command("restart").description("Restart the daemon").action(async()=>{await Bo()});ue.command("warm").description("Pre-open the background automation window (keeps a tab alive for reuse)").option("--url <url>","http(s) info page to keep in the warm tab (falls back to about:blank)").action(async(e)=>{await zo(e.url)});const be=Me(),Te=i.command("external").description("Manage external CLI passthrough commands");Te.command("install").description("Install an external CLI").argument("<name>","Name of the external CLI").action((e)=>{const t=be.find((o)=>o.name===e);if(!t){console.error(`External CLI '${e}' not found in registry.`);process.exitCode=f.USAGE_ERROR;return}mo(t)});Te.command("register").description("Register an external CLI").argument("<name>","Name of the CLI").option("--binary <bin>","Binary name if different from name").option("--install <cmd>","Auto-install command").option("--desc <text>","Description").action((e,t)=>{po(e,{binary:t.binary,install:t.install,description:t.desc})});Te.command("list").description("List registered external CLIs").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((e)=>{const t=Me().map((o)=>({name:o.name,package:o.package??"",binary:o.binary,installed:je(o.binary),description:o.description??"",homepage:o.homepage??"",tags:o.tags?.join(", ")??""}));xe(t,{fmt:e.format,columns:["name","package","binary","installed","description","homepage","tags"],title:"ppcli/external/list",source:"ppcli external list"})});function Zt(e,t){const o=t??(()=>{const r=process.argv.indexOf(e);return process.argv.slice(r+1)})();try{uo(e,o,be)}catch(r){if(r instanceof Ue){process.stderr.write(so.dump(vo(r),{sortKeys:!1,lineWidth:120,noRefs:!0}));process.exitCode=r.exitCode}else{console.error(`Error: ${M(r)}`);process.exitCode=f.GENERIC_ERROR}}}for(const e of be){if(i.commands.some((t)=>t.name()===e.name))continue;i.command(e.name).description(`(External) ${e.description||e.name}`).argument("[args...]").allowUnknownOption().passThroughOptions().helpOption(!1).action((t)=>Zt(e.name,t))}const ct=i.command("antigravity").description("antigravity commands");ct.command("serve").description("Start Anthropic-compatible API proxy for Antigravity").option("--port <port>","Server port (default: 8082)","8082").option("--timeout <seconds>","Maximum time to wait for a reply (default: 120s)").action(async(e)=>{const{startServe:t}=await import("../../clis/antigravity/serve.js");await t({port:parseInt(e.port,10),timeout:e.timeout?Bt(e.timeout,"--timeout",120):void 0})});const lt=new Map;lt.set("antigravity",ct);const dt=ho(i,lt);xr(i);const Yt=be.map((e)=>e.name),Qt=be.map((e)=>({name:e.name,label:wt(e)})),Ie=new Map;for(const[,e]of Se())if(!Ie.has(e.site))Ie.set(e.site,e.domain);const ut=[],mt=[];for(const e of dt)if(yt(Ie.get(e))==="app")ut.push(e);else mt.push(e);const pt={external:Qt,apps:ut,sites:mt},eo=new Set([...Yt,...dt]);pe(g,{globalCommand:i,description:N});pe(b,{globalCommand:i,description:"Inspect website login status"});pe(ue,{globalCommand:i,description:Kt});pe(se,{globalCommand:i,description:Vt});pe(ye,{globalCommand:i,description:qt});pe(U,{globalCommand:i,description:Xt});i.configureHelp({visibleCommands:(e)=>e.commands.filter((t)=>e!==i||!eo.has(t.name()))});const to=(e)=>{const t=[];let o=e.parent;while(o){const r=bo(o);t.unshift(r?`${o.name()} ${r}`:o.name());o=o.parent}return[...t,e.name(),e.usage()].filter(Boolean).join(" ").trim()};function ft(e){e.configureHelp({commandUsage:to});for(const t of e.commands)ft(t)}ft(g);yo(i,()=>So(i,pt),()=>wo(pt));i.on("command:*",(e)=>{const t=e[0];console.error(`error: unknown command '${t}'`);if(je(t))console.error(` Tip: '${t}' exists on your PATH. Use 'ppcli external register ${t}' to add it as an external CLI.`);i.outputHelp();process.exitCode=f.USAGE_ERROR});return i}export function runCli(a,c){createProgram(a,c).parse()}export{findPackageRoot};export function resolveBrowserVerifyInvocation(a={}){const c=a.platform??process.platform,i=a.fileExists??O.existsSync,p=a.readFile??((R)=>O.readFileSync(R,"utf-8")),b=a.projectRoot??findPackageRoot(Le,i);for(const R of io(b,p))if(i(R))return{binary:process.execPath,args:[R],cwd:b};const g=k.join(b,"src","main.ts");if(!i(g))throw Error(`Could not find ppcli entrypoint under ${b}. Expected built entry from package.json or src/main.ts.`);const N=k.join(b,"node_modules",".bin",c==="win32"?"tsx.cmd":"tsx");if(i(N))return{binary:N,args:[g],cwd:b,...c==="win32"?{shell:!0}:{}};return{binary:c==="win32"?"npx.cmd":"npx",args:["tsx",g],cwd:b,...c==="win32"?{shell:!0}:{}}}
|
|
132
|
+
Official baseline: ${s.length} sites in package`)}catch{console.log("No local adapter overrides. All sites use the official baseline.")}});ye.command("eject").description("Copy an official adapter to ~/.ppcli/clis/ for local editing").argument("<site>","Site name (e.g. twitter, bilibili)").action(async(e)=>{const t=await import("node:os"),o=k.join(t.homedir(),".ppcli","clis"),r=k.join(a,e),n=k.join(o,e);try{await O.promises.access(r)}catch{console.error(`Error: Site "${e}" not found in official adapters.`);process.exitCode=f.USAGE_ERROR;return}try{await O.promises.access(n);console.error(`Site "${e}" already exists in ~/.ppcli/clis/. Use "ppcli adapter reset ${e}" first to restore official version.`);process.exitCode=f.USAGE_ERROR;return}catch{}O.cpSync(r,n,{recursive:!0});console.log(`✅ Ejected "${e}" to ~/.ppcli/clis/${e}/`);console.log("You can now edit the adapter files. Changes take effect immediately.");console.log("Note: Official updates to this adapter will overwrite your changes.")});ye.command("reset").description("Remove local override and restore official adapter version").argument("[site]","Site name (e.g. twitter, bilibili)").option("--all","Reset all local overrides").action(async(e,t)=>{const o=await import("node:os"),r=k.join(o.homedir(),".ppcli","clis");if(t.all){try{const d=(await O.promises.readdir(r,{withFileTypes:!0})).filter((u)=>u.isDirectory());if(d.length===0){console.log("No local sites to reset.");return}for(const u of d)O.rmSync(k.join(r,u.name),{recursive:!0,force:!0});console.log(`✅ Reset ${d.length} site(s). All adapters now use official baseline.`)}catch{console.log("No local sites to reset.")}return}if(!e){console.error("Error: Please specify a site name or use --all.");process.exitCode=f.USAGE_ERROR;return}const n=k.join(r,e);try{await O.promises.access(n)}catch{console.error(`Site "${e}" has no local override.`);return}const s=O.existsSync(k.join(a,e));O.rmSync(n,{recursive:!0,force:!0});console.log(s?`✅ Reset "${e}". Now using official baseline.`:`✅ Removed custom site "${e}".`)});const U=i.command("profile").description("Manage Browser Bridge Chrome profiles"),Xt=U.description();U.command("list").description("List Chrome profiles: extension-connected and managed (dedicated Chrome instances)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e)=>{const t=await Vo(),o=qo(),r=t?.profiles??[],n=!!t&&!sr(t,Ee)&&Array.isArray(t.profiles),s=await Promise.all(De().map(async({name:u,entry:m})=>({name:u,port:m.port,userDataDir:m.userDataDir,running:await Qo(m),isDefault:o.defaultContextId===u,createdAt:m.createdAt??"",label:m.label??"",proxy:m.proxy?{scheme:m.proxy.scheme,host:m.proxy.host,port:m.proxy.port,hasAuth:!!m.proxy.username,username:m.proxy.username??""}:null,fingerprint:m.fingerprint?.enabled?{enabled:!0,timezone:m.fingerprint.timezone??"",locale:m.fingerprint.locale??"",hardwareConcurrency:m.fingerprint.hardwareConcurrency??0,deviceMemory:m.fingerprint.deviceMemory??0,screen:m.fingerprint.screen??null,webglVendor:m.fingerprint.webglVendor??"",webglRenderer:m.fingerprint.webglRenderer??"",canvasNoise:m.fingerprint.canvasNoise===!0}:{enabled:!1},webrtc:m.webrtc!==!1,clean:m.clean!==!1})));if(e.format==="json"){const u=n?r.map((m)=>({contextId:m.contextId,alias:_t(o,m.contextId)??"",connected:!0,version:m.extensionVersion??"",isDefault:o.defaultContextId===m.contextId})):[];console.log(JSON.stringify({managed:s,extension:u,daemonRunning:!!t,daemonStale:!!t&&!n,defaultProfile:o.defaultContextId??""},null,2));return}if(s.length>0){console.log("Managed profiles (dedicated Chrome instances)");console.log();for(const u of s){const m=u.isDefault?" default":"";console.log(` ${u.name}${m} — ${u.running?`running on port ${u.port}`:"stopped"}`)}console.log()}if(!t){console.log("Daemon is not running. Run ppcli doctor after opening Chrome.");return}if(!n){console.log(`Daemon ${nr(t)} is stale for CLI v${Ee}.`);console.log("Run: ppcli daemon restart");return}if(r.length===0){console.log("No Browser Bridge profiles connected.");console.log("Open a Chrome profile with the ppcli extension installed, then run ppcli profile list again.");return}const l=new Set(r.map((u)=>u.contextId));console.log("Connected Browser Bridge profiles");console.log();for(const u of r){const m=_t(o,u.contextId),w=o.defaultContextId===u.contextId?" default":"",x=m?` ${m}`:"",_=u.extensionVersion?` v${u.extensionVersion}`:" version unknown";console.log(` ${u.contextId}${x}${w} — connected${_}`)}if(s.some((u)=>u.running)){console.log();console.log("注意:托管 Chrome 也装了同一个扩展,上面的某个 contextId 可能就是它,");console.log(" 无法从这份列表分辨。写操作请显式带 --profile <名字>,不要依赖默认路由。")}const d=Object.entries(o.aliases).filter(([,u])=>!l.has(u));if(d.length>0||o.defaultContextId&&!l.has(o.defaultContextId)){console.log();console.log("Disconnected saved profiles:");const u=new Set;for(const[m,w]of d){u.add(w);console.log(` ${w} ${m} — not connected`)}if(o.defaultContextId&&!u.has(o.defaultContextId)&&!l.has(o.defaultContextId))console.log(` ${o.defaultContextId} — default, not connected`)}});const nt=(e)=>{if(e.config){const r=JSON.parse(e.config);if(typeof r.proxy==="string")r.proxy=Be(r.proxy);return r}const t={};if(e.label)t.label=e.label;if(e.proxy)t.proxy=Be(e.proxy);if(e.webrtc===!1)t.webrtc=!1;if(e.clean===!1)t.clean=!1;const o=e.fingerprint;if(o&&o!=="off")t.fingerprint={enabled:!0,spoof:o!=="lite",...e.fpTimezone?{timezone:e.fpTimezone}:{},...e.fpLocale?{locale:e.fpLocale}:{},...e.fpUa?{userAgent:e.fpUa}:{},...e.fpPlatform?{platform:e.fpPlatform}:{},...e.fpSeed?{seed:Number(e.fpSeed)}:{}};return t},st=(e)=>e?{scheme:e.scheme,host:e.host,port:e.port,hasAuth:!!e.username,username:e.username??""}:null,it=(e)=>e?.enabled?{enabled:!0,seed:e.seed,timezone:e.timezone??"",locale:e.locale??"",userAgent:e.userAgent??"",platform:e.platform??"",hardwareConcurrency:e.hardwareConcurrency??0,deviceMemory:e.deviceMemory??0,screen:e.screen??null,webglVendor:e.webglVendor??"",webglRenderer:e.webglRenderer??"",canvasNoise:e.canvasNoise===!0}:{enabled:!1},at=(e)=>e.option("--label <text>","Display label / note for the environment").option("--proxy <url>","Per-env proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("--fingerprint <mode>","Fingerprint mode: off | on (spoof soft signals) | lite (timezone/locale follow only)").option("--fp-timezone <iana>","Fingerprint timezone, e.g. America/New_York (must be set explicitly)").option("--fp-locale <locale>","Fingerprint locale, e.g. en-US (must be set explicitly)").option("--fp-ua <ua>","Override navigator.userAgent (use with care)").option("--fp-platform <p>","Override navigator.platform, e.g. Win32").option("--fp-seed <n>","Fixed fingerprint seed (for reproducible fingerprint)").option("--no-webrtc","Disable WebRTC leak protection (on by default)").option("--no-clean","Disable lean launch flags / telemetry suppression (on by default)").option("--config <json>","Full config as JSON: { label, proxy, fingerprint } — overrides granular flags");U.command("create").description("Create a managed profile: a dedicated Chrome instance with its own user data dir (for a second account)").argument("<name>","Profile name, e.g. work / brand2").option("--browser-path <path>","Explicit browser executable (defaults to auto-discovered Chrome)").option("--no-start","Create only; do not launch Chrome now").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=nt(t),{opts:r}=await $t({...o,browserPath:t.browserPath}),n=await Yo(e,{...r,browserPath:t.browserPath});if(t.start!==!1)await Ge(e,n);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,port:n.port,userDataDir:n.userDataDir,label:n.label??"",proxy:st(n.proxy),fingerprint:it(n.fingerprint),webrtc:n.webrtc!==!1,clean:n.clean!==!1,started:t.start!==!1},null,2));return}console.log(`✅ 托管 profile "${e}" 已创建(端口 ${n.port})`);console.log(` 数据目录:${n.userDataDir}`);if(n.proxy)console.log(` 代理:${n.proxy.scheme}://${n.proxy.host}:${n.proxy.port}`);if(n.fingerprint?.enabled)console.log(` 指纹:已开启${n.fingerprint.timezone?` 时区 ${n.fingerprint.timezone}`:""}${n.fingerprint.locale?` 语言 ${n.fingerprint.locale}`:""}`);if(t.start!==!1)console.log(` Chrome 已启动。用 ppcli <site> login --profile ${e} 在新窗口里登录账号。`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});at(U.commands[U.commands.length-1]);U.command("start").description("Launch the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=De().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在,先 ppcli profile create ${e}`);const o=await Ge(t.name,t.entry);console.log(`✅ profile "${e}" 运行中(${o})`)}catch(t){console.error(`Error: ${M(t)}`);process.exitCode=f.USAGE_ERROR}});U.command("stop").description("Gracefully close the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(e)=>{try{const t=De().find((r)=>r.name===e.trim());if(!t)throw Error(`托管 profile "${e}" 不存在`);const o=await tr(t.entry);console.log(o?`✅ profile "${e}" 已关闭`:`profile "${e}" 本来就没在运行`)}catch(t){console.error(`Error: ${M(t)}`);process.exitCode=f.USAGE_ERROR}});U.command("remove").description("Remove a managed profile (closes Chrome and deletes its login data)").argument("<name>","Managed profile name").option("--keep-data","Keep the user data dir on disk",!1).action(async(e,t)=>{try{await er(e,{keepData:t.keepData===!0});console.log(`✅ 托管 profile "${e}" 已删除${t.keepData?"(保留数据目录)":"(含登录数据)"}`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});U.command("update").description("Reconfigure a managed profile (label / proxy / fingerprint); takes effect on next start").argument("<name>","Managed profile name").option("--clear-proxy","Remove the proxy from this environment").option("--restart","Relaunch the environment after updating",!1).option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=nt(t),{opts:r}=await $t(o),n={};if(o.label!==void 0)n.label=o.label??null;if(t.clearProxy)n.proxy=null;else if(r.proxy)n.proxy=r.proxy;if(t.fingerprint==="off")n.fingerprint=null;else if(r.fingerprint)n.fingerprint=r.fingerprint;if(o.webrtc!==void 0)n.webrtc=o.webrtc;if(o.clean!==void 0)n.clean=o.clean;const{entry:s,wasRunning:l}=await or(e,n);if(t.restart&&l)await Ge(e,s);if(t.format==="json"){console.log(JSON.stringify({ok:!0,name:e,label:s.label??"",proxy:st(s.proxy),fingerprint:it(s.fingerprint),webrtc:s.webrtc!==!1,clean:s.clean!==!1,wasRunning:l,restarted:!!(t.restart&&l)},null,2));return}console.log(`✅ 托管 profile "${e}" 配置已更新${l?"(已关闭,重启后生效)":""}`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});at(U.commands[U.commands.length-1]);U.command("test-proxy").description("Test proxy connectivity and detect exit IP / country / timezone").argument("<url>","Proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("-f, --format <fmt>","Output format: text, json","text").action(async(e,t)=>{try{const o=Be(e),r=await rr(o);if(t.format==="json"){console.log(JSON.stringify(r,null,2));return}if(r.ok){console.log(`✅ 代理可用 — 出口 IP ${r.ip}`);console.log(` 位置:${r.country??"?"}${r.city?" / "+r.city:""}`);console.log(` 时区:${r.timezone??"?"} 语言:${r.locale??"?"}`)}else{console.log(`❌ 代理不可用:${r.error}`);process.exitCode=f.USAGE_ERROR}}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});U.command("rename").description("Assign a local alias to a connected Browser Bridge profile").argument("<contextId>","Profile contextId from ppcli profile list").argument("<alias>","Local alias, e.g. work or personal").action((e,t)=>{try{Xo(e,t);console.log(`Profile ${e} is now aliased as ${t}.`)}catch(o){console.error(`Error: ${M(o)}`);process.exitCode=f.USAGE_ERROR}});U.command("use").description("Set the default Browser Bridge profile for future commands").argument("<profile>","Profile alias or contextId").action((e)=>{try{const t=Zo(e);console.log(`Default Browser Bridge profile: ${t.defaultContextId??e}`)}catch(t){console.error(`Error: ${M(t)}`);process.exitCode=f.USAGE_ERROR}});const ue=i.command("daemon").description("Manage the ppcli daemon"),Kt=ue.description();ue.command("status").description("Show daemon status").option("--json","Machine-readable status (includes bridgeReady)").action(async(e)=>{await Fo(e)});ue.command("stop").description("Stop the daemon").action(async()=>{await Lo()});ue.command("restart").description("Restart the daemon").action(async()=>{await Bo()});ue.command("warm").description("Pre-open the background automation window (keeps a tab alive for reuse)").option("--url <url>","http(s) info page to keep in the warm tab (falls back to about:blank)").action(async(e)=>{await zo(e.url)});const be=Me(),Te=i.command("external").description("Manage external CLI passthrough commands");Te.command("install").description("Install an external CLI").argument("<name>","Name of the external CLI").action((e)=>{const t=be.find((o)=>o.name===e);if(!t){console.error(`External CLI '${e}' not found in registry.`);process.exitCode=f.USAGE_ERROR;return}mo(t)});Te.command("register").description("Register an external CLI").argument("<name>","Name of the CLI").option("--binary <bin>","Binary name if different from name").option("--install <cmd>","Auto-install command").option("--desc <text>","Description").action((e,t)=>{po(e,{binary:t.binary,install:t.install,description:t.desc})});Te.command("list").description("List registered external CLIs").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((e)=>{const t=Me().map((o)=>({name:o.name,package:o.package??"",binary:o.binary,installed:je(o.binary),description:o.description??"",homepage:o.homepage??"",tags:o.tags?.join(", ")??""}));xe(t,{fmt:e.format,columns:["name","package","binary","installed","description","homepage","tags"],title:"ppcli/external/list",source:"ppcli external list"})});function Zt(e,t){const o=t??(()=>{const r=process.argv.indexOf(e);return process.argv.slice(r+1)})();try{uo(e,o,be)}catch(r){if(r instanceof Ue){process.stderr.write(so.dump(vo(r),{sortKeys:!1,lineWidth:120,noRefs:!0}));process.exitCode=r.exitCode}else{console.error(`Error: ${M(r)}`);process.exitCode=f.GENERIC_ERROR}}}for(const e of be){if(i.commands.some((t)=>t.name()===e.name))continue;i.command(e.name).description(`(External) ${e.description||e.name}`).argument("[args...]").allowUnknownOption().passThroughOptions().helpOption(!1).action((t)=>Zt(e.name,t))}const ct=i.command("antigravity").description("antigravity commands");ct.command("serve").description("Start Anthropic-compatible API proxy for Antigravity").option("--port <port>","Server port (default: 8082)","8082").option("--timeout <seconds>","Maximum time to wait for a reply (default: 120s)").action(async(e)=>{const{startServe:t}=await import("../../clis/antigravity/serve.js");await t({port:parseInt(e.port,10),timeout:e.timeout?Bt(e.timeout,"--timeout",120):void 0})});const lt=new Map;lt.set("antigravity",ct);const dt=ho(i,lt);xr(i);const Yt=be.map((e)=>e.name),Qt=be.map((e)=>({name:e.name,label:wt(e)})),Ie=new Map;for(const[,e]of Se())if(!Ie.has(e.site))Ie.set(e.site,e.domain);const ut=[],mt=[];for(const e of dt)if(yt(Ie.get(e))==="app")ut.push(e);else mt.push(e);const pt={external:Qt,apps:ut,sites:mt},eo=new Set([...Yt,...dt]);pe(g,{globalCommand:i,description:N});pe(b,{globalCommand:i,description:"Inspect website login status"});pe(ue,{globalCommand:i,description:Kt});pe(se,{globalCommand:i,description:Vt});pe(ye,{globalCommand:i,description:qt});pe(U,{globalCommand:i,description:Xt});i.configureHelp({visibleCommands:(e)=>e.commands.filter((t)=>e!==i||!eo.has(t.name()))});const to=(e)=>{const t=[];let o=e.parent;while(o){const r=bo(o);t.unshift(r?`${o.name()} ${r}`:o.name());o=o.parent}return[...t,e.name(),e.usage()].filter(Boolean).join(" ").trim()};function ft(e){e.configureHelp({commandUsage:to});for(const t of e.commands)ft(t)}ft(g);yo(i,()=>So(i,pt),()=>wo(pt));i.on("command:*",(e)=>{const t=e[0];console.error(`error: unknown command '${t}'`);if(je(t))console.error(` Tip: '${t}' exists on your PATH. Use 'ppcli external register ${t}' to add it as an external CLI.`);i.outputHelp();process.exitCode=f.USAGE_ERROR});return i}export function runCli(a,c){createProgram(a,c).parse()}export{findPackageRoot};export function resolveBrowserVerifyInvocation(a={}){const c=a.platform??process.platform,i=a.fileExists??O.existsSync,p=a.readFile??((R)=>O.readFileSync(R,"utf-8")),b=a.projectRoot??findPackageRoot(Le,i);for(const R of io(b,p))if(i(R))return{binary:process.execPath,args:[R],cwd:b};const g=k.join(b,"src","main.ts");if(!i(g))throw Error(`Could not find ppcli entrypoint under ${b}. Expected built entry from package.json or src/main.ts.`);const N=k.join(b,"node_modules",".bin",c==="win32"?"tsx.cmd":"tsx");if(i(N))return{binary:N,args:[g],cwd:b,...c==="win32"?{shell:!0}:{}};return{binary:c==="win32"?"npx.cmd":"npx",args:["tsx",g],cwd:b,...c==="win32"?{shell:!0}:{}}}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* ppcli daemon restart — graceful shutdown, then start a fresh daemon
|
|
6
6
|
* ppcli daemon warm — [pp-only] 预热后台自动化窗口(挂一个持久空白标签)
|
|
7
7
|
*/
|
|
8
|
-
export declare function daemonStatus(
|
|
8
|
+
export declare function daemonStatus(opts?: {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}): Promise<void>;
|
|
9
11
|
export declare function daemonStop(): Promise<void>;
|
|
10
12
|
export declare function daemonRestart(): Promise<void>;
|
|
11
13
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fetchDaemonStatus as
|
|
1
|
+
import{fetchDaemonStatus as a,requestDaemonShutdown as u}from"../browser/daemon-transport.js";import{restartDaemon as f}from"../browser/daemon-lifecycle.js";import{formatDuration as m}from"../download/progress.js";import{log as o}from"../logger.js";import{PKG_VERSION as c}from"../version.js";import{formatDaemonVersion as l,isDaemonStale as d}from"../browser/daemon-version.js";import{browserSession as w,getBrowserFactory as x}from"../runtime.js";export async function daemonStatus(s={}){const e=await a();if(s.json){console.log(JSON.stringify(e?{daemon:"running",pid:e.pid,daemonVersion:l(e),stale:d(e,c),uptimeSeconds:Math.round(e.uptime),bridgeReady:!!e.extensionConnected,extensionConnected:!!e.extensionConnected,extensionVersion:e.extensionVersion??null,profileRequired:!!e.profileRequired,profileDisconnected:!!e.profileDisconnected,profiles:(e.profiles??[]).map((t)=>({contextId:t.contextId,extensionVersion:t.extensionVersion??null})),port:e.port}:{daemon:"not_running",bridgeReady:!1},null,2));return}if(!e){console.log("Daemon: not running");return}let r;if(e.extensionConnected)r=e.extensionVersion?`connected (v${e.extensionVersion})`:"connected (version unknown)";else if(e.profileRequired){const t=e.profiles?.length??0;r=`${t} ${t===1?"profile":"profiles"} connected, none selected — run \`ppcli profile use <name>\``}else if(e.profileDisconnected)r="requested profile not connected — run `ppcli profile use <name>`";else r="disconnected";const i=l(e),n=d(e,c);console.log(`Daemon: ${n?"stale":"running"} (PID ${e.pid})`);console.log(`Version: ${i}${n?` (CLI v${c}; run: ppcli daemon restart)`:""}`);console.log(`Uptime: ${m(Math.round(e.uptime*1000))}`);console.log(`Extension: ${r}`);if(e.profiles&&e.profiles.length>0)console.log(`Profiles: ${e.profiles.map((t)=>{const p=t.extensionVersion?` v${t.extensionVersion}`:"";return`${t.contextId}${p}`}).join(", ")}`);console.log(`Memory: ${e.memoryMB} MB`);console.log(`Port: ${e.port}`)}export async function daemonStop(){if(!await a()){o.info("Daemon is not running.");return}if(await u())o.success("Daemon stopped.");else{o.error("Failed to stop daemon.");process.exitCode=1}}export async function daemonRestart(){const s=await a();if(s?.profiles&&s.profiles.length>0)o.warn(`Restarting daemon will disconnect ${s.profiles.length} browser profile(s); the extension should reconnect automatically.`);const e=await f();if(!e.stopped){o.error("Failed to stop daemon before restart.");process.exitCode=1;return}if(!e.status){o.error("Daemon restart timed out before the new daemon reported status.");process.exitCode=1;return}const r=e.previousStatus?"restarted":"started",i=l(e.status);o.success(`Daemon ${r} on port ${e.status.port} (${i}).`);if(e.status.extensionConnected){const n=e.status.profiles?.length??0,t=n>0?`; profiles connected: ${n}`:"";o.status(`Extension connected${t}.`)}else o.warn("Daemon is running, but the Browser Bridge extension has not connected yet.")}export async function daemonWarm(s){const e=await a();if(!e){o.warn("Daemon/extension not ready; skip warm (retry after the browser extension connects).");return}if(!e.extensionConnected){o.warn("Browser extension not connected yet; skip warm.");return}const r=s&&/^https?:\/\//i.test(s)?s:"about:blank",i=x("__warm__");try{await w(i,async(n)=>{await n.goto(r).catch(()=>{})},{session:"site:__warm__",windowMode:"background",surface:"adapter",siteSession:"persistent"});o.success(`Automation window warmed (${r}, kept alive for reuse).`)}catch(n){o.warn(`Warm failed (non-fatal): ${n instanceof Error?n.message:String(n)}`)}}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -174,7 +174,8 @@ export interface IPage {
|
|
|
174
174
|
match_level: 'exact' | 'stable' | 'reidentified';
|
|
175
175
|
mode?: 'input' | 'textarea' | 'contenteditable';
|
|
176
176
|
}>;
|
|
177
|
-
|
|
177
|
+
/** 返回实际走的通道:'native' = CDP 真按键(编辑器认);'synthetic' = 页面内合成事件(untrusted,不触发默认动作)。 */
|
|
178
|
+
pressKey(key: string): Promise<'native' | 'synthetic'>;
|
|
178
179
|
scrollTo(ref: string, opts?: {
|
|
179
180
|
nth?: number;
|
|
180
181
|
firstOnMulti?: boolean;
|