publishport-opencli 1.0.27 → 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 +280 -8
- 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/inbox.js +1 -1
- package/clis/nowcoder/like.js +1 -1
- package/clis/nowcoder/recommend.js +1 -1
- package/clis/nowcoder/utils.js +13 -13
- 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/package.json +1 -1
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/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??""}]}});
|
|
@@ -1 +1 @@
|
|
|
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","id"],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.
|
|
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/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,13 +8,13 @@ import{AuthRequiredError as d,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(t,n
|
|
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
13
|
const res = await fetch(${s}, {
|
|
14
14
|
method: 'POST',
|
|
15
15
|
credentials: 'include',
|
|
16
|
-
headers: ${
|
|
17
|
-
body: ${
|
|
16
|
+
headers: ${i},
|
|
17
|
+
body: ${o},
|
|
18
18
|
});
|
|
19
19
|
const text = await res.text();
|
|
20
20
|
try {
|
|
@@ -23,13 +23,13 @@ import{AuthRequiredError as d,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,n
|
|
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(${
|
|
28
|
+
const body = new URLSearchParams(${o});
|
|
29
29
|
const res = await fetch(${s}, {
|
|
30
30
|
method: 'POST',
|
|
31
31
|
credentials: 'include',
|
|
32
|
-
headers: ${
|
|
32
|
+
headers: ${i},
|
|
33
33
|
body: body.toString(),
|
|
34
34
|
});
|
|
35
35
|
const text = await res.text();
|
|
@@ -39,14 +39,14 @@ import{AuthRequiredError as d,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
39
39
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
`)}export async function postMultipart(t,n
|
|
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(`
|
|
43
43
|
async () => {
|
|
44
44
|
const fd = new FormData();
|
|
45
|
-
for (const [k, v] of Object.entries(${
|
|
45
|
+
for (const [k, v] of Object.entries(${o})) fd.append(k, v);
|
|
46
46
|
const res = await fetch(${s}, {
|
|
47
47
|
method: 'POST',
|
|
48
48
|
credentials: 'include',
|
|
49
|
-
headers: ${
|
|
49
|
+
headers: ${i},
|
|
50
50
|
body: fd,
|
|
51
51
|
});
|
|
52
52
|
const text = await res.text();
|
|
@@ -56,11 +56,11 @@ import{AuthRequiredError as d,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
56
56
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
`)}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(`
|
|
60
60
|
async () => {
|
|
61
61
|
const link = document.querySelector('a[href*="/users/"]');
|
|
62
62
|
const href = link ? (link.getAttribute('href') || '') : '';
|
|
63
63
|
const match = href.match(/\\/users\\/(\\d+)/);
|
|
64
64
|
return match ? match[1] : '';
|
|
65
65
|
}
|
|
66
|
-
`);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,"&")}
|
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
1
|
+
import{ArgumentError as u,AuthRequiredError as g,CommandExecutionError as t}from"@jackwener/opencli/errors";import{cli as h,Strategy as p}from"@jackwener/opencli/registry";import{assertLiteralContent as q}from"../_shared/content-guard.js";import{tagEditorJs as y,readEditorTextJs as A,insertEditorText as S,clickSubmitJs as b,requireExecute as L,resolveTextInput as R}from"./shared.js";const E=20,B=15,v=["answer","add answer","回答","写回答","répondre","responder","antworten","回答する"],U=["\\bpost\\b","submit","发布","提交","publier","publicar","veröffentlichen","投稿"],x=["\\bpost\\b","done","save","continue","confirm","确定","完成","保存","继续"];export function probeAndOpenJs(e=!0){return`(() => {
|
|
2
2
|
const path = location.pathname || '';
|
|
3
3
|
if (/\\blogin\\b/i.test(path + (location.search || ''))) return 'login';
|
|
4
4
|
const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
|
|
5
5
|
// 编辑器已开着?
|
|
6
6
|
const scope = document.querySelector('[role="dialog"]') || document;
|
|
7
|
-
const editor = Array.from(scope.querySelectorAll('[contenteditable="true"]'))
|
|
7
|
+
const editor = Array.from(scope.querySelectorAll('[contenteditable="true"][data-kind="doc"]'))
|
|
8
8
|
.filter(visible)
|
|
9
9
|
.find((el) => (el.getBoundingClientRect().height || 0) >= 24);
|
|
10
10
|
if (editor) return 'editor';
|
|
11
|
+
if (!${JSON.stringify(e)}) return 'wait';
|
|
11
12
|
// 否则找「Answer」入口按钮点开
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
.
|
|
13
|
+
// 顶部 /answer 导航 aria-label 含 "questions to answer",绝不能作为回答入口。
|
|
14
|
+
const re = new RegExp(${JSON.stringify("^(?:"+v.join("|")+")(?:\\s*[·•]\\s*[\\d.,Kk万]+)?$")}, 'i');
|
|
15
|
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'))
|
|
16
|
+
.filter(el => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true')
|
|
17
|
+
.filter(el => re.test((el.getAttribute('aria-label') || el.textContent || '').replace(/\\s+/g, ' ').trim()));
|
|
18
|
+
if (buttons.length > 1) return 'ambiguous';
|
|
19
|
+
const btn = buttons[0];
|
|
16
20
|
if (btn) { btn.click(); return 'clicked'; }
|
|
17
21
|
return 'wait';
|
|
18
|
-
})()`}
|
|
22
|
+
})()`}h({site:"quora",name:"create-answer",access:"write",description:"Publish an answer to a Quora question from your logged-in account. Pass the target question URL and the answer body (multi-line allowed). Write a genuine, substantive answer — Quora aggressively flags low-effort/automated content.",domain:"quora.com",strategy:p.UI,browser:!0,args:[{name:"questionUrl",type:"string",required:!0,positional:!0,help:"Target Quora question URL (https://www.quora.com/<question-slug>)"},{name:"content",type:"string",positional:!0,help:"Answer body text, or use --file for long content"},{name:"file",type:"string",help:"Read answer body from a local UTF-8 text file"},{name:"execute",type:"boolean",help:"Actually post the answer"}],columns:["status","questionUrl","message"],func:async(e,n)=>{if(!e)throw new t("Browser session required for quora create-answer");L(n);const r=String(n.questionUrl??"").trim();if(!r)throw new u("Question URL is required.");let c;try{c=new URL(r)}catch{throw new u(`Invalid question URL: ${r}`)}if(!/(^|\.)quora\.com$/.test(c.hostname))throw new u("URL must be a quora.com question URL.");const a=await R(n,{field:"content",label:"Answer content"});if(!n.file)q(a,{label:"回答正文",fileFlag:"--file"});await e.goto(r,{waitUntil:"none"});let w=!1,d=!1;for(let o=0;o<E;o++){const i=await e.evaluate(probeAndOpenJs(!d));if(i==="login")throw new g("www.quora.com","Quora 重定向到登录页——未登录");if(i==="editor"){w=!0;break}if(i==="ambiguous")throw new t("当前页面存在多个回答入口,无法唯一定位目标问题的 Answer 按钮。");if(i==="clicked")d=!0;await e.wait(1)}if(!w)throw new t("未能打开回答编辑器——问题可能不可回答,或页面结构已变(需真机 recon 收紧锚点)。");if(await e.evaluate(y())!=="ready")throw new t("回答编辑器打标失败——未定位到可编辑正文区。");const f=await S(e,a);if(!f?.ok)throw new t(f?.message??"无法把正文写入 Quora 回答编辑器。");const m=await e.evaluate(b(U));if(!m?.ok)throw new t(m?.message??"未找到 Quora 回答提交按钮。");await e.wait(2);if((await e.evaluate(`(() => {
|
|
19
23
|
const dlg = document.querySelector('[role="dialog"]');
|
|
20
24
|
if (!dlg || (dlg.offsetParent === null && dlg.getClientRects().length === 0)) return { present: false };
|
|
21
25
|
return { present: true };
|
|
22
|
-
})()`))?.present){await e.evaluate(
|
|
26
|
+
})()`))?.present){await e.evaluate(b(x));await e.wait(2)}const s=(o)=>String(o||"").replace(/\s+/g," ").trim(),l=await e.evaluate(A());if(l!=null&&s(l).length>0&&s(l)===s(a))throw new t("Quora 回答提交后正文仍在编辑器中——提交被拦(限流/风控/无权限,或 credential 弹窗未确认)。回查后再决定是否重试。");return[{status:"success",questionUrl:r,message:"Answer posted."}]}});
|
package/clis/reddit/comment.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
1
|
+
import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as o,Strategy as a}from"@jackwener/opencli/registry";o({site:"reddit",name:"comment",access:"write",description:"Post a comment on a Reddit post",domain:"reddit.com",strategy:a.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Comment text"}],columns:["status","message","comment_id","url"],func:async(t,r)=>{if(!t)throw new s("Browser session required");await t.goto("https://www.reddit.com");const e=await t.evaluate(`(async () => {
|
|
2
2
|
try {
|
|
3
|
-
let postId = ${JSON.stringify(
|
|
3
|
+
let postId = ${JSON.stringify(r["post-id"])};
|
|
4
4
|
const urlMatch = postId.match(/comments\\/([a-z0-9]+)/);
|
|
5
5
|
if (urlMatch) postId = urlMatch[1];
|
|
6
6
|
const fullname = postId.startsWith('t3_') || postId.startsWith('t1_')
|
|
7
7
|
? postId : 't3_' + postId;
|
|
8
8
|
|
|
9
|
-
const text = ${JSON.stringify(
|
|
9
|
+
const text = ${JSON.stringify(r.text)};
|
|
10
10
|
|
|
11
11
|
// Get modhash
|
|
12
12
|
const meRes = await fetch('/api/me.json', { credentials: 'include' });
|
|
@@ -23,14 +23,35 @@ import{CommandExecutionError as o}from"@jackwener/opencli/errors";import{cli as
|
|
|
23
23
|
+ (modhash ? '&uh=' + encodeURIComponent(modhash) : ''),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const raw = await res.text();
|
|
27
|
+
let data;
|
|
28
|
+
try { data = JSON.parse(raw); } catch {
|
|
29
|
+
const doc = new DOMParser().parseFromString(raw, 'text/html');
|
|
30
|
+
doc.querySelectorAll('script,style').forEach(el => el.remove());
|
|
31
|
+
const detail = doc.body.textContent.replace(/\\s+/g, ' ').trim().slice(0, 500);
|
|
32
|
+
return { ok: false, message: 'HTTP ' + res.status + ': non-JSON response: ' + detail };
|
|
33
|
+
}
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const detail = data?.json?.errors?.map(e => e.join(': ')).join('; ') || data?.message || data?.reason || data?.error || 'Reddit rejected request';
|
|
36
|
+
return { ok: false, message: 'HTTP ' + res.status + ': ' + String(detail).slice(0, 500) };
|
|
37
|
+
}
|
|
28
38
|
const errors = data?.json?.errors;
|
|
29
39
|
if (errors && errors.length > 0) {
|
|
30
40
|
return { ok: false, message: errors.map(e => e.join(': ')).join('; ') };
|
|
31
41
|
}
|
|
32
|
-
|
|
42
|
+
const things = data?.json?.data?.things;
|
|
43
|
+
const created = Array.isArray(things) ? things.find(t => t?.kind === 't1')?.data : null;
|
|
44
|
+
const commentId = created?.id || String(created?.name || '').replace(/^t1_/, '');
|
|
45
|
+
if (!/^[a-z0-9]+$/i.test(commentId || '')) {
|
|
46
|
+
return { ok: false, message: 'Reddit response did not include a created comment id; publication is unconfirmed' };
|
|
47
|
+
}
|
|
48
|
+
let permalink = '';
|
|
49
|
+
if (created.permalink) {
|
|
50
|
+
const parsed = new URL(created.permalink, 'https://www.reddit.com');
|
|
51
|
+
if (parsed.protocol === 'https:' && (parsed.hostname === 'reddit.com' || parsed.hostname.endsWith('.reddit.com'))) permalink = parsed.href;
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, notice: 'Comment submitted on ' + fullname + '; public visibility not verified', commentId, permalink };
|
|
33
54
|
} catch (e) {
|
|
34
55
|
return { ok: false, message: e.toString() };
|
|
35
56
|
}
|
|
36
|
-
})()`);return[{status:
|
|
57
|
+
})()`);if(!e?.ok)throw new s(e?.message||"Reddit comment returned no result","403 时停止写入并检查平台提示;断连或回执不完整时,先用 user-comments 或 read 回查,禁止直接重复评论。");return[{status:"success",message:e.notice,comment_id:e.commentId,url:e.permalink}]}});
|
package/clis/reddit/upvote.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as n,Strategy as i}from"@jackwener/opencli/registry";n({site:"reddit",name:"upvote",access:"write",description:"Upvote or downvote a Reddit post",domain:"reddit.com",strategy:i.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"direction",type:"string",default:"up",help:"Vote direction: up, down, none"}],columns:["status","message"],func:async(e,
|
|
1
|
+
import{ArgumentError as a,CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as n,Strategy as i}from"@jackwener/opencli/registry";n({site:"reddit",name:"upvote",access:"write",description:"Upvote or downvote a Reddit post",domain:"reddit.com",strategy:i.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"direction",type:"string",default:"up",help:"Vote direction: up, down, none"}],columns:["status","message"],func:async(e,o)=>{const r=o.direction??"up";if(!["up","down","none"].includes(r))throw new a("--direction must be up, down, or none");if(!e)throw new s("Browser session required");await e.goto("https://www.reddit.com");const t=await e.evaluate(`(async () => {
|
|
2
2
|
try {
|
|
3
|
-
let postId = ${JSON.stringify(
|
|
3
|
+
let postId = ${JSON.stringify(o["post-id"])};
|
|
4
4
|
// Extract ID from URL if needed
|
|
5
5
|
const urlMatch = postId.match(/comments\\/([a-z0-9]+)/);
|
|
6
6
|
if (urlMatch) postId = urlMatch[1];
|
|
@@ -8,7 +8,7 @@ import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as
|
|
|
8
8
|
const fullname = postId.startsWith('t3_') || postId.startsWith('t1_')
|
|
9
9
|
? postId : 't3_' + postId;
|
|
10
10
|
|
|
11
|
-
const dir = ${JSON.stringify(
|
|
11
|
+
const dir = ${JSON.stringify(r)};
|
|
12
12
|
const direction = dir === 'down' ? -1 : dir === 'none' ? 0 : 1;
|
|
13
13
|
|
|
14
14
|
// Get modhash from Reddit config
|
|
@@ -33,11 +33,27 @@ import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as
|
|
|
33
33
|
+ (modhash ? '&uh=' + encodeURIComponent(modhash) : ''),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const raw = await res.text();
|
|
37
|
+
let data;
|
|
38
|
+
try { data = JSON.parse(raw); } catch {
|
|
39
|
+
const doc = new DOMParser().parseFromString(raw, 'text/html');
|
|
40
|
+
doc.querySelectorAll('script,style').forEach(el => el.remove());
|
|
41
|
+
const detail = doc.body.textContent.replace(/\\s+/g, ' ').trim().slice(0, 500);
|
|
42
|
+
return { ok: false, message: 'HTTP ' + res.status + ': non-JSON response: ' + detail };
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const detail = data?.json?.errors?.map(e => e.join(': ')).join('; ') || data?.message || data?.reason || data?.error || 'Reddit rejected request';
|
|
46
|
+
return { ok: false, message: 'HTTP ' + res.status + ': ' + String(detail).slice(0, 500) };
|
|
47
|
+
}
|
|
48
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return { ok: false, message: 'Reddit vote returned a malformed response' };
|
|
49
|
+
if (data?.json?.errors?.length) {
|
|
50
|
+
return { ok: false, message: data.json.errors.map(e => e.join(': ')).join('; ') };
|
|
51
|
+
}
|
|
52
|
+
if (data?.error) return { ok: false, message: String(data.message || data.error).slice(0, 500) };
|
|
37
53
|
|
|
38
54
|
const labels = { '1': 'Upvoted', '-1': 'Downvoted', '0': 'Vote removed' };
|
|
39
55
|
return { ok: true, message: (labels[String(direction)] || 'Voted') + ' ' + fullname };
|
|
40
56
|
} catch (e) {
|
|
41
57
|
return { ok: false, message: e.toString() };
|
|
42
58
|
}
|
|
43
|
-
})()`);return[{status:
|
|
59
|
+
})()`);if(!t?.ok)throw new s(t?.message||"Reddit vote returned no result","403 时停止写入并检查平台提示;网络中断时先回查当前投票状态,不要循环重试。");return[{status:"success",message:t.message}]}});
|
|
@@ -537,7 +537,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
537
537
|
const dateText = (card.querySelector('.time_status, .time')?.innerText || '').trim();
|
|
538
538
|
return { id, title, dateText };
|
|
539
539
|
}).filter((entry) => entry.title);
|
|
540
|
-
})()`))}catch{return null}if(Array.isArray(i)){const a=i.find((s)=>{const d=String(s.title||"").trim();if(!(d===o||d.startsWith(o)||o.startsWith(d)))return!1;const c=String(s.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!c)return!0;const m=Date.UTC(Number(c[1]),Number(c[2])-1,Number(c[3]),Number(c[4]),Number(c[5]))-28800000;return Math.abs(Date.now()-m)<=93600000});if(a)return a}}return null}mt({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:pt.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${G} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${S}。可选: ${X.map(([t,e])=>`${t}(${e})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号。平台话题库匹配不上的会自动跳过并在结果中提示,不影响发布"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail","url"],func:async(t,e)=>{if(!t)throw Error("Browser page required");const n=String(e.title??"").trim(),o=e.file?String(e.file).trim():"";let r=String(e.content??"").trim();if(o){if(r)throw new b("正文位置参数和 --file 只能二选一,不能同时给");const l=M.resolve(o);if(!D.existsSync(l))throw new b(`--file 指向的文件不存在: ${l}`);r=D.readFileSync(l,"utf-8").trim();if(!r)throw new b(`--file 指向的文件内容为空: ${l}`)}const i=e.images?String(e.images).split(",").map((l)=>l.trim()).filter(Boolean):[],a=e.topics?String(e.topics).split(",").map((l)=>l.trim()).filter(Boolean):[],s=Boolean(e.draft),d=e["card-text"]?String(e["card-text"]):"",c=d?d.split(G).map((l)=>l.trim()).filter(Boolean):[],m=e["card-style"]?String(e["card-style"]).trim():"",w=c.length>0;if(!n)throw new b("--title is required");if(n.length>z)throw new b(`Title is ${n.length} chars — must be ≤ ${z}`);if(!r)throw new b("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");wt(r,{fileFlag:"--file"});if(!w&&i.length===0)throw new b("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(i.length>V)throw new b(`Too many images: ${i.length} (max ${V})`);if(w&&i.some((l)=>M.extname(l).toLowerCase()===".gif"))throw new b("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const f=Tt(i);await t.goto(yt);await t.wait({time:1});await t.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let k="";for(let l=0;l<30;l++){await t.wait({time:0.5});k=await t.evaluate("() => location.href");if(k.includes("creator.xiaohongshu.com"))break}if(!k.includes("creator.xiaohongshu.com")){await t.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${k}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const E=await Nt(t);if((await Ft(t,E?.ok?5000:2000)).state==="video_surface"){await t.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const l=E?.ok?`clicked "${E.text}"`:`visible candidates: ${(E?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${l}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const A=await jt(t);let C=m;if(w)C=await Jt(t,c,m);else{const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片注入失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await et(t);if(A.length){await ct(t,A);await t.wait({time:0.5})}const N=await at(t);if(N&&typeof N.count==="number"&&N.count>f.length){await t.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new u(`清掉恢复态旧图后编辑器内仍有 ${N.count} 张图,超过本次注入的 ${f.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await Ut(t)){await t.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new u("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(w)await st(t,c.length,"文字配图 generated images");if(w&&f.length>0){const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await et(t);await st(t,c.length+f.length,"文字配图 appended images")}if(w&&A.length){await ct(t,A);await t.wait({time:0.5})}await nt(t,F,n,"title");await v(t,0.6);await nt(t,Q,r,"content");await v(t,0.6);let R=[],I=[];if(a.length){const l=await Ct(t,Q,a);R=l.added;I=l.failed}await v(t,1,0.6);const O=await xt(t,["xhs-publish-btn"]);if(O){await gt(t,O.x,O.y);await v(t,0.4)}const U=s?["暂存离开","存草稿"]:["发布","发布笔记"],p=await t.evaluate(`
|
|
540
|
+
})()`))}catch{return null}if(Array.isArray(i)){const a=i.find((s)=>{const d=String(s.title||"").trim();if(!(d===o||d.startsWith(o)||o.startsWith(d)))return!1;const c=String(s.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!c)return!0;const m=Date.UTC(Number(c[1]),Number(c[2])-1,Number(c[3]),Number(c[4]),Number(c[5]))-28800000;return Math.abs(Date.now()-m)<=93600000});if(a)return a}}return null}mt({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:pt.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${G} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${S}。可选: ${X.map(([t,e])=>`${t}(${e})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号。平台话题库匹配不上的会自动跳过并在结果中提示,不影响发布"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"},{name:"timeout",type:"int",default:300,help:"整段发布流程的等待预算(秒);超时后先用 creator-notes 回查,勿直接重复发布"}],columns:["status","detail","url"],func:async(t,e)=>{if(!t)throw Error("Browser page required");const n=String(e.title??"").trim(),o=e.file?String(e.file).trim():"";let r=String(e.content??"").trim();if(o){if(r)throw new b("正文位置参数和 --file 只能二选一,不能同时给");const l=M.resolve(o);if(!D.existsSync(l))throw new b(`--file 指向的文件不存在: ${l}`);r=D.readFileSync(l,"utf-8").trim();if(!r)throw new b(`--file 指向的文件内容为空: ${l}`)}const i=e.images?String(e.images).split(",").map((l)=>l.trim()).filter(Boolean):[],a=e.topics?String(e.topics).split(",").map((l)=>l.trim()).filter(Boolean):[],s=Boolean(e.draft),d=e["card-text"]?String(e["card-text"]):"",c=d?d.split(G).map((l)=>l.trim()).filter(Boolean):[],m=e["card-style"]?String(e["card-style"]).trim():"",w=c.length>0;if(!n)throw new b("--title is required");if(n.length>z)throw new b(`Title is ${n.length} chars — must be ≤ ${z}`);if(!r)throw new b("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");wt(r,{fileFlag:"--file"});if(!w&&i.length===0)throw new b("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(i.length>V)throw new b(`Too many images: ${i.length} (max ${V})`);if(w&&i.some((l)=>M.extname(l).toLowerCase()===".gif"))throw new b("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const f=Tt(i);await t.goto(yt);await t.wait({time:1});await t.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let k="";for(let l=0;l<30;l++){await t.wait({time:0.5});k=await t.evaluate("() => location.href");if(k.includes("creator.xiaohongshu.com"))break}if(!k.includes("creator.xiaohongshu.com")){await t.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${k}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const E=await Nt(t);if((await Ft(t,E?.ok?5000:2000)).state==="video_surface"){await t.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const l=E?.ok?`clicked "${E.text}"`:`visible candidates: ${(E?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${l}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const A=await jt(t);let C=m;if(w)C=await Jt(t,c,m);else{const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片注入失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await et(t);if(A.length){await ct(t,A);await t.wait({time:0.5})}const N=await at(t);if(N&&typeof N.count==="number"&&N.count>f.length){await t.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new u(`清掉恢复态旧图后编辑器内仍有 ${N.count} 张图,超过本次注入的 ${f.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await Ut(t)){await t.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new u("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(w)await st(t,c.length,"文字配图 generated images");if(w&&f.length>0){const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await et(t);await st(t,c.length+f.length,"文字配图 appended images")}if(w&&A.length){await ct(t,A);await t.wait({time:0.5})}await nt(t,F,n,"title");await v(t,0.6);await nt(t,Q,r,"content");await v(t,0.6);let R=[],I=[];if(a.length){const l=await Ct(t,Q,a);R=l.added;I=l.failed}await v(t,1,0.6);const O=await xt(t,["xhs-publish-btn"]);if(O){await gt(t,O.x,O.y);await v(t,0.4)}const U=s?["暂存离开","存草稿"]:["发布","发布笔记"],p=await t.evaluate(`
|
|
541
541
|
(cfg => {
|
|
542
542
|
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
543
543
|
const isVisible = (el) => {
|
package/clis/zhihu/comment.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{CliError as s,CommandExecutionError as
|
|
1
|
+
import{CliError as s,CommandExecutionError as m}from"@jackwener/opencli/errors";import{cli as u,Strategy as d}from"@jackwener/opencli/registry";import{assertAllowedKinds as c,parseTarget as l}from"./target.js";import{buildResultRow as p,requireExecute as h,resolveCurrentUserIdentity as w,resolvePayload as y}from"./write-shared.js";u({site:"zhihu",name:"comment",access:"write",description:"Create a top-level comment on a Zhihu answer or article",domain:"zhihu.com",strategy:d.COOKIE,browser:!0,args:[{name:"target",positional:!0,required:!0,help:"Zhihu target URL or typed target"},{name:"text",positional:!0,help:"Comment text"},{name:"file",help:"Comment text file path"},{name:"execute",type:"boolean",help:"Actually perform the write action"}],columns:["status","outcome","message","target_type","target","author_identity","created_url"],func:async(e,r)=>{if(!e)throw new m("Browser session required for zhihu comment");h(r);const o=String(r.target),t=c("comment",l(o)),i=await y(r);await e.goto(t.url);await e.wait(3);const n=await w(e),a=await e.evaluate(`(async () => {
|
|
2
2
|
var targetKind = ${JSON.stringify(t.kind)};
|
|
3
3
|
var targetId = ${JSON.stringify(t.id)};
|
|
4
4
|
var content = ${JSON.stringify(i)};
|
|
@@ -14,4 +14,4 @@ import{CliError as s,CommandExecutionError as u}from"@jackwener/opencli/errors";
|
|
|
14
14
|
if (!resp.ok) return { ok: false, status: resp.status, message: data.error ? data.error.message : 'unknown error' };
|
|
15
15
|
if (!data || !data.id) return { ok: false, status: resp.status, message: 'Comment API response did not include a created comment id' };
|
|
16
16
|
return { ok: true, id: data.id, url: data.url };
|
|
17
|
-
})()`);if(!a?.ok)throw new s("COMMAND_EXEC",a?.message||"Failed to create comment");return p(`Commented on ${t.kind} ${t.id}`,t.kind,o,"created",{author_identity:n,created_url
|
|
17
|
+
})()`);if(!a?.ok)throw new s("COMMAND_EXEC",a?.message||"Failed to create comment");return p(`Commented on ${t.kind} ${t.id}`,t.kind,o,"created",{author_identity:n,created_url:`${t.url}#comment-${a.id}`})}});
|