publishport-opencli 1.0.3 → 1.0.6

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.
@@ -1 +1 @@
1
- import{CliError as A,CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as N,Strategy as W}from"@jackwener/opencli/registry";import{publishArticle as j}from"../_shared/article/publish.js";export const jianshuProfile={home:"https://www.jianshu.com",outputFormat:"markdown",checkAuth:async(B)=>{try{const G=await fetch("https://www.jianshu.com/settings/basic.json",{method:"GET",credentials:"include",headers:{accept:"application/json"}});if(!G.ok)return{isAuthenticated:!1};const z=await G.text();let K=null;try{K=JSON.parse(z)}catch(V){return{isAuthenticated:!1}}const J=K&&K.data;if(!J||!J.nickname)return{isAuthenticated:!1};const Q=/\/upload_avatars\/(\d+)\//.exec(J.avatar||"");return{isAuthenticated:!0,userId:Q?Q[1]:"",username:J.nickname,avatar:J.avatar||""}}catch(G){return{isAuthenticated:!1,error:String(G&&G.message||G)}}},image:{skip:["jianshu.io"],uploadFn:async(B,G)=>{const z=await fetch(B,{method:"GET"});if(!z.ok)throw Error("下载图片失败: "+z.status);const K=await z.blob(),Q={"image/png":"png","image/jpeg":"jpg","image/gif":"gif","image/webp":"webp"}[K.type]||"png",V=Date.now()+"-"+Math.random().toString(16).slice(2,8)+"."+Q,Z=await fetch("https://www.jianshu.com/upload_images/token.json?filename="+encodeURIComponent(V),{credentials:"include",headers:{accept:"application/json"}}).then((L)=>L.json());if(!Z||!Z.token||!Z.key)throw Error("获取简书图片上传凭证失败");const Y=new FormData;Y.append("token",Z.token);Y.append("key",Z.key);Y.append("x:protocol","https");Y.append("file",K,V);const q=await fetch("https://upload.qiniup.com/",{method:"POST",body:Y}),$=await q.text();let X=null;try{X=JSON.parse($)}catch(L){}if(!q.ok||!X||!X.url)throw Error("七牛上传失败: "+q.status+" "+$.slice(0,120));return{url:X.url}}},publish:async(B,G)=>{const z=B.params||{},K={accept:"application/json","content-type":"application/json"},J=String(z.notebook||"").trim();if(!J)return{ok:!1,stage:"validate",message:"发布简书文章必须指定文集:--notebook <文集名>(用 `jianshu notebooks` 查看可选文集)"};let Q=[];try{Q=await(await fetch("https://www.jianshu.com/author/notebooks",{credentials:"include",headers:{accept:"application/json"}})).json()}catch(M){return{ok:!1,stage:"notebook",message:"获取简书文集列表失败:"+String(M&&M.message||M)}}if(!Array.isArray(Q)||!Q.length)return{ok:!1,stage:"notebook",message:"简书账号下没有文集,请先在简书创作中心创建一个文集"};let V=Q.find((M)=>M&&M.name===J);if(!V&&/^\d+$/.test(J))V=Q.find((M)=>M&&String(M.id)===J);if(!V)return{ok:!1,stage:"notebook",message:"未找到文集「"+J+"」,可选:"+Q.map((M)=>M.name).join(" / ")};const Z=V.id,Y=await fetch("https://www.jianshu.com/author/notes",{method:"POST",credentials:"include",headers:K,body:JSON.stringify({at_bottom:!1,notebook_id:Z,title:B.title})}),q=await Y.text();let $=null;try{$=JSON.parse(q)}catch(M){}if(!Y.ok||!$||!$.id)return{ok:!1,stage:"create",status:Y.status,message:q.slice(0,300)};const X=$.id,L=$.slug||"",F="https://www.jianshu.com/writer#/notebooks/"+Z+"/notes/"+X;if($.note_type!==2)return{ok:!1,stage:"create",message:"简书账号默认编辑器不是 Markdown(note_type="+$.note_type+")。请到 https://www.jianshu.com/settings/basic 将默认编辑器改为 Markdown 后重试。"};const O=await fetch("https://www.jianshu.com/author/notes/"+X,{method:"PUT",credentials:"include",headers:K,body:JSON.stringify({id:X,autosave_control:1,title:B.title,content:B.content})}),P=await O.text();if(!O.ok)return{ok:!1,stage:"content",status:O.status,message:P.slice(0,300),id:String(X),url:F};if(B.draftOnly)return{ok:!0,draft:!0,id:String(X),url:F};const _=await fetch("https://www.jianshu.com/author/notes/"+X+"/publicize",{method:"POST",credentials:"include",headers:K,body:JSON.stringify({})}),S=await _.text();if(!_.ok)return{ok:!1,stage:"publish",status:_.status,message:S.slice(0,300),id:String(X),url:F};const H="https://www.jianshu.com/p/"+L;try{const M=await fetch(H,{credentials:"include"});if(!M.ok)return{ok:!1,stage:"verify",status:M.status,message:"发布请求已受理但文章页不可访问,请到写作页确认状态:"+F,id:String(X),url:F}}catch(M){}return{ok:!0,draft:!1,id:String(X),url:H}}};import{readFile as y,stat as U}from"node:fs/promises";function T(B){if(!B.execute)throw new A("INVALID_INPUT","此命令需要 --execute 参数才能执行写操作。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function C(B){const G=typeof B.text==="string"?B.text:void 0,z=typeof B.file==="string"?B.file:void 0;if(G&&z)throw new A("INVALID_INPUT","不能同时指定正文参数和 --file,请二选一");let K=G??"";if(z){let J;try{J=await U(z)}catch{throw new A("INVALID_INPUT","文件不存在: "+z)}if(!J.isFile())throw new A("INVALID_INPUT","必须是可读的文本文件: "+z);let Q;try{Q=await y(z)}catch{throw new A("INVALID_INPUT","无法读取文件: "+z)}try{K=new TextDecoder("utf-8",{fatal:!0}).decode(Q)}catch{throw new A("INVALID_INPUT","文件必须是 UTF-8 编码: "+z)}}if(!K.trim())throw new A("INVALID_INPUT","正文不能为空");return K}function h(B,G,z,K,J={}){return[{status:"success",outcome:K,message:B,target_type:G,target:z,...J}]}N({site:"jianshu",name:"article",access:"write",description:"发布简书文章。默认正式发布;加 --draft 仅存草稿。必须指定文集 --notebook(用 `jianshu notebooks` 查看)。正文为 Markdown;图片自动转存至简书图床(七牛)。",domain:"jianshu.com",strategy:W.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(Markdown)"},{name:"file",help:"正文文件路径(UTF-8 编码,Markdown)"},{name:"notebook",required:!0,help:"【必填】文集名(精确匹配;也接受文集数字 ID)。可用 `jianshu notebooks` 查看全部文集。"},{name:"draft",type:"boolean",help:"仅保存草稿,不发布"},{name:"execute",type:"boolean",help:"确认执行写操作。不加此参数则拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(B,G)=>{if(!B)throw new v("简书文章发布需要浏览器会话");T(G);const z=String(G.title??"").trim();if(!z)throw new A("INVALID_INPUT","文章标题不能为空");const K=await C(G),J=Boolean(G.draft),Q={notebook:typeof G.notebook==="string"?G.notebook.trim():""},V=await j(B,{title:z,body:K,format:"markdown",draftOnly:J,profile:jianshuProfile,publishParams:Q}),Z=V.images.uploaded.length|0,Y=V.images.failed.length|0;let q=V.draft?"草稿已保存至简书(可在写作页手动发布)":"文章已正式发布至简书";if(Z||Y)q+=";图片:"+Z+" 张已转存"+(Y?","+Y+" 张失败":"");return h(q,"article","",V.draft?"draft":"publish",{created_target:"article:"+V.id,created_url:V.url})}});
1
+ import{CliError as L,CommandExecutionError as W}from"@jackwener/opencli/errors";import{cli as j,Strategy as y}from"@jackwener/opencli/registry";import{publishArticle as U}from"../_shared/article/publish.js";export const jianshuProfile={home:"https://www.jianshu.com",outputFormat:"markdown",checkAuth:async(G)=>{try{const K=await fetch("https://www.jianshu.com/settings/basic.json",{method:"GET",credentials:"include",headers:{accept:"application/json"}});if(!K.ok)return{isAuthenticated:!1};const B=await K.text();let Q=null;try{Q=JSON.parse(B)}catch(Y){return{isAuthenticated:!1}}const M=Q&&Q.data;if(!M||!M.nickname)return{isAuthenticated:!1};const X=/\/upload_avatars\/(\d+)\//.exec(M.avatar||"");return{isAuthenticated:!0,userId:X?X[1]:"",username:M.nickname,avatar:M.avatar||""}}catch(K){return{isAuthenticated:!1,error:String(K&&K.message||K)}}},image:{skip:["jianshu.io"],uploadFn:async(G,K)=>{const B=await fetch(G,{method:"GET"});if(!B.ok)throw Error("下载图片失败: "+B.status);const Q=await B.blob(),X={"image/png":"png","image/jpeg":"jpg","image/gif":"gif","image/webp":"webp"}[Q.type]||"png",Y=Date.now()+"-"+Math.random().toString(16).slice(2,8)+"."+X,q=await fetch("https://www.jianshu.com/upload_images/token.json?filename="+encodeURIComponent(Y),{credentials:"include",headers:{accept:"application/json"}}).then((H)=>H.json());if(!q||!q.token||!q.key)throw Error("获取简书图片上传凭证失败");const $=new FormData;$.append("token",q.token);$.append("key",q.key);$.append("x:protocol","https");$.append("file",Q,Y);const F=await fetch("https://upload.qiniup.com/",{method:"POST",body:$}),A=await F.text();let Z=null;try{Z=JSON.parse(A)}catch(H){}if(!F.ok||!Z||!Z.url)throw Error("七牛上传失败: "+F.status+" "+A.slice(0,120));return{url:Z.url}}},publish:async(G,K)=>{const B=G.params||{},Q={accept:"application/json","content-type":"application/json"},M=String(B.notebook||"").trim();if(!M)return{ok:!1,stage:"validate",message:"发布简书文章必须指定文集:--notebook <文集名>(用 `jianshu notebooks` 查看可选文集)"};let X=[];try{X=await(await fetch("https://www.jianshu.com/author/notebooks",{credentials:"include",headers:{accept:"application/json"}})).json()}catch(V){return{ok:!1,stage:"notebook",message:"获取简书文集列表失败:"+String(V&&V.message||V)}}if(!Array.isArray(X)||!X.length)return{ok:!1,stage:"notebook",message:"简书账号下没有文集,请先在简书创作中心创建一个文集"};let Y=X.find((V)=>V&&V.name===M);if(!Y&&/^\d+$/.test(M))Y=X.find((V)=>V&&String(V.id)===M);if(!Y)return{ok:!1,stage:"notebook",message:"未找到文集「"+M+"」,可选:"+X.map((V)=>V.name).join(" / ")};const q=Y.id,$=await fetch("https://www.jianshu.com/author/notes",{method:"POST",credentials:"include",headers:Q,body:JSON.stringify({at_bottom:!1,notebook_id:q,title:G.title})}),F=await $.text();let A=null;try{A=JSON.parse(F)}catch(V){}if(!$.ok||!A||!A.id)return{ok:!1,stage:"create",status:$.status,message:F.slice(0,300)};const Z=A.id,H=A.slug||"",O="https://www.jianshu.com/writer#/notebooks/"+q+"/notes/"+Z;if(A.note_type!==2)return{ok:!1,stage:"create",message:"简书账号默认编辑器不是 Markdown(note_type="+A.note_type+")。请到 https://www.jianshu.com/settings/basic 将默认编辑器改为 Markdown 后重试。"};const v=await fetch("https://www.jianshu.com/author/notes/"+Z,{method:"PUT",credentials:"include",headers:Q,body:JSON.stringify({id:Z,autosave_control:1,title:G.title,content:G.content})}),N=await v.text();if(!v.ok)return{ok:!1,stage:"content",status:v.status,message:N.slice(0,300),id:String(Z),url:O};if(G.draftOnly)return{ok:!0,draft:!0,id:String(Z),url:O};const P=await fetch("https://www.jianshu.com/author/notes/"+Z+"/publicize",{method:"POST",credentials:"include",headers:Q,body:JSON.stringify({})}),z=await P.text();let _=null;try{_=JSON.parse(z)}catch(V){}const S=_&&Array.isArray(_.error)&&_.error.length?_.error[0]:null;if(S)return{ok:!1,stage:"publish",status:P.status,message:(S.message||"简书拒绝了本次发布")+(S.code!=null?"(code "+S.code+")":""),id:String(Z),url:O};if(!P.ok)return{ok:!1,stage:"publish",status:P.status,message:z.slice(0,300),id:String(Z),url:O};const J="https://www.jianshu.com/p/"+H;try{const V=await fetch(J,{credentials:"include"});if(!V.ok)return{ok:!1,stage:"verify",status:V.status,message:"发布请求已受理但文章页不可访问,请到写作页确认状态:"+O,id:String(Z),url:O}}catch(V){}return{ok:!0,draft:!1,id:String(Z),url:J}}};import{readFile as T,stat as C}from"node:fs/promises";function h(G){if(!G.execute)throw new L("INVALID_INPUT","此命令需要 --execute 参数才能执行写操作。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function D(G){const K=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(K&&B)throw new L("INVALID_INPUT","不能同时指定正文参数和 --file,请二选一");let Q=K??"";if(B){let M;try{M=await C(B)}catch{throw new L("INVALID_INPUT","文件不存在: "+B)}if(!M.isFile())throw new L("INVALID_INPUT","必须是可读的文本文件: "+B);let X;try{X=await T(B)}catch{throw new L("INVALID_INPUT","无法读取文件: "+B)}try{Q=new TextDecoder("utf-8",{fatal:!0}).decode(X)}catch{throw new L("INVALID_INPUT","文件必须是 UTF-8 编码: "+B)}}if(!Q.trim())throw new L("INVALID_INPUT","正文不能为空");return Q}function m(G,K,B,Q,M={}){return[{status:"success",outcome:Q,message:G,target_type:K,target:B,...M}]}j({site:"jianshu",name:"article",access:"write",description:"发布简书文章。默认正式发布;加 --draft 仅存草稿。必须指定文集 --notebook(用 `jianshu notebooks` 查看)。正文为 Markdown;图片自动转存至简书图床(七牛)。",domain:"jianshu.com",strategy:y.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(Markdown)"},{name:"file",help:"正文文件路径(UTF-8 编码,Markdown)"},{name:"notebook",required:!0,help:"【必填】文集名(精确匹配;也接受文集数字 ID)。可用 `jianshu notebooks` 查看全部文集。"},{name:"draft",type:"boolean",help:"仅保存草稿,不发布"},{name:"execute",type:"boolean",help:"确认执行写操作。不加此参数则拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,K)=>{if(!G)throw new W("简书文章发布需要浏览器会话");h(K);const B=String(K.title??"").trim();if(!B)throw new L("INVALID_INPUT","文章标题不能为空");const Q=await D(K),M=Boolean(K.draft),X={notebook:typeof K.notebook==="string"?K.notebook.trim():""},Y=await U(G,{title:B,body:Q,format:"markdown",draftOnly:M,profile:jianshuProfile,publishParams:X}),q=Y.images.uploaded.length|0,$=Y.images.failed.length|0;let F=Y.draft?"草稿已保存至简书(可在写作页手动发布)":"文章已正式发布至简书";if(q||$)F+=";图片:"+q+" 张已转存"+($?","+$+" 张失败":"");return m(F,"article","",Y.draft?"draft":"publish",{created_target:"article:"+Y.id,created_url:Y.url})}});
@@ -1 +1 @@
1
- import{CliError as V,CommandExecutionError as A}from"@jackwener/opencli/errors";import{cli as F,Strategy as O}from"@jackwener/opencli/registry";import{readFile as _,stat as q}from"node:fs/promises";import{publishArticle as T}from"../_shared/article/publish.js";export const oschinaProfile={home:"https://my.oschina.net",outputFormat:"markdown",image:{spec:{url:"https://apiv1.oschina.net/oschinapi/ai/creation/project/uploadDetail",method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image",body:{},responsePath:"result"},skip:["apiv1.oschina.net","oscimg.oschina.net","static.oschina.net"]},checkAuth:async()=>{const G=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!G.ok)return{isAuthenticated:!1,error:"HTTP "+G.status};let z=null;try{z=await G.json()}catch(U){return{isAuthenticated:!1,error:"解析响应失败"}}if(!z||!z.success||!z.result||!z.result.userId)return{isAuthenticated:!1,error:z&&z.message||"未登录"};const B=String(z.result.userId),M=z.result.userVo&&z.result.userVo.name||B,Q=z.result.userVo&&z.result.userVo.portraitUrl||"";return{isAuthenticated:!0,userId:B,username:M,avatar:Q}},publish:async(G)=>{const z=G.params||{};let B="";try{const L=await(await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"})).json();if(L&&L.success&&L.result&&L.result.userId)B=String(L.result.userId)}catch(K){}if(!B)return{ok:!1,stage:"auth",status:401,message:"未登录开源中国,无法获取用户 ID"};let M=0;if(z.category){let K=[];try{const X=await(await fetch("https://apiv1.oschina.net/oschinapi/blog_catalog/list_by_user",{credentials:"include"})).json();K=X&&X.result||[]}catch(H){return{ok:!1,stage:"category",message:"获取开源中国博客分类失败:"+String(H&&H.message||H)}}const L=K.find((H)=>String(H.name)===String(z.category));if(!L)return{ok:!1,stage:"category",message:"未找到博客分类「"+z.category+"」,可选:"+(K.map((H)=>H.name).join(" / ")||"(你还没有任何分类)")};M=L.id}if(G.draftOnly){const K=await fetch("https://apiv1.oschina.net/oschinapi/api/draft/save_draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,user:Number(B),content:G.content,contentType:1,catalog:M,originUrl:"",privacy:!0,disableComment:!1})}),L=await K.text();let H=null;try{H=JSON.parse(L)}catch(S){}if(!K.ok||!H||!H.success||!H.result||!H.result.id)return{ok:!1,stage:"save_draft",status:K.status,message:H&&H.message||L.slice(0,300)};const X=String(H.result.id);return{ok:!0,draft:!0,id:X,url:"https://my.oschina.net/u/"+B+"/blog/ai-write/draft/"+X}}const Q=await fetch("https://apiv1.oschina.net/oschinapi/blog/web/add",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,content:G.content,contentType:1,type:"1",originUrl:"",catalog:M,privacy:!1,disableComment:!1,isAiRelated:!1,user:Number(B)})}),U=await Q.text();let J=null;try{J=JSON.parse(U)}catch(K){}if(!Q.ok||!J||J.code!==200||J.result==null)return{ok:!1,stage:"publish",status:Q.status,message:J&&(J.message||J.msg)||U.slice(0,300)};const W=String(J.result);return{ok:!0,draft:!1,id:W,url:"https://my.oschina.net/u/"+B+"/blog/"+W}}};function Y(G){if(!G.execute)throw new V("INVALID_INPUT","发布开源中国文章需要 --execute 参数。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}function Z(G,z,B,M,Q={}){return[{status:"success",outcome:M,message:G,target_type:z,target:B,...Q}]}async function $(G){const z=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(z&&B)throw new V("INVALID_INPUT","正文和文件路径只能二选一,不能同时指定");let M=z??"";if(B){let Q;try{Q=await q(B)}catch{throw new V("INVALID_INPUT","文件不存在:"+B)}if(!Q.isFile())throw new V("INVALID_INPUT","必须指定可读的文本文件:"+B);let U;try{U=await _(B)}catch{throw new V("INVALID_INPUT","文件读取失败:"+B)}try{M=new TextDecoder("utf-8",{fatal:!0}).decode(U)}catch{throw new V("INVALID_INPUT","文件编码不是 UTF-8:"+B)}}if(!M.trim())throw new V("INVALID_INPUT","文章正文不能为空");return M}F({site:"oschina",name:"article",access:"write",description:"发布文章到开源中国博客。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,外链图片自动转存到开源中国图床。",domain:"my.oschina.net",strategy:O.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;--html 表示原始 HTML)"},{name:"file",help:"从文件读取正文(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"category",help:"个人博客分类名(精确匹配),合法值用 `oschina catalogs` 列举;可空=不分类"},{name:"draft",type:"boolean",help:"仅存草稿,不发布"},{name:"execute",type:"boolean",help:"真正执行写操作;不加此参数命令拒绝写入"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,z)=>{if(!G)throw new A("开源中国文章发布需要浏览器会话");Y(z);const B=String(z.title??"").trim();if(!B)throw new V("INVALID_INPUT","文章标题不能为空");const M=await $(z),Q=Boolean(z.draft),U={category:typeof z.category==="string"?z.category.trim():""},J=await T(G,{title:B,body:M,format:z.html?"html":"markdown",draftOnly:Q,profile:oschinaProfile,publishParams:U}),W=J.images.uploaded.length|0,K=J.images.failed.length|0;let L=J.draft?"已保存到开源中国草稿箱":"已正式发布到开源中国博客";if(W||K)L+=`·图片:${W} 张已转存${K?`,${K} 张失败`:""}`;return Z(L,"article","",J.draft?"draft":"created",{created_target:(J.draft?"draft:":"article:")+J.id,created_url:J.url})}});export const __test__={oschinaProfile,requireExecute:Y,buildResultRow:Z,resolvePayload:$};
1
+ import{CliError as W,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as O,Strategy as _}from"@jackwener/opencli/registry";import{readFile as q,stat as T}from"node:fs/promises";import{publishArticle as S}from"../_shared/article/publish.js";export const oschinaProfile={home:"https://my.oschina.net",outputFormat:"markdown",image:{spec:{url:"https://apiv1.oschina.net/oschinapi/ai/creation/project/uploadDetail",method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image",body:{},responsePath:"result"},skip:["apiv1.oschina.net","oscimg.oschina.net","static.oschina.net"]},checkAuth:async()=>{const G=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!G.ok)return{isAuthenticated:!1,error:"HTTP "+G.status};let z=null;try{z=await G.json()}catch(V){return{isAuthenticated:!1,error:"解析响应失败"}}if(!z||!z.success||!z.result||!z.result.userId)return{isAuthenticated:!1,error:z&&z.message||"未登录"};const B=String(z.result.userId),K=z.result.userVo&&z.result.userVo.name||B,M=z.result.userVo&&z.result.userVo.portraitUrl||"";return{isAuthenticated:!0,userId:B,username:K,avatar:M}},publish:async(G)=>{const z=G.params||{};let B="",K="";try{const H=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!H.ok)return{ok:!1,stage:"auth",status:H.status,message:"获取开源中国用户信息失败:HTTP "+H.status+"(登录态可能已失效,请在客户端重新登录)"};const L=await H.json();if(L&&L.success&&L.result&&L.result.userId)B=String(L.result.userId);else K=L&&L.message||"myDetails 未返回 userId(登录态可能已失效或为匿名)"}catch(H){return{ok:!1,stage:"auth",message:"校验开源中国登录态时请求异常:"+String(H&&H.message||H)}}if(!B)return{ok:!1,stage:"auth",message:"未登录开源中国,无法获取用户 ID"+(K?":"+K:"")};let M=0;if(z.category){let H=[];try{const Y=await(await fetch("https://apiv1.oschina.net/oschinapi/blog_catalog/list_by_user",{credentials:"include"})).json();H=Y&&Y.result||[]}catch(J){return{ok:!1,stage:"category",message:"获取开源中国博客分类失败:"+String(J&&J.message||J)}}const L=H.find((J)=>String(J.name)===String(z.category));if(!L)return{ok:!1,stage:"category",message:"未找到博客分类「"+z.category+"」,可选:"+(H.map((J)=>J.name).join(" / ")||"(你还没有任何分类)")};M=L.id}if(G.draftOnly){const H=await fetch("https://apiv1.oschina.net/oschinapi/api/draft/save_draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,user:Number(B),content:G.content,contentType:1,catalog:M,originUrl:"",privacy:!0,disableComment:!1})}),L=await H.text();let J=null;try{J=JSON.parse(L)}catch(j){}if(!H.ok||!J||!J.success||!J.result||!J.result.id)return{ok:!1,stage:"save_draft",status:H.status,message:J&&J.message||L.slice(0,300)};const Y=String(J.result.id);return{ok:!0,draft:!0,id:Y,url:"https://my.oschina.net/u/"+B+"/blog/ai-write/draft/"+Y}}const V=await fetch("https://apiv1.oschina.net/oschinapi/blog/web/add",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,content:G.content,contentType:1,type:"1",originUrl:"",catalog:M,privacy:!1,disableComment:!1,isAiRelated:!1,user:Number(B)})}),U=await V.text();let Q=null;try{Q=JSON.parse(U)}catch(H){}if(!V.ok||!Q||Q.code!==200||Q.result==null)return{ok:!1,stage:"publish",status:V.status,message:Q&&(Q.message||Q.msg)||U.slice(0,300)};const X=String(Q.result);return{ok:!0,draft:!1,id:X,url:"https://my.oschina.net/u/"+B+"/blog/"+X}}};function Z(G){if(!G.execute)throw new W("INVALID_INPUT","发布开源中国文章需要 --execute 参数。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}function $(G,z,B,K,M={}){return[{status:"success",outcome:K,message:G,target_type:z,target:B,...M}]}async function A(G){const z=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(z&&B)throw new W("INVALID_INPUT","正文和文件路径只能二选一,不能同时指定");let K=z??"";if(B){let M;try{M=await T(B)}catch{throw new W("INVALID_INPUT","文件不存在:"+B)}if(!M.isFile())throw new W("INVALID_INPUT","必须指定可读的文本文件:"+B);let V;try{V=await q(B)}catch{throw new W("INVALID_INPUT","文件读取失败:"+B)}try{K=new TextDecoder("utf-8",{fatal:!0}).decode(V)}catch{throw new W("INVALID_INPUT","文件编码不是 UTF-8:"+B)}}if(!K.trim())throw new W("INVALID_INPUT","文章正文不能为空");return K}O({site:"oschina",name:"article",access:"write",description:"发布文章到开源中国博客。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,外链图片自动转存到开源中国图床。",domain:"my.oschina.net",strategy:_.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;--html 表示原始 HTML)"},{name:"file",help:"从文件读取正文(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"category",help:"个人博客分类名(精确匹配),合法值用 `oschina catalogs` 列举;可空=不分类"},{name:"draft",type:"boolean",help:"仅存草稿,不发布"},{name:"execute",type:"boolean",help:"真正执行写操作;不加此参数命令拒绝写入"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,z)=>{if(!G)throw new F("开源中国文章发布需要浏览器会话");Z(z);const B=String(z.title??"").trim();if(!B)throw new W("INVALID_INPUT","文章标题不能为空");const K=await A(z),M=Boolean(z.draft),V={category:typeof z.category==="string"?z.category.trim():""},U=await S(G,{title:B,body:K,format:z.html?"html":"markdown",draftOnly:M,profile:oschinaProfile,publishParams:V}),Q=U.images.uploaded.length|0,X=U.images.failed.length|0;let H=U.draft?"已保存到开源中国草稿箱":"已正式发布到开源中国博客";if(Q||X)H+=`·图片:${Q} 张已转存${X?`,${X} 张失败`:""}`;return $(H,"article","",U.draft?"draft":"created",{created_target:(U.draft?"draft:":"article:")+U.id,created_url:U.url})}});export const __test__={oschinaProfile,requireExecute:Z,buildResultRow:$,resolvePayload:A};
@@ -1,4 +1,4 @@
1
- import{cli as A,Strategy as C}from"@jackwener/opencli/registry";import{CommandExecutionError as L,EmptyResultError as G}from"@jackwener/opencli/errors";import{gotoWritePage as P}from"../_shared/article/publish.js";const T="https://mp.sohu.com/mpfe/v4/contentManagement/news/articlelist",_=10;function R(M){if(!M)return"";const F=new Date(M+28800000),J=(Q)=>String(Q).padStart(2,"0");return`${F.getUTCFullYear()}-${J(F.getUTCMonth()+1)}-${J(F.getUTCDate())} ${J(F.getUTCHours())}:${J(F.getUTCMinutes())}`}function W(M){const F=new Date(Date.now()+28800000-M*86400000),J=(Q)=>String(Q).padStart(2,"0");return`${F.getUTCFullYear()}-${J(F.getUTCMonth()+1)}-${J(F.getUTCDate())}`}A({site:"sohu",name:"stats",access:"read",description:"搜狐号文章数据(每篇阅读/评论/播放等全量运营指标;点赞/分享/投票仅覆盖最近30天已发布内容)",domain:"mp.sohu.com",strategy:C.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","status","views","likes","comments","collects","shares","video_plays","votes","extra"],func:async(M,F)=>{if(!M)throw new L("搜狐号文章数据需要浏览器会话");const J=Number(F.limit)>0?Math.floor(Number(F.limit)):20;await P(M,T);const Q={limit:J,pageSize:_,startDate:W(30),endDate:W(0)},H=await M.evaluate(`(async () => {
1
+ import{cli as C,Strategy as G}from"@jackwener/opencli/registry";import{CommandExecutionError as R,EmptyResultError as P}from"@jackwener/opencli/errors";import{gotoWritePage as T}from"../_shared/article/publish.js";const _="https://mp.sohu.com/mpfe/v4/contentManagement/news/articlelist",v=10;function W(M){if(!M)return"";const H=new Date(M+28800000),J=(Q)=>String(Q).padStart(2,"0");return`${H.getUTCFullYear()}-${J(H.getUTCMonth()+1)}-${J(H.getUTCDate())} ${J(H.getUTCHours())}:${J(H.getUTCMinutes())}`}function Z(M){const H=new Date(Date.now()+28800000-M*86400000),J=(Q)=>String(Q).padStart(2,"0");return`${H.getUTCFullYear()}-${J(H.getUTCMonth()+1)}-${J(H.getUTCDate())}`}C({site:"sohu",name:"stats",access:"read",description:"搜狐号文章数据(每篇阅读/评论/播放等全量运营指标;点赞/分享/投票仅覆盖最近30天已发布内容)",domain:"mp.sohu.com",strategy:G.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","status","views","likes","comments","collects","shares","video_plays","votes","extra"],func:async(M,H)=>{if(!M)throw new R("搜狐号文章数据需要浏览器会话");const J=Number(H.limit)>0?Math.floor(Number(H.limit)):20;await T(M,_);const Q={limit:J,pageSize:v,startDate:Z(30),endDate:Z(0)},z=await M.evaluate(`(async () => {
2
2
  const P = ${JSON.stringify(Q)};
3
3
 
4
4
  // ── 请求头:dv-id / sp-cm 优先取页面自己存的值,逐字复刻 ──
@@ -41,6 +41,19 @@ import{cli as A,Strategy as C}from"@jackwener/opencli/registry";import{CommandEx
41
41
  return { err: '获取搜狐账号信息失败(可能未登录搜狐号):' + String((e && e.message) || e) };
42
42
  }
43
43
 
44
+ // ── 频道 id→名 映射(与 article --channel 同一张表 channels-data-api)──
45
+ // 用于把 news 里的 channelId 解析成人类可读频道名。注意:这个 channelId 是
46
+ // 搜狐后台按内容自动归类的**展示频道**,可能与发布时 --channel 所选不同。
47
+ let channelMap = {};
48
+ try {
49
+ const cj = await getJson('https://mp.sohu.com/mpbp/bp/account/common/channels-data-api?status=1&accountId=' + accountId);
50
+ const clist = Array.isArray(cj) ? cj : ((cj && cj.data) || []);
51
+ for (const c of clist) {
52
+ const cid = c && (c.id != null ? c.id : c.channelId);
53
+ if (cid != null) channelMap[String(cid)] = c.name || '';
54
+ }
55
+ } catch (e) { /* 映射失败不阻断,channelId 回落为裸 id */ }
56
+
44
57
  // ── 内容列表:statusType=1(全部),按页面原样 psize=10 翻页 ──
45
58
  const listUrl = (statusType, pno) =>
46
59
  'https://mp.sohu.com/mpbp/bp/news/v4/users/news?psize=' + P.pageSize
@@ -91,5 +104,5 @@ import{cli as A,Strategy as C}from"@jackwener/opencli/registry";import{CommandEx
91
104
  statError = String((e && e.message) || e);
92
105
  }
93
106
 
94
- return { accountId, news: news.slice(0, P.limit), totalCount: total, publishedIds, statRows, statError };
95
- })()`);if(!H||H.err)throw new L(H&&H.err||"搜狐号文章数据获取失败:页面未返回任何数据(请确认已登录 mp.sohu.com)");const V=Array.isArray(H.news)?H.news:[];if(!V.length)throw new G("sohu stats","该搜狐号名下没有任何内容(或列表接口返回为空)。请先在 mp.sohu.com 发布内容后再查数据。");const X=new Map,Y=new Map;for(const q of Array.isArray(H.statRows)?H.statRows:[]){if(q&&q.id!=null)X.set(String(q.id),q);if(q&&typeof q.title==="string"&&q.title)Y.set(q.title,q)}const U=(q,O)=>{if(!q)return"";for(const K of O)if(typeof q[K]==="number")return q[K];return""},Z=new Set(Array.isArray(H.publishedIds)?H.publishedIds:[]);return V.map((q)=>{const O=q.id!=null?String(q.id):"",K=X.get(O)||Y.get(q.title||"")||null,$=Z.has(O),z={};if(q.auditStatus!=null)z.audit_status=q.auditStatus;if(q.status!=null)z.status_raw=q.status;if(q.rejectReason)z.reject_reason=q.rejectReason;if(q.original!=null)z.original=q.original;if(q.star!=null)z.star=q.star;if(q.secureScore!=null)z.secure_score=q.secureScore;if(q.channelId!=null)z.channel_id=q.channelId;if(q.modifiedTime)z.modified_at=R(q.modifiedTime);if(q.brief)z.brief=String(q.brief).slice(0,80);if(K)z.stat=K;if(H.statError)z.stat_error="单篇统计接口失败:"+H.statError;return{id:O,type:q.type!=null?String(q.type):"",title:q.title||q.mobileTitle||"",url:O?"https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus="+($?1:2)+"&id="+O:"",published_at:R(q.postTime||q.createdTime),status:$?"已发布":"未发布",views:typeof q.pv==="number"?q.pv:"",likes:U(K,["digg","diggCount","like","likeCount","praise","zan"]),comments:typeof q.cmt==="number"?q.cmt:U(K,["cmt","comment","commentCount"]),collects:"",shares:U(K,["share","shareCount","forward","forwardCount"]),video_plays:typeof q.videoPlayPv==="number"?q.videoPlayPv:"",votes:U(K,["vote","voteCount"]),extra:Object.keys(z).length?JSON.stringify(z):""}})}});
107
+ return { accountId, news: news.slice(0, P.limit), totalCount: total, publishedIds, statRows, statError, channelMap };
108
+ })()`);if(!z||z.err)throw new R(z&&z.err||"搜狐号文章数据获取失败:页面未返回任何数据(请确认已登录 mp.sohu.com)");const V=Array.isArray(z.news)?z.news:[];if(!V.length)throw new P("sohu stats","该搜狐号名下没有任何内容(或列表接口返回为空)。请先在 mp.sohu.com 发布内容后再查数据。");const X=new Map,Y=new Map;for(const q of Array.isArray(z.statRows)?z.statRows:[]){if(q&&q.id!=null)X.set(String(q.id),q);if(q&&typeof q.title==="string"&&q.title)Y.set(q.title,q)}const U=(q,O)=>{if(!q)return"";for(const K of O)if(typeof q[K]==="number")return q[K];return""},A=new Set(Array.isArray(z.publishedIds)?z.publishedIds:[]);return V.map((q)=>{const O=q.id!=null?String(q.id):"",K=X.get(O)||Y.get(q.title||"")||null,$=A.has(O),F={};if(q.auditStatus!=null)F.audit_status=q.auditStatus;if(q.status!=null)F.status_raw=q.status;if(q.rejectReason)F.reject_reason=q.rejectReason;if(q.original!=null)F.original=q.original;if(q.star!=null)F.star=q.star;if(q.secureScore!=null)F.secure_score=q.secureScore;if(q.channelId!=null){const L=z.channelMap&&z.channelMap[String(q.channelId)]||"";F.display_channel=L?L+"("+q.channelId+")":String(q.channelId);F.display_channel_note="搜狐后台按内容自动归类的展示频道,可能与发布时 --channel 所选不同"}if(q.modifiedTime)F.modified_at=W(q.modifiedTime);if(q.brief)F.brief=String(q.brief).slice(0,80);if(K)F.stat=K;if(z.statError)F.stat_error="单篇统计接口失败:"+z.statError;return{id:O,type:q.type!=null?String(q.type):"",title:q.title||q.mobileTitle||"",url:O?"https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus="+($?1:2)+"&id="+O:"",published_at:W(q.postTime||q.createdTime),status:$?"已发布":"未发布",views:typeof q.pv==="number"?q.pv:"",likes:U(K,["digg","diggCount","like","likeCount","praise","zan"]),comments:typeof q.cmt==="number"?q.cmt:U(K,["cmt","comment","commentCount"]),collects:"",shares:U(K,["share","shareCount","forward","forwardCount"]),video_plays:typeof q.videoPlayPv==="number"?q.videoPlayPv:"",votes:U(K,["vote","voteCount"]),extra:Object.keys(F).length?JSON.stringify(F):""}})}});
@@ -1,11 +1,11 @@
1
- import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy as U}from"@jackwener/opencli/registry";import{AuthRequiredError as D,CommandExecutionError as W}from"@jackwener/opencli/errors";import{describeTwitterApiError as L,resolveTwitterOperationMetadata as I,unwrapBrowserResult as b}from"./shared.js";import{isRecoverableFileInputError as m,TWITTER_BEARER_TOKEN as M}from"./utils.js";import{assertLiteralContent as S}from"../_shared/content-guard.js";const k=4,N=500,F=30000,f=250,c=1e4,j=500,y=15000,O="https://x.com/compose/post",x="https://x.com/home",J='input[type="file"][data-testid="fileInput"]',h=new Set([".jpg",".jpeg",".png",".gif",".webp"]),_={queryId:"5CdvsV_zjv4L64XFifAglw",features:{premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!1,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!0,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,articles_preview_enabled:!0,rweb_cashtags_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0},fieldToggles:{}};function d(z){const Q=z.split(",").map((H)=>H.trim()).filter(Boolean);if(Q.length>k)throw new W(`Too many images: ${Q.length} (max ${k})`);return Q.map((H)=>{const Y=X.resolve(H),$=X.extname(Y).toLowerCase();if(!h.has($))throw new W(`Unsupported image format "${$}". Supported: jpg, png, gif, webp`);const Z=B.statSync(Y,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new W(`Not a valid file: ${Y}`);return Y})}function P(z){const H=(z instanceof Error?z.message:String(z)).toLowerCase();return H.includes("unknown action")||H.includes("not supported")||H.includes("inserttext returned no inserted flag")}async function T(z){return z.evaluate(`(() => {
1
+ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy as w}from"@jackwener/opencli/registry";import{AuthRequiredError as N,CommandExecutionError as G}from"@jackwener/opencli/errors";import{describeTwitterApiError as L,resolveTwitterOperationMetadata as I,unwrapBrowserResult as M}from"./shared.js";import{isRecoverableFileInputError as S,TWITTER_BEARER_TOKEN as O}from"./utils.js";import{assertLiteralContent as m}from"../_shared/content-guard.js";const f=4,y=500,v=30000,A=250,h=1e4,J=500,k=15000,x="https://x.com/compose/post",_="https://x.com/home",D='input[type="file"][data-testid="fileInput"]',c=new Set([".jpg",".jpeg",".png",".gif",".webp"]),P={queryId:"5CdvsV_zjv4L64XFifAglw",features:{premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!1,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!0,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,articles_preview_enabled:!0,rweb_cashtags_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0},fieldToggles:{}};function b(z){const Q=z.split(",").map((H)=>H.trim()).filter(Boolean);if(Q.length>f)throw new G(`Too many images: ${Q.length} (max ${f})`);return Q.map((H)=>{const Y=j.resolve(H),$=j.extname(Y).toLowerCase();if(!c.has($))throw new G(`Unsupported image format "${$}". Supported: jpg, png, gif, webp`);const Z=B.statSync(Y,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new G(`Not a valid file: ${Y}`);return Y})}function T(z){const H=(z instanceof Error?z.message:String(z)).toLowerCase();return H.includes("unknown action")||H.includes("not supported")||H.includes("inserttext returned no inserted flag")}async function d(z){return z.evaluate(`(() => {
2
2
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
3
3
  const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
4
4
  const box = boxes.find(visible) || boxes[0];
5
5
  if (!box) return { ok: false, message: 'Could not find the tweet composer text area. Are you logged in?' };
6
6
  box.focus();
7
7
  return { ok: true };
8
- })()`)}async function l(z,Q){const H=Math.ceil(c/f);return z.evaluate(`(async () => {
8
+ })()`)}async function l(z,Q){const H=Math.ceil(h/A);return z.evaluate(`(async () => {
9
9
  const expected = ${JSON.stringify(Q)};
10
10
  const normalize = s => String(s || '').replace(/ /g, ' ').replace(/s+/g, ' ').trim();
11
11
  const normalizedExpected = normalize(expected);
@@ -13,7 +13,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
13
13
  const box = document.querySelector('[data-testid="tweetTextarea_0"]');
14
14
  const actual = box ? (box.innerText || box.textContent || '') : '';
15
15
  if (box && normalize(actual).includes(normalizedExpected)) return { ok: true };
16
- await new Promise(r => setTimeout(r, ${JSON.stringify(f)}));
16
+ await new Promise(r => setTimeout(r, ${JSON.stringify(A)}));
17
17
  }
18
18
  const box = document.querySelector('[data-testid="tweetTextarea_0"]');
19
19
  return {
@@ -21,7 +21,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
21
21
  message: 'Could not verify tweet text in the composer after typing.',
22
22
  actualText: box ? (box.innerText || box.textContent || '') : ''
23
23
  };
24
- })()`)}async function E(z,Q){const H=await T(z);if(!H?.ok)return H;const Y=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const $ of Y)try{await $(Q);const Z=await l(z,Q);if(Z?.ok)return Z}catch(Z){if(!P(Z))throw Z}return z.evaluate(`(async () => {
24
+ })()`)}async function n(z,Q){const H=await d(z);if(!H?.ok)return H;const Y=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const $ of Y)try{await $(Q);const Z=await l(z,Q);if(Z?.ok)return Z}catch(Z){if(!T(Z))throw Z}return z.evaluate(`(async () => {
25
25
  try {
26
26
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
27
27
  const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
@@ -40,11 +40,11 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
40
40
  if (normalize(actual).includes(normalize(textToInsert))) return { ok: true };
41
41
  return { ok: false, message: 'Could not verify tweet text in the composer after typing.', actualText: actual };
42
42
  } catch (e) { return { ok: false, message: String(e) }; }
43
- })()`)}async function n(z,Q){const H=Math.ceil(F/N);return z.evaluate(`(async () => {
43
+ })()`)}async function E(z,Q){const H=Math.ceil(v/y);return z.evaluate(`(async () => {
44
44
  const expected = ${JSON.stringify(Q)};
45
45
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
46
46
  for (let i = 0; i < ${JSON.stringify(H)}; i++) {
47
- await new Promise(r => setTimeout(r, ${JSON.stringify(N)}));
47
+ await new Promise(r => setTimeout(r, ${JSON.stringify(y)}));
48
48
  const attachments = document.querySelector('[data-testid="attachments"]');
49
49
  const previewCount = Math.max(
50
50
  attachments ? attachments.querySelectorAll('[role="group"], img, video').length : 0,
@@ -58,9 +58,9 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
58
58
  const buttonReady = !!button && !button.disabled && button.getAttribute('aria-disabled') !== 'true';
59
59
  if (previewCount >= expected && buttonReady) return { ok: true, previewCount };
60
60
  }
61
- return { ok: false, message: 'Image upload timed out (${F/1000}s).' };
62
- })()`)}async function A(z,Q){const H=Q.map(($)=>{const Z=X.extname($).toLowerCase(),V=Z===".png"?"image/png":Z===".gif"?"image/gif":Z===".webp"?"image/webp":"image/jpeg";return{name:X.basename($),mime:V,base64:B.readFileSync($).toString("base64")}}),Y=await z.evaluate(`(() => {
63
- const input = document.querySelector(${JSON.stringify(J)});
61
+ return { ok: false, message: 'Image upload timed out (${v/1000}s).' };
62
+ })()`)}async function R(z,Q){const H=Q.map(($)=>{const Z=j.extname($).toLowerCase(),q=Z===".png"?"image/png":Z===".gif"?"image/gif":Z===".webp"?"image/webp":"image/jpeg";return{name:j.basename($),mime:q,base64:B.readFileSync($).toString("base64")}}),Y=await z.evaluate(`(() => {
63
+ const input = document.querySelector(${JSON.stringify(D)});
64
64
  if (!input) return { ok: false, error: 'No file input found' };
65
65
  const dt = new DataTransfer();
66
66
  for (const file of ${JSON.stringify(H)}) {
@@ -86,7 +86,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
86
86
  input.dispatchEvent(new Event('change', { bubbles: true }));
87
87
  input.dispatchEvent(new Event('input', { bubbles: true }));
88
88
  return { ok: true };
89
- })()`);if(!Y?.ok)throw new W(`Image upload failed (base64 fallback): ${Y?.error??"unknown error"}`)}async function u(z,Q){const H=Math.ceil(y/j),Y=await z.evaluate(`(async () => {
89
+ })()`);if(!Y?.ok)throw new G(`Image upload failed (base64 fallback): ${Y?.error??"unknown error"}`)}async function u(z,Q){const H=Math.ceil(k/J),Y=await z.evaluate(`(async () => {
90
90
  try {
91
91
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
92
92
  // For text-only posts the Tweet button only becomes enabled once
@@ -99,13 +99,13 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
99
99
  const buttons = Array.from(document.querySelectorAll('[data-testid="tweetButtonInline"], [data-testid="tweetButton"]'));
100
100
  btn = buttons.find((el) => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true');
101
101
  if (btn) break;
102
- await new Promise(r => setTimeout(r, ${JSON.stringify(j)}));
102
+ await new Promise(r => setTimeout(r, ${JSON.stringify(J)}));
103
103
  }
104
104
  if (!btn) return { ok: false, message: 'Tweet button is disabled or not found.' };
105
105
  btn.click();
106
106
  return { ok: true };
107
107
  } catch (e) { return { ok: false, message: String(e) }; }
108
- })()`);if(!Y?.ok)return Y;const $=Math.ceil(y/j);return z.evaluate(`(async () => {
108
+ })()`);if(!Y?.ok)return Y;const $=Math.ceil(k/J);return z.evaluate(`(async () => {
109
109
  const expected = ${JSON.stringify(Q)};
110
110
  const normalize = s => String(s || '').replace(/ /g, ' ').replace(/s+/g, ' ').trim();
111
111
  const expectedText = normalize(expected);
@@ -124,7 +124,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
124
124
  return {};
125
125
  };
126
126
  for (let i = 0; i < ${JSON.stringify($)}; i++) {
127
- await new Promise(r => setTimeout(r, ${JSON.stringify(j)}));
127
+ await new Promise(r => setTimeout(r, ${JSON.stringify(J)}));
128
128
  const toasts = Array.from(document.querySelectorAll('[role="alert"], [data-testid="toast"]'))
129
129
  .filter((el) => visible(el));
130
130
  const successToast = toasts.find((el) => /sent|posted|your post was sent|your tweet was sent/i.test(el.textContent || ''));
@@ -144,15 +144,15 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as C,Strategy a
144
144
  }
145
145
  }
146
146
  return { ok: false, message: 'Tweet submission did not complete before timeout.' };
147
- })()`)}function R(z){const Q=Array.isArray(z?.errors)?z.errors:[];if(Q.length===0)return"";return Q.map((H)=>H?.message||JSON.stringify(H)).filter(Boolean).join("; ").slice(0,300)}function i(z){const Q=z?.data?.create_tweet?.tweet_results?.result,H=Q?.tweet||Q;if(!H||typeof H!=="object")return null;const Y=String(H.rest_id||H.legacy?.id_str||"").trim();if(!/^\d+$/.test(Y))return null;const $=H.core?.user_results?.result,Z=String($?.core?.screen_name||$?.legacy?.screen_name||"").trim();return{id:Y,url:Z?`https://x.com/${Z}/status/${Y}`:`https://x.com/i/status/${Y}`}}async function g(z,Q){await z.goto(x,{waitUntil:"load",settleMs:1000});const Y=(await z.getCookies({url:"https://x.com"})).find((G)=>G.name==="ct0")?.value||null;if(!Y)throw new D("x.com","Not logged into x.com (no ct0 cookie)");const $=await I(z,"CreateTweet",_),Z={variables:{tweet_text:Q,dark_request:!1,media:{media_entities:[],possibly_sensitive:!1},semantic_annotation_ids:[]},features:$.features,queryId:$.queryId};if($.fieldToggles&&Object.keys($.fieldToggles).length>0)Z.fieldToggles=$.fieldToggles;const V=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(M)}`,"X-Csrf-Token":Y,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),K=`/i/api/graphql/${$.queryId}/CreateTweet`,w=JSON.stringify(Z),q=b(await z.evaluate(`async () => {
148
- const r = await fetch(${JSON.stringify(K)}, {
147
+ })()`)}function C(z){const Q=Array.isArray(z?.errors)?z.errors:[];if(Q.length===0)return"";return Q.map((H)=>H?.message||JSON.stringify(H)).filter(Boolean).join("; ").slice(0,300)}function i(z){const Q=z?.data?.create_tweet?.tweet_results?.result,H=Q?.tweet||Q;if(!H||typeof H!=="object")return null;const Y=String(H.rest_id||H.legacy?.id_str||"").trim();if(!/^\d+$/.test(Y))return null;const $=H.core?.user_results?.result,Z=String($?.core?.screen_name||$?.legacy?.screen_name||"").trim();return{id:Y,url:Z?`https://x.com/${Z}/status/${Y}`:`https://x.com/i/status/${Y}`}}async function g(z,Q){await z.goto(_,{waitUntil:"load",settleMs:1000});const Y=(await z.getCookies({url:"https://x.com"})).find((X)=>X.name==="ct0")?.value||null;if(!Y)throw new N("x.com","Not logged into x.com (no ct0 cookie)");const $=await I(z,"CreateTweet",P),Z={variables:{tweet_text:Q,dark_request:!1,media:{media_entities:[],possibly_sensitive:!1},semantic_annotation_ids:[]},features:$.features,queryId:$.queryId};if($.fieldToggles&&Object.keys($.fieldToggles).length>0)Z.fieldToggles=$.fieldToggles;const q=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(O)}`,"X-Csrf-Token":Y,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),W=`/i/api/graphql/${$.queryId}/CreateTweet`,K=JSON.stringify(Z),V=M(await z.evaluate(`async () => {
148
+ const r = await fetch(${JSON.stringify(W)}, {
149
149
  method: 'POST',
150
- headers: ${V},
150
+ headers: ${q},
151
151
  credentials: 'include',
152
- body: ${JSON.stringify(w)},
152
+ body: ${JSON.stringify(K)},
153
153
  });
154
154
  const bodyText = await r.text();
155
155
  let bodyJson = null;
156
156
  try { bodyJson = JSON.parse(bodyText); } catch {}
157
157
  return { ok: r.ok, httpStatus: r.status, bodyJson, bodyText };
158
- }`));if(q?.httpStatus===401||q?.httpStatus===403)throw new D("x.com",`Twitter CreateTweet returned HTTP ${q.httpStatus}`);if(!q?.ok){const G=R(q?.bodyJson);return{ok:!1,message:L("CreateTweet",q?.httpStatus??0,G||void 0)}}const v=i(q.bodyJson);if(!v){const G=R(q.bodyJson);return{ok:!1,message:G?`CreateTweet failed: ${G}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...v}}C({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:U.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new W("Browser session required for twitter post");const H=Q.images?d(String(Q.images)):[],Y=String(Q.text??"");S(Y,{label:"推文内容"});try{await z.goto(O,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(V){if(H.length>0)throw V;const K=await g(z,Y);if(!K?.ok)throw new W(K?.message??"Tweet failed to post.");return[{status:"success",message:K?.message??"Tweet posted.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:J,timeout:20});if(z.setFileInput)try{await z.setFileInput(H,J)}catch(K){if(!m(K))throw K;await A(z,H)}else await A(z,H);const V=await n(z,H.length);if(!V?.ok)throw new W(V?.message??`Image upload timed out (${F/1000}s).`)}const $=await E(z,Y);if(!$?.ok)throw new W($?.message??"Could not type tweet text.");await z.wait(1);const Z=await u(z,Y);if(!Z?.ok)throw new W(Z?.message??"Tweet was not confirmed sent; verify via readback before retrying.");return[{status:"success",message:Z?.message??"Tweet posted.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
158
+ }`));if(V?.httpStatus===401||V?.httpStatus===403)throw new N("x.com",`Twitter CreateTweet returned HTTP ${V.httpStatus}`);if(!V?.ok){const X=C(V?.bodyJson);return{ok:!1,message:L("CreateTweet",V?.httpStatus??0,X||void 0)}}const F=i(V.bodyJson);if(!F){const X=C(V.bodyJson);return{ok:!1,message:X?`CreateTweet failed: ${X}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...F}}U({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:w.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new G("Browser session required for twitter post");const H=Q.images?b(String(Q.images)):[],Y=String(Q.text??"");m(Y,{label:"推文内容"});try{await z.goto(x,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(W){if(H.length>0)throw W;const K=await g(z,Y);if(!K?.ok)throw new G(K?.message??"Tweet failed to post.");return[{status:"success",message:K?.message??"Tweet posted.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:D,timeout:20});if(z.setFileInput)try{await z.setFileInput(H,D)}catch(K){if(!S(K))throw K;await R(z,H)}else await R(z,H);const W=await E(z,H.length);if(!W?.ok)throw new G(W?.message??`Image upload timed out (${v/1000}s).`)}const $=await n(z,Y);if(!$?.ok)throw new G($?.message??"Could not type tweet text.");await z.wait(1);let Z=null,q=null;try{Z=await u(z,Y)}catch(W){q=W}if(!Z?.ok){const{confirmTweetByReadback:W}=await import("./tweets.js"),K=await W(z,Y);if(K.found)return[{status:"success",message:"提交回执未确认(可能超时),但发后回查已在时间线确认发布成功,请勿重发。",text:Y,...K.id?{id:K.id}:{},...K.url?{url:K.url}:{},confirmed_by:"readback"}];const V=q?String(q&&q.message||q):Z?.message??"Tweet was not confirmed sent.",F=K.error?`发后回查失败(${K.error})——无法确认,重试前请先人工核对时间线`:`发后回查已检查最近 ${K.checked} 条推文,未发现该内容,可安全重试`;throw new G(`${V};${F}`)}return[{status:"success",message:Z?.message??"Tweet posted.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
@@ -1,12 +1,23 @@
1
- import{cli as u,Strategy as o}from"@jackwener/opencli/registry";import{ArgumentError as A,AuthRequiredError as L,CommandExecutionError as O,EmptyResultError as s}from"@jackwener/opencli/errors";import{resolveTwitterOperationMetadata as R,sanitizeQueryId as l,extractMedia as t,extractQuotedTweet as r,normalizeTwitterGraphqlPayload as a,unwrapBrowserResult as T,normalizeTwitterScreenName as U,describeTwitterApiError as e}from"./shared.js";import{TWITTER_BEARER_TOKEN as jj,applyTopByEngagement as qj}from"./utils.js";const f="lrMzG9qPQHpqJdP3AbM-bQ",x="IGgvgiOx4QZndDHuD3x9TQ",S=100,C=100,$=S*C,E=2,b={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,payments_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},B={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},D={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},_={withPayments:!0,withAuxiliaryUserLabels:!0},Fj={queryId:f,features:b,fieldToggles:B},Gj={queryId:x,features:D,fieldToggles:_};function Hj(j){if(typeof j==="string")return{queryId:j,features:b,fieldToggles:B};return{queryId:j?.queryId||f,features:j?.features||b,fieldToggles:j?.fieldToggles||B}}function Jj(j){if(typeof j==="string")return{queryId:j,features:D,fieldToggles:_};return{queryId:j?.queryId||x,features:j?.features||D,fieldToggles:j?.fieldToggles||_}}function k(j,q,F){const G=F.fieldToggles||{},J=[`variables=${encodeURIComponent(JSON.stringify(q))}`,`features=${encodeURIComponent(JSON.stringify(F.features||{}))}`];if(Object.keys(G).length>0)J.push(`fieldToggles=${encodeURIComponent(JSON.stringify(G))}`);return`${j}?${J.join("&")}`}function N(j,q,F,G){const J=Hj(j),K={userId:q,count:F,includePromotedContent:!1,withQuickPromoteEligibilityTweetFields:!0,withVoice:!0};if(G)K.cursor=G;return k(`/i/api/graphql/${J.queryId}/UserTweets`,K,J)}function h(j,q){const F=Jj(j),G={screen_name:q,withSafetyModeUserFields:!0};return k(`/i/api/graphql/${F.queryId}/UserByScreenName`,G,F)}function y(j,q){if(!j)return null;const F=j.__typename==="TweetWithVisibilityResults"&&j.tweet?j.tweet:j.tweet||j,G=F.legacy||{};if(!F.rest_id||q.has(F.rest_id))return null;q.add(F.rest_id);const J=F.core?.user_results?.result,K=J?.legacy?.screen_name||J?.core?.screen_name||"unknown",Z=J?.legacy?.name||J?.core?.name||"",W=F.note_tweet?.note_tweet_results?.result?.text,H=Boolean(G.retweeted_status_result||G.full_text?.startsWith("RT @"));return{id:F.rest_id,author:K,name:Z,text:W||G.full_text||"",likes:G.favorite_count||0,retweets:G.retweet_count||0,replies:G.reply_count||0,views:Number(F.views?.count)||0,is_retweet:H,created_at:G.created_at||"",url:`https://x.com/${K}/status/${F.rest_id}`,...t(G),quoted_tweet:r(F)}}function d(j,q){const F=[];let G=null;const J=j?.data?.user?.result||{},Z=[J.timeline_v2?.timeline?.instructions,J.timeline?.timeline?.instructions].filter(Array.isArray).flat(),W=(H)=>{if(!H||typeof H!=="object")return;if(H.type==="TimelinePinEntry")return;if(H.tweet_results?.result){const Q=y(H.tweet_results.result,q);if(Q)F.push(Q)}if((H.entryType==="TimelineTimelineCursor"||H.__typename==="TimelineTimelineCursor")&&(H.cursorType==="Bottom"||H.cursorType==="ShowMore")&&H.value)G=H.value;if(Array.isArray(H)){for(const Q of H)W(Q);return}for(const Q of Object.values(H))if(Q&&typeof Q==="object")W(Q)};W(Z);return{tweets:F,nextCursor:G}}function w(j){const q=j??20;if(!Number.isInteger(q)||q<1||q>$)throw new A(`twitter tweets --limit must be an integer between 1 and ${$}`,"Example: ppcli twitter tweets @jack --limit 250");return q}function c(j){const q=j??E;if(!Number.isInteger(q)||q<0||q>60)throw new A("twitter tweets --page-delay must be an integer between 0 and 60 seconds","Example: ppcli twitter tweets @jack --limit 250 --page-delay 2");return q}u({site:"twitter",name:"tweets",access:"read",description:"Fetch a Twitter user's most recent tweets (chronological, excludes pinned; defaults to the logged-in user when no username is given)",domain:"x.com",strategy:o.COOKIE,browser:!0,args:[{name:"username",type:"string",positional:!0,help:"Twitter screen name (with or without @). Defaults to the logged-in user when omitted."},{name:"limit",type:"int",default:20,help:`Max tweets to return (1-${$}; fetched across cursor pages)`},{name:"page-delay",type:"int",default:E,help:"Seconds to wait between paginated timeline requests to reduce rate-limit risk. Use 0 to disable."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the tweets by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the chronological ordering."}],columns:["id","author","created_at","is_retweet","text","likes","retweets","replies","views","url","has_media","media_urls","media_posters","quoted_tweet"],func:async(j,q)=>{const F=w(q.limit),G=c(q["page-delay"]),J=String(q.username??"").trim();let K=U(J);if(J&&!K)throw new A("twitter tweets username must be a valid Twitter/X handle","Example: ppcli twitter tweets @jack --limit 20");if(!K){await j.goto("https://x.com/home");await j.wait({selector:'[data-testid="primaryColumn"]'});const V=T(await j.evaluate(`() => {
1
+ import{cli as r,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as C,AuthRequiredError as I,CommandExecutionError as h,EmptyResultError as e}from"@jackwener/opencli/errors";import{resolveTwitterOperationMetadata as A,sanitizeQueryId as jj,extractMedia as qj,extractQuotedTweet as Fj,normalizeTwitterGraphqlPayload as y,unwrapBrowserResult as D,normalizeTwitterScreenName as R,describeTwitterApiError as v}from"./shared.js";import{TWITTER_BEARER_TOKEN as w,applyTopByEngagement as Gj}from"./utils.js";const c="lrMzG9qPQHpqJdP3AbM-bQ",d="IGgvgiOx4QZndDHuD3x9TQ",m=100,g=100,B=m*g,p=2,_={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,payments_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},O={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},S={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},T={withPayments:!0,withAuxiliaryUserLabels:!0},i={queryId:c,features:_,fieldToggles:O},n={queryId:d,features:S,fieldToggles:T};function Hj(j){if(typeof j==="string")return{queryId:j,features:_,fieldToggles:O};return{queryId:j?.queryId||c,features:j?.features||_,fieldToggles:j?.fieldToggles||O}}function Jj(j){if(typeof j==="string")return{queryId:j,features:S,fieldToggles:T};return{queryId:j?.queryId||d,features:j?.features||S,fieldToggles:j?.fieldToggles||T}}function u(j,q,F){const G=F.fieldToggles||{},H=[`variables=${encodeURIComponent(JSON.stringify(q))}`,`features=${encodeURIComponent(JSON.stringify(F.features||{}))}`];if(Object.keys(G).length>0)H.push(`fieldToggles=${encodeURIComponent(JSON.stringify(G))}`);return`${j}?${H.join("&")}`}function x(j,q,F,G){const H=Hj(j),J={userId:q,count:F,includePromotedContent:!1,withQuickPromoteEligibilityTweetFields:!0,withVoice:!0};if(G)J.cursor=G;return u(`/i/api/graphql/${H.queryId}/UserTweets`,J,H)}function E(j,q){const F=Jj(j),G={screen_name:q,withSafetyModeUserFields:!0};return u(`/i/api/graphql/${F.queryId}/UserByScreenName`,G,F)}function o(j,q){if(!j)return null;const F=j.__typename==="TweetWithVisibilityResults"&&j.tweet?j.tweet:j.tweet||j,G=F.legacy||{};if(!F.rest_id||q.has(F.rest_id))return null;q.add(F.rest_id);const H=F.core?.user_results?.result,J=H?.legacy?.screen_name||H?.core?.screen_name||"unknown",Q=H?.legacy?.name||H?.core?.name||"",X=F.note_tweet?.note_tweet_results?.result?.text,K=Boolean(G.retweeted_status_result||G.full_text?.startsWith("RT @"));return{id:F.rest_id,author:J,name:Q,text:X||G.full_text||"",likes:G.favorite_count||0,retweets:G.retweet_count||0,replies:G.reply_count||0,views:Number(F.views?.count)||0,is_retweet:K,created_at:G.created_at||"",url:`https://x.com/${J}/status/${F.rest_id}`,...qj(G),quoted_tweet:Fj(F)}}function N(j,q){const F=[];let G=null;const H=j?.data?.user?.result||{},Q=[H.timeline_v2?.timeline?.instructions,H.timeline?.timeline?.instructions].filter(Array.isArray).flat(),X=(K)=>{if(!K||typeof K!=="object")return;if(K.type==="TimelinePinEntry")return;if(K.tweet_results?.result){const V=o(K.tweet_results.result,q);if(V)F.push(V)}if((K.entryType==="TimelineTimelineCursor"||K.__typename==="TimelineTimelineCursor")&&(K.cursorType==="Bottom"||K.cursorType==="ShowMore")&&K.value)G=K.value;if(Array.isArray(K)){for(const V of K)X(V);return}for(const V of Object.values(K))if(V&&typeof V==="object")X(V)};X(Q);return{tweets:F,nextCursor:G}}function l(j){const q=j??20;if(!Number.isInteger(q)||q<1||q>B)throw new C(`twitter tweets --limit must be an integer between 1 and ${B}`,"Example: ppcli twitter tweets @jack --limit 250");return q}function s(j){const q=j??p;if(!Number.isInteger(q)||q<0||q>60)throw new C("twitter tweets --page-delay must be an integer between 0 and 60 seconds","Example: ppcli twitter tweets @jack --limit 250 --page-delay 2");return q}export async function confirmTweetByReadback(j,q,F={}){const G=Math.max(1,Math.min(40,Number(F.limit)||20)),H=(Q)=>String(Q||"").replace(/ /g," ").replace(/\s+/g," ").trim(),J=H(q);if(!J)return{found:!1,checked:0,error:"empty expected text"};try{await j.goto("https://x.com/home");await j.wait({selector:'[data-testid="primaryColumn"]'});const Q=D(await j.evaluate(`() => {
2
+ const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
3
+ return link ? link.getAttribute('href') : null;
4
+ }`)),X=R(typeof Q==="string"?Q:"");if(!X)return{found:!1,checked:0,error:"could not detect logged-in user"};const V=(await j.getCookies({url:"https://x.com"})).find(($)=>$.name==="ct0")?.value||null;if(!V)return{found:!1,checked:0,error:"no ct0 cookie"};const b=await A(j,"UserTweets",i),z=await A(j,"UserByScreenName",n),M=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(w)}`,"X-Csrf-Token":V,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),U=E(z,X),Z=D(await j.evaluate(`async () => {
5
+ const resp = await fetch("${U}", { headers: ${M}, credentials: 'include' });
6
+ if (!resp.ok) return null;
7
+ const d = await resp.json();
8
+ return d?.data?.user?.result?.rest_id || null;
9
+ }`));if(!Z)return{found:!1,checked:0,error:`could not resolve @${X}`};const P=x(b,Z,G,null),Y=y(await j.evaluate(`async () => {
10
+ const r = await fetch("${P}", { headers: ${M}, credentials: 'include' });
11
+ return r.ok ? await r.json() : { error: r.status };
12
+ }`));if(Y?.error)return{found:!1,checked:0,error:v("UserTweets",Y.error)};const{tweets:W}=N(Y,new Set);for(const $ of W){const L=H($?.text);if(!L)continue;if(L===J||L.includes(J)||J.includes(L))return{found:!0,id:$.id,url:$.url,checked:W.length}}return{found:!1,checked:W.length}}catch(Q){return{found:!1,checked:0,error:String(Q&&Q.message||Q)}}}r({site:"twitter",name:"tweets",access:"read",description:"Fetch a Twitter user's most recent tweets (chronological, excludes pinned; defaults to the logged-in user when no username is given)",domain:"x.com",strategy:a.COOKIE,browser:!0,args:[{name:"username",type:"string",positional:!0,help:"Twitter screen name (with or without @). Defaults to the logged-in user when omitted."},{name:"limit",type:"int",default:20,help:`Max tweets to return (1-${B}; fetched across cursor pages)`},{name:"page-delay",type:"int",default:p,help:"Seconds to wait between paginated timeline requests to reduce rate-limit risk. Use 0 to disable."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the tweets by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the chronological ordering."}],columns:["id","author","created_at","is_retweet","text","likes","retweets","replies","views","url","has_media","media_urls","media_posters","quoted_tweet"],func:async(j,q)=>{const F=l(q.limit),G=s(q["page-delay"]),H=String(q.username??"").trim();let J=R(H);if(H&&!J)throw new C("twitter tweets username must be a valid Twitter/X handle","Example: ppcli twitter tweets @jack --limit 20");if(!J){await j.goto("https://x.com/home");await j.wait({selector:'[data-testid="primaryColumn"]'});const W=D(await j.evaluate(`() => {
2
13
  const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
3
14
  return link ? link.getAttribute('href') : null;
4
- }`));if(!V||typeof V!=="string")throw new L("x.com","Could not detect logged-in user. Are you logged in?");K=U(V);if(!K)throw new L("x.com","Could not detect logged-in user. Are you logged in?")}const W=(await j.getCookies({url:"https://x.com"})).find((V)=>V.name==="ct0")?.value||null;if(!W)throw new L("x.com","Not logged into x.com (no ct0 cookie)");const H=await R(j,"UserTweets",Fj),Q=await R(j,"UserByScreenName",Gj),z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(jj)}`,"X-Csrf-Token":W,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),m=h(Q,K),I=T(await j.evaluate(`async () => {
5
- const resp = await fetch("${m}", { headers: ${z}, credentials: 'include' });
15
+ }`));if(!W||typeof W!=="string")throw new I("x.com","Could not detect logged-in user. Are you logged in?");J=R(W);if(!J)throw new I("x.com","Could not detect logged-in user. Are you logged in?")}const X=(await j.getCookies({url:"https://x.com"})).find((W)=>W.name==="ct0")?.value||null;if(!X)throw new I("x.com","Not logged into x.com (no ct0 cookie)");const K=await A(j,"UserTweets",i),V=await A(j,"UserByScreenName",n),b=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(w)}`,"X-Csrf-Token":X,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),z=E(V,J),M=D(await j.evaluate(`async () => {
16
+ const resp = await fetch("${z}", { headers: ${b}, credentials: 'include' });
6
17
  if (!resp.ok) return null;
7
18
  const d = await resp.json();
8
19
  return d?.data?.user?.result?.rest_id || null;
9
- }`));if(!I)throw new O(`Could not resolve @${K}`);const v=new Set,X=[];let M=null;for(let V=0;V<S&&X.length<F;V++){if(V>0&&G>0)await j.wait(G);const p=Math.min(C,F-X.length+10),n=N(H,I,p,M),P=a(await j.evaluate(`async () => {
10
- const r = await fetch("${n}", { headers: ${z}, credentials: 'include' });
20
+ }`));if(!M)throw new h(`Could not resolve @${J}`);const U=new Set,Z=[];let P=null;for(let W=0;W<m&&Z.length<F;W++){if(W>0&&G>0)await j.wait(G);const $=Math.min(g,F-Z.length+10),L=x(K,M,$,P),f=y(await j.evaluate(`async () => {
21
+ const r = await fetch("${L}", { headers: ${b}, credentials: 'include' });
11
22
  return r.ok ? await r.json() : { error: r.status };
12
- }`));if(P?.error){if(X.length===0)throw new O(e("UserTweets",P.error));break}const{tweets:i,nextCursor:Y}=d(P,v);X.push(...i);if(!Y||Y===M)break;M=Y}if(X.length===0)throw new s(`@${K} has no recent tweets`,"Account may be private or suspended");const g=X.slice(0,F);return qj(g,q["top-by-engagement"])}});export const __test__={MAX_TWEETS_LIMIT:$,sanitizeQueryId:l,buildUserTweetsUrl:N,buildUserByScreenNameUrl:h,extractTweet:y,parseUserTweets:d,normalizeLimit:w,normalizePageDelaySeconds:c};
23
+ }`));if(f?.error){if(Z.length===0)throw new h(v("UserTweets",f.error));break}const{tweets:t,nextCursor:k}=N(f,U);Z.push(...t);if(!k||k===P)break;P=k}if(Z.length===0)throw new e(`@${J} has no recent tweets`,"Account may be private or suspended");const Y=Z.slice(0,F);return Gj(Y,q["top-by-engagement"])}});export const __test__={MAX_TWEETS_LIMIT:B,sanitizeQueryId:jj,buildUserTweetsUrl:x,buildUserByScreenNameUrl:E,extractTweet:o,parseUserTweets:N,normalizeLimit:l,normalizePageDelaySeconds:s};
@@ -1 +1 @@
1
- import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(L)=>{if(L?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.proxyAuth){this.on("Fetch.requestPaused",(L)=>{const Y=L;if(Y.requestId)this.send("Fetch.continueRequest",{requestId:Y.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(L)=>{const Y=L;if(!Y.requestId)return;this.send("Fetch.continueWithAuth",{requestId:Y.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(L){Z.close();X(L instanceof Error?L:Error(String(L)));return}K(new O(this))});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
1
+ import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(Y)=>{if(Y?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.windowMode==="background")await this.send("Emulation.setFocusEmulationEnabled",{enabled:!0}).catch(()=>{});else if(G?.windowMode==="foreground")await this.send("Page.bringToFront").catch(()=>{});if(G?.proxyAuth){this.on("Fetch.requestPaused",(Y)=>{const A=Y;if(A.requestId)this.send("Fetch.continueRequest",{requestId:A.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(Y)=>{const A=Y;if(!A.requestId)return;this.send("Fetch.continueWithAuth",{requestId:A.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(Y){Z.close();X(Y instanceof Error?Y:Error(String(Y)));return}const L=new O(this);if(G?.session)L.session=G.session;if(G?.contextId)L.contextId=G.contextId;K(L)});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;session;contextId;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
@@ -19,8 +19,12 @@ export declare function isManagedChromeRunning(entry: ManagedProfileEntry): Prom
19
19
  * 已在跑 → 直接复用;没在跑 → 拉起并等就绪。
20
20
  * 「探测→拉起→等就绪」整段按 profile 互斥(进程内 Promise 去重 + 跨进程文件锁),
21
21
  * 保证任一时刻同一 profile 只有一路真正执行启动。
22
+ * windowMode 决定窗口状态协调:background 新建窗口即最小化(真后台),
23
+ * foreground 把最小化窗口还原(登录等用户必须看见的流程)。
22
24
  */
23
- export declare function ensureManagedChrome(name: string, entry: ManagedProfileEntry): Promise<string>;
25
+ export declare function ensureManagedChrome(name: string, entry: ManagedProfileEntry, opts?: {
26
+ windowMode?: 'foreground' | 'background';
27
+ }): Promise<string>;
24
28
  /** 通过 CDP Browser.close 优雅关闭托管 Chrome。返回是否真的关了(本来没跑返回 false)。 */
25
29
  export declare function stopManagedChrome(entry: ManagedProfileEntry): Promise<boolean>;
26
30
  export type ManagedProfileOpts = {
@@ -1 +1 @@
1
- import{execFileSync as R}from"node:child_process";import*as B from"node:fs";import*as W from"node:path";import{request as x}from"node:http";import{WebSocket as T}from"ws";import{probeCDP as q,launchDetachedApp as v}from"../launcher.js";import{withBrowserSessionLock as C}from"../browser-session-lock.js";import{CommandExecutionError as O}from"../errors.js";import{log as J}from"../logger.js";import{addManagedProfile as M,getManagedProfile as N,listManagedProfiles as _,managedProfilesBaseDir as w,removeManagedProfileEntry as b,updateManagedProfile as A}from"./profile.js";import{proxyServerFlag as D}from"./proxy-test.js";import{resolveFingerprint as E,randomSeed as P}from"./fingerprint.js";const f=500,L=20000,j=9301,k=9399;function F(K){for(const H of K)try{B.accessSync(H,B.constants.X_OK);return H}catch{}return null}function g(K){for(const H of K)try{const Q=R("which",[H],{encoding:"utf-8",stdio:"pipe"}).trim();if(Q)return Q}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return F(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const H=process.env.ProgramFiles??"C:\\Program Files",Q=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Y=process.env.LOCALAPPDATA??"";return F([W.join(H,"Google","Chrome","Application","chrome.exe"),W.join(Q,"Google","Chrome","Application","chrome.exe"),...Y?[W.join(Y,"Google","Chrome","Application","chrome.exe")]:[],W.join(H,"Microsoft","Edge","Application","msedge.exe"),W.join(Q,"Microsoft","Edge","Application","msedge.exe")])}return g(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function G(K,H,Q=5000){return new Promise((Y,Z)=>{const V=new URL(H),$=x({hostname:V.hostname,port:V.port,path:V.pathname+V.search,method:K,timeout:Q},(z)=>{const U=[];z.on("data",(X)=>U.push(Buffer.isBuffer(X)?X:Buffer.from(X)));z.on("end",()=>{const X=Buffer.concat(U).toString("utf8");if((z.statusCode??0)<200||(z.statusCode??0)>=300){Z(Error(`HTTP ${z.statusCode}: ${X.slice(0,200)}`));return}try{Y(X?JSON.parse(X):null)}catch{Y(X)}})});$.on("error",Z);$.on("timeout",()=>$.destroy(Error(`请求 ${H} 超时`)));$.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return q(K.port)}const m=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function d(K){const H=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)H.push(...m);if(K.webrtc!==!1)H.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)H.push(`--proxy-server=${D(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)H.push(`--lang=${K.fingerprint.locale}`);H.push("about:blank");return H}function u(K){const H=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return H?{TZ:H}:void 0}async function y(K,H){const Q=Date.now()+L;while(Date.now()<Q){if(await q(K,1000))return;await new Promise((Y)=>setTimeout(Y,f))}throw new O(`托管 Chrome「${H}」已启动但 ${L/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}async function S(K){try{const H=await G("GET",`${K}/json`);if(!(Array.isArray(H)&&H.some((Y)=>Y.type==="page")))await G("PUT",`${K}/json/new?about:blank`)}catch(H){J.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${H instanceof Error?H.message:H}`)}}const I=new Map;export async function ensureManagedChrome(K,H){const Q=managedEndpoint(H),Y=I.get(K);if(Y)return Y;if(await q(H.port)){J.debug(`[managed-chrome] profile「${K}」已在端口 ${H.port} 运行`);await S(Q);return Q}const Z=I.get(K);if(Z)return Z;const V=C(async()=>{if(await q(H.port)){J.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await S(Q);return Q}const $=H.browserPath??discoverChromePath();if(!$)throw new O("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(H.userDataDir,{recursive:!0});J.debug(`[managed-chrome] 拉起 profile「${K}」:${$} 端口 ${H.port}${H.proxy?" (代理)":""}`);await v($,d(H),`托管 profile ${K}`,u(H));await y(H.port,K);await S(Q);return Q},`launch-${K}`).finally(()=>I.delete(K));I.set(K,V);return V}export async function stopManagedChrome(K){if(!await q(K.port))return!1;const Q=(await G("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Q)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise((Z,V)=>{const $=new T(Q),z=setTimeout(()=>{$.close();V(Error("CDP Browser.close 超时"))},1e4);$.on("open",()=>{$.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});$.on("close",()=>{clearTimeout(z);Z()});$.on("error",(U)=>{clearTimeout(z);V(U)})});const Y=Date.now()+5000;while(Date.now()<Y){if(!await q(K.port,500))break;await new Promise((Z)=>setTimeout(Z,200))}if(process.platform!=="win32"){const Z=W.join(K.userDataDir,"SingletonLock"),V=Date.now()+15000;while(Date.now()<V){try{B.lstatSync(Z)}catch{return!0}await new Promise(($)=>setTimeout($,200))}J.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${Z}`)}return!0}async function h(K){return!await q(K,500)}async function l(){const K=new Set(_().map(({entry:H})=>H.port));for(let H=j;H<=k;H++){if(K.has(H))continue;if(await h(H))return H}throw Error(`托管 profile 端口区间 ${j}-${k} 已耗尽`)}export async function resolveManagedOpts(K){const H={};if(K.label)H.label=K.label;if(K.proxy)H.proxy=K.proxy;if(K.webrtc===!1)H.webrtc=!1;if(K.clean===!1)H.clean=!1;if(K.fingerprint?.enabled){const Q=K.fingerprint,Y=Q.seed??P();H.fingerprint=E(Y,{timezone:Q.timezone,locale:Q.locale,userAgent:Q.userAgent,platform:Q.platform,spoof:Q.spoof,hardwareConcurrency:Q.hardwareConcurrency,deviceMemory:Q.deviceMemory,screen:Q.screen,webglVendor:Q.webglVendor,webglRenderer:Q.webglRenderer,canvasNoise:Q.canvasNoise})}return{opts:H}}export async function createManagedProfile(K,H={}){const Q=await l(),Y=W.join(w(),K.trim()),Z={userDataDir:Y,port:Q,...H.browserPath?{browserPath:H.browserPath}:{},...H.label?{label:H.label}:{},...H.proxy?{proxy:H.proxy}:{},...H.fingerprint?{fingerprint:H.fingerprint}:{},...H.webrtc===!1?{webrtc:!1}:{},...H.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};M(K,Z);B.rmSync(Y,{recursive:!0,force:!0});B.mkdirSync(Y,{recursive:!0});return Z}export async function updateManagedProfileConfig(K,H){const Q=N(K);if(!Q)throw Error(`托管 profile "${K}" 不存在`);const Y=H.webrtc!==void 0?H.webrtc:Q.webrtc,Z=H.clean!==void 0?H.clean:Q.clean,V={userDataDir:Q.userDataDir,port:Q.port,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.createdAt?{createdAt:Q.createdAt}:{},...H.label!==void 0?H.label?{label:H.label}:{}:Q.label?{label:Q.label}:{},...H.proxy!==void 0?H.proxy?{proxy:H.proxy}:{}:Q.proxy?{proxy:Q.proxy}:{},...H.fingerprint!==void 0?H.fingerprint?{fingerprint:H.fingerprint}:{}:Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Y===!1?{webrtc:!1}:{},...Z===!1?{clean:!1}:{}};let $=!1;try{$=await stopManagedChrome(Q)}catch(z){J.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${z instanceof Error?z.message:z}`)}A(K,V);return{entry:V,wasRunning:$}}async function c(K){const H=Date.now()+15000;let Q=0;while(Date.now()<H){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Y)=>setTimeout(Y,300));if(!B.existsSync(K)){if(++Q>=2)return}else Q=0}throw new O(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,H={}){const Q=N(K);if(!Q)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Q)}catch(Y){J.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Y instanceof Error?Y.message:Y}`)}if(!H.keepData){const Y=w(),Z=W.resolve(Q.userDataDir);if(Z.startsWith(Y+W.sep))await c(Z);else J.warn(`[managed-chrome] user-data-dir 不在 ${Y} 下,跳过数据删除:${Z}`)}b(K)}
1
+ import{execFileSync as D}from"node:child_process";import*as B from"node:fs";import*as q from"node:path";import{request as E}from"node:http";import{WebSocket as L}from"ws";import{probeCDP as I,launchDetachedApp as P}from"../launcher.js";import{withBrowserSessionLock as f}from"../browser-session-lock.js";import{CommandExecutionError as S}from"../errors.js";import{log as U}from"../logger.js";import{addManagedProfile as w,getManagedProfile as R,listManagedProfiles as u,managedProfilesBaseDir as C,removeManagedProfileEntry as y,updateManagedProfile as g}from"./profile.js";import{proxyServerFlag as m}from"./proxy-test.js";import{resolveFingerprint as h,randomSeed as l}from"./fingerprint.js";const c=500,_=20000,j=9301,v=9399;function A(K){for(const Q of K)try{B.accessSync(Q,B.constants.X_OK);return Q}catch{}return null}function d(K){for(const Q of K)try{const Y=D("which",[Q],{encoding:"utf-8",stdio:"pipe"}).trim();if(Y)return Y}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return A(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const Q=process.env.ProgramFiles??"C:\\Program Files",Y=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Z=process.env.LOCALAPPDATA??"";return A([q.join(Q,"Google","Chrome","Application","chrome.exe"),q.join(Y,"Google","Chrome","Application","chrome.exe"),...Z?[q.join(Z,"Google","Chrome","Application","chrome.exe")]:[],q.join(Q,"Microsoft","Edge","Application","msedge.exe"),q.join(Y,"Microsoft","Edge","Application","msedge.exe")])}return d(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function F(K,Q,Y=5000){return new Promise((Z,$)=>{const X=new URL(Q),H=E({hostname:X.hostname,port:X.port,path:X.pathname+X.search,method:K,timeout:Y},(V)=>{const J=[];V.on("data",(z)=>J.push(Buffer.isBuffer(z)?z:Buffer.from(z)));V.on("end",()=>{const z=Buffer.concat(J).toString("utf8");if((V.statusCode??0)<200||(V.statusCode??0)>=300){$(Error(`HTTP ${V.statusCode}: ${z.slice(0,200)}`));return}try{Z(z?JSON.parse(z):null)}catch{Z(z)}})});H.on("error",$);H.on("timeout",()=>H.destroy(Error(`请求 ${Q} 超时`)));H.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return I(K.port)}const i=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function p(K){const Q=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)Q.push(...i);if(K.webrtc!==!1)Q.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)Q.push(`--proxy-server=${m(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)Q.push(`--lang=${K.fingerprint.locale}`);Q.push("--no-startup-window");return Q}function o(K){const Q=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return Q?{TZ:Q}:void 0}async function s(K,Q){const Y=Date.now()+_;while(Date.now()<Y){if(await I(K,1000))return;await new Promise((Z)=>setTimeout(Z,c))}throw new S(`托管 Chrome「${Q}」已启动但 ${_/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}const a="https://publishport.app/automation";async function b(K,Q){const Z=(await F("GET",`${K}/json/version`))?.webSocketDebuggerUrl;if(!Z)throw Error(`CDP 端点 ${K} 没有暴露 browser target`);const $=new L(Z);await new Promise((J,z)=>{const W=setTimeout(()=>{$.close();z(Error("browser CDP 连接超时"))},1e4);$.on("open",()=>{clearTimeout(W);J()});$.on("error",(G)=>{clearTimeout(W);z(G)})});let X=0;const H=new Map;$.on("message",(J)=>{try{const z=JSON.parse(String(J));if(!z.id||!H.has(z.id))return;const W=H.get(z.id);H.delete(z.id);if(z.error)W.reject(Error(z.error.message??"CDP error"));else W.resolve(z.result??{})}catch{}});const V=(J,z={})=>{const W=++X;return new Promise((G,T)=>{const k=setTimeout(()=>{H.delete(W);T(Error(`CDP ${J} 超时`))},1e4);H.set(W,{resolve:(N)=>{clearTimeout(k);G(N)},reject:(N)=>{clearTimeout(k);T(N)}});$.send(JSON.stringify({id:W,method:J,params:z}))})};try{return await Q(V)}finally{$.close()}}async function x(K,Q){try{const Y=await F("GET",`${K}/json`),Z=Array.isArray(Y)?Y.find(($)=>$.type==="page"):void 0;if(!Z)await b(K,async($)=>{const H=(await $("Target.createTarget",{url:a,background:!0})).targetId;if(H&&Q==="background"){const V=await $("Browser.getWindowForTarget",{targetId:H});if(V.windowId!==void 0)for(let J=0;J<4;J++){await $("Browser.setWindowBounds",{windowId:V.windowId,bounds:{windowState:"minimized"}});await new Promise((W)=>setTimeout(W,250));if((await $("Browser.getWindowBounds",{windowId:V.windowId})).bounds?.windowState==="minimized")break}}});else if(Q==="foreground"&&Z.id)await b(K,async($)=>{const X=await $("Browser.getWindowForTarget",{targetId:Z.id});if((await $("Browser.getWindowBounds",{windowId:X.windowId})).bounds?.windowState==="minimized")await $("Browser.setWindowBounds",{windowId:X.windowId,bounds:{windowState:"normal"}})})}catch(Y){U.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${Y instanceof Error?Y.message:Y}`)}}const O=new Map;async function M(K){if(await I(K.port))return!0;if(process.platform==="win32")return!1;const Q=q.join(K.userDataDir,"SingletonLock"),Y=()=>{try{B.lstatSync(Q);return!0}catch{return!1}};if(!Y())return!1;for(let Z=0;Z<3;Z++){await new Promise(($)=>setTimeout($,500));if(await I(K.port,4000))return!0;if(!Y())return!1}return!1}export async function ensureManagedChrome(K,Q,Y={}){const Z=managedEndpoint(Q),$=O.get(K);if($)return $;if(await M(Q)){U.debug(`[managed-chrome] profile「${K}」已在端口 ${Q.port} 运行`);await x(Z,Y.windowMode);return Z}const X=O.get(K);if(X)return X;const H=f(async()=>{if(await M(Q)){U.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await x(Z,Y.windowMode);return Z}const V=Q.browserPath??discoverChromePath();if(!V)throw new S("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(Q.userDataDir,{recursive:!0});U.debug(`[managed-chrome] 拉起 profile「${K}」:${V} 端口 ${Q.port}${Q.proxy?" (代理)":""}`);await P(V,p(Q),`托管 profile ${K}`,o(Q));await s(Q.port,K);await x(Z,Y.windowMode);return Z},`launch-${K}`).finally(()=>O.delete(K));O.set(K,H);return H}export async function stopManagedChrome(K){if(!await I(K.port))return!1;const Y=(await F("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Y)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise(($,X)=>{const H=new L(Y),V=setTimeout(()=>{H.close();X(Error("CDP Browser.close 超时"))},1e4);H.on("open",()=>{H.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});H.on("close",()=>{clearTimeout(V);$()});H.on("error",(J)=>{clearTimeout(V);X(J)})});const Z=Date.now()+5000;while(Date.now()<Z){if(!await I(K.port,500))break;await new Promise(($)=>setTimeout($,200))}if(process.platform!=="win32"){const $=q.join(K.userDataDir,"SingletonLock"),X=Date.now()+15000;while(Date.now()<X){try{B.lstatSync($)}catch{return!0}await new Promise((H)=>setTimeout(H,200))}U.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${$}`)}return!0}async function n(K){return!await I(K,500)}async function t(){const K=new Set(u().map(({entry:Q})=>Q.port));for(let Q=j;Q<=v;Q++){if(K.has(Q))continue;if(await n(Q))return Q}throw Error(`托管 profile 端口区间 ${j}-${v} 已耗尽`)}export async function resolveManagedOpts(K){const Q={};if(K.label)Q.label=K.label;if(K.proxy)Q.proxy=K.proxy;if(K.webrtc===!1)Q.webrtc=!1;if(K.clean===!1)Q.clean=!1;if(K.fingerprint?.enabled){const Y=K.fingerprint,Z=Y.seed??l();Q.fingerprint=h(Z,{timezone:Y.timezone,locale:Y.locale,userAgent:Y.userAgent,platform:Y.platform,spoof:Y.spoof,hardwareConcurrency:Y.hardwareConcurrency,deviceMemory:Y.deviceMemory,screen:Y.screen,webglVendor:Y.webglVendor,webglRenderer:Y.webglRenderer,canvasNoise:Y.canvasNoise})}return{opts:Q}}export async function createManagedProfile(K,Q={}){const Y=await t(),Z=q.join(C(),K.trim()),$={userDataDir:Z,port:Y,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.label?{label:Q.label}:{},...Q.proxy?{proxy:Q.proxy}:{},...Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Q.webrtc===!1?{webrtc:!1}:{},...Q.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};w(K,$);B.rmSync(Z,{recursive:!0,force:!0});B.mkdirSync(Z,{recursive:!0});return $}export async function updateManagedProfileConfig(K,Q){const Y=R(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);const Z=Q.webrtc!==void 0?Q.webrtc:Y.webrtc,$=Q.clean!==void 0?Q.clean:Y.clean,X={userDataDir:Y.userDataDir,port:Y.port,...Y.browserPath?{browserPath:Y.browserPath}:{},...Y.createdAt?{createdAt:Y.createdAt}:{},...Q.label!==void 0?Q.label?{label:Q.label}:{}:Y.label?{label:Y.label}:{},...Q.proxy!==void 0?Q.proxy?{proxy:Q.proxy}:{}:Y.proxy?{proxy:Y.proxy}:{},...Q.fingerprint!==void 0?Q.fingerprint?{fingerprint:Q.fingerprint}:{}:Y.fingerprint?{fingerprint:Y.fingerprint}:{},...Z===!1?{webrtc:!1}:{},...$===!1?{clean:!1}:{}};let H=!1;try{H=await stopManagedChrome(Y)}catch(V){U.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${V instanceof Error?V.message:V}`)}g(K,X);return{entry:X,wasRunning:H}}async function r(K){const Q=Date.now()+15000;let Y=0;while(Date.now()<Q){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Z)=>setTimeout(Z,300));if(!B.existsSync(K)){if(++Y>=2)return}else Y=0}throw new S(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,Q={}){const Y=R(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Y)}catch(Z){U.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Z instanceof Error?Z.message:Z}`)}if(!Q.keepData){const Z=C(),$=q.resolve(Y.userDataDir);if($.startsWith(Z+q.sep))await r($);else U.warn(`[managed-chrome] user-data-dir 不在 ${Z} 下,跳过数据删除:${$}`)}y(K)}