publishport-opencli 1.0.3 → 1.0.5

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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false