publishport-opencli 1.0.30 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-manifest.json +344 -70
- package/clis/_shared/article/login.js +1 -1
- package/clis/_shared/login-wait.js +1 -0
- package/clis/_shared/site-auth.js +1 -1
- package/clis/baijiahao/article.js +2 -2
- package/clis/baijiahao/auth.js +18 -0
- package/clis/baijiahao/lists.js +1 -1
- package/clis/baijiahao/stats.js +1 -1
- package/clis/bilibili/coin.js +1 -1
- package/clis/bilibili/comment.js +1 -1
- package/clis/bilibili/comments.js +1 -1
- package/clis/bilibili/dm-list.js +1 -1
- package/clis/bilibili/dm-read.js +1 -1
- package/clis/bilibili/favorite-add.js +1 -1
- package/clis/bilibili/me.js +1 -1
- package/clis/bilibili/search.js +1 -1
- package/clis/bilibili/stats.js +1 -1
- package/clis/bilibili/user-videos.js +1 -1
- package/clis/bilibili/utils.js +7 -7
- package/clis/cnblogs/article.js +1 -1
- package/clis/csdn/stats.js +7 -13
- package/clis/devto/publish.js +20 -4
- package/clis/douyin/_shared/work-list.js +1 -0
- package/clis/douyin/publish.js +9 -8
- package/clis/douyin/stats.js +1 -1
- package/clis/douyin/videos.js +1 -1
- package/clis/jike/comments.js +2 -1
- package/clis/jike/like.js +1 -1
- package/clis/jike/notifications.js +2 -115
- package/clis/jike/post.js +2 -2
- package/clis/jike/topic.js +1 -1
- package/clis/jike/topics.js +3 -0
- package/clis/nowcoder/auth.js +1 -24
- package/clis/nowcoder/comment.js +1 -1
- package/clis/nowcoder/comments.js +1 -1
- package/clis/nowcoder/delete.js +1 -1
- package/clis/nowcoder/detail.js +1 -48
- package/clis/nowcoder/dm-list.js +1 -1
- package/clis/nowcoder/dm-read.js +1 -1
- package/clis/nowcoder/dm-send.js +1 -1
- package/clis/nowcoder/experience.js +1 -22
- package/clis/nowcoder/inbox.js +1 -1
- package/clis/nowcoder/like-status.js +1 -0
- package/clis/nowcoder/like.js +1 -1
- package/clis/nowcoder/my.js +1 -1
- package/clis/nowcoder/notifications.js +1 -16
- package/clis/nowcoder/post.js +1 -1
- package/clis/nowcoder/recommend.js +1 -1
- package/clis/nowcoder/search.js +1 -32
- package/clis/nowcoder/utils.js +38 -44
- package/clis/qiita/publish.js +2 -2
- package/clis/quora/__fixtures__/dom.html +24 -0
- package/clis/quora/answer-detail.js +1 -1
- package/clis/quora/question.js +25 -1
- package/clis/quora/search.js +1 -1
- package/clis/quora/shared.js +18 -17
- package/clis/quora/upvote.js +5 -13
- package/clis/reddit/comment.js +25 -25
- package/clis/reddit/reply.js +7 -5
- package/clis/reddit/search.js +1 -51
- package/clis/reddit/shared.js +13 -0
- package/clis/reddit/subreddit-info.js +10 -4
- package/clis/reddit/subreddit-rules.js +1 -0
- package/clis/reddit/subreddit.js +1 -52
- package/clis/reddit/upvote.js +33 -10
- package/clis/reddit/user-comments.js +1 -20
- package/clis/reddit/user-posts.js +1 -17
- package/clis/twitter/post.js +63 -39
- package/clis/wechat-channels/auth.js +34 -31
- package/clis/wechat-channels/publish.js +295 -130
- package/clis/xiaohongshu/_shared/note-manager.js +37 -0
- package/clis/xiaohongshu/creator-notes.js +9 -43
- package/clis/xiaohongshu/delete-note.js +7 -19
- package/clis/xiaohongshu/draft-delete.js +2 -2
- package/clis/xiaohongshu/publish.js +34 -43
- package/clis/xiaohongshu/search.js +18 -3
- package/clis/youtube/publish.js +11 -11
- package/clis/zhihu/answer-comments.js +2 -11
- package/clis/zhihu/answer-detail.js +1 -11
- package/clis/zhihu/comment.js +4 -4
- package/clis/zhihu/like.js +17 -3
- package/clis/zhihu/question.js +20 -6
- package/clis/zhihu/read-shared.js +26 -0
- package/clis/zhihu/recommend.js +1 -7
- package/clis/zhihu/search.js +1 -11
- package/dist/src/browser/base-page.d.ts +41 -1
- package/dist/src/browser/base-page.js +29 -16
- package/dist/src/browser/find.d.ts +31 -4
- package/dist/src/browser/find.js +76 -23
- package/dist/src/browser/utils.d.ts +30 -0
- package/dist/src/browser/utils.js +7 -1
- package/dist/src/cli.js +22 -22
- package/dist/src/commanderAdapter.d.ts +14 -0
- package/dist/src/commanderAdapter.js +4 -3
- package/dist/src/doctor.d.ts +7 -0
- package/dist/src/doctor.js +9 -9
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.d.ts +1 -0
- package/dist/src/execution.js +2 -2
- package/dist/src/main.js +6 -6
- package/dist/src/types.d.ts +28 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as m,Strategy as d}from"@jackwener/opencli/registry";import{checkLogin as p}from"./auth.js";import{runLoginFlow as c}from"../login-wait.js";const s=300,g=3000;export function registerArticleLogin(e){if(!e?.site||!e?.domain||!e?.profile||typeof e.profile.checkAuth!=="function")throw Error("registerArticleLogin 需要 site、domain 和 profile.checkAuth");const o=e.loginUrl??e.profile.home;m({site:e.site,name:"login",access:"write",description:e.loginDescription??`打开${e.site}登录页并等待浏览器会话完成登录`,domain:e.domain,strategy:d.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:s,help:"等待用户完成登录的最长秒数"}],columns:["status","logged_in","user_id","username"],func:async(n,u)=>{const r=Number(u.timeout??s),{status:a,identity:i}=await c(n,{site:e.site,timeoutSeconds:r,loginUrl:o,pollIntervalMs:g,probe:async(l)=>{const t=await p(l,e.profile);return t.isAuthenticated?{loggedIn:!0,identity:{user_id:t.userId||"",username:t.username||""}}:{loggedIn:!1,message:t.error||""}},timeoutHint:()=>`用户没有在 ${r} 秒内完成${e.site}登录。登录窗口已关闭、站点会话已释放,`+`没有产生任何改动;等用户准备好后可以直接再次运行 \`ppcli ${e.site} login\`。`});return{status:a,logged_in:!0,user_id:i.user_id??"",username:i.username??""}}})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{TimeoutError as p}from"@jackwener/opencli/errors";const y=2000,_=4000,T=300,M=3000,f=Symbol("ppcli.loginDeadline");function m(){const e=Error("login deadline reached");e[f]=!0;return e}function E(e){return!!(e&&typeof e==="object"&&e[f]===!0)}export class LoginTimeoutError extends p{constructor(e,i,a){super(`${e} login`,i,a??`The user did not finish logging in within ${i}s. The login window was closed and the site session released — `+`nothing was changed, so it is safe to run \`ppcli ${e} login\` again when the user is ready.`);this.code="LOGIN_TIMEOUT";this.details={status:"timeout",logged_in:!1,site:e,timeout_seconds:i,window_closed:!0}}}export function createLoginDeadline(e,i){const a=Date.now()+Math.max(0,Number(i)||0)*1000;let s=!1,l=null,c=null;const d=new Promise((n,o)=>{c=o});d.catch(()=>{});l=setTimeout(()=>{s=!0;c(m())},Math.max(0,a-Date.now()));const t=new Set;return{page:new Proxy(e,{get(n,o){const r=Reflect.get(n,o,n);if(typeof r!=="function")return r;return(...h)=>{if(s)throw m();const g=r.apply(n,h);if(g&&typeof g.then==="function"){const w=Promise.resolve(g).then(()=>{},()=>{}).finally(()=>t.delete(w));t.add(w)}return g}}}),expired(){if(!s&&Date.now()>=a)s=!0;return s},remainingMs(){return Math.max(0,a-Date.now())},race(n){let o;try{o=Promise.resolve(n())}catch(r){o=Promise.reject(r)}o.catch(()=>{});return Promise.race([o,d])},dispose(){s=!0;if(l)clearTimeout(l);l=null},async settleInFlight(n){if(t.size===0)return!1;let o=null;await Promise.race([Promise.all([...t]),new Promise((r)=>{o=setTimeout(r,n)})]);if(o)clearTimeout(o);return!0}}}export async function closeLoginWindow(e){if(typeof e?.closeWindow!=="function")return;let i=null;try{await Promise.race([Promise.resolve().then(()=>e.closeWindow()),new Promise((a)=>{i=setTimeout(a,_)})])}catch{}finally{if(i)clearTimeout(i)}}export async function runLoginFlow(e,i){const{site:a,loginUrl:s,probe:l}=i,c=Number(i.timeoutSeconds),d=i.pollIntervalMs??y,t=createLoginDeadline(e,c);let u="";try{const n=await t.race(()=>l(t.page,"identity"));if(n?.loggedIn)return{status:"already_logged_in",identity:n.identity??{}};u=n?.message??"";await t.race(()=>t.page.goto(s));while(!t.expired()){const o=Math.min(d/1000,Math.max(0.2,t.remainingMs()/1000));await t.race(()=>t.page.wait(o));if(t.remainingMs()<T)break;const r=await t.race(()=>l(t.page,"poll"));if(r?.loggedIn)return{status:"login_complete",identity:r.identity??{}};u=r?.message??u}throw m()}catch(n){if(!E(n))throw n;throw new LoginTimeoutError(a,c,i.timeoutHint?.(u))}finally{t.dispose();await closeLoginWindow(e);if(await t.settleInFlight(M))await closeLoginWindow(e)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{AuthRequiredError as h,getErrorMessage as y}from"@jackwener/opencli/errors";import{cli as a,Strategy as u}from"@jackwener/opencli/registry";import{runLoginFlow as p}from"./login-wait.js";const l=300,w=2000;function g(e,t){const r=t&&typeof t==="object"&&!Array.isArray(t)?t:{};return{logged_in:!0,site:e,...r}}function f(e){return e instanceof h}async function d(e,t,r){const o=r==="poll"&&e.poll?e.poll:e.verify;return g(e.site,await o(t,{phase:r}))}function m(e){return["logged_in","site",...e.columns??["id","username","name"]]}function C(e){if(typeof e==="boolean")return{logged_in:e};if(e&&typeof e==="object"&&!Array.isArray(e))return{logged_in:!!e.logged_in,...e};return{logged_in:!1}}function k(e){if(e&&typeof e==="object"&&!Array.isArray(e))return e;return{touched:!0}}export function registerSiteAuthCommands(e){if(!e?.site||!e?.domain||!e?.loginUrl||typeof e.verify!=="function")throw Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");a({site:e.site,name:"whoami",access:"read",description:e.whoamiDescription??`Show the current logged-in ${e.site} account`,domain:e.domain,strategy:u.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:m(e),authStatus:{...typeof e.quickCheck==="function"?{quickCheck:async(t)=>C(await e.quickCheck(t))}:{},...e.quickCheckRequiresNavigation===!0?{quickCheckRequiresNavigation:!0}:{},...typeof e.refresh==="function"?{refresh:async(t,r)=>k(await e.refresh(t,r))}:{}},func:async(t)=>d(e,t,"identity")});a({site:e.site,name:"login",access:"write",description:e.loginDescription??`Open ${e.site} login and wait until the browser session is authenticated`,domain:e.domain,strategy:u.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:l,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...m(e)],func:async(t,r)=>{const{status:o,identity:c}=await p(t,{site:e.site,timeoutSeconds:Number(r.timeout??l),loginUrl:e.loginUrl,pollIntervalMs:w,probe:async(i,n)=>{try{return{loggedIn:!0,identity:await d(e,i,n)}}catch(s){if(!f(s))throw s;return{loggedIn:!1,message:y(s)}}},timeoutHint:(i)=>{const n="The user did not finish logging in in time. The login window was closed and the site session released — "+`nothing was changed, so it is safe to run \`ppcli ${e.site} login\` again when the user is ready.`;return i?`${n} Last auth check: ${i}`:n}});return{status:o,...c}}})}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{CliError as
|
|
1
|
+
import{CliError as p,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as U,Strategy as k}from"@jackwener/opencli/registry";import{publishArticle as L}from"../_shared/article/publish.js";import{readFile as C,stat as O}from"node:fs/promises";import{requireBaijiahaoLogin as R}from"./auth.js";function E(a){if(!a.execute)throw new p("INVALID_INPUT","此百家号写操作需要 --execute 参数才能真正发布。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function J(a){const e=typeof a.text==="string"?a.text:void 0,t=typeof a.file==="string"?a.file:void 0;if(e&&t)throw new p("INVALID_INPUT","正文和 --file 只能选一个");let r=e??"";if(t){let i;try{i=await O(t)}catch{throw new p("INVALID_INPUT",`找不到文件: ${t}`)}if(!i.isFile())throw new p("INVALID_INPUT",`--file 必须是可读文本文件: ${t}`);let l;try{l=await C(t)}catch{throw new p("INVALID_INPUT",`文件无法读取: ${t}`)}try{r=new TextDecoder("utf-8",{fatal:!0}).decode(l)}catch{throw new p("INVALID_INPUT",`文件必须是 UTF-8 编码: ${t}`)}}if(!r.trim())throw new p("INVALID_INPUT","正文不能为空");return r}function F(a,e,t,r,i={}){return[{status:"success",outcome:r,message:a,target_type:e,target:t,...i}]}export async function resolvePublishedReceipt(a,e){const t=new URL(e,"https://baijiahao.baidu.com");if(t.hostname!=="baijiahao.baidu.com")throw Error("发布回执域名不匹配");const r=t.searchParams.get("id")||t.searchParams.get("nid");if(!r||!/^\d+$/.test(r))throw Error("发布回执缺少可识别的文章 ID");if(t.pathname==="/s")return{id:r,url:`https://baijiahao.baidu.com/s?id=${r}`};if(!t.pathname.startsWith("/builder/preview"))throw Error("未识别的发布回执地址");const i=await a.evaluate(`(async () => {
|
|
2
2
|
const html = await (await fetch('/builder/rc/edit', { credentials: 'include' })).text();
|
|
3
3
|
const token = html.match(/window\\.__BJH__INIT__AUTH__\\s*=\\s*['"]([^'"]+)['"]/)?.[1];
|
|
4
4
|
if (!token) throw new Error('无法读取百家号登录凭据');
|
|
@@ -14,4 +14,4 @@ import{CliError as u,CommandExecutionError as P}from"@jackwener/opencli/errors";
|
|
|
14
14
|
const articleId = matches[0].article_id;
|
|
15
15
|
if (typeof articleId !== 'string' || !/^\\d+$/.test(articleId)) throw new Error('列表未返回精确的 article_id');
|
|
16
16
|
return articleId;
|
|
17
|
-
})()`);return{id:i,url:`https://baijiahao.baidu.com/s?id=${i}`}}export const baijiahaoProfile={home:"https://baijiahao.baidu.com",outputFormat:"html",preprocessConfig:{},image:{skip:["baijiahao.baidu.com","bdstatic.com","bcebos.com"],uploadFn:async(a,e)=>{const t=await fetch(a,{credentials:"omit"});if(!t.ok)throw Error("图片下载失败: "+a+" HTTP "+t.status);let r=await t.blob();r=await e.webpToJpeg(r);const i=new FormData;i.append("media",r,"image.jpg");i.append("type","image");i.append("app_id","1589639493090963");i.append("is_waterlog","1");i.append("save_material","1");i.append("no_compress","0");i.append("is_events","");i.append("article_type","news");const c=await(await fetch("https://baijiahao.baidu.com/pcui/picture/uploadproxy",{method:"POST",credentials:"include",body:i})).json();if(c.errmsg!=="success"||!c.ret||!c.ret.https_url)throw Error(c.errmsg||"图片上传失败");return{url:c.ret.https_url}}},publish:async(a,e)=>{const t=a.params||{},r=await fetch("https://baijiahao.baidu.com/builder/rc/edit",{credentials:"include"})
|
|
17
|
+
})()`);return{id:i,url:`https://baijiahao.baidu.com/s?id=${i}`}}export const baijiahaoProfile={home:"https://baijiahao.baidu.com",outputFormat:"html",preprocessConfig:{},image:{skip:["baijiahao.baidu.com","bdstatic.com","bcebos.com"],uploadFn:async(a,e)=>{const t=await fetch(a,{credentials:"omit"});if(!t.ok)throw Error("图片下载失败: "+a+" HTTP "+t.status);let r=await t.blob();r=await e.webpToJpeg(r);const i=new FormData;i.append("media",r,"image.jpg");i.append("type","image");i.append("app_id","1589639493090963");i.append("is_waterlog","1");i.append("save_material","1");i.append("no_compress","0");i.append("is_events","");i.append("article_type","news");const c=await(await fetch("https://baijiahao.baidu.com/pcui/picture/uploadproxy",{method:"POST",credentials:"include",body:i})).json();if(c.errmsg!=="success"||!c.ret||!c.ret.https_url)throw Error(c.errmsg||"图片上传失败");return{url:c.ret.https_url}}},publish:async(a,e)=>{const t=a.params||{};let r,i;try{r=await fetch("https://baijiahao.baidu.com/builder/rc/edit",{credentials:"include"});i=await r.text()}catch(o){return{ok:!1,stage:"auth",message:"无法读取百家号编辑页凭据(尚未提交任何内容,文章未创建):"+String(o&&o.message||o)}}const l=i.match(/window\.__BJH__INIT__AUTH__\s*=\s*['"]([^'"]+)['"]/);if(!l)return{ok:!1,stage:"auth",status:r.status,message:"登录失效,请重新登录百家号"};const c=l[1],s=a.content;if(a.draftOnly){const o=new URLSearchParams({title:a.title,content:s,feed_cat:"1",len:String(s.length),activity_list:JSON.stringify([{id:408,is_checked:0}]),source_reprinted_allow:"0",original_status:"0",original_handler_status:"1",isBeautify:"false",subtitle:"",bjhtopic_id:"",bjhtopic_info:"",type:"news"}),g=await fetch("https://baijiahao.baidu.com/pcui/article/save?callback=bjhdraft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",token:c},body:o}),n=(await g.text()).replace(/^bjhdraft\(/,"").replace(/\)$/,"");let u=null;try{u=JSON.parse(n)}catch(M){}if(!u||u.errmsg!=="success"||!u.ret||!u.ret.article_id)return{ok:!1,stage:"save",status:g.status,message:u&&u.errmsg||"保存草稿失败"};const m=String(u.ret.article_id),x="https://baijiahao.baidu.com/builder/rc/edit?type=news&article_id="+m;return{ok:!0,id:m,url:x,draft:!0}}if(!t.cover||!String(t.cover).trim())return{ok:!1,stage:"cover",message:"正式发布百家号文章必须提供封面图(--cover)"};const b=String(t.cover);let f="";try{f=new URL(b,location.href).hostname}catch(o){}const w=/(^|\.)baijiahao\.baidu\.com$|(^|\.)bdstatic\.com$|(^|\.)bcebos\.com$/.test(f);let d="";if(w)d=b;else try{const o=f===location.hostname,g=await fetch(b,{credentials:o?"include":"omit"});if(!g.ok)return{ok:!1,stage:"cover",status:g.status,message:"封面图下载失败:"+t.cover};let y=await g.blob();if(/text\/html/i.test(y.type))return{ok:!1,stage:"cover",message:"封面图下载到的是 HTML 页面而非图片(防盗链/登录墙?):"+t.cover};y=await e.webpToJpeg(y);const n=new FormData;n.append("media",y,"cover.jpg");n.append("type","image");n.append("app_id","1589639493090963");n.append("is_waterlog","1");n.append("save_material","1");n.append("no_compress","0");n.append("is_events","");n.append("article_type","news");const m=await(await fetch("https://baijiahao.baidu.com/pcui/picture/uploadproxy",{method:"POST",credentials:"include",body:n})).json();if(m.errmsg!=="success"||!m.ret||!m.ret.https_url)return{ok:!1,stage:"cover",message:"封面图上传失败:"+(m.errmsg||"未知错误")};d=m.ret.https_url}catch(o){return{ok:!1,stage:"cover",message:"封面图处理异常:"+String(o&&o.message||o)}}const _=s+'<img src="'+d+'"><br>',j=[{src:d,cropData:{x:0,y:0,width:2048,height:1365},machine_chooseimg:0,isLegal:1}],D=[{src:d}],A={type:"news",title:a.title,author:t.author?String(t.author):"",abstract:t.abstract?String(t.abstract):"",content:_,auto_mount_goods:"1",len:String(_.length),vertical_cover:d,cover_images:JSON.stringify(j),_cover_images_map:JSON.stringify(D)},S="&activity_list%5B0%5D%5Bid%5D=408&activity_list%5B0%5D%5Bis_checked%5D=0&activity_list%5B1%5D%5Bid%5D=ttv&activity_list%5B1%5D%5Bis_checked%5D=1&activity_list%5B2%5D%5Bid%5D=reward&activity_list%5B2%5D%5Bis_checked%5D=1&activity_list%5B3%5D%5Bid%5D=aigc_bjh_status&activity_list%5B3%5D%5Bis_checked%5D=0&source_reprinted_allow=0&abstract_from=2&isBeautify=false&usingImgFilter=false&cover_layout=one",T="&source=upload&cover_source=upload&subtitle=&bjhtopic_id=&bjhtopic_info=&clue=1&bjhmt=&order_id=&aigc_rebuild=&image_edit_point=%5B%7B%22img_type%22%3A%22cover%22%2C%22img_num%22%3A%7B%22template%22%3A0%2C%22font%22%3A0%2C%22filter%22%3A0%2C%22paster%22%3A0%2C%22cut%22%3A0%2C%22any%22%3A0%7D%7D%2C%7B%22img_type%22%3A%22body%22%2C%22img_num%22%3A%7B%22template%22%3A0%2C%22font%22%3A0%2C%22filter%22%3A0%2C%22paster%22%3A0%2C%22cut%22%3A0%2C%22any%22%3A0%7D%7D%5D",B=new URLSearchParams(A).toString()+S+T,v=await fetch("https://baijiahao.baidu.com/pcui/article/publish?callback=bjhpublish",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",token:c},body:B}),N=(await v.text()).replace(/^bjhpublish\(/,"").replace(/\)$/,"");let h=null;try{h=JSON.parse(N)}catch(o){}if(!h||h.errno!==0||!h.ret)return{ok:!1,stage:"publish",status:v.status,message:h&&(h.errmsg||h.errno)||"百家号发布失败"};const I=h.ret.url||"";return{ok:!0,id:I,url:I,draft:!1}},checkAuth:async(a)=>{try{const t=await(await fetch("https://baijiahao.baidu.com/builder/app/appinfo?_="+Date.now(),{credentials:"include"})).json();if(t.errmsg==="success"&&t.data&&t.data.user){const r=t.data.user;return{isAuthenticated:!0,userId:String(r.userid||""),username:r.name||"",avatar:r.avatar||""}}return{isAuthenticated:!1}}catch(e){return{isAuthenticated:!1,error:String(e&&e.message||e)}}}};U({site:"baijiahao",name:"article",access:"write",description:"发布百家号文章。默认正式发布(需 --cover 封面),加 --draft 仅存草稿(草稿不需要封面)。正文默认 Markdown,图片自动转存到百家号图床。",domain:"baijiahao.baidu.com",strategy:k.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:"cover",help:"封面图 URL(正式发布必填;会先转存到百家号图床。草稿模式可不传)"},{name:"abstract",help:"文章摘要(可空,正式发布时写入;草稿模式忽略)"},{name:"author",help:"作者署名(可空,正式发布时写入;草稿模式忽略)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布(草稿不需要封面)"},{name:"execute",type:"boolean",help:"真正执行写操作。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url","management_url"],func:async(a,e)=>{if(!a)throw new P("百家号文章发布需要浏览器会话");E(e);const t=String(e.title??"").trim();if(!t)throw new p("INVALID_INPUT","文章标题不能为空");const r=await J(e),i=Boolean(e.draft),l=typeof e.cover==="string"?e.cover.trim():"";if(!i&&!l)throw new p("INVALID_INPUT","正式发布百家号文章必须提供封面图:请传 --cover <图片URL>(或加 --draft 只存草稿)");const c={cover:l,abstract:typeof e.abstract==="string"?e.abstract:"",author:typeof e.author==="string"?e.author:""};await R(a);const s=await L(a,{title:t,body:r,format:e.html?"html":"markdown",draftOnly:i,profile:baijiahaoProfile,publishParams:c}),b=s.images.uploaded.length|0,f=s.images.failed.length|0;let w=s.draft?"已保存百家号文章草稿":"已正式发布百家号文章";const d=s.url;if(!s.draft){s.url="";try{Object.assign(s,await resolvePublishedReceipt(a,d))}catch(_){w+=`;公开链接尚未确认:${_ instanceof Error?_.message:String(_)}。请用 lists 按预览 nid 回查,禁止重复发布`}}if(b||f)w+=`(图片:${b} 张已转存${f?`,${f} 张失败`:""})`;return F(w,"article","",s.draft?"draft":"created",{created_target:"article:"+s.id,created_url:s.url,management_url:d})}});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import{AuthRequiredError as s,CommandExecutionError as o}from"@jackwener/opencli/errors";import{gotoWritePage as u}from"../_shared/article/publish.js";const c="https://baijiahao.baidu.com",h="baijiahao.baidu.com",m=10001401;function n(t){return new s(h,"百家号未登录或登录已失效,无法继续。需要用户在 PublishPort 客户端重新登录百家号后再执行本命令"+"(这不是接口故障,重试无效)。"+(t?`平台返回:${t}`:""))}export async function requireBaijiahaoLogin(t){try{await u(t,c)}catch(a){if(a&&a.landedOffSite)throw n(`被重定向到 ${a.lastHref}`);throw a}const r=await t.evaluate(`(async () => {
|
|
2
|
+
var out = { path: '' };
|
|
3
|
+
try { out.path = location.pathname; } catch (e) {}
|
|
4
|
+
try {
|
|
5
|
+
var r = await fetch('https://baijiahao.baidu.com/builder/app/appinfo?_=' + Date.now(), { credentials: 'include' });
|
|
6
|
+
out.status = r.status;
|
|
7
|
+
var j = null; try { j = await r.json(); } catch (e) {}
|
|
8
|
+
out.parsed = !!j;
|
|
9
|
+
if (j) {
|
|
10
|
+
out.errno = j.errno;
|
|
11
|
+
out.errmsg = j.errmsg == null ? '' : String(j.errmsg);
|
|
12
|
+
out.hasUser = !!(j.data && j.data.user);
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
out.fetchError = String((e && e.message) || e);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
})()`);if(!r)throw new o("百家号登录检测未返回结果(页面内脚本没有执行成功),无法判定登录状态,请稍后重试。");if(r.errmsg==="success"&&r.hasUser)return;const e=/\/login(\/|$)/.test(r.path||"");if(r.errno===m||e)throw n(r.errmsg||(e?"页面已被踢回登录页":""));const i=r.fetchError?`页面内请求异常:${r.fetchError}`:r.parsed?`接口返回 errno=${r.errno} errmsg=${r.errmsg}`:`接口返回非 JSON(HTTP ${r.status})`;throw new o(`百家号登录检测失败(${i})。这不代表未登录,请稍后重试。`)}
|
package/clis/baijiahao/lists.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as s,Strategy as
|
|
1
|
+
import{cli as s,Strategy as u}from"@jackwener/opencli/registry";import{CommandExecutionError as e}from"@jackwener/opencli/errors";import{requireBaijiahaoLogin as _}from"./auth.js";s({site:"baijiahao",name:"lists",access:"read",description:"列出当前账号在百家号已发布的文章(article_id + 标题 + 阅读/推荐量 + 是否可撤回),供发布后回查幂等、取 article_id。",domain:"baijiahao.baidu.com",strategy:u.COOKIE,browser:!0,args:[{name:"search",help:"按标题关键词过滤(可空)"},{name:"page",help:"页码,默认 1"},{name:"page_size",help:"每页条数,默认 10"}],columns:["article_id","title","read_amount","rec_amount","can_withdraw"],func:async(r,a)=>{if(!r)throw new e("百家号文章列举需要浏览器会话");await _(r);const i=typeof a.search==="string"?a.search:"",n=Number(a.page)>0?Number(a.page):1,c=Number(a.page_size)>0?Number(a.page_size):10,o=JSON.stringify({search:i,currentPage:n,pageSize:c}),t=await r.evaluate("(async () => {const I = "+o+";const er = await fetch('https://baijiahao.baidu.com/builder/rc/edit', { credentials: 'include' });const eh = await er.text();const m = eh.match(/window\\.__BJH__INIT__AUTH__\\s*=\\s*['\\\"]([^'\\\"]+)['\\\"]/);"+"if (!m) return { __error: '登录失效,请重新登录百家号' };"+"const token = m[1];const qs = new URLSearchParams({ currentPage: String(I.currentPage), pageSize: String(I.pageSize), search: I.search || '', type: '', collection: '', dynamic: '1' }).toString();const r = await fetch('https://baijiahao.baidu.com/pcui/article/lists?' + qs, { credentials: 'include', headers: { token } });let j = null; try { j = await r.json(); } catch (e) {}"+"if (!j || !j.data) return { __error: (j && j.errmsg) || ('列举失败 HTTP ' + r.status) };"+'const list = (j.data && j.data.list) || [];return { rows: list.map(a => ({ article_id: String(a.article_id == null ? "" : a.article_id), title: a.title || "", read_amount: a.read_amount == null ? "" : String(a.read_amount), rec_amount: a.rec_amount == null ? "" : String(a.rec_amount), can_withdraw: (a.withdraw_status && a.withdraw_status.can_withdraw != null) ? String(a.withdraw_status.can_withdraw) : "" })) };})()');if(t&&t.__error)throw new e("百家号文章列举失败:"+t.__error);return Array.isArray(t&&t.rows)?t.rows:[]}});
|
package/clis/baijiahao/stats.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as d,Strategy as _}from"@jackwener/opencli/registry";import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{
|
|
1
|
+
import{cli as d,Strategy as _}from"@jackwener/opencli/registry";import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{requireBaijiahaoLogin as m}from"./auth.js";d({site:"baijiahao",name:"stats",access:"read",description:"百家号内容数据(每篇文章/视频的展现/阅读/点赞/评论/收藏/分享/收益等全量运营指标)",domain:"baijiahao.baidu.com",strategy:_.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","status","title","url","published_at","views","shows","likes","comments","collects","shares","profit","extra"],func:async(r,c)=>{if(!r)throw new s("百家号内容数据查询需要浏览器会话");await m(r);const l=Number(c.limit)>0?Math.floor(Number(c.limit)):20,a=await r.evaluate("(async () => {const er = await fetch('https://baijiahao.baidu.com/builder/rc/edit', { credentials: 'include' });const eh = await er.text();const m = eh.match(/window\\.__BJH__INIT__AUTH__\\s*=\\s*['\\\"]([^'\\\"]+)['\\\"]/);"+"if (!m) return { __error: '登录失效,请重新登录百家号' };"+"const token = m[1];const LIMIT = "+JSON.stringify(l)+";const pageSize = Math.min(Math.max(LIMIT, 10), 20);const list = []; let totalPage = 1;for (let cur = 1; cur <= totalPage && list.length < LIMIT; cur++) { const qs = new URLSearchParams({ currentPage: String(cur), pageSize: String(pageSize), search: '', type: '', collection: '', startDate: '', endDate: '', clearBeforeFetch: 'false', dynamic: '1' }).toString(); const r = await fetch('https://baijiahao.baidu.com/pcui/article/lists?' + qs, { credentials: 'include', headers: { token } }); let j = null; try { j = await r.json(); } catch (e) {}"+" if (!j || j.errno !== 0 || !j.data) return { __error: (j && j.errmsg) || ('列表接口失败 HTTP ' + r.status) };"+" const rows = j.data.list || []; if (j.data.page && j.data.page.totalPage) totalPage = j.data.page.totalPage; list.push(...rows); if (rows.length < pageSize) break;}return { rows: list.slice(0, LIMIT).map(a => ({ id: String(a.article_id == null ? '' : a.article_id), raw_type: a.type == null ? '' : String(a.type), status: a.status == null ? '' : String(a.status), title: a.title || '', url: a.share_url || a.url || '', published_at: a.publish_time || '', views: a.read_amount, shows: a.rec_amount, likes: a.like_amount, comments: a.comment_amount, collects: a.collection_amount, shares: a.share_amount, profit: a.profit, feed_push_amount: a.feed_push_amount, add_view_count: a.add_view_count, rec_count: a.rec_count, is_good: a.is_good, is_published: a.is_published, has_edited_times: a.has_edited_times, open_comment: a.open_comment, duration: a.duration, video_num: a.video_num, updated_at: a.updated_at,})) };})()");if(a&&a.__error)throw new s("百家号内容数据查询失败:"+a.__error);const o=Array.isArray(a&&a.rows)?a.rows:null;if(!o)throw new s("百家号内容数据查询失败:接口未返回列表数据");if(o.length===0)throw new s("百家号后台没有查到任何内容:账号尚未发布过任何文章/视频(登录态已确认有效)。");const e=(t,n)=>typeof t==="number"&&Number.isFinite(t)?t:n;return o.map((t)=>{const n=t.raw_type==="news"?"article":t.raw_type||"",i={};if(e(t.feed_push_amount,0))i.feed_push_amount=t.feed_push_amount;const u=Number(t.add_view_count);if(Number.isFinite(u)&&u>0)i.add_view_count=u;if(e(t.rec_count,0))i.rec_count=t.rec_count;if(t.is_good===1)i.is_good=1;if(e(t.has_edited_times,0))i.has_edited_times=t.has_edited_times;if(t.open_comment===0)i.open_comment=0;if(t.duration!=null&&t.duration!=="")i.duration=t.duration;if(e(t.video_num,0))i.video_num=t.video_num;if(t.is_published!=null)i.is_published=t.is_published;if(t.updated_at)i.updated_at=t.updated_at;return{id:t.id,type:n,status:t.status,title:t.title,url:t.url,published_at:t.published_at,views:e(t.views,""),shows:e(t.shows,""),likes:e(t.likes,""),comments:e(t.comments,""),collects:e(t.collects,""),shares:e(t.shares,""),profit:e(t.profit,""),extra:JSON.stringify(i)}})}});
|
package/clis/bilibili/coin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as M,Strategy as P}from"@jackwener/opencli/registry";import{ArgumentError as h,CommandExecutionError as p}from"@jackwener/opencli/errors";import{apiGet as d,apiPost as R,requireOkPayload as b,resolveVideoIds as q,sleep as D}from"./utils.js";const N=[600,1200,2000,2500,2500],v={delays:N};function C(i){return Number.isInteger(i)&&i>=0&&i<=2}function B(i){const n=b(i,"archive coins")?.multiply;if(!C(n))throw new p("Bilibili archive coins returned a missing or invalid multiply");return n}async function _(i,n,t){try{const o=await d(i,"/x/web-interface/archive/relation",{params:{aid:n,bvid:t}}),a=b(o,"archive relation")?.coin;return C(a)?a:null}catch{return null}}async function A(i){try{const n=await d(i,"/x/web-interface/nav"),t=b(n,"nav")?.money;return typeof t==="number"&&Number.isFinite(t)?t:null}catch{return null}}const s=(i)=>i==null?"读取失败":String(i);M({site:"bilibili",name:"coin",access:"write",description:"给 B站视频投币(官方 API,需登录;消耗硬币不可撤销,需 --execute)",domain:"www.bilibili.com",strategy:P.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"count",type:"int",default:1,help:"投币数量,1 或 2(单视频累计上限 2 枚)"},{name:"with-like",type:"boolean",help:"投币同时点赞"},{name:"execute",type:"boolean",help:"Actually spend coins. Without it the command refuses to write."}],columns:["bvid","coined","total","status","confirmed_by","balance","url"],func:async(i,n)=>{if(!i)throw new p("Browser session required for bilibili coin");const t=Number(n.count??1);if(t!==1&&t!==2)throw new h("bilibili coin --count must be 1 or 2");if(!n.execute)throw new h("Refusing to spend coins: pass --execute to actually pay coins");const{bvid:o,aid:a}=await q(i,n.bvid),x=`https://www.bilibili.com/video/${o}`,I=await d(i,"/x/web-interface/archive/coins",{params:{aid:a,bvid:o}}),l=Math.max(B(I),await _(i,a,o)??0);if(l>=2)return[{bvid:o,coined:0,total:l,status:"already-maxed",confirmed_by:"",balance:"",url:x}];if(l+t>2)throw new h(`已投 ${l} 枚,再投 ${t} 枚会超过单视频 2 枚上限;改用 --count ${2-l}`);const f=await A(i),S=await R(i,"/x/web-interface/coin/add",{params:{aid:a,bvid:o,multiply:t,like:n["with-like"]?1:0}});b(S,"coin add");const u=l+t,e={multiply:null,relation:null,lastError:""};let r="";for(let c=0;c<=v.delays.length&&!r;c++){if(c>0)await D(v.delays[c-1]);try{e.multiply=B(await d(i,"/x/web-interface/archive/coins",{params:{aid:a,bvid:o}}))}catch(w){e.lastError=w instanceof Error?w.message:String(w)}if(e.multiply!=null&&e.multiply>=u){r="archive/coins";break}const E=await _(i,a,o);if(E!=null)e.relation=E;if(e.relation!=null&&e.relation>=u)r="archive/relation"}const m=await A(i),y=f!=null&&m!=null?Math.round((f-m)*10)/10:null;if(!r&&y!=null&&y>=t)r="balance";if(!r){const c=`archive/coins multiply=${s(e.multiply)},archive/relation coin=${s(e.relation)},`+`硬币余额 ${s(f)} → ${s(m)}(差 ${s(y)}),期望该视频累计 ≥ ${u}`;throw new p(`Bilibili 已受理 ${o} 的 ${t} 枚投币请求,但回查未确认;硬币可能已扣,禁止重投。观测读数:${c}`+(e.lastError?`;最近一次回查报错:${e.lastError}`:""),"硬币可能已扣除,禁止直接重投。本次请求只提交了一次;稍后用同一账号核对硬币余额与该视频的投币状态再决定。")}const k=Math.max(u,e.multiply??0,e.relation??0);return[{bvid:o,coined:t,total:k,status:"coined",confirmed_by:r,balance:m??"",url:x}]}});export const __test__={backoff:v};
|
package/clis/bilibili/comment.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as x,Strategy as E}from"@jackwener/opencli/registry";import{ArgumentError as b,CommandExecutionError as f,EmptyResultError as S}from"@jackwener/opencli/errors";import{apiGet as g,apiPost as B,describeReplyVisibility as I,requireOkPayload as w,resolveBvid as N,resolveUid as P,sleep as C}from"./utils.js";import{assertLiteralContent as D}from"../_shared/content-guard.js";function R(i,t){const e=Number(i);if(!Number.isInteger(e)||e<=0)throw new b(`bilibili comment ${t} must be a positive integer`);return e}const A=20,_={delayMs:1200};async function O(i,t,e,a){const r=(s)=>String(s?.rpid_str??s?.rpid??"")===String(e);if(a==null){const s=await g(i,"/x/v2/reply/main",{params:{oid:t,type:1,mode:2,ps:A},signed:!0}),d=w(s,"reply main");return(Array.isArray(d?.replies)?d.replies:[]).find(r)??null}const l=async(s)=>w(await g(i,"/x/v2/reply/reply",{params:{oid:t,type:1,root:a,pn:s,ps:A}}),"reply thread"),n=await l(1),o=(Array.isArray(n?.replies)?n.replies:[]).find(r);if(o)return o;const c=Number(n?.page?.count),v=Number(n?.page?.size)||A,u=Number.isFinite(c)?Math.ceil(c/v):1;if(u<=1)return null;const h=await l(u);return(Array.isArray(h?.replies)?h.replies:[]).find(r)??null}async function V(i,t,e,a){let r="回读列表里没找到这条评论";for(let l=0;l<2;l++){if(l>0)await C(_.delayMs);try{const n=await O(i,t,e,a);if(n){const o=I(n);return{...o,visibility_note:o.visibility==="unknown"?`平台返回了未识别的 state=${o.state}`:""}}}catch(n){r=`回读失败:${n instanceof Error?n.message:String(n)}`}}return{state:null,attr:null,visibility:"unknown",visibility_note:`评论已发出,可见性未知(${r})`}}x({site:"bilibili",name:"comment",access:"write",description:"在 B站视频下发表评论或回复(官方 API,需登录;消息里的 @用户 会被解析为真实提及;需 --execute;回执带 visibility 标明是否公开可见)",domain:"www.bilibili.com",strategy:E.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"message",fromFile:!0,required:!0,positional:!0,help:"Comment text. Any @username in it is resolved to a real mention"},{name:"parent",type:"int",help:"top-level/root rpid to reply under (omit for a top-level comment)"},{name:"execute",type:"boolean",help:"Actually post the comment. Without it the command refuses to write."}],columns:["rpid","bvid","oid","message","url","state","attr","visibility","visibility_note"],func:async(i,t)=>{if(!i)throw new f("Browser session required for bilibili comment");const e=String(t.message??"").trim();D(e,{label:"评论内容"});if(!e)throw new b("bilibili comment message cannot be empty");if(!t.execute)throw new b("Refusing to post: pass --execute to actually publish this comment");const a=t.parent!=null?R(t.parent,"parent"):null;let r;try{r=await N(t.bvid)}catch(p){throw new b(`Cannot resolve Bilibili BV ID from input: ${String(t.bvid??"")}`,p instanceof Error?p.message:String(p))}const l=await g(i,"/x/web-interface/view",{params:{bvid:r}}),o=w(l,"view")?.aid;if(!o)throw new f(`Cannot resolve aid for bvid: ${r}`);const c={};for(const p of e.matchAll(/@([^\s@]+)/g)){const y=p[1];if(y in c)continue;try{const m=Number(await P(i,y));if(!Number.isInteger(m)||m<=0)throw new f(`Bilibili user search returned malformed mid for @${y}`);c[y]=m}catch(m){if(!(m instanceof S))throw m}}const v={oid:o,type:1,message:e,plat:1,...a!=null?{root:a,parent:a}:{},...Object.keys(c).length>0?{at_name_to_mid:JSON.stringify(c)}:{}},u=await B(i,"/x/v2/reply/add",{params:v}),s=w(u,"reply add")?.rpid;if(!s)throw new f("Bilibili reply add API did not return rpid for the posted comment");const d=await V(i,o,s,a);return[{rpid:String(s),bvid:r,oid:String(o),message:e,url:`https://www.bilibili.com/video/${r}#reply${s}`,...d}]}});export const __test__={backoff:_};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as g,Strategy as
|
|
1
|
+
import{cli as g,Strategy as y}from"@jackwener/opencli/registry";import{ArgumentError as s,AuthRequiredError as A,CommandExecutionError as r,EmptyResultError as v}from"@jackwener/opencli/errors";import{apiGet as c,describeReplyVisibility as B,formatBeijingTime as I,resolveBvid as E}from"./utils.js";const b=50;function S(i,e){return i===-101||i===-403||/登录|账号|权限|forbidden|permission|login/i.test(String(e??""))}function O(i){const e=i==null?20:i,t=Number(e);if(!Number.isInteger(t)||t<=0||t>b)throw new s(`bilibili comments limit must be an integer between 1 and ${b}`);return t}const f={hot:3,time:2};function j(i){const e=i==null?"hot":String(i).trim().toLowerCase(),t=f[e];if(!t)throw new s(`bilibili comments sort must be one of: ${Object.keys(f).join(", ")}`);return t}function P(i){if(i==null)return null;const e=Number(i);if(!Number.isInteger(e)||e<=0)throw new s("bilibili comments parent must be a positive integer rpid");return e}function w(i,e){if(!i||typeof i!=="object"||Array.isArray(i)||!Object.hasOwn(i,"code"))throw new r(`Bilibili ${e} API returned a malformed payload`);if(i.code!==0){const t=i.message??"unknown error";if(S(i.code,t))throw new A("bilibili.com",`Bilibili ${e} API requires login or permission: ${t} (${i.code})`);throw new r(`Bilibili ${e} API failed: ${t} (${i.code})`)}return i.data}function N(i,e){if(!i||typeof i!=="object"||Array.isArray(i))throw new r(`Bilibili ${e} API returned malformed data`);if(!Object.hasOwn(i,"replies"))throw new r(`Bilibili ${e} API did not return replies`);if(i.replies===null)return[];if(!Array.isArray(i.replies))throw new r(`Bilibili ${e} API returned malformed replies`);return i.replies}function R(i,e){if(!i||typeof i!=="object"||Array.isArray(i))throw new r(`Bilibili comments reply ${e+1} was malformed`);const t=String(i.rpid??"").trim();if(!t)throw new r(`Bilibili comments reply ${e+1} was missing rpid`);const o=Number(i.ctime);if(!Number.isFinite(o))throw new r(`Bilibili comments reply ${e+1} was missing ctime`);return{rank:e+1,rpid:t,author:String(i.member?.uname??""),text:String(i.content?.message??"").replace(/\n/g," ").trim(),likes:i.like??0,replies:i.rcount??0,time:I(o),...B(i)}}g({site:"bilibili",name:"comments",access:"read",description:"获取 B站视频评论(官方 API;用 --parent <rpid> 读取某条评论下的「楼中楼」回复;--sort time 按最新排序,发评论后回查用;time 为北京时间;visibility=self-only 表示被平台压成仅自己可见)",domain:"www.bilibili.com",strategy:y.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID (e.g. BV1WtAGzYEBm)"},{name:"parent",type:"int",help:"rpid of a comment — fetch the replies under it instead of top-level comments"},{name:"sort",default:"hot",help:"Top-level order: hot (default) or time (newest first; use to verify a just-posted comment)"},{name:"limit",type:"int",default:20,help:"Number of comments (max 50)"}],columns:["rank","rpid","author","text","likes","replies","time","state","attr","visibility"],func:async(i,e)=>{if(!i)throw new r("Browser session required for bilibili comments");let t;try{t=await E(e.bvid)}catch(l){throw new s(`Cannot resolve Bilibili BV ID from input: ${String(e.bvid??"")}`,l instanceof Error?l.message:String(l))}const o=O(e.limit),n=P(e.parent),p=j(e.sort),d=await c(i,"/x/web-interface/view",{params:{bvid:t}}),m=w(d,"view")?.aid;if(!m)throw new r(`Cannot resolve aid for bvid: ${t}`);const h=n!=null?await c(i,"/x/v2/reply/reply",{params:{oid:m,type:1,root:n,pn:1,ps:o}}):await c(i,"/x/v2/reply/main",{params:{oid:m,type:1,mode:p,ps:o},signed:!0}),a=n!=null?"reply thread":"reply main",u=N(w(h,a),a);if(u.length===0)throw new v(n!=null?`bilibili comment replies: ${n}`:`bilibili comments: ${t}`,n!=null?`该评论(rpid ${n})下当前没有可读到的回复。`+"用于去重时:读不到回复即代表本账号没在该楼回复过,可以安全发布。":`该视频(${t})当前没有可见评论(评论区为空或已关闭)。`+"用于去重时:这就是「本账号没在此视频评论过」的确证,可以安全发布。");return u.slice(0,o).map(R)}});
|
package/clis/bilibili/dm-list.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as w,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as S,CommandExecutionError as y}from"@jackwener/opencli/errors";import{apiGet as k,describeDmMessage as I,fetchJson as A,getSelfUid as v,requireOkPayload as E}from"./utils.js";const m=200,M=12;function N(s){const i=Number(s??20);if(!Number.isInteger(i)||i<1||i>m)throw new S(`bilibili dm-list --limit must be an integer between 1 and ${m}`);return i}w({site:"bilibili",name:"dm-list",access:"read",description:"B站私信会话列表(talker_id 即对方 UID,供 dm-read / dm-send 用;last_direction 标明最后一条是谁发的;--limit 超过 20 自动翻页)",domain:"www.bilibili.com",strategy:h.COOKIE,args:[{name:"limit",type:"int",default:20,help:"返回会话数上限(1-200;接口每页 20 条,超过自动翻页)"}],columns:["talker_id","name","last_content","last_direction","last_sender_uid","unread","last_time"],func:async(s,i)=>{if(!s)throw new y("Browser session required for bilibili dm-list");const _=N(i.limit),p=await v(s),c=[],u=new Set;let o=null;for(let n=0;n<M&&c.length<_;n++){const a=new URLSearchParams({session_type:"1",group_fold:"1",unfollow_fold:"0",sort_rule:"2",build:"0",mobi_app:"web",...o!=null?{end_ts:String(o)}:{}}).toString(),l=await A(s,`https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?${a}`),t=E(l,"dm get_sessions"),e=Array.isArray(t?.session_list)?t.session_list:[];let r=0;for(const g of e){const b=String(g?.talker_id??"");if(u.has(b))continue;u.add(b);c.push(g);r++}const d=e.length?e[e.length-1]?.session_ts:null;if(!t?.has_more||r===0||d==null||String(d)===String(o))break;o=d}const f=[];for(const n of c.slice(0,_)){const a=String(n?.talker_id??"");let l="";try{l=(await k(s,"/x/web-interface/card",{params:{mid:a}}))?.data?.card?.name??""}catch{}const t=n?.last_msg??{},e=Number(t?.timestamp??0),r=t?.sender_uid!=null?String(t.sender_uid):"";f.push({talker_id:a,name:l,last_content:I(t).content.replace(/\s+/g," ").slice(0,80),last_direction:r?r===p?"sent":"received":"",last_sender_uid:r,unread:Number(n?.unread_count??0),last_time:e?new Date(e*1000).toISOString():""})}return f}});export const __test__={MAX_LIMIT:m};
|
package/clis/bilibili/dm-read.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as u,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as _,CommandExecutionError as w,EmptyResultError as p}from"@jackwener/opencli/errors";import{describeDmMessage as f,fetchJson as y,getSelfUid as h,requireOkPayload as S,resolveUid as I,wbiSign as v}from"./utils.js";const l=50;u({site:"bilibili",name:"dm-read",access:"read",description:"读取与某 B站用户的私信记录(默认近 30 条,--limit 最多 50;target 为 UID / 用户名;recalled=true 为已撤回)",domain:"www.bilibili.com",strategy:b.COOKIE,args:[{name:"target",required:!0,positional:!0,help:"对方 UID / 用户名(dm-list 的 talker_id)"},{name:"limit",type:"int",default:30,help:"读取最近多少条(1-50,默认 30)"}],columns:["time","direction","sender_uid","content","msg_type","recalled","image_url"],func:async(e,s)=>{if(!e)throw new w("Browser session required for bilibili dm-read");const t=Number(s.limit??30);if(!Number.isInteger(t)||t<1||t>l)throw new _(`bilibili dm-read --limit must be an integer between 1 and ${l}`);const n=await I(e,String(s.target??"").trim()),m=await h(e),d=await v(e,{talker_id:n,session_type:1,size:t,begin_seqno:0}),c=new URLSearchParams(d).toString().replace(/\+/g,"%20"),g=await y(e,`https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?${c}`),a=S(g,"dm fetch_session_msgs")?.messages??[];if(!a.length)throw new p(`bilibili dm-read: ${n}`,"与该用户还没有私信往来。");return a.slice().reverse().map((i)=>{const o=Number(i?.timestamp??0),r=f(i);return{time:o?new Date(o*1000).toISOString():"",direction:String(i?.sender_uid??"")===m?"sent":"received",sender_uid:String(i?.sender_uid??""),content:r.content,msg_type:Number(i?.msg_type??0),recalled:r.recalled,image_url:r.image_url}})}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as D,Strategy as F}from"@jackwener/opencli/registry";import{ArgumentError as b,CommandExecutionError as u}from"@jackwener/opencli/errors";import{apiGet as p,apiPost as q,getSelfUid as C,requireOkPayload as w,resolveVideoIds as O,sleep as P}from"./utils.js";const V=[600,1200,2000,2500],y={delays:V};async function h(t,s){const i=await C(t),a=await p(t,"/x/v3/fav/folder/created/list-all",{params:{up_mid:i,type:2,rid:s},signed:!0}),o=w(a,"fav folder list")?.list??[];if(!Array.isArray(o)||o.length===0)throw new u("Bilibili fav folder list returned no folders for this account");return o}function _(t,s){const i=String(s??"").trim();if(!i)return t[0];const a=t.find((n)=>String(n.id)===i);if(a)return a;const r=t.filter((n)=>String(n.title??"")===i);if(r.length===1)return r[0];if(r.length>1)throw new b(`收藏夹标题「${i}」有 ${r.length} 个同名,请改用 ID:${r.map((n)=>n.id).join(", ")}`);const o=t.map((n)=>`${n.id}=${n.title}`).join(", ");throw new b(`找不到收藏夹「${i}」;现有收藏夹:${o}`)}function g(t){if(!t||t.fav_state!==0&&t.fav_state!==1)throw new u("Bilibili fav folder list is missing the target folder or its fav_state");return t.fav_state===1}async function I(t,s,i){try{const a=await p(t,"/x/v3/fav/resource/ids",{params:{media_id:s,platform:"web"}}),r=w(a,"fav resource ids");if(!Array.isArray(r))return null;return r.some((o)=>Number(o?.type)===2&&String(o?.id)===String(i))}catch{return null}}D({site:"bilibili",name:"favorite-add",access:"write",description:"把 B站视频加入 / 移出收藏夹(官方 API,需登录;--folder 缺省用默认收藏夹)",domain:"www.bilibili.com",strategy:F.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"folder",help:"目标收藏夹 ID 或标题(缺省用默认收藏夹)"},{name:"remove",type:"boolean",help:"从收藏夹移出(默认是加入)"}],columns:["bvid","folder_id","folder","status","url"],func:async(t,s)=>{if(!t)throw new u("Browser session required for bilibili favorite-add");const i=Boolean(s.remove),{bvid:a,aid:r}=await O(t,s.bvid),o=`https://www.bilibili.com/video/${a}`,n=await h(t,r),d=_(n,s.folder),E=g(d),A=await I(t,d.id,r)??E,S={bvid:a,folder_id:String(d.id),folder:String(d.title??""),url:o};if(A===!i)return[{...S,status:i?"not-in-folder":"already-favorited"}];const B=await q(t,"/x/v3/fav/resource/deal",{params:{rid:r,type:2,add_media_ids:i?"":String(d.id),del_media_ids:i?String(d.id):""}});w(B,"fav resource deal");const v=!i,e={inIds:null,favState:null,lastError:""};let m=!1;for(let l=0;l<=y.delays.length&&!m;l++){if(l>0)await P(y.delays[l-1]);const f=await I(t,d.id,r);if(f!=null)e.inIds=f;try{const c=await h(t,r);e.favState=g(c.find((x)=>String(x.id)===String(d.id)))?1:0}catch(c){e.lastError=c instanceof Error?c.message:String(c)}m=e.inIds!=null?e.inIds===v||v&&e.favState===1:e.favState!=null&&e.favState===1===v}if(!m){const l=(f)=>f==null?"读取失败":String(f);throw new u(`Bilibili 已受理 ${a} 在收藏夹 ${d.id} 的${i?"移除":"添加"}请求,但回查未确认:`+`fav_state=${l(e.favState)} after ${i?"remove":"add"},resource/ids 在列=${l(e.inIds)}`+(e.lastError?`;最近一次回查报错:${e.lastError}`:""),"请先在同一账号的目标收藏夹核对该视频,禁止直接重跑写命令;本次请求只提交了一次。")}return[{...S,status:i?"removed":"favorited"}]}});export const __test__={pickFolder:_,backoff:y};
|
package/clis/bilibili/me.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as r,Strategy as l}from"@jackwener/opencli/registry";import{AuthRequiredError as n,CommandExecutionError as m}from"@jackwener/opencli/errors";import{apiGet as t,requireOkPayload as f}from"./utils.js";function e(o,i){if(o==null||!Number.isFinite(Number(o)))throw new m(`Bilibili myinfo API did not return ${i}`);return Number(o)}r({site:"bilibili",name:"me",access:"read",description:"My Bilibili profile info",domain:"www.bilibili.com",strategy:l.COOKIE,args:[],columns:["name","uid","level","coins","followers","following"],func:async(o)=>{const i=f(await t(o,"/x/space/myinfo"),"myinfo");if(!i?.mid)throw new n("bilibili.com");return{name:i.name??"",uid:i.mid,level:i.level??0,coins:i.coins??0,followers:e(i.follower,"follower"),following:e(i.following,"following")}}});
|
package/clis/bilibili/search.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as h,Strategy as m}from"@jackwener/opencli/registry";import{ArgumentError as y,EmptyResultError as g}from"@jackwener/opencli/errors";import{apiGet as f,readSearchResults as w,stripHtml as l}from"./utils.js";h({site:"bilibili",name:"search",access:"read",description:"Search Bilibili videos or users(uid = 视频作者 / 用户的 UID;--type user 时 sign 为个人简介、score 为粉丝数)",domain:"www.bilibili.com",strategy:m.COOKIE,args:[{name:"query",required:!0,positional:!0,help:"Search keyword"},{name:"type",default:"video",help:"video or user"},{name:"page",type:"int",default:1,help:"Result page"},{name:"limit",type:"int",default:20,help:"Number of results"}],columns:["rank","title","author","uid","score","bvid","url","sign"],func:async(n,d)=>{const{query:i,type:r="video",page:c=1,limit:p=20}=d;if(r!=="video"&&r!=="user")throw new y("bilibili search --type must be video or user");const s=r==="user"?"bili_user":"video",b=await f(n,"/x/web-interface/wbi/search/type",{params:{search_type:s,keyword:i,page:c},signed:!0}),t=w(b,`${r} search for ${i}`);if(t.length===0)throw new g(`bilibili search: ${i}`,`没有搜到匹配的${r==="user"?"用户":"视频"}(接口正常返回 0 条,不是故障)。`);const a=(e)=>e.mid?String(e.mid):"";return t.slice(0,Number(p)).map((e,o)=>{if(s==="bili_user"){const u=l(e.uname??"");return{rank:o+1,title:u,author:u,uid:a(e),score:e.fans??0,bvid:"",url:e.mid?`https://space.bilibili.com/${e.mid}`:"",sign:(e.usign??"").trim()}}return{rank:o+1,title:l(e.title??""),author:e.author??"",uid:a(e),score:e.play??0,bvid:e.bvid??"",url:e.bvid?`https://www.bilibili.com/video/${e.bvid}`:"",sign:""}})}});
|
package/clis/bilibili/stats.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as v,Strategy as k}from"@jackwener/opencli/registry";import{CommandExecutionError as w,EmptyResultError as N}from"@jackwener/opencli/errors";import{fetchJson as y,requireOkPayload as _}from"./utils.js";const f=10;async function S(i){for(let s=0;s<6;s++){await i.goto("https://member.bilibili.com/platform/upload-manager/article");await i.wait({time:1});let r="";try{r=String(await i.evaluate("location.href")||"")}catch{r=""}if(/^https?:\/\/member\.bilibili\.com\//.test(r))return}throw new w("无法进入 B站创作中心(可能未登录或被风控)。请先在客户端登录哔哩哔哩。")}function g(i){if(!i||!Number.isFinite(Number(i)))return"";const s=new Date((Number(i)+28800)*1000),r=(n)=>String(n).padStart(2,"0");return`${s.getUTCFullYear()}-${r(s.getUTCMonth()+1)}-${r(s.getUTCDate())} ${r(s.getUTCHours())}:${r(s.getUTCMinutes())}`}function a(i){if(i==null||i===""||i==="-")return"";const s=Number(String(i).replace(/,/g,""));return Number.isFinite(s)?s:""}async function x(i,s){const r=[];for(let n=1;r.length<s;n++){const m=`https://member.bilibili.com/x/web/archives?status=is_pubing%2Cpubed%2Cnot_pubed&pn=${n}&ps=${f}&coop=1&interactive=1`,d=await y(i,m),u=_(d,"archives"),o=Array.isArray(u?.arc_audits)?u.arc_audits:[];if(o.length===0)break;for(const l of o){const t=l?.Archive??{},e=l?.stat??{},p=Number(t.ptime)||Number(t.ctime)||0,c={};if(e.dislike)c.dislike=e.dislike;if(e.now_rank)c.now_rank=e.now_rank;if(e.his_rank)c.his_rank=e.his_rank;if(e.vt)c.vt=e.vt;if(l.typename)c.category=l.typename;if(l.parent_tname)c.parent_category=l.parent_tname;if(t.aid!=null)c.aid=t.aid;if(t.duration)c.duration=t.duration;r.push({id:t.bvid||(t.aid!=null?`av${t.aid}`:""),type:"video",title:t.title??"",url:t.bvid?`https://www.bilibili.com/video/${t.bvid}`:"",published_at:g(p),scheduled_at:g(Number(t.dtime)||0),views:a(e.view),likes:a(e.like),comments:a(e.reply),collects:a(e.favorite),shares:a(e.share),danmaku:a(e.danmaku),coins:a(e.coin),status:t.state_desc??"",extra:JSON.stringify(c),_ts:p})}const b=Number(u?.page?.count)||0;if(n*f>=b)break}return r}async function C(i,s){const r="%7B%22platform%22:%22web%22,%22device%22:%22pc%22,%22spmid%22:%22333.1374%22%7D",n=[];for(let m=1;n.length<s;m++){const d=`https://api.bilibili.com/x/polymer/web-dynamic/v1/opus/creationlist?ps=${f}&pn=${m}&classification_type=0&creation_type=0&web_location=333.1374&x-bili-device-req-json=${r}`,u=await y(i,d),o=_(u,"opus creationlist"),b=Array.isArray(o?.items)?o.items:[];if(b.length===0)break;for(const t of b){const e=t?.stat??{},p=String(t?.dyn_type)==="64",c=Math.floor((Date.parse(`${String(t?.pub_time??"").replace(" ","T")}:00+08:00`)||0)/1000),h={dyn_id:t?.dyn_id??"",dyn_type:t?.dyn_type??""};if(t?.filter_group?.reason)h.reason=t.filter_group.reason;n.push({id:p?`cv${t.rid}`:String(t?.dyn_id??""),type:p?"article":"opus",title:t?.title??"",url:p?`https://www.bilibili.com/read/cv${t.rid}`:t?.dyn_id?`https://www.bilibili.com/opus/${t.dyn_id}`:"",published_at:t?.pub_time??"",scheduled_at:"",views:a(e.view),likes:a(e.like),comments:a(e.reply),collects:a(e.favorite),shares:"",danmaku:"",coins:a(e.coin),status:"",extra:JSON.stringify(h),_ts:c})}if(!o?.has_more)break;const l=Number(o?.total)||0;if(l&&m*f>=l)break}return n}v({site:"bilibili",name:"stats",access:"read",description:"B站稿件数据(每个视频/图文/专栏稿件的播放、点赞、投币、收藏、弹幕、分享、评论等全量运营指标)",domain:"member.bilibili.com",strategy:k.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20,help:"返回条数(视频与图文合并后按发布时间倒序截取)"}],columns:["id","type","title","url","published_at","scheduled_at","views","likes","comments","collects","shares","danmaku","coins","status","extra"],func:async(i,s)=>{if(!i)throw new w("B站稿件数据需要浏览器会话");const r=s?.limit||20;await S(i);const[n,m]=[await x(i,r),await C(i,r)],d=[...n,...m].sort((u,o)=>(o._ts||0)-(u._ts||0)).slice(0,r);if(d.length===0)throw new N("bilibili stats","未取到任何稿件数据。请确认已登录 B站且该账号发布过视频或图文稿件。");return d.map(({_ts:u,...o})=>o)}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as b,Strategy as h}from"@jackwener/opencli/registry";import{CommandExecutionError as w,EmptyResultError as f}from"@jackwener/opencli/errors";import{apiGet as y,formatBeijingTime as g,requireOkPayload as v,resolveUser as k}from"./utils.js";b({site:"bilibili",name:"user-videos",access:"read",description:"查看指定用户的投稿视频(uid / author 为实际查到的那位用户;按名字查时务必核对,避免撞名)",domain:"www.bilibili.com",strategy:h.COOKIE,args:[{name:"uid",required:!0,positional:!0,help:"User UID or username"},{name:"limit",type:"int",default:20,help:"Number of results"},{name:"order",default:"pubdate",help:"Sort: pubdate, click, stow"},{name:"page",type:"int",default:1,help:"Page number"}],columns:["rank","title","plays","likes","date","url","uid","author","name_match"],func:async(s,o)=>{const{uid:u,limit:l=20,order:d="pubdate",page:n=1}=o,t=String(u).trim(),a=await k(s,t),r=a.mid,m=await y(s,"/x/space/wbi/arc/search",{params:{mid:r,pn:n,ps:Math.min(Number(l),50),order:d},signed:!0}),i=v(m,"space arc search")?.list?.vlist;if(i!=null&&!Array.isArray(i))throw new w("Bilibili space arc search returned a malformed vlist");if(!i||i.length===0)throw new f(`bilibili user-videos: ${t}`,`UID ${r}${a.name?`(${a.name})`:""} 在第 ${n} 页没有公开投稿视频。`+(/^\d+$/.test(t)?"":`「${t}」是按名字解析到这个 UID 的,确认是不是要找的人。`));const c=/^\d+$/.test(t)?"":a.exact?"exact":"first-result";return i.slice(0,Number(l)).map((e,p)=>({rank:p+1,title:e.title??"",plays:e.play??0,likes:e.like??0,date:g(e.created).slice(0,10),url:e.bvid?`https://www.bilibili.com/video/${e.bvid}`:"",uid:String(e.mid??r),author:e.author??a.name,name_match:c}))}});
|
package/clis/bilibili/utils.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import g from"node:https";import{ArgumentError as
|
|
1
|
+
import g from"node:https";import{ArgumentError as m,AuthRequiredError as d,CommandExecutionError as u,EmptyResultError as b}from"@jackwener/opencli/errors";export function resolveBvid(e){const t=String(e).trim();if(/^BV[A-Za-z0-9]+$/i.test(t))return Promise.resolve(t);try{const i=new URL(t);if(/(\.|^)bilibili\.com$/i.test(i.hostname)){const s=i.pathname.match(/\/(?:video|bangumi\/play)\/(BV[A-Za-z0-9]+)/i);if(s)return Promise.resolve(s[1])}}catch{}const r=t.replace(/^https?:\/\//,"").replace(/^(www\.)?b23\.tv\//,"");if(!/^[A-Za-z0-9]+$/.test(r))return Promise.reject(Error(`Cannot resolve BV ID from invalid b23.tv short code: ${t}`));const n="https://b23.tv/"+r;return new Promise((i,s)=>{const o=g.get(n,(a)=>{const c=a.headers.location;if(c){const f=c.match(/\/video\/(BV[A-Za-z0-9]+)/);if(f){a.resume();i(f[1]);return}}a.resume();s(Error(`Cannot resolve BV ID from short URL: ${t}`))});o.on("error",s);o.setTimeout(4000,()=>{o.destroy();s(Error(`Timeout resolving short URL: ${t}`))})})}export function parsePageArg(e){if(e==null||e==="")return null;if(typeof e==="number"){if(Number.isSafeInteger(e)&&e>=1)return e;throw new m(`--page must be a positive decimal integer, got: ${e}`)}if(typeof e!=="string"||!/^[1-9]\d*$/.test(e))throw new m(`--page must be a positive decimal integer, got: ${String(e)}`);const t=Number(e);if(!Number.isSafeInteger(t))throw new m(`--page is too large: ${e}`);return t}function p(e,t){if(typeof e==="number"&&Number.isSafeInteger(e)&&e>=1)return e;if(typeof e==="string"&&/^[1-9]\d*$/.test(e)){const r=Number(e);if(Number.isSafeInteger(r))return r}throw new u(`Bilibili view API returned a malformed ${t}`)}export function selectVideoPart(e,t){const r=Array.isArray(e?.pages)?e.pages:null;if(!r||r.length===0)throw new u("Bilibili view API did not return pages[] for --page selection");const n=[];for(const s of r){if(!s||typeof s!=="object"||Array.isArray(s))throw new u("Bilibili view API returned a malformed pages[] entry");if(p(s.page,"page number")===t)n.push(s)}if(n.length>1)throw new u(`Bilibili view API returned duplicate page entries for p=${t}`);const i=n[0];if(!i){const s=r.length||e?.videos||1;throw new u(`分P 序号超出范围:p=${t}(该视频共 ${s} 集)`)}p(i.cid,`cid for p=${t}`);return i}const h=[46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52];export function stripHtml(e){return e.replace(/<[^>]+>/g,"").replace(/&[a-z]+;/gi," ").trim()}export function payloadData(e){return e?.data??e}async function w(e){return e.evaluate(`
|
|
2
2
|
async () => {
|
|
3
3
|
const res = await fetch('https://api.bilibili.com/x/web-interface/nav', { credentials: 'include' });
|
|
4
4
|
return await res.json();
|
|
5
5
|
}
|
|
6
|
-
`)}async function y(
|
|
6
|
+
`)}async function y(e){const r=(await w(e))?.data?.wbi_img??{},n=r.img_url??"",i=r.sub_url??"",s=n.split("/").pop()?.split(".")[0]??"",o=i.split("/").pop()?.split(".")[0]??"";return{imgKey:s,subKey:o}}function S(e,t){const r=e+t;return h.map((n)=>r[n]||"").join("").slice(0,32)}async function x(e){const{createHash:t}=await import("node:crypto");return t("md5").update(e).digest("hex")}export async function wbiSign(e,t){const{imgKey:r,subKey:n}=await y(e),i=S(r,n),s=Math.floor(Date.now()/1000),o={},a={...t,wts:String(s)};for(const l of Object.keys(a).sort())o[l]=String(a[l]).replace(/[!'()*]/g,"");const c=new URLSearchParams(o).toString().replace(/\+/g,"%20"),f=await x(c+i);o.w_rid=f;return o}export async function apiGet(e,t,r={}){const n="https://api.bilibili.com";let i=r.params??{};if(r.signed)i=await wbiSign(e,i);const s=new URLSearchParams(Object.fromEntries(Object.entries(i).map(([a,c])=>[a,String(c)]))).toString().replace(/\+/g,"%20"),o=`${n}${t}?${s}`;return fetchJson(e,o)}export async function fetchJson(e,t){const r=JSON.stringify(t);return e.evaluate(`
|
|
7
7
|
async () => {
|
|
8
8
|
const res = await fetch(${r}, { credentials: "include" });
|
|
9
9
|
return await res.json();
|
|
10
10
|
}
|
|
11
|
-
`)}export function isAuthLikeBilibiliError(t
|
|
11
|
+
`)}export function isAuthLikeBilibiliError(e,t){return e===-101||e===-111||e===-403||/csrf|登录|账号|权限|forbidden|permission|login/i.test(String(t??""))}export function requireOkPayload(e,t){if(!e||typeof e!=="object"||Array.isArray(e)||!Object.hasOwn(e,"code"))throw new u(`Bilibili ${t} API returned a malformed payload`);if(e.code!==0){const r=e.message??"unknown error";if(isAuthLikeBilibiliError(e.code,r))throw new d("bilibili.com",`Bilibili ${t} API requires login or permission: ${r} (${e.code})`);throw new u(`Bilibili ${t} API failed: ${r} (${e.code})`)}return e.data}export async function apiPost(e,t,r={}){const n=r.params??{},i=Object.fromEntries(Object.entries(n).map(([a,c])=>[a,String(c)])),s=JSON.stringify(i),o=JSON.stringify(`https://api.bilibili.com${t}`);return e.evaluate(`
|
|
12
12
|
async () => {
|
|
13
13
|
const csrf = (document.cookie.match(/bili_jct=([^;]+)/) || [])[1] || "";
|
|
14
|
-
const body = new URLSearchParams(${
|
|
14
|
+
const body = new URLSearchParams(${s});
|
|
15
15
|
body.set("csrf", csrf);
|
|
16
16
|
const res = await fetch(${o}, {
|
|
17
17
|
method: "POST",
|
|
@@ -28,10 +28,10 @@ import g from"node:https";import{ArgumentError as u,AuthRequiredError as d,Comma
|
|
|
28
28
|
return { code: -1, message: "Non-JSON response (HTTP " + res.status + "): " + text.slice(0, 200) };
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
`)}export async function apiPostAbs(t,
|
|
31
|
+
`)}export async function apiPostAbs(e,t,r={}){const n=r.params??{},i=Object.fromEntries(Object.entries(n).map(([c,f])=>[c,String(f)])),s=JSON.stringify(i);let o=t;if(r.query&&Object.keys(r.query).length>0){const c=new URLSearchParams(Object.fromEntries(Object.entries(r.query).map(([f,l])=>[f,String(l)]))).toString().replace(/\+/g,"%20");o=`${t}?${c}`}const a=JSON.stringify(o);return e.evaluate(`
|
|
32
32
|
async () => {
|
|
33
33
|
const csrf = (document.cookie.match(/bili_jct=([^;]+)/) || [])[1] || "";
|
|
34
|
-
const body = new URLSearchParams(${
|
|
34
|
+
const body = new URLSearchParams(${s});
|
|
35
35
|
body.set("csrf", csrf);
|
|
36
36
|
body.set("csrf_token", csrf);
|
|
37
37
|
const res = await fetch(${a}, {
|
|
@@ -47,4 +47,4 @@ import g from"node:https";import{ArgumentError as u,AuthRequiredError as d,Comma
|
|
|
47
47
|
return { code: -1, message: "Non-JSON response (HTTP " + res.status + "): " + text.slice(0, 200) };
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
`)}export async function resolveVideoIds(t
|
|
50
|
+
`)}export async function resolveVideoIds(e,t){let r;try{r=await resolveBvid(t)}catch(o){throw new m(`Cannot resolve Bilibili BV ID from input: ${String(t??"")}`,o instanceof Error?o.message:String(o))}const n=await apiGet(e,"/x/web-interface/view",{params:{bvid:r}}),s=requireOkPayload(n,"view")?.aid;if(!s)throw new u(`Cannot resolve aid for bvid: ${r}`);return{bvid:r,aid:Number(s)}}export async function getSelfUid(e){const r=(await w(e))?.data?.mid;if(!r)throw new d("bilibili.com");return String(r)}export function readSearchResults(e,t){if(!e||typeof e!=="object"||Array.isArray(e))throw new u(`Bilibili ${t} returned malformed result`);const r=e.data,n=Boolean(r&&typeof r==="object"&&!Array.isArray(r)&&r.v_voucher);if(Object.hasOwn(e,"code")&&e.code!==0||n){const i=e.message??"";if(!n&&isAuthLikeBilibiliError(e.code,i))throw new d("bilibili.com",`Bilibili ${t} requires login or permission: ${i} (${e.code})`);throw new u(`Bilibili ${t} 被平台限流/风控拦截(code=${e.code}${i?`, ${i}`:""}${n?", v_voucher":""})`,"这是搜索接口的频率限制,不是「用户不存在」。停止连续搜索,隔一段时间再试;已知 UID 时直接传数字 UID 可完全绕开搜索。")}if(!r||typeof r!=="object"||Array.isArray(r))throw new u(`Bilibili ${t} returned malformed result`);if(!Object.hasOwn(r,"result")){if(Number(r.numResults)===0)return[];throw new u(`Bilibili ${t} returned malformed result(data 键:${Object.keys(r).join(",")||"空"})`)}if(!Array.isArray(r.result))throw new u(`Bilibili ${t} returned malformed result`);return r.result}export async function resolveUser(e,t){if(/^\d+$/.test(t))return{mid:t,name:"",exact:!0};const r=await apiGet(e,"/x/web-interface/wbi/search/type",{params:{search_type:"bili_user",keyword:t},signed:!0}),n=readSearchResults(r,`user search for ${t}`);if(n.length===0)throw new b(`bilibili user search: ${t}`,"User may not exist or username may have changed.");const i=(c)=>stripHtml(String(c?.uname??"")),s=n.find((c)=>i(c)===t),o=s??n[0],a=String(o?.mid??"").trim();if(!a)throw new u(`Bilibili user search returned malformed mid for ${t}`);return{mid:a,name:i(o),exact:Boolean(s)}}export async function resolveUid(e,t){return(await resolveUser(e,t)).mid}export function formatBeijingTime(e){const t=Number(e);if(!e||!Number.isFinite(t))return"";return new Date((t+28800)*1000).toISOString().slice(0,16).replace("T"," ")}export function sleep(e){return new Promise((t)=>setTimeout(t,e))}export function describeReplyVisibility(e){const t=Number.isInteger(e?.state)?e.state:null,r=Number.isInteger(e?.attr)?e.attr:null;let n="unknown";if(t===0)n="public";else if(t===17)n="self-only";return{state:t,attr:r,visibility:n}}export function parseMsgContent(e){const t=String(e??"");try{const r=JSON.parse(t);if(r&&typeof r==="object"){if(typeof r.content==="string")return r.content;if(typeof r.title==="string"||typeof r.text==="string")return[r.title,r.text].filter(Boolean).join(":");return t}}catch{}return t}export function describeDmMessage(e){const t=Number(e?.msg_type??0),r=String(e?.content??"");let n=null;try{n=JSON.parse(r)}catch{}const i=n&&typeof n==="object"&&!Array.isArray(n)?n:null,s=Number(e?.msg_status??0)===1;if(t===5)return{content:`[撤回了一条消息,被撤回消息 msg_key=${r}]`,image_url:"",recalled:!0};if(t===2&&i&&typeof i.url==="string")return{content:"[图片]",image_url:i.url,recalled:s};if(i&&typeof i.pic_url==="string")return{content:[i.title,i.jump_url].filter((o)=>typeof o==="string"&&o).join(" ")||"[图片]",image_url:i.pic_url,recalled:s};return{content:parseMsgContent(r),image_url:"",recalled:s}}
|
package/clis/cnblogs/article.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CliError as
|
|
1
|
+
import{CliError as u,CommandExecutionError as T}from"@jackwener/opencli/errors";import{cli as I,Strategy as N}from"@jackwener/opencli/registry";import{publishArticle as x}from"../_shared/article/publish.js";export const cnblogsProfile={home:"https://i.cnblogs.com/posts/edit",outputFormat:"markdown",image:{uploadFn:async(e,t)=>{const a=t.cookie("XSRF-TOKEN");if(!a)throw Error("未找到 XSRF-TOKEN cookie,请确认已登录博客园");const r=await fetch(e,{credentials:"omit"});if(!r.ok)throw Error("图片下载失败: "+e+"(HTTP "+r.status+")");const l=await r.blob(),o=new FormData;o.append("image",l,"image.png");o.append("app","blog");o.append("uploadType","Select");const n=await fetch("https://upload.cnblogs.com/v2/images/cors-upload",{method:"POST",credentials:"include",headers:{"x-xsrf-token":a},body:o}),i=await n.text();if(!n.ok)throw Error("图片上传失败: HTTP "+n.status+" "+i.slice(0,150));let s=null;try{s=JSON.parse(i)}catch(d){}if(!s)throw Error("图片上传失败: 响应不是 JSON - "+i.slice(0,100));const c=s.data||s.url||s.imageUrl||s.src;if(!c||typeof c!=="string")throw Error("图片上传失败: 无法解析图片 URL - "+JSON.stringify(s).slice(0,150));return{url:c}},skip:["cnblogs.com"]},publish:async(e,t)=>{const a=t.cookie("XSRF-TOKEN");if(!a)return{ok:!1,stage:"auth",status:401,message:"未找到 XSRF-TOKEN cookie,请确认已登录博客园并在 i.cnblogs.com 下访问过"};const r={"Content-Type":"application/json","x-xsrf-token":a},l={id:null,postType:2,accessPermission:0,title:e.title,url:null,postBody:e.content,categoryIds:null,categories:null,collectionIds:[],inSiteCandidate:!1,inSiteHome:!1,siteCategoryId:null,blogTeamIds:null,isPublished:!1,displayOnHomePage:!1,isAllowComments:!0,includeInMainSyndication:!1,isPinned:!1,showBodyWhenPinned:!1,isOnlyForRegisterUser:!1,isUpdateDateAdded:!1,entryName:null,description:null,featuredImage:null,tags:e.params&&Array.isArray(e.params.tags)&&e.params.tags.length?e.params.tags:null,password:null,publishAt:null,datePublished:new Date().toISOString(),dateUpdated:null,isMarkdown:!0,isDraft:!0,autoDesc:null,changePostType:!1,blogId:0,author:null,removeScript:!1,clientInfo:null,changeCreatedTime:!1,canChangeCreatedTime:!1,isContributeToImpressiveBugActivity:!1,usingEditorId:5,sourceUrl:null},o=await fetch("https://i.cnblogs.com/api/posts",{method:"POST",credentials:"include",headers:r,body:JSON.stringify(l)}),n=await o.text();if(!o.ok)return{ok:!1,stage:"create",status:o.status,message:n.slice(0,300)};let i=null;try{i=JSON.parse(n)}catch(y){}if(!i||!i.id)return{ok:!1,stage:"create",status:o.status,message:"创建草稿失败: "+n.slice(0,300)};const s=String(i.id),c="https://i.cnblogs.com/articles/edit;postId="+s;if(e.draftOnly)return{ok:!0,draft:!0,id:s,url:c};const d=await fetch("https://i.cnblogs.com/api/articles/"+s,{credentials:"include",headers:r}),g=await d.text();let p=null;try{p=JSON.parse(g)}catch(y){}const f=p&&p.blogPost;if(!d.ok||!f)return{ok:!1,stage:"publish",status:d.status,message:"读取草稿完整对象失败: "+g.slice(0,300),id:s};f.isPublished=!0;f.isDraft=!1;const m=await fetch("https://i.cnblogs.com/api/posts",{method:"POST",credentials:"include",headers:r,body:JSON.stringify(f)}),w=await m.text();if(!m.ok)return{ok:!1,stage:"publish",status:m.status,message:w.slice(0,300),id:s};let h=null;try{h=JSON.parse(w)}catch(y){}const b=h&&h.url||c;return{ok:!0,draft:!1,id:s,url:b}}},cnblogsAuthProfile={home:"https://home.cnblogs.com",checkAuth:async(e)=>{try{const t=await fetch("https://home.cnblogs.com/user/CurrentUserInfo",{method:"GET",credentials:"include"});if(!t.ok)return{isAuthenticated:!1,error:"HTTP "+t.status};const a=await t.text(),r=a.match(/href="\/u\/([^/]+)\/"/),l=a.match(/<img[^>]+class="pfs"[^>]+src="([^"]+)"/);if(!r)return{isAuthenticated:!1};const o=r[1];return{isAuthenticated:!0,userId:o,username:o,avatar:l?l[1]:""}}catch(t){return{isAuthenticated:!1,error:String(t&&t.message||t)}}}};function P(e){if(!e.execute)throw new u("INVALID_INPUT","此命令需要 --execute 参数才会实际写入博客园。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function S(e){const t=typeof e.text==="string"?e.text:void 0,a=typeof e.file==="string"?e.file:void 0;if(t&&a)throw new u("INVALID_INPUT","不能同时使用 <text> 和 --file,选其一即可");let r=t??"";if(a){const{readFile:l,stat:o}=await import("node:fs/promises");let n;try{n=await o(a)}catch{throw new u("INVALID_INPUT","文件不存在: "+a)}if(!n.isFile())throw new u("INVALID_INPUT","必须是可读文本文件: "+a);let i;try{i=await l(a)}catch{throw new u("INVALID_INPUT","文件读取失败: "+a)}try{r=new TextDecoder("utf-8",{fatal:!0}).decode(i)}catch{throw new u("INVALID_INPUT","文件不是有效 UTF-8 编码: "+a)}}if(!r.trim())throw new u("INVALID_INPUT","正文不能为空");return r}function k(e,t,a,r,l={}){return[{status:"success",outcome:r,message:e,target_type:t,target:a,...l}]}I({site:"cnblogs",name:"article",access:"write",description:"发布博客园文章(Markdown)。正文默认 Markdown;图片自动转存到博客园图床。",domain:"cnblogs.com",strategy:N.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:"tags",help:"文章标签,逗号分隔(可空)"},{name:"draft",type:"boolean",help:"仅保存草稿,不发布"},{name:"execute",type:"boolean",help:"实际执行写入。不加此参数命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url","images_uploaded","images_failed"],func:async(e,t)=>{if(!e)throw new T("博客园文章发布需要浏览器会话");P(t);const a=String(t.title??"").trim();if(!a)throw new u("INVALID_INPUT","文章标题不能为空");const r=await S(t),l=Boolean(t.draft),o=[...new Set(String(t.tags??"").split(/[,,]/).map((d)=>d.trim()).filter(Boolean))],n=await x(e,{title:a,body:r,format:t.html?"html":"markdown",draftOnly:l,profile:cnblogsProfile,...o.length?{publishParams:{tags:o}}:{}}),i=n.images.uploaded.length|0,s=n.images.failed.length|0;let c=n.draft?"已保存博客园草稿":"已发布博客园文章";if(i||s)c+="·图片: "+i+" 张已转存"+(s?","+s+" 张失败":"");return k(c,"article","",n.draft?"draft":"created",{created_target:"article:"+n.id,created_url:n.url,images_uploaded:i,images_failed:s})}});
|
package/clis/csdn/stats.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as h,Strategy as w}from"@jackwener/opencli/registry";import{AuthRequiredError as y,CliError as A,EmptyResultError as S,EXIT_CODES as _}from"@jackwener/opencli/errors";const x="https://mp.csdn.net/mp_blog/manage/article",T=20,g=`
|
|
2
2
|
() => {
|
|
3
3
|
const dateRe = /^\\d{4}-\\d{2}-\\d{2}(\\s|$)/;
|
|
4
4
|
return Array.from(document.querySelectorAll('.article-list-item-mp')).map((row) => {
|
|
@@ -38,22 +38,16 @@ import{cli as b,Strategy as p}from"@jackwener/opencli/registry";import{EmptyResu
|
|
|
38
38
|
};
|
|
39
39
|
}).filter((r) => r.title);
|
|
40
40
|
}
|
|
41
|
-
`,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (document.querySelector('.article-list-item-mp')) break;
|
|
45
|
-
if (/passport\\.csdn\\.net/.test(location.href)) break;
|
|
46
|
-
const t = document.body?.innerText || '';
|
|
47
|
-
if (t.includes('暂无数据') || t.includes('暂无文章')) break;
|
|
48
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
49
|
-
}
|
|
41
|
+
`,k=`
|
|
42
|
+
() => {
|
|
43
|
+
const t = document.body?.innerText || '';
|
|
50
44
|
return {
|
|
51
45
|
href: location.href,
|
|
52
46
|
hasRows: !!document.querySelector('.article-list-item-mp'),
|
|
53
|
-
|
|
47
|
+
emptyList: t.includes('暂无数据') || t.includes('暂无文章'),
|
|
54
48
|
};
|
|
55
49
|
}
|
|
56
|
-
`,
|
|
50
|
+
`,f=20;function b(t){return/passport\.csdn\.net/.test(String(t||""))}async function C(t){let n=null;for(let s=0;s<f*2;s++){let e=null;try{e=o(await t.evaluate(k))}catch{e=null}if(e){n=e;if(e.hasRows||e.emptyList||b(e.href))return e}await t.wait(0.5)}return n}async function E(t){const n=await t.getCookies({url:"https://www.csdn.net"});return Array.isArray(n)&&n.some((s)=>s.name==="UserToken"&&s.value)}function p(t){return new y("mp.csdn.net",`CSDN 未登录:${t}。需要用户在 PublishPort 客户端登录 CSDN 后再试`+"(若命令带了 --site-session / --profile,确认它指向的是已登录 CSDN 的那个浏览器环境)。")}const L=`
|
|
57
51
|
() => {
|
|
58
52
|
const next = document.querySelector('[class*=pagination] .btn-next, [class*=pagination] button.next, .el_mcm-pagination .btn-next');
|
|
59
53
|
if (next && !next.disabled && !next.classList.contains('is-disabled')) {
|
|
@@ -68,4 +62,4 @@ import{cli as b,Strategy as p}from"@jackwener/opencli/registry";import{EmptyResu
|
|
|
68
62
|
}
|
|
69
63
|
return false;
|
|
70
64
|
}
|
|
71
|
-
`;function
|
|
65
|
+
`;function o(t){if(t&&typeof t==="object"&&!Array.isArray(t)&&"session"in t&&"data"in t)return t.data;return t}h({site:"csdn",name:"stats",access:"read",description:"CSDN 文章数据(每篇阅读/点赞/评论/收藏等全量运营指标)",domain:"mp.csdn.net",strategy:w.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","status","badges","edit_url"],func:async(t,n)=>{const s=Number(n.limit)||20;await t.goto(x);const e=await C(t);if(e&&b(e.href))throw p("内容管理页被重定向到登录页");if(!e||!e.hasRows&&!e.emptyList){if(!await E(t))throw p("当前浏览器环境里没有 CSDN 登录 cookie(UserToken)");throw new A("TEMPORARILY_UNAVAILABLE",`CSDN 内容管理页 ${f} 秒内没有渲染出文章列表`+`(当前页面:${e?.href||"未知"})。登录 cookie 在,这不是未登录,是页面加载慢或被临时拦截。`,"稍后重试同一条命令;连续失败时让用户在浏览器里打开 mp.csdn.net/mp_blog/manage/article 看是否有验证或异常提示。",_.TEMPFAIL)}const a=new Set,r=[];for(let i=1;i<=T&&r.length<s;i++){const c=o(await t.evaluate(g));let u=0;for(const l of Array.isArray(c)?c:[]){const d=l.id||`${l.title}@@${l.published_at}`;if(a.has(d))continue;a.add(d);r.push(l);u++}if(r.length>=s)break;const m=o(await t.evaluate(L));if(!m)break;await t.wait(2);if(u===0&&!m)break}if(r.length===0)throw new S("csdn stats","内容管理页没有任何文章(登录正常,账号下确实没有内容)。");return r.slice(0,s).map((i)=>({id:i.id,type:"文章",title:i.title,url:i.url,published_at:i.published_at,views:i.views,likes:i.likes,comments:i.comments,collects:i.collects,shares:"",status:i.status,badges:i.badges,edit_url:i.edit_url}))}});
|