publishport-opencli 1.0.26 → 1.0.28
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 +308 -18
- package/clis/_shared/video-publish.js +2 -1
- package/clis/baijiahao/article.js +17 -1
- package/clis/bilibili/coin.js +1 -1
- package/clis/bilibili/danmaku.js +5 -0
- package/clis/bilibili/favorite-add.js +1 -1
- package/clis/bilibili/upload.js +1 -1
- package/clis/bilibili/video-edit.js +3 -0
- package/clis/bilibili/video.js +1 -1
- package/clis/jike/api.js +2 -2
- package/clis/jike/comments.js +1 -0
- package/clis/jike/delete.js +1 -0
- package/clis/jike/topic.js +1 -20
- package/clis/kuaishou/auth.js +8 -1
- package/clis/kuaishou/publish.js +6 -6
- package/clis/nowcoder/comment.js +1 -1
- package/clis/nowcoder/comments.js +1 -1
- package/clis/nowcoder/detail.js +4 -2
- 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/drafts.js +1 -1
- package/clis/nowcoder/experience.js +4 -2
- package/clis/nowcoder/inbox.js +1 -1
- package/clis/nowcoder/like.js +1 -1
- package/clis/nowcoder/my.js +1 -1
- package/clis/nowcoder/recommend.js +1 -1
- package/clis/nowcoder/search.js +3 -2
- package/clis/nowcoder/utils.js +14 -17
- package/clis/nowcoder/watch.js +1 -1
- package/clis/quora/create-answer.js +12 -8
- package/clis/reddit/comment.js +28 -7
- package/clis/reddit/upvote.js +21 -5
- package/clis/xiaohongshu/publish.js +1 -1
- package/clis/zhihu/comment.js +2 -2
- package/dist/src/cli.js +23 -23
- package/dist/src/commands/auth.js +3 -3
- package/dist/src/doctor.js +1 -1
- package/dist/src/pipeline/template.js +3 -1
- package/package.json +1 -1
package/clis/bilibili/upload.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as B from"node:fs";import*as z from"node:path";import{cli as A,Strategy as F}from"@jackwener/opencli/registry";import{ArgumentError as P,AuthRequiredError as T,CommandExecutionError as y}from"@jackwener/opencli/errors";import{resolveVideoFile as L,resolveImageFile as N,videoTimeoutArg as D}from"../_shared/video-publish.js";import{setInputFiles as J}from"../_shared/file-inject.js";const R={bda2:{os:"upos",upcdn:"bda2",probe_version:20221109},bldsa:{os:"upos",upcdn:"bldsa",probe_version:20221109},qn:{os:"upos",upcdn:"qn",probe_version:20221109},ws:{os:"upos",upcdn:"ws",probe_version:20221109}},C="bda2",M=["SESSDATA","bili_jct","DedeUserID"],q="https://member.bilibili.com/platform/home",O="__pp_bili_video_input";function K(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{error:"文件未就绪(input 为空)"};var n=e.files[0],r=n.size,o=t.line;return async function(){try{var a=new URLSearchParams({profile:"ugcfx/bup",name:n.name,size:String(r),r:o.os,ssl:"0",version:"2.14.0",build:"2100400",upcdn:o.upcdn,probe_version:String(o.probe_version)}),i=await(await fetch("https://member.bilibili.com/preupload?"+a.toString(),{credentials:"include",headers:{Referer:"https://www.bilibili.com"}})).json();if(i.OK!==1)return{error:"preupload 失败:"+JSON.stringify(i)};var s="https:"+i.endpoint+"/"+String(i.upos_uri).replace(/^upos:\/\//,""),b=new URLSearchParams({uploads:"",output:"json",profile:"ugcfx/bup",filesize:String(r),partsize:String(i.chunk_size),biz_id:String(i.biz_id)}),m=await(await fetch(s+"?"+b.toString(),{method:"POST",headers:{"x-upos-auth":i.auth}})).json();if(m.OK!==1)return{error:"获取 upload_id 失败:"+JSON.stringify(m)};var S=Math.max(1,Math.ceil(r/i.chunk_size));return{url:s,auth:i.auth,uploadId:m.upload_id,chunkSize:i.chunk_size,bizId:i.biz_id,size:r,name:n.name,total:S}}catch(h){return{error:"preupload 异常:"+String(h&&h.message||h)}}}()}function V(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{ok:!1,error:"文件丢失(页面可能已导航)"};var n=e.files[0],r=t.idx*t.chunkSize,o=n.slice(r,Math.min(r+t.chunkSize,t.size)),a=o.size,i=new URLSearchParams({partNumber:String(t.idx+1),uploadId:String(t.uploadId),chunk:String(t.idx),chunks:String(t.total),size:String(a),start:String(r),end:String(r+a),total:String(t.size)});return async function(){for(var s=0;s<5;s++){try{var b=await fetch(t.url+"?"+i.toString(),{method:"PUT",headers:{"x-upos-auth":t.auth},body:o});if(b.status<400){var m=await b.text();if(m==="MULTIPART_PUT_SUCCESS"||m==="")return{ok:!0}}}catch(S){}await new Promise(function(S){setTimeout(S,800*(s+1))})}return{ok:!1,error:"分块 "+t.idx+" 多次重试仍失败"}}()}function G(t){var e=[];for(var n=1;n<=t.total;n++)e.push({partNumber:n,eTag:"etag"});var r=new URLSearchParams({output:"json",name:t.name,profile:"ugcfx/bup",uploadId:String(t.uploadId),biz_id:String(t.bizId)});return async function(){try{var o=await(await fetch(t.url+"?"+r.toString(),{method:"POST",headers:{"x-upos-auth":t.auth,"content-type":"application/json; charset=UTF-8"},body:JSON.stringify({parts:e})})).json();if(o.OK!==1)return{error:"complete 失败:"+JSON.stringify(o)};var a=String(o.key).replace(/^\//,"").replace(/\.[^.]+$/,"");return{filename:a,cid:t.bizId}}catch(i){return{error:"complete 异常:"+String(i&&i.message||i)}}}()}function H(t){return async function(){try{var e=await fetch("https://member.bilibili.com/x/vupre/web/topic/type?type_id="+encodeURIComponent(t.tid)+"&pn=0&ps=200",{credentials:"include"}),n=await e.json();if(!n||n.code!==0||!n.data||!Array.isArray(n.data.topics))return{error:"获取话题列表失败:"+(n&&n.message||"code="+(n&&n.code))};var r=null;for(var o=0;o<n.data.topics.length;o++)if(Number(n.data.topics[o].topic_id)===Number(t.topicId)){r=n.data.topics[o];break}if(!r)return{error:"topic_id "+t.topicId+" 不在分区 "+t.tid+" 的可用话题里(用 bilibili topics --tid "+t.tid+" 查合法值)"};return{topicId:Number(r.topic_id),missionId:r.mission_id!=null?Number(r.mission_id):null,name:r.topic_name||""}}catch(a){return{error:"解析话题异常:"+String(a&&a.message||a)}}}()}function Q(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]);try{var r=t.dataUri;if(typeof r==="string"&&r.indexOf("data:image/webp")===0){var o=r.indexOf(",");if(o===-1)return{error:"封面 data URI 格式非法"};var a=r.slice(5,o),i=r.slice(o+1),s;if(a.indexOf("base64")!==-1){var b=atob(i);s=new Uint8Array(b.length);for(var m=0;m<b.length;m++)s[m]=b.charCodeAt(m)}else s=new TextEncoder().encode(decodeURIComponent(i));var S=new Blob([s],{type:"image/webp"}),h=await createImageBitmap(S),v=document.createElement("canvas");v.width=h.width;v.height=h.height;var c=v.getContext("2d");c.fillStyle="#ffffff";c.fillRect(0,0,v.width,v.height);c.drawImage(h,0,0);var p=await new Promise(function(f){v.toBlob(f,"image/jpeg",0.92)});if(!p)return{error:"封面 webp 转码 JPEG 失败"};r=await new Promise(function(f,x){var w=new FileReader;w.onload=function(){f(String(w.result))};w.onerror=function(){x(Error("FileReader 读取转码结果失败"))};w.readAsDataURL(p)})}var _=new URLSearchParams;_.append("cover",r);_.append("csrf",n);var u=await(await fetch("https://member.bilibili.com/x/vu/web/cover/up",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:_.toString()})).json();if(!u||u.code!==0||!u.data||!u.data.url)return{error:"封面上传失败:"+(u&&u.message?u.message:JSON.stringify(u))};return{url:u.data.url}}catch(f){return{error:"封面上传异常:"+String(f&&f.message||f)}}}()}function W(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.meta;for(var o in r)if(r[o]===null||r[o]===void 0)delete r[o];r.csrf=n;try{var a=await fetch("https://member.bilibili.com/x/vu/web/add/v3?csrf="+encodeURIComponent(n)+"&t="+Date.now(),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),i=await a.json();if(!i||i.code!==0)return{error:"add/v3 提交失败:"+(i&&i.message?i.message:"")+"(code="+(i&&i.code)+" status="+a.status+")"};return{bvid:i.data&&i.data.bvid,aid:i.data&&i.data.aid}}catch(s){return{error:"add/v3 异常:"+String(s&&s.message||s)}}}()}function X(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.body;r.csrf=n;try{var o=await fetch("https://member.bilibili.com/x/vupre/web/draft/add?t="+Date.now()+"&csrf="+encodeURIComponent(n),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),a=await o.json();if(!a||a.code!==0)return{error:"draft/add 保存草稿失败:"+(a&&a.message?a.message:"")+"(code="+(a&&a.code)+" status="+o.status+")"};return{data:a.data}}catch(i){return{error:"draft/add 异常:"+String(i&&i.message||i)}}}()}const g=(t,e,n)=>t.evaluateWithArgs(`(${e.toString()})(a)`,{a:n});function Y(t){const e=N(t),n=z.extname(e).toLowerCase(),r=n===".png"?"image/png":n===".webp"?"image/webp":n===".bmp"?"image/bmp":"image/jpeg",o=B.readFileSync(e).toString("base64");return`data:${r};base64,${o}`}async function Z(t){for(let e=0;e<8;e++){await t.goto(q);await t.wait({time:1});let n="";try{n=String(await t.evaluate("location.href")||"")}catch{n=""}if(/^https?:\/\/member\.bilibili\.com\//.test(n)){const r=await t.getCookies({url:"https://member.bilibili.com"}),o=new Map;for(const i of Array.isArray(r)?r:[])if(i&&typeof i.name==="string")o.set(i.name,String(i.value??""));const a=M.filter((i)=>!o.get(i));if(!a.length)return o;throw new T("member.bilibili.com",`缺少 B站登录 cookie(${a.join(", ")})。请先在客户端登录哔哩哔哩。`)}}throw new T("member.bilibili.com","无法进入 B站创作中心(可能未登录或被风控)。请先在客户端登录哔哩哔哩。")}A({site:"bilibili",name:"upload",access:"write",description:"投稿视频到 B站(在真实浏览器登录态里走 web 投稿:preupload→upos 分块→add/v3)。默认仅 dry-run 校验,加 --execute 才真正上传并提交。",domain:"member.bilibili.com",strategy:F.COOKIE,browser:!0,args:[{name:"file",required:!0,positional:!0,help:"视频文件路径"},{name:"title",help:"稿件标题(默认取文件名)"},{name:"tid",type:"number",required:!0,help:"分区 id(B站 typeid,必填,禁止臆造)。合法值用 `bilibili partitions` 列举后取。"},{name:"tag",required:!0,help:"标签,逗号分隔(B站要求至少 1 个)"},{name:"desc",fromFile:"desc-file",help:"简介"},{name:"topic",type:"number",help:"参与话题的 topic_id(重要流量入口)。合法值用 `bilibili topics --tid <tid>` 列举,禁止臆造;mission_id 自动匹配。"},{name:"cover",help:"封面图片路径(不传由 B站自动截取)"},{name:"copyright",type:"number",help:"1=自制 2=转载(默认 1)"},{name:"source",help:"转载来源 URL(copyright=2 时必填)"},{name:"dynamic",help:"同步发布的动态文案(可空)"},{name:"no-reprint",type:"boolean",help:"禁止转载(默认允许)"},{name:"dtime",type:"number",help:"定时发布的 10 位 unix 时间戳(可空=立即)"},{name:"line",help:`上传线路:${Object.keys(R).join("/")}(默认 ${C})`},{name:"concurrency",type:"number",help:"分块并发数(默认 3)"},{name:"draft",type:"boolean",help:"存草稿而非直接发布(上传视频后存到创作中心草稿箱,不公开)"},{name:"execute",type:"boolean",help:"真正发布投稿;不带(也不带 --draft)则只做 dry-run 校验"},D()],columns:["status","title","bvid","url"],func:async(t,e)=>{if(!t)throw new y("bilibili upload 需要浏览器会话");const n=L(String(e.file??"")),r=String(e.title??"").trim()||z.basename(n).replace(/\.[^.]+$/,""),o=String(e.tag??"").split(",").map((d)=>d.trim()).filter(Boolean);if(!o.length)throw new P("B站投稿至少需要 1 个标签,用 --tag 传(逗号分隔)");if(e.tid==null||e.tid===""||!Number.isFinite(Number(e.tid)))throw new P("必须用 --tid 指定分区 id(数字)。合法值用 `bilibili partitions` 列举,禁止臆造。");const a=Number(e.tid),i=e.copyright!=null&&e.copyright!==""?Number(e.copyright):1;if(i!==1&&i!==2)throw new P("--copyright 只能是 1(自制) 或 2(转载)");const s=String(e.source??"").trim();if(i===2&&!s)throw new P("转载(copyright=2)必须用 --source 提供转载来源");if(e.cover)N(String(e.cover));const b=String(e.line??C),m=R[b];if(!m)throw new P(`未知上传线路「${b}」,可选:${Object.keys(R).join("/")}`);const S=e.concurrency!=null&&e.concurrency!==""?Number(e.concurrency):3,h=Boolean(e.draft),v=Boolean(e.execute)&&!h;await Z(t);await t.evaluate(`(() => { var id=${JSON.stringify(O)}; var el=document.getElementById(id); if(!el){ el=document.createElement('input'); el.type='file'; el.id=id; el.style.cssText='position:fixed;left:-9999px;top:0;'; document.body.appendChild(el);} el.value=''; return true; })()`);await J(t,`#${O}`,[n]);if(!h&&!v)return{status:"dry-run",title:r,bvid:"",url:`校验通过:登录态有效、视频已注入就绪、分区 ${a}、标签 [${o.join(", ")}]、版权 ${i===1?"自制":"转载"}。加 --execute 发布,或 --draft 存草稿。`};const c=await g(t,K,{inputId:O,line:m});if(!c||c.error)throw new y(`B站 preupload 失败:${c&&c.error||"无返回"}`);for(let d=0;d<c.total;d++){const l=await g(t,V,{inputId:O,url:c.url,auth:c.auth,uploadId:c.uploadId,idx:d,total:c.total,size:c.size,chunkSize:c.chunkSize});if(!l||!l.ok)throw new y(`B站分块上传失败(${d+1}/${c.total}):${l&&l.error||"无返回"}`)}const p=await g(t,G,{url:c.url,auth:c.auth,uploadId:c.uploadId,bizId:c.bizId,name:c.name,total:c.total});if(!p||p.error||!p.filename)throw new y(`B站 complete 失败:${p&&p.error||"未拿到 filename"}`);let _="";if(e.cover){const d=Y(String(e.cover)),l=await g(t,Q,{dataUri:d});if(!l||l.error||!l.url)throw new y(`B站封面上传失败:${l&&l.error||"未拿到 url"}`);_=l.url}let u=null,f=null;if(e.topic!=null&&e.topic!==""){if(!Number.isFinite(Number(e.topic)))throw new P("--topic 必须是 topic_id 数字,用 `bilibili topics --tid <tid>` 取合法值");const d=await g(t,H,{tid:a,topicId:Number(e.topic)});if(!d||d.error)throw new y(`B站话题解析失败:${d&&d.error||"无返回"}`);u=d.topicId;f=d.missionId}if(h){const d={videos:[{filename:p.filename,title:r,desc:"",cid:p.cid,is_4k:!1,is_8k:!1,is_hdr:!1}],cover:_,cover43:_,ai_cover:0,is_ab_cover:0,ab_cover_info:null,title:r,copyright:i,tid:a,tag:o.join(","),desc:String(e.desc??""),recreate:-1,dynamic:String(e.dynamic??""),is_only_self:0,space_hidden:2,watermark:{state:0},no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1};if(i===2)d.source=s;if(u!=null){d.topic_id=u;d.topic_detail={from_topic_id:u,from_source:"arc.web.search"};if(f!=null)d.mission_id=f}const l=await g(t,X,{body:d});if(!l||l.error)throw new y(`B站 ${l&&l.error||"draft/add 无返回"}`);const U=l.data&&(l.data.aid!=null?l.data.aid:l.data.draft_id!=null?l.data.draft_id:l.data.id);return{status:"✅ 草稿已保存",title:r,bvid:U!=null?`draft:${U}`:"",url:"https://member.bilibili.com/platform/upload-manager/article?group=draft"}}const x=e.dtime!=null&&e.dtime!==""?Number(e.dtime):null,w={title:r,copyright:i,tid:a,tag:o.join(","),mission_id:f,topic_id:u,topic_detail:u!=null?{from_topic_id:u,from_source:"arc.web.recommend"}:null,desc_format_id:9999,desc:String(e.desc??""),dtime:x,recreate:-1,dynamic:String(e.dynamic??""),interactive:0,act_reserve_create:0,no_disturbance:0,porder:null,adorder_type:9,no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},neutral_mark:null,dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1,web_os:1,source:i===2?s:null,watermark:{state:0},cover:_,videos:[{title:r,desc:"",filename:p.filename,cid:p.cid}]},I=await g(t,W,{meta:w});if(!I||I.error)throw new y(`B站 ${I&&I.error||"add/v3 无返回"}`);const E=I.bvid?String(I.bvid):"";return{status:e.dtime?"✅ 定时投稿已提交":"✅ 投稿成功",title:r,bvid:E,url:E?`https://www.bilibili.com/video/${E}`:"(已提交,稍后在创作中心查看)"}}});
|
|
1
|
+
import*as B from"node:fs";import*as N from"node:path";import{cli as L,Strategy as F}from"@jackwener/opencli/registry";import{ArgumentError as w,AuthRequiredError as U,CommandExecutionError as v}from"@jackwener/opencli/errors";import{resolveVideoFile as D,resolveImageFile as z,videoTimeoutArg as J}from"../_shared/video-publish.js";import{setInputFiles as M}from"../_shared/file-inject.js";const O={bda2:{os:"upos",upcdn:"bda2",probe_version:20221109},bldsa:{os:"upos",upcdn:"bldsa",probe_version:20221109},qn:{os:"upos",upcdn:"qn",probe_version:20221109},ws:{os:"upos",upcdn:"ws",probe_version:20221109}},A="bda2",q=["SESSDATA","bili_jct","DedeUserID"],K="https://member.bilibili.com/platform/home",T="__pp_bili_video_input";export async function PP_RESOLVE_AI_STATEMENT(){try{const t=await fetch("https://member.bilibili.com/x/vupre/web/archive/pre?lang=cn",{credentials:"include"}),e=await t.json();if(!t.ok||e?.code!==0)return{error:`读取创作声明失败:HTTP ${t.status} / ${e?.message??e?.code}`};const n=e.data?.neutral_mark?.mark_list;if(!Array.isArray(n))return{error:"当前账号的投稿配置缺少创作声明列表"};const r=n.filter((o)=>o.content==="含AI生成内容");if(r.length!==1||!Number.isSafeInteger(r[0].id)||r[0].id<=0)return{error:"当前账号未返回唯一有效的“含AI生成内容”选项"};return{id:r[0].id,label:r[0].content}}catch(t){return{error:`读取创作声明失败:${t instanceof Error?t.message:String(t)}`}}}function V(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{error:"文件未就绪(input 为空)"};var n=e.files[0],r=n.size,o=t.line;return async function(){try{var a=new URLSearchParams({profile:"ugcfx/bup",name:n.name,size:String(r),r:o.os,ssl:"0",version:"2.14.0",build:"2100400",upcdn:o.upcdn,probe_version:String(o.probe_version)}),i=await(await fetch("https://member.bilibili.com/preupload?"+a.toString(),{credentials:"include",headers:{Referer:"https://www.bilibili.com"}})).json();if(i.OK!==1)return{error:"preupload 失败:"+JSON.stringify(i)};var s="https:"+i.endpoint+"/"+String(i.upos_uri).replace(/^upos:\/\//,""),h=new URLSearchParams({uploads:"",output:"json",profile:"ugcfx/bup",filesize:String(r),partsize:String(i.chunk_size),biz_id:String(i.biz_id)}),m=await(await fetch(s+"?"+h.toString(),{method:"POST",headers:{"x-upos-auth":i.auth}})).json();if(m.OK!==1)return{error:"获取 upload_id 失败:"+JSON.stringify(m)};var g=Math.max(1,Math.ceil(r/i.chunk_size));return{url:s,auth:i.auth,uploadId:m.upload_id,chunkSize:i.chunk_size,bizId:i.biz_id,size:r,name:n.name,total:g}}catch(_){return{error:"preupload 异常:"+String(_&&_.message||_)}}}()}function H(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{ok:!1,error:"文件丢失(页面可能已导航)"};var n=e.files[0],r=t.idx*t.chunkSize,o=n.slice(r,Math.min(r+t.chunkSize,t.size)),a=o.size,i=new URLSearchParams({partNumber:String(t.idx+1),uploadId:String(t.uploadId),chunk:String(t.idx),chunks:String(t.total),size:String(a),start:String(r),end:String(r+a),total:String(t.size)});return async function(){for(var s=0;s<5;s++){try{var h=await fetch(t.url+"?"+i.toString(),{method:"PUT",headers:{"x-upos-auth":t.auth},body:o});if(h.status<400){var m=await h.text();if(m==="MULTIPART_PUT_SUCCESS"||m==="")return{ok:!0}}}catch(g){}await new Promise(function(g){setTimeout(g,800*(s+1))})}return{ok:!1,error:"分块 "+t.idx+" 多次重试仍失败"}}()}function G(t){var e=[];for(var n=1;n<=t.total;n++)e.push({partNumber:n,eTag:"etag"});var r=new URLSearchParams({output:"json",name:t.name,profile:"ugcfx/bup",uploadId:String(t.uploadId),biz_id:String(t.bizId)});return async function(){try{var o=await(await fetch(t.url+"?"+r.toString(),{method:"POST",headers:{"x-upos-auth":t.auth,"content-type":"application/json; charset=UTF-8"},body:JSON.stringify({parts:e})})).json();if(o.OK!==1)return{error:"complete 失败:"+JSON.stringify(o)};var a=String(o.key).replace(/^\//,"").replace(/\.[^.]+$/,"");return{filename:a,cid:t.bizId}}catch(i){return{error:"complete 异常:"+String(i&&i.message||i)}}}()}function Q(t){return async function(){try{var e=await fetch("https://member.bilibili.com/x/vupre/web/topic/type?type_id="+encodeURIComponent(t.tid)+"&pn=0&ps=200",{credentials:"include"}),n=await e.json();if(!n||n.code!==0||!n.data||!Array.isArray(n.data.topics))return{error:"获取话题列表失败:"+(n&&n.message||"code="+(n&&n.code))};var r=null;for(var o=0;o<n.data.topics.length;o++)if(Number(n.data.topics[o].topic_id)===Number(t.topicId)){r=n.data.topics[o];break}if(!r)return{error:"topic_id "+t.topicId+" 不在分区 "+t.tid+" 的可用话题里(用 bilibili topics --tid "+t.tid+" 查合法值)"};return{topicId:Number(r.topic_id),missionId:r.mission_id!=null?Number(r.mission_id):null,name:r.topic_name||""}}catch(a){return{error:"解析话题异常:"+String(a&&a.message||a)}}}()}function W(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]);try{var r=t.dataUri;if(typeof r==="string"&&r.indexOf("data:image/webp")===0){var o=r.indexOf(",");if(o===-1)return{error:"封面 data URI 格式非法"};var a=r.slice(5,o),i=r.slice(o+1),s;if(a.indexOf("base64")!==-1){var h=atob(i);s=new Uint8Array(h.length);for(var m=0;m<h.length;m++)s[m]=h.charCodeAt(m)}else s=new TextEncoder().encode(decodeURIComponent(i));var g=new Blob([s],{type:"image/webp"}),_=await createImageBitmap(g),y=document.createElement("canvas");y.width=_.width;y.height=_.height;var p=y.getContext("2d");p.fillStyle="#ffffff";p.fillRect(0,0,y.width,y.height);p.drawImage(_,0,0);var d=await new Promise(function(u){y.toBlob(u,"image/jpeg",0.92)});if(!d)return{error:"封面 webp 转码 JPEG 失败"};r=await new Promise(function(u,I){var P=new FileReader;P.onload=function(){u(String(P.result))};P.onerror=function(){I(Error("FileReader 读取转码结果失败"))};P.readAsDataURL(d)})}var b=new URLSearchParams;b.append("cover",r);b.append("csrf",n);var f=await(await fetch("https://member.bilibili.com/x/vu/web/cover/up",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:b.toString()})).json();if(!f||f.code!==0||!f.data||!f.data.url)return{error:"封面上传失败:"+(f&&f.message?f.message:JSON.stringify(f))};return{url:f.data.url}}catch(u){return{error:"封面上传异常:"+String(u&&u.message||u)}}}()}function X(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.meta;for(var o in r)if(r[o]===null||r[o]===void 0)delete r[o];r.csrf=n;try{var a=await fetch("https://member.bilibili.com/x/vu/web/add/v3?csrf="+encodeURIComponent(n)+"&t="+Date.now(),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),i=await a.json();if(!i||i.code!==0)return{error:"add/v3 提交失败:"+(i&&i.message?i.message:"")+"(code="+(i&&i.code)+" status="+a.status+")"};return{bvid:i.data&&i.data.bvid,aid:i.data&&i.data.aid}}catch(s){return{error:"add/v3 异常:"+String(s&&s.message||s)}}}()}function Y(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.body;r.csrf=n;try{var o=await fetch("https://member.bilibili.com/x/vupre/web/draft/add?t="+Date.now()+"&csrf="+encodeURIComponent(n),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),a=await o.json();if(!a||a.code!==0)return{error:"draft/add 保存草稿失败:"+(a&&a.message?a.message:"")+"(code="+(a&&a.code)+" status="+o.status+")"};return{data:a.data}}catch(i){return{error:"draft/add 异常:"+String(i&&i.message||i)}}}()}const S=(t,e,n)=>t.evaluateWithArgs(`(${e.toString()})(a)`,{a:n});function Z(t){const e=z(t),n=N.extname(e).toLowerCase(),r=n===".png"?"image/png":n===".webp"?"image/webp":n===".bmp"?"image/bmp":"image/jpeg",o=B.readFileSync(e).toString("base64");return`data:${r};base64,${o}`}async function k(t){for(let e=0;e<8;e++){await t.goto(K);await t.wait({time:1});let n="";try{n=String(await t.evaluate("location.href")||"")}catch{n=""}if(/^https?:\/\/member\.bilibili\.com\//.test(n)){const r=await t.getCookies({url:"https://member.bilibili.com"}),o=new Map;for(const i of Array.isArray(r)?r:[])if(i&&typeof i.name==="string")o.set(i.name,String(i.value??""));const a=q.filter((i)=>!o.get(i));if(!a.length)return o;throw new U("member.bilibili.com",`缺少 B站登录 cookie(${a.join(", ")})。请先在客户端登录哔哩哔哩。`)}}throw new U("member.bilibili.com","无法进入 B站创作中心(可能未登录或被风控)。请先在客户端登录哔哩哔哩。")}L({site:"bilibili",name:"upload",access:"write",description:"投稿视频到 B站(在真实浏览器登录态里走 web 投稿:preupload→upos 分块→add/v3)。默认仅 dry-run 校验,加 --execute 才真正上传并提交。",domain:"member.bilibili.com",strategy:F.COOKIE,browser:!0,args:[{name:"file",required:!0,positional:!0,help:"视频文件路径"},{name:"title",help:"稿件标题(默认取文件名)"},{name:"tid",type:"number",required:!0,help:"分区 id(B站 typeid,必填,禁止臆造)。合法值用 `bilibili partitions` 列举后取。"},{name:"tag",required:!0,help:"标签,逗号分隔(B站要求至少 1 个)"},{name:"desc",fromFile:"desc-file",help:"简介"},{name:"topic",type:"number",help:"参与话题的 topic_id(重要流量入口)。合法值用 `bilibili topics --tid <tid>` 列举,禁止臆造;mission_id 自动匹配。"},{name:"cover",help:"封面图片路径(不传由 B站自动截取)"},{name:"copyright",type:"number",help:"1=自制 2=转载(默认 1)"},{name:"ai-generated",type:"boolean",help:"声明含 AI 生成内容(发布和草稿均支持;按当前账号可用选项校验,不能与转载同时选择)"},{name:"source",help:"转载来源 URL(copyright=2 时必填)"},{name:"dynamic",help:"同步发布的动态文案(可空)"},{name:"no-reprint",type:"boolean",help:"禁止转载(默认允许)"},{name:"dtime",type:"number",help:"定时发布的 10 位 unix 时间戳(可空=立即)"},{name:"line",help:`上传线路:${Object.keys(O).join("/")}(默认 ${A})`},{name:"concurrency",type:"number",help:"分块并发数(默认 3)"},{name:"draft",type:"boolean",help:"存草稿而非直接发布(上传视频后存到创作中心草稿箱,不公开)"},{name:"execute",type:"boolean",help:"真正发布投稿;不带(也不带 --draft)则只做 dry-run 校验"},J()],columns:["status","title","bvid","url"],func:async(t,e)=>{if(!t)throw new v("bilibili upload 需要浏览器会话");const n=D(String(e.file??"")),r=String(e.title??"").trim()||N.basename(n).replace(/\.[^.]+$/,""),o=String(e.tag??"").split(",").map((c)=>c.trim()).filter(Boolean);if(!o.length)throw new w("B站投稿至少需要 1 个标签,用 --tag 传(逗号分隔)");if(e.tid==null||e.tid===""||!Number.isFinite(Number(e.tid)))throw new w("必须用 --tid 指定分区 id(数字)。合法值用 `bilibili partitions` 列举,禁止臆造。");const a=Number(e.tid),i=e.copyright!=null&&e.copyright!==""?Number(e.copyright):1;if(i!==1&&i!==2)throw new w("--copyright 只能是 1(自制) 或 2(转载)");const s=String(e.source??"").trim();if(i===2&&!s)throw new w("转载(copyright=2)必须用 --source 提供转载来源");if(e["ai-generated"]&&i===2)throw new w("B站当前创作声明只能选一项,--ai-generated 不能与 --copyright 2 同时使用");if(e.cover)z(String(e.cover));const h=String(e.line??A),m=O[h];if(!m)throw new w(`未知上传线路「${h}」,可选:${Object.keys(O).join("/")}`);const g=e.concurrency!=null&&e.concurrency!==""?Number(e.concurrency):3,_=Boolean(e.draft),y=Boolean(e.execute)&&!_;await k(t);let p;if(e["ai-generated"]){p=await S(t,PP_RESOLVE_AI_STATEMENT,{});if(!p||p.error)throw new v(p?.error||"B站创作声明查询未返回结果")}await t.evaluate(`(() => { var id=${JSON.stringify(T)}; var el=document.getElementById(id); if(!el){ el=document.createElement('input'); el.type='file'; el.id=id; el.style.cssText='position:fixed;left:-9999px;top:0;'; document.body.appendChild(el);} el.value=''; return true; })()`);await M(t,`#${T}`,[n]);if(!_&&!y)return{status:"dry-run",title:r,bvid:"",url:`校验通过:登录态有效、视频已注入就绪、分区 ${a}、标签 [${o.join(", ")}]、版权 ${i===1?"自制":"转载"}${p?"、创作声明:"+p.label:""}。加 --execute 发布,或 --draft 存草稿。`};const d=await S(t,V,{inputId:T,line:m});if(!d||d.error)throw new v(`B站 preupload 失败:${d&&d.error||"无返回"}`);for(let c=0;c<d.total;c++){const l=await S(t,H,{inputId:T,url:d.url,auth:d.auth,uploadId:d.uploadId,idx:c,total:d.total,size:d.size,chunkSize:d.chunkSize});if(!l||!l.ok)throw new v(`B站分块上传失败(${c+1}/${d.total}):${l&&l.error||"无返回"}`)}const b=await S(t,G,{url:d.url,auth:d.auth,uploadId:d.uploadId,bizId:d.bizId,name:d.name,total:d.total});if(!b||b.error||!b.filename)throw new v(`B站 complete 失败:${b&&b.error||"未拿到 filename"}`);let f="";if(e.cover){const c=Z(String(e.cover)),l=await S(t,W,{dataUri:c});if(!l||l.error||!l.url)throw new v(`B站封面上传失败:${l&&l.error||"未拿到 url"}`);f=l.url}let u=null,I=null;if(e.topic!=null&&e.topic!==""){if(!Number.isFinite(Number(e.topic)))throw new w("--topic 必须是 topic_id 数字,用 `bilibili topics --tid <tid>` 取合法值");const c=await S(t,Q,{tid:a,topicId:Number(e.topic)});if(!c||c.error)throw new v(`B站话题解析失败:${c&&c.error||"无返回"}`);u=c.topicId;I=c.missionId}if(_){const c={videos:[{filename:b.filename,title:r,desc:"",cid:b.cid,is_4k:!1,is_8k:!1,is_hdr:!1}],cover:f,cover43:f,ai_cover:0,is_ab_cover:0,ab_cover_info:null,title:r,copyright:i,tid:a,tag:o.join(","),desc:String(e.desc??""),recreate:-1,dynamic:String(e.dynamic??""),is_only_self:0,space_hidden:2,watermark:{state:0},no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1};if(i===2)c.source=s;if(p)c.creation_statement_id=p.id;if(u!=null){c.topic_id=u;c.topic_detail={from_topic_id:u,from_source:"arc.web.search"};if(I!=null)c.mission_id=I}const l=await S(t,Y,{body:c});if(!l||l.error)throw new v(`B站 ${l&&l.error||"draft/add 无返回"}`);const R=l.data&&(l.data.aid!=null?l.data.aid:l.data.draft_id!=null?l.data.draft_id:l.data.id);return{status:"✅ 草稿已保存",title:r,bvid:R!=null?`draft:${R}`:"",url:"https://member.bilibili.com/platform/upload-manager/article?group=draft"}}const P=e.dtime!=null&&e.dtime!==""?Number(e.dtime):null,C={title:r,copyright:i,tid:a,tag:o.join(","),mission_id:I,topic_id:u,topic_detail:u!=null?{from_topic_id:u,from_source:"arc.web.recommend"}:null,desc_format_id:9999,desc:String(e.desc??""),dtime:P,recreate:-1,dynamic:String(e.dynamic??""),interactive:0,act_reserve_create:0,no_disturbance:0,porder:null,adorder_type:9,no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},creation_statement:p?{id:p.id}:null,dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1,web_os:1,source:i===2?s:null,watermark:{state:0},cover:f,videos:[{title:r,desc:"",filename:b.filename,cid:b.cid}]},E=await S(t,X,{meta:C});if(!E||E.error)throw new v(`B站 ${E&&E.error||"add/v3 无返回"}`);const x=E.bvid?String(E.bvid):"";return{status:e.dtime?"✅ 定时投稿已提交":"✅ 投稿成功",title:r,bvid:x,url:x?`https://www.bilibili.com/video/${x}`:"(已提交,稍后在创作中心查看)"}}});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{readFileSync as S}from"node:fs";import{cli as T,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentError as c,AuthRequiredError as E,CommandExecutionError as u}from"@jackwener/opencli/errors";const b='input[placeholder="请输入稿件标题"]',A='[editor_id="desc_at_editor"] .ql-editor',j='#tag-container input[placeholder="按回车键Enter创建标签"]',q="#tag-container .tag-pre-wrp .close";export function readEditForm(){const i=document.querySelector('input[placeholder="请输入稿件标题"]'),r=document.querySelector('[editor_id="desc_at_editor"] .ql-editor')?.parentElement.__quill,n=document.querySelectorAll("#tag-container");if(!i||!r||n.length!==1)return null;const e=r.getText();if(typeof e!=="string"||!e.endsWith(`
|
|
2
|
+
`))return null;return{title:i.value,desc:e.slice(0,-1),tags:[...n[0].querySelectorAll(".tag-pre-wrp .label-item-v2-container")].map((a)=>({name:a.querySelector(".label-item-v2-content")?.textContent,removable:Boolean(a.querySelector(".close"))}))}}export async function readArchive({bvid:i}){try{const r=await fetch("/x/vupre/web/archive/view?"+new URLSearchParams({bvid:i,topic_grey:"1"}),{credentials:"include"}),n=await r.json();if(r.status===401||n?.code===-101)return{auth:!0,error:"创作中心登录已失效"};if(!r.ok||n?.code!==0)return{error:`HTTP ${r.status} / ${n?.message??n?.code}`};const e=n.data?.archive,a=document.cookie.split(";").map((o)=>o.trim()).find((o)=>o.startsWith("DedeUserID="))?.slice(11);if(!a||!e||String(e.mid)!==a||e.bvid!==i)return{error:"稿件归属或 BV 号不匹配,停止编辑"};if(typeof e.title!=="string"||typeof e.desc!=="string"||typeof e.tag!=="string")return{error:"稿件缺少标题、简介或标签字段"};return{title:e.title,desc:e.desc,tags:e.tag.split(",").filter(Boolean),revision:JSON.stringify(e)}}catch(r){return{error:r instanceof Error?r.message:String(r)}}}const w=(i,r,n={})=>i.evaluateWithArgs(`(${r.toString()})(args)`,{args:n}),v=(i,r)=>i.length===r.length&&[...i].sort().every((n,e)=>n===[...r].sort()[e]),p=(i,r)=>i?.title===r.title&&i.desc===r.desc&&Array.isArray(i.tags)&&v(i.tags,r.tags);async function y(i,r){const n=await w(i,readArchive,{bvid:r});if(n?.auth)throw new E("member.bilibili.com",n.error);if(!n||n.error)throw new u(`读取稿件 ${r} 失败:${n?.error||"空响应"}`);return n}T({site:"bilibili",name:"video-edit",access:"write",description:"编辑自己的视频标题、简介和普通标签;未指定字段及活动话题保持原样。默认 dry-run,加 --execute 才保存并回查后台。AI 声明在官网编辑页不可修改。",domain:"member.bilibili.com",strategy:x.UI,browser:!0,args:[{name:"bvid",type:"string",required:!0,positional:!0,help:"已有视频的完整 BV 号"},{name:"title",type:"string",help:"新的完整标题,最多 80 字"},{name:"desc",type:"string",help:"替换简介(纯文本),可传空字符串清空"},{name:"desc-file",type:"string",help:"从本机 UTF-8 文件读取新的完整简介"},{name:"tag",type:"string",help:"替换全部普通标签,逗号分隔;活动标签保留,不要重复传入"},{name:"execute",type:"boolean",help:"保存修改;缺省仅读取并展示修改内容"},{name:"timeout",type:"int",default:120,help:"整段编辑流程的等待预算(秒);超时后先回查稿件,勿直接重提"}],columns:["bvid","status","field","before","after","url"],func:async(i,r)=>{const n=String(r.bvid||"");if(!/^BV[0-9A-Za-z]{10}$/.test(n))throw new c("请传入完整 BV 号");if(r.desc!==void 0&&r["desc-file"]!==void 0)throw new c("--desc 与 --desc-file 不能同时使用");const e={};if(r.title!==void 0){e.title=String(r.title);if(!e.title.trim()||e.title.length>80)throw new c("标题不能为空且不能超过 80 字")}if(r.desc!==void 0)e.desc=String(r.desc);if(r["desc-file"]!==void 0)try{e.desc=new TextDecoder("utf-8",{fatal:!0}).decode(S(String(r["desc-file"])))}catch(t){throw new c(`读取 UTF-8 简介文件失败:${t.message}`)}if(e.desc!==void 0){e.desc=e.desc.replace(/\r\n/g,`
|
|
3
|
+
`);if(e.desc.length>2000)throw new c("简介不能超过 2000 字")}if(r.tag!==void 0){e.tags=String(r.tag).split(",").map((t)=>t.trim());if(e.tags.some((t)=>!t||t.length>20)||new Set(e.tags).size!==e.tags.length)throw new c("标签不能为空、不能重复,每项最多 20 字")}if(!Object.keys(e).length)throw new c("至少指定 --title、--desc、--desc-file 或 --tag");if(!i)throw new u("稿件编辑需要浏览器会话");const a=`https://member.bilibili.com/platform/upload/video/frame?type=edit&bvid=${n}`;await i.goto(a);await i.wait({selector:b,timeout:20000});const o=await y(i,n),d=await w(i,readEditForm);if(!d||d.tags.some((t)=>typeof t.name!=="string")||!p({...d,tags:d.tags.map((t)=>t.name)},o))throw new u("编辑表单与后台稿件不一致或尚未加载完成,停止修改");const m=d.tags.filter((t)=>!t.removable).map((t)=>t.name);if(e.tags&&(e.tags.length+m.length>10||e.tags.some((t)=>m.includes(t))))throw new c(`普通标签与保留的活动标签合计不能超过 10 个,且不能重复活动标签:${m.join(",")}`);const s={...o,...e,tags:e.tags?[...m,...e.tags]:o.tags},f=Object.fromEntries(Object.keys(e).filter((t)=>t==="tags"?!v(o.tags,s.tags):o[t]!==s[t]).map((t)=>[t,{from:o[t],to:s[t]}])),g=(t)=>(Object.keys(f).length?Object.keys(f):Object.keys(e)).map((l)=>({bvid:n,status:t,field:l,before:l==="tags"?o.tags.join(","):o[l],after:l==="tags"?s.tags.join(","):s[l],url:a}));if(!Object.keys(f).length)return g("unchanged");if(!r.execute)return g("dry-run");if(f.title)await i.fillText(b,s.title);if(f.desc)await i.fillText(A,s.desc);if(f.tags){for(let t=0;t<d.tags.filter((l)=>l.removable).length;t++)await i.click(q,{nth:0});for(const t of e.tags){await i.fillText(j,t);await i.pressKey("Enter")}}const h=await w(i,readEditForm);if(!h||!p({...h,tags:h.tags.map((t)=>t.name)},s))throw new u("写入表单后的内容不匹配,尚未提交;请检查当前编辑页");if((await y(i,n)).revision!==o.revision)throw new u("后台稿件在编辑期间已变化,停止提交,请重新读取");try{await i.click(".submit-add");for(let t=0;t<15;t++){await i.wait({time:1});if(p(await y(i,n),s))return g("updated")}throw Error("后台尚未返回期望的修改内容")}catch(t){throw new u(`稿件 ${n} 已尝试提交,但保存结果未确认:${t.message}。请先回查该稿件,不要直接重复提交。`)}}});
|
package/clis/bilibili/video.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as N,Strategy as j}from"@jackwener/opencli/registry";import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{apiGet as k,resolveBvid as D,parsePageArg as E,selectVideoPart as U}from"./utils.js";function f(i,t){if(!i||typeof i!=="object"||Array.isArray(i))throw new s(`${t} returned a malformed payload`);return i}function C(i){if(i&&typeof i==="object"&&typeof i.session==="string"&&Object.hasOwn(i,"data"))return i.data;return i}function l(i,t){if(i==null)return!1;if(typeof i==="boolean")return i;if(typeof i==="number")return i!==0;throw new s(`${t} returned a malformed flag`)}function T(i,t){if(i==null)return"";if(typeof i==="string")return i;throw new s(`${t} returned a malformed string`)}N({site:"bilibili",name:"video",access:"read",description:"Get Bilibili video metadata (title, author, duration, stats, etc.)",strategy:j.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"BV ID, video URL, or b23.tv short link"},{name:"page",required:!1,help:"分P 选集序号(从 1 开始)。多 P 视频指定某一集,title/cid 返回该集;缺省取整集默认(P1)"}],columns:["field","value"],func:async(i,t)=>{if(!i)throw new s("Browser session required for bilibili video");const n=E(t.page),m=String(t.bvid??"").trim(),g=m.match(/bilibili\.com\/(?:video|bangumi\/play)\/(BV[A-Za-z0-9]+)/i),b=g?g[1]:await D(m);await i.goto(`https://www.bilibili.com/video/${b}/`);const o=C(await k(i,"/x/web-interface/view",{params:{bvid:b}}));f(o,"Bilibili view API");if(o.code!==0)throw new s(`Bilibili view API failed: ${o.message} (${o.code})`);const e=f(o.data,"Bilibili view API data"),r=e.stat||{},p=e.owner||{},d=f(e.rights,"Bilibili view API data.rights"),_=l(d.pay,"Bilibili rights.pay"),S=l(d.ugc_pay,"Bilibili rights.ugc_pay"),B=l(d.arc_pay,"Bilibili rights.arc_pay"),P=l(e.is_upower_exclusive,"Bilibili is_upower_exclusive"),v=_?"vip":S||B?"ugc_pay":P?"upower":"",A=l(d.ugc_pay_preview,"Bilibili rights.ugc_pay_preview")||l(e.is_upower_preview,"Bilibili is_upower_preview"),O=T(e.redirect_url,"Bilibili redirect_url"),I=e.pubdate?new Date((Number(e.pubdate)+28800)*1000).toISOString().slice(0,19)+"+08:00":"";let w=e.title??"",y="",h=e.duration||0;if(n!=null){const a=U(e,n);y=String(a.cid??"");w=(typeof a.part==="string"?a.part.trim():"")||`${e.title??""} P${n}`;if(Number.isFinite(Number(a.duration))&&Number(a.duration)>0)h=Number(a.duration)}const u=h||0,q=Math.floor(u/60),x=u%60,c=[{field:"bvid",value:e.bvid??""},{field:"aid",value:String(e.aid??"")},{field:"title",value:w},{field:"author",value:p.name?`${p.name} (mid: ${p.mid})`:""},{field:"category",value:e.tname_v2||e.tname||""},{field:"publish_time",value:I},{field:"duration",value:u?`${q}m${x}s (${u}s)`:""},{field:"view",value:String(r.view??"")},{field:"danmaku",value:String(r.danmaku??"")},{field:"reply",value:String(r.reply??"")},{field:"like",value:String(r.like??"")},{field:"coin",value:String(r.coin??"")},{field:"favorite",value:String(r.favorite??"")},{field:"share",value:String(r.share??"")},{field:"parts",value:String(e.videos??1)},{field:"thumbnail",value:e.pic??""},{field:"description",value:e.desc??""},{field:"requires_payment",value:String(!!v)},{field:"payment_type",value:v},{field:"pay_preview",value:String(A)},{field:"redirect_url",value:O}];if(n!=null){c.push({field:"page",value:String(n)});c.push({field:"cid",value:y});c.push({field:"series_title",value:e.title??""})}return c}});
|
package/clis/jike/api.js
CHANGED
|
@@ -19,7 +19,7 @@ import{CommandExecutionError as a,AuthRequiredError as n}from"@jackwener/opencli
|
|
|
19
19
|
try {
|
|
20
20
|
resp = await fetch(${JSON.stringify("https://api.ruguoapp.com"+o)}, init);
|
|
21
21
|
} catch (e) {
|
|
22
|
-
//
|
|
22
|
+
// fetch 失败也可能是平台已处理、响应在途中丢失,不能据此重发写操作。
|
|
23
23
|
return { networkError: String((e && e.message) || e) };
|
|
24
24
|
}
|
|
25
25
|
var raw = '';
|
|
@@ -27,4 +27,4 @@ import{CommandExecutionError as a,AuthRequiredError as n}from"@jackwener/opencli
|
|
|
27
27
|
var data = null;
|
|
28
28
|
try { data = JSON.parse(raw); } catch (e) {}
|
|
29
29
|
return { status: resp.status, ok: resp.ok, data: data, raw: raw.slice(0, 500) };
|
|
30
|
-
})()`,t=await r.evaluate(i);if(t&&t.authMissing)throw new n("web.okjike.com","即刻未登录(读不到 JK_ACCESS_TOKEN)");if(t&&t.networkError)throw new a(`即刻 API
|
|
30
|
+
})()`,t=await r.evaluate(i);if(t&&t.authMissing)throw new n("web.okjike.com","即刻未登录(读不到 JK_ACCESS_TOKEN)");if(t&&t.networkError)throw new a(`即刻 API 未收到响应(${t.networkError})。无法确认平台是否已处理;写操作请先回查,禁止直接重试。`);if(!t)throw new a("即刻 API 调用没有返回结果");if(t.status===401)throw new n("web.okjike.com","即刻登录态已失效(API 返回 401),请重新登录");return t}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as A,Strategy as C}from"@jackwener/opencli/registry";import{ArgumentError as m,CommandExecutionError as l,EmptyResultError as O}from"@jackwener/opencli/errors";import{gotoJikeApiContext as E,jikeApi as f}from"./api.js";function I(t,i){if(!t||typeof t.id!=="string"||!t.id||t.targetId!==i||t.targetType!=="ORIGINAL_POST")throw new l("即刻返回了缺少 ID 或不属于目标帖子的评论");return t}async function y(t,i,n){const r=await f(t,"/1.0/comments/get?id="+encodeURIComponent(i)+"&targetType=ORIGINAL_POST");if(!r.ok)throw new l(`即刻评论查询失败(HTTP ${r.status}):${r.raw}`);const e=I(r.data?.data,n);if(e.id!==i)throw new l("即刻返回的评论 ID 与请求不一致");return e}function g(t){return{id:t.id,postId:t.targetId,threadId:t.threadId??null,author:t.user?.screenName??null,content:t.content??null,likes:t.likeCount??null,replies:t.replyCount??null,time:t.createdAt??null,level:t.level??null,status:t.status??null}}A({site:"jike",name:"comments",description:"读取即刻帖子的评论、楼中楼,或按评论回执 ID 精确回查(需登录)",access:"read",example:"ppcli jike comments 6a9f43388262cfba44f550db --limit 20 -f json",domain:"web.okjike.com",strategy:C.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"comment-id",type:"string",help:"只回查这一条评论(例如 comment 返回的 comment_id)"},{name:"thread",type:"string",help:"读取这个顶级评论 ID 下的楼中楼;与 --comment-id 互斥"},{name:"sort",type:"string",default:"time",choices:["time","likes"],help:"列表排序:time 时间 / likes 官网智能排序"},{name:"limit",type:"int",default:20,help:"列表最多返回多少条;不含各条 hotReplies 的预览"}],columns:["id","postId","threadId","author","content","likes","replies","time","level","status"],func:async(t,i)=>{const n=String(i.id??"").trim(),r=String(i["comment-id"]??"").trim(),e=String(i.thread??"").trim(),d=i.limit??20,u=i.sort??"time";if(!n)throw new m("帖子 ID 不能为空");if(r&&e)throw new m("--comment-id 与 --thread 不能同时使用");if(!Number.isSafeInteger(d)||d<1)throw new m("--limit 必须是正整数");if(!["time","likes"].includes(u))throw new m("--sort 必须是 time 或 likes");await E(t);if(r)return[g(await y(t,r,n))];if(e){const s=await y(t,e,n);if(s.level!==1||s.threadId!==e)throw new m("--thread 必须是顶级评论 ID;请使用该评论返回的 threadId")}const k=e?"/1.0/comments/list":"/1.0/comments/listPrimary",o=new Map,w=new Set;let p;while(o.size<d){const s=await f(t,k,{targetId:n,targetType:"ORIGINAL_POST",order:u==="time"?"TIME":"LIKES",limit:20,loadMoreKey:p,...e?{primaryCommentId:e,level:1}:{}}),a=s.data;if(!s.ok||!Array.isArray(a?.data))throw new l(`即刻评论列表读取失败(HTTP ${s.status}):${s.raw}`);const S=o.size;for(const T of a.data){const c=I(T,n);if(e&&c.threadId!==e)throw new l("即刻返回了其他评论串的回复");o.set(c.id,c)}if(o.size>=d||a.loadMoreKey==null||a.data.length===0)break;const h=JSON.stringify(a.loadMoreKey);if(w.has(h)||o.size===S)throw new l("即刻评论分页没有前进");w.add(h);p=a.loadMoreKey}if(!o.size)throw new O("jike comments","当前接口没有返回可见评论;不能据此断言评论从未发布");return[...o.values()].slice(0,d).map(g)}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as k,Strategy as A}from"@jackwener/opencli/registry";import{ArgumentError as l,CommandExecutionError as d}from"@jackwener/opencli/errors";import{gotoJikeApiContext as P,jikeApi as m}from"./api.js";function w(t,e){if(!t.ok||!t.data||typeof t.data!=="object"||Array.isArray(t.data)||t.data.error||t.data.code!==void 0&&t.data.code!==0)throw new d(`${e}失败(HTTP ${t.status}):${t.data?.message||t.data?.error||t.raw||"响应格式异常"}`);return t.data}function I(t,e,r,o,i){if(!t||t.id!==e||(i?t.targetId!==r||t.targetType!==o:t.type!==o))throw new d("即刻返回的目标 ID、类型或所属帖子不匹配,停止删除");return t}const g=(t)=>t.status==="DELETED"||t.deleted===!0;k({site:"jike",name:"delete",access:"write",description:"删除自己的即刻动态、转发或指定评论。默认 dry-run 核对目标;删除不可恢复,加 --execute 才执行。按 ID 校验作者与所属帖子,提交后回查,不自动重试。",domain:"web.okjike.com",strategy:A.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"动态/转发 ID;删除评论时传该评论所属的帖子 ID"},{name:"type",type:"string",choices:["post","repost"],default:"post",help:"目标帖子类型:post 原创动态,repost 转发"},{name:"comment-id",type:"string",help:"删除此评论,id 必须是评论所属的帖子;不删除帖子本身"},{name:"execute",type:"boolean",help:"实际删除;不带时只核对目标并展示内容"}],columns:["id","postId","type","status","content","url"],func:async(t,e)=>{const r=String(e.id||""),o=e["comment-id"]===void 0?null:String(e["comment-id"]),i=e.type??"post";if(!/^[a-f0-9]{24}$/.test(r)||o!==null&&!/^[a-f0-9]{24}$/.test(o))throw new l("帖子和评论 ID 必须是 24 位十六进制字符串");if(!["post","repost"].includes(i))throw new l("--type 只能是 post 或 repost");const a=o!==null,s=o??r,u=i==="post"?"ORIGINAL_POST":"REPOST",y=a?"comments":i==="post"?"originalPosts":"reposts",D=new URLSearchParams({id:s,...a?{targetType:u}:{}}),h=`/1.0/${y}/get?${D}`;await P(t);const p=w(await m(t,"/1.0/users/profile"),"读取当前账号");if(typeof p.user?.id!=="string"||!p.user.id)throw new d("即刻未返回当前账号 ID,停止删除");const c=I(w(await m(t,h),"读取删除目标").data,s,r,u,a);if(c.user?.id!==p.user.id)throw new l("只能删除当前账号自己发布的内容;目标作者与当前账号不一致");const f=(n)=>[{id:s,postId:r,type:a?"comment":i,status:n,content:c.content??"",url:`https://web.okjike.com/${i==="post"?"originalPost":"repost"}/${r}`}];if(g(c))return f("already-deleted");if(c.status!=="NORMAL")throw new d(`目标状态为 ${c.status??"未知"},未确认可删除,停止操作`);if(!e.execute)return f("dry-run");try{w(await m(t,`/1.0/${y}/remove`,{id:s,...a?{targetType:u}:{}}),"删除请求");for(let n=0;n<10;n++){const E=I(w(await m(t,h),"删除后回查").data,s,r,u,a);if(g(E))return f("deleted");if(n<9)await t.wait({time:0.5})}throw Error("回查仍未返回明确删除状态")}catch(n){throw new d(`已尝试删除 ${s},结果未确认:${n.message}。请先回查同一内容,不要直接重试删除。`)}}});
|
package/clis/jike/topic.js
CHANGED
|
@@ -1,20 +1 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
const el = document.querySelector('script[type="application/json"]');
|
|
3
|
-
if (!el) return { ok: false, reason: 'missing-data-script' };
|
|
4
|
-
try {
|
|
5
|
-
const data = JSON.parse(el.textContent || '{}');
|
|
6
|
-
const pageProps = data?.props?.pageProps || {};
|
|
7
|
-
const posts = Array.isArray(pageProps.posts) ? pageProps.posts : [];
|
|
8
|
-
return posts.map(p => ({
|
|
9
|
-
content: (p.content || '').replace(/\\n/g, ' ').slice(0, 80),
|
|
10
|
-
author: p.user?.screenName || '',
|
|
11
|
-
likes: p.likeCount || 0,
|
|
12
|
-
comments: p.commentCount || 0,
|
|
13
|
-
time: p.actionTime || p.createdAt || '',
|
|
14
|
-
id: p.id || '',
|
|
15
|
-
}));
|
|
16
|
-
} catch (e) {
|
|
17
|
-
return { ok: false, reason: 'parse-error', message: e?.message || String(e) };
|
|
18
|
-
}
|
|
19
|
-
})()
|
|
20
|
-
`);if(Array.isArray(e)){if(e.length===0)throw new n("jike topic",`No posts were returned for topic ${o.id}. Confirm the topic ID and login state.`);return e.slice(0,s).map((t)=>({content:t.content??"",author:t.author??"",likes:t.likes??0,comments:t.comments??0,time:t.time??"",url:`https://web.okjike.com/originalPost/${t.id??""}`}))}if(e?.reason==="missing-data-script")throw new r("Jike topic page did not expose the expected data script");if(e?.reason==="parse-error")throw new r(`Failed to parse Jike topic data: ${e.message||"unknown error"}`);throw new r("Jike topic returned an unreadable payload")}});
|
|
1
|
+
import{cli as y,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as h,CommandExecutionError as a,EmptyResultError as k}from"@jackwener/opencli/errors";import{gotoJikeApiContext as g,jikeApi as u}from"./api.js";y({site:"jike",name:"topic",access:"read",description:"即刻话题/圈子帖子(登录后按游标分页)",domain:"web.okjike.com",strategy:b.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"Topic ID (from topic URL, e.g. 553870e8e4b0cafb0a1bef68)"},{name:"limit",type:"int",default:20,help:"最多返回多少条帖子"}],columns:["content","author","likes","comments","time","url","id"],func:async(c,d)=>{const o=d.limit??20;if(!Number.isSafeInteger(o)||o<1)throw new h("--limit 必须是正整数");await g(c);const n=await u(c,"/1.0/topics/getDetail?id="+encodeURIComponent(d.id)),r=n.data?.data;if(!n.ok||!r||!Array.isArray(r.tabs))throw new a("即刻话题信息读取失败:"+n.status+" / "+n.raw);const m=r.tabs.find((e)=>e.type==="square")??r.tabs.find((e)=>e.type===r.entryTab);if(!m||typeof m.type!=="string")throw new a("即刻话题未提供可读取的帖子栏目");const t=new Map,l=new Set;let f;while(t.size<o){const e=await u(c,"/1.0/topics/tabs/"+encodeURIComponent(m.type)+"/feed",{limit:20,loadMoreKey:f,topicId:d.id}),i=e.data;if(!e.ok||!Array.isArray(i?.data))throw new a("即刻话题帖子读取失败:"+e.status+" / "+e.raw);const w=t.size;for(const s of i.data){if(typeof s.id!=="string"||!s.id)throw new a("即刻话题帖子缺少 id");t.set(s.id,s)}if(t.size>=o||!i.loadMoreKey||i.data.length===0)break;const p=JSON.stringify(i.loadMoreKey);if(l.has(p)||t.size===w)throw new a("即刻话题分页没有前进");l.add(p);f=i.loadMoreKey}if(t.size===0)throw new k("jike topic","该圈子没有返回帖子");return[...t.values()].slice(0,o).map((e)=>({id:e.id,content:(e.content||"").replace(/\n/g," ").slice(0,80),author:e.user?.screenName||"",likes:e.likeCount??0,comments:e.commentCount??0,time:e.actionTime||e.createdAt||"",url:"https://web.okjike.com/originalPost/"+e.id}))}});
|
package/clis/kuaishou/auth.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{AuthRequiredError as i,TimeoutError as r}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as a}from"../_shared/site-auth.js";import{currentUrl as n,unwrap as c}from"../_shared/video-publish.js";const l="https://cp.kuaishou.com/article/publish/video";async function s(t){return(await t.getCookies({url:"https://cp.kuaishou.com"})).some((e)=>e.value&&(/_st$/.test(e.name)||e.name==="userId"||e.name==="kuaishou.web.cp.api_st"))}export async function verifyKuaishouIdentity(t){await t.goto(l);let o="";for(let e=0;e<30;e++){o=await n(t);if(o&&(/passport\.kuaishou\.com/.test(o)||/\/account\/login/.test(o)))throw new i("cp.kuaishou.com","快手创作者平台需要登录");if(/^https:\/\/cp\.kuaishou\.com\/article\//.test(o)){const u=c(await t.evaluate(`(() => {
|
|
2
|
+
const visible = el => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
|
|
3
|
+
const login = Array.from(document.querySelectorAll('a,button')).some(el =>
|
|
4
|
+
visible(el) && (el.textContent || '').trim() === '立即登录');
|
|
5
|
+
const uploadButton = document.querySelector("button[class^='_upload-btn']");
|
|
6
|
+
const uploader = visible(uploadButton) && !!document.querySelector('input[type="file"]');
|
|
7
|
+
return { login, uploader };
|
|
8
|
+
})()`));if(u?.login)throw new i("cp.kuaishou.com","快手上传页显示“立即登录”,当前会话未登录");if(u?.uploader)return{logged_in:!0}}await t.wait({time:0.5})}throw new r("快手登录态校验",15,`上传表单尚未出现,无法确认登录态(当前: ${o||"unknown"});请检查页面加载状态。`)}a({site:"kuaishou",domain:"cp.kuaishou.com",loginUrl:"https://passport.kuaishou.com/pc/account/login/?sid=kuaishou.web.cp.api&callback=https%3A%2F%2Fcp.kuaishou.com%2Frest%2Finfra%2Fsts%3FfollowUrl%3Dhttps%253A%252F%252Fcp.kuaishou.com%252Farticle%252Fpublish%252Fvideo%26setRootDomain%3Dtrue",columns:["logged_in"],quickCheck:s,verify:verifyKuaishouIdentity,poll:async(t)=>{if(!await s(t))throw new i("cp.kuaishou.com","等待快手登录 cookie");return verifyKuaishouIdentity(t)}});
|
package/clis/kuaishou/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as y,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as x,CommandExecutionError as l}from"@jackwener/opencli/errors";import{verifyKuaishouIdentity as E}from"./auth.js";import{resolveVideoFile as D,resolveImageFile as T,setVideoInput as w,unwrap as s,currentUrl as k,parseScheduleDate as _,clickByText as r,countByText as m,existsByText as B,waitForUrlIncludes as I,waitForSelector as f,videoTimeoutArg as O}from"../_shared/video-publish.js";const q="button[class^='_upload-btn']",b=["input[class^='upload-btn-input']","div.progress-div [class^='upload-btn-input']","button[class^='_upload-btn'] input[type='file']","input[type='file']"];async function C(t,e){if(!Boolean(s(await t.evaluate(`
|
|
2
2
|
(() => {
|
|
3
3
|
const norm = (v) => (v || '').replace(/\\s+/g, ' ').trim();
|
|
4
4
|
let label = null;
|
|
@@ -17,7 +17,7 @@ import{cli as S,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
17
17
|
editable.focus();
|
|
18
18
|
return true;
|
|
19
19
|
})()
|
|
20
|
-
`))))throw new
|
|
20
|
+
`))))throw new l("找不到快手「描述」输入框");try{await t.pressKey("Backspace")}catch{}try{await t.pressKey("Control+a")}catch{}try{await t.pressKey("Delete")}catch{}await t.insertText(e);await t.pressKey("Enter")}async function K(t,e){const i=(u)=>String(u).padStart(2,"0"),a=`${e.getFullYear()}-${i(e.getMonth()+1)}-${i(e.getDate())} ${i(e.getHours())}:${i(e.getMinutes())}:${i(e.getSeconds())}`;await r(t,"定时发布",{timeoutMs:5000});await t.wait({time:2});await t.click('input[placeholder="选择日期时间"]');await t.wait({time:1});if(!Boolean(s(await t.evaluate(`
|
|
21
21
|
((newValue) => {
|
|
22
22
|
const input = document.querySelector('input[placeholder="选择日期时间"]');
|
|
23
23
|
if (!input) return false;
|
|
@@ -26,8 +26,8 @@ import{cli as S,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
26
26
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
27
27
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
28
28
|
return true;
|
|
29
|
-
})(${JSON.stringify(
|
|
30
|
-
`))))throw new
|
|
29
|
+
})(${JSON.stringify(a)})
|
|
30
|
+
`))))throw new l("找不到快手定时发布时间选择器输入框");await t.wait({time:1});await t.pressKey("Enter");await t.wait({time:2})}async function V(t,e){if(!e)return;if(!Boolean(s(await t.evaluate(`
|
|
31
31
|
(() => {
|
|
32
32
|
const norm = (v) => (v || '').replace(/\\s+/g, ' ').trim();
|
|
33
33
|
let label = null;
|
|
@@ -42,7 +42,7 @@ import{cli as S,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
42
42
|
first.click();
|
|
43
43
|
return true;
|
|
44
44
|
})()
|
|
45
|
-
`))))throw new
|
|
45
|
+
`))))throw new l("找不到快手「封面设置」入口");if(!await f(t,'div[role="document"].ant-modal',30000))throw new l("封面设置弹窗未出现");await r(t,"上传封面",{exact:!0,timeoutMs:1e4});await t.wait({time:0.5});await w(t,['div[role="document"].ant-modal input[type="file"]','input[type="file"]'],[e]);await t.wait({time:1});await r(t,"确认",{exact:!0,timeoutMs:1e4});const a=Date.now()+30000;while(Date.now()<a){if(!Boolean(s(await t.evaluate(`(() => { const m = document.querySelector('div[role="document"].ant-modal'); return !!(m && m.getBoundingClientRect().width > 0); })()`))))break;await t.wait({time:0.5})}}y({site:"kuaishou",name:"publish",access:"write",description:"发布视频到快手(一条命令完成上传+填写+发布;可选封面/定时/话题)",domain:"cp.kuaishou.com",strategy:S.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"作品标题(无 --desc 时作为描述)"},{name:"desc",fromFile:!0,default:"",help:"视频描述(默认用标题)"},{name:"tags",default:"",help:"话题标签,逗号分隔,最多取前 3 个(不含 #)"},{name:"cover",default:"",help:"封面图片路径(可选)"},{name:"schedule",default:"",help:"定时发布时间(ISO8601 或 Unix 秒;不填即立即发布)"},O()],columns:["status","title","url"],func:async(t,e)=>{const i=String(e.title??"").trim();if(!i)throw new x("快手视频上传时,title 是必须的");const a=D(e.video),o=e.cover?T(e.cover):"",u=String(e.desc||"").trim()||i,p=e.tags?String(e.tags).split(",").map((n)=>n.trim()).filter(Boolean):[],c=_(e.schedule);await E(t);if(!await f(t,q,15000))throw new l("快手上传按钮未出现");await w(t,b,[a]);await t.wait({time:2});if(await B(t,"我知道了",{exact:!0}))await r(t,"我知道了",{exact:!0});await t.evaluate(`
|
|
46
46
|
(() => {
|
|
47
47
|
const tip = document.querySelector('div[id^="react-joyride-step"] div[role="alertdialog"]');
|
|
48
48
|
if (!tip) return false;
|
|
@@ -50,4 +50,4 @@ import{cli as S,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
50
50
|
if (skip) { skip.click(); return true; }
|
|
51
51
|
return false;
|
|
52
52
|
})()
|
|
53
|
-
`);await C(t,
|
|
53
|
+
`);await C(t,u);for(const n of p.slice(0,3)){await t.insertText(`#${n} `);await t.wait({time:2})}const h=60;for(let n=0;n<h;n++){if(await m(t,"上传中")===0)break;if(await m(t,"上传失败")>0)await w(t,b,[a]);await t.wait({time:2})}if(o)await V(t,o);if(c)await K(t,c);const v=Date.now()+60000;let d=!1;while(Date.now()<v){await r(t,"发布",{exact:!0});await t.wait({time:1});await r(t,"确认发布");if(await I(t,"article/manage",5000)){d=!0;break}await t.wait({time:1})}if(!d)throw new l("快手发布未确认成功(未跳转到作品管理页),请检查页面状态");return[{status:c?"✅ 定时发布已提交":"✅ 发布成功",title:i,url:await k(t)}]}});
|
package/clis/nowcoder/comment.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as a,Strategy as c}from"@jackwener/opencli/registry";import{ArgumentError as d}from"@jackwener/opencli/errors";import{WWW_BASE as u,postForm as l,requireOk as p,resolvePostTarget as y,resolveReplyTarget as f}from"./utils.js";import{assertLiteralContent as w}from"../_shared/content-guard.js";a({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:c.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",fromFile:!0,positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status"],func:async(n,e)=>{const r=await y(n,e.id),o=String(e.content??"").trim();w(o,{label:"评论内容"});if(!o)throw new d("评论内容不能为空");const m=String(e["to-user"]??"0").trim()||"0",t=String(e["to-comment"]??"0").trim()||"0",i=t==="0"?r:await f(n,r,t,m),s=i.authorId||m;p(await l(n,`${u}/comment/create?_=${Date.now()}`,{entityType:String(i.entityType),entityId:i.id,commentContent:o,contentV2:JSON.stringify({pureText:o,imgs:[]}),toFeed:"false",toUserId:s,toCommentId:t,entityOwnerId:s,isAnonymousFlag:"false"}),"评论");return[{post_id:r.id,kind:t!=="0"?`reply-to-${t}`:"comment",status:"ok"}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as a,Strategy as d}from"@jackwener/opencli/registry";import{GW_BASE as m,decodeHtmlEntities as c,getJson as s,requireOk as p,resolvePostTarget as u}from"./utils.js";a({site:"nowcoder",name:"comments",access:"read",description:"某帖的评论列表(评论前先读,避免重复/不合语境)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"page",type:"int",default:1,help:"页码"}],columns:["comment_id","user_id","nickname","content"],func:async(t,n)=>{const{id:o,entityType:r}=await u(t,n.id),i=`${m}/comment/list-by-page?entityId=${encodeURIComponent(o)}&entityType=${r}&order=2&pageNo=${n.page}&toCommentId=0&_=${Date.now()}`;return(p(await s(t,i),"评论列表").data?.records??[]).map((e)=>({comment_id:e.id??"",user_id:e.userBrief?.userId??"",nickname:e.userBrief?.nickname??"",content:c(e.content).trim()}))}});
|
package/clis/nowcoder/detail.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"detail",access:"read",description:"Post detail view (supports ID / UUID / URL)",domain:"www.nowcoder.com",args:[{name:"id",positional:!0,required:!0,help:"Post ID, UUID, or URL"}],columns:["title","author","school","content","likes","comments","views","time","location"],pipeline:[{navigate:"https://www.nowcoder.com"},{evaluate:`(async () => {
|
|
2
2
|
const raw = \${{ args.id | json }};
|
|
3
3
|
const base = 'https://gw-c.nowcoder.com';
|
|
4
|
-
|
|
4
|
+
// 牛客把正文里的半角引号按 HTML 实体存库,网页端渲染时才反转义;这里一并解回来,
|
|
5
|
+
// 否则调用方读到的是 "。& 必须放最后,不然 &quot; 会被二次解码成引号。
|
|
6
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/'/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
5
7
|
|
|
6
8
|
let id = raw;
|
|
7
9
|
const urlMatch = raw.match(/discuss\\/(\\d+)/);
|
|
@@ -32,7 +34,7 @@ import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"detai
|
|
|
32
34
|
const user = data.userBrief || {};
|
|
33
35
|
const freq = data.frequencyData || {};
|
|
34
36
|
return [{
|
|
35
|
-
title: data.title || '(untitled)',
|
|
37
|
+
title: strip(data.title || '') || '(untitled)',
|
|
36
38
|
author: user.nickname || '',
|
|
37
39
|
school: user.educationInfo || '',
|
|
38
40
|
content: strip(data.content || '').substring(0, 500),
|
package/clis/nowcoder/dm-list.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{WWW_BASE as i,
|
|
1
|
+
import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{WWW_BASE as i,decodeHtmlEntities as c,getJson as d,getSelfUid as l,requireOk as g}from"./utils.js";o({site:"nowcoder",name:"dm-list",access:"read",description:"私信会话列表(conversationId / 最后一条消息 / 未读数)",domain:"www.nowcoder.com",strategy:r.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["conversation_id","target_user_id","target_name","last_content","unread","allow_send","is_official"],func:async(t,a)=>{const n=await l(t),s=`${i}/sns/message/${n}/get-conversation-list?page=${a.page}`;return(g(await d(t,s),"私信会话列表").data?.conversations??[]).map((e)=>({conversation_id:e.conversationId??"",target_user_id:e.targetUserId??"",target_name:e.targetUserName??"",last_content:c(e.content).replace(/\s+/g," ").slice(0,80),unread:e.unreadMessageCount??0,allow_send:e.allowSendMessage??"",is_official:e.targetIsOfficialAccount??""}))}});
|
package/clis/nowcoder/dm-read.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as s,Strategy as d}from"@jackwener/opencli/registry";import{WWW_BASE as c,decodeHtmlEntities as g,getJson as m,getSelfUid as p,requireOk as l,resolveConversation as u}from"./utils.js";const _={0:"text",1:"image"};s({site:"nowcoder",name:"dm-read",access:"read",description:"读某私信会话的消息(target 可填 dm-list 的 conversation_id 或 target_user_id)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"target",positional:!0,required:!0,help:"dm-list 的 conversation_id,或对方 userId"},{name:"page",type:"int",default:1,help:"页码"},{name:"page-size",type:"int",default:15,help:"每页条数"}],columns:["message_id","direction","from_user_id","type","content","created_at"],func:async(n,a)=>{const t=await p(n),{conversationId:i}=await u(n,a.target,t),r=`${c}/sns/message/${t}/get-msg-list?conversationId=${encodeURIComponent(i)}&ownerId=${t}&page=${a.page}&pageSize=${a["page-size"]}&_=${Date.now()}`;return((l(await m(n,r),"私信读取").data??{}).messages??[]).map((e)=>{const o=String(e.send??"");return{message_id:e.id??"",direction:o===String(t)?"out":"in",from_user_id:o,type:_[e.contentType]??String(e.contentType??""),content:g(e.content).trim(),created_at:e.createDate?new Date(e.createDate).toISOString():""}})}});
|
package/clis/nowcoder/dm-send.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as o,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as a}from"@jackwener/opencli/errors";import{WWW_BASE as m,getSelfUid as d,postForm as c,requireOk as l,resolveConversation as p}from"./utils.js";import{assertLiteralContent as u}from"../_shared/content-guard.js";o({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(target 可填对方 userId 或 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:n.COOKIE,args:[{name:"target",positional:!0,required:!0,help:"对方 userId(dm-list 的 target_user_id),也接受 conversation_id"},{name:"content",fromFile:!0,positional:!0,required:!0,help:"私信内容"}],columns:["message_id","receiver","status"],func:async(e,i)=>{const t=String(i.content??"").trim();u(t,{label:"私信内容"});if(!t)throw new a("私信内容不能为空");const r=String(i.target??"").trim(),s=/^\d+$/.test(r)?r:(await p(e,r,await d(e))).peerUid;return[{message_id:l(await c(e,`${m}/sns/message/send-msg?_=${Date.now()}`,{type:"0",title:"",content:t,images:"",receiver:s},{Accept:"application/json, text/plain, */*"}),"私信发送").data?.messageId??"",receiver:s,status:"sent"}]}});
|
package/clis/nowcoder/drafts.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as c,Strategy as n}from"@jackwener/opencli/registry";import{GW_BASE as s,decodeHtmlEntities as a,getJson as d,getSelfUid as l,requireOk as p}from"./utils.js";c({site:"nowcoder",name:"drafts",access:"read",description:"我的草稿列表",domain:"www.nowcoder.com",strategy:n.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["draft_id","title","content"],func:async(e,r)=>{const o=await l(e),i=`${s}/discuss-draft/list?authorId=${o}&pageNo=${r.page}&pageSize=10&entityType=250`;return(p(await d(e,i),"草稿列表").data?.records??[]).map((t)=>({draft_id:t.id??"",title:a(t.title).trim(),content:a(t.content).replace(/\s+/g," ").slice(0,120)}))}});
|
|
@@ -4,12 +4,14 @@ import{cli as e}from"@jackwener/opencli/registry";e({site:"nowcoder",name:"exper
|
|
|
4
4
|
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/home/tab/content?tabId=818&categoryType=1&pageNo=' + page + '&pageSize=' + limit, {credentials: 'include'});
|
|
5
5
|
const d = await r.json();
|
|
6
6
|
if (!d.success) throw new Error(d.msg || 'API failed');
|
|
7
|
+
// 牛客正文里的引号按 HTML 实体存库,网页端渲染才反转义;& 放最后避免二次解码。
|
|
8
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
7
9
|
return (d.data?.records || []).map((item, i) => ({
|
|
8
10
|
rank: i + 1,
|
|
9
|
-
title: item.contentData?.title || item.momentData?.title || '',
|
|
11
|
+
title: strip(item.contentData?.title || item.momentData?.title || ''),
|
|
10
12
|
author: item.userBrief?.nickname || '',
|
|
11
13
|
school: item.userBrief?.educationInfo || '',
|
|
12
|
-
content: item.momentData?.content || item.contentData?.content || item.momentData?.newContent || item.contentData?.newContent || '',
|
|
14
|
+
content: strip(item.momentData?.content || item.contentData?.content || item.momentData?.newContent || item.contentData?.newContent || ''),
|
|
13
15
|
likes: item.frequencyData?.likeCnt || 0,
|
|
14
16
|
comments: item.frequencyData?.commentCnt || 0,
|
|
15
17
|
views: item.frequencyData?.viewCnt || 0,
|
package/clis/nowcoder/inbox.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as d,Strategy as u}from"@jackwener/opencli/registry";import{ArgumentError as g,CommandExecutionError as i}from"@jackwener/opencli/errors";import{GW_BASE as f,decodeHtmlEntities as l,getJson as p,requireOk as _}from"./utils.js";d({site:"nowcoder",name:"inbox",access:"read",description:"评论回复收件箱(谁回复/@了我们,含定向回复所需的评论 id)",domain:"www.nowcoder.com",strategy:u.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["msg_id","from_user_id","from_nickname","reply_content","reply_comment_id","quoted_ours","url","time"],func:async(a,s)=>{const n=s.page;if(!Number.isSafeInteger(n)||n<1)throw new g("--page 必须是正整数");let o,t;for(let e=1;e<=n;e++){const c=o===void 0?"":`?startIndex=${encodeURIComponent(o)}`,m=_(await p(a,`${f}/message/pc/comment/list${c}`),"评论收件箱");t=m.data?.records;if(!Array.isArray(t))throw new i("牛客评论收件箱未返回 records 数组");if(e===n)break;if(t.length===0||m.data.hasMore===!1)return[];const r=t[t.length-1].id;if(!(typeof r==="string"&&r.length>0||Number.isSafeInteger(r)))throw new i("牛客评论收件箱未返回精确的分页游标");if(String(r)===o)throw new i("牛客评论收件箱分页游标没有前进");o=String(r)}return t.map((e)=>({msg_id:e.id??"",from_user_id:e.user?.userId??"",from_nickname:e.user?.nickname??"",reply_content:l(e.triggerEntity?.content).trim(),reply_comment_id:e.triggerEntity?.entityId??"",quoted_ours:(e.quote?.content??"").trim(),url:e.pcUrl??"",time:e.createTime??""}))}});
|
package/clis/nowcoder/like.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as s,Strategy as n}from"@jackwener/opencli/registry";import{WWW_BASE as a,postMultipart as c,requireOk as d,resolvePostTarget as l}from"./utils.js";s({site:"nowcoder",name:"like",access:"write",description:"点赞帖子(toggle,再次调用取消;有频控,务必低频)",domain:"www.nowcoder.com",strategy:n.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"}],columns:["post_id","status","like_count"],func:async(t,o)=>{const{id:e,entityType:i}=await l(t,o.id),r=d(await c(t,`${a}/sns/like?_=${Date.now()}`,{id:e,type:String(i)}),"点赞");return[{post_id:e,status:"toggled",like_count:r.count??""}]}});
|
package/clis/nowcoder/my.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as d,Strategy as m}from"@jackwener/opencli/registry";import{GW_BASE as l,decodeHtmlEntities as a,getJson as w,getSelfUid as u,requireOk as p}from"./utils.js";d({site:"nowcoder",name:"my",access:"read",description:"我发布的内容列表(动态 + 长文,含可供 delete 使用的 id)",domain:"www.nowcoder.com",strategy:m.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["content_id","kind","title","content","url","views","likes","comments","created_at"],func:async(c,i)=>{const r=await u(c),s=`${l}/user/content/my-content?pageNo=${i.page}&userId=${r}&_=${Date.now()}`;return(p(await w(c,s),"我的内容").data?.records??[]).map((n)=>{const t=n.contentData??{},e=n.frequencyData??{},o=String(t.id??n.contentId??"");return{content_id:o,kind:t.typeName??"",title:a(t.title).trim(),content:a(t.content).replace(/\s+/g," ").slice(0,120),url:o?`https://www.nowcoder.com/discuss/${o}`:"",views:e.viewCnt??0,likes:e.likeCnt??0,comments:e.totalCommentCnt??e.commentCnt??0,created_at:t.createTime?new Date(t.createTime).toISOString():""}})}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as t,Strategy as e}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"recommend",access:"read",description:"Recommended feed",domain:"www.nowcoder.com",strategy:e.PUBLIC,browser:!1,args:[{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:15,help:"Number of items"}],columns:["rank","title","author","likes","comments","views","content","id","momentId","userId","entityType"],pipeline:[{fetch:{url:"https://gw-c.nowcoder.com/api/sparta/home/recommend?page=${{ args.page }}&size=${{ args.limit }}"}},{select:"data.records"},{map:{rank:"${{ index + 1 }}",title:"${{ item.momentData?.title || item.longContentData?.title || item.contentData?.title || '' | text }}",author:"${{ item.userBrief?.nickname || '' }}",likes:"${{ item.frequencyData?.likeCnt || 0 }}",comments:"${{ item.frequencyData?.commentCnt || 0 }}",views:"${{ item.frequencyData?.viewCnt || 0 }}",content:"${{ item.momentData?.content || item.longContentData?.content || item.contentData?.content || '' | text }}",id:"${{ item.contentData?.id || item.momentData?.uuid || item.longContentData?.uuid || item.contentId || '' }}",momentId:"${{ item.contentData?.id || item.momentData?.id || item.longContentData?.id || item.contentId || '' }}",userId:"${{ item.userBrief?.userId || '' }}",entityType:"${{ item.contentType }}"}},{filter:"item.title"},{limit:"${{ args.limit }}"}]});
|
package/clis/nowcoder/search.js
CHANGED
|
@@ -2,7 +2,8 @@ import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"searc
|
|
|
2
2
|
const query = \${{ args.query | json }};
|
|
3
3
|
const type = \${{ args.type | json }};
|
|
4
4
|
const limit = \${{ args.limit }};
|
|
5
|
-
|
|
5
|
+
// " 也要解(牛客把正文引号转义存库);& 挪到最后,否则 &quot; 会被二次解码。
|
|
6
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/'/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
6
7
|
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/pc/search', {
|
|
7
8
|
method: 'POST',
|
|
8
9
|
credentials: 'include',
|
|
@@ -20,7 +21,7 @@ import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"searc
|
|
|
20
21
|
const id = data.contentId || '';
|
|
21
22
|
return {
|
|
22
23
|
rank: i + 1,
|
|
23
|
-
title: moment.title || contentData.title || user.nickname || '',
|
|
24
|
+
title: strip(moment.title || contentData.title || user.nickname || ''),
|
|
24
25
|
author: user.nickname || '',
|
|
25
26
|
school: user.educationInfo || '',
|
|
26
27
|
content: strip(moment.content || contentData.content || ''),
|
package/clis/nowcoder/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{ArgumentError as u,AuthRequiredError as h,CommandExecutionError as c}from"@jackwener/opencli/errors";export const GW_BASE="https://gw-c.nowcoder.com/api/sparta",WWW_BASE="https://www.nowcoder.com";export async function getJson(e,r){const t=JSON.stringify(r);return e.evaluate(`
|
|
2
2
|
async () => {
|
|
3
|
-
const res = await fetch(${
|
|
3
|
+
const res = await fetch(${t}, { credentials: 'include' });
|
|
4
4
|
const text = await res.text();
|
|
5
5
|
try {
|
|
6
6
|
return JSON.parse(text);
|
|
@@ -8,12 +8,12 @@ import{AuthRequiredError as a,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
8
8
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
`)}export async function postJson(
|
|
11
|
+
`)}export async function postJson(e,r,t,n={}){const s=JSON.stringify(r),o=JSON.stringify(JSON.stringify(t)),i=JSON.stringify({"Content-Type":"application/json",...n});return e.evaluate(`
|
|
12
12
|
async () => {
|
|
13
|
-
const res = await fetch(${
|
|
13
|
+
const res = await fetch(${s}, {
|
|
14
14
|
method: 'POST',
|
|
15
15
|
credentials: 'include',
|
|
16
|
-
headers: ${
|
|
16
|
+
headers: ${i},
|
|
17
17
|
body: ${o},
|
|
18
18
|
});
|
|
19
19
|
const text = await res.text();
|
|
@@ -23,16 +23,13 @@ import{AuthRequiredError as a,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
23
23
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
`)}export async function postForm(t,
|
|
26
|
+
`)}export async function postForm(e,r,t,n={}){const s=JSON.stringify(r),o=JSON.stringify(t),i=JSON.stringify({"Content-Type":"application/x-www-form-urlencoded","x-requested-with":"XMLHttpRequest",...n});return e.evaluate(`
|
|
27
27
|
async () => {
|
|
28
|
-
const body = new URLSearchParams(${
|
|
29
|
-
const res = await fetch(${
|
|
28
|
+
const body = new URLSearchParams(${o});
|
|
29
|
+
const res = await fetch(${s}, {
|
|
30
30
|
method: 'POST',
|
|
31
31
|
credentials: 'include',
|
|
32
|
-
headers: {
|
|
33
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
34
|
-
'x-requested-with': 'XMLHttpRequest',
|
|
35
|
-
},
|
|
32
|
+
headers: ${i},
|
|
36
33
|
body: body.toString(),
|
|
37
34
|
});
|
|
38
35
|
const text = await res.text();
|
|
@@ -42,14 +39,14 @@ import{AuthRequiredError as a,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
42
39
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
|
-
`)}export async function postMultipart(
|
|
42
|
+
`)}export async function postMultipart(e,r,t,n={}){const s=JSON.stringify(r),o=JSON.stringify(t),i=JSON.stringify(n);return e.evaluate(`
|
|
46
43
|
async () => {
|
|
47
44
|
const fd = new FormData();
|
|
48
45
|
for (const [k, v] of Object.entries(${o})) fd.append(k, v);
|
|
49
|
-
const res = await fetch(${
|
|
46
|
+
const res = await fetch(${s}, {
|
|
50
47
|
method: 'POST',
|
|
51
48
|
credentials: 'include',
|
|
52
|
-
headers: ${
|
|
49
|
+
headers: ${i},
|
|
53
50
|
body: fd,
|
|
54
51
|
});
|
|
55
52
|
const text = await res.text();
|
|
@@ -59,11 +56,11 @@ import{AuthRequiredError as a,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
59
56
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
|
-
`)}export function requireOk(
|
|
59
|
+
`)}export function requireOk(e,r){if(!e||typeof e!=="object"||Array.isArray(e))throw new c(`牛客 ${r} 接口返回了畸形响应`);if(Object.hasOwn(e,"success")?!!e.success:Object.hasOwn(e,"code")?e.code===0:Object.hasOwn(e,"data"))return e;const n=String(e.msg??e.message??"未知错误");if(/登录|登陆|未授权|token|身份/i.test(n))throw new h("nowcoder.com",`牛客 ${r} 需要登录: ${n}`);throw new c(`牛客 ${r} 失败: ${n} (code=${e.code??"?"})`)}export async function resolveMomentId(e,r){let t=String(r??"").trim();for(const o of[/detail\/([0-9a-fA-F]{20,})/,/discuss\/(\d+)/]){const i=t.match(o);if(i){t=i[1];break}}if(/^\d+$/.test(t))return t;if(!/^[0-9a-fA-F]{20,}$/.test(t))throw new c(`无法从输入解析牛客帖子 id: ${r}`);const s=requireOk(await getJson(e,`${GW_BASE}/detail/moment-data/detail/${t}`),"帖子解析").data?.id;if(!s)throw new c(`牛客帖子 ${t} 的详情缺少数字 momentId`);return String(s)}export async function resolvePostTarget(e,r){if(typeof r==="number"&&!Number.isSafeInteger(r))throw new u("牛客帖子 ID 已丢失精度,请传推荐结果中的原始字符串 ID");const t=String(r??"").trim(),n=t.match(/\/discuss\/(\d+)/)?.[1]??t;if(/^\d+$/.test(n)&&BigInt(n)>BigInt(Number.MAX_SAFE_INTEGER)){const o=requireOk(await getJson(e,`${GW_BASE}/detail/content-data/detail/${n}`),"讨论帖解析").data;if(typeof o?.id!=="string"||o.id!==n)throw new c("牛客讨论帖详情未返回匹配的原始字符串 ID");const i=String(o.authorId??o.userBrief?.userId??"");if(!/^[1-9]\d*$/.test(i))throw new c("牛客讨论帖详情缺少作者 ID");return{id:n,entityType:250,authorId:i}}return{id:await resolveMomentId(e,r),entityType:74,authorId:""}}export async function resolveReplyTarget(e,r,t,n){if(!/^\d+$/.test(t))throw new u("--to-comment 必须是评论 ID");const o=requireOk(await getJson(e,`${GW_BASE}/detail/comment-terminal/${t}`),"回复目标").data;if(!o||String(o.id)!==t||!o.authorId)throw new c("牛客回复目标缺少评论 ID 或作者信息");const i=String(o.rootLongId??o.rootId??o.entryId??""),a=Number(o.rootType??o.entryType);if(i!==r.id||a!==r.entityType)throw new u("目标评论不属于指定帖子,已停止回复");const d=String(o.authorId);if(n!=="0"&&n!==d)throw new u("--to-user 与目标评论的实际作者不一致");const f=Number(o.entryType)===2?String(o.entryId):t;if(!/^\d+$/.test(f))throw new c("牛客回复目标缺少父评论 ID");return{id:f,entityType:2,authorId:d}}export async function getSelfUid(e){const r=await e.evaluate(`
|
|
63
60
|
async () => {
|
|
64
61
|
const link = document.querySelector('a[href*="/users/"]');
|
|
65
62
|
const href = link ? (link.getAttribute('href') || '') : '';
|
|
66
63
|
const match = href.match(/\\/users\\/(\\d+)/);
|
|
67
64
|
return match ? match[1] : '';
|
|
68
65
|
}
|
|
69
|
-
`);if(!
|
|
66
|
+
`);if(!r)throw new h("nowcoder.com","导航栏无登录用户链接(未登录)");return r}export function peerUidFromConversationId(e,r){const t=String(e??"").split("_");if(t.length!==2||!t.every((n)=>/^\d+$/.test(n)))return"";if(t[0]===t[1])return t[0];return t.find((n)=>n!==String(r))??""}export async function resolveConversation(e,r,t){const n=String(r??"").trim();if(!n)throw new c("缺少会话标识");if(n.includes("_")){const s=peerUidFromConversationId(n,t);if(!s)throw new c(`无法解析牛客会话 id: ${r}(应形如 <uidA>_<uidB>)`);return{conversationId:n,peerUid:s}}if(!/^\d+$/.test(n))throw new c(`无法解析牛客私信标识: ${r}(应为对方 userId 或 dm-list 的 conversation_id)`);for(let s=1;s<=5;s+=1){const i=requireOk(await getJson(e,`${WWW_BASE}/sns/message/${t}/get-conversation-list?page=${s}&_=${Date.now()}`),"私信会话列表").data?.conversations??[];if(!i.length)break;const a=i.find((d)=>String(d?.targetUserId??"")===n);if(a?.conversationId)return{conversationId:String(a.conversationId),peerUid:n}}throw new c(`在前 5 页会话列表里没找到与用户 ${n} 的会话(对方从未与你私信过时会话尚不存在,先用 dm-send 发起)`)}export function decodeHtmlEntities(e){return String(e??"").replace(/"/g,'"').replace(/�?39;/g,"'").replace(/'/g,"'").replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/&#(\d+);/g,(r,t)=>String.fromCodePoint(Number(t))).replace(/&/g,"&")}
|
package/clis/nowcoder/watch.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{registerWatch as d}from"../_shared/watch.js";import{WWW_BASE as w,
|
|
1
|
+
import{registerWatch as d}from"../_shared/watch.js";import{WWW_BASE as w,decodeHtmlEntities as m,getJson as g,getSelfUid as c,requireOk as l}from"./utils.js";function u(t,e){const r=t?.data?.conversations;if(!Array.isArray(r))return[];const i=[];for(const n of r){const a=Number(n?.unreadMessageCount??0);if(a<=0&&!e?.includeOwn)continue;const s=String(n?.conversationId??""),o=Number(n?.createDate??0);if(!s||!o)continue;i.push({event:"dm",id:`${s}:${o}`,ts:o,conversation_id:s,sender:{id:String(n?.targetUserId??""),name:String(n?.targetUserName??"")},text:m(n?.content).trim(),url:"https://www.nowcoder.com/users/notice/privateChat",raw:{unread_count:a,message_count:n?.messageCount??0}})}return i}d({site:"nowcoder",name:"watch",domain:"www.nowcoder.com",mode:"reconcile",description:"牛客私信监听:按间隔拉取会话列表,有未读私信按 NDJSON 逐条输出",watchUrl:"https://www.nowcoder.com/",pattern:"/sns/message/",parse:(t,e)=>{if(!String(t?.url??"").includes("get-conversation-list"))return null;try{return u(JSON.parse(String(t.responsePreview??"")),e)}catch{return null}},onAttach:async(t,e)=>{e.state.uid=await c(t)},reconcile:async(t,e)=>{if(!e.state.uid)e.state.uid=await c(t);const r=`${w}/sns/message/${e.state.uid}/get-conversation-list?page=1`;return u(l(await g(t,r),"私信会话列表"),e)}});
|